Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ESC50 Audio Classifier</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| } | |
| .container { | |
| background: white; | |
| border-radius: 20px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| max-width: 800px; | |
| width: 100%; | |
| padding: 40px; | |
| } | |
| h1 { | |
| color: #333; | |
| margin-bottom: 10px; | |
| font-size: 2em; | |
| } | |
| .subtitle { | |
| color: #666; | |
| margin-bottom: 30px; | |
| font-size: 1.1em; | |
| } | |
| .upload-area { | |
| border: 3px dashed #667eea; | |
| border-radius: 15px; | |
| padding: 40px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| background: #f8f9ff; | |
| } | |
| .upload-area:hover { | |
| background: #eef1ff; | |
| border-color: #764ba2; | |
| } | |
| .upload-area.dragover { | |
| background: #e0e7ff; | |
| border-color: #667eea; | |
| transform: scale(1.02); | |
| } | |
| .upload-icon { | |
| font-size: 48px; | |
| margin-bottom: 20px; | |
| } | |
| input[type="file"] { | |
| display: none; | |
| } | |
| .file-name { | |
| margin-top: 20px; | |
| color: #667eea; | |
| font-weight: 500; | |
| } | |
| .button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 15px 40px; | |
| border-radius: 25px; | |
| font-size: 1.1em; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| margin-top: 20px; | |
| width: 100%; | |
| } | |
| .button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4); | |
| } | |
| .button:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .results { | |
| margin-top: 30px; | |
| padding: 25px; | |
| background: #f8f9ff; | |
| border-radius: 15px; | |
| display: none; | |
| } | |
| .results.show { | |
| display: block; | |
| animation: slideIn 0.4s ease; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .prediction-main { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| border-left: 5px solid #667eea; | |
| } | |
| .prediction-class { | |
| font-size: 1.5em; | |
| color: #333; | |
| font-weight: 700; | |
| margin-bottom: 10px; | |
| } | |
| .confidence { | |
| font-size: 1.2em; | |
| color: #667eea; | |
| font-weight: 600; | |
| } | |
| .top-predictions { | |
| margin-top: 20px; | |
| } | |
| .top-predictions h3 { | |
| color: #333; | |
| margin-bottom: 15px; | |
| font-size: 1.2em; | |
| } | |
| .prediction-item { | |
| background: white; | |
| padding: 12px 15px; | |
| border-radius: 8px; | |
| margin-bottom: 8px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .prediction-rank { | |
| background: #667eea; | |
| color: white; | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 700; | |
| margin-right: 15px; | |
| } | |
| .prediction-name { | |
| flex: 1; | |
| font-weight: 500; | |
| color: #333; | |
| } | |
| .prediction-conf { | |
| color: #667eea; | |
| font-weight: 600; | |
| } | |
| .loading { | |
| display: none; | |
| text-align: center; | |
| padding: 20px; | |
| } | |
| .loading.show { | |
| display: block; | |
| } | |
| .spinner { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #667eea; | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 15px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .error { | |
| background: #fee; | |
| color: #c33; | |
| padding: 15px; | |
| border-radius: 8px; | |
| margin-top: 20px; | |
| display: none; | |
| } | |
| .error.show { | |
| display: block; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 6px; | |
| background: #e0e0e0; | |
| border-radius: 3px; | |
| overflow: hidden; | |
| margin-top: 15px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #667eea, #764ba2); | |
| width: 0%; | |
| transition: width 0.3s; | |
| } | |
| .classes-info { | |
| margin-top: 20px; | |
| border-radius: 15px; | |
| overflow: hidden; | |
| border: 2px solid #e0e4ff; | |
| } | |
| .classes-toggle { | |
| width: 100%; | |
| background: #f8f9ff; | |
| border: none; | |
| padding: 15px 20px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| cursor: pointer; | |
| font-size: 1em; | |
| font-weight: 600; | |
| color: #667eea; | |
| transition: background 0.2s; | |
| } | |
| .classes-toggle:hover { background: #eef1ff; } | |
| .classes-toggle .arrow { | |
| transition: transform 0.3s; | |
| font-style: normal; | |
| } | |
| .classes-toggle.open .arrow { transform: rotate(180deg); } | |
| .classes-grid { | |
| display: none; | |
| grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | |
| gap: 8px; | |
| padding: 15px; | |
| background: white; | |
| } | |
| .classes-grid.show { display: grid; } | |
| .class-tag { | |
| background: #f8f9ff; | |
| border: 1px solid #e0e4ff; | |
| border-radius: 20px; | |
| padding: 6px 12px; | |
| font-size: 0.82em; | |
| color: #555; | |
| text-align: center; | |
| } | |
| .k-selector { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 15px; | |
| margin-top: 20px; | |
| color: #667eea; | |
| font-weight: 600; | |
| } | |
| .k-selector input[type="range"] { | |
| appearance: none; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: #e0e4ff; | |
| outline: none; | |
| } | |
| .k-selector input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #667eea, #764ba2); | |
| cursor: pointer; | |
| box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>ESC50 Audio Classifier</h1> | |
| <p class="subtitle">Upload an audio file to classify environmental sounds</p> | |
| <div class="upload-area" id="uploadArea"> | |
| <h3>Drag & Drop Audio File</h3> | |
| <p>or click to browse</p> | |
| <input type="file" id="fileInput" accept="audio/*"> | |
| <div class="file-name" id="fileName"></div> | |
| </div> | |
| <div class="k-selector"> | |
| <label for="kInput">Number of predictions: <span id="kValue">5</span> </label> | |
| <input type="range" id="kInput" min="1" max="10" value="5"> | |
| </div> | |
| <button class="button" id="classifyBtn" disabled>Classify Audio</button> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <p>Analyzing audio...</p> | |
| </div> | |
| <div class="error" id="error"></div> | |
| <div class="results" id="results"> | |
| <div class="prediction-main"> | |
| <div class="prediction-class" id="predictedClass"></div> | |
| <div class="confidence" id="confidence"></div> | |
| </div> | |
| <div class="top-predictions"> | |
| <h3></h3> | |
| <div id="topPredictions"></div> | |
| </div> | |
| </div> | |
| <div class="classes-info"> | |
| <button class="classes-toggle", id="classesToggle"> | |
| <span>ℹ️ 50 Identifiable Sound Classes </span> | |
| <i class="arrow">▼</i> | |
| </button> | |
| <div class="classes-grid" id="classesGrid"></div> | |
| </div> | |
| </div> | |
| <script> | |
| const uploadArea = document.getElementById('uploadArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const fileName = document.getElementById('fileName'); | |
| const classifyBtn = document.getElementById('classifyBtn'); | |
| const loading = document.getElementById('loading'); | |
| const results = document.getElementById('results'); | |
| const error = document.getElementById('error'); | |
| let selectedFile = null; | |
| // API endpoint - update this to your server URL | |
| const API_URL = ''; | |
| // Click to upload | |
| uploadArea.addEventListener('click', () => fileInput.click()); | |
| // File selection | |
| fileInput.addEventListener('change', (e) => { | |
| handleFile(e.target.files[0]); | |
| }); | |
| // Drag and drop | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.add('dragover'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => { | |
| uploadArea.classList.remove('dragover'); | |
| }); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadArea.classList.remove('dragover'); | |
| handleFile(e.dataTransfer.files[0]); | |
| }); | |
| function handleFile(file) { | |
| if (!file) return; | |
| // Check if it's an audio file | |
| if (!file.type.startsWith('audio/')) { | |
| showError('Please select an audio file'); | |
| return; | |
| } | |
| selectedFile = file; | |
| fileName.textContent = `Selected: ${file.name}`; | |
| classifyBtn.disabled = false; | |
| hideError(); | |
| } | |
| const kInput = document.getElementById('kInput'); | |
| kInput.addEventListener('input', () => { | |
| document.getElementById('kValue').textContent = kInput.value; | |
| }); | |
| // Classify button | |
| classifyBtn.addEventListener('click', async () => { | |
| if (!selectedFile) return; | |
| classifyBtn.disabled = true; | |
| loading.classList.add('show'); | |
| results.classList.remove('show'); | |
| hideError(); | |
| try { | |
| const formData = new FormData(); | |
| formData.append('file', selectedFile); | |
| const response = await fetch(`${API_URL}/predict-top-k?k=${kInput.value}`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Server error: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| displayResults(data); | |
| } catch (err) { | |
| showError(`Error: ${err.message}. Make sure the server is running at ${API_URL}`); | |
| } finally { | |
| loading.classList.remove('show'); | |
| classifyBtn.disabled = false; | |
| } | |
| }); | |
| function displayResults(data) { | |
| document.querySelector('.top-predictions h3').textContent = `Top ${kInput.value} Predictions`; | |
| // Main prediction | |
| document.getElementById('predictedClass').textContent = | |
| data.predicted_class.replace(/_/g, ' ').toUpperCase(); | |
| document.getElementById('confidence').textContent = | |
| `Confidence: ${(data.confidence * 100).toFixed(1)}% (Raw softmax output, not normalized)`; | |
| // Top predictions | |
| const topPredictionsDiv = document.getElementById('topPredictions'); | |
| topPredictionsDiv.innerHTML = data.top_predictions.map((pred, idx) => ` | |
| <div class="prediction-item"> | |
| <span class="prediction-rank">${idx + 1}</span> | |
| <span class="prediction-name">${pred.class.replace(/_/g, ' ')}</span> | |
| <span class="prediction-conf">${(pred.confidence * 100).toFixed(1)}%</span> | |
| </div> | |
| `).join(''); | |
| results.classList.add('show'); | |
| } | |
| function showError(message) { | |
| error.textContent = message; | |
| error.classList.add('show'); | |
| } | |
| function hideError() { | |
| error.classList.remove('show'); | |
| } | |
| // Check server status on load | |
| window.addEventListener('load', async () => { | |
| try { | |
| const response = await fetch(`${API_URL}/`); | |
| if (response.ok) { | |
| console.log('Server is running'); | |
| } | |
| } catch (err) { | |
| showError(`Cannot connect to server at ${API_URL}. Make sure it's running.`); | |
| } | |
| }); | |
| const classesToggle = document.getElementById('classesToggle'); | |
| const classesGrid = document.getElementById('classesGrid'); | |
| window.addEventListener('load', async () => { | |
| const res = await fetch(`${API_URL}/labels`); | |
| const { labels } = await res.json(); | |
| classesGrid.innerHTML = labels.map(c => | |
| `<span class="class-tag">${c.replace(/_/g, ' ')}</span>` | |
| ).join(''); | |
| }); | |
| classesToggle.addEventListener('click', () =>{ | |
| classesToggle.classList.toggle('open'); | |
| classesGrid.classList.toggle('show'); | |
| }); | |
| </script> | |
| </body> | |
| </html> |