把ASR换到实时语音转文字时踩过的坑

从静态ASR到实时语音转文字,我用过哪些方案,踩过哪些坑,最后怎么走到现在的。

但我当时有几个现实问题:

  • 成本控制:项目预算有限,而且音频量会持续增长
  • 隐私要求:有些录音包含敏感信息,不想上传到第三方
  • 延迟要求:需要近实时的转文字能力,云端API的网络延迟无法接受
  • 定制需求:需要训练特定领域的词汇表,比如一些行业术语

所以我决定先从本地部署入手。

为什么不用现成的云服务?

云服务确实方便,阿里云、腾讯云、科大讯飞都有现成的ASR API,按次付费,接入简单。但我当时有几个现实问题:

  • 成本控制:项目预算有限,而且音频量会持续增长
  • 隐私要求:有些录音包含敏感信息,不想上传到第三方
  • 延迟要求:需要近实时的转文字能力,云端API的网络延迟无法接受
  • 定制需求:需要训练特定领域的词汇表,比如一些行业术语

所以我决定先从本地部署入手。

本地部署方案:OpenAI Whisper

Whisper 是OpenAI开源的通用语音识别模型,支持多语言,效果还不错。我的第一步是安装:

pip install openai-whisper
pip install torch torchaudio

然后用最简单的命令测试:

whisper audio.mp3 --model base --output_format txt

第一次运行就遇到了坑:PyTorch 版本冲突,CUDA 显存不足,音频格式不支持。折腾了一下午,终于跑通了第一个测试文件。

模型选择和性能对比

Whisper 有多个模型大小可选:

模型参数量显存需求速度准确率
tiny39M~1GB最快基本可用
base74M~1GB日常够用
small244M~2GB中等推荐使用
medium769M~5GB更准确
large1550M~10GB很慢最佳效果

Whisper 各档模型的显存占用差异很大,选型时先对照硬件上限比看参数量更实际。

Whisper 各模型规模的显存需求对比

显存从 tiny 的约 1GB 到 large 的约 10GB 跨了一个数量级,small 落在中间偏下的 sweet spot。

我的经验是:除非对准确率有极致要求,否则 small 模型是性价比最高的选择。在 Intel i7 + 16G RAM 的机器上,一段10分钟的音频大约需要30秒处理。

代码封装和实际使用

很快就发现命令行不够用,需要集成到Python代码里:

import whisper
import time

model = whisper.load_model("small")

def transcribe_audio(audio_path):
    start_time = time.time()
    result = model.transcribe(audio_path, language="zh", fp16=False)
    duration = time.time() - start_time

    return {
        "text": result["text"],
        "segments": result["segments"],
        "processing_time": duration
    }

result = transcribe_audio("meeting_recording.mp3")
print(f"处理时间: {result['processing_time']:.2f}秒")
print(f"转文字结果: {result['text'][:100]}...")

这里有个重要细节:fp16=False。如果用半精度浮点数,速度确实会快一些,但在某些老显卡上会报错。为了兼容性,我关掉了这个优化。

第一个坑:音频格式处理

Whisper 对音频格式挺挑剔,WAV、MP3、FLAC 都支持,但录音笔生成的 M4A 文件直接喂进去就会出错。需要先转换:

ffmpeg -i input.m4a -acodec pcm_s16le -ar 16000 -ac 1 output.wav

-ar 16000 很关键,把采样率降到16kHz,既能保证识别质量,又能减少处理时间。太高了浪费资源,太低了损失细节。

实时转文字的需求

静态ASR很快用上了,但新需求来了:需要实时转文字,比如在线会议、直播字幕。这完全是另一个问题。

实时处理的技术难点

实时和离线有几个本质区别:

  • 需要边录音边识别,不能等整段音频录完再处理
  • 延迟要够低,最好能在说话结束后1-2秒内出结果
  • 音频是流式的,需要分段处理,又不能简单切分破坏语义
  • 需要处理音频重叠、说话人切换的情况

VAD:语音活动检测

第一个问题是知道什么时候有语音、什么时候是静音。纯能量检测不够用,环境噪音、键盘敲击都会误判。我用了 webrtcvad 库:

import webrtcvad
import collections

vad = webrtcvad.Vad(2)  # 0-3,越大越严格
sample_rate = 16000
frame_duration = 30  # ms

def vad_collector(audio_stream, aggressiveness=3):
    vad = webrtcvad.Vad(aggressiveness)
    frames = []
    for frame in audio_stream:
        is_speech = vad.is_speech(frame, sample_rate)
        if is_speech:
            frames.append(frame)
        elif frames:
            # 收集到一段语音,返回
            yield b''.join(frames)
            frames = []
    if frames:
        yield b''.join(frames)

