Commit Graph
3 Commits
Author SHA1 Message Date
steveandClaude Opus 5 c1b2d41cc5 fix(gif): make the contact sheet an actual contact sheet
Closes mort#1519.

`_contact_sheet` built one tile per SCENE, and a single-scene animation — the
common case — short-circuited to `tiles[0][1].first_frame.save(path)`, a bare
frame. The harness then printed "still: … (contact sheet)" next to it.

Every single-scene worker across two production runs corrected that claim
unprompted, in the vision model's own words: "this is a single frame (not a
contact sheet)", "there is exactly 1 distinct panel/tile", "a single static
illustration, not an animation contact sheet" — ten of them. One worked out why
its critic could not see the brine tear it had animated, then proved it with a
pixel count, having already spent three full re-renders and 690 seconds on it.
It was the last of nine parallel workers to finish, holding the whole fan-out
open.

The sheet is now sampled across TIME: nine tiles by default, spread over the
whole animation, each labelled with its `t`. One scene gets all nine time
points; many scenes still get one tile each, as before, so nothing regresses
for the multi-scene case that already worked.

Two details that matter more than they look:

**Never sample t=1.0.** A loop is built to return to its starting state, so its
closing frame is the one instant guaranteed to show none of the motion. That is
exactly the frame the tear had faded out of.

**Label every tile with its t.** Without it the critic cannot tell "the tear is
missing" from "the tear is missing at t=0.75", and only the second is
actionable. The SKILL.md critique step now says so directly: check the other
tiles before re-rendering.

Reconciling the discrepancy the issue raised: the body said the still is the
t≈1.0 frame (backed by a pixel count on a live render), the code reads
`first_frame`. At HEAD it is NEITHER — `if sc.first_frame is None and
k >= n // 2` captures the MIDDLE frame, and that line arrived with the harness
in 7906a1a. The t≈1.0 observation therefore came from an older deployed pack.
It does not change the fix: one time point is one time point, whichever one it
is, and the single-scene short-circuit was the real defect.

Also: a failed scene now drops its samples along with its partial frames, so
the sheet can't advertise a scene that never shipped. Thumbnails are downscaled
at capture rather than held full-size — nine 640x480 RGB copies is ~8 MiB held
for the whole render, and the sheet shrinks them anyway.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-26 12:09:28 -04:00
steve 7906a1ab4c feat(gif): staging rails + bundled render harness
Two changes, both driven by measurement against 128 production gifsmith runs
and a local replica of the agent loop (docs/audits/2026-07-24-gifsmith-quality.md
in mort).

Staging rules. The renders that "worked" still read badly, always the same four
ways: the subject drawn 5-10% of the frame height under an empty sky, long runs
of pixel-identical frames, captions narrating action that was never drawn, and
flat untextured shapes. The recipe never asked for anything else. SKILL.md now
states four checkable rules — fill the frame, every frame moves, show it don't
caption it, give it depth — plus easing/anticipation/squash-and-stretch, and the
self-critique makes a "no" on depiction, subject size or caption legibility a
mandatory re-render instead of a suggestion.

Render harness. 26% of production code_exec calls end in a traceback, and every
crash class lives in plumbing the agent re-derives each run: scratch dir, frame
loop, numbering, canvas size, contact sheet, encode. scripts/gifsmith.py owns all
of it and the agent writes only @anim.scene drawing functions. A scene that
raises is now skipped while the others still render and ship, the canvas is
size-locked so ffmpeg cannot fail on jittering frames, and the drawing vocabulary
is re-exported so a missing import cannot NameError.

The harness also unblocks the multi-minute pieces this skill advertises but could
never produce: /tmp is a 16 MiB tmpfs (~13s of frames) and /workspace 64 MiB
(~51s), which is why "No space left on device" is 15% of all production crashes.
Anything past the GIF length threshold is an MP4, and an MP4 encodes in one
streaming pass, so those frames now go straight into ffmpeg's stdin and never
touch disk — verified at 1350 frames / 90 s with 0 MiB on disk.

encode.py is removed; the harness supersedes it.
2026-07-24 18:49:11 -04:00
Steve DudenhoefferandClaude Opus 4.8 fae89a1ed0 feat(gif): animated GIF/MP4 skill pack for Discord
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]>
2026-07-04 23:06:25 -04:00