首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >给 AI 装上耳朵:国内环境下的离线语音识别与唤醒踩坑指南

给 AI 装上耳朵:国内环境下的离线语音识别与唤醒踩坑指南

原创
作者头像
用户12864818
发布于 2026-11-02 09:09:23
发布于 2026-11-02 09:09:23
1860
举报

一个本地 AI 中控要做到「随叫随到」,得先解决两件事:听清(ASR)和听见(唤醒)。 这两件事在国内网络环境下各有一串坑,我把踩过的都记在这了。


一、先把两件事拆开:很多人一开始就把它们混在一起

做语音时最常见的第一个错误,是把「语音识别」和「语音唤醒」当成一件事。

语音识别(ASR)

语音唤醒(Wake Word)

干什么

把波形转成文字

判断「刚才那声是不是在叫我」

输出

一整句文本

(keyword, score) 一个事件

谁来打分

语言模型(解码整段声学特征)

小分类器(跑在冻结的特征提取器上)

延迟要求

秒级可接受

毫秒级,必须常驻

能不能云端

可以(但违数据主权)

绝对不行,要 7×24 在线

本项目对应

Vosk / FunASR / faster-whisper

openWakeWord / Porcupine / Vosk 关键词

这个区分为什么重要?因为它决定了两条完全不同的工程路径。

ASR 是「按需调用」的——用户按住说话才启动,装 1.3GB 还是 42MB 只影响加载时间。

唤醒是「常驻」的——它 7×24 开着,每 80 毫秒算一次分,CPU 占用和内存都是硬约束。

我在这一篇里要讲的三个大坑,恰好一头踩在 ASR 上,一头踩在唤醒上,还有一个踩在两边交汇的地方。


二、坑一:1.3GB 的大模型下载了也不会被用(最阴的一个)

2.1 症状

用户反馈:「语音识别老是识别错,换个大模型是不是就好了?」

我当时的第一反应是——那肯定是他还没下大模型。vosk 官方提供了中文大模型 vosk-model-cn-0.22(1.3GB),WER(字错率)官方数据是:

模型

体积

WER · SpeechIO-02

WER · THCHS

vosk-model-small-cn-0.22

42 MB

23.54

17.15

vosk-model-cn-0.22

1.3 GB

13.98

7.43

错误率近乎腰斩,值得换。

于是我写了个下载脚本,做了断点续传、大小校验、ZIP 完整性校验、磁盘预检、退避重试——脚本本身没问题。

然后我去看了一眼加载路径,发现问题根本不在模型上。

2.2 根因:列表顺序 + 「首个命中即返回」

lingjing_voice.py 原本的模型候选列表是按体积从小到大排的,而查找函数 find_model_path()(L99)的逻辑是:

代码语言:python
复制
# 遍历候选列表,返回【第一个】命中的目录

两件事凑在一起,就是一个必然结果:

只要 42MB 小模型还躺在 models/ 目录里,那 1.3GB 大模型永远不会被加载。 哪怕你亲手把它下载好、放在同一个目录下。

这不是「模型不好」,是「模型永远轮不到上班」。用户以为换了模型,其实在换寂寞。

2.3 修法:一行顺序 + 一个逃生舱

改完的候选列表(lingjing_voice.py L61-63):

代码语言:python
复制
_FORCE_MODEL = os.environ.get("LINGJING_VOSK_MODEL", "").strip()

# 【关键】顺序即优先级:大模型优先
_MODEL_CANDIDATES = (
    os.path.join(MODEL_DIR, "vosk-model-cn-0.22"),         # 约 1.3GB,准确率最高
    os.path.join(MODEL_DIR, "vosk-model-small-cn-0.22"),   # 约 42MB,体积小、速度快
    os.path.join(MODEL_DIR, "vosk-model-cn"),              # 其它版本
)

加上 LINGJING_VOSK_MODEL 环境变量作为逃生舱——低配机吃不住 1.3GB(官方提示最高可能需要约 16GB 内存),可以一键指回小模型:

代码语言:bat
复制
set LINGJING_VOSK_MODEL=D:\lingjing_test\models\vosk-model-small-cn-0.22

改完立刻跑全量回归:556 项断言全通过 / 0 失败。

2.4 一条通用教训

任何「遍历候选 + 取第一个命中」的加载逻辑,都是隐形的优先级陷阱。 你以为你装了什么不重要——顺序才决定谁生效。

我后来把这条写进了项目的工程记忆。它在模型加载、插件加载、配置文件读取这几个场景里反复出现。


三、坑二:中文唤醒词,Colab 在国内连不上

