added alternative tts package for windows users (#1065)
* added alternative tts package for windows users * fix function for determining OS
This commit is contained in:
parent
27e829380c
commit
007467d488
7 changed files with 128 additions and 35 deletions
|
|
@ -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<String> availableLangCodes = [];
|
||||
final flutter_tts.FlutterTts tts = flutter_tts.FlutterTts();
|
||||
List<String> _availableLangCodes = [];
|
||||
final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts();
|
||||
final TextToSpeech _alternativeTTS = TextToSpeech();
|
||||
|
||||
TtsController() {
|
||||
setupTTS();
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await tts.stop();
|
||||
bool get _useAlternativeTTS {
|
||||
return PlatformInfos.getOperatingSystem() == 'Windows';
|
||||
}
|
||||
|
||||
onError(dynamic message) => ErrorHandler.logError(
|
||||
Future<void> dispose() async {
|
||||
await _tts.stop();
|
||||
}
|
||||
|
||||
void _onError(dynamic message) => ErrorHandler.logError(
|
||||
e: message,
|
||||
m: (message.toString().isNotEmpty) ? message.toString() : 'TTS error',
|
||||
data: {
|
||||
|
|
@ -32,22 +40,23 @@ class TtsController {
|
|||
);
|
||||
|
||||
Future<void> setupTTS() async {
|
||||
if (_useAlternativeTTS) {
|
||||
await _setupAltTTS();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
tts.setErrorHandler(onError);
|
||||
|
||||
targetLanguage ??=
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
_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<String>()
|
||||
.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<void> _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<void> showMissingVoicePopup(
|
||||
Future<void> _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<void> speak(String text) async {
|
||||
Future<void> _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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class WordAudioButtonState extends State<WordAudioButton> {
|
|||
_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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
36
pubspec.lock
36
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:
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue