mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-30 21:19:40 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fae2848dd7 | |||
| a4179dd6a1 | |||
| 640096e0d9 | |||
| 51ea892a4f |
@@ -239,6 +239,60 @@ Every module is on by default and can be toggled independently (set to
|
||||
| `--vqa.restrict_to_default_camera` | `false` | Ground VQA only on `--vlm.camera_key` (else every camera). |
|
||||
| `--executor.episode_parallelism` | `16` | Episodes processed concurrently within each phase. |
|
||||
|
||||
## Camera-view curation
|
||||
|
||||
`lerobot-curate-cameras` is a separate, lightweight command that uses the same
|
||||
VLM backend for a **dataset-filtering / curation** pass. It downloads only the
|
||||
**first episode**, then for each camera view asks the VLM to:
|
||||
|
||||
1. **flag** whether the view is blurry / unusable, and
|
||||
2. **label** the view with a canonical name from a closed vocabulary
|
||||
(`top`, `wrist`, `front`, `bottom`, `left`, `right`, plus two-word combos
|
||||
like `left_wrist`).
|
||||
|
||||
It runs in one of two modes:
|
||||
|
||||
- `--mode=report` (default) — write the labels + verdicts into `meta/`
|
||||
(`meta/camera_curation.json` and a `curation` block on each camera in
|
||||
`meta/info.json`). Nothing is moved; this is the cheap triage pass and works
|
||||
for any dataset.
|
||||
- `--mode=rename` — apply the labels by renaming each camera key to
|
||||
`observation.images.<label>`. For **video** datasets this is a
|
||||
**download-free, server-side Hub commit**: the `videos/<key>/` files are moved
|
||||
with the Hub's LFS copy/delete (no video is downloaded or re-encoded), and only
|
||||
the small `meta/` files are edited.
|
||||
|
||||
```bash
|
||||
# Cheap, mutation-free triage (writes meta/camera_curation.json):
|
||||
uv run lerobot-curate-cameras --repo_id=user/dataset --mode=report
|
||||
|
||||
# Apply the labels by renaming camera keys on a new branch (keeps `main` intact):
|
||||
uv run lerobot-curate-cameras --repo_id=user/dataset --mode=rename --branch=curated
|
||||
|
||||
# Run the VLM decision on a GPU via HF Jobs (same --job.* flags as above):
|
||||
uv run lerobot-curate-cameras --repo_id=user/dataset --mode=rename --job.target=h200
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The Hub rename is **in place** on the source repo — the Hub does not support
|
||||
cross-repo LFS copies. Use `--branch` to commit to a branch so `main` is
|
||||
preserved.
|
||||
- **Image** datasets store frames inside the data parquet, so their rename can't
|
||||
avoid touching the data; the rename falls back to a local rewrite (via
|
||||
[`rename_features`](./using_dataset_tools#rename-features)). Prefer `--mode=report`
|
||||
for image datasets.
|
||||
- Views judged unusable are only flagged by default (still renamed). Pass
|
||||
`--drop_unusable=true` (local path) to remove them.
|
||||
- When two cameras get the same label (e.g. two `wrist` views), the tool first
|
||||
tries to disambiguate from each camera's **original key** — a `..._left`
|
||||
source becomes `left_wrist`, `..._right` becomes `right_wrist`. Only labels
|
||||
still colliding after that fall back to `--on_collision` (`error` or `suffix`).
|
||||
|
||||
Key options: `--mode`, `--branch`, `--n_frames`, `--view_vocabulary`,
|
||||
`--allow_combos`, `--on_collision`, `--drop_unusable`, and the shared
|
||||
`--vlm.*` / `--job.*` flags documented above.
|
||||
|
||||
## Contributing new modules
|
||||
|
||||
The pipeline is built to grow, and **contributions are very welcome** —
|
||||
|
||||
@@ -59,7 +59,6 @@ The `lerobot-rollout --strategy.type=dagger` mode requires **teleoperators with
|
||||
|
||||
- `bi_openarm_mini` - Bimanual OpenArm Mini
|
||||
- `so_leader` - SO100 / SO101 leader arm
|
||||
- `bi_so_leader` - Bimanual SO100 / SO101 leader arms
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The provided commands default to `bi_openarm_follower` + `bi_openarm_mini`.
|
||||
|
||||
@@ -82,8 +82,6 @@ By default the env samples objects only from the `lightwheel` registry (what `--
|
||||
|
||||
All eval snippets below mirror the CI command (see `.github/workflows/benchmark_tests.yml`). The `--rename_map` argument maps RoboCasa's native camera keys (`robot0_agentview_left` / `robot0_eye_in_hand` / `robot0_agentview_right`) onto the three-camera (`camera1` / `camera2` / `camera3`) input layout the released `smolvla_robocasa` policy was trained on.
|
||||
|
||||
By default, each task uses the rollout horizon registered by RoboCasa. Set `--env.episode_length=<steps>` to apply the same explicit horizon to every selected task.
|
||||
|
||||
### Single-task evaluation (recommended for quick iteration)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -338,7 +338,7 @@ It is advisable to install one 3-pin cable in the motor after placing them befor
|
||||
<hfoption id="Leader">
|
||||
|
||||
- Mount the leader holder onto the wrist and secure it with 4 M3x6mm screws.
|
||||
- Attach the handle to the leader holder using 1 M2x6mm screw.
|
||||
- Attach the handle to motor 5 using 1 M2x6mm screw.
|
||||
- Insert the gripper motor, secure it with 2 M2x6mm screws on each side, attach a motor horn using a M3x6mm horn screw.
|
||||
- Attach the follower trigger with 4 M3x6mm screws.
|
||||
|
||||
|
||||
@@ -89,6 +89,28 @@ lerobot-edit-dataset \
|
||||
--operation.feature_names "['observation.images.top']"
|
||||
```
|
||||
|
||||
#### Rename Features
|
||||
|
||||
Rename feature keys — typically to canonicalize camera views (e.g.
|
||||
`observation.images.cam_0` → `observation.images.left_wrist`). A rename changes
|
||||
no pixel data, so it is a cheap key-remap: it rewrites `meta/` (info features,
|
||||
episode `videos/*` and `stats/*` columns, `stats.json`), moves the
|
||||
`videos/<key>/` directory, and — for image datasets — renames the embedded
|
||||
image column. Videos are **not** re-encoded.
|
||||
|
||||
```bash
|
||||
# Rename one or more camera keys
|
||||
lerobot-edit-dataset \
|
||||
--repo_id lerobot/pusht \
|
||||
--operation.type rename_features \
|
||||
--operation.name_mapping '{"observation.images.cam_0": "observation.images.left_wrist"}'
|
||||
```
|
||||
|
||||
If two targets collide (e.g. two cameras both labeled `top`), the operation
|
||||
raises by default; pass `--operation.on_collision suffix` to disambiguate
|
||||
deterministically (`top`, `top_2`, …). To label camera views automatically with
|
||||
a VLM, see the [Annotation Pipeline](./annotation_pipeline#camera-view-curation).
|
||||
|
||||
#### Convert to Video
|
||||
|
||||
Convert an image-based dataset to video format, creating a new LeRobotDataset where images are stored as videos. This is useful for reducing storage requirements and improving data loading performance. The new dataset will have the exact same structure as the original, but with images encoded as MP4 videos in the proper LeRobot format.
|
||||
|
||||
@@ -44,7 +44,6 @@ from typing import Protocol
|
||||
|
||||
import numpy as np
|
||||
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.model.kinematics import RobotKinematics
|
||||
from lerobot.processor import (
|
||||
RobotProcessorPipeline,
|
||||
@@ -57,6 +56,7 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||
EEBoundsAndSafety,
|
||||
InverseKinematicsEEToJoints,
|
||||
)
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.constants import HF_LEROBOT_CALIBRATION, HF_LEROBOT_HOME, TELEOPERATORS
|
||||
from lerobot.utils.robot_utils import precise_sleep
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from lerobot.lerobot_types import RobotAction
|
||||
from lerobot.types import RobotAction
|
||||
|
||||
from .base import _GRIPPER_MOTOR_SCALE, IsaacTeleopTeleoperator, _isaacteleop_available
|
||||
from .config_isaac_teleop import SO101LeaderArmConfig
|
||||
|
||||
@@ -32,7 +32,7 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from lerobot.lerobot_types import RobotAction
|
||||
from lerobot.types import RobotAction
|
||||
|
||||
from .base import IsaacTeleopTeleoperator, _isaacteleop_available
|
||||
from .config_isaac_teleop import XRControllerConfig
|
||||
|
||||
@@ -26,8 +26,8 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
from lerobot.configs.types import FeatureType, PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import RobotAction
|
||||
from lerobot.processor import ProcessorStepRegistry, RobotActionProcessorStep
|
||||
from lerobot.types import RobotAction
|
||||
from lerobot.utils.rotation import Rotation
|
||||
|
||||
from .base import _GRIPPER_MOTOR_SCALE
|
||||
|
||||
@@ -21,7 +21,6 @@ from lerobot.cameras.opencv import OpenCVCameraConfig
|
||||
from lerobot.common.control_utils import predict_action
|
||||
from lerobot.configs import FeatureType, PolicyFeature
|
||||
from lerobot.datasets import LeRobotDataset, aggregate_pipeline_dataset_features, create_initial_features
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.model.kinematics import RobotKinematics
|
||||
from lerobot.policies import make_pre_post_processors
|
||||
from lerobot.policies.act import ACTPolicy
|
||||
@@ -39,6 +38,7 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||
ForwardKinematicsJointsToEE,
|
||||
InverseKinematicsEEToJoints,
|
||||
)
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.constants import ACTION, OBS_STR
|
||||
from lerobot.utils.feature_utils import build_dataset_frame, combine_feature_dicts
|
||||
from lerobot.utils.keyboard_input import init_keyboard_listener
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
from lerobot.cameras.opencv import OpenCVCameraConfig
|
||||
from lerobot.datasets import LeRobotDataset, aggregate_pipeline_dataset_features, create_initial_features
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.model.kinematics import RobotKinematics
|
||||
from lerobot.processor import (
|
||||
RobotProcessorPipeline,
|
||||
@@ -37,6 +36,7 @@ from lerobot.scripts.lerobot_record import record_loop
|
||||
from lerobot.teleoperators.phone import Phone, PhoneConfig
|
||||
from lerobot.teleoperators.phone.config_phone import PhoneOS
|
||||
from lerobot.teleoperators.phone.phone_processor import MapPhoneActionToRobotAction
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.feature_utils import combine_feature_dicts
|
||||
from lerobot.utils.keyboard_input import init_keyboard_listener
|
||||
from lerobot.utils.utils import log_say
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import time
|
||||
|
||||
from lerobot.datasets import LeRobotDataset
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.model.kinematics import RobotKinematics
|
||||
from lerobot.processor import (
|
||||
RobotProcessorPipeline,
|
||||
@@ -28,6 +27,7 @@ from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig
|
||||
from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||
InverseKinematicsEEToJoints,
|
||||
)
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.constants import ACTION
|
||||
from lerobot.utils.robot_utils import precise_sleep
|
||||
from lerobot.utils.utils import log_say
|
||||
|
||||
@@ -27,7 +27,6 @@ Highlight, or DAgger via ``lerobot-rollout --strategy.type=...``.
|
||||
|
||||
from lerobot.cameras.opencv import OpenCVCameraConfig
|
||||
from lerobot.configs import PreTrainedConfig
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.model.kinematics import RobotKinematics
|
||||
from lerobot.processor import (
|
||||
RobotProcessorPipeline,
|
||||
@@ -44,6 +43,7 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||
from lerobot.rollout import BaseStrategyConfig, RolloutConfig, build_rollout_context
|
||||
from lerobot.rollout.inference import SyncInferenceConfig
|
||||
from lerobot.rollout.strategies import BaseStrategy
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.process import ProcessSignalHandler
|
||||
from lerobot.utils.utils import init_logging
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
import time
|
||||
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.model.kinematics import RobotKinematics
|
||||
from lerobot.processor import (
|
||||
RobotProcessorPipeline,
|
||||
@@ -32,6 +31,7 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||
from lerobot.teleoperators.phone import Phone, PhoneConfig
|
||||
from lerobot.teleoperators.phone.config_phone import PhoneOS
|
||||
from lerobot.teleoperators.phone.phone_processor import MapPhoneActionToRobotAction
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.robot_utils import precise_sleep
|
||||
from lerobot.utils.visualization_utils import init_rerun, log_rerun_data
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ from lerobot.cameras.opencv import OpenCVCameraConfig
|
||||
from lerobot.common.control_utils import predict_action
|
||||
from lerobot.configs import FeatureType, PolicyFeature
|
||||
from lerobot.datasets import LeRobotDataset, aggregate_pipeline_dataset_features, create_initial_features
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.model.kinematics import RobotKinematics
|
||||
from lerobot.policies import make_pre_post_processors
|
||||
from lerobot.policies.act import ACTPolicy
|
||||
@@ -39,6 +38,7 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||
ForwardKinematicsJointsToEE,
|
||||
InverseKinematicsEEToJoints,
|
||||
)
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.constants import ACTION, OBS_STR
|
||||
from lerobot.utils.feature_utils import build_dataset_frame, combine_feature_dicts
|
||||
from lerobot.utils.keyboard_input import init_keyboard_listener
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
from lerobot.cameras.opencv import OpenCVCameraConfig
|
||||
from lerobot.datasets import LeRobotDataset, aggregate_pipeline_dataset_features, create_initial_features
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.model.kinematics import RobotKinematics
|
||||
from lerobot.processor import (
|
||||
RobotProcessorPipeline,
|
||||
@@ -34,6 +33,7 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||
)
|
||||
from lerobot.scripts.lerobot_record import record_loop
|
||||
from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.feature_utils import combine_feature_dicts
|
||||
from lerobot.utils.keyboard_input import init_keyboard_listener
|
||||
from lerobot.utils.utils import log_say
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
import time
|
||||
|
||||
from lerobot.datasets import LeRobotDataset
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.model.kinematics import RobotKinematics
|
||||
from lerobot.processor import (
|
||||
RobotProcessorPipeline,
|
||||
@@ -29,6 +28,7 @@ from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig
|
||||
from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||
InverseKinematicsEEToJoints,
|
||||
)
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.constants import ACTION
|
||||
from lerobot.utils.robot_utils import precise_sleep
|
||||
from lerobot.utils.utils import log_say
|
||||
|
||||
@@ -25,7 +25,6 @@ forward/inverse kinematics.
|
||||
|
||||
from lerobot.cameras.opencv import OpenCVCameraConfig
|
||||
from lerobot.configs import PreTrainedConfig
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.model.kinematics import RobotKinematics
|
||||
from lerobot.processor import (
|
||||
RobotProcessorPipeline,
|
||||
@@ -42,6 +41,7 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||
from lerobot.rollout import BaseStrategyConfig, RolloutConfig, build_rollout_context
|
||||
from lerobot.rollout.inference import SyncInferenceConfig
|
||||
from lerobot.rollout.strategies import BaseStrategy
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.process import ProcessSignalHandler
|
||||
from lerobot.utils.utils import init_logging
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import time
|
||||
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.model.kinematics import RobotKinematics
|
||||
from lerobot.processor import (
|
||||
RobotProcessorPipeline,
|
||||
@@ -31,6 +30,7 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||
InverseKinematicsEEToJoints,
|
||||
)
|
||||
from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.robot_utils import precise_sleep
|
||||
from lerobot.utils.visualization_utils import init_rerun, log_rerun_data
|
||||
|
||||
|
||||
+7
-2
@@ -67,7 +67,7 @@ dependencies = [
|
||||
"einops>=0.8.0,<0.9.0",
|
||||
|
||||
# Config & Hub
|
||||
"draccus>=0.11.6,<0.12.0",
|
||||
"draccus==0.10.0", # TODO: Relax version constraint
|
||||
"huggingface-hub>=1.0.0,<2.0.0",
|
||||
"requests>=2.32.0,<3.0.0",
|
||||
|
||||
@@ -356,6 +356,7 @@ lerobot-imgtransform-viz="lerobot.scripts.lerobot_imgtransform_viz:main"
|
||||
lerobot-edit-dataset="lerobot.scripts.lerobot_edit_dataset:main"
|
||||
lerobot-setup-can="lerobot.scripts.lerobot_setup_can:main"
|
||||
lerobot-annotate="lerobot.scripts.lerobot_annotate:main"
|
||||
lerobot-curate-cameras="lerobot.scripts.lerobot_curate_cameras:main"
|
||||
lerobot-rollout="lerobot.scripts.lerobot_rollout:main"
|
||||
|
||||
# ---------------- Tool Configurations ----------------
|
||||
@@ -374,7 +375,11 @@ torch = [{ index = "pytorch-cu128", marker = "sys_platform == 'linux'" }]
|
||||
torchvision = [{ index = "pytorch-cu128", marker = "sys_platform == 'linux'" }]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
lerobot = ["envs/*.json", "annotations/steerable_pipeline/prompts/*.txt"]
|
||||
lerobot = [
|
||||
"envs/*.json",
|
||||
"annotations/steerable_pipeline/prompts/*.txt",
|
||||
"annotations/camera_curation/prompts/*.txt",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""VLM camera-view curation for LeRobot datasets.
|
||||
|
||||
For each dataset, the first episode is inspected by a vision-language model to
|
||||
(1) judge whether each camera view is blurry/unusable and (2) assign a canonical
|
||||
view label (``top``/``wrist``/``front``/…). The labels can then be applied by
|
||||
renaming the camera keys — for video datasets via a download-free, server-side
|
||||
Hub commit. Exposed as the ``lerobot-curate-cameras`` CLI.
|
||||
"""
|
||||
|
||||
from .config import DEFAULT_VIEW_VOCABULARY, CameraCurationConfig
|
||||
from .curator import (
|
||||
CameraVerdict,
|
||||
build_name_mapping,
|
||||
build_report,
|
||||
curate_cameras,
|
||||
is_valid_view_label,
|
||||
rename_camera_keys_on_hub,
|
||||
write_report,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_VIEW_VOCABULARY",
|
||||
"CameraCurationConfig",
|
||||
"CameraVerdict",
|
||||
"build_name_mapping",
|
||||
"build_report",
|
||||
"curate_cameras",
|
||||
"is_valid_view_label",
|
||||
"rename_camera_keys_on_hub",
|
||||
"write_report",
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Config for ``lerobot-curate-cameras`` (VLM camera-view curation)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from lerobot.annotations.steerable_pipeline.config import AnnotationJobConfig, VlmConfig
|
||||
|
||||
# The closed vocabulary of canonical camera-view labels. Combos are formed by
|
||||
# joining two of these with ``_`` (e.g. ``left_wrist``).
|
||||
DEFAULT_VIEW_VOCABULARY: tuple[str, ...] = ("top", "wrist", "front", "bottom", "left", "right")
|
||||
|
||||
|
||||
@dataclass
|
||||
class CameraCurationConfig:
|
||||
"""Top-level config for ``lerobot-curate-cameras``.
|
||||
|
||||
The VLM decision only ever reads the first episode (a cheap partial
|
||||
download). ``mode="report"`` writes the labels + quality verdicts into
|
||||
``meta/`` and moves nothing (works for any dataset). ``mode="rename"``
|
||||
additionally renames the camera keys to ``observation.images.<label>`` —
|
||||
for video datasets this is a server-side, download-free Hub commit.
|
||||
"""
|
||||
|
||||
# Hub dataset id (downloaded when ``root`` is unset) — also the rename target.
|
||||
repo_id: str | None = None
|
||||
# Local dataset directory (skips the Hub download).
|
||||
root: Path | None = None
|
||||
|
||||
# "report": write mapping + verdicts into meta/, no file moves.
|
||||
# "rename": physically rename camera keys to observation.images.<label>.
|
||||
mode: str = "report"
|
||||
|
||||
# Commit target branch for the Hub rename; keeps ``main`` intact when set.
|
||||
# None commits to the default branch.
|
||||
branch: str | None = None
|
||||
|
||||
# Episode inspected by the VLM (first episode by default).
|
||||
episode_index: int = 0
|
||||
# Frames sampled from that episode per camera and shown to the VLM.
|
||||
n_frames: int = 4
|
||||
|
||||
# Closed label vocabulary and whether two-token combos (left_wrist) are allowed.
|
||||
view_vocabulary: tuple[str, ...] = DEFAULT_VIEW_VOCABULARY
|
||||
allow_combos: bool = True
|
||||
|
||||
# "error" raises on colliding target labels; "suffix" disambiguates (top -> top_2).
|
||||
on_collision: str = "error"
|
||||
# Remove cameras judged unusable (default: only flag them, still rename).
|
||||
drop_unusable: bool = False
|
||||
|
||||
# Where to write the machine-readable report (default <root>/meta/camera_curation.json).
|
||||
report_path: Path | None = None
|
||||
|
||||
vlm: VlmConfig = field(default_factory=VlmConfig)
|
||||
job: AnnotationJobConfig = field(default_factory=AnnotationJobConfig)
|
||||
|
||||
seed: int = 1729
|
||||
# Keyframe decode backend forwarded to ``decode_video_frames`` (None = default).
|
||||
video_backend: str | None = None
|
||||
|
||||
# Upload the result (rename mode). Kept off by default so runs are dry.
|
||||
push_to_hub: bool = False
|
||||
push_commit_message: str | None = None
|
||||
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Camera-view curation: per-camera VLM quality + label judgments and the
|
||||
lightweight (download-free) Hub rename that applies the chosen labels.
|
||||
|
||||
The decision (:func:`curate_cameras`) is a pure function of a
|
||||
``{camera_key: [frames]}`` map and a VLM client, so it unit-tests with a stub
|
||||
VLM and no dataset. The orchestrating CLI (``lerobot-curate-cameras``) samples
|
||||
those frames from the dataset's first episode.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from collections import Counter
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from lerobot.annotations.steerable_pipeline.frames import to_image_blocks
|
||||
from lerobot.datasets.dataset_tools import _remap_camera_key_in_meta, _resolve_rename_collisions
|
||||
from lerobot.datasets.io_utils import load_info, write_info
|
||||
from lerobot.utils.io_utils import write_json
|
||||
|
||||
from .config import CameraCurationConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PROMPT_PATH = Path(__file__).parent / "prompts" / "camera_curation.txt"
|
||||
|
||||
# The canonical prefix every curated camera key gets.
|
||||
OBS_IMAGE_PREFIX = "observation.images."
|
||||
|
||||
|
||||
@dataclass
|
||||
class CameraVerdict:
|
||||
"""One camera's VLM verdict."""
|
||||
|
||||
camera_key: str
|
||||
usable: bool
|
||||
view_label: str | None
|
||||
blur_reason: str | None = None
|
||||
confidence: float | None = None
|
||||
# Populated by ``build_name_mapping`` once collisions are resolved.
|
||||
proposed_new_key: str | None = None
|
||||
|
||||
|
||||
def _load_prompt() -> str:
|
||||
return _PROMPT_PATH.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def is_valid_view_label(label: str, vocabulary: tuple[str, ...], allow_combos: bool) -> bool:
|
||||
"""True if ``label`` is a single vocab word, or (when allowed) an underscore
|
||||
combo of at most two distinct vocab words."""
|
||||
if not label:
|
||||
return False
|
||||
tokens = label.split("_")
|
||||
if not allow_combos:
|
||||
return len(tokens) == 1 and tokens[0] in vocabulary
|
||||
if not (1 <= len(tokens) <= 2):
|
||||
return False
|
||||
return all(tok in vocabulary for tok in tokens) and len(set(tokens)) == len(tokens)
|
||||
|
||||
|
||||
def _build_messages(frames: list[Any], cfg: CameraCurationConfig) -> list[dict[str, Any]]:
|
||||
if cfg.allow_combos:
|
||||
combo_rule = (
|
||||
"You may combine at most two of these words with an underscore when "
|
||||
"one word is not precise enough (e.g. \"left_wrist\"). "
|
||||
)
|
||||
else:
|
||||
combo_rule = "Use exactly one of these words (no combinations). "
|
||||
prompt = _load_prompt().format(
|
||||
vocabulary=", ".join(cfg.view_vocabulary),
|
||||
combo_rule=combo_rule,
|
||||
)
|
||||
content = [*to_image_blocks(frames), {"type": "text", "text": prompt}]
|
||||
return [{"role": "user", "content": content}]
|
||||
|
||||
|
||||
def _parse_verdict(camera_key: str, result: Any, cfg: CameraCurationConfig) -> CameraVerdict:
|
||||
"""Turn a parsed VLM JSON object into a :class:`CameraVerdict` (defensively)."""
|
||||
if not isinstance(result, dict):
|
||||
return CameraVerdict(camera_key=camera_key, usable=True, view_label=None, blur_reason=None)
|
||||
|
||||
usable = bool(result.get("usable", True))
|
||||
blur_reason = result.get("blur_reason")
|
||||
blur_reason = str(blur_reason) if blur_reason else None
|
||||
|
||||
raw_label = result.get("view_label")
|
||||
label = str(raw_label).strip().lower().replace(" ", "_") if raw_label else ""
|
||||
view_label = label if is_valid_view_label(label, cfg.view_vocabulary, cfg.allow_combos) else None
|
||||
if raw_label and view_label is None:
|
||||
logger.warning(
|
||||
"camera %s: VLM returned view_label=%r which is not in the vocabulary %s; leaving unlabeled",
|
||||
camera_key,
|
||||
raw_label,
|
||||
cfg.view_vocabulary,
|
||||
)
|
||||
|
||||
confidence = result.get("confidence")
|
||||
try:
|
||||
confidence = float(confidence) if confidence is not None else None
|
||||
except (TypeError, ValueError):
|
||||
confidence = None
|
||||
|
||||
return CameraVerdict(
|
||||
camera_key=camera_key,
|
||||
usable=usable,
|
||||
view_label=view_label,
|
||||
blur_reason=blur_reason,
|
||||
confidence=confidence,
|
||||
)
|
||||
|
||||
|
||||
def curate_cameras(
|
||||
frames_by_camera: dict[str, list[Any]],
|
||||
cfg: CameraCurationConfig,
|
||||
vlm: Any,
|
||||
) -> list[CameraVerdict]:
|
||||
"""Judge each camera's quality + view label from a few sampled frames.
|
||||
|
||||
``frames_by_camera`` maps a camera key to a list of decoded frames (torch
|
||||
tensors or PIL images). One batched ``generate_json`` call is issued across
|
||||
all cameras. Cameras with no frames are still reported (usable, unlabeled)
|
||||
so the caller sees the full camera set.
|
||||
"""
|
||||
ordered_keys = list(frames_by_camera)
|
||||
callable_keys = [k for k in ordered_keys if frames_by_camera[k]]
|
||||
|
||||
verdicts: dict[str, CameraVerdict] = {
|
||||
k: CameraVerdict(camera_key=k, usable=True, view_label=None) for k in ordered_keys
|
||||
}
|
||||
|
||||
if callable_keys:
|
||||
messages_batch = [_build_messages(frames_by_camera[k], cfg) for k in callable_keys]
|
||||
results = vlm.generate_json(messages_batch)
|
||||
for key, result in zip(callable_keys, results, strict=True):
|
||||
verdicts[key] = _parse_verdict(key, result, cfg)
|
||||
|
||||
return [verdicts[k] for k in ordered_keys]
|
||||
|
||||
|
||||
def build_name_mapping(
|
||||
verdicts: list[CameraVerdict],
|
||||
existing_features: dict[str, dict],
|
||||
cfg: CameraCurationConfig,
|
||||
) -> dict[str, str]:
|
||||
"""Compute ``{old_key: observation.images.<label>}`` for labeled cameras.
|
||||
|
||||
Cameras without a valid label (or already at their canonical name) are
|
||||
skipped. When several cameras share a label (e.g. two ``wrist`` views), we
|
||||
first try to disambiguate each from a distinguishing vocabulary word found in
|
||||
its *original* key (``..._left`` + ``wrist`` → ``left_wrist``); only labels
|
||||
still colliding after that fall through to ``cfg.on_collision``. The resolved
|
||||
target is written back onto each verdict's ``proposed_new_key``.
|
||||
"""
|
||||
label_by_cam = {v.camera_key: v.view_label for v in verdicts if v.view_label is not None}
|
||||
if cfg.allow_combos:
|
||||
label_by_cam = _disambiguate_from_source_names(label_by_cam, cfg.view_vocabulary)
|
||||
|
||||
desired: dict[str, str] = {}
|
||||
for cam, label in label_by_cam.items():
|
||||
target = f"{OBS_IMAGE_PREFIX}{label}"
|
||||
if target != cam:
|
||||
desired[cam] = target
|
||||
|
||||
if not desired:
|
||||
return {}
|
||||
|
||||
resolved = _resolve_rename_collisions(desired, existing_features, cfg.on_collision)
|
||||
by_key = {v.camera_key: v for v in verdicts}
|
||||
for old, new in resolved.items():
|
||||
by_key[old].proposed_new_key = new
|
||||
return resolved
|
||||
|
||||
|
||||
def _extract_vocab_tokens(camera_key: str, vocabulary: tuple[str, ...]) -> list[str]:
|
||||
"""Vocabulary words present in a camera key, in vocabulary order.
|
||||
|
||||
Splits on non-alphanumeric boundaries so ``observation.images.cam_left`` →
|
||||
``["left"]`` and ``left_wrist_0_rgb`` → ``["wrist", "left"]``.
|
||||
"""
|
||||
parts = set(re.split(r"[^a-z0-9]+", camera_key.lower()))
|
||||
return [tok for tok in vocabulary if tok in parts]
|
||||
|
||||
|
||||
def _order_combo(tokens: list[str], vocabulary: tuple[str, ...]) -> str:
|
||||
"""Join vocab tokens into a combo label, ``wrist`` last, else vocab order.
|
||||
|
||||
Keeps the mount word (``wrist``) as the suffix so directional words read
|
||||
first — ``{wrist, left}`` → ``left_wrist``.
|
||||
"""
|
||||
uniq = list(dict.fromkeys(tokens))
|
||||
|
||||
def sort_key(tok: str) -> tuple[bool, int]:
|
||||
return (tok == "wrist", vocabulary.index(tok) if tok in vocabulary else len(vocabulary))
|
||||
|
||||
return "_".join(sorted(uniq, key=sort_key))
|
||||
|
||||
|
||||
def _disambiguate_from_source_names(
|
||||
label_by_cam: dict[str, str], vocabulary: tuple[str, ...]
|
||||
) -> dict[str, str]:
|
||||
"""When several cameras share a label, enrich each from its source-key words.
|
||||
|
||||
Only labels shared by 2+ cameras are touched; a distinguishing vocab word is
|
||||
pulled from the camera's original key and combined with the label (kept only
|
||||
if the result is a valid ≤2-word combo). Anything still colliding afterwards
|
||||
is left for the caller's collision policy.
|
||||
"""
|
||||
counts = Counter(label_by_cam.values())
|
||||
out = dict(label_by_cam)
|
||||
for cam, label in label_by_cam.items():
|
||||
if counts[label] < 2:
|
||||
continue # unique label — leave the VLM's clean single word alone
|
||||
label_tokens = label.split("_")
|
||||
extra = next(
|
||||
(tok for tok in _extract_vocab_tokens(cam, vocabulary) if tok not in label_tokens), None
|
||||
)
|
||||
if extra is None:
|
||||
continue
|
||||
combined = _order_combo([*label_tokens, extra], vocabulary)
|
||||
if is_valid_view_label(combined, vocabulary, allow_combos=True):
|
||||
out[cam] = combined
|
||||
return out
|
||||
|
||||
|
||||
def build_report(
|
||||
verdicts: list[CameraVerdict],
|
||||
mapping: dict[str, str],
|
||||
cfg: CameraCurationConfig,
|
||||
) -> dict[str, Any]:
|
||||
"""Assemble the machine-readable curation report."""
|
||||
return {
|
||||
"repo_id": cfg.repo_id,
|
||||
"episode_index": cfg.episode_index,
|
||||
"view_vocabulary": list(cfg.view_vocabulary),
|
||||
"cameras": {
|
||||
v.camera_key: {
|
||||
"view_label": v.view_label,
|
||||
"usable": v.usable,
|
||||
"blur_reason": v.blur_reason,
|
||||
"confidence": v.confidence,
|
||||
"proposed_new_key": mapping.get(v.camera_key),
|
||||
}
|
||||
for v in verdicts
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def write_report(
|
||||
root: Path,
|
||||
verdicts: list[CameraVerdict],
|
||||
mapping: dict[str, str],
|
||||
cfg: CameraCurationConfig,
|
||||
) -> Path:
|
||||
"""Write ``meta/camera_curation.json`` and stamp verdicts into ``info.json``.
|
||||
|
||||
Stamping goes into each camera's ``features[key]["info"]["curation"]`` so the
|
||||
verdict travels with the dataset. Returns the report path.
|
||||
"""
|
||||
report = build_report(verdicts, mapping, cfg)
|
||||
default_report_path = root / "meta" / "camera_curation.json"
|
||||
report_path = Path(cfg.report_path) if cfg.report_path is not None else default_report_path
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
write_json(report, report_path)
|
||||
|
||||
info = load_info(root)
|
||||
changed = False
|
||||
for v in verdicts:
|
||||
feature = info.features.get(v.camera_key)
|
||||
if feature is None:
|
||||
continue
|
||||
feature.setdefault("info", {})
|
||||
if feature["info"] is None:
|
||||
feature["info"] = {}
|
||||
feature["info"]["curation"] = {
|
||||
"view_label": v.view_label,
|
||||
"usable": v.usable,
|
||||
"blur_reason": v.blur_reason,
|
||||
"confidence": v.confidence,
|
||||
}
|
||||
changed = True
|
||||
if changed:
|
||||
write_info(info, root)
|
||||
|
||||
return report_path
|
||||
|
||||
|
||||
def _swap_key_in_path(path: str, old_key: str, new_key: str) -> str:
|
||||
"""Rewrite the ``<old_key>`` path segment of a ``videos/<key>/...`` repo path."""
|
||||
prefix = f"videos/{old_key}/"
|
||||
return f"videos/{new_key}/{path[len(prefix):]}" if path.startswith(prefix) else path
|
||||
|
||||
|
||||
def rename_camera_keys_on_hub(
|
||||
repo_id: str,
|
||||
name_mapping: dict[str, str],
|
||||
local_root: Path,
|
||||
*,
|
||||
revision: str | None = None,
|
||||
branch: str | None = None,
|
||||
token: str | None = None,
|
||||
commit_message: str | None = None,
|
||||
) -> Any:
|
||||
"""Rename camera keys on the Hub without downloading video data.
|
||||
|
||||
Edits the small ``meta/`` files locally (under ``local_root``, which must be
|
||||
a writable dataset root whose ``meta/`` is already present), then commits, in
|
||||
one atomic ``create_commit``: ``CommitOperationCopy`` + ``CommitOperationDelete``
|
||||
to move each ``videos/<old>/*`` LFS file server-side, and ``CommitOperationAdd``
|
||||
for the edited meta files. Renames in place on ``repo_id`` (cross-repo copies
|
||||
are unsupported); pass ``branch`` to commit to a branch and keep ``main`` intact.
|
||||
|
||||
Only video keys can be moved this way — reject swaps/cycles and image keys
|
||||
(handled by the local ``rename_features`` path instead).
|
||||
"""
|
||||
from huggingface_hub import CommitOperationAdd, CommitOperationCopy, CommitOperationDelete, HfApi
|
||||
|
||||
# A swap/cycle (a target that is also a source) cannot be expressed in a
|
||||
# single base-revision commit; defer to the local rename path.
|
||||
swaps = set(name_mapping.values()) & set(name_mapping)
|
||||
if swaps:
|
||||
raise NotImplementedError(
|
||||
f"Hub rename cannot swap keys in one commit (offending: {sorted(swaps)}); "
|
||||
"use the local rename_features path for swaps/cycles."
|
||||
)
|
||||
|
||||
# Determine which OLD keys are video-stored (only those have a videos/ tree)
|
||||
# BEFORE remapping the metadata.
|
||||
info = load_info(local_root)
|
||||
video_old_keys = {
|
||||
old for old in name_mapping if info.features.get(old, {}).get("dtype") == "video"
|
||||
}
|
||||
image_old_keys = {
|
||||
old for old in name_mapping if info.features.get(old, {}).get("dtype") == "image"
|
||||
}
|
||||
if image_old_keys:
|
||||
raise NotImplementedError(
|
||||
f"Hub rename cannot move image data stored in the data parquet (keys: {sorted(image_old_keys)}); "
|
||||
"use --mode report (metadata mapping) or the local rename_features path for image datasets."
|
||||
)
|
||||
|
||||
# 1. Rewrite meta/ locally (info features, episodes columns, stats keys).
|
||||
_remap_camera_key_in_meta(local_root, name_mapping)
|
||||
|
||||
api = HfApi(token=token)
|
||||
operations: list[Any] = []
|
||||
|
||||
# 2. Add the (small) meta files we just edited.
|
||||
meta_dir = local_root / "meta"
|
||||
meta_files = [meta_dir / "info.json"]
|
||||
stats_file = meta_dir / "stats.json"
|
||||
if stats_file.exists():
|
||||
meta_files.append(stats_file)
|
||||
meta_files.extend(sorted((meta_dir / "episodes").glob("*/*.parquet")))
|
||||
for fpath in meta_files:
|
||||
rel = fpath.relative_to(local_root).as_posix()
|
||||
operations.append(CommitOperationAdd(path_in_repo=rel, path_or_fileobj=str(fpath)))
|
||||
|
||||
# 3. Move video LFS files server-side (copy + delete), no download.
|
||||
repo_files = api.list_repo_files(repo_id, repo_type="dataset", revision=revision)
|
||||
n_moved = 0
|
||||
for old in video_old_keys:
|
||||
new = name_mapping[old]
|
||||
prefix = f"videos/{old}/"
|
||||
for f in repo_files:
|
||||
if f.startswith(prefix):
|
||||
operations.append(
|
||||
CommitOperationCopy(src_path_in_repo=f, path_in_repo=_swap_key_in_path(f, old, new))
|
||||
)
|
||||
operations.append(CommitOperationDelete(path_in_repo=f))
|
||||
n_moved += 1
|
||||
logger.info(
|
||||
"hub rename: moving %d video file(s) server-side across %d camera(s)",
|
||||
n_moved,
|
||||
len(video_old_keys),
|
||||
)
|
||||
|
||||
commit_info = api.create_commit(
|
||||
repo_id=repo_id,
|
||||
repo_type="dataset",
|
||||
operations=operations,
|
||||
revision=branch or revision,
|
||||
commit_message=commit_message or "curate: rename camera views (lerobot-curate-cameras)",
|
||||
)
|
||||
return commit_info
|
||||
|
||||
|
||||
def as_report_dict(verdicts: list[CameraVerdict]) -> list[dict[str, Any]]:
|
||||
"""Convenience: verdicts as plain dicts (for logging/JSON)."""
|
||||
return [asdict(v) for v in verdicts]
|
||||
@@ -0,0 +1,28 @@
|
||||
You are inspecting frames from ONE camera of a robot manipulation dataset. All
|
||||
frames come from the same fixed camera during a single episode; use them
|
||||
together to judge the camera, not any single moment.
|
||||
|
||||
Do two things and return them as one JSON object.
|
||||
|
||||
1. QUALITY. Decide whether this camera view is usable for training a policy.
|
||||
Mark it UNUSABLE if it is blurry / out of focus, badly over- or
|
||||
under-exposed, mostly occluded, static/frozen, corrupted, or otherwise does
|
||||
not clearly show the scene. Otherwise it is usable.
|
||||
|
||||
2. VIEW LABEL. Choose the single best label for where this camera is mounted /
|
||||
what it looks at, using ONLY this closed vocabulary:
|
||||
|
||||
{vocabulary}
|
||||
|
||||
{combo_rule}Pick the label that best matches the viewpoint (e.g. a
|
||||
downward overhead shot is "top"; a camera on the robot's gripper/hand that
|
||||
moves with the arm is "wrist"). Do not invent words outside the vocabulary.
|
||||
|
||||
Output strictly valid JSON, no prose, no code fences, with exactly these keys:
|
||||
|
||||
{{
|
||||
"usable": true or false,
|
||||
"blur_reason": "<short reason if unusable, else null>",
|
||||
"view_label": "<one label from the vocabulary, combos joined by '_'>",
|
||||
"confidence": <number between 0 and 1>
|
||||
}}
|
||||
@@ -38,7 +38,6 @@ import draccus
|
||||
import grpc
|
||||
import torch
|
||||
|
||||
from lerobot.lerobot_types import PolicyAction
|
||||
from lerobot.policies import get_policy_class, make_pre_post_processors
|
||||
from lerobot.processor import PolicyProcessorPipeline
|
||||
from lerobot.transport import (
|
||||
@@ -46,6 +45,7 @@ from lerobot.transport import (
|
||||
services_pb2_grpc, # type: ignore
|
||||
)
|
||||
from lerobot.transport.utils import receive_bytes_in_chunks
|
||||
from lerobot.types import PolicyAction
|
||||
|
||||
from .configs import PolicyServerConfig
|
||||
from .constants import SUPPORTED_POLICIES
|
||||
|
||||
@@ -35,9 +35,9 @@ else:
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lerobot.datasets import LeRobotDataset
|
||||
from lerobot.lerobot_types import PolicyAction
|
||||
from lerobot.processor import PolicyProcessorPipeline
|
||||
from lerobot.robots import Robot
|
||||
from lerobot.types import PolicyAction
|
||||
|
||||
|
||||
def predict_action(
|
||||
|
||||
@@ -163,10 +163,8 @@ class PreTrainedConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC): # type: igno
|
||||
return None
|
||||
|
||||
def _save_pretrained(self, save_directory: Path) -> None:
|
||||
# Encode against the base class so draccus includes the choice "type" key,
|
||||
# which `from_pretrained` needs to resolve the concrete subclass.
|
||||
with open(save_directory / CONFIG_NAME, "w") as f:
|
||||
json.dump(draccus.encode(self, PreTrainedConfig), f, indent=4)
|
||||
with open(save_directory / CONFIG_NAME, "w") as f, draccus.config_type("json"):
|
||||
draccus.dump(self, f, indent=4)
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(
|
||||
|
||||
@@ -103,10 +103,8 @@ class RewardModelConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC):
|
||||
pass
|
||||
|
||||
def _save_pretrained(self, save_directory: Path) -> None:
|
||||
# Encode against the base class so draccus includes the choice "type" key,
|
||||
# which `from_pretrained` needs to resolve the concrete subclass.
|
||||
with open(save_directory / CONFIG_NAME, "w") as f:
|
||||
json.dump(draccus.encode(self, RewardModelConfig), f, indent=4)
|
||||
with open(save_directory / CONFIG_NAME, "w") as f, draccus.config_type("json"):
|
||||
draccus.dump(self, f, indent=4)
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(
|
||||
|
||||
@@ -194,11 +194,7 @@ class TrainPipelineConfig(HubMixin):
|
||||
)
|
||||
|
||||
if Path(config_path).resolve().exists():
|
||||
# `config_path` may point at the checkpoint's train_config.json or at its
|
||||
# pretrained_model/ directory (both documented above) — resolve either to
|
||||
# the pretrained_model/ directory.
|
||||
config_path_obj = Path(config_path)
|
||||
policy_dir = config_path_obj.parent if config_path_obj.is_file() else config_path_obj
|
||||
policy_dir = Path(config_path).parent
|
||||
self.checkpoint_path = policy_dir.parent
|
||||
elif self.job.is_remote:
|
||||
return
|
||||
|
||||
@@ -33,6 +33,7 @@ from .dataset_tools import (
|
||||
recompute_stats,
|
||||
reencode_dataset,
|
||||
remove_feature,
|
||||
rename_features,
|
||||
split_dataset,
|
||||
)
|
||||
from .factory import make_dataset, make_train_eval_datasets, resolve_delta_timestamps
|
||||
@@ -96,6 +97,7 @@ __all__ = [
|
||||
"recompute_stats",
|
||||
"reencode_dataset",
|
||||
"remove_feature",
|
||||
"rename_features",
|
||||
"resolve_delta_timestamps",
|
||||
"safe_stop_image_writer",
|
||||
"split_dataset",
|
||||
|
||||
@@ -47,6 +47,7 @@ from lerobot.configs import (
|
||||
)
|
||||
from lerobot.configs.video import DEPTH_ENCODER_INFO_FIELD_NAMES
|
||||
from lerobot.utils.constants import ACTION, HF_LEROBOT_HOME, OBS_IMAGE, OBS_STATE
|
||||
from lerobot.utils.io_utils import load_json, write_json
|
||||
from lerobot.utils.utils import flatten_dict
|
||||
|
||||
from .aggregate import aggregate_datasets
|
||||
@@ -60,6 +61,8 @@ from .image_writer import write_image
|
||||
from .io_utils import (
|
||||
get_parquet_file_size_in_mb,
|
||||
load_episodes,
|
||||
load_info,
|
||||
to_parquet_one_row_group_per_episode,
|
||||
write_info,
|
||||
write_stats,
|
||||
write_tasks,
|
||||
@@ -72,7 +75,9 @@ from .utils import (
|
||||
DEFAULT_DATA_PATH,
|
||||
DEFAULT_EPISODES_PATH,
|
||||
DEPTH_FILE_PATTERN,
|
||||
EPISODES_DIR,
|
||||
IMAGE_FILE_PATTERN,
|
||||
STATS_PATH,
|
||||
VIDEO_DIR,
|
||||
update_chunk_file_indices,
|
||||
)
|
||||
@@ -484,6 +489,250 @@ def remove_feature(
|
||||
)
|
||||
|
||||
|
||||
# Columns in ``meta/episodes/*.parquet`` are namespaced by feature key under
|
||||
# these prefixes (e.g. ``videos/observation.images.top/from_timestamp`` and
|
||||
# ``stats/observation.images.top/mean``). Renaming a feature means rewriting the
|
||||
# middle ``<key>`` segment of every such column. Note ``stats/*`` columns are
|
||||
# invisible via ``meta.episodes`` (``load_episodes`` drops them), so we operate
|
||||
# on the raw parquet.
|
||||
_EPISODE_KEY_PREFIXES = ("videos", "stats")
|
||||
|
||||
# Features that must never be renamed (or become a rename target): the dataset
|
||||
# indexing/bookkeeping columns.
|
||||
_REQUIRED_FEATURES = frozenset({"timestamp", "frame_index", "episode_index", "index", "task_index"})
|
||||
|
||||
|
||||
def _resolve_rename_collisions(
|
||||
name_mapping: dict[str, str],
|
||||
existing_features: dict[str, dict],
|
||||
on_collision: str,
|
||||
) -> dict[str, str]:
|
||||
"""Validate/disambiguate a ``{old_key: new_key}`` mapping against collisions.
|
||||
|
||||
The post-rename key set is ``(features \\ sources) ∪ targets``. A collision is
|
||||
either two sources mapping to the same target, or a target equal to an
|
||||
untouched existing key. Swaps/cycles between sources are *not* collisions
|
||||
(handled downstream). ``on_collision="error"`` raises listing every offending
|
||||
pair; ``"suffix"`` disambiguates deterministically (``top`` → ``top_2`` → …)
|
||||
in sorted-source order.
|
||||
"""
|
||||
if on_collision not in ("error", "suffix"):
|
||||
raise ValueError(f"on_collision must be 'error' or 'suffix', got {on_collision!r}")
|
||||
|
||||
sources = set(name_mapping)
|
||||
untouched = set(existing_features) - sources
|
||||
targets = list(name_mapping.values())
|
||||
duplicate_targets = {t for t in targets if targets.count(t) > 1}
|
||||
untouched_collisions = set(targets) & untouched
|
||||
|
||||
if on_collision == "error":
|
||||
problems = []
|
||||
if duplicate_targets:
|
||||
problems.append(f"multiple cameras map to the same target(s): {sorted(duplicate_targets)}")
|
||||
if untouched_collisions:
|
||||
problems.append(
|
||||
f"target(s) collide with existing feature(s) not being renamed: "
|
||||
f"{sorted(untouched_collisions)}"
|
||||
)
|
||||
if problems:
|
||||
raise ValueError(
|
||||
"rename_features collision(s): "
|
||||
+ "; ".join(problems)
|
||||
+ ". Resolve the labels (e.g. use combos like 'left_wrist') or pass "
|
||||
"on_collision='suffix'."
|
||||
)
|
||||
return dict(name_mapping)
|
||||
|
||||
# suffix mode: greedily de-collide in a deterministic (sorted) order.
|
||||
used = set(untouched)
|
||||
resolved: dict[str, str] = {}
|
||||
for src in sorted(name_mapping):
|
||||
target = name_mapping[src]
|
||||
if target in used:
|
||||
base, i = target, 2
|
||||
while target in used:
|
||||
target = f"{base}_{i}"
|
||||
i += 1
|
||||
resolved[src] = target
|
||||
used.add(target)
|
||||
return resolved
|
||||
|
||||
|
||||
def _remap_camera_key_in_meta(root: Path, name_mapping: dict[str, str]) -> None:
|
||||
"""Rename feature keys across the dataset's ``meta/`` files (no file moves).
|
||||
|
||||
Touches: ``meta/info.json`` ``features`` (key renamed, feature dict carried
|
||||
verbatim so codec ``info`` / depth params survive), every
|
||||
``meta/episodes/*/*.parquet`` (``videos/<old>/*`` and ``stats/<old>/*``
|
||||
columns), and ``meta/stats.json`` (top-level ``<old>`` key). All three are
|
||||
simultaneous relabels, so swaps/cycles are safe here.
|
||||
"""
|
||||
# info.json — rebuild features preserving insertion order.
|
||||
info = load_info(root)
|
||||
info.features = {name_mapping.get(key, key): ft for key, ft in info.features.items()}
|
||||
write_info(info, root)
|
||||
|
||||
# episodes parquet — rename namespaced columns by prefix.
|
||||
def _rename_column(col: str) -> str:
|
||||
for prefix in _EPISODE_KEY_PREFIXES:
|
||||
head = f"{prefix}/"
|
||||
if col.startswith(head):
|
||||
rest = col[len(head) :]
|
||||
for old, new in name_mapping.items():
|
||||
if rest == old or rest.startswith(f"{old}/"):
|
||||
return f"{head}{new}{rest[len(old) :]}"
|
||||
return col
|
||||
|
||||
for path in sorted((root / EPISODES_DIR).glob("*/*.parquet")):
|
||||
df = pd.read_parquet(path)
|
||||
col_map = {c: _rename_column(c) for c in df.columns if _rename_column(c) != c}
|
||||
if col_map:
|
||||
df = df.rename(columns=col_map)
|
||||
to_parquet_one_row_group_per_episode(df, path)
|
||||
|
||||
# stats.json — remap top-level feature keys.
|
||||
stats_path = root / STATS_PATH
|
||||
if stats_path.exists():
|
||||
stats = load_json(stats_path)
|
||||
if isinstance(stats, dict):
|
||||
stats = {name_mapping.get(key, key): value for key, value in stats.items()}
|
||||
write_json(stats, stats_path)
|
||||
|
||||
|
||||
def _move_camera_key_dirs(root: Path, name_mapping: dict[str, str]) -> None:
|
||||
"""Move ``videos/<old>`` and ``images/<old>`` trees to their new key names.
|
||||
|
||||
Two-phase (source → sentinel → target) so a swap like ``{a: b, b: a}`` cannot
|
||||
clobber. Missing source dirs are skipped (a key may be stored one way only).
|
||||
"""
|
||||
for subdir in (VIDEO_DIR, "images"):
|
||||
base = root / subdir
|
||||
if not base.exists():
|
||||
continue
|
||||
# Phase 1: move every source to a unique sentinel.
|
||||
sentinels: dict[str, Path] = {}
|
||||
for i, old in enumerate(name_mapping):
|
||||
src = base / old
|
||||
if src.exists():
|
||||
sentinel = base / f".__rename_tmp_{i}__"
|
||||
shutil.move(str(src), str(sentinel))
|
||||
sentinels[old] = sentinel
|
||||
# Phase 2: sentinel → final target.
|
||||
for old, sentinel in sentinels.items():
|
||||
shutil.move(str(sentinel), str(base / name_mapping[old]))
|
||||
|
||||
|
||||
def _rename_image_data_columns(root: Path, name_mapping: dict[str, str]) -> None:
|
||||
"""Rename image-feature columns inside ``data/*.parquet`` at the Arrow level.
|
||||
|
||||
Image datasets embed frames as HF ``Image()`` columns in the data parquet.
|
||||
We rename the Arrow field *and* the matching key in the schema-level
|
||||
``huggingface`` metadata (which references columns by name), so no pixel
|
||||
bytes are decoded or re-embedded and ``datasets`` still types the column as
|
||||
an image after the rename.
|
||||
"""
|
||||
import json
|
||||
|
||||
data_dir = root / DATA_DIR
|
||||
if not data_dir.exists():
|
||||
return
|
||||
for path in sorted(data_dir.glob("*/*.parquet")):
|
||||
table = pq.read_table(path)
|
||||
col_map = {c: name_mapping[c] for c in table.column_names if c in name_mapping}
|
||||
if not col_map:
|
||||
continue
|
||||
table = table.rename_columns([col_map.get(c, c) for c in table.column_names])
|
||||
metadata = dict(table.schema.metadata or {})
|
||||
hf_key = b"huggingface"
|
||||
if hf_key in metadata:
|
||||
hf_meta = json.loads(metadata[hf_key])
|
||||
features = hf_meta.get("info", {}).get("features")
|
||||
if isinstance(features, dict):
|
||||
for old, new in col_map.items():
|
||||
if old in features:
|
||||
features[new] = features.pop(old)
|
||||
metadata[hf_key] = json.dumps(hf_meta).encode()
|
||||
table = table.replace_schema_metadata(metadata)
|
||||
pq.write_table(table, str(path))
|
||||
|
||||
|
||||
def rename_features(
|
||||
dataset: LeRobotDataset,
|
||||
name_mapping: dict[str, str],
|
||||
output_dir: str | Path | None = None,
|
||||
repo_id: str | None = None,
|
||||
*,
|
||||
on_collision: str = "error",
|
||||
) -> LeRobotDataset:
|
||||
"""Rename dataset feature keys without re-encoding any pixel data.
|
||||
|
||||
A rename changes zero frame content, so this does a cheap key-remap rather
|
||||
than the full-copy ``modify_features`` path (which would re-embed images and
|
||||
byte-copy videos). It rewrites ``meta/`` (info features, episodes
|
||||
``videos/*``+``stats/*`` columns, stats.json keys), moves the physical
|
||||
``videos/<key>/`` (and ``images/<key>/``) directories, and — for image
|
||||
datasets — renames the embedded ``data/*.parquet`` image column at the Arrow
|
||||
level. Feature ``info`` dicts (video codec params, depth ``is_depth_map``) are
|
||||
carried verbatim.
|
||||
|
||||
Args:
|
||||
dataset: The source LeRobotDataset.
|
||||
name_mapping: ``{old_feature_key: new_feature_key}``. Identity pairs are
|
||||
ignored. Typically used to canonicalize camera keys, e.g.
|
||||
``{"observation.images.cam_0": "observation.images.left_wrist"}``.
|
||||
output_dir: Where the renamed dataset is written. Defaults to
|
||||
``$HF_LEROBOT_HOME/repo_id``. When it equals ``dataset.root`` the
|
||||
rename is applied in place.
|
||||
repo_id: Identifier for the renamed dataset (default ``<repo_id>_renamed``).
|
||||
on_collision: ``"error"`` (default) raises on colliding targets;
|
||||
``"suffix"`` disambiguates deterministically (``top`` → ``top_2``).
|
||||
|
||||
Returns:
|
||||
The renamed LeRobotDataset.
|
||||
"""
|
||||
if not name_mapping:
|
||||
raise ValueError("name_mapping must be a non-empty {old_key: new_key} dict")
|
||||
|
||||
features = dataset.meta.features
|
||||
mapping = {old: new for old, new in name_mapping.items() if old != new}
|
||||
if not mapping:
|
||||
raise ValueError("name_mapping only contains identity renames (old == new); nothing to do")
|
||||
|
||||
missing = [old for old in mapping if old not in features]
|
||||
if missing:
|
||||
raise ValueError(f"Feature(s) not found in dataset: {missing}")
|
||||
|
||||
bad_required = sorted(
|
||||
{name for pair in mapping.items() for name in pair if name in _REQUIRED_FEATURES}
|
||||
)
|
||||
if bad_required:
|
||||
raise ValueError(f"Cannot rename to/from required features: {bad_required}")
|
||||
|
||||
bad_names = [new for new in mapping.values() if "/" in new]
|
||||
if bad_names:
|
||||
raise ValueError(f"Target feature name(s) cannot contain '/': {bad_names}")
|
||||
|
||||
mapping = _resolve_rename_collisions(mapping, features, on_collision)
|
||||
|
||||
if repo_id is None:
|
||||
repo_id = f"{dataset.repo_id}_renamed"
|
||||
output_dir = Path(output_dir) if output_dir is not None else HF_LEROBOT_HOME / repo_id
|
||||
|
||||
in_place = output_dir.resolve() == Path(dataset.root).resolve()
|
||||
if not in_place:
|
||||
shutil.copytree(dataset.root, output_dir)
|
||||
|
||||
image_keys = set(dataset.meta.image_keys)
|
||||
|
||||
_remap_camera_key_in_meta(output_dir, mapping)
|
||||
_move_camera_key_dirs(output_dir, mapping)
|
||||
image_mapping = {old: new for old, new in mapping.items() if old in image_keys}
|
||||
if image_mapping:
|
||||
_rename_image_data_columns(output_dir, image_mapping)
|
||||
|
||||
return LeRobotDataset(repo_id=repo_id, root=output_dir)
|
||||
|
||||
|
||||
def _fractions_to_episode_indices(
|
||||
total_episodes: int,
|
||||
splits: dict[str, float],
|
||||
|
||||
@@ -17,8 +17,8 @@ from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from lerobot.configs import PipelineFeatureType
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.processor import DataProcessorPipeline
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.constants import ACTION, OBS_IMAGES, OBS_STATE, OBS_STR
|
||||
from lerobot.utils.feature_utils import hw_to_dataset_features
|
||||
|
||||
|
||||
@@ -58,10 +58,6 @@ class LookAheadError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class _ShardExhaustedError(Exception):
|
||||
"""Raised when a streaming dataset shard has no more items."""
|
||||
|
||||
|
||||
class Backtrackable[T]:
|
||||
"""
|
||||
Wrap any iterator/iterable so you can step back up to `history` items
|
||||
@@ -182,7 +178,7 @@ class Backtrackable[T]:
|
||||
"""
|
||||
Check if we can go back `steps` items without raising an IndexError.
|
||||
"""
|
||||
return steps < len(self._back_buf) + self._cursor
|
||||
return steps <= len(self._back_buf) + self._cursor
|
||||
|
||||
def can_peek_ahead(self, steps: int = 1) -> bool:
|
||||
"""
|
||||
@@ -426,7 +422,10 @@ class StreamingLeRobotDataset(torch.utils.data.IterableDataset):
|
||||
else:
|
||||
frames_buffer.append(frame)
|
||||
break # random shard sampled, switch shard
|
||||
except _ShardExhaustedError:
|
||||
except (
|
||||
RuntimeError,
|
||||
StopIteration,
|
||||
): # NOTE: StopIteration inside a generator throws a RuntimeError since python 3.7
|
||||
del idx_to_backtrack_dataset[shard_key] # Remove exhausted shard, onto another shard
|
||||
|
||||
# Once shards are all exhausted, shuffle the buffer and yield the remaining frames
|
||||
@@ -504,11 +503,7 @@ class StreamingLeRobotDataset(torch.utils.data.IterableDataset):
|
||||
|
||||
def make_frame(self, dataset_iterator: Backtrackable) -> Generator:
|
||||
"""Makes a frame starting from a dataset iterator"""
|
||||
try:
|
||||
item = next(dataset_iterator)
|
||||
except StopIteration as e:
|
||||
# Translate exhaustion here, before PEP 479 turns it into an indistinguishable RuntimeError.
|
||||
raise _ShardExhaustedError from e
|
||||
item = next(dataset_iterator)
|
||||
item = item_to_torch(item)
|
||||
|
||||
updates = [] # list of "updates" to apply to the item retrieved from hf_dataset (w/o camera features)
|
||||
|
||||
@@ -507,7 +507,7 @@ class MetaworldEnv(EnvConfig):
|
||||
class RoboCasaEnv(EnvConfig):
|
||||
task: str = "CloseFridge"
|
||||
fps: int = 20
|
||||
episode_length: int | None = None
|
||||
episode_length: int = 1000
|
||||
obs_type: str = "pixels_agent_pos"
|
||||
render_mode: str = "rgb_array"
|
||||
camera_name: str = "robot0_agentview_left,robot0_eye_in_hand,robot0_agentview_right"
|
||||
|
||||
@@ -30,7 +30,7 @@ from gymnasium import spaces
|
||||
from libero.libero import benchmark, get_libero_path
|
||||
from libero.libero.envs import OffScreenRenderEnv
|
||||
|
||||
from lerobot.lerobot_types import RobotObservation
|
||||
from lerobot.types import RobotObservation
|
||||
|
||||
from .utils import _LazyAsyncVectorEnv, parse_camera_names
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import metaworld.policies as policies
|
||||
import numpy as np
|
||||
from gymnasium import spaces
|
||||
|
||||
from lerobot.lerobot_types import RobotObservation
|
||||
from lerobot.types import RobotObservation
|
||||
|
||||
from .utils import _LazyAsyncVectorEnv
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import gymnasium as gym
|
||||
import numpy as np
|
||||
from gymnasium import spaces
|
||||
|
||||
from lerobot.lerobot_types import RobotObservation
|
||||
from lerobot.types import RobotObservation
|
||||
|
||||
from .utils import _LazyAsyncVectorEnv, parse_camera_names
|
||||
|
||||
@@ -98,19 +98,6 @@ def _resolve_tasks(task: str) -> tuple[list[str], str | None]:
|
||||
return names, None
|
||||
|
||||
|
||||
def _get_task_horizon(task: str) -> int:
|
||||
"""Return the rollout horizon registered by RoboCasa for a task."""
|
||||
from robocasa.utils.dataset_registry_utils import get_task_horizon
|
||||
|
||||
try:
|
||||
return int(get_task_horizon(task))
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"No RoboCasa horizon is registered for task '{task}'. "
|
||||
"Set `--env.episode_length=<steps>` explicitly."
|
||||
) from exc
|
||||
|
||||
|
||||
def convert_action(flat_action: np.ndarray) -> dict[str, Any]:
|
||||
"""Split a flat (12,) action vector into a RoboCasa action dict.
|
||||
|
||||
@@ -167,7 +154,7 @@ class RoboCasaEnv(gym.Env):
|
||||
|
||||
self.camera_name = parse_camera_names(camera_name)
|
||||
|
||||
self._max_episode_steps = episode_length if episode_length is not None else _get_task_horizon(task)
|
||||
self._max_episode_steps = episode_length if episode_length is not None else 1000
|
||||
|
||||
# Deferred — created on first reset() inside the worker subprocess
|
||||
# to avoid inheriting stale GPU/EGL contexts across fork().
|
||||
|
||||
@@ -28,7 +28,7 @@ import numpy as np
|
||||
import torch
|
||||
from gymnasium import spaces
|
||||
|
||||
from lerobot.lerobot_types import RobotObservation
|
||||
from lerobot.types import RobotObservation
|
||||
from lerobot.utils.import_utils import _scipy_available
|
||||
|
||||
from .utils import _LazyAsyncVectorEnv
|
||||
|
||||
@@ -37,7 +37,7 @@ import numpy as np
|
||||
from gymnasium import spaces
|
||||
from scipy.spatial.transform import Rotation
|
||||
|
||||
from lerobot.lerobot_types import RobotObservation
|
||||
from lerobot.types import RobotObservation
|
||||
|
||||
from .utils import _LazyAsyncVectorEnv
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Run ``lerobot-curate-cameras`` on HF Jobs (HuggingFace GPUs).
|
||||
|
||||
Same shape as the annotation submitter (``lerobot.jobs.annotate``): the VLM
|
||||
decision needs a GPU, so the pod boots the ``vllm/vllm-openai`` image, installs
|
||||
lerobot on top, and replays the user's CLI with ``lerobot-curate-cameras``. The
|
||||
``--mode=rename`` commit runs from the pod (which holds ``HF_TOKEN``); a bare
|
||||
``--mode=report`` run leaves its output only on the pod, so we warn.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shlex
|
||||
import sys
|
||||
from dataclasses import is_dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from huggingface_hub import HfApi, get_token, run_job
|
||||
|
||||
from .annotate import build_pod_setup
|
||||
from .dataset import ensure_dataset_available
|
||||
from .hf import _pod_forwarded_args, follow_job, resolve_job_tags
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lerobot.annotations.camera_curation.config import CameraCurationConfig
|
||||
|
||||
# Same rationale as the annotate submitter: --root is host-local, --repo_id is
|
||||
# re-emitted, config files can't be read on the pod, and --job could smuggle a
|
||||
# remote target back onto the pod.
|
||||
_SUBMITTER_OWNED_ARGS = ("--root", "--repo_id", "--config_path", "--job")
|
||||
|
||||
|
||||
def _local_config_file_args(cfg: CameraCurationConfig) -> list[str]:
|
||||
return ["--config_path", *(f"--{name}" for name in vars(cfg) if is_dataclass(getattr(cfg, name)))]
|
||||
|
||||
|
||||
def build_pod_command(repo_id: str, lerobot_ref: str, argv: list[str]) -> list[str]:
|
||||
"""``bash -c`` command the pod runs: setup prelude, then curate-cameras."""
|
||||
forwarded = _pod_forwarded_args(argv, drop_names=_SUBMITTER_OWNED_ARGS, drop_prefixes=("--job.",))
|
||||
curate = shlex.join(
|
||||
["lerobot-curate-cameras", f"--repo_id={repo_id}", *forwarded, "--job.target=local"]
|
||||
)
|
||||
return ["bash", "-c", f"{build_pod_setup(lerobot_ref)} && {curate}"]
|
||||
|
||||
|
||||
def submit_curate_to_hf(cfg: CameraCurationConfig) -> None:
|
||||
"""Submit a camera-curation run to HF Jobs and tail its logs."""
|
||||
token = get_token()
|
||||
if not token:
|
||||
raise RuntimeError("Not logged in to Hugging Face. Run `hf auth login` first.")
|
||||
|
||||
if cfg.repo_id is None:
|
||||
raise ValueError(
|
||||
"Remote curation requires --repo_id: the pod downloads the dataset from the Hub, "
|
||||
"and --root only names a directory on this machine."
|
||||
)
|
||||
|
||||
argv = sys.argv[1:]
|
||||
passed = {tok.split("=", 1)[0] for tok in argv}
|
||||
used_config_files = sorted(passed.intersection(_local_config_file_args(cfg)))
|
||||
if used_config_files:
|
||||
raise ValueError(
|
||||
f"{', '.join(used_config_files)} cannot be used with a remote --job.target: the pod "
|
||||
"cannot read config files from this machine. Pass the settings as CLI flags instead."
|
||||
)
|
||||
|
||||
if cfg.mode == "report":
|
||||
print(
|
||||
"WARNING: --mode=report writes its result into the pod's local copy, which is discarded "
|
||||
"when the job ends. Use --mode=rename to commit the result to the Hub."
|
||||
)
|
||||
|
||||
api = HfApi(token=token)
|
||||
tags = resolve_job_tags(cfg.job.tags)
|
||||
ensure_dataset_available(cfg.repo_id, api=api, tags=tags)
|
||||
|
||||
command = build_pod_command(cfg.repo_id, cfg.job.lerobot_ref, argv)
|
||||
|
||||
print(f"Submitting job to HF Jobs (flavor={cfg.job.target}, image={cfg.job.image}) ...")
|
||||
job_info = run_job(
|
||||
image=cfg.job.image,
|
||||
command=command,
|
||||
flavor=cfg.job.target,
|
||||
secrets={"HF_TOKEN": token},
|
||||
timeout=cfg.job.timeout,
|
||||
labels=dict.fromkeys(tags, "true"),
|
||||
)
|
||||
job_id = job_info.id
|
||||
job_url = getattr(job_info, "url", None)
|
||||
print(f"Job submitted: {job_id}")
|
||||
if job_url:
|
||||
print(f" Job page: {job_url}")
|
||||
print(f" Dataset repo: https://huggingface.co/datasets/{cfg.repo_id}")
|
||||
print(f" Monitor: hf jobs logs {job_id}")
|
||||
print(f" Cancel: hf jobs cancel {job_id}")
|
||||
|
||||
if not follow_job(job_id, detach=cfg.job.detach):
|
||||
return
|
||||
|
||||
print("\nCuration complete.")
|
||||
@@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Any
|
||||
import torch
|
||||
|
||||
from lerobot.configs.types import FeatureType, PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import TransitionKey
|
||||
from lerobot.processor import (
|
||||
ComplementaryDataProcessorStep,
|
||||
PolicyAction,
|
||||
@@ -32,6 +31,7 @@ from lerobot.processor import (
|
||||
make_default_policy_processor_steps,
|
||||
make_policy_processor_pipelines,
|
||||
)
|
||||
from lerobot.types import TransitionKey
|
||||
from lerobot.utils.constants import OBS_STATE
|
||||
from lerobot.utils.import_utils import _transformers_available, require_package
|
||||
|
||||
|
||||
@@ -42,9 +42,6 @@ class Evo1Policy(PreTrainedPolicy):
|
||||
config_class = Evo1Config
|
||||
name = "evo1"
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, config: Evo1Config, *, vlm_hub_kwargs: dict | None = None, **kwargs):
|
||||
super().__init__(config)
|
||||
config.validate_features()
|
||||
|
||||
@@ -21,7 +21,6 @@ from typing import Any
|
||||
import torch
|
||||
|
||||
from lerobot.configs import FeatureType, PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||
from lerobot.processor import (
|
||||
AddBatchDimensionProcessorStep,
|
||||
DeviceProcessorStep,
|
||||
@@ -41,6 +40,7 @@ from lerobot.processor.converters import (
|
||||
policy_action_to_transition,
|
||||
transition_to_policy_action,
|
||||
)
|
||||
from lerobot.types import EnvTransition, TransitionKey
|
||||
from lerobot.utils.constants import (
|
||||
ACTION,
|
||||
DONE,
|
||||
|
||||
@@ -28,7 +28,6 @@ if TYPE_CHECKING:
|
||||
|
||||
from lerobot.configs import FeatureType, PreTrainedConfig
|
||||
from lerobot.envs import EnvConfig, env_to_policy_features
|
||||
from lerobot.lerobot_types import PolicyAction
|
||||
from lerobot.processor import (
|
||||
AbsoluteActionsProcessorStep,
|
||||
PolicyProcessorPipeline,
|
||||
@@ -38,6 +37,7 @@ from lerobot.processor import (
|
||||
transition_to_batch,
|
||||
transition_to_policy_action,
|
||||
)
|
||||
from lerobot.types import PolicyAction
|
||||
from lerobot.utils.constants import (
|
||||
ACTION,
|
||||
POLICY_POSTPROCESSOR_DEFAULT_NAME,
|
||||
|
||||
@@ -68,9 +68,6 @@ class GrootPolicy(PreTrainedPolicy):
|
||||
name = "groot"
|
||||
config_class = GrootConfig
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, config: GrootConfig, **kwargs):
|
||||
"""Initialize Groot policy wrapper."""
|
||||
require_package("transformers", extra="groot")
|
||||
|
||||
@@ -50,7 +50,6 @@ if TYPE_CHECKING or _datasets_available:
|
||||
else:
|
||||
LeRobotDataset = None
|
||||
|
||||
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||
from lerobot.processor import (
|
||||
AbsoluteActionsProcessorStep,
|
||||
AddBatchDimensionProcessorStep,
|
||||
@@ -67,6 +66,7 @@ from lerobot.processor import (
|
||||
transition_to_batch,
|
||||
transition_to_policy_action,
|
||||
)
|
||||
from lerobot.types import EnvTransition, TransitionKey
|
||||
from lerobot.utils.constants import (
|
||||
ACTION,
|
||||
OBS_IMAGE,
|
||||
|
||||
@@ -520,9 +520,6 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
||||
config_class = MolmoAct2Config
|
||||
name = "molmoact2"
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
return self.config.inference_action_mode == "continuous"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: MolmoAct2Config,
|
||||
|
||||
@@ -36,7 +36,6 @@ import torch
|
||||
from torch import Tensor
|
||||
|
||||
from lerobot.configs import FeatureType, PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||
from lerobot.processor import (
|
||||
AddBatchDimensionProcessorStep,
|
||||
DeviceProcessorStep,
|
||||
@@ -50,6 +49,7 @@ from lerobot.processor import (
|
||||
policy_action_to_transition,
|
||||
transition_to_policy_action,
|
||||
)
|
||||
from lerobot.types import EnvTransition, TransitionKey
|
||||
from lerobot.utils.constants import (
|
||||
ACTION,
|
||||
OBS_IMAGES,
|
||||
|
||||
@@ -749,9 +749,6 @@ class PI0Policy(PreTrainedPolicy):
|
||||
config_class = PI0Config
|
||||
name = "pi0"
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: PI0Config,
|
||||
|
||||
@@ -714,9 +714,6 @@ class PI05Policy(PreTrainedPolicy):
|
||||
config_class = PI05Config
|
||||
name = "pi05"
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: PI05Config,
|
||||
|
||||
@@ -22,7 +22,6 @@ import numpy as np
|
||||
import torch
|
||||
|
||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||
from lerobot.processor import (
|
||||
AbsoluteActionsProcessorStep,
|
||||
PolicyAction,
|
||||
@@ -34,6 +33,7 @@ from lerobot.processor import (
|
||||
make_default_policy_processor_steps,
|
||||
make_policy_processor_pipelines,
|
||||
)
|
||||
from lerobot.types import EnvTransition, TransitionKey
|
||||
from lerobot.utils.constants import OBS_STATE
|
||||
|
||||
from .configuration_pi05 import PI05Config
|
||||
|
||||
@@ -22,7 +22,6 @@ import numpy as np
|
||||
import torch
|
||||
|
||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||
from lerobot.processor import (
|
||||
AbsoluteActionsProcessorStep,
|
||||
ActionTokenizerProcessorStep,
|
||||
@@ -35,6 +34,7 @@ from lerobot.processor import (
|
||||
make_default_policy_processor_steps,
|
||||
make_policy_processor_pipelines,
|
||||
)
|
||||
from lerobot.types import EnvTransition, TransitionKey
|
||||
from lerobot.utils.constants import OBS_STATE
|
||||
|
||||
from .configuration_pi0_fast import PI0FastConfig
|
||||
|
||||
@@ -249,10 +249,6 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
"""Whether this policy implements Real-Time Chunking inference semantics."""
|
||||
return False
|
||||
|
||||
# TODO(aliberts, rcadene): split into 'forward' and 'compute_loss'?
|
||||
@abc.abstractmethod
|
||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict | None]:
|
||||
|
||||
@@ -145,9 +145,6 @@ class SmolVLAPolicy(PreTrainedPolicy):
|
||||
config_class = SmolVLAConfig
|
||||
name = "smolvla"
|
||||
|
||||
def supports_rtc(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: SmolVLAConfig,
|
||||
|
||||
@@ -168,23 +168,14 @@ class SmolVLMWithExpertModel(nn.Module):
|
||||
last_layers.append(self.num_vlm_layers - 2)
|
||||
frozen_layers = [
|
||||
"lm_head",
|
||||
"text_model.norm.weight",
|
||||
"text_model.model.norm.weight",
|
||||
]
|
||||
for layer in last_layers:
|
||||
frozen_layers.append(f"text_model.layers.{layer}.")
|
||||
frozen_layers.append(f"text_model.model.layers.{layer}.")
|
||||
|
||||
unmatched_patterns = set(frozen_layers)
|
||||
for name, params in self.vlm.named_parameters():
|
||||
matched_patterns = [k for k in frozen_layers if k in name]
|
||||
if matched_patterns:
|
||||
if any(k in name for k in frozen_layers):
|
||||
params.requires_grad = False
|
||||
unmatched_patterns.difference_update(matched_patterns)
|
||||
if unmatched_patterns:
|
||||
raise RuntimeError(
|
||||
"Some frozen layer patterns matched no VLM parameters, so the corresponding layers "
|
||||
"would silently remain trainable (parameter naming may have changed in transformers): "
|
||||
f"{sorted(unmatched_patterns)}"
|
||||
)
|
||||
# To avoid unused params issue with distributed training
|
||||
for name, params in self.lm_expert.named_parameters():
|
||||
if "lm_head" in name:
|
||||
|
||||
@@ -22,7 +22,7 @@ import torch
|
||||
from torch import nn
|
||||
|
||||
from lerobot.configs import FeatureType, PolicyFeature, PreTrainedConfig
|
||||
from lerobot.lerobot_types import PolicyAction, RobotAction, RobotObservation
|
||||
from lerobot.types import PolicyAction, RobotAction, RobotObservation
|
||||
from lerobot.utils.constants import ACTION, OBS_STR
|
||||
from lerobot.utils.feature_utils import build_dataset_frame
|
||||
|
||||
|
||||
@@ -15,13 +15,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from lerobot.configs.policies import PreTrainedConfig
|
||||
from lerobot.configs.types import FeatureType, NormalizationMode, PolicyFeature
|
||||
from lerobot.configs.types import NormalizationMode
|
||||
from lerobot.optim.optimizers import AdamWConfig
|
||||
from lerobot.optim.schedulers import CosineDecayWithWarmupSchedulerConfig
|
||||
from lerobot.utils.constants import OBS_STATE
|
||||
|
||||
|
||||
@PreTrainedConfig.register_subclass("vla_jepa")
|
||||
@@ -124,13 +122,6 @@ class VLAJEPAConfig(PreTrainedConfig):
|
||||
if self.robot_state_feature is not None:
|
||||
self.state_dim = self.robot_state_feature.shape[0]
|
||||
|
||||
def set_dataset_feature_metadata(self, dataset_features: dict[str, Any]) -> None:
|
||||
"""Add `observation.state` to `input_features` if missing, so it gets normalized."""
|
||||
if OBS_STATE in self.input_features or OBS_STATE not in dataset_features:
|
||||
return
|
||||
shape = tuple(dataset_features[OBS_STATE]["shape"])
|
||||
self.input_features[OBS_STATE] = PolicyFeature(type=FeatureType.STATE, shape=shape)
|
||||
|
||||
def get_optimizer_preset(self) -> AdamWConfig:
|
||||
return AdamWConfig(
|
||||
lr=self.optimizer_lr,
|
||||
|
||||
@@ -399,8 +399,7 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
||||
state = batch.get(OBS_STATE)
|
||||
if state is not None:
|
||||
if state.ndim > 2:
|
||||
# deltas are forward-looking here, so index 0 is the current observation, not -1.
|
||||
state = state[:, 0, :]
|
||||
state = state[:, -1, :]
|
||||
inputs["state"] = (state.unsqueeze(1) if state.ndim == 2 else state).float() # [B, 1, dim]
|
||||
|
||||
return inputs
|
||||
|
||||
@@ -150,7 +150,7 @@ class XVLAModel(nn.Module):
|
||||
# Freeze or unfreeze policy transformer
|
||||
if not self.config.train_policy_transformer:
|
||||
for name, param in self.transformer.named_parameters():
|
||||
if "soft_prompt" not in name:
|
||||
if "soft_prompts" not in name:
|
||||
param.requires_grad = False
|
||||
|
||||
# Freeze or unfreeze soft prompts
|
||||
|
||||
@@ -21,7 +21,6 @@ import numpy as np
|
||||
import torch
|
||||
|
||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||
from lerobot.processor import (
|
||||
ObservationProcessorStep,
|
||||
PolicyAction,
|
||||
@@ -32,6 +31,7 @@ from lerobot.processor import (
|
||||
make_default_policy_processor_steps,
|
||||
make_policy_processor_pipelines,
|
||||
)
|
||||
from lerobot.types import EnvTransition, TransitionKey
|
||||
from lerobot.utils.constants import (
|
||||
IMAGENET_STATS,
|
||||
OBS_IMAGES,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from lerobot.lerobot_types import (
|
||||
from lerobot.types import (
|
||||
EnvAction,
|
||||
EnvTransition,
|
||||
PolicyAction,
|
||||
|
||||
@@ -25,7 +25,7 @@ from dataclasses import dataclass, field
|
||||
from torch import Tensor
|
||||
|
||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, PolicyAction
|
||||
from lerobot.types import EnvTransition, PolicyAction
|
||||
from lerobot.utils.constants import OBS_ENV_STATE, OBS_IMAGE, OBS_IMAGES, OBS_STATE
|
||||
|
||||
from .pipeline import (
|
||||
|
||||
@@ -23,7 +23,7 @@ from typing import Any
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from lerobot.lerobot_types import EnvTransition, PolicyAction, RobotAction, RobotObservation, TransitionKey
|
||||
from lerobot.types import EnvTransition, PolicyAction, RobotAction, RobotObservation, TransitionKey
|
||||
from lerobot.utils.constants import ACTION, DONE, INFO, OBS_PREFIX, REWARD, TRUNCATED
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from lerobot.configs import FeatureType, PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import PolicyAction, RobotAction
|
||||
from lerobot.types import PolicyAction, RobotAction
|
||||
|
||||
from .pipeline import ActionProcessorStep, ProcessorStepRegistry, RobotActionProcessorStep
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from typing import Any
|
||||
import torch
|
||||
|
||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, PolicyAction, TransitionKey
|
||||
from lerobot.types import EnvTransition, PolicyAction, TransitionKey
|
||||
from lerobot.utils.device_utils import get_safe_torch_device
|
||||
|
||||
from .pipeline import ProcessorStep, ProcessorStepRegistry
|
||||
|
||||
@@ -20,7 +20,7 @@ from typing import Any
|
||||
import torch
|
||||
|
||||
from lerobot.configs.policies import PreTrainedConfig
|
||||
from lerobot.lerobot_types import PolicyAction, RobotAction, RobotObservation
|
||||
from lerobot.types import PolicyAction, RobotAction, RobotObservation
|
||||
from lerobot.utils.constants import POLICY_POSTPROCESSOR_DEFAULT_NAME, POLICY_PREPROCESSOR_DEFAULT_NAME
|
||||
|
||||
from .batch_processor import AddBatchDimensionProcessorStep
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvAction, EnvTransition, PolicyAction, TransitionKey
|
||||
from lerobot.types import EnvAction, EnvTransition, PolicyAction, TransitionKey
|
||||
|
||||
from .converters import to_tensor
|
||||
from .hil_processor import TELEOP_ACTION_KEY
|
||||
|
||||
@@ -29,7 +29,7 @@ from lerobot.teleoperators.utils import TeleopEvents
|
||||
if TYPE_CHECKING:
|
||||
from lerobot.teleoperators.teleoperator import Teleoperator
|
||||
|
||||
from lerobot.lerobot_types import EnvTransition, PolicyAction, TransitionKey
|
||||
from lerobot.types import EnvTransition, PolicyAction, TransitionKey
|
||||
|
||||
from .pipeline import (
|
||||
ComplementaryDataProcessorStep,
|
||||
|
||||
@@ -25,7 +25,7 @@ import torch
|
||||
from torch import Tensor
|
||||
|
||||
from lerobot.configs import FeatureType, NormalizationMode, PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, PolicyAction, TransitionKey
|
||||
from lerobot.types import EnvTransition, PolicyAction, TransitionKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lerobot.datasets import LeRobotDataset
|
||||
|
||||
@@ -45,14 +45,7 @@ from huggingface_hub import hf_hub_download
|
||||
from safetensors.torch import load_file, save_file
|
||||
|
||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import (
|
||||
EnvAction,
|
||||
EnvTransition,
|
||||
PolicyAction,
|
||||
RobotAction,
|
||||
RobotObservation,
|
||||
TransitionKey,
|
||||
)
|
||||
from lerobot.types import EnvAction, EnvTransition, PolicyAction, RobotAction, RobotObservation, TransitionKey
|
||||
from lerobot.utils.constants import HF_LEROBOT_HOME
|
||||
from lerobot.utils.hub import HubMixin
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from typing import Any
|
||||
import torch
|
||||
|
||||
from lerobot.configs import FeatureType, PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import PolicyAction, RobotAction
|
||||
from lerobot.types import PolicyAction, RobotAction
|
||||
from lerobot.utils.constants import ACTION
|
||||
|
||||
from .pipeline import ActionProcessorStep, ProcessorStepRegistry
|
||||
|
||||
@@ -20,7 +20,7 @@ import torch
|
||||
from torch import Tensor
|
||||
|
||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||
from lerobot.types import EnvTransition, TransitionKey
|
||||
from lerobot.utils.constants import OBS_STATE
|
||||
|
||||
from .delta_action_processor import MapDeltaActionToRobotActionStep, MapTensorToDeltaActionDictStep
|
||||
|
||||
@@ -23,7 +23,7 @@ from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||
from lerobot.configs.recipe import TrainingRecipe
|
||||
from lerobot.datasets.language import LANGUAGE_EVENTS, LANGUAGE_PERSISTENT
|
||||
from lerobot.datasets.language_render import render_sample
|
||||
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||
from lerobot.types import EnvTransition, TransitionKey
|
||||
from lerobot.utils.utils import unwrap_scalar
|
||||
|
||||
from .pipeline import ProcessorStep, ProcessorStepRegistry
|
||||
|
||||
@@ -30,7 +30,7 @@ from typing import TYPE_CHECKING, Any
|
||||
import torch
|
||||
|
||||
from lerobot.configs import FeatureType, PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, RobotObservation, TransitionKey
|
||||
from lerobot.types import EnvTransition, RobotObservation, TransitionKey
|
||||
from lerobot.utils.constants import (
|
||||
ACTION_TOKEN_MASK,
|
||||
ACTION_TOKENS,
|
||||
|
||||
@@ -57,10 +57,10 @@ import torch
|
||||
from tqdm import tqdm
|
||||
|
||||
from lerobot.datasets import LeRobotDataset
|
||||
from lerobot.lerobot_types import TransitionKey
|
||||
from lerobot.rewards.robometer.configuration_robometer import RobometerConfig
|
||||
from lerobot.rewards.robometer.modeling_robometer import RobometerRewardModel
|
||||
from lerobot.rewards.robometer.processor_robometer import RobometerEncoderProcessorStep
|
||||
from lerobot.types import TransitionKey
|
||||
|
||||
DEFAULT_OUTPUT_FILENAME = "robometer_progress.parquet"
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ from PIL import Image
|
||||
from torch import Tensor
|
||||
|
||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||
from lerobot.processor import (
|
||||
AddBatchDimensionProcessorStep,
|
||||
DeviceProcessorStep,
|
||||
@@ -40,6 +39,7 @@ from lerobot.rewards.robometer.configuration_robometer import (
|
||||
RobometerConfig,
|
||||
)
|
||||
from lerobot.rewards.robometer.modeling_robometer import ROBOMETER_FEATURE_PREFIX
|
||||
from lerobot.types import EnvTransition, TransitionKey
|
||||
from lerobot.utils.constants import (
|
||||
OBS_IMAGES,
|
||||
POLICY_POSTPROCESSOR_DEFAULT_NAME,
|
||||
|
||||
@@ -47,7 +47,6 @@ else:
|
||||
Faker = None # type: ignore[assignment, misc]
|
||||
|
||||
from lerobot.configs import FeatureType, PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, PolicyAction, TransitionKey
|
||||
from lerobot.processor import (
|
||||
AddBatchDimensionProcessorStep,
|
||||
DeviceProcessorStep,
|
||||
@@ -59,6 +58,7 @@ from lerobot.processor import (
|
||||
policy_action_to_transition,
|
||||
transition_to_policy_action,
|
||||
)
|
||||
from lerobot.types import EnvTransition, PolicyAction, TransitionKey
|
||||
from lerobot.utils.constants import POLICY_POSTPROCESSOR_DEFAULT_NAME, POLICY_PREPROCESSOR_DEFAULT_NAME
|
||||
|
||||
from .configuration_sarm import SARMConfig
|
||||
|
||||
@@ -48,10 +48,10 @@ import torch
|
||||
from tqdm import tqdm
|
||||
|
||||
from lerobot.datasets import LeRobotDataset
|
||||
from lerobot.lerobot_types import TransitionKey
|
||||
from lerobot.rewards.topreward.configuration_topreward import TOPRewardConfig
|
||||
from lerobot.rewards.topreward.modeling_topreward import TOPRewardModel
|
||||
from lerobot.rewards.topreward.processor_topreward import TOPRewardEncoderProcessorStep
|
||||
from lerobot.types import TransitionKey
|
||||
|
||||
DEFAULT_OUTPUT_FILENAME = "topreward_progress.parquet"
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import torch
|
||||
from torch import Tensor
|
||||
|
||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||
from lerobot.processor import (
|
||||
AddBatchDimensionProcessorStep,
|
||||
DeviceProcessorStep,
|
||||
@@ -38,6 +37,7 @@ from lerobot.rewards.topreward.configuration_topreward import (
|
||||
DEFAULT_PROMPT_SUFFIX_TEMPLATE,
|
||||
TOPRewardConfig,
|
||||
)
|
||||
from lerobot.types import EnvTransition, TransitionKey
|
||||
from lerobot.utils.constants import (
|
||||
OBS_IMAGES,
|
||||
OBS_PREFIX,
|
||||
|
||||
@@ -28,7 +28,7 @@ from huggingface_hub.errors import HfHubHTTPError
|
||||
from safetensors.torch import load_file as load_safetensors, save_file as save_safetensors
|
||||
from torch.optim import Optimizer
|
||||
|
||||
from lerobot.lerobot_types import BatchType
|
||||
from lerobot.types import BatchType
|
||||
from lerobot.utils.hub import HubMixin
|
||||
|
||||
from .configs import RLAlgorithmConfig, TrainingStats
|
||||
|
||||
@@ -16,7 +16,6 @@ from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import builtins
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
@@ -79,10 +78,8 @@ class RLAlgorithmConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC):
|
||||
|
||||
def _save_pretrained(self, save_directory: Path) -> None:
|
||||
"""Serialize this config as ``config.json`` inside ``save_directory``."""
|
||||
# Encode against the base class so draccus includes the choice "type" key,
|
||||
# which `from_pretrained` needs to resolve the concrete subclass.
|
||||
with open(save_directory / CONFIG_NAME, "w") as f:
|
||||
json.dump(draccus.encode(self, RLAlgorithmConfig), f, indent=4)
|
||||
with open(save_directory / CONFIG_NAME, "w") as f, draccus.config_type("json"):
|
||||
draccus.dump(self, f, indent=4)
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(
|
||||
|
||||
@@ -26,7 +26,6 @@ import torch.nn.functional as F # noqa: N812
|
||||
from torch import Tensor
|
||||
from torch.optim import Optimizer
|
||||
|
||||
from lerobot.lerobot_types import BatchType
|
||||
from lerobot.policies.gaussian_actor.modeling_gaussian_actor import (
|
||||
DISCRETE_DIMENSION_INDEX,
|
||||
MLP,
|
||||
@@ -36,6 +35,7 @@ from lerobot.policies.gaussian_actor.modeling_gaussian_actor import (
|
||||
orthogonal_init,
|
||||
)
|
||||
from lerobot.policies.utils import get_device_from_parameters
|
||||
from lerobot.types import BatchType
|
||||
from lerobot.utils.constants import ACTION
|
||||
from lerobot.utils.transition import move_state_dict_to_device
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from lerobot.lerobot_types import BatchType
|
||||
from lerobot.types import BatchType
|
||||
|
||||
from .data_mixer import DataMixer, OnlineOfflineMixer
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from __future__ import annotations
|
||||
|
||||
import abc
|
||||
|
||||
from lerobot.lerobot_types import BatchType
|
||||
from lerobot.types import BatchType
|
||||
|
||||
from ..buffer import ReplayBuffer, concatenate_batch_transitions
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from __future__ import annotations
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
from lerobot.lerobot_types import BatchType
|
||||
from lerobot.types import BatchType
|
||||
|
||||
from .algorithms.base import RLAlgorithm
|
||||
from .algorithms.configs import TrainingStats
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import logging
|
||||
from functools import cached_property
|
||||
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.bimanual import BimanualMixin
|
||||
from lerobot.utils.decorators import check_if_not_connected
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import logging
|
||||
from functools import cached_property
|
||||
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.bimanual import BimanualMixin
|
||||
from lerobot.utils.decorators import check_if_not_connected
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import logging
|
||||
from functools import cached_property
|
||||
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.bimanual import BimanualMixin
|
||||
from lerobot.utils.decorators import check_if_not_connected
|
||||
|
||||
@@ -62,7 +62,6 @@ class BiSOFollower(BimanualMixin, Robot):
|
||||
position_i_coefficient=config.left_arm_config.position_i_coefficient,
|
||||
position_d_coefficient=config.left_arm_config.position_d_coefficient,
|
||||
use_degrees=config.left_arm_config.use_degrees,
|
||||
num_read_retries=config.left_arm_config.num_read_retries,
|
||||
cameras=left_arm_cameras,
|
||||
)
|
||||
|
||||
@@ -76,7 +75,6 @@ class BiSOFollower(BimanualMixin, Robot):
|
||||
position_i_coefficient=config.right_arm_config.position_i_coefficient,
|
||||
position_d_coefficient=config.right_arm_config.position_d_coefficient,
|
||||
use_degrees=config.right_arm_config.use_degrees,
|
||||
num_read_retries=config.right_arm_config.num_read_retries,
|
||||
cameras=config.right_arm_config.cameras,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import cv2
|
||||
import numpy as np
|
||||
import requests
|
||||
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.decorators import check_if_already_connected, check_if_not_connected
|
||||
from lerobot.utils.errors import DeviceNotConnectedError
|
||||
|
||||
|
||||
@@ -19,12 +19,12 @@ import time
|
||||
from functools import cached_property
|
||||
|
||||
from lerobot.cameras import make_cameras_from_configs
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.motors import Motor, MotorNormMode
|
||||
from lerobot.motors.calibration_gui import RangeFinderGUI
|
||||
from lerobot.motors.feetech import (
|
||||
FeetechMotorsBus,
|
||||
)
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.decorators import check_if_already_connected, check_if_not_connected
|
||||
|
||||
from ..robot import Robot
|
||||
|
||||
@@ -19,12 +19,12 @@ import time
|
||||
from functools import cached_property
|
||||
|
||||
from lerobot.cameras import make_cameras_from_configs
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.motors import Motor, MotorNormMode
|
||||
from lerobot.motors.calibration_gui import RangeFinderGUI
|
||||
from lerobot.motors.feetech import (
|
||||
FeetechMotorsBus,
|
||||
)
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.decorators import check_if_already_connected, check_if_not_connected
|
||||
|
||||
from ..robot import Robot
|
||||
|
||||
@@ -19,12 +19,12 @@ import time
|
||||
from functools import cached_property
|
||||
|
||||
from lerobot.cameras import make_cameras_from_configs
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.motors import Motor, MotorCalibration, MotorNormMode
|
||||
from lerobot.motors.dynamixel import (
|
||||
DynamixelMotorsBus,
|
||||
OperatingMode,
|
||||
)
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.decorators import check_if_already_connected, check_if_not_connected
|
||||
|
||||
from ..robot import Robot
|
||||
|
||||
@@ -23,12 +23,12 @@ from typing import Any
|
||||
import numpy as np
|
||||
|
||||
from lerobot.cameras import make_cameras_from_configs
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.motors import Motor, MotorCalibration, MotorNormMode
|
||||
from lerobot.motors.feetech import (
|
||||
FeetechMotorsBus,
|
||||
OperatingMode,
|
||||
)
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.decorators import check_if_already_connected, check_if_not_connected
|
||||
|
||||
from ..robot import Robot
|
||||
|
||||
@@ -22,7 +22,7 @@ from functools import cached_property
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.constants import ACTION, OBS_STATE
|
||||
from lerobot.utils.decorators import check_if_already_connected, check_if_not_connected
|
||||
from lerobot.utils.errors import DeviceNotConnectedError
|
||||
|
||||
@@ -19,13 +19,13 @@ import time
|
||||
from functools import cached_property
|
||||
|
||||
from lerobot.cameras import make_cameras_from_configs
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.motors import Motor, MotorCalibration, MotorNormMode
|
||||
from lerobot.motors.dynamixel import (
|
||||
DriveMode,
|
||||
DynamixelMotorsBus,
|
||||
OperatingMode,
|
||||
)
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.decorators import check_if_already_connected, check_if_not_connected
|
||||
|
||||
from ..robot import Robot
|
||||
|
||||
@@ -20,9 +20,9 @@ from functools import cached_property
|
||||
from typing import Any
|
||||
|
||||
from lerobot.cameras import make_cameras_from_configs
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.motors import Motor, MotorCalibration, MotorNormMode
|
||||
from lerobot.motors.damiao import DamiaoMotorsBus
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.decorators import check_if_already_connected, check_if_not_connected
|
||||
|
||||
from ..robot import Robot
|
||||
|
||||
@@ -19,7 +19,7 @@ import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from lerobot.cameras import make_cameras_from_configs
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.import_utils import _reachy2_sdk_available, require_package
|
||||
|
||||
from ..robot import Robot
|
||||
|
||||
@@ -21,8 +21,8 @@ from functools import cached_property
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from lerobot.cameras import make_cameras_from_configs
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.motors import MotorCalibration
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.decorators import check_if_already_connected, check_if_not_connected
|
||||
from lerobot.utils.import_utils import _motorbridge_available, require_package
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ from pathlib import Path
|
||||
|
||||
import draccus
|
||||
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.motors import MotorCalibration
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.constants import HF_LEROBOT_CALIBRATION, ROBOTS
|
||||
|
||||
from .config import RobotConfig
|
||||
|
||||
@@ -19,12 +19,12 @@ import time
|
||||
from functools import cached_property
|
||||
|
||||
from lerobot.cameras import make_cameras_from_configs
|
||||
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||
from lerobot.motors import Motor, MotorCalibration, MotorNormMode
|
||||
from lerobot.motors.feetech import (
|
||||
FeetechMotorsBus,
|
||||
OperatingMode,
|
||||
)
|
||||
from lerobot.types import RobotAction, RobotObservation
|
||||
from lerobot.utils.decorators import check_if_already_connected, check_if_not_connected
|
||||
|
||||
from ..robot import Robot
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user