☰
Android端侧语音识别与合成:sherpa-onnx集成实战与性能优化
2026/10/18 5:41:46 网站建设 项目流程

1. 为什么要在手机本地跑语音模型

把语音识别和语音合成塞进手机,这件事放在三四年前,很多人的第一反应是"不现实"。云端API调用多方便,为什么要折腾端侧?但这两年情况变了。一方面,用户对隐私和响应速度的要求越来越高,录音上传到云端再等结果,那个延迟感在实时对话场景里是致命的;另一方面,端侧算力确实上来了,中端手机跑个量化后的小模型已经不怎么吃力。

我最初接触 sherpa-onnx 是因为一个离线字幕的需求。场景很简单:用户录一段视频,希望在本地生成字幕文件,不想联网,也不想把音频传到任何服务器。试过几个方案之后,sherpa-onnx 是少数能在 Android 上把 ASR 和 TTS 都跑通、而且部署相对干净的选择。它基于 onnxruntime,模型是 ONNX 格式,跨平台一致性很好,Android、iOS、桌面端用的是同一套推理后端,这对维护多端项目的人来说省心不少。

这篇文章面向的是有一定 Android 开发基础、想把语音能力落到端侧的开发者。如果你正在纠结"端侧语音到底能不能用""模型怎么选""Android 上怎么集成",下面这些内容应该能帮你少走一些弯路。我会从整体设计思路讲起,然后拆解模型选型、集成步骤、参数调优,最后把我在实际项目里踩过的坑整理出来。

需要先说明一点:sherpa-onnx 本身是一个推理框架和模型集合,它不负责帮你训练模型,也不提供现成的云端服务。你要做的是选一个合适的预训练模型,把它和 onnxruntime 一起打包进 APK,然后在 Android 层写好音频采集、推理调用和结果回调。听起来步骤不多,但每一步都有细节。

2. 整体方案设计与技术选型思路

2.1 端侧语音方案的核心矛盾

端侧跑语音模型,本质上是在三个维度上做权衡:模型精度、推理速度、包体积。这三个指标互相拉扯,你不可能同时把三个都拉满。

云端方案之所以精度高,是因为可以用几百兆甚至上G的大模型,算力无限。端侧不行,一个中端手机的可用内存和算力有限,APK 体积也不能太夸张。所以端侧方案的第一件事就是接受"精度会有损失",然后在这个前提下尽量把体验做好。

sherpa-onnx 的思路是用 ONNX Runtime 做推理引擎,模型经过量化和结构优化,把参数量压到几十兆甚至几兆的级别。它的 ASR 模型有流式和非流式两种,TTS 模型也有多种声学模型和声码器组合。选型的时候,你要先明确自己的场景是"实时"还是"离线整段"。

2.2 流式与非流式的选择逻辑

流式 ASR 的特点是边说边出结果,延迟低,适合实时字幕、语音助手这类场景。它的代价是模型结构更复杂,通常用 chunk-based 的编码器,精度会比非流式略低一点。非流式 ASR 是等你说完一整段再识别,精度更高,适合录音转写、会议纪要这种场景。

我个人的经验是:如果交互场景对延迟敏感(比如语音输入法、实时翻译),优先选流式模型;如果是"录完再处理"的场景,非流式模型更划算,精度和稳定性都更好。sherpa-onnx 里流式模型通常带streaming字样,非流式则没有,选模型的时候看文件名就能区分。

TTS 这边相对简单一些,主要看音色和推理速度。sherpa-onnx 支持的 TTS 模型有 VITS、Matcha 等,VITS 音质好但推理稍慢,Matcha 更快但音色选择少一些。如果你的场景是"读一段文字",对实时性要求不高,VITS 是更稳妥的选择。

2.3 为什么选 ONNX Runtime 而不是其他推理框架

Android 上能用的推理框架不少,TensorFlow Lite、NCNN、MNN 都有人用。sherpa-onnx 选 ONNX Runtime 有几个实际好处:

  • 模型生态统一:ONNX 是模型交换格式,PyTorch、TensorFlow 训练的模型都能转过来,不用为每个框架单独适配。
  • 算子覆盖全:语音模型里有些特殊算子(比如某些归一化层),ONNX Runtime 的支持比较完整,不容易遇到"算子不支持"的尴尬。
  • 跨平台一致:同一份模型在 Android、iOS、Linux 上行为一致,调试的时候可以在桌面端先验证,再往手机上搬。

当然它也有代价,ONNX Runtime 的库体积不算小,Android 上大概会增加几兆到十几兆的 APK 体积,具体取决于你编译时保留的算子。这个后面在集成部分会细说。

