POD 趋势感知 Agent:缓存热点模式 + 三图合成 + 热点去重/风格去重 + review 兜底

- 缓存热点批量流程(有采集缓存不触发 Google)
- 简报不足直接从采集缓存生成(轻量补齐)
- 三图合成(模特/印花/底图)+ 底图压缩 <2MB
- 热点去重→风格去重自动切换 + 不适合类目 review 兜底
- 透明背景(background=transparent)+ 提示词清洗(敏感词/背景描述)
- 任务前 basemap 校验 + 模板国家校验 + 模特任务级分配
This commit is contained in:
2026-08-22 14:14:01 +08:00
commit f493bde8a9
98 changed files with 10280 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
"""用墨西哥主题简报装配 MX 提示词产物(Google Trends 429 限流期间离线验证模板效果)。
构造 5 个墨西哥风格 safe 简报 → prompt_node 用 MX 模板装配 image_prompt →
输出 output/MX/pure_print_prompts.{json,md}(结构同 regen 脚本)。
"""
import json
import sys
import datetime
from pathlib import Path
BASE = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(BASE))
import yaml
from graph.nodes.prompt_node import prompt_node
MX_BRIEFS = [
{"topic": "dia de los muertos catrina", "design_category": "Style",
"motif": "elegant catrina sugar skull with marigold flower crown",
"art_style": "vibrant sugar-skull folk art illustration",
"color_palette": "marigold orange, magenta, deep purple, black, gold",
"composition": "centered portrait emblem with floral frame", "risk_level": "safe"},
{"topic": "loteria mexicana cards", "design_category": "Niche",
"motif": "retro loteria card with el corazon symbol",
"art_style": "retro loteria card illustration with bold frame",
"color_palette": "cream, crimson, teal, gold",
"composition": "card-style centered layout with banner", "risk_level": "safe"},
{"topic": "aztec calendar sun", "design_category": "Pattern",
"motif": "aztec sun calendar geometric emblem",
"art_style": "aztec geometric pattern emblem",
"color_palette": "jade green, terracotta, obsidian black, gold",
"composition": "concentric circular sun emblem", "risk_level": "safe"},
{"topic": "taco fiesta food", "design_category": "Niche",
"motif": "happy taco with avocado and chili peppers",
"art_style": "playful colorful mexican food icon illustration",
"color_palette": "cactus green, chili red, corn yellow, avocado green",
"composition": "centered food icon with border", "risk_level": "safe"},
{"topic": "lucha libre luchador mask", "design_category": "Style",
"motif": "luchador wrestler mask with stars",
"art_style": "bold retro lucha libre poster graphic",
"color_palette": "bright red, electric blue, gold, black",
"composition": "centered mask emblem with rays", "risk_level": "safe"},
{"topic": "talavera ceramic tile", "design_category": "Pattern",
"motif": "talavera blue ceramic tile floral motif",
"art_style": "talavera ceramic pattern style",
"color_palette": "cobalt blue, white, yellow, green",
"composition": "repeating tile pattern with central medallion", "risk_level": "safe"},
{"topic": "monarch butterfly migration", "design_category": "Style",
"motif": "monarch butterfly among marigold flowers",
"art_style": "delicate monarch butterfly folk pattern",
"color_palette": "burnt orange, black, white, gold",
"composition": "centered butterfly with floral border", "risk_level": "safe"},
{"topic": "charro horse rider", "design_category": "Style",
"motif": "charro cowboy with sombrero on horseback",
"art_style": "bold charro cowboy folk illustration",
"color_palette": "black, silver, red, gold",
"composition": "centered rider emblem with banner", "risk_level": "safe"},
]
config = yaml.safe_load(open(BASE / "config.yaml", encoding="utf-8"))
mx_cfg = yaml.safe_load(open(BASE / "configs" / "countries" / "MX.yaml", encoding="utf-8"))
state = {
"country": "MX", "config": config, "country_config": mx_cfg,
"screened": MX_BRIEFS, "stats": {},
}
out = prompt_node(state)
briefs = out["briefs"]
out_dir = BASE / "output" / "MX"
out_dir.mkdir(parents=True, exist_ok=True)
result = {
"country": "MX", "country_name": "墨西哥",
"template": "pure_print_15x18cm_to_26x32cm_v3",
"template_source": "config.yaml prompt_templates.countries.MX",
"generated_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"total": len(briefs), "safe": len(briefs), "review": 0,
"note": "离线演示产物(Google Trends 429 限流期间用墨西哥主题简报装配);正式数据需等限流恢复后跑流水线",
"items": [
{"topic": b["topic"], "category": b.get("design_category"), "verdict": "safe",
"motif": b["motif"], "art_style": b["art_style"],
"color_palette": b["color_palette"], "composition": b["composition"],
"pure_print_prompt": b["image_prompt"]}
for b in briefs
],
}
(out_dir / "pure_print_prompts.json").write_text(
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
lines = [
"# 墨西哥(MX)纯印花设计提示词产物(15×18cm ~ 26×32cm · v3 国家化模板)\n",
f"- 生成时间:{result['generated_at']}",
"- 模板来源:config.yaml → prompt_templates.countries.MX",
f"- 说明:{result['note']}",
"- 规则:尺寸 **约 15×18cm ~ 26×32cm 自由选择**;英文/西语短标语可加可不加;**任何文字严禁政治/宗教/仇恨/暴力/性/品牌/商标/真实人物(含 Frida Kahlo)/毒枭等敏感内容**\n",
]
for i, it in enumerate(result["items"], 1):
lines.append(f"## {i}. {it['topic']} [{it['category']}] ✅ 可直接用\n")
lines.append(f"- **motif**{it['motif']}")
lines.append(f"- **art_style**{it['art_style']}")
lines.append(f"- **color_palette**{it['color_palette']}")
lines.append(f"- **composition**{it['composition']}\n")
lines.append("```text")
lines.append(it["pure_print_prompt"])
lines.append("```\n")
(out_dir / "pure_print_prompts.md").write_text("\n".join(lines), encoding="utf-8")
print(f"[MX] 产物生成完成:{len(briefs)}")
for b in briefs:
print(f" {b['topic']} | MX-market={'MX-market' in b['image_prompt']} | Spanish={'Spanish slogan' in b['image_prompt']}")
+149
View File
@@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
"""用 v3 国家化纯印花模板重新生成各国提示词产物。
- 模板来源:config.yaml 的 prompt_templatescountries.<CODE> 按国家覆盖,resolve_templates 解析),
与流水线 prompt_node 完全一致,保证产物=实际生图提示词。
- 数据源优先 output/<c>/llm_verified_prompts.json(含 verdict),
否则 output/<c>/design_briefs.jsonrisk_level 全 safe 时视作 verdict=safe)。
- 输出:output/<c>/pure_print_prompts.{json,md}
"""
import argparse
import datetime
import json
import os
import sys
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE)
from graph.templates import resolve_templates # noqa: E402
COUNTRY_NAMES = {"US": "美国", "GB": "英国", "JP": "日本", "AU": "澳大利亚"}
STATUS = {"safe": "✅ 可直接用", "review": "⚠️ 需人工复核", "blocked": "🚫 已拦截"}
def load_briefs(country: str):
"""优先 llm_verified_prompts.json,否则 design_briefs.json。"""
verified = os.path.join(BASE, "output", country, "llm_verified_prompts.json")
briefs_path = os.path.join(BASE, "output", country, "design_briefs.json")
if os.path.exists(verified):
with open(verified, "r", encoding="utf-8") as f:
data = json.load(f)
items = data.get("design_briefs", [])
# 归一化 verdict 字段名
for b in items:
if "verdict" not in b and "risk_level" in b:
b["verdict"] = b["risk_level"]
return [b for b in items if b.get("motif")]
if os.path.exists(briefs_path):
with open(briefs_path, "r", encoding="utf-8") as f:
data = json.load(f)
items = data.get("design_briefs", [])
for b in items:
if "verdict" not in b:
b["verdict"] = b.get("risk_level") or ("safe" if b.get("safe_for_print") else "review")
return [b for b in items if b.get("motif")]
return []
def gen_country(country: str):
briefs = load_briefs(country)
if not briefs:
print(f"[{country}] 无简报数据(output/{country}/llm_verified_prompts.json 或 design_briefs.json 缺失/无 motif),跳过")
return False
with open(os.path.join(BASE, "config.yaml"), "r", encoding="utf-8") as f:
import yaml
tpls = yaml.safe_load(f).get("prompt_templates") or {}
t = resolve_templates(tpls, country)
image_tpl = t["image_prompt"]
out_items = []
for b in briefs:
out_items.append({
"topic": b.get("topic"),
"category": b.get("design_category") or b.get("category"),
"score": round(float(b.get("score") or 0), 3),
"verdict": b.get("verdict"),
"compliance_note": b.get("compliance_note") or b.get("concept") or b.get("risk_reasons"),
"motif": b.get("motif"),
"art_style": b.get("art_style"),
"color_palette": b.get("color_palette"),
"composition": b.get("composition"),
"pure_print_prompt": image_tpl.format(
motif=b.get("motif"), art_style=b.get("art_style"),
color_palette=b.get("color_palette"), composition=b.get("composition")),
})
result = {
"country": country,
"country_name": COUNTRY_NAMES.get(country, country),
"template": "pure_print_15x18cm_to_26x32cm_v3",
"template_source": "config.yaml prompt_templates.countries." + country,
"generated_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"total": len(out_items),
"safe": sum(1 for i in out_items if i["verdict"] == "safe"),
"review": sum(1 for i in out_items if i["verdict"] in ("review", "blocked")),
"items": out_items,
}
out_dir = os.path.join(BASE, "output", country)
os.makedirs(out_dir, exist_ok=True)
json_path = os.path.join(out_dir, "pure_print_prompts.json")
with open(json_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
lines = [
f"# {result['country_name']}{country})纯印花设计提示词产物(15×18cm ~ 26×32cm · v3 国家化模板)\n",
f"- 生成时间:{result['generated_at']}",
f"- 模板版本:`pure_print_15x18cm_to_26x32cm_v3`,来源:config.yaml → prompt_templates.countries.{country}",
"- 规则:尺寸在**约 15×18cm ~ 26×32cm 区间内自由选择**(防默认出满幅大图,不拉伸、不铺满、留边距);英文**可加可不加**,与印花适配即可;**任何文字严禁政治/宗教/仇恨/暴力/性/品牌/商标/真实人物等敏感内容**",
f"- 总数 {result['total']} 条:✅ safe {result['safe']} 条(可直接生成) / ⚠️ review {result['review']} 条(需人工复核)\n",
]
for i, it in enumerate(result["items"], 1):
verdict = it["verdict"]
tag = STATUS.get(verdict, verdict)
lines.append(f"## {i}. {it['topic']} [{it['category'] or 'N/A'}] (score {it['score']}) {tag}\n")
note = it["compliance_note"]
lines.append(f"- **合规/说明**{note if note else '(无)'}")
lines.append(f"- **motif**{it['motif']}")
lines.append(f"- **art_style**{it['art_style']}")
lines.append(f"- **color_palette**{it['color_palette']}")
lines.append(f"- **composition**{it['composition']}\n")
lines.append("```text")
lines.append(it["pure_print_prompt"])
lines.append("```\n")
md_path = os.path.join(out_dir, "pure_print_prompts.md")
with open(md_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
# 校验
with open(json_path, "r", encoding="utf-8") as f:
chk = json.load(f)
bad = [i["topic"] for i in chk["items"] if "choose freely between a MINIMUM print area of about 15x18 cm" not in i["pure_print_prompt"]]
print(f"[{country}] 生成完成: {len(chk['items'])} 条 (safe {chk['safe']} / review {chk['review']}) | "
f"新模板未命中: {bad or ''} | 旧模板残留: {sum(1 for i in chk['items'] if 'designed to fit a maximum print area' in i['pure_print_prompt'])}")
return True
def main():
ap = argparse.ArgumentParser()
ap.add_argument("-c", "--country", default="all",
help="国家代码 US/GB/JP/AU,或 all(默认,处理所有有数据的国家)")
args = ap.parse_args()
if args.country == "all":
countries = [c for c in ("US", "GB", "JP", "AU")]
else:
countries = [args.country.upper()]
for cc in countries:
try:
gen_country(cc)
except Exception as e: # noqa: BLE001
print(f"[{cc}] 失败: {e}")
if __name__ == "__main__":
main()
+76
View File
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
"""JP 流程测试:JPTM001 黑色 × 2 个产品(缓存热点模式)。
步骤:
1) 用 mock 种子(6 style + 6 related)生成 JP 主题简报(JP-market 模板装配)→ 写入 output/JP/design_briefs.json 作为缓存
2) run_product_batchtasks=[JPTM001-BL01 × 2]count=2 → 2 个产品各绑一个热点
3) 验证:选色/热点分配/used 去重/JP 标题模板路由/模板导出
"""
import json
import sys
import time
from pathlib import Path
BASE = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(BASE))
import yaml
from graph.templates import assemble_prompts
config = yaml.safe_load(open(BASE / "config.yaml", encoding="utf-8"))
# 种子词 6 个
config["seed_provider"] = "mock"
cfg = config.setdefault("seed_provider_cfg", {})
cfg["max_style_seeds"] = 6
cfg["max_related_seeds"] = 6
# 图像后端 mock、不上传图床
config["compose"]["backend"] = "mock"
config["product"]["backend"] = "mock"
config["oss"]["enabled"] = False
config["seed_shot"]["count"] = 1
jp_topics = [
("kawaii cat cafe", "Style", "original kawaii cat with latte art", "kawaii style, soft rounded shapes", "pink, cream, caramel", "centered mascot"),
("tokyo neon night", "Style", "original neon street lamps silhouette", "retro cyberpunk flat vector", "magenta, cyan, black", "symmetrical emblem"),
("shiba inu summer", "Style", "original shiba inu with watermelon slice", "cute japanese mascot illustration", "orange, red, white", "centered"),
("sakura spring", "Style", "original cherry blossom branch with petals", "soft minimal japanese print", "sakura pink, white, mint", "corner flourish"),
("osaka takoyaki food", "Style", "original takoyaki balls with bonito flakes", "playful food illustration", "golden brown, red, green", "centered badge"),
("mt fuji sunrise", "Style", "original mt fuji with rising sun rays", "ukiyo-e inspired flat design", "indigo, red, cream", "symmetrical"),
]
tpls = config.get("prompt_templates") or {}
briefs = []
for topic, cat, motif, style, palette, comp in jp_topics:
prompts = assemble_prompts(motif, style, palette, comp, tpls, "JP")
briefs.append({
"country": "JP", "topic": topic, "design_category": cat, "risk_level": "safe",
"score": 80 - len(briefs) * 3, "motif": motif, "art_style": style,
"color_palette": palette, "composition": comp,
"image_prompt": prompts["image_prompt"],
"composite_prompt": prompts["composite_prompt"],
"composite_negative": prompts["composite_negative"],
})
out_dir = BASE / "output" / "JP"
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "design_briefs.json").write_text(
json.dumps({"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S"), "total": len(briefs),
"design_briefs": briefs}, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[setup] JP 缓存热点已写入: {len(briefs)} 条(mock 种子 6/6")
p0 = briefs[0]["image_prompt"]
print("[setup] 模板校验: JP-market 风格段 =", "JP-market" in p0, "| 尺寸上限 26x32 =", "26x32" in p0)
from graph.product_batch import run_product_batch
tasks = [{"spu": "JPTM001", "skus": "JPTM001-BL01"}, # 黑色
{"spu": "JPTM001", "skus": "JPTM001-BL01"}] # 数量 2:同一款号生成 2 个产品
out = run_product_batch("JP", config, BASE, BASE, tasks, 2, log_q=None)
print("\n=== 测试结果 ===")
prods = out.get("product") or []
print("产品数:", len(prods))
for r in prods:
print(f" {r.get('spu_code')} | {r.get('sku_code')} | 色={r.get('color')} | topic={r.get('topic')} | "
f"template={bool(r.get('template_path'))} | design={bool(r.get('design_path'))}")
print("errors:", [e.get("message", "")[:80] for e in out.get("errors") or []])
used = json.loads((out_dir / "used_designs.json").read_text(encoding="utf-8")).get("used", []) if (out_dir / "used_designs.json").exists() else []
print("used_designs 记录:", len(used), "条 →", [u.get("topic") for u in used])
+32
View File
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
"""真实 API 全链路验证:JP JPTM001 黑色 × 1oss 真上传)。"""
import json
import sys
from pathlib import Path
BASE = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(BASE))
import yaml
from graph.product_batch import run_product_batch
config = yaml.safe_load(open(BASE / "dist_v2" / "config.yaml", encoding="utf-8"))
config["seed_provider"] = "openai"
config["llm_screen"]["provider"] = "openai"
config["compose"]["backend"] = "openai"
config["product"]["backend"] = "openai"
config["oss"]["enabled"] = True
config["seed_shot"]["count"] = 1
print("使用配置: llm_model =", config["llm_screen"].get("model"), "| img_model =", config["compose"].get("model"))
out = run_product_batch("JP", config, BASE, BASE / "dist_v2",
[{"spu": "JPTM001", "skus": "JPTM001-BL01"}], 1, log_q=None)
print("\n=== 结果 ===")
prods = out.get("product") or []
print("产品数:", len(prods))
for r in prods:
print(f" {r.get('spu_code')} | {r.get('sku_code')} | design={bool(r.get('design_path'))} "
f"| composite={bool(r.get('composite_path'))} | en_title={bool(r.get('en_title'))} "
f"| ja_title={bool(r.get('ja_title'))} | oss_code={r.get('oss_code')}")
print(" URLs:", {k: v[:60] for k, v in r.items() if k.endswith("_url")})
print("errors:", [e.get("message", "")[:100] for e in out.get("errors") or []])