halkyon/packages/halkyon_app/lib/src/testing/fake_speech_input.dart
Tizian 「ティツィアーン」 603218ef96
refactor(app): the device's speech recognition moves from Learn into halkyon_app, with dictation into a text
Learn's `SpeechInput` and `DeviceSpeechInput` (ADR 0021, D67) are
halkyon_app's now, and its `FakeSpeechInput` is in `testing.dart`, so
Meditate's dream journal (ADR 0038, D15) listens through the same code.
Learn imports them from there and behaves as before: its composer and
its tests are unchanged but for the imports, and `DeviceSpeechInput()`
asks for what it asked before.

`DeviceSpeechInput` takes three options, off by default:
- `preferOnDevice`: on Android, recognition on the device where it can
  (Android 12 on), else offline; should that fail before a word is
  heard (not a pause), it listens as Android otherwise does from then
  on.
- `pauseFor`: how long a pause may last before the recogniser stops.
- `listenFor`: how long one listen may last at most.

`Dictation` writes what is heard where the cursor stands
(`DictationInsertion`: in place of a selection, set apart by a space),
shows the words so far, and listens again after each pause while it is
on, stopping by itself after three listens running heard nothing.

speech_to_text moves from Learn's pubspec to halkyon_app's, the lock
unchanged. Todo, Train and Meditate link the plugin now; their
generated plugin registrants (macOS, Windows) say so. An app declares
the microphone itself, and Todo and Train declare none, so nothing
there can listen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:58 +01:00

51 lines
1.2 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:halkyon_app/src/speech/speech_input.dart';
/// A [SpeechInput] the test speaks into: [say] hands words on, [finish]
/// ends the listen.
class FakeSpeechInput() implements SpeechInput {
@override
bool supported = true;
/// Whether the device can listen when asked: false as if the microphone
/// were refused.
bool listens = true;
/// The languages asked to listen in, in order.
final languages = <String?>[];
ValueChanged<String>? _onWords;
VoidCallback? _onDone;
/// Whether a listen is under way.
bool get listening => _onWords != null;
/// Hands on [words], as all heard so far.
void say(String words) => _onWords?.call(words);
/// Ends the listen, as the recogniser does after a pause.
void finish() {
final done = _onDone;
_onWords = null;
_onDone = null;
done?.call();
}
@override
Future<bool> listen({
required String? language,
required ValueChanged<String> onWords,
required VoidCallback onDone,
}) async {
languages.add(language);
if (!listens) {
return false;
}
_onWords = onWords;
_onDone = onDone;
return true;
}
@override
Future<void> stop() async => finish();
}