在做语音交互产品时,我们经常需要实现 "曼波,播放音乐"、"曼波,打开灯光" 这类多轮语音控制。
很多人第一反应是用通用离线语音识别,但这其实是杀鸡用牛刀 —— 通用模型动辄几十 MB,占用资源多、功耗高,对于固定几个唤醒词 / 命令词,识别速度和准确率反而不如专门的 WakeWord 方案。
目前行业主流的 WakeWord 方案包括 WeKWS、OpenWakeWord、NanoWakeWord 等,都是针对端侧优化的轻量级模型。最近我基于 Mamba 架构实现了一套新一代轻量 WakeWord 方案,做到了100KB 级体积、完全离线、低功耗常驻监听,并解决了开源方案普遍存在的误唤醒问题。我把模型调用逻辑和独家 5 层防误触发机制全部开源,本文详细记录 Android 平台的完整集成过程。
为什么不用通用语音识别?
先说说通用语音识别做关键词检测的几个痛点:
- 体积太大:最小的通用离线模型也要几十 MB,对于嵌入式设备和资源受限的移动端来说负担很重
- 功耗太高:常驻后台运行会显著增加耗电,用户根本无法接受
- 延迟高:通用模型需要先把整句话说完再识别,响应速度慢
- 准确率低:对于固定的几个关键词,通用模型的识别率反而不如专门训练的小模型
- 定制麻烦:想要添加或修改关键词非常困难,很多商业方案还要按设备收费
而单关键词模型正好解决了这些问题:
- 体积只有 100-200KB
- 单帧推理时间 < 5ms,CPU 占用 < 1%
- 响应延迟 < 500ms
- 可以随时更换关键词
- 完全离线,数据不出设备
开源项目介绍
我把整个模型调用和防误触发逻辑整理成了一个开源库:onnx-wakeword
这个库的特点:
- 基于 ONNX Runtime,跨平台支持 Android、Linux、Web、ESP32
- 支持同时加载多个关键词模型
- 内置 5 层防误触发机制
- 提供统一的 API 接口
- 自己训练模型放上去,没有任何商业限制
注意:这个库只包含模型调用和后处理逻辑,不包含模型训练部分。如果你不想自己训练模型,可以在文章末尾找到在线生成工具。
Android 平台集成步骤
1. 添加依赖
首先在你的build.gradle中添加 ONNX Runtime 依赖:
gradle
dependencies {
implementation 'com.microsoft.onnxruntime:onnxruntime-android:1.26.0'
}
2. 准备模型文件
将你的模型文件放到assets目录下,需要包含以下文件:
melspectrogram.onnx:通用的梅尔频谱提取模型your_wake_word.onnx:你的自定义关键词模型model_info.json:模型配置文件
model_info.json的格式如下:
json
{
"model_type": "dscnn",
"wake_word": "曼波",
"model_file": "manbo.onnx",
"emb_frames": 16,
"cons_frames": 2
}
3. 初始化唤醒词引擎
在你的 Activity 或 Service 中初始化WakeWordEngine:
java
运行
public class MainActivity extends AppCompatActivity {
private WakeWordEngine wakeWordEngine;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
wakeWordEngine = new WakeWordEngine(this);
if (!wakeWordEngine.isLoaded()) {
Log.e("WakeWord", "模型加载失败: " + wakeWordEngine.getErrorMessage());
return;
}
Log.i("WakeWord", "加载成功,支持的唤醒词: " + wakeWordEngine.getWakeWordDisplay());
startAudioRecording();
}
}
4. 音频采集
使用 Android 的AudioRecord采集 16kHz 单声道 16bit PCM 音频:
java
运行
private static final int SAMPLE_RATE = 16000;
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
private static final int BUFFER_SIZE = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT);
private AudioRecord audioRecord;
private boolean isRecording = false;
private void startAudioRecording() {
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, BUFFER_SIZE);
isRecording = true;
audioRecord.startRecording();
new Thread(() -> {
short[] buffer = new short[wakeWordEngine.getAudioSamplesNeeded()];
while (isRecording) {
int read = audioRecord.read(buffer, 0, buffer.length);
if (read > 0) {
WakeWordEngine.DetectionResult result = wakeWordEngine.process(buffer);
if (result != null && result.wakeWord != null) {
runOnUiThread(() -> {
Toast.makeText(MainActivity.this,
"检测到唤醒词: " + result.wakeWord,
Toast.LENGTH_SHORT).show();
});
}
}
}
}).start();
}
5. 释放资源
在 Activity 销毁时记得释放资源:
java
运行
@Override
protected void onDestroy() {
super.onDestroy();
isRecording = false;
if (audioRecord != null) {
audioRecord.stop();
audioRecord.release();
}
if (wakeWordEngine != null) {
wakeWordEngine.close();
}
}
核心:5 层防误触发机制详解
这是整个项目最有价值的部分,也是我花了最多时间打磨的地方。我在推理结果后加了 5 层过滤逻辑,把安静环境下的误唤醒率降到了约 1 次 / 24 小时。
java
运行
/**
* Wake word detection pipeline (5 layers).
*
* L1: N consecutive frames above threshold → filters transient noise
* L2: peak ≫ background level (3×) → filters model fluctuation
* L3: 1.5s cooldown → prevents double-trigger
* L4: burst 3×/3s → suppress 5s → blocks playback loops
* L5: energy jump ratio → blocks video/music
*/
class DetectionLogic {
// 实现代码见开源仓库
}
L1:连续帧检测
解决问题:键盘敲击、椅子响、关门声等瞬态短噪声
原理:只有连续 2-3 帧的推理结果都超过阈值,才认为是有效的唤醒词。人类说话的持续时间至少有几百毫秒,而瞬态噪声通常只有几十毫秒。
L2:峰值 / 背景抑制
解决问题:安静环境下模型对底噪的幻觉输出
原理:计算过去 1.5 秒内的最大概率值和背景平均概率值,只有当峰值大于背景值的 3 倍时,才认为是有效的检测结果。
L3:冷却机制
解决问题:同一句话被重复识别多次
原理:每次成功触发后,进入 1.5 秒的冷却期,冷却期内不再响应任何唤醒词。
L4:爆发封锁
解决问题:音频回路、扬声器回声导致的密集误触发
原理:如果 3 秒内连续检测到 3 次相同的唤醒词,就进入 5 秒的封锁期。这种情况通常是设备自己的扬声器发出的声音又被麦克风录进去了。
L5:能量跳变检测
解决问题:视频播放、背景音乐等持续噪声
原理:
- 检测语音开始前 0.5-2 秒的背景能量
- 只有当当前能量是背景能量的 3 倍以上时,才认为是人类说话
- 语音结束后,能量应该回到背景水平
- 如果能量一直保持在高位,说明是持续的背景噪声
这 5 层逻辑层层递进,基本上过滤掉了 99% 的误触发情况。而且所有参数都是自适应的,不需要用户根据不同的设备和环境手动调整。
实际测试结果
我在小米 13 手机上做了详细的测试,结果如下:
- 模型总大小:128KB(包含所有通用模型和关键词模型)
- 模型加载时间:<10ms
- 单帧推理时间:<5ms
- 后台常驻 CPU 占用:<1%
- 唤醒响应时间:<500ms
- 安静环境误唤醒率:约 1 次 / 24 小时
这个表现已经达到了工业级产品的水平,完全可以用于实际的产品开发。
如何获取自定义关键词模型?
这个开源库只提供了模型调用和后处理逻辑,如果你需要自己的自定义关键词模型,有两种方式:
-
自己训练:可使用 WeKWS、OpenWakeWord、NanoWakeWord、FunASR 等主流开源框架,我个人用的是 NanoWakeWord,但训练 WakeWord 模型需要大量数据和专业调参经验,非语音方向开发者不建议自行折腾。
-
在线生成:如果你不想自己折腾训练环境和数据,也可以使用在线工具:听词 - 轻量离线语音唤醒词生成
只需要输入你想要的关键词,系统会自动合成语音数据、训练模型,15 分钟即可导出 ONNX 格式的模型文件。训练好的模型可以直接在这个开源库中使用,支持 Android、Linux、Web、ESP32 等多个平台,没有设备数量限制,可以免费商用。
总结
轻量级单关键词识别是一个非常实用的技术,特别适合智能家居、智能硬件、车载设备等场景。它体积小、功耗低、响应快,而且误唤醒率可以控制得非常好。
我开源的这个onnx-wakeword库,解决了最麻烦的模型调用和防误触发问题,你只需要专注于自己的业务逻辑即可。
如果这篇文章对你有帮助,欢迎给我的开源仓库点个 Star。有任何问题或者建议,也可以在评论区留言讨论。