fix(viral_video): VLM图片分析3个P0 bug (#2188) #2188
@@ -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]
|
||||
|
||||
@@ -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 # 视频生成轮询总超时(秒)
|
||||
|
||||
Reference in New Issue
Block a user