Pular para o conteúdo
int8labs

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/2026
WSwss://api.int8labs.com/v1/stt/stream

Como funciona

  1. Abra o WebSocket com a credencial: Authorization: Bearer int8_live_… no servidor, ou ?token=int8_tmp_… no navegador.
  2. Mande {"type":"start"} com a taxa do áudio em até 10 s e espere o ready.
  3. Mande o áudio em binário, PCM16 LE mono, no ritmo da fala, em pedaços de até 1 s.
  4. Receba partial (rascunho) e final (frase fechada, com início e fim).
  5. Mande {"type":"stop"} no fim. O servidor fecha as frases que faltam, manda o done com o custo e fecha.

Mensagens

VOCÊ →"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"]}
← SERVIDOR"ready"

Pode mandar o áudio. O relógio da sessão começa aqui.

{"type":"ready"}
VOCÊ →áudiobinário

O 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
← SERVIDOR"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"}
← SERVIDOR"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}
VOCÊ →"stop"

Acabou o áudio. O servidor fecha as frases que faltam e manda o done.

{"type":"stop"}
← SERVIDOR"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}
← SERVIDOR"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 reamostra
await contexto.resume(); // chame dentro de um clique; sem gesto do usuário o navegador deixa o contexto suspenso
const 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 asyncio
import json
import os
import wave
​
import websockets # pip install "websockets>=14"
​
PEDACO_MS = 100
​
​
async 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 48000
amostras = 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 ms
for 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 4429
await 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 envio
​
​
asyncio.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_limited e 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 error chegam os final do áudio já recebido (e cobrado), sem done; 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 keyterms para nomes próprios, remédios e marcas: até 100 termos de até 50 caracteres.
  • Mostre o partial em cinza e troque pelo final em 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.