sherpa-kws 关键词语音识别原理、环境配置、代码实现与应用
一、sherpa-kws 是什么
sherpa-kws 通常指的是 sherpa-onnx 里的 Keyword Spotting,关键词检测 / 语音唤醒功能。它和普通 ASR 语音识别不完全一样。普通 ASR 会把一句话完整转成文字,而 KWS 只关心你有没有说出指定关键词,例如“你好山河”“帮我开灯”“帮我关灯”。
sherpa-onnx 官方把这种方式称为 open vocabulary keyword spotting,也就是开放词表关键词检测。它的特点是:可以在不重新训练模型的情况下,自定义关键词;系统只会在给定关键词范围内解码,如果没检测到目标关键词,就输出空结果。官方文档举例说明,如果关键词是 HELLO WORLD,输出应该只有 HELLO WORLD 或空结果。
对于你的项目来说,sherpa-kws 非常适合做:
“你好山河”作为唤醒词;
“帮我开灯 / 开灯”触发 light on;
“帮我关灯 / 关灯”触发 light off;
识别过程全部本地运行,不需要联网调用 API。
二、核心原理
1. 它本质上是一个小型流式 ASR
sherpa-kws 并不是传统的“录几百段关键词模板然后 DTW 匹配”。它更接近一个小型的流式语音识别模型,只不过解码时被限制在关键词列表里。
整体流程可以理解为:
麦克风音频
↓
16kHz 单通道 float32 音频流
↓
特征提取,例如 80 维声学特征
↓
Zipformer / Transducer 模型
↓
encoder 编码声学信息
↓
decoder + joiner 预测 token
↓
beam search 只搜索关键词路径
↓
输出关键词或空结果
官方 KWS 模型使用的是 transducer 结构,模型文件一般包含:
encoder-xxx.onnx
decoder-xxx.onnx
joiner-xxx.onnx
tokens.txt
keywords.txt
其中:
encoder:提取音频上下文特征。decoder:根据历史 token 建模。joiner:把 encoder 和 decoder 的输出结合起来,得到当前 token 概率。tokens.txt:模型能识别的 token 表。keywords.txt:你的关键词列表,已经被转换成模型 token 格式。
2. 关键词并不是直接写中文,而是先转成 token
例如你想检测:
帮我开灯
帮我关灯
开灯
关灯
不能简单把这几行中文直接丢给模型用。需要先写一个原始关键词文件:
帮我开灯 @帮我开灯
帮我关灯 @帮我关灯
开灯 @开灯
关灯 @关灯
然后用 sherpa-onnx-cli text2token 转换成 token 格式。官方文档说明,安装 sherpa-onnx 后可以使用 sherpa-onnx-cli text2token 命令,并且中文 KWS 常用 ppinyin 形式,例如“你好问问”会被转成类似 n ǐ h ǎo w èn w èn 的 token 序列。
转换后的 keywords.txt 可能类似:
b āng w ǒ k āi d ēng @帮我开灯
b āng w ǒ g uān d ēng @帮我关灯
k āi d ēng @开灯
g uān d ēng @关灯
实际输出以工具转换结果为准,不建议自己手写拼音 token。
3. beam search、boosting score 和 threshold
sherpa-kws 用 beam search 做关键词解码。官方文档说明,为了在“触发率”和“误唤醒率”之间平衡,它引入了两个重要参数:boosting score 和 trigger threshold。boosting score 越大,关键词路径越容易在 beam search 中保留下来;trigger threshold 是触发阈值,越低越容易触发,越高越不容易触发。
所以调参原则是:
误触发太多:
提高 keywords_threshold
降低 keywords_score
经常漏检:
降低 keywords_threshold
提高 keywords_score
关键词很相似,例如“开灯”和“关灯”:
适当提高 num_trailing_blanks
或者把关键词设计得更长,比如“帮我开灯”“帮我关灯”
建议初始参数:
keywords_score = 1.0
keywords_threshold = 0.35
num_trailing_blanks = 2
max_active_paths = 4
如果你的现场噪声比较大,误触发明显,可以把阈值改成:
keywords_threshold = 0.45 或 0.55
三、环境配置
下面以 Linux / Windows 都能用的 Python 方案为主。
1. 创建 Python 环境
Linux:
conda create -n sherpa_kws python=3.10 -y
conda activate sherpa_kws
Windows:
conda create -n sherpa_kws python=3.10 -y
conda activate sherpa_kws
2. 安装 Python 依赖
pip install -U pip
pip install sherpa-onnx sounddevice numpy
如果 Linux 上安装 sounddevice 报错,可以补充安装音频依赖:
sudo apt update
sudo apt install -y portaudio19-dev libasound2-dev
pip install sounddevice
sounddevice 用于读取麦克风音频。官方 Python 麦克风示例中也是用 sounddevice 读取实时音频流。
Windows 如果中文输出显示乱码,先在命令行执行:
CHCP 65001
官方文档也提示 Windows 遇到编码问题时可以执行 CHCP 65001。
四、下载 KWS 模型
方案 1:推荐中文场景使用纯中文模型
适合你的“你好山河 / 开灯 / 关灯”项目。
mkdir -p ~/sherpa_kws_demo
cd ~/sherpa_kws_demo
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 xf sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01.tar.bz2
rm sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01.tar.bz2
官方文档说明,这个 wenetspeech-3.3M-2024-01-01 模型是中文 KWS 模型,目录里包含 encoder、decoder、joiner、tokens.txt、keywords.txt 等文件。
方案 2:中英文混合模型
如果后面你还要识别英文命令,例如 light on、light off,可以用 2025-12-20 的中英文模型。官方文档列出了 sherpa-onnx-kws-zipformer-zh-en-3M-2025-12-20,支持中文和英文,并提供 chunk-8 与 chunk-16 模型;其中 chunk-8 延迟约 160ms,chunk-16 延迟约 320ms,低延迟通常会牺牲一点准确率。
下载命令:
mkdir -p ~/sherpa_kws_demo
cd ~/sherpa_kws_demo
wget https://github.com/k2-fsa/sherpa-onnx/releases/download/kws-models/sherpa-onnx-kws-zipformer-zh-en-3M-2025-12-20.tar.bz2
tar xf sherpa-onnx-kws-zipformer-zh-en-3M-2025-12-20.tar.bz2
rm sherpa-onnx-kws-zipformer-zh-en-3M-2025-12-20.tar.bz2
五、制作自己的关键词文件
这里以纯中文模型为例。
进入工作目录:
cd ~/sherpa_kws_demo
新建 keywords_raw.txt:
cat > keywords_raw.txt << EOF
帮我开灯 @帮我开灯
帮我关灯 @帮我关灯
开灯 @开灯
关灯 @关灯
EOF
转换成模型需要的 keywords.txt:
sherpa-onnx-cli text2token \
--tokens sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/tokens.txt \
--tokens-type ppinyin \
keywords_raw.txt keywords.txt
官方中文模型自定义关键词时,也是使用 sherpa-onnx-cli text2token --tokens-type ppinyin 将中文关键词转成 token 文件。
查看生成结果:
cat keywords.txt
六、命令行测试
先用模型自带测试音频跑一下:
cd ~/sherpa_kws_demo
sherpa-onnx-keyword-spotter \
--encoder=sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/encoder-epoch-12-avg-2-chunk-16-left-64.onnx \
--decoder=sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/decoder-epoch-12-avg-2-chunk-16-left-64.onnx \
--joiner=sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/joiner-epoch-12-avg-2-chunk-16-left-64.onnx \
--tokens=sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/tokens.txt \
--keywords-file=keywords.txt \
--keywords-score=1.0 \
--keywords-threshold=0.35 \
sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/test_wavs/3.wav
官方也提供了类似的 sherpa-onnx-keyword-spotter 命令行测试方式,并说明 wave 输入要求是单通道、16-bit PCM,采样率不一定必须是 16kHz。
七、Python 实时麦克风识别代码
保存为:
kws_microphone.py
完整代码如下:
import sys
import time
from pathlib import Path
import numpy as np
import sounddevice as sd
import sherpa_onnx
# =========================
# 1. 路径配置
# =========================
MODEL_DIR = Path("sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01")
TOKENS = MODEL_DIR / "tokens.txt"
ENCODER = MODEL_DIR / "encoder-epoch-12-avg-2-chunk-16-left-64.onnx"
DECODER = MODEL_DIR / "decoder-epoch-12-avg-2-chunk-16-left-64.onnx"
JOINER = MODEL_DIR / "joiner-epoch-12-avg-2-chunk-16-left-64.onnx"
KEYWORDS_FILE = Path("keywords.txt")
# =========================
# 2. KWS 参数
# =========================
SAMPLE_RATE = 16000
# 每次读取 100ms 音频
SAMPLES_PER_READ = int(0.1 * SAMPLE_RATE)
# CPU 线程数,Linux 上建议显式设置,避免 onnxruntime 线程亲和性警告
NUM_THREADS = 2
# cpu / cuda
PROVIDER = "cpu"
# 保留多少条搜索路径
MAX_ACTIVE_PATHS = 4
# 关键词增强分数:越大越容易触发
KEYWORDS_SCORE = 1.0
# 触发阈值:越大越严格,误触发越少,但可能漏检
KEYWORDS_THRESHOLD = 0.35
# 关键词后面需要跟随多少 blank,关键词相似时可以适当加大
NUM_TRAILING_BLANKS = 2
# =========================
# 3. 状态机配置
# =========================
WAKE_WORD = "你好山河"
COMMAND_ON = {"帮我开灯", "开灯"}
COMMAND_OFF = {"帮我关灯", "关灯"}
STATE_SLEEP = "sleep"
STATE_AWAKE = "awake"
# 唤醒后最多等待多少秒,超过就回到睡眠状态
AWAKE_TIMEOUT = 6.0
def check_files():
files = [TOKENS, ENCODER, DECODER, JOINER, KEYWORDS_FILE]
for f in files:
if not f.is_file():
raise FileNotFoundError(f"文件不存在: {f}")
def create_keyword_spotter():
kws = sherpa_onnx.KeywordSpotter(
tokens=str(TOKENS),
encoder=str(ENCODER),
decoder=str(DECODER),
joiner=str(JOINER),
num_threads=NUM_THREADS,
max_active_paths=MAX_ACTIVE_PATHS,
keywords_file=str(KEYWORDS_FILE),
keywords_score=KEYWORDS_SCORE,
keywords_threshold=KEYWORDS_THRESHOLD,
num_trailing_blanks=NUM_TRAILING_BLANKS,
provider=PROVIDER,
)
return kws
def normalize_result(result: str) -> str:
"""
sherpa-kws 的返回有时可能是 JSON 字符串,有时直接包含 keyword。
这里做一个简单兼容:
- 如果结果里包含 @ 后面的原始关键词,就优先取出来;
- 否则按字符串包含关系判断。
"""
if not result:
return ""
# 常见输出中会包含中文关键词
candidates = [WAKE_WORD, "帮我开灯", "帮我关灯", "开灯", "关灯"]
for c in candidates:
if c in result:
return c
return result.strip()
def handle_keyword(keyword: str, state: str, last_wake_time: float):
"""
返回:
new_state, new_last_wake_time, action
action:
None / light_on / light_off / wake
"""
now = time.time()
# 睡眠状态:只接受唤醒词
if state == STATE_SLEEP:
if keyword == WAKE_WORD:
return STATE_AWAKE, now, "wake"
else:
# 没唤醒时,开灯/关灯不响应
return state, last_wake_time, None
# 唤醒状态超时
if state == STATE_AWAKE and now - last_wake_time > AWAKE_TIMEOUT:
return STATE_SLEEP, last_wake_time, None
# 唤醒后执行命令
if state == STATE_AWAKE:
if keyword in COMMAND_ON:
return STATE_SLEEP, last_wake_time, "light_on"
if keyword in COMMAND_OFF:
return STATE_SLEEP, last_wake_time, "light_off"
# 唤醒状态下再次听到唤醒词,刷新等待时间
if keyword == WAKE_WORD:
return STATE_AWAKE, now, "wake"
return state, last_wake_time, None
def main():
check_files()
print("========== sherpa-kws 实时关键词检测 ==========")
print(f"模型目录: {MODEL_DIR}")
print(f"关键词文件: {KEYWORDS_FILE}")
print(f"采样率: {SAMPLE_RATE}")
print(f"阈值 keywords_threshold: {KEYWORDS_THRESHOLD}")
print(f"增强 keywords_score: {KEYWORDS_SCORE}")
print("按 Ctrl+C 退出")
print()
devices = sd.query_devices()
if len(devices) == 0:
print("没有检测到麦克风设备")
sys.exit(1)
print("当前音频设备:")
print(devices)
print()
kws = create_keyword_spotter()
stream = kws.create_stream()
state = STATE_SLEEP
last_wake_time = 0.0
print("当前状态: sleep,等待唤醒词:你好山河")
with sd.InputStream(
channels=1,
dtype="float32",
samplerate=SAMPLE_RATE,
) as mic:
while True:
samples, _ = mic.read(SAMPLES_PER_READ)
samples = samples.reshape(-1).astype(np.float32)
stream.accept_waveform(SAMPLE_RATE, samples)
while kws.is_ready(stream):
kws.decode_stream(stream)
result = kws.get_result(stream)
if result:
keyword = normalize_result(result)
print(f"[检测结果] raw={result}, keyword={keyword}")
state, last_wake_time, action = handle_keyword(
keyword=keyword,
state=state,
last_wake_time=last_wake_time,
)
if action == "wake":
print(">>> 已唤醒,请说:帮我开灯 / 帮我关灯")
elif action == "light_on":
print(">>> 执行动作: light on")
# 这里可以接你的业务逻辑
# 比如调用串口、GPIO、HTTP接口等
# send_command("light on")
elif action == "light_off":
print(">>> 执行动作: light off")
# send_command("light off")
else:
print(">>> 未唤醒或无效命令,不执行动作")
print(f"当前状态: {state}")
# 检测到关键词后必须 reset,否则可能重复触发
kws.reset_stream(stream)
# 唤醒超时自动回到 sleep
if state == STATE_AWAKE and time.time() - last_wake_time > AWAKE_TIMEOUT:
state = STATE_SLEEP
print(">>> 唤醒超时,回到 sleep 状态")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n退出程序")
官方麦克风示例也是每次读取约 100ms 音频,调用 accept_waveform() 输入音频,再循环 is_ready() / decode_stream() 解码,最后通过 get_result() 获取结果;检测到关键词后需要 reset_stream(),否则可能重复触发。
八、运行方式
目录结构建议如下:
~/sherpa_kws_demo/
├── kws_microphone.py
├── keywords_raw.txt
├── keywords.txt
└── sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/
├── encoder-epoch-12-avg-2-chunk-16-left-64.onnx
├── decoder-epoch-12-avg-2-chunk-16-left-64.onnx
├── joiner-epoch-12-avg-2-chunk-16-left-64.onnx
├── tokens.txt
└── ...
运行:
cd ~/sherpa_kws_demo
conda activate sherpa_kws
python kws_microphone.py
说话流程:
用户:你好小玲
程序:已唤醒,请说:帮我开灯 / 帮我关灯
用户:帮我开灯
程序:执行动作 light on,然后回到 sleep
用户:帮我关灯
程序:如果没有重新说“你好山河”,不会执行
用户:你好小玲
程序:再次唤醒
用户:帮我关灯
程序:执行动作 light off,然后回到 sleep
这样可以避免“后面说什么话都触发”的问题。
九、常见问题和调参建议
1. 误触发太多
修改:
KEYWORDS_THRESHOLD = 0.45
或者:
KEYWORDS_SCORE = 0.8
如果还是误触发,把关键词改长。例如不要只用“开灯”,而是用“帮我开灯”。短词越容易误触发。
2. 经常检测不到
修改:
KEYWORDS_THRESHOLD = 0.25
KEYWORDS_SCORE = 1.2
同时保证麦克风采样率是 16000,环境噪声不要太大。
3. “开灯”和“关灯”混淆
这是因为两个词都包含“灯”,并且“开 / 关”都是短音节。建议:
NUM_TRAILING_BLANKS = 3
KEYWORDS_THRESHOLD = 0.4
更稳的做法是只保留长命令:
帮我开灯
帮我关灯
少用单独的:
开灯
关灯
如果一定要模糊检测,可以保留,但阈值要更严格。
4. 中文乱码
Windows 命令行先执行:
CHCP 65001
Python 文件保存为 UTF-8。
5. CPU 性能不够
可以优先使用 int8 模型。官方中文模型目录里同时提供了 fp32 和 int8 的 encoder / decoder / joiner 文件,int8 更适合 CPU 或边缘设备部署。
把代码路径改成:
ENCODER = MODEL_DIR / "encoder-epoch-12-avg-2-chunk-16-left-64.int8.onnx"
DECODER = MODEL_DIR / "decoder-epoch-12-avg-2-chunk-16-left-64.int8.onnx"
JOINER = MODEL_DIR / "joiner-epoch-12-avg-2-chunk-16-left-64.int8.onnx"
十、应用场景
1. 智能灯控
你的项目可以这样设计:
sleep 状态:
只监听“你好小玲”
awake 状态:
监听“帮我开灯 / 帮我关灯”
执行动作后:
立即回到 sleep
这样能避免误操作,也符合智能音箱的常见逻辑。
2. 坐姿检测系统联动
你现在有视频检测 / 分类模型,可以把语音控制加进去:
你好山河 + 帮我开灯:
开启检测
light on
开始 YOLO det + cls 推理
你好山河 + 帮我关灯:
light off
停止检测或暂停检测
也可以设置:
light off 状态:
不运行检测模型,节省算力
light on 状态:
才运行坐姿检测 / warning 检测
3. 边缘设备离线唤醒
sherpa-kws 可以部署在:
Linux 工控机
Windows 电脑
树莓派
Jetson
Android 设备
它不需要把音频上传到云端,适合隐私要求高、网络不稳定、低延迟的场景。
4. 工业控制
例如:
你好小玲,开始检测
你好小玲,停止检测
你好小玲,保存结果
你好小玲,退出程序
这些都可以通过关键词触发 Python 里的函数。
十一、总结
sherpa-kws 的核心思想是:用一个小型流式 ASR 模型做关键词受限解码。它比传统模板匹配更鲁棒,也比完整 ASR 更轻量。它支持自定义关键词,不需要你为每个关键词重新训练模型。对于“你好山河 + 开灯 / 关灯”这类项目,推荐使用中文 wenetspeech-3.3M KWS 模型,配合状态机控制逻辑,实现稳定的语音唤醒和命令执行。
最推荐的项目方案是:
中文 KWS 模型
+
keywords.txt 自定义关键词
+
实时麦克风输入
+
状态机:
sleep -> wake -> command -> sleep
+
业务动作:
light on / light off / 开启检测 / 停止检测
这样既能降低误触发,又方便后续接入你的视频检测、坐姿分类、灯控和其他自动化功能。
更多推荐



所有评论(0)