Transcrição ao vivo
Mande o áudio do microfone por WebSocket e receba o texto enquanto a pessoa fala: um rascunho a cada 600 ms e a frase fechada a cada pausa. R$ 0,11 por hora de áudio, cobrado por segundo.
3 min de leitura · atualizado em 13/10/2026wss://api.int8labs.com/v1/stt/streamComo funciona
- Abra o WebSocket com a credencial:
Authorization: Bearer int8_live_…no servidor, ou?token=int8_tmp_…no navegador. - Mande
{"type":"start"}com a taxa do áudio em até 10 s e espere oready. - Mande o áudio em binário, PCM16 LE mono, no ritmo da fala, em pedaços de até 1 s.
- Receba
partial(rascunho) efinal(frase fechada, com início e fim). - Mande
{"type":"stop"}no fim. O servidor fecha as frases que faltam, manda odonecom o custo e fecha.
Mensagens
"start"A primeira mensagem, em até 10 s: a taxa do áudio (8000, 16000, 24000, 44100 ou 48000), encoding sempre pcm_s16le e termos-chave opcionais.
{"type":"start","sample_rate":16000,"encoding":"pcm_s16le","keyterms":["Dra. Helena Prado"]}"ready"Pode mandar o áudio. O relógio da sessão começa aqui.
{"type":"ready"}áudiobinárioO som, no ritmo da fala. Pedaços de 100 a 250 ms dão a menor latência.
PCM16 LE mono, na taxa do start, até 1 s por mensagem
"partial"Rascunho da frase em andamento, a cada 600 ms. O próximo partial ou o final do mesmo segment substitui este.
{"type":"partial","segment":3,"text":"pode confirmar a consulta de"}"final"A frase fechada (depois de 700 ms de silêncio ou 15 s de fala), com o início e o fim em milissegundos, contados do primeiro áudio da sessão.
{"type":"final","segment":3,"text":"pode confirmar a consulta de terça às catorze e trinta","start_ms":12480,"end_ms":15920}"stop"Acabou o áudio. O servidor fecha as frases que faltam e manda o done.
{"type":"stop"}"done"Fim da sessão: áudio recebido, custo total e saldo. Depois dele a conexão fecha com 1000.
{"type":"done","audio_ms":63210,"cost_brl":0.00193142,"balance_brl":87.39806858}"error"Erro, com o mesmo code do REST. Os que fecham a conexão vêm seguidos do fechamento 4000 + o status HTTP.
{"type":"error","code":"insufficient_balance","message":"Saldo insuficiente. Recarregue em console › Faturamento.","request_id":"req_01JAA4M7Q2W8E5R3T6Y9U1I0PZ"}No navegador, com o microfone
O exemplo usa um token temporário (o seu servidor pede com a chave em POST /v1/tokens) e um AudioWorklet que entrega pedaços de 100 ms em PCM16. A taxa é a do aparelho; o servidor reamostra.
// No navegador, com o microfone. O token vem do seu backend (POST /v1/tokens com o escopo stt).const { token } = await (await fetch("/int8/token", { method: "POST" })).json();const microfone = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true } });const contexto = new AudioContext(); // a taxa do aparelho (44100 ou 48000); o servidor reamostraawait contexto.resume(); // chame dentro de um clique; sem gesto do usuário o navegador deixa o contexto suspensoconst ws = new WebSocket(`wss://api.int8labs.com/v1/stt/stream?token=${encodeURIComponent(token)}`);// Um AudioWorklet pequeno: junta 100 ms de som e entrega em PCM16 LE.const worklet = `class Pcm16 extends AudioWorkletProcessor {constructor() { super(); this.amostras = []; }process([entrada]) {for (const s of entrada[0] ?? []) this.amostras.push(Math.max(-1, Math.min(1, s)) * 0x7fff);if (this.amostras.length >= sampleRate / 10) {const pcm = Int16Array.from(this.amostras).buffer;this.port.postMessage(pcm, [pcm]);this.amostras = [];}return true;}}registerProcessor("pcm16", Pcm16);`;await contexto.audioWorklet.addModule(URL.createObjectURL(new Blob([worklet], { type: "text/javascript" })));const pcm16 = new AudioWorkletNode(contexto, "pcm16");ws.onopen = () => {ws.send(JSON.stringify({ type: "start", sample_rate: contexto.sampleRate, encoding: "pcm_s16le", keyterms: ["Dra. Helena Prado"] }));};ws.onmessage = ({ data }) => {const evento = JSON.parse(data);if (evento.type === "ready") {contexto.createMediaStreamSource(microfone).connect(pcm16);pcm16.port.onmessage = ({ data: pedaco }) => ws.readyState === WebSocket.OPEN && ws.send(pedaco);}if (evento.type === "partial") mostrarRascunho(evento.segment, evento.text);if (evento.type === "final") fixarFrase(evento.segment, evento.text, evento.start_ms, evento.end_ms);if (evento.type === "done") console.log(`${evento.audio_ms} ms de áudio, R$ ${evento.cost_brl}`);if (evento.type === "error") console.error(evento.code, evento.message);};// Para parar: ws.send(JSON.stringify({ type: "stop" })) e microfone.getTracks().forEach((t) => t.stop()).function mostrarRascunho(segmento, texto) {// troque o rascunho do segmento na tela}function fixarFrase(segmento, texto, inicioMs, fimMs) {// fixe a frase do segmento, em tinta}
Em Python, de um arquivo
Para testar com um arquivo WAV (PCM16 mono), mande no ritmo da fala, como faria um microfone. Para transcrever um arquivo gravado de verdade, use o lote: é mais rápido e sai pelo mesmo preço.
import asyncioimport jsonimport osimport waveimport websockets # pip install "websockets>=14"PEDACO_MS = 100async def transcrever(caminho: str) -> None:with wave.open(caminho, "rb") as wav:assert wav.getsampwidth() == 2 and wav.getnchannels() == 1, "precisa de PCM16 mono"taxa = wav.getframerate() # 8000, 16000, 24000, 44100 ou 48000amostras = wav.readframes(wav.getnframes())async with websockets.connect("wss://api.int8labs.com/v1/stt/stream",additional_headers={"Authorization": f"Bearer {os.environ['INT8_API_KEY']}"},) as ws:await ws.send(json.dumps({"type": "start", "sample_rate": taxa, "encoding": "pcm_s16le"}))assert json.loads(await ws.recv())["type"] == "ready"async def enviar() -> None:passo = taxa * PEDACO_MS // 1000 * 2 # bytes de 100 msfor inicio in range(0, len(amostras), passo):await ws.send(amostras[inicio : inicio + passo])await asyncio.sleep(PEDACO_MS / 1000) # no ritmo da fala; mais rápido fecha com 4429await ws.send(json.dumps({"type": "stop"}))envio = asyncio.create_task(enviar())async for mensagem in ws:evento = json.loads(mensagem)if evento["type"] == "final":print(f'[{evento["start_ms"]} ms] {evento["text"]}')elif evento["type"] == "done":print("custo R$", evento["cost_brl"])elif evento["type"] == "error":print("erro:", evento["code"], evento["message"])await envioasyncio.run(transcrever("reuniao.wav"))
Regras da sessão
- Todo erro fecha a conexão (4000 + o status HTTP), depois de cobrar o áudio que já entrou.
- O áudio não pode passar do relógio da sessão em mais de 10 s: quem manda um arquivo gravado de uma vez recebe
rate_limitede o fechamento 4429. Arquivo gravado vai pelo lote. - 10 s sem áudio encerram a sessão com o
done. A sessão dura até 4 horas. - O saldo e a credencial são conferidos no começo e a cada 60 s: saldo zerado fecha com 4402; chave revogada ou token vencido, com 4401. Antes do
errorchegam osfinaldo áudio já recebido (e cobrado), semdone; o áudio enviado depois disso não é recebido nem cobrado. - Com 4 frases esperando o transcritor, o servidor para de ler até a fila andar (o TCP segura o cliente). Essa espera não conta como silêncio.
Dicas de latência
- Pedaços de 100 a 250 ms. Pedaços maiores atrasam o primeiro rascunho; menores só gastam rede.
- Mande na taxa nativa do microfone (44100 ou 48000 Hz). Reamostrar no navegador não deixa nada mais rápido.
- Ligue o cancelamento de eco (
echoCancellation) quando a pessoa usa alto-falante. - Use
keytermspara nomes próprios, remédios e marcas: até 100 termos de até 50 caracteres. - Mostre o
partialem cinza e troque pelofinalem preto: o rascunho muda até a frase fechar. - O texto sai em minúsculas, sem pontuação e com os números por extenso.
Saldo e cobrança
O áudio recebido é cobrado por segundo, a cada frase fechada e no done. O saldo e a credencial são conferidos no começo e a cada minuto.
Quando o saldo zera (4402), ou quando a chave é revogada ou o token vence no meio da sessão (4401), o servidor para de receber o áudio. Ele fecha a frase em andamento e manda os final que faltam, do áudio já recebido e cobrado. Só então manda o error (insufficient_balance, api_key_revoked ou api_key_expired) e fecha com 4402 ou 4401, sem done. O áudio enviado depois disso não é recebido nem cobrado.
Com um token que vence, peça outro ao seu servidor depois do fechamento 4401 e abra uma sessão nova.