全能创作与模特库收口:品牌品名输入、确认角色循环、换款落点与鱼眼模板

补充品牌品名改为预填输入框且空骨架不跳步;痛点确认角色后「继续完善方案」可继续推进;点击换款要求每次落点不同;鱼眼换装改用小云雀连环换装 prompt 模板;模特详情标签自动换行。
This commit is contained in:
Azmat@qq.com
2026-09-21 16:47:50 +08:00
parent 0bd1db6bf9
commit 9f15b71f19
6 changed files with 395 additions and 62 deletions
+118 -15
View File
@@ -37,11 +37,14 @@ from .creation_presets import (
format_plot_twist_direction_contract,
apply_video_preset_prompt,
is_click_swap_preset,
is_fish_eye_outfit_preset,
fish_eye_outfit_prompt_template,
plot_twist_story_depth,
plot_twist_story_contract,
preset_guidance,
preset_workflow_guidance,
video_preset_delivery_contract,
scrub_click_swap_same_position_wording,
)
from .mentions import TYPE_LABELS, infer_field_types, resolve_refs, search_mentions
from .models import CreationConversation, CreationMessage, ModelConfig
@@ -769,12 +772,65 @@ def has_locked_product_reference(conversation: CreationConversation) -> bool:
return len(locked_product_references(conversation)) > 0
PRODUCT_BRAND_EMPTY_TEMPLATE = "品牌是:,商品名是:"
def is_incomplete_product_brand_answer(text: str) -> bool:
"""空模板或只有「品牌是:/商品名是:」骨架、没有真实内容时视为未填完。"""
raw = (text or "").strip()
if not raw:
return True
skeletons = {
PRODUCT_BRAND_EMPTY_TEMPLATE,
"品牌是:,商品名是:",
"品牌是:,品名是:",
"品牌是:,品名是:",
}
if raw in skeletons:
return True
cleaned = raw
for sk in skeletons:
cleaned = cleaned.replace(sk, "")
cleaned = (
cleaned.replace("品牌是:", "")
.replace("品牌是:", "")
.replace("商品名是:", "")
.replace("商品名是:", "")
.replace("品名是:", "")
.replace("品名是:", "")
.replace("", "")
.replace(",", "")
.strip()
)
if not cleaned:
return True
bm = re.search(r"品牌[:是为]\s*([^\n,。!!]+)", raw)
nm = re.search(r"(?:品名|商品名|产品名)[::是为]\s*([^\n,。!!]+)", raw)
if bm or nm:
brand = (bm.group(1).strip() if bm else "")
name = (nm.group(1).strip() if nm else "")
token = re.compile(r"[\u4e00-\u9fffA-Za-z0-9]{2,}")
if not token.search(brand) and not token.search(name) and not token.search(cleaned):
return True
return False
def product_info_needs_confirmation(conversation: CreationConversation, user_text: str = "") -> bool:
"""针对本地上传的商品素材,在对话开始或方案前必须确认品牌与具体品名。"""
if conversation.preset not in _PRODUCT_REQUIRED_PRESETS:
return False
memory = conversation.memory if isinstance(conversation.memory, dict) else {}
if memory.get("product_info_resolved") or memory.get("product_brand") or memory.get("product_name"):
brand = str(memory.get("product_brand") or "").strip()
name = str(memory.get("product_name") or "").strip()
combined = str(memory.get("product_brand_and_name") or "").strip()
token = re.compile(r"[\u4e00-\u9fffA-Za-z0-9]{2,}")
has_real_brand = bool(token.search(brand))
has_real_name = bool(token.search(name))
has_real_combined = bool(combined) and not is_incomplete_product_brand_answer(combined)
if memory.get("product_info_resolved") and (has_real_brand or has_real_name or has_real_combined):
return False
if has_real_brand or has_real_name:
return False
# 如果已锁定的首个商品是正式商品库商品(已自带品牌和品名),无需重复询问
first_prod = next(
@@ -827,7 +883,11 @@ def append_product_info_gate(conversation: CreationConversation) -> CreationMess
"reply_hint": "输入品牌和品名,例如「品牌XX,品名YY」…",
"reply_options": [
{"label": "按图片内容创作", "text": "直接根据图片内容设计品牌与品名,不用再问"},
{"label": "我来补充品牌品名", "text": "品牌是:,商品名是:"},
{
"label": "我来补充品牌品名",
"text": PRODUCT_BRAND_EMPTY_TEMPLATE,
"action": "compose",
},
],
},
)
@@ -923,7 +983,7 @@ def append_click_swap_sequence_gate(conversation: CreationConversation) -> Creat
hint = "先确认要切换的款式和展示顺序。角色会在日常场景中按这个顺序变色换款。"
placeholder = "例如:曜石黑 → 象牙白 → 樱花粉"
else:
hint = "先确认要切换的款式和展示顺序。后续按这个顺序用手指点击触发换款即可,不必强调每次点同一位置"
hint = "先确认要切换的款式和展示顺序。后续按这个顺序用手指点击触发换款;每一次点击落点都要与上一次不同"
placeholder = "例如:黑色 → 白色 → 樱花粉"
return append_message(
conversation,
@@ -1004,7 +1064,7 @@ def apply_click_swap_mode(conversation: CreationConversation, mode: str) -> str:
if value == "finger":
return (
"商家已选择【只出手指出镜】。不要生成角色定妆图,不要追问出镜人物;"
"后续只围绕固定机位 + 手指点击触发换款推进;分镜不要反复写「每次点击同一位置」。"
"后续只围绕固定机位 + 手指点击触发换款推进;每一次换款点击落点必须与上一次不同,禁止写「每次点击同一位置」。"
)
return (
"商家已选择【角色在日常场景中换款】。下一步先锁定出镜角色来源;"
@@ -1927,8 +1987,11 @@ def default_reply_options(
{"label": "从素材里判断", "text": "先根据我上传的素材判断可表达的卖点"},
{"label": "先只突出一个点", "text": "先围绕一个最核心的卖点创作"},
]
# 没有足够语境时只留自由输入,不伪造“继续/改卖点”这种人机按钮
return []
# 已有创作上下文但未命中更具体的追问时,给一个可识别的「继续完善方案」按钮
# 避免只剩文案引导、用户手输后又不算创作意图而卡住。
return [
{"label": "继续完善方案", "text": "继续完善方案"},
]
kind = "短视频" if is_video else "商品图"
return [
{"label": f"帮我做一条{kind}" if is_video else "帮我做一张商品图", "text": f"帮我做一条{kind}" if is_video else "帮我做一张商品图"},
@@ -2010,7 +2073,7 @@ _CREATIVE_INTENT_RE = re.compile(
r"帮我做|帮我拍|帮我出|帮我写|帮我改|帮我生成|"
r"创作[一两]?[条个张]?|创作(?:一条|个|短)?|"
r"做[一两]?[条个张](?:视频|片|图|广告)?|拍[一两]?[条个张](?:视频|片|图|广告)?|"
r"出[一两]?[条个张](?:视频|片|图|广告)?|出片|出图|出方案|写方案|改方案|重写方案|重新写|"
r"出[一两]?[条个张](?:视频|片|图|广告)?|出片|出图|出方案|写方案|改方案|完善方案|继续完善|继续创作|重写方案|重新写|"
r"生成(?:一下|一张|几张|一条)?(?:视频|图|片|广告|脚本)?|做条|做个片|短视频|带货视频|短广告|广告片|带货片|"
r"换卖点|改卖点|换剧情|改剧情|重做|重新出|重新来|再来一次|从头开始|重新做|按这个出|确认出片|"
r"分镜|脚本|口播稿|storyboard|拟人|"
@@ -2047,12 +2110,20 @@ def has_creative_intent(user_text: str, refs: list | None = None) -> bool:
_CONTINUE_INTENT_RE = re.compile(
r"^\s*(继续|继续做|接着|接着做|往下做|开始吧|开做吧|就这样|就按这个|按这个来|照这个做|直接做|直接来)[吧啊呀呢。.!!]*\s*$"
r"^\s*("
r"继续|继续做|接着|接着做|往下做|开始吧|开做吧|就这样|就按这个|按这个来|照这个做|直接做|直接来|"
r"继续完善方案|完善方案|继续创作|继续推进|往下推进|"
r"按当前描述继续|按这个继续|没有其他调整[,,]?按当前描述继续|"
r"使用这[个位只]角色继续创作|使用这[个位只]宠物角色继续创作"
r")[吧啊呀呢。.!]*\s*$"
)
def is_continue_intent(user_text: str) -> bool:
"""已有创作上下文时,这些短句是在授权继续,不是闲聊。"""
"""已有创作上下文时,这些短句是在授权继续,不是闲聊。
含系统引导里的继续完善方案若不识别,用户一点就又回到同一句引导,死循环
"""
return bool(_CONTINUE_INTENT_RE.match((user_text or "").strip()))
@@ -3669,10 +3740,19 @@ def build_system_prompt(context: AgentContext, *, allow_plan: bool = True, has_c
)
if context.is_video and is_click_swap_preset(conversation.preset) and click_swap_mode(conversation) != "character":
lines.append(
"【点击换款分镜硬性要求】换款由手指点击触发即可;撰写 video_prompt / 方案分镜时,"
"禁止反复写「每次点击同一位置 / 点同一点 / 手指点在完全相同的位置」"
"各镜重点写款式变化与节奏,点击动作点到为止。"
"【点击换款分镜硬性要求】换款由手指点击触发;撰写 video_prompt / 方案分镜时,"
"每一次换款的点击落点必须与上一次不同(点商品的不同区域/角点)"
"禁止写或暗示「每次点击同一位置 / 点同一点 / 手指始终点同一处」;"
"各镜要写明本次点击位置不同于上次,并重点写款式变化与节奏。"
)
if context.is_video and is_fish_eye_outfit_preset(conversation.preset):
lines.append(
"【鱼眼换装 Prompt 硬性要求】write_plan / write_prompt 的 video_prompt 必须按小云雀「鱼眼连环换装」模板撰写:"
"时长按套数估算、9:16、超广角鱼眼风格、全片单一机位节拍换装、转场瞬间切装、主体只取五官发型妆容姿态、"
"服装状态机 S0→SN、无对白、转场配快门/whoosh;禁止穿衣过程与口播。"
"不要套用通用「制作级分镜四行+人声」长文模板。"
)
lines.append(fish_eye_outfit_prompt_template())
if context.is_video and is_pet_preset(conversation.preset):
lines.append(
"【宠物拟人台词硬性要求】策略、方案和 video_prompt 里的人声必须是宠物第一人称开口,"
@@ -4125,7 +4205,7 @@ def apply_click_swap_plan_card(
"usp": f"手指点击触发,商品按「{sequence}」连续换款",
"points": [
"固定机位、背景与光线,商品构图稳定",
"换款由手指点击触发,分镜不反复写同一点击位置",
"换款由手指点击触发,每一次点击落点必须与上一次不同",
f"严格按「{sequence}」逐款展示,结尾给出全款式总览",
],
"timeline": [
@@ -4139,13 +4219,13 @@ def apply_click_swap_plan_card(
"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,
@@ -4676,6 +4756,27 @@ def iter_creation_agent_events(
if stop:
break
# 强制创作回合若模型既没落闸门也没写正文,补一条可点的「继续完善方案」引导,
# 且该文案已被 is_continue_intent 识别,避免用户再点又回到空引导死循环。
if force_creative_turn and not turn_has_gate and last_text_bubble is None:
nudge = append_message(
conversation,
role="assistant",
text=(
"角色已锁定。接下来直接推进创作方案;"
"也可以回复「继续完善方案」让我继续写策略。"
),
payload={
"reply_hint": "可以回复「继续完善方案」,也可以直接说卖点或痛点方向。",
"reply_options": [
{"label": "继续完善方案", "text": "继续完善方案"},
{"label": "先定痛点方向", "text": "先帮我定痛点方向"},
],
},
)
last_text_bubble = nudge
yield {"type": "message", "message": _message_payload(nudge)}
# 收束校验:本轮若只剩散文、没有追问/确认卡,补 reply_hint 或短引导。
for event in ensure_turn_guides(
conversation,
@@ -5169,6 +5270,8 @@ def _dispatch_tool(
f"{video_prompt}\n\n【商家确认的换款顺序】{click_swap_sequence(context.conversation)}\n"
"只允许按这个顺序逐款切换;不得跳序、漏款或自行增加颜色和款式。"
)
if click_swap_mode(context.conversation) != "character":
video_prompt = scrub_click_swap_same_position_wording(video_prompt)
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)