Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>YOLO Vision AI - Multi-Image Analysis</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| position: relative; | |
| color: #e0e0e0; | |
| } | |
| .particles { | |
| position: absolute; width: 100%; height: 100%; overflow: hidden; z-index: 0; | |
| } | |
| .particle { | |
| position: absolute; width: 2px; height: 2px; background: #00d4ff; border-radius: 50%; animation: float 6s ease-in-out infinite; opacity: 0.6; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0px) rotate(0deg); } | |
| 50% { transform: translateY(-20px) rotate(180deg); } | |
| } | |
| .container { | |
| position: relative; z-index: 1; max-width: 800px; margin: 0 auto; padding: 2rem; min-height: 100vh; display: flex; flex-direction: column; justify-content: flex-start; align-items: center; | |
| } | |
| .header { | |
| text-align: center; margin-bottom: 2rem; animation: slideDown 1s ease-out; width: 100%; | |
| } | |
| .title { | |
| font-size: 3.5rem; font-weight: 700; background: linear-gradient(45deg, #00d4ff, #ff00ff, #00ff88); background-size: 200% 200%; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; animation: gradientShift 3s ease-in-out infinite; margin-bottom: 1rem; text-shadow: 0 0 30px rgba(0, 212, 255, 0.5); | |
| } | |
| .subtitle { | |
| font-size: 1.2rem; color: #a0a0a0; font-weight: 300; | |
| } | |
| .upload-area { | |
| width: 100%; max-width: 550px; min-height: 300px; border: 2px dashed #00d4ff; border-radius: 20px; background: rgba(0, 212, 255, 0.05); backdrop-filter: blur(10px); display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; animation: slideUp 1s ease-out 0.3s both; | |
| } | |
| .upload-area:hover { | |
| border-color: #ff00ff; background: rgba(255, 0, 255, 0.05); transform: translateY(-5px); box-shadow: 0 20px 40px rgba(0, 212, 255, 0.2); | |
| } | |
| .upload-area.dragover { | |
| border-color: #00ff88; background: rgba(0, 255, 136, 0.1); transform: scale(1.02); | |
| } | |
| .upload-icon { | |
| font-size: 4rem; color: #00d4ff; margin-bottom: 1rem; transition: all 0.3s ease; | |
| } | |
| .upload-area:hover .upload-icon { | |
| color: #ff00ff; transform: scale(1.1); | |
| } | |
| .upload-text { | |
| color: #ffffff; font-size: 1.1rem; margin-bottom: 0.5rem; font-weight: 500; | |
| } | |
| .upload-subtext { | |
| color: #a0a0a0; font-size: 0.9rem; | |
| } | |
| .file-input { | |
| display: none; | |
| } | |
| .file-list-container { | |
| display: none; | |
| width: 100%; | |
| max-width: 550px; | |
| margin-top: 2rem; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| #fileList { | |
| list-style: none; | |
| background: rgba(0, 212, 255, 0.05); | |
| border-radius: 10px; | |
| padding: 1rem; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| border: 1px solid rgba(0, 212, 255, 0.2); | |
| } | |
| #fileList li { | |
| padding: 0.5rem; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| color: #c0c0c0; | |
| } | |
| #fileList li:last-child { | |
| border-bottom: none; | |
| } | |
| .analyze-button { | |
| display: block; | |
| width: 100%; | |
| background: linear-gradient(45deg, #00d4ff, #0099cc); border: none; color: white; padding: 15px 40px; font-size: 1.1rem; font-weight: 600; border-radius: 50px; cursor: pointer; margin-top: 1.5rem; transition: all 0.3s ease; text-transform: uppercase; letter-spacing: 1px; | |
| } | |
| .analyze-button:hover { | |
| transform: translateY(-2px); box-shadow: 0 10px 25px rgba(0, 212, 255, 0.4); background: linear-gradient(45deg, #ff00ff, #cc0099); | |
| } | |
| .analyze-button:disabled { | |
| opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; background: #555; | |
| } | |
| .loading { | |
| display: none; margin-top: 2rem; text-align: center; | |
| } | |
| .spinner { | |
| width: 40px; height: 40px; border: 4px solid rgba(0, 212, 255, 0.3); border-top: 4px solid #00d4ff; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto; | |
| } | |
| #results-container { | |
| margin-top: 2rem; | |
| width: 100%; | |
| max-width: 550px; /* Adjusted max-width */ | |
| } | |
| .result-card { | |
| padding: 1.5rem; | |
| background: rgba(255, 255, 255, 0.05); | |
| backdrop-filter: blur(15px); | |
| border: 1px solid rgba(0, 212, 255, 0.3); | |
| animation: slideUp 0.5s ease-out; | |
| margin-bottom: 2rem; | |
| border-radius: 15px; /* Unified border radius */ | |
| } | |
| /* --- NEW: Image style within the card --- */ | |
| .result-image { | |
| width: 100%; | |
| height: auto; | |
| max-height: 400px; | |
| object-fit: contain; | |
| border-radius: 10px; | |
| margin-bottom: 1.5rem; | |
| background-color: rgba(0,0,0,0.2); | |
| } | |
| .result-card h3 { | |
| font-size: 1.2rem; | |
| color: #00d4ff; | |
| margin-bottom: 1.5rem; | |
| padding-bottom: 1rem; | |
| border-bottom: 1px solid rgba(0, 212, 255, 0.2); | |
| font-weight: 600; | |
| word-wrap: break-word; | |
| } | |
| .prediction-block { | |
| margin-bottom: 1.5rem; | |
| } | |
| .prediction-block:last-child { | |
| margin-bottom: 0; | |
| } | |
| .prediction-title { | |
| font-size: 0.9rem; | |
| color: #a0a0a0; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| margin-bottom: 0.5rem; | |
| } | |
| .prediction-class { | |
| font-size: 1.8rem; /* Made class name larger */ | |
| font-weight: 700; | |
| color: #00ff88; | |
| text-transform: capitalize; | |
| line-height: 1.2; | |
| } | |
| .prediction-confidence { | |
| font-size: 1rem; /* Slightly larger confidence text */ | |
| color: #e0e0e0; | |
| } | |
| .damage-note { | |
| font-size: 0.8rem; | |
| color: #aaa; | |
| font-style: italic; | |
| margin-top: 4px; | |
| } | |
| .error { | |
| color: #ff4444; background: rgba(255, 68, 68, 0.1); padding: 1rem; border-radius: 10px; border: 1px solid #ff4444; margin-top: 2rem; width: 100%; max-width: 550px; | |
| } | |
| @keyframes slideDown { from { opacity: 0; transform: translateY(-50px); } to { opacity: 1; transform: translateY(0); } } | |
| @keyframes slideUp { from { opacity: 0; transform: translateY(50px); } to { opacity: 1; transform: translateY(0); } } | |
| @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } | |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| @keyframes gradientShift { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } } | |
| @media (max-width: 768px) { | |
| .title { font-size: 2.5rem; } .container { padding: 1rem; } .upload-area { min-height: 250px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="particles" id="particles"></div> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1 class="title">YOLO Vision AI</h1> | |
| <p class="subtitle">Multi-Image Vehicle Part & Damage Analysis</p> | |
| </div> | |
| <div class="upload-area" id="uploadArea"> | |
| <div class="upload-icon">🔮</div> | |
| <div class="upload-text">Drop your images here or click to upload</div> | |
| <div class="upload-subtext">Supports PNG, JPG, JPEG formats</div> | |
| <input type="file" id="fileInput" class="file-input" accept=".png,.jpg,.jpeg" multiple> | |
| </div> | |
| <div class="file-list-container" id="fileListContainer"> | |
| <ul id="fileList"></ul> | |
| <button class="analyze-button" id="analyzeButton">🚀 Analyze Images</button> | |
| </div> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <p style="color: #00d4ff; margin-top: 1rem;">Processing your images...</p> | |
| </div> | |
| <div id="results-container"></div> | |
| <div class="error" id="errorContainer" style="display: none;"></div> | |
| </div> | |
| <script> | |
| // Create animated particles | |
| function createParticles() { | |
| const container = document.getElementById('particles'); | |
| if (container.children.length > 0) return; | |
| for (let i = 0; i < 50; i++) { | |
| const particle = document.createElement('div'); | |
| particle.className = 'particle'; | |
| particle.style.left = Math.random() * 100 + '%'; | |
| particle.style.top = Math.random() * 100 + '%'; | |
| particle.style.animationDelay = Math.random() * 6 + 's'; | |
| particle.style.animationDuration = (3 + Math.random() * 3) + 's'; | |
| container.appendChild(particle); | |
| } | |
| } | |
| createParticles(); | |
| // DOM elements | |
| const uploadArea = document.getElementById('uploadArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const fileListContainer = document.getElementById('fileListContainer'); | |
| const fileList = document.getElementById('fileList'); | |
| const analyzeButton = document.getElementById('analyzeButton'); | |
| const loading = document.getElementById('loading'); | |
| const errorContainer = document.getElementById('errorContainer'); | |
| const resultsContainer = document.getElementById('results-container'); | |
| // --- NEW: Store for file objects and their data URLs for preview --- | |
| let fileDataStore = []; | |
| // Event Listeners | |
| uploadArea.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| uploadArea.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| uploadArea.addEventListener(eventName, () => uploadArea.classList.add('dragover'), false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| uploadArea.addEventListener(eventName, () => uploadArea.classList.remove('dragover'), false); | |
| }); | |
| uploadArea.addEventListener('drop', handleDrop, false); | |
| analyzeButton.addEventListener('click', analyzeImages); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| function handleDrop(e) { | |
| handleFiles(e.dataTransfer.files); | |
| } | |
| function handleFileSelect(e) { | |
| handleFiles(e.target.files); | |
| } | |
| // --- UPDATED: Reads files and generates data URLs for previews --- | |
| async function handleFiles(files) { | |
| if (files.length === 0) return; | |
| // Clear previous selections and results | |
| resetUI(); | |
| fileDataStore = []; | |
| const filePromises = Array.from(files).map(file => { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| fileDataStore.push({ file: file, dataURL: e.target.result }); | |
| resolve(); | |
| }; | |
| reader.onerror = reject; | |
| reader.readAsDataURL(file); | |
| }); | |
| }); | |
| await Promise.all(filePromises); | |
| // Update the UI list | |
| fileDataStore.forEach(item => { | |
| const listItem = document.createElement('li'); | |
| listItem.textContent = `${item.file.name} (${(item.file.size / 1024).toFixed(1)} KB)`; | |
| fileList.appendChild(listItem); | |
| }); | |
| fileListContainer.style.display = 'block'; | |
| uploadArea.style.display = 'none'; // Hide upload area after selection | |
| } | |
| async function analyzeImages() { | |
| if (fileDataStore.length === 0) { | |
| showError('Please select one or more images first'); | |
| return; | |
| } | |
| loading.style.display = 'block'; | |
| analyzeButton.disabled = true; | |
| hideError(); | |
| resultsContainer.innerHTML = ''; | |
| try { | |
| const formData = new FormData(); | |
| fileDataStore.forEach(item => { | |
| formData.append('file', item.file); | |
| }); | |
| const response = await fetch('/predict', { method: 'POST', body: formData }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| displayResults(data); | |
| } else { | |
| showError(data.error || 'An unknown error occurred during prediction'); | |
| } | |
| } catch (error) { | |
| showError('Failed to connect to the server. Please check your connection and try again.'); | |
| console.error('Error:', error); | |
| } finally { | |
| loading.style.display = 'none'; | |
| analyzeButton.disabled = false; | |
| fileInput.value = ''; | |
| } | |
| } | |
| // --- UPDATED: Displays results with image previews --- | |
| function displayResults(results) { | |
| if (!Array.isArray(results) || results.length === 0) { | |
| resultsContainer.innerHTML = '<p>No results were returned from the server.</p>'; | |
| return; | |
| } | |
| results.forEach(result => { | |
| // Find the corresponding image data URL from our store | |
| const fileData = fileDataStore.find(item => item.file.name === result.filename); | |
| if (!fileData) return; // Skip if no matching image found | |
| const card = document.createElement('div'); | |
| card.className = 'result-card'; | |
| const partPred = result.part_prediction; | |
| const damagePred = result.damage_prediction; | |
| const damageNote = damagePred.note ? `<div class="damage-note">${damagePred.note}</div>` : ''; | |
| card.innerHTML = ` | |
| <img src="${fileData.dataURL}" alt="${result.filename}" class="result-image"> | |
| <h3>${result.filename}</h3> | |
| <div class="prediction-block"> | |
| <div class="prediction-title">Part Detected</div> | |
| <div class="prediction-class">${partPred.class.replace(/_/g, ' ')}</div> | |
| <div class="prediction-confidence">Confidence: ${(partPred.confidence * 100).toFixed(2)}%</div> | |
| </div> | |
| <div class="prediction-block"> | |
| <div class="prediction-title">Damage Status</div> | |
| <div class="prediction-class" style="color: ${damagePred.class === 'correct' ? '#00ff88' : '#ff4444'};">${damagePred.class}</div> | |
| <div class="prediction-confidence">Confidence: ${(damagePred.confidence * 100).toFixed(2)}%</div> | |
| ${damageNote} | |
| </div> | |
| `; | |
| resultsContainer.appendChild(card); | |
| }); | |
| } | |
| function resetUI() { | |
| fileList.innerHTML = ''; | |
| resultsContainer.innerHTML = ''; | |
| fileListContainer.style.display = 'none'; | |
| uploadArea.style.display = 'flex'; | |
| hideError(); | |
| } | |
| function showError(message) { | |
| errorContainer.textContent = message; | |
| errorContainer.style.display = 'block'; | |
| } | |
| function hideError() { | |
| errorContainer.style.display = 'none'; | |
| } | |
| </script> | |
| </body> | |
| </html> |