// ── 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)}`
: ``).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);