POD 趋势感知 Agent:缓存热点模式 + 三图合成 + 热点去重/风格去重 + review 兜底
- 缓存热点批量流程(有采集缓存不触发 Google) - 简报不足直接从采集缓存生成(轻量补齐) - 三图合成(模特/印花/底图)+ 底图压缩 <2MB - 热点去重→风格去重自动切换 + 不适合类目 review 兜底 - 透明背景(background=transparent)+ 提示词清洗(敏感词/背景描述) - 任务前 basemap 校验 + 模板国家校验 + 模特任务级分配
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
"""节点 5.5/6:生成印花设计稿 + 导出简报包(compose)。
|
||||
|
||||
流程位置:prompt_build → compose → product(compose 在 product 之前)。
|
||||
职责��
|
||||
1. 生成纯印花设计稿:对前 N 个 safe 简报(N=config.compose.design_count,默认 1),
|
||||
用 image_prompt 调图像后端 generate()(白底、可直接打印),产物存 output/<country>/designs/,
|
||||
设计稿路径写回 brief.design_path,并汇总返回 designs 列表供 product 节点使用(图2)。
|
||||
2. 导出简报包:design_briefs.json/md、composite_prompts.json/md、report.md。
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from graph.validate import with_fallback
|
||||
|
||||
RISK_LABEL = {"safe": "✅ 安全", "review": "⚠️ 待复核", "blocked": "⛔ 拦截"}
|
||||
|
||||
|
||||
def _build_briefs_md(briefs: List[Dict[str, Any]], generated_at: str) -> str:
|
||||
lines = [
|
||||
"# POD 印花设计简报(LLM 合规筛选 + 生图提示词)",
|
||||
"",
|
||||
f"- 生成时间: {generated_at}",
|
||||
f"- 通过筛选: {len(briefs)} 条",
|
||||
"",
|
||||
"## 一、安全设计清单(按综合分排序)",
|
||||
"",
|
||||
"| 排名 | 国家 | 热点词 | 类别 | 风险 | 设计概念 |",
|
||||
"|---|---|---|---|---|---|",
|
||||
]
|
||||
for i, r in enumerate(briefs, 1):
|
||||
flag = RISK_LABEL.get(r.get("risk_level"), r.get("risk_level"))
|
||||
lines.append(
|
||||
f"| {i} | {r.get('country','')} | {r.get('topic','')} | {r.get('design_category','')} | {flag} | {r.get('concept','')} |"
|
||||
)
|
||||
lines += ["", "## 二、设计要素 + 封装提示词", ""]
|
||||
lines.append("> 工作流:① `image_prompt` = 印花设计稿(白底,单独生图);② 上传平铺衣服底图(图1)后,")
|
||||
lines.append("> 用 `composite_prompt` + 图1 经 img2img 把设计印到衣服;规则写死:保留衣服、胸前居中印花、真实丝网质感。")
|
||||
lines.append("")
|
||||
for i, r in enumerate(briefs, 1):
|
||||
flag = RISK_LABEL.get(r.get("risk_level"), r.get("risk_level"))
|
||||
lines.append(f"### {i}. [{r.get('country','')}] {r.get('topic','')} ({flag})")
|
||||
lines.append(f"- 类别: {r.get('design_category','')}")
|
||||
lines.append(f"- 设计要素: 主体=「{r.get('motif','')}」 | 风格=「{r.get('art_style','')}」 | 配色=「{r.get('color_palette','')}」 | 构图=「{r.get('composition','')}」")
|
||||
lines.append(f"- 概念: {r.get('concept','')}")
|
||||
if r.get("risk_reasons"):
|
||||
lines.append(f"- 风险提示: {'; '.join(r['risk_reasons'])}")
|
||||
if r.get("design_path"):
|
||||
lines.append(f"- **设计稿**: {r['design_path']}")
|
||||
lines.append(f"- **设计稿 Prompt (image_prompt)**: {r.get('image_prompt','')}")
|
||||
lines.append(f"- **印到底图 Prompt (composite_prompt)**: {r.get('composite_prompt','')}")
|
||||
lines.append(f"- **Composite Negative**: {r.get('composite_negative','')}")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _build_composite_md(briefs: List[Dict[str, Any]]) -> str:
|
||||
lines = [
|
||||
"# 封装提示词包(印到平铺衣服底图 图1)",
|
||||
"",
|
||||
f"- 共 {len(briefs)} 条,每条含 `composite_prompt`(印图指令)+ `composite_negative`。",
|
||||
"- 用法:将你的平铺衣服参考图作为图1,连同 `composite_prompt` 送入任意 img2img / inpaint 模型。",
|
||||
"",
|
||||
]
|
||||
for i, r in enumerate(briefs, 1):
|
||||
lines.append(f"### {i}. [{r.get('country','')}] {r.get('topic','')}")
|
||||
lines.append(f"- composite_prompt: {r.get('composite_prompt','')}")
|
||||
lines.append(f"- composite_negative: {r.get('composite_negative','')}")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _build_report_md(state: Dict[str, Any]) -> str:
|
||||
country = state.get("country", "")
|
||||
stats = state.get("stats") or {}
|
||||
errors = state.get("errors") or []
|
||||
lines = [
|
||||
f"# POD 热点抓取报告 - {country}",
|
||||
"",
|
||||
f"- 生成时间: {time.strftime('%Y-%m-%dT%H:%M:%S')}",
|
||||
"",
|
||||
"## 各阶段统计",
|
||||
"",
|
||||
"| 阶段 | 指标 |",
|
||||
"|---|---|",
|
||||
]
|
||||
for k, v in stats.items():
|
||||
lines.append(f"| {k} | {v} |")
|
||||
lines += ["", "## 兜底错误记录(节点级 fallback 捕获)", ""]
|
||||
if errors:
|
||||
for e in errors:
|
||||
lines.append(f"- [{e.get('node')}] {e.get('type')}: {e.get('message')}")
|
||||
else:
|
||||
lines.append("- 无(全部节点正常)")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@with_fallback("compose")
|
||||
def compose_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
briefs: List[Dict[str, Any]] = state.get("briefs") or []
|
||||
output_dir = Path(state["output_dir"]) # 本次任务产物(时间戳文件夹)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
cache_dir = Path(state.get("cache_dir") or output_dir) # 缓存/去重(根目录)
|
||||
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")
|
||||
|
||||
# 5) 生成纯印花设计稿(图2):前 N 个 safe 简报用 image_prompt 文生图
|
||||
designs: List[Dict[str, Any]] = []
|
||||
compose_cfg = config.get("compose") or {}
|
||||
backend_name = compose_cfg.get("backend", "")
|
||||
ib = None
|
||||
if backend_name:
|
||||
try:
|
||||
from graph.backends import get_image_backend
|
||||
ib = get_image_backend(backend_name)
|
||||
if ib is not None:
|
||||
ib.bind_config(compose_cfg)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"[compose] 图像后端 {backend_name} 不可用: {e}")
|
||||
if ib is None:
|
||||
print("[compose] 未配置 compose.backend(openai/mock),跳过印花设计稿生成。")
|
||||
else:
|
||||
# 设计稿覆盖所有简报(含 review):每个热点一张设计,避免 review 热点无设计
|
||||
# 导致 product 回退生成重复占位图;风险由 assign 层(allow_review)控制是否分配
|
||||
safe_briefs = briefs
|
||||
design_count = int(compose_cfg.get("design_count", 1))
|
||||
# 联动总任务数:每个产品一张设计 → 生成 扩展后 spu_tasks 总数 张设计
|
||||
task_n = int(len((state.get("config") or {}).get("product", {}).get("spu_tasks") or []))
|
||||
if task_n > design_count:
|
||||
design_count = task_n
|
||||
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]):
|
||||
"""单张设计稿生成(并发线程内调用,每设计一线程)。"""
|
||||
try:
|
||||
img_prompt = sanitize_image_prompt(b.get("image_prompt", ""))
|
||||
img_prompt = ensure_rebrand_hint(b, img_prompt) # review → 原创化魔改引导
|
||||
out_path = ib.generate(
|
||||
img_prompt,
|
||||
str(design_dir / f"{country}_{i:02d}_design.png"),
|
||||
b.get("composite_negative", ""),
|
||||
size="1024x1024") # 印花设计统一 1024x1024
|
||||
return i, b, out_path, None
|
||||
except Exception as e: # noqa: BLE001
|
||||
return i, b, None, e
|
||||
|
||||
targets = [(i, b) for i, b in enumerate(safe_briefs[:design_count], 1)]
|
||||
# 并发生成:每张设计一个线程(并行调图像网关),数量多时不串行等待
|
||||
workers = max(1, min(len(targets), int((config.get("compose") or {}).get("design_workers", 5))))
|
||||
print(f"[compose] 并发生成 {len(targets)} 张设计稿({workers} 线程)…")
|
||||
with ThreadPoolExecutor(max_workers=workers) as _ex:
|
||||
_futs = [_ex.submit(_gen_one, i, b) for i, b in targets]
|
||||
for _f in as_completed(_futs):
|
||||
i, b, out_path, err = _f.result()
|
||||
if err is not None:
|
||||
print(f"[compose] 设计稿生成失败 {b.get('topic', '')}: {err}")
|
||||
state.setdefault("errors", []).append({
|
||||
"node": "compose", "type": type(err).__name__,
|
||||
"message": f"设计稿生成失败 {b.get('topic','')}: {err}", "trace": ""})
|
||||
else:
|
||||
b["design_path"] = out_path
|
||||
designs.append({"topic": b.get("topic", ""), "path": out_path, "design_path": out_path})
|
||||
print(f"[compose] 印花设计稿已生成: {out_path}")
|
||||
|
||||
stats = dict(state.get("stats") or {})
|
||||
stats["compose"] = {"written": len(briefs), "designs": len(designs), "output_dir": str(output_dir)}
|
||||
return {"composite": briefs, "designs": designs, "stats": stats,
|
||||
"errors": state.get("errors") or []}
|
||||
Reference in New Issue
Block a user