CreatorPlus / creatorplus /UI /index.html
Habib-HF's picture
Upload folder using huggingface_hub
0211274 verified
Raw
History Blame Contribute Delete
10.8 kB
<!doctype html>
<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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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>