@@ -370,6 +370,9 @@ class AIService {
370370 // This uses tts_mode=interleaved to stream both text and audio
371371 final responseBuffer = StringBuffer ();
372372 bool audioHeaderSent = false ;
373+ bool hasReceivedAudio = false ;
374+ DateTime ? lastGlassesUpdate;
375+ const glassesUpdateInterval = Duration (milliseconds: 500 );
373376
374377 await for (final event in _chatWidget.sendChatMessageStreamingWithTTS (
375378 transcription,
@@ -378,16 +381,23 @@ class AIService {
378381 case ChatStreamEventType .text:
379382 if (event.text != null ) {
380383 responseBuffer.write (event.text);
381- // Stream text to glasses as it arrives
384+ // Stream accumulated text to glasses progressively (rate limited)
382385 if (_bluetoothManager.isConnected) {
383- // Send partial text updates
384- await _bluetoothManager.sendText (event.text! );
386+ final now = DateTime .now ();
387+ if (lastGlassesUpdate == null ||
388+ now.difference (lastGlassesUpdate! ) > glassesUpdateInterval) {
389+ lastGlassesUpdate = now;
390+ // Send full accumulated text so far
391+ await _bluetoothManager.sendAIResponse (
392+ responseBuffer.toString (),
393+ );
394+ }
385395 }
386396 }
387397 break ;
388398
389399 case ChatStreamEventType .audioHeader:
390- // Audio format info - could be used for local playback setup
400+ // Audio format info - used for watch playback
391401 if (event.sampleRate != null ) {
392402 debugPrint (
393403 'AIService: Audio header - ${event .sampleRate }Hz, ${event .bitsPerSample }bit, ${event .channels }ch' );
@@ -405,10 +415,13 @@ class AIService {
405415
406416 case ChatStreamEventType .audioChunk:
407417 // Stream audio to watch speaker
408- if (event.audioData != null &&
409- audioHeaderSent &&
410- _watchService.isConnected) {
411- await _watchService.sendAudioChunk (event.audioData! );
418+ if (event.audioData != null && audioHeaderSent) {
419+ // Only mark as received if watch is connected and audio is actually played
420+ if (_watchService.isConnected) {
421+ hasReceivedAudio = true ;
422+ await _watchService.sendAudioChunk (event.audioData! );
423+ }
424+ // If watch not connected, audio is not played - fallback TTS will be used
412425 }
413426 break ;
414427
@@ -433,14 +446,20 @@ class AIService {
433446 final fullResponse = responseBuffer.toString ();
434447
435448 if (fullResponse.isNotEmpty) {
436- // Display final response on glasses
449+ // Display final complete response on glasses
437450 if (_bluetoothManager.isConnected) {
438451 await _bluetoothManager.sendAIResponse (fullResponse);
439452 }
440453 // Display on watch
441454 if (_watchService.isConnected) {
442455 await _watchService.displayMessage (fullResponse, durationMs: 10000 );
443456 }
457+ // If no audio was streamed (no watch connected or AGiXT didn't return audio),
458+ // use phone TTS as fallback
459+ if (! hasReceivedAudio && _ttsService.shouldUseTTS ()) {
460+ debugPrint ('AIService: No streaming audio received, using phone TTS' );
461+ await _ttsService.speak (fullResponse);
462+ }
444463 } else {
445464 await _showErrorMessage ('No response from AGiXT' );
446465 }
@@ -683,6 +702,9 @@ class AIService {
683702 // Use streaming TTS to send audio to watch (like ESP32 does)
684703 final responseBuffer = StringBuffer ();
685704 bool audioHeaderSent = false ;
705+ bool hasReceivedAudio = false ;
706+ DateTime ? lastGlassesUpdate;
707+ const glassesUpdateInterval = Duration (milliseconds: 500 );
686708
687709 await for (final event in _chatWidget.sendChatMessageStreamingWithTTS (
688710 message,
@@ -691,9 +713,16 @@ class AIService {
691713 case ChatStreamEventType .text:
692714 if (event.text != null ) {
693715 responseBuffer.write (event.text);
694- // Stream text to glasses as it arrives
716+ // Stream accumulated text to glasses progressively (rate limited)
695717 if (_bluetoothManager.isConnected) {
696- await _bluetoothManager.sendText (event.text! );
718+ final now = DateTime .now ();
719+ if (lastGlassesUpdate == null ||
720+ now.difference (lastGlassesUpdate! ) > glassesUpdateInterval) {
721+ lastGlassesUpdate = now;
722+ await _bluetoothManager.sendAIResponse (
723+ responseBuffer.toString (),
724+ );
725+ }
697726 }
698727 }
699728 break ;
@@ -716,10 +745,13 @@ class AIService {
716745
717746 case ChatStreamEventType .audioChunk:
718747 // Stream audio to watch speaker
719- if (event.audioData != null &&
720- audioHeaderSent &&
721- _watchService.isConnected) {
722- await _watchService.sendAudioChunk (event.audioData! );
748+ if (event.audioData != null && audioHeaderSent) {
749+ // Only mark as received if watch is connected and audio is actually played
750+ if (_watchService.isConnected) {
751+ hasReceivedAudio = true ;
752+ await _watchService.sendAudioChunk (event.audioData! );
753+ }
754+ // If watch not connected, audio is not played - fallback TTS will be used
723755 }
724756 break ;
725757
@@ -750,6 +782,11 @@ class AIService {
750782 if (_watchService.isConnected) {
751783 await _watchService.displayMessage (response, durationMs: 10000 );
752784 }
785+ // If no audio was streamed, use phone TTS as fallback
786+ if (! hasReceivedAudio && _ttsService.shouldUseTTS ()) {
787+ debugPrint ('AIService: No streaming audio received, using phone TTS' );
788+ await _ttsService.speak (response);
789+ }
753790 } else {
754791 await _showErrorMessage ('No response from AGiXT' );
755792 }
@@ -811,6 +848,9 @@ class AIService {
811848 // Use streaming TTS to send audio to watch (like foreground mode)
812849 final responseBuffer = StringBuffer ();
813850 bool audioHeaderSent = false ;
851+ bool hasReceivedAudio = false ;
852+ DateTime ? lastGlassesUpdate;
853+ const glassesUpdateInterval = Duration (milliseconds: 500 );
814854
815855 await for (final event in _chatWidget.sendChatMessageStreamingWithTTS (
816856 message,
@@ -819,6 +859,17 @@ class AIService {
819859 case ChatStreamEventType .text:
820860 if (event.text != null ) {
821861 responseBuffer.write (event.text);
862+ // Stream accumulated text to glasses progressively
863+ if (_bluetoothManager.isConnected) {
864+ final now = DateTime .now ();
865+ if (lastGlassesUpdate == null ||
866+ now.difference (lastGlassesUpdate! ) > glassesUpdateInterval) {
867+ lastGlassesUpdate = now;
868+ await _bluetoothManager.sendAIResponse (
869+ responseBuffer.toString (),
870+ );
871+ }
872+ }
822873 }
823874 break ;
824875
@@ -836,10 +887,13 @@ class AIService {
836887 break ;
837888
838889 case ChatStreamEventType .audioChunk:
839- if (event.audioData != null &&
840- audioHeaderSent &&
841- _watchService.isConnected) {
842- await _watchService.sendAudioChunk (event.audioData! );
890+ if (event.audioData != null && audioHeaderSent) {
891+ // Only mark as received if watch is connected and audio is actually played
892+ if (_watchService.isConnected) {
893+ hasReceivedAudio = true ;
894+ await _watchService.sendAudioChunk (event.audioData! );
895+ }
896+ // If watch not connected, audio is not played - fallback TTS will be used
843897 }
844898 break ;
845899
@@ -862,6 +916,10 @@ class AIService {
862916 if (_watchService.isConnected) {
863917 await _watchService.displayMessage (response, durationMs: 10000 );
864918 }
919+ // Fallback to phone TTS if no streaming audio
920+ if (! hasReceivedAudio && _ttsService.shouldUseTTS ()) {
921+ await _ttsService.speak (response);
922+ }
865923 } else {
866924 await _showErrorMessage ('No response from AGiXT' );
867925 }
0 commit comments