VAD 的 aggressiveness 参数很关键,从0到3,数字越大过滤越严格。我的经验是:会议室环境用2-3,安静的独白环境用1-2就行。

流式Whisper处理

有了VAD分段,就可以用Whisper做流式处理了。但不能简单地把每段语音独立喂给模型,因为上下文丢失会影响准确率。

我的方案是维护一个滑动窗口,保留最近几段的识别结果作为上下文:

class RealtimeTranscriber:
    def __init__(self, model_name="small"):
        self.model = whisper.load_model(model_name)
        self.history = collections.deque(maxlen=3)
        self.buffer = []

    def process_chunk(self, audio_chunk):
        # 把历史上下文拼接起来
        context = " ".join([h for h in self.history])
        # 先识别当前段落
        result = self.model.transcribe(audio_chunk, language="zh")
        current_text = result["text"].strip()

        # 如果历史上下文和当前文本有重复,尝试合并
        if self.history:
            last = self.history[-1]
            if current_text.startswith(last[-20:]):
                current_text = current_text[len(last[-20:]):].strip()

        self.history.append(current_text)
        return current_text

这个方案不算完美,但在实际使用中表现还可以。延迟控制在2-3秒,准确率比纯段落处理高出不少。

Web端实时转文字:WebAssembly

项目后来有了Web端需求,需要在浏览器里做实时转文字。这又是另一个故事。

为什么选WebAssembly

纯JavaScript做不了这么重的计算,WebAssembly可以把Whisper模型跑在浏览器里,虽然慢一点,但不用上传音频,隐私保护好,延迟也低。

Whisper.cpp和ONNX Runtime

我试了两个方案:

  1. whisper.cpp: C++重写的Whisper,编译成WASM,体积小、速度快
  2. ONNX Runtime: 把Whisper转成ONNX格式,用ORT的WASM版本跑

最后选了 whisper.cpp,因为它更轻量,社区活跃,有现成的WASM版本:

<script src="https://cdn.jsdelivr.net/npm/@xenova/whisper"></script>

实际使用代码

// 加载模型
const transcriber = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny');

// 处理音频流
async function processAudio(audioBuffer) {
    const result = await transcriber(audioBuffer, {
        language: 'chinese',
        task: 'transcribe',
        chunk_length_s: 30,
        stride_length_s: 5
    });

    return result.text;
}

这里的 chunk_length_sstride_length_s 很关键,它们控制分段和重叠策略。我的经验是:chunk_length_s=30stride_length_s=5,在浏览器里能稳定运行。

Web端的性能坑

浏览器里跑Whisper有几个坑:

  1. 模型加载慢: tiny模型也要几秒,大模型直接卡死
  2. 内存占用高: Chrome单标签页内存有限,medium模型就吃紧
  3. 兼容性问题: iOS Safari的WASM支持有bug,经常崩溃
  4. 电池消耗: 手机上跑几分钟就发热严重

我的妥协方案是:Web端只跑 tiny 模型,而且做了降级处理——检测到性能问题时切换到云端API。

// 检测性能
const startTime = performance.now();
await transcriber(dummy_audio);
const loadTime = performance.now() - startTime;

if (loadTime > 2000) {
    // 切换到云端API
    return useCloudAPI(audioBuffer);
}

云端API兜底方案

虽然项目以本地部署为主,但云端API还是做了兜底。选了讯飞开放星的实时语音转文字API,原因简单:

  • 低价位套餐够用
  • 支持WebSocket长连接
  • 对中文优化好
  • 文档相对清晰

WebSocket接入代码

const ws = new WebSocket('wss://rtasr.xfyun.cn/v1/ws');

ws.onopen = () => {
    // 发送认证信息
    const auth = generateAuth(appId, apiKey, apiSecret);
    ws.send(JSON.stringify({ auth }));
};

ws.onmessage = (event) => {
    const result = JSON.parse(event.data);
    if (result.result) {
        console.log('实时结果:', result.result.ws[0].cw[0].w);
    }
};

// 发送音频数据
function sendAudio(audioData) {
    const base64 = arrayBufferToBase64(audioData);
    ws.send(JSON.stringify({
        data: {
            status: 1,  // 1: 首包, 2: 中间包, 3: 尾包
            format: 'audio/L16;rate=16000',
            audio: base64,
            encoding: 'raw'
        }
    }));
}

这里的 status 字段很重要:1表示第一段音频,2表示中间段落,3表示结束。讯飞的WebSocket协议还算规范,但文档里有些细节没写清楚,我是看了开源项目才搞明白的。

云端API的坑

云端API虽然方便,但也有问题:

  1. 成本累积: 按时长计费,长期使用下来不便宜
  2. 网络依赖: 网络波动会中断连接,需要重连机制
  3. 隐私顾虑: 音频数据上传到第三方
  4. 定制限制: 热词、行业词汇需要额外付费

我的折中方案是:重要录音用本地Whisper,实时场景先用Web端WASM,性能不够时再切云端API。

性能优化的一些经验

折腾这么久,积累了一些性能优化的经验:

1. 音频预处理

  • 统一采样率到16kHz:大部分语音识别模型在这个采样率下表现最好
  • 单声道:立体声没必要,还浪费计算
  • 动态范围压缩:提高低音量的识别率
ffmpeg -i input.mp3 -ac 1 -ar 16000 -filter:a "acompressor" output.wav

2. 模型量化

把模型从FP32量化到INT8,能减少75%的显存,速度提升30%,准确率损失不到1%。

# 使用whisper.cpp的量化工具
./quantize ./models/ggml-small.bin ./models/ggml-small-q8_0.bin q8_0

3. 批处理优化

如果有多个音频文件,不要串行处理,可以用Python的多进程:

from multiprocessing import Pool

def process_single(audio_path):
    model = whisper.load_model("small")
    result = model.transcribe(audio_path)
    return result["text"]

with Pool(4) as p:
    results = p.map(process_single, audio_files)

但要注意,每个进程都会加载模型,内存占用会翻倍。我的经验是:CPU核心数的一半就够了。

4. 结果缓存

重复的音频(比如反复测试的录音)可以缓存识别结果,避免重复计算:

import hashlib
import pickle

def get_cache_key(audio_path):
    with open(audio_path, 'rb') as f:
        return hashlib.md5(f.read()).hexdigest()

cache = {}

def transcribe_with_cache(audio_path):
    cache_key = get_cache_key(audio_path)
    if cache_key in cache:
        return cache[cache_key]

    result = transcribe_audio(audio_path)
    cache[cache_key] = result
    return result

实际项目中的架构

最终的项目架构大概是这个样子:

graph LR A[音频输入] --> B{处理方式} B -->|本地部署| C[Whisper.cpp] B -->|Web端| D[Whisper WASM] B -->|实时兜底| E[云端API] C --> F[结果存储] D --> F E --> F F --> G[业务逻辑]

选择逻辑是:

  1. 先判断能否本地处理(性能、隐私要求)
  2. Web端优先用WASM,性能不够切云端
  3. 重要录音用本地Whisper,保证准确率和隐私
  4. 实时场景优先低延迟,准确率可以稍微牺牲

还没解决的问题

折腾了这么久,有些问题还是没解决好:

  1. 说话人分离: 多人会议时很难区分谁说了什么,用pyannote.audio效果一般
  2. 标点符号优化: Whisper生成的标点经常不准确,需要后处理
  3. 同音字纠错: “建设"和"见解"这种,ASR经常搞混
  4. 实时滚动显示: WebSocket返回的是增量结果,做平滑滚动展示挺麻烦

标点符号我试过几种方案,最后用的是简单规则+模型校验:

import re

def fix_punctuation(text):
    # 简单规则
    text = re.sub(r',。', ',', text)  # 连续标点
    text = re.sub(r'([。,!?])\1+', r'\1', text)  # 重复标点

    # 长句子加标点
    if len(text) > 50 and ',' not in text:
        text = text[:25] + ',' + text[25:]

    return text

不算完美,但在实际场景里够用了。

一点个人判断

语音识别这几年进步确实大,从十年前的垃圾到现在的可用,已经是实用级别的技术了。但我不觉得它会完全替代人工输入,有些场景文字输入还是更精确、更可控。

实时转文字的场景反而是最有价值的:会议记录、直播字幕、辅助听障。这些场景对准确率要求不那么苛刻,但对实时性、隐私性要求高,正好是本地ASR的用武之地。

我的建议是:如果你只是偶尔要转一段录音,用云服务就行;但如果要做产品级别的功能,本地部署值得投入。前期开发成本高一些,但后期维护、成本控制、用户体验都更好。

最后一句:语音识别已经过了"看起来很酷"的阶段,进入了"真正解决问题"的阶段。这时候的技术选择,不能只看演示效果,要看具体场景的限制条件。

参考资料

版权声明: 本文首发于 指尖魔法屋-把ASR换到实时语音转文字时踩过的坑https://blog.thinkmoon.cn/post/154-asr-realtime-speech-to-text/) 转载或引用必须申明原指尖魔法屋来源及源地址!