Data collection

EVA records into one LeRobot v2.1 raw layout, then exports quality-reviewed episodes to the format your downstream stack needs.

Two ways to record, one raw format

Both paths first produce the same LeRobot v2.1 raw dataset and can be replayed through the dataset transport. They differ only in what each frame contains. LeRobot v3.0, HDF5, and MCAP are post-QC export targets, not alternate live writers.

Commanded action source. During teleop the action is not computed by EVA — it arrives from the leader device. The collection config names the stream that carries it.
WebXR input. PICO-based WebXR VR teleoperation maps controller deltas to EEF targets and runs FK/IK plus safety checks in EVA. Hardware Leader/Follower paths are configured through their corresponding robot presets.

Before you record

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.

SectionPurpose
storageOutput folder (log_dir), target fps, and the background-save queue depth (save_queue_max).
schemaPer-frame contents: robot type, minimum episode length, arms, cameras, output column names.
teleopSelects control_source="transport" for a hardware-side Leader or "client" plus a VR client definition.
tasksInstructions selectable at record time. Each task becomes its own dataset folder.

Inside schema:

eef encoding. 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:

python
_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_piper
        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',
        ),
    ),
    teleop=dict(control_source='transport'),  # hardware-side Leader; use 'client' for VR
    tasks=dict(
        pick_up_the_apple=[('pick up the apple', -1)],  # UI key → (prompt, repeat limit)
    ),
)

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:

bash
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:

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.

One dataset folder per task. Each task creates its own dataset under the output folder, named after the task (spaces become _, 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:

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.

Review before export. The Console's QUALITY EXPORT step splits the raw dataset into accepted/ and rejected/, then writes both in the selected format. Only the current accepted export can be uploaded. See Data formats for paths, atomic publication, and failure behavior.

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.

text
<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.

FlagRaised when
episode_too_shortfewer frames than min_episode_frames.
non_monotonic_timestampcapture time does not advance past the previous frame.
missing_camera · invalid_image_shapeconfigured camera produced no frame, or wrong shape.
missing_configured_column · invalid_vector_dimconfigured field missing, or length mismatch.
non_finite_valuefield contains an invalid number (replaced with zeros).
frame_count_mismatchcamera 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.

Dataset metadata and statistics

On finalization EVA writes info.json and stats.json:

Inference runs as datasets

Inference runs write the same format with two differences:

Overwrite vs append. Teleop adds new episodes; eval re-runs of the same prompt/trial replace the slot.

For eval setup and scoring, see the Configuration page and the EVAL workflow.