> ## Documentation Index
> Fetch the complete documentation index at: https://docs.almond.bot/llms.txt
> Use this file to discover all available pages before exploring further.

# collect-data

> Record teleoperation episodes to a LeRobot-format dataset.

Records teleoperation episodes using VR controller inputs and locally attached ZED cameras. Saves to a [LeRobot](https://github.com/huggingface/lerobot)-format dataset. Loops until `Ctrl+C`.

<Tip>
  For the end-to-end workflow, see the [Data Collection guide](/operations/data-collection).
</Tip>

This command is configured via draccus. The robot and teleop subsystems are exposed as the nested `robot_config` / `teleop_config` configs (cameras, per-joint gains, IK, VR server) — nest into them with dots or pass a whole-config file. See [Command configuration](/cli/configuration).

| Flag                                                           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--repo_id <user>/<dataset>`                                   | HuggingFace dataset repo ID (required)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `--task TEXT`                                                  | Natural language task description (required)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `--fps INT`                                                    | Dataset recording frame rate — camera frames captured at this rate (default: 60). On the Jetson recording path dataset rows are paced by camera frame arrival, so this **must match the camera capture fps** — a mismatch would stamp the dataset with a frame rate it wasn't captured at (replay/training would run at the wrong speed), so collect-data errors early instead.                                                                                                                                                                                                                                                                                                                                                                                 |
| `--dataset_resolution {SVGA,HD1080,HD1200}`                    | Resolution the **dataset** video is recorded at (default: `SVGA` = 960×600), downscaled from the camera capture on the GPU's VIC. SVGA is \~4× lighter to encode and store than `HD1200` while the **headset still gets the full-resolution stream**. Resuming an existing dataset keeps its original resolution regardless of this flag.                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `--vcodec STR`                                                 | Video codec recorded for the dataset (default: per-platform — `h264` on Jetson/aarch64, `auto` elsewhere). Accepts any LeRobot codec (e.g. `auto`, `h264`, `libsvtav1`). On the Jetson `h264` is encoded on the NVENC hardware via GStreamer, in **VBR mode with a peak cap** (\~0.17 bits/pixel target, ≈6 Mbps at SVGA/60fps) so every camera's dataset video stays bounded and uniformly sized — a pathologically noisy sensor is compressed down to the target instead of ballooning the dataset.                                                                                                                                                                                                                                                           |
| `--teleop_hz INT`                                              | Motor command rate in Hz; decoupled from `--fps` for smooth control (default: 120)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `--root PATH`                                                  | Local dataset root (default: `$HF_LEROBOT_HOME`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `--push_to_hub true`                                           | Push to HuggingFace Hub when done                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--robot_config.cameras DICT`                                  | The camera slots (`overhead` / `left_arm` / `right_arm`) to use, with each ZED camera's serial number, as one inline YAML/JSON value: `--robot_config.cameras "{overhead: {serial: 41234567}, left_arm: {serial: 41234568}, right_arm: {serial: 41234569}}"`. Unassigned slots are dropped, so you only configure the cameras you have. **At least one assigned camera must have recording enabled** (the default) — collect-data errors early otherwise.                                                                                                                                                                                                                                                                                                       |
| ↳ `record` / `stream` (per camera)                             | Each camera opts into two independent branches: `record` (default `true`) writes it to the dataset, and `stream` (default `true`) relays it to the headset. Set `record: false` to stream a camera to the operator without recording it, or `stream: false` to record it without sending it to the headset. A camera with both off is dropped.                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ↳ `stereo` (auto-detected)                                     | Stereo ZED X cameras are detected from their serial — on the CLI *and* the [control panel](/guides/control-panel#cameras) — so you never flag it by hand. Auto-detection applies the head/wrist eye convention: a stereo **overhead** records both eyes as two observation keys, `overhead_left` / `overhead_right` (one grab), while a stereo **wrist** (`left_arm` / `right_arm`) records only its left eye under the plain slot name, so it captures, encodes, and records exactly like a mono camera. Setting `stereo: true` by hand **skips** auto-detection and uses the `eyes` field instead (default `both` → `X_left` / `X_right`; `left` / `right` → a single eye under the plain name `X`) — so to force a single-eye wrist, also pass `eyes: left`. |
| ↳ `eyes` / `stream_eyes` (per stereo camera)                   | `eyes` picks which eye(s) are **recorded** (`both` / `left` / `right`); `stream_eyes` independently picks which are **streamed** to the headset (`null`, the default, follows `eyes`). Decouple them to e.g. stream `both` eyes for depth perception while recording only `left`: `--robot_config.cameras "{overhead: {serial: 41234567, eyes: left, stream_eyes: both}}"`.                                                                                                                                                                                                                                                                                                                                                                                     |
| `--robot_config.video_backend {auto,gst,sdk}`                  | Camera capture backend (default: `auto`). `gst` is the GPU-resident zed-gstreamer pipeline (low latency, shared with the headset relay); `sdk` is the ZED Python SDK; `auto` prefers `gst` when its stack ([`gst.install`](/cli/gst-install) + [`gst.build-zed`](/cli/gst-build-zed)) is installed and falls back to the SDK.                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `--robot_config.axol_config.<side>.gripper.torque_limit FLOAT` | Max torque (Nm) for a gripper in POSITION\_FORCE mode (default: 0.5); `<side>` is `left`/`right`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `--robot_config.axol_config.left_stiffness S`                  | Compliance↔stiffness blend for the left arm in `[0, 1]`. Scalar or 7-element list (one per arm joint, in `Joint` enum order). `0` = fully compliant; `1` = pre-tuning industrial gains; `0.5` (default) is the geometric mean. Use `right_stiffness` for the right arm. See [`AxolConfig.left_stiffness`](/api/robot).                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `--teleop_config.vr_teleop_config.position_multiplier FLOAT`   | Scale factor on hand **position** (not orientation): the end-effector target moves this many times as far as your hand. `1.0` (default) is 1:1; `2.0` moves the arm twice as far, helping cover the robot's full reach when the arm is longer than the operator's.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `--teleop_config.vr_teleop_config.rotation_multiplier FLOAT`   | Scale factor on hand **orientation** (not position): the end-effector target rotates this many times as far as your wrist. `1.0` (default) is 1:1; `2.0` rotates the arm twice as far.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `--rerun_ip IP`                                                | IP of a Rerun viewer on your local machine for live visualization                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `--rerun_port INT`                                             | Rerun viewer port (default: 9876); only used when `--rerun_ip` is set                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `--log_level {DEBUG,INFO,WARNING,ERROR}`                       | Default: `INFO`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `--config_path PATH`                                           | Load a whole-config JSON/YAML file; CLI overrides layer on top.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |

```bash theme={null}
axol collect-data --repo_id myorg/pick-place --task "Pick the red cube and place it in the bin" \
    --robot_config.cameras "{overhead: {serial: 41234567}, left_arm: {serial: 41234568}, right_arm: {serial: 41234569}}"
axol collect-data --repo_id myorg/pick-place --task "Pick the red cube" --fps 30 \
    --robot_config.cameras "{overhead: {serial: 41234567, stereo: true}, left_arm: {serial: 41234568}, right_arm: {serial: 41234569}}" \
    --robot_config.axol_config.left_stiffness 1.0 --robot_config.axol_config.right_stiffness 1.0
```

## Headset camera views

During collection the camera feeds are also relayed to the headset over WebRTC (hardware-encoded on the Jetson's NVENC, installed by [`gst.install`](/cli/gst-install)), so the operator sees the overhead and both wrist views at once while recording — freely moved and resized, same as teleop. Just like teleop, the cameras run in a **dedicated subprocess** (`almond_axol.video.video_proc.VideoRelayProcess`) so the grab/encode/WebRTC pump never contends with the teleop/IK loops — running it in-process measurably halves the IK rate. A **single grab per camera** feeds two independent GPU NVENC branches: the headset branch (fixed-bitrate H.264 over WebRTC) and the dataset branch — downscaled to `--dataset_resolution` on the GPU's VIC and encoded to **capped-VBR H.264** (see the `--vcodec` note above). On the Jetson the relay ships the dataset branch's encoded H.264 access units over a gst-native `shmsink` (the recorder reads them with `shmsrc`), so the relay runs **no Python per frame** and — crucially — the **recorder never re-encodes**, it only *muxes* the already-encoded stream into the dataset's mp4. That removes the second, redundant encode that used to run in the recorder (and the \~51 MB/s raw-frame copy that fed it), which was saturating CPU and dropping frames. A raw `multiprocessing` shared-memory copy (recorder-side NVENC) is the fallback when gst's `shm` plugin is absent, and when the zed-gstreamer plugins aren't built (see [`gst.build-zed`](/cli/gst-build-zed)) collection falls back to opening the cameras in-process (SDK grab + in-Python NVENC). Either way it's still one grab — no second capture. See the [VR Interface guide](/guides/vr-interface#camera-views).

## VR controller controls

Episodes are driven from the Quest controllers. The session is locked to **data collection** mode (no in-headset mode toggle). For the full controller layout and step-by-step flow, see the [Data Collection guide](/operations/data-collection#controller-layout).

| Button | Action                                                                                                                                                                                                                      |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A**  | Start a take (3-second countdown). While recording, arms the **Save episode?** confirmation — press **A** again to save (the headset then enters `Saving` until the write completes), or **X** to cancel and keep recording |
| **X**  | During the start countdown, cancels it; otherwise resets to the rest pose. While recording, arms the **Discard episode?** confirmation — press **X** again to discard and re-record, or **A** to cancel and keep recording  |

After each episode the robot automatically returns to its rest pose before the next take begins. If an existing dataset is found at `--root`, collection resumes from where it left off; conversely, if no episodes were saved before exit the empty dataset directory is removed on shutdown so an aborted session does not leave a half-initialized dataset on disk.

<Info>
  Dataset capture runs in a dedicated recorder subprocess (`almond_axol.recording.record_proc`), not just a thread — a thread would still share the control loop's GIL, and the per-frame numpy / `dataset.add_frame()` / stats work was enough to stall the 120 Hz loop even with spare CPU cores. The control loop now only writes a small joint/action snapshot to shared memory each tick. On the Jetson (encoded `shmsink` path) the recorder is **frame-driven**: it consumes one pre-encoded H.264 access unit per camera, pairs it with the joint snapshot captured at the same `perf_counter` instant, **muxes** it into the mp4 (no re-encode), and saves the episode — so per-frame dataset work off the hot control loop is just the mux and the row bookkeeping. On the fallback paths the recorder instead ticks at `--fps` and NVENC-encodes the raw frames itself. Since the cameras are captured locally, frames and joint samples share a single `perf_counter` clock, so the pairing matches the cameras' exposure timeline with no cross-machine time sync.

  The hot control loop runs **on the robot's own asyncio event loop** (`AxolRobot.event_loop`), so each `motion_control` is awaited inline — cooperatively interleaved on a single thread, exactly like `axol teleop`. This removes the per-step cross-thread `send_action` round trip (`run_coroutine_threadsafe(...).result()`) that otherwise capped — and jittered — the control rate once teleop was engaged.

  For the same reason, data collection defaults to **`telemetry_hz=0`**: the control loop already issues `motion_control` every step, and every impedance/gripper command returns a feedback frame that refreshes the joint position/torque cache. Running the background 120 Hz × 16-motor poll loop on top of that just adds redundant CAN traffic and CPU that contends with `motion_control` — so `collect-data` skips it and relies on command replies, matching teleop's bus load. (`run-policy` and other `AxolRobot` users keep the poll loop, since they don't command every step.)
</Info>
