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:
@@ -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_schema(OpenAI 原生 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} 占位符(可为空)。
|
||||
模板 1(US/GB/AU/MX)返回 {"en_title","cn_title"};
|
||||
模板 2(JP)额外返回 {"ja_title"};
|
||||
模板 3(ES)返回 {"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}},
|
||||
|
||||
Reference in New Issue
Block a user