The .dreamrc file

One .dreamrc at the root of a dataset renders every episode in it. The file answers three questions — where the episodes are, how to parse them, and how to lay out the visualization — and nothing else. It is plain YAML, never contains credentials, and works unchanged on any storage backend. Why the system is shaped this way — views with declared input contracts, meaning stated in the config — is the architecture; this page is the grammar.

yaml
# .dreamrc — at the dataset root
version: 1
name: Kitchen Manipulation v2   # optional display name

dataset:                        # ── data entry + parsing ──
  format: lerobot               # lerobot | folder | umi | mcap
  episodes: auto                # ask the format adapter (default)

views:                          # ── visualization: compose views ──
  - view: videoStack
    cameras: ["observation.images.*"]
  - view: lineChart
    series:
      - { field: [action, "*"] }

That is a complete, working file. Note what it does not say: where the dataset lives. A .dreamrc at its dataset's root inherits the storage it sits in — the app injects it. A standalone file (a demo, a config pointing at another bucket) adds one more block, storage: — see below. Everything else is the reference for the blocks.

Gallery — 10 real datasets, one grammar

LeRobot v2/v3 · depth maps · point clouds · UMI zarr · MCAP · annotations + 3D — a full-screen two-pane switcher of complete .dreamrc files

Live example

One file exercising most of the spec at once — glob enumeration, auto-discovered annotation tracks (COCO hand keypoints drawn over the video, WebVTT subtask cues on a timeline), and a real 3D hand–object reconstruction (glTF geometry + per-frame parquet tracks) driven by the shared cursor. It uses the folder format on purpose: the zero-convention layout — any files you can put in folders, no conversion, the easiest dataset there is to prepare. An existing LeRobot / zarr / MCAP dataset needs an even shorter file (format: + episodes: auto — see the gallery). The source pane is the complete standalone .dreamrc; copy it and it runs anywhere:

Resolving dataset…

dataset: — which episodes, parsed how

format names the dataset format; episodes says how to enumerate the episodes. Both live here and nowhere else.

Which format is mine? List the dataset root and match the signature:

you see at the rootformatepisodes
meta/info.jsonlerobotauto
a *.zarr.zip or a .zarr/ directoryumiauto
*.mcap filesmcapauto
one folder per recordingfoldera glob, e.g. "episodes/*/"

If none matches, folder is the escape hatch: it asks nothing of the layout beyond one directory per episode, and reads whatever standard files it finds. HDF5 (RoboMimic, ManiSkill, AgiBotWorld) and RLDS/TFRecord (Open X-Embodiment) have no adapter yet — most publishers also ship a LeRobot export, which does.

keyvaluesmeaning
formatlerobot | folder | umi | mcapthe format adapter. One name per format — no aliases.
episodesauto (default)ask the format adapter. Container formats know their own episode count (LeRobot meta/info.json total_episodes, zarr episode_ends).
"episodes/*"a glob over storage paths — one episode per match. A trailing / matches directories only. Only * is supported, one path segment per star.
{ glob, sort?, limit? }glob with options. sort: name (numeric-aware, default) | name-desc | none. limit: cap per pattern (default 1000).
annotations{ <track>: <path> | { path, kind? } }extra annotation tracks merged into every episode's catalog — see below. kind is the author stating the payload once in the declaration instead of at every binding.
labels{ "<name or glob>": "Display name" }rename a field, or one dimension of one ("observation.state[3]"), when the dataset's own names are serial numbers, null, or absent.

Use auto for container formats (lerobot, umi) and a glob for folder-per-episode layouts (folder):

yaml
dataset:
  format: folder
  episodes: "episodes/*/"       # each matched folder is one episode

In glob mode the matched path is handed to the adapter as the episode root — you never write per-episode config. { limit } on its own (no glob) caps what a container format enumerated, which is how you preview a 300-episode dataset.

Writing views: for a dataset you have not seen — start with view: fieldsCatalog and nothing else. It prints the inventory: every field's address plus the dtype, shape and names the container reported, and no conclusion drawn from them. Reading that listing is where the judgment happens — you are the one who knows that observation.environment_state is a point cloud — and the bindings you write next are where it gets recorded.

dataset.labels — names the container got wrong

A camera keyed by its serial number, a 14-dim state whose names is null: the data is right and only the label is unreadable. Patterns match field names (or a feature[dim] address), first match wins:

yaml
dataset:
  format: lerobot
  labels:
    "observation.images.cam_035622060973": Front camera
    "observation.state[3]": wrist_flex

labels changes what is displayed and nothing else; bindings still use the real names. There is no companion key for meaning — nothing to correct, because nothing was guessed. What a field is gets stated where it is bound.

dataset.annotations — tracks that live beside the data

Annotations belong inside your dataset's container — see what your data must look like. This block is for the two cases that cannot: static geometry, which no container models, and a dataset you cannot write into.

yaml
dataset:
  format: lerobot
  annotations:
    scene: "recon/scene.glb"     # a path, nothing more

The merge happens after the format adapter runs, so a declared track lands in the same catalog as the dataset's own fields, and a declaration overrides a native track of the same name. Paths, formats and the rest: files beside the data.

views: — compose views

Each entry names a view from the registry and binds fields to it. Everything that is not a binding key is passed through to the view as props — each view's reference section lists what it accepts. You own the layout: nest split nodes to build any arrangement.

yaml
views:
  - view: videoStack
    cameras: ["observation.images.*"]  # binding — read as video
    overlays:
      - { field: observation.keypoints_2d.left.ego, as: keypoints }
    columns: 2                         # passthrough prop
  - view: timeline
    tracks: [{ field: subtask_index, as: segments }]   # joins its label table
  - split: row                         # layout node: row | column | grid
    children:
      - view: lineChart
        series:
          - { field: [action, left_waist], label: waist · cmd }
          - { field: [observation.state, left_waist], label: waist · actual, dash: "3 2" }
      - view: lineChart
        series: [{ field: [action, right_waist] }]

