"""字幕幻觉清洗测试(先红后绿)。 背景(实测 run 20260905115050):修复时间对齐后,字幕仍残留四类问题, 其中"寒暄/收尾幻觉词"最具确定性、可规则化: - 产物里 '晚安 / 感谢观看 / 感谢收看 / 感谢您的观看' 等固定套话出现 36 次; - 其中 **33 条展示时长 = 30s(整块占满)**,明显是 ASR/LLM 对无内容段 的音量幻觉占位,与视频内容毫无关系; - 仅 2 条时长 ~2s(如 720.00-722.00 '晚安')可能是剧情里真的说了"晚安", 属于真实内容,不应误删。 方案(用户 2026-09 确认改为**整条剔除**):幻觉识别出后(展示时长 ≥ 阈值 且文本命中寒暄词表),应**连带时间戳把整条字幕 cue 删除**(剩余重新编号), 而不是替换成 '-' 占位——占位会一路流到 ASS 渲染成可见减号,处理位置绕且 不彻底。因此清洗统一为在幻觉产生处(whisper decode_full 转录后 / LLM 翻译 后)直接删除整条。 两类词表: - HALLUCINATION_TOKENS:中文(LLM 翻译产物); - JAPANESE_HALLUCINATION_TOKENS:日文(whisper decode_full 直出)。 阈值从本次真实运行实测数据判定:30s 幻觉占位 vs 2s 真实词,分界明显, 本测试选 threshold=15s(≥15s 才删除;≤15s 保留)。 """ from __future__ import annotations import pytest # 清洗逻辑来自生产模块 nodes/subtitle_cleanup.py(纯函数),测试只 import。 from nodes.subtitle_cleanup import ( DEFAULT_THRESHOLD_SECONDS, HALLUCINATION_TOKENS, clean_japanese_hallucinations, clean_srt_text, ) def test_remove_long_hallucination_whole_cue() -> None: """30s 的'晚安/感谢观看'(幻觉占位)须整条删除:序号+时间轴+文本都消失。""" srt = ( "1\n00:00:00,000 --> 00:00:02,000\n真实内容\n\n" "2\n00:01:00,000 --> 00:01:30,000\n晚安\n\n" "3\n00:02:00,000 --> 00:02:30,000\n感谢您的观看\n\n" "4\n00:03:00,000 --> 00:03:02,000\n继续真实\n\n" ) out = clean_srt_text(srt) # 幻觉条目连带时间戳整条消失。 assert "00:01:00,000 --> 00:01:30,000" not in out assert "晚安" not in out assert "00:02:00,000 --> 00:02:30,000" not in out assert "感谢您的观看" not in out # 真实条目保留且序号重新连续编号(原 1、4 -> 新 1、2)。 assert out.startswith("1\n00:00:00,000 --> 00:00:02,000\n真实内容") assert "2\n00:03:00,000 --> 00:03:02,000\n继续真实" in out def test_remove_short_hallucination_preserved() -> None: """2s 的'晚安'(剧情真实道晚安)必须保留,不误删。""" srt = ( "1\n00:12:00,000 --> 00:12:02,000\n晚安\n\n" "2\n00:12:03,000 --> 00:12:06,000\n明天见\n\n" ) out = clean_srt_text(srt) assert "晚安" in out assert "明天见" in out assert out.count("-->") == 2 def test_remove_non_hallucination_always_preserved() -> None: """普通内容(即使很长)绝不能被当成寒暄幻觉处理。""" srt = ( "1\n00:00:00,000 --> 00:00:25,000\n" "今天我将为您提供精神调适服务\n\n" ) out = clean_srt_text(srt) assert "精神调适" in out def test_remove_threshold_boundary() -> None: """阈值边界:恰好 ≥ 阈值才删除;< 阈值保留。""" srt_ge = "1\n00:00:00,000 --> 00:00:15,000\n晚安\n\n" assert "晚安" not in clean_srt_text(srt_ge) srt_lt = "1\n00:00:00,000 --> 00:00:14,990\n晚安\n\n" assert "晚安" in clean_srt_text(srt_lt) def test_remove_resequences_numbers() -> None: """删除中间 cue 后,剩余条目序号从 1 连续递增(合法 SRT)。""" srt = ( "1\n00:00:00,000 --> 00:00:01,000\n甲\n\n" "2\n00:01:00,000 --> 00:01:30,000\n晚安\n\n" # 幻觉被删 "3\n00:02:00,000 --> 00:02:01,000\n乙\n\n" "4\n00:03:00,000 --> 00:03:01,000\n丙\n\n" ) out = clean_srt_text(srt) lines = [l for l in out.splitlines() if l.strip()] # 重新编号:序号应为 1,2,3 各一次。 import re numbers = [int(l) for l in lines if re.fullmatch(r"\d+", l.strip())] assert numbers == [1, 2, 3] # --------------------------------------------------------------------------- # 日语(ASR 直出)幻觉清洗 —— whisper 节点 decode_full 兜底用 # --------------------------------------------------------------------------- def test_jp_long_hallucination_removed() -> None: """30s 的'おやすみなさい/ご視聴ありがとうございました'(无语音段幻觉)整条删除。""" srt = ( "1\n00:00:00,000 --> 00:00:02,000\n気持ちいい\n\n" "2\n00:00:10,000 --> 00:00:40,000\nおやすみなさい\n\n" "3\n00:00:41,000 --> 00:01:11,000\nご視聴ありがとうございました\n\n" "4\n00:01:12,000 --> 00:01:14,000\nまた明日ね\n\n" ) out = clean_japanese_hallucinations(srt) assert "おやすみなさい" not in out assert "ご視聴ありがとうございました" not in out assert "00:00:10,000 --> 00:00:40,000" not in out assert "気持ちいい" in out assert "また明日ね" in out def test_jp_short_hallucination_preserved() -> None: """2s 的'おやすみなさい'(剧情真实道晚安)须保留,不误删。""" srt = ( "1\n00:12:00,000 --> 00:12:02,000\nおやすみなさい\n\n" "2\n00:12:03,000 --> 00:12:06,000\nまた明日ね\n\n" ) out = clean_japanese_hallucinations(srt) assert "おやすみなさい" in out assert "また明日ね" in out def test_jp_non_hallucination_always_preserved() -> None: """普通长句(即使很长)绝不能被当成日语幻觉处理。""" srt = ( "1\n00:00:00,000 --> 00:00:25,000\n" "今日はお客様のために精神整備を務めさせていただきます\n\n" ) out = clean_japanese_hallucinations(srt) assert "精神整備" in out def test_jp_threshold_15s() -> None: """日语清洗同样遵守 15s 时长阈值:15s 恰好删除,14.99s 保留。""" srt = "1\n00:00:00,000 --> 00:00:15,000\nおやすみなさい\n\n" assert "おやすみなさい" not in clean_japanese_hallucinations(srt) srt2 = "1\n00:00:00,000 --> 00:00:14,990\nおやすみなさい\n\n" assert "おやすみなさい" in clean_japanese_hallucinations(srt2) @pytest.mark.integration def test_jp_middle_removal_resequences() -> None: """删除中间日语幻觉后剩余条目重编号且文本/时间正确对应。""" srt = ( "1\n00:00:00,000 --> 00:00:02,000\nあ\n\n" "2\n00:00:10,000 --> 00:00:40,000\nおやすみなさい\n\n" # 删 "3\n00:00:41,000 --> 00:00:43,000\nい\n\n" ) out = clean_japanese_hallucinations(srt) assert out.count("-->") == 2 assert out.startswith("1\n00:00:00,000 --> 00:00:02,000\nあ") assert "2\n00:00:41,000 --> 00:00:43,000\nい" in out