Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta name="theme-color" content="#2c3e50"> | |
| <meta name="description" content="Analyze engine sounds to identify potential mechanical issues"> | |
| <meta name="apple-mobile-web-app-capable" content="yes"> | |
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> | |
| <title>Engine Sound Classifier</title> | |
| <link rel="manifest" href="/static/manifest.json"> | |
| <link rel="icon" type="image/png" href="/static/icons/icon-192x192.png"> | |
| <link rel="apple-touch-icon" href="/static/icons/icon-192x192.png"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary-color: #2c3e50; | |
| --secondary-color: #3498db; | |
| --success-color: #2ecc71; | |
| --warning-color: #f39c12; | |
| --danger-color: #e74c3c; | |
| --background-color: #f5f5f5; | |
| --card-background: #ffffff; | |
| --text-color: #2c3e50; | |
| --border-radius: 12px; | |
| --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| line-height: 1.6; | |
| background-color: var(--background-color); | |
| color: var(--text-color); | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| header { | |
| background-color: var(--primary-color); | |
| color: white; | |
| padding: 2rem; | |
| margin-bottom: 2rem; | |
| text-align: center; | |
| box-shadow: var(--shadow); | |
| } | |
| .upload-container { | |
| background-color: var(--card-background); | |
| border-radius: var(--border-radius); | |
| box-shadow: var(--shadow); | |
| padding: 2rem; | |
| margin-bottom: 2rem; | |
| text-align: center; | |
| } | |
| .upload-area { | |
| border: 2px dashed var(--secondary-color); | |
| border-radius: var(--border-radius); | |
| padding: 2rem; | |
| margin: 1rem 0; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| background-color: rgba(52, 152, 219, 0.05); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .upload-icon { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| color: var(--secondary-color); | |
| } | |
| .upload-area:hover { | |
| background-color: rgba(52, 152, 219, 0.1); | |
| transform: translateY(-2px); | |
| } | |
| .upload-area.drag-over { | |
| background-color: rgba(52, 152, 219, 0.2); | |
| border-color: var(--primary-color); | |
| } | |
| .btn { | |
| background-color: var(--secondary-color); | |
| color: white; | |
| border: none; | |
| padding: 12px 24px; | |
| border-radius: var(--border-radius); | |
| cursor: pointer; | |
| font-size: 1rem; | |
| font-weight: 500; | |
| transition: all 0.3s ease; | |
| } | |
| .btn:hover { | |
| background-color: #2980b9; | |
| transform: translateY(-1px); | |
| } | |
| .btn:disabled { | |
| background-color: #bdc3c7; | |
| cursor: not-allowed; | |
| } | |
| .results-container { | |
| background-color: var(--card-background); | |
| border-radius: var(--border-radius); | |
| box-shadow: var(--shadow); | |
| padding: 2rem; | |
| margin-bottom: 2rem; | |
| display: none; | |
| } | |
| .result-card { | |
| background-color: rgba(52, 152, 219, 0.05); | |
| border-radius: var(--border-radius); | |
| padding: 1.5rem; | |
| margin-bottom: 1.5rem; | |
| border-left: 5px solid var(--secondary-color); | |
| } | |
| .confidence-bar-container { | |
| background-color: #e9ecef; | |
| border-radius: var(--border-radius); | |
| height: 25px; | |
| margin: 10px 0; | |
| overflow: hidden; | |
| } | |
| .confidence-bar { | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: flex-end; | |
| color: white; | |
| padding-right: 10px; | |
| transition: width 1s ease-in-out; | |
| border-radius: 0 var(--border-radius) var(--border-radius) 0; | |
| } | |
| .charts-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| gap: 2rem; | |
| margin-top: 2rem; | |
| } | |
| .chart-box { | |
| background-color: var(--card-background); | |
| border-radius: var(--border-radius); | |
| box-shadow: var(--shadow); | |
| padding: 1.5rem; | |
| } | |
| .chart-title { | |
| text-align: center; | |
| margin-bottom: 1rem; | |
| font-weight: 600; | |
| } | |
| .chart-image { | |
| width: 100%; | |
| height: auto; | |
| border-radius: var(--border-radius); | |
| } | |
| .loading { | |
| display: none; | |
| text-align: center; | |
| padding: 2rem; | |
| } | |
| .spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 4px solid rgba(52, 152, 219, 0.1); | |
| border-left-color: var(--secondary-color); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 1rem; | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| margin-left: 8px; | |
| } | |
| .badge-success { background-color: var(--success-color); color: white; } | |
| .badge-warning { background-color: var(--warning-color); color: white; } | |
| .badge-danger { background-color: var(--danger-color); color: white; } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| @media (max-width: 768px) { | |
| .container { padding: 16px; } | |
| header { padding: 1.5rem; } | |
| .upload-container, .results-container { padding: 1.5rem; } | |
| .charts-container { grid-template-columns: 1fr; } | |
| .btn { width: 100%; } | |
| .upload-area { padding: 1.5rem 1rem; } | |
| } | |
| .offline-banner { | |
| display: none; | |
| background-color: var(--warning-color); | |
| color: white; | |
| text-align: center; | |
| padding: 0.5rem; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| z-index: 1000; | |
| } | |
| .install-prompt { | |
| display: none; | |
| background-color: var(--primary-color); | |
| color: white; | |
| padding: 1rem; | |
| border-radius: var(--border-radius); | |
| margin-bottom: 1rem; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .install-btn { | |
| background-color: white; | |
| color: var(--primary-color); | |
| border: none; | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="offline-banner" id="offlineBanner"> | |
| You are currently offline. Some features may be limited. | |
| </div> | |
| <header> | |
| <h1>Engine Sound Classifier</h1> | |
| <p>Upload engine audio to identify potential issues</p> | |
| </header> | |
| <div class="container"> | |
| <div id="installPrompt" class="install-prompt"> | |
| <span>Install this app on your device for offline use</span> | |
| <button id="installBtn" class="install-btn">Install</button> | |
| </div> | |
| <div class="upload-container"> | |
| <h2>Upload Engine Sound</h2> | |
| <p>Upload a WAV file of engine sound to analyze and identify potential issues</p> | |
| <div class="upload-area" id="dropArea"> | |
| <div class="upload-icon">📁</div> | |
| <p>Drag and drop your audio file here or click to browse</p> | |
| <input type="file" id="fileInput" accept=".wav" class="hidden" style="display: none;"> | |
| </div> | |
| <button id="uploadBtn" class="btn" disabled>Analyze Sound</button> | |
| </div> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <p>Analyzing engine sound...</p> | |
| </div> | |
| <div class="results-container" id="results"> | |
| <div class="result-card"> | |
| <h3>Analysis Results</h3> | |
| <div id="bestPrediction"></div> | |
| <div id="confidence"></div> | |
| </div> | |
| <div class="charts-container"> | |
| <div class="chart-box"> | |
| <h4 class="chart-title">Model Confidence Comparison</h4> | |
| <img id="confidenceChart" class="chart-image" alt="Confidence Chart"> | |
| </div> | |
| <div class="chart-box"> | |
| <h4 class="chart-title">Model Voting Distribution</h4> | |
| <img id="votingChart" class="chart-image" alt="Voting Chart"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Service Worker Registration | |
| if ('serviceWorker' in navigator) { | |
| window.addEventListener('load', () => { | |
| navigator.serviceWorker.register('/static/sw.js') | |
| .then(registration => console.log('ServiceWorker registered')) | |
| .catch(err => console.log('ServiceWorker registration failed:', err)); | |
| }); | |
| } | |
| // PWA Installation | |
| let deferredPrompt; | |
| const installPrompt = document.getElementById('installPrompt'); | |
| const installBtn = document.getElementById('installBtn'); | |
| window.addEventListener('beforeinstallprompt', (e) => { | |
| // Prevent Chrome 67 and earlier from automatically showing the prompt | |
| e.preventDefault(); | |
| // Stash the event so it can be triggered later | |
| deferredPrompt = e; | |
| // Show the install button | |
| installPrompt.style.display = 'flex'; | |
| }); | |
| installBtn.addEventListener('click', (e) => { | |
| // Hide the app provided install promotion | |
| installPrompt.style.display = 'none'; | |
| // Show the install prompt | |
| deferredPrompt.prompt(); | |
| // Wait for the user to respond to the prompt | |
| deferredPrompt.userChoice.then((choiceResult) => { | |
| if (choiceResult.outcome === 'accepted') { | |
| console.log('User accepted the install prompt'); | |
| } else { | |
| console.log('User dismissed the install prompt'); | |
| } | |
| deferredPrompt = null; | |
| }); | |
| }); | |
| // Hide install prompt when already installed | |
| window.addEventListener('appinstalled', (evt) => { | |
| installPrompt.style.display = 'none'; | |
| }); | |
| // Offline detection | |
| const offlineBanner = document.getElementById('offlineBanner'); | |
| window.addEventListener('online', () => offlineBanner.style.display = 'none'); | |
| window.addEventListener('offline', () => offlineBanner.style.display = 'block'); | |
| // DOM Elements | |
| const dropArea = document.getElementById('dropArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const uploadBtn = document.getElementById('uploadBtn'); | |
| const loading = document.getElementById('loading'); | |
| const results = document.getElementById('results'); | |
| const bestPrediction = document.getElementById('bestPrediction'); | |
| const confidence = document.getElementById('confidence'); | |
| const confidenceChart = document.getElementById('confidenceChart'); | |
| const votingChart = document.getElementById('votingChart'); | |
| // Prevent default drag behaviors | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropArea.addEventListener(eventName, preventDefaults, false); | |
| document.body.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| // Highlight drop zone when dragging over it | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropArea.addEventListener(eventName, highlight, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropArea.addEventListener(eventName, unhighlight, false); | |
| }); | |
| function highlight(e) { | |
| dropArea.classList.add('drag-over'); | |
| } | |
| function unhighlight(e) { | |
| dropArea.classList.remove('drag-over'); | |
| } | |
| // Handle dropped files | |
| dropArea.addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| handleFiles(files); | |
| } | |
| // Handle selected files | |
| fileInput.addEventListener('change', function() { | |
| handleFiles(this.files); | |
| }); | |
| dropArea.addEventListener('click', () => fileInput.click()); | |
| function handleFiles(files) { | |
| if (files.length > 0) { | |
| const file = files[0]; | |
| if (file.type === 'audio/wav' || file.name.endsWith('.wav')) { | |
| uploadBtn.disabled = false; | |
| } else { | |
| alert('Please upload a WAV file'); | |
| uploadBtn.disabled = true; | |
| } | |
| } | |
| } | |
| // Upload and analyze | |
| uploadBtn.addEventListener('click', async () => { | |
| if (fileInput.files.length > 0) { | |
| const formData = new FormData(); | |
| formData.append('file', fileInput.files[0]); | |
| loading.style.display = 'block'; | |
| results.style.display = 'none'; | |
| uploadBtn.disabled = true; | |
| try { | |
| const response = await fetch('/api/predict', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) throw new Error('Network response was not ok'); | |
| const data = await response.json(); | |
| displayResults(data); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| alert('An error occurred while analyzing the audio file'); | |
| } finally { | |
| loading.style.display = 'none'; | |
| uploadBtn.disabled = false; | |
| } | |
| } | |
| }); | |
| function displayResults(data) { | |
| results.style.display = 'block'; | |
| // Display best prediction and confidence | |
| const confidencePercent = (data.confidence * 100).toFixed(1); | |
| let confidenceClass = 'badge-success'; | |
| if (confidencePercent < 70) confidenceClass = 'badge-danger'; | |
| else if (confidencePercent < 85) confidenceClass = 'badge-warning'; | |
| bestPrediction.innerHTML = ` | |
| <h4>Best Prediction: ${data.best_prediction} | |
| <span class="badge ${confidenceClass}">${confidencePercent}% Confident</span> | |
| </h4> | |
| <p>Predicted by: ${data.best_model}</p> | |
| `; | |
| // Display charts | |
| if (data.confidence_chart) { | |
| confidenceChart.src = `data:image/png;base64,${data.confidence_chart}`; | |
| } | |
| if (data.voting_chart) { | |
| votingChart.src = `data:image/png;base64,${data.voting_chart}`; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |