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
This commit is contained in:
1 parent
b3489e1ede
commit
98b5db029f
70 files changed
+9466
-9191
No files matched your search
@@ -0,0 +1,225 @@
|
||||
# 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)://<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:
|
||||
|
||||
```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.
|
||||
Reference in new issue
Block a user