Saltar al contenido principal

Ciclo de vida de la superposición de voz (macOS)

Audiencia: colaboradores de la app para macOS. Objetivo: mantener predecible la superposición de voz cuando la palabra de activación y pulsar para hablar se solapan.

Comportamiento

  • Si la superposición ya está visible por la palabra de activación y el usuario pulsa la tecla de acceso rápido, la sesión de tecla de acceso rápido adopta el texto existente en lugar de reiniciarlo. La superposición permanece visible mientras se mantiene pulsada la tecla de acceso rápido. Al soltarla: se envía si hay texto recortado; de lo contrario, se descarta.
  • La palabra de activación por sí sola sigue enviándose automáticamente en silencio; pulsar para hablar envía inmediatamente al soltar.

Implementación

  • VoiceSessionCoordinator (apps/macos/Sources/OpenClaw/VoiceSessionCoordinator.swift) es el único propietario de la sesión de voz activa. Es un singleton @MainActor @Observable, no un actor. API: startSession, updatePartial, finalize, sendNow, dismiss, updateLevel, snapshot. Cada sesión lleva un token UUID; las llamadas con un token obsoleto o no coincidente se descartan.
  • VoiceWakeOverlayController (VoiceWakeOverlayController+Session.swift) renderiza la superposición y reenvía las acciones del usuario (requestSend, dismiss) al coordinador mediante el token de sesión. Nunca posee por sí mismo el estado de la sesión.
  • Pulsar para hablar (VoicePushToTalk.begin()) adopta cualquier texto visible de la superposición como adoptedPrefix (mediante VoiceSessionCoordinator.shared.snapshot()), de modo que pulsar la tecla de acceso rápido mientras la superposición de activación está visible conserva el texto y añade la nueva voz. Al soltar, espera hasta 1,5 s un transcrito final antes de recurrir al texto actual.
  • En dismiss, la superposición llama a VoiceSessionCoordinator.overlayDidDismiss, lo que activa VoiceWakeRuntime.refresh(state:) para que el cierre manual con X, el descarte por texto vacío y el descarte posterior al envío reanuden la escucha de la palabra de activación.
  • Ruta de envío unificada: si el texto recortado está vacío, descartar; de lo contrario, sendNow reproduce una vez el sonido de envío, reenvía mediante VoiceWakeForwarder y luego descarta.

Registro

El subsistema de voz es ai.openclaw; cada componente registra en su propia categoría:
CategoríaComponente
voicewake.coordinatorVoiceSessionCoordinator
voicewake.overlayVoiceWakeOverlayController/VoiceWakeOverlay
voicewake.pttTecla de acceso rápido y captura de pulsar para hablar
voicewake.runtimeRuntime de palabra de activación
voicewake.chimeReproducción de sonido
voicewake.syncSincronización de ajustes globales
voicewake.forwardReenvío de transcritos
voicewake.meterMonitor de nivel de micrófono

Lista de comprobación de depuración

  • Transmite los registros mientras reproduces una superposición persistente:
    sudo log stream --predicate 'subsystem == "ai.openclaw" AND category CONTAINS "voicewake"' --level info --style compact
    
  • Verifica que solo haya un token de sesión activo; las devoluciones de llamada obsoletas las descarta el coordinador.
  • Confirma que soltar pulsar para hablar siempre llama a end() con el token activo; si el texto está vacío, se espera un descarte sin sonido ni envío.

Relacionado