From 007467d488a5c559e66f37bf344f4310fc28def0 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:36:09 -0500 Subject: [PATCH] added alternative tts package for windows users (#1065) * added alternative tts package for windows users * fix function for determining OS --- lib/pangea/widgets/chat/tts_controller.dart | 95 +++++++++++++------ .../practice_activity/word_audio_button.dart | 2 +- .../word_focus_listening_activity.dart | 6 +- lib/utils/platform_infos.dart | 17 ++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 36 ++++++- pubspec.yaml | 3 +- 7 files changed, 127 insertions(+), 34 deletions(-) diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index 5242f2f8d..6eb762ef0 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -3,27 +3,35 @@ import 'dart:developer'; import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/missing_voice_button.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_tts/flutter_tts.dart' as flutter_tts; import 'package:matrix/matrix_api_lite/utils/logs.dart'; +import 'package:text_to_speech/text_to_speech.dart'; class TtsController { - String? targetLanguage; + String? get targetLanguage => + MatrixState.pangeaController.languageController.userL2?.langCode; - List availableLangCodes = []; - final flutter_tts.FlutterTts tts = flutter_tts.FlutterTts(); + List _availableLangCodes = []; + final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts(); + final TextToSpeech _alternativeTTS = TextToSpeech(); TtsController() { setupTTS(); } + bool get _useAlternativeTTS { + return PlatformInfos.getOperatingSystem() == 'Windows'; + } + Future dispose() async { - await tts.stop(); + await _tts.stop(); } - onError(dynamic message) => ErrorHandler.logError( + void _onError(dynamic message) => ErrorHandler.logError( e: message, m: (message.toString().isNotEmpty) ? message.toString() : 'TTS error', data: { @@ -32,22 +40,23 @@ class TtsController { ); Future setupTTS() async { - try { - tts.setErrorHandler(onError); - - targetLanguage ??= - MatrixState.pangeaController.languageController.userL2?.langCode; + if (_useAlternativeTTS) { + await _setupAltTTS(); + return; + } + try { + _tts.setErrorHandler(_onError); debugger(when: kDebugMode && targetLanguage == null); - tts.setLanguage( + _tts.setLanguage( targetLanguage ?? "en", ); - await tts.awaitSpeakCompletion(true); + await _tts.awaitSpeakCompletion(true); - final voices = (await tts.getVoices) as List?; - availableLangCodes = (voices ?? []) + final voices = (await _tts.getVoices) as List?; + _availableLangCodes = (voices ?? []) .map((v) { // on iOS / web, the codes are in 'locale', but on Android, they are in 'name' final nameCode = v['name']?.split("-").first; @@ -58,9 +67,34 @@ class TtsController { .cast() .toList(); - debugPrint("availableLangCodes: $availableLangCodes"); + debugPrint("availableLangCodes: $_availableLangCodes"); - debugger(when: kDebugMode && !isLanguageFullySupported); + debugger(when: kDebugMode && !_isLanguageFullySupported); + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s); + } + } + + Future _setupAltTTS() async { + try { + final languages = await _alternativeTTS.getLanguages(); + _availableLangCodes = + languages.map((lang) => lang.split("-").first).toSet().toList(); + + debugPrint("availableLangCodes: $_availableLangCodes"); + + final langsMatchingTarget = languages + .where( + (lang) => + targetLanguage != null && + lang.toLowerCase().startsWith(targetLanguage!.toLowerCase()), + ) + .toList(); + + if (langsMatchingTarget.isNotEmpty) { + await _alternativeTTS.setLanguage(langsMatchingTarget.first); + } } catch (e, s) { debugger(when: kDebugMode); ErrorHandler.logError(e: e, s: s); @@ -71,8 +105,10 @@ class TtsController { try { // return type is dynamic but apparent its supposed to be 1 // https://pub.dev/packages/flutter_tts - final result = await tts.stop(); - if (result != 1) { + final result = + await (_useAlternativeTTS ? _alternativeTTS.stop() : _tts.stop()); + + if (!_useAlternativeTTS && result != 1) { ErrorHandler.logError( m: 'Unexpected result from tts.stop', data: { @@ -86,7 +122,7 @@ class TtsController { } } - Future showMissingVoicePopup( + Future _showMissingVoicePopup( BuildContext context, String eventID, ) async { @@ -111,28 +147,29 @@ class TtsController { BuildContext context, String eventID, ) async { - if (isLanguageFullySupported) { - await speak(text); + if (_isLanguageFullySupported) { + await _speak(text); } else { ErrorHandler.logError( e: 'Language not supported by TTS engine', data: { 'targetLanguage': targetLanguage, - 'availableLangCodes': availableLangCodes, + 'availableLangCodes': _availableLangCodes, }, ); - await showMissingVoicePopup(context, eventID); + await _showMissingVoicePopup(context, eventID); } } - Future speak(String text) async { + Future _speak(String text) async { try { stop(); - targetLanguage ??= - MatrixState.pangeaController.languageController.userL2?.langCode; Logs().i('Speaking: $text'); - final result = await tts.speak(text).timeout( + final result = await (_useAlternativeTTS + ? _alternativeTTS.speak(text) + : _tts.speak(text)) + .timeout( const Duration(seconds: 5), onTimeout: () { ErrorHandler.logError( @@ -160,6 +197,6 @@ class TtsController { } } - bool get isLanguageFullySupported => - availableLangCodes.contains(targetLanguage); + bool get _isLanguageFullySupported => + _availableLangCodes.contains(targetLanguage); } diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart index 938fca468..2499a34c3 100644 --- a/lib/pangea/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -40,7 +40,7 @@ class WordAudioButtonState extends State { _isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio, onPressed: () async { if (_isPlaying) { - await widget.ttsController.tts.stop(); + await widget.ttsController.stop(); if (mounted) { setState(() => _isPlaying = false); } diff --git a/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart b/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart index 168758137..112abce16 100644 --- a/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart +++ b/lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart @@ -163,7 +163,11 @@ class WordFocusListeningActivityState } return GestureDetector( - onTap: () => tts.speak(widget.activityContent.choices[index]), + onTap: () => tts.tryToSpeak( + widget.activityContent.choices[index], + context, + widget.practiceCardController.widget.pangeaMessageEvent.eventId, + ), child: CircleAvatar( radius: buttonSize, backgroundColor: dragging ? Colors.grey.withOpacity(0.5) : buttonColor, diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index 83d4c9e74..5532ca79b 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:universal_html/html.dart' as html; import 'package:url_launcher/url_launcher_string.dart'; import '../config/app_config.dart'; @@ -86,4 +87,20 @@ abstract class PlatformInfos { applicationName: AppConfig.applicationName, ); } + + // #Pangea + static String? getOperatingSystem() { + if (!kIsWeb) return null; + final String platform = html.window.navigator.platform?.toLowerCase() ?? ''; + + if (platform.contains('mac')) { + return 'macOS'; + } else if (platform.contains('win')) { + return 'Windows'; + } else if (platform.contains('linux')) { + return 'Linux'; + } + return null; + } +// Pangea# } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c97a460b3..94d578564 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -36,6 +36,7 @@ import share_plus import shared_preferences_foundation import sqflite import sqlcipher_flutter_libs +import text_to_speech_macos import url_launcher_macos import video_compress import video_player_avfoundation @@ -74,6 +75,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) + TextToSpeechMacOsPlugin.register(with: registry.registrar(forPlugin: "TextToSpeechMacOsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index f1eff4109..3fa9dd08e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -881,10 +881,10 @@ packages: dependency: "direct main" description: name: flutter_tts - sha256: aed2a00c48c43af043ed81145fd8503ddd793dafa7088ab137dbef81a703e53d + sha256: cbec5f0447223e1b4c47f893c7f8ef663ac582120c147e4a1e2cade7f2e8b0c8 url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.2.0" flutter_typeahead: dependency: "direct main" description: @@ -2370,6 +2370,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.4" + text_to_speech: + dependency: "direct main" + description: + name: text_to_speech + sha256: f9adeb82bf0c912fd7f0ce656b1283e49b0869f9247bf865859dcf0186ed32f3 + url: "https://pub.dev" + source: hosted + version: "0.2.3" + text_to_speech_macos: + dependency: transitive + description: + name: text_to_speech_macos + sha256: "11d1b7d4eff579743b04d371e86d17bebd599f7d998b9fa4cf07a5821cda3b6d" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + text_to_speech_platform_interface: + dependency: transitive + description: + name: text_to_speech_platform_interface + sha256: "9d637f0ae36e296f42a0e555bd65ba4c64a28a7c26a2752fdae62f6d78b6c2d0" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + text_to_speech_web: + dependency: transitive + description: + name: text_to_speech_web + sha256: "47d006c0a377c9eb3f6bcca4d92b3ece2c67f5eb31b9416727cc81b92c36d6d1" + url: "https://pub.dev" + source: hosted + version: "0.1.2" timezone: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 37d03ea9d..b23de53f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 1.23.10+3569 +version: 1.23.11+3570 environment: sdk: ">=3.0.0 <4.0.0" @@ -134,6 +134,7 @@ dependencies: shimmer: ^3.0.0 syncfusion_flutter_xlsio: ^25.1.40 rive: 0.11.11 + text_to_speech: ^0.2.3 flutter_tts: ^4.2.0 # Pangea#