{"record":{"id":"a30aa881f205a250","repo":"unslothai/unsloth","slug":"gif-export-failed-the-clip-has-no-video-stream","errorCode":null,"errorMessage":"GIF export failed: the clip has no video stream.","messagePattern":"GIF export failed: the clip has no video stream\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":501,"severity":"error","filePath":"studio/backend/core/inference/video_gallery.py","lineNumber":223,"sourceCode":"# Ceilings for a GIF export, which must hold every kept frame in memory before encoding. 720 px and 300 frames (25s at the\n# 12 fps target) bound that at roughly 150 MB for the widest clip a generate request allows.\n_GIF_MAX_EDGE = 720\n_GIF_MAX_FRAMES = 300\n\n\ndef _transcode_gif(path: Path) -> bytes:\n    import io\n\n    try:\n        import av\n        from PIL import Image\n    except Exception as exc:  # noqa: BLE001 -- missing deps -> no transcode\n        raise RuntimeError(\"GIF export needs the 'av' and 'Pillow' packages.\") from exc\n    frames: list[Any] = []\n    try:\n        with av.open(str(path)) as src:\n            if not src.streams.video:\n                raise RuntimeError(\"GIF export failed: the clip has no video stream.\")\n            in_v = src.streams.video[0]\n            rate = float(in_v.average_rate or 24)\n            # Full-rate GIFs are huge and stutter; ~12 fps (skipping source frames) is the sweet spot.\n            step = max(1, round(rate / 12))\n            # 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\n            # 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.\n            total = in_v.frames or 0\n            kept = (total + step - 1) // step if total else 0\n            if kept > _GIF_MAX_FRAMES:\n                step = -(-total // _GIF_MAX_FRAMES)\n            for i, frame in enumerate(src.decode(in_v)):\n                if i % step:\n                    continue\n                if len(frames) >= _GIF_MAX_FRAMES:\n                    # Frame count unknown up front (no stream metadata): stop at the cap.\n                    break\n                image = frame.to_image()\n                if max(image.size) > _GIF_MAX_EDGE:","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/inference/video_gallery.py#L205-L241","documentation":"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.","triggerScenarios":"GIF-exporting an audio-only or subtitle-only container; a zero-length video file; a corrupt clip whose video track failed to parse.","commonSituations":"Upstream generation crashed but left a stub file; user passes a music file to a GIF export endpoint.","solutions":["Verify the input with ffprobe or an av.streams check before exporting.","Regenerate or repair the source clip.","Guard the export UI/API to reject files without a video stream early."],"exampleFix":"# before\n_transcode_gif(Path(\"empty.mp4\"))  # RuntimeError\n\n# after\nimport av\nwith av.open(str(src_path)) as c:\n    if not c.streams.video:\n        raise HTTPException(400, \"file has no video track\")","handlingStrategy":"validation","validationCode":"import av\n\ndef gif_exportable(path) -> bool:\n    try:\n        with av.open(str(path)) as c:\n            return bool(c.streams.video)\n    except Exception:\n        return False","typeGuard":null,"tryCatchPattern":"try:\n    gif = _transcode_gif(path)\nexcept RuntimeError as e:\n    if \"no video stream\" in str(e):\n        raise HTTPException(400, \"source clip has no video track\") from e\n    raise","preventionTips":["Check streams.video before any export call.","Regenerate clips that fail the check — the file itself is defective.","Reject video-less uploads at the request boundary."],"tags":["video","gif","corrupt-file","validation"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}