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]
|
||||
|
||||
### 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,
|
||||
|
||||
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.
|
||||
- [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`.
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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.31">
|
||||
<meta name="app-version" content="1.24.32">
|
||||
<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,7 +27,7 @@
|
||||
|
||||
<!-- ── 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.31">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.24.32">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -392,7 +392,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- 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>
|
||||
</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,
|
||||
};
|
||||
|
||||
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() {
|
||||
const seq = [];
|
||||
const lines = rehState.lines;
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user