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