From dff6a3be05ec9a1d1df2f49bfef8603e929cb94f Mon Sep 17 00:00:00 2001 From: yaso Date: Wed, 5 Feb 2025 16:08:20 +0200 Subject: [PATCH 1/3] Adroid Config --- Frontend/android/app/src/main/AndroidManifest.xml | 4 ++++ Frontend/macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ Frontend/windows/flutter/generated_plugin_registrant.cc | 3 +++ Frontend/windows/flutter/generated_plugins.cmake | 1 + 4 files changed, 10 insertions(+) diff --git a/Frontend/android/app/src/main/AndroidManifest.xml b/Frontend/android/app/src/main/AndroidManifest.xml index 6ece29d0..9a6761d7 100644 --- a/Frontend/android/app/src/main/AndroidManifest.xml +++ b/Frontend/android/app/src/main/AndroidManifest.xml @@ -64,5 +64,9 @@ + + + + diff --git a/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift index ae2d0f30..4e16bb3b 100644 --- a/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import device_info_plus import firebase_core +import flutter_tts import geolocator_apple import local_auth_darwin import mobile_scanner @@ -20,6 +21,7 @@ import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) diff --git a/Frontend/windows/flutter/generated_plugin_registrant.cc b/Frontend/windows/flutter/generated_plugin_registrant.cc index 4d868f3a..1858f987 100644 --- a/Frontend/windows/flutter/generated_plugin_registrant.cc +++ b/Frontend/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FlDownloaderPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlDownloaderPluginCApi")); + FlutterTtsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterTtsPlugin")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); LocalAuthPluginRegisterWithRegistrar( diff --git a/Frontend/windows/flutter/generated_plugins.cmake b/Frontend/windows/flutter/generated_plugins.cmake index 7aeca6fd..45e2a4f4 100644 --- a/Frontend/windows/flutter/generated_plugins.cmake +++ b/Frontend/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_core fl_downloader + flutter_tts geolocator_windows local_auth_windows permission_handler_windows From 1db25b2f5e592c8bbe18de466542332e7ce109b1 Mon Sep 17 00:00:00 2001 From: yaso Date: Wed, 5 Feb 2025 16:08:35 +0200 Subject: [PATCH 2/3] add flutter_tts package --- Frontend/pubspec.lock | 8 ++++++++ Frontend/pubspec.yaml | 1 + 2 files changed, 9 insertions(+) diff --git a/Frontend/pubspec.lock b/Frontend/pubspec.lock index 19f33b23..922c081a 100644 --- a/Frontend/pubspec.lock +++ b/Frontend/pubspec.lock @@ -507,6 +507,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_tts: + dependency: "direct main" + description: + name: flutter_tts + sha256: baa3cb6b4990318460fe28bfa8c7869399e97223971532c02bd97c5e876aa3c5 + url: "https://pub.dev" + source: hosted + version: "4.2.2" flutter_web_plugins: dependency: "direct main" description: flutter diff --git a/Frontend/pubspec.yaml b/Frontend/pubspec.yaml index f6488f79..7930cf75 100644 --- a/Frontend/pubspec.yaml +++ b/Frontend/pubspec.yaml @@ -74,6 +74,7 @@ dependencies: flutter_chat_ui: ^1.6.15 flutter_chat_types: ^3.6.2 uuid: ^4.5.1 + flutter_tts: ^4.2.2 dev_dependencies: flutter_test: From 1eae19526c4fd90562f9b63445b30df1e08b3220 Mon Sep 17 00:00:00 2001 From: yaso Date: Wed, 5 Feb 2025 16:08:48 +0200 Subject: [PATCH 3/3] Add tts after stream is complete --- .../lib/mih_packages/mzansi_ai/ai_chat.dart | 153 ++++++++++++++++-- 1 file changed, 142 insertions(+), 11 deletions(-) diff --git a/Frontend/lib/mih_packages/mzansi_ai/ai_chat.dart b/Frontend/lib/mih_packages/mzansi_ai/ai_chat.dart index b2266087..190eb7ee 100644 --- a/Frontend/lib/mih_packages/mzansi_ai/ai_chat.dart +++ b/Frontend/lib/mih_packages/mzansi_ai/ai_chat.dart @@ -13,6 +13,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_tts/flutter_tts.dart'; import 'package:ollama_dart/ollama_dart.dart' as ollama; import 'package:uuid/uuid.dart'; @@ -28,9 +29,14 @@ class AiChat extends StatefulWidget { } class _AiChatState extends State { - final TextEditingController _modelCopntroller = TextEditingController(); - final TextEditingController _fontSizeCopntroller = TextEditingController(); + final TextEditingController _modelController = TextEditingController(); + final TextEditingController _fontSizeController = TextEditingController(); + final TextEditingController _ttsController = TextEditingController(); final ValueNotifier _showModelOptions = ValueNotifier(false); + FlutterTts _flutterTts = FlutterTts(); + String? textStream; + List _voices = []; + Map? _currentVoice; List _messages = []; late types.User _user; late types.User _mihAI; @@ -109,12 +115,67 @@ class _AiChatState extends State { stream: aiChatStream, builder: (context, snapshot) { if (snapshot.hasData) { + textStream = snapshot.requireData; + // print("Text: $textStream"); + // _speakText(textStream!); return MihAppWindow( fullscreen: false, windowTitle: 'Mzansi AI Thoughts', - windowTools: const [], + windowTools: [ + Visibility( + visible: _aiThinking == false, + child: Padding( + padding: const EdgeInsets.all(5.0), + child: Container( + //color: MzanziInnovationHub.of(context)!.theme.successColor(), + decoration: BoxDecoration( + color: + MzanziInnovationHub.of(context)!.theme.successColor(), + borderRadius: const BorderRadius.all( + Radius.circular(100), + ), + ), + child: IconButton( + color: + MzanziInnovationHub.of(context)!.theme.primaryColor(), + onPressed: () { + print("Start TTS now"); + _speakText(snapshot.requireData); + }, + icon: const Icon(Icons.volume_up), + ), + ), + ), + ), + Visibility( + visible: _aiThinking == true, + child: Padding( + padding: const EdgeInsets.all(5.0), + child: Container( + // color: MzanziInnovationHub.of(context)!.theme.errorColor(), + decoration: BoxDecoration( + color: + MzanziInnovationHub.of(context)!.theme.errorColor(), + borderRadius: const BorderRadius.all( + Radius.circular(100), + ), + ), + child: IconButton( + color: + MzanziInnovationHub.of(context)!.theme.primaryColor(), + onPressed: () { + //print("Start TTS now"); + _flutterTts.stop(); + }, + icon: const Icon(Icons.volume_off), + ), + ), + ), + ), + ], onWindowTapClose: () { _captureAIResponse(snapshot.requireData); + _flutterTts.stop(); Navigator.of(context).pop(); }, windowBody: [ @@ -141,6 +202,7 @@ class _AiChatState extends State { autofocus: true, onPressed: () { _captureAIResponse(snapshot.requireData); + _flutterTts.stop(); Navigator.of(context).pop(); }, focusColor: MzanziInnovationHub.of(context)! @@ -220,7 +282,7 @@ class _AiChatState extends State { ) async* { final aiStream = client.generateChatCompletionStream( request: ollama.GenerateChatCompletionRequest( - model: _modelCopntroller.text, + model: _modelController.text, messages: _chatHistory, ), ); @@ -328,7 +390,7 @@ class _AiChatState extends State { child: SizedBox( width: 300, child: MIHDropdownField( - controller: _modelCopntroller, + controller: _modelController, hintText: "AI Model", dropdownOptions: const [ 'deepseek-r1:1.5b', @@ -350,7 +412,7 @@ class _AiChatState extends State { onPressed: () { setState(() { _chatFrontSize -= 1; - _fontSizeCopntroller.text = + _fontSizeController.text = _chatFrontSize.ceil().toString(); }); }, @@ -362,7 +424,7 @@ class _AiChatState extends State { SizedBox( width: 200, child: MIHTextField( - controller: _fontSizeCopntroller, + controller: _fontSizeController, hintText: "Chat Font Size", editable: false, required: true, @@ -373,7 +435,7 @@ class _AiChatState extends State { onPressed: () { setState(() { _chatFrontSize += 1; - _fontSizeCopntroller.text = + _fontSizeController.text = _chatFrontSize.ceil().toString(); }); }, @@ -384,6 +446,28 @@ class _AiChatState extends State { ], ), const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: SizedBox( + width: 300, + child: MIHDropdownField( + controller: _ttsController, + hintText: "AI Voice", + dropdownOptions: _voices + .map((_voice) => _voice["name"] as String) + .toList(), + required: true, + editable: true, + enableSearch: false, + ), + ), + ), + ], + ), + const SizedBox(height: 10), ], ), ), @@ -395,12 +479,58 @@ class _AiChatState extends State { ); } + void setTtsVoice(String voiceName) { + _flutterTts.setVoice( + { + "name": voiceName, + "locale": _voices + .where((_voice) => _voice["name"].contains(voiceName)) + .first["locale"] + }, + ); + _ttsController.text = _currentVoice!["name"]; + } + + void _speakText(String text) async { + try { + await _flutterTts.stop(); // Stop any ongoing speech + await _flutterTts.speak(text); // Speak the new text + } catch (e) { + print("TTS Error: $e"); + } + } + @override void dispose() { // TODO: implement dispose super.dispose(); - _modelCopntroller.dispose(); + _modelController.dispose(); + _fontSizeController.dispose(); + _ttsController.dispose(); client.endSession(); + _flutterTts.stop(); + } + + void initTTS() { + _flutterTts.setVolume(0.7); + _flutterTts.getVoices.then( + (data) { + try { + _voices = List.from(data); + + print("=================== Voices ===================\n$_voices"); + setState(() { + _voices = _voices + .where((_voice) => _voice["name"].contains("en")) + .toList(); + _currentVoice = _voices.first; + setTtsVoice(_currentVoice!["name"]); + }); + } catch (e) { + print(e); + } + }, + ); } @override @@ -414,8 +544,8 @@ class _AiChatState extends State { firstName: "Mzansi AI", id: const Uuid().v4(), ); - _modelCopntroller.text = 'gemma2:2b'; - _fontSizeCopntroller.text = _chatFrontSize.ceil().toString(); + _modelController.text = 'gemma2:2b'; + _fontSizeController.text = _chatFrontSize.ceil().toString(); _chatHistory.add( ollama.Message( role: ollama.MessageRole.system, @@ -423,6 +553,7 @@ class _AiChatState extends State { ), ); _loadMessages(); + initTTS(); } @override