Speaker diarization con pyannote: separare voci nelle trascrizioni

Whisper mi dà un testo continuo, una sequenza di segmenti temporizzati che sa cosa è stato detto ma non sa chi l’ha detto. Per un webinar con un solo relatore è sufficiente, per una tavola rotonda con quattro o cinque relatori che si alternano diventa illeggibile. Per chiudere il cerchio uso pyannote, un toolkit Python open source che si occupa esattamente di questo: ascoltare un file audio e produrre una segmentazione “questo è il parlante A, qui parte il parlante B, qui torna A”.

Lo uso accoppiato a faster-whisper: la trascrizione viene da una parte, la diarizzazione dall’altra, poi unisco i due output combinando i timestamp. Il risultato è un transcript con etichette SPEAKER_00, SPEAKER_01 che rinomino a mano dopo, sostituendo con i nomi delle persone quando li conosco.

Setup con pyannote

pyannote si installa via pip, ma c’è un passaggio che inciampa molti la prima volta: il modello vero e proprio sta su Hugging Face dietro un gate, e va accettata la sua licenza dal browser prima che il token API funzioni. Una volta fatto, il resto fila.


sudo apt install -y ffmpeg python3-venv
python3 -m venv ~/.venvs/pyannote
source ~/.venvs/pyannote/bin/activate
pip install --upgrade pip
pip install pyannote.audio

Servono tre cose:

1. Un account Hugging Face e un token con scope read esportato come HF_TOKEN nelle env var della shell.

2. Accettare i termini d’uso del modello pyannote/speaker-diarization-community-1 sulla sua pagina Hugging Face.

3. Aver scaricato la cache pesi una prima volta (succede in automatico al primo run, restano in ~/.cache/huggingface/hub/).

Lo script di base che uso è poche righe. Lo lancio sul portatile, gira solo su CPU, non ho VRAM da spendere e per questi audio non mi serve.


import os
from pyannote.audio import Pipeline

pipeline = Pipeline.from_pretrained(
    "pyannote/speaker-diarization-community-1",
    use_auth_token=os.environ["HF_TOKEN"],
)

diarization = pipeline("webinar-panel.wav")

with open("webinar-panel.rttm", "w") as f:
    diarization.write_rttm(f)

for turn, _, speaker in diarization.itertracks(yield_label=True):
    print(f"[{turn.start:7.2f} -> {turn.end:7.2f}] {speaker}")

L’output nativo è in formato RTTM (Rich Transcription Time Marked), uno standard testuale che lista una riga per ogni turno di parola con start, durata e label del parlante. Comodo perché si confronta facilmente con la lista di segmenti che mi sputa fuori Whisper.

Per fonderle insieme uso uno script che per ogni segmento Whisper cerca il parlante pyannote che ha massima sovrapposizione temporale e gli appiccica l’etichetta. Una ventina di righe Python con un doppio for loop, niente di sofisticato.


def assign_speakers(whisper_segments, diarization):
    out = []
    for seg in whisper_segments:
        best_speaker = "UNKNOWN"
        best_overlap = 0
        for turn, _, speaker in diarization.itertracks(yield_label=True):
            overlap = max(0, min(seg.end, turn.end) - max(seg.start, turn.start))
            if overlap > best_overlap:
                best_overlap = overlap
                best_speaker = speaker
        out.append((seg.start, seg.end, best_speaker, seg.text.strip()))
    return out

Un esempio reale

Il mese scorso ho lavorato sulla registrazione di un webinar tecnico di 55 minuti su un cluster Postgres andato in split brain durante una rolling upgrade. Era una tavola rotonda con cinque relatori che si alternavano: chi raccontava il caso, chi entrava nei dettagli applicativi, chi moderava e poneva le domande. Tutti italiani, microfono decente, qualche interruzione qua e là ma audio nel complesso pulito.

Whisper small mi aveva tirato fuori la trascrizione in nove minuti, ma era un blocco unico di testo dove non si capiva mai chi stesse parlando: per seguire il filo dovevo riascoltare per memorizzare le voci, e poi tornare al testo. Con pyannote sopra la stessa registrazione, dopo altri 20 minuti di elaborazione, ho ottenuto la segmentazione in cinque parlanti che ho rinominato a mano dopo aver ascoltato 30 secondi per ciascuno per identificarli. Da lì in poi il transcript era leggibile come un copione: il moderatore che chiede “ma allora a che punto avete capito che era split brain?”, il relatore che risponde con il dettaglio tecnico, un altro che spiega quando i client avevano iniziato a scrivere su due primari diversi. I miei appunti si sono scritti da sé.

Cosa fa bene

Webinar e tavole rotonde con tre-cinque relatori in audio decente: la separazione è netta, gli scambi rapidi vengono catturati bene, i parlanti rimangono coerenti per tutta la durata (lo SPEAKER_00 di inizio è lo stesso SPEAKER_00 di fine). Funziona bene anche su voci miste maschili e femminili e su accenti italiani regionali diversi.

Cosa fa meno bene

Quando due persone parlano davvero sopra l’una all’altra per più di qualche secondo, il modello unisce o spezza male. Le risatine generiche del gruppo vengono attribuite a uno qualsiasi. Se qualcuno parla solo cinque secondi in tutto il webinar, spesso lo accorpa al vicino acustico. La memoria sale parecchio su file lunghi (ho visto picchi di 4 GB su una registrazione di un’ora), quindi sui file molto lunghi pre-spezzo con ffmpeg.

Privacy – vantaggio del modello locale

Le registrazioni di webinar tecnici sono materiale che preferisco non caricare da nessuna parte: possono contenere riferimenti a sistemi, configurazioni e dettagli che è meglio tenere in casa. Tutto deve restare sull’host. Con pyannote locale processo il file, salvo l’RTTM, cancello l’audio originale dalla cartella di lavoro quando ho finito, e nulla è mai passato per la rete oltre al download iniziale dei pesi.

I pesi del modello stanno in ~/.cache/huggingface/hub/models--pyannote--speaker-diarization-community-1/ insieme ai modelli embeddedati che la pipeline usa internamente. Cancellabili con rm -rf se voglio liberare spazio. Confronto con servizi cloud equivalenti: piattaforme tipo Otter.ai, Rev.ai o gli endpoint di diarizzazione di Google e Microsoft richiedono upload dell’audio e hanno policy di retention specifiche, qui non c’è nulla di tutto questo.

Licenza: pyannote.audio è MIT, sviluppato dal team pyannote (Hervé Bredin e collaboratori). Il modello speaker-diarization-community-1 è rilasciato con licenza permissiva e gate solo amministrativo per tracciare gli utilizzatori. Stack pulito, posso integrarlo dentro tooling interno senza vincoli copyleft.

In pratica

Pyannote sta nello stesso virtualenv di whisperx e si lancia con uno script che orchestra prima la trascrizione, poi la diarizzazione, poi la fusione. Per i webinar a più voci è il pezzo che trasforma un blob di testo in un transcript leggibile. Sui webinar a più relatori mi ha cambiato il modo di lavorare: anziché riascoltare per ricostruire chi ha detto cosa, leggo il testo già attribuito e mi concentro sui contenuti.


Immagine generata con ComfyUI Mac M1 / RealVisXL V5 Lightning.