tqdm in Jupyter for a 30-second AI video render, no percentage

Sume gives job statuses, not a percent. Use a tqdm elapsed-time bar with the status as its label, and stop on terminal. Runs in a notebook cell.

5 min readSume
All posts

Sume does not report a render percentage, so the honest progress bar for a 30-second video job is an elapsed-time counter labeled with the current status. Poll GET /v1/jobs/:id/status, update the bar's description with sume_status, and stop when terminal is true.

A job moves through queued, processing, and then one of completed, failed, or canceled. There is nothing between processing and completed to count, so a fake 0 to 100 bar would lie.

What the status call gives you

The status response carries booleans terminal and result_ready, the sume_status value, and, while the job runs, next_poll_after_seconds. When that hint is present, obey it; when it is not, back off yourself. Do not call the endpoint in a tight loop across many jobs.

queued is a normal state. When your workspace is at its concurrency limit, valid jobs wait in queued until a slot opens, so a bar that sits in queued is waiting its turn, not failing.

The notebook cell

Set SUME_JOB_ID to the id from your submit response. The cell uses a tqdm bar with no total, so it shows elapsed time and an updating label. A deadline of 20 minutes is a reasonable client-side limit for a video job, and hitting it does not cancel the job.

import os, time, requests
from tqdm.auto import tqdm

job = os.environ["SUME_JOB_ID"]
url = f"https://api.sume.com/v1/jobs/{job}/status"
hdr = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"]}
deadline = time.monotonic() + 20 * 60
delay = 5.0

with tqdm(total=None, bar_format="{desc} | {elapsed}") as bar:
    while time.monotonic() < deadline:
        s = requests.get(url, headers=hdr, timeout=15).json()
        bar.set_description(s.get("sume_status", "?"))
        if s.get("terminal"):
            break
        wait = s.get("next_poll_after_seconds") or delay
        delay = min(delay * 1.5, 30.0)
        time.sleep(wait)
print(s.get("sume_status"), "result_ready:", s.get("result_ready"))

Reading the label

Job statuses and what the bar should say, Sume docs read 2026-10-05
sume_statusTerminalBar label idea
queuedNowaiting for a slot
processingNorendering
completedYesdone, fetch the result
failedYesfailed, read the error
canceledYescanceled

After the bar stops

Fetch GET /v1/jobs/:id/result only when result_ready is true or the status is completed. For any other status the result call answers 409 job_not_completed, so read the failure from the job record instead.

If your notebook kernel dies or the deadline passes, the job keeps running and billing. Keep the job id in a cell output or a file, and attach a new bar to it later rather than submitting again.

Stopping cleanly

If you interrupt the cell, the job is not canceled. Cancellation works only before generation starts, through POST /v1/jobs/:id/cancel. After processing begins the API answers 409 job_generation_already_started and the render finishes and bills normally. Treat the interrupt as stopping the wait, not the work.

Wrap the loop in try/finally and print the job id in the finally block, so a stopped notebook always leaves you the id you need to resume.

Show the queue honestly

Sume shows queue counts for the workspace but no per-job queue position or ETA. If you submit a few clips at once on a small plan, some will sit in queued by design. Show that label rather than a spinner, so the person watching the notebook knows nothing is stuck.

If you want a rough sense of timing, read GET /v1/jobs/:id/events after the fact. The job.started event tells you when the wait in the queue ended and the render began.

Where the bar belongs

Use the notebook widget version of tqdm for a cell that runs for minutes, so the output does not scroll. Close the bar in a finally block so an interrupted cell leaves a clean display.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume