Robot data tutorial
LeRobot Dataset Format Explained: Episodes, Timestamps and Validation
Read LeRobot v3 metadata and episode timing, inspect video, state and action with the official visualizer, and run a bounded Python numeric checker.
LeRobot dataset v3.0 separates storage files from episodes: multiple episodes can share Parquet and MP4 shards, while metadata identifies each episode and its offsets. A filename is not an episode boundary.
Start by checking what each observation and action means, then check shapes, timestamps and episode coverage. Passing a numeric check does not establish that a policy can use the data.
Checked on September 16, 2026. The PyArrow-only numeric checker passed 16 behavior tests on Windows x64. A separate LeRobot 0.6.1 exercise also wrote and loaded 8 numeric frames across 2 episodes through the official writer and loader, and rejected missing action and wrong-shape inputs. Neither exercise tests video decoding, policy training, real hardware or complete format compatibility. On October 25, 2026, a separate pinned public-data check loaded 445 numeric rows from episode 0, decoded two camera streams and read five complete samples through LeRobot 0.6.1. This tests file timestamps, not physical synchronization or hardware calibration. The visualizer workflow was source-checked; its UI was not run.
Which version does this guide describe?
This guide describes the v3.0 dataset format. That format identifier is separate from the LeRobot Python package version. The official main-branch documentation and source were checked on September 16, 2026; the inspected source commit is 89236ea0f4f81a81ca566081e20dd1ff5f823cbe and its CODEBASE_VERSION is v3.0.
The documentation still contains older installation and migration language. Its simplified layout lists meta/tasks.jsonl, whereas the inspected implementation uses meta/tasks.parquet and names tasks.jsonl as a legacy path. Use the metadata and code revision belonging to your dataset; do not mix v2.1 per-episode filenames with v3 shard offsets. The exercise below needs PyArrow only and does not install LeRobot.
Read the storage layout before a training sample
The layout below is a simplified map of a v3 dataset with video, not the contents of our deliberately smaller fixture. A shard may hold several episodes. Camera streams use their own files, and an episode is reconstructed through metadata rather than by pairing similarly named files.
| Component | What it describes | What to inspect |
|---|---|---|
| meta/info.json | Format version, FPS, feature names, shapes, dtypes and path templates. | Read features and data_path/video_path before assuming a vector order or camera layout. |
| meta/stats.json | Feature statistics used by normalization pipelines. | Check the population and split used to compute them; statistics are outside this checker. |
| meta/tasks.parquet | Task text and task identifiers in the inspected implementation. | A numeric task_index alone does not explain the instruction. Task-text mapping is outside this exercise. |
| meta/episodes/ | Episode length, global row boundaries and shard references; video-related offsets when present. | dataset_from_index is inclusive; dataset_to_index is exclusive. Do not infer boundaries from filenames. |
| data/ | Per-frame state, action, timestamp and index columns in Parquet. | Match each vector to its feature specification and each row to the episode table. |
| videos/ | Encoded camera streams, potentially shared by several episodes. | Use camera-specific metadata and timestamps; decoding and frame alignment require separate tests. |
meta/info.json
meta/stats.json
meta/tasks.parquet
meta/episodes/chunk-000/file-000.parquet
data/chunk-000/file-000.parquet
videos/observation.images.front/chunk-000/file-000.mp4State, action and time are different contracts
observation.state records the observed robot state. action describes the target used by a particular controller or policy. They can have different dimensions and units. The format alone cannot tell you whether an action is a joint target, pose delta or another command. Record coordinate frames, dimension order and normalization alongside the dataset.
The inspected writer generates frame_index within each episode and timestamp = frame_index / fps. timestamp is therefore an episode-relative time in seconds in this workflow; it resets between episodes. It is not a UNIX timestamp, a sensor capture timestamp or a measured end-to-end delay. Preserve capture and receive clocks separately when collecting real streams, document how they are aligned, and retain alignment residuals before resampling.
| Fixture field | Meaning | Unit or validity |
|---|---|---|
| index / episode_index / frame_index / task_index | Global row / episode / row within episode / task identifier. | Non-negative integers; task text is not supplied in this numeric subset. |
| timestamp | frame_index / 10 in two separate four-frame episodes. | Seconds; 0, 0.1, 0.2, 0.3 within each episode. |
| observation.state [3] | Synthetic x, y, z position. | Metres in an invented demonstration frame; not measured robot state. |
| action [2] | Synthetic dx, dy values for inspecting a two-dimensional vector. | Metres; illustrative data only, never sent to a robot. |
| observation.tactile [4] | Four independently designed teaching taxels in row-major 2 × 2 order. | Arbitrary units, not newtons or pressure; an invalid sample remains unknown. |
| observation.tactile_valid [4] | Boolean validity for the four custom tactile values. | One false flag accompanies a null; a null marked true is an error. |
A valid row can still become a padded training window
LeRobot 0.6.1 separates raw row access from temporal sample construction. In the inspected DatasetReader, requested indices outside the current episode are clamped to its first or last row, and per-feature *_is_pad flags identify those requests. The returned window can therefore repeat a boundary value without recording another observation. This is source inspection; we did not run a new temporal-loader experiment.
For a hypothetical episode with rows 0, 1 and 2, asking at row 0 for offsets -1, 0 and +1 selects rows 0, 0 and 1. The first requested position is padding. That example explains the index rule; it does not establish how your policy applies the mask or handles a missing terminal state.
- Keep temporal-window review separate from capture synchronization and sensor calibration. Correct index arithmetic does not recover physical timing.
- The numeric exercise below does not test these loader or policy behaviors. Preserve its bounded purpose rather than treating PASS as approval for training.
| Review sample | Check | Decision it supports |
|---|---|---|
| First and last rows | Requested offsets, selected episode indices and padding flags | Distinguish repeated boundary values from recorded transitions |
| Filtered episode selection | Original episode IDs, global offsets and returned sample indices | Prevent a mistaken join between adjacent stored episodes |
| Policy input and loss | Which observations/targets are used and whether padding is respected | Avoid counting manufactured context as independent recorded evidence |
Use the LeRobot dataset visualizer to inspect an episode
The official browser visualizer lets you choose a Hub dataset and episode, then inspect camera recordings alongside state and action charts. Open the linked SO-101 example, pause near a visible movement and select the corresponding chart position. Record the repository, revision and episode ID with any finding. This is a route to the official tool, not a RoboSkin-hosted viewer or a verified tactile dataset.
For local viewing, the command below follows the LeRobot 0.6.1 documentation and script. Use a separate environment with the official installation prerequisites, including its video dependencies. The dataset_viz extra supplies visualization dependencies; the Hub example may download dataset files. The package version is separate from format v3.0, and the live browser Space can change independently. These commands were source-checked on October 25, 2026, not executed in this review.
- The existing numeric fixtures below contain no video. Their passing reports do not validate either viewer, this public example or a new dataset.
- If the command or decoder fails, record the installed version, dataset revision and full error before following the matching official installation guidance. Do not change FPS, pad data or remove episodes just to obtain playback.
| Inspect | Review step | What it does not establish |
|---|---|---|
| Time and frame | In the local Rerun viewer, inspect the timestamp and frame_index timelines around a selected event. Compare the dataset row and declared FPS; check the episode boundaries in metadata. | Smooth playback does not prove capture-clock synchronization or recover timing lost during resampling. |
| State and action | Read dimension names and units from the dataset contract. At the same selected time, compare observation.state with action and the visible motion; retain the interval if a discontinuity needs investigation. | A command need not equal the observed state. Different units, control conventions or physical response can explain a difference. |
| Camera frames | Inspect each camera at the event and nearby frames. Check camera identity, freezes, missing views and whether visible motion agrees with the selected interval; retain camera keys and timestamps. | Visual agreement is a screening observation, not a calibrated alignment measurement or a training-quality certificate. |
python -m pip install "lerobot[dataset_viz]==0.6.1"
lerobot-dataset-viz --help
lerobot-dataset-viz --repo-id lerobot/pusht --episode-index 0Verified public episode: load state, action and two video streams
On October 25, 2026, RoboSkin inspected episode 0 of lerobot/koch_pick_place_1_lego at commit afc0b3e54679cb8a94283fbab5a4ccf0d405f10b. The source card declares Apache-2.0; its metadata says robot_type is unknown. We retain that value and do not infer hardware identity or physical units from the repository name. No tactile feature is declared.
In Windows x64 with Python 3.13.3, LeRobot 0.6.1 and CPU PyTorch 2.11.0, the official local loader exposed 445 rows. Every 6-dimensional state/action vector was finite and matched the source Parquet values. Episode/frame/global indices and the nominal 30 Hz timestamp grid were checked. The shared Parquet and MP4 shards contain other episodes, which this run did not validate.
Both laptop and phone video streams were decoded across this episode. All 445 row timestamps matched distinct decoded frame timestamps within the explicit 0.0001-second tolerance. Five complete official-loader samples, at frames 0, 111, 222, 333 and 444, returned two float32 camera tensors of shape [3, 480, 640]. A deliberate half-frame query was rejected by the decoder timestamp check.
This is file-level matching, not measured capture-clock synchronization, camera latency or training readiness. A nominal timestamp grid can be generated or resampled. The run does not validate visualizer UI behavior, tactile measurements, calibration, a policy, robot hardware or ROS 2.
- The ZIP contains no third-party dataset files. Its downloader retrieves about 410 MB of pinned public shards and checks byte counts and SHA-256. Dependencies require additional disk space.
- Use a fresh extracted folder and follow README.txt. The verifier runs with Hub access disabled and preserves existing reports; it does not upload data, train a model or control hardware.
| Check | Observed result | Scope limit |
|---|---|---|
| Numeric rows | 445 rows; state/action shape [6]; finite and identical to source Parquet. | One episode; feature units and physical accuracy are not certified. |
| Video timestamps | Two camera streams; 445 unique matches per stream; maximum file-timestamp difference below 0.000001 seconds. | The tolerance was 0.0001 seconds; neither number measures acquisition synchronization. |
| Official loader | Five complete samples; two [3, 480, 640] image tensors per sample. | This is not an exhaustive loader pass over all episodes or a visual quality audit. |
| Negative control | Half-frame time offset rejected with FrameTimestampError. | A rejection demonstrates the selected timestamp check, not detection of all data faults. |
Download the complete numeric exercise
The ZIP includes check_dataset.py, generate_fixtures.py, test_validator.py, requirements.txt, two tiny Parquet fixtures, an example field contract and the reports produced in our run. All data is independently generated synthetic data. It does not reuse robot demonstrations or represent a physical tactile sensor.
This is intentionally not a complete loadable LeRobot dataset: the fixtures omit task-text metadata, statistics, images and videos. The checker validates a small numeric snapshot using v3 field and boundary conventions. Use it to learn failure modes or adapt the checks to an export; use the official loader and your policy pipeline for subsequent compatibility checks.
The only third-party dependency is pyarrow==23.0.1. The recorded environment is CPython 3.13.3 on Windows x64. No robot, tactile sensor, GPU, ROS installation, Hugging Face account or model download is required. Code is Apache-2.0; generated fixtures and example contracts are CC0-1.0. Third-party datasets keep their own licenses.
Run the checker and inspect both outcomes
Extract the ZIP into a fresh directory and open a terminal there. The commands read the included fixtures and write JSON reports to a separate results directory. The invalid run intentionally exits with code 1; this is the expected failure demonstration. A passing run exits with code 0.
PowerShell commands below use the virtual environment directly, so activation and execution-policy changes are unnecessary. On macOS or Linux, use the second block; that command variant has not been executed in this Windows verification environment.
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe -B check_dataset.py fixtures/valid --report results/valid-report.json
.\.venv\Scripts\python.exe -B check_dataset.py fixtures/invalid --report results/invalid-report.json
.\.venv\Scripts\python.exe -B -m unittest -v test_validatorpython3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -B check_dataset.py fixtures/valid --report results/valid-report.json
.venv/bin/python -B check_dataset.py fixtures/invalid --report results/invalid-report.json
.venv/bin/python -B -m unittest -v test_validatorPASS (numeric subset): 8 frames, 2 episodes, 0 errors, 1 unknown tactile values
FAIL (numeric subset): 8 frames, 2 episodes, 17 errors, 1 unknown tactile valuesWhat the checks catch, and what a pass leaves open
The valid fixture has eight frames across two episodes in one data shard. One tactile value is explicitly unknown and is not replaced with zero. The invalid fixture removes action, shortens one state vector, repeats a timestamp and corrupts an episode endpoint. Some faults cause several diagnostics, so the error count is not a count of independent defects.
The default time tolerance is 0.0001 seconds against frame_index/fps. This is an exercise consistency tolerance, not a synchronization guarantee for real sensors. The checker loads all rows into memory, expects a complete contiguous snapshot starting at index and episode zero, and accepts one-dimensional numeric/boolean features only. It is not a streaming validator for a large production corpus.
| Check | Detected failure | Limit |
|---|---|---|
| Required fields and finite values | Missing action/state fields, null valid measurements, NaN and invalid index values. | Does not certify Arrow dtype conformance or measurement accuracy. |
| Feature dimensions | Vector lengths that disagree with features in info.json. | Cannot determine whether a joint order or coordinate frame is physically correct. |
| Episode-relative timing | Duplicate/backward times or disagreement with the declared fixed-rate grid. | Cannot measure original capture jitter, clock offsets or sensor latency from resampled rows. |
| Episode boundaries and shard references | Gaps, overlaps, inconsistent lengths, unmatched frame/episode IDs and incomplete row coverage. | Requires a complete small snapshot, not an arbitrary episode selection. |
| Explicit unsupported inputs | Video layout and nonnumeric or multidimensional feature declarations. | No video decoding, task-text mapping, statistics, train/test leakage or policy compatibility validation. |
Video findings: review, repair or retain the episode
A numeric-format pass leaves video quality open. For a separate review workflow, we inspected rdq source and maintainer reports on October 17, 2026, through commit 4dc7271. The reason-preserving expected-findings rules and swapped-camera detector are listed under Unreleased at that revision, not as stable package features. This was source review only: RoboSkin did not install or run rdq, decode dataset videos or reproduce the reported findings.
At this revision, --video samples six frames per episode per camera; --video-full decodes every frame and associates findings with episode IDs. Sampling can miss a short freeze. The cameras_swapped heuristic looks for two streams resembling each other's usual view, with an agreeing reference group; it is a review lead, not proof of incorrect wiring. A simulation renderer, calibration hold or legitimate task pause can also produce repeated frames.
Keep the dataset revision, original episode IDs, camera keys, affected time spans, finding details and before/after evidence with each decision. The choices below are our review recommendations, not an automated guarantee from rdq. Preserve the original recording and any repair mapping so later users can trace the change.
| Finding | Decision and evidence to retain |
|---|---|
| Frozen feed: review first | Compare the flagged interval with the other camera, timestamps and robot motion. Distinguish a stalled feed from a planned hold or simulation repetition. Record the affected camera and interval before deciding to repair or exclude an episode. |
| Swapped cameras: repair only after confirmation | Check camera identities against session logs and representative views before and after the suspected boundary. If confirmed, correct the affected camera-to-feature mapping in a derived revision, retain original episode IDs and document the mapping; recheck timing and downstream policy inputs. |
| Intentional negatives: retain with a reason | The maintainer reports empty-scene episodes deliberately recorded to teach a policy not to grasp. Confirm that intent with the dataset owner and task definition. Keep the episodes and rationale rather than treating every unusual image as corruption. |
Retain the reason without declaring the dataset ready
In the reviewed source, meta/rdq_expected.yaml can scope an owner-supplied reason to episode IDs and check names. A matching finding becomes informational, retaining its original severity, message and details plus the reason; unmatched episodes remain findings. This prevents that matched finding from excluding an episode from the keep list, but another unresolved finding may still exclude it. Prefer explicit episodes and narrow checks over blanket exceptions.
An expected finding is an attributed decision, not a repaired recording or a benchmark pass. The audit does not establish training readiness, task-label correctness or tactile calibration. The pinned README excludes non-joint features such as depth maps, point clouds, audio and tactile grids wider than 1,024 values from numeric loading. Inspect what was not checked, then validate the intended policy inputs and sensor calibration separately.
Add tactile fields only with an explicit consumer
observation.tactile and observation.tactile_valid are RoboSkin example field designs. They are not a universal LeRobot tactile schema, and a policy does not gain tactile support merely because these columns exist. A policy adapter must deliberately consume the representation, validity mask, timing and normalization; its training and evaluation must include that modality.
Our earlier CSV exercise uses separate time, unit and validity fields. It is an independent synthetic exercise with normalized values and its own schema, not a direct export of this arbitrary-unit fixture. To convert real logs, first segment episodes, choose a reference time axis, preserve capture/receive clock provenance, align observations to actions and document exclusions. ROS messages and bags provide transport and recording; they do not define the learning dataset or calibrate its values.
Optional: run the official numeric writer and loader
This second exercise uses LeRobot 0.6.1 to create, finalize and reload a local dataset in format v3.0. It generates its own complete numeric example; the limited PyArrow fixtures above are not presented as official-loader-ready exports. No Hub account, upload, GPU or robot is required. The script disables Hub access.
On Windows x64 with CPython 3.13.3 and uv 0.9.28, we checked 2 episodes, 8 frames, metadata totals, episode/frame/global indices, per-episode timestamps and the exact state, action and tactile arrays. Before the valid write, the official writer rejected a missing action and a wrong-shaped state vector. A second clean environment produced the same report using the supplied dependency lock.
This optional environment is substantially larger than the PyArrow-only checker: it installs the official package and CPU PyTorch dependencies. Tested versions are LeRobot 0.6.1, PyTorch 2.11.0+cpu, datasets 4.8.5, PyArrow 25.0.1 and NumPy 2.2.6. Video encoding/decoding, task-policy compatibility and training remain untested. The custom observation.tactile vector contains four synthetic normalized arbitrary values, with no implied policy support or physical unit.
uv venv .venv --python 3.13
uv pip install --python .\.venv\Scripts\python.exe --torch-backend cpu -r requirements-windows-py313.lock.txt
.\.venv\Scripts\python.exe verify_loader.py --output resultsPASS: LeRobot 0.6.1 wrote and loaded 8 numeric frames in 2 episodes (v3.0).Resolve common validation failures
A missing field usually means the export mapping or shard selection is wrong. Compare the actual Parquet columns with info.json; do not add fabricated zero vectors to make a check pass. A timestamp failure may be an episode segmentation error, a duplicated row or a mismatch between declared FPS and the export grid. Preserve the raw log while correcting the export.
Boundary failures require comparing inclusive starts and exclusive ends with the actual global row indices. Do not treat each shard as one episode. Dimension failures require checking the feature definition and controller mapping before padding or truncating vectors.
An unreadable Parquet file can be an incomplete download or a writer that was not closed. The official v3 guide calls for dataset.finalize() before upload to flush buffered metadata and close writers. The optional official-loader exercise above runs finalize(); the lightweight PyArrow checker does not use that API.
Complete Python script
This is the same check_dataset.py supplied in the download. It reads small local Parquet snapshots and writes a separate JSON report.
Read the complete checker (149 lines)
"""Validate a small numeric LeRobot v3-style snapshot, not full compatibility.
No videos, task-text mapping, statistics, dtype/codec conformance, or policy
compatibility checks. Read-only; all input is loaded into memory.
Copyright 2026 RoboSkin.ai. SPDX-License-Identifier: Apache-2.0
"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
import sys
import pyarrow.parquet as pq
SCOPE = "numeric-v3-subset; not an official LeRobot compatibility validator"
SCALARS = ("index", "episode_index", "frame_index", "task_index", "timestamp")
REQUIRED = (*SCALARS, "observation.state", "action")
TACTILE = "observation.tactile"
VALID = "observation.tactile_valid"
def finite(value):
return type(value) in (int, float) and math.isfinite(value)
def integer(value):
return type(value) is int and value >= 0
def validate(root: Path, tolerance: float = 1e-4) -> dict:
"""Return all detected errors. A pass applies only to the documented subset."""
errors = []
report = {"scope": SCOPE, "ok": False, "frames": 0, "episodes": 0,
"unknown_tactile_values": 0, "errors": errors}
if not finite(tolerance) or tolerance < 0:
errors.append("config: tolerance must be finite and non-negative")
return report
try:
info = json.loads((root / "meta/info.json").read_text(encoding="utf-8"))
if not isinstance(info, dict): raise ValueError("info.json must be an object")
features = info.get("features")
if not isinstance(features, dict): raise ValueError("features must be an object")
if info.get("codebase_version") != "v3.0":
errors.append("version: this exercise supports codebase_version v3.0 only")
fps = info.get("fps")
if not finite(fps) or fps <= 0: raise ValueError("fps must be finite and positive")
for field in REQUIRED:
if field not in features: errors.append(f"schema: missing feature {field}")
for name, spec in features.items():
if not isinstance(spec, dict): raise ValueError(f"invalid feature specification: {name}")
if spec.get("dtype") not in ("float32", "float64", "int64", "bool"):
errors.append(f"unsupported: {name} dtype {spec.get('dtype')}; numeric subset only")
shape = spec.get("shape")
if not isinstance(shape, list) or len(shape) != 1 or not integer(shape[0]) or shape[0] == 0:
errors.append(f"unsupported: {name} needs a positive one-dimensional shape")
if name in SCALARS and shape != [1]: errors.append(f"schema: {name} must have shape [1]")
if (TACTILE in features) != (VALID in features):
errors.append("schema: example tactile values and validity must be declared together")
if TACTILE in features and VALID in features and features[TACTILE].get("shape") != features[VALID].get("shape"):
errors.append("schema: tactile values and validity shapes differ")
if info.get("video_path") is not None:
errors.append("unsupported: video layout is outside this exercise")
files = sorted((root / "data").glob("chunk-*/file-*.parquet"))
ep_files = sorted((root / "meta/episodes").glob("chunk-*/file-*.parquet"))
if not files or not ep_files: raise ValueError("data and episode Parquet shards are required")
rows, locations = [], []
for file in files:
table = pq.read_table(file)
for name in set(REQUIRED) | set(features):
if name not in table.column_names: errors.append(f"fields: {file.relative_to(root).as_posix()} missing {name}")
chunk_rows = table.to_pylist()
rows.extend(chunk_rows)
locations.extend([file.resolve()] * len(chunk_rows))
episodes = [row for file in ep_files for row in pq.read_table(file).to_pylist()]
report.update(frames=len(rows), episodes=len(episodes))
if not rows or not episodes: errors.append("empty: frames and episodes must be non-empty")
for key, count in (("total_frames", len(rows)), ("total_episodes", len(episodes))):
if not integer(info.get(key)) or info[key] != count: errors.append(f"counts: {key} does not match files")
for i, row in enumerate(rows):
for key in SCALARS[:-1]:
if not integer(row.get(key)): errors.append(f"values: row {i} {key} must be a non-negative integer")
if row.get("index") != i: errors.append(f"index: row {i} must have global index {i}")
if not finite(row.get("timestamp")): errors.append(f"time: row {i} timestamp must be finite")
for name, spec in features.items():
if name in SCALARS: continue
values = row.get(name)
shape = spec.get("shape", [])
if not isinstance(values, list) or len(shape) != 1 or len(values) != shape[0]:
errors.append(f"dimension: row {i} {name} does not match {shape}")
continue
flags = row.get(VALID)
for j, value in enumerate(values):
if name == TACTILE and isinstance(flags, list) and j < len(flags) and flags[j] is False:
report["unknown_tactile_values"] += 1
continue # Unknown stays unknown; never replace it with zero.
dtype = spec.get("dtype")
good = type(value) is bool if dtype == "bool" else (type(value) is int if dtype == "int64" else finite(value))
if not good: errors.append(f"values: row {i} {name}[{j}] invalid or missing")
cursor = 0
for number, ep in enumerate(episodes):
keys = ("episode_index", "length", "dataset_from_index", "dataset_to_index", "data/chunk_index", "data/file_index")
if not all(integer(ep.get(key)) for key in keys):
errors.append(f"boundary: episode {number} missing or invalid metadata")
continue
start, stop = ep["dataset_from_index"], ep["dataset_to_index"]
if ep["episode_index"] != number or start != cursor or stop <= start or stop > len(rows) or stop-start != ep["length"]:
errors.append(f"boundary: episode {number} has a gap, overlap, count or index error")
cursor = stop
template = info.get("data_path")
if not isinstance(template, str): raise ValueError("data_path must be a string")
linked = (root / template.format(chunk_index=ep["data/chunk_index"], file_index=ep["data/file_index"])).resolve()
if not linked.is_relative_to(root.resolve()): raise ValueError("data_path escapes dataset root")
previous = None
for frame, i in enumerate(range(start, min(stop, len(rows)))):
row = rows[i]
if row.get("episode_index") != number or row.get("frame_index") != frame:
errors.append(f"boundary: row {i} episode/frame index disagrees with metadata")
if locations[i] != linked: errors.append(f"path: row {i} does not match episode data shard")
stamp = row.get("timestamp")
if finite(stamp):
if previous is not None and stamp <= previous: errors.append(f"time: episode {number} timestamp is not strictly increasing at frame {frame}")
if abs(stamp - frame/fps) > tolerance: errors.append(f"cadence: episode {number} frame {frame} differs from frame_index/fps")
previous = stamp
if cursor != len(rows): errors.append("boundary: episode metadata does not cover all rows")
except (OSError, ValueError, TypeError, KeyError, IndexError) as exc:
errors.append(f"input: {exc}")
report["ok"] = not errors
return report
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("dataset", type=Path)
parser.add_argument("--report", type=Path, required=True)
parser.add_argument("--tolerance", type=float, default=1e-4, help="seconds, default 0.0001")
args = parser.parse_args()
report = validate(args.dataset, args.tolerance)
# Reports are an explicit output; never overwrite a file inside the input.
if args.report.resolve().is_relative_to(args.dataset.resolve()):
parser.error("--report must be outside the dataset directory")
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, indent=2, allow_nan=False) + "\n", encoding="utf-8")
print(f"{'PASS' if report['ok'] else 'FAIL'} (numeric subset): {report['frames']} frames, {report['episodes']} episodes, {len(report['errors'])} errors, {report['unknown_tactile_values']} unknown tactile values")
for error in report["errors"]: print(f"- {error}")
return 0 if report["ok"] else 1
if __name__ == "__main__": sys.exit(main())
Common questions
Is this a complete LeRobot v3 validator?
No. It checks a documented numeric subset. A passing report does not certify official loader compatibility, video alignment, task metadata, statistics, training readiness or real-world validity.
Why can the timestamp go back to zero?
In the inspected writer, time is frame_index divided by FPS within each episode. An episode reset is expected; a repeated or decreasing timestamp inside the same episode is not.
Can every LeRobot policy train with these tactile fields?
No. The tactile columns and validity mask are example designs. A matching policy data adapter, representation, training procedure and evaluation are required.
Next steps
- Robotics programming learning path →
Choose a data or tactile-feedback starting point.
- Find robot and tactile datasets →
Compare fields, access conditions and original dataset licenses.
- Robot data collection and teleoperation →
Plan demonstrations, synchronization and quality review before export.
- Process tactile CSV data with Python →
Generate heatmaps and teaching contact events without hardware.