POD 趋势感知 Agent:缓存热点模式 + 三图合成 + 热点去重/风格去重 + review 兜底
- 缓存热点批量流程(有采集缓存不触发 Google) - 简报不足直接从采集缓存生成(轻量补齐) - 三图合成(模特/印花/底图)+ 底图压缩 <2MB - 热点去重→风格去重自动切换 + 不适合类目 review 兜底 - 透明背景(background=transparent)+ 提示词清洗(敏感词/背景描述) - 任务前 basemap 校验 + 模板国家校验 + 模特任务级分配
This commit is contained in:
@@ -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())
|
||||
@@ -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
|
||||
@@ -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_seeds,0 或缺失=不限)。
|
||||
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,
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user