feat: 转写支持静音段幻觉抑制参数(HST)

无 VAD 的 decode_full 会在无语音段"编"出字幕(`こんにちは`/`おはようございます`/
`東京都交通局8800形電車`)。实测这类幻觉与"呻吟间隙里的真实短台词"在
`no_speech_prob`、`avg_logprob`、silero VAD 与音频能量四个维度上都不可分,
只能用 faster-whisper 自带的 `hallucination_silence_threshold`(HST)抑制:
怀疑幻觉时跳过超过阈值的静音部分(需 `word_timestamps=True`)。

- `nodes/whisper.py` 透传 `word_timestamps` / `hallucination_silence_threshold` /
  `no_speech_threshold` / `log_prob_threshold` / `compression_ratio_threshold`,
  未配置的键不传,保持 faster-whisper 默认值与改造前行为。
- `learn-translate` 默认 HST=2.0 + word_timestamps:同一片头 120 秒无对话段由 15 条
  字幕降到 4 条且无套话幻觉残留,呻吟段基本保留;代价转写约慢 1.8×。
- 实测数据与取舍记录见 docs/workflows.md#decode_full-与幻觉呻吟清洗。
This commit is contained in:
2026-09-18 23:41:32 +08:00
parent 4d2823c005
commit 2c8bbb6469
4 changed files with 86 additions and 0 deletions
+12
View File
@@ -45,6 +45,18 @@ Expecting value: line 1 column 1 (char 0)`),也会直接渲染成超长字
展示时长 ≥15s 且同一 1–6 字单元连续重复 ≥6 次且覆盖正文 ≥70%;真实短促呻吟
`ぇ`×15、`ああああああ`)靠时长区分,零误删。VAD 开关都会生效。
**静音段幻觉抑制**2026-09):decode_full 的固有副作用是"无语音段照样写字幕"——
whisper 会在静音/音乐段输出 `こんにちは``おはようございます``東京都交通局8800形電車`
这类短幻觉。实测这类幻觉与"呻吟间隙里的真实短台词"在 `no_speech_prob``avg_logprob`
silero VAD 与音频能量四个维度上**都不可分**(数据见 `data/experiments/regen_plan/REPORT.md`),
所以走 faster-whisper 自带的 `hallucination_silence_threshold`HST,需
`word_timestamps=True`):怀疑该段是幻觉时,跳过超过阈值的静音部分。learn-translate
默认 `hallucination_silence_threshold=2.0` + `word_timestamps=true`:同一片头 120 秒
无对话段由 15 条字幕降到 4 条(无套话幻觉残留),呻吟段基本保留;代价是转写约慢 1.8×。
参数由工作流直接传给节点(`_guard_transcribe_params` 透传 `word_timestamps` /
`hallucination_silence_threshold` / `no_speech_threshold` / `log_prob_threshold` /
`compression_ratio_threshold`,未配置的键保持 faster-whisper 默认值)。
调研过程与结论见 [调研-whisper漏句与decode_full验证.md](./调研-whisper漏句与decode_full验证.md)。
## 切换模型不改代码
+20
View File
@@ -170,6 +170,24 @@ def _wav_duration_seconds(path: Path, fallback: float) -> float:
return fallback
# 静音段幻觉抑制参数:无 VAD 的整段解码会在无语音处"编"出字幕,这些参数直接
# 交给 faster-whisper(不传时保持其内置默认值,行为与改造前一致)。其中
# `hallucination_silence_threshold` 需要 `word_timestamps=True` 才生效:它按词级
# 时间戳跳过幻觉段里的静音部分,是"无语音段别写字幕"的主要开关。
_GUARD_PARAM_KEYS = (
"word_timestamps",
"hallucination_silence_threshold",
"no_speech_threshold",
"log_prob_threshold",
"compression_ratio_threshold",
)
def _guard_transcribe_params(params: dict) -> dict:
"""挑出工作流显式传入的幻觉抑制参数,未传的键交给 faster-whisper 默认值。"""
return {key: params[key] for key in _GUARD_PARAM_KEYS if params.get(key) is not None}
def _append_srt_lines(lines: list[str], segments, offset: float, start_index: int) -> int:
"""把一段转写结果按 SRT 格式追加到 lines,时间加上 offset 偏移。
@@ -287,6 +305,8 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
condition_on_previous_text=bool(
request.params.get("condition_on_previous_text", False)
),
# 静音段幻觉抑制(未配置时不传,保持默认行为)。
**_guard_transcribe_params(request.params),
)
# 进度日志:块序号/总数、单块耗时、实时倍率(块音频时长/墙钟耗时)
# 与转写累计耗时,直观反映数据处理速度。
@@ -565,3 +565,53 @@ def test_invoke_drops_repetition_artifact_in_decode_full(tmp_path: Path, monkeyp
content = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8")
assert artifact not in content
assert "そこ、だめ" in content
def test_invoke_passes_hallucination_guard_params(tmp_path: Path, monkeypatch) -> None:
"""数据:工作流传入幻觉抑制参数(HST + word_timestamps + 两个阈值)。
过程:调用 invoke。
验证:参数原样传给 faster-whisper——静音段幻觉只能靠这些参数抑制:
`hallucination_silence_threshold` 需要 word_timestamps 才生效。
"""
# 数据:无语音段也能"编"出字幕的假模型 + 显式传入的抑制参数。
model = FakeModel([FakeSegment(0.0, 2.0, "こんにちは")])
_inject_model(monkeypatch, model)
# 测试过程
invoke(_request(
tmp_path, SPEECH_WAV, chunk_seconds=0, language="ja",
word_timestamps=True, hallucination_silence_threshold=2.0,
no_speech_threshold=0.3, log_prob_threshold=-1.2,
compression_ratio_threshold=2.4,
))
# 验证结果
call = model.calls[0]
assert call["word_timestamps"] is True
assert call["hallucination_silence_threshold"] == 2.0
assert call["no_speech_threshold"] == 0.3
assert call["log_prob_threshold"] == -1.2
assert call["compression_ratio_threshold"] == 2.4
def test_invoke_omits_guard_params_by_default(tmp_path: Path, monkeypatch) -> None:
"""数据:不传抑制参数(默认工作流)。
过程:调用 invoke。
验证:不透传这些键,保持 faster-whisper 自身默认值,行为与改造前一致。
"""
# 数据:普通假模型。
model = FakeModel([FakeSegment(0.0, 1.0, "x")])
_inject_model(monkeypatch, model)
# 测试过程
invoke(_request(tmp_path, SPEECH_WAV, chunk_seconds=0))
# 验证结果
call = model.calls[0]
assert "hallucination_silence_threshold" not in call
assert "no_speech_threshold" not in call
assert "word_timestamps" not in call
+4
View File
@@ -31,6 +31,10 @@
"condition_on_previous_text": false,
"chunk_seconds": 60,
"vad_filter": false,
"word_timestamps": true,
"hallucination_silence_threshold": 2.0,
"_note_word_timestamps": "开启词级时间戳:hallucination_silence_threshold 只在该模式下生效(它按词级时间戳跳过幻觉段里的静音部分)。代价是解码更慢(实测同段音频约 1.8×),换来的是无语音段不再产生\"こんにちは/おはようございます\"这类套话幻觉。",
"_note_hallucination_silence_threshold": "静音段幻觉抑制:怀疑该段是幻觉时,跳过超过 2 秒的静音。实测片头无对话段的字幕条数由 15 降到 4、且不再出现套话幻觉,呻吟段基本保留。设 0 或删除该键即关闭(回到改造前行为)。",
"beam_size": 1,
"_note_decode_full": "【本次修复核心】decode_full=true 强制无 VAD 整段解码,绕过 silero VAD 对呻吟/轻语/快速讲解/BGM 混叠人声的切段误杀。实测 savr-1054 全片:生产 VAD 仅召回 115 条,decode_full 召回 340 条(弱语音全找回)。正例:教师快速带过一句'つまりね'(弱语音),decode_full 能捕捉;反例(decode_full=false 默认):该句被 silero 概率<阈值当静音剔除,字幕整句消失。代价是无语音段会产生长时'おやすみなさい/ご視聴ありがとうございました'幻觉——处理方式:whisper 转录后立即**连带时间戳把整条 cue 删除**(不留下 '-' 占位污染下游,占位会渲染进 ASS 成减号),短时(≤15s)相同词可能是剧情真实道晚安则保留。实测 speech_60s:前 30s 无语音幻觉被整条剔除,字幕直接从 30s 真实内容开始、序号连续。",
"_note_vad_filter": "本工作流 decode_full=true 时 vad_filter 被强制置 false(两者互斥,decode_full 优先)。保留 vad_filter=false 仅为显式声明'不启用 VAD 切段'。正例:学习视频讲解者偶有停顿、翻页声,无 VAD 不误删。反例:vad_filter=true + 讲解者语速快/带气声,弱音节被整段吞掉(见 decode_full 反例)。",