Tutto è partito da un telefono. Per una piccola attività avevo costruito un receptionist AI che risponde alle chiamate, risponde alle domande e propone appuntamenti. Il riconoscimento vocale (Parakeet), la voce (un modello Qwen3-TTS clonato) e il cervello (Claude tramite Claude Code) girano sul mio Mac, non in un servizio cloud per la voce. A un certo punto è arrivata la domanda ovvia: perché non usare la stessa tecnica per me, su questo sito?
L'idea
Non volevo un fastidioso pop-up di chat, ma qualcosa di discreto: una piccola icona rotonda in basso a destra in ogni pagina, nella lingua della pagina (DE, EN, IT). Un clic apre una finestra con un mio ritratto animato che respira, sbatte le ciglia e muove la bocca quando parla. L'assistente risponde a domande su di me e sul contenuto del sito, e se vuoi puoi chiamarlo direttamente dal browser. Dice fin dall'inizio di essere un'AI e non inventa: quello che non è sul sito non lo sa, e allora rimanda al modulo di contatto.
Come funziona
Browser (widget)
│ wss://daniele-michelin.com/assistent/ws
▼
nginx sul server ──► il mio Mac a casa
├─ riconoscimento vocale + voce (locali)
└─ cervello (Claude tramite Claude Code)
▲
└── impostazioni: il Mac le legge dal sito ogni minuto (token)
Il browser non parla mai direttamente con il mio Mac. Parla con il mio dominio e nginx inoltra la connessione:
location = /assistent/ws {
proxy_pass http://IL-MIO-MAC:5070/ospite/ws$is_args$args;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
Così la content security policy della pagina resta rigida (connect-src 'self') e nessun visitatore vede l'indirizzo di casa mia.
Tutte le impostazioni stanno in una pagina admin protetta del sito: widget, tono, conoscenza, voce, limiti, appuntamenti, mail. Il Mac le legge ogni minuto con un token. Non volevo un canale attraverso cui il sito può cambiare qualcosa sul mio computer: è il Mac che chiede, mai il contrario.
Quando il Mac è spento
Un Mac in casa non è un data center. Se è spento o occupato, il widget passa a una chat di solo testo a cui risponde il sito stesso. Dietro non c'è volutamente nessun modello AI e nessuna terza parte: le risposte sono scritte a partire dai contenuti pubblici del sito e scelte per parole chiave. È meno flessibile, ma onesto e non esce nulla dal server. Le mie coppie di domanda e risposta le aggiungo dall'admin.
Appuntamenti e messaggi
L'assistente può fissare una conversazione con me. Gli orari liberi derivano dalle mie fasce di disponibilità, meno gli appuntamenti già presi e gli impegni nel mio calendario Nextcloud (via CalDAV; solo lettura per l'“occupato”, e gli appuntamenti prenotati li scrivo lì). C'è anche un feed ICS a cui abbonarsi e le mail di conferma. I messaggi per me funzionano esattamente come il modulo di contatto: stesso destinatario, stessa grafica, stesso invio.
Qui ho trovato un bug che mi ha fatto riflettere. In un test l'assistente ha detto “appuntamento confermato” senza che nulla fosse stato prenotato: il modello non aveva attivato l'azione e aveva comunque trovato le parole giuste. Da allora decide il sistema, non il modello. Una prenotazione resta in sospeso, il riepilogo lo dice il sistema stesso e solo dopo un “sì” esplicito si prenota. Anche l'esito lo comunica il sistema: “fatto”, “quell'orario è appena stato preso” oppure un errore. Non si dovrebbe mai lasciare a un modello linguistico il compito di dire se un'azione è riuscita.
Piccole cose che fanno la differenza
- Se per 30 secondi nessuno dice nulla, l'assistente riattacca e libera la linea. La chat continua a funzionare: la domanda successiva si ricollega da sola, senza salutare di nuovo e con la conversazione finora come contesto.
- Con il modello linguistico parla una sola persona alla volta; gli altri vedono un gentile “torno subito” oppure passano alla modalità testo.
- Per la voce si clona solo chi è d'accordo. L'assistente ora parla con la mia voce clonata (una registrazione in tedesco di 16 secondi che funziona anche per italiano e inglese), e l'ho registrata io.
Privacy
Il contenuto delle conversazioni non viene salvato; nei log restano al massimo l'orario e la lunghezza di un messaggio. L'audio non lascia il mio Mac. Il testo riconosciuto oggi va al modello linguistico (Claude): è il prezzo della qualità. L'indirizzo IP di un visitatore sta solo in memoria, per un limite giornaliero. I dati degli appuntamenti (nome, e-mail, motivo, orario) sono in un file non pubblico e vengono cancellati automaticamente 90 giorni dopo l'appuntamento. Nella pagina privacy la sezione sull'assistente compare solo quando la funzione è accesa, e i paragrafi su appuntamenti e messaggi solo quando sono attivi.
Cosa è andato storto
- Il microfono funziona, l'audio no. Avevo creato il contesto audio solo dopo una richiesta di rete. Safari, e a volte anche Chrome, lasciano “sospeso” un contesto creato così. Il microfono andava, l'assistente mi capiva, ma io non sentivo nulla. Va creato in modo sincrono dentro il gestore del clic.
- Le porte 5060 e 5061. Sono porte SIP e
fetche i browser le rifiutano con “bad port”. Le avevo scelte solo perché erano libere. - La cache. Il mio
main.jsha una cache immutabile di un anno. Dopo la modifica ero l'unico a non vedere l'assistente. Ora la versione del widget arriva da un piccolo file di configurazione letto a ogni caricamento di pagina. - La licenza. Il sistema vocale da cui sono partito è un fork di un progetto senza licenza. Quindi non posso pubblicarlo e il mio fork resta privato.
Il codice è aperto
Per questo il repository pubblico contiene solo il mio codice: il widget, gli endpoint PHP e la pagina admin, la configurazione di nginx, la logica del motore (limiti, il flusso di prenotazione gestito dal sistema) e un piccolo motore di riferimento solo testo con un cervello sostituibile, per impostazione predefinita un Ollama locale. La pipeline vocale vera e propria (riconoscimento e voce clonata) non è inclusa; il protocollo che parla è documentato, quindi si può collegare qualsiasi stack vocale. È sotto licenza MIT, con documentazione in inglese, screenshot, un diagramma dell'architettura e una demo con un solo comando: github.com/Mikweb2025-design/ai-site-assistant.
Cosa voglio ancora raggiungere: il cervello in locale
Oggi il testo riconosciuto va a Claude. Il mio obiettivo è che non esca più nulla dal Mac: un modello linguistico che giri solo in locale. Stato onesto: nei miei test precedenti i piccoli modelli locali erano troppo lenti e troppo deboli per una conversazione a velocità telefonica. Adesso voglio provare modelli piccoli più recenti su Apple Silicon (MLX) e rendere il cervello sostituibile dietro un'interfaccia compatibile con OpenAI. Per questo nella roadmap del repository c'è un'opzione Ollama / compatibile OpenAI. Se trovo un modello che risponde abbastanza in fretta e resta sui fatti, ne scriverò.
Un primo giro l'ho già fatto con il motore di riferimento e il testo vero di questo sito come conoscenza. Un modello da 2 miliardi di parametri, a caldo, risponde in 2-6 secondi, ha risposto bene a una domanda in tedesco e ha rifiutato correttamente una domanda fuori tema. I modelli da 4B e 12B che avevo installato hanno impiegato oltre 30 secondi a risposta con il prompt lungo: troppo lenti. La direzione è chiara: prompt più corti (solo il pezzo di conoscenza pertinente invece dell'intera pagina), streaming frase per frase e un piccolissimo classificatore davanti per i saluti. I numeri e lo script per ripeterli sulla propria macchina sono nel repository (docs/roadmap.md).