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]
### 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

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
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`);

View File

@ -1 +1 @@
1.24.56
1.24.57

View File

@ -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 `<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.
## 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
legacy globals available while the `window.TTVC` surface is adopted.
- 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="color-scheme" content="light dark">
<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="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,12 +27,12 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<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/css/resource-diagnostics.css?v=1.24.56">
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.56">
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.56">
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.24.56">
<link rel="stylesheet" href="/static/css/audiobook.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.57">
<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.57">
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.24.57">
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.24.57">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -397,7 +397,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script>
<!-- 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>
</html>

View File

@ -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);

View File

@ -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,
},
});

View File

@ -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);

View File

@ -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,
});

View File

@ -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);

View File

@ -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);

View File

@ -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' });
});