3.1 起点:openWakeWord 是好引擎,但没有中文模型

我做唤醒的时候选的是 openWakeWord(Apache-2.0),理由很实在:

  • 纯本地 ONNX 推理,麦克风音频不出本机——这是数据主权铁律的底线;
  • 极轻量:实测均值 2.06 ms/帧(P95 2.02ms),80ms 帧长意味着 CPU 占用约 2.6%,可以 7×24 常驻;
  • 它不跑 while True 裸奔,自带三重有界(墙钟上限 + 步数硬上限 + 冷却去抖)。

安检时 check_safety.py 报了 BLOCK=2 / WARN=7 → REJECT。逐条人工复核后,两条 BLOCK 都是条件不可达:

位置

命中

复核结论

B1

openwakeword/model.py:187

pickle.load(...)

仅当调用方显式传入非空 custom_verifier_models 字典且键匹配才执行;默认参数是 {}。集成时永不传该参数

B2

openwakeword/train.py:642

yaml.load(..., yaml.Loader)

运行时不可达——__init__.py 只导入 model / vad / custom_verifier_model,不导入 train.py

WARN 7 条性质正常(1 处模型下载是显式调用才触发,无静默联网)。

但真正的问题在功能层面:openWakeWord 官方预训练唤醒词只有

代码语言:markdown
复制
alexa / hey_jarvis / hey_mycroft / hey_rhasspy / timer / weather

全是英文。「灵境」没有现成模型,必须自训练。而官方指定的训练环境是 Google Colab。

3.2 撞墙:Colab 国内直连失败

colab.research.google.com 连不上。原训练方案直接作废。

这不是「再等等」的问题——是路线要重选。我不可能让辉哥为了训一个唤醒词去搞科学上网。

3.3 破解:把 GPU 训练搬到国内平台,把模型下载走镜像

我重写了一整套《国内平替平台训练指南》,核心是三个替换:

替换一:训练平台 → 国内免费 GPU

原方案

平替方案

说明

Google Colab

百度 AI Studio(V100/T4,免费 8h/日)

账号直接登录,界面与 Colab 几乎一致

—

阿里魔搭 ModelScope(A10 免费额度)

备选

替换二:HuggingFace → 国内镜像(这条是卡死重灾区)

训练要拉特征文件和噪声数据集,直连 HF 会卡到怀疑人生。解法是改环境变量:

代码语言:python
复制
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"

替换三:GitHub 拉仓库 → REST API 通道

这一条我要单独强调,因为它是当晚最阴的一个坑。

我 git clone openWakeWord 时成功了——但那纯属侥幸。08:00 之后 github.com 彻底 502 / CONNECT 000,而 api.github.com 和 raw.githubusercontent.com 依然可达。

在代理环境下,直连 github.com 是不稳定的。正确姿势是走 REST API:GET /repos/{repo}/releases/tags/v0.5.1 → 取资源清单 GET /releases/assets/{id} → 带 Accept: application/octet-stream全程只走 GitHub 官方域名,不引入任何第三方镜像。

下载脚本还得做:60 秒超时、重试 3 次带指数退避 + 抖动、按官方 size 逐字节校验、打印 SHA256。

3.4 训练配置长什么样

给「灵境」写死的配置(已按国内网络与中文适配):

代码语言:yaml
复制
target_phrase: ["灵境"]
custom_negative_phrases: ["你好", "小艺小艺", "小度小度", "天猫精灵",
                          "Hey jarvis", "灵镜", "零境"]
n_samples: 1000
n_samples_val: 500
steps: 10000          # 免费 T4/A10 约 10~30 分钟;想更准加到 30000~50000
model_type: fc
layer_size: 32
target_false_positives_per_hour: 0.2

负样本里特意塞了「灵镜」「零境」——近似音必须进负样本,否则模型分不清。

正样本则是 300 条中文 TTS 合成音频(一键生成唤醒音频.bat 一键生成)。

3.5 训完之后怎么用:零代码改动

这是 openWakeWord 最漂亮的地方——模型和代码解耦:

代码语言:bash
复制
# 把灵境.onnx 丢进模型目录
"$PY" voice_wakeup.py --model 灵境.onnx --threshold 0.4 --verbose

模块会自动用文件名(去 .onnx)作为唤醒词键名,命中时回调 (key="灵境", score=...)。

换唤醒词这件事,从「改代码」降级成了「换文件」。 这才是好的架构。


四、坑三:口音唤醒不了,降阈值救不了

4.1 症状:阈值降到 0.05 能响,但像被风吹就醒

