run() is where your model’s logic lives. The runtime calls it once after load() and it runs for
the lifetime of the model. What happens inside is entirely yours.
Basic pattern
run() is required. A model whose whole job lives in @event handlers still needs one, and parks
instead of generating:
The connection signal
self.connected is an asyncio.Event the runtime owns. It is set while at least one client is
connected and cleared once the last one leaves.
@connected handler runs and cleared
before the @disconnected handler for the last client to leave. For a model serving one client at a
time, that is simply the moment they arrive and the moment they go.
Emitting frames
emit() takes an instance of your Output class, with one payload per declared track:
uint8 array shaped
(H, W, 3); an audio payload is int16 shaped (1, samples).
emit() waits while the frames it already handed downstream are still playing, so a model that
generates faster than its playout rate is throttled to that rate instead of piling up latency. The
wait happens off the model’s event loop, so commands and lifecycle hooks keep dispatching while it
holds. Rate limiting is the runtime’s job.
A producer that would rather skip a frame than wait — anything driven by a live source, where the
newest frame is the only one worth sending — passes drop=True, and the overflow is discarded
downstream:
Batches
Models that produce several frames per forward pass can emit them in one call. Pass a(N, H, W, 3)
array and the runtime splits it into individual frames downstream:
Frame rate
Each emitted chunk carries the rate its frames should play out at. There are two ways to set it. Declare a fixed rate with thefps class attribute when your model produces at a predictable
speed:
n_frames / compute_time, so the stream tracks your real throughput. A
model that speeds up after a warm-up, or slows down under a heavier setting, stays in sync without
you touching fps. When you pass compute_time, fps is ignored.
Controlling playout
self.output is the model’s handle onto its outbound stream. It is bound for you, and every
operation on it fans out to each connected client — including clients that connect later.
fps re-paces frames that are already queued rather than waiting for the next emit, which
is what makes it usable from a command handler:
compute_time supersedes it, so it is a durable
setting on a model that declares fps and a one-chunk nudge on a model that measures every pass.
Call flush() when generation resets or restarts — a new scene, a cleared prompt, a seek. It drops
everything queued, releases a producer waiting in emit(), and cuts the client to black, so none of
the old content plays after the reset:
Buffered latency
buffer_size declares how many frames may sit between your model and each client. It is the
latency bound: a smaller value means a command takes effect on screen sooner, and a larger one
absorbs more variance in your generation speed.
Sending messages
Send structured data to clients from insiderun():
Concurrency
@event handlers run on the same event loop as run(), so a handler can fire at any await in
your loop. If you read an attribute, await something, and read it again, it may have changed in
between.
Snapshot what a forward pass depends on before you start it:
Next
Sessions & Clients
Session hooks, multiple clients, and what resets when.
Managing State
One event loop, shared by your loop and your handlers.