v88 功能增强:产品落盘持久化 + 生图网关适配 + 模板导出优化

- 产品持久化:每完成一个产品立即追加写入 products_pending.jsonl,崩溃不丢已完成产品,finish 读盘合并后统一写模板
- 503 致命错误提前终止:compose/product/seed_shot 端到端识别,提前终止搜索分析,丢弃未完成简报,保留已完成落盘产品直接合成模板
- 模特分配:material_library 合格模特图按任务序号独立随机,同 SPU 多款不再共用同一模特
- 图像网关适配:execution_mode/background 默认不再传入 yunfei 等标准网关,base_url 需带 /v1;429/5xx/空响应退避重试
- Pinterest 分析:删除 term 注入与纯文本降级,失败直接放弃;图片上传前 PIL 完整性校验;suitable_for_print=False 过滤丢弃
- 模板导出:不再产生空白 xlsx,文件名=模板原文件名_已填写;写入前按货号末 3 位升序排序
- 删除对接文档.md,更新 README,gitignore 排除测试产物
This commit is contained in:
2026-08-28 10:28:35 +08:00
parent 685b7b0862
commit 2a96ec0870
28 changed files with 1187 additions and 729 deletions
+100
View File
@@ -11,6 +11,8 @@ import sqlite3
from pathlib import Path
from typing import Any, Dict, List, Optional
from PIL import Image
# 支持的图片格式:模特图/底图均按此识别(png/jpg 等常见格式全覆盖;AVIF/GIF/TIFF 亦支持)
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".avif", ".gif", ".tiff", ".tif"}
@@ -114,6 +116,104 @@ def find_first_model_folder(material_root, preferred: Optional[str] = None):
return None, []
def image_ratio_ok(path, target_ratio: float = 3 / 4, tolerance: float = 0.06) -> bool:
"""图片宽高比是否接近目标比例(默认 3:4,相对容差 6%)。
相对容差:接受 [target*(1-tol), target*(1+tol)],对 3:4 即 0.705~0.795。
无法解析的图片(损坏/非标准)按不通过处理,避免坏图被当模特。
"""
try:
with Image.open(path) as im:
w, h = im.size
if w <= 0 or h <= 0:
return False
ratio = w / h
lo = target_ratio * (1 - tolerance)
hi = target_ratio * (1 + tolerance)
return lo <= ratio <= hi
except Exception: # noqa: BLE001
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:
return []
imgs = [f for f in sorted(d.iterdir()) if f.is_file() and f.suffix.lower() in IMG_EXTS]
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} 比例:"
f"{len(imgs)}{len(ok)}")
return ok
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):