tts-voice-creator-clone-and.../static/js/i18n.js
mARTin-B78 36437478a6 feat: Read Aloud reader, multi-voice audiobook casting, character sheets (v1.7.0)
Read Aloud (new "Vorlesen" tab):
- PDF (real page render + overlay highlight) / TXT reader with live word
  highlighting, voice + speed, per-sentence synthesis-state colours, zoom
  (fit-width/height, two-page, ±), resume, and a server-side book library
  (syncs across devices; per-unit MP3 audio fetched on demand).

Book -> multi-speaker audiobook:
- "Cast as audiobook" attributes dialogue to characters via the LLM
  (guillemet/quote-style aware, turn-taking, recent-context), with a
  deterministic speech-tag fallback. Editable preview, non-blocking live
  casting panel, then auto-saved as a reopenable Script Rehearser play.
- Audiobook export: synthesise every cast line -> one MP3 per chapter.

Character sheets:
- LLM-extracted, self-filling RPG-style sheets (with page+quote sources)
  in both Read Aloud and the Rehearser.

Also: MP3 storage + per-page/sentence export, voice-library "Precompute
embeddings" pre-warm, German "Vorlesen" i18n + flag language toggle,
large-PDF performance (lazy raster, buffer/canvas eviction, yielded parse),
and the Seed Finder changelog entry.

New: routes/reader.py, POST /api/attribute-dialogue, POST /api/character-sheets,
static/js/{reader,audiobook,character-sheets}.js, static/sections/s-reader.html.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:08:46 +02:00

