Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>EpicSync — Minimalist AI Lip Sync</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #0d0f12; | |
| --surface: #15181d; | |
| --surface-hover: #1e2229; | |
| --border: #282d37; | |
| --text-main: #f0f2f5; | |
| --text-muted: #8b94a0; | |
| --accent: #ffffff; | |
| --accent-bg: #242932; | |
| --success: #3dd68c; | |
| --danger: #f85149; | |
| --warning: #e3b341; | |
| --font-main: 'Outfit', -apple-system, sans-serif; | |
| --font-mono: 'JetBrains Mono', monospace; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| background-color: var(--bg); | |
| color: var(--text-main); | |
| font-family: var(--font-main); | |
| line-height: 1.5; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Navbar */ | |
| header { | |
| border-bottom: 1px solid var(--border); | |
| padding: 1.25rem 2.5rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background-color: var(--bg); | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| letter-spacing: -0.03em; | |
| text-transform: uppercase; | |
| } | |
| .status-badge { | |
| font-family: var(--font-mono); | |
| font-size: 0.75rem; | |
| padding: 0.3rem 0.75rem; | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| color: var(--text-muted); | |
| } | |
| /* Layout */ | |
| main { | |
| display: grid; | |
| grid-template-columns: 420px 1fr; | |
| gap: 0; | |
| flex: 1; | |
| } | |
| @media (max-width: 1024px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* Left Panel - Control Form */ | |
| .panel-left { | |
| border-right: 1px solid var(--border); | |
| padding: 2.5rem; | |
| background-color: var(--surface); | |
| overflow-y: auto; | |
| } | |
| .section-title { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--text-muted); | |
| margin-bottom: 1.5rem; | |
| } | |
| .form-group { | |
| margin-bottom: 1.5rem; | |
| } | |
| label { | |
| display: block; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| margin-bottom: 0.5rem; | |
| color: var(--text-main); | |
| } | |
| input[type="text"], | |
| input[type="password"], | |
| textarea, | |
| select { | |
| width: 100%; | |
| background-color: var(--bg); | |
| border: 1px solid var(--border); | |
| color: var(--text-main); | |
| padding: 0.75rem 1rem; | |
| font-family: inherit; | |
| font-size: 0.95rem; | |
| border-radius: 4px; | |
| outline: none; | |
| transition: border-color 0.2s; | |
| } | |
| input:focus, | |
| textarea:focus, | |
| select:focus { | |
| border-color: var(--accent); | |
| } | |
| textarea { | |
| resize: vertical; | |
| min-height: 100px; | |
| } | |
| .file-upload { | |
| border: 1px dashed var(--border); | |
| padding: 1.5rem; | |
| text-align: center; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| background-color: var(--bg); | |
| transition: border-color 0.2s; | |
| } | |
| .file-upload:hover { | |
| border-color: var(--text-muted); | |
| } | |
| .file-upload input[type="file"] { | |
| display: none; | |
| } | |
| .btn-submit { | |
| width: 100%; | |
| background-color: var(--accent); | |
| color: #000; | |
| border: none; | |
| padding: 1rem; | |
| font-size: 0.95rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| transition: opacity 0.2s; | |
| margin-top: 1rem; | |
| } | |
| .btn-submit:hover { | |
| opacity: 0.85; | |
| } | |
| .btn-submit:disabled { | |
| background-color: var(--border); | |
| color: var(--text-muted); | |
| cursor: not-allowed; | |
| } | |
| /* Right Panel - Logs & Gallery */ | |
| .panel-right { | |
| padding: 2.5rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2.5rem; | |
| overflow-y: auto; | |
| max-height: calc(100vh - 75px); | |
| } | |
| .header-actions { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| } | |
| .btn-secondary { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--text-muted); | |
| padding: 0.4rem 0.8rem; | |
| font-size: 0.8rem; | |
| font-family: var(--font-mono); | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| .btn-secondary:hover { | |
| color: var(--text-main); | |
| border-color: var(--text-muted); | |
| } | |
| /* Jobs Container */ | |
| .job-card { | |
| border: 1px solid var(--border); | |
| background-color: var(--surface); | |
| border-radius: 6px; | |
| overflow: hidden; | |
| margin-bottom: 1.5rem; | |
| } | |
| .job-header { | |
| padding: 1rem 1.5rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background-color: var(--bg); | |
| } | |
| .job-id { | |
| font-family: var(--font-mono); | |
| font-size: 0.85rem; | |
| color: var(--text-main); | |
| } | |
| .job-status { | |
| font-family: var(--font-mono); | |
| font-size: 0.75rem; | |
| padding: 0.2rem 0.6rem; | |
| border-radius: 3px; | |
| font-weight: 600; | |
| } | |
| .status-RUNNING, .status-STAGING { background-color: rgba(227, 179, 65, 0.15); color: var(--warning); } | |
| .status-SUCCESS { background-color: rgba(61, 214, 140, 0.15); color: var(--success); } | |
| .status-FAILED, .status-CANCELLED { background-color: rgba(248, 81, 73, 0.15); color: var(--danger); } | |
| .job-body { | |
| padding: 1.5rem; | |
| } | |
| .job-meta { | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| margin-bottom: 1rem; | |
| } | |
| .progress-box { | |
| background-color: var(--bg); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| padding: 1.2rem; | |
| margin-bottom: 1.2rem; | |
| } | |
| .progress-header { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 0.85rem; | |
| margin-bottom: 0.6rem; | |
| font-family: var(--font-mono); | |
| } | |
| .step-text { | |
| color: var(--text-main); | |
| } | |
| .progress-pct { | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| } | |
| .progress-track { | |
| width: 100%; | |
| height: 6px; | |
| background-color: var(--border); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background-color: var(--accent); | |
| transition: width 0.5s ease; | |
| } | |
| .progress-fill.FAILED, .progress-fill.CANCELLED { background-color: var(--danger); } | |
| .progress-fill.SUCCESS { background-color: var(--success); } | |
| details.tech-logs { | |
| margin-top: 1rem; | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| } | |
| details.tech-logs summary { | |
| outline: none; | |
| user-select: none; | |
| padding: 0.4rem 0; | |
| font-family: var(--font-mono); | |
| } | |
| /* Terminal Log Stream */ | |
| .terminal { | |
| background-color: #000; | |
| border: 1px solid var(--border); | |
| padding: 1rem; | |
| font-family: var(--font-mono); | |
| font-size: 0.8rem; | |
| color: #d1d5db; | |
| max-height: 220px; | |
| overflow-y: auto; | |
| border-radius: 4px; | |
| white-space: pre-wrap; | |
| line-height: 1.4; | |
| } | |
| /* Video Output Preview */ | |
| .video-container { | |
| margin-top: 1rem; | |
| padding: 1rem; | |
| border: 1px solid var(--border); | |
| background-color: var(--bg); | |
| border-radius: 4px; | |
| } | |
| video { | |
| width: 100%; | |
| max-height: 360px; | |
| border-radius: 4px; | |
| outline: none; | |
| background-color: #000; | |
| } | |
| .job-actions { | |
| margin-top: 1rem; | |
| display: flex; | |
| gap: 0.75rem; | |
| justify-content: flex-end; | |
| } | |
| .btn-cancel { | |
| background-color: var(--danger); | |
| color: #fff; | |
| border: none; | |
| padding: 0.5rem 1rem; | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| .btn-download { | |
| background-color: var(--success); | |
| color: #000; | |
| text-decoration: none; | |
| padding: 0.5rem 1rem; | |
| font-size: 0.8rem; | |
| font-weight: 700; | |
| border-radius: 4px; | |
| display: inline-block; | |
| } | |
| .btn-cancel { | |
| background: transparent; | |
| color: var(--danger); | |
| border: 1px solid var(--danger); | |
| } | |
| .btn-cancel:hover { | |
| background: rgba(248, 81, 73, 0.1); | |
| } | |
| /* Gold Accent Premium Theme */ | |
| body.premium-theme { | |
| --bg: #0b0a08; | |
| --surface: #14120e; | |
| --surface-hover: #1f1b13; | |
| --border: #382e1b; | |
| --accent: #f9a825; | |
| --accent-bg: #2b220d; | |
| } | |
| body.premium-theme .btn-submit { | |
| background: linear-gradient(135deg, #f9a825 0%, #d4af37 50%, #aa771c 100%); | |
| color: #000; | |
| font-weight: 700; | |
| box-shadow: 0 0 20px rgba(249, 168, 37, 0.3); | |
| } | |
| .tab-switcher { | |
| display: flex; | |
| gap: 0.5rem; | |
| margin-bottom: 2rem; | |
| border-bottom: 1px solid var(--border); | |
| padding-bottom: 1rem; | |
| } | |
| .tab-btn { | |
| flex: 1; | |
| padding: 0.75rem 1rem; | |
| border: 1px solid var(--border); | |
| background: var(--bg); | |
| color: var(--text-muted); | |
| font-family: var(--font-main); | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| cursor: pointer; | |
| border-radius: 4px; | |
| transition: all 0.2s; | |
| } | |
| .tab-btn.active { | |
| background: var(--surface-hover); | |
| color: var(--accent); | |
| border-color: var(--accent); | |
| } | |
| body.premium-theme .tab-btn.active { | |
| background: linear-gradient(135deg, #2b220d 0%, #1f1b13 100%); | |
| color: #f9a825; | |
| border-color: #f9a825; | |
| box-shadow: 0 0 12px rgba(249, 168, 37, 0.2); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo">Epic<span style="color: var(--accent);">Sync</span></div> | |
| <div class="status-badge" id="modeBadge">STANDARD STUDIO (OPENTALKER 2D)</div> | |
| </header> | |
| <main> | |
| <!-- Left Control Panel --> | |
| <div class="panel-left"> | |
| <div class="tab-switcher"> | |
| <button type="button" class="tab-btn active" id="tabStandard" onclick="switchTab('standard')">Standard Studio</button> | |
| <button type="button" class="tab-btn" id="tabPremium" onclick="switchTab('premium')">✨ Premium Studio (LTX 3D)</button> | |
| </div> | |
| <div class="section-title" id="formTitle">New Sync Task</div> | |
| <form id="syncForm"> | |
| <div class="form-group"> | |
| <label id="fileLabel">Source Video or Photo File</label> | |
| <div class="file-upload" onclick="document.getElementById('mediaFile').click()"> | |
| <span id="fileName">Select or Drop MP4/JPG File</span> | |
| <input type="file" id="mediaFile" name="video" accept="video/mp4,image/*" required onchange="updateFileName(this)"> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label>Speech Script (Text to Synthesize)</label> | |
| <textarea name="script_text" placeholder="Enter the exact script you want the character to articulate..." required></textarea> | |
| </div> | |
| <div class="form-group"> | |
| <label>Neural Voice Model</label> | |
| <select name="voice"> | |
| <option value="en-US-AnaNeural">Ana (en-US, Cute Child / Baby Girl Voice)</option> | |
| <option value="en-US-ChristopherNeural">Christopher (en-US, Deep Male)</option> | |
| <option value="en-GB-SoniaNeural">Sonia (en-GB, British Female)</option> | |
| <option value="en-GB-RyanNeural">Ryan (en-GB, British Male)</option> | |
| <option value="fr-FR-DeniseNeural">Denise (French Female)</option> | |
| </select> | |
| </div> | |
| <input type="hidden" name="hf_repo" value="Airpyk98/EpicSync-Dataset"> | |
| <input type="hidden" name="hf_token" value=""> | |
| <input type="hidden" name="kaggle_user" value="ikechukwuebiringa1"> | |
| <input type="hidden" name="kaggle_key" value="KGAT_fc473ab2c166567756eac24217d1fbd2"> | |
| <button type="submit" class="btn-submit" id="submitBtn">Launch EpicSync</button> | |
| </form> | |
| </div> | |
| <!-- Right Live Logs & Output Panel --> | |
| <div class="panel-right"> | |
| <div> | |
| <div class="header-actions"> | |
| <div class="section-title" style="margin-bottom:0;">Live Logs & Persistent Tasks</div> | |
| <button class="btn-secondary" onclick="clearLogs()">Clear Inactive Runs</button> | |
| </div> | |
| <div id="jobsList"> | |
| <div style="color: var(--text-muted); font-size: 0.9rem; padding: 2rem 0;">No active synchronization tasks found. Submit a task on the left to begin real-time generation.</div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| let currentMode = 'standard'; | |
| function switchTab(mode) { | |
| currentMode = mode; | |
| document.getElementById('tabStandard').classList.toggle('active', mode === 'standard'); | |
| document.getElementById('tabPremium').classList.toggle('active', mode === 'premium'); | |
| const mediaInput = document.getElementById('mediaFile'); | |
| if (mode === 'premium') { | |
| document.body.classList.add('premium-theme'); | |
| document.getElementById('modeBadge').innerText = 'PREMIUM STUDIO (LTX 3D ACCELERATED)'; | |
| document.getElementById('fileLabel').innerText = 'Source Portrait Photo File (.png/.jpg)'; | |
| document.getElementById('fileName').innerText = 'Select or Drop Portrait Image File'; | |
| mediaInput.name = 'image'; | |
| mediaInput.accept = 'image/*'; | |
| } else { | |
| document.body.classList.remove('premium-theme'); | |
| document.getElementById('modeBadge').innerText = 'STANDARD STUDIO (OPENTALKER 2D)'; | |
| document.getElementById('fileLabel').innerText = 'Source Video or Photo File'; | |
| document.getElementById('fileName').innerText = 'Select or Drop MP4/JPG File'; | |
| mediaInput.name = 'video'; | |
| mediaInput.accept = 'video/mp4,image/*'; | |
| } | |
| } | |
| function updateFileName(input) { | |
| const display = document.getElementById('fileName'); | |
| if (input.files && input.files[0]) { | |
| display.innerText = input.files[0].name; | |
| display.style.color = '#fff'; | |
| } | |
| } | |
| document.getElementById('syncForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const btn = document.getElementById('submitBtn'); | |
| btn.disabled = true; | |
| btn.innerText = 'Submitting Task...'; | |
| const formData = new FormData(e.target); | |
| const endpoint = currentMode === 'premium' ? '/api/run_premium' : '/api/run'; | |
| try { | |
| const res = await fetch(endpoint, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (res.ok) { | |
| e.target.reset(); | |
| document.getElementById('fileName').innerText = currentMode === 'premium' ? 'Select or Drop Portrait Image File' : 'Select or Drop MP4/JPG File'; | |
| fetchJobs(); | |
| } else { | |
| const errData = await res.json().catch(() => ({})); | |
| alert('Submission failed: ' + (errData.detail || 'Unknown error')); | |
| } | |
| } catch (err) { | |
| alert('Network error connecting to EpicSync server.'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerText = 'Launch EpicSync'; | |
| } | |
| }); | |
| async function cancelJob(jobId) { | |
| await fetch(`/api/cancel/${jobId}`, { method: 'POST' }); | |
| fetchJobs(); | |
| } | |
| async function clearLogs() { | |
| await fetch('/api/clear_logs', { method: 'POST' }); | |
| fetchJobs(); | |
| } | |
| async function fetchJobs() { | |
| try { | |
| const res = await fetch('/api/jobs'); | |
| const jobs = await res.json(); | |
| const container = document.getElementById('jobsList'); | |
| const keys = Object.keys(jobs).sort((a,b) => jobs[b].created_at - jobs[a].created_at); | |
| if (keys.length === 0) { | |
| container.innerHTML = '<div style="color: var(--text-muted); font-size: 0.9rem; padding: 2rem 0;">No active synchronization tasks found. Submit a task on the left to begin real-time generation.</div>'; | |
| return; | |
| } | |
| container.innerHTML = keys.map(k => { | |
| const job = jobs[k]; | |
| const logText = (job.logs || []).join('\n'); | |
| const isRunning = job.status === 'RUNNING' || job.status === 'STAGING'; | |
| const isSuccess = job.status === 'SUCCESS'; | |
| const prog = job.progress !== undefined ? job.progress : (isRunning ? 40 : (isSuccess ? 100 : 0)); | |
| const stepText = job.step_text || (isRunning ? 'Processing video lip-sync on compute acceleration...' : job.status); | |
| return ` | |
| <div class="job-card"> | |
| <div class="job-header"> | |
| <span class="job-id">${job.title}</span> | |
| <span class="job-status status-${job.status}">${job.status}</span> | |
| </div> | |
| <div class="job-body"> | |
| <div class="job-meta"> | |
| Voice: <strong>${job.voice}</strong> | Script: "${job.script.substring(0, 60)}..." | |
| </div> | |
| <div class="progress-box"> | |
| <div class="progress-header"> | |
| <span class="step-text">${stepText}</span> | |
| <span class="progress-pct">${prog}%</span> | |
| </div> | |
| <div class="progress-track"> | |
| <div class="progress-fill ${job.status}" style="width: ${prog}%;"></div> | |
| </div> | |
| </div> | |
| ${isSuccess && job.output_file ? ` | |
| <div class="video-container"> | |
| <video controls autoplay loop> | |
| <source src="/api/video/${k}" type="video/mp4"> | |
| Your browser does not support HTML5 video. | |
| </video> | |
| <div class="job-actions"> | |
| <a href="/api/download/${k}" download="EpicSync_${k}.mp4" class="btn-download">Download Video</a> | |
| </div> | |
| </div> | |
| ` : ''} | |
| ${isRunning ? ` | |
| <div class="job-actions"> | |
| <button onclick="cancelJob('${k}')" class="btn-cancel">Cancel Task</button> | |
| </div> | |
| ` : ''} | |
| <details class="tech-logs"> | |
| <summary>▸ View Diagnostics / System Logs</summary> | |
| <div class="terminal" id="term_${k}" style="max-height: 180px; margin-top: 0.8rem;">${logText}</div> | |
| </details> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| // Scroll terminals to bottom | |
| keys.forEach(k => { | |
| const term = document.getElementById(`term_${k}`); | |
| if (term) term.scrollTop = term.scrollHeight; | |
| }); | |
| } catch (err) { | |
| console.error('Failed to poll jobs:', err); | |
| } | |
| } | |
| // Real-time polling every 4 seconds | |
| setInterval(fetchJobs, 4000); | |
| fetchJobs(); | |
| </script> | |
| </body> | |
| </html> | |