Origem: D:/Documents/Github/PiAgentUI/PiAgentUi Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 4
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
InputBoxem 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.tsusaMediaRecordere só transcreve noonstop.src/api/stt.tsenviaPOST /api/stt/transcriptionscomFormData.extensions/piagentui-server.tsproxy a requisição parabaseUrl + transcriptionEndpoint.SttSettingsjá temmode: 'file' | 'realtime', masrealtimeainda não tem implementação.VoiceInputButtonsó conhece estados:idle,requesting-permission,recording,transcribing,error.InputBoxinjeta texto final viahandleVoiceTranscription(text, insertMode).
Comportamento desejado
Iniciar gravação
Quando o usuário clica no microfone:
- o hook recarrega settings com
getSttSettings(); - se
settings.mode === 'realtime', abre WebSocket para/api/stt/realtime; - ao receber
ready, inicia captura comgetUserMediaeMediaRecorderem timeslices curtos; - 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:
- o hook para o
MediaRecorder; - envia
audio.commitpara o backend; - fecha tracks do microfone;
- aguarda finalizações curtas do backend;
- fecha WebSocket;
- retorna para
idlemantendo 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 peloMediaRecorder.audio.commitindica que o usuário parou de falar.session.closeforç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 atranscriptionModel.
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
Modecomfileerealtime; - em
realtime, mostra camposRealtime endpointeRealtime model; - mantém
transcriptionEndpointpara modofile.
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
textcom 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.appendenquanto grava; - chama callback parcial ao receber
transcript.partial; - chama callback final ao receber
transcript.final; - envia
audio.commite 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/realtimerejeita 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.