想象一个再普通不过的早晨:你刚睡醒,走进书房,随口问了一句——"Hey Jarvis,昨晚开源社区有什么值得关注的 AI 资讯?”
角落里那台平时毫无存在感的 Mac mini 瞬间苏醒,音箱里传出干净利落的回应:“我在。” 短暂的思考后,它开始为你流畅地播报昨夜的最新进展。当你听完,它又无缝切回静音,隐入背景,仿佛什么都没发生过,安静地等待下一次召唤。
过去几天,我把这个充满极客浪漫的场景变成了现实。这并不是简单地把几个远端 API 拼凑成一个“会说话的壳子”,而是一套完全本地优先(local-first)的智能管家原型。这件事真正有意思的地方在于:Mac mini 本身已经具备了做家庭 AI 中枢的大部分软硬件基础,而 OpenClaw 刚好是那个最完美的统一智能内核。
架构:双进程拆分,深谙极简与模块化
整个系统我没有揉成一团,而是将其拆分成了两个本地进程,这实际上非常契合“一个程序只做一件事并做好”的模块化设计理念:
wakeword-daemon(Python):纯粹的设备层。 assistant-main(Node.js / TypeScript):纯粹的智能调度层。
wakeword-daemon 像一个不知疲倦的哨兵,只负责最底层的脏活累活:麦克风持续采集、唤醒词检测、pre-roll 音频缓冲、以及接收控制指令(暂停/恢复)。一旦捕捉到唤醒词,它就通过本地 HTTP 向主服务开火(发送 event)。
assistant-main 则负责一次唤醒后的单轮会话编排:接收唤醒事件、下发指令让哨兵暂停检测、提示用户说话、录音、调用本地 ASR(自动语音识别)转写文本,再把文本发给 OpenClaw 思考,最后拿到结果调用本地 TTS(文本转语音)播报,播报完毕后重新唤醒哨兵。
这里最核心的设计原则是:OpenClaw 是唯一的大脑。 语音层仅仅是一个 I/O 接口,我们绝不在这里重复造轮子(比如另外弄一个“会话智能体”)。这样一来,后续要给 Jarvis 增加长期记忆、工具调用(Tool Use)或者复杂的自动化工作流,都可以直接复用 OpenClaw 强大的底层能力,而不是痛苦地维护两套上下文。
Wake Word:openWakeWord + ONNX 的真·本地唤醒
当前的唤醒检测走的是 openwakeword,模型使用 hey_jarvis_v0.1.onnx,推理框架为 onnx。音频输入配置极为克制且高效:
采样率: 16kHz声道: 1帧长: 80mspre-roll: 1.5s
在实现上,wakeword-daemon 利用 sounddevice.RawInputStream 从麦克风直读 int16 PCM 裸数据。每一帧音频被封装后送入 detector,利用 numpy 转化为数组,交给 ONNX 模型计算 wake word score。一旦 top score 击穿阈值,立刻触发唤醒。
这是一条毫无妥协的纯本地声学唤醒链路,绝非那种“先全部录下来扔给云端转写,再去匹配字符串”的伪唤醒。对于一个需要 24/7 在线、随时待命的家庭终端来说,只有这种基于声学特征的 Edge 端唤醒,才具备真正的设备级体验。
Pre-roll:为什么一定要保留唤醒前的 1.5 秒?
代码里藏着一个极具工程价值的设计:AudioRingBuffer。 系统在底层会永远保留最近约 1.5s 的音频帧,形成一个环形缓冲区。
这个机制专门用来解决语音交互中的一个经典痛点:人类说话时,唤醒词和指令往往是连在一起的("Hey Jarvis帮我开灯")。 如果系统在确认唤醒后才“正式开机录音”,这中间几百毫秒的延迟会直接导致指令的第一个字被吞掉,进而引发 ASR 识别彻底翻车。
当前实现中,pre-roll 的元数据(如 preroll_frame_count 和时间长度)已经随 wake event 传给主控。虽然还没完全拼接进 post-wake 录音,但这说明整套系统的地基是按真实物理设备交互逻辑打的,而不是为了跑通 Demo 做的玩具。
单轮会话:严丝合缝的状态机门控
收到唤醒指令后,assistant-main 会执行一套极其严格的单轮状态机:
调用 wakeword-daemon的/control/pause强制哨兵闭嘴(暂停检测)本地 TTS 播放提示语:“我在。” ffmpeg 介入,从本机设备开始正式录音 本地 ASR 登场,音频转文字 低置信度过滤:如果转写为空、只剩下模糊语气词或过短残片,触发 Retry;若二次失败,优雅终止当前回合。 若文本可用,先轻巧地回一句:“好。” 将文本抛给 OpenClaw 获取回复,本地 TTS 播报 等待短暂 cooldown 避免余音干扰,最后 /control/resume恢复监听哨兵。
这里的 pause/resume 门控(Gating)是维持系统稳定的生命线。不暂停 detector,它就会把自己的播报甚至环境回声再次识别进去,导致幽灵触发或无限套娃对话。通过本地 HTTP 接口显式切断和恢复,逻辑如手术刀般干净利落。
ASR 与 TTS:拒绝云端拼接,死磕本地优先
为了极致的响应速度和隐私安全,ASR 和 TTS 都以本地 helper 脚本的形式运行。
这看似是工程细节,实则决定了整个系统的“灵魂”。当 ASR 和 TTS 完全剥离对云端的依赖,整条链路的延迟骤降,那种“它就住在我这台机器里”的实体感会呈指数级上升。它不再是一个每次交互都要向服务器乞求数据的傀儡,而是一个真正属于你个人的常驻服务。
为什么是 OpenClaw?
如果只是想要一个会聊天的喇叭,你大可随便接入一个模型的 API。但如果你渴望的是一个“家庭 AI 总管”,能力象限就完全不同了。你需要它具备:
跨越周期的上下文延续 长期记忆管理 本地文件与系统工具的调用权限 能够执行 Cron 任务和自动化 Workflow
这正是 OpenClaw 的统治区。它不仅仅是一个对话接口,更是一个高维度的 agent 基础设施。当我们将本地唤醒、ASR 和 TTS 只是作为它的“耳”和“口”接入后,这台 Mac mini 就真正长出了“大脑”,具备了无限进化的可能。
结语
从底层代码来看,这套系统已经构建了语音设备最核心的骨架:本地声学唤醒、环形缓冲、严格的门控逻辑、全本地化的音字转换,以及由 OpenClaw 驱动的统一智能内核。
如果要用一句话来概括这个项目:
Mac mini 赋予了它强大的肉体,OpenClaw 注入了智能的灵魂;将两者完美拼图,你的个人电脑,就是下一个“贾维斯”。
附架构图:
图 1:整体架构图
┌──────────────────────┐│ Mac mini ││ 本地常驻语音终端 │└─────────┬────────────┘ │ ▼┌──────────────────────┐│ wakeword-daemon ││ Python ││ - 麦克风采集 ││ - openWakeWord 检测 ││ - pre-roll ring buf ││ - pause / resume │└─────────┬────────────┘ │ wake event (HTTP) ▼┌──────────────────────┐│ assistant-main ││ Node.js / TypeScript ││ - 提示音 / 录音 ││ - 本地 ASR ││ - 调用 OpenClaw ││ - 本地 TTS 播报 │└─────────┬────────────┘ │ ▼┌──────────────────────┐│ OpenClaw ││ - 对话理解 ││ - 记忆 ││ - 工具调用 ││ - 自动化工作流 │└──────────────────────┘图 2:单轮语音数据流
待命→ “Hey Jarvis”→ wake word 检测→ pause detector→ 提示“我在”→ 录音→ ASR 转写→ OpenClaw 生成回答→ TTS 播报→ cooldown→ resume detector→ 恢复待命
夜雨聆风