The linesolid node = trunkdashed node = fork

assetfarmer Stage 06 · judgment & pack qa.py — 297 lines · pack.py · tests/ — 13 files

input: canvas-locked frames · output: validation.json, atlas, manifest, review.html

Three machine checks, then a human look

The line ends where automation stops being honest. Code can tell you the frame count came back, the base did not slide, and no key colour survived. It cannot tell you the gesture reads, or that the character's glasses vanished in frame four. So the last stage produces two things: a JSON verdict on the parts that are decidable, and a set of artifacts built for looking at.

  1. 06a

    What pass means

    farmer/qa.py:105

    Exactly three booleans, and pass is their conjunction. Everything else in validation.json is evidence, not a gate.

    The checks dict in validation.json, and what each one can and cannot catch
    CheckConditionCatchesBlind to
    frame_count len(frames) == declared Frames lost between slicing and packing Nothing much — slicing already raised if the count was wrong
    jitter_under_limit max anchor jitter ≤ JITTER_LIMIT_PX = 3.0 A subject whose standing base slides between frames Identity drift, wrong pose, a limb amputated by the despill
    residual_key_under_limit total residual key pixels ≤ RESIDUAL_LIMIT = 400 Surviving magenta fringe or an unresolved emissive halo Over-keying: a subject eaten by the deep unmix scores zero here

    Per frame the report also stores the alpha bbox, the visible pixel count, the anchor jitter, the torso motion and the residual count — enough to tell which frame is the problem without opening an image. And two images are written for the case where you do want to open something: a contact sheet with an onion-skin panel appended, and a GIF at the declared fps.

    farmer/qa.py:19–26anchor_jitter — measuring the thing that actually reads as broken
    """Ground-anchor stability: centroid drift of the feet/base band.
    
    The band is fixed in canvas coordinates from frame 0's subject bbox, so a
    choreographed torso lean or a raised arm cannot pollute the measurement —
    only actual translation of the standing base registers (the artifact the
    eye reads as jitter/sliding).
    """
    The band is the bottom FEET_BAND_FRACTION = 0.15 of frame 0's subject rows, and it stays at those canvas coordinates for every frame — deliberately not recomputed per frame, which is what makes it immune to the choreography.
  2. validate() → pack() → write_review_page()
  3. 06b

    Packing, and the review loop

    farmer/pack.py · farmer/qa.py:226

    Packing is short and strict: per-frame PNGs, a near-square atlas laid out row-major at cols = ceil(sqrt(n)), and a manifest with absolute rects, frame size, fps and loop flag. Non-uniform frame sizes are rejected outright, because a canvas-locked run is the precondition for everything downstream.

    Then the loop that closes the line. write_review_page() emits a self-contained HTML file — no server, no dependencies, no network — showing every frame on a checkerboard with a keep/reject checkbox, the GIF, the contact sheet, and a button that copies a curation.json to the clipboard.

    The review and curation loop, from packed run to re-validated run A cycle in five nodes. First, pack writes per-frame PNGs, an atlas and a manifest into the run directory. Second, write_review_page emits review.html, a self-contained page with one checkbox per frame plus the preview GIF and contact sheet. Third, the agent or human unticks bad frames and copies curation.json, a JSON object with name, keep and reject arrays. Fourth, farmer apply-curation reads the manifest and the curation file, refuses out-of-range indices and refuses to reject every frame, deletes the stale frame files, and repacks the kept frames under the same name. Fifth, the curated set is re-validated so validation.json, the contact sheet and the preview never describe the pre-curation frames, and a fresh review page is written, closing the loop. A side panel notes that torso motion is carried over from the old report because it cannot be recomputed after the fact. the P3 curation loop 1 · pack() frames/{name}_{i}.png {name}-atlas.png manifest.json atlas grid: cols = ceil(sqrt(n)), rows = ceil(n / cols), row-major 2 · write_review_page() review.html one checkbox per frame on a checkerboard, plus preview.gif and contact-sheet.png inline no server, no deps, no network 3 · the human look untick a frame → it is outlined red at 45% opacity, and the JSON below updates live {"name":…, "keep":[…], "reject":[4]} 4 · farmer apply-curation <run_dir> curation.json reject an index outside the run → ValueError naming the bad indices reject every frame → refuses, rather than producing an empty animation otherwise: delete every stale frames/*.png, repack the kept frames under the same name, same fps, same loop flag qa.py:243–297 5 · re-validate fresh validation.json, contact sheet and preview for the kept frames only a new review.html closes the loop curation-applied.json records kept / rejected / revalidated_pass loop back the fix that P4.1 shipped Before that release, curation repacked the frames but left the old validation.json in place — a report describing frames that no longer existed. Now the curated set is re-validated. One value cannot be recomputed after the fact: torso motion is a registration residual, so the surviving frames' old values are carried across and the code says so in a comment rather than silently zeroing them. what the review page is not It cannot regenerate a frame, reorder frames, or edit anything. The only decision it captures is keep or drop, and the only way to act on it is a second CLI invocation with a file you moved yourself. That is the whole interaction model: the page is an inspection surface, and every mutation goes back through the CLI where it can be checked, refused and recorded.
    Figure 13 — the loop that closes the line. The review page is a single HTML template with an inlined script (qa.py:161–223) and a clipboard fallback that builds a hidden textarea when navigator.clipboard is unavailable — which is what happens when you open the file from disk.
    assetfarmer — dropping one frame from a six-frame run
    $ cat curation.json
    {"name":"guide-gesture-presenting","keep":[0,1,2,3,5],"reject":[4]}
    
    $ python3 -m farmer apply-curation runs/gesture-presenting curation.json
    {
     "name": "guide-gesture-presenting",
     "kept": 5,
     "rejected": [
      4
     ],
     "frames_before": 6,
     "revalidated_pass": true
    }
    Export targets

    Beyond the manifest, --site-export writes the CatPlayer runtime format: {name}_{i}.png files plus an anim.json that is merged into any existing one rather than overwritten, so several animations can share one export directory (pack.py:94–105).

  4. and underneath all of it: tests/run_tests.py
  5. 06c

    A self-test that calls nothing

    tests/ · farmer/cli.py:144

    Thirteen files, plain assert statements, no framework. Each runs in its own interpreter and passes if it exits zero; the runner's exit code is the number of failures. Every fixture is synthetic — magenta sheets drawn at 4× supersampling so the blob edges are genuine blend pixels, and mock providers that repaint the template they were handed. Nothing reaches the network.

    assetfarmer — python3 -m farmer selfcheck
    $ python3 -m farmer selfcheck
    [PASS] test_align.py (2.5s)
    [PASS] test_composite.py (0.2s)
    [PASS] test_curation.py (0.4s)
    [PASS] test_despill.py (0.3s)
    [PASS] test_grids.py (0.2s)
    [PASS] test_juice.py (0.2s)
    [PASS] test_manifest.py (0.2s)
    [PASS] test_overlay.py (1.2s)
    [PASS] test_providers.py (0.5s)
    [PASS] test_registry.py (0.2s)
    [PASS] test_silhouette.py (0.6s)
    [PASS] test_slice.py (0.2s)
    [PASS] test_states.py (1.3s)
    
    13/13 test files passed
    What each of the thirteen test files asserts, mapped onto the pipeline stages Four columns grouped by pipeline area. The generation column lists test_grids for grid mapping and tiled sheets, and test_providers for selection-time unavailability, the auto chain, the unknown-name error and a mock gemini CLI. The pixel recovery column lists test_despill on synthetic magenta sheets including the halo retry and its emissive guard, test_slice for the no-silent-fallback errors, and test_align for recovering known shifts and merging canvas boxes. The state and scene column lists test_composite for outside-region bit identity and glow round-trip, test_states which drives run_state and run_transition end to end with a mock provider, test_overlay for the lighting rig, and test_silhouette which measures intersection over union against a synthetic ground truth. The output column lists test_manifest for atlas geometry and canvas-lock enforcement, test_registry for identity schema and adoption, test_juice for the recipe schema, and test_curation for the review page and apply-curation guardrails. A footer notes the two fixture techniques: supersampled magenta sheets and mock providers that repaint the template they are given. 13 test files, mapped onto the line GENERATION SIDE test_grids.py frame → grid mapping, never 1×N sheet and cell sizes tiled sheet construction test_providers.py ProviderUnavailable at selection time, never mid-pipeline the auto fallback chain unknown name → error; mock CLI no API keys, no network, no CLI binaries required to run these PIXEL RECOVERY test_despill.py background removal, edge unmix halo retry the emissive edge-case guard test_slice.py missing, fused and undersized components are hard errors grid mode as explicit opt-in test_align.py registration recovers known shifts canvas boxes merge monotonically lock survives a save/load round trip STATES & SCENES test_composite.py outside-region bit identity, auto region, glow extraction round-trip test_states.py run_state and run_transition driven end to end by a mock provider that injects +1 drift on every subject pixel test_overlay.py extraction and application; t=0 is the day plate test_silhouette.py IoU against a synthetic ground truth OUTPUT CONTRACTS test_manifest.py manifest schema, atlas geometry, canvas-lock enforcement test_registry.py identity schema, pivot, footprint, run migration, scene adoption test_juice.py recipe schema, pivot origin, pulse entries test_curation.py review page per run, apply-curation repack, guardrails on bad files The two fixture techniques that make AI-free testing possible magenta_sheet(w, h, blobs, aa=4) draws ellipses at 4× supersampling and downscales, so blob edges are genuine magenta-subject blend pixels — exactly the case the soft-alpha unmix exists for. Testing despill on hard-edged shapes would prove nothing. MockSheetProvider opens the anchor-sheet template it was handed, adds +1 to every non-magenta channel to simulate model drift, paints a warm ellipse into each cell, and saves. The composite-back assertion then has real drift to erase.
    Figure 14 — the test surface. The interesting design choice is in the footer: both fixtures were built to make the assertions meaningful rather than merely green. A mock that returned the template unchanged would pass composite-back trivially; this one injects drift so the guarantee has something to remove.
    A footnote on running it

    selfcheck shells out with sys.executable, so it uses whatever interpreter invoked it. On a machine where the system Python lacks numpy, all thirteen files fail on import — which is a packaging observation, not a code failure. Inside the project's environment the suite is green in roughly eight seconds.

  6. 06d

    Where the guarantees stop

    README.md · DESIGN.md

    Everything this pipeline can prove, it proves in code and writes down. It is worth being equally clear about the boundary of that.

    • identity drift

      Nothing measures whether the character in frame 4 is the same character as in frame 0. Accessory details and small faces wander; the anchor sheet reduces it and the contact sheet exposes it, but the check is a person looking.

    • over-keying

      The residual metric counts key colour that survived. A subject partly eaten by the deep unmix scores zero and passes. Only the onion-skin panel and the GIF catch that.

    • gesture quality

      Torso motion is recorded and explicitly not gated. Whether the presenting gesture reads as presenting is not a decidable property, and the code does not pretend otherwise.

    • plane cutting

      Silhouette refinement degrades toward the bounding rectangle when object and surroundings share colour statistics, and keeps occluders that sit inside the box. The module documents this in its own docstring under the heading “Honest scope”.

    “Inspect outputs visually after every stage — Read the contact sheet and preview GIF; never claim success without looking.”

    skills/claude/SKILL.md, the instruction given to the calling agent

    That instruction is the honest summary of the whole system. The Python is there so that when the agent does look, everything it sees is a real consequence of the generation rather than an artefact of the plumbing.