Add job progress bar to the dashboard

This commit is contained in:
justin committed 2026-09-20 16:49:03 -07:00
1 parent 4214a1351c
commit 37d3b5e6b7
2 files changed
+43 -7

No files matched your search

+26 -7
View File
@@ -198,7 +198,7 @@ stop_event = threading.Event()
runner = None runner = None
pending_flow = None pending_flow = None
pending_profile = None pending_profile = None
status = {"running": False, "stage": "idle"} status = {"running": False, "stage": "idle", "step": 0, "steps": 0, "job": "", "started": None}
class Cancelled(Exception): class Cancelled(Exception):
@@ -214,12 +214,23 @@ def check():
raise Cancelled() raise Cancelled()
def set_stage(stage, log=True): def set_stage(stage, log=True, step=None, steps=None):
status["stage"] = stage status["stage"] = stage
if step is not None:
status["step"] = step
if steps is not None:
status["steps"] = steps
if log: if log:
logger.info("Stage: %s", stage) logger.info("Stage: %s", stage)
def reset_progress(job=""):
status["step"] = 0
status["steps"] = 0
status["job"] = job
status["started"] = datetime.now().timestamp() if job else None
def truthy(v): def truthy(v):
return v is True or str(v).strip().lower() in ("true", "yes", "1", "pass", "safe") return v is True or str(v).strip().lower() in ("true", "yes", "1", "pass", "safe")
@@ -1351,6 +1362,9 @@ def run_job(s):
"location": str(job_dir), "location": str(job_dir),
}] + st["history"])[:200] }] + st["history"])[:200]
save_state(st) save_state(st)
scenes_n = max(1, int(s["target_seconds"]) // max(1, int(s["clip_seconds"])))
reset_progress(job_id)
set_stage(status["stage"], log=False, step=1, steps=3 + scenes_n * 2 + 2)
logger.info("Job %s: source %s '%s' (%s views)", job_id, src["id"], src["title"], f"{src['views']:,}") logger.info("Job %s: source %s '%s' (%s views)", job_id, src["id"], src["title"], f"{src['views']:,}")
logger.info("Working folder: %s", job_dir) logger.info("Working folder: %s", job_dir)
compliance = {} compliance = {}
@@ -1366,7 +1380,7 @@ def run_job(s):
n = len(script["scenes"]) n = len(script["scenes"])
for i, scene in enumerate(script["scenes"], 1): for i, scene in enumerate(script["scenes"], 1):
check() check()
set_stage(f"generating scene {i}/{n}") set_stage(f"generating scene {i}/{n}", step=3 + (i - 1) * 2)
path = job_dir / f"clip_{i:02d}.mp4" path = job_dir / f"clip_{i:02d}.mp4"
for attempt in range(1, 4): for attempt in range(1, 4):
try: try:
@@ -1384,7 +1398,7 @@ def run_job(s):
stop_event.wait(10) stop_event.wait(10)
continue continue
if kids: if kids:
set_stage(f"vision check scene {i}/{n}") set_stage(f"vision check scene {i}/{n}", step=4 + (i - 1) * 2)
try: try:
kids_vision_check(s, path) kids_vision_check(s, path)
vision_log.append({"clip": i, "attempt": attempt, "result": "passed"}) vision_log.append({"clip": i, "attempt": attempt, "result": "passed"})
@@ -1410,10 +1424,10 @@ def run_job(s):
clips.append(path) clips.append(path)
if kids: if kids:
compliance["vision_checks"] = vision_log compliance["vision_checks"] = vision_log
set_stage("stitching video") set_stage("stitching video", step=3 + n * 2)
final = job_dir / "final.mp4" final = job_dir / "final.mp4"
concat_clips(clips, final, s["aspect_ratio"]) concat_clips(clips, final, s["aspect_ratio"])
set_stage("uploading to YouTube") set_stage("uploading to YouTube", step=4 + n * 2)
vid, held = upload_video(s, final, script, tags) vid, held = upload_video(s, final, script, tags)
compliance["made_for_kids_flag"] = kids compliance["made_for_kids_flag"] = kids
compliance["synthetic_media_flag"] = True compliance["synthetic_media_flag"] = True
@@ -1434,7 +1448,7 @@ def run_job(s):
set_stage("moving files") set_stage("moving files")
location = finish_files(s, job_dir, job_id) location = finish_files(s, job_dir, job_id)
update_history(job_id, status="review" if held else "published", youtube_id=vid, location=location) update_history(job_id, status="review" if held else "published", youtube_id=vid, location=location)
set_stage("job complete") set_stage("job complete", step=status["steps"])
except Cancelled: except Cancelled:
update_history(job_id, status="cancelled") update_history(job_id, status="cancelled")
logger.warning("Job %s cancelled", job_id) logger.warning("Job %s cancelled", job_id)
@@ -1476,6 +1490,7 @@ def run_loop():
stop_event.wait(gap * 60) stop_event.wait(gap * 60)
finally: finally:
status["running"] = False status["running"] = False
reset_progress()
set_stage("idle") set_stage("idle")
logger.info("Autopilot stopped") logger.info("Autopilot stopped")
@@ -1498,6 +1513,10 @@ def api_status():
return { return {
"running": bool(runner and runner.is_alive()), "running": bool(runner and runner.is_alive()),
"stage": status["stage"], "stage": status["stage"],
"step": status["step"],
"steps": status["steps"],
"job": status["job"],
"elapsed": int(datetime.now().timestamp() - status["started"]) if status["started"] else 0,
"profile": s["name"], "profile": s["name"],
"engine": s["video_engine"], "engine": s["video_engine"],
"made_for_kids": s["made_for_kids"], "made_for_kids": s["made_for_kids"],
+17
View File
@@ -27,6 +27,10 @@ main{padding:20px;max-width:1100px;margin:0 auto}
.stat .v{font-size:17px;margin-top:4px} .stat .v{font-size:17px;margin-top:4px}
.stat .v.small{font-size:13px;word-break:break-all} .stat .v.small{font-size:13px;word-break:break-all}
.stat select{min-width:200px;margin-top:4px} .stat select{min-width:200px;margin-top:4px}
#progress-panel{display:none}
.bar{height:10px;background:#0f1115;border:1px solid var(--border);border-radius:6px;overflow:hidden;margin-top:6px}
.bar span{display:block;height:100%;width:0;background:var(--ok);transition:width .4s ease}
.bar.done span{background:#1b5e20}
.badge{display:none;background:#6a1b9a;color:#fff;border-radius:10px;padding:3px 10px;font-size:12px} .badge{display:none;background:#6a1b9a;color:#fff;border-radius:10px;padding:3px 10px;font-size:12px}
table{width:100%;border-collapse:collapse;font-size:13px} table{width:100%;border-collapse:collapse;font-size:13px}
th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--border);vertical-align:top} th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--border);vertical-align:top}
@@ -66,6 +70,10 @@ a{color:#64b5f6}
<div class="stat"><div class="k">Channel</div><div class="v" id="st-channel">Not linked</div></div> <div class="stat"><div class="k">Channel</div><div class="v" id="st-channel">Not linked</div></div>
<span class="badge" id="kids-badge">Made for kids: compliance checks on</span> <span class="badge" id="kids-badge">Made for kids: compliance checks on</span>
</div> </div>
<div class="panel" id="progress-panel">
<div class="row" style="justify-content:space-between"><div id="pg-stage" style="font-size:15px"></div><div class="hint" id="pg-meta"></div></div>
<div class="bar"><span id="pg-fill"></span></div>
</div>
<div class="panel row"> <div class="panel row">
<div class="stat"><div class="k">Storage folder</div><div class="v small" id="st-storage"></div></div> <div class="stat"><div class="k">Storage folder</div><div class="v small" id="st-storage"></div></div>
<div class="stat"><div class="k">Free space</div><div class="v" id="st-free">?</div></div> <div class="stat"><div class="k">Free space</div><div class="v" id="st-free">?</div></div>
@@ -219,6 +227,15 @@ $("#st-free").textContent=s.storage_free_gb===null?"not created yet":s.storage_f
$("#st-free").style.color=s.storage_free_gb!==null&&s.storage_free_gb<10?"#ffb74d":""; $("#st-free").style.color=s.storage_free_gb!==null&&s.storage_free_gb<10?"#ffb74d":"";
$("#st-after").textContent=s.delete_after_publish?"Delete files":"Move to published"; $("#st-after").textContent=s.delete_after_publish?"Delete files":"Move to published";
$("#kids-badge").style.display=s.made_for_kids?"inline-block":"none"; $("#kids-badge").style.display=s.made_for_kids?"inline-block":"none";
const panel=$("#progress-panel");
if(s.steps>0&&(s.running||s.step<s.steps)){
panel.style.display="block";
const pct=Math.min(100,Math.round(s.step*100/s.steps));
$("#pg-fill").style.width=pct+"%";
$("#pg-stage").textContent=s.stage;
const mins=Math.floor(s.elapsed/60),secs=s.elapsed%60;
$("#pg-meta").textContent=pct+"% | step "+s.step+" of "+s.steps+(s.elapsed?" | "+(mins?mins+"m ":"")+secs+"s":"");
}else{panel.style.display="none"}
const lb=$("#link");lb.textContent=s.channel?"Unlink YouTube Channel":"Link YouTube Channel";lb.dataset.linked=s.channel?"1":""; const lb=$("#link");lb.textContent=s.channel?"Unlink YouTube Channel":"Link YouTube Channel";lb.dataset.linked=s.channel?"1":"";
setButton(s.running); setButton(s.running);
$("#jobs").innerHTML="<tr><th>Started</th><th>Profile</th><th>Source</th><th>Title</th><th>Status</th><th>Video</th></tr>"+s.history.map(h=>`<tr><td>${esc(h.started)}</td><td>${esc(h.profile)}</td><td><a target="_blank" href="https://youtube.com/watch?v=${esc(h.source_id)}">${esc(h.source_title)}</a></td><td>${esc(h.title)}</td><td><span class="pill ${esc(h.status)}">${h.status==="review"?"private, needs review":esc(h.status)}</span>${h.notes?`<div class="hint notes">Reviewer notes: ${esc(h.notes)}</div>`:""}${h.error?`<div class="hint">${esc(h.error)}</div>`:""}${h.location?`<div class="hint">${esc(h.location)}</div>`:""}</td><td>${h.youtube_id?`<a target="_blank" href="https://studio.youtube.com/video/${esc(h.youtube_id)}/edit">studio</a> <a target="_blank" href="https://youtube.com/shorts/${esc(h.youtube_id)}">open</a>`:""}</td></tr>`).join("") $("#jobs").innerHTML="<tr><th>Started</th><th>Profile</th><th>Source</th><th>Title</th><th>Status</th><th>Video</th></tr>"+s.history.map(h=>`<tr><td>${esc(h.started)}</td><td>${esc(h.profile)}</td><td><a target="_blank" href="https://youtube.com/watch?v=${esc(h.source_id)}">${esc(h.source_title)}</a></td><td>${esc(h.title)}</td><td><span class="pill ${esc(h.status)}">${h.status==="review"?"private, needs review":esc(h.status)}</span>${h.notes?`<div class="hint notes">Reviewer notes: ${esc(h.notes)}</div>`:""}${h.error?`<div class="hint">${esc(h.error)}</div>`:""}${h.location?`<div class="hint">${esc(h.location)}</div>`:""}</td><td>${h.youtube_id?`<a target="_blank" href="https://studio.youtube.com/video/${esc(h.youtube_id)}/edit">studio</a> <a target="_blank" href="https://youtube.com/shorts/${esc(h.youtube_id)}">open</a>`:""}</td></tr>`).join("")