feat(learn): voice mode, a conversation spoken as Claude's "Let's talk" (ADR 0021, D68)
Beside the field's microphone, a button opens voice mode over the whole screen: a glow rising from below, a big microphone, and +, who answers, the speaker and End in one row. It listens with the device's recogniser, asks what was heard in one kept conversation, reads the answer aloud with the device's speech (flutter_tts, its Markdown left out) and listens again. On Linux there is none, as there is no microphone; where nothing reads aloud, answers are shown as text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7be31dcbfc
commit
c984678c5b
2 changed files with 1180 additions and 6 deletions
|
|
@ -0,0 +1,40 @@
|
|||
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,6 +25,7 @@ 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';
|
||||
|
|
@ -102,6 +103,7 @@ 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';
|
||||
|
|
@ -175,6 +177,7 @@ Dependencies testDependencies({
|
|||
WebSearch? webSearch,
|
||||
SpeechInput? speech,
|
||||
PhotoPicker? photos,
|
||||
SpeechOutput? speechOutput,
|
||||
JellyfinGateway? jellyfinGateway,
|
||||
JellyfinStore? jellyfinStore,
|
||||
JellyfinSuggestion? jellyfinSuggestion,
|
||||
|
|
@ -268,6 +271,7 @@ Dependencies testDependencies({
|
|||
),
|
||||
speech: speech ?? FakeSpeechInput(),
|
||||
photos: photos ?? FakePhotoPicker(),
|
||||
speechOutput: speechOutput ?? FakeSpeechOutput(),
|
||||
jellyfin: JellyfinRepository(
|
||||
(device) => jellyfinGateway ?? FakeJellyfinGateway(),
|
||||
jellyfinStore ?? MemoryJellyfinStore(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue