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> |