
语音识别这块我前后折腾过三四套方案从最早直接调云端接口到后来因为项目要求音频数据不能出内网被迫转本地离线方案最后落点在 Vosk 上。Vosk 是一个基于 Kaldi 的开源离线语音识别工具包支持二十多种语言和方言最小模型只有几十兆能在树莓派、老笔记本、安卓手机上跑起来不需要联网也不按调用次数收费。这篇东西不讲概念科普只讲我从零把它跑通、再做到能在真实项目里用的全过程环境怎么搭、模型怎么选、麦克风实时识别怎么写、识别率上不去的时候往哪儿调、以及那些官方文档里不会写的坑。如果你手上正好有要在内网跑语音转文字要给一个离线设备加语音交互要把一段录音批量转成字幕这类需求下面的内容基本可以直接抄。如果只是想让电脑听懂两三个命令词那么看完第 5 章就够了后面的重活儿可以省掉。1. 先把选型想明白Vosk 解决的是哪一类问题1.1 真正卡住我的是音频不出内网这条硬约束一开始我也是走云端路线请求发出去、结果返回来接入成本极低。问题出在第二个项目上现场设备录到的都是内部沟通内容客户明确要求音频不许离开本地网络。这时候摆在面前的选择其实只有三个一是自建一套 Kaldi 或 Whisper 服务二是买商业离线 SDK三是找轻量的开源离线工具包。前两个方案要么工程量太大要么授权费用不便宜尤其是嵌入式场景下想装机就跑几乎没什么余地。Vosk 就是在这样的背景下被翻出来的。它的定位很清楚把 Kaldi 那套东西封装成一个可以直接调用的库Python、Java、C#、Node、Go、Rust 都有绑定还提供了移动端和树莓派的预编译产物。对我这种不想研究声学模型训练、只想把语音变成文字的人来说这个抽象层级刚刚好——再薄一层就得自己处理特征提取再厚一层就变成了黑盒 SaaS。1.2 三条技术路线的横向对比为了把选型讲清楚我把当时评估过的几条路整理成了一张表。需要说明的是这里的数字都是量级上的感受具体跟硬件、模型大小、音频质量强相关不要当成精确指标来用。对比项Vosk离线云端 ASR 接口自建 Whisper 服务是否需要联网不需要必须不需要中文识别效果中等偏上安静环境下可用通常最好好尤其大模型最小资源占用模型几十 MB 起客户端几乎为零模型几百 MB 到数 GB实时性CPU 上可做到接近实时取决于网络往返需要 GPU 才舒服接入成本pip 装完就能跑最低需要自己搭服务、做并发定制能力支持词表约束、n-best基本靠参数可微调费用一次性投入算力按量计费一次性投入算力我最终选 Vosk 的核心原因不是它效果最好而是它在离线 轻量 可嵌入这三个条件同时成立时几乎是性价比最高的那个。识别效果确实不如云端大模型但在安静环境、近距离麦克风、常见口语表达这几个前提下够用。1.3 Vosk 的能力边界它不做什么用之前一定要知道它不做什么否则会在错误的方向上耗掉大量时间。第一它不做声纹识别虽然能返回词级置信度但那是给识别结果打分的不是用来判断说话人身份的。第二它不做语音唤醒没有低功耗常驻唤醒能力你想做喊一声就响应得另配一个唤醒词引擎。第三它不做翻译输出的就是原始语言的文本。还有一个容易被忽略的点Vosk 的模型是通用的不认识你的行业黑话、产品名、内部代号。我做过一个客服场景的项目模型把产品名识别成了完全不相干的词组后来通过词表约束才解决。所以选型阶段就要想清楚如果业务里有大量专有名词是准备用词表限制还是准备攒数据自己训练前者成本低但只适合命令词集合后者成本高但通用性更强。1.4 模型命名规则与体积对照Vosk 模型的命名有一定规律看名字基本能判断适用场景。带 small 的是精简版体积小、速度快、准确率略低不带 small 的是完整版体积大、效果好、吃内存。名字里的数字是版本号比如 0.22 就是比较新的一版。中文目前有两个主要模型英文、俄文等其他语种的模型数量更多选择余地更大。模型名称体积量级适用场景内存占用感受vosk-model-small-cn-0.22几十 MB树莓派、移动端、命令词加载后几百 MBvosk-model-cn-0.221 GB 上下桌面端、服务器、长音频转写加载后 1 GB 以上英文 small 模型几十 MB嵌入式、实时字幕几百 MB英文完整模型1 GB 以上高准确率转写1 GB 以上我的建议是先用 small 模型把所有流程跑通确认链路没问题再换大模型对比效果。很多人一上来就下大模型结果加载慢、识别慢还没验证代码对不对就先被性能问题劝退了。这个顺序反过来排查成本会低很多。2. 环境与模型准备把第一段中文识别跑通2.1 解释器与依赖安装的几个版本坑Python 侧安装其实就一行命令pip install vosk但这一行背后有几个坑。首先是 Python 版本vosk 的预编译轮子在 3.7 到 3.11 之间覆盖得比较好太新的版本比如刚发布的 3.13经常没有对应轮子pip 会退化成源码编译然后因为缺 C 工具链而失败。我现在的做法是固定用 3.10 或者 3.11 建虚拟环境不折腾新版本。其次是平台。Windows、Linux、macOS 都有轮子但树莓派这类 ARM 平台要额外注意架构64 位系统上装 32 位 Python 会找不到匹配的轮子。最后是网络如果下载超时可以换国内镜像源比如pip install vosk -i https://pypi.tuna.tsinghua.edu.cn/simple注意不要在同一台机器上混用系统 Python 和虚拟环境之前遇到过系统里装了 vosk、虚拟环境里没装代码跑起来报找不到模块查了半小时才发现是解释器选错了。2.2 模型下载与目录规划模型不在 pip 包里得单独去 Vosk 官网的模型列表页下载。下载下来的通常是 zip 文件解压后得到一个目录目录里包含 am、conf、graph、ivector 等子目录这些是 Kaldi 格式的模型文件。这里有个细节代码里传给 Model 的路径应该指向解压出来的那个包含上述子目录的目录而不是它的父目录。我习惯的目录结构是这样的方便同时放多个模型做对比project/ ├── models/ │ ├── cn-small/ │ │ ├── am/ │ │ ├── conf/ │ │ └── ... │ └── cn-big/ ├── audio/ └── main.py模型解压出来有几千个文件别用图形界面的解压工具一个个点直接用命令行解压快得多。另外建议把模型目录加进版本控制的忽略列表这些文件又大又不常变没必要每次提交都带上。2.3 最小可运行示例文件识别先拿 WAV 文件跑排除麦克风采集环节的干扰。下面这段代码是我最常用的模板import json import wave from vosk import Model, KaldiRecognizer, SetLogLevel # 关掉 Kaldi 的日志输出不然控制台会刷屏 SetLogLevel(-1) model Model(models/cn-small) rec KaldiRecognizer(model, 16000) # 打开词级信息会返回每个词的时间戳和置信度 rec.SetWords(True) wf wave.open(audio/test.wav, rb) while True: data wf.readframes(4000) if len(data) 0: break if rec.AcceptWaveform(data): result json.loads(rec.Result()) print(最终结果:, result.get(text)) else: partial json.loads(rec.PartialResult()) print(中间结果:, partial.get(partial)) print(收尾结果:, json.loads(rec.FinalResult()).get(text))这段代码里有三个概念必须理解否则后面做实时识别会卡住。第一AcceptWaveform 是流式的你不断往里喂音频块它自己维护状态。第二它的返回值是一个布尔返回 True 表示这段音频里检测到了句尾此时应该去调 Result 取最终结果返回 False 表示还在识别中调 PartialResult 取中间结果。第三最后一定要调一次 FinalResult把缓冲区里剩下的音频处理完否则结尾几个字会丢。2.4 音频格式的三个硬约束Vosk 对输入音频有三条硬性要求违反了不是报错就是结果乱七八糟单声道立体声必须下混否则左右声道会被当成一段奇怪的音频。16 位有符号整型 PCM浮点格式要先转换32 位整数也不行。采样率与创建识别器时传入的值一致模型本身是 16 kHz 训练的虽然你可以在 KaldiRecognizer 里传 8000但实际音频必须是 8000否则时间轴和音素全部错位。用 ffmpeg 转换是最省事的办法一条命令搞定ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le output.wav参数含义很直白-ar 16000 指定采样率-ac 1 指定单声道-c:a pcm_s16le 指定编码格式。转换完建议用代码里的断言再检查一遍尤其是别人给你的素材很多录音笔默认是 44.1 kHz 立体声直接丢进去必然出问题。3. 原理拆解Vosk 在后台忙什么3.1 声学模型、发音词典、语言模型三件套Vosk 的内核是 KaldiKaldi 那一套东西说穿了就是三个组件协同工作。声学模型负责把一小段一小段的音频特征映射成音素概率它不关心你说的是什么词。发音词典负责描述每个词由哪些音素按什么顺序组成也就是词的发音表。语言模型负责描述这些词按什么顺序出现的概率高比如今天天气后面接不错的概率远高于接冰箱。解码器的活儿就是在这三者的约束下找出一条概率最高的词序列。理解这一点非常有用因为它直接解释了后面很多调优手段为什么有效词表约束本质上是把语言模型的搜索空间砍掉音频前处理本质上是让声学模型的输入更干净换模型本质上是三件套同时换了一套。3.2 流式解码与端点检测实时识别最大的技术难点不是识别本身而是怎么判断这句话说完了。Vosk 内部有一套端点检测机制根据静音时长、能量变化来判断一句话的结束点。默认情况下静音持续大概半秒左右就会触发一次句尾判定AcceptWaveform 返回 True。这个阈值可以直接改配置在模型目录下的 conf 文件里涉及的关键项是尾随静音时长trailing silence。调小会让结果出得更快但会把一句话切成好几段调大会让结果更完整但延迟感明显。我做过客服质检场景把尾随静音调大了一点因为用户说话经常有停顿切碎了会影响后续的语义处理。3.3 采样率、位深、声道为什么不能随便改很多人不理解为什么偏偏是 16 kHz。原因是模型在训练时用的就是 16 kHz 音频特征提取阶段会把音频切成 25 毫秒一帧、帧移 10 毫秒然后做频谱分析得到梅尔滤波器组特征。如果送入的音频采样率不对同样的帧长对应的时间长度就变了频率轴的刻度也全错了声学模型看到的是一片完全陌生的东西。位深同理16 位整数能表示的范围是正负 32767这是 Kaldi 默认的音频输入格式。如果传进去的是浮点数内存里字节的排列方式不一样解码出来的就是噪声。所以遇到识别结果完全不着调的时候先别怀疑模型用代码把音频的三个参数打出来看一眼十有八九是格式问题。3.4 三个常用开关SetWords、SetGrammar、SetMaxAlternatives有三个方法用好了能省很多事。SetWords(True) 打开后返回结果里会带上每个词的开始时间、结束时间和置信度做字幕对齐的时候特别有用可以直接按词切分时间轴。SetMaxAlternatives(n) 让解码器返回 n 个候选结果取第一个通常是最优的但在噪声环境下有时第二个更靠谱可以做交叉校验。SetGrammar 是最实用的一个它接收一个 JSON 数组把识别范围限制在给定的词句集合里import json from vosk import Model, KaldiRecognizer grammar json.dumps([打开空调, 关闭空调, 调高温度, 调低温度, [unk]]) model Model(models/cn-small) rec KaldiRecognizer(model, 16000, grammar)这里的[unk]是个特殊标记代表其他内容不加的话所有不在词表里的语音都会被强行映射成词表里的某一条反而容易误触发。官方说明里提到词表约束在小模型上的表现明显好于大模型我自己的测试也印证了这一点小模型加词表约束命令词识别准确率提升非常明显大模型加约束反而偶尔会出现莫名其妙的降级。4. 实时麦克风识别完整工程实现4.1 采集链路选型麦克风采集有两条路一条是 PyAudio一条是 sounddevice。PyAudio 是老牌方案资料多但安装时依赖 PortAudioWindows 上要手动下轮子Linux 上要装开发包偶尔会卡在编译环节。sounddevice 底层是 PortAudio 的 Python 绑定接口更贴合 NumPy 的使用习惯安装也更省心。我现在默认用 sounddevice主要是因为它的 RawInputStream 可以直接吐 bytes跟 Vosk 需要的输入格式严丝合缝中间不需要做类型转换。如果你已经有一套基于 PyAudio 的代码在跑也不用换两者都能用关键是把回调里的数据原样丢进 Vosk 就行。4.2 主循环代码与关键参数下面是我在项目里实际用的实时识别模板稍微改改就能上import json import queue import sounddevice as sd from vosk import Model, KaldiRecognizer, SetLogLevel SetLogLevel(-1) audio_queue queue.Queue() def on_audio(indata, frames, time_info, status): if status: print(音频状态异常:, status) audio_queue.put(bytes(indata)) model Model(models/cn-small) rec KaldiRecognizer(model, 16000) rec.SetWords(True) with sd.RawInputStream( samplerate16000, blocksize8000, dtypeint16, channels1, callbackon_audio): while True: data audio_queue.get() if rec.AcceptWaveform(data): text json.loads(rec.Result()).get(text, ) if text: print(识别:, text) else: partial json.loads(rec.PartialResult()).get(partial, ) if partial: print(实时:, partial, end\r)几个参数需要解释。samplerate 必须是 16000跟模型匹配。blocksize 我设 8000对应 16 kHz 下 0.5 秒的数据量这个值直接决定识别结果的刷新频率设小了 CPU 唤醒频繁设大了实时感变差。dtype 和 channels 分别是 int16 和 1对应前面说的格式约束。回调函数里绝对不能做耗时操作否则会阻塞音频线程导致丢帧所以必须用队列把数据转出去在另一个线程里处理。4.3 partial 与 final 的正确用法中间结果和最终结果用错场景体验会差很多。中间结果适合做边说边显示的实时字幕它不稳定同一个字可能反复变化因为它只是当前时刻的最优猜测。最终结果才是稳定的适合作为后续业务逻辑的输入比如触发命令、写入数据库、送去翻译。我踩过的一个坑是把中间结果直接拿去做语义判断。结果就是同一个词被识别出来三次命令被触发了三遍。后来改成只在最终结果上做判断中间结果只用于界面展示问题就没了。如果你的业务对响应速度要求高可以折中一下对中间结果做防抖处理同一个命令在短时间内只响应一次。4.4 实测性能与调优性能这块我只能给量级上的参考因为差异太大了。在一台普通的 i5 笔记本上small 中文模型处理一段 10 秒的音频大概耗时 2 秒左右也就是说实时率大致在 0.2 附近跑实时麦克风识别完全没有压力。换成 1 GB 多的完整中文模型实时率会到 0.8 甚至接近 1.0也就是刚好跟上说话速度稍微复杂一点的句子就会积压。有两条调优路径。一是降模型规模实时场景优先用 small 模型除非准确率实在不达标。二是控制音频链路把 blocksize 调大一点、减少回调次数同时确保回调里不做任何阻塞操作。还有一个容易被忽略的点模型加载本身要花时间大模型可能要几秒到十几秒千万别在每次识别前重新 new 一个 Model正确做法是全局加载一次然后复用。5. 把识别率往上抬的几个实操手段5.1 模型选择小模型和大模型差在哪小模型和大模型的差距在安静近距离场景下没那么夸张大概就是有几个字听错和基本全对的区别。但一旦换到远场、有混响、有背景噪声的环境差距会被放大。我的经验是如果是耳机麦克风近距离使用small 模型够用如果是会议室吊顶麦克风、车载、或者设备有一定距离直接上完整模型省下的调参时间比省下的内存值钱。还有一种折中做法就是做双模型策略常驻一个 small 模型做唤醒和命令词识别到特定指令后再切换到完整模型做长句识别。这样兼顾了响应速度和准确率代价是内存里要同时装两套模型或者做动态加载但动态加载会有几秒延迟。5.2 音频前处理增益、重采样、降噪的取舍音频前处理的原则是能少做就少做因为每一步处理都可能引入失真。必须做的是格式转换也就是重采样和声道下混。增益要谨慎Vosk 对音量不是特别敏感但音量过低会导致信噪比恶化过高则会削波两种情况都会掉准确率。落在一个不削波的合理范围内就行。降噪我一般不建议在送进 Vosk 之前做重度处理。原因很简单声学模型是在带噪数据上训练的它有相当的抗噪能力而各种降噪算法在去噪的同时会损伤语音的高频成分反而让声学模型看到不熟悉的特征。如果确实需要处理优先考虑物理手段换一个指向性更好的麦克风或者给麦克风加个防风罩。5.3 用词表约束把开放识别变成命令词识别如果你的业务本质上是命令词识别比如打开关闭上一首下一首这种千万别用开放识别再去匹配字符串那样既慢又容易错。用 SetGrammar 把词表收紧识别空间从几十万词降到几十个速度和准确率都是另一个量级。实践中有个细节词表里最好把常见的同音误识别也加进去。比如播放经常被听成波放你可以在词表里同时写这两种然后在上层做归一化映射。另外词表别写太长几百条以内效果比较稳定上千条之后约束的收益就开始递减了。5.4 关于男女声识别差异的工程解释经常有人问为什么同样的模型女声识别率看起来比男声低。这个问题没有单一答案但有几个工程上说得通的原因。一是训练数据的性别比例早期的公开语音数据集里男性说话人占比偏高模型对男性发音的建模更充分。二是基频差异女性基频通常在 200 Hz 以上男性在 100 Hz 上下这带来两个连带影响谐波间隔更大在窄带音频下高频共振峰更容易超出有效带宽同时基频与第一共振峰的区分更困难给特征提取带来额外难度。三是麦克风频响很多廉价麦克风在低频响应上偏强这反而对男声有利。好消息是这个问题在新版模型上改善明显训练数据的性别平衡做得好很多。如果你确实遇到某类声音识别率明显偏低别急着怀疑原理先做两件事单独测一组同性别说话人的样本看看差异是不是稳定存在把音频用同一套前处理流程过一遍再对比很多时候问题出在采集设备而不是模型。5.5 长音频切分与批处理做录音转写的时候别把整个文件一次性塞进识别器。一是内存会涨二是中间结果无法控制三是万一某个环节出错整批都得重来。我的做法是按静音点切分先用简单的能量检测把音频切成一段段每段几秒到几十秒然后逐段识别再拼接。Vosk 的 Python 包里其实带了一个命令行工具可以直接转写文件内部就是用 ffmpeg 做解码、用模型做识别适合做批处理脚本的基础vosk-transcriber -i audio/meeting.mp3 -o output.txt -l cn这个命令依赖 ffmpeg所以得先保证 ffmpeg 在 PATH 里。批量处理的时候建议写个脚本遍历目录每处理完一个文件记录状态避免中途崩溃后全部重跑。6. 踩坑与排查实录6.1 安装与加载类问题最常见的问题是导入失败和模型加载失败这两个基本占了新手问题的一大半。导入失败多半是解释器选错或者平台不匹配先执行 pip show vosk 看装到哪个环境里了再确认当前 Python 是不是那个环境。模型加载失败通常是路径问题路径里不要有中文和空格传给 Model 的目录要包含 am、conf 这些子目录。还有一个隐蔽的坑是模型和代码版本不匹配。Vosk 的 Python 包一直在更新如果模型是很老的一版偶尔会出现加载异常。遇到这种情况先更新一下包再试。6.2 结果异常类问题速查表下面这张表是我实际遇到过的现象和对应的排查方向出问题的时候按这个顺序过一遍基本能定位到八成的情况。现象可能原因排查动作识别结果为空音频全是静音或音量极低打印音频数据的最大振幅值结果全是乱码词位深或采样率不匹配检查是否为 16 位、16 kHz结果只剩半句没调用 FinalResult循环结束后补一次收尾调用识别延迟越来越大回调里做了耗时操作确认音频线程只做入队结果重复触发命令用了 partial 做判断改为只在 final 上判断特定词总是错模型不含该词用词表约束或做上层替换6.3 性能与内存类问题CPU 打满通常是因为模型太大加上音频块太小导致解码调用过于频繁。解决办法是换小模型、把 blocksize 调大、以及确认没有多个识别器同时跑。这里要提醒一点KaldiRecognizer 不是线程安全的一个识别器只能在一个线程里用。如果你要处理多路音频正确做法是每路音频一个独立的识别器但 Model 对象可以共享它是只读的。内存持续增长一般不是 Vosk 的问题而是代码里队列没有消费完或者结果对象被一直引用没有释放。长时间运行的服务一定要检查队列长度加个上限超了就丢弃旧数据否则音频生产速度快于消费速度时内存会一路涨上去。6.4 ESP32 这类设备为什么跑不动 Vosk经常有人想把 Vosk 塞进单片机做离线语音这里必须泼一盆冷水跑不动。原因不是代码优化问题而是资源量级不匹配。哪怕是 Vosk 最小的中文模型也有几十兆加载后常驻内存要几百兆而典型单片机只有几百 KB 的片上内存加了外部存储也就几兆到十几兆差了两个数量级。这类设备的正确路线是两分法本地做关键词唤醒和命令词识别用专门为单片机设计的小型模型需要长句识别时再走云端接口。前者响应快、功耗低后者识别能力强但依赖网络。把这两种能力组合起来才是嵌入式语音交互的常规做法。至于具体的云端服务选择各家都有开放的语音识别接口接入方式大同小异按项目所在地的网络条件和服务条款来选就行。7. 再往上走一步从识别到可用的产品能力7.1 语音识别和机器翻译的边界在哪这两个概念经常被混在一起说其实分工非常清楚。语音识别解决的是把声音变成文字输入是音频输出是文字的原始语言。机器翻译解决的是把一种文字变成另一种文字输入输出都是文本。两者级联起来就是常说的语音翻译先识别出原文再把原文翻译成目标语言。理解这个边界在实际做方案时很有用。Vosk 只负责第一段它输出的文本你可以直接喂给任意翻译组件也可以先做一轮文本纠错再翻译。级联方案的好处是每一环都可以单独替换和优化缺点是误差会累积识别错了一个字翻译就可能跑偏。如果你的场景对翻译质量要求高可以在识别结果上做一个领域词典的替换把专有名词先修正再送翻译。7.2 服务化部署的基本思路单机跑通之后下一步通常是把它变成服务。思路是把模型加载和识别逻辑封装成一个长驻进程对外暴露 WebSocket 或者 HTTP 接口客户端只负责采集音频和播放结果。这么做的最大好处是模型只加载一次多个客户端共享内存利用率高很多。工程上有几个点要注意。一是模型加载放在进程启动阶段不要放在请求处理里。二是做好并发控制识别器不共享每个连接一个。三是加心跳和超时音频流断了要能及时释放资源否则跑几天就会积累一堆僵尸识别器。四是对识别结果做限流避免短时间内大量请求把 CPU 打满。7.3 一些个人体会我用 Vosk 做过三个不同量级的项目感受最深的一点是语音识别的准确率从来不是单一变量决定的它是模型、音频质量、使用场景三者共同作用的结果。很多人把精力全花在换模型上却忽略了麦克风摆放位置和音频格式收益其实完全不成比例。另一个体会是一定要先建立评测集。随便录二十条你实际业务里会说的话标注好正确答案每次调整参数后跑一遍记录准确率变化。听起来很笨但这是唯一能让你判断这次改动到底有没有用的方法。没有评测集的时候你只能靠感觉而感觉在语音识别这件事上非常不靠谱因为它对噪声和说话方式的变化太敏感了。