两部分同属字幕质量优化,共用 llm.py 翻译链路:
1) 长时寒暄幻觉词清洗(nodes/subtitle_cleanup.py)
对展示时长超过阈值(默认 15s,实测 30s 幻觉占位 vs 2s 真实词的分界)
且文本含收尾/开场寒暄(晚安、感谢观看等)的字幕,文本替换为 '-',
由后续过滤流程移除;短时寒暄(如剧情中真实互道晚安)保留不误删。
写文件前调用 。
2) 专名/隐语不直译(nodes/proper_nouns.py)
片假名专名(人名/品牌/角色)与成人语境隐语是 LLM 误译重灾区:
- ジンゴ 被误译成'芒果'、カンタくん 被保留日文而非音译;
- マンゴー/バナナ/リンゴ/金玉/おまんこ/ちんちん 等在色情语境中
是生殖器官代称,字面直译严重错译。
整理三张规则表(专名/拟声词/成人隐语,用户提供隐语表),
检测原文命中后动态注入翻译提示词,
让 LLM 按正确语义处理。文档见 docs/proper_nouns.md。
测试(红→绿):
- tests/test_hallucination_mask.py:长时掩码/短时保留/非寒暄保留/阈值边界
- tests/test_proper_nouns.py:专名命中/拟声词/成人隐语/真实数据集成
- 真实 LLM 集成测试(完整 1440 行翻译 + 清洗 + 专名注入)通过
87 lines
3.5 KiB
Python
87 lines
3.5 KiB
Python
"""长时寒暄幻觉词清洗测试(先红后绿)。
|
||
|
||
背景(实测 run 20260905115050):修复时间对齐后,字幕仍残留四类问题,
|
||
其中"寒暄/收尾幻觉词"最具确定性、可规则化:
|
||
|
||
- 产物里 '晚安 / 感谢观看 / 感谢收看 / 感谢您的观看' 等固定套话出现 36 次;
|
||
- 其中 **33 条展示时长 = 30s(整块占满)**,明显是 ASR/LLM 对无内容段
|
||
的音量幻觉占位,与视频内容毫无关系;
|
||
- 仅 2 条时长 ~2s(如 720.00-722.00 '晚安')可能是剧情里真的说了"晚安",
|
||
属于真实内容,不应误删。
|
||
|
||
方案(用户确认):日文转译完成后,对**展示时长过长**(≥阈值)且文本匹配
|
||
寒暄词表的条目,把文本替换为 '-' 占位,由后续处理(SRT/过滤流程)移除。
|
||
这样既清掉幻觉占位,又用"时长阈值"保住可能为真实对话的短时寒暄词。
|
||
|
||
阈值从本次真实运行实测数据判定: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,
|
||
mask_hallucination_text,
|
||
)
|
||
|
||
|
||
def _mk(start: float, end: float, text: str) -> dict:
|
||
"""构造一个字幕条目(测试辅助)。"""
|
||
return {"start": start, "end": end, "text": text}
|
||
|
||
|
||
def test_long_hallucination_masked() -> None:
|
||
"""30s 的'晚安/感谢观看'(幻觉占位)必须被替换为 '-'。"""
|
||
entries = [
|
||
_mk(60.0, 90.0, "晚安"),
|
||
_mk(90.0, 120.0, "感谢您的观看"),
|
||
_mk(1260.0, 1289.98, "感谢您的观看"),
|
||
]
|
||
out = mask_hallucination_text(entries)
|
||
assert all(e["text"] == "-" for e in out)
|
||
|
||
|
||
def test_short_hallucination_preserved() -> None:
|
||
"""2s 的'晚安'(可能为剧情真实对话)必须保留,不误删。"""
|
||
entries = [
|
||
_mk(720.0, 722.0, "晚安"),
|
||
_mk(238.0, 240.0, "非常感谢您的观看。"),
|
||
]
|
||
out = mask_hallucination_text(entries)
|
||
assert out[0]["text"] == "晚安"
|
||
assert out[1]["text"] == "非常感谢您的观看。"
|
||
|
||
|
||
def test_non_hallucination_always_preserved() -> None:
|
||
"""普通内容(即使很长)绝不能被当成寒暄幻觉处理。"""
|
||
entries = [
|
||
_mk(0.0, 30.0, "今天我将为您提供精神调适服务"),
|
||
_mk(10.0, 40.0, "请尽量放松,无论多少次都能感到舒适愉悦"),
|
||
]
|
||
out = mask_hallucination_text(entries)
|
||
assert out[0]["text"] == "今天我将为您提供精神调适服务"
|
||
assert out[1]["text"] == "请尽量放松,无论多少次都能感到舒适愉悦"
|
||
|
||
|
||
def test_threshold_boundary() -> None:
|
||
"""阈值边界:刚好 ≥ 阈值才清洗;< 阈值保留。"""
|
||
entries = [
|
||
_mk(0.0, 15.0, "晚安"), # 恰好 15s → 清洗(≥ threshold)
|
||
_mk(0.0, 14.99, "晚安"), # 14.99s → 保留
|
||
]
|
||
out = mask_hallucination_text(entries, threshold_seconds=15.0)
|
||
assert out[0]["text"] == "-"
|
||
assert out[1]["text"] == "晚安"
|
||
|
||
|
||
@pytest.mark.integration
|
||
def test_mask_does_not_mutate_input() -> None:
|
||
"""清洗不得修改原始条目对象(纯函数约束)。"""
|
||
entries = [_mk(60.0, 90.0, "晚安")]
|
||
original_text = entries[0]["text"]
|
||
mask_hallucination_text(entries)
|
||
assert entries[0]["text"] == original_text |