From ba7b3115b9ec9d32f3b9ee7afcc22b133a020c12 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 4 Sep 2026 12:05:50 +0200 Subject: [PATCH] refactor: extract audiobook preview export --- CHANGELOG.md | 2 + TODO.md | 2 +- VERSION | 2 +- docs/ARCHITECTURE.md | 1 + static/index.html | 16 +- static/js/audiobook-preview-export.js | 787 ++++++++++++++++++++++++++ static/js/audiobook.js | 769 ------------------------- static/module-manifest.json | 6 +- tests/lazy-loading.spec.js | 4 +- 9 files changed, 806 insertions(+), 783 deletions(-) create mode 100644 static/js/audiobook-preview-export.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 6315f86..6d52905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi ## [Unreleased] ### Changed +- **Audiobook preview/export extraction.** Editable cast review, Rehearser handoff, chapter-aware synthesis/export, + pause settings, and export browsing now live in `audiobook-preview-export.js`. - **Audiobook casting-pipeline extraction.** LLM casting, unknown-speaker recast, automatic repair, and cast-view orchestration now live in `audiobook-pipeline.js`. - **Rehearser I/O extraction.** Session serialization, library save/load/autosave, REH/Fountain/FDX/OSF export, and diff --git a/TODO.md b/TODO.md index 1eb3797..9765119 100644 --- a/TODO.md +++ b/TODO.md @@ -122,7 +122,7 @@ architecture change: the larger Audiobook/Rehearser/style split. - [ ] Split `audiobook.js`, `rehearser.js`, and `style.css` by responsibility; Rehearser casting, playback, training, voice design, I/O, library, and stage blocks are now isolated, with import/event wiring and remaining Audiobook orchestration - still in the monolith; Audiobook LLM orchestration is now isolated from the Reader/export UI. + still in the monolith; Audiobook LLM orchestration and preview/export are now isolated from core Reader wiring. - [x] Extract Rehearser stage display and cast-sidebar controls into `rehearser-stage.js`. - [ ] Replace broad globals with namespaces such as `window.TTVC.audiobook`; a compatibility namespace bridge now exposes the existing Audiobook, Rehearser, and job APIs while responsibility-by-responsibility migration continues. diff --git a/VERSION b/VERSION index e7a3fa9..3ad2df4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.24.52 +1.24.53 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 55cc557..b4f0201 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -42,6 +42,7 @@ Done incrementally as global-scope ordered files (no ES-module change needed) - ✅ `audiobook-parse.js` — dialogue detection, dehyphenation, and speaker-heuristic constants; the casting pipeline remains in `audiobook.js`. - ✅ `audiobook-cast.js` — cast Markdown export, legacy page-stamping migration, deterministic speaker/sentence/duplicate/alias normalization helpers, and the live cast-view renderer. - ✅ `audiobook-pipeline.js` — LLM casting, unknown-speaker recast, automatic repair, and cast-view orchestration. +- ✅ `audiobook-preview-export.js` — editable cast preview, Rehearser handoff, chapter-aware export, and export browsing. - ✅ `static/css/resource-diagnostics.css` — isolated Performance/resource diagnostic panel styles. - ✅ `static/css/background-jobs.css` — isolated background-job history and retry-panel styles. - ✅ `static/css/voice-design.css` — isolated Voice Design form, samples, and preview-match styles. diff --git a/static/index.html b/static/index.html index c268e86..0d923ff 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,12 +27,12 @@ - - - - - - + + + + + + @@ -397,7 +397,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/audiobook-preview-export.js b/static/js/audiobook-preview-export.js new file mode 100644 index 0000000..a5a9972 --- /dev/null +++ b/static/js/audiobook-preview-export.js @@ -0,0 +1,787 @@ +// Audiobook cast preview, Rehearser handoff, and chapter export UI. +// Loaded after the casting pipeline and before audiobook.js; dependencies resolve at call time. + +// ── Editable attribution preview ───────────────────────────────────────────── + +async function audiobookOpenCurrentInRehearser() { + const segs = _audiobook.segments || []; + if (!segs.length) { toast('No cast to open', 'error'); return false; } + + // Build the record straight from the CURRENT in-memory segments and load + // it directly — a prior version saved this to IndexedDB and then + // re-fetched it by id before loading, which could hand back a stale + // record (a read-after-write race, or a stale `_audiobook.rehId` left + // over from an earlier pass) instead of the just-corrected data. Building + // once and loading that same object removes the possibility entirely; + // the DB write below is now just for persistence, not the source of what + // gets loaded. + if (typeof window.rehLoadRecord === 'function' && typeof _audiobookBuildRehRecord === 'function') { + try { + const rec = await _audiobookBuildRehRecord(segs); + if (_audiobook.rehId) rec.id = _audiobook.rehId; + document.getElementById('audiobook-preview')?.remove(); + if (typeof navTo === 'function') navTo('s-rehearser'); + window.rehLoadRecord(rec); + // rehLoadRecord always lands on the Cast phase, but by the time you're + // opening the Rehearser from the Audiobook pipeline, voices were already + // assigned in Assign Voices — Cast there is pure redundant re-work. + // Jump straight to Stage (phase 3), the line-by-line editing view. + // showPhase() alone only toggles which phase
is visible — the + // actual script/character panes are built by buildScriptPage(), same + // as the Stage sub-tab's own click handler does. + if (rehState.lines.length && typeof buildScriptPage === 'function') { + buildScriptPage(); + if (typeof showPhase === 'function') showPhase(3); + if (typeof highlightCurrentLine === 'function') highlightCurrentLine(); + } else if (typeof showPhase === 'function') showPhase(3); + toast('Opened cast in Script Rehearser', 'success'); + audiobookSaveAsRehearsal({ silent: true }).catch(() => {}); + return true; + } catch (err) { + console.warn('[audiobook] failed to build rehearser record directly, falling back:', err); + } + } + + await audiobookSaveAsRehearsal({ silent: true }); + const { script, emotions } = audiobookBuildScript(segs); + await audiobookOpenInRehearser(script, (readerState.title || 'Audiobook'), emotions); + if (rehState.lines.length && typeof buildScriptPage === 'function') { + buildScriptPage(); + if (typeof showPhase === 'function') showPhase(3); + if (typeof highlightCurrentLine === 'function') highlightCurrentLine(); + } else if (typeof showPhase === 'function') showPhase(3); + return true; +} + +function audiobookShowPreview() { + audiobookOpenCurrentInRehearser(); + return; + const segs = _audiobook.segments || []; + const roster = _audiobook.roster || []; + document.getElementById('audiobook-preview')?.remove(); + const ov = document.createElement('div'); + ov.id = 'audiobook-preview'; + ov.className = 'audiobook-overlay'; + // datalist = Narrator + LLM roster + any speakers present in the segments (incl. Unknown) + const speakerSet = [...new Set(['Narrator', ...roster, ...segs.filter(s => s.type === 'dialogue' && s.speaker).map(s => s.speaker)])]; + const charCount = speakerSet.filter(n => !/^Unknown|Unbekannt/i.test(n)).length; + const opts = speakerSet.map(n => `