Files
PiAgentUI/docs/superpowers/specs/2026-06-02-piagentui-stt-realtime-design.md
T
Evandro 98b5db029f wip: snapshot das alterações não commitadas (2026-10-20)
Origem: D:/Documents/Github/PiAgentUI/PiAgentUi
Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 4
2026-10-20 00:48:28 -03:00

7.0 KiB

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:

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:

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:

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:

ws(s)://<baseUrl sem /v1 se necessário><realtimeEndpoint>

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:

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:

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:

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.