From 450e95eb19f66271427236b5c9b48a353a09109a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 23 May 2026 18:08:28 +0400 Subject: [PATCH] =?UTF-8?q?v107:=20pronunciation=20assessment=20UI=20?= =?UTF-8?q?=E2=80=94=20score,=20phonemes,=20rhythm,=20feedback=20=D0=B2=20?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D1=83=D0=B7=D0=B5=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.sh | 2 +- dist/index.html | 116 ++++++++++++++++++++++++++++++++++++++++++++++-- index.html | 2 + js/compare.js | 12 ++++- js/main.js | 2 +- js/pronounce.js | 98 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 js/pronounce.js diff --git a/build.sh b/build.sh index cbd5180..d4c1ce2 100755 --- a/build.sh +++ b/build.sh @@ -9,7 +9,7 @@ css = open(os.path.join(BASE, 'css/style.css')).read() html = html.replace('', f'') -for jsf in ['storage.js', 'syllables.js', 'transcribe.js', 'audio.js', 'compare.js', 'main.js']: +for jsf in ['storage.js', 'syllables.js', 'transcribe.js', 'pronounce.js', 'audio.js', 'compare.js', 'main.js']: js = open(os.path.join(BASE, 'js', jsf)).read() html = html.replace(f'', f'') diff --git a/dist/index.html b/dist/index.html index b44494d..032dc04 100644 --- a/dist/index.html +++ b/dist/index.html @@ -48,6 +48,7 @@ button:disabled { background:#ccc; cursor:not-allowed; }
+
@@ -432,6 +433,107 @@ async function translateToRussian(text) { } catch(e) { /* тихо */ } } + + + diff --git a/js/compare.js b/js/compare.js index f746385..8d2a124 100644 --- a/js/compare.js +++ b/js/compare.js @@ -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%' + '' + 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 перезаписывается) diff --git a/js/main.js b/js/main.js index cfd4817..1db07b1 100644 --- a/js/main.js +++ b/js/main.js @@ -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; diff --git a/js/pronounce.js b/js/pronounce.js new file mode 100644 index 0000000..11c9345 --- /dev/null +++ b/js/pronounce.js @@ -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 = '
'; + + // Score bar + const barColor = s >= 90 ? '#4caf50' : s >= 70 ? '#ff9800' : s >= 50 ? '#f44336' : '#9e9e9e'; + html += '
'; + html += '' + emoji + ''; + html += '
'; + html += '
Произношение: ' + s + '%
'; + html += '
' + (assess.quality || '') + '
'; + // Bar + html += '
'; + html += '
'; + html += '
'; + + // Details grid + html += '
'; + + // Phonemes + if (assess.phoneme_comparison) { + const pc = assess.phoneme_comparison; + html += '
🔤 Фонемы: ' + pc.accuracy + '%
'; + html += '
✓ Совпадений: ' + (pc.matches || 0) + '/' + (pc.total_phonemes || 0) + '
'; + 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 += '
'; + if (subs.length) html += 'Замен: ' + subs.map(e => e.expected + '→' + e.actual).join(', ') + ' '; + if (dels.length) html += 'Пропущено: ' + dels.map(e => e.expected).join(', ') + ' '; + if (inss.length) html += 'Лишних: ' + inss.map(e => e.actual).join(', ') + ''; + html += '
'; + } + } + + // Timing + if (assess.timing) { + const tm = assess.timing; + html += '
⏱ Ритм: ' + (tm.rhythm_score || 0) + '%
'; + html += '
📏 Длит-ть: ' + (tm.total_duration || 0).toFixed(1) + 'с
'; + if (tm.timing_quality) { + const tq = tm.timing_quality; + html += '
'; + html += tq === 'good' ? '✅ Ритм ровный' : tq === 'ok' ? '⚠️ Ритм неровный' : '❌ Ритм сбит'; + html += '
'; + } + } + + html += '
'; + + // Feedback + if (assess.feedback) { + html += '
'; + html += '💬 ' + assess.feedback; + html += '
'; + } + + // Phoneme detail + if (assess.expected && assess.expected.phonemes) { + html += '
'; + html += '🎯 Эталон: /' + assess.expected.phonemes.join(' ') + '/'; + if (assess.actual && assess.actual.phonemes) { + html += ' | 🗣 Вы: /' + assess.actual.phonemes.join(' ') + '/'; + } + html += '
'; + } + + html += '
'; + return html; +}