moonlantern1 commited on
Commit
ed35a5c
·
verified ·
1 Parent(s): 4ce778e

Harden ClipForge progress polling UI

Browse files
Files changed (1) hide show
  1. app.py +144 -14
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
- @media (max-width: 600px) { nav { padding: 16px 20px; } .input-card { padding: 24px 20px; } #screen-processing { padding: 32px 16px 60px; } .pipeline { padding: 20px 16px; } .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; } }
 
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
- currentJobId = job.id;
722
  renderedClips = [];
723
  document.getElementById('clips-grid').innerHTML = '';
724
- document.getElementById('screen-input').classList.remove('active');
725
- document.getElementById('screen-processing').classList.add('active');
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
- await new Promise(r => setTimeout(r, 1400));
741
- const res = await fetch(`/api/jobs/${id}`);
742
- const job = await res.json();
743
- syncJob(job);
744
- done = job.done;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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').style.display = 'block';
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
- currentJobId = job.id;
 
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>"""