From b4d280f08b23ff716191277f548b53871a4d6c77 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 5 Oct 2026 11:44:09 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(viral=5Fvideo):=20#2188=20VLM=20?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=88=86=E6=9E=903=E4=B8=AAP0=20bug=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG1 storage_key→公网URL+空值400: - 新增 _normalize_image_url():非http开头的一律用 get_storage_service().get_url() 转公网URL - 空值/None/非法类型直接 raise ValueError,_step_image_analysis 里透传异常(不再默默走'未识别·无法判断'fallback) - 转换后做 HTTP HEAD 5s 可达性检查,失败打warning日志便于排查 BUG2 VLM 提速: - _step_image_analysis 强制 lite-first:无视 doubao_vision_use_lite 开关,永远 lite 主跑+pro 降级 - lite timeout=30s(目标20-30s),pro fallback timeout=90s - 并发数 min(4,n)→min(2,n) 防方舟限流排队雪崩 - _analyze_single_image 里 max_tokens 2048→1200(结构化 XML 600-900字足够) BUG3 ENV 持久化: - packages/config/base.py doubao_vision_use_lite 默认 False→True - .env.example 本就是 true 无需改 - deploy/configs/.env.staging 通过 Gitea secret DOUBAO_VISION_USE_LITE=true 注入 --- apps/worker/worker_app/tasks/viral_video.py | 86 ++++++++++++++++----- packages/config/base.py | 2 +- 2 files changed, 67 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..247409e4a 100644 --- a/apps/worker/worker_app/tasks/viral_video.py +++ b/apps/worker/worker_app/tasks/viral_video.py @@ -349,6 +349,41 @@ 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 +426,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 +695,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 +705,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 +721,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 +758,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 # 视频生成轮询总超时(秒) -- 2.54.0 From 2862976246eec49e15ddbefc618dfacac7f60417 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 5 Oct 2026 03:51:11 +0000 Subject: [PATCH 2/2] style: auto-format with black + isort + ruff + prettier [skip ci-format-check] --- apps/worker/worker_app/tasks/viral_video.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/worker/worker_app/tasks/viral_video.py b/apps/worker/worker_app/tasks/viral_video.py index 247409e4a..156a0554e 100644 --- a/apps/worker/worker_app/tasks/viral_video.py +++ b/apps/worker/worker_app/tasks/viral_video.py @@ -349,7 +349,6 @@ 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 @@ -358,6 +357,7 @@ def _normalize_image_url(raw: str, idx: int) -> str: 返回前做 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() @@ -369,6 +369,7 @@ def _normalize_image_url(raw: str, idx: int) -> str: 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: -- 2.54.0