The linesolid node = trunkdashed node = fork

assetfarmer Stage 01 · choreograph choreograph.py · grids.py · prompts.py · providers/

input: an action word and an accepted anchor · output: one raw sheet PNG

Everything that happens before a pixel is generated

The first four nodes of the trunk exist to make the generation boring. By the time a provider is called, the pose of every cell has been written out in prose, the master frame has been stamped into every cell at a fixed scale and foot line, and the prompt carries an explicit contract about what the background must be. The model is left with one job: change the pose.

  1. 01a

    Action word → per-cell script

    farmer/choreograph.py

    Twelve keys map to eight template functions. Each function holds an ordered phase list — six phases for the character actions, four for the object presets — and _resample() spreads that list across however many frames were requested. It is a lookup, not a generation; the file makes the position explicit in its own docstring.

    How _resample maps a six-phase idle-breathing script onto 2, 3, 4 and 6 cells A column on the left lists the six stored phases of the idle breathing template: neutral, inhale begins, inhale peak, exhale begins, exhale trough, settle. To the right, four rows show the cell counts 6, 4, 3 and 2. Each row draws connector lines from the phases actually selected by the index formula round of i times five over n minus one. Six cells use every phase; four cells use phases 1, 3, 4 and 6; three cells use phases 1, 3 and 6; two cells use only the first and last. A note at the bottom gives the exact expression from choreograph.py line 136. TEMPLATES["idle breathing"] 1  neutral relaxed stance 2  inhale begins 3  inhale peak 4  exhale begins 5  exhale trough 6  settle, ready to loop cells requested → phases selected -‌-frames 6 p1p2p3 p4p5p6 -‌-frames 4 p1p3 p4p6 -‌-frames 3 p1p3p6 -‌-frames 2 p1p6 same six phases The index formula, verbatim phases[round(i * (len(phases) - 1) / max(1, n - 1))] — choreograph.py:136 Python's banker's rounding is load-bearing: at n=3 the middle index is round(2.5) = 2, so the middle cell gets phase 3, not phase 4.
    Figure 2 — resampling a phase list. The same six lines serve any frame count; only the sampling changes. Object presets store four phases instead of six, which is why flicker at its default three frames lands on phases 1, 3 and 4.
    Hard stop

    LOCOMOTION_WORDS holds thirteen entries — walk, walking, run, running, jog, sprint, stride, locomotion, crawl, climb, swim, fly, flying. If the action's words intersect that set and no --script was passed, run_strip() raises at pipeline.py:61, before the anchor sheet is even built. The comment gives the reason: grid sheets were found to fail for locomotion, so the command routes you to an image-to-video brief instead of producing something bad.

    assetfarmer — locomotion is refused before the API call
    $ python3 -m farmer character --name hero --action "walk cycle" --frames 6 \
        --anchor characters/guide/anchor.png --out out/hero --subject "x" --provider auto
    [providers] auto: using 'gemini' (skipped: codex: dispatch script not found at
    /Users/…/.claude/plugins/codex-image/scripts/codex-image.mjs — set
    FARMER_CODEX_IMAGE_SCRIPT or pick another provider.)
    Traceback (most recent call last):
      File "/…/farmer/cli.py", line 37, in cmd_character
        report = run_strip(
                 ^^^^^^^^^^
      File "/…/farmer/pipeline.py", line 61, in run_strip
        raise ValueError(f"{action!r} is locomotion — use `farmer brief` (i2v) instead")
    ValueError: 'walk cycle' is locomotion — use `farmer brief` (i2v) instead

    The replacement command emits a start frame and a prompt, and calls nothing:

    assetfarmer — farmer brief writes a handoff, not an image
    $ python3 -m farmer brief --start-frame characters/guide/anchor.png \
        --action "walking left to right" --out out/guide-walk \
        --subject "the clay guide character" \
        --style "handcrafted claymation miniature style" --seconds 5
    {
     "tool": "Dreamina / Seedance (image-to-video)",
     "start_frame": "start-frame.png",
     "duration_seconds": 5,
     "prompt": "the clay guide character performing: walking left to right. Art style:
       handcrafted claymation miniature style. Perfect seamless loop: the last frame matches
       the first frame exactly. locked-off static camera, no camera motion, no zoom, no cuts.
       The subject stays in place (treadmill-style walking left to right if locomotion) —
       centered, constant scale, feet on a constant baseline. Plain unchanging background
       exactly as in the start frame. Natural weight and secondary motion; no morphing, no
       identity drift, no added objects.",
     "post": "Downstream: sample frames from the returned video at the target fps, then run
       `farmer cutout`/align/pack on the sampled frames if a sprite strip is needed, or ship
       the video loop directly."
    }
  2. scripts: list[str] → grids.build_anchor_sheet()
  3. 01b

    The anchor-tiled sheet

    farmer/grids.py

    This is the anti-drift device. Rather than describing the character again in words, the pipeline builds an image where the accepted master frame already occupies every cell at the exact intended scale and ground contact, and hands that to the model as the last reference. The prompt then only has to say change the pose.

    Anchor sheet geometry for a six-frame character run On the left, a 1536 by 1024 sheet drawn as two rows of three 512 pixel square cells, each containing the same tiled anchor silhouette sitting on a common foot line. On the right, one cell is enlarged to show the placement arithmetic: the master frame's alpha bounding box of 306 by 871 pixels is scaled by 0.3645 to 112 by 317, the foot line is at 0.84 of the cell height, giving y equals 430, the paste offset is 200 across and 113 down, and the central 70 percent safe area is marked. Callouts name the three ratios and the functions that compute them. build_anchor_sheet(master, rows=2, cols=3, cell_w=512, cell_h=512) sheet 1536 × 1024 px — sheet_size_for_grid(2, 3), cols > rows flat #FF00FF background; the same master in all 6 cells dashed line = foot line, identical in every cell one cell, exploded — 512 × 512 central 70% safe area y = 430 sh = 317 sw = 112 px = 200 the arithmetic bbox = (359,73,665,944) subject = 306 × 871 scale = min( 512×0.62 / 871, 512×0.72 / 306) = 0.3645 sw,sh = 112, 317 feet_y = round( 512 × 0.84) = 430 px = (512-112)//2 = 200 py = 430 - 317 = 113 values computed from characters/guide/anchor.png Three ratios, all overridable from the CLI -‌-subject-height-ratio 0.62 — share of cell height the subject fills subject_width_ratio 0.72 — width cap, function default only -‌-feet-ratio 0.84 — where the ground contact line sits Never a single row _GRIDS maps 4→2×2, 6→2×3, 9→3×3 — grids.py:20. Single rows drift horizontally.
    Figure 3 — anchor-sheet geometry. Numbers computed from the repository's own characters/guide/anchor.png using the character path's defaults. Because the same transform is applied to every cell, camera distance, anatomical scale and ground contact are identical across the sheet before generation begins.
    Grid selection, sheet size and resulting cell resolution
    Frames requestedGridSheetCellFiller cells
    2, 3, 42 × 21024 × 1024512 × 512up to 2
    5, 62 × 31536 × 1024512 × 512up to 1
    7, 8, 93 × 31024 × 1024341 × 341up to 2
    10, 11, 123 × 41536 × 1024384 × 341up to 2
    13 – 164 × 41024 × 1024256 × 256up to 3
    A consequence worth naming

    Cell resolution collapses as frame count rises: a sixteen-frame sheet gives each frame a 256 px cell, one quarter the linear resolution of a four-frame sheet. Nothing in the code warns about this. Frame counts above nine are supported arithmetically but the output is small.

    Filler cells are never left blank. build_sheet_prompt() pads the script list up to rows × cols with EXACT repeat of frame 1's pose (loop-return filler frame), the pipeline declares the full cell count to the slicer, and the extra frames are dropped afterwards with a single slice: sliced = sliced[:frames] (pipeline.py:106).

  4. anchor-sheet.png + scripts → prompts.build_sheet_prompt()
  5. 01c

    The prompt contract

    farmer/prompts.py

    Four builders share one structure and one closing block of strict rules. The rules are not stylistic; each one exists to protect a downstream stage. Read them as preconditions for despill and slicing.

    Each prompt invariant and the stage it protects
    Rule in the promptProtectsFailure it prevents
    Background is 100% solid flat magenta #FF00FF; no gradients, vignette, floor or background shadow despill pass 1 A gradient background falls outside the colour-distance ball and survives the hard cut
    No bloom, halo, glow gradient or cast light painted onto the background despill halo retry Emissive glow blended into the key sits beyond the normal unmix reach and leaves a pink corona
    Nothing may touch or cross a cell edge; subject stays inside the central 70% slice Two frames fusing into one connected component, which is a hard SliceError
    No borders, lines or frames between cells slice Drawn separators become their own components and confuse the seed ranking
    Same camera angle and distance; anatomical scale constant align, canvas lock Scale drift that registration cannot correct, since alignment is translation-only
    No text, labels, watermarks or UI slice, QA Stray marks attaching to the nearest frame as “noise”
    farmer/prompts.py:48–56build_sheet_prompt(), the anchor clause
    parts.append(
        "The reference images own the subject's identity and geometry — this generation "
        "owns MOTION ONLY. The last reference image is a scale-and-root template: the same "
        "accepted master frame repeated in every cell at the exact intended position. "
        "Preserve its exact cell locations, camera distance, standing-equivalent anatomical "
        "scale, body proportions, foot-contact line, and padding. Change ONLY the pose in "
        "each cell per the frame list below. Never zoom, resize, restyle, or redesign the "
        "subject. Never reproduce guide boxes, borders, labels, or separators.")
    The written prompt is saved to prompt.txt in the run directory before generation, so a bad sheet can be diagnosed against the exact text that produced it.
  6. prompt + refs[] → provider.generate()
  7. 01d

    One call, four possible backends

    farmer/providers/

    The provider layer is deliberately small: an abstract generate(), a Capabilities dataclass, and a verify_png() helper that re-opens and loads the file before it is handed back. Two of the four backends shell out to a CLI; the other two speak HTTP with nothing but the standard library, because the package refuses to grow a dependency beyond pillow and numpy.

    The auto provider chain and its fail-fast selection rule A vertical decision chain starting at get_provider with the argument auto. Four candidate providers are tried in order: codex, gemini, openai and fal. Each has a constructor precondition drawn as a diamond: a dispatch script file for codex, the gemini executable on PATH, the OPENAI_API_KEY variable, and the FAL_KEY variable. A failed precondition raises ProviderUnavailable and appends a reason to a list, moving to the next candidate. The first success returns a usable instance. If all four fail, a single ProviderUnavailable is raised listing every reason. A panel on the right states that this check happens at selection time only and never mid-pipeline. get_provider("auto") — providers/__init__.py:28 AUTO_CHAIN 1 · codex CLI dispatch script script file exists? yes return CodexProvider no — record reason, continue 2 · gemini local CLI gemini on PATH? return GeminiProvider 3 · openai stdlib HTTP OPENAI_ API_KEY? return OpenAIProvider 4 · fal stdlib HTTP FAL_KEY? return FalProvider raise ProviderUnavailable Why the check lives in the constructor A provider that cannot work must fail before the anchor sheet is built, not two minutes into a run with a half-written run directory. "Never raised mid-pipeline: if you got a Provider instance, it is usable." providers/base.py:17–18 Never trust a printed path codex parses the last SAVED: line, PIL-verifies that file, and copies it if it landed elsewhere. gemini runs inside a private temp directory and diffs the directory listing as well as scanning stdout, then verifies whatever it found. Every backend retries exactly once, after a five second sleep, then gives up with GenerationError.
    Figure 4 — provider selection. Skipped candidates are not silent: the chain prints the list of reasons alongside the provider it settled on, which is exactly what the locomotion transcript above shows happening.
    Provider capabilities, declared in each module's Capabilities instance
    ProviderTransportPrerequisite Max refsEdit endpoint Native transparencyFixed sizes
    codex (default) node dispatch script, 560 s timeout FARMER_CODEX_IMAGE_SCRIPT 5nono 1024², 1536×1024, 1024×1536
    gemini gemini --yolo slash command gemini on PATH or FARMER_GEMINI_CLI 3yesno any
    openai urllib + hand-rolled multipart OPENAI_API_KEY 4yesyes 1024², 1536×1024, 1024×1536
    fal urllib, refs as data: URIs FAL_KEY 4yesno any
    Live coverage

    Only codex has been exercised live end-to-end. The other three are implemented and unit-tested at the level of selection behaviour and, for gemini, a mock CLI — tests/test_providers.py. Treat their real-world behaviour as unproven, which is what the repository's own limitations section says.

    “NOTE (spec §4.7): the edit endpoint regenerates the whole canvas; pixel stability is enforced by the pipeline's composite-back, never by trusting this provider.”

    farmer/providers/openai_api.py:10–12