Split a long video into Shorts episodes: Python trim ranges
Generate back-to-back video trim ranges for a long vertical video with a tail rule, so no episode is a sliver. Respects the 1800 s source and 900 s output caps.

Why a tail rule
Cutting a long vertical video into equal episodes is easy until the last piece. A 412-second source cut at 60 seconds leaves a 52-second tail; a 361-second source leaves a one-second tail that is not an episode and, below 0.2 seconds, is not a valid trim at all. Video trim requires a duration between 0.2 and 900 seconds, and accepts a source up to 1800 seconds, so the splitter has to deal with both limits and with the tail.
The October platform roundup reports Shorts series, with seasons, episodes and sequential playback, rolling out from 23 September. An episode run needs the same range logic every time, and a function you can unit test is better than counting on a spreadsheet.
The limits the splitter respects
Each constraint below is printed on the trim page. The script raises on the first two and chooses ranges inside the third.
| Limit | Value | What the splitter does |
|---|---|---|
| Source length | Up to 1800 s | Raises before submitting anything |
| Output length | 0.2 to 900 s | Raises if an episode would exceed 900 s |
| Range inputs | start plus exactly one of end or duration | Uses start and duration |
| Price | $0.02 per job | Counts jobs, prints the sum |
| Idempotency-Key | Required | Build one per episode from season and number |
The splitter
episodes(412) returns seven ranges: six of 60 seconds and a final 52-second tail, since the tail is at least min_last. For a 370-second source the tail is ten seconds, below the minimum, so it folds into the sixth episode, which becomes 70 seconds. Change length and min_last to match your series. The script uses no network and runs as written.
def episodes(source_seconds, length=60, min_last=15):
"""Split a source into back-to-back trim ranges; fold a short tail into the last episode."""
if source_seconds > 1800:
raise ValueError("video trim sources are limited to 1800 s")
n = max(1, source_seconds // length)
if source_seconds - n * length >= min_last:
n += 1
ranges = []
for i in range(n):
start = i * length
end = source_seconds if i == n - 1 else start + length
if end - start > 900:
raise ValueError("a trim output is limited to 900 s")
ranges.append({"start": start, "duration": round(end - start, 3)})
return ranges
for r in episodes(412):
print(r)
print(len(episodes(412)), "trim jobs,", round(len(episodes(412)) * 0.02, 2), "USD")Sending the ranges
For each range, post to /v1/video-trim with the same video_url, the start and the duration, and an Idempotency-Key such as s1-ep03-trim-v1. Each response is a job: poll its status and read video_url from its result. If a keyframe-precision cut would start before the point you asked for, the result reports the real value in actual_start_seconds; use the default exact precision when the first frame of an episode matters.
A range that ends after the source clamps and returns trim_clamped_to_source. Since the splitter ends the last range at the source length, you should never see it. If you do, your idea of the source length is stale: read the real duration with a probe-only video inspect call, which is free.
Choosing the length
YouTube's help page says Shorts can be up to three minutes, so 60 seconds is a choice and not a limit. Pick the length from the content, not from the platform: a one-minute length leaves room for captions at the quoted price, a three-minute length does not.
Budget the run before you submit it. Seven ranges is seven jobs at $0.02 each, so the script's last line prints $0.14, and a thirty-minute source cut into 60-second episodes is thirty jobs, or $0.60. Compare that with the number of episodes you will actually publish, because an unpublished episode is still billed. The source limit is 1800 seconds, which is exactly thirty minutes; a source longer than that has to be cut in two passes.
Finally, store each range next to its job id when you submit. If a job fails, you want to re-submit that range with its original key, not recompute the list, and a stored range makes that a lookup instead of a calculation.
Test the function before you run it on real files. Four cases are enough: a source exactly divisible by the length, a source with a long tail, a source with a short tail, and a source over 1800 seconds. The first three return ranges that sum to the source length; the fourth raises. Put the cases in a unit test and the splitter stays correct when you change the numbers next season.
If a sentence straddles a boundary, the cut will split it. Equal ranges are a starting point. Read a transcript with video inspect, which bills transcription at $0.01 per audio minute, and move each boundary to the nearest pause. The splitter then takes a list of cut points instead of a fixed length, and the rest of the pipeline does not change.
Sources
Related posts
More in Developers
- Start a song at the chorus in a Short: split, then soundtrack
Timeline's soundtrack has no in-point. Cut the chorus with Timeline audio split, then pass the new audio_url as the soundtrack. Steps and cost: $0.01 + $0.10.
- Stitch four voice takes into one 3-minute Short with audio.parts
Timeline audio.parts joins up to 20 gapless narration slices inside one render, no re-synthesis. Build a 180-second Short from four takes, with the math.
- Sume 401 halfway through a batch: stop every worker, do not retry
A 401 on a Sume submit means a missing, malformed or revoked key. The SDK does not retry it, and neither should you. Python sample that keeps job ids.
- Sume SDK idempotencyKey: null sends no key, so POST retries stop
subscribeFormatRun mints a UUID Idempotency-Key by default. Pass null and the create call carries no key, so the SDK will not retry it on a 429 or 5xx.
Written by Sume