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 WAV(pcm_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.py — ASRFeatExtractor.__call__() 和 KaldifeatFbank.__call__()
  2. 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 作为服务暴露给外部时,输入格式不可控,必须做归一化防御

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions