Voltar para o blog
Engenharia

Transcrevendo áudio e vídeo com Python: do arquivo à legenda

Transcreva gravações com a API da OpenAI ou com o Whisper local, divida áudios longos nos silêncios e gere legendas .srt.

PythonOpenAIWhisper

Reunião gravada, aula, entrevista, vídeo do canal: muito conhecimento fica preso em áudio.

Enquanto está lá, não dá para buscar, citar, resumir nem legendar.

Neste tutorial, vamos transformar áudio e vídeo em texto com Python, por dois caminhos:

  • pela API da OpenAI, com o modelo gpt-transcribe, quando o que importa é o texto;
  • localmente, com o Whisper, quando você quer uma legenda com tempo (.srt) ou não pode mandar o arquivo para fora.

No caminho, vamos resolver os dois problemas que todo mundo encontra na primeira tentativa: o vídeo é grande demais e a gravação é longa demais.


Dois caminhos, dois perfis

Antes do código, vale entender o que cada caminho entrega.

API (gpt-transcribe)Local (faster-whisper)
Onde rodaServidores da OpenAINa sua máquina
CustoPor minuto de áudioGrátis (paga em tempo de CPU/GPU)
PrivacidadeO arquivo sai da sua máquinaNada sai da sua máquina
Limite por arquivo25 MBNenhum
Timestamps (para legenda)Não, devolve só o textoSim, trecho a trecho
Termos técnicosAceita uma lista de palavras-chaveAceita um texto de contexto

A regra prática que usamos aqui:

  • Quer o texto para ler, buscar ou passar para um LLM? Comece pela API.
  • Quer legenda, tem material sensível ou vai transcrever muitas horas? Rode local.

Os dois caminhos começam no mesmo lugar: um bom arquivo de áudio.

Do vídeo ao áudio com ffmpeg

Um vídeo de uma hora facilmente passa de 1 GB. Quase tudo isso é imagem, que não ajuda em nada na transcrição.

O ffmpeg extrai só o áudio e já deixa no formato que os modelos de fala preferem:

ffmpeg -i reuniao.mp4 -vn -ac 1 -ar 16000 -b:a 32k reuniao.mp3

O que cada opção faz:

  • -vn descarta o vídeo;
  • -ac 1 junta os canais em mono (voz não precisa de estéreo);
  • -ar 16000 reduz a taxa de amostragem para 16 kHz, a mesma que o Whisper usa internamente;
  • -b:a 32k comprime a 32 kbps, o suficiente para voz.

Com essas opções, uma hora de áudio fica em torno de 14 MB. Ou seja, gravações de até uma hora e meia já cabem no limite de 25 MB da API sem precisar dividir nada.

Caminho 1: transcrição pela API

Instale o SDK e defina sua chave na variável OPENAI_API_KEY:

pip install --upgrade openai

A transcrição em si são poucas linhas:

transcrever_api.py
from openai import OpenAI
 
client = OpenAI()
 
with open("reuniao.mp3", "rb") as audio:
    resultado = client.audio.transcriptions.create(
        model="gpt-transcribe",
        file=audio,
        languages=["pt"],
        keywords=["LangGraph", "FastAPI", "Salada de Dados"],
    )
 
print(resultado.text)

Dois parâmetros fazem diferença na prática:

  • languages diz em que idioma(s) está o áudio, no formato ISO-639-1 (pt, en). Ajuda quando a gravação mistura português com termos em inglês.
  • keywords é a lista de palavras que o modelo deve reconhecer: nomes de produtos, siglas, jargão da sua área. É o jeito mais simples de evitar um "Lang Graf" no meio do texto.

Repare no open(..., "rb"): a API espera o arquivo aberto em modo binário, não o caminho como texto.

Os modelos de transcrição mudam rápido. O whisper-1, que muitos tutoriais ainda usam, vai ser desligado em fevereiro de 2027. Antes de colocar em produção, confira os modelos atuais na documentação de speech-to-text.

Gravações longas: dividir sem cortar palavras

Passou de 25 MB mesmo comprimido? Aí é preciso dividir o áudio em pedaços.

O jeito ingênuo é cortar a cada X minutos. O problema é que o corte cai no meio de uma palavra, e o modelo erra justamente nas bordas.

O jeito melhor é cortar nos silêncios. O próprio ffmpeg sabe encontrá-los, com o filtro silencedetect:

dividir.py
import re
import subprocess
from pathlib import Path
 
 
def duracao(caminho: str) -> float:
    saida = subprocess.run(
        ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", caminho],
        capture_output=True, text=True, check=True,
    )
    return float(saida.stdout)
 
 
def silencios(caminho: str) -> list[float]:
    """Devolve o ponto médio de cada silêncio do áudio, em segundos."""
    saida = subprocess.run(
        ["ffmpeg", "-hide_banner", "-i", caminho, "-af", "silencedetect=noise=-35dB:d=0.5", "-f", "null", "-"],
        capture_output=True, text=True,
    )
    pares = re.findall(r"silence_end: ([\d.]+) \| silence_duration: ([\d.]+)", saida.stderr)
    return [float(fim) - float(dur) / 2 for fim, dur in pares]
 
 
