Last frame of an Omni clip: fixing frame_time_out_of_range

Asking Sume video-frames for t equal to the clip length fails with frame_time_out_of_range. Take the last frame at duration minus a frame, in Python.

4 min readSume
All posts

To get the last frame of an Omni clip on Sume, call POST /v1/video-frames with at set slightly below the clip length, for example 9.9 for a 10-second clip. If you send at: [10], the worker fails with frame_time_out_of_range, because each time must satisfy 0 <= t < duration. The end instant itself is outside the range.

Why you want that frame

Google's Gemini Omni 1.1 Flash scene extension reads up to 10 seconds of prior context and extends in 10-second steps to 40 seconds (Google, read 2026-10-05). The catalog model on Sume has no extend mode. The by-hand version is to take the last frame of clip N and send it as image_url, the first frame of clip N+1 (Sume docs: Video Router, read 2026-10-05).

What the API says

Those rules come from Sume docs: Video frames, read 2026-10-05. The generated clip is a Sume artifact, so its URL from GET /v1/jobs/{id}/result is already on the media host and needs no import.

  • at[] takes 1 to 24 values, each at least 0 and strictly less than the probed duration.
  • The source must be a media.sume.com artifact of your workspace, and at most 300 seconds.
  • A submit always returns 202. Poll GET /v1/video-frames/:id; when resource_status is ready, frames[] holds {t, url, width, height}.
  • The result carries source_duration_seconds, the duration the worker probed. The failure message also gives the probed duration.
  • Frames keep the source frame size unless you send max_edge.

Python

The code asks for 0.1 seconds before the requested duration. A provider can return a clip a few hundredths longer or shorter than the request, so if you see the error, retry with the probed duration minus 0.1.

import os, time, requests

API = "https://api.sume.com"
AUTH = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

def last_frame(video_url, duration):
    body = {"video_url": video_url, "at": [round(duration - 0.1, 2)],
            "format": "png"}
    r = requests.post(f"{API}/v1/video-frames", json=body,
                      headers={**AUTH, "Idempotency-Key": f"lf-{video_url[-24:]}"})
    r.raise_for_status()
    rid = r.json()["request_id"]
    while True:
        s = requests.get(f"{API}/v1/video-frames/{rid}", headers=AUTH).json()
        obj = s.get("video_frames", s)
        if obj.get("resource_status") == "ready":
            return obj["frames"][0]["url"]
        if obj.get("resource_status") == "failed":
            raise RuntimeError(obj)
        time.sleep(3)

# url = result["artifacts"][0]["url"] from GET /v1/jobs/{id}/result
# print(last_frame(url, 10))

Then chain

Send the returned image URL as image_url for the next clip, with a prompt that continues the action. Add end_image_url only if you also have a target last frame; end_image_url needs image_url too. The Sume docs describe image_to_video as an input-shape route, so you do not switch endpoints. The next clip is a new job with its own price. If you used png, the frame is lossless but larger than the default jpeg, which suits a first frame.

Limits of the by-hand chain

A single frame carries the pose and the framing but not the motion in progress. Google's extension feeds up to 10 seconds of context, so it can continue a movement. A frame chain restarts the motion, which you can mask with a prompt that names the continuing action. Sume's Omni route also takes up to 3 reference clips of at most 3 seconds each, addressed as <VIDEO_REF_0>. That is a second carrier for motion, but it is a reference for a fresh generation, not an extension.

Each hop adds a 10-second job, a frame job and a few seconds of waiting. For a 40-second piece, that is four video jobs and three frame extractions. The frame jobs bill by Modal compute and are small next to the clips; the exact amount is on the receipt.

Keep the margin small. A margin of one frame period is enough for a clip at 24 frames per second, and the returned frame is still the last one you can see.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume