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
+40 -16
View File
@@ -20,6 +20,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional
from graph.paths import project_root, runtime_root
from graph.validate import ThreadSafeErrors
class PinterestPipeline:
@@ -83,6 +84,12 @@ class PinterestPipeline:
self._country_config = state.get("country_config") or {}
# 补充重试次数:设计生成失败/侵权时,从图池取新图重新分析的最多尝试次数
self._supply_attempts = int((self.config.get("pinterest") or {}).get("supply_attempts", 3))
# 自定义模式标志:pinterest_init 先于 pinterest_custom_load 执行(state.custom_mode 尚未置位),
# 故从 config 判断(mode=custom 或 custom_image_dir 非空),供 generate_design 决定是否追加原创化指令
_pcfg = self.config.get("pinterest") or {}
self.custom_mode = bool(state.get("custom_mode")) \
or (str(_pcfg.get("mode") or "").strip().lower() == "custom") \
or bool(str(_pcfg.get("custom_image_dir") or "").strip())
# 400 计数(per 种子词):多模态 + 生图模型合计,超限放弃当前种子词
self._err400_lock = threading.Lock()
@@ -470,6 +477,9 @@ class PinterestPipeline:
# 单条简报完整链路:设计 → 三合一 → OSS → 种草图
# ------------------------------------------------------------------ #
def _process_one(self, brief: Dict[str, Any], idx: int) -> None:
# 每个 worker 用独立线程安全错误收集器,后台调用链(generate_design/_process_spu
# 的 append 与 finish() 的 list(self._errors) 读取不再互相竞态,结束时统一合并。
errs = ThreadSafeErrors()
try:
if self.is_fatal_503_aborted():
return
@@ -480,7 +490,7 @@ class PinterestPipeline:
spu, skus = self._worklist[idx]
img_code = f"{self._prefix}{idx:03d}"
# 1) 生成设计(compose)——直接按货号命名 designs/{img_code}_design.png
design_path = self._gen_design(brief, img_code)
design_path = self._gen_design(brief, img_code, errs)
if self.is_fatal_503_aborted():
return
if self.is_400_aborted():
@@ -496,7 +506,7 @@ class PinterestPipeline:
if new_brief is None:
return
brief = new_brief
design_path = self._gen_design(brief, img_code)
design_path = self._gen_design(brief, img_code, errs)
if self.is_fatal_503_aborted():
return
if design_path:
@@ -505,7 +515,7 @@ class PinterestPipeline:
return
brief["design_path"] = design_path
# 2) 三合一(product)——同一货号;task_idx=本次简报序号,模特按任务独立随机
prod = self._process_spu(brief, spu, skus, img_code, design_path, task_idx=idx)
prod = self._process_spu(brief, spu, skus, img_code, design_path, task_idx=idx, errors=errs)
if self.is_fatal_503_aborted():
return
if not prod:
@@ -532,6 +542,9 @@ class PinterestPipeline:
"message": f"简报 {idx} 处理失败: {e}", "trace": ""})
print(f"[pinterest_pipeline] 简报 {idx} 处理失败: {e}")
finally:
# 合并本 worker 收集的错误到共享 _errors(持锁,供 finish() 安全读取)
with self._errors_lock:
self._errors.extend(list(errs))
with self._cond:
self._in_flight = max(0, self._in_flight - 1)
self._cond.notify_all()
@@ -571,7 +584,7 @@ class PinterestPipeline:
print(f"[pinterest_pipeline] 读回落盘产品失败: {e}")
return out
def _gen_design(self, brief: Dict[str, Any], img_code: str) -> Optional[str]:
def _gen_design(self, brief: Dict[str, Any], img_code: str, errors: ThreadSafeErrors) -> Optional[str]:
if self._ib is None:
return None
try:
@@ -587,18 +600,18 @@ class PinterestPipeline:
self.record_503()
self.abort_unfinished()
return generate_design(self._ib, brief, design_dir, img_code, self._errors,
return generate_design(self._ib, brief, design_dir, img_code, errors,
on_400=_on_400, on_503=_on_503,
size=str((self.config.get("compose") or {}).get("design_size") or "1024x1024"))
size=str((self.config.get("compose") or {}).get("design_size") or "1024x1024"),
custom_mode=self.custom_mode)
except Exception as e: # noqa: BLE001
if self.is_fatal_503(e):
self.record_503()
self.abort_unfinished()
return None
with self._errors_lock:
self._errors.append({"node": "compose", "type": type(e).__name__,
"message": f"设计稿生成失败 {brief.get('topic', '')}: {e}",
"trace": ""})
errors.append({"node": "compose", "type": type(e).__name__,
"message": f"设计稿生成失败 {brief.get('topic', '')}: {e}",
"trace": ""})
return None
def _supply_from_pool(self, reason: str) -> Optional[Dict[str, Any]]:
@@ -677,18 +690,29 @@ class PinterestPipeline:
"model_kind": src.get("kind", "model")}
def _process_spu(self, brief: Dict[str, Any], spu, skus: str, img_code: str,
design_path: str, task_idx: int = 0) -> Optional[Dict[str, Any]]:
design_path: str, task_idx: int = 0, errors: Optional[ThreadSafeErrors] = None) -> Optional[Dict[str, Any]]:
from graph.nodes.product_node import _process_spu as _ps
prod_dir = self.output_dir / "product"
prod_dir.mkdir(parents=True, exist_ok=True)
# 设计稿已按货号命名(designs/{img_code}_design.png),直接复用,无需再拷贝
brief = dict(brief)
brief["design_path"] = design_path
# 标题生成用:模版「类目」完整路径(注入标题提示词 {category_path}),懒加载缓存一次
if not hasattr(self, "_category_path"):
self._category_path = ""
try:
_tp = str((self.config.get("product") or {}).get("template_path") or "").strip()
if _tp:
from graph.seed_shot import read_template_category
self._category_path = read_template_category(_tp)
except Exception as _e: # noqa: BLE001
print(f"[pinterest_pipeline] 读取模版类目路径失败(标题 category_path 留空): {_e}")
r = _ps(self._db_path, self._basemap_root, self._material_root, self._category,
prod_dir, brief, self._ib, spu, skus, self.config.get("product") or {},
self._errors, design_path, self._title_backend, self.country,
errors, design_path, self._title_backend, self.country,
img_code=img_code,
on_503=lambda: (self.record_503(), self.abort_unfinished()),
on_503=lambda: (self.record_503(), self.abort_unfinished()),
category_path=self._category_path,
**self._model_source(task_idx, spu))
if r:
r["img_code"] = img_code
@@ -710,7 +734,7 @@ class PinterestPipeline:
continue
try:
compressed = compress_for_oss(src, str(Path(src).with_suffix(".oss.jpg")))
key = build_oss_key(self.country, self.task_timestamp, base_code, _gen_rand4())
key = build_oss_key(self.country, self.task_timestamp, base_code, _gen_rand4(), local=compressed)
url = upload_to_oss(self._oss_cfg, compressed, key)
if url:
r[f"{kind}_url"] = url
@@ -728,7 +752,7 @@ class PinterestPipeline:
continue
try:
compressed = compress_for_oss(src, str(Path(src).with_suffix(".oss.jpg")))
key = build_oss_key(self.country, self.task_timestamp, base_code, _gen_rand4())
key = build_oss_key(self.country, self.task_timestamp, base_code, _gen_rand4(), local=compressed)
url = upload_to_oss(self._oss_cfg, compressed, key)
if url:
cc["url"] = url
@@ -796,7 +820,7 @@ class PinterestPipeline:
compressed = compress_for_oss(pth, str(Path(pth).with_suffix(".oss.jpg")))
url = upload_to_oss(self._oss_cfg, compressed,
build_oss_key(self.country, self.task_timestamp,
code, _gen_rand4()))
code, _gen_rand4(), local=compressed))
if url:
urls.append(url)
r["seed_shot_urls"] = urls