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:
2026-08-28 16:24:50 +08:00
parent 2a96ec0870
commit 71a48e4ed5
40 changed files with 780 additions and 200 deletions
+45 -71
View File
@@ -135,85 +135,59 @@ def image_ratio_ok(path, target_ratio: float = 3 / 4, tolerance: float = 0.06) -
return False
def build_mark_model_map(db_path, material_root) -> Dict[str, str]:
"""启动任务前检测 spu.mark 字段,建立 {mark: 模特文件夹名} 字典。
规则:
- 读取 spu 表全部 mark 值(去重);
- 每个 mark 映射到 material_library/<mark> 目录(目录名与 mark 一致);
- 目录不存在时回退到 category 默认目录(T-shirt);
- 目前库中 mark=1 → 映射到 material_library/T-shirt。
"""
mark_map: Dict[str, str] = {}
try:
root = Path(material_root)
if not root.exists():
return mark_map
folders = [d.name for d in sorted(root.iterdir()) if d.is_dir()]
if not folders:
return mark_map
# 读 spu.mark 实际值(去重)
marks: List[str] = []
try:
conn = _connect(db_path)
rows = conn.execute("SELECT DISTINCT mark FROM SPU WHERE mark IS NOT NULL AND mark != ''").fetchall()
conn.close()
marks = [str(r["mark"]).strip() for r in rows if str(r["mark"]).strip()]
except Exception: # noqa: BLE001
marks = []
if not marks:
marks = ["1"] # 库无 mark 数据时按默认 1 处理
for m in marks:
if m in folders:
mark_map[m] = m
else:
# mark 无同名目录 → 回退默认 T-shirt(当前 mark=1 → T-shirt
mark_map[m] = "T-shirt" if "T-shirt" in folders else folders[0]
print(f"[product] mark→模特目录映射: {mark_map}")
except Exception as e: # noqa: BLE001
print(f"[product] mark→模特目录映射构建失败: {e}")
return mark_map
def find_model_images_for_mark(db_path, material_root, mark, category: str = "T-shirt",
ratio: float = 3 / 4, tolerance: float = 0.06) -> List[Path]:
"""按 spu.mark 定位模特目录,过滤非目标比例图片,返回合格图片列表。
- mark 有对应目录(material_library/<mark>)→ 用该目录;
- 否则回退 category(如 T-shirt);
- 过滤掉非 3:4 比例(默认容差 6%)的图片;
- 返回过滤后的图片列表(供调用方随机抽取)。
"""
root = Path(material_root)
if not root.exists():
return []
d = None
if mark is not None:
cand = root / str(mark)
if cand.is_dir():
d = cand
if d is None:
cand = root / category
if cand.is_dir():
d = cand
if d is None:
# 兜底:第一个有图的目录(跳过无图目录)
for sub in sorted(root.iterdir()):
if not sub.is_dir():
continue
if any(f.is_file() and f.suffix.lower() in IMG_EXTS for f in sub.iterdir()):
d = sub
break
if d is None:
def _dir_images(d: Path, ratio: float, tolerance: float) -> List[Path]:
"""返回目录内满足比例过滤(默认3:4)的图片列表;目录不存在/无图片返回空。"""
if d is None or not d.is_dir():
return []
imgs = [f for f in sorted(d.iterdir()) if f.is_file() and f.suffix.lower() in IMG_EXTS]
if not imgs:
return []
ok = [f for f in imgs if image_ratio_ok(f, ratio, tolerance)]
if len(ok) < len(imgs):
print(f"[product] 模特目录 {d.name}/ 过滤非 {int(ratio * 100)}:{int(ratio * 100) + 1} 比例:"
print(f"[product] 目录 {d.name}/ 过滤非 {int(ratio * 100)}:{int(ratio * 100) + 1} 比例:"
f"{len(imgs)}{len(ok)}")
return ok
def build_mark_sources(material_root, mark_dirs: Optional[Dict] = None,
category: str = "T-shirt", mark: str = "1",
ratio: float = 3 / 4, tolerance: float = 0.06) -> Dict[str, Any]:
"""按可配置的 mark→图源映射,返回指定 mark 的图源选择信息。
图源 = {"model": [Path...], "flat": [Path...]}(模特图 / 平铺图,各带英文键)。
从 config.product.mark_dirs 读 mark 对应的两个子目录名(model_dir/flat_dir),
在 material_library 下解析 → 过滤 3:4 → 返回「有图的文件夹」图片列表。
mark 未配置时回退 mark_dirs["1"];完全无配置则回退 category 默认目录。
返回结构:{"model": [imgs], "flat": [imgs]}。
"""
root = Path(material_root)
result: Dict[str, Any] = {"model": [], "flat": []}
if not root.exists():
return result
mcfg = mark_dirs or {}
cfg = mcfg.get(str(mark)) or mcfg.get("1") or {} # 优先按 mark,其次回退默认 "1"
model_name = str(cfg.get("model_dir") or "").strip()
flat_name = str(cfg.get("flat_dir") or "").strip()
if model_name:
result["model"] = _dir_images(root / model_name, ratio, tolerance)
if flat_name:
result["flat"] = _dir_images(root / flat_name, ratio, tolerance)
# 回退:某类目录未配置时,使用 category 默认目录补充模特图
if not result["model"] and not result["flat"]:
d = root / category
if d.is_dir():
result["model"] = _dir_images(d, ratio, tolerance)
if not result["model"] and not result["flat"]:
for sub in sorted(root.iterdir()):
if not sub.is_dir():
continue
imgs = _dir_images(sub, ratio, tolerance)
if imgs:
result["model"] = imgs
break
return result
def first_available_sku(db_path, basemap_root, spu_code: str) -> Optional[str]:
"""返回该款号下第一个「本地有底图」的 SKU.code;无则 None。"""
for c in list_colors(db_path, spu_code):