diff --git a/core/backend/apps/ai/script_agent.py b/core/backend/apps/ai/script_agent.py index 7f4161e..21b5b7d 100644 --- a/core/backend/apps/ai/script_agent.py +++ b/core/backend/apps/ai/script_agent.py @@ -288,28 +288,29 @@ def normalize_draft(raw_text: str, *, aspect_ratio: str, total_duration: int) -> speaker = speaker if (speaker in valid_ids) else None refs = [r for r in (seg.get("entity_refs") or []) if r in valid_ids] # 对白(剧情向):[{speaker(合法 entity id 或 null=旁白), line}];默认空 = 纯口播。 - # 模型变体里 dialogue 可能是字符串(整句口播)而非数组——只在数组时按结构解析。 + # 模型变体的口播字段五花八门:dialogue(字符串/数组)/ lines(数组)/ 每项 line|text|content。 + # 这里把任意数组形态归一成结构化对白,字符串形态留给下面当整句旁白。 raw_dialogue = seg.get("dialogue") dialogue = [] - if isinstance(raw_dialogue, list): - for d in raw_dialogue: - if not isinstance(d, dict): - continue - line = (d.get("line") or d.get("text") or "").strip() - if not line: - continue - sp = d.get("speaker") - dialogue.append({"speaker": sp if sp in valid_ids else None, "line": line}) - # 旁白:narration > 字符串 dialogue(整句口播)> subtitle(字幕)> 结构化对白拼接 - narration = (seg.get("narration") or "").strip() + dialogue_array = raw_dialogue if isinstance(raw_dialogue, list) else (seg.get("lines") if isinstance(seg.get("lines"), list) else []) + for d in dialogue_array: + if not isinstance(d, dict): + continue + line = (d.get("line") or d.get("text") or d.get("content") or "").strip() + if not line: + continue + sp = d.get("speaker") + dialogue.append({"speaker": sp if sp in valid_ids else None, "line": line}) + # 旁白:narration > 字符串 dialogue(整句口播)> 结构化对白/lines 拼接 > subtitle/caption(字幕兜底) + narration = (seg.get("narration") or seg.get("voiceover") or "").strip() if not narration and isinstance(raw_dialogue, str): narration = raw_dialogue.strip() - if not narration: - narration = (seg.get("subtitle") or "").strip() if not narration and dialogue: narration = " ".join(d["line"] for d in dialogue) # 扁平拼接,兼容下游字幕/配音 - # 画面:visual > visual_prompt > scene(模型变体常用 scene 描述画面) - visual = (seg.get("visual") or seg.get("visual_prompt") or seg.get("scene") or "").strip() + if not narration: + narration = (seg.get("subtitle") or seg.get("caption") or "").strip() + # 画面:visual > visual_prompt > scene > shot/visual_description(模型变体常用 scene 描述画面) + visual = (seg.get("visual") or seg.get("visual_prompt") or seg.get("scene") or seg.get("visual_description") or "").strip() norm_segments.append( { "index": i, diff --git a/core/backend/apps/ai/tests.py b/core/backend/apps/ai/tests.py index 186f24b..c3ab3b9 100644 --- a/core/backend/apps/ai/tests.py +++ b/core/backend/apps/ai/tests.py @@ -31,6 +31,26 @@ class NormalizeDraftTests(SimpleTestCase): self.assertEqual(draft["segments"][0]["narration"], "卡裆太社死!") self.assertEqual(draft["segments"][0]["visual"], "更衣室扯卡裆旧裤") + def test_lines_and_caption_variant_fills_narration(self): + """另一种变体(Doubao-Seed-2.0-P):shots 用 lines:[{role,content}] 放口播、caption 放字幕。 + 必须把 lines 的 content 当对白/旁白,caption 兜底,而不是只填画面留旁白空。""" + raw = json.dumps({ + "ScriptDraft": { + "basicInfo": {"totalDuration": 30, "aspectRatio": "9:16"}, + "shots": [ + {"shotNo": 1, "scene": "化妆台两闺蜜", "lines": [{"role": "女主", "content": "防晒泛白太尴尬!"}, {"role": "闺蜜", "content": "试试这个!"}], "caption": "防晒踩雷?"}, + {"shotNo": 2, "scene": "手背挤膏体", "lines": [{"role": "闺蜜", "content": "质地清透不泛白"}], "caption": "清透质地"}, + ], + } + }, ensure_ascii=False) + draft = normalize_draft(raw, aspect_ratio="9:16", total_duration=30) + self.assertEqual(len(draft["segments"]), 2) + for seg in draft["segments"]: + self.assertTrue(seg["narration"], "旁白不应为空") + self.assertTrue(seg["visual"], "画面不应为空") + self.assertIn("防晒泛白太尴尬", draft["segments"][0]["narration"]) + self.assertEqual(len(draft["segments"][0]["dialogue"]), 2) # lines→结构化对白 + def test_string_dialogue_not_iterated_as_chars(self): """dialogue 为整句字符串时,要当作旁白而不是逐字符遍历。""" raw = json.dumps({