What your data must look like
This page is the data side of the
contract: what your container must declare, the
shape each payload demands, and where annotation tracks live. The program
never decides what your data means — your .dreamrc does
(the architecture) — so the requirements are much
smaller than a list of naming rules. There are only two:
- The container must say which bytes are encoded media, so they can be
addressed a frame at a time instead of being read as numbers. Every format
already does this — LeRobot's
dtype, a zarr array's codec, an MCAP channel's schema, a file's extension. - A column bound to a view must have the shape that view decodes. Ask
for a skeleton and the numbers must be
[J,2]or[J,3]. That is not a rule we impose; it is what a skeleton is.
Everything else — which camera a hand belongs to, whether a [7] column is a
pose or a gripper command, what a .json beside your video contains — is
stated in the .dreamrc. You never rename a feature to make it render.
Which datasets can be read
Four formats, named in dataset.format. Each is somebody else's
specification, read as published:
format | the dataset is | episodes come from | how you recognize yours |
|---|---|---|---|
lerobot | a LeRobot dataset, v2.0 / v2.1 / v3.0 | meta/episodes (v3) or meta/episodes.jsonl (v2) | there is a meta/info.json at the root |
umi | a Zarr store — a v2 .zarr.zip ReplayBuffer or a v3 .zarr/ directory | meta/episode_ends, or the store's attributes | there is a *.zarr or *.zarr.zip |
mcap | MCAP v1 logs, one file per episode | one *.mcap file each, listed or globbed | there are *.mcap files |
folder | no container at all — directories of files | a glob you write, e.g. "episodes/*/" | none of the above; one directory per recording |
folder is the escape hatch and asks nothing of the layout beyond one
directory per episode. HDF5 (RoboMimic, ManiSkill, AgiBotWorld) and
RLDS/TFRecord (Open X-Embodiment) have no adapter yet — most publishers also
ship a LeRobot export, which does. A format we do not read gets an adapter,
not a conversion demand
(the extension path).
What the viewer sees before you configure anything
The catalog is an inventory, not a classification. Point the checker at any dataset and it lists what exists, with the facts the container reported and no conclusions drawn from them:
The kinds describe how the bytes are addressed, never what they mean:
| kind | means |
|---|---|
video | an encoded stream, seekable |
frames / image | encoded images, one per frame or one file |
tensor | numbers, with dtype and shape as the container reported them |
text | strings |
file | an address, with its extension |
That listing is what you write the config against — it is the raw truth,
and an agent authoring a .dreamrc reads exactly this. Binding a field to a
view slot is what states its meaning, and asking for something the bytes
cannot become fails by name
(observation.state is float32 [6]; keypoints needs [J,2] or [J,3]) instead
of rendering a guess.
What each payload needs
The shape requirements, in full. This is the whole contract on the data side:
| ask for | the column must be | notes |
|---|---|---|
series | any numeric scalar or [n] | names in the container label the traces |
keypoints | float [J,2] or [J,3] | third component is a score; NaN, not 0, for not-measured |
segments | an int column plus a label table, or a string column, or a .vtt / .srt | equal consecutive values merge into one span |
depth | float [H,W] or [H,W,1] | metres or millimetres — declare the scale |
pointcloud | float [N,3] or [N,6] | xyz, or xyz + rgb |
transform3d | float [7] or [N,7] | translation + quaternion |
vertices3d | float [V,3] | topology comes from a bound glTF node |
pose3d | float [J,3] or a flat [J*3] | a point set per frame |
frames | encoded image bytes per row, or a per-frame path template | |
mesh3d | a .glb / .gltf / .obj | node names bind per-frame 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 belong inside the container
A payload's preferred home is inside the dataset, expressed in the container's own idiom: a keypoint feature sits in the same parquet as your state and action columns, gets read by episode row range, and travels with the dataset.
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 the columns can be read one episode at a time while the file must be fetched and parsed whole. One container, one round trip, no second thing to keep in sync.
What that looks like per container, and the one thing each must get right:
LeRobot — meta/info.json features gives every column its dtype,
shape and names, and those appear verbatim in the listing. The one thing
to get right: cameras must be dtype: video or dtype: image, so they
are addressed as media. Episode boundaries come from meta/episodes — v3
packs many episodes into one parquet and one mp4 per camera, and each
episode's row range and video window are read from there. A labelled span is
an index column plus a label table (meta/<name>s.jsonl — LeRobot's own
task_index + meta/tasks.jsonl pattern, one level down); hand keypoints
are a [J,2] or [J,3] float feature like any other.
MCAP — every channel is listed with its schema name and encoding.
Foxglove schemas (PointCloud, FrameTransform, CompressedImage,
SceneUpdate, …) have decoders; cdr / ros2msg / ros1msg do not, so a
plain ROS 2 bag currently yields nothing.
Zarr — an array's codec says whether it holds encoded images or numbers.
Episodes come from meta/episode_ends or the store's attributes.
A folder of files — one directory per episode; a file's basename is its
track name; annotations/ is searched. Three placement conventions, and none
of them says what a file contains.
Files beside the data
Two cases genuinely cannot go inside, and only two:
- The container cannot model it. Static geometry is the real example — there is no LeRobot feature shape for a mesh.
- The dataset is not yours. A public mirror you cannot write into, so any extra tracks (and the config itself) have to live elsewhere.
For those, put a file beside the data in an established format and name it in the config. The original dataset is never modified — delete the extra files and it is untouched.
Declaring a track
dataset.annotations maps a track name to a path. The merge happens after
the format adapter runs, so a declared track lands in the same catalog as the
dataset's own fields and binds the same way:
- Paths are relative to the dataset root; templates use the same
{var}/{var:06d}style LeRobot itself uses. Variables:episode_index,episode_name,episode_path(glob mode). - A declaration is an address, not a claim. The file lands in the catalog
as
filewith its extension recorded, and the view that binds it says what to make of it. The extension gets its say at read time, where all it picks is a PARSER: asked forsegments, a.vttgoes through the WebVTT reader and a.jsonthrough the COCO one. { path, kind }is you saying it once, in the declaration instead of at every binding — legitimate because it is the author speaking, not the library deducing. A binding's ownas:still wins, and either way the decoder checks the bytes and fails loudly when they cannot produce what was asked.- A declaration overrides a native track of the same name. That is user intent: replacing a dataset's coarse task segments with a refined re-annotation is exactly what this is for. Undeclared native tracks stay.
- The
folderformat auto-discovers anything in each episode'sannotations/folder, so declaring is never required when files live there.
Which format to write
Use the standard that already exists for the job — none of these is ours:
| for | write | spec |
|---|---|---|
| labelled time spans | WebVTT .vtt (or SubRip .srt) | W3C |
| 2D keypoints | COCO keypoints .json | COCO |
| geometry, and its motion | glTF 2.0 .glb | Khronos |
| per-frame numbers | Apache Parquet | Parquet |
Time spans — WebVTT, the web's own "time range → text" format:
2D keypoints — COCO, stock, plus a top-level fps because COCO indexes
images and a player needs seconds (our one documented extension to COCO):
Every key is COCO's, including the 1-based skeleton pairs. A frame with no
detection simply has no annotation — absence, not a zero coordinate.
Geometry and motion — glTF. Export one .glb with your objects as
named nodes: the node name is the join key, so a node called ruler
binds the per-frame pose track named ruler, and no side file describes the
relationship. If the objects move, animate the glTF — per-node
translation/rotation/scale channels with their own keyframe times are already
standardized, so one animated .glb carries geometry AND motion in a file
Blender, three.js and every other glTF tool reads. (The first clip's TRS
channels are read; skinning, morph targets and multiple clips are not yet.)
Per-frame numbers — Parquet, 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 |
| 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 a decoder's
requirement, consulted only once a binding has asked for transform3d or
vertices3d — never evidence about what a file holds.
Those column names are ours — Parquet is Apache's, but "tx,ty,tz plus a
quaternion plus a grouping column" is not written down anywhere else, so we
define it deliberately: the motion-track Parquet profile v1, currently
the one self-defined on-disk structure in the spec
(when we define our own).
An animated glTF carries the same information inside an existing standard
and opens in Blender and three.js; the profile is the columnar route when
your pipeline is already writing Parquet. Pick whichever it produces more
naturally.
Cameras: the encoding that actually matters
The single most common cause of a sluggish dataset, and it applies to every container. 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: reads the .dreamrc,
decodes every bound field, and prints what came back. With no config it
prints the inventory instead — which is how you find out what to write.
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
- 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 (spec) - Resolved once from a shell, with every binding reporting the payload you expected