語音覆蓋層生命週期(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)、cooldown 截止時間。
  3. 覆蓋層綁定
    • VoiceSessionPublisherObservableObject)將活動中的工作階段映射至 SwiftUI。
    • VoiceWakeOverlayView 僅透過 publisher 呈現;它永遠不直接修改全域 singleton。
    • 覆蓋層使用者操作(sendNowdismissedit)帶著工作階段 token 回呼 coordinator。
  4. 統一送出路徑
    • endCapture 時:文字為空 → 關閉;否則 performSend(session:)(播放一次送出提示音、轉發、關閉)。
    • 按住說話:不延遲;喚醒詞:可選延遲用於自動送出。
    • 按住說話結束後對喚醒執行環境套用短暫 cooldown,防止喚醒詞立即重新觸發。
  5. 日誌
    • Coordinator 在子系統 ai.openclaw、類別 voicewake.overlayvoicewake.chime 中輸出 .info 日誌。
    • 關鍵事件:session_startedadopted_by_push_to_talkpartialfinalizedsenddismisscancelcooldown

除錯檢查清單

  • 重現覆蓋層卡住時同步串流日誌:

    sudo log stream --predicate 'subsystem == "ai.openclaw" AND category CONTAINS "voicewake"' --level info --style compact
  • 確認只有一個活動中的工作階段 token;過時的回呼應被 coordinator 丟棄。

  • 確保按住說話放開時總是以活動 token 呼叫 endCapture;文字為空時預期 dismiss 而不觸發提示音或送出。

遷移步驟(建議)

  1. 新增 VoiceSessionCoordinatorVoiceSessionVoiceSessionPublisher
  2. 重構 VoiceWakeRuntime,改為建立/更新/結束 session,而非直接操作 VoiceWakeOverlayController
  3. 重構 VoicePushToTalk,改為接管既有 session 並在放開時呼叫 endCapture;套用 runtime cooldown。
  4. VoiceWakeOverlayController 接上 publisher;移除 runtime/PTT 的直接呼叫。
  5. 新增工作階段接管、cooldown 和空文字關閉的整合測試。