用 Moonshine Voice 与 AgentFlow 在树莓派上打造语音控制机器人(My Dalek 实战指南)
2026/9/16 19:18:31 网站建设 项目流程

用 Moonshine Voice 与 AgentFlow 在树莓派上打造语音控制机器人(My Dalek 实战指南)

【免费下载链接】moonshineVery low latency speech to text, intent recognition, and text to speech, for building voice agents and interfaces项目地址: https://gitcode.com/GitHub_Trending/moonshine3/moonshine

本指南基于 Moonshine Voice 开源仓库中的 My Dalek 示例,完整讲解如何用 Python 绑定库moonshine_voice在 Raspberry Pi 上搭建一个"听到自然语言命令 → 语义匹配 → 触发对应动作"的语音控制界面。读完本文,你将掌握AgentFlow的安装、运行、命令注册与阈值调优全流程,并能将其扩展到多轮对话、机器控制、FAQ 应答等真实场景。

一、示例概览:一句"Exterminate!"触发一个动作

My Dalek 是一个以科幻剧《神秘博士》中的 Dalek 机器人命名的演示程序:树莓派接上 USB 麦克风后,程序持续监听语音,把"move forward""turn left""exterminate"这类自然语言短语语义化地匹配到对应的处理函数上。示例本身只打印动作名("Moving forward""EXTERMINATE!"),真正的机器人硬件(轮子、吸盘式死光炮)留给读者自行实现——但它完整展示了 Moonshine Voice 语音交互链路的最小闭环:识别(STT)→ 语义匹配(Embedding)→ 动作执行

核心入口是AgentFlow,它把语音识别、语音合成、短语语义匹配、麦克风管理全部封装起来,调用方只需注册短语与回调。

二、环境准备:安装 moonshine-voice

先进入示例目录并安装 Moonshine Voice 的 Python 包:

cd examples/raspberry-pi/my-dalek pip install moonshine-voice

如果系统提示关于系统包(system packages)的警告(常见于较新的 Debian/Ubuntu 系树莓派 OS),有两种处理方式:

方式一:显式覆盖警告(不推荐在共享环境使用)

pip install --break-system-packages moonshine-voice

方式二:使用 uv 创建虚拟环境(推荐)

# 安装 uv curl -LsSf https://astral.sh/uv/install.sh | sh uv venv source .venv/bin/activate uv pip install moonshine-voice

注意:使用uv后,每次重新登录终端都需要先执行source .venv/bin/activate再运行脚本,否则moonshine_voice不会被解释器找到。

装好后,把 USB 麦克风插到树莓派上(程序依赖麦克风捕捉你的语音)。运行:

python my-dalek.py

首次运行时AgentFlow.load()会自动下载语音识别与短语匹配所需的模型,随后进入监听状态,屏幕输出类似:

============================================================ 🎤 Listening for voice commands... Try saying phrases with the same meaning as these actions: - 'move forward' - 'move backward' - 'turn left' - 'turn right' - 'kill all humans' - 'exterminate' We're doing fuzzy matching of natural language, so phrases like 'Go forward' or 'Move ahead' or 'Advance' will trigger the 'move forward' action, for example. ============================================================ Press Ctrl+C to stop.

正如提示所说,试着用不同说法下达同一指令,例如 "Go ahead" 或 "Murder everyone"——它们会分别命中 "move forward" 和 "kill all humans"。Ctrl+C退出。

三、逐行拆解 my-dalek.py:命令注册与链式配置

示例脚本本身只有 90 行,位于 examples/raspberry-pi/my-dalek/my-dalek.py。核心结构如下。

3.1 定义动作处理函数

def on_move_forward(d): print("Moving forward") def on_move_backward(d): print("Moving backward") def on_turn_left(d): print("Turning left") def on_turn_right(d): print("Turning right") def on_exterminate(d): print("EXTERMINATE!")

