OpenClaw 源码解读(7): 跨平台语音唤醒实现机制
深入 OpenClaw 跨平台语音唤醒架构,解析从本地 VAD 检测、端侧唤醒词引擎到云端流式 ASR 的完整链路,以及多设备统一调度机制。
在前面的篇章中,我们解析了 OpenClaw 的路由、A2UI 和工具链。但作为个人 AI 助手,如果每次都要你掏出手机、打开 App、打字输入,那它充其量只是个“高级搜索引擎”。真正的助手,应该是在你做饭、开车或者敲代码时,只需喊一声,它就能随时待命。
语音交互(Voice UI)是通向 AGI 终极形态的必经之路。但跨平台语音开发是个大坑:macOS、iOS 和 Android 对麦克风权限和后台存活机制的管理完全不同。
今天,我们就来拆解 OpenClaw 的 Nodes(节点)架构,看看它是如何通过 TypeScript、Swift 和 Kotlin 的混编,实现全平台“唤醒与聆听”的。
语音流转核心:从声波到文字,再到声音
在 OpenClaw 中,所有的硬件设备(Mac、iPhone、安卓机)都被抽象为一个个“Node(节点)”。节点负责收集声音,而 Gateway(网关)负责大脑的思考和转化。
一次完整的语音交互流转如下:
-
Node(端侧): 监听唤醒词 -> 录制音频 -> 通过 WebSocket 将音频流(PCM/WAV)推送到 Gateway。
-
Gateway(云/局域网端): 调用 STT(Speech-to-Text,如 Whisper)将语音转为文字 -> 路由给 Agent -> Agent 生成回复文本。
-
Gateway(云/局域网端): 调用 TTS(Text-to-Speech,如 ElevenLabs 或系统自带 TTS)生成音频流 -> 推送回 Node。
-
Node(端侧): 播放音频。
我们看看 Gateway 是如何处理这段双向音频流的:
// src/core/audio/VoicePipeline.ts (源码概念简化版)
import { STTEngine } from './stt';
import { TTSEngine } from './tts';
export class VoicePipeline {
/**
* 处理来自 Node 的音频流
*/
public async handleNodeAudioStream(nodeId: string, audioBuffer: Buffer) {
// 1. 语音转文本 (STT)
const transcribedText = await STTEngine.transcribe(audioBuffer);
// 2. 扔给 Agent 处理,获取回复文本
const agentReply = await AgentManager.routeAndProcess(nodeId, transcribedText);
// 3. 文本转语音 (TTS),结合 ElevenLabs 实现高度拟人化的声音
// 注意:这里通常采用流式 (Streaming) 处理来降低首字节延迟 (TTFB)
const audioStream = await TTSEngine.synthesizeStream(agentReply, {
voiceId: 'eleven_labs_molty_voice',
model: 'eleven_turbo_v2'
});
// 4. 将音频流通过 WebSocket 发送回设备 Node 进行播放
await GatewayServer.sendAudioToNode(nodeId, audioStream);
}
}
macOS/iOS:轻量级本地唤醒词机制
在苹果生态中,一直开着麦克风把音频传到服务器是不可行的(既耗电,又会被 iOS 的后台机制杀掉)。因此,OpenClaw 在 apps/apple 目录下,使用了约 8.9% 的 Swift 原生代码来实现一个端侧轻量级唤醒监听器。
端侧只负责一件事:在本地运行一个极小的离线模型(比如基于 Apple 框架或 Porcupine),专门监听唤醒词。
// apps/apple/OpenClawNode/Speech/WakeWordEngine.swift (源码概念简化版)
import AVFoundation
import Speech
class WakeWordEngine: ObservableObject {
private let audioEngine = AVAudioEngine()
private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))
func startListening() {
let inputNode = audioEngine.inputNode
let recordingFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in
// 本地极速分析音频帧,匹配唤醒词
if self.detectWakeWord(in: buffer) {
self.triggerGatewayWakeup()
self.startRecordingCommand() // 唤醒后开始录制真正的指令
}
}
try? audioEngine.start()
}
private func triggerGatewayWakeup() {
// 通过 WebSocket 通知 Gateway:主人叫你了!
WebSocketClient.shared.send(event: "NODE_WAKED", payload: ["nodeId": Device.currentId])
}
}
通过 Swift 原生介入,Mac 和 iPhone 节点可以以极低的功耗在后台常驻。只有听到唤醒词的瞬间,才会打通与 TypeScript 编写的 Gateway 的高耗能长连接。
Android 端侧:持续语音拾音逻辑(Talk Mode)
相比之下,安卓系统的开放性给了 OpenClaw 更多发挥空间。在 apps/android 目录下,OpenClaw 利用 2.1% 的 Kotlin 代码实现了一个非常硬核的“Talk Mode(持续对话模式)”。
在这个模式下,Agent 不再是一问一答,而是通过 VAD(Voice Activity Detection,静音检测) 算法,像真人打语音电话一样持续沟通。
// apps/android/OpenClawNode/audio/VADManager.kt (源码概念简化版)
class VADManager {
private val threshold = -40.0 // 分贝阈值
private var isSpeaking = false
private val audioBufferChunk = mutableListOf<ByteArray>()
fun processAudioFrame(frame: ByteArray, dbLevel: Double) {
if (dbLevel > threshold) {
// 用户正在说话
if (!isSpeaking) {
isSpeaking = true
WebSocketClient.sendEvent("USER_SPEECH_START")
}
audioBufferChunk.add(frame)
} else {
// 检测到停顿 (例如超过 1.5 秒没有说话)
if (isSpeaking && hasSilenceExceeded(1500)) {
isSpeaking = false
// 将刚才的一整段话打包发给 Gateway
WebSocketClient.sendAudioStream(audioBufferChunk.toByteArray())
audioBufferChunk.clear()
}
}
}
}
在 Android 端的 Talk Mode 中,VAD 会自动切分你的句子。你停顿思考时,它就把前半句发给云端处理;你继续说,它就继续录。这种体验打破了传统语音助手“说完必须傻等”的僵硬感,实现了真正意义上的全双工(Full-duplex)对话流。
总结
跨平台开发不应该只是简单的套壳(Webview)。OpenClaw 为我们做了一个极佳的示范:
-
核心业务逻辑 留在 TypeScript(Gateway)中,保证迭代速度与跨平台复用。
-
高频、底层、敏感的硬件交互(如 iOS 的唤醒词、Android 的 VAD),果断采用 Swift 和 Kotlin 编写 Native Node,以获取系统级最高权限与最优性能。
正是这种“各司其职”的设计,让那只叫 Open Claw 的空间龙虾真正长出了灵敏的“耳朵”和拟真的“嘴巴”。
当我们用语音唤醒了设备之后,AI 还能对这台设备做些什么?仅仅是聊天吗? 敬请期待下一篇,老金带你看看 Agent 是如何跨端调用你的摄像头、截取屏幕,甚至执行本地终端命令的!我们下期见。