Bug: 32-bit float WAV 输入导致 ASR 识别结果完全错误(hallucination)
环境信息
- Python 3.x
- kaldiio, kaldi_native_fbank
- FireRedASR2 模型
问题描述
当输入音频为 32-bit float PCM WAV(pcm_f32le)时,ASR 识别结果完全错误(hallucination),confidence 极低。同一段音频转为 MP3 后再识别则结果正确。
复现步骤
- 准备一段 32-bit float WAV 音频(如通过
ffmpeg -i input.mp3 -c:a pcm_f32le output.wav)
- 用
FireRedAsr2.transcribe() 识别该 WAV 文件
- 对比同一段音频的 MP3 版本识别结果
实际结果
| 格式 |
识别文本 |
confidence |
| MP3 |
"我来帮你" |
0.939 |
| 32-bit float WAV |
"我有防御塔" |
0.324 |
期望结果
两种格式应产生相同(或高度一致)的识别结果。
根因分析
kaldiio.load_mat() 对 float WAV 不归一化
FireRedAsr2.transcribe()
→ ASRFeatExtractor.__call__()
→ kaldiio.load_mat(wav_path) # ← 问题出在这里
→ 返回 ±1.0 范围的 float64 数组(32-bit float WAV)
→ KaldifeatFbank.__call__()
→ kaldi_native_fbank.accept_waveform(sr, wav_np.tolist())
# kaldi 内部假设输入是 int16 范围(±32768)
# ±1.0 的信号能量小 ~30000 倍
→ fbank 输出接近 log(0)
→ CMVN 后变成噪声特征
→ 模型 hallucination
kaldiio.load_mat() 对不同位深的 WAV 返回不同值域的数据:
| WAV 格式 |
kaldiio.load_mat() 返回值域 |
kaldi 期望 |
| 16-bit PCM (int16) |
±32768 |
✅ 匹配 |
| 32-bit float (pcm_f32le) |
±1.0 |
❌ 差 ~30000 倍 |
| 24-bit PCM |
±8388608 |
❌ 差 ~256 倍 |
kaldi_native_fbank 内部假设输入是 int16 量级的 PCM 数据。当输入值域为 ±1.0 时,信号能量极小,fbank 输出接近零,CMVN 归一化后变成噪声,模型产生 hallucination。
影响范围
所有使用 kaldiio.load_mat() 读取音频文件的入口:
fireredasr2/data/asr_feat.py — ASRFeatExtractor.__call__() 和 KaldifeatFbank.__call__()
fireredlid/data/feat.py — FeatExtractor.__call__() 和 KaldifeatFbank.__call__()
不受影响的路径
fireredvad/core/audio_feat.py 使用 sf.read(dtype="int16"),libsndfile 会自动归一化 float 到 int16 范围
fireredasr2system.py 原来也用 sf.read(dtype="int16"),相对安全
建议修复
在特征提取入口替换 kaldiio.load_mat() 为统一解码函数,确保输出始终是 int16 范围的 PCM 数据:
方案 A:用 soundfile.read(path, dtype="int16") 替换
- libsndfile 会自动将 float 归一化到 int16 范围
- 返回顺序不同:soundfile 返回
(data, sr),kaldiio 返回 (sr, data),需注意解包顺序
方案 B:用 ffmpeg 子进程统一解码为 16kHz mono int16 PCM(推荐)
- 支持更多格式(MP3、FLAC、OGG 等),不依赖 libsndfile 对各格式的支持
- 更适合作为服务暴露给外部输入的场景
- 输出格式完全可控,不会因上游库的隐式行为产生意外
方案 C(最小改动):保留 kaldiio.load_mat(),在读入后加归一化校验
sample_rate, wav_np = kaldiio.load_mat(wav_path)
# 如果值域不在 int16 范围,自动归一化
if np.abs(wav_np).max() < 100: # 远小于 int16 范围,说明是 float 归一化数据
wav_np = (wav_np * 32768).astype(np.int16)
⚠️ 额外提醒:上游 pydub 音频预处理也会导致同类问题
在修复本 bug 的过程中,我们发现上层应用(如 Gradio demo)如果使用 pydub 做音频预处理再传入 ASR,也会产生独立的增益异常问题,且与本 bug 叠加后更难排查:
pydub 对 32-bit float WAV 转换时增益异常放大 ~10 倍:
| 路径 |
PCM std |
PCM range |
结果 |
| 原始 WAV → ffmpeg 直接解码 |
1582.8 |
[-9247, 6538] |
✅ 正确识别 |
| 原始 WAV → pydub 转换 → 解码 |
14949.6 |
[-19878, 15948] |
❌ 放大 ~10 倍 |
| 原始 MP3 → pydub 转换 → 解码 |
1503.5 |
[-8952, 6169] |
✅ 正确识别 |
建议:pydub 不适合作为 ASR 推理管线的音频预处理工具。ASR 模型对输入值域敏感,pydub 对 PCM 格式转换有隐式的增益处理(float → int16 缩放系数不当),会导致输入分布偏移。音频解码应统一由模型入口层处理(如 ffmpeg 子进程或 soundfile.read(dtype="int16")),pydub 仅限获取时长等非推理辅助用途。
补充说明
- 本 bug 不影响训练流程(训练数据是 kaldi 生态的标准 16-bit PCM WAV)
- 当 ASR 作为服务暴露给外部时,输入格式不可控,必须做归一化防御
Bug: 32-bit float WAV 输入导致 ASR 识别结果完全错误(hallucination)
环境信息
问题描述
当输入音频为 32-bit float PCM WAV(
pcm_f32le)时,ASR 识别结果完全错误(hallucination),confidence 极低。同一段音频转为 MP3 后再识别则结果正确。复现步骤
ffmpeg -i input.mp3 -c:a pcm_f32le output.wav)FireRedAsr2.transcribe()识别该 WAV 文件实际结果
期望结果
两种格式应产生相同(或高度一致)的识别结果。
根因分析
kaldiio.load_mat()对 float WAV 不归一化kaldiio.load_mat()对不同位深的 WAV 返回不同值域的数据:kaldiio.load_mat()返回值域kaldi_native_fbank内部假设输入是 int16 量级的 PCM 数据。当输入值域为 ±1.0 时,信号能量极小,fbank 输出接近零,CMVN 归一化后变成噪声,模型产生 hallucination。影响范围
所有使用
kaldiio.load_mat()读取音频文件的入口:fireredasr2/data/asr_feat.py—ASRFeatExtractor.__call__()和KaldifeatFbank.__call__()fireredlid/data/feat.py—FeatExtractor.__call__()和KaldifeatFbank.__call__()不受影响的路径
fireredvad/core/audio_feat.py使用sf.read(dtype="int16"),libsndfile 会自动归一化 float 到 int16 范围fireredasr2system.py原来也用sf.read(dtype="int16"),相对安全建议修复
在特征提取入口替换
kaldiio.load_mat()为统一解码函数,确保输出始终是 int16 范围的 PCM 数据:方案 A:用
soundfile.read(path, dtype="int16")替换(data, sr),kaldiio 返回(sr, data),需注意解包顺序方案 B:用 ffmpeg 子进程统一解码为 16kHz mono int16 PCM(推荐)
方案 C(最小改动):保留
kaldiio.load_mat(),在读入后加归一化校验在修复本 bug 的过程中,我们发现上层应用(如 Gradio demo)如果使用 pydub 做音频预处理再传入 ASR,也会产生独立的增益异常问题,且与本 bug 叠加后更难排查:
pydub 对 32-bit float WAV 转换时增益异常放大 ~10 倍:
建议:pydub 不适合作为 ASR 推理管线的音频预处理工具。ASR 模型对输入值域敏感,pydub 对 PCM 格式转换有隐式的增益处理(float → int16 缩放系数不当),会导致输入分布偏移。音频解码应统一由模型入口层处理(如 ffmpeg 子进程或
soundfile.read(dtype="int16")),pydub 仅限获取时长等非推理辅助用途。补充说明