Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>NudeNet - Nudity Detection API</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px; | |
| } | |
| .container { | |
| background: white; | |
| border-radius: 12px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| max-width: 900px; | |
| width: 100%; | |
| padding: 40px; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 40px; | |
| } | |
| .header h1 { | |
| color: #333; | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| } | |
| .header p { | |
| color: #666; | |
| font-size: 1.1em; | |
| } | |
| .documentation-link { | |
| display: inline-block; | |
| margin-top: 15px; | |
| padding: 10px 20px; | |
| background-color: #667eea; | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 6px; | |
| font-size: 0.9em; | |
| transition: background-color 0.3s; | |
| } | |
| .documentation-link:hover { | |
| background-color: #764ba2; | |
| } | |
| .tabs { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 30px; | |
| border-bottom: 2px solid #e0e0e0; | |
| } | |
| .tab-button { | |
| padding: 12px 24px; | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 1em; | |
| color: #666; | |
| border-bottom: 3px solid transparent; | |
| transition: all 0.3s; | |
| font-weight: 500; | |
| } | |
| .tab-button:hover { | |
| color: #667eea; | |
| } | |
| .tab-button.active { | |
| color: #667eea; | |
| border-bottom-color: #667eea; | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .upload-area { | |
| border: 3px dashed #667eea; | |
| border-radius: 8px; | |
| padding: 40px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| margin-bottom: 20px; | |
| } | |
| .upload-area:hover { | |
| background-color: #f5f5f5; | |
| border-color: #764ba2; | |
| } | |
| .upload-area.dragover { | |
| background-color: #f0f0ff; | |
| border-color: #764ba2; | |
| } | |
| .upload-icon { | |
| font-size: 3em; | |
| margin-bottom: 15px; | |
| } | |
| .upload-area h3 { | |
| color: #333; | |
| margin-bottom: 10px; | |
| } | |
| .upload-area p { | |
| color: #666; | |
| margin-bottom: 15px; | |
| } | |
| input[type="file"] { | |
| display: none; | |
| } | |
| .model-selector { | |
| margin-bottom: 20px; | |
| } | |
| .model-selector label { | |
| display: inline-block; | |
| margin-right: 20px; | |
| color: #333; | |
| font-weight: 500; | |
| } | |
| .radio-group { | |
| display: flex; | |
| gap: 20px; | |
| } | |
| .radio-group label { | |
| display: flex; | |
| align-items: center; | |
| margin: 0; | |
| cursor: pointer; | |
| } | |
| .radio-group input[type="radio"] { | |
| margin-right: 8px; | |
| cursor: pointer; | |
| } | |
| .button-group { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| } | |
| button { | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 1em; | |
| font-weight: 600; | |
| transition: all 0.3s; | |
| } | |
| .btn-primary { | |
| background-color: #667eea; | |
| color: white; | |
| flex: 1; | |
| } | |
| .btn-primary:hover:not(:disabled) { | |
| background-color: #764ba2; | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); | |
| } | |
| .btn-primary:disabled { | |
| background-color: #ccc; | |
| cursor: not-allowed; | |
| } | |
| .btn-secondary { | |
| background-color: #e0e0e0; | |
| color: #333; | |
| } | |
| .btn-secondary:hover { | |
| background-color: #d0d0d0; | |
| } | |
| .preview-section { | |
| margin-bottom: 30px; | |
| } | |
| .preview-section h3 { | |
| color: #333; | |
| margin-bottom: 15px; | |
| } | |
| .preview-image { | |
| max-width: 100%; | |
| max-height: 400px; | |
| border-radius: 8px; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
| } | |
| .results-section { | |
| background-color: #f5f5f5; | |
| padding: 20px; | |
| border-radius: 8px; | |
| margin-top: 20px; | |
| } | |
| .results-section h3 { | |
| color: #333; | |
| margin-bottom: 15px; | |
| } | |
| .result-item { | |
| background: white; | |
| padding: 15px; | |
| border-radius: 6px; | |
| margin-bottom: 10px; | |
| border-left: 4px solid #667eea; | |
| } | |
| .result-label { | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| .result-value { | |
| color: #666; | |
| margin-top: 5px; | |
| } | |
| .loading { | |
| display: none; | |
| text-align: center; | |
| padding: 20px; | |
| } | |
| .loading.active { | |
| 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 10px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .error { | |
| background-color: #fee; | |
| border-left: 4px solid #f44; | |
| color: #c33; | |
| padding: 15px; | |
| border-radius: 6px; | |
| margin-bottom: 20px; | |
| } | |
| .success { | |
| background-color: #efe; | |
| border-left: 4px solid #4a4; | |
| color: #3a3; | |
| padding: 15px; | |
| border-radius: 6px; | |
| margin-bottom: 20px; | |
| } | |
| .detection-box { | |
| background: white; | |
| padding: 12px; | |
| border-radius: 6px; | |
| margin-bottom: 8px; | |
| border-left: 4px solid #667eea; | |
| } | |
| .detection-box strong { | |
| color: #667eea; | |
| } | |
| .base64-input { | |
| width: 100%; | |
| min-height: 150px; | |
| padding: 15px; | |
| border: 1px solid #ddd; | |
| border-radius: 6px; | |
| font-family: monospace; | |
| font-size: 0.9em; | |
| resize: vertical; | |
| margin-bottom: 15px; | |
| } | |
| .filename { | |
| color: #667eea; | |
| font-weight: 600; | |
| margin-bottom: 10px; | |
| } | |
| .video-info { | |
| background-color: #f9f9f9; | |
| padding: 15px; | |
| border-radius: 6px; | |
| margin-bottom: 15px; | |
| border-left: 4px solid #667eea; | |
| } | |
| .video-info p { | |
| color: #666; | |
| margin-bottom: 5px; | |
| } | |
| .timestamp { | |
| color: #667eea; | |
| font-weight: 600; | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 20px; | |
| } | |
| .header h1 { | |
| font-size: 1.8em; | |
| } | |
| .tabs { | |
| flex-wrap: wrap; | |
| } | |
| .tab-button { | |
| padding: 10px 16px; | |
| font-size: 0.9em; | |
| } | |
| .button-group { | |
| flex-direction: column; | |
| } | |
| .radio-group { | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .upload-area { | |
| padding: 20px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🔍 NudeNet Detection API</h1> | |
| <p>Test nudity detection in images and videos</p> | |
| <a href="/docs" class="documentation-link">📖 View API Documentation</a> | |
| </div> | |
| <div class="tabs"> | |
| <button class="tab-button active" data-tab="image">Image Detection</button> | |
| <button class="tab-button" data-tab="base64">Base64 Image</button> | |
| <button class="tab-button" data-tab="video">Video Detection</button> | |
| </div> | |
| <!-- Image Detection Tab --> | |
| <div id="image" class="tab-content active"> | |
| <div class="model-selector"> | |
| <label>Select Model:</label> | |
| <div class="radio-group"> | |
| <label> | |
| <input type="radio" name="imageModel" value="320n" checked> | |
| 320n (Faster, Less Accurate) | |
| </label> | |
| <label> | |
| <input type="radio" name="imageModel" value="640m"> | |
| 640m (Slower, More Accurate) | |
| </label> | |
| </div> | |
| </div> | |
| <div class="upload-area" id="imageUploadArea"> | |
| <div class="upload-icon">📸</div> | |
| <h3>Drag and drop your image here</h3> | |
| <p>or click to select an image</p> | |
| <p style="font-size: 0.9em; color: #999;">Supported formats: JPG, PNG, GIF, BMP</p> | |
| <input type="file" id="imageInput" accept="image/*"> | |
| </div> | |
| <div id="imageFilename" class="filename" style="display: none;"></div> | |
| <div class="preview-section" id="imagePreviewSection" style="display: none;"> | |
| <h3>Preview</h3> | |
| <img id="imagePreview" class="preview-image" alt="Preview"> | |
| </div> | |
| <div class="button-group"> | |
| <button class="btn-primary" id="detectImageBtn" style="display: none;">Detect Nudity</button> | |
| <button class="btn-secondary" id="clearImageBtn" onclick="clearImage()">Clear</button> | |
| </div> | |
| <div class="loading" id="imageLoading"> | |
| <div class="spinner"></div> | |
| <p>Analyzing image...</p> | |
| </div> | |
| <div id="imageError" class="error" style="display: none;"></div> | |
| <div id="imageResults" class="results-section" style="display: none;"> | |
| <h3>Detection Results</h3> | |
| <div id="imageResultsContent"></div> | |
| </div> | |
| </div> | |
| <!-- Base64 Image Tab --> | |
| <div id="base64" class="tab-content"> | |
| <div class="model-selector"> | |
| <label>Select Model:</label> | |
| <div class="radio-group"> | |
| <label> | |
| <input type="radio" name="base64Model" value="320n" checked> | |
| 320n (Faster, Less Accurate) | |
| </label> | |
| <label> | |
| <input type="radio" name="base64Model" value="640m"> | |
| 640m (Slower, More Accurate) | |
| </label> | |
| </div> | |
| </div> | |
| <h3>Base64 Encoded Image</h3> | |
| <textarea id="base64Input" class="base64-input" placeholder="Paste your base64 encoded image here..."></textarea> | |
| <div class="button-group"> | |
| <button class="btn-primary" id="detectBase64Btn">Detect Nudity</button> | |
| <button class="btn-secondary" onclick="clearBase64()">Clear</button> | |
| </div> | |
| <div class="loading" id="base64Loading"> | |
| <div class="spinner"></div> | |
| <p>Analyzing image...</p> | |
| </div> | |
| <div id="base64Error" class="error" style="display: none;"></div> | |
| <div id="base64Results" class="results-section" style="display: none;"> | |
| <h3>Detection Results</h3> | |
| <div id="base64ResultsContent"></div> | |
| </div> | |
| </div> | |
| <!-- Video Detection Tab --> | |
| <div id="video" class="tab-content"> | |
| <div class="model-selector"> | |
| <label>Select Model:</label> | |
| <div class="radio-group"> | |
| <label> | |
| <input type="radio" name="videoModel" value="320n" checked> | |
| 320n (Faster, Less Accurate) | |
| </label> | |
| <label> | |
| <input type="radio" name="videoModel" value="640m"> | |
| 640m (Slower, More Accurate) | |
| </label> | |
| </div> | |
| </div> | |
| <div class="model-selector"> | |
| <label for="frameInterval">Frame Interval (seconds):</label> | |
| <input type="number" id="frameInterval" value="1" min="0" max="30" step="0.1" style="padding: 8px; border: 1px solid #ddd; border-radius: 4px; width: 100px;"> | |
| <p style="font-size: 0.9em; color: #666; margin-top: 5px;">0 = every frame, 1 = every second, etc.</p> | |
| </div> | |
| <div class="upload-area" id="videoUploadArea"> | |
| <div class="upload-icon">🎥</div> | |
| <h3>Drag and drop your video here</h3> | |
| <p>or click to select a video</p> | |
| <p style="font-size: 0.9em; color: #999;">Supported formats: MP4, AVI, MOV, MKV</p> | |
| <input type="file" id="videoInput" accept="video/*"> | |
| </div> | |
| <div id="videoFilename" class="filename" style="display: none;"></div> | |
| <div class="button-group"> | |
| <button class="btn-primary" id="detectVideoBtn" style="display: none;">Detect Nudity in Video</button> | |
| <button class="btn-secondary" id="clearVideoBtn" onclick="clearVideo()">Clear</button> | |
| </div> | |
| <div class="loading" id="videoLoading"> | |
| <div class="spinner"></div> | |
| <p>Analyzing video... This may take a while</p> | |
| </div> | |
| <div id="videoError" class="error" style="display: none;"></div> | |
| <div id="videoResults" class="results-section" style="display: none;"> | |
| <h3>Detection Results</h3> | |
| <div id="videoResultsContent"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Tab switching | |
| document.querySelectorAll('.tab-button').forEach(button => { | |
| button.addEventListener('click', () => { | |
| const tabName = button.dataset.tab; | |
| // Hide all tabs | |
| document.querySelectorAll('.tab-content').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| // Remove active class from all buttons | |
| document.querySelectorAll('.tab-button').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| // Show selected tab and mark button as active | |
| document.getElementById(tabName).classList.add('active'); | |
| button.classList.add('active'); | |
| }); | |
| }); | |
| // Image Upload | |
| const imageUploadArea = document.getElementById('imageUploadArea'); | |
| const imageInput = document.getElementById('imageInput'); | |
| let selectedImageFile = null; | |
| imageUploadArea.addEventListener('click', () => imageInput.click()); | |
| imageUploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| imageUploadArea.classList.add('dragover'); | |
| }); | |
| imageUploadArea.addEventListener('dragleave', () => { | |
| imageUploadArea.classList.remove('dragover'); | |
| }); | |
| imageUploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| imageUploadArea.classList.remove('dragover'); | |
| handleImageUpload(e.dataTransfer.files[0]); | |
| }); | |
| imageInput.addEventListener('change', (e) => { | |
| if (e.target.files[0]) { | |
| handleImageUpload(e.target.files[0]); | |
| } | |
| }); | |
| function handleImageUpload(file) { | |
| if (!file.type.startsWith('image/')) { | |
| showImageError('Please upload a valid image file'); | |
| return; | |
| } | |
| selectedImageFile = file; | |
| document.getElementById('imageFilename').textContent = `File: ${file.name}`; | |
| document.getElementById('imageFilename').style.display = 'block'; | |
| document.getElementById('imagePreviewSection').style.display = 'block'; | |
| document.getElementById('detectImageBtn').style.display = 'block'; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| document.getElementById('imagePreview').src = e.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| document.getElementById('detectImageBtn').addEventListener('click', async () => { | |
| if (!selectedImageFile) return; | |
| const model = document.querySelector('input[name="imageModel"]:checked').value; | |
| const use640m = model === '640m'; | |
| const formData = new FormData(); | |
| formData.append('image', selectedImageFile); | |
| try { | |
| showImageLoading(true); | |
| hideImageError(); | |
| document.getElementById('imageResults').style.display = 'none'; | |
| const response = await fetch(`/detect?use_640m=${use640m}`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| displayImageResults(data.result); | |
| } catch (error) { | |
| showImageError(`Error: ${error.message}`); | |
| } finally { | |
| showImageLoading(false); | |
| } | |
| }); | |
| function clearImage() { | |
| selectedImageFile = null; | |
| imageInput.value = ''; | |
| document.getElementById('imageFilename').style.display = 'none'; | |
| document.getElementById('imagePreviewSection').style.display = 'none'; | |
| document.getElementById('detectImageBtn').style.display = 'none'; | |
| document.getElementById('imageResults').style.display = 'none'; | |
| hideImageError(); | |
| } | |
| function displayImageResults(results) { | |
| const resultsContent = document.getElementById('imageResultsContent'); | |
| if (!results || results.length === 0) { | |
| resultsContent.innerHTML = '<div class="success">✓ No nudity detected!</div>'; | |
| } else { | |
| let html = ''; | |
| results.forEach((detection, index) => { | |
| html += ` | |
| <div class="detection-box"> | |
| <strong>Detection ${index + 1}</strong><br> | |
| Class: ${detection.class}<br> | |
| Confidence: ${(detection.confidence * 100).toFixed(2)}% | |
| </div> | |
| `; | |
| }); | |
| resultsContent.innerHTML = html; | |
| } | |
| document.getElementById('imageResults').style.display = 'block'; | |
| } | |
| function showImageError(message) { | |
| const errorDiv = document.getElementById('imageError'); | |
| errorDiv.textContent = message; | |
| errorDiv.style.display = 'block'; | |
| } | |
| function hideImageError() { | |
| document.getElementById('imageError').style.display = 'none'; | |
| } | |
| function showImageLoading(show) { | |
| document.getElementById('imageLoading').classList.toggle('active', show); | |
| } | |
| // Base64 Upload | |
| document.getElementById('detectBase64Btn').addEventListener('click', async () => { | |
| const base64Input = document.getElementById('base64Input').value.trim(); | |
| if (!base64Input) { | |
| showBase64Error('Please paste a base64 encoded image'); | |
| return; | |
| } | |
| const model = document.querySelector('input[name="base64Model"]:checked').value; | |
| const use640m = model === '640m'; | |
| try { | |
| showBase64Loading(true); | |
| hideBase64Error(); | |
| document.getElementById('base64Results').style.display = 'none'; | |
| const response = await fetch(`/detect_base64?use_640m=${use640m}`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ image: base64Input }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| displayBase64Results(data.result); | |
| } catch (error) { | |
| showBase64Error(`Error: ${error.message}`); | |
| } finally { | |
| showBase64Loading(false); | |
| } | |
| }); | |
| function clearBase64() { | |
| document.getElementById('base64Input').value = ''; | |
| document.getElementById('base64Results').style.display = 'none'; | |
| hideBase64Error(); | |
| } | |
| function displayBase64Results(results) { | |
| const resultsContent = document.getElementById('base64ResultsContent'); | |
| if (!results || results.length === 0) { | |
| resultsContent.innerHTML = '<div class="success">✓ No nudity detected!</div>'; | |
| } else { | |
| let html = ''; | |
| results.forEach((detection, index) => { | |
| html += ` | |
| <div class="detection-box"> | |
| <strong>Detection ${index + 1}</strong><br> | |
| Class: ${detection.class}<br> | |
| Confidence: ${(detection.confidence * 100).toFixed(2)}% | |
| </div> | |
| `; | |
| }); | |
| resultsContent.innerHTML = html; | |
| } | |
| document.getElementById('base64Results').style.display = 'block'; | |
| } | |
| function showBase64Error(message) { | |
| const errorDiv = document.getElementById('base64Error'); | |
| errorDiv.textContent = message; | |
| errorDiv.style.display = 'block'; | |
| } | |
| function hideBase64Error() { | |
| document.getElementById('base64Error').style.display = 'none'; | |
| } | |
| function showBase64Loading(show) { | |
| document.getElementById('base64Loading').classList.toggle('active', show); | |
| } | |
| // Video Upload | |
| const videoUploadArea = document.getElementById('videoUploadArea'); | |
| const videoInput = document.getElementById('videoInput'); | |
| let selectedVideoFile = null; | |
| videoUploadArea.addEventListener('click', () => videoInput.click()); | |
| videoUploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| videoUploadArea.classList.add('dragover'); | |
| }); | |
| videoUploadArea.addEventListener('dragleave', () => { | |
| videoUploadArea.classList.remove('dragover'); | |
| }); | |
| videoUploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| videoUploadArea.classList.remove('dragover'); | |
| handleVideoUpload(e.dataTransfer.files[0]); | |
| }); | |
| videoInput.addEventListener('change', (e) => { | |
| if (e.target.files[0]) { | |
| handleVideoUpload(e.target.files[0]); | |
| } | |
| }); | |
| function handleVideoUpload(file) { | |
| if (!file.type.startsWith('video/')) { | |
| showVideoError('Please upload a valid video file'); | |
| return; | |
| } | |
| selectedVideoFile = file; | |
| document.getElementById('videoFilename').textContent = `File: ${file.name}`; | |
| document.getElementById('videoFilename').style.display = 'block'; | |
| document.getElementById('detectVideoBtn').style.display = 'block'; | |
| } | |
| document.getElementById('detectVideoBtn').addEventListener('click', async () => { | |
| if (!selectedVideoFile) return; | |
| const model = document.querySelector('input[name="videoModel"]:checked').value; | |
| const use640m = model === '640m'; | |
| const frameInterval = document.getElementById('frameInterval').value; | |
| const formData = new FormData(); | |
| formData.append('video', selectedVideoFile); | |
| try { | |
| showVideoLoading(true); | |
| hideVideoError(); | |
| document.getElementById('videoResults').style.display = 'none'; | |
| const response = await fetch(`/detect_video?frame_interval=${frameInterval}&use_640m=${use640m}`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| displayVideoResults(data.results); | |
| } catch (error) { | |
| showVideoError(`Error: ${error.message}`); | |
| } finally { | |
| showVideoLoading(false); | |
| } | |
| }); | |
| function clearVideo() { | |
| selectedVideoFile = null; | |
| videoInput.value = ''; | |
| document.getElementById('videoFilename').style.display = 'none'; | |
| document.getElementById('detectVideoBtn').style.display = 'none'; | |
| document.getElementById('videoResults').style.display = 'none'; | |
| hideVideoError(); | |
| } | |
| function displayVideoResults(results) { | |
| const resultsContent = document.getElementById('videoResultsContent'); | |
| if (!results || results.length === 0) { | |
| resultsContent.innerHTML = '<div class="success">✓ No nudity detected in the video!</div>'; | |
| } else { | |
| let html = ''; | |
| results.forEach((result) => { | |
| html += ` | |
| <div class="video-info"> | |
| <p><span class="timestamp">Timestamp: ${result.timestamp}s</span></p> | |
| `; | |
| result.detections.forEach((detection, index) => { | |
| html += ` | |
| <div class="detection-box"> | |
| <strong>Detection ${index + 1}</strong><br> | |
| Class: ${detection.class}<br> | |
| Confidence: ${(detection.confidence * 100).toFixed(2)}% | |
| </div> | |
| `; | |
| }); | |
| html += '</div>'; | |
| }); | |
| resultsContent.innerHTML = html; | |
| } | |
| document.getElementById('videoResults').style.display = 'block'; | |
| } | |
| function showVideoError(message) { | |
| const errorDiv = document.getElementById('videoError'); | |
| errorDiv.textContent = message; | |
| errorDiv.style.display = 'block'; | |
| } | |
| function hideVideoError() { | |
| document.getElementById('videoError').style.display = 'none'; | |
| } | |
| function showVideoLoading(show) { | |
| document.getElementById('videoLoading').classList.toggle('active', show); | |
| } | |
| </script> | |
| </body> | |
| </html> | |