目录

端侧唤醒词检测实战:给语音助手装上离线守门员

为什么语音助手需要一个守门员

智能音箱、车载助手、会议耳机——只要涉及语音交互,第一个问题都是:怎么知道用户开始说话了?

最笨的方案是全时跑 ASR。但上一篇里那个 24MB 的 int8 识别模型,全时转写依然会吃掉可观的 CPU 和电量。更聪明的做法是三级接力:

  1. 唤醒词(KWS):常驻运行,只监听几个关键词,功耗极低
  2. VAD(语音活动检测):唤醒后检测人声边界,截断有效音频
  3. ASR + TTS:完整识别与回复,唤醒后才启动

唤醒词就是这条流水线的第一道门。本文用 sherpa-onnx 的开源词表 KWS 完整跑一遍,10 分钟给应用装上离线唤醒能力,和之前写的 ASR、TTS 文章正好组成闭环。

开源词表 KWS 是什么

传统唤醒词方案(如固定训练的 Hey Siri)要针对每个词准备大量音频重新训练。sherpa-onnx 的 open vocabulary KWS 换了个思路:模型本质是一个微型 ASR(Zipformer,仅 3.3M 参数),解码时只允许输出你指定的关键词——换唤醒词不用重新训练,改一行文本即可。

每个关键词带两个旋钮(官方文档定义):

  • boosting score(如 :2.0):beam search 中给包含该词的路径加分,越大越容易触发
  • trigger threshold(如 #0.6):解码序列的最低声学概率,取值 0-1,越小越容易触发
方案 特点 适用场景
sherpa-onnx KWS (k2-fsa) 开源词表,免训练换词,Python/C++/iOS/Android 全 API 生产级端侧语音助手,本文主角
openWakeWord (dscripka) PyTorch 训练自己的专属唤醒词 需要完全自定义、可解释的唤醒词
Porcupine(Picovoice) 精度高、多语言,闭源 SDK 商用快速集成,注意授权费用

三分钟跑通中文唤醒

安装并下载官方中文模型(wenetspeech 训练,3.3M 参数,int8 版本仅数 MB):

1
2
3
4
pip install sherpa-onnx
wget https://github.com/k2-fsa/sherpa-onnx/releases/download/kws-models/sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01.tar.bz2
tar xjf sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01.tar.bz2
cd sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01

编写关键词文件(: 后是 boosting score,# 后是触发阈值,注意两者与数值之间不能有空格):

1
2
3
4
5
6
7
8
cat > keywords.txt << 'EOF'
小爱同学 :2.0 #0.6 @小爱同学
你好问问 :3.5 @你好问问
EOF

# 中文模型用 cjkchar 分词,把关键词转成模型 token
sherpa-onnx-cli text2token --text=keywords.txt --output=keywords.bpe \
  --tokens=tokens.txt --tokens-type=cjkchar

用官方测试音频验证(模型目录自带 test_wavs):

1
2
3
4
5
6
7
sherpa-onnx-keyword-spotter \
  --tokens=tokens.txt \
  --encoder=encoder-epoch-99-avg-1.int8.onnx \
  --decoder=decoder-epoch-99-avg-1.int8.onnx \
  --joiner=joiner-epoch-99-avg-1.int8.onnx \
  --keywords-file=keywords.bpe \
  test_wavs/0.wav

Python 集成:流式监听

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import sherpa_onnx

cfg = sherpa_onnx.KeywordSpotterConfig(
    model=sherpa_onnx.OnlineModelConfig(
        tokens="tokens.txt",
        encoder="encoder-epoch-99-avg-1.int8.onnx",
        decoder="decoder-epoch-99-avg-1.int8.onnx",
        joiner="joiner-epoch-99-avg-1.int8.onnx",
    ),
    keywords_file="keywords.bpe",
    keywords_score=1.0,      # 全局 boosting,可被单关键词覆盖
    keywords_threshold=0.25, # 全局触发阈值
)
spotter = sherpa_onnx.KeywordSpotter(cfg)
stream = spotter.create_stream()

for chunk in read_audio_chunks("mic.wav", 16000):  # 必须是 16k 单声道
    stream.accept_waveform(16000, chunk)
    while spotter.is_ready(stream):
        result = spotter.decode(stream)
        if result.keyword:
            print(f"唤醒词命中: {result.keyword}")
            # 在这里启动 ASR,进入正式对话
            break

调参实战:误唤醒与漏唤醒

  • 触发阈值:#0.25 起步。误唤醒多就往上调,漏唤醒多就往下调,用带噪音的回归音频反复测
  • boosting score:唤醒词和日常对话里相似发音冲突时(如「小爱同学」vs「小爱通知」),适当降低 score 减少抢词
  • 先跑官方 demo 再换词:模型自带 keywords.txt 和测试音频,先用默认词验证环境,再逐步换成自己的词

端到端闭环

唤醒命中后,接 VAD 截断人声,送进 sherpa-onnx ASR 识别,再用 Piper TTS 回复,一条完全离线的语音链路就通了。三个模型加起来不到 50MB,普通手机 CPU 即可实时运行,全程音频不出设备。

踩坑记录

  1. token 类型选错:中文模型必须用 cjkchar,英文模型用 bpe,混用会解码出乱码
  2. 音频格式:输入必须是 16kHz 单声道,麦克风采集时记得先重采样
  3. 关键词文件格式:: 和 # 与数值之间不能有空格,否则解析失败;中文关键词建议加 @ 原文,便于日志排查