fix(#2173 P0): 文案生成超时修复+VLM快速失败+Seedream换flash模型 #2173

Merged
auto-approve-bot merged 2 commits from fix/2173-p0-lite-model-timeout-trustchain-disable into develop 2026-10-04 13:03:54 +08:00
4 changed files with 65 additions and 45 deletions
+37 -31
View File
@@ -438,7 +438,7 @@ def _analyze_single_image(
# 第二次:pro 降级重试
if pro_fallback_model and pro_fallback_model != vision_model:
pro_raw = _call(pro_fallback_model, max(60, timeout))
pro_raw = _call(pro_fallback_model, 25) # #2173: pro 25s 上限(lite 已失败过,快速兜底)
pro_result = _normalize(pro_raw, "pro_fallback")
if _is_vision_result_usable(pro_result):
pro_result["_fallback_used"] = True
@@ -472,7 +472,7 @@ def _step_image_analysis(job: ViralVideoJob) -> dict:
if _s.doubao_vision_use_lite:
vision_model = _s.doubao_vision_lite_model
pro_model = _s.doubao_vision_model
vision_timeout = 45 # lite 给到 45s 避免首轮就 timeout 降级 pro
vision_timeout = 15 # #2173: lite 15s 快速失败转 pro(实测 lite 持续超时时白等137s是P0)
else:
vision_model = _s.doubao_vision_model
pro_model = None # 已经是 pro,不再降级
@@ -581,17 +581,20 @@ def _step_intent_parsing(job: ViralVideoJob, image_analysis: dict) -> dict:
_s = get_shared_settings()
_fast = _s.doubao_fast_model
try:
# 用快模型提速(结构化输出任务,不需要推理模型)
result = call_llm(prompt, temperature=0.4, max_tokens=800, model=_fast)
return (
result
if isinstance(result, dict)
else {"intent": str(result)[:200], "key_messages": [], "tone": "专业", "suggested_title": ""}
)
except Exception as e:
logger.warning("[爆款视频] 意图解析失败: %s", e)
return {"intent": "推广产品", "key_messages": ["产品亮点"], "tone": "专业", "suggested_title": ""}
_pro = _s.doubao_model
for _m, _lbl in [(_fast, "fast"), (_pro, "pro-fallback")]:
try:
logger.info("[爆款视频] 意图解析 model=%s label=%s", _m, _lbl)
result = call_llm(prompt, temperature=0.4, max_tokens=800, model=_m, timeout=25)
return (
result
if isinstance(result, dict)
else {"intent": str(result)[:200], "key_messages": [], "tone": "专业", "suggested_title": ""}
)
except Exception as e:
logger.warning("[爆款视频] 意图解析失败 label=%s err=%s", _lbl, e)
continue
return {"intent": "推广产品", "key_messages": ["产品亮点"], "tone": "专业", "suggested_title": ""}
# ── 编导分镜脚本生成(核心,v1.6 新 prompt) ──────────────────────────────
@@ -982,9 +985,9 @@ def _step_script_generation(job: ViralVideoJob, intent: dict, image_analysis: di
_fast = _s.doubao_fast_model
_pro = getattr(_s, "doubao_model", None) or _fast
def _try_gen(model: str, temp: float, max_tok: int, label: str):
logger.info("[爆款视频] 编导脚本生成 model=%s label=%s", model, label)
r = call_llm(prompt, temperature=temp, max_tokens=max_tok, model=model)
def _try_gen(model: str, temp: float, max_tok: int, label: str, tmo: int = 25):
logger.info("[爆款视频] 编导脚本生成 model=%s label=%s timeout=%d", model, label, tmo)
r = call_llm(prompt, temperature=temp, max_tokens=max_tok, model=model, timeout=tmo)
if r is None:
logger.warning("[爆款视频] 编导脚本返回None label=%s", label)
return None
@@ -1016,17 +1019,17 @@ def _step_script_generation(job: ViralVideoJob, intent: dict, image_analysis: di
return normalized
try:
# 第一次:快模型
normalized = _try_gen(_fast, 0.8, 2500, "fast-first")
# 第一次:快模型 25s
normalized = _try_gen(_fast, 0.8, 2500, "fast-first", tmo=25)
if normalized is not None:
return normalized
# 第二次:快模型降温度+加大 max_tokens
normalized = _try_gen(_fast, 0.6, 3200, "fast-retry")
# 第二次:快模型降温度+加大 max_tokens,25s
normalized = _try_gen(_fast, 0.6, 3200, "fast-retry", tmo=25)
if normalized is not None:
return normalized
# 第三次:用主力模型兜底
# 第三次:用主力模型兜底,给 40s
if _pro and _pro != _fast:
normalized = _try_gen(_pro, 0.7, 3500, "pro-fallback")
normalized = _try_gen(_pro, 0.7, 3500, "pro-fallback", tmo=40)
if normalized is not None:
return normalized
logger.warning("[爆款视频] 编导脚本三次都未生成合格结果,使用兜底脚本")
@@ -1059,13 +1062,16 @@ def _step_review(job: ViralVideoJob, copy_result: dict) -> dict:
- issues: 需要修改的问题列表(如有)"""
_s = get_shared_settings()
_fast = _s.doubao_fast_model
try:
# 合规审核用快模型 + 短输出(结构化判断)
result = call_llm(prompt, temperature=0.1, max_tokens=500, model=_fast)
return result if isinstance(result, dict) else {"passed": True, "score": 80, "details": {}}
except Exception as e:
logger.warning("[爆款视频] 合规审核失败: %s", e)
return {"passed": True, "score": 75, "details": {d: "默认通过" for d in dimensions}}
_pro = _s.doubao_model
for _m, _lbl in [(_fast, "fast"), (_pro, "pro-fallback")]:
try:
logger.info("[爆款视频] 合规审核 model=%s label=%s", _m, _lbl)
result = call_llm(prompt, temperature=0.1, max_tokens=500, model=_m, timeout=20)
return result if isinstance(result, dict) else {"passed": True, "score": 80, "details": {}}
except Exception as e:
logger.warning("[爆款视频] 合规审核失败 label=%s err=%s", _lbl, e)
continue
return {"passed": True, "score": 75, "details": {d: "默认通过" for d in dimensions}}
def _step_tts(job: ViralVideoJob, voiceover_script: str):
@@ -1573,8 +1579,8 @@ def run_viral_video_analyze(self: Task, job_id: str) -> dict:
bind=True,
max_retries=1,
name="worker.run_viral_video_generate_copy",
soft_time_limit=180, # 3min(编导脚本生成含三级重试)
time_limit=240,
soft_time_limit=360, # #2173: 6min(编导脚本含意图+三级重试+审核,fast超时转pro)
time_limit=420, # #2173: 7min hard limit
)
def run_viral_video_generate_copy(self: Task, job_id: str) -> dict:
"""v1.6 阶段2(v1.6.1 提速版):意图解析 → 编导分镜脚本生成 → 直接返回,合规审核后置到出片前。
+8 -2
View File
@@ -108,8 +108,14 @@ class SharedSettings(BaseSettings):
doubao_video_model: str = "doubao-seedance-2-5-260628"
doubao_video_timeout: int = 600 # 视频生成轮询总超时(秒)
doubao_video_poll_interval: int = 10 # 轮询间隔(秒)
doubao_image_model: str = "doubao-seedream-5-0-pro-260628" # 图生图/文生图(信任链真人照片AI化)
doubao_image_timeout: int = 120 # 图片生成超时(秒)
doubao_image_model: str = (
"doubao-seedream-5-0-flash-260915" # #2173: 信任链 Seedream 改 flash 模型(实测 pro 46.5s→flash 13s;pro AI化图仍被Seedance拦截)
)
doubao_image_size: str = "1K" # #2173: 1K 已足够做 Seedance 参考图,2K 在 flash 下也 22s,1K 13s
doubao_image_timeout: int = 60 # #2173: flash+1K 通常15s内,给60s余量
doubao_trust_chain_enabled: bool = (
True # #2173: 信任链总开关;若Seedream产物仍被Seedance拦截,可配 False 关闭直接t2v降级
)
# ── DashScope (阿里云百炼 Wan 3.0 等) ─────────────────────────────────
dashscope_api_key: str = ""
+18 -11
View File
@@ -182,7 +182,9 @@ class DoubaoClient:
self.fast_model: str = settings.doubao_fast_model
self.embedding_model: str = settings.doubao_embedding_model
self.image_model: str = settings.doubao_image_model
self.image_timeout: int = getattr(settings, "doubao_image_timeout", 120) or 120
self.image_size: str = getattr(settings, "doubao_image_size", "1K") or "1K"
self.image_timeout: int = getattr(settings, "doubao_image_timeout", 60) or 60
self.trust_chain_enabled: bool = getattr(settings, "doubao_trust_chain_enabled", True)
# 最近一次视频生成的详细错误(error_code + user_message + raw detail),供上层读取后展示给用户
self.last_video_error: dict = {}
# 最近一次图片生成的详细错误,供上层读取
@@ -240,6 +242,7 @@ class DoubaoClient:
temperature: float = 0.7,
max_tokens: int = 1024,
model: str | None = None,
timeout: int | None = None,
) -> Optional[str]:
"""调用 Chat Completion 接口.
@@ -270,18 +273,19 @@ class DoubaoClient:
_t0 = time.time()
for attempt in range(self.max_retries + 1):
try:
_req_timeout = timeout if timeout is not None else self.timeout
response = httpx.post(
url,
headers=headers,
json=payload,
timeout=self.timeout,
timeout=_req_timeout,
)
response.raise_for_status()
data = response.json()
content = data["choices"][0]["message"]["content"]
_elapsed = time.time() - _t0
logger.info(
"[doubao] chat_completion 完成 model=%s tokens_in=%d tokens_out=%d elapsed=%.1fs attempt=%d",
"[doubao] chat_completion 完成 model=%s tokens_in=%d tokens_out=%d elapsed=%.1fs attempt=%d timeout=%d",
payload.get("model"),
data.get("usage", {}).get("prompt_tokens", 0),
data.get("usage", {}).get("completion_tokens", 0),
@@ -417,15 +421,17 @@ class DoubaoClient:
self,
portrait_urls: list[str],
*,
timeout: int = 120,
timeout: int | None = None,
size: str | None = None,
) -> list[str] | None:
"""#2172 信任链预热:对一组人像 URL 执行 Seedream AI 化,返回 AI 化后的 URL 列表。
- 全部成功返回 list[str](顺序与输入一致)
- 任何一张失败返回 None(保留上层回退原图直传的路径)
- 若 trust_chain_enabled=False 直接返回 None(#2173: 可配置关闭)
- 供 worker 在视频生成前并行预热使用;video_generation 内部若收到 preheated 结果会直接使用,不再现场跑。
"""
if not portrait_urls or not self.is_available:
if not portrait_urls or not self.is_available or not getattr(self, "trust_chain_enabled", True):
return None
seedream_prompt = (
"保持此人五官特征、发型、肤色、面部轮廓、年龄感,生成一张高清写实人像照片,"
@@ -438,8 +444,8 @@ class DoubaoClient:
sd_result = self.image_generation(
prompt=sd_prompt,
reference_images=[raw_url],
size="2K",
timeout=timeout,
size=size or getattr(self, "image_size", "1K"),
timeout=timeout or getattr(self, "image_timeout", 60),
)
if not sd_result:
logger.warning(
@@ -564,7 +570,7 @@ class DoubaoClient:
# 预热结果有效则直接使用,否则现场跑一次 Seedream AI 化。
# 信任链只作用于 doubao provider;DashScope(Wan) 保持原行为。
trust_chain_applied = False
if provider == "doubao":
if provider == "doubao" and getattr(self, "trust_chain_enabled", True):
raw_portrait_urls: list[str] = []
if image_url:
raw_portrait_urls.append(image_url)
@@ -953,6 +959,7 @@ class DoubaoClient:
return None
img_model = model or self.image_model
_img_size = size or getattr(self, "image_size", "1K")
url = f"{self.base_url}/images/generations"
headers = {
"Authorization": f"Bearer {self.api_key}",
@@ -961,7 +968,7 @@ class DoubaoClient:
payload: dict[str, Any] = {
"model": img_model,
"prompt": prompt.strip(),
"size": size,
"size": _img_size,
"response_format": "url",
"output_format": output_format,
"watermark": bool(watermark),
@@ -973,7 +980,7 @@ class DoubaoClient:
else:
payload["image"] = ref_imgs_local[:10]
req_timeout = timeout or self.image_timeout
req_timeout = timeout or getattr(self, "image_timeout", 60)
last_err: Exception | None = None
last_sc = 0
last_body = ""
@@ -1003,7 +1010,7 @@ class DoubaoClient:
"Seedream 图片生成成功 model=%s ref_imgs=%d size=%s elapsed=%.1fs attempt=%d",
img_model,
len(ref_imgs_local),
size,
_img_size,
time.time() - _img_t0,
attempt + 1,
)
+2 -1
View File
@@ -502,6 +502,7 @@ def call_llm(
max_tokens: int = 2048,
model: str | None = None,
system_prompt: str | None = None,
timeout: int | None = None,
) -> object:
"""调用豆包大模型(文本对话),返回解析后的 JSON(dict/list)或原文字符串;失败返回 None。
@@ -521,7 +522,7 @@ def call_llm(
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
]
raw = client.chat_completion(messages, temperature=temperature, max_tokens=max_tokens, model=model)
raw = client.chat_completion(messages, temperature=temperature, max_tokens=max_tokens, model=model, timeout=timeout)
if raw is None:
return None
try: