{"record":{"id":"b5cf03d12ddb3441","repo":"unslothai/unsloth","slug":"gif-export-needs-the-av-and-pillow-packages","errorCode":null,"errorMessage":"GIF export needs the 'av' and 'Pillow' packages.","messagePattern":"GIF export needs the 'av' and 'Pillow' packages\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":501,"severity":"error","filePath":"studio/backend/core/inference/video_gallery.py","lineNumber":218,"sourceCode":"        raise\n    except Exception as exc:  # noqa: BLE001 -- surface as \"encoder unavailable\"\n        raise RuntimeError(f\"WebM export failed (libvpx-vp9 unavailable?): {exc}\") from exc\n\n\n# 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","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/inference/video_gallery.py#L200-L236","documentation":"_transcode_gif lazily imports both PyAV ('av') and Pillow ('PIL.Image') and raises this RuntimeError when either import fails, since GIF export decodes with PyAV and re-encodes paletted frames with Pillow. MP4 export needs neither, and WebM needs only av, so this is GIF-specific.","triggerScenarios":"Requesting GIF export where Pillow or av (or both) is missing from the environment; virtualenvs created without the extras after a fresh clone.","commonSituations":"Headless servers without Pillow; partial dependency installs where av was added for WebM but Pillow never was; Python upgrades breaking one of the two packages.","solutions":["pip install av Pillow.","Reinstall whichever import fails (test with python -c \"import av; from PIL import Image\").","Export MP4 instead if installing is not an option."],"exampleFix":"# before\npip install av\n# GIF export -> RuntimeError('needs av and Pillow')\n\n# after\npip install av Pillow","handlingStrategy":"fallback","validationCode":"def gif_deps_available() -> bool:\n    try:\n        import av\n        from PIL import Image  # noqa: F401,F403\n        return True\n    except Exception:\n        return False","typeGuard":null,"tryCatchPattern":"try:\n    data = export_clip(clip, fmt=\"gif\")\nexcept RuntimeError as e:\n    if str(e).startswith(\"GIF export needs\"):\n        data = export_clip(clip, fmt=\"mp4\")\n    else:\n        raise","preventionTips":["Install both 'av' and 'Pillow' when GIF export is in scope.","Gate the GIF option in the UI on an import probe.","Remember dependency asymmetry: MP4 needs neither, WebM needs av, GIF needs both."],"tags":["video","gif","dependency","pyav","pillow"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}