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:
kairee committed 2026-11-01 19:00:48 +08:00
1 parent e04908ccad
commit 742c67a1c3
9 files changed
+210 -107

No files matched your search

+71 -20
View File
@@ -10,6 +10,8 @@ sys.stdout.reconfigure(encoding="utf-8")
from reaction_board.classify import ( # noqa: E402
clean_title,
contains_any,
date_hits,
matched_words,
normalize_title,
parse_duration,
passes_required_title,
@@ -19,13 +21,22 @@ from reaction_board.classify import ( # noqa: E402
CLASSIFY = {
"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},
}
def score(title, up_mid=100, duration=0, classify=CLASSIFY):
return score_item(
title=title, up_mid=up_mid,
duration_seconds=duration, classify=classify,
)
def test_title_helpers():
assert normalize_title("咻咻满 REACTION") == "咻咻满 reaction"
assert clean_title('<em class="keyword">咻咻满</em> reaction') == "咻咻满 reaction"
@@ -34,6 +45,8 @@ def test_title_helpers():
assert not passes_required_title("其他 UP 的 reaction", "咻咻满")
assert contains_any("咻咻满 REACTION", ["reaction"])
assert not contains_any("咻咻满", [])
assert matched_words("咻咻满 REACTION 反应", ["reaction", "反应"]) == ["reaction", "反应"]
assert matched_words("咻咻满", []) == []
def test_parse_duration():
@@ -46,32 +59,70 @@ def test_parse_duration():
assert parse_duration(-5) == 0
def test_score_rules():
def score(title, up_mid=100, duration=0):
return score_item(
title=title, up_mid=up_mid,
duration_seconds=duration, classify=CLASSIFY,
)
def test_date_hits():
assert date_hits("咻咻满-2026-10-28-素颜满") == ["2026-10-28"]
assert date_hits("【咻咻满】10月25日直播片段") == ["10月25日"]
assert date_hits("咻咻满 ☆ 2026.10.26") == ["2026.10.26"]
assert date_hits("【咻咻满】2026/10/25直播回放") == ["2026/10/25"]
assert date_hits("咻咻满:《发如雪》2024年10月5日直播歌切") == ["2024年10月5日"]
assert date_hits("咻咻满 reaction") == []
assert score("咻咻满 reaction", duration=60) == 2
assert score("咻咻满 充电", duration=60) == 2
def test_score_strong_reaction_requires_title():
# 强正向 reaction 只在标题含 require_title 时成立。
assert score("咻咻满 reaction", duration=60) == 3
assert score("咻咻满 反应", duration=60) == 3
assert score("其他 UP 的 reaction", duration=61 * 60) == 0
def test_score_charge_levels():
assert score("咻咻满 充电", duration=60) == 1
assert score("咻咻满 加急专场", duration=60) == 3
assert score("咻咻满 reaction 充电", duration=60) == 4
assert score("咻咻满 reaction", duration=61 * 60) == 4
assert score("咻咻满 reaction", duration=31 * 60) == 3
assert score("咻咻满 reaction", duration=30 * 60) == 2
assert score("咻咻满 切片", duration=60) == -5
assert score("咻咻满 reaction 加急充电专场", duration=60) == 6
def test_score_weak_positive():
assert score("咻咻满 双人", duration=60) == 1
assert score("咻咻满 reaction 双人", duration=60) == 4
def test_duration_only_counts_with_signal():
assert score("咻咻满 reaction", duration=61 * 60) == 5
assert score("咻咻满 reaction", duration=31 * 60) == 4
assert score("咻咻满 reaction", duration=30 * 60) == 3
# 无 reaction / charge 信号时,时长不给分(避免纯长录像进池)。
assert score("咻咻满", duration=61 * 60) == 0
assert score("咻咻满 双人", duration=61 * 60) == 1
def test_score_negative_and_self():
assert score("咻咻满 切片", duration=60) == -6
assert score("咻咻满 reaction 切片", duration=60) == -3
assert score("咻咻满 reaction", up_mid=37754047, duration=60) == -1
assert score("咻咻满", duration=0) == 0
assert score("咻咻满", duration=61 * 60) == 2
assert score("咻咻满 reaction 直播录像", duration=60) == -3
assert score("咻咻满 reaction", up_mid=37754047, duration=60) == 0
def test_score_date_negative():
assert score("咻咻满 reaction 2026-10-28 完整录播", duration=60) == -9
assert score("咻咻满 reaction 2026-10-28", duration=60) == -3
assert score("咻咻满 充电 10月25日", duration=60) == -5
disabled = dict(CLASSIFY, date_negative=False)
assert score("咻咻满 reaction 2026-10-28", duration=60, classify=disabled) == 3
def main():
test_title_helpers()
test_parse_duration()
test_score_rules()
test_date_hits()
test_score_strong_reaction_requires_title()
test_score_charge_levels()
test_score_weak_positive()
test_duration_only_counts_with_signal()
test_score_negative_and_self()
test_score_date_negative()
print("OK: 候选标题归一化、时长解析与分类打分测试全部通过")
if __name__ == "__main__":
main()
main()
+4 -2
View File
@@ -54,9 +54,11 @@ def test_dual_accounts_and_env_override():
assert (cfg.bilibili.write.mid, cfg.bilibili.write.csrf, cfg.bilibili.write.cookie) == (202, "env-write", "env-write-cookie")
assert cfg.storage.database_path == Path(tmp) / "custom.db"
assert cfg.up_blacklist_seed == (1, 2)
assert cfg.discover.keywords == ("咻咻满 reaction", "咻咻满 充电")
assert cfg.discover.pages == 2
assert cfg.discover.keywords == ("咻咻满",)
assert cfg.discover.pages == 3
assert cfg.classify["self_mid"] == 37754047
assert cfg.classify["reaction"] == ["reaction", "反应"]
assert cfg.classify["date_negative"] is True
def test_flat_and_missing_account_rejected():
+4 -2
View File
@@ -40,12 +40,14 @@ def write_config(tmp, **discover):
"classify": {
"require_title": "咻咻满",
"self_mid": 37754047,
"positive": ["reaction"],
"reaction": ["reaction"],
"charge": ["充电"],
"weak_positive": [],
"negative": ["切片"],
"date_negative": True,
"duration_boost": {"gt_30min": 1, "gt_60min": 2},
},
"notify": {"min_score": 2, "max_items_per_message": 10},
"notify": {"min_score": 3, "max_items_per_message": 10},
}
path = Path(tmp) / "config.json"
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")