feat(classify): gate loose re token, non-linear negatives, drop 纯享/转场

reaction 词表用 re|reaction|反应 但须与咻咻满或充电同现,兼容 reactio/reation 等拼写又避免通用词加分;负向改为每词 -2 且命中多条额外叠加(k*(k-1)),日期 -4;纯享(误伤纯享受)移出负向,转场移出正向。库内 796 条有效视频复测:线以下由 14 降至 8。
This commit is contained in:
kairee committed 2026-11-01 19:18:20 +08:00
1 parent 337e379514
commit aa9aa7c589
8 files changed
+89 -52

No files matched your search

+43 -20
View File
@@ -21,10 +21,10 @@ from reaction_board.classify import ( # noqa: E402
CLASSIFY = {
"require_title": "咻咻满",
"self_mid": 37754047,
"reaction": ["reaction", "反应"],
"charge": ["加急", "特急", "充电", "专场", "转场"],
"reaction": ["re", "reaction", "反应"],
"charge": ["加急", "特急", "充电", "专场"],
"weak_positive": ["双人", "路人", "第一次", "首次"],
"negative": ["歌切", "切片", "纯享", "搬运", "完整版", "录播", "录像"],
"negative": ["歌切", "切片", "搬运", "完整版", "录播", "录像"],
"date_negative": True,
"duration_boost": {"gt_30min": 1, "gt_60min": 2},
}
@@ -69,8 +69,7 @@ def test_date_hits():
def test_hit_count_is_superlinear():
# 正向命中次数 n:require_title + reaction + charge + weak,得分 = n^2
# 1 -> 1, 2 -> 4, 3 -> 9(满足 1+1>2、1+1+1>5)
# 正向命中次数 n:require_title + reaction(0/1) + charge 词数 + weak 词数,得分 = n^2
assert score("咻咻满", duration=60) == 1
assert score("咻咻满 reaction", duration=60) == 4
assert score("咻咻满 反应", duration=60) == 4
@@ -82,41 +81,59 @@ def test_hit_count_is_superlinear():
assert score("咻咻满 reaction 双人", duration=60) == 9
def test_reaction_requires_title():
# 标题不含 require_title 直接 0 分,即使命中 reaction
def test_reaction_typo_and_gate():
# "re" 兼容 reaction/reactio/reation 等拼写
assert score("咻咻满 reactio", duration=60) == 4
assert score("咻咻满 Reactuon 充电", duration=60) == 9
# 标题不含 require_title 直接 0 分
assert score("其他 UP 的 reaction", duration=61 * 60) == 0
assert score("普通视频 reactio", duration=61 * 60) == 0
def test_duration_boost():
assert score("咻咻满 reaction", duration=61 * 60) == 6
assert score("咻咻满 reaction", duration=31 * 60) == 5
assert score("咻咻满 reaction", duration=30 * 60) == 4
# 只有 require_title 一个命中时,时长仍计入(基础分偏低,不足以过线)
assert score("咻咻满", duration=0) == 1
assert score("咻咻满", duration=61 * 60) == 3
assert score("咻咻满 双人", duration=61 * 60) == 6
def test_negative_and_self():
assert score("咻咻满 切片", duration=60) == -5
assert score("咻咻满 reaction 切片", duration=60) == -2
assert score("咻咻满 reaction 直播录像", duration=60) == -2
def test_negative_single_and_multi():
# 单个负向词 -2
assert score("咻咻满 切片", duration=60) == -1
assert score("咻咻满 reaction 切片", duration=60) == 2
assert score("咻咻满 reaction 直播录像", duration=60) == 2
# 2 个负向词:-2*2 -2 = -6
assert score("咻咻满 歌切 切片", duration=60) == -5
# 3 个负向词:-2*3 -6 = -12
assert score("咻咻满 reaction 歌切 切片 完整版", duration=60) == -8
def test_self_penalty():
assert score("咻咻满 reaction", up_mid=37754047, duration=60) == 1
def test_date_negative():
assert score("咻咻满 reaction 2026-10-28 完整录播", duration=60) == -8
assert score("咻咻满 reaction 2026-10-28", duration=60) == -2
assert score("咻咻满 充电 10月25日", duration=60) == -2
assert score("咻咻满 reaction 2026-10-28 完整录播", duration=60) == -2
assert score("咻咻满 reaction 2026-10-28", duration=60) == 0
assert score("咻咻满 充电 10月25日", duration=60) == 0
disabled = dict(CLASSIFY, date_negative=False)
assert score("咻咻满 reaction 2026-10-28", duration=60, classify=disabled) == 4
def test_removed_words():
# 纯享不再算负向
assert score("咻咻满 reaction 纯享受", duration=60) == 4
# 转场不再算正向(作为错别字忽略)
assert score("咻咻满 转场", duration=60) == 1
def test_multi_hit_survives_single_negative():
# 回归:命中 咻咻满+reaction+充电 的 204min reaction 标题,扣掉「切片」后仍应过线
# 回归:命中 咻咻满+reaction+充电 的 204min reaction 标题,扣掉单个「切片」后仍应过线
title = "【充电2.0 | 咻咻满reaction】翻唱和直播切片完全是在开盲盒呀!不知道今天会开到哪一款"
assert score(title, duration=204 * 60) == 5
assert score(title, duration=60) == 3
assert score(title, duration=204 * 60) == 9
assert score(title, duration=60) == 7
def main():
@@ -124,11 +141,17 @@ def main():
test_parse_duration()
test_date_hits()
test_hit_count_is_superlinear()
test_reaction_requires_title()
test_reaction_typo_and_gate()
test_duration_boost()
test_negative_and_self()
test_negative_single_and_multi()
test_self_penalty()
test_date_negative()
test_removed_words()
test_multi_hit_survives_single_negative()
# 直播回放示例:命中 require+专场+路人+第一次 = 4 -> 16;39min > 30 -> +1 = 17
title = "6【直播回放】咻咻满专场 路人第一次听满满唱歌 真的超级绝超级好听耶~"
assert score(title, duration=39 * 60) == 17
print("OK: 候选标题归一化、时长解析与分类打分测试全部通过")
+1 -1
View File
@@ -57,7 +57,7 @@ def test_dual_accounts_and_env_override():
assert cfg.discover.keywords == ("咻咻满",)
assert cfg.discover.pages == 3
assert cfg.classify["self_mid"] == 37754047
assert cfg.classify["reaction"] == ["reaction", "反应"]
assert cfg.classify["reaction"] == ["re", "reaction", "反应"]
assert cfg.classify["date_negative"] is True
+2 -2
View File
@@ -128,10 +128,10 @@ def test_filters_and_scoring():
assert summary.title_miss == 1
assert summary.old == 1
assert summary.blacklisted == 1
assert summary.candidates == 1 # aid 6
assert summary.candidates == 2 # aid 6(1 分)与 aid 2(2 分)
assert summary.notified == 0
assert repo.get_candidate(6)["score"] == 1
assert repo.get_candidate(2) is None
assert repo.get_candidate(2)["score"] == 2 # reaction 命中后单个「切片」不再清零
assert repo.get_candidate(4) is None
assert repo.get_candidate(5) is None
assert notifier.batches == []