Prepare your data
The viewer reads datasets as published — there is no DreamLake file format and no conversion step. Preparing data therefore means one of three things, and route 1 means nothing at all. Whichever route you take, the per-column shape rules at the bottom of this page are the same.
Route 1: an existing dataset — nothing to do
A LeRobot dataset, a zarr store, or MCAP logs render unmodified. Add the
.dreamrc and go (start here). Per container, the one
thing worth checking:
| you have | format | the one thing to get right |
|---|---|---|
| LeRobot v2.0 / v2.1 / v3.0 | lerobot | cameras must be dtype: video or dtype: image in meta/info.json — that is what addresses them as media |
Zarr — *.zarr.zip ReplayBuffer or .zarr/ v3 directory | umi | camera arrays need an image codec (JPEG/JPEG-XL/PNG) — that is what makes one chunk one frame |
| MCAP v1, one file per episode | mcap | Foxglove/JSON schemas, plain ROS 2 bags (cdr+ros2msg) and ROS 1 bags all decode — see ROS bags & robot models |
For MCAP the schema names are not an extra requirement — they are a
ladder. A plain json channel of numeric leaves is a perfectly legal channel
and charts as numbers; writing the same data under a
Foxglove well-known schema is the writer
declaring what it is, and each declaration unlocks the matching view —
foxglove.PoseInFrame unlocks as: transform3d and the 3D pane,
foxglove.PointCloud unlocks as: pointcloud, image schemas unlock the
frame stack. The more standard the file, the more the
default mcap view shows
with no .dreamrc at all; less standard is never invalid, it just charts.
Adding annotations to a dataset you own? Put them inside the container as ordinary features — below.
Route 2: raw recordings — the folder layout
You have videos, image folders, CSVs, and no container. Arrange them like this and you are done — no manifest, no metadata file, nothing to generate:
The three conventions, in full:
- one directory per episode — the glob in the
.dreamrcfinds them; - a file's basename is its track name (
subtasks.vtt→subtasks); media keep the extension (cam_front.mp4); annotations/inside an episode is searched too, and a file there wins a name collision with one at the episode root.
Details that save a round trip:
- Consecutive numbered stills (
rgb_00000.jpg,rgb_00001.jpg, …) collapse into ONE scrubbable track. Stills have no frame rate, so declare it:dataset.fps: 15— without it every clock in the episode assumes 30 and annotation tracks drift off the pictures. - Numeric tables (
.csv/.parquet) chart directly; atimestamp-like column becomes the x-axis, row index otherwise. - Episodes may differ. Some annotated, some not; a camera missing from one run — each episode renders what it has.
Route 3: a container we cannot read yet
HDF5 (RoboMimic, ManiSkill, AgiBotWorld) and RLDS/TFRecord (Open X-Embodiment) have no reader. Your options, in order:
- most publishers also ship a LeRobot export — route 1;
- export to the folder layout — route 2, and the export is plain files you already know how to write;
- a host app can register a reader for the foreign layout (the extension path) — nothing is ever converted, an adapter is added.
The shape each payload needs
Whatever the container, a column bound to a view must have the shape that view decodes. This is the whole contract on the data side — not rules we impose, but what a skeleton or a point cloud is:
| to render | the data must be | notes |
|---|---|---|
| line chart traces | any numeric scalar or [n] column | the container's names label the traces |
| 2D keypoints | float [J,2] or [J,3] | third component is a score; NaN, not 0, for not-measured |
| labelled time spans | an int column plus a label table, a string column, or a .vtt/.srt | equal consecutive values merge into one span |
| depth maps | float [H,W] / [H,W,1], or 16-bit PNGs | metres or millimetres — declare the scale |
| point clouds | float [N,3] or [N,6] | xyz, or xyz + rgb |
| an object's pose track | float [7] or [N,7] | translation + quaternion |
| a deforming mesh | float [V,3] per frame | topology comes from a bound glTF node |
| 3D keypoints | float [J,3] or flat [J*3] | a point set per frame |
| 3D geometry | a .glb / .gltf / .obj / .stl file | node names bind the motion tracks |
Zero is a real coordinate — the top-left pixel, the origin. Nothing downstream can tell a fabricated zero from a measured one, so a frame written as zeros draws a collapsed skeleton in the corner. Write NaN and it renders as nothing, because it is nothing.
Annotations: inside the container
When the dataset is yours, annotations belong inside it, as ordinary features in the container's own idiom — they travel with the dataset, read one episode at a time, with no second file to keep in sync.
Measured on our own template: 150 frames of two hands cost 57.7 KB as parquet columns and 142.3 KB as a JSON file next to them — and columns read by episode row range while a file is fetched and parsed whole.
Concretely, per container:
- LeRobot — a keypoint track is a
float32 [J,2|3]feature like any other; a labelled span is an index column plus a label table (subtask_index+meta/subtasks.jsonl— LeRobot's owntask_index+meta/tasks.jsonlpattern, one level down). Copy the shape from the annotated template. - Zarr — another array in the store, same episode slicing.
- MCAP — another channel with a schema.
Annotations: files beside the data
Two cases genuinely cannot go inside: static geometry (no container
models a mesh) and a dataset you cannot write into. For those, put a
file beside the data in an established format and declare it in the
.dreamrc (the annotations key):
WebVTT — the web's own "time range → text" format:
COCO keypoints — stock COCO, plus a top-level fps because COCO
indexes images and a player needs seconds (our one documented extension):
A frame with no detection simply has no annotation — absence, not a zero coordinate.
glTF — export one .glb with your objects as named nodes: the node
name is the join key (a node called ruler binds the pose track named
ruler). If the objects move, animate the glTF — per-node TRS channels
carry geometry AND motion in one file Blender and three.js read. (First
clip's TRS channels are read; skinning and morph targets are not yet.)
Parquet motion tracks — when the motion is not in a glTF:
| track | columns |
|---|---|
| object poses | frame, timestamp, object, tx,ty,tz + a quaternion — the object column splits it into one track per object, bound as poses[ruler] |
| mesh vertices | frame, timestamp, and a list-of-float column of flat xyz |
Quaternion order is read from the column order: qw first means w,x,y,z,
qw last means x,y,z,w. (These column names are the motion-track
Parquet profile v1 — our one self-defined on-disk structure;
why it exists.
An animated glTF carries the same information — pick whichever your
pipeline writes more naturally.)
Cameras: the encoding that actually matters
The single most common cause of a sluggish dataset. Scrubbing means seeking, and a seek decodes forward from the previous keyframe — keyframes 10 seconds apart feel stuck no matter how fast the machine is:
H.264 + yuv420p decodes everywhere; HEVC, AV1 and MPEG-4 Part 2 do not.
Independent frames scrub better than any video — one fetch per frame,
no decode chain, exact at any file size.
Verify
It resolves the dataset exactly as the app does: with no config it prints
the inventory (how you find out what to write); with one it decodes every
binding and prints what came back — whether the [21,3] column you called
a skeleton really is one, before anybody looks at a panel.
Checklist
- Cameras are declared as media by the container (
dtype: video/dtype: image/ an image codec / a media extension) - Videos are H.264
yuv420p,+faststart, keyframes ≈ 1 s apart - Every column you intend to render has the shape its payload needs
- Not-measured is NaN, not zero
- Span index columns have their label table
-
folderstills runs declaredataset.fps - Annotations live inside the container where it can hold them; sidecar
files are established formats, declared in
dataset.annotations - A
.dreamrcat the dataset root, nostorage:block — the app injects it (why) - Resolved once from a shell, with every binding reporting the payload you expected