The AY-Robots teleoperator page, showing remote operator work driving a real SO-100 robot arm from anywhere in the world
Data collectionLeRobotDROIDMulti-siteDataset mergingSO-100

Fleet Data Collection: One Task, Several Locations

AY-Robots ResearchAugust 23, 202619 min read

DROID standardised the robot at 13 institutions and let the scenes vary. Here is what the LeRobot merge really enforces, what it silently accepts, and how to plan a multi-site recording fleet.

One arm on one desk produces a policy that works on that desk. That is the most repeatable result in low-cost manipulation learning, and it is why teams start looking for a second room. Recording the same task at three sites is not three times the data. It is a different kind of data, and it only pays off if the three recordings pour into one LeRobot dataset without anyone rewriting metadata by hand.

This page covers the mechanics: which fields the merge refuses to reconcile, which differences it accepts in silence, what the DROID team standardised across 13 institutions, and the commands that turn three site folders into one training set. Every LeRobot detail comes from lerobot 0.6.2 on main, read on 24 August 2026. Upstream moves, so check the version before copying flags.

What you need to know

  • Three things block a merge outright: fps, robot_type, and the feature dictionary (camera key names, tensor shapes, joint name lists). LeRobot raises ValueError and stops.
  • The video codec is compared too. video.codec and video.pix_fmt are read off the stream, not exempted as encoder settings, so two sites whose machines pick different encoders will not merge. Pin --dataset.rgb_encoder.vcodec.
  • Everything a policy needs in order to generalise is free to vary: lighting, background, table height, camera pose, operator, object instance, clutter.
  • DROID shipped the same Franka Panda, two Zed 2 cameras and a 15 Hz control loop to 13 institutions, then let 50 collectors choose their own scenes across 52 buildings. Its ablation: 7362 trajectories from the 20 densest scenes lost to 7362 uniformly sampled diverse ones out of distribution.
  • Merge with lerobot-edit-dataset --operation.type merge. It pools statistics correctly, but unifies task strings by exact match, so wording and casing must be agreed in advance.
  • You cannot hand a list of repo ids to lerobot-train. make_dataset raises NotImplementedError for MultiLeRobotDataset, so merging first is not optional.
  • GR00T is the exception: its fine-tune entry point takes several dataset roots separated by os.pathsep, with --ds-weights-alpha controlling the mixture.

What recording in several places actually buys

The strongest public evidence comes from DROID, designed as a distributed collection effort rather than a single-lab dataset. The team built one hardware platform and replicated it 18 times: a Franka Emika Panda 7 DoF arm with a Robotiq 2F-85 gripper, two adjustable Zed 2 stereo cameras on tripods, a wrist-mounted Zed Mini, all on a height-adjustable standing desk with wheels so the station could be pushed into a different room. Actions were recorded in both joint space and end-effector space at 15 Hz.

QuantityDROID valueWhat it implies for a small fleet
Successful trajectories76,000A further 16k were labelled not successful. They ship in the release but do not count toward the size of DROID
Interaction time350 hoursCollected over 12 months, so throughput per station was modest
Scenes564Collectors switched scene roughly every 20 minutes, up to about 100 trajectories per scene
Buildings52Scene diversity came from moving between buildings, not from rearranging one table
Tasks86A randomly sampled task per episode, so easy ones did not dominate. The arXiv listing abstract still says 84
Institutions13Identical hardware at every one
Robots18One platform design, replicated, updates rolled out fleet-wide
Data collectors50North America, Asia and Europe
Camera viewpoints1417Third-person cameras were deliberately moved and re-calibrated during collection

The number that matters is not the headline size, it is the ablation in section V-C, "How important is the scene diversity in DROID?". The authors built two subsets of equal size: 7362 trajectories from the 20 scenes with the most demonstrations each, against 7362 successful demonstrations sampled uniformly at random. Same trajectory count, very different scene coverage, and the diverse subset won out of distribution. In the headline comparison, across 6 tasks in 4 locations, co-training with full DROID beat the next best method by 22 percent absolute success rate in distribution and 17 percent out of distribution.

Hold on to that when someone proposes a second arm in the same lab. Two arms in one room give you throughput. Two arms in two buildings give you throughput plus the thing that actually fixes a policy that only works in one setup. If the budget covers exactly one extra arm, the second room is usually the better spend.

