Skip to main content
ReactorModel gives you full control over how client-facing parameters are stored and updated. You keep them as instance attributes and write explicit @event handlers for each one.

Declaring parameters

Store client-facing parameters as instance attributes. Initialize them in your @connected handler so each client starts with a clean slate:
@connected
async def on_connect(self):
    self.prompt = "a sunny meadow"
    self.brightness = 1.0
    self._step = 0
Read them directly in run():
async def run(self):
    while True:
        await self.connected.wait()
        while self.connected.is_set():
            # ✅ Read instance attributes directly
            frame = self.pipe.forward(
                prompt=self.prompt,
                brightness=self.brightness,
            )
            await self.emit(MyOutput(main_video=frame))

Explicit event handlers

Every parameter the client can change needs an explicit @event handler.
from reactor_runtime.interface import event, InputField

@event(name="set_prompt", description="Change the scene prompt")
def set_prompt(self, prompt: str = InputField(default="a sunny meadow")):
    self.prompt = prompt

@event(name="set_brightness", description="Adjust brightness")
def set_brightness(self, brightness: float = InputField(default=1.0, ge=0.0, le=2.0)):
    self.brightness = brightness
The client sends {"type": "set_prompt", "data": {"prompt": "a dark forest"}} and the runtime calls your method.

Validation with InputField

InputField defines constraints that the runtime enforces automatically. Invalid values from clients are silently rejected.
# Numeric range (min/max inclusive)
brightness: float = InputField(default=1.0, ge=0.0, le=1.0)

# String / sequence length
prompt: str = InputField(default="hello", min_length=1, max_length=500)

# Exhaustive allowed values
style: str = InputField(default="none", choices=["none", "oil_paint", "sketch"])

# Description (shown in schema)
seed: int = InputField(default=42, description="Random seed for generation")
These work on @event handler parameters, and the runtime enforces them before your handler is called.

Resetting between clients

If you need to reset your model’s state between clients, use the connection hooks:
@connected
async def on_connect(self):
    self.prompt = "a sunny meadow"
    self.brightness = 1.0

@disconnected
async def on_disconnect(self):
    # flush the model's output so it doesn't carry over
    self.output_buffer.flush() 
If you skip initialization in @connected, attributes may carry over from the previous session.

Scheduling

run() and @event handlers share the same event loop, so handlers can run at any await in your loop. If you need consistent values across a forward pass, snapshot them:
async def run(self):
    while True:
        await self.connected.wait()
        while self.connected.is_set():
            # ✅ Snapshot — handlers can fire at any await below
            prompt = self.prompt
            brightness = self.brightness
            frame = self.pipe.forward(prompt=prompt)
            frame = (frame * brightness).clip(0, 255).astype("uint8")
            await self.emit(MyOutput(main_video=frame))

Derived state

For expensive derived values (embeddings, cached tensors), compute them in the event handler and store them alongside the raw value:
@event(name="set_prompt", description="Scene prompt with encoding")
def set_prompt(self, prompt: str = InputField(default="")):
    self.prompt = prompt
    # ✅ Cache the expensive encoding so run() doesn't redo it each frame
    self._embedding = self.encoder.encode(prompt)
Then read self._embedding in run() instead of re-encoding every frame.

Next

Events & Messages

Custom events, lifecycle hooks, and outbound messages.

Video Input

Read webcam frames with manual buffer management.