diff --git a/CHANGELOG.md b/CHANGELOG.md index ff43f0f..76576cb 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 +- **Rehearser training extraction.** Transcript normalization, LCS matching, and word-level comparison now live in + `rehearser-train.js`; recording and state-machine wiring remain behavior-compatible in `rehearser.js`. - **Rehearser cast-controls extraction.** Cast view switching, sorting, filtering, and their guarded event wiring now live in `rehearser-cast.js`, reducing the main renderer's responsibility while preserving the legacy API. - **Namespace migration surface.** Extracted Rehearser and Audiobook modules now publish parse, stage, voice-picker, diff --git a/TODO.md b/TODO.md index 8d0f5e1..dfaf6b7 100644 --- a/TODO.md +++ b/TODO.md @@ -131,6 +131,7 @@ architecture change: the larger Audiobook/Rehearser/style split. - [ ] Extract Rehearser voice matching and voice design. - [x] Extract Rehearser voice-picker presentation and preview state into `rehearser-voice-picker.js`. - [x] Extract Rehearser cast view, sorting, filtering, and toolbar wiring into `rehearser-cast.js`. +- [x] Extract Rehearser training transcript comparison helpers into `rehearser-train.js`. - [x] Extract Rehearser library persistence and IndexedDB migration into `rehearser-library.js`. - [ ] Extract Audiobook parsing and casting. - [x] Extract Audiobook parsing primitives and speaker-heuristic constants into `audiobook-parse.js`. diff --git a/VERSION b/VERSION index e74fe9d..db49fca 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.24.31 +1.24.32 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8150fc4..ce4a00d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -35,7 +35,7 @@ Done incrementally as global-scope ordered files (no ES-module change needed) - ✅ `rehearser-voice-picker.js` — voice preview state and matched-choice presentation; import and assignment actions remain in `rehearser.js`. - ✅ `rehearser-cast.js` — cast view, sort/filter state handlers, and cast toolbar wiring; card rendering remains in `rehearser.js`. - ✅ `audiobook-parse.js` — dialogue detection, dehyphenation, and speaker-heuristic constants; the casting pipeline remains in `audiobook.js`. -- ⬜ `rehearser-train.js` — mic record / STT compare. +- ✅ `rehearser-train.js` — transcript normalization, LCS comparison, and word-level training feedback; microphone/state wiring remains in `rehearser.js`. - ✅ `rehearser-library.js` — SQLite persistence helpers and IndexedDB migration; import/export and auto-save remain in `rehearser.js` until their UI/event-handler dependencies are isolated. diff --git a/static/index.html b/static/index.html index 8f09852..afefdfc 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -392,7 +392,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/rehearser-train.js b/static/js/rehearser-train.js new file mode 100644 index 0000000..feecdb5 --- /dev/null +++ b/static/js/rehearser-train.js @@ -0,0 +1,41 @@ +// ── 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 _trainNormWords(str) { + return (str || '').toLowerCase().replace(/[^a-zäöüàáâãèéêëìíîïòóôõùúûüýÿæœßа-яёА-ЯЁ0-9\s]/g, '').trim().split(/\s+/).filter(Boolean); +} + +function _trainLcs(a, b) { + const R = a.length, C = b.length; + const dp = Array.from({length: R + 1}, () => new Int16Array(C + 1)); + for (let i = 1; i <= R; i++) for (let j = 1; j <= C; j++) + dp[i][j] = a[i-1] === b[j-1] ? dp[i-1][j-1] + 1 : Math.max(dp[i-1][j], dp[i][j-1]); + let i = R, j = C; const seq = []; + while (i > 0 && j > 0) { + if (a[i-1] === b[j-1]) { seq.unshift([i-1, j-1]); i--; j--; } + else if (dp[i-1][j] >= dp[i][j-1]) i--; + else j--; + } + return seq; +} + +function _trainCompare(expected, actual) { + const ew = _trainNormWords(expected), aw = _trainNormWords(actual); + if (!ew.length) return { html_exp: '', html_act: '', score: 1 }; + const pairs = _trainLcs(ew, aw); + const matchedE = new Set(pairs.map(p => p[0])); + const matchedA = new Set(pairs.map(p => p[1])); + const expHtml = ew.map((w, i) => matchedE.has(i) + ? `${escHtml(w)}` + : `${escHtml(w)}`).join(' '); + const actHtml = aw.map((w, i) => matchedA.has(i) + ? `${escHtml(w)}` + : `${escHtml(w)}`).join(' '); + const score = ew.length ? Math.round((matchedE.size / ew.length) * 100) : 100; + return { html_exp: expHtml, html_act: actHtml, score }; +} + +window.TTVC = window.TTVC || {}; +window.TTVC.rehearser = window.TTVC.rehearser || {}; +window.TTVC.rehearser.train = { compare: _trainCompare }; diff --git a/static/js/rehearser.js b/static/js/rehearser.js index 4453421..4018b3f 100644 --- a/static/js/rehearser.js +++ b/static/js/rehearser.js @@ -4513,40 +4513,6 @@ const trainState = { recSecs: 0, }; -function _trainNormWords(str) { - return (str || '').toLowerCase().replace(/[^a-zäöüàáâãèéêëìíîïòóôõùúûüýÿæœßа-яёА-ЯЁ0-9\s]/g, '').trim().split(/\s+/).filter(Boolean); -} - -function _trainLcs(a, b) { - const R = a.length, C = b.length; - const dp = Array.from({length: R + 1}, () => new Int16Array(C + 1)); - for (let i = 1; i <= R; i++) for (let j = 1; j <= C; j++) - dp[i][j] = a[i-1] === b[j-1] ? dp[i-1][j-1] + 1 : Math.max(dp[i-1][j], dp[i][j-1]); - let i = R, j = C; const seq = []; - while (i > 0 && j > 0) { - if (a[i-1] === b[j-1]) { seq.unshift([i-1, j-1]); i--; j--; } - else if (dp[i-1][j] >= dp[i][j-1]) i--; - else j--; - } - return seq; -} - -function _trainCompare(expected, actual) { - const ew = _trainNormWords(expected), aw = _trainNormWords(actual); - if (!ew.length) return { html_exp: '', html_act: '', score: 1 }; - const pairs = _trainLcs(ew, aw); - const matchedE = new Set(pairs.map(p => p[0])); - const matchedA = new Set(pairs.map(p => p[1])); - const expHtml = ew.map((w, i) => matchedE.has(i) - ? `${escHtml(w)}` - : `${escHtml(w)}`).join(' '); - const actHtml = aw.map((w, i) => matchedA.has(i) - ? `${escHtml(w)}` - : `${escHtml(w)}`).join(' '); - const score = ew.length ? Math.round((matchedE.size / ew.length) * 100) : 100; - return { html_exp: expHtml, html_act: actHtml, score }; -} - function buildTrainSeq() { const seq = []; const lines = rehState.lines; diff --git a/static/module-manifest.json b/static/module-manifest.json index 232e888..e7fc6ad 100644 --- a/static/module-manifest.json +++ b/static/module-manifest.json @@ -4,9 +4,9 @@ "integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace" ], "lazy": { - "s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"], - "s-reader": ["rehearser-parse", "rehearser-cast", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets"], - "s-library": ["rehearser-parse", "rehearser-cast", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"], - "s-caststudio": ["rehearser-parse", "rehearser-cast", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"] + "s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"], + "s-reader": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets"], + "s-library": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"], + "s-caststudio": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"] } }