Blog
AI/Voice
10 dicembre 20258 min

Pipeline vocale in tempo reale: dall'audio grezzo all'IA

Punti chiave: Costruire una pipeline vocale in tempo reale significa concatenare Deepgram STT (streaming con VAD), un LLM tramite OpenRouter (function calling) e OpenAI TTS — il tutto connesso da un WebSocket autenticato JWT. Questo articolo descrive in dettaglio ogni fase, le latenze, i fallback e i bug che mi sono costati notti insonni.

Il cuore di TAMSIV è la voce. Non un gadget, non un pulsante microfono nascosto in un angolo. La voce È l'interfaccia principale. Tu premi, tu parli, l'IA capisce ed esegue. Ma costruire una pipeline vocale in tempo reale da solo significa entrare in un mondo dove ogni millisecondo conta e dove tutto può rompersi in qualsiasi momento.

Dopo tre settimane di sviluppo intensivo e una quantità irragionevole di caffè, ho una pipeline che risponde in 1,5-3 secondi. Ecco come funziona, pezzo per pezzo.

Onde sonore che si propagano in un ambiente scuro con particelle di luce blu e ciano
Dall'onda sonora alla risposta strutturata: ogni millisecondo conta.

Come funziona l'architettura della pipeline vocale?

La pipeline completa in una riga:

Audio PCM 16kHz mono → WebSocket (JWT) → Deepgram Live STT (VAD) → OpenRouter LLM → Function calling → OpenAI TTS → Risposta vocale

Sei fasi, sei potenziali punti di fallimento. Ognuna ha i suoi vincoli, le sue latenze e le sue insidie. Il tutto deve concatenarsi in meno di 3 secondi affinché l'esperienza sia fluida. Oltre, l'utente pensa che l'app si sia bloccata.

Decomponiamo ogni fase.

Perché il WebSocket è indispensabile per l'audio in tempo reale?

L'HTTP classico non funziona per lo streaming audio. Dovresti inviare l'intera registrazione in un blocco, attendere l'elaborazione, quindi ricevere la risposta. La latenza sarebbe inaccettabile.

Il WebSocket consente un flusso bidirezionale continuo: il telefono invia blocchi audio mentre l'utente parla, e il backend può iniziare a elaborare prima ancora che l'utente abbia finito.

L'autenticazione JWT

Ogni connessione WebSocket è autenticata tramite un token JWT Supabase:

ws://backend:3001?token=eyJhbGciOiJIUzI1NiIs...

Il token è valido alla connessione. Se il token scade durante la conversazione (i token Supabase scadono dopo 1 ora), il client rileva la disconnessione e si riconnette automaticamente con un token fresco. Ho dovuto gestire questo caso esplicitamente — all'inizio, le conversazioni lunghe si bloccavano misteriosamente.

La sicurezza del WebSocket è dettagliata nell'articolo sull'audit di sicurezza e il rate limiting.

Come Deepgram gestisce lo Speech-to-Text in streaming?

L'audio deve essere in PCM 16 bit, 16kHz, mono. Il telefono cattura l'audio in questo formato e invia blocchi binari grezzi tramite il WebSocket. Nessuna compressione, nessuna codifica — il PCM grezzo è il formato più veloce da elaborare.

Deepgram riceve questi blocchi e trascrive in streaming. Ma la vera magia è il VAD (Voice Activity Detection).

Perché il VAD cambia tutto?

Senza VAD, è necessario implementare un timeout di silenzio lato client: se l'utente non parla per X secondi, si considera che abbia finito. Il problema:

  • Troppo corto (1s): tagli l'utente che riflette tra due frasi.
  • Troppo lungo (3s): l'app rallenta, l'utente aspetta.
  • Variabile a seconda dell'utente: alcuni parlano velocemente, altri si prendono il loro tempo.

Il VAD di Deepgram rileva quando l'utente ha finito di parlare con una precisione notevole. Analizza il segnale audio in tempo reale e invia un evento speech_final quando è certo che l'utente abbia terminato. Ci vogliono circa 200ms dopo la fine del parlato.

I risultati intermedi vs finali

Deepgram invia due tipi di risultati:

  • is_final: false — Risultati intermedi, instabili. La parola rilevata può cambiare man mano che il contesto si arricchisce.
  • is_final: true — Risultati confermati. Il testo non cambierà più.

