feat(classify): tighten scoring with reaction co-occurrence and date negatives
强正向 reaction 需标题同时含 require_title;移除听/看等宽泛词,新增弱正向组;charge 命中>=2 记+3、==1 记+1;负向补充录播/录像并新增标题带日期强负向;时长仅在已命中 reaction/charge 时加权;discover 关键词改为单个咻咻满、pages=3、window_days=7,notify.min_score=4;同步示例配置、project.md/README 与相关测试。
This commit is contained in:
1 parent
e04908ccad
commit
742c67a1c3
9 files changed
+210
-107
No files matched your search
+67
-18
@@ -47,11 +47,16 @@ def parse_duration(value: Any) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def contains_any(title: str, words: Any) -> bool:
|
||||
def matched_words(title: str, words: Any) -> list[str]:
|
||||
"""返回标题命中(归一化子串)的词,保持 ``words`` 顺序、跳过空词。"""
|
||||
normalized = normalize_title(title)
|
||||
if not isinstance(words, (list, tuple)):
|
||||
return False
|
||||
return any(normalize_title(str(word)) in normalized for word in words if word)
|
||||
return []
|
||||
return [str(word) for word in words if word and normalize_title(str(word)) in normalized]
|
||||
|
||||
|
||||
def contains_any(title: str, words: Any) -> bool:
|
||||
return bool(matched_words(title, words))
|
||||
|
||||
|
||||
def passes_required_title(title: str, require_title: str) -> bool:
|
||||
@@ -60,6 +65,30 @@ def passes_required_title(title: str, require_title: str) -> bool:
|
||||
return normalize_title(require_title) in normalize_title(title)
|
||||
|
||||
|
||||
# 标题带日期通常意味着录播/回放/录像(按具体某场直播命名),作为强负向信号。
|
||||
# 覆盖 `2026-10-28` / `2026/10/25` / `2026.10.26` / `2024年11月24日` / `10月25日`。
|
||||
_DATE_PATTERNS = (
|
||||
r"\d{4}\s*[-/.]\s*\d{1,2}\s*[-/.]\s*\d{1,2}",
|
||||
r"\d{4}\s*年\s*\d{1,2}\s*月(?:\s*\d{1,2}\s*日)?",
|
||||
r"\d{1,2}\s*月\s*\d{1,2}\s*日",
|
||||
)
|
||||
_DATE_RE = re.compile("|".join(_DATE_PATTERNS))
|
||||
|
||||
# 各信号的权重(见 project.md §3.3)。
|
||||
REACTION_SCORE = 3
|
||||
CHARGE_MULTI_SCORE = 3
|
||||
CHARGE_SINGLE_SCORE = 1
|
||||
WEAK_SCORE = 1
|
||||
NEGATIVE_SCORE = 6
|
||||
DATE_SCORE = 6
|
||||
SELF_SCORE = 3
|
||||
|
||||
|
||||
def date_hits(title: str) -> list[str]:
|
||||
"""返回标题中的日期片段,用于判定「带日期≈录播/回放」。"""
|
||||
return _DATE_RE.findall(title or "")
|
||||
|
||||
|
||||
def score_item(
|
||||
*,
|
||||
title: str,
|
||||
@@ -67,23 +96,43 @@ def score_item(
|
||||
duration_seconds: int,
|
||||
classify: Mapping[str, Any],
|
||||
) -> int:
|
||||
"""按 project.md §3.3 计算候选分数。"""
|
||||
"""按 project.md §3.3 计算候选分数。
|
||||
|
||||
强正向 ``reaction`` 只在标题同时命中 ``require_title`` 时成立;时长只作为
|
||||
已存在 reaction / charge 信号时的加权,避免纯长录像仅靠时长进池。
|
||||
"""
|
||||
score = 0
|
||||
if contains_any(title, classify.get("positive")):
|
||||
score += 2
|
||||
if contains_any(title, classify.get("charge")):
|
||||
score += 2
|
||||
require_title = str(classify.get("require_title") or "")
|
||||
|
||||
boost = classify.get("duration_boost") or {}
|
||||
gt_60 = int(boost.get("gt_60min", 2))
|
||||
gt_30 = int(boost.get("gt_30min", 1))
|
||||
if duration_seconds > 60 * 60:
|
||||
score += gt_60
|
||||
elif duration_seconds > 30 * 60:
|
||||
score += gt_30
|
||||
reaction_matched = bool(matched_words(title, classify.get("reaction")))
|
||||
reaction_hit = reaction_matched and (
|
||||
not require_title or passes_required_title(title, require_title)
|
||||
)
|
||||
if reaction_hit:
|
||||
score += REACTION_SCORE
|
||||
|
||||
if contains_any(title, classify.get("negative")):
|
||||
score -= 5
|
||||
charge_hits = matched_words(title, classify.get("charge"))
|
||||
if len(charge_hits) >= 2:
|
||||
score += CHARGE_MULTI_SCORE
|
||||
elif len(charge_hits) == 1:
|
||||
score += CHARGE_SINGLE_SCORE
|
||||
|
||||
if matched_words(title, classify.get("weak_positive")):
|
||||
score += WEAK_SCORE
|
||||
|
||||
if reaction_hit or charge_hits:
|
||||
boost = classify.get("duration_boost") or {}
|
||||
gt_60 = int(boost.get("gt_60min", 2))
|
||||
gt_30 = int(boost.get("gt_30min", 1))
|
||||
if duration_seconds > 60 * 60:
|
||||
score += gt_60
|
||||
elif duration_seconds > 30 * 60:
|
||||
score += gt_30
|
||||
|
||||
if matched_words(title, classify.get("negative")):
|
||||
score -= NEGATIVE_SCORE
|
||||
if classify.get("date_negative", True) and date_hits(title):
|
||||
score -= DATE_SCORE
|
||||
if up_mid is not None and int(up_mid) == int(classify.get("self_mid", 37754047)):
|
||||
score -= 3
|
||||
score -= SELF_SCORE
|
||||
return score
|
||||
+14
-11
@@ -25,12 +25,11 @@ from .client_ip import (
|
||||
DEFAULT_CLASSIFY: dict[str, Any] = {
|
||||
"require_title": "咻咻满",
|
||||
"self_mid": 37754047,
|
||||
"positive": [
|
||||
"reaction", "re", "反应", "看", "听", "初听", "首次",
|
||||
"点评", "锐评", "路人", "震惊", "双人",
|
||||
],
|
||||
"charge": ["充电", "加急", "专场"],
|
||||
"negative": ["歌切", "切片", "纯享", "搬运", "完整版"],
|
||||
"reaction": ["reaction", "反应"],
|
||||
"charge": ["加急", "特急", "充电", "专场", "转场"],
|
||||
"weak_positive": ["双人", "路人", "第一次", "首次"],
|
||||
"negative": ["歌切", "切片", "纯享", "搬运", "完整版", "录播", "录像"],
|
||||
"date_negative": True,
|
||||
"duration_boost": {"gt_30min": 1, "gt_60min": 2},
|
||||
}
|
||||
|
||||
@@ -87,11 +86,11 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
||||
"candidate_ttl_days": 7,
|
||||
},
|
||||
"discover": {
|
||||
"keywords": ["咻咻满 reaction", "咻咻满 充电"],
|
||||
"pages": 2,
|
||||
"window_days": 14,
|
||||
"keywords": ["咻咻满"],
|
||||
"pages": 3,
|
||||
"window_days": 7,
|
||||
},
|
||||
"notify": {"min_score": 2, "max_items_per_message": 10},
|
||||
"notify": {"min_score": 4, "max_items_per_message": 10},
|
||||
"classify": DEFAULT_CLASSIFY,
|
||||
"heat_weights": DEFAULT_HEAT_WEIGHTS,
|
||||
"analytics": DEFAULT_ANALYTICS,
|
||||
@@ -500,8 +499,12 @@ def _build_config(data: Mapping[str, Any], path: Path) -> AppConfig:
|
||||
classify["self_mid"] = _as_int(
|
||||
classify.get("self_mid", 37754047), "classify.self_mid", minimum=1
|
||||
)
|
||||
for key in ("positive", "charge", "negative"):
|
||||
for key in ("reaction", "charge", "weak_positive", "negative"):
|
||||
classify[key] = _as_str_list(classify.get(key, []), f"classify.{key}")
|
||||
date_negative = classify.get("date_negative", True)
|
||||
if not isinstance(date_negative, bool):
|
||||
raise ConfigError("classify.date_negative 必须是布尔值")
|
||||
classify["date_negative"] = date_negative
|
||||
boost = classify.get("duration_boost", {})
|
||||
if not isinstance(boost, Mapping):
|
||||
raise ConfigError("classify.duration_boost 必须是对象")
|
||||
|
||||
Reference in new issue
Block a user