语音浮层生命周期 (macOS)

受众:macOS 应用贡献者。目标:在唤醒词和按键说话重叠时保持语音浮层的可预测性。

当前设计意图

  • 如果浮层已经因唤醒词而显示,用户按下热键时,热键会话会_接管_现有文本而不是重置它。浮层在热键按住期间保持显示。当用户松开时:如果有修剪后的文本则发送,否则关闭浮层。
  • 单独的唤醒词仍然在静音时自动发送;按键说话在松开时立即发送。

已实现功能 (2025 年 12 月 9 日)

  • 浮层会话现在为每次捕获(唤醒词或按键说话)携带一个 token。当 token 不匹配时,部分/最终/发送/关闭/音量更新会被丢弃,避免过时的回调。
  • 按键说话会接管任何可见的浮层文本作为前缀(因此在唤醒浮层显示时按下热键会保留文本并追加新的语音)。它会等待最多 1.5 秒以获取最终转录,然后回退到当前文本。
  • 提示音/浮层日志以 info 级别输出,分类为 voicewake.overlayvoicewake.pttvoicewake.chime(会话开始、部分、最终、发送、关闭、提示音原因)。

后续步骤

  1. VoiceSessionCoordinator (actor)
    • 同时只拥有一个 VoiceSession
    • API(基于 token):beginWakeCapturebeginPushToTalkupdatePartialendCapturecancelapplyCooldown
    • 丢弃携带过时 token 的回调(防止旧识别器重新打开浮层)。
  2. VoiceSession (model)
    • 字段:tokensource(wakeWord|pushToTalk)、已提交/易失文本、提示音标志、计时器(自动发送、空闲)、overlayMode(display|editing|sending)、冷却截止时间。
  3. 浮层绑定
    • VoiceSessionPublisherObservableObject)将活动会话镜像到 SwiftUI。
    • VoiceWakeOverlayView 仅通过 publisher 渲染;它从不直接修改全局单例。
    • 浮层用户操作(sendNowdismissedit)使用会话 token 回调到协调器。
  4. 统一发送路径
    • endCapture 时:如果修剪后的文本为空 → 关闭;否则 performSend(session:)(播放一次发送提示音、转发、关闭)。
    • 按键说话:无延迟;唤醒词:自动发送可选延迟。
    • 在按键说话结束后对唤醒 runtime 应用短暂冷却,这样唤醒词不会立即重新触发。
  5. 日志记录
    • 协调器在子系统 bot.molt 中输出 .info 日志,分类为 voicewake.overlayvoicewake.chime
    • 关键事件:session_startedadopted_by_push_to_talkpartialfinalizedsenddismisscancelcooldown

调试检查清单

  • 在重现粘滞浮层时流式输出日志:

    sudo log stream --predicate 'subsystem == "bot.molt" AND category CONTAINS "voicewake"' --level info --style compact
  • 验证只有一个活动会话 token;协调器应该丢弃过时的回调。

  • 确保按键说话松开时始终使用活动 token 调用 endCapture;如果文本为空,期望 dismiss 而不播放提示音或发送。

迁移步骤(建议)

  1. 添加 VoiceSessionCoordinatorVoiceSessionVoiceSessionPublisher
  2. 重构 VoiceWakeRuntime 以创建/更新/结束会话,而不是直接操作 VoiceWakeOverlayController
  3. 重构 VoicePushToTalk 以接管现有会话并在松开时调用 endCapture;应用 runtime 冷却。
  4. VoiceWakeOverlayController 连接到 publisher;移除来自 runtime/PTT 的直接调用。
  5. 添加会话接管、冷却和空文本关闭的集成测试。