def pontos_de_corte(caminho: str, alvo: float = 20 * 60) -> list[float]:
    """Escolhe cortes a cada ~20 minutos, sempre no silêncio mais próximo antes do alvo."""
    total, meios = duracao(caminho), silencios(caminho)
    pontos = [0.0]
    # A folga de 25% evita um último pedaço de poucos segundos.
    while total - pontos[-1] > alvo * 1.25:
        inicio = pontos[-1]
        candidatos = [s for s in meios if inicio + alvo / 2 < s <= inicio + alvo]
        pontos.append(max(candidatos) if candidatos else inicio + alvo)
    return pontos + [total]
 
 
def dividir(caminho: str, pasta: str = "pedacos") -> list[Path]:
    destino = Path(pasta)
    destino.mkdir(exist_ok=True)
    pontos = pontos_de_corte(caminho)
    arquivos = []
    for i, (inicio, fim) in enumerate(zip(pontos, pontos[1:])):
        arquivo = destino / f"{i:03d}.mp3"
        subprocess.run(
            ["ffmpeg", "-y", "-loglevel", "error", "-i", caminho,
             "-ss", str(inicio), "-to", str(fim), "-c", "copy", str(arquivo)],
            check=True,
        )
        arquivos.append(arquivo)
    return arquivos

A lógica de pontos_de_corte: a cada passo, procura o último silêncio entre a metade e o fim da janela de 20 minutos. Se a gravação não tiver nenhuma pausa nesse intervalo (raro, mas acontece), corta no alvo mesmo.

Para transcrever, mandamos os pedaços em ordem. O detalhe que faz diferença é o parâmetro prompt: passamos o final do texto anterior, para o modelo manter o contexto, a grafia dos nomes e o estilo de pontuação de um pedaço para o outro.

transcrever_longo.py
from openai import OpenAI
 
from dividir import dividir
 
client = OpenAI()
 
 
def transcrever_longo(caminho: str) -> str:
    textos: list[str] = []
    for pedaco in dividir(caminho):
        extras = {"prompt": textos[-1][-500:]} if textos else {}
        with open(pedaco, "rb") as audio:
            resultado = client.audio.transcriptions.create(
                model="gpt-transcribe",
                file=audio,
                languages=["pt"],
                **extras,
            )
        textos.append(resultado.text)
    return "\n\n".join(textos)
 
 
print(transcrever_longo("aula-completa.mp3"))

Caminho 2: legenda com Whisper local

Para legendar, precisamos saber quando cada frase é dita. É aqui que o caminho local brilha.

O faster-whisper é uma reimplementação do Whisper bem mais rápida e leve que a original, e roda bem até em CPU:

pip install faster-whisper

Ele devolve a transcrição em trechos (segments), cada um com início, fim e texto. Isso é praticamente um arquivo .srt pronto:

legendar.py
from faster_whisper import WhisperModel
 
 
def tempo_srt(segundos: float) -> str:
    ms = round(segundos * 1000)
    h, ms = divmod(ms, 3_600_000)
    m, ms = divmod(ms, 60_000)
    s, ms = divmod(ms, 1000)
    return f"{h:02}:{m:02}:{s:02},{ms:03}"
 
 
modelo = WhisperModel("turbo", device="cpu", compute_type="int8")
trechos, info = modelo.transcribe("reuniao.mp4", language="pt", vad_filter=True)
 
with open("reuniao.srt", "w", encoding="utf-8") as srt:
    for i, trecho in enumerate(trechos, start=1):
        srt.write(f"{i}\n{tempo_srt(trecho.start)} --> {tempo_srt(trecho.end)}\n{trecho.text.strip()}\n\n")

Algumas observações:

  • Ele aceita o vídeo direto. O faster-whisper extrai o áudio sozinho, então o passo do ffmpeg é opcional aqui.
  • Não há limite de tamanho, nem é preciso dividir nada: ele processa a gravação inteira, trecho a trecho.
  • vad_filter=True pula os silêncios. Isso acelera a transcrição e evita um defeito conhecido do Whisper, que é "inventar" frases em trechos sem fala.
  • trechos é um gerador: a transcrição acontece enquanto o loop roda, não na linha do transcribe.

Sobre o modelo:

  • "turbo" é um bom equilíbrio entre qualidade e velocidade.
  • Numa máquina mais modesta, "small" é bem mais rápido, com alguma perda de qualidade.
  • Com uma GPU NVIDIA, troque para device="cuda", compute_type="float16", e a transcrição fica muitas vezes mais rápida.

O .srt gerado pode ser enviado como legenda no YouTube Studio, e a maioria dos players de vídeo também o lê.

Qual escolher?

Na dúvida, siga o que você precisa no final:

  • Texto para ler, buscar ou resumir: API. É o caminho com menos atrito, e o keywords resolve bem os termos técnicos.
  • Legenda: local. A API atual não devolve tempo.
  • Áudio sensível (cliente, saúde, jurídico): local. Nada sai da sua máquina.
  • Volume grande e recorrente: faça as contas. Compare o preço por minuto da API com o custo de manter uma máquina com GPU rodando o Whisper.

E depois da transcrição?

Texto transcrito é o começo, não o fim.

Com a transcrição em mãos, um LLM consegue gerar o resumo de uma reunião, os capítulos de um vídeo do YouTube ou uma lista de decisões e próximos passos. Esse é o assunto de um próximo artigo.