refactor: scope pure frontend leaves

This commit is contained in:
mARTin-B78 2026-09-04 12:20:33 +02:00
parent d32985256d
commit 302dbe1279
12 changed files with 64 additions and 36 deletions

View File

@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
## [Unreleased] ## [Unreleased]
### Changed ### 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 - **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. 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 - **Rehearser training extraction.** Training-mode state, cue playback, transcription, microphone recording, and phase

View File

@ -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 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. 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`. - [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 - [ ] Replace broad globals with namespaces such as `window.TTVC.audiobook`; pure leaf modules now use private scopes
exposes the existing Audiobook, Rehearser, and job APIs while responsibility-by-responsibility migration continues. 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; - [x] Generate the feature-module list for `loader.js` and `scripts/minify.mjs` from one shared manifest;
larger responsibility splits remain. larger responsibility splits remain.
- [x] Add a dependency-free frontend syntax and top-level declaration collision gate (`npm run check:frontend`); - [x] Add a dependency-free frontend syntax and top-level declaration collision gate (`npm run check:frontend`);

View File

@ -1 +1 @@
1.24.56 1.24.57

View File

@ -27,7 +27,7 @@
### 1. Decouple globals → ES modules ### 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**: 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. 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. 3. Replace the `renderVoiceList` monkey-patch with a normal function + an `onRender` hook.
4. Switch `loader.js` to a single `<script type="module">` entry that imports the graph; `esbuild --bundle` then produces one minified, tree-shaken file. 4. Switch `loader.js` to a single `<script type="module">` entry that imports the graph; `esbuild --bundle` then produces one minified, tree-shaken file.
@ -60,6 +60,8 @@ Done incrementally as global-scope ordered files (no ES-module change needed)
**Pattern (proven):** extract a cohesive block whose only cross-file references resolve at *call time* (event handlers), not at module load. Top-level `const rehState` and top-level `addEventListener` registrations must stay ordered — extract those last and load them after their dependencies. Re-run `npx playwright test` after each extraction. **Pattern (proven):** extract a cohesive block whose only cross-file references resolve at *call time* (event handlers), not at module load. Top-level `const rehState` and top-level `addEventListener` registrations must stay ordered — extract those last and load them after their dependencies. Re-run `npx playwright test` after each extraction.
## Remaining recommendations ## Remaining recommendations
- Complete the eventual ES-module/global-compatibility migration one leaf at a time; current behavior intentionally keeps
legacy globals available while the `window.TTVC` surface is adopted.
- Complete the eventual ES-module/global-compatibility migration one leaf at a time; current behavior intentionally keeps - Complete the eventual ES-module/global-compatibility migration one leaf at a time; current behavior intentionally keeps
legacy globals available while the `window.TTVC` surface is adopted. legacy globals available while the `window.TTVC` surface is adopted.
- Replace remaining silent `.catch(() => {})` with `logErr(context, e)` (helper added in `utils.js`). - Replace remaining silent `.catch(() => {})` with `logErr(context, e)` (helper added in `utils.js`).

View File

@ -10,7 +10,7 @@
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#2563EB"> <meta name="theme-color" content="#2563EB">
<meta name="app-version" content="1.24.56"> <meta name="app-version" content="1.24.57">
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icon.svg" type="image/svg+xml"> <link rel="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg"> <link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,12 +27,12 @@
<!-- ── 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.24.56"> <link rel="stylesheet" href="/static/style.css?v=1.24.57">
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.56"> <link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.57">
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.56"> <link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.57">
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.56"> <link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.57">
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.24.56"> <link rel="stylesheet" href="/static/css/rehearser.css?v=1.24.57">
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.24.56"> <link rel="stylesheet" href="/static/css/audiobook.css?v=1.24.57">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── --> <!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -397,7 +397,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script> </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.24.56"></script> <script src="/static/loader.js?v=1.24.57"></script>
</body> </body>
</html> </html>

View File

@ -2,6 +2,7 @@
// Kept dependency-free so the Reader can load these helpers before the larger // Kept dependency-free so the Reader can load these helpers before the larger
// audiobook casting workflow. The casting pipeline continues to use the same // audiobook casting workflow. The casting pipeline continues to use the same
// legacy global function names during the incremental migration. // legacy global function names during the incremental migration.
(function (root) {
const AB_DIALOGUE_RE = /[«»„“”"‟‚‘’『「<]|(?:^|\n)\s*[—–]\s/; const AB_DIALOGUE_RE = /[«»„“”"‟‚‘’『「<]|(?:^|\n)\s*[—–]\s/;
function audiobookHasDialogue(t) { return AB_DIALOGUE_RE.test(t || ''); } 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', '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']); 'Bogenschütze','Schmied','Schmiedin','Heiler','Heilerin','Gelehrte','Gelehrter','Kapitän','Anführer','Anführerin']);
window.TTVC = window.TTVC || {}; root.audiobookHasDialogue = audiobookHasDialogue;
window.TTVC.audiobook = window.TTVC.audiobook || {}; root.audiobookDehyphenate = audiobookDehyphenate;
window.TTVC.audiobook.parse = { audiobookHasDialogue, audiobookDehyphenate }; root.TTVC = root.TTVC || {};
root.TTVC.audiobook = root.TTVC.audiobook || {};
root.TTVC.audiobook.parse = { audiobookHasDialogue, audiobookDehyphenate };
})(window);

View File

@ -1063,6 +1063,9 @@ async function audiobookCast(overrideUrl, overrideModel, resume) {
window.TTVC=window.TTVC||{}; window.TTVC=window.TTVC||{};
window.TTVC.audiobook=window.TTVC.audiobook||{}; window.TTVC.audiobook=window.TTVC.audiobook||{};
Object.assign(window.TTVC.audiobook,{ Object.assign(window.TTVC.audiobook,{
startCast:audiobookCast,
openCastView:audiobookOpenCastView,
recastUnknown:audiobookRecastUnknown,
pipeline:{ pipeline:{
recastUnknown:audiobookRecastUnknown, recastUnknown:audiobookRecastUnknown,
cast:audiobookCast, cast:audiobookCast,
@ -1070,4 +1073,3 @@ Object.assign(window.TTVC.audiobook,{
autoRepair:audiobookAutoRepairCast, autoRepair:audiobookAutoRepairCast,
}, },
}); });

View File

@ -2,6 +2,7 @@
// Quote-boundary segmentation and conservative turn-taking are kept separate // Quote-boundary segmentation and conservative turn-taking are kept separate
// from the live LLM casting workflow. Their shared quote/heuristic helpers are // from the live LLM casting workflow. Their shared quote/heuristic helpers are
// resolved when a passage is processed, preserving the legacy global API. // resolved when a passage is processed, preserving the legacy global API.
(function (root) {
function _abImplausibleDialoguePiece(text) { function _abImplausibleDialoguePiece(text) {
const t = String(text || '').trim(); const t = String(text || '').trim();
@ -58,6 +59,10 @@ function audiobookTurnTaking(segs) {
return segs; return segs;
} }
window.TTVC = window.TTVC || {}; root._abImplausibleDialoguePiece = _abImplausibleDialoguePiece;
window.TTVC.audiobook = window.TTVC.audiobook || {}; root.audiobookSplitByQuotes = audiobookSplitByQuotes;
window.TTVC.audiobook.segmentation = { splitByQuotes: audiobookSplitByQuotes, turnTaking: audiobookTurnTaking }; root.audiobookTurnTaking = audiobookTurnTaking;
root.TTVC = root.TTVC || {};
root.TTVC.audiobook = root.TTVC.audiobook || {};
root.TTVC.audiobook.segmentation = { splitByQuotes: audiobookSplitByQuotes, turnTaking: audiobookTurnTaking };
})(window);

View File

@ -1548,6 +1548,9 @@ async function audiobookRecastSelectedCharacters(selectedNames) {
return null; return null;
} }
window.audiobookRecastSelectedCharacters = audiobookRecastSelectedCharacters; 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 // 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. // that fills up as speakers are discovered. Far clearer than a bare bar.
@ -1735,14 +1738,3 @@ function audiobookExportDialog() {
modal.style.display = 'flex'; 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,
});

View File

@ -1,6 +1,7 @@
// ── Script parser (extracted from rehearser.js — global scope, load before it) ── // ── Script parser (extracted from rehearser.js — global scope, load before it) ──
// Pure functions: parse screenplay/theatre text into typed lines, and derive the // 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). // initial cast. No load-time dependencies (SPEAKER_COLORS is read only at call time).
(function (root) {
function parseScript(text) { function parseScript(text) {
const rawLines = text.split('\n'); const rawLines = text.split('\n');
const result = []; const result = [];
@ -192,6 +193,9 @@ function detectCharacters(lines) {
return cast; return cast;
} }
window.TTVC = window.TTVC || {}; root.parseScript = parseScript;
window.TTVC.rehearser = window.TTVC.rehearser || {}; root.detectCharacters = detectCharacters;
window.TTVC.rehearser.parse = { parseScript, detectCharacters }; root.TTVC = root.TTVC || {};
root.TTVC.rehearser = root.TTVC.rehearser || {};
root.TTVC.rehearser.parse = { parseScript, detectCharacters };
})(window);

View File

@ -1,6 +1,7 @@
// ── Rehearser train-mode comparison helpers ───────────────────────────────── // ── Rehearser train-mode comparison helpers ─────────────────────────────────
// Recording/state-machine wiring remains in rehearser.js; these pure helpers // Recording/state-machine wiring remains in rehearser.js; these pure helpers
// are isolated so transcript comparison can be tested and migrated first. // are isolated so transcript comparison can be tested and migrated first.
(function (root) {
function _trainNormWords(str) { function _trainNormWords(str) {
return (str || '').toLowerCase().replace(/[^a-zäöüàáâãèéêëìíîïòóôõùúûüýÿæœßа-яёА-ЯЁ0-9\s]/g, '').trim().split(/\s+/).filter(Boolean); 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 }; return { html_exp: expHtml, html_act: actHtml, score };
} }
window.TTVC = window.TTVC || {}; root._trainCompare = _trainCompare;
window.TTVC.rehearser = window.TTVC.rehearser || {}; root.TTVC = root.TTVC || {};
window.TTVC.rehearser.train = { compare: _trainCompare }; root.TTVC.rehearser = root.TTVC.rehearser || {};
root.TTVC.rehearser.train = { compare: _trainCompare };
})(window);

View File

@ -56,3 +56,17 @@ test('Rehearser playback module publishes transport APIs after loading', async (
training: typeof window.TTVC?.rehearser?.training?.enter, training: typeof window.TTVC?.rehearser?.training?.enter,
}))).toEqual({ start: 'function', stop: 'function', synthAll: 'function', loadRecord: 'function', exportSession: 'function', recording: 'function', training: 'function' }); }))).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' });
});