The three fields that must be identical

LeRobot does not guess. Before it copies a frame, the merge loads the metadata of every source and compares three things against the first in the list. If any differ, it raises and stops. The check itself, from src/lerobot/datasets/aggregate.py in lerobot 0.6.2:

python
fps = all_metadata[0].fps
robot_type = all_metadata[0].robot_type
features = all_metadata[0].features

for meta in tqdm.tqdm(all_metadata, desc="Validate all meta data"):
    if fps != meta.fps:
        raise ValueError(f"Same fps is expected, but got fps={meta.fps} instead of {fps}.")
    if robot_type != meta.robot_type:
        raise ValueError(
            f"Same robot_type is expected, but got robot_type={meta.robot_type} instead of {robot_type}."
        )
    if not features_equal_for_merge(features, meta.features):
        raise ValueError(
            f"Same features is expected, but got features={meta.features} instead of {features}."
        )
validate_all_metadata in lerobot 0.6.2. Three comparisons, three ways a fleet dataset fails to assemble.
PropertyMerge behaviourWhere it comes from
fpsValueError if any source differsThe camera config at record time, written to meta/info.json
robot_typeValueError if any source differs--robot.type on the record command
Set of feature keysValueError if the key sets differ at allCamera names from --robot.cameras, prefixed observation.images.
Feature dtypeValueError if it differsvideo versus image storage, float32 versus float64 state
Feature shapeValueError if it differsCamera resolution, e.g. [480, 640, 3], and state or action vector length
Feature names listValueError if it differsJoint naming, e.g. main_shoulder_pan versus shoulder_pan.pos
video.codec, video.pix_fmtValueError if either differsDerived from the stored stream, so the encoder each machine actually used
Six encoder tuning keys: g, crf, preset, fast_decode, extra_options, video_backendIgnored during validation, reconciled afterwardsVIDEO_ENCODER_INFO_KEYS in configs/video.py. Keys the sources disagree on become null, with a warning
Task stringsNot validated. Unified by exact string match--dataset.single_task, or whatever the operator typed

The encoder exemption is narrower than it looks. features_equal_for_merge strips exactly six keys from a video feature before comparing: video.g, video.crf, video.preset, video.fast_decode, video.extra_options and video.video_backend. Two keys people assume are in that set are not. video.codec and video.pix_fmt are derived from the stored stream, so they stay in the comparison, and the recorder's vcodec resolves per machine: left on auto it takes the first available hardware encoder and falls back to libsvtav1 when there is none. Two sites running the same script on different machines can write two different codec strings, and that is a ValueError at merge time, not a harmless difference. Resolution is not exempt either, since it lives in the feature shape.

Two datasets published by the LeRobot team itself will not merge with each other

lerobot/svla_so100_pickplace and lerobot/svla_so101_pickplace are both 50-episode SO-100 class pick-and-place sets, both LeRobot v3.0, both 30 fps, both 640x480, both av1. They still carry three independent merge blockers, although validate_all_metadata only reports the first one it reaches: robot_type is so100 against so100_follower, the joint names are main_shoulder_pan style against shoulder_pan.pos style, and the camera keys are top plus wrist against up plus side. Fix the robot_type and the next run fails on the features instead. If the reference datasets drift this far apart, three volunteers recording in three cities certainly will.

json
// lerobot/svla_so100_pickplace  meta/info.json (condensed)
"robot_type": "so100",  "codebase_version": "v3.0",  "fps": 30,
"features": {
  "action": { "names": ["main_shoulder_pan", "main_shoulder_lift", "main_elbow_flex",
                        "main_wrist_flex", "main_wrist_roll", "main_gripper"] },
  "observation.images.top":   { "shape": [480, 640, 3], "info": { "video.codec": "av1" } },
  "observation.images.wrist": { "shape": [480, 640, 3], "info": { "video.codec": "av1" } }
}

// lerobot/svla_so101_pickplace meta/info.json (condensed)
"robot_type": "so100_follower",  "codebase_version": "v3.0",  "fps": 30,
"features": {
  "action": { "names": ["shoulder_pan.pos", "shoulder_lift.pos", "elbow_flex.pos",
                        "wrist_flex.pos", "wrist_roll.pos", "gripper.pos"] },
  "observation.images.up":   { "shape": [480, 640, 3], "info": { "video.codec": "av1" } },
  "observation.images.side": { "shape": [480, 640, 3], "info": { "video.codec": "av1" } }
}
Condensed from the two published meta/info.json files, fetched 24 August 2026. Same fps, same shapes, same codec, and three independent merge blockers anyway.

