refactor(core): 脚本收口——只留 agent 出稿,删旧散文路 + 单镜重跑改走 agent
删 /generate-script/ 端点 + generate_project_script + build_script_prompt + 旧单镜 build_segment_rerun_prompt;前端删静默兜底(agent 失败明确报错,不再掉回旧路)+ onGenerateScript prop/api。rerun-script-segment 改用 agent 单镜逻辑(regenerate_segment_via_agent:读全脚本上下文/保留其余镜/落新版本)。根因:旧路不产 script_entities/entity_refs,下游故事板/视频参考图断线→图飘。测试:删 3 个旧 generate 用例 + 重写 rerun 为 agent 版,ai+projects 23 测试除 4 个既有 base-asset 失败外全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0695c36f4c
commit
1ecb186ddf
@@ -82,48 +82,30 @@ class ProjectApiTests(TestCase):
|
||||
self.assertTrue(VideoSegment.objects.filter(project=project).exists())
|
||||
|
||||
@patch("apps.ai.services.VolcanoArkProvider")
|
||||
def test_generate_script_creates_script_segments_and_charges_credit(self, provider_cls):
|
||||
provider = provider_cls.return_value
|
||||
provider.chat_completion.return_value = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": "1. 开场吸引\n2. 展示卖点\n3. 使用场景\n4. 促单转化",
|
||||
}
|
||||
}
|
||||
]
|
||||
def test_rerun_script_segment_via_agent_rewrites_one_and_charges_once(self, provider_cls):
|
||||
"""单镜重跑改走 agent:读结构化全脚本,只重写目标镜、保留其余镜,落新版本,计费一次。"""
|
||||
import json as _json
|
||||
base_draft = {
|
||||
"hook": "钩子", "tone": "种草", "aspect_ratio": "9:16", "total_duration": 30, "segment_count": 2,
|
||||
"entities": [],
|
||||
"segments": [
|
||||
{"index": 0, "duration": 15, "role": "钩子", "narration": "旧0", "visual": "画0", "speaker": None, "product_exposure": "", "entity_refs": [], "dialogue": []},
|
||||
{"index": 1, "duration": 15, "role": "CTA", "narration": "旧1", "visual": "画1", "speaker": None, "product_exposure": "", "entity_refs": [], "dialogue": []},
|
||||
],
|
||||
}
|
||||
provider.extract_text.return_value = "1. 开场吸引\n2. 展示卖点\n3. 使用场景\n4. 促单转化"
|
||||
project = Project.objects.create(team=self.team, created_by=self.user, product=self.product, name="Launch Video")
|
||||
for stage in [
|
||||
ProjectStage.Stage.SCRIPT,
|
||||
ProjectStage.Stage.BASE_ASSETS,
|
||||
ProjectStage.Stage.STORYBOARD,
|
||||
ProjectStage.Stage.VIDEO,
|
||||
ProjectStage.Stage.EXPORT,
|
||||
]:
|
||||
ProjectStage.objects.create(project=project, stage=stage)
|
||||
|
||||
response = self.client.post(
|
||||
f"/api/projects/{project.id}/generate-script/",
|
||||
{"prompt": "突出高转化"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
script = ScriptVersion.objects.get(project=project)
|
||||
self.assertEqual(script.segments.count(), 4)
|
||||
# 出稿(1)+ 人物/场景提取(1)各计费一次;此处 mock 提取返回非 JSON,提取仍按调用成功计费
|
||||
self.assertEqual(CreditLedger.objects.filter(team=self.team, ledger_type=CreditLedger.Type.CHARGE).count(), 2)
|
||||
|
||||
@patch("apps.ai.services.VolcanoArkProvider")
|
||||
def test_rerun_script_segment_updates_one_segment_and_charges_once(self, provider_cls):
|
||||
new_draft = _json.loads(_json.dumps(base_draft))
|
||||
new_draft["segments"][1]["narration"] = "全新口播文案"
|
||||
new_draft["segments"][1]["visual"] = "全新画面描述"
|
||||
provider = provider_cls.return_value
|
||||
provider.chat_completion.return_value = {"choices": [{"message": {"content": "x"}}]}
|
||||
provider.extract_text.return_value = "旁白:全新口播文案\n画面:全新画面描述"
|
||||
provider.extract_text.return_value = "```json\n" + _json.dumps(new_draft, ensure_ascii=False) + "\n```"
|
||||
|
||||
project = Project.objects.create(team=self.team, created_by=self.user, product=self.product, name="P")
|
||||
script = ScriptVersion.objects.create(project=project, title="脚本", content="...", is_adopted=True)
|
||||
seg0 = ScriptSegment.objects.create(script_version=script, sort_order=0, narration="旧0", visual_prompt="画0")
|
||||
script = ScriptVersion.objects.create(
|
||||
project=project, title="脚本", content=_json.dumps(base_draft, ensure_ascii=False),
|
||||
is_adopted=True, metadata={"hook": "钩子", "entities": []},
|
||||
)
|
||||
ScriptSegment.objects.create(script_version=script, sort_order=0, narration="旧0", visual_prompt="画0")
|
||||
seg1 = ScriptSegment.objects.create(script_version=script, sort_order=1, narration="旧1", visual_prompt="画1")
|
||||
|
||||
response = self.client.post(
|
||||
@@ -131,17 +113,13 @@ class ProjectApiTests(TestCase):
|
||||
{"segment_id": str(seg1.id), "instruction": "更俏皮"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
seg0.refresh_from_db()
|
||||
seg1.refresh_from_db()
|
||||
self.assertEqual(seg1.narration, "全新口播文案")
|
||||
self.assertEqual(seg1.visual_prompt, "全新画面描述")
|
||||
# 其它分镜保持不动
|
||||
self.assertEqual(seg0.narration, "旧0")
|
||||
self.assertEqual(seg0.visual_prompt, "画0")
|
||||
# 返回的是该镜所属 ScriptVersion
|
||||
self.assertEqual(str(response.data["id"]), str(script.id))
|
||||
# agent 落新 ScriptVersion:目标镜改了,其余镜保留
|
||||
new_version = ScriptVersion.objects.filter(project=project).order_by("-created_at").first()
|
||||
segs = list(new_version.segments.order_by("sort_order"))
|
||||
self.assertEqual(segs[1].narration, "全新口播文案")
|
||||
self.assertEqual(segs[1].visual_prompt, "全新画面描述")
|
||||
self.assertEqual(segs[0].narration, "旧0")
|
||||
self.assertEqual(CreditLedger.objects.filter(team=self.team, ledger_type=CreditLedger.Type.CHARGE).count(), 1)
|
||||
|
||||
def test_rerun_script_segment_rejects_foreign_segment(self):
|
||||
@@ -157,44 +135,6 @@ class ProjectApiTests(TestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# ── 流程文档 步骤③④:出稿后自动提取人物/场景(含建议提示词)并计费 ──
|
||||
@patch("apps.ai.services.VolcanoArkProvider")
|
||||
def test_generate_script_extracts_cast_scenes_and_bills(self, provider_cls):
|
||||
provider = provider_cls.return_value
|
||||
script_text = "镜头1\n旁白:开场\n画面:地铁里女主掏出面膜\n\n镜头2\n旁白:卖点\n画面:同事围观"
|
||||
# 提取调用让模型回 JSON(允许外面裹解释文字,验证容错)
|
||||
extract_text = '这是结果:{"cast":[{"name":"女主","prompt":"26岁都市女性,自然妆"},{"name":"同事","prompt":"职场女性"}],"scenes":[{"name":"地铁","prompt":"早高峰地铁车厢"}]}'
|
||||
provider.chat_completion.side_effect = [{"r": 1}, {"r": 2}]
|
||||
provider.extract_text.side_effect = [script_text, extract_text]
|
||||
project = Project.objects.create(team=self.team, created_by=self.user, product=self.product, name="P")
|
||||
ProjectStage.objects.create(project=project, stage=ProjectStage.Stage.SCRIPT)
|
||||
|
||||
response = self.client.post(f"/api/projects/{project.id}/generate-script/", {"prompt": "x"}, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
project.refresh_from_db()
|
||||
self.assertEqual(project.metadata.get("cast"), ["女主", "同事"])
|
||||
self.assertEqual(project.metadata.get("scenes"), ["地铁"])
|
||||
self.assertEqual(project.metadata.get("cast_prompts", {}).get("女主"), "26岁都市女性,自然妆")
|
||||
self.assertEqual(project.metadata.get("scene_prompts", {}).get("地铁"), "早高峰地铁车厢")
|
||||
# 出稿 + 提取,各计费一次
|
||||
self.assertEqual(CreditLedger.objects.filter(team=self.team, ledger_type=CreditLedger.Type.CHARGE).count(), 2)
|
||||
|
||||
@patch("apps.ai.services.VolcanoArkProvider")
|
||||
def test_extract_failure_releases_credit_and_keeps_script(self, provider_cls):
|
||||
"""提取调用抛错:必须退掉提取的预扣(不计费),且不影响脚本已出稿(出稿那笔照扣)。"""
|
||||
provider = provider_cls.return_value
|
||||
provider.chat_completion.side_effect = [{"r": 1}, RuntimeError("ark down")]
|
||||
provider.extract_text.side_effect = ["镜头1\n旁白:x\n画面:y", AssertionError("不应走到")]
|
||||
project = Project.objects.create(team=self.team, created_by=self.user, product=self.product, name="P")
|
||||
ProjectStage.objects.create(project=project, stage=ProjectStage.Stage.SCRIPT)
|
||||
|
||||
response = self.client.post(f"/api/projects/{project.id}/generate-script/", {"prompt": "x"}, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(ScriptVersion.objects.filter(project=project).count(), 1)
|
||||
# 出稿计费 1 笔;提取失败 → release,不产生第 2 笔 CHARGE
|
||||
self.assertEqual(CreditLedger.objects.filter(team=self.team, ledger_type=CreditLedger.Type.CHARGE).count(), 1)
|
||||
|
||||
@patch("apps.ai.services._store_generated_media")
|
||||
@patch("apps.ai.services.get_image_provider")
|
||||
def test_generate_base_asset_stores_tag_label(self, get_provider, store_media):
|
||||
|
||||
Reference in New Issue
Block a user