Files
pod_trend_agent/graph/nodes/prompt_node.py
T
3218485270 5ab5cf6586 v110-v112 自定义模式完善 + 模板导出增强 + 多模态兼容优化
- 自定义模式:分析模型输出 delta 唯一改动指令,生图模板 custom_image_prompt.md({delta} 占位符),不再使用负向提示词;generate_design 按 custom_mode 分支,Pinterest 模式保留原创化指令,两模式互不影响
- 多模态分析 response_format 三级回退(json_schema → json_object → none),兼容 DeepSeek
- 模板导出:details 扩展列(细节1/2/3)、target_audience 扩展列(适用人群1)、固定值风格1=休闲/风格2=运动
- 童装特征库更新 + 标题模板外部化 + 图源映射增强
2026-09-03 18:28:39 +08:00

180 lines
10 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/6:提示词构造(prompt_build)。
读取 prompts/<country>/ 的 extra 风格规则,用固定模板装配四种最终提示词
image_prompt / wearable_prompt / composite_prompt / composite_negative)。
四要素缺失时用 derive_style_palette 动态兜底,保证每条提示词结构一致、有规则。
"""
from typing import Any, Dict, List
import random
from graph.style_rules import derive_style_palette, derive_composition
from graph.templates import assemble_prompts
from graph.validate import validate_brief, with_fallback
# —— Pinterest 图生图生最终设计稿时统一追加的「小印花 + 纯白底」约束段 ——
# (从热点搜集的文字生图模板里提炼:尺寸缩小、禁止自带背景/满幅)
PINTEREST_PRINT_SUFFIX = (
" standalone pure print design on a pure white background, "
"the print artwork is SMALL and CENTERED with clearly larger white margins around it, "
"print area between about 15x18 cm and 26x32 cm, "
"do NOT fill the entire canvas, do NOT force full-bleed, "
"do NOT add any gradient, texture or background color behind the artwork, "
"no garment, no shirt, no model, no mannequin, no watermark"
)
# Pinterest 生图提示词 3 段结构中第 2 段(仅当设计含文字时追加):
# 要求模型把引号内的文字按原文逐字正确拼写,避免乱码/拼错
SPELLING_RULE = (
"Render every phrase shown in quotes exactly as written, "
"correctly spelled."
)
# review(疑似商标/受保护主题)简报统一追加的「原创化魔改」引导段:
# 只做风格参考,禁复刻品牌/商标/角色,换名换细节,生成通用非侵权致敬式设计。
# 两个模式(热点采集 / Pinterest 参考)共用同一文本,避免不一致。
REVIEW_REBRAND_HINT = (
"IMPORTANT: this theme is ONLY a loose stylistic reference. "
"Do NOT reproduce any brand logo, trademark, character, mascot, copyrighted artwork or real person. "
"Create a fully ORIGINAL design with a different name and distinct visual details and colors — "
"a generic, non-infringing homage in the same mood, clearly distinct from the original."
)
# —— 自定义模式(custom)生图模板:固定前缀 + 分析模型 image_prompt + delta 改动指令 ——
# 自定义模式分析模型产出的是「新设计描述 + 唯一改动指令 delta」,生图时套用这套固定模板
# (含防复制/防服装约束),不再使用负向提示词——约束已全部内置进模板。
CUSTOM_IMAGE_PROMPT_TEMPLATE = (
"Use the attached bestseller product photo only as loose inspiration for "
"overall mood, theme, era and style genre — do NOT reproduce, trace, "
"rearrange, recolor or closely imitate its printed artwork, characters, "
"layout or text, and do NOT render a shirt, garment, model, hanger, photo "
"scene, product mockup or background of any kind. First, carefully examine "
"the printed artwork on the garment in the attached photo: zoom in mentally "
"on the print area, identify its subject, technique, palette and layout, and "
"base the new design on THOSE observed traits. Ignore the model, background "
"and photo quality. Then: {image_prompt}"
)
# 图像生成策略敏感词 → 安全等效描述(生成设计稿前清洗 motif,
# 避免 gpt-image 等内容策略频繁拦截导致"生图限制多")
_IMG_RISKY_SWAP = {
"skull": "smiley mascot", "skeleton": "cute mascot", "blood": "red accents",
"gore": "bold shapes", "gun": "star", "weapon": "tool", "bomb": "firework",
"drug": "confetti", "demon": "cute monster", "devil": "mischievous imp",
"occult": "mystic pattern", "satanic": "dark pattern", "nazi": "retro emblem",
"hitler": "retro emblem", "zombie": "friendly ghoul", "horror": "spooky-cute",
"vampire": "night owl", "politics": "abstract shapes", "political": "abstract",
"president": "captain", "army": "team", "police": "officer",
}
def _safe_motif(motif: str) -> str:
"""清洗 motif 中的图像策略敏感词(替换为安全等效描述),降低生图内容政策拦截率。"""
low = motif.lower()
for k, v in _IMG_RISKY_SWAP.items():
if k in low:
# 按词边界替换(避免误伤 "letterhead" 等)
import re
motif = re.sub(rf"\b{re.escape(k)}\b", v, motif, flags=re.IGNORECASE)
low = motif.lower()
return motif
def _read_custom_prompt_md(filename: str) -> str:
"""读取 prompts/<filename>(自定义模式生图模板),优先运行根 exe 旁、回退数据根;无/空返回空串。"""
from graph.paths import project_root, runtime_root
for base in (runtime_root(), project_root()):
p = base / "prompts" / filename
if p.exists():
t = p.read_text(encoding="utf-8").strip()
if t:
return t
return ""
@with_fallback("prompt_build")
def prompt_node(state: Dict[str, Any]) -> Dict[str, Any]:
screened: List[Dict[str, Any]] = state.get("screened") or []
config = state["config"]
country = state["country"]
cc = state["country_config"]
extra_rules = cc.get("extra_style_rules") or []
tpls = config.get("prompt_templates") or {}
# Pinterest 生图提示词固定段:可配置(config.pinterest.prompt_pieces),留空/缺失回退内置常量。
# 自定义模式(custom_mode)用独立的一套固定段(config.custom.prompt_pieces),默认与 Pinterest 相同、可单独编辑。
if bool(state.get("custom_mode")):
pp = (config.get("custom") or {}).get("prompt_pieces") or {}
else:
pp = (config.get("pinterest") or {}).get("prompt_pieces") or {}
print_suffix = (pp.get("print_suffix") or "").strip() or PINTEREST_PRINT_SUFFIX
spelling_rule = (pp.get("spelling_rule") or "").strip() or SPELLING_RULE
review_rebrand_hint = (pp.get("review_rebrand_hint") or "").strip() or REVIEW_REBRAND_HINT
# 自定义模式生图模板优先读 prompts/custom_image_prompt.md(可编辑),
# 其次 config.custom.prompt_pieces,最后回退代码内置默认。
# 自定义模式不再使用负向提示词(约束已内置进模板),不再读取 custom_negative_prompt.md。
custom_ip_tpl = _read_custom_prompt_md("custom_image_prompt.md") \
or (pp.get("image_prompt_template") or "").strip() or CUSTOM_IMAGE_PROMPT_TEMPLATE
briefs: List[Dict[str, Any]] = []
for r in screened:
r = validate_brief(r)
art, pal = derive_style_palette(
r["topic"], country, extra_rules=extra_rules, category=r.get("design_category")
)
motif = (r.get("motif") or "").strip() or r.get("topic", "")
cleaned = _safe_motif(motif)
if cleaned != motif:
print(f"[prompt] motif 敏感词清洗: 「{motif}」→「{cleaned}」(降低生图内容政策拦截)")
r["motif"] = cleaned
motif = cleaned
art_style = (r.get("art_style") or art).strip()
palette = (r.get("color_palette") or pal).strip()
composition = (r.get("composition") or derive_composition(r["topic"], r.get("design_category"))).strip()
prompts = assemble_prompts(motif, art_style, palette, composition, tpls, country)
llm_ip = (r.get("image_prompt") or "").strip()
delta = (r.get("delta") or "").strip()
if r.get("source") == "pinterest":
if bool(state.get("custom_mode")):
# —— 自定义模式:固定模板(前缀 + 分析 image_prompt + delta 改动指令),负向留空 ——
# 新分析模板只产出 delta(无 image_prompt),生图模板也只引用 {delta};
# 只要 delta 或 image_prompt 任一存在即可装配,不再使用负向提示词
# (约束已全部内置进模板),composite_negative 置空避免后端追加 Negative 段。
if llm_ip or delta:
prompts["image_prompt"] = (
custom_ip_tpl.replace("{delta}", delta).replace("{image_prompt}", llm_ip)
)
prompts["composite_negative"] = ""
print(f"[prompt] 自定义模式按固定模板拼 image_prompt(含 delta 改动指令,无负向): 「{r['topic']}」")
elif llm_ip:
# —— Pinterest 参考模式:跳过四要素模板,按 3 段结构拼 image_prompt ——
# ① image_prompt(分析模型产出) + 固定输出形态后缀 PINTEREST_PRINT_SUFFIX
# ② 拼写锁定句 SPELLING_RULE(仅当 LLM image_prompt 已含引号文字段时)
# 是否含文字、拼写与否均由分析模型产出决定,本模式不注入 slogan。
seg: List[str] = [llm_ip, print_suffix]
if '"' in llm_ip:
seg.append(spelling_rule)
prompts["image_prompt"] = ", ".join(seg)
print(f"[prompt] Pinterest 简报按 3 段结构拼 image_prompt(跳过四要素模板): 「{r['topic']}」")
else:
# —— 热点采集模式:四要素模板装配 + 文字印花(约 30% 概率注入 slogan)——
slogan = (r.get("slogan") or "").strip()
if slogan and random.random() < float(config.get("prompt_templates", {}).get("text_ratio", 0.3)):
text_seg = (f', with the text "{slogan}" rendered as bold retro typography, '
f'lettering clean and correctly spelled, high contrast, as the focal text of the print')
prompts["image_prompt"] = prompts["image_prompt"] + text_seg
r["used_slogan"] = slogan
# review(疑似商标/受保护主题)→ 追加「原创化魔改」引导(两个模式通用)
if str(r.get("risk_level", "")).strip().lower() == "review":
prompts["image_prompt"] = prompts["image_prompt"] + " " + review_rebrand_hint
print(f"[prompt] review 简报注入原创化魔改引导: 「{r['topic']}」")
r.update(prompts)
r["motif"] = motif
r["art_style"] = art_style
r["color_palette"] = palette
r["composition"] = composition
briefs.append(r)
stats = dict(state.get("stats") or {})
stats["prompt"] = {"briefs": len(briefs)}
return {"briefs": briefs, "stats": stats}