The AY-Robots public dataset directory, where LeRobot demonstration datasets are listed with their episode counts
Dataset QualityLeRobotData CurationImitation LearningSO-100Review Workflow

Quality Control for Robot Demonstrations: A Review Workflow

AY-Robots ResearchAugust 23, 202621 min read

A five-gate review workflow for LeRobot demonstration data: what to check, in what order, and what to automate. Cheap metadata checks first, human review last, with runnable commands.

What you need to know

  • Order the checks by cost. The metadata pass downloads 84 KB and runs in about a second on a 50 episode dataset once that is cached; watching video costs minutes per episode. Run the cheap gate first and let it decide what a human looks at.
  • In a LeRobot v3.0 dataset, per-episode min, max, mean, std and length already live in meta/episodes/*.parquet. You do not need to touch a single MP4 to rank episodes by how unlike the rest they are.
  • Use a robust z score built on the median absolute deviation, not a standard deviation. Two bad episodes inflate the standard deviation enough to hide themselves.
  • Flags that cluster across consecutive episodes mean the environment changed. Flags that appear on isolated episodes mean the operator slipped. The two need different fixes.
  • Published curation work reports real gains from throwing data away: CUPID reached state of the art on RoboMimic with under 33 percent of the data, and DemInf reports 5 to 10 percent improvements from mutual-information filtering.
  • No automated gate tells you whether the demonstration shows the task you actually meant. That one still needs a person, on a sample.

The problem is triage, not detection

Everybody who records LeRobot datasets eventually writes a script that finds broken episodes. The script is the easy part. The hard part is that a 300 episode dataset contains roughly 300 things you could look at, each one takes about a minute to watch properly, and you have a GPU rental waiting. So the script gets written, gets run once, produces a list of 40 suspicious episodes, and then nobody reviews the list because reviewing the list is the expensive bit.

A review workflow fixes that by ordering the checks so that every stage only receives what the stage before it could not settle. Cheap and automatic first, expensive and human last. The point is not to catch every defect. The point is to spend your review minutes on the episodes most likely to be worth reviewing, and to know what you skipped.

GateQuestion it answersCost on a 50 episode setAutomate it?
0. SchemaWill the trainer load this at allseconds, metadata onlyFully
1. StatisticsWhich episodes are unlike the rest84 KB, under 3 secondsFully
2. Targeted replayIs the flagged episode actually brokenminutes, flagged episodes onlyPartly
3. Sampled reviewDoes the demonstration show the task you meantabout a minute per episodeNever fully
4. Edit and re-checkDid the prune leave a coherent datasetminutes, metadata onlyFully

The rest of this article walks each gate with the commands that run it. The worked example is a real public dataset, lerobot/svla_so101_pickplace: 50 episodes, 11,939 frames at 30 fps, two cameras, six joints on an SO-100 class follower arm. You can point every command below at your own dataset instead, or at anything on the public dataset directory.

The AY-Robots public dataset directory listing recorded LeRobot datasets with episode counts
The /directory page. Before you audit your own recording, it helps to have a reference dataset of the same task and arm to compare distributions against.

Gate 0: does the trainer accept the format at all

This gate takes ten seconds and saves the most wasted GPU hours, because the failure it catches happens after the pod is already billing. Read meta/info.json and check four things: the codebase version, the frame rate, the feature schema, and the episode count.

bash
python - <<'EOF'
import json
from huggingface_hub import hf_hub_download

p = hf_hub_download("lerobot/svla_so101_pickplace", "meta/info.json", repo_type="dataset")
info = json.load(open(p))
print(info["codebase_version"], info["robot_type"], info["fps"], "fps")
print(info["total_episodes"], "episodes,", info["total_frames"], "frames")
for k, v in info["features"].items():
    print(" ", k, v["dtype"], v["shape"])
EOF
Everything gate 0 needs is in one JSON file.
text
v3.0 so100_follower 30 fps
50 episodes, 11939 frames
  action float32 [6]
  observation.state float32 [6]
  observation.images.up video [480, 640, 3]
  observation.images.side video [480, 640, 3]
  timestamp float32 [1]
  frame_index int64 [1]
  episode_index int64 [1]
  index int64 [1]
  task_index int64 [1]
Actual output for lerobot/svla_so101_pickplace, checked 2026-08-24.

Three checks fail here often enough to be worth naming. First, the codebase version against the policy you plan to train: GR00T N1.7 and GR00T N1.5 want LeRobot v2.0 or v2.1, and a v3.0 dataset crashes the GR00T loader, so it has to be converted down first (see dataset rejected as v3). ACT, SmolVLA and Pi0.5 take v3.0. Second, the episode count against the minimum the policy needs: 30 for SmolVLA, 50 for ACT, Pi0.5, GR00T N1.5 and GR00T N1.7. Third, the camera keys, because a policy config that names observation.images.front will not find a dataset that recorded observation.images.up.

The version mismatch that eats a day

A v3.0 to v2.1 conversion changes the file layout but not the content, so a dataset that passes gate 0 in one format still has the same defects in the other. Convert first, audit second. Auditing the v3.0 copy and then converting means your episode indices may no longer line up with what you reviewed. Both formats are covered in the dataset documentation.

Gate 1: the statistics pass that costs 84 KB

This is the gate most people skip, and it is the one with the best ratio of information to effort. A LeRobot v3.0 dataset stores per-episode summary statistics in the metadata itself, in meta/episodes/chunk-000/file-000.parquet. Reading that file gives you, for every episode and every feature, the min, max, mean, std and count, plus the episode length in frames, the task string, and the pointers into the shared shards: a chunk and file index, the row range into the parquet (dataset_from_index and dataset_to_index), and the start and end timestamp inside each MP4.

The size difference is the whole argument for running this first. For the example dataset the full repository is 86.1 MB, of which 85.7 MB is the two MP4 shards. The entire meta/ directory is 84,121 bytes, about one tenth of one percent of the repository. You can rank every episode in the dataset before downloading any video.

python
#!/usr/bin/env python3
"""Gate 1: rank LeRobot v3.0 episodes from metadata alone. No video is decoded."""
import json, sys
import numpy as np
import pyarrow.parquet as pq
from huggingface_hub import snapshot_download

repo = sys.argv[1] if len(sys.argv) > 1 else "lerobot/svla_so101_pickplace"
root = snapshot_download(repo, repo_type="dataset", allow_patterns=["meta/*"])
info = json.load(open(f"{root}/meta/info.json"))
t = pq.read_table(f"{root}/meta/episodes").to_pandas()

def flat(v):                    # image stats arrive as nested per-channel arrays
    a = np.asarray(v, dtype=object).ravel()
    return np.array([flat(x)[0] if isinstance(x, np.ndarray) else float(x) for x in a])

def col(name):
    return np.stack([flat(v) for v in t[name]])

def rz(x):                      # robust z score: median absolute deviation, not std
    med = np.median(x)
    mad = np.median(np.abs(x - med)) or 1e-9
    return (x - med) / (1.4826 * mad)

joints = info["features"]["action"]["names"]
cams = [k.split("observation.images.")[1] for k in info["features"] if ".images." in k]
length = t["length"].to_numpy().astype(float)
span = col("stats/action/max") - col("stats/action/min")

flags = {"length": np.abs(rz(length)) > 3}
for j, name in enumerate(joints):
    flags["span:" + name.split(".")[0]] = np.abs(rz(span[:, j])) > 3
for cam in cams:
    lum = col(f"stats/observation.images.{cam}/mean").mean(1)
    flags["light:" + cam] = np.abs(rz(lum)) > 3

for i in range(len(t)):
    hit = [k for k, v in flags.items() if v[i]]
    if hit:
        print(f"episode {i:>4}  {int(length[i]):>4} frames  {', '.join(hit)}")

n = sum(any(v[i] for v in flags.values()) for i in range(len(t)))
print(f"\n{n}/{len(t)} episodes flagged for review, {len(t) - n} clean")
audit.py: ranks episodes from metadata only, no video decode. Tested against lerobot on 2026-08-24.
bash
$ python audit.py lerobot/svla_so101_pickplace
episode   13   231 frames  span:gripper
episode   27   203 frames  light:up
episode   28   224 frames  light:up, light:side
episode   30   288 frames  span:gripper, light:side
episode   37   227 frames  span:gripper

5/50 episodes flagged for review, 45 clean
Warm run: 0.9 seconds wall clock. A cold run adds the 84 KB metadata download, so what you measure there is your connection, not the arithmetic.

Five episodes out of fifty go to a human. Forty-five do not. That is the entire job of gate 1, and the output is already more useful than a flat list, because of how the flags group.

  • Episodes 27, 28 and 30 are flagged on brightness, and they are consecutive. Consecutive flags on an environment feature are not three bad operators, they are one lighting change: a lamp switched on, a blind moved, the sun came round. Peak to peak brightness across the whole run is 9.2 percent on the up camera and 12.3 percent on the side camera.
  • Episodes 13, 30 and 37 are flagged on gripper travel, and they are scattered. Scattered flags on an action feature are operator variance: the gripper opened less far on those attempts. Median gripper span across the set is 23.22 units, the smallest is 17.47.
  • Episode 30 carries two independent flags. Episodes that trip more than one rule are where a reviewer should start.
Why the median absolute deviation and not the standard deviation

A standard deviation is computed from the same numbers you are trying to screen. Two frozen episodes with an extreme mean pull the standard deviation up far enough that their own z score drops below the threshold, and they pass. The median absolute deviation has a breakdown point of 50 percent: up to half the dataset can be garbage before the estimate moves. The 1.4826 factor rescales it so that a threshold of 3 means roughly what it means for a normal distribution.

One honest limit of this gate, visible in the metadata itself. The count field for the image statistics is 100 on every episode, while the count for observation.state equals the episode length (303 for episode 0). Image statistics are computed from a sample of 100 frames per episode. That is plenty to detect a lighting shift across a whole episode. It is useless for finding a single frozen frame or a two second camera dropout, which is a different check that does require decoding video.

The other structural limit: summary statistics describe the envelope, not the path. Two episodes with identical per-joint min and max can trace completely different trajectories, one smooth and one full of the jerk that comes from a stuttering leader-follower link. Gate 1 will not separate them. That is what gate 2 is for.

Gate 2: replay only what gate 1 flagged

Now you decode video, but only for five episodes instead of fifty. LeRobot ships a viewer that renders camera streams, joint states and actions on a shared timeline.

  1. 1
    Open the worst offender first

    Episode 30 tripped two rules, so it goes first. The viewer opens a rerun.io window with every camera and every joint on one timeline. It sits behind an optional extra: install lerobot[viz] first, or the command exits telling you rerun-sdk is missing. The same extra ships the Foxglove backend used in step three.

    bash
    lerobot-dataset-viz \
        --repo-id lerobot/svla_so101_pickplace \
        --episode-index 30
  2. 2
    Point it at a local recording instead

    If the dataset came off your own machine rather than the Hub, add --root and switch the mode. This is the normal case right after a recording session with the desktop client.

    bash
    lerobot-dataset-viz \
        --repo-id my-user/pick-place-run3 \
        --root ./my_local_data_dir \
        --mode local \
        --episode-index 30
  3. 3
    Scrub instead of watching, if you prefer a timeline

    The Foxglove display mode starts a WebSocket server on port 8765 and serves the episode as a seekable timeline, which is faster than watching linearly when you already know roughly where the problem is.

    bash
    lerobot-dataset-viz \
        --repo-id lerobot/svla_so101_pickplace \
        --episode-index 30 \
        --display-mode foxglove
    # then connect Foxglove to ws://127.0.0.1:8765
  4. 4
    Write the verdict down as you go

    Keep a plain text file with one line per reviewed episode: index, verdict, reason. You will need the index list intact for gate 4, and you will want the reasons when you decide whether to re-record.

    bash
    cat >> review.tsv <<'EOF'
    30	drop	light changed mid-episode, gripper never fully opened
    13	keep	short grip travel but the cube was already close
    27	keep	brightness only, task completed cleanly
    EOF

One more viewer is worth knowing, and it is worth being clear that it is one tool and not two. The hosted LeRobot Dataset Visualizer Space is the deployed instance of the open source huggingface/lerobot-dataset-visualizer repo (Apache-2.0, same README, run it yourself with bun install and bun dev). Either way you get the same app: synchronized video and time series charts, and a filtering panel that flags low movement, jerky motion and outlier episode length, then exports the flagged episode ids as a ready to run LeRobot CLI command. Its heuristics overlap with gate 1, but it is built for looking at one episode closely rather than ranking fifty at once. Use it when you want to look, not when you want to rank. The upstream question of how to record demonstrations that need less of this in the first place is covered in collecting high quality VLA training data.

Gate 3: the sampled human review

Everything so far finds episodes that are statistically odd. None of it finds an episode where the arm moved smoothly, the lighting was fine, the gripper closed properly, and the operator picked up the wrong object. That failure looks completely normal in every summary statistic, and it is the one that quietly teaches your policy the wrong thing.

What the reviewer seesVerdictActionWhy
Task completed, clean motionKeepnothingThis is the reference class
Task completed, recovered from a fumbleKeepnothingRecovery behaviour is worth learning, not a defect
Task completed, wrong object or wrong placeRelabel or dropmodify_tasks, or deleteThe language annotation no longer matches the trajectory
Task not completed, operator gave upDropdelete_episodesTeaches the policy to stop halfway
Arm moved but nothing was in frameDropdelete_episodesCamera framing or object placement error
Whole run drifted (lighting, camera nudged)Re-recordstart a new sessionDeleting the drifted half throws away most of your data

Sample size, not full coverage. At 30 fps, the 11,939 frames of the example dataset are 6.6 minutes of footage per camera, 13 minutes for both. That is watchable. Scale to 500 episodes and it is more than an hour per camera, which nobody does twice. A workable rule: review every episode gate 1 flagged, plus a fixed random 10 percent of the unflagged ones. The random sample is not there to catch defects. It is there to measure your false negative rate, so you find out whether gate 1 is calibrated for your task before you trust it on the next dataset.

If somebody else recorded the data, this gate is also where operator variance shows up. DROID was collected by 50 people across three continents over 12 months, 76,000 trajectories in 564 scenes; the RoboTurk work collected 137.5 hours from remote workers. At that scale the reviewer is not checking whether a single episode is broken, they are checking whether one operator's habit has become a systematic bias in the data. The same applies at small scale as soon as two people take turns on the teleoperation station.

The AY-Robots teleoperator page showing the SO-100 arm that remote operators drive to record demonstrations
The /teleoperator page. When more than one person records for the same dataset, the review gate has to check for per-operator bias, not just per-episode defects.

Gate 4: act on the verdict, then re-check

LeRobot has a first-party editing CLI. It handles the bookkeeping that makes manual deletion dangerous: renumbering the remaining episodes, rewriting the parquet and MP4 offsets, and pruning tasks that no episode references any more.

bash
# delete every episode you marked "drop", in ONE call, into a NEW repo
lerobot-edit-dataset \
    --repo_id my-user/pick-place-run3 \
    --new_repo_id my-user/pick-place-run3-clean \
    --operation.type delete_episodes \
    --operation.episode_indices "[30, 41, 44]"

# fix a language annotation instead of deleting the episode
lerobot-edit-dataset \
    --repo_id my-user/pick-place-run3-clean \
    --operation.type modify_tasks \
    --operation.episode_tasks '{"12": "Pick up the blue cube and place it in the bin"}'

# confirm what survived
lerobot-edit-dataset \
    --repo_id my-user/pick-place-run3-clean \
    --operation.type info \
    --operation.show_features true
lerobot-edit-dataset, documented operations as of the current LeRobot docs, checked 2026-08-24.
Delete once, in one call

Two traps here, both silent. First: omit --new_repo_id and delete_episodes modifies the original dataset in place. Second, and worse: after a delete, the remaining episodes are renumbered 0, 1, 2 and so on in their original order. Your review list of indices is now wrong. If you delete episode 13 and then run a second call to delete episode 30, you have just deleted what used to be episode 31. Collect every index first, delete them in a single call, and re-run the audit against the new repo rather than trusting the old list. Note also that modify_tasks is always in place and ignores --new_repo_id entirely.

Then run gate 1 again against the cleaned repo. This is not ceremony. Pruning changes the reference distribution, so episodes that sat just inside the threshold before may sit outside it now, and, more usefully, you find out whether you removed a defect or removed the evidence of one. If your gripper-span flags disappear entirely after deleting three episodes, the problem was three episodes. If new ones appear, the problem is your calibration and no amount of deleting will fix it. That distinction is the difference between a dataset you can train on and a recording session you need to repeat.

What the published curation work actually found

It is easy to treat pruning as damage control. The research points the other way: filtering is a lever, and a fairly strong one. Three results are worth carrying into your own workflow.

WorkMethodReported result
Belkhale, Cui and Sadigh, 2023Formalises data quality as action divergence and transition diversityState diversity is not always beneficial; more varied data can make a policy worse
CUPID, 2025Influence functions ranked against evaluation rolloutsState of the art diffusion policies on RoboMimic from under 33 percent of the data
DemInf, 2025k-nearest-neighbour mutual information on VAE embeddings of states and actions5 to 10 percent improvement on RoboMimic, gains on real ALOHA and Franka setups
Northcutt et al., 2021Confident learning on ten standard test setsAt least 3.3 percent label errors on average, at least 6 percent of the ImageNet validation set

The Belkhale result is the one that changes how you read your own flags. If diversity were unambiguously good, every unusual episode would be worth keeping and gate 1 would be pointing you at your best data. It is not that simple: the paper separates variation in the action you took at a given state, which hurts, from variation in how the world responds, which is just noise you have to model. An episode where the operator took a different but equally valid route to the goal is diversity worth keeping. An episode where the operator took a worse route to the same goal is action divergence, and it makes the policy hesitate at exactly that state. Both look identical in a min-max table. Only a person watching can tell them apart, which is why gate 3 does not go away.

The CUPID and DemInf numbers come with a caveat worth stating plainly, because it constrains what you can do before your first training run. CUPID ranks demonstrations by their influence on a policy's expected return, which requires evaluation rollouts, which requires a trained policy. You cannot run it on day one. DemInf is unsupervised and needs no policy, which makes it the closer analogue to what gate 1 does, just with a learned embedding instead of hand-written rules. Neither replaces looking at the video.

Pruning aggressively
Advantages
  • Directly reported gains: under 33 percent of curated data matched state of the art in the CUPID experiments
  • Shorter training runs, because fewer frames per epoch on a fixed step budget means more passes over the good data
  • A smaller, cleaner set is far easier to reason about when a policy misbehaves later
  • Removing systematically bad episodes removes a systematic bias, not just noise
Trade-offs
  • Below the policy minimum you cannot train at all: 30 episodes for SmolVLA, 50 for ACT, Pi0.5 and both GR00T variants
  • Pruning to a narrow distribution produces a policy that only works in the exact setup you kept, a failure documented on the policy only works in one setup page
  • Recovery and correction behaviour looks like a defect to every automated rule, and deleting it removes the policy's ability to recover
  • Every rule you tune against one dataset is a rule you have overfitted to that dataset's lighting and task

Two ways to run this

Everything above runs on a laptop against a Hub repo or a local folder. The full loop, start to finish:

bash
pip install "lerobot[viz]"   # the viz extra is what ships lerobot-dataset-viz

# gate 0 + 1: metadata only, no video
python audit.py my-user/pick-place-run3

# gate 2: replay only the flagged indices
lerobot-dataset-viz --repo-id my-user/pick-place-run3 --episode-index 30

# optional: automated scoring across nine dimensions
git clone https://github.com/RoboticsData/score_lerobot_episodes.git
cd score_lerobot_episodes && pip install -e .
python score_dataset.py --repo_id my-user/pick-place-run3 --threshold 0.5

# gate 4: prune into a new repo
lerobot-edit-dataset \
    --repo_id my-user/pick-place-run3 \
    --new_repo_id my-user/pick-place-run3-clean \
    --operation.type delete_episodes \
    --operation.episode_indices "[30, 41, 44]"
The whole workflow, minus the part where a human watches five videos.

The score_lerobot_episodes toolkit (Apache-2.0) is the closest thing to an off-the-shelf gate 1 and gate 2 combined. It scores visual clarity, smoothness from the second derivative of joint angles, path efficiency, collision spikes, joint stability, gripper consistency, actuator saturation, runtime against a nominal duration, and, with --vision_type vlm_gemini, task success graded by a vision-language model. Default retention threshold is 0.5. It writes per-episode scores to a JSON file and can emit a filtered copy of the dataset. It downloads and decodes video, so it is minutes not seconds, which is exactly why it belongs after the metadata pass rather than instead of it.

You own the thresholds

Every number in this workflow (robust z of 3, a 10 percent random sample, a 0.5 score threshold) is a starting point, not a constant. Run the audit against a dataset you already trained on successfully and see which of its episodes get flagged. If your known-good set trips five rules, your thresholds are too tight for your task.

What to automate, and what never to automate

The split is not about difficulty. It is about whether the check has a ground truth that exists independently of your intent.

CheckGround truthAutomate
Schema, version, fps, feature namesDefined by the formatAlways
Episode length outliersThe rest of the datasetAlways
Per-joint travel outliersThe rest of the datasetAlways
Brightness and contrast driftThe rest of the datasetAlways
Frozen frames, camera dropoutsFrame-to-frame differenceAlways, but it costs video decode
Motion smoothness, jerkSecond derivative of joint anglesAlways, with a task-tuned threshold
Did the gripper actually graspContact, which you did not recordProxy only, verify by eye
Did the episode complete the taskYour definition of the taskProxy only, or a VLM grader you also have to check
Is this the task you meant to teachOnly in your headNever
The cheapest quality control is upstream

Every gate here is downstream repair. The checks that pay best are the ones you run before the session: fixed camera mounts that cannot be nudged, a lighting setup that does not depend on the time of day, a calibration pass at the start of every session, and a written task description you read out loud before recording so the language annotation and the trajectory cannot drift apart. The recording walkthrough and the SO-100 data collection guide cover the setup side.

What this workflow cannot tell you

Being honest about the boundary matters more than the checks themselves, because a green audit report creates confidence that the audit did not earn.

  • It cannot tell you the task is learnable. A dataset can pass every gate and still be impossible to learn from, because the information the policy needs (an occluded object, a force, a state the cameras never see) was never recorded. Nothing in the metadata reveals a missing observation.
  • It cannot predict closed-loop success. Every metric here is computed on the demonstration in isolation. The published methods that do relate data to closed-loop return, such as CUPID, need evaluation rollouts from a trained policy first.
  • It cannot separate a hard episode from a bad one. An episode that took 40 percent longer because the object started in an awkward pose is exactly the data you want; the same statistics describe an episode where the operator was distracted.
  • It cannot fix a policy that fails at inference but trained fine. That is usually an environment gap or a latency problem rather than a data defect, and it lives on the loss falls but the policy does nothing and policy freezes mid-motion pages.
  • It does not document the dataset. The Datasheets for Datasets proposal is worth following here: record who collected it, on what hardware, under what lighting, with what task wording, and what you pruned and why. Six months later that file is worth more than the audit output.
The AY-Robots pricing page showing GPU tiers and per-run cost ranges for policy training
The /pricing page. A second training run to verify a prune actually helped costs 1 to 3 USD on the 24 GB tier, which is what makes the re-check in gate 4 affordable.

One last framing. The reason to run this before renting a GPU is not that the GPU is expensive. On the 24 GB tier a SmolVLA run is a couple of dollars, and even a GR00T N1.7 run on an A100 lands between 4 and 12 USD. The reason is attribution. If you train on unaudited data and the policy is bad, you cannot tell whether the data was bad, the hyperparameters were wrong, or the task is hard, so the next thing you do is guess. An audited dataset does not guarantee a good policy. It guarantees that when the policy is bad, you are debugging something else. That is worth three seconds and 84 KB.

The policy is bad and you do not know why

The failure mode index maps what you are seeing on the arm back to the cause, from datasets rejected as v3 to a gripper that never closes to a policy that only works in one setup.

Open the failure modes
How many episodes should I review by hand?

Review every episode the automated pass flagged, plus a fixed random 10 percent of the ones it did not. The flagged set is the work; the random sample exists to measure your false negative rate. If the random sample keeps turning up defects the script missed, your thresholds are too loose and you fix the script rather than reviewing more.

Should I delete a flagged episode or keep it?

Delete only if a person watched it and the task was not completed, or was completed on the wrong object. Statistical oddness on its own is not a reason. An episode where the operator fumbled and recovered is valuable, because recovery behaviour is exactly what a policy needs and cannot invent. Deleting it makes the policy brittle rather than clean.

How few episodes can I get away with after pruning?

The trainable policies have hard minimums: 30 episodes for SmolVLA, 50 for ACT, Pi0.5, GR00T N1.5 and GR00T N1.7. Those are floors for the run to be worth starting, not targets. If pruning takes you near the floor, re-record rather than train on the remainder, because a set that is both small and narrow produces a policy that only works in the one setup you kept.

Can I run the metadata audit on a v2.1 dataset?

The layout differs. LeRobot v3.0 keeps per-episode statistics in chunked parquet under meta/episodes/, while v2.1 keeps one file per episode and stores episode statistics separately. The rules are identical, only the reader changes. If you are training GR00T you will be on v2.1 anyway, since a v3.0 dataset crashes the GR00T loader and has to be converted down first.

Is an automated quality score enough on its own?

No, and the tools that offer one do not claim it is. score_lerobot_episodes gives a 0 to 1 aggregate across nine dimensions with a default cut at 0.5, which is a good ranking and a bad verdict. Nothing in it knows what task you intended, and an episode that scores well while demonstrating the wrong behaviour is the single most damaging thing in a training set.

Does the audit have to run before every training run?

Before the first run on a dataset, yes. After that, run it again whenever the dataset changes: after a prune, after merging in a second recording session, after a format conversion. Merges are the common trap, because two individually clean sessions recorded weeks apart can have completely different lighting distributions and the merged set is bimodal in a way neither half was.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started