v64: рефакторинг — разбил монолит на css/ js/ build.sh dist/ (781→43 строки index.html)
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
// ---------- Сравнение текстов (Левенштейн) и doCompare ----------
|
||||
|
||||
function normalize(text) {
|
||||
return text.toLowerCase().replace(/[.,!?;:«»"'’]/g, '').trim();
|
||||
}
|
||||
|
||||
function levenshtein(a, b) {
|
||||
const m = a.length, n = b.length;
|
||||
const dp = Array.from({length: m+1}, (_,i) => [i]);
|
||||
for (let j=0; j<=n; j++) dp[0][j] = j;
|
||||
for (let i=1; i<=m; i++)
|
||||
for (let j=1; j<=n; j++)
|
||||
dp[i][j] = a[i-1] === b[j-1] ? dp[i-1][j-1] : 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
|
||||
return dp[m][n];
|
||||
}
|
||||
|
||||
function wordSimilarity(a, b) {
|
||||
const maxLen = Math.max(a.length, b.length);
|
||||
if (maxLen === 0) return 1;
|
||||
return 1 - levenshtein(a, b) / maxLen;
|
||||
}
|
||||
|
||||
function compareTexts(original, transcribed) {
|
||||
const a = normalize(original).split(/\s+/);
|
||||
const b = normalize(transcribed).split(/\s+/);
|
||||
let totalScore = 0;
|
||||
const result = a.map(word => {
|
||||
let best = 0, bestMatch = '';
|
||||
for (const t of b) {
|
||||
const sim = wordSimilarity(word, t);
|
||||
if (sim > best) { best = sim; bestMatch = t; }
|
||||
}
|
||||
totalScore += best;
|
||||
const quality = best >= 0.8 ? 'ok' : best >= 0.5 ? 'near' : 'bad';
|
||||
return { word, quality, bestMatch, score: Math.round(best*100) };
|
||||
});
|
||||
const score = Math.round((totalScore / a.length) * 100);
|
||||
return { score, words: result };
|
||||
}
|
||||
|
||||
function renderDiff(words) {
|
||||
return words.map(({ word, quality, bestMatch, score }) => {
|
||||
const color = quality === 'ok' ? 'green' : quality === 'near' ? '#e6a800' : 'red';
|
||||
const tip = quality !== 'ok' ? ` → ${bestMatch} (${score}%)` : '';
|
||||
return `<span style="color:${color}; font-weight:bold">${word}</span><span style="color:#888; font-size:0.85em">${tip}</span>`;
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
async function doCompare(blob, originalText, duration) {
|
||||
if (isAnalyzing) return '';
|
||||
isAnalyzing = true;
|
||||
const startTime = Date.now();
|
||||
const timerId = setInterval(() => {
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
setStatus('Анализ... ' + elapsed + 'с');
|
||||
}, 150);
|
||||
let whisperResult, phon;
|
||||
try {
|
||||
const phonTimeout = new Promise(resolve =>
|
||||
setTimeout(() => resolve({ pitchHz: 0, pitchStability: 0, voiceClarity: 0, phonScore: 0 }), 15000)
|
||||
);
|
||||
[whisperResult, phon] = await Promise.all([
|
||||
transcribe(blob),
|
||||
Promise.race([phoneticAnalysis(blob), phonTimeout])
|
||||
]);
|
||||
} catch(e) {
|
||||
clearInterval(timerId);
|
||||
isAnalyzing = false;
|
||||
setStatus('Ошибка анализа.');
|
||||
return '';
|
||||
}
|
||||
clearInterval(timerId);
|
||||
|
||||
const phonEmoji = phon.phonScore >= 85 ? '🟢' : phon.phonScore >= 60 ? '🟡' : '🔴';
|
||||
let recHTML = '', diffHTML = '';
|
||||
|
||||
if (whisperResult.text) {
|
||||
const _transcribed = whisperResult.text;
|
||||
const { score, words } = compareTexts(originalText, whisperResult.text);
|
||||
const recEmoji = score >= 90 ? '🟢' : score >= 70 ? '🟡' : '🔴';
|
||||
recHTML = recEmoji + ' Распознание: <b>' + score + '%</b> | 🗣 <i>' + whisperResult.text + '</i> | ';
|
||||
diffHTML =
|
||||
'<div style="margin:6px 0;font-size:0.8em;color:#999">' +
|
||||
'📋 Текст: зелёный ≥80%, жёлтый ≥50%, красный <50%' +
|
||||
'</div>' +
|
||||
renderDiff(words);
|
||||
} else {
|
||||
recHTML = '';
|
||||
diffHTML = '';
|
||||
}
|
||||
|
||||
document.getElementById('score').innerHTML =
|
||||
recHTML + phonEmoji + ' Произношение: <b>' + phon.phonScore + '%</b>';
|
||||
|
||||
document.getElementById('diff').innerHTML = diffHTML +
|
||||
'<div class="phonetics">' +
|
||||
'<span>🎵 Тон: ' + phon.pitchHz + ' Hz</span>' +
|
||||
'<span>📏 Стабильность: ' + phon.pitchStability + '%</span>' +
|
||||
'<span>🗣 Артикуляция: ' + phon.voiceClarity + '%</span>' +
|
||||
'</div>';
|
||||
|
||||
// Слоги — кликабельные
|
||||
_currentBlob = blob;
|
||||
_currentSylBuf = null;
|
||||
const sy = syllabifyIT(originalText);
|
||||
if (sy && sy.includes('-')) {
|
||||
const syls = sy.split('-');
|
||||
const sylHTML = '<div style="margin-top:8px;font-size:1.2em;letter-spacing:2px">' +
|
||||
syls.map((s, i) =>
|
||||
'<span class="syl-chip" onclick="playSyllable(' + i + ',' + syls.length + ')">' + s + '</span>'
|
||||
).join('<span style="color:#ccc">·</span>') +
|
||||
'</div>';
|
||||
document.getElementById('diff').innerHTML += sylHTML;
|
||||
}
|
||||
|
||||
setStatus('Готово.');
|
||||
isAnalyzing = false;
|
||||
return whisperResult.text || '';
|
||||
}
|
||||
Reference in New Issue
Block a user