DreamLake

Write the .dreamrc

One .dreamrc at the root of a dataset renders every episode in it. Plain YAML, no credentials, works unchanged on any storage backend. The file answers three questions — where the episodes are, how to parse them, how to lay out the visualization — and this page lists every key it can contain. (Haven't written one before? Start here walks the five steps.)

The file at a glance

Every key that exists, in one annotated file. Only version, dataset and views are required:

.dreamrcyaml
version: 1 # required — always 1
name: Kitchen Manipulation v2 # optional display name

storage: # ONLY in standalone files — a .dreamrc at
  driver: hf # its dataset's root omits this block and
  repo: my-name/kitchen-v2 # inherits the storage it sits in

dataset:
  format: lerobot # required: lerobot | folder | umi | mcap
  episodes: auto # auto (default) | "episodes/*/" | { glob, sort, limit }
  fps: 15 # folder/umi only — the clock for stills / ReplayBuffer
  labels: # optional: display names, when the dataset's are unreadable
    'observation.state[3]': wrist_flex
  annotations: # optional: tracks in files BESIDE the data
    subtasks: 'annotations/subtasks/episode_{episode_index:06d}.vtt'

views: # required — there is no default layout
  - view: videoStack # a view from the registry…
    cameras: ['observation.images.*'] # …with fields bound to its slots
    overlays:
      - { field: hand_keypoints, as: keypoints }
    height: 320 # every view takes width / height / aspectRatio
  - split: row # layout node: row | column | grid
    height: 240
    children:
      - view: lineChart
        series: [{ field: [action, '*'] }]
      - view: recon3d
        tracks: [{ field: hand_3d, as: pose3d }]
        aspectRatio: '16 / 9'

Gallery — real datasets, one grammar, editable live

Complete .dreamrc files over public data — pick one, edit the YAML, watch the render follow

→

dataset: — which episodes, parsed how

keyvaluesdefaultmeaning
formatlerobot | folder | umi | mcaprequiredthe reader. Which is mine?
episodesautoautoask the format — container formats know their own episode count
"episodes/*/"a glob over storage paths, one episode per match; trailing / matches directories only; one * per path segment
{ glob?, sort?, limit? }sort: name (numeric-aware) | name-desc | none · limit caps the count (a { limit } alone previews a 300-episode container)
fpsnumberfolder: 30 · umi: 60folder: the clock for numbered still runs — a directory of JPEGs states no frame rate, so declare it or annotation tracks drift. umi: the ReplayBuffer time axis (a v3 manifest's own fps wins)
pathstringauto-detectedumi: the store when not at the root · mcap: a single file at a subpath
labels{ "<name | glob | feature[dim]>": "Display" }—display names only; bindings keep the real names (below)
annotations{ <track>: <path> | { path, kind? } }—tracks in files beside the data (below)

Container formats (lerobot, umi, mcap) use auto; folder needs a glob — the matched path becomes the episode root, and you never write per-episode config.

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, only the label is unreadable. Patterns match field names or one dimension (feature[dim]), 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.

dataset.annotations — tracks that live beside the data

For the two cases that cannot live inside the container — static geometry, and a dataset you cannot write into (where annotations belong):

yaml
dataset:
  format: lerobot
  annotations:
    subtasks: 'annotations/subtasks/episode_{episode_index:06d}.vtt'
    scene: { path: 'recon/scene.glb' }
  • Paths are dataset-root-relative; templates use LeRobot's own {var} / {var:06d} style. Variables: episode_index, episode_name, episode_path (glob mode).
  • A declaration is an address, not a claim — the file lands in the catalog as file, and the binding (or an optional kind in the declaration) says what to make of it.
  • A declaration overrides a native track of the same name — replacing a dataset's coarse segments with a refined re-annotation is what that is for.
  • The folder format auto-discovers each episode's annotations/ dir, so declaring is only needed for paths outside that convention.

views: — compose the visualization

Each entry either names a view and binds fields to its slots, or is a split layout node. views is required — a dataset with no views is one nobody has described yet.

Binding fields to slots

A binding names a field by the name the inventory lists (the fields view, or check-dreamrc with no config). Three spellings, used everywhere below:

  • observation.state — the whole field. Names are never split on dots: observation.state is one name.
  • [observation.state, left_waist] — one named dimension of a field.
  • "observation.images.*" — a glob; * matches within a name and binds only fields the slot can read.

A bare name is shorthand for { field: <name> }; the object form is for when an entry also needs as:, label:, on: or styling.

The slot's name says what it takes; binding a field to a slot is what decides how its bytes are decoded:

slotontakesentry form
camerasvideoStack frameStack depthStackcamera fieldsfield names: cameras: [cam_front, "observation.images.*"]
serieslineChart trajectory2d bandTracknumeric columnsa field name, or { field, label?, color?, … } (styling)
trackstimelinespan columns / filesa field name, or { field, as }
tracksrecon3dmotion tracks{ field, as: transform3d | vertices3d | pose3d }
overlaysthe camera viewskeypoints / captions{ field, as: keypoints | segments, on? }
geometryrecon3dglTF/OBJ filesfield names
cloudpointCloudone cloud columna field name
  • as: — write it when the slot could read the field more than one way; the validator tells you when. The common cases: overlays (a .json is a skeleton file or captions), timeline tracks (an int column becomes spans only when you say so), every recon3d track (all three kinds are tensors of numbers). Wrong or missing as fails naming the choice — nothing renders on a guess.
  • on: (overlays only) pins an overlay to one camera: { field: hands, as: keypoints, on: cam_front }. With one camera bound there is nothing to disambiguate and on can be omitted.

series entry styling

lineChart traces take per-entry styling (validated, all optional); trajectory2d and bandTrack take the addressing keys and label:

keytypedefaultmeaning
labelstringthe dim's namelegend / readout label; on a multi-dim entry it prefixes each trace
colorCSS colorpalettestroke color
dashstringsoliddash pattern, e.g. "3 2" — the convention for command vs actual
widthnumber1.4stroke width
opacity0–11stroke opacity
linecapbutt | round | squarestroke linecap
readoutbooleantrueinclude in the cursor readout
ghostbooleanfalsedim the readout row and suffix "tgt" — for target/reference traces

Sizing — every view, three keys

Every view entry takes width, height and aspectRatio (number or "16 / 9"). A sized view fills its box exactly like a row-strip child does — media at its aspect width behind a horizontal scroll, charts and 3D panes stretched to fit; width alone just constrains the flow width:

yaml
views:
  - view: recon3d
    tracks: [{ field: hand_3d, as: pose3d }]
    aspectRatio: '16 / 9' # or: height: 360, or width: 480

Views also have their own defaults when unsized — each view's table on view components lists them.

split: — layout nodes

Nest freely: row and column and grid compose.

keyondefaultmeaning
splitrequiredrow | column | grid
childrenallrequiredthe nested views / splits
columnsgrid2grid column count
heightrow280the strip height in px

A row is a fixed-height strip: the layout never reflows as media loads — extra width overflows into a horizontal scrollbar that syncs across episodes. Per child, keep one dimension:

child keymeaning
(nothing)keep the row height — media takes its aspect-derived width, everything else stretches to share the leftover
widtha fixed-width box
aspectRatiowidth derived from the strip height
flexweight among the stretching children (default 1)
minWidthsquish floor for a stretching child (default 220) — past it, the row scrolls
heightthat child's own strip height

View options

Binding slots and sizing aside, every other key on a view entry passes through to the view — columns on a camera grid, colormap on depthStack, up on the 3D views. The complete per-view tables live on view components, next to each live demo.

storage: — where the dataset lives

Every path in the file is relative to the dataset root; storage: says where that root is — and who writes it is the rule:

  • At the dataset root, omit it. The app injects the storage the file sits in; moving the dataset never breaks it. This is the normal, uploaded form.
  • Standalone files declare it — a config in another repo, a doc example. A declaration always wins over an injected root.
driverkeysnotes
hfrepo required · root · revision (default main) · repoType (default datasets)public HuggingFace repos, credential-free
httpurl required — the dataset root URLglobs need per-directory index.json manifests; episodes: auto formats don't
dlSource / dlProjectidentifiers onlyregistered by the DreamLake app; tokens come from the host, never the file
yaml
storage: { driver: hf, repo: lerobot/pusht }
storage: { driver: http, url: https://my-cdn.example.com/kitchen-v2 }

Validate, then trust the errors

check-dreamrc resolves the file exactly as the app does — with no config it prints the inventory; with one it decodes every binding:

bash
npx tsx scripts/check-dreamrc.mts ./draft.dreamrc
npx tsx scripts/check-dreamrc.mts hf:your-name/your-dataset

One catch with local drafts: a root-arranged file — one that omits storage: because the app will inject it — cannot resolve standalone. Temporarily add a storage: line pointing at your bucket or repo while validating, and drop it again before upload.

Every error names the offending key, the allowed values, and the nearest registered name — a write → validate → fix loop a person or an agent can run without reading anything else:

.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: views[0].overlays[0].as 'keypoint' — did you mean 'keypoints'?
.dreamrc: episodes glob "episodes/**" — '**' is not supported, use one '*' per path segment

Live example

One standalone file exercising most of the language at once — glob enumeration, auto-discovered annotations (COCO keypoints over video, WebVTT cues on a timeline), and a real 3D reconstruction driven by the shared cursor. Copy it; it runs anywhere:

Resolving dataset…

TypeScript API

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

ts
import {
  validateDreamrc,
  openDataset,
  EpisodeBrowser,
  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 ds = await openDataset(rc, {
  rootStorage: { driver: 'http', url: 'https://…/my-dataset' },
})
// ds.infos — the full enumeration as plain facts (name, length, task
// strings), uncapped: a few MB even at ~100k episodes. Hand it to
// <EpisodeBrowser ds={ds} views={rc.views} fill /> — it owns the task
// search box, page-at-a-time materialization, and the scroll container.

Search inside <EpisodeBrowser> is a provider, not a fixed matcher: the default scans meta.tasks substrings; pass search (an EpisodeSearch: (infos, query) → infos in display order, sync or async) to swap in fuzzy, semantic, or server-index implementations. The box only renders when the enumeration carries task strings — today that means the lerobot format, whose episodes metadata is where per-episode task text lives.

resolveDataset(rc, opts) is the one-shot form of the same call — everything materialized as ResolvedEpisode[], default-capped at 2000 — for tests and small fixed lists; <EpisodeList episodes views> renders such an array, and <DatasetViz episode views> renders a single episode.