辉哥实测中文发音说「Hey Jarvis」,唤醒分峰值只有 0.0776——远低于官方推荐的 0.5 阈值。

降阈值能触发。但降阈值带来一个更糟的问题:

在非安静环境下,阈值 0.05 下 5 秒误触发 6 次,峰值冲到 0.1901——一声空调 / 电流瞬态就把我叫醒了。

0.05 是「能响」但不是「能用」的阈值。纯降阈值是权宜之计,不是解法。

4.2 我做了一组实验,想搞清楚到底差在哪

我写了 _record_and_analyze.py,做本地 DSP 增强(带通滤波 / 预加重 / AGC / 组合),然后在仓库自带的真人语音上做自检:

变体

hey_jarvis 分

alexa 分

原始

0.0000

1.0000

带通 / 预加重 / AGC / 组合

0.0000

0.99+

这张表是整篇文章里最重要的一张表。

它的意思是:无论怎么增强,"alexa" 的音频都不可能变成 "hey_jarvis" 的分数。

同时另一组数据:组合增强把我环境里的噪声尖峰从 0.2342 压到 0.0008。

两件事合起来,结论非常干净:

DSP 增强能压误报,但抬不动唤醒分。 因为问题根本不是「音质差 / 音量小 / 有噪声」, 而是音素 mismatch——中文口音的 "Hey Jarvis" 和英文训练集里的声学特征根本不对齐。

4.3 根因确认后的分诊

方案

做法

判定

A-纯降阈值

--threshold 0.05

能触发,但误报高,仅权宜

A-增强+低阈值

组合增强 + 阈值 0.1~0.15

推荐过渡:增强压误报,分略升

B-中文自训练

训「灵境 / 贾维斯」专模

根因解

D-换引擎

Porcupine 免训练中文

当天就能用(见下节)

4.4 顺便补一个工程细节:音量

历史录音 RMS 只有约 523/32767,偏低。音量不足本身也会显著拖累识别率——所以「模型换大之后还是不准」的时候,第一件事应该是检查输入电平,而不是继续折腾模型。


五、后来的解法:唤醒引擎三选一,不要只认 openWakeWord

既然 B 方案(自训练)需要平台和半小时,我顺手把「今天就能用」的路也铺好了。最终形成三套接口完全同构的唤醒引擎:

引擎

需训练

联网需求

中文「灵境」

推理位置

openWakeWord

✅ 需训 灵境.onnx

训练需 GPU 平台

✅ 自训专模

本地 ONNX

Porcupine

❌ 免训练

仅一次性下 .ppn

✅ 官方中文模型

本地 100%

Vosk 关键词

❌ 免训练

❌ 断网也行

✅ 本机已有模型

本地 100%

5.1 Porcupine:工业级引擎,5 分钟上手

Porcupine 是 Picovoice 的商业级唤醒引擎,自带中文模型 porcupine_params_zh.pv。

流程:

  1. console.picovoice.ai 注册拿 AccessKey(免费);
  2. Console → Wake Word → Create Wake Word,Language 选 Chinese (zh),输入「灵境」,下载 灵境.ppn;
  3. 下 porcupine_params_zh.pv;
  4. 本地推理:
代码语言:python
复制
handle = pvporcupine.create(
    access_key,
    keyword_paths=["灵境.ppn"],
    model_path="porcupine_params_zh.pv",
)
# 每帧 int16 音频喂进去
if handle.process(int16_frame) >= 0:
    print("唤醒!")

推理 100% 在本机跑,不联网、不上传。唯一需要网络的就是「去控制台生成 .ppn」这一步,一次性。

5.2 Vosk 关键词:最硬的兜底

既然 ASR 已经用 Vosk 了,那唤醒为什么不能也用?答案是完全可以:

代码语言:markdown
复制
麦克风实时采音 → Vosk 识别中文 → 文本里出现"灵境"即触发

不训练、不联网、不注册、不下载任何东西——本机已有中文模型。

代价是:Vosk 是通用识别,可能把「灵镜 / 零境」等近似音也触发,CPU 占用比专用引擎高。作保底 OK,日常主力仍建议 Porcupine 或自训模型。

5.3 统一接口契约

三个脚本对外接口完全同构:

代码语言:markdown
复制
on_detect(key, score)  回调
start() / stop() / run()  方法
后台守护线程采音 · 冷却去抖 · PCM 不落盘不上传

未来可以互替换,或者加一个 --engine 统一开关,互不影响主代码。这就是好设计的味道——加一条新路线不用改旧路线。


六、顺带说说「嘴」:TTS 的两个独立问题

