You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
246 lines
11 KiB
246 lines
11 KiB
{% extends "layout.html" %}
|
|
{% block main %}
|
|
|
|
<div class="bg-slate-900 min-h-screen py-12">
|
|
<div class="container mx-auto px-4 max-w-2xl">
|
|
<div class="card bg-slate-800 border border-white/5 shadow-2xl">
|
|
<div class="card-body p-8">
|
|
<div class="flex items-center gap-4 mb-8">
|
|
<div class="bg-indigo-600/20 p-3 rounded-xl">
|
|
<i data-lucide="plus-square" class="w-8 h-8 text-indigo-500"></i>
|
|
</div>
|
|
<div>
|
|
<h2 class="card-title text-2xl text-white">Create New Short</h2>
|
|
<p class="text-slate-400 text-sm">Start the automated video creation pipeline.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-8">
|
|
<!-- Action Button -->
|
|
<button id="create-btn" class="btn btn-indigo btn-lg w-full bg-indigo-600 hover:bg-indigo-500 border-none text-white h-16 text-lg"
|
|
onclick="startPipeline()" disabled>
|
|
<span id="btn-text">Start Generation</span>
|
|
<span id="btn-spinner" class="loading loading-spinner loading-md hidden"></span>
|
|
</button>
|
|
|
|
<!-- Progress Visualization -->
|
|
<div id="progress-area" class="hidden space-y-4 animate-in fade-in duration-500">
|
|
<div class="flex justify-between items-end">
|
|
<div class="space-y-1">
|
|
<span class="text-xs uppercase tracking-widest text-slate-500 font-bold">Current Stage</span>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-2 h-2 rounded-full bg-indigo-500 animate-pulse"></div>
|
|
<h3 id="stage-text" class="text-indigo-400 font-semibold text-lg capitalize">Preparing...</h3>
|
|
</div>
|
|
</div>
|
|
<span id="pct-text" class="text-2xl font-black text-slate-700 font-mono">0%</span>
|
|
</div>
|
|
|
|
<progress id="progress-bar" class="progress progress-indigo w-full h-3 bg-slate-900" value="0" max="100"></progress>
|
|
|
|
<div class="grid grid-cols-2 gap-4 pt-4">
|
|
<div class="bg-slate-900/50 p-3 rounded-lg border border-white/5 flex items-center gap-3">
|
|
<i data-lucide="clock" class="w-4 h-4 text-slate-500"></i>
|
|
<div class="flex flex-col">
|
|
<span class="text-[10px] uppercase text-slate-500 font-bold">Elapsed</span>
|
|
<span id="elapsed-time" class="text-sm font-mono text-slate-300">00:00</span>
|
|
</div>
|
|
</div>
|
|
<div class="bg-slate-900/50 p-3 rounded-lg border border-white/5 flex items-center gap-3">
|
|
<i data-lucide="layers" class="w-4 h-4 text-slate-500"></i>
|
|
<div class="flex flex-col">
|
|
<span class="text-[10px] uppercase text-slate-500 font-bold">Status</span>
|
|
<span class="text-sm text-indigo-400">Processing</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success Message -->
|
|
<div id="done-area" class="hidden animate-in zoom-in duration-300">
|
|
<div class="alert bg-emerald-500/10 border-emerald-500/20 text-emerald-400 flex flex-col items-start gap-4 p-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="bg-emerald-500 text-slate-900 p-1 rounded-full">
|
|
<i data-lucide="check" class="w-4 h-4"></i>
|
|
</div>
|
|
<span class="font-bold text-lg">Generation Complete!</span>
|
|
</div>
|
|
<p id="done-msg" class="text-slate-300 text-sm">Your video has been rendered and saved to the library.</p>
|
|
<a href="/" class="btn btn-emerald btn-sm bg-emerald-600 hover:bg-emerald-500 border-none text-white px-6">View Video</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Message -->
|
|
<div id="error-area" class="hidden">
|
|
<div class="alert bg-red-500/10 border-red-500/20 text-red-400 p-6">
|
|
<i data-lucide="alert-triangle" class="w-6 h-6"></i>
|
|
<div>
|
|
<h3 class="font-bold">Pipeline Failed</h3>
|
|
<div id="error-text" class="text-xs mt-2 font-mono bg-black/20 p-3 rounded overflow-x-auto whitespace-pre-wrap"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Log Output -->
|
|
<div id="log-area" class="hidden space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<h4 class="text-xs uppercase tracking-widest text-slate-500 font-bold">Execution Logs</h4>
|
|
<span class="badge badge-outline border-slate-700 text-slate-500 text-[10px]">Real-time</span>
|
|
</div>
|
|
<div id="log-list" class="bg-slate-900 rounded-xl p-4 font-mono text-[11px] leading-relaxed text-slate-400 h-48 overflow-y-auto border border-white/5 shadow-inner">
|
|
<!-- Logs will appear here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let pollTimer = null;
|
|
let startTime = null;
|
|
let elapsedTimer = null;
|
|
|
|
const stageWeights = {
|
|
'configuring': 5,
|
|
'discovering': 15,
|
|
'scraping': 20,
|
|
'fetching': 25,
|
|
'saving': 35,
|
|
'tts': 45,
|
|
'screenshots': 60,
|
|
'background': 70,
|
|
'chopping': 75,
|
|
'creating': 80,
|
|
'rendering': 90,
|
|
'done': 100,
|
|
'error': 0
|
|
};
|
|
|
|
function updateElapsedTime() {
|
|
if (!startTime) return;
|
|
const now = new Date();
|
|
const diff = Math.floor((now - startTime) / 1000);
|
|
const mins = Math.floor(diff / 60).toString().padStart(2, '0');
|
|
const secs = (diff % 60).toString().padStart(2, '0');
|
|
document.getElementById('elapsed-time').textContent = `${mins}:${secs}`;
|
|
}
|
|
|
|
function stageProgress(stage) {
|
|
let pct = 0;
|
|
const s = stage.toLowerCase();
|
|
for (let [key, val] of Object.entries(stageWeights)) {
|
|
if (s.includes(key)) { pct = val; }
|
|
}
|
|
return pct;
|
|
}
|
|
|
|
async function startPipeline() {
|
|
const btn = document.getElementById('create-btn');
|
|
const btnText = document.getElementById('btn-text');
|
|
const spinner = document.getElementById('btn-spinner');
|
|
|
|
btn.disabled = true;
|
|
spinner.classList.remove('hidden');
|
|
btnText.textContent = 'Initializing...';
|
|
|
|
document.getElementById('progress-area').classList.remove('hidden');
|
|
document.getElementById('log-area').classList.remove('hidden');
|
|
document.getElementById('done-area').classList.add('hidden');
|
|
document.getElementById('error-area').classList.add('hidden');
|
|
|
|
try {
|
|
const r = await fetch('/create', { method: 'POST' });
|
|
const data = await r.json();
|
|
|
|
if (data.status === 'started' || data.status === 'already_running') {
|
|
btnText.textContent = 'Processing...';
|
|
startTime = new Date();
|
|
elapsedTimer = setInterval(updateElapsedTime, 1000);
|
|
pollTimer = setInterval(pollStatus, 2000);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to start pipeline:", err);
|
|
btn.disabled = false;
|
|
spinner.classList.add('hidden');
|
|
btnText.textContent = 'Retry';
|
|
}
|
|
}
|
|
|
|
async function pollStatus() {
|
|
try {
|
|
const r = await fetch('/create/status');
|
|
const state = await r.json();
|
|
|
|
const stageText = document.getElementById('stage-text');
|
|
const progressBar = document.getElementById('progress-bar');
|
|
const pctText = document.getElementById('pct-text');
|
|
const logList = document.getElementById('log-list');
|
|
|
|
stageText.textContent = state.stage || 'Running...';
|
|
const pct = stageProgress(state.stage || '');
|
|
progressBar.value = pct;
|
|
pctText.textContent = `${pct}%`;
|
|
|
|
if (state.log && state.log.length > 0) {
|
|
logList.innerHTML = state.log.map(l =>
|
|
`<div class="py-0.5 border-b border-white/5 last:border-0">${l}</div>`
|
|
).join('');
|
|
logList.scrollTop = logList.scrollHeight;
|
|
}
|
|
|
|
if (!state.running) {
|
|
clearInterval(pollTimer);
|
|
clearInterval(elapsedTimer);
|
|
pollTimer = null;
|
|
|
|
document.getElementById('btn-spinner').classList.add('hidden');
|
|
|
|
if (state.stage === 'done' || state.result) {
|
|
progressBar.value = 100;
|
|
progressBar.classList.add('progress-success');
|
|
document.getElementById('btn-text').textContent = 'Create New';
|
|
document.getElementById('done-area').classList.remove('hidden');
|
|
if (state.result) {
|
|
document.getElementById('done-msg').textContent = state.result.message;
|
|
}
|
|
} else if (state.error) {
|
|
progressBar.classList.add('progress-error');
|
|
document.getElementById('btn-text').textContent = 'Retry';
|
|
document.getElementById('error-area').classList.remove('hidden');
|
|
document.getElementById('error-text').textContent = state.error;
|
|
}
|
|
document.getElementById('create-btn').disabled = false;
|
|
}
|
|
} catch (err) {
|
|
console.error("Status poll failed:", err);
|
|
}
|
|
}
|
|
|
|
window.addEventListener('load', async function() {
|
|
lucide.createIcons();
|
|
try {
|
|
const r = await fetch('/create/status');
|
|
const state = await r.json();
|
|
const btn = document.getElementById('create-btn');
|
|
|
|
if (state.running) {
|
|
document.getElementById('progress-area').classList.remove('hidden');
|
|
document.getElementById('log-area').classList.remove('hidden');
|
|
btn.disabled = true;
|
|
document.getElementById('btn-spinner').classList.remove('hidden');
|
|
document.getElementById('btn-text').textContent = 'Running...';
|
|
startTime = new Date(); // Approximate
|
|
elapsedTimer = setInterval(updateElapsedTime, 1000);
|
|
pollTimer = setInterval(pollStatus, 2000);
|
|
} else {
|
|
btn.disabled = false;
|
|
}
|
|
} catch (err) {
|
|
console.error("Initial status check failed:", err);
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|