Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Magic Cut - Video Face Splitter</title> | |
| <style> | |
| :root { | |
| --primary: #a855f7; | |
| --bg: #0f0f1a; | |
| --surface: #1a1a2e; | |
| --text: #f3f4f6; | |
| } | |
| body { | |
| font-family: 'Inter', system-ui, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| margin: 0; | |
| padding: 1rem; | |
| } | |
| .container { | |
| background: var(--surface); | |
| padding: 2rem; | |
| border-radius: 16px; | |
| width: 100%; | |
| max-width: 600px; | |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); | |
| border: 1px solid #2a2a4a; | |
| } | |
| h2 { | |
| margin-top: 0; | |
| text-align: center; | |
| background: linear-gradient(135deg, #a855f7, #ec4899); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| font-size: 1.8rem; | |
| } | |
| h4 { | |
| margin: 0; | |
| color: #9ca3af; | |
| text-align: center; | |
| font-weight: 400; | |
| margin-bottom: 1.5rem; | |
| } | |
| .form-group { | |
| margin-bottom: 1.5rem; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 0.5rem; | |
| font-size: 0.9rem; | |
| color: #d1d5db; | |
| } | |
| input, | |
| textarea { | |
| width: 100%; | |
| padding: 0.75rem; | |
| background: #0f0f1a; | |
| border: 1px solid #374151; | |
| border-radius: 8px; | |
| color: white; | |
| box-sizing: border-box; | |
| font-family: inherit; | |
| } | |
| input:focus, | |
| textarea:focus { | |
| outline: 2px solid var(--primary); | |
| border-color: transparent; | |
| } | |
| button { | |
| width: 100%; | |
| padding: 0.875rem; | |
| background: linear-gradient(135deg, #a855f7, #ec4899); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-size: 1rem; | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(168, 85, 247, 0.3); | |
| } | |
| button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| #statusBox { | |
| margin-top: 2rem; | |
| display: none; | |
| background: #0f0f1a; | |
| padding: 1.5rem; | |
| border-radius: 12px; | |
| border: 1px solid #374151; | |
| } | |
| .status-badge { | |
| display: inline-block; | |
| padding: 6px 14px; | |
| border-radius: 99px; | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| margin-bottom: 1rem; | |
| } | |
| .status-badge.queued { | |
| background: #f59e0b; | |
| color: black; | |
| } | |
| .status-badge.processing { | |
| background: #3b82f6; | |
| color: white; | |
| } | |
| .status-badge.completed { | |
| background: #10b981; | |
| color: black; | |
| } | |
| .status-badge.failed { | |
| background: #ef4444; | |
| color: white; | |
| } | |
| #progressText { | |
| color: #d1d5db; | |
| margin-bottom: 1rem; | |
| font-size: 0.95rem; | |
| } | |
| .result-box { | |
| background: #1a1a2e; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| margin-top: 1rem; | |
| } | |
| .result-url { | |
| word-break: break-all; | |
| font-size: 0.85rem; | |
| color: var(--primary); | |
| margin-bottom: 0.5rem; | |
| } | |
| .copy-btn { | |
| background: #374151; | |
| border: none; | |
| color: white; | |
| padding: 8px 16px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 0.85rem; | |
| width: auto; | |
| margin-top: 0.5rem; | |
| } | |
| .copy-btn:hover { | |
| background: #4b5563; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| .spinner { | |
| border: 4px solid #374151; | |
| border-top: 4px solid var(--primary); | |
| border-radius: 50%; | |
| width: 30px; | |
| height: 30px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 1rem auto; | |
| display: none; | |
| } | |
| @keyframes spin { | |
| 0% { | |
| transform: rotate(0deg); | |
| } | |
| 100% { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .info-box { | |
| background: rgba(168, 85, 247, 0.1); | |
| border: 1px solid rgba(168, 85, 247, 0.3); | |
| border-radius: 8px; | |
| padding: 1rem; | |
| margin-bottom: 1.5rem; | |
| font-size: 0.85rem; | |
| color: #d1d5db; | |
| } | |
| .segments-info { | |
| margin-top: 1rem; | |
| font-size: 0.85rem; | |
| color: #9ca3af; | |
| } | |
| video { | |
| width: 100%; | |
| max-height: 400px; | |
| border-radius: 8px; | |
| margin-top: 1rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h2>✂️ Magic Cut</h2> | |
| <h4>Transform 16:9 videos into vertical shorts with face tracking</h4> | |
| <div class="info-box"> | |
| <strong>How it works:</strong><br> | |
| 1. Paste your Cloudinary video URL with <code>so_X,du_Y</code> (start time, duration)<br> | |
| 2. We analyze each frame for faces (every 500ms)<br> | |
| 3. When 2+ faces detected → split-screen layout<br> | |
| 4. Get your final 9:16 video URL! | |
| </div> | |
| <div class="form-group"> | |
| <label>1. Your HF Space URL (Direct link)</label> | |
| <input type="text" id="serverUrl" value="https://adxabhi-magic-cut.hf.space" | |
| placeholder="https://username-space-name.hf.space"> | |
| <small style="color: #6b7280; display: block; margin-top: 4px;">Found in Space > Embed this Space > Direct | |
| URL</small> | |
| </div> | |
| <div class="form-group"> | |
| <label>2. Cloudinary Video URL</label> | |
| <textarea id="videoUrl" rows="3" | |
| placeholder="https://res.cloudinary.com/doxoms9hd/video/upload/so_55,du_30/fl_getinfo/video_id.jpg"></textarea> | |
| <small style="color: #6b7280; display: block; margin-top: 4px;"> | |
| Format: so_X,du_Y (start at X seconds, duration Y seconds) | |
| </small> | |
| </div> | |
| <button id="processBtn" onclick="submitJob()">🎬 Process Video</button> | |
| <div id="statusBox"> | |
| <div id="spinner" class="spinner"></div> | |
| <span id="statusBadge" class="status-badge">Waiting</span> | |
| <div id="progressText">Initializing...</div> | |
| <div id="resultBox"></div> | |
| </div> | |
| </div> | |
| <script> | |
| let pollInterval = null; | |
| async function submitJob() { | |
| let serverUrl = document.getElementById('serverUrl').value.trim(); | |
| serverUrl = serverUrl.replace(/\/$/, ""); | |
| const videoUrl = document.getElementById('videoUrl').value.trim(); | |
| const btn = document.getElementById('processBtn'); | |
| const statusBox = document.getElementById('statusBox'); | |
| if (!serverUrl || !videoUrl) { | |
| alert("Please fill in both fields"); | |
| return; | |
| } | |
| btn.disabled = true; | |
| statusBox.style.display = 'block'; | |
| document.getElementById('resultBox').innerHTML = ''; | |
| updateStatus("queued", "Submitting job..."); | |
| try { | |
| const response = await fetch(`${serverUrl}/jobs`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ video_url: videoUrl }) | |
| }); | |
| const data = await response.json(); | |
| if (data.job_id) { | |
| console.log("Job Submitted:", data.job_id); | |
| startPolling(serverUrl, data.job_id); | |
| } else { | |
| updateStatus("failed", "Failed to get Job ID"); | |
| btn.disabled = false; | |
| } | |
| } catch (error) { | |
| console.error(error); | |
| updateStatus("failed", "Connection Error. Check URL."); | |
| btn.disabled = false; | |
| } | |
| } | |
| function startPolling(serverUrl, jobId) { | |
| if (pollInterval) clearInterval(pollInterval); | |
| pollInterval = setInterval(async () => { | |
| try { | |
| const res = await fetch(`${serverUrl}/jobs/${jobId}`); | |
| const job = await res.json(); | |
| updateStatus(job.status, job.progress); | |
| if (job.status === 'completed') { | |
| clearInterval(pollInterval); | |
| showResults(job.result); | |
| document.getElementById('processBtn').disabled = false; | |
| } | |
| if (job.status === 'failed') { | |
| clearInterval(pollInterval); | |
| document.getElementById('progressText').innerText = "Error: " + job.error; | |
| document.getElementById('processBtn').disabled = false; | |
| } | |
| } catch (e) { | |
| console.error("Polling error", e); | |
| } | |
| }, 2000); | |
| } | |
| function updateStatus(status, message) { | |
| const badge = document.getElementById('statusBadge'); | |
| const spinner = document.getElementById('spinner'); | |
| const text = document.getElementById('progressText'); | |
| badge.className = `status-badge ${status}`; | |
| badge.innerText = status.toUpperCase(); | |
| text.innerText = message || "Processing..."; | |
| if (status === 'processing' || status === 'queued') { | |
| spinner.style.display = 'block'; | |
| } else { | |
| spinner.style.display = 'none'; | |
| } | |
| } | |
| function showResults(result) { | |
| const box = document.getElementById('resultBox'); | |
| const segments = result.multi_face_segments || []; | |
| let segmentsHtml = ''; | |
| if (segments.length > 0) { | |
| segmentsHtml = ` | |
| <div class="segments-info"> | |
| <strong>🎭 Multi-face segments found:</strong><br> | |
| ${segments.map((s, i) => `Segment ${i + 1}: ${s.start}s - ${s.end}s`).join('<br>')} | |
| </div> | |
| `; | |
| } else { | |
| segmentsHtml = `<div class="segments-info">No multi-face segments detected (single speaker throughout)</div>`; | |
| } | |
| box.innerHTML = ` | |
| <div class="result-box"> | |
| <div style="margin-bottom: 0.5rem; color: #10b981; font-weight: 600;">✅ Video Ready!</div> | |
| <div class="result-url">${result.video_url}</div> | |
| <button class="copy-btn" onclick="navigator.clipboard.writeText('${result.video_url}').then(() => this.innerText = 'Copied!')"> | |
| 📋 Copy URL | |
| </button> | |
| ${segmentsHtml} | |
| <div class="segments-info"> | |
| <strong>📊 Stats:</strong> ${result.total_frames_analyzed} frames analyzed | |
| </div> | |
| <video controls src="${result.video_url}"></video> | |
| </div> | |
| `; | |
| } | |
| </script> | |
| </body> | |
| </html> |