From 44bd96b1486a69b7d7205ffec376f40d4f0158ed Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 4 Oct 2026 21:38:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(#2183):=20=E4=BF=A1=E4=BB=BB=E9=93=BE?= =?UTF-8?q?=E7=9C=9F=C2=B7=E7=8E=B0=E5=9C=BAt2i+preheat=E6=8E=A5=E7=BA=BF+?= =?UTF-8?q?portrait=5Fintercept=E9=99=8D=E7=BA=A7=E7=BA=AFt2v+timeout?= =?UTF-8?q?=E8=B0=83=E5=A4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 信任链3处断链修复: 1. run_viral_video_analyze VLM完成后立即启动_start_trust_chain_preheat(后台daemon与文案阶段并行) 2. _step_render 预热不可用时真·同步调用Seedream t2i(而非打假日志直接传原图),120s超时 3. ai_client.video_generation遇到portrait_intercept 400时自动移除所有参考图降级纯t2v重试一次 - LLM timeout调大(实测pro 1500tok输出需75.8s): - _step_script_generation: fast-first/fast-retry 45→90s, pro-fallback 60→120s - _step_intent_parsing: 45→60s - _step_review: 30→45s - generate_copy celery时限放宽:soft=360→600s, hard=420→660s(三级重试最坏~405s) - 三层兜底:后台预热→现场t2i→400纯t2v,保证人像链路必出片 --- apps/worker/worker_app/tasks/viral_video.py | 67 +++++++++++++++++---- packages/shared/ai_client.py | 24 +++++++- 2 files changed, 78 insertions(+), 13 deletions(-) diff --git a/apps/worker/worker_app/tasks/viral_video.py b/apps/worker/worker_app/tasks/viral_video.py index 4d410c0b9..ce37f9206 100644 --- a/apps/worker/worker_app/tasks/viral_video.py +++ b/apps/worker/worker_app/tasks/viral_video.py @@ -598,7 +598,7 @@ def _step_intent_parsing(job: ViralVideoJob, image_analysis: dict) -> dict: 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=45) + result = call_llm(prompt, temperature=0.4, max_tokens=800, model=_m, timeout=60) # #2183: pro 实测800tok约40s,给60s return ( result if isinstance(result, dict) @@ -1033,16 +1033,16 @@ def _step_script_generation(job: ViralVideoJob, intent: dict, image_analysis: di try: # 第一次:快模型 25s - normalized = _try_gen(_fast, 0.8, 2500, "fast-first", tmo=45) + normalized = _try_gen(_fast, 0.8, 2500, "fast-first", tmo=90) if normalized is not None: return normalized - # 第二次:快模型降温度+加大 max_tokens,25s - normalized = _try_gen(_fast, 0.6, 3200, "fast-retry", tmo=45) + # #2183: 实测pro 1500tok输出需75.8s,单次timeout提到90s + normalized = _try_gen(_fast, 0.6, 3200, "fast-retry", tmo=90) if normalized is not None: return normalized - # 第三次:用主力模型兜底,给 40s + # 第三次:用主力模型兜底,给 120s if _pro and _pro != _fast: - normalized = _try_gen(_pro, 0.7, 3500, "pro-fallback", tmo=60) + normalized = _try_gen(_pro, 0.7, 3500, "pro-fallback", tmo=120) if normalized is not None: return normalized logger.warning("[爆款视频] 编导脚本三次都未生成合格结果,使用兜底脚本") @@ -1079,7 +1079,7 @@ def _step_review(job: ViralVideoJob, copy_result: dict) -> dict: 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=30) + result = call_llm(prompt, temperature=0.1, max_tokens=500, model=_m, timeout=45) # #2183: 对齐pro响应 return result if isinstance(result, dict) else {"passed": True, "score": 80, "details": {}} except Exception as e: logger.warning("[爆款视频] 合规审核失败 label=%s err=%s", _lbl, e) @@ -1251,13 +1251,44 @@ def _step_render(job: ViralVideoJob, copy_result: dict, tts_audio_url: str | Non pre_trusted = list(pti) logger.info("[爆款视频] 使用信任链预热结果 n=%d,跳过现场 Seedream AI 化", len(pre_trusted)) elif all_portrait_urls and _mcfg.get("provider", "doubao") == "doubao": + # #2183: 真·现场跑信任链——同步调用 Seedream t2i,拿到 AI 人像 URL 后再传 Seedance logger.info( - "[爆款视频] 预热结果不可用(%s/%d张),将现场跑信任链", + "[爆款视频] 预热结果不可用(%s/%d张),现场同步跑信任链Seedream t2i", "缺失" if not pti else f"{len(pti)}/{len(all_portrait_urls)}", len(all_portrait_urls), ) + try: + from packages.shared.ai_service import preheat_trust_chain + _ia = getattr(job, "image_analysis", None) or {} + _prods = (_ia.get("products") if isinstance(_ia, dict) else None) or [] + _pdescs = [] + if _prods: + _pdescs = [(pp.get("portrait_prompt") or "无人像") for pp in _prods] + elif isinstance(_ia, dict): + _pp0 = _ia.get("portrait_prompt") or "无人像" + if _pp0 and _pp0 != "无人像": + _pdescs = [_pp0] + _valid = [d for d in _pdescs if d and isinstance(d, str) and "无人像" not in d and len(d) >= 10] + if _valid: + _t0 = time.time() + _live_urls = preheat_trust_chain(_valid, timeout=120) + if _live_urls and len(_live_urls) == len(all_portrait_urls): + pre_trusted = list(_live_urls) + logger.info( + "[爆款视频] 现场信任链t2i完成 %d张 耗时%.1fs,将用AI人像传Seedance", + len(pre_trusted), time.time()-_t0, + ) + else: + logger.warning( + "[爆款视频] 现场信任链t2i返回不匹配 urls=%s n_portraits=%d,回退原图+400降级纯t2v", + _live_urls, len(all_portrait_urls), + ) + else: + logger.info("[爆款视频] 无有效人物描述(可能是商品图),无需现场跑信任链") + except Exception as _te: + logger.warning("[爆款视频] 现场跑信任链异常: %s,回退原图+400降级纯t2v", _te, exc_info=True) - # 第一次调用:带参考图/首帧/音频/参考视频 + # 第一次调用:带参考图/首帧/音频/参考视频(pre_trusted有值→走信任链;无值→原图;若400 ai_client内部自动降级纯t2v) result = call_video_generation( prompt=prompt, image_url=first_image, @@ -1550,8 +1581,20 @@ def run_viral_video_analyze(self: Task, job_id: str) -> dict: image_analysis = _step_image_analysis(job) job.image_analysis = image_analysis + # #2183/#2174: VLM完成后立即启动信任链t2i预热(后台daemon线程,与后续阶段并行) + if job.images: + try: + _products = (image_analysis or {}).get("products", []) or [] + _portrait_descs = [(p.get("portrait_prompt") or "无人像") for p in _products] if _products else [] + if not _portrait_descs and isinstance(image_analysis, dict): + _pp = image_analysis.get("portrait_prompt") or "无人像" + if _pp and _pp != "无人像": + _portrait_descs = [_pp] + _start_trust_chain_preheat(job.id, _portrait_descs) + except Exception as _e: + logger.warning("[爆款视频][阶段1] 启动信任链t2i预热失败: %s", _e) _save_job(repo, job, session) - _emit_progress(job_id, ViralVideoStage.IMAGE_ANALYSIS, 60.0, "图片分析完成", {"result": image_analysis}) + _emit_progress(job_id, ViralVideoStage.IMAGE_ANALYSIS, 60.0, "图片分析完成,AI人像预热中...", {"result": image_analysis, "trust_chain_preheating": True}) style_guide = None if job.reference_video_url or job.style_template_id: @@ -1598,8 +1641,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=360, # #2173: 6min(编导脚本含意图+三级重试+审核,fast超时转pro) - time_limit=420, # #2173: 7min hard limit + soft_time_limit=600, # #2183: pro长脚本~120s+三级重试最坏300s+intent/review ~105s,给到10min + time_limit=660, # #2183: 11min hard limit ) def run_viral_video_generate_copy(self: Task, job_id: str) -> dict: """v1.6 阶段2(v1.6.1 提速版):意图解析 → 编导分镜脚本生成 → 直接返回,合规审核后置到出片前。 diff --git a/packages/shared/ai_client.py b/packages/shared/ai_client.py index 885d09e50..5e115cf63 100755 --- a/packages/shared/ai_client.py +++ b/packages/shared/ai_client.py @@ -585,7 +585,7 @@ class DoubaoClient: # #2172/#2174: 信任链——使用预热好的 Seedream t2i 文生图(纯模型生成人像,是方舟信任产物, # 不会触发肖像审核)。预热在 VLM 分析后由 daemon 线程后台完成,结果通过 pre_trusted_images 传入。 # - 预热结果有效 → 替换原参考图,走 omni_ref 模式 - # - 预热结果不可用 → 直接用原图(若被400肖像拦截,#2166自动降级纯t2v),避免现场跑t2i阻塞渲染 + # - 预热结果不可用 → 直接用原图(上层 _step_render 已现场同步跑 Seedream t2i 兜底;若再被400拦截,下方自动降级纯t2v) # 信任链只作用于 doubao provider;DashScope(Wan) 保持原行为。 trust_chain_applied = False if provider == "doubao" and getattr(self, "trust_chain_enabled", True) and pre_trusted_images: @@ -763,6 +763,28 @@ class DoubaoClient: # 保留第二次的错误信息 sc, body = sc2, body2 + # #2183: portrait_intercept / 真人肖像审核拦截 → 去掉所有参考图(含image_url首帧),纯 t2v 重试一次 + # 信任链预热或现场 t2i 都失败时的最后兜底,保证能出片 + if ( + not task_id + and sc == 400 + ): + _err_code_for_400, _ = _classify_video_error(sc, body, last_err) + if _err_code_for_400 == "portrait_intercept" and (image_url or ref_imgs): + logger.warning( + "Seedance 创建因 portrait_intercept 失败,降级纯 t2v(移除所有参考图)重试: img=%d ref=%d", + 1 if image_url else 0, len(ref_imgs), + ) + _t2v_payload = dict(create_payload) + _t2v_payload["content"] = [{"type": "text", "text": prompt.strip()}] + _t2v_payload["ratio"] = ratio or "9:16" + task_id, last_err, sc3, body3 = _do_create(_t2v_payload) + if task_id: + sc, body = sc3, body3 + logger.info("[trust-chain] portrait_intercept 降级纯 t2v 成功 task_id=%s", task_id) + else: + sc, body = sc3, body3 + if not task_id: err_code, user_msg = _classify_video_error(sc, body, last_err) self.last_video_error = {