Ciclo de vida del overlay de voz (macOS)
Audiencia: contribuidores de la app macOS. Objetivo: mantener el overlay de voz predecible cuando la activación por voz y push-to-talk se superponen.
Intención actual
- Si el overlay ya es visible por la activación por voz y el usuario presiona el hotkey, la sesión del hotkey adopta el texto existente en lugar de resetearlo. El overlay permanece visible mientras el hotkey está presionado. Cuando el usuario suelta: envía si hay texto con contenido, de lo contrario descarta.
- La activación por voz sola sigue auto-enviando en silencio; push-to-talk envía inmediatamente al soltar.
Implementado (9 dic, 2025)
- Las sesiones del overlay ahora llevan un token por captura (activación por voz o push-to-talk). Las actualizaciones partial/final/send/dismiss/level se descartan cuando el token no coincide, evitando callbacks obsoletos.
- Push-to-talk adopta cualquier texto visible del overlay como prefijo (así que presionar el hotkey mientras el overlay de activación está visible mantiene el texto y añade nueva voz). Espera hasta 1.5s por una transcripción final antes de recurrir al texto actual.
- El logging de chime/overlay se emite a nivel
infoen las categoríasvoicewake.overlay,voicewake.pttyvoicewake.chime(inicio de sesión, parcial, final, envío, descarte, razón del chime).
Próximos pasos
- VoiceSessionCoordinator (actor)
- Gestiona exactamente una
VoiceSessiona la vez. - API (basada en tokens):
beginWakeCapture,beginPushToTalk,updatePartial,endCapture,cancel,applyCooldown. - Descarta callbacks que lleven tokens obsoletos (evita que reconocedores antiguos reabran el overlay).
- Gestiona exactamente una
- VoiceSession (modelo)
- Campos:
token,source(wakeWord|pushToTalk), texto committed/volatile, flags de chime, timers (auto-send, idle),overlayMode(display|editing|sending), deadline de cooldown.
- Campos:
- Binding del overlay
VoiceSessionPublisher(ObservableObject) refleja la sesión activa en SwiftUI.VoiceWakeOverlayViewrenderiza solo vía el publisher; nunca muta singletons globales directamente.- Las acciones del usuario en el overlay (
sendNow,dismiss,edit) llaman de vuelta al coordinador con el token de sesión.
- Ruta de envío unificada
- En
endCapture: si el texto trimmed está vacío → descartar; si noperformSend(session:)(reproduce chime de envío una vez, reenvía, descarta). - Push-to-talk: sin retraso; activación por voz: retraso opcional para auto-envío.
- Aplica un cooldown corto al runtime de activación después de que push-to-talk termina para que la activación no se re-dispare inmediatamente.
- En
- Logging
- El coordinador emite logs
.infoen el subsistemaai.openclaw, categoríasvoicewake.overlayyvoicewake.chime. - Eventos clave:
session_started,adopted_by_push_to_talk,partial,finalized,send,dismiss,cancel,cooldown.
- El coordinador emite logs
Lista de verificación de depuración
-
Transmite logs mientras reproduces un overlay atascado:
sudo log stream --predicate 'subsystem == "ai.openclaw" AND category CONTAINS "voicewake"' --level info --style compact -
Verifica que solo hay un token de sesión activo; los callbacks obsoletos deben ser descartados por el coordinador.
-
Asegúrate de que al soltar push-to-talk siempre se llama
endCapturecon el token activo; si el texto está vacío, esperadismisssin chime ni envío.
Pasos de migración (sugeridos)
- Añade
VoiceSessionCoordinator,VoiceSessionyVoiceSessionPublisher. - Refactoriza
VoiceWakeRuntimepara crear/actualizar/terminar sesiones en lugar de tocarVoiceWakeOverlayControllerdirectamente. - Refactoriza
VoicePushToTalkpara adoptar sesiones existentes y llamarendCaptureal soltar; aplica cooldown al runtime. - Conecta
VoiceWakeOverlayControlleral publisher; elimina llamadas directas desde runtime/PTT. - Añade tests de integración para adopción de sesión, cooldown y descarte con texto vacío.