← Back to portfolio Français

Technocompost

Architecture overview — project: cbicari/technocompost

Purpose

Technocompost is a self-sustaining, multi-agent image processing installation running on a cluster of Raspberry Pi 4 boards. Each Pi runs an OpenClaw AI agent connected to a shared local LM Studio inference server. The agents pass an image through a continuous decay-and-regrowth cycle, each Pi handling one stage and deciding for itself which transformation script to apply, informed by its own system sensors (CPU temperature, load, memory, disk, network).

The repository contains two different orchestration modes built around the same shared infrastructure:

Both modes share the same physical setup, the same Syncthing-synced folder, and the same live display subsystem (display_server.py / preview.py).

Shared infrastructure

ComponentRole
3× Raspberry Pi 4 (rpiagent01/02/03)One per pipeline stage
LM Studio serverCentral inference backend (e.g. google/gemma-4-e4b, nvidia/nemotron-3-nano-4b), reachable at a fixed LAN address, used by every Pi's OpenClaw agent
OpenClawAI agent runtime installed on each Pi; can also talk peer-to-peer over its own ACP protocol when bound to lan mode
SyncthingKeeps ~/.openclaw/workspace/tmp/ (and its canvas/ subfolder) identical across all Pis
display_server.pyA pygame fullscreen viewer (on one designated "display Pi") that receives frames over TCP and cross-fades between them
preview.pyTiny client used by processing scripts to stream a frame to display_server.py via flush()

install.sh sets up a fresh Pi: system packages (git, curl, build-essential, feh, python3-pillow, inotify-tools, btop), Syncthing, Node.js 22 LTS + npm, and OpenClaw, then symlinks the scripts/ folder into ~/.openclaw/workspace/scripts. The openclaw setup wizard (LM Studio base URL + model choice) and the optional openclaw.service systemd unit are configured manually afterwards, per the README.

Technocompost shared infrastructure A cluster of three Raspberry Pi agents share a Syncthing-synced folder, all served by one central LM Studio inference server, with one Pi driving a fullscreen pygame display. LM Studio server Shared inference backend Raspberry Pi cluster Syncthing-synced shared folder rpiagent01 OpenClaw agent Stage 1 rpiagent02 OpenClaw agent Stage 2 rpiagent03 OpenClaw agent Stage 3 Display Pi Fullscreen pygame window
Shared infrastructure common to both orchestration modes

Mode A — distributed agentic pipeline

Flow

hummus.png → [rpiagent01] → gas.png → [rpiagent02] → liquid.png → [rpiagent03] → hummus.png → ...
Technocompost mode A: distributed agentic pipeline Each Pi's watcher script detects the input file for its stage, decide.sh builds a sensor-aware prompt, the local OpenClaw agent picks a script and parameters, the script runs and writes an output file, and Syncthing carries that file to the next Pi, closing the loop. watcher.sh Polls for this stage's keyword file decide.sh Builds a sensor-aware prompt OpenClaw agent Picks a script and its parameters Run chosen script Writes this stage's output file Syncthing sync Delivers the file to the next Pi ↻ next Pi's watcher.sh picks up the file, restarting the cycle
Mode A — one full stage cycle, repeated by each Pi

Steps

  1. watcher.sh <stage> polls the shared folder every 3 seconds for that stage's keyword file (hummus/gas/liquid), tracking already-seen files in /tmp/watcher_seen_stage<N>.txt. On first run it also starts display_server.py if it isn't already running.
  2. On a new file, it calls decide.sh <stage> <input_file>, which:
  3. The output file (gas.png, liquid.png, or hummus.png) is written back into the shared folder, where Syncthing propagates it to every Pi; the next stage's watcher.sh picks it up, closing the loop.

Stage → script pools

