LeRobot Datasets
Import a LeRobot Dataset v3 table group, capture frames directly, or train from retained data. These workflows use a conn from Multimodal Tables. Importing a complete dataset and capturing a frame-only table produce different metadata; choose the workflow needed by your training reader.
Load LeRobot Dataset v3
load_from_lerobot imports a local directory, FileIO URI, or Hugging Face
repository. It derives the schema from meta/info.json and writes one row per
frame. The import creates <table>__episodes, <table>__tasks, <table>__info,
and optional <table>__stats and <table>__subtasks companion tables. Task text
remains in the task table; frames retain task_index.
Info and stats use key STRING, value STRING: each top-level info property or
stats feature becomes one row. Each value is JSON-encoded, preserving nested
objects, arrays, nulls, and scalar types. Decode it with json.loads; for example,
fps stores 30, while codebase_version stores "v3.0" (including quotes).
Statistics may contain NaN and Infinity, supported by Python's JSON decoder.
Missing or empty stats create no stats table. The frame table's
pypaimon.lerobot.<component>-table options identify the components created.
pip install 'pypaimon[lerobot]'
conn.load_from_lerobot(
"robot_data",
"/data/lerobot_dataset",
tag_name="initial-import", # Optional: tag all imported component snapshots.
)
The call returns None on success. Omitting tag_name imports the tables
without creating tags. The one-time importer requires a new target table and
a non-empty source dataset; subsequent table edits use the normal Paimon APIs.
For FileIO URIs, pass credentials through source_options:
conn.load_from_lerobot(
"robot_data",
"oss://source-bucket/lerobot_dataset",
source_options={
"fs.oss.endpoint": "oss-cn-hangzhou.aliyuncs.com",
"fs.oss.accessKeyId": "SOURCE_ACCESS_KEY_ID",
"fs.oss.accessKeySecret": "SOURCE_ACCESS_KEY_SECRET",
},
)
Before training, finish any related data/metadata updates and pause writes to this table group. Create a common named tag over the current component snapshots:
tag = "train-2026-09-07"
snapshots = conn.create_lerobot_tag("robot_data", tag)
frames = conn.get_table("robot_data").scan(tag_name=tag).to_arrow()
# Companion tables are ordinary Paimon tables, read with the same tag.
info_table = conn.catalog.get_table("default.robot_data__info").copy(
{"scan.tag-name": tag})
builder = info_table.new_read_builder()
info_rows = builder.new_read().to_arrow(builder.new_scan().plan().splits())
import json
info = {
row["key"]: json.loads(row["value"])
for row in info_rows.to_pylist()
}
create_lerobot_tag returns component names mapped to snapshot IDs; these IDs
may differ across tables. Later appends do not change tagged reads. Read every
required component (including training statistics) through the same tag, and
never fall back to latest if a tag is missing.
Cross-table tagging is not atomic. Use a tag only after the creation call succeeds. A failure may leave partial tags; retry with writes still paused and unchanged snapshots, or choose a new name after repairing the group. Existing tags are never moved to different snapshots. Retain or delete component tags together, and keep writers paused until the call returns.
Scalars map to scalar types, vectors to VECTOR, higher-rank tensors to nested
ARRAY, and images to BLOB. Images keep their compressed bytes.
Video features map to BLOB. Frame rows reference MP4 payloads copied once per
aligned file group. Video imports use the video grouping policy and check
rolling before each Episode. They require a bucket-unaware table. Read them
with a Paimon scan and VideoFrameCollator; PaimonLeRobotDataset currently
supports image features only.
Capture LeRobot frames directly into Paimon
PaimonLeRobotWriter implements the write-side surface used by LeRobot's
recording loop without first creating a LeRobot Parquet/image dataset. Pass the
same user feature mapping that would be passed to LeRobotDataset.create.
The writer adds the standard timestamp, frame_index, episode_index,
index, and task_index features itself.
The writer preserves native LeRobot recording metadata in a managed Paimon
table group. The named table stores frames, while <table>__episodes,
<table>__tasks, <table>__info, and <table>__stats store episode
boundaries, task labels, JSON-encoded dataset information, and global
statistics. When subtask_index is declared, <table>__subtasks stores its
ordered text vocabulary. The root table's managed options identify these
companions.
from pypaimon.multimodal.lerobot import PaimonLeRobotWriter
writer = PaimonLeRobotWriter(
conn,
"recorded_frames",
fps=30,
features=dataset_features,
# Required for a new table when features includes subtask_index:
# subtasks=["approach object", "grasp object"],
)
# LeRobot's record_loop only needs writer.fps, writer.features, and
# writer.add_frame(frame), so the writer can be passed as its dataset argument.
record_loop(..., fps=30, dataset=writer)
writer.save_episode()
# Optional durability/visibility boundary before finalize.
writer.flush()
writer.finalize()
Like native LeRobot, add_frame requires every declared user feature plus a
string task, and rejects caller-provided generated fields. Numeric features
must be NumPy arrays (Torch tensors are converted) with the declared dtype and
shape. Image dimension names may declare CHW or native HWC layout; image values
may be CHW, HWC, or PIL. Images are encoded as PNG bytes and stored in Paimon
BLOB columns; no LeRobot data directory or MP4 is created. video features
remain unsupported. Task text is stored once in the tasks table; frame rows
retain only task_index. Optional subtask text is likewise stored once in the
subtasks table, while each frame supplies its declared NumPy subtask_index.
Subtask labels must be non-empty and unique, and frame indices must reference
that ordered vocabulary. On resume, the writer restores the vocabulary from
the existing table; an explicitly supplied vocabulary must match it exactly.
When save_episode() accepts an episode, the writer uses LeRobot's native
statistics implementation to calculate min, max, mean, std, count,
q01, q10, q50, q90, and q99 for every non-string feature. Flattened
episode statistics are appended to the episodes table. Image statistics use
the encoded PNG frames and LeRobot's sampling, downsampling, CHW, and [0,1]
normalization rules. Each flush aggregates the accepted episode statistics and
replaces the global stats table; it does not rescan frame data.
save_episode accepts the current episode and writes it to a long-lived Paimon
batch writer. The default episodes_per_commit=-1 keeps all completed episodes
in that batch until finalize(). Set a positive threshold for periodic commits,
or call flush() at an operational boundary. save_episode, flush, and
finalize always return None. Calling save_episode() without any buffered
frames raises ValueError, matching native LeRobot.
Call clear_episode_buffer() before save_episode() to discard a re-recorded
episode without advancing frame or episode indices. Once save_episode()
accepts an episode, clear_episode_buffer() no longer affects it, even when the
batch has not yet been committed. finalize() rejects an unfinished episode
instead of silently dropping its frames.
The writer creates a missing table group and appends to an existing compatible
group. Before writing, it requires the root columns, order, Arrow types,
nullability, LeRobot feature metadata, managed options, companion schemas, and
component counts to match. A legacy frame-only writer table is not implicitly
migrated. An existing group must have the stats companion and must not have a
subtasks companion unless its frame schema declares subtask_index. On resume,
new index and episode_index values continue after the published metadata,
existing task and subtask mappings come from their companion tables, and the
episode-local frame_index starts again at zero. Snapshot properties retain
only the next global frame and episode indices; resume restores global
statistics from the stats table and does not scan frame data.
When configured, the first flush() writes the immutable subtask vocabulary.
Each flush appends new tasks and episodes, replaces global stats and info, and
commits frames last. Paimon
does not provide a transaction across these tables. A component commit
exception leaves the group result unknown, makes the writer terminal, and is
not automatically retried. Reopening validates the component state and rejects
a partial batch. Pause writes and use create_lerobot_tag before training to
pin one named snapshot on every component.
Train with Paimon LeRobot data
For map-style training, read a tagged table group created by
load_from_lerobot directly from Paimon. PaimonLeRobotDataset requires the
complete table group; a frame-only table created by PaimonLeRobotWriter is
not sufficient.
from torch.utils.data import DataLoader
from pypaimon.multimodal import PaimonLeRobotDataset
dataset = PaimonLeRobotDataset(
conn.get_table("robot_data"),
tag_name=tag,
)
loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4)
If tag_name is omitted, the latest snapshots are used. Metadata is available
through dataset.meta. Frame lookups use the BTree on index; payload columns
remain lazy.