Data collection
EVA writes LeRobot v2.1 datasets from both teleop collection and inference runs.
Two ways to record, one format
Both paths produce the same dataset and can be replayed through the dataset transport. They differ only in what each frame contains.
- Teleoperation collection — a leader device drives the robot; EVA records the robot's pose alongside the commanded action. Runs from the COLLECT, enabled by a collection schema in the config.
- Inference runs — a trained policy drives the robot; EVA records the robot's pose paired with the action the policy sent. This is what SFT consumes.
hardware layer to keep the core clean, and the core only records data. If you need real-time IK/FK during collection, call the core's solvers from your hardware layer. Once the approaches settle, later versions will fold real-time IK/FK for collection into the core.Before you record
- EVA installed — the
evacommand must resolve. - Live robot connection — powered on and reachable over
ros1,ros2, orzmq. Dataset-only configs cannot collect. - Teleoperation device — the leader is connected and publishing.
- A collection config — defines a collection schema. Examples ship under
configs/02_collection/.
Fill in the collection config
A config becomes a collection config once it lists the data columns to save (collection.schema.columns). EVA validates the schema on launch and refuses to start if anything required is missing.
| Section | Purpose |
|---|---|
storage | Output folder (log_dir), target fps, and the background-save queue depth (save_queue_max). |
schema | Per-frame contents: robot type, minimum episode length, arms, cameras, output column names. |
transport | Live streams to read. Each arm names the streams carrying its pose and commanded action. |
tasks | Instructions selectable at record time. Each task becomes its own dataset folder. |
Inside schema:
- Arms map each arm to a short prefix (e.g.
left) used in per-joint column names likeleft.j0,left.gripper. - Cameras map each camera to its dataset video name.
- Columns name four fields:
qpos,eef,action_qpos,action_eef. - min_episode_frames sets the shortest episode kept without a flag; shorter takes are flagged for review.
eef is eight numbers per arm: position (x y z), rotation quaternion (qw qx qy qz), gripper opening.Start from the matching example under configs/02_collection/ and edit the marked values. Keep the column names unless your training pipeline expects different ones — they are the dataset's contract:
_base_ = ['../01_deploy/<your-robot>/_base.py'] # ← reuse your robot's deploy config collection = dict( storage=dict( log_dir='<output folder>', # ← where datasets are written; blank = work_dirs/<config name> fps=30, # target frame rate (sane default; lower for slow tasks) save_queue_max=15, # how many finished episodes may wait to be saved ), schema=dict( robot_type='<your robot type>', # ← e.g. agilex_AGILEX min_episode_frames=10, # shorter takes get an 'episode_too_short' flag arms=dict(left_arm='left', right_arm='right'), # ← your arms → name prefixes cameras=dict( cam_high='observation.images.cam_high', # ← your camera → its dataset name ), columns=dict( # keep these names unless training needs others qpos='observations.state.qpos', eef='observations.state.eef', action_qpos='action.qpos', action_eef='action.eef', ), ), transport=dict( # ← name the live streams that carry pose + commanded action ros1=dict(groups=dict( left_arm=dict( qpos_topic='<robot joint stream>', # ← where the arm reports its joints action_qpos_topic='<leader joint stream>', # ← where your commanded action comes from ), )), ), tasks=['<your task instruction>'], # ← e.g. 'pick up the apple' )
If log_dir is blank, EVA writes to work_dirs/<config name>/.
Run a collection
Launch EVA with the collection config; the COLLECT opens in the browser:
eva --config configs/02_collection/<your-config>.py
The full button workflow is on the COLLECT page. Each take runs on a background pipeline so live capture never stalls:
- Start — recording begins, frame buffers reset.
- Capture — frames are grabbed instantly and handed off for background processing. Sustained over-capture grows memory and logs a warning rather than dropping data.
- Skip before ready — frames before the teleop link is calibrated are skipped (no commanded action yet) rather than saved with empty actions.
- Stop — recording ends, every frame is checked, and the episode is handed to a background saver. Zero-frame takes save nothing.
- Cancel — discards the current take; nothing is written.
The console reports live status: collecting / saving / idle, frame count, save-queue depth, estimated time to flush. Each kept take is appended to the dataset for that task; recording never overwrites earlier episodes.
_, slashes become -). Data lands under <log_dir>/<task name>/.Confirm a recording worked
After STOP, wait for status to return to idle. A successful take adds:
- a new
episode_NNNNNN.parquetunderdata/chunk-000/; - one
episode_NNNNNN.mp4per camera undervideos/chunk-000/; - a new line in
meta/episodes.jsonlwith the episode's quality flag.
Each line records the episode's quality: green if every frame passed, red if any check failed. Red episodes are saved (bad values repaired, see below) but should be replayed from COLLECT or REPLAY before deciding to re-record.
What the dataset looks like
One folder per task. Episodes are six-digit numbered (episode_000000…) inside chunk-000. A chunk caps at 1000 episodes; EVA only fills the first.
<log_dir>/<task name>/ ├── data/ │ └── chunk-000/ │ └── episode_000000.parquet # main table, 1 row per frame ├── videos/ │ └── chunk-000/ │ └── <camera>/ │ └── episode_000000.mp4 # one mp4 per camera └── meta/ ├── info.json # dataset metadata: format version, fps, columns ├── episodes.jsonl # one line per episode: number, task, length, quality ├── episodes_stats.jsonl # per-episode statistics (collection only) ├── tasks.jsonl # the task text for each episode └── stats.json # whole-dataset statistics
A worked example ships at examples/agilex_dataset/ (dual-arm AGILEX, three cameras).
Per-frame quality checks
Every frame is checked on save. Any failure marks the episode red and the reason is stored in episodes.jsonl; otherwise the episode is green.
| Flag | Raised when |
|---|---|
episode_too_short | fewer frames than min_episode_frames. |
non_monotonic_timestamp | capture time does not advance past the previous frame. |
missing_camera · invalid_image_shape | configured camera produced no frame, or wrong shape. |
missing_configured_column · invalid_vector_dim | configured field missing, or length mismatch. |
non_finite_value | field contains an invalid number (replaced with zeros). |
frame_count_mismatch | camera video frame count differs from the data table. |
Missing or wrong-sized fields are replaced with zeros of the correct length so the table stays rectangular; the failure is flagged. Red episodes remain usable but are worth checking.
How timestamps are recorded
LeRobot requires perfectly even frame times. EVA synthesizes the official timestamp from the configured fps and stores the real capture time in a separate capture_time column for diagnostics.
- Stored
fpsequals the configured target, not a measured average. - Measured average frame rate (jitter) is recorded per episode for diagnostics.
- Each camera video runs at the target
fps, exactly one video frame per data row.
Dataset metadata and statistics
On finalization EVA writes info.json and stats.json:
- Column descriptions — for each video, size and codec; for each data field, type and per-element labels (joints as
<arm>.j0…/<arm>.gripper, end-effector as<arm>.{x,y,z,qw,qx,qy,qz,gripper}). - Statistics — per-feature min/max/mean/std, computed per episode and rolled up. Training tools use these to normalize.
- Tasks — instruction text per episode.
Inference runs as datasets
Inference runs write the same format with two differences:
- Each row pairs robot state with the action the policy sent — one row per executed step. End-effector pose is saved as a second state column if the deployment tracks it; joint-only runs omit it.
- Eval runs save immediately on STOP so the trial is scorable. The score lives in dataset metadata, never as a data column. Re-running the same prompt-and-trial combination overwrites that episode in place — teleop collection always appends.
For eval setup and scoring, see the Configuration page and the EVAL workflow.