style: auto-format with black + isort + ruff + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m22s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m49s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 18s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 6m53s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m53s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 10m40s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 10m43s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 13m43s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m14s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Failing after 21m43s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 27m10s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 3s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped

This commit is contained in:
CI Bot
2026-10-05 11:55:06 +00:00
parent aac785a9ed
commit 3353af0fb8
4 changed files with 103 additions and 38 deletions
@@ -6,6 +6,7 @@
- ark: doubao-seed-2-1-lite-260915 / doubao-seed-2-1-pro-260915
- dashscope: qwen3.8-flash / qwen3.7-plus
"""
from __future__ import annotations
import logging
@@ -70,6 +71,7 @@ def api_key() -> str | None:
if PROVIDER == "ark":
try:
from packages.shared import get_shared_settings
return get_shared_settings().doubao_api_key
except Exception:
return os.environ.get("DOUBAO_API_KEY")
@@ -85,7 +85,9 @@ def analyze_image_v2(idx: int, img_url: str) -> dict[str, Any]:
assembled["_fast_elapsed"] = round(fast_elapsed, 2)
logger.info(
"[vision.v2] 图片 #%d fast命中 provider=%s elapsed=%.2fs pp=%s",
idx, _provider.get_provider(), fast_elapsed,
idx,
_provider.get_provider(),
fast_elapsed,
(assembled.get("portrait_prompt") or "")[:40],
)
return assembled
@@ -101,7 +103,9 @@ def analyze_image_v2(idx: int, img_url: str) -> dict[str, Any]:
logger.info("[vision.v2] 图片 #%d pro兜底命中 total=%.2fs", idx, time.time() - t0)
return pro_result
logger.warning("[vision.v2] 图片 #%d 全路径失败 provider=%s elapsed=%.2fs", idx, _provider.get_provider(), time.time() - t0)
logger.warning(
"[vision.v2] 图片 #%d 全路径失败 provider=%s elapsed=%.2fs", idx, _provider.get_provider(), time.time() - t0
)
out = dict(_FALLBACK_RESULT)
out["_source"] = "v2_all_failed"
out["text_on_package"] = ocr_result[:8]
@@ -117,7 +121,11 @@ def analyze_images_v2(img_urls: list[str]) -> list[dict[str, Any]]:
logger.info(
"[vision.v2] 开始图片分析 n=%d workers=%d provider=%s fast_timeout=%.0fs pro_timeout=%.0fs",
len(img_urls), workers, _provider.get_provider(), _FAST_TIMEOUT, _PRO_TIMEOUT,
len(img_urls),
workers,
_provider.get_provider(),
_FAST_TIMEOUT,
_PRO_TIMEOUT,
)
t0 = time.time()
with ThreadPoolExecutor(max_workers=workers) as pool:
@@ -109,26 +109,47 @@ def _xml_to_product(raw: str, idx: int) -> dict[str, Any]:
text_list = [x.strip() for x in re.split(r"[,,;;]", top_text) if x.strip()] if top_text else []
summary = _xml_attr("product", "summary", raw) or f"{brand} {name}"
return {
"name": name, "brand": brand, "category": category,
"appearance": appearance, "packaging": packaging,
"text_on_package": text_list, "key_features": feat_list,
"scene": scene, "mood": mood, "portrait_prompt": portrait_prompt,
"summary": summary, "_source": "vlm_pro_xml",
"name": name,
"brand": brand,
"category": category,
"appearance": appearance,
"packaging": packaging,
"text_on_package": text_list,
"key_features": feat_list,
"scene": scene,
"mood": mood,
"portrait_prompt": portrait_prompt,
"summary": summary,
"_source": "vlm_pro_xml",
}
if portrait_prompt != "无人像":
return {
"name": "未识别", "brand": "无法判断", "category": "无法判断",
"appearance": "无法判断", "packaging": "无法判断",
"text_on_package": [], "key_features": ["无法判断"],
"scene": scene, "mood": mood, "portrait_prompt": portrait_prompt,
"summary": "未识别", "_source": "vlm_pro_no_product",
"name": "未识别",
"brand": "无法判断",
"category": "无法判断",
"appearance": "无法判断",
"packaging": "无法判断",
"text_on_package": [],
"key_features": ["无法判断"],
"scene": scene,
"mood": mood,
"portrait_prompt": portrait_prompt,
"summary": "未识别",
"_source": "vlm_pro_no_product",
}
return {
"name": "未识别", "brand": "无法判断", "category": "无法判断",
"appearance": "无法判断", "packaging": "无法判断",
"text_on_package": [], "key_features": ["无法判断"],
"scene": scene, "mood": mood, "portrait_prompt": "无人像",
"summary": "未识别", "_source": "vlm_pro_no_tag",
"name": "未识别",
"brand": "无法判断",
"category": "无法判断",
"appearance": "无法判断",
"packaging": "无法判断",
"text_on_package": [],
"key_features": ["无法判断"],
"scene": scene,
"mood": mood,
"portrait_prompt": "无人像",
"summary": "未识别",
"_source": "vlm_pro_no_tag",
}
@@ -162,6 +183,7 @@ def _assemble_pp_from_json(obj: dict[str, Any]) -> str:
def _call_dashscope_pro(img_url: str, idx: int, timeout: int) -> dict[str, Any] | None:
"""DashScope qwen3.7-plus 兜底,直接 httpx 发精简 JSON prompt。"""
import httpx
t0 = time.time()
api_key = _provider.api_key()
if not api_key:
@@ -172,10 +194,13 @@ def _call_dashscope_pro(img_url: str, idx: int, timeout: int) -> dict[str, Any]
"model": _provider.pro_model(),
"messages": [
{"role": "system", "content": _DS_PRO_SYSTEM},
{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": img_url}},
{"type": "text", "text": _DS_PRO_USER},
]},
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": img_url}},
{"type": "text", "text": _DS_PRO_USER},
],
},
],
"temperature": 0.3,
"max_tokens": DEFAULT_MAX_TOKENS,
@@ -191,7 +216,9 @@ def _call_dashscope_pro(img_url: str, idx: int, timeout: int) -> dict[str, Any]
)
elapsed = time.time() - t0
if r.status_code != 200:
logger.warning("[vision.vlm] dashscope pro HTTP %d elapsed=%.1fs body=%s", r.status_code, elapsed, r.text[:200])
logger.warning(
"[vision.vlm] dashscope pro HTTP %d elapsed=%.1fs body=%s", r.status_code, elapsed, r.text[:200]
)
return None
data = r.json()
raw = (data.get("choices") or [{}])[0].get("message", {}).get("content")
@@ -201,14 +228,17 @@ def _call_dashscope_pro(img_url: str, idx: int, timeout: int) -> dict[str, Any]
usage = data.get("usage") or {}
logger.info(
"[vision.vlm] dashscope pro 完成 idx=%d elapsed=%.1fs in=%d out=%d",
idx, elapsed, usage.get("prompt_tokens", 0), usage.get("completion_tokens", 0),
idx,
elapsed,
usage.get("prompt_tokens", 0),
usage.get("completion_tokens", 0),
)
text = _strip_code_fence(raw)
l, r_pos = text.find("{"), text.rfind("}")
if l < 0 or r_pos <= l:
logger.warning("[vision.vlm] dashscope pro 无JSON elapsed=%.1fs head=%s", elapsed, raw[:200])
return None
obj = json.loads(text[l:r_pos+1])
obj = json.loads(text[l : r_pos + 1])
if not isinstance(obj, dict):
return None
scene = obj.get("scene") or "通用"
@@ -218,17 +248,21 @@ def _call_dashscope_pro(img_url: str, idx: int, timeout: int) -> dict[str, Any]
brand = obj.get("brand") or "无法判断"
category = obj.get("category") or ("非产品图" if obj.get("has_person") else "无法判断")
return {
"name": name, "brand": brand, "category": category,
"name": name,
"brand": brand,
"category": category,
"appearance": obj.get("outfit") or "无法判断",
"packaging": "无法判断",
"text_on_package": [],
"key_features": obj.get("key_features") or ["无法判断"],
"scene": scene, "mood": mood, "portrait_prompt": pp,
"scene": scene,
"mood": mood,
"portrait_prompt": pp,
"summary": f"{brand} {name}" if name != "未识别" else "未识别",
"_source": "vlm_pro_dashscope_json",
}
except Exception as e:
logger.warning("[vision.vlm] dashscope pro 异常 idx=%d elapsed=%.1fs err=%s", idx, time.time()-t0, e)
logger.warning("[vision.vlm] dashscope pro 异常 idx=%d elapsed=%.1fs err=%s", idx, time.time() - t0, e)
return None
@@ -237,7 +271,9 @@ def _call_ark_pro(img_url: str, idx: int, model: str | None, timeout: int) -> di
t0 = time.time()
try:
from packages.application.viral_video.prompt_loader import (
get_template, render_system_prompt, render_user_prompt,
get_template,
render_system_prompt,
render_user_prompt,
)
from packages.shared.ai_client import get_doubao_client
except ImportError as e:
@@ -266,7 +302,7 @@ def _call_ark_pro(img_url: str, idx: int, model: str | None, timeout: int) -> di
model=use_model,
)
except Exception as e:
logger.warning("[vision.vlm] ark pro 调用失败 idx=%d elapsed=%.1fs err=%s", idx, time.time()-t0, e)
logger.warning("[vision.vlm] ark pro 调用失败 idx=%d elapsed=%.1fs err=%s", idx, time.time() - t0, e)
client.max_retries = _orig_retries
return None
client.max_retries = _orig_retries
@@ -278,7 +314,7 @@ def _call_ark_pro(img_url: str, idx: int, model: str | None, timeout: int) -> di
l, r_pos = text.find("{"), text.rfind("}")
if l >= 0 and r_pos > l:
try:
obj = json.loads(text[l:r_pos+1])
obj = json.loads(text[l : r_pos + 1])
if isinstance(obj, dict):
logger.info("[vision.vlm] ark pro JSON 完成 idx=%d elapsed=%.1fs", idx, elapsed)
return {
@@ -122,7 +122,9 @@ def call_fast_json(
if resp.status_code == 400:
logger.warning(
"[vision.v2] fast_json HTTP 400 provider=%s elapsed=%.1fs body=%s",
_provider.get_provider(), elapsed, resp.text[:200],
_provider.get_provider(),
elapsed,
resp.text[:200],
)
if _provider.is_400_thinking_error(resp.text[:500]):
_provider.pop_thinking_param(payload)
@@ -141,7 +143,10 @@ def call_fast_json(
elif resp.status_code != 200:
logger.warning(
"[vision.v2] fast_json HTTP %d provider=%s elapsed=%.1fs body=%s",
resp.status_code, _provider.get_provider(), elapsed, resp.text[:200],
resp.status_code,
_provider.get_provider(),
elapsed,
resp.text[:200],
)
return None
data = resp.json()
@@ -156,8 +161,12 @@ def call_fast_json(
reasoning_tokens = ctd.get("reasoning_tokens", 0)
logger.info(
"[vision.v2] fast_json 完成 provider=%s model=%s elapsed=%.1fs in=%d out=%d reasoning=%d",
_provider.get_provider(), use_model, elapsed,
usage.get("prompt_tokens", 0), usage.get("completion_tokens", 0), reasoning_tokens,
_provider.get_provider(),
use_model,
elapsed,
usage.get("prompt_tokens", 0),
usage.get("completion_tokens", 0),
reasoning_tokens,
)
text = _strip_code_fence(raw)
l = text.find("{")
@@ -174,11 +183,21 @@ def call_fast_json(
return None
logger.info(
"[vision.v2] fast_json 完成 provider=%s model=%s elapsed=%.1fs has_person=%s has_product=%s category=%s",
_provider.get_provider(), use_model, elapsed,
obj.get("has_person"), obj.get("has_product"), obj.get("category"),
_provider.get_provider(),
use_model,
elapsed,
obj.get("has_person"),
obj.get("has_product"),
obj.get("category"),
)
return obj
except Exception as e:
elapsed = time.time() - t0
logger.warning("[vision.v2] fast_json 异常 provider=%s elapsed=%.1fs err=%s", _provider.get_provider(), elapsed, e, exc_info=True)
logger.warning(
"[vision.v2] fast_json 异常 provider=%s elapsed=%.1fs err=%s",
_provider.get_provider(),
elapsed,
e,
exc_info=True,
)
return None