Files
pod_trend_agent/graph/nodes/compose_node.py
T
3218485270 f493bde8a9 POD 趋势感知 Agent:缓存热点模式 + 三图合成 + 热点去重/风格去重 + review 兜底
- 缓存热点批量流程(有采集缓存不触发 Google)
- 简报不足直接从采集缓存生成(轻量补齐)
- 三图合成(模特/印花/底图)+ 底图压缩 <2MB
- 热点去重→风格去重自动切换 + 不适合类目 review 兜底
- 透明背景(background=transparent)+ 提示词清洗(敏感词/背景描述)
- 任务前 basemap 校验 + 模板国家校验 + 模特任务级分配
2026-08-22 14:14:01 +08:00

195 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""节点 5.5/6:生成印花设计稿 + 导出简报包(compose)。
流程位置:prompt_build → compose → productcompose 在 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.backendopenai/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 []}