Início → Blog → Voz para texto em Node.js: código pronto e as armadilhas de sempre
Voz para texto em Node.js: código pronto e as armadilhas de sempre
No Node.js a chamada de transcrição em si é trivial. O que derruba as pessoas é tudo em volta: handles de arquivo contra buffers, uploads chegando do navegador e um timeout padrão que mata gravações longas em silêncio.
Instalação e primeira requisição
O SDK oficial funciona sem alterações — só a URL base e a chave mudam:
npm install openai
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://voicesscribe.com/v1",
apiKey: process.env.VS_KEY,
});
const r = await client.audio.transcriptions.create({
model: "whisper-1",
file: fs.createReadStream("audio.mp3"),
});
console.log(r.text);
Repare no createReadStream em vez de readFileSync: o arquivo é enviado em fluxo para o servidor, sem ser carregado inteiro na memória antes. Num servidor atendendo várias requisições ao mesmo tempo, essa diferença é todo o perfil de memória.
Áudio que chega do navegador
O caso comum não é um arquivo em disco — é um upload. Com Express e multer, o buffer precisa ser embrulhado para que o SDK enxergue um arquivo com nome:
import express from "express";
import multer from "multer";
import { toFile } from "openai/uploads";
const upload = multer({ limits: { fileSize: 25 * 1024 * 1024 } });
const app = express();
app.post("/transcribe", upload.single("audio"), async (req, res) => {
try {
const file = await toFile(req.file.buffer, req.file.originalname);
const r = await client.audio.transcriptions.create({
model: "whisper-1", file,
});
res.json({ text: r.text });
} catch (e) {
res.status(502).json({ error: String(e) });
}
});
O limite fileSize do multer importa: sem ele, um navegador consegue empurrar um gigabyte para a memória do seu processo antes que qualquer coisa recuse. O limite da API é 25 MB, e espelhá-lo antes falha rápido e barato.
Gravar no navegador e enviar
O lado do navegador produz webm com o MediaRecorder, aceito como está — sem conversão:
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const rec = new MediaRecorder(stream);
const chunks = [];
rec.ondataavailable = (e) => chunks.push(e.data);
rec.onstop = async () => {
const blob = new Blob(chunks, { type: "audio/webm" });
const form = new FormData();
form.append("audio", blob, "voz.webm");
const res = await fetch("/transcribe", { method: "POST", body: form });
console.log((await res.json()).text);
};
rec.start();
setTimeout(() => rec.stop(), 5000);
Nunca coloque a chave de API no código do navegador. O upload vai para o seu próprio endpoint e o seu servidor guarda a chave — é o único arranjo seguro.
Timeouts e novas tentativas
O timeout padrão serve a trechos curtos e mata os longos em silêncio. Defina-o a partir da duração de áudio que você realmente processa e deixe o SDK repetir as falhas transitórias:
const client = new OpenAI({
baseURL: "https://voicesscribe.com/v1",
apiKey: process.env.VS_KEY,
timeout: 5 * 60 * 1000, // cinco minutos: suficiente para uma hora de áudio
maxRetries: 2, // 5xx e erros de conexão, tratados pelo SDK
});
Novas tentativas valem a pena em 5xx e erros de conexão. Um 400 em arquivo corrompido falhará igual todas as vezes, então repetir só desperdiça tempo.
Formatos e marcações de tempo
Legendas e tempos de segmento vêm da mesma chamada, com um parâmetro:
// arquivo de legenda pronto como string
const srt = await client.audio.transcriptions.create({
model: "whisper-1", file, response_format: "srt",
});
// segmentos com tempos
const detalhado = await client.audio.transcriptions.create({
model: "whisper-1", file, response_format: "verbose_json",
});
for (const s of detalhado.segments) {
console.log(s.start.toFixed(1), s.text.trim());
}Erros que valem tratamento explícito
| Código | Significado | O que fazer |
|---|---|---|
| 401 | Chave errada ou bloqueada | Confirme que a variável de ambiente chegou ao processo |
| 413 | Arquivo acima de 25 MB | Recodifique para mono 16 kHz e divida se preciso |
| 429 | Limite de taxa ou minutos esgotados | Recue exponencialmente; não repita na hora |
| 502 | Nó de reconhecimento indisponível | Repita depois de uma pausa |
Um text vazio numa resposta bem-sucedida não é erro — significa que não havia fala na gravação. Trate como resultado normal e não repita.
Teste com as suas próprias gravações. O cadastro leva um minuto e os minutos gratuitos bastam para avaliar a qualidade.
Obter uma chave de API grátisPerguntas frequentes
Preciso de um cliente Node.js específico?
Não, o pacote oficial openai funciona sem alterações. A API é compatível no protocolo, então só baseURL e apiKey diferem dos padrões da OpenAI.
Como passo um arquivo enviado pelo Express?
Embrulhe o buffer com toFile de openai/uploads para que o SDK veja um arquivo com nome, e passe-o no campo file.
Posso chamar a API direto do navegador?
Não — isso exporia a sua chave a quem abrir a página. Envie para o seu próprio endpoint e mantenha a chave no servidor.
O webm do MediaRecorder é aceito?
Sim, webm é aceito como está, junto com ogg, opus, mp3, wav e m4a. Nenhuma etapa de conversão é necessária.
Por que gravações longas falham por timeout?
O padrão do cliente é ajustado para requisições curtas. Aumente a opção timeout até o maior áudio que você processa; cinco minutos cobrem uma gravação de uma hora com folga.
Leitura relacionada
- Voz para texto em Python: de um arquivo a um script que funciona — Transcrição em Python funcionando em dez minutos: instalação, a primeira requisição, formatos de resposta, tratamento de erros e os erros que mais custam tempo.
- Transcrição de mensagens de voz: WhatsApp, Telegram e outros — Como transformar mensagens de voz em texto automaticamente: o formato ogg/opus, o problema das gravações curtas e exemplos de código para bots de atendimento.
- Como montar um pipeline de transcrição para um acervo de áudio — Transcrever milhares de gravações sem perder nenhuma: fila de trabalho, concorrência, novas tentativas, retomada após uma queda e controle do valor da fatura.