LeoRad ASR
v3Reconhecimento de fala próprio da LeoRad para ditado médico em português, com formatação de laudo embutida.
https://asr.leorad.com.br#Visão geral
O LeoRad ASR é o serviço de reconhecimento de fala da própria LeoRad, treinado em ditado médico em português do Brasil. Ele recebe trechos curtos de áudio e devolve, na mesma resposta, a transcrição crua e o texto já formatado segundo as normas do laudo.
É a integração indicada para ditado em tempo real: o áudio é enviado em trechos enquanto o médico fala e cada resposta chega em menos de um segundo. Para transcrever um arquivo de áudio inteiro, de uma vez só, use o endpoint POST /speech da API de IA.
Como o texto é produzido
áudio WAV 16 kHz mono
│
▼
modelo de fala (fine-tuning em ditado médico) ──▶ verbatim
│ "análise dois pontos parágrafo nível l quatro l cinco ..."
▼
normas do laudo (números, datas, níveis, medidas)
│ "análise dois pontos parágrafo nível L4-L5 ..."
▼
formatação (pontuação, parágrafos, cabeçalhos) ──▶ formatado
"Análise:\nNível L4-L5 ..."Rotas disponíveis
verbatim e são convertidos em pontuação no campo formatado. Renderize o formatado e guarde o verbatim se precisar aplicar o dicionário do próprio usuário.#Autenticação
O ASR não tem login próprio: ele aceita um token temporário emitido pela plataforma a partir do seu Token de API. O token vale 30 minutos e é o que o navegador envia em cada trecho de áudio, sem nunca carregar credenciais de longa duração.
- 1Token de API: Obtenha o Token de API da conta como descrito na documentação da API REST. É um JWT que identifica o usuário da LeoRad.
- 2Token do ASR: Troque o Token de API por um token temporário em
POST /realtime-v3/get-token. - 3Chamada ao ASR: Envie o áudio para
https://asr.leorad.com.brcom o cabeçalhoAuthorization: Bearer <token>.
Obtendo o token temporário
A emissão acontece na API de IA, não no ASR. Autentique com o Token de API no cabeçalho Authorization:
curl -X POST "https://api-ai.leorad.com.br/realtime-v3/get-token" \
-H "Authorization: Bearer <SEU_TOKEN_DE_API>"Exemplo de resposta
{
"token": "eyJzdWIiOiIxMjM0IiwiaWF0IjoxNzY4...",
"expires_at": "2026-09-11T18:42:07.000Z"
}Utilizando o token do ASR
O token devolvido vai no cabeçalho Authorization de toda requisição ao ASR:
Authorization: Bearer eyJzdWIiOiIxMjM0IiwiaWF0IjoxNzY4...401, descarte o token em cache, peça outro e repita a requisição uma única vez.#Rotas
Duas rotas, ambas sob https://asr.leorad.com.br. /transcrever exige o token temporário; /saude é pública.
#POST /transcrever
Recebe um trecho de áudio em multipart/form-data e devolve a transcrição em duas formas: verbatim (texto cru, comandos como palavras) e formatado (pontuação, parágrafos, medidas e cabeçalhos já aplicados).
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| arquivo | binary | Sim | Trecho de áudio. O caminho rápido é WAV PCM 16 kHz mono; outros formatos são convertidos no servidor. Corpo limitado a 2 MB. |
| seq | integer | Não | Número do trecho na sessão de ditado. É devolvido intacto na resposta, o que permite remontar os trechos na ordem certa mesmo com requisições em paralelo. |
| audio | binary | Não | Nome alternativo do campo arquivo, mantido por compatibilidade. Prefira arquivo em integrações novas. |
verbatim e formatado vazios e inferencia_ms igual a zero. É uma defesa contra alucinação em silêncio — trate a string vazia como "nada a inserir".Resposta
{
"seq": 1,
"verbatim": "análise dois pontos parágrafo nível l quatro l cinco dois pontos abaulamento discal medindo três vírgula cinco por dois centímetros",
"formatado": "Análise:\nNível L4-L5: abaulamento discal medindo 3,5 x 2 cm",
"duracao_audio_ms": 7320,
"inferencia_ms": 284
}Exemplo de requisição (JavaScript)
const formData = new FormData();
formData.append('arquivo', wavBlob, 'trecho-1.wav'); // WAV 16 kHz mono
formData.append('seq', '1');
const response = await fetch('https://asr.leorad.com.br/transcrever', {
method: 'POST',
headers: {
'Authorization': 'Bearer <SEU_TOKEN_DO_ASR>',
},
body: formData,
});
if (!response.ok) {
throw new Error(`ASR respondeu ${response.status}`);
}
const { seq, verbatim, formatado } = await response.json();
console.log(seq, formatado);504. Trechos de 5 a 15 segundos são o ponto de equilíbrio entre latência e contexto para o modelo.#GET /saude
Verificação mínima e pública, sem token. Responde 200 com ok: true quando o modelo está carregado e pronto. Use no carregamento da tela para habilitar o botão do microfone.
Resposta
{ "ok": true }Exemplo de requisição (JavaScript)
const disponivel = await fetch('https://asr.leorad.com.br/saude')
.then((r) => r.ok)
.catch(() => false);
// Use o resultado para habilitar (ou não) o botão do microfone.
setMicrofoneHabilitado(disponivel);502 ou ok: false — trate como indisponibilidade temporária.#Formato do áudio
O modelo trabalha em 16 kHz mono. Enviar o áudio já nesse formato evita uma conversão no servidor e tira algumas dezenas de milissegundos da latência de cada trecho.
| Propriedade | Valor |
|---|---|
| Formato preferido | WAV PCM 16 bits |
| Taxa de amostragem | 16 kHz |
| Canais | Mono (estéreo é mixado no servidor) |
| Tamanho máximo do corpo | 2 MB (cerca de 60 s de WAV 16 kHz) |
| Duração recomendada | 5 a 15 segundos por trecho |
| Outros formatos | webm, ogg, mp3 e afins são convertidos por ffmpeg no servidor |
Convertendo antes de enviar
O áudio que o navegador produz com MediaRecorder costuma ser webm ou ogg. Se o seu fluxo não gera WAV diretamente, converta antes de enviar:
ffmpeg -i entrada.webm -ac 1 -ar 16000 saida.wav#Ditado ao vivo
O padrão validado em produção é o commit por pausa: em vez de manter uma conexão aberta, o cliente acumula áudio e fecha um trecho quando detecta uma pausa na fala. Cada trecho é uma requisição independente, o que simplifica a reconexão e mantém a resposta abaixo de um segundo.
- 1Capture PCM em 16 kHz: Abra o microfone com
getUserMediae leia as amostras em umAudioWorkletouScriptProcessor. - 2Detecte a pausa: Acompanhe o RMS de cada quadro. Silêncio contínuo por cerca de 600 ms fecha o trecho; um limite de duração (10 a 15 s) fecha trechos de fala ininterrupta.
- 3Envie com seq crescente: Codifique o trecho em WAV e envie com
seqincremental. Não espere a resposta anterior para começar a gravar o próximo trecho. - 4Insira na ordem do seq: Use o
seqdevolvido para inserir o texto na posição certa do editor, mesmo que as respostas cheguem fora de ordem.
Exemplo completo (JavaScript)
// Ditado ao vivo com commit por pausa.
// Captura PCM em 16 kHz, fecha um trecho quando o médico faz uma pausa
// e envia cada trecho ao ASR com um "seq" crescente.
const TAXA = 16000;
const LIMIAR_RMS = 0.005; // abaixo disso consideramos silêncio
const PAUSA_MS = 600; // silêncio contínuo que fecha o trecho
const TRECHO_MAX_MS = 15000; // fecha o trecho mesmo sem pausa
const contexto = new AudioContext({ sampleRate: TAXA });
const fonte = contexto.createMediaStreamSource(
await navigator.mediaDevices.getUserMedia({ audio: true }),
);
const no = contexto.createScriptProcessor(4096, 1, 1);
let buffer = [];
let amostrasSilencio = 0;
let seq = 0;
no.onaudioprocess = (evento) => {
const quadro = new Float32Array(evento.inputBuffer.getChannelData(0));
buffer.push(quadro);
const rms = Math.sqrt(quadro.reduce((s, v) => s + v * v, 0) / quadro.length);
amostrasSilencio = rms < LIMIAR_RMS ? amostrasSilencio + quadro.length : 0;
const amostras = buffer.reduce((s, q) => s + q.length, 0);
const pausa = amostrasSilencio > (PAUSA_MS / 1000) * TAXA;
const cheio = amostras > (TRECHO_MAX_MS / 1000) * TAXA;
if ((pausa && amostras > TAXA) || cheio) {
enviarTrecho(paraWav(buffer, TAXA), ++seq);
buffer = [];
amostrasSilencio = 0;
}
};
fonte.connect(no);
no.connect(contexto.destination);
async function enviarTrecho(wav, seq) {
const corpo = new FormData();
corpo.append('arquivo', wav, `trecho-${seq}.wav`);
corpo.append('seq', String(seq));
const resposta = await fetch('https://asr.leorad.com.br/transcrever', {
method: 'POST',
headers: { 'Authorization': `Bearer ${await obterToken()}` },
body: corpo,
});
const { formatado } = await resposta.json();
if (formatado) inserirNoEditor(formatado); // na ordem do seq
}
// Float32 mono -> WAV PCM 16 bits.
function paraWav(quadros, taxa) {
const total = quadros.reduce((s, q) => s + q.length, 0);
const buffer = new ArrayBuffer(44 + total * 2);
const view = new DataView(buffer);
const texto = (pos, s) => [...s].forEach((c, i) => view.setUint8(pos + i, c.charCodeAt(0)));
texto(0, 'RIFF');
view.setUint32(4, 36 + total * 2, true);
texto(8, 'WAVEfmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, taxa, true);
view.setUint32(28, taxa * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
texto(36, 'data');
view.setUint32(40, total * 2, true);
let pos = 44;
for (const quadro of quadros) {
for (const amostra of quadro) {
const v = Math.max(-1, Math.min(1, amostra));
view.setInt16(pos, v < 0 ? v * 0x8000 : v * 0x7fff, true);
pos += 2;
}
}
return new Blob([view], { type: 'audio/wav' });
}formatado antes de exibir.#Comandos e normas
O campo formatado é o verbatim depois de duas camadas de regras: os comandos de ditado viram pontuação e as normas do laudo padronizam números, medidas, datas e níveis vertebrais.
Comandos de ditado
| O médico fala | Sai como |
|---|---|
| “vírgula” | , |
| “ponto” | . |
| “ponto final” | . |
| “ponto e vírgula” | ; |
| “dois pontos” | : |
| “parágrafo” | quebra de parágrafo |
| “ponto parágrafo” | . + quebra de parágrafo |
| “nova linha” | quebra de linha |
| “abre parênteses / fecha parênteses” | ( ) |
| “interrogação” | ? |
| “reticências” | ... |
Normas do laudo
Além da pontuação, o serviço padroniza os elementos numéricos do laudo. Cabeçalhos de seção ditados com "dois pontos" (Análise, Técnica, Conclusão, Impressão diagnóstica, entre outros) vão para uma linha própria com inicial maiúscula.
| O médico fala | Sai como |
|---|---|
| “três vírgula cinco” | 3,5 |
| “três por dois centímetros” | 3 x 2 cm |
| “cinquenta por cento” | 50% |
| “nível l quatro l cinco” | L4-L5 |
| “cinco de outubro de dois mil e vinte e três” | 05/10/2023 |
| “sete centímetros” | 7 cm |
Exemplo
verbatim (como o modelo transcreve)
análise dois pontos parágrafo nível l quatro l cinco dois pontos abaulamento discal medindo três vírgula cinco por dois centímetros vírgula sem compressão radicular ponto parágrafo observação dois pontos controle em seis meses pontoformatado (o que você renderiza)
Análise:
Nível L4-L5: abaulamento discal medindo 3,5 x 2 cm, sem compressão radicular.
Observação:
Controle em seis meses.formatado é estável entre versões.#Erros
Os erros chegam como código HTTP; quando há detalhe, ele vem no corpo em detail. O tratamento importa no ditado ao vivo, onde uma falha isolada não deve derrubar a sessão.
| Código | Significado |
|---|---|
| 400 | Campo arquivo ausente, áudio vazio ou em formato ilegível. |
| 401 | Token ausente, inválido ou vencido. Emita outro e repita a requisição. |
| 413 | Corpo acima de 2 MB. Divida o áudio em trechos menores. |
| 500 | Falha interna na inferência. Repita uma vez antes de mostrar erro ao usuário. |
| 503 | Modelo ainda carregando, normalmente logo após uma atualização do serviço. Aguarde e tente de novo. |
| 504 | A inferência passou de 10 segundos. Reenvie um trecho menor. |
Política de retentativa recomendada
Uma tentativa extra por motivo, nunca em cascata — é o comportamento adotado no cliente oficial da plataforma:
AbortController. Um cancelamento proposital não deve ser tratado como falha nem gerar retentativa.#Suporte
Dúvidas sobre a integração, liberação do seu domínio no CORS ou acesso ao serviço de ditado.
Suporte técnico
Fale com o time de engenharia da LeoRad. Inclua o domínio do seu front e, se possível, o horário e o código de erro observado.
contato@leorad.com.br