v110-v112 自定义模式完善 + 模板导出增强 + 多模态兼容优化

- 自定义模式:分析模型输出 delta 唯一改动指令,生图模板 custom_image_prompt.md({delta} 占位符),不再使用负向提示词;generate_design 按 custom_mode 分支,Pinterest 模式保留原创化指令,两模式互不影响
- 多模态分析 response_format 三级回退(json_schema → json_object → none),兼容 DeepSeek
- 模板导出:details 扩展列(细节1/2/3)、target_audience 扩展列(适用人群1)、固定值风格1=休闲/风格2=运动
- 童装特征库更新 + 标题模板外部化 + 图源映射增强
This commit is contained in:
2026-09-03 18:28:39 +08:00
parent d68cc3b9e3
commit 5ab5cf6586
40 changed files with 1967 additions and 149 deletions
+79 -36
View File
@@ -10,7 +10,7 @@ import os
import re
import time
from pathlib import Path
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
import requests
import yaml
@@ -197,20 +197,38 @@ def _read_title_prompt(tpl_no: str) -> str:
return ""
_TITLE_ROUTE_CACHE: Optional[Dict[str, str]] = None
_TITLE_ROUTE_CACHE_FILE: str = ""
def _load_title_route() -> Dict[str, str]:
"""从 config.yaml 顶层 title_templates.route 读取国家→模板编号路由;缺失/留空回退内置默认。"""
"""从 config.yaml 顶层 title_templates.route 读取国家→模板编号路由;缺失/留空回退内置默认。
路由在运行期不变,模块级缓存避免并发多产品时每次重读 config.yaml(mtime 变化会刷新)。
"""
global _TITLE_ROUTE_CACHE, _TITLE_ROUTE_CACHE_FILE
cfg_file = ""
for base in (runtime_root(), project_root()):
p = base / "config.yaml"
if not p.exists():
continue
try:
data = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
route = (data.get("title_templates") or {}).get("route") or {}
if isinstance(route, dict) and route:
return {str(k): str(v) for k, v in route.items()}
except Exception as e: # noqa: BLE001
print(f"[titles] 读取 config.yaml title_templates.route 失败: {e}")
return dict(_TITLE_ROUTE_FALLBACK)
if p.exists():
cfg_file = str(p)
break
if cfg_file == _TITLE_ROUTE_CACHE_FILE and _TITLE_ROUTE_CACHE is not None:
return dict(_TITLE_ROUTE_CACHE)
route: Dict[str, str] = {}
try:
data = yaml.safe_load(open(cfg_file, encoding="utf-8")) or {}
r = (data.get("title_templates") or {}).get("route") or {}
if isinstance(r, dict) and r:
route = {str(k): str(v) for k, v in r.items()}
except Exception as e: # noqa: BLE001
print(f"[titles] 读取 config.yaml title_templates.route 失败: {e}")
if not route:
route = dict(_TITLE_ROUTE_FALLBACK)
cfg_file = ""
_TITLE_ROUTE_CACHE = dict(route)
_TITLE_ROUTE_CACHE_FILE = cfg_file
return dict(route)
def _inject_now(prompt: str) -> str:
@@ -225,12 +243,17 @@ def _inject_now(prompt: str) -> str:
.replace("{season}", season))
def resolve_title_prompt(country: str = "") -> str:
"""按国家解析标题生成提示词(自动注入当前时间变量);未知国家/留空回退模板 1。"""
def resolve_title_prompt(country: str = "", category_path: str = "") -> str:
"""按国家解析标题生成提示词(自动注入当前时间 + 类目路径变量);未知国家/留空回退模板 1。
category_path:商品上传模版「类目」完整路径(如
服装、鞋靴和珠宝饰品>女童时尚>女童服装>女童上衣、T恤、衬衫>女童T恤),
模板含 {category_path} 占位符时替换;未提供/为空则替换为空串。"""
route = _load_title_route()
tpl_no = route.get(country or "", "1")
prompt = _read_title_prompt(tpl_no) or _TITLE_TEMPLATES_FALLBACK.get(tpl_no, _TITLE_TEMPLATES_FALLBACK["1"])
return _inject_now(prompt)
prompt = _inject_now(prompt)
return prompt.replace("{category_path}", str(category_path or ""))
def build_seed_user_prompt(context: Dict[str, Any]) -> str:
@@ -409,6 +432,7 @@ PINTEREST_ANALYZE_SCHEMA = {
"properties": {
"suitable_for_print": {"type": "boolean"},
"image_prompt": {"type": "string"},
"delta": {"type": "string"},
},
"required": ["suitable_for_print", "image_prompt"],
"additionalProperties": False,
@@ -777,32 +801,43 @@ class OpenAICompatBackend(LLMBackend):
{"type": "text", "text": user_prompt},
]
user_content += [{"type": "image_url", "image_url": {"url": u}} for u in data_uris]
payload = {
base_payload = {
"model": model,
"messages": [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": user_content},
],
"temperature": 0.5,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": PINTEREST_ANALYZE_SCHEMA["name"],
"strict": True,
"schema": PINTEREST_ANALYZE_SCHEMA["schema"],
},
},
}
try:
resp = requests.post(url, json=payload, headers=headers, timeout=180, proxies=NO_PROXY)
resp.raise_for_status()
return str(resp.json()["choices"][0]["message"].get("content") or "")
except Exception as e: # noqa: BLE001 兼容厂商不支持 json_schema
_notify_400(e)
payload["response_format"] = {"type": "json_object"}
resp = requests.post(url, json=payload, headers=headers, timeout=180, proxies=NO_PROXY)
resp.raise_for_status()
return str(resp.json()["choices"][0]["message"].get("content") or "")
# 兼容厂商差异的尝试链:
# 1) json_schemaOpenAI 原生 strict 输出)
# 2) json_object(部分兼容厂商支持,但要求提示词含 "json",如 DeepSeek → 追加显式指令)
# 3) 无 response_format(依赖 _extract_json 兜底解析)
last_err: Optional[Exception] = None
for variant in ("json_schema", "json_object", "none"):
payload = dict(base_payload)
if variant == "json_schema":
payload["response_format"] = {
"type": "json_schema",
"json_schema": {
"name": PINTEREST_ANALYZE_SCHEMA["name"],
"strict": True,
"schema": PINTEREST_ANALYZE_SCHEMA["schema"],
},
}
elif variant == "json_object":
payload["response_format"] = {"type": "json_object"}
payload["messages"][1]["content"] = [
{"type": "text", "text": user_prompt + "\nReturn your answer as a JSON object."},
] + [{"type": "image_url", "image_url": {"url": u}} for u in data_uris]
try:
resp = requests.post(url, json=payload, headers=headers, timeout=180, proxies=NO_PROXY)
resp.raise_for_status()
return str(resp.json()["choices"][0]["message"].get("content") or "")
except Exception as e: # noqa: BLE001
last_err = e
_notify_400(e)
raise last_err
# 图片输入失败/无有效图片 → 直接放弃该产品(不降级纯文本),由节点跳过后续流程
if not data_uris:
@@ -826,6 +861,10 @@ class OpenAICompatBackend(LLMBackend):
designs_raw = parsed
else:
designs_raw = []
# 自定义模式:分析模型按 custom_analyze_system.md 直接返回 {"delta": "..."}(无 designs 包装)
# → 包装成单条简报;suitable_for_print 缺省 True(下游 _brief_suitable 默认放行)
if not designs_raw and custom_mode and isinstance(parsed, dict) and str(parsed.get("delta") or "").strip():
designs_raw = [parsed]
for i, d in enumerate(designs_raw):
if not isinstance(d, dict):
continue
@@ -833,17 +872,21 @@ class OpenAICompatBackend(LLMBackend):
"topic": term,
"suitable_for_print": bool(d.get("suitable_for_print", True)),
"image_prompt": str(d.get("image_prompt", "")).strip(),
# 自定义模式分析模型产出的「唯一改动」指令,注入生图模板 {delta} 占位符
"delta": str(d.get("delta", "")).strip(),
# 生图参考:每条简报对应其来源爬取图(LLM 按图逐张产出简报,顺序一一对应)
"ref_images": [str(image_paths[i])] if i < len(image_paths) else [],
"source": "pinterest",
})
return designs
def generate_title(self, image_path: str, system_prompt: str = "", country: str = "") -> Dict[str, Any]:
def generate_title(self, image_path: str, system_prompt: str = "", country: str = "",
category_path: str = "") -> Dict[str, Any]:
"""多模态:分析服装图片,生成商品标题(按国家路由模板)。
系统提示词:显式传入优先;否则按 country 经 config.yaml title_templates.route 路由到
prompts/title_prompt_<编号>.md 对应模板(缺失回退内置默认)。
category_path:模版「类目」完整路径,注入模板 {category_path} 占位符(可为空)。
模板 1US/GB/AU/MX)返回 {"en_title","cn_title"}
模板 2(JP)额外返回 {"ja_title"}
模板 3ES)返回 {"es_title","cn_title"}。
@@ -874,7 +917,7 @@ class OpenAICompatBackend(LLMBackend):
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt or resolve_title_prompt(country)},
{"role": "system", "content": system_prompt or resolve_title_prompt(country, category_path)},
{"role": "user", "content": [
{"type": "text", "text": "请分析这张服装图片,按规则输出标题,结果以 JSON 格式返回。"},
{"type": "image_url", "image_url": {"url": data_uri}},