ISR / demo.html
Zhen Ye
reload input
422c79a
raw
history blame
20.7 kB
<!DOCTYPE html>
<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>