GLM-ASR

模型介绍

GLM-ASR-Nano-2512 是一款鲁棒的开源语音识别模型,参数量为 1.5B
该模型专为应对真实世界的复杂场景而设计,在多项基准测试中超越 OpenAI Whisper V3,同时保持紧凑的模型规模。

核心能力包括:

  • 卓越的方言支持
    除标准普通话和英语外,模型针对粤语及其他方言进行了深度优化,有效填补了方言语音识别领域的空白。

  • 低音量语音鲁棒性
    专门针对“低语/轻声”场景进行训练,能够捕捉并准确转录传统模型难以识别的极低音量音频。

  • SOTA 性能
    在同类开源模型中实现最低平均错误率 (4.10),在中文基准测试(Wenet Meeting、Aishell-1 等)中展现出显著优势。

https://github.com/zai-org/GLM-ASR/tree/main

基准测试

我们将 GLM-ASR-Nano 与主流开源和闭源模型进行了对比评测。结果表明,GLM-ASR-Nano (1.5B) 表现优异,尤其在复杂声学环境下优势明显。

说明:

  • Wenet Meeting 反映了包含噪声和语音重叠的真实会议场景。
  • Aishell-1 是标准普通话基准测试集。

模型下载

推理

GLM-ASR-Nano-2512 可通过 transformers 库轻松集成。
我们将支持 transformers 5.x 以及 vLLMSGLang 等推理框架。

环境依赖

pip install -r requirements.txt
sudo apt install ffmpeg

示例代码

python inference.py --checkpoint_dir zai-org/GLM-ASR-Nano-2512 --audio examples/example_en.wav # 英文
python inference.py --checkpoint_dir zai-org/GLM-ASR-Nano-2512 --audio examples/example_zh.wav # 中文

对于上述两段示例音频,模型能够生成准确的转录结果:

be careful not to allow fabric to become too hot which can cause shrinkage or in extreme cases scorch
我还能再搞一个,就算是非常小的声音也能识别准确

🔥 只有 1.5B 参数,却在中文、方言(尤其是粤语)、甚至“窃窃私语”的低音量场景下,全面超越了 Whisper V3!

GLM-ASR-Nano-2512 特点

  • 💥 核心能力:专治各种“听不清”

  • 🗣️ 方言之王(尤其是粤语): 除标准普通话和英语外,模型针对粤语及其他方言进行了深度优化。对于做港剧字幕组、粤语客服质检的小伙伴来说,有效填补了方言识别领域的空白。

  • 🤫 低音量语音鲁棒性: 它专门针对 “低语/轻声”场景进行训练,能够捕捉并准确转录传统模型难以识别的极低音量音频。以后开会偷偷录音(误),也不怕听不清了。

  • 🏆 SOTA 性能: 在同类开源模型中实现了最低平均错误率 (4.10),在 Wenet Meeting、Aishell-1 等中文基准测试中展现出显著优势。

  • 🌍 多语言支持: 支持 17 种语言(WER ≤ 20%),包括日、英、法、德、俄、西等主流语言,甚至连加泰罗尼亚语、立陶宛语这种小语种都支持。

5分钟本地部署

光说不练假把式。趁着周五下班前,咱们用 Python 把它跑起来!

1. 准备工作

首先,你需要安装 ffmpeg,它是处理音频的核心工具。

  • Conda 用户(推荐):
    conda install "ffmpeg" -c conda-forge
  • Ubuntu/Debian: sudo apt install ffmpeg
  • Windows: 下载 release 包并配置环境变量。
  • 接下来,安装 Python 依赖(注意版本要求):
pip install torch>=2.9.1 torchaudio>=2.9.1 torchcodec>=0.9.0 transformers==4.51.3

2. 模型下载

为了演示方便,我已经手动将模型下载到了本地目录:zai-org/GLM-ASR-Nano-2512。 大家可以从上面的 ModelScope 或 HuggingFace 地址下载。

目录结构:

📢 获取源码

本系列所有章节的可运行代码都会同步上传至 GitHub。

欢迎关注后私信关键字:实战

获取仓库地址。

3. 硬核推理代码

这个模型的调用方式稍微有点硬核,需要手动处理音频特征。我已经帮大家把坑都踩平了,直接复制下面的代码即可运行!

import torch
import torchaudio
import soundfile as sf
from pathlib import Path
from transformers import AutoTokenizer, WhisperFeatureExtractor, AutoConfig, AutoModelForCausalLM

# -----------------------------
# 固定配置
# -----------------------------
WHISPER_FEAT_CFG = {
    "chunk_length": 30,
    "feature_extractor_type": "WhisperFeatureExtractor",
    "feature_size": 128,
    "hop_length": 160,
    "n_fft": 400,
    "n_samples": 480000,
    "nb_max_frames": 3000,
    "padding_side": "right",
    "padding_value": 0.0,
    "processor_class": "WhisperProcessor",
    "return_attention_mask": False,
    "sampling_rate": 16000,
}

