Skip to content

Bug: 32-bit float WAV 输入导致 ASR 识别结果完全错误(hallucination) #61

Description

@gpww

Bug: 32-bit float WAV 输入导致 ASR 识别结果完全错误(hallucination)

环境信息

  • Python 3.x
  • kaldiio, kaldi_native_fbank
  • FireRedASR2 模型

问题描述

当输入音频为 32-bit float PCM WAVpcm_f32le)时,ASR 识别结果完全错误(hallucination),confidence 极低。同一段音频转为 MP3 后再识别则结果正确。

复现步骤

  1. 准备一段 32-bit float WAV 音频(如通过 ffmpeg -i input.mp3 -c:a pcm_f32le output.wav
  2. FireRedAsr2.transcribe() 识别该 WAV 文件
  3. 对比同一段音频的 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() 读取音频文件的入口:

  1. fireredasr2/data/asr_feat.pyASRFeatExtractor.__call__()KaldifeatFbank.__call__()
  2. fireredlid/data/feat.pyFeatExtractor.__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 作为服务暴露给外部时,输入格式不可控,必须做归一化防御

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions