Skip to content

Commit 4d27a93

Browse files
committed
different tts method
1 parent 5426a61 commit 4d27a93

File tree

6 files changed

+613
-194
lines changed

6 files changed

+613
-194
lines changed

android/app/proguard-rules.pro

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@
2222
-dontwarn com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener
2323
-dontwarn com.google.android.play.core.tasks.OnFailureListener
2424
-dontwarn com.google.android.play.core.tasks.OnSuccessListener
25-
-dontwarn com.google.android.play.core.tasks.Task
25+
-dontwarn com.google.android.play.core.tasks.Task
26+
27+
# Vosk speech recognition (JNA required)
28+
-keep class com.sun.jna.* { *; }
29+
-keepclassmembers class * extends com.sun.jna.* { public *; }

android/app/src/main/kotlin/dev/agixt/agixt/MainActivity.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ class MainActivity: FlutterActivity() {
133133
override fun onDestroy() {
134134
super.onDestroy()
135135
// Clean up handlers
136-
wakeWordHandler?.destroy()
136+
// wakeWordHandler?.destroy() // Now using Vosk in Flutter
137137
watchHandler?.destroy()
138138
BackgroundService.stopService(this@MainActivity, null)
139139
}
@@ -205,10 +205,12 @@ class MainActivity: FlutterActivity() {
205205
// Mark that the method channels are initialized
206206
methodChannelInitialized = true
207207

208-
// Initialize Voice & Watch handlers
209-
wakeWordHandler = WakeWordHandler(this, binaryMessenger)
210-
wakeWordHandler?.initialize()
208+
// Wake word detection is now handled by Vosk in Flutter (pure Dart)
209+
// Native WakeWordHandler is no longer needed
210+
// wakeWordHandler = WakeWordHandler(this, binaryMessenger)
211+
// wakeWordHandler?.initialize()
211212

213+
// Initialize Watch handler for Pixel Watch support
212214
watchHandler = WatchHandler(this, binaryMessenger)
213215
watchHandler?.initialize()
214216

lib/models/agixt/widgets/agixt_chat.dart

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:convert';
23
import 'package:agixt/models/agixt/auth/auth.dart';
34
import 'package:agixt/models/agixt/calendar.dart';
@@ -12,6 +13,7 @@ import 'package:agixt/services/secure_storage_service.dart';
1213
import 'package:agixt/services/location_service.dart'; // Import LocationService
1314
import 'package:agixt/services/client_commands_service.dart'; // Import ClientSideTools
1415
import 'package:device_calendar/device_calendar.dart';
16+
import 'package:flutter/foundation.dart';
1517
import 'package:flutter/material.dart';
1618
import 'package:geolocator/geolocator.dart'; // Import Geolocator
1719
import 'package:hive/hive.dart';
@@ -232,6 +234,156 @@ class AGiXTChatWidget implements AGiXTWidget {
232234
}
233235
}
234236

237+
/// Send chat message with streaming response
238+
/// Returns a stream of response chunks as they arrive
239+
Stream<String> sendChatMessageStreaming(String message) async* {
240+
try {
241+
final jwt = await AuthService.getJwt();
242+
if (jwt == null) {
243+
yield "Please login to use AGiXT chat.";
244+
return;
245+
}
246+
247+
final conversationId = await _getCurrentConversationId();
248+
debugPrint('Using conversation ID for streaming chat: $conversationId');
249+
250+
// Build context data with timeout
251+
String contextData = '';
252+
try {
253+
contextData = await _buildContextData().timeout(
254+
const Duration(seconds: 3),
255+
onTimeout: () {
256+
debugPrint('Context building timed out');
257+
return '';
258+
},
259+
);
260+
} catch (e) {
261+
debugPrint('Error building context data: $e');
262+
}
263+
264+
// Get available tools
265+
final availableTools = await ClientSideTools.getToolDefinitions();
266+
267+
final Map<String, dynamic> requestBody = {
268+
"model": await _getAgentName(),
269+
"messages": [
270+
{
271+
"role": "user",
272+
"content": message,
273+
if (contextData.isNotEmpty) "context": contextData,
274+
},
275+
],
276+
"user": conversationId,
277+
"stream": true, // Enable streaming!
278+
if (availableTools.isNotEmpty) "tools": availableTools,
279+
if (availableTools.isNotEmpty) "tool_choice": "auto",
280+
};
281+
282+
// Create streaming request
283+
final client = http.Client();
284+
final request = http.Request(
285+
'POST',
286+
Uri.parse('${AuthService.serverUrl}/v1/chat/completions'),
287+
);
288+
request.headers.addAll({
289+
'Content-Type': 'application/json',
290+
'Authorization': 'Bearer $jwt',
291+
'Accept': 'text/event-stream',
292+
});
293+
request.body = jsonEncode(requestBody);
294+
295+
debugPrint('Sending streaming chat request...');
296+
final streamedResponse = await client.send(request);
297+
298+
if (streamedResponse.statusCode == 200) {
299+
String buffer = '';
300+
String fullResponse = '';
301+
String? newConversationId;
302+
303+
await for (final chunk in streamedResponse.stream.transform(utf8.decoder)) {
304+
buffer += chunk;
305+
306+
// Process complete SSE events (lines starting with "data: ")
307+
while (buffer.contains('\n')) {
308+
final newlineIndex = buffer.indexOf('\n');
309+
final line = buffer.substring(0, newlineIndex).trim();
310+
buffer = buffer.substring(newlineIndex + 1);
311+
312+
if (line.isEmpty) continue;
313+
if (!line.startsWith('data: ')) continue;
314+
315+
final data = line.substring(6); // Remove "data: " prefix
316+
317+
// Check for stream end
318+
if (data == '[DONE]') {
319+
debugPrint('Stream complete');
320+
continue;
321+
}
322+
323+
try {
324+
final jsonData = jsonDecode(data);
325+
326+
// Extract conversation ID from first chunk
327+
if (newConversationId == null && jsonData['id'] != null) {
328+
newConversationId = jsonData['id'].toString();
329+
debugPrint('Got conversation ID from stream: $newConversationId');
330+
}
331+
332+
// Extract content delta
333+
if (jsonData['choices'] != null &&
334+
jsonData['choices'].isNotEmpty) {
335+
final delta = jsonData['choices'][0]['delta'];
336+
if (delta != null && delta['content'] != null) {
337+
final content = delta['content'].toString();
338+
fullResponse += content;
339+
yield content; // Yield each chunk as it arrives
340+
}
341+
}
342+
} catch (e) {
343+
// Skip malformed JSON chunks
344+
debugPrint('Error parsing SSE chunk: $e');
345+
}
346+
}
347+
}
348+
349+
// Save conversation ID after stream completes
350+
if (newConversationId != null && newConversationId != '-') {
351+
final cookieManager = CookieManager();
352+
await cookieManager.saveAgixtConversationId(newConversationId);
353+
debugPrint('Saved conversation ID: $newConversationId');
354+
_navigateToConversation(newConversationId, jwt);
355+
}
356+
357+
// Save the full interaction
358+
if (fullResponse.isNotEmpty) {
359+
await _saveInteraction(message, fullResponse);
360+
}
361+
362+
client.close();
363+
} else if (streamedResponse.statusCode == 401) {
364+
await SessionManager.clearSession();
365+
yield "Authentication expired. Please login again.";
366+
client.close();
367+
} else {
368+
debugPrint('Streaming API Error: ${streamedResponse.statusCode}');
369+
yield "Error: Unable to get response (${streamedResponse.statusCode})";
370+
client.close();
371+
}
372+
} catch (e) {
373+
debugPrint('Streaming chat error: $e');
374+
yield "An error occurred while connecting to AGiXT.";
375+
}
376+
}
377+
378+
/// Convenience method to collect full streaming response as a single string
379+
Future<String?> sendChatMessageStreamingFull(String message) async {
380+
final buffer = StringBuffer();
381+
await for (final chunk in sendChatMessageStreaming(message)) {
382+
buffer.write(chunk);
383+
}
384+
return buffer.isEmpty ? null : buffer.toString();
385+
}
386+
235387
// Navigate to the conversation in the WebView
236388
Future<void> _navigateToConversation(
237389
String conversationId,

lib/services/ai_service.dart

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,38 @@ class AIService {
9393

9494
/// Handle wake word events
9595
void _handleWakeWordEvent(WakeWordEvent event) {
96-
if (event.type == WakeWordEventType.detected) {
97-
debugPrint('AIService: Wake word detected from ${event.source}');
98-
// Provide haptic feedback
99-
_playWakeWordFeedback();
100-
// Start voice recording when wake word is detected
101-
if (!_isProcessing) {
102-
_startVoiceRecording();
103-
}
96+
switch (event.type) {
97+
case WakeWordEventType.detected:
98+
debugPrint('AIService: Wake word detected from ${event.source}');
99+
// Provide haptic feedback
100+
_playWakeWordFeedback();
101+
// Start voice recording when wake word is detected
102+
if (!_isProcessing) {
103+
_startVoiceRecording();
104+
}
105+
break;
106+
107+
case WakeWordEventType.modelDownloadStarted:
108+
debugPrint('AIService: Wake word model download started');
109+
_showInfoMessage('Downloading speech model for wake word...');
110+
break;
111+
112+
case WakeWordEventType.modelDownloadProgress:
113+
final progress = ((event.progress ?? 0) * 100).toInt();
114+
debugPrint('AIService: Wake word model download progress: $progress%');
115+
break;
116+
117+
case WakeWordEventType.modelDownloadComplete:
118+
debugPrint('AIService: Wake word model download complete');
119+
_showInfoMessage('Wake word ready! Say "computer" to activate.');
120+
break;
121+
122+
case WakeWordEventType.error:
123+
debugPrint('AIService: Wake word error: ${event.error}');
124+
break;
125+
126+
default:
127+
break;
104128
}
105129
}
106130

@@ -171,12 +195,22 @@ class AIService {
171195

172196
debugPrint('AIService: Transcription: $transcription');
173197

174-
// Send transcribed text to AGiXT chat
175-
final response = await _chatWidget.sendChatMessage(transcription);
198+
// Send transcribed text to AGiXT chat with streaming for better responsiveness
199+
final responseBuffer = StringBuffer();
200+
201+
await for (final chunk in _chatWidget.sendChatMessageStreaming(transcription)) {
202+
responseBuffer.write(chunk);
203+
204+
// Stream chunks to connected devices as they arrive
205+
// For glasses and watch, we accumulate and send at reasonable intervals
206+
// For TTS, we'll wait for complete sentences
207+
}
208+
209+
final fullResponse = responseBuffer.toString();
176210

177-
if (response != null && response.isNotEmpty) {
178-
// Output response based on connected devices
179-
await _outputResponse(response);
211+
if (fullResponse.isNotEmpty) {
212+
// Output the full response (TTS and final display)
213+
await _outputResponse(fullResponse);
180214
} else {
181215
await _showErrorMessage('No response from AGiXT');
182216
}
@@ -410,16 +444,22 @@ class AIService {
410444
}
411445
}
412446

413-
// Send message to AGiXT API and display response
447+
// Send message to AGiXT API and display response (with streaming)
414448
Future<void> _sendMessageToAGiXT(String message) async {
415449
try {
416450
// Show sending message using AI response method
417451
await _bluetoothManager.sendAIResponse('Sending to AGiXT: "$message"');
418452

419-
// Get response using the AGiXTChatWidget
420-
final response = await _chatWidget.sendChatMessage(message);
453+
// Stream response from AGiXT for better responsiveness
454+
final responseBuffer = StringBuffer();
455+
456+
await for (final chunk in _chatWidget.sendChatMessageStreaming(message)) {
457+
responseBuffer.write(chunk);
458+
}
459+
460+
final response = responseBuffer.toString();
421461

422-
if (response != null && response.isNotEmpty) {
462+
if (response.isNotEmpty) {
423463
// Output response to appropriate devices
424464
await _outputResponse(response);
425465
} else {
@@ -516,6 +556,13 @@ class AIService {
516556
}
517557
}
518558

559+
Future<void> _showInfoMessage(String message) async {
560+
await _bluetoothManager.sendAIResponse(message);
561+
if (_watchService.isConnected) {
562+
await _watchService.displayMessage(message, durationMs: 5000);
563+
}
564+
}
565+
519566
/// Check if AIService is in background mode
520567
bool get isBackgroundMode => _isBackgroundMode;
521568

0 commit comments

Comments
 (0)