The linesolid node = trunkdashed node = fork

assetfarmer Fork 04 · composite-back states.py · composite.py · registry.py · pack.emit_juice

entry: farmer state / farmer transition · requires a registered asset with identity.json

A lamp that turns on must not also move two pixels

Edit endpoints regenerate the whole canvas. Ask a model for “the same lamp, but lit” and you get a lamp that is lit and also very slightly different everywhere — a shifted shade, a redrawn base, a resampled edge. At runtime that reads as a bug. This fork does not ask the model to be stable. It takes the pixels it wanted from the generation, pastes the anchor's pixels back over everything else, and then asserts the result.

“Code guarantees over model trust: composite-back for states, endpoint locking for transitions, recomposite diffs for scenes — each asserted, with the check artifact saved next to the output.”

README.md, key design rules
  1. 04a

    The state pass

    farmer/states.py:69

    A registered object's current state is tiled into all four cells of a 2×2 sheet and the model is asked for the same new state in every cell. Co-generation on one canvas forces identity between the candidates; the pipeline then picks one, and throws away everything it did not ask for.

    The composite-back guarantee, with the lamp's real measurements A left-to-right sequence. First, the idle anchor of the lamp on a 201 by 341 canvas. Second, a two by two co-generated sheet whose four candidate cells changed 26455, 26533, 26810 and 26733 pixels respectively against the anchor. Third, the selection step: candidates are sorted by changed pixels and the upper median is taken, which selects cell three. Fourth, the auto change region, a bounding box of x 0, y 0, width 194, height 341 covering 96.5 percent of the canvas. Fifth, the composite-back itself: inside the region the candidate's pixels are kept verbatim, outside it the anchor's array is used unmodified. Finally the verification panel reports 2387 outside pixels checked and zero different, so identical outside region is true. A side note records the same three numbers for the wallclock: 473310 checked, zero different. farmer state lamp -‌-to on -‌-emissive 1 · idle anchor 201 × 341 px 68,541 pixels 2 · one 2×2 co-generation cell 0cell 1 cell 2cell 3 26,45526,533 26,81026,733 changed_px vs the anchor, after each candidate is registered back to its canvas 3 · pick the upper median valid.sort(key=changed_px) [26455, 26533, 26733, 26810] chosen = valid[len(valid)//2] → 26,733 → cell 3 robust against a cell that barely changed and one that drifted everywhere the refusal case no candidate changing at least MIN_CHANGE_PX = 40 → RuntimeError, pointing at candidates.png 4 · region, then paste back region [0,0,194,341] inside: candidate pixels, verbatim outside: a[y0:y1, x0:x1] = s[y0:y1, x0:x1] 5 · verify, or raise verify_outside_region(base, on, region) outside_pixels_checked 2,387 outside_pixels_different 0 identical_outside_region true false here would raise RuntimeError and nothing would be written to the registry The guarantee is exact. The region is not small. The lamp's auto-detected change region covers 66,154 of 68,541 pixels — 96.5% of the canvas. Only 2,387 pixels were actually protected by the composite-back. The wallclock's cracked state is better but still broad: 575,266 of 1,048,576 pixels, 54.9%, leaving 473,310 verified identical. Cause: co-generation re-renders the whole object, so a global shading shift pushes the detected diff toward the full silhouette. Pass -‌-region x,y,w,h What auto_change_region actually filters diff = (any RGB channel differs by more than 16 on pixels visible in both) OR (alpha differs by more than 32) − a 2 px rim band around the anchor's silhouette edge, because doubly   resampled antialiasing shimmers and is never a semantic change then morphologically opened by 1 px, dilated by 3 px, and any surviving component under 24 px is dropped. Returns the union bbox, or None. composite.py:144–186 Registration first, always Candidates are placed on the anchor's canvas by whole-silhouette FFT correlation — torso weighting is meaningless for a lamp. A gross scale mismatch above 10% is corrected by resampling; smaller deltas are left alone, because a glow that extends the bbox is a real change.
    Figure 9 — the guarantee, on real numbers. Every figure in this diagram is read from objects/lamp/runs/state-on/state-report.json and objects/wallclock/runs/state-cracked/state-report.json in the repository. The median selection can be checked by hand: sorting the lamp's four candidate scores puts 26,733 at index 2, and the report records picked_cell: 3.
    assetfarmer — the report the state pass left behind
    $ cat objects/lamp/runs/state-on/state-report.json
    {
     "object": "lamp",
     "state": "on",
     "from": "idle",
     "file": "states/on.png",
     "region": [ 0, 0, 194, 341 ],
     "picked_cell": 3,
     "changed_px": 26733,
     "candidates_changed_px": [ 26455, 26533, 26810, 26733 ],
     "outside_region": {
      "outside_pixels_checked": 2387,
      "outside_pixels_different": 0,
      "identical_outside_region": true
     },
     "despill": { "mode": "default", "residual": 0 },
     "emissive": true,
     "run_dir": "objects/lamp/runs/state-on",
     "pass": true
    }

    The command that writes it takes its flags from cli.py:242–253: --to names the new state, --from defaults to idle, --delta describes what changes (defaulting to the object switched to its <state> state), --region overrides the auto-detected box, --emissive requests the glow overlay, and --pick forces a candidate cell index.

  2. --emissive → composite.extract_glow(on, off)
  3. 04b

    Separating the glow instead of baking it

    farmer/composite.py:227

    An emissive state is stored twice: once as the finished state PNG, and once as a screen-blend layer that reproduces it from the unlit state. The separation is a direct algebraic solve, not a heuristic.

    farmer/composite.py:227–255extract_glow — solve screen(off, glow) = on
    # premultiply against transparent black so alpha differences (a glow that
    # extends past the off-state silhouette) contribute correctly
    n_rgb = n[..., :3] * n[..., 3:4]
    d_rgb = d[..., :3] * d[..., 3:4]
    denom = np.clip(1.0 - d_rgb, 1e-4, 1.0)
    glow  = np.clip((n_rgb - d_rgb) / denom, 0.0, 1.0)     # only brightening is
                                                           # representable in screen
    glow_u8[glow_u8 < threshold] = 0                       # threshold = 10 of 255
    im = im.filter(ImageFilter.GaussianBlur(blur))         # blur = 2.0, so a runtime
                                                           # can pulse it without banding
    alpha = arr.max(axis=-1)                               # normal-blend runtimes get
                                                           # a usable falloff too
    The returned PNG carries the screen layer in RGB and a per-pixel intensity in alpha, so a runtime that cannot do screen blending still has something sensible to composite. The lamp's copy lives at objects/lamp/overlays/on_glow.png and is recorded in the identity as "overlay" with "blend": "screen".
  4. two registered states → run_transition()
  5. 04c

    Transitions whose endpoints are not opinions

    farmer/states.py:198

    A transition strip has one obvious failure mode: the first frame nearly matches the start state and the last nearly matches the end state, so playing it snaps at both ends. The fix is mechanical. The endpoints are put into the template as actual pixels, the model is asked to reproduce them exactly, and then — after slicing — they are thrown away and replaced with the real state images anyway.

    Endpoint locking on a four-frame transition strip Top row: the template sheet for a four-frame transition on a two by two grid. Cell zero carries the actual start state pixels, cell three the actual end state pixels, and the two middle cells carry the nearer endpoint. Each cell is annotated with the per-cell script text, including the instruction to reproduce endpoint cells unchanged and the percentage progress lines for the in-betweens. Middle row: after generation, despill and slicing, frame zero is discarded and replaced with a copy of the true start state, frame three with a copy of the true end state, and frames one and two are registered to the start state and composited back inside the transition region. Bottom: the transition region is defined as the union of the two states' stored regions, and a verification step checks every frame against the start state outside that region, raising if any frame differs. 1 — the template sheet: endpoints are real pixels, not descriptions cell 0 — state A, actual script: "EXACTLY the starting state 'idle' as shown in the template cell — reproduce it unchanged" cells[0] = a cell 1 — in-between script: "the object 33% of the way through <delta>" cells[1] = a (nearer endpoint) cell 2 — in-between script: "the object 67% of the way through <delta>" cells[2] = b (nearer endpoint) cell 3 — state B, actual script: "EXACTLY the ending state 'on' as shown in the template cell — reproduce it unchanged" cells[3] = b generate → despill → slice → sliced[:4] 2 — what is actually written to states/idle_to_on/ frame_0.png out_frames.append(a.copy()) the generated cell 0 is discarded — this is the state PNG, bit for bit frame_1.png register_to_anchor(a, cand) composite_back(a, reg, region) generated content only inside the region frame_2.png register_to_anchor(a, cand) composite_back(a, reg, region) generated content only inside the region frame_3.png out_frames.append(b.copy()) the strip ends on the exact pixels the runtime will hold as the resting state the transition region Union of the two states' stored regions from identity.json. If neither state has one, the auto diff between the endpoints is used — and if the endpoints are identical, that raises rather than producing a strip of nothing. RuntimeError: states 'idle' and 'on' are identical — nothing to transition the assertion checks = [verify_outside_region(a, f, region) for f in out_frames] Every frame — including the endpoints — must be identical to state A outside the region, or the whole run raises before anything is registered.
    Figure 10 — endpoint locking. Strips are 2–6 frames (states.py:204) and are packed with loop=False, because a transition is not a cycle. The percentage lines in the middle cells are generated as round(100 * i / (frames - 1)).
    Status, stated plainly

    The transition path is fully implemented and exercised end to end by tests/test_states.py with a mock provider — including the assertion that frame_0 and frame_3 are bit-identical to the state images. It is not represented in the repository's shipped registry: objects/lamp/identity.json records "transitions": {}, and objects/lamp/runs/transition-idle_to_on/ contains only anchor-sheet.png and prompt.txt — a run that was started and never finished, despite the changelog's P4 entry describing a finished strip.

  6. identity.json → pack.emit_juice() → juice.json
  7. 04d

    Interaction feedback that is never art

    farmer/pack.py:50

    Hover, press, release, invalid, disabled. None of these are generated as frames. They are emitted as a transform-space recipe anchored to the asset's stored pivot, so a runtime animates them and the art stays one PNG.

    The lamp's recipe is derived arithmetically from its identity: pivot (100, 339) on a 201 × 341 canvas gives transformOrigin: "49.75% 99.41%". Every transform in the file repeats that origin, so a press squashes the object into the ground rather than around its middle.

    objects/lamp/juice.jsonwritten by emit_juice(), regenerated on every state pass
    {
     "object": "lamp",
     "pivot": { "x": 100, "y": 339, "convention": "bottom-center" },
     "transformOrigin": "49.75% 99.41%",
     "press":   { "scaleY": 0.92, "scaleX": 1.06, "duration": 0.09, "ease": "power3.out", ... },
     "release": { "scaleY": 1.0,  "scaleX": 1.0,  "duration": 0.34,
                  "ease": "elastic.out(1, 0.45)", ... },
     "invalid": { "keyframes": { "x": [-4, 4, -2, 2, 0] }, "duration": 0.3 },
     "emissive": {
      "on": { "layer": "overlays/on_glow.png", "blend": "screen",
             "pulse": { "opacity": [1.0, 0.72, 1.0], "duration": 2.4, "repeat": -1 } }
     }
    }
    The emissive block is added for any state whose identity entry carries an overlay, binding the pulse to the separated glow layer rather than to the object itself. Note the scope: emit_juice() is called from adopt_object() and run_state() only — characters never get a juice file.
    Registry shape

    Each registered asset owns an identity.json holding the anchor path, canvas spec, bottom-centre pivot, a ground footprint taken from the bottom 12% band of the subject bbox, a style pointer, and the states, transitions or loops ledger. Three declared fields — sockets, and for scenes lightRig and slots — are written as empty lists by registry.py and never populated by any code path in the package.