add flux-klein trickle example with USD per hour pricing - #35
Conversation
runners.json advertises the new decimal price format, the env example drops the retired PRICE_PER_UNIT and PIXELS_PER_UNIT vars, the onchain overlay gains the maxFaceValue floor from the shared file, and the compose files move to the compose.yml naming. The SDK pin stays on rs/live-runner-session-payments, which now carries the pricing refactor (merged from ja/live-runner). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VtVeDdep8i9UrNKbm9yr8G
There was a problem hiding this comment.
Pull request overview
Adds a new flux-klein example app demonstrating realtime trickle video-to-video using FLUX.2-klein-4B, aligned with the repo’s static-runner pattern and USD/hour pricing format.
Changes:
- Introduces the
flux-kleinexample (runner, client, model wrapper, Docker/Compose, staticrunners.json, and usage docs/scripts). - Adds off-chain and on-chain compose configurations for running the orchestrator + app (and signer overlay on-chain).
- Updates the repo root README to include
flux-kleinin the supported schema list and examples table.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Lists flux-klein among trickle/static examples and updates the examples table. |
| flux-klein/view.sh | Adds a turnkey “webcam → runner → player” helper script. |
| flux-klein/runners.json | Adds static-runner config with price_info.price (USD/hour). |
| flux-klein/runner.py | Implements the aiohttp runner with warm-up gating, trickle channels, stats, and live /update. |
| flux-klein/README.md | Documents wiring, operation, parameters, and usage for off-chain/on-chain runs. |
| flux-klein/pyproject.toml | Defines host-side deps and pins livepeer-gateway to the required branch. |
| flux-klein/flux_klein.py | Provides the self-contained FLUX Klein feedback-loop inference implementation. |
| flux-klein/Dockerfile | Builds a CUDA image with torch + diffusers-from-git and the pinned gateway SDK. |
| flux-klein/compose.yml | Off-chain compose setup extending the shared orchestrator config with -liveRunnerConfig. |
| flux-klein/compose.onchain.yml | On-chain overlay adding signer + on-chain orchestrator flags and -maxFaceValue. |
| flux-klein/client.py | Adds a client that reserves sessions, streams media, and live-updates prompt/seed/blend. |
| flux-klein/.gitignore | Ignores local output TS and cached model weights directory. |
| flux-klein/.env.example | Adds on-chain env placeholders (keystores/accounts) and signer price cap. |
Comments suppressed due to low confidence (5)
flux-klein/runner.py:80
- Introduce a process-wide model lock so inference running in a worker thread (via
asyncio.to_thread) can’t race with/updateor session teardown mutatingMODELstate (prompt cache, noise cache, reset).
MODEL = FluxKleinModel() # single, container-wide pipeline (loaded once at warm-up)
_ORCHESTRATOR_URL = "http://localhost:8935"
flux-klein/runner.py:152
_close_session()sets the globalsessiontoNoneimmediately and then awaits cleanup. A new/streamrequest can start another session while the old one is still shutting down, andMODEL.reset()can race with an in-flightasyncio.to_thread(_transform, ...)call (task cancellation won’t stop the underlying thread). Holding_session_lockthrough cleanup and using_model_lockaroundMODEL.reset()avoids these races.
async def _close_session() -> None:
global session
if session is None:
return
current, session = session, None
flux-klein/runner.py:205
/streamparses the request body withjson.loads(...)but doesn’t handle malformed JSON. That will currently raise and return a 500 instead of a 400. Also, since inference runs in a background thread, updates toMODELparameters should be synchronized with_model_lockto avoid racing with in-flight inference.
body = json.loads(await request.read() or "{}")
prompt = str(body.get("prompt", DEFAULT_PROMPT))
MODEL.update_prompt(prompt) # model/res fixed per container; this is one assignment
if "seed" in body:
MODEL.update_seed(int(body["seed"])) # -1 random; fixed = steadier output
flux-klein/runner.py:400
/updatehas the same malformed-JSON issue as/stream(500 instead of 400). Additionally, it can run concurrently with the inference thread, so model updates and reads ofMODEL.seed/MODEL.input_blendshould be done under_model_lockto avoid races and inconsistent responses.
body = json.loads(await request.read() or "{}")
prompt = str(body.get("prompt", session.prompt))
MODEL.update_prompt(prompt)
session.prompt = prompt
if "seed" in body:
flux-klein/runner.py:253
- Inference is executed in a worker thread, but
MODEL.process_batch(...)is called without synchronization. Since/updateand session teardown can mutate/resetMODELconcurrently, protect the inference call with_model_lockto avoid races (task cancellation won’t stop the underlying thread).
rgbs = [f.to_ndarray(format="rgb24") for f in frames]
outs = []
for src, out_rgb in zip(frames, MODEL.process_batch(rgbs)):
out = av.VideoFrame.from_ndarray(out_rgb, format="rgb24")
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| docker compose -f compose.yml -f compose.onchain.yml down | ||
| ``` | ||
|
|
||
| Keep `PIXELS_PER_UNIT` small — if it's too large the per-unit price floors to 0 wei and calls are effectively free despite being on-chain. |
| import json | ||
| import logging | ||
| import os | ||
| import time |
|
Tested on-chain on Arbitrum mainnet (RTX 3090): session reserved and paid through the remote signer (tickets signed, orchestrator credited balance=2e12, clean release + payment processor shutdown), FLUX.2-klein-4B generated frames back through the trickle output. Full logs in the session referenced below. Two notes:
🤖 Generated with Claude Code |
What
Carries #32 (flux-klein trickle example by @zargarzadehm, commit preserved) forward with the decimal USD per hour pricing scheme and the repo's current conventions, so it lands consistent with #34.
On top of the original example:
runners.json:price_infois now{ "price": 0.01 }(decimal USD per hour) instead ofprice_per_unit/pixels_per_unit..env.example: drops the retiredPRICE_PER_UNIT/PIXELS_PER_UNITvars; the price lives inrunners.json(static runner),MAX_PRICE_PER_UNITstays as the signer cap.compose.onchain.yml: adds the-maxFaceValue=4e15floor from the shared file (a faceValue below the redemption tx cost makes the orchestrator reject every session with "insufficient sender reserve"; see refactor(examples): move to USD per hour pricing #34).docker-compose*.ymlrenamed tocompose*.ymlto match refactor(examples): move to USD per hour pricing #34's convention, with all README and header references updated.SDK note
The example keeps its
rs/live-runner-session-paymentspin: a long-lived streaming session needssession.start_payments(), whichja/live-runnerdoes not have. That branch now carries the pricing refactor (merged fromja/live-runnerat 89ac0f7, all SDK tests passing), so the pin and the new price format work together. Client imports verified against the merged branch.Not yet done
Runtime not re-tested here (needs the CUDA image build and a webcam/video input); the original functionality is untouched apart from the pricing/naming alignment.
Supersedes #32.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VtVeDdep8i9UrNKbm9yr8G