Check pause-cut seams with video frames: 24 stills cover 12 seams
Video frames returns up to 24 stills per call. Two stills per seam covers 12 seams per call, so a 14-cut Reel needs 2 calls to see every join.

After a pause cut the risky moments are the joins: a clipped syllable, a face mid-blink, a jump in the background. POST /v1/video-frames takes up to 24 explicit at[] instants per call from one clip, so with two stills around each seam (just before and just after) one call covers 12 seams, and a Reel with 14 cuts (13 seams, 26 stills) needs 2 calls.
Instagram's First Draft hands you a starting cut that you review and reverse (read 2026-10-09). If you build the cut yourself, stills at the seams are the cheap review step.
Seam arithmetic
A seam is the boundary between two adjacent slots, so N cuts leave N - 1 seams. Two stills per seam gives 2(N - 1) frames, and each call carries at most 24. The source clip for the extract must be 300 s or shorter, which a short-form Reel is.
| Cuts | Seams | Stills (2 per seam) | Calls at 24 max |
|---|---|---|---|
| 10 | 9 | 18 | 1 |
| 14 | 13 | 26 | 2 |
| 20 | 19 | 38 | 2 |
| 34 | 33 | 66 | 3 |
Request
Send the rendered Reel (a media.sume.com artifact), one at[] list, optional format (jpeg default or png) and max_edge between 16 and 2160 to keep the images small. The submit always returns 202; poll GET /v1/video-frames/:id until resource_status is ready, then read frames[{t,url,width,height}]. Use output-timeline times, not source times, because you are looking at the rendered file.
import math
def seam_times(starts, lengths):
"""starts/lengths: slot start and duration on the OUTPUT timeline."""
t = []
for s, d in zip(starts[1:], lengths[:-1]):
edge = s # first instant of the next slot
t += [round(max(edge - 0.1, 0), 3), round(edge + 0.1, 3)]
return t
def batches(times, size=24):
return [times[i:i + size] for i in range(0, len(times), size)]
# one POST /v1/video-frames per batch, same render as video_url
body = {"video_url": RENDER_URL, "at": batches(seam_times(S, L))[0],
"format": "jpeg", "max_edge": 540}Gotchas
- Every
atvalue must be at least 0 and below the clip duration, or the worker fails the job withframe_time_out_of_range. Clamp the last seam if it sits near the end. - Send
at[]orfps, never both. WithfpsSume samples mid-bin and caps at 24 frames, which will not land on your seams. - A failed instant returns
url: nullfor that frame; the job still succeeds, so check every frame before you trust the batch. - Video frames is billed by Modal compute (reserved at submit, captured at actual usage), so this post does not give a flat price; read the live number from the catalog.
- Do not send ffmpeg-style fields such as
selectorvf; they are refused withffmpeg_fields_rejected.
Sources
Related posts
More in Media tools
- Concat 20 voice lines into one wav with timeline audio for 1 cent
Timeline audio concat joins up to 20 Sume-hosted audio parts into one gapless wav for a flat $0.01, and returns segment offsets to re-base your video slots.
- Conform a trim to 540x960 at 30 fps for TikTok non-Spark ads
TikTok's non-Spark ad spec sets 540x960 as the 9:16 minimum. One video-trim job with output 540x960 at 30 fps costs $0.02. Bitrate is not a field.
- Crop 1920x1080 to 3:4 with video filter: width 0.421875, x 0.2890625
A centered 3:4 crop of a 1920x1080 frame keeps 810x1080 pixels. In video-filter fractions that is width 0.421875, height 1, x 0.2890625. One job, $0.02.
- Crop 1920x1080 to 9:16 with an API: width 0.3164, x 0.3418
Center-crop a 16:9 clip to vertical with Sume video filter: the crop fractions, the arithmetic, and why 1280x720 falls under TikTok's 540x960 minimum.
Written by Sume