Files
openclaw/docs/platforms/mac/voice-overlay.md
Peter Steinberger f7d7148cf0 docs: rewrite published docs grounded in current source (#100142)
Source-grounded rewrite of 529 published docs pages with per-unit information-loss verification: 1,713 factual corrections cited to src/**, generated surfaces regenerated, frontmatter titles preserved for i18n, release notes pages untouched. All docs gates green.

Closes #100141
2026-07-05 00:32:47 -04:00

3.4 KiB

summary, read_when, title
summary read_when title
Voice overlay lifecycle when wake-word and push-to-talk overlap
Adjusting voice overlay behavior
Voice overlay

Voice Overlay Lifecycle (macOS)

Audience: macOS app contributors. Goal: keep the voice overlay predictable when wake-word and push-to-talk overlap.

Behavior

  • If the overlay is already visible from wake-word and the user presses the hotkey, the hotkey session adopts the existing text instead of resetting it. The overlay stays up while the hotkey is held. On release: send if there is trimmed text, otherwise dismiss.
  • Wake-word alone still auto-sends on silence; push-to-talk sends immediately on release.

Implementation

  • VoiceSessionCoordinator (apps/macos/Sources/OpenClaw/VoiceSessionCoordinator.swift) is the single owner of the active voice session. It is a @MainActor @Observable singleton, not an actor. API: startSession, updatePartial, finalize, sendNow, dismiss, updateLevel, snapshot. Each session carries a UUID token; calls with a stale or mismatched token are dropped.
  • VoiceWakeOverlayController (VoiceWakeOverlayController+Session.swift) renders the overlay and forwards user actions (requestSend, dismiss) back through the coordinator via the session token. It never owns the session state itself.
  • Push-to-talk (VoicePushToTalk.begin()) adopts any visible overlay text as adoptedPrefix (via VoiceSessionCoordinator.shared.snapshot()) so pressing the hotkey while the wake overlay is up keeps the text and appends new speech. On release, it waits up to 1.5s for a final transcript before falling back to the current text.
  • On dismiss, the overlay calls VoiceSessionCoordinator.overlayDidDismiss, which triggers VoiceWakeRuntime.refresh(state:) so manual X-dismiss, empty-text dismiss, and post-send dismiss all resume wake-word listening.
  • Unified send path: if trimmed text is empty, dismiss; otherwise sendNow plays the send chime once, forwards via VoiceWakeForwarder, then dismisses.

Logging

Voice subsystem is ai.openclaw; each component logs under its own category:

Category Component
voicewake.coordinator VoiceSessionCoordinator
voicewake.overlay VoiceWakeOverlayController/VoiceWakeOverlay
voicewake.ptt Push-to-talk hotkey and capture
voicewake.runtime Wake-word runtime
voicewake.chime Chime playback
voicewake.sync Global settings sync
voicewake.forward Transcript forwarding
voicewake.meter Mic level monitor

Debugging checklist

  • Stream logs while reproducing a sticky overlay:

    sudo log stream --predicate 'subsystem == "ai.openclaw" AND category CONTAINS "voicewake"' --level info --style compact
    
  • Verify only one active session token; stale callbacks are dropped by the coordinator.

  • Confirm push-to-talk release always calls end() with the active token; if text is empty, expect a dismiss without chime or send.