148 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── Lightweight i18n ─────────────────────────────────────────────────────────
// English is the source language (keys = the exact English UI text). Switching
// language persists the choice and reloads, so translations are applied once to the
// freshly-rendered English DOM (no fragile reverse-translation / re-render tracking).
//
// Coverage v1: the static UI chrome (nav, section titles/subtitles, common
// buttons/labels). Add more by adding "English": "translation" pairs below, or call
// window.t('English') for JS-generated strings. Dynamic lists can be translated by
// calling window.applyI18n(container) after rendering.
const I18N_LANGS = { en: 'English', de: 'Deutsch' };
const I18N_DICT = {
de: {
// Brand / sidebar groups
'Voice Creator': 'Voice Creator',
'Clone · Design · Deploy': 'Klonen · Entwerfen · Bereitstellen',
'Voices': 'Stimmen', 'Setup': 'Einrichtung', 'Tags': 'Tags',
// Sidebar nav
'My Voices': 'Meine Stimmen', 'All voices': 'Alle Stimmen', 'Cloned': 'Geklont',
'Designed': 'Entworfen', 'Favorites': 'Favoriten', 'Hidden': 'Ausgeblendet',
'Library tools': 'Bibliothekswerkzeuge',
'Clone a Voice': 'Stimme klonen', 'Design a Voice': 'Stimme entwerfen',
'Get Voices Online': 'Stimmen online holen', 'Try It Out': 'Ausprobieren',
'Read Aloud': 'Vorlesen',
'Script Rehearser': 'Skript-Probe', 'Library': 'Bibliothek', 'Cast': 'Besetzung',
'Stage': 'Bühne', 'Summary': 'Zusammenfassung', 'Import / Export': 'Import / Export',
'Conversation': 'Unterhaltung', 'Benchmark': 'Benchmark', 'Engines': 'Engines',
'Language Models': 'Sprachmodelle', 'Speech to Text': 'Sprache-zu-Text',
'Text to Speech': 'Text-zu-Sprache', 'App Routing': 'App-Routing',
'Connect Apps': 'Apps verbinden', 'Settings': 'Einstellungen',
// Section titles
'Conversation Playground': 'Unterhaltungs-Spielplatz',
// Section subtitles
'Pick a voice on the left, edit on the right.': 'Wähle links eine Stimme, bearbeite sie rechts.',
'Capture 320 seconds of clean speech, trim it, name it, then save it as a reusable voice clone.':
'Nimm 320 Sekunden saubere Sprache auf, schneide sie zu, benenne sie und speichere sie als wiederverwendbaren Stimmklon.',
'Describe a voice in words and let the AI create it. No recording needed.':
'Beschreibe eine Stimme in Worten und lass sie von der KI erstellen. Keine Aufnahme nötig.',
'Browse public voice clip sources, preview direct audio files, and import voices from the web.':
'Durchsuche öffentliche Stimm-Quellen, höre Audiodateien vorab an und importiere Stimmen aus dem Web.',
'Generate speech from text using any backend and voice. Also transcribe audio and re-speak it.':
'Erzeuge Sprache aus Text mit beliebigem Backend und Stimme. Transkribiere Audio und sprich es neu ein.',
'Upload a script, cast characters to TTS voices or your own mic, then rehearse scene by scene.':
'Lade ein Skript hoch, besetze Figuren mit TTS-Stimmen oder deinem Mikrofon und probe Szene für Szene.',
'Import a PDF or text document, pick a voice and speed, then have it read to you while the word being spoken is highlighted.':
'Importiere ein PDF- oder Textdokument, wähle Stimme und Tempo und lass es dir vorlesen, während das gerade gesprochene Wort hervorgehoben wird.',
// Read Aloud UI
'My books': 'Meine Bücher', 'Voice consistency': 'Stimmkonstanz',
'Normalise loudness': 'Lautstärke angleichen', 'Export MP3': 'MP3 exportieren',
'Select range': 'Bereich wählen', 'Synthesise': 'Synthetisieren',
'Tone / style': 'Ton / Stil', 'Temperature': 'Temperatur', 'Speed': 'Tempo',
'Saved documents with their synthesised audio — reopen to continue.':
'Gespeicherte Dokumente mit ihrem synthetisierten Audio — zum Fortsetzen erneut öffnen.',
// Common buttons / labels
'Save changes': 'Änderungen speichern', 'Save to library': 'In Bibliothek speichern',
'Save to Voice Library': 'In Stimmbibliothek speichern', 'Delete voice': 'Stimme löschen',
'Cancel': 'Abbrechen', 'Refresh': 'Aktualisieren', 'Delete': 'Löschen', 'Save': 'Speichern',
'Back': 'Zurück', 'Apply →': 'Anwenden →', 'Browse': 'Durchsuchen', 'Download': 'Herunterladen',
'Record': 'Aufnehmen', 'Stop': 'Stopp', 'Play': 'Abspielen', 'Play selection': 'Auswahl abspielen',
'Check level': 'Pegel prüfen', 'Stop monitor': 'Monitor stoppen', 'Auto trim': 'Auto-Zuschnitt',
'Auto-transcribe': 'Auto-Transkription', 'Active': 'Aktiv', 'copy ID': 'ID kopieren',
'edit ID': 'ID bearbeiten', 'New voice': 'Neue Stimme', 'All languages': 'Alle Sprachen',
'All genders': 'Alle Geschlechter', 'Microphone': 'Mikrofon', 'Upload file': 'Datei hochladen',
'Sort': 'Sortieren', 'Cards': 'Karten', 'List': 'Liste', 'Develop': 'Ausarbeiten',
'Match local': 'Lokal zuordnen', 'Match online': 'Online zuordnen', 'Design all': 'Alle entwerfen',
'I play this': 'Ich spiele das', 'Save to library': 'In Bibliothek speichern',
'Open rehearsal': 'Probe öffnen', 'Fetch voices': 'Stimmen abrufen',
'Name your voice': 'Benenne deine Stimme', 'Reference transcript': 'Referenz-Transkript',
'Preview': 'Vorschau', 'Trim your sample': 'Probe zuschneiden', 'Language': 'Sprache',
'Gender': 'Geschlecht', 'Voice': 'Stimme', 'Tags': 'Tags', 'Speaking style · voice-design prompt': 'Sprechstil · Voice-Design-Prompt',
// Common placeholders
'Search voices…': 'Stimmen suchen…', 'Search voices...': 'Stimmen suchen...',
'Filter name or tag…': 'Name oder Tag filtern…',
},
};
let _appLang = 'en';
try { _appLang = localStorage.getItem('app-lang') || 'en'; } catch (_) {}
window.__APP_LANG__ = _appLang;
// Translate a single source string (for JS-generated UI).
window.t = function (s) {
if (_appLang === 'en' || s == null) return s;
const d = I18N_DICT[_appLang];
return (d && d[String(s).trim()] != null) ? d[String(s).trim()] : s;
};
const _I18N_SKIP = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'CODE', 'PRE', 'OPTION']);
// Translate visible text nodes + common attributes within `root` (default: whole doc).
window.applyI18n = function (root) {
if (_appLang === 'en') return;
const dict = I18N_DICT[_appLang]; if (!dict) return;
root = root || document.body; if (!root) return;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(n) {
if (!n.nodeValue || !n.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
const p = n.parentNode;
if (p && _I18N_SKIP.has(p.nodeName)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
},
});
const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode);
for (const n of nodes) {
const key = n.nodeValue.trim();
if (dict[key] != null) n.nodeValue = n.nodeValue.replace(key, dict[key]);
}
root.querySelectorAll('[placeholder],[title],[aria-label]').forEach(el => {
['placeholder', 'title', 'aria-label'].forEach(a => {
const v = el.getAttribute(a);
if (v && dict[v.trim()] != null) el.setAttribute(a, dict[v.trim()]);
});
});
};
// Language change → persist + reload (translations re-apply to fresh English DOM).
window.setAppLang = function (lang) {
if (!I18N_LANGS[lang]) return;
try { localStorage.setItem('app-lang', lang); } catch (_) {}
location.reload();
};
// Flag of a language code (flag-icons classes; English → GB).
function _langFlag(code) { return code === 'en' ? 'gb' : code; }
// Inject a flag toggle next to the "Voice Creator" headline. Clicking cycles to
// the next language (a simple toggle while there are two).
window.initLangPicker = function () {
const brand = document.querySelector('.sidebar-brand');
const head = brand && brand.querySelector('h1');
if (!head || document.getElementById('app-lang-toggle')) return;
const codes = Object.keys(I18N_LANGS);
const btn = document.createElement('button');
btn.id = 'app-lang-toggle';
btn.className = 'app-lang-toggle';
btn.type = 'button';
const nextOf = () => codes[(codes.indexOf(_appLang) + 1) % codes.length];
const render = () => {
btn.innerHTML = `<span class="fi fi-${_langFlag(_appLang)}"></span>`;
const label = 'Language: ' + I18N_LANGS[_appLang] + ' — switch to ' + I18N_LANGS[nextOf()];
btn.title = label; btn.setAttribute('aria-label', label);
};
render();
btn.addEventListener('click', () => window.setAppLang(nextOf()));
head.appendChild(btn);
};