Merge pull request #3182 from pangeachat/3168-allow-clicking-of-words-in-transcripts

feat: make tokens in STT transcript clickable
pull/2245/head
ggurdin 5 months ago committed by GitHub
commit 12e66cfcdc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -28,9 +28,6 @@ class VocabDetailsView extends StatelessWidget {
ConstructUses get _construct => constructId.constructUses;
String? get _userL1 =>
MatrixState.pangeaController.languageController.userL1?.langCode;
/// Get the language code for the current lemma
String? get _userL2 =>
MatrixState.pangeaController.languageController.userL2?.langCode;

@ -256,14 +256,7 @@ class IgcController {
timeline: choreographer.chatController.timeline!,
ownMessage: event.senderId ==
choreographer.pangeaController.matrixState.client.userID,
)
.getSpeechToTextLocal(
choreographer.l1LangCode,
choreographer.l2LangCode,
)
?.transcript
.text
.trim(); // trim whitespace
).getSpeechToTextLocal()?.transcript.text.trim(); // trim whitespace
if (content == null) continue;
messages.add(
PreviousMessage(

@ -232,13 +232,7 @@ class PangeaMessageEvent {
null;
}).toSet();
SpeechToTextModel? getSpeechToTextLocal(
String? l1Code,
String? l2Code,
) {
if (l1Code == null || l2Code == null) {
return null;
}
SpeechToTextModel? getSpeechToTextLocal() {
return representations
.firstWhereOrNull(
(element) => element.content.speechToText != null,

@ -53,7 +53,7 @@ class LemmaReactionPickerState extends State<LemmaReactionPicker> {
} catch (e, s) {
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
} finally {
setState(() => loading = false);
if (mounted) setState(() => loading = false);
}
}

@ -1,29 +1,85 @@
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
class TokenPositionModel {
/// Start index of the full substring in the message
final int start;
class TokenPosition {
final PangeaToken? token;
final int startIndex;
final int endIndex;
const TokenPosition({
this.token,
required this.startIndex,
required this.endIndex,
});
}
/// End index of the full substring in the message
final int end;
class TokensUtil {
static List<TokenPosition> getTokenPositions(
List<PangeaToken> tokens,
) {
final List<TokenPosition> tokenPositions = [];
int tokenPointer = 0;
int globalPointer = 0;
/// Start index of the token in the message
final int tokenStart;
while (tokenPointer < tokens.length) {
int endIndex = tokenPointer;
PangeaToken token = tokens[tokenPointer];
/// End index of the token in the message
final int tokenEnd;
if (token.text.offset > globalPointer) {
// If the token starts after the current global pointer, we need to
// create a new token position for the gap
tokenPositions.add(
TokenPosition(
startIndex: globalPointer,
endIndex: token.text.offset,
),
);
final bool selected;
final bool hideContent;
final PangeaToken? token;
globalPointer = token.text.offset;
}
const TokenPositionModel({
required this.start,
required this.end,
required this.tokenStart,
required this.tokenEnd,
required this.hideContent,
required this.selected,
this.token,
});
// move the end index if the next token is right next to the current token
// and either the current token is punctuation or the next token is punctuation
while (endIndex < tokens.length - 1) {
final PangeaToken currentToken = tokens[endIndex];
final PangeaToken nextToken = tokens[endIndex + 1];
final currentIsPunct = currentToken.pos == 'PUNCT' &&
currentToken.text.content.trim().isNotEmpty;
final nextIsPunct = nextToken.pos == 'PUNCT' &&
nextToken.text.content.trim().isNotEmpty;
if (currentToken.text.offset + currentToken.text.length !=
nextToken.text.offset) {
break;
}
if ((currentIsPunct && nextIsPunct) ||
(currentIsPunct && nextToken.text.content.trim().isNotEmpty) ||
(nextIsPunct && currentToken.text.content.trim().isNotEmpty)) {
if (token.pos == 'PUNCT' && !nextIsPunct) {
token = nextToken;
}
endIndex++;
} else {
break;
}
}
tokenPositions.add(
TokenPosition(
token: token,
startIndex: tokens[tokenPointer].text.offset,
endIndex: tokens[endIndex].text.offset + tokens[endIndex].text.length,
),
);
// Move to the next token
tokenPointer = tokenPointer + (endIndex - tokenPointer) + 1;
globalPointer =
tokens[endIndex].text.offset + tokens[endIndex].text.length;
}
return tokenPositions;
}
}

@ -461,9 +461,18 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
pangeaMessageEvent?.messageDisplayLangCode.split("-")[0] ==
MatrixState.pangeaController.languageController.userL2?.langCodeShort;
PangeaToken? get selectedToken =>
pangeaMessageEvent?.messageDisplayRepresentation?.tokens
?.firstWhereOrNull(isTokenSelected);
PangeaToken? get selectedToken {
if (pangeaMessageEvent?.isAudioMessage == true) {
final stt = pangeaMessageEvent!.getSpeechToTextLocal();
if (stt == null || stt.transcript.sttTokens.isEmpty) return null;
return stt.transcript.sttTokens
.firstWhereOrNull((t) => isTokenSelected(t.token))
?.token;
}
return pangeaMessageEvent?.messageDisplayRepresentation?.tokens
?.firstWhereOrNull(isTokenSelected);
}
/// Whether the overlay is currently displaying a selection
bool get isSelection => _selectedSpan != null || _highlightedTokens != null;

@ -15,6 +15,7 @@ import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/stt_transcript_tokens.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/file_description.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -177,15 +178,17 @@ class OverlayMessage extends StatelessWidget {
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
overlayController
.transcription!.transcript.text,
SttTranscriptTokens(
model: overlayController.transcription!,
style: AppConfig.messageTextStyle(
event,
textColor,
).copyWith(
fontStyle: FontStyle.italic,
),
onClick: overlayController
.onClickOverlayMessageToken,
isSelected: overlayController.isTokenSelected,
),
if (MatrixState.pangeaController
.languageController.showTrancription)

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/message_token_text/token_position_model.dart';
import 'package:fluffychat/pangea/toolbar/models/speech_to_text_models.dart';
import 'package:fluffychat/widgets/matrix.dart';
class SttTranscriptTokens extends StatelessWidget {
final SpeechToTextModel model;
final TextStyle? style;
final void Function(PangeaToken)? onClick;
final bool Function(PangeaToken)? isSelected;
const SttTranscriptTokens({
super.key,
required this.model,
this.onClick,
this.isSelected,
this.style,
});
List<PangeaToken> get tokens =>
model.transcript.sttTokens.map((t) => t.token).toList();
@override
Widget build(BuildContext context) {
if (model.transcript.sttTokens.isEmpty) {
return Text(
model.transcript.text,
style: style ?? DefaultTextStyle.of(context).style,
textScaler: TextScaler.noScaling,
);
}
final messageCharacters = model.transcript.text.characters;
return RichText(
textScaler: TextScaler.noScaling,
text: TextSpan(
style: style ?? DefaultTextStyle.of(context).style,
children: TokensUtil.getTokenPositions(tokens).map((tokenPosition) {
final text = messageCharacters
.skip(tokenPosition.startIndex)
.take(tokenPosition.endIndex - tokenPosition.startIndex)
.toString();
if (tokenPosition.token == null) {
return TextSpan(
text: text,
style: style ?? DefaultTextStyle.of(context).style,
);
}
final token = tokenPosition.token!;
final selected = isSelected?.call(token) ?? false;
return WidgetSpan(
child: CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey(token.text.uniqueKey)
.link,
child: MouseRegion(
key: MatrixState.pAnyState
.layerLinkAndKey(token.text.uniqueKey)
.key,
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onClick != null ? () => onClick?.call(token) : null,
child: RichText(
text: TextSpan(
text: text,
style: (style ?? DefaultTextStyle.of(context).style)
.copyWith(
decoration: TextDecoration.underline,
decorationThickness: 4,
decorationColor: selected
? Theme.of(context).colorScheme.primary
: Colors.white.withAlpha(0),
),
),
),
),
),
),
);
}).toList(),
),
);
}
}
Loading…
Cancel
Save