What should vary, and by how much

Everything the validator does not look at is fair game, and most of it should move. DROID put this in the tooling rather than the guidelines: the collection GUI periodically prompted the operator to perform a randomly sampled scene augmentation, drawn from nudges to the mobile base, moving and re-calibrating the third-person cameras, changing the room lighting, and adding or removing items. Diversity was a scheduled interruption, not a hope.

  • Room, table, and background. This is the whole point of a second site and costs nothing extra.
  • Lighting. Window light at different times of day beats one ring light in three rooms.
  • Third-person camera pose. DROID ended up with 1417 distinct viewpoints and treated that as a headline feature.
  • Object instances. Same task, different mug, different cube colour.
  • Clutter. DROID collectors were told to pick scenes with a healthy amount of it.
  • Operator. Different hands produce different approach angles and pause patterns.
  • Start pose randomisation, within whatever range your gripper can recover from.
The AY-Robots recording tutorial page, showing the steps for capturing a LeRobot dataset from a teleoperation session
The recording walkthrough on /learn/record-your-first-dataset. Every site should follow the same version of this page.

One variation looks harmless and is not: wrist camera mounting. The wrist view is what most policies use to decide when to close the gripper, so a rotated wrist camera at one site is not scene diversity, it is a conflicting sensor definition wearing the same feature key. Nothing in the merge catches it, because the shape and the key name are unchanged. Fix the mount with a printed part and a photograph in the contract sheet.

Write the site contract before anyone records

The merge validator is a late, blunt check: it tells you the datasets are incompatible after three people have each spent a week recording. The cheap version is a one-page contract every site signs off before episode zero, plus a five-minute verification run per site that produces a metadata dump you can diff.

Contract itemFixed byVerify with
Arm model and firmwareBuying the same arm, e.g. all sites on the SO-100Photo of the build plus a calibration file per arm
robot_type stringThe exact --robot.type value, written down verbatimjq -r .robot_type meta/info.json
Camera count and key namesThe --robot.cameras dictionary, copy-pasted between sitesjq -r '.features | keys' meta/info.json
Camera resolutionwidth and height in the camera configThe shape field of each observation.images.* feature
Video codec--dataset.rgb_encoder.vcodec pinned to one value, never left on autoThe video.codec and video.pix_fmt keys under each camera feature
fpsfps in the camera configjq -r .fps meta/info.json
Joint namingSame lerobot version at every siteThe names list under action and observation.state
Task string--dataset.single_task, agreed character for charactermeta/tasks.parquet
Episode length policyA written rule, e.g. stop at 20 seconds or on successAverage episode time from the info operation
Success labellingA written rule for what counts as a successYour own notes. LeRobot carries no success flag by default
Check the power supply at every site, individually

A fleet means arms assembled by people you are not standing next to. Feetech STS3215 servos on the SO-100 and SO-101 run on 7.4 V. Feeding them 12 V destroys them. The Koch v1.1 uses 5 V and 12 V rails and the LeKiwi mixes a 7.4 V arm with a 12 V base, so a shared box of power bricks across a mixed fleet is a genuine hazard. Label every brick, and check the label at each site before anyone plugs anything in.

Recording the same task at several sites at once
Advantages
  • Scene diversity, which the DROID ablation shows matters more than raw trajectory count at equal size
  • Parallel throughput: three operators produce three times the episodes in the same week
  • A natural held-out site, if you decline to merge one of them
  • Hardware failures do not stop the effort, because the other sites keep recording
  • Different operators contribute real variation in approach angle and timing
Trade-offs
  • Every site is one more chance for a metadata mismatch that only surfaces at merge time
  • Calibration drift is invisible remotely and pollutes the pooled normalisation statistics
  • Task strings diverge unless someone owns them centrally
  • Coordinating three people is a management job, not a technical one
  • Storage and transfer cost multiplies, and video is the bulk of it

Merging: the actual commands

