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

341 lines
17 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.
"""动态风格 / 配色推导(领域知识,可插拔)。
关键:art_style 与 color_palette 不再按国家写死,而是根据「热点词本身语义」动态推导:
① 该国专属 extra 规则(prompts/<country>/aesthetics.yaml,最具体优先)
② 全局关键词规则 STYLE_PALETTE_RULES(覆盖更全、配色更丰富、风格更具体新颖)
③ 确定性风格微调 STYLE_TWISTS(按主题哈希选,同词稳定、跨词不同,打破千篇一律)
④ 按 classify 类别兜底
⑤ 国家基调最末兜底(极少走到)
关键词匹配用「单词边界」,避免 eco 误中 cottagecore、kit 误中 jacket 等子串歧义。
具体规则排在通用 retro/vintage 之前,避免 retro games / retro trainers 被笼统归为复古风。
"""
import hashlib
import re
from typing import Dict, List, Optional, Tuple
# 规则按「优先级」排列:越靠前越具体,命中任一关键词即采用该条(取第一个命中)。
# 关键词匹配为单词边界。配色统一 5 色,艺术风格尽量具体+新颖。
STYLE_PALETTE_RULES: List[Tuple[tuple, str, str]] = [
(("skull", "death", "gothic", "horror", "zombie", "vampire", "spooky", "occult"),
"dark gothic engraving with ornate filigree",
"oxblood red, charcoal black, antique silver, deep purple, bone white"),
(("metal", "rock", "band", "grunge", "punk", "emo", "anarchist"),
"gritty risograph grunge zine",
"muted olive, rust orange, dirty cream, faded black, safety-pin silver"),
(("kawaii", "cute", "chibi", "sanrio"),
"kawaii chibi sticker with thick outlines",
"pastel pink, baby blue, mint, butter yellow, cream"),
(("cat", "kitten"),
"cute cat cartoon with bold outlines",
"warm cream, soft pink, charcoal, peach, sky blue"),
(("dog", "puppy"),
"playful dog cartoon with wagging tail",
"warm brown, cream, navy, tan, rust"),
(("coffee", "cafe", "tea", "latte", "brew"),
"cozy drink line illustration with steam curls",
"espresso brown, cream, caramel, sage, terracotta"),
(("mountain", "nature", "forest", "camping", "hiking", "outdoor", "wilderness", "woodland"),
"minimal outdoor landscape with layered ridgelines",
"forest green, slate gray, sand, cream, pine"),
(("beach", "surf", "ocean", "summer", "tropical", "sun", "seaside"),
"bright tropical screen-print",
"sky blue, coral, sand, sun-bleached white, turquoise"),
(("synthwave", "vaporwave", "retrowave"),
"synthwave neon with grid horizon",
"neon purple, magenta, cyan, deep navy, hot pink"),
(("anime", "manga", "otaku", "waifu"),
"anime-inspired flat cel with speed lines",
"vivid cyan, magenta, white, ink black, lemon"),
(("space", "galaxy", "star", "astro", "cosmic", "moon", "universe", "nebula"),
"cosmic vector with nebula glow",
"deep navy, violet, silver, starlight white, magenta"),
(("heart", "love", "valentine", "romance", "couple"),
"romantic badge with hand-lettered flourish",
"rose red, blush pink, cream, gold, burgundy"),
(("book", "reading", "library", "bookish", "novel"),
"cozy bookish line art with marginalia",
"warm brown, cream, forest green, oxblood, gold"),
(("music", "song", "concert", "festival", "dj", "gig"),
"dynamic gig poster with spotlight beams",
"electric purple, magenta, black, neon lime, silver"),
(("food", "pizza", "burger", "baking", "donut", "taco"),
"appetizing flat food illustration",
"warm red, cheese yellow, leaf green, cream, tomato"),
(("cyber", "cyberpunk", "neon", "tech", "robot", "mecha", "glitch"),
"neon digital with glitch grid",
"neon magenta, cyan, electric purple, black, lime"),
(("steam", "steampunk", "gear", "cog", "airship", "mechanical"),
"detailed mechanical engraving with brass",
"brass, copper, aged brown, sepia, gunmetal"),
(("solar", "eco", "green", "sustainable", "earth", "climate"),
"hopeful solarpunk eco with clean lines",
"leaf green, solar gold, sky blue, terracotta, cream"),
(("cottage", "cottagecore", "pastoral", "farm", "rustic"),
"soft storybook watercolor with wildflowers",
"sage green, butter yellow, dusty rose, cream, moss"),
(("car", "automotive", "vehicle", "classic", "racer"),
"retro automotive poster with chrome sheen",
"cherry red, cream, chrome silver, navy, tan"),
(("game", "gaming", "arcade", "pixel", "8-bit"),
"8-bit pixel-art with scanlines",
"neon green, magenta, cyan, black, yellow"),
(("shoe", "sneaker", "trainer", "footwear", "boots"),
"retro product sneaker illustration",
"white, red, navy, gum-sole tan, court grey"),
(("watch", "clock", "timepiece", "chronograph"),
"elegant engraving with roman numerals",
"antique gold, navy, cream, burgundy, slate"),
(("lion", "shield", "crest", "heraldic", "crown", "queen", "king", "royal"),
"heraldic emblem with original rampant beast",
"royal blue, crimson, gold, cream, navy"),
(("bee", "animal", "wildlife", "bird", "fox", "bear", "rabbit"),
"charming zoological illustration",
"honey gold, charcoal, leaf green, cream, rust"),
(("map", "city", "london", "travel", "trip", "skyline", "landmark"),
"mid-century travel poster with skyline",
"royal red, teal, navy, cream, mustard"),
(("rain", "weather", "cloud", "moody", "storm", "fog"),
"moody rain illustration with droplets",
"slate blue, pewter, cream, oxblood, charcoal"),
(("witch", "magic", "halloween", "wizard", "spell"),
"whimsical witchy illustration with moon",
"deep purple, black, moss green, gold, amethyst"),
(("christmas", "xmas", "santa", "snowflake", "festive"),
"festive christmas with needle-felt texture",
"pine green, berry red, cream, gold, ice blue"),
(("sport", "gym", "football", "soccer", "baseball", "workout", "jersey", "kit", "athletic", "england"),
"bold athletic emblem with motion streaks",
"navy, white, athletic red, silver, volt green"),
(("patriotic", "flag", "america", "freedom", "usa"),
"bold patriotic emblem with stars",
"navy, red, cream, gold, slate"),
(("retro", "80s", "90s", "y2k", "memphis"),
"1980s Memphis pop with geometric confetti",
"faded navy, cream, burnt orange, hot pink, teal"),
(("vintage", "antique", "distressed", "classic", "aged"),
"aged letterpress vintage with halftone",
"faded sepia, cream, muted teal, oxblood, distressed black"),
]
# 预编译单词边界正则,避免 eco 误中 cottagecore、kit 误中 jacket 等子串歧义。
# 每个关键词附加可选复数 (?:s)?,覆盖 games/cats 等复数形态。
def _pattern_from(kws: List[str]) -> "re.Pattern":
return re.compile(r"\b(?:" + "|".join(re.escape(k) + "(?:s)?" for k in kws) + r")\b")
_RULE_PATTERNS = [
(_pattern_from(list(kws)), art, pal)
for kws, art, pal in STYLE_PALETTE_RULES
]
def _kw_pattern(kws: List[str]) -> "re.Pattern":
return _pattern_from(kws)
# 风格微调池:确定性地给每条设计追加一个“技法/质感”修饰,提升新颖度与差异度。
# 选择基于主题的稳定哈希,保证「同词稳定、跨词不同」(不依赖进程随机种子)。
STYLE_TWISTS: List[str] = [
"with subtle risograph grain and slight misregistration",
"with bold halftone dot shading",
"with hand-drawn imperfect ink edges",
"with limited-palette screen-print separation",
"with fine stipple and engraving texture",
"with paper-cut layered depth",
"with art-deco geometric framing",
"with Memphis-style confetti shapes",
"with soft watercolor bleed at the edges",
"with iridescent foil accent",
]
def _stable_hash(text: str) -> int:
"""跨进程稳定的字符串哈希(不依赖 PYTHONHASHSEED)。"""
return int(hashlib.md5(text.encode("utf-8")).hexdigest(), 16)
# 类别兜底:关键词都没命中时,按 classify 类别给一个合理风格/配色(含 5 色)
STYLE_BY_CATEGORY: Dict[str, str] = {
"Event": "festive badge illustration",
"Meme": "bold comic meme illustration",
"Style": "trendy flat vector illustration",
"Niche": "clean modern vector illustration",
"Pattern": "seamless pattern tile illustration",
"Quote": "bold typographic illustration",
}
PALETTE_BY_CATEGORY: Dict[str, str] = {
"Event": "festive multi-color: red, gold, forest green, cream, berry",
"Meme": "bold high-contrast: black, white, pop yellow, magenta, cyan",
"Style": "trendy balanced modern: navy, coral, cream, sage, slate",
"Niche": "versatile balanced: teal, sand, charcoal, blush, white",
"Pattern": "harmonious repeating: terracotta, olive, cream, rust, gold",
"Quote": "high-contrast typographic: ink black, off-white, accent red, gold",
}
# 国家基调最末兜底(代码内置;真正按国家定制走 prompts/<country>/aesthetics.yaml
COUNTRY_AESTHETICS: Dict[str, Dict[str, str]] = {
"US": {
"label": "美国",
"style_hint": "Bold vintage / retro Americana, humorous and punchy, high-contrast poster style.",
"art_style": "bold vintage retro Americana poster, clean vector",
"palette": "muted retro Americana palette: faded navy, cream, burnt orange, distressed black, mustard",
},
"GB": {
"label": "英国",
"style_hint": "Witty, self-deprecating British humor; punk / bold lettering; tea-and-rain mood.",
"art_style": "witty punk zine illustration, bold hand-lettered",
"palette": "punk-zine palette: high-contrast black, off-white, safety-orange, oxblood red, slate",
},
"JP": {
"label": "日本",
"style_hint": "Kawaii / minimalist / anime-inspired; clean lines, Tokyo street edge, original kanji accents.",
"art_style": "kawaii minimalist flat illustration, clean lines",
"palette": "soft kawaii palette: pastel pink, mint, butter yellow, soft lavender, cream",
},
"AU": {
"label": "澳大利亚",
"style_hint": "Sunny, laid-back coastal vibe; surf / beach / BBQ; relaxed and warm.",
"art_style": "sunny laid-back coastal illustration, relaxed",
"palette": "sunny coastal palette: sky blue, sand beige, coral, sun-bleached white, turquoise",
},
}
def derive_style_palette(
topic: str,
country: str,
extra_rules: Optional[List[Dict[str, str]]] = None,
category: Optional[str] = None,
apply_twist: bool = True,
) -> Tuple[str, str]:
"""根据热点词语义确定性地推导 (art_style, color_palette)。
优先级:① 国家 extra 规则(最具体)→ ② 全局关键词规则 → ③ 类别兜底 → ④ 国家基调最末兜底。
结果与具体词一一对应,跨词不同、同词稳定。apply_twist=True 时追加确定性风格微调。
"""
from .classify import classify
tl = (topic or "").lower()
cat = category or classify(topic)
# ① 国家专属 extra 规则(来自 prompts/<country>/aesthetics.yaml
for rule in (extra_rules or []):
kws = [k.lower() for k in (rule.get("keywords") or [])]
if kws and _kw_pattern(kws).search(tl):
art = rule.get("art_style", "clean vector illustration")
pal = rule.get("color_palette", "balanced modern palette")
if apply_twist:
art = _apply_twist(art, topic)
return art, pal
# ② 全局关键词规则(单词边界匹配,具体规则优先于通用 retro/vintage
for pat, art, pal in _RULE_PATTERNS:
if pat.search(tl):
if apply_twist:
art = _apply_twist(art, topic)
return art, pal
# ③ 类别兜底
art = STYLE_BY_CATEGORY.get(cat, "clean vector illustration")
pal = PALETTE_BY_CATEGORY.get(cat, "balanced modern color palette")
# ④ 国家基调最末兜底(极少走到)
if art == "clean vector illustration":
a = COUNTRY_AESTHETICS.get(country, {})
art = a.get("art_style", art)
pal = a.get("palette", pal)
if apply_twist:
art = _apply_twist(art, topic)
return art, pal
def _apply_twist(art: str, topic: str) -> str:
"""确定性地给 art_style 追加一个技法微调(同词稳定、跨词不同)。"""
twist = STYLE_TWISTS[_stable_hash(topic) % len(STYLE_TWISTS)]
return f"{art}, {twist}"
# 构图变体池:与风格 twist 同理,确定性地为每条设计选一个差异化构图,打破千篇一律。
COMPOSITION_VARIANTS: List[str] = [
"centered circular emblem with balanced negative space",
"all-over repeat pattern with a centered focal badge",
"side-profile hero with motion lines",
"symmetrical mandala emblem, centered",
"scattered botanical border framing a centered wreath",
"bold central emblem with spray texture",
"layered landscape with a centered focal subject",
"centered badge inside a decorative ring",
"dynamic diagonal composition with speed streaks",
"tiled geometric grid with a centered motif",
"vertical stack emblem with a banner ribbon",
"framed portrait window with ornate border",
]
def derive_composition(topic: str, category: Optional[str] = None) -> str:
"""确定性地推导构图(同词稳定、跨词不同),避免所有设计共用同一句构图。"""
return COMPOSITION_VARIANTS[_stable_hash(topic) % len(COMPOSITION_VARIANTS)]
# 图像生成策略敏感词 → 安全等效描述(生成提示词前清洗,降低内容政策拦截)
_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 sanitize_image_prompt(prompt: str) -> str:
"""清洗生图提示词:
1) 删除一切背景描述(官方要求:透明背景由 background="transparent" 参数控制,
提示词中不得提到背景,否则无法正常生成透明背景);
2) 敏感词替换为安全等效描述(避免内容政策拦截)。
"""
import re
out = prompt or ""
# 1) 删除背景短语(中英文都处理)
for pat in (r",\s*isolated on (transparent|pure white|white) background\b",
r"\s+isolated on (transparent|pure white|white) background\b",
r",\s*(transparent|white) background\b",
r"\s+on a (transparent|white|pure white) background\b",
r",\s*no background scene\b", r",\s*no background\b", r",\s*plain (transparent|white) background\b"):
out = re.sub(pat, "", out, flags=re.IGNORECASE)
# 1.5) 删除内容策略触发段(旧模板残留的安全规则说明:no politics/religion/hate/violence/sexual 等
# 一旦出现在生图提示词中,图像 API 直接 content_policy_violation
out = re.sub(r';?\s*any text must be safe[^;]*?(?:no gibberish|gibberish|\.[^,;]*)', '', out, flags=re.IGNORECASE)
out = re.sub(r'no (politics|religion|hate|violence|sexual|nude|nudity|bikini|nsfw|racist|profanity|swearing|adult content)s?,?', '', out, flags=re.IGNORECASE)
out = re.sub(r'(politics|religion|hate|violence|sexual content|nudity|nsfw)', '', out, flags=re.IGNORECASE)
# 2) 敏感词替换
low = out.lower()
for k, v in _IMG_RISKY_SWAP.items():
if k in low:
out = re.sub(rf"\b{re.escape(k)}\b", v, out, flags=re.IGNORECASE)
low = out.lower()
# 3) 清理多余空格/逗号
out = re.sub(r",\s*,+", ",", out)
out = re.sub(r"\s{2,}", " ", out).strip(" ,")
return out
# review(疑似商标/受保护主题)简报的「原创化魔改」引导:只做风格参考,禁止复刻商标/品牌/角色
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."
)
def ensure_rebrand_hint(brief: dict, prompt: str) -> str:
"""review 简报生成设计时兜底追加原创化魔改引导(旧缓存简报未注入时补上)。"""
if str(brief.get("risk_level", "")).strip().lower() == "review" \
and "IMPORTANT: this theme is ONLY" not in (prompt or ""):
return (prompt or "") + REVIEW_REBRAND_HINT
return prompt or ""