feat(learn,todo,meditate): dictate through the phone's voice input app (ADR 0038, D18)
On Android the microphone now opens the app handling RECOGNIZE_SPEECH (FUTO Voice Input, Google's), the default the person chose in Android, and puts the text it returns where the cursor stood, as Dictation does; cancelled, nothing. Only where none is installed, or it can't be opened, does speech_to_text listen in line as before. halkyon_app: VoiceInputApp (one listen, an answer once closed) on the channel app.halkyon/voice_input, which DeviceSpeechInput asks first at every listen, answering SpeechStart.finished so that Dictation doesn't listen again; speech_to_text isn't touched on that route, so our app asks for no microphone. SpeechInput.listen and Dictation.start take the prompt the voice input app shows (the field's hint). Each app: VoiceInput.kt (queryIntentActivities, startActivityForResult, the first of EXTRA_RESULTS) attached in MainActivity, which hands it activity results, and RECOGNIZE_SPEECH among the manifest's queries. The privacy pages of Learn and Meditate say so. Our own Whisper and Meditate's dream recordings are dropped (D18). Whats-New: The microphone now dictates through your phone's voice input app, such as FUTO Voice Input. Whats-New-De: Das Mikrofon diktiert jetzt über die Spracheingabe-App deines Telefons, etwa FUTO Voice Input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4940230ef1
commit
c36bd83c6a
8 changed files with 827 additions and 27 deletions
|
|
@ -4,7 +4,7 @@ What holds for every app and package of this repository is in the [root AGENTS.m
|
|||
|
||||
- **Packages**: `halkyon_meditate_server`, `halkyon_meditate_client` (generated, never edited), `halkyon_meditate_flutter`; app ID `app.halkyon.meditate` on every platform; no web build. Licences as Learn's (server SSPL, client and app AGPL; `LICENSE.md`). Copied from Todo's tree on 3 Nov 2026 and stripped to the shell, so its frame, keys and sealed rows are Todo's and Train's.
|
||||
- **The server** includes `halkyon_base` (ADR 0014) and configures it in `server.dart` (`meditateBase()`, `lib/src/meditate_base.dart`): its basics say it serves `meditate` (never change it once apps use it), sign-in mails name Halkyon Meditate, the source is this repository, `minimumAppBuild` 1, a person's space deleted with them (`deletePersonData: [deleteMeditateData]`, `lib/src/meditate_data.dart`). **Accounts** (ADR 0016): Meditate holds its own register when it runs alone, and is a member of the instance's register where the operator sets `ACCOUNT_REGISTER_URL` (`AccountsMode.fromEnvironment()`) with the instance's shared `INSTANCE_SECRET`; `INSTANCE_MEMBER_MEDITATE` names it to the register. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled once each. Synced tables (ADR 0038, D7; `database: sync`, every row sealed): `content_key`, `meditate_settings` (one row a person, under an ID keyed by the account), `sitting`, `dream` (D14) and `lesson_done` (D16), each indexing `spaceId`. Migrations: `initial` (Serverpod's, the auth modules', the module's and the sync engine's tables), `sittings` (the three tables), `dreams` (`dream`) and `library` (`lesson_done`). Tests: `test/integration/meditate_server_test.dart` (the module's endpoints through the server's own dispatch, `halkyon_base.<endpoint>`, and the sync engine's connector refusing calls without a session), `test/integration/sitting/person_deletion_test.dart`, the library's in `test/unit/library/` (the manifest, the files over HTTP) and `test/integration/library/` (the endpoint).
|
||||
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Meditate's frame (`meditateFrame`) and `ServerpodInstanceChecker(app: 'meditate')`; `MeditateDependencies` per instance, `MeditatePerson` per person (their sync, `SettingsRepository`, `SittingRepository`, `DreamRepository`, `LibraryRepository`, `LessonRepository`, and the device's `BellPlayer`, `GuidePlayer` and `ScreenAwake`); SimpleLogin's way back on `app.halkyon.meditate://` and the loopback port 43826. The person's app (`MeditatePersonApp`) washes in fern (`ScreenColor`, D3) and holds `PracticeCubit` (settings and sittings, read again on every change), `JournalCubit` (the dreams) and `LibraryCubit` (the instance's library, lessons done, recordings kept) for every screen. go_router (`buildMeditateRouter`, `MeditatePaths`): halkyon_ui's `NavigationFrame` (`MeditateShell`) with Today, Explore, Journal and Profile (D2, D14; a bar below medium windows, a rail with the icon from medium on), a practice's page in Explore's branch (`PracticeScreen`) with a library item's and course's (`/explore/library/:id`, `?course=` for a lesson, `/explore/courses/:id`), and over the tabs the sitting (`SittingScreen`, handed its `SittingPlan` as `extra`), a recording playing (`ListeningScreen`, its `ListeningPlan` as `extra`), the library's credits (`/library-credits`), a dream (`DreamScreen`, `/dream` new, `/dream/:id` kept) and Settings. **Today** (`TodayTab`, D9): greeting by the hour, from 4 to 12 o'clock a card to write down last night's dream while it has none (`dreamPromptDue`, D14), Sit now (5, 10, 20 and the timer's own length), the practice of the day (`practiceOfDay`), the week and the days in a row (`WeekCard`), favourites, recently sat. **Explore** (`ExploreTab`, D10): search folded without case or accents (`matchesSearch`), four goal tiles, kind chips, practice cards. **The Journal** (`JournalTab`, D14; `lib/src/feature/journal/`): dreams by month, the newest first (`dreamsByMonth`), searched over title, text and labels and filtered by marks (`findDreams`), *+* to write one; a dream's night is the date of the morning after it (`defaultNight`: today's date at any hour); its screen writes with the text first and focused, the rest folded, and reads a kept one with Edit and Delete; a microphone under the text dictates where the device recognises speech (D15: halkyon_app's `Dictation` on `MeditatePerson.speech`, `DeviceSpeechInput` preferring Android's on-device recogniser, listening again after each pause until stopped or three silent listens; none on Linux, `NoSpeechInput` in tests that don't say), and Today's card offers *Speak it* (`/dream?speak=1`). The platforms declare the microphone as Learn's do (Android's `RECORD_AUDIO` and recognition query, iOS's and macOS's usage texts, macOS's `audio-input`), and the privacy page's points are `meditate_base.dart`'s. **Profile** (D2, D8): halkyon_app's `HalkyonProfile` with a *Practice* tab (minutes, sittings, days, the week, the journal's dreams this month and lucid ones once it holds one, every sitting with Delete). **The practices** are `practices` in `lib/src/feature/practice/model/practice.dart` (IDs never change once released), their words in the ARBs by ID (`practiceTexts`), their cues spread evenly over the length (`Practice.cueAt`). **A sitting** (`SittingCubit`, D6): its time from `clock.now()`, the ticks only showing it; the bell at the start (where on), between every interval and at the end; the screen kept on while it runs (where on); kept whole at its end, and ended early kept from a minute on where the person says so. **The figures** (`practiceFigures`, D8) on the device: a sitting's day by the wall clock it was sat by (`Sitting.offsetMinutes`), the streak up to today or yesterday, the week from the language's first day. **Settings** (D11): Sync, the operator's card, the device's appearance and language (halkyon_app's `DeviceLookCard` on `LookCubit`, kept in `<app support>/look.json`, `meditateLanguages`; ADR 0014's amendment of 4 Nov 2026), *Sitting* (bell, starting bell, bells between, screen on, the timer's length), *Practice* (the streak shown), *Journal* (Today's morning card, `dreamPrompt`), the account, What's new, About, the version. Texts in British English (the template) and German (du), `en_US` and `en_GB` empty. Widget tests start the whole app with halkyon_app's fakes (`test/app/meditate_app_test.dart`), the bells and screen recorded (`test/support/recording_device.dart`); a sitting on screen ticks every second, so tests pump seconds (`sit`) rather than `pumpAndSettle`; a test needing the hour (Today's morning card) starts the app inside `withClock` (`startAt`).
|
||||
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Meditate's frame (`meditateFrame`) and `ServerpodInstanceChecker(app: 'meditate')`; `MeditateDependencies` per instance, `MeditatePerson` per person (their sync, `SettingsRepository`, `SittingRepository`, `DreamRepository`, `LibraryRepository`, `LessonRepository`, and the device's `BellPlayer`, `GuidePlayer` and `ScreenAwake`); SimpleLogin's way back on `app.halkyon.meditate://` and the loopback port 43826. The person's app (`MeditatePersonApp`) washes in fern (`ScreenColor`, D3) and holds `PracticeCubit` (settings and sittings, read again on every change), `JournalCubit` (the dreams) and `LibraryCubit` (the instance's library, lessons done, recordings kept) for every screen. go_router (`buildMeditateRouter`, `MeditatePaths`): halkyon_ui's `NavigationFrame` (`MeditateShell`) with Today, Explore, Journal and Profile (D2, D14; a bar below medium windows, a rail with the icon from medium on), a practice's page in Explore's branch (`PracticeScreen`) with a library item's and course's (`/explore/library/:id`, `?course=` for a lesson, `/explore/courses/:id`), and over the tabs the sitting (`SittingScreen`, handed its `SittingPlan` as `extra`), a recording playing (`ListeningScreen`, its `ListeningPlan` as `extra`), the library's credits (`/library-credits`), a dream (`DreamScreen`, `/dream` new, `/dream/:id` kept) and Settings. **Today** (`TodayTab`, D9): greeting by the hour, from 4 to 12 o'clock a card to write down last night's dream while it has none (`dreamPromptDue`, D14), Sit now (5, 10, 20 and the timer's own length), the practice of the day (`practiceOfDay`), the week and the days in a row (`WeekCard`), favourites, recently sat. **Explore** (`ExploreTab`, D10): search folded without case or accents (`matchesSearch`), four goal tiles, kind chips, practice cards. **The Journal** (`JournalTab`, D14; `lib/src/feature/journal/`): dreams by month, the newest first (`dreamsByMonth`), searched over title, text and labels and filtered by marks (`findDreams`), *+* to write one; a dream's night is the date of the morning after it (`defaultNight`: today's date at any hour); its screen writes with the text first and focused, the rest folded, and reads a kept one with Edit and Delete; a microphone under the text dictates where the device recognises speech (D15: halkyon_app's `Dictation` on `MeditatePerson.speech`, `DeviceSpeechInput`: on Android the voice input app first where one is installed, D18, opened once a tap and its text put where the cursor stood, else Android's recogniser, on the device where it can, listening again after each pause until stopped or three silent listens; none on Linux, `NoSpeechInput` in tests that don't say), and Today's card offers *Speak it* (`/dream?speak=1`). The platforms declare the microphone as Learn's do (Android's `RECORD_AUDIO`, the recognition service's and `RECOGNIZE_SPEECH`'s queries, `VoiceInput.kt` answering `app.halkyon/voice_input` from `MainActivity`, iOS's and macOS's usage texts, macOS's `audio-input`), and the privacy page's points are `meditate_base.dart`'s. **Profile** (D2, D8): halkyon_app's `HalkyonProfile` with a *Practice* tab (minutes, sittings, days, the week, the journal's dreams this month and lucid ones once it holds one, every sitting with Delete). **The practices** are `practices` in `lib/src/feature/practice/model/practice.dart` (IDs never change once released), their words in the ARBs by ID (`practiceTexts`), their cues spread evenly over the length (`Practice.cueAt`). **A sitting** (`SittingCubit`, D6): its time from `clock.now()`, the ticks only showing it; the bell at the start (where on), between every interval and at the end; the screen kept on while it runs (where on); kept whole at its end, and ended early kept from a minute on where the person says so. **The figures** (`practiceFigures`, D8) on the device: a sitting's day by the wall clock it was sat by (`Sitting.offsetMinutes`), the streak up to today or yesterday, the week from the language's first day. **Settings** (D11): Sync, the operator's card, the device's appearance and language (halkyon_app's `DeviceLookCard` on `LookCubit`, kept in `<app support>/look.json`, `meditateLanguages`; ADR 0014's amendment of 4 Nov 2026), *Sitting* (bell, starting bell, bells between, screen on, the timer's length), *Practice* (the streak shown), *Journal* (Today's morning card, `dreamPrompt`), the account, What's new, About, the version. Texts in British English (the template) and German (du), `en_US` and `en_GB` empty. Widget tests start the whole app with halkyon_app's fakes (`test/app/meditate_app_test.dart`), the bells and screen recorded (`test/support/recording_device.dart`); a sitting on screen ticks every second, so tests pump seconds (`sit`) rather than `pumpAndSettle`; a test needing the hour (Today's morning card) starts the app inside `withClock` (`startAt`).
|
||||
- **The operator's library** (D16; `lib/src/library/` on the server, `lib/src/feature/library/` in the app): a folder named by `MEDITATE_LIBRARY_DIR` with recordings, pictures and `library.yaml` (commented sample: the server's `library.example.yaml`), read by `readLibrary` (every check and refusal, with the reason), held in memory by `MeditateLibrary` and reread every ten minutes (`LibraryReloadFutureCall`, `startLibrary`); checked before deploying with `dart run bin/check_library.dart <folder>` in the server package. `LibraryEndpoint.catalogue` for signed-in people alone, with an access (`LibraryFileAccess`: the hour begun plus 12 hours, signed with a key made at start); the web server's `/library/<served name>?access=…` serves the files by ranges (`serveLibraryFile`), never by a path. In the app: Explore's section (none without a library), the item's page (recordings, Play, favourite as `library/<id>` in the settings, keep on the device, a sound under a silent sitting, its credit), the course's (lessons ticked), the player (a finished item is a sitting `library/<id>` and its lesson done), Settings' *Library credits*; the last catalogue in the person's folder (`library.json`), recordings kept in `library/` beside it. Tests: `test/feature/library/library_test.dart` and the app test's group, on `test/support/fake_library.dart` (made-up entries only: nothing of 7Mind's, whose structure alone D16 follows).
|
||||
- **The bells** (D5) are `assets/bells/*.wav`, synthesised by `apps/meditate/tool/bells.py` (Python's standard library, run from `apps/meditate`; the same bytes every run) and played with `audioplayers` (`AudioplayersBellPlayer`). Only while the app runs: bells with the screen off are for later.
|
||||
- **The icon** is Lucide's `sprout` from cobalt through pacific into fern (ADR 0031, D5, D7), drawn by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py meditate` from the root): `assets/icon/halkyon_meditate_icon.png`, Lucide's licence beside it, every platform's launcher icon.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<!-- A dream spoken into the journal (ADR 0038, D15): the microphone, asked
|
||||
for once the person taps it, and the device's speech recogniser, which
|
||||
Android 11 and later only shows the app when it is declared below. -->
|
||||
Android 11 and later only shows the app when it is declared below.
|
||||
Through a voice input app (D18) the microphone is that app's, and this
|
||||
one asks for none. -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<!-- No backup and no transfer to another phone (ADR 0038, D7, as
|
||||
Train's ADR 0030, D53): what is sealed comes back from the
|
||||
|
|
@ -73,5 +75,10 @@
|
|||
<intent>
|
||||
<action android:name="android.speech.RecognitionService"/>
|
||||
</intent>
|
||||
<!-- The voice input app dictation opens first (Meditate's ADR 0038, D18),
|
||||
FUTO Voice Input or Google's, which records itself. -->
|
||||
<intent>
|
||||
<action android:name="android.speech.action.RECOGNIZE_SPEECH"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
package app.halkyon.meditate
|
||||
|
||||
import android.content.Intent
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
/** The voice input app dictation opens (ADR 0038, D18). */
|
||||
private val voiceInput = VoiceInput(this)
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
// Secrets halkyon_app shows kept out of screenshots and recents.
|
||||
ScreenGuard.attach(this, flutterEngine.dartExecutor.binaryMessenger)
|
||||
SensitiveClipboard.attach(this, flutterEngine.dartExecutor.binaryMessenger)
|
||||
voiceInput.attach(flutterEngine.dartExecutor.binaryMessenger)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (!voiceInput.onActivityResult(requestCode, resultCode, data)) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
package app.halkyon.meditate
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.speech.RecognizerIntent
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
/**
|
||||
* The device's voice input app for halkyon_app's ChannelVoiceInputApp
|
||||
* (Halkyon Meditate's ADR 0038, D18): whichever app handles
|
||||
* ACTION_RECOGNIZE_SPEECH (FUTO Voice Input, Google's) opens over the app,
|
||||
* records and returns text alone, so this app asks for no microphone.
|
||||
* `installed` says whether one is (the manifest's <queries> lets Android 11
|
||||
* on show it); `listen` opens it and answers once it is closed: its first
|
||||
* result, empty where cancelled, an error where it couldn't be opened. The
|
||||
* activity hands its results to [onActivityResult].
|
||||
*/
|
||||
class VoiceInput(private val activity: Activity) {
|
||||
private var pending: MethodChannel.Result? = null
|
||||
|
||||
fun attach(messenger: BinaryMessenger) {
|
||||
MethodChannel(messenger, "app.halkyon/voice_input").setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"installed" -> result.success(installed())
|
||||
"listen" -> {
|
||||
if (pending != null) {
|
||||
result.error("busy", "The voice input app is open already", null)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
|
||||
.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
||||
call.argument<String>("language")?.let { intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, it) }
|
||||
call.argument<String>("prompt")?.let { intent.putExtra(RecognizerIntent.EXTRA_PROMPT, it) }
|
||||
try {
|
||||
pending = result
|
||||
activity.startActivityForResult(intent, REQUEST)
|
||||
} catch (error: ActivityNotFoundException) {
|
||||
pending = null
|
||||
result.error("noApp", error.message, null)
|
||||
} catch (error: SecurityException) {
|
||||
pending = null
|
||||
result.error("refused", error.message, null)
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun installed(): Boolean {
|
||||
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
|
||||
val packages = activity.packageManager
|
||||
val found = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packages.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
packages.queryIntentActivities(intent, 0)
|
||||
}
|
||||
return found.isNotEmpty()
|
||||
}
|
||||
|
||||
/** Whether [requestCode] was the voice input app's, its text handed on. */
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode != REQUEST) return false
|
||||
val result = pending ?: return true
|
||||
pending = null
|
||||
val text = if (resultCode == Activity.RESULT_OK) {
|
||||
data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
result.success(text ?: "")
|
||||
return true
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** Any number the app's other activity results don't use. */
|
||||
const val REQUEST = 0x766f
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ class const NoSpeechInput() implements SpeechInput {
|
|||
required String? language,
|
||||
required ValueChanged<String> onWords,
|
||||
required VoidCallback onDone,
|
||||
String? prompt,
|
||||
}) async => SpeechStart.unavailable;
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -95,8 +95,11 @@ class _DreamScreenState() extends State<DreamScreen> {
|
|||
final messenger = ScaffoldMessenger.of(context);
|
||||
final problem = switch (await _dictation.start(
|
||||
language: Localizations.localeOf(context).toLanguageTag(),
|
||||
// Shown by the voice input app, where it shows a prompt (D18).
|
||||
prompt: l10n.dreamTextHint,
|
||||
)) {
|
||||
SpeechStart.listening => null,
|
||||
// Listening in line, or the voice input app has listened.
|
||||
SpeechStart.listening || SpeechStart.finished => null,
|
||||
// A phone without Google's services has no recogniser to listen with.
|
||||
SpeechStart.noRecognizer => words.speechNoRecognizer,
|
||||
SpeechStart.unavailable => l10n.dreamSpeechUnavailable,
|
||||
|
|
|
|||
|
|
@ -1083,6 +1083,43 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets('with a voice input app installed, the microphone opens it, '
|
||||
'asking what was dreamt, and what it returns goes where the cursor '
|
||||
'stands, once; cancelled, nothing (D18)', (tester) async {
|
||||
speech.app = true;
|
||||
await newDream(tester);
|
||||
await tester.enterText(text, 'I was there.');
|
||||
tester.widget<TextField>(text).controller!.selection =
|
||||
const TextSelection.collapsed(offset: 5);
|
||||
|
||||
await tester.tap(find.byTooltip('Dictate'));
|
||||
await tester.pump();
|
||||
expect(speech.appOpen, isTrue);
|
||||
expect(speech.listening, isFalse, reason: 'nothing else listens');
|
||||
expect(speech.prompts, ['What did you dream?']);
|
||||
expect(find.byTooltip('Stop dictating'), findsOneWidget);
|
||||
|
||||
speech.answer('at the sea');
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<TextField>(text).controller!.text,
|
||||
'I was at the sea there.',
|
||||
);
|
||||
expect(speech.languages, hasLength(1), reason: 'not opened again');
|
||||
expect(find.byTooltip('Dictate'), findsOneWidget);
|
||||
expect(find.byType(SnackBar), findsNothing);
|
||||
|
||||
await tester.tap(find.byTooltip('Dictate'));
|
||||
await tester.pump();
|
||||
speech.cancel();
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.widget<TextField>(text).controller!.text,
|
||||
'I was at the sea there.',
|
||||
);
|
||||
expect(find.byTooltip('Dictate'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("the recogniser's pause doesn't end it: it listens on, the "
|
||||
'next words after the last, until stopped', (tester) async {
|
||||
await newDream(tester);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,10 @@ const List<PrivacyPagePoint> _privacyPagePoints = [
|
|||
(
|
||||
english:
|
||||
'A dream you speak is written by your device’s own speech '
|
||||
'recognition. On Android, Meditate asks for recognition on the '
|
||||
'recognition. On Android, where a voice input app is installed '
|
||||
'(FUTO Voice Input, for one), that app listens and hands Meditate '
|
||||
'the text alone, and it decides whether the sound leaves the device; '
|
||||
'without one, Meditate asks for recognition on the '
|
||||
'device, which Android 12 and later offer where the language is '
|
||||
'installed; otherwise Android’s recogniser may send the sound to '
|
||||
'Google or the phone’s maker. Apple’s and Windows’ recognisers decide '
|
||||
|
|
@ -84,7 +87,10 @@ const List<PrivacyPagePoint> _privacyPagePoints = [
|
|||
'the instance.',
|
||||
german:
|
||||
'Einen Traum, den du erzählst, schreibt die Spracherkennung deines '
|
||||
'Geräts auf. Unter Android bittet Meditate um Erkennung auf dem '
|
||||
'Geräts auf. Unter Android hört, wo eine Spracheingabe-App '
|
||||
'installiert ist (etwa FUTO Voice Input), diese App zu und gibt '
|
||||
'Meditate nur den Text; ob der Ton das Gerät verlässt, entscheidet '
|
||||
'sie. Ohne eine bittet Meditate um Erkennung auf dem '
|
||||
'Gerät, die Android ab Version 12 bietet, wo die Sprache installiert '
|
||||
'ist; sonst kann Androids Spracherkennung den Ton an Google oder den '
|
||||
'Hersteller des Telefons senden. Die Spracherkennung von Apple und '
|
||||
|
|
|
|||
Loading…
Reference in a new issue