2.4 整体架构分层

一个典型的端侧语音应用,架构上可以分成四层:

层级职责关键组件
音频采集层麦克风采集、重采样、分帧AudioRecord、Oboe
推理层模型加载、前向计算ONNX Runtime、sherpa-onnx JNI
业务逻辑层结果拼接、标点、状态管理Kotlin/Java 业务代码
展示层UI 渲染、交互反馈Compose/View

这个分层的好处是每层职责清晰,音频采集和推理解耦,方便单独调试。比如你怀疑是采集有问题,可以先把音频存成文件,用桌面端工具验证,排除推理层的问题。

3. 模型选型与资源准备

3.1 ASR 模型怎么挑

sherpa-onnx 官方提供了一批预训练模型,命名规则一般是语言-类型-数据集-参数量。比如sherpa-onnx-streaming-zipformer-bilingual-zh-en就是流式、Zipformer 结构、中英双语的模型。

选模型的时候重点看这几个参数:

  • 参数量:参数量越大精度越高,但推理越慢、体积越大。中端手机建议选 20M 以下的模型,旗舰机可以上到 50M 左右。
  • 是否流式:前面说过,看场景。
  • 语言覆盖:单语模型通常比多语模型精度高,如果你的场景只用中文,别选双语模型。
  • 量化方式:int8 量化能把体积压到 fp32 的四分之一左右,精度损失通常在可接受范围内。

我实测下来,一个 30M 左右的 int8 量化流式模型,在中端骁龙 7 系芯片上,单帧推理大概在 20-40ms,基本能满足实时字幕的需求。如果换成 fp32,推理时间会翻倍,中端机就有点吃力了。

3.2 TTS 模型的组成

TTS 比 ASR 复杂一点,它通常由两部分组成:声学模型和声码器。声学模型负责把文本转成声学特征(比如梅尔频谱),声码器负责把特征转成波形。

sherpa-onnx 的 TTS 模型包里一般会包含这两个文件,外加一个tokens.txt和lexicon.txt。选型的时候要注意:

  • 声学模型和声码器要配套,不能混用。
  • 中文模型需要 lexicon 或者前端文本处理,英文模型通常用字符级 token。
  • 有些模型支持多说话人,文件里会有 speaker 相关的配置。

3.3 模型文件的组织方式

把模型放进 Android 项目,有两种常见做法:

  1. 放 assets 目录:打包进 APK,首次启动时拷贝到内部存储。优点是分发简单,缺点是 APK 体积大。
  2. 首次启动下载:APK 里只放一个空壳,模型从服务器下载。优点是 APK 小,缺点是需要网络,且要处理下载失败的情况。

我一般推荐第一种,因为端侧语音的核心卖点就是"离线可用",如果首次启动还要联网下载模型,体验就打了折扣。如果模型实在太大,可以考虑把 ASR 和 TTS 拆成两个可选模块,用户按需下载。

模型文件在 assets 里的目录结构建议这样组织:

assets/ models/ asr/ encoder.onnx decoder.onnx joiner.onnx tokens.txt tts/ model.onnx vocoder.onnx tokens.txt lexicon.txt

这样结构清晰,加载的时候按目录读取就行。

4. Android 集成实操步骤

4.1 引入 sherpa-onnx 的 Android 库

sherpa-onnx 提供了预编译的 Android AAR,可以直接通过 Maven 引入。如果你用的是较新的 Gradle,在build.gradle.kts里加:

dependencies { implementation("com.k2fsa.sherpa.onnx:sherpa-onnx:1.x.x") }

版本号以官方发布为准。如果预编译库不满足需求(比如你要裁剪算子减小体积),可以自己从源码编译。自己编译的话需要 Android NDK 和 CMake,流程稍微长一点,但能精确控制保留哪些算子。

注意:自己编译时,ONNX Runtime 的算子裁剪要小心。语音模型里常用的 LSTM、GRU、LayerNorm 这些算子如果被裁掉,模型加载会直接报错。建议先用完整算子集跑通,再逐步裁剪。

4.2 音频采集的正确姿势

Android 上采集音频,AudioRecord是最基础的选择。关键参数是采样率、声道数和位深。sherpa-onnx 的模型通常要求16kHz、单声道、16bit的 PCM 数据,所以采集的时候最好直接按这个格式来,避免后面重采样。

val sampleRate = 16000 val channelConfig = AudioFormat.CHANNEL_IN_MONO val audioFormat = AudioFormat.ENCODING_PCM_16BIT val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) val audioRecord = AudioRecord( MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, audioFormat, bufferSize * 2 )

