Diarizzazione locale degli speaker: come abbiamo creato un assistente per le riunioni 100% privato
OpenWhispr è l'assistente per riunioni open source in cui il tuo audio non lascia mai il tuo dispositivo. Ogni impronta vocale, ogni etichetta dello speaker, ogni embedding — elaborati e archiviati sulla tua macchina.
OpenWhispr
Ingegneria
Sommario
La diarizzazione locale degli speaker è il processo di etichettatura di chi ha parlato e quando in una registrazione di riunione, eseguito interamente sul dispositivo dell'utente senza trasmettere alcun audio, embedding o trascrizione a server esterni. Le trascrizioni delle riunioni hanno un problema di “chi ha detto cosa”. Zoom ti restituisce un muro di testo. La maggior parte degli assistenti IA per le note — Otter, Fireflies, Read, Granola — lo risolve caricando l'audio della tua riunione sui propri server, eseguendo lì un modello per gli speaker e rimandando indietro i segmenti etichettati. La tua voce, e quella dei tuoi colleghi, finisce come vettori nel database di qualcun altro. Noi non lo volevamo, e nemmeno i nostri utenti. OpenWhispr è l'assistente per riunioni open source costruito sul presupposto opposto: l'audio non lascia mai il tuo dispositivo, le impronte vocali risiedono in un file SQLite locale sul tuo disco e niente di una riunione viene caricato a meno che tu non lo scelga esplicitamente. Questo articolo è l'analisi tecnica del lato diarizzazione di quella promessa — che cos'è, quali modelli abbiamo scelto, come si combinano tra loro ed esattamente dove vive ogni byte di dati in ogni passaggio.
Ultimo aggiornamento: 15 aprile 2026. I dettagli implementativi fanno riferimento alla funzionalità di diarizzazione degli speaker integrata nell'app desktop OpenWhispr il 14 aprile 2026. Ogni affermazione tecnica qui sotto proviene da fonti primarie o è direttamente ispezionabile nel repository open source.
Riepilogo del fact-check (doppia fonte)
- La diarizzazione degli speaker risponde a “chi ha parlato e quando”. Viene valutata rispetto a benchmark della community come NIST Rich Transcription e la pipeline di riferimento pyannote.audio. Valutazione NIST RT · Paper pyannote.audio (Bredin et al., 2020)
- Usiamo pyannote-segmentation-3.0 e CAM++ del progetto 3D-Speaker. Entrambi sono open source, entrambi sono portabili in ONNX ed entrambi vengono scaricati direttamente dalle release dei modelli sherpa-onnx. Scheda del modello pyannote-segmentation-3.0 · Paper CAM++ / 3D-Speaker (Wang et al., 2023)
- Tutta l'inferenza passa attraverso sherpa-onnx, un binario ONNX nativo — niente runtime Python, niente PyTorch, nessuna GPU richiesta. Fissiamo la versione del binario e delle sue dipendenze al momento della build. sherpa-onnx su GitHub · Release di sherpa-onnx
- Le impronte vocali sono vettori float32 a 512 dimensioni archiviati come BLOB SQLite locali, circa 2KB per speaker. Non lasciano mai il dispositivo. Documentazione sui tipi di dato SQLite · ONNX Runtime
- Nessun audio, nessun embedding e nessuna trascrizione vengono trasmessi fuori dal dispositivo durante la diarizzazione. L'unica attività di rete dell'intera funzionalità è un download una tantum dei modelli al primo avvio, che puoi ispezionare tu stesso nel repository open source. Informativa sulla privacy di OpenWhispr · OpenWhispr su GitHub
Cos'è davvero la diarizzazione degli speaker
La diarizzazione degli speaker è il processo di etichettatura di chi ha parlato e quando in una registrazione — non di cosa ha detto. La trascrizione risponde a cosa; la diarizzazione risponde a chi. Le due cose sono complementari: un buon assistente per le note delle riunioni ha bisogno di entrambe, e farle funzionare insieme è più difficile che farle funzionare singolarmente.
La diarizzazione è davvero difficile per una manciata di motivi ostinati. Due voci possono sovrapporsi sulla stessa chiamata. Il rumore di fondo si insinua. Le frasi brevi (“sì”, “ok”) portano con sé pochissime informazioni sullo speaker. Nuovi speaker compaiono a metà riunione. E a differenza della trascrizione, la diarizzazione deve prendere decisioni globali — non può etichettare il segmento sette senza confrontarlo con il segmento due. È proprio per quel contesto globale che la maggior parte delle soluzioni commerciali esegue la diarizzazione lato server sull'intera registrazione, per poi rispedire indietro la trascrizione etichettata.
La sola trascrizione non basta. Una riunione di dieci persone con quarantacinque minuti di sovrapposizioni, resa come un unico muro di testo indistinto, è illeggibile. Le note delle riunioni — il vero risultato che le persone desiderano — richiedono l'attribuzione degli speaker, perché action item, decisioni e impegni sono tutti legati a persone. “Alice consegnerà la migrazione entro venerdì” è una nota. “Consegnerà la migrazione entro venerdì” è rumore.
La diarizzazione non è la stessa cosa del riconoscimento dello speaker
La diarizzazione dice “questi tre frammenti sono lo Speaker A; questi due sono lo Speaker B.” Non sa chi sia lo Speaker A. Il riconoscimento dello speaker mette un nome sullo Speaker A facendo corrispondere un'impronta vocale a un profilo archiviato. OpenWhispr fa entrambe le cose — la diarizzazione gira su ogni riunione, il riconoscimento entra in azione una volta che hai etichettato qualcuno con un nome. Entrambe restano in locale.
La pipeline locale a quattro stadi
La pipeline di diarizzazione degli speaker di OpenWhispr ha quattro stadi: rilevamento dell'attività vocale, segmentazione, embedding degli speaker e clustering — seguiti da un passaggio di merge che riconcilia i segmenti degli speaker con la trascrizione. Ogni stadio è un modello ONNX separato o un binario nativo. Niente runtime Python, niente PyTorch, niente CUDA. L'ingombro totale dei modelli su disco è di circa 45MB.
La pipeline di diarizzazione locale
Perché rimane locale:
Ogni fase viene eseguita sul tuo dispositivo tramite un singolo sherpa-onnx binario: no Python, no cloud.
~45MB di ONNX modelli scaricati una volta, quindi memorizzati nella cache in ~/.cache/openwhispr/diarization-models/.
Gli incorporamenti sono vettori 512-dim float32 memorizzati come SQLite BLOBs sul disco locale.
L'acquisizione audio è a doppio flusso fin dal primo giorno. Il microfono per definizione sei “tu”, quindi non spendiamo mai potenza di calcolo per cercare di diarizzare la tua stessa voce — ogni segmento del microfono viene etichettato come te in base alla sorgente, non al riconoscimento vocale. L'audio di sistema (tutti gli altri sulla chiamata) è l'unica cosa che ha davvero bisogno della suddivisione per speaker, il che dimezza immediatamente il lavoro.
Una volta acquisito l'audio, i quattro stadi vengono eseguiti in sequenza. Il VAD scarta il silenzio in modo che il costoso stadio di embedding non sprechi mai cicli su frame vuoti. La segmentazione divide il parlato continuo in frammenti di un singolo speaker. Il modello di embedding trasforma ogni frammento in un'impronta vocale a 512 dimensioni. Il clustering agglomerativo raggruppa le impronte simili, producendo un insieme finale di ID degli speaker. Un ultimo passaggio reintegra quegli ID nella trascrizione tramite sovrapposizione temporale, così ogni parola finisce taggata con uno speaker.
Il tutto gira all'interno di un singolo processo figlio generato dall'app — nessun server, nessuna porta in ascolto, nessun IPC verso qualcosa al di fuori di Electron. Puoi verificarlo leggendo src/helpers/diarization.js nel repository open source.
Stadio 1: rilevamento dell'attività vocale (Silero)
Il rilevamento dell'attività vocale (VAD) è il filtro economico che separa il parlato dal silenzio prima che vengano eseguiti gli stadi costosi. Eseguire un modello di embedding degli speaker sul silenzio è CPU sprecata e produce vettori spazzatura. Il VAD è ciò che mantiene veloce il resto della pipeline.
Usiamo Silero VAD: un modello ONNX da 2MB, con licenza MIT, di fatto lo standard di settore open source. Silero restituisce, per ogni frame, una probabilità che la finestra corrente di 32 millisecondi contenga parlato. Su una CPU moderna il costo è trascurabile — circa 0,1 millisecondi per frammento di 32 millisecondi, ovvero all'incirca un terzo di punto percentuale di un core.
La pipeline in tempo reale esegue Silero in modo continuo sull'audio di sistema durante una chiamata. Le soglie con cui effettivamente rilasciamo il prodotto sono calibrate per audio reale di riunioni, non per un benchmark pulito:
- Dimensione finestra: 512 campioni (32ms a 16kHz)
- Soglia di parlato: 0,15 — volutamente aggressiva, così gli speaker bassi o lontani non vengono persi
- Soglia di silenzio: 0,08
- Il segmento termina dopo: 16 finestre silenziose consecutive (~512ms)
- Segmento minimo per l'embedding: 0,8 secondi
- Cadenza di identificazione in tempo reale: ogni 1 secondo, una volta accumulati ≥1,6 secondi di parlato
Un compromesso onesto
Una soglia di parlato aggressiva di 0,15 cattura chi parla piano su microfoni lontani, ma di tanto in tanto si attiva per errore su pressioni di tasti rumorose o rumore di stanza. Il filtro di segmento minimo di 0,8 secondi scarta la maggior parte di questi falsi positivi prima che raggiungano lo stadio di embedding, così l'accuratezza a valle è protetta.
Stadio 2: segmentazione (pyannote 3.0)
La segmentazione prende il parlato continuo e lo divide in frammenti che contengono ciascuno un solo speaker, rilevando sia i punti di cambio speaker sia le regioni di sovrapposizione. È lo stadio che decide dove sono i confini tra gli speaker. Tutto ciò che viene dopo assume che quei confini siano corretti.
Usiamo pyannote-segmentation-3.0, la versione attuale del modello del team pyannote.audio, esportato in ONNX dal progetto sherpa-onnx . Pyannote è di fatto la baseline accademica: la pipeline pyannote.audio raggiunge tassi di errore di diarizzazione nella fascia del 12–15% su benchmark standard come AMI e CALLHOME — gli stessi numeri che i fornitori cloud commerciali amano citare.
Perché proprio il port ONNX? Perché distribuire PyTorch dentro un'app Electron è impensabile — il solo runtime pesa circa 1,5GB e di fatto richiede una GPU per essere veloce. L'export ONNX è un singolo file da 6,6MB che gira su CPU tramite ONNX Runtime. Stesso modello, una frazione minima del peso.
Lo invochiamo tramite il binario di diarizzazione offline di sherpa-onnx con una manciata di flag. Le impostazioni di durata minima provengono dai default raccomandati nel paper di pyannote: i brevi scoppi di parlato vengono uniti al silenzio adiacente, e i brevi silenzi non interrompono un segmento continuo di uno speaker. L'output è una lista di coppie di tempi inizio/fine taggate con ID provvisori degli speaker ( speaker_0, speaker_1, ecc.).
Stadio 3: embedding degli speaker (CAM++)
Un embedding dello speaker è una lista di numeri a lunghezza fissa che rappresenta una voce come un hash rappresenta un file. Due registrazioni della stessa voce producono embedding vicini nello spazio vettoriale; voci diverse producono embedding lontani tra loro. La “vicinanza” si misura con la similarità del coseno — un singolo numero compreso tra -1 e 1. Questo è il cuore matematico di tutto il resto della pipeline.
Il nostro modello di embedding è CAM++ del progetto 3D-Speaker della DAMO Academy di Alibaba, addestrato sul benchmark di verifica dello speaker VoxCeleb . Il file specifico che distribuiamo è 3dspeaker_speech_campplus_sv_en_voxceleb_16k.onnx, un export ONNX di circa 28MB che produce un vettore float32 a 512 dimensioni per segmento. Il paper è arXiv:2303.00332 (Wang et al., 2023).
Abbiamo scelto CAM++ rispetto all'alternativa più ovvia, ECAPA-TDNN, per tre motivi concreti: circa la metà dei parametri, un tasso di errore uguale (EER) inferiore su VoxCeleb1-O e un'inferenza su CPU più veloce. Il modello MSDD di NVIDIA NeMo non ha un export ONNX di prima parte e si appoggia pesantemente a PyTorch. Picovoice Falcon prevede una licenza commerciale per postazione. Il framework CoreML Speech di Apple è solo per macOS. CAM++ era l'unica opzione che soddisfaceva i nostri vincoli — multipiattaforma, ONNX, licenza aperta, veloce su CPU.
Per alimentare CAM++ distribuiamo il nostro estrattore di feature log-mel filterbank in src/helpers/speakerEmbeddings.js: 80 bande mel, finestre di 25 millisecondi, hop di 10 millisecondi, parametri standard per questa classe di modello. Le feature confluiscono nel modello ONNX tramite onnxruntime-node, in esecuzione nel processo principale di Electron. Dall'altra parte esce un Float32Array di 512 elementi, che scriviamo in SQLite come BLOB da 2.048 byte.
Il passaggio di matching in sé è la cosa più semplice possibile: la similarità del coseno tra due vettori. Ecco il codice letterale che decide se due segmenti di riunione appartengono allo stesso speaker:
// src/helpers/speakerEmbeddings.js
cosineSimilarity(a, b) {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
return denom === 0 ? 0 : dot / denom;
}Perché 512 dimensioni?
512 è l'attuale punto di equilibrio sulla leaderboard di verifica dello speaker di VoxCeleb tra accuratezza e costo di archiviazione. Con 256 dimensioni si inizia a perdere potere discriminante su voci simili ma diverse; con 1024 dimensioni si aggiungono archiviazione e calcolo con rendimenti decrescenti. 512 float32 × 2KB × mille riunioni da dieci speaker fanno 20MB su disco. Stanno comodamente in un file SQLite locale.
Stadio 4: clustering — il problema “quanti speaker?”
Dopo aver fatto l'embedding di ogni segmento hai N vettori. Il clustering li raggruppa in K speaker — ma la parte difficile è che di solito non conosci K in anticipo. Una chiamata di vendita potrebbe avere 2 speaker. Una riunione plenaria potrebbe averne 8. Il clustering a K fisso si rompe a entrambi gli estremi.
Usiamo il clustering agglomerativo con una soglia di similarità del coseno di 0,5: si parte con ogni segmento come cluster a sé, si uniscono i due cluster con la similarità più alta, si ripete finché nessuna coppia residua supera la soglia. È la soglia, non un conteggio target, a decidere dove fermarsi. Questo scala in modo naturale da due a dieci speaker senza alcuna configurazione.
L'invocazione vera e propria vive in src/helpers/diarization.js. È un'esecuzione del binario sherpa-onnx con una manciata di flag CLI:
// src/helpers/diarization.js
const args = [
`--segmentation.pyannote-model=${segPath}`,
`--embedding.model=${embPath}`,
`--clustering.num-clusters=${numSpeakers}`, // -1 = auto
`--clustering.cluster-threshold=${threshold}`, // 0.5 default
"--min-duration-on=0.2",
"--min-duration-off=0.5",
wavPath,
];L'output è testo semplice: una riga per segmento, formattata come start_sec -- end_sec speaker_NN. La analizziamo trasformandola in un array di oggetti e poi percorriamo la trascrizione, facendo corrispondere ogni segmento della trascrizione al segmento di diarizzazione con la massima sovrapposizione temporale. I segmenti provenienti dal microfono sono sempre etichettati you — la sorgente batte la voce, ogni volta.
La soglia di 0,5 non è un'ipotesi. L'abbiamo calibrata rispetto al nostro eval harness (scripts/meeting-diarization-eval.js) su registrazioni reali di riunioni. Soglie più alte sotto-raggruppano — uno speaker viene diviso in due. Soglie più basse sovra-raggruppano — due voci simili vengono unite. 0,5 è il punto di equilibrio su cui siamo convergiti.
Diarizzazione in tempo reale: etichette mentre parli
La diarizzazione solo in batch è accurata ma invisibile durante una chiamata. Quella solo in tempo reale è immediata ma rumorosa. OpenWhispr le esegue entrambe: le etichette in tempo reale compaiono durante la riunione, e un passaggio batch completo le affina dopo.
Il percorso in tempo reale vive in src/helpers/liveSpeakerIdentifier.js. Intercetta il flusso dell'audio di sistema a 16kHz, alimenta ogni frame di 32 millisecondi in Silero VAD e accumula i frame di parlato finché un segmento non si chiude (dopo ~512ms di silenzio) o non raggiunge la cadenza di identificazione in tempo reale (ogni 1 secondo, una volta accumulati ≥1,6 secondi). Quando si attiva, estrae un embedding CAM++, lo confronta con la mappa in memoria degli embedding della chiamata attiva e con i profili degli speaker archiviati, e lancia un evento IPC — meeting-speaker-identified — con la corrispondenza migliore. Il lato React la raccoglie e aggiorna sul posto la bolla della trascrizione.
Una volta che una persona ha impostato manualmente il nome di uno speaker nell'interfaccia, quell'assegnazione è bloccata. Al passaggio batch post-riunione non è consentito sovrascriverla, anche se la sua stessa analisi è in disaccordo. Questo comportamento di blocco vive in src/utils/transcriptSpeakerState.ts, ed è importante: la peggiore UX possibile è lasciare che un passaggio automatizzato sovrascriva silenziosamente la correzione di un utente.
Quando la registrazione si ferma, eseguiamo la diarizzazione offline completa sull'intero file WAV. Il batch ha il contesto completo — vede l'intera riunione, non solo gli ultimi 1,6 secondi — e produce un'etichettatura finale più pulita. Il risultato del batch viene riconciliato con le etichette in tempo reale, rispetta ogni blocco dell'utente e sostituisce le ipotesi provvisorie con il raggruppamento più accurato.
Perché l'ibrido batte entrambi gli approcci da soli
Il solo tempo reale è veloce ma rumoroso sui segmenti brevi. Il solo batch è accurato ma silenzioso finché la riunione non finisce. L'ibrido ti dà entrambe le cose — feedback istantaneo durante la chiamata, e una trascrizione finale che riflette l'analisi a contesto completo. Il costo è un passaggio di riconciliazione in più, che gira in background una volta sola ed è di fatto gratuito.
Profili vocali: ricordare gli speaker tra una riunione e l'altra
Una volta che etichetti uno speaker come “Alice” in una riunione, OpenWhispr la etichetta automaticamente in ogni riunione futura — senza che tu faccia nulla, e senza che la sua impronta vocale lasci mai il tuo dispositivo.
Il meccanismo è semplice: l'embedding del centroide di ogni speaker viene archiviato nel tuo database SQLite locale. Quando nuovi embedding compaiono in una chiamata futura, li confrontiamo tramite similarità del coseno con ogni profilo archiviato. Una corrispondenza sopra la soglia assegna automaticamente il nome. Niente di tutto questo tocca un server. Le tre tabelle che lo alimentano sono semplice SQLite:
-- src/helpers/database.js
CREATE TABLE speaker_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
display_name TEXT NOT NULL,
email TEXT,
embedding BLOB NOT NULL, -- 512 float32s (~2KB)
sample_count INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE speaker_mappings (
note_id INTEGER NOT NULL,
speaker_id TEXT NOT NULL,
profile_id INTEGER,
display_name TEXT NOT NULL,
PRIMARY KEY (note_id, speaker_id),
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE,
FOREIGN KEY (profile_id) REFERENCES speaker_profiles(id) ON DELETE SET NULL
);
CREATE TABLE note_speaker_embeddings (
note_id INTEGER NOT NULL,
speaker_id TEXT NOT NULL,
embedding BLOB NOT NULL,
PRIMARY KEY (note_id, speaker_id),
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
);Il matching è a tre livelli, per bilanciare la confidenza rispetto ai falsi positivi:
- Coseno ≥ 0,70: conferma automatica. L'etichetta compare immediatamente senza alcun prompt.
- 0,55 ≤ Coseno < 0,70: suggerimento. L'interfaccia mostra “È Alice?” con i controlli conferma / ignora, e attende il tuo input.
- Coseno < 0,55: resta anonimo. Il segmento rimane Speaker N finché non lo nomini manualmente.
Quando confermi una corrispondenza, il centroide archiviato si aggiorna tramite media mobile anziché con una sovrascrittura grezza. La formula è semplice: new_avg = (stored_avg × sample_count + new_embedding) / (sample_count + 1). Questo gestisce la deriva della voce — una mattina fredda, un microfono diverso, un cavo difettoso — senza compromettere le corrispondenze future. Più campioni della voce di Alice il sistema ha visto, più stabile diventa il suo profilo.
I nuovi profili attivano anche la rietichettatura retroattiva. Quando nomini uno speaker per la prima volta, un task in background percorre gli embedding archiviati di ogni riunione passata, trova le corrispondenze e aggiorna gli speaker precedentemente senza nome nelle trascrizioni storiche. Le associazioni bloccate dall'utente non vengono mai toccate. Il risultato: nominare Alice una volta la etichetta retroattivamente in ogni chiamata precedente a cui ha partecipato.
Se Google Calendar è collegato e la riunione ha partecipanti, l'interfaccia di selezione dello speaker viene pre-popolata con i loro nomi ed email. Un clic mappa Speaker 2 → alice@example.com e lo mantiene per sempre — partecipanti e profili vocali restano collegati, così i nomi giusti compaiono anche su dispositivi diversi.
Dove vivono davvero i tuoi dati
L'audio non lascia mai il tuo dispositivo. Non è uno slogan di marketing — è una proprietà meccanica del codice, e dato che OpenWhispr è open source puoi verificarlo tu stesso. Ecco esattamente cosa succede a ciascun dato in ogni passaggio.
| Cosa | Dove vive | Lascia il dispositivo? |
|---|---|---|
| Audio grezzo della riunione | Cartella temporanea del sistema operativo, eliminata dopo la diarizzazione | No — mai |
| Modelli ONNX di diarizzazione | ~/.cache/openwhispr/diarization-models/ | Scaricati una volta al primo avvio, poi mai più |
| Embedding degli speaker (impronte vocali) | BLOB SQLite locale | No — mai |
| Associazioni speaker-nome | Tabella SQLite locale | No — mai |
| Testo della trascrizione | SQLite locale (sincronizzazione cloud opzionale, se la attivi) | Solo se acconsenti |
L'audio PCM grezzo di una riunione viene scritto in un file nella cartella temporanea del tuo sistema operativo, passato al binario sherpa-onnx generato in locale ed eliminato in un blocco finallyquando la diarizzazione si completa. Non esiste alcun percorso di codice nella pipeline di diarizzazione di OpenWhispr che apra un socket di rete per caricare l'audio. Dato che il codice è open source, puoi fare grep su src/helpers/diarization.js, src/helpers/liveSpeakerIdentifier.js, e src/helpers/speakerEmbeddings.js per fetch, axios, o http e verificarlo tu stesso.
L'IPC tra il processo main e quello renderer di Electron usa il contextBridge integrato — una pipe a livello di kernel all'interno di un singolo processo, non un socket di rete. Gli embedding degli speaker e i profili vocali sono archiviati come BLOB SQLite nello stesso file di database delle tue note, con gli stessi permessi di file a livello di sistema operativo.
Un'unica eccezione, in modo trasparente
L'unica attività di rete di questa funzionalità è il download una tantum dei modelli al primo avvio — tre file ONNX recuperati dalla CDN delle release GitHub di sherpa-onnx. Lo documentiamo esplicitamente perché “una chiamata di rete durante il setup, zero dopo” è una storia di privacy molto diversa da “inferenza cloud sempre attiva”, e come assistente per riunioni open source preferiamo dirtelo piuttosto che lasciartelo scoprire.
Limiti, onestamente
La diarizzazione locale è buona, non perfetta. Ecco i casi in cui fatica davvero, e cosa facciamo per ciascuno.
- Speaker sovrapposti. Due persone che parlano contemporaneamente comportano intrinsecamente una perdita di informazione per l'attribuzione a etichetta singola. Pyannote 3.0 rileva la regione di sovrapposizione, ma un singolo speaker_id per segmento non basta a rappresentare “due persone che parlano”. Marchiamo quei segmenti come provvisori e ti lasciamo correggerli nell'interfaccia.
- Accuratezza all'avvio a freddo. La prima riunione con un nuovo collega usa solo le feature generiche addestrate su VoxCeleb. Una volta che lo etichetti una volta e il profilo ha qualche campione, le riunioni successive sono notevolmente più accurate.
- Frasi brevi. “Sì”, “ok”, una risata veloce — qualsiasi cosa sotto i ~0,8 secondi non può produrre un embedding affidabile. Scartiamo quei segmenti dallo stadio di embedding e ripieghiamo sulla propagazione dell'etichetta dal contesto circostante.
- Audio in campo lontano o a basso SNR. Qualcuno in viva voce su un telefono in una stanza rumorosa degrada in modo graduale ma visibile. Il controllo automatico del guadagno e la calibrazione del VAD aiutano, ma nessun livello di sofisticatezza del modello supera del tutto la cattiva fisica.
- Inferenza solo su CPU. Una GPU sarebbe più veloce, ma deliberatamente non ne richiediamo una. In pratica, il throughput su una CPU moderna è più che sufficiente: ~100 millisecondi per embedding, e circa 30 secondi di diarizzazione batch per una riunione di 45 minuti su un Mac M1.
Pubblichiamo il nostro eval harness ( (scripts/meeting-diarization-eval.js) ) nel repository open source così puoi misurare tutto questo tu stesso, sulle tue registrazioni — non sui nostri clip dimostrativi selezionati ad arte. Ci sembra più onesto di un numero patinato in una presentazione di marketing.
Perché abbiamo scelto Sherpa-ONNX
Ci serviva un runtime di diarizzazione che fosse multipiattaforma, senza Python, veloce su CPU e open source. Sherpa-ONNX era l'unica opzione a centrare tutti e quattro i requisiti.
I nostri requisiti erano concreti. Doveva girare su macOS (sia Intel sia Apple Silicon), Windows x64 e Linux x64 senza percorsi di codice separati. Non poteva includere PyTorch — il solo runtime pesa circa 1,5GB e di fatto richiede una GPU per essere usabile a latenze interattive. Non poteva richiedere CUDA. E la licenza doveva essere abbastanza permissiva da poter essere distribuita in un'app desktop commerciale ma open source.
Ecco cosa abbiamo effettivamente valutato:
- pyannote.audio (Python): escluso. Includere PyTorch in un'app Electron è impensabile.
- NVIDIA NeMo MSDD: dipendente da PyTorch, nessun export ONNX di prima parte, centrato sulla GPU.
- Picovoice Falcon: licenza commerciale per utente attivo più attivazione cloud al primo avvio.
- Apple CoreML + framework Speech: solo per macOS, nessuna parità multipiattaforma.
- sherpa-onnx: Apache 2.0, binario ONNX nativo, ~35MB di modelli, interfaccia spawn-e-leggi-stdout, mantenuto attivamente da k2-fsa. Lo stesso toolchain alimenta l'ASR in produzione in altri prodotti già rilasciati.
Un'avvertenza onesta: il binario di diarizzazione offline degli speaker di sherpa-onnx è relativamente nuovo. Fissiamo la versione a v1.12.23 ed eseguiamo il nostro eval harness completo su ogni aggiornamento prima di alzare il pin. È il compromesso giusto per qualcosa di così cruciale.
Domande frequenti
- Esiste un assistente privato per le note delle riunioni che non carica il mio audio nel cloud?
- Sì. OpenWhispr è un assistente per le note delle riunioni gratuito e open source in cui il tuo audio non lascia mai il tuo dispositivo. La trascrizione gira in locale con OpenAI Whisper o NVIDIA Parakeet, la diarizzazione degli speaker gira in locale con la segmentazione pyannote e gli embedding CAM++, e le impronte vocali sono archiviate in un file SQLite locale sulla tua macchina — non sui nostri server.
- Otter, Fireflies o Granola possono vedere le mie riunioni?
- Sì. Gli assistenti IA cloud per le note caricano l'audio della tua riunione sui propri server per trascriverla e diarizzarla. Etichette degli speaker, trascrizioni ed embedding vocali vengono calcolati nella loro infrastruttura e archiviati nei loro database. Se hai bisogno di un'alternativa che mantenga ogni byte di audio sul tuo dispositivo, ti serve uno strumento local-first come OpenWhispr.
- Cos'è la diarizzazione degli speaker?
- La diarizzazione degli speaker è il processo di etichettatura di chi ha parlato e quando in una registrazione, a differenza della trascrizione, che risponde a cosa è stato detto. Una trascrizione diarizzata mostra ogni intervento taggato con un'etichetta dello speaker — Alice, Bob, Speaker 3 — invece di un unico muro di testo indistinto.
- La diarizzazione locale degli speaker funziona davvero, o è un peggioramento rispetto al cloud?
- Funziona. La pipeline di OpenWhispr — pyannote-segmentation-3.0 più gli embedding CAM++ del progetto 3D-Speaker, eseguiti tramite sherpa-onnx — raggiunge tassi di errore di diarizzazione nello stesso ordine di grandezza delle API cloud commerciali sui benchmark standard. I principali compromessi onesti riguardano la gestione degli speaker sovrapposti e l'accuratezza all'avvio a freddo su voci che il sistema non ha mai sentito prima.
- Quanto spazio su disco occupano i modelli di diarizzazione locale?
- Circa 45MB in totale: la segmentazione pyannote (6,6MB) più il modello di embedding degli speaker CAM++ (28MB) più Silero VAD (2MB). Tutti e tre vengono scaricati una volta al primo avvio e memorizzati nella cache in ~/.cache/openwhispr/diarization-models/. Dopodiché restano su disco e non vengono mai riscaricati.
- Mi serve una GPU per eseguire la diarizzazione locale di OpenWhispr?
- No. Tutto gira su CPU tramite ONNX Runtime. Apple Silicon moderni e le CPU x86 recenti la gestiscono comodamente — in pratica, circa 30 secondi di diarizzazione batch per una riunione di 45 minuti su un Mac M1, con le etichette degli speaker in tempo reale che compaiono entro un secondo o due durante la chiamata stessa.
- OpenWhispr è davvero open source?
- Sì. Il codice di diarizzazione, la pipeline di trascrizione e il resto dell'app desktop sono open source su GitHub. Le affermazioni sulla privacy in questo articolo sono direttamente verificabili: fai grep sugli helper di diarizzazione per fetch, axios o http e non troverai alcuna chiamata di rete nel percorso di diarizzazione.
- Cosa succede quando due persone parlano sovrapponendosi durante una chiamata?
- Pyannote 3.0 rileva e annota esplicitamente le regioni di sovrapposizione, ma l'attribuzione a etichetta singola durante la sovrapposizione comporta intrinsecamente una perdita di informazione. Marchiamo quei segmenti come provvisori nella trascrizione e ti lasciamo correggerli cliccando sull'etichetta dello speaker. Le tue correzioni manuali sono bloccate e non vengono mai sovrascritte dai passaggi automatizzati successivi.
Prova l'assistente privato per le note delle riunioni
OpenWhispr è un assistente per riunioni gratuito e open source in cui il tuo audio non lascia mai il tuo dispositivo. Diarizzazione locale, trascrizione locale, profili vocali locali. Funziona su macOS, Windows e Linux.
Nessun account richiesto · Funziona offline · Open source per sempre