Diarização de áudio local em GPU AMD
O guia de transcrição de áudio resolve metade do problema de tirar sentido de uma gravação: ele responde o que foi dito. A outra metade é quem disse o quê, e é aí que entra a diarização. Uma reunião de quatro pessoas e um monólogo produzem, do ponto de vista do Whisper, o mesmo tipo de saída, uma sequência de frases com marcações de tempo, sem qualquer noção de locutor. Quem já transcreveu uma entrevista conhece o resultado: um paredão de texto em que a pergunta e a resposta se fundem. A diarização é o passo que segmenta esse áudio por voz e rotula cada trecho com um locutor, transformando o paredão em um diálogo atribuído.
Vale fixar o que a diarização faz e o que ela não faz. Ela descobre quantas vozes distintas existem e quando cada uma fala, atribuindo rótulos anônimos como SPEAKER_00 e SPEAKER_01, e mantendo cada rótulo consistente ao longo da gravação. O que ela não faz é saber que SPEAKER_00 é a Ana. Colocar nomes reais é um passo posterior, feito à mão ou por um reconhecimento de locutor que compare as vozes contra amostras conhecidas. A diarização é o caso não-supervisionado: você entrega o áudio e ela descobre a estrutura sozinha.
Por que a diarização recoloca o ROCm em cena
Section titled “Por que a diarização recoloca o ROCm em cena”Aqui aparece a primeira tensão, e ela é consequência direta de uma decisão tomada no guia de transcrição. Lá, a escolha do whisper.cpp com backend Vulkan foi deliberada, justamente para fugir do ROCm: Vulkan fala com o driver Mesa que já vem instalado, não exige casar versões de ROCm com PyTorch, e não depende de a placa estar na lista de GPUs oficialmente suportadas. É o caminho de menor atrito numa Radeon.
A diarização quebra esse arranjo. A ferramenta de referência para o problema, o pyannote.audio, é construída sobre PyTorch, e numa GPU AMD PyTorch significa ROCm. Não há um backend Vulkan de pyannote como há de whisper.cpp. Então a diarização recoloca em cena exatamente a stack que a transcrição tinha contornado. A boa notícia é que esse retorno é menos doloroso do que parece, por duas razões. A primeira é que, como observado no próprio guia de transcrição, é comum já existir uma stack ROCm instalada na máquina a serviço de outro programa (o Ollama com aceleração AMD, por exemplo); se for o caso, o terreno já está preparado. A segunda é que o modelo de diarização é pequeno, na casa de algumas centenas de megabytes, então o custo de VRAM é modesto e a fragilidade de versão do ROCm pesa menos do que pesaria com um modelo grande.
Por que o WhisperX não é o atalho que parece
Section titled “Por que o WhisperX não é o atalho que parece”A pesquisa por “diarização local” leva quase sempre ao WhisperX, e a recomendação faz sentido na maioria das máquinas: ele empacota Whisper, pyannote e alinhamento em nível de palavra numa biblioteca só, e entrega a transcrição já diarizada. O problema é que o backend de transcrição do WhisperX é o faster-whisper, cujo motor é o CTranslate2, e o CTranslate2 não tem suporte a ROCm. Numa Radeon, a transcrição do WhisperX cai para a CPU sem avisar, o mesmo sintoma silencioso que o guia de transcrição descreve na armadilha do build CPU-only.
O efeito prático é perverso: adotar o WhisperX numa máquina AMD jogaria fora justamente o trabalho de compilar o whisper.cpp com Vulkan, trocando uma transcrição que roda a vinte e poucas vezes o tempo real na GPU por uma que rasteja na CPU, para ganhar em troca só a comodidade de o pyannote já vir acoplado. É um mau negócio nesse hardware. O WhisperX brilha em máquinas NVIDIA, onde o CTranslate2 tem CUDA e todo o pipeline roda na placa; numa Radeon, o caminho sensato é outro.
Esse caminho é tratar transcrição e diarização como dois processos independentes, que é o que eles de fato são. O whisper.cpp por Vulkan continua fazendo a transcrição rápida que já foi montada. O pyannote, por ROCm, faz só a diarização, produzindo os turnos de fala. No fim, as duas saídas são casadas por tempo. Perde-se a comodidade de uma ferramenta única e ganha-se o melhor de cada backend na placa: Vulkan para o trabalho pesado da transcrição, ROCm apenas para o modelo pequeno da diarização.
O modelo: pyannote community-1
Section titled “O modelo: pyannote community-1”O pyannote 3.1 foi por muito tempo o padrão-ouro aberto, mas a referência atual é o pyannote/speaker-diarization-community-1, que o substituiu e o bate de forma consistente nos benchmarks. No AMI ele marca 17,0% de diarization error rate contra 18,8% do 3.1, com ganhos também na contagem de locutores e, o que mais importa para o uso aqui, numa reconciliação mais simples dos tempos com a transcrição. Ele é licenciado como CC-BY-4.0 e, segundo a página do modelo, permanecerá gratuito. Como todo modelo do pyannote, o download inicial exige um token do Hugging Face e a aceitação dos termos na página do modelo.
Existem alternativas, mas nenhuma desloca o pyannote nesse contexto. A NeMo da NVIDIA, com a arquitetura Sortformer que trata a diarização como um problema único de ponta a ponta em vez de um pipeline de etapas, é forte, mas é otimizada para CUDA e não é o que se quer numa AMD. E há opções voltadas a CPU, como o Falcon da Picovoice e a biblioteca diarize, que prometem rodar com uma fração da memória e várias vezes mais rápido que o pyannote sem GPU; elas fazem sentido numa máquina sem placa, mas com uma Radeon disponível o pyannote sobre ROCm aproveita melhor o hardware.
Instalando o pyannote sobre ROCm
Section titled “Instalando o pyannote sobre ROCm”O que carrega o peso da compatibilidade não é o pyannote, é o PyTorch compilado para ROCm; o resto herda o suporte de GPU dele. A instalação é o PyTorch do índice ROCm seguido do pyannote:
pip install torch --index-url https://download.pytorch.org/whl/rocm6.2pip install pyannote.audioexport HSA_OVERRIDE_GFX_VERSION=11.0.1 # RDNA3 / RX 7000; use 10.3.0 para RDNA2 / RX 6000export HF_TOKEN="seu_token_do_hugging_face"O HSA_OVERRIDE_GFX_VERSION é o mesmo tipo de ajuste que aparece em qualquer uso de ROCm numa placa de consumo: ele faz o runtime aceitar a GPU quando a versão de GFX dela não está na lista oficial. Para a RX 7900 GRE do guia de transcrição, uma RDNA3, o valor é 11.0.1. Recomenda-se ROCm 6.x; o índice do PyTorch acompanha a versão instalada.
Há um detalhe que confunde na primeira vez e vale registrar: o PyTorch compilado para ROCm continua expondo a GPU sob o nome cuda. Não é engano nem sobra de código NVIDIA, é uma decisão de compatibilidade da AMD para que o mesmo código Python rode nos dois mundos sem alteração. Então mandar o pipeline para torch.device("cuda") numa Radeon é o certo, por mais estranho que a linha pareça.
Rodando a diarização
Section titled “Rodando a diarização”Com o token aceito e o PyTorch enxergando a placa, a diarização é uma invocação do pipeline do pyannote sobre o mesmo WAV de 16 kHz mono que o guia de transcrição já ensina a extrair com o ffmpeg. Reaproveitar esse WAV evita reprocessar o áudio e garante que os tempos das duas saídas se refiram exatamente à mesma linha temporal:
import torchfrom pyannote.audio import Pipeline
pipeline = Pipeline.from_pretrained( "pyannote/speaker-diarization-community-1", use_auth_token=True, # lê o HF_TOKEN do ambiente)pipeline.to(torch.device("cuda")) # numa Radeon, "cuda" é a própria GPU AMD via ROCm
diarizacao = pipeline("aula.wav")
for turno, _, locutor in diarizacao.itertracks(yield_label=True): print(f"{turno.start:7.2f} {turno.end:7.2f} {locutor}")A saída é uma lista de turnos, cada um com início, fim e um rótulo de locutor. Ligar a diarização a um fluxo que antes só transcrevia acrescenta um custo de tempo da ordem de 40% a 60% sobre a transcrição pura, o que é esperado: é um segundo modelo passando pelo áudio inteiro. O consumo de VRAM, porém, é baixo, porque o modelo é pequeno e, como o Whisper, o pyannote processa o som em janelas, não de uma vez.
Casando transcrição e diarização
Section titled “Casando transcrição e diarização”As duas saídas existem em paralelo e falam da mesma linha do tempo, mas ainda não se conhecem: o whisper.cpp tem o que foi dito e quando, o pyannote tem quem falou e quando. Costurá-las é uma questão de sobreposição temporal. Para cada segmento de texto da transcrição, procura-se o turno de fala do pyannote que mais se sobrepõe a ele no tempo, e o locutor daquele turno é atribuído ao segmento.
Para isso o whisper.cpp precisa entregar os tempos de cada segmento, o que se consegue pedindo a saída em JSON na hora de transcrever, com o flag -oj já mencionado no guia de transcrição. O JSON traz, para cada segmento, o texto e os deslocamentos de início e fim em milissegundos. Com isso, o casamento é um script curto:
import json
def carrega_segmentos_whisper(caminho_json): dados = json.loads(open(caminho_json).read()) for seg in dados["transcription"]: # offsets vêm em milissegundos; converte para segundos yield seg["offsets"]["from"] / 1000, seg["offsets"]["to"] / 1000, seg["text"].strip()
def sobreposicao(a_ini, a_fim, b_ini, b_fim): return max(0, min(a_fim, b_fim) - max(a_ini, b_ini))
turnos = [(t.start, t.end, loc) for t, _, loc in diarizacao.itertracks(yield_label=True)]
for ini, fim, texto in carrega_segmentos_whisper("aula.json"): # o locutor do segmento é o do turno com maior sobreposição locutor = max( turnos, key=lambda t: sobreposicao(ini, fim, t[0], t[1]), default=(0, 0, "?"), )[2] print(f"[{ini:7.2f} → {fim:7.2f}] {locutor}: {texto}")O resultado é a transcrição diarizada que se queria desde o início:
[ 4.10 → 9.30] SPEAKER_00: então, sobre o deploy de ontem...[ 9.30 → 15.80] SPEAKER_01: pois é, o pipeline quebrou no passo do buildA escolha pelo turno de maior sobreposição é uma heurística simples e resolve a esmagadora maioria dos casos. Onde ela erra é nas fronteiras, quando um segmento do Whisper cai a cavaleiro entre dois turnos, ou em fala sobreposta, quando duas pessoas falam ao mesmo tempo e o modelo teria de dividir o mesmo intervalo entre dois locutores. Para trabalho fino há como cortar o texto na fronteira do turno, mas para uma reunião ou uma aula comum a atribuição por segmento inteiro basta.
O que esperar da qualidade
Section titled “O que esperar da qualidade”A diarização é boa, não perfeita, e conhecer seus limites evita frustração. Fala sobreposta é o caso mais difícil, e é justamente o mais comum em conversas reais animadas. A contagem de locutores também pode escorregar quando duas pessoas têm timbre parecido ou quando alguém fala muito pouco, aparecendo como um locutor a mais ou a menos. E os rótulos são sempre anônimos e podem trocar de identidade se a gravação for longa e a voz de alguém variar bastante. Nenhuma dessas limitações é específica da AMD; são próprias do estado da arte da diarização. O que o caminho aqui garante é aproveitar a placa nos dois passos sem abrir mão da transcrição rápida por Vulkan que já estava montada.
Palavras-chave
Section titled “Palavras-chave”Ferramentas: pyannote.audio, PyTorch, whisper.cpp, WhisperX, faster-whisper, CTranslate2, ffmpeg, Hugging Face Conceitos: diarização, speaker diarization, quem falou quando, DER, reconhecimento de locutor, sobreposição temporal, fala sobreposta, VAD, embeddings de locutor Hardware: GPU AMD, Radeon, RX 7900 GRE, RDNA3, RDNA2, ROCm, Vulkan, CUDA, VRAM Modelos: pyannote community-1, pyannote 3.1, NeMo, Sortformer, Falcon, Whisper Comandos: HSA_OVERRIDE_GFX_VERSION, HF_TOKEN, torch.device cuda, whisper-cli -oj Formatos: WAV, 16 kHz mono, JSON, RTTM, SRT