Files
re-xxm/tests/test_classify.py
kairee c6790f0ba1 feat(classify): zero title-only titles, penalize <10min, add discover.candidate_min_score
标题仅含 require_title 不再计分(必须咻咻满+任一正向词);时长 <10 分钟 -1(时长未知不扣);新增 discover.candidate_min_score(默认=notify.min_score)控制是否进候选池,只过标题关的不再入池。库内 796 条:仅过标题关的 8 条从 1/3 分降为 0/-1,其余不变。
2026-11-01 20:18:43 +08:00

179 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""候选分类纯函数测试: uv run python tests/test_classify.py"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
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,
score_item,
)
CLASSIFY = {
"require_title": "咻咻满",
"self_mid": 37754047,
"reaction": ["re", "reaction", "反应"],
"charge": ["加急", "特急", "充电", "专场"],
"weak_positive": ["双人", "路人", "第一次", "首次"],
"negative": ["歌切", "切片", "搬运", "完整版", "录播", "录像"],
"date_negative": True,
"duration_boost": {"gt_30min": 1, "gt_60min": 2},
}
NEUTRAL = 600 # 10min:不触发 <10min 惩罚,也不触发 >30min 加权
SHORT = 60 # 1min:<10min
MID = 31 * 60 # >30min
LONG = 61 * 60 # >60min
def score(title, up_mid=100, duration=NEUTRAL, 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"
assert passes_required_title("【咻咻满】初听", "咻咻满")
assert passes_required_title("咻咻满", "咻咻满")
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():
assert parse_duration("105:5") == 105 * 60 + 5
assert parse_duration("1:02:03") == 3723
assert parse_duration("15:30") == 930
assert parse_duration(600) == 600
assert parse_duration("bad") == 0
assert parse_duration(None) == 0
assert parse_duration(-5) == 0
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") == []
def test_title_only_scores_zero():
# 只过标题关(标题仅含 require_title、无任何正向词)不给分,时长也不计入
assert score("咻咻满") == 0
assert score("咻咻满", duration=LONG) == 0
assert score("咻咻满 日常") == 0
assert score("【咻咻满】交换余生(升2)") == 0
# 但时长未知(0) vs <10min 的惩罚不同
assert score("咻咻满", duration=0) == 0
assert score("咻咻满", duration=SHORT) == -1
def test_hit_count_is_superlinear():
# 正向命中次数 n:require_title(仅在有正向命中时计 1)+ reaction(0/1) + charge 词数 + weak 词数
assert score("咻咻满 reaction") == 4
assert score("咻咻满 反应") == 4
assert score("咻咻满 reaction 充电") == 9
assert score("咻咻满 reaction 加急充电专场") == 25
assert score("咻咻满 充电") == 4
assert score("咻咻满 加急专场") == 9
assert score("咻咻满 双人") == 4
assert score("咻咻满 reaction 双人") == 9
def test_reaction_typo_and_gate():
# "re" 兼容 reaction/reactio/reation 等拼写
assert score("咻咻满 reactio") == 4
assert score("咻咻满 Reactuon 充电") == 9
# 标题不含 require_title 直接 0 分
assert score("其他 UP 的 reaction", duration=LONG) == 0
assert score("普通视频 reactio", duration=LONG) == 0
def test_duration_boost_and_short_penalty():
assert score("咻咻满 reaction", duration=LONG) == 6
assert score("咻咻满 reaction", duration=MID) == 5
assert score("咻咻满 reaction", duration=30 * 60) == 4
assert score("咻咻满 reaction", duration=NEUTRAL) == 4
# 时长 <10 分钟 -1
assert score("咻咻满 reaction", duration=SHORT) == 3
assert score("咻咻满 reaction 充电", duration=SHORT) == 8
assert score("咻咻满 双人", duration=SHORT) == 3
# 时长未知(0) 不扣
assert score("咻咻满 reaction", duration=0) == 4
def test_negative_single_and_multi():
# 单个负向词 -2("咻咻满 切片" 无正向命中,基础为 0)
assert score("咻咻满 切片") == -2
assert score("咻咻满 reaction 切片") == 2
assert score("咻咻满 reaction 直播录像") == 2
# 2 个负向词:-2*2 -2 = -6
assert score("咻咻满 歌切 切片") == -6
# 3 个负向词:-2*3 -6 = -12
assert score("咻咻满 reaction 歌切 切片 完整版") == -8
def test_self_penalty():
assert score("咻咻满 reaction", up_mid=37754047) == 1
def test_date_negative():
assert score("咻咻满 reaction 2026-10-28 完整录播") == -2
assert score("咻咻满 reaction 2026-10-28") == 0
assert score("咻咻满 充电 10月25日") == 0
disabled = dict(CLASSIFY, date_negative=False)
assert score("咻咻满 reaction 2026-10-28", classify=disabled) == 4
def test_removed_words():
# 纯享不再算负向
assert score("咻咻满 reaction 纯享受") == 4
# 转场不再算正向(作为错别字忽略),且只过标题关不给分
assert score("咻咻满 转场") == 0
def test_multi_hit_survives_single_negative():
# 回归:命中 咻咻满+reaction+充电 的 204min reaction 标题,扣掉单个「切片」后仍应过线
title = "【充电2.0 | 咻咻满reaction】翻唱和直播切片完全是在开盲盒呀!不知道今天会开到哪一款"
assert score(title, duration=204 * 60) == 9
assert score(title, duration=NEUTRAL) == 7
def main():
test_title_helpers()
test_parse_duration()
test_date_hits()
test_title_only_scores_zero()
test_hit_count_is_superlinear()
test_reaction_typo_and_gate()
test_duration_boost_and_short_penalty()
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: 候选标题归一化、时长解析与分类打分测试全部通过")
if __name__ == "__main__":
main()