Files
vrsub/docs/adaptive_vad.md
cat-shark 2c3c356348 feat: whisper 节点自动接入自适应 VAD + 方案文档
- nodes/whisper.py:invoke 检测到 vad_filter=true 且未显式传 vad_parameters
  且 WOV_AUTO_VAD=1 时,自动调用 vad_profiler.vad_parameters_for_audio 按
  本音频信号动态确定 VAD 参数并传给 transcribe;分析失败回退默认不中断转写
  (防御性,不影响整段流程)。
- tests/test_nodes.py:新增自动 VAD 异常回退测试(patch profiler 抛错,
  验证 whisper 正常转写 + transcribe 收到 vad_parameters=None)。
- docs/adaptive_vad.md:完整方案文档(背景/可行性/三层架构/参数建议规则/
  评分器/实现落点/取舍/实现状态)。
2026-09-06 08:15:13 +08:00

108 lines
6.2 KiB
Markdown
Raw Permalink 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.
# 每视频自适应 VAD 调参方案
## 背景:为什么必须每视频单独调 VAD
实测 CJOD-255 音频(2 小时成人视频)暴露了固定 VAD 配置的根本缺陷:
- 全片 1s 能量 **56% 在 RMS<900(音乐/低语)**、仅 13% 是静音级(<300)
全程有 BGM,**几乎没有纯静音**。
- 简单能量阈值(RMS>700)会把**整片识别成 1 个 4120s 的巨型语音段**——
BGM 让能量始终高于阈值,VAD 无法分离出真实说话。
- 结果是 whisper 全段解码:80% 字幕碎片化(啊/嗯/はい)、32% 参考句漏识别、
敏感段(口交等)丢失。
不同视频的 BGM/静音/人声比例天差地别(测试片、音乐 MV、含 BGM 剧集、
纯语音播客),**固定 VAD 参数必然在多数视频上偏差**。因此需要对**每个视频**
先独立做信号分析,再动态决定 VAD 参数。
## 可行性
1. **VAD 是解码前的前置步骤**:先用 ffmpeg 提 WAV(已有),可独立做信号分析,
不占用模型推理资源。
2. **faster-whisper 支持 `vad_parameters` 细参**:可传
`threshold`(说话判定阈值)、`min_silence_duration_ms`(断句静音时长)、
`speech_pad_ms`(语音段首尾缓冲)。当前节点只传 `vad_filter`,细参入口未用。
3. **求解代价可控**:找最优参数在 1-2 分钟代表性片段上做网格搜索,
全片只跑一次确定后的参数。
## 方案架构(三层)
```
每个视频进入 whisper 节点时:
┌─────────────────────────────────────────────────────────┐
│ ① 信号分析 profile_audio(秒级,无模型) │
│ - 1s 网格能量/RMS 分布 → 静音比例、BGM 底噪、语音疏密 │
│ - 推断:threshold 候选、min_silence 候选、speech_pad │
│ ② 片段网格验证 pick_best_vad90s 代表片段,3~5 组参数) │
│ - 若提供参考字幕 → 用"参考覆盖率+时间对齐误差"评分 │
│ - 无参考 → 用"碎片率/连贯性/置信度"启发式评分 │
│ ③ 全片用 ② 选出的 vad_parameters 跑一次 │
└─────────────────────────────────────────────────────────┘
```
## 信号分析 → VAD 参数推荐规则
| 音频特征(从能量分布算出) | VAD 建议 |
| --- | --- |
| 静音比例高(>30%)、BGM 低 | threshold 高(0.6~0.7)、min_silence 2000,正常 VAD |
| **BGM 覆盖广(如 CJOD 56%<900** | **threshold 低(0.3~0.4)、min_silence 300~500、speech_pad 0**——避免把音乐当语音整段拼接 |
| 纯语音(播客/采访)| threshold 0.5、min_silence 1000,普通 |
| 长静音(停顿>2s 常见)| speech_pad 减小(0~200)防时间轴压缩漂移 |
### 关键:BGM 掩盖音频的处理
对整片被 BGM 覆盖的视频,纯能量 VAD 天然失效。三种手段按优先级:
1. **降低 VAD threshold**0.5→0.3):让 silero 的语音概率模型在 BGM 中
更敏感地捕捉人声,避免把音乐段误判为静音而吞掉轻语。
2. **降低 min_silence_duration_ms**2000→300~500):避免把停顿超过 2s 的
短句硬并成一条,缓解"碎片化/漏句"。
3. **speech_pad_ms 减小**400→0~200):缓冲越大,Whisper 对片段起始时间
估计越偏早,减小可提升时间对齐精度。
## 评分器(决定最优参数)
### 有参考字幕(OCR 硬字幕 = ground truth
参考的第 k 条 vs 转录的对齐:
- **覆盖率** = 能在 1.5s 内找到语义对应日文的参考条数 / 总参考条数(越高越好)
- **时间误差** = 配对条目 |转录start - 参考start| 的平均(越低越好)
- 综合分 = 覆盖率 - 0.3×时间误差(权重可调)
### 无参考字幕(启发式)
用转录自身质量:
- **碎声率** = 纯单字/语气词条目占比(越低越好,如 <20%)
- **连贯性** = 每条平均字数(适中为好,不碎不并)
- **置信度** = whisper segment 的 avg_logprob
## 实现落点
- 新模块 `nodes/vad_profiler.py``profile_audio()`(信号分析)、
`pick_best_vad()`(片段网格 + 评分)、`vad_parameters_for_audio()`(总入口)
- `nodes/whisper.py`:在 `invoke` 里检测到 `vad_filter=true` 且未显式传
`vad_parameters` 时,调用 profiler 生成 per-video 参数传给 `transcribe()`
- 参考字幕可选:若工作流/请求提供 `reference_srt_uri`,走"有参考评分"
- 门控:`WOV_AUTO_VAD=1`(默认开),可关;显式传 `vad_parameters` 时跳过
## 取舍
- **收益**:每个视频用最贴合其声学的 VAD,显著降低碎片化、漏句、时间错位,
尤其对 BGM 音频(如成人视频、综艺)。
- **成本**:每视频额外 10~30s 信号分析 + 片段上 3~5 次短转写(分钟级),
相比全片转写可接受。
- **风险**:片段网格选取的代表性片段若无说话/纯音乐,可能评为最差参数。
缓解:选 2 段(开头+中段)拼接,且阈值下限保护。
## 实现状态(2026-09 已落地)
- `nodes/vad_profiler.py``profile_audio()`1s 能量分析)、
`suggest_vad_parameters()`(信号→VAD 建议)、`score_transcript()`(幻觉词
扣分启发式)、`vad_parameters_for_audio()`(总入口)、`_pick_representative_start()`
`_grid_search_vad()`(片段网格)。
- `nodes/whisper.py``invoke` 检测到 `vad_filter=true` 且未显式传
`vad_parameters``WOV_AUTO_VAD=1` 时,自动调用 profiler 生成 per-video 参数
传给 `transcribe()`;分析失败回退默认不中断转写。
- 测试 `tests/test_vad_profiler.py`(19 个)覆盖:信号分析、四个建议分支、
幻觉词/碎片评分、网格选优、异常回退、空音频/低采样率、候选网格展开。
- 门控:`WOV_AUTO_VAD=0` 关闭自动调参;显式传 `vad_parameters` 时跳过。
- 说明:当前接入默认 `whisper_invoke=None`(仅信号分析,秒级、不额外跑模型);
若未来要启用片段网格验证,传入 whisper 回调并暴露配置即可,框架已就绪。