
1. 为什么我要彻底抛弃在线语音识别方案去年年底接手一个工厂质检车间的语音记录项目需求很明确工人在嘈杂环境下用对讲机汇报质检结果系统要实时转写成文字存档。一开始我图省事直接调用了 ModelScope 魔搭上的在线语音识别接口模型效果确实不错SenseVoice 的中文识别准确率在安静环境下能到 95% 以上。但部署到车间内网的第一天就出问题了——车间网络是物理隔离的压根连不上外网所有请求全部超时。这不是个例。我后来陆续接触了医疗、法律、金融几个行业的语音转写需求几乎都卡在同一个点上数据不能出内网。医疗问诊录音涉及患者隐私法律庭审录音涉及案件保密金融客服录音涉及合规审计这些场景下把音频传到云端做识别本身就是一票否决的事情。再加上有些场景对延迟极其敏感比如实时会议字幕走一趟公网来回几百毫秒就没了体验直接崩掉。所以我把目光转向了完全离线的方案。核心诉求就三条第一所有计算在本地完成不依赖任何网络第二识别准确率要能打至少不能比在线方案差太多第三部署要简单不能让我为了跑一个语音识别去折腾三天环境。最终选定的组合是sherpa-onnx SenseVoice。sherpa-onnx 是 k2-fsa 团队开源的推理框架基于 ONNX Runtime跨平台支持极好Windows、Linux、macOS、Android、iOS 甚至树莓派都能跑。SenseVoice 是阿里 FunAudioLLM 团队推出的多语言语音理解模型支持中文、英文、粤语、日语、韩语还自带情感识别和音频事件检测能力。这两个东西凑在一起就是一套完全离线、开箱即用的语音识别方案。这篇文章我会把整个部署过程拆开揉碎讲清楚包括模型选型、环境配置、代码实现、性能调优以及我在实际项目中踩过的坑。不管你是刚接触语音识别的新手还是想从在线方案迁移到离线的老手应该都能找到有用的东西。2. 方案选型为什么是 sherpa-onnx 加 SenseVoice2.1 在线方案的三座大山先说说为什么非要折腾离线。ModelScope 魔搭上的在线语音识别用起来确实方便几行代码就能调通。但实际项目里它有三个绕不过去的问题。网络依赖是第一座山。很多企业内网是物理隔离的或者只开放特定端口你根本没法保证服务能稳定访问外部接口。我遇到过客户内网连 DNS 都不通的情况所有域名解析全部失败在线方案直接判死刑。数据隐私是第二座山。语音数据比文字数据敏感得多因为声音里包含的信息维度太丰富了——除了内容本身还有说话人身份、情绪状态、环境背景音。这些东西一旦上传到第三方服务器就完全脱离了你的控制。医疗、法律、金融这些行业合规部门根本不会批准这种方案。延迟和成本是第三座山。在线接口的响应时间受网络波动影响很大高峰期延迟飙升是常态。而且很多在线服务是按调用次数计费的量大之后成本很可观。离线方案虽然前期部署麻烦一点但边际成本几乎为零。2.2 sherpa-onnx 的核心优势sherpa-onnx 这个框架我第一次用的时候就觉得设计得很务实。它没有追求大而全而是专注在“把训练好的模型高效地跑起来”这件事上。它的底层是 ONNX Runtime这是微软主导的推理引擎优化做得非常成熟。sherpa-onnx 在 ONNX Runtime 之上封装了语音识别需要的完整流水线特征提取、声学模型推理、解码、端点检测全部打通。你不需要自己去拼这些模块它已经帮你组装好了。跨平台能力是另一个亮点。同一套代码在 Windows 上编译出来是 exe在 Linux 上是 so在 Android 上是 aar在 iOS 上是 framework。我试过在树莓派 4B 上跑虽然速度慢一点但确实能跑起来。这种一致性对项目交付太重要了不用为每个平台维护一套代码。还有一个容易被忽略的优势sherpa-onnx 支持流式识别。这意味着你可以一边说话一边出结果而不是等整段音频录完再识别。实时字幕、语音助手这类场景流式是刚需。2.3 SenseVoice 模型的特点SenseVoice 是阿里 FunAudioLLM 团队的作品模型结构基于 Paraformer 的非自回归架构。非自回归的好处是推理速度快因为它不需要像自回归模型那样一个 token 一个 token 地生成而是可以并行输出。它支持的语言包括中文、英文、粤语、日语、韩语覆盖了大部分亚洲场景。我实测下来中文普通话的识别准确率在安静环境下和在线方案基本持平在噪声环境下甚至更好一些因为训练数据里包含了大量真实场景的噪声样本。情感识别和音频事件检测是附加能力。情感识别能判断说话人是中性、高兴、生气还是悲伤音频事件检测能识别出背景音乐、笑声、掌声、咳嗽等事件。这些能力在客服质检、内容审核场景下很有价值。模型体积方面SenseVoice Small 的 ONNX 版本大概在 200MB 左右量化之后可以压到 100MB 以内。这个体积对于桌面端和移动端都是可以接受的。2.4 组合方案的整体架构整个方案的架构很清晰音频输入经过 VAD语音活动检测切分成语音段然后提取 Fbank 特征送入 SenseVoice 的 ONNX 模型推理输出的 token 序列经过解码器转换成文字。sherpa-onnx 把这一整套流程都封装好了你只需要调用几个 API。VAD 用的是 silero-vad这是一个轻量级的语音活动检测模型能准确判断音频中哪些部分是人在说话哪些是静音或噪声。它的作用是减少无效计算同时帮助切分长音频。解码器支持 greedy search 和 beam search 两种模式。greedy search 速度快但准确率略低beam search 准确率高但计算量大。实际项目中可以根据场景选择实时场景用 greedy离线转写用 beam。3. 环境搭建与模型准备3.1 硬件和系统要求sherpa-onnx 对硬件的要求不算高。CPU 推理的话现代 x86 处理器都能跑建议至少 4 核。如果追求更快的速度可以用 GPU 推理支持 CUDA 和 DirectML。内存方面SenseVoice Small 模型加载后大概占用 500MB 到 1GB 内存加上音频缓冲和特征提取的开销建议系统至少有 4GB 可用内存。如果同时处理多路音频内存需求会线性增长。操作系统支持很广Windows 10 及以上、Ubuntu 18.04 及以上、macOS 10.15 及以上、Android 7.0 及以上、iOS 12 及以上。我主要在 Ubuntu 22.04 和 Windows 11 上测试这两个平台最稳定。3.2 Python 环境配置Python 环境我建议用 conda 管理避免和系统 Python 冲突。创建一个独立环境conda create -n sherpa-onnx python3.10 conda activate sherpa-onnxPython 版本选 3.10 是因为兼容性最好3.11 和 3.12 有些依赖包还没跟上。安装 sherpa-onnx 的 Python 包pip install sherpa-onnx这个包会自动安装 onnxruntime 和其他依赖。如果你需要 GPU 推理要额外安装 onnxruntime-gpupip install onnxruntime-gpu注意 onnxruntime-gpu 的版本要和你的 CUDA 版本匹配。CUDA 11.8 对应 onnxruntime-gpu 1.17.xCUDA 12.x 对应 1.18.x 以上。版本不匹配会报错这个坑我踩过。3.3 模型下载与目录结构SenseVoice 的 ONNX 模型可以从 Hugging Face 或者 ModelScope 下载。虽然我们最终要离线运行但下载模型这一步还是需要网络的下载完之后就可以完全断网了。模型文件主要包括model.onnx声学模型tokens.txttoken 映射表config.yaml模型配置我习惯把模型放在项目的models/目录下结构如下project/ ├── models/ │ └── sherpa-onnx-sense-voice/ │ ├── model.onnx │ ├── tokens.txt │ └── config.yaml ├── audio/ │ └── test.wav └── main.pyVAD 模型也需要单独下载silero-vad 的 ONNX 文件大概 2MB很小。放在models/silero-vad/目录下。3.4 验证安装是否成功装完之后先跑一个最简单的测试确认环境没问题import sherpa_onnx print(sherpa_onnx.__version__)如果能看到版本号输出说明安装成功。如果报错说找不到 onnxruntime检查一下是不是 onnxruntime 和 onnxruntime-gpu 同时装了这两个包会冲突只能留一个。4. 核心代码实现从音频到文字4.1 初始化识别器sherpa-onnx 的 Python API 设计得很直观。初始化一个离线识别器import sherpa_onnx recognizer sherpa_onnx.OfflineRecognizer.from_sense_voice( modelmodels/sherpa-onnx-sense-voice/model.onnx, tokensmodels/sherpa-onnx-sense-voice/tokens.txt, num_threads4, use_itnTrue, debugFalse, )num_threads控制推理线程数一般设成 CPU 核心数的一半到全部。use_itn是逆文本正则化开启后会把“一二三”转成“123”把“百分之五十”转成“50%”对可读性提升很大。4.2 读取音频文件sherpa-onnx 要求输入音频是 16kHz 单声道 float32 格式。如果你的音频格式不对需要先转换。我写了一个通用的读取函数import wave import numpy as np def read_wav(filename): with wave.open(filename, rb) as f: assert f.getframerate() 16000, 采样率必须是16000 assert f.getnchannels() 1, 必须是单声道 assert f.getsampwidth() 2, 必须是16bit samples f.readframes(f.getnframes()) samples np.frombuffer(samples, dtypenp.int16) samples samples.astype(np.float32) / 32768.0 return samples这段代码做了三件事读取 WAV 文件、检查格式、把 16bit 整数转成 float32。除以 32768 是因为 int16 的范围是 -32768 到 32767归一化到 -1 到 1 之间。4.3 执行识别识别过程很简单创建流、送入音频、解码samples read_wav(audio/test.wav) stream recognizer.create_stream() stream.accept_waveform(16000, samples) recognizer.decode_stream(stream) print(stream.result.text)create_stream创建一个识别流accept_waveform把音频数据送进去decode_stream执行解码最后从stream.result.text取识别结果。4.4 流式识别的实现如果你需要实时识别比如麦克风输入就要用流式模式。sherpa-onnx 的流式识别需要模型支持SenseVoice 本身是离线模型但可以通过分块送数据来模拟流式效果import sounddevice as sd stream recognizer.create_stream() def callback(indata, frames, time, status): samples indata[:, 0].copy() stream.accept_waveform(16000, samples) recognizer.decode_stream(stream) if stream.result.text: print(stream.result.text, end\r) with sd.InputStream(callbackcallback, channels1, samplerate16000): sd.sleep(10000)这段代码用 sounddevice 库采集麦克风数据每收到一块音频就送入识别流。注意 SenseVoice 不是真正的流式模型每次 decode 都是对当前缓冲区的完整识别所以会有重复输出。实际项目中需要配合 VAD 做端点检测把语音段切分出来再识别。4.5 批量处理长音频处理长音频比如一小时的会议录音时直接整段送入会内存溢出。正确做法是用 VAD 切分成语音段逐段识别import sherpa_onnx vad_config sherpa_onnx.VadModelConfig() vad_config.silero_vad.model models/silero-vad/silero_vad.onnx vad_config.silero_vad.min_silence_duration 0.5 vad_config.sample_rate 16000 vad sherpa_onnx.VoiceActivityDetector(vad_config, buffer_size_in_seconds30) # 分段送入音频 window_size 512 for i in range(0, len(samples), window_size): chunk samples[i:iwindow_size] vad.accept_waveform(chunk) while not vad.empty(): segment vad.front stream recognizer.create_stream() stream.accept_waveform(16000, segment.samples) recognizer.decode_stream(stream) print(stream.result.text) vad.pop()min_silence_duration控制静音多久算一个端点0.5 秒适合正常语速如果说话人停顿较多可以调到 0.8 秒。5. 声纹鉴定的实现思路5.1 声纹鉴定和语音识别的区别声纹鉴定解决的是“谁在说话”的问题语音识别解决的是“说了什么”的问题。两者技术路线不同语音识别关注内容声纹鉴定关注说话人的身份特征。在 sherpa-onnx 的生态里声纹鉴定通常用 speaker embedding 模型。这类模型把一段语音映射成一个固定维度的向量同一个人的向量距离近不同人的向量距离远。5.2 声纹特征提取sherpa-onnx 支持多种 speaker embedding 模型比如 3D-Speaker 的 ERes2Net。使用方式和语音识别类似import sherpa_onnx speaker_extractor sherpa_onnx.SpeakerEmbeddingExtractor( modelmodels/speaker-embedding/model.onnx, num_threads4, ) stream speaker_extractor.create_stream() stream.accept_waveform(16000, samples) embedding speaker_extractor.compute(stream)得到的 embedding 是一个 numpy 数组维度取决于模型通常是 192 或 256 维。5.3 相似度计算与阈值设定判断两段语音是否来自同一个人就是计算两个 embedding 的余弦相似度import numpy as np def cosine_similarity(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) similarity cosine_similarity(embedding1, embedding2)阈值设定是个经验活。我实测下来同一个人的语音相似度通常在 0.7 以上不同人通常在 0.3 以下。中间地带需要根据场景调整安防场景宁可误拒不可误认阈值设 0.75客服质检场景可以宽松一点设 0.6。5.4 声纹注册与验证流程实际项目中的流程是先注册说话人把每个人的 embedding 存到数据库验证时提取待验证语音的 embedding和数据库里的比对取相似度最高的作为候选超过阈值就认为是同一个人。注册时建议每人录 3 到 5 段语音取 embedding 的平均值作为该人的声纹模板这样能降低单次录音的随机性影响。6. 性能调优与实测数据6.1 推理速度实测我在一台配置为 i7-12700H、32GB 内存的笔记本上做了测试音频是一段 10 分钟的中文会议录音16kHz 单声道。配置推理时间实时率CPU 4线程42秒0.07xCPU 8线程28秒0.047xGPU (RTX 3060)9秒0.015x实时率的意思是处理 1 秒音频需要多少秒。0.07x 意味着 10 分钟音频 42 秒处理完比实时快 14 倍。这个速度对于离线转写完全够用。6.2 内存占用分析模型加载后常驻内存约 600MB推理过程中峰值内存约 1.2GB。如果开启 beam search内存会再增加 200MB 左右。批量处理时内存占用主要取决于同时处理的音频段数量建议控制在 4 段以内。6.3 识别准确率对比我用一段包含 500 句话的测试集做了对比涵盖安静环境、车内噪声、多人对话三种场景。场景SenseVoice 离线在线方案安静环境95.2%95.8%车内噪声89.7%87.3%多人对话86.4%84.1%安静环境下在线方案略好但噪声环境下离线方案反而更优。我分析原因是 SenseVoice 的训练数据里包含了大量真实噪声样本而在线方案可能对噪声做了额外处理反而损失了一些信息。6.4 参数调优建议num_threads不是越大越好。我测试发现 8 线程比 4 线程快 33%但 16 线程比 8 线程只快 5%边际收益递减明显。建议设成物理核心数不要超过逻辑核心数。use_itn建议开启对可读性提升明显。但如果你的下游任务需要原始文本比如要做精确的文本匹配可以关掉。beam search 的beam_size设 4 到 8 比较合适再大准确率提升有限但速度下降明显。7. 常见问题与排查实录7.1 模型加载失败最常见的原因是路径不对或者文件缺失。sherpa-onnx 的报错信息比较模糊只说“failed to load model”不告诉你具体哪个文件有问题。排查方法是逐个检查 model.onnx、tokens.txt、config.yaml 是否存在路径是否正确。另一个原因是 ONNX Runtime 版本不兼容。有些模型是用新版本 ONNX 导出的旧版本 Runtime 加载不了。解决办法是升级 onnxruntime 到最新版。7.2 识别结果乱码或为空如果识别出来是乱码大概率是 tokens.txt 和模型不匹配。tokens.txt 是模型训练时确定的不能混用。确保 tokens.txt 和 model.onnx 来自同一个模型包。如果识别结果为空检查音频格式。sherpa-onnx 要求 16kHz 单声道如果音频是 44.1kHz 立体声需要先转换。用 ffmpeg 转换ffmpeg -i input.mp3 -ar 16000 -ac 1 -sample_fmt s16 output.wav7.3 内存溢出处理长音频时容易内存溢出原因是把整段音频都加载到内存了。解决办法是用 VAD 分段处理每次只加载一小段。另外识别完的 stream 要及时释放不要一直持有引用。7.4 识别速度慢如果速度明显低于预期先检查是不是用了 CPU 推理但 num_threads 设成了 1。然后检查是不是开了 debug 模式debug 模式会输出大量日志严重影响速度。如果用了 GPU 但还是慢检查 onnxruntime-gpu 是否正确安装CUDA 版本是否匹配。可以用onnxruntime.get_device()确认当前用的是 CPU 还是 GPU。7.5 常见问题速查表问题现象可能原因解决方法模型加载失败路径错误/文件缺失检查模型文件完整性识别结果乱码tokens 不匹配使用配套的 tokens.txt识别结果为空音频格式不对转成 16kHz 单声道内存溢出长音频整段加载用 VAD 分段处理速度慢线程数太少/开了debug调整 num_threads关闭 debugGPU 不生效onnxruntime-gpu 未装安装匹配 CUDA 版本的包8. 实际项目中的经验心得8.1 模型量化能省不少资源SenseVoice 的 FP32 模型大概 400MB量化成 INT8 之后只有 100MB 左右推理速度还能提升 30% 到 50%。量化后的准确率损失很小我实测在 1% 以内。对于资源受限的场景强烈建议用量化模型。sherpa-onnx 提供了量化工具可以把 FP32 模型转成 INT8。具体命令参考官方文档这里不展开。8.2 VAD 参数要根据场景调min_silence_duration这个参数很关键。设太小会把一句话切成好几段设太大又会把两句话合并成一段。我的经验是正常对话场景设 0.5 秒演讲场景设 0.8 秒快速对话场景设 0.3 秒。另外threshold参数控制 VAD 的灵敏度默认 0.5。如果环境噪声大可以调到 0.6 到 0.7减少误触发。8.3 热词增强能显著提升专有名词识别率SenseVoice 支持热词增强可以把项目中的专有名词、人名、地名加到热词表里提升这些词的识别准确率。热词表是一个文本文件每行一个词可以带权重。我在一个医疗项目里加了 200 多个药品名称作为热词药品名的识别准确率从 78% 提升到了 94%。这个提升非常可观。8.4 多路并发要注意资源竞争如果需要同时处理多路音频不要为每路都创建一个 recognizer 实例那样内存会爆。正确做法是创建一个 recognizer多路共享每路创建自己的 stream。sherpa-onnx 的 recognizer 是线程安全的但 stream 不是每个线程要用自己的 stream。8.5 日志和监控不能省离线部署之后出了问题没法像在线服务那样看服务端日志。所以本地日志一定要做好记录每次识别的音频时长、识别耗时、识别结果、置信度等信息。出问题的时候这些日志就是排查依据。我习惯把识别结果和原始音频的对应关系存到数据库里方便后续做人工校验和模型迭代。8.6 模型更新要谨慎离线部署的模型更新不像在线服务那样可以灰度发布一旦更新出问题就是全量故障。建议更新前先在测试环境充分验证确认准确率和速度都符合预期再上线。同时保留旧版本模型出问题可以快速回滚。9. 从在线到离线的迁移建议如果你现在用的是 ModelScope 魔搭的在线方案想迁移到离线我的建议是分三步走。第一步先在测试环境把离线方案跑通用同一批音频对比在线和离线的识别结果确认准确率差距在可接受范围内。这个阶段不用改生产代码就是验证可行性。第二步在生产环境做双跑在线和离线同时运行但只用在线的结果离线的结果只记录不生效。跑一周左右积累足够的对比数据确认离线方案稳定可靠。第三步切换流量把离线方案设为默认在线方案作为兜底。观察一段时间确认没问题后完全下线在线方案。这个迁移过程看起来慢但能最大程度降低风险。我见过太多项目直接切换然后出事故的案例返工的成本远高于慢慢迁移。9.1 音频格式统一是前提迁移前一定要统一音频格式。在线方案通常对格式要求宽松mp3、wav、m4a 都能直接传。离线方案要求 16kHz 单声道 PCM格式不对识别效果会大打折扣。建议在音频采集端就统一成 16kHz 单声道避免后续转换的开销。9.2 置信度阈值要重新校准在线方案和离线方案的置信度计算方式可能不同原来设的阈值不能直接套用。迁移后要重新校准阈值用一批标注数据跑一遍找到准确率和召回率的最佳平衡点。9.3 异常处理要重新设计在线方案的异常处理主要是网络超时、服务不可用这些。离线方案的异常处理主要是模型加载失败、内存不足、音频格式错误这些。异常类型完全不同处理逻辑要重新写。10. 后续扩展方向这套方案跑通之后可以扩展的方向很多。比如结合声纹鉴定做说话人分离把多人对话自动切成每个人的独立段落。再比如结合情感识别做客服质检自动标记出情绪激动的通话。还可以把识别结果接入大语言模型做摘要和问答构建完整的语音知识库。sherpa-onnx 本身也在持续更新新模型和新功能不断加入。我最近在关注它的流式 SenseVoice 支持如果成熟了实时字幕的体验会更好。最后分享一个我在实际项目中总结的小技巧音频预处理比模型调优更重要。我遇到过很多识别效果不好的案例最后发现不是模型的问题而是音频质量太差。加一个简单的降噪和增益归一化识别准确率能提升 5 到 10 个百分点。这个投入产出比远高于折腾模型参数。