完善二期清单
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""套路模板:从一版脚本抽出可复用的结构,以及把模板还原成给脚本 agent 的指令。
|
||||
|
||||
设计前提:模板只承载**怎么讲**(镜数、每镜作用、节奏、商品怎么露、口播还是对白、转化写法),
|
||||
不承载**讲什么**(旧商品的口播词)。换商品重跑时才不会把上一个品牌带出去。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
ROLE_FALLBACK = "叙述"
|
||||
DELIVERY_DIALOGUE = "角色对白"
|
||||
DELIVERY_NARRATION = "口播/旁白"
|
||||
|
||||
# 脚本 agent 落库存中文标签(「短剧」),设定卡 / 模板表用 ASCII key(drama)。两边都认。
|
||||
# 人物同理:一期向导偶尔把中文写进 wizard.persona。
|
||||
_PERSONA_KEYS = {"urban", "bestie", "ceo", "reviewer", "mom", "genz"}
|
||||
_PERSONA_KEY_BY_LABEL = {
|
||||
"都市白领女性": "urban",
|
||||
"闺蜜种草": "bestie",
|
||||
"总裁亲选": "ceo",
|
||||
"专业测评师": "reviewer",
|
||||
"专业测评": "reviewer",
|
||||
"实用宝妈": "mom",
|
||||
"学生党": "genz",
|
||||
}
|
||||
|
||||
|
||||
def coerce_template_combo(presentation_format: str, video_structure: str) -> tuple[str, str]:
|
||||
from apps.ai.script_agent import combo_keys
|
||||
|
||||
return combo_keys(presentation_format or None, video_structure or None)
|
||||
|
||||
|
||||
def coerce_persona(value: str) -> str:
|
||||
raw = (value or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
if raw in _PERSONA_KEYS:
|
||||
return raw
|
||||
return _PERSONA_KEY_BY_LABEL.get(raw, raw)
|
||||
|
||||
|
||||
def build_template_fields(*, project, script) -> dict:
|
||||
"""从 ScriptVersion 抽出模板字段。不落库,调用方决定怎么存。"""
|
||||
meta = script.metadata or {}
|
||||
wizard = (project.metadata or {}).get("wizard") or {}
|
||||
segments = list(script.segments.all())
|
||||
|
||||
outline = []
|
||||
cta = ""
|
||||
for index, segment in enumerate(segments):
|
||||
role = (segment.role or "").strip() or ROLE_FALLBACK
|
||||
outline.append(
|
||||
{
|
||||
"index": index,
|
||||
"role": role,
|
||||
"duration": int(segment.duration_seconds or 0),
|
||||
"product_exposure": (segment.product_exposure or "").strip(),
|
||||
"delivery": DELIVERY_DIALOGUE if segment.dialogue else DELIVERY_NARRATION,
|
||||
}
|
||||
)
|
||||
if role == "CTA":
|
||||
cta = (segment.narration or "").strip()
|
||||
if not cta and segments:
|
||||
cta = (segments[-1].narration or "").strip()
|
||||
|
||||
total = meta.get("total_duration")
|
||||
if not isinstance(total, int) or total <= 0:
|
||||
total = sum(item["duration"] for item in outline)
|
||||
|
||||
raw_format = str(meta.get("presentation_format") or wizard.get("presentation_format") or "")
|
||||
raw_structure = str(meta.get("video_structure") or wizard.get("video_structure") or "")
|
||||
fmt, structure = coerce_template_combo(raw_format, raw_structure) if (raw_format or raw_structure) else ("", "")
|
||||
|
||||
return {
|
||||
"presentation_format": fmt,
|
||||
"video_structure": structure,
|
||||
"persona": coerce_persona(str(wizard.get("persona") or "")),
|
||||
"total_duration": total,
|
||||
"outline": outline,
|
||||
"cta": cta[:500],
|
||||
"scenes": [s for s in ((project.metadata or {}).get("scenes") or []) if isinstance(s, str)][:6],
|
||||
}
|
||||
|
||||
|
||||
def render_outline_text(template_fields: dict) -> str:
|
||||
"""把模板渲染成人能读、模型也能照着做的一段中文,供前端预览与生成时拼 prompt 复用。"""
|
||||
outline = template_fields.get("outline") or []
|
||||
lines = []
|
||||
for item in outline:
|
||||
parts = [f"第 {int(item.get('index', 0)) + 1} 镜", f"{item.get('duration', 0)}s", str(item.get("role") or ROLE_FALLBACK)]
|
||||
exposure = str(item.get("product_exposure") or "").strip()
|
||||
if exposure:
|
||||
parts.append(f"商品{exposure}")
|
||||
parts.append(str(item.get("delivery") or DELIVERY_NARRATION))
|
||||
lines.append(" · ".join(parts))
|
||||
body = "\n".join(lines)
|
||||
cta = str(template_fields.get("cta") or "").strip()
|
||||
if cta:
|
||||
body += f"\n结尾转化写法参考:{cta}"
|
||||
return body
|
||||
Reference in New Issue
Block a user