input: sheet-transparent.png · output: canvas-locked RGBA frames + canvas.json
Recovering frames, and refusing to guess
Two stages, one shared conviction. Slicing declines to invent frames it cannot find; registration declines to let a gesturing arm move the body. Both are small pieces of code whose value lies almost entirely in what they refuse to do.
-
03a
Components, not grid lines
farmer/slice.pyThe obvious way to cut a 2×3 sheet is to divide by three and by two. assetfarmer does not do that by default. It labels 4-connected components of
alpha > 16, ranks them by area, and takes the declared largest as frame seeds. The reason is diagnostic: grid cutting always “succeeds”, even when two frames have fused into one blob or a subject has drifted across a cell edge. Component labelling notices.Figure 7 — slicing with two guards. The second guard is the subtle one: even when enough components exist, a smallest seed under a tenth of the largest usually means two frames fused and a speck of noise was promoted into the gap. Both guards raise rather than repair. “If the declared frame count cannot be recovered from the components found, this is a HARD ERROR listing what was found — never a silent guess.”
farmer/slice.py:3–5Grid mode is not a repairslice_by_grid()exists and is reachable, but only because the caller asked for it. It keeps each cell's largest component plus any detached part at least 2% of that area — a lamp's separated glow, a held prop — and raises if a cell is empty. The distinction the codebase insists on is that a fallback the user chose is a decision, and a fallback the code chose is a lie. -
list[Image] → align_frames(frames, cell_w, cell_h)
-
03b
Registration that ignores the arms
farmer/align.pySliced frames arrive as independent crops with no shared origin. Registration puts them back on a common canvas by translation only — no rotation, no scale, because scale drift is a generation failure, not something to paper over. The choice that makes it work is which pixels get to vote.
Figure 8 — registration and the lock. The upper-65% mask is what lets a presenting gesture animate without the body sliding: the arm is outside the vote. The canvas lock is what lets two different actions of the same character be composited at the same on-screen scale without any runtime bookkeeping. Two different numbers that both look like “jitter”
The validation report carries both, and only one of them gates the run. Confusing them would make every gesture animation look broken.
Measurements from the repository's committed validation.json files Run max anchor jitter (px) max torso motion (px) Gated? receptionist/idle-breathing0.25 0.86 jitter only guide/idle-breathing0.72 0.90 jitter only guide/gesture-presenting1.12 9.02 jitter only janitor-idle-breathing1.17 1.12 jitter only lamp/flicker0.41 23.04 jitter only Anchor jitter is measured by
qa.anchor_jitter()on the bottom 15% of frame 0's subject bounding box, in fixed canvas coordinates. It answers one question: did the standing base slide? That is the artefact the eye reads as broken, and it is the number checked againstJITTER_LIMIT_PX = 3.0.Torso motion is the residual torso-centroid distance left after registration. It is supposed to be non-zero — it contains the choreography. The two extremes in the table make the point: a presenting gesture registers 9 px of torso movement because the character leans, and the lamp's flicker loop registers 23 px because a growing glow changes what the upper-65% mask even covers. Both runs pass, correctly, because their bases are planted to within half a pixel.
A limitation the code states about itselfJitter <3px is measured on the torso, not on gesturing limbs
— the README's wording, matching the comment onmax_torso_motion_pxinqa.py:138, markedinformational: includes choreographed motion
. No check in the codebase gates gesture quality; that judgment stays with the agent looking at the contact sheet.