L'insidia: accumulare i risultati intermedi per visualizzare un'anteprima in tempo reale (ottimo per l'UX) pur trasmettendo solo i risultati finali al LLM (ottimo per la qualità). Visualizzo i risultati intermedi in grigio e quelli finali in bianco — l'utente vede la sua voce trasformarsi in testo in diretta.

Per il confronto tra STT nativo (gratuito, sul dispositivo) e Deepgram cloud (più preciso), leggi il mio articolo dettagliato STT nativo vs Deepgram.

Microfono professionale da studio con indicatore LED in un ambiente scuro, illuminazione blu e viola
La cattura audio è la prima fase critica della pipeline.

Come il LLM orchestra le azioni tramite function calling?

La trascrizione completa parte verso OpenRouter con function calling. OpenRouter è un router che dà accesso a oltre 400 modelli LLM con fallback automatico — se il modello principale è inattivo, un fallback subentra in pochi secondi.

Il LLM riceve la trascrizione e deve comprendere l'intenzione dell'utente. Dispone di 7 funzioni:

  • create_task — Crea un'attività
  • update_task — Modifica un'attività esistente
  • create_memo — Crea un memo
  • update_memo — Modifica un memo esistente
  • create_calendar_event — Crea un evento del calendario
  • ask_clarification — Chiedi una precisazione all'utente
  • end_conversation — Termina la conversazione

Il LLM analizza la frase ("ricordami di comprare il pane domani alle 10"), identifica l'azione (create_task), estrae i parametri (titolo, data, ora) e restituisce una chiamata di funzione strutturata. Il backend esegue l'azione e restituisce il risultato al frontend.

Il pattern PendingCreation è cruciale qui: il backend crea un'anteprima dell'elemento, e l'utente può convalidare, modificare o annullare prima del salvataggio definitivo nel database. Zero brutte sorprese.

La latenza LLM

Il LLM rappresenta la maggior parte della latenza: tra 800ms e 2 secondi a seconda del modello e della complessità della richiesta. È qui che la scelta del modello tramite OpenRouter fa la differenza — un modello veloce ma meno preciso vs un modello lento ma più affidabile. TAMSIV utilizza un modello configurabile con fallback automatico se il modello principale è troppo lento o non disponibile.

Come OpenAI TTS genera la risposta vocale?

Una volta eseguita l'azione, il backend genera una risposta testuale ("È annotato! Ho creato l'attività 'Comprare il pane' per domani alle 10"). Questo testo parte verso OpenAI TTS con la voce nova.

L'audio viene trasmesso in streaming in ritorno tramite lo stesso WebSocket. Il frontend inizia a riprodurre i primi blocchi audio, senza attendere la risposta completa. Ciò riduce la latenza percepita di circa 500ms — l'utente sente l'inizio della risposta mentre la fine è ancora in fase di generazione.

Per la personalizzazione vocale e la scelta della voce TTS, ne parlo nell'articolo sulla personalizzazione vocale.

Quali sono le tre modalità WebSocket di TAMSIV?

Nel corso dello sviluppo, sono emerse tre modalità WebSocket:

  1. LiveWebSocketServer (predefinito) — STT nativo del dispositivo + Deepgram fallback, orchestrazione LLM, OpenAI TTS. Questa è la modalità standard, la più economica.
  2. RealtimeWebSocketServer — API OpenAI Realtime, bidirezionale, bassa latenza. Più costosa ma più fluida per le conversazioni lunghe.
  3. WebSocketServer — Batch STT/TTS, modalità legacy. Utilizzata per i casi in cui lo streaming non è necessario.

La modalità è selezionabile lato amministratore tramite la tabella app_config in Supabase. Ciò consente di passare da una modalità all'altra senza distribuire una nuova versione dell'app.

Cavi in fibra ottica luminosi blu e verdi in un data center, visualizzazione dei flussi di dati
I dati attraversano la pipeline alla velocità della luce — in teoria.

Come gestire gli errori in una pipeline in tempo reale?

La regola d'oro: tutto può fallire in qualsiasi momento. Deepgram potrebbe essere inattivo. OpenRouter potrebbe andare in timeout. OpenAI TTS potrebbe restituire un errore 429. La connessione WebSocket potrebbe interrompersi nel bel mezzo di una conversazione.

Ecco i meccanismi di resilienza:

  • Retry intelligenti: Ogni fase ha un numero di retry configurato con backoff esponenziale. Nessun retry in un ciclo infinito.
  • Circuit breakers: Se un servizio fallisce troppo spesso, smettiamo di chiamarlo per X secondi per evitare di sovraccaricare un servizio già in difficoltà.
  • Fallback ad ogni fase: STT nativo se Deepgram è inattivo, modello LLM alternativo tramite OpenRouter, risposta testuale se TTS fallisce.
  • AlertService: Ogni fallback attiva un'email di avviso (tramite Resend) + log Supabase. So in tempo reale quando qualcosa si degrada.

Ogni riga di gestione degli errori rappresenta un bug riscontrato in produzione. La pipeline è robusta oggi, ma ci sono volute decine di sessioni di debug per arrivarci.

Qual è il budget di latenza di ogni fase?

Decomposizione di un'interazione vocale completa:

  • Cattura audio + invio WebSocket: ~50ms (trascurabile)
  • Deepgram STT + VAD: ~200-400ms dopo la fine del parlato
  • OpenRouter LLM (function calling): ~800ms-2000ms (variabile)
  • OpenAI TTS (primo chunk): ~300-500ms

Totale: da 1,3 a 3 secondi. L'obiettivo è rimanere sotto i 2 secondi per il 90% delle interazioni. Oltre, l'esperienza diventa frustrante.

Lo streaming TTS è la leva migliore: l'utente sente l'inizio della risposta dopo circa 1,5 secondi in media, anche se la generazione completa richiede 3 secondi. La latenza percepita è molto inferiore alla latenza reale.

Quali lezioni trarre per costruire la tua pipeline vocale?

Dopo tre settimane di sviluppo intensivo, ecco cosa consiglierei:

  1. Inizia con il WebSocket: È la spina dorsale. Se il WebSocket è solido, il resto si incastra.
  2. Usa il VAD del provider STT: Non implementare la tua rilevazione del silenzio — è un abisso di complessità per un risultato inferiore.
  3. Trasmetti tutto in streaming: STT in streaming, TTS in streaming. Ogni millisecondo risparmiato migliora l'UX.
  4. Prevedi i fallback dal primo giorno: Non in modalità "vedrò più tardi". Ogni fase deve avere un piano B.
  5. Misura la latenza in produzione: I benchmark locali sono ingannevoli. La latenza reale dipende dalla rete, dal carico del server e dalla posizione geografica. La dashboard admin mi è stata indispensabile per questo.

FAQ

Perché Deepgram piuttosto che Google Speech-to-Text o AWS Transcribe?

Deepgram offre il miglior rapporto qualità/latenza per lo streaming. Google STT è eccellente ma più costoso e più lento in modalità streaming. AWS Transcribe è robusto ma l'integrazione WebSocket è più complessa. Deepgram ha anche un VAD integrato, il che semplifica enormemente il codice.

La pipeline funziona in modalità offline?

Lo STT nativo del dispositivo funziona offline. Ma il LLM e il TTS richiedono una connessione internet. In modalità offline, TAMSIV consente l'inserimento di testo classico e mette in coda le richieste vocali per quando la connessione torna.

Quanto costa un'interazione vocale completa?

Con lo STT nativo (gratuito), un LLM economico tramite OpenRouter (~$0.001-0.01) e OpenAI TTS (~$0.015/1000 caratteri), un'interazione costa tra $0.01 e $0.03. Il dettaglio dei costi è nell'articolo retrospettivo sui 650 commit.

Si può sostituire OpenAI TTS con una soluzione open-source?

Tecnicamente sì. Progetti come Coqui TTS o Bark producono risultati corretti. Ma la qualità della voce "nova" di OpenAI rimane superiore, e lo streaming è meglio supportato. Per un progetto in produzione, il costo aggiuntivo di OpenAI TTS ($15/milione di caratteri) si giustifica con la qualità.

Come gestire più lingue nella pipeline vocale?

Deepgram supporta il rilevamento automatico della lingua. Il LLM tramite OpenRouter è naturalmente multilingue. Il TTS OpenAI genera audio nella lingua del testo fornito. TAMSIV supporta 6 lingue (FR, EN, DE, ES, IT, PT) senza configurazione specifica per lingua nella pipeline. Il dettaglio dell'internazionalizzazione è nell'articolo sull'i18n in 6 lingue.