The tool is lerobot-edit-dataset, a console script in the lerobot package. It exposes nine operations: delete_episodes, split, merge, remove_feature, modify_tasks, convert_image_to_video, recompute_stats, reencode_videos and info. Merge runs aggregate_datasets underneath. Note the ordering: fix the strings and drop the bad episodes first, because the merge rewrites episode indices and afterwards you no longer know which came from where.

  1. 1
    Install and read all three datasets before touching anything

    The info operation prints repo id, total episodes, total tasks, total frames, average frames per episode, average episode time, fps and size on disk. With show_features it dumps the feature dictionary as JSON, which is what you diff between sites. It does not print robot_type, so read that from meta/info.json separately.

    bash
    pip install 'lerobot[dataset]'
    
    for SITE in berlin lisbon tallinn; do
      echo "===== $SITE"
      lerobot-edit-dataset \
        --repo_id myorg/pickplace_$SITE \
        --operation.type info \
        --operation.show_features true
      jq -r '.robot_type, .fps' ~/.cache/huggingface/lerobot/myorg/pickplace_$SITE/meta/info.json
    done
  2. 2
    Normalise the task strings

    The merge builds its task table with pd.concat(...).index.unique(), so two spellings of one instruction survive as two tasks and a task-conditioned policy treats them as two goals. This operation modifies in place, so copy first if you care.

    bash
    lerobot-edit-dataset \
      --repo_id myorg/pickplace_lisbon \
      --operation.type modify_tasks \
      --operation.task_replacements '{"pick up the cube and put it in the box": "Pick up the cube and place it in the box."}'
  3. 3
    Delete the episodes you already know are broken

    Blocked cameras, a servo that sagged, a run where the operator gave up. Do it per site, while the episode indices still mean something locally.

    bash
    lerobot-edit-dataset \
      --repo_id myorg/pickplace_tallinn \
      --new_repo_id myorg/pickplace_tallinn_clean \
      --operation.type delete_episodes \
      --operation.episode_indices "[7, 19, 20, 41]"
  4. 4
    Merge

    new_repo_id is required for merge, and repo_id is the one argument that is not. Pass roots when the datasets sit at explicit local paths rather than under $HF_LEROBOT_HOME. Episode and frame indices get constant offsets in the order you list the sources, task indices are recomputed from the stable task strings, and video timestamps are shifted per source file.

    bash
    lerobot-edit-dataset \
      --new_repo_id myorg/pickplace_fleet \
      --new_root /data/pickplace_fleet \
      --operation.type merge \
      --operation.repo_ids "['myorg/pickplace_berlin', 'myorg/pickplace_lisbon', 'myorg/pickplace_tallinn_clean']" \
      --operation.roots "['/data/berlin', '/data/lisbon', '/data/tallinn_clean']"
  5. 5
    Check the totals and the task table

    Total episodes should equal the sum of the three sources. Total tasks should equal the number of distinct instructions you intended, not the number of sites. If it came out as three when you meant one, step two did not take.

    bash
    lerobot-edit-dataset \
      --repo_id myorg/pickplace_fleet \
      --root /data/pickplace_fleet \
      --operation.type info \
      --operation.show_features false
  6. 6
    Push it, or keep it local

    push_to_hub is a top-level flag and every operation honours it, merge included, so add it to the merge rather than running a second command. Do not use recompute_stats as a way to push: with neither --new_repo_id nor --new_root set it copies the dataset to <repo_id>_recomputed_stats and pushes that instead, and in-place recomputation refuses to run without --operation.overwrite true.

    bash
    # push straight from the merge, rather than as a second step
    lerobot-edit-dataset \
      --new_repo_id myorg/pickplace_fleet \
      --new_root /data/pickplace_fleet \
      --operation.type merge \
      --operation.repo_ids "['myorg/pickplace_berlin', 'myorg/pickplace_lisbon', 'myorg/pickplace_tallinn_clean']" \
      --operation.roots "['/data/berlin', '/data/lisbon', '/data/tallinn_clean']" \
      --push_to_hub true
The merge pools statistics, it does not recompute them

