Open episode 2 on episode 1's last frame: video_frames, frame_images

Pull the closing still with POST /v1/video-frames, then send it as first_frame to POST /v1/videos so the next episode starts where the last cut ended.

5 min readSume
All posts

To open episode 2 exactly where episode 1 stopped, extract the closing still of episode 1 with POST /v1/video-frames, then pass that still to POST /v1/videos as a first_frame entry in frame_images. The still is a durable media.sume.com image, so the second call needs nothing else from the first.

This matters now because episodic series are being pushed on short-video platforms. Metricool reports that TikTok and the creator agency Amplify launched "The Next Episode", a program that helps fund creator-led series instead of one-off posts (reported, read 2026-10-03; the page gives no launch date). A series lives or dies on continuity between cuts, and the cheapest continuity trick is to share a frame.

Why the last frame is not the last frame you want

The at list on video frames accepts values from 0 up to, but not including, the clip duration. Ask for a time equal to the duration and the worker fails the job with frame_time_out_of_range and tells you the duration it probed. So pick a value just under the end. For a 90.0 second episode, 89.8 is safe.

There is a second trap. If your Timeline render used output.fade_out_seconds, the final frames are fading to black, and a first frame that is nearly black gives the next clip nothing to continue from. Timeline accepts a fade out of 0 to 5 seconds, so take the still from before the fade starts. With a 1 second fade on a 90 second cut, ask for 88.5 instead.

Use format: "png" for this still. The default is jpeg, and a lossless first frame avoids stacking compression on a frame that a video model will treat as ground truth. If the episode is large, max_edge (16 to 2160) clamps the long edge; omit it to keep the source size.

The two calls, step by step

Both calls are asynchronous in practice. Video frames always answers 202 and you read the result from GET /v1/video-frames/{id}; the video job is polled at GET /v1/videos/{jobId} or the job envelope. Send an Idempotency-Key on both so a retry does not queue a second paid job.

Episode hand-off, call by call (read 2026-10-03)
StepCallWhat to keep
1POST /v1/video-frames with video_url, at: [89.8], format: pngrequest_id, which is the resource id
2GET /v1/video-frames/{id} until resource_status is readyframes[0].url, a durable image artifact
3GET /v1/videos/models and read supported_frame_imagesConfirm the model lists first_frame
4POST /v1/videos with frame_images of type first_frameThe job id for episode 2's opening clip

Runnable sketch

The script below extracts the still and submits the next opening clip. Set SUME_API_KEY, and replace the video URL with a media.sume.com artifact from your workspace. Other hosts are refused at admission, so import external footage first with POST /v1/media-imports.

import os, time, requests

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

def closing_still(video_url, t, key):
    r = requests.post(f"{API}/v1/video-frames", headers={**H, "Idempotency-Key": key},
                      json={"video_url": video_url, "at": [t], "format": "png"})
    r.raise_for_status()
    rid = r.json()["request_id"]
    while True:
        d = requests.get(f"{API}/v1/video-frames/{rid}", headers=H).json()
        vf = d.get("video_frames", d)
        if vf.get("resource_status") == "ready":
            return vf["frames"][0]["url"]
        if vf.get("resource_status") in ("failed", "canceled"):
            raise RuntimeError(d)
        time.sleep(3)

ep1 = "https://media.sume.com/artifacts/artf_demo/ep01.mp4"
still = closing_still(ep1, 89.8, "ep01-closing-still")
body = {"model": "seedance-2.5", "duration": 8, "aspect_ratio": "9:16",
        "prompt": "Same room, a door opens behind her. Handheld, tense.",
        "frame_images": [{"type": "image_url", "image_url": {"url": still},
                          "frame_type": "first_frame"}]}
r = requests.post(f"{API}/v1/videos", headers={**H, "Idempotency-Key": "ep02-open"}, json=body)
print(r.status_code, r.json())

What to do before you queue a season

Run the hand-off once by hand on episodes 1 and 2 and look at the seam. A first-frame image fixes the pose, the wardrobe and the set, but it does not carry sound, motion direction or the lighting of the previous shot, so describe those in the prompt. Write the opening beat of episode 2 as a continuation: same location, same time of day, what changes.

Then check the model. Limits are not uniform across the catalog: seedance-2.5 accepts 4 to 30 seconds, most other models top out at 15 seconds, and each model advertises which frame types it takes in supported_frame_images. Read that field from GET /v1/videos/models rather than assuming a model accepts a first frame.

Finally, wire it so the next episode cannot start before the still exists. In a season script, make the closing still a dependency of the opening clip, store the still URL beside the episode record, and reuse it if you regenerate the opening, so a retake does not need another extract.

What Sume does and does not do

Sume extracts frames from a clip already hosted on media.sume.com and accepts first and last frame images on models that advertise them. It does not detect a good closing frame for you, it does not carry audio between episodes, and it does not promise the new clip will keep a face identical to the still; check the opening seconds of each result. Video frames is billed by its own compute rather than a flat fee, so confirm the live figure on GET /v1/catalog before a large run.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume