编程语言研究书库

语音 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。

防错配的三件套(本章的工程精华):

  1. 唤醒词(wake word)「robot」:只处理以它开头的整句;
  2. 未知词占位 [unk]:整句里混入未知词就丢弃——「robot teapot banana」 只配当 Partial,永不执行1;
  3. 限定词表(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. 可带走的

  1. 语音 agent 五拍:收音 → 识别 → 过滤 → 发 MQTT → 合成应答;语音只是又一种遥控器13。
  2. 机载音频三宗罪:麦克风要凑近、喇叭磁铁扰 IMU、有线拴人——无线 USB 耳麦全解4。
  3. TTS 选型:小模型优先——低质量 Piper 在 Pi 上才「说得快」;中高质量慢到不可用7。
  4. 识别三件套:唤醒词 + [unk] 拒收 + 限定词表——用小词表换准确率112。
  5. Result() 是整句、PartialResult() 是半成品;0.2 秒一块喂识别器11。
  6. 命令 = 字典 + 任务函数;任务函数 = MQTT launcher 消息,和网页按钮同权13。
  7. 安全:停机器人永远用按钮,不用嘴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

  1. 出处:「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

  2. 出处:「Connect commands to the robot」(text/142-fm-connect-commands-to-the-robot.txt:198,搜「three neural network AI models」)。原文:现在 Pi 上同时跑着三个神经网络 AI 模型——一个认脸、一个认声、一个(合成语音)发声。 ↩ ↩2

  3. 出处:「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

  4. 出处:「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

  5. 出处:「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 测试。 ↩

  6. 出处:「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。 ↩

  7. 出处:「Getting Python to speak」第 45 段(text/140-fm-getting-python-to-speak.txt:45,搜「Low-quality models」)。原文:Piper 音色名 = 语言-地区-质量;低质量模型在 Pi 上跑得快,中高可能慢到不像话;先试 low。 ↩ ↩2 ↩3

  8. 出处:「Getting Python to speak」第 89 段(text/140-fm-getting-python-to-speak.txt:89,搜「en_US-ryan-low.onnx」)。原文:ls 可见语音 onnx + json,与人脸检测 onnx 并列——都是神经网络模型,一个生成音频、一个认脸;JSON 描述模型(采样率等)。 ↩

  9. 出处:「Getting Python to speak」(text/141-fm-speech-recognition-with-python.txt:11,搜「GPU warning」)。原文:首次 import 有 GPU 警告(无害);建 VoiceSynthesizer 加载模型要一会;voice.speak 一两秒出音;小模型发音不完美;排错:静久了首声发闷属无线耳机常态。 ↩ ↩2 ↩3

  10. 出处:「Speech recognition with Python」第 63 段(text/141-fm-speech-recognition-with-python.txt:63,搜「symmetrical」)。原文:识别与合成镜像对称——识别器用语言模型初始化,输入流监听麦克风送音频,识别器从声音帧生成文字 utterances。 ↩

  11. 出处:「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

  12. 出处:「Connect commands to the robot」第 103 段(text/142-fm-connect-commands-to-the-robot.txt:103,搜「full_vocabulary」)。原文:词表 = utterances 键 + 唤醒词 + [unk],JSON 喂给 KaldiRecognizer——限定输出范围,减少错配。 ↩ ↩2

  13. 出处:「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

  14. 出处:「Connect commands to the robot」第 302 段(text/142-fm-connect-commands-to-the-robot.txt:302,搜「synonym」)。原文:加命令两步——扩展 utterances 字典(可加同义词) + 任务函数;「模式相当重复,但扩展简单」。 ↩

  15. 出处:「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

  16. 出处:「Challenges」(text/144-fm-further-reading.txt:11,搜「wake word」)。原文挑战:加更多菜单项(如 face direction)、加同义词、换唤醒词、试 Alan/Amy 音色、进阶——跟踪时听 face_detector 检测,初见人脸说 hello。 ↩ ↩2

On this page