finalize_aggregation calls aggregate_stats over the source stats: new min is the min of the mins, new max the max of the maxes, mean and standard deviation pooled weighted by each source's sample count. Correct pooling, and also why one miscalibrated arm quietly widens the normalisation range for every site. Quantiles are the documented exception: they cannot be recovered exactly from per-source summaries, so lerobot keeps a conservative envelope, min for lower quantiles and max for upper ones, and the code calls those bounds rather than global estimates. If a joint at one site reads ten degrees off from a bad calibration, the merged stats absorb the offset and every batch is normalised against it. Check the per-site min and max before merging, not after.

Weighting the sites, and why you usually cannot

Once merged, the sites are indistinguishable. There is no site column and no sampler that draws evenly from three unequal contributions. If Berlin recorded 300 episodes and Tallinn managed 60, the merged set is 83 percent Berlin and your policy learns Berlin's lighting as the default world. There are two honest ways around it and both have a cost.

ApproachHowCost
Cap before mergingUse the split operation on the large site and merge only one splitYou throw away real data
Duplicate the small siteMerge the small dataset in twicePooled statistics skew toward it, and identical frames appear twice per epoch
Let it be unbalancedMerge as recordedHonest, and often fine if the small site is at least 20 percent of the total
Use GR00T's mixture flagsPass several dataset roots and set --ds-weights-alphaGR00T only, and it changes how the loader samples rather than what the dataset contains

That last row is the one real escape hatch, and it belongs to NVIDIA rather than LeRobot. The GR00T N1.7 fine-tune entry point takes dataset_path, documented as "Path to one dataset root, or an os.pathsep-separated list of dataset roots", and ds_weights_alpha, a power-law exponent where each dataset's sampling weight is len(dataset) to the power alpha and per-dataset mix_ratio values are ignored once it is set. Alpha of 1 samples in proportion to size, which is what merging gives you anyway. Alpha of 0 makes every weight 1, so every site is sampled equally. The default is None, so the flag is off unless you set it.

bash
# Isaac-GR00T, gr00t/experiment/launch_finetune.py
# dataset_path is split on os.pathsep (":" on Linux)
python -m gr00t.experiment.launch_finetune \
  --base-model-path nvidia/GR00T-N1.7-3B \
  --dataset-path /data/berlin:/data/lisbon:/data/tallinn \
  --embodiment-tag new_embodiment \
  --ds-weights-alpha 0.0 \
  --output-dir /data/out
Flag names read from gr00t/configs/finetune_config.py and gr00t/experiment/launch_finetune.py on 24 August 2026. Alpha 0.0 gives every site the same sampling weight.

For every other policy, merging is mandatory. In lerobot 0.6.2 make_dataset raises NotImplementedError when repo_id is not a string, with the MultiLeRobotDataset branch sitting unreachable behind it. Passing a list of datasets to SmolVLA or ACT training is not a supported path today, whatever older tutorials suggest.

The AY-Robots public dataset directory, listing LeRobot datasets available for training
The public dataset directory at /directory, useful for seeing how other SO-100 datasets are structured before you commit to a naming convention.

Do it yourself, or do it here

You own the arms, the machines and the merge. Each site runs lerobot-record against its own arm, ships the folder, and one person runs the merge and the training. Fewest moving parts, most manual coordination.

bash
# Run this identical command at every site.
# Only --robot.port, --robot.id and the camera indices may differ.
lerobot-record \
  --robot.type=so100_follower \
  --robot.port=/dev/ttyACM0 \
  --robot.id=site_berlin_follower \
  --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}}" \
  --teleop.type=so100_leader \
  --teleop.port=/dev/ttyACM1 \
  --teleop.id=site_berlin_leader \
  --dataset.repo_id=myorg/pickplace_berlin \
  --dataset.num_episodes=100 \
  --dataset.single_task="Pick up the cube and place it in the box." \
  --dataset.rgb_encoder.vcodec=h264
The camera keys front and wrist become observation.images.front and observation.images.wrist. Change one of them at one site and the merge fails. Pinning vcodec stops each machine from picking its own hardware encoder.
  • One GPU or a rented one per training run, plus somewhere to hold three copies of the video before merging.
  • A person who owns the contract sheet and checks each site's meta/info.json before recording starts in earnest.
  • Pin the version and the extras. lerobot-record needs pip install 'lerobot[core_scripts]', lerobot-edit-dataset needs pip install 'lerobot[dataset]'. Nothing stops a site upgrading lerobot mid-collection and changing the joint naming convention under you.

Where multi-site collection does not help

