Spaces:
Sleeping
Sleeping
| const micBtn = document.getElementById('micBtn'); | |
| const statusText = document.getElementById('statusText'); | |
| const visualizer = document.getElementById('visualizer'); | |
| const dropZone = document.getElementById('dropZone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| // Result Modal Elements | |
| const resultModal = document.getElementById('resultModal'); | |
| const closeModal = document.getElementById('closeModal'); | |
| const resultEmoji = document.getElementById('resultEmoji'); | |
| const resultLabel = document.getElementById('resultLabel'); | |
| const resultConfidence = document.getElementById('resultConfidence'); | |
| const btnCorrect = document.getElementById('btnCorrect'); | |
| const btnIncorrect = document.getElementById('btnIncorrect'); | |
| const correctionArea = document.getElementById('correctionArea'); | |
| const submitCorrection = document.getElementById('submitCorrection'); | |
| let mediaRecorder; | |
| let audioChunks = []; | |
| let currentTempFilename = null; | |
| let currentPrediction = null; | |
| // Emotion to Emoji Map | |
| const emotionEmojis = { | |
| 'neutral': 'π', | |
| 'calm': 'π', | |
| 'happiness': 'π', | |
| 'happy': 'π', | |
| 'sadness': 'π’', | |
| 'sad': 'π’', | |
| 'anger': 'π ', | |
| 'angry': 'π ', | |
| 'fear': 'π±', | |
| 'disgust': 'π€’', | |
| 'surprise': 'π²' | |
| }; | |
| // --- Recording Logic --- | |
| micBtn.addEventListener('mousedown', startRecording); | |
| micBtn.addEventListener('mouseup', stopRecording); | |
| micBtn.addEventListener('mouseleave', () => { | |
| if (mediaRecorder && mediaRecorder.state === 'recording') { | |
| stopRecording(); | |
| } | |
| }); | |
| // Training Logic | |
| const trainingModal = document.getElementById('trainingModal'); | |
| const trainingLog = document.getElementById('trainingLog'); | |
| const closeTrainingModal = document.getElementById('closeTrainingModal'); | |
| closeTrainingModal.addEventListener('click', () => { | |
| trainingModal.classList.add('hidden'); | |
| }); | |
| // --- Training & Password Logic --- | |
| const passwordModal = document.getElementById('passwordModal'); | |
| const closePasswordModal = document.getElementById('closePasswordModal'); | |
| const submitPasswordBtn = document.getElementById('submitPasswordBtn'); | |
| const adminPasswordInput = document.getElementById('adminPasswordInput'); | |
| // Open Password Modal | |
| document.getElementById('trainBtn').addEventListener('click', () => { | |
| passwordModal.classList.remove('hidden'); | |
| adminPasswordInput.value = ''; | |
| adminPasswordInput.focus(); | |
| }); | |
| // Close Password Modal | |
| closePasswordModal.addEventListener('click', () => { | |
| passwordModal.classList.add('hidden'); | |
| }); | |
| // Handle Password Submission | |
| // Handle Password Submission | |
| function submitPassword() { | |
| const password = adminPasswordInput.value; | |
| if (!password) { | |
| showToast("Please enter a password", "error"); | |
| return; | |
| } | |
| passwordModal.classList.add('hidden'); | |
| startTraining(password); | |
| } | |
| submitPasswordBtn.addEventListener('click', submitPassword); | |
| // Allow Enter key to submit password and Esc to close modals | |
| document.addEventListener('keydown', (e) => { | |
| // Enter Key in Password Input | |
| if (e.key === 'Enter' && document.activeElement === adminPasswordInput) { | |
| submitPassword(); | |
| } | |
| // Escape Key Global | |
| if (e.key === 'Escape') { | |
| passwordModal.classList.add('hidden'); | |
| resultModal.classList.add('hidden'); | |
| trainingModal.classList.add('hidden'); | |
| } | |
| }); | |
| async function startTraining(password) { | |
| // Open Terminal | |
| trainingModal.classList.remove('hidden'); | |
| trainingLog.innerHTML = '<span class="log-line">Authenticating...</span>'; | |
| try { | |
| const response = await fetch('/train', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ password: password }) | |
| }); | |
| if (response.status === 401) { | |
| trainingLog.innerHTML += '<span class="log-line" style="color:red">Error: Unauthorized. Incorrect Password.</span>'; | |
| showToast("Incorrect Admin Password", "error"); | |
| return; | |
| } | |
| const data = await response.json(); | |
| if (data.status === 'training_started') { | |
| trainingLog.innerHTML += '<span class="log-line">Access Granted. Starting training sequence...</span>'; | |
| pollLogs(); | |
| } | |
| } catch (e) { | |
| showToast("Failed to start training.", "error"); | |
| trainingLog.innerHTML += `<span class="log-line" style="color:red">Error: ${e.message}</span>`; | |
| } | |
| } | |
| async function pollLogs(startIndex = 0) { | |
| try { | |
| const response = await fetch(`/logs?after=${startIndex}`); | |
| const data = await response.json(); | |
| if (data.logs && data.logs.length > 0) { | |
| data.logs.forEach(log => { | |
| const line = document.createElement('span'); | |
| line.className = 'log-line'; | |
| line.innerText = log; | |
| if (log.includes("CRITICAL") || log.includes("Error")) line.style.color = '#ff5555'; | |
| if (log.includes("Success") || log.includes("complete")) line.style.color = '#55ff55'; | |
| trainingLog.appendChild(line); | |
| }); | |
| // Auto scroll | |
| trainingLog.scrollTop = trainingLog.scrollHeight; | |
| } | |
| // Continue polling if not complete (simple check: if logs stop or specific message?) | |
| // Better: The backend just keeps logs coming. We'll poll until we see "Training complete" | |
| const lastLog = data.logs.length > 0 ? data.logs[data.logs.length - 1] : ""; | |
| if (lastLog.includes("Training complete")) { | |
| trainingLog.innerHTML += '<span class="log-line">>> Process finished. You may close this window.</span>'; | |
| return; | |
| } | |
| setTimeout(() => pollLogs(data.next_index), 500); // Poll every 500ms | |
| } catch (e) { | |
| console.error("Polling error", e); | |
| setTimeout(() => pollLogs(startIndex), 2000); // Retry slower on error | |
| } | |
| } | |
| // Touch support for mobile | |
| micBtn.addEventListener('touchstart', (e) => { e.preventDefault(); startRecording(); }); | |
| micBtn.addEventListener('touchend', (e) => { e.preventDefault(); stopRecording(); }); | |
| function startRecording() { | |
| statusText.innerText = "Recording..."; | |
| micBtn.classList.add('recording'); | |
| visualizer.classList.remove('hidden'); | |
| audioChunks = []; | |
| navigator.mediaDevices.getUserMedia({ audio: true }) | |
| .then(stream => { | |
| mediaRecorder = new MediaRecorder(stream); | |
| mediaRecorder.start(); | |
| mediaRecorder.addEventListener("dataavailable", event => { | |
| audioChunks.push(event.data); | |
| }); | |
| mediaRecorder.addEventListener("stop", () => { | |
| const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); // Default typically webm, assumes backend handles it or we send as file | |
| // Usually comes as webm/ogg from browser. We'll verify mimetype. | |
| uploadAudio(audioBlob, "recording.wav"); // Naming it .wav but content might be webm, backend pydub handles it. | |
| }); | |
| }) | |
| .catch(err => { | |
| console.error("Error accessing mic:", err); | |
| statusText.innerText = "Error Accessing Mic"; | |
| }); | |
| } | |
| function stopRecording() { | |
| if (mediaRecorder && mediaRecorder.state !== 'inactive') { | |
| mediaRecorder.stop(); | |
| statusText.innerText = "Processing..."; | |
| micBtn.classList.remove('recording'); | |
| visualizer.classList.add('hidden'); | |
| } | |
| } | |
| // --- File Upload Logic --- | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| handleFile(e.target.files[0]); | |
| } | |
| }); | |
| dropZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.add('dragover'); | |
| }); | |
| dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); | |
| dropZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('dragover'); | |
| if (e.dataTransfer.files.length > 0) { | |
| handleFile(e.dataTransfer.files[0]); | |
| } | |
| }); | |
| function handleFile(file) { | |
| statusText.innerText = `Uploading ${file.name}...`; | |
| uploadAudio(file, file.name); | |
| } | |
| // --- Toast Notifications --- | |
| function showToast(message, type = 'info') { | |
| const container = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| // Icon selection | |
| let icon = 'fa-info-circle'; | |
| if (type === 'success') icon = 'fa-check-circle'; | |
| if (type === 'error') icon = 'fa-exclamation-circle'; | |
| toast.innerHTML = ` | |
| <i class="fa-solid ${icon}"></i> | |
| <span>${message}</span> | |
| `; | |
| container.appendChild(toast); | |
| // Auto remove | |
| setTimeout(() => { | |
| toast.classList.add('hide'); | |
| toast.addEventListener('animationend', () => toast.remove()); | |
| }, 3000); | |
| } | |
| // --- API Calls --- | |
| async function uploadAudio(fileOrBlob, filename) { | |
| const formData = new FormData(); | |
| formData.append("file", fileOrBlob, filename); // Append file | |
| try { | |
| const response = await fetch('/predict', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| const errData = await response.json(); | |
| throw new Error(errData.detail || "Prediction failed"); | |
| } | |
| const data = await response.json(); | |
| showResult(data); | |
| statusText.innerText = "Click & Hold to Record"; | |
| showToast("Analysis Complete", "success"); | |
| } catch (error) { | |
| console.error(error); | |
| statusText.innerText = "Error: " + error.message; | |
| showToast("Error: " + error.message, "error"); | |
| } | |
| } | |
| function showResult(data) { | |
| currentTempFilename = data.temp_filename; | |
| currentPrediction = data.prediction; | |
| resultEmoji.innerText = emotionEmojis[data.prediction.toLowerCase()] || 'β'; | |
| resultLabel.innerText = data.prediction.charAt(0).toUpperCase() + data.prediction.slice(1); | |
| resultConfidence.innerText = `Confidence: ${(data.confidence * 100).toFixed(1)}%`; | |
| // Reset feedback UI | |
| correctionArea.classList.add('hidden'); | |
| resultModal.classList.remove('hidden'); | |
| if (data.is_fallback) { | |
| showToast("Model not trained. Please label this audio to build the dataset.", "info"); | |
| correctionArea.classList.remove('hidden'); | |
| resultLabel.innerText = "Label Required"; | |
| resultEmoji.innerText = "π·οΈ"; | |
| resultConfidence.innerText = "Help the AI learn!"; | |
| } | |
| // --- NLP Analysis Display --- | |
| let nlpDiv = document.getElementById('nlp-results'); | |
| if (!nlpDiv) { | |
| nlpDiv = document.createElement('div'); | |
| nlpDiv.id = 'nlp-results'; | |
| nlpDiv.className = 'nlp-container'; | |
| // Insert before feedback section | |
| const feedbackSection = resultModal.querySelector('.feedback-section'); | |
| resultModal.querySelector('.modal-content').insertBefore(nlpDiv, feedbackSection); | |
| } | |
| // Clear previous | |
| nlpDiv.innerHTML = ''; | |
| if (data.nlp_analysis && data.nlp_analysis.transcription) { | |
| const textEmotion = data.nlp_analysis.text_emotion; | |
| const confidencePct = (textEmotion.score * 100).toFixed(1); | |
| // Show Hybrid Breakdown | |
| nlpDiv.innerHTML = ` | |
| <div class="divider">Hybrid Analysis</div> | |
| <p class="transcription">"${data.nlp_analysis.transcription}"</p> | |
| <div class="breakdown-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; font-size: 0.9rem;"> | |
| <div class="breakdown-item"> | |
| <div style="color: #94a3b8;">Audio Tone</div> | |
| <div class="highlight">${data.audio_emotion.label}</div> | |
| <div class="confidence-small">${(data.audio_emotion.confidence * 100).toFixed(1)}%</div> | |
| </div> | |
| <div class="breakdown-item"> | |
| <div style="color: #94a3b8;">Text Context</div> | |
| <div class="highlight">${textEmotion.label}</div> | |
| <div class="confidence-small">${confidencePct}%</div> | |
| </div> | |
| </div> | |
| <div style="margin-top: 10px; font-size: 0.8rem; color: #64748b;"> | |
| Result fused from acoustic and semantic models. | |
| </div> | |
| `; | |
| } else { | |
| nlpDiv.innerHTML = ` | |
| <div class="divider">Context Analysis</div> | |
| <p style="color: #64748b; font-style: italic;">No speech detected or analysis unavailable.</p> | |
| `; | |
| } | |
| } | |
| // --- Modal & Feedback --- | |
| closeModal.addEventListener('click', () => resultModal.classList.add('hidden')); | |
| window.onclick = (event) => { | |
| if (event.target == resultModal) resultModal.classList.add('hidden'); | |
| }; | |
| btnCorrect.addEventListener('click', () => { | |
| submitFeedback(currentPrediction); | |
| }); | |
| btnIncorrect.addEventListener('click', () => { | |
| correctionArea.classList.remove('hidden'); | |
| }); | |
| submitCorrection.addEventListener('click', () => { | |
| const selected = document.getElementById('emotionSelect').value; | |
| if (selected) { | |
| submitFeedback(selected); | |
| } | |
| }); | |
| async function submitFeedback(correctLabel) { | |
| try { | |
| const response = await fetch('/feedback', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| filename: currentTempFilename, | |
| original_emotion: currentPrediction, | |
| correct_emotion: correctLabel | |
| }) | |
| }); | |
| const res = await response.json(); | |
| if (res.status === 'success') { | |
| showToast("Feedback saved successfully!", "success"); | |
| resultModal.classList.add('hidden'); | |
| } | |
| } catch (e) { | |
| showToast("Failed to save feedback.", "error"); | |
| } | |
| } | |