Rules, all of them:

  • One binding style per view, and the slot's name says what it takes. Camera views (videoStack, frameStack, depthStack) bind cameras, lineChart binds series, timeline binds tracks, pointCloud binds cloud; camera views also take overlays, and recon3d takes both geometry (the glTF) and tracks (motion). A bare string in series/tracks/overlays is shorthand for { field }. A slot a view does not read is an error, never ignored.
  • The slot says what the bytes become. Binding a field to a slot is what decides how it is decoded — series reads numbers as traces, tracks on timeline reads them as spans, cloud on pointCloud reads them as a cloud. When the field's addressing kind leaves the slot only one possibility, nothing is written. When it leaves several, the entry says which with as (overlays draws a skeleton or captions; a .json could be either), and until it does the panel refuses and prints the choice (the rule).
  • Field references are "feature" or [feature, dim] — a feature name (never split on dots: observation.state is one name), optionally drilled into one named dimension. One glob rule: * matches within a name.
  • Layout is split: row | column | grid with children; nest freely. grid accepts columns. A row is a fixed-height strip (height, default 280): the layout never reflows as media loads — extra width overflows into a horizontal scrollbar instead, and that scroll syncs across episodes (the list renderer wraps episodes in SyncScrollProvider). Per child you choose what to keep: nothing — keep the row height (media takes its aspect-derived width, everything else stretches to share the leftover, flex weighting it and minWidth flooring it); width — a fixed-width box; height — that child's own strip height.

views is required. There is no default layout: a dataset with no views is one nobody has described yet, and inventing an arrangement for it would be the program deciding what its data means. To find out what there is to bind, resolve the dataset with no config and read the inventory — check-dreamrc prints every field with its dtype and shape.

The views

The initial registry — it grows over time, and a host app can register its own with registerComponent(spec):

viewrendersslot → payload it asks for
videoStackcamera videos as a tile grid, with overlay supportcamerasvideo | image · overlayskeypoints | segments
frameStackper-frame image sequences (chunked cameras)camerasframes · overlayskeypoints | segments
lineCharttime series, styled per-dim traces, synced cursorseriesseries
timelineruler + labelled track blockstrackssegments
metaPanelepisode name / duration / task strings header card— (note prop)
fieldsCatalogthe episode's inventory as a table
recon3danimated 3D scene: glTF geometry moved by per-frame tracks, plus point sets — orbit + cursor-driven playbackgeometrymesh3d · trackstransform3d | vertices3d | pose3d
depthStackper-frame depth maps, turbo-colorizedcamerasdepth · overlayskeypoints | segments
trajectory2dplanar series as a top-down xy pathseriesseries
bandTrackdiscrete series as categorical color bandsseriesseries
pointCloudper-frame 3D point clouds, orbitablecloudpointcloud

Every slot, its payload, and the shape that payload needs: reference.

storage: — where the dataset lives

Every path in the file is relative to the dataset root; storage: says where that root is. It is optional, and who writes it is the design:

  • At the dataset root, omit it. The app that found the file injects the storage it sits in — a DreamLake source, a project folder, any browsed directory. This is the normal, uploaded form: the file never repeats what its own location already says, and moving the dataset never breaks it.
  • Standalone files declare it. A docs example, a demo gallery, a config that points at another bucket — anything not sitting at its data's root names the root explicitly. Every live example on this page is this form: copy the YAML and it resolves the same data anywhere.
  • A declaration wins. If a file with storage: is opened inside the app, it renders as written (same rule as dataset.annotations: an explicit entry is user intent). Omission — not override — is what makes a file portable.
yaml
storage: { driver: hf, repo: lerobot/pusht }     # public HuggingFace repo
storage: { driver: http, url: https://my-cdn.example.com/kitchen-v2 }

Built-in drivers are http (url — the dataset root URL) and hf (repo, plus optional root, revision, repoType) — both credential-free. Host apps register more: DreamLake registers dlSource and dlProject, whose configs carry identifiers only — a .dreamrc never contains credentials; drivers that need auth get their tokens from the host at registration time. Per-driver keys: reference.

The whole storage contract is two methods — list(path) and resolveUrl(path) — which is why any backend can be a root.

Validation

validateDreamrc(parsed) checks the file before anything renders and throws errors written to be fixed mechanically — each names the offending key, the allowed values, and the episode where expansion failed. Typical messages:

.dreamrc: dataset.format 'lerobot3' is not a registered format (available: lerobot, folder, umi, mcap)
.dreamrc: views[2].view 'lineChart2' is not registered (did you mean 'lineChart'?)
.dreamrc: episodes glob "episodes/**" — '**' is not supported, use one '*' per path segment
.dreamrc declares no 'storage:' and no host root storage was supplied — standalone files need storage: { driver, … }

TypeScript API

For hosts and tests — the library is credential-free and YAML-free (parse upstream, pass the object):

ts
import {
  validateDreamrc, resolveDataset, DatasetViz,
  registerStorage, registerFormat, registerComponent,
} from '@dreamlake/viz/dataset-viz'

const rc = validateDreamrc(parseYaml(text))
// A self-contained file (declares storage:) resolves alone; for a file found
// at a dataset root, inject that root — the file's own storage: would win.
const { episodes, warnings } = await resolveDataset(rc, {
  rootStorage: { driver: 'http', url: 'https://…/my-dataset' },
})
// episodes: ResolvedEpisode[] — id, name, meta, and a ready-to-render handle
// <DatasetViz episode={episodes[0]} views={rc.views} />