Three sites will not rescue a task a single site cannot demonstrate cleanly. If the policy fails because the gripper cannot hold the object or the wrist camera cannot see the contact point, adding rooms multiplies the same flaw. Fix the task at one site, get a working checkpoint, and only then scale. The data quality guide is the prerequisite for this page.

Three further limits are worth stating plainly, because none are fixed by adding sites. The third catches teams who assume a distributed dataset implies a distributed deployment.

  • No held-out site. The merged dataset has no site column, and lerobot's eval_split holds out the last ceil(n * eval_split) episodes per task rather than a sample per location. Because the merge appends sources in the order you list them and offsets episode indices by a constant, that tail is mostly whichever site you merged last: an accidental held-out site, not a chosen one. For a real cross-environment test, leave one site unmerged and evaluate on its arm.
  • No seed in GR00T. Its fine-tune entry point exposes none, so two runs on two site mixtures are not bit-for-bit comparable and a small success-rate gap may be run-to-run noise. lerobot's default seed is 1000, so ACT, SmolVLA and Pi0.5 comparisons are on firmer ground.
  • Unchanged inference latency. The control loop is 20 to 485 ms per action step depending on the model, and running the policy on a cloud GPU while the arm sits in another country adds public-internet round trips on top. Workable for slow pick-and-place, not for fast reactive motion, whatever the training data looked like.

There is also a format trap on the way in. GR00T N1.7 and N1.5 want LeRobot v2.0 or v2.1, and a v3.0 dataset crashes the GR00T loader, so a freshly merged v3.0 fleet dataset has to be converted down before a GR00T run. Pi0.5, SmolVLA and ACT take v3.0 directly, and the dataset rejected as v3 page covers the conversion. How heterogeneous data behaves once pooled is the subject of Open X-Embodiment, which pooled 60 datasets from 34 labs into 22 embodiments and had to coarsely align action spaces to do it, accepting that camera observations still varied substantially across sources.

Staff the second site without buying a second building

Operators drive real SO-100 arms over the internet and record LeRobot episodes from wherever they are. That is the labour half of multi-site collection, and it is the half that is hardest to hire locally.

See how operator work runs
How many sites do I actually need?

No threshold in the literature applies to a two-arm setup. What DROID showed is that at a fixed trajectory count, spreading trajectories across many scenes beat concentrating them in 20 scenes out of distribution. Two rooms is meaningfully better than one. The platform minimums are per policy and count pooled episodes, not sites: 50 episodes for GR00T N1.7, GR00T N1.5, Pi0.5 and ACT, 30 for SmolVLA.

Can I merge datasets recorded on an SO-100 and an SO-101?

Only if both were recorded with the same robot_type string, joint names, camera keys, resolution and codec. The arms are mechanically close enough that people assume this works, but LeRobot compares the recorded metadata, not the hardware. The two official svla pick-and-place datasets are the cautionary example: same class of arm, three separate mismatches, no merge.

Do I have to recompute statistics after merging?

No. finalize_aggregation pools the source statistics: min of mins, max of maxes, count-weighted mean and standard deviation. Quantiles are pooled as a conservative envelope rather than exact global values, which the code documents. Run recompute_stats if you deleted episodes afterwards, or want relative-action statistics. Watch the output path: without --new_repo_id it writes to _recomputed_stats, and in-place needs --operation.overwrite true.

What if one site records at 60 fps and the rest at 30?

The merge refuses. Re-record that site, or treat it as a separate dataset and train separately. There is no downsampling operation in lerobot-edit-dataset as of 0.6.2. This is why fps belongs in the contract sheet rather than in the recording script's defaults, and the same argument applies to the video codec, since video.codec is compared as well.

Can I keep track of which site each episode came from?

Not through a dedicated field. The usual workaround encodes the site in the task string, which makes it visible in meta/tasks.parquet but splits your task table and changes what a task-conditioned policy sees. The cleaner one is to record the episode ranges per site before merging, since episode indices are offset by a constant per source and stay in source order.

Does the platform merge my site datasets for me?

No. Recording, training and inference are covered; the merge is a lerobot-edit-dataset step you run yourself. What the platform removes is the more common failure, three sites producing three different formats, because every desktop client writes the same LeRobot structure from a teleoperation session.

Sources

Sources

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started