Voice — wake-word & talk mode

Toggles obrigatórios em Settings (`voice.enabled`, `voice.talkMode`, `voice.hotWord`, `voice.backend`, `voice.bargeIn`, `voice.debugRecord`), defaults seguros (tudo OFF na instalação fresca exceto `bargeIn` que é ON quando `talkMode` ativo), e contrato de privacidade — ring buffer pré-wake de 2 s **nunca** sai do dispositivo, post-wake só vai pro `services/ai/voice` configurado, nada é logado/persistido a menos que `debugRecord = true`. Surface compartilhada: `KodeVoiceSettingsTile` em `engines/sdk/koder_kit`.

Source spec: specs/voice/wake-word.kmd

Settings: voice toggles

The six knobs every Koder app with a microphone surfaces under Settings → Voice. Defaults are paranoid: nothing captures audio until the user opts in.

voice.enabled OFF

Master switch. While OFF the entire voice subsystem stays inactive; the secondary toggles are hidden.

voice.talkMode OFF

Continuous-conversation mode. Mic stays open while in talk mode; idle timeout returns to standby.

voice.hotWord OFF

Wake-word detection ("Hey Koder"). Pre-wake ring buffer is strictly local — never leaves the device.

voice.backend openWakeWord

openWakeWord by default; falls back to Porcupine when openWakeWord is unavailable.

voice.bargeIn ON when talkMode

Allows the user to interrupt mid-response. ON automatically when talkMode flips ON.

voice.debugRecord OFF (dev only)

Captures raw audio to a local file for debugging. Dev builds only; ignored in non-dev builds.

Privacy contract

Hot-phrase example

"Hey Koder" Talk mode active

Wake-word fires only when the configured backend matches the hot phrase. Pre-wake samples that do not match are dropped instantly.

Error map (APP-VOICE-*)

Voice subsystem errors carry the canonical PRODUCT-CAT-CODE-SEQ shape. <APP> stands in for the calling product (KALL, KORE, …).

Code prefix Meaning
<APP>-VOICE-001-* Microphone permission denied by the OS or revoked in Settings.
<APP>-VOICE-002-* Configured wake-word backend (openWakeWord / Porcupine) failed to load.
<APP>-VOICE-003-* Wake-word detection collapsed mid-session; toggles flip OFF defensively.
<APP>-VOICE-004-* Voice toggle ON but Settings consent never granted; aborting capture.

Full specification

Voice Wake-word + Talk Mode Spec — v0.1

Normative spec derived from services/ai/voice/docs/rfcs/RFC-001-wake-word-talk-mode.md. Implementação obrigatória via KodeVoiceSettingsTile em engines/sdk/koder_kit (Flutter) — nunca rolar UI de voz local.


Scope

Aplica-se a todo app Koder com microfone, em qualquer plataforma: Flutter mobile (Android + iOS), Flutter desktop (Linux + macOS + Windows), e qualquer surface futura que capture áudio do usuário (TV em casos específicos, web PWA com getUserMedia). CLI/TUI sem mic não implementam wake-word; recebem texto via stdin como hoje.


1 — MUST: expose "Voz" toggle group in Settings

Todo app Koder com mic capability deve ter um agrupamento "Voz" (Voice em en-US) na tela de Settings, contendo, na ordem:

  1. Voz ativa (voice.enabled)
  2. Modo Talk (voice.talkMode) — mostrado apenas quando voice.enabled = true
  3. Palavra de ativação (voice.hotWord) — input com sugestões
  4. Detector (voice.backend) — dropdown
  5. Barge-in (voice.bargeIn) — visível apenas em Talk Mode
  6. Gravar áudio para depuração (voice.debugRecord) — visível apenas em builds dev/debug

A implementação é via drop-in KodeVoiceSettingsTile do koder_kit. Nunca desenhar a tile localmente — todo app importa o componente e o renderiza onde quiser na tela de Settings.


2 — MUST: defaults seguros em fresh install

Em uma instalação nova (ou após Reset Settings) os valores armazenados devem corresponder à tabela abaixo. Absência da chave = mesmo valor que default, ou seja, configuração mínima é silently OFF.

Chave Default Notas
voice.enabled OFF Privacy-by-default; sem opt-in nenhuma feature de voz roda
voice.talkMode OFF Exige voice.enabled = true; UI deve desabilitar (greyed-out) quando enabled é false
voice.hotWord "hey kode" Lower-case ASCII; pode ser sobrescrito por hot-words alternativos (PT-BR: "oi kode")
voice.backend autoselect Plugin escolhe (preferindo openWakeWord); usuário pode forçar "openwakeword" ou "porcupine"
voice.bargeIn ON (em Talk Mode) Cancela TTS quando o usuário começa a falar — relevante apenas com talkMode ativo
voice.debugRecord OFF Disponível apenas em builds dev/debug; em release builds o toggle não aparece sequer

Quando voice.enabled for toggled OFF em runtime, o app deve parar imediatamente o capture de mic (interromper qualquer detector ativo) + limpar o ring buffer (ver §4).


3 — MUST: hot-word configurável

