优化只能创作里的图片创作
This commit is contained in:
@@ -27,7 +27,7 @@ from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import transaction
|
||||
|
||||
from .creation import append_message, pin_refs
|
||||
from .creation_presets import preset_guidance
|
||||
from .creation_presets import apply_image_preset_prompt, preset_guidance
|
||||
from .mentions import TYPE_LABELS, infer_field_types, resolve_refs, search_mentions
|
||||
from .models import CreationConversation, CreationMessage, ModelConfig
|
||||
from .services import build_provider, get_default_model, get_seed_text_model, resolve_text_model
|
||||
@@ -188,6 +188,7 @@ def emit_prompt_gate(
|
||||
if not prompt:
|
||||
return []
|
||||
prompt = apply_product_voice_visual_guard(conversation, prompt)
|
||||
prompt = apply_product_reality_guard(prompt)
|
||||
messages = []
|
||||
prompt_file = append_message(
|
||||
conversation,
|
||||
@@ -225,6 +226,7 @@ def emit_final_confirm_gate(
|
||||
if not prompt:
|
||||
return None
|
||||
prompt = apply_product_voice_visual_guard(conversation, prompt)
|
||||
prompt = apply_product_reality_guard(prompt)
|
||||
credits = 0
|
||||
if context is not None:
|
||||
try:
|
||||
@@ -321,6 +323,9 @@ video_prompt 是交给出片模型的完整制作文件,不是方案摘要、
|
||||
- 15 秒 Prompt 通常不少于约 1200 个汉字;短片按时长同比例展开。每一段都要有可拍的连续动作、物理反馈或产品证据,不能用「高级感、展示质感、氛围拉满」代替动作。
|
||||
- 人声必须是自然口语,约 5.0–5.7 字/秒;钩子前 15 个字禁止「大家好 / 今天分享 / 给你们推荐」。CTA 像朋友提醒,禁止小黄车、立即购买、闭眼入等平台指令腔。
|
||||
- 全片围绕一个具体情境和一个主卖点推进,卖点必须有可见证据,例如质地、使用动作、前后变化或真实反应。
|
||||
- 商品演示先过“真实用途与状态”检查:只展示商品在其已知用途和已提供事实范围内的正常、完整状态;
|
||||
禁止无依据出现破损、漏液、渗水、失效、异常变形、脏污,或把商品拿去做超出用途的压力测试。
|
||||
不确定防水、防漏、承重、耐热、容量、材质等关键能力时,不编造测试和结果;要么问用户,要么改用可观察的正常使用动作。
|
||||
- 画面中不出现新增字幕、花字、标题贴片、弹幕、角标、水印、购物浮层或说明性文字;口播只存在于声音,包装本身原有印刷字除外。
|
||||
- 结尾单列「全片一致性与禁用项」:重申角色、商品、场景、光线、服装/材质的连续性,以及本片最重要的画面禁用项。
|
||||
- 用户说「商品说话 / 商品自述 / 商品拟人」时,默认无脸拟人:商品声音是画外角色声,商品本体不做口型、不新增卡通五官;性格只通过整体倾斜、转向、弹跳、进退、镜头和音效表达。只有用户明确要求可见卡通五官时才例外。
|
||||
@@ -345,6 +350,15 @@ PRODUCT_VOICE_VISUAL_GUARD = (
|
||||
"与克制幽默。前文若有改造商品外观的动作描述,统一改成商品整体运动的对应表达。"
|
||||
)
|
||||
|
||||
# 这段在最终出片 Prompt 上再加一道确定性约束,避免模型虽然在方案里写了“真实使用”,
|
||||
# 但生成时又把商品拍进不合理的失效状态。它不替用户补商品性能,只限制无依据的错误演示。
|
||||
PRODUCT_REALITY_GUARD = (
|
||||
"【商品真实使用约束·最高优先级】商品只按已知真实用途和用户提供的事实展示,"
|
||||
"全程保持正常、完整、可用的状态。没有用户明确提供的性能依据时,不出现破损、漏液、渗水、"
|
||||
"失效、异常变形、脏污、超载或超出用途的测试;也不把不确定的防水、防漏、承重、耐热、容量、"
|
||||
"材质等能力拍成已被验证的结果。若关键信息不足,使用可观察的正常操作替代夸张测试。"
|
||||
)
|
||||
|
||||
|
||||
def apply_product_voice_visual_guard(conversation: CreationConversation, prompt: str) -> str:
|
||||
"""商品拟人默认不长脸;用户明确要求可见卡通五官时尊重其创作选择。"""
|
||||
@@ -367,6 +381,14 @@ def apply_product_voice_visual_guard(conversation: CreationConversation, prompt:
|
||||
return f"{base}\n{PRODUCT_VOICE_VISUAL_GUARD}".strip()
|
||||
|
||||
|
||||
def apply_product_reality_guard(prompt: str) -> str:
|
||||
"""所有商品视频在出片前补上物理与用途边界,避免最终模型误演“产品坏了”。"""
|
||||
base = str(prompt or "").strip()
|
||||
if not base or PRODUCT_REALITY_GUARD in base:
|
||||
return base
|
||||
return f"{base}\n{PRODUCT_REALITY_GUARD}".strip()
|
||||
|
||||
|
||||
class AgentError(Exception):
|
||||
"""Agent 循环里的业务错误,已经是可以直接给用户看的中文。"""
|
||||
|
||||
@@ -444,9 +466,8 @@ def is_pure_chitchat(user_text: str) -> bool:
|
||||
|
||||
_GUIDANCE_MARKER_RE = re.compile(
|
||||
r"("
|
||||
r"请回复|请直接|直接说|直接丢|告诉我|可以回|下一步|"
|
||||
r"想改|点「|回复:|回复:|发给我|选一个|怎么回|"
|
||||
r"继续补充|想做什么"
|
||||
r"请回复|回复[::]|请直接|请告诉我|请选|点「|直接输入|"
|
||||
r"选择一个|发「|例如「"
|
||||
r")"
|
||||
)
|
||||
|
||||
@@ -474,19 +495,65 @@ def default_reply_hint(
|
||||
if stage == "confirm":
|
||||
return "请在确认卡上核对参数后点「开始生成」,或直接说要改的参数。"
|
||||
if stage == "done":
|
||||
return "想改哪一处直接说;要重新来就回复「重新来」。"
|
||||
return "可以回复「再出一版」,也可以选下方想调整的部分。"
|
||||
if has_context:
|
||||
return "请直接说下一步:改哪里、换素材,或继续推进。"
|
||||
if is_video:
|
||||
return "可以回复「继续完善方案」,也可以选下方想调整的方向。"
|
||||
return "可以回复「按这个方向出图」,也可以选下方想调整的方向。"
|
||||
kind = "短视频" if is_video else "商品图"
|
||||
return f"请直接丢一句想法,例如「帮我做一条{kind}」。"
|
||||
|
||||
|
||||
def apply_reply_hint(message: CreationMessage, hint: str) -> CreationMessage:
|
||||
"""给文字气泡挂上 reply_hint,供前端展示「下一步可以这样回」。"""
|
||||
def default_reply_options(
|
||||
conversation: CreationConversation | None = None,
|
||||
*,
|
||||
has_context: bool = False,
|
||||
is_video: bool = True,
|
||||
) -> list[dict[str, str]]:
|
||||
"""纯文字收束时给前端的可点下一步,避免用户面对一段说明不知道怎么继续。"""
|
||||
stage = ""
|
||||
if conversation is not None:
|
||||
try:
|
||||
stage = get_video_gate_stage(conversation)
|
||||
except Exception: # noqa: BLE001
|
||||
stage = ""
|
||||
if stage == "done":
|
||||
return [
|
||||
{"label": "再出一版", "text": "按当前方向再出一版"},
|
||||
{"label": "调整画面", "text": "我想调整画面"},
|
||||
{"label": "重新来", "text": "重新来"},
|
||||
]
|
||||
if has_context:
|
||||
if is_video:
|
||||
return [
|
||||
{"label": "继续完善方案", "text": "继续完善方案"},
|
||||
{"label": "换个场景", "text": "我想换个场景"},
|
||||
{"label": "改卖点", "text": "我想改卖点"},
|
||||
]
|
||||
return [
|
||||
{"label": "按这个方向出图", "text": "按这个方向出图"},
|
||||
{"label": "换个场景", "text": "我想换个场景"},
|
||||
{"label": "改人物状态", "text": "我想改人物状态"},
|
||||
]
|
||||
kind = "短视频" if is_video else "商品图"
|
||||
return [
|
||||
{"label": f"帮我做一条{kind}" if is_video else "帮我做一张商品图", "text": f"帮我做一条{kind}" if is_video else "帮我做一张商品图"},
|
||||
{"label": "我先说想法", "text": "我想做一个新的创作"},
|
||||
]
|
||||
|
||||
|
||||
def apply_reply_hint(message: CreationMessage, hint: str, options: list[dict[str, str]]) -> CreationMessage:
|
||||
"""给文字气泡挂上回复示例和可点选项,供前端把下一步直接交到用户手里。"""
|
||||
payload = dict(message.payload or {})
|
||||
if str(payload.get("reply_hint") or "").strip():
|
||||
changed = False
|
||||
if not str(payload.get("reply_hint") or "").strip():
|
||||
payload["reply_hint"] = hint
|
||||
changed = True
|
||||
if not isinstance(payload.get("reply_options"), list) or not payload.get("reply_options"):
|
||||
payload["reply_options"] = options
|
||||
changed = True
|
||||
if not changed:
|
||||
return message
|
||||
payload["reply_hint"] = hint
|
||||
message.payload = payload
|
||||
message.save(update_fields=["payload"])
|
||||
return message
|
||||
@@ -516,11 +583,12 @@ def ensure_turn_guides(
|
||||
return []
|
||||
|
||||
hint = default_reply_hint(conversation, has_context=has_context, is_video=is_video)
|
||||
options = default_reply_options(conversation, has_context=has_context, is_video=is_video)
|
||||
events: list[dict] = []
|
||||
if last_text_bubble is not None:
|
||||
if text_already_guides(last_text_bubble.text):
|
||||
return []
|
||||
updated = apply_reply_hint(last_text_bubble, hint)
|
||||
updated = apply_reply_hint(last_text_bubble, hint, options)
|
||||
events.append({"type": "message", "message": _message_payload(updated)})
|
||||
return events
|
||||
|
||||
@@ -528,7 +596,7 @@ def ensure_turn_guides(
|
||||
conversation,
|
||||
role="assistant",
|
||||
text=hint,
|
||||
payload={"reply_hint": hint},
|
||||
payload={"reply_hint": hint, "reply_options": options},
|
||||
)
|
||||
events.append({"type": "message", "message": _message_payload(guide)})
|
||||
return events
|
||||
@@ -542,7 +610,7 @@ _CREATIVE_INTENT_RE = re.compile(
|
||||
r"("
|
||||
r"帮我做|帮我拍|帮我出|帮我写|帮我改|帮我生成|"
|
||||
r"创作[一两]?[条个张]?|创作(?:一条|个|短)?|"
|
||||
r"做[一两]?[条个张](?:视频|片|图|广告)?|拍[一两]?[条个](?:视频|片|广告)?|"
|
||||
r"做[一两]?[条个张](?:视频|片|图|广告)?|拍[一两]?[条个张](?:视频|片|图|广告)?|"
|
||||
r"出[一两]?[条个张](?:视频|片|图|广告)?|出片|出图|出方案|写方案|改方案|重写方案|重新写|"
|
||||
r"生成(?:一下|一张|几张|一条)?(?:视频|图|片|广告|脚本)?|做条|做个片|短视频|带货视频|短广告|广告片|带货片|"
|
||||
r"换卖点|改卖点|换剧情|改剧情|重做|重新出|重新来|再来一次|从头开始|重新做|按这个出|确认出片|"
|
||||
@@ -1207,6 +1275,7 @@ def _run_generate_image(context: AgentContext, args: dict) -> tuple[dict, list]:
|
||||
prompt = str(args.get("prompt") or "").strip()
|
||||
if not prompt:
|
||||
raise AgentError("生成失败:模型没有给出画面描述")
|
||||
prompt = apply_image_preset_prompt(context.conversation.preset, prompt)
|
||||
|
||||
params = context.conversation.params or {}
|
||||
resolved = resolve_refs(context.team, context.conversation.pinned_refs or [])
|
||||
@@ -1237,6 +1306,7 @@ def _video_submit_params(context: AgentContext, prompt: str) -> tuple[dict, list
|
||||
params = context.conversation.params or {}
|
||||
resolved = resolve_refs(context.team, context.conversation.pinned_refs or [])
|
||||
prompt = apply_product_voice_visual_guard(context.conversation, prompt)
|
||||
prompt = apply_product_reality_guard(prompt)
|
||||
submit = {
|
||||
"prompt": prompt,
|
||||
"feature": "omni_create",
|
||||
@@ -1493,6 +1563,10 @@ def build_system_prompt(context: AgentContext, *, allow_plan: bool = True, has_c
|
||||
"- 每一轮回复结束时,用户必须知道下一步怎么做:要么调用 ask_user 弹出可选项,"
|
||||
"要么在文字里明确告诉用户该回复什么(例如「请回复:1… 2…」或「直接说想改的卖点」)。",
|
||||
"- 禁止只丢一段解释/分析就结束、让用户不知道该回什么。",
|
||||
"- 用户已经给出可直接执行的图片需求(主体、场景或氛围已足够)时,直接调用 generate_image 进入确认卡;"
|
||||
"不要只描述你准备怎么拍,再让用户继续补一句。",
|
||||
"- 禁止用「有想调整可以直接说」这类泛泛收尾代替引导;要么 ask_user 给出可选项,"
|
||||
"要么给出一句用户可以直接点击或复制回复的话。",
|
||||
"- 缺信息或要用户做选择时:优先 ask_user(带 options 的 single/multi,或 asset)。",
|
||||
"- 纯说明/判断/闲聊回应:文末必须带一句可执行的回复指引。",
|
||||
"- 视频闸门的 step_confirm / 积分确认卡本身已是引导,不要再口头追问流程。",
|
||||
@@ -1922,6 +1996,9 @@ def iter_creation_agent_events(
|
||||
"reply_hint": default_reply_hint(
|
||||
conversation, has_context=False, is_video=conversation.mode == CreationConversation.Mode.VIDEO
|
||||
),
|
||||
"reply_options": default_reply_options(
|
||||
conversation, has_context=False, is_video=conversation.mode == CreationConversation.Mode.VIDEO
|
||||
),
|
||||
},
|
||||
)
|
||||
yield {"type": "message", "message": _message_payload(reply)}
|
||||
@@ -1941,6 +2018,9 @@ def iter_creation_agent_events(
|
||||
"reply_hint": default_reply_hint(
|
||||
conversation, has_context=False, is_video=conversation.mode == CreationConversation.Mode.VIDEO
|
||||
),
|
||||
"reply_options": default_reply_options(
|
||||
conversation, has_context=False, is_video=conversation.mode == CreationConversation.Mode.VIDEO
|
||||
),
|
||||
},
|
||||
)
|
||||
yield {"type": "message", "message": _message_payload(reply)}
|
||||
@@ -2414,6 +2494,7 @@ def _dispatch_tool(
|
||||
if not video_prompt:
|
||||
return {"payload": {"error": "video_prompt 不能为空,请把完整出片指令写进去"}}, False
|
||||
video_prompt = apply_product_voice_visual_guard(context.conversation, video_prompt)
|
||||
video_prompt = apply_product_reality_guard(video_prompt)
|
||||
card = _coerce_plan_card_args(args if isinstance(args, dict) else {})
|
||||
if not card["usp"] or not card["points"]:
|
||||
return {
|
||||
@@ -2463,6 +2544,7 @@ def _dispatch_tool(
|
||||
if not video_prompt:
|
||||
return {"payload": {"error": "video_prompt 不能为空,请把完整出片指令写进去"}}, False
|
||||
video_prompt = apply_product_voice_visual_guard(context.conversation, video_prompt)
|
||||
video_prompt = apply_product_reality_guard(video_prompt)
|
||||
emitted = emit_prompt_gate(context.conversation, video_prompt)
|
||||
if not emitted:
|
||||
return {"payload": {"error": "无法写出 Prompt 卡"}}, False
|
||||
@@ -2479,6 +2561,7 @@ def _dispatch_tool(
|
||||
prompt = str(args.get("prompt") or "").strip()
|
||||
if not prompt:
|
||||
return {"payload": {"error": "生成失败:模型没有给出画面描述"}}, False
|
||||
prompt = apply_image_preset_prompt(context.conversation.preset, prompt)
|
||||
# 出图也走确认卡:用户先看当前模型/比例/张数,点了才提交。
|
||||
credits = estimate_image_credits(context)
|
||||
confirm = append_message(
|
||||
|
||||
Reference in New Issue
Block a user