* feat(ai-engine-burn): InSPyReNet for image.matte (Remove background) A second model on the Burn engine: `inspyrenet` answers `image.matte` from the picture alone, a grayscale PNG at the picture's size (white is the subject, soft along hair and fur). `remove-background` has had no local model; this is one. - Network: InSPyReNet with the Swin-B backbone, written by hand in Burn after the MIT reference code. Module names follow the PyTorch keys, so the published .safetensors loads by name through Burn's PyTorch adapter. A load that leaves a weight unset, fails one, or finds a foreign tensor is refused in plain words. - Any picture size: full resolution up to 1280 px on the long side; above that a reflect-padded square reduced with antialiased Lanczos-3, the matte enlarged back and refined at full resolution with a colour guided filter on the tiles near the edge (the Gyre approach), then a 0.2 fringe shrink. - Weights: 1038lab/inspyrenet inspyrenet.safetensors at a pinned revision, checked tensor for tensor against the author's ckpt_base.pth. Checked on one Windows machine (release): every stage against transparent-background's PyTorch code (worst relative difference 3.2e-5 on the processor, 2.4e-4 on the card, no pixel changing side); the shared conformance suite; on a real PNG the mask is byte-identical to the same port in PhotoCraft. Four new deliberate-bug cases in `be`, all caught. Not done: a catalogue recipe (a recipe names a ComfyUI workflow today), clean timings, macOS and Linux runs, a run through an app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ai-features): a recipe that is built in only, InSPyReNet for Remove background `LocalRecipe::comfyui` (default true) marks a recipe ComfyUI runs. With false it is only its built-in routes: never a ComfyUI setup option, plan, install offer or connection, and engine advice does not list ComfyUI for a tool without a ComfyUI recipe. `Feature::comfyui_recipes()` gives those recipes; `engine_recipe`, `plan_native`, `setup_offer_native` and `enable_native` are unchanged. `remove-background` gets one such recipe: InSPyReNet on Burn, the pinned `inspyrenet.safetensors` (1038lab/inspyrenet @ 0f594663, 367,120,444 bytes) in the folder `inspyrenet`, so Set up AI offers one checked download and no ComfyUI. Tests: the catalogue, setup options, plans and offers (`ai-features/tests/built_in_only.rs`), registration and advice (`ai-client/tests/built_in_only.rs`), and through `AiClient` on Burn (`ai-client/tests/burn_matte.rs`, the real run ignored). Mutation group `bo`: 8 of 8 caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tools): retarget two `lc` mutation cases at the ComfyUI recipe lookups `unrunnable-recipe-accepted` and `second-recipe-takes-plain-id` searched for the `feature.locals` lookups in `ai-client/src/local.rs`, which now go through `Feature::comfyui_recipes()`. Same mutations, new targets; group `lc`: 21 caught, one known equivalent. Every case in `GROUPS` again has exactly one target (640 checked). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ai-engine-burn): InSPyReNet runner checks around a stand-in, readout without panic CI has no graphics card, only a software renderer (lavapipe, WARP), and the engine takes it. There the tiny network was too slow for the conformance suite's poll limit, and on lavapipe the readout failed. Unoptimised, Burn's processor backend takes minutes for the same network. - The loaded model runs the pipeline around a `Predict`: the network, or in tests a stand-in, as the SAM 2.1 tests here use a fake backend. The conformance suite and the mask test use the stand-in (debug: the crate's unit tests in about 18 s). - The tiny network still runs through load and run on the processor in an ignored release test (about 4 s). The runner's device choice is a field, so tests can name the processor. - The prediction is read with `try_into_data`, so a device that fails while running is an error, not a guarded panic. - docs/BURN-ENGINE.md: the test split and that a software renderer counts as a card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ai-engine-burn): InSPyReNet moves to the processor when the card fails A card can answer the engine's probe and still fail on the network: on CI a software renderer (lavapipe) failed while reading the answer back. The first failure on a card that is not a cancel now builds the network again on the processor from the same file, runs that picture again there with a notice, and stays on the processor (`inspyrenet/fallback.rs`). A network loaded on the processor in the first place has nothing to move to. Tests with stand-ins: a working card is kept; a failing one moves once and stays; a cancel is not a failure; a rebuild that fails is the answer. Mutation group `be`: three new cases, all seven InSPyReNet cases caught. A real photo on the card still runs on the card. docs/BURN-ENGINE.md: the move, and the client run's picture is 2848 x 4272, not 3101 x 1613. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(ai-engine-burn): the move to the processor stays only after a rebuild that worked The rebuild is tried once. If it fails, its error is the answer and later pictures go to the card again, as `a_rebuild_that_fails_is_the_answer` already pins. The module docs and docs/BURN-ENGINE.md said the model stays on the processor without that condition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ai-engine-burn): retry a failed InSPyReNet load on the card on the processor The move to the processor covered failures while predicting, not a load on the card that fails. `load_or_processor` now retries such a load on the processor, unless the file itself is not the checkpoint (`Unsupported`), which is refused at once rather than read a second time. A network loaded on the processor in the first place is loaded once. Tests: a card that loads keeps the network; one that fails loads on the processor; a bad file is not read twice; on the processor there is one load. Mutation group `be`: two new cases, all nine InSPyReNet cases caught; every case in the checker has one target (645). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(ai-engine-burn): method references in the guided filter, one name for the fallback path Three SonarCloud findings on #112: two closures in `guided.rs` become method references, and the mutation cases name `inspyrenet/fallback.rs` once. No change in behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: imperator <imperator.maximus@gmail.com>
13 KiB
The Burn engine (SAM 2.1 and InSPyReNet, optional)
craft-ai-engine-burn runs SAM 2.1 tiny for image.segment through Burn, on a graphics card when
one answers and on the processor when none does. It is a choice beside rten, never the default:
rten reads the publisher's ONNX files as they are and stays first in the recipe.
The pieces, and which crate owns each:
| Piece | Where |
|---|---|
| Request handling (clicks, box, the picture kept between requests, empty-mask answer, size checks) | craft-ai-engine-rten (Loaded over a Backend), shared by both engines |
| The model run on Burn, weight loading, the panic guard | craft-ai-engine-burn |
| The compiled architecture (generated) | crates/ai-engine-burn/src/model/ |
| One-time local conversion of the published ONNX files to saved weights | craft-ai-burn-setup (docs/LOCAL-BURN-PREPARATION.md) |
| The catalogue route | crates/ai-features/src/catalogue/sam21.rs: a burn route after rten, same two published files |
| The cargo feature | craft-ai-client feature burn |
Nothing converted is hosted. The saved weights (encoder.bpk, decoder.bpk) are made on the
person's computer, once, and shared by every app. Given the downloaded encoder.onnx (with
decoder.onnx beside it), the runner prepares the saved copy itself: it calls the setup crate,
which checks the exact source bytes, then writes burn-prepared/ beside the model folder (never
inside it) and loads from there. Later loads only check hashes. The converter cannot be
interrupted and load has no Cancel handle, so the first load runs to the end (seconds on the
test PC, to be measured on a clean machine). Any other .onnx is refused in plain words. The
prepare cargo feature of craft-ai-engine-burn (on by default) bundles the converter; with it
off, a downloaded file is answered with "open Set up AI".
Regenerating the architecture
The generated code is never edited by hand. It comes from one script, so a reviewer can rerun it:
python tools/burn/generate_sam21.py <folder holding encoder.onnx and decoder.onnx>
python tools/burn/generate_sam21.py <folder> --check # exit 1 if the tree is not what it makes
The script checks the two files against the sizes and SHA-256 pinned in craft-ai-burn-setup,
runs the pinned burn-onnx (an example of that crate), applies five fixes to its output (each must
match exactly the stated number of times, or the script stops), drops the generated loaders that
panic on a bad file, splits by submodule so every file is under 700 lines, runs rustfmt and writes
src/model/. The five fixes, all found on SAM 2.1 tiny with burn-onnx 0.22.0:
- An integer scalar divided by an Int tensor was emitted as a float divide (once, encoder).
one_hotoutput rank was not inferred (once, decoder).- A zero-length integer
ConstantOfShape(Hiera's window padding is 0 when the size is a multiple of the window), which Burn'sexpandrefuses (five, encoder). - The same for the float one, the decoder's empty box placeholder (three, decoder).
Wherewith a condition that broadcasts: Burn'smask_wherewants it at full size (five, decoder).
They are worth reporting upstream to tracel-ai/burn-onnx.
The revision rule
REVISION (in the script) names the compiled architecture and the converter that made its weights.
It is part of the saved-weights cache key in crates/ai-burn-setup/src/sam21.rs. Change it in both
places, and regenerate, whenever any of these changes: the burn-onnx or Burn version, a patch in
the script, or the source ONNX files. Saved weights made for an older revision are then never reused.
crates/ai-engine-burn/tests/revision.rs fails if the script, the generated model/mod.rs and the
cache key disagree.
Exercising it
# Unit tests with a fake backend (no model, no graphics card needed)
cargo test -p craft-ai-engine-burn -j 8
# Prepare the weights once, then the live comparison with rten (release; skipped without the variables)
cargo run -p craft-ai-burn-setup --features conversion --example prepare_sam21 -- <onnx dir> <cache dir>
CRAFT_TEST_SAM21_ONNX_DIR=<onnx dir> CRAFT_TEST_SAM21_BURN_DIR=<folder printed above> \
CRAFT_TEST_SAM21_IMAGE=<512 square sample> \
cargo test --release -p craft-ai-engine-burn --test live -j 8 -- --ignored --nocapture
# Through the client, two apps
cargo test -p craft-ai-client --features burn --test burn_engine -j 8 -- --ignored
python tools/mutation/mutation_check.py be
Limits to know
- SAM 2.1 tiny only. A different architecture needs its own generated code and runner.
- Click and box prompts. A description stays on the ComfyUI route.
- The processor fallback uses Burn's
ndarraybackend, which Burn has marked deprecated in favour ofburn-flex. Swap it once flex is measured. It is built with Burn'srayonandsimdfeatures: the crate turns Burn's default features off, and without these two the processor runs on one core with scalar maths (3.7 times slower on the one network measured, see the pull request that turned them on). The SAM 2.1 timings below were taken before that change. CRAFT_AI_BURN_DEVICE=cpuforces the processor even when a graphics card answers. Run on the test PC (release, astronaut sample): one click and one box both gave IoU 1.0000 against rten (22.5 % selected by both). Timings on the processor: first run 8.70 s (rten 0.71 s), second click on the same picture 1.78 s (rten 0.030 s). Usable, but the card is the point of this engine.- A few generated
forwardfunctions are 500 to 699 lines because a function cannot be split without tracking every value that passes between the halves. No file is 700 lines or more. - Progress and Cancel during the first preparation:
loadhas neither, so the app shows its own busy state until the first selection returns.
InSPyReNet for image.matte
The second model on this engine: inspyrenet (engine burn), the subject's soft alpha for
Remove background, from the picture alone. It is not SAM: no clicks, no box, no prompt.
| Piece | Where |
|---|---|
| Runner, request and answer | src/inspyrenet/mod.rs: a PNG or JPEG source in, one grayscale PNG at the picture's size out (white is the subject, soft along hair and fur) |
| The network | src/inspyrenet/net/: InSPyReNet with the Swin-B backbone (T. Kim et al., ACCV 2022), written by hand in Burn after the MIT-licensed reference code (plemeri/InSPyReNet, as packaged by transparent-background). Module names follow the PyTorch keys, so the checkpoint loads by name |
| Weights | src/inspyrenet/weights.rs: the .safetensors checkpoint read with Burn's PyTorch adapter (linear weights transposed as they load); a load that leaves any weight unset, fails one, or finds a tensor this network does not have is refused in plain words |
| Any picture size | src/inspyrenet/pipeline/: full resolution up to 1280 pixels on the long side (reflect-padded to a multiple of 32); above that, reflect-padded to a square, reduced with antialiased Lanczos-3 to 1280², the matte enlarged back and, below half scale, refined at full resolution with a colour guided filter (He et al., PAMI 2013) on the tiles near the edge; then a 0.2 fringe shrink. The approach of the Gyre background remover |
Unlike SAM 2.1 here, nothing is generated and nothing is prepared on the computer: the runner reads the checkpoint file as downloaded.
The weights file
inspyrenet.safetensors from 1038lab/inspyrenet at
revision 0f5946638dcbc7dbc1e56306b823959958ac0ae0: 367,120,444 bytes, SHA-256
5c8ab4541f9a960b376a80362b07f1d96262e689c501365e6323faa958de47ea. It is a format conversion
with no model card of its own. On 29/10/2026 it was checked tensor for tensor (all 1,625 identical)
against the author's own checkpoint, ckpt_base.pth (InSPyReNet Swin-B Plus Ultra, MIT) at
plemeri/InSPyReNet revision
d94c2baaa4d023ab018c6f97be6ef37548e3bd1f: 367,520,613 bytes, SHA-256
0a6fe2a73ab0532d6d0b8d82849a9760a226df719e3063d09b4149ece6f80fcd, the file transparent-background
downloads. The author's file is not used directly because it is a pickle (.pth), and a local
connection takes only .safetensors, .onnx or .bpk weights.
In the catalogue
remove-background has one recipe, InSPyReNet (crates/ai-features/src/catalogue/inspyrenet.rs),
and it is built in only: LocalRecipe::comfyui is false, so it is never a ComfyUI setup option,
plan, install offer or connection, and engine advice does not offer ComfyUI for the tool. Its one
route is burn / inspyrenet with the pinned file above in the folder inspyrenet, so Set up AI
offers one 367 MB download, checked by size and SHA-256, and records that file on the connection.
A node pack and a workflow would be far more to set up for one file and one tool.
What was run
On one Windows machine (i5-12600KF, RTX 3090 Ti), release:
- Parity with PyTorch (
src/inspyrenet/parity.rs): every stage of the network (patch embedding, four Swin stages, the saliency and Laplacian pyramids, the prediction) against transparent-background's own code on a synthetic 384² input. Worst relative difference over the stages 3.2e-5 on the processor and 2.4e-4 on the graphics card; no pixel of the prediction changes side at 0.5. - The shared conformance suite (
check_runner) on the runner's request handling and pipeline around a stand-in for the network, as the SAM 2.1 tests here do. The network itself, a tiny one saved as.bpk, runs through load and run in an ignored test on the processor, in release (about 4 s). Unoptimised, Burn's processor backend takes minutes for it, and a build server's software renderer is too slow and, on Linux (lavapipe), fails. - The whole runner on a real photo, against the same port inside PhotoCraft (which runs it through the same pipeline): on a 3101 × 1613 PNG the two masks are identical, byte for byte. On a JPEG they differ by 0.003 on average (IoU 0.994), from the two JPEG decoders.
- Through the shared client (
crates/ai-client/tests/burn_matte.rs): the launcher sets up the catalogue's built-in route with the real file, then two app ids askAiClientfor Remove background only and each gets one grayscale mask at the picture's size (a 2848 × 4272 PNG). A file that is not the checkpoint is refused in words that point to Set up AI. - Not measured cleanly: the time per picture. During these runs another program held about 20 GB of the card's memory and the PhotoCraft port slowed down just as much (about 20 s a picture instead of about 2.5 s). No run on macOS or Linux, and no real app has mounted it yet.
# The PyTorch reference, once (Python with torch, safetensors and transparent-background)
python tools/burn/inspyrenet_reference.py <inspyrenet.safetensors> <inspyrenet-ref-384.safetensors>
# Then the parity and weight tests, release
CRAFT_TEST_INSPYRENET_WEIGHTS=<inspyrenet.safetensors> CRAFT_TEST_INSPYRENET_REFERENCE=<inspyrenet-ref-384.safetensors> cargo test --release -p craft-ai-engine-burn -j 8 --lib inspyrenet -- --include-ignored --nocapture
# One photo through the runner, the mask written out
CRAFT_TEST_INSPYRENET_WEIGHTS=<inspyrenet.safetensors> CRAFT_TEST_INSPYRENET_IMAGE=<photo> CRAFT_TEST_INSPYRENET_OUT=<mask.png> cargo test --release -p craft-ai-engine-burn -j 8 --lib removes_the_background -- --ignored --nocapture
# Two apps through the shared client, from the catalogue's route
CRAFT_TEST_INSPYRENET_WEIGHTS=<inspyrenet.safetensors> CRAFT_TEST_INSPYRENET_IMAGE=<photo.png> cargo test --release -p craft-ai-client --features burn --test burn_matte -- --ignored --nocapture
# The tiny network through load and run, on the processor
cargo test --release -p craft-ai-engine-burn --lib tiny_network -- --ignored
python tools/mutation/mutation_check.py be inspyrenet
python tools/mutation/mutation_check.py bo
Limits to know
- Graphics card advice. The route is not marked quick on the processor, so engine advice calls it slow on a computer without a card the engine can use.
- A software renderer counts as a card. The engine takes the graphics card when one answers,
and a software renderer (lavapipe, WARP) answers too. On CI the network was far slower there and
failed on lavapipe. So the first failure on a card that is not a cancel builds the network again
on the processor from the same file and runs that picture again there with a notice. Once that
rebuild has worked, the model stays on the processor. The move is tried once: if the rebuild
fails, its error is the answer and later pictures go to the card again. A load on the card that
fails is likewise retried on the processor, unless the file itself is not the checkpoint
(
src/inspyrenet/fallback.rs). SAM 2.1 on this engine makes the same choice of device without that move.CRAFT_AI_BURN_DEVICE=cpuforces the processor from the start. - The processor is slow. The fallback runs on Burn's
ndarraybackend, which this crate builds without itsmulti-threadsandsimdfeatures, so it uses one core. The same network on one core took about 230 s a picture in PhotoCraft (itsflexbackend), about 35 s with four threads and SIMD. The graphics card is the point of this runner. - The parity reference is made by
tools/burn/inspyrenet_reference.py, which needs Python with PyTorch and transparent-background.