Spaces:
Sleeping
Sleeping
Harden ClipForge progress polling UI
Browse files
app.py
CHANGED
|
@@ -250,6 +250,8 @@ def _snapshot(job: Job) -> dict[str, object]:
|
|
| 250 |
"nav_status": job.nav_status,
|
| 251 |
"done": job.done,
|
| 252 |
"error": job.error,
|
|
|
|
|
|
|
| 253 |
"logs": "\n".join(job.logs[-MAX_LOG_LINES:]),
|
| 254 |
"steps": job.steps,
|
| 255 |
"clips": [clip.__dict__ for clip in job.clips.values()],
|
|
@@ -406,6 +408,31 @@ async def create_job(
|
|
| 406 |
return JSONResponse(_snapshot(job))
|
| 407 |
|
| 408 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
@app.post("/api/probe-upload")
|
| 410 |
async def probe_upload(file: Annotated[UploadFile | None, File()] = None) -> JSONResponse:
|
| 411 |
if file is None:
|
|
@@ -552,8 +579,16 @@ INDEX_HTML = r"""<!DOCTYPE html>
|
|
| 552 |
.modal-actions { display:flex; align-items:center; gap:8px; }
|
| 553 |
.modal-close, .modal-download { padding: 8px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; font-family: 'DM Sans', sans-serif; font-size: 0.82rem; cursor: pointer; transition: all 0.15s; color:var(--ink); text-decoration:none; }
|
| 554 |
.modal-close:hover, .modal-download:hover { background: var(--champagne); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
.log-panel { display:none; margin-top:24px; background:var(--ink); color:var(--cream); border-radius:12px; padding:14px; font:12px/1.45 ui-monospace, SFMono-Regular, Consolas, monospace; white-space:pre-wrap; max-height:240px; overflow:auto; text-align:left; }
|
| 556 |
-
|
|
|
|
| 557 |
.thumb-1 { background: linear-gradient(135deg, #D4A96A 0%, #8B5E3C 100%); } .thumb-2 { background: linear-gradient(135deg, #7A9E8A 0%, #3D6650 100%); }
|
| 558 |
.thumb-3 { background: linear-gradient(135deg, #9E8A7A 0%, #5C3E2E 100%); } .thumb-4 { background: linear-gradient(135deg, #8A7A9E 0%, #4A3866 100%); }
|
| 559 |
.thumb-5 { background: linear-gradient(135deg, #9E9A7A 0%, #5C5820 100%); } .thumb-6 { background: linear-gradient(135deg, #C4856A 0%, #7A3020 100%); }
|
|
@@ -600,6 +635,16 @@ INDEX_HTML = r"""<!DOCTYPE html>
|
|
| 600 |
<h2 class="processing-title">Your clips are being crafted</h2>
|
| 601 |
<p class="processing-sub" id="processing-sub">Sit back - long videos can take a little while</p>
|
| 602 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
<div class="pipeline" id="pipeline">
|
| 604 |
<div class="pipeline-step" id="step-0"><div class="step-icon">Up</div><div class="step-content"><div class="step-name">Uploading video <span class="step-pct" id="pct-0">0%</span></div><div class="progress-track"><div class="progress-fill" id="fill-0"></div></div></div></div>
|
| 605 |
<div class="pipeline-step" id="step-1"><div class="step-icon">Text</div><div class="step-content"><div class="step-name">Generating transcript <span class="step-pct" id="pct-1"></span></div><div class="progress-track"><div class="progress-fill" id="fill-1"></div></div></div></div>
|
|
@@ -649,6 +694,44 @@ INDEX_HTML = r"""<!DOCTYPE html>
|
|
| 649 |
let currentJobId = null;
|
| 650 |
let renderedClips = [];
|
| 651 |
const iconLabels = ['Up','Text','Cut','Film','Edit'];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
|
| 653 |
function switchMode(m) {
|
| 654 |
currentMode = m;
|
|
@@ -718,12 +801,11 @@ INDEX_HTML = r"""<!DOCTYPE html>
|
|
| 718 |
btn.disabled = true;
|
| 719 |
btn.textContent = 'Starting...';
|
| 720 |
const job = await createJob();
|
| 721 |
-
|
| 722 |
renderedClips = [];
|
| 723 |
document.getElementById('clips-grid').innerHTML = '';
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
document.getElementById('nav-status').style.display = 'block';
|
| 727 |
syncJob(job);
|
| 728 |
pollJob(job.id);
|
| 729 |
} catch (err) {
|
|
@@ -734,18 +816,43 @@ INDEX_HTML = r"""<!DOCTYPE html>
|
|
| 734 |
}
|
| 735 |
}
|
| 736 |
|
| 737 |
-
async function pollJob(id) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
let done = false;
|
| 739 |
-
while (!done && currentJobId === id) {
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 745 |
}
|
| 746 |
}
|
| 747 |
|
| 748 |
function syncJob(job) {
|
|
|
|
|
|
|
| 749 |
document.getElementById('nav-status').textContent = job.nav_status || 'Processing...';
|
| 750 |
document.getElementById('processing-sub').textContent = job.error ? job.error : job.status;
|
| 751 |
document.getElementById('log-panel').textContent = job.logs || '';
|
|
@@ -773,7 +880,7 @@ INDEX_HTML = r"""<!DOCTYPE html>
|
|
| 773 |
}
|
| 774 |
if (job.done) {
|
| 775 |
document.getElementById('regen-section').style.display = 'block';
|
| 776 |
-
if (job.error) document.getElementById('log-panel').
|
| 777 |
}
|
| 778 |
}
|
| 779 |
|
|
@@ -829,13 +936,36 @@ INDEX_HTML = r"""<!DOCTYPE html>
|
|
| 829 |
window.scrollTo({ top: 0, behavior: 'smooth' });
|
| 830 |
try {
|
| 831 |
const job = await createJob(prompt);
|
| 832 |
-
|
|
|
|
| 833 |
syncJob(job);
|
| 834 |
pollJob(job.id);
|
| 835 |
} catch (err) {
|
| 836 |
alert(err.message || err);
|
| 837 |
}
|
| 838 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 839 |
</script>
|
| 840 |
</body>
|
| 841 |
</html>"""
|
|
|
|
| 250 |
"nav_status": job.nav_status,
|
| 251 |
"done": job.done,
|
| 252 |
"error": job.error,
|
| 253 |
+
"created_at": job.created_at,
|
| 254 |
+
"age_sec": max(0, int(time.time() - job.created_at)),
|
| 255 |
"logs": "\n".join(job.logs[-MAX_LOG_LINES:]),
|
| 256 |
"steps": job.steps,
|
| 257 |
"clips": [clip.__dict__ for clip in job.clips.values()],
|
|
|
|
| 408 |
return JSONResponse(_snapshot(job))
|
| 409 |
|
| 410 |
|
| 411 |
+
@app.get("/api/jobs")
|
| 412 |
+
def list_jobs() -> JSONResponse:
|
| 413 |
+
with JOBS_LOCK:
|
| 414 |
+
jobs = sorted(JOBS.values(), key=lambda item: item.created_at, reverse=True)[:10]
|
| 415 |
+
for job in jobs:
|
| 416 |
+
_publish_files(job)
|
| 417 |
+
return JSONResponse(
|
| 418 |
+
{
|
| 419 |
+
"jobs": [
|
| 420 |
+
{
|
| 421 |
+
"id": job.id,
|
| 422 |
+
"status": job.status,
|
| 423 |
+
"nav_status": job.nav_status,
|
| 424 |
+
"done": job.done,
|
| 425 |
+
"error": job.error,
|
| 426 |
+
"created_at": job.created_at,
|
| 427 |
+
"age_sec": max(0, int(time.time() - job.created_at)),
|
| 428 |
+
"clip_count": len(job.clips),
|
| 429 |
+
}
|
| 430 |
+
for job in jobs
|
| 431 |
+
]
|
| 432 |
+
}
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
@app.post("/api/probe-upload")
|
| 437 |
async def probe_upload(file: Annotated[UploadFile | None, File()] = None) -> JSONResponse:
|
| 438 |
if file is None:
|
|
|
|
| 579 |
.modal-actions { display:flex; align-items:center; gap:8px; }
|
| 580 |
.modal-close, .modal-download { padding: 8px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; font-family: 'DM Sans', sans-serif; font-size: 0.82rem; cursor: pointer; transition: all 0.15s; color:var(--ink); text-decoration:none; }
|
| 581 |
.modal-close:hover, .modal-download:hover { background: var(--champagne); }
|
| 582 |
+
.job-status-card { margin: -18px 0 24px; background: var(--white); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; display: flex; align-items: center; justify-content: space-between; gap: 12px; box-shadow: 0 2px 10px rgba(42,31,14,0.04); }
|
| 583 |
+
.job-status-main { display: flex; flex-direction: column; gap: 3px; text-align: left; min-width: 0; }
|
| 584 |
+
.job-status-main strong { font-size: 0.78rem; color: var(--ink); font-weight: 500; overflow-wrap: anywhere; }
|
| 585 |
+
.job-status-main span { font-size: 0.75rem; color: var(--ink-muted); }
|
| 586 |
+
.job-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
| 587 |
+
.job-action-btn { padding: 8px 12px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; color: var(--ink); font-family: 'DM Sans', sans-serif; font-size: 0.78rem; cursor: pointer; }
|
| 588 |
+
.job-action-btn:hover { background: var(--champagne); }
|
| 589 |
.log-panel { display:none; margin-top:24px; background:var(--ink); color:var(--cream); border-radius:12px; padding:14px; font:12px/1.45 ui-monospace, SFMono-Regular, Consolas, monospace; white-space:pre-wrap; max-height:240px; overflow:auto; text-align:left; }
|
| 590 |
+
.log-panel.open { display:block; }
|
| 591 |
+
@media (max-width: 600px) { nav { padding: 16px 20px; } .input-card { padding: 24px 20px; } #screen-processing { padding: 32px 16px 60px; } .pipeline { padding: 20px 16px; } .job-status-card { flex-direction: column; align-items: stretch; } .job-actions { justify-content: flex-start; } .clips-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; } .regen-section { padding: 22px 18px; } .regen-btn { width: 100%; margin-left: 0; } .regen-row { flex-direction: column; align-items: flex-start; } }
|
| 592 |
.thumb-1 { background: linear-gradient(135deg, #D4A96A 0%, #8B5E3C 100%); } .thumb-2 { background: linear-gradient(135deg, #7A9E8A 0%, #3D6650 100%); }
|
| 593 |
.thumb-3 { background: linear-gradient(135deg, #9E8A7A 0%, #5C3E2E 100%); } .thumb-4 { background: linear-gradient(135deg, #8A7A9E 0%, #4A3866 100%); }
|
| 594 |
.thumb-5 { background: linear-gradient(135deg, #9E9A7A 0%, #5C5820 100%); } .thumb-6 { background: linear-gradient(135deg, #C4856A 0%, #7A3020 100%); }
|
|
|
|
| 635 |
<h2 class="processing-title">Your clips are being crafted</h2>
|
| 636 |
<p class="processing-sub" id="processing-sub">Sit back - long videos can take a little while</p>
|
| 637 |
</div>
|
| 638 |
+
<div class="job-status-card">
|
| 639 |
+
<div class="job-status-main">
|
| 640 |
+
<strong id="job-id-label">Job pending</strong>
|
| 641 |
+
<span id="poll-status-label">Waiting for updates</span>
|
| 642 |
+
</div>
|
| 643 |
+
<div class="job-actions">
|
| 644 |
+
<button class="job-action-btn" onclick="manualReconnect()">Reconnect</button>
|
| 645 |
+
<button class="job-action-btn" onclick="toggleLogs()">Logs</button>
|
| 646 |
+
</div>
|
| 647 |
+
</div>
|
| 648 |
<div class="pipeline" id="pipeline">
|
| 649 |
<div class="pipeline-step" id="step-0"><div class="step-icon">Up</div><div class="step-content"><div class="step-name">Uploading video <span class="step-pct" id="pct-0">0%</span></div><div class="progress-track"><div class="progress-fill" id="fill-0"></div></div></div></div>
|
| 650 |
<div class="pipeline-step" id="step-1"><div class="step-icon">Text</div><div class="step-content"><div class="step-name">Generating transcript <span class="step-pct" id="pct-1"></span></div><div class="progress-track"><div class="progress-fill" id="fill-1"></div></div></div></div>
|
|
|
|
| 694 |
let currentJobId = null;
|
| 695 |
let renderedClips = [];
|
| 696 |
const iconLabels = ['Up','Text','Cut','Film','Edit'];
|
| 697 |
+
const JOB_STORAGE_KEY = 'clipforge_current_job_id';
|
| 698 |
+
let pollToken = 0;
|
| 699 |
+
let pollFailures = 0;
|
| 700 |
+
let lastJobSnapshot = null;
|
| 701 |
+
|
| 702 |
+
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
|
| 703 |
+
|
| 704 |
+
function showProcessingScreen() {
|
| 705 |
+
document.getElementById('screen-input').classList.remove('active');
|
| 706 |
+
document.getElementById('screen-processing').classList.add('active');
|
| 707 |
+
document.getElementById('nav-status').style.display = 'block';
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
function setPollStatus(text) {
|
| 711 |
+
document.getElementById('poll-status-label').textContent = text;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
function rememberJob(id) {
|
| 715 |
+
currentJobId = id;
|
| 716 |
+
try { localStorage.setItem(JOB_STORAGE_KEY, id); } catch (_) {}
|
| 717 |
+
document.getElementById('job-id-label').textContent = `Job ${id}`;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
function forgetJob(id) {
|
| 721 |
+
if (!id || currentJobId === id) {
|
| 722 |
+
try { localStorage.removeItem(JOB_STORAGE_KEY); } catch (_) {}
|
| 723 |
+
}
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
function toggleLogs() {
|
| 727 |
+
document.getElementById('log-panel').classList.toggle('open');
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
function manualReconnect() {
|
| 731 |
+
if (currentJobId) {
|
| 732 |
+
pollJob(currentJobId, { immediate: true });
|
| 733 |
+
}
|
| 734 |
+
}
|
| 735 |
|
| 736 |
function switchMode(m) {
|
| 737 |
currentMode = m;
|
|
|
|
| 801 |
btn.disabled = true;
|
| 802 |
btn.textContent = 'Starting...';
|
| 803 |
const job = await createJob();
|
| 804 |
+
rememberJob(job.id);
|
| 805 |
renderedClips = [];
|
| 806 |
document.getElementById('clips-grid').innerHTML = '';
|
| 807 |
+
showProcessingScreen();
|
| 808 |
+
setPollStatus('Connected - waiting for progress');
|
|
|
|
| 809 |
syncJob(job);
|
| 810 |
pollJob(job.id);
|
| 811 |
} catch (err) {
|
|
|
|
| 816 |
}
|
| 817 |
}
|
| 818 |
|
| 819 |
+
async function pollJob(id, options = {}) {
|
| 820 |
+
const token = ++pollToken;
|
| 821 |
+
rememberJob(id);
|
| 822 |
+
showProcessingScreen();
|
| 823 |
+
if (options.immediate) setPollStatus('Reconnecting...');
|
| 824 |
let done = false;
|
| 825 |
+
while (!done && currentJobId === id && token === pollToken) {
|
| 826 |
+
if (!options.immediate) await sleep(pollFailures ? Math.min(10000, 1800 + pollFailures * 1800) : 1400);
|
| 827 |
+
options.immediate = false;
|
| 828 |
+
try {
|
| 829 |
+
const res = await fetch(`/api/jobs/${id}?t=${Date.now()}`, { cache: 'no-store' });
|
| 830 |
+
if (!res.ok) {
|
| 831 |
+
if (res.status === 404) {
|
| 832 |
+
forgetJob(id);
|
| 833 |
+
currentJobId = null;
|
| 834 |
+
setPollStatus('Job not found - start a new upload');
|
| 835 |
+
break;
|
| 836 |
+
}
|
| 837 |
+
throw new Error(`Job refresh failed (${res.status})`);
|
| 838 |
+
}
|
| 839 |
+
const job = await res.json();
|
| 840 |
+
pollFailures = 0;
|
| 841 |
+
lastJobSnapshot = job;
|
| 842 |
+
syncJob(job);
|
| 843 |
+
done = Boolean(job.done);
|
| 844 |
+
setPollStatus(done ? 'Finished' : `Connected - updated ${new Date().toLocaleTimeString()}`);
|
| 845 |
+
} catch (err) {
|
| 846 |
+
pollFailures += 1;
|
| 847 |
+
setPollStatus(`Connection hiccup - retrying (${pollFailures})`);
|
| 848 |
+
if (lastJobSnapshot) syncJob(lastJobSnapshot);
|
| 849 |
+
}
|
| 850 |
}
|
| 851 |
}
|
| 852 |
|
| 853 |
function syncJob(job) {
|
| 854 |
+
if (!job || !job.id) return;
|
| 855 |
+
document.getElementById('job-id-label').textContent = `Job ${job.id}`;
|
| 856 |
document.getElementById('nav-status').textContent = job.nav_status || 'Processing...';
|
| 857 |
document.getElementById('processing-sub').textContent = job.error ? job.error : job.status;
|
| 858 |
document.getElementById('log-panel').textContent = job.logs || '';
|
|
|
|
| 880 |
}
|
| 881 |
if (job.done) {
|
| 882 |
document.getElementById('regen-section').style.display = 'block';
|
| 883 |
+
if (job.error) document.getElementById('log-panel').classList.add('open');
|
| 884 |
}
|
| 885 |
}
|
| 886 |
|
|
|
|
| 936 |
window.scrollTo({ top: 0, behavior: 'smooth' });
|
| 937 |
try {
|
| 938 |
const job = await createJob(prompt);
|
| 939 |
+
rememberJob(job.id);
|
| 940 |
+
setPollStatus('Connected - waiting for progress');
|
| 941 |
syncJob(job);
|
| 942 |
pollJob(job.id);
|
| 943 |
} catch (err) {
|
| 944 |
alert(err.message || err);
|
| 945 |
}
|
| 946 |
}
|
| 947 |
+
|
| 948 |
+
async function resumeStoredJob() {
|
| 949 |
+
let id = '';
|
| 950 |
+
try { id = localStorage.getItem(JOB_STORAGE_KEY) || ''; } catch (_) {}
|
| 951 |
+
if (id) {
|
| 952 |
+
setPollStatus('Resuming previous job...');
|
| 953 |
+
pollJob(id, { immediate: true });
|
| 954 |
+
return;
|
| 955 |
+
}
|
| 956 |
+
try {
|
| 957 |
+
const res = await fetch(`/api/jobs?t=${Date.now()}`, { cache: 'no-store' });
|
| 958 |
+
if (!res.ok) return;
|
| 959 |
+
const data = await res.json();
|
| 960 |
+
const active = (data.jobs || []).find(job => !job.done);
|
| 961 |
+
if (active && active.id) {
|
| 962 |
+
setPollStatus('Resuming active job...');
|
| 963 |
+
pollJob(active.id, { immediate: true });
|
| 964 |
+
}
|
| 965 |
+
} catch (_) {}
|
| 966 |
+
}
|
| 967 |
+
|
| 968 |
+
window.addEventListener('DOMContentLoaded', resumeStoredJob);
|
| 969 |
</script>
|
| 970 |
</body>
|
| 971 |
</html>"""
|