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 roda | Servidores da OpenAI | Na sua máquina |
| Custo | Por minuto de áudio | Grátis (paga em tempo de CPU/GPU) |
| Privacidade | O arquivo sai da sua máquina | Nada sai da sua máquina |
| Limite por arquivo | 25 MB | Nenhum |
| Timestamps (para legenda) | Não, devolve só o texto | Sim, trecho a trecho |
| Termos técnicos | Aceita uma lista de palavras-chave | Aceita 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.mp3O que cada opção faz:
-vndescarta o vídeo;-ac 1junta os canais em mono (voz não precisa de estéreo);-ar 16000reduz a taxa de amostragem para 16 kHz, a mesma que o Whisper usa internamente;-b:a 32kcomprime 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 openaiA transcrição em si são poucas linhas:
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:
languagesdiz 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:
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 arquivosA 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.
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-whisperEle devolve a transcrição em trechos (segments), cada um com início, fim e texto. Isso é praticamente um arquivo .srt pronto:
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-whisperextrai 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=Truepula 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 dotranscribe.
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
keywordsresolve 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.
