Tenzro Media Gen.
tenzro-media-gen) plus a Python reference worker over HuggingFace diffusers.- STATUS
- Testnet
- CRATE
- tenzro-media-gen
- KINDS
- 4
- REFERENCE
- diffusers
Architecture
A requester posts a job with a price ceiling; a worker claims it, renders it, publishes the output, and signs a receipt over what it produced. The same identity, staking bond, reputation, and settlement asset that underwrite inference and training underwrite this too — a media worker is a provider registration with a different capability record.
Two layers. The Rust crate owns the job queue, the worker registry, the pricing function, the payment split, the three signing preimages, the output-store trait, persistence, and the gossip envelope. No tensor library enters the Rust workspace. The Python worker owns the denoising loop and nothing else: pipeline construction, scheduler manipulation, VAE decode, video muxing. It never decides what a job is worth, who else is working on it, or whether its own receipt is acceptable.
Job kinds
Four kinds. The image-conditioned kinds bind the conditioning image's hash into the job id, so the job commits to the exact bytes it was conditioned on. Video kinds carry a frame count and fps; image kinds reject both.
text2image prompt → image
image2image prompt + conditioning image → image
text2video prompt → video
image2video prompt + conditioning image → videoParameters are prompt, optional negative prompt, width, height, steps, guidance scale, optional seed, optional frames and fps, optional conditioning-image hash, and an opaque metadata map. Admission bounds: 8192 px per dimension, 500 steps, 3600 frames, 8192 prompt bytes.
Catalog
Workers read the catalog at enrollment. Each row names the HuggingFace repo, the diffusers pipeline class, the kinds it serves, default and maximum resolutions, default steps and guidance scale, frames and fps for video, a VRAM floor, and — for split models — the expert pair.
qwen-image Qwen/Qwen-Image t2i 1328² 50 steps 48 GB
qwen-image-flash nvidia/Qwen-Image-Flash t2i 1024² 4 steps 48 GB
qwen-image-edit Qwen/Qwen-Image-Edit-2511 i2i 1328² 40 steps 48 GB
z-image-turbo Tongyi-MAI/Z-Image-Turbo t2i 1024² 9 steps 16 GB
flux2-klein-4b black-forest-labs/FLUX.2-klein-4B t2i, i2i 1024² 4 steps 12 GB
wan2.2-t2v-a14b Wan-AI/Wan2.2-T2V-A14B-Diffusers t2v 1280×720 40 steps 80 GB [split: 48 GB each]
wan2.2-i2v-a14b Wan-AI/Wan2.2-I2V-A14B-Diffusers i2v 1280×720 40 steps 80 GB [split: 48 GB each]
wan2.2-ti2v-5b Wan-AI/Wan2.2-TI2V-5B-Diffusers t2v, i2v 1280×704 50 steps 24 GBA row serving an image-conditioned kind resolves to a sibling pipeline class where the family provides one — WanPipeline becomes WanImageToVideoPipeline for an image2video job — while a class that already covers image input keeps it.
qwen-image-flash is qwen-image distilled onto a four-step trajectory with guidance disabled: the same transformer, the same VRAM floor, one twelfth of the pixel-steps and so one twelfth of the quote. It is also the one row outside the permissive tier — the NVIDIA Open Model License puts it in the custom-license tier, so a worker declaring it must enroll on a node started with --accept-license nvidia-open-model. Enrollment rejects a capability naming a model the operator has not accepted, or one absent from the catalog; that is the enforcement point because the node never loads media-gen weights, the Python worker does.
Split-expert rendering
Two distinct model shapes are called mixture-of-experts in the generative-media literature, and only one of them is a distribution primitive.
Token-routed MoE has a learned router selecting experts per token inside every forward pass. Splitting it across machines means a round trip per layer per token. That is the shape the language-model dispatch planner addresses; it is not what this catalog carries.
Timestep-boundary expert pairs are two transformers trained for different noise regimes — one for the high-noise prefix of the schedule, one for the low-noise remainder. There is no learned router: a fixed noise threshold decides which expert owns a step. Exactly one intermediate latent crosses between the two halves, once per job. One expert needs 48 GB where the whole model needs 80, so two commodity accelerators render what one could not, and the coordination cost is one blob transfer rather than one per layer.
The boundary is a noise level, not a step index. A step belongs to the high-noise expert while:
t >= boundary_ratio × scheduler.config.num_train_timestepsTimesteps descend through the schedule, so that set is always a prefix and one integer index splits it. boundary_ratio is a fraction of the scheduler's training timestep count — 0.875 of 1000 for Wan 2.2 A14B. A 40-step job and a 100-step job therefore split at the same noise level and at different indices, which is why the protocol records the completed step count from the worker rather than assuming a fixed fraction.
Both transformer slots are optional in the pipeline, and every internal read falls back to the other slot when one is unset. A high-noise holder loads its expert into transformer and leaves transformer_2 unset; a low-noise holder does the reverse and resumes the schedule at the boundary index. A worker with the VRAM for both halves claims each half separately anyway — the protocol makes no exception for co-location, which keeps the signed step counts and the payment split identical whether the two halves run on one machine or two.
Pricing and the payment split
The work unit is the pixel-step: width × height × steps × frames, with frames defaulting to 1 for image kinds. A quote is a base fee plus a per-pixel-step rate. A job whose ceiling falls below the quote is rejected at admission rather than claimed and abandoned.
A non-split job pays the single worker the full 10,000 basis points. A split job pays proportionally to the schedule each half actually rendered:
high_bps = steps_completed × 10_000 / total_steps
low_bps = 10_000 − high_bpsThe completed step count comes from the signed handoff, not from either worker's later claim. Overstating a half would take a forged signature over the handoff preimage.
Commitments
Three SHA-256 preimages under three distinct domain tags. Distinct tags keep a handoff signature from being replayed as a receipt signature.
tenzro/media-gen/job-id requester DID + address, model, kind, every parameter,
price ceiling, creation timestamp
tenzro/media-gen/handoff job id, handing-off worker DID + address, latent hash,
latent byte length, steps completed, handoff timestamp
tenzro/media-gen/receipt job id, executed task spec, worker DID + address,
output hash, MIME, byte length, seed, generation time,
price paid, completion timestampEncoding: integers big-endian at their declared width, timestamps as two's-complement i64 milliseconds, floats as the IEEE-754 big-endian bit pattern, variable-length fields prefixed with a big-endian u32 byte count, optionals as a presence byte then the value. Raw 32-byte hashes embed bare; addresses are length-prefixed. The metadata map is excluded from every preimage — a map has no canonical ordering across encoders, so binding it would make the digest encoder-dependent.
The job id is the digest of its own contents, so a spec carrying someone else's id still hashes to what it actually says. The Rust and Python implementations recompute the same preimages against the same fixture values, so a change on either side shows up as a digest change on one side only.
Payload store
Three payload kinds share one content-addressed store: the rendered output, the intermediate latent on a split job, and the requester's conditioning image. All three are addressed by tenzro://blob/, fetched over the node's own endpoint, and verified on read.
The canonical Tenzro hash is SHA-256, and that is what the commitments bind. The blob store indexes by BLAKE3. Publishing therefore returns both: the SHA-256 for the commitment, the BLAKE3 locator for the fetch. A worker publishing a latent records the SHA-256 in the handoff it signs; its partner fetches by locator and verifies the SHA-256 before resuming, so the transport's own verification and the protocol's hash check are independent.
Lifecycle
postJob → claimJob → markRunning → render
├─ recordHandoff (high-noise half of a split job)
└─ submitReceipt (whole job, or low-noise half)A failed job is terminal and does not requeue. A worker waiting on a split partner waits a bounded interval and then fails the job explicitly rather than abandoning it. Cancellation is the requester's path, valid until a worker has claimed.
Job status is pending, claimed, running, completed, failed, or cancelled. Expert role is high_noise or low_noise.
CLI
tenzro media-gen catalog # [split] marks expert pairs
tenzro media-gen quote --kind text2image --params ./params.json
tenzro media-gen post-job --spec ./spec.json
tenzro media-gen get-job --job-id <id> # assignments, roles, unclaimed halves
tenzro media-gen get-receipt --job-id <id>
tenzro media-gen fetch-output --job-id <id> --out ./render.png
tenzro media-gen enroll-worker --capability ./capability.json
tenzro media-gen claim-job --job-id <id> --worker-did <did> --role high_noise
tenzro media-gen publish-output --file ./render.png
tenzro media-gen record-handoff --handoff ./handoff.json
tenzro media-gen submit-receipt --receipt ./receipt.json
tenzro media-gen fetch-latent --job-id <id> --out ./latent.safetensorsEighteen JSON-RPC methods under tenzro_mediaGen_ back these, grouped as discovery (catalog, quote, workers), requester (post, list, get, cancel, receipt, fetch output, fetch input), and worker (enroll, claim, mark running, fail, publish, handoff, receipt, fetch latent). The same surface is reachable through the MCP server and the A2A media-gen skill. Job, worker, and receipt events broadcast on the tenzro/media-gen gossip topic.
Running a worker
The Python worker has its own CLI covering both sides. The requester surface — wire types, commitment preimages, JSON-RPC client — installs without torch; the render extra adds torch, diffusers, transformers, accelerate.
tenzro-media-gen keygen
export TENZRO_MEDIA_GEN_SEED=<seed_hex>
tenzro-media-gen serve --worker-did <did> --worker-address <64-hex> \
--model qwen-image --model z-image-turbo \
--max-resolution 1328 --gpu-vram-gb 48
# one half of a split model, 48 GB where the whole model needs 80
tenzro-media-gen serve --worker-did <did> --worker-address <64-hex> \
--expert wan2.2-t2v-a14b:high_noise \
--max-resolution 1280 --max-frames 81 --gpu-vram-gb 48serve enrolls, then polls for pending jobs it qualifies for and renders them until interrupted. The Rust CLI subcommands above are the inspection and manual-recovery path; serve drives the same methods for an operator.