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.

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.
| Rule from the docs | Sume code | Local check |
|---|---|---|
| video[0].start must be 0 | timeline_must_start_at_zero | Compare the first start |
| Starts must increase | invalid_segment_timing | Compare each start with the last |
| No transition on the first slot | transition_on_first_segment | Slot 0 has no transition |
| Transition 1 s max and within half of the shorter neighbor | transition_too_long | Compare to both neighbors |
| Coverage within 0.5 s of the spine end | Coverage rule | End of the last slot against the spine |
| Even output sizes 256 to 2160 | Request refused | Range and parity |
| fps 24, 25, 30 or 60 | Request refused | Membership |
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
- Check an image request against the Sume catalog before you send it
Fetch GET /v1/images/models once, then reject a bad ratio, quality or reference count in Python before it reaches POST /v1/images and returns a 400.
- TTS then H3 Max lip sync: check the 5 to 14.8 s audio window
H3 Max lip sync takes 5 to 14.8 seconds of Sume-hosted audio and clips the rest. Measure a TTS line from its word timings and price it before you submit.
- CI smoke test for Ideogram 4.5 on Sume: one low-quality image
A bash and jq check that submits one 1K low-quality Ideogram 4.5 image, passes on 200 or 202, and explains 401, 402 and 429. List price is $0.03.
- Clamp video duration when swapping Sora for a Sume video model
Each Sume video id has its own seconds range, so old Sora code can ask for a length a model rejects. Clamp per id before you submit; Python table of limits.
Written by Sume