Let's go!
Read more
// Простые настройки const BACKEND_URL = (new URLSearchParams(location.search).get("api") || "").trim() || "http://localhost:8000"; const LESSON_ID = (new URLSearchParams(location.search).get("lesson") || "L01").trim(); let lessonData = null; let itemIndex = 0; let mediaRecorder = null; let chunks = []; const lessonTitle = document.getElementById("lessonTitle"); const itemCounter = document.getElementById("itemCounter"); const promptEl = document.getElementById("prompt"); const tipsEl = document.getElementById("tips"); const feedbackEl = document.getElementById("feedback"); const player = document.getElementById("player"); const buddyAudio = document.getElementById("buddyAudio"); const recBtn = document.getElementById("recBtn"); const nextBtn = document.getElementById("nextBtn"); const repeatBtn = document.getElementById("repeatBtn"); const progressBar = document.getElementById("progressBar"); const buddySpeakBtn = document.getElementById("buddySpeakBtn"); async function loadLesson() { const res = await fetch(`./lessons/${LESSON_ID}.json`); if (!res.ok) { promptEl.innerHTML = "Не найден файл урока. Проверьте параметр ?lesson= и наличие JSON."; return; } lessonData = await res.json(); lessonTitle.textContent = `${lessonData.title || LESSON_ID}`; itemIndex = 0; showItem(); } function showItem() { if (!lessonData) return; const items = lessonData.items || []; const item = items[itemIndex]; itemCounter.textContent = `${itemIndex + 1}/${items.length}`; promptEl.textContent = item.prompt || ""; tipsEl.textContent = (item.tips || []).join(" • "); feedbackEl.textContent = ""; progressBar.style.width = `${Math.round(((itemIndex) / items.length) * 100)}%`; nextBtn.disabled = true; repeatBtn.disabled = true; buddySpeakBtn.disabled = true; buddyAudio.src = ""; } async function startRecording() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaRecorder = new MediaRecorder(stream, { mimeType: "audio/webm" }); chunks = []; mediaRecorder.ondataavailable = (e) => chunks.push(e.data); mediaRecorder.onstop = async () => { const blob = new Blob(chunks, { type: "audio/webm" }); player.src = URL.createObjectURL(blob); const form = new FormData(); const refText = (lessonData.items[itemIndex].reference_text || "").trim(); form.append("audio", blob, "speech.webm"); form.append("lesson_id", LESSON_ID); form.append("item_index", String(itemIndex)); form.append("reference_text", refText); feedbackEl.textContent = "⏳ Оцениваем произношение…"; try { const res = await fetch(`${BACKEND_URL}/assess`, { method: "POST", body: form }); const data = await res.json(); const lines = []; lines.push(`Точность: ${data.score_accuracy ?? "-"}%`); (data.diagnostics || []).forEach(d => { lines.push(`• ${d.target}: ${d.issue} — ${d.advice}`); }); feedbackEl.textContent = lines.join("\n"); // Синтез ответа Бади голосом (по желанию) const reply = data.reply || "Хорошая попытка! Попробуй ещё раз — удлиняй гласный и будь внимательнее к конечному согласному."; buddySpeakBtn.onclick = async () => { const tform = new FormData(); tform.append("text", reply); const tres = await fetch(`${BACKEND_URL}/tts`, { method: "POST", body: tform }); const tdata = await tres.json(); if (tdata.url) { buddyAudio.src = tdata.url; } else if (tdata.inline_base64) { buddyAudio.src = `data:audio/mp3;base64,${tdata.inline_base64}`; } }; buddySpeakBtn.disabled = false; nextBtn.disabled = false; repeatBtn.disabled = false; } catch (e) { feedbackEl.textContent = "Ошибка при оценке. Проверь BACKEND_URL и доступность сервера."; } }; mediaRecorder.start(); recBtn.textContent = "■ Стоп"; } catch (e) { alert("Браузер не дал доступ к микрофону. Откройте страницу напрямую (не в приватном режиме) и разрешите микрофон."); } } function stopRecording() { if (mediaRecorder && mediaRecorder.state !== "inactive") { mediaRecorder.stop(); recBtn.textContent = "● Записать"; } } recBtn.onclick = () => { if (!mediaRecorder || mediaRecorder.state === "inactive") startRecording(); else stopRecording(); }; nextBtn.onclick = () => { const total = (lessonData.items || []).length; if (itemIndex < total - 1) { itemIndex += 1; showItem(); } else { progressBar.style.width = "100%"; feedbackEl.textContent = "Готово! Урок завершён ✅"; nextBtn.disabled = true; repeatBtn.disabled = true; } }; repeatBtn.onclick = () => { showItem(); }; loadLesson();
Made on
Tilda