./run geena-assistente-vocale-raspberry-wake-word-voce-reale
Geena sente il mio Mac meglio di me: 7 «Gina» su 40
Diario tecnico di un assistente vocale su Raspberry Pi 5: barge-in a 0,72 s, riconoscimento del parlante, un modello locale bocciato e una wake word che non mi sente.
- status
- wip
- project
- geena
- updated
- 2026-09-28
- tags
Per giorni ho accumulato clip di successo: «Gina» pronunciato, bip, turno partito. Trecento registrazioni, statistiche che salivano. Poi ho ascoltato le clip. Erano quasi tutte della voce sintetica del Mac che uso per i test automatici, non mia. Ogni numero costruito su quel mucchio era falso, e la verità misurata sulle mie quaranta registrazioni vere è questa: 7 su 40. La stessa parola, detta dal Mac, arriva a 0,86.
L’idea
Geena sta sulla scrivania, non è un altoparlante intelligente sparso per casa. Un solo dispositivo, un Raspberry Pi 5 da 8 GB con una scheda a due microfoni e codec WM8960. L’obiettivo l’ho corretto in corsa: ero partito da «meglio di Alexa» e sono arrivato a «meglio di J.A.R.V.I.S.» — un interlocutore, non un telecomando a voce.
La scelta strutturale che regge tutto: il programma locale non è un cervello. È orecchie, bocca e LED. Il cervello è un agente LLM con strumenti veri (terminale, promemoria, memoria, rete), su un profilo dedicato. Ogni comando passa da lì. Prima c’era un prototipo appoggiato a un hub domotico: buttato, perché un assistente che sa solo accendere le luci non è un interlocutore.
Come funziona davvero
microfono ──► dsnoop (ALSA) ──┬─► cancellazione eco (AEC3) ─► wake word
│ │
│ └─► rilevatore "qualcuno le parla sopra"
│ ─► pausa ─► ASR conferma «Gina»
└─► registratore comando ─► ASR ─► testo
│
impronta vocale (chi parla)
▼
agente LLM + strumenti ──► esecuzione reale
│
altoparlante ◄── TTS ◄───────────────┘ + LED + pulsante
Riconoscitore: Parakeet TDT 0.6B v3 via whisper.cpp. Voce: Piper, voce italiana media. Wake word: openWakeWord. Ogni funzione ha un interruttore in features.yaml e un comando di rollback provato nei due sensi, impilati in ordine: si torna indietro un pezzo alla volta.
La parte che funziona: interrompere a voce
Il pezzo di cui vado più fiero è il barge-in — le parli sopra mentre risponde e lei tace. Misurato: 28 interruzioni riuscite su 30, 0,72 s mediani dalla fine della parola di richiamo al bip, 0 falsi su 19 tentativi con parole simili («Tina», «Gino», «Regina»), 0 auto-interruzioni in 13 minuti di parlato continuo.
La scoperta controintuitiva sta qui: la cancellazione d’eco funziona troppo bene. Mentre lei parla, l’AEC cancella l’eco del suo altoparlante — e insieme cancella anche la voce di chi le parla sopra. La soluzione non è stata un filtro migliore, ma farla respirare: circa 0,45 s di pausa fra le frasi. È nei suoi silenzi che il riconoscitore sente l’umano.
Chi sta parlando
Serviva che dicesse al suo cervello chi le sta parlando. Modello di embedding vocale in ONNX (~26 MB), sullo stesso onnxruntime già presente per la wake word; front-end mel in stile Kaldi riscritto in numpy per non installare nulla nell’ambiente dell’agente; impronta = centroide a 256 dimensioni da 8 frasi, confronto per coseno.
def session():
global _sess
if _sess is None:
opts = ort.SessionOptions()
opts.intra_op_num_threads = 2 # lascia core alla catena audio
_sess = ort.InferenceSession(str(MODEL), sess_options=opts,
providers=["CPUExecutionProvider"])
return _sess
def cmd_verify(path, name=None):
e = embed(path)
scores = {k: float(e @ np.array(d["centroid"])) for k, d in load_voices().items()}
Validazione seria, con frasi tenute fuori dall’impronta: +0,857 e +0,801 per la mia voce, +0,10 per la voce di Geena stessa, da −0,21 a +0,16 per voci sintetiche. Soglia in esercizio 0,55. Dal vivo, la voce sintetica passata per altoparlante, stanza e microfono ha preso +0,526 in una prova e +0,085 in quella definitiva: il canale conta, l’impronta cattura anche microfono e stanza, non solo il timbro.
La prima validazione, quella fatta di notte, dava 0,92 contro 0,10. Era falsata dalle stesse clip sbagliate. Due volte lo stesso errore, sullo stesso mucchio di dati.
La scelta di prodotto: consapevole, non portinaia. Geena risponde a tutti, ma dice al suo cervello chi le sta parlando. Nessun comando viene rifiutato.
La wake word: il problema ancora aperto
| Condizione | Riconosciute (soglia 0,4) |
|---|---|
| Voce normale | 2/10 |
| Voce bassa | 3/10 |
| Girato di lato | 0/10 |
| Da lontano | 2/10 |
| Totale | 7/40 (17%) |
Molti punteggi sono esattamente 0,00: non è questione di soglia, il modello proprio non reagisce alla mia voce. È addestrato su voce sintetica inglese.
Cosa ho provato, in ordine di ottimismo decrescente:
- Classificatore personale sulle mie 40 registrazioni: 11–45% tenendo fuori una condizione alla volta, con un falso ogni 1000 finestre. Meglio del 17%, non abbastanza.
- Dataset sintetico italiano: 288 positivi (voci di sistema a tre velocità più voci commerciali) e 822 negativi confondibili — Dina, Gianna, cucina, pagina, macchina. Addestrato solo su quello e misurato sulla mia voce: AUC 0,42, sotto il caso. Le voci sintetiche non trasferiscono alla voce reale, e aumentarle non serve a niente.
- Confronto diretto per similarità, senza addestramento: 0% a parità di falsi allarmi.
Restano tre strade: molti più campioni miei, una frase di richiamo più lunga, oppure usare la trascrizione come riconoscitore — quella la parola la prende, ed è la stessa catena già costruita per il barge-in.
Il modello locale: provato e bocciato
Ho provato a servire un modello da 2B sul Pi stesso per togliere la latenza di rete. Contesa sui quattro core, misurata: trascrizione del comando da 1,5–1,6 s a 2,4–3,3 s (+68%) mentre genera; prima parola pronunciata da 0,8–0,9 s a 1,8–2,0 s (+125%). I quattro thread sono controproducenti — 2 thread 7,89 token/s, 3 thread 7,09, 4 thread 6,31: la generazione è limitata dalla banda di memoria, non dal calcolo.
Ma la bocciatura è arrivata altrove: zero chiamate agli strumenti in 6 turni reali, di cui 4 ne avevano bisogno. Invece di eseguire, inventava: «sono le [current time]», «volume a sette» senza aver toccato niente. Con il ragionamento riacceso gli strumenti li usa, ma scende a 0,88 token/s. Per un assistente che deve fare, un modello che allucina l’esecuzione è il difetto peggiore possibile. Sono tornato a un modello remoto.
Gotcha onesti (quasi tutti hardware)
- I guasti «misteriosi» erano alimentazione. Registro
throttleda 0x50000,Undervoltage detected!subito prima di ogni caduta, 25 cali in un solo avvio. Lezione pagata cara: non contano i watt totali, conta quanto dà a 5 V — moltissimi alimentatori da 65 W dichiarano quella potenza a 9/12/20 V e a 5 V si fermano a 3 A. - Il watchdog hardware era inutile. Durante un blocco notturno systemd continuava a dare il colpetto mentre il sistema non riusciva più ad avviare un processo di login. Kernel vivo, userspace morto: battito regolare, paziente morto.
- La microSD è stata accusata a torto. Filesystem in sola lettura tre volte, sempre sotto scrittura intensa.
f3ha scritto e riletto 42 GB senza perdere un byte; l’alimentatore teneva 5,10–5,16 V a pieno carico. Il test che ha chiuso la questione è stato togliere una variabile: senza la scheda audio, 56 GB scritti a pieno ritmo, zero errori; con la scheda montata, sola lettura dopo 80 secondi. Rimontata la scheda con il kernel aggiornato, sette prove da 30 minuti sono passate tutte. Il colpevole era quasi certamente un kernel che avevo bloccato a mano — l’unica cosa cambiata fra i crolli e la stabilità. La causa precisa dentro quel kernel non è dimostrata, e non ho intenzione di fingere il contrario. - Il kernel l’avevo bloccato per riparare un altro guasto (audio morto dopo un aggiornamento: l’overlay della scheda dichiarava il clock master sul nodo sbagliato, la frequenza restava a zero e ogni apertura audio falliva con
-22). Quando è emersa la causa vera, il motivo del blocco non esisteva più. Ma il blocco era rimasto lì a fare danni. - Sbagli di partenza, per completezza: registrazione sempre di 20 s perché la mediana RMS del rumore di stanza (278) stava sopra la soglia di silenzio (200); comandi eseguiti in UTC, due ore di sfasamento; audio a 44,1/48 kHz portato a 16 kHz facendo la media di 2–3 campioni, cosa che distorce proprio le frequenze della voce.
Come va
Funziona, e si misura a voce: ogni funzione è provata facendo parlare un altro computer nella stanza e registrando la risposta con un secondo microfono, poi si confrontano i tempi reali. Wake word, barge-in, conversazione continua (dopo la risposta resta in ascolto, la frase successiva non richiede «Gina»), promemoria, timer, sveglie, annunci di sua iniziativa, riconoscimento del parlante. Mai muta e mai troncata: frasi di attesa a 60 e 120 s, annullamento solo dopo 300 s di silenzio. L’unico pezzo che non regge è proprio la porta d’ingresso — la wake word sulla mia voce.
Cosa ho imparato
Guarda i dati prima delle metriche. Trecento clip «riuscite» che erano di un altro parlante mi hanno regalato due validazioni false di fila. Adesso, prima di fidarmi di un numero, ascolto o trascrivo il campione.
I dati sintetici non insegnano una voce vera. 288 positivi sintetici hanno prodotto AUC 0,42 — sotto il lancio di una moneta. Quaranta registrazioni mie valgono più di mille del Mac, e la soluzione non è generarne altre mille.
Togli una variabile alla volta. L’ultimo guasto aveva tutta l’aria di una microSD morente e non lo era. Non l’ha risolto nessuna ipotesi: l’ha risolto smontare fisicamente un pezzo e riprovare.
Un assistente che inventa l’esecuzione è peggio di uno lento. Preferisco pagare un giro di rete a un modello che mi dice di aver alzato il volume senza averlo toccato.
Fallire in silenzio è vietato. Ogni funzione nuova ha un interruttore e un rollback provato nei due sensi; se non funziona si torna al comportamento di prima e lo si scrive nel log. Meglio Geena com’era che Geena muta.
Aggiornamenti
2026-09Il secondo orecchio: da 7 a 32 «Gina» su 40
Il problema aperto della wake word l’ho chiuso con il trascrittore che usavo già per i comandi: Parakeet le mie «Gina» le aveva sempre capite. Sulle stesse 40 registrazioni vere del post passo da 7 a 32, con zero falsi allarmi su 95 s di conversazione vera.
| Condizione | Modello wake | Parakeet |
|---|---|---|
| Voce normale | 2/10 | 10/10 |
| Voce bassa | 3/10 | 9/10 |
| Da lontano | 2/10 | 9/10 |
| Girato di lato | 0/10 | 4/10 |
| Totale | 7/40 | 32/40 (80%) |
parakeet-serve: la CLI ricaricava il modello a ogni chiamata e ci metteva 5,3 s. Ho scritto apposta un processo residente che carica il modello una volta sola (1 s, 653 MB) e risponde in 0,57 s su una clip da 4 s e in 0,2 s su una chiamata breve. Riceve i percorsi su stdin e restituisce una riga per richiesta.earshot.py: un rivelatore di voce elementare (energia contro un rumore di fondo stimato di continuo) ritaglia il parlato. Manda a trascrivere solo i tratti sotto i 2 s, e di una frase lunga solo l’inizio, perché «Gina, che ore sono» ha il nome in testa. Se nella trascrizione c’è una parola della famiglia (gina, dina, gino, china…), è una sveglia. Le troncature a voce bassa («Gin.», «Ging.», «Gene.») le ho aggiunte dopo aver verificato che non compaiono mai in 353 parole di conversazione vera nella stanza. Tutto sta dietroasr_wakeinfeatures.yaml, con un rollback dedicato.- Due trappole: il microfono ha un offset costante di −968, quindi il silenzio pesava 964 di RMS e superava ogni soglia; l’ho tolto con un passa-alto a 12 Hz. Poi la doppia sveglia: i due rilevatori sentivano la stessa parola e il secondo annullava il turno appena aperto dal primo. Ora il secondo orecchio controlla l’ultima sveglia del demone.
- Il test che non si può fare: ho riprodotto le 40 registrazioni dagli altoparlanti di un altro computer nella stanza e ho ottenuto 3/40, contro 32/40 sugli stessi file letti da disco. Passando da altoparlante, stanza e microfono si somma una seconda riverberazione a quella già registrata, le consonanti spariscono e le trascrizioni diventano «Yeah.» o restano vuote. Una registrazione fatta nella stanza non si può rimisurare riproducendola nella stanza. Con la voce sintetica invece si può, perché è pulita all’origine.
- Soglia del parlante da 0,55 a 0,40: con un’impronta da 7 frasi, misurata sull’ottava tenuta fuori, ottengo 8/8 con 1,5 s di parlato e 7/8 sotto circa 1 s. Sulla sola «Gina» (mezzo secondo) nessun punteggio su 40 supera 0,55, quindi il riconoscimento gira sul comando che segue. A 0,40 prende anche i comandi brevi e resta il doppio del miglior impostore osservato (0,19). Però gli impostori misurati sono voci sintetiche e conversazione di stanza, non una seconda persona reale.
Resta aperto il «girato di lato» a 4/10: l’HAT ha due microfoni e ne uso uno solo, unirli è la strada naturale. La verifica definitiva sarà dire «Gina» dal vivo, non una misura su file. La voce è il prossimo capitolo.


