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
+27
View File
@@ -0,0 +1,27 @@
"""graph/seeds 可插拔种子词策略注册表。
seed_provider 取值:
- static : 仅用 yaml 写死种子,零动态
- mock : 规则生成(借用 trending/历史/月份节日),零 API 成本
- openai_compat : 真 LLM 生成(OpenAI / DeepSeek / Qwen / Kimi 等兼容协议)
- openai / deepseek / qwen / moonshot : 同 openai_compat,仅别名
"""
from typing import Dict
from .base import SeedStrategy
from .static_strategy import StaticStrategy
from .dynamic_strategy import DynamicStrategy
SEED_STRATEGIES: Dict[str, SeedStrategy] = {
"static": StaticStrategy(),
"mock": DynamicStrategy(),
"openai_compat": DynamicStrategy(),
"openai": DynamicStrategy(),
"deepseek": DynamicStrategy(),
"qwen": DynamicStrategy(),
"moonshot": DynamicStrategy(),
}
def get_seed_strategy(name: str) -> SeedStrategy:
return SEED_STRATEGIES.get((name or "static").strip().lower(), StaticStrategy())
+31
View File
@@ -0,0 +1,31 @@
"""种子词策略基类(可插拔核心)。
新增一个种子词策略只需:① 继承 SeedStrategy 实现 resolve();② 在 __init__.py
的 SEED_STRATEGIES 注册表里登记。config 的 ``seed_provider`` 选择用哪个。
"""
from typing import Any, Dict, List, Optional
class SeedStrategy:
#: 注册名(与 config.seed_provider 对应)
name: str = "base"
def resolve(
self,
country: str,
cc: Dict[str, Any],
context: Dict[str, Any],
llm_backend: Optional[Any] = None,
) -> Dict[str, Any]:
"""产出种子词。
返回至少包含:
- "style_seeds": [str] 风格/美学向种子
- "related_seeds": [str] 行业/主体向种子(行业交叉验证)
- "dynamic": bool 是否经过 LLM 动态生成
可选附带 "llm_style_seeds" / "llm_related_seeds" 便于观测。
cc 为合并后的国家配置(含 yaml 静态种子);context 为 seed_node 收集的
trending/历史/月份节日上下文;llm_backend 为 LLM 后端实例(可能 None)。
"""
raise NotImplementedError
+111
View File
@@ -0,0 +1,111 @@
"""动态策略:以 yaml 静态种子为基础,叠加动态种子(统一池 + 加权随机 + 用完全用)。
种子词机制(v49 起):
1. 全部类型放一起(统一池):静态 style + 静态 related + 月份主题 + 节日 + LLM 动态
——合并去重(跨类型同词只保留一个,权重累加 = 多来源更受重视);
2. 节日种子词提供权重:节日权重 3.0 > 月份主题 2.0 > 静态/动态 1.0,随机抽取时加权;
3. 每次随机取:从池中按权重随机抽取(不重复),limit 内数量;
4. 用完全用:池中种子数 ≤ 需要数时全部使用(不再随机限量/截断);
5. 每个国家独立配置:configs/countries/<country>.yaml 的 style.seeds / related.seed_keywords。
limit 由 seed_node 从 context 注入(max_style_seeds / max_related_seeds0 或缺失=不限)。
LLM 后端生成失败时自动回退到静态+节日主题,保证不中断。
"""
import random
from typing import Any, Dict, List
from .base import SeedStrategy
def _weighted_sample(pool: List[Dict[str, Any]], k: int) -> List[Dict[str, Any]]:
"""按权重随机不重复取 k 个;池数量 ≤ k(或用完)时全部返回(不随机限量)。"""
if k <= 0 or len(pool) <= k:
return list(pool)
out: List[Dict[str, Any]] = []
rest = list(pool)
for _ in range(k):
weights = [max(float(it["weight"]), 0.0) for it in rest]
if sum(weights) <= 0:
out.extend(rest)
break
idx = random.choices(range(len(rest)), weights=weights)[0]
out.append(rest.pop(idx))
return out
class DynamicStrategy(SeedStrategy):
name = "dynamic"
def resolve(
self,
country: str,
cc: Dict[str, Any],
context: Dict[str, Any],
llm_backend: Any = None,
) -> Dict[str, Any]:
base_style = list((cc.get("style", {}) or {}).get("seeds", []) or [])
base_related = list((cc.get("related", {}) or {}).get("seed_keywords", []) or [])
month_style = list(context.get("month_themes", []) or [])
holidays = list(context.get("upcoming_holidays", []) or [])
holiday_style = [f"{h.lower()} aesthetic" for h in holidays]
# 节日主题同时扩充 related 源(提高节日权重 + 增加 related 扩展)
holiday_related = [f"{h.lower()} tee" if not h.lower().endswith("day") else f"{h.lower()} gift"
for h in holidays]
# LLM 动态种子(失败回退,不影响静态/节日)
dyn_style: List[str] = []
dyn_related: List[str] = []
if llm_backend is not None and hasattr(llm_backend, "generate_seeds"):
try:
res = llm_backend.generate_seeds(context) or {}
dyn_style = list(res.get("style_seeds", []) or [])
dyn_related = list(res.get("related_seeds", []) or [])
except Exception as e: # noqa: BLE001
print(f"[seed] LLM 生成种子失败,仅用静态+节日主题: {e}")
# 1) 统一池:全部类型合并,跨类型去重(同词权重累加 = 多来源更受重视)
pool: Dict[str, Dict[str, Any]] = {}
def add(items: List[str], weight: float, src: str) -> None:
for it in items:
it = (it or "").strip()
if not it:
continue
key = it.lower()
if key in pool:
pool[key]["weight"] += weight
pool[key]["sources"].append(src)
else:
pool[key] = {"word": it, "weight": weight, "sources": [src]}
add(base_style, 1.0, "static")
add(base_related, 1.0, "static")
add(month_style, 2.0, "month")
add(holiday_style, 3.0, "holiday")
add(holiday_related, 3.0, "holiday")
add(dyn_style, 1.0, "dynamic")
add(dyn_related, 1.0, "dynamic")
items = list(pool.values())
limit_style = int(context.get("max_style_seeds") or 0)
limit_related = int(context.get("max_related_seeds") or 0)
# 2) 每次随机取(加权,不重复);池不足 → 全部用
style_pick = _weighted_sample(items, limit_style)
style_keys = {id(it) for it in style_pick}
remaining = [it for it in items if id(it) not in style_keys]
related_pick = _weighted_sample(remaining, limit_related)
return {
"style_seeds": [it["word"] for it in style_pick],
"related_seeds": [it["word"] for it in related_pick],
"dynamic": True,
"pool_size": len(items),
"pool": [it["word"] for it in items],
"llm_style_seeds": dyn_style,
"llm_related_seeds": dyn_related,
"holiday_style_seeds": holiday_style,
"holiday_related_seeds": holiday_related,
"static_style_seeds": base_style,
"static_related_seeds": base_related,
}
+186
View File
@@ -0,0 +1,186 @@
"""月份 / 季节 / 临近节日上下文(按国家),供动态种子词生成的 LLM 上下文使用。
提供 build_holiday_context(country, now=None) -> dict
{
"date": "2026-08-21",
"year": 2026,
"month": 8,
"season": "Summer",
"month_themes": ["back to school", "late summer", "outdoor adventure"],
"upcoming_holidays": ["Back to School", "Summer Solstice"]
}
- month_themes:固定月度灵感,作为种子词生成的稳定基础。
- upcoming_holidays:按国家节日表,用窗口计算临近(含刚过的)固定/浮动节日,
让 LLM 注入"当前该国的可能节日",生成对应节日主题种子。
"""
import datetime
from typing import Dict, List, Optional
# 北半球季节(南半球可反向扩展)
SEASON_BY_MONTH = {
12: "Winter", 1: "Winter", 2: "Winter",
3: "Spring", 4: "Spring", 5: "Spring",
6: "Summer", 7: "Summer", 8: "Summer",
9: "Autumn", 10: "Autumn", 11: "Autumn",
}
# 月度主题词(通用印花设计灵感)
MONTH_THEMES: Dict[int, List[str]] = {
1: ["new year", "winter cozy", "resolution"],
2: ["valentine", "love", "heart"],
3: ["spring bloom", "st patrick", "fresh start"],
4: ["easter", "spring garden", "pastel"],
5: ["mother day", "flower", "spring outdoor"],
6: ["pride", "summer start", "beach"],
7: ["summer vibe", "travel", "festival"],
8: ["back to school", "late summer", "outdoor adventure"],
9: ["autumn equinox", "harvest", "cozy"],
10: ["halloween", "autumn goth", "spooky"],
11: ["thanksgiving", "gratitude", "autumn warm"],
12: ["christmas", "winter holiday", "cozy festive"],
}
# (name, month, day, rule, window_days)
# rule: None=固定日; "mother"=第2周日; "father"=第3周日; "thanks"=第4周四; "easter"=Computus; "bf"=thanks+1
_HOLIDAYS_BY_COUNTRY: Dict[str, List[tuple]] = {
"US": [
("New Year", 1, 1, None, 14),
("Valentine's Day", 2, 14, None, 21),
("St Patrick's Day", 3, 17, None, 14),
("Easter", 0, 0, "easter", 21),
("Mother's Day", 5, 0, "mother", 14),
("Father's Day", 6, 0, "father", 14),
("Pride Month", 6, 1, None, 7),
("Independence Day", 7, 4, None, 21),
("Summer Solstice", 6, 21, None, 14),
("Back to School", 8, 15, None, 30),
("Labor Day", 9, 0, "labor", 14),
("Halloween", 10, 31, None, 30),
("Thanksgiving (US)", 11, 0, "thanks", 21),
("Black Friday", 11, 0, "bf", 14),
("Christmas", 12, 25, None, 30),
("Winter Solstice", 12, 21, None, 14),
],
"GB": [
("New Year", 1, 1, None, 14),
("Valentine's Day", 2, 14, None, 21),
("St Patrick's Day", 3, 17, None, 14),
("Easter", 0, 0, "easter", 21),
("Mother's Day (UK)", 3, 0, "mother_uk", 14),
("Father's Day", 6, 0, "father", 14),
("Summer Bank Holiday", 8, 25, None, 14),
("Halloween", 10, 31, None, 30),
("Bonfire Night", 11, 5, None, 21),
("Remembrance Day", 11, 11, None, 14),
("Christmas", 12, 25, None, 30),
("Boxing Day", 12, 26, None, 14),
],
"JP": [
("New Year", 1, 1, None, 14),
("Valentine's Day", 2, 14, None, 21),
("Hinamatsuri", 3, 3, None, 14), # 雏祭
("Hanami", 4, 1, None, 21), # 花见(樱花季)
("Golden Week", 4, 29, None, 21),
("Children's Day", 5, 5, None, 14), # 子供の日
("Tanabata", 7, 7, None, 14), # 七夕
("Fireworks Season", 8, 1, None, 30), # 花火大会
("Obon", 8, 13, None, 21), # お盆
("Halloween", 10, 31, None, 30),
("Christmas", 12, 25, None, 30),
("New Year Eve", 12, 31, None, 14), # 大晦日
],
"AU": [
("New Year", 1, 1, None, 14),
("Australia Day", 1, 26, None, 21),
("Valentine's Day", 2, 14, None, 21),
("Easter", 0, 0, "easter", 21),
("Anzac Day", 4, 25, None, 21),
("Mother's Day (AU)", 5, 0, "mother", 14),
("Father's Day (AU)", 9, 0, "father", 14),
("Summer Christmas", 12, 25, None, 30),
("Boxing Day", 12, 26, None, 21),
("Halloween", 10, 31, None, 21),
],
}
def _easter(year: int) -> datetime.date:
a = year % 19
b = year // 100
c = year % 100
d = b // 4
e = b % 4
f = (b + 8) // 25
g = (b - f + 1) // 3
h = (19 * a + b - d - g + 15) % 30
i = c // 4
k = c % 4
l = (32 + 2 * e + 2 * i - h - k) % 7
m = (a + 11 * h + 22 * l) // 451
month = (h + l - 7 * m + 114) // 31
day = ((h + l - 7 * m + 114) % 31) + 1
return datetime.date(year, month, day)
def _resolve(name: str, month: int, day: int, rule, year: int) -> Optional[datetime.date]:
if rule is None:
return datetime.date(year, month, day)
if rule == "easter":
return _easter(year)
if rule in ("mother", "mother_uk"):
# 第2个周日(UK 用"母亲节"但实际 3 月第4周日前的第4大斋期周日——简化为 3 月第2周日)
if rule == "mother_uk":
month, week = 3, 2
else:
month, week = month, 2
first = datetime.date(year, month, 1)
return first + datetime.timedelta(days=(6 - first.weekday()) % 7 + (week - 1) * 7)
if rule == "father":
first = datetime.date(year, month, 1)
return first + datetime.timedelta(days=(6 - first.weekday()) % 7 + 2 * 7)
if rule == "thanks":
first = datetime.date(year, month, 1)
return first + datetime.timedelta(days=(3 - first.weekday()) % 7 + 3 * 7)
if rule == "bf":
t = _resolve("", 11, 0, "thanks", year)
return t + datetime.timedelta(days=1) if t else None
if rule == "labor":
first = datetime.date(year, month, 1)
return first + datetime.timedelta(days=(0 - first.weekday()) % 7)
return None
def holidays_for(country: str = "US") -> List[tuple]:
key = (country or "US").upper()
return _HOLIDAYS_BY_COUNTRY.get(key, _HOLIDAYS_BY_COUNTRY["US"])
def upcoming_holidays(now: Optional[datetime.date] = None, country: str = "US",
lower: int = -10) -> List[str]:
"""返回该国临近(未来 window 内,或刚过 lower 天内)的节日名。"""
now = now or datetime.date.today()
out: List[str] = []
for name, month, day, rule, window in holidays_for(country):
try:
d = _resolve(name, month, day, rule, now.year)
except Exception:
continue
if d is None:
continue
delta = (d - now).days
if lower <= delta <= window:
out.append(name)
return out
def build_holiday_context(country: str = "US",
now: Optional[datetime.date] = None) -> Dict[str, object]:
now = now or datetime.date.today()
return {
"date": now.isoformat(),
"year": now.year,
"month": now.month,
"season": SEASON_BY_MONTH.get(now.month, ""),
"month_themes": MONTH_THEMES.get(now.month, []),
"upcoming_holidays": upcoming_holidays(now, country),
}
+23
View File
@@ -0,0 +1,23 @@
"""静态策略:直接使用 configs/countries/<country>.yaml 里写死的种子词,不做任何动态生成。"""
from typing import Any, Dict, List
from .base import SeedStrategy
class StaticStrategy(SeedStrategy):
name = "static"
def resolve(
self,
country: str,
cc: Dict[str, Any],
context: Dict[str, Any],
llm_backend: Any = None,
) -> Dict[str, Any]:
style = list((cc.get("style", {}) or {}).get("seeds", []) or [])
related = list((cc.get("related", {}) or {}).get("seed_keywords", []) or [])
return {
"style_seeds": style,
"related_seeds": related,
"dynamic": False,
}