feat(core): 脚本 agent 体感打磨 — skill松绑/对白去重/模型自写收尾/品牌遮罩/删除提速
- skill:旁白/对白每镜交给模型按内容判断(去掉「默认旁白·对白按门槛」);visual 加厚撑满15秒 - 脚本 agent:输出协议加收尾交付语 + _closing_summary → summary 事件(替写死的「已生成」) - 前端:有对白的镜不再重复显旁白;summary 当 AI 气泡;进度时间轴 @keyframes 嵌套失效→移顶层(动画真跑起来) - 生成遮罩改品牌生成卡(icon-box转圈+标题+动态点+橙扫光条);AI全生确定后不再闪三菜单 - 删除分镜:不再误显改写罩子(拆 rewriteShot);放开「至少保留一个分镜」可删空;删除提速(后端 bulk_update + 前端 liteRefresh) - 去掉脚本助手标题旁冗余的模型名 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
9fce7e8fff
commit
a71be19500
@@ -12,6 +12,7 @@ SSE 事件(每帧 `data: {json}\n\n`,json 带 type):
|
||||
delta {text} —— 模型自然语言前言(JSON 部分不外露)
|
||||
draft {draft} —— 规范化后的 ScriptDraft(前端结构化渲染)
|
||||
saved {script_version_id, version} —— 已落库的 ScriptVersion(含 segments/metadata)
|
||||
summary {text} —— 模型自己写的收尾交付语(当 AI 回复气泡,替代写死的「已生成」)
|
||||
done {} —— 结束
|
||||
error {detail} —— 失败(已回滚额度)
|
||||
"""
|
||||
@@ -72,7 +73,7 @@ _OUTPUT_PROTOCOL = """
|
||||
严格按以下顺序输出,不要有别的内容:
|
||||
1. 先用 **1 句中文口语**告诉用户你正在做什么(≤40 字,例:「在为这款保温杯生成 4 镜痛点脚本…」),让用户看到进展;
|
||||
2. 紧接着输出**且仅输出一个** ```json 代码块,内容为符合技能契约(铁律1)的 ScriptDraft 对象;
|
||||
3. json 代码块之后**不要再写任何文字**。
|
||||
3. json 代码块**收尾之后另起一行**,用 **1–2 句中文口语**跟用户交付这一版:做了什么、为什么这么改、可以怎么接着调(像同事汇报,**别复述 JSON 字段、别再写代码块**)。这句会作为你的回复气泡展示给用户。
|
||||
|
||||
### 字段名锚定(硬性 · 下游靠它取数,跑偏即数据全空)
|
||||
- 分镜数组的键名**必须**叫 `segments`(禁止用 scenes / script / shots / 分镜 等同义词)。
|
||||
@@ -536,6 +537,21 @@ def _visible_cut(text: str) -> int:
|
||||
return min(cands) if cands else len(text)
|
||||
|
||||
|
||||
def _closing_summary(raw: str) -> str:
|
||||
"""模型在 json 之后写的收尾交付语 = 给用户的回复气泡(像同事汇报「这版做了啥/怎么接着调」)。
|
||||
取最后一个 json 对象之后的文字,去掉收尾围栏与空白;没写或残留花括号就返回空(前端兜底)。"""
|
||||
js = _extract_json(raw)
|
||||
tail = ""
|
||||
if js:
|
||||
idx = raw.rfind(js)
|
||||
if idx != -1:
|
||||
tail = raw[idx + len(js):]
|
||||
tail = re.sub(r"`+", " ", tail).strip() # 去掉 json 收尾的 ``` 围栏
|
||||
if "{" in tail or len(tail) < 4:
|
||||
return ""
|
||||
return tail[:160]
|
||||
|
||||
|
||||
def stream_script_agent(
|
||||
*,
|
||||
project,
|
||||
@@ -697,6 +713,10 @@ def stream_script_agent(
|
||||
"version": ScriptVersionSerializer(script).data,
|
||||
}
|
||||
)
|
||||
# 模型自己写的收尾交付语 → AI 回复气泡(没写则前端兜底默认句)
|
||||
summary = _closing_summary(raw)
|
||||
if summary:
|
||||
yield _sse({"type": "summary", "text": summary})
|
||||
yield _sse({"type": "done"})
|
||||
finally:
|
||||
# 断连(GeneratorExit)或任何 settled=False 的退出路径:释放预扣,避免额度冻结
|
||||
|
||||
@@ -444,8 +444,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
id=request.data.get("segment_id"), script_version__project=project
|
||||
)
|
||||
script = segment.script_version
|
||||
if script.segments.count() <= 1:
|
||||
return Response({"detail": "至少保留一个分镜"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
# 允许删空:用户可以把镜头全删了(再整体重新生成或手动加),不再强制「至少保留一个分镜」。
|
||||
# 删本场脚本时,它对应的故事板分镜帧也一起删 —— frame.script_segment 是 SET_NULL,
|
||||
# 不主动删会留下孤儿帧,故事板里仍显示这一(已删的)场。各版本受影响的帧统一收口后重排 sort_order。
|
||||
affected_storyboards = list(StoryboardVersion.objects.filter(frames__script_segment=segment).distinct())
|
||||
@@ -456,10 +455,14 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
frame.sort_order = index
|
||||
frame.save(update_fields=["sort_order", "updated_at"])
|
||||
segment.delete()
|
||||
# 删后重排 sort_order:收集变动的镜一次性 bulk_update,替代逐条 save(远程库少跑 N 个往返 → 删除快很多)
|
||||
resort = []
|
||||
for index, seg in enumerate(script.segments.order_by("sort_order")):
|
||||
if seg.sort_order != index:
|
||||
seg.sort_order = index
|
||||
seg.save(update_fields=["sort_order", "updated_at"])
|
||||
resort.append(seg)
|
||||
if resort:
|
||||
ScriptSegment.objects.bulk_update(resort, ["sort_order"])
|
||||
self._sync_video_segments_to_script(project, script)
|
||||
return Response(ScriptVersionSerializer(script).data)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user