| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Caption Renderer V4 - Test Client</title> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); |
| min-height: 100vh; |
| color: #fff; |
| padding: 2rem; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| } |
| |
| h1 { |
| text-align: center; |
| margin-bottom: 2rem; |
| font-size: 2rem; |
| background: linear-gradient(90deg, #00ff00, #00cc00); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| .grid { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 2rem; |
| } |
| |
| @media (max-width: 900px) { |
| .grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| .card { |
| background: rgba(255, 255, 255, 0.05); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| border-radius: 16px; |
| padding: 1.5rem; |
| backdrop-filter: blur(10px); |
| } |
| |
| .card h2 { |
| font-size: 1.1rem; |
| margin-bottom: 1rem; |
| color: #888; |
| } |
| |
| textarea { |
| width: 100%; |
| height: 300px; |
| background: #111; |
| border: 1px solid #333; |
| border-radius: 8px; |
| color: #0f0; |
| font-family: 'Consolas', monospace; |
| font-size: 0.85rem; |
| padding: 1rem; |
| resize: vertical; |
| } |
| |
| textarea:focus { |
| outline: none; |
| border-color: #00ff00; |
| } |
| |
| .controls { |
| margin-top: 1rem; |
| display: flex; |
| gap: 1rem; |
| flex-wrap: wrap; |
| align-items: center; |
| } |
| |
| select, |
| input[type="text"] { |
| background: #222; |
| border: 1px solid #444; |
| color: #fff; |
| padding: 0.75rem 1rem; |
| border-radius: 8px; |
| font-size: 0.9rem; |
| } |
| |
| select { |
| min-width: 150px; |
| } |
| |
| input[type="text"] { |
| flex: 1; |
| min-width: 200px; |
| } |
| |
| button { |
| background: linear-gradient(135deg, #00cc00, #00aa00); |
| border: none; |
| color: #fff; |
| padding: 0.75rem 2rem; |
| border-radius: 8px; |
| font-size: 1rem; |
| font-weight: bold; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| } |
| |
| button:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 20px rgba(0, 255, 0, 0.3); |
| } |
| |
| button:disabled { |
| opacity: 0.5; |
| cursor: not-allowed; |
| transform: none; |
| } |
| |
| .output-area { |
| margin-top: 1.5rem; |
| } |
| |
| .status { |
| padding: 1rem; |
| background: #111; |
| border-radius: 8px; |
| margin-bottom: 1rem; |
| font-family: monospace; |
| font-size: 0.85rem; |
| color: #888; |
| } |
| |
| .status.success { |
| color: #0f0; |
| border-left: 3px solid #0f0; |
| } |
| |
| .status.error { |
| color: #f55; |
| border-left: 3px solid #f55; |
| } |
| |
| .status.loading { |
| color: #ff0; |
| border-left: 3px solid #ff0; |
| } |
| |
| video { |
| width: 100%; |
| max-height: 400px; |
| background: #000; |
| border-radius: 8px; |
| margin-top: 1rem; |
| } |
| |
| .url-output { |
| margin-top: 1rem; |
| word-break: break-all; |
| padding: 0.75rem; |
| background: #111; |
| border-radius: 8px; |
| font-size: 0.8rem; |
| color: #0af; |
| } |
| |
| .url-output a { |
| color: #0af; |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div class="container"> |
| <h1>🎬 Caption Renderer V4 - Test Client</h1> |
|
|
| <div class="grid"> |
| |
| <div class="card"> |
| <h2>📝 TRANSCRIPT JSON</h2> |
| <textarea id="transcriptInput">[ |
| {"text": "राधे", "start": 0.3, "end": 0.44}, |
| {"text": "राधे", "start": 0.48, "end": 0.62}, |
| {"text": "महाराज", "start": 0.68, "end": 0.9}, |
| {"text": "जी,", "start": 0.94, "end": 1.0}, |
| {"text": "महाराज", "start": 1.52, "end": 1.7}, |
| {"text": "इस", "start": 1.82, "end": 1.9}, |
| {"text": "कलयुग", "start": 2.04, "end": 2.36}, |
| {"text": "में", "start": 2.46, "end": 2.56} |
| ]</textarea> |
|
|
| <div class="controls"> |
| <select id="styleSelect"> |
| <option value="hormozi">Hormozi (Gold)</option> |
| <option value="cinematic">Cinematic</option> |
| <option value="netflix">Netflix (Red)</option> |
| <option value="neon">Neon (Magenta)</option> |
| </select> |
|
|
| <input type="text" id="apiUrl" placeholder="HF Space URL" |
| value="https://adxabhi-cap2.hf.space"> |
|
|
| <button id="generateBtn" onclick="generateVideo()"> |
| 🎥 Generate Video |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="card"> |
| <h2>📺 OUTPUT</h2> |
|
|
| <div class="output-area"> |
| <div id="status" class="status">Ready. Click "Generate Video" to start.</div> |
|
|
| <video id="videoOutput" controls style="display: none;"></video> |
|
|
| <div id="urlOutput" class="url-output" style="display: none;"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| const statusEl = document.getElementById('status'); |
| const videoEl = document.getElementById('videoOutput'); |
| const urlEl = document.getElementById('urlOutput'); |
| const generateBtn = document.getElementById('generateBtn'); |
| |
| function setStatus(message, type = '') { |
| statusEl.textContent = message; |
| statusEl.className = 'status ' + type; |
| } |
| |
| async function generateVideo() { |
| const transcript = document.getElementById('transcriptInput').value; |
| const style = document.getElementById('styleSelect').value; |
| const apiUrl = document.getElementById('apiUrl').value.trim(); |
| |
| |
| try { |
| JSON.parse(transcript); |
| } catch (e) { |
| setStatus('Invalid JSON: ' + e.message, 'error'); |
| return; |
| } |
| |
| if (!apiUrl) { |
| setStatus('Please enter the HF Space URL', 'error'); |
| return; |
| } |
| |
| |
| videoEl.style.display = 'none'; |
| urlEl.style.display = 'none'; |
| |
| generateBtn.disabled = true; |
| setStatus('⏳ Connecting to HF Space...', 'loading'); |
| |
| try { |
| |
| const gradioApi = apiUrl.replace(/\/$/, '') + '/api/predict'; |
| |
| |
| const callUrl = apiUrl.replace(/\/$/, '') + '/call/generate_video'; |
| |
| setStatus('⏳ Starting video generation...', 'loading'); |
| |
| |
| const submitRes = await fetch(callUrl, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| data: [transcript, style] |
| }) |
| }); |
| |
| if (!submitRes.ok) { |
| throw new Error(`Submit failed: ${submitRes.status} ${submitRes.statusText}`); |
| } |
| |
| const submitData = await submitRes.json(); |
| const eventId = submitData.event_id; |
| |
| setStatus(`⏳ Job submitted (${eventId}). Waiting for result...`, 'loading'); |
| |
| |
| const resultUrl = apiUrl.replace(/\/$/, '') + '/call/generate_video/' + eventId; |
| const eventSource = new EventSource(resultUrl); |
| |
| eventSource.onmessage = (event) => { |
| const data = JSON.parse(event.data); |
| |
| if (data[0] === 'process_starts') { |
| setStatus('⏳ Processing...', 'loading'); |
| } else if (data[0] === 'process_completed') { |
| eventSource.close(); |
| handleResult(data[1]); |
| } else if (data[0] === 'error') { |
| eventSource.close(); |
| setStatus('❌ Error: ' + JSON.stringify(data), 'error'); |
| generateBtn.disabled = false; |
| } |
| }; |
| |
| eventSource.onerror = (err) => { |
| eventSource.close(); |
| |
| |
| pollForResult(eventId, apiUrl); |
| }; |
| |
| } catch (error) { |
| setStatus('❌ Error: ' + error.message, 'error'); |
| generateBtn.disabled = false; |
| } |
| } |
| |
| async function pollForResult(eventId, apiUrl) { |
| const maxAttempts = 60; |
| let attempts = 0; |
| |
| const poll = async () => { |
| try { |
| const res = await fetch(apiUrl.replace(/\/$/, '') + '/call/generate_video/' + eventId); |
| const text = await res.text(); |
| |
| |
| const lines = text.split('\n'); |
| for (const line of lines) { |
| if (line.startsWith('data: ')) { |
| const data = JSON.parse(line.slice(6)); |
| if (Array.isArray(data) && data.length > 0) { |
| if (data[0] && data[0].path) { |
| handleResult({ data: data }); |
| return; |
| } |
| } |
| } |
| } |
| |
| if (attempts++ < maxAttempts) { |
| setStatus(`⏳ Processing... (${attempts}/${maxAttempts})`, 'loading'); |
| setTimeout(poll, 2000); |
| } else { |
| setStatus('❌ Timeout waiting for result', 'error'); |
| generateBtn.disabled = false; |
| } |
| } catch (e) { |
| if (attempts++ < maxAttempts) { |
| setTimeout(poll, 2000); |
| } else { |
| setStatus('❌ Error: ' + e.message, 'error'); |
| generateBtn.disabled = false; |
| } |
| } |
| }; |
| |
| poll(); |
| } |
| |
| function handleResult(result) { |
| generateBtn.disabled = false; |
| |
| if (!result || !result.data) { |
| setStatus('❌ Invalid response', 'error'); |
| return; |
| } |
| |
| const [videoData, cloudinaryUrl] = result.data; |
| |
| setStatus('✅ Video generated successfully!', 'success'); |
| |
| |
| if (videoData && videoData.url) { |
| videoEl.src = videoData.url; |
| videoEl.style.display = 'block'; |
| } else if (typeof videoData === 'string') { |
| |
| videoEl.src = videoData; |
| videoEl.style.display = 'block'; |
| } |
| |
| |
| if (cloudinaryUrl) { |
| urlEl.innerHTML = `<strong>Cloudinary URL:</strong><br><a href="${cloudinaryUrl}" target="_blank">${cloudinaryUrl}</a>`; |
| urlEl.style.display = 'block'; |
| } |
| } |
| </script> |
| </body> |
|
|
| </html> |