做语音时我顺手把输出端(播报)也修了,因为它的两个坑和识别端遥相呼应。

6.1 黑框根因:兜底路径才是元凶

现网 lingjing_tts.py 的播放链路:

位置

函数

起子进程?

说明

L93–123

_mci_play()

❌ 无

ctypes 直调 winmm.dll,注释写着「全程不起任何子进程,物理上不可能弹出窗口」——设计正确,首选

L126–150

_ps_play()

✅ 有

subprocess.run(["powershell", ...])

L153–161

_play_mp3()

条件

先试 MCI,失败则兜底到 PowerShell

所以黑框的真正成因是:_mci_play() 在某些环境返回 False → 自动兜底到 PowerShell → 闪黑框。

而断网发不出声是另一个独立问题:_speak_blocking() 用 edge_tts.Communicate 合成,必须联网。

一句话:黑框来自播放兜底的子进程,断网哑来自云端合成。这是两个独立问题,必须分别治。

6.2 解法:Windows 原生 SAPI5(0 MB 模型,完全离线)

代码语言:python
复制
import win32com.client
speaker = win32com.client.Dispatch("SAPI.SpVoice")
# 中文 LCID: 804 简体 / 404 繁体 / c04 香港 / 1004 新加坡 / 1404 澳门
for v in speaker.GetVoices():
    if (v.GetAttribute("Language") or "").lower() in ("804", "404", "c04", "1004", "1404"):
        speaker.Voice = v
        break
speaker.Speak(text, 1)   # 1 = SVSFlagsAsync,调用立即返回,不阻塞 UI

实测数据:

文本长度

合成耗时

音频时长

实时率 RTF

WAV 体积

RMS

6 字

246 ms

2.32 s

0.106

99.9 KB

1000.4

22 字

265 ms

5.91 s

0.045

254.5 KB

1723.9

66 字

252 ms

17.28 s

0.015

744.1 KB

2095.7

几个关键点:

  • 模型体积 0 MB(用系统自带语音,不下任何模型);
  • 异步调用 187ms 就返回,后台继续播,不阻塞 UI;
  • 全程零子进程 → 物理上不可能弹黑框;
  • 项目主解释器已装 pywin32,零新增 pip 依赖;
  • 本机原生自带中文语音 Microsoft Huihui Desktop - Chinese (Simplified)(LCID 804,Female);
  • 合成耗时几乎恒定在 ~250ms,与文本长度无关——TTS 绝不是交互瓶颈。

6.3 两个明确劝退的方案

方案

劝退理由

ChatTTS

依赖 torch>=2.1.0 / torchaudio / transformers(数百 MB~GB 级),许可证 AGPL-3.0(最强传染性),打包闭源分发有合规风险

Piper

原版 rhasspy/piper(MIT)已归档;后继 OHF-Voice/piper1-gpl 是 GPL-3.0,且本环境 HuggingFace 不可达无法实测

「能跑」和「能打包分发」是两件事。AGPL/GPL 在分发场景是硬伤,不能因为「跑起来了」就放行。


七、这一篇的所有真机数字(可复现)

用例

实测

判定

环境自检(openwakeword 0.6.0 / onnxruntime 1.31.1 / 模型齐备)

—

PASS

正样本 alexa_test.wav(仓库自带真人语音)

峰值 1.0000

PASS

正样本 hey_mycroft_test.wav

峰值 1.0000

PASS

负样本 · 3 秒静音

峰值 0.0000

PASS(零误报)

负样本 · 3 秒白噪声

峰值 0.0314

PASS

麦克风实时 5 秒(Cirrus Logic 麦克风)

62 帧、均值 RMS 1512、峰值 0.0038、触发 0 次

采音通路正常、底噪零误报

推理性能

均值 2.06 ms/帧、P95 2.02 ms → CPU 占用约 2.6%

可 7×24 常驻

辉哥中文口音峰值

0.0776(阈值 0.05 下 5 秒误触发 6 次,峰值 0.1901)

音素 mismatch,坐实

DSP 增强自检

hey_jarvis 分恒为 0.0000;噪声尖峰 0.2342 → 0.0008

增强压噪不抬分

ASR 模型优先级修复后回归

test_night.py 556 项通过 / 0 失败

未跌破基线

SAPI5 合成(6/22/66 字)

246 / 265 / 252 ms,RMS 1000~2095

真实有声内容


