| <!DOCTYPE html> |
| <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> |