def load_audio(path, target_sr=16000):
    wav, sr = sf.read(path)
    if wav.ndim > 1:  # 多声道取第一声道
        wav = wav[:, 0]
    wav = torch.tensor(wav, dtype=torch.float32).unsqueeze(0)
    if sr != target_sr:
        wav = torchaudio.transforms.Resample(sr, target_sr)(wav)
        sr = target_sr
    return wav, sr

def get_audio_token_length(seconds, merge_factor=2):
    # 计算音频token长度
    def get_T_after_cnn(L_in, dilation=1):
        for padding, kernel_size, stride in eval("[(1,3,1)] + [(1,3,2)] "):
            L_out = L_in + 2 * padding - dilation * (kernel_size - 1) - 1
            L_out = 1 + L_out // stride
            L_in = L_out
        return L_out

    mel_len = int(seconds * 100)
    audio_len_after_cnn = get_T_after_cnn(mel_len)
    audio_token_num = (audio_len_after_cnn - merge_factor) // merge_factor + 1
    return min(audio_token_num, 1500 // merge_factor)


def build_prompt(audio_path, tokenizer, feature_extractor, merge_factor):
    # wav, sr = load_audio(str(audio_path))
    wav, sr = torchaudio.load(str(audio_path))
    wav = wav[:1, :]
    if sr != feature_extractor.sampling_rate:
        wav = torchaudio.transforms.Resample(sr, feature_extractor.sampling_rate)(wav)

    tokens = []
    tokens += tokenizer.encode("<|user|>\n")

    audios, audio_offsets, audio_length = [], [], []
    chunk_size = 30 * feature_extractor.sampling_rate

    # 分块处理音频
    for start in range(0, wav.shape[1], chunk_size):
        chunk = wav[:, start:start + chunk_size]
        mel = feature_extractor(
            chunk.numpy(),
            sampling_rate=feature_extractor.sampling_rate,
            return_tensors="pt",
            padding="max_length",
        )["input_features"]

        audios.append(mel)

        seconds = chunk.shape[1] / feature_extractor.sampling_rate
        num_tokens = get_audio_token_length(seconds, merge_factor)

        tokens += tokenizer.encode("<|begin_of_audio|>")
        audio_offsets.append(len(tokens))
        tokens += [0] * num_tokens
        tokens += tokenizer.encode("<|end_of_audio|>")
        audio_length.append(num_tokens)

    tokens += tokenizer.encode("<|user|>\nPlease transcribe this audio into text")
    tokens += tokenizer.encode("<|assistant|>\n")

    return {
        "input_ids": torch.tensor([tokens]),
        "audios": torch.cat(audios, dim=0),
        "audio_offsets": [audio_offsets],
        "audio_length": [audio_length],
        "attention_mask": torch.ones(1, len(tokens)),
    }


def run_asr(model_dir, audio_path):
    print("🎤 开始识别音频:", audio_path)
    device = "cuda"if torch.cuda.is_available() else"cpu"

    tokenizer = AutoTokenizer.from_pretrained(model_dir)
    feature_extractor = WhisperFeatureExtractor(**WHISPER_FEAT_CFG)

    config = AutoConfig.from_pretrained(model_dir, trust_remote_code=True)
    model = AutoModelForCausalLM.from_pretrained(
        model_dir, config=config, trust_remote_code=True, torch_dtype=torch.bfloat16
    ).to(device)
    model.eval()

    batch = build_prompt(audio_path, tokenizer, feature_extractor, config.merge_factor)

    model_inputs = {
        "inputs": batch["input_ids"].to(device),
        "attention_mask": batch["attention_mask"].to(device),
        "audios": batch["audios"].to(device, dtype=torch.bfloat16),
        "audio_offsets": batch["audio_offsets"],
        "audio_length": batch["audio_length"],
    }

    prompt_len = batch["input_ids"].shape[1]

    with torch.inference_mode():
        generated = model.generate(
            **model_inputs,
            max_new_tokens=128,
            do_sample=False,
        )

    transcript_ids = generated[0, prompt_len:].cpu().tolist()
    result = tokenizer.decode(transcript_ids, skip_special_tokens=True)
    print("\n-------- 📝 识别结果 --------")
    print(result)
    print("-----------------------------\n")
    return result


if __name__ == "__main__":
    # 请确保模型路径正确!
    model_path = "./zai-org/GLM-ASR-Nano-2512"

    # run_asr(model_path, "./audio/example_en.wav")
    # run_asr(model_path, "./audio/example_zh.mp3")

    # 🔥 重点测试:粤语识别
    run_asr(model_path, "./audio/example_yy.mp3")
作者:Jeebiz  创建时间:2025-12-13 08:54
最后编辑:Jeebiz  更新时间:2025-12-13 09:02