v89-v91 模板增强 + 图源映射 + 多模态提示词可配置化
- 图源映射统一:热点采集与 Pinterest 模式均走 config.product.mark_dirs 配置,按任务序号随机抽模特图/平铺图 - 商品产地固定:统一为「中国大陆」+「产地省份=广东省」(不再读站点/字典映射) - 模板 SKU 字段检测:按建议售价同一套路检测 SKU分类/SKU数量/SKU数量单位,必填时填入单品/1/件 - 多模态分析提示词可配置:prompts/pinterest_analyze_system.md + user.md,支持国家覆盖,不丢文件回退内置 - 自定义图片模式:新增 pinterest_custom_load_node,图片数量硬校验,选品清单 ≤ 有效图片数 - 模板导出优化:写入前按货号末 3 位升序排序,不再产生空白 xlsx - 修复 v90 project review 10 项(503 致命终止、线程安全、原子写入等)
This commit is contained in:
+68
-21
@@ -199,29 +199,50 @@ class PinterestPipeline:
|
||||
return None
|
||||
|
||||
def _assign_models(self) -> Dict[str, Any]:
|
||||
"""任务级模特分配:按 spu.mark 映射模特目录 → 过滤非 3:4 图片 → 每个任务随机抽取。
|
||||
"""任务级图源分配:按每个任务 spu.mark 从可配置的「模特图/平铺图」文件夹间随机抽图。
|
||||
|
||||
每个产品任务(含同一 SPU 的多个款)都独立随机抽一个模特,保证同款多产品模特不重复。
|
||||
每个任务独立随机抽一张:先合并该 mark 对应「有图的」模特/平铺目录的全部合格图,再从其中随机抽一张,
|
||||
抽到哪个文件夹的图就返回对应 kind(model/flat),供 product_node 用对应提示词合成。
|
||||
某类目录无图则只用另一类;两类都无图则该任务无图源(跳过合成)。
|
||||
返回 {task_key: {"img": Path, "kind": "model"|"flat", "prompts": {…}}}。
|
||||
"""
|
||||
model_assign: Dict[str, Any] = {}
|
||||
import random as _random
|
||||
assign: Dict[str, Any] = {}
|
||||
pcfg = self.config.get("product") or {}
|
||||
mark_dirs = pcfg.get("mark_dirs") or {}
|
||||
try:
|
||||
from graph.product import build_mark_model_map, find_model_images_for_mark
|
||||
mark_map = build_mark_model_map(self._db_path, self._material_root)
|
||||
except Exception: # noqa: BLE001
|
||||
mark_map = {}
|
||||
# 每个任务按序号绑定独立随机模特(同 SPU 多款也各自随机,不共用)
|
||||
from graph.product import build_mark_sources
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"[pinterest_pipeline] 图源映射导入失败: {e}")
|
||||
return assign
|
||||
# 每个出现过的 mark 各建一个图源池,避免多 mark 错配
|
||||
pool_by_mark: Dict[str, list] = {}
|
||||
for _i, (spu, _skus) in enumerate(self._worklist):
|
||||
mark = str(spu.get("mark") or "").strip() or "1"
|
||||
if mark in pool_by_mark:
|
||||
continue
|
||||
try:
|
||||
sources = build_mark_sources(self._material_root, mark_dirs, self._category, mark=mark)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"[pinterest_pipeline] mark={mark} 图源构建失败: {e}")
|
||||
sources = {"model": [], "flat": []}
|
||||
pool = []
|
||||
for kind in ("model", "flat"):
|
||||
for p in sources.get(kind) or []:
|
||||
pool.append((p, kind))
|
||||
pool_by_mark[mark] = pool
|
||||
if pool:
|
||||
print(f"[pinterest_pipeline] mark={mark} 图源池:{len(pool)} 张(模特/平铺)")
|
||||
for _i, (spu, _skus) in enumerate(self._worklist):
|
||||
key = f"task_{_i}"
|
||||
mark = str(spu.get("mark") or "").strip() or "1"
|
||||
folder = mark_map.get(mark, self._category)
|
||||
try:
|
||||
pool_imgs = find_model_images_for_mark(self._db_path, self._material_root,
|
||||
mark, folder)
|
||||
except Exception: # noqa: BLE001
|
||||
pool_imgs = []
|
||||
if pool_imgs:
|
||||
model_assign[key] = random.choice(pool_imgs) # 过滤后随机抽
|
||||
return model_assign
|
||||
pool = pool_by_mark.get(mark) or []
|
||||
if not pool:
|
||||
continue
|
||||
img, kind = _random.choice(pool) # 每任务独立随机抽一张(含 kind)
|
||||
assign[key] = {"img": img, "kind": kind,
|
||||
"prompts": (mark_dirs.get(mark) or mark_dirs.get("1") or {})}
|
||||
return assign
|
||||
|
||||
def _load_materials(self) -> Dict[str, str]:
|
||||
material_map: Dict[str, str] = {}
|
||||
@@ -384,6 +405,17 @@ class PinterestPipeline:
|
||||
self._cond.wait()
|
||||
return not self._briefs and self._in_flight <= 0
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
# 异常路径也确保排空并释放线程池,避免 dispatcher/worker 泄漏
|
||||
try:
|
||||
self.finish()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return False
|
||||
|
||||
def finish(self) -> tuple:
|
||||
"""排空简报池、等待全部产品完成,返回 (products, errors)。
|
||||
|
||||
@@ -507,13 +539,16 @@ class PinterestPipeline:
|
||||
def _persist_product(self, prod: Dict[str, Any]) -> None:
|
||||
"""把已完成产品追加写入 products_pending.jsonl(JSONL 每行一个产品)。
|
||||
|
||||
并发安全:写入持 _products_lock,整行一次写(含换行),避免并发 append 交织;
|
||||
落盘失败不阻塞主流程(仅告警);finish() 时读盘合并,保证已完成产品不丢。
|
||||
"""
|
||||
try:
|
||||
import json as _json
|
||||
self._pending_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self._pending_file, "a", encoding="utf-8") as f:
|
||||
f.write(_json.dumps(prod, ensure_ascii=False) + "\n")
|
||||
line = _json.dumps(prod, ensure_ascii=False) + "\n"
|
||||
with self._products_lock:
|
||||
with open(self._pending_file, "a", encoding="utf-8") as f:
|
||||
f.write(line)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"[pinterest_pipeline] 产品落盘失败(不影响流程): {e}")
|
||||
|
||||
@@ -630,6 +665,18 @@ class PinterestPipeline:
|
||||
print(f"[pinterest_pipeline] 补充简报装配失败: {e}")
|
||||
return None
|
||||
|
||||
def _model_source(self, task_idx: int, spu=None) -> Dict[str, Any]:
|
||||
"""返回某任务给 _process_spu 的图源参数:model_img / model_kind / prompts。"""
|
||||
src = self._model_assign.get(f"task_{task_idx}") or {}
|
||||
img = src.get("img")
|
||||
if img is None:
|
||||
return {}
|
||||
if spu is not None and int(spu.get("mark") or 0) != 1:
|
||||
return {"model_img": img} # 非 mark=1 走旧逻辑(不传 kind,product_node 按其 mark 自行判定)
|
||||
return {"model_img": img,
|
||||
"model_kind": src.get("kind", "model"),
|
||||
"prompts": src.get("prompts") or {}}
|
||||
|
||||
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]]:
|
||||
from graph.nodes.product_node import _process_spu as _ps
|
||||
@@ -642,8 +689,8 @@ class PinterestPipeline:
|
||||
prod_dir, brief, self._ib, spu, skus, self.config.get("product") or {},
|
||||
self._errors, design_path, self._title_backend, self.country,
|
||||
img_code=img_code,
|
||||
model_img=self._model_assign.get(f"task_{task_idx}"),
|
||||
on_503=lambda: (self.record_503(), self.abort_unfinished()))
|
||||
on_503=lambda: (self.record_503(), self.abort_unfinished()),
|
||||
**self._model_source(task_idx, spu))
|
||||
if r:
|
||||
r["img_code"] = img_code
|
||||
return r
|
||||
|
||||
Reference in New Issue
Block a user