A one-file Python CLI that replaces a Sora script, on Sume

One argparse script, standard library only: prompt, model, seconds, resolution and aspect flags in, mp4 out, with an idempotency key derived from the arguments.

5 min readSume
All posts

If the thing you lost on 2026-09-24 was a small command-line script, the replacement is one Python file: argparse for flags, one POST to https://api.sume.com/v1/videos, a poll loop, and a download. The version below uses only the standard library and derives its Idempotency-Key from the arguments.

OpenAI's deprecations page lists the Sora video models and the Videos API as removed on that date and names no replacement. A CLI you wrote for it stops working with the rest, and a CLI is the easiest thing to rebuild.

Flags that map to Sume fields

Sume's /v1/videos takes model, prompt, duration as an integer number of seconds, resolution and aspect_ratio. The repo docs say it rejects size and seed with 400 unsupported_parameter, so the CLI has no flags for either. Fewer flags means fewer ways to send a request that cannot succeed.

CLI flag to request field (Sume docs, read 2026-10-05)
FlagRequest fieldDefaultLimit on the default model
promptpromptrequirednone stated
--modelmodelgemini-omni-flash-1.1catalog id
--secondsduration53 to 10 s
--resolutionresolution720p360p, 720p, 1080p, 4K
--aspectaspect_ratio16:916:9 or 9:16

The script

Save it as sumevid.py, export SUME_API_KEY, and run python3 sumevid.py "Paper boat on a rain puddle" --seconds 4. The default key is a SHA-256 of the parsed arguments, so running the exact same command twice returns the same job instead of a second paid one. Python's built-in hash() would be wrong here, because string hashing is randomized per process.

import argparse, hashlib, json, os, sys, time, urllib.request, urllib.error
ap = argparse.ArgumentParser(prog="sumevid")
ap.add_argument("prompt")
ap.add_argument("--model", default="gemini-omni-flash-1.1")
ap.add_argument("--seconds", type=int, default=5)
ap.add_argument("--resolution", default="720p")
ap.add_argument("--aspect", default="16:9")
ap.add_argument("--key", help="Idempotency-Key, default derived from args")
ap.add_argument("--out", default="clip.mp4")
a = ap.parse_args()

def call(url, body=None):
    h = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"], "Content-Type": "application/json"}
    if body is not None: h["Idempotency-Key"] = a.key or "cli-" + hashlib.sha256(repr(vars(a)).encode()).hexdigest()[:16]
    req = urllib.request.Request(url, json.dumps(body).encode() if body else None, h)
    try:
        with urllib.request.urlopen(req, timeout=60) as r: return json.load(r)
    except urllib.error.HTTPError as e: sys.exit(f"{e.code}: {e.read().decode()}")

job = call("https://api.sume.com/v1/videos", {"model": a.model, "prompt": a.prompt,
    "duration": a.seconds, "resolution": a.resolution, "aspect_ratio": a.aspect})
while job["status"] in ("pending", "in_progress"):
    time.sleep(10); job = call(job["polling_url"])
if job["status"] != "completed": sys.exit(f'{job["status"]}: {job.get("error")}')
urllib.request.urlretrieve(job["unsigned_urls"][0], a.out)
print("saved", a.out, job.get("usage"))

Why a derived key is the right default

A person at a terminal retries by pressing the up arrow. If the first run died after the request left, the second run should find the original job. Hashing the arguments gives exactly that. If they change the prompt, the key changes, and a new job is the right result. Pass --key explicitly when you want to rerun the identical command and pay for a fresh render.

At the repo-documented Omni 720p rate of $0.125 a second, the 5-second default costs 0.625, billed as $0.63.

  • Add a --dry-run that prints the body and exits, if you share the script.
  • Keep the poll at 10 seconds for interactive use; background jobs can wait longer.
  • Exit nonzero on failure so shell scripts can chain it.

What it leaves out

There is no reference image, no first frame and no webhook. Each of those is another field on the same request, and the stored posts on first frames and reference tags cover them. Add flags only for the fields you actually use.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume