Skip to main content
One of the biggest strengths of building with Reactor Runtime is that your model becomes an API server. But not just any API server: one that runs in real time, streaming video while accepting commands. Reactor handles all the networking for you. Messages from users are forwarded to your model through clean, easy-to-use decorators. You define which inputs your model accepts, and Reactor routes them to the right methods. Your ML code stays completely separate from networking code.

What Commands Are For

Commands are for real-time interaction while your model is generating. Think of them as the controls a user has while watching and interacting with the video stream:
  • Keyboard inputs (WASD for movement, spacebar for actions)
  • Changing the prompt mid-generation
  • Camera controls (pan, tilt, zoom)
  • Adjusting parameters like speed or intensity
  • Triggering actions (attack, jump, reset)
Commands are not for switching configurations between runs. Configuration (like model paths, resolution, or number of inference steps) belongs in your YAML config file and is loaded once at startup. Commands are what users send while the session is active.

The @command Decorator

To accept user input, you decorate methods on your model with @command. Each decorated method becomes an endpoint that users (or frontends) can call while your model is generating.
from reactor_runtime import VideoModel, command

class MyVideoModel(VideoModel):
    def __init__(self, config):
        self.current_prompt = "a sunny day"
        # ... load your model ...

    @command("set_prompt", description="Change the text prompt in real time")
    def set_prompt(self, prompt: str):
        self.current_prompt = prompt

    def start_session(self):
        # Your generation loop reads self.current_prompt every frame
        ...
The @command decorator takes two arguments:
  • name (required): The command name that clients use to call this method
  • description (optional): A human-readable description of what the command does
When a user sends a message like {"type": "set_prompt", "data": {"prompt": "a rainy night"}}, Reactor automatically routes it to your set_prompt method, and your next generated frames will reflect the new prompt.

Typing Your Parameters

Reactor uses your function’s type annotations to automatically generate a schema for each command. This schema is used for validation and can be read by frontends to auto-generate UI controls. Supported types include str, int, float, bool, and Literal for enumerated values.

Using Literal for Fixed Options

When a parameter should only accept specific values, use Literal:
from typing import Literal

@command("move", description="Set movement direction")
def move(self, direction: Literal["forward", "back", "left", "right", "none"]):
    self.movement = direction
This tells Reactor (and any frontend) that direction must be one of those five strings. Frontends can render this as a dropdown or button group.

Using Pydantic Field for Rich Metadata

For more control over validation and UI hints, use Pydantic’s Field:
from pydantic import Field

@command("set_speed", description="Adjust movement speed")
def set_speed(self, speed: float = Field(..., ge=0.0, le=1.0, description="Speed multiplier")):
    self.speed = speed
The Field lets you specify:
  • ge/le: Minimum and maximum values (great for sliders)
  • description: What this parameter does
  • ...: Makes the parameter required (use a default value to make it optional)

Combining Everything

Here is a complete example with multiple parameter types:
from typing import Literal
from pydantic import Field
from reactor_runtime import VideoModel, command

class MyVideoModel(VideoModel):
    @command("set_camera", description="Control camera movement")
    def set_camera(
        self,
        direction: Literal["up", "down", "left", "right", "none"] = Field(
            ..., description="Camera movement direction"
        ),
        speed: float = Field(0.5, ge=0.0, le=1.0, description="Movement speed"),
    ):
        self.camera_direction = direction
        self.camera_speed = speed

Automatic UI Generation

When you properly type your commands, Reactor can expose a schema that frontends read to automatically generate controls.
EDITOR: Add reference to the ReactorController component in the frontend SDK docs, which auto-generates UI controls based on your model’s command schema.
This means you can test your model interactively without writing any frontend code. The schema includes command names, descriptions, parameter types, and constraints, so a generic controller can render appropriate inputs (text fields, sliders, dropdowns) for each command. As a bonus, the headless runtime (reactor run --runtime headless) also discovers your commands from the @command decorators, letting you test them from the command line without any frontend.

Thread Safety: Commands Run in Parallel

Here is something important to understand: your command methods run in a different thread than your start_session loop. When a user sends a command, Reactor calls your decorated method immediately, while your generation loop continues running in its own thread. The safest pattern is to store user input in instance variables, and have your pipeline read from those variables each frame:
from reactor_runtime import VideoModel, command, get_ctx

class MyVideoModel(VideoModel):
    def __init__(self, config):
        self._movement = "none"
        self.pipeline = load_pipeline()

    @command("move", description="Control movement direction")
    def move(self, direction: str):
        self._movement = direction

    def start_session(self):
        while True:  # We will learn about cooperative stopping in the next section
            frame = self.pipeline.generate(movement=self._movement)
            get_ctx().emit_block(frame)
This works because Python’s GIL makes simple attribute reads/writes atomic. The pipeline reads the current value each iteration, and commands can update it at any time without blocking. If you need to update multiple related values atomically (like x and y coordinates together), use a threading.Lock.
If your pipeline is a separate class, consider giving it setters that your commands call directly, rather than passing every user input through the generate() signature. This keeps your code cleaner as the number of inputs grows. See Writing a Pipeline for architectural patterns.

Now that you know how to accept user input, the next step is understanding how to structure your pipeline for continuous, real-time generation.

Writing a Pipeline

Learn how to structure your model for continuous real-time generation.