From e62abbcd1dc486cf1110181de5f5498e9beee9c5 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 26 Jun 2026 21:44:30 +0200 Subject: [PATCH] Fix character-sheet LLM URL + audiobook preview; Stage font controls (v1.9.1) - Character sheets: stop sending the dead localhost:11434 default; fall back to the server's configured llm_url so extraction uses the same working LLM. - Audiobook casting: hoist highlightText to module scope so the "Review & cast" manual-correction preview renders its segment rows again. - Stage: unify narrator/dialog font size, add A-/A+ play text-size control (scales A4 + paginated views), and make the cast chip list collapsible. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 13 +++++++++ VERSION | 2 +- static/index.html | 4 +-- static/js/audiobook.js | 20 +++++++++++++ static/js/character-sheets.js | 13 ++++++++- static/js/rehearser.js | 49 +++++++++++++++++++++++++++++++- static/sections/s-rehearser.html | 13 ++++++++- static/style.css | 47 +++++++++++++++++++++--------- 8 files changed, 141 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac48fbd..a1bb505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,19 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.9.1] — 2026-06-26 + +### Fixed +- **Character sheets — "Connection refused" failures**: the analysis could send a dead `localhost:11434` LLM URL to the server (when app settings hadn't loaded yet), causing every passage to fail. It now never falls back to that hard-coded default — when no endpoint is explicitly chosen it lets the **server use its own configured `llm_url`**, so character-sheet extraction uses the same working LLM as the rest of the app. +- **Audiobook casting — empty "Review & cast" preview**: opening the manual-correction preview after a cast rendered no lines because the name-highlighter (`highlightText`) was scoped to the live cast view only. It is now shared, so you can review, fix speakers/emotions, and open in the Rehearser again. + +### Changed +- **Script Rehearser / Stage — one text size**: narrator (action) text and spoken dialogue now use the **same font size** instead of mismatched 15px/16px, so the play reads evenly. +- **Script Rehearser / Stage — text-size control**: a new **A− / A+** control in the Stage toolbar shrinks or enlarges the whole play (persists per browser), in both A4 and paginated/scroll views. +- **Script Rehearser / Stage — collapsible character list**: the row of cast chips can now be **collapsed or expanded** via a "Characters (N)" toggle to free up vertical space. + +--- + ## [1.9.0] — 2026-06-26 ### Added diff --git a/VERSION b/VERSION index f8e233b..9ab8337 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.0 +1.9.1 diff --git a/static/index.html b/static/index.html index 28fdf3a..2a4b3a2 100644 --- a/static/index.html +++ b/static/index.html @@ -26,7 +26,7 @@ - + @@ -308,7 +308,7 @@ - + diff --git a/static/js/audiobook.js b/static/js/audiobook.js index e468b15..7754f1a 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -131,6 +131,26 @@ function audiobookProgress(total) { const _AB_PALETTE = ['#3b82f6', '#10b981', '#8b5cf6', '#f59e0b', '#ef4444', '#ec4899', '#06b6d4', '#84cc16', '#f97316', '#14b8a6', '#6366f1', '#d946ef']; +// Escape a segment's text and underline any known character names. Module-level +// so the review/preview overlay (audiobookShowPreview) can use it too — the cast +// view (audiobookCastView) defines its own roster-coloured version that shadows +// this inside its closure. Names default to the current run's roster. +function highlightText(text, names) { + if (!text) return ''; + let html = escHtml(text); + const list = (names || (_audiobook && _audiobook.roster) || []) + .filter(n => n && n.toLowerCase() !== 'narrator' && !/^Unknown|Unbekannt/i.test(n)) + .sort((a, b) => b.length - a.length); + list.forEach((name, idx) => { + if (name.length < 2) return; + const safe = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const color = _AB_PALETTE[idx % _AB_PALETTE.length]; + html = html.replace(new RegExp(`\\b(${safe})\\b`, 'gi'), + m => `${m}`); + }); + return html; +} + // Live casting view: a scrolling feed of attributed lines + a character roster // that fills up as speakers are discovered. Far clearer than a bare bar. function audiobookCastView(total, llmUrl, defaultModel, isIdle = false) { diff --git a/static/js/character-sheets.js b/static/js/character-sheets.js index 039ff02..e15dcee 100644 --- a/static/js/character-sheets.js +++ b/static/js/character-sheets.js @@ -7,7 +7,18 @@ const CS_CHUNK_CHARS = 4000; const _cs = { running: false, cancel: false, cache: {} }; -function csLlmUrl() { return $('reh-llm-url')?.value.trim() || (typeof rehDefaultLlmUrl === 'function' ? rehDefaultLlmUrl() : ''); } +// Resolve the LLM endpoint. Prefer an explicitly-typed rehearser URL, then the +// app's loaded settings. Crucially, NEVER fall back to the hardcoded +// localhost:11434 default — if settings aren't loaded yet, return '' so the +// SERVER uses its own configured llm_url instead of a dead localhost address +// (which caused "Connection refused" character-sheet failures). +function csLlmUrl() { + const explicit = $('reh-llm-url')?.value.trim(); + if (explicit) return explicit; + const fromSettings = (typeof _appSettings !== 'undefined' && _appSettings && _appSettings.llm_url) ? _appSettings.llm_url : ''; + if (fromSettings && !/localhost:11434|127\.0\.0\.1:11434/.test(fromSettings)) return fromSettings; + return ''; +} function csLlmModel() { return $('reh-llm-model')?.value || ''; } function csLang() { return $('reh-design-lang')?.value || ''; } diff --git a/static/js/rehearser.js b/static/js/rehearser.js index e004357..87e3f8b 100644 --- a/static/js/rehearser.js +++ b/static/js/rehearser.js @@ -2129,6 +2129,48 @@ $('reh-start-btn')?.addEventListener('click', () => { highlightCurrentLine(); }); +// ── Stage display controls (font size + collapsible cast) ─────────────────── + +const REH_STAGE_FONT_KEY = 'ttsvc_reh_stage_scale'; +const REH_CAST_COLLAPSED_KEY = 'ttsvc_reh_cast_collapsed'; + +function rehStageScale() { + const v = parseFloat(localStorage.getItem(REH_STAGE_FONT_KEY) || '1'); + return isNaN(v) ? 1 : Math.max(0.7, Math.min(2, v)); +} +function rehApplyStageFont() { + // Set on the page wrapper so the scale inherits to BOTH the single A4 page + // (#reh-a4-page) and the paginated `.reh-paper` pages built by page-mode. + const scale = String(rehStageScale()); + const wrap = document.querySelector('.reh-page-wrap'); + if (wrap) wrap.style.setProperty('--reh-stage-scale', scale); + const page = $('reh-a4-page'); + if (page) page.style.setProperty('--reh-stage-scale', scale); +} +function rehStageFontStep(delta) { + const next = Math.max(0.7, Math.min(2, Math.round((rehStageScale() + delta) * 100) / 100)); + localStorage.setItem(REH_STAGE_FONT_KEY, String(next)); + rehApplyStageFont(); +} + +function rehApplyCastCollapsed() { + const row = $('reh-cast-row'); + if (!row) return; + const collapsed = localStorage.getItem(REH_CAST_COLLAPSED_KEY) === '1'; + row.classList.toggle('reh-cast-collapsed', collapsed); + const btn = $('reh-cast-toggle'); + if (btn) btn.setAttribute('aria-expanded', String(!collapsed)); +} +function rehToggleCast() { + const collapsed = localStorage.getItem(REH_CAST_COLLAPSED_KEY) === '1'; + localStorage.setItem(REH_CAST_COLLAPSED_KEY, collapsed ? '0' : '1'); + rehApplyCastCollapsed(); +} + +$('reh-font-inc')?.addEventListener('click', () => rehStageFontStep(0.1)); +$('reh-font-dec')?.addEventListener('click', () => rehStageFontStep(-0.1)); +$('reh-cast-toggle')?.addEventListener('click', rehToggleCast); + // ── A4 Script page ───────────────────────────────────────────────────────── function buildScriptPage() { @@ -2138,7 +2180,8 @@ function buildScriptPage() { // Cast strip const castStrip = $('reh-cast-strip'); if (castStrip) { - castStrip.innerHTML = Object.keys(rehState.cast).map(sp => { + const names = Object.keys(rehState.cast); + castStrip.innerHTML = names.map(sp => { const c = rehState.cast[sp], isMe = c.voice === 'me'; return `
${voiceAvatarHtml(isMe?'':c.voice, c.color, 28)} @@ -2146,7 +2189,11 @@ function buildScriptPage() { ${isMe?'':''}
`; }).join(''); + const lbl = $('reh-cast-toggle-label'); + if (lbl) lbl.textContent = `Characters (${names.length})`; } + rehApplyStageFont(); + rehApplyCastCollapsed(); const linesEl = $('reh-script-lines'); if (!linesEl) return; diff --git a/static/sections/s-rehearser.html b/static/sections/s-rehearser.html index 498dead..4e7fe0d 100644 --- a/static/sections/s-rehearser.html +++ b/static/sections/s-rehearser.html @@ -291,7 +291,14 @@
-
+
+ +
+
@@ -333,6 +340,10 @@ + + + + diff --git a/static/style.css b/static/style.css index ea9b6c3..abe7100 100644 --- a/static/style.css +++ b/static/style.css @@ -3537,42 +3537,46 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami /* ── Proper screenplay A4 formatting ─────────────────────────────────── */ /* Use Courier New throughout — 1 page ≈ 1 minute of screen time */ +/* Base play font size. Everything below is expressed in `em` so the whole + play scales together via --reh-stage-scale (the +A / -A control). */ .reh-a4-page { font-family: 'Courier New', Courier, monospace !important; - font-size: 12pt; + font-size: calc(12pt * var(--reh-stage-scale, 1)); line-height: 1.6; } .reh-page-title { font-family: 'Courier New', Courier, monospace; - font-size: 14pt; + font-size: 1.17em; } /* Scene headings: all-caps, bold, with spacing */ .reh-scene { - font-size: 12pt; + font-size: 1em; letter-spacing: .04em; } -/* Action text: normal weight, left-aligned */ -.reh-action-block { font-size: 12pt; } +/* Narrator / action text and spoken dialog share ONE size (1em) so the play + reads evenly — no narrator-vs-dialogue size mismatch. */ +.reh-action-block { font-size: 1em; } +.reh-action-text { font-size: 1em; } /* Act headings: larger, extra top space */ -.reh-act { font-size: 13pt; } +.reh-act { font-size: 1.08em; } /* Transitions: right-aligned */ -.reh-transition { font-size: 12pt; } +.reh-transition { font-size: 1em; } /* Dialog block — screenplay format */ .reh-block-name { - font-size: 11pt; + font-size: 0.92em; letter-spacing: .1em; } .reh-block-dialog { - font-size: 12pt; + font-size: 1em; padding-left: 32px; /* ~1.5" dialog indent */ max-width: 440px; /* ~3.5" dialog column width */ } /* Parenthetical directions: centered-ish */ .reh-direction { - font-size: 11pt; + font-size: 0.92em; padding-left: 80px; color: #444; } @@ -3664,6 +3668,20 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami /* Cast strip takes its own full row inside the console */ .reh-console .reh-cast-strip { flex-basis: 100%; order: -1; padding-bottom: 5px; border-bottom: 1px solid var(--border); margin-bottom: 0; } +/* Collapsible character list (Stage) */ +.reh-console .reh-cast-row { flex-basis: 100%; order: -1; display: flex; align-items: flex-start; gap: 10px; padding-bottom: 5px; border-bottom: 1px solid var(--border); } +.reh-console .reh-cast-row .reh-cast-strip { flex-basis: auto; order: 0; flex: 1; padding-bottom: 0; border-bottom: none; min-width: 0; } +.reh-cast-toggle { flex-shrink: 0; display: inline-flex; align-items: center; gap: 5px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 16px; background: var(--panel); color: var(--text); font-size: 11px; font-weight: 700; cursor: pointer; white-space: nowrap; height: 28px; } +.reh-cast-toggle:hover { background: var(--border); } +.reh-cast-toggle .reh-cast-chev { transition: transform .2s; font-size: 14px; } +.reh-cast-row.reh-cast-collapsed .reh-cast-strip { display: none; } +.reh-cast-row.reh-cast-collapsed .reh-cast-chev { transform: rotate(-90deg); } + +/* Play font-size stepper (+A / -A) */ +.reh-fontsize-ctl { display: inline-flex; gap: 2px; } +.reh-fontsize-btn { font-weight: 700; padding: 2px 7px; } +.reh-fontsize-btn .reh-fontsize-plus, .reh-fontsize-btn .reh-fontsize-minus { font-size: 9px; vertical-align: super; margin-left: 1px; } + /* Act heading on A4 page */ .reh-act { font-family: 'Courier New', Courier, monospace; font-size: 13px; font-weight: 700; @@ -3695,15 +3713,16 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami } .reh-scene .mdi { font-size: 11px; opacity: .5; flex-shrink: 0; } -/* Action / description block */ +/* Action / description block — 1em so narrator matches spoken dialog and + scales with the +A / -A control. */ .reh-action-block { - font-family: 'Courier New', Courier, monospace; font-size: 15px; line-height: 1.75; + font-family: 'Courier New', Courier, monospace; font-size: 1em; line-height: 1.75; color: #2c2c3e; padding: 2px 0 10px; cursor: pointer; } /* Transition line */ .reh-transition { - font-family: 'Courier New', Courier, monospace; font-size: 11.5px; font-weight: 700; + font-family: 'Courier New', Courier, monospace; font-size: 0.75em; font-weight: 700; text-align: right; text-transform: uppercase; letter-spacing: .1em; color: #555; padding: 8px 0; } @@ -3944,7 +3963,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .reh-paper { width: min(794px, 100%); background: #fff; color: #1a1a2e; padding: 56px 72px 40px; box-shadow: 0 4px 24px rgba(0,0,0,.18); - font-family: 'Courier New', Courier, monospace; font-size: 12pt; line-height: 1.6; + font-family: 'Courier New', Courier, monospace; font-size: calc(12pt * var(--reh-stage-scale, 1)); line-height: 1.6; border-radius: 2px; position: relative; box-sizing: border-box; min-height: min(1123px, calc((100vw - 32px) * 1.414)); }