diff --git a/apps/worker/worker_app/tasks/viral_video.py b/apps/worker/worker_app/tasks/viral_video.py index bbbc7a728..86bbd2478 100644 --- a/apps/worker/worker_app/tasks/viral_video.py +++ b/apps/worker/worker_app/tasks/viral_video.py @@ -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 提速版):意图解析 → 编导分镜脚本生成 → 直接返回,合规审核后置到出片前。 diff --git a/packages/config/base.py b/packages/config/base.py index c50bfb84e..aaf6a4066 100755 --- a/packages/config/base.py +++ b/packages/config/base.py @@ -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 = "" diff --git a/packages/shared/ai_client.py b/packages/shared/ai_client.py index 78afc9f9b..ffe7426b7 100755 --- a/packages/shared/ai_client.py +++ b/packages/shared/ai_client.py @@ -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, ) diff --git a/packages/shared/ai_service.py b/packages/shared/ai_service.py index 09054c2df..9330ffb58 100755 --- a/packages/shared/ai_service.py +++ b/packages/shared/ai_service.py @@ -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: