- 缓存热点批量流程(有采集缓存不触发 Google) - 简报不足直接从采集缓存生成(轻量补齐) - 三图合成(模特/印花/底图)+ 底图压缩 <2MB - 热点去重→风格去重自动切换 + 不适合类目 review 兜底 - 透明背景(background=transparent)+ 提示词清洗(敏感词/背景描述) - 任务前 basemap 校验 + 模板国家校验 + 模特任务级分配
254 lines
11 KiB
Python
254 lines
11 KiB
Python
"""归一化、跨源融合、合规黑名单过滤、人名过滤(从原 src/scoring.py 迁移到 graph 包)。
|
||
|
||
所有函数纯逻辑、无 IO,便于节点内调用与单元测试。
|
||
"""
|
||
import re
|
||
from collections import defaultdict
|
||
from typing import Dict, List, Optional, Tuple
|
||
|
||
|
||
DEFAULT_NAME_PATTERNS = [r"^[A-Z][a-z]+(?: [A-Z][a-z]+){1,2}$"]
|
||
DEFAULT_EXTRA_NAMES = [
|
||
"taylor swift", "trump", "biden", "kardashian", "lebron", "charlie sheen",
|
||
"bernie sanders", "elon musk", "beyonce", "drake", "rihanna", "justin bieber",
|
||
"ariana grande", "selena gomez", "eminem", "kanye", "travis scott", "messi",
|
||
"ronaldo", "harry styles", "bts", "blackpink", "pewdiepie", "mrbeast",
|
||
"kamala harris", "joe biden", "donald trump", "kim kardashian", "pearl jam",
|
||
"nirvana", "michael jackson", "madonna", "britney spears", "lady gaga",
|
||
"justin timberlake", "tom cruise", "brad pitt", "keanu reeves", "robert downey",
|
||
"cristiano ronaldo", "lionel messi", "billie eilish", "the weeknd", "post malone",
|
||
"kendrick lamar", "joe rogan", "andrew tate", "elon", "musk", "obama", "clinton",
|
||
"springsteen", "reiner", "eliza lopes", "camilla", "noah kahan", "gina carano",
|
||
# —— 常见人名扩充(歌手/演员/运动员/政客/企业家/网红/王室,子串匹配,避免真人印花)——
|
||
"ed sheeran", "dua lipa", "adele", "bruno mars", "shakira", "elton john",
|
||
"david bowie", "freddie mercury", "whitney houston", "celine dion", "olivia rodrigo",
|
||
"sabrina carpenter", "chappell roan", "ice spice", "nicki minaj", "cardi b",
|
||
"doja cat", "sza", "lil nas x", "bad bunny", "shawn mendes", "zayn malik",
|
||
"dwayne johnson", "johnny depp", "leonardo dicaprio", "chris hemsworth", "chris evans",
|
||
"tom holland", "zendaya", "jennifer lawrence", "emma watson", "scarlett johansson",
|
||
"miley cyrus", "hugh jackman", "nicole kidman", "cate blanchett", "steve irwin",
|
||
"kylie minogue", "morgan freeman", "will smith", "denzel washington", "angelina jolie",
|
||
"jennifer aniston", "george clooney", "robert pattinson", "daniel radcliffe", "emma stone",
|
||
"ryan reynolds", "ryan gosling", "kobe bryant", "michael jordan", "serena williams",
|
||
"venus williams", "tiger woods", "usain bolt", "tom brady", "patrick mahomes",
|
||
"stephen curry", "kevin durant", "lewis hamilton", "max verstappen", "novak djokovic",
|
||
"rafael nadal", "roger federer", "conor mcgregor", "putin", "zelensky",
|
||
"boris johnson", "rishi sunak", "narendra modi", "justin trudeau", "emmanuel macron",
|
||
"olaf scholz", "bill gates", "jeff bezos", "mark zuckerberg", "logan paul", "jake paul",
|
||
"ksi", "charli damelio", "addison rae", "kylie jenner", "kendall jenner",
|
||
"queen elizabeth", "king charles", "prince william", "prince harry", "meghan markle",
|
||
"princess diana",
|
||
]
|
||
DEFAULT_EXEMPTIONS = [
|
||
"new album", "best seller", "top gear", "red cross", "black cat", "blue moon",
|
||
"green day", "red hot chili peppers", "cold play", "one direction", "little mix",
|
||
"west life", "back street", "new york", "los angeles", "san diego", "new orleans",
|
||
"san francisco", "las vegas", "white house", "high school", "middle earth",
|
||
]
|
||
|
||
|
||
def filter_person_names(
|
||
rows: List[Dict],
|
||
extra_names: Optional[List[str]] = None,
|
||
patterns: Optional[List[str]] = None,
|
||
exemptions: Optional[List[str]] = None,
|
||
pattern_sources: Optional[set] = None,
|
||
) -> Tuple[List[Dict], List[Dict]]:
|
||
"""剔除真实人物(明星/政客/名人),避免肖像权风险。返回 (kept, dropped)。"""
|
||
extra = [e.lower() for e in (extra_names if extra_names is not None else DEFAULT_EXTRA_NAMES)]
|
||
pats = patterns if patterns is not None else DEFAULT_NAME_PATTERNS
|
||
exempt = [e.lower() for e in (exemptions if exemptions is not None else DEFAULT_EXEMPTIONS)]
|
||
compiled = [re.compile(p) for p in pats]
|
||
pattern_sources = set(pattern_sources) if pattern_sources is not None else {"gt_trending"}
|
||
|
||
kept, dropped = [], []
|
||
for r in rows:
|
||
topic = str(r.get("topic", "")).strip()
|
||
tl = topic.lower()
|
||
if any(e and e in tl for e in exempt):
|
||
kept.append(r)
|
||
continue
|
||
reason = None
|
||
hit_name = [n for n in extra if n and n in tl]
|
||
if hit_name:
|
||
reason = f"命中人名名单: {hit_name}"
|
||
elif r.get("source") in pattern_sources and any(p.search(topic) for p in compiled):
|
||
reason = "匹配人名模式(疑似真实人物)"
|
||
if reason:
|
||
r2 = dict(r)
|
||
r2["drop_reason"] = reason
|
||
dropped.append(r2)
|
||
else:
|
||
kept.append(r)
|
||
return kept, dropped
|
||
|
||
|
||
def filter_design_relevance(
|
||
rows: List[Dict],
|
||
drop_patterns: Optional[List[str]] = None,
|
||
keep_patterns: Optional[List[str]] = None,
|
||
) -> Tuple[List[Dict], List[Dict]]:
|
||
"""丢弃不可作印花主体的泛新闻/科技/赛事词。返回 (kept, dropped)。"""
|
||
drop = [re.compile(p, re.I) for p in (drop_patterns or [])]
|
||
keep = [re.compile(p, re.I) for p in (keep_patterns or [])]
|
||
kept, dropped = [], []
|
||
for r in rows:
|
||
topic = str(r.get("topic", "")).strip()
|
||
tl = topic.lower()
|
||
if keep and not any(p.search(tl) for p in keep):
|
||
r2 = dict(r)
|
||
r2["drop_reason"] = "未命中设计相关性白名单"
|
||
dropped.append(r2)
|
||
continue
|
||
if drop and any(p.search(tl) for p in drop):
|
||
r2 = dict(r)
|
||
r2["drop_reason"] = "非印花设计主体(泛新闻/科技/赛事)"
|
||
dropped.append(r2)
|
||
continue
|
||
kept.append(r)
|
||
return kept, dropped
|
||
|
||
|
||
# 查询噪声:非“可印花设计概念”的检索问句 / 命名清单 / 损坏碎片,应直接丢弃而非标 safe。
|
||
_QUERY_NOISE_LEAD = re.compile(
|
||
r"^(what|who|how|why|when|where|which|is|are|was|were|do|does|did|can|will|"
|
||
r"should|would|may|might|has|have|whose|whom)\b", re.I)
|
||
# 任意位置的疑问词:覆盖 "punk sprite what does it do" 这类词序在中的问句
|
||
_QUERY_NOISE_WH_ANY = re.compile(r"\b(what|who|how|why|when|where|which)\b", re.I)
|
||
# names/surnames:覆盖 "cottagecore surnames" 这类变体
|
||
_QUERY_NOISE_NAMES = re.compile(
|
||
r"\b((?:boy|girl|baby|pet|dog|cat|last|first|middle)?\s*names?|surnames)"
|
||
r"(?:\s+(?:ideas|list))?\b$", re.I)
|
||
# 损坏/拼接碎片:数字前缀可选,覆盖 "gothic remake review"(无数字)与 "gothic remake metacritic"
|
||
_QUERY_NOISE_CORRUPT = re.compile(
|
||
r"\b(?:\d{1,2}\s+)?(remake|review|version|copy|edit|replica|metacritic)\b", re.I)
|
||
_QUERY_NOISE_WORDS = [re.compile(p, re.I) for p in
|
||
[r"\bstory\b", r"\bmeaning\b", r"\bdefinition\b",
|
||
r"\btutorial\b", r"\bguide\b", r"\bquests?\b"]]
|
||
|
||
|
||
# —— 新闻类热点过滤(突发新闻不适合做印花主题,各国语言词表)——
|
||
NEWS_WORDS_GLOBAL = [
|
||
"weather", "forecast", "typhoon", "earthquake", "tsunami", "hurricane",
|
||
"missile", "election", "vote", "prime minister", "president", "minister",
|
||
"cabinet", "senate", "congress", "parliament", "shooting", "ceasefire",
|
||
"nuclear", "summit", "hostage", "emergency", "warning", "breaking news",
|
||
"stock market", "oil price", "inflation", "deadline", "live update",
|
||
]
|
||
NEWS_WORDS_JP = [
|
||
"天気", "台風", "気象", "地震", "津波", "ミサイル", "首相", "大臣",
|
||
"会見", "速報", "選挙", "防衛", "自衛隊", "警報", "注意報", "ニュース",
|
||
"報道", "豪雨", "猛暑", "熱中症", "株価", "円相場", "物価", "国会",
|
||
"衆院", "参院", "裁判", "逮捕", "捜査", "事故", "死亡", "追悼", "慰霊",
|
||
]
|
||
|
||
|
||
def filter_news(rows: List[Dict], country: str = "") -> Tuple[List[Dict], List[Dict]]:
|
||
"""丢弃新闻类热点(天气/灾害/政治/事故等突发新闻,非印花主题)。按国家语言补充词表。"""
|
||
words = list(NEWS_WORDS_GLOBAL)
|
||
if str(country).upper() == "JP":
|
||
words += NEWS_WORDS_JP
|
||
elif str(country).upper() == "US":
|
||
words += ["weather alert", "live coverage", "breaking"]
|
||
kept, dropped = [], []
|
||
for r in rows:
|
||
tl = str(r.get("topic", "")).lower()
|
||
hit = next((w for w in words if w in tl), None)
|
||
if hit:
|
||
r2 = dict(r)
|
||
r2["drop_reason"] = f"新闻类热点(非印花主题): {hit}"
|
||
dropped.append(r2)
|
||
else:
|
||
kept.append(r)
|
||
return kept, dropped
|
||
|
||
|
||
def filter_query_noise(
|
||
rows: List[Dict],
|
||
enabled: bool = True,
|
||
) -> Tuple[List[Dict], List[Dict]]:
|
||
"""丢弃“查询噪声/非设计概念”词(问句、命名清单、损坏碎片、模糊名词)。
|
||
|
||
返回 (kept, dropped)。这些词不是可印花主体,进入 screen 会被 Mock 误标 safe,
|
||
故在过滤阶段就剔除,避免污染生图环节。
|
||
"""
|
||
if not enabled:
|
||
return rows, []
|
||
kept, dropped = [], []
|
||
for r in rows:
|
||
topic = str(r.get("topic", "")).strip()
|
||
tl = topic.lower()
|
||
reason = None
|
||
if _QUERY_NOISE_WH_ANY.search(tl) or _QUERY_NOISE_LEAD.search(tl):
|
||
reason = "查询问句(非设计概念)"
|
||
elif _QUERY_NOISE_NAMES.search(tl):
|
||
reason = "命名清单类查询(非设计概念)"
|
||
elif _QUERY_NOISE_CORRUPT.search(tl):
|
||
reason = "损坏/拼接的查询碎片"
|
||
elif any(p.search(tl) for p in _QUERY_NOISE_WORDS):
|
||
reason = "模糊名词(非设计概念)"
|
||
if reason:
|
||
r2 = dict(r)
|
||
r2["drop_reason"] = reason
|
||
dropped.append(r2)
|
||
else:
|
||
kept.append(r)
|
||
return kept, dropped
|
||
|
||
|
||
def normalize(rows: List[Dict], key: str = "raw_score") -> List[Dict]:
|
||
"""min-max 归一化到 0-1,按 (source, kind) 分组分别归一化。"""
|
||
if not rows:
|
||
return rows
|
||
groups = defaultdict(list)
|
||
for r in rows:
|
||
groups[(r.get("source", "_"), r.get("kind", "_"))].append(r)
|
||
for grp in groups.values():
|
||
vals = [r[key] for r in grp if r.get(key) is not None]
|
||
if not vals:
|
||
for r in grp:
|
||
r["norm"] = 0.0
|
||
continue
|
||
lo, hi = min(vals), max(vals)
|
||
span = (hi - lo) or 1.0
|
||
for r in grp:
|
||
v = r.get(key)
|
||
r["norm"] = (v - lo) / span if v is not None else 0.0
|
||
return rows
|
||
|
||
|
||
def apply_blacklist(rows: List[Dict], blacklist: List[str]) -> Tuple[List[Dict], List[Dict]]:
|
||
"""命中黑名单的词丢弃,返回 (保留, 丢弃)。"""
|
||
if not blacklist:
|
||
return rows, []
|
||
bl = [b.lower() for b in blacklist]
|
||
kept, dropped = [], []
|
||
for r in rows:
|
||
text = f"{r.get('topic', '')} {r.get('seed', '')}".lower()
|
||
if any(b in text for b in bl):
|
||
dropped.append(r)
|
||
else:
|
||
kept.append(r)
|
||
return kept, dropped
|
||
|
||
|
||
def combine(rows: List[Dict], weights: Dict[str, float]) -> List[Dict]:
|
||
"""按 topic 跨源融合,权重来自 config。"""
|
||
agg = {}
|
||
for r in rows:
|
||
t = r["topic"].lower().strip()
|
||
if t not in agg:
|
||
agg[t] = {"topic": r["topic"], "countries": set(), "sources": set(), "score": 0.0}
|
||
w = weights.get(r["source"], 0.5)
|
||
agg[t]["score"] += r.get("norm", 0.0) * w
|
||
if r.get("country"):
|
||
agg[t]["countries"].add(r["country"])
|
||
agg[t]["sources"].add(r["source"])
|
||
out = []
|
||
for o in agg.values():
|
||
o["countries"] = ",".join(sorted(o["countries"])) or "GLOBAL"
|
||
o["sources"] = ",".join(sorted(o["sources"]))
|
||
out.append(o)
|
||
out.sort(key=lambda x: x["score"], reverse=True)
|
||
return out
|