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:
parent
e62abbcd1d
commit
65e2784d2d
@ -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
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user