# Design — STT realtime no composer do PiAgentUI ## Objetivo Implementar transcrição de fala em tempo real no PiAgentUI: enquanto o usuário fala, o texto aparece diretamente no campo de mensagem; ao parar, o texto permanece editável e pode ser enviado manualmente. ## Escopo aprovado A abordagem aprovada é **Backend realtime**. Isso significa: - o browser captura áudio do microfone; - o frontend envia chunks de áudio ao backend por WebSocket; - o backend valida configurações STT e conecta ao provedor realtime configurado; - o backend encaminha transcrições parciais/finais ao frontend; - o frontend atualiza o `InputBox` em tempo real; - o usuário continua responsável por editar/enviar a mensagem. Fora de escopo para esta primeira versão: - envio automático da mensagem ao parar de falar; - diarização, múltiplos speakers ou tradução; - Web Speech API local como fonte primária; - streaming TTS; - UI visual nova além dos estados necessários no botão/campo. ## Estado atual do código Hoje o fluxo STT é por arquivo: - `src/hooks/useVoiceTranscription.ts` usa `MediaRecorder` e só transcreve no `onstop`. - `src/api/stt.ts` envia `POST /api/stt/transcriptions` com `FormData`. - `extensions/piagentui-server.ts` proxy a requisição para `baseUrl + transcriptionEndpoint`. - `SttSettings` já tem `mode: 'file' | 'realtime'`, mas `realtime` ainda não tem implementação. - `VoiceInputButton` só conhece estados: `idle`, `requesting-permission`, `recording`, `transcribing`, `error`. - `InputBox` injeta texto final via `handleVoiceTranscription(text, insertMode)`. ## Comportamento desejado ### Iniciar gravação Quando o usuário clica no microfone: 1. o hook recarrega settings com `getSttSettings()`; 2. se `settings.mode === 'realtime'`, abre WebSocket para `/api/stt/realtime`; 3. ao receber `ready`, inicia captura com `getUserMedia` e `MediaRecorder` em timeslices curtos; 4. o botão muda para estado de gravação/stop. ### Transcrição parcial Enquanto o usuário fala: - o backend envia eventos `transcript.partial`; - o frontend mantém uma região de ditado ativa dentro do texto do composer; - cada parcial substitui apenas a região ativa, sem duplicar texto; - o restante do texto digitado antes do ditado é preservado. ### Transcrição final Quando o provedor envia `transcript.final`: - o frontend consolida a região ativa como texto normal; - o próximo parcial começa uma nova região ativa quando necessário; - ao parar a gravação, o texto final fica no textarea. ### Parar gravação Quando o usuário clica em stop: 1. o hook para o `MediaRecorder`; 2. envia `audio.commit` para o backend; 3. fecha tracks do microfone; 4. aguarda finalizações curtas do backend; 5. fecha WebSocket; 6. retorna para `idle` mantendo o texto no campo. ## Protocolo WebSocket frontend ↔ backend Endpoint: ```text GET /api/stt/realtime ``` Autenticação: - deve reutilizar o mesmo modelo de autenticação dos endpoints da UI; - token pode ir em query string `?token=...` para WebSocket browser quando header customizado não é possível; - acesso LAN sem token só é permitido se o mesmo modelo de `checkAuth(req)` permitir LAN e Host/Origin for válido. Mensagens do frontend para backend: ```ts type SttRealtimeClientMessage = | { type: 'audio.append'; audio: string } | { type: 'audio.commit' } | { type: 'session.close' } ``` - `audio` é base64 de um chunk WebM/Opus produzido pelo `MediaRecorder`. - `audio.commit` indica que o usuário parou de falar. - `session.close` força cleanup. Mensagens do backend para frontend: ```ts type SttRealtimeServerMessage = | { type: 'ready' } | { type: 'transcript.partial'; text: string } | { type: 'transcript.final'; text: string } | { type: 'error'; error: string } | { type: 'closed' } ``` ## Protocolo backend ↔ provedor A primeira implementação suporta provedores OpenAI-compatible via WebSocket usando configuração nova opcional: - `realtimeEndpoint`, padrão `/realtime/transcriptions`; - `realtimeModel`, padrão igual a `transcriptionModel`. O backend monta a URL: ```text ws(s):// ``` Para OpenAI oficial, a implementação deve aceitar futura adaptação para protocolo específico, mas esta entrega mantém o contrato backend↔frontend estável e testável. Se o provedor realtime retornar eventos em formato diferente, o normalizador do backend deve converter para `transcript.partial` e `transcript.final`. ## Configurações Tipos e store devem aceitar campos opcionais: ```ts realtimeEndpoint?: string realtimeModel?: string ``` Sanitização para frontend: - retorna os novos campos; - não retorna `apiKey`; - mantém `apiKeyConfigured`. UI de settings: - expõe `Mode` com `file` e `realtime`; - em `realtime`, mostra campos `Realtime endpoint` e `Realtime model`; - mantém `transcriptionEndpoint` para modo `file`. ## Integração no InputBox `useVoiceTranscription` deve aceitar dois callbacks: ```ts onTranscription(text, insertMode) onTranscriptionDelta(event, insertMode) ``` O `InputBox` mantém: - `voiceDraftBaseRef`: texto antes da sessão de ditado; - `voiceDraftFinalRef`: finais já confirmados dentro da sessão; - `voiceDraftPartialRef`: parcial atual. Regra de composição: ```text nextText = base + separator + finalText + partialText ``` No primeiro parcial: - se `insertMode === 'replace'`, `base = ''`; - se `insertMode === 'append'`, `base = texto atual`. Ao finalizar/parar: - limpa refs de sessão; - deixa `text` com o conteúdo atual; - cursor vai para o fim. ## Estados de erro - STT desabilitado → erro no botão, sem abrir microfone. - API key obrigatória ausente → erro no botão. - Realtime não configurado → erro no botão. - WebSocket backend falhou → erro no botão, microfone fechado. - Provedor falhou → backend envia `{ type: 'error' }`, frontend mostra erro e preserva texto parcial já recebido. ## Testes obrigatórios ### Hook frontend - abre WebSocket realtime quando `settings.mode === 'realtime'`; - envia chunks `audio.append` enquanto grava; - chama callback parcial ao receber `transcript.partial`; - chama callback final ao receber `transcript.final`; - envia `audio.commit` e fecha recursos ao parar. ### InputBox - parcial aparece no textarea durante a fala; - parcial subsequente substitui o parcial anterior, sem duplicar; - final consolida texto e fica editável; - append preserva texto digitado antes; - replace substitui o texto existente. ### Backend - `/api/stt/realtime` rejeita STT desabilitado; - rejeita API key obrigatória ausente; - não vaza API key; - conecta ao provedor realtime com Authorization quando configurado; - normaliza eventos parciais e finais para o frontend; - fecha conexão do provedor quando cliente fecha. ## Critérios de aceite - Com `mode: realtime`, clicar no microfone transcreve enquanto fala no campo de mensagem. - Ao parar, o texto continua no campo e pode ser editado/enviado. - O fluxo antigo por arquivo continua funcionando quando `mode: file`. - Nenhuma chave de API é exposta ao frontend. - Testes focados, typecheck e build passam.