POD 趋势感知 Agent:缓存热点模式 + 三图合成 + 热点去重/风格去重 + review 兜底
- 缓存热点批量流程(有采集缓存不触发 Google) - 简报不足直接从采集缓存生成(轻量补齐) - 三图合成(模特/印花/底图)+ 底图压缩 <2MB - 热点去重→风格去重自动切换 + 不适合类目 review 兜底 - 透明背景(background=transparent)+ 提示词清洗(敏感词/背景描述) - 任务前 basemap 校验 + 模板国家校验 + 模特任务级分配
This commit is contained in:
@@ -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']}")
|
||||
@@ -0,0 +1,149 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""用 v3 国家化纯印花模板重新生成各国提示词产物。
|
||||
|
||||
- 模板来源:config.yaml 的 prompt_templates(countries.<CODE> 按国家覆盖,resolve_templates 解析),
|
||||
与流水线 prompt_node 完全一致,保证产物=实际生图提示词。
|
||||
- 数据源优先 output/<c>/llm_verified_prompts.json(含 verdict),
|
||||
否则 output/<c>/design_briefs.json(risk_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()
|
||||
@@ -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_batch:tasks=[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])
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""真实 API 全链路验证:JP JPTM001 黑色 × 1(oss 真上传)。"""
|
||||
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 []])
|
||||
Reference in New Issue
Block a user