全能创作与模特库收口:品牌品名输入、确认角色循环、换款落点与鱼眼模板
补充品牌品名改为预填输入框且空骨架不跳步;痛点确认角色后「继续完善方案」可继续推进;点击换款要求每次落点不同;鱼眼换装改用小云雀连环换装 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)
|
||||
|
||||
@@ -8,12 +8,14 @@ key 必须是同一个中文名(会话建的时候原样存进 CreationConversat
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
CLICK_SWAP_PRESETS = frozenset({"点击换款", "点触换款", "多色商品换款"})
|
||||
_CLICK_SWAP_FINGER_GUIDANCE = (
|
||||
"形态:只出手指出镜。固定机位的商品点击换款短片。必须先确认要展示的颜色/款式/SKU 及顺序,"
|
||||
"画面只围绕同一件商品的逐款切换:机位、背景与灯光稳定,商品构图稳定。"
|
||||
"用手指点击/轻触触发换款,接触后以干净 match cut 切到下一款。"
|
||||
"撰写分镜时不要反复写「每次点击同一位置 / 点同一点」;点击动作点到为止,重点写款式变化与节奏。"
|
||||
"每一次换款的手指点击落点必须与上一次不同(点商品的不同区域/角点),禁止写或暗示「每次点击同一位置 / 点同一点 / 手指始终点同一处」;分镜与 video_prompt 要体现「本次点击位置不同于上次」,不要复读同一落点。"
|
||||
"禁止完整人物出镜、禁止生成角色定妆图、禁止改成口播/剧情/使用教程;最后才用同一构图做全款式收束。"
|
||||
)
|
||||
_CLICK_SWAP_CHARACTER_GUIDANCE = (
|
||||
@@ -61,10 +63,12 @@ VIDEO_PRESETS: dict[str, str] = {
|
||||
"节奏清晰,转场干净。商品外观严格以参考图为准。"
|
||||
),
|
||||
"鱼眼换装": (
|
||||
"鱼眼/广角近距离透视,连续换装节奏。**人物面部和身形必须全程一致**,"
|
||||
"只有服装在变。人物从第一镜起就已经穿着当前套装;换装用甩头、抬手、转身等触发瞬间切换到已穿好的下一套,"
|
||||
"禁止拍穿衣服、套袖、拉拉链等穿戴过程。"
|
||||
),
|
||||
"超广角鱼眼/近距离透视的连环换装短片。"
|
||||
"全片单一机位、同一构图与背景锁定;画面中心人物清晰、四边缘畸变拉伸,高饱和时尚质感,逐拍卡点剪辑。"
|
||||
"人物从第一镜起就已经穿着当前套装;换装发生在遮挡/甩头/甩镜等动态瞬间,瞬间切到已穿好的下一套。"
|
||||
"禁止拍穿衣服、套袖、拉拉链等穿戴过程。video_prompt 必须按「鱼眼连环换装」制作模板撰写"
|
||||
"(时长比例、标题、风格、单机位节拍、转场规则、主体、服装状态机、无对白卡点音效)。"
|
||||
),
|
||||
"点击换款": _CLICK_SWAP_GUIDANCE,
|
||||
"多色商品换款": _CLICK_SWAP_GUIDANCE,
|
||||
# 旧会话仍会保存「点触换款」,保留同一拍法约束以支持历史继续创作。
|
||||
@@ -270,10 +274,10 @@ VIDEO_PRESET_WORKFLOWS: dict[str, str] = {
|
||||
"商品拟人广告": "先确认商品外观和性格表达方式;默认无脸拟人,商品保持真实完整,台词用画外声。",
|
||||
"达人口播种草": "优先确认人物、多人出镜关系、真实体验和主卖点;生成前核对口播字数能在时长内说完,每个卖点都有画面证明。",
|
||||
"商品图一键成片": "优先从商品参考图锁定外观;自动补场景和动作,但不替换或改变用户商品图里的结构、颜色和包装。",
|
||||
"鱼眼换装": "优先确认人物参考、服装套数和展示顺序;生成前核对脸、身形、场景稳定,只有服装瞬间切换;人物始终已穿好当前套,不写穿衣过程。",
|
||||
"点击换款": "先让用户确认颜色/款式/SKU 和切换顺序;脚本只能是固定机位 + 手指点击触发换款;分镜不要反复写「每次点击同一位置」,禁止转成剧情或口播。",
|
||||
"多色商品换款": "先让用户确认颜色/款式/SKU 和切换顺序;脚本只能是固定机位 + 手指点击触发换款;分镜不要反复写「每次点击同一位置」,禁止转成剧情或口播。",
|
||||
"点触换款": "先让用户确认颜色/款式/SKU 和切换顺序;脚本只能是固定机位 + 手指点击触发换款;分镜不要反复写「每次点击同一位置」,禁止转成剧情或口播。",
|
||||
"鱼眼换装": "优先确认人物参考、服装套数N与展示顺序、本条机位(低机位仰拍/高机位俯拍/荷兰角/贴地滑行/手持环绕等择一全片不变)、场景与转场方式;时长按 N×1.5–2.5 秒估算(约 4–15 秒);write_prompt 严格用「鱼眼连环换装」模板:节拍换装 + 服装状态机,无对白,禁止穿衣过程。",
|
||||
"点击换款": "先让用户确认颜色/款式/SKU 和切换顺序;脚本只能是固定机位 + 手指点击触发换款;每次换款点击位置必须不同(换商品不同区域),禁止「每次点击同一位置 / 点同一点」。禁止转成剧情或口播。",
|
||||
"多色商品换款": "先让用户确认颜色/款式/SKU 和切换顺序;脚本只能是固定机位 + 手指点击触发换款;每次换款点击位置必须不同(换商品不同区域),禁止「每次点击同一位置 / 点同一点」。禁止转成剧情或口播。",
|
||||
"点触换款": "先让用户确认颜色/款式/SKU 和切换顺序;脚本只能是固定机位 + 手指点击触发换款;每次换款点击位置必须不同(换商品不同区域),禁止「每次点击同一位置 / 点同一点」。禁止转成剧情或口播。",
|
||||
"探店漫游": "优先确认门店动线与主推项目;用连续移动串联入口、环境、细节和服务,不做碎片化硬切。",
|
||||
"品牌质感大片": "优先确认品牌气质、材质和商品主卖点;由 Agent 决定光线和镜头,不把专业选择反复抛给用户。",
|
||||
"前后对比实测": "优先确认同一对象的前、中、后素材或可比较条件;生成前核对对比不夸大且没有缺失关键阶段。",
|
||||
@@ -313,15 +317,23 @@ VIDEO_PRESET_DELIVERY_CONTRACTS: dict[str, str] = {
|
||||
"最后以干净的产品收束。包装文字、颜色、比例和结构全程稳定,不能用 AI 生成的相似商品替代参考图。"
|
||||
),
|
||||
"鱼眼换装": (
|
||||
"【预设执行层·鱼眼换装】使用近距离鱼眼/广角透视和稳定的同一机位;人物脸、身形、发型、场景、光线连续一致。"
|
||||
"人物始终已穿着当前套装;每一套由甩头、抬手、转身等动作触发瞬间切换到已穿好的下一套,按用户提供顺序完整展示。"
|
||||
"禁止穿衣、套袖、扣扣、拉拉链等穿戴过程镜头;镜头变化来自触发动作与节奏,不额外编复杂营销剧情。"
|
||||
),
|
||||
"【预设执行层·鱼眼换装·小云雀模板】生成时尚换装短视频。"
|
||||
"比例默认 9:16;时长按服装套数 N 估算(每套约 1.5–2.5 秒,通常 4–15 秒)。"
|
||||
"风格:超广角鱼眼,中心清晰、四边缘明显畸变拉伸,高饱和,时尚质感,逐拍卡点剪辑。"
|
||||
"镜头:全片单一机位(本条自选一种并整条不变:低机位仰拍 / 高机位俯拍 / 荷兰角倾斜 / 贴地滑行 / 手持环绕);"
|
||||
"0–1 秒该机位对准主体、第 1 套造型定格 pose;之后逐拍换装,机位始终不变,仅服装与动作逐拍更新;最后一拍同机位定格收尾。"
|
||||
"转场:每次换装发生在遮挡/甩镜动态瞬间(抬手遮挡镜头 / 甩头转身划过画面 / 镜头快速甩动出虚影);"
|
||||
"转场瞬间服装切换,机位保持原位,下一拍回到同一构图。"
|
||||
"主体:只采用参考图五官、发型、妆容、姿态,不采用参考图服装与背景。"
|
||||
"服装状态机:S0 造型1 → 转场动作 → S1 造型2 → … → SN 定格收尾;"
|
||||
"全过程人物身份/体型/发型一致,机位/构图/背景锁定,仅服装逐拍变化;人物始终已穿好当前套,禁止穿戴过程镜头。"
|
||||
"声音:无对白,预留卡点 BGM,每次转场配快门/whoosh 音效。"
|
||||
),
|
||||
"点击换款": (
|
||||
"【预设执行层·点击换款·强制】这不是口播片、剧情片或普通商品展示片。"
|
||||
"使用单一固定机位和同一背景,商品构图、尺寸与角度保持稳定。"
|
||||
"换款由手指轻触/点击触发,接触后以干净 match cut 切到下一款;"
|
||||
"分镜不要反复写「每次点击同一位置 / 点同一点」,点击动作点到为止,重点写款式变化。"
|
||||
"每一次换款的手指点击落点必须与上一次不同(点商品的不同区域/角点),禁止写或暗示「每次点击同一位置 / 点同一点 / 手指始终点同一处」;分镜与 video_prompt 要体现「本次点击位置不同于上次」,不要复读同一落点。"
|
||||
"不改场景、不改机位、不运镜、不说话、不出现完整人物、不演剧情、不演示使用步骤。"
|
||||
"各款必须按用户确认的顺序逐一出现,不自行发明颜色或款式;结尾停在干净的全款式总览。"
|
||||
),
|
||||
@@ -329,7 +341,7 @@ VIDEO_PRESET_DELIVERY_CONTRACTS: dict[str, str] = {
|
||||
"【预设执行层·点击换款·强制】这不是口播片、剧情片或普通商品展示片。"
|
||||
"使用单一固定机位和同一背景,商品构图、尺寸与角度保持稳定。"
|
||||
"换款由手指轻触/点击触发,接触后以干净 match cut 切到下一款;"
|
||||
"分镜不要反复写「每次点击同一位置 / 点同一点」,点击动作点到为止,重点写款式变化。"
|
||||
"每一次换款的手指点击落点必须与上一次不同(点商品的不同区域/角点),禁止写或暗示「每次点击同一位置 / 点同一点 / 手指始终点同一处」;分镜与 video_prompt 要体现「本次点击位置不同于上次」,不要复读同一落点。"
|
||||
"不改场景、不改机位、不运镜、不说话、不出现完整人物、不演剧情、不演示使用步骤。"
|
||||
"各款必须按用户确认的顺序逐一出现,不自行发明颜色或款式;结尾停在干净的全款式总览。"
|
||||
),
|
||||
@@ -337,7 +349,7 @@ VIDEO_PRESET_DELIVERY_CONTRACTS: dict[str, str] = {
|
||||
"【预设执行层·点击换款·强制】这不是口播片、剧情片或普通商品展示片。"
|
||||
"使用单一固定机位和同一背景,商品构图、尺寸与角度保持稳定。"
|
||||
"换款由手指轻触/点击触发,接触后以干净 match cut 切到下一款;"
|
||||
"分镜不要反复写「每次点击同一位置 / 点同一点」,点击动作点到为止,重点写款式变化。"
|
||||
"每一次换款的手指点击落点必须与上一次不同(点商品的不同区域/角点),禁止写或暗示「每次点击同一位置 / 点同一点 / 手指始终点同一处」;分镜与 video_prompt 要体现「本次点击位置不同于上次」,不要复读同一落点。"
|
||||
"不改场景、不改机位、不运镜、不说话、不出现完整人物、不演剧情、不演示使用步骤。"
|
||||
"各款必须按用户确认的顺序逐一出现,不自行发明颜色或款式;结尾停在干净的全款式总览。"
|
||||
),
|
||||
@@ -403,7 +415,9 @@ _CLICK_SWAP_FINGER_CONTRACT = (
|
||||
"【预设执行层·点击换款·只出手】这不是口播片、剧情片或普通商品展示片。"
|
||||
"使用单一固定机位和同一背景,商品构图、尺寸与角度保持稳定。"
|
||||
"换款由手指轻触/点击触发,接触后以干净 match cut 切到下一款;"
|
||||
"分镜不要反复写「每次点击同一位置 / 点同一点」,点击动作点到为止,重点写款式变化。"
|
||||
"每一次换款的手指点击落点必须与上一次不同(点商品的不同区域/角点),"
|
||||
"禁止写或暗示「每次点击同一位置 / 点同一点 / 手指始终点同一处」;"
|
||||
"分镜与 video_prompt 要体现「本次点击位置不同于上次」。"
|
||||
"不改场景、不改机位、不运镜、不说话、不出现完整人物、不演剧情、不演示使用步骤、不生成角色定妆图。"
|
||||
"各款必须按用户确认的顺序逐一出现,不自行发明颜色或款式;结尾停在干净的全款式总览。"
|
||||
)
|
||||
@@ -425,10 +439,102 @@ def video_preset_delivery_contract(name: str, *, click_swap_mode: str = "") -> s
|
||||
return VIDEO_PRESET_DELIVERY_CONTRACTS.get(preset, "")
|
||||
|
||||
|
||||
|
||||
FISH_EYE_OUTFIT_PRESET = "鱼眼换装"
|
||||
|
||||
_FISH_EYE_OUTFIT_PROMPT_TEMPLATE = """
|
||||
【鱼眼连环换装 · video_prompt 强制模板(来自小云雀写法)】
|
||||
当预设为「鱼眼换装」时,write_plan / write_prompt 的 video_prompt 必须按下面骨架填写,
|
||||
用会话里真实的人物、服装套数、机位、场景与顺序替换【】占位;不要改回通用「制作级分镜四行」长文,也不要编口播。
|
||||
|
||||
时长:【4-15秒,按N推算,每套服装约1.5-2.5秒】秒
|
||||
比例:9:16
|
||||
任务:生成时尚换装短视频。
|
||||
|
||||
标题:《鱼眼连环换装·【机位名】版》
|
||||
|
||||
风格与视觉参考:
|
||||
超广角鱼眼镜头风格,画面中心人物清晰、四边缘明显畸变拉伸,
|
||||
高饱和色彩,时尚质感,逐拍卡点剪辑。
|
||||
|
||||
镜头语言:
|
||||
全片单一机位:【本条视频机位描述,如:低机位仰拍 / 高机位俯拍 /
|
||||
荷兰角倾斜 / 贴地滑行 / 手持环绕——每条视频自选一种,整条不变】
|
||||
0-1秒:该机位对准【主体】,第1套造型定格pose
|
||||
之后逐拍换装,机位始终不变,仅服装与动作逐拍更新:
|
||||
节拍1:造型1,随节拍做动作
|
||||
节拍2:造型2,随节拍做动作
|
||||
节拍3:造型3,随节拍做动作
|
||||
……按N套延伸
|
||||
最后一拍:同机位稳定构图,定格收尾pose
|
||||
|
||||
转场规则:每次换装发生在遮挡/甩镜的动态瞬间——
|
||||
【转场方式:抬手遮挡镜头 / 甩头转身划过画面 / 镜头快速甩动出虚影,
|
||||
转场瞬间服装切换,机位保持原位,下一拍回到同一构图】
|
||||
|
||||
主体:
|
||||
主体1:【人物描述】,对应@图片1
|
||||
(只采用五官、发型、妆容、姿态,不采用服装与背景)
|
||||
|
||||
服装状态机:
|
||||
S0 造型1:【服装1描述】
|
||||
→ 动作:【转场方式】
|
||||
→ S1 造型2:【服装2描述】
|
||||
→ 动作:【转场方式】
|
||||
→ ……按N套延伸
|
||||
→ SN 造型N:【服装N描述】定格收尾
|
||||
全过程不变量:人物身份、体型、发型全程一致;
|
||||
机位、构图、背景全程锁定不变,仅服装逐拍变化;
|
||||
人物始终已穿好当前套,禁止穿衣/套袖/拉拉链等穿戴过程镜头。
|
||||
|
||||
声音:无对白,预留卡点BGM位,每次转场配快门/whoosh音效
|
||||
|
||||
全片保持主体1身份一致、鱼眼畸变风格统一、
|
||||
机位与构图全片同一,背景【场景描述】不变。
|
||||
"""
|
||||
|
||||
|
||||
def is_fish_eye_outfit_preset(name: str) -> bool:
|
||||
return (name or "").strip() == FISH_EYE_OUTFIT_PRESET
|
||||
|
||||
|
||||
def fish_eye_outfit_prompt_template() -> str:
|
||||
return _FISH_EYE_OUTFIT_PROMPT_TEMPLATE.strip()
|
||||
|
||||
def is_click_swap_preset(name: str) -> bool:
|
||||
return (name or "").strip() in CLICK_SWAP_PRESETS
|
||||
|
||||
|
||||
|
||||
_SAME_CLICK_POS_RE = re.compile(
|
||||
r"(?:每次(?:都)?)?(?:点击|点触|轻触|手指(?:点击|点)?)(?:在|到)?(?:完全)?(?:相?同的?)?(?:一个位置|一(?:个)?位置|一点|同一处|同一点|同一个位置)"
|
||||
r"|每次点击同一位置"
|
||||
r"|点同一点"
|
||||
r"|手指点在完全相同的位置"
|
||||
r"|手指始终点同一"
|
||||
r"|点击位置保持不变"
|
||||
r"|固定点击位置"
|
||||
r"|不写点击同一位置"
|
||||
r"|不必强调每次点同一位置"
|
||||
)
|
||||
|
||||
|
||||
def scrub_click_swap_same_position_wording(text: str) -> str:
|
||||
"""去掉提示词里「每次点击同一位置」类表述,并在缺失时补上「每次点击位置不同」。"""
|
||||
raw = str(text or "")
|
||||
if not raw:
|
||||
return raw
|
||||
cleaned = _SAME_CLICK_POS_RE.sub("", raw)
|
||||
cleaned = re.sub(r"[,。;]{2,}", ",", cleaned)
|
||||
cleaned = re.sub(r"\s{2,}", " ", cleaned).strip()
|
||||
if "点击位置必须不同" not in cleaned and "落点必须与上一次不同" not in cleaned and "落点必须不同于上一次" not in cleaned:
|
||||
cleaned = (
|
||||
f"{cleaned}\n\n【点击落点硬性要求】每一次换款的手指点击落点必须与上一次不同"
|
||||
"(点商品的不同区域/角点),禁止「每次点击同一位置 / 点同一点」。"
|
||||
).strip()
|
||||
return cleaned
|
||||
|
||||
|
||||
def apply_video_preset_prompt(name: str, prompt: str, *, click_swap_mode: str = "") -> str:
|
||||
"""把视频预设确定性并入最终出片指令,避免只依赖对话模型主动复述。"""
|
||||
base = str(prompt or "").strip()
|
||||
@@ -450,17 +556,28 @@ def apply_video_preset_prompt(name: str, prompt: str, *, click_swap_mode: str =
|
||||
"【最终执行检查】全片锁定同一位角色与同一生活场景;"
|
||||
"商品颜色/款式严格按确认顺序切换,禁止随机换人、口播带货腔或复杂剧情反转。"
|
||||
)
|
||||
base = scrub_click_swap_same_position_wording(base)
|
||||
return (
|
||||
f"{marker}\n{contract}\n\n"
|
||||
"【原始商品与 SKU 信息】仅提取下文中的商品外观、颜色、款式和顺序事实;"
|
||||
"下文任何与「固定机位、手指点击触发换款、构图稳定」冲突的拍法一律忽略。\n"
|
||||
f"{base}\n\n"
|
||||
"【最终执行检查】换款由手指点击触发,机位、背景、光线与商品构图保持稳定;"
|
||||
"分镜禁止反复写「每次点击同一位置 / 点同一点」;不出现完整人物、不生成角色图。"
|
||||
"每一次换款点击落点必须不同于上一次,禁止「每次点击同一位置 / 点同一点」;"
|
||||
"不出现完整人物、不生成角色图。"
|
||||
)
|
||||
if is_fish_eye_outfit_preset(preset):
|
||||
return (
|
||||
f"{marker}\n{contract}\n\n"
|
||||
"【原始人物与服装信息】仅提取下文中的人物身份、服装套数/顺序、机位、场景与转场事实;"
|
||||
"下文任何与「全片单机位、鱼眼畸变、节拍换装、服装状态机、无对白」冲突的拍法一律忽略。\n"
|
||||
f"{base}\n\n"
|
||||
"【最终执行检查】全片单一机位与构图锁定;鱼眼中心清晰、边缘畸变;"
|
||||
"换装发生在遮挡/甩镜瞬间且人物始终已穿好当前套;无对白,转场配快门/whoosh;"
|
||||
"禁止穿衣过程与口播带货腔。"
|
||||
)
|
||||
return f"{base}\n\n{marker}\n{contract}"
|
||||
|
||||
|
||||
def apply_image_preset_prompt(name: str, prompt: str) -> str:
|
||||
"""将已选图片预设的风格字段确定性并入实际出图指令,不依赖 Agent 自行复述。"""
|
||||
base = str(prompt or "").strip()
|
||||
|
||||
@@ -2511,7 +2511,7 @@ class VideoPlanAndConfirmTests(CreationAgentBaseTests):
|
||||
self.assertIn("手指轻触/点击", prompt)
|
||||
self.assertIn("不演剧情", prompt)
|
||||
self.assertIn("干净 match cut", prompt)
|
||||
self.assertIn("不要反复写「每次点击同一位置", prompt)
|
||||
self.assertIn("每一次换款的手指点击落点必须与上一次不同", prompt)
|
||||
|
||||
def test_personified_product_uses_offscreen_voice_without_changing_packaging(self):
|
||||
self.conversation.preset = "商品拟人广告"
|
||||
@@ -2891,8 +2891,10 @@ class PresetGuidanceTests(CreationAgentBaseTests):
|
||||
text="开始", model_config=self.model))
|
||||
system = fake.calls[0]["messages"][0]["content"]
|
||||
self.assertIn("鱼眼换装", system)
|
||||
# 光有名字模型只能靠猜,拍法约束必须一起给
|
||||
self.assertIn("人物面部和身形必须全程一致", system)
|
||||
# 光有名字模型只能靠猜,拍法约束必须一起给(小云雀连环换装模板)
|
||||
self.assertIn("小云雀", system)
|
||||
self.assertIn("服装状态机", system)
|
||||
self.assertIn("超广角鱼眼", system)
|
||||
|
||||
def test_product_personification_defaults_to_faceless_expression(self):
|
||||
product = Product.objects.create(team=self.team, created_by=self.user, title="拟人测试商品")
|
||||
@@ -2943,7 +2945,7 @@ class PresetGuidanceTests(CreationAgentBaseTests):
|
||||
self.assertIn("当前预设的工作重点", system)
|
||||
self.assertIn("确认颜色/款式/SKU 和切换顺序", system)
|
||||
self.assertIn("固定机位 + 手指点击触发换款", system)
|
||||
self.assertIn("不要反复写「每次点击同一位置」", system)
|
||||
self.assertIn("每一次换款的点击落点必须与上一次不同", system)
|
||||
self.assertIn("禁止转成剧情或口播", system)
|
||||
|
||||
def test_video_preset_is_injected_into_the_actual_generation_prompt(self):
|
||||
@@ -2951,8 +2953,10 @@ class PresetGuidanceTests(CreationAgentBaseTests):
|
||||
|
||||
prompt = apply_video_preset_prompt("鱼眼换装", "人物抬手完成连续换装")
|
||||
self.assertIn("【视频预设】鱼眼换装", prompt)
|
||||
self.assertIn("近距离鱼眼/广角透视", prompt)
|
||||
self.assertIn("人物脸、身形、发型、场景、光线连续一致", prompt)
|
||||
self.assertIn("小云雀模板", prompt)
|
||||
self.assertIn("服装状态机", prompt)
|
||||
self.assertIn("超广角鱼眼", prompt)
|
||||
self.assertIn("无对白", prompt)
|
||||
# 出片确认、重试等多次经过此处,规则只写一次。
|
||||
self.assertEqual(apply_video_preset_prompt("鱼眼换装", prompt), prompt)
|
||||
|
||||
@@ -2970,7 +2974,7 @@ class PresetGuidanceTests(CreationAgentBaseTests):
|
||||
self.assertIn("原始商品与 SKU 信息", prompt)
|
||||
self.assertIn("冲突的拍法一律忽略", prompt)
|
||||
self.assertIn("换款由手指点击触发", prompt)
|
||||
self.assertIn("分镜禁止反复写「每次点击同一位置", prompt)
|
||||
self.assertIn("每一次换款点击落点必须不同于上一次", prompt)
|
||||
self.assertEqual(apply_video_preset_prompt("点击换款", prompt), prompt)
|
||||
|
||||
def test_every_video_preset_has_a_delivery_contract(self):
|
||||
@@ -3267,6 +3271,7 @@ class CreativeIntentTests(SimpleTestCase):
|
||||
for text in (
|
||||
"做条带货视频",
|
||||
"帮我出个方案",
|
||||
"继续完善方案",
|
||||
"重新写方案",
|
||||
"出一张主图",
|
||||
"拍一张自然、有温度的生活方式照片",
|
||||
@@ -3276,7 +3281,17 @@ class CreativeIntentTests(SimpleTestCase):
|
||||
self.assertTrue(has_creative_intent(text), text)
|
||||
|
||||
def test_short_continue_phrases_are_explicit_continuations(self):
|
||||
for text in ("继续", "接着做", "往下做吧", "就按这个", "直接来"):
|
||||
for text in (
|
||||
"继续",
|
||||
"接着做",
|
||||
"往下做吧",
|
||||
"就按这个",
|
||||
"直接来",
|
||||
"继续完善方案",
|
||||
"完善方案",
|
||||
"继续创作",
|
||||
"使用这个角色继续创作",
|
||||
):
|
||||
self.assertTrue(is_continue_intent(text), text)
|
||||
|
||||
for text in ("好的", "嗯", "先不用", "今天怎么样"):
|
||||
|
||||
@@ -58,6 +58,8 @@ from .creation_agent import (
|
||||
submit_confirmed_image,
|
||||
submit_confirmed_video,
|
||||
submit_generated_person_reference,
|
||||
is_incomplete_product_brand_answer,
|
||||
PRODUCT_BRAND_EMPTY_TEMPLATE,
|
||||
)
|
||||
from .tasks import run_creation_agent_turn_task
|
||||
from .mentions import TYPE_LABELS, VALID_TYPES, refs_from_elicit_answers, search_mentions
|
||||
@@ -2237,6 +2239,20 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
fields = [item for item in (payload.get("fields") or []) if isinstance(item, dict)]
|
||||
if fields:
|
||||
field = fields[0]
|
||||
if payload.get("topic") == "product_info":
|
||||
clean_probe = text.strip()
|
||||
design_from_image = any(
|
||||
kw in clean_probe for kw in ("按图片", "设计品牌", "不用再问")
|
||||
)
|
||||
if (not design_from_image) and is_incomplete_product_brand_answer(clean_probe):
|
||||
conversation.agent_status = CreationConversation.AgentStatus.AWAITING_USER
|
||||
conversation.save(update_fields=["agent_status", "updated_at"])
|
||||
return JsonResponse({
|
||||
"conversation_id": str(conversation.id),
|
||||
"agent_status": conversation.agent_status,
|
||||
"detail": "请填写品牌和品名后再发送,例如「品牌浅蓝小熊,品名婴儿柔湿巾」。",
|
||||
"messages": [CreationMessageSerializer(pending).data],
|
||||
}, status=200)
|
||||
answers = {
|
||||
str(field.get("key") or "answer"):
|
||||
_chat_answer_for_field(field, text, conversation.team)
|
||||
@@ -2270,8 +2286,20 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
elif payload.get("topic") == "product_info":
|
||||
clean_ans = text.strip()
|
||||
memory = dict(conversation.memory or {})
|
||||
memory["product_info_resolved"] = True
|
||||
if clean_ans and not any(kw in clean_ans for kw in ("按图片", "设计品牌", "不用再问")):
|
||||
design_from_image = any(kw in clean_ans for kw in ("按图片", "设计品牌", "不用再问"))
|
||||
if design_from_image:
|
||||
memory["product_info_resolved"] = True
|
||||
continuation_instruction = (
|
||||
"用户要求直接根据图片内容设计品牌与品名推进创作。"
|
||||
"直接基于图片外观特征推进方案,不要重复追问品牌。"
|
||||
)
|
||||
elif is_incomplete_product_brand_answer(clean_ans):
|
||||
memory.pop("product_info_resolved", None)
|
||||
continuation_instruction = (
|
||||
"用户尚未填写完整品牌与品名。请继续请他补充,不要进入下一步。"
|
||||
)
|
||||
else:
|
||||
memory["product_info_resolved"] = True
|
||||
memory["product_brand_and_name"] = clean_ans
|
||||
bm = re.search(r"品牌[::是为]?\s*([^\n,,。!!]+)", clean_ans)
|
||||
nm = re.search(r"(?:品名|商品名|产品名)[::是为]?\s*([^\n,,。!!]+)", clean_ans)
|
||||
@@ -2284,9 +2312,10 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
r["name"] = clean_ans
|
||||
break
|
||||
conversation.pinned_refs = conversation.pinned_refs
|
||||
continuation_instruction = f"用户已提供商品品牌与品名:【{clean_ans}】。直接围绕该商品推进方案,不要重复追问。"
|
||||
else:
|
||||
continuation_instruction = "用户要求直接根据图片内容设计品牌与品名推进创作。直接基于图片外观特征推进方案,不要重复追问品牌。"
|
||||
continuation_instruction = (
|
||||
f"用户已提供商品品牌与品名:【{clean_ans}】。"
|
||||
"直接围绕该商品推进方案,不要重复追问。"
|
||||
)
|
||||
conversation.memory = memory
|
||||
conversation.save(update_fields=["memory", "pinned_refs", "updated_at"])
|
||||
# 保留用户原文气泡,便于回看刚填的品牌/品名;不要清空 text,
|
||||
@@ -2331,8 +2360,11 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
conversation.save(update_fields=["memory", "updated_at"])
|
||||
force_creative_turn = True
|
||||
continuation_instruction = (
|
||||
"用户已确认使用当前生成的角色出镜。直接基于该角色继续推进创作方案"
|
||||
"(若尚未选商品则确定商品,已选好商品则开始写创作策略和方案),不要再次追问角色来源。"
|
||||
"用户已确认使用当前生成的角色出镜。"
|
||||
"本轮必须实质推进创作:若还缺商品则 ask_user 选商品;"
|
||||
"痛点解决演示若还缺痛点方向则 ask_user(pain_point_direction);"
|
||||
"信息够了就立刻 write_strategy。"
|
||||
"禁止只回一句「可以回复继续完善方案」之类的空引导,也不要再次追问角色来源。"
|
||||
)
|
||||
elif is_platform_person and (regenerate_button or (person_confirm_pending and clean_text and not upload_person)):
|
||||
prev_prompt = str(memory_now.get("person_prompt") or "").strip()
|
||||
@@ -2662,24 +2694,45 @@ class CreationConversationViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
elif payload.get("topic") == "product_info":
|
||||
ans = str(answers.get("product_brand_and_name") or "").strip()
|
||||
memory = dict(conversation.memory or {})
|
||||
memory["product_info_resolved"] = True
|
||||
clean_ans = ans.replace("品牌是:,商品名是:", "").strip()
|
||||
if clean_ans and not any(kw in clean_ans for kw in ("按图片", "设计品牌", "不用再问")):
|
||||
memory["product_brand_and_name"] = clean_ans
|
||||
bm = re.search(r"品牌[::是为]?\s*([^\n,,。!!]+)", clean_ans)
|
||||
nm = re.search(r"(?:品名|商品名|产品名)[::是为]?\s*([^\n,,。!!]+)", clean_ans)
|
||||
design_from_image = any(kw in ans for kw in ("按图片", "设计品牌", "不用再问"))
|
||||
clean_ans = ans.replace(PRODUCT_BRAND_EMPTY_TEMPLATE, "").strip()
|
||||
if (not design_from_image) and is_incomplete_product_brand_answer(ans):
|
||||
payload["submitted"] = False
|
||||
payload["answers"] = {}
|
||||
pending.payload = payload
|
||||
pending.save(update_fields=["payload", "updated_at"])
|
||||
conversation.agent_status = CreationConversation.AgentStatus.AWAITING_USER
|
||||
conversation.save(update_fields=["agent_status", "updated_at"])
|
||||
return JsonResponse({
|
||||
"conversation_id": str(conversation.id),
|
||||
"agent_status": conversation.agent_status,
|
||||
"detail": "请填写品牌和品名后再发送,例如「品牌浅蓝小熊,品名婴儿柔湿巾」。",
|
||||
"messages": [CreationMessageSerializer(pending).data],
|
||||
}, status=200)
|
||||
if design_from_image:
|
||||
memory["product_info_resolved"] = True
|
||||
continuation_instruction = (
|
||||
"用户要求直接根据图片内容设计品牌与品名推进创作。"
|
||||
"直接基于图片外观特征推进方案,不要重复追问品牌。"
|
||||
)
|
||||
else:
|
||||
memory["product_info_resolved"] = True
|
||||
memory["product_brand_and_name"] = clean_ans or ans
|
||||
bm = re.search(r"品牌[::是为]?\s*([^\n,,。!!]+)", clean_ans or ans)
|
||||
nm = re.search(r"(?:品名|商品名|产品名)[::是为]?\s*([^\n,,。!!]+)", clean_ans or ans)
|
||||
if bm:
|
||||
memory["product_brand"] = bm.group(1).strip()
|
||||
if nm:
|
||||
memory["product_name"] = nm.group(1).strip()
|
||||
for r in (conversation.pinned_refs or []):
|
||||
if isinstance(r, dict) and r.get("type") in ("asset", "product"):
|
||||
r["name"] = clean_ans
|
||||
r["name"] = clean_ans or ans
|
||||
break
|
||||
conversation.pinned_refs = conversation.pinned_refs
|
||||
continuation_instruction = f"用户已提供商品品牌与品名:【{clean_ans}】。直接围绕该商品推进方案,不要重复追问。"
|
||||
else:
|
||||
continuation_instruction = "用户要求直接根据图片内容设计品牌与品名推进创作。直接基于图片外观特征推进方案,不要重复追问品牌。"
|
||||
continuation_instruction = (
|
||||
f"用户已提供商品品牌与品名:【{clean_ans or ans}】。"
|
||||
"直接围绕该商品推进方案,不要重复追问。"
|
||||
)
|
||||
conversation.memory = memory
|
||||
conversation.save(update_fields=["memory", "pinned_refs", "updated_at"])
|
||||
text = ""
|
||||
|
||||
Reference in New Issue
Block a user