video_inspect 400s: frames at[] vs fps, and transcribe fields
Three video_inspect refusals: frames with both at[] and fps, frames with neither, and language_code without transcribe. Causes, fixes, a Python lint.

POST /v1/video-inspect accepts one frames program per call: frames: { at: [...] } for explicit seconds, or frames: { fps: n } for a sampling rate. Sending both gives video_inspect_frames_program_conflict, sending an object with neither gives video_inspect_frames_program_required, and language_code, segmentation or duration_seconds without transcribe: true gives video_inspect_transcribe_required.
What does each refusal mean?
These codes come from the Video inspect page. An omitted frames means 8 mid-bin stills, frames: false means probe only, and an object must pass exactly one of at[] (1 to 24 values, each 0 or more) or fps (above 0, up to 2, capped at 24 stills). The speech-to-text fields only make sense when a transcript is requested, so they are refused on their own instead of ignored.
| Code | Request | Fix |
|---|---|---|
| video_inspect_frames_program_conflict | frames has both at[] and fps | Keep one |
| video_inspect_frames_program_required | frames is an object with neither | Add at[] or fps, or use frames: false |
| video_inspect_transcribe_required | language_code, segmentation or duration_seconds without transcribe | Add transcribe: true, or remove the fields |
| inspect_source_has_no_audio | transcribe: true on a silent clip | Probe with frames: false and read probe.has_audio first |
How does this map to ffprobe and FFmpeg frame grabs?
With ffprobe you ask for stream facts, and with ffmpeg -vf fps=... you ask for stills in a separate command. Inspect folds both into one hosted call: the probe is always returned, and the stills follow the program above, with max_edge from 64 to 2160 (default 768) and a seek of precise or fast.
If you need source-size frames at known instants, the Video frames page is the other route: it takes at[] or fps too, with max_edge from 16 and no default clamp.
A lint you can run
The function below mirrors the three request-shape rules, so a client can reject a bad body before it spends a round trip. It runs as written.
Inspect defaults to mode: sync and waits up to 30 seconds for a 200, or answers 202 to poll. A transcript adds $0.01 per audio minute to the reservation, and omitting duration_seconds reserves 1 minute; the page says to confirm rates in GET /v1/catalog.
def inspect_error(body):
f = body.get("frames")
if isinstance(f, dict):
if "at" in f and "fps" in f:
return "video_inspect_frames_program_conflict"
if "at" not in f and "fps" not in f:
return "video_inspect_frames_program_required"
if not body.get("transcribe") and any(
k in body for k in ("language_code", "segmentation", "duration_seconds")):
return "video_inspect_transcribe_required"
return None
url = "https://media.sume.com/artifacts/artf_demo/talk.mp4"
print(inspect_error({"video_url": url, "frames": {"at": [1], "fps": 1}}))
print(inspect_error({"video_url": url, "frames": {}}))
print(inspect_error({"video_url": url, "language_code": "en"}))
print(inspect_error({"video_url": url, "frames": {"fps": 0.5}}))Which program should I pick?
Use at[] when you know the moments that matter, for example the first frame of each scene, and fps when you want an even sample. With fps the output is capped at 24 stills, so a 60 s clip at 2 fps is truncated, and a lower rate gives better coverage of a long clip.
For a quick health check of a file, send frames: false and read the probe. It is the lightest call and tells you the duration, size and whether audio exists before you decide on stills or a transcript.
What does a valid request look like?
A probe-only request is { "video_url": ..., "frames": false }. A stills request is { "video_url": ..., "frames": { "at": [0, 2.5, 5] } }. A transcript request adds "transcribe": true, and only then may language_code appear. Everything else is a refusal at submit.
Since the rules are all about the shape of the body, they are easy to enforce in a typed client. Model frames as a union of three variants, and the three codes above become compile-time impossibilities.
Sources
Related posts
More in Developers
- A video provider interface after the Sora shutdown, Sume behind it
OpenAI lists the Sora API shutdown as 2026-09-24 with no replacement. Put video generation behind one interface so the next vendor exit is a config change.
- Video API 400 unsupported_parameter: size, seed and provider.options
Sume's /v1/videos returns 400 unsupported_parameter for size, seed and a non-empty provider.options. Use resolution and aspect_ratio, and strip the rest.
- Video Router aliases first_frame_url, last_frame_url, duration_seconds
Three Video Router fields are aliases: first_frame_url for image_url, last_frame_url for end_image_url, duration_seconds for duration. Use the canonical names.
- Video Router sync waits 30 seconds; your Seedance 2.5 clip keeps going
mode sync and subscribe are the same bounded wait of at most 30 seconds. A long Seedance 2.5 job returns 2xx with the current state; poll, do not resubmit.
Written by Sume