Un agente telefonico in lingua italiana che risponde alle chiamate al posto tuo: dialoga con il chiamante in linguaggio naturale, trasmette in tempo reale il contenuto della conversazione a un’app mobile abbinata (dove puoi lasciarlo proseguire, subentrare o riattaccare), e a chiamata conclusa produce trascrizione, riassunto e classificazione. Interamente open source (AGPL) e sviluppato end-to-end da me: backend, DSP audio, app React Native. L’idea era nata come progetto personale durante Human-Computer Interaction all’OMSCS (Georgia Tech). Avevo prodotto dei prototipi con Figma, l’idea era piaciuta ai professori, ad alcuni miei compagni di corso e mi aveva dato la fiducia necessaria ad imbarcarmi nel progetto.
All’inizio sembrava una passeggiata: SDK di Twilio, un modello realtime collegato in mezzo, fine. Poi ho cominciato ad alzare l’asticella da solo. Non voglio legarmi a Twilio: ok, mi gestisco il WebSocket e astraggo il provider di telefonia. Non voglio dipendere per sempre da OpenAI e dal Realtime: ok, ora serve una factory completa di provider AI. Non posso permettermi una tariffa da API realtime per i miei utenti: ed è qui che è iniziato il caos.
Il turn taking in un contesto telefonico è estremamente difficile. Ho avuto problemi con l’eco, con l’audio telefonico a 8 kHz di bassa qualità, col barge-in, col calcolo dei livelli di energia, con snippet di bassa qualità, e la lista continua. Il caso tipico: il messaggio audio dell’assistente viene bufferizzato e trasmesso, e il rilevamento del parlato scatta o troppo presto, così l’eco dell’assistente apre un turno e interrompe il suo stesso messaggio, o troppo tardi, e si perdono pezzi di quello che l’utente sta dicendo. Ho passato fin troppo tempo a debuggare questi trigger; ne è uscito un gate audio a stati, con doppia soglia di conferma per il barge-in, soglie più severe mentre l’assistente sta parlando, una finestra di assorbimento dell’eco dopo ogni risposta e una soppressione spettrale dell’eco che gira in parallelo.
chiamante ─► Twilio ─► audio in ─► AudioGate ─► STT ─┐
▲ ▲ VAD, doppia │
│ │ soglia, stati ▼
│ eco│ LLM
│ │ frase per │
│ │ frase ▼
└── Twilio ◄────── audio out ◄───────────────── TTS
Il motore Realtime sostituisce l’intero blocco AudioGate → TTS con un unico flusso audio-to-audio.
Sono arrivato a uno stato soddisfacente: meno di un secondo al primo audio grazie al doppio streaming (STT in streaming, generazione e sintesi frase per frase in parallelo), anche se con una naturalezza comunque inferiore all’audio-to-audio di Realtime, a fronte di un costo 4-5 volte più basso. E per me non era un’ottimizzazione: era la differenza tra un prodotto con un futuro e uno senza, perché i costi in token si sarebbero mangiati tutto il margine. La lezione di questo progetto sta qui: i costi, e la complessità di costruire a mano, in una persona sola, una pipeline realtime. I due motori restano comunque intercambiabili dietro un’interfaccia unica, selezionabili per deployment e per singolo utente, con lo stesso orchestratore di chiamata e il registro dei costi per chiamata.
Un dettaglio a cui tengo: la trasparenza richiesta dall’AI Act (art. 50) non è un disclaimer appiccicato, ma uno stato della macchina a stati del barge-in. Durante la riproduzione della disclosure iniziale (“questa è un’intelligenza artificiale…”) l’interruzione è bloccata, così l’avviso arriva per intero a ogni chiamante, e l’utente non può disattivarlo. Anche la scelta dei formati segue questa filosofia di ridurre le occasioni di errore: Twilio parla μ-law a 8 kHz e il modello viene configurato nello stesso formato, zero transcodifiche nel percorso di chiamata.
Sul fronte spam, lo screening del numero prima ancora di rispondere l’ho accantonato: ero entusiasta di farlo ed era tecnicamente fattibile, ma somiglia troppo a una schedatura del chiamante e non era legalmente difendibile (oltre che forse poco etico). Restano il comportamento dinamico sui temi segnalati come spam dall’utente e la blocklist personale come scudo. Nel frattempo il progetto è scivolato da “filtro antispam” a qualcosa di più simile a un assistente vero e proprio, con l’estrazione di entità dalle conversazioni e gli agganci al calendario (predisposti, non ancora collegati).
Prima di considerarlo rilasciabile ho fatto una cosa che consiglio a chiunque sviluppi da solo: una security review formale del repo, con tanto di score, lista di MUST FIX bloccanti (validazione degli input, prompt injection via istruzioni custom, logging di dati sensibili) e gate esplicito sulla produzione. L’infrastruttura era già impostata in quella direzione: container non-root con filesystem in sola lettura e capabilities ridotte, rate limiting Redis multilivello con finestre scorrevoli e circuit breaker, CI con Bandit, Semgrep, Trufflehog e Trivy. È un’implementazione funzionante e deployabile, non un prodotto: il repo lo dichiara esplicitamente, disclaimer compreso.