Files
UmbraAudio/samples/translations/speakers.json
B.D. Murphy 2b294d7fa4 Per-utterance language flag + a working language selector (spec 31)
The review UI's language dropdown did nothing: every selection kept
rendering the English text. Two independent causes, both fixed.

1. The translations were never being read. The pipeline's
   `utterance_translations` table holds one row in Deal Hunter, and that
   row stores the *English* text under a `zh-Hans` code. The real
   translations live in the on-disk sample files under
   `samples/translations/`. `umbraaudio/translations.py` now resolves both
   sources and merges them by (chapter_id, utterance_index): the table wins
   where it holds a real translation, the sample files fill the gaps. A
   candidate whose text is identical to the source is a placeholder, not a
   translation, so it is ignored - otherwise the page would show English
   under a Chinese heading and claim it was translated.

2. The query that read them was broken. `language_routes.py` selected
   `u.index` and `u.speaker`; the columns are `utterance_index` and
   `speaker_name`, so every read raised `sqlite3.OperationalError: near
   "index": syntax error` and `/language-mockup` returned 500. It also
   opened `<project>/project.db` - a 0-byte decoy - instead of
   `<project>/.umbra/project.db`, the file that actually holds the data.

Also in this commit:

* The chapter review page honours `?lang=`, remembers the choice in a
  cookie (set by a middleware, so it survives on every page), and shows the
  selected language's *name* in the "has not been translated yet" message.
* The page now shows the translation, not the source, and the edit box is
  bound to the source text so saving cannot overwrite the English with the
  Chinese.
* The stray language selector that sat after `</html>` in base.html is
  gone; the chapters-list page's dropdown had zero options because the
  route never passed the language list.
* The language tag is wired into generation: `utterance.language_code`
  (NULL = project source language) reaches `engine.generate()` and the
  staleness marker, so a retagged line is re-rendered instead of keeping
  English phonology behind an unchanged text. The marker records the
  language; a marker written before the tag existed means English, so the
  ~4,300 existing clips are not invalidated.
* `project.source_language` (spec 31 section 13.1) records the book's own
  language, with a migration for existing project DBs.
* A scene-break marker deliberately records no language: it is silence,
  identical in every language, and tagging it would claim a phonology the
  audio does not have.

Tests: 883 passed / 8 failed - the same 8 pre-existing failures
(2 test_pronunciation, 6 voice_profile_manager accent). 26 new tests
cover the resolver and the generation wiring. ruff introduces no new
findings.
2026-11-06 16:55:49 -08:00

139 lines
3.0 KiB
JSON

{
"speakers": [
{
"name": "Kainda Walters Foster Murphy",
"type": "narration",
"voice_ref": "kainda_walters_foster_murphy_ref.wav"
},
{
"name": "narrator",
"type": "narration",
"voice_ref": "narrator_ref.wav"
},
{
"name": "Deal Hunter",
"type": "narration",
"voice_ref": "deal_hunter_ref.wav"
},
{
"name": "Nanny",
"type": "narration",
"voice_ref": "nanny_ref.wav"
},
{
"name": "Princess Kainda",
"type": "dialogue",
"voice_ref": "princess_kainda_ref.wav"
},
{
"name": "Duchess Victoria",
"type": "narration",
"voice_ref": "duchess_victoria_ref.wav"
},
{
"name": "Adric",
"type": "dialogue",
"voice_ref": "adric_ref.wav"
},
{
"name": "Kabir",
"type": "dialogue",
"voice_ref": "kabir_ref.wav"
},
{
"name": "Adonai Maul",
"type": "dialogue",
"voice_ref": "adonai_maul_ref.wav"
},
{
"name": "Xu Ming",
"type": "dialogue",
"voice_ref": "xu_ming_ref.wav"
},
{
"name": "Willamina Wake",
"type": "dialogue",
"voice_ref": "willamina_wake_ref.wav"
},
{
"name": "Distorted Voice",
"type": "dialogue",
"voice_ref": "distorted_voice_ref.wav"
},
{
"name": "Worker Robot (Bill)",
"type": "dialogue",
"voice_ref": "worker_robot_(bill)_ref.wav"
},
{
"name": "Captain Weston",
"type": "dialogue",
"voice_ref": "captain_weston_ref.wav"
},
{
"name": "Security Administrator",
"type": "dialogue",
"voice_ref": "security_administrator_ref.wav"
},
{
"name": "Pirate Clan Leader",
"type": "dialogue",
"voice_ref": "pirate_clan_leader_ref.wav"
},
{
"name": "Narrator",
"type": "dialogue",
"voice_ref": "narrator_ref.wav"
},
{
"name": "Trader Wakam",
"type": "dialogue",
"voice_ref": "trader_wakam_ref.wav"
},
{
"name": "Service Robot",
"type": "dialogue",
"voice_ref": "service_robot_ref.wav"
},
{
"name": "Admiral Wilbur Wake",
"type": "dialogue",
"voice_ref": "admiral_wilbur_wake_ref.wav"
},
{
"name": "Pilot (Freighter)",
"type": "dialogue",
"voice_ref": "pilot_(freighter)_ref.wav"
},
{
"name": "Trader Romano",
"type": "narration",
"voice_ref": "trader_romano_ref.wav"
},
{
"name": "Frigate Captain",
"type": "dialogue",
"voice_ref": "frigate_captain_ref.wav"
},
{
"name": "Crew Member",
"type": "dialogue",
"voice_ref": "crew_member_ref.wav"
},
{
"name": "Brutus",
"type": "dialogue",
"voice_ref": "brutus_ref.wav"
},
{
"name": "Guard Detail Speaker 1",
"type": "dialogue",
"voice_ref": "guard_detail_speaker_1_ref.wav"
},
{
"name": "Guard Detail Speaker 2",
"type": "dialogue",
"voice_ref": "guard_detail_speaker_2_ref.wav"
}
]
}