From b1c26f057286e3071ac67842bcde801b0cccf116 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Thu, 11 Apr 2024 16:31:10 -0400 Subject: [PATCH] speech to text fully drafted --- lib/pages/chat/events/message.dart | 3 +- lib/pages/chat/events/message_content.dart | 3 +- lib/pangea/controllers/pangea_controller.dart | 3 + .../speech_to_text_controller.dart | 81 ++ .../text_to_speech_controller.dart | 19 - lib/pangea/enum/audio_encoding_enum.dart | 56 + lib/pangea/enum/message_mode_enum.dart | 48 + lib/pangea/models/pangea_message_event.dart | 12 +- lib/pangea/models/speech_to_text_models.dart | 137 +++ lib/pangea/network/urls.dart | 1 + lib/pangea/repo/text_to_speech_repo.dart | 66 -- .../widgets/chat/message_audio_card.dart | 2 +- .../chat/message_speech_to_text_card.dart | 187 ++++ lib/pangea/widgets/chat/message_toolbar.dart | 98 +- .../chat/message_unsubscribed_card.dart | 3 +- .../widgets/chat/text_to_speech_button.dart | 4 +- lib/pangea/widgets/igc/pangea_rich_text.dart | 3 +- needed-translations.txt | 958 ++++++++++++++++-- 18 files changed, 1467 insertions(+), 217 deletions(-) create mode 100644 lib/pangea/controllers/speech_to_text_controller.dart create mode 100644 lib/pangea/enum/audio_encoding_enum.dart create mode 100644 lib/pangea/enum/message_mode_enum.dart create mode 100644 lib/pangea/models/speech_to_text_models.dart delete mode 100644 lib/pangea/repo/text_to_speech_repo.dart create mode 100644 lib/pangea/widgets/chat/message_speech_to_text_card.dart diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 5b1a56cf5..7ce2c9cc3 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -145,7 +145,8 @@ class Message extends StatelessWidget { controller.getPangeaMessageEvent(event.eventId); ToolbarDisplayController? toolbarController; if (event.messageType == MessageTypes.Text || - event.messageType == MessageTypes.Notice) { + event.messageType == MessageTypes.Notice || + event.messageType == MessageTypes.Audio) { toolbarController = controller.getToolbarDisplayController(event.eventId); } // Pangea# diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 9b930cf83..301e972f4 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/pages/chat/events/html_message.dart'; import 'package:fluffychat/pages/chat/events/video_player.dart'; +import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; @@ -340,7 +341,7 @@ class MessageContent extends StatelessWidget { ), onListen: () => toolbarController?.showToolbar( context, - mode: MessageMode.play, + mode: MessageMode.conversion, ), ), enableInteractiveSelection: diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index 86946f314..ddc619d27 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/controllers/local_settings.dart'; 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/speech_to_text_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'; @@ -47,6 +48,7 @@ class PangeaController { late InstructionsController instructions; late SubscriptionController subscriptionController; late TextToSpeechController textToSpeech; + late SpeechToTextController speechToText; ///store Services late PLocalStore pStoreService; @@ -93,6 +95,7 @@ class PangeaController { subscriptionController = SubscriptionController(this); itFeedback = ITFeedbackController(this); textToSpeech = TextToSpeechController(this); + speechToText = SpeechToTextController(this); PAuthGaurd.pController = this; } diff --git a/lib/pangea/controllers/speech_to_text_controller.dart b/lib/pangea/controllers/speech_to_text_controller.dart new file mode 100644 index 000000000..c0eefb390 --- /dev/null +++ b/lib/pangea/controllers/speech_to_text_controller.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; + +import '../config/environment.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; + +// Assuming SpeechToTextRequestModel, SpeechToTextResponseModel and related models are already defined as in your provided code. + +class _SpeechToTextCacheItem { + Future data; + + _SpeechToTextCacheItem({required this.data}); +} + +class SpeechToTextController { + static final Map _cache = {}; + late final PangeaController _pangeaController; + Timer? _cacheClearTimer; + + SpeechToTextController(this._pangeaController) { + _initializeCacheClearing(); + } + + void _initializeCacheClearing() { + const duration = Duration(minutes: 15); + _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache()); + } + + void _clearCache() { + _cache.clear(); + } + + void dispose() { + _cacheClearTimer?.cancel(); + } + + Future get( + SpeechToTextRequestModel requestModel) async { + final int cacheKey = requestModel.hashCode; + + if (_cache.containsKey(cacheKey)) { + return _cache[cacheKey]!.data; + } else { + final Future response = _fetchResponse( + accessToken: await _pangeaController.userController.accessToken, + requestModel: requestModel, + ); + _cache[cacheKey] = _SpeechToTextCacheItem(data: response); + return response; + } + } + + static Future _fetchResponse({ + required String accessToken, + required SpeechToTextRequestModel requestModel, + }) async { + final Requests request = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: accessToken, + ); + + final Response res = await request.post( + url: PApiUrls.speechToText, + body: requestModel.toJson(), + ); + + if (res.statusCode == 200) { + final Map json = jsonDecode(utf8.decode(res.bodyBytes)); + return SpeechToTextResponseModel.fromJson(json); + } else { + debugPrint('Error converting speech to text: ${res.body}'); + throw Exception('Failed to convert speech to text'); + } + } +} diff --git a/lib/pangea/controllers/text_to_speech_controller.dart b/lib/pangea/controllers/text_to_speech_controller.dart index b7007ad1a..b34092618 100644 --- a/lib/pangea/controllers/text_to_speech_controller.dart +++ b/lib/pangea/controllers/text_to_speech_controller.dart @@ -126,25 +126,6 @@ class TextToSpeechController { return TextToSpeechResponse.fromJson(json); } - // if (json["wave_form"] == null) { - // json["wave_form"] = getWaveForm(); - // } - - // return TextToSpeechResponse( - // audioContent: String.fromCharCodes(base64Decode(json["audio_content"])), - // mediaType: json["media_type"], - // durationMillis: durationMillis(json["duration_millis"]), - // waveform: getWaveForm(json["audio_content"]), - // ); - // } - - // static List getWaveForm(audioContent) { - // return []; - // } - - // static int durationMillis(audioContent) { - // return 0; - // } static bool isOggFile(Uint8List bytes) { // Check if the file has enough bytes for the header diff --git a/lib/pangea/enum/audio_encoding_enum.dart b/lib/pangea/enum/audio_encoding_enum.dart new file mode 100644 index 000000000..729dec147 --- /dev/null +++ b/lib/pangea/enum/audio_encoding_enum.dart @@ -0,0 +1,56 @@ +AudioEncodingEnum mimeTypeToAudioEncoding(String mimeType) { + switch (mimeType) { + case 'audio/mpeg': + return AudioEncodingEnum.mp3; + case 'audio/mp4': + return AudioEncodingEnum.mp4; + case 'audio/ogg': + return AudioEncodingEnum.oggOpus; + default: + return AudioEncodingEnum.encodingUnspecified; + } +} + +enum AudioEncodingEnum { + encodingUnspecified, + linear16, + flac, + mulaw, + amr, + amrWb, + oggOpus, + speexWithHeaderByte, + mp3, + mp4, + webmOpus, +} + +// Utility extension to map enum values to their corresponding string value as used by the API +extension AudioEncodingExtension on AudioEncodingEnum { + String get value { + switch (this) { + case AudioEncodingEnum.linear16: + return 'LINEAR16'; + case AudioEncodingEnum.flac: + return 'FLAC'; + case AudioEncodingEnum.mulaw: + return 'MULAW'; + case AudioEncodingEnum.amr: + return 'AMR'; + case AudioEncodingEnum.amrWb: + return 'AMR_WB'; + case AudioEncodingEnum.oggOpus: + return 'OGG_OPUS'; + case AudioEncodingEnum.speexWithHeaderByte: + return 'SPEEX_WITH_HEADER_BYTE'; + case AudioEncodingEnum.mp3: + return 'MP3'; + case AudioEncodingEnum.mp4: + return 'MP4'; + case AudioEncodingEnum.webmOpus: + return 'WEBM_OPUS'; + default: + return 'ENCODING_UNSPECIFIED'; + } + } +} diff --git a/lib/pangea/enum/message_mode_enum.dart b/lib/pangea/enum/message_mode_enum.dart new file mode 100644 index 000000000..e4d5c5c5d --- /dev/null +++ b/lib/pangea/enum/message_mode_enum.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum MessageMode { conversion, translation, definition } + +extension MessageModeExtension on MessageMode { + IconData icon(bool isAudioMessage) { + switch (this) { + case MessageMode.translation: + return Icons.g_translate; + case MessageMode.conversion: + return Icons.play_arrow; + //TODO change icon for audio messages + case MessageMode.definition: + return Icons.book; + default: + return Icons.error; // Icon to indicate an error or unsupported mode + } + } + + String title(BuildContext context) { + switch (this) { + case MessageMode.translation: + return L10n.of(context)!.translations; + case MessageMode.conversion: + return L10n.of(context)!.messageAudio; + case MessageMode.definition: + return L10n.of(context)!.definitions; + default: + return L10n.of(context)! + .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode + } + } + + String tooltip(BuildContext context) { + switch (this) { + case MessageMode.translation: + return L10n.of(context)!.translationTooltip; + case MessageMode.conversion: + return L10n.of(context)!.audioTooltip; + case MessageMode.definition: + return L10n.of(context)!.define; + default: + return L10n.of(context)! + .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode + } + } +} diff --git a/lib/pangea/models/pangea_message_event.dart b/lib/pangea/models/pangea_message_event.dart index ebc8c2665..ccc44e49a 100644 --- a/lib/pangea/models/pangea_message_event.dart +++ b/lib/pangea/models/pangea_message_event.dart @@ -148,7 +148,7 @@ class PangeaMessageEvent { }, ); - debugPrint("eventId in getAudioGlobal $eventId"); + debugPrint("eventId in getTextToSpeechGlobal $eventId"); final Event? audioEvent = eventId != null ? await room.getEventById(eventId) : null; @@ -162,10 +162,10 @@ class PangeaMessageEvent { //get audio for text and language //if no audio exists, create it //if audio exists, return it - Future getAudioGlobal(String langCode) async { + Future getTextToSpeechGlobal(String langCode) async { final String text = representationByLanguage(langCode)?.text ?? body; - final local = getAudioLocal(langCode, text); + final local = getTextToSpeechLocal(langCode, text); if (local != null) return Future.value(local); @@ -223,16 +223,16 @@ class PangeaMessageEvent { // .timeout( // Durations.long4, // onTimeout: () { - // debugPrint("timeout in getAudioGlobal"); + // debugPrint("timeout in getTextToSpeechGlobal"); // return null; // }, // ); - debugPrint("eventId in getAudioGlobal $eventId"); + debugPrint("eventId in getTextToSpeechGlobal $eventId"); return eventId != null ? room.getEventById(eventId) : null; } - Event? getAudioLocal(String langCode, String text) { + Event? getTextToSpeechLocal(String langCode, String text) { return allAudio.firstWhereOrNull( (element) { // Safely access the transcription map diff --git a/lib/pangea/models/speech_to_text_models.dart b/lib/pangea/models/speech_to_text_models.dart new file mode 100644 index 000000000..d55de33d9 --- /dev/null +++ b/lib/pangea/models/speech_to_text_models.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart'; +import 'package:flutter/foundation.dart'; + +class SpeechToTextAudioConfigModel { + final AudioEncodingEnum encoding; + final int sampleRateHertz; + final bool enableWordConfidence; + final bool enableAutomaticPunctuation; + final String userL1; + final String userL2; + + SpeechToTextAudioConfigModel({ + required this.encoding, + required this.userL1, + required this.userL2, + this.sampleRateHertz = 16000, + this.enableWordConfidence = true, + this.enableAutomaticPunctuation = true, + }); + + Map toJson() => { + "encoding": encoding.value, + "sample_rate_hertz": sampleRateHertz, + "user_l1": userL1, + "user_l2": userL2, + "enable_word_confidence": enableWordConfidence, + "enable_automatic_punctuation": enableAutomaticPunctuation, + }; +} + +class SpeechToTextRequestModel { + final Uint8List audioContent; + final SpeechToTextAudioConfigModel config; + + SpeechToTextRequestModel({ + required this.audioContent, + required this.config, + }); + + Map toJson() => { + "audio_content": base64Encode(audioContent), + "config": config.toJson(), + }; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! SpeechToTextRequestModel) return false; + + return listEquals(audioContent, other.audioContent) && + config == other.config; + } + + @override + int get hashCode { + final bytesSample = + audioContent.length > 10 ? audioContent.sublist(0, 10) : audioContent; + return Object.hashAll([ + Object.hashAll(bytesSample), + config.hashCode, + ]); + } +} + +class WordInfo { + final String word; + final Duration? startTime; + final Duration? endTime; + final double? confidence; + + WordInfo({ + required this.word, + this.startTime, + this.endTime, + this.confidence, + }); + + factory WordInfo.fromJson(Map json) => WordInfo( + word: json['word'], + startTime: json['start_time'] != null + ? Duration(milliseconds: json['start_time']) + : null, + endTime: json['end_time'] != null + ? Duration(milliseconds: json['end_time']) + : null, + confidence: (json['confidence'] as num?)?.toDouble(), + ); +} + +class Transcript { + final String transcript; + final double confidence; + final List words; + + Transcript({ + required this.transcript, + required this.confidence, + required this.words, + }); + + factory Transcript.fromJson(Map json) => Transcript( + transcript: json['transcript'], + confidence: json['confidence'].toDouble(), + words: + (json['words'] as List).map((e) => WordInfo.fromJson(e)).toList(), + ); +} + +class SpeechToTextResult { + final List transcripts; + + SpeechToTextResult({required this.transcripts}); + + factory SpeechToTextResult.fromJson(Map json) => + SpeechToTextResult( + transcripts: (json['transcripts'] as List) + .map((e) => Transcript.fromJson(e)) + .toList(), + ); +} + +class SpeechToTextResponseModel { + final List results; + + SpeechToTextResponseModel({ + required this.results, + }); + + factory SpeechToTextResponseModel.fromJson(Map json) => + SpeechToTextResponseModel( + results: (json['results'] as List) + .map((e) => SpeechToTextResult.fromJson(e)) + .toList(), + ); +} diff --git a/lib/pangea/network/urls.dart b/lib/pangea/network/urls.dart index d559065ee..16d8bcd23 100644 --- a/lib/pangea/network/urls.dart +++ b/lib/pangea/network/urls.dart @@ -51,6 +51,7 @@ class PApiUrls { static String subseqStep = "/it_step"; static String textToSpeech = "${Environment.choreoApi}/text_to_speech"; + static String speechToText = "${Environment.choreoApi}/speech_to_text"; ///-------------------------------- revenue cat -------------------------- static String rcApiV1 = "https://api.revenuecat.com/v1"; diff --git a/lib/pangea/repo/text_to_speech_repo.dart b/lib/pangea/repo/text_to_speech_repo.dart deleted file mode 100644 index aafd299bd..000000000 --- a/lib/pangea/repo/text_to_speech_repo.dart +++ /dev/null @@ -1,66 +0,0 @@ -// 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_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index ddd8caf61..71e3fa629 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -33,7 +33,7 @@ class MessageAudioCardState extends State { widget.messageEvent.representationByLanguage(langCode)?.text; if (text != null) { final Event? localEvent = - widget.messageEvent.getAudioLocal(langCode, text); + widget.messageEvent.getTextToSpeechLocal(langCode, text); if (localEvent != null) { localAudioEvent = localEvent; if (mounted) setState(() => _isLoading = false); diff --git a/lib/pangea/widgets/chat/message_speech_to_text_card.dart b/lib/pangea/widgets/chat/message_speech_to_text_card.dart new file mode 100644 index 000000000..88b77e6cf --- /dev/null +++ b/lib/pangea/widgets/chat/message_speech_to_text_card.dart @@ -0,0 +1,187 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart'; +import 'package:fluffychat/pangea/models/message_data_models.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class MessageSpeechToTextCard extends StatefulWidget { + final PangeaMessageEvent messageEvent; + + const MessageSpeechToTextCard({ + super.key, + required this.messageEvent, + }); + + @override + MessageSpeechToTextCardState createState() => MessageSpeechToTextCardState(); +} + +enum AudioFileStatus { notDownloaded, downloading, downloaded } + +class MessageSpeechToTextCardState extends State { + PangeaRepresentation? repEvent; + String? transcription; + bool _fetchingTranscription = true; + AudioFileStatus status = AudioFileStatus.notDownloaded; + MatrixFile? matrixFile; + // File? audioFile; + + String? get l1Code => + MatrixState.pangeaController.languageController.activeL1Code( + roomID: widget.messageEvent.room.id, + ); + String? get l2Code => + MatrixState.pangeaController.languageController.activeL2Code( + roomID: widget.messageEvent.room.id, + ); + + // get transcription from local events + Future getLocalTranscription() async { + return "This is a dummy transcription"; + } + + // This code is duplicated from audio_player.dart. Is there some way to reuse that code? + Future _downloadAction() async { + // #Pangea + // if (status != AudioFileStatus.notDownloaded) return; + if (status != AudioFileStatus.notDownloaded) { + return; + } + // Pangea# + setState(() => status = AudioFileStatus.downloading); + try { + // #Pangea + // final matrixFile = await widget.event.downloadAndDecryptAttachment(); + final matrixFile = + await widget.messageEvent.event.downloadAndDecryptAttachment(); + // Pangea# + // File? file; + + // TODO: Test on mobile and see if we need this case + // if (!kIsWeb) { + // final tempDir = await getTemporaryDirectory(); + // final fileName = Uri.encodeComponent( + // // #Pangea + // // widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last, + // widget.messageEvent.event + // .attachmentOrThumbnailMxcUrl()! + // .pathSegments + // .last, + // // Pangea# + // ); + // file = File('${tempDir.path}/${fileName}_${matrixFile.name}'); + // await file.writeAsBytes(matrixFile.bytes); + // } + + // audioFile = file; + this.matrixFile = matrixFile; + status = AudioFileStatus.downloaded; + } catch (e, s) { + Logs().v('Could not download audio file', e, s); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toLocalizedString(context)), + ), + ); + } + } + + AudioEncodingEnum? get encoding { + if (matrixFile == null) return null; + return mimeTypeToAudioEncoding(matrixFile!.mimeType); + } + + // call API to transcribe audio + Future transcribeAudio() async { + await _downloadAction(); + + final localmatrixFile = matrixFile; + final info = matrixFile?.info; + + if (matrixFile == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: 'Audio file or matrix file is null ${widget.messageEvent.eventId}', + s: StackTrace.current, + data: widget.messageEvent.event.content, + ); + return null; + } + + if (l1Code == null || l2Code == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: 'Language codes are null ${widget.messageEvent.eventId}', + s: StackTrace.current, + data: widget.messageEvent.event.content, + ); + return null; + } + + final SpeechToTextResponseModel response = + await MatrixState.pangeaController.speechToText.get( + SpeechToTextRequestModel( + audioContent: matrixFile!.bytes, + config: SpeechToTextAudioConfigModel( + encoding: encoding ?? AudioEncodingEnum.encodingUnspecified, + //this is the default in the RecordConfig in record package + sampleRateHertz: 44100, + userL1: l1Code!, + userL2: l2Code!, + ), + ), + ); + return response.results.first.transcripts.first.transcript; + } + + // look for transcription in message event + // if not found, call API to transcribe audio + Future loadTranscription() async { + // transcription ??= await getLocalTranscription(); + transcription ??= await transcribeAudio(); + setState(() => _fetchingTranscription = false); + } + + @override + void initState() { + super.initState(); + loadTranscription(); + } + + @override + Widget build(BuildContext context) { + // if (!_fetchingTranscription && repEvent == null && transcription == null) { + // return const CardErrorWidget(); + // } + + return Padding( + padding: const EdgeInsets.all(8), + child: _fetchingTranscription + ? SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: Theme.of(context).colorScheme.primary, + ), + ) + : transcription != null + ? Text( + transcription!, + style: BotStyle.text(context), + ) + : Text( + repEvent!.text, + style: BotStyle.text(context), + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 2e9627c71..1a722dac1 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -1,25 +1,27 @@ import 'dart:async'; +import 'dart:developer'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; +import 'package:fluffychat/pangea/enum/message_mode_enum.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; import 'package:fluffychat/pangea/utils/any_state_holder.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart'; import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart'; import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.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:matrix/matrix.dart'; -enum MessageMode { translation, play, definition } - class ToolbarDisplayController { final PangeaMessageEvent pangeaMessageEvent; final String targetId; @@ -90,6 +92,7 @@ class ToolbarDisplayController { ], ); } catch (err) { + debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: StackTrace.current); return; } @@ -147,53 +150,12 @@ class MessageToolbar extends StatefulWidget { } class MessageToolbarState extends State { - Widget? child; + Widget? toolbarContent; MessageMode? currentMode; bool updatingMode = false; late StreamSubscription selectionStream; late StreamSubscription toolbarModeStream; - IconData getIconData(MessageMode mode) { - switch (mode) { - case MessageMode.translation: - return Icons.g_translate; - case MessageMode.play: - return Icons.play_arrow; - case MessageMode.definition: - return Icons.book; - default: - return Icons.error; // Icon to indicate an error or unsupported mode - } - } - - String getModeTitle(MessageMode mode) { - switch (mode) { - case MessageMode.translation: - return L10n.of(context)!.translations; - case MessageMode.play: - return L10n.of(context)!.messageAudio; - case MessageMode.definition: - return L10n.of(context)!.definitions; - default: - return L10n.of(context)! - .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode - } - } - - String getModeTooltip(MessageMode mode) { - switch (mode) { - case MessageMode.translation: - return L10n.of(context)!.translationTooltip; - case MessageMode.play: - return L10n.of(context)!.audioTooltip; - case MessageMode.definition: - return L10n.of(context)!.define; - default: - return L10n.of(context)! - .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode - } - } - void updateMode(MessageMode newMode) { if (updatingMode) return; debugPrint("updating toolbar mode"); @@ -204,8 +166,8 @@ class MessageToolbarState extends State { updatingMode = true; }); if (!subscribed) { - child = MessageUnsubscribedCard( - languageTool: getModeTitle(newMode), + toolbarContent = MessageUnsubscribedCard( + languageTool: newMode.title(context), mode: newMode, toolbarModeStream: widget.toolbarModeStream, ); @@ -214,8 +176,8 @@ class MessageToolbarState extends State { case MessageMode.translation: showTranslation(); break; - case MessageMode.play: - playAudio(); + case MessageMode.conversion: + showConversion(); break; case MessageMode.definition: showDefinition(); @@ -231,28 +193,35 @@ class MessageToolbarState extends State { void showTranslation() { debugPrint("show translation"); - child = MessageTranslationCard( + toolbarContent = MessageTranslationCard( messageEvent: widget.pangeaMessageEvent, immersionMode: widget.immersionMode, selection: widget.textSelection, ); } - void playAudio() { - debugPrint("play audio"); - child = MessageAudioCard( - messageEvent: widget.pangeaMessageEvent, - ); + void showConversion() { + debugPrint("show conversion"); + if (isAudioMessage) { + debugPrint("is audio message"); + toolbarContent = MessageSpeechToTextCard( + messageEvent: widget.pangeaMessageEvent, + ); + } else { + toolbarContent = MessageAudioCard( + messageEvent: widget.pangeaMessageEvent, + ); + } } void showDefinition() { if (widget.textSelection.selectedText == null || widget.textSelection.selectedText!.isEmpty) { - child = const SelectToDefine(); + toolbarContent = const SelectToDefine(); return; } - child = WordDataCard( + toolbarContent = WordDataCard( word: widget.textSelection.selectedText!, wordLang: widget.pangeaMessageEvent.messageDisplayLangCode, fullText: widget.textSelection.messageText, @@ -262,6 +231,10 @@ class MessageToolbarState extends State { ); } + bool get isAudioMessage { + return widget.pangeaMessageEvent.event.messageType == MessageTypes.Audio; + } + void showImage() {} void spellCheck() {} @@ -286,7 +259,7 @@ class MessageToolbarState extends State { ) ?? true; autoplay - ? updateMode(MessageMode.play) + ? updateMode(MessageMode.conversion) : updateMode(MessageMode.translation); }); @@ -339,8 +312,8 @@ class MessageToolbarState extends State { duration: FluffyThemes.animationDuration, child: Column( children: [ - child ?? const SizedBox(), - SizedBox(height: child == null ? 0 : 20), + toolbarContent ?? const SizedBox(), + SizedBox(height: toolbarContent == null ? 0 : 20), ], ), ), @@ -349,10 +322,13 @@ class MessageToolbarState extends State { Row( mainAxisSize: MainAxisSize.min, children: MessageMode.values.map((mode) { + if (mode == MessageMode.definition && isAudioMessage) { + return const SizedBox.shrink(); + } return Tooltip( - message: getModeTooltip(mode), + message: mode.tooltip(context), child: IconButton( - icon: Icon(getIconData(mode)), + icon: Icon(mode.icon(isAudioMessage)), color: currentMode == mode ? Theme.of(context).colorScheme.primary : null, diff --git a/lib/pangea/widgets/chat/message_unsubscribed_card.dart b/lib/pangea/widgets/chat/message_unsubscribed_card.dart index 2d37328ec..c0574aa6f 100644 --- a/lib/pangea/widgets/chat/message_unsubscribed_card.dart +++ b/lib/pangea/widgets/chat/message_unsubscribed_card.dart @@ -2,11 +2,12 @@ import 'dart:async'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import '../../enum/message_mode_enum.dart'; + class MessageUnsubscribedCard extends StatelessWidget { final String languageTool; final MessageMode mode; diff --git a/lib/pangea/widgets/chat/text_to_speech_button.dart b/lib/pangea/widgets/chat/text_to_speech_button.dart index ec3dbd0db..5facb684b 100644 --- a/lib/pangea/widgets/chat/text_to_speech_button.dart +++ b/lib/pangea/widgets/chat/text_to_speech_button.dart @@ -50,7 +50,7 @@ class _TextToSpeechButtonState extends State { Event? get localAudioEvent => langCode != null && text != null && text!.isNotEmpty - ? _pangeaMessageEvent.getAudioLocal(langCode!, text!) + ? _pangeaMessageEvent.getTextToSpeechLocal(langCode!, text!) : null; String? get langCode => @@ -69,7 +69,7 @@ class _TextToSpeechButtonState extends State { if (langCode == null || langCode!.isEmpty) return; setState(() => _isLoading = true); - await _pangeaMessageEvent.getAudioGlobal(langCode!); + await _pangeaMessageEvent.getTextToSpeechGlobal(langCode!); setState(() => _isLoading = false); } catch (e) { setState(() => _isLoading = false); diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index fb0fcef1c..e083a67b6 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import '../../enum/message_mode_enum.dart'; import '../../models/pangea_match_model.dart'; class PangeaRichText extends StatefulWidget { @@ -157,7 +158,7 @@ class PangeaRichTextState extends State { ), onListen: () => widget.toolbarController?.showToolbar( context, - mode: MessageMode.play, + mode: MessageMode.conversion, ), ), TextSpan( diff --git a/needed-translations.txt b/needed-translations.txt index ce3531669..b5d0fb74a 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -815,7 +815,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "bn": [ @@ -1649,7 +1668,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "bo": [ @@ -2483,7 +2521,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "ca": [ @@ -3317,7 +3374,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "cs": [ @@ -4151,7 +4227,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "de": [ @@ -4954,7 +5049,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "el": [ @@ -5788,7 +5902,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "eo": [ @@ -6622,20 +6755,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" - ], - - "es": [ - "errorGettingAudio", - "define", - "listen", - "showDefinition", - "acceptedKeyVerification", - "canceledKeyVerification", - "completedKeyVerification", - "isReadyForKeyVerification", - "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "et": [ @@ -7454,7 +7593,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "eu": [ @@ -8257,7 +8415,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "fa": [ @@ -9091,7 +9268,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "fi": [ @@ -9925,7 +10121,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "fr": [ @@ -10759,7 +10974,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "ga": [ @@ -11593,7 +11827,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "gl": [ @@ -12396,7 +12649,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "he": [ @@ -13230,7 +13502,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "hi": [ @@ -14064,7 +14355,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "hr": [ @@ -14885,7 +15195,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "hu": [ @@ -15719,7 +16048,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "id": [ @@ -16553,7 +16901,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "ie": [ @@ -17387,7 +17754,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "it": [ @@ -18206,7 +18592,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "ja": [ @@ -19040,7 +19445,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "ko": [ @@ -19874,7 +20298,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "lt": [ @@ -20708,7 +21151,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "lv": [ @@ -21542,7 +22004,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "nb": [ @@ -22376,7 +22857,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "nl": [ @@ -23210,7 +23710,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "pl": [ @@ -24044,7 +24563,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "pt": [ @@ -24878,7 +25416,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "pt_BR": [ @@ -25681,7 +26238,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "pt_PT": [ @@ -26515,7 +27091,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "ro": [ @@ -27349,7 +27944,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "ru": [ @@ -28156,7 +28770,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "sk": [ @@ -28990,7 +29623,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "sl": [ @@ -29824,7 +30476,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "sr": [ @@ -30658,7 +31329,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "sv": [ @@ -31492,7 +32182,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "ta": [ @@ -32326,7 +33035,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "th": [ @@ -33160,7 +33888,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "tr": [ @@ -33979,7 +34726,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "uk": [ @@ -34798,7 +35564,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "vi": [ @@ -35632,7 +36417,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "zh": [ @@ -36451,7 +37255,26 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ], "zh_Hant": [ @@ -37285,6 +38108,25 @@ "completedKeyVerification", "isReadyForKeyVerification", "requestedKeyVerification", - "startedKeyVerification" + "startedKeyVerification", + "subscriptionPopupTitle", + "subscriptionPopupDesc", + "seeOptions", + "continuedWithoutSubscription", + "trialPeriodExpired", + "selectToDefine", + "translations", + "messageAudio", + "definitions", + "subscribedToUnlockTools", + "more", + "translationTooltip", + "audioTooltip", + "certifyAge", + "kickBotWarning", + "joinToView", + "refresh", + "autoPlayTitle", + "autoPlayDesc" ] }