Highlight audio text (#1333)

* Highlight text as TTS plays - attempt 1

* Make highlighting actually work

* Fix to minor version of punctuation issue

* Highlights all applicable text

* fix: filter out punctuation tokens in the client side when highlighing audio tokens

* Highlight selection separate from normal selection

* cleanup: further decouple tts highlighting and token selection, renamed temporarySelection => _highlightedTokens

* fix: don't show token highlights for non-overlay messages

---------

Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com>
Co-authored-by: ggurdin <ggurdin@gmail.com>
pull/1593/head
Kelrap 10 months ago committed by GitHub
parent 3d85d2ec9f
commit 6f63a6d710
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -13,6 +13,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/error_reporter.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/utils/url_launcher.dart';
@ -28,6 +29,7 @@ class AudioPlayerWidget extends StatefulWidget {
final bool autoplay; final bool autoplay;
final Function(bool)? setIsPlayingAudio; final Function(bool)? setIsPlayingAudio;
final double padding; final double padding;
final MessageOverlayController? overlayController;
// Pangea# // Pangea#
static String? currentId; static String? currentId;
@ -50,6 +52,7 @@ class AudioPlayerWidget extends StatefulWidget {
this.sectionEndMS, this.sectionEndMS,
this.setIsPlayingAudio, this.setIsPlayingAudio,
this.padding = 12.0, this.padding = 12.0,
this.overlayController,
// Pangea# // Pangea#
super.key, super.key,
}); });
@ -187,6 +190,15 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
audioPlayer.stop(); audioPlayer.stop();
audioPlayer.seek(null); audioPlayer.seek(null);
} }
// #Pangea
// Pass current timestamp to overlay, so it can highlight as necessary
if (widget.matrixFile != null) {
widget.overlayController?.highlightCurrentText(
state.inMilliseconds,
widget.matrixFile!.tokens,
);
}
// Pangea#
}); });
onDurationChanged ??= audioPlayer.durationStream.listen((max) { onDurationChanged ??= audioPlayer.durationStream.listen((max) {
if (max == null || max == Duration.zero) return; if (max == null || max == Duration.zero) return;

@ -337,7 +337,12 @@ class MessageContent extends StatelessWidget {
selectedToken: token, selectedToken: token,
); );
}, },
isSelected: overlayController?.isTokenSelected, isSelected: overlayController != null
? (token) {
return overlayController!.isTokenSelected(token) ||
overlayController!.isTokenHighlighted(token);
}
: null,
); );
} }

@ -64,7 +64,7 @@ class LemmaActivityGenerator {
final choices = sortedLemmas.take(4).toList(); final choices = sortedLemmas.take(4).toList();
if (!choices.contains(token.lemma.text)) { if (!choices.contains(token.lemma.text)) {
final random = Random(); final random = Random();
choices[random.nextInt(4)] = token.lemma.text; choices[random.nextInt(choices.length - 1)] = token.lemma.text;
} }
return choices; return choices;
} }

@ -201,6 +201,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
fontSize: fontSize:
AppConfig.messageFontSize * AppConfig.fontSizeFactor, AppConfig.messageFontSize * AppConfig.fontSizeFactor,
padding: 0, padding: 0,
overlayController: widget.overlayController,
) )
: const CardErrorWidget( : const CardErrorWidget(
error: "Null audio file in message_audio_card", error: "Null audio file in message_audio_card",

@ -17,6 +17,7 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart'; import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar.dart';
@ -61,6 +62,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
MessageMode toolbarMode = MessageMode.noneSelected; MessageMode toolbarMode = MessageMode.noneSelected;
PangeaTokenText? _selectedSpan; PangeaTokenText? _selectedSpan;
List<PangeaTokenText>? _highlightedTokens;
List<PangeaToken>? tokens; List<PangeaToken>? tokens;
bool initialized = false; bool initialized = false;
@ -129,6 +131,26 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
setState(() {}); setState(() {});
} }
/// If sentence TTS is playing a word, highlight that word in message overlay
void highlightCurrentText(int currentPosition, List<TTSToken> ttsTokens) {
final List<TTSToken> textToSelect = [];
// Check if current time is between start and end times of tokens
for (final TTSToken token in ttsTokens) {
if (token.endMS > currentPosition) {
if (token.startMS < currentPosition) {
textToSelect.add(token);
} else {
break;
}
}
}
if (const ListEquality().equals(textToSelect, _highlightedTokens)) return;
_highlightedTokens =
textToSelect.isEmpty ? null : textToSelect.map((t) => t.text).toList();
setState(() {});
}
void _setupSubscriptions() { void _setupSubscriptions() {
_animationController = AnimationController( _animationController = AnimationController(
vsync: this, vsync: this,
@ -278,6 +300,9 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
debugPrint("updateToolbarMode: $mode - clearing selectedSpan"); debugPrint("updateToolbarMode: $mode - clearing selectedSpan");
_selectedSpan = null; _selectedSpan = null;
} }
if (mode != MessageMode.textToSpeech) {
_highlightedTokens = null;
}
toolbarMode = mode; toolbarMode = mode;
}); });
} }
@ -327,17 +352,24 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
setState(() {}); setState(() {});
} }
/// Whether the given token is currently selected /// Whether the given token is currently selected or highlighted
bool isTokenSelected(PangeaToken token) { bool isTokenSelected(PangeaToken token) {
final isSelected = _selectedSpan?.offset == token.text.offset && final isSelected = _selectedSpan?.offset == token.text.offset &&
_selectedSpan?.length == token.text.length; _selectedSpan?.length == token.text.length;
return isSelected; return isSelected;
} }
bool isTokenHighlighted(PangeaToken token) {
if (_highlightedTokens == null) return false;
return _highlightedTokens!.any(
(t) => t.offset == token.text.offset && t.length == token.text.length,
);
}
PangeaToken? get selectedToken => tokens?.firstWhereOrNull(isTokenSelected); PangeaToken? get selectedToken => tokens?.firstWhereOrNull(isTokenSelected);
/// Whether the overlay is currently displaying a selection /// Whether the overlay is currently displaying a selection
bool get isSelection => _selectedSpan != null; bool get isSelection => _selectedSpan != null || _highlightedTokens != null;
PangeaTokenText? get selectedSpan => _selectedSpan; PangeaTokenText? get selectedSpan => _selectedSpan;

Loading…
Cancel
Save