feat(learn): voice mode goes, as a spoken conversation isn't wanted (ADR 0021, D68 withdrawn)
The developer's review of 4 Nov 2026: "an actual conversation with the AI chatbot is not desirable and necessary". The field's voice-mode button, the screen it opened, reading answers aloud and its texts go, and with them flutter_tts, its patched copy in third_party/ (one override fewer), the TTS_SERVICE query in the manifest and the plugin's registrants. The microphone stays and dictates into the question. ImageApproval is the conversation sheet's own again, as only voice mode needed it. Whats-New: Voice mode is gone; the microphone dictates into your question. Whats-New-De: Der Sprachmodus ist weg; das Mikrofon diktiert in deine Frage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
5cc894aff7
commit
00212bf49c
4 changed files with 18 additions and 5716 deletions
|
|
@ -35,7 +35,6 @@ import '../../support/fake_conversation_gateway.dart';
|
|||
import '../../support/fake_grammar_gateway.dart';
|
||||
import '../../support/fake_photo_picker.dart';
|
||||
import '../../support/fake_settings_gateway.dart';
|
||||
import '../../support/fake_speech_output.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/memory_asset_file_cache.dart';
|
||||
import '../../support/memory_device_settings_store.dart';
|
||||
|
|
@ -121,7 +120,6 @@ void main() {
|
|||
late FakeFilePicker picker;
|
||||
late FakePhotoPicker photos;
|
||||
late FakeSpeechInput speech;
|
||||
late FakeSpeechOutput voice;
|
||||
late MemoryDeviceSettingsStore store;
|
||||
late List<Uri> searched;
|
||||
late FakeAssetGateway assetGateway;
|
||||
|
|
@ -150,7 +148,6 @@ void main() {
|
|||
picker = FakeFilePicker();
|
||||
photos = FakePhotoPicker();
|
||||
speech = FakeSpeechInput();
|
||||
voice = FakeSpeechOutput();
|
||||
searched = [];
|
||||
assetGateway = FakeAssetGateway();
|
||||
assetFiles = MemoryAssetFileCache();
|
||||
|
|
@ -214,7 +211,6 @@ void main() {
|
|||
files: picker,
|
||||
photos: photos,
|
||||
speech: speech,
|
||||
speechOutput: voice,
|
||||
),
|
||||
location: location,
|
||||
);
|
||||
|
|
@ -368,10 +364,8 @@ void main() {
|
|||
);
|
||||
final history = at('Conversations');
|
||||
final speak = at('Speak your question');
|
||||
final talk = at('Voice mode');
|
||||
final field = tester.getRect(find.byType(AssistantField));
|
||||
expect(field.right - talk.right, lessThan(16), reason: 'at the right');
|
||||
expect(speak.right, lessThan(talk.left), reason: 'the microphone before');
|
||||
expect(field.right - speak.right, lessThan(16), reason: 'at the right');
|
||||
|
||||
await openPicker(tester);
|
||||
await tester.tap(find.text('Claude Opus, thinking longer').last);
|
||||
|
|
@ -379,7 +373,6 @@ void main() {
|
|||
expect(find.text('Ask Claude Opus, thinking longer…'), findsOneWidget);
|
||||
expect(at('Conversations'), history);
|
||||
expect(at('Speak your question'), speak);
|
||||
expect(at('Voice mode'), talk);
|
||||
|
||||
for (final icon in [
|
||||
LucideIcons.plus,
|
||||
|
|
@ -1559,129 +1552,4 @@ void main() {
|
|||
expect(find.text('Was not shown an image'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group("voice mode, as Claude's (D68)", () {
|
||||
/// Opens voice mode from the field.
|
||||
Future<void> talk(WidgetTester tester) async {
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(AssistantField),
|
||||
matching: find.byTooltip('Voice mode'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Says [words] and stops, as the recogniser does after a pause, and
|
||||
/// lets the answer come.
|
||||
Future<void> say(WidgetTester tester, String words) async {
|
||||
speech.say(words);
|
||||
await tester.pump();
|
||||
speech.finish();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('the field opens it over the whole screen, no bar beneath; '
|
||||
'it listens, asks what was said, reads the answer aloud without its '
|
||||
'marks, and listens again; ending it keeps the conversation', (
|
||||
tester,
|
||||
) async {
|
||||
await showApp(tester);
|
||||
await talk(tester);
|
||||
|
||||
expect(find.text("Let's talk"), findsOneWidget);
|
||||
expect(find.text('Home'), findsNothing, reason: 'no navigation bar');
|
||||
expect(speech.listening, isTrue);
|
||||
expect(speech.languages.single, 'en-US');
|
||||
|
||||
assistants.answers.add(_says('Because it **joins** clauses.'));
|
||||
speech.say('Why て-form?');
|
||||
await tester.pump();
|
||||
expect(find.text('Why て-form?'), findsOneWidget, reason: 'as heard');
|
||||
speech.finish();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(_asked(assistants.asked.single.$2), startsWith('Why て-form?'));
|
||||
expect(voice.spoken.single, ('Because it joins clauses.', 'en-US'));
|
||||
expect(
|
||||
find.text('Because it **joins** clauses.', findRichText: true),
|
||||
findsNothing,
|
||||
);
|
||||
expect(speech.listening, isTrue, reason: 'listening again');
|
||||
|
||||
await tester.tap(find.byTooltip('End voice mode'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text("Let's talk"), findsNothing);
|
||||
expect(speech.listening, isFalse);
|
||||
expect(kept.written, isNotEmpty, reason: 'kept, as any conversation');
|
||||
});
|
||||
|
||||
testWidgets('a second question goes on the same conversation', (
|
||||
tester,
|
||||
) async {
|
||||
await showApp(tester);
|
||||
await talk(tester);
|
||||
assistants.answers
|
||||
..add(_says('It joins.'))
|
||||
..add(_says('As in 食べて.'));
|
||||
|
||||
await say(tester, 'Why て-form?');
|
||||
await say(tester, 'An example?');
|
||||
|
||||
final second = assistants.asked.last.$2;
|
||||
expect(_asked(second), startsWith('An example?'));
|
||||
expect(second.messages, hasLength(3), reason: 'the first turn before');
|
||||
});
|
||||
|
||||
testWidgets('with the speaker off, answers are shown, not read, and it '
|
||||
'listens again at once', (tester) async {
|
||||
await showApp(tester);
|
||||
await talk(tester);
|
||||
await tester.tap(find.byTooltip("Don't read answers aloud"));
|
||||
await tester.pumpAndSettle();
|
||||
assistants.answers.add(_says('Because it joins clauses.'));
|
||||
|
||||
await say(tester, 'Why て-form?');
|
||||
|
||||
expect(voice.spoken, isEmpty);
|
||||
expect(
|
||||
find.text('Because it joins clauses.', findRichText: true),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(speech.listening, isTrue);
|
||||
expect(find.byTooltip('Read answers aloud'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the microphone stops the reading and listens at once; '
|
||||
'nothing heard, it waits for the microphone', (tester) async {
|
||||
voice.holds = true;
|
||||
await showApp(tester);
|
||||
await talk(tester);
|
||||
assistants.answers.add(_says('A long answer.'));
|
||||
await say(tester, 'Why?');
|
||||
expect(voice.speaking, isTrue);
|
||||
expect(speech.listening, isFalse);
|
||||
|
||||
await tester.tap(find.byTooltip('Talk'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(voice.speaking, isFalse);
|
||||
expect(speech.listening, isTrue);
|
||||
|
||||
speech.finish();
|
||||
await tester.pumpAndSettle();
|
||||
expect(speech.listening, isFalse, reason: 'nothing heard');
|
||||
expect(find.text('Tap the microphone to talk'), findsOneWidget);
|
||||
expect(assistants.asked, hasLength(1));
|
||||
});
|
||||
|
||||
testWidgets('only for an assistant, and only where the device listens', (
|
||||
tester,
|
||||
) async {
|
||||
speech.supported = false;
|
||||
await showApp(tester);
|
||||
expect(find.byTooltip('Voice mode'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/speech_output.dart';
|
||||
|
||||
void main() {
|
||||
test('an answer is read without its marks, a link as its words, a '
|
||||
"list's items as lines (ADR 0021, D68)", () {
|
||||
expect(
|
||||
speakableText(
|
||||
'## Why て-form?\n\n'
|
||||
'It **joins** clauses, as `食べて` does.\n\n'
|
||||
'- First, *this*.\n'
|
||||
'1. Then [that one](https://example.org).\n'
|
||||
'> A quote.',
|
||||
),
|
||||
'Why て-form?\n\n'
|
||||
'It joins clauses, as 食べて does.\n\n'
|
||||
'First, this.\n'
|
||||
'Then that one.\n'
|
||||
'A quote.',
|
||||
);
|
||||
});
|
||||
|
||||
test('a table reads as its cells, a code block as its code', () {
|
||||
expect(
|
||||
speakableText('| a | b |\n|---|---|\n| 1 | 2 |'),
|
||||
contains('1, 2'),
|
||||
);
|
||||
expect(speakableText('```dart\nprint(1);\n```'), 'print(1);');
|
||||
});
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/speech_output.dart';
|
||||
|
||||
/// A [SpeechOutput] that notes what it reads; while [holds], a reading
|
||||
/// lasts until [finish].
|
||||
class FakeSpeechOutput() implements SpeechOutput {
|
||||
@override
|
||||
bool supported = true;
|
||||
|
||||
/// Whether a reading lasts until [finish] rather than ending at once.
|
||||
bool holds = false;
|
||||
|
||||
/// What was read aloud, in order, with the language asked for.
|
||||
final spoken = <(String, String?)>[];
|
||||
|
||||
Completer<void>? _reading;
|
||||
|
||||
/// Whether a reading is under way.
|
||||
bool get speaking => _reading != null;
|
||||
|
||||
/// Ends the reading under way.
|
||||
void finish() {
|
||||
final reading = _reading;
|
||||
_reading = null;
|
||||
reading?.complete();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> speak(String text, {String? language}) async {
|
||||
spoken.add((text, language));
|
||||
if (holds) {
|
||||
final reading = _reading = Completer<void>();
|
||||
await reading.future;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async => finish();
|
||||
}
|
||||
|
|
@ -25,7 +25,6 @@ import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_files
|
|||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/assistant_lookups.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/conversation_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/photo_picker.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/speech_output.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/assistant/data/web_search.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_gateway.dart';
|
||||
|
|
@ -103,7 +102,6 @@ import 'fake_publish_gateway.dart';
|
|||
import 'fake_review_gateway.dart';
|
||||
import 'fake_settings_gateway.dart';
|
||||
import 'fake_sound_player.dart';
|
||||
import 'fake_speech_output.dart';
|
||||
import 'fake_studyset_gateway.dart';
|
||||
import 'fake_tag_gateway.dart';
|
||||
import 'fake_youtube_videos.dart';
|
||||
|
|
@ -177,7 +175,6 @@ Dependencies testDependencies({
|
|||
WebSearch? webSearch,
|
||||
SpeechInput? speech,
|
||||
PhotoPicker? photos,
|
||||
SpeechOutput? speechOutput,
|
||||
JellyfinGateway? jellyfinGateway,
|
||||
JellyfinStore? jellyfinStore,
|
||||
JellyfinSuggestion? jellyfinSuggestion,
|
||||
|
|
@ -271,7 +268,6 @@ Dependencies testDependencies({
|
|||
),
|
||||
speech: speech ?? FakeSpeechInput(),
|
||||
photos: photos ?? FakePhotoPicker(),
|
||||
speechOutput: speechOutput ?? FakeSpeechOutput(),
|
||||
jellyfin: JellyfinRepository(
|
||||
(device) => jellyfinGateway ?? FakeJellyfinGateway(),
|
||||
jellyfinStore ?? MemoryJellyfinStore(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue