Add job progress bar to the dashboard
This commit is contained in:
@@ -198,7 +198,7 @@ stop_event = threading.Event()
|
||||
runner = None
|
||||
pending_flow = None
|
||||
pending_profile = None
|
||||
status = {"running": False, "stage": "idle"}
|
||||
status = {"running": False, "stage": "idle", "step": 0, "steps": 0, "job": "", "started": None}
|
||||
|
||||
|
||||
class Cancelled(Exception):
|
||||
@@ -214,12 +214,23 @@ def check():
|
||||
raise Cancelled()
|
||||
|
||||
|
||||
def set_stage(stage, log=True):
|
||||
def set_stage(stage, log=True, step=None, steps=None):
|
||||
status["stage"] = stage
|
||||
if step is not None:
|
||||
status["step"] = step
|
||||
if steps is not None:
|
||||
status["steps"] = steps
|
||||
if log:
|
||||
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):
|
||||
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),
|
||||
}] + st["history"])[:200]
|
||||
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("Working folder: %s", job_dir)
|
||||
compliance = {}
|
||||
@@ -1366,7 +1380,7 @@ def run_job(s):
|
||||
n = len(script["scenes"])
|
||||
for i, scene in enumerate(script["scenes"], 1):
|
||||
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"
|
||||
for attempt in range(1, 4):
|
||||
try:
|
||||
@@ -1384,7 +1398,7 @@ def run_job(s):
|
||||
stop_event.wait(10)
|
||||
continue
|
||||
if kids:
|
||||
set_stage(f"vision check scene {i}/{n}")
|
||||
set_stage(f"vision check scene {i}/{n}", step=4 + (i - 1) * 2)
|
||||
try:
|
||||
kids_vision_check(s, path)
|
||||
vision_log.append({"clip": i, "attempt": attempt, "result": "passed"})
|
||||
@@ -1410,10 +1424,10 @@ def run_job(s):
|
||||
clips.append(path)
|
||||
if kids:
|
||||
compliance["vision_checks"] = vision_log
|
||||
set_stage("stitching video")
|
||||
set_stage("stitching video", step=3 + n * 2)
|
||||
final = job_dir / "final.mp4"
|
||||
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)
|
||||
compliance["made_for_kids_flag"] = kids
|
||||
compliance["synthetic_media_flag"] = True
|
||||
@@ -1434,7 +1448,7 @@ def run_job(s):
|
||||
set_stage("moving files")
|
||||
location = finish_files(s, job_dir, job_id)
|
||||
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:
|
||||
update_history(job_id, status="cancelled")
|
||||
logger.warning("Job %s cancelled", job_id)
|
||||
@@ -1476,6 +1490,7 @@ def run_loop():
|
||||
stop_event.wait(gap * 60)
|
||||
finally:
|
||||
status["running"] = False
|
||||
reset_progress()
|
||||
set_stage("idle")
|
||||
logger.info("Autopilot stopped")
|
||||
|
||||
@@ -1498,6 +1513,10 @@ def api_status():
|
||||
return {
|
||||
"running": bool(runner and runner.is_alive()),
|
||||
"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"],
|
||||
"engine": s["video_engine"],
|
||||
"made_for_kids": s["made_for_kids"],
|
||||
|
||||
@@ -27,6 +27,10 @@ main{padding:20px;max-width:1100px;margin:0 auto}
|
||||
.stat .v{font-size:17px;margin-top:4px}
|
||||
.stat .v.small{font-size:13px;word-break:break-all}
|
||||
.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}
|
||||
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}
|
||||
@@ -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>
|
||||
<span class="badge" id="kids-badge">Made for kids: compliance checks on</span>
|
||||
</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="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>
|
||||
@@ -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-after").textContent=s.delete_after_publish?"Delete files":"Move to published";
|
||||
$("#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":"";
|
||||
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("")
|
||||
|
||||
Reference in New Issue
Block a user