Save a 30-second Seedance 2.5 clip to S3 with boto3, no temp file

Stream the finished MP4 from Sume's /content endpoint straight into S3 with boto3 upload_fileobj. Includes the 30 s price table and a short script.

5 min readSume
All posts

To save a finished 30-second Sume render to S3, call GET /v1/videos/{id}/content?index=0 with your API key, stream the response, and hand it to boto3's upload_fileobj. The file never touches local disk, and the whole job is one request and one upload call.

A 30-second clip is the largest file a Sume video job produces today: seedance-2.5 accepts 4 to 30 seconds and wan-3.0 accepts 2 to 30 seconds. That makes it the case where a temp-file copy actually hurts, especially in a small container or a Lambda with limited /tmp.

Why stream instead of saving first

upload_fileobj takes any binary file-like object that implements read() and returns bytes, and it performs a multipart upload in several threads when the object is large enough (boto3 docs, read 2026-10-05). A streaming requests response body is exactly that kind of object, so you can connect the two directly.

The Sume side is a plain authenticated GET. The unsigned_urls array on a completed /v1/videos job points at the same content endpoint, and it still needs your key in the header, so a bare browser URL will not work. Keep the key on your server.

The script

Set SUME_API_KEY, SUME_JOB_ID, and BUCKET, and make sure AWS credentials are available in the usual boto3 way. Run it only after the job is completed; on any other status the content call will not return a video.

import os
import boto3
import requests

job = os.environ["SUME_JOB_ID"]
url = f"https://api.sume.com/v1/videos/{job}/content?index=0"
headers = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"]}
s3 = boto3.client("s3")

with requests.get(url, headers=headers, stream=True, timeout=(10, 60)) as r:
    r.raise_for_status()
    r.raw.decode_content = True
    s3.upload_fileobj(
        r.raw,
        os.environ["BUCKET"],
        f"clips/{job}.mp4",
        ExtraArgs={"ContentType": "video/mp4"},
    )
print("saved", job)

What to check before you trust the copy

Two cheap checks catch most bad copies. First, call raise_for_status() before reading the body, as the script does, so an error envelope is never written to S3 as if it were a video. Second, compare the stored object size with the Content-Length header if the response carries one.

Name the object after the Sume job id. A retry after a crash then overwrites the same key instead of leaving two copies, and the job id is the one identifier you should already be storing for recovery.

What a 30-second clip costs to keep

Storage is cheap next to generation. The expensive mistake is rendering again because a copy failed. The job is already paid for, so retry the download, not the submit.

Seedance 2.5, 30 s, Sume price (provider list x 1.25), Sume pricing tables, read 2026-10-05
ResolutionPrice per 30 s clip
480p$8.07
720p$17.34
1080p$42.65

Failure handling

If the upload raises halfway, boto3 aborts the multipart upload for you in the normal case, but you should still treat the object as absent and run the whole copy again from the Sume content URL. The content endpoint is a read, so repeating it does not bill anything and does not create a job.

Do not resubmit the render because a copy failed. Poll or read the stored job id instead, as Jobs and results explains.

Make the bucket side safe

Give the IAM role only s3:PutObject on the clips/ prefix. The script never lists or reads, so it should not be able to. If several workers can finish the same job, say a poller and a webhook handler, the shared key means the second upload simply replaces the first with identical bytes.

Add a lifecycle rule if you do not need the originals forever. Sume hosts its artifacts, but a copy in your own bucket is what your product serves, so decide how long it should live and set the rule once rather than cleaning up by hand.

Run it for a whole batch

Loop the same function over your stored job ids and bound the concurrency. A small thread pool of four is enough, because each copy is limited by the network and by S3, not by Sume. Skip ids whose status is not completed, and log the ones you skipped so a later pass can pick them up.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume