"""视频生成任务 finalize 服务(#2024)。 Worker 渲染+上传完成后不再自动入库,标记为 awaiting_cover;用户在 Step5 选好封面 点「完成」时由 API 调用本服务:创建 GeneratedVideo 成品库记录(复用 worker 预计算 的查重结果)、绑定封面、推进任务到 completed。 与 AI 数字人 ``ai_avatar_render_service.finalize_job`` 模式一致, 只是走 GenerationTask 而非 AiAvatarRenderJob。 """ from __future__ import annotations import logging from datetime import UTC, datetime from typing import Optional from sqlalchemy.orm import Session logger = logging.getLogger(__name__) class GenerationFinalizeError(Exception): """finalize 业务错误,code 供 API 层映射 HTTP 状态码。""" def __init__(self, message: str, code: str = "FinalizeError", status_code: int = 400): super().__init__(message) self.code = code self.status_code = status_code class GenerationFinalizeService: def __init__(self, db: Session): self.db = db def finalize_task(self, task_id: str, user_id: str, cover_url: Optional[str] = None): """执行 finalize:状态校验 → 幂等 → 绑定封面 → 入库 → 推进 completed。 Returns: GeneratedVideo 领域对象 """ from packages.adapters.sqlalchemy_impl.generated_video_repository import ( SQLAlchemyGeneratedVideoRepository, ) from packages.adapters.sqlalchemy_impl.generation_task_repository import ( SQLAlchemyGenerationTaskRepository, ) from packages.adapters.sqlalchemy_impl.models import GeneratedVideoModel from packages.application.generated_video_finalize import finalize_generated_video task_repo = SQLAlchemyGenerationTaskRepository(self.db) video_repo = SQLAlchemyGeneratedVideoRepository(self.db) task = task_repo.get(task_id) if task is None: raise GenerationFinalizeError(f"任务 {task_id} 不存在", "TaskNotFound", 404) # ── 幂等:已入库直接返回 ───────────────────────────────── existing = self.db.query(GeneratedVideoModel).filter(GeneratedVideoModel.generation_task_id == task_id).first() if existing is not None: logger.info("[finalize] 幂等命中 task=%s video=%s", task_id, existing.id) if cover_url and cover_url.strip() and existing.thumbnail_url != cover_url.strip(): existing.thumbnail_url = cover_url.strip() task.cover_url = cover_url.strip() self.db.commit() if task.status.value != "completed": try: task.mark_completed(result_count=1) if cover_url and cover_url.strip(): task.cover_url = cover_url.strip() task_repo.update(task) self.db.commit() except Exception as e: logger.warning("[finalize] 幂等补 mark_completed 失败: %s", e) self.db.rollback() return video_repo.get(existing.id) # ── 状态校验 ───────────────────────────────────────────── if task.status.value != "awaiting_cover": raise GenerationFinalizeError( f"任务当前状态 {task.status.value},无法 finalize(需 awaiting_cover)", "InvalidTaskStatus", 400, ) # ── 封面 ───────────────────────────────────────────────── effective_cover = (cover_url or "").strip() if cover_url else (task.cover_url or "").strip() # ── 入库+查重(复用 worker 预计算结果) ────────────────── try: result = finalize_generated_video( task=task, session=self.db, effective_cover_url=effective_cover, ) except ValueError as e: raise GenerationFinalizeError(str(e), "RenderedOutputMissing", 400) from e video_id = result["video_id"] # ── 推进任务 ───────────────────────────────────────────── task.mark_completed(result_count=1) task.cover_url = effective_cover # 清理 rendered_output(体积较大,入库后不再需要) meta = dict(task.extra_meta or {}) meta.pop("rendered_output", None) task.extra_meta = meta task.updated_at = datetime.now(UTC) task_repo.update(task) self.db.commit() video = video_repo.get(video_id) logger.info( "[finalize] task=%s finalized -> video=%s cover=%s dup=%s", task_id, video_id, bool(effective_cover), result.get("is_duplicate", False), ) return video