InícioBlog → Como montar um pipeline de transcrição para um acervo de áudio

Como montar um pipeline de transcrição para um acervo de áudio

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

Uma gravação é uma linha de código. Dez mil gravações são outro problema: alguma coisa vai falhar no meio, o processo vai ser morto, metade dos arquivos já estará pronta e você vai precisar saber qual metade. É este o formato de um pipeline em lote que sobrevive a tudo isso.

O que quebra ao ir de um arquivo para dez mil

Um laço sobre uma pasta funciona bem até o acervo crescer. Aí quatro coisas dão errado:

Cada ponto abaixo existe para resolver um desses quatro.

Passo 1. Torne o progresso durável

A decisão mais importante: o registro do que já foi feito precisa sobreviver ao processo. Um diretório de arquivos de resultado basta — a presença da saída é o estado.

import pathlib

SRC = pathlib.Path("gravacoes")
OUT = pathlib.Path("transcricoes")
OUT.mkdir(exist_ok=True)

def pendentes():
    for p in sorted(SRC.glob("*.mp3")):
        if not (OUT / (p.stem + ".txt")).exists():
            yield p

Agora o trabalho é idempotente: rode, mate, rode de novo — ele retoma exatamente de onde parou e nunca paga duas vezes pelo mesmo arquivo. Para acervos maiores, uma tabela SQLite com coluna de status funciona igual e ainda dá consultas; o princípio não muda.

Escreva a saída de forma atômica, ou um processo morto no meio da escrita deixa um arquivo truncado com cara de pronto:

tmp = out.with_suffix(".part")
tmp.write_text(texto, encoding="utf-8")
tmp.rename(out)          # atômico: ou não há arquivo novo, ou ele está completo

Passo 2. Processe vários arquivos ao mesmo tempo

Transcrição é espera de rede, não trabalho de CPU local, então threads são a ferramenta certa e um pool basta:

import os
from concurrent.futures import ThreadPoolExecutor
from openai import OpenAI

client = OpenAI(base_url="https://voicesscribe.com/v1", api_key=os.environ["VS_KEY"], max_retries=0)

def transcrever(path):
    with path.open("rb") as f:
        return client.audio.transcriptions.create(model="whisper-1", file=f).text

with ThreadPoolExecutor(max_workers=4) as pool:
    for path, texto in zip(pendentes(), pool.map(transcrever, pendentes())):
        (OUT / (path.stem + ".txt")).write_text(texto, encoding="utf-8")

Escolha max_workers pelo limite de requisições por segundo do seu plano, não pelo número de núcleos. Ir além do limite não acelera nada — apenas converte requisições bem-sucedidas em 429.

Passo 3. Repita as falhas, não os sucessos

Duas classes de erro pedem tratamentos opostos. Um 5xx ou um timeout valem uma nova tentativa; um 400 num arquivo corrompido vai falhar igual para sempre.

import time
from openai import APIStatusError, APIConnectionError

def com_retry(path, tentativas=3):
    for n in range(tentativas):
        try:
            return transcrever(path)
        except APIConnectionError:
            pass                                  # oscilação de rede — repetir
        except APIStatusError as e:
            if e.status_code == 429 or e.status_code >= 500:
                pass                              # limite ou erro do servidor — repetir
            else:
                raise                             # 400/401 — repetir não resolve
        time.sleep(2 ** n)                        # 1s, 2s, 4s
    raise RuntimeError(f"falhou após {tentativas}: {path}")

O recuo exponencial importa especialmente no 429: repetir imediatamente mantém você acima do limite. Note o max_retries=0 no cliente acima — sem isso o SDK também repete e as duas políticas brigam entre si.

Não deixe um arquivo ruim parar a execução. Registre, siga em frente e trate a lista de falhas no fim — normalmente são dois ou três arquivos em milhares.

Passo 4. Prepare o áudio antes de enviar

É aqui que a fatura é decidida de verdade. Duas regras fazem quase todo o trabalho:

# converte o acervo inteiro numa passada
find gravacoes -name '*.wav' -print0 |
  xargs -0 -P4 -I{} ffmpeg -loglevel error -i {} -ac 1 -ar 16000 -b:a 48k {}.mp3

Arquivos acima de 25 MB precisam ser divididos de qualquer jeito; com mono a 48 kbit/s esse limiar fica em torno de uma hora de áudio, então a maioria dos acervos deixa de precisar de divisão depois da conversão.

Passo 5. Saiba o custo antes de começar

A cobrança é por minuto de áudio, então o total é conhecível de antemão — meça o acervo em vez de chutar:

ffprobe -v error -show_entries format=duration -of csv=p=0 arquivo.mp3
import subprocess

def minutos(path):
    out = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
                          "format=duration", "-of", "csv=p=0", str(path)],
                         capture_output=True, text=True).stdout
    return float(out) / 60

total = sum(minutos(p) for p in SRC.glob("*.mp3"))
print(f"{total:.0f} min, {total * TARIFA:.2f} na tarifa atual")

Rode isso antes do pipeline, não depois. Leva um minuto, não custa nada e é a diferença entre um gasto planejado e uma surpresa.

Passo 6. Acompanhe a execução

Um lote longo precisa de três números visíveis: prontos, falhos e ritmo. Um contador impresso a cada cem arquivos já basta:

prontos = falhos = 0
for path in pendentes():
    try:
        salvar(path, com_retry(path))
        prontos += 1
    except Exception as e:
        falhos += 1
        print("FALHA", path.name, e)
    if (prontos + falhos) % 100 == 0:
        print(f"{prontos} prontos, {falhos} falhos")

A área do cliente mostra a mesma execução pelo outro lado — minutos consumidos, requisições e o histórico por requisição — que é a forma mais rápida de confirmar que o pipeline está fazendo o que você imagina.

O que não construir

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

Quantos arquivos posso enviar ao mesmo tempo?

A concorrência é limitada pelo teto de requisições por segundo do seu plano, não pelo serviço. Deixe o pool de threads um pouco abaixo desse teto; ir além só produz respostas 429.

O que acontece se o processo morrer no meio?

Nada se perde quando o progresso vem dos arquivos de saída: na execução seguinte o pipeline pula tudo que já tem transcrição e continua dali.

Pago de novo por um arquivo reenviado?

Uma requisição que retornou erro não consome minutos de áudio; uma transcrição bem-sucedida consome. Por isso pular arquivos já prontos importa: repetir trabalho bem-sucedido é o que custa dinheiro.

Devo converter o áudio antes de enviar?

Para um acervo, sim, e compensa duas vezes: mono a 16 kHz sobe muito mais rápido e os arquivos deixam de cruzar o limite de 25 MB, o que evita ter de dividi-los.

Como estimar o custo de um acervo grande?

Some as durações com ffprobe e multiplique pela sua tarifa por minuto. A cobrança é por minuto de áudio, então a estimativa feita antes é o número que você vai pagar.

Leitura relacionada