Skip to content

1. Start from the scaffold

Create the motion watcher project with tatolab new, run it, and find your way around its files.

tatolab new writes a small, working stream. You’ll change it into the motion watcher over the next steps.

Terminal window
tatolab new motion-watcher
cd motion-watcher
uv sync
tatolab dev

A window opens with your camera, colors inverted, and the terminal shows a brightness record every second. Keep tatolab dev running for the whole tutorial. Each time you save a file, it loads the stream again with your change.

If tatolab dev says the name main is held by another stream, the quickstart’s stream is still loaded: press Ctrl-C in its terminal, or run tatolab rm main if you kept it.

Open stream.py. Apart from its docstring, it’s this:

stream.py
from tatolab.stream import CameraSource, DisplayWindow, StreamBuilder, stream
from nodes.brightness_meter import BrightnessMeter
from nodes.inverting_effect import InvertingEffect
@stream
def main(stream_builder: StreamBuilder) -> None:
"""Camera, inverted, in a window; brightness logged once a second."""
source = stream_builder.add(CameraSource)
effect = stream_builder.add(InvertingEffect)
meter = stream_builder.add(BrightnessMeter)
window = stream_builder.add(
DisplayWindow, config={"title": "Tatolab", "scaling": "fit"}
)
stream_builder.connect(source.output("video"), effect.input("video_from_upstream"))
stream_builder.connect(effect.output("video_to_downstream"), window.input("video"))
stream_builder.connect(
effect.output("video_to_downstream"), meter.input("video_from_upstream")
)
stream_builder.expose(effect.output("video_to_downstream"))

Line by line:

  • @stream marks main as a stream. The runtime calls it once, when the stream loads, to find out which nodes to create and how to connect them. It doesn’t run per frame.
  • stream_builder.add(CameraSource) adds a node. You pass the class itself, not an instance. CameraSource and DisplayWindow are built into the runtime; InvertingEffect and BrightnessMeter are your project’s own, in nodes/.
  • config={...} sets a node’s settings. For built-in nodes, your editor can complete the keys and flag typos.
  • stream_builder.connect(output, input) links an output port to an input port. The effect’s output is connected twice, to the window and to the meter: one output can feed any number of inputs, and it isn’t split between them.
  • stream_builder.expose(...) lets other streams on this machine read that output. Outputs are internal to their stream until you expose them. You’ll use this in step 3.

nodes/inverting_effect.py is a GPU effect: one GLSL function that runs on the GPU for every pixel of every frame.

nodes/brightness_meter.py is a CPU node. Once a second it reads one frame’s pixels as a numpy array with frame.cpu(), computes the average brightness, and logs it. It runs in its own process, reading the effect’s output beside the window, so if it were slow, the window wouldn’t notice.

In the next step you’ll replace both with a node of your own.

The stream’s name comes from the function’s name, so this one is called main. Give it a name that says what it is. In stream.py, change the function’s name, its docstring and the window’s title:

stream.py
@stream
def motion_watcher(stream_builder: StreamBuilder) -> None:
"""Watches the camera for motion."""
source = stream_builder.add(CameraSource)
effect = stream_builder.add(InvertingEffect)
meter = stream_builder.add(BrightnessMeter)
window = stream_builder.add(
DisplayWindow, config={"title": "Motion watcher", "scaling": "fit"}
)
# … the connect and expose lines stay as they are

Save. tatolab dev loads the stream again, now named motion_watcher, and the window’s title changes. The name matters later: it’s how you refer to the stream in commands like tatolab logs --stream motion_watcher, and it’s part of every exposed port’s address.

Next you’ll write a node that reports motion: Write your first node.