ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Android端侧语音交互实战:sherpa-onnx集成ASR与TTS全指南

Android端侧语音交互实战:sherpa-onnx集成ASR与TTS全指南 1. 端侧语音交互的现状与 sherpa-onnx 的切入点把语音识别和语音合成塞进手机这件事放在三五年前还属于“实验室里跑得动、真机上跑不动”的范畴。我最早接触端侧语音是在一个离线字幕工具的项目里当时用某款云端 ASR 接口网络一抖字幕就断用户骂声一片。后来转向端侧方案试过 TensorFlow Lite 的语音模型、也试过 PyTorch Mobile 的量化版本踩了不少坑直到遇到 sherpa-onnx才算找到一个在 Android 上真正能落地的方案。sherpa-onnx 是 k2-fsa 团队开源的一套端侧语音推理框架底层基于 onnxruntime支持流式和非流式 ASR、TTS、VAD、说话人分离等任务。它的核心价值在于模型体积小、推理速度快、跨平台一致性好而且提供了完整的 Android 预编译库和 Kotlin/Java API。换句话说你不需要自己从 PyTorch 导出 ONNX、不需要手写 JNI 胶水层、不需要折腾 NDK 编译直接拿 AAR 包就能在 Android 项目里跑起来。这篇文章面向的是有一定 Android 开发基础、想在 App 里集成离线语音能力的开发者。不管你是想做语音输入法、离线字幕、语音助手还是想做 TTS 朗读功能sherpa-onnx 都能给你一条相对平滑的路径。我会从架构设计、模型选型、实操集成、性能调优、问题排查几个维度把我在实际项目中积累的经验完整地分享出来。2. 为什么选 sherpa-onnx 而不是其他端侧方案2.1 端侧语音推理的三种主流路线对比在决定用 sherpa-onnx 之前我认真对比过市面上几条主流路线这里把关键差异整理成表格方便你根据自己的场景做判断。方案底层引擎模型格式Android 支持流式 ASRTTS 支持集成难度sherpa-onnxonnxruntimeONNX官方 AAR支持支持低TensorFlow LiteTFLiteTFLite官方库需自行实现有限中PyTorch MobilePyTorchTorchScript官方库需自行实现有限中高云端 API服务端不涉及HTTP 调用支持支持低但依赖网络从表格能看出来sherpa-onnx 最大的优势是“开箱即用”——它把流式 ASR 和 TTS 这两块最难啃的骨头都封装好了而且模型格式统一为 ONNX社区里现成的模型也最多。TensorFlow Lite 和 PyTorch Mobile 虽然也是端侧方案但流式推理的逻辑需要你自己写TTS 的支持也不如 sherpa-onnx 成熟。2.2 sherpa-onnx 的架构设计思路sherpa-onnx 的架构可以分成三层来理解。最底层是 onnxruntime负责实际的矩阵运算和算子执行中间层是 sherpa-onnx 自己实现的语音处理逻辑包括特征提取fbank、解码器greedy search 或 beam search、端点检测VAD等最上层是各语言的 API 封装Android 这边提供的是 Kotlin/Java 接口。这种分层设计的好处是模型和推理引擎解耦你可以随时替换 ONNX 模型而不需要改代码同时特征提取和解码逻辑用 C 实现性能比纯 Java 实现高一个数量级。我在实测中对比过同样的模型sherpa-onnx 的 C 解码比用 Java 自己写的贪心解码快了将近 3 倍尤其是在长语音场景下差距更明显。2.3 模型选型的核心考量sherpa-onnx 支持的 ASR 模型主要有几类Paraformer中文效果好、Zipformer中英混合强、Whisper多语言但体积大、Conformer通用。TTS 模型则有 VITS、Matcha、Kokoro 等。选型时我一般看三个指标模型体积、实时率RTF、识别准确率。以中文场景为例Paraformer 的小模型大概 70MB 左右RTF 在骁龙 8 Gen 1 上能跑到 0.15 以下也就是说 10 秒的语音 1.5 秒就能出结果。Zipformer 的中英混合模型大概 120MBRTF 稍高但也在 0.3 以内。Whisper tiny 虽然只有 40MB但中文识别准确率明显不如 Paraformer而且不支持流式。所以如果你的场景是纯中文离线识别Paraformer 是首选如果需要中英混说Zipformer 更合适。注意模型体积和准确率永远是一对矛盾。我见过有开发者为了追求小体积用了 Whisper tiny结果中文识别错字率高达 15%用户体验极差。建议在体积和准确率之间找平衡点一般 70-150MB 是比较合理的区间。3. Android 集成实操从零到跑通第一个 Demo3.1 环境准备与依赖引入先说一下我的开发环境Android Studio Hedgehog 以上、Gradle 8.x、minSdk 24、targetSdk 34、NDK 版本 25.2。sherpa-onnx 的 AAR 包对 NDK 版本有要求太低会报链接错误太高也可能有兼容问题25.x 是比较稳的区间。依赖引入有两种方式。第一种是直接从 Maven Central 拉取dependencies { implementation(com.k2fsa.sherpa.onnx:sherpa-onnx:1.10.0) }第二种是手动下载 AAR 放到 libs 目录适合需要定制编译或者内网环境的情况。我一般推荐第一种版本管理方便升级也简单。引入依赖后需要在build.gradle里加上 abiFilters只保留 arm64-v8a 和 armeabi-v7a这样能显著减小 APK 体积android { defaultConfig { ndk { abiFilters listOf(arm64-v8a, armeabi-v7a) } } }3.2 模型文件的放置与加载策略模型文件不能放在 assets 里直接读因为 onnxruntime 需要文件路径而不是 InputStream。我的做法是在 App 首次启动时把 assets 里的模型拷贝到filesDir下之后直接从文件系统加载。拷贝逻辑要注意用后台线程并且加一个版本标记模型更新时重新拷贝。fun copyModelIfNeeded(context: Context, modelName: String) { val targetDir File(context.filesDir, sherpa-models/$modelName) val versionFile File(targetDir, version.txt) val currentVersion 1.0 if (versionFile.exists() versionFile.readText() currentVersion) return targetDir.mkdirs() context.assets.open(models/$modelName).use { input - File(targetDir, model.onnx).outputStream().use { output - input.copyTo(output) } } versionFile.writeText(currentVersion) }模型加载本身是同步的但耗时可能到几百毫秒建议放在 Application 启动时或者用懒加载。我实测 Paraformer 小模型加载大概 200msZipformer 大概 400msTTS 的 VITS 模型大概 300ms。如果放在主线程会明显卡顿一定要放到子线程。3.3 流式 ASR 的完整调用流程流式 ASR 的核心是“边录边识别”需要把录音线程和识别线程分开。录音线程负责从 AudioRecord 读取 PCM 数据识别线程负责把数据喂给 sherpa-onnx 的 stream 对象。val config OnlineRecognizerConfig( featConfig FeatureConfig(sampleRate 16000, featureDim 80), modelConfig OnlineModelConfig( transducer OnlineTransducerModelConfig( encoder $modelDir/encoder.onnx, decoder $modelDir/decoder.onnx, joiner $modelDir/joiner.onnx ), tokens $modelDir/tokens.txt, numThreads 2, provider cpu ), decodingMethod greedy_search ) val recognizer OnlineRecognizer(config) val stream recognizer.createStream()录音线程每次读到 1600 个采样点100ms就调用stream.acceptWaveform(samples, 16000)然后调用recognizer.isReady(stream)判断是否有可解码的帧有的话就recognizer.decode(stream)再通过recognizer.getResult(stream)拿到当前识别结果。这套流程看起来简单但有几个坑我踩过一是采样率必须是 16000录音时如果设备默认是 44100 需要重采样二是 numThreads 不要设太大2 线程在多数手机上已经够用设 4 反而会因为线程调度开销导致延迟增加。3.4 TTS 合成的参数调优TTS 这边相对简单核心是文本前端和声学模型的配合。sherpa-onnx 的 TTS 支持中英文混合但需要模型本身支持。VITS 中文模型大概 60MB合成一句话10 个字左右耗时在 200-400ms 之间。val ttsConfig OfflineTtsConfig( model OfflineTtsModelConfig( vits OfflineTtsVitsModelConfig( model $ttsModelDir/model.onnx, tokens $ttsModelDir/tokens.txt, lexicon $ttsModelDir/lexicon.txt ), numThreads 2 ), ruleFsts $ttsModelDir/rule.fst ) val tts OfflineTts(ttsConfig) val audio tts.generate(你好欢迎使用语音合成, sid 0, speed 1.0f)speed 参数控制语速1.0 是正常0.8 偏慢1.2 偏快。sid 是说话人 ID多说话人模型才有意义。生成的 audio 是 FloatArray需要转成 16bit PCM 再用 AudioTrack 播放。这里有个细节AudioTrack 的 buffer size 要设够太小会断音我一般设成采样率的 0.5 秒对应的字节数。4. 性能调优与真机适配经验4.1 不同芯片平台的实测数据我在几台不同芯片的手机上跑了同一套 Paraformer 流式模型记录下 RTF 和首字延迟数据如下芯片平台代表机型档位RTF首字延迟内存占用骁龙 8 Gen 2旗舰0.08180ms180MB骁龙 778G中端0.18320ms160MB天玑 8100中高端0.12240ms170MB骁龙 665入门0.45680ms140MB从数据能看出来旗舰机和中端机的差距主要在首字延迟上RTF 差距反而不大。入门机骁龙 665 这个级别的 RTF 到了 0.45意味着 10 秒语音要 4.5 秒才能出结果体验就比较勉强了。所以如果你的 App 要覆盖低端机建议用更小的模型或者干脆只在中高端机上开启流式识别。4.2 内存与线程的平衡策略sherpa-onnx 默认用 2 线程这个设置在中高端机上表现最好。我试过在骁龙 778G 上把 numThreads 调到 4RTF 从 0.18 降到 0.15但首字延迟反而从 320ms 涨到 380ms原因是线程创建和同步的开销增加了。所以我的建议是除非你的场景对吞吐量要求极高比如批量处理录音文件否则 numThreads 保持 2 就行。内存方面模型加载后会常驻内存Paraformer 大概占 150-180MB。如果同时加载 ASR 和 TTS内存占用会到 300MB 左右。在 4GB 内存的低端机上这个占用已经比较危险了可能触发系统杀后台。我的做法是 ASR 和 TTS 按需加载用完就释放虽然重新加载有几百毫秒开销但能显著降低内存压力。4.3 音频采集的坑与解决方案Android 的 AudioRecord 在不同设备上行为不一致这是端侧语音最头疼的问题之一。我遇到过几种典型情况某些机型默认采样率是 48000 而不是 16000需要手动重采样某些机型在蓝牙耳机连接时采样率会变还有的机型在录音时会有明显的底噪。重采样我一般用线性插值虽然音质不如 sinc 插值但计算量小对识别准确率影响可以忽略。底噪问题可以通过 VAD 过滤sherpa-onnx 自带的 VAD 模型silero-vad效果不错能在识别前把静音段切掉既省算力又提高准确率。提示录音权限在 Android 6.0 以上需要动态申请而且从 Android 11 开始如果 App 在后台录音需要额外声明 foregroundServiceType。这些系统限制一定要提前处理否则用户反馈“录不了音”你排查半天才发现是权限问题。5. 常见问题排查与避坑指南5.1 模型加载失败的几种典型原因模型加载失败是最常见的问题我整理了一个排查表报错信息可能原因解决方法Failed to load model文件路径错误检查 filesDir 下文件是否存在Invalid ONNX model模型文件损坏重新从官方仓库下载Unsupported operatoronnxruntime 版本不匹配升级 sherpa-onnx 到最新版Out of memory模型太大换小模型或降低 numThreadsTokens file not foundtokens.txt 缺失确认模型目录包含所有文件其中“Unsupported operator”这个坑我踩过最深。当时用了一个自己导出的 ONNX 模型里面有个算子 onnxruntime 不支持报错信息又很模糊排查了一整天才发现是导出时用了 opset 17 而 onnxruntime 只支持到 15。后来我养成了一个习惯导出模型后先用 onnxruntime 的 Python 版跑一遍确认没问题再放到 Android 上。5.2 识别结果异常的调试思路识别结果异常一般分三种完全没结果、结果乱码、结果延迟高。完全没结果通常是音频格式不对检查采样率、声道数、位深是否匹配。结果乱码多半是 tokens.txt 和模型不匹配或者模型本身是英文的却用来识别中文。结果延迟高则要看 RTF如果 RTF 超过 0.5 就要考虑换模型或降 numThreads。我一般会在调试阶段把每帧的识别结果打日志观察是从第几帧开始出字的。正常情况下流式模型在 300-500ms 内应该出第一个字如果超过 1 秒还没出说明模型或配置有问题。5.3 打包与混淆的注意事项sherpa-onnx 的 AAR 里包含 native 库混淆时要注意保留相关类。我在proguard-rules.pro里加了这几条-keep class com.k2fsa.sherpa.onnx.** { *; } -keep class ai.onnxruntime.** { *; }另外如果用了abiFilters只保留 arm64-v8a在 32 位设备上会直接崩溃。虽然现在 32 位设备越来越少但为了保险起见建议同时保留 armeabi-v7aAPK 体积会增加大概 8MB但兼容性更好。6. 从 Demo 到产品的工程化建议6.1 模型热更新的设计思路模型文件不应该硬编码在 APK 里否则每次换模型都要发版。我的做法是把模型放在 CDN 上App 启动时检查版本号有更新就下载到 filesDir。下载要用断点续传模型文件动辄几十上百 MB用户网络不稳定时很容易断。下载完成后要做 MD5 校验防止文件损坏。版本管理用一个 JSON 文件描述包含模型名称、版本号、下载地址、MD5、大小等字段。App 端对比本地版本号和远端版本号不一致就触发下载。这个机制我在实际项目里跑了半年多更新过三次模型用户侧无感知。6.2 多场景下的资源调度一个 App 里可能同时用到 ASR 和 TTS甚至还有 VAD。如果全部常驻内存低端机扛不住。我的策略是分优先级VAD 常驻模型小才几 MBASR 按需加载用户点录音按钮时加载松开后延迟 30 秒释放TTS 也是按需加载。这样峰值内存能控制在 250MB 以内大多数设备都能接受。释放模型时要注意调用recognizer.release()和tts.release()否则 native 内存不会回收。我见过有开发者只置空引用结果 native 内存泄漏跑几次就 OOM 了。6.3 用户体验层面的细节打磨端侧语音和云端最大的区别是“没有网络等待”所以用户体验的预期也更高。首字延迟超过 500ms 用户就会觉得卡超过 1 秒就会觉得坏了。所以优化首字延迟比优化整体 RTF 更重要。我的做法是录音开始后立刻喂数据不要等缓冲区满解码用 greedy search 而不是 beam search虽然准确率略低但速度快很多VAD 的静音阈值调低一点让用户一开口就能触发识别。另外识别结果的展示也要注意。流式识别是逐字出结果的如果每出一个字就刷新 UI界面会闪得厉害。我一般用 100ms 的节流每 100ms 更新一次显示既流畅又不卡顿。7. 我个人的一些实操体会这套方案我在两个项目里落地过一个是离线会议记录工具一个是语音朗读助手。会议记录那个项目对准确率要求高用的是 Zipformer 中英混合模型120MB在骁龙 870 上 RTF 0.15体验很好。朗读助手对延迟敏感用的是 Paraformer 小模型加 VITS首字延迟控制在 300ms 以内用户反馈“跟云端差不多”。踩过的坑里最深刻的是音频采集的兼容性问题。同一套代码在小米上正常在华为上就没声音排查后发现是华为某些机型默认采样率是 48000而我没做重采样。后来加了一个采样率检测逻辑如果设备默认不是 16000 就强制重采样问题就解决了。所以我的建议是不要假设设备行为一致所有和硬件相关的参数都要做兼容处理。还有一个经验是模型不是越大越好。我一开始追求准确率用了大模型结果低端机上卡得没法用。后来换成小模型准确率只降了 2 个百分点但流畅度提升了一倍。端侧场景下流畅度往往比那一点点准确率更重要毕竟用户能容忍偶尔错字但容忍不了卡顿。最后分享一个小技巧如果你不确定该选哪个模型可以先用 sherpa-onnx 官方提供的预编译 APK 跑一遍里面内置了多个模型可以切换。实测哪个模型在你的目标机型上表现最好再决定集成哪个。这样能省掉大量试错时间。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表