macOS 语音叠加层生命周期OpenClaw 中 Wake-Word 与 Push-to-Talk 的会话协调机制【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw导读本文面向 macOS 应用贡献者与语音交互调试者围绕 voice-overlay.md 展开深入讲解 OpenClaw macOS 应用里语音叠加层Voice Overlay在「唤醒词wake-word自动监听」与「按住说话push-to-talk」两条输入路径重叠时的完整生命周期从会话令牌session token的单一所有权模型、文本收养adoptedPrefix规则到统一发送路径、日志分类与调试手段。读完你将掌握叠加层在什么条件下保持、重置、发送或消失并能用log stream快速定位「叠加层卡住」类问题。配置入口与角色边界叠加层归谁管语音输入在 macOS 应用的Dashboard → Settings → Talk → This Mac中配置。这里需要明确一条重要的职责划分叠加层overlay与麦克风测试保持原生实现——即由 macOS 应用本体渲染的浮层窗口与原生麦克风电平面板Dashboard 只负责持有它们的设备设置——麦克风选择、语言选择、触发词、提示音开关等配置存放在 Dashboard 侧叠加层本身不拥有这些设置。语音唤醒的具体开关与权限说明见 Voice wakemacOS该文档还包含 push-to-talk 的权限要求、转发载荷格式等配套信息。叠加层与唤醒词运行时通过VoiceSessionCoordinator建立联系下文将逐步展开这条调用链。核心行为契约两条输入路径重叠时的规则当用户已经通过唤醒词唤起叠加层浮层可见、正在显示部分转写此时又按下 push-to-talk 热键会发生什么voice-overlay.md 定义了三条必须保证的「可预测行为」热键会话收养既有文本叠加层已由唤醒词显示时按下热键新的 push-to-talk 会话会采纳adopt现有文本而不是重置它。按住热键期间叠加层保持显示释放热键的收尾规则释放时——如果存在去除空白后的文本trimmed text则发送否则直接消失dismiss两条发送语义不可混淆唤醒词单独触发的会话仍然「静音即自动发送」auto-send on silencepush-to-talk 则是「释放立即发送」send immediately on release。这三条规则构成了整个生命周期设计的验收标准。下面看它们是如何在源码中实现的。实现解剖单一会话权威与令牌校验VoiceSessionCoordinator唯一的会话所有者VoiceSessionCoordinator.swift 是整个语音会话的单一所有者类型声明为MainActor Observable final class VoiceSessionCoordinator { static let shared VoiceSessionCoordinator() // ... }注意它不是 actor而是MainActor Observable单例——所有会话状态都运行在主线程上UI 可直接读取同时通过Observable让叠加层视图对状态变化自动响应。它对外暴露的 API 与职责如下API职责startSession(source:text:attributed:forwardEnabled:voiceWakeTrigger:) - UUID开启新会话返回一个UUID令牌并把会话同步到叠加层控制器源码updatePartial(token:text:attributed:)更新进行中的部分转写partial transcripttoken 不匹配则静默丢弃源码finalize(token:text:sendChime:autoSendAfter:voiceWakeTrigger:)标记会话为最终结果可附带自动发送延时源码sendNow(token:reason:)统一发送路径空文本直接 dismiss否则播放发送提示音并转发源码dismiss(token:reason:outcome:)主动关闭会话源码updateLevel(token:_:)更新麦克风电平驱动浮层上的电平表源码snapshot()返回(token, text, visible)快照供 push-to-talk 判断是否收养文本源码每个会话携带一个UUID令牌任何携带过期或不匹配令牌的调用都会被直接丢弃func updatePartial(token: UUID, text: String, attributed: NSAttributedString? nil) { guard let session, session.token token else { return } // ... }这个「先校验令牌、后执行」的模式贯穿updatePartial/finalize/sendNow/dismiss/updateLevel全部接口是防止旧会话回调污染新会话的关键防线。VoiceWakeOverlayController只渲染、不持有状态VoiceWakeOverlayControllerSession.swift 中的VoiceWakeOverlayController负责渲染浮层并把用户动作requestSend、dismiss通过会话令牌转发回协调器。它从不自己拥有会话状态activeToken只是当前被授权渲染的令牌引用真正的文本与发送决策都在协调器里。它的会话相关方法与行为startSession(token:source:transcript:...)接收协调器创建的令牌展示浮层源码updatePartial/presentFinal更新浮层文本presentFinal时若autoSendAfter有值则安排自动发送任务源码requestSend(token:reason:)用户点击发送按钮时调用经令牌校验后回拨VoiceSessionCoordinator.shared.sendNow(token:reason:)源码dismiss(token:reason:outcome:)执行 0.18s 的淡出动画后真正隐藏窗口并根据 outcome 触发「眨眼」或「发送庆祝」反馈最后回调协调器的overlayDidDismiss源码guardToken/evaluateToken令牌守卫逻辑判定结果为accept/dropMismatch/dropNoActive三态源码。架构要点叠加层是「哑视图」所有决策上移给协调器。即使浮层被多次创建或重建会话状态也只有一份从根上避免「浮层与状态不同步」。Push-to-Talk 如何收养叠加文本adoptedPrefix 与 1.5 秒最终转录窗口VoicePushToTalk.swift 实现了按住说话逻辑。触发方式是右 Option 键keyCode 61 .option通过全局与本地NSEvent的.flagsChanged监听器探测只观察、不吞掉事件源码。begin()先快照、再收养当叠加层已由唤醒词显示时按下热键begin()的关键步骤源码let snapshot await MainActor.run { VoiceSessionCoordinator.shared.snapshot() } self.adoptedPrefix snapshot.visible ? snapshot.text.trimmingCharacters(in: .whitespacesAndNewlines) : 即叠加层可见则把当前文本收养为adoptedPrefix否则为空。随后启动会话时文本直接以adoptedPrefix起步VoiceSessionCoordinator.shared.startSession( source: .pushToTalk, text: adoptedPrefix, attributed: adoptedAttributed, forwardEnabled: true)这样按住热键说话时新识别出的词通过join(prefix, suffix)拼接在收养文本之后叠加层内容连续、不丢失源码。同时begin()还会调用VoiceWakeRuntime.shared.pauseForPushToTalk()暂停常驻的唤醒词识别器——避免两条音频管线争抢麦克风 tap源码。end()1.5 秒宽限窗释放热键时源码若什么都没捕获到且没有收养前缀committed/volatile/adoptedPrefix全空立即以空文本结束——对应「无文本即 dismiss、不响提示音、不发送」否则启动一个1.5 秒的宽限任务等待 Speech 送达最终转录结果self.timeoutTask Task { [weak self] in try? await Task.sleep(nanoseconds: 1_500_000_000) // 1.5s grace period await self?.finalize(transcriptOverride: nil, reason: timeout, sessionID: sessionID) }如果 1.5 秒内收到了isFinal结果则立即提交否则回退到当前已累积文本。最终文本 join(adoptedPrefix, 最终识别文本)随后走统一发送路径源码。结束后还会调用applyPushToTalkCooldown()与refresh(state:)让唤醒词识别器重新上岗。实现细节AVAudioEngine与识别器均为惰性创建begin()时才初始化避免应用启动即抢占音频资源、把蓝牙耳机切到低质量 HFP 模式——唤醒词运行时与 push-to-talk 都遵循这一策略源码。统一发送路径与 dismiss 后的恢复空文本 dismiss非空则发送无论来源是唤醒词还是 push-to-talk最终都收敛到协调器的sendNow(token:reason:)源码取session.text并去除首尾空白若为空以.empty结果 dismiss并清除会话若非空先进入beginSendUI(token:sendChime:)——只播一次发送提示音send chime、展示发送中状态0.28 秒后自动关闭浮层源码实际的转发由 VoiceWakeForwarder.swift 的forwardToSelectedSession(transcript:voiceWakeTrigger:)在Task.detached中异步完成——选路逻辑是优先当前活跃 WebChat 会话键否则回退网关主会话键再经sessions.list解析投递通道与目标最终默认 WebChat源码。overlayDidDismiss让唤醒监听「复活」叠加层关闭时无论用户点 X、空文本自动关闭、还是发送后关闭协调器都会回调overlayDidDismiss(token:)源码func overlayDidDismiss(token: UUID) { if self.session?.token token { self.clearSession() } Task { await VoiceWakeRuntime.shared.refresh(state: AppStateStore.shared) } }VoiceWakeRuntime.refresh(state:)源码读取当前配置开关、触发词、麦克风、语言、提示音在权限齐备的前提下重新启动常驻识别器——于是每次 dismiss 之后唤醒词监听都会恢复这是「叠加层不可见时系统仍在听」这一生命周期不变量得以成立的保证。日志体系与调试清单日志分类语音子系统统一使用ai.openclaw作为 subsystem各组件在各自 category 下记录Category组件voicewake.coordinatorVoiceSessionCoordinatorvoicewake.overlayVoiceWakeOverlayController/VoiceWakeOverlayvoicewake.pttPush-to-talk 热键与采集voicewake.runtime唤醒词运行时voicewake.chime提示音播放voicewake.sync全局设置同步voicewake.forward转写转发voicewake.meter麦克风电平监控复现「叠加层卡住」时的排查步骤流式查看语音日志边复现边观察sudo log stream --predicate subsystem ai.openclaw AND category CONTAINS voicewake --level info --style compact验证只有一个活跃会话令牌协调器与叠加层都会在令牌不匹配时打出drop ... token_mismatch/drop ... no_active日志。如果看到大量 mismatch 丢弃说明存在旧会话回调未清理确认 push-to-talk 释放时总是以活跃令牌调用end()若释放时文本为空应观察到「dismiss 且无提示音、无发送」——如果反而发生了发送说明adoptedPrefix或宽限窗逻辑出现偏差可结合ptt finalize reason...日志核对reason是emptyOnRelease还是timeout。测试佐证令牌语义的自动化保障仓库中的 VoiceWakeOverlayControllerTests.swift 直接锁定了本节描述的两条核心语义无 UI 模式下的完整生命周期startSession后snapshot().token token、isVisible trueupdatePartial后文本更新为 hello world电平被钳制在 0…1dismiss后isVisible false、token nil令牌守卫三态evaluateToken对no active/mismatch/accept的判定与实现一一对应且传入nil令牌被接受视为「当前活跃会话」的隐式引用。push-to-talk 侧另有 VoicePushToTalkTests.swift 与 VoicePushToTalkHotkeyTests.swift 覆盖热键与会话行为可作为修改叠加层逻辑时的回归基线。小结与延伸阅读叠加层生命周期可以浓缩为一句话会话状态唯一属于VoiceSessionCoordinator叠加层只是渲染窗口push-to-talk 用快照收养文本所有关闭路径都经由overlayDidDismiss恢复唤醒监听。理解这层「单一所有者 令牌校验 统一发送」的结构后无论是排查叠加层异常还是新增交互入口都能快速定位改动点。相关文档Voice wakemacOS唤醒词与 push-to-talk 模式、权限要求、转发载荷格式macOS 应用总览macOS 应用的整体架构与入口Talk 模式唤醒触发 Talk Mode 时的会话语义【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考