feat(core): 角色对白(剧情向按需)+ 聊天精准改一镜 + 模型下拉移位 + 全流程e2e + 交叉验证修复

新能力(均已端到端验证):
- 角色对白 dialogue:[{speaker,line}](speaker=null 即旁白)。默认口播,用户在聊天提要求才出多角色对白(剧情向);narration 保留扁平拼接兼容下游字幕/配音。skill 同步:默认不强制对白。
- 聊天精准改一镜:聊天说「第N镜改XX」→ agent 读全脚本上下文、只重写那一镜(target_index + 后端 _merge_single_segment 强制保其余镜原样),保衔接。
- 前端:分镜卡渲染对白/镜型/露出;模型下拉从顶部移到输入框下方小按钮(对齐 ChatGPT/Lovart)。

全流程 e2e 真跑通:商品→脚本→基础资产(gpt-image-2)→故事板(@图N 多图合成锁脸锁商品)→视频(Seedance 6.4分钟出片,带音效+人声)。

对抗式交叉验证(3 审查员)修复:
- 改稿用基准稿时长 effective_duration(prompt head + normalize + merge),避免请求默认 60 把 90s/6镜稿尾镜截掉/单镜改空转。
- target_index 越界服务端早返回报错、不计费;模型未产出目标镜时抛错释放额度(不静默空转)。
- 前端「第N镜」正则收窄为 镜|场(去掉会误判「第3个卖点」的 个|段)。
- 回归:测试 setUp 停用 seed 中转站文本模型,保证命中可 mock 的 provider(18/18 过)。

CLAUDE.md 加「AI 生成 Agent 化架构」一节供后续维护。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-17 05:09:51 +08:00
co-authored by Claude Opus 4.8
parent 163a6627ba
commit 3d38c173e6
12 changed files with 186 additions and 29 deletions
@@ -0,0 +1,18 @@
# Generated by Django 5.1.15 on 2026-06-16 20:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('projects', '0002_scriptsegment_structured_fields'),
]
operations = [
migrations.AddField(
model_name='scriptsegment',
name='dialogue',
field=models.JSONField(blank=True, default=list),
),
]
+3
View File
@@ -85,6 +85,9 @@ class ScriptSegment(TimeStampedModel):
speaker = models.CharField(max_length=32, blank=True) # 指向某 entity id;画外旁白为空
product_exposure = models.CharField(max_length=64, blank=True) # 手持/特写/使用中…
entity_refs = models.JSONField(default=list, blank=True) # 本镜引用的 entity id 列表(→ 故事板 @图N)
# 角色对白(剧情向):[{speaker, line}],speaker=entity id 即角色台词、null 即旁白。
# 默认空 = 纯口播/旁白(用 narration);用户在聊天里明确要对白时 agent 才填。narration 仍存扁平拼接兼容下游。
dialogue = models.JSONField(default=list, blank=True)
class Meta:
ordering = ["sort_order", "created_at"]
+1 -1
View File
@@ -230,7 +230,7 @@ class ScriptSegmentSerializer(serializers.ModelSerializer):
model = ScriptSegment
fields = [
"id", "sort_order", "duration_seconds", "narration", "visual_prompt", "product_points",
"role", "speaker", "product_exposure", "entity_refs",
"role", "speaker", "product_exposure", "entity_refs", "dialogue",
]
read_only_fields = fields
+4
View File
@@ -41,6 +41,10 @@ class ProjectApiTests(TestCase):
endpoint="chat/completions",
unit_price="2.0000",
)
# 数据迁移(0006)会 seed 中转站文本模型,created_at 更早 → get_default_model 会选到它,
# 从而路由到未被 patch 的 OpenAICompatibleProvider 打真网络。这里只保留自建 volcengine 模型为
# active,确保命中可 mock 的 VolcanoArkProvider(生产有 bootstrap 豆包在前,默认仍是豆包,不受影响)。
ModelConfig.objects.filter(capability=ModelConfig.Capability.TEXT).exclude(id=self.model.id).update(status="disabled")
self.client = APIClient()
self.client.force_authenticate(self.user)
+6
View File
@@ -166,6 +166,11 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
total_duration = int(request.data.get("total_duration") or 60)
except (TypeError, ValueError):
total_duration = 60
target_index = request.data.get("target_index")
try:
target_index = int(target_index) if target_index is not None else None
except (TypeError, ValueError):
target_index = None
model_config = None
requested = request.data.get("model_config_id")
@@ -191,6 +196,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
base_version_id=base_version_id,
aspect_ratio=aspect_ratio,
total_duration=total_duration,
target_index=target_index,
)
response = StreamingHttpResponse(stream, content_type="text/event-stream")
response["Cache-Control"] = "no-cache"