// ── 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); } 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 }; } root._trainCompare = _trainCompare; root.TTVC = root.TTVC || {}; root.TTVC.rehearser = root.TTVC.rehearser || {}; root.TTVC.rehearser.train = { compare: _trainCompare }; })(window);