每个处理函数接收一个d参数,即AgentFlow注入的Dialog上下文对象(在 agent_flow.py 中定义为Dialog类,见 agent_flow.py#L427-L521)。即使你暂时不用它,参数也必须保留。在真实项目中,这里就是控制机器人轮子、启动吸盘死光炮的位置。

3.2 短语 → 处理函数的映射

commands = { "move forward": on_move_forward, "move backward": on_move_backward, "turn left": on_turn_left, "turn right": on_turn_right, "kill all humans": on_exterminate, "exterminate": on_exterminate, }

注意"kill all humans""exterminate"都映射到on_exterminate——一个动作可以有多个触发短语。AgentFlow对这些短语做的是语义匹配(semantic matching),而非字符串精确匹配,所以 "Go forward""Move ahead""Advance" 都能触发 "move forward",这正是示例输出中 "fuzzy matching" 的含义。

3.3 链式配置与注册

dalek = ( AgentFlow() .language("en") .trigger_threshold(args.threshold) .speech(False) .beeps(False) .on_heard(lambda text: print(text)) .on_progress( lambda fraction, name: print( f"Loading {name}... {fraction:.0%}", file=sys.stderr ) ) ) if args.model_arch is not None: dalek.model_arch(args.model_arch) for phrase, handler in commands.items(): dalek.always(phrase, handler) dalek.load() dalek.start_listening()

这里出现的链式配置项都有实际意义,对应 agent_flow.py 中的同名 setter:

配置项作用默认值源码位置
.language("en")设置识别与合成的语言"en"agent_flow.py#L728-L731
.trigger_threshold(x)短语匹配所需达到的相似度阈值,越接近 1.0 要求越严格0.7agent_flow.py#L782-L791
.speech(False)关闭语音合成器(本例只用文字反馈)Trueagent_flow.py#L763-L770
.beeps(False)关闭识别成功/失败提示音Trueagent_flow.py#L830-L839
.on_heard(cb)每听到一句语音就回调(这里直接打印原文)agent_flow.py#L798-L801
.on_progress(cb)汇报模型下载/加载进度(fraction, name)agent_flow.py#L793-L796
.model_arch(arch)指定识别模型架构(大小)按语言自动选择agent_flow.py#L733-L736

示例代码注释解释了.beeps(False)的原因:提示音是 runner 对"匹配成功/失败"的音频反馈,而本例已经用文字汇报听到的内容,因此关掉避免噪音。

3.4 命令行参数

脚本通过argparse暴露两个实用参数:

parser.add_argument( "--model-arch", type=int, default=None, help="Model architecture to use for transcription", ) parser.add_argument( "--threshold", type=float, default=0.7, help="Similarity threshold for command matching (default: 0.7)", )
  • --threshold:语义匹配阈值,默认0.7。阈值过高会导致本该命中的指令不触发,过低会导致无关语音误触发,需要根据麦克风环境和说话习惯实测调整。
  • --model-arch:指定语音识别模型架构。可选值定义在 moonshine_api.py#L197-L205 的ModelArch枚举中:TINY=0BASE=1TINY_STREAMING=2BASE_STREAMING=3SMALL_STREAMING=4MEDIUM_STREAMING=5。在内存有限的树莓派上,选择 TINY 级别可以在精度与资源占用之间取得平衡。

3.5 主循环与退出

dalek.start_listening() try: while True: time.sleep(0.1) except KeyboardInterrupt: print("\n\nStopping...", file=sys.stderr) finally: dalek.close()

start_listening()是非阻塞的:转录事件在音频线程上到达并驱动你的回调,因此主线程只需要sleep保持存活。Ctrl+Cclose()会释放 runner 自己创建的所有资源(麦克风、模型等)。

四、AgentFlow 的工作原理:globals 与 flows

原文档指出:"AgentFlowis the entry point for voice interfaces"。这里只注册了globals(随时生效的单次命令),但同一个 runner 也能通过listen_for处理多轮对话。理解两者的区别是扩展这个示例的关键,它们定义在 agent_flow.py 中:

  • always(phrase, handler)(agent_flow.py#L1165-L1182):注册一个任何时刻都活跃的全局短语。handler 接收当前Dialog,可以返回一个Prompt(如Say)让 runner 说出来,或返回None
  • listen_for(trigger_phrase, flow)(agent_flow.py#L1144-L1156):注册一个多轮对话流程。flow是一个生成器函数,通过yield d.ask(...)yield d.confirm(...)与用户一来一回。

load()(agent_flow.py#L975-L1042)负责下载并打开语音识别、语音合成、短语匹配三类模型;start_listening()(agent_flow.py#L1082-L1102)打开麦克风并立刻返回——除此之外没有任何需要手动接线的部分

从源码结构看,两条内置全局短语值得一提:"cancel""start over"在 runner 初始化时就被注册(见 agent_flow.py#L710-L711),但默认只在多轮对话进行中生效(flow-scoped)。My Dalek 示例是纯单次命令场景,这两个短语不影响行为。

五、语义匹配的底层机制

"Go ahead 能触发 move forward" 并非魔法,而是由嵌入模型(embedding model)与余弦相似度实现的。核心类PhraseMatcher(agent_flow.py#L267-L360)的工作方式:

  1. 构造时:用嵌入后端为每个注册短语计算一次 embedding 并缓存;
  2. 匹配时:把用户说出的整句话也嵌入一次,与所有短语的 embedding 逐一计算余弦相似度(相似度计算封装在distance()中);
  3. 取分:返回相似度最高且超过trigger_threshold(默认 0.7)的短语对应的 key,低于阈值则返回None,此时 runner 播放"没听懂"提示音。

值得注意的工程细节:

  • 若嵌入模型不可用或use_embeddings(False)AgentFlow会回退到SubstringMatcher(agent_flow.py#L363-L414),即大小写不敏感的子串匹配。它只认用户逐字说出的内容,适合离线测试与冒烟检查,不适合真实语音场景。
  • 触发匹配器按候选短语集合缓存(见_get_trigger_matcher,agent_flow.py#L1460-L1485),进入/退出流程时只是切换缓存,不会对短语重复做 embedding。
  • 库内置的 yes/no 短语等固定文本的 embedding 通过assets/cached_embeddings.tsv预置(见CachedEmbeddings),缓存未命中(通常是用户 utterance)才回落到嵌入模型,从而减少下载与计算开销。

5.1 无音频环境的验证方式

若你暂时没有树莓派或麦克风,官方测试给出了纯文本驱动 runner 的方法。测试文件 test_agent_flow_api.py 中的 fixture 展示了关键组合:

moonshine_voice.AgentFlow() .microphone(False) .speech(False) .use_embeddings(False)

关闭麦克风、合成器与嵌入模型后,即可通过runner.handle_utterance("...")以文本方式喂入语句,观察触发行为。同文件中的test_the_built_in_cancel_stops_the_active_flowtest_the_built_in_start_over_restarts_the_active_flow等用例验证了流程生命周期;My Dalek 这类单命令场景同样可以用handle_utterance("go ahead")做离线验证。

六、从单命令到多轮对话:listen_for 与 Dialog

原文档提到 "the same runner also handles multi-turn conversations throughlisten_for, where it can ask a question, wait for the answer, and confirm it"。仓库中的 examples/python/agent_flow.py 提供了一个完整的 Wi-Fi 配置多轮对话示例,展示了Dialog的三种核心 Prompt:

def setup_wifi(d): ssid = yield d.ask("What's the name of your wifi network?") if not (yield d.confirm(f"I heard, {ssid}. Is that right?")): yield d.say("No problem, let's start over.") return password = yield d.ask( "Please spell the wifi password, one letter at a time, and say 'done' when finished.", mode=SPELLED, ) if (yield d.confirm("Would you like to hear it read back?")): yield d.say(f"I heard: {spell_out(password)}") if (yield d.confirm("Apply these changes?")): _apply_wifi_config(ssid, password) yield d.say("Done. Your wifi is set up.") else: yield d.say("Okay, nothing changed.")

然后注册触发短语并启动:

runner = ( AgentFlow() .listen_for("set up wifi", setup_wifi) .listen_for("configure wifi", setup_wifi) ) runner.load() runner.start_listening()

关键点:

  • d.ask(prompt):说话提问,下一句用户语音以字符串形式返回(支持mode=SPELLED/DIGITS拼写模式);
  • d.confirm(prompt):提问并返回布尔值(内建 yes/no 短语表,可覆盖yes_phrases/no_phrases);
  • d.say(text):纯播报,播完继续流程;
  • d.cancel()/d.restart():抛出异常以放弃或重启当前流程,与内置的 "cancel"/"start over" 全局短语对应;
  • 子流程可用yield from组合,流程像脚本一样从上往下读:分支是if/else,重试是while

从源码看,Dialog本身不做任何 I/O(agent_flow.py#L427-L435 的类注释明确说明),所有 Prompt 由 runner 执行后把结果send回生成器,因此流程函数可以在无音频、无 TTS、无事件循环的条件下做单元测试。

七、扩展到你的应用:自定义短语与调优建议

原文档强调:"You can also change the phrases to whatever you need for your application, and the same kind of semantic matching will work for them too"。仓库 README(language-bindings/python/README.md#L159-L205)给出了同样的模式,例如灯光控制:

from moonshine_voice import AgentFlow def lights_on(d): print("\n💡 LIGHTS ON!") def lights_off(d): print("\n🌑 LIGHTS OFF!") runner = ( AgentFlow() .always("turn on the lights", lights_on) .always("turn off the lights", lights_off) ) runner.load() runner.start_listening()

实战调优建议(依据源码行为与参数默认值):

  1. 短语用"一句话的语义核心"而非字面词AgentFlow按意义匹配,因此 "kill all humans" 既能被 "Murder everyone" 命中,也可能被语义相近但你不希望触发的说法命中。触发过于敏感时调高trigger_threshold(如0.8),触发不灵敏时调低(如0.6)。
  2. 一个动作注册多个变体短语:像 "kill all humans" 与 "exterminate" 共用一个 handler,是最直接的误触发控制手段。
  3. 树莓派资源受限时选小模型:通过--model-arch(或代码里的.model_arch())选择TINY(0)/TINY_STREAMING(2)等架构,减小内存与延迟开销。
  4. 监听反馈:用on_heard打印用户原话、on_progress汇报模型下载进度,便于排查"没听到"还是"没匹配上"。
  5. 多轮场景注册为 flow:需要确认、追问或拼写输入时使用listen_for,并利用内置的 "cancel"/"start over" 让用户随时退出或重来。

八、小结与进一步阅读

My Dalek 示例展示了 Moonshine Voice 构建语音界面的完整闭环:安装 → 配置 → 注册语义短语 →load()加载模型 →start_listening()开始监听。AgentFlow把识别、合成、语义匹配和麦克风管理封装在一个对象里,单次命令用always,多轮对话用listen_for,无论控制机器人、工业机械还是应答 FAQ,注册短语的模式完全一致。

想继续深入,可参考仓库中的这些资源:

  • 示例源码:examples/raspberry-pi/my-dalek/my-dalek.py
  • 多轮对话完整示例:examples/python/agent_flow.py
  • AgentFlow核心实现:language-bindings/python/src/moonshine_voice/agent_flow.py
  • Python 绑定使用说明:language-bindings/python/README.md
  • 行为验证测试:language-bindings/python/tests/test_agent_flow_api.py

【免费下载链接】moonshineVery low latency speech to text, intent recognition, and text to speech, for building voice agents and interfaces项目地址: https://gitcode.com/GitHub_Trending/moonshine3/moonshine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询