{"id":82,"date":"2026-06-21T21:15:00","date_gmt":"2026-06-21T21:15:00","guid":{"rendered":"https:\/\/rpi.temporiti.net\/wordpress\/?p=82"},"modified":"2026-06-22T10:05:04","modified_gmt":"2026-06-22T08:05:04","slug":"pyannote-diarization","status":"publish","type":"post","link":"https:\/\/rpi.temporiti.net\/wordpress\/?p=82","title":{"rendered":"Speaker diarization con pyannote: separare voci nelle trascrizioni"},"content":{"rendered":"<p>Whisper mi d\u00e0 un testo continuo, una sequenza di segmenti temporizzati che sa cosa \u00e8 stato detto ma non sa chi l&#8217;ha detto. Per un webinar con un solo relatore \u00e8 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 &#8220;questo \u00e8 il parlante A, qui parte il parlante B, qui torna A&#8221;.<\/p>\n<p>Lo uso accoppiato a faster-whisper: la trascrizione viene da una parte, la diarizzazione dall&#8217;altra, poi unisco i due output combinando i timestamp. Il risultato \u00e8 un transcript con etichette <code>SPEAKER_00<\/code>, <code>SPEAKER_01<\/code> che rinomino a mano dopo, sostituendo con i nomi delle persone quando li conosco.<\/p>\n<h2>Setup con pyannote<\/h2>\n<p>pyannote si installa via pip, ma c&#8217;\u00e8 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.<\/p>\n<pre><code class=\"language-bash\">\nsudo apt install -y ffmpeg python3-venv\npython3 -m venv ~\/.venvs\/pyannote\nsource ~\/.venvs\/pyannote\/bin\/activate\npip install --upgrade pip\npip install pyannote.audio\n<\/code><\/pre>\n<p>Servono tre cose:<\/p>\n<p>1. Un account Hugging Face e un token con scope <code>read<\/code> esportato come <code>HF_TOKEN<\/code> nelle env var della shell.<\/p>\n<p>2. Accettare i termini d&#8217;uso del modello <code>pyannote\/speaker-diarization-community-1<\/code> sulla sua pagina Hugging Face.<\/p>\n<p>3. Aver scaricato la cache pesi una prima volta (succede in automatico al primo run, restano in <code>~\/.cache\/huggingface\/hub\/<\/code>).<\/p>\n<p>Lo script di base che uso \u00e8 poche righe. Lo lancio sul portatile, gira solo su CPU, non ho VRAM da spendere e per questi audio non mi serve.<\/p>\n<pre><code class=\"language-python\">\nimport os\nfrom pyannote.audio import Pipeline\n\npipeline = Pipeline.from_pretrained(\n    \"pyannote\/speaker-diarization-community-1\",\n    use_auth_token=os.environ[\"HF_TOKEN\"],\n)\n\ndiarization = pipeline(\"webinar-panel.wav\")\n\nwith open(\"webinar-panel.rttm\", \"w\") as f:\n    diarization.write_rttm(f)\n\nfor turn, _, speaker in diarization.itertracks(yield_label=True):\n    print(f\"[{turn.start:7.2f} -&gt; {turn.end:7.2f}] {speaker}\")\n<\/code><\/pre>\n<p>L&#8217;output nativo \u00e8 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\u00e9 si confronta facilmente con la lista di segmenti che mi sputa fuori Whisper.<\/p>\n<p>Per fonderle insieme uso uno script che per ogni segmento Whisper cerca il parlante pyannote che ha massima sovrapposizione temporale e gli appiccica l&#8217;etichetta. Una ventina di righe Python con un doppio for loop, niente di sofisticato.<\/p>\n<pre><code class=\"language-python\">\ndef assign_speakers(whisper_segments, diarization):\n    out = []\n    for seg in whisper_segments:\n        best_speaker = \"UNKNOWN\"\n        best_overlap = 0\n        for turn, _, speaker in diarization.itertracks(yield_label=True):\n            overlap = max(0, min(seg.end, turn.end) - max(seg.start, turn.start))\n            if overlap &gt; best_overlap:\n                best_overlap = overlap\n                best_speaker = speaker\n        out.append((seg.start, seg.end, best_speaker, seg.text.strip()))\n    return out\n<\/code><\/pre>\n<h2>Un esempio reale<\/h2>\n<p>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\u00e0 ma audio nel complesso pulito.<\/p>\n<p>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\u00ec in poi il transcript era leggibile come un copione: il moderatore che chiede &#8220;ma allora a che punto avete capito che era split brain?&#8221;, 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\u00e9.<\/p>\n<h2>Cosa fa bene<\/h2>\n<p>Webinar e tavole rotonde con tre-cinque relatori in audio decente: la separazione \u00e8 netta, gli scambi rapidi vengono catturati bene, i parlanti rimangono coerenti per tutta la durata (lo <code>SPEAKER_00<\/code> di inizio \u00e8 lo stesso <code>SPEAKER_00<\/code> di fine). Funziona bene anche su voci miste maschili e femminili e su accenti italiani regionali diversi.<\/p>\n<h2>Cosa fa meno bene<\/h2>\n<p>Quando due persone parlano davvero sopra l&#8217;una all&#8217;altra per pi\u00f9 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&#8217;ora), quindi sui file molto lunghi pre-spezzo con ffmpeg.<\/p>\n<h2>Privacy &#8211; vantaggio del modello locale<\/h2>\n<p>Le registrazioni di webinar tecnici sono materiale che preferisco non caricare da nessuna parte: possono contenere riferimenti a sistemi, configurazioni e dettagli che \u00e8 meglio tenere in casa. Tutto deve restare sull&#8217;host. Con pyannote locale processo il file, salvo l&#8217;RTTM, cancello l&#8217;audio originale dalla cartella di lavoro quando ho finito, e nulla \u00e8 mai passato per la rete oltre al download iniziale dei pesi.<\/p>\n<p>I pesi del modello stanno in <code>~\/.cache\/huggingface\/hub\/models--pyannote--speaker-diarization-community-1\/<\/code> insieme ai modelli embeddedati che la pipeline usa internamente. Cancellabili con <code>rm -rf<\/code> 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&#8217;audio e hanno policy di retention specifiche, qui non c&#8217;\u00e8 nulla di tutto questo.<\/p>\n<p>Licenza: pyannote.audio \u00e8 MIT, sviluppato dal team pyannote (Herv\u00e9 Bredin e collaboratori). Il modello <code>speaker-diarization-community-1<\/code> \u00e8 rilasciato con licenza permissiva e gate solo amministrativo per tracciare gli utilizzatori. Stack pulito, posso integrarlo dentro tooling interno senza vincoli copyleft.<\/p>\n<h2>In pratica<\/h2>\n<p>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\u00f9 voci \u00e8 il pezzo che trasforma un blob di testo in un transcript leggibile. Sui webinar a pi\u00f9 relatori mi ha cambiato il modo di lavorare: anzich\u00e9 riascoltare per ricostruire chi ha detto cosa, leggo il testo gi\u00e0 attribuito e mi concentro sui contenuti.<\/p>\n<hr>\n<blockquote>\n<p>Immagine generata con ComfyUI Mac M1 \/ RealVisXL V5 Lightning.<\/p>\n<\/blockquote>\n","protected":false},"excerpt":{"rendered":"<p>Whisper mi d\u00e0 un testo continuo, una sequenza di segmenti temporizzati che sa cosa \u00e8 stato detto ma non sa chi l&#8217;ha detto. Per un webinar con un solo relatore \u00e8 sufficiente, per una tavola rotonda con quattro o cinque relatori che si alternano diventa illeggibile. Per chiudere il cerchio uso pyannote, un toolkit Python [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":83,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6],"tags":[],"class_list":["post-82","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai-locale"],"_links":{"self":[{"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/82","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=82"}],"version-history":[{"count":6,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/82\/revisions"}],"predecessor-version":[{"id":397,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/82\/revisions\/397"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/media\/83"}],"wp:attachment":[{"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=82"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=82"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=82"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}