PiStageWatches forProducesScript pool
rpiagent01 ("Fragmenteur")1hummus.pnggas.pngchunk_shuffle, conway, diffuse, erode, spore, degrade
rpiagent02 ("Hydrolyseur")2gas.pngliquid.pnghydrolyse, decompose, diffuse, erode
rpiagent03 ("Lignivore")3liquid.pnghummus.pnglignine, spore, decompose

(The README additionally lists mushroom, wind, and growth as part of the broader script reference, used similarly by the pipeline.)

Mode B — concurrent tile-based canvas

This mode replaces watcher.sh + decide.sh entirely with tile_worker.py, run once per Pi/stage, and does not require a language-model call per operation — script choice is driven by local weighted random selection and a sensor-derived "stress level" instead.

Technocompost mode B: concurrent tile-based canvas Every Pi runs the same loop on a shared grid of image tiles: claim a free tile, check local sensor stress and pick a persona script and preset, run it and blend the result back at low alpha, release the claim, then composite and stream the full canvas to the display. tile_init.py Splits the seed image into tiles Claim a free tile Exclusive .claim file, 30s TTL Pick persona script Weighted by CPU stress level Run and blend Alpha-blends result back into tile Composite and flush Sends full canvas to display server ↻ release the tile, then claim another and repeat
Mode B — one worker cycle, run concurrently by every Pi

Setup

tile_init.py <seed.png> [--rows N] [--cols N] [--reset] crops a seed image into an R×C grid (default 4×4) and writes each piece as canvas/tile_<row>_<col>.png. --reset lets the grid be reseeded from a new image without disturbing files that already exist otherwise.

Worker loop (per Pi, per stage)

Each tile_worker.py --stage <1|2|3> instance repeats, indefinitely:

  1. Sweep stale claims — any .claim file older than 30 seconds is removed, in case a worker died mid-tile.
  2. Claim a free tile — tiles and coordinates are shuffled, and the first tile with no tile_RR_CC.png missing and no existing .claim file is claimed atomically (an exclusive file create, so two Pis can't grab the same tile).
  3. Read local sensor stress — CPU temperature and 1-minute load average are mapped to a 0/1/2 "cool / warm / hot" level.
  4. Pick a persona script — from that stage's weighted pool (the script used last time has its odds roughly halved, to encourage variety) and its stress-matched parameter preset.
  5. Run the script on the tile into a scratch file, then alpha-blend the result back onto the tile (--alpha, default 0.30) so changes accumulate gradually rather than replacing the tile outright.
  6. Release the claim, recomposite the full canvas from all current tiles, and stream it to display_server.py via preview.flush().
  7. Pause briefly (--pause, default 0.3 s) and repeat from step 1.

Stage → persona script pools

StagePersonaScript pool
1insectsfault_shift, chunk_hoard, repetition_smash, chunk_shuffle, byte_scramble, metadata_wipe
2bacteriagas_liquid, xor_cascade, bit_rotate, hydrolyse, memory_purge, activity_log
3fungibutterfly_permute, hilbert_shatter, cat_map_scramble, lignin_bore, lignine, mycelium_spread

Each persona script lives under a stage-named subfolder (scripts/insects/, scripts/bacteria/, scripts/fungi/) and accepts a "cool / warm / hot" parameter preset selected by tile_worker.py according to the current sensor stress level.

Shared display subsystem

Typical usage

Mode A

./watcher.sh 1   # on rpiagent01
./watcher.sh 2   # on rpiagent02
./watcher.sh 3   # on rpiagent03

Drop an image named hummus.png into ~/.openclaw/workspace/tmp/ on any Pi to start the loop.

Mode B

python3 scripts/tile_init.py seed.png --rows 4 --cols 4
python3 scripts/tile_worker.py --stage 1 --display   # on the Pi with a screen
python3 scripts/tile_worker.py --stage 2             # on the other Pis
python3 scripts/tile_worker.py --stage 3

Displaying a single image directly

DISPLAY=:0 feh --fullscreen /path/to/image.png &
killall feh   # close