diff --git a/routes/tts.py b/routes/tts.py index 3cfee3d..2e351be 100644 --- a/routes/tts.py +++ b/routes/tts.py @@ -458,6 +458,27 @@ async def tts_preview(request: Request): backend = _clean_preview_backend(data.get("backend", "voice_clone")) settings = _load_settings() + # Per-request generation overrides (seed / temperature / top_p). When provided, + # force them onto the stability params for this backend so _apply_tts_extra_params + # forwards them to the engine (backends that reject them fall back gracefully). + _overrides = {} + for _k in ("seed", "temperature", "top_p"): + _v = data.get(_k) + if _v is not None and _v != "": + try: + _overrides[_k] = int(_v) if _k == "seed" else float(_v) + except (TypeError, ValueError): + pass + if _overrides: + from core.config import _tts_extra_params + settings = dict(settings) + eff = dict(_tts_extra_params(settings, backend) or {}) + eff.update(_overrides) + settings["tts_stability_enabled"] = True + by_backend = dict(settings.get("tts_extra_params_by_backend") or {}) + by_backend[backend] = eff + settings["tts_extra_params_by_backend"] = by_backend + if data.get("apply_persona"): scan_dir = Path(settings.get("voices_scan_dir", _VOICES_DIR_DEFAULT)) wav = _find_voice_audio(voice, scan_dir) @@ -854,3 +875,32 @@ async def openai_speech_proxy(request: Request): @router.post("/v1") async def openai_speech_proxy_v1_shortcut(request: Request): return await openai_speech_proxy(request) + + +@router.post("/api/tts-voice-seed") +async def tts_voice_seed(request: Request): + """Proxy to the faster-qwen3-tts /voice-seed endpoint. + + Writes or clears the seed for a voice in the TTS server's voices.json. + Body: {"voice": "EN_F_NatashaNeural", "seed": 7} (seed: null to remove) + """ + data = await request.json() + settings = _load_settings() + tts_base = _preview_backend_base_url(settings, "voice_clone").rstrip("/") + try: + resp = requests.post( + f"{tts_base}/voice-seed", + json=data, + timeout=10, + ) + resp.raise_for_status() + return resp.json() + except requests.exceptions.ConnectionError: + raise HTTPException(502, "Could not reach TTS server") + except requests.exceptions.HTTPError as e: + detail = "" + try: + detail = e.response.json().get("detail", "") + except Exception: + pass + raise HTTPException(e.response.status_code, detail or str(e)) diff --git a/static/js/seed-finder.js b/static/js/seed-finder.js new file mode 100644 index 0000000..c27f065 --- /dev/null +++ b/static/js/seed-finder.js @@ -0,0 +1,213 @@ +// ── Seed Finder — find the best RNG seed for a voice ───────────────────────── +// +// Appended to the voice inspector body by voice-inspector.js. +// Generates audio samples for a voice across a range of seeds so the user +// can listen and pick the most natural-sounding one, then save it to the +// TTS server's voices.json with one click. + +const SEED_FINDER_TEXT_DE = ( + 'Die 3.500 neuen High-End Geräte für das Server-Update benötigen eine ' + + 'außergewöhnlich starke Kühlung und regelmäßige Maßnahmen, um die Performance ' + + 'bei großer Last zu gewährleisten.' +); +const SEED_FINDER_TEXT_EN = ( + 'The system administrator successfully configured the customized Docker stacks ' + + 'and benchmarked the inference engines at exactly 8:45 AM.' +); +const SEED_FINDER_TEXT_MIXED = SEED_FINDER_TEXT_DE + ' - ' + SEED_FINDER_TEXT_EN; + +function _seedFinderDefaultText(voiceId) { + const lc = (voiceId || '').toLowerCase(); + if (lc.startsWith('de_')) return SEED_FINDER_TEXT_MIXED; + if (lc.startsWith('en_') || lc.startsWith('gb_')) return SEED_FINDER_TEXT_EN; + return SEED_FINDER_TEXT_MIXED; +} + +// Called from voice-inspector.js after the persona panel is appended. +function attachSeedFinder(voiceId, body) { + const panel = document.createElement('div'); + panel.className = 'opt-group seed-finder-group'; + panel.innerHTML = ` +
+ + 🎲 Seed Finder + Find the most natural-sounding voice +
+
+ Generates audio for each seed so you can listen and pick the best one. + Click "Use this seed" to save it to the TTS server. +
+
+
+
+ + +
+
+
+ + + + +
+
+ + +
+
+
+ + + +
+
+ + + +
+
+ `; + + body.appendChild(panel); + + // Toggle open/closed + panel.querySelector('.opt-group-title').addEventListener('click', () => { + panel.classList.toggle('open'); + }); + + const textEl = panel.querySelector('.seed-finder-text'); + const fromEl = panel.querySelector('.seed-finder-from'); + const toEl = panel.querySelector('.seed-finder-to'); + const backendEl = panel.querySelector('.seed-finder-backend'); + const runBtn = panel.querySelector('.seed-finder-run-btn'); + const cancelBtn = panel.querySelector('.seed-finder-cancel-btn'); + const statusEl = panel.querySelector('.seed-finder-status'); + const progressEl = panel.querySelector('.seed-finder-progress'); + const progLabel = panel.querySelector('.seed-finder-progress-label'); + const progCount = panel.querySelector('.seed-finder-progress-count'); + const progBar = panel.querySelector('.seed-finder-bar'); + const resultsEl = panel.querySelector('.seed-finder-results'); + + textEl.value = _seedFinderDefaultText(voiceId); + + let _cancelled = false; + let _currentAudio = null; + + function _playBlob(blob) { + if (_currentAudio) { _currentAudio.pause(); _currentAudio = null; } + const url = URL.createObjectURL(blob); + const audio = new Audio(url); + _currentAudio = audio; + audio.play().catch(() => {}); + audio.addEventListener('ended', () => { URL.revokeObjectURL(url); _currentAudio = null; }); + } + + function _addResultRow(seed, blob, durationSec, err, activeSeedSet) { + const row = document.createElement('div'); + row.className = 'seed-finder-result-row' + (err ? ' seed-finder-result-error' : ''); + row.dataset.seed = seed; + + if (err) { + row.innerHTML = ` + Seed ${seed} + Failed: ${escHtml(String(err))} + `; + } else { + const durStr = durationSec ? `${durationSec.toFixed(1)}s` : ''; + row.innerHTML = ` + Seed ${seed} + ${durStr} + + + + `; + row.querySelector('.sfr-play-btn').addEventListener('click', () => _playBlob(blob)); + row.querySelector('.sfr-use-btn').addEventListener('click', async () => { + // Stop any playback + if (_currentAudio) { _currentAudio.pause(); _currentAudio = null; } + try { + const resp = await fetch('/api/tts-voice-seed', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ voice: voiceId, seed }), + }); + if (!resp.ok) { + const e = await resp.json().catch(() => ({})); + throw new Error(e.detail || resp.statusText); + } + // Clear all other "saved" markers + resultsEl.querySelectorAll('.sfr-saved').forEach(el => el.textContent = ''); + resultsEl.querySelectorAll('.seed-finder-result-row').forEach(r => r.classList.remove('sfr-active')); + row.classList.add('sfr-active'); + row.querySelector('.sfr-saved').textContent = '✓ Saved'; + if (typeof toast === 'function') toast(`Seed ${seed} saved for ${voiceId}`, 'success'); + } catch (e) { + if (typeof toast === 'function') toast('Save failed: ' + e.message, 'error'); + } + }); + } + + resultsEl.appendChild(row); + } + + async function _runFinder() { + const text = textEl.value.trim(); + if (!text) { statusEl.textContent = 'Enter a test sentence.'; return; } + + const seedFrom = Math.max(0, parseInt(fromEl.value, 10) || 1); + const seedTo = Math.max(seedFrom, parseInt(toEl.value, 10) || 15); + const backend = backendEl.value; + const seeds = []; + for (let s = seedFrom; s <= seedTo; s++) seeds.push(s); + const total = seeds.length; + + _cancelled = false; + resultsEl.innerHTML = ''; + progressEl.style.display = ''; + runBtn.style.display = 'none'; + cancelBtn.style.display = ''; + statusEl.textContent = ''; + progBar.style.width = '0%'; + progCount.textContent = `0 / ${total}`; + progLabel.textContent = 'Generating…'; + + for (let i = 0; i < seeds.length; i++) { + if (_cancelled) break; + const seed = seeds[i]; + progLabel.textContent = `Seed ${seed}…`; + progCount.textContent = `${i} / ${total}`; + progBar.style.width = `${Math.round(i / total * 100)}%`; + + try { + const blob = await fetchTtsPreviewBlob( + voiceId, text, 'wav', '', backend, false, { seed } + ); + // Estimate duration from WAV byte length: (bytes - 44 header) / (24000 * 2) + const durSec = Math.max(0, (blob.size - 44) / (24000 * 2)); + _addResultRow(seed, blob, durSec, null, false); + } catch (e) { + _addResultRow(seed, null, 0, e.message || String(e), false); + } + } + + progBar.style.width = '100%'; + progCount.textContent = `${seeds.length} / ${total}`; + progLabel.textContent = _cancelled ? 'Cancelled.' : 'Done — listen and pick your seed.'; + runBtn.style.display = ''; + cancelBtn.style.display = 'none'; + } + + runBtn.addEventListener('click', _runFinder); + cancelBtn.addEventListener('click', () => { _cancelled = true; }); +} diff --git a/static/js/voice-inspector.js b/static/js/voice-inspector.js index a352e55..6e592d1 100644 --- a/static/js/voice-inspector.js +++ b/static/js/voice-inspector.js @@ -410,6 +410,11 @@ function selectVoice(wrap) { }); }); + // ── Seed Finder panel ──────────────────────────────────────────────────── + if (typeof attachSeedFinder === 'function') { + attachSeedFinder(voiceId, body); + } + // Track extracted elements for restoreToRow wrap._extracted = [ activeEl ? {el: activeEl, target: detailRow} : null, diff --git a/static/loader.js b/static/loader.js index 1120e22..48f7cc8 100644 --- a/static/loader.js +++ b/static/loader.js @@ -3,7 +3,7 @@ const SECTIONS = [ 's-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-rehearser', - 's-performance', 's-routing', 's-connect', 's-settings', + 's-reader', 's-performance', 's-routing', 's-connect', 's-settings', 's-llms', 's-conversation', ]; @@ -110,6 +110,7 @@ '/static/js/voice-picker.js', '/static/js/benchmark-voice-picker.js', '/static/js/voice-inspector.js', + '/static/js/seed-finder.js', '/static/js/voice-sources.js', '/static/js/fishaudio-browser.js', '/static/js/integrations.js', @@ -122,6 +123,9 @@ '/static/js/stt.js', '/static/js/rehearser-parse.js', '/static/js/rehearser.js', + '/static/js/reader.js', + '/static/js/audiobook.js', + '/static/js/character-sheets.js', ]; var _useBundle = window.APP_USE_BUNDLE === true || location.search.indexOf('bundle=1') !== -1; var _bundleOk = false; diff --git a/static/style.css b/static/style.css index 75d762a..92c13c3 100644 --- a/static/style.css +++ b/static/style.css @@ -60,8 +60,15 @@ body { } /* Language picker in the sidebar brand */ -.app-lang-picker { margin-top: 8px; font-size: 12px; padding: 4px 8px; border: 1px solid var(--border); border-radius: 7px; background: var(--panel); color: var(--text); font-family: var(--font); cursor: pointer; width: 100%; } -.app-lang-picker:focus { outline: none; border-color: var(--accent); } +.app-lang-toggle { + margin-left: auto; padding: 2px; border: 1px solid var(--border); border-radius: 5px; + background: var(--panel); cursor: pointer; line-height: 0; display: inline-flex; + align-items: center; transition: border-color .15s, transform .1s; +} +.app-lang-toggle:hover { border-color: var(--accent); } +.app-lang-toggle:active { transform: scale(.94); } +.app-lang-toggle:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(37,99,235,.18); } +.app-lang-toggle .fi { width: 20px; height: 14px; border-radius: 2px; } /* ── App shell ───────────────────────────────────────────────────────────── */ #app-shell { @@ -82,6 +89,7 @@ body { .sidebar-brand h1 { font-size: 15px; font-weight: 800; color: var(--accent); letter-spacing: -0.01em; line-height: 1.2; + display: flex; align-items: center; gap: 6px; } .sidebar-brand p { font-size: 14px; color: var(--subtext); margin-top: 3px; @@ -2707,6 +2715,55 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .preview-persona-btn:hover { background: rgba(13,148,136,.12); } .preview-persona-btn:disabled { opacity: .5; cursor: default; } +/* ── Seed Finder ─────────────────────────────────────────────────────────── */ +.seed-finder-group { margin-top: 12px; } +.seed-finder-body { padding: 12px 14px 14px; display: flex; flex-direction: column; gap: 12px; } +.seed-finder-config { display: flex; flex-direction: column; gap: 10px; } +.seed-finder-row { display: flex; flex-direction: column; gap: 5px; } +.seed-finder-label { font-size: 12px; font-weight: 600; color: var(--subtext); } +.seed-finder-text { + width: 100%; font-size: 13px; padding: 8px 10px; + border: 1px solid var(--border); border-radius: var(--radius); + background: var(--surface); color: var(--text); + font-family: var(--font); resize: vertical; min-height: 68px; + transition: border-color .15s; +} +.seed-finder-text:focus { border-color: var(--accent); outline: none; } +.seed-finder-params { flex-direction: row; flex-wrap: wrap; align-items: flex-end; gap: 14px; } +.seed-finder-field { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text); } +.seed-finder-field input[type="number"] { + width: 64px; padding: 5px 7px; font-size: 13px; + border: 1px solid var(--border); border-radius: var(--radius); + background: var(--surface); color: var(--text); text-align: center; +} +.seed-finder-field select { + padding: 5px 8px; font-size: 13px; + border: 1px solid var(--border); border-radius: var(--radius); + background: var(--surface); color: var(--text); +} +.seed-finder-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.seed-finder-status { font-size: 12px; color: var(--subtext); } + +.seed-finder-progress { display: flex; flex-direction: column; gap: 6px; } +.seed-finder-progress-head { display: flex; justify-content: space-between; font-size: 12px; color: var(--subtext); } +.seed-finder-bar { height: 100%; background: var(--accent); transition: width .2s ease; border-radius: 999px; } + +.seed-finder-results { display: flex; flex-direction: column; gap: 6px; } +.seed-finder-result-row { + display: flex; align-items: center; gap: 8px; flex-wrap: wrap; + padding: 7px 10px; border-radius: var(--radius); + border: 1px solid var(--border); background: var(--surface); + font-size: 13px; +} +.seed-finder-result-row.sfr-active { border-color: var(--accent); background: rgba(var(--accent-rgb, 124,77,255),.07); } +.seed-finder-result-row.seed-finder-result-error { opacity: .6; } +.sfr-seed { font-weight: 700; min-width: 58px; color: var(--text); } +.sfr-dur { color: var(--subtext); font-size: 12px; min-width: 36px; } +.sfr-play-btn { font-size: 12px; padding: 4px 10px; } +.sfr-use-btn { font-size: 12px; padding: 4px 10px; } +.sfr-saved { font-size: 12px; color: var(--green); font-weight: 600; } +.sfr-error { font-size: 12px; color: var(--red); } + /* ── Conversation Playground ─────────────────────────────────────────────── */ .conv-config-bar { padding: 14px 18px 10px; margin-bottom: 0; } .conv-config-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-end; } @@ -3771,3 +3828,246 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami min-height: 46px; flex: 0 0 auto; } + +/* ── Read Aloud reader ─────────────────────────────────────────────────── */ +.reader-controls { + display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; +} +.reader-ctl { margin: 0; min-width: 150px; } +.reader-ctl-grow { flex: 1 1 200px; } +.reader-ctl-sm { min-width: 96px; max-width: 130px; } +.reader-ctl-sm input { width: 100%; } +.reader-norm-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--subtext); white-space: nowrap; align-self: center; cursor: pointer; } +.reader-backend-hint { margin-top: 10px; font-size: 12px; color: var(--subtext); display: flex; align-items: flex-start; gap: 6px; } +.reader-backend-hint .mdi { color: var(--accent); flex-shrink: 0; } +.reader-backend-hint b { color: var(--text); } +.reader-backend-hint.reader-hint-warn { color: var(--text); } +.reader-backend-hint:empty { display: none; } +.reader-voice-pick { display: flex; gap: 6px; } +.reader-voice-pick select { flex: 1; min-width: 0; } +.reader-speed-label { color: var(--accent); font-weight: 700; } +.reader-ctl input[type=range] { width: 100%; accent-color: var(--accent); } +.reader-import-btn { cursor: pointer; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; } + +.reader-dropzone { + margin-top: 14px; padding: 30px 16px; border: 2px dashed var(--border); + border-radius: 12px; text-align: center; color: var(--subtext); + display: flex; flex-direction: column; align-items: center; gap: 5px; + transition: border-color .15s, background .15s; +} +.reader-dropzone-icon { font-size: 34px; opacity: .5; } +.reader-dropzone.dragover { border-color: var(--accent); background: rgba(37,99,235,.06); color: var(--accent); } +.reader-dropzone.dragover .reader-dropzone-icon { opacity: 1; } + +.reader-transport { + display: flex; align-items: center; gap: 14px; flex-wrap: wrap; + margin-top: 14px; padding: 10px 14px; background: var(--surface); + border: 1px solid var(--border); border-radius: var(--radius); +} +.reader-tb-group { display: flex; gap: 4px; } +.reader-progress-wrap { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 160px; } +.reader-progress-track { flex: 1; height: 6px; border-radius: 4px; background: var(--panel); overflow: hidden; } +.reader-progress-fill { height: 100%; width: 0; background: var(--accent); transition: width .25s; } +.reader-progress-label { font-size: 12px; color: var(--subtext); white-space: nowrap; min-width: 56px; text-align: right; } +.reader-doc-title { font-size: 13px; font-weight: 700; color: var(--text); max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* Saved-books library */ +.reader-lib-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; } +.reader-lib-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; } +.reader-book { + position: relative; padding: 14px; border: 1px solid var(--border); border-radius: 10px; + background: var(--panel); cursor: pointer; transition: border-color .15s, transform .1s, box-shadow .15s; +} +.reader-book:hover { border-color: var(--accent); box-shadow: 0 4px 14px rgba(0,0,0,.08); transform: translateY(-1px); } +.reader-book-icon { font-size: 26px; color: var(--accent); line-height: 1; } +.reader-book-title { font-weight: 700; margin-top: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.reader-book-meta { font-size: 11px; color: var(--subtext); margin-top: 2px; } +.reader-book-bar { position: relative; height: 6px; border-radius: 4px; background: var(--border); margin-top: 10px; overflow: hidden; } +.reader-book-bar-synth { position: absolute; inset: 0 auto 0 0; background: rgba(34,197,94,.45); } +.reader-book-bar-read { position: absolute; inset: 0 auto 0 0; background: var(--accent); } +.reader-book-stats { display: flex; justify-content: space-between; gap: 6px; font-size: 10.5px; color: var(--subtext); margin-top: 5px; } +.reader-book-del { + position: absolute; top: 6px; right: 6px; border: none; background: rgba(0,0,0,.04); + color: var(--subtext); border-radius: 6px; width: 24px; height: 24px; cursor: pointer; opacity: 0; transition: opacity .15s; +} +.reader-book:hover .reader-book-del { opacity: 1; } +.reader-book-del:hover { color: var(--red, #ef4444); background: rgba(239,68,68,.1); } + +/* Synthesise-ahead bar + range selection */ +.reader-synthbar { + display: flex; align-items: center; flex-wrap: wrap; gap: 10px; + margin-top: 10px; padding: 8px 12px; background: var(--surface); + border: 1px solid var(--border); border-radius: var(--radius); +} +.reader-page-range { display: inline-flex; align-items: center; gap: 6px; } +.reader-page-inp { + width: 56px; padding: 4px 6px; border: 1px solid var(--border); + border-radius: 6px; background: var(--panel); color: var(--text); font-family: var(--font); +} +#reader-select-toggle.active { background: var(--accent); color: #fff; border-color: var(--accent); } +.reader-sel-info { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--accent); font-weight: 600; } +.reader-sel-clear { border: none; background: none; color: var(--subtext); cursor: pointer; font-size: 13px; padding: 0 2px; } +.reader-sel-clear:hover { color: var(--red, #ef4444); } +.reader-sel-hint { font-size: 12px; color: var(--subtext); } +.reader-sel-hint b { color: var(--accent); } +.reader-export { display: inline-flex; align-items: center; gap: 6px; } +#reader-audiobook-btn .mdi { color: var(--accent); } + +/* Audiobook live casting — floating, non-blocking, minimisable panel */ +.ab-castpanel { + position: fixed; right: 18px; bottom: 18px; z-index: 1000; + width: min(880px, 96vw); max-height: 82vh; display: flex; flex-direction: column; + background: var(--surface); border: 1px solid var(--border); border-radius: 12px; + box-shadow: 0 16px 48px rgba(0,0,0,.28); overflow: hidden; +} +.ab-castpanel-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); cursor: default; } +.ab-castpanel-head > .mdi { color: var(--accent); font-size: 18px; } +.ab-castpanel-title { font-weight: 800; font-size: 14px; } +.ab-castpanel-count { font-size: 12px; color: var(--subtext); } +.ab-castpanel-btn { border: none; background: none; color: var(--subtext); cursor: pointer; padding: 2px 5px; border-radius: 6px; line-height: 1; } +.ab-castpanel-btn:hover { background: var(--panel); color: var(--text); } +.ab-castpanel-bar { margin: 0; border-radius: 0; } +.ab-castpanel-foot { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-top: 1px solid var(--border); } +.ab-cv-done { font-size: 13px; font-weight: 700; display: inline-flex; align-items: center; gap: 6px; } +.ab-cv-done .mdi { color: #10b981; } +/* minimised → just the header chip in the corner */ +.ab-castpanel.minimized { width: auto; max-width: 320px; } +.ab-castpanel.minimized .ab-castpanel-bar, +.ab-castpanel.minimized .ab-cv-body, +.ab-castpanel.minimized .ab-castpanel-foot { display: none; } +.ab-castpanel.minimized .ab-castpanel-head { cursor: pointer; border-bottom: none; } + +.ab-cv-body { display: grid; grid-template-columns: 1fr 220px; gap: 12px; padding: 12px; } +.ab-cv-feed { height: 44vh; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; background: var(--panel); display: flex; flex-direction: column; gap: 3px; } +.ab-cv-row { display: grid; grid-template-columns: 130px 1fr; gap: 8px; font-size: 12.5px; padding: 2px 0; align-items: baseline; } +.ab-cv-row.is-narr { opacity: .6; } +.ab-cv-spk { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.ab-cv-row.is-narr .ab-cv-spk { color: var(--subtext); font-weight: 600; } +.ab-cv-txt { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ab-cv-note { font-size: 11.5px; color: var(--subtext); font-style: italic; padding: 3px 0 3px 4px; border-left: 2px solid var(--border); } +.ab-cv-side { border: 1px solid var(--border); border-radius: 8px; padding: 8px; background: var(--panel); overflow-y: auto; height: 44vh; } +.ab-cv-side-head { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; color: var(--subtext); margin-bottom: 6px; } +.ab-cv-chars { display: flex; flex-direction: column; gap: 5px; } +.ab-cv-empty { font-size: 12px; color: var(--subtext); font-style: italic; } +.ab-chip { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; padding: 3px 8px; border: 1px solid var(--border); border-radius: 20px; } +.ab-chip-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--c); flex-shrink: 0; } +.ab-chip b { margin-left: auto; color: var(--subtext); font-weight: 700; } +.audiobook-narr-note { color: var(--subtext); } +@media (max-width: 560px) { .ab-cv-body { grid-template-columns: 1fr; } .ab-cv-side { height: auto; max-height: 22vh; } } + +/* Audiobook casting overlay */ +.audiobook-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: flex; align-items: center; justify-content: center; z-index: 1000; } +.audiobook-box { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 22px 24px; width: min(420px, 90vw); box-shadow: 0 12px 40px rgba(0,0,0,.25); } +.audiobook-title { font-weight: 800; font-size: 15px; display: flex; align-items: center; gap: 8px; } +.audiobook-title .mdi { color: var(--accent); font-size: 20px; } +.audiobook-msg { color: var(--subtext); font-size: 13px; margin: 10px 0 12px; } +.audiobook-box .reader-synth-track { width: 100%; } +.audiobook-actions { margin-top: 14px; text-align: right; display: flex; gap: 8px; justify-content: flex-end; } +.audiobook-count { font-weight: 500; font-size: 12px; color: var(--subtext); margin-left: auto; } +.audiobook-preview-box { width: min(760px, 94vw); } +.audiobook-preview-box .audiobook-title { flex-wrap: wrap; } +.audiobook-seglist { max-height: 56vh; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 6px; margin-top: 6px; } +.audiobook-seg { display: grid; grid-template-columns: 130px 96px 1fr; gap: 8px; align-items: center; padding: 4px 6px; border-radius: 6px; } +.audiobook-seg:nth-child(odd) { background: var(--panel); } +.audiobook-seg.is-dialog { box-shadow: inset 3px 0 0 var(--accent); } +.audiobook-seg input { padding: 3px 6px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text); font-size: 12px; font-family: var(--font); min-width: 0; } +.audiobook-seg input:disabled { opacity: .4; } +.audiobook-seg-text { font-size: 12px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +#reh-tb-audiobook .mdi { color: var(--accent); } +#reader-charsheets-btn .mdi, #reh-charsheets-btn .mdi { color: var(--accent); } + +/* Character sheets overlay */ +.cs-box { width: min(900px, 95vw); max-height: 90vh; display: flex; flex-direction: column; } +.cs-titlebar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; } +.cs-list { overflow-y: auto; padding-right: 4px; display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 12px; align-content: start; } +.cs-group-label { grid-column: 1 / -1; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; color: var(--subtext); margin-top: 4px; } +.cs-card { border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; background: var(--panel); } +.cs-card.cs-main { box-shadow: inset 4px 0 0 var(--accent); } +.cs-card.cs-supp { box-shadow: inset 4px 0 0 var(--border); } +.cs-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; } +.cs-name { font-weight: 800; font-size: 15px; } +.cs-archetype { font-size: 12px; font-weight: 700; color: var(--accent); } +.cs-tier { margin-left: auto; font-size: 10px; text-transform: uppercase; letter-spacing: .04em; color: var(--subtext); border: 1px solid var(--border); border-radius: 20px; padding: 1px 8px; } +.cs-aliases { font-size: 12px; color: var(--subtext); font-style: italic; margin: 2px 0 6px; } +.cs-fields { margin: 0; display: flex; flex-direction: column; gap: 5px; } +.cs-row { display: grid; grid-template-columns: 120px 1fr; gap: 8px; font-size: 12.5px; } +.cs-row dt { font-weight: 700; color: var(--subtext); } +.cs-row dd { margin: 0; color: var(--text); } +.cs-row ul { margin: 0; padding-left: 16px; } +.cs-sources { margin-top: 8px; padding-top: 7px; border-top: 1px dashed var(--border); font-size: 11px; color: var(--subtext); } +.cs-sources b { color: var(--text); } +.cs-sources .mdi { color: var(--accent); } +.reader-synth-prog { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; } +.reader-synth-track { width: 130px; height: 6px; border-radius: 4px; background: var(--panel); overflow: hidden; } +.reader-synth-fill { height: 100%; width: 0; background: rgba(234,179,8,.9); transition: width .15s; } +.reader-synth-label { font-size: 12px; color: var(--subtext); min-width: 52px; } + +/* Select mode: clickable sentences (pointer, not crosshair — it's click-to-pick) */ +.reader-doc.reader-selecting { box-shadow: inset 0 0 0 2px var(--accent); } +.reader-doc.reader-selecting .reader-stat { pointer-events: auto; cursor: pointer; } +.reader-doc.reader-selecting .reader-stat:hover { background: rgba(37,99,235,.16); outline: 1px dashed rgba(37,99,235,.5); } +.reader-doc.reader-selecting .reader-word { cursor: pointer; } +.reader-doc.reader-selecting .reader-word:hover { background: rgba(37,99,235,.18); } +/* committed range */ +.reader-stat.sel { outline: 2px solid var(--accent); outline-offset: 0; background: rgba(37,99,235,.10); } +.reader-word.in-sel { box-shadow: inset 0 -2px 0 var(--accent); } +/* the start "anchor" pulses while waiting for the end click */ +.reader-stat.sel-anchor { outline: 2px solid var(--accent); background: rgba(37,99,235,.20); animation: readerAnchorPulse 1s ease-in-out infinite; } +.reader-word.sel-anchor { box-shadow: inset 0 -2px 0 var(--accent); background: rgba(37,99,235,.22); animation: readerAnchorPulse 1s ease-in-out infinite; } +@keyframes readerAnchorPulse { 0%,100% { opacity: 1; } 50% { opacity: .45; } } + +/* Zoom toolbar + status legend */ +.reader-toolbar { + display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; + margin-top: 10px; padding: 8px 12px; background: var(--surface); + border: 1px solid var(--border); border-radius: var(--radius); +} +.reader-zoom { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } +.reader-zoom-sep { width: 1px; height: 20px; background: var(--border); margin: 0 4px; } +.reader-zoom-pct { font-size: 12px; font-weight: 700; min-width: 44px; text-align: center; color: var(--subtext); } +.reader-legend { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; font-size: 11px; color: var(--subtext); } +.reader-leg { display: inline-flex; align-items: center; gap: 5px; } +.reader-leg::before { content: ''; width: 11px; height: 11px; border-radius: 3px; } +.reader-leg-pending::before { background: rgba(239,68,68,.45); } +.reader-leg-synth::before { background: rgba(234,179,8,.65); } +.reader-leg-ready::before { background: rgba(34,197,94,.55); } +.reader-leg-reading::before { background: rgba(37,99,235,.6); } + +.reader-doc { + margin-top: 14px; max-height: 72vh; overflow-y: auto; + background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); + padding: 16px; display: flex; flex-direction: column; align-items: center; gap: 16px; +} +.reader-doc:empty { display: none; } +.reader-parsing { display: flex; align-items: center; gap: 10px; padding: 24px; color: var(--subtext); font-size: 13px; } +.reader-doc.two-page { flex-direction: row; flex-wrap: wrap; justify-content: center; align-items: flex-start; } +.reader-page { + position: relative; flex: 0 0 auto; + box-shadow: 0 2px 10px rgba(0,0,0,.12); background: #fff; +} +.reader-canvas { display: block; } +.reader-overlay { position: absolute; inset: 0; pointer-events: none; } +.reader-hl-word { + position: absolute; border-radius: 3px; background: rgba(37,99,235,.30); z-index: 3; + box-shadow: 0 0 0 1.5px rgba(37,99,235,.75); transition: left .12s, top .12s, width .12s, height .12s; +} +/* Per-sentence synthesis-state overlay (PDF) */ +.reader-stat { position: absolute; border-radius: 3px; z-index: 1; transition: background .2s; } +.reader-stat-pending { background: rgba(239,68,68,.12); } +.reader-stat-synth { background: rgba(234,179,8,.30); } +.reader-stat-ready { background: rgba(34,197,94,.18); } +.reader-stat-reading { background: rgba(37,99,235,.20); z-index: 2; } + +.reader-text { + max-width: 760px; background: var(--surface); color: var(--text); + border: 1px solid var(--border); border-radius: var(--radius); + padding: 28px 34px; line-height: 1.85; font-size: 17px; +} +.reader-text-para { margin: 0 0 1em; } +.reader-word { border-radius: 3px; padding: 0 1px; cursor: pointer; transition: background .12s; } +/* Per-sentence synthesis-state tint (text) */ +.reader-word.stat-pending { background: rgba(239,68,68,.09); } +.reader-word.stat-synth { background: rgba(234,179,8,.22); } +.reader-word.stat-ready { background: rgba(34,197,94,.14); } +.reader-word.stat-reading { background: rgba(37,99,235,.15); } +.reader-word.is-word { background: rgba(37,99,235,.42) !important; box-shadow: 0 0 0 1px rgba(37,99,235,.6); }