模板导出增强 + 模特性别分组 + 三合一提示词精简

1) 模板导出:识别「基码表-胸围」填 sku.bust(多个胸围列都填);申报价格模糊匹配多列统一按加价后价格填写;详情图文不再拼接 img_url_2;SPU 款式来源统一填「现货款」;商品产地国家简称映射(沙特→沙特阿拉伯)
2) 模特性别分组:model_features 按男女分组,按模板类目含男/女固定取对应性别模特(含 Pinterest 模式 pipeline)
3) 三合一提示词:去掉 DESIGN CONTENT 四要素描述(设计已由设计稿提供)
4) 生图尺寸:全部改为读 config 不再硬编码(设计图 compose.design_size / 合成图 compose.size / 种草图 seed_shot.size)
This commit is contained in:
2026-08-26 18:05:11 +08:00
parent e317547b8b
commit b7f429db89
93 changed files with 2708 additions and 583 deletions
+110 -57
View File
@@ -10,13 +10,25 @@
import json
import time
from pathlib import Path
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
from graph.validate import with_fallback
RISK_LABEL = {"safe": "✅ 安全", "review": "⚠️ 待复核", "blocked": "⛔ 拦截"}
def _notify_400(on_400, exc) -> None:
"""HTTP 400(且含「内容/图片」)时触发 on_400 回调(供调用方累计放弃计数)。"""
if on_400 is None:
return
try:
from graph.pinterest import is_400_content_image
if is_400_content_image(exc):
on_400()
except Exception: # noqa: BLE001
pass
def _build_briefs_md(briefs: List[Dict[str, Any]], generated_at: str) -> str:
lines = [
"# POD 印花设计简报(LLM 合规筛选 + 生图提示词)",
@@ -96,6 +108,88 @@ def _build_report_md(state: Dict[str, Any]) -> str:
return "\n".join(lines)
def generate_design(ib, brief: Dict[str, Any], design_dir: Path, out_stem: str,
errors: List[Dict[str, Any]] = None,
seed: Optional[int] = None,
on_400=None,
size: str = "1024x1024") -> Optional[str]:
"""生成单张纯印花设计稿(图2)。返回设计稿路径;失败 / 全局 MD5 重复返回 None。
out_stem: 输出文件名主干(不含扩展名),最终文件 = {out_stem}_design.png。
货号模式传 img_code(如 DG000)→ designs/DG000_design.png
旧 compose 模式传 {country}_{idx:02d}(如 JP_01)→ designs/JP_01_design.png。
Pinterest 参考模式:简报带 ref_images(爬取图)→ 用 ib.print() 图生图,
把爬取图 + 多模态分析简报(已封装进 image_prompt)一起发给生图模型;
无参考图或图生图失败 → 回退 ib.generate() 纯文生图。
seed: 随机种子(None=不传,网关随机;固定值=可复现,网关支持才生效)。
on_400: 每次 HTTP 400(且含「内容/图片」)时回调(供调用方累计放弃计数)。
"""
try:
from graph.style_rules import sanitize_image_prompt, ensure_rebrand_hint
img_prompt = sanitize_image_prompt(brief.get("image_prompt", ""))
img_prompt = ensure_rebrand_hint(brief, img_prompt) # review → 原创化魔改引导
out_path = str(design_dir / f"{out_stem}_design.png")
ref_images = [str(p) for p in (brief.get("ref_images") or []) if str(p)]
if ref_images and hasattr(ib, "print"):
try:
# 图生图:以爬取图为参考,按分析简报生成原创设计(不复制原图)
ref_prompt = img_prompt + (
" Create an ORIGINAL, non-copying flat print design inspired ONLY by "
"the reference image's style and mood. Do NOT reproduce the reference "
"image, its characters, logos, or any text.")
out_path = ib.print(
ref_prompt, ref_images[0], out_path,
brief.get("composite_negative", ""),
extra_images=ref_images[1:] or None,
size=size, seed=seed) # 设计稿尺寸按 config compose.design_size
except Exception as e: # noqa: BLE001
print(f"[compose] 图生图(参考图)失败,回退文生图 {brief.get('topic','')}: {e}")
_notify_400(on_400, e)
out_path = ib.generate(
img_prompt, str(design_dir / f"{out_stem}_design.png"),
brief.get("composite_negative", ""), size=size, seed=seed)
else:
out_path = ib.generate(
img_prompt, str(design_dir / f"{out_stem}_design.png"),
brief.get("composite_negative", ""), size=size, seed=seed)
# 全局 MD5 去重:生成了设计后,把 MD5 加入全局过滤(对所有国家生效);
# 已存在的重复设计 → 跳过(不用于产品),避免跨国家重复使用同一设计
from graph.pinterest import design_md5_ok
if not design_md5_ok(out_path):
print(f"[compose] 设计稿 MD5 全局重复,跳过(不用于产品): {out_path}")
return None
return out_path
except Exception as e: # noqa: BLE001
_notify_400(on_400, e)
if errors is not None:
errors.append({"node": "compose", "type": type(e).__name__,
"message": f"设计稿生成失败 {brief.get('topic','')}: {e}", "trace": ""})
print(f"[compose] 设计稿生成失败 {brief.get('topic', '')}: {e}")
return None
def write_compose_reports(state: Dict[str, Any], briefs: List[Dict[str, Any]]) -> None:
"""写 compose 阶段简报报告(design_briefs / composite_prompts / report.md)。
Pinterest 并发生成模式下 compose_node 不再整体执行,由收尾节点调用本函数补写报告。
"""
output_dir = Path(state["output_dir"])
output_dir.mkdir(parents=True, exist_ok=True)
cache_dir = Path(state.get("cache_dir") or output_dir)
generated_at = time.strftime("%Y-%m-%dT%H:%M:%S")
(cache_dir / "design_briefs.json").write_text(
json.dumps({"generated_at": generated_at, "total": len(briefs), "design_briefs": briefs},
ensure_ascii=False, indent=2), encoding="utf-8")
(cache_dir / "design_briefs.md").write_text(
_build_briefs_md(briefs, generated_at), encoding="utf-8")
(cache_dir / "composite_prompts.json").write_text(
json.dumps({"generated_at": generated_at, "total": len(briefs), "composite_prompts": briefs},
ensure_ascii=False, indent=2), encoding="utf-8")
(cache_dir / "composite_prompts.md").write_text(
_build_composite_md(briefs), encoding="utf-8")
(output_dir / "report.md").write_text(_build_report_md(state), encoding="utf-8")
@with_fallback("compose")
def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
briefs: List[Dict[str, Any]] = state.get("briefs") or []
@@ -105,26 +199,8 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
config = state["config"]
country = state.get("country", "")
generated_at = time.strftime("%Y-%m-%dT%H:%M:%S")
# 1) design_briefs.json(缓存 → 根目录,不进时间戳任务文件夹)
(cache_dir / "design_briefs.json").write_text(
json.dumps({"generated_at": generated_at, "total": len(briefs), "design_briefs": briefs},
ensure_ascii=False, indent=2), encoding="utf-8")
# 2) design_briefs.md
(cache_dir / "design_briefs.md").write_text(
_build_briefs_md(briefs, generated_at), encoding="utf-8")
# 3) composite_prompts.json / .md
(cache_dir / "composite_prompts.json").write_text(
json.dumps({"generated_at": generated_at, "total": len(briefs), "composite_prompts": briefs},
ensure_ascii=False, indent=2), encoding="utf-8")
(cache_dir / "composite_prompts.md").write_text(
_build_composite_md(briefs), encoding="utf-8")
# 4) report.md(本次任务报告 → 产物目录)
(output_dir / "report.md").write_text(_build_report_md(state), encoding="utf-8")
# 1-4) 简报报告(design_briefs / composite_prompts / report.md
write_compose_reports(state, briefs)
# 5) 生成纯印花设计稿(图2):前 N 个 safe 简报用 image_prompt 文生图
designs: List[Dict[str, Any]] = []
@@ -153,45 +229,20 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
design_dir = output_dir / "designs"
design_dir.mkdir(exist_ok=True)
from graph.style_rules import sanitize_image_prompt, ensure_rebrand_hint
from concurrent.futures import ThreadPoolExecutor, as_completed
def _gen_one(i: int, b: Dict[str, Any]):
"""单张设计稿生成(并发线程内调用,每设计一线程)。
# 随机种子:config compose.seed >0 时固定(可复现,网关支持才生效);0/留空=每次随机
_seed = int(compose_cfg.get("seed") or 0)
_seed = _seed if _seed > 0 else None
Pinterest 参考模式:简报带 ref_images(爬取图)→ 用 ib.print() 图生图,
把爬取图 + 多模态分析简报(已封装进 image_prompt)一起发给生图模型;
无参考图或图生图失败 → 回退 ib.generate() 纯文生图。
"""
try:
img_prompt = sanitize_image_prompt(b.get("image_prompt", ""))
img_prompt = ensure_rebrand_hint(b, img_prompt) # review → 原创化魔改引导
out_path = str(design_dir / f"{country}_{i:02d}_design.png")
ref_images = [str(p) for p in (b.get("ref_images") or []) if str(p)]
if ref_images and hasattr(ib, "print"):
try:
# 图生图:以爬取图为参考,按分析简报生成原创设计(不复制原图)
ref_prompt = img_prompt + (
" Create an ORIGINAL, non-copying flat print design inspired ONLY by "
"the reference image's style and mood. Do NOT reproduce the reference "
"image, its characters, logos, or any text.")
out_path = ib.print(
ref_prompt, ref_images[0], out_path,
b.get("composite_negative", ""),
extra_images=ref_images[1:] or None,
size="1024x1024") # 印花设计统一 1024x1024
except Exception as e: # noqa: BLE001
print(f"[compose] 图生图(参考图)失败,回退文生图 {b.get('topic','')}: {e}")
out_path = ib.generate(
img_prompt, str(design_dir / f"{country}_{i:02d}_design.png"),
b.get("composite_negative", ""), size="1024x1024")
else:
out_path = ib.generate(
img_prompt, str(design_dir / f"{country}_{i:02d}_design.png"),
b.get("composite_negative", ""), size="1024x1024")
return i, b, out_path, None
except Exception as e: # noqa: BLE001
return i, b, None, e
def _gen_one(i: int, b: Dict[str, Any]):
"""单张设计稿生成(并发线程内调用,每设计一线程)。"""
out_path = generate_design(ib, b, design_dir, f"{country}_{i:02d}",
state.get("errors"), seed=_seed,
size=compose_cfg.get("design_size", "1024x1024"))
if out_path is None:
return i, b, None, None
return i, b, out_path, None
targets = [(i, b) for i, b in enumerate(safe_briefs[:design_count], 1)]
# 并发生成:每张设计一个线程(并行调图像网关),数量多时不串行等待
@@ -206,6 +257,8 @@ def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
state.setdefault("errors", []).append({
"node": "compose", "type": type(err).__name__,
"message": f"设计稿生成失败 {b.get('topic','')}: {err}", "trace": ""})
elif out_path is None:
print(f"[compose] 设计稿跳过(MD5 全局去重): {b.get('topic', '')}")
else:
b["design_path"] = out_path
designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": out_path})