7.0 KiB
字幕时间轴质量深度解析
本文档记录从 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,只是默认关闭。打开后可以获得每个词的精确起止时间,用来:
- 更精准的 SRT 断句(不在词中间切断)
- 检测长停顿,自动拆分句子
- 修复"黏连"(一句字幕占好几秒不消失)
出现黏连的根因
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-turbo 是 fp16(float16 半精度),约 1.5GB。
| 量化 | 大小 | 说明 |
|---|---|---|
| 原始 fp32 | ~3GB | OpenAI 原版 |
| MLX fp16 | 1.5GB | Apple Silicon native 格式,计算效率最高 |
| 4-bit | ~400MB | 精度损失大,MLX 社区很少出 |
Apple Silicon 上 fp16 是原生格式,不需要进一步量化。想要更小模型应换 small(~500MB)或 medium(~1.5GB)。
快速诊断清单
当用户抱怨字幕质量时,按以下流程排查:
- 黏连:检查是否开启了
word_timestamps和smart_split - 幽灵字幕:检查是否开启了
vad_filter - 幻觉复读:检查是否开启了
clean - 断句不自然:调整
max_line_duration参数 - 识别准确度低:检查
initial_prompt是否包含专有名词
代码审查修复记录(2026-07-06)
一轮完整代码审查发现并修复了 4 个问题:
Bug 1:浮点精度导致 SRT 毫秒偏移(🔴 高影响)
问题: format_timestamp_srt 用 int((seconds % 1) * 1000) 提取毫秒。Python 浮点精度导致 8.2 % 1 = 0.1999...,int() 截断后变成 199 而不是 200。实测 642 段字幕中 623 段受影响(偏差 1ms)。
修复: 改用 int(round(seconds * 1000)) 先转总毫秒再取模。
Bug 2:detect_silence 尾部静音丢失(🔴 高影响)
问题: 音频以静音结尾时,ffmpeg 只输出 silence_start 没有 silence_end。原代码用 zip(starts, ends) 配对,zip 静默截断不匹配项,导致最后一段静音被丢弃,VAD 过滤失效。
修复: 改用索引配对,未闭合的 silence_start 用 audio_duration 参数补上结尾。
Bug 3:format_timestamp_srt 负数乱码(🟡 防御性)
问题: -0.1 输出 -1:59:59,900。Whisper 正常不返回负数,但不防御。
修复: 开头加 if seconds < 0: seconds = 0.0。
Bug 4:remove_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 输入