Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>CreatorPlus</title> | |
| <style> | |
| body { font-family: Segoe UI, sans-serif; max-width: 920px; margin: 24px auto; padding: 0 16px; background: #0c111d; color: #e8ecf3; } | |
| textarea, input, select, button { width: 100%; margin-top: 8px; margin-bottom: 12px; padding: 10px; border-radius: 8px; border: 1px solid #2a3345; background: #141b2d; color: #e8ecf3; } | |
| .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } | |
| .card { background: #111827; border: 1px solid #2a3345; padding: 16px; border-radius: 12px; } | |
| button { cursor: pointer; background: linear-gradient(90deg, #0ea5e9, #10b981); border: none; font-weight: 600; } | |
| pre { background: #0b1220; border: 1px solid #2a3345; border-radius: 8px; padding: 10px; overflow: auto; } | |
| .status-card { background: #111827; border: 1px solid #2a3345; padding: 14px; border-radius: 12px; margin-top: 8px; } | |
| .chip { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; margin-right: 6px; } | |
| .chip.ok { background: #064e3b; color: #6ee7b7; } | |
| .chip.warn { background: #78350f; color: #fcd34d; } | |
| .chip.info { background: #1e3a8a; color: #93c5fd; } | |
| .chip.err { background: #7f1d1d; color: #fca5a5; } | |
| .progress-track { background: #0b1220; border: 1px solid #2a3345; border-radius: 999px; height: 10px; overflow: hidden; margin: 8px 0; } | |
| .progress-fill { background: linear-gradient(90deg,#0ea5e9,#10b981); height: 100%; transition: width 0.3s; } | |
| .stage-list { font-size: 13px; color: #9ca3af; } | |
| .stage-list span { margin-right: 12px; } | |
| details summary { cursor: pointer; color: #93c5fd; font-size: 12px; margin-top: 8px; } | |
| .download-btn { display: inline-block; margin-right: 8px; margin-top: 8px; padding: 6px 12px; background: #10b981; color: white; border-radius: 6px; text-decoration: none; font-size: 13px; } | |
| .secondary-btn { margin-top: 8px; background: linear-gradient(90deg,#f59e0b,#ef4444); } | |
| .hidden { display: none; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>CreatorPlus MVP</h1> | |
| <div class="card"> | |
| <label>Script</label> | |
| <textarea id="script" rows="8" placeholder="Write your script here..."></textarea> | |
| <div class="row"> | |
| <div> | |
| <label>Category</label> | |
| <select id="category" title="Category"> | |
| <option value="narration">Narration</option> | |
| <option value="storytelling">Storytelling</option> | |
| <option value="teaching">Teaching</option> | |
| <option value="history">History</option> | |
| <option value="social">Social</option> | |
| <option value="custom">Custom</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Background query</label> | |
| <input id="bgQuery" placeholder="nature, city, classroom..." /> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div> | |
| <label>Aspect ratio</label> | |
| <select id="aspect" title="Aspect ratio"> | |
| <option value="9:16">9:16</option> | |
| <option value="1:1">1:1</option> | |
| <option value="16:9">16:9</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Music volume (0-1)</label> | |
| <input id="music" type="number" step="0.05" min="0" max="1" value="0.25" title="Music volume" /> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div> | |
| <label>Voice provider</label> | |
| <select id="voiceProvider" title="Voice provider"> | |
| <option value="auto">Auto</option> | |
| <option value="edge_tts">Edge TTS</option> | |
| <option value="gtts">gTTS</option> | |
| <option value="pyttsx3">pyttsx3</option> | |
| <option value="coqui_xtts">Coqui XTTS (local)</option> | |
| <option value="piper">Piper (local)</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Language</label> | |
| <input id="language" value="en" placeholder="en, ar, fr, es..." /> | |
| </div> | |
| </div> | |
| <label>Background music file (optional)</label> | |
| <input id="musicFile" type="file" accept=".mp3,.wav,.m4a,.aac,.ogg" title="Background music file" /> | |
| <label>Speaker sample for cloning (optional)</label> | |
| <input id="speakerFile" type="file" accept=".wav,.mp3,.m4a,.ogg,.flac" title="Speaker sample file" /> | |
| <button onclick="generate()">Generate</button> | |
| <button onclick="checkProviders()" class="secondary-btn">Check Providers</button> | |
| </div> | |
| <h3>Status</h3> | |
| <div id="statusPanel" class="status-card">Idle</div> | |
| <pre id="status" class="hidden">Idle</pre> | |
| <script> | |
| function renderStatus(d) { | |
| const panel = document.getElementById('statusPanel'); | |
| if (!d || typeof d !== 'object') { | |
| panel.textContent = String(d || 'Idle'); | |
| return; | |
| } | |
| if (d.ok === false) { | |
| panel.innerHTML = ` | |
| <div><span class="chip err">Error</span> <strong>${d.error_code || 'error'}</strong></div> | |
| <div style="margin-top:6px">${d.message || d.error || ''}</div> | |
| <details><summary>Raw</summary><pre>${escapeHtml(JSON.stringify(d, null, 2))}</pre></details>`; | |
| return; | |
| } | |
| const status = d.status || 'unknown'; | |
| const statusChip = status === 'complete' ? 'ok' : status === 'error' ? 'err' : 'info'; | |
| const progress = Math.max(0, Math.min(100, d.progress || 0)); | |
| const tts = d.tts || {}; | |
| const providerChip = tts.provider ? `<span class="chip ok">TTS: ${tts.provider}</span>` : ''; | |
| const fallbackChain = (tts.fallback_chain || []).join(' → '); | |
| const primaryFail = tts.primary_failure ? `<span class="chip warn">Fallback used</span>` : ''; | |
| const bgSource = d.background_source_used ? `<span class="chip info">BG: ${d.background_source_used}</span>` : ''; | |
| const timings = d.timings_ms || {}; | |
| const timingHtml = Object.keys(timings).length | |
| ? `<div class="stage-list">${Object.entries(timings).map(([k, v]) => `<span>${k}: ${v}ms</span>`).join('')}</div>` | |
| : ''; | |
| let downloads = ''; | |
| if (status === 'complete') { | |
| const jid = d.id || d.jobId; | |
| downloads = ` | |
| <div> | |
| <a class="download-btn" href="/api/outputs/${jid}/video" download>Download Video</a> | |
| <a class="download-btn" href="/api/outputs/${jid}/audio" download>Download Mix</a> | |
| <a class="download-btn" href="/api/outputs/${jid}/narration" download>Download Narration</a> | |
| </div>`; | |
| } | |
| const errorBlock = d.error | |
| ? `<div style="margin-top:6px;color:#fca5a5">${escapeHtml(d.error)}</div>` | |
| : ''; | |
| panel.innerHTML = ` | |
| <div> | |
| <span class="chip ${statusChip}">${status}</span> | |
| ${providerChip} ${primaryFail} ${bgSource} | |
| </div> | |
| <div style="margin-top:6px">${escapeHtml(d.message || '')}</div> | |
| <div class="progress-track"><div class="progress-fill" style="width:${progress}%"></div></div> | |
| ${timingHtml} | |
| ${fallbackChain ? `<div class="stage-list">Chain: ${escapeHtml(fallbackChain)}</div>` : ''} | |
| ${errorBlock} | |
| ${downloads} | |
| <details><summary>Raw JSON</summary><pre>${escapeHtml(JSON.stringify(d, null, 2))}</pre></details>`; | |
| } | |
| function escapeHtml(s) { | |
| return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); | |
| } | |
| async function generate() { | |
| let uploadedMusicPath = null; | |
| let uploadedSpeakerPath = null; | |
| const musicFile = document.getElementById('musicFile').files[0]; | |
| const speakerFile = document.getElementById('speakerFile').files[0]; | |
| if (musicFile) { | |
| const formData = new FormData(); | |
| formData.append('music', musicFile); | |
| const upResp = await fetch('/api/upload/music', { | |
| method: 'POST', | |
| body: formData, | |
| }); | |
| const upData = await upResp.json(); | |
| if (!upData.ok) { | |
| renderStatus(upData); | |
| return; | |
| } | |
| uploadedMusicPath = upData.musicPath || upData.path; | |
| } | |
| if (speakerFile) { | |
| const formData = new FormData(); | |
| formData.append('speaker', speakerFile); | |
| const upResp = await fetch('/api/upload/speaker', { | |
| method: 'POST', | |
| body: formData, | |
| }); | |
| const upData = await upResp.json(); | |
| if (!upData.ok) { | |
| renderStatus(upData); | |
| return; | |
| } | |
| uploadedSpeakerPath = upData.speakerWavPath || upData.path; | |
| } | |
| const payload = { | |
| script: document.getElementById('script').value, | |
| backgroundQuery: document.getElementById('bgQuery').value, | |
| musicPath: uploadedMusicPath, | |
| tts: { | |
| provider: document.getElementById('voiceProvider').value, | |
| language: document.getElementById('language').value || 'en', | |
| speakerWavPath: uploadedSpeakerPath, | |
| }, | |
| settings: { | |
| category: document.getElementById('category').value, | |
| aspectRatio: document.getElementById('aspect').value, | |
| musicVolume: parseFloat(document.getElementById('music').value || '0.25'), | |
| voiceProvider: document.getElementById('voiceProvider').value, | |
| language: document.getElementById('language').value || 'en', | |
| } | |
| }; | |
| const start = await fetch('/api/generate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const startData = await start.json(); | |
| if (!startData.ok) { | |
| renderStatus(startData); | |
| return; | |
| } | |
| const jobId = startData.jobId; | |
| const timer = setInterval(async () => { | |
| const r = await fetch('/api/progress?jobId=' + encodeURIComponent(jobId)); | |
| const d = await r.json(); | |
| renderStatus(d); | |
| if (d.status === 'complete' || d.status === 'error') { | |
| clearInterval(timer); | |
| } | |
| }, 1200); | |
| } | |
| async function checkProviders() { | |
| const q = encodeURIComponent(document.getElementById('bgQuery').value || 'nature'); | |
| const a = encodeURIComponent(document.getElementById('aspect').value || '9:16'); | |
| const r = await fetch('/api/providers/status?query=' + q + '&aspectRatio=' + a); | |
| const d = await r.json(); | |
| document.getElementById('statusPanel').innerHTML = `<details open><summary>Providers</summary><pre>${escapeHtml(JSON.stringify(d, null, 2))}</pre></details>`; | |
| } | |
| </script> | |
| </body> | |
| </html> | |