ea9e536740
方案A: 后端扩展3处API改动,支持前端模板中心化模型。
1. POST /api/v1/generation/tasks — project_id改为可选,新增template_id/asset_ids/title_ids/voice_ids
2. GET /api/v1/generation/tasks + GET /api/v1/tasks — 新增用户级列表(跨project)
3. POST /api/v1/tasks/{task_id}/retry + POST /api/v1/generation/tasks/{task_id}/retry — 简化重试
改动涉及:
- Domain: GenerationTask新增6个字段,放宽create()校验
- Application: CreateGenerationTaskCommand新增字段
- Ports: GenerationTaskRepository新增list_by_user()
- Adapter: SQLAlchemy模型+仓储实现新字段和list_by_user
- Schema: 双模式校验(project模式/模板模式)
- Routes: generation_tasks + task_center双路由注册
- Migration: 015_add_generation_task_extensions
向后兼容:所有旧端点和参数不变。
239 lines
9.3 KiB
Python
239 lines
9.3 KiB
Python
from typing import Any
|
|
|
|
from app.auth import AuthenticatedUser, get_current_user
|
|
from app.core.celery_app import celery_app
|
|
from app.dependencies import (
|
|
get_asset_library_repository,
|
|
get_asset_repository,
|
|
get_generated_video_repository,
|
|
get_generation_task_repository,
|
|
get_project_repository,
|
|
)
|
|
from app.schemas.generated_video import (
|
|
GeneratedVideoResponse,
|
|
ListGeneratedVideosResponse,
|
|
)
|
|
from app.schemas.generation_task import (
|
|
CreateGenerationTaskRequest,
|
|
GenerationTaskResponse,
|
|
ListGenerationTasksResponse,
|
|
)
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
from packages.application import (
|
|
CreateGenerationTaskCommand,
|
|
CreateGenerationTaskUseCase,
|
|
GetGenerationTaskUseCase,
|
|
ListGeneratedVideosByTaskUseCase,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _check_project_access(project_id: str, user_id: str, project_repository) -> None:
|
|
"""检查用户是否有项目访问权限"""
|
|
project = project_repository.find_by_id(project_id)
|
|
if project is None:
|
|
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
|
|
if not project.can_access(user_id):
|
|
raise HTTPException(status_code=403, detail="Access denied to project")
|
|
|
|
|
|
def _to_generation_task_response(task) -> GenerationTaskResponse:
|
|
return GenerationTaskResponse(
|
|
id=task.id,
|
|
project_id=task.project_id,
|
|
asset_library_id=task.asset_library_id,
|
|
strategy_id=task.strategy_id,
|
|
voice_library_id=task.voice_library_id,
|
|
template_id=task.template_id,
|
|
asset_ids=task.asset_ids,
|
|
title_ids=task.title_ids,
|
|
voice_ids=task.voice_ids,
|
|
status=task.status,
|
|
progress=task.progress,
|
|
result_count=task.result_count,
|
|
error_message=task.error_message,
|
|
)
|
|
|
|
|
|
def _to_generated_video_response(item) -> GeneratedVideoResponse:
|
|
return GeneratedVideoResponse(
|
|
id=item.id,
|
|
project_id=item.project_id,
|
|
generation_task_id=item.generation_task_id,
|
|
name=item.name,
|
|
file_url=item.file_url,
|
|
file_size=item.file_size,
|
|
duration=item.duration,
|
|
thumbnail_url=item.thumbnail_url,
|
|
width=item.width,
|
|
height=item.height,
|
|
fps=item.fps,
|
|
)
|
|
|
|
|
|
def _ensure_library_has_ready_video_assets(assets) -> None:
|
|
ready_video_assets = [
|
|
asset for asset in assets if asset.status.value == "ready" and asset.mime_type.startswith("video")
|
|
]
|
|
if not ready_video_assets:
|
|
raise HTTPException(
|
|
status_code=422,
|
|
detail="当前素材库没有 ready 状态的视频素材,请先上传并等待导入完成后再生成。",
|
|
)
|
|
|
|
|
|
async def _resolve_project_and_library(
|
|
request: CreateGenerationTaskRequest,
|
|
project_repository: Any,
|
|
asset_library_repository: Any,
|
|
asset_repository: Any,
|
|
authenticated_user: AuthenticatedUser,
|
|
) -> tuple[str, str]:
|
|
"""解析 project_id 和 asset_library_id。
|
|
|
|
支持两种模式:
|
|
- 显式传入(向后兼容)
|
|
- 从 asset_ids 反查 asset_library(模板模式)
|
|
返回 (project_id, asset_library_id)。
|
|
"""
|
|
project_id = request.project_id.strip()
|
|
asset_library_id = request.asset_library_id.strip()
|
|
|
|
# 模板模式:project_id 未提供时,从 asset_ids 反查所属 project
|
|
if not project_id and request.asset_ids:
|
|
first_asset_id = request.asset_ids[0]
|
|
asset = await asset_repository.find_by_id(first_asset_id)
|
|
if asset is not None:
|
|
project_id = asset.project_id
|
|
if not asset_library_id:
|
|
asset_library_id = asset.library_id
|
|
|
|
# 向后兼容校验:project_id 已提供时验证权限
|
|
if project_id:
|
|
project = project_repository.find_by_id(project_id)
|
|
if project is None:
|
|
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
|
|
if not project.can_access(authenticated_user.user.id):
|
|
raise HTTPException(status_code=403, detail="Access denied to project")
|
|
|
|
return project_id, asset_library_id
|
|
|
|
|
|
@router.post("/tasks", response_model=GenerationTaskResponse)
|
|
async def create_generation_task(
|
|
request: CreateGenerationTaskRequest,
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
generation_task_repository: Any = Depends(get_generation_task_repository),
|
|
project_repository: Any = Depends(get_project_repository),
|
|
asset_library_repository: Any = Depends(get_asset_library_repository),
|
|
asset_repository: Any = Depends(get_asset_repository),
|
|
) -> GenerationTaskResponse:
|
|
project_id, asset_library_id = await _resolve_project_and_library(
|
|
request, project_repository, asset_library_repository, asset_repository, authenticated_user
|
|
)
|
|
|
|
# asset_library 存在性校验(仅在提供了 asset_library_id 时)
|
|
if asset_library_id:
|
|
library = asset_library_repository.get(asset_library_id)
|
|
if library is None or (project_id and library.project_id != project_id):
|
|
raise HTTPException(status_code=404, detail=f"AssetLibrary {asset_library_id} not found")
|
|
|
|
assets = await asset_repository.find_by_library(asset_library_id)
|
|
_ensure_library_has_ready_video_assets(assets)
|
|
|
|
use_case = CreateGenerationTaskUseCase(generation_task_repository)
|
|
task = use_case.execute(
|
|
CreateGenerationTaskCommand(
|
|
project_id=project_id,
|
|
asset_library_id=asset_library_id,
|
|
strategy_id=request.strategy_id,
|
|
voice_library_id=request.voice_library_id,
|
|
template_id=request.template_id,
|
|
asset_ids=request.asset_ids,
|
|
title_ids=request.title_ids,
|
|
voice_ids=request.voice_ids,
|
|
created_by_user_id=authenticated_user.user.id,
|
|
)
|
|
)
|
|
celery_app.send_task("worker.generate_video", args=[task.id])
|
|
return _to_generation_task_response(task)
|
|
|
|
|
|
@router.get("/tasks", response_model=ListGenerationTasksResponse)
|
|
def list_generation_tasks(
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
generation_task_repository: Any = Depends(get_generation_task_repository),
|
|
) -> ListGenerationTasksResponse:
|
|
"""用户级生成任务列表(跨 project)。"""
|
|
tasks = generation_task_repository.list_by_user(authenticated_user.user.id)
|
|
items = [_to_generation_task_response(task) for task in tasks]
|
|
return ListGenerationTasksResponse(items=items)
|
|
|
|
|
|
@router.get("/tasks/{task_id}", response_model=GenerationTaskResponse)
|
|
def get_generation_task(
|
|
task_id: str,
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
generation_task_repository: Any = Depends(get_generation_task_repository),
|
|
project_repository: Any = Depends(get_project_repository),
|
|
) -> GenerationTaskResponse:
|
|
use_case = GetGenerationTaskUseCase(generation_task_repository)
|
|
task = use_case.execute(task_id)
|
|
if task is None:
|
|
raise HTTPException(status_code=404, detail=f"GenerationTask {task_id} not found")
|
|
if task.project_id:
|
|
_check_project_access(task.project_id, authenticated_user.user.id, project_repository)
|
|
return _to_generation_task_response(task)
|
|
|
|
|
|
@router.get("/tasks/{task_id}/results", response_model=ListGeneratedVideosResponse)
|
|
def list_generation_results(
|
|
task_id: str,
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
generation_task_repository: Any = Depends(get_generation_task_repository),
|
|
generated_video_repository: Any = Depends(get_generated_video_repository),
|
|
project_repository: Any = Depends(get_project_repository),
|
|
) -> ListGeneratedVideosResponse:
|
|
task = generation_task_repository.get(task_id)
|
|
if task is None:
|
|
raise HTTPException(status_code=404, detail=f"GenerationTask {task_id} not found")
|
|
if task.project_id:
|
|
_check_project_access(task.project_id, authenticated_user.user.id, project_repository)
|
|
use_case = ListGeneratedVideosByTaskUseCase(generated_video_repository)
|
|
items = use_case.execute(task_id)
|
|
return ListGeneratedVideosResponse(items=[_to_generated_video_response(item) for item in items])
|
|
|
|
|
|
@router.post("/tasks/{task_id}/retry", response_model=GenerationTaskResponse)
|
|
def retry_generation_task(
|
|
task_id: str,
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
generation_task_repository: Any = Depends(get_generation_task_repository),
|
|
) -> GenerationTaskResponse:
|
|
"""简化重试:通过 task_id 直接重试失败任务。"""
|
|
task = generation_task_repository.get(task_id)
|
|
if task is None:
|
|
raise HTTPException(status_code=404, detail="Generation task not found")
|
|
if task.status != "failed":
|
|
raise HTTPException(status_code=409, detail="Only failed tasks can be retried")
|
|
|
|
use_case = CreateGenerationTaskUseCase(generation_task_repository)
|
|
retried = use_case.execute(
|
|
CreateGenerationTaskCommand(
|
|
project_id=task.project_id,
|
|
asset_library_id=task.asset_library_id,
|
|
strategy_id=task.strategy_id,
|
|
voice_library_id=task.voice_library_id,
|
|
edit_plan_id=task.edit_plan_id,
|
|
template_id=task.template_id,
|
|
asset_ids=task.asset_ids,
|
|
title_ids=task.title_ids,
|
|
voice_ids=task.voice_ids,
|
|
created_by_user_id=authenticated_user.user.id,
|
|
)
|
|
)
|
|
celery_app.send_task("worker.generate_video", args=[retried.id])
|
|
return _to_generation_task_response(retried)
|