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.

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.
IK / FK for teleop. The EVA core does provide IK and FK solvers. Today's teleop samples cover gello-like isomorphic leader arms only — ARX and UR — with no VR example yet. These solvers are deliberately not wired into the core: teleop schemes vary too much, so for now they live in the 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

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.
transportLive streams to read. Each arm names the streams carrying its pose and commanded action.
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_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:

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.

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.