unslothai/unsloth · error · RuntimeError

GIF export failed: the clip has no video stream.

Error message

GIF export failed: the clip has no video stream.

What it means

Raised by _transcode_gif right after opening the clip when src.streams.video is empty — the container has no video stream to decode into frames. Identical in shape to the WebM path's check (error 504) but on the GIF path, which decodes frames into memory rather than transcoding stream-to-stream.

Source

Thrown at studio/backend/core/inference/video_gallery.py:223

# Ceilings for a GIF export, which must hold every kept frame in memory before encoding. 720 px and 300 frames (25s at the
# 12 fps target) bound that at roughly 150 MB for the widest clip a generate request allows.
_GIF_MAX_EDGE = 720
_GIF_MAX_FRAMES = 300


def _transcode_gif(path: Path) -> bytes:
    import io

    try:
        import av
        from PIL import Image
    except Exception as exc:  # noqa: BLE001 -- missing deps -> no transcode
        raise RuntimeError("GIF export needs the 'av' and 'Pillow' packages.") from exc
    frames: list[Any] = []
    try:
        with av.open(str(path)) as src:
            if not src.streams.video:
                raise RuntimeError("GIF export failed: the clip has no video stream.")
            in_v = src.streams.video[0]
            rate = float(in_v.average_rate or 24)
            # Full-rate GIFs are huge and stutter; ~12 fps (skipping source frames) is the sweet spot.
            step = max(1, round(rate / 12))
            # Every kept frame is held as a paletted image until the encoder runs, so an unbounded walk is a memory bomb (a 2048x2048 clip of 1024
            # frames is >4 GB). Bound both axes: downscale past _GIF_MAX_EDGE and widen the step to at most _GIF_MAX_FRAMES. MP4 keeps the full clip.
            total = in_v.frames or 0
            kept = (total + step - 1) // step if total else 0
            if kept > _GIF_MAX_FRAMES:
                step = -(-total // _GIF_MAX_FRAMES)
            for i, frame in enumerate(src.decode(in_v)):
                if i % step:
                    continue
                if len(frames) >= _GIF_MAX_FRAMES:
                    # Frame count unknown up front (no stream metadata): stop at the cap.
                    break
                image = frame.to_image()
                if max(image.size) > _GIF_MAX_EDGE:

View on GitHub (pinned to 203007d190)

Solutions

  1. Verify the input with ffprobe or an av.streams check before exporting.
  2. Regenerate or repair the source clip.
  3. Guard the export UI/API to reject files without a video stream early.

Example fix

# before
_transcode_gif(Path("empty.mp4"))  # RuntimeError

# after
import av
with av.open(str(src_path)) as c:
    if not c.streams.video:
        raise HTTPException(400, "file has no video track")
Defensive patterns

Strategy: validation

Validate before calling

import av

def gif_exportable(path) -> bool:
    try:
        with av.open(str(path)) as c:
            return bool(c.streams.video)
    except Exception:
        return False

Try / catch

try:
    gif = _transcode_gif(path)
except RuntimeError as e:
    if "no video stream" in str(e):
        raise HTTPException(400, "source clip has no video track") from e
    raise

Prevention

When it happens

Trigger: GIF-exporting an audio-only or subtitle-only container; a zero-length video file; a corrupt clip whose video track failed to parse.

Common situations: Upstream generation crashed but left a stub file; user passes a music file to a GIF export endpoint.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/a30aa881f205a250. Report an issue: GitHub.