Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 015fd2c381 | |||
| 8caf3ac8c3 | |||
| 3577108e29 |
@@ -421,10 +421,14 @@ def _step_intent_parsing(job: ViralVideoJob, image_analysis: dict) -> dict:
|
||||
render_system_prompt,
|
||||
render_user_prompt,
|
||||
)
|
||||
from packages.shared.ai_service import call_llm
|
||||
from packages.shared.ai_client import get_doubao_client
|
||||
except ImportError:
|
||||
return {"intent": "推广产品", "key_messages": ["产品亮点"], "tone": "专业", "suggested_title": ""}
|
||||
|
||||
_llm_client = get_doubao_client()
|
||||
if not _llm_client.is_available:
|
||||
return {"intent": "推广产品", "key_messages": ["产品亮点"], "tone": "专业", "suggested_title": ""}
|
||||
|
||||
products_summary = ""
|
||||
products = (image_analysis or {}).get("products", []) or []
|
||||
for p in products:
|
||||
@@ -479,13 +483,13 @@ def _step_intent_parsing(job: ViralVideoJob, image_analysis: dict) -> dict:
|
||||
for _m, _lbl in [(_fast, "fast"), (_pro, "pro-fallback")]:
|
||||
try:
|
||||
logger.info("[爆款视频] 意图解析 model=%s label=%s", _m, _lbl)
|
||||
raw = call_llm(
|
||||
raw = _llm_client.chat_completion(
|
||||
[{"role": "system", "content": system}, {"role": "user", "content": user}],
|
||||
temperature=0.4,
|
||||
max_tokens=1024,
|
||||
model=_m,
|
||||
timeout=60,
|
||||
) # #2180: 意图解析 LLM 实测需更长响应,原25s太紧
|
||||
) # #2180/#2215: 直接用 client.chat_completion 传 messages list,不再走 call_llm 字符串包装
|
||||
if not raw:
|
||||
continue
|
||||
parsed = _parse(raw)
|
||||
@@ -825,10 +829,14 @@ def _step_script_generation(job: ViralVideoJob, intent: dict, image_analysis: di
|
||||
GLOBAL_CONSTRAINTS,
|
||||
NEGATIVE_RULES,
|
||||
)
|
||||
from packages.shared.ai_service import call_llm
|
||||
from packages.shared.ai_client import get_doubao_client
|
||||
except ImportError:
|
||||
return _fallback_script(job)
|
||||
|
||||
_llm_client2 = get_doubao_client()
|
||||
if not _llm_client2.is_available:
|
||||
return _fallback_script(job)
|
||||
|
||||
products_summary = _build_products_summary(image_analysis)
|
||||
dur = max(5, min(30, int(getattr(job, "duration", 15) or 15)))
|
||||
|
||||
@@ -864,7 +872,7 @@ def _step_script_generation(job: ViralVideoJob, intent: dict, image_analysis: di
|
||||
|
||||
def _try_gen(model: str, temp: float, max_tok: int, label: str, tmo: int = 25):
|
||||
logger.info("[爆款视频] 编导脚本生成 model=%s label=%s timeout=%d", model, label, tmo)
|
||||
raw = call_llm(
|
||||
raw = _llm_client2.chat_completion(
|
||||
[{"role": "system", "content": system_tpl}, {"role": "user", "content": user}],
|
||||
temperature=temp,
|
||||
max_tokens=max_tok,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""V2 prompt 解析:优先读后台 viral_video_prompt_templates 配置,30s TTL 热加载;
|
||||
DB 不可用/读到默认XML模板时,fallback 到硬编码 JSON schema prompt。
|
||||
"""V2 prompt 解析:优先读后台 viral_video_prompt_templates 表(prompt_type='image_analysis'
|
||||
且 is_active=true),30s TTL 热加载;DB 无有效记录/异常时,fallback 到纯硬编码 JSON schema prompt。
|
||||
|
||||
规则(简单直接,不做字符串匹配判断):
|
||||
- DB 有 is_active=true 的 image_analysis 记录(含种子默认XML和用户修改后的版本):
|
||||
* system = DB.system_prompt + JSON_SCHEMA_APPEND(追加完整JSON字段schema,覆盖XML等其他输出格式要求)
|
||||
* user = DB.user_prompt_template 渲染后使用;渲染后为空则用硬编码默认
|
||||
- DB 无记录/连接异常/返回空:system/user 全部用纯硬编码 JSON schema prompt
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@@ -11,9 +18,9 @@ from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---- 默认硬编码 prompt(DB 不可用或读到默认XML模板时使用) ----
|
||||
# ---- 纯硬编码 JSON schema(DB 无有效配置时全量使用) ----
|
||||
|
||||
DEFAULT_FAST_SYSTEM = (
|
||||
_FAST_JSON_SCHEMA = (
|
||||
"你是图片结构化识别器。严格按下方 JSON schema 返回一个对象,不要任何解释、"
|
||||
"不要markdown、不要代码块、不要前后缀文字。字段值不确定时填 null 或空数组。\n"
|
||||
"{\n"
|
||||
@@ -39,12 +46,13 @@ DEFAULT_FAST_SYSTEM = (
|
||||
' "pattern": "图案,如纯色/条纹/波点/格子/印花/碎花/Logo等",\n'
|
||||
' "colors": ["主色数组"],\n'
|
||||
' "mood": "整体氛围/情绪,如清新/活力/高级/温暖/冷峻/甜美/复古等"\n'
|
||||
"}"
|
||||
"}\n\n"
|
||||
"你必须只返回一个合法的JSON对象,不要输出任何其他文字、解释、XML标签或markdown。"
|
||||
)
|
||||
DEFAULT_FAST_USER = "识别这张图片的人物穿搭与主体信息,只返回JSON对象。"
|
||||
|
||||
DEFAULT_PRO_SYSTEM = (
|
||||
"你是图片分析专家。严格按下方 JSON schema 返回一个对象,不要解释、不要markdown、不要代码块。\n"
|
||||
_PRO_JSON_SCHEMA = (
|
||||
"你是图片分析专家。严格按下方 JSON schema 返回一个对象,不要解释、不要markdown、不要代码块、不要XML标签。\n"
|
||||
"{\n"
|
||||
' "has_person": true/false,\n'
|
||||
' "gender": "男"/"女"/null,\n'
|
||||
@@ -60,51 +68,98 @@ DEFAULT_PRO_SYSTEM = (
|
||||
' "product_name": "产品名,非产品图填null",\n'
|
||||
' "brand": "品牌,无则null",\n'
|
||||
' "key_features": ["核心特征数组,3-6个短语"]\n'
|
||||
"}"
|
||||
"}\n\n"
|
||||
"你必须只返回一个合法的JSON对象,不要输出任何其他文字、解释、XML标签或markdown。"
|
||||
)
|
||||
DEFAULT_PRO_USER = "分析这张图片,返回符合schema的JSON。"
|
||||
|
||||
# 用户自定义 prompt 末尾追加的硬约束
|
||||
_JSON_TAIL_FAST = "\n\n你必须只返回一个合法的JSON对象,不要输出任何其他文字、解释或markdown。"
|
||||
_JSON_TAIL_PRO = "\n\n你必须只返回一个合法的JSON对象,不要输出任何其他文字、解释或markdown。"
|
||||
# DB 配置存在时,追加在用户 system_prompt 末尾的JSON schema约束
|
||||
_FAST_JSON_APPEND = (
|
||||
"\n\n【输出格式要求】无论上文如何要求,最终你必须只返回一个合法的JSON对象,"
|
||||
"严格包含以下字段(字段值不确定时填null或空数组):\n"
|
||||
"{\n"
|
||||
' "has_person": true/false,\n'
|
||||
' "gender": "男"/"女"/null,\n'
|
||||
' "age_range": "儿童"/"青少年"/"青年"/"中年"/"老年"/null,\n'
|
||||
' "upper_wear": "上装款式字符串",\n'
|
||||
' "upper_color": "上装主色",\n'
|
||||
' "lower_wear": "下装款式(穿连衣裙时填null)",\n'
|
||||
' "lower_color": "下装主色",\n'
|
||||
' "dress_color": "连衣裙主色(穿连衣裙时填)",\n'
|
||||
' "accessories": ["配饰数组"],\n'
|
||||
' "hairstyle": "发型",\n'
|
||||
' "expression": "表情",\n'
|
||||
' "pose": "姿势",\n'
|
||||
' "scene": "场景",\n'
|
||||
' "style": "风格",\n'
|
||||
' "has_product": true/false,\n'
|
||||
' "category": "产品类目:服饰/鞋包/美妆/数码/食品/家居/配饰/母婴/非产品图",\n'
|
||||
' "product_name": "产品名称,非产品图填null",\n'
|
||||
' "brand": "品牌或文字标识,无则null",\n'
|
||||
' "material": "材质",\n'
|
||||
' "pattern": "图案",\n'
|
||||
' "colors": ["主色数组"],\n'
|
||||
' "mood": "整体氛围"\n'
|
||||
"}\n"
|
||||
"不要输出任何其他文字、解释、XML标签或markdown。"
|
||||
)
|
||||
|
||||
# 默认模板特征头(用于识别是否是内置XML模板)
|
||||
_DEFAULT_XML_MARKER = "你是电商商品视觉分析师"
|
||||
_PRO_JSON_APPEND = (
|
||||
"\n\n【输出格式要求】无论上文如何要求,最终你必须只返回一个合法的JSON对象,"
|
||||
"严格包含以下字段(字段值不确定时填null或空数组):\n"
|
||||
"{\n"
|
||||
' "has_person": true/false,\n'
|
||||
' "gender": "男"/"女"/null,\n'
|
||||
' "age_range": "儿童"/"青少年"/"青年"/"中年"/"老年"/null,\n'
|
||||
' "outfit": "整体穿着描述(含颜色款式)",\n'
|
||||
' "hair": "发型发色",\n'
|
||||
' "pose": "姿势",\n'
|
||||
' "expression": "表情",\n'
|
||||
' "scene": "场景",\n'
|
||||
' "mood": "氛围",\n'
|
||||
' "has_product": true/false,\n'
|
||||
' "category": "类目:服饰/鞋包/美妆/数码/食品/家居/配饰/母婴/非产品图",\n'
|
||||
' "product_name": "产品名,非产品图填null",\n'
|
||||
' "brand": "品牌,无则null",\n'
|
||||
' "key_features": ["核心特征3-6个短语"]\n'
|
||||
"}\n"
|
||||
"不要输出任何其他文字、解释、XML标签或markdown。"
|
||||
)
|
||||
|
||||
_cache_lock = threading.Lock()
|
||||
_cache: dict[str, tuple[float, Any]] = {}
|
||||
_CACHE_TTL = 30.0
|
||||
|
||||
|
||||
def _load_template() -> Any | None:
|
||||
"""从DB读image_analysis模板,失败返回None。"""
|
||||
def _load_db_template() -> Any | None:
|
||||
"""直接查DB viral_video_prompt_templates 中 is_active=true 的 image_analysis 记录;
|
||||
DB不可达/无记录/异常返回None。
|
||||
复用 prompt_loader._load_from_db,它只查DB不做DEFAULT_TEMPLATES fallback,
|
||||
返回None表示DB无记录或异常。"""
|
||||
try:
|
||||
from packages.application.viral_video.prompt_loader import get_template
|
||||
return get_template("image_analysis")
|
||||
from packages.application.viral_video.prompt_loader import _load_from_db
|
||||
|
||||
return _load_from_db("image_analysis")
|
||||
except Exception as e:
|
||||
logger.warning("[vision.v2] 读取后台prompt配置失败: %s", e)
|
||||
logger.warning("[vision.v2] 查询DB prompt配置失败: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def _is_custom(template: Any) -> bool:
|
||||
"""判断读到的模板是不是用户自定义的(不是内置默认XML长prompt)。"""
|
||||
if not template:
|
||||
return False
|
||||
sp = getattr(template, "system_prompt", "") or ""
|
||||
# 默认模板开头是"你是电商商品视觉分析师...",XML格式,不适合qwen+JSON
|
||||
if _DEFAULT_XML_MARKER in sp[:30]:
|
||||
return False
|
||||
# 其他有内容的system_prompt视为用户自定义
|
||||
return bool(sp.strip())
|
||||
def _render_user(tpl: Any | None, default_user: str) -> str:
|
||||
if not tpl:
|
||||
return default_user
|
||||
tpl_str = getattr(tpl, "user_prompt_template", "") or ""
|
||||
if not tpl_str.strip():
|
||||
return default_user
|
||||
rendered = tpl_str.replace("{image_count}", "1").replace("{industry}", "通用").replace("{image_urls}", "").strip()
|
||||
return rendered or default_user
|
||||
|
||||
|
||||
def resolve_fast_prompt() -> tuple[str, str]:
|
||||
"""返回 (system_prompt, user_prompt) 给 qwen3.8-flash fast 路径。"""
|
||||
return _resolve("fast")
|
||||
|
||||
|
||||
def resolve_pro_prompt() -> tuple[str, str]:
|
||||
"""返回 (system_prompt, user_prompt) 给 qwen3.7-plus fallback 路径。"""
|
||||
return _resolve("pro")
|
||||
|
||||
|
||||
@@ -116,32 +171,31 @@ def _resolve(kind: str) -> tuple[str, str]:
|
||||
if hit and now - hit[0] < _CACHE_TTL:
|
||||
return hit[1]
|
||||
|
||||
default_sys = DEFAULT_FAST_SYSTEM if kind == "fast" else DEFAULT_PRO_SYSTEM
|
||||
default_sys = _FAST_JSON_SCHEMA if kind == "fast" else _PRO_JSON_SCHEMA
|
||||
default_user = DEFAULT_FAST_USER if kind == "fast" else DEFAULT_PRO_USER
|
||||
tail = _JSON_TAIL_FAST if kind == "fast" else _JSON_TAIL_PRO
|
||||
append = _FAST_JSON_APPEND if kind == "fast" else _PRO_JSON_APPEND
|
||||
|
||||
sys_prompt = default_sys
|
||||
usr_prompt = default_user
|
||||
try:
|
||||
tpl = _load_template()
|
||||
if _is_custom(tpl):
|
||||
custom_sys = (getattr(tpl, "system_prompt", "") or "").strip()
|
||||
custom_user_tpl = getattr(tpl, "user_prompt_template", "") or ""
|
||||
if custom_sys:
|
||||
sys_prompt = custom_sys + tail
|
||||
if custom_user_tpl:
|
||||
# V2 是单图调用,简单替换几个常用占位符;缺键原样保留
|
||||
usr_prompt = (custom_user_tpl
|
||||
.replace("{image_count}", "1")
|
||||
.replace("{industry}", "通用")
|
||||
.replace("{image_urls}", "")
|
||||
.strip())
|
||||
if not usr_prompt:
|
||||
usr_prompt = default_user
|
||||
logger.info("[vision.v2] 使用后台自定义prompt (kind=%s version=%s)",
|
||||
kind, getattr(tpl, "version", "?"))
|
||||
tpl = _load_db_template()
|
||||
if tpl is not None:
|
||||
db_sys = (getattr(tpl, "system_prompt", "") or "").strip()
|
||||
if db_sys:
|
||||
sys_prompt = db_sys + append
|
||||
usr_prompt = _render_user(tpl, default_user)
|
||||
logger.info(
|
||||
"[vision.v2] 使用DB image_analysis prompt (kind=%s version=%s sys_len=%d)",
|
||||
kind,
|
||||
getattr(tpl, "version", "?"),
|
||||
len(db_sys),
|
||||
)
|
||||
else:
|
||||
logger.debug("[vision.v2] DB image_analysis system_prompt为空,使用默认JSON (kind=%s)", kind)
|
||||
else:
|
||||
logger.debug("[vision.v2] DB无image_analysis记录/不可达,使用默认JSON prompt (kind=%s)", kind)
|
||||
except Exception as e:
|
||||
logger.warning("[vision.v2] 解析后台prompt失败,使用默认: %s", e)
|
||||
logger.warning("[vision.v2] 解析DB prompt异常,使用默认: %s", e)
|
||||
|
||||
with _cache_lock:
|
||||
_cache[cache_key] = (now, (sys_prompt, usr_prompt))
|
||||
|
||||
@@ -23,10 +23,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# 超时(可通过环境变量覆盖)
|
||||
_IMG_WORKERS = int(os.environ.get("VISION_V2_IMG_WORKERS", "8"))
|
||||
_FAST_TIMEOUT = float(os.environ.get("VISION_V2_FAST_TIMEOUT", "12"))
|
||||
_FAST_JSON_TIMEOUT = float(os.environ.get("VISION_V2_FAST_JSON_TIMEOUT", "12"))
|
||||
_FAST_TIMEOUT = float(os.environ.get("VISION_V2_FAST_TIMEOUT", "15"))
|
||||
_FAST_JSON_TIMEOUT = float(os.environ.get("VISION_V2_FAST_JSON_TIMEOUT", "15"))
|
||||
_OCR_TIMEOUT = float(os.environ.get("VISION_V2_OCR_TIMEOUT", "6"))
|
||||
_PRO_TIMEOUT = float(os.environ.get("VISION_V2_PRO_TIMEOUT", "25"))
|
||||
_PRO_TIMEOUT = float(os.environ.get("VISION_V2_PRO_TIMEOUT", "30"))
|
||||
|
||||
_FALLBACK_RESULT = {
|
||||
"name": "未识别",
|
||||
|
||||
@@ -25,7 +25,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
_PRO_MODEL = "qwen3.7-plus"
|
||||
_DEFAULT_TIMEOUT = 25
|
||||
_DEFAULT_TIMEOUT = 30
|
||||
_DEFAULT_MAX_TOKENS = 800
|
||||
|
||||
|
||||
@@ -131,10 +131,13 @@ def call_pro_vlm(
|
||||
"model": _PRO_MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": [
|
||||
{"type": "image_url", "image_url": {"url": img_url}},
|
||||
{"type": "text", "text": user_prompt},
|
||||
]},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "image_url", "image_url": {"url": img_url}},
|
||||
{"type": "text", "text": user_prompt},
|
||||
],
|
||||
},
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"max_tokens": _DEFAULT_MAX_TOKENS,
|
||||
@@ -165,8 +168,11 @@ def call_pro_vlm(
|
||||
reasoning_tokens = ctd.get("reasoning_tokens", 0)
|
||||
logger.info(
|
||||
"[vision.v2] pro 完成 model=%s elapsed=%.1fs in=%d out=%d reasoning=%d",
|
||||
_PRO_MODEL, elapsed,
|
||||
usage.get("prompt_tokens", 0), usage.get("completion_tokens", 0), reasoning_tokens,
|
||||
_PRO_MODEL,
|
||||
elapsed,
|
||||
usage.get("prompt_tokens", 0),
|
||||
usage.get("completion_tokens", 0),
|
||||
reasoning_tokens,
|
||||
)
|
||||
s = raw.strip()
|
||||
if s.startswith("```"):
|
||||
|
||||
@@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
|
||||
# DashScope OpenAI 兼容 endpoint
|
||||
_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
_FAST_MODEL = "qwen3.8-flash"
|
||||
_DEFAULT_TIMEOUT = 12
|
||||
_DEFAULT_TIMEOUT = 15
|
||||
_DEFAULT_MAX_TOKENS = 350
|
||||
|
||||
|
||||
@@ -102,7 +102,9 @@ def call_fast_json(
|
||||
)
|
||||
elapsed = time.time() - t0
|
||||
if resp.status_code != 200:
|
||||
logger.warning("[vision.v2] fast_json HTTP %d elapsed=%.1fs body=%s", resp.status_code, elapsed, resp.text[:200])
|
||||
logger.warning(
|
||||
"[vision.v2] fast_json HTTP %d elapsed=%.1fs body=%s", resp.status_code, elapsed, resp.text[:200]
|
||||
)
|
||||
return None
|
||||
data = resp.json()
|
||||
raw = (data.get("choices") or [{}])[0].get("message", {}).get("content")
|
||||
@@ -116,8 +118,11 @@ def call_fast_json(
|
||||
reasoning_tokens = ctd.get("reasoning_tokens", 0)
|
||||
logger.info(
|
||||
"[vision.v2] fast_json 完成 model=%s elapsed=%.1fs in=%d out=%d reasoning=%d",
|
||||
_FAST_MODEL, elapsed,
|
||||
usage.get("prompt_tokens", 0), usage.get("completion_tokens", 0), reasoning_tokens,
|
||||
_FAST_MODEL,
|
||||
elapsed,
|
||||
usage.get("prompt_tokens", 0),
|
||||
usage.get("completion_tokens", 0),
|
||||
reasoning_tokens,
|
||||
)
|
||||
text = _strip_code_fence(raw)
|
||||
lpos, r = text.find("{"), text.rfind("}")
|
||||
@@ -133,7 +138,10 @@ def call_fast_json(
|
||||
return None
|
||||
logger.info(
|
||||
"[vision.v2] fast_json 完成 elapsed=%.1fs has_person=%s has_product=%s category=%s",
|
||||
elapsed, obj.get("has_person"), obj.get("has_product"), obj.get("category"),
|
||||
elapsed,
|
||||
obj.get("has_person"),
|
||||
obj.get("has_product"),
|
||||
obj.get("category"),
|
||||
)
|
||||
return obj
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user