Check a Shorts timeline body offline: a Python mirror of the rules

Catch Timeline 1.0 refusals you can see in the body (start at 0, 0.5 s coverage, transitions, even sizes) in 28 lines of Python before the plan call.

6 min readSume
All posts

Why check offline when the plan is free

Sume gives you an unbilled POST /v1/timeline-1.0/plan that runs the real compiler and creates no job. For a single body it is the right check, and for a season it is still the last word. A local check earns its place elsewhere: it needs no network or key, so it can run in a unit test, a pre-commit hook or the loop that generates twelve bodies, and it tells you which of your own numbers is wrong before any request goes out.

The script in this post mirrors the rules you can read from the body alone, using the limits in the Timeline 1.0 page. It is deliberately not authoritative. If it says yes and the plan says no, the plan wins. If it says no, you saved a call.

Which refusals can be seen from the body

The table lists the rules the script mirrors and the Sume code each one maps to.

Timeline 1.0 refusals that a local check can mirror (read 2026-10-06)
Rule from the docsSume codeLocal check
video[0].start must be 0timeline_must_start_at_zeroCompare the first start
Starts must increaseinvalid_segment_timingCompare each start with the last
No transition on the first slottransition_on_first_segmentSlot 0 has no transition
Transition 1 s max and within half of the shorter neighbortransition_too_longCompare to both neighbors
Coverage within 0.5 s of the spine endCoverage ruleEnd of the last slot against the spine
Even output sizes 256 to 2160Request refusedRange and parity
fps 24, 25, 30 or 60Request refusedMembership

The checker

Pass it any body shaped like the render request. It returns a list of plain-English problems, empty when the body looks sound. It reads only audio.duration_seconds, video[] timings, transitions and output, so it works on bodies that do not yet have real URLs.

def check(body):
    errs, v, dur = [], body["video"], body["audio"]["duration_seconds"]
    out = body.get("output", {})
    if not 1 <= dur <= 1800 or not 1 <= len(v) <= 200:
        errs.append("spine must be 1-1800 s and slots 1-200")
    if v[0]["start"] != 0:
        errs.append("timeline_must_start_at_zero")
    for i, s in enumerate(v):
        t = s.get("transition")
        if s["duration"] < 0.2:
            errs.append(f"slot {i}: duration under 0.2 s")
        if i and s["start"] <= v[i - 1]["start"]:
            errs.append(f"slot {i}: starts must increase")
        if t and (i == 0 or t["duration"] > min(1, s["duration"] / 2, v[i - 1]["duration"] / 2)):
            errs.append(f"slot {i}: transition not allowed or too long")
    if v[-1]["start"] + v[-1]["duration"] < dur - 0.5:
        errs.append("coverage stops more than 0.5 s before the spine ends")
    for k in ("width", "height"):
        if k in out and not (256 <= out[k] <= 2160 and out[k] % 2 == 0):
            errs.append(f"output.{k} must be an even integer 256-2160")
    if out.get("fps", 30) not in (24, 25, 30, 60):
        errs.append("output.fps must be 24, 25, 30 or 60")
    return errs

body = {"audio": {"duration_seconds": 59}, "output": {"width": 1080, "height": 1921},
        "video": [{"start": 0, "duration": 30},
                  {"start": 30, "duration": 28, "transition": {"type": "fade", "duration": 1.5}}]}
print(check(body))

What it does not check, on purpose

Everything that needs the network or the media stays with Sume. That includes whether each URL is a media.sume.com artifact of your workspace (unsupported_media_source, source_not_found), whether a short source will be padded or looped, the maximum of 8 chained transitions, and the strategy rule that refuses render.strategy: "single" above 12 slots. The plan covers the first two partly: it runs Sume-host URL checks but does not download media, so it cannot predict a pad.

The script also does not catch overlapping slots that happen because of crossfades. Sume's compiler compensates for crossfades and treats declared starts as authoritative, so reproducing that exactly is better left to the real compiler.

Where it fits in a season script

Call check(body) for every episode body in the loop that builds them, fail the run on any non-empty result, and only then call the plan. That gives you three layers: your own check, the unbilled plan, and the real render with its warnings. YouTube's help page says Shorts uploads max out at 1080p and Shorts tools make videos up to 3 minutes long, so the default 1080 by 1920 output and a spine of 180 seconds or less are sensible bounds to assert in your own function as well.

The October platform roundup puts YouTube's Shorts series, with episodes and seasons, in rollout from 23 September. When a season is twelve or thirty renders, a cheap offline gate that runs in milliseconds is the difference between one typo and thirty failed jobs.

The sample call at the bottom of the script is a body with three deliberate mistakes: a 1.5-second fade on a 28-second slot, a last slot that stops half a second early, and an odd output height. Running it prints three problems and takes no network. Delete the sample and import check into your own loop. If you keep a test file next to the generator, add one passing body and one failing body per rule, so a refactor of the generator cannot quietly weaken the gate.

The same pattern extends cleanly. Add the stable codes from the docs as the strings your function returns, so a failure in your own gate and a refusal from Sume read the same in your logs. When Sume adds or changes a rule, the Timeline page is the source of truth; update the mirror from it, and keep the unbilled plan as the final check in the pipeline.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume