v107: pronunciation assessment UI — score, phonemes, rhythm, feedback в браузере
This commit is contained in:
+10
-2
@@ -65,7 +65,7 @@ async function doCompare(blob, originalText, duration) {
|
||||
}
|
||||
clearInterval(timerId);
|
||||
|
||||
let recHTML = '', diffHTML = '';
|
||||
let recHTML = '', diffHTML = '', pronounceHTML = '';
|
||||
|
||||
if (whisperResult.text) {
|
||||
const _transcribed = whisperResult.text;
|
||||
@@ -77,13 +77,21 @@ async function doCompare(blob, originalText, duration) {
|
||||
'📋 Текст: зелёный ≥80%, жёлтый ≥50%, красный <50%' +
|
||||
'</div>' +
|
||||
renderDiff(words);
|
||||
|
||||
// Запускаем pronunciation assessment (параллельно, не блокируем)
|
||||
assessPronunciation(originalText, whisperResult).then(assess => {
|
||||
if (assess) {
|
||||
const el = document.getElementById('pronounce');
|
||||
if (el) el.innerHTML = renderPronunciation(assess);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
recHTML = '⚠️ Не распознано';
|
||||
diffHTML = '';
|
||||
}
|
||||
|
||||
document.getElementById('score').innerHTML = recHTML;
|
||||
|
||||
document.getElementById('pronounce').innerHTML = pronounceHTML;
|
||||
document.getElementById('diff').innerHTML = diffHTML;
|
||||
|
||||
// Слоги — в отдельный div, не в diff (diff перезаписывается)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// ---------- Глобальные переменные и инициализация ----------
|
||||
const GROQ_API_KEY = '';
|
||||
const VERSION = 'v106';
|
||||
const VERSION = 'v107';
|
||||
|
||||
let mediaRecorder, audioChunks = [], userAudioBlob = null, ttsText = '';
|
||||
let isRecording = false, isAnalyzing = false, micAnalyser = null, micAnimId = null;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// ---------- Pronunciation Assessment ----------
|
||||
|
||||
async function assessPronunciation(expectedText, whisperResult) {
|
||||
try {
|
||||
const r = await fetch('/pronounce/assess', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
expected: expectedText,
|
||||
whisper: whisperResult,
|
||||
lang: 'it'
|
||||
})
|
||||
});
|
||||
if (!r.ok) { console.log('[PRONOUNCE] HTTP', r.status); return null; }
|
||||
const data = await r.json();
|
||||
console.log('[PRONOUNCE] score=' + data.overall_score + ' ' + data.quality);
|
||||
return data;
|
||||
} catch(e) {
|
||||
console.log('[PRONOUNCE] ERR', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderPronunciation(assess) {
|
||||
if (!assess) return '';
|
||||
|
||||
const s = assess.overall_score;
|
||||
const emoji = s >= 90 ? '🟢' : s >= 70 ? '🟡' : s >= 50 ? '🟠' : '🔴';
|
||||
|
||||
let html = '<div style="margin:10px 0;padding:12px;background:#1a1a2e;border-radius:8px;color:#e0e0e0">';
|
||||
|
||||
// Score bar
|
||||
const barColor = s >= 90 ? '#4caf50' : s >= 70 ? '#ff9800' : s >= 50 ? '#f44336' : '#9e9e9e';
|
||||
html += '<div style="display:flex;align-items:center;gap:12px;margin-bottom:10px">';
|
||||
html += '<span style="font-size:2em">' + emoji + '</span>';
|
||||
html += '<div style="flex:1">';
|
||||
html += '<div style="font-size:1.4em;font-weight:bold">Произношение: <span style="color:' + barColor + '">' + s + '%</span></div>';
|
||||
html += '<div style="color:#aaa;font-size:0.9em">' + (assess.quality || '') + '</div>';
|
||||
// Bar
|
||||
html += '<div style="height:6px;background:#333;border-radius:3px;margin-top:6px">';
|
||||
html += '<div style="width:' + s + '%;height:100%;background:' + barColor + ';border-radius:3px;transition:width 0.5s"></div></div>';
|
||||
html += '</div></div>';
|
||||
|
||||
// Details grid
|
||||
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:0.85em">';
|
||||
|
||||
// Phonemes
|
||||
if (assess.phoneme_comparison) {
|
||||
const pc = assess.phoneme_comparison;
|
||||
html += '<div><b>🔤 Фонемы:</b> ' + pc.accuracy + '%</div>';
|
||||
html += '<div><b>✓ Совпадений:</b> ' + (pc.matches || 0) + '/' + (pc.total_phonemes || 0) + '</div>';
|
||||
if (pc.errors && pc.errors.length > 0) {
|
||||
const subs = pc.errors.filter(e => e.type === 'sub');
|
||||
const dels = pc.errors.filter(e => e.type === 'del');
|
||||
const inss = pc.errors.filter(e => e.type === 'ins');
|
||||
html += '<div style="grid-column:1/-1">';
|
||||
if (subs.length) html += '<span style="color:#ff9800">Замен: ' + subs.map(e => e.expected + '→' + e.actual).join(', ') + '</span> ';
|
||||
if (dels.length) html += '<span style="color:#f44336">Пропущено: ' + dels.map(e => e.expected).join(', ') + '</span> ';
|
||||
if (inss.length) html += '<span style="color:#9c27b0">Лишних: ' + inss.map(e => e.actual).join(', ') + '</span>';
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Timing
|
||||
if (assess.timing) {
|
||||
const tm = assess.timing;
|
||||
html += '<div><b>⏱ Ритм:</b> ' + (tm.rhythm_score || 0) + '%</div>';
|
||||
html += '<div><b>📏 Длит-ть:</b> ' + (tm.total_duration || 0).toFixed(1) + 'с</div>';
|
||||
if (tm.timing_quality) {
|
||||
const tq = tm.timing_quality;
|
||||
html += '<div style="grid-column:1/-1;color:' + (tq === 'good' ? '#4caf50' : tq === 'ok' ? '#ff9800' : '#f44336') + '">';
|
||||
html += tq === 'good' ? '✅ Ритм ровный' : tq === 'ok' ? '⚠️ Ритм неровный' : '❌ Ритм сбит';
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Feedback
|
||||
if (assess.feedback) {
|
||||
html += '<div style="margin-top:8px;padding:6px 10px;background:#2a2a3e;border-radius:4px;font-size:0.9em;color:#ccc">';
|
||||
html += '💬 ' + assess.feedback;
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Phoneme detail
|
||||
if (assess.expected && assess.expected.phonemes) {
|
||||
html += '<div style="margin-top:6px;font-size:0.75em;color:#666">';
|
||||
html += '🎯 Эталон: /' + assess.expected.phonemes.join(' ') + '/';
|
||||
if (assess.actual && assess.actual.phonemes) {
|
||||
html += ' | 🗣 Вы: /' + assess.actual.phonemes.join(' ') + '/';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
Reference in New Issue
Block a user