全能创作闸门与多人物定妆收口
主页已填卖点跳过卖点确认;按图片内容创作不再重复问品牌品名;方案卡支撑改为 P0/P1/P2;多人物视频一次生成多张定妆图并在出片前校验数量;超管登录直进后台。
This commit is contained in:
@@ -506,12 +506,126 @@ def get_pending_video_prompt(conversation: CreationConversation) -> str:
|
||||
return str(memory.get("pending_video_prompt") or "").strip()
|
||||
|
||||
|
||||
_SELLING_POINT_DECLARE_RE = re.compile(
|
||||
r"(?:核心)?卖点\s*(?:是|为|:|:)\s*(.+)",
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
_SELLING_POINT_FOCUS_RE = re.compile(
|
||||
# 「重点讲」必须带「是/为/:」,避免预设文案「重点讲清使用场景…」被当成已填卖点。
|
||||
r"(?:我想?突出(?:的)?|重点突出|主要讲|主打)\s*(?:的是|是|为|:|:)\s*(.+)"
|
||||
r"|重点讲\s*(?:的是|是|为|:|:)\s*(.+)",
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
_SELLING_POINT_BRIEF_CREATIVE_RE = re.compile(
|
||||
r"(?:创作|做[一条]?|视频|帮我|生成|脚本|出片|参考|预设|方向)",
|
||||
)
|
||||
|
||||
|
||||
def extract_declared_selling_point(text: str) -> str:
|
||||
"""从用户话术里抽出已声明的核心卖点。
|
||||
|
||||
主页开场描述常写成「核心卖点是…」或一串短卖点列表;这些都应视为已确认,
|
||||
避免 write_strategy 前再弹一次卖点闸门。
|
||||
"""
|
||||
raw = (text or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
for pattern in (_SELLING_POINT_DECLARE_RE, _SELLING_POINT_FOCUS_RE):
|
||||
match = pattern.search(raw)
|
||||
if not match:
|
||||
continue
|
||||
value = next((group for group in match.groups() if group), "").strip()
|
||||
value = re.split(r"[\n\r]", value)[0].strip()
|
||||
value = value.strip("。;;!!??,,、 ")
|
||||
if len(value) >= 2:
|
||||
return value[:200]
|
||||
# 整段就是短卖点列表(如「补水保湿,便于携带,买三送一」),且不像创作指令
|
||||
if len(raw) <= 80 and not _SELLING_POINT_BRIEF_CREATIVE_RE.search(raw):
|
||||
parts = [part.strip() for part in re.split(r"[,,、;;]", raw) if part.strip()]
|
||||
if len(parts) >= 2 and all(2 <= len(part) <= 24 for part in parts):
|
||||
return raw[:200]
|
||||
return ""
|
||||
|
||||
|
||||
def product_selling_points_summary(conversation: CreationConversation) -> str:
|
||||
"""取已钉商品库商品上的卖点标题,供闸门预填或作为已填卖点。"""
|
||||
from apps.products.models import Product
|
||||
|
||||
for ref in locked_product_references(conversation):
|
||||
if str(ref.get("type") or "") != "product":
|
||||
continue
|
||||
product = (
|
||||
Product.objects.filter(id=ref.get("id"), team_id=conversation.team_id)
|
||||
.prefetch_related("selling_points")
|
||||
.first()
|
||||
)
|
||||
if product is None:
|
||||
continue
|
||||
titles: list[str] = []
|
||||
for point in product.selling_points.order_by("sort_order", "created_at"):
|
||||
title = str(point.title or "").strip()
|
||||
if title and title not in titles:
|
||||
titles.append(title)
|
||||
if titles:
|
||||
return ",".join(titles)[:200]
|
||||
return ""
|
||||
|
||||
|
||||
def lock_selling_point(
|
||||
conversation: CreationConversation,
|
||||
selling_point: str,
|
||||
*,
|
||||
mode: str = "manual",
|
||||
) -> None:
|
||||
memory = dict(conversation.memory or {})
|
||||
memory["selling_point_ready"] = True
|
||||
memory["selling_point_mode"] = mode
|
||||
memory["selling_point"] = selling_point if mode == "manual" else ""
|
||||
conversation.memory = memory
|
||||
conversation.save(update_fields=["memory", "updated_at"])
|
||||
|
||||
|
||||
def ensure_selling_point_ready_from_context(
|
||||
conversation: CreationConversation,
|
||||
user_text: str = "",
|
||||
) -> bool:
|
||||
"""主页/聊天已声明卖点,或商品库已填卖点时,直接锁定,避免重复询问。"""
|
||||
if conversation.mode != CreationConversation.Mode.VIDEO:
|
||||
return False
|
||||
# 痛点解决演示用「方向三选一」把方向写成核心卖点;这里抢先 ready 会跳过那道闸门。
|
||||
if is_pain_point_conversation(conversation):
|
||||
return False
|
||||
memory = conversation.memory if isinstance(conversation.memory, dict) else {}
|
||||
if memory.get("selling_point_ready"):
|
||||
return True
|
||||
|
||||
candidates: list[str] = []
|
||||
declared = extract_declared_selling_point(user_text)
|
||||
if declared:
|
||||
candidates.append(declared)
|
||||
for text in conversation.messages.filter(role=CreationMessage.Role.USER).order_by("seq").values_list("text", flat=True):
|
||||
found = extract_declared_selling_point(text)
|
||||
if found:
|
||||
candidates.append(found)
|
||||
if candidates:
|
||||
# 以最近一次声明为准(主页首条或后续补充)
|
||||
lock_selling_point(conversation, candidates[-1], mode="manual")
|
||||
return True
|
||||
|
||||
summary = product_selling_points_summary(conversation)
|
||||
if summary:
|
||||
lock_selling_point(conversation, summary, mode="manual")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def append_selling_point_gate(conversation: CreationConversation) -> CreationMessage:
|
||||
"""在视频方案生成前确认卖点来源。
|
||||
|
||||
商家可以直接给真实卖点;若交给系统,则后续只从商品资料和参考素材中选择可证实的表达,
|
||||
不把“系统推荐”误做成无依据的夸大文案。
|
||||
"""
|
||||
prefill = product_selling_points_summary(conversation)
|
||||
return append_message(
|
||||
conversation,
|
||||
role="assistant",
|
||||
@@ -529,7 +643,7 @@ def append_selling_point_gate(conversation: CreationConversation) -> CreationMes
|
||||
}
|
||||
],
|
||||
"submitted": False,
|
||||
"answers": {},
|
||||
"answers": {"selling_point": prefill} if prefill else {},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -821,6 +935,12 @@ def product_info_needs_confirmation(conversation: CreationConversation, user_tex
|
||||
if conversation.preset not in _PRODUCT_REQUIRED_PRESETS:
|
||||
return False
|
||||
memory = conversation.memory if isinstance(conversation.memory, dict) else {}
|
||||
# 「按图片内容创作」只标记 resolved、不写真实品牌;也必须视为已确认,否则会反复弹闸门。
|
||||
if memory.get("product_info_resolved") or memory.get("product_info_from_image"):
|
||||
return False
|
||||
probe = str(user_text or "")
|
||||
if any(kw in probe for kw in ("按图片", "设计品牌", "不用再问")):
|
||||
return False
|
||||
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()
|
||||
@@ -828,9 +948,7 @@ def product_info_needs_confirmation(conversation: CreationConversation, user_tex
|
||||
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:
|
||||
if has_real_brand or has_real_name or has_real_combined:
|
||||
return False
|
||||
# 如果已锁定的首个商品是正式商品库商品(已自带品牌和品名),无需重复询问
|
||||
first_prod = next(
|
||||
@@ -850,13 +968,14 @@ def product_info_needs_confirmation(conversation: CreationConversation, user_tex
|
||||
else []
|
||||
)
|
||||
all_text = " ".join(str(m.text or "") for m in user_messages) + " " + (user_text or "")
|
||||
if any(kw in all_text for kw in ("按图片", "设计品牌", "不用再问")):
|
||||
return False
|
||||
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 = (
|
||||
@@ -929,11 +1048,21 @@ def append_person_source_gate(conversation: CreationConversation) -> CreationMes
|
||||
field_label = "选择宠物来源"
|
||||
library_label = "从角色库选择"
|
||||
else:
|
||||
prompt_text = (
|
||||
f"商品已选定【{product_name}】。这条视频想由哪位角色/达人出镜?选定后,所有镜头和分段都会锁定同一位人物。"
|
||||
if product_name else
|
||||
"先确定这条视频的出镜人物。选定后,所有镜头和分段都会锁定同一位人物。"
|
||||
)
|
||||
cast_needed = infer_needed_cast_count(conversation)
|
||||
if cast_needed > 1:
|
||||
prompt_text = (
|
||||
f"商品已选定【{product_name}】。这条视频需要 {cast_needed} 位出镜人物;"
|
||||
"请上传/选择/生成对应数量的角色定妆图,所有镜头和分段都会按这些图锁脸。"
|
||||
if product_name else
|
||||
f"这条视频需要 {cast_needed} 位出镜人物。请上传/选择/生成对应数量的角色定妆图,"
|
||||
"所有镜头和分段都会按这些图锁脸,避免长视频前后形象漂移。"
|
||||
)
|
||||
else:
|
||||
prompt_text = (
|
||||
f"商品已选定【{product_name}】。这条视频想由哪位角色/达人出镜?选定后,所有镜头和分段都会锁定同一位人物。"
|
||||
if product_name else
|
||||
"先确定这条视频的出镜人物。选定后,所有镜头和分段都会锁定同一位人物。"
|
||||
)
|
||||
field_label = "选择人物来源"
|
||||
library_label = "从模特库选择"
|
||||
|
||||
@@ -1087,16 +1216,188 @@ def _character_only_appearance_text(raw: str) -> str:
|
||||
return text[:500]
|
||||
|
||||
|
||||
def _conversation_cast_text_blob(conversation: CreationConversation, extra_text: str = "") -> str:
|
||||
"""汇总可用来判断「几位出镜人物」的文本。"""
|
||||
chunks: list[str] = [str(extra_text or "").strip()]
|
||||
memory = conversation.memory if isinstance(conversation.memory, dict) else {}
|
||||
for key in ("person_prompt", "cast_relation", "pending_video_prompt", "pain_point_direction"):
|
||||
value = str(memory.get(key) or "").strip()
|
||||
if value:
|
||||
chunks.append(value)
|
||||
if getattr(conversation, "created_at", None):
|
||||
for text in conversation.messages.filter(role=CreationMessage.Role.USER).order_by("-seq").values_list("text", flat=True)[:8]:
|
||||
if text and str(text).strip():
|
||||
chunks.append(str(text).strip())
|
||||
for payload in conversation.messages.filter(kind__in=[
|
||||
CreationMessage.Kind.STRATEGY,
|
||||
CreationMessage.Kind.PLAN,
|
||||
]).order_by("-seq").values_list("payload", flat=True)[:4]:
|
||||
if not isinstance(payload, dict):
|
||||
continue
|
||||
for key in ("belief", "direction", "usp", "video_prompt", "target", "trust"):
|
||||
value = str(payload.get(key) or "").strip()
|
||||
if value:
|
||||
chunks.append(value)
|
||||
points = payload.get("points")
|
||||
if isinstance(points, list):
|
||||
chunks.extend(str(item).strip() for item in points if str(item).strip())
|
||||
return "\n".join(chunks)
|
||||
|
||||
|
||||
def infer_needed_cast_count(conversation: CreationConversation, extra_text: str = "") -> int:
|
||||
"""从主页描述/策略/方案推断需要几张角色定妆图。
|
||||
|
||||
多人物长视频若只锁一张脸,分段后很容易前后形象漂移;这里至少按剧情人数生成。
|
||||
宠物预设默认 1;人手上限 4,避免一次刷太多出图。
|
||||
"""
|
||||
if is_pet_preset(conversation.preset):
|
||||
return 1
|
||||
blob = _conversation_cast_text_blob(conversation, extra_text)
|
||||
if not blob.strip():
|
||||
return 1
|
||||
|
||||
explicit = [
|
||||
(5, r"(?:五位|五个|5\s*人|5\s*位|五人)"),
|
||||
(4, r"(?:四位|四个|4\s*人|4\s*位|四人)"),
|
||||
(3, r"(?:三位|三个|3\s*人|3\s*位|三人)"),
|
||||
(2, r"(?:两位|两个|2\s*人|2\s*位|两人|双人|一对|情侣|夫妻|母女|父女|母子|父子|姐妹|兄弟|闺蜜|搭档|CP|男女主|男主.*?女主|女主.*?男主)"),
|
||||
]
|
||||
needed = 1
|
||||
for count, pattern in explicit:
|
||||
if re.search(pattern, blob, flags=re.IGNORECASE | re.DOTALL):
|
||||
needed = max(needed, count)
|
||||
|
||||
# 「角色1 / 角色2」或分行描述多个角色
|
||||
role_labels = re.findall(r"(?:角色|人物|出演)\s*([1-4一二三四])", blob)
|
||||
if len(set(role_labels)) >= 2:
|
||||
mapping = {"1": 1, "2": 2, "3": 3, "4": 4, "一": 1, "二": 2, "三": 3, "四": 4}
|
||||
needed = max(needed, max(mapping.get(item, 1) for item in role_labels))
|
||||
|
||||
split_roles = [
|
||||
part.strip()
|
||||
for part in re.split(r"(?:^|\n)\s*(?:角色|人物)\s*[1-4一二三四][::、..\s]", blob)
|
||||
if part.strip()
|
||||
]
|
||||
if len(split_roles) >= 2:
|
||||
needed = max(needed, min(4, len(split_roles)))
|
||||
|
||||
return max(1, min(4, needed))
|
||||
|
||||
|
||||
def build_cast_role_briefs(count: int, appearance_prompt: str = "") -> list[str]:
|
||||
"""把用户外观描述拆成 N 条;不够时补「与其他角色外貌明显区分」的配角 brief。"""
|
||||
raw = (appearance_prompt or "").strip()
|
||||
briefs: list[str] = []
|
||||
if raw:
|
||||
labeled = re.findall(
|
||||
r"(?:角色|人物)\s*[1-4一二三四][::、..\s]+([^\n;;]+)",
|
||||
raw,
|
||||
)
|
||||
labeled = [part.strip() for part in labeled if part.strip()]
|
||||
if len(labeled) >= 2:
|
||||
briefs = labeled
|
||||
else:
|
||||
numbered = re.split(
|
||||
r"(?:^|[\n;;])\s*(?:角色|人物)\s*[1-4一二三四][::、..\s]+",
|
||||
raw,
|
||||
)
|
||||
numbered = [part.strip() for part in numbered if part.strip()]
|
||||
if len(numbered) >= 2:
|
||||
briefs = numbered
|
||||
else:
|
||||
lined = [line.strip(" -•\t") for line in raw.splitlines() if line.strip()]
|
||||
if len(lined) >= 2:
|
||||
briefs = lined
|
||||
else:
|
||||
parts = [part.strip() for part in re.split(r"[;;//|]", raw) if part.strip()]
|
||||
briefs = parts if len(parts) >= 2 else [raw]
|
||||
|
||||
while len(briefs) < count:
|
||||
index = len(briefs) + 1
|
||||
if index == 1:
|
||||
briefs.append("")
|
||||
else:
|
||||
briefs.append(
|
||||
f"第{index}位出镜角色,成年,五官发型服装气质与其余角色明显不同,"
|
||||
"可作为配角/对手戏人物独立辨认"
|
||||
)
|
||||
return [_character_only_appearance_text(item)[:500] for item in briefs[:count]]
|
||||
|
||||
|
||||
|
||||
def _build_single_person_reference_prompt(
|
||||
*,
|
||||
conversation: CreationConversation,
|
||||
appearance_only: str,
|
||||
context_brief: str,
|
||||
cast_index: int,
|
||||
cast_total: int,
|
||||
) -> tuple[str, str, str]:
|
||||
"""返回 (prompt, generating_label, model_name)。"""
|
||||
is_pet = is_pet_preset(conversation.preset)
|
||||
no_product = (
|
||||
"画面中只允许出现角色本身与简洁背景,禁止出现任何商品、包装、瓶罐、袋装、纸盒、"
|
||||
"电子产品、食品零食、广告道具或手持卖品;不要商品特写,不要把商品画进画面。"
|
||||
)
|
||||
if is_pet:
|
||||
context_text = f"{appearance_only} {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 ("狗", "犬", "咬胶", "磨牙", "骨头", "汪", "狗粮", "犬粮", "幼犬", "成犬", "中大型犬", "小型犬")):
|
||||
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"画面中只出现这一只可爱的宠物动物主角,绝对不要出现真人人类!不要文字、水印、拼图或变形。"
|
||||
f"{no_product}"
|
||||
)
|
||||
if appearance_only:
|
||||
prompt += f" 用户指定的宠物外观与品种:{appearance_only}。严格保留这些宠物外观要求。"
|
||||
label = "正在生成宠物角色参考"
|
||||
model_name = "平台生成宠物角色"
|
||||
else:
|
||||
role_tag = f"第{cast_index}位/共{cast_total}位" if cast_total > 1 else "唯一"
|
||||
prompt = (
|
||||
f"为短视频生成一张可反复用于锁定身份的真人模特定妆参考图({role_tag}出镜角色)。"
|
||||
"只出现一位成年人物,正面或轻微三分之四角度,中近景,表情自然,"
|
||||
"五官、发型、肤色、身形和服装细节清晰,简洁中性背景,写实摄影,"
|
||||
"不要文字、水印、拼图、多人、遮挡脸部或夸张滤镜。"
|
||||
f"{no_product}"
|
||||
)
|
||||
if appearance_only:
|
||||
prompt += f" 用户指定的人物外观:{appearance_only}。严格保留这些外观要求。"
|
||||
if cast_total > 1:
|
||||
prompt += (
|
||||
f" 这是多人物视频中的角色{cast_index},必须与其他角色在性别或发型或年龄段或服装气质上"
|
||||
"有清晰可辨的差异,便于整片锁脸。"
|
||||
)
|
||||
label = f"正在生成人物参考({cast_index}/{cast_total})"
|
||||
model_name = f"平台生成出镜人物{cast_index}"
|
||||
else:
|
||||
label = "正在生成人物参考"
|
||||
model_name = "平台生成出镜人物"
|
||||
return prompt, label, model_name
|
||||
|
||||
|
||||
def submit_generated_person_reference(
|
||||
*,
|
||||
conversation: CreationConversation,
|
||||
user,
|
||||
appearance_prompt: str = "",
|
||||
) -> CreationMessage:
|
||||
"""先生成独立人物/宠物角色定妆参考,完成后再由 creation.py 自动建模特并锁定。
|
||||
cast_count: int | None = None,
|
||||
) -> list:
|
||||
"""生成人物/宠物角色定妆参考;多人物视频会一次生成多张,完成后再建模特并锁定。
|
||||
|
||||
定妆图 prompt 只写角色外观,不写入商品名/卖点/用户创作简述,也不带商品参考图,
|
||||
否则出图模型常会把商品画进角色照。
|
||||
返回 GENERATING 消息列表(单人时长度为 1)。
|
||||
"""
|
||||
from .services import enqueue_standalone_images
|
||||
|
||||
@@ -1111,78 +1412,79 @@ def submit_generated_person_reference(
|
||||
# 仅用于推断宠物品种;绝不拼进最终定妆 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"{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 ("狗", "犬", "咬胶", "磨牙", "骨头", "汪", "狗粮", "犬粮", "幼犬", "成犬", "中大型犬", "小型犬")):
|
||||
pet_type_hint = "一只呆萌可爱、神采奕奕的小狗(如金毛幼犬、柯基或柴犬等萌犬)"
|
||||
elif any(w in context_text for w in ("猫", "喵", "猫砂", "猫条", "猫粮", "冻干", "猫草", "逗猫", "幼猫")):
|
||||
pet_type_hint = "一只大眼睛、圆脸可爱的萌宠猫咪(如英短、美短或布偶等萌猫)"
|
||||
else:
|
||||
pet_type_hint = "一只可爱灵动的萌宠(如呆萌小狗或可爱猫咪)"
|
||||
total = cast_count if cast_count is not None else infer_needed_cast_count(conversation, raw_appearance)
|
||||
total = max(1, min(4, int(total or 1)))
|
||||
if is_pet_preset(conversation.preset):
|
||||
total = 1
|
||||
role_briefs = build_cast_role_briefs(total, raw_appearance)
|
||||
|
||||
prompt = (
|
||||
f"为AI宠物拟人短视频生成一张可反复用于锁定角色身份的萌宠主角定妆参考图。"
|
||||
f"主角必须是{pet_type_hint},展现拟人化的生动表情与灵动神态,"
|
||||
f"正面或微侧三分之四角度,中近景特写,眼神清澈,毛发蓬松有光泽且根根分明,"
|
||||
f"可搭配精致简约的拟人小配饰(如可爱小领结、小方巾或背带,契合萌宠调性),"
|
||||
f"写实摄影风格,电影级柔和摄影棚光影,简洁纯净背景,"
|
||||
f"画面中只出现这一只可爱的宠物动物主角,绝对不要出现真人人类!不要文字、水印、拼图或变形。"
|
||||
f"{no_product}"
|
||||
)
|
||||
if appearance_only:
|
||||
prompt += f" 用户指定的宠物外观与品种:{appearance_only}。严格保留这些宠物外观要求。"
|
||||
label = "正在生成宠物角色参考"
|
||||
else:
|
||||
prompt = (
|
||||
"为短视频生成一张可反复用于锁定身份的真人模特定妆参考图。"
|
||||
"只出现一位成年人物,正面或轻微三分之四角度,中近景,表情自然,"
|
||||
"五官、发型、肤色、身形和服装细节清晰,简洁中性背景,写实摄影,"
|
||||
"不要文字、水印、拼图、多人、遮挡脸部或夸张滤镜。"
|
||||
f"{no_product}"
|
||||
)
|
||||
if appearance_only:
|
||||
prompt += f" 用户指定的人物外观:{appearance_only}。严格保留这些外观要求。"
|
||||
label = "正在生成人物参考"
|
||||
|
||||
tasks = enqueue_standalone_images(
|
||||
team=conversation.team,
|
||||
user=user,
|
||||
prompt=prompt,
|
||||
mode="model",
|
||||
count=1,
|
||||
ratio="portrait",
|
||||
feature="omni_create",
|
||||
)
|
||||
task = tasks[0]
|
||||
memory = dict(conversation.memory or {})
|
||||
memory["person_source"] = "platform_generate"
|
||||
memory["person_source_pending"] = True
|
||||
memory["person_prompt"] = appearance_prompt
|
||||
memory["person_cast_total"] = total
|
||||
memory["person_cast_pending"] = total
|
||||
memory["person_cast_model_ids"] = []
|
||||
memory.pop("person_confirm_pending", None)
|
||||
memory.pop("person_source_ready", None)
|
||||
memory.pop("person_model_id", None)
|
||||
conversation.memory = memory
|
||||
conversation.status = CreationConversation.Status.RUNNING
|
||||
conversation.agent_status = CreationConversation.AgentStatus.IDLE
|
||||
conversation.save(update_fields=["memory", "status", "agent_status", "updated_at"])
|
||||
return append_message(
|
||||
conversation,
|
||||
role="assistant",
|
||||
kind=CreationMessage.Kind.GENERATING,
|
||||
payload={
|
||||
"task_id": str(task.id),
|
||||
"kind": "person_reference",
|
||||
"prompt": prompt,
|
||||
"label": label,
|
||||
},
|
||||
task=task,
|
||||
|
||||
messages = []
|
||||
for index, brief in enumerate(role_briefs, start=1):
|
||||
prompt, label, model_name = _build_single_person_reference_prompt(
|
||||
conversation=conversation,
|
||||
appearance_only=brief,
|
||||
context_brief=context_brief,
|
||||
cast_index=index,
|
||||
cast_total=total,
|
||||
)
|
||||
tasks = enqueue_standalone_images(
|
||||
team=conversation.team,
|
||||
user=user,
|
||||
prompt=prompt,
|
||||
mode="model",
|
||||
count=1,
|
||||
ratio="portrait",
|
||||
feature="omni_create",
|
||||
)
|
||||
task = tasks[0]
|
||||
messages.append(
|
||||
append_message(
|
||||
conversation,
|
||||
role="assistant",
|
||||
kind=CreationMessage.Kind.GENERATING,
|
||||
payload={
|
||||
"task_id": str(task.id),
|
||||
"kind": "person_reference",
|
||||
"prompt": prompt,
|
||||
"label": label,
|
||||
"cast_index": index,
|
||||
"cast_total": total,
|
||||
"cast_model_name": model_name,
|
||||
},
|
||||
task=task,
|
||||
)
|
||||
)
|
||||
return messages
|
||||
|
||||
|
||||
def insufficient_cast_refs_message(conversation: CreationConversation, prompt: str = "") -> str:
|
||||
"""出片前:多人物但角色图不够时给出阻断文案。"""
|
||||
memory = conversation.memory if isinstance(conversation.memory, dict) else {}
|
||||
if str(memory.get("person_source") or "") == "finger_only":
|
||||
return ""
|
||||
needed = infer_needed_cast_count(conversation, prompt)
|
||||
have = len(locked_person_references(conversation))
|
||||
if needed <= have:
|
||||
return ""
|
||||
return (
|
||||
f"这是多人物视频,需要 {needed} 张角色定妆图锁定前后形象,当前只有 {have} 张。"
|
||||
"请先用「平台帮忙生成」一次生成多位角色,或从模特库/本地补齐后再出片。"
|
||||
)
|
||||
|
||||
|
||||
@@ -3319,6 +3621,10 @@ def submit_confirmed_video(*, conversation: CreationConversation, user, confirm_
|
||||
if not prompt:
|
||||
return None, "这条方案没有存下出片指令,请让我重新写一次方案。"
|
||||
|
||||
cast_gap = insufficient_cast_refs_message(conversation, prompt)
|
||||
if cast_gap:
|
||||
return None, cast_gap
|
||||
|
||||
context = AgentContext(conversation=conversation, user=user, model_config=None)
|
||||
submit, _references = _video_submit_params(context, prompt)
|
||||
total_duration = int(submit["duration"])
|
||||
@@ -4027,7 +4333,7 @@ def _coerce_timeline(raw) -> list[dict]:
|
||||
def _default_plan_matrix(usp: str, points: list[str]) -> dict:
|
||||
"""设计稿里的「卖点覆盖矩阵」:有 USP/支撑点就自动铺一版,避免方案卡干瘪。"""
|
||||
rows = [{"point": "主打卖点 USP", "hits": [1, 3]}]
|
||||
labels = ["体验卖点 P0", "视觉卖点 P0", "转化卖点 P0"]
|
||||
labels = ["核心支撑 P0", "视觉支撑 P1", "转化支撑 P2"]
|
||||
for index, point in enumerate(points[:3]):
|
||||
label = labels[index] if index < len(labels) else f"支撑卖点 P{index}"
|
||||
# 点名用文案前缀,hits 错落分布到 4 镜
|
||||
@@ -4276,6 +4582,9 @@ def iter_creation_agent_events(
|
||||
user_message = append_message(conversation, role="user", text=text, refs=refs)
|
||||
if user_message is not None:
|
||||
yield {"type": "message", "message": _message_payload(user_message)}
|
||||
# 主页/聊天已写卖点时尽早锁定,避免模型再追问或 write_strategy 弹闸门
|
||||
if conversation.mode == CreationConversation.Mode.VIDEO:
|
||||
ensure_selling_point_ready_from_context(conversation, text)
|
||||
set_public_agent_progress(conversation, "starting")
|
||||
# brief 里直接写「做一条 120 秒视频」就是明确参数,不能仍沿用顶部的智能时长。
|
||||
# 要在任何创作闸门之前落库,后面的策略、脚本和出片分段才会统一使用该时长。
|
||||
@@ -5191,6 +5500,10 @@ def _dispatch_tool(
|
||||
)
|
||||
}
|
||||
}, False
|
||||
if context.is_video and not memory.get("selling_point_ready"):
|
||||
# 主页开场描述 / 聊天声明 / 商品库已填卖点 → 直接锁定,不再弹闸门
|
||||
ensure_selling_point_ready_from_context(context.conversation)
|
||||
memory = context.conversation.memory if isinstance(context.conversation.memory, dict) else {}
|
||||
if context.is_video and is_pain_point_conversation(context.conversation) and not memory.get("selling_point_ready"):
|
||||
return {
|
||||
"payload": {
|
||||
|
||||
Reference in New Issue
Block a user