voice.hotWord aceita qualquer string que tenha um modelo carregável no detector configurado. O KodeVoiceSettingsTile deve:

  • Sugerir a lista de hot-words shipped (hey kode, oi kode, etc.)
  • Aceitar input livre com validação preview (resolve modelo? exibe ✓/✗ inline)
  • Quando o detector não tem modelo para a palavra escolhida, retornar o erro voice.ErrModelMissing e exibir a mensagem padronizada per specs/errors/user-facing-messages.kmd

4 — MUST: privacidade do ring buffer pré-wake

O detector mantém um ring buffer rolante de 2 segundos de áudio PCM 16-bit mono 16 kHz. Esse buffer é uma estrutura de memória local e nunca sai do dispositivo. Especificamente:

  • NUNCA enviado a services/ai/voice antes da wake-event detection
  • NUNCA persistido em disco
  • NUNCA logado em telemetria, breadcrumbs, traces ou error reports (mesmo quando voice.debugRecord = true — o pré-wake permanece efêmero)
  • NUNCA acessível por outro processo (no Flutter mobile o buffer vive no plugin native; no desktop vive no processo do app — nenhum IPC channel exporta esse buffer)

Quando o detector dispara (wake event), o conteúdo do buffer + o áudio capturado a partir desse instante é encaminhado ao endpoint configurado em services/ai/voice para STT. O encaminhamento dessa amostra deve documentar via KoderApp.config():

  • A URL do endpoint para o qual o áudio é enviado
  • Confirmação visual no app (ex: indicador de mic ativo, sound effect curto) — operadores nunca devem ouvir o app sem saber que está ativo

5 — MUST: debugRecord só em dev builds

voice.debugRecord = true faz com que toda a captura post-wake seja salva localmente em ~/.kode/voice/debug/<utc-timestamp>.wav. Esse caminho é local-only, mas é uma feature trapdoor — em release builds o toggle não pode aparecer.

  • A flag é gateada por KoderApp.isDebugBuild no koder_kit.
  • Quando o usuário ativa debugRecord (mesmo em dev), o app deve exibir um warning visível ("Debug recording está ON — áudio sendo gravado em disco") e logar a ativação no console.
  • O ring buffer pré-wake continua não sendo persistido mesmo em modo debug. Privacy-by-default não pisa nesse contrato.

6 — MUST: barge-in semantics em Talk Mode

voice.bargeIn = true (default em Talk Mode) significa: quando o TTS está reproduzindo a resposta do agente E o VAD detecta fala do usuário, o TTS deve ser cancelado imediatamente (fade-out ≤ 100 ms para evitar pop) e o detector volta para escuta.

voice.bargeIn = false é o modo half-duplex tradicional: o usuário espera o TTS terminar antes de falar. Útil em ambientes ruidosos onde o falso-positivo de VAD seria mais incômodo que esperar.


7 — MUST: error surface

Erros de voz que cheguem ao usuário devem seguir specs/errors/user-facing-messages.kmd: texto humanizado em pt-BR/en-US, botão "Ver detalhes" expandindo o erro técnico, ID único no formato <APP>-VOICE-<CODE>-<SEQ>. Mapping mínimo:

Cenário ID Texto pt-BR
Permissão de mic negada <APP>-VOICE-MIC-001 "Permita o acesso ao microfone para usar comandos de voz."
Modelo de hot-word não encontrado <APP>-VOICE-MODEL-001 "A palavra '$hot' não tem modelo no detector $backend."
Endpoint de voz indisponível <APP>-VOICE-NET-001 "Servidor de voz indisponível. Tente novamente."
Detector backend falhou na inicialização <APP>-VOICE-INIT-001 "Não foi possível ativar o detector $backend."

8 — Observability

  • KodeApp.metrics() deve emitir três counters bumping em wake-event: voice.wake.detected, voice.wake.false_positive (registrado quando o usuário cancela explicitamente o pipeline pós-wake), voice.wake.error.
  • Latency histograms (per RFC-001 §performance): voice.wake.detect_ms, voice.stt.first_token_ms, voice.tts.first_audio_ms.
  • Não emitir métricas que vazem o conteúdo capturado (transcrição, texto post-wake) — apenas counters + latência.

9 — Adoption checklist (per app)

Quando um app Koder ganha mic capability, os pull requests devem incluir:

  • Importa KodeVoiceSettingsTile do koder_kit na tela de Settings
  • Toggles aparecem na ordem da §1
  • Em fresh install, todos os defaults da §2 são respeitados
  • Ring buffer pré-wake satisfaz o contrato da §4 (nunca leaves device)
  • debugRecord toggle não aparece em release builds
  • Mensagens de erro seguem a tabela da §7

CI gate: koder-relay status-style probe não cobre voice; falar com /k-test <module> quando o tile + adoption sweep landar (KSTACK-89).


Non-normative — referências

  • RFC: services/ai/voice/docs/rfcs/RFC-001-wake-word-talk-mode.md
  • Sub-tickets: KSTACK-45 (openWakeWord), KSTACK-46 (Porcupine), KSTACK-47 (streaming STT), KSTACK-48 (streaming TTS), KSTACK-49 (Flutter native plugin), KSTACK-50 (este spec), KSTACK-51 (latency benchmark gate)
  • Implementation surface: engines/sdk/koder_kit/lib/src/voice/ (a criar — esta surface ainda não existe; landa em KSTACK-89 junto com a adoção apps-side)