语音 Agent — 本地语音识别、语音合成与 MQTT 命令
语音 Agent — 本地语音识别、语音合成与 MQTT 命令
这一章讲什么: 给机器人装上耳朵和嘴。戴上耳麦说「robot start tracking faces」, 机器人用合成音答「I'm now looking at faces!」,同时云台转头开始追你的脸。 全链:Vosk 语音模型把声音认成文字 → 唤醒词 + 限定词表过滤 → 查命令表 → 发 MQTT 启停行为 → Piper TTS 合成回应。所有 AI 都跑在 Pi 本地,断网也能用。
1. 先看现象:一句「robot」唤醒的机器仆人
本章收束时的体验:耳麦里响起一声没有感情的「Robot ready for commands.」; 你说「robot start following lines」,机器人应声开走,顺便告诉你它在干嘛; 你说「robot teapot banana」,它礼貌地忽略——识别器只认以「robot」开头、 且不含未知词的句子1。
这一幕背后是三个神经网络同时在 Pi 上跑:认脸的 YuNet、认声的 Vosk、 发声的 Piper——书里专门数给你看2。机器人从一个「会动的平台」 正式变成「能被自然语言指挥的 agent」。
2. 顶层全景:五拍循环
① 等 耳麦收音(0.2 秒一块)
② 认 Vosk 把声音帧认成文字(整句 Result / 半途 PartialResult)
③ 滤 以唤醒词("robot")开头 + 不含 [unk] 未知词 → 才算命令
④ 做 命令表查任务 → 发 MQTT(launcher/start、stop)
⑤ 答 Piper 合成一句确认语音
图说:五拍全是本地软件;MQTT 是唯一的对外接口——语音只是又一种「遥控器」。3. 核心原理之一:语音 agent 是什么、为什么本地做
语音代理(voice agent)= 能响应语音请求、执行任务、再用自己的声音回应的程序—— Alexa、Siri 都是3。交互的基本单位是 utterance(话语):你说给它的词或短语, 以及它说回来的那些3。
书里先做了个架构选择:云上识别 vs 本地识别。语音代理可以靠把数据发到互联网上的 大机器去解码,但本章选本地,理由两条:
- 没网也能用;
- 不依赖可能挂掉的外部服务3。
代价明说:性能打折扣(和整套机器人系统挤在同一台 Pi 上,为了省成本)4。 识别的匹配策略分三档:精确匹配、同义词表、小语言模型(SLM)—— 本章用「精确匹配 + 少量同义词」,SLM 明说是范围外3。
4. 核心原理之二:声音硬件——为什么是无线 USB 耳麦
机器人主板不是没音频口,但书把三条路都堵死了:
- 机载麦克风:你得把嘴凑上去,或者盖过电机噪音喊;麦克风一米外基本失效4。
- 机载喇叭:喇叭里的磁铁会干扰 IMU(磁力计)4——第 13 章的账这里还。
- 有线耳机:人被拴在机器人旁边,Wi-Fi 全白搭。
答案:无线 USB 接收器耳麦——接收器插 USB、免驱即认,人戴着走4。
两个 Linux 音频命令先打通通路:pw-play 放、pw-record 录,
音量用 alsamixer 调。术语一对:audio source(音频源)= 输入,
audio sink(音频汇)= 输出5。
5. 核心原理之三:让 Python 说话——Piper TTS
语音合成(text-to-speech, TTS) 的选型是 Piper TTS——基于 AI 模型的
Python 合成器;播放用 sounddevice 库;系统层装 libportaudio2 + pipewire-alsa
给 Python 铺音频路6。
选「嗓子」是本章最好玩的决策:Piper 的音色库里名字分三段——语言、地区、质量
(作者选 en_US-ryan-low)。质量选低档是性能决定:
「低质量模型在 Pi 上跑得很快;中高质量可能慢到不像话。」7
装完 piper.download_voices 后 SD 卡上躺着 en_US-ryan-low.onnx——
和第 14 章的人脸模型同后缀:都是神经网络模型,只是一个认脸、一个发声8。
首次加载模型要等一阵;说完一句一两秒内出音;小模型发音不完美,书里如实说9。
Python 侧薄成三层:VoiceSynthesizer() 加载 → speak("hello there!") 即说。
无线耳机还有个怪癖:静了太久的第一声会发闷——耳机在休眠省电,预热即好9。
6. 核心原理之四:听懂话——Vosk 与限定词表
语音识别和合成是镜像关系:Vosk 拿语言模型初始化识别器,
输入流从麦克风收音频帧,识别器吐出文字10。核心组件两个:
Model(lang="en-us") 加载模型,KaldiRecognizer 做识别
(Vosk 站在 Kaldi 项目肩膀上,接口简化过)11。
循环的机制:
with sd.InputStream(samplerate=44100, channels=1, dtype="int16") as stream:
while True:
audio_data = stream.read(int(0.2 * 44100)) # 每 0.2 秒一块
if recognizer.AcceptWaveform(audio_data[0].tobytes()):
result = json.loads(recognizer.Result()) # 说完了:整句
else:
result = json.loads(recognizer.PartialResult()) # 还在说:半成品AcceptWaveform 返回真 = 一句说完了,拿 Result();否则只有 PartialResult()
——当前说了一半的词,调试时盯着它很有用11。
防错配的三件套(本章的工程精华):
- 唤醒词(wake word)「robot」:只处理以它开头的整句;
- 未知词占位
[unk]:整句里混入未知词就丢弃——「robot teapot banana」 只配当 Partial,永不执行1; - 限定词表(vocabulary):把识别器可能输出的词预先圈定成
命令集 + 唤醒词 + [unk] 喂给
KaldiRecognizer——识别器不用在十万词里猜, 错配率大降。这是「用小词表换准确率」的经典手法,代价是它只能说/听表内的话12。
7. 核心原理之五:命令表——语音就是又一种遥控器
voice_agent.py 的结构一览:
self.utterances = {
"start tracking faces": self.start_tracking_faces,
"stop tracking faces": self.stop_tracking_faces,
"start following lines": self.start_following_lines,
"follow lines": self.start_following_lines, # 同义词
}- 命令 = 字典查询:剥掉唤醒词 →
utterances.get(command)→ 命中就调任务函数, 没命中就合成一句「I didn't understand that task.」13; - 任务 = MQTT 消息:任务函数发
launcher/start、launcher/stop—— 和网页按钮完全同权。语音没有特权,只是多了一个发声渠道, 这正是「一切接 MQTT」架构的红利13; - 加命令 = 加两行:字典加条目 + 任务函数发消息 + 说一声。 模式刻意重复而简单——书里原话:「这模式相当重复,但扩展简单。」14
安全提醒书里用 Tip 框标了,必须带走:
「别指望语音命令停机器人!它永远不会像 Stop 按钮那么快。」15
8. 作者的判断与证据
- 判断: 全本地运行是价值观(离线可用、不依赖外部服务),也是成本控制 (和机器人系统共用一台 Pi,性能让位)34。
- 判断: 识别用「唤醒词 + 词表圈定」而非 SLM——在小词表场景, 确定性(同样的输入必然给同样的输出)匹配 + 同义词比小语言模型更简单可靠;SLM 留作范围外3。
- 事实层: 采样率 44.1 kHz、单声道 int16、0.2 秒分块、ryna-low 模型、 三模型同跑——可复现7112。
- 诚实: 小模型发音不完美;安静久了首声发闷;耳麦麦克风一米外失效; 挑战题承认词表、唤醒词、音色都该可换可扩916。
9. 边界与局限
- 仅限安静环境。 电机噪音、人声混杂都会显著掉识别率; 「相对安静的测试空间」在材料清单里明写。
- 词表即天花板。 圈定词表之外的词一律
[unk];词汇量即能力边界, 扩展要动代码(挑战题给出加词、换唤醒词的路径)16。 - 没有自然语言理解。 「听懂」= 字符串匹配;同义词是硬编码(写死在代码里、改要重新部署)的穷人版 NLU。
- 说话和不能同时。 半双工流程:说完才听、听完才说;打断、连续对话不在本书。
10. 可带走的
- 语音 agent 五拍:收音 → 识别 → 过滤 → 发 MQTT → 合成应答;语音只是又一种遥控器13。
- 机载音频三宗罪:麦克风要凑近、喇叭磁铁扰 IMU、有线拴人——无线 USB 耳麦全解4。
- TTS 选型:小模型优先——低质量 Piper 在 Pi 上才「说得快」;中高质量慢到不可用7。
- 识别三件套:唤醒词 + [unk] 拒收 + 限定词表——用小词表换准确率112。
Result()是整句、PartialResult()是半成品;0.2 秒一块喂识别器11。- 命令 = 字典 + 任务函数;任务函数 = MQTT launcher 消息,和网页按钮同权13。
- 安全:停机器人永远用按钮,不用嘴15。
11. 原文地图
| 主题 | 原书位置 |
|---|---|
| 语音 agent 定义与本地运行 | text/138-fm-technical-requirements.txt:13(搜「respond to voice requests」) |
| utterance 与匹配策略 | text/138-fm-technical-requirements.txt:17(搜「utterance」) |
| 机载音频三宗罪与耳麦 | text/139-fm-adding-sound-input-and-output-to-raspberry-pi.txt:33(搜「interfere with the IMU」) |
| Piper TTS 与选嗓子 | text/140-fm-getting-python-to-speak.txt:36(搜「Piper TTS」) · text/140-fm-getting-python-to-speak.txt:45(搜「Low-quality models」) |
| 语音模型文件 | text/140-fm-getting-python-to-speak.txt:89(搜「en_US-ryan-low.onnx」) |
| Vosk 识别循环 | text/141-fm-speech-recognition-with-python.txt:85(搜「KaldiRecognizer」) · text/141-fm-speech-recognition-with-python.txt:113(搜「PartialResult」) |
| 唤醒词与拒收 | text/142-fm-connect-commands-to-the-robot.txt:18(搜「wake word」) |
| 安全提示 | text/142-fm-connect-commands-to-the-robot.txt:48(搜「Do not rely on voice commands」) |
| 命令表与任务 | text/142-fm-connect-commands-to-the-robot.txt:98(搜「utterances」) |
| 三模型同跑 | text/142-fm-connect-commands-to-the-robot.txt:198(搜「three neural network AI models」) |
| 章节总结 | text/143-fm-summary.txt:32(搜「Piper TTS」) |
Footnotes
-
出处:「Connect commands to the robot」(
text/142-fm-connect-commands-to-the-robot.txt:23,搜「teapot banana」)。原文:整句须以唤醒词 "robot" 开头且不含 [unk];演示 "robot teapot banana" 只产生 partial 匹配并被丢弃;不以 robot 开头的一律忽略。 ↩ ↩2 ↩3 -
出处:「Connect commands to the robot」(
text/142-fm-connect-commands-to-the-robot.txt:198,搜「three neural network AI models」)。原文:现在 Pi 上同时跑着三个神经网络 AI 模型——一个认脸、一个认声、一个(合成语音)发声。 ↩ ↩2 -
出处:「Technical requirements」第 13 段(
text/138-fm-technical-requirements.txt:13,搜「respond to voice requests」)与第 14 段(搜「utterance」)。原文:voice agent 定义;utterance 定义;可上云解码,但本章本地做——无网可用、不依赖可能不可用外部服务;匹配三档:精确/同义词/小语言模型(SLM 超出范围)。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
出处:「Adding sound input and output to Raspberry Pi」第 33 段(
text/139-fm-adding-sound-input-and-output-to-raspberry-pi.txt:33,搜「interfere with the IMU」)。原文:机载麦克风要凑近或大喊、一米外失效;喇叭磁铁干扰 IMU;有线耳机拴人——故选无线 USB 耳麦;在同台 Pi 上跑是成本取舍。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
出处:「Adding sound input and output to Raspberry Pi」第 10 段(
text/140-fm-getting-python-to-speak.txt:10,搜「audio sink」)。原文:audio sink = 输出(喇叭/写文件);alsamixer 调音量;pw-play/pw-record 测试。 ↩ -
出处:「Getting Python to speak」第 36 段(
text/140-fm-getting-python-to-speak.txt:36,搜「Piper TTS」)。原文:TTS 又称语音合成;text-to-speech 把输入文字变成 spoken audio;装 libportaudio2 + pipewire-alsa;Python 装 piper-tts、pathvalidate、sounddevice。 ↩ -
出处:「Getting Python to speak」第 45 段(
text/140-fm-getting-python-to-speak.txt:45,搜「Low-quality models」)。原文:Piper 音色名 = 语言-地区-质量;低质量模型在 Pi 上跑得快,中高可能慢到不像话;先试 low。 ↩ ↩2 ↩3 -
出处:「Getting Python to speak」第 89 段(
text/140-fm-getting-python-to-speak.txt:89,搜「en_US-ryan-low.onnx」)。原文:ls 可见语音 onnx + json,与人脸检测 onnx 并列——都是神经网络模型,一个生成音频、一个认脸;JSON 描述模型(采样率等)。 ↩ -
出处:「Getting Python to speak」(
text/141-fm-speech-recognition-with-python.txt:11,搜「GPU warning」)。原文:首次 import 有 GPU 警告(无害);建 VoiceSynthesizer 加载模型要一会;voice.speak 一两秒出音;小模型发音不完美;排错:静久了首声发闷属无线耳机常态。 ↩ ↩2 ↩3 -
出处:「Speech recognition with Python」第 63 段(
text/141-fm-speech-recognition-with-python.txt:63,搜「symmetrical」)。原文:识别与合成镜像对称——识别器用语言模型初始化,输入流监听麦克风送音频,识别器从声音帧生成文字 utterances。 ↩ -
出处:「Speech recognition with Python」第 85 段(
text/141-fm-speech-recognition-with-python.txt:85,搜「KaldiRecognizer」)与第 90 段(搜「PartialResult」)。原文:KaldiRecognizer 是 Vosk 主识别器(基于 Kaldi);Model(lang="en-us") + 采样率初始化;InputStream(44.1 kHz、单声道、int16);0.2 秒一块;AcceptWaveform 真 = 说完(Result),否则 PartialResult(半成品,调试用)。 ↩ ↩2 ↩3 ↩4 -
出处:「Connect commands to the robot」第 103 段(
text/142-fm-connect-commands-to-the-robot.txt:103,搜「full_vocabulary」)。原文:词表 = utterances 键 + 唤醒词 + [unk],JSON 喂给 KaldiRecognizer——限定输出范围,减少错配。 ↩ ↩2 -
出处:「Connect commands to the robot」第 126 段(
text/142-fm-connect-commands-to-the-robot.txt:126,搜「handle_command」)。原文:剥唤醒词 → 字典查任务 → 无匹配则合成「没听懂」;任务函数发 launcher/start、stop 再 speak;与网页按钮同权。 ↩ ↩2 ↩3 ↩4 -
出处:「Connect commands to the robot」第 302 段(
text/142-fm-connect-commands-to-the-robot.txt:302,搜「synonym」)。原文:加命令两步——扩展 utterances 字典(可加同义词) + 任务函数;「模式相当重复,但扩展简单」。 ↩ -
出处:「Connect commands to the robot」第 48 段(
text/142-fm-connect-commands-to-the-robot.txt:48,搜「Do not rely on voice commands」)。原文 Tip:不要依赖语音命令停止行为——永远不如 Stop 按钮快。 ↩ ↩2 -
出处:「Challenges」(
text/144-fm-further-reading.txt:11,搜「wake word」)。原文挑战:加更多菜单项(如 face direction)、加同义词、换唤醒词、试 Alan/Amy 音色、进阶——跟踪时听 face_detector 检测,初见人脸说 hello。 ↩ ↩2