Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Audio Classification Workstation</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| /* Custom scrollbar for history (WebKit browsers) */ | |
| .history-scrollbar::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .history-scrollbar::-webkit-scrollbar-track { | |
| background: #1f2937; /* bg-gray-800 */ | |
| } | |
| .history-scrollbar::-webkit-scrollbar-thumb { | |
| background: #4b5563; /* bg-gray-600 */ | |
| border-radius: 4px; | |
| } | |
| .history-scrollbar::-webkit-scrollbar-thumb:hover { | |
| background: #6b7280; /* bg-gray-500 */ | |
| } | |
| /* Icon styling */ | |
| .icon { | |
| width: 1.25rem; /* 20px */ | |
| height: 1.25rem; /* 20px */ | |
| display: inline-block; | |
| vertical-align: middle; | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| display: inline-block; | |
| margin-right: 0.5rem; | |
| } | |
| .tag { | |
| display: inline-block; | |
| background-color: #374151; /* bg-gray-700 */ | |
| color: #d1d5db; /* text-gray-300 */ | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 9999px; /* rounded-full */ | |
| font-size: 0.75rem; /* text-xs */ | |
| margin: 0.25rem; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-gray-200 min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="bg-gray-800 shadow-md"> | |
| <div class="container mx-auto px-6 py-3 flex justify-between items-center"> | |
| <h1 class="text-xl font-semibold text-white">Audio Classification Workstation</h1> | |
| <div class="flex items-center space-x-3"> | |
| <button title="Help" class="text-gray-400 hover:text-white"> | |
| <svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.79 4 4s-1.79 4-4 4c-1.742 0-3.223-.835-3.772-2M9 12l3 3m0 0l3-3m-3 3v6m-1.732-8.066A8.969 8.969 0 015.34 6.309m13.42 2.592a8.969 8.969 0 01-2.888 2.592m0 0A8.968 8.968 0 0112 21c-2.485 0-4.733-.985-6.364-2.592m12.728 0A8.969 8.969 0 0121.66 9.63m-16.022-.098A8.969 8.969 0 013.34 6.309m1.991 11.808A8.969 8.969 0 015.34 17.69m13.42-2.592a8.969 8.969 0 012.888-2.592M9 12a3 3 0 11-6 0 3 3 0 016 0z" /> | |
| </svg> | |
| </button> | |
| <button title="Settings" class="text-gray-400 hover:text-white"> | |
| <svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content Area --> | |
| <main class="flex-grow container mx-auto px-6 py-4 grid grid-cols-1 md:grid-cols-3 gap-6"> | |
| <!-- Left Column --> | |
| <div class="md:col-span-1 space-y-6"> | |
| <!-- Audio Input --> | |
| <div class="bg-gray-800 p-5 rounded-lg shadow-lg"> | |
| <h2 class="text-lg font-semibold text-gray-100 mb-4">Audio Input</h2> | |
| <div class="space-y-3"> | |
| <div> | |
| <label for="audioSource" class="block text-sm font-medium text-gray-300 mb-1">Input Device</label> | |
| <select id="audioSource" name="audioSource" class="w-full bg-gray-700 border border-gray-600 text-gray-200 rounded-md p-2 focus:ring-green-500 focus:border-green-500 text-sm"> | |
| <option>Default Microphone</option> | |
| <!-- More options could be populated by JS --> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-300 mb-1">Audio Level</label> | |
| <div class="w-full bg-gray-700 rounded-full h-2.5"> | |
| <div class="bg-green-500 h-2.5 rounded-full" style="width: 45%"></div> <!-- Placeholder level --> | |
| </div> | |
| </div> | |
| <button id="toggleRecordButton" class="w-full bg-gradient-to-br from-green-500 to-green-700 hover:from-green-600 hover:to-green-800 text-white font-bold py-2.5 px-4 rounded-lg shadow-md transition duration-150 ease-in-out transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:ring-offset-gray-800 flex items-center justify-center space-x-2"> | |
| <svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" /> | |
| </svg> | |
| <span>Start Recording</span> | |
| </button> | |
| <p class="text-xs text-gray-400 text-center">Recording Time: <span id="timer">0s</span></p> | |
| </div> | |
| </div> | |
| <!-- Upload Audio File --> | |
| <div class="bg-gray-800 p-5 rounded-lg shadow-lg"> | |
| <h2 class="text-lg font-semibold text-gray-100 mb-4">Upload Audio File</h2> | |
| <div class="space-y-3"> | |
| <div class="border-2 border-dashed border-gray-600 rounded-lg p-6 text-center cursor-pointer hover:border-gray-500"> | |
| <svg class="icon mx-auto mb-2 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> | |
| </svg> | |
| <p class="text-sm text-gray-400">Drag & drop an audio file or</p> | |
| <input type="file" id="audioFile" accept="audio/*" class="hidden"> | |
| <button id="browseButton" class="mt-2 text-sm text-green-400 hover:text-green-300 font-semibold">Browse Files</button> | |
| <p class="text-xs text-gray-500 mt-1">Supported formats: MP3, WAV, OGG, FLAC</p> | |
| </div> | |
| <button id="uploadButton" class="w-full bg-gradient-to-br from-blue-500 to-blue-700 hover:from-blue-600 hover:to-blue-800 text-white font-bold py-2.5 px-4 rounded-lg shadow-md transition duration-150 ease-in-out transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-800 flex items-center justify-center space-x-2"> | |
| <svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="width:18px; height:18px;"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /> | |
| </svg> | |
| <span>Upload and Recognize</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Model Information --> | |
| <div class="bg-gray-800 p-5 rounded-lg shadow-lg"> | |
| <h2 class="text-lg font-semibold text-gray-100 mb-4">Model Information</h2> | |
| <div class="space-y-2 text-sm"> | |
| <p><strong class="text-gray-300">Current Model:</strong> <span class="text-gray-400">Deep Learning Model</span></p> | |
| <div class="flex flex-wrap items-center"> | |
| <strong class="text-gray-300 mr-2">Supported Categories:</strong> | |
| <span class="tag">Music</span><span class="tag">Humming</span><span class="tag">Custom Audio</span> | |
| </div> | |
| <p><strong class="text-gray-300">Status:</strong> <span class="status-dot bg-green-500"></span><span class="text-green-400">Ready for processing</span></p> | |
| <button class="mt-2 text-sm text-green-400 hover:text-green-300 font-semibold">View Model Details</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Middle Column --> | |
| <div class="md:col-span-1 bg-gray-800 p-5 rounded-lg shadow-lg flex flex-col"> | |
| <h2 class="text-lg font-semibold text-gray-100 mb-4 flex items-center"> | |
| <svg class="icon mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" /> | |
| </svg> | |
| Classification Results | |
| <span id="classificationStatus" class="ml-auto text-xs py-1 px-2.5 rounded-full bg-gray-700 text-gray-300">Ready</span> | |
| </h2> | |
| <div id="results" class="flex-grow bg-gray-700 p-4 rounded-md min-h-[200px] text-gray-300 overflow-auto"> | |
| <p class="italic text-gray-400">Record or upload audio to start classification.</p> | |
| </div> | |
| </div> | |
| <!-- Right Column --> | |
| <div class="md:col-span-1 bg-gray-800 p-5 rounded-lg shadow-lg flex flex-col"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-lg font-semibold text-gray-100">Chat with AI</h2> | |
| <button title="Clear Chat" id="clearChatButton" class="text-gray-400 hover:text-white"> | |
| <svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
| </svg> | |
| </button> | |
| </div> | |
| <div id="chatContainer" class="flex-grow space-y-3 overflow-y-auto history-scrollbar pr-1 min-h-[200px] mb-4"> | |
| <div class="text-center text-gray-500 pt-10"> | |
| <svg class="icon mx-auto mb-2 text-gray-500 w-10 h-10" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /> | |
| </svg> | |
| <p>Start a conversation about the classification results.</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <input type="text" id="chatInput" placeholder="Type your message..." class="flex-grow bg-gray-700 border border-gray-600 text-gray-200 rounded-md p-2 focus:ring-green-500 focus:border-green-500 text-sm"> | |
| <button id="sendMessageButton" class="p-2 text-gray-400 hover:text-white bg-gray-700 rounded-md hover:bg-gray-600"> | |
| <svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| // DOM Elements | |
| const toggleRecordButton = document.getElementById('toggleRecordButton'); | |
| const timerDisplay = document.getElementById('timer'); | |
| const resultsDiv = document.getElementById('results'); | |
| const classificationStatus = document.getElementById('classificationStatus'); | |
| const audioFileInput = document.getElementById('audioFile'); | |
| const uploadButton = document.getElementById('uploadButton'); | |
| const browseButton = document.getElementById('browseButton'); | |
| const chatContainer = document.getElementById('chatContainer'); | |
| const chatInput = document.getElementById('chatInput'); | |
| const sendMessageButton = document.getElementById('sendMessageButton'); | |
| const clearChatButton = document.getElementById('clearChatButton'); | |
| // Recording state and logic | |
| let mediaRecorder; | |
| let audioChunks = []; | |
| let recognitionIntervalMs = 5000; | |
| let periodicRecognitionTimer; | |
| let recordingStartTime; | |
| let durationUpdateTimer; | |
| let isRecording = false; | |
| let currentClassificationResult = null; | |
| // --- Existing Helper Functions (Slightly Modified) --- | |
| function updateTimerDisplay() { | |
| if (!recordingStartTime) return; | |
| const secondsElapsed = Math.floor((Date.now() - recordingStartTime) / 1000); | |
| timerDisplay.textContent = String(secondsElapsed) + 's'; | |
| } | |
| function setButtonState(recording) { | |
| isRecording = recording; | |
| const iconSVG = toggleRecordButton.querySelector('svg'); | |
| const textSpan = toggleRecordButton.querySelector('span'); | |
| if (isRecording) { | |
| toggleRecordButton.classList.remove('from-green-500', 'to-green-700', 'hover:from-green-600', 'hover:to-green-800', 'focus:ring-green-500'); | |
| toggleRecordButton.classList.add('from-red-500', 'to-red-700', 'hover:from-red-600', 'hover:to-red-800', 'focus:ring-red-500'); | |
| textSpan.textContent = 'Stop Recording'; | |
| iconSVG.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12v0a9 9 0 01-9 9m0-9a9 9 0 00-9 9m9-9V3m0 0a9 9 0 00-9 9m9-9h1.5M3 12h1.5m15 0V3m0 0a9 9 0 00-9-9m9 9c1.657 0 3-4.03 3-9" transform="matrix(1 0 0 1 0 0) rotate(0 12 12)" style="display: none;"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" transform="matrix(1 0 0 1 0 0) rotate(0 12 12)"></path>'; // Stop Icon (X) | |
| resultsDiv.innerHTML = '<p class="italic text-gray-400">Listening...</p>'; | |
| classificationStatus.textContent = 'Listening'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-yellow-600 text-yellow-100'; | |
| recordingStartTime = Date.now(); | |
| updateTimerDisplay(); | |
| durationUpdateTimer = setInterval(updateTimerDisplay, 1000); | |
| } else { | |
| toggleRecordButton.classList.remove('from-red-500', 'to-red-700', 'hover:from-red-600', 'hover:to-red-800', 'focus:ring-red-500'); | |
| toggleRecordButton.classList.add('from-green-500', 'to-green-700', 'hover:from-green-600', 'hover:to-green-800', 'focus:ring-green-500'); | |
| textSpan.textContent = 'Start Recording'; | |
| iconSVG.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />'; // Mic Icon | |
| clearInterval(durationUpdateTimer); | |
| recordingStartTime = null; | |
| timerDisplay.textContent = '0s'; | |
| if (periodicRecognitionTimer) clearInterval(periodicRecognitionTimer); | |
| } | |
| } | |
| async function startLiveRecording() { | |
| if (isRecording) return; // Should not happen if button state is managed | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| setButtonState(true); | |
| const options = { | |
| mimeType: 'audio/webm;codecs=opus', | |
| audioBitsPerSecond: 128000 | |
| }; | |
| try { | |
| mediaRecorder = new MediaRecorder(stream, options); | |
| } catch (e1) { | |
| console.warn('Failed to create MediaRecorder with audio/webm;codecs=opus: ' + e1.message + '. Trying with default.'); | |
| options.mimeType = ''; | |
| mediaRecorder = new MediaRecorder(stream, options); | |
| } | |
| console.log('Using mimeType:', mediaRecorder.mimeType); | |
| audioChunks = []; | |
| mediaRecorder.addEventListener('dataavailable', event => { | |
| audioChunks.push(event.data); | |
| }); | |
| mediaRecorder.addEventListener('stop', async () => { | |
| stream.getTracks().forEach(track => track.stop()); | |
| setButtonState(false); // Reset button state | |
| if (audioChunks.length > 0) { | |
| await sendAudioChunkForRecognition(true); // isFinalChunk = true | |
| } else { | |
| resultsDiv.innerHTML = '<p class="italic text-gray-400">Recording stopped. No audio data.</p>'; | |
| classificationStatus.textContent = 'Ready'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-gray-700 text-gray-300'; | |
| } | |
| }); | |
| mediaRecorder.start(); | |
| // Send first chunk after a short delay | |
| setTimeout(async () => { | |
| if (mediaRecorder.state === 'recording') await sendAudioChunkForRecognition(); | |
| }, 2000); | |
| // Setup periodic recognition | |
| periodicRecognitionTimer = setInterval(async () => { | |
| if (mediaRecorder.state === 'recording' && audioChunks.length > 0) { | |
| await sendAudioChunkForRecognition(); | |
| } | |
| }, recognitionIntervalMs); | |
| } catch (err) { | |
| console.error('Error accessing microphone:', err); | |
| resultsDiv.innerHTML = '<p class="text-red-400">Could not access microphone. Please ensure permission is granted.</p>'; | |
| classificationStatus.textContent = 'Error'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-red-700 text-red-100'; | |
| setButtonState(false); // Reset button state on error | |
| } | |
| } | |
| function stopLiveRecording() { | |
| if (mediaRecorder && mediaRecorder.state === 'recording') { | |
| mediaRecorder.stop(); | |
| } | |
| // setButtonState(false) is called by mediaRecorder 'stop' event listener | |
| if (periodicRecognitionTimer) clearInterval(periodicRecognitionTimer); | |
| if (durationUpdateTimer) clearInterval(durationUpdateTimer); | |
| // recordingStartTime will be reset by setButtonState via mediaRecorder 'stop' | |
| } | |
| toggleRecordButton.addEventListener('click', () => { | |
| if (!isRecording) { | |
| startLiveRecording(); | |
| } else { | |
| stopLiveRecording(); | |
| } | |
| }); | |
| async function sendAudioChunkForRecognition(isFinalChunk = false) { | |
| if (audioChunks.length === 0 && !isFinalChunk) return; | |
| const audioBlob = new Blob(audioChunks, { type: mediaRecorder.mimeType || 'audio/webm;codecs=opus' }); | |
| let tempAudioChunks = [...audioChunks]; | |
| audioChunks = []; | |
| if (!isFinalChunk && tempAudioChunks.length === 0) return; | |
| // Update status for intermediate chunks if not already showing success | |
| if (!isFinalChunk && !resultsDiv.querySelector('.text-green-400')) { | |
| resultsDiv.innerHTML = '<p class="italic text-gray-400">Processing audio...</p>'; | |
| classificationStatus.textContent = 'Processing'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-blue-600 text-blue-100'; | |
| } | |
| const formData = new FormData(); | |
| const fileExtension = (mediaRecorder.mimeType.split('/')[1]?.split(';')[0]) || 'webm'; | |
| formData.append('file', audioBlob, 'live_audio_chunk.' + fileExtension); | |
| try { | |
| const response = await fetch('/recognize-live-chunk/', { | |
| method: 'POST', | |
| body: formData, | |
| }); | |
| const result = await response.json(); | |
| displayCombinedResults(result, isFinalChunk); | |
| } catch (error) { | |
| console.error('Error sending audio chunk:', error); | |
| resultsDiv.innerHTML = | |
| '<p class="text-red-400">Error sending audio data.</p>' + | |
| '<p class="text-xs text-gray-500">' + error.message + '</p>'; | |
| classificationStatus.textContent = 'Error'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-red-700 text-red-100'; | |
| } | |
| } | |
| function displayCombinedResults(result, isFinalChunk) { | |
| let html = ''; | |
| if (result.success) { | |
| currentClassificationResult = result; // Store the current classification result | |
| if (result.type === 'music') { | |
| // Display song recognition results | |
| const musicResult = result.music_result; | |
| html += '<div class="mb-4">'; | |
| html += '<p class="text-green-400 font-semibold text-lg mb-2">Song Match Found!</p>'; | |
| if (musicResult.song_name) { | |
| html += '<div class="mb-1"><strong class="text-gray-300">Title:</strong> <span class="text-gray-100">' + musicResult.song_name + '</span></div>'; | |
| } | |
| if (musicResult.artists) { | |
| html += '<div class="mb-1"><strong class="text-gray-300">Artists:</strong> <span class="text-gray-100">' + musicResult.artists + '</span></div>'; | |
| } | |
| if (musicResult.album) { | |
| html += '<div class="mb-1"><strong class="text-gray-300">Album:</strong> <span class="text-gray-100">' + musicResult.album + '</span></div>'; | |
| } | |
| html += '</div>'; | |
| // Add initial AI message about the song | |
| addAIMessage(`I've detected a song! It's "${musicResult.song_name}" by ${musicResult.artists}. Would you like to know more about this song or artist?`); | |
| classificationStatus.textContent = 'Music Found'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-green-600 text-green-100'; | |
| } else if (result.type === 'vehicle') { | |
| // Display vehicle classification results with model and make | |
| const vehicleResult = result.vehicle_result; | |
| const vehicleInfo = { | |
| 'Car': { | |
| make: 'Toyota', | |
| model: 'Camry' | |
| }, | |
| 'Truck': { | |
| make: 'Ford', | |
| model: 'F-150' | |
| } | |
| }; | |
| const info = vehicleInfo[vehicleResult.vehicle_type] || { make: 'Unknown', model: 'Unknown' }; | |
| html += '<div class="mb-4">'; | |
| html += '<p class="text-purple-400 font-semibold text-lg mb-2">Vehicle Detected:</p>'; | |
| html += '<div class="bg-gray-700 p-4 rounded-lg">'; | |
| html += '<div class="mb-2"><span class="text-gray-100 text-xl font-medium">' + vehicleResult.vehicle_type + '</span></div>'; | |
| html += '<div class="text-gray-300 text-sm">'; | |
| html += '<div class="mb-1"><strong>Make:</strong> ' + info.make + '</div>'; | |
| html += '<div><strong>Model:</strong> ' + info.model + '</div>'; | |
| html += '</div></div></div>'; | |
| // Add initial AI message about the vehicle | |
| addAIMessage(`I've detected a ${info.make} ${info.model} ${vehicleResult.vehicle_type.toLowerCase()}. Would you like to know more about this vehicle?`); | |
| classificationStatus.textContent = 'Vehicle Detected'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-purple-600 text-purple-100'; | |
| } else if (result.type === 'sound') { | |
| // Display only the top YAMNet classification result | |
| const soundResult = result.sound_result; | |
| const [topLabel] = soundResult.top_classes[0]; | |
| html += '<div class="mb-4">'; | |
| html += '<p class="text-blue-400 font-semibold text-lg mb-2">Sound Classification:</p>'; | |
| html += '<div class="bg-gray-700 p-4 rounded-lg">'; | |
| html += '<span class="text-gray-100 text-xl font-medium">' + topLabel + '</span>'; | |
| html += '</div></div>'; | |
| // Add initial AI message about the sound | |
| addAIMessage(`I've detected a ${topLabel} sound. Would you like to know more about this type of sound?`); | |
| classificationStatus.textContent = 'Sound Classified'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-blue-600 text-blue-100'; | |
| } | |
| } else { | |
| // No results from any classification | |
| if (isFinalChunk) { | |
| html = '<p class="italic text-gray-400">Recording stopped. No matches found.</p>'; | |
| classificationStatus.textContent = 'No Match'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-yellow-600 text-yellow-100'; | |
| } else if (mediaRecorder && mediaRecorder.state === 'recording') { | |
| const isCurrentlyDisplayingSuccess = resultsDiv.querySelector('.text-green-400, .text-blue-400, .text-purple-400'); | |
| if (!isCurrentlyDisplayingSuccess) { | |
| html = '<p class="italic text-gray-400">No match yet. Keep recording...</p>'; | |
| classificationStatus.textContent = 'Listening'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-yellow-600 text-yellow-100'; | |
| } | |
| } | |
| } | |
| resultsDiv.innerHTML = html; | |
| } | |
| // Chat functionality | |
| function addUserMessage(message) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'bg-blue-600 p-3 rounded-lg ml-4'; | |
| messageDiv.innerHTML = `<p class="text-white">${message}</p>`; | |
| chatContainer.appendChild(messageDiv); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| function addAIMessage(message) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = 'bg-gray-700 p-3 rounded-lg mr-4'; | |
| messageDiv.innerHTML = `<p class="text-gray-100">${message}</p>`; | |
| chatContainer.appendChild(messageDiv); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| async function sendMessageToMistral(message) { | |
| if (!currentClassificationResult) { | |
| addAIMessage("I don't have any classification results to discuss yet. Please record or upload some audio first."); | |
| return; | |
| } | |
| let systemPrompt = "You are a helpful assistant discussing audio classification results. "; | |
| if (currentClassificationResult.type === 'music') { | |
| const music = currentClassificationResult.music_result; | |
| systemPrompt += `The user is asking about a song: "${music.song_name}" by ${music.artists} from the album "${music.album}". `; | |
| } else if (currentClassificationResult.type === 'vehicle') { | |
| const vehicle = currentClassificationResult.vehicle_result; | |
| systemPrompt += `The user is asking about a ${vehicle.vehicle_type}. `; | |
| } else if (currentClassificationResult.type === 'sound') { | |
| const sound = currentClassificationResult.sound_result; | |
| systemPrompt += `The user is asking about a sound classified as "${sound.top_classes[0][0]}". `; | |
| } | |
| try { | |
| const response = await fetch('/chat-with-mistral/', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| system_prompt: systemPrompt, | |
| user_message: message | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| addAIMessage(data.response); | |
| } else { | |
| addAIMessage("I'm sorry, I encountered an error while processing your request."); | |
| } | |
| } catch (error) { | |
| console.error('Error sending message to Mistral:', error); | |
| addAIMessage("I'm sorry, I encountered an error while processing your request."); | |
| } | |
| } | |
| sendMessageButton.addEventListener('click', async () => { | |
| const message = chatInput.value.trim(); | |
| if (message) { | |
| addUserMessage(message); | |
| chatInput.value = ''; | |
| await sendMessageToMistral(message); | |
| } | |
| }); | |
| chatInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| sendMessageButton.click(); | |
| } | |
| }); | |
| clearChatButton.addEventListener('click', () => { | |
| chatContainer.innerHTML = ` | |
| <div class="text-center text-gray-500 pt-10"> | |
| <svg class="icon mx-auto mb-2 text-gray-500 w-10 h-10" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /> | |
| </svg> | |
| <p>Start a conversation about the classification results.</p> | |
| </div>`; | |
| }); | |
| // --- File Upload Logic (Slightly Modified) --- | |
| browseButton.addEventListener('click', () => audioFileInput.click()); | |
| audioFileInput.addEventListener('change', () => { | |
| if (audioFileInput.files.length > 0) { | |
| // Optionally display file name or trigger upload automatically | |
| // For now, user still needs to click "Upload and Recognize" | |
| console.log("File selected:", audioFileInput.files[0].name); | |
| } | |
| }); | |
| uploadButton.addEventListener('click', async () => { | |
| const file = audioFileInput.files[0]; | |
| if (!file) { | |
| resultsDiv.innerHTML = '<p class="text-red-400">Please select an audio file first.</p>'; | |
| classificationStatus.textContent = 'Error'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-red-700 text-red-100'; | |
| return; | |
| } | |
| resultsDiv.innerHTML = '<p class="text-gray-400 italic">Uploading and analyzing...</p>'; | |
| classificationStatus.textContent = 'Uploading'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-blue-600 text-blue-100'; | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const response = await fetch('/classify/', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| displayCombinedResults(result, true); | |
| } catch (error) { | |
| console.error("Error during file upload:", error); | |
| resultsDiv.innerHTML = '<p class="text-red-400">Error during file upload. Check console for details.</p>'; | |
| classificationStatus.textContent = 'Error'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-red-700 text-red-100'; | |
| } | |
| }); | |
| function displayFileUploadResult(data) { | |
| resultsDiv.innerHTML = ''; | |
| if (data.success === true) { | |
| const title = data.song_name || 'Unknown Title'; | |
| const artists = data.artists || 'Unknown Artist'; | |
| const album = data.album || 'Unknown Album'; | |
| resultsDiv.innerHTML = | |
| '<h3 class="text-xl font-semibold text-green-400 mb-2">Song Recognized!</h3>' + | |
| '<p class="mb-1"><strong class="text-gray-300">Title:</strong> <span class="text-gray-100">' + title + '</span></p>' + | |
| '<p class="mb-1"><strong class="text-gray-300">Artist(s):</strong> <span class="text-gray-100">' + artists + '</span></p>' + | |
| '<p class="mb-1"><strong class="text-gray-300">Album:</strong> <span class="text-gray-100">' + album + '</span></p>'; | |
| classificationStatus.textContent = 'Success'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-green-600 text-green-100'; | |
| } else { | |
| let errorMessage = data.message || "Could not recognize the song."; | |
| resultsDiv.innerHTML = '<p class="text-yellow-400">' + errorMessage + '</p>'; | |
| if (data.raw_acr_response) { | |
| resultsDiv.innerHTML += '<p class="text-xs text-gray-500 mt-2">Details:</p><pre class="text-xs text-gray-600 bg-gray-800 p-2 rounded">' + JSON.stringify(data.raw_acr_response, null, 2) + '</pre>'; | |
| } else { | |
| resultsDiv.innerHTML += '<p class="text-xs text-gray-500 mt-2">Full Response:</p><pre class="text-xs text-gray-600 bg-gray-800 p-2 rounded">' + JSON.stringify(data, null, 2) + '</pre>'; | |
| } | |
| classificationStatus.textContent = 'No Match'; | |
| classificationStatus.className = 'ml-auto text-xs py-1 px-2.5 rounded-full bg-yellow-600 text-yellow-100'; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |