cap2 / client.html
ADXabhi's picture
Upload 8 files
372630e verified
<!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">
<!-- Input Panel -->
<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>
<!-- Output Panel -->
<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();
// Validate JSON
try {
JSON.parse(transcript);
} catch (e) {
setStatus('Invalid JSON: ' + e.message, 'error');
return;
}
if (!apiUrl) {
setStatus('Please enter the HF Space URL', 'error');
return;
}
// Hide previous output
videoEl.style.display = 'none';
urlEl.style.display = 'none';
generateBtn.disabled = true;
setStatus('⏳ Connecting to HF Space...', 'loading');
try {
// Gradio API endpoint
const gradioApi = apiUrl.replace(/\/$/, '') + '/api/predict';
// For Gradio 4.x, we use the /call endpoint pattern
const callUrl = apiUrl.replace(/\/$/, '') + '/call/generate_video';
setStatus('⏳ Starting video generation...', 'loading');
// Step 1: Submit the job
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');
// Step 2: Poll for result using SSE
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();
// Try alternative: direct polling
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();
// Parse SSE format
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');
// Display video
if (videoData && videoData.url) {
videoEl.src = videoData.url;
videoEl.style.display = 'block';
} else if (typeof videoData === 'string') {
// Direct URL
videoEl.src = videoData;
videoEl.style.display = 'block';
}
// Display Cloudinary URL
if (cloudinaryUrl) {
urlEl.innerHTML = `<strong>Cloudinary URL:</strong><br><a href="${cloudinaryUrl}" target="_blank">${cloudinaryUrl}</a>`;
urlEl.style.display = 'block';
}
}
</script>
</body>
</html>