# -*- 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('咻咻满 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()