fix(#2173 P0): 文案生成超时修复+VLM快速失败+Seedream换flash模型 #2173
@@ -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 提速版):意图解析 → 编导分镜脚本生成 → 直接返回,合规审核后置到出片前。
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user