优化只能创作里的图片创作

This commit is contained in:
Azmat@qq.com
2026-09-14 18:56:07 +08:00
parent 3fed9b9435
commit f15fe21b1a
16 changed files with 540 additions and 106 deletions
+96 -13
View File
@@ -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(