Preserve audiobook pagination when saving to Rehearser (v1.9.2)

audiobookScopeText now records PDF page boundaries as character offsets in the
scope text; audiobookBuildScript realigns each segment against the source and
emits \f page-break markers at the nearest boundary. Saved/opened cast scripts
keep the book's pagination instead of collapsing to one flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-26 21:49:51 +02:00
parent e62abbcd1d
commit 65e2784d2d
3 changed files with 54 additions and 3 deletions

View File

@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
--- ---
## [1.9.2] — 2026-06-26
### Fixed
- **Audiobook casting — pagination preserved on save**: saving (or opening) a cast audiobook as a Script Rehearsal now keeps the book's **page breaks**. Read-aloud page boundaries are tracked through casting and re-emitted as `\f` markers at the nearest segment boundary, so the Rehearser paginates the saved script to match the source PDF instead of producing one continuous flow.
---
## [1.9.1] — 2026-06-26 ## [1.9.1] — 2026-06-26
### Fixed ### Fixed

View File

@ -1 +1 @@
1.9.1 1.9.2

View File

@ -97,10 +97,33 @@ function audiobookLlmModel() { return $('reh-llm-model')?.value || ''; }
function audiobookLang() { return $('reh-design-lang')?.value || ''; } function audiobookLang() { return $('reh-design-lang')?.value || ''; }
// Gather the plain text of the current reader scope (selection > page range > all). // Gather the plain text of the current reader scope (selection > page range > all).
// Also records PDF page boundaries as character offsets in the returned text
// (_audiobook.pageMarks) so saving to the Rehearser can reconstruct page breaks.
function audiobookScopeText() { function audiobookScopeText() {
_audiobook.pageMarks = [];
if (typeof readerScopeIndices !== 'function' || !readerState?.sentences?.length) return ''; if (typeof readerScopeIndices !== 'function' || !readerState?.sentences?.length) return '';
const raw = readerScopeIndices().map(i => readerState.sentences[i].text).join(' ').replace(/\s+/g, ' ').trim(); const idxs = readerScopeIndices();
return audiobookDehyphenate(raw); // mend PDF line-break hyphenation for clean speech + tag matching const anchors = []; // {page, anchor} — first sentence of each new page
let lastPage = null;
for (const i of idxs) {
const u = readerState.sentences[i];
const pg = u?.words?.[0]?.page;
if (pg != null && pg !== lastPage) {
anchors.push({ page: pg, anchor: (u.text || '').trim().slice(0, 40) });
lastPage = pg;
}
}
const raw = idxs.map(i => readerState.sentences[i].text).join(' ').replace(/\s+/g, ' ').trim();
const text = audiobookDehyphenate(raw); // mend PDF line-break hyphenation for clean speech + tag matching
// Locate each page anchor in the final text → page-break offsets.
let from = 0;
for (const a of anchors) {
const probe = audiobookDehyphenate(a.anchor).slice(0, 24);
if (!probe) continue;
const pos = text.indexOf(probe, from);
if (pos >= 0) { _audiobook.pageMarks.push({ offset: pos, page: a.page }); from = pos; }
}
return text;
} }
// ── Progress overlay ───────────────────────────────────────────────────────── // ── Progress overlay ─────────────────────────────────────────────────────────
@ -1246,11 +1269,32 @@ function audiobookApplyPreviewAndOpen() {
// Build a rehearser script (CAPS speaker + line; narration as plain paragraphs) // Build a rehearser script (CAPS speaker + line; narration as plain paragraphs)
// and a parallel list of per-dialogue-line emotions (same order as dialog lines). // and a parallel list of per-dialogue-line emotions (same order as dialog lines).
// Inserts \f page-break markers at the segment boundary nearest each source PDF
// page start (so saved/opened scripts keep the book's pagination). Page positions
// are realigned per-segment against the source text to avoid cumulative drift.
function audiobookBuildScript(segments) { function audiobookBuildScript(segments) {
let script = ''; let script = '';
const emotions = []; const emotions = [];
const marks = _audiobook.pageMarks || [];
const src = _audiobook.lastText || '';
let markIdx = 0, searchPos = 0;
// The first page mark sits at the start of the document — no leading break.
if (marks.length && marks[0].offset <= 2) markIdx = 1;
for (const s of segments) { for (const s of segments) {
const t = (s.text || '').trim(); if (!t) continue; const t = (s.text || '').trim(); if (!t) continue;
// Where does this segment sit in the source text? (verbatim narration matches
// exactly; dialogue text — quotes stripped — still occurs in the source.)
if (src && markIdx < marks.length) {
const probe = t.slice(0, 24);
const at = probe ? src.indexOf(probe, searchPos) : -1;
const pos = at >= 0 ? at : searchPos;
if (at >= 0) searchPos = at + probe.length;
while (markIdx < marks.length && pos >= marks[markIdx].offset) {
if (script.trim()) script += '\n\f\n'; // page break before this segment
markIdx++;
}
}
const isDialogue = s.type === 'dialogue' && s.speaker && s.speaker.toLowerCase() !== 'narrator'; const isDialogue = s.type === 'dialogue' && s.speaker && s.speaker.toLowerCase() !== 'narrator';
if (isDialogue) { if (isDialogue) {
script += '\n' + s.speaker.toUpperCase() + '\n' + t + '\n'; script += '\n' + s.speaker.toUpperCase() + '\n' + t + '\n';