refactor: extract rehearser training comparison
This commit is contained in:
parent
d613591526
commit
d3bc806f33
@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### 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
|
- **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.
|
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,
|
- **Namespace migration surface.** Extracted Rehearser and Audiobook modules now publish parse, stage, voice-picker,
|
||||||
|
|||||||
1
TODO.md
1
TODO.md
@ -131,6 +131,7 @@ architecture change: the larger Audiobook/Rehearser/style split.
|
|||||||
- [ ] Extract Rehearser voice matching and voice design.
|
- [ ] Extract Rehearser voice matching and voice design.
|
||||||
- [x] Extract Rehearser voice-picker presentation and preview state into `rehearser-voice-picker.js`.
|
- [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 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`.
|
- [x] Extract Rehearser library persistence and IndexedDB migration into `rehearser-library.js`.
|
||||||
- [ ] Extract Audiobook parsing and casting.
|
- [ ] Extract Audiobook parsing and casting.
|
||||||
- [x] Extract Audiobook parsing primitives and speaker-heuristic constants into `audiobook-parse.js`.
|
- [x] Extract Audiobook parsing primitives and speaker-heuristic constants into `audiobook-parse.js`.
|
||||||
|
|||||||
@ -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-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`.
|
- ✅ `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`.
|
- ✅ `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
|
- ✅ `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.
|
in `rehearser.js` until their UI/event-handler dependencies are isolated.
|
||||||
|
|
||||||
|
|||||||
@ -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.31">
|
<meta name="app-version" content="1.24.32">
|
||||||
<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,7 +27,7 @@
|
|||||||
|
|
||||||
<!-- ── 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.31">
|
<link rel="stylesheet" href="/static/style.css?v=1.24.32">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -392,7 +392,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.31"></script>
|
<script src="/static/loader.js?v=1.24.32"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
41
static/js/rehearser-train.js
Normal file
41
static/js/rehearser-train.js
Normal file
@ -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)
|
||||||
|
? `<span class="tw-ok">${escHtml(w)}</span>`
|
||||||
|
: `<span class="tw-miss">${escHtml(w)}</span>`).join(' ');
|
||||||
|
const actHtml = aw.map((w, i) => matchedA.has(i)
|
||||||
|
? `<span class="tw-ok">${escHtml(w)}</span>`
|
||||||
|
: `<span class="tw-extra">${escHtml(w)}</span>`).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 };
|
||||||
@ -4513,40 +4513,6 @@ const trainState = {
|
|||||||
recSecs: 0,
|
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)
|
|
||||||
? `<span class="tw-ok">${escHtml(w)}</span>`
|
|
||||||
: `<span class="tw-miss">${escHtml(w)}</span>`).join(' ');
|
|
||||||
const actHtml = aw.map((w, i) => matchedA.has(i)
|
|
||||||
? `<span class="tw-ok">${escHtml(w)}</span>`
|
|
||||||
: `<span class="tw-extra">${escHtml(w)}</span>`).join(' ');
|
|
||||||
const score = ew.length ? Math.round((matchedE.size / ew.length) * 100) : 100;
|
|
||||||
return { html_exp: expHtml, html_act: actHtml, score };
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTrainSeq() {
|
function buildTrainSeq() {
|
||||||
const seq = [];
|
const seq = [];
|
||||||
const lines = rehState.lines;
|
const lines = rehState.lines;
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
"integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace"
|
"integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace"
|
||||||
],
|
],
|
||||||
"lazy": {
|
"lazy": {
|
||||||
"s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"],
|
"s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser-train", "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-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", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
"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", "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-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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user