Skip to content

Robot Dataset Formats

Every robot-learning dataset stores the same thing — synchronized video plus per-frame signals — and no two agree on how. Potato reads four layouts into one shape, Episode, and everything downstream speaks only that.

Format Needs Detected by
Potato episode manifest nothing episode.json with streams or series
LeRobot v2 pyarrow meta/info.json
HDF5 (RoboMimic / ALOHA) h5py .h5 / .hdf5
RLDS / TFDS (Open X-Embodiment) tensorflow_datasets by dataset name, not path

Detection is by content and layout, not extension: .h5 is claimed by half of scientific computing, and a LeRobot dataset is a directory with no distinguishing suffix at all.

What is deliberately not normalized

Units and semantics. The reader records what the file said — a field name, a stated unit — and does not convert. A joint angle in radians and one in degrees look identical to a normalizer and differ by 57× to an annotator, so guessing is worse than carrying the label through and letting the config say.

Potato's own manifest

The one that needs no dependency. Use it for your own logs, or convert into it.

{
  "episode_id": "pick_place_0007",
  "fps": 20,
  "num_frames": 240,
  "instruction": "pick up the red block and put it in the bowl",
  "streams": [{"name": "wrist", "url": "video/wrist.webm", "kind": "wrist"}],
  "series": [{"name": "gripper", "unit": "m", "values": [0.06, 0.058, ...]}]
}

num_frames may be omitted and is then taken from the longest series — the frame count is a property of the data, and repeating it is one more thing to get out of step.

Stream URLs are resolved relative to the manifest and served through /media. Absolute URLs are left alone.

LeRobot v2

The de-facto standard, and the format most new datasets ship in.

meta/info.json                          fps, features, path templates
meta/tasks.jsonl                        task_index -> language instruction
data/chunk-000/episode_000000.parquet   one row per frame
videos/chunk-000/observation.images.wrist/episode_000000.mp4

Potato reads the path templates out of info.json rather than assuming the layout. Chunk size is configurable, and a dataset with more than chunks_size episodes really does put the next one in chunk-001; a hardcoded path reads the right file for episode 0 and nothing after it.

Vector columns are flattened to one series per component, named from features[col]["names"] where the dataset provides them — joint_0 tells an annotator nothing, shoulder_pan tells them where to look. Index columns (frame_index, episode_index, timestamp, task_index) get no lane: they are straight lines by construction and would waste the only vertical space there is.

Pointing an item at one episode:

# in your data file
{"id": "ep_0", "episode": "lerobot_ds", "episode_index": 0}
# in the schema
source_field: episode
episode_field: episode_index

HDF5

Two conventions dominate and they disagree about almost everything, so the reader detects which it is rather than requiring you to say.

ALOHA / ACT — one file per episode:

/observations/qpos          (T, 14)
/observations/images/<cam>  (T, H, W, 3)
/action                     (T, 14)

RoboMimic — one file for a whole dataset:

/data/demo_0/obs/<key>      (T, ...)
/data/demo_0/actions        (T, A)
/data/demo_0/rewards        (T,)

Demo keys sort numerically, so demo_10 does not come between demo_1 and demo_2, and they are treated as keys rather than indices — a filtered dataset has holes, and indexing would read the wrong demonstration.

Frames stay in the file. ALOHA stores raw RGB arrays; extracting 500 frames into an MP4 is an ffmpeg job with its own failure modes, and doing it inside a reader would make opening an episode arbitrarily slow with no progress shown. The episode reports which image datasets exist and the timeline draws the series lanes.

RLDS / TFDS

The Open X-Embodiment collection, and the heaviest to read.

TensorFlow is hundreds of megabytes and pulls CUDA on many platforms, which would triple Potato's install for a format most projects never touch. So it is imported inside the reader, never at module scope, and its absence produces a message naming the install command and the offline alternative:

Reading RLDS/TFDS datasets needs tensorflow_datasets, which Potato does not install by default… Or convert: many Open X-Embodiment datasets are also published in LeRobot v2 form on the HuggingFace hub, which Potato reads with pyarrow alone.

RLDS says an observation is a dict; it does not say what is in it. So the reader takes whatever numeric per-step fields it finds and names them by their path, rather than looking for a fixed set and returning an empty episode when the dataset does not use those names.

Frame rate is a parameter, not data. RLDS records steps, not time, and most Open X-Embodiment datasets do not state their control frequency anywhere machine-readable. The default of 10 Hz is documented as a guess, and the episode's metadata carries fps_is_assumed: true. A timeline built on a wrong frame rate is still correctly ordered, which is what phase annotation needs — but the exported frame indices would be wrong, so set it if you know it.

Adding a format

A detect and a read in a new module, and one line in potato/episodes/registry.py. The same shape as the importer, exporter, schema and display registries, so someone who has added one of those already knows how.