60 lines
2.5 KiB
Markdown
60 lines
2.5 KiB
Markdown
# Empty Segment Gap(空白段漏转写)
|
||
|
||
## 症状
|
||
|
||
全长转写结果中,某一段(几秒到几十秒)输出为**空文本**:
|
||
|
||
```json
|
||
{"start": 1376.42, "end": 1406.42, "text": ""}
|
||
```
|
||
|
||
该段音频实际包含清晰的对话,单独提取转写完全正常。
|
||
|
||
## 根因
|
||
|
||
与 [token repetition loop](token-repetition-loop.md) 同一类问题——**Whisper autoregressive decoder 的边界故障**,但表现不同:
|
||
|
||
- 循环幻觉:低置信度边界 → 猜错第一个 token → 自激放大 → 重复输出
|
||
- 空段:低置信度边界 → decoder 全程置信度不足 → 输出空字符串
|
||
|
||
两种故障都发生在长音频中后段(20+ 分钟),此时 KV cache 积累了大量上下文,在模糊发音、犹豫、或者声学域偏移(如不同录音源的拼接点)处模型失去对齐。
|
||
|
||
## 修复方案
|
||
|
||
与 token repetition loop 相同——**检测 → 提取 → 冷启动重转写**:
|
||
|
||
### 检测
|
||
|
||
```python
|
||
def find_empty_gaps(segments: list[dict], min_duration: float = 1.0) -> list[int]:
|
||
"""返回所有空文本且时长超过 min_duration 的 segment 索引"""
|
||
return [
|
||
i for i, s in enumerate(segments)
|
||
if not s.get("text", "").strip() and s["end"] - s["start"] > min_duration
|
||
]
|
||
```
|
||
|
||
### 合并连续空段
|
||
|
||
连续的空段合并为一个时间范围,避免对同一片音频发起多次重转写。
|
||
|
||
### 冷启动重写
|
||
|
||
ffmpeg 切出该段 → `mlx_whisper.transcribe()` 不带前序上下文 → 如果重转写得到有效文本,替换回原结果。
|
||
|
||
**如果重转写也无输出**(说明该段确实是静音/纯噪),则直接删除该段,不保留原始空字幕。这与旧版"保留 originals"的做法不同——避免静音段冷启动逼出幻听。
|
||
|
||
## 为什么 VAD 过滤没抓到
|
||
|
||
VAD(静音过滤)检查的是 segments 是否落在 ffmpeg `silencedetect` 标记的**静音区**。空段大多不是静音(有音乐、背景音甚至人声),只是 Whisper 的 decoder 没有输出文字——所以不会被 VAD 丢弃。
|
||
|
||
## 在脚本中的实现
|
||
|
||
空段检测已合并进 `fix_repetition_loops()` 函数(Mode B),与循环检测共享重转写逻辑骨架。位于 `whisper_transcribe.py` 的 Post-processing 阶段,在 `--no-clean` 未设置时运行。
|
||
|
||
核心行为:
|
||
- 空段 ≥2s → 标记为异常,冷启动重试
|
||
- 重试成功 → 用新 segments 替换
|
||
- 重试仍为空 → `del` 删除该段(不入库)
|
||
- 使用 `reversed(ranges)` 逆序遍历,防止多区间替换时的索引漂移
|