GIF Forge: make a model produce the GIF it promised.
GIF Forge forces a code model to produce a real animated GIF — not a description of one, not an HTML page instead of one — by making it write a Python/Pillow render script under a frozen, machine-checked acceptance.
GIF Forge — first entry on the Forge shelf · built from the Starship catch run, 2026-10-20.
Raw vs forced: the same model, the same request
James's original prompt to DeepSeek: “Make me the most impressive .gif of a spacex starship catch that you can.” The raw answer was an HTML canvas page — not a GIF at all, with a thermos-shaped booster floating in a black void.
Under GIF Forge, the same model wrote a render script against an acceptance frozen before generation, and the harness ran it, measured the output, and fed the failures back inside a four-round bound. This is what came out — a real GIF89a, measured, not described:

The model didn't get smarter between those two answers. The structure did the work.
The loop
- Contract — you describe the scene as layout constants and measurable claims, not vibes.
- Frozen acceptance — before any generation, you write the gates a checker can measure. Then you freeze them.
- Sealed author — the model writes the whole render script. It never sees the checker.
- Deterministic checker — a script measures the GIF: format, background, motion, and your scene claims. It also emits a contact sheet for the one thing machines can't grade — whether the scene reads.
- Bounded repairs — failures go back verbatim: the exact traceback, the exact failing numbers. At most 4 generation rounds. Miss the bar and you report the loss with the measurements — you never lower the bar.
The worked example: the Starship run, round by round
| Round | What happened | |
|---|---|---|
| R1 | Rejected before it ever ran. DeepSeek returned a complete 14.6 KB render script — and the import gate refused it: the script imported os, outside the allowed set (PIL, math, random). The contract's own “print the file size” line had invited it; that mismatch is owned by the harness author in the record. | |
| R2 | Ran, then crashed on geometry. Imports fixed. The render died on a ground rectangle drawn from y=540 to y=534 — inverted, off the canvas edge. The exact traceback went back as the repair instruction, nothing vaguer. | |
| R3 | Crashed again, one layer deeper. Rectangle fixed — then a float from the eased descent math was fed to range() in the booster drawing and the render died again. Point fixes were chasing the crash class one instance at a time. | |
| R4 | Clean render, all 72 frames. The final repair ordered a whole-script audit of that entire class of bug — every range(), every coordinate pair, ints and clamps at every draw boundary. The script the model had designed in R1 finally ran start to finish and produced the GIF below. |
Measured results (checker output, final GIF)
| Format | PASS — GIF89a, 960×540, 72 frames, 5.76 s, loops forever, 4.1 MB |
| Background exists | PASS — 5,797 unique colors mid-frame, near-black fraction ~0 (the raw answer was a void) |
| Motion, then hold | PASS — frame difference 7.79 while descending, settling to 0.4 at the catch |
| Booster proportions | PASS — 458 metallic rows in the measured strip: tall and slender, no thermos |
| Catch arms | PASS — both arm bands measure full in the caught frame (963 / 604 px) |
| Flame | PASS as corrected — see the disclosure below |
Disclosure — the flame gate, corrected in the open.The frozen flame predicate demanded neon-saturated orange (R>180, B<90) and scored the finished GIF 0. The flame was visibly present on the contact sheet — rendered in muted rust and white-hot tones (peak orange (180,105,50)) that the predicate simply didn't cover. Re-graded with a predicate that measures the palette that exists, the plume region counts 3,628 flame-toned pixels against a ≥200 gate. The correction is recorded in the run report, not quietly patched — the third checker correction of that day, and the reason this pack ships its checker as a template you calibrate, not gospel.
The nits, stated plainly
- The flame palette is a muted rust, not the bright orange a real plume suggests.
- The booster sits low in its own smoke at the catch — a 40 px hover that partly reads as a landing.
- The two chopstick arms move differently from each other, and neither moves like the real mechanism. James noticed, was equal parts fascinated and annoyed — and chose to leave them exactly as they are.
The scene, the story, and the catch all read at a glance, and every frozen gate passed. The nits ship on this page because a Forge entry prints its limitations, not despite them.
Run it yourself
You bring a model API key; the pack brings the discipline. It defaults to DeepSeek (FORGE_API_KEY), and any OpenAI-compatible endpoint works — set FORGE_API_BASE and FORGE_MODEL for Qwen, OpenRouter, a local server, whatever you have.
pip install pillow export FORGE_API_KEY=sk-... # your key — DeepSeek by default # optional: export FORGE_API_BASE=https://api.deepseek.com # optional: export FORGE_MODEL=deepseek-chat cp contract-template.md my-contract.md # fill in YOUR scene cp acceptance-template.md my-acceptance.md python3 cycle.py my-contract.md r1 # generates render.py and runs it python3 checker-template.py out.gif r1 # measures it (add your checks!)
The honest limitation
The loop generalizes. The measurements don't. The Starship checker knew where the booster was because the contract pinned it there. For your scene, you pick three to six claims a script can measure — a position, a count, a color family, a before/after difference. That choice, turning “impressive” into numbers before the model starts, is the entire method.
Download the pack
| File | What it is | Size |
|---|---|---|
| README.md | What the pack is, the loop, the quickstart, the limitation | 3,351 B |
| contract-template.md | The sealed contract: describe your scene as layout constants and measurable claims | 1,470 B |
| acceptance-template.md | The frozen acceptance: gates a checker can measure, written before generation | 1,621 B |
| cycle.py | The generation cycle: prompt → model → import gate → run the render | 2,734 B |
| checker-template.py | The deterministic checker: format, background, motion — plus your scene claims | 3,945 B |
⬇ Download all five — gif-forge-pack.zip▶ The forced GIF's own page
Adding to the Forge
The Forge is a shelf, not a monument — this entry is the first, and others land alongside it as James or other participants contribute them. A Forge entry needs: a name, a one-line claim, an author line, a worked example with real measurements, the files, and its limitations stated on its page. If it can't state what would prove it failed, it doesn't go on the shelf.