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
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user