From a978e39e49204f39a16a84dcd8d27807c7c12031 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 26 Jun 2026 22:23:19 +0200 Subject: [PATCH] feat: expandable LLM panel + page markers in casting feed; fix page numbering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Casting view improvements: - "LLM Reading…" row now shows a preview of the passage being processed. A chevron button expands it to a full scrollable view of the passage text (up to 260px), so you can follow what the LLM is reading live. - Page-break dividers ("Page N") appear in the casting feed whenever the source PDF page changes, giving a real-time view of page boundaries. Bug fix: - Page numbers in the Rehearser script were 0-indexed (PDF-internal) so the first page break showed "Page 1" for what was PDF page 2, etc. Now always emits 1-indexed page numbers (\f${page+1}). Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 11 +++++++++++ VERSION | 2 +- static/index.html | 4 ++-- static/js/audiobook.js | 41 +++++++++++++++++++++++++++++++++++++---- static/style.css | 37 ++++++++++++++++++++++++++++--------- 5 files changed, 79 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b597fa1..2b53241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.9.5] — 2026-06-26 + +### Added +- **Casting view — page-break lines**: as the LLM processes a PDF audiobook, a **"Page N"** divider row now appears in the casting feed whenever the source PDF page changes. This gives a live view of where each page boundary falls within the script. +- **Casting view — expandable LLM passage panel**: the "LLM Reading…" indicator now shows a preview of the passage being processed. A **chevron button** (▾/▴) expands it into a full scrollable view of the passage text so you can follow exactly what the LLM is reading and thinking about. + +### Fixed +- **Page numbers were off by one**: page-break markers emitted into the Rehearser script now use the correct 1-indexed PDF page number (was storing 0-indexed, so "Page 1" showed for what was actually the second PDF page). + +--- + ## [1.9.4] — 2026-06-26 ### Changed diff --git a/VERSION b/VERSION index d615fd0..158c747 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.4 +1.9.5 diff --git a/static/index.html b/static/index.html index bb4829a..c17ec8d 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 2612c67..4ea9ad3 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -772,8 +772,25 @@ STRIKTE FORMAT- UND TEXTREGELN: processing(text) { if (this._procRow) this._procRow.remove(); this._procRow = document.createElement('div'); - this._procRow.className = 'ab-cv-row is-processing'; - this._procRow.innerHTML = ` LLM Reading…${escHtml(text.slice(0, 200))}${text.length > 200 ? '…' : ''}`; + this._procRow.className = 'ab-cv-row is-processing ab-cv-llm-row'; + const isLong = text.length > 160; + const preview = escHtml(text.slice(0, 160)) + (isLong ? '…' : ''); + this._procRow.innerHTML = ` +
+ LLM Reading… + ${isLong ? '' : ''} +
+
${preview}
+ ${isLong ? `` : ''} + `; + if (isLong) { + this._procRow.querySelector('.ab-cv-expand-btn').addEventListener('click', function () { + const full = this.closest('.ab-cv-llm-row').querySelector('.ab-cv-llm-full'); + full.hidden = !full.hidden; + const icon = this.querySelector('span'); + if (icon) icon.className = full.hidden ? 'mdi mdi-chevron-down' : 'mdi mdi-chevron-up'; + }); + } feed.appendChild(this._procRow); trim(); }, @@ -807,6 +824,13 @@ STRIKTE FORMAT- UND TEXTREGELN: r.title = 'There is more text before and after this passage'; feed.appendChild(r); trim(); }, + // Page-break marker in the casting feed (from source PDF page boundaries). + pagemark(pageNum) { + const r = document.createElement('div'); + r.className = 'ab-cv-pagemark'; + r.innerHTML = ` Page ${pageNum}`; + feed.appendChild(r); trim(); + }, // Park the panel in a "done" state with a Review button instead of auto-popping // the preview — so it waits for you if you wandered off to do something else. complete(summary, onOpen, onRecast, onRecastUnknown) { @@ -1061,10 +1085,20 @@ async function audiobookCast(overrideUrl, overrideModel) { const roster = []; let narrationOnly = 0; // passages with no quotes at all — legitimately all narration let degraded = 0; // passages with dialogue the LLM couldn't analyse → quotes auto-extracted + // Page-mark tracking for visual page-break rows in the casting feed. + const _pgMarks = (_audiobook.pageMarks || []).slice(); + if (_pgMarks.length && _pgMarks[0].offset <= 2) _pgMarks.shift(); // skip page-1 mark at pos 0 + let _pgMarkIdx = 0, _pgCharPos = 0; try { for (let i = 0; i < chunks.length; i++) { if (_audiobook.cancel) break; view.update(i); + // Insert page-break markers in the feed when the source PDF page changes. + while (_pgMarkIdx < _pgMarks.length && _pgCharPos >= _pgMarks[_pgMarkIdx].offset) { + view.pagemark(_pgMarks[_pgMarkIdx].page + 1); + _pgMarkIdx++; + } + _pgCharPos += chunks[i].length + 1; // +1 for joining space between chunks // No quotation marks anywhere → pure narration; skip the LLM entirely (faster, not an error) if (!audiobookHasDialogue(chunks[i])) { const seg = { speaker: 'Narrator', type: 'narration', text: chunks[i], emotion: '' }; @@ -1292,8 +1326,7 @@ function audiobookBuildScript(segments) { if (at >= 0) searchPos = at + probe.length; while (markIdx < marks.length && pos >= marks[markIdx].offset) { if (script.trim()) { - const pn = marks[markIdx].page; - script += pn ? `\n\f${pn}\n` : '\n\f\n'; // page break with page number + script += `\n\f${marks[markIdx].page + 1}\n`; // page break — 1-indexed page number } markIdx++; } diff --git a/static/style.css b/static/style.css index abe7100..cec150a 100644 --- a/static/style.css +++ b/static/style.css @@ -4202,11 +4202,10 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami } .ab-cv-row.is-processing { font-family: inherit; - display: flex; - align-items: center; - gap: 8px; + display: block; padding: 4px 8px; } +.ab-cv-llm-head { display: flex; align-items: center; gap: 6px; } .ab-cv-row.is-processing .ab-cv-spk { display: inline-block; padding-left: 0; @@ -4215,13 +4214,33 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami font-size: 12px; margin: 0; } -.ab-cv-row.is-processing .ab-cv-txt { - display: inline-block; - padding-left: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +.ab-cv-expand-btn { + border: none; background: none; cursor: pointer; + color: var(--subtext); padding: 0 2px; line-height: 1; + font-size: 15px; border-radius: 4px; } +.ab-cv-expand-btn:hover { color: var(--text); background: var(--panel); } +.ab-cv-llm-preview { + font-size: 12px; color: var(--subtext); font-style: italic; + padding: 2px 0 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.ab-cv-llm-full { margin-top: 6px; } +.ab-cv-llm-pre { + font-family: inherit; font-size: 12px; color: var(--text); + white-space: pre-wrap; word-break: break-word; + background: var(--bg); border: 1px solid var(--border); + border-radius: 6px; padding: 8px 10px; margin: 0; + max-height: 260px; overflow-y: auto; +} +.ab-cv-pagemark { + display: flex; align-items: center; gap: 6px; + font-size: 11px; font-weight: 700; letter-spacing: .06em; + color: var(--subtext); text-transform: uppercase; + padding: 6px 4px 2px; + border-top: 1px dashed var(--border); + margin: 4px 0 2px; +} +.ab-cv-pagemark .mdi { font-size: 13px; opacity: .7; } .ab-cv-txt { color: var(--text); white-space: pre-wrap; word-wrap: break-word; } .ab-cv-note { font-size: 11.5px; color: var(--subtext); font-style: italic; padding: 3px 0 3px 4px; border-left: 2px solid var(--border); font-family: sans-serif; } .ab-cv-side { border: 1px solid var(--border); border-radius: 8px; padding: 8px; background: var(--panel); overflow-y: auto; min-height: 0; }