bufferSize建议取最小值的两倍,避免采集过程中出现 underrun。采集线程里用read()把数据读到一个 short 数组,然后喂给推理接口。

这里有个容易忽略的点:麦克风采集的音频通常有直流偏移和底噪,直接喂给模型会影响识别率。建议在采集后加一个简单的高通滤波,去掉低频噪声。如果不想自己写滤波,可以在模型输入端做归一化,效果也还行。

4.3 模型加载与推理调用

sherpa-onnx 的 Android 接口是 JNI 封装的,调用方式大致是这样:

val config = OnlineRecognizerConfig( featConfig = FeatureConfig(sampleRate = 16000, featureDim = 80), modelConfig = OnlineModelConfig( transducer = OnlineTransducerModelConfig( encoder = "models/asr/encoder.onnx", decoder = "models/asr/decoder.onnx", joiner = "models/asr/joiner.onnx" ), tokens = "models/asr/tokens.txt", numThreads = 2, provider = "cpu" ), decodingMethod = "greedy_search" ) val recognizer = OnlineRecognizer(config) val stream = recognizer.createStream() // 采集循环里 stream.acceptWaveform(samples, sampleRate) while (recognizer.isReady(stream)) { recognizer.decode(stream) } val result = recognizer.getResult(stream)

numThreads建议设成 2,设太多反而会因为线程调度开销导致延迟增加。provider在 Android 上一般用cpu,除非你的设备支持 NNAPI 且模型能完整跑在 NPU 上,否则用 CPU 更稳。

4.4 TTS 的调用流程

TTS 的调用比 ASR 简单,输入文本,输出音频:

val ttsConfig = OfflineTtsConfig( model = OfflineTtsModelConfig( vits = OfflineTtsVitsModelConfig( model = "models/tts/model.onnx", lexicon = "models/tts/lexicon.txt", tokens = "models/tts/tokens.txt" ), numThreads = 2 ) ) val tts = OfflineTts(ttsConfig) val audio = tts.generate("你好,这是一段测试文本", speed = 1.0f)

generate返回的是 float 数组,采样率通常是 22050 或 16000,具体看模型配置。播放的时候用AudioTrack就行,注意采样率要和模型输出一致,否则声音会变调。

4.5 线程模型与内存管理

端侧推理一定要放在独立线程里,不能阻塞主线程。我的做法是开一个HandlerThread专门做推理,采集线程把音频数据通过队列传给它,推理结果再通过回调发回主线程更新 UI。

内存管理方面,ONNX Runtime 的 session 创建比较耗时,不要每次推理都新建。正确的做法是应用启动时创建一次,全局复用。stream 对象可以每次会话新建,用完记得释放,否则会内存泄漏。

提示:sherpa-onnx 的 JNI 对象需要手动释放,Kotlin 里可以用use {}或者try-finally确保释放。我见过不少项目因为忘记释放 stream,跑久了就 OOM。

5. 参数调优与性能优化

5.1 影响识别率的关键参数

ASR 的识别率不只取决于模型,特征提取的参数也很关键。sherpa-onnx 的FeatureConfig里有两个参数要特别注意:

  • sampleRate:必须和模型训练时一致,通常是 16000。如果采集是 44100,要先重采样。
  • featureDim:通常是 80,表示梅尔滤波器的数量。这个值要和模型匹配,改错了模型直接报错。

解码方法上,greedy_search最快但精度一般,modified_beam_search精度更高但慢一些。实时场景用 greedy,离线转写可以用 beam search。

5.2 延迟优化的几个手段

延迟是端侧语音最敏感的指标。我总结下来,优化手段主要有这几个:

  1. 减小 chunk 大小:流式模型的 chunk 越小,出结果越快,但精度会略降。一般 320ms 的 chunk 是延迟和精度的平衡点。
  2. 降低采样率:如果模型支持 8kHz,用 8kHz 采集能减少一半的数据量,推理更快。但 8kHz 音质差,识别率也会降。
  3. int8 量化:量化后推理速度通常能提升 30%-50%,精度损失在 1%-3% 左右。
  4. 多线程:numThreads设成 2 通常最优,设成 4 在小核上反而更慢。

5.3 包体积控制

APK 体积是很多团队关心的指标。sherpa-onnx 加上模型,很容易让 APK 增加几十兆。控制体积的办法:

  • 模型量化:int8 量化能把模型体积压到四分之一。
  • 算子裁剪:自己编译 ONNX Runtime,只保留用到的算子。
  • ABI 过滤:只保留arm64-v8a,去掉armeabi-v7a和x86,能省不少体积。现在主流手机都是 arm64,兼容性影响不大。
  • 模型按需下载:如果实在太大,把 TTS 模型做成可选下载。

