# Voices, accents, and TTS

The read-aloud feature lets a learner tap a speaker icon on any question to have the stem + each option spoken aloud, with karaoke-style highlight on the line currently being read.

## The architecture

```
Flutter button tap
    │
    ▼
PersonaRotation.pickRandom()   ←── loaded from /api/voices at app startup
    │
    ▼
ReadAloudService.speakSequence(text[], profile)
    │
    ▼ (only on web)
BrowserTts.speakWithProfile()  ←── dart:js_interop binding
    │
    ▼
window.agsTtsSpeakWithProfile(text[], profile)   ←── inline JS in mobile/web/index.html
    │
    ▼
SpeechSynthesisUtterance       ←── browser native TTS, uses installed OS voices
    │
    ├── onstart  → callback → Flutter highlights that line
    └── onend    → queue next segment
```

The `flutter_tts` package is **not** used. Its web implementation throws `MissingPluginException` on Flutter 3.38, so we bypass it entirely with a 60-line JS bridge and `dart:js_interop`.

## Voice selection rules

The inline JS scores every installed browser voice on two axes:

1. **Persona voice_hints** (per-persona, ranked) — first substring match wins. Each persona in `voices` table has a ranked list like `['jenny', 'aria', 'samantha', 'google us english', 'zira', 'female']`.
2. **Quality boost** — voices whose name contains `natural`, `neural`, `online`, `enhanced`, `premium`, or `studio` get +50 score.
3. **Locale boost** — `en-US` voices get +5.

Per-utterance the JS bridge also applies the persona's `pitch` (typically 0.85–1.25) and `rate` (typically 0.80–1.10), so personas feel distinct even when they're falling back to the same underlying voice.

## Personas (currently seeded, 14 across 6 accents)

| Accent | Personas |
|---|---|
| American (en-US) | **Asha** (girl, high pitch) · **Ben** (boy, fast) · **Coach Max** (man, low pitch) · **Ms. Vera** (teacher, measured) |
| British (en-GB) | **Lily** (girl) · **Oliver** (boy) · **Mrs. Holly** (teacher) · **Grandpa Reg** (elder, slow) |
| Australian (en-AU) | **Ruby** (girl) · **Jack** (boy) |
| Singaporean (en-SG) | **Mei** (girl) · **Aaron** (boy) |
| Indian (en-IN) | **Priya** (girl) · **Arjun** (boy) |

Each persona has its own stickman appearance:
- A unique alphabet **letter** on the placard
- A **placard color** matching the persona vibe
- A **mood** (idle / happy / thinking / encouraging / celebrating) that drives the mascot animation

## How the admin controls it

**System → Site Configuration → `default_accent_code`** = the school's accent (`en-US` / `en-GB` / `en-AU` / `en-SG` / `en-IN` / blank for all).

`/api/voices` returns only voices whose `accent_code` matches the configured accent. Flutter rotates through whatever's in the returned roster — so picking British gives the learner Lily/Oliver/Mrs. Holly/Grandpa Reg in rotation; picking Singaporean gives Mei/Aaron.

**Taxonomy → Reader Voices** is the underlying table. Admin can:
- Toggle `is_active` to remove a persona from rotation
- Adjust `pitch` / `rate` to tune a persona's character
- Edit `voice_hints` to prefer different installed voices
- Add new personas / new accents

## Voice quality on the host machine

Browser TTS uses **whatever the OS has installed**. On Windows the default install ships only Microsoft David (male) and Microsoft Zira (female) — both robotic SAPI 5 voices. With only those two installed, all 14 personas end up using one of them with pitch/rate variation.

To upgrade voice quality dramatically (free, ~5 min):

1. Open `ms-settings:speech` (Windows 11) — Settings → Time & language → Speech
2. **Manage voices → Add voices**
3. Install **English (United States) — Aria, Jenny, Tony** (Natural / Neural quality)
4. Also install British (Sonia, Libby, Ryan), Australian (Natasha, William), Indian (Neerja, Prabhat) if you intend to use those accents
5. Hard-refresh the Flutter web bundle — personas pick up the new voices automatically (their `voice_hints` lists already prefer Aria/Jenny/Sonia/etc.)

## For studio-grade voices (future work)

Browser TTS, even with Natural voices, can't match cloud AI voices. The path:

1. Pre-generate audio per question using **Microsoft Azure Neural** or **ElevenLabs** API
2. Store paths in `questions.audio_path` (or add a `question_audio` table for multiple persona variants per question)
3. Flutter plays the pre-generated MP3 via `audioplayers` (already a dependency for sound effects)
4. Karaoke highlight becomes time-synced via the audio metadata

Adds ~1 day of work + a recurring API cost (~$0.002 per minute generated, cached forever after).

## TTS bridge JS — where it lives

`mobile/web/index.html` contains an inline `<script>` defining 5 globals:

| Function | Purpose |
|---|---|
| `agsTtsSpeak(segments)` | Speak each segment in sequence, using auto-picked voice |
| `agsTtsSpeakWithProfile(segments, profile)` | Speak with a one-shot voice profile (voice_hints, rate, pitch) — used by persona rotation |
| `agsTtsStop()` | Cancel any in-progress speech |
| `agsTtsSetCallbacks(onSegmentStart, onAllFinished)` | Register Dart callbacks for karaoke highlight + completion |
| `agsTtsIsSupported()` | True if `window.speechSynthesis` exists |
| `agsTtsListVoices()` | Debug: return all available en-* voices |
| `agsTtsCurrentVoice()` | Debug: name of voice currently chosen |

The Dart side (`mobile/lib/services/read_aloud_browser_web.dart`) binds to these via `dart:js_interop` extension types. The non-web stub at `mobile/lib/services/read_aloud_browser_stub.dart` makes the build pass on non-web targets (TTS is no-op outside the browser for now).

## Diagnostic page

A standalone HTML diagnostic lives at `mobile/build/web/tts-test.html` (only present after `flutter build web`). Open it directly in the browser to see:
- Whether `window.speechSynthesis` exists
- The full list of installed voices + their `lang` + `default` flag
- Live event log when you tap "Speak" — useful for diagnosing why a voice isn't playing
