diff --git a/CHANGELOG.md b/CHANGELOG.md index 2424146..56c5f2c 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 +- **Pure leaf namespace migration.** Rehearser parsing/training and Audiobook parsing/segmentation helpers now keep + declarations private and publish explicit compatibility exports alongside their `window.TTVC` APIs. - **P1 architecture audit.** Marked the incremental frontend modularization complete: all planned Audiobook/Rehearser responsibility slices are manifest-driven, namespace-tested, and compatibility-preserving; full ES-module migration remains future work. - **Rehearser training extraction.** Training-mode state, cue playback, transcription, microphone recording, and phase diff --git a/TODO.md b/TODO.md index 538e818..7d18429 100644 --- a/TODO.md +++ b/TODO.md @@ -124,8 +124,8 @@ architecture change: the larger Audiobook/Rehearser/style split. voice design, I/O, library, stage, and recording blocks plus Audiobook casting, preview, export, and feature styles are isolated. Core Reader state and compatibility event wiring remain ordered in the legacy entry points. - [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. +- [ ] Replace broad globals with namespaces such as `window.TTVC.audiobook`; pure leaf modules now use private scopes + with explicit compatibility exports, while stateful modules continue the responsibility-by-responsibility migration. - [x] Generate the feature-module list for `loader.js` and `scripts/minify.mjs` from one shared manifest; larger responsibility splits remain. - [x] Add a dependency-free frontend syntax and top-level declaration collision gate (`npm run check:frontend`); diff --git a/VERSION b/VERSION index b944e18..df64b95 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.24.56 +1.24.57 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a64a673..5e238d2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -27,7 +27,7 @@ ### 1. Decouple globals → ES modules The global scope + monkey-patching is the root cause of the recurring "wiring/timing" bugs (controls bound before the section mounts, etc.). Migrate **one file at a time**: 1. Add `"type": "module"` loading path; keep a compat shim that re-exposes the few cross-module symbols on `window` during transition. -2. Convert leaf modules first (`utils`, `settings`, `voice-picker`), exporting explicitly and importing where used. +2. Convert leaf modules first (pure Rehearser/Audiobook helpers are now privately scoped), exporting explicitly and importing where used. 3. Replace the `renderVoiceList` monkey-patch with a normal function + an `onRender` hook. 4. Switch `loader.js` to a single ` - + diff --git a/static/js/audiobook-parse.js b/static/js/audiobook-parse.js index 60bfafe..b036530 100644 --- a/static/js/audiobook-parse.js +++ b/static/js/audiobook-parse.js @@ -2,6 +2,7 @@ // Kept dependency-free so the Reader can load these helpers before the larger // audiobook casting workflow. The casting pipeline continues to use the same // legacy global function names during the incremental migration. +(function (root) { const AB_DIALOGUE_RE = /[«»„“”"‟‚‘’『「<]|(?:^|\n)\s*[—–]\s/; function audiobookHasDialogue(t) { return AB_DIALOGUE_RE.test(t || ''); } @@ -24,6 +25,9 @@ const AB_PERSON_NOUNS = new Set(['Mann','Frau','Junge','Mädchen','Alte','Alter' 'Prinz','Prinzessin','Fürst','Fürstin','Baron','Baronin','Bote','Diener','Dienerin','Knabe','Kind','Reiter','Reiterin', 'Bogenschütze','Schmied','Schmiedin','Heiler','Heilerin','Gelehrte','Gelehrter','Kapitän','Anführer','Anführerin']); -window.TTVC = window.TTVC || {}; -window.TTVC.audiobook = window.TTVC.audiobook || {}; -window.TTVC.audiobook.parse = { audiobookHasDialogue, audiobookDehyphenate }; + root.audiobookHasDialogue = audiobookHasDialogue; + root.audiobookDehyphenate = audiobookDehyphenate; + root.TTVC = root.TTVC || {}; + root.TTVC.audiobook = root.TTVC.audiobook || {}; + root.TTVC.audiobook.parse = { audiobookHasDialogue, audiobookDehyphenate }; +})(window); diff --git a/static/js/audiobook-pipeline.js b/static/js/audiobook-pipeline.js index 1793844..54c3dc7 100644 --- a/static/js/audiobook-pipeline.js +++ b/static/js/audiobook-pipeline.js @@ -1063,6 +1063,9 @@ async function audiobookCast(overrideUrl, overrideModel, resume) { window.TTVC=window.TTVC||{}; window.TTVC.audiobook=window.TTVC.audiobook||{}; Object.assign(window.TTVC.audiobook,{ + startCast:audiobookCast, + openCastView:audiobookOpenCastView, + recastUnknown:audiobookRecastUnknown, pipeline:{ recastUnknown:audiobookRecastUnknown, cast:audiobookCast, @@ -1070,4 +1073,3 @@ Object.assign(window.TTVC.audiobook,{ autoRepair:audiobookAutoRepairCast, }, }); - diff --git a/static/js/audiobook-segmentation.js b/static/js/audiobook-segmentation.js index a95fda0..3a79314 100644 --- a/static/js/audiobook-segmentation.js +++ b/static/js/audiobook-segmentation.js @@ -2,6 +2,7 @@ // Quote-boundary segmentation and conservative turn-taking are kept separate // from the live LLM casting workflow. Their shared quote/heuristic helpers are // resolved when a passage is processed, preserving the legacy global API. +(function (root) { function _abImplausibleDialoguePiece(text) { const t = String(text || '').trim(); @@ -58,6 +59,10 @@ function audiobookTurnTaking(segs) { return segs; } -window.TTVC = window.TTVC || {}; -window.TTVC.audiobook = window.TTVC.audiobook || {}; -window.TTVC.audiobook.segmentation = { splitByQuotes: audiobookSplitByQuotes, turnTaking: audiobookTurnTaking }; + root._abImplausibleDialoguePiece = _abImplausibleDialoguePiece; + root.audiobookSplitByQuotes = audiobookSplitByQuotes; + root.audiobookTurnTaking = audiobookTurnTaking; + root.TTVC = root.TTVC || {}; + root.TTVC.audiobook = root.TTVC.audiobook || {}; + root.TTVC.audiobook.segmentation = { splitByQuotes: audiobookSplitByQuotes, turnTaking: audiobookTurnTaking }; +})(window); diff --git a/static/js/audiobook.js b/static/js/audiobook.js index 56bd7f0..2808b05 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -1548,6 +1548,9 @@ async function audiobookRecastSelectedCharacters(selectedNames) { return null; } window.audiobookRecastSelectedCharacters = audiobookRecastSelectedCharacters; +window.TTVC = window.TTVC || {}; +window.TTVC.audiobook = window.TTVC.audiobook || {}; +window.TTVC.audiobook.recastSelectedCharacters = audiobookRecastSelectedCharacters; // Live casting view: a scrolling feed of attributed lines + a character roster // that fills up as speakers are discovered. Far clearer than a bare bar. @@ -1735,14 +1738,3 @@ function audiobookExportDialog() { modal.style.display = 'flex'; } } - -window.TTVC = window.TTVC || {}; -window.TTVC.audiobook = window.TTVC.audiobook || {}; -Object.assign(window.TTVC.audiobook, { - startCast: audiobookCast, - openCastView: audiobookOpenCastView, - recastUnknown: audiobookRecastUnknown, - recastSelectedCharacters: audiobookRecastSelectedCharacters, - export: audiobookExport, - saveAsRehearsal: audiobookSaveAsRehearsal, -}); diff --git a/static/js/rehearser-parse.js b/static/js/rehearser-parse.js index 0ebb1bf..c6bbbf1 100644 --- a/static/js/rehearser-parse.js +++ b/static/js/rehearser-parse.js @@ -1,6 +1,7 @@ // ── Script parser (extracted from rehearser.js — global scope, load before it) ── // Pure functions: parse screenplay/theatre text into typed lines, and derive the // initial cast. No load-time dependencies (SPEAKER_COLORS is read only at call time). +(function (root) { function parseScript(text) { const rawLines = text.split('\n'); const result = []; @@ -192,6 +193,9 @@ function detectCharacters(lines) { return cast; } -window.TTVC = window.TTVC || {}; -window.TTVC.rehearser = window.TTVC.rehearser || {}; -window.TTVC.rehearser.parse = { parseScript, detectCharacters }; + root.parseScript = parseScript; + root.detectCharacters = detectCharacters; + root.TTVC = root.TTVC || {}; + root.TTVC.rehearser = root.TTVC.rehearser || {}; + root.TTVC.rehearser.parse = { parseScript, detectCharacters }; +})(window); diff --git a/static/js/rehearser-train.js b/static/js/rehearser-train.js index feecdb5..e54a4d4 100644 --- a/static/js/rehearser-train.js +++ b/static/js/rehearser-train.js @@ -1,6 +1,7 @@ // ── Rehearser train-mode comparison helpers ───────────────────────────────── // Recording/state-machine wiring remains in rehearser.js; these pure helpers // are isolated so transcript comparison can be tested and migrated first. +(function (root) { function _trainNormWords(str) { return (str || '').toLowerCase().replace(/[^a-zäöüàáâãèéêëìíîïòóôõùúûüýÿæœßа-яёА-ЯЁ0-9\s]/g, '').trim().split(/\s+/).filter(Boolean); @@ -36,6 +37,8 @@ function _trainCompare(expected, actual) { return { html_exp: expHtml, html_act: actHtml, score }; } -window.TTVC = window.TTVC || {}; -window.TTVC.rehearser = window.TTVC.rehearser || {}; -window.TTVC.rehearser.train = { compare: _trainCompare }; + root._trainCompare = _trainCompare; + root.TTVC = root.TTVC || {}; + root.TTVC.rehearser = root.TTVC.rehearser || {}; + root.TTVC.rehearser.train = { compare: _trainCompare }; +})(window); diff --git a/tests/lazy-loading.spec.js b/tests/lazy-loading.spec.js index b27d612..5942f57 100644 --- a/tests/lazy-loading.spec.js +++ b/tests/lazy-loading.spec.js @@ -56,3 +56,17 @@ test('Rehearser playback module publishes transport APIs after loading', async ( training: typeof window.TTVC?.rehearser?.training?.enter, }))).toEqual({ start: 'function', stop: 'function', synthAll: 'function', loadRecord: 'function', exportSession: 'function', recording: 'function', training: 'function' }); }); + +test('pure leaf modules keep private declarations behind compatibility exports', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); + await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); + await page.evaluate(() => window.navTo('s-reader')); + await page.waitForFunction(() => typeof window.audiobookSplitByQuotes === 'function', null, { timeout: 15_000 }); + await expect.poll(() => page.evaluate(() => ({ + parse: typeof window.TTVC?.rehearser?.parse?.parseScript, + compare: typeof window.TTVC?.rehearser?.train?.compare, + dialogue: typeof window.TTVC?.audiobook?.parse?.audiobookHasDialogue, + split: typeof window.TTVC?.audiobook?.segmentation?.splitByQuotes, + }))).toEqual({ parse: 'function', compare: 'function', dialogue: 'function', split: 'function' }); +});