feat: expandable LLM panel + page markers in casting feed; fix page numbering
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 <noreply@anthropic.com>
This commit is contained in:
parent
868ce6a7bd
commit
a978e39e49
11
CHANGELOG.md
11
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
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.9.4">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.9.5">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -308,7 +308,7 @@
|
||||
<script src="/static/vendor/wavesurfer-regions.min.js"></script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.9.4"></script>
|
||||
<script src="/static/loader.js?v=1.9.5"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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 = `<span class="ab-cv-spk" style="color:var(--blue)"><span class="mdi mdi-loading mdi-spin"></span> LLM Reading…</span><span class="ab-cv-txt" style="opacity:0.6; font-style:italic;">${escHtml(text.slice(0, 200))}${text.length > 200 ? '…' : ''}</span>`;
|
||||
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 = `
|
||||
<div class="ab-cv-llm-head">
|
||||
<span class="ab-cv-spk" style="color:var(--blue)"><span class="mdi mdi-loading mdi-spin"></span> LLM Reading…</span>
|
||||
${isLong ? '<button class="ab-cv-expand-btn" title="Show full passage"><span class="mdi mdi-chevron-down"></span></button>' : ''}
|
||||
</div>
|
||||
<div class="ab-cv-llm-preview">${preview}</div>
|
||||
${isLong ? `<div class="ab-cv-llm-full" hidden><pre class="ab-cv-llm-pre">${escHtml(text)}</pre></div>` : ''}
|
||||
`;
|
||||
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 = `<span class="mdi mdi-book-open-page-variant"></span> 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++;
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user