The most complete format we ship
HDF5 is the format Firsthand’s own QA tooling reads, so it carries the full episode: every stream, the annotations and the calibration, rather than a projection of them.
Delivery format

What an HDF5 delivery includes
HDF5 is the format Firsthand’s own QA tooling reads, so it carries the full episode: every stream, the annotations and the calibration, rather than a projection of them.
Episodes are chunked time-major in 30-frame chunks, so you can read a window of frames from the middle of a long recording without decoding everything before it.
streams/depth_mm, streams/depth_conf, streams/imu_head, streams/imu_wrist_l and streams/imu_wrist_r are their own arrays, each with its own timestamps, so a policy reads only what it consumes.
Every stream keeps its own nanosecond timestamps, so you can recompute the cross-stream residual on your own machine instead of trusting a published number.
Definition
HDF5 is a hierarchical file format that organises data as groups and datasets, a little like folders and arrays, and it is read in Python with h5py. A custom HDF5 robot dataset is egocentric video that a buyer specifies, that is captured against that written spec, and that is delivered in this layout: one group per episode, with the streams, annotations and calibration for that episode inside it.
This page is about ordering a collection with HDF5 as the delivery format. It is the HDF5 counterpart of the custom RLDS dataset and custom LeRobot dataset pages, for teams whose training or evaluation code reads arrays from a file rather than a TFDS or LeRobot layout.
Why HDF5
The first reason is completeness. HDF5 is the format the Firsthand QA tooling reads, so it is the most complete of the delivery formats: nothing is folded into a state vector or left in a separate export. If a team wants to see every stream and every annotation exactly as QA saw it, this is the delivery to ask for.
The second is access pattern. Episodes are stored as one group each, chunked time-major, which suits random access into long episodes. A training loop that samples windows, or an analysis script that inspects one stretch of an episode, reads just the chunks it needs.
The third is verification. HDF5 carries the per-stream nanosecond timestamps, so a team can verify synchronization itself.
Streams
The streams/ group carries the head RGB stream, the two wrist RGB streams, metric depth with a confidence channel, and the IMU streams, each with its own timestamps. Which streams a given delivery actually holds depends on which the written spec ordered: a perception model may need depth more than IMU, while a manipulation policy may lean on wrist motion.
Depth is recovered from the calibrated stereo rig rather than from a standalone depth sensor, and it is registered to the head-camera frame using the solved calibration extrinsics. The three IMUs are 6-DoF sensors, one on the head and one on each wrist, so each is its own array and can be analysed alone or together.
Layout
Each episode is one .h5 file (or one .zarr group) named for the episode, such as EP_0117_KITCHEN_A.h5. Inside it are three groups that mirror the canonical episode schema: streams/, annotations/ and calibration/. The episode’s identity and licence live in meta.json under the same schema: episode_id, skill_id, site_id, participant_id, duration_ns, license and reject_flags.
Units are metric throughout. Depth is in millimetres and hand joints are in metres, in the head frame. T is the number of head-camera frames and N is the number of IMU samples, and they differ because the streams run at different rates. Streams are stored at native rate and resampled by your loader, never re-encoded in the delivery.
The calibration/ group carries intrinsics, extrinsics as SE3 transforms, and rms_reproj_px, along with both the opening and the closing solve, so you can check drift across a session. The head-camera frame is the identity for all extrinsics. For object storage, zarr is the better choice; for a single local file, HDF5.
Chunk shape and compression are stated on the format page as time-major, 30-frame chunks with blosc-zstd level 5, so read whole chunks rather than single frames to avoid decompression overhead. Per-stream chunk shapes beyond that, file sizes and shard sizes are not itemised on this site, and are set per program in the skill spec.
Labels
The annotations/ group holds the labels. The ones most orders ask for are hand_joints, a float32 array of shape [T, 2, 21, 3] in metres, so 21 joints per hand in 3D; hand_visible, the per-joint visibility flag; contact_events; and action_segments, a list of start_ns, end_ns, verb and noun entries that mark sub-steps of a skill.
The episode schema also lists instance_masks (uint16, [T, 480, 848]), body_joints, objects and gaze_target in the same group. Whether a given delivery includes them is a spec decision, so name the ones your policy needs in the skill spec and confirm them on the Spec Pilot batch rather than assuming them.
Comparison
They are three exports of the same QA-passed episodes, so the choice is about your pipeline, not about data availability. HDF5 is the most complete and suits random access and sync verification. RLDS suits TensorFlow-based pipelines and RL-style step sequences, with depth carried per step. LeRobot suits imitation-learning pipelines on that ecosystem and is the smallest on disk.
The practical differences are depth, pose shape and footprint. HDF5 keeps depth, IMU and hand joints as separate arrays; RLDS keeps hand_joints as its own field per step; LeRobot flattens 2×21×3 hand joints, then 24×3 body joints, into one observation.state vector and leaves depth out of the mp4.
Sync
Every active stream is hardware-triggered onto one PTP clock domain, and each stream carries int64 nanosecond timestamps in that domain. The site states a cross-stream sync ceiling of 2.0 ms, hardware-triggered. The IMUs are aligned during calibration with a static hold plus a figure-eight excitation, and the stereo rig that produces depth is validated in the calibration pass.
Because the IMUs sample at 200 Hz and the cameras at 30 or 60 Hz, an episode holds more IMU samples than video frames for the same span of time. Consuming them together means resampling or windowing the IMU data to the frame you need, rather than assuming one sample per frame.
The HDF5 export carries the per-stream timestamps, so you can recompute the residual yourself. The head_rgb dataset exposes them as a timestamp_ns attribute, and the sync-error guide shows how the residual is measured. The Rerun recording plots the same residual against the ceiling if you would rather look first.
Skill spec
Every custom collection starts with a written skill spec: the task your pipeline needs covered, the setting, the object set, the capture tier, the annotation depth and the accept criteria. That spec is quoted for reach, timeline and price before capture begins, and the accept criteria are signed into it.
For an HDF5 order the spec also names which streams and annotations go into each episode group and that the delivery format is HDF5, zarr or both. One attempt at the task is one episode and one group, tagged with skill_id and site_id in meta.json, so any episode traces back to the skill and setting it came from.
Collection and QA
Paid, consented contributors perform the real task against your spec while wearing the capture rig. The instrumented tier is a head-mounted stereo rig: first-person RGB, a metric depth stream recovered from the calibrated stereo rig, and 6-DoF IMU data at 200 Hz from head- and wrist-mounted sensors. The rig is calibrated before the session and again after it. We do not capture robot or teleoperation data, and we do not capture at a customer’s own facility.
QA happens once, against the canonical episode, before any format-specific export is produced, so the HDF5 files inherit the same calibrated, synced, QA-passed data as every other format. An episode is accepted only if RMS reprojection stays under 0.28 px and the drift is within tolerance. Otherwise it goes to the reject log, which ships with every batch. Only footage that clears the signed accept criteria is billed as a validated hour, and rejected items are recaptured at our cost.
Privacy sits in the same gate. Every episode is linked to a signed contributor release under a documented consent chain. Faces and screens in frame are detected and irreversibly blurred at ingest, with a reviewer confirming the result before anything ships, and clips that cannot be de-identified are discarded rather than delivered.
Licence and delivery
You receive the dataset under a perpetual, irrevocable, worldwide commercial licence that is non-exclusive by default, with the buyer owning the resulting dataset and exclusivity available. The same terms apply whichever format the episodes are exported into.
The delivery is the HDF5 (or zarr) episodes and the reject log for the batch.
Speed
Where coverage for the requested tasks and regions is already deep, the median time from signed spec to first validated batch is a median of 48 hours. Where it is thin, turnaround runs three to four weeks while recruiting expands in parallel across more regions. Choosing HDF5 adds no separate stage, because the export is built from episodes that already passed QA.
Getting started
Write down the tasks, environments, objects and camera tier your model needs, name the streams and annotations you want in each episode group, and specify HDF5 or zarr as the delivery format. Most orders begin with a Spec Pilot: a small, fast first batch that proves the spec and the accept criteria hold up on real footage before you scale.
On that pilot, open an episode with h5py and check the stream paths and shapes you depend on. Anything not itemised on the format pages is agreed in the spec before volume is committed.
At a glance
Every collection runs through the same seven-stage end-to-end custom collection pipeline. End-to-end custom data collection is a managed service that takes an AI data need from problem to owned dataset in a single accountable pipeline.
Stream map
| Stream | Dtype, shape and rate | Source | HDF5 path |
|---|---|---|---|
| Head RGB | uint8 [T, 2160, 3840, 3], 60 Hz | Head-mounted stereo rig | streams/head_rgb |
| Wrist RGB | uint8 [T, 1080, 1920, 3], 60 Hz | Left and right wrist cameras | streams/wrist_l, streams/wrist_r |
| Metric depth | uint16 [T, 480, 848], millimetres, 30 Hz | Recovered from the calibrated stereo rig | streams/depth_mm |
| Depth confidence | Per-pixel confidence channel; shape not itemised | Same stereo rig | streams/depth_conf |
| Head IMU | float32 [N, 6], 200 Hz | Head-mounted 6-DoF sensor | streams/imu_head |
| Wrist IMUs | float32 [N, 6], 200 Hz | One 6-DoF sensor per wrist | streams/imu_wrist_l, streams/imu_wrist_r |
| Timestamps | int64 nanoseconds per stream, PTP domain | Hardware-triggered shared clock | *_timestamp_ns; timestamp_ns attribute on each dataset |
Annotations
| Label | What it records | Stated shape and units |
|---|---|---|
| annotations/hand_joints | 3D hand pose, 21 joints per hand | float32 [T, 2, 21, 3], metres, head frame |
| annotations/hand_visible | Per-joint visibility flag | Not itemised |
| annotations/contact_events | Contact events | Not itemised |
| annotations/action_segments | Sub-steps of a skill | List of {start_ns, end_ns, verb, noun} |
| annotations/instance_masks | Instance masks | uint16 [T, 480, 848] |
| body_joints, objects, gaze_target | Also listed in the episode schema annotations group | Not itemised; included only if the spec orders them |
HDF5 vs RLDS vs LeRobot
| HDF5 / zarr | RLDS | LeRobot | |
|---|---|---|---|
| Container | One .h5 or .zarr group per episode | TFDS-compatible tfrecord shards | Parquet + mp4 per camera, Hugging Face datasets layout |
| Read with | h5py (HDF5); zarr | tensorflow_datasets | lerobot.common.datasets.lerobot_dataset |
| Depth | streams/depth_mm and streams/depth_conf | observation.depth_mm per step | Not embedded in the mp4; pull it from the HDF5 export |
| Hand pose | annotations/hand_joints [T, 2, 21, 3] | observation.hand_joints per step | 2×21×3 hand joints flattened into observation.state, then 24×3 body joints |
| Timestamps | int64 nanoseconds per stream, PTP domain | int64 nanoseconds, PTP domain, per stream | Per-frame, aligned to the head-camera clock |
| Layout unit and size | Time-major, 30-frame chunks, blosc-zstd level 5 | ~1.9 GB shards, one skill line per directory | The smaller on-disk option |
| Best for | Random access, sync verification, the most complete export | TensorFlow-based pipelines, RL-style step sequences | Imitation-learning pipelines on the LeRobot ecosystem |
Skill spec
| Spec element | What it decides | Where it shows up |
|---|---|---|
| Task and skill | What one attempt is and how it is named | One group per episode; skill_id in meta.json |
| Setting | Where the task is performed | site_id in meta.json |
| Capture tier | Which streams are recorded | Which paths exist under streams/ |
| Annotation depth | Which labels are produced | Which paths exist under annotations/ |
| Accept criteria | What passes QA | Reject log per batch; rms_reproj_px and reject_flags |
| File and shard sizes | How a batch is split across files | Set per program in the skill spec |
| Delivery format | How the data is packaged | HDF5, zarr or both |
FAQ
Sources
Tell us the tasks, environments and camera tier your model needs, and specify HDF5 or zarr as the delivery format. The same episodes can also ship as RLDS, LeRobot or WebDataset, with a first validated batch in a median of 48 hours where coverage is deep.