diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index a859c6229..05701909d 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3956,5 +3956,6 @@ "inNoSpaces": "You are not a member of any classes or exchanges", "successfullySubscribed": "You have successfully subscribed!", "clickToManageSubscription": "Click here to manage your subscription.", - "emptyInviteWarning": "Add this chat to a class or exchange to invite other users." + "emptyInviteWarning": "Add this chat to a class or exchange to invite other users.", + "errorGettingAudio": "Error getting audio. Please refresh and try again." } \ No newline at end of file diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 6c29f4ddd..eef7aaca0 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,17 +1,17 @@ +import 'package:animations/animations.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_actions.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import 'package:animations/animations.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../config/themes.dart'; import 'chat.dart'; import 'input_bar.dart'; @@ -58,18 +58,21 @@ class ChatInputRow extends StatelessWidget { ), ) else - SizedBox( - height: 56, - child: TextButton( - onPressed: controller.forwardEventsAction, - child: Row( - children: [ - const Icon(Icons.keyboard_arrow_left_outlined), - Text(L10n.of(context)!.forward), - ], - ), - ), - ), + // #Pangea + PangeaMessageActions(chatController: controller), + // SizedBox( + // height: 56, + // child: TextButton( + // onPressed: controller.forwardEventsAction, + // child: Row( + // children: [ + // const Icon(Icons.keyboard_arrow_left_outlined), + // Text(L10n.of(context)!.forward), + // ], + // ), + // ), + // ), + // Pangea# controller.selectedEvents.length == 1 ? controller.selectedEvents.first .getDisplayEvent(controller.timeline!) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index f801df69e..c1b8e1bfd 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -29,7 +29,10 @@ import '../../utils/stream_extension.dart'; import 'chat_emoji_picker.dart'; import 'chat_input_row.dart'; -enum _EventContextAction { info, report } +//#Pangea +// enum _EventContextAction { info, report } +enum _EventContextAction { info, forward, report } +//Pangea# class ChatView extends StatelessWidget { final ChatController controller; @@ -85,18 +88,34 @@ class ChatView extends StatelessWidget { case _EventContextAction.report: controller.reportEventAction(); break; + // #Pangea + case _EventContextAction.forward: + controller.forwardEventsAction(); + break; + // Pangea# } }, itemBuilder: (context) => [ // #Pangea + // PopupMenuItem( + // value: _EventContextAction.info, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // const Icon(Icons.info_outlined), + // const SizedBox(width: 12), + // Text(L10n.of(context)!.messageInfo), + // ], + // ), + // ), PopupMenuItem( - value: _EventContextAction.info, + value: _EventContextAction.forward, child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.info_outlined), + const Icon(Icons.forward), const SizedBox(width: 12), - Text(L10n.of(context)!.messageInfo), + Text(L10n.of(context)!.forward), ], ), ), diff --git a/lib/pangea/choreographer/widgets/language_display_toggle.dart b/lib/pangea/choreographer/widgets/language_display_toggle.dart index a399fda44..451e31782 100644 --- a/lib/pangea/choreographer/widgets/language_display_toggle.dart +++ b/lib/pangea/choreographer/widgets/language_display_toggle.dart @@ -1,5 +1,5 @@ +import 'package:fluffychat/pangea/widgets/flag.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../../../config/app_config.dart'; @@ -55,3 +55,70 @@ class LanguageDisplayToggle extends StatelessWidget { // ); } } + +class LanguageToggleSwitch extends StatefulWidget { + final ChatController controller; + + const LanguageToggleSwitch({super.key, required this.controller}); + + @override + _LanguageToggleSwitchState createState() => _LanguageToggleSwitchState(); +} + +class _LanguageToggleSwitchState extends State { + @override + Widget build(BuildContext context) { + final borderRadius = + BorderRadius.circular(20.0); // Use the same radius as your LanguageFlag + + return Tooltip( + message: L10n.of(context)!.toggleLanguages, + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: Colors.transparent, // No background color + shape: RoundedRectangleBorder(borderRadius: borderRadius), + padding: EdgeInsets.zero, // Aligns with your custom padding + ), + onPressed: _toggleLanguage, // Use the onTap logic for onPressed + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .background, // Adapt to your app theme or custom color + borderRadius: borderRadius, + ), + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + //trranslatte icon + Opacity( + opacity: isL1Selected ? 1.0 : 0.6, + child: LanguageFlag( + language: widget.controller.choreographer.l1Lang, + ), + ), + const SizedBox(width: 8.0), // Spacing between flags + Opacity( + opacity: isL1Selected ? 0.6 : 1.0, + child: LanguageFlag( + language: widget.controller.choreographer.l2Lang, + ), + ), + ], + ), + ), + ), + ); + } + + bool get isL1Selected => + widget.controller.choreographer.messageOptions.isTranslationOn; + + void _toggleLanguage() { + setState(() { + widget.controller.choreographer.messageOptions + .toggleSelectedDisplayLang(); + }); + } +} diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index acec26c80..094ccaa84 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -51,7 +51,7 @@ class ModelKey { static const String word = "word"; static const String lang = "lang"; static const String deepL = "deepl"; - static const String langCode = 'langCode'; + static const String langCode = 'lang_code'; static const String wordLang = "word_lang"; static const String lemma = "lemma"; static const String saveVocab = "save_vocab"; @@ -87,4 +87,6 @@ class ModelKey { static const String currentText = "current"; static const String bestContinuance = "best_continuance"; static const String feedbackLang = "feedback_lang"; + + static const String transcription = "transcription"; } diff --git a/lib/pangea/constants/pangea_event_types.dart b/lib/pangea/constants/pangea_event_types.dart index 68e849c88..b98492871 100644 --- a/lib/pangea/constants/pangea_event_types.dart +++ b/lib/pangea/constants/pangea_event_types.dart @@ -13,4 +13,6 @@ class PangeaEventTypes { static const vocab = "p.vocab"; static const roomInfo = "pangea.roomtopic"; + + static const audio = "p.audio"; } diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index 2d612ecab..e60b44b40 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/controllers/message_data_controller.dart'; import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/permissions_controller.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/controllers/user_controller.dart'; import 'package:fluffychat/pangea/controllers/word_net_controller.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; @@ -44,6 +45,7 @@ class PangeaController { late ITFeedbackController itFeedback; late InstructionsController instructions; late SubscriptionController subscriptionController; + late TextToSpeechController textToSpeech; ///store Services late PLocalStore pStoreService; @@ -89,6 +91,7 @@ class PangeaController { instructions = InstructionsController(this); subscriptionController = SubscriptionController(this); itFeedback = ITFeedbackController(this); + textToSpeech = TextToSpeechController(this); PAuthGaurd.pController = this; } diff --git a/lib/pangea/controllers/text_to_speech_controller.dart b/lib/pangea/controllers/text_to_speech_controller.dart new file mode 100644 index 000000000..bcd2c556f --- /dev/null +++ b/lib/pangea/controllers/text_to_speech_controller.dart @@ -0,0 +1,125 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/network/urls.dart'; +import 'package:http/http.dart'; + +import '../network/requests.dart'; + +class TextToSpeechRequest { + String text; + String langCode; + + TextToSpeechRequest({required this.text, required this.langCode}); + + Map toJson() => { + ModelKey.text: text, + ModelKey.langCode: langCode, + }; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is TextToSpeechRequest && + other.text == text && + other.langCode == langCode; + } + + @override + int get hashCode => text.hashCode ^ langCode.hashCode; +} + +class TextToSpeechResponse { + String audioContent; + String mediaType; + int durationMillis; + List waveform; + + TextToSpeechResponse({ + required this.audioContent, + required this.mediaType, + required this.durationMillis, + required this.waveform, + }); + + factory TextToSpeechResponse.fromJson( + Map json, + ) => + TextToSpeechResponse( + audioContent: json["audio_content"], + mediaType: json["media_type"], + durationMillis: json["duration_millis"], + waveform: List.from(json["wave_form"]), + ); +} + +class _TextToSpeechCacheItem { + Future data; + + _TextToSpeechCacheItem({ + required this.data, + }); +} + +class TextToSpeechController { + static final Map _cache = {}; + late final PangeaController _pangeaController; + + Timer? _cacheClearTimer; + + TextToSpeechController(PangeaController pangeaController) { + _pangeaController = pangeaController; + _initializeCacheClearing(); + } + + void _initializeCacheClearing() { + const duration = Duration(minutes: 15); // Adjust the duration as needed + _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache()); + } + + void _clearCache() { + _cache.clear(); + } + + void dispose() { + _cacheClearTimer?.cancel(); + } + + Future get( + TextToSpeechRequest params, + ) async { + if (_cache.containsKey(params)) { + return _cache[params]!.data; + } else { + final Future response = _fetchResponse( + await _pangeaController.userController.accessToken, + params, + ); + _cache[params] = _TextToSpeechCacheItem(data: response); + return response; + } + } + + static Future _fetchResponse( + String accessToken, + TextToSpeechRequest params, + ) async { + final Requests request = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: accessToken, + ); + + final Response res = await request.post( + url: PApiUrls.textToSpeech, + body: params.toJson(), + ); + + final Map json = jsonDecode(res.body); + + return TextToSpeechResponse.fromJson(json); + } +} diff --git a/lib/pangea/models/pangea_audio_events.dart b/lib/pangea/models/pangea_audio_events.dart new file mode 100644 index 000000000..3583d021e --- /dev/null +++ b/lib/pangea/models/pangea_audio_events.dart @@ -0,0 +1,9 @@ +// relates to a pangea representation event +// the matrix even fits the form of a regular matrix audio event +// but with something to distinguish it as a pangea audio event + +import 'package:matrix/matrix.dart'; + +class PangeaAudioEvent { + Event? _event; +} diff --git a/lib/pangea/models/pangea_message_event.dart b/lib/pangea/models/pangea_message_event.dart index 9a8a45d44..0a5b7602a 100644 --- a/lib/pangea/models/pangea_message_event.dart +++ b/lib/pangea/models/pangea_message_event.dart @@ -1,8 +1,10 @@ +import 'dart:convert'; import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_message_types.dart'; +import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/message_data_models.dart'; @@ -81,6 +83,116 @@ class PangeaMessageEvent { return true; } + //get audio for text and language + //if no audio exists, create it + //if audio exists, return it + Future getAudioGlobal(String langCode) async { + // try { + final String text = representationByLanguage(langCode)?.text ?? body; + + final local = getAudioLocal(langCode, text); + + if (local != null) return Future.value(local.eventId); + + final TextToSpeechRequest params = TextToSpeechRequest( + text: text, + langCode: langCode, + ); + + final TextToSpeechResponse response = + await MatrixState.pangeaController.textToSpeech.get( + params, + ); + + if (response.mediaType != 'audio/ogg') { + throw Exception('Unexpected media type: ${response.mediaType}'); + } + + final audioBytes = base64.decode(response.audioContent); + + // from text, trim whitespace, remove special characters, and limit to 20 characters + // final fileName = + // text.trim().replaceAll(RegExp('[^A-Za-z0-9]'), '').substring(0, 20); + final fileName = "audio_for_${eventId}_$langCode"; + + final file = MatrixAudioFile( + bytes: audioBytes, + name: fileName, + ); + + return room.sendFileEvent( + file, + inReplyTo: _event, + extraContent: { + 'info': { + ...file.info, + 'duration': response.durationMillis, + }, + 'org.matrix.msc3245.voice': {}, + 'org.matrix.msc1767.audio': { + 'duration': response.durationMillis, + 'waveform': null, + // 'waveform': response.waveform, + }, + 'transcription': { + ModelKey.text: text, + ModelKey.langCode: langCode, + }, + }, + ).timeout( + Durations.long4, + onTimeout: () { + debugPrint("timeout in getAudioGlobal"); + return null; + }, + ).then((eventId) { + debugPrint("eventId in getAudioGlobal $eventId"); + return eventId; + }).catchError((err, s) { + debugPrint("error in getAudioGlobal"); + debugPrint(err); + debugPrint(s); + debugger(when: kDebugMode); + return null; + }); + + // } catch (err, s) { + // debugger(when: kDebugMode); + // ErrorHandler.logError( + // e: err, + // s: s, + // ); + // return Future.value(null); + // } + } + + Event? getAudioLocal(String langCode, String text) { + return allAudio.firstWhereOrNull( + (element) { + // Safely access the transcription map + final transcription = + element.content.tryGet>(ModelKey.transcription); + if (transcription == null) { + // If transcription is null, this element does not match. + return false; + } + + // Safely get language code and text from the transcription + final elementLangCode = transcription.tryGet(ModelKey.langCode); + final elementText = transcription.tryGet(ModelKey.text); + + // Check if both language code and text match + return elementLangCode == langCode && elementText == text; + }, + ); + } + + // get audio events that are related to this event + Set get allAudio => _latestEdit.aggregatedEvents( + timeline, + EventTypes.Message, + ); + List? _representations; List get representations { if (_representations != null) return _representations!; @@ -188,11 +300,6 @@ class PangeaMessageEvent { RepresentationEvent? rep = representationByLanguage(langCode); - //if event is less than 1 minute old, then print new event - if (isNew) { - debugger(when: kDebugMode); - } - while ((isNew || eventId.contains("web")) && tries < 20) { if (rep != null) return rep; await Future.delayed(const Duration(milliseconds: 500)); diff --git a/lib/pangea/network/urls.dart b/lib/pangea/network/urls.dart index 0b0d6ad59..912376e45 100644 --- a/lib/pangea/network/urls.dart +++ b/lib/pangea/network/urls.dart @@ -51,6 +51,8 @@ class PApiUrls { static String firstStep = "/it_initialstep"; static String subseqStep = "/it_step"; + static String textToSpeech = "$choreoBaseApi/text_to_speech"; + ///-------------------------------- revenue cat -------------------------- static String rcApiV1 = "https://api.revenuecat.com/v1"; static String rcApiV2 = diff --git a/lib/pangea/repo/image_repo.dart b/lib/pangea/repo/image_repo.dart new file mode 100644 index 000000000..624e06e22 --- /dev/null +++ b/lib/pangea/repo/image_repo.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; + +import '../config/environment.dart'; +import '../network/requests.dart'; + +class GenerateImageeResponse { + final String imageUrl; + final String prompt; + + GenerateImageeResponse({ + required this.imageUrl, + required this.prompt, + }); + + factory GenerateImageeResponse.fromJson(Map json) { + return GenerateImageeResponse( + imageUrl: json['image_url'], + prompt: json['prompt'], + ); + } + + factory GenerateImageeResponse.error() { + return GenerateImageeResponse( + imageUrl: 'https://i.imgur.com/2L2JYqk.png', + prompt: 'Error', + ); + } +} + +class GenerateImageRequest { + String prompt; + + GenerateImageRequest({required this.prompt}); + + Map toJson() => { + 'prompt': prompt, + }; +} + +class ImageRepo { + static Future fetchImage( + GenerateImageRequest request) async { + final Requests req = + Requests(baseUrl: Environment.choreoApi); // Set your API base URL + final requestBody = request.toJson(); + + try { + final Response res = await req.post( + url: '/generate-image/', // Endpoint in your FastAPI server + body: requestBody, + ); + + if (res.statusCode == 200) { + final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); + return GenerateImageeResponse.fromJson( + decodedBody); // Convert response to ImageModel + } else { + throw Exception('Failed to load image'); + } + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack, data: requestBody); + return GenerateImageeResponse + .error(); // Return an error model or handle accordingly + } + } +} diff --git a/lib/pangea/repo/text_to_speech_repo.dart b/lib/pangea/repo/text_to_speech_repo.dart new file mode 100644 index 000000000..aafd299bd --- /dev/null +++ b/lib/pangea/repo/text_to_speech_repo.dart @@ -0,0 +1,66 @@ +// import 'dart:async'; +// import 'dart:convert'; + +// import 'package:fluffychat/pangea/config/environment.dart'; +// import 'package:fluffychat/pangea/constants/model_keys.dart'; +// import 'package:fluffychat/pangea/network/urls.dart'; +// import 'package:http/http.dart'; + +// import '../network/requests.dart'; + +// class TextToSpeechRequest { +// String text; +// String langCode; + +// TextToSpeechRequest({required this.text, required this.langCode}); + +// Map toJson() => { +// ModelKey.text: text, +// ModelKey.langCode: langCode, +// }; +// } + +// class TextToSpeechResponse { +// String audioContent; +// String mediaType; +// int durationMillis; +// List waveform; + +// TextToSpeechResponse({ +// required this.audioContent, +// required this.mediaType, +// required this.durationMillis, +// required this.waveform, +// }); + +// factory TextToSpeechResponse.fromJson( +// Map json, +// ) => +// TextToSpeechResponse( +// audioContent: json["audio_content"], +// mediaType: json["media_type"], +// durationMillis: json["duration_millis"], +// waveform: List.from(json["wave_form"]), +// ); +// } + +// class TextToSpeechService { +// static Future get({ +// required String accessToken, +// required TextToSpeechRequest params, +// }) async { +// final Requests request = Requests( +// choreoApiKey: Environment.choreoApiKey, +// accessToken: accessToken, +// ); + +// final Response res = await request.post( +// url: PApiUrls.textToSpeech, +// body: params.toJson(), +// ); + +// final Map json = jsonDecode(res.body); + +// return TextToSpeechResponse.fromJson(json); +// } +// } diff --git a/lib/pangea/widgets/chat/message_actions.dart b/lib/pangea/widgets/chat/message_actions.dart new file mode 100644 index 000000000..b8eebbb43 --- /dev/null +++ b/lib/pangea/widgets/chat/message_actions.dart @@ -0,0 +1,27 @@ +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/language_display_toggle.dart'; +import 'package:fluffychat/pangea/widgets/chat/text_to_speech_button.dart'; +import 'package:flutter/material.dart'; + +class PangeaMessageActions extends StatelessWidget { + final ChatController chatController; + + const PangeaMessageActions({super.key, required this.chatController}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + LanguageToggleSwitch(controller: chatController), + TextToSpeechButton( + controller: chatController, + ), + // IconButton( + // icon: Icon(Icons.mic), + // onPressed: chatController.onMicTap, + // ), + // Add more IconButton widgets here + ], + ); + } +} diff --git a/lib/pangea/widgets/chat/text_to_speech_button.dart b/lib/pangea/widgets/chat/text_to_speech_button.dart new file mode 100644 index 000000000..c5c229a47 --- /dev/null +++ b/lib/pangea/widgets/chat/text_to_speech_button.dart @@ -0,0 +1,128 @@ +import 'dart:developer'; + +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:matrix/matrix.dart'; + +class TextToSpeechButton extends StatefulWidget { + final ChatController controller; + + const TextToSpeechButton({ + super.key, + required this.controller, + }); + + @override + _TextToSpeechButtonState createState() => _TextToSpeechButtonState(); +} + +class _TextToSpeechButtonState extends State { + final AudioPlayer _audioPlayer = AudioPlayer(); + + @override + void dispose() { + _audioPlayer.dispose(); + super.dispose(); + } + + void _playSpeech() { + try { + final String langCode = widget.controller.choreographer.messageOptions + .selectedDisplayLang?.langCode ?? + widget.controller.choreographer.l2LangCode ?? + 'en'; + final Event event = widget.controller.selectedEvents.first; + + PangeaMessageEvent( + event: event, + timeline: widget.controller.timeline!, + ownMessage: event.senderId == Matrix.of(context).client.userID, + selected: true, + ).getAudioGlobal(langCode); + + // final String? text = PangeaMessageEvent( + // event: event, + // timeline: widget.controller.timeline!, + // ownMessage: event.senderId == Matrix.of(context).client.userID, + // selected: true, + // ).representationByLanguage(langCode)?.text; + + // if (text == null || text.isEmpty) { + // throw Exception("text is null or empty in text_to_speech_button.dart"); + // } + + // final TextToSpeechRequest params = TextToSpeechRequest( + // text: text, + // langCode: widget.controller.choreographer.messageOptions + // .selectedDisplayLang?.langCode ?? + // widget.controller.choreographer.l2LangCode ?? + // LanguageKeys.unknownLanguage, + // ); + + // final TextToSpeechResponse response = await TextToSpeechService.get( + // accessToken: + // await MatrixState.pangeaController.userController.accessToken, + // params: params, + // ); + + // if (response.mediaType != 'audio/ogg') { + // throw Exception('Unexpected media type: ${response.mediaType}'); + // } + + // // Decode the base64 audio content to bytes + // final audioBytes = base64.decode(response.audioContent); + + // final encoding = Uri.dataFromBytes(audioBytes); + // final uri = AudioSource.uri(encoding); + // // gets here without problems + + // await _audioPlayer.setAudioSource(uri); + // await _audioPlayer.play(); + + // final audioBytes = base64.decode(response.audioContent); + // final tempDir = await getTemporaryDirectory(); + // final file = File('${tempDir.path}/speech.ogg'); + // await file.writeAsBytes(audioBytes); + + // await _audioPlayer.setFilePath(file.path); + + // await _audioPlayer.play(); + } catch (e) { + debugger(when: kDebugMode); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + L10n.of(context)!.errorGettingAudio, + ), + ), + ); + ErrorHandler.logError( + e: Exception(), + s: StackTrace.current, + m: 'text is null or empty in text_to_speech_button.dart', + data: { + 'event': widget.controller.selectedEvents.first, + 'langCode': widget.controller.choreographer.messageOptions + .selectedDisplayLang?.langCode ?? + widget.controller.choreographer.l2LangCode ?? + LanguageKeys.unknownLanguage, + }, + ); + } + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: _playSpeech, + child: const Text('Convert to Speech'), + ); + } +} diff --git a/needed-translations.txt b/needed-translations.txt index cdb97658e..64a3f81b8 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -765,7 +765,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "bn": [ @@ -1539,7 +1540,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "bo": [ @@ -2313,7 +2315,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ca": [ @@ -3082,7 +3085,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "cs": [ @@ -3851,7 +3855,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "de": [ @@ -4620,7 +4625,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "el": [ @@ -5394,7 +5400,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "eo": [ @@ -6163,7 +6170,12 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" + ], + + "es": [ + "errorGettingAudio" ], "et": [ @@ -6932,7 +6944,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "eu": [ @@ -7701,7 +7714,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "fa": [ @@ -8470,7 +8484,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "fi": [ @@ -9239,7 +9254,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "fr": [ @@ -10008,7 +10024,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ga": [ @@ -10777,7 +10794,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "gl": [ @@ -11546,7 +11564,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "he": [ @@ -12315,7 +12334,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "hi": [ @@ -13089,7 +13109,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "hr": [ @@ -13858,7 +13879,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "hu": [ @@ -14627,7 +14649,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "id": [ @@ -15396,7 +15419,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ie": [ @@ -16167,7 +16191,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "it": [ @@ -16936,7 +16961,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ja": [ @@ -17705,7 +17731,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ko": [ @@ -18474,7 +18501,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "lt": [ @@ -19243,7 +19271,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "lv": [ @@ -20017,7 +20046,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "nb": [ @@ -20786,7 +20816,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "nl": [ @@ -21555,7 +21586,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "pl": [ @@ -22324,7 +22356,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "pt": [ @@ -23098,7 +23131,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "pt_BR": [ @@ -23867,7 +23901,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "pt_PT": [ @@ -24636,7 +24671,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ro": [ @@ -25405,7 +25441,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ru": [ @@ -26174,7 +26211,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "sk": [ @@ -26944,7 +26982,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "sl": [ @@ -27716,7 +27755,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "sr": [ @@ -28485,7 +28525,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "sv": [ @@ -29254,7 +29295,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ta": [ @@ -30028,7 +30070,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "th": [ @@ -30802,7 +30845,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "tr": [ @@ -31571,7 +31615,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "uk": [ @@ -32340,7 +32385,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "vi": [ @@ -33112,7 +33158,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "zh": [ @@ -33881,7 +33928,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "zh_Hant": [ @@ -34650,6 +34698,7 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ] }