语音浮窗生命周期(macOS)
面向 macOS 应用贡献者。目标:当唤醒词和按键说话(push-to-talk)重叠时,保持语音浮窗行为可预测。
当前设计意图
- 如果浮窗因唤醒词已经显示,此时用户按下快捷键,快捷键会话会_接管_已有文本而非重置。浮窗在按键期间保持显示。松开时:有有效文本就发送,否则关闭。
- 唤醒词模式仍然在静默后自动发送;按键说话松开后立即发送。
已实现(2025年12月9日)
- 浮窗会话现在每次捕获都带有一个 token(唤醒词或按键说话)。partial/final/send/dismiss/level 更新在 token 不匹配时被丢弃,避免旧回调干扰。
- 按键说话会接管浮窗中已有文本作为前缀(所以在唤醒浮窗显示时按下快捷键会保留文本并追加新语音)。松开后最多等待 1.5 秒以获取最终转录,否则回退到当前文本。
- 音效/浮窗日志以
info级别记录在voicewake.overlay、voicewake.ptt和voicewake.chime类别下(会话开始、partial、final、发送、关闭、音效原因)。
后续规划
- VoiceSessionCoordinator(actor)
- 同一时间只持有一个
VoiceSession。 - API(基于 token):
beginWakeCapture、beginPushToTalk、updatePartial、endCapture、cancel、applyCooldown。 - 丢弃携带过期 token 的回调(防止旧识别器重新打开浮窗)。
- 同一时间只持有一个
- VoiceSession(model)
- 字段:
token、source(wakeWord|pushToTalk)、committed/volatile 文本、音效标记、定时器(自动发送、空闲超时)、overlayMode(display|editing|sending)、冷却截止时间。
- 字段:
- 浮窗绑定
VoiceSessionPublisher(ObservableObject)将活跃会话映射到 SwiftUI。VoiceWakeOverlayView仅通过 publisher 渲染;不直接修改全局单例。- 浮窗用户操作(
sendNow、dismiss、edit)通过会话 token 回调 coordinator。
- 统一发送路径
endCapture时:去空白后文本为空 → 关闭;否则performSend(session:)(播放一次发送音效,转发,关闭)。- 按键说话:无延迟;唤醒词:可选自动发送延迟。
- 按键说话结束后对唤醒运行时施加短暂冷却期,防止唤醒词立即重新触发。
- 日志
- Coordinator 在 subsystem
ai.openclaw下以.info级别记录日志,类别为voicewake.overlay和voicewake.chime。 - 关键事件:
session_started、adopted_by_push_to_talk、partial、finalized、send、dismiss、cancel、cooldown。
- Coordinator 在 subsystem
调试清单
-
重现浮窗卡住时流式查看日志:
sudo log stream --predicate 'subsystem == "ai.openclaw" AND category CONTAINS "voicewake"' --level info --style compact -
确认只有一个活跃会话 token;过期回调应被 coordinator 丢弃。
-
确保按键说话松开时一定用活跃 token 调用
endCapture;文本为空时预期为无音效无发送的dismiss。
迁移步骤(建议)
- 添加
VoiceSessionCoordinator、VoiceSession和VoiceSessionPublisher。 - 重构
VoiceWakeRuntime:创建/更新/结束会话,而非直接操作VoiceWakeOverlayController。 - 重构
VoicePushToTalk:接管现有会话,松开时调用endCapture;施加运行时冷却。 - 将
VoiceWakeOverlayController接入 publisher;移除运行时/PTT 的直接调用。 - 添加会话接管、冷却和空文本关闭的集成测试。