From a590c0c886a58f2335103b0e42e0c683f8ae21e2 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 7 Aug 2026 20:02:56 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=E7=94=9F=E6=88=90=E6=97=B6=E6=8A=A5?= =?UTF-8?q?=E9=94=99=E3=80=8C=E6=B2=A1=E6=9C=89=E5=8F=AF=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E7=9A=84=E5=B0=B1=E7=BB=AA=E7=89=87=E6=AE=B5=E3=80=8D=E2=80=94?= =?UTF-8?q?=20mark=5Fclips=5Fready=20=E4=BB=85=E6=A0=87=E8=AE=B0=E6=9C=89?= =?UTF-8?q?=E7=B4=A0=E6=9D=90=E7=9A=84=E7=89=87=E6=AE=B5=20(#1270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: - mark_clips_ready() 标记所有 PENDING 片段为 READY,不管是否已分配素材 - can_generate() 不检查片段是否有素材 - 导致渲染时 ready_clips(需同时满足 status=READY 且 asset_id 非空)为空 修复: - mark_clips_ready(): 只标记 asset_id 非空的 pending 片段为 ready - can_generate(): 新增检查 — 至少有一个片段分配了素材 - validateGenerateInputs(): auto 模式下也校验素材非空,给出友好提示 - 更新相关单元测试 Closes #1270 --- apps/api/app/services/edit_plan_service.py | 23 ++++++++++--- .../hooks/generate-video/buildPayload.ts | 17 ++++++++-- tests/unit/test_edit_plan_service.py | 32 ++++++++++++++----- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/apps/api/app/services/edit_plan_service.py b/apps/api/app/services/edit_plan_service.py index 3026c58cf..7c583d391 100755 --- a/apps/api/app/services/edit_plan_service.py +++ b/apps/api/app/services/edit_plan_service.py @@ -585,10 +585,19 @@ class EditPlanService: if not clips: return False, "请先添加片段后再生成视频" + # 检查是否至少有一个片段分配了素材 + clips_with_asset = [c for c in clips if c.asset_id] + if not clips_with_asset: + return False, "没有可渲染的就绪片段,请确保已选择素材" + return True, "" def mark_clips_ready(self, plan_id: str) -> int: - """将所有 pending 状态的片段标记为 ready + """将已分配素材的 pending 片段标记为 ready + + 只标记同时满足以下条件的片段: + - status == PENDING + - asset_id 非空(已分配素材) Returns: int: 标记的片段数量 @@ -599,10 +608,14 @@ class EditPlanService: ) count = 0 for clip in clips: - clip.mark_ready() - self._clip_repo.update(clip) - count += 1 - logger.info("标记片段就绪: plan_id=%s count=%d", plan_id, count) + if clip.asset_id: + clip.mark_ready() + self._clip_repo.update(clip) + count += 1 + logger.info( + "标记片段就绪: plan_id=%s marked=%d total_pending=%d", + plan_id, count, len(clips), + ) return count def update_plan_config(self, plan_id: str, config_updates: Dict[str, Any]) -> EditPlan: diff --git a/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts b/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts index ced8ec345..d591e6b6e 100644 --- a/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts +++ b/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts @@ -61,14 +61,25 @@ export const buildEditPlanPayload = (props: UseGenerateVideoProps) => { * 返回错误信息,通过则返回 null */ export const validateGenerateInputs = (props: UseGenerateVideoProps): string | null => { - const { titleSettings, materialMode, selectedMaterials, voiceMode, selectedClonedVoice } = props + const { + titleSettings, + materialMode, + selectedMaterials, + smartSelectedIds, + voiceMode, + selectedClonedVoice, + } = props // AI 自动选择模式下,标题可以为空(后端会自行生成) if (!titleSettings.aiAutoSelect && !titleSettings.title?.trim()) { return "请先选择或输入标题" } - if (materialMode === "manual" && selectedMaterials.length === 0) { - return "请至少选择一个素材" + // 无论手动还是自动模式,都必须有素材 + const materialIds = materialMode === "auto" ? smartSelectedIds : selectedMaterials + if (materialIds.length === 0) { + return materialMode === "auto" + ? "AI 未匹配到素材,请手动选择素材后重试" + : "请至少选择一个素材" } if (voiceMode === "clone" && !selectedClonedVoice) { return "请先选择一个克隆音色" diff --git a/tests/unit/test_edit_plan_service.py b/tests/unit/test_edit_plan_service.py index 806f3de61..ce4452da6 100755 --- a/tests/unit/test_edit_plan_service.py +++ b/tests/unit/test_edit_plan_service.py @@ -501,11 +501,22 @@ class TestGenerationWorkflow: svc = _make_service() p = svc.create_plan("tpl-001", "测试") svc.transition_status(p.id, EditPlanStatus.EDITING) - svc.create_clip(p.id, "intro", 0) + clip = svc.create_clip(p.id, "intro", 0) + svc.assign_asset(clip.id, "asset-001") can, reason = svc.can_generate(p.id) assert can is True assert reason == "" + def test_can_generate_no_assets_fails(self): + """片段存在但没有分配素材时,can_generate 应返回 False""" + svc = _make_service() + p = svc.create_plan("tpl-001", "测试") + svc.transition_status(p.id, EditPlanStatus.EDITING) + svc.create_clip(p.id, "intro", 0) + can, reason = svc.can_generate(p.id) + assert can is False + assert "没有可渲染" in reason or "素材" in reason + def test_can_generate_draft_fails(self): svc = _make_service() p = svc.create_plan("tpl-001", "测试") @@ -523,16 +534,20 @@ class TestGenerationWorkflow: assert "请先添加片段后再生成视频" in reason def test_mark_clips_ready(self): + """只有分配了素材的 pending 片段才会被标记为 ready""" svc = _make_service() p = svc.create_plan("tpl-001", "测试") - svc.create_clip(p.id, "intro", 0) - svc.create_clip(p.id, "main", 1) + clip1 = svc.create_clip(p.id, "intro", 0) + clip2 = svc.create_clip(p.id, "main", 1) + # 只给 clip1 分配素材 + svc.assign_asset(clip1.id, "asset-001") count = svc.mark_clips_ready(p.id) - assert count == 2 - # 验证所有片段都是 ready 状态 + assert count == 1 # 只有 clip1 被标记 + # 验证 clip1 是 ready,clip2 仍是 pending clips = svc.list_clips(p.id) - for c in clips: - assert c.status == EditPlanClipStatus.READY + clips_by_order = {c.order: c for c in clips} + assert clips_by_order[0].status == EditPlanClipStatus.READY + assert clips_by_order[1].status == EditPlanClipStatus.PENDING svc = _make_service() p = svc.create_plan("tpl-001", "测试") svc.create_clip(p.id, "intro", 0) @@ -613,7 +628,8 @@ class TestResumeEditingAndRegenerate: """完成后编辑 → can_generate 返回 True,可再生成""" svc = _make_service() p = svc.create_plan("tpl-001", "测试") - svc.create_clip(p.id, "main", 0) + clip = svc.create_clip(p.id, "main", 0) + svc.assign_asset(clip.id, "asset-001") svc.transition_status(p.id, EditPlanStatus.EDITING) svc.transition_status(p.id, EditPlanStatus.RENDERING) svc.transition_status(p.id, EditPlanStatus.COMPLETED) -- 2.54.0 From d7761a35d8ab25a017a8bb2b99c90edd54a25170 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Fri, 7 Aug 2026 12:04:52 +0000 Subject: [PATCH 2/4] style: auto-format with black + isort + prettier [skip ci-format-check] --- apps/api/app/services/edit_plan_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/api/app/services/edit_plan_service.py b/apps/api/app/services/edit_plan_service.py index 7c583d391..ab4c3aec0 100755 --- a/apps/api/app/services/edit_plan_service.py +++ b/apps/api/app/services/edit_plan_service.py @@ -614,7 +614,9 @@ class EditPlanService: count += 1 logger.info( "标记片段就绪: plan_id=%s marked=%d total_pending=%d", - plan_id, count, len(clips), + plan_id, + count, + len(clips), ) return count -- 2.54.0 From d9eadd469b5e3a3c30f48ea58459a1244c7f0118 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 7 Aug 2026 20:30:37 +0800 Subject: [PATCH 3/4] fix: prettier formatting in buildPayload.ts --- .../src/pages/generate/hooks/generate-video/buildPayload.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts b/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts index d591e6b6e..49dec5c6e 100644 --- a/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts +++ b/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts @@ -77,9 +77,7 @@ export const validateGenerateInputs = (props: UseGenerateVideoProps): string | n // 无论手动还是自动模式,都必须有素材 const materialIds = materialMode === "auto" ? smartSelectedIds : selectedMaterials if (materialIds.length === 0) { - return materialMode === "auto" - ? "AI 未匹配到素材,请手动选择素材后重试" - : "请至少选择一个素材" + return materialMode === "auto" ? "AI 未匹配到素材,请手动选择素材后重试" : "请至少选择一个素材" } if (voiceMode === "clone" && !selectedClonedVoice) { return "请先选择一个克隆音色" -- 2.54.0 From 5f06fde68b8297abf2b75bd7f67bcbd6215217b1 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 7 Aug 2026 20:32:58 +0800 Subject: [PATCH 4/4] fix: AI review feedback - null safety in buildPayload, use any() in can_generate, remove dead test code --- apps/api/app/services/edit_plan_service.py | 3 +-- .../pages/generate/hooks/generate-video/buildPayload.ts | 2 +- tests/unit/test_edit_plan_service.py | 7 ------- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/apps/api/app/services/edit_plan_service.py b/apps/api/app/services/edit_plan_service.py index ab4c3aec0..195d6321b 100755 --- a/apps/api/app/services/edit_plan_service.py +++ b/apps/api/app/services/edit_plan_service.py @@ -586,8 +586,7 @@ class EditPlanService: return False, "请先添加片段后再生成视频" # 检查是否至少有一个片段分配了素材 - clips_with_asset = [c for c in clips if c.asset_id] - if not clips_with_asset: + if not any(c.asset_id for c in clips): return False, "没有可渲染的就绪片段,请确保已选择素材" return True, "" diff --git a/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts b/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts index 49dec5c6e..e7f39ece8 100644 --- a/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts +++ b/apps/web/src/pages/generate/hooks/generate-video/buildPayload.ts @@ -75,7 +75,7 @@ export const validateGenerateInputs = (props: UseGenerateVideoProps): string | n return "请先选择或输入标题" } // 无论手动还是自动模式,都必须有素材 - const materialIds = materialMode === "auto" ? smartSelectedIds : selectedMaterials + const materialIds = materialMode === "auto" ? smartSelectedIds || [] : selectedMaterials || [] if (materialIds.length === 0) { return materialMode === "auto" ? "AI 未匹配到素材,请手动选择素材后重试" : "请至少选择一个素材" } diff --git a/tests/unit/test_edit_plan_service.py b/tests/unit/test_edit_plan_service.py index ce4452da6..b79bf5d18 100755 --- a/tests/unit/test_edit_plan_service.py +++ b/tests/unit/test_edit_plan_service.py @@ -548,13 +548,6 @@ class TestGenerationWorkflow: clips_by_order = {c.order: c for c in clips} assert clips_by_order[0].status == EditPlanClipStatus.READY assert clips_by_order[1].status == EditPlanClipStatus.PENDING - svc = _make_service() - p = svc.create_plan("tpl-001", "测试") - svc.create_clip(p.id, "intro", 0) - result = svc.get_generation_status(p.id) - assert result["plan"].id == p.id - assert len(result["clips"]) == 1 - assert result["generation_task_id"] is None def test_update_plan_config(self): svc = _make_service() -- 2.54.0