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

163 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 字幕时间轴质量深度解析
本文档记录从 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 重构断句逻辑。
## 智能断句算法
```python
# 核心逻辑
for 每个词:
检测词间停顿 pause = 下一个词.start - 当前词.end
如果 pause >= 0.3s 且当前块长度 >= 1.5s:
在这里切断形成新的字幕行
否则如果当前块长度 >= max_line_duration默认 5s:
强制切断
```
这比 stable-ts 的动态重构交叉注意力机制简单,但在大多数场景下足够有效。
## 幽灵字幕的根因与解决
**幽灵字幕**指背景音乐/静音期间出现的无意义字幕。
Whisper 是一个语音识别模型,它不区分"人声"和"背景音乐",只是尝试预测文字。在音乐声音中,模型可能会"幻想"出一些文字。
### VAD 预处理方案
不需要 PyTorch 的 Silero VAD用 ffmpeg 内置的 `silencedetect` 即可:
```bash
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 后又卸载,可能留下空目录:
```bash
# 检查残留
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`**fp16float16 半精度)**,约 1.5GB。
| 量化 | 大小 | 说明 |
|---|---|---|
| 原始 fp32 | ~3GB | OpenAI 原版 |
| MLX fp16 | 1.5GB | Apple Silicon native 格式,计算效率最高 |
| 4-bit | ~400MB | 精度损失大MLX 社区很少出 |
Apple Silicon 上 fp16 是原生格式,不需要进一步量化。想要更小模型应换 `small`~500MB`medium`~1.5GB)。
## 快速诊断清单
当用户抱怨字幕质量时,按以下流程排查:
1. **黏连**:检查是否开启了 `word_timestamps``smart_split`
2. **幽灵字幕**:检查是否开启了 `vad_filter`
3. **幻觉复读**:检查是否开启了 `clean`
4. **断句不自然**:调整 `max_line_duration` 参数
5. **识别准确度低**:检查 `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 2detect_silence 尾部静音丢失(🔴 高影响)
**问题:** 音频以静音结尾时ffmpeg 只输出 `silence_start` 没有 `silence_end`。原代码用 `zip(starts, ends)` 配对,`zip` 静默截断不匹配项导致最后一段静音被丢弃VAD 过滤失效。
**修复:** 改用索引配对,未闭合的 `silence_start``audio_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 输入