whisper-transcribe/references/subtitle-timing-deep-dive.md

7.0 KiB
Raw Permalink Blame History

字幕时间轴质量深度解析

本文档记录从 PyTorch 方案stable-ts / whisper-timestamped向 MLX 增强方案迁移的决策过程与技术细节。

为什么不用 stable-ts / whisper-timestamped

框架 底层依赖 与 mlx-whisper 兼容性
stable-ts PyTorch + openai-whisper 不兼容
whisper-timestamped PyTorch + openai-whisper 不兼容
mlx-whisper 增强 MLX 框架 原生兼容

这两个工具都是 PyTorch 生态的插件,直接修改/包装的是 OpenAI 官方 Whisper 模型。mlx-whisper 使用的是 MLX 框架和不同的模型格式,两者推理代码完全不同,无法混用。

如果硬要引入 stable-ts意味着

  • 安装 PyTorch + openai-whisper~2GB 额外依赖)
  • 放弃 Apple Silicon 的 MLX 加速
  • turbo 模型在 PyTorch 上跑得更慢

mlx-whisper 内建方案word_timestamps

mlx-whisper 本身支持 word_timestamps=True,只是默认关闭。打开后可以获得每个词的精确起止时间,用来:

  1. 更精准的 SRT 断句(不在词中间切断)
  2. 检测长停顿,自动拆分句子
  3. 修复"黏连"(一句字幕占好几秒不消失)

出现黏连的根因

Whisper 的默认输出是 segment-level,一个 segment 可能包含多句话,但只有一个起止时间。当这个 segment 跨度很大时,字幕就会"黏在屏幕上"。

解决方法:用 word-level timestamps 重构断句逻辑。

智能断句算法

# 核心逻辑
for 每个词:
    检测词间停顿 pause = 下一个词.start - 当前词.end
    
    如果 pause >= 0.3s 且当前块长度 >= 1.5s:
        在这里切断形成新的字幕行
    否则如果当前块长度 >= max_line_duration默认 5s:
        强制切断

这比 stable-ts 的动态重构交叉注意力机制简单,但在大多数场景下足够有效。

幽灵字幕的根因与解决

幽灵字幕指背景音乐/静音期间出现的无意义字幕。

Whisper 是一个语音识别模型,它不区分"人声"和"背景音乐",只是尝试预测文字。在音乐声音中,模型可能会"幻想"出一些文字。

VAD 预处理方案

不需要 PyTorch 的 Silero VAD用 ffmpeg 内置的 silencedetect 即可:

ffmpeg -i 音频.wav -af silencedetect=noise=-40dB:d=0.5 -f null -

这会输出所有静音区域的起止时间。转写后,如果某段字幕 70% 以上落在静音区,则自动丢弃。

手动分段已移除

原先脚本里用 ffmpeg 做 10 分钟一段的手动分段 + 重叠合并,后来发现:

  • mlx-whisper 内部自带滑动窗口机制,会自动处理长音频,不需要外部切块
  • 手动分段导致 chunk 重叠区的幻觉(重复「うんうん」等)
  • 额外 I/O 开销ffmpeg 切 → 写盘 → 读回 → merge

移除了 segment_audio()merge_segments()--segment-minutes / --overlap-seconds / --no-segment 参数。现在流程更简洁:预处理 → mlx-whisper 整段转写 → 后处理。

残留依赖清理注意事项

如果曾经尝试装过 PyTorch 后又卸载,可能留下空目录:

# 检查残留
ls ~/.local/venvs/default/lib/python*/site-packages/ | grep torch

# 清理
rm -rf ~/.local/venvs/default/lib/python*/site-packages/torch \
       ~/.local/venvs/default/lib/python*/site-packages/functorch \
       ~/.local/venvs/default/lib/python*/site-packages/torchgen

uv pip uninstall 对非标准 wheel 的清理可能不完整,需要手动检查。

现场对比Whisper vs Deepgram Nova-2日语播客 #379

29 分钟日语广播节目,同一段音频,分别用 mlx-whisper turbo有 prompt和 Deepgram Nova-2无提示转写

对比项 Deepgram (Nova-2, 裸跑) Whisper (mlx turbo, 有 prompt)
分段数 296 段(偏长) 635 段(偏细,智能断句后)
首段开始 41.67s(错过片头音乐) 8.20s(抓到片头标签)
末尾幻觉 有「ご視聴ありがとうございました」重复
标点符号 有、。 基本没有
嘉宾名 浅見春奈(错字) 浅見春菜prompt 起作用)
整体识别率 很高,自然流畅 高,但有小错

结论: 在人声清晰的广播场景下mlx-whisper turbo + promt 并不比 Deepgram Nova-2 差。Deepgram 的优势在嘈杂环境、多人重叠、带 BGM 的现场录音。注意Nova-2 不支持 keyterm 提示,需 Nov-3 才有。)

模型量化信息mlx-whisper

mlx-community/whisper-large-v3-turbofp16float16 半精度),约 1.5GB。

量化 大小 说明
原始 fp32 ~3GB OpenAI 原版
MLX fp16 1.5GB Apple Silicon native 格式,计算效率最高
4-bit ~400MB 精度损失大MLX 社区很少出

Apple Silicon 上 fp16 是原生格式,不需要进一步量化。想要更小模型应换 small~500MBmedium~1.5GB)。

快速诊断清单

当用户抱怨字幕质量时,按以下流程排查:

  1. 黏连:检查是否开启了 word_timestampssmart_split
  2. 幽灵字幕:检查是否开启了 vad_filter
  3. 幻觉复读:检查是否开启了 clean
  4. 断句不自然:调整 max_line_duration 参数
  5. 识别准确度低:检查 initial_prompt 是否包含专有名词

代码审查修复记录2026-07-06

一轮完整代码审查发现并修复了 4 个问题:

Bug 1浮点精度导致 SRT 毫秒偏移(🔴 高影响)

问题: format_timestamp_srtint((seconds % 1) * 1000) 提取毫秒。Python 浮点精度导致 8.2 % 1 = 0.1999...int() 截断后变成 199 而不是 200。实测 642 段字幕中 623 段受影响(偏差 1ms

修复: 改用 int(round(seconds * 1000)) 先转总毫秒再取模。

Bug 2detect_silence 尾部静音丢失(🔴 高影响)

问题: 音频以静音结尾时ffmpeg 只输出 silence_start 没有 silence_end。原代码用 zip(starts, ends) 配对,zip 静默截断不匹配项导致最后一段静音被丢弃VAD 过滤失效。

修复: 改用索引配对,未闭合的 silence_startaudio_duration 参数补上结尾。

Bug 3format_timestamp_srt 负数乱码(🟡 防御性)

问题: -0.1 输出 -1:59:59,900。Whisper 正常不返回负数,但不防御。

修复: 开头加 if seconds < 0: seconds = 0.0

Bug 4remove_vad_gaps 除零风险(🟡 防御性)

问题: 零长度段(start == end)时 overlap / seg_len 除零崩溃。

修复:if seg_len <= 0: continue

清理:删除未使用的 import math

教训

  • 浮点取整陷阱: 任何 int(float_val * N) 都应该用 int(round(float_val * N)) 替代,避免 IEEE 754 精度问题
  • zip 配对陷阱: 当两个列表长度可能不匹配时,不要用 zip(),用索引配对并处理 unmatched 项
  • 代码审查应包含边界值测试: 负数、零、空列表、unmatched 输入