Recording and Exporting Data
The Adamo data service records a robot’s cameras and the topics it publishes. Recordings appear on the Recordings page of app.adamohq.com, and the Python SDK lists, downloads and exports them as LeRobot datasets.
Recording a robot
Section titled “Recording a robot”A robot says what to record with its Recording:
import jsonimport adamo
recording = adamo.Recording( adamo.ControllerMask.HUMAN | adamo.ControllerMask.POLICY, mode=adamo.RecordingMode.Episodes(frame_rate=adamo.FrameRate.Fixed(30)), topics=["left/joint_pos", "left/joint_cmd"],)robot = adamo.Robot(name="my-arm", record=recording)joint_pos = robot.session.publisher("my-arm/left/joint_pos", reliable=True)joint_pos.put(json.dumps([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]).encode())use std::num::NonZeroU32;use std::time::Duration;use adamo::{ControllerMask, FrameRate, Mode, PublisherOptions, Recording, Robot};
let robot = Robot::new_default("ak_...", Some("my-arm"))?.with_recording(Recording { controller_mask: ControllerMask::HUMAN | ControllerMask::POLICY, mode: Mode::Episodes { frame_rate: FrameRate::Fixed { rate: NonZeroU32::new(30).unwrap(), gop: NonZeroU32::new(2).unwrap() }, min_duration: Duration::from_secs(2) }, topics: vec!["left/joint_pos".to_owned(), "left/joint_cmd".to_owned()], ..Recording::default()});let joint_pos = robot.publisher("my-arm/left/joint_pos", PublisherOptions { reliable: true, ..PublisherOptions::default() })?;joint_pos.put(b"[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]")?;When it records
Section titled “When it records”controller_mask selects the time to record: sessions in which a person (HUMAN) or a policy (POLICY) controls the robot, and time when nobody does (UNATTENDED). The default is HUMAN | POLICY. robot.start_recording() records the robot whether or not anyone is in control, until robot.stop_recording().
mode says what the recording is for:
| Mode | For | Files |
|---|---|---|
RecordingMode.Episodes(frame_rate, min_duration=2.0) (default) | Training control policies | One per task, from its start to its stop or abort. Tasks shorter than min_duration seconds are left out. |
RecordingMode.Continuous(frame_rate) | Training video models | One per stretch of recorded time. |
RecordingMode.Interventions(pre_roll=5.0, post_roll=0.0, frame_rate) | Auditing | One whenever the controller changes, with pre_roll seconds before it and post_roll seconds after. |
Files are cut into parts after max_duration seconds (default 1800) or max_size bytes (default 1.5 GB).
Frame rate
Section titled “Frame rate”frame_rate is FrameRate.Fixed(rate) for a number of frames a second, FrameRate.Max() (the default) for the rate of the fastest camera, or FrameRate.Raw() to keep each camera’s video as the robot sent it. The data service puts every camera on that one clock as it records, so frame k of each camera is at k / rate seconds into the file, re-encoded with a keyframe every gop frames (default 2): FrameRate.Fixed(30, gop=4), FrameRate.Max(gop=4). Recordings saved with FrameRate.Raw() can be downloaded but not exported.
gop is the number of frames from one keyframe to the next. A training loader can only start decoding at a keyframe, so a small gop lets it jump to any frame quickly, and a larger one makes smaller files. With FrameRate.Raw() the video keeps the keyframes the robot’s encoder made.
Topics
Section titled “Topics”topics are the keys, relative to the robot, that it publishes on with a put or a persistent publisher: "left/joint_pos" records my-arm/left/joint_pos. Each value is stamped with the time it was sent, on the same clock as the video frames, and recorded next to the frames of that moment. Each file also starts with the latest value of every topic sent before it, so a topic sent once, or more slowly than a task lasts, is in every file. Payloads are recorded as they are, in any format: values that are JSON numbers or arrays of numbers export directly, and anything else exports through a decoder (see Decoders).
ROS topics
Section titled “ROS topics”With ROS enabled, record_ros records native ROS topics. Each message is decoded to JSON and published on the robot’s key for that topic, without its leading slash, as one of the topics it records:
robot.record_ros(["/joint_states", "/gripper/state"], max_age=1.0)/joint_states is recorded as joint_states, and its fields export as joint_states.position. Messages older than max_age seconds when they are read are skipped. Custom message types need their .msg definitions in definitions={"package/msg/Type": msg_text}. Call it once, with every ROS topic to record.
record_audio=True (record_audio: true in Rust) also records every audio track on the robot: the ones it publishes, under the names it gave them, and each operator’s microphone, as the track operator-{session id}. It is off by default.
Audio is not put on the frame clock. Each track is kept as the Opus packets it sent, one every 10 ms by default, under audio/{track} in the file, each at its capture time on the same clock as the video frames and topics. The track’s metadata (sample rate, channels, frame duration) is under audio/{track}/meta. A recording lists its tracks in audio_tracks. See Exporting audio to decode them.
In Episodes mode, the files follow the tasks the operator runs from the task list in the web app (Settings, Tasks, “Show task list”). Starting a task opens a file, and stopping it with Success or Failure, or aborting it, closes it with that outcome. The signals are published on {robot}/tasks/signal; see Task Signals.
Finding recordings
Section titled “Finding recordings”adamo.Recordings reads the recordings of the organization of your API key (ADAMO_API_KEY):
from datetime import datetime, timedelta, timezoneimport adamo
client = adamo.Recordings()recordings = client.list( start=datetime.now(timezone.utc) - timedelta(days=7), devices=["my-arm"], tasks=["PickUpCube"], tags=["data_collection"],)client.download(recordings, "recordings")list also filters by end, controllers, sessions, folder and a search over names and notes. The Copy Query button on the Recordings page writes this code for the current filters.
Each recording’s frame_rate is the clock it was saved on: FrameRate.Fixed(rate, gop), with FrameRate.Max() resolved to the rate of the fastest camera, or FrameRate.Raw().
Exporting to LeRobot
Section titled “Exporting to LeRobot”pip install "adamo[export]" (Python 3.12 or newer) adds Recordings.export, which writes the recordings as a LeRobot dataset, one episode per recording, and returns the LeRobotDataset:
dataset = client.export(recordings, adamo.Export( state=["left/joint_pos", "left/gripper_pos"], action=["left/joint_cmd"], outcomes=["success", "failure"], tasks={"PickUpCube": "Pick up the cube."},))stateandactionname recorded topics, or fields inside a JSON topic ("arm/state.position"), concatenated in that order. Each is sampled at every video frame: a frame holds the latest value sent at or before it.outcomesselects which Episodes recordings are kept by how their task ended:"success"(the default),"failure"and"abort".tasksmaps each task to the prompt written into the dataset, and keeps only the recordings of those tasks, along with recordings that have no task. Without it, every task is kept and its prompt is its name as a sentence.
The video is copied into the dataset as recorded. Recordings exported together must share one frame rate, whatever their gop, and one set of cameras; any that do not, or that lack a topic in state or action, are skipped with a warning that names each one and why.
The dataset goes to lerobot-{hash of the recordings, config and decoders} in the current folder unless output_folder= says where. An existing folder is opened as it is, so running the same export again is instant. The recordings are downloaded to a temporary folder unless download_folder= keeps them.
The Export Dataset button on the Recordings page writes this code for the current filters, with the state, action, outcomes and prompts you pick.
Decoders
Section titled “Decoders”A topic whose values are not JSON numbers, such as packed binary or MessagePack, needs a decoder that turns one recorded payload into numbers:
import struct
dataset = client.export(recordings, adamo.Export(state=["proprioception/joints"], action=["left/joint_cmd"]), decoders={ "proprioception/joints": lambda payload: struct.unpack(">8x14f", payload),})An export that names such a topic without its decoder stops before downloading anything, with a ValueError that names the topics missing one.
Exporting audio
Section titled “Exporting audio”Recordings.export_audio decodes the audio of recordings made with record_audio=True into one file per track, for training a voice model:
files = client.export_audio(recordings, "audio", tracks=["mic", "operator-*"])- Each track goes to
audio/{device}/{name}/{track}.wav, at 48 kHz.format=also takes"flac"and"mp3"; any other format is aValueError. trackskeeps the tracks whose name matches any of its patterns:"mic"is the robot’s track of that name, and"operator-*"is every operator’s microphone. Without it, every track is exported. A pattern that matches no track of any recording is named in a warning that lists the tracks the recordings have. When no pattern matches at all, the export stops before downloading anything, with aValueErrorthat lists them.- Each file starts at the recording’s first video frame, or at its first audio packet when it has no camera, with silence wherever the track sent nothing. The tracks of one recording line up with each other and with its video.
Recordings without a matching track are skipped. The Export Dataset button on the Recordings page adds this call when the recordings have audio and you tick Export audio.