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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user