From fc27d4e81ed7144fce409ebfe2705ed712764d9e Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 5 Oct 2026 12:00:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(viral=5Fvideo):=20VLM=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=88=86=E6=9E=903=E4=B8=AAP0=20bug=20(#2188)=20(#2188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: xiaoxia Co-committed-by: xiaoxia --- apps/worker/worker_app/tasks/viral_video.py | 87 ++++++++++++++++----- packages/config/base.py | 2 +- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/apps/worker/worker_app/tasks/viral_video.py b/apps/worker/worker_app/tasks/viral_video.py index 9e6c6f919..156a0554e 100644 --- a/apps/worker/worker_app/tasks/viral_video.py +++ b/apps/worker/worker_app/tasks/viral_video.py @@ -349,6 +349,42 @@ def _is_vision_result_usable(result: dict) -> bool: return True +def _normalize_image_url(raw: str, idx: int) -> str: + """#2188: 将 job.images 中的 storage_key/相对路径/空值统一归一化为可公网访问 URL。 + - 以 http:// 或 https:// 开头 → 视为公网 URL + - 其他 → 视为 storage_key,用 SharedStorageService.get_url() 转公网 URL + - 空值/None/非字符串 → 抛 ValueError(上层 catch 后走 400 错误) + 返回前做 HTTP HEAD 检查可达性。 + """ + import requests as _req + + if not raw or not isinstance(raw, str): + raise ValueError(f"图片 #{idx} URL 为空或类型错误: {type(raw).__name__}={raw!r}") + url = raw.strip() + if not url: + raise ValueError(f"图片 #{idx} URL 为空白字符串") + # storage_key 判定:不以 http 开头 + if not url.startswith("http://") and not url.startswith("https://"): + # 去掉可能的前导斜杠 + storage_key = url.lstrip("/") + try: + from packages.shared.storage import get_storage_service + + _svc = get_storage_service() + url = _svc.get_url(storage_key) + except Exception as _e: + raise ValueError(f"图片 #{idx} storage_key={storage_key!r} 转公网URL失败: {_e}") from _e + logger.info("[爆款视频] 图片 #%d storage_key 已转公网 URL: %s", idx, url[:120]) + # HTTP HEAD 可达性检查 + try: + _r = _req.head(url, timeout=5, allow_redirects=True) + if _r.status_code >= 400: + logger.warning("[爆款视频] 图片 #%d URL HEAD 检查返回 %d: %s", idx, _r.status_code, url[:120]) + except Exception as _e: + logger.warning("[爆款视频] 图片 #%d URL HEAD 检查异常: %s url=%s", idx, _e, url[:120]) + return url + + def _analyze_single_image( idx: int, img_url: str, @@ -391,7 +427,7 @@ def _analyze_single_image( image_url=img_url, prompt=user, model=model, - max_tokens=2048, + max_tokens=1200, # #2188: 结构化 XML 输出 600-900 字足够,2048 翻倍耗时 temperature=0.3, timeout=tmo, system_prompt=system, @@ -660,7 +696,7 @@ def _analyze_single_image( return first_result if pro_fallback_model and pro_fallback_model != vision_model: - pro_raw = _call(pro_fallback_model, 60) # #2180: pro VLM 实测也需25-38s,原25s太短,提到60s + pro_raw = _call(pro_fallback_model, 90) # #2188: pro fallback 给 90s 余量 pro_result = _normalize(pro_raw, "pro_fallback") if _is_vision_result_usable(pro_result): pro_result["_fallback_used"] = True @@ -670,7 +706,12 @@ def _analyze_single_image( def _step_image_analysis(job: ViralVideoJob) -> dict: - """步骤 1: 图片 VLM 分析 — 识别产品特征(v1.6 优化:并行 + lite 模型提速)。""" + """步骤 1: 图片 VLM 分析 — 识别产品特征(v1.6 优化:并行 + lite 模型提速)。 + #2188: (1) 所有图片 URL 先归一化(storage_key→公网URL+空值报400) + (2) 爆款视频强制 lite-first,不依赖 .env USE_LITE 开关 + (3) max_tokens=1200,max_workers=min(2,n) 防方舟限流 + (4) lite timeout=30s,pro fallback timeout=90s + """ try: from packages.shared.ai_service import call_vision # noqa: F401 except ImportError: @@ -681,30 +722,36 @@ def _step_image_analysis(job: ViralVideoJob) -> dict: logger.warning("[爆款视频] 任务无 images,跳过图片分析") return {"products": []} - # 选择视觉模型:lite 速度优先(默认),pro 作为降级备用 + # #2188 BUG1: URL 归一化 — storage_key→公网URL + 空值报400 + normalized_urls: list[str] = [] + for idx, raw in enumerate(job.images): + try: + normalized_urls.append(_normalize_image_url(raw, idx)) + except ValueError as _ve: + # 空/非法URL:直接让任务失败,不默默走 fallback + logger.error("[爆款视频] 图片 #%d URL 归一化失败: %s", idx, _ve) + raise # 上层 celery 捕获后标记任务失败,避免"未识别·无法判断"误导 + + # #2188 BUG2: 爆款视频强制 lite-first(不依赖 .env 开关),lite timeout=30s,pro fallback 90s try: _s = get_shared_settings() - if _s.doubao_vision_use_lite: - vision_model = _s.doubao_vision_lite_model - pro_model = _s.doubao_vision_model - vision_timeout = 45 # #2180: 方舟 VLM 实测服务端处理24-38s,原15s必超时3次重试全挂,提到45s - else: - vision_model = _s.doubao_vision_model - pro_model = None # 已经是 pro,不再降级 - vision_timeout = 60 + lite_model = _s.doubao_vision_lite_model + pro_model = _s.doubao_vision_model except Exception: - vision_model = "doubao-1-5-vision-lite-250315" - pro_model = "doubao-1-5-vision-pro-250328" - vision_timeout = 45 + lite_model = "doubao-seed-2-1-lite-260915" + pro_model = "doubao-seed-2-1-pro-260915" + vision_model = lite_model # 永远 lite 主跑 + vision_timeout = 30 # lite 目标 20-30s - results: list[dict] = [None] * len(job.images) # type: ignore - max_workers = min(4, max(1, len(job.images))) + results: list[dict] = [None] * len(normalized_urls) # type: ignore + max_workers = min(2, max(1, len(normalized_urls))) # #2188: 并发≤2 防方舟限流 logger.info( - "[爆款视频] 开始并行图片分析 n=%d model=%s pro_fallback=%s timeout=%d workers=%d", - len(job.images), + "[爆款视频] 开始并行图片分析 n=%d model=%s pro_fallback=%s lite_timeout=%d pro_timeout=%d workers=%d", + len(normalized_urls), vision_model, pro_model, vision_timeout, + 90, max_workers, ) with ThreadPoolExecutor(max_workers=max_workers) as pool: @@ -712,7 +759,7 @@ def _step_image_analysis(job: ViralVideoJob) -> dict: pool.submit( _analyze_single_image, idx, url, vision_model, vision_timeout, pro_fallback_model=pro_model ): idx - for idx, url in enumerate(job.images) + for idx, url in enumerate(normalized_urls) } for fut in as_completed(future_to_idx): idx = future_to_idx[fut] diff --git a/packages/config/base.py b/packages/config/base.py index 050d2b0e5..f41290b52 100755 --- a/packages/config/base.py +++ b/packages/config/base.py @@ -103,7 +103,7 @@ class SharedSettings(BaseSettings): doubao_vision_lite_model: str = ( "doubao-seed-2-1-lite-260915" # 快速视觉(Seed 2.1 Lite 原生多模态;原 vision-lite-250315 不可用) ) - doubao_vision_use_lite: bool = False # #2181: lite视觉模型100%超时,默认关闭走pro(25-38s稳定返回) + doubao_vision_use_lite: bool = True # #2188: lite恢复稳定,爆款视频默认lite-first提速(20-30s) doubao_embedding_model: str = "doubao-embedding-vision-251215" # 多模态向量化(原 large-text-240915 已 Retiring) doubao_video_model: str = "doubao-seedance-2-5-260628" doubao_video_timeout: int = 600 # 视频生成轮询总超时(秒)