Skip to main content
Your model’s generation speed is unpredictable. It runs on hardware with varying load, thermal throttling, and resource contention. On top of this, models generate in blocks: multiple frames become available all at once. If you displayed all frames from a block immediately, users would only see the last one, as the others flash by in a single render cycle. Reactor solves this. When you call emit_block(), Reactor buffers your frames and delivers them smoothly to the client, spacing them evenly based on your model’s actual generation speed. You do not need to worry about timing or frame pacing. Just generate frames and emit them.
EDITOR: Add diagram showing blocks being emitted all at once and then reactor evenly spacing them out.

Using emit_block()

Call get_ctx().emit_block() with your frame tensor:
from reactor_runtime import get_ctx

def start_session(self):
    while not get_ctx().should_stop():
        frames = self.generate_block()
        get_ctx().emit_block(frames)

Required Format

Your frames must be a NumPy array with the following specification:
PropertyRequirement
Shape(N, H, W, 3) for N frames, or (H, W, 3) for a single frame
Dtypenp.uint8
Range0 to 255
ChannelsRGB (not BGR)
Where N is the number of frames in the block, H is height, W is width, and 3 is the RGB channel dimension.

Converting from PyTorch

Most pipelines output tensors in (B, C, T, H, W) or (B, T, C, H, W) format with float values in [-1, 1] or [0, 1]. Here is how to convert:
# From (B, C, T, H, W) float tensor in [-1, 1] range
video = self.generate()  # Shape: (1, 3, 12, 704, 1280), dtype: float16

# Remove batch, reorder to (T, H, W, C)
frames = video.squeeze(0).permute(1, 2, 3, 0)  # (12, 704, 1280, 3)

# Convert from [-1, 1] to [0, 255] uint8
frames = ((frames + 1.0) * 127.5).clamp(0, 255).to(torch.uint8)

# Move to CPU and convert to NumPy
frames_np = frames.cpu().numpy()

get_ctx().emit_block(frames_np)
For tensors already in [0, 1] range:
frames_np = (frames * 255.0).clamp(0, 255).to(torch.uint8).cpu().numpy()

Special Cases

Emitting Black Frames

Pass None to emit a black frame:
get_ctx().emit_block(None)
This is useful for signaling “no content yet” at session start or during resets.

Stopping Emission

If you stop calling emit_block(), Reactor holds the last emitted frame on screen. This maintains visual continuity if your model temporarily pauses, though at the cost of a frozen image.

Frame Monitoring

Reactor tracks your emission rate to adapt playback speed. If your pipeline stops emitting frames (for example, while waiting for user input), Reactor interprets this as slow performance and lowers the target framerate. To prevent this, use disable_monitoring() when you intentionally pause generation, and enable_monitoring() when you resume:
def start_session(self):
    while not get_ctx().should_stop():
        # Wait for user to provide initial input
        while not self.has_input():
            if get_ctx().should_stop():
                return
            get_ctx().disable_monitoring()  # Tell Reactor we are intentionally paused
            time.sleep(0.1)
        
        get_ctx().enable_monitoring()  # Resume tracking
        
        # Generate and emit frames
        frames = self.generate()
        get_ctx().emit_block(frames)
This pattern is common in interactive models that wait for user actions before proceeding. Without disable_monitoring(), Reactor would think your model is struggling and degrade the stream quality.

Sending Messages

Beyond video frames, you can send JSON messages to the frontend using get_ctx().send(). This is useful for progress updates, state synchronization, or custom events:
def start_session(self):
    frame_count = 0
    while not get_ctx().should_stop():
        frames = self.generate_block()
        get_ctx().emit_block(frames)
        
        frame_count += len(frames)
        get_ctx().send({
            "type": "progress",
            "data": {"frames_generated": frame_count}
        })
Your frontend receives these messages and can update UI accordingly. For the full messaging API, see Context.

Block Size and Reactivity

Each block is generated from a single set of conditions. If a user changes their input mid-block, that change will not affect frames already being generated. It will only apply to the next block. This means smaller block sizes provide more reactivity. A block size of 1 frame means every frame can respond to the latest input. A block size of 12 frames means users wait up to 12 frames before seeing their input reflected.
EDITOR: Add diagram showing how inputs affect blocks.
Choose your block size based on your model’s architecture and the level of interactivity you need. Some models only support specific block sizes based on their temporal attention patterns.
Now that you understand how to emit frames, the final step is learning how to clean up session state so your model can serve multiple users efficiently.

Model Cleanup

Learn how to prepare your model for the next user.