@@ -69,20 +69,10 @@ export function useVoiceChat() {
6969 const audioPlayerRef = useRef < AudioPlayer | null > ( null ) ;
7070 const recordingIntervalRef = useRef < NodeJS . Timeout | null > ( null ) ;
7171 const shouldAutoStartRecordingRef = useRef < boolean > ( false ) ;
72- const agentEndedRef = useRef < boolean > ( true ) ; // true = agent ha finito, possiamo registrare
72+ const agentEndedRef = useRef < boolean > ( true ) ;
7373 const isMutedRef = useRef < boolean > ( false ) ;
74- const isManuallyMutedRef = useRef < boolean > ( false ) ; // Distingue tra mute manuale e automatico
7574 const isStartingRecordingRef = useRef < boolean > ( false ) ; // Previene avvii concorrenti di registrazione
76- const isReceivingAudioRef = useRef < boolean > ( false ) ; // true quando stiamo ricevendo chunk audio dal server
7775 const isMountedRef = useRef < boolean > ( true ) ; // Guard per evitare setState dopo unmount
78- // Quando true il mic è acceso ma i frame NON vengono inviati al server.
79- // Viene usato subito dopo la fine di una risposta: il mic resta pronto localmente
80- // ma non invia nulla finché il server non segnala che è pronto ad ascoltare
81- // (speech_started o nuovo agent_start). Evita il loop interrupted causato da
82- // audio/silenzio inviato nella finestra tra audio_end e il prossimo turno.
83- const isAudioGatedRef = useRef < boolean > ( false ) ;
84- const consecutiveEmptyAgentsRef = useRef < number > ( 0 ) ; // Contatore cicli agente vuoti consecutivi
85- const agentHadAudioRef = useRef < boolean > ( false ) ; // True se l'agente corrente ha prodotto audio
8676
8777 /**
8878 * Verifica e richiede i permessi audio
@@ -196,250 +186,67 @@ export function useVoiceChat() {
196186
197187 switch ( phase ) {
198188 case 'speech_started' :
199- // Utente ha iniziato a parlare (VAD di OpenAI): apri il gate audio.
200- // Da questo momento i frame vengono inviati al server.
201- consecutiveEmptyAgentsRef . current = 0 ; // Reset: l'utente sta davvero parlando
202- isAudioGatedRef . current = false ;
189+ // VAD OpenAI: utente sta parlando
203190 setState ( 'recording' ) ;
204191 break ;
205192
206193 case 'speech_stopped' :
207- // Utente ha finito di parlare (VAD di OpenAI)
208- // IMPORTANTE: Fermiamo il microfono QUI e non lo riattiveremo
209- // finché l'agent non ha completato TUTTO (elaborazione + riproduzione)
210-
211- // Auto-mute: ferma la registrazione
212- if ( audioRecorderRef . current ?. isCurrentlyRecording ( ) ) {
213- audioRecorderRef . current . stopRecording ( ) . catch ( err => {
214- console . error ( 'Errore fermando registrazione su speech_stopped:' , err ) ;
215- } ) ;
216- if ( recordingIntervalRef . current ) {
217- clearInterval ( recordingIntervalRef . current ) ;
218- recordingIntervalRef . current = null ;
219- }
220- setRecordingDuration ( 0 ) ;
221- }
222-
223- // Aggiorna UI del mute (solo se non è mutato manualmente)
224- if ( ! isManuallyMutedRef . current ) {
225- setIsMuted ( true ) ;
226- isMutedRef . current = true ;
227- }
228-
194+ // VAD OpenAI: utente ha smesso di parlare — il mic resta acceso,
195+ // il server gate blocca i frame finché OpenAI non è pronto ad ascoltare
229196 setState ( 'processing' ) ;
230197 break ;
231198
232199 case 'agent_start' :
233- console . log ( '[useVoiceChat] Inizio risposta chatbot' ) ;
234200 setState ( 'processing' ) ;
235- agentEndedRef . current = false ; // Agent sta elaborando
236- agentHadAudioRef . current = false ; // Reset: nuovo agente, nessun audio ancora
237- // Gate: mentre l'agent elabora il mic è fisicamente fermo (stopRecording sotto),
238- // quindi il gate non è rilevante qui. Viene chiuso al riavvio del mic dopo agent_end.
239-
240- // Auto-mute (safety check): assicuriamoci che il microfono sia fermato
241- if ( audioRecorderRef . current ?. isCurrentlyRecording ( ) ) {
242- audioRecorderRef . current . stopRecording ( ) . catch ( err => {
243- console . error ( 'Errore fermando registrazione su agent_start:' , err ) ;
244- } ) ;
245- if ( recordingIntervalRef . current ) {
246- clearInterval ( recordingIntervalRef . current ) ;
247- recordingIntervalRef . current = null ;
248- }
249- setRecordingDuration ( 0 ) ;
250- }
251-
252- // Aggiorna UI del mute (solo se non è mutato manualmente)
253- if ( ! isManuallyMutedRef . current ) {
254- setIsMuted ( true ) ;
255- isMutedRef . current = true ;
256- }
201+ agentEndedRef . current = false ;
257202 break ;
258203
259204 case 'agent_end' :
260- // Agent ha finito di elaborare
261205 agentEndedRef . current = true ;
262-
263- // IMPORTANTE: Non riattivare il microfono se:
264- // 1. Ci sono chunk audio in coda o in fase di processamento
265- // 2. L'audio player sta ATTIVAMENTE riproducendo
266- // 3. Stiamo ancora ricevendo audio dal server
267- const hasPending = audioPlayerRef . current ?. hasPendingOrQueuedChunks ( ) ;
268- const isPlaying = audioPlayerRef . current ?. isCurrentlyPlaying ( ) ;
269-
270- if ( ! hasPending && ! isPlaying && ! isReceivingAudioRef . current ) {
206+ // Se non ci sono chunk audio pendenti, torna subito a ready
207+ if ( ! audioPlayerRef . current ?. hasPendingOrQueuedChunks ( ) &&
208+ ! audioPlayerRef . current ?. isCurrentlyPlaying ( ) ) {
271209 console . log ( '[useVoiceChat] Fine risposta chatbot' ) ;
272210 setState ( 'ready' ) ;
273-
274- // Auto-unmute: riattiva microfono (solo se non mutato manualmente)
275- if ( ! isManuallyMutedRef . current ) {
276- setIsMuted ( false ) ;
277- isMutedRef . current = false ;
278-
279- // Controlla se l'agente ha prodotto audio (risposta legittima) o era vuoto (ciclo spurio).
280- // Un ciclo agente senza audio indica una fase di pianificazione tool call:
281- // il modello ha deciso cosa fare ma non ha ancora parlato. Riavviare il mic
282- // in questo momento crea una finestra (~374ms) in cui il mic invia audio
283- // mentre il tool è in esecuzione → VAD trigge → risposta del secondo
284- // ciclo interrotta prima ancora di partire.
285- // Fix: al primo ciclo senza audio non riavviare il mic; il secondo ciclo
286- // (con audio) gestirà correttamente il riavvio.
287- if ( ! agentHadAudioRef . current ) {
288- consecutiveEmptyAgentsRef . current ++ ;
289- } else {
290- consecutiveEmptyAgentsRef . current = 0 ;
291- }
292-
293- // Non riavviare il mic se questo ciclo agente non ha prodotto audio.
294- // Copre sia i cicli di pianificazione tool (1 ciclo vuoto) sia eventuali
295- // cicli spurii multipli consecutivi.
296- if ( consecutiveEmptyAgentsRef . current >= 1 ) {
297- consecutiveEmptyAgentsRef . current = 0 ;
298- // Non riavviare il mic: l'agente sta ancora elaborando (tool call in corso).
299- // Il ciclo successivo con audio gestirà il riavvio.
300- break ;
301- }
302-
303- isAudioGatedRef . current = true ;
304-
305- setTimeout ( ( ) => {
306- if ( audioRecorderRef . current && websocketRef . current ?. isReady ( ) ) {
307- startRecording ( ) ;
308- setTimeout ( ( ) => { isAudioGatedRef . current = false ; } , 400 ) ;
309- }
310- } , 100 ) ;
311- }
312- } else {
313211 }
314212 break ;
315213
316214 case 'audio_end' :
317- // Server ha finito di inviare chunk audio per questo segmento.
318- // Segna che non arriveranno altri chunk.
319- isReceivingAudioRef . current = false ;
320-
321- // Con TrackPlayer la riproduzione è già in corso in streaming.
322- // Controlliamo sia i chunk processati che quelli ancora pending.
323- if ( audioPlayerRef . current && audioPlayerRef . current . hasPendingOrQueuedChunks ( ) ) {
215+ if ( audioPlayerRef . current ?. hasPendingOrQueuedChunks ( ) ) {
324216 setState ( 'speaking' ) ;
325217 audioPlayerRef . current . signalAllChunksReceived ( ( ) => {
326- // Riattiva il microfono SOLO se l'agent ha finito completamente
327218 if ( agentEndedRef . current ) {
328219 console . log ( '[useVoiceChat] Fine risposta chatbot' ) ;
329220 setState ( 'ready' ) ;
330-
331- // Auto-unmute: riattiva microfono (solo se non mutato manualmente)
332- if ( ! isManuallyMutedRef . current ) {
333- setIsMuted ( false ) ;
334- isMutedRef . current = false ;
335-
336- // Chiudi il gate brevemente: nei primi 400ms dopo la fine della risposta
337- // non inviamo audio per evitare il loop "interrupted".
338- // Dopo 400ms apriamo il gate automaticamente: il server è pronto ad
339- // ascoltare e speech_started potrebbe non arrivare mai se non inviamo audio.
340- isAudioGatedRef . current = true ;
341-
342- // Riavvia registrazione dopo un breve delay
343- setTimeout ( ( ) => {
344- if ( audioRecorderRef . current && websocketRef . current ?. isReady ( ) ) {
345- startRecording ( ) ;
346- setTimeout ( ( ) => { isAudioGatedRef . current = false ; } , 400 ) ;
347- }
348- } , 100 ) ;
349- }
350221 } else {
351- // Agent non ha ancora finito, torna in processing
352- // e aspetta altri chunk audio o agent_end
353222 setState ( 'processing' ) ;
354223 }
355224 } ) ;
356225 } else if ( agentEndedRef . current ) {
357- // Nessun audio da riprodurre e agent finito, torna pronto
358226 console . log ( '[useVoiceChat] Fine risposta chatbot' ) ;
359227 setState ( 'ready' ) ;
360-
361- // Auto-unmute: riattiva microfono (solo se non mutato manualmente)
362- if ( ! isManuallyMutedRef . current ) {
363- setIsMuted ( false ) ;
364- isMutedRef . current = false ;
365-
366- isAudioGatedRef . current = true ;
367-
368- setTimeout ( ( ) => {
369- if ( audioRecorderRef . current && websocketRef . current ?. isReady ( ) ) {
370- startRecording ( ) ;
371- setTimeout ( ( ) => { isAudioGatedRef . current = false ; } , 400 ) ;
372- }
373- } , 100 ) ;
374- }
375228 }
376229 break ;
377230
378231 case 'interrupted' :
379- // Risposta interrotta dall'utente, torna pronto
380- agentEndedRef . current = true ; // Reset
381- isReceivingAudioRef . current = false ; // Reset
382- agentHadAudioRef . current = false ; // Reset
383-
384- // IMPORTANTE: ferma PRIMA il microfono (con await) per evitare che il server
385- // riceva audio tra interrupted e il prossimo agent_start.
386- // Il await è critico: senza di esso il mic non è ancora fermo quando il
387- // setTimeout da 400ms scatta, causando un riavvio quasi immediato.
388- if ( audioRecorderRef . current ?. isCurrentlyRecording ( ) ) {
389- try {
390- await audioRecorderRef . current . stopRecording ( ) ;
391- } catch ( err ) {
392- console . error ( '[useVoiceChat] Errore fermando registrazione su interrupted:' , err ) ;
393- }
394- if ( recordingIntervalRef . current ) {
395- clearInterval ( recordingIntervalRef . current ) ;
396- recordingIntervalRef . current = null ;
397- }
398- setRecordingDuration ( 0 ) ;
399- }
400- isStartingRecordingRef . current = false ; // Reset mutex per permettere riavvio
401-
232+ agentEndedRef . current = true ;
233+ // Ferma solo la riproduzione audio, il mic resta acceso
402234 if ( audioPlayerRef . current ) {
403235 await audioPlayerRef . current . stopPlayback ( ) ;
404236 audioPlayerRef . current . clearChunks ( ) ;
405237 }
406238 setState ( 'ready' ) ;
407-
408- // Auto-unmute: riattiva microfono (solo se non mutato manualmente)
409- if ( ! isManuallyMutedRef . current ) {
410- setIsMuted ( false ) ;
411- isMutedRef . current = false ;
412-
413- // Chiudi il gate: il mic si riaccende ma non invia audio per 400ms.
414- // Questo previene il loop "interrupted → mic invia silenzio → altro interrupted".
415- // Dopo 400ms dal riavvio del mic il gate si apre automaticamente.
416- isAudioGatedRef . current = true ;
417-
418- setTimeout ( ( ) => {
419- if ( audioRecorderRef . current && websocketRef . current ?. isReady ( ) ) {
420- startRecording ( ) ;
421- setTimeout ( ( ) => { isAudioGatedRef . current = false ; } , 400 ) ;
422- }
423- } , 400 ) ;
424- }
425239 break ;
426240 }
427241 } ,
428242
429243 onAudioChunk : ( audioData : string , chunkIndex : number ) => {
430244 if ( ! isMountedRef . current ) return ;
431245 if ( audioPlayerRef . current ) {
432- // Segna che stiamo ricevendo audio SINCRONAMENTE prima dell'async addChunk
433- isReceivingAudioRef . current = true ;
434- agentHadAudioRef . current = true ; // L'agente corrente ha prodotto audio → risposta legittima
435-
436- // Aggiunge il chunk alla queue di TrackPlayer e avvia riproduzione streaming
437246 audioPlayerRef . current . addChunk ( audioData , chunkIndex ) . catch ( err => {
438247 console . error ( '[useVoiceChat] Errore aggiunta chunk a TrackPlayer:' , err ) ;
439248 } ) ;
440249 setChunksReceived ( prev => prev + 1 ) ;
441-
442- // Transiziona a 'speaking' al primo chunk ricevuto
443250 setState ( prev => prev !== 'speaking' ? 'speaking' : prev ) ;
444251 }
445252 } ,
@@ -529,10 +336,7 @@ export function useVoiceChat() {
529336 setActiveTools ( [ ] ) ;
530337 setChunksReceived ( 0 ) ;
531338 shouldAutoStartRecordingRef . current = true ;
532- agentEndedRef . current = true ; // Reset per nuova sessione
533- isManuallyMutedRef . current = false ; // Reset mute manuale
534- isReceivingAudioRef . current = false ; // Reset audio reception
535- isAudioGatedRef . current = false ; // Gate aperto all'inizio della sessione
339+ agentEndedRef . current = true ;
536340
537341 try {
538342 const connected = await websocketRef . current ! . connect ( ) ;
@@ -580,11 +384,9 @@ export function useVoiceChat() {
580384
581385 try {
582386 // Callback invocato per ogni chunk audio PCM16 a 24kHz.
583- // Se isAudioGatedRef è true, i frame vengono scartati localmente senza inviarli
584- // al server. Il gate si chiude subito dopo la fine di una risposta e si apre
585- // quando il server è pronto ad ascoltare (speech_started o nuovo agent_start).
387+ // Il mic rimane sempre acceso: è il server gate in voice_bridge.py
388+ // a bloccare i frame durante l'elaborazione dell'agente.
586389 const onChunk = ( base64Chunk : string ) => {
587- if ( isAudioGatedRef . current ) return ; // gate chiuso: scarta silenziosamente
588390 try {
589391 const arrayBuffer = base64ToArrayBuffer ( base64Chunk ) ;
590392 websocketRef . current ?. sendAudio ( arrayBuffer ) ;
@@ -695,24 +497,16 @@ export function useVoiceChat() {
695497 const mute = useCallback ( async ( ) : Promise < void > => {
696498 setIsMuted ( true ) ;
697499 isMutedRef . current = true ;
698- isManuallyMutedRef . current = true ; // Marca come mute manuale
699500
700- // Ferma la registrazione se è attiva
701501 if ( audioRecorderRef . current ?. isCurrentlyRecording ( ) ) {
702502 try {
703503 await audioRecorderRef . current . cancelRecording ( ) ;
704-
705- // Pulisci il timer della durata
706504 if ( recordingIntervalRef . current ) {
707505 clearInterval ( recordingIntervalRef . current ) ;
708506 recordingIntervalRef . current = null ;
709507 }
710-
711508 setRecordingDuration ( 0 ) ;
712- // Mantieni lo stato 'ready' invece di tornare a 'recording'
713- if ( state === 'recording' ) {
714- setState ( 'ready' ) ;
715- }
509+ if ( state === 'recording' ) setState ( 'ready' ) ;
716510 } catch ( err ) {
717511 console . error ( 'Errore durante il mute:' , err ) ;
718512 }
@@ -725,15 +519,11 @@ export function useVoiceChat() {
725519 const unmute = useCallback ( async ( ) : Promise < void > => {
726520 setIsMuted ( false ) ;
727521 isMutedRef . current = false ;
728- isManuallyMutedRef . current = false ; // Rimuove il flag di mute manuale
729522
730- // Riavvia la registrazione se siamo in stato 'ready'
731- if ( state === 'ready' && websocketRef . current ?. isReady ( ) ) {
732- setTimeout ( ( ) => {
733- startRecording ( ) ;
734- } , 100 ) ;
523+ if ( websocketRef . current ?. isReady ( ) ) {
524+ setTimeout ( ( ) => startRecording ( ) , 100 ) ;
735525 }
736- } , [ state , startRecording ] ) ;
526+ } , [ startRecording ] ) ;
737527
738528 /**
739529 * Disconnette dal servizio
@@ -776,12 +566,9 @@ export function useVoiceChat() {
776566 setTranscripts ( [ ] ) ;
777567 setActiveTools ( [ ] ) ;
778568 setRecordingDuration ( 0 ) ;
779- setIsMuted ( false ) ; // Reset mute state
569+ setIsMuted ( false ) ;
780570 isMutedRef . current = false ;
781- isManuallyMutedRef . current = false ; // Reset mute manuale
782- isStartingRecordingRef . current = false ; // Reset mutex registrazione
783- isReceivingAudioRef . current = false ; // Reset ricezione audio
784- isAudioGatedRef . current = false ; // Reset gate audio
571+ isStartingRecordingRef . current = false ;
785572 shouldAutoStartRecordingRef . current = false ;
786573 agentEndedRef . current = true ;
787574 } , [ ] ) ;
0 commit comments