From ac80e6217cd22c515b10dbc020da49490af874bb Mon Sep 17 00:00:00 2001
From: ggurdin <46800240+ggurdin@users.noreply.github.com>
Date: Mon, 21 Oct 2024 12:53:34 -0400
Subject: [PATCH] Audio section widget (#744)
first draft of word focus listening activities using text to speech library
---
android/app/src/main/AndroidManifest.xml | 6 +
assets/l10n/intl_en.arb | 4 +
lib/pages/chat/events/audio_player.dart | 184 ++++++++--
...actice_activity_generation_controller.dart | 2 +-
.../text_to_speech_controller.dart | 103 +++++-
.../activity_display_instructions_enum.dart | 11 +-
lib/pangea/enum/activity_type_enum.dart | 10 +-
lib/pangea/enum/construct_use_type_enum.dart | 109 +++---
.../extensions/pangea_event_extension.dart | 42 +++
.../pangea_message_event.dart | 144 +++-----
lib/pangea/models/headwords.dart | 334 +++++++++---------
lib/pangea/models/pangea_token_model.dart | 4 -
.../message_activity_request.dart | 16 +-
.../multiple_choice_activity_model.dart | 8 +-
.../practice_activity_model.dart | 137 +------
.../widgets/chat/message_audio_card.dart | 168 +++++++--
.../chat/message_selection_overlay.dart | 25 +-
lib/pangea/widgets/chat/message_toolbar.dart | 1 +
.../widgets/chat/message_toolbar_buttons.dart | 48 ++-
.../widgets/chat/missing_voice_button.dart | 61 ++++
lib/pangea/widgets/chat/tts_controller.dart | 77 ++++
.../multiple_choice_activity.dart | 26 +-
.../practice_activity_card.dart | 9 +-
.../target_tokens_controller.dart | 20 +-
.../practice_activity/word_audio_button.dart | 69 ++++
.../word_focus_listening_activity.dart | 173 +++++++++
macos/Flutter/GeneratedPluginRegistrant.swift | 2 +
pubspec.lock | 16 +
pubspec.yaml | 2 +
.../flutter/generated_plugin_registrant.cc | 3 +
windows/flutter/generated_plugins.cmake | 1 +
31 files changed, 1221 insertions(+), 594 deletions(-)
create mode 100644 lib/pangea/widgets/chat/missing_voice_button.dart
create mode 100644 lib/pangea/widgets/chat/tts_controller.dart
create mode 100644 lib/pangea/widgets/practice_activity/word_audio_button.dart
create mode 100644 lib/pangea/widgets/practice_activity/word_focus_listening_activity.dart
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 2eb411a23..68c90c59e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -158,4 +158,10 @@
android:name="flutterEmbedding"
android:value="2" />
+
+
+
+
+
+
diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb
index 83de9e423..4e709a9c2 100644
--- a/assets/l10n/intl_en.arb
+++ b/assets/l10n/intl_en.arb
@@ -4239,6 +4239,10 @@
"l2SupportAlpha": "Alpha",
"l2SupportBeta": "Beta",
"l2SupportFull": "Full",
+ "voiceNotAvailable": "It looks like you don't have a voice installed for this language.",
+ "openVoiceSettings": "Click here to open voice settings",
+ "playAudio": "Play",
+ "stop": "Stop",
"grammarCopySCONJ": "Subordinating Conjunction",
"grammarCopyNUM": "Number",
"grammarCopyVERB": "Verb",
diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart
index 3213d085f..66417d921 100644
--- a/lib/pages/chat/events/audio_player.dart
+++ b/lib/pages/chat/events/audio_player.dart
@@ -25,7 +25,13 @@ class AudioPlayerWidget extends StatefulWidget {
static String? currentId;
- static const int wavesCount = 40;
+ // #Pangea
+ // static const int wavesCount = 40;
+ static const int wavesCount = kIsWeb ? 100 : 40;
+
+ final int? sectionStartMS;
+ final int? sectionEndMS;
+ // Pangea#
const AudioPlayerWidget(
this.event, {
@@ -33,6 +39,8 @@ class AudioPlayerWidget extends StatefulWidget {
// #Pangea
this.matrixFile,
this.autoplay = false,
+ this.sectionStartMS,
+ this.sectionEndMS,
// Pangea#
super.key,
});
@@ -72,6 +80,24 @@ class AudioPlayerState extends State {
super.dispose();
}
+ // #Pangea
+ // @override
+ // void didUpdateWidget(covariant oldWidget) {
+ // if ((oldWidget.sectionEndMS != widget.sectionEndMS) ||
+ // (oldWidget.sectionStartMS != widget.sectionStartMS)) {
+ // debugPrint('selection changed');
+ // if (widget.sectionStartMS != null) {
+ // audioPlayer?.seek(Duration(milliseconds: widget.sectionStartMS!));
+ // audioPlayer?.play();
+ // } else {
+ // audioPlayer?.stop();
+ // audioPlayer?.seek(null);
+ // }
+ // }
+ // super.didUpdateWidget(oldWidget);
+ // }
+ // Pangea#
+
Future _downloadAction() async {
// #Pangea
// if (status != AudioPlayerStatus.notDownloaded) return;
@@ -160,7 +186,16 @@ class AudioPlayerState extends State {
AudioPlayerWidget.wavesCount)
.round();
});
+ // #Pangea
+ // if (widget.sectionStartMS != null &&
+ // widget.sectionEndMS != null &&
+ // state.inMilliseconds.toDouble() >= widget.sectionEndMS!) {
+ // audioPlayer.stop();
+ // audioPlayer.seek(Duration(milliseconds: widget.sectionStartMS!));
+ // } else
if (state.inMilliseconds.toDouble() == maxPosition) {
+ // if (state.inMilliseconds.toDouble() == maxPosition) {
+ // Pangea#
audioPlayer.stop();
audioPlayer.seek(null);
}
@@ -194,6 +229,11 @@ class AudioPlayerState extends State {
}
// Pangea#
}
+ // #Pangea
+ // if (widget.sectionStartMS != null) {
+ // audioPlayer.seek(Duration(milliseconds: widget.sectionStartMS!));
+ // }
+ // Pangea#
audioPlayer.play().onError(
ErrorReporter(context, 'Unable to play audio message')
.onErrorCallback,
@@ -311,6 +351,17 @@ class AudioPlayerState extends State {
final statusText = this.statusText ??= _durationString ?? '00:00';
final audioPlayer = this.audioPlayer;
+
+ // #Pangea
+ final msPerWave = (maxPosition / AudioPlayerWidget.wavesCount);
+ final int? startWave = widget.sectionStartMS != null && msPerWave > 0
+ ? (widget.sectionStartMS! / msPerWave).floor()
+ : null;
+ final int? endWave = widget.sectionEndMS != null && msPerWave > 0
+ ? (widget.sectionEndMS! / msPerWave).ceil()
+ : null;
+ // Pangea#
+
return Padding(
// #Pangea
// padding: const EdgeInsets.all(12.0),
@@ -352,44 +403,101 @@ class AudioPlayerState extends State {
// #Pangea
// const SizedBox(width: 8),
const SizedBox(width: 5),
- // Pangea#
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
- GestureDetector(
- onTapDown: (_) => audioPlayer?.seek(
- Duration(
- milliseconds:
- (maxPosition / AudioPlayerWidget.wavesCount).round() *
- i,
- ),
- ),
- child: Container(
- height: 32,
- color: widget.color.withAlpha(0),
- alignment: Alignment.center,
- child: Opacity(
- opacity: currentPosition > i ? 1 : 0.5,
- child: Container(
- margin: const EdgeInsets.symmetric(horizontal: 1),
- decoration: BoxDecoration(
- color: widget.color,
- borderRadius: BorderRadius.circular(2),
- ),
- // #Pangea
- // width: 2,
- width: 1,
- // Pangea#
- height: 32 * (waveform[i] / 1024),
- ),
- ),
- ),
- ),
- ],
- ),
- // #Pangea
+ // Row(
+ // mainAxisSize: MainAxisSize.min,
+ // children: [
+ // for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
+ // GestureDetector(
+ // onTapDown: (_) => audioPlayer?.seek(
+ // Duration(
+ // milliseconds:
+ // (maxPosition / AudioPlayerWidget.wavesCount).round() *
+ // i,
+ // ),
+ // ),
+ // child: Container(
+ // height: 32,
+ // color: widget.color.withAlpha(0),
+ // alignment: Alignment.center,
+ // child: Opacity(
+ // opacity: currentPosition > i ? 1 : 0.5,
+ // child: Container(
+ // margin: const EdgeInsets.symmetric(horizontal: 1),
+ // decoration: BoxDecoration(
+ // color: widget.color,
+ // borderRadius: BorderRadius.circular(2),
+ // ),
+ // // #Pangea
+ // // width: 2,
+ // width: 1,
+ // // Pangea#
+ // height: 32 * (waveform[i] / 1024),
+ // ),
+ // ),
+ // ),
+ // ),
+ // ],
+ // ),
// const SizedBox(width: 8),
+ Expanded(
+ child: Row(
+ children: [
+ for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
+ Builder(
+ builder: (context) {
+ final double barOpacity = currentPosition > i ? 1 : 0.5;
+ return Expanded(
+ child: GestureDetector(
+ onTapDown: (_) {
+ audioPlayer?.seek(
+ Duration(
+ milliseconds:
+ (maxPosition / AudioPlayerWidget.wavesCount)
+ .round() *
+ i,
+ ),
+ );
+ },
+ child: Stack(
+ children: [
+ Container(
+ margin: const EdgeInsets.symmetric(
+ horizontal: 0.5,
+ ),
+ decoration: BoxDecoration(
+ color: widget.color.withOpacity(barOpacity),
+ borderRadius: BorderRadius.circular(2),
+ ),
+ height: 32 * (waveform[i] / 1024),
+ ),
+ ],
+ ),
+ ),
+ );
+ // return Container(
+ // height: 32,
+ // width: 2,
+ // alignment: Alignment.center,
+ // child: Opacity(
+ // opacity: barOpacity,
+ // child: Container(
+ // margin: const EdgeInsets.symmetric(
+ // horizontal: 1,
+ // ),
+ // decoration: BoxDecoration(
+ // color: widget.color,
+ // borderRadius: BorderRadius.circular(2),
+ // ),
+ // height: 32 * (waveform[i] / 1024),
+ // width: 2,
+ // ),
+ // ),
+ // );
+ },
+ ),
+ ],
+ ),
+ ),
const SizedBox(width: 5),
// SizedBox(
// width: 36,
diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart
index 1d3c7f7ae..a8d7cca36 100644
--- a/lib/pangea/controllers/practice_activity_generation_controller.dart
+++ b/lib/pangea/controllers/practice_activity_generation_controller.dart
@@ -162,7 +162,7 @@ class PracticeGenerationController {
activityType: ActivityTypeEnum.multipleChoice,
langCode: event.messageDisplayLangCode,
msgId: event.eventId,
- multipleChoice: MultipleChoice(
+ content: ActivityContent(
question: "What is a synonym for 'happy'?",
choices: ["sad", "angry", "joyful", "tired"],
answer: "joyful",
diff --git a/lib/pangea/controllers/text_to_speech_controller.dart b/lib/pangea/controllers/text_to_speech_controller.dart
index 069722590..e032c4045 100644
--- a/lib/pangea/controllers/text_to_speech_controller.dart
+++ b/lib/pangea/controllers/text_to_speech_controller.dart
@@ -5,20 +5,93 @@ import 'dart:typed_data';
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/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/network/urls.dart';
import 'package:http/http.dart';
import '../network/requests.dart';
-class TextToSpeechRequest {
- String text;
- String langCode;
+class PangeaAudioEventData {
+ final String text;
+ final String langCode;
+ final List tokens;
- TextToSpeechRequest({required this.text, required this.langCode});
+ PangeaAudioEventData({
+ required this.text,
+ required this.langCode,
+ required this.tokens,
+ });
+
+ factory PangeaAudioEventData.fromJson(dynamic json) => PangeaAudioEventData(
+ text: json[ModelKey.text] as String,
+ langCode: json[ModelKey.langCode] as String,
+ tokens: List.from(
+ (json[ModelKey.tokens] as Iterable)
+ .map((x) => TTSToken.fromJson(x))
+ .toList(),
+ ),
+ );
Map toJson() => {
ModelKey.text: text,
ModelKey.langCode: langCode,
+ ModelKey.tokens:
+ List