feat: show page numbers on page-break markers in Script Rehearser

When a PDF audiobook is cast and saved to the Rehearser, page breaks now
carry the source PDF page number. In the Stage view each divider renders
as "— Page N —" instead of the generic "— Page break —".

Implementation:
- audiobook.js: `audiobookBuildScript()` encodes the page number in the
  form-feed line (\fN instead of bare \f) at each page boundary.
- rehearser-parse.js: `parseScript()` now matches `line.startsWith('\f')`
  and extracts the trailing page number into `line.page`.
- rehearser.js: `buildScriptPage()` renders "— Page N —" when `line.page`
  is set; `toScript()` round-trips the number back (\fN) so it survives
  save/reload; `importPDFScript()` also encodes page numbers (\f<pageIdx+1>)
  when importing screenplay PDFs directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-26 22:09:32 +02:00
parent 01a2097c5e
commit 868ce6a7bd
6 changed files with 26 additions and 11 deletions

View File

@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
--- ---
## [1.9.4] — 2026-06-26
### Changed
- **Audiobook casting — page numbers in Rehearser**: page-break markers now carry the source PDF page number. In the Script Rehearser Stage, each page divider shows **"— Page N —"** instead of the generic "— Page break —", making it easy to cross-reference the audiobook script against the book. Applies both to audiobooks cast from Read Aloud PDFs and to screenplay PDFs imported directly into the Rehearser.
---
## [1.9.3] — 2026-06-26 ## [1.9.3] — 2026-06-26
### Added ### Added

View File

@ -1 +1 @@
1.9.3 1.9.4

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.3"> <link rel="stylesheet" href="/static/style.css?v=1.9.4">
<!-- ── 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.3"></script> <script src="/static/loader.js?v=1.9.4"></script>
</body> </body>
</html> </html>

View File

@ -1291,7 +1291,10 @@ function audiobookBuildScript(segments) {
const pos = at >= 0 ? at : searchPos; const pos = at >= 0 ? at : searchPos;
if (at >= 0) searchPos = at + probe.length; if (at >= 0) searchPos = at + probe.length;
while (markIdx < marks.length && pos >= marks[markIdx].offset) { while (markIdx < marks.length && pos >= marks[markIdx].offset) {
if (script.trim()) script += '\n\f\n'; // page break before this segment if (script.trim()) {
const pn = marks[markIdx].page;
script += pn ? `\n\f${pn}\n` : '\n\f\n'; // page break with page number
}
markIdx++; markIdx++;
} }
} }

View File

@ -54,10 +54,13 @@ function parseScript(text) {
continue; continue;
} }
// PDF page-break marker — preserved from importPDFScript // PDF page-break marker — preserved from importPDFScript / audiobook casting
if (line === '\f') { // Format: bare \f or \f<number> (e.g. \f3 = page 3)
if (line.startsWith('\f')) {
flushDialog(); flushAction(); flushDialog(); flushAction();
result.push({ type: 'pagebreak', speaker: '', text: '', isDirection: true }); const pnStr = line.slice(1).trim();
const pageNum = pnStr && /^\d+$/.test(pnStr) ? parseInt(pnStr, 10) : null;
result.push({ type: 'pagebreak', speaker: '', text: '', page: pageNum, isDirection: true });
state = 'action'; currentSpeaker = null; state = 'action'; currentSpeaker = null;
continue; continue;
} }

View File

@ -204,7 +204,7 @@ function linesToScriptText() {
case 'dialog': return '\n' + line.speaker + '\n' + line.text + '\n'; case 'dialog': return '\n' + line.speaker + '\n' + line.text + '\n';
case 'direction': case 'direction':
return line.speaker ? line.text + '\n' : '\n' + line.text + '\n'; return line.speaker ? line.text + '\n' : '\n' + line.text + '\n';
case 'pagebreak': return '\n\f\n'; case 'pagebreak': return line.page ? `\n\f${line.page}\n` : '\n\f\n';
default: return ''; default: return '';
} }
}).join('').trim(); }).join('').trim();
@ -450,7 +450,7 @@ async function importPDFScript(file) {
for (let pageIdx = 0; pageIdx < pdf.numPages; pageIdx++) { for (let pageIdx = 0; pageIdx < pdf.numPages; pageIdx++) {
// Emit a page-break marker between PDF pages so the rehearser can paginate exactly // Emit a page-break marker between PDF pages so the rehearser can paginate exactly
if (pageIdx > 0) { blank(); out.push('\f'); blank(); } if (pageIdx > 0) { blank(); out.push(`\f${pageIdx + 1}`); blank(); }
const pageLns = allLinesByPage[pageIdx] || []; const pageLns = allLinesByPage[pageIdx] || [];
for (const ln of pageLns) { for (const ln of pageLns) {
@ -2239,8 +2239,10 @@ function buildScriptPage() {
${editBtn} ${synthDot} ${editBtn} ${synthDot}
</div>`; </div>`;
case 'pagebreak': case 'pagebreak': {
return `<div class="reh-block-pagebreak${lineFlags}" data-index="${i}">${bulkCheck}<span class="reh-pagebreak-label">— Page break —</span></div>`; const pbLabel = line.page ? `— Page ${line.page}` : '— Page break —';
return `<div class="reh-block-pagebreak${lineFlags}" data-index="${i}">${bulkCheck}<span class="reh-pagebreak-label">${pbLabel}</span></div>`;
}
case 'transition': case 'transition':
return `<div class="reh-transition${lineFlags}" data-index="${i}">${bulkCheck}${escHtml(line.text)}</div>`; return `<div class="reh-transition${lineFlags}" data-index="${i}">${bulkCheck}${escHtml(line.text)}</div>`;