- 自定义模式:分析模型输出 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=运动
- 童装特征库更新 + 标题模板外部化 + 图源映射增强
180 lines
10 KiB
Python
180 lines
10 KiB
Python
"""节点 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}
|