Files
xiaoxia-saas/apps/api/app/api/routes/generation_tasks.py
T
灵应 356df4663e
Deploy / Staging E2E Tests (push) Has been cancelled
Deploy / Deploy Staging (push) Failing after 111h7m48s
CI/CD Pipeline / Frontend Lint (push) Failing after 111h7m57s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 111h7m57s
Deploy / Production Browser E2E (push) Failing after 1769h43m24s
Deploy / Deploy Production (push) Failing after 1769h43m26s
Deploy / Build Production Runtime Images (push) Failing after 1769h43m28s
fix: 修复素材库诊断/缩略图/视频URL + 剪辑计划错误处理
Task #105: 修复剪辑计划/模板页面报服务器繁忙
- templates.py: list_templates/get_template/list_categories 加 try/except
- templates.py: 新增 POST /{template_id}/toggle-favorite 兼容端点
- edit_plans.py: list_plans 加 try/except,ai_tasks 加 ImportError 守卫

Task #106: 修复素材库诊断按钮 + 缩略图/视频URL
- asset_diagnosis.py: get_project_asset_diagnosis 加 try/except
- assets.py: 注入 storage_service,生成签名 file_url
- asset.py schema: 新增 file_url 字段
- 视频素材 thumbnail_url 为空时复用 file_url 作为封面

其他:
- edit_plans/generation_tasks 支持 source_edit_plan_id
- Alembic 迁移 022: 两表加 source_edit_plan_id 字段
2026-07-04 21:46:32 +08:00

244 lines
9.7 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,
source_edit_plan_id=task.source_edit_plan_id or "",
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 状态的视频素材,请先上传并等待导入完成后再生成。",
)
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 = 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)
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 = _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 = 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,
source_edit_plan_id=request.source_edit_plan_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.created_by_user_id and task.created_by_user_id != authenticated_user.user.id:
raise HTTPException(status_code=403, detail="Access denied to this task")
status_val = task.status.value if hasattr(task.status, "value") else str(task.status)
if status_val != "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,
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,
source_edit_plan_id=task.source_edit_plan_id or "",
)
)
celery_app.send_task("worker.generate_video", args=[retried.id])
return _to_generation_task_response(retried)