--- name: gif description: Make a funny animated GIF (or MP4) for Discord from a description — plans scenes, keeps recurring people/props consistent via a reusable cast, renders frames in Python, encodes cleanly, and automatically hands Discord an H.264 MP4 it can play when the piece is long or a GIF comes out too big. Use whenever someone asks to draw/animate/gif something, including multi-minute bits about people or things that happened. license: MIT allowed-tools: [code_exec, image_describe, send_attachments, file_get_metadata, think] metadata: version: "1.0.0" adapted-from: mort/skills/animate --- # Make an animated GIF / MP4 for Discord You make HIGH-QUALITY, funny animations from a description — often caricatures of the people in the channel or a bit about something that just happened. Naive PIL GIF export looks bad (256-colour banding, muddy dithering) and a multi-minute GIF is physically absurd (hundreds of MB), so follow this recipe: render frames in Python, let the bundled `encode.py` choose GIF vs MP4, and self-critique the result. **Work by calling tools — do not introduce yourself or list capabilities.** ## 1. Plan first: length, scenes, and cast Before drawing, decide (use `think` for anything non-trivial): - **How long / how many scenes?** Count the distinct beats the prompt actually describes, and render exactly that many. **ASSUME ONE.** A single continuous action ("a bouncing ball", "Steve slipping on a banana") is ONE scene — don't chop it into cuts. Start a new scene only when the prompt moves to a new beat (new action, place, or time jump). There is **no upper limit** — a bit that genuinely describes twelve beats gets twelve scenes and runs for minutes — but **never pad a simple request with invented beats**, and never compress beats the user clearly separated. When in doubt, fewer scenes. - **Who/what is the cast?** List every recurring character and prop (a person's persona, "the dog", a logo). For a bit "about ", pick a simple, recognizable caricature — one or two signature features (glasses, hair colour, a hat) — and pin each cast member's look ONCE as a reusable Python helper (see §4). That is what keeps the SAME person drawn identically across every scene instead of re-invented each cut. A one-scene GIF with a single subject needs no cast block. ## 2. Pick the format from the length (encode.py does this for you) - **Short & loopable (≤ ~20 s):** a GIF — fun, loops, autoplays. - **Long (tens of seconds to minutes):** an **MP4**. A 3–10 minute GIF is hundreds of MB and unusable; H.264 is the right tool and Discord plays it inline. - You don't hand-pick this: `encode.py`'s `encode_animation(...)` takes a `seconds_hint` and (a) goes straight to MP4 for long pieces, and (b) for short ones, makes the GIF but **auto-re-encodes to an MP4 if the GIF is too big for Discord to render nicely.** It returns `{"path", "mime", "bytes"}` — deliver whatever it hands back. ## 3. code_exec workspace — READ THIS (it is why draws fail) The `code_exec` sandbox has three hard rules; breaking them is the #1 cause of "vanished" files and wasted budget: 1. **`/workspace` is WIPED before every `code_exec` call.** Nothing survives between calls. So render → encode → write your critique still **all in ONE call**. Never write frames in one call and read them in another. 2. **Only TOP-LEVEL files in `/workspace/` come back as `files_out` file_ids.** A file in a subdir (`/workspace/frames/x.png`) is NOT returned. Write the final artifact and the ONE critique still directly at the top: `encode.py` already writes `/workspace/final.gif` or `/workspace/final.mp4`; you add `/workspace/still.png`. Render scratch frames under `/tmp`, never `/workspace`. 3. **Don't dump many files into `/workspace`.** Hundreds of frame PNGs blow the `files_out` size cap and get dropped. Keep `/workspace` to just the final artifact + one still; everything else lives under `/tmp`. `image_describe`, `send_attachments`, `file_get_metadata` take a **file_id from `files_out` — never a path.** Passing `"/workspace/final.mp4"` fails `file_not_found`. Read the file_id out of `files_out` and pass THAT. ## 4. Render → encode (ONE code_exec call, using encode.py) This skill bundles `scripts/encode.py`. Load it into the render call with `files_in` (its file_id is listed when you open this skill), then in a SINGLE `code_exec` call: 1. **Clear scratch** so stale frames can't leak into this encode: `import shutil, os; shutil.rmtree('/tmp/frames', ignore_errors=True); os.makedirs('/tmp/frames')` 2. **Render frames** with Python (matplotlib / Pillow / numpy) into `/tmp/frames` as `frame_00000.png, frame_00001.png, …` — **fixed 5-digit zero-pad, ONE continuous counter across all scenes** (do NOT restart numbering per scene, or the encoder reorders/overwrites frames). 3. **Encode** — let the helper choose GIF vs MP4: ```python from encode import encode_animation art = encode_animation("/tmp/frames", fps=15, seconds_hint=) print(art) # {'path': '/workspace/final.mp4', 'mime': 'video/mp4', 'bytes': ...} ``` 4. **Write ONE critique still** to `/workspace/still.png` in the same call — a representative frame, or for a multi-scene piece a COMPACT contact sheet with one small (~200–320px) tile per scene, labelled by scene index, so the critique can check every scene and cast consistency. Keep it low-res and legible — it's an internal aid, not the deliverable. The call ends with the final artifact + `still.png` at the top of `/workspace`; `files_out` returns a file_id for each. Give `code_exec` a generous `timeout_seconds` (e.g. 120+) — encoding is CPU-bound, and a long piece is real work, not a hang. ### Multi-scene: define the cast ONCE, reuse it everywhere It all still happens in the one call — structure the program as: 1. **Cast block at the top, defined ONCE:** helpers like `def draw_steve(ax, x, y, pose): …` and shared constants (`PALETTE`, `SKY`). 2. **One function per scene** (`def scene_0(...): …`) that draws its beat by CALLING the cast helpers — never by re-describing a character inline. 3. **A timeline** that runs the scenes in order into `/tmp/frames` with the one continuous counter, then encodes the whole sequence. Because every scene calls the same `draw_*` helpers, recurring people/props come out pixel-identical scene to scene — the entire point of the cast block. If you catch yourself re-describing a character inside a scene, hoist it into a cast helper. Carry shared state (positions, score, time of day) in plain variables between scene calls so cuts stay continuous. ### Budget for LONG pieces (minutes) A 3–10 minute animation is thousands of frames. Keep it renderable: **fps 10–12**, cap the long edge ~**480–540px** (`long_edge=` in `encode_animation`), and favour procedural motion that's cheap per frame — don't render 9000 frames of heavy matplotlib if a lighter draw reads the same. **If code_exec times out or errors mid-render you get nothing back**, so on the retry SCALE DOWN decisively (lower fps, smaller canvas, trim hold frames, reduce the longest scenes) — keep every beat, just render it leaner. Each retry counts toward the 3-pass cap; never burn all three on an over-ambitious render and leave the user with nothing. ## 5. Text in GIFs — make it READABLE Adding captions/labels/punchlines is fine, but pace them for a human. Motion can stay 10–20 fps; what matters is how long each distinct TEXT STATE holds — short phrases ~1.5–2 s, full sentences longer (budget by word count). At 15 fps that's ~25–30 repeated frames per text state, not a handful. Hold by duplicating frames, or with PIL pass a per-frame `duration=[…]` list (long on text, short on motion). Text flashing past unreadably is the #1 complaint — check it in the critique. ## 6. Self-critique (AT MOST 3 render passes) Before delivering, LOOK at what you made: call `image_describe` on the `still.png` **file_id** (from `files_out`, not a path) and judge it against the request — motion, colour banding, artifacts, framing, "does it depict what was asked", and text readability. For a multi-scene piece, judge scene by scene off the contact sheet AND check each recurring character/prop is the SAME across scenes (drift means that scene drew it ad hoc — fix the cast helper and re-render). If it falls short, change something concrete and re-run the WHOLE pipeline in one fresh `code_exec` call (workspace was wiped — rebuild from scratch). **At most 3 render passes total**; for a long piece be more decisive (1–2 passes) since each re-render is expensive. After the 3rd, deliver the best version with a one-line note on any trade-off. ## 7. Deliver Deliver the final artifact with a SINGLE `send_attachments` call using `channel_id` from your platform context and the **file_id that `encode_animation` produced** (read it from that render call's `files_out` by matching `final.gif` / `final.mp4` — NOT the still, NOT a path). Finish with a one-line caption. Critique aids (contact sheets, stills) are INTERNAL — never attach them unless the user explicitly asked. ## Forbidden - NEVER pass a `/workspace/...` path to image_describe / send_attachments / file_get_metadata — they take a file_id from `files_out`. - Do not write the final artifact into a `/workspace` subdir — top level only. - Do not split render and read across separate code_exec calls — `/workspace` is wiped between them. - Do not hand-roll the encode/format decision — call `encode_animation`; it guarantees a Discord-playable MP4 (yuv420p, even dims, faststart) when needed. - Do not restart the frame counter per scene — one continuous 5-digit counter. - Do not re-describe a recurring character inline per scene — one shared cast helper so it stays identical. - Do not invent beats the user didn't ask for — assume ONE scene, add more only for beats the prompt describes (no cap, but no padding). - Do not exceed 3 render passes; do not ship an empty/broken result without saying so.