Voice — Wake-word + Talk Mode (settings + privacy)
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`.
Normative spec derived from
services/ai/voice/docs/rfcs/RFC-001-wake-word-talk-mode.md. Implementação obrigatória viaKodeVoiceSettingsTileemengines/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:
- Voz ativa (
voice.enabled) - Modo Talk (
voice.talkMode) — mostrado apenas quandovoice.enabled = true - Palavra de ativação (
voice.hotWord) — input com sugestões - Detector (
voice.backend) — dropdown - Barge-in (
voice.bargeIn) — visível apenas em Talk Mode - Gravar áudio para depuração (
voice.debugRecord) — visívelapenas 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.ErrModelMissinge exibir a mensagem padronizada perspecs/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/voiceantes 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.isDebugBuildnokoder_kit. - Quando o usuário ativa
debugRecord(mesmo em dev), o app deveexibir 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
KodeVoiceSettingsTiledokoder_kitna 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)
- [ ]
debugRecordtoggle 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).
See also — sibling specs
Voice (este spec) cobre wake-word + Talk Mode: ouvir o usuário ativamente. Para áudio genérico (memos, podcast, attachments, ringtones) o spec separado é specs/media/audio.kmd. Mic permission do SO é compartilhada, mas Voice e Audio têm UX, toggles em Settings e contratos próprios.
Sibling spec mais amplo: specs/media/ — image, video, audio e document.
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)