全能创作:定妆/台词/换款与剧情方向硬约束,资源可下载

修角色定妆修改不重出图、角色图混入商品、宠物旁白腔、换款同点复读、剧情反转方向漂移,并补会话资源下载与外观/空消息闸门。
This commit is contained in:
Azmat@qq.com
2026-09-20 14:52:51 +08:00
parent f2e8a5f3c1
commit fb3b2904ae
10 changed files with 1149 additions and 142 deletions
+290 -29
View File
@@ -32,6 +32,8 @@ from .creation_presets import (
PLOT_TWIST_STORY_DEPTH_OPTIONS,
apply_image_preset_prompt,
apply_plot_twist_story_contract,
apply_plot_twist_direction_contract,
format_plot_twist_direction_contract,
apply_video_preset_prompt,
is_click_swap_preset,
plot_twist_story_depth,
@@ -110,6 +112,36 @@ def is_plot_twist_conversation(conversation: CreationConversation) -> bool:
return conversation.mode == CreationConversation.Mode.VIDEO and conversation.preset == PLOT_TWIST_PRESET
def plot_twist_selected_direction(conversation: CreationConversation) -> dict:
"""读取用户已选剧情方向(结构化优先,兼容旧会话只有 title/detail)。"""
memory = conversation.memory if isinstance(conversation.memory, dict) else {}
payload = memory.get("plot_twist_story_direction_payload")
if isinstance(payload, dict) and any(
str(payload.get(k) or "").strip() for k in ("title", "conflict", "product_role", "reversal", "detail")
):
return {
"title": str(payload.get("title") or memory.get("plot_twist_story_direction") or "").strip(),
"conflict": str(payload.get("conflict") or "").strip(),
"product_role": str(payload.get("product_role") or "").strip(),
"reversal": str(payload.get("reversal") or "").strip(),
"tone": str(payload.get("tone") or "").strip(),
"detail": str(memory.get("plot_twist_story_direction_detail") or "").strip(),
}
title = str(memory.get("plot_twist_story_direction") or "").strip()
detail = str(memory.get("plot_twist_story_direction_detail") or "").strip()
if not title and not detail:
return {}
return {
"title": title,
"conflict": "",
"product_role": "",
"reversal": "",
"tone": "",
"detail": detail,
}
def is_pain_point_conversation(conversation: CreationConversation) -> bool:
return conversation.mode == CreationConversation.Mode.VIDEO and conversation.preset == PAIN_POINT_PRESET
@@ -756,7 +788,7 @@ def append_click_swap_sequence_gate(conversation: CreationConversation) -> Creat
hint = "先确认要切换的款式和展示顺序。角色会在日常场景中按这个顺序变色换款。"
placeholder = "例如:曜石黑 → 象牙白 → 樱花粉"
else:
hint = "先确认要切换的款式和展示顺序。后续每次手指点击都会严格按这个顺序原位换款。"
hint = "先确认要切换的款式和展示顺序。后续按这个顺序用手指点击触发换款即可,不必强调每次点同一位置。"
placeholder = "例如:黑色 → 白色 → 樱花粉"
return append_message(
conversation,
@@ -837,7 +869,7 @@ def apply_click_swap_mode(conversation: CreationConversation, mode: str) -> str:
if value == "finger":
return (
"商家已选择【只出手指出镜】。不要生成角色定妆图,不要追问出镜人物;"
"后续只围绕固定机位 + 手指点击 + 商品原位换款推进。"
"后续只围绕固定机位 + 手指点击触发换款推进;分镜不要反复写「每次点击同一位置」。"
)
return (
"商家已选择【角色在日常场景中换款】。下一步先锁定出镜角色来源;"
@@ -845,13 +877,32 @@ def apply_click_swap_mode(conversation: CreationConversation, mode: str) -> str:
)
def _character_only_appearance_text(raw: str) -> str:
"""定妆图只保留角色外观描述,去掉商品/卖点/带货指令,避免图里跑出商品。"""
text = str(raw or "").strip()
if not text:
return ""
text = re.sub(
r"(?:商品|产品|品牌|品名|卖点|空气炸锅|猫粮|狗粮|包装|瓶身|机身|SKU|链接|下单|购买)[^。!?\n]{0,40}",
" ",
text,
flags=re.IGNORECASE,
)
text = re.sub(r"[\s,,、;;]{2,}", ",", text).strip(",,。;; ")
return text[:500]
def submit_generated_person_reference(
*,
conversation: CreationConversation,
user,
appearance_prompt: str = "",
) -> CreationMessage:
"""先生成独立人物/宠物角色定妆参考,完成后再由 creation.py 自动建模特并锁定。"""
"""先生成独立人物/宠物角色定妆参考,完成后再由 creation.py 自动建模特并锁定。
定妆图 prompt 只写角色外观,不写入商品名/卖点/用户创作简述,也不带商品参考图,
否则出图模型常会把商品画进角色照。
"""
from .services import enqueue_standalone_images
recent_user = (
@@ -862,13 +913,19 @@ def submit_generated_person_reference(
if getattr(conversation, "created_at", None)
else []
)
brief = "\n".join(reversed([item.strip() for item in recent_user if item and item.strip()]))[:700]
appearance_prompt = (appearance_prompt or "").strip()[:500]
# 仅用于推断宠物品种;绝不拼进最终定妆 prompt。
context_brief = "\n".join(reversed([item.strip() for item in recent_user if item and item.strip()]))[:700]
raw_appearance = (appearance_prompt or "").strip()[:500]
appearance_only = _character_only_appearance_text(raw_appearance)
is_pet = is_pet_preset(conversation.preset)
no_product = (
"画面中只允许出现角色本身与简洁背景,禁止出现任何商品、包装、瓶罐、袋装、纸盒、"
"电子产品、食品零食、广告道具或手持卖品;不要商品特写,不要把商品画进画面。"
)
if is_pet:
# 智能推断宠物类别:用户指定 > 商品及需求上下文 > 默认萌宠
context_text = f"{appearance_prompt} {brief} {conversation.preset}"
# 智能推断宠物类别:用户指定 > 商品及需求上下文 > 默认萌宠(只影响品种提示,不写入商品名)
context_text = f"{raw_appearance} {context_brief} {conversation.preset}"
for ref in locked_product_references(conversation):
context_text += " " + str(ref.get("name") or "")
if any(w in context_text for w in ("狗", "犬", "咬胶", "磨牙", "骨头", "汪", "狗粮", "犬粮", "幼犬", "成犬", "中大型犬", "小型犬")):
@@ -885,11 +942,10 @@ def submit_generated_person_reference(
f"可搭配精致简约的拟人小配饰(如可爱小领结、小方巾或背带,契合萌宠调性),"
f"写实摄影风格,电影级柔和摄影棚光影,简洁纯净背景,"
f"画面中只出现这一只可爱的宠物动物主角,绝对不要出现真人人类!不要文字、水印、拼图或变形。"
f"{no_product}"
)
if appearance_prompt:
prompt += f" 用户指定的宠物外观与品种:{appearance_prompt}。严格保留这些宠物外观要求。"
if brief:
prompt += f" 参考用户需求:{brief}"
if appearance_only:
prompt += f" 用户指定的宠物外观与品种:{appearance_only}。严格保留这些宠物外观要求。"
label = "正在生成宠物角色参考"
else:
prompt = (
@@ -897,13 +953,10 @@ def submit_generated_person_reference(
"只出现一位成年人物,正面或轻微三分之四角度,中近景,表情自然,"
"五官、发型、肤色、身形和服装细节清晰,简洁中性背景,写实摄影,"
"不要文字、水印、拼图、多人、遮挡脸部或夸张滤镜。"
f"{no_product}"
)
if appearance_prompt:
prompt += f" 用户指定的人物外观:{appearance_prompt}。严格保留这些外观要求。"
if conversation.preset:
prompt += f" 适配视频预设:{conversation.preset}。"
if brief:
prompt += f" 参考用户需求:{brief}"
if appearance_only:
prompt += f" 用户指定的人物外观:{appearance_only}。严格保留这些外观要求。"
label = "正在生成人物参考"
tasks = enqueue_standalone_images(
@@ -938,6 +991,7 @@ def submit_generated_person_reference(
)
def _step_confirm_payload(step: str) -> dict:
return {
"interaction": "step_confirm",
@@ -1046,6 +1100,9 @@ def emit_prompt_gate(
return []
prompt = apply_product_voice_visual_guard(conversation, prompt)
prompt = apply_product_reality_guard(prompt)
prompt = apply_clothing_video_guard(conversation, prompt)
prompt = apply_product_appearance_guard(conversation, prompt)
prompt = apply_pet_dialogue_guard(conversation, prompt)
prompt = apply_video_platform_safety_guard(prompt)
messages = []
prompt_file = append_message(
@@ -1085,6 +1142,9 @@ def emit_final_confirm_gate(
return None
prompt = apply_product_voice_visual_guard(conversation, prompt)
prompt = apply_product_reality_guard(prompt)
prompt = apply_clothing_video_guard(conversation, prompt)
prompt = apply_product_appearance_guard(conversation, prompt)
prompt = apply_pet_dialogue_guard(conversation, prompt)
prompt = apply_video_platform_safety_guard(prompt)
credits = 0
if context is not None:
@@ -1195,8 +1255,11 @@ video_prompt 是交给出片模型的完整制作文件,不是方案摘要、
- 商品演示先过“真实用途与状态”检查:只展示商品在其已知用途和已提供事实范围内的正常、完整状态;
禁止无依据出现破损、漏液、渗水、失效、异常变形、脏污,或把商品拿去做超出用途的压力测试。
不确定防水、防漏、承重、耐热、容量、材质等关键能力时,不编造测试和结果;要么问用户,要么改用可观察的正常使用动作。
- 服装/服饰类商品:人物从第一镜起就已经穿着当前商品,只拍上身效果、版型、面料、走动与搭配;
不要写穿衣服、套袖、扣扣、拉拉链、脱换过程等穿戴动作镜头。换款用瞬间切换或已穿好的下一套展示。
- 画面中不出现新增字幕、花字、标题贴片、弹幕、角标、水印、购物浮层或说明性文字;口播只存在于声音,包装本身原有印刷字除外。
- 结尾单列「全片一致性与禁用项」:用正向、可执行的句子重申角色、商品、场景、光线、服装/材质的连续性。
有商品参考图时,商品颜色与外形必须写明「以参考商品图为准」,禁止另写冲突颜色。
- 用户说「商品说话 / 商品自述 / 商品拟人」时,默认无脸拟人:商品声音是画外角色声,商品本体不做口型、不新增卡通五官;性格只通过整体倾斜、转向、弹跳、进退、镜头和音效表达。只有用户明确要求可见卡通五官时才例外。
- 审核安全必须在第一次生成时完成,不能依赖提交前清洗。没有锁定人物素材时,人物只写「成年女性 / 成年男性 / 成年人」,不要自创精确年龄区间,不使用带幼态联想的称呼、音色或人设。服装写日常得体,默认平视、自然俯拍或尊重主体的正面构图,不强调身体局部。
- 最终 video_prompt 只使用正向安全描述。不要把平台风险类别、禁用词或用户原始高风险措辞逐项写进 Prompt,否定句、免责声明和「禁止出现某词」也不能照抄;先在内部把冲突改写成成年人之间积极、友善的日常互动,再输出改写后的可拍内容。
@@ -1232,6 +1295,45 @@ PRODUCT_REALITY_GUARD = (
"材质等能力拍成已被验证的结果。若关键信息不足,使用可观察的正常操作替代夸张测试。"
)
# 服装类视频禁止「穿衣过程」镜头:模特从第一帧就已穿着商品,只展示上身效果。
CLOTHING_VIDEO_NO_DRESSING_GUARD = (
"【服装类镜头约束·最高优先级】人物从第一镜起就已经穿着当前服装商品,"
"只展示上身效果、版型线条、面料质感、走动垂坠与场景搭配。"
"不要出现穿衣服、往身上套、伸袖、套头、拉拉链、扣纽扣、系腰带、脱衣换装过程等穿戴动作镜头;"
"换款或换装用瞬间切换、转场或已穿好的下一套直接展示,不要拍穿衣过程。"
)
# 已锁定商品参考图时:文字描述不得改写商品颜色/外形;与参考图冲突时以图为准。
PRODUCT_APPEARANCE_GUARD = (
"【商品外观锁定·最高优先级】商品颜色、材质、外形、面板、按键、把手与整体结构必须与用户上传的商品参考图完全一致。"
"禁止把参考图里的商品改成其他颜色或近似款(例如把黑色写成白色、把深色写成浅色)。"
"若前文颜色或外形描述与参考图冲突,一律以参考图为准;不确定时只写「严格按参考商品图外观」,不要猜测颜色。"
)
PET_DIALOGUE_GUARD = (
"【宠物拟人台词·最高优先级】片中人声必须是出镜宠物的第一人称说话,像这只宠物自己在开口:"
"短句、口语、带性格,可用贴合物种的自称;声音从宠物角色发出(可配合口型或明确的角色声)。"
"禁止旁白解说、第三人称讲述、达人口播腔、画外音旁白,或「今天给大家介绍/测评」类带货开场。"
)
_CLOTHING_CATEGORY_HINTS = (
"服饰", "女装", "男装", "服装", "内衣", "裤装", "裙装", "上衣", "外套", "鞋服",
"连衣裙", "半身裙", "衬衫", "卫衣", "毛衣", "夹克", "大衣", "羽绒服", "风衣",
"牛仔裤", "阔腿裤", "休闲裤", "西装", "套装", "吊带", "背心", "T恤", "t恤",
"短袖", "长袖", "针织", "打底", "睡衣", "家居服", "泳装", "泳衣",
)
_CLOTHING_DRESSING_REWRITES: tuple[tuple[str, str], ...] = (
(r"把(?:这款|该|那件|这件)?(?:衣服|服装|外套|上衣|裙子|裤子)?穿上", "已穿着该服装展示上身效果"),
(r"(?:开始|正在|慢慢)?穿(?:上|好)?(?:这款|该|那件|这件)?(?:衣服|服装|外套|上衣|裙子|裤子)", "已穿着该服装自然展示"),
(r"穿衣服(?:的)?(?:过程|镜头|动作)?", "已穿好服装的上身展示"),
(r"(?:套上|换上|穿进)(?:袖子|衣袖|外套|上衣|裙子|裤子|衣服)", "已穿着该服装"),
(r"(?:拉上|拉开)拉链(?:穿上|穿好)?", "已穿着该服装,拉链细节特写"),
(r"(?:扣上|解开)纽扣", "已穿着该服装,门襟与纽扣细节特写"),
(r"脱下(?:旧衣|旧的)?(?:衣服|外套|上衣)?(?:再|后)?(?:换|穿)", "瞬间切换为已穿好的下一套"),
(r"换装过程中.{0,12}(?:穿|套|脱)", "瞬间切换为已穿好的下一套"),
)
# 这一层不是替代平台审核,而是在脚本落成最终 video_prompt 前先把最容易被视频模型
# 拦截的明确高风险表述改成等价的安全叙事。模型仍会收到下方完整约束,避免只靠关键词替换。
VIDEO_PLATFORM_SAFETY_GUARD = (
@@ -1308,6 +1410,88 @@ def apply_product_reality_guard(prompt: str) -> str:
return f"{base}\n{PRODUCT_REALITY_GUARD}".strip()
def _clothing_context_blob(conversation: CreationConversation | None, prompt: str = "") -> str:
"""汇总商品名、品类、预设与 prompt,用于判断是否服装类视频。"""
parts = [str(prompt or "")]
if conversation is None:
return "\n".join(parts)
parts.append(str(conversation.preset or ""))
memory = conversation.memory if isinstance(conversation.memory, dict) else {}
for key in ("product_name", "product_brand", "product_brand_and_name", "selling_point", "pending_video_prompt"):
parts.append(str(memory.get(key) or ""))
for ref in locked_product_references(conversation):
if not isinstance(ref, dict):
continue
parts.append(str(ref.get("name") or ""))
parts.append(str(ref.get("category") or ""))
parts.append(str(ref.get("title") or ""))
return "\n".join(parts)
def is_clothing_video_context(conversation: CreationConversation | None, prompt: str = "") -> bool:
"""服装/服饰类视频:含鱼眼换装预设,或商品名/品类命中服装关键词。"""
if conversation is not None and str(conversation.preset or "").strip() == "鱼眼换装":
return True
blob = _clothing_context_blob(conversation, prompt)
lower = blob.lower()
return any(hint.lower() in lower for hint in _CLOTHING_CATEGORY_HINTS)
def apply_clothing_video_guard(
conversation: CreationConversation | None,
prompt: str,
) -> str:
"""服装类视频去掉穿衣过程镜头,改为已穿好上身展示;可重复调用。"""
base = str(prompt or "").strip()
if not base or not is_clothing_video_context(conversation, base):
return base
for pattern, replacement in _CLOTHING_DRESSING_REWRITES:
base = re.sub(pattern, replacement, base)
if CLOTHING_VIDEO_NO_DRESSING_GUARD not in base:
base = f"{base}\n{CLOTHING_VIDEO_NO_DRESSING_GUARD}".strip()
return base
def apply_product_appearance_guard(
conversation: CreationConversation | None,
prompt: str,
) -> str:
"""已锁定商品参考图时,出图/出片指令追加外观锁定,避免文字把黑商品写成白。"""
base = str(prompt or "").strip()
if not base or conversation is None:
return base
if not has_locked_product_reference(conversation):
return base
if PRODUCT_APPEARANCE_GUARD in base:
return base
return f"{base}\n{PRODUCT_APPEARANCE_GUARD}".strip()
def apply_pet_dialogue_guard(
conversation: CreationConversation | None,
prompt: str,
) -> str:
"""AI 宠物拟人:台词必须是宠物第一人称,禁止旁白/口播/画外音感。"""
base = str(prompt or "").strip()
if not base or conversation is None:
return base
if not is_pet_preset(conversation.preset):
return base
# 轻量改写常见旁白开场,避免最终模型照念
rewrites = (
(r"今天给大家介绍", "我今天发现"),
(r"给大家推荐", "我超喜欢"),
(r"旁白[::]", "宠物说:"),
(r"画外音[::]", "宠物说:"),
(r"口播[::]", "宠物说:"),
)
for pattern, replacement in rewrites:
base = re.sub(pattern, replacement, base)
if PET_DIALOGUE_GUARD not in base:
base = f"{base}\n{PET_DIALOGUE_GUARD}".strip()
return base
def apply_video_platform_safety_guard(prompt: str) -> str:
"""将最终出片指令收束为较不易触发视频模型审核的安全版本,且可重复调用。"""
base = str(prompt or "").strip()
@@ -1714,6 +1898,9 @@ def apply_restart_intent(conversation: CreationConversation) -> int:
memory.pop("product_source_resolved", None)
memory.pop("pain_point_direction_ready", None)
memory.pop("pain_point_direction", None)
memory.pop("plot_twist_story_direction", None)
memory.pop("plot_twist_story_direction_detail", None)
memory.pop("plot_twist_story_direction_payload", None)
memory.pop("click_swap_ready", None)
memory.pop("click_swap_sequence", None)
memory.pop("click_swap_mode", None)
@@ -2195,7 +2382,9 @@ def tool_schemas(context: AgentContext, *, allow_plan: bool = True) -> list[dict
"name": "generate_image",
"description": (
"生成图片。prompt 必须是完整、可独立执行的画面描述(主体/动作/环境/光线/构图/风格),"
"不要写成对用户说的话。已 @ 引用的素材会自动作为参考图带上,不用在 prompt 里重复描述它们的外观。"
"不要写成对用户说的话。已 @ 引用的素材会自动作为参考图带上。"
"若已锁定商品参考图:商品颜色、材质、外形必须与参考图一致;看不清就写「严格按参考商品图外观」,"
"禁止凭印象编造颜色(例如把黑色空气炸锅写成白色)。人物参考图同理,外貌以图为准。"
),
"parameters": {
"type": "object",
@@ -2588,6 +2777,7 @@ def _run_generate_image(context: AgentContext, args: dict) -> tuple[dict, list]:
if not prompt:
raise AgentError("生成失败:模型没有给出画面描述")
prompt = apply_image_preset_prompt(context.conversation.preset, prompt)
prompt = apply_product_appearance_guard(context.conversation, prompt)
params = context.conversation.params or {}
resolved = resolve_refs(context.team, context.conversation.pinned_refs or [])
@@ -2619,12 +2809,19 @@ def _video_submit_params(context: AgentContext, prompt: str) -> tuple[dict, list
prompt = apply_video_preset_prompt(context.conversation.preset, prompt, click_swap_mode=click_swap_mode(context.conversation))
prompt = apply_product_voice_visual_guard(context.conversation, prompt)
prompt = apply_product_reality_guard(prompt)
prompt = apply_clothing_video_guard(context.conversation, prompt)
prompt = apply_product_appearance_guard(context.conversation, prompt)
prompt = apply_pet_dialogue_guard(context.conversation, prompt)
prompt = apply_video_platform_safety_guard(prompt)
prompt = apply_plot_twist_story_contract(
context.conversation.preset,
active_plot_twist_story_depth(context.conversation),
prompt,
)
if is_plot_twist_conversation(context.conversation):
selected = plot_twist_selected_direction(context.conversation)
if selected:
prompt = apply_plot_twist_direction_contract(prompt, **selected)
prompt = apply_person_identity_guard(prompt, resolved.references)
duration = resolve_smart_video_duration(context.conversation, prompt=prompt)
# resolve_smart_video_duration 可能为了完整脚本切到支持长时长的模型,必须重新取参数。
@@ -2871,7 +3068,8 @@ def _attach_ref_images(messages: list[dict], image_urls: list[str]) -> list[dict
return messages
note = (
f"【参考图·请亲眼看】下面 {len(image_urls)} 张是用户锁定的素材。"
"人物的性别、年龄段、发型、服装必须以图为准;看不清再问用户,禁止凭文件名猜测性别。"
"人物的性别、年龄段、发型、服装必须以图为准;商品的颜色、材质、外形、面板与结构也必须以图为准。"
"看不清再问用户;禁止凭文件名、品类常识或默认印象猜测性别或把商品写成别的颜色(例如把黑色写成白色)。"
)
out = [dict(message) for message in messages]
index = next((i for i in range(len(out) - 1, -1, -1) if out[i].get("role") == "user"), None)
@@ -3025,6 +3223,8 @@ def build_system_prompt(context: AgentContext, *, allow_plan: bool = True, has_c
"【出图】",
"- 决定出图时必须调用 generate_image,不要只口头说「我这就出图」。",
"- 一次用户消息只出一轮;张数用会话已定参数,不要自己加张。",
"- 已锁定商品参考图时:prompt 里商品颜色、材质、外形必须与参考图一致;"
"看不清就写「严格按参考商品图外观」,禁止凭印象编造颜色。",
])
if not allow_plan:
lines.extend([
@@ -3065,9 +3265,16 @@ def build_system_prompt(context: AgentContext, *, allow_plan: bool = True, has_c
else:
lines.append(plot_twist_story_contract(depth))
memory = conversation.memory if isinstance(conversation.memory, dict) else {}
selected_direction = str(memory.get("plot_twist_story_direction") or "").strip()
if selected_direction:
lines.append(f"用户已选剧情方向:{selected_direction}。直接围绕此方向写策略,不要重发方向卡。")
selected = plot_twist_selected_direction(conversation)
if selected:
contract = format_plot_twist_direction_contract(**{
k: selected.get(k, "") for k in ("title", "conflict", "product_role", "reversal", "tone", "detail")
})
lines.append(contract)
lines.append(
"已选定剧情方向:策略卡的创作方向、方案时间轴和 video_prompt 必须严格按上方冲突→商品作用→反转展开;"
"禁止另起一套常见带货桥段;不要重发方向卡。"
)
else:
lines.append(
"【强制下一步】现在必须调用 present_story_directions,直接展示 3 张完整剧情方向卡。"
@@ -3095,6 +3302,29 @@ def build_system_prompt(context: AgentContext, *, allow_plan: bool = True, has_c
delivery_contract = video_preset_delivery_contract(conversation.preset, click_swap_mode=click_swap_mode(conversation)) if context.is_video else ""
if delivery_contract:
lines.append(f"【当前预设必须贯穿脚本与出片】{delivery_contract}")
if context.is_video and is_clothing_video_context(conversation):
lines.append(
"【服装类视频硬性要求】人物从第一镜起就已经穿着商品;只拍上身效果、版型、面料与搭配。"
"禁止写穿衣服、套袖、扣扣、拉拉链、脱换过程等穿戴动作镜头;换款用瞬间切换或已穿好的下一套。"
)
if context.is_video and is_click_swap_preset(conversation.preset) and click_swap_mode(conversation) != "character":
lines.append(
"【点击换款分镜硬性要求】换款由手指点击触发即可;撰写 video_prompt / 方案分镜时,"
"禁止反复写「每次点击同一位置 / 点同一点 / 手指点在完全相同的位置」;"
"各镜重点写款式变化与节奏,点击动作点到为止。"
)
if context.is_video and is_pet_preset(conversation.preset):
lines.append(
"【宠物拟人台词硬性要求】策略、方案和 video_prompt 里的人声必须是宠物第一人称开口,"
"像这只宠物自己在说话;禁止旁白、口播腔、画外音解说或第三人称讲述。"
)
if context.is_video:
lines.append(
"【角色定妆图硬性要求】生成人物/宠物定妆参考图时,prompt 只写角色外貌、发型、服装气质与表情;"
"禁止写商品名、卖点、包装或使用动作;出图画面中不得出现任何商品、瓶罐、袋装或手持卖品。"
"用户对已出定妆图提出修改(如短发、御姐气质)时,必须真正重新提交定妆出图;"
"禁止只在聊天里写「我重新生成…prompt:…」而不调用出图。"
)
resolved = resolve_refs(context.team, conversation.pinned_refs or [])
if resolved.facts:
@@ -3103,7 +3333,8 @@ def build_system_prompt(context: AgentContext, *, allow_plan: bool = True, has_c
lines.append(
"以上素材的参考图会附给你看,出片时也会自动锁人锁物。"
"人物性别、年龄段、发型、服装、商品颜色外形必须以图为准;"
"图上看不清或没附图时,必须问用户,禁止凭文件名猜测男女。"
"禁止凭品类常识或默认印象把商品写成别的颜色(例如参考图是黑色却写成白色)。"
"图上看不清或没附图时,必须问用户,禁止凭文件名猜测男女或颜色。"
)
memory = conversation.memory or {}
people = locked_person_references(conversation)
@@ -3531,10 +3762,10 @@ def apply_click_swap_plan_card(
}
return {
**card,
"usp": f"手指逐次点击,商品按「{sequence}」在原位连续换款",
"usp": f"手指点击触发,商品按「{sequence}」连续换款",
"points": [
"固定机位、背景、光线与商品中心位置",
"每次换款都由一次清楚的手指点击触发",
"固定机位、背景与光线,商品构图稳定",
"换款由手指点击触发,分镜不反复写同一点击位置",
f"严格按「{sequence}」逐款展示,结尾给出全款式总览",
],
"timeline": [
@@ -3542,19 +3773,19 @@ def apply_click_swap_plan_card(
"start": 0,
"end": first_end,
"stage": "首款定帧",
"desc": "固定机位建立首款,商品位置、尺寸、角度和背景作为后续唯一基准。",
"desc": "固定机位建立首款,机位、背景与商品构图作为后续基准。",
},
{
"start": first_end,
"end": second_end,
"stage": "首次点击换款",
"desc": "手指清晰点击商品,接触瞬间在原位 match cut 为下一款。",
"desc": "手指点击商品触发换款,接触后以干净 match cut 切到下一款;不写点击同一位置。",
},
{
"start": second_end,
"end": third_end,
"stage": "按序连续换款",
"desc": f"按「{sequence}」继续一触一换;机位、构图、商品比例和光线完全不变。",
"desc": f"按「{sequence}」继续点击触发换款;机位、构图与光线保持稳定,不反复描写点击位置。",
},
{
"start": third_end,
@@ -4369,6 +4600,15 @@ def _dispatch_tool(
"_events": [{"type": "message", "message": _message_payload(gate)}],
}, True
memory = context.conversation.memory if isinstance(context.conversation.memory, dict) else {}
if context.is_video and is_plot_twist_conversation(context.conversation) and not plot_twist_selected_direction(context.conversation):
return {
"payload": {
"error": (
"剧情反转带货必须先选定剧情方向:调用 present_story_directions 展示 3 张方向卡,"
"等用户选择后再写策略。不得自行编造未选中的故事桥段。"
)
}
}, False
if context.is_video and is_pain_point_conversation(context.conversation) and not memory.get("selling_point_ready"):
return {
"payload": {
@@ -4450,12 +4690,25 @@ def _dispatch_tool(
)
video_prompt = apply_product_voice_visual_guard(context.conversation, video_prompt)
video_prompt = apply_product_reality_guard(video_prompt)
video_prompt = apply_clothing_video_guard(context.conversation, video_prompt)
video_prompt = apply_product_appearance_guard(context.conversation, video_prompt)
video_prompt = apply_pet_dialogue_guard(context.conversation, video_prompt)
video_prompt = apply_video_platform_safety_guard(video_prompt)
video_prompt = apply_plot_twist_story_contract(
context.conversation.preset,
active_plot_twist_story_depth(context.conversation),
video_prompt,
)
if is_plot_twist_conversation(context.conversation):
selected = plot_twist_selected_direction(context.conversation)
if selected:
video_prompt = apply_plot_twist_direction_contract(video_prompt, **selected)
else:
return {
"payload": {
"error": "剧情反转带货必须先选定剧情方向,再写方案。请先 present_story_directions 让用户选择。"
}
}, False
card = _coerce_plan_card_args(args if isinstance(args, dict) else {})
if not card["usp"] or not card["points"]:
return {
@@ -4533,12 +4786,19 @@ def _dispatch_tool(
video_prompt = apply_video_preset_prompt(context.conversation.preset, video_prompt, click_swap_mode=click_swap_mode(context.conversation))
video_prompt = apply_product_voice_visual_guard(context.conversation, video_prompt)
video_prompt = apply_product_reality_guard(video_prompt)
video_prompt = apply_clothing_video_guard(context.conversation, video_prompt)
video_prompt = apply_product_appearance_guard(context.conversation, video_prompt)
video_prompt = apply_pet_dialogue_guard(context.conversation, video_prompt)
video_prompt = apply_video_platform_safety_guard(video_prompt)
video_prompt = apply_plot_twist_story_contract(
context.conversation.preset,
active_plot_twist_story_depth(context.conversation),
video_prompt,
)
if is_plot_twist_conversation(context.conversation):
selected = plot_twist_selected_direction(context.conversation)
if selected:
video_prompt = apply_plot_twist_direction_contract(video_prompt, **selected)
prompt_messages = emit_prompt_gate(context.conversation, video_prompt)
if not prompt_messages:
return {"payload": {"error": "无法准备出片指令"}}, False
@@ -4559,6 +4819,7 @@ def _dispatch_tool(
if not prompt:
return {"payload": {"error": "生成失败:模型没有给出画面描述"}}, False
prompt = apply_image_preset_prompt(context.conversation.preset, prompt)
prompt = apply_product_appearance_guard(context.conversation, prompt)
# 出图也走确认卡:用户先看当前模型/比例/张数,点了才提交。
credits = estimate_image_credits(context)
confirm = append_message(