优化全能创作
Deploy dev / deploy (push) Successful in 38s

This commit is contained in:
Azmat@qq.com
2026-09-29 10:46:59 +08:00
parent 4421706290
commit 1130ecd4ba
9 changed files with 574 additions and 65 deletions
+151 -14
View File
@@ -65,6 +65,8 @@ CREATION_AGENT_MODEL_TIMEOUT_SECONDS = 180
CREATION_AGENT_TURN_TIMEOUT_SECONDS = 420
# 输出被 max_tokens 截断后最多让模型重写几次;再失败就明确告诉用户,不再闷着重试到超时。
MAX_TRUNCATION_RETRIES = 2
# 已明确要求推进创作时,模型若只回说明文字而没调工具,最多强制纠正两次。
MAX_PROGRESS_TOOL_RETRIES = 2
LONG_VIDEO_DURATION_SLACK_SECONDS = 2
# 单条用户消息最多触发一次计费生成(契约 §4)
MAX_BILLED_GENERATIONS = 1
@@ -145,6 +147,16 @@ TRUNCATION_GIVE_UP_NOTICE = (
"可以把时长改短一点,或直接点「按这个继续」让我按更紧凑的结构重写一次。"
)
PROGRESS_TOOL_RETRY_INSTRUCTION = (
"用户已经明确授权继续创作,本轮不能用说明文字、建议或‘继续完善方案’结束。"
"信息不足就调用 ask_user;信息足够就立即调用 write_strategy,并在同一轮继续调用 write_plan,"
"最终必须落下可见的视频架构卡。不要输出工具调用以外的解释文字。"
)
PROGRESS_TOOL_GIVE_UP_NOTICE = (
"这次没有成功整理出视频方案,已经停止。请把刚才的要求再发一次,我会直接生成方案卡。"
)
def long_video_script_covers_requested_duration(
raw_duration: int | None,
@@ -195,6 +207,17 @@ def set_public_agent_progress(
):
return
history = list(memory.get("agent_progress_history") or [])
# reasoning 可能在「理解需求 / 商品 / 角色」之间来回切换。再次回到同一阶段时
# 把旧位置移除后放到末尾,既保留当前顺序,也不让加载卡重复刷同一句话。
history = [
item
for item in history
if not (
isinstance(item, dict)
and str(item.get("phase") or "") == phase
and str(item.get("detail_key") or "") == detail_key
)
]
history.append({"phase": phase, "detail_key": detail_key})
memory["agent_progress_history"] = history[-12:]
memory["agent_progress_phase"] = phase
@@ -1446,6 +1469,70 @@ def build_cast_role_briefs(count: int, appearance_prompt: str = "") -> list[str]
return [_character_only_appearance_text(item)[:500] for item in briefs[:count]]
_FEMALE_CAST_WORDS = r"女性|女生|女人|女孩|女演员|女模特|女士|女主|妈妈|母亲|女儿|姐姐|妹妹"
_MALE_CAST_WORDS = r"男性|男生|男人|男孩|男演员|男模特|男士|男主|爸爸|父亲|儿子|哥哥|弟弟"
_CAST_COUNT_WORDS = {"一": 1, "1": 1, "两": 2, "二": 2, "2": 2, "三": 3, "3": 3, "四": 4, "4": 4}
def _cast_gender_from_text(text: str) -> str:
"""只读明确的角色性别描述;不从商品或风格词推断。"""
female = bool(re.search(_FEMALE_CAST_WORDS, text))
male = bool(re.search(_MALE_CAST_WORDS, text))
if female == male:
return ""
return "女性" if female else "男性"
def _shared_cast_gender_from_text(text: str, total: int) -> str:
"""仅当文本明确说整组角色同一性别时,才把性别约束复制给每张定妆图。"""
matches = re.finditer(
rf"(?<!\d)([一1两二2三3四4])\s*(?:位|名|个|人)?\s*(?:成年|年轻|中年|年长)?\s*"
rf"({_FEMALE_CAST_WORDS}|{_MALE_CAST_WORDS})",
text,
)
genders = {
_cast_gender_from_text(match.group(2))
for match in matches
if _CAST_COUNT_WORDS.get(match.group(1)) == total
}
genders.discard("")
if len(genders) == 1:
return genders.pop()
if genders:
return ""
if total == 2:
if re.search(r"母女|姐妹|闺蜜|双女主", text) and not re.search(r"父子|兄弟|双男主|一男一女|男女主", text):
return "女性"
if re.search(r"父子|兄弟|双男主", text) and not re.search(r"母女|姐妹|闺蜜|双女主|一男一女|男女主", text):
return "男性"
return ""
def _cast_gender_requirements(
conversation: CreationConversation,
role_briefs: list[str],
appearance_prompt: str,
) -> list[str]:
"""外观输入优先,其次读取已确认的视频 Prompt;只传性别,不带商品文案。"""
total = len(role_briefs)
memory = conversation.memory if isinstance(conversation.memory, dict) else {}
sources = [
appearance_prompt,
str(memory.get("pending_video_prompt") or ""),
]
if getattr(conversation, "created_at", None):
sources.extend(
str(item or "")
for item in conversation.messages.filter(role=CreationMessage.Role.USER)
.order_by("-seq").values_list("text", flat=True)[:6]
)
shared = next(
(gender for source in sources if (gender := _shared_cast_gender_from_text(source, total))),
"",
)
return [_cast_gender_from_text(brief) or shared for brief in role_briefs]
def _build_single_person_reference_prompt(
*,
@@ -1454,6 +1541,7 @@ def _build_single_person_reference_prompt(
context_brief: str,
cast_index: int,
cast_total: int,
gender_requirement: str = "",
) -> tuple[str, str, str]:
"""返回 (prompt, generating_label, model_name)。"""
is_pet = is_pet_preset(conversation.preset)
@@ -1486,9 +1574,13 @@ def _build_single_person_reference_prompt(
model_name = "平台生成宠物角色"
else:
role_tag = f"第{cast_index}位/共{cast_total}位" if cast_total > 1 else "唯一"
gender_instruction = (
f"这位角色必须是成年{gender_requirement},外貌和性别不得改变。"
if gender_requirement else ""
)
prompt = (
f"为短视频生成一张可反复用于锁定身份的真人模特定妆参考图({role_tag}出镜角色)。"
"只出现一位成年人物,正面或轻微三分之四角度,中近景,表情自然,"
f"只出现一位成年人物。{gender_instruction}正面或轻微三分之四角度,中近景,表情自然,"
"五官、发型、肤色、身形和服装细节清晰,简洁中性背景,写实摄影,"
"不要文字、水印、拼图、多人、遮挡脸部或夸张滤镜。"
f"{no_product}"
@@ -1497,7 +1589,7 @@ def _build_single_person_reference_prompt(
prompt += f" 用户指定的人物外观:{appearance_only}。严格保留这些外观要求。"
if cast_total > 1:
prompt += (
f" 这是多人物视频中的角色{cast_index},必须与其他角色在性别或发型或年龄段或服装气质上"
f" 这是多人物视频中的角色{cast_index},必须与其他角色在发型、年龄段或服装气质上"
"有清晰可辨的差异,便于整片锁脸。"
)
label = f"正在生成人物参考({cast_index}/{cast_total})"
@@ -1542,6 +1634,7 @@ def submit_generated_person_reference(
if is_pet_preset(conversation.preset):
total = 1
role_briefs = build_cast_role_briefs(total, raw_appearance)
gender_requirements = _cast_gender_requirements(conversation, role_briefs, raw_appearance)
memory = dict(conversation.memory or {})
memory["person_source"] = "platform_generate"
@@ -1566,6 +1659,7 @@ def submit_generated_person_reference(
context_brief=context_brief,
cast_index=index,
cast_total=total,
gender_requirement=gender_requirements[index - 1],
)
tasks = enqueue_standalone_images(
team=conversation.team,
@@ -1876,6 +1970,8 @@ VIDEO_MODEL_BY_LABEL = {
"Seedance 2.0 Fast": "doubao-seedance-2-0-fast-260128",
"Seedance 2.0 Mini": "doubao-seedance-2-0-mini-260615",
}
FAST_VIDEO_MODEL = "doubao-seedance-2-0-fast-260128"
FAST_VIDEO_MODEL_LABEL = "Seedance 2.0 Fast"
DEFAULT_VIDEO_MODEL = "doubao-seedance-2-5-260628"
IMAGE_MODEL_BY_LABEL = {
"Seedream5.0": "volcano",
@@ -2588,6 +2684,7 @@ _CONTINUE_INTENT_RE = re.compile(
r"^\s*("
r"继续|继续做|接着|接着做|往下做|开始吧|开做吧|就这样|就按这个|按这个来|照这个做|直接做|直接来|"
r"继续完善方案|完善方案|继续创作|继续推进|往下推进|"
r"推荐|推荐一下|你来推荐|帮我推荐|按你推荐(?:的)?|你来定|你决定|按你来|你看着办|随便|都行|"
r"按当前描述继续|按这个继续|没有其他调整[,,]?按当前描述继续|"
r"使用这[个位只]角色继续创作|使用这[个位只]宠物角色继续创作"
r")[吧啊呀呢。.!!]*\s*$"
@@ -2873,6 +2970,7 @@ def apply_session_params(conversation, fields, answers: dict) -> bool:
current = dict(conversation.params or {})
field_by_key = {str(item.get("key") or ""): item for item in (fields or []) if isinstance(item, dict)}
changed = False
model_user_selected = False
for key, raw in (answers or {}).items():
field = field_by_key.get(str(key)) or {}
if field.get("type") == "asset":
@@ -2889,9 +2987,17 @@ def apply_session_params(conversation, fields, answers: dict) -> bool:
value = f"{MAX_VIDEO_DURATION} 秒"
current[stored] = value
changed = True
if stored == "model":
model_user_selected = True
if changed:
conversation.params = current
conversation.save(update_fields=["params", "updated_at"])
update_fields = ["params", "updated_at"]
if model_user_selected and conversation.mode == CreationConversation.Mode.VIDEO:
memory = dict(conversation.memory or {})
memory["video_model_user_selected"] = True
conversation.memory = memory
update_fields.append("memory")
conversation.save(update_fields=update_fields)
if is_plot_twist_conversation(conversation):
# 用户在参数卡里改了时长,剧情结构也必须立即跟着切换。
set_plot_twist_story_depth(conversation, str(current.get("duration") or ""))
@@ -2958,11 +3064,17 @@ def apply_confirm_params(conversation, incoming: dict | None) -> tuple[dict, boo
)
if changed:
conversation.params = current
conversation.save(update_fields=["params", "updated_at"])
if is_plot_twist_conversation(conversation):
update_fields = ["params", "updated_at"]
if model_changed:
memory = dict(conversation.memory or {})
memory["video_model_user_selected"] = True
conversation.memory = memory
update_fields.append("memory")
conversation.save(update_fields=update_fields)
if duration_changed and is_plot_twist_conversation(conversation):
# 在确认卡改时长也要切换故事契约;随后视图会要求重写旧方案。
set_plot_twist_story_depth(conversation, str(current.get("duration") or ""))
needs_rebuild = duration_changed or model_changed
needs_rebuild = duration_changed
if needs_rebuild:
# 角色和商品素材继续保留;只让 GPT 基于新参数重写受影响的架构与 Prompt。
set_video_gate_stage(conversation, "strategy", clear_pending_prompt=True)
@@ -3616,17 +3728,20 @@ def resolve_smart_video_duration(
params["duration"] = f"{duration} 秒"
selected_model = video_model_name(params)
switched = False
# Seedance 2.5 是当前唯一可稳定承载 16–30 秒单段、以及 31–180 秒分段的模型。
# 总时长不在单段能力表里时,后续会拆成多条 <=30 秒的 2.5 任务。
if duration > 15 and selected_model != DEFAULT_VIDEO_MODEL:
params["model"] = "Seedance 2.5"
memory = dict(conversation.memory or {})
model_user_selected = bool(memory.get("video_model_user_selected"))
# 默认按目标时长选模型:15 秒及以下优先 Fast,超过 15 秒使用 2.5。
# 用户在最终确认卡手动换过模型后尊重其选择;只有能力不支持时才自动回退。
preferred_model = FAST_VIDEO_MODEL if duration <= 15 else DEFAULT_VIDEO_MODEL
preferred_label = FAST_VIDEO_MODEL_LABEL if duration <= 15 else "Seedance 2.5"
if not model_user_selected and selected_model != preferred_model:
params["model"] = preferred_label
switched = True
elif not _model_supports_duration(selected_model, duration):
# 智能模式可自动选能完成完整脚本的模型;确认卡会清楚展示变更,用户仍能手动调整。
if _model_supports_duration(DEFAULT_VIDEO_MODEL, duration):
params["model"] = "Seedance 2.5"
if _model_supports_duration(preferred_model, duration):
params["model"] = preferred_label
switched = True
memory = dict(conversation.memory or {})
memory.pop("video_model_user_selected", None)
memory["smart_duration_resolved"] = duration
if switched:
memory["smart_duration_model_switched"] = True
@@ -4980,6 +5095,7 @@ def iter_creation_agent_events(
turn_deadline = turn_started + CREATION_AGENT_TURN_TIMEOUT_SECONDS
extra_body = creation_model_extra_body(model_config, tools)
truncation_retries = 0
progress_tool_retries = 0
for _round in range(MAX_TOOL_ROUNDS):
if is_agent_cancel_requested(conversation.id):
# 用户终止:干净收束,不落 ERROR,已落库消息保留
@@ -5199,6 +5315,27 @@ def iter_creation_agent_events(
turn_has_gate = True
break
# 用户已经明确说「推荐 / 继续 / 做方案」,模型却只回一段说明文字时,
# 不能把加载状态正常收起后只留“继续完善方案”。把这一轮文本作废并强制重试工具调用,
# 直到真正落下追问卡或视频架构卡。
if not calls and allow_plan and context.is_video and not fallback_fields:
progress_tool_retries += 1
if progress_tool_retries <= MAX_PROGRESS_TOOL_RETRIES:
if said:
messages.append({"role": "assistant", "content": said})
messages.append({"role": "user", "content": PROGRESS_TOOL_RETRY_INSTRUCTION})
continue
clear_public_agent_progress(conversation)
failure = append_message(
conversation,
role="assistant",
kind=CreationMessage.Kind.ERROR,
text=PROGRESS_TOOL_GIVE_UP_NOTICE,
)
yield {"type": "message", "message": _message_payload(failure)}
yield {"type": "done"}
return
# ask_user 自己会落一条可追踪的聊天问题。模型同时吐出的过渡文案不再
# 另存一条,否则界面会连续出现两遍几乎相同的问题。
asks_user = bool(fallback_fields) or any(call.get("name") == "ask_user" for call in calls)