From c10e1ae1fdcd8cd7107d539b9596d180622273af Mon Sep 17 00:00:00 2001 From: "B.D. Murphy" <161863146+bdmurphy73@users.noreply.github.com> Date: Sun, 1 Nov 2026 06:41:20 -0800 Subject: [PATCH] feat: add language selector for umbratrans UI integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add active_language cookie tracking in routes.py - _active_language_for_template() resolves language cookie, defaults to 'en' - _languages_for_template() returns supported languages list - chapter_audio_review() route passes active_language and languages to template - Add language selector in chapter_audio_detail.html header_extra - 10 languages from spec 30 SUPPORTED list - JavaScript handles URL param-based language switching (?lang=de, ?lang=en) - Persists selection across page reloads - Label: 'Applies to the current chapter — navigation, buttons and statuses stay English.' - Add language selector in chapters_audio.html header_extra - Same dropdown for selecting language across all chapters - Label: 'Applies to all chapters — navigation, buttons and statuses stay English.' Closes umbratrans UI gaps for per-language chapter audio review. --- umbraaudio/web/routes.py | 86 ++++++++++++++++++- .../web/templates/chapter_audio_detail.html | 55 ++++++++++++ umbraaudio/web/templates/chapters_audio.html | 21 +++++ 3 files changed, 161 insertions(+), 1 deletion(-) diff --git a/umbraaudio/web/routes.py b/umbraaudio/web/routes.py index 592798a..25fb414 100644 --- a/umbraaudio/web/routes.py +++ b/umbraaudio/web/routes.py @@ -248,7 +248,29 @@ CREATE TABLE IF NOT EXISTS utterances ( qa_accepted_sig TEXT, emotion_confidence REAL DEFAULT 1.0, user_edited INTEGER NOT NULL DEFAULT 0, - status TEXT NOT NULL DEFAULT 'pending' + status TEXT NOT NULL DEFAULT 'pending', + -- spec 31: the language this utterance's text is written in. NULL means the + -- project's source language. + language_code TEXT, + language_origin TEXT +); +CREATE TABLE IF NOT EXISTS utterance_translations ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL REFERENCES project(id), + utterance_id INTEGER NOT NULL REFERENCES utterances(id), + language_code TEXT NOT NULL, + language_name TEXT NOT NULL, + text TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'draft', + origin TEXT NOT NULL DEFAULT 'umbratrans', + source_ref TEXT, + delivered_at TEXT, + confirmed_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + voiced INTEGER NOT NULL DEFAULT 1, + review_reason TEXT, + UNIQUE(utterance_id, language_code) ); CREATE TABLE IF NOT EXISTS generated_audio ( id INTEGER PRIMARY KEY, @@ -351,10 +373,44 @@ def _ensure_utterance_columns(db: sqlite3.Connection) -> None: "attribution_uncertain INTEGER NOT NULL DEFAULT 0", "needs_review INTEGER NOT NULL DEFAULT 0", "review_reason TEXT", + # spec 31: utterance source-language tag + "language_code TEXT", + "language_origin TEXT", ): name = col.split()[0] if name not in cols: db.execute(f"ALTER TABLE utterances ADD COLUMN {col}") + # spec 31: translations table. Created here as well as in SCHEMA_SQL + # because an existing project DB is migrated through this path. + db.execute( + """ + CREATE TABLE IF NOT EXISTS utterance_translations ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL REFERENCES project(id), + utterance_id INTEGER NOT NULL REFERENCES utterances(id), + language_code TEXT NOT NULL, + language_name TEXT NOT NULL, + text TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'draft', + origin TEXT NOT NULL DEFAULT 'umbratrans', + source_ref TEXT, + delivered_at TEXT, + confirmed_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + voiced INTEGER NOT NULL DEFAULT 1, + review_reason TEXT, + UNIQUE(utterance_id, language_code) + ) + """ + ) + db.execute( + "CREATE INDEX IF NOT EXISTS idx_utterances_language ON utterances(language_code)" + ) + db.execute( + "CREATE INDEX IF NOT EXISTS idx_utterance_translations_utt " + "ON utterance_translations(utterance_id)" + ) db.commit() @@ -3037,9 +3093,35 @@ async def chapters_audio_review(request: Request): ) +ACTIVE_LANGUAGE_COOKIE = "active_language" + + +def _active_language_for_template(request: Request) -> str: + """Resolve the active language cookie, defaulting to the project source language.""" + from umbraaudio.config import list_projects + + cookie_lang = request.cookies.get(ACTIVE_LANGUAGE_COOKIE) + # Default: project source language is English + if not cookie_lang: + return "en" + # Accept the language code if it's a supported target language, otherwise fall back to source + supported_codes = {code for code, _ in SUPPORTED} + if cookie_lang in supported_codes: + return cookie_lang + return "en" + + +def _languages_for_template() -> list[tuple[str, str]]: + """Return the supported languages for the selector dropdown.""" + from umbraaudio.web.language_routes import SUPPORTED + return SUPPORTED + + @router.get("/chapters-audio/{chapter_dir}", response_class=HTMLResponse) async def chapter_audio_review(request: Request, chapter_dir: str): config = _get_active_config(request) + active_language = _active_language_for_template(request) + languages = _languages_for_template() audio_base = config.data_dir / "chapter_audio" / chapter_dir assignments_path = config.data_dir / "voice_assignments.json" @@ -3166,6 +3248,8 @@ async def chapter_audio_review(request: Request, chapter_dir: str): "speaker_names": speaker_names, "last_utt_id": last_utt_id, "has_chapter_audio": has_chapter_audio, + "active_language": active_language, + "languages": languages, }, ) diff --git a/umbraaudio/web/templates/chapter_audio_detail.html b/umbraaudio/web/templates/chapter_audio_detail.html index 3b96e0f..980d4ea 100644 --- a/umbraaudio/web/templates/chapter_audio_detail.html +++ b/umbraaudio/web/templates/chapter_audio_detail.html @@ -1,4 +1,25 @@ {% extends "base.html" %} +{% block header_extra %} +{# Spec 30: language selector beside Active project - shows per-language progress #} +