我做过一个对比,一个包含流式 ASR 和 VITS TTS 的 APK,不做任何优化大概 80MB,做了 int8 量化和 ABI 过滤后能压到 35MB 左右。

5.4 功耗与发热

端侧推理是耗电大户,长时间跑语音识别,手机发热明显。优化思路:

  • 按需推理:不要一直开着麦克风,用 VAD(语音活动检测)判断有没有人说话,没人说话就暂停推理。
  • 降低推理频率:流式模型不需要每帧都推理,可以隔几帧跑一次。
  • 动态调整线程数:检测到设备温度高时,降低numThreads。

VAD 这块 sherpa-onnx 也提供了模型,可以配合使用。实测下来,加上 VAD 之后,待机功耗能降一半以上。

6. 常见问题与排查实录

6.1 模型加载失败

这是最常见的问题,报错信息通常是Failed to load model或者Invalid model file。排查顺序:

  1. 检查模型文件是否完整拷贝到了内部存储,assets 里的文件不能直接用路径访问,要先拷贝出来。
  2. 检查模型路径是否正确,Android 的文件路径和桌面端不一样。
  3. 检查模型是否和推理库版本匹配,不同版本的 sherpa-onnx 对模型格式要求可能不同。

我踩过的坑:assets 里的模型文件如果超过 1MB,用AssetManager.open()读取时可能被压缩,导致 ONNX Runtime 解析失败。解决办法是在build.gradle里加androidResources { noCompress += listOf("onnx") },禁止压缩 onnx 文件。

6.2 识别结果乱码或为空

如果模型加载成功但识别结果是乱码,通常是 tokens 文件不匹配。tokens 文件定义了模型输出的索引到字符的映射,用错了模型就输出乱码。

如果结果为空,检查音频数据是否正常。可以把采集的音频存成 wav 文件,用桌面端工具播放确认。常见原因是采样率不对或者音频全是静音。

6.3 推理速度慢

推理慢的原因可能有很多,按这个顺序排查:

现象可能原因解决办法
首次推理慢模型首次加载、JIT 编译预热一次,忽略首次结果
持续慢线程数设置不当调整为 2
特定设备慢CPU 架构差异检查是否走了 arm64
越来越慢内存泄漏检查 stream 是否释放

6.4 TTS 声音异常

TTS 输出声音异常,常见的有几种:

  • 变调:采样率不匹配,检查播放时的采样率和模型输出是否一致。
  • 断断续续:AudioTrack 缓冲区太小,增大 buffer size。
  • 杂音:声码器输出有截断,检查音频数据是否做了归一化。

6.5 常见问题速查表

问题排查方向快速验证
模型加载失败文件完整性、路径、版本用桌面端加载同一模型
识别乱码tokens 文件检查 tokens 和模型是否配套
识别为空音频数据存 wav 文件播放确认
推理慢线程数、量化、ABI对比不同配置的耗时
声音异常采样率、缓冲区检查 AudioTrack 配置
内存泄漏stream 释放用 Profiler 观察内存曲线

7. 一些实操心得

做端侧语音这段时间,有几个体会比较深。

第一,不要一上来就追求大模型。我一开始选了个 100M 的模型,精度确实好,但中端机跑起来卡顿明显,用户体验反而差。后来换成 30M 的量化模型,精度降了一点点,但流畅度上来了,整体体验更好。端侧场景下,"够用且流畅"比"极致精度"更重要。

第二,音频采集的质量决定上限。模型再好,如果采集的音频有噪声、有削波,识别率也上不去。花点时间在采集端做滤波和增益控制,收益比换模型大。

第三,测试要覆盖真实场景。实验室里安静环境跑得好,不代表在地铁、商场里也能用。我建议在开发阶段就录一批真实场景的音频做测试集,别等到上线才发现问题。

第四,VAD 是省电的关键。没有 VAD 的语音应用,麦克风一直开着,电量掉得飞快。加上 VAD 之后,只有检测到人声才启动推理,功耗能降一大截。

第五,模型更新要有机制。端侧模型打包在 APK 里,更新起来麻烦。建议设计一个模型版本管理机制,允许通过下载更新模型文件,这样后续优化模型不用重新发版。

最后分享一个小技巧:调试 ASR 的时候,可以把采集的音频和识别结果同时打日志,格式化成音频时间戳 -> 识别文本,这样能直观看到哪段音频识别错了,方便定位是采集问题还是模型问题。这个日志在排查问题时特别有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询