一、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 scoretrigger thresholdboosting 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 onlight off,可以用 2025-12-20 的中英文模型。官方文档列出了 sherpa-onnx-kws-zipformer-zh-en-3M-2025-12-20,支持中文和英文,并提供 chunk-8chunk-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 / 开启检测 / 停止检测

这样既能降低误触发,又方便后续接入你的视频检测、坐姿分类、灯控和其他自动化功能。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