Files
re-xxm/tests/test_classify.py
T
kairee aa9aa7c589 feat(classify): gate loose re token, non-linear negatives, drop 纯享/转场
reaction 词表用 re|reaction|反应 但须与咻咻满或充电同现,兼容 reactio/reation 等拼写又避免通用词加分;负向改为每词 -2 且命中多条额外叠加(k*(k-1)),日期 -4;纯享(误伤纯享受)移出负向,转场移出正向。库内 796 条有效视频复测:线以下由 14 降至 8。
2026-11-01 19:18:20 +08:00

159 lines
6.1 KiB
Python
Raw 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},
}
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"
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_hit_count_is_superlinear():
# 正向命中次数 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
assert score("咻咻满 reaction 充电", duration=60) == 9
assert score("咻咻满 reaction 加急充电专场", duration=60) == 25
assert score("咻咻满 充电", duration=60) == 4
assert score("咻咻满 加急专场", duration=60) == 9
assert score("咻咻满 双人", duration=60) == 4
assert score("咻咻满 reaction 双人", duration=60) == 9
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
assert score("咻咻满", duration=0) == 1
assert score("咻咻满", duration=61 * 60) == 3
assert score("咻咻满 双人", duration=61 * 60) == 6
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) == -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 标题,扣掉单个「切片」后仍应过线
title = "【充电2.0 | 咻咻满reaction】翻唱和直播切片完全是在开盲盒呀!不知道今天会开到哪一款"
assert score(title, duration=204 * 60) == 9
assert score(title, duration=60) == 7
def main():
test_title_helpers()
test_parse_duration()
test_date_hits()
test_hit_count_is_superlinear()
test_reaction_typo_and_gate()
test_duration_boost()
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()