Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Perception System</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: "IBM Plex Sans", "Avenir Next", "Helvetica Neue", sans-serif; | |
| background: linear-gradient(180deg, #f6f7f9 0%, #eef1f4 100%); | |
| color: #1f2933; | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| h1 { | |
| color: #1f2933; | |
| text-align: center; | |
| margin-bottom: 30px; | |
| font-size: 2.5rem; | |
| letter-spacing: 0.5px; | |
| } | |
| .main-card { | |
| background: #ffffff; | |
| border-radius: 16px; | |
| box-shadow: 0 18px 40px rgba(16, 24, 40, 0.12); | |
| padding: 40px; | |
| } | |
| .section { | |
| margin-bottom: 30px; | |
| } | |
| .section-title { | |
| font-size: 1.2rem; | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 15px; | |
| } | |
| /* Mode selector */ | |
| .mode-selector { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| } | |
| .mode-card { | |
| position: relative; | |
| padding: 20px; | |
| border: 1px solid #d6dbe0; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-align: center; | |
| background: #f9fafb; | |
| } | |
| .mode-card:hover { | |
| border-color: #4b5563; | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 16px rgba(16, 24, 40, 0.12); | |
| } | |
| .mode-card.selected { | |
| border-color: #1f2933; | |
| background: #eef2f6; | |
| } | |
| .mode-card.disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .mode-card input[type="radio"] { | |
| position: absolute; | |
| opacity: 0; | |
| } | |
| .mode-icon { | |
| display: none; | |
| } | |
| .mode-title { | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 5px; | |
| } | |
| .mode-badge { | |
| display: inline-block; | |
| padding: 4px 8px; | |
| background: #6b7280; | |
| color: #f9fafb; | |
| font-size: 0.7rem; | |
| border-radius: 4px; | |
| font-weight: 600; | |
| margin-top: 8px; | |
| } | |
| /* Input fields */ | |
| .input-group { | |
| margin-bottom: 20px; | |
| } | |
| .input-group label { | |
| display: block; | |
| font-weight: 500; | |
| color: #555; | |
| margin-bottom: 8px; | |
| } | |
| .input-group input[type="text"], | |
| .input-group select { | |
| width: 100%; | |
| padding: 12px; | |
| border: 1px solid #d6dbe0; | |
| border-radius: 8px; | |
| font-size: 1rem; | |
| transition: border-color 0.3s; | |
| background: #ffffff; | |
| } | |
| .input-group input[type="text"]:focus, | |
| .input-group select:focus { | |
| outline: none; | |
| border-color: #4b5563; | |
| } | |
| .file-input-wrapper { | |
| position: relative; | |
| display: inline-block; | |
| width: 100%; | |
| } | |
| .file-input-label { | |
| display: block; | |
| padding: 15px; | |
| background: #f3f4f6; | |
| border: 1px dashed #bfc5cc; | |
| border-radius: 8px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .file-input-label:hover { | |
| border-color: #4b5563; | |
| background: #eceff3; | |
| } | |
| .file-input-label.has-file { | |
| border-color: #1f2933; | |
| background: #e8edf2; | |
| } | |
| input[type="file"] { | |
| position: absolute; | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| /* Buttons */ | |
| .btn { | |
| padding: 14px 28px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| width: 100%; | |
| } | |
| .btn-primary { | |
| background: #1f2933; | |
| color: #f9fafb; | |
| } | |
| .btn-primary:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 16px rgba(16, 24, 40, 0.2); | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| /* Results */ | |
| .results-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| gap: 20px; | |
| } | |
| .video-card { | |
| border: 1px solid #e0e0e0; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| } | |
| .video-card-header { | |
| background: #f8f9fa; | |
| padding: 12px 16px; | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| .video-card-body { | |
| padding: 16px; | |
| } | |
| video { | |
| width: 100%; | |
| border-radius: 8px; | |
| background: #000; | |
| } | |
| .frame-preview { | |
| width: 100%; | |
| border-radius: 8px; | |
| background: #f3f4f6; | |
| display: block; | |
| } | |
| .download-btn { | |
| margin-top: 12px; | |
| padding: 10px 16px; | |
| background: #374151; | |
| color: #f9fafb; | |
| text-decoration: none; | |
| border-radius: 6px; | |
| display: inline-block; | |
| font-size: 0.9rem; | |
| } | |
| .download-btn:hover { | |
| background: #1f2933; | |
| } | |
| /* Loading spinner */ | |
| .loading { | |
| display: none; | |
| text-align: center; | |
| padding: 20px; | |
| } | |
| .loading.show { | |
| display: block; | |
| } | |
| .status-line { | |
| margin-top: 12px; | |
| font-size: 0.95rem; | |
| color: #4b5563; | |
| text-align: center; | |
| } | |
| .spinner { | |
| border: 4px solid #e5e7eb; | |
| border-top: 4px solid #1f2933; | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 10px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Perception System</h1> | |
| <div class="main-card"> | |
| <!-- Mode Selection --> | |
| <div class="section"> | |
| <div class="section-title">1. Select Detection Mode</div> | |
| <div class="mode-selector"> | |
| <label class="mode-card selected"> | |
| <input type="radio" name="mode" value="object_detection" checked> | |
| <div class="mode-title">Object Detection</div> | |
| </label> | |
| <label class="mode-card"> | |
| <input type="radio" name="mode" value="segmentation"> | |
| <div class="mode-title">Segmentation</div> | |
| </label> | |
| <label class="mode-card"> | |
| <input type="radio" name="mode" value="drone_detection"> | |
| <div class="mode-title">Drone Detection</div> | |
| </label> | |
| </div> | |
| </div> | |
| <!-- Text Prompts Input (for all modes) --> | |
| <div class="section" id="queriesSection"> | |
| <div class="input-group"> | |
| <label for="queries" id="queriesLabel">Text Prompts (comma-separated)</label> | |
| <input | |
| type="text" | |
| id="queries" | |
| placeholder="person, car, dog, bicycle" | |
| > | |
| <small id="queriesHint" style="color: #666; display: block; margin-top: 5px;"> | |
| Enter objects to detect or segment | |
| </small> | |
| </div> | |
| </div> | |
| <!-- Detector Selection --> | |
| <div class="section" id="detectorSection"> | |
| <div class="input-group"> | |
| <label for="detector">2. Select Detection Model</label> | |
| <select id="detector"> | |
| <option value="hf_yolov8">YOLOv8 (Fast, COCO classes)</option> | |
| <option value="detr_resnet50">DETR ResNet-50 (Transformer-based)</option> | |
| <option value="grounding_dino">Grounding DINO (Open-vocabulary)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Segmenter Selection --> | |
| <div class="section hidden" id="segmenterSection"> | |
| <div class="input-group"> | |
| <label for="segmenter">2. Select Segmentation Model</label> | |
| <select id="segmenter"> | |
| <option value="sam3">SAM3 (Segment Anything Model 3)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Drone Model Selection --> | |
| <div class="section hidden" id="droneModelSection"> | |
| <div class="input-group"> | |
| <label for="droneModel">2. Select Drone Model</label> | |
| <select id="droneModel" disabled> | |
| <option value="drone_yolo">Drone YOLO (HF pretrained)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Video Upload --> | |
| <div class="section"> | |
| <div class="input-group"> | |
| <label>3. Upload Video</label> | |
| <div class="file-input-wrapper"> | |
| <label class="file-input-label" id="fileLabel" for="videoFile"> | |
| Click to select video file (MP4) | |
| </label> | |
| <input type="file" id="videoFile" accept="video/*"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Process Button --> | |
| <div class="section"> | |
| <button class="btn btn-primary" id="processBtn" disabled> | |
| Process Video | |
| </button> | |
| </div> | |
| <!-- Loading --> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <p>Processing video... This may take a while depending on video length.</p> | |
| </div> | |
| <p class="status-line hidden" id="statusLine"></p> | |
| <!-- Results --> | |
| <div class="section hidden" id="resultsSection"> | |
| <div class="section-title">Results</div> | |
| <div class="results-grid"> | |
| <div class="video-card"> | |
| <div class="video-card-header">First Frame</div> | |
| <div class="video-card-body"> | |
| <img id="firstFrameImage" class="frame-preview" alt="First frame preview"> | |
| </div> | |
| </div> | |
| <div class="video-card"> | |
| <div class="video-card-header">Original Video</div> | |
| <div class="video-card-body"> | |
| <video id="originalVideo" controls></video> | |
| </div> | |
| </div> | |
| <div class="video-card"> | |
| <div class="video-card-header">Processed Video</div> | |
| <div class="video-card-body"> | |
| <video id="processedVideo" controls autoplay loop></video> | |
| <a id="downloadBtn" class="download-btn" download="processed.mp4"> | |
| Download Processed Video | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // State | |
| let selectedMode = 'object_detection'; | |
| let videoFile = null; | |
| // Elements | |
| const modeCards = document.querySelectorAll('.mode-card'); | |
| const queriesSection = document.getElementById('queriesSection'); | |
| const queriesLabel = document.getElementById('queriesLabel'); | |
| const queriesHint = document.getElementById('queriesHint'); | |
| const detectorSection = document.getElementById('detectorSection'); | |
| const segmenterSection = document.getElementById('segmenterSection'); | |
| const droneModelSection = document.getElementById('droneModelSection'); | |
| const fileInput = document.getElementById('videoFile'); | |
| const fileLabel = document.getElementById('fileLabel'); | |
| const processBtn = document.getElementById('processBtn'); | |
| const loading = document.getElementById('loading'); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const originalVideo = document.getElementById('originalVideo'); | |
| const processedVideo = document.getElementById('processedVideo'); | |
| const firstFrameImage = document.getElementById('firstFrameImage'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| let statusPoller = null; | |
| const statusLine = document.getElementById('statusLine'); | |
| // Mode selection handler | |
| modeCards.forEach(card => { | |
| card.addEventListener('click', (e) => { | |
| const input = card.querySelector('input[type="radio"]'); | |
| const mode = input.value; | |
| // Update selected state | |
| modeCards.forEach(c => c.classList.remove('selected')); | |
| card.classList.add('selected'); | |
| selectedMode = mode; | |
| // Update query label and hint based on mode | |
| if (mode === 'object_detection') { | |
| queriesLabel.textContent = 'Objects to Detect (comma-separated)'; | |
| queriesHint.textContent = 'Example: person, car, dog, bicycle'; | |
| detectorSection.classList.remove('hidden'); | |
| segmenterSection.classList.add('hidden'); | |
| droneModelSection.classList.add('hidden'); | |
| } else if (mode === 'segmentation') { | |
| queriesLabel.textContent = 'Objects to Segment (comma-separated)'; | |
| queriesHint.textContent = 'Example: person, car, building, tree'; | |
| detectorSection.classList.add('hidden'); | |
| segmenterSection.classList.remove('hidden'); | |
| droneModelSection.classList.add('hidden'); | |
| } else if (mode === 'drone_detection') { | |
| queriesLabel.textContent = 'Optional Labels (comma-separated)'; | |
| queriesHint.textContent = 'Example: drone, quadcopter'; | |
| detectorSection.classList.add('hidden'); | |
| segmenterSection.classList.add('hidden'); | |
| droneModelSection.classList.remove('hidden'); | |
| } | |
| // Always show queries section | |
| queriesSection.classList.remove('hidden'); | |
| }); | |
| }); | |
| // File input handler | |
| fileInput.addEventListener('change', (e) => { | |
| videoFile = e.target.files[0]; | |
| if (videoFile) { | |
| fileLabel.textContent = `✅ ${videoFile.name}`; | |
| fileLabel.classList.add('has-file'); | |
| processBtn.disabled = false; | |
| // Preview original video | |
| originalVideo.src = URL.createObjectURL(videoFile); | |
| } | |
| }); | |
| // Process button handler | |
| processBtn.addEventListener('click', async () => { | |
| if (!videoFile) { | |
| alert('Please select a video file first.'); | |
| return; | |
| } | |
| // Show loading | |
| processBtn.disabled = true; | |
| loading.classList.add('show'); | |
| resultsSection.classList.add('hidden'); | |
| if (statusPoller) { | |
| clearInterval(statusPoller); | |
| statusPoller = null; | |
| } | |
| firstFrameImage.removeAttribute('src'); | |
| processedVideo.removeAttribute('src'); | |
| processedVideo.load(); | |
| downloadBtn.removeAttribute('href'); | |
| statusLine.classList.add('hidden'); | |
| statusLine.textContent = ''; | |
| // Prepare form data | |
| const formData = new FormData(); | |
| formData.append('video', videoFile); | |
| formData.append('mode', selectedMode); | |
| formData.append('queries', document.getElementById('queries').value); | |
| formData.append('detector', document.getElementById('detector').value); | |
| formData.append('segmenter', document.getElementById('segmenter').value); | |
| try { | |
| const response = await fetch('/detect/async', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| const error = await response.json(); | |
| alert(`Error: ${error.detail || error.error || 'Processing failed'}`); | |
| return; | |
| } | |
| const data = await response.json(); | |
| firstFrameImage.src = `${data.first_frame_url}?t=${Date.now()}`; | |
| resultsSection.classList.remove('hidden'); | |
| statusLine.textContent = 'Status: processing'; | |
| statusLine.classList.remove('hidden'); | |
| statusPoller = setInterval(async () => { | |
| try { | |
| const statusResponse = await fetch(data.status_url); | |
| if (!statusResponse.ok) { | |
| clearInterval(statusPoller); | |
| statusPoller = null; | |
| statusLine.textContent = 'Status: expired (please re-upload)'; | |
| alert('Job expired. Please re-upload the video.'); | |
| return; | |
| } | |
| const statusData = await statusResponse.json(); | |
| if (statusData.status === 'completed') { | |
| clearInterval(statusPoller); | |
| statusPoller = null; | |
| statusLine.textContent = 'Status: completed'; | |
| const videoResponse = await fetch(data.video_url); | |
| if (!videoResponse.ok) { | |
| alert('Failed to fetch processed video.'); | |
| return; | |
| } | |
| const blob = await videoResponse.blob(); | |
| const videoUrl = URL.createObjectURL(blob); | |
| processedVideo.src = videoUrl; | |
| downloadBtn.href = videoUrl; | |
| } else if (statusData.status === 'failed') { | |
| clearInterval(statusPoller); | |
| statusPoller = null; | |
| statusLine.textContent = 'Status: failed'; | |
| alert(statusData.error || 'Processing failed.'); | |
| } else if (statusData.status) { | |
| statusLine.textContent = `Status: ${statusData.status}`; | |
| } | |
| } catch (pollError) { | |
| clearInterval(statusPoller); | |
| statusPoller = null; | |
| console.error('Polling error:', pollError); | |
| statusLine.textContent = 'Status: polling error'; | |
| alert('Polling error: ' + pollError.message); | |
| } | |
| }, 10000); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| alert('Network error: ' + error.message); | |
| } finally { | |
| loading.classList.remove('show'); | |
| processBtn.disabled = false; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |