| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Hugging Face Video Uploader (Async V2)</title> |
| <style> |
| :root { |
| --primary: #10b981; |
| |
| --bg: #111827; |
| --surface: #1f2937; |
| --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; |
| } |
| |
| .container { |
| background: var(--surface); |
| padding: 2rem; |
| border-radius: 12px; |
| width: 100%; |
| max-width: 550px; |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5); |
| border: 1px solid #374151; |
| } |
| |
| h2 { |
| margin-top: 0; |
| text-align: center; |
| color: var(--primary); |
| } |
| |
| 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 { |
| width: 100%; |
| padding: 0.75rem; |
| background: #111827; |
| border: 1px solid #374151; |
| border-radius: 6px; |
| color: white; |
| box-sizing: border-box; |
| } |
| |
| input:focus { |
| outline: 2px solid var(--primary); |
| border-color: transparent; |
| } |
| |
| button { |
| width: 100%; |
| padding: 0.75rem; |
| background: var(--primary); |
| color: #064e3b; |
| border: none; |
| border-radius: 6px; |
| font-weight: 700; |
| cursor: pointer; |
| transition: transform 0.1s; |
| } |
| |
| button:hover { |
| opacity: 0.9; |
| transform: scale(1.02); |
| } |
| |
| button:disabled { |
| opacity: 0.5; |
| cursor: not-allowed; |
| transform: none; |
| } |
| |
| #statusBox { |
| margin-top: 2rem; |
| display: none; |
| background: #111827; |
| padding: 1.5rem; |
| border-radius: 8px; |
| border: 1px solid #374151; |
| text-align: center; |
| } |
| |
| .status-badge { |
| display: inline-block; |
| padding: 4px 12px; |
| border-radius: 99px; |
| font-size: 0.8rem; |
| font-weight: 600; |
| background: #374151; |
| color: white; |
| 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-item { |
| background: #374151; |
| padding: 0.5rem; |
| border-radius: 6px; |
| margin-bottom: 0.5rem; |
| word-break: break-all; |
| font-size: 0.85rem; |
| text-align: left; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| |
| .copy-btn { |
| background: #1f2937; |
| border: none; |
| color: white; |
| padding: 4px 8px; |
| border-radius: 4px; |
| font-size: 0.75rem; |
| cursor: pointer; |
| width: auto; |
| margin-left: 10px; |
| } |
| |
| |
| .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); |
| } |
| } |
| </style> |
| </head> |
|
|
| <body> |
|
|
| <div class="container"> |
| <h2>⚡ Async Video Uploader</h2> |
| <h4>Support for Huge Files (2GB+)</h4> |
|
|
| <div class="form-group"> |
| <label>1. Your Space URL (Direct link)</label> |
| <input type="text" id="serverUrl" value="https://adxabhi-v3.hf.space" |
| placeholder="https://username-space-name.hf.space" required> |
| <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. Video URL (Direct link)</label> |
| <input type="text" id="videoUrl" placeholder="https://example.com/huge_video.mp4" required> |
| </div> |
|
|
| <button id="processBtn" onclick="submitJob()">Start Background Job</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="linksList"></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'; |
| 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); |
| } |
| }, 3000); |
| } |
| |
| 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 formatDuration(seconds) { |
| const mins = Math.floor(seconds / 60); |
| const secs = Math.floor(seconds % 60); |
| return `${mins}:${secs.toString().padStart(2, '0')}`; |
| } |
| |
| function showResults(results) { |
| const list = document.getElementById('linksList'); |
| list.innerHTML = ''; |
| results.forEach((item, index) => { |
| const div = document.createElement('div'); |
| div.className = 'result-item'; |
| |
| const url = typeof item === 'string' ? item : item.url; |
| const duration = typeof item === 'object' && item.duration ? formatDuration(item.duration) : ''; |
| const durationBadge = duration ? `<span style="color:#f59e0b; margin-left:8px;">[${duration}]</span>` : ''; |
| div.innerHTML = ` |
| <span>Part ${index + 1}${durationBadge}: <a href="${url}" target="_blank" style="color:#10b981">${url.substring(0, 30)}...</a></span> |
| <button class="copy-btn" onclick="navigator.clipboard.writeText('${url}')">Copy</button> |
| `; |
| list.appendChild(div); |
| }); |
| } |
| </script> |
|
|
| </body> |
|
|
| </html> |