如何让 Android 识别 “曼波” 关键词|自定义唤醒词

0 阅读7分钟

在做语音交互产品时,我们经常需要实现 "曼波,播放音乐"、"曼波,打开灯光" 这类多轮语音控制。

很多人第一反应是用通用离线语音识别,但这其实是杀鸡用牛刀 —— 通用模型动辄几十 MB,占用资源多、功耗高,对于固定几个唤醒词 / 命令词,识别速度和准确率反而不如专门的 WakeWord 方案。

目前行业主流的 WakeWord 方案包括 WeKWS、OpenWakeWord、NanoWakeWord 等,都是针对端侧优化的轻量级模型。最近我基于 Mamba 架构实现了一套新一代轻量 WakeWord 方案,做到了100KB 级体积、完全离线、低功耗常驻监听,并解决了开源方案普遍存在的误唤醒问题。我把模型调用逻辑和独家 5 层防误触发机制全部开源,本文详细记录 Android 平台的完整集成过程。

screencap.png


为什么不用通用语音识别?

先说说通用语音识别做关键词检测的几个痛点:

  1. 体积太大:最小的通用离线模型也要几十 MB,对于嵌入式设备和资源受限的移动端来说负担很重
  2. 功耗太高:常驻后台运行会显著增加耗电,用户根本无法接受
  3. 延迟高:通用模型需要先把整句话说完再识别,响应速度慢
  4. 准确率低:对于固定的几个关键词,通用模型的识别率反而不如专门训练的小模型
  5. 定制麻烦:想要添加或修改关键词非常困难,很多商业方案还要按设备收费

而单关键词模型正好解决了这些问题:

  • 体积只有 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:能量跳变检测

解决问题:视频播放、背景音乐等持续噪声

原理

  1. 检测语音开始前 0.5-2 秒的背景能量
  2. 只有当当前能量是背景能量的 3 倍以上时,才认为是人类说话
  3. 语音结束后,能量应该回到背景水平
  4. 如果能量一直保持在高位,说明是持续的背景噪声

这 5 层逻辑层层递进,基本上过滤掉了 99% 的误触发情况。而且所有参数都是自适应的,不需要用户根据不同的设备和环境手动调整。


实际测试结果

我在小米 13 手机上做了详细的测试,结果如下:

  • 模型总大小:128KB(包含所有通用模型和关键词模型)
  • 模型加载时间:<10ms
  • 单帧推理时间:<5ms
  • 后台常驻 CPU 占用:<1%
  • 唤醒响应时间:<500ms
  • 安静环境误唤醒率:约 1 次 / 24 小时

这个表现已经达到了工业级产品的水平,完全可以用于实际的产品开发。


如何获取自定义关键词模型?

这个开源库只提供了模型调用和后处理逻辑,如果你需要自己的自定义关键词模型,有两种方式:

  1. 自己训练:可使用 WeKWS、OpenWakeWord、NanoWakeWord、FunASR 等主流开源框架,我个人用的是 NanoWakeWord,但训练 WakeWord 模型需要大量数据和专业调参经验,非语音方向开发者不建议自行折腾。

  2. 在线生成:如果你不想自己折腾训练环境和数据,也可以使用在线工具:听词 - 轻量离线语音唤醒词生成

    只需要输入你想要的关键词,系统会自动合成语音数据、训练模型,15 分钟即可导出 ONNX 格式的模型文件。训练好的模型可以直接在这个开源库中使用,支持 Android、Linux、Web、ESP32 等多个平台,没有设备数量限制,可以免费商用。


总结

轻量级单关键词识别是一个非常实用的技术,特别适合智能家居、智能硬件、车载设备等场景。它体积小、功耗低、响应快,而且误唤醒率可以控制得非常好。

我开源的这个onnx-wakeword库,解决了最麻烦的模型调用和防误触发问题,你只需要专注于自己的业务逻辑即可。

如果这篇文章对你有帮助,欢迎给我的开源仓库点个 Star。有任何问题或者建议,也可以在评论区留言讨论。