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.

4 min readSume
All posts

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.

Stills and calls for a pause-cut Reel (limits read 2026-10-09, arithmetic only)
CutsSeamsStills (2 per seam)Calls at 24 max
109181
1413262
2019382
3433663

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 at value must be at least 0 and below the clip duration, or the worker fails the job with frame_time_out_of_range. Clamp the last seam if it sits near the end.
  • Send at[] or fps, never both. With fps Sume samples mid-bin and caps at 24 frames, which will not land on your seams.
  • A failed instant returns url: null for 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 select or vf; they are refused with ffmpeg_fields_rejected.

Sources

Related posts

More in Media tools

All Media tools posts

Written by Sume