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 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-26 21:44:30 +02:00
parent 495550bf6a
commit e62abbcd1d
8 changed files with 141 additions and 20 deletions

View File

@ -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 ## [1.9.0] — 2026-06-26
### Added ### Added

View File

@ -1 +1 @@
1.9.0 1.9.1

View File

@ -26,7 +26,7 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── --> <!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css"> <link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.9.0-1"> <link rel="stylesheet" href="/static/style.css?v=1.9.1-3">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── --> <!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -308,7 +308,7 @@
<script src="/static/vendor/wavesurfer-regions.min.js"></script> <script src="/static/vendor/wavesurfer-regions.min.js"></script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton --> <!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.9.0-1"></script> <script src="/static/loader.js?v=1.9.1-3"></script>
</body> </body>
</html> </html>

View File

@ -131,6 +131,26 @@ function audiobookProgress(total) {
const _AB_PALETTE = ['#3b82f6', '#10b981', '#8b5cf6', '#f59e0b', '#ef4444', '#ec4899', '#06b6d4', '#84cc16', '#f97316', '#14b8a6', '#6366f1', '#d946ef']; 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 => `<span style="border-bottom: 2px solid ${color}; font-weight: 600;">${m}</span>`);
});
return html;
}
// Live casting view: a scrolling feed of attributed lines + a character roster // 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. // that fills up as speakers are discovered. Far clearer than a bare bar.
function audiobookCastView(total, llmUrl, defaultModel, isIdle = false) { function audiobookCastView(total, llmUrl, defaultModel, isIdle = false) {

View File

@ -7,7 +7,18 @@
const CS_CHUNK_CHARS = 4000; const CS_CHUNK_CHARS = 4000;
const _cs = { running: false, cancel: false, cache: {} }; 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 csLlmModel() { return $('reh-llm-model')?.value || ''; }
function csLang() { return $('reh-design-lang')?.value || ''; } function csLang() { return $('reh-design-lang')?.value || ''; }

View File

@ -2129,6 +2129,48 @@ $('reh-start-btn')?.addEventListener('click', () => {
highlightCurrentLine(); 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 ───────────────────────────────────────────────────────── // ── A4 Script page ─────────────────────────────────────────────────────────
function buildScriptPage() { function buildScriptPage() {
@ -2138,7 +2180,8 @@ function buildScriptPage() {
// Cast strip // Cast strip
const castStrip = $('reh-cast-strip'); const castStrip = $('reh-cast-strip');
if (castStrip) { 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'; const c = rehState.cast[sp], isMe = c.voice === 'me';
return `<div class="reh-strip-char" title="${escHtml(sp)}${isMe?'me':(c.voice||'no voice')}"> return `<div class="reh-strip-char" title="${escHtml(sp)}${isMe?'me':(c.voice||'no voice')}">
${voiceAvatarHtml(isMe?'':c.voice, c.color, 28)} ${voiceAvatarHtml(isMe?'':c.voice, c.color, 28)}
@ -2146,7 +2189,11 @@ function buildScriptPage() {
${isMe?'<span class="mdi mdi-microphone" style="font-size:10px;color:var(--subtext)"></span>':''} ${isMe?'<span class="mdi mdi-microphone" style="font-size:10px;color:var(--subtext)"></span>':''}
</div>`; </div>`;
}).join(''); }).join('');
const lbl = $('reh-cast-toggle-label');
if (lbl) lbl.textContent = `Characters (${names.length})`;
} }
rehApplyStageFont();
rehApplyCastCollapsed();
const linesEl = $('reh-script-lines'); if (!linesEl) return; const linesEl = $('reh-script-lines'); if (!linesEl) return;

View File

@ -291,7 +291,14 @@
<!-- Director's console --> <!-- Director's console -->
<div class="reh-console" id="reh-transport-bar"> <div class="reh-console" id="reh-transport-bar">
<div class="reh-cast-row" id="reh-cast-row">
<button class="reh-cast-toggle" id="reh-cast-toggle" title="Show or hide the character list" aria-expanded="true">
<span class="mdi mdi-account-group"></span>
<span id="reh-cast-toggle-label">Characters</span>
<span class="mdi mdi-chevron-down reh-cast-chev"></span>
</button>
<div class="reh-cast-strip" id="reh-cast-strip"></div> <div class="reh-cast-strip" id="reh-cast-strip"></div>
</div>
<div class="reh-console-transport"> <div class="reh-console-transport">
<button class="reh-tb-btn" id="reh-tb-prev" title="Previous line"><span class="mdi mdi-skip-previous"></span></button> <button class="reh-tb-btn" id="reh-tb-prev" title="Previous line"><span class="mdi mdi-skip-previous"></span></button>
@ -333,6 +340,10 @@
<button class="btn-secondary btn-sm" id="reh-tb-save" title="Save to library"><span class="mdi mdi-content-save-outline"></span> Save</button> <button class="btn-secondary btn-sm" id="reh-tb-save" title="Save to library"><span class="mdi mdi-content-save-outline"></span> Save</button>
<button class="btn-secondary btn-sm" id="reh-tb-export" title="Export .reh"><span class="mdi mdi-export"></span></button> <button class="btn-secondary btn-sm" id="reh-tb-export" title="Export .reh"><span class="mdi mdi-export"></span></button>
<button class="btn-secondary btn-sm" id="reh-fountain-btn" title="Export as .fountain (plain text)"><span class="mdi mdi-file-document-outline"></span></button> <button class="btn-secondary btn-sm" id="reh-fountain-btn" title="Export as .fountain (plain text)"><span class="mdi mdi-file-document-outline"></span></button>
<span class="reh-fontsize-ctl" title="Adjust the play's text size">
<button class="btn-secondary btn-sm reh-fontsize-btn" id="reh-font-dec" title="Smaller text" aria-label="Decrease font size">A<span class="reh-fontsize-minus"></span></button>
<button class="btn-secondary btn-sm reh-fontsize-btn" id="reh-font-inc" title="Larger text" aria-label="Increase font size">A<span class="reh-fontsize-plus">+</span></button>
</span>
<button class="btn-secondary btn-sm" id="reh-page-mode-btn" title="Switch between A4 pages / scroll / PDF pages"><span class="mdi mdi-file-document-outline"></span> A4 pages</button> <button class="btn-secondary btn-sm" id="reh-page-mode-btn" title="Switch between A4 pages / scroll / PDF pages"><span class="mdi mdi-file-document-outline"></span> A4 pages</button>
<button class="btn-secondary btn-sm" id="reh-train-open-btn" title="Train mode — practice your lines with cue playback &amp; speech recognition"><span class="mdi mdi-school-outline"></span> Train</button> <button class="btn-secondary btn-sm" id="reh-train-open-btn" title="Train mode — practice your lines with cue playback &amp; speech recognition"><span class="mdi mdi-school-outline"></span> Train</button>
<button class="btn-secondary btn-sm" id="reh-exit-btn">Exit</button> <button class="btn-secondary btn-sm" id="reh-exit-btn">Exit</button>

View File

@ -3537,42 +3537,46 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
/* ── Proper screenplay A4 formatting ─────────────────────────────────── */ /* ── Proper screenplay A4 formatting ─────────────────────────────────── */
/* Use Courier New throughout — 1 page ≈ 1 minute of screen time */ /* 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 { .reh-a4-page {
font-family: 'Courier New', Courier, monospace !important; font-family: 'Courier New', Courier, monospace !important;
font-size: 12pt; font-size: calc(12pt * var(--reh-stage-scale, 1));
line-height: 1.6; line-height: 1.6;
} }
.reh-page-title { .reh-page-title {
font-family: 'Courier New', Courier, monospace; font-family: 'Courier New', Courier, monospace;
font-size: 14pt; font-size: 1.17em;
} }
/* Scene headings: all-caps, bold, with spacing */ /* Scene headings: all-caps, bold, with spacing */
.reh-scene { .reh-scene {
font-size: 12pt; font-size: 1em;
letter-spacing: .04em; letter-spacing: .04em;
} }
/* Action text: normal weight, left-aligned */ /* Narrator / action text and spoken dialog share ONE size (1em) so the play
.reh-action-block { font-size: 12pt; } 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 */ /* Act headings: larger, extra top space */
.reh-act { font-size: 13pt; } .reh-act { font-size: 1.08em; }
/* Transitions: right-aligned */ /* Transitions: right-aligned */
.reh-transition { font-size: 12pt; } .reh-transition { font-size: 1em; }
/* Dialog block — screenplay format */ /* Dialog block — screenplay format */
.reh-block-name { .reh-block-name {
font-size: 11pt; font-size: 0.92em;
letter-spacing: .1em; letter-spacing: .1em;
} }
.reh-block-dialog { .reh-block-dialog {
font-size: 12pt; font-size: 1em;
padding-left: 32px; /* ~1.5" dialog indent */ padding-left: 32px; /* ~1.5" dialog indent */
max-width: 440px; /* ~3.5" dialog column width */ max-width: 440px; /* ~3.5" dialog column width */
} }
/* Parenthetical directions: centered-ish */ /* Parenthetical directions: centered-ish */
.reh-direction { .reh-direction {
font-size: 11pt; font-size: 0.92em;
padding-left: 80px; padding-left: 80px;
color: #444; 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 */ /* 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; } .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 */ /* Act heading on A4 page */
.reh-act { .reh-act {
font-family: 'Courier New', Courier, monospace; font-size: 13px; font-weight: 700; 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; } .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 { .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; color: #2c2c3e; padding: 2px 0 10px; cursor: pointer;
} }
/* Transition line */ /* Transition line */
.reh-transition { .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; text-align: right; text-transform: uppercase; letter-spacing: .1em;
color: #555; padding: 8px 0; color: #555; padding: 8px 0;
} }
@ -3944,7 +3963,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.reh-paper { .reh-paper {
width: min(794px, 100%); background: #fff; color: #1a1a2e; width: min(794px, 100%); background: #fff; color: #1a1a2e;
padding: 56px 72px 40px; box-shadow: 0 4px 24px rgba(0,0,0,.18); 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; border-radius: 2px; position: relative; box-sizing: border-box;
min-height: min(1123px, calc((100vw - 32px) * 1.414)); min-height: min(1123px, calc((100vw - 32px) * 1.414));
} }