fix(viral_video): VLM图片分析3个P0 bug (#2188) #2188

Merged
auto-approve-bot merged 2 commits from fix/vlm-url-lite-speed into develop 2026-10-05 12:00:38 +08:00
2 changed files with 68 additions and 21 deletions
+67 -20
View File
@@ -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]
+1 -1
View File
@@ -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 # 视频生成轮询总超时(秒)