feat(tts): Phase 4 — Read Page Aloud with OS-native voices
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled

Platform-dispatched TTS (spd-say + espeak-ng fallback on Linux, say on
macOS, PowerShell System.Speech on Windows) with a shared SpeakerButton
component. Tap to speak, tap again to stop; only one button speaks at
a time so two surfaces don't talk over each other. Text always travels
via argv (or a PowerShell here-string delivered through -EncodedCommand
on Windows) so user content never enters a shell string.

Mount points: DictationPage transcript footer, transcript viewer header,
per-step in MicroSteps. Settings gains a "Read aloud" accordion with
voice picker (lazy-loaded from the OS synth), rate slider 0.5-2.0x,
and a British-English test utterance.

Rust tests cover rate mapping, NaN handling, and Windows here-string
terminator safety. No pause/resume, no SSML, no cloud voices — that
stays out of scope per the Layer-1 roadmap.
This commit is contained in:
2026-04-24 16:01:47 +01:00
parent b344e8a580
commit 9f53702c7e
12 changed files with 699 additions and 7 deletions

View File

@@ -2,6 +2,7 @@
import { invoke } from '@tauri-apps/api/core';
import { ListTree, Check, Timer, Loader2, ThumbsUp, ThumbsDown, Pencil } from 'lucide-svelte';
import { profilesStore } from '$lib/stores/profiles.svelte.ts';
import SpeakerButton from '$lib/components/SpeakerButton.svelte';
let { parentTaskId, parentTaskText = '', reduceMotion = false } = $props();
@@ -270,6 +271,12 @@
>
<Pencil size={10} aria-hidden="true" />
</button>
<span
class="opacity-0 group-hover:opacity-100"
style={reduceMotion ? '' : 'transition: opacity var(--duration-ui)'}
>
<SpeakerButton text={step.text} label="Read this step aloud" size={10} />
</span>
<button
class="opacity-0 group-hover:opacity-100 flex items-center gap-1 text-[10px] text-text-tertiary hover:text-accent"
onclick={() => startTimer(step.id)}