feat(generation): #2024 视频渲染后延迟到封面确认才入库(新增 AWAITING_COVER 状态 + finalize 接口) #2026
Reference in New Issue
Block a user
Delete Branch "feat/defer-video-finalization-until-cover-confirmed"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
背景
需求来自 #2024:Worker 渲染视频并上传 OSS 后,不要立刻创建 GeneratedVideo 成品记录,而是停留在「待确认封面」中间态;用户在 Step5 选完封面/自动封面、点「完成」时才调用 finalize 接口正式入库。AI 数字人流程独立不受影响。
核心改动
领域模型
GenerationTaskStatus新增AWAITING_COVER(非终态)mark_awaiting_cover():progress=100,清空 error_message,不设completed_atrunning → {awaiting_cover, completed, failed, cancelled};awaiting_cover → {completed, failed, cancelled}_missing_兼容别名:waiting_cover/video_ready/rendered/pending_coverrunning→completed兜底兼容(历史代码/测试)Worker(
apps/worker/worker_app/tasks/generation.py)_record_video_and_dedup→_precompute_render_metadata:调用新的compute_render_fingerprint_and_dedup,返回可序列化 dict(指纹+查重结果),不写任何 DB 记录task.extra_meta["rendered_output"]mark_awaiting_cover代替mark_completed查重工具(
apps/worker/video_processing/dedup_helpers.py)compute_render_fingerprint_and_dedup(video_path, ...) → dict:本地指纹计算 + 历史查重 + 批次查重,返回可序列化结果(含fingerprint_dict、fingerprint_chunks列表、is_duplicate/duplicate_of/duplicate_rate/match_count/visual_similarity/video_fingerprint_md5)create_video_record_and_dedup()保留兼容:支持直接传pre_dedup_result复用预计算结果,避免重复读本地视频VideoFingerprint.from_dict()新增类方法,用于 finalize 阶段从 dict 还原指纹对象应用层(新增
packages/application/generated_video_finalize.py)RenderedOutputdataclass +from_dict():承载 worker 预计算结果finalize_generated_video(task, session, effective_cover_url) → dict:读 meta → 构造GeneratedVideo→ 批量写入VideoFingerprintChunkModel→video_repo.create()→ commit,不依赖任何 worker 模块导入API Service(新增
apps/api/app/services/generation_finalize_service.py)GenerationFinalizeService.finalize_task(task_id, user_id, cover_url=None)awaiting_cover(兼容历史completed数据)API 端点(
apps/api/app/api/routes/generation_tasks.py)POST /api/v1/generation/tasks/{task_id}/finalize{ "cover_url": "..." }(可选;不传则用 task.cover_url){ video_id, cover_url, file_url, status="success", is_duplicate }(file_url 是签名下载链接)confirm_generation:confirm 预览后mark_confirmed不再隐式mark_completed,任务停留在awaiting_cover;历史已 completed 的预览任务回退到 awaiting_coversource_task.is_completed放宽到(completed, awaiting_cover)预览/任务中心兼容
GET /preview/{task_id}:awaiting_cover 状态下从extra_meta.rendered_output构造轻量 _PreviewVideo(file_url/duration/file_size),finalize 前前端即可预览播放awaiting_cover → 等待确认封面;状态筛选加入 awaiting_cover不需要 DB 迁移
GenerationTaskModel.status字段为String(20)无 CHECK 约束,新增枚举值可直接写入。未触碰范围(明确保证)
AiAvatarRenderJob表和ai_avatar_render_service.finalize_job完全独立,未做任何修改ass_subtitle_builder/video_filter_builder/subtitle_generator)未触碰,不回退修复测试
tests/unit/test_finalize_generation.py:13 个用例覆盖状态枚举/属性/转换、mark_awaiting_cover、RenderedOutput 解析、finalize 用例(成功/缺 meta/封面兜底)tests/unit/domain/test_generation_task.py:6 个状态断言前端联调提示
POST /api/v1/generation/tasks/{task_id}/finalizefile_url是签名链接,可直接用于播放/下载;is_duplicate=true时前端可按需提示Two-phase pipeline: worker renders and precomputes dedup metadata, but does not create GeneratedVideo until user confirms cover in Step 5. Domain - Add GenerationTaskStatus.AWAITING_COVER (non-terminal) - Add mark_awaiting_cover(): progress=100, clears error, leaves completed_at unset - Transitions: running → {awaiting_cover, completed, failed, cancelled}; awaiting_cover → {completed, failed, cancelled} - _missing_ aliases: waiting_cover/video_ready/rendered/pending_cover - Keep running→completed for backward compat / legacy paths Worker (apps/worker/worker_app/tasks/generation.py) - Replace _record_video_and_dedup with _precompute_render_metadata: calls compute_render_fingerprint_and_dedup, returns dict without DB writes - Remove runtime batch-rerender decision (should_rerender_for_batch_dedup path); dedup now happens at finalize time against all finished records - Persist precomputed metadata into task.extra_meta['rendered_output'] - Final status: mark_awaiting_cover instead of mark_completed Dedup helpers (apps/worker/video_processing/dedup_helpers.py) - New compute_render_fingerprint_and_dedup(video_path,...): local fingerprint + historical dedup + batch dedup, returns fully serializable dict (fingerprint_dict, fingerprint_chunks list, is_duplicate, duplicate_of, etc.) - create_video_record_and_dedup() retained for compat/tests; now supports pre_dedup_result to reuse worker-precomputed data without re-reading video - VideoFingerprint.from_dict() added to reconstruct from serialized form Application layer (packages/application/generated_video_finalize.py) - RenderedOutput dataclass + from_dict() for deserializing worker output - finalize_generated_video(): creates GeneratedVideo, bulk-inserts VideoFingerprintChunk rows, commits; API-layer free API service (apps/api/app/services/generation_finalize_service.py) - GenerationFinalizeService.finalize_task(task_id, user_id, cover_url): * permission / existence check * idempotent: if GeneratedVideo already exists for this task, just ensure task is marked completed and return (safe for double-click) * status gate: only awaiting_cover (or legacy completed) accepted * cover resolution: explicit cover_url arg > task.cover_url * delegates to finalize_generated_video, marks task completed, clears extra_meta['rendered_output'] API endpoint (apps/api/app/api/routes/generation_tasks.py) - POST /api/v1/generation/tasks/{task_id}/finalize - Body: { cover_url?: string }; returns video_id/cover_url/file_url/is_duplicate - Adjust confirm_generation fast path: mark_confirmed keeps task in awaiting_cover (don't auto-complete); historical 'completed' previews migrated back to awaiting_cover - Preview-reuse accepts awaiting_cover tasks Preview / task center - GET /preview/{task_id} constructs lightweight _PreviewVideo from extra_meta.rendered_output when task is awaiting_cover - Task center step map: awaiting_cover → 等待确认封面; status filter includes it Tests - tests/unit/test_finalize_generation.py: 13 new tests covering status transitions, mark_awaiting_cover, RenderedOutput parsing, finalize use case (success / missing metadata / cover fallback) - test_generation_task.py updated for 6th status value - Full suite: 15978 passed, 28 skipped (matches #2023 baseline, no regressions) - black/isort/ruff clean Out of scope (intentionally untouched) - AI avatar pipeline uses separate AiAvatarRenderJob; finalize_job and /{job_id}/finalize are unchanged - PR #2023 subtitle font scaling files (ass_subtitle_builder, video_filter_builder, subtitle_generator) not modified - No DB migration: GenerationTaskModel.status is String(20) without CHECK - Temp file cleanup: leave to existing periodic job🚀 预览环境已部署
🗑️ 预览环境已清理
PR #2026 已关闭或合并,对应的预览环境已被清理。