Adapts mort's builtin animate skill into an executus/skillpack: - plans scenes + a reusable cast so recurring people/props stay consistent (built for funny bits 'about ourselves'), incl. multi-minute pieces - bundled scripts/encode.py owns the fragile encode + format decision: GIF for short loops, H.264 MP4 (yuv420p, even dims, +faststart — plays inline in Discord) for long pieces OR when a GIF comes out too big, with a gifski→ffmpeg-palette fallback - keeps the workspace discipline, text-readability pacing, and <=3-pass vision self-critique from the original; adds a frame budget for long renders encode.py verified end-to-end (short->gif, long->mp4 confirmed h264/yuv420p/ even/faststart); SKILL.md parses cleanly via the executus skillpack loader. Co-Authored-By: Claude Opus 4.8 <[email protected]>
10 KiB
name, description, license, allowed-tools, metadata
| name | description | license | allowed-tools | metadata | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| gif | 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. | MIT |
|
|
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'sencode_animation(...)takes aseconds_hintand (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:
/workspaceis WIPED before everycode_execcall. 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.- Only TOP-LEVEL files in
/workspace/come back asfiles_outfile_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.pyalready writes/workspace/final.gifor/workspace/final.mp4; you add/workspace/still.png. Render scratch frames under/tmp, never/workspace. - Don't dump many files into
/workspace. Hundreds of frame PNGs blow thefiles_outsize cap and get dropped. Keep/workspaceto 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:
- 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') - Render frames with Python (matplotlib / Pillow / numpy) into
/tmp/framesasframe_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). - Encode — let the helper choose GIF vs MP4:
from encode import encode_animation art = encode_animation("/tmp/frames", fps=15, seconds_hint=<intended seconds>) print(art) # {'path': '/workspace/final.mp4', 'mime': 'video/mp4', 'bytes': ...} - Write ONE critique still to
/workspace/still.pngin 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:
- Cast block at the top, defined ONCE: helpers like
def draw_steve(ax, x, y, pose): …and shared constants (PALETTE,SKY). - One function per scene (
def scene_0(...): …) that draws its beat by CALLING the cast helpers — never by re-describing a character inline. - A timeline that runs the scenes in order into
/tmp/frameswith 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 fromfiles_out. - Do not write the final artifact into a
/workspacesubdir — top level only. - Do not split render and read across separate code_exec calls —
/workspaceis 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.