Download index (2).html from TDN-M/plugin: direct link, hf CLI and curl.
- Browser
- Download file 37.9 kB
-
https://huggingface.co/spaces/TDN-M/plugin/resolve/main/index%20(2).html
- Command line
-
hf download 'hf://spaces/TDN-M/plugin/index (2).html'
-
curl -L -o 'index (2).html' 'https://huggingface.co/spaces/TDN-M/plugin/resolve/main/index%20(2).html'
37.9 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>OBS AI Background Remover</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.11.0/dist/tf.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/body-pix@2.0.5/dist/body-pix.min.js"></script> | |
| <style> | |
| .video-container { | |
| position: relative; | |
| width: 100%; | |
| height: 0; | |
| padding-bottom: 56.25%; /* 16:9 aspect ratio */ | |
| } | |
| .video-element { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .processing-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| font-size: 1.5rem; | |
| z-index: 10; | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| .settings-panel { | |
| transition: all 0.3s ease; | |
| max-height: 0; | |
| overflow: hidden; | |
| } | |
| .settings-panel.open { | |
| max-height: 500px; | |
| } | |
| .preview-thumbnail { | |
| transition: all 0.2s ease; | |
| } | |
| .preview-thumbnail:hover { | |
| transform: scale(1.05); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| } | |
| .preview-thumbnail.active { | |
| border: 3px solid #3b82f6; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <div class="flex flex-col md:flex-row gap-8"> | |
| <!-- Main Content --> | |
| <div class="flex-1"> | |
| <div class="bg-gray-800 rounded-lg shadow-xl p-6"> | |
| <h1 class="text-3xl font-bold mb-6 text-blue-400">OBS AI Background Remover</h1> | |
| <!-- Video Preview Section --> | |
| <div class="mb-8"> | |
| <h2 class="text-xl font-semibold mb-4">Live Preview</h2> | |
| <div class="video-container bg-gray-700 rounded-lg overflow-hidden relative"> | |
| <video id="videoInput" class="video-element" autoplay muted></video> | |
| <canvas id="outputCanvas" class="video-element hidden"></canvas> | |
| <div id="processingOverlay" class="processing-overlay hidden"> | |
| <div class="text-center"> | |
| <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-400 mx-auto mb-4"></div> | |
| <p>Loading AI Model...</p> | |
| </div> | |
| </div> | |
| <div id="noVideoOverlay" class="processing-overlay"> | |
| <p>No video source selected</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Background Replacement Section --> | |
| <div class="mb-8"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold">Background Replacement</h2> | |
| <button id="settingsToggle" class="text-blue-400 hover:text-blue-300"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> | |
| </svg> | |
| </button> | |
| </div> | |
| <!-- Settings Panel --> | |
| <div id="settingsPanel" class="settings-panel bg-gray-700 rounded-lg p-4 mb-4"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">AI Model Quality</label> | |
| <select id="modelQuality" class="w-full bg-gray-600 border border-gray-500 rounded-md px-3 py-2"> | |
| <option value="0.5">Fast (Low Quality)</option> | |
| <option value="0.75" selected>Balanced</option> | |
| <option value="1.0">High Quality (Slow)</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Edge Smoothness</label> | |
| <select id="edgeSmoothness" class="w-full bg-gray-600 border border-gray-500 rounded-md px-3 py-2"> | |
| <option value="1">Low</option> | |
| <option value="2" selected>Medium</option> | |
| <option value="3">High</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Background Blur</label> | |
| <input id="bgBlur" type="range" min="0" max="10" value="0" class="w-full"> | |
| <div class="flex justify-between text-xs text-gray-400"> | |
| <span>Off</span> | |
| <span>Max</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Foreground Brightness</label> | |
| <input id="fgBrightness" type="range" min="80" max="120" value="100" class="w-full"> | |
| <div class="flex justify-between text-xs text-gray-400"> | |
| <span>Darker</span> | |
| <span>Brighter</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Background Selection --> | |
| <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4"> | |
| <!-- Transparent Background --> | |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer active" id="bgTransparent"> | |
| <div class="aspect-w-16 aspect-h-9 bg-gradient-to-br from-gray-600 to-gray-800 flex items-center justify-center"> | |
| <div class="text-center p-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mx-auto text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> | |
| </svg> | |
| <span class="text-xs mt-1">Transparent</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Color Backgrounds --> | |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer" id="bgBlack"> | |
| <div class="aspect-w-16 aspect-h-9 bg-black flex items-center justify-center"> | |
| <span class="text-xs">Solid Black</span> | |
| </div> | |
| </div> | |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer" id="bgGray"> | |
| <div class="aspect-w-16 aspect-h-9 bg-gray-600 flex items-center justify-center"> | |
| <span class="text-xs">Solid Gray</span> | |
| </div> | |
| </div> | |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer" id="bgGreen"> | |
| <div class="aspect-w-16 aspect-h-9 bg-green-600 flex items-center justify-center"> | |
| <span class="text-xs">Solid Green</span> | |
| </div> | |
| </div> | |
| <!-- Custom Background Upload --> | |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer relative" id="bgCustom"> | |
| <div class="aspect-w-16 aspect-h-9 bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center"> | |
| <div class="text-center p-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mx-auto text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> | |
| </svg> | |
| <span class="text-xs mt-1">Custom Image</span> | |
| </div> | |
| </div> | |
| <input type="file" id="customBgInput" accept="image/*, video/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"> | |
| </div> | |
| <!-- Video Background Upload --> | |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer relative" id="bgVideo"> | |
| <div class="aspect-w-16 aspect-h-9 bg-gradient-to-br from-red-600 to-yellow-600 flex items-center justify-center"> | |
| <div class="text-center p-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mx-auto text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /> | |
| </svg> | |
| <span class="text-xs mt-1">Custom Video</span> | |
| </div> | |
| </div> | |
| <input type="file" id="customVideoInput" accept="video/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"> | |
| </div> | |
| <!-- Blur Background --> | |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer" id="bgBlurred"> | |
| <div class="aspect-w-16 aspect-h-9 bg-gray-600 flex items-center justify-center relative overflow-hidden"> | |
| <div class="absolute inset-0 bg-gray-400 filter blur-md"></div> | |
| <span class="text-xs relative z-10">Blurred</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Controls Sidebar --> | |
| <div class="w-full md:w-80"> | |
| <div class="bg-gray-800 rounded-lg shadow-xl p-6 sticky top-6"> | |
| <h2 class="text-xl font-semibold mb-4">Controls</h2> | |
| <!-- Video Source Selection --> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium mb-2">Video Source</label> | |
| <select id="videoSource" class="w-full bg-gray-600 border border-gray-500 rounded-md px-3 py-2"> | |
| <option value="">Select a video source</option> | |
| </select> | |
| </div> | |
| <!-- Toggle Buttons --> | |
| <div class="space-y-4 mb-6"> | |
| <div> | |
| <label class="inline-flex items-center"> | |
| <input type="checkbox" id="toggleEffect" class="form-checkbox h-5 w-5 text-blue-400"> | |
| <span class="ml-2">Enable Background Removal</span> | |
| </label> | |
| </div> | |
| <div> | |
| <label class="inline-flex items-center"> | |
| <input type="checkbox" id="togglePreview" class="form-checkbox h-5 w-5 text-blue-400" checked> | |
| <span class="ml-2">Show Preview</span> | |
| </label> | |
| </div> | |
| </div> | |
| <!-- Performance Stats --> | |
| <div class="bg-gray-700 rounded-lg p-4 mb-6"> | |
| <h3 class="text-sm font-medium mb-2 text-gray-400">Performance</h3> | |
| <div class="grid grid-cols-2 gap-2 text-sm"> | |
| <div> | |
| <div class="text-gray-400">Processing Time:</div> | |
| <div id="processingTime">0 ms</div> | |
| </div> | |
| <div> | |
| <div class="text-gray-400">FPS:</div> | |
| <div id="fpsCounter">0</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- OBS Integration --> | |
| <div class="bg-blue-900 bg-opacity-20 rounded-lg p-4 border border-blue-800"> | |
| <h3 class="text-sm font-medium mb-2 text-blue-400">OBS Integration</h3> | |
| <p class="text-xs text-gray-400 mb-3">Add this as a Browser Source in OBS with these settings:</p> | |
| <div class="text-xs space-y-1"> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-400">Width:</span> | |
| <span>1920</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-400">Height:</span> | |
| <span>1080</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-400">Custom CSS:</span> | |
| <span>None</span> | |
| </div> | |
| </div> | |
| <button id="copyOBSLink" class="mt-3 w-full bg-blue-600 hover:bg-blue-500 text-white py-2 px-4 rounded-md text-sm flex items-center justify-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" /> | |
| </svg> | |
| Copy OBS Browser Source URL | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // DOM Elements | |
| const videoInput = document.getElementById('videoInput'); | |
| const outputCanvas = document.getElementById('outputCanvas'); | |
| const processingOverlay = document.getElementById('processingOverlay'); | |
| const noVideoOverlay = document.getElementById('noVideoOverlay'); | |
| const videoSourceSelect = document.getElementById('videoSource'); | |
| const toggleEffect = document.getElementById('toggleEffect'); | |
| const togglePreview = document.getElementById('togglePreview'); | |
| const modelQuality = document.getElementById('modelQuality'); | |
| const edgeSmoothness = document.getElementById('edgeSmoothness'); | |
| const bgBlur = document.getElementById('bgBlur'); | |
| const fgBrightness = document.getElementById('fgBrightness'); | |
| const settingsToggle = document.getElementById('settingsToggle'); | |
| const settingsPanel = document.getElementById('settingsPanel'); | |
| const processingTimeElement = document.getElementById('processingTime'); | |
| const fpsCounterElement = document.getElementById('fpsCounter'); | |
| const copyOBSLink = document.getElementById('copyOBSLink'); | |
| // Background selection elements | |
| const bgThumbnails = document.querySelectorAll('.preview-thumbnail'); | |
| const bgTransparent = document.getElementById('bgTransparent'); | |
| const bgBlack = document.getElementById('bgBlack'); | |
| const bgGray = document.getElementById('bgGray'); | |
| const bgGreen = document.getElementById('bgGreen'); | |
| const bgCustom = document.getElementById('bgCustom'); | |
| const bgVideo = document.getElementById('bgVideo'); | |
| const bgBlurred = document.getElementById('bgBlurred'); | |
| const customBgInput = document.getElementById('customBgInput'); | |
| const customVideoInput = document.getElementById('customVideoInput'); | |
| // Variables | |
| let net; | |
| let isProcessing = false; | |
| let lastTimestamp = 0; | |
| let frameCount = 0; | |
| let fps = 0; | |
| let currentBackground = 'transparent'; | |
| let customBackgroundImage = null; | |
| let customBackgroundVideo = null; | |
| let backgroundVideoElement = null; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', async () => { | |
| // Load video sources | |
| loadVideoSources(); | |
| // Initialize settings panel toggle | |
| settingsToggle.addEventListener('click', () => { | |
| settingsPanel.classList.toggle('open'); | |
| }); | |
| // Initialize background selection | |
| bgThumbnails.forEach(thumb => { | |
| thumb.addEventListener('click', () => { | |
| bgThumbnails.forEach(t => t.classList.remove('active')); | |
| thumb.classList.add('active'); | |
| if (thumb === bgTransparent) { | |
| currentBackground = 'transparent'; | |
| } else if (thumb === bgBlack) { | |
| currentBackground = 'black'; | |
| } else if (thumb === bgGray) { | |
| currentBackground = 'gray'; | |
| } else if (thumb === bgGreen) { | |
| currentBackground = 'green'; | |
| } else if (thumb === bgCustom) { | |
| currentBackground = 'customImage'; | |
| customBgInput.click(); | |
| } else if (thumb === bgVideo) { | |
| currentBackground = 'customVideo'; | |
| customVideoInput.click(); | |
| } else if (thumb === bgBlurred) { | |
| currentBackground = 'blurred'; | |
| } | |
| }); | |
| }); | |
| // Handle custom background upload | |
| customBgInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| customBackgroundImage = new Image(); | |
| customBackgroundImage.src = event.target.result; | |
| customBackgroundImage.onload = () => { | |
| bgCustom.classList.add('active'); | |
| }; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| // Handle custom video background upload | |
| customVideoInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| if (backgroundVideoElement) { | |
| backgroundVideoElement.pause(); | |
| backgroundVideoElement.remove(); | |
| } | |
| backgroundVideoElement = document.createElement('video'); | |
| backgroundVideoElement.autoplay = true; | |
| backgroundVideoElement.loop = true; | |
| backgroundVideoElement.muted = true; | |
| backgroundVideoElement.src = URL.createObjectURL(file); | |
| backgroundVideoElement.style.display = 'none'; | |
| document.body.appendChild(backgroundVideoElement); | |
| bgVideo.classList.add('active'); | |
| } | |
| }); | |
| // Toggle effect | |
| toggleEffect.addEventListener('change', async () => { | |
| if (toggleEffect.checked) { | |
| await initBodyPix(); | |
| } else { | |
| stopProcessing(); | |
| } | |
| }); | |
| // Toggle preview | |
| togglePreview.addEventListener('change', () => { | |
| if (togglePreview.checked) { | |
| if (toggleEffect.checked) { | |
| outputCanvas.classList.remove('hidden'); | |
| videoInput.classList.add('hidden'); | |
| } else { | |
| videoInput.classList.remove('hidden'); | |
| } | |
| } else { | |
| outputCanvas.classList.add('hidden'); | |
| videoInput.classList.add('hidden'); | |
| } | |
| }); | |
| // Video source selection | |
| videoSourceSelect.addEventListener('change', () => { | |
| const deviceId = videoSourceSelect.value; | |
| if (deviceId) { | |
| startVideo(deviceId); | |
| } else { | |
| stopVideo(); | |
| } | |
| }); | |
| // Copy OBS link | |
| copyOBSLink.addEventListener('click', () => { | |
| const currentUrl = window.location.href; | |
| navigator.clipboard.writeText(currentUrl).then(() => { | |
| const originalText = copyOBSLink.textContent; | |
| copyOBSLink.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | |
| </svg> | |
| Copied! | |
| `; | |
| setTimeout(() => { | |
| copyOBSLink.innerHTML = originalText; | |
| }, 2000); | |
| }); | |
| }); | |
| // Start FPS counter | |
| setInterval(updateFPS, 1000); | |
| }); | |
| // Load available video sources | |
| async function loadVideoSources() { | |
| try { | |
| const devices = await navigator.mediaDevices.enumerateDevices(); | |
| const videoDevices = devices.filter(device => device.kind === 'videoinput'); | |
| videoDevices.forEach(device => { | |
| const option = document.createElement('option'); | |
| option.value = device.deviceId; | |
| option.text = device.label || `Camera ${videoSourceSelect.length + 1}`; | |
| videoSourceSelect.appendChild(option); | |
| }); | |
| // Select the first camera by default if available | |
| if (videoDevices.length > 0) { | |
| videoSourceSelect.value = videoDevices[0].deviceId; | |
| startVideo(videoDevices[0].deviceId); | |
| } | |
| } catch (err) { | |
| console.error('Error enumerating devices:', err); | |
| } | |
| } | |
| // Start video stream | |
| async function startVideo(deviceId) { | |
| try { | |
| const constraints = { | |
| video: { | |
| deviceId: { exact: deviceId }, | |
| width: { ideal: 1280 }, | |
| height: { ideal: 720 } | |
| } | |
| }; | |
| const stream = await navigator.mediaDevices.getUserMedia(constraints); | |
| videoInput.srcObject = stream; | |
| noVideoOverlay.classList.add('hidden'); | |
| // Show video input by default | |
| if (!toggleEffect.checked) { | |
| videoInput.classList.remove('hidden'); | |
| outputCanvas.classList.add('hidden'); | |
| } | |
| // Start processing if effect is enabled | |
| if (toggleEffect.checked) { | |
| await initBodyPix(); | |
| videoInput.classList.add('hidden'); | |
| outputCanvas.classList.remove('hidden'); | |
| } | |
| } catch (err) { | |
| console.error('Error starting video:', err); | |
| noVideoOverlay.textContent = 'Error accessing camera'; | |
| } | |
| } | |
| // Stop video stream | |
| function stopVideo() { | |
| if (videoInput.srcObject) { | |
| videoInput.srcObject.getTracks().forEach(track => track.stop()); | |
| videoInput.srcObject = null; | |
| } | |
| videoInput.classList.add('hidden'); | |
| outputCanvas.classList.add('hidden'); | |
| noVideoOverlay.classList.remove('hidden'); | |
| stopProcessing(); | |
| } | |
| // Initialize BodyPix model | |
| async function initBodyPix() { | |
| if (net) return; | |
| processingOverlay.classList.remove('hidden'); | |
| outputCanvas.classList.remove('hidden'); | |
| try { | |
| // Load the BodyPix model with selected quality | |
| const multiplier = parseFloat(modelQuality.value); | |
| net = await bodyPix.load({ | |
| architecture: 'MobileNetV1', | |
| outputStride: 16, | |
| multiplier: multiplier, | |
| quantBytes: 2 | |
| }); | |
| processingOverlay.classList.add('hidden'); | |
| startProcessing(); | |
| } catch (err) { | |
| console.error('Error loading BodyPix model:', err); | |
| processingOverlay.textContent = 'Error loading AI model'; | |
| toggleEffect.checked = false; | |
| } | |
| } | |
| // Start background removal processing | |
| function startProcessing() { | |
| if (!net || isProcessing) return; | |
| isProcessing = true; | |
| outputCanvas.width = videoInput.videoWidth; | |
| outputCanvas.height = videoInput.videoHeight; | |
| processFrame(); | |
| } | |
| // Stop background removal processing | |
| function stopProcessing() { | |
| isProcessing = false; | |
| if (net) { | |
| // net.dispose(); // Don't dispose if we might reuse it | |
| net = null; | |
| } | |
| } | |
| // Process each video frame | |
| async function processFrame() { | |
| if (!isProcessing || !net || videoInput.readyState < 2) { | |
| if (isProcessing) { | |
| requestAnimationFrame(processFrame); | |
| } | |
| return; | |
| } | |
| const startTime = performance.now(); | |
| try { | |
| // Perform segmentation | |
| const segmentation = await net.segmentPerson(videoInput, { | |
| flipHorizontal: false, | |
| internalResolution: 'medium', | |
| segmentationThreshold: 0.7 | |
| }); | |
| // Get canvas context | |
| const ctx = outputCanvas.getContext('2d'); | |
| // Draw the background based on selection | |
| ctx.clearRect(0, 0, outputCanvas.width, outputCanvas.height); | |
| if (currentBackground === 'transparent') { | |
| // For transparent, we just leave it clear | |
| } else if (currentBackground === 'black') { | |
| ctx.fillStyle = 'black'; | |
| ctx.fillRect(0, 0, outputCanvas.width, outputCanvas.height); | |
| } else if (currentBackground === 'gray') { | |
| ctx.fillStyle = '#4B5563'; | |
| ctx.fillRect(0, 0, outputCanvas.width, outputCanvas.height); | |
| } else if (currentBackground === 'green') { | |
| ctx.fillStyle = '#059669'; | |
| ctx.fillRect(0, 0, outputCanvas.width, outputCanvas.height); | |
| } else if (currentBackground === 'customImage' && customBackgroundImage) { | |
| // Calculate aspect ratio and draw custom background | |
| const imgAspect = customBackgroundImage.width / customBackgroundImage.height; | |
| const canvasAspect = outputCanvas.width / outputCanvas.height; | |
| if (imgAspect > canvasAspect) { | |
| // Image is wider than canvas | |
| const scale = outputCanvas.width / customBackgroundImage.width; | |
| const scaledHeight = customBackgroundImage.height * scale; | |
| const y = (outputCanvas.height - scaledHeight) / 2; | |
| ctx.drawImage(customBackgroundImage, 0, y, outputCanvas.width, scaledHeight); | |
| } else { | |
| // Image is taller than canvas | |
| const scale = outputCanvas.height / customBackgroundImage.height; | |
| const scaledWidth = customBackgroundImage.width * scale; | |
| const x = (outputCanvas.width - scaledWidth) / 2; | |
| ctx.drawImage(customBackgroundImage, x, 0, scaledWidth, outputCanvas.height); | |
| } | |
| } else if (currentBackground === 'customVideo' && backgroundVideoElement) { | |
| // Draw video background | |
| const videoAspect = backgroundVideoElement.videoWidth / backgroundVideoElement.videoHeight; | |
| const canvasAspect = outputCanvas.width / outputCanvas.height; | |
| if (videoAspect > canvasAspect) { | |
| // Video is wider than canvas | |
| const scale = outputCanvas.width / backgroundVideoElement.videoWidth; | |
| const scaledHeight = backgroundVideoElement.videoHeight * scale; | |
| const y = (outputCanvas.height - scaledHeight) / 2; | |
| ctx.drawImage(backgroundVideoElement, 0, y, outputCanvas.width, scaledHeight); | |
| } else { | |
| // Video is taller than canvas | |
| const scale = outputCanvas.height / backgroundVideoElement.videoHeight; | |
| const scaledWidth = backgroundVideoElement.videoWidth * scale; | |
| const x = (outputCanvas.width - scaledWidth) / 2; | |
| ctx.drawImage(backgroundVideoElement, x, 0, scaledWidth, outputCanvas.height); | |
| } | |
| } else if (currentBackground === 'blurred') { | |
| // Draw blurred background | |
| ctx.drawImage(videoInput, 0, 0, outputCanvas.width, outputCanvas.height); | |
| applyBlurEffect(ctx, outputCanvas.width, outputCanvas.height, parseInt(bgBlur.value)); | |
| } | |
| // Draw the foreground (person) with alpha mask | |
| const foregroundColor = { r: 255, g: 255, b: 255, a: 255 }; | |
| const backgroundColor = { r: 0, g: 0, b: 0, a: 0 }; | |
| const edgeBlurAmount = parseInt(edgeSmoothness.value); | |
| const brightness = parseInt(fgBrightness.value) / 100; | |
| bodyPix.drawBokehEffect( | |
| outputCanvas, | |
| videoInput, | |
| segmentation, | |
| parseFloat(bgBlur.value), | |
| foregroundColor, | |
| backgroundColor, | |
| edgeBlurAmount, | |
| brightness | |
| ); | |
| // Update performance stats | |
| const endTime = performance.now(); | |
| const processingTime = endTime - startTime; | |
| processingTimeElement.textContent = `${processingTime.toFixed(1)} ms`; | |
| frameCount++; | |
| // Process next frame | |
| requestAnimationFrame(processFrame); | |
| } catch (err) { | |
| console.error('Error processing frame:', err); | |
| isProcessing = false; | |
| } | |
| } | |
| // Apply blur effect to context | |
| function applyBlurEffect(ctx, width, height, radius) { | |
| if (radius <= 0) return; | |
| // Create temporary canvas for blur effect | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = width; | |
| tempCanvas.height = height; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| // Draw the current content to temp canvas | |
| tempCtx.drawImage(outputCanvas, 0, 0, width, height); | |
| // Apply blur by drawing scaled down and up | |
| const iterations = Math.min(radius, 10); | |
| const scale = 1 / (iterations * 0.5 + 1); | |
| for (let i = 0; i < iterations; i++) { | |
| // Scale down | |
| const scaledWidth = width * scale; | |
| const scaledHeight = height * scale; | |
| tempCtx.drawImage( | |
| tempCanvas, | |
| 0, 0, width, height, | |
| 0, 0, scaledWidth, scaledHeight | |
| ); | |
| // Scale up | |
| tempCtx.drawImage( | |
| tempCanvas, | |
| 0, 0, scaledWidth, scaledHeight, | |
| 0, 0, width, height | |
| ); | |
| } | |
| // Draw the blurred content back to main canvas | |
| ctx.drawImage(tempCanvas, 0, 0, width, height); | |
| } | |
| // Update FPS counter | |
| function updateFPS() { | |
| const now = performance.now(); | |
| if (lastTimestamp) { | |
| const delta = (now - lastTimestamp) / 1000; | |
| fps = Math.round(frameCount / delta); | |
| fpsCounterElement.textContent = fps; | |
| } | |
| lastTimestamp = now; | |
| frameCount = 0; | |
| } | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=TDN-M/plugin" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |