fix(core): 脚本 agent 用通用字段解析,根治模型 schema 漂移致旁白/画面空

模型每次生成都换字段名(scene/screenDescription/画面…;dialogue 字符串 /
lines:[{role,content}] / caption…),逐一追变体是治标。改为通用解析:
- _pick_field:优先键命中 → 否则按关键词模糊匹配,跳过 bgMusic/note/shotNo 等非内容键
- 画面来源:visual/scene/screenDescription/画面… 任意命中
- 旁白来源:结构化 dialogue/lines 优先,其次整句 dialogue,再退 narration/voiceover/caption/字幕
实测覆盖 4 种真实/契约变体,全部正确填充;新增通用解析回归用例,23 测试通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-17 10:38:23 +08:00
co-authored by Claude Opus 4.8
parent 0280c2e192
commit 521f5f4535
2 changed files with 55 additions and 7 deletions
+19
View File
@@ -51,6 +51,25 @@ class NormalizeDraftTests(SimpleTestCase):
self.assertIn("防晒泛白太尴尬", draft["segments"][0]["narration"])
self.assertEqual(len(draft["segments"][0]["dialogue"]), 2) # lines→结构化对白
def test_generic_resolver_covers_unseen_field_names(self):
"""模型每次换字段名(scene/screenDescription/画面…、dialogue/lines/caption…)。
通用解析应「优先键 + 关键词模糊匹配」都能填,且跳过 bgMusic/note 等非内容键。"""
variants = {
"canonical": {"total_duration": 15, "segments": [{"role": "钩子", "narration": "口播A", "visual": "画面A"}]},
"scene+dialogue_str+subtitle": {"total_duration": 15, "shots": [{"scene": "画面B", "dialogue": "口播B", "subtitle": "字幕B"}]},
"scene+lines+caption": {"total_duration": 15, "shots": [{"scene": "画面C", "lines": [{"role": "女主", "content": "口播C"}], "caption": "字幕C"}]},
"screenDescription+dialogue+bgMusic+note": {"total_duration": 15, "shots": [{"screenDescription": "画面D", "dialogue": "口播D", "bgMusic": "音乐D", "note": "备注D"}]},
}
for name, raw in variants.items():
draft = normalize_draft(json.dumps(raw, ensure_ascii=False), aspect_ratio="9:16", total_duration=15)
seg = draft["segments"][0]
self.assertTrue(seg["narration"], f"{name}: 旁白为空")
self.assertTrue(seg["visual"], f"{name}: 画面为空")
# bgMusic/note 不得被误当画面/旁白
d = normalize_draft(json.dumps(variants["screenDescription+dialogue+bgMusic+note"], ensure_ascii=False), aspect_ratio="9:16", total_duration=15)
self.assertEqual(d["segments"][0]["visual"], "画面D")
self.assertEqual(d["segments"][0]["narration"], "口播D")
def test_string_dialogue_not_iterated_as_chars(self):
"""dialogue 为整句字符串时,要当作旁白而不是逐字符遍历。"""
raw = json.dumps({