优化住流程和添加复刻视频页面

This commit is contained in:
Azmat@qq.com
2026-08-26 18:44:03 +08:00
parent 0ee498d807
commit c25060c6c6
30 changed files with 2574 additions and 155 deletions
+135 -23
View File
@@ -68,6 +68,51 @@ STRUCTURE_MIN_DURATION: dict[str, int] = {
"pain": 15, "contrast": 15, "review": 30, "scene": 30, "promo": 15, "knowledge": 30,
}
# 用户在设定卡里选的是「怎么说服」,不是一个仅供展示的标签。这里把每种结构收成
# 可直接塞进任务上下文的验收合同;模型不得拿痛点结构去代替知识分享、拿种草去代替测评。
STRUCTURE_CONTRACTS: dict[str, str] = {
"pain": (
"【痛点解决·结构合同】必须按顺序完成:具体问题出现 → 这个问题造成的不便 → 商品作为解法介入"
"→ 对应卖点与真实使用过程 → 可见的改善 → 一个行动引导。开头先给可拍到的具体问题,"
"商品不能在第一句话直接硬推;一个痛点只对应一个卖点。禁止把普通不便夸成严重后果或制造焦虑。\n"
),
"contrast": (
"【前后对比·结构合同】必须建立同一人物/场景/角度/光线下可比较的“使用前”和“使用后”,"
"中间必须有商品使用过程作为转折,再总结差异并 CTA。旁白只交代条件、过程和差异,"
"不能写成长篇原理讲解;不得跳过过程、换条件伪造差异,或同时对比多个维度。\n"
),
"review": (
"【测评验证·结构合同】必须按顺序完成:提出要验证什么 → 说清测评标准/条件 → 完整展示测试动作"
"→ 呈现画面可观察的结果 → 给出带适用边界的结论和 CTA。开场必须是疑问或待验证目标,"
"不是直接夸商品;每个卖点都要改写成可演示的验证项目。禁止虚构检测数据、权威结论、用户评价或编造缺点。\n"
),
"scene": (
"【场景种草·结构合同】必须按顺序完成:先交代人物、具体时间和地点 → 场景中自然出现需求"
"→ 商品以正在被使用的方式入场 → 写出该场景里的真实体验 → 轻度推荐/软 CTA。第一镜先建立场景,"
"不要一上来抛痛点提问或商品参数;商品不能像硬广道具摆拍,卖点要融入动作与感受。\n"
),
"promo": (
"【促销抢购·结构合同】必须按顺序完成:真实福利/价格钩子(仅在资料明确时)→ 商品基础价值"
"→ 优惠适用的规格、条件与领取方式 → 真实的时间/库存信息(仅在资料明确时)→ 明确购买入口和操作 CTA。"
"若资料没有价格、优惠、库存或时间,就不得捏造或制造抢购氛围,改为商品价值与查看详情的引导。\n"
),
"knowledge": (
"【知识分享·结构合同】必须按顺序完成:提出一个选购/使用问题或常见误区 → 给出清楚的判断方法"
"→ 用实物细节、步骤或正确/错误示范讲清方法 → 商品在方法建立后作为符合该标准的实例出现"
"→ 总结选择建议和自然 CTA。只讲 1–3 个知识点,商品不能在开头就变成卖点罗列。"
"禁止虚构数据、标准、专业身份或功效承诺,也不能只靠人物口头科普而没有画面演示。\n"
),
}
STRUCTURE_STAGES: dict[str, tuple[str, ...]] = {
"pain": ("具体问题与不便", "商品介入和真实用法", "可见改善与行动引导"),
"contrast": ("使用前的基准状态", "商品使用过程", "使用后的同条件状态", "差异总结与行动引导"),
"review": ("测评目标与标准", "完整测试过程", "可观察的结果", "适用结论与行动引导"),
"scene": ("具体时间、地点与人物状态", "场景中的自然需求", "商品被使用着入场", "真实体验与软 CTA"),
"promo": ("真实福利或商品价值钩子", "商品与适用场景", "明确的优惠条件和购买方法", "入口与操作 CTA"),
"knowledge": ("问题或常见误区", "判断方法或核心结论", "实物步骤或正反示范", "商品承接、选择建议与 CTA"),
}
# 设定卡人物 key → 中文(与前端 WIZ_PERSONA_LABEL / 模板 coerce_persona 对齐)。
PERSONA_LABELS: dict[str, str] = {
"urban": "都市白领女性",
@@ -177,6 +222,68 @@ def coerce_combo(fmt: str | None, structure: str | None) -> tuple[str, str]:
return fmt, structure
# 与前端 script-setup.ts CATEGORY_COMBOS 对齐。改这里时两边一起改。
_CATEGORY_COMBOS: dict[str, dict[str, str]] = {
"美妆个护": {"format": "oral", "structure": "contrast", "persona": "bestie"},
"食品饮料": {"format": "oral", "structure": "scene", "persona": "bestie"},
"服饰鞋包": {"format": "oral", "structure": "scene", "persona": "urban"},
"家居日用": {"format": "oral", "structure": "pain", "persona": "mom"},
"数码家电": {"format": "oral", "structure": "review", "persona": "reviewer"},
"母婴玩具": {"format": "oral", "structure": "pain", "persona": "mom"},
"运动健康": {"format": "oral", "structure": "scene", "persona": "urban"},
"珠宝配饰": {"format": "oral", "structure": "scene", "persona": "ceo"},
"汽车用品": {"format": "oral", "structure": "review", "persona": "reviewer"},
"宠物用品": {"format": "oral", "structure": "pain", "persona": "mom"},
"图书文教": {"format": "oral", "structure": "knowledge", "persona": "urban"},
"五金农资": {"format": "oral", "structure": "review", "persona": "reviewer"},
"其他商品": {"format": "oral", "structure": "pain", "persona": "urban"},
}
_CATEGORY_KEYWORD_RULES: tuple[tuple[tuple[str, ...], dict[str, str]], ...] = (
(("美妆", "护肤", "彩妆", "面膜", "精华", "洗护", "个护"), _CATEGORY_COMBOS["美妆个护"]),
(("保健", "营养", "膳食", "益生菌", "维生素"), {"format": "oral", "structure": "review", "persona": "reviewer"}),
(("数码", "3c", "电子", "电器", "手机", "耳机", "相机", "工具", "家电"), _CATEGORY_COMBOS["数码家电"]),
(("食品", "零食", "饮料", "咖啡", "", "生鲜", ""), _CATEGORY_COMBOS["食品饮料"]),
(("服饰", "女装", "男装", "服装", "", "", "配饰", "内衣"), _CATEGORY_COMBOS["服饰鞋包"]),
(("家居", "家纺", "收纳", "厨具", "清洁", "日用"), _CATEGORY_COMBOS["家居日用"]),
(("母婴", "宝宝", "儿童", "玩具"), _CATEGORY_COMBOS["母婴玩具"]),
(("宠物",), _CATEGORY_COMBOS["宠物用品"]),
(("户外", "露营", "运动", "健身"), _CATEGORY_COMBOS["运动健康"]),
)
_FALLBACK_COMBO: dict[str, str] = {"format": "oral", "structure": "pain", "persona": "urban"}
def recommend_script_setup(category: str = "", title: str = "") -> dict:
"""按商品品类推荐表现形式 × 视频结构 × 人物 × 时长。用户随时可改。"""
category = (category or "").strip()
title = (title or "").strip()
combo = _FALLBACK_COMBO
reason = "商品信息不足,先给一组最通用的"
if category in _CATEGORY_COMBOS:
combo = _CATEGORY_COMBOS[category]
reason = f"按「{category}」品类推荐"
else:
haystack = f"{category} {title}".lower()
for keywords, item in _CATEGORY_KEYWORD_RULES:
hit = next((word for word in keywords if word.lower() in haystack), None)
if hit:
combo = item
reason = f"按「{hit}」品类推荐"
break
# 自动推荐只给口播;Vlog / 短剧留给用户自己选。
fmt, structure = coerce_combo("oral", combo["structure"])
persona = coerce_persona(combo.get("persona") or "urban")
if persona not in PERSONA_LABELS:
persona = "urban"
duration = max(FORMAT_DEFAULT_DURATION[fmt], STRUCTURE_MIN_DURATION[structure])
return {
"format": fmt,
"structure": structure,
"persona": persona,
"duration": duration,
"reason": reason,
}
def narration_limit(duration: int) -> int:
"""这一镜旁白的字数上限:秒数 × 5.2,且不超过硬上限 78。"""
return max(1, min(NARRATION_CHARS_HARD_CAP, int(duration * NARRATION_CHARS_PER_SECOND)))
@@ -367,8 +474,12 @@ _CREATIVE_DIRECTION = """
## 出片感与转化感(硬性创作方向)
**先服从用户选定的视频结构。** 用户选的是痛点解决、前后对比、测评验证、场景种草、促销抢购或知识分享中的哪一种,
就只走该结构合同的顺序与说服方式;不能因为“更好写”而擅自换成痛点开场、测评口吻或场景氛围。
下列通用写法仅用于增强已选结构,和结构合同冲突时一律以结构合同为准。
你的脚本不能像商品详情页、说明书或主播念稿;它必须让用户在第一秒看到一个
**正在发生的具体瞬间**。先选一个最能代表目标人群的「时间 + 地点 + 小麻烦/小欲望」,
**正在发生的具体瞬间**。先选一个最能代表典型使用者的「时间 + 地点 + 小麻烦/小欲望」,
全片只围绕这一个情境推进。不要在一条短视频里罗列所有卖点。
### 每一镜都必须有推进,禁止平铺直叙
@@ -487,8 +598,7 @@ def _product_context(project, selling_point_ids: list[str] | None, persona: str
f"品牌:{product.brand or '未填写'}\n"
f"{type_line}"
f"品类:{product.category or '未填写'}\n"
f"目标人群:{product.target_audience or '填写'}\n"
f"人物设定:{persona_text or '未指定,按目标人群自行定一个具体身份'}\n"
f"人物设定:{persona_text or '指定,按商品信息自行定一个具体身份'}\n"
f"人物口吻:{persona_brief or '按人物设定自己定一个具体身份,不要用万能主播腔'}\n"
f"商品描述:{desc}\n"
f"规格:{specs_block}\n"
@@ -572,6 +682,7 @@ def build_agent_messages(
suggested = plan_segment_durations(total, fmt)
shot_n = len(suggested)
stage_plan = structure_stage_plan(structure, shot_n)
extra = (user_prompt or "").strip()
if target_index is not None and base_draft:
existing_n = len(base_draft.get("segments") or [])
@@ -610,24 +721,10 @@ def build_agent_messages(
f"【写法硬约束】有人声的镜口播/对白 {speech_floor}{speech_cap} 字,允许个别镜纯画面;"
f"visual 至少 {VISUAL_CHARS_MIN} 字。\n"
)
structure_line = ""
if structure == "scene":
structure_line = (
"【场景种草骨架】按镜走完:场景建立(商品先不出现)→商品被使用着入场→场景因它更好/软CTA。"
"每镜写出具体时刻+地点+光/声音/物件。口播时每镜仍要说满字数,留白靠停顿不是靠少写字。\n"
)
elif structure == "promo":
structure_line = (
"【促销抢购合规】只可使用商品资料中明确给出的价格、优惠、库存、赠品和活动时间;"
"没有明确数据时,绝不写原价、最低价、限时、限量、领券或赠品。"
"结尾只给一个清楚的购买入口和操作动作,不制造虚假的紧迫感。\n"
)
elif structure == "knowledge":
structure_line = (
"【知识分享骨架】先提出一个与商品相关的选购/使用误区或判断方法,再用实物与步骤证明,"
"最后把商品作为符合该判断的具体实例。只能讲商品资料可支持的事实,"
"禁止编造专业身份、数据、标准或功效承诺。\n"
)
structure_line = STRUCTURE_CONTRACTS[structure]
stage_line = "【镜序验收】" + "".join(
f"{index + 1} 镜:{stage}" for index, stage in enumerate(stage_plan)
) + "。镜数不够时只可合并相邻步骤,顺序不得颠倒,也不得插入其他结构的主线。\n"
combo_line = (
f"严格按已加载的「{PRESENTATION_FORMATS[fmt]} × {VIDEO_STRUCTURES[structure]}」套路写,"
f"不要串成别的结构的套话。\n"
@@ -646,6 +743,7 @@ def build_agent_messages(
f"{writing_line}"
f"{beats_line}"
f"{structure_line}"
f"{stage_line}"
f"{combo_line}"
f"{authenticity_line}"
f"【商品信息】\n{_product_context(project, selling_point_ids, persona)}"
@@ -685,7 +783,7 @@ def build_agent_messages(
"写 visual 时把拆解稿里的景别/机位/运镜/人物动作/表情/音效/背景音乐/字幕/备注"
"折进【声音】和【画面内容】的秒级分镜,不要压成一句画面摘要。"
"原稿写「无 / 不可见 / 听不清」的栏不要编造。"
"钩子、痛点、卖点必须能对上这份商品"
"钩子、已选结构的核心步骤和商品事实必须能对上。"
"每镜旁白要能撑满指定时长;画面必须按秒拆分镜,够导演在 15 秒里切 3–5 刀。\n"
f"{head}"
f"{extra_block}\n"
@@ -695,7 +793,7 @@ def build_agent_messages(
user = (
"【任务】全自动(模式①):仅凭上面的商品事实与前置条件,按指定的表现形式与视频结构套路"
"自动定镜/选 tone/造 entity/填结构骨架。"
"不要另起一个空主题;钩子、痛点、卖点必须能对上这份商品,而不是品类套话。"
"不要另起一个空主题;钩子、已选结构的核心步骤和商品事实必须能对上这份商品,而不是品类套话。"
"每镜旁白要能撑满 15 秒;画面必须按秒拆分镜,够导演在 15 秒里切 3–5 刀。\n"
f"{head}"
f"{extra_block}\n"
@@ -793,6 +891,20 @@ def plan_segment_durations(total_duration: int, presentation_format: str | None
return [SEGMENT_DURATION_MAX] * count
def structure_stage_plan(video_structure: str, segment_count: int) -> list[str]:
"""把选中结构的完整说服路径按当前镜数连续压缩,绝不换成通用痛点模板。"""
stages = list(STRUCTURE_STAGES[video_structure])
count = max(1, int(segment_count or 1))
if count >= len(stages):
return stages + [stages[-1]] * (count - len(stages))
# 前面镜先承接前置步骤,最后一镜必须保留收束/CTA;每个阶段只会向相邻镜合并。
groups: list[list[str]] = [[] for _ in range(count)]
for index, stage in enumerate(stages):
target = min(count - 1, index * count // len(stages))
groups[target].append(stage)
return ["".join(group) for group in groups]
def plan_roles(count: int) -> list[str]:
"""镜数 → role 序列。通用规则:首钩子、次痛点、末 CTA,中间全是卖点。"""
if count <= 1: