InícioBlog → 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

Publicado em 2026-08-18 · 6 min de leitura

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ódigoSignificadoO que fazer
401Chave errada ou bloqueadaConfirme que a variável de ambiente chegou ao processo
413Arquivo acima de 25 MBRecodifique para mono 16 kHz e divida se preciso
429Limite de taxa ou minutos esgotadosRecue exponencialmente; não repita na hora
502Nó de reconhecimento indisponívelRepita 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átis

Perguntas 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