101 lines
3.9 KiB
Python
101 lines
3.9 KiB
Python
"""套路模板:从一版脚本抽出可复用的结构,以及把模板还原成给脚本 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
|