The SDK ships the join: videosdk_teleop.export reads a recorded session and builds one row per observation. The LeRobot dataset writer lives in the examples repo, because dataset layouts change between LeRobot versions.

export_lerobot.py

Clone the examples repo and run it against a session directory.
--dry-run reports the row count, the measured frame rate and how many rows were joined by id rather than by time. Add --explicit-only to drop the guessed rows entirely.
Run exports on your own machine, not the robot. Transcoding images is CPU heavy, and the robot needs its CPU for the control loop.

Walking a session

build_rows() is pure. It touches no filesystem beyond checking that the images a row references exist, and it does not import LeRobot, so you can test a join without a training stack installed.

Correlation quality

Each row records how its action was matched to its observation. explicit is what you want. temporal happens when the leader never stamped an observation id, and those rows are labelled rather than mixed in silently, so you can filter, weight or reject them.
Even an explicit row is an upper bound rather than proof. The id a command carries is the newest observation the leader had received, and video reaches the operator’s screen by its own path with its own delay, so what they were actually looking at may be slightly older.The follower records both ids for exactly this reason: observation_id is what the leader claimed, observation_id_local is what was current on the robot at that moment. The gap between them in applied.jsonl is the size of the doubt, so you can measure it rather than assume it away.
summarize() also gives you rows, fps_effective, fps_jitter_ms, camera_skew_ms_max, camera_skew_ms_mean, obs_to_applied_ms_mean and rows_limited.
fps_effective is measured from the joint read times rather than declared, so a recording that ran at 27 Hz reports 27 rather than the 30 you asked for. fps_jitter_ms is the spread around it.

What a row holds

action is the clamped value, what the arm was actually told to do. Training on operator intent would teach a policy to command through the safety envelope and rely on a clamp it will not have when deployed. target keeps the pre-clamp intent on the same row, so the difference stays recoverable. Per-camera capture times are kept rather than averaged away, so a wrist camera running 60 ms behind the front camera is visible in the data.

Recording format

The streams these functions read, and the drop counters to check first.

Recording

Turning recording on and bounding episodes.