全能创作与模特库收口:品牌品名输入、确认角色循环、换款落点与鱼眼模板
补充品牌品名改为预填输入框且空骨架不跳步;痛点确认角色后「继续完善方案」可继续推进;点击换款要求每次落点不同;鱼眼换装改用小云雀连环换装 prompt 模板;模特详情标签自动换行。
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user