English | ζ₯ζ¬θͺ
Decode a ROS bag once. Query it like a database β and bake it into LeRobot training data, as many times as you like.
rebake converts ROS bags (.bag / .mcap) into a queryable Parquet + video dataset, and from that into LeRobot v2.1 training data β as a CLI, a Python package, and a Rust library.
A ROS bag is built for recording, not for using. It's large, slow to load, and serialized message by message β so every analysis or training run begins by re-parsing the ROS bag, rebuilding the transform tree, and re-aligning clocks that never matched. Training formats like LeRobot fix the loading problem but are deliberately lossy: they freeze in one set of choices β which topics, which rate, which features β so changing your mind means starting over from the ROS bag.
rebake decodes a ROS bag once into a queryable Parquet + video intermediate format, and runs everything else β synchronization, transform-tree math, the LeRobot export β on that:
- β‘ Query without deserialization. Each topic becomes a columnar Parquet table, so you read just the fields you need β never the whole message β straight from DuckDB, Polars, or pandas.
- π¦ Smaller, and archival. Per-column compression, plus video for camera streams, makes the intermediate typically 7β10Γ smaller than the ROS bag β while keeping state data losslessly. A durable archive, not just a temp file.
- π§± Typed and structured. Nested ROS messages stay nested, with their types intact β so the fragile, rewrite-it-every-time parsing code disappears.
- π Part of the data ecosystem. Parquet and video are first-class everywhere β DuckDB, Polars, pandas, Arrow, Spark, FFmpeg β so your robot data drops into the tools your team already runs, with nothing rebake-specific to install.
Because the heavy work lives in the intermediate format, re-curating with a different topic set or sample rate never touches the ROS bag again β and LeRobot v2.1 is simply the first export target the pipeline knows how to write.
Note
Each ROS bag needs a small meta.json sidecar (its dataset id, plus segment labels for the full pipeline); the shipped configs already expect it. See docs/metadata.md.
Build it in the dev container:
git clone --recursive https://github.com/airoa-org/rebake.git
cd rebake
docker compose -f docker/docker-compose.yml up -d --build
docker compose -f docker/docker-compose.yml exec rebake-dev bash
# inside the container
just build # β ./target/release/rebake-cliDecode your ROS bags into a queryable intermediate format. Point at a single .bag/.mcap or a whole directory of them; -j converts the ROS bags in parallel, each in its own process:
rebake-cli export ./yubi_recordings -o ./out -j 8Your opaque ROS bags are now plain Parquet and video β explore them with anything, no rebake required:
import pandas as pd
pd.read_parquet("out/<id>/parquet/joint_states.parquet") # also: polars, pyarrowduckdb -c "SELECT * FROM 'out/*/parquet/joint_states.parquet' LIMIT 5"Bake LeRobot v2.1 datasets when you're ready to train β one declarative pipeline, no per-robot code, the same parallel batch over a directory:
rebake-cli run ./yubi_recordings -c config/pipeline/yubi.yaml -j 8lerobot_dataset/
βββ meta/ info.json, episodes.jsonl, tasks.jsonl, episodes_stats.jsonl
βββ data/ one Parquet file per episode
βββ videos/ one video per camera, per episode
It's a standard LeRobot v2.1 dataset β load it with the lerobot library and start training. To re-curate it β a different topic set, sample rate, or feature mapping β re-run pointed at the intermediate format: rebake re-ingests that instead of re-parsing the original ROS bags.
A pipeline is a declarative list of stages that share a context β reorder, add, or drop stages in YAML, no code changes.
# abridged from config/pipeline/yubi.yaml
stage_configs:
- Rosbag2IngestorConfig: {} # read .bag / .mcap
- TfBufferEnricherConfig: {} # build the transform tree
- TfChainEnricherConfig: # compute poses between frame pairs
frame_pairs:
- source: quest_origin
target: quest_hmd
- ZeroOrderHoldTimeSynchronizerConfig: # resample to one timeline
fps: 30
- LeRobotV21TransformerConfig: # write the LeRobot dataset
robot_model: ./config/robot_model/yubi.yaml- Synchronize β resample topics recorded at different rates onto one timeline (zero-order-hold, nearest-neighbor, or timestamp-merge), marking each row
is_freshso a held value is never mistaken for a fresh measurement. - Enrich β build the transform tree from
/tfand/tf_static, then compute end-effector and camera poses between any two frames with forward kinematics (SE(3) composition, lowest-common-ancestor lookup), and derive action labels β the things a policy needs but the ROS bag never stored directly. - Encode β write camera streams to AV1/H.264/H.265 video, and keep depth metric β lossless FFV1 or a compact 10-bit form β instead of crushing 16-bit millimeters into 8-bit RGB.
The full stage list
Ingest (ROS 1/2, or re-ingest a rebake intermediate) Β· Synchronize (ZOH / nearest-neighbor / timestamp-merge) Β· Enrich (TF buffer & chain, joint/transform deltas, action shift, uuid) Β· Encode (RGB & depth video, software or VA-API/NVENC) Β· Export (Parquet + video intermediate) Β· Transform (LeRobot v2.1) Β· Merge (combine datasets without re-encoding). Full reference: docs/configuration.md.
A new robot is one YAML file that maps its ROS topics and field paths to LeRobot features:
# robot_model.yaml
- type: Parquet
topic: /joint_states
field: /position
feature: observation.state
- type: Video
topic: /camera/color/image_raw/compressed
feature: observation.image.head
- type: Parquet
topic: /right_hand/command
field: /position
feature: action.right_hand
- type: Parquet
topic: /left_hand/command
field: /position
feature: action.left_handSee config/robot_model/ for complete examples.
The same stages from Python β built from source with maturin, with zero-copy Arrow/PyArrow exchange. Every stage is Config().build().run(context), all the way to a LeRobot dataset:
from rebake.core import Context
from rebake.encode import VideoEncoderConfig
from rebake.enrich import FramePair, TfBufferEnricherConfig, TfChainEnricherConfig
from rebake.ingest import Rosbag2IngestorConfig
from rebake.synchronize import ZeroOrderHoldTimeSynchronizerConfig
from rebake.transform import LeRobotV21TransformerConfig
context = Context()
context.set_rosbag_path("recording.mcap")
context = Rosbag2IngestorConfig().build().run(context)
context = TfBufferEnricherConfig().build().run(context)
context = TfChainEnricherConfig(
frame_pairs=[FramePair(source="quest_origin", target="quest_hmd")],
).build().run(context)
context = ZeroOrderHoldTimeSynchronizerConfig(fps=10).build().run(context)
context = LeRobotV21TransformerConfig(
outdir="./lerobot_dataset",
robot_model="config/robot_model/yubi.yaml",
video_config=VideoEncoderConfig(fps=10),
).build().run(context)See python/ for the full API and examples.
- Guide β create a dataset for a new robot
- CLI β
run,export,merge - Configuration β pipelines and robot models
- Encoding β RGB and depth codec settings
- Metadata β the
meta.jsonsidecar - Intermediate format β Parquet + video layout
- Hardware acceleration β VA-API and NVENC
- Changelog β what's in this release (rebake is pre-1.0; expect changes)
Issues and pull requests are welcome β see CONTRIBUTING.md and come say hello in Discussions.
Licensed under the Apache License, Version 2.0 β see the LICENSE file for details.
Copyright Β© 2026 AI Robot Association.
