优化已发现问题
This commit is contained in:
@@ -72,9 +72,16 @@ _STEP_CONFIRM_LABELS = {
|
||||
_PERSON_SOURCE_PRESETS = {
|
||||
"痛点解决演示",
|
||||
PLOT_TWIST_PRESET,
|
||||
"短剧反转带货",
|
||||
"达人口播种草",
|
||||
"鱼眼换装",
|
||||
"AI 宠物拟人",
|
||||
}
|
||||
|
||||
|
||||
def is_pet_preset(preset: str | None) -> bool:
|
||||
name = str(preset or "").strip()
|
||||
return "宠物" in name
|
||||
_PERSON_VISUAL_RE = re.compile(
|
||||
r"(人物|角色|模特|主角|达人|主播|出镜|口播|女生|女性|男生|男性|"
|
||||
r"女主|男主|年轻人|手模|手部|真人|换装|穿搭|剧情|短剧)"
|
||||
@@ -537,7 +544,7 @@ def video_needs_person_source(conversation: CreationConversation, user_text: str
|
||||
memory = conversation.memory if isinstance(conversation.memory, dict) else {}
|
||||
if memory.get("person_source_ready") or memory.get("person_source_pending"):
|
||||
return False
|
||||
if conversation.preset in _PERSON_SOURCE_PRESETS:
|
||||
if conversation.preset in _PERSON_SOURCE_PRESETS or is_pet_preset(conversation.preset):
|
||||
return True
|
||||
recent = list(
|
||||
conversation.messages.order_by("-seq").values_list("text", flat=True)[:12]
|
||||
@@ -546,10 +553,96 @@ def video_needs_person_source(conversation: CreationConversation, user_text: str
|
||||
return bool(_PERSON_VISUAL_RE.search("\n".join([user_text, pending_prompt, *recent])))
|
||||
|
||||
|
||||
def locked_product_references(conversation: CreationConversation) -> list[dict]:
|
||||
"""返回会话里所有已锁定的商品/产品素材实体(包括商品库商品和本地上传的商品图片)。"""
|
||||
products: list[dict] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
person_ids = {
|
||||
str(ref.get("id"))
|
||||
for ref in locked_person_references(conversation)
|
||||
if isinstance(ref, dict) and ref.get("id")
|
||||
}
|
||||
for ref in (conversation.pinned_refs or []):
|
||||
if not isinstance(ref, dict):
|
||||
continue
|
||||
type_ = str(ref.get("type") or "").strip()
|
||||
ref_id = str(ref.get("id") or "").strip()
|
||||
if not ref_id or ref_id in person_ids:
|
||||
continue
|
||||
# 显式商品,或上传的素材图片(排除人物角色/模特)
|
||||
if type_ == "product" or (type_ == "asset" and ref.get("category") != "character"):
|
||||
mark = (type_, ref_id)
|
||||
if mark not in seen:
|
||||
products.append(ref)
|
||||
seen.add(mark)
|
||||
return products
|
||||
|
||||
|
||||
def has_locked_product_reference(conversation: CreationConversation) -> bool:
|
||||
return any(
|
||||
isinstance(ref, dict) and ref.get("type") == "product" and ref.get("id")
|
||||
for ref in (conversation.pinned_refs or [])
|
||||
return len(locked_product_references(conversation)) > 0
|
||||
|
||||
|
||||
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"):
|
||||
return False
|
||||
# 如果已锁定的首个商品是正式商品库商品(已自带品牌和品名),无需重复询问
|
||||
first_prod = next(
|
||||
(ref for ref in (conversation.pinned_refs or []) if isinstance(ref, dict) and ref.get("type") == "product"),
|
||||
None,
|
||||
)
|
||||
if first_prod is not None:
|
||||
return False
|
||||
# 检查是否包含本地上传的商品素材
|
||||
uploaded_prods = [ref for ref in locked_product_references(conversation) if ref.get("type") == "asset"]
|
||||
if not uploaded_prods:
|
||||
return False
|
||||
# 检查用户输入的文字中是否已经明确提供了品牌/品名
|
||||
user_messages = (
|
||||
conversation.messages.filter(role="user").order_by("seq")
|
||||
if getattr(conversation, "created_at", None)
|
||||
else []
|
||||
)
|
||||
all_text = " ".join(str(m.text or "") for m in user_messages) + " " + (user_text or "")
|
||||
has_explicit_brand = bool(re.search(r"(?:品牌|牌子)[::是为]\s*([^\n,,。!!]{2,30})", all_text))
|
||||
has_explicit_name = bool(re.search(r"(?:品名|商品名|产品名)[::是为]\s*([^\n,,。!!]{2,30})", all_text))
|
||||
if has_explicit_brand or has_explicit_name:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def append_product_info_gate(conversation: CreationConversation) -> CreationMessage:
|
||||
"""针对已上传商品图但尚未说明品牌/品名的情况,主动询问商品品牌与品名。"""
|
||||
question = (
|
||||
"已收到你上传的商品图。请问这款商品的**品牌**和**具体品名**是什么?"
|
||||
"有想要重点突出的核心卖点也可以一起告诉我,以便在后续脚本中精准植入。"
|
||||
)
|
||||
return append_message(
|
||||
conversation,
|
||||
role="assistant",
|
||||
kind=CreationMessage.Kind.ELICIT,
|
||||
text=question,
|
||||
payload={
|
||||
"interaction": "chat",
|
||||
"topic": "product_info",
|
||||
"fields": [{
|
||||
"key": "product_brand_and_name",
|
||||
"label": "请提供商品品牌与具体品名",
|
||||
"type": "text",
|
||||
"required": True,
|
||||
"placeholder": "例如:品牌「浅蓝小熊」,品名「婴儿柔湿巾」",
|
||||
}],
|
||||
"submitted": False,
|
||||
"answers": {},
|
||||
"reply_hint": "输入品牌和品名,例如「品牌XX,品名YY」…",
|
||||
"reply_options": [
|
||||
{"label": "按图片内容创作", "text": "直接根据图片内容设计品牌与品名,不用再问"},
|
||||
{"label": "我来补充品牌品名", "text": "品牌是:,商品名是:"},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -568,17 +661,35 @@ def creation_needs_product_source(conversation: CreationConversation, user_text:
|
||||
|
||||
|
||||
def append_person_source_gate(conversation: CreationConversation) -> CreationMessage:
|
||||
"""可视化的人物来源闸门;三个选项分别进文件、模特库和生图流程。"""
|
||||
"""可视化的人物/角色来源闸门;三个选项分别进文件、模特库和生图流程。"""
|
||||
product_name = ""
|
||||
for ref in (conversation.pinned_refs or []):
|
||||
if isinstance(ref, dict) and ref.get("type") == "product":
|
||||
product_name = str(ref.get("name") or "").split(" · ")[0].strip()
|
||||
for ref in locked_product_references(conversation):
|
||||
name = str(ref.get("name") or "").split(" · ")[0].strip()
|
||||
if name and not name.lower().endswith((".png", ".jpg", ".jpeg", ".webp")):
|
||||
product_name = name
|
||||
break
|
||||
prompt_text = (
|
||||
f"商品已选定【{product_name}】。这条视频想由哪位角色/达人出镜?选定后,所有镜头和分段都会锁定同一位人物。"
|
||||
if product_name else
|
||||
"先确定这条视频的出镜人物。选定后,所有镜头和分段都会锁定同一位人物。"
|
||||
)
|
||||
if not product_name:
|
||||
memory = conversation.memory if isinstance(conversation.memory, dict) else {}
|
||||
product_name = str(memory.get("product_name") or memory.get("product_brand_and_name") or "").strip()
|
||||
|
||||
is_pet = is_pet_preset(conversation.preset)
|
||||
if is_pet:
|
||||
prompt_text = (
|
||||
f"商品已选定【{product_name}】。这条视频想由哪只宠物角色出镜?选定后,所有镜头和分段都会锁定同一只宠物形象。"
|
||||
if product_name else
|
||||
"先确定这条视频出镜的宠物角色。选定后,所有镜头和分段都会锁定同一只宠物形象。"
|
||||
)
|
||||
field_label = "选择宠物来源"
|
||||
library_label = "从角色库选择"
|
||||
else:
|
||||
prompt_text = (
|
||||
f"商品已选定【{product_name}】。这条视频想由哪位角色/达人出镜?选定后,所有镜头和分段都会锁定同一位人物。"
|
||||
if product_name else
|
||||
"先确定这条视频的出镜人物。选定后,所有镜头和分段都会锁定同一位人物。"
|
||||
)
|
||||
field_label = "选择人物来源"
|
||||
library_label = "从模特库选择"
|
||||
|
||||
return append_message(
|
||||
conversation,
|
||||
role="assistant",
|
||||
@@ -586,14 +697,15 @@ def append_person_source_gate(conversation: CreationConversation) -> CreationMes
|
||||
text=prompt_text,
|
||||
payload={
|
||||
"interaction": "person_source_gate",
|
||||
"is_pet": is_pet,
|
||||
"fields": [{
|
||||
"key": "person_source",
|
||||
"label": "选择人物来源",
|
||||
"label": field_label,
|
||||
"type": "single",
|
||||
"required": True,
|
||||
"options": [
|
||||
{"value": "local_upload", "label": "本地上传"},
|
||||
{"value": "model_library", "label": "从模特库选择"},
|
||||
{"value": "model_library", "label": library_label},
|
||||
{"value": "platform_generate", "label": "平台帮忙生成"},
|
||||
],
|
||||
}],
|
||||
@@ -639,25 +751,67 @@ def append_click_swap_sequence_gate(conversation: CreationConversation) -> Creat
|
||||
)
|
||||
|
||||
|
||||
def submit_generated_person_reference(*, conversation: CreationConversation, user) -> CreationMessage:
|
||||
"""先生成独立人物定妆参考,完成后再由 creation.py 自动建模特并锁定。"""
|
||||
def submit_generated_person_reference(
|
||||
*,
|
||||
conversation: CreationConversation,
|
||||
user,
|
||||
appearance_prompt: str = "",
|
||||
) -> CreationMessage:
|
||||
"""先生成独立人物/宠物角色定妆参考,完成后再由 creation.py 自动建模特并锁定。"""
|
||||
from .services import enqueue_standalone_images
|
||||
|
||||
recent_user = list(
|
||||
conversation.messages.filter(role=CreationMessage.Role.USER)
|
||||
.order_by("-seq").values_list("text", flat=True)[:6]
|
||||
recent_user = (
|
||||
list(
|
||||
conversation.messages.filter(role=CreationMessage.Role.USER)
|
||||
.order_by("-seq").values_list("text", flat=True)[:6]
|
||||
)
|
||||
if getattr(conversation, "created_at", None)
|
||||
else []
|
||||
)
|
||||
brief = "\n".join(reversed([item.strip() for item in recent_user if item and item.strip()]))[:700]
|
||||
prompt = (
|
||||
"为短视频生成一张可反复用于锁定身份的真人模特定妆参考图。"
|
||||
"只出现一位成年人物,正面或轻微三分之四角度,中近景,表情自然,"
|
||||
"五官、发型、肤色、身形和服装细节清晰,简洁中性背景,写实摄影,"
|
||||
"不要文字、水印、拼图、多人、遮挡脸部或夸张滤镜。"
|
||||
)
|
||||
if conversation.preset:
|
||||
prompt += f" 适配视频预设:{conversation.preset}。"
|
||||
if brief:
|
||||
prompt += f" 参考用户需求:{brief}"
|
||||
appearance_prompt = (appearance_prompt or "").strip()[:500]
|
||||
|
||||
is_pet = is_pet_preset(conversation.preset)
|
||||
if is_pet:
|
||||
# 智能推断宠物类别:用户指定 > 商品及需求上下文 > 默认萌宠
|
||||
context_text = f"{appearance_prompt} {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 ("狗", "犬", "咬胶", "磨牙", "骨头", "汪", "狗粮", "犬粮", "幼犬", "成犬", "中大型犬", "小型犬")):
|
||||
pet_type_hint = "一只呆萌可爱、神采奕奕的小狗(如金毛幼犬、柯基或柴犬等萌犬)"
|
||||
elif any(w in context_text for w in ("猫", "喵", "猫砂", "猫条", "猫粮", "冻干", "猫草", "逗猫", "幼猫")):
|
||||
pet_type_hint = "一只大眼睛、圆脸可爱的萌宠猫咪(如英短、美短或布偶等萌猫)"
|
||||
else:
|
||||
pet_type_hint = "一只可爱灵动的萌宠(如呆萌小狗或可爱猫咪)"
|
||||
|
||||
prompt = (
|
||||
f"为AI宠物拟人短视频生成一张可反复用于锁定角色身份的萌宠主角定妆参考图。"
|
||||
f"主角必须是{pet_type_hint},展现拟人化的生动表情与灵动神态,"
|
||||
f"正面或微侧三分之四角度,中近景特写,眼神清澈,毛发蓬松有光泽且根根分明,"
|
||||
f"可搭配精致简约的拟人小配饰(如可爱小领结、小方巾或背带,契合萌宠调性),"
|
||||
f"写实摄影风格,电影级柔和摄影棚光影,简洁纯净背景,"
|
||||
f"画面中只出现这一只可爱的宠物动物主角,绝对不要出现真人人类!不要文字、水印、拼图或变形。"
|
||||
)
|
||||
if appearance_prompt:
|
||||
prompt += f" 用户指定的宠物外观与品种:{appearance_prompt}。严格保留这些宠物外观要求。"
|
||||
if brief:
|
||||
prompt += f" 参考用户需求:{brief}"
|
||||
label = "正在生成宠物角色参考"
|
||||
else:
|
||||
prompt = (
|
||||
"为短视频生成一张可反复用于锁定身份的真人模特定妆参考图。"
|
||||
"只出现一位成年人物,正面或轻微三分之四角度,中近景,表情自然,"
|
||||
"五官、发型、肤色、身形和服装细节清晰,简洁中性背景,写实摄影,"
|
||||
"不要文字、水印、拼图、多人、遮挡脸部或夸张滤镜。"
|
||||
)
|
||||
if appearance_prompt:
|
||||
prompt += f" 用户指定的人物外观:{appearance_prompt}。严格保留这些外观要求。"
|
||||
if conversation.preset:
|
||||
prompt += f" 适配视频预设:{conversation.preset}。"
|
||||
if brief:
|
||||
prompt += f" 参考用户需求:{brief}"
|
||||
label = "正在生成人物参考"
|
||||
|
||||
tasks = enqueue_standalone_images(
|
||||
team=conversation.team,
|
||||
user=user,
|
||||
@@ -671,6 +825,7 @@ def submit_generated_person_reference(*, conversation: CreationConversation, use
|
||||
memory = dict(conversation.memory or {})
|
||||
memory["person_source"] = "platform_generate"
|
||||
memory["person_source_pending"] = True
|
||||
memory["person_prompt"] = appearance_prompt
|
||||
conversation.memory = memory
|
||||
conversation.status = CreationConversation.Status.RUNNING
|
||||
conversation.agent_status = CreationConversation.AgentStatus.IDLE
|
||||
@@ -683,7 +838,7 @@ def submit_generated_person_reference(*, conversation: CreationConversation, use
|
||||
"task_id": str(task.id),
|
||||
"kind": "person_reference",
|
||||
"prompt": prompt,
|
||||
"label": "正在生成人物参考",
|
||||
"label": label,
|
||||
},
|
||||
task=task,
|
||||
)
|
||||
@@ -1258,6 +1413,16 @@ def default_reply_options(
|
||||
{"label": "我直接说商品名", "text": "我直接告诉你商品名"},
|
||||
{"label": "你来推荐", "text": "你根据当前需求推荐一款"},
|
||||
]
|
||||
if re.search(
|
||||
r"是否使用这[个位]角色|角色.{0,8}(?:已生成|合适吗)|你看这[个位]角色|人物参考已生成",
|
||||
guidance,
|
||||
re.IGNORECASE,
|
||||
):
|
||||
return [
|
||||
{"label": "使用这个角色", "text": "使用这个角色继续创作"},
|
||||
{"label": "重新生成一个", "text": "重新生成一个角色"},
|
||||
{"label": "上传其他人物", "text": "我上传人物参考图"},
|
||||
]
|
||||
if re.search(
|
||||
r"(?:哪位|哪个|什么|选择|选|换|更换|调整|修改|改).{0,12}(?:人物|角色|模特|出镜)|"
|
||||
r"(?:人物|角色|模特|出镜).{0,12}(?:哪位|哪个|选择|选|换|更换|调整|修改|改)",
|
||||
@@ -1962,7 +2127,31 @@ def _coerce_fields(raw) -> list[dict]:
|
||||
continue
|
||||
key = str(item.get("key") or "").strip()
|
||||
label = str(item.get("label") or "").strip()
|
||||
raw_options = item.get("options") or []
|
||||
if isinstance(raw_options, dict):
|
||||
raw_options = [{"value": str(k), "label": str(v)} for k, v in raw_options.items()]
|
||||
options = []
|
||||
for idx, o in enumerate(raw_options, start=1):
|
||||
if isinstance(o, str):
|
||||
s = o.strip()
|
||||
if s:
|
||||
options.append({"value": f"option_{idx}", "label": s})
|
||||
elif isinstance(o, dict):
|
||||
val = o.get("value")
|
||||
lbl = o.get("label") or o.get("text") or o.get("title") or o.get("name")
|
||||
if val is None or str(val).strip() == "":
|
||||
val = lbl if lbl is not None else f"option_{idx}"
|
||||
if lbl is None or str(lbl).strip() == "":
|
||||
lbl = val
|
||||
val_str = str(val).strip()
|
||||
lbl_str = str(lbl).strip()
|
||||
if lbl_str:
|
||||
options.append({"value": val_str, "label": lbl_str})
|
||||
type_ = str(item.get("type") or "").strip()
|
||||
if not type_:
|
||||
type_ = "single" if options else "text"
|
||||
elif type_ in ("choice", "select", "radio"):
|
||||
type_ = "single"
|
||||
if not key or not label or type_ not in FIELD_TYPES:
|
||||
continue
|
||||
field = {
|
||||
@@ -1971,15 +2160,13 @@ def _coerce_fields(raw) -> list[dict]:
|
||||
"type": type_,
|
||||
"required": bool(item.get("required", True)),
|
||||
}
|
||||
options = [
|
||||
{"value": str(o.get("value")), "label": str(o.get("label"))}
|
||||
for o in (item.get("options") or [])
|
||||
if isinstance(o, dict) and o.get("value") and o.get("label")
|
||||
]
|
||||
if type_ in ("single", "multi"):
|
||||
if not options:
|
||||
continue # 单选/多选没选项 = 废卡,丢掉
|
||||
field["options"] = options
|
||||
# 明确提供了具体 options 的选择题(如痛点方向三选一),保留 options,绝不降级转为 asset
|
||||
fields.append(field)
|
||||
continue
|
||||
if type_ == "asset":
|
||||
asset_types = [t for t in (item.get("asset_types") or []) if t in TYPE_LABELS]
|
||||
field["asset_types"] = asset_types or list(TYPE_LABELS)
|
||||
@@ -3324,6 +3511,16 @@ def iter_creation_agent_events(
|
||||
yield {"type": "done"}
|
||||
return
|
||||
|
||||
# 0. 本地上传商品图优先确认品牌与具体品名
|
||||
if product_info_needs_confirmation(conversation, text):
|
||||
question = append_product_info_gate(conversation)
|
||||
set_video_gate_stage(conversation, "clarify")
|
||||
conversation.agent_status = CreationConversation.AgentStatus.AWAITING_USER
|
||||
conversation.save(update_fields=["agent_status", "updated_at"])
|
||||
yield {"type": "message", "message": _message_payload(question)}
|
||||
yield {"type": "done"}
|
||||
return
|
||||
|
||||
# 剧情反转预设必须先选故事深度。平台直接落选择卡,不能把这一步交给模型猜,
|
||||
# 否则默认 15 秒会吞掉 30/60 秒该有的人物关系和冲突发展。
|
||||
if is_plot_twist_conversation(conversation) and not active_plot_twist_story_depth(conversation):
|
||||
@@ -3826,7 +4023,8 @@ def _guided_elicit_text(field: dict) -> str:
|
||||
return label
|
||||
if str(field.get("key") or "") in SESSION_PARAM_KEYS:
|
||||
return label
|
||||
if field.get("type") == "single" and field.get("options"):
|
||||
options = field.get("options")
|
||||
if field.get("type") in ("single", "multi") and isinstance(options, list) and len(options) > 0:
|
||||
return f"{label} 直接点击一个选项,也可以输入自己的想法。"
|
||||
return f"{label} 直接用一句话告诉我就行,不用整理成完整需求。"
|
||||
|
||||
@@ -3852,9 +4050,9 @@ def _elicit_payload_for_fields(
|
||||
|
||||
if primary_type == "product":
|
||||
gate_label = (
|
||||
"这条视频想推哪款商品?可以直接告诉我商品名,或者需要我把商品库列表发给你选吗?"
|
||||
"这条视频想推哪款商品?可以直接告诉我商品的品牌与品名,也可以上传一张商品实物图,或需要我把商品库列表发给你选吗?"
|
||||
if is_video else
|
||||
"这次想做哪款商品?可以直接告诉我商品名,或者需要我把商品库列表发给你选吗?"
|
||||
"这次想做哪款商品?可以直接告诉我商品的品牌与品名,也可以上传一张商品实物图,或需要我把商品库列表发给你选吗?"
|
||||
)
|
||||
else:
|
||||
gate_label = _GATE_LABELS.get(primary_type, _GATE_LABELS["asset"])
|
||||
@@ -3865,15 +4063,18 @@ def _elicit_payload_for_fields(
|
||||
"model": "模特",
|
||||
"scene": "场景",
|
||||
}.get(primary_type, "素材")
|
||||
gate_options = [
|
||||
{"value": "send", "label": f"发{asset_label}列表"},
|
||||
{"value": "auto", "label": "你来推荐"},
|
||||
]
|
||||
if primary_type == "product":
|
||||
gate_options.insert(0, {"value": "upload", "label": "上传商品图"})
|
||||
gate_field = {
|
||||
"key": "_asset_gate",
|
||||
"label": gate_label,
|
||||
"type": "text",
|
||||
"required": False,
|
||||
"options": [
|
||||
{"value": "send", "label": f"发{asset_label}列表"},
|
||||
{"value": "auto", "label": "你来推荐"},
|
||||
],
|
||||
"options": gate_options,
|
||||
}
|
||||
return {
|
||||
# 这是 Agent 的一句自然追问,不是让用户点选流程的卡片。
|
||||
|
||||
Reference in New Issue
Block a user