Skip to content

Commit 8a0b4ef

Browse files
[python] Import LeRobot video datasets (#9494)
1 parent 1f68a07 commit 8a0b4ef

17 files changed

Lines changed: 1643 additions & 75 deletions

‎docs/docs/pypaimon/lerobot.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ together, and keep writers paused until the call returns.
107107
Scalars map to scalar types, vectors to `VECTOR`, higher-rank tensors to nested
108108
`ARRAY`, and images to `BLOB`. Images keep their compressed bytes.
109109

110+
Video features map to `BLOB`. Frame rows reference MP4 payloads copied once per
111+
aligned file group. Video imports use the video grouping policy and check
112+
rolling before each Episode. They require a bucket-unaware table. Read them
113+
with a Paimon scan and `VideoFrameCollator`; `PaimonLeRobotDataset` currently
114+
supports image features only.
115+
110116
## Capture LeRobot frames directly into Paimon
111117

112118
`PaimonLeRobotWriter` implements the write-side surface used by LeRobot's

‎docs/docs/pypaimon/video.md‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,9 @@ frames.add_video(video, episode_43_rows)
8989
```
9090

9191
The writer deduplicates exact payload descriptor identity inside each `.video`
92-
file. Payload boundaries may be nested across configured video columns, but
93-
every change must occur at an episode boundary. When a file target is reached
94-
within an episode, normal, BLOB, and vector rolling waits for the next payload
95-
boundary and closes the aligned file group together. A single large episode
96-
may therefore exceed the target. The normal `.blob` format is unchanged.
92+
file. Its video grouping policy coordinates normal, BLOB, and vector rolling
93+
at payload boundaries. A file may exceed its target before the next boundary.
94+
The normal `.blob` format is unchanged.
9795

9896
### Update frame rows and replace a video
9997

‎paimon-python/pypaimon/multimodal/lerobot/api.py‎

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
_require_v3,
3838
_schema_from_info,
3939
_validate_v3_required_features,
40+
_video_feature_names,
4041
)
4142
from pypaimon.multimodal.lerobot.source import (
4243
_close_quietly,
@@ -52,6 +53,12 @@
5253
)
5354

5455

56+
_VIDEO_LAYOUT_ERROR = (
57+
"LeRobot video import requires a bucket-unaware target table so each "
58+
"Episode is written by one writer."
59+
)
60+
61+
5562
def load_from_lerobot(
5663
connection,
5764
table_name: str,
@@ -94,14 +101,20 @@ def load_from_lerobot(
94101
_positive_integer(local_info.get("fps"), "fps")
95102
_validated_counts(local_info, resolved_source.path)
96103
_validate_v3_required_features(local_info)
104+
video_fields = _video_feature_names(local_info)
97105
LeRobotDataset = _import_lerobot_dataset()
98106
dataset = _open_resolved_dataset(
99-
LeRobotDataset, resolved_source, local_info)
107+
LeRobotDataset,
108+
resolved_source,
109+
local_info,
110+
download_videos=bool(video_fields),
111+
)
100112
try:
101113
info = dict(dataset.meta.info)
102114
_require_v3(info, resolved_source.path)
103115
_validated_counts(info, resolved_source.path)
104116
_validate_v3_required_features(info)
117+
video_fields = _video_feature_names(info)
105118

106119
lerobot_schema = _schema_from_info(info)
107120
metadata = _load_dataset_metadata(
@@ -117,6 +130,7 @@ def load_from_lerobot(
117130
options,
118131
metadata,
119132
tag_name,
133+
video_fields,
120134
)
121135
finally:
122136
close = getattr(dataset, "close", None)
@@ -134,14 +148,21 @@ def _import_dataset(
134148
batch_size,
135149
options,
136150
metadata,
137-
tag_name):
151+
tag_name,
152+
video_fields):
138153
table = _create_target_table(
139-
connection, table_name, source_schema, options, metadata)
154+
connection,
155+
table_name,
156+
source_schema,
157+
options,
158+
metadata,
159+
video_fields,
160+
)
140161
tables = _prepare_metadata_tables(
141162
connection, table.raw_table, metadata)
142163
episodes_snapshot_id = _append_arrow_tables(
143164
tables["episodes"],
144-
_validated_episode_tables(metadata),
165+
_validated_episode_tables(metadata, video_fields),
145166
)
146167
frames_snapshot_id = None
147168
if int(info["total_frames"]) > 0:
@@ -153,6 +174,7 @@ def _import_dataset(
153174
source_schema,
154175
batch_size,
155176
metadata,
177+
video_fields,
156178
)
157179
_commit_metadata(
158180
connection,
@@ -195,8 +217,25 @@ def _required_count(info, name, source):
195217

196218

197219
def _create_target_table(
198-
connection, table_name, source_schema, options, metadata):
220+
connection, table_name, source_schema, options, metadata,
221+
video_fields=()):
199222
create_options = dict(options or {})
223+
configured = create_options.get("video-frame-field")
224+
if configured is not None:
225+
requested = {
226+
name.strip() for name in str(configured).split(",")
227+
if name.strip()
228+
}
229+
if requested != set(video_fields):
230+
raise ValueError(
231+
"LeRobot video features %s do not match "
232+
"'video-frame-field'=%r."
233+
% (list(video_fields), configured)
234+
)
235+
if video_fields:
236+
if str(create_options.get("bucket", "-1")).strip() != "-1":
237+
raise ValueError(_VIDEO_LAYOUT_ERROR)
238+
create_options["video-frame-field"] = ",".join(video_fields)
200239
managed_options = _managed_table_options(
201240
connection._identifier(table_name), metadata)
202241
reserved_options = set(_COMPANION_OPTION_KEYS.values()).intersection(

0 commit comments

Comments
 (0)