八、还没解决的(诚实边界)

  1. 真人唤醒率没有实测数字。所有正样本峰值都是仓库自带真人语音(alexa / hey_mycroft)跑出来的。辉哥用自己的中文口音喊「灵境」的实际触发率与误报率,我没有数据——AI 没法替人说话。这一项必须本人跑 --mic 15,喊 5~10 遍回报次数。
  2. FunASR / faster-whisper 的中文实际准确率未实测。官方称 FunASR 是中文 SOTA,但具体 WER 待本机跑过才算数。Tier 2/Tier 3 的模型体积、CPU 耗时也未验证——我只真实验证了「回归 556 绿 + 下载器可用 + 小模型链路连通」。
  3. 本机没有 ffmpeg,沙箱环境装不上。所以 mp3 → wav 的转换没跑通,需要时在辉哥本机执行 pip install imageio-ffmpeg(自带静态 ffmpeg)。
  4. Porcupine 的 .ppn 需要你亲自去 console.picovoice.ai 生成——这是唯一涉及外部账号的步骤,我不能替你注册。
  5. SAPI5 音质是我的盲区。沙箱听不到声音,我只能给你 RMS / 时长 / RTF 这些客观数字,好不好听必须你自己听。样本已生成:_sandbox/tts_local_test.wav(2.32s,22050Hz)。

九、一句话总结

给 AI 装耳朵,难点从来不是「能不能识别」,而是「能不能常驻、不误报、不联网」。 三个坑分别对应三层:加载顺序坑 → 让你辛苦下的模型永远不上场(解:优先级显式化 + 逃生舱环境变量) 网络生态坑 → 让你依赖的官方训练环境在国内用不了(解:国内平台平替 + API 通道 + 镜像) 音素 mismatch 坑 → 让你用降阈值这种会自欺欺人的手段硬凑(解:认清根因,换引擎或自训专模) 最后那条最重要:当一个方案需要你不断调低阈值来换取"能触发",它多半已经错了。 阈值是最后一根稻草,不是解决方案。


附:本文涉及的真实文件

文件

作用

voice_wakeup.py

openWakeWord 唤醒模块(独立,不 import 任何 lingjing_*)

porcupine_wakeup.py

Porcupine 中文唤醒(免训练首选)

vosk_wakeup.py

Vosk 离线关键词触发(断网兜底)

lingjing_voice.py

ASR 采集 + 识别(已修为大模型优先)

lingjing_asr_hq.py

三引擎统一入口(vosk / funasr / whisper)

lingjing_tts.py

播报链路(SAPI5 本地优先,零子进程)

一键测试_Porcupine唤醒.bat · 一键测试_Vosk唤醒.bat

双击即测

一键生成唤醒音频.bat · 一键打包训练资料.bat

造 300 条中文正样本

一键升级高精度语音模型.bat

下载 1.3GB 大模型(断点续传 + 校验)

本文所有性能数字均为本机真机实测,WER 数据引自 Vosk 官方模型页原文,非估算。


首发于腾讯云开发者社区。WER 与耗时数据均来自本机真机评测。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先把两件事拆开:很多人一开始就把它们混在一起
  • 二、坑一:1.3GB 的大模型下载了也不会被用(最阴的一个)
    • 2.1 症状
    • 2.2 根因:列表顺序 + 「首个命中即返回」
    • 2.3 修法:一行顺序 + 一个逃生舱
    • 2.4 一条通用教训
  • 三、坑二:中文唤醒词,Colab 在国内连不上
    • 3.1 起点:openWakeWord 是好引擎,但没有中文模型
    • 3.2 撞墙:Colab 国内直连失败
    • 3.3 破解:把 GPU 训练搬到国内平台,把模型下载走镜像
    • 3.4 训练配置长什么样
    • 3.5 训完之后怎么用:零代码改动
  • 四、坑三:口音唤醒不了,降阈值救不了
    • 4.1 症状:阈值降到 0.05 能响,但像被风吹就醒
    • 4.2 我做了一组实验,想搞清楚到底差在哪
    • 4.3 根因确认后的分诊
    • 4.4 顺便补一个工程细节:音量
  • 五、后来的解法:唤醒引擎三选一,不要只认 openWakeWord
    • 5.1 Porcupine:工业级引擎,5 分钟上手
    • 5.2 Vosk 关键词:最硬的兜底
    • 5.3 统一接口契约
  • 六、顺带说说「嘴」:TTS 的两个独立问题
    • 6.1 黑框根因:兜底路径才是元凶
    • 6.2 解法:Windows 原生 SAPI5(0 MB 模型,完全离线)
    • 6.3 两个明确劝退的方案
  • 七、这一篇的所有真机数字(可复现)
  • 八、还没解决的(诚实边界)
  • 九、一句话总结
  • 附:本文涉及的真实文件
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档