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:

Animated GIF of a SpaceX Starship booster descending on a flame toward a launch tower and being caught by its chopstick arms
GIF89a · 960 × 540 · 72 frames · 5.76 s · loops forever · 4.1 MB — published at /examples/starship-catch-harness/

The model didn't get smarter between those two answers. The structure did the work.

The loop

1Contract2Frozen acceptance3Sealed author4Deterministic checker5Bounded repairs
  1. Contract — you describe the scene as layout constants and measurable claims, not vibes.
  2. Frozen acceptance — before any generation, you write the gates a checker can measure. Then you freeze them.
  3. Sealed author — the model writes the whole render script. It never sees the checker.
  4. 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.
  5. 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

RoundWhat happened
R1Rejected 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.
R2Ran, 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.
R3Crashed 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.
R4Clean 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)

FormatPASS — GIF89a, 960×540, 72 frames, 5.76 s, loops forever, 4.1 MB
Background existsPASS — 5,797 unique colors mid-frame, near-black fraction ~0 (the raw answer was a void)
Motion, then holdPASS — frame difference 7.79 while descending, settling to 0.4 at the catch
Booster proportionsPASS — 458 metallic rows in the measured strip: tall and slender, no thermos
Catch armsPASS — both arm bands measure full in the caught frame (963 / 604 px)
FlamePASS 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

FileWhat it isSize
README.mdWhat the pack is, the loop, the quickstart, the limitation3,351 B
contract-template.mdThe sealed contract: describe your scene as layout constants and measurable claims1,470 B
acceptance-template.mdThe frozen acceptance: gates a checker can measure, written before generation1,621 B
cycle.pyThe generation cycle: prompt → model → import gate → run the render2,734 B
checker-template.pyThe deterministic checker: format, background, motion — plus your scene claims3,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.

🔨 Back to the Forge shelf 🤖 The DeepSeek proof