mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-31 13:39:40 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea5cabe6ff | |||
| 228ecd480f | |||
| e4152a2481 | |||
| 4c12ad427f | |||
| 6e196eea0e | |||
| 573e7d0243 | |||
| 4808d8457e | |||
| 6f2e71ec31 | |||
| 59a7d1b0a0 | |||
| 54a2fef9f0 | |||
| 0a4510c74e | |||
| 8e2a077f09 | |||
| 414d0eecbd | |||
| af4d15f9ac | |||
| 1f35876d16 | |||
| 570c58873f | |||
| bff56dda2c | |||
| fc90a56c52 | |||
| efdc920137 | |||
| f2d64506e3 | |||
| 63efc93b0c | |||
| 62600065cd | |||
| 0d0737ab57 | |||
| 1fe58f2d3a | |||
| bd2a796217 | |||
| 4c302572c0 | |||
| 72a1858015 | |||
| 2b578e68f6 | |||
| 7b1419a7fa | |||
| fbe8f5c9da | |||
| d632a103ae | |||
| 7e0fd0d653 | |||
| 0187856202 | |||
| 2939168c33 | |||
| 40a5e70352 | |||
| 0cef9cd197 | |||
| 643ffb4785 | |||
| d59505a735 | |||
| 6ac95363b0 | |||
| ede1fc2978 | |||
| 49d5ea49bc | |||
| d23b65416f | |||
| a6b06eac38 | |||
| 36b8face98 | |||
| cd8984cc0a | |||
| b9ded9e761 | |||
| 185f3e1708 | |||
| e36783253a | |||
| 289e577fc7 | |||
| 9c32722eb9 | |||
| b49cb50e01 | |||
| dd08d4eb53 | |||
| 6e5f6df6e7 | |||
| 265abe6c79 | |||
| b4e2d0b610 | |||
| 5594eba06a | |||
| 207183c2f8 | |||
| 7d615acf9a | |||
| 09572babee | |||
| 35339d31e5 | |||
| f37be3edbe | |||
| 4d076845ac | |||
| 413972c812 | |||
| 0449aa02f6 | |||
| a05c0833e1 | |||
| 7b76d94c5b | |||
| ec2dbc1c98 | |||
| d526785e47 | |||
| 4af7c70664 | |||
| a855570097 | |||
| 167e22ba51 | |||
| 00c25c65c2 | |||
| 23f6d5dabd | |||
| 9b25b7fe0a |
@@ -72,19 +72,19 @@ jobs:
|
|||||||
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/setup-buildx-action@v4 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/login-action@v4.4.0 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
# from source-copy, so code-only changes skip the slow uv-sync layer
|
# from source-copy, so code-only changes skip the slow uv-sync layer
|
||||||
# when the runner has a warm Docker daemon cache.
|
# when the runner has a warm Docker daemon cache.
|
||||||
- name: Build Libero benchmark image
|
- name: Build Libero benchmark image
|
||||||
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses]
|
uses: docker/build-push-action@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.benchmark.libero
|
file: docker/Dockerfile.benchmark.libero
|
||||||
@@ -151,7 +151,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Libero rollout video
|
- name: Upload Libero rollout video
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: libero-rollout-video
|
name: libero-rollout-video
|
||||||
path: /tmp/libero-artifacts/videos/
|
path: /tmp/libero-artifacts/videos/
|
||||||
@@ -159,7 +159,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Libero eval metrics
|
- name: Upload Libero eval metrics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: libero-metrics
|
name: libero-metrics
|
||||||
path: /tmp/libero-artifacts/metrics.json
|
path: /tmp/libero-artifacts/metrics.json
|
||||||
@@ -214,7 +214,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Libero train-smoke eval video
|
- name: Upload Libero train-smoke eval video
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: libero-train-smoke-video
|
name: libero-train-smoke-video
|
||||||
path: /tmp/libero-train-smoke-artifacts/eval/
|
path: /tmp/libero-train-smoke-artifacts/eval/
|
||||||
@@ -230,19 +230,19 @@ jobs:
|
|||||||
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/setup-buildx-action@v4 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/login-action@v4.4.0 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
@@ -250,7 +250,7 @@ jobs:
|
|||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
|
|
||||||
- name: Build MetaWorld benchmark image
|
- name: Build MetaWorld benchmark image
|
||||||
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses]
|
uses: docker/build-push-action@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.benchmark.metaworld
|
file: docker/Dockerfile.benchmark.metaworld
|
||||||
@@ -303,7 +303,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload MetaWorld rollout video
|
- name: Upload MetaWorld rollout video
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: metaworld-rollout-video
|
name: metaworld-rollout-video
|
||||||
path: /tmp/metaworld-artifacts/videos/
|
path: /tmp/metaworld-artifacts/videos/
|
||||||
@@ -311,7 +311,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload MetaWorld eval metrics
|
- name: Upload MetaWorld eval metrics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: metaworld-metrics
|
name: metaworld-metrics
|
||||||
path: /tmp/metaworld-artifacts/metrics.json
|
path: /tmp/metaworld-artifacts/metrics.json
|
||||||
@@ -332,19 +332,19 @@ jobs:
|
|||||||
ROBOTWIN_TASKS: beat_block_hammer,click_bell,handover_block,stack_blocks_two,click_alarmclock,open_microwave,adjust_bottle,lift_pot,stamp_seal,turn_switch
|
ROBOTWIN_TASKS: beat_block_hammer,click_bell,handover_block,stack_blocks_two,click_alarmclock,open_microwave,adjust_bottle,lift_pot,stamp_seal,turn_switch
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/setup-buildx-action@v4 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/login-action@v4.4.0 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
@@ -355,7 +355,7 @@ jobs:
|
|||||||
# simulation assets (~4 GB). Layer cache lives in the runner's local
|
# simulation assets (~4 GB). Layer cache lives in the runner's local
|
||||||
# Docker daemon — reused across re-runs on the same machine.
|
# Docker daemon — reused across re-runs on the same machine.
|
||||||
- name: Build RoboTwin 2.0 benchmark image
|
- name: Build RoboTwin 2.0 benchmark image
|
||||||
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses]
|
uses: docker/build-push-action@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.benchmark.robotwin
|
file: docker/Dockerfile.benchmark.robotwin
|
||||||
@@ -413,7 +413,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload RoboTwin rollout video
|
- name: Upload RoboTwin rollout video
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: robotwin-rollout-video
|
name: robotwin-rollout-video
|
||||||
path: /tmp/robotwin-artifacts/videos/
|
path: /tmp/robotwin-artifacts/videos/
|
||||||
@@ -421,7 +421,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload RoboTwin eval metrics
|
- name: Upload RoboTwin eval metrics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: robotwin-metrics
|
name: robotwin-metrics
|
||||||
path: /tmp/robotwin-artifacts/metrics.json
|
path: /tmp/robotwin-artifacts/metrics.json
|
||||||
@@ -439,19 +439,19 @@ jobs:
|
|||||||
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/setup-buildx-action@v4 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/login-action@v4.4.0 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
@@ -459,7 +459,7 @@ jobs:
|
|||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
|
|
||||||
- name: Build RoboCasa365 benchmark image
|
- name: Build RoboCasa365 benchmark image
|
||||||
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses]
|
uses: docker/build-push-action@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.benchmark.robocasa
|
file: docker/Dockerfile.benchmark.robocasa
|
||||||
@@ -514,7 +514,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload RoboCasa365 rollout video
|
- name: Upload RoboCasa365 rollout video
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: robocasa-rollout-video
|
name: robocasa-rollout-video
|
||||||
path: /tmp/robocasa-artifacts/videos/
|
path: /tmp/robocasa-artifacts/videos/
|
||||||
@@ -522,7 +522,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload RoboCasa365 eval metrics
|
- name: Upload RoboCasa365 eval metrics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: robocasa-metrics
|
name: robocasa-metrics
|
||||||
path: /tmp/robocasa-artifacts/metrics.json
|
path: /tmp/robocasa-artifacts/metrics.json
|
||||||
@@ -540,19 +540,19 @@ jobs:
|
|||||||
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/setup-buildx-action@v4 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/login-action@v4.4.0 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
@@ -560,7 +560,7 @@ jobs:
|
|||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
|
|
||||||
- name: Build RoboCerebra benchmark image
|
- name: Build RoboCerebra benchmark image
|
||||||
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses]
|
uses: docker/build-push-action@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.benchmark.robocerebra
|
file: docker/Dockerfile.benchmark.robocerebra
|
||||||
@@ -621,7 +621,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload RoboCerebra rollout video
|
- name: Upload RoboCerebra rollout video
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: robocerebra-rollout-video
|
name: robocerebra-rollout-video
|
||||||
path: /tmp/robocerebra-artifacts/videos/
|
path: /tmp/robocerebra-artifacts/videos/
|
||||||
@@ -629,7 +629,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload RoboCerebra eval metrics
|
- name: Upload RoboCerebra eval metrics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: robocerebra-metrics
|
name: robocerebra-metrics
|
||||||
path: /tmp/robocerebra-artifacts/metrics.json
|
path: /tmp/robocerebra-artifacts/metrics.json
|
||||||
@@ -648,19 +648,19 @@ jobs:
|
|||||||
ROBOMME_TASKS: PickXtimes,BinFill,StopCube,MoveCube,InsertPeg,SwingXtimes,VideoUnmask,ButtonUnmask,PickHighlight,PatternLock
|
ROBOMME_TASKS: PickXtimes,BinFill,StopCube,MoveCube,InsertPeg,SwingXtimes,VideoUnmask,ButtonUnmask,PickHighlight,PatternLock
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/setup-buildx-action@v4 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/login-action@v4.4.0 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
@@ -668,7 +668,7 @@ jobs:
|
|||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
|
|
||||||
- name: Build RoboMME benchmark image
|
- name: Build RoboMME benchmark image
|
||||||
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses]
|
uses: docker/build-push-action@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.benchmark.robomme
|
file: docker/Dockerfile.benchmark.robomme
|
||||||
@@ -726,7 +726,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload RoboMME rollout video
|
- name: Upload RoboMME rollout video
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: robomme-rollout-video
|
name: robomme-rollout-video
|
||||||
path: /tmp/robomme-artifacts/videos/
|
path: /tmp/robomme-artifacts/videos/
|
||||||
@@ -734,7 +734,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload RoboMME eval metrics
|
- name: Upload RoboMME eval metrics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: robomme-metrics
|
name: robomme-metrics
|
||||||
path: /tmp/robomme-artifacts/metrics.json
|
path: /tmp/robomme-artifacts/metrics.json
|
||||||
@@ -754,19 +754,19 @@ jobs:
|
|||||||
LIBERO_PLUS_TASK_IDS: "[0,100,260,500,1000,1500,2000,2400]"
|
LIBERO_PLUS_TASK_IDS: "[0,100,260,500,1000,1500,2000,2400]"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/setup-buildx-action@v4 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/login-action@v4.4.0 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
@@ -774,7 +774,7 @@ jobs:
|
|||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
|
|
||||||
- name: Build LIBERO-plus benchmark image
|
- name: Build LIBERO-plus benchmark image
|
||||||
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses]
|
uses: docker/build-push-action@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.benchmark.libero_plus
|
file: docker/Dockerfile.benchmark.libero_plus
|
||||||
@@ -834,7 +834,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload LIBERO-plus rollout video
|
- name: Upload LIBERO-plus rollout video
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: libero-plus-rollout-video
|
name: libero-plus-rollout-video
|
||||||
path: /tmp/libero-plus-artifacts/videos/
|
path: /tmp/libero-plus-artifacts/videos/
|
||||||
@@ -842,7 +842,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload LIBERO-plus eval metrics
|
- name: Upload LIBERO-plus eval metrics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: libero-plus-metrics
|
name: libero-plus-metrics
|
||||||
path: /tmp/libero-plus-artifacts/metrics.json
|
path: /tmp/libero-plus-artifacts/metrics.json
|
||||||
@@ -858,19 +858,19 @@ jobs:
|
|||||||
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/setup-buildx-action@v4 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/login-action@v4.4.0 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
@@ -878,7 +878,7 @@ jobs:
|
|||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
|
|
||||||
- name: Build VLABench benchmark image
|
- name: Build VLABench benchmark image
|
||||||
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses]
|
uses: docker/build-push-action@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.benchmark.vlabench
|
file: docker/Dockerfile.benchmark.vlabench
|
||||||
@@ -936,7 +936,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload VLABench rollout video
|
- name: Upload VLABench rollout video
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: vlabench-rollout-video
|
name: vlabench-rollout-video
|
||||||
path: /tmp/vlabench-artifacts/videos/
|
path: /tmp/vlabench-artifacts/videos/
|
||||||
@@ -944,7 +944,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload VLABench eval metrics
|
- name: Upload VLABench eval metrics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: vlabench-metrics
|
name: vlabench-metrics
|
||||||
path: /tmp/vlabench-artifacts/metrics.json
|
path: /tmp/vlabench-artifacts/metrics.json
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -52,21 +52,21 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install git-lfs
|
sudo apt-get install git-lfs
|
||||||
git lfs install
|
git lfs install
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/setup-buildx-action@v4 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/login-action@v4.4.0 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
- name: Build and push Docker image CPU
|
- name: Build and push Docker image CPU
|
||||||
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses]
|
uses: docker/build-push-action@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/Dockerfile.user
|
file: ./docker/Dockerfile.user
|
||||||
@@ -87,21 +87,21 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install git-lfs
|
sudo apt-get install git-lfs
|
||||||
git lfs install
|
git lfs install
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/setup-buildx-action@v4 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/login-action@v4.4.0 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
- name: Build and push Docker image GPU
|
- name: Build and push Docker image GPU
|
||||||
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses]
|
uses: docker/build-push-action@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/Dockerfile.internal
|
file: ./docker/Dockerfile.internal
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
github.event.workflow_run.event == 'pull_request' &&
|
github.event.workflow_run.event == 'pull_request' &&
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
github.repository == 'huggingface/lerobot'
|
github.repository == 'huggingface/lerobot'
|
||||||
uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@2430c1ec91d04667414e2fa31ecfc36c153ea391 # main
|
uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@6108e850ae1cf2f71bb0815a600bcd50c39abfa7 # main
|
||||||
with:
|
with:
|
||||||
package_name: lerobot
|
package_name: lerobot
|
||||||
secrets:
|
secrets:
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
github.repository == 'huggingface/lerobot'
|
github.repository == 'huggingface/lerobot'
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@e60a538eea9817ab312196d0d233604b01697265 # main
|
uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@6108e850ae1cf2f71bb0815a600bcd50c39abfa7 # main
|
||||||
with:
|
with:
|
||||||
commit_sha: ${{ github.sha }}
|
commit_sha: ${{ github.sha }}
|
||||||
package: lerobot
|
package: lerobot
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@e60a538eea9817ab312196d0d233604b01697265 # main
|
uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@6108e850ae1cf2f71bb0815a600bcd50c39abfa7 # main
|
||||||
with:
|
with:
|
||||||
commit_sha: ${{ github.event.pull_request.head.sha }}
|
commit_sha: ${{ github.event.pull_request.head.sha }}
|
||||||
pr_number: ${{ github.event.number }}
|
pr_number: ${{ github.event.number }}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot
|
HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot
|
||||||
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
lfs: true
|
lfs: true
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
libusb-1.0-0-dev speech-dispatcher libgeos-dev portaudio19-dev
|
libusb-1.0-0-dev speech-dispatcher libgeos-dev portaudio19-dev
|
||||||
|
|
||||||
- name: Setup uv and Python
|
- name: Setup uv and Python
|
||||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
version: ${{ env.UV_VERSION }}
|
version: ${{ env.UV_VERSION }}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot
|
HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot
|
||||||
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
speech-dispatcher libgeos-dev portaudio19-dev
|
speech-dispatcher libgeos-dev portaudio19-dev
|
||||||
|
|
||||||
- name: Setup uv and Python
|
- name: Setup uv and Python
|
||||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
version: ${{ env.UV_VERSION }}
|
version: ${{ env.UV_VERSION }}
|
||||||
@@ -137,21 +137,21 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install git-lfs
|
sudo apt-get install git-lfs
|
||||||
git lfs install
|
git lfs install
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/Dockerfile.internal
|
file: ./docker/Dockerfile.internal
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'huggingface/lerobot'
|
if: github.repository == 'huggingface/lerobot'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v8
|
- uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
// Setup Input Text
|
// Setup Input Text
|
||||||
|
|||||||
@@ -48,12 +48,12 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
changed: ${{ steps.diff.outputs.changed }}
|
changed: ${{ steps.diff.outputs.changed }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup uv and Python
|
- name: Setup uv and Python
|
||||||
uses: astral-sh/setup-uv@v6 # zizmor: ignore[unpinned-uses]
|
uses: astral-sh/setup-uv@v8.3.2 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
version: ${{ env.UV_VERSION }}
|
version: ${{ env.UV_VERSION }}
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload updated lockfile
|
- name: Upload updated lockfile
|
||||||
if: steps.diff.outputs.changed == 'true'
|
if: steps.diff.outputs.changed == 'true'
|
||||||
uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/upload-artifact@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: uv-lock
|
name: uv-lock
|
||||||
path: uv.lock
|
path: uv.lock
|
||||||
@@ -93,13 +93,13 @@ jobs:
|
|||||||
HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot
|
HF_LEROBOT_HOME: /mnt/cache/.cache/huggingface/lerobot
|
||||||
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
HF_USER_TOKEN: ${{ secrets.LEROBOT_HF_USER }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download updated lockfile
|
- name: Download updated lockfile
|
||||||
uses: actions/download-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/download-artifact@v8 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: uv-lock
|
name: uv-lock
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
speech-dispatcher libgeos-dev portaudio19-dev
|
speech-dispatcher libgeos-dev portaudio19-dev
|
||||||
|
|
||||||
- name: Setup uv and Python
|
- name: Setup uv and Python
|
||||||
uses: astral-sh/setup-uv@v6 # zizmor: ignore[unpinned-uses]
|
uses: astral-sh/setup-uv@v8.3.2 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
version: ${{ env.UV_VERSION }}
|
version: ${{ env.UV_VERSION }}
|
||||||
@@ -153,27 +153,27 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install git-lfs
|
sudo apt-get install git-lfs
|
||||||
git lfs install
|
git lfs install
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download updated lockfile
|
- name: Download updated lockfile
|
||||||
uses: actions/download-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/download-artifact@v8 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: uv-lock
|
name: uv-lock
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/setup-buildx-action@v4 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
cache-binary: false
|
cache-binary: false
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3 # zizmor: ignore[unpinned-uses]
|
uses: docker/login-action@v4.4.0 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_LEROBOT_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_LEROBOT_PASSWORD }}
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6 # zizmor: ignore[unpinned-uses]
|
uses: docker/build-push-action@v7 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/Dockerfile.internal
|
file: ./docker/Dockerfile.internal
|
||||||
@@ -247,12 +247,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.UPDATE_LOCK_TOKEN }}
|
GH_TOKEN: ${{ secrets.UPDATE_LOCK_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download updated lockfile
|
- name: Download updated lockfile
|
||||||
uses: actions/download-artifact@v4 # zizmor: ignore[unpinned-uses]
|
uses: actions/download-artifact@v8 # zizmor: ignore[unpinned-uses]
|
||||||
with:
|
with:
|
||||||
name: uv-lock
|
name: uv-lock
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'huggingface/lerobot' && !github.event.pull_request.draft
|
if: github.repository == 'huggingface/lerobot' && !github.event.pull_request.draft
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v6
|
- uses: actions/labeler@v7
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
sync-labels: true # Removes labels if files are removed from the PR
|
sync-labels: true # Removes labels if files are removed from the PR
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
|
|
||||||
|
|||||||
@@ -38,12 +38,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
- name: Publish to TestPyPI for pre-releases
|
- name: Publish to TestPyPI for pre-releases
|
||||||
# True for tags like 'v0.2.0-rc1'
|
# True for tags like 'v0.2.0-rc1'
|
||||||
if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-')
|
if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-')
|
||||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
|
||||||
with:
|
with:
|
||||||
repository-url: https://test.pypi.org/legacy/
|
repository-url: https://test.pypi.org/legacy/
|
||||||
verbose: true
|
verbose: true
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-')
|
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-')
|
||||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
|
||||||
with:
|
with:
|
||||||
verbose: true
|
verbose: true
|
||||||
print-hash: true
|
print-hash: true
|
||||||
@@ -127,7 +127,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
MUJOCO_GL: egl
|
MUJOCO_GL: egl
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -137,7 +137,7 @@ jobs:
|
|||||||
git curl libglib2.0-0 libegl1-mesa-dev ffmpeg libusb-1.0-0-dev \
|
git curl libglib2.0-0 libegl1-mesa-dev ffmpeg libusb-1.0-0-dev \
|
||||||
speech-dispatcher libgeos-dev portaudio19-dev
|
speech-dispatcher libgeos-dev portaudio19-dev
|
||||||
- name: Setup uv and Python
|
- name: Setup uv and Python
|
||||||
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||||
with:
|
with:
|
||||||
enable-cache: true # zizmor: ignore[cache-poisoning]
|
enable-cache: true # zizmor: ignore[cache-poisoning]
|
||||||
version: ${{ env.UV_VERSION }}
|
version: ${{ env.UV_VERSION }}
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Secret Scanning
|
- name: Secret Scanning
|
||||||
uses: trufflesecurity/trufflehog@eafb8c5f6a06175141c27f17bcc17941853d0047 # v3.90.0
|
uses: trufflesecurity/trufflehog@27b0417c16317ca9a472a9a8092acce143b49c55 # v3.95.9
|
||||||
with:
|
with:
|
||||||
extra_args: --only-verified
|
extra_args: --only-verified
|
||||||
|
|||||||
+12
-8
@@ -61,15 +61,19 @@ Full details in [`docs/source/so101.mdx`](./docs/source/so101.mdx) and [`docs/so
|
|||||||
**4.1 Install**
|
**4.1 Install**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install 'lerobot[feetech]' # SO-100/SO-101 motor stack
|
# uv (recommended — see AGENTS.md and CLAUDE.md)
|
||||||
# pip install 'lerobot[all]' # everything
|
uv sync --locked --extra feetech # SO-100/SO-101 motor stack
|
||||||
# pip install 'lerobot[aloha,pusht]' # specific features
|
# uv sync --locked --extra all # everything
|
||||||
# pip install 'lerobot[smolvla]' # add SmolVLA deps
|
# uv sync --locked --extra smolvla # add SmolVLA deps
|
||||||
git lfs install && git lfs pull
|
|
||||||
hf auth login # required to push datasets/policies
|
|
||||||
```
|
|
||||||
|
|
||||||
Contributors can alternatively use `uv sync --locked --extra feetech` (see `AGENTS.md`).
|
# pip (alternative, e.g. when not working from source)
|
||||||
|
# pip install 'lerobot[feetech]'
|
||||||
|
# pip install 'lerobot[all]'
|
||||||
|
# pip install 'lerobot[smolvla]'
|
||||||
|
|
||||||
|
git lfs install && git lfs pull
|
||||||
|
hf auth login # required to push datasets/policies
|
||||||
|
```
|
||||||
|
|
||||||
**4.2 Find USB ports** — run once per arm, unplug when prompted.
|
**4.2 Find USB ports** — run once per arm, unplug when prompted.
|
||||||
|
|
||||||
|
|||||||
@@ -68,17 +68,16 @@ ENV HOME=/home/user_lerobot \
|
|||||||
# issues with MuJoCo and OpenGL drivers.
|
# issues with MuJoCo and OpenGL drivers.
|
||||||
RUN uv venv --python python${PYTHON_VERSION}
|
RUN uv venv --python python${PYTHON_VERSION}
|
||||||
|
|
||||||
# Install Python dependencies for caching
|
# Install third-party dependencies separately for layer caching
|
||||||
COPY --chown=user_lerobot:user_lerobot setup.py pyproject.toml uv.lock README.md MANIFEST.in ./
|
COPY --chown=user_lerobot:user_lerobot setup.py pyproject.toml uv.lock README.md MANIFEST.in ./
|
||||||
COPY --chown=user_lerobot:user_lerobot src/ src/
|
RUN uv sync --locked --extra all --no-install-project --no-cache
|
||||||
|
|
||||||
RUN uv sync --locked --extra all --no-cache
|
|
||||||
|
|
||||||
RUN chmod +x /lerobot/.venv/lib/python${PYTHON_VERSION}/site-packages/triton/backends/nvidia/bin/ptxas
|
RUN chmod +x /lerobot/.venv/lib/python${PYTHON_VERSION}/site-packages/triton/backends/nvidia/bin/ptxas
|
||||||
|
|
||||||
# Copy the rest of the application source code
|
# Copy the application source code and install the local project
|
||||||
# Make sure to have the git-LFS files for testing
|
# Make sure to have the git-LFS files for testing
|
||||||
COPY --chown=user_lerobot:user_lerobot . .
|
COPY --chown=user_lerobot:user_lerobot . .
|
||||||
|
RUN uv sync --locked --extra all --no-cache
|
||||||
|
|
||||||
# Set the default command
|
# Set the default command
|
||||||
CMD ["/bin/bash"]
|
CMD ["/bin/bash"]
|
||||||
|
|||||||
@@ -60,15 +60,14 @@ ENV HOME=/home/user_lerobot \
|
|||||||
# run other Python projects in the same container without dependency conflicts.
|
# run other Python projects in the same container without dependency conflicts.
|
||||||
RUN uv venv
|
RUN uv venv
|
||||||
|
|
||||||
# Install Python dependencies for caching
|
# Install third-party dependencies separately for layer caching
|
||||||
COPY --chown=user_lerobot:user_lerobot setup.py pyproject.toml uv.lock README.md MANIFEST.in ./
|
COPY --chown=user_lerobot:user_lerobot setup.py pyproject.toml uv.lock README.md MANIFEST.in ./
|
||||||
COPY --chown=user_lerobot:user_lerobot src/ src/
|
RUN uv sync --locked --extra all --no-install-project --no-cache
|
||||||
|
|
||||||
RUN uv sync --locked --extra all --no-cache
|
# Copy the application code and install the local project
|
||||||
|
|
||||||
# Copy the rest of the application code
|
|
||||||
# Make sure to have the git-LFS files for testing
|
# Make sure to have the git-LFS files for testing
|
||||||
COPY --chown=user_lerobot:user_lerobot . .
|
COPY --chown=user_lerobot:user_lerobot . .
|
||||||
|
RUN uv sync --locked --extra all --no-cache
|
||||||
|
|
||||||
# Set the default command
|
# Set the default command
|
||||||
CMD ["/bin/bash"]
|
CMD ["/bin/bash"]
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ In summary, you need to specify instructions for:
|
|||||||
Importantly,
|
Importantly,
|
||||||
|
|
||||||
- `actions_per_chunk` and `chunk_size_threshold` are key parameters to tune for your setup.
|
- `actions_per_chunk` and `chunk_size_threshold` are key parameters to tune for your setup.
|
||||||
- `aggregate_fn_name` is the function to aggregate actions on overlapping portions. You can either add a new one to a registry of functions, or add your own in `robot_client.py` (see [here](NOTE:addlinktoLOC))
|
- `aggregate_fn_name` is the function to aggregate actions on overlapping portions. You can either add a new one to a registry of functions, or add your own in `robot_client.py` (see [here](https://github.com/huggingface/lerobot/blob/main/src/lerobot/async_inference/robot_client.py#L224))
|
||||||
- `debug_visualize_queue_size` is a useful tool to tune the `CLIENT` parameters.
|
- `debug_visualize_queue_size` is a useful tool to tune the `CLIENT` parameters.
|
||||||
|
|
||||||
## Done! You should see your robot moving around by now 😉
|
## Done! You should see your robot moving around by now 😉
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ final_action = postprocessor(action)
|
|||||||
|
|
||||||
## Hardware API redesign
|
## Hardware API redesign
|
||||||
|
|
||||||
PR [#777](https://github.com/huggingface/lerobot/pull/777) improves the LeRobot calibration but is **not backward-compatible**. Below is a overview of what changed and how you can continue to work with datasets created before this pull request.
|
PR [#777](https://github.com/huggingface/lerobot/pull/777) improves the LeRobot calibration but is **not backward-compatible**. Below is an overview of what changed and how you can continue to work with datasets created before this pull request.
|
||||||
|
|
||||||
### What changed?
|
### What changed?
|
||||||
|
|
||||||
@@ -129,8 +129,8 @@ python examples/backward_compatibility/replay.py \
|
|||||||
|
|
||||||
Policies output actions in the same format as the datasets (`torch.Tensors`). Therefore, the same transformations should be applied.
|
Policies output actions in the same format as the datasets (`torch.Tensors`). Therefore, the same transformations should be applied.
|
||||||
|
|
||||||
To find these transformations, we recommend to first try and and replay an episode of the dataset your policy was trained on using the section above.
|
To find these transformations, we recommend first replaying an episode of the dataset your policy was trained on using the section above.
|
||||||
Then, add these same transformations on your inference script (shown here in the `record.py` script):
|
Then, add these same transformations to your inference script (shown here in the `record.py` script):
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
action_values = predict_action(
|
action_values = predict_action(
|
||||||
|
|||||||
@@ -136,6 +136,10 @@ config = RealSenseCameraConfig(
|
|||||||
height=480,
|
height=480,
|
||||||
color_mode=ColorMode.RGB,
|
color_mode=ColorMode.RGB,
|
||||||
use_depth=True,
|
use_depth=True,
|
||||||
|
# Optional fixed color controls. Omit them to leave the current sensor settings unchanged.
|
||||||
|
exposure=120,
|
||||||
|
gain=64,
|
||||||
|
white_balance=4600,
|
||||||
rotation=Cv2Rotation.NO_ROTATION
|
rotation=Cv2Rotation.NO_ROTATION
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -154,6 +158,15 @@ finally:
|
|||||||
```
|
```
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
Manual color controls disable the corresponding automatic exposure or white-balance mode. Their
|
||||||
|
supported ranges vary by camera model; an invalid value raises an error at connection time that
|
||||||
|
includes the range reported by the sensor. Requesting an unsupported control also raises an error.
|
||||||
|
Omitted controls leave the sensor's existing automatic or manual setting unchanged. These options
|
||||||
|
require `use_rgb=True`.
|
||||||
|
|
||||||
|
Manual color controls require a dedicated RGB module. Cameras without one, such as the RealSense
|
||||||
|
D405, do not support them and raise an error at connection time.
|
||||||
|
|
||||||
</hfoption>
|
</hfoption>
|
||||||
</hfoptions>
|
</hfoptions>
|
||||||
|
|
||||||
|
|||||||
@@ -88,20 +88,6 @@ policy_preprocessor = NormalizerProcessorStep(stats=dataset_stats)
|
|||||||
|
|
||||||
The same policy can work with different environment processors, and the same environment processor can work with different policies:
|
The same policy can work with different environment processors, and the same environment processor can work with different policies:
|
||||||
|
|
||||||
````python
|
|
||||||
# Use SmolVLA policy with LIBERO environment
|
|
||||||
# Use SmolVLA policy with LIBERO environment
|
|
||||||
libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(
|
|
||||||
env_cfg=libero_cfg,
|
|
||||||
policy_cfg=smolvla_cfg,
|
|
||||||
)
|
|
||||||
smolvla_preprocessor, smolvla_postprocessor = make_pre_post_processors(smolvla_cfg)
|
|
||||||
# Or use ACT policy with the same LIBERO environment
|
|
||||||
libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(
|
|
||||||
env_cfg=libero_cfg,
|
|
||||||
policy_cfg=act_cfg,
|
|
||||||
)
|
|
||||||
act_preprocessor, act_postprocessor = make_pre_post_processors(act_cfg)
|
|
||||||
```python
|
```python
|
||||||
# Use SmolVLA policy with LIBERO environment
|
# Use SmolVLA policy with LIBERO environment
|
||||||
libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(
|
libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(
|
||||||
@@ -116,6 +102,7 @@ libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(
|
|||||||
policy_cfg=act_cfg,
|
policy_cfg=act_cfg,
|
||||||
)
|
)
|
||||||
act_preprocessor, act_postprocessor = make_pre_post_processors(act_cfg)
|
act_preprocessor, act_postprocessor = make_pre_post_processors(act_cfg)
|
||||||
|
```
|
||||||
|
|
||||||
### 3. **Easier Experimentation**
|
### 3. **Easier Experimentation**
|
||||||
|
|
||||||
@@ -145,7 +132,7 @@ class LiberoVelocityProcessorStep(ObservationProcessorStep):
|
|||||||
state = torch.cat([eef_pos, eef_axisangle, eef_vel,
|
state = torch.cat([eef_pos, eef_axisangle, eef_vel,
|
||||||
gripper_pos, gripper_vel], dim=-1) # 14D
|
gripper_pos, gripper_vel], dim=-1) # 14D
|
||||||
return state
|
return state
|
||||||
````
|
```
|
||||||
|
|
||||||
### 4. **Cleaner Environment Code**
|
### 4. **Cleaner Environment Code**
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ This tutorial guides you through updating the firmware of Feetech motors using t
|
|||||||
For each motor you want to update:
|
For each motor you want to update:
|
||||||
|
|
||||||
1. **Select the motor** from the list by clicking on it
|
1. **Select the motor** from the list by clicking on it
|
||||||
2. **Click on Upgrade tab**:
|
2. **Click the Upgrade tab**:
|
||||||
3. **Click on Online button**:
|
3. **Click the Online button**:
|
||||||
- If an potential firmware update is found, it will be displayed in the box
|
- If a potential firmware update is found, it will be displayed in the box
|
||||||
4. **Click on Upgrade button**:
|
4. **Click the Upgrade button**:
|
||||||
- The update progress will be displayed
|
- The update progress will be displayed
|
||||||
|
|
||||||
## Step 6: Verify Update
|
## Step 6: Verify Update
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ The `lerobot-rollout --strategy.type=dagger` mode requires **teleoperators with
|
|||||||
|
|
||||||
- `bi_openarm_mini` - Bimanual OpenArm Mini
|
- `bi_openarm_mini` - Bimanual OpenArm Mini
|
||||||
- `so_leader` - SO100 / SO101 leader arm
|
- `so_leader` - SO100 / SO101 leader arm
|
||||||
|
- `bi_so_leader` - Bimanual SO100 / SO101 leader arms
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> The provided commands default to `bi_openarm_follower` + `bi_openarm_mini`.
|
> The provided commands default to `bi_openarm_follower` + `bi_openarm_mini`.
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ Record, Replay and Train with Hope-JR is still experimental.
|
|||||||
|
|
||||||
### Record
|
### Record
|
||||||
|
|
||||||
This step records the dataset, which can be seen as an example [here](https://huggingface.co/datasets/nepyope/hand_record_test_with_video_data/settings).
|
This step records the dataset, which can be seen as an example [here](https://huggingface.co/datasets/nepyope/hand_record_test_with_video_data).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lerobot-record \
|
lerobot-record \
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ The teleoperate command will automatically:
|
|||||||
|
|
||||||
## Cameras
|
## Cameras
|
||||||
|
|
||||||
To add cameras to your setup, follow this [Guide](./cameras#setup-cameras).
|
To add cameras to your setup, follow this [Guide](./cameras).
|
||||||
|
|
||||||
## Teleoperate with cameras
|
## Teleoperate with cameras
|
||||||
|
|
||||||
|
|||||||
+163
-9
@@ -1,23 +1,177 @@
|
|||||||
|
# LeRobot
|
||||||
|
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<a target="_blank" href="https://huggingface.co/lerobot">
|
<a target="_blank" href="https://huggingface.co/lerobot">
|
||||||
<img
|
<img
|
||||||
alt="HuggingFace Expert Acceleration Program"
|
alt="LeRobot, Hugging Face Robotics Library"
|
||||||
src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/lerobot/lerobot-logo-thumbnail.png"
|
src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/lerobot/lerobot-logo-thumbnail.png"
|
||||||
style="width: 100%"
|
|
||||||
></img>
|
></img>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# LeRobot
|
|
||||||
|
|
||||||
**State-of-the-art machine learning for real-world robotics**
|
**State-of-the-art machine learning for real-world robotics**
|
||||||
|
|
||||||
🤗 LeRobot aims to provide models, datasets, and tools for real-world robotics in PyTorch. The goal is to lower the barrier for entry to robotics so that everyone can contribute and benefit from sharing datasets and pretrained models.
|
🤗 LeRobot provides a hardware-agnostic, Python-native interface for controlling real robots - from affordable arms like the SO-ARM101 to full humanoids. Plus the tools to record, store, and share the datasets they generate. Every dataset uses the standardized **LeRobotDataset** format (synchronized video + action/state data) and can be streamed directly from the [Hugging Face Hub](https://huggingface.co/lerobot).
|
||||||
|
|
||||||
🤗 LeRobot contains state-of-the-art approaches that have been shown to transfer to the real-world with a focus on imitation learning and reinforcement learning.
|
🤗 On top of that data, LeRobot implements state-of-the-art policies - from lightweight imitation-learning models like ACT to large vision-language-action models like π₀ and SmolVLA - all trainable, shareable, and deployable with the same handful of CLI commands.
|
||||||
|
|
||||||
🤗 LeRobot already provides a set of pretrained models, datasets with human collected demonstrations, and simulated environments so that everyone can get started.
|
The goal: lower the barrier to entry for robotics, so that everyone can contribute to, and benefit from, shared datasets and pretrained models.
|
||||||
|
|
||||||
🤗 LeRobot hosts pretrained models and datasets on the LeRobot HuggingFace page.
|
<div align="center" style="display: flex; justify-content: center; gap: 8px; flex-wrap: wrap; margin: 20px 0;">
|
||||||
|
<a href="https://discord.gg/s3KuuzsPFb" target="_blank">
|
||||||
|
<img alt="Discord" src="https://img.shields.io/badge/Discord-Join_the_Community-5865F2?style=flat&logo=discord&logoColor=white">
|
||||||
|
</a>
|
||||||
|
<a href="https://x.com/LeRobotHF" target="_blank">
|
||||||
|
<img alt="X (Twitter)" src="https://img.shields.io/badge/X-Follow_%40LeRobotHF-black?style=flat&logo=x&logoColor=white">
|
||||||
|
</a>
|
||||||
|
<a href="https://huggingface.co/lerobot" target="_blank">
|
||||||
|
<img alt="Hugging Face Hub" src="https://img.shields.io/badge/HF_Hub-Models_%26_Datasets-FFD21E?style=flat">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
Join the LeRobot community on [Discord](https://discord.gg/s3KuuzsPFb)
|
<div align="center">
|
||||||
|
<img src="../../media/readme/robots_control_video.webp" width="640px" alt="Reachy 2 Demo">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
**Teleoperate → Record → Train → Deploy**
|
||||||
|
|
||||||
|
1. **Teleoperate** - control the robot yourself (with a leader arm, keyboard, or phone) so it can learn from your movements.
|
||||||
|
2. **Record** - each demonstration is saved as a dataset: synchronized camera video plus the actions you took.
|
||||||
|
3. **Train** - a policy (the neural network that will control the robot) learns to imitate your demonstrations.
|
||||||
|
4. **Deploy** - run the trained policy on the robot and watch it complete the task on its own.
|
||||||
|
|
||||||
|
## Get Started
|
||||||
|
|
||||||
|
New here? [Install LeRobot](./installation), then pick your path:
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 my-6">
|
||||||
|
<div class="border dark:border-gray-700 rounded-lg p-4 shadow">
|
||||||
|
<div class="text-lg font-semibold mb-2">🔧 I have a robot</div>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm">
|
||||||
|
LeRobot supports a wide range of arms and mobile robots. Popular picks:
|
||||||
|
</p>
|
||||||
|
<ul class="text-gray-700 dark:text-gray-300 text-sm list-disc pl-5 mb-2">
|
||||||
|
<li>
|
||||||
|
<a href="./so101">SO-101</a> - our flagship, low-cost arm
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./lekiwi">LeKiwi</a> - a mobile base with an arm on top
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./koch">Koch v1.1</a> - a long-time community favorite
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
or find yours under <strong>Robots</strong> in the sidebar
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm">
|
||||||
|
Once it's assembled and calibrated, record a dataset and train your first
|
||||||
|
policy with the <a href="./il_robots">imitation learning tutorial</a> - or
|
||||||
|
skip the CLI entirely with <a href="./lelab">LeLab</a>, a browser GUI for
|
||||||
|
the same workflow.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="border dark:border-gray-700 rounded-lg p-4 shadow">
|
||||||
|
<div class="text-lg font-semibold mb-2">💻 No hardware yet</div>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm">
|
||||||
|
You can still train and evaluate policies without owning a robot:
|
||||||
|
</p>
|
||||||
|
<ul class="text-gray-700 dark:text-gray-300 text-sm list-disc pl-5 mb-2">
|
||||||
|
<li>
|
||||||
|
train on an existing
|
||||||
|
<a href="https://huggingface.co/datasets?other=LeRobot">
|
||||||
|
LeRobot dataset
|
||||||
|
</a>
|
||||||
|
from the Hub
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
evaluate in <a href="./envhub">simulation</a>, against benchmarks like
|
||||||
|
LIBERO or Meta-World
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
try the free <a href="./notebooks">Colab notebooks</a> - nothing to
|
||||||
|
install
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="border dark:border-gray-700 rounded-lg p-4 shadow">
|
||||||
|
<div class="text-lg font-semibold mb-2">🤝 I want to contribute</div>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm">
|
||||||
|
Start with the <a href="./contributing">Contributing guide</a>, then
|
||||||
|
<a href="./bring_your_own_policies">add a new policy</a> or
|
||||||
|
<a href="./integrate_hardware">bring your own hardware</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Explore the Docs
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 my-6">
|
||||||
|
<a
|
||||||
|
class="!no-underline border dark:border-gray-700 rounded-lg p-4 shadow hover:shadow-lg"
|
||||||
|
href="./cheat-sheet"
|
||||||
|
>
|
||||||
|
<div class="font-semibold mb-1">📋 Cheat Sheet</div>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm">
|
||||||
|
Every LeRobot CLI command, copy-paste ready.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="!no-underline border dark:border-gray-700 rounded-lg p-4 shadow hover:shadow-lg"
|
||||||
|
href="./hardware_guide"
|
||||||
|
>
|
||||||
|
<div class="font-semibold mb-1">🖥️ Compute & Hardware Guide</div>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm">
|
||||||
|
Which policy fits your GPU, and how long training takes.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="!no-underline border dark:border-gray-700 rounded-lg p-4 shadow hover:shadow-lg"
|
||||||
|
href="./lerobot-dataset-v3"
|
||||||
|
>
|
||||||
|
<div class="font-semibold mb-1">🗂️ LeRobotDataset</div>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm">
|
||||||
|
Load, stream, and visualize robot datasets from the Hub.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="!no-underline border dark:border-gray-700 rounded-lg p-4 shadow hover:shadow-lg"
|
||||||
|
href="./lelab"
|
||||||
|
>
|
||||||
|
<div class="font-semibold mb-1">🖼 LeLab</div>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm">
|
||||||
|
A browser GUI for calibrating, recording, and training - no CLI required.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="!no-underline border dark:border-gray-700 rounded-lg p-4 shadow hover:shadow-lg"
|
||||||
|
href="./act"
|
||||||
|
>
|
||||||
|
<div class="font-semibold mb-1">🧠 Policies</div>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm">
|
||||||
|
Start with ACT, our recommended first policy - or browse SmolVLA, π₀, and
|
||||||
|
more in the sidebar.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="!no-underline border dark:border-gray-700 rounded-lg p-4 shadow hover:shadow-lg"
|
||||||
|
href="./envhub"
|
||||||
|
>
|
||||||
|
<div class="font-semibold mb-1">🎮 Simulation & Benchmarks</div>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-sm">
|
||||||
|
Train and evaluate in simulated environments before touching real
|
||||||
|
hardware.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Common Problems
|
||||||
|
|
||||||
|
Running into issues? A few of the most frequent ones:
|
||||||
|
|
||||||
|
- **Blurry or unusable camera footage** - lighting matters more than resolution. See the [Cameras](./cameras) guide.
|
||||||
|
- **Build or install errors** (`cmake`, `ffmpeg`, CUDA) - see the Troubleshooting section of the [Installation guide](./installation#troubleshooting).
|
||||||
|
- **Not sure which policy fits your GPU** - check the [Compute & Hardware Guide](./hardware_guide).
|
||||||
|
- **Still stuck?** Ask on [Discord](https://discord.gg/s3KuuzsPFb) - the community (and the LeRobot team) is there to help.
|
||||||
|
|||||||
+17
-15
@@ -149,13 +149,14 @@ lerobot-rollout \
|
|||||||
|
|
||||||
Foot pedal input is also supported via `--strategy.input_device=pedal`. Configure pedal codes with `--strategy.pedal.*` flags.
|
Foot pedal input is also supported via `--strategy.input_device=pedal`. Configure pedal codes with `--strategy.pedal.*` flags.
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
| ------------------------------------ | ------------------------------------------------------- |
|
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `--strategy.num_episodes` | Number of correction episodes to record (default: 10) |
|
| `--strategy.num_episodes` | Number of correction episodes to record (default: 10) |
|
||||||
| `--strategy.record_autonomous` | Record autonomous frames too (default: false) |
|
| `--strategy.record_autonomous` | Record autonomous frames too (default: false) |
|
||||||
| `--strategy.upload_every_n_episodes` | Push to Hub every N episodes (default: 5) |
|
| `--strategy.upload_every_n_episodes` | Push to Hub every N episodes (default: 5) |
|
||||||
| `--strategy.input_device` | Input device: `keyboard` or `pedal` (default: keyboard) |
|
| `--strategy.input_device` | Input device: `keyboard` or `pedal` (default: keyboard) |
|
||||||
| `--teleop.type` | **Required.** Teleoperator type |
|
| `--strategy.smooth_handover` | Smoothly hand control over at pause / correction start (default: true). Disable for clutch-style teleops that re-reference at the current robot pose on engage |
|
||||||
|
| `--teleop.type` | **Required.** Teleoperator type |
|
||||||
|
|
||||||
### Episodic (`--strategy.type=episodic`)
|
### Episodic (`--strategy.type=episodic`)
|
||||||
|
|
||||||
@@ -186,14 +187,15 @@ Teleop is optional — if omitted the robot holds its position during the reset
|
|||||||
| `←` (left) | Discard episode and re-record it |
|
| `←` (left) | Discard episode and re-record it |
|
||||||
| `ESC` | Stop the recording session |
|
| `ESC` | Stop the recording session |
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
| ----------------------------------------------- | -------------------------------------------------------------------------- |
|
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `--dataset.num_episodes` | Number of episodes to record |
|
| `--dataset.num_episodes` | Number of episodes to record |
|
||||||
| `--dataset.episode_time_s` | Duration of each recording episode in seconds |
|
| `--dataset.episode_time_s` | Duration of each recording episode in seconds |
|
||||||
| `--dataset.reset_time_s` | Duration of the reset phase between episodes in seconds |
|
| `--dataset.reset_time_s` | Duration of the reset phase between episodes in seconds |
|
||||||
| `--teleop.type` | Optional. Teleoperator to drive the robot during resets |
|
| `--teleop.type` | Optional. Teleoperator to drive the robot during resets |
|
||||||
| `--strategy.reset_to_initial_position` | Whether to reset the robot to its initial position between episodes |
|
| `--strategy.reset_to_initial_position` | Whether to reset the robot to its initial position between episodes |
|
||||||
| `--strategy.smooth_leader_to_follower_handover` | Whether to turn on or off the leader -> follower smooth handover behavior. |
|
| `--strategy.smooth_leader_to_follower_handover` | Whether to turn on or off the leader -> follower smooth handover behavior. |
|
||||||
|
| `--strategy.smooth_handover` | Smoothly hand control to the teleop at reset start (default: true). Disable for clutch-style teleops that re-reference at the current robot pose on engage |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ If you're using Feetech or Dynamixel motors, LeRobot provides built-in bus inter
|
|||||||
- [`DynamixelMotorsBus`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/dynamixel/dynamixel.py) – for controlling Dynamixel servos
|
- [`DynamixelMotorsBus`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/dynamixel/dynamixel.py) – for controlling Dynamixel servos
|
||||||
|
|
||||||
Please refer to the [`MotorsBus`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/motors_bus.py) abstract class to learn about its API.
|
Please refer to the [`MotorsBus`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/motors_bus.py) abstract class to learn about its API.
|
||||||
For a good example of how it can be used, you can have a look at our own [SO101 follower implementation](https://github.com/huggingface/lerobot/blob/main/src/lerobot/robots/so_follower/so101_follower/so101_follower.py)
|
For a good example of how it can be used, you can have a look at our own [SO101 follower implementation](https://github.com/huggingface/lerobot/blob/main/src/lerobot/robots/so_follower/so_follower.py)
|
||||||
|
|
||||||
Use these if compatible. Otherwise, you'll need to find or write a Python interface (not covered in this tutorial):
|
Use these if compatible. Otherwise, you'll need to find or write a Python interface (not covered in this tutorial):
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ In addition to these instructions, you need to install the Feetech SDK & ZeroMQ
|
|||||||
pip install -e ".[lekiwi]"
|
pip install -e ".[lekiwi]"
|
||||||
```
|
```
|
||||||
|
|
||||||
Great :hugs:! You are now done installing LeRobot, and we can begin assembling the SO100/SO101 arms and the mobile base :robot:.
|
Great 🤗! You are now done installing LeRobot, and we can begin assembling the SO100/SO101 arms and the mobile base 🤖.
|
||||||
Every time you now want to use LeRobot, you can go to the `~/lerobot` folder where we installed LeRobot and run one of the commands.
|
Every time you now want to use LeRobot, you can go to the `~/lerobot` folder where we installed LeRobot and run one of the commands.
|
||||||
|
|
||||||
# Step-by-Step Assembly Instructions
|
# Step-by-Step Assembly Instructions
|
||||||
|
|||||||
+32
-12
@@ -114,38 +114,58 @@ LIBERO supports two control modes — `relative` (default) and `absolute`. Diffe
|
|||||||
|
|
||||||
### Recommended evaluation episodes
|
### Recommended evaluation episodes
|
||||||
|
|
||||||
For reproducible benchmarking, use **10 episodes per task** across all four standard suites (Spatial, Object, Goal, Long). This gives 400 total episodes and matches the protocol used for published results.
|
For reproducible benchmarking, use **10 episodes per task** across all four standard suites (Spatial, Object, Goal, Long). This gives 400 total episodes and matches the protocol used for published results. Success rates may vary by a few percent across evaluation seeds, so we recommend averaging over 3 seeds.
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
To compare two policies on the same episodes, use the same `--seed`, keep
|
||||||
|
`--env.init_states=true`, and run each task in a single batch
|
||||||
|
(`--eval.batch_size` equal to episodes per task).
|
||||||
|
</Tip>
|
||||||
|
|
||||||
## Training
|
## Training
|
||||||
|
|
||||||
### Dataset
|
### Dataset
|
||||||
|
|
||||||
We provide a preprocessed LIBERO dataset fully compatible with LeRobot:
|
Two preprocessed LIBERO datasets are fully compatible with LeRobot. They contain the same demonstrations with the same schema and differ in how camera frames are stored:
|
||||||
|
|
||||||
- [HuggingFaceVLA/libero](https://huggingface.co/datasets/HuggingFaceVLA/libero)
|
| | [lerobot/libero](https://huggingface.co/datasets/lerobot/libero) | [HuggingFaceVLA/libero](https://huggingface.co/datasets/HuggingFaceVLA/libero) |
|
||||||
|
| ------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||||
|
| episodes / frames / tasks | 1,693 / 273,465 / 40 | 1,693 / 273,465 / 40 |
|
||||||
|
| cameras | 2× 256×256×3 | 2× 256×256×3 |
|
||||||
|
| state / action dims | 8 / 7 | 8 / 7 |
|
||||||
|
| dataset format | v3.0 | v3.0 |
|
||||||
|
| camera encoding | MP4 video | PNG in parquet |
|
||||||
|
| download size | **1.9 GB** | 69.9 GB |
|
||||||
|
| extra dependency | video backend (`torchcodec` or `pyav`) | none |
|
||||||
|
|
||||||
|
**We recommend [lerobot/libero](https://huggingface.co/datasets/lerobot/libero)**: **37× smaller download** with **equivalent loading speed** (~330 samples/s per worker). Video re-encoding is slightly lossy; use the image-based variant if you cannot install a video decoding backend.
|
||||||
|
|
||||||
For reference, the original dataset published by Physical Intelligence:
|
For reference, the original dataset published by Physical Intelligence:
|
||||||
|
|
||||||
- [physical-intelligence/libero](https://huggingface.co/datasets/physical-intelligence/libero)
|
- [physical-intelligence/libero](https://huggingface.co/datasets/physical-intelligence/libero)
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Pin `--dataset.revision=<commit-sha>` when reporting results — Hub datasets can be re-uploaded, and success rates are only comparable against the same data revision.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
### Example training command
|
### Example training command
|
||||||
|
|
||||||
|
Train SmolVLA on the recommended dataset:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lerobot-train \
|
lerobot-train \
|
||||||
--policy.type=smolvla \
|
--policy.type=smolvla \
|
||||||
--policy.repo_id=${HF_USER}/libero-test \
|
|
||||||
--policy.load_vlm_weights=true \
|
--policy.load_vlm_weights=true \
|
||||||
--dataset.repo_id=HuggingFaceVLA/libero \
|
--policy.push_to_hub=false \
|
||||||
--env.type=libero \
|
--dataset.repo_id=lerobot/libero \
|
||||||
--env.task=libero_10 \
|
--dataset.video_backend=torchcodec \
|
||||||
--output_dir=./outputs/ \
|
--output_dir=./outputs/libero_smolvla \
|
||||||
--steps=100000 \
|
--steps=100000 \
|
||||||
--batch_size=4 \
|
--batch_size=64
|
||||||
--eval.batch_size=1 \
|
|
||||||
--eval.n_episodes=1 \
|
|
||||||
--env_eval_freq=1000
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To share the result on the Hub, replace `--policy.push_to_hub=false` with `--policy.repo_id=${HF_USER}/libero-smolvla`. Evaluate saved checkpoints with `lerobot-eval` as shown in the [Evaluation](#evaluation) section.
|
||||||
|
|
||||||
## Reproducing published results
|
## Reproducing published results
|
||||||
|
|
||||||
We reproduce the results of Pi0.5 on the LIBERO benchmark. We take the Physical Intelligence LIBERO base model (`pi05_libero`) and finetune for an additional 6k steps in bfloat16, with batch size of 256 on 8 H100 GPUs using the [HuggingFace LIBERO dataset](https://huggingface.co/datasets/HuggingFaceVLA/libero).
|
We reproduce the results of Pi0.5 on the LIBERO benchmark. We take the Physical Intelligence LIBERO base model (`pi05_libero`) and finetune for an additional 6k steps in bfloat16, with batch size of 256 on 8 H100 GPUs using the [HuggingFace LIBERO dataset](https://huggingface.co/datasets/HuggingFaceVLA/libero).
|
||||||
|
|||||||
+114
-24
@@ -36,6 +36,12 @@ This diverse training mixture creates a "curriculum" that enables generalization
|
|||||||
pip install -e ".[pi]"
|
pip install -e ".[pi]"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you installed LeRobot from PyPI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install 'lerobot[pi]'
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
To use π₀.₅ in your LeRobot configuration, specify the policy type as:
|
To use π₀.₅ in your LeRobot configuration, specify the policy type as:
|
||||||
@@ -46,27 +52,106 @@ policy.type=pi05
|
|||||||
|
|
||||||
## Training
|
## Training
|
||||||
|
|
||||||
### Training Command Example
|
### Quickstart on LIBERO
|
||||||
|
|
||||||
Here's a complete training command for finetuning the base π₀.₅ model on your own dataset:
|
Finetune the LIBERO base model on [lerobot/libero](https://huggingface.co/datasets/lerobot/libero), a ~1.9 GB video-encoded copy of the demonstrations behind the [results below](#libero-benchmark-results).
|
||||||
|
|
||||||
|
It carries the keys π₀.₅ reads, which are also the ones the LIBERO environment produces at evaluation time:
|
||||||
|
|
||||||
|
| Feature | Shape in the dataset | How π₀.₅ consumes it |
|
||||||
|
| --------------------------- | -------------------- | ------------------------------------------------------- |
|
||||||
|
| `observation.images.image` | 256×256×3, agentview | resized to 224×224 |
|
||||||
|
| `observation.images.image2` | 256×256×3, wrist | resized to 224×224 |
|
||||||
|
| `observation.state` | 8 | discretized into 256 bins and written into the prompt |
|
||||||
|
| `action` | 7 | padded to 32 internally; the loss uses the first 7 dims |
|
||||||
|
|
||||||
|
**No `--rename_map` is needed here** — the keys already match; see [Rename Map and Empty Cameras](./rename_map) if yours differ.
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
π₀.₅ uses the gated
|
||||||
|
[google/paligemma-3b-pt-224](https://huggingface.co/google/paligemma-3b-pt-224)
|
||||||
|
tokenizer — accept its license on the Hub and log in with `hf auth login`
|
||||||
|
before training.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
Sized for a single 80 GB GPU:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lerobot-train \
|
lerobot-train \
|
||||||
--dataset.repo_id=your_dataset \
|
--dataset.repo_id=lerobot/libero \
|
||||||
--policy.type=pi05 \
|
--policy.type=pi05 \
|
||||||
--output_dir=./outputs/pi05_training \
|
--policy.pretrained_path=lerobot/pi05_libero_base \
|
||||||
--job_name=pi05_training \
|
--policy.normalization_mapping='{"ACTION": "MEAN_STD", "STATE": "MEAN_STD", "VISUAL": "IDENTITY"}' \
|
||||||
--policy.repo_id=your_repo_id \
|
--policy.n_action_steps=10 \
|
||||||
--policy.pretrained_path=lerobot/pi05_base \
|
--policy.empty_cameras=1 \
|
||||||
--policy.compile_model=true \
|
|
||||||
--policy.gradient_checkpointing=true \
|
|
||||||
--wandb.enable=true \
|
|
||||||
--policy.dtype=bfloat16 \
|
|
||||||
--policy.freeze_vision_encoder=false \
|
--policy.freeze_vision_encoder=false \
|
||||||
--policy.train_expert_only=false \
|
--policy.train_expert_only=false \
|
||||||
--steps=3000 \
|
--policy.gradient_checkpointing=true \
|
||||||
|
--policy.dtype=bfloat16 \
|
||||||
--policy.device=cuda \
|
--policy.device=cuda \
|
||||||
--batch_size=32
|
--policy.push_to_hub=false \
|
||||||
|
--output_dir=./outputs/pi05_libero \
|
||||||
|
--job_name=pi05_libero \
|
||||||
|
--batch_size=64 \
|
||||||
|
--num_workers=8 \
|
||||||
|
--steps=30000 \
|
||||||
|
--save_freq=5000 \
|
||||||
|
--seed=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mean/std normalization, not π₀.₅'s [quantile default](#quantile-statistics)** — matching [pi05_libero_finetuned_v044](https://huggingface.co/lerobot/pi05_libero_finetuned_v044), the checkpoint the results below were measured on.
|
||||||
|
|
||||||
|
**`--policy.n_action_steps=10` and `--policy.empty_cameras=1` are explicit** because `--policy.pretrained_path` loads weights only — `lerobot/pi05_libero_base` stores both, and they would otherwise fall back to `50` and `0` (see [Loading a checkpoint](#loading-a-checkpoint)).
|
||||||
|
|
||||||
|
Then evaluate a checkpoint with `lerobot-eval` and compare against the reference success rates — see [LIBERO](./libero).
|
||||||
|
|
||||||
|
### Quantile statistics
|
||||||
|
|
||||||
|
π₀.₅ normalizes `STATE` and `ACTION` with quantiles, so your dataset's `meta/stats.json` needs `q01` and `q99`. Older datasets carry only `min`/`max`/`mean`/`std` and fail on the first batch:
|
||||||
|
|
||||||
|
```
|
||||||
|
ValueError: QUANTILES normalization mode requires q01 and q99 stats
|
||||||
|
```
|
||||||
|
|
||||||
|
Recompute them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lerobot-edit-dataset \
|
||||||
|
--repo_id your_dataset \
|
||||||
|
--new_repo_id your_dataset \
|
||||||
|
--operation.type recompute_stats \
|
||||||
|
--operation.overwrite true
|
||||||
|
```
|
||||||
|
|
||||||
|
**The result lands in `$HF_LEROBOT_HOME/your_dataset`**, not the cache `--dataset.repo_id` reads — so train with `--dataset.root=$HF_LEROBOT_HOME/your_dataset`, or add `--push_to_hub true` above.
|
||||||
|
|
||||||
|
Or keep the dataset as-is and pass `--policy.normalization_mapping='{"ACTION": "MEAN_STD", "STATE": "MEAN_STD", "VISUAL": "IDENTITY"}'`.
|
||||||
|
|
||||||
|
### Training Command Example
|
||||||
|
|
||||||
|
The same finetune with the VLM frozen: less memory, at some cost in success rate. Swap `--dataset.repo_id` for your own dataset.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lerobot-train \
|
||||||
|
--dataset.repo_id=lerobot/libero \
|
||||||
|
--policy.type=pi05 \
|
||||||
|
--policy.pretrained_path=lerobot/pi05_libero_base \
|
||||||
|
--policy.normalization_mapping='{"ACTION": "MEAN_STD", "STATE": "MEAN_STD", "VISUAL": "IDENTITY"}' \
|
||||||
|
--policy.n_action_steps=10 \
|
||||||
|
--policy.empty_cameras=1 \
|
||||||
|
--policy.freeze_vision_encoder=true \
|
||||||
|
--policy.train_expert_only=true \
|
||||||
|
--policy.gradient_checkpointing=true \
|
||||||
|
--policy.dtype=bfloat16 \
|
||||||
|
--policy.device=cuda \
|
||||||
|
--policy.push_to_hub=false \
|
||||||
|
--output_dir=./outputs/pi05_libero_expert \
|
||||||
|
--job_name=pi05_libero_expert \
|
||||||
|
--batch_size=64 \
|
||||||
|
--num_workers=8 \
|
||||||
|
--steps=30000 \
|
||||||
|
--save_freq=5000 \
|
||||||
|
--seed=1000
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Training Parameters
|
### Key Training Parameters
|
||||||
@@ -74,10 +159,24 @@ lerobot-train \
|
|||||||
- **`--policy.compile_model=true`**: Enables model compilation for faster training
|
- **`--policy.compile_model=true`**: Enables model compilation for faster training
|
||||||
- **`--policy.gradient_checkpointing=true`**: Reduces memory usage significantly during training
|
- **`--policy.gradient_checkpointing=true`**: Reduces memory usage significantly during training
|
||||||
- **`--policy.dtype=bfloat16`**: Use mixed precision training for efficiency
|
- **`--policy.dtype=bfloat16`**: Use mixed precision training for efficiency
|
||||||
- **`--batch_size=32`**: Batch size for training, adapt this based on your GPU memory
|
- **`--batch_size=64`**: Batch size for training, adapt this based on your GPU memory
|
||||||
- **`--policy.pretrained_path=lerobot/pi05_base`**: The base π₀.₅ model you want to finetune, options are:
|
- **`--policy.pretrained_path=lerobot/pi05_base`**: The base π₀.₅ model you want to finetune, options are:
|
||||||
- [lerobot/pi05_base](https://huggingface.co/lerobot/pi05_base)
|
- [lerobot/pi05_base](https://huggingface.co/lerobot/pi05_base)
|
||||||
- [lerobot/pi05_libero](https://huggingface.co/lerobot/pi05_libero) (specifically trained on the Libero dataset)
|
- [lerobot/pi05_libero_base](https://huggingface.co/lerobot/pi05_libero_base) (specifically trained on the Libero dataset)
|
||||||
|
|
||||||
|
### Loading a checkpoint
|
||||||
|
|
||||||
|
The two forms are not interchangeable:
|
||||||
|
|
||||||
|
| | `--policy.path` | `--policy.pretrained_path` |
|
||||||
|
| -------------------------------------- | ---------------------------------------------- | ------------------------------------ |
|
||||||
|
| Loads | weights **and** the checkpoint's `config.json` | weights only |
|
||||||
|
| Feature names | from the checkpoint | from your dataset |
|
||||||
|
| Stored settings, e.g. `n_action_steps` | inherited | reset to the defaults |
|
||||||
|
| `--policy.type` | must be omitted | required |
|
||||||
|
| `--rename_map` | needed when your camera keys differ | never — the keys come from your data |
|
||||||
|
|
||||||
|
Passing a `--rename_map` alongside `--policy.pretrained_path` renames the batch away from those names, and the first batch fails with `All image features are missing from the batch`.
|
||||||
|
|
||||||
### Training Parameters Explained
|
### Training Parameters Explained
|
||||||
|
|
||||||
@@ -88,15 +187,6 @@ lerobot-train \
|
|||||||
|
|
||||||
**💡 Tip**: Setting `train_expert_only=true` freezes the VLM and trains only the action expert and projections, allowing finetuning with reduced memory usage.
|
**💡 Tip**: Setting `train_expert_only=true` freezes the VLM and trains only the action expert and projections, allowing finetuning with reduced memory usage.
|
||||||
|
|
||||||
If your dataset is not converted with `quantiles`, you can convert it with the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python src/lerobot/scripts/augment_dataset_quantile_stats.py \
|
|
||||||
--repo-id=your_dataset \
|
|
||||||
```
|
|
||||||
|
|
||||||
Or train pi05 with this normalization mapping: `--policy.normalization_mapping='{"ACTION": "MEAN_STD", "STATE": "MEAN_STD", "VISUAL": "IDENTITY"}'`
|
|
||||||
|
|
||||||
## Relative Actions
|
## Relative Actions
|
||||||
|
|
||||||
By default, π₀.₅ predicts absolute actions. You can enable **relative actions** so the model predicts offsets relative to the current robot state. This can improve training stability for certain setups.
|
By default, π₀.₅ predicts absolute actions. You can enable **relative actions** so the model predicts offsets relative to the current robot state. This can improve training stability for certain setups.
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ The model takes images, text instructions, and robot state as input, and outputs
|
|||||||
|
|
||||||
## Reproducing π₀Fast results
|
## Reproducing π₀Fast results
|
||||||
|
|
||||||
We reproduce the results of π₀Fast on the LIBERO benchmark using the LeRobot implementation. We take the LeRobot PiFast base model [lerobot/pi0fast-base](https://huggingface.co/lerobot/pi0fast-base) and finetune for an additional 40kk steps in bfloat16, with batch size of 256 on 8 H100 GPUs using the [HuggingFace LIBERO dataset](https://huggingface.co/datasets/HuggingFaceVLA/libero).
|
We reproduce the results of π₀Fast on the LIBERO benchmark using the LeRobot implementation. We take the LeRobot PiFast base model [lerobot/pi0fast-base](https://huggingface.co/lerobot/pi0fast-base) and finetune for an additional 40k steps in bfloat16, with batch size of 256 on 8 H100 GPUs using the [HuggingFace LIBERO dataset](https://huggingface.co/datasets/HuggingFaceVLA/libero).
|
||||||
|
|
||||||
The finetuned model can be found here:
|
The finetuned model can be found here:
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ With processors, you choose the learning features you want to use for your polic
|
|||||||
## Three pipelines
|
## Three pipelines
|
||||||
|
|
||||||
We often compose three pipelines. Depending on your setup, some can be empty if action and observation spaces already match.
|
We often compose three pipelines. Depending on your setup, some can be empty if action and observation spaces already match.
|
||||||
Each of these pipelines handle different conversions between different action and observation spaces. Below is a quick explanation of each pipeline.
|
Each of these pipelines handles different conversions between different action and observation spaces. Below is a quick explanation of each pipeline.
|
||||||
|
|
||||||
1. Pipeline 1: Teleop action space → dataset action space (phone pose → EE targets)
|
1. Pipeline 1: Teleop action space → dataset action space (phone pose → EE targets)
|
||||||
2. Pipeline 2: Dataset action space → robot command space (EE targets → joints)
|
2. Pipeline 2: Dataset action space → robot command space (EE targets → joints)
|
||||||
@@ -74,15 +74,15 @@ In the phone to SO-100 follower examples we use the following adapters:
|
|||||||
- `robot_action_to_transition`: transforms the teleop action dict to a pipeline transition.
|
- `robot_action_to_transition`: transforms the teleop action dict to a pipeline transition.
|
||||||
- `transition_to_robot_action`: transforms the pipeline transition to a robot action dict.
|
- `transition_to_robot_action`: transforms the pipeline transition to a robot action dict.
|
||||||
- `observation_to_transition`: transforms the robot observation dict to a pipeline transition.
|
- `observation_to_transition`: transforms the robot observation dict to a pipeline transition.
|
||||||
- `transition_to_observation`: transforms the pipeline transition to a observation dict.
|
- `transition_to_observation`: transforms the pipeline transition to an observation dict.
|
||||||
|
|
||||||
Checkout [src/lerobot/processor/converters.py](https://github.com/huggingface/lerobot/blob/main/src/lerobot/processor/converters.py) for more details.
|
Check out [src/lerobot/processor/converters.py](https://github.com/huggingface/lerobot/blob/main/src/lerobot/processor/converters.py) for more details.
|
||||||
|
|
||||||
## Dataset feature contracts
|
## Dataset feature contracts
|
||||||
|
|
||||||
Dataset features are determined by the keys saved in the dataset. Each step can declare what features it modifies in a contract called `transform_features(...)`. Once you build a processor, the processor can then aggregate all of these features with `aggregate_pipeline_dataset_features()` and merge multiple feature dicts with `combine_feature_dicts(...)`.
|
Dataset features are determined by the keys saved in the dataset. Each step can declare what features it modifies in a contract called `transform_features(...)`. Once you build a processor, the processor can then aggregate all of these features with `aggregate_pipeline_dataset_features()` and merge multiple feature dicts with `combine_feature_dicts(...)`.
|
||||||
|
|
||||||
Below is and example of how we declare features with the `transform_features` method in the phone to SO-100 follower examples:
|
Below is an example of how we declare features with the `transform_features` method in the phone to SO-100 follower examples:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def transform_features(
|
def transform_features(
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ 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.
|
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)
|
### Single-task evaluation (recommended for quick iteration)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -35,14 +35,11 @@ pip install --override <(printf 'gymnasium==0.29.1\nnumpy==1.26.4\n') \
|
|||||||
### Docker (recommended)
|
### Docker (recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build base image first (from repo root)
|
# Build the RoboMME evaluation image from the repo root
|
||||||
docker build -f docker/Dockerfile.eval-base -t lerobot-eval-base .
|
docker build -f docker/Dockerfile.benchmark.robomme -t lerobot-benchmark-robomme .
|
||||||
|
|
||||||
# Build RoboMME eval image (applies gymnasium + numpy pin overrides)
|
|
||||||
docker build -f docker/Dockerfile.benchmark.robomme -t lerobot-robomme .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `docker/Dockerfile.benchmark.robomme` image overrides `gymnasium==0.29.1` and `numpy==1.26.4` after lerobot's install. Both versions are runtime-safe for lerobot's actual API usage.
|
The benchmark Dockerfile extends the published `huggingface/lerobot-gpu:latest` image, then overrides `gymnasium==0.29.1` and `numpy==1.26.4`. Both versions are runtime-safe for lerobot's actual API usage.
|
||||||
|
|
||||||
## Running Evaluation
|
## Running Evaluation
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -57,7 +57,7 @@ policy_cfg.rtc_config = RTCConfig(
|
|||||||
policy = PI0Policy.from_pretrained("lerobot/pi0_base", policy_cfg=policy_cfg, device="cuda")
|
policy = PI0Policy.from_pretrained("lerobot/pi0_base", policy_cfg=policy_cfg, device="cuda")
|
||||||
|
|
||||||
# Now use predict_action_chunk with RTC parameters
|
# Now use predict_action_chunk with RTC parameters
|
||||||
inference_delay = 4 # How many steps of inference latency, this values should be calculated based on the inference latency of the policy
|
inference_delay = 4 # How many steps of inference latency, this value should be calculated based on the inference latency of the policy
|
||||||
|
|
||||||
# Initialize the action queue
|
# Initialize the action queue
|
||||||
action_queue = ActionQueue(policy_cfg.rtc_config)
|
action_queue = ActionQueue(policy_cfg.rtc_config)
|
||||||
@@ -100,7 +100,7 @@ Typical values: 8-12 steps
|
|||||||
RTCConfig(execution_horizon=10)
|
RTCConfig(execution_horizon=10)
|
||||||
```
|
```
|
||||||
|
|
||||||
**`max_guidance_weight`**: How strongly to enforce consistency with the previous chunk. This is a hyperparameter that can be tuned to balance the smoothness of the transitions and the reactivity of the policy. For 10 steps flow matching (SmolVLA, Pi0, Pi0.5), a value of 10.0 is a optimal value.
|
**`max_guidance_weight`**: How strongly to enforce consistency with the previous chunk. This is a hyperparameter that can be tuned to balance the smoothness of the transitions and the reactivity of the policy. For 10 steps flow matching (SmolVLA, Pi0, Pi0.5), a value of 10.0 is an optimal value.
|
||||||
|
|
||||||
**`prefix_attention_schedule`**: How to weight consistency across the overlap region.
|
**`prefix_attention_schedule`**: How to weight consistency across the overlap region.
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ lerobot-train --help
|
|||||||
|
|
||||||
## Evaluate the finetuned model and run it in real-time
|
## Evaluate the finetuned model and run it in real-time
|
||||||
|
|
||||||
Similarly for when recording an episode, it is recommended that you are logged in to the HuggingFace Hub. You can follow the corresponding steps: [Record a dataset](./il_robots).
|
Similarly for when recording an episode, it is recommended that you are logged in to the HuggingFace Hub. You can follow the corresponding steps: [Record a dataset](./il_robots#record-a-dataset).
|
||||||
Once you are logged in, you can run inference in your setup by doing:
|
Once you are logged in, you can run inference in your setup by doing:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ It is advisable to install one 3-pin cable in the motor after placing them befor
|
|||||||
<hfoption id="Leader">
|
<hfoption id="Leader">
|
||||||
|
|
||||||
- Mount the leader holder onto the wrist and secure it with 4 M3x6mm screws.
|
- Mount the leader holder onto the wrist and secure it with 4 M3x6mm screws.
|
||||||
- Attach the handle to motor 5 using 1 M2x6mm screw.
|
- Attach the handle to the leader holder 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.
|
- 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.
|
- Attach the follower trigger with 4 M3x6mm screws.
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ LeRobot provides several utilities for manipulating datasets:
|
|||||||
3. **Merge Datasets** - Combine multiple datasets into one. The datasets must have identical features, and episodes are concatenated in the order specified in `repo_ids`
|
3. **Merge Datasets** - Combine multiple datasets into one. The datasets must have identical features, and episodes are concatenated in the order specified in `repo_ids`
|
||||||
4. **Add Features** - Add new features to a dataset
|
4. **Add Features** - Add new features to a dataset
|
||||||
5. **Remove Features** - Remove features from a dataset
|
5. **Remove Features** - Remove features from a dataset
|
||||||
6. **Convert to Video** - Convert image-based datasets to video format for efficient storage (RGB and depth cameras are encoded with separate encoders)
|
6. **Modify Tasks** - Change the natural-language task descriptions associated with episodes
|
||||||
7. **Re-encode Videos** - Re-encode an existing video dataset's RGB and/or depth streams with new encoder settings
|
7. **Convert to Video** - Convert image-based datasets to video format for efficient storage (RGB and depth cameras are encoded with separate encoders)
|
||||||
8. **Show the Info of Datasets** - Show the summary of datasets information such as number of episode etc.
|
8. **Re-encode Videos** - Re-encode an existing video dataset's RGB and/or depth streams with new encoder settings
|
||||||
|
9. **Show the Info of Datasets** - Show the summary of datasets information such as number of episode etc.
|
||||||
|
|
||||||
The core implementation is in `lerobot.datasets.dataset_tools`.
|
The core implementation is in `lerobot.datasets.dataset_tools`.
|
||||||
An example script detailing how to use the tools API is available in `examples/dataset/use_dataset_tools.py`.
|
An example script detailing how to use the tools API is available in `examples/dataset/use_dataset_tools.py`.
|
||||||
@@ -50,11 +51,11 @@ lerobot-edit-dataset \
|
|||||||
Divide a dataset into multiple subsets.
|
Divide a dataset into multiple subsets.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Split by fractions (e.g. 80% train, 20% test, 20% val)
|
# Split by fractions (e.g. 60% train, 20% val, 20% test)
|
||||||
lerobot-edit-dataset \
|
lerobot-edit-dataset \
|
||||||
--repo_id lerobot/pusht \
|
--repo_id lerobot/pusht \
|
||||||
--operation.type split \
|
--operation.type split \
|
||||||
--operation.splits '{"train": 0.8, "test": 0.2, "val": 0.2}'
|
--operation.splits '{"train": 0.6, "val": 0.2, "test": 0.2}'
|
||||||
|
|
||||||
# Split by specific episode indices
|
# Split by specific episode indices
|
||||||
lerobot-edit-dataset \
|
lerobot-edit-dataset \
|
||||||
@@ -89,6 +90,53 @@ lerobot-edit-dataset \
|
|||||||
--operation.feature_names "['observation.images.top']"
|
--operation.feature_names "['observation.images.top']"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Modify Tasks
|
||||||
|
|
||||||
|
Change the natural-language task descriptions attached to episodes. This is useful for fixing typos, standardizing wording, or re-labeling episodes.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> `modify_tasks` modifies the dataset **in-place** (updating `meta/tasks.parquet`, the `task_index` column in the data files, the `tasks` column in the episode metadata, and `total_tasks` in `meta/info.json`). The `--new_repo_id` and `--new_root` parameters are ignored for this operation.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set a single task for all episodes
|
||||||
|
lerobot-edit-dataset \
|
||||||
|
--repo_id lerobot/pusht \
|
||||||
|
--operation.type modify_tasks \
|
||||||
|
--operation.new_task "Pick up the cube and place it"
|
||||||
|
|
||||||
|
# Set different tasks for specific episodes
|
||||||
|
lerobot-edit-dataset \
|
||||||
|
--repo_id lerobot/pusht \
|
||||||
|
--operation.type modify_tasks \
|
||||||
|
--operation.episode_tasks '{"0": "Task A", "1": "Task B", "2": "Task A"}'
|
||||||
|
|
||||||
|
# Replace existing task strings wherever they appear
|
||||||
|
lerobot-edit-dataset \
|
||||||
|
--repo_id lerobot/pusht \
|
||||||
|
--operation.type modify_tasks \
|
||||||
|
--operation.task_replacements '{"Pick up the red cube": "Lift the red cube"}'
|
||||||
|
|
||||||
|
# Combine modes in a single run
|
||||||
|
lerobot-edit-dataset \
|
||||||
|
--repo_id lerobot/pusht \
|
||||||
|
--operation.type modify_tasks \
|
||||||
|
--operation.new_task "Default task" \
|
||||||
|
--operation.task_replacements '{"Pick up the red cube": "Lift the red cube"}' \
|
||||||
|
--operation.episode_tasks '{"5": "Special task for episode 5"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `new_task`: A single task string used as the default for episodes not otherwise covered.
|
||||||
|
- `episode_tasks`: Mapping from episode index to task string.
|
||||||
|
- `task_replacements`: Mapping from existing task strings to their replacements, applied to episodes whose current task matches a key. Every key must be an existing task in the dataset.
|
||||||
|
|
||||||
|
The modes can be combined in a single run. Per episode, the task is resolved with the following precedence:
|
||||||
|
|
||||||
|
`episode_tasks` > `task_replacements` > `new_task` > original task
|
||||||
|
|
||||||
|
At least one of `new_task`, `episode_tasks`, or `task_replacements` must be specified. An episode that ends up with no task raises an error.
|
||||||
|
|
||||||
#### Convert to Video
|
#### 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.
|
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.
|
||||||
|
|||||||
@@ -49,16 +49,16 @@ lerobot-record \
|
|||||||
|
|
||||||
All flags below are prefixed with `--dataset.rgb_encoder.` on the CLI.
|
All flags below are prefixed with `--dataset.rgb_encoder.` on the CLI.
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
| Parameter | Type | Default | Description |
|
||||||
| --------------- | ---------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------- | ---------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `vcodec` | `str` | `"libsvtav1"` | Video codec name. `"auto"` picks the first available hardware encoder from a fixed preference list, falling back to `libsvtav1`. |
|
| `vcodec` | `str` | `"libsvtav1"` | Video codec name. `"auto"` picks the first available hardware encoder from a fixed preference list, falling back to `libsvtav1`. |
|
||||||
| `pix_fmt` | `str` | `"yuv420p"` | Output pixel format. Must be supported by the chosen codec in your FFmpeg build. |
|
| `pix_fmt` | `str` | `"yuv420p"` | Output pixel format. Must be supported by the chosen codec in your FFmpeg build. |
|
||||||
| `g` | `int` | `2` | GOP size — a keyframe every `g` frames. Emitted as FFmpeg option `g`. |
|
| `g` | `int` | `2` | GOP size — a keyframe every `g` frames. Emitted as FFmpeg option `g`. |
|
||||||
| `crf` | `int` or `float` | `30` | Abstract quality value, mapped per codec (see the [mapping](#mapping-videoencoderconfig--ffmpeg-options) below). Lower → higher quality / larger output where the mapping is monotone. |
|
| `crf` | `int` or `float` | `30` | Abstract quality value, mapped per codec (see the [mapping](https://github.com/huggingface/lerobot/blob/main/src/lerobot/configs/video.py#L197)). Lower → higher quality / larger output where the mapping is monotone. |
|
||||||
| `preset` | `int` or `str` | `12` \* | Encoder speed preset; meaning depends on the codec. <br/>\* When unset and `vcodec=libsvtav1`, LeRobot defaults to `12`. |
|
| `preset` | `int` or `str` | `12` \* | Encoder speed preset; meaning depends on the codec. <br/>\* When unset and `vcodec=libsvtav1`, LeRobot defaults to `12`. |
|
||||||
| `fast_decode` | `int` | `0` | `libsvtav1`: `0–2`, passed via `svtav1-params`. <br/>`h264` / `hevc` (software): if `>0`, sets `tune=fastdecode`. <br/>Other codecs: usually unused. |
|
| `fast_decode` | `int` | `0` | `libsvtav1`: `0–2`, passed via `svtav1-params`. <br/>`h264` / `hevc` (software): if `>0`, sets `tune=fastdecode`. <br/>Other codecs: usually unused. |
|
||||||
| `video_backend` | `str` | `"pyav"` | Only `"pyav"` is currently implemented for video encoding. |
|
| `video_backend` | `str` | `"pyav"` | Only `"pyav"` is currently implemented for video encoding. |
|
||||||
| `extra_options` | `dict` | `{}` | Extra FFmpeg or codec specific options merged after the structured fields above. Cannot override keys already set by those fields. |
|
| `extra_options` | `dict` | `{}` | Extra FFmpeg or codec specific options merged after the structured fields above. Cannot override keys already set by those fields. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ from typing import Protocol
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||||
from lerobot.model.kinematics import RobotKinematics
|
from lerobot.model.kinematics import RobotKinematics
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
RobotProcessorPipeline,
|
RobotProcessorPipeline,
|
||||||
@@ -56,7 +57,6 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
|||||||
EEBoundsAndSafety,
|
EEBoundsAndSafety,
|
||||||
InverseKinematicsEEToJoints,
|
InverseKinematicsEEToJoints,
|
||||||
)
|
)
|
||||||
from lerobot.types import RobotAction, RobotObservation
|
|
||||||
from lerobot.utils.constants import HF_LEROBOT_CALIBRATION, HF_LEROBOT_HOME, TELEOPERATORS
|
from lerobot.utils.constants import HF_LEROBOT_CALIBRATION, HF_LEROBOT_HOME, TELEOPERATORS
|
||||||
from lerobot.utils.robot_utils import precise_sleep
|
from lerobot.utils.robot_utils import precise_sleep
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from lerobot.types import RobotAction
|
from lerobot.lerobot_types import RobotAction
|
||||||
|
|
||||||
from .base import _GRIPPER_MOTOR_SCALE, IsaacTeleopTeleoperator, _isaacteleop_available
|
from .base import _GRIPPER_MOTOR_SCALE, IsaacTeleopTeleoperator, _isaacteleop_available
|
||||||
from .config_isaac_teleop import SO101LeaderArmConfig
|
from .config_isaac_teleop import SO101LeaderArmConfig
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from typing import TYPE_CHECKING, Any
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from lerobot.types import RobotAction
|
from lerobot.lerobot_types import RobotAction
|
||||||
|
|
||||||
from .base import IsaacTeleopTeleoperator, _isaacteleop_available
|
from .base import IsaacTeleopTeleoperator, _isaacteleop_available
|
||||||
from .config_isaac_teleop import XRControllerConfig
|
from .config_isaac_teleop import XRControllerConfig
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from lerobot.configs.types import FeatureType, PipelineFeatureType, PolicyFeature
|
from lerobot.configs.types import FeatureType, PipelineFeatureType, PolicyFeature
|
||||||
|
from lerobot.lerobot_types import RobotAction
|
||||||
from lerobot.processor import ProcessorStepRegistry, RobotActionProcessorStep
|
from lerobot.processor import ProcessorStepRegistry, RobotActionProcessorStep
|
||||||
from lerobot.types import RobotAction
|
|
||||||
from lerobot.utils.rotation import Rotation
|
from lerobot.utils.rotation import Rotation
|
||||||
|
|
||||||
from .base import _GRIPPER_MOTOR_SCALE
|
from .base import _GRIPPER_MOTOR_SCALE
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from lerobot.cameras.opencv import OpenCVCameraConfig
|
|||||||
from lerobot.common.control_utils import predict_action
|
from lerobot.common.control_utils import predict_action
|
||||||
from lerobot.configs import FeatureType, PolicyFeature
|
from lerobot.configs import FeatureType, PolicyFeature
|
||||||
from lerobot.datasets import LeRobotDataset, aggregate_pipeline_dataset_features, create_initial_features
|
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.model.kinematics import RobotKinematics
|
||||||
from lerobot.policies import make_pre_post_processors
|
from lerobot.policies import make_pre_post_processors
|
||||||
from lerobot.policies.act import ACTPolicy
|
from lerobot.policies.act import ACTPolicy
|
||||||
@@ -38,7 +39,6 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
|||||||
ForwardKinematicsJointsToEE,
|
ForwardKinematicsJointsToEE,
|
||||||
InverseKinematicsEEToJoints,
|
InverseKinematicsEEToJoints,
|
||||||
)
|
)
|
||||||
from lerobot.types import RobotAction, RobotObservation
|
|
||||||
from lerobot.utils.constants import ACTION, OBS_STR
|
from lerobot.utils.constants import ACTION, OBS_STR
|
||||||
from lerobot.utils.feature_utils import build_dataset_frame, combine_feature_dicts
|
from lerobot.utils.feature_utils import build_dataset_frame, combine_feature_dicts
|
||||||
from lerobot.utils.keyboard_input import init_keyboard_listener
|
from lerobot.utils.keyboard_input import init_keyboard_listener
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
from lerobot.cameras.opencv import OpenCVCameraConfig
|
from lerobot.cameras.opencv import OpenCVCameraConfig
|
||||||
from lerobot.datasets import LeRobotDataset, aggregate_pipeline_dataset_features, create_initial_features
|
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.model.kinematics import RobotKinematics
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
RobotProcessorPipeline,
|
RobotProcessorPipeline,
|
||||||
@@ -36,7 +37,6 @@ from lerobot.scripts.lerobot_record import record_loop
|
|||||||
from lerobot.teleoperators.phone import Phone, PhoneConfig
|
from lerobot.teleoperators.phone import Phone, PhoneConfig
|
||||||
from lerobot.teleoperators.phone.config_phone import PhoneOS
|
from lerobot.teleoperators.phone.config_phone import PhoneOS
|
||||||
from lerobot.teleoperators.phone.phone_processor import MapPhoneActionToRobotAction
|
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.feature_utils import combine_feature_dicts
|
||||||
from lerobot.utils.keyboard_input import init_keyboard_listener
|
from lerobot.utils.keyboard_input import init_keyboard_listener
|
||||||
from lerobot.utils.utils import log_say
|
from lerobot.utils.utils import log_say
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from lerobot.datasets import LeRobotDataset
|
from lerobot.datasets import LeRobotDataset
|
||||||
|
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||||
from lerobot.model.kinematics import RobotKinematics
|
from lerobot.model.kinematics import RobotKinematics
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
RobotProcessorPipeline,
|
RobotProcessorPipeline,
|
||||||
@@ -27,7 +28,6 @@ from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig
|
|||||||
from lerobot.robots.so_follower.robot_kinematic_processor import (
|
from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||||
InverseKinematicsEEToJoints,
|
InverseKinematicsEEToJoints,
|
||||||
)
|
)
|
||||||
from lerobot.types import RobotAction, RobotObservation
|
|
||||||
from lerobot.utils.constants import ACTION
|
from lerobot.utils.constants import ACTION
|
||||||
from lerobot.utils.robot_utils import precise_sleep
|
from lerobot.utils.robot_utils import precise_sleep
|
||||||
from lerobot.utils.utils import log_say
|
from lerobot.utils.utils import log_say
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ Highlight, or DAgger via ``lerobot-rollout --strategy.type=...``.
|
|||||||
|
|
||||||
from lerobot.cameras.opencv import OpenCVCameraConfig
|
from lerobot.cameras.opencv import OpenCVCameraConfig
|
||||||
from lerobot.configs import PreTrainedConfig
|
from lerobot.configs import PreTrainedConfig
|
||||||
|
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||||
from lerobot.model.kinematics import RobotKinematics
|
from lerobot.model.kinematics import RobotKinematics
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
RobotProcessorPipeline,
|
RobotProcessorPipeline,
|
||||||
@@ -43,7 +44,6 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
|||||||
from lerobot.rollout import BaseStrategyConfig, RolloutConfig, build_rollout_context
|
from lerobot.rollout import BaseStrategyConfig, RolloutConfig, build_rollout_context
|
||||||
from lerobot.rollout.inference import SyncInferenceConfig
|
from lerobot.rollout.inference import SyncInferenceConfig
|
||||||
from lerobot.rollout.strategies import BaseStrategy
|
from lerobot.rollout.strategies import BaseStrategy
|
||||||
from lerobot.types import RobotAction, RobotObservation
|
|
||||||
from lerobot.utils.process import ProcessSignalHandler
|
from lerobot.utils.process import ProcessSignalHandler
|
||||||
from lerobot.utils.utils import init_logging
|
from lerobot.utils.utils import init_logging
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||||
from lerobot.model.kinematics import RobotKinematics
|
from lerobot.model.kinematics import RobotKinematics
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
RobotProcessorPipeline,
|
RobotProcessorPipeline,
|
||||||
@@ -31,7 +32,6 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
|||||||
from lerobot.teleoperators.phone import Phone, PhoneConfig
|
from lerobot.teleoperators.phone import Phone, PhoneConfig
|
||||||
from lerobot.teleoperators.phone.config_phone import PhoneOS
|
from lerobot.teleoperators.phone.config_phone import PhoneOS
|
||||||
from lerobot.teleoperators.phone.phone_processor import MapPhoneActionToRobotAction
|
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.robot_utils import precise_sleep
|
||||||
from lerobot.utils.visualization_utils import init_rerun, log_rerun_data
|
from lerobot.utils.visualization_utils import init_rerun, log_rerun_data
|
||||||
|
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ class RTCEvaluator:
|
|||||||
def run_evaluation(self):
|
def run_evaluation(self):
|
||||||
"""Run evaluation on two random dataset samples using three separate policies.
|
"""Run evaluation on two random dataset samples using three separate policies.
|
||||||
|
|
||||||
Note: Policies are deinitalized after each step to free memory. Large models
|
Note: Policies are deinitialized after each step to free memory. Large models
|
||||||
(e.g., VLA models with billions of parameters) cannot fit three instances in
|
(e.g., VLA models with billions of parameters) cannot fit three instances in
|
||||||
memory simultaneously. By deleting and garbage collecting after each step,
|
memory simultaneously. By deleting and garbage collecting after each step,
|
||||||
we ensure only one policy is loaded at a time.
|
we ensure only one policy is loaded at a time.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from lerobot.cameras.opencv import OpenCVCameraConfig
|
|||||||
from lerobot.common.control_utils import predict_action
|
from lerobot.common.control_utils import predict_action
|
||||||
from lerobot.configs import FeatureType, PolicyFeature
|
from lerobot.configs import FeatureType, PolicyFeature
|
||||||
from lerobot.datasets import LeRobotDataset, aggregate_pipeline_dataset_features, create_initial_features
|
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.model.kinematics import RobotKinematics
|
||||||
from lerobot.policies import make_pre_post_processors
|
from lerobot.policies import make_pre_post_processors
|
||||||
from lerobot.policies.act import ACTPolicy
|
from lerobot.policies.act import ACTPolicy
|
||||||
@@ -38,7 +39,6 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
|||||||
ForwardKinematicsJointsToEE,
|
ForwardKinematicsJointsToEE,
|
||||||
InverseKinematicsEEToJoints,
|
InverseKinematicsEEToJoints,
|
||||||
)
|
)
|
||||||
from lerobot.types import RobotAction, RobotObservation
|
|
||||||
from lerobot.utils.constants import ACTION, OBS_STR
|
from lerobot.utils.constants import ACTION, OBS_STR
|
||||||
from lerobot.utils.feature_utils import build_dataset_frame, combine_feature_dicts
|
from lerobot.utils.feature_utils import build_dataset_frame, combine_feature_dicts
|
||||||
from lerobot.utils.keyboard_input import init_keyboard_listener
|
from lerobot.utils.keyboard_input import init_keyboard_listener
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
from lerobot.cameras.opencv import OpenCVCameraConfig
|
from lerobot.cameras.opencv import OpenCVCameraConfig
|
||||||
from lerobot.datasets import LeRobotDataset, aggregate_pipeline_dataset_features, create_initial_features
|
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.model.kinematics import RobotKinematics
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
RobotProcessorPipeline,
|
RobotProcessorPipeline,
|
||||||
@@ -33,7 +34,6 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
|||||||
)
|
)
|
||||||
from lerobot.scripts.lerobot_record import record_loop
|
from lerobot.scripts.lerobot_record import record_loop
|
||||||
from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig
|
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.feature_utils import combine_feature_dicts
|
||||||
from lerobot.utils.keyboard_input import init_keyboard_listener
|
from lerobot.utils.keyboard_input import init_keyboard_listener
|
||||||
from lerobot.utils.utils import log_say
|
from lerobot.utils.utils import log_say
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from lerobot.datasets import LeRobotDataset
|
from lerobot.datasets import LeRobotDataset
|
||||||
|
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||||
from lerobot.model.kinematics import RobotKinematics
|
from lerobot.model.kinematics import RobotKinematics
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
RobotProcessorPipeline,
|
RobotProcessorPipeline,
|
||||||
@@ -28,7 +29,6 @@ from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig
|
|||||||
from lerobot.robots.so_follower.robot_kinematic_processor import (
|
from lerobot.robots.so_follower.robot_kinematic_processor import (
|
||||||
InverseKinematicsEEToJoints,
|
InverseKinematicsEEToJoints,
|
||||||
)
|
)
|
||||||
from lerobot.types import RobotAction, RobotObservation
|
|
||||||
from lerobot.utils.constants import ACTION
|
from lerobot.utils.constants import ACTION
|
||||||
from lerobot.utils.robot_utils import precise_sleep
|
from lerobot.utils.robot_utils import precise_sleep
|
||||||
from lerobot.utils.utils import log_say
|
from lerobot.utils.utils import log_say
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ forward/inverse kinematics.
|
|||||||
|
|
||||||
from lerobot.cameras.opencv import OpenCVCameraConfig
|
from lerobot.cameras.opencv import OpenCVCameraConfig
|
||||||
from lerobot.configs import PreTrainedConfig
|
from lerobot.configs import PreTrainedConfig
|
||||||
|
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||||
from lerobot.model.kinematics import RobotKinematics
|
from lerobot.model.kinematics import RobotKinematics
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
RobotProcessorPipeline,
|
RobotProcessorPipeline,
|
||||||
@@ -41,7 +42,6 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
|||||||
from lerobot.rollout import BaseStrategyConfig, RolloutConfig, build_rollout_context
|
from lerobot.rollout import BaseStrategyConfig, RolloutConfig, build_rollout_context
|
||||||
from lerobot.rollout.inference import SyncInferenceConfig
|
from lerobot.rollout.inference import SyncInferenceConfig
|
||||||
from lerobot.rollout.strategies import BaseStrategy
|
from lerobot.rollout.strategies import BaseStrategy
|
||||||
from lerobot.types import RobotAction, RobotObservation
|
|
||||||
from lerobot.utils.process import ProcessSignalHandler
|
from lerobot.utils.process import ProcessSignalHandler
|
||||||
from lerobot.utils.utils import init_logging
|
from lerobot.utils.utils import init_logging
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||||
from lerobot.model.kinematics import RobotKinematics
|
from lerobot.model.kinematics import RobotKinematics
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
RobotProcessorPipeline,
|
RobotProcessorPipeline,
|
||||||
@@ -30,7 +31,6 @@ from lerobot.robots.so_follower.robot_kinematic_processor import (
|
|||||||
InverseKinematicsEEToJoints,
|
InverseKinematicsEEToJoints,
|
||||||
)
|
)
|
||||||
from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig
|
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.robot_utils import precise_sleep
|
||||||
from lerobot.utils.visualization_utils import init_rerun, log_rerun_data
|
from lerobot.utils.visualization_utils import init_rerun, log_rerun_data
|
||||||
|
|
||||||
|
|||||||
+16
-3
@@ -67,8 +67,8 @@ dependencies = [
|
|||||||
"einops>=0.8.0,<0.9.0",
|
"einops>=0.8.0,<0.9.0",
|
||||||
|
|
||||||
# Config & Hub
|
# Config & Hub
|
||||||
"draccus==0.10.0", # TODO: Relax version constraint
|
"draccus>=0.11.6,<0.12.0",
|
||||||
"huggingface-hub>=1.0.0,<2.0.0",
|
"huggingface-hub>=1.6.0,<2.0.0",
|
||||||
"requests>=2.32.0,<3.0.0",
|
"requests>=2.32.0,<3.0.0",
|
||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
@@ -95,7 +95,7 @@ dependencies = [
|
|||||||
|
|
||||||
# ── Feature-scoped extras ──────────────────────────────────
|
# ── Feature-scoped extras ──────────────────────────────────
|
||||||
dataset = [
|
dataset = [
|
||||||
"datasets>=4.7.0,<5.0.0",
|
"datasets>=4.8.0,<5.0.0",
|
||||||
"pandas>=2.0.0,<3.0.0", # NOTE: Transitive dependency of datasets
|
"pandas>=2.0.0,<3.0.0", # NOTE: Transitive dependency of datasets
|
||||||
"pyarrow>=21.0.0,<30.0.0", # NOTE: Transitive dependency of datasets
|
"pyarrow>=21.0.0,<30.0.0", # NOTE: Transitive dependency of datasets
|
||||||
"lerobot[av-dep]",
|
"lerobot[av-dep]",
|
||||||
@@ -494,6 +494,19 @@ ignore_errors = true
|
|||||||
module = "lerobot.envs.*"
|
module = "lerobot.envs.*"
|
||||||
ignore_errors = false
|
ignore_errors = false
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "lerobot.annotations.*"
|
||||||
|
ignore_errors = false
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
check_untyped_defs = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "lerobot.transforms.*"
|
||||||
|
ignore_errors = false
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
check_untyped_defs = true
|
||||||
|
|
||||||
# [[tool.mypy.overrides]]
|
# [[tool.mypy.overrides]]
|
||||||
# module = "lerobot.utils.*"
|
# module = "lerobot.utils.*"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import draccus
|
|||||||
import grpc
|
import grpc
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
|
from lerobot.lerobot_types import PolicyAction
|
||||||
from lerobot.policies import get_policy_class, make_pre_post_processors
|
from lerobot.policies import get_policy_class, make_pre_post_processors
|
||||||
from lerobot.processor import PolicyProcessorPipeline
|
from lerobot.processor import PolicyProcessorPipeline
|
||||||
from lerobot.transport import (
|
from lerobot.transport import (
|
||||||
@@ -45,7 +46,6 @@ from lerobot.transport import (
|
|||||||
services_pb2_grpc, # type: ignore
|
services_pb2_grpc, # type: ignore
|
||||||
)
|
)
|
||||||
from lerobot.transport.utils import receive_bytes_in_chunks
|
from lerobot.transport.utils import receive_bytes_in_chunks
|
||||||
from lerobot.types import PolicyAction
|
|
||||||
|
|
||||||
from .configs import PolicyServerConfig
|
from .configs import PolicyServerConfig
|
||||||
from .constants import SUPPORTED_POLICIES
|
from .constants import SUPPORTED_POLICIES
|
||||||
|
|||||||
@@ -120,14 +120,22 @@ class OpenCVCamera(Camera):
|
|||||||
self.rotation: int | None = get_cv2_rotation(config.rotation)
|
self.rotation: int | None = get_cv2_rotation(config.rotation)
|
||||||
self.backend: int = config.backend
|
self.backend: int = config.backend
|
||||||
|
|
||||||
if self.height and self.width:
|
self.capture_width: int | None = None
|
||||||
self.capture_width, self.capture_height = self.width, self.height
|
self.capture_height: int | None = None
|
||||||
if self.rotation in [cv2.ROTATE_90_CLOCKWISE, cv2.ROTATE_90_COUNTERCLOCKWISE]:
|
self._reset_connection_settings()
|
||||||
self.capture_width, self.capture_height = self.height, self.width
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.__class__.__name__}({self.index_or_path})"
|
return f"{self.__class__.__name__}({self.index_or_path})"
|
||||||
|
|
||||||
|
def _reset_connection_settings(self) -> None:
|
||||||
|
"""Restore settings that may have been auto-detected during a failed connection."""
|
||||||
|
self.fps = self.config.fps
|
||||||
|
self.width = self.config.width
|
||||||
|
self.height = self.config.height
|
||||||
|
self.capture_width, self.capture_height = self.width, self.height
|
||||||
|
if self.rotation in [cv2.ROTATE_90_CLOCKWISE, cv2.ROTATE_90_COUNTERCLOCKWISE]:
|
||||||
|
self.capture_width, self.capture_height = self.height, self.width
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connected(self) -> bool:
|
def is_connected(self) -> bool:
|
||||||
"""Checks if the camera is currently connected and opened."""
|
"""Checks if the camera is currently connected and opened."""
|
||||||
@@ -164,17 +172,25 @@ class OpenCVCamera(Camera):
|
|||||||
f"Failed to open {self}.Run `lerobot-find-cameras opencv` to find available cameras."
|
f"Failed to open {self}.Run `lerobot-find-cameras opencv` to find available cameras."
|
||||||
)
|
)
|
||||||
|
|
||||||
self._configure_capture_settings()
|
try:
|
||||||
self._start_read_thread()
|
self._configure_capture_settings()
|
||||||
|
self._start_read_thread()
|
||||||
|
|
||||||
if warmup and self.warmup_s > 0:
|
if warmup and self.warmup_s > 0:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while time.time() - start_time < self.warmup_s:
|
while time.time() - start_time < self.warmup_s:
|
||||||
self.async_read(timeout_ms=self.warmup_s * 1000)
|
self.async_read(timeout_ms=self.warmup_s * 1000)
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
with self.frame_lock:
|
with self.frame_lock:
|
||||||
if self.latest_frame is None:
|
if self.latest_frame is None:
|
||||||
raise ConnectionError(f"{self} failed to capture frames during warmup.")
|
raise ConnectionError(f"{self} failed to capture frames during warmup.")
|
||||||
|
except BaseException:
|
||||||
|
try:
|
||||||
|
self._cleanup_resources()
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f"Failed to fully clean up {self} after connect() failed.")
|
||||||
|
self._reset_connection_settings()
|
||||||
|
raise
|
||||||
|
|
||||||
logger.info(f"{self} connected.")
|
logger.info(f"{self} connected.")
|
||||||
|
|
||||||
@@ -312,32 +328,36 @@ class OpenCVCamera(Camera):
|
|||||||
|
|
||||||
for target in targets_to_scan:
|
for target in targets_to_scan:
|
||||||
camera = cv2.VideoCapture(target)
|
camera = cv2.VideoCapture(target)
|
||||||
if camera.isOpened():
|
try:
|
||||||
default_width = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH))
|
if camera.isOpened():
|
||||||
default_height = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
default_width = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||||
default_fps = camera.get(cv2.CAP_PROP_FPS)
|
default_height = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||||
default_format = camera.get(cv2.CAP_PROP_FORMAT)
|
default_fps = camera.get(cv2.CAP_PROP_FPS)
|
||||||
|
default_format = camera.get(cv2.CAP_PROP_FORMAT)
|
||||||
|
|
||||||
# Get FOURCC code and convert to string
|
# Get FOURCC code and convert to string
|
||||||
default_fourcc_code = camera.get(cv2.CAP_PROP_FOURCC)
|
default_fourcc_code = camera.get(cv2.CAP_PROP_FOURCC)
|
||||||
default_fourcc_code_int = int(default_fourcc_code)
|
default_fourcc_code_int = int(default_fourcc_code)
|
||||||
default_fourcc = "".join([chr((default_fourcc_code_int >> 8 * i) & 0xFF) for i in range(4)])
|
default_fourcc = "".join(
|
||||||
|
[chr((default_fourcc_code_int >> 8 * i) & 0xFF) for i in range(4)]
|
||||||
|
)
|
||||||
|
|
||||||
camera_info = {
|
camera_info = {
|
||||||
"name": f"OpenCV Camera @ {target}",
|
"name": f"OpenCV Camera @ {target}",
|
||||||
"type": "OpenCV",
|
"type": "OpenCV",
|
||||||
"id": target,
|
"id": target,
|
||||||
"backend_api": camera.getBackendName(),
|
"backend_api": camera.getBackendName(),
|
||||||
"default_stream_profile": {
|
"default_stream_profile": {
|
||||||
"format": default_format,
|
"format": default_format,
|
||||||
"fourcc": default_fourcc,
|
"fourcc": default_fourcc,
|
||||||
"width": default_width,
|
"width": default_width,
|
||||||
"height": default_height,
|
"height": default_height,
|
||||||
"fps": default_fps,
|
"fps": default_fps,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
found_cameras_info.append(camera_info)
|
found_cameras_info.append(camera_info)
|
||||||
|
finally:
|
||||||
camera.release()
|
camera.release()
|
||||||
|
|
||||||
return found_cameras_info
|
return found_cameras_info
|
||||||
@@ -496,6 +516,26 @@ class OpenCVCamera(Camera):
|
|||||||
self.latest_timestamp = None
|
self.latest_timestamp = None
|
||||||
self.new_frame_event.clear()
|
self.new_frame_event.clear()
|
||||||
|
|
||||||
|
def _cleanup_resources(self) -> None:
|
||||||
|
"""Stop background reads and release the capture, including after partial setup."""
|
||||||
|
read_thread = self.thread
|
||||||
|
videocapture = self.videocapture
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._stop_read_thread()
|
||||||
|
finally:
|
||||||
|
self.videocapture = None
|
||||||
|
try:
|
||||||
|
if videocapture is not None:
|
||||||
|
videocapture.release()
|
||||||
|
finally:
|
||||||
|
# Releasing the device may unblock a hardware read that outlived
|
||||||
|
# the first bounded join in _stop_read_thread().
|
||||||
|
if read_thread is not None and read_thread.is_alive():
|
||||||
|
read_thread.join(timeout=2.0)
|
||||||
|
if read_thread.is_alive(): # pragma: no cover
|
||||||
|
logger.warning(f"{self} read thread remained alive after releasing the capture.")
|
||||||
|
|
||||||
@check_if_not_connected
|
@check_if_not_connected
|
||||||
def async_read(self, timeout_ms: float = 200) -> NDArray[Any]:
|
def async_read(self, timeout_ms: float = 200) -> NDArray[Any]:
|
||||||
"""
|
"""
|
||||||
@@ -586,16 +626,6 @@ class OpenCVCamera(Camera):
|
|||||||
if not self.is_connected and self.thread is None:
|
if not self.is_connected and self.thread is None:
|
||||||
raise DeviceNotConnectedError(f"{self} not connected.")
|
raise DeviceNotConnectedError(f"{self} not connected.")
|
||||||
|
|
||||||
if self.thread is not None:
|
self._cleanup_resources()
|
||||||
self._stop_read_thread()
|
|
||||||
|
|
||||||
if self.videocapture is not None:
|
|
||||||
self.videocapture.release()
|
|
||||||
self.videocapture = None
|
|
||||||
|
|
||||||
with self.frame_lock:
|
|
||||||
self.latest_frame = None
|
|
||||||
self.latest_timestamp = None
|
|
||||||
self.new_frame_event.clear()
|
|
||||||
|
|
||||||
logger.info(f"{self} disconnected.")
|
logger.info(f"{self} disconnected.")
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ class RealSenseCamera(Camera):
|
|||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
|
self.width: int | None = config.width
|
||||||
|
self.height: int | None = config.height
|
||||||
|
|
||||||
if config.serial_number_or_name.isdigit():
|
if config.serial_number_or_name.isdigit():
|
||||||
self.serial_number = config.serial_number_or_name
|
self.serial_number = config.serial_number_or_name
|
||||||
else:
|
else:
|
||||||
@@ -131,6 +134,9 @@ class RealSenseCamera(Camera):
|
|||||||
self.use_rgb = config.use_rgb
|
self.use_rgb = config.use_rgb
|
||||||
self.use_depth = config.use_depth
|
self.use_depth = config.use_depth
|
||||||
self.warmup_s = config.warmup_s
|
self.warmup_s = config.warmup_s
|
||||||
|
self.exposure: int | None = config.exposure
|
||||||
|
self.gain: int | None = config.gain
|
||||||
|
self.white_balance: int | None = config.white_balance
|
||||||
|
|
||||||
self.rs_pipeline: rs.pipeline | None = None
|
self.rs_pipeline: rs.pipeline | None = None
|
||||||
self.rs_profile: rs.pipeline_profile | None = None
|
self.rs_profile: rs.pipeline_profile | None = None
|
||||||
@@ -145,14 +151,23 @@ class RealSenseCamera(Camera):
|
|||||||
|
|
||||||
self.rotation: int | None = get_cv2_rotation(config.rotation)
|
self.rotation: int | None = get_cv2_rotation(config.rotation)
|
||||||
|
|
||||||
if self.height and self.width:
|
self.capture_width: int | None = None
|
||||||
self.capture_width, self.capture_height = self.width, self.height
|
self.capture_height: int | None = None
|
||||||
if self.rotation in [cv2.ROTATE_90_CLOCKWISE, cv2.ROTATE_90_COUNTERCLOCKWISE]:
|
self._reset_connection_settings()
|
||||||
self.capture_width, self.capture_height = self.height, self.width
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.__class__.__name__}({self.serial_number})"
|
return f"{self.__class__.__name__}({self.serial_number})"
|
||||||
|
|
||||||
|
def _reset_connection_settings(self) -> None:
|
||||||
|
"""Restore settings that may have been auto-detected during a failed connection."""
|
||||||
|
self.fps = self.config.fps
|
||||||
|
self.width = self.config.width
|
||||||
|
self.height = self.config.height
|
||||||
|
self.warmup_s = self.config.warmup_s
|
||||||
|
self.capture_width, self.capture_height = self.width, self.height
|
||||||
|
if self.rotation in [cv2.ROTATE_90_CLOCKWISE, cv2.ROTATE_90_COUNTERCLOCKWISE]:
|
||||||
|
self.capture_width, self.capture_height = self.height, self.width
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connected(self) -> bool:
|
def is_connected(self) -> bool:
|
||||||
"""Checks if the camera pipeline is started and streams are active."""
|
"""Checks if the camera pipeline is started and streams are active."""
|
||||||
@@ -172,7 +187,8 @@ class RealSenseCamera(Camera):
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
DeviceAlreadyConnectedError: If the camera is already connected.
|
DeviceAlreadyConnectedError: If the camera is already connected.
|
||||||
ValueError: If the configuration is invalid (e.g., missing serial/name, name not unique).
|
ValueError: If the configuration is invalid, a requested sensor option is unsupported,
|
||||||
|
or a requested sensor value is invalid.
|
||||||
ConnectionError: If the camera is found but fails to start the pipeline or no RealSense devices are detected at all.
|
ConnectionError: If the camera is found but fails to start the pipeline or no RealSense devices are detected at all.
|
||||||
RuntimeError: If the pipeline starts but fails to apply requested settings.
|
RuntimeError: If the pipeline starts but fails to apply requested settings.
|
||||||
"""
|
"""
|
||||||
@@ -190,22 +206,31 @@ class RealSenseCamera(Camera):
|
|||||||
f"Failed to open {self}.Run `lerobot-find-cameras realsense` to find available cameras."
|
f"Failed to open {self}.Run `lerobot-find-cameras realsense` to find available cameras."
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
self._configure_capture_settings()
|
try:
|
||||||
self._start_read_thread()
|
self._configure_capture_settings()
|
||||||
|
self._configure_sensor_options()
|
||||||
|
self._start_read_thread()
|
||||||
|
|
||||||
# NOTE(Steven/Caroline): Enforcing at least one second of warmup as RS cameras need a bit of time before the first read. If we don't wait, the first read from the warmup will raise.
|
# NOTE(Steven/Caroline): Enforcing at least one second of warmup as RS cameras need a bit of time before the first read. If we don't wait, the first read from the warmup will raise.
|
||||||
self.warmup_s = max(self.warmup_s, 1)
|
self.warmup_s = max(self.warmup_s, 1)
|
||||||
|
|
||||||
warmup_read = self.async_read if self.use_rgb else self.async_read_depth
|
warmup_read = self.async_read if self.use_rgb else self.async_read_depth
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while time.time() - start_time < self.warmup_s:
|
while time.time() - start_time < self.warmup_s:
|
||||||
warmup_read(timeout_ms=self.warmup_s * 1000)
|
warmup_read(timeout_ms=self.warmup_s * 1000)
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
with self.frame_lock:
|
with self.frame_lock:
|
||||||
if (self.use_rgb and self.latest_color_frame is None) or (
|
if (self.use_rgb and self.latest_color_frame is None) or (
|
||||||
self.use_depth and self.latest_depth_frame is None
|
self.use_depth and self.latest_depth_frame is None
|
||||||
):
|
):
|
||||||
raise ConnectionError(f"{self} failed to capture frames during warmup.")
|
raise ConnectionError(f"{self} failed to capture frames during warmup.")
|
||||||
|
except BaseException:
|
||||||
|
try:
|
||||||
|
self._cleanup_resources()
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f"Failed to fully clean up {self} after connect() failed.")
|
||||||
|
self._reset_connection_settings()
|
||||||
|
raise
|
||||||
|
|
||||||
logger.info(f"{self} connected.")
|
logger.info(f"{self} connected.")
|
||||||
|
|
||||||
@@ -339,6 +364,114 @@ class RealSenseCamera(Camera):
|
|||||||
self.new_frame_event.clear()
|
self.new_frame_event.clear()
|
||||||
return self._async_read(timeout_ms=10000, read_depth=read_depth)
|
return self._async_read(timeout_ms=10000, read_depth=read_depth)
|
||||||
|
|
||||||
|
def _get_color_sensor(self) -> "rs.sensor":
|
||||||
|
"""Returns the dedicated "RGB Camera" sensor that controls the color stream.
|
||||||
|
|
||||||
|
Manual color controls are only applied to a dedicated RGB module. Cameras
|
||||||
|
without one (e.g. the D405, whose color stream comes from the shared
|
||||||
|
"Stereo Module") are unsupported, so we never fall back to another sensor
|
||||||
|
to avoid altering the depth stream.
|
||||||
|
"""
|
||||||
|
if self.rs_profile is None:
|
||||||
|
raise RuntimeError(f"{self}: rs_profile must be initialized before use.")
|
||||||
|
|
||||||
|
device = self.rs_profile.get_device()
|
||||||
|
sensors = {s.get_info(rs.camera_info.name): s for s in device.query_sensors()}
|
||||||
|
|
||||||
|
if "RGB Camera" in sensors:
|
||||||
|
return sensors["RGB Camera"]
|
||||||
|
|
||||||
|
available = list(sensors.keys())
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{self}: manual color controls require a dedicated 'RGB Camera' module, which this camera does not have. ",
|
||||||
|
f"Available sensors: {available}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _set_sensor_option(self, sensor: "rs.sensor", option: "rs.option", value: float, label: str) -> None:
|
||||||
|
"""Sets a sensor option, re-raising range errors with actionable diagnostics."""
|
||||||
|
try:
|
||||||
|
sensor.set_option(option, value)
|
||||||
|
except Exception as e:
|
||||||
|
range_info = ""
|
||||||
|
try:
|
||||||
|
option_range = sensor.get_option_range(option)
|
||||||
|
range_info = (
|
||||||
|
f" (supported range: min={option_range.min}, max={option_range.max}, "
|
||||||
|
f"step={option_range.step}, default={option_range.default})"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
range_info = " (option range unavailable)"
|
||||||
|
raise ValueError(
|
||||||
|
f"{self}: failed to set {label} to {value}{range_info}. Original error: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
def _configure_sensor_options(self) -> None:
|
||||||
|
"""Applies manual sensor options (exposure, gain, white balance) to the color sensor.
|
||||||
|
|
||||||
|
When exposure or gain is set, auto-exposure is disabled first. When white_balance
|
||||||
|
is set, auto white balance is disabled first. An omitted option is left unchanged,
|
||||||
|
and configuration is skipped entirely if all options are omitted.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the sensor does not support a requested option or a requested
|
||||||
|
value is invalid. Invalid-value errors include the option name, requested
|
||||||
|
value, and supported range when available.
|
||||||
|
"""
|
||||||
|
if self.exposure is None and self.gain is None and self.white_balance is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
color_sensor = self._get_color_sensor()
|
||||||
|
|
||||||
|
requested_options = (
|
||||||
|
(rs.option.exposure, self.exposure, "exposure"),
|
||||||
|
(rs.option.gain, self.gain, "gain"),
|
||||||
|
(rs.option.white_balance, self.white_balance, "white balance"),
|
||||||
|
)
|
||||||
|
unsupported_options = [
|
||||||
|
label
|
||||||
|
for option, value, label in requested_options
|
||||||
|
if value is not None and not color_sensor.supports(option)
|
||||||
|
]
|
||||||
|
if unsupported_options:
|
||||||
|
raise ValueError(
|
||||||
|
f"{self}: color sensor does not support requested manual options: {unsupported_options}."
|
||||||
|
)
|
||||||
|
|
||||||
|
manual_exposure_requested = self.exposure is not None or self.gain is not None
|
||||||
|
if manual_exposure_requested:
|
||||||
|
if color_sensor.supports(rs.option.enable_auto_exposure):
|
||||||
|
self._set_sensor_option(color_sensor, rs.option.enable_auto_exposure, 0, "auto-exposure")
|
||||||
|
logger.info(f"{self} auto-exposure disabled.")
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"{self} sensor does not support disabling auto-exposure; "
|
||||||
|
"applying manual exposure/gain directly."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.exposure is not None:
|
||||||
|
self._set_sensor_option(color_sensor, rs.option.exposure, self.exposure, "exposure")
|
||||||
|
logger.info(f"{self} exposure set to {self.exposure}.")
|
||||||
|
|
||||||
|
if self.gain is not None:
|
||||||
|
self._set_sensor_option(color_sensor, rs.option.gain, self.gain, "gain")
|
||||||
|
logger.info(f"{self} gain set to {self.gain}.")
|
||||||
|
|
||||||
|
if self.white_balance is not None:
|
||||||
|
if color_sensor.supports(rs.option.enable_auto_white_balance):
|
||||||
|
self._set_sensor_option(
|
||||||
|
color_sensor, rs.option.enable_auto_white_balance, 0, "auto white balance"
|
||||||
|
)
|
||||||
|
logger.info(f"{self} auto white balance disabled.")
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"{self} sensor does not support disabling auto white balance; "
|
||||||
|
"applying manual white balance directly."
|
||||||
|
)
|
||||||
|
self._set_sensor_option(
|
||||||
|
color_sensor, rs.option.white_balance, self.white_balance, "white balance"
|
||||||
|
)
|
||||||
|
logger.info(f"{self} white balance set to {self.white_balance}.")
|
||||||
|
|
||||||
@check_if_not_connected
|
@check_if_not_connected
|
||||||
def read_depth(self, timeout_ms: int = 200) -> NDArray[Any]:
|
def read_depth(self, timeout_ms: int = 200) -> NDArray[Any]:
|
||||||
"""
|
"""
|
||||||
@@ -541,6 +674,27 @@ class RealSenseCamera(Camera):
|
|||||||
self.latest_timestamp = None
|
self.latest_timestamp = None
|
||||||
self.new_frame_event.clear()
|
self.new_frame_event.clear()
|
||||||
|
|
||||||
|
def _cleanup_resources(self) -> None:
|
||||||
|
"""Stop background reads and stop the pipeline, including after partial setup."""
|
||||||
|
read_thread = self.thread
|
||||||
|
rs_pipeline = self.rs_pipeline
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._stop_read_thread()
|
||||||
|
finally:
|
||||||
|
self.rs_pipeline = None
|
||||||
|
self.rs_profile = None
|
||||||
|
try:
|
||||||
|
if rs_pipeline is not None:
|
||||||
|
rs_pipeline.stop()
|
||||||
|
finally:
|
||||||
|
# Stopping the pipeline may unblock a hardware read that outlived
|
||||||
|
# the first bounded join in _stop_read_thread().
|
||||||
|
if read_thread is not None and read_thread.is_alive():
|
||||||
|
read_thread.join(timeout=2.0)
|
||||||
|
if read_thread.is_alive(): # pragma: no cover
|
||||||
|
logger.warning(f"{self} read thread remained alive after stopping the pipeline.")
|
||||||
|
|
||||||
def _async_read(self, timeout_ms: float, read_depth: bool = False) -> NDArray[Any]:
|
def _async_read(self, timeout_ms: float, read_depth: bool = False) -> NDArray[Any]:
|
||||||
"""Shared helper for :meth:`async_read`/:meth:`async_read_depth`: return the latest buffered frame."""
|
"""Shared helper for :meth:`async_read`/:meth:`async_read_depth`: return the latest buffered frame."""
|
||||||
if self.thread is None or not self.thread.is_alive():
|
if self.thread is None or not self.thread.is_alive():
|
||||||
@@ -684,18 +838,5 @@ class RealSenseCamera(Camera):
|
|||||||
f"Attempted to disconnect {self}, but it appears already disconnected."
|
f"Attempted to disconnect {self}, but it appears already disconnected."
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.thread is not None:
|
self._cleanup_resources()
|
||||||
self._stop_read_thread()
|
|
||||||
|
|
||||||
if self.rs_pipeline is not None:
|
|
||||||
self.rs_pipeline.stop()
|
|
||||||
self.rs_pipeline = None
|
|
||||||
self.rs_profile = None
|
|
||||||
|
|
||||||
with self.frame_lock:
|
|
||||||
self.latest_color_frame = None
|
|
||||||
self.latest_depth_frame = None
|
|
||||||
self.latest_timestamp = None
|
|
||||||
self.new_frame_event.clear()
|
|
||||||
|
|
||||||
logger.info(f"{self} disconnected.")
|
logger.info(f"{self} disconnected.")
|
||||||
|
|||||||
@@ -46,6 +46,17 @@ class RealSenseCameraConfig(CameraConfig):
|
|||||||
use_depth: Whether to enable depth stream. Defaults to False.
|
use_depth: Whether to enable depth stream. Defaults to False.
|
||||||
rotation: Image rotation setting (0°, 90°, 180°, or 270°). Defaults to no rotation.
|
rotation: Image rotation setting (0°, 90°, 180°, or 270°). Defaults to no rotation.
|
||||||
warmup_s: Time reading frames before returning from connect (in seconds)
|
warmup_s: Time reading frames before returning from connect (in seconds)
|
||||||
|
exposure: Manual exposure value for the color sensor. When set, auto-exposure is
|
||||||
|
disabled and this fixed value is used. Valid ranges are camera-model specific
|
||||||
|
and reported if the value is rejected. Defaults to None (leave unchanged).
|
||||||
|
gain: Manual gain value for the color sensor. When set, auto-exposure is disabled
|
||||||
|
and this fixed gain is used, which also freezes exposure at its current value
|
||||||
|
when no exposure is configured. Valid ranges are camera-model specific and
|
||||||
|
reported if the value is rejected. Defaults to None (leave unchanged).
|
||||||
|
white_balance: Manual white balance value for the color sensor. When set, auto
|
||||||
|
white balance is disabled and this fixed value is used. Valid ranges are
|
||||||
|
camera-model specific and reported if the value is rejected. Defaults to None
|
||||||
|
(leave unchanged).
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
- Either name or serial_number must be specified.
|
- Either name or serial_number must be specified.
|
||||||
@@ -61,6 +72,9 @@ class RealSenseCameraConfig(CameraConfig):
|
|||||||
use_depth: bool = False
|
use_depth: bool = False
|
||||||
rotation: Cv2Rotation = Cv2Rotation.NO_ROTATION
|
rotation: Cv2Rotation = Cv2Rotation.NO_ROTATION
|
||||||
warmup_s: int = 1
|
warmup_s: int = 1
|
||||||
|
exposure: int | None = None
|
||||||
|
gain: int | None = None
|
||||||
|
white_balance: int | None = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
self.color_mode = ColorMode(self.color_mode)
|
self.color_mode = ColorMode(self.color_mode)
|
||||||
@@ -69,6 +83,18 @@ class RealSenseCameraConfig(CameraConfig):
|
|||||||
if not self.use_rgb and not self.use_depth:
|
if not self.use_rgb and not self.use_depth:
|
||||||
raise ValueError("At least one of `use_rgb` or `use_depth` must be enabled.")
|
raise ValueError("At least one of `use_rgb` or `use_depth` must be enabled.")
|
||||||
|
|
||||||
|
manual_color_options = {
|
||||||
|
"exposure": self.exposure,
|
||||||
|
"gain": self.gain,
|
||||||
|
"white_balance": self.white_balance,
|
||||||
|
}
|
||||||
|
configured_color_options = [name for name, value in manual_color_options.items() if value is not None]
|
||||||
|
if configured_color_options and not self.use_rgb:
|
||||||
|
raise ValueError(
|
||||||
|
"Manual color sensor options require `use_rgb=True`. "
|
||||||
|
f"Configured options: {configured_color_options}."
|
||||||
|
)
|
||||||
|
|
||||||
values = (self.fps, self.width, self.height)
|
values = (self.fps, self.width, self.height)
|
||||||
if any(v is not None for v in values) and any(v is None for v in values):
|
if any(v is not None for v in values) and any(v is None for v in values):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ else:
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from lerobot.datasets import LeRobotDataset
|
from lerobot.datasets import LeRobotDataset
|
||||||
|
from lerobot.lerobot_types import PolicyAction
|
||||||
from lerobot.processor import PolicyProcessorPipeline
|
from lerobot.processor import PolicyProcessorPipeline
|
||||||
from lerobot.robots import Robot
|
from lerobot.robots import Robot
|
||||||
from lerobot.types import PolicyAction
|
|
||||||
|
|
||||||
|
|
||||||
def predict_action(
|
def predict_action(
|
||||||
|
|||||||
@@ -52,6 +52,17 @@ def get_step_checkpoint_dir(output_dir: Path, total_steps: int, step: int) -> Pa
|
|||||||
return output_dir / CHECKPOINTS_DIR / step_identifier
|
return output_dir / CHECKPOINTS_DIR / step_identifier
|
||||||
|
|
||||||
|
|
||||||
|
def should_save_checkpoint(step: int, save_freq: int, total_steps: int) -> bool:
|
||||||
|
"""Whether a checkpoint should be saved at ``step``.
|
||||||
|
|
||||||
|
A checkpoint is saved every ``save_freq`` steps and always after the final step. A
|
||||||
|
non-positive ``save_freq`` disables periodic saving (only the final checkpoint is
|
||||||
|
written), mirroring how ``log_freq``/``eval_freq`` treat non-positive values and
|
||||||
|
avoiding a ``ZeroDivisionError`` from ``step % 0``.
|
||||||
|
"""
|
||||||
|
return (save_freq > 0 and step % save_freq == 0) or step == total_steps
|
||||||
|
|
||||||
|
|
||||||
def save_training_step(
|
def save_training_step(
|
||||||
step: int, save_dir: Path, num_processes: int | None = None, batch_size: int | None = None
|
step: int, save_dir: Path, num_processes: int | None = None, batch_size: int | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -71,13 +71,19 @@ class DatasetRecordConfig:
|
|||||||
# Number of threads per encoder instance. None = auto (codec default).
|
# Number of threads per encoder instance. None = auto (codec default).
|
||||||
# Lower values reduce CPU usage, maps to 'lp' (via svtav1-params) for libsvtav1 and 'threads' for h264/hevc..
|
# Lower values reduce CPU usage, maps to 'lp' (via svtav1-params) for libsvtav1 and 'threads' for h264/hevc..
|
||||||
encoder_threads: int | None = None
|
encoder_threads: int | None = None
|
||||||
|
# Skip appending the date-time tag to repo_id, keeping the user-provided name as-is
|
||||||
|
# (e.g. self-managed versioned names intended for a later `lerobot-edit-dataset merge`).
|
||||||
|
no_stamp: bool = False
|
||||||
|
|
||||||
def stamp_repo_id(self) -> None:
|
def stamp_repo_id(self) -> None:
|
||||||
"""Append a date-time tag to ``repo_id`` so each recording session gets a unique name.
|
"""Append a date-time tag to ``repo_id`` so each recording session gets a unique name.
|
||||||
|
|
||||||
Must be called explicitly at dataset *creation* time — not on resume,
|
Must be called explicitly at dataset *creation* time — not on resume,
|
||||||
where the existing ``repo_id`` (already stamped) must be preserved.
|
where the existing ``repo_id`` (already stamped) must be preserved.
|
||||||
|
No-op when ``no_stamp`` is set, preserving a user-managed ``repo_id``.
|
||||||
"""
|
"""
|
||||||
|
if self.no_stamp:
|
||||||
|
return
|
||||||
if self.repo_id:
|
if self.repo_id:
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
self.repo_id = f"{self.repo_id}_{timestamp}"
|
self.repo_id = f"{self.repo_id}_{timestamp}"
|
||||||
|
|||||||
+145
-29
@@ -27,6 +27,13 @@ from typing import Any, TypeVar, cast
|
|||||||
|
|
||||||
import draccus
|
import draccus
|
||||||
import yaml # type: ignore[import-untyped]
|
import yaml # type: ignore[import-untyped]
|
||||||
|
from draccus.help_formatter import SimpleHelpFormatter
|
||||||
|
from draccus.utils import DecodingError
|
||||||
|
from draccus.wrappers import DataclassWrapper
|
||||||
|
from draccus.wrappers.choice_wrapper import ChoiceWrapper, UnionWrapper
|
||||||
|
from draccus.wrappers.field_wrapper import FieldWrapper
|
||||||
|
from draccus.wrappers.suppressing_argparse import SuppressingArgumentParser
|
||||||
|
from draccus.wrappers.wrapper import AggregateWrapper, Wrapper
|
||||||
|
|
||||||
from lerobot.utils.utils import has_method
|
from lerobot.utils.utils import has_method
|
||||||
|
|
||||||
@@ -72,11 +79,18 @@ def get_cli_overrides(field_name: str, args: Sequence[str] | None = None) -> lis
|
|||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
attr_level_args = []
|
attr_level_args = []
|
||||||
detect_string = f"--{field_name}."
|
detect_string = f"--{field_name}."
|
||||||
exclude_strings = (f"--{field_name}.{draccus.CHOICE_TYPE_KEY}=", f"--{field_name}.{PATH_KEY}=")
|
excluded_names = (draccus.CHOICE_TYPE_KEY, PATH_KEY)
|
||||||
for arg in args:
|
for index, arg in enumerate(args):
|
||||||
if arg.startswith(detect_string) and not arg.startswith(exclude_strings):
|
if not arg.startswith(detect_string):
|
||||||
denested_arg = f"--{arg.removeprefix(detect_string)}"
|
continue
|
||||||
attr_level_args.append(denested_arg)
|
|
||||||
|
denested_arg = arg.removeprefix(detect_string)
|
||||||
|
if denested_arg.split("=", maxsplit=1)[0] in excluded_names:
|
||||||
|
continue
|
||||||
|
|
||||||
|
attr_level_args.append(f"--{denested_arg}")
|
||||||
|
if "=" not in arg and index + 1 < len(args) and not args[index + 1].startswith("--"):
|
||||||
|
attr_level_args.append(args[index + 1])
|
||||||
|
|
||||||
return attr_level_args
|
return attr_level_args
|
||||||
|
|
||||||
@@ -84,10 +98,12 @@ def get_cli_overrides(field_name: str, args: Sequence[str] | None = None) -> lis
|
|||||||
def parse_arg(arg_name: str, args: Sequence[str] | None = None) -> str | None:
|
def parse_arg(arg_name: str, args: Sequence[str] | None = None) -> str | None:
|
||||||
if args is None:
|
if args is None:
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
prefix = f"--{arg_name}="
|
option = f"--{arg_name}"
|
||||||
for arg in args:
|
for index, arg in enumerate(args):
|
||||||
if arg.startswith(prefix):
|
if arg.startswith(f"{option}="):
|
||||||
return arg[len(prefix) :]
|
return arg.removeprefix(f"{option}=")
|
||||||
|
if arg == option and index + 1 < len(args) and not args[index + 1].startswith("--"):
|
||||||
|
return args[index + 1]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -95,7 +111,7 @@ def parse_plugin_args(plugin_arg_suffix: str, args: Sequence[str]) -> dict[str,
|
|||||||
"""Parse plugin-related arguments from command-line arguments.
|
"""Parse plugin-related arguments from command-line arguments.
|
||||||
|
|
||||||
This function extracts arguments from command-line arguments that match a specified suffix pattern.
|
This function extracts arguments from command-line arguments that match a specified suffix pattern.
|
||||||
It processes arguments in the format '--key=value' and returns them as a dictionary.
|
It accepts arguments in the formats '--key=value' and '--key value' and returns them as a dictionary.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
plugin_arg_suffix (str): The suffix to identify plugin-related arguments.
|
plugin_arg_suffix (str): The suffix to identify plugin-related arguments.
|
||||||
@@ -112,13 +128,18 @@ def parse_plugin_args(plugin_arg_suffix: str, args: Sequence[str]) -> dict[str,
|
|||||||
{'env.discover_packages_path': 'my_package'}
|
{'env.discover_packages_path': 'my_package'}
|
||||||
"""
|
"""
|
||||||
plugin_args = {}
|
plugin_args = {}
|
||||||
for arg in args:
|
for index, arg in enumerate(args):
|
||||||
if "=" in arg and plugin_arg_suffix in arg:
|
if not arg.startswith("--"):
|
||||||
key, value = arg.split("=", 1)
|
continue
|
||||||
# Remove leading '--' if present
|
|
||||||
if key.startswith("--"):
|
key, separator, value = arg[2:].partition("=")
|
||||||
key = key[2:]
|
if plugin_arg_suffix not in key:
|
||||||
plugin_args[key] = value
|
continue
|
||||||
|
if not separator:
|
||||||
|
if index + 1 >= len(args) or args[index + 1].startswith("--"):
|
||||||
|
continue
|
||||||
|
value = args[index + 1]
|
||||||
|
plugin_args[key] = value
|
||||||
return plugin_args
|
return plugin_args
|
||||||
|
|
||||||
|
|
||||||
@@ -185,10 +206,82 @@ def get_type_arg(field_name: str, args: Sequence[str] | None = None) -> str | No
|
|||||||
return parse_arg(f"{field_name}.{draccus.CHOICE_TYPE_KEY}", args)
|
return parse_arg(f"{field_name}.{draccus.CHOICE_TYPE_KEY}", args)
|
||||||
|
|
||||||
|
|
||||||
|
def _register_scoped_actions(
|
||||||
|
wrapper: Wrapper, parser: SuppressingArgumentParser, cli_args: Sequence[str]
|
||||||
|
) -> None:
|
||||||
|
"""Like draccus's own Wrapper.register_actions, but for a ChoiceType field only recurses into
|
||||||
|
the already-selected subclass (per CLI `.type` args), instead of every registered choice.
|
||||||
|
|
||||||
|
This mirrors draccus 0.11.x's internal wrapper traversal because its public parser eagerly registers
|
||||||
|
every choice before parsing the command line. Keep this in sync when updating draccus.
|
||||||
|
"""
|
||||||
|
if isinstance(wrapper, ChoiceWrapper):
|
||||||
|
group = parser.add_argument_group(title=wrapper.title, description=wrapper.description)
|
||||||
|
children = wrapper._children
|
||||||
|
arg_name = f"{wrapper.dest}.{draccus.CHOICE_TYPE_KEY}" if wrapper.dest else draccus.CHOICE_TYPE_KEY
|
||||||
|
group.add_argument(
|
||||||
|
f"--{arg_name}",
|
||||||
|
choices=list(children.keys()),
|
||||||
|
help=f"Which type of {wrapper.title} to use",
|
||||||
|
required=wrapper.required,
|
||||||
|
)
|
||||||
|
selected = get_type_arg(wrapper.dest, cli_args) if wrapper.dest else None
|
||||||
|
if selected in children:
|
||||||
|
_register_scoped_actions(children[selected], parser, cli_args)
|
||||||
|
elif isinstance(wrapper, DataclassWrapper):
|
||||||
|
group = parser.add_argument_group(title=wrapper.title, description=wrapper.description)
|
||||||
|
for child in wrapper._children:
|
||||||
|
if isinstance(child, AggregateWrapper):
|
||||||
|
parser.add_argument(
|
||||||
|
f"--{child.name}", type=str, required=False, help=f"Config file for {child.name}"
|
||||||
|
)
|
||||||
|
_register_scoped_actions(child, parser, cli_args)
|
||||||
|
elif isinstance(child, FieldWrapper):
|
||||||
|
child.add_action(group)
|
||||||
|
elif isinstance(wrapper, UnionWrapper):
|
||||||
|
group = parser.add_argument_group(title=wrapper.title, description=wrapper.description)
|
||||||
|
has_field_wrapper = False
|
||||||
|
for child in wrapper._children:
|
||||||
|
if isinstance(child, (DataclassWrapper, ChoiceWrapper)):
|
||||||
|
_register_scoped_actions(child, parser, cli_args)
|
||||||
|
elif isinstance(child, FieldWrapper):
|
||||||
|
has_field_wrapper = True
|
||||||
|
if has_field_wrapper:
|
||||||
|
group.add_argument(f"--{wrapper.dest}", required=False)
|
||||||
|
else:
|
||||||
|
wrapper.register_actions(parser)
|
||||||
|
|
||||||
|
|
||||||
|
def print_scoped_help(config_class: type, cli_args: Sequence[str]) -> None:
|
||||||
|
"""Prints --help output scoped to the choices already resolved on the CLI (e.g. --env.type=pusht),
|
||||||
|
instead of draccus's default of expanding every registered subclass of every ChoiceType field."""
|
||||||
|
parser = SuppressingArgumentParser(formatter_class=SimpleHelpFormatter)
|
||||||
|
parser.add_argument(
|
||||||
|
f"--{draccus.utils.CONFIG_ARG}", type=str, help="Path for a config file to parse with draccus"
|
||||||
|
)
|
||||||
|
_register_scoped_actions(DataclassWrapper(config_class), parser, cli_args)
|
||||||
|
parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
def filter_arg(field_to_filter: str, args: Sequence[str] | None = None) -> list[str]:
|
def filter_arg(field_to_filter: str, args: Sequence[str] | None = None) -> list[str]:
|
||||||
if args is None:
|
if args is None:
|
||||||
return []
|
return []
|
||||||
return [arg for arg in args if not arg.startswith(f"--{field_to_filter}=")]
|
option = f"--{field_to_filter}"
|
||||||
|
filtered_args = []
|
||||||
|
index = 0
|
||||||
|
while index < len(args):
|
||||||
|
arg = args[index]
|
||||||
|
if arg == option:
|
||||||
|
index += 1
|
||||||
|
if index < len(args) and not args[index].startswith("--"):
|
||||||
|
index += 1
|
||||||
|
continue
|
||||||
|
if arg.startswith(f"{option}="):
|
||||||
|
index += 1
|
||||||
|
continue
|
||||||
|
filtered_args.append(arg)
|
||||||
|
index += 1
|
||||||
|
return filtered_args
|
||||||
|
|
||||||
|
|
||||||
def filter_path_args(fields_to_filter: str | list[str], args: Sequence[str] | None = None) -> list[str]:
|
def filter_path_args(fields_to_filter: str | list[str], args: Sequence[str] | None = None) -> list[str]:
|
||||||
@@ -220,7 +313,23 @@ def filter_path_args(fields_to_filter: str | list[str], args: Sequence[str] | No
|
|||||||
argument=None,
|
argument=None,
|
||||||
message=f"Cannot specify both --{field}.{PATH_KEY} and --{field}.{draccus.CHOICE_TYPE_KEY}",
|
message=f"Cannot specify both --{field}.{PATH_KEY} and --{field}.{draccus.CHOICE_TYPE_KEY}",
|
||||||
)
|
)
|
||||||
filtered_args = [arg for arg in filtered_args if not arg.startswith(f"--{field}.")]
|
option_prefix = f"--{field}."
|
||||||
|
retained_args = []
|
||||||
|
index = 0
|
||||||
|
while index < len(filtered_args):
|
||||||
|
arg = filtered_args[index]
|
||||||
|
if arg.startswith(option_prefix):
|
||||||
|
index += 1
|
||||||
|
if (
|
||||||
|
"=" not in arg
|
||||||
|
and index < len(filtered_args)
|
||||||
|
and not filtered_args[index].startswith("--")
|
||||||
|
):
|
||||||
|
index += 1
|
||||||
|
continue
|
||||||
|
retained_args.append(arg)
|
||||||
|
index += 1
|
||||||
|
filtered_args = retained_args
|
||||||
|
|
||||||
return filtered_args
|
return filtered_args
|
||||||
|
|
||||||
@@ -299,6 +408,9 @@ def wrap(config_path: Path | None = None) -> Callable[[F], F]:
|
|||||||
# add the relevant CLI arg to the error message
|
# add the relevant CLI arg to the error message
|
||||||
raise PluginLoadError(f"{e}\nFailed plugin CLI Arg: {plugin_cli_arg}") from e
|
raise PluginLoadError(f"{e}\nFailed plugin CLI Arg: {plugin_cli_arg}") from e
|
||||||
cli_args = filter_arg(plugin_cli_arg, cli_args)
|
cli_args = filter_arg(plugin_cli_arg, cli_args)
|
||||||
|
if "--help" in cli_args or "-h" in cli_args:
|
||||||
|
print_scoped_help(argtype, cli_args)
|
||||||
|
sys.exit(0)
|
||||||
config_path_cli = parse_arg("config_path", cli_args)
|
config_path_cli = parse_arg("config_path", cli_args)
|
||||||
if has_method(argtype, "__get_path_fields__"):
|
if has_method(argtype, "__get_path_fields__"):
|
||||||
path_fields = argtype.__get_path_fields__()
|
path_fields = argtype.__get_path_fields__()
|
||||||
@@ -306,17 +418,21 @@ def wrap(config_path: Path | None = None) -> Callable[[F], F]:
|
|||||||
# Also extract path fields from the YAML/JSON config file
|
# Also extract path fields from the YAML/JSON config file
|
||||||
if config_path_cli:
|
if config_path_cli:
|
||||||
config_path_cli = extract_path_fields_from_config(config_path_cli, path_fields)
|
config_path_cli = extract_path_fields_from_config(config_path_cli, path_fields)
|
||||||
if has_method(argtype, "from_pretrained") and config_path_cli:
|
try:
|
||||||
cli_args = filter_arg("config_path", cli_args)
|
if has_method(argtype, "from_pretrained") and config_path_cli:
|
||||||
cfg = argtype.from_pretrained(config_path_cli, cli_args=cli_args)
|
|
||||||
else:
|
|
||||||
if config_path_cli:
|
|
||||||
cli_args = filter_arg("config_path", cli_args)
|
cli_args = filter_arg("config_path", cli_args)
|
||||||
cfg = draccus.parse(
|
cfg = argtype.from_pretrained(config_path_cli, cli_args=cli_args)
|
||||||
config_class=argtype,
|
else:
|
||||||
config_path=config_path_cli or config_path,
|
if config_path_cli:
|
||||||
args=cli_args,
|
cli_args = filter_arg("config_path", cli_args)
|
||||||
)
|
cfg = draccus.parse(
|
||||||
|
config_class=argtype,
|
||||||
|
config_path=config_path_cli or config_path,
|
||||||
|
args=cli_args,
|
||||||
|
)
|
||||||
|
except DecodingError as e:
|
||||||
|
print(f"error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
response = fn(cfg, *args, **kwargs)
|
response = fn(cfg, *args, **kwargs)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -163,8 +163,10 @@ class PreTrainedConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC): # type: igno
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _save_pretrained(self, save_directory: Path) -> None:
|
def _save_pretrained(self, save_directory: Path) -> None:
|
||||||
with open(save_directory / CONFIG_NAME, "w") as f, draccus.config_type("json"):
|
# Encode against the base class so draccus includes the choice "type" key,
|
||||||
draccus.dump(self, f, indent=4)
|
# 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)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_pretrained(
|
def from_pretrained(
|
||||||
|
|||||||
@@ -103,8 +103,10 @@ class RewardModelConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _save_pretrained(self, save_directory: Path) -> None:
|
def _save_pretrained(self, save_directory: Path) -> None:
|
||||||
with open(save_directory / CONFIG_NAME, "w") as f, draccus.config_type("json"):
|
# Encode against the base class so draccus includes the choice "type" key,
|
||||||
draccus.dump(self, f, indent=4)
|
# 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)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_pretrained(
|
def from_pretrained(
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ class TrainPipelineConfig(HubMixin):
|
|||||||
tolerance_s: float = 1e-4
|
tolerance_s: float = 1e-4
|
||||||
save_checkpoint: bool = True
|
save_checkpoint: bool = True
|
||||||
# Checkpoint is saved every `save_freq` training iterations and after the last training step.
|
# Checkpoint is saved every `save_freq` training iterations and after the last training step.
|
||||||
|
# A non-positive value disables periodic saving, keeping only the final checkpoint.
|
||||||
save_freq: int = 20_000
|
save_freq: int = 20_000
|
||||||
use_policy_training_preset: bool = True
|
use_policy_training_preset: bool = True
|
||||||
optimizer: OptimizerConfig | None = None
|
optimizer: OptimizerConfig | None = None
|
||||||
@@ -194,7 +195,11 @@ class TrainPipelineConfig(HubMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if Path(config_path).resolve().exists():
|
if Path(config_path).resolve().exists():
|
||||||
policy_dir = Path(config_path).parent
|
# `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
|
||||||
self.checkpoint_path = policy_dir.parent
|
self.checkpoint_path = policy_dir.parent
|
||||||
elif self.job.is_remote:
|
elif self.job.is_remote:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import copy
|
|||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, NotRequired, TypedDict
|
||||||
|
|
||||||
import datasets
|
import datasets
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -49,8 +50,32 @@ from .utils import (
|
|||||||
)
|
)
|
||||||
from .video_utils import concatenate_video_files, get_video_duration_in_s
|
from .video_utils import concatenate_video_files, get_video_duration_in_s
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def merge_video_feature_info_for_aggregate(all_metadata: list[LeRobotDatasetMetadata]) -> dict[str, dict]:
|
type FeatureDict = dict[str, dict[str, Any]]
|
||||||
|
type ChunkFile = tuple[int, int]
|
||||||
|
|
||||||
|
|
||||||
|
class IndexState(TypedDict):
|
||||||
|
chunk: int
|
||||||
|
file: int
|
||||||
|
src_to_dst: NotRequired[dict[ChunkFile, ChunkFile]]
|
||||||
|
|
||||||
|
|
||||||
|
class VideoIndex(TypedDict):
|
||||||
|
chunk: int
|
||||||
|
file: int
|
||||||
|
latest_duration: float
|
||||||
|
episode_duration: float
|
||||||
|
src_to_offset: NotRequired[dict[ChunkFile, float]]
|
||||||
|
src_to_dst: NotRequired[dict[ChunkFile, ChunkFile]]
|
||||||
|
dst_file_durations: NotRequired[dict[ChunkFile, float]]
|
||||||
|
|
||||||
|
|
||||||
|
type VideoIndexState = dict[str, VideoIndex]
|
||||||
|
|
||||||
|
|
||||||
|
def merge_video_feature_info_for_aggregate(all_metadata: list[LeRobotDatasetMetadata]) -> FeatureDict:
|
||||||
"""Create a merged video feature info dictionary for aggregation. The video encoder info is merged field-by-field: each key is kept only when every source agrees; otherwise that key is set to ``null`` (or ``{}`` for ``video.extra_options``) and a warning is logged.
|
"""Create a merged video feature info dictionary for aggregation. The video encoder info is merged field-by-field: each key is kept only when every source agrees; otherwise that key is set to ``null`` (or ``{}`` for ``video.extra_options``) and a warning is logged.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -59,14 +84,14 @@ def merge_video_feature_info_for_aggregate(all_metadata: list[LeRobotDatasetMeta
|
|||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary of merged video feature info.
|
dict: A dictionary of merged video feature info.
|
||||||
"""
|
"""
|
||||||
merged_info = copy.deepcopy(all_metadata[0].features)
|
merged_info: FeatureDict = copy.deepcopy(all_metadata[0].features)
|
||||||
video_keys = [k for k in merged_info if merged_info[k].get("dtype") == "video"]
|
video_keys = [k for k in merged_info if merged_info[k].get("dtype") == "video"]
|
||||||
|
|
||||||
for vk in video_keys:
|
for vk in video_keys:
|
||||||
video_infos = [m.features.get(vk, {}).get("info") or {} for m in all_metadata]
|
video_infos = [m.features.get(vk, {}).get("info") or {} for m in all_metadata]
|
||||||
base_video_info = video_infos[0]
|
base_video_info = video_infos[0]
|
||||||
|
|
||||||
merged_encoder_info: dict = {}
|
merged_encoder_info: dict[str, Any] = {}
|
||||||
fallback_keys: list[str] = []
|
fallback_keys: list[str] = []
|
||||||
for info_key in VIDEO_ENCODER_INFO_KEYS:
|
for info_key in VIDEO_ENCODER_INFO_KEYS:
|
||||||
values = [info.get(info_key, None) for info in video_infos]
|
values = [info.get(info_key, None) for info in video_infos]
|
||||||
@@ -80,7 +105,7 @@ def merge_video_feature_info_for_aggregate(all_metadata: list[LeRobotDatasetMeta
|
|||||||
merged_encoder_info[info_key] = {} if info_key == "video.extra_options" else None
|
merged_encoder_info[info_key] = {} if info_key == "video.extra_options" else None
|
||||||
|
|
||||||
if fallback_keys:
|
if fallback_keys:
|
||||||
logging.warning(
|
logger.warning(
|
||||||
f"Merging heterogeneous or incomplete video encoder metadata for feature {vk}. "
|
f"Merging heterogeneous or incomplete video encoder metadata for feature {vk}. "
|
||||||
f"Setting these keys to null: {fallback_keys}.",
|
f"Setting these keys to null: {fallback_keys}.",
|
||||||
)
|
)
|
||||||
@@ -92,7 +117,7 @@ def merge_video_feature_info_for_aggregate(all_metadata: list[LeRobotDatasetMeta
|
|||||||
return merged_info
|
return merged_info
|
||||||
|
|
||||||
|
|
||||||
def validate_all_metadata(all_metadata: list[LeRobotDatasetMetadata]):
|
def validate_all_metadata(all_metadata: list[LeRobotDatasetMetadata]) -> tuple[int, str | None, FeatureDict]:
|
||||||
"""Validates that all dataset metadata have consistent properties.
|
"""Validates that all dataset metadata have consistent properties.
|
||||||
|
|
||||||
Ensures all datasets have the same fps, robot_type, and features to guarantee
|
Ensures all datasets have the same fps, robot_type, and features to guarantee
|
||||||
@@ -129,7 +154,9 @@ def validate_all_metadata(all_metadata: list[LeRobotDatasetMetadata]):
|
|||||||
return fps, robot_type, features
|
return fps, robot_type, features
|
||||||
|
|
||||||
|
|
||||||
def update_data_df(df, src_meta, dst_meta):
|
def update_data_df(
|
||||||
|
df: pd.DataFrame, src_meta: LeRobotDatasetMetadata, dst_meta: LeRobotDatasetMetadata
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""Updates a data DataFrame with new indices and task mappings for aggregation.
|
"""Updates a data DataFrame with new indices and task mappings for aggregation.
|
||||||
|
|
||||||
Adjusts episode indices, frame indices, and task indices to account for
|
Adjusts episode indices, frame indices, and task indices to account for
|
||||||
@@ -154,12 +181,12 @@ def update_data_df(df, src_meta, dst_meta):
|
|||||||
|
|
||||||
|
|
||||||
def update_meta_data(
|
def update_meta_data(
|
||||||
df,
|
df: pd.DataFrame,
|
||||||
dst_meta,
|
dst_meta: LeRobotDatasetMetadata,
|
||||||
meta_idx,
|
meta_idx: IndexState,
|
||||||
data_idx,
|
data_idx: IndexState,
|
||||||
videos_idx,
|
videos_idx: VideoIndexState,
|
||||||
):
|
) -> pd.DataFrame:
|
||||||
"""Updates metadata DataFrame with new chunk, file, and timestamp indices.
|
"""Updates metadata DataFrame with new chunk, file, and timestamp indices.
|
||||||
|
|
||||||
Adjusts all indices and timestamps to account for previously aggregated
|
Adjusts all indices and timestamps to account for previously aggregated
|
||||||
@@ -289,7 +316,7 @@ def aggregate_datasets(
|
|||||||
chunk_size: int | None = None,
|
chunk_size: int | None = None,
|
||||||
concatenate_videos: bool = True,
|
concatenate_videos: bool = True,
|
||||||
concatenate_data: bool = True,
|
concatenate_data: bool = True,
|
||||||
):
|
) -> None:
|
||||||
"""Aggregates multiple LeRobot datasets into a single unified dataset.
|
"""Aggregates multiple LeRobot datasets into a single unified dataset.
|
||||||
|
|
||||||
This is the main function that orchestrates the aggregation process by:
|
This is the main function that orchestrates the aggregation process by:
|
||||||
@@ -309,7 +336,7 @@ def aggregate_datasets(
|
|||||||
concatenate_videos: When False, keep one mp4 per source file instead of packing into shards.
|
concatenate_videos: When False, keep one mp4 per source file instead of packing into shards.
|
||||||
concatenate_data: When False, keep one parquet per source file instead of packing into shards.
|
concatenate_data: When False, keep one parquet per source file instead of packing into shards.
|
||||||
"""
|
"""
|
||||||
logging.info("Start aggregate_datasets")
|
logger.info("Start aggregate_datasets")
|
||||||
|
|
||||||
if data_files_size_in_mb is None:
|
if data_files_size_in_mb is None:
|
||||||
data_files_size_in_mb = DEFAULT_DATA_FILE_SIZE_IN_MB
|
data_files_size_in_mb = DEFAULT_DATA_FILE_SIZE_IN_MB
|
||||||
@@ -341,15 +368,15 @@ def aggregate_datasets(
|
|||||||
video_files_size_in_mb=video_files_size_in_mb,
|
video_files_size_in_mb=video_files_size_in_mb,
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.info("Find all tasks")
|
logger.info("Find all tasks")
|
||||||
unique_tasks = pd.concat([m.tasks for m in all_metadata]).index.unique()
|
unique_tasks = pd.concat([m.tasks for m in all_metadata]).index.unique()
|
||||||
dst_meta.tasks = pd.DataFrame(
|
dst_meta.tasks = pd.DataFrame(
|
||||||
{"task_index": range(len(unique_tasks))}, index=pd.Index(unique_tasks, name="task")
|
{"task_index": range(len(unique_tasks))}, index=pd.Index(unique_tasks, name="task")
|
||||||
)
|
)
|
||||||
|
|
||||||
meta_idx = {"chunk": 0, "file": 0}
|
meta_idx: IndexState = {"chunk": 0, "file": 0}
|
||||||
data_idx = {"chunk": 0, "file": 0}
|
data_idx: IndexState = {"chunk": 0, "file": 0}
|
||||||
videos_idx = {
|
videos_idx: VideoIndexState = {
|
||||||
key: {"chunk": 0, "file": 0, "latest_duration": 0, "episode_duration": 0} for key in video_keys
|
key: {"chunk": 0, "file": 0, "latest_duration": 0, "episode_duration": 0} for key in video_keys
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,12 +400,17 @@ def aggregate_datasets(
|
|||||||
dst_meta.info.total_frames += src_meta.total_frames
|
dst_meta.info.total_frames += src_meta.total_frames
|
||||||
|
|
||||||
finalize_aggregation(dst_meta, all_metadata)
|
finalize_aggregation(dst_meta, all_metadata)
|
||||||
logging.info("Aggregation complete.")
|
logger.info("Aggregation complete.")
|
||||||
|
|
||||||
|
|
||||||
def aggregate_videos(
|
def aggregate_videos(
|
||||||
src_meta, dst_meta, videos_idx, video_files_size_in_mb, chunk_size, concatenate_videos=True
|
src_meta: LeRobotDatasetMetadata,
|
||||||
):
|
dst_meta: LeRobotDatasetMetadata,
|
||||||
|
videos_idx: VideoIndexState,
|
||||||
|
video_files_size_in_mb: float,
|
||||||
|
chunk_size: int,
|
||||||
|
concatenate_videos: bool = True,
|
||||||
|
) -> VideoIndexState:
|
||||||
"""Aggregates video chunks from a source dataset into the destination dataset.
|
"""Aggregates video chunks from a source dataset into the destination dataset.
|
||||||
|
|
||||||
Handles video file concatenation and rotation based on file size limits.
|
Handles video file concatenation and rotation based on file size limits.
|
||||||
@@ -406,15 +438,16 @@ def aggregate_videos(
|
|||||||
videos_idx[key]["dst_file_durations"] = {}
|
videos_idx[key]["dst_file_durations"] = {}
|
||||||
|
|
||||||
for key, video_idx in videos_idx.items():
|
for key, video_idx in videos_idx.items():
|
||||||
unique_chunk_file_pairs = {
|
unique_chunk_file_pairs: list[ChunkFile] = sorted(
|
||||||
(chunk, file)
|
{
|
||||||
for chunk, file in zip(
|
(chunk, file)
|
||||||
src_meta.episodes[f"videos/{key}/chunk_index"],
|
for chunk, file in zip(
|
||||||
src_meta.episodes[f"videos/{key}/file_index"],
|
src_meta.episodes[f"videos/{key}/chunk_index"],
|
||||||
strict=False,
|
src_meta.episodes[f"videos/{key}/file_index"],
|
||||||
)
|
strict=False,
|
||||||
}
|
)
|
||||||
unique_chunk_file_pairs = sorted(unique_chunk_file_pairs)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
chunk_idx = video_idx["chunk"]
|
chunk_idx = video_idx["chunk"]
|
||||||
file_idx = video_idx["file"]
|
file_idx = video_idx["file"]
|
||||||
@@ -489,7 +522,14 @@ def aggregate_videos(
|
|||||||
return videos_idx
|
return videos_idx
|
||||||
|
|
||||||
|
|
||||||
def aggregate_data(src_meta, dst_meta, data_idx, data_files_size_in_mb, chunk_size, concatenate_data=True):
|
def aggregate_data(
|
||||||
|
src_meta: LeRobotDatasetMetadata,
|
||||||
|
dst_meta: LeRobotDatasetMetadata,
|
||||||
|
data_idx: IndexState,
|
||||||
|
data_files_size_in_mb: float,
|
||||||
|
chunk_size: int,
|
||||||
|
concatenate_data: bool = True,
|
||||||
|
) -> IndexState:
|
||||||
"""Aggregates data chunks from a source dataset into the destination dataset.
|
"""Aggregates data chunks from a source dataset into the destination dataset.
|
||||||
|
|
||||||
Reads source data files, updates indices to match the aggregated dataset,
|
Reads source data files, updates indices to match the aggregated dataset,
|
||||||
@@ -510,14 +550,16 @@ def aggregate_data(src_meta, dst_meta, data_idx, data_files_size_in_mb, chunk_si
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Updated data_idx with current chunk and file indices.
|
dict: Updated data_idx with current chunk and file indices.
|
||||||
"""
|
"""
|
||||||
unique_chunk_file_ids = {
|
unique_chunk_file_ids: list[ChunkFile] = sorted(
|
||||||
(c, f)
|
{
|
||||||
for c, f in zip(
|
(c, f)
|
||||||
src_meta.episodes["data/chunk_index"], src_meta.episodes["data/file_index"], strict=False
|
for c, f in zip(
|
||||||
)
|
src_meta.episodes["data/chunk_index"],
|
||||||
}
|
src_meta.episodes["data/file_index"],
|
||||||
|
strict=False,
|
||||||
unique_chunk_file_ids = sorted(unique_chunk_file_ids)
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
contains_images = len(dst_meta.image_keys) > 0
|
contains_images = len(dst_meta.image_keys) > 0
|
||||||
|
|
||||||
# retrieve features schema for proper image typing in parquet
|
# retrieve features schema for proper image typing in parquet
|
||||||
@@ -525,7 +567,7 @@ def aggregate_data(src_meta, dst_meta, data_idx, data_files_size_in_mb, chunk_si
|
|||||||
|
|
||||||
# Track source to destination file mapping for metadata update
|
# Track source to destination file mapping for metadata update
|
||||||
# This is critical for handling datasets that are already results of a merge
|
# This is critical for handling datasets that are already results of a merge
|
||||||
src_to_dst: dict[tuple[int, int], tuple[int, int]] = {}
|
src_to_dst: dict[ChunkFile, ChunkFile] = {}
|
||||||
|
|
||||||
for src_chunk_idx, src_file_idx in unique_chunk_file_ids:
|
for src_chunk_idx, src_file_idx in unique_chunk_file_ids:
|
||||||
src_path = src_meta.root / DEFAULT_DATA_PATH.format(
|
src_path = src_meta.root / DEFAULT_DATA_PATH.format(
|
||||||
@@ -564,7 +606,13 @@ def aggregate_data(src_meta, dst_meta, data_idx, data_files_size_in_mb, chunk_si
|
|||||||
return data_idx
|
return data_idx
|
||||||
|
|
||||||
|
|
||||||
def aggregate_metadata(src_meta, dst_meta, meta_idx, data_idx, videos_idx):
|
def aggregate_metadata(
|
||||||
|
src_meta: LeRobotDatasetMetadata,
|
||||||
|
dst_meta: LeRobotDatasetMetadata,
|
||||||
|
meta_idx: IndexState,
|
||||||
|
data_idx: IndexState,
|
||||||
|
videos_idx: VideoIndexState,
|
||||||
|
) -> IndexState:
|
||||||
"""Aggregates metadata from a source dataset into the destination dataset.
|
"""Aggregates metadata from a source dataset into the destination dataset.
|
||||||
|
|
||||||
Reads source metadata files, updates all indices and timestamps,
|
Reads source metadata files, updates all indices and timestamps,
|
||||||
@@ -580,16 +628,16 @@ def aggregate_metadata(src_meta, dst_meta, meta_idx, data_idx, videos_idx):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Updated meta_idx with current chunk and file indices.
|
dict: Updated meta_idx with current chunk and file indices.
|
||||||
"""
|
"""
|
||||||
chunk_file_ids = {
|
chunk_file_ids: list[ChunkFile] = sorted(
|
||||||
(c, f)
|
{
|
||||||
for c, f in zip(
|
(c, f)
|
||||||
src_meta.episodes["meta/episodes/chunk_index"],
|
for c, f in zip(
|
||||||
src_meta.episodes["meta/episodes/file_index"],
|
src_meta.episodes["meta/episodes/chunk_index"],
|
||||||
strict=False,
|
src_meta.episodes["meta/episodes/file_index"],
|
||||||
)
|
strict=False,
|
||||||
}
|
)
|
||||||
|
}
|
||||||
chunk_file_ids = sorted(chunk_file_ids)
|
)
|
||||||
for chunk_idx, file_idx in chunk_file_ids:
|
for chunk_idx, file_idx in chunk_file_ids:
|
||||||
src_path = src_meta.root / DEFAULT_EPISODES_PATH.format(chunk_index=chunk_idx, file_index=file_idx)
|
src_path = src_meta.root / DEFAULT_EPISODES_PATH.format(chunk_index=chunk_idx, file_index=file_idx)
|
||||||
df = pd.read_parquet(src_path)
|
df = pd.read_parquet(src_path)
|
||||||
@@ -622,16 +670,16 @@ def aggregate_metadata(src_meta, dst_meta, meta_idx, data_idx, videos_idx):
|
|||||||
def append_or_create_parquet_file(
|
def append_or_create_parquet_file(
|
||||||
df: pd.DataFrame,
|
df: pd.DataFrame,
|
||||||
src_path: Path,
|
src_path: Path,
|
||||||
idx: dict[str, int],
|
idx: IndexState,
|
||||||
max_mb: float,
|
max_mb: float,
|
||||||
chunk_size: int,
|
chunk_size: int,
|
||||||
default_path: str,
|
default_path: str,
|
||||||
contains_images: bool = False,
|
contains_images: bool = False,
|
||||||
aggr_root: Path = None,
|
aggr_root: Path | None = None,
|
||||||
hf_features: datasets.Features | None = None,
|
hf_features: datasets.Features | None = None,
|
||||||
concatenate: bool = True,
|
concatenate: bool = True,
|
||||||
one_row_group_per_episode: bool = False,
|
one_row_group_per_episode: bool = False,
|
||||||
) -> tuple[dict[str, int], tuple[int, int]]:
|
) -> tuple[IndexState, ChunkFile]:
|
||||||
"""Appends data to an existing parquet file or creates a new one based on size constraints.
|
"""Appends data to an existing parquet file or creates a new one based on size constraints.
|
||||||
|
|
||||||
Manages file rotation when size limits are exceeded to prevent individual files
|
Manages file rotation when size limits are exceeded to prevent individual files
|
||||||
@@ -654,7 +702,13 @@ def append_or_create_parquet_file(
|
|||||||
Returns:
|
Returns:
|
||||||
tuple: (updated_idx, (dst_chunk, dst_file)) where updated_idx is the index dict
|
tuple: (updated_idx, (dst_chunk, dst_file)) where updated_idx is the index dict
|
||||||
and (dst_chunk, dst_file) is the actual destination file the data was written to.
|
and (dst_chunk, dst_file) is the actual destination file the data was written to.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If aggr_root is not provided.
|
||||||
"""
|
"""
|
||||||
|
if aggr_root is None:
|
||||||
|
raise ValueError("aggr_root must be provided.")
|
||||||
|
|
||||||
dst_chunk, dst_file = idx["chunk"], idx["file"]
|
dst_chunk, dst_file = idx["chunk"], idx["file"]
|
||||||
dst_path = aggr_root / default_path.format(chunk_index=dst_chunk, file_index=dst_file)
|
dst_path = aggr_root / default_path.format(chunk_index=dst_chunk, file_index=dst_file)
|
||||||
|
|
||||||
@@ -698,7 +752,9 @@ def append_or_create_parquet_file(
|
|||||||
return idx, (dst_chunk, dst_file)
|
return idx, (dst_chunk, dst_file)
|
||||||
|
|
||||||
|
|
||||||
def finalize_aggregation(aggr_meta, all_metadata):
|
def finalize_aggregation(
|
||||||
|
aggr_meta: LeRobotDatasetMetadata, all_metadata: list[LeRobotDatasetMetadata]
|
||||||
|
) -> None:
|
||||||
"""Finalizes the dataset aggregation by writing summary files and statistics.
|
"""Finalizes the dataset aggregation by writing summary files and statistics.
|
||||||
|
|
||||||
Writes the tasks file, info file with total counts and splits, and
|
Writes the tasks file, info file with total counts and splits, and
|
||||||
@@ -708,16 +764,16 @@ def finalize_aggregation(aggr_meta, all_metadata):
|
|||||||
aggr_meta: Aggregated dataset metadata.
|
aggr_meta: Aggregated dataset metadata.
|
||||||
all_metadata: List of all source dataset metadata objects.
|
all_metadata: List of all source dataset metadata objects.
|
||||||
"""
|
"""
|
||||||
logging.info("write tasks")
|
logger.info("write tasks")
|
||||||
write_tasks(aggr_meta.tasks, aggr_meta.root)
|
write_tasks(aggr_meta.tasks, aggr_meta.root)
|
||||||
|
|
||||||
logging.info("write info")
|
logger.info("write info")
|
||||||
aggr_meta.info.total_tasks = len(aggr_meta.tasks)
|
aggr_meta.info.total_tasks = len(aggr_meta.tasks)
|
||||||
aggr_meta.info.total_episodes = sum(m.total_episodes for m in all_metadata)
|
aggr_meta.info.total_episodes = sum(m.total_episodes for m in all_metadata)
|
||||||
aggr_meta.info.total_frames = sum(m.total_frames for m in all_metadata)
|
aggr_meta.info.total_frames = sum(m.total_frames for m in all_metadata)
|
||||||
aggr_meta.info.splits = {"train": f"0:{sum(m.total_episodes for m in all_metadata)}"}
|
aggr_meta.info.splits = {"train": f"0:{sum(m.total_episodes for m in all_metadata)}"}
|
||||||
write_info(aggr_meta.info, aggr_meta.root)
|
write_info(aggr_meta.info, aggr_meta.root)
|
||||||
|
|
||||||
logging.info("write stats")
|
logger.info("write stats")
|
||||||
aggr_meta.stats = aggregate_stats([m.stats for m in all_metadata])
|
aggr_meta.stats = aggregate_stats([m.stats for m in all_metadata])
|
||||||
write_stats(aggr_meta.stats, aggr_meta.root)
|
write_stats(aggr_meta.stats, aggr_meta.root)
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ import logging
|
|||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import packaging.version
|
import packaging.version
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pyarrow as pa
|
import pyarrow as pa
|
||||||
import pyarrow.parquet as pq
|
import pyarrow.parquet as pq
|
||||||
from huggingface_hub import snapshot_download
|
from huggingface_hub import snapshot_download, sync_bucket
|
||||||
|
from huggingface_hub.utils import WeakFileLock
|
||||||
|
|
||||||
from lerobot.configs import DEPTH_METER_UNIT, VideoEncoderConfig
|
from lerobot.configs import DEPTH_METER_UNIT, VideoEncoderConfig
|
||||||
from lerobot.utils.constants import DEFAULT_FEATURES, HF_LEROBOT_HOME, HF_LEROBOT_HUB_CACHE
|
from lerobot.utils.constants import DEFAULT_FEATURES, HF_LEROBOT_HOME, HF_LEROBOT_HUB_CACHE
|
||||||
@@ -74,6 +76,7 @@ class LeRobotDatasetMetadata:
|
|||||||
force_cache_sync: bool = False,
|
force_cache_sync: bool = False,
|
||||||
metadata_buffer_size: int = 10,
|
metadata_buffer_size: int = 10,
|
||||||
*,
|
*,
|
||||||
|
repo_type: Literal["dataset", "bucket"] = "dataset",
|
||||||
token: str | bool | None = None,
|
token: str | bool | None = None,
|
||||||
):
|
):
|
||||||
"""Load or download metadata for an existing LeRobot dataset.
|
"""Load or download metadata for an existing LeRobot dataset.
|
||||||
@@ -96,36 +99,53 @@ class LeRobotDatasetMetadata:
|
|||||||
even when local files exist.
|
even when local files exist.
|
||||||
metadata_buffer_size: Number of episode metadata records to buffer
|
metadata_buffer_size: Number of episode metadata records to buffer
|
||||||
in memory before flushing to parquet.
|
in memory before flushing to parquet.
|
||||||
|
repo_type: Repository type: "dataset" (default) or "bucket" for an
|
||||||
|
HF Storage Bucket streamed over hf://buckets/.
|
||||||
token: Authentication token used for Hub requests. Pass a string
|
token: Authentication token used for Hub requests. Pass a string
|
||||||
token, ``True`` to require the locally stored token, ``False``
|
token, ``True`` to require the locally stored token, ``False``
|
||||||
to disable authentication, or ``None`` to use the Hugging Face
|
to disable authentication, or ``None`` to use the Hugging Face
|
||||||
Hub default.
|
Hub default.
|
||||||
"""
|
"""
|
||||||
|
if repo_type not in ("dataset", "bucket"):
|
||||||
|
raise ValueError(f"repo_type must be 'dataset' or 'bucket', got {repo_type!r}")
|
||||||
|
|
||||||
self.repo_id = repo_id
|
self.repo_id = repo_id
|
||||||
|
self.repo_type = repo_type
|
||||||
self.revision = revision if revision else CODEBASE_VERSION
|
self.revision = revision if revision else CODEBASE_VERSION
|
||||||
self._requested_root = Path(root) if root is not None else None
|
self._requested_root = Path(root) if root is not None else None
|
||||||
self.root = self._requested_root if self._requested_root is not None else HF_LEROBOT_HOME / repo_id
|
if self._requested_root is not None:
|
||||||
|
self.root = self._requested_root
|
||||||
|
elif self.repo_type == "bucket":
|
||||||
|
self.root = HF_LEROBOT_HUB_CACHE / ("buckets--" + self.repo_id.replace("/", "--"))
|
||||||
|
else:
|
||||||
|
self.root = HF_LEROBOT_HOME / repo_id
|
||||||
self._pq_writer = None
|
self._pq_writer = None
|
||||||
self.latest_episode = None
|
self.latest_episode = None
|
||||||
self._metadata_buffer: list[dict] = []
|
self._metadata_buffer: list[dict] = []
|
||||||
self._metadata_buffer_size = metadata_buffer_size
|
self._metadata_buffer_size = metadata_buffer_size
|
||||||
self._finalized = False
|
self._finalized = False
|
||||||
|
|
||||||
try:
|
metadata_lock = contextlib.nullcontext()
|
||||||
if force_cache_sync or (
|
if self.repo_type == "bucket":
|
||||||
self._requested_root is None and has_legacy_hub_download_metadata(self.root)
|
self.root.parent.mkdir(parents=True, exist_ok=True)
|
||||||
):
|
metadata_lock = WeakFileLock(self.root.parent / f".{self.root.name}.lock")
|
||||||
raise FileNotFoundError
|
|
||||||
self._load_metadata()
|
|
||||||
except (FileNotFoundError, NotADirectoryError):
|
|
||||||
if is_valid_version(self.revision):
|
|
||||||
if token is None:
|
|
||||||
self.revision = get_safe_version(self.repo_id, self.revision)
|
|
||||||
else:
|
|
||||||
self.revision = get_safe_version(self.repo_id, self.revision, token=token)
|
|
||||||
|
|
||||||
self._pull_from_repo(allow_patterns="meta/", token=token)
|
with metadata_lock:
|
||||||
self._load_metadata()
|
try:
|
||||||
|
if force_cache_sync or (
|
||||||
|
self._requested_root is None and has_legacy_hub_download_metadata(self.root)
|
||||||
|
):
|
||||||
|
raise FileNotFoundError
|
||||||
|
self._load_metadata()
|
||||||
|
except (FileNotFoundError, NotADirectoryError):
|
||||||
|
if self.repo_type != "bucket" and is_valid_version(self.revision):
|
||||||
|
if token is None:
|
||||||
|
self.revision = get_safe_version(self.repo_id, self.revision)
|
||||||
|
else:
|
||||||
|
self.revision = get_safe_version(self.repo_id, self.revision, token=token)
|
||||||
|
|
||||||
|
self._pull_from_repo(allow_patterns="meta/", token=token)
|
||||||
|
self._load_metadata()
|
||||||
|
|
||||||
def _flush_metadata_buffer(self) -> None:
|
def _flush_metadata_buffer(self) -> None:
|
||||||
"""Write all buffered episode metadata to parquet file."""
|
"""Write all buffered episode metadata to parquet file."""
|
||||||
@@ -154,6 +174,12 @@ class LeRobotDatasetMetadata:
|
|||||||
self._pq_writer = pq.ParquetWriter(
|
self._pq_writer = pq.ParquetWriter(
|
||||||
path, schema=table.schema, compression="snappy", use_dictionary=True
|
path, schema=table.schema, compression="snappy", use_dictionary=True
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# Column order in `combined_dict` follows the source episode dict's insertion
|
||||||
|
# order, which can differ between batches (e.g. episodes originally stored in
|
||||||
|
# different parquet shards with different column orders). Realign to the
|
||||||
|
# writer's established schema so `write_table` doesn't reject a reordered match.
|
||||||
|
table = table.select(self._pq_writer.schema.names)
|
||||||
|
|
||||||
self._pq_writer.write_table(table)
|
self._pq_writer.write_table(table)
|
||||||
|
|
||||||
@@ -188,8 +214,8 @@ class LeRobotDatasetMetadata:
|
|||||||
def _load_metadata(self):
|
def _load_metadata(self):
|
||||||
self.info = load_info(self.root)
|
self.info = load_info(self.root)
|
||||||
check_version_compatibility(self.repo_id, self._version, CODEBASE_VERSION)
|
check_version_compatibility(self.repo_id, self._version, CODEBASE_VERSION)
|
||||||
self.tasks = load_tasks(self.root)
|
self.tasks = load_tasks(self.root) if self.total_tasks > 0 else None
|
||||||
self.episodes = load_episodes(self.root)
|
self.episodes = load_episodes(self.root) if self.total_episodes > 0 else None
|
||||||
self.stats = load_stats(self.root)
|
self.stats = load_stats(self.root)
|
||||||
|
|
||||||
def ensure_readable(self) -> None:
|
def ensure_readable(self) -> None:
|
||||||
@@ -232,6 +258,16 @@ class LeRobotDatasetMetadata:
|
|||||||
*,
|
*,
|
||||||
token: str | bool | None = None,
|
token: str | bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if self.repo_type == "bucket":
|
||||||
|
self.root.mkdir(parents=True, exist_ok=True)
|
||||||
|
sync_bucket(
|
||||||
|
f"hf://buckets/{self.repo_id}/meta",
|
||||||
|
str(self.root / "meta"),
|
||||||
|
delete=True,
|
||||||
|
quiet=True,
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
return
|
||||||
token_kwargs = {} if token is None else {"token": token}
|
token_kwargs = {} if token is None else {"token": token}
|
||||||
if self._requested_root is None:
|
if self._requested_root is None:
|
||||||
self.root = Path(
|
self.root = Path(
|
||||||
@@ -262,6 +298,8 @@ class LeRobotDatasetMetadata:
|
|||||||
@property
|
@property
|
||||||
def url_root(self) -> str:
|
def url_root(self) -> str:
|
||||||
"""Hugging Face Hub URL root for this dataset."""
|
"""Hugging Face Hub URL root for this dataset."""
|
||||||
|
if self.repo_type == "bucket":
|
||||||
|
return f"hf://buckets/{self.repo_id}"
|
||||||
return f"hf://datasets/{self.repo_id}"
|
return f"hf://datasets/{self.repo_id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -1045,10 +1045,12 @@ def _copy_data_with_feature_changes(
|
|||||||
df[feature_name] = feature_values
|
df[feature_name] = feature_values
|
||||||
else:
|
else:
|
||||||
feature_slice = values[frame_idx:end_idx]
|
feature_slice = values[frame_idx:end_idx]
|
||||||
if len(feature_slice.shape) > 1 and feature_slice.shape[1] == 1:
|
if feature_slice.ndim == 1:
|
||||||
|
df[feature_name] = feature_slice
|
||||||
|
elif feature_slice.ndim == 2 and feature_slice.shape[1] == 1:
|
||||||
df[feature_name] = feature_slice.flatten()
|
df[feature_name] = feature_slice.flatten()
|
||||||
else:
|
else:
|
||||||
df[feature_name] = feature_slice
|
df[feature_name] = list(feature_slice)
|
||||||
frame_idx = end_idx
|
frame_idx = end_idx
|
||||||
|
|
||||||
# Write using the same chunk/file structure as source
|
# Write using the same chunk/file structure as source
|
||||||
@@ -1435,15 +1437,18 @@ def modify_tasks(
|
|||||||
dataset: LeRobotDataset,
|
dataset: LeRobotDataset,
|
||||||
new_task: str | None = None,
|
new_task: str | None = None,
|
||||||
episode_tasks: dict[int, str] | None = None,
|
episode_tasks: dict[int, str] | None = None,
|
||||||
|
task_replacements: dict[str, str] | None = None,
|
||||||
) -> LeRobotDataset:
|
) -> LeRobotDataset:
|
||||||
"""Modify tasks in a LeRobotDataset.
|
"""Modify tasks in a LeRobotDataset.
|
||||||
|
|
||||||
This function allows you to either:
|
This function allows you to either:
|
||||||
1. Set a single task for the entire dataset (using `new_task`)
|
1. Set a single task for the entire dataset (using `new_task`)
|
||||||
2. Set specific tasks for specific episodes (using `episode_tasks`)
|
2. Set specific tasks for specific episodes (using `episode_tasks`)
|
||||||
|
3. Replace existing task strings wherever they appear (using `task_replacements`)
|
||||||
|
|
||||||
You can combine both: `new_task` sets the default, and `episode_tasks` overrides
|
Per episode, the task is resolved with precedence:
|
||||||
specific episodes.
|
`episode_tasks` > `task_replacements` > `new_task` > original task. An episode that ends
|
||||||
|
up with no task (none of the above apply and it had no original task) raises an error.
|
||||||
|
|
||||||
The dataset is modified in-place, updating only the task-related files:
|
The dataset is modified in-place, updating only the task-related files:
|
||||||
- meta/tasks.parquet
|
- meta/tasks.parquet
|
||||||
@@ -1453,11 +1458,14 @@ def modify_tasks(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
dataset: The source LeRobotDataset to modify.
|
dataset: The source LeRobotDataset to modify.
|
||||||
new_task: A single task string to apply to all episodes. If None and episode_tasks
|
new_task: Default task applied to any episode not covered by `episode_tasks` or a
|
||||||
is also None, raises an error.
|
matching `task_replacements` entry.
|
||||||
episode_tasks: Optional dict mapping episode indices to their task strings.
|
episode_tasks: Optional dict mapping episode indices to task strings. Takes precedence
|
||||||
Overrides `new_task` for specific episodes.
|
over both `task_replacements` and `new_task`.
|
||||||
|
task_replacements: Optional dict mapping existing task strings to new ones. Applied to
|
||||||
|
episodes whose current task matches a key. Every key must be an existing task.
|
||||||
|
|
||||||
|
At least one of `new_task`, `episode_tasks`, or `task_replacements` must be provided.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
Set a single task for all episodes:
|
Set a single task for all episodes:
|
||||||
@@ -1475,11 +1483,17 @@ def modify_tasks(
|
|||||||
new_task="Default task",
|
new_task="Default task",
|
||||||
episode_tasks={5: "Special task for episode 5"}
|
episode_tasks={5: "Special task for episode 5"}
|
||||||
)
|
)
|
||||||
"""
|
|
||||||
if new_task is None and episode_tasks is None:
|
|
||||||
raise ValueError("Must specify at least one of new_task or episode_tasks")
|
|
||||||
|
|
||||||
if episode_tasks is not None:
|
Replace existing task strings in-place:
|
||||||
|
dataset = modify_tasks(
|
||||||
|
dataset,
|
||||||
|
task_replacements={"Pick up the cube": "Lift the cube"}
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
if not new_task and not episode_tasks and not task_replacements:
|
||||||
|
raise ValueError("Must specify at least one of new_task, episode_tasks, or task_replacements")
|
||||||
|
|
||||||
|
if episode_tasks:
|
||||||
valid_indices = set(range(dataset.meta.total_episodes))
|
valid_indices = set(range(dataset.meta.total_episodes))
|
||||||
invalid = set(episode_tasks.keys()) - valid_indices
|
invalid = set(episode_tasks.keys()) - valid_indices
|
||||||
if invalid:
|
if invalid:
|
||||||
@@ -1489,19 +1503,29 @@ def modify_tasks(
|
|||||||
if dataset.meta.episodes is None:
|
if dataset.meta.episodes is None:
|
||||||
dataset.meta.episodes = load_episodes(dataset.root)
|
dataset.meta.episodes = load_episodes(dataset.root)
|
||||||
|
|
||||||
|
if task_replacements:
|
||||||
|
current_tasks = set(dataset.meta.tasks.index)
|
||||||
|
invalid_tasks = set(task_replacements) - current_tasks
|
||||||
|
if invalid_tasks:
|
||||||
|
raise ValueError(f"Task replacements reference unknown tasks: {sorted(invalid_tasks)}")
|
||||||
|
|
||||||
# Build the mapping from episode index to task string
|
# Build the mapping from episode index to task string
|
||||||
episode_to_task: dict[int, str] = {}
|
episode_to_task: dict[int, str] = {}
|
||||||
for ep_idx in range(dataset.meta.total_episodes):
|
for ep_idx in range(dataset.meta.total_episodes):
|
||||||
|
original_tasks = dataset.meta.episodes[ep_idx]["tasks"]
|
||||||
|
original_task = original_tasks[0] if original_tasks else None
|
||||||
|
|
||||||
if episode_tasks and ep_idx in episode_tasks:
|
if episode_tasks and ep_idx in episode_tasks:
|
||||||
episode_to_task[ep_idx] = episode_tasks[ep_idx]
|
episode_to_task[ep_idx] = episode_tasks[ep_idx]
|
||||||
elif new_task is not None:
|
elif task_replacements and original_task in task_replacements:
|
||||||
|
episode_to_task[ep_idx] = task_replacements[original_task]
|
||||||
|
elif new_task:
|
||||||
episode_to_task[ep_idx] = new_task
|
episode_to_task[ep_idx] = new_task
|
||||||
else:
|
elif original_task:
|
||||||
# Keep original task if not overridden and no default provided
|
# Keep original task if not overridden and no default provided
|
||||||
original_tasks = dataset.meta.episodes[ep_idx]["tasks"]
|
episode_to_task[ep_idx] = original_task
|
||||||
if not original_tasks:
|
else:
|
||||||
raise ValueError(f"Episode {ep_idx} has no tasks and no default task was provided")
|
raise ValueError(f"Episode {ep_idx} has no task; provide new_task or episode_tasks")
|
||||||
episode_to_task[ep_idx] = original_tasks[0]
|
|
||||||
|
|
||||||
# Collect all unique tasks and create new task mapping
|
# Collect all unique tasks and create new task mapping
|
||||||
unique_tasks = sorted(set(episode_to_task.values()))
|
unique_tasks = sorted(set(episode_to_task.values()))
|
||||||
|
|||||||
@@ -386,6 +386,9 @@ class DatasetWriter:
|
|||||||
self._episodes_since_last_encoding = 0
|
self._episodes_since_last_encoding = 0
|
||||||
|
|
||||||
if episode_data is None:
|
if episode_data is None:
|
||||||
|
# Post-save cleanup deliberately does not go through clear_episode_buffer():
|
||||||
|
# staging frames of video cameras must survive here — the (possibly batched)
|
||||||
|
# encoder still needs them and deletes them once each video is written.
|
||||||
if len(self._meta.image_keys) > 0:
|
if len(self._meta.image_keys) > 0:
|
||||||
self._delete_camera_frame_dirs(self._meta.image_keys)
|
self._delete_camera_frame_dirs(self._meta.image_keys)
|
||||||
self.episode_buffer = self._create_episode_buffer()
|
self.episode_buffer = self._create_episode_buffer()
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ from collections.abc import Sequence
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from lerobot.configs import PipelineFeatureType
|
from lerobot.configs import PipelineFeatureType
|
||||||
|
from lerobot.lerobot_types import RobotAction, RobotObservation
|
||||||
from lerobot.processor import DataProcessorPipeline
|
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.constants import ACTION, OBS_IMAGES, OBS_STATE, OBS_STR
|
||||||
from lerobot.utils.feature_utils import hw_to_dataset_features
|
from lerobot.utils.feature_utils import hw_to_dataset_features
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from collections.abc import Callable, Generator, Iterable, Iterator
|
from collections.abc import Callable, Generator, Iterable, Iterator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import datasets
|
import datasets
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -58,6 +59,10 @@ class LookAheadError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _ShardExhaustedError(Exception):
|
||||||
|
"""Raised when a streaming dataset shard has no more items."""
|
||||||
|
|
||||||
|
|
||||||
class Backtrackable[T]:
|
class Backtrackable[T]:
|
||||||
"""
|
"""
|
||||||
Wrap any iterator/iterable so you can step back up to `history` items
|
Wrap any iterator/iterable so you can step back up to `history` items
|
||||||
@@ -178,7 +183,7 @@ class Backtrackable[T]:
|
|||||||
"""
|
"""
|
||||||
Check if we can go back `steps` items without raising an IndexError.
|
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:
|
def can_peek_ahead(self, steps: int = 1) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -257,15 +262,16 @@ class StreamingLeRobotDataset(torch.utils.data.IterableDataset):
|
|||||||
return_uint8: bool = False,
|
return_uint8: bool = False,
|
||||||
depth_output_unit: str = DEFAULT_DEPTH_UNIT,
|
depth_output_unit: str = DEFAULT_DEPTH_UNIT,
|
||||||
*,
|
*,
|
||||||
|
repo_type: Literal["dataset", "bucket"] = "dataset",
|
||||||
token: str | bool | None = None,
|
token: str | bool | None = None,
|
||||||
):
|
):
|
||||||
"""Initialize a StreamingLeRobotDataset.
|
"""Initialize a StreamingLeRobotDataset.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repo_id (str): This is the repo id that will be used to fetch the dataset.
|
repo_id (str): This is the repo id that will be used to fetch the dataset.
|
||||||
root (Path | None, optional): Local directory to use for local datasets. When omitted, Hub
|
root (Path | None, optional): Local directory to use for local datasets. In bucket mode,
|
||||||
metadata is resolved through a revision-safe snapshot cache under
|
this is an optional local metadata-cache directory; parquet and video data remain remote.
|
||||||
``$HF_LEROBOT_HOME/hub``.
|
When omitted, Hub metadata is resolved through the cache under ``$HF_LEROBOT_HOME/hub``.
|
||||||
episodes (list[int] | None, optional): If specified, this will only load episodes specified by
|
episodes (list[int] | None, optional): If specified, this will only load episodes specified by
|
||||||
their episode_index in this list.
|
their episode_index in this list.
|
||||||
image_transforms (Callable | None, optional): Transform to apply to image data.
|
image_transforms (Callable | None, optional): Transform to apply to image data.
|
||||||
@@ -280,6 +286,8 @@ class StreamingLeRobotDataset(torch.utils.data.IterableDataset):
|
|||||||
shuffle (bool, optional): Whether to shuffle the dataset across exhaustions. Defaults to True.
|
shuffle (bool, optional): Whether to shuffle the dataset across exhaustions. Defaults to True.
|
||||||
depth_output_unit (str, optional): Physical unit depth maps are dequantized to ("m" or "mm").
|
depth_output_unit (str, optional): Physical unit depth maps are dequantized to ("m" or "mm").
|
||||||
Defaults to "mm".
|
Defaults to "mm".
|
||||||
|
repo_type: "dataset" (default) or "bucket" to stream from an HF Storage Bucket
|
||||||
|
over ``hf://buckets/``.
|
||||||
token: Authentication token used while streaming this dataset from
|
token: Authentication token used while streaming this dataset from
|
||||||
the Hub. Pass a string token, ``True`` to require the locally
|
the Hub. Pass a string token, ``True`` to require the locally
|
||||||
stored token, ``False`` to disable authentication, or ``None``
|
stored token, ``False`` to disable authentication, or ``None``
|
||||||
@@ -287,10 +295,14 @@ class StreamingLeRobotDataset(torch.utils.data.IterableDataset):
|
|||||||
on the dataset instance after initialization.
|
on the dataset instance after initialization.
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
if repo_type not in ("dataset", "bucket"):
|
||||||
|
raise ValueError(f"repo_type must be 'dataset' or 'bucket', got {repo_type!r}")
|
||||||
|
|
||||||
self.repo_id = repo_id
|
self.repo_id = repo_id
|
||||||
self._requested_root = Path(root) if root else None
|
self.repo_type = repo_type
|
||||||
|
self._requested_root = Path(root) if root is not None else None
|
||||||
self.root = self._requested_root if self._requested_root is not None else HF_LEROBOT_HOME / repo_id
|
self.root = self._requested_root if self._requested_root is not None else HF_LEROBOT_HOME / repo_id
|
||||||
self.streaming_from_local = root is not None
|
self.streaming_from_local = root is not None and self.repo_type == "dataset"
|
||||||
|
|
||||||
self.image_transforms = image_transforms
|
self.image_transforms = image_transforms
|
||||||
self.episodes = episodes
|
self.episodes = episodes
|
||||||
@@ -317,6 +329,7 @@ class StreamingLeRobotDataset(torch.utils.data.IterableDataset):
|
|||||||
self._requested_root,
|
self._requested_root,
|
||||||
self.revision,
|
self.revision,
|
||||||
force_cache_sync=force_cache_sync,
|
force_cache_sync=force_cache_sync,
|
||||||
|
repo_type=self.repo_type,
|
||||||
token=token,
|
token=token,
|
||||||
)
|
)
|
||||||
self.root = self.meta.root
|
self.root = self.meta.root
|
||||||
@@ -345,15 +358,26 @@ class StreamingLeRobotDataset(torch.utils.data.IterableDataset):
|
|||||||
self.delta_timestamps = delta_timestamps
|
self.delta_timestamps = delta_timestamps
|
||||||
self.delta_indices = get_delta_indices(self.delta_timestamps, self.fps)
|
self.delta_indices = get_delta_indices(self.delta_timestamps, self.fps)
|
||||||
|
|
||||||
token_kwargs = {} if token is None or self.streaming_from_local else {"token": token}
|
token_kwargs = {} if token is None else {"token": token}
|
||||||
self.hf_dataset: datasets.IterableDataset = load_dataset(
|
if self.repo_type == "bucket":
|
||||||
self.repo_id if not self.streaming_from_local else str(self.root),
|
self.hf_dataset: datasets.IterableDataset = load_dataset(
|
||||||
split="train",
|
"parquet",
|
||||||
streaming=self.streaming,
|
data_files=f"hf://buckets/{self.repo_id}/data/*/*.parquet",
|
||||||
data_files="data/*/*.parquet",
|
split="train",
|
||||||
revision=self.revision,
|
streaming=self.streaming,
|
||||||
**token_kwargs,
|
**token_kwargs,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
if self.streaming_from_local:
|
||||||
|
token_kwargs = {}
|
||||||
|
self.hf_dataset: datasets.IterableDataset = load_dataset(
|
||||||
|
self.repo_id if not self.streaming_from_local else str(self.root),
|
||||||
|
split="train",
|
||||||
|
streaming=self.streaming,
|
||||||
|
data_files="data/*/*.parquet",
|
||||||
|
revision=self.revision,
|
||||||
|
**token_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
self.num_shards = min(self.hf_dataset.num_shards, max_num_shards)
|
self.num_shards = min(self.hf_dataset.num_shards, max_num_shards)
|
||||||
|
|
||||||
@@ -422,10 +446,7 @@ class StreamingLeRobotDataset(torch.utils.data.IterableDataset):
|
|||||||
else:
|
else:
|
||||||
frames_buffer.append(frame)
|
frames_buffer.append(frame)
|
||||||
break # random shard sampled, switch shard
|
break # random shard sampled, switch shard
|
||||||
except (
|
except _ShardExhaustedError:
|
||||||
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
|
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
|
# Once shards are all exhausted, shuffle the buffer and yield the remaining frames
|
||||||
@@ -503,7 +524,11 @@ class StreamingLeRobotDataset(torch.utils.data.IterableDataset):
|
|||||||
|
|
||||||
def make_frame(self, dataset_iterator: Backtrackable) -> Generator:
|
def make_frame(self, dataset_iterator: Backtrackable) -> Generator:
|
||||||
"""Makes a frame starting from a dataset iterator"""
|
"""Makes a frame starting from a dataset iterator"""
|
||||||
item = next(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 = item_to_torch(item)
|
item = item_to_torch(item)
|
||||||
|
|
||||||
updates = [] # list of "updates" to apply to the item retrieved from hf_dataset (w/o camera features)
|
updates = [] # list of "updates" to apply to the item retrieved from hf_dataset (w/o camera features)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import numpy as np
|
|||||||
import packaging.version
|
import packaging.version
|
||||||
import torch
|
import torch
|
||||||
from huggingface_hub import DatasetCard, DatasetCardData, HfApi
|
from huggingface_hub import DatasetCard, DatasetCardData, HfApi
|
||||||
from huggingface_hub.errors import RevisionNotFoundError
|
|
||||||
|
|
||||||
from lerobot.utils.utils import flatten_dict, unflatten_dict
|
from lerobot.utils.utils import flatten_dict, unflatten_dict
|
||||||
|
|
||||||
@@ -51,6 +50,17 @@ The dataset you requested ({repo_id}) is only available in {version} format.
|
|||||||
As we cannot ensure forward compatibility with it, please update your current version of lerobot.
|
As we cannot ensure forward compatibility with it, please update your current version of lerobot.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
MISSING_VERSION_TAG_MESSAGE = """
|
||||||
|
Your dataset must be tagged with a codebase version.
|
||||||
|
Assuming _version_ is the codebase_version value in the info.json, you can run this:
|
||||||
|
```python
|
||||||
|
from huggingface_hub import HfApi
|
||||||
|
|
||||||
|
hub_api = HfApi()
|
||||||
|
hub_api.create_tag("{repo_id}", tag="_version_", repo_type="dataset")
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class CompatibilityError(Exception): ...
|
class CompatibilityError(Exception): ...
|
||||||
|
|
||||||
@@ -368,7 +378,7 @@ def get_safe_version(
|
|||||||
str: The safe version string (e.g., "v1.2.3") to use as a revision.
|
str: The safe version string (e.g., "v1.2.3") to use as a revision.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
RevisionNotFoundError: If the repo has no version tags.
|
RuntimeError: If the repo has no version tags.
|
||||||
BackwardCompatibilityError: If only older major versions are available.
|
BackwardCompatibilityError: If only older major versions are available.
|
||||||
ForwardCompatibilityError: If only newer major versions are available.
|
ForwardCompatibilityError: If only newer major versions are available.
|
||||||
"""
|
"""
|
||||||
@@ -378,17 +388,7 @@ def get_safe_version(
|
|||||||
hub_versions = get_repo_versions(repo_id) if token is None else get_repo_versions(repo_id, token=token)
|
hub_versions = get_repo_versions(repo_id) if token is None else get_repo_versions(repo_id, token=token)
|
||||||
|
|
||||||
if not hub_versions:
|
if not hub_versions:
|
||||||
raise RevisionNotFoundError(
|
raise RuntimeError(MISSING_VERSION_TAG_MESSAGE.format(repo_id=repo_id))
|
||||||
f"""Your dataset must be tagged with a codebase version.
|
|
||||||
Assuming _version_ is the codebase_version value in the info.json, you can run this:
|
|
||||||
```python
|
|
||||||
from huggingface_hub import HfApi
|
|
||||||
|
|
||||||
hub_api = HfApi()
|
|
||||||
hub_api.create_tag("{repo_id}", tag="_version_", repo_type="dataset")
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
if target_version in hub_versions:
|
if target_version in hub_versions:
|
||||||
return f"v{target_version}"
|
return f"v{target_version}"
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ class MetaworldEnv(EnvConfig):
|
|||||||
class RoboCasaEnv(EnvConfig):
|
class RoboCasaEnv(EnvConfig):
|
||||||
task: str = "CloseFridge"
|
task: str = "CloseFridge"
|
||||||
fps: int = 20
|
fps: int = 20
|
||||||
episode_length: int = 1000
|
episode_length: int | None = None
|
||||||
obs_type: str = "pixels_agent_pos"
|
obs_type: str = "pixels_agent_pos"
|
||||||
render_mode: str = "rgb_array"
|
render_mode: str = "rgb_array"
|
||||||
camera_name: str = "robot0_agentview_left,robot0_eye_in_hand,robot0_agentview_right"
|
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 import benchmark, get_libero_path
|
||||||
from libero.libero.envs import OffScreenRenderEnv
|
from libero.libero.envs import OffScreenRenderEnv
|
||||||
|
|
||||||
from lerobot.types import RobotObservation
|
from lerobot.lerobot_types import RobotObservation
|
||||||
|
|
||||||
from .utils import _LazyAsyncVectorEnv, parse_camera_names
|
from .utils import _LazyAsyncVectorEnv, parse_camera_names
|
||||||
|
|
||||||
@@ -384,7 +384,12 @@ class LiberoEnv(gym.Env):
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self._env is not None:
|
if self._env is not None:
|
||||||
self._env.close()
|
try:
|
||||||
|
self._env.close()
|
||||||
|
finally:
|
||||||
|
# LIBERO deletes its inner env on close, so this wrapper must
|
||||||
|
# be recreated before the next reset.
|
||||||
|
self._env = None
|
||||||
|
|
||||||
|
|
||||||
def _make_env_fns(
|
def _make_env_fns(
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import metaworld.policies as policies
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from gymnasium import spaces
|
from gymnasium import spaces
|
||||||
|
|
||||||
from lerobot.types import RobotObservation
|
from lerobot.lerobot_types import RobotObservation
|
||||||
|
|
||||||
from .utils import _LazyAsyncVectorEnv
|
from .utils import _LazyAsyncVectorEnv
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import gymnasium as gym
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from gymnasium import spaces
|
from gymnasium import spaces
|
||||||
|
|
||||||
from lerobot.types import RobotObservation
|
from lerobot.lerobot_types import RobotObservation
|
||||||
|
|
||||||
from .utils import _LazyAsyncVectorEnv, parse_camera_names
|
from .utils import _LazyAsyncVectorEnv, parse_camera_names
|
||||||
|
|
||||||
@@ -98,6 +98,19 @@ def _resolve_tasks(task: str) -> tuple[list[str], str | None]:
|
|||||||
return names, 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]:
|
def convert_action(flat_action: np.ndarray) -> dict[str, Any]:
|
||||||
"""Split a flat (12,) action vector into a RoboCasa action dict.
|
"""Split a flat (12,) action vector into a RoboCasa action dict.
|
||||||
|
|
||||||
@@ -154,7 +167,7 @@ class RoboCasaEnv(gym.Env):
|
|||||||
|
|
||||||
self.camera_name = parse_camera_names(camera_name)
|
self.camera_name = parse_camera_names(camera_name)
|
||||||
|
|
||||||
self._max_episode_steps = episode_length if episode_length is not None else 1000
|
self._max_episode_steps = episode_length if episode_length is not None else _get_task_horizon(task)
|
||||||
|
|
||||||
# Deferred — created on first reset() inside the worker subprocess
|
# Deferred — created on first reset() inside the worker subprocess
|
||||||
# to avoid inheriting stale GPU/EGL contexts across fork().
|
# to avoid inheriting stale GPU/EGL contexts across fork().
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import numpy as np
|
|||||||
import torch
|
import torch
|
||||||
from gymnasium import spaces
|
from gymnasium import spaces
|
||||||
|
|
||||||
from lerobot.types import RobotObservation
|
from lerobot.lerobot_types import RobotObservation
|
||||||
from lerobot.utils.import_utils import _scipy_available
|
from lerobot.utils.import_utils import _scipy_available
|
||||||
|
|
||||||
from .utils import _LazyAsyncVectorEnv
|
from .utils import _LazyAsyncVectorEnv
|
||||||
@@ -384,7 +384,9 @@ class RoboTwinEnv(gym.Env):
|
|||||||
|
|
||||||
self._env: Any | None = None # deferred — created on first reset() inside worker
|
self._env: Any | None = None # deferred — created on first reset() inside worker
|
||||||
self._step_count: int = 0
|
self._step_count: int = 0
|
||||||
self._black_frame = np.zeros((self.observation_height, self.observation_width, 3), dtype=np.uint8)
|
self._black_frame: np.ndarray = np.zeros(
|
||||||
|
(self.observation_height, self.observation_width, 3), dtype=np.uint8
|
||||||
|
)
|
||||||
|
|
||||||
image_spaces = {
|
image_spaces = {
|
||||||
cam: spaces.Box(
|
cam: spaces.Box(
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import numpy as np
|
|||||||
from gymnasium import spaces
|
from gymnasium import spaces
|
||||||
from scipy.spatial.transform import Rotation
|
from scipy.spatial.transform import Rotation
|
||||||
|
|
||||||
from lerobot.types import RobotObservation
|
from lerobot.lerobot_types import RobotObservation
|
||||||
|
|
||||||
from .utils import _LazyAsyncVectorEnv
|
from .utils import _LazyAsyncVectorEnv
|
||||||
|
|
||||||
@@ -373,7 +373,7 @@ class VLABenchEnv(gym.Env):
|
|||||||
|
|
||||||
if action.shape[0] != 7:
|
if action.shape[0] != 7:
|
||||||
# Unknown layout — fall back to zero-pad so the sim doesn't crash.
|
# Unknown layout — fall back to zero-pad so the sim doesn't crash.
|
||||||
padded = np.zeros(ctrl_dim, dtype=np.float64)
|
padded: np.ndarray = np.zeros(ctrl_dim, dtype=np.float64)
|
||||||
padded[: min(action.shape[0], ctrl_dim)] = action[:ctrl_dim]
|
padded[: min(action.shape[0], ctrl_dim)] = action[:ctrl_dim]
|
||||||
return padded
|
return padded
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ MODEL_ENCODING_TABLE = {
|
|||||||
"xm430-w350": X_SERIES_ENCODINGS_TABLE,
|
"xm430-w350": X_SERIES_ENCODINGS_TABLE,
|
||||||
"xm540-w270": X_SERIES_ENCODINGS_TABLE,
|
"xm540-w270": X_SERIES_ENCODINGS_TABLE,
|
||||||
"xc430-w150": X_SERIES_ENCODINGS_TABLE,
|
"xc430-w150": X_SERIES_ENCODINGS_TABLE,
|
||||||
|
"xh540-w150": X_SERIES_ENCODINGS_TABLE,
|
||||||
|
"xc330-t288": X_SERIES_ENCODINGS_TABLE,
|
||||||
|
"xc330-t181": X_SERIES_ENCODINGS_TABLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
# {model: model_resolution}
|
# {model: model_resolution}
|
||||||
@@ -134,6 +137,9 @@ MODEL_RESOLUTION = {
|
|||||||
"xm430-w350": 4096,
|
"xm430-w350": 4096,
|
||||||
"xm540-w270": 4096,
|
"xm540-w270": 4096,
|
||||||
"xc430-w150": 4096,
|
"xc430-w150": 4096,
|
||||||
|
"xh540-w150": 4096,
|
||||||
|
"xc330-t288": 4096,
|
||||||
|
"xc330-t181": 4096,
|
||||||
}
|
}
|
||||||
|
|
||||||
# {model: model_number}
|
# {model: model_number}
|
||||||
@@ -145,6 +151,9 @@ MODEL_NUMBER_TABLE = {
|
|||||||
"xm430-w350": 1020,
|
"xm430-w350": 1020,
|
||||||
"xm540-w270": 1120,
|
"xm540-w270": 1120,
|
||||||
"xc430-w150": 1070,
|
"xc430-w150": 1070,
|
||||||
|
"xh540-w150": 1110,
|
||||||
|
"xc330-t288": 1220,
|
||||||
|
"xc330-t181": 1210,
|
||||||
}
|
}
|
||||||
|
|
||||||
# {model: available_operating_modes}
|
# {model: available_operating_modes}
|
||||||
@@ -156,6 +165,9 @@ MODEL_OPERATING_MODES = {
|
|||||||
"xm430-w350": [0, 1, 3, 4, 5, 16],
|
"xm430-w350": [0, 1, 3, 4, 5, 16],
|
||||||
"xm540-w270": [0, 1, 3, 4, 5, 16],
|
"xm540-w270": [0, 1, 3, 4, 5, 16],
|
||||||
"xc430-w150": [1, 3, 4, 16],
|
"xc430-w150": [1, 3, 4, 16],
|
||||||
|
"xh540-w150": [0, 1, 3, 4, 5, 16],
|
||||||
|
"xc330-t288": [0, 1, 3, 4, 5, 16],
|
||||||
|
"xc330-t181": [0, 1, 3, 4, 5, 16],
|
||||||
}
|
}
|
||||||
|
|
||||||
MODEL_CONTROL_TABLE = {
|
MODEL_CONTROL_TABLE = {
|
||||||
@@ -166,6 +178,9 @@ MODEL_CONTROL_TABLE = {
|
|||||||
"xm430-w350": X_SERIES_CONTROL_TABLE,
|
"xm430-w350": X_SERIES_CONTROL_TABLE,
|
||||||
"xm540-w270": X_SERIES_CONTROL_TABLE,
|
"xm540-w270": X_SERIES_CONTROL_TABLE,
|
||||||
"xc430-w150": X_SERIES_CONTROL_TABLE,
|
"xc430-w150": X_SERIES_CONTROL_TABLE,
|
||||||
|
"xh540-w150": X_SERIES_CONTROL_TABLE,
|
||||||
|
"xc330-t288": X_SERIES_CONTROL_TABLE,
|
||||||
|
"xc330-t181": X_SERIES_CONTROL_TABLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
MODEL_BAUDRATE_TABLE = {
|
MODEL_BAUDRATE_TABLE = {
|
||||||
@@ -176,6 +191,9 @@ MODEL_BAUDRATE_TABLE = {
|
|||||||
"xm430-w350": X_SERIES_BAUDRATE_TABLE,
|
"xm430-w350": X_SERIES_BAUDRATE_TABLE,
|
||||||
"xm540-w270": X_SERIES_BAUDRATE_TABLE,
|
"xm540-w270": X_SERIES_BAUDRATE_TABLE,
|
||||||
"xc430-w150": X_SERIES_BAUDRATE_TABLE,
|
"xc430-w150": X_SERIES_BAUDRATE_TABLE,
|
||||||
|
"xh540-w150": X_SERIES_BAUDRATE_TABLE,
|
||||||
|
"xc330-t288": X_SERIES_BAUDRATE_TABLE,
|
||||||
|
"xc330-t181": X_SERIES_BAUDRATE_TABLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
AVAILABLE_BAUDRATES = [
|
AVAILABLE_BAUDRATES = [
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Any
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from lerobot.configs.types import FeatureType, PipelineFeatureType, PolicyFeature
|
from lerobot.configs.types import FeatureType, PipelineFeatureType, PolicyFeature
|
||||||
|
from lerobot.lerobot_types import TransitionKey
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
ComplementaryDataProcessorStep,
|
ComplementaryDataProcessorStep,
|
||||||
PolicyAction,
|
PolicyAction,
|
||||||
@@ -31,7 +32,6 @@ from lerobot.processor import (
|
|||||||
make_default_policy_processor_steps,
|
make_default_policy_processor_steps,
|
||||||
make_policy_processor_pipelines,
|
make_policy_processor_pipelines,
|
||||||
)
|
)
|
||||||
from lerobot.types import TransitionKey
|
|
||||||
from lerobot.utils.constants import OBS_STATE
|
from lerobot.utils.constants import OBS_STATE
|
||||||
from lerobot.utils.import_utils import _transformers_available, require_package
|
from lerobot.utils.import_utils import _transformers_available, require_package
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ class Evo1Policy(PreTrainedPolicy):
|
|||||||
config_class = Evo1Config
|
config_class = Evo1Config
|
||||||
name = "evo1"
|
name = "evo1"
|
||||||
|
|
||||||
|
def supports_rtc(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
def __init__(self, config: Evo1Config, *, vlm_hub_kwargs: dict | None = None, **kwargs):
|
def __init__(self, config: Evo1Config, *, vlm_hub_kwargs: dict | None = None, **kwargs):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
config.validate_features()
|
config.validate_features()
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from typing import Any
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from lerobot.configs import FeatureType, PipelineFeatureType, PolicyFeature
|
from lerobot.configs import FeatureType, PipelineFeatureType, PolicyFeature
|
||||||
|
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
AddBatchDimensionProcessorStep,
|
AddBatchDimensionProcessorStep,
|
||||||
DeviceProcessorStep,
|
DeviceProcessorStep,
|
||||||
@@ -40,7 +41,6 @@ from lerobot.processor.converters import (
|
|||||||
policy_action_to_transition,
|
policy_action_to_transition,
|
||||||
transition_to_policy_action,
|
transition_to_policy_action,
|
||||||
)
|
)
|
||||||
from lerobot.types import EnvTransition, TransitionKey
|
|
||||||
from lerobot.utils.constants import (
|
from lerobot.utils.constants import (
|
||||||
ACTION,
|
ACTION,
|
||||||
DONE,
|
DONE,
|
||||||
@@ -302,6 +302,33 @@ def _pad_evo1_stats(
|
|||||||
return padded_stats
|
return padded_stats
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_evo1_normalization_steps(
|
||||||
|
config: Evo1Config,
|
||||||
|
preprocessor: PolicyProcessorPipeline,
|
||||||
|
postprocessor: PolicyProcessorPipeline,
|
||||||
|
) -> None:
|
||||||
|
"""Re-pad checkpoint-loaded (un)normalizer stats/features to EVO1's fixed widths.
|
||||||
|
|
||||||
|
Loading a checkpoint injects the raw dataset stats (unpadded to max_state_dim/max_action_dim)
|
||||||
|
into the (un)normalizer via the generic override path in make_pre_post_processors. Those stats
|
||||||
|
and their declared features must be re-padded/reshaped to EVO1's fixed widths, otherwise
|
||||||
|
normalization fails against the padded state/action tensors (e.g. state padded to 24 vs. 8-dim
|
||||||
|
LIBERO stats). Padding is a no-op when stats are already at the target width.
|
||||||
|
"""
|
||||||
|
normalization_features = _evo1_normalization_features(config)
|
||||||
|
action_features = _evo1_action_features(config)
|
||||||
|
for step in preprocessor.steps:
|
||||||
|
if isinstance(step, NormalizerProcessorStep):
|
||||||
|
step.features = normalization_features
|
||||||
|
step.stats = _pad_evo1_stats(config, step.stats)
|
||||||
|
step.to(device=step.device, dtype=step.dtype)
|
||||||
|
for step in postprocessor.steps:
|
||||||
|
if isinstance(step, UnnormalizerProcessorStep):
|
||||||
|
step.features = action_features
|
||||||
|
step.stats = _pad_evo1_stats(config, step.stats)
|
||||||
|
step.to(device=step.device, dtype=step.dtype)
|
||||||
|
|
||||||
|
|
||||||
def reconcile_evo1_processors(
|
def reconcile_evo1_processors(
|
||||||
config: Evo1Config,
|
config: Evo1Config,
|
||||||
preprocessor: PolicyProcessorPipeline,
|
preprocessor: PolicyProcessorPipeline,
|
||||||
@@ -309,16 +336,19 @@ def reconcile_evo1_processors(
|
|||||||
) -> tuple[PolicyProcessorPipeline, PolicyProcessorPipeline]:
|
) -> tuple[PolicyProcessorPipeline, PolicyProcessorPipeline]:
|
||||||
"""Reconcile checkpoint-loaded pipelines with the current EVO1 config.
|
"""Reconcile checkpoint-loaded pipelines with the current EVO1 config.
|
||||||
|
|
||||||
Two things cannot be restored from a serialized pipeline alone: the EVO1 batch converter
|
Three things cannot be restored from a serialized pipeline alone: the EVO1 batch converter
|
||||||
(converters are plain functions and are never serialized), and eval-time CLI overrides of the
|
(converters are plain functions and are never serialized), eval-time CLI overrides of the
|
||||||
action postprocessing flags (`postprocess_action_dim`, `binarize_gripper`, `gripper_*`). This
|
action postprocessing flags (`postprocess_action_dim`, `binarize_gripper`, `gripper_*`), and the
|
||||||
restores the converter and rebuilds the action step from the current config so those overrides
|
(un)normalizer stats/features when the generic override path injects raw, unpadded dataset
|
||||||
take effect.
|
stats. This restores the converter, re-pads the normalization stats to EVO1's fixed widths, and
|
||||||
|
rebuilds the action step from the current config so those overrides take effect.
|
||||||
"""
|
"""
|
||||||
# Pipelines reloaded from a checkpoint come back with the default batch converter, which drops
|
# Pipelines reloaded from a checkpoint come back with the default batch converter, which drops
|
||||||
# non-observation extras (embodiment_id, state_mask, custom task fields) needed by EVO1.
|
# non-observation extras (embodiment_id, state_mask, custom task fields) needed by EVO1.
|
||||||
preprocessor.to_transition = evo1_batch_to_transition
|
preprocessor.to_transition = evo1_batch_to_transition
|
||||||
|
|
||||||
|
_refresh_evo1_normalization_steps(config, preprocessor, postprocessor)
|
||||||
|
|
||||||
action_step = Evo1ActionProcessorStep(
|
action_step = Evo1ActionProcessorStep(
|
||||||
action_dim=_evo1_action_dim(config),
|
action_dim=_evo1_action_dim(config),
|
||||||
binarize_gripper=config.binarize_gripper,
|
binarize_gripper=config.binarize_gripper,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
from lerobot.configs import FeatureType, PreTrainedConfig
|
from lerobot.configs import FeatureType, PreTrainedConfig
|
||||||
from lerobot.envs import EnvConfig, env_to_policy_features
|
from lerobot.envs import EnvConfig, env_to_policy_features
|
||||||
|
from lerobot.lerobot_types import PolicyAction
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
AbsoluteActionsProcessorStep,
|
AbsoluteActionsProcessorStep,
|
||||||
PolicyProcessorPipeline,
|
PolicyProcessorPipeline,
|
||||||
@@ -37,19 +38,25 @@ from lerobot.processor import (
|
|||||||
transition_to_batch,
|
transition_to_batch,
|
||||||
transition_to_policy_action,
|
transition_to_policy_action,
|
||||||
)
|
)
|
||||||
from lerobot.types import PolicyAction
|
|
||||||
from lerobot.utils.constants import (
|
from lerobot.utils.constants import (
|
||||||
ACTION,
|
ACTION,
|
||||||
POLICY_POSTPROCESSOR_DEFAULT_NAME,
|
POLICY_POSTPROCESSOR_DEFAULT_NAME,
|
||||||
POLICY_PREPROCESSOR_DEFAULT_NAME,
|
POLICY_PREPROCESSOR_DEFAULT_NAME,
|
||||||
)
|
)
|
||||||
from lerobot.utils.feature_utils import dataset_to_policy_features
|
from lerobot.utils.feature_utils import dataset_to_policy_features
|
||||||
|
from lerobot.utils.import_utils import _peft_available, require_package
|
||||||
|
|
||||||
from .evo1.configuration_evo1 import Evo1Config
|
from .evo1.configuration_evo1 import Evo1Config
|
||||||
from .groot.configuration_groot import GrootConfig
|
from .groot.configuration_groot import GrootConfig
|
||||||
from .pretrained import PreTrainedPolicy
|
from .pretrained import PreTrainedPolicy
|
||||||
from .utils import validate_visual_features_consistency
|
from .utils import validate_visual_features_consistency
|
||||||
|
|
||||||
|
if TYPE_CHECKING or _peft_available:
|
||||||
|
from peft import PeftConfig, PeftModel
|
||||||
|
else:
|
||||||
|
PeftConfig = None
|
||||||
|
PeftModel = None
|
||||||
|
|
||||||
|
|
||||||
def _reconnect_relative_absolute_steps(
|
def _reconnect_relative_absolute_steps(
|
||||||
preprocessor: PolicyProcessorPipeline, postprocessor: PolicyProcessorPipeline
|
preprocessor: PolicyProcessorPipeline, postprocessor: PolicyProcessorPipeline
|
||||||
@@ -334,12 +341,15 @@ def make_policy(
|
|||||||
# Load a pretrained PEFT model on top of the policy. The pretrained path points to the folder/repo
|
# Load a pretrained PEFT model on top of the policy. The pretrained path points to the folder/repo
|
||||||
# of the adapter and the adapter's config contains the path to the base policy. So we need the
|
# of the adapter and the adapter's config contains the path to the base policy. So we need the
|
||||||
# adapter config first, then load the correct policy and then apply PEFT.
|
# adapter config first, then load the correct policy and then apply PEFT.
|
||||||
from peft import PeftConfig, PeftModel
|
require_package("peft", extra="peft")
|
||||||
|
|
||||||
logging.info("Loading policy's PEFT adapter.")
|
logging.info("Loading policy's PEFT adapter.")
|
||||||
|
|
||||||
peft_pretrained_path = str(cfg.pretrained_path)
|
peft_pretrained_path = str(cfg.pretrained_path)
|
||||||
peft_config = PeftConfig.from_pretrained(peft_pretrained_path)
|
peft_config = PeftConfig.from_pretrained(
|
||||||
|
peft_pretrained_path,
|
||||||
|
revision=cfg.pretrained_revision,
|
||||||
|
)
|
||||||
|
|
||||||
kwargs["pretrained_name_or_path"] = peft_config.base_model_name_or_path
|
kwargs["pretrained_name_or_path"] = peft_config.base_model_name_or_path
|
||||||
if not kwargs["pretrained_name_or_path"]:
|
if not kwargs["pretrained_name_or_path"]:
|
||||||
@@ -350,9 +360,14 @@ def make_policy(
|
|||||||
"the adapter was trained."
|
"the adapter was trained."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
kwargs["revision"] = peft_config.revision
|
||||||
policy = policy_cls.from_pretrained(**kwargs)
|
policy = policy_cls.from_pretrained(**kwargs)
|
||||||
policy = PeftModel.from_pretrained(
|
policy = PeftModel.from_pretrained(
|
||||||
policy, peft_pretrained_path, config=peft_config, is_trainable=True
|
policy,
|
||||||
|
peft_pretrained_path,
|
||||||
|
config=peft_config,
|
||||||
|
revision=cfg.pretrained_revision,
|
||||||
|
is_trainable=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ class GrootPolicy(PreTrainedPolicy):
|
|||||||
name = "groot"
|
name = "groot"
|
||||||
config_class = GrootConfig
|
config_class = GrootConfig
|
||||||
|
|
||||||
|
def supports_rtc(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
def __init__(self, config: GrootConfig, **kwargs):
|
def __init__(self, config: GrootConfig, **kwargs):
|
||||||
"""Initialize Groot policy wrapper."""
|
"""Initialize Groot policy wrapper."""
|
||||||
require_package("transformers", extra="groot")
|
require_package("transformers", extra="groot")
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ if TYPE_CHECKING or _datasets_available:
|
|||||||
else:
|
else:
|
||||||
LeRobotDataset = None
|
LeRobotDataset = None
|
||||||
|
|
||||||
|
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
AbsoluteActionsProcessorStep,
|
AbsoluteActionsProcessorStep,
|
||||||
AddBatchDimensionProcessorStep,
|
AddBatchDimensionProcessorStep,
|
||||||
@@ -66,7 +67,6 @@ from lerobot.processor import (
|
|||||||
transition_to_batch,
|
transition_to_batch,
|
||||||
transition_to_policy_action,
|
transition_to_policy_action,
|
||||||
)
|
)
|
||||||
from lerobot.types import EnvTransition, TransitionKey
|
|
||||||
from lerobot.utils.constants import (
|
from lerobot.utils.constants import (
|
||||||
ACTION,
|
ACTION,
|
||||||
OBS_IMAGE,
|
OBS_IMAGE,
|
||||||
|
|||||||
@@ -43,11 +43,22 @@ from torch.distributions import Beta
|
|||||||
|
|
||||||
from lerobot.policies.pretrained import PreTrainedPolicy
|
from lerobot.policies.pretrained import PreTrainedPolicy
|
||||||
from lerobot.utils.constants import ACTION
|
from lerobot.utils.constants import ACTION
|
||||||
from lerobot.utils.import_utils import _scipy_available, _transformers_available, require_package
|
from lerobot.utils.import_utils import (
|
||||||
|
_peft_available,
|
||||||
|
_scipy_available,
|
||||||
|
_transformers_available,
|
||||||
|
require_package,
|
||||||
|
)
|
||||||
|
|
||||||
from ..rtc.modeling_rtc import RTCProcessor
|
from ..rtc.modeling_rtc import RTCProcessor
|
||||||
from .configuration_molmoact2 import MolmoAct2Config
|
from .configuration_molmoact2 import MolmoAct2Config
|
||||||
|
|
||||||
|
if TYPE_CHECKING or _peft_available:
|
||||||
|
from peft import LoraConfig, get_peft_model
|
||||||
|
else:
|
||||||
|
LoraConfig = None
|
||||||
|
get_peft_model = None
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -509,6 +520,9 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
|||||||
config_class = MolmoAct2Config
|
config_class = MolmoAct2Config
|
||||||
name = "molmoact2"
|
name = "molmoact2"
|
||||||
|
|
||||||
|
def supports_rtc(self) -> bool:
|
||||||
|
return self.config.inference_action_mode == "continuous"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: MolmoAct2Config,
|
config: MolmoAct2Config,
|
||||||
@@ -1731,13 +1745,11 @@ class MolmoAct2Policy(PreTrainedPolicy):
|
|||||||
|
|
||||||
def _build_inner_lora_config(self):
|
def _build_inner_lora_config(self):
|
||||||
require_package("peft", extra="molmoact2")
|
require_package("peft", extra="molmoact2")
|
||||||
from peft import LoraConfig
|
|
||||||
|
|
||||||
return LoraConfig(**self._get_inner_peft_targets())
|
return LoraConfig(**self._get_inner_peft_targets())
|
||||||
|
|
||||||
def _apply_lora_adapters(self) -> None:
|
def _apply_lora_adapters(self) -> None:
|
||||||
require_package("peft", extra="molmoact2")
|
require_package("peft", extra="molmoact2")
|
||||||
from peft import get_peft_model
|
|
||||||
|
|
||||||
peft_config = self._build_inner_lora_config()
|
peft_config = self._build_inner_lora_config()
|
||||||
self._validate_peft_config(peft_config)
|
self._validate_peft_config(peft_config)
|
||||||
|
|||||||
@@ -1801,7 +1801,7 @@ class MolmoAct2VisionBackbone(nn.Module):
|
|||||||
valid = pooled_patches_idx >= 0
|
valid = pooled_patches_idx >= 0
|
||||||
valid_token = torch.any(valid, -1)
|
valid_token = torch.any(valid, -1)
|
||||||
|
|
||||||
# Use `pooled_patches_idx` to arange the features for image pooling
|
# Use `pooled_patches_idx` to arrange the features for image pooling
|
||||||
batch_idx = torch.arange(
|
batch_idx = torch.arange(
|
||||||
pooled_patches_idx.shape[0],
|
pooled_patches_idx.shape[0],
|
||||||
dtype=torch.long,
|
dtype=torch.long,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import torch
|
|||||||
from torch import Tensor
|
from torch import Tensor
|
||||||
|
|
||||||
from lerobot.configs import FeatureType, PipelineFeatureType, PolicyFeature
|
from lerobot.configs import FeatureType, PipelineFeatureType, PolicyFeature
|
||||||
|
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
AddBatchDimensionProcessorStep,
|
AddBatchDimensionProcessorStep,
|
||||||
DeviceProcessorStep,
|
DeviceProcessorStep,
|
||||||
@@ -49,7 +50,6 @@ from lerobot.processor import (
|
|||||||
policy_action_to_transition,
|
policy_action_to_transition,
|
||||||
transition_to_policy_action,
|
transition_to_policy_action,
|
||||||
)
|
)
|
||||||
from lerobot.types import EnvTransition, TransitionKey
|
|
||||||
from lerobot.utils.constants import (
|
from lerobot.utils.constants import (
|
||||||
ACTION,
|
ACTION,
|
||||||
OBS_IMAGES,
|
OBS_IMAGES,
|
||||||
|
|||||||
@@ -749,6 +749,9 @@ class PI0Policy(PreTrainedPolicy):
|
|||||||
config_class = PI0Config
|
config_class = PI0Config
|
||||||
name = "pi0"
|
name = "pi0"
|
||||||
|
|
||||||
|
def supports_rtc(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: PI0Config,
|
config: PI0Config,
|
||||||
|
|||||||
@@ -714,6 +714,9 @@ class PI05Policy(PreTrainedPolicy):
|
|||||||
config_class = PI05Config
|
config_class = PI05Config
|
||||||
name = "pi05"
|
name = "pi05"
|
||||||
|
|
||||||
|
def supports_rtc(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: PI05Config,
|
config: PI05Config,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import numpy as np
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||||
|
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
AbsoluteActionsProcessorStep,
|
AbsoluteActionsProcessorStep,
|
||||||
PolicyAction,
|
PolicyAction,
|
||||||
@@ -33,7 +34,6 @@ from lerobot.processor import (
|
|||||||
make_default_policy_processor_steps,
|
make_default_policy_processor_steps,
|
||||||
make_policy_processor_pipelines,
|
make_policy_processor_pipelines,
|
||||||
)
|
)
|
||||||
from lerobot.types import EnvTransition, TransitionKey
|
|
||||||
from lerobot.utils.constants import OBS_STATE
|
from lerobot.utils.constants import OBS_STATE
|
||||||
|
|
||||||
from .configuration_pi05 import PI05Config
|
from .configuration_pi05 import PI05Config
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import numpy as np
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
from lerobot.configs import PipelineFeatureType, PolicyFeature
|
||||||
|
from lerobot.lerobot_types import EnvTransition, TransitionKey
|
||||||
from lerobot.processor import (
|
from lerobot.processor import (
|
||||||
AbsoluteActionsProcessorStep,
|
AbsoluteActionsProcessorStep,
|
||||||
ActionTokenizerProcessorStep,
|
ActionTokenizerProcessorStep,
|
||||||
@@ -34,7 +35,6 @@ from lerobot.processor import (
|
|||||||
make_default_policy_processor_steps,
|
make_default_policy_processor_steps,
|
||||||
make_policy_processor_pipelines,
|
make_policy_processor_pipelines,
|
||||||
)
|
)
|
||||||
from lerobot.types import EnvTransition, TransitionKey
|
|
||||||
from lerobot.utils.constants import OBS_STATE
|
from lerobot.utils.constants import OBS_STATE
|
||||||
|
|
||||||
from .configuration_pi0_fast import PI0FastConfig
|
from .configuration_pi0_fast import PI0FastConfig
|
||||||
|
|||||||
@@ -34,14 +34,22 @@ from lerobot.configs import PreTrainedConfig
|
|||||||
from lerobot.configs.train import TrainPipelineConfig
|
from lerobot.configs.train import TrainPipelineConfig
|
||||||
from lerobot.utils.device_utils import resolve_safetensors_device
|
from lerobot.utils.device_utils import resolve_safetensors_device
|
||||||
from lerobot.utils.hub import HubMixin
|
from lerobot.utils.hub import HubMixin
|
||||||
|
from lerobot.utils.import_utils import _peft_available, require_package
|
||||||
|
|
||||||
from .utils import log_model_loading_keys
|
from .utils import log_model_loading_keys
|
||||||
|
|
||||||
T = TypeVar("T", bound="PreTrainedPolicy")
|
if TYPE_CHECKING or _peft_available:
|
||||||
|
from peft import PEFT_TYPE_TO_CONFIG_MAPPING, PeftType, get_peft_model
|
||||||
|
else:
|
||||||
|
PEFT_TYPE_TO_CONFIG_MAPPING = None
|
||||||
|
PeftType = None
|
||||||
|
get_peft_model = None
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from lerobot.datasets.dataset_metadata import LeRobotDatasetMetadata
|
from lerobot.datasets.dataset_metadata import LeRobotDatasetMetadata
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="PreTrainedPolicy")
|
||||||
|
|
||||||
|
|
||||||
def _build_card_context(
|
def _build_card_context(
|
||||||
cfg: TrainPipelineConfig | None,
|
cfg: TrainPipelineConfig | None,
|
||||||
@@ -241,6 +249,10 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
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'?
|
# TODO(aliberts, rcadene): split into 'forward' and 'compute_loss'?
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict | None]:
|
def forward(self, batch: dict[str, Tensor]) -> tuple[Tensor, dict | None]:
|
||||||
@@ -384,7 +396,7 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
|||||||
peft_cli_overrides: Optional dict of CLI overrides (method_type, target_modules, r, etc.)
|
peft_cli_overrides: Optional dict of CLI overrides (method_type, target_modules, r, etc.)
|
||||||
These are merged with policy defaults to build the final config.
|
These are merged with policy defaults to build the final config.
|
||||||
"""
|
"""
|
||||||
from peft import get_peft_model
|
require_package("peft", extra="peft")
|
||||||
|
|
||||||
# If user provided a complete config, use it directly (with overrides)
|
# If user provided a complete config, use it directly (with overrides)
|
||||||
if peft_config is not None:
|
if peft_config is not None:
|
||||||
@@ -455,7 +467,7 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
|||||||
Returns:
|
Returns:
|
||||||
Preprocessed dict with renamed keys and init_type mapped to method-specific key.
|
Preprocessed dict with renamed keys and init_type mapped to method-specific key.
|
||||||
"""
|
"""
|
||||||
from peft import PeftType
|
require_package("peft", extra="peft")
|
||||||
|
|
||||||
cli_overrides = cli_overrides.copy()
|
cli_overrides = cli_overrides.copy()
|
||||||
|
|
||||||
@@ -480,7 +492,7 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
|||||||
|
|
||||||
def _build_peft_config(self, cli_overrides: dict):
|
def _build_peft_config(self, cli_overrides: dict):
|
||||||
"""Build a PEFT config from policy defaults and CLI overrides."""
|
"""Build a PEFT config from policy defaults and CLI overrides."""
|
||||||
from peft import PEFT_TYPE_TO_CONFIG_MAPPING, PeftType
|
require_package("peft", extra="peft")
|
||||||
|
|
||||||
# Determine PEFT method type (default to LORA)
|
# Determine PEFT method type (default to LORA)
|
||||||
method_type_str = cli_overrides.get("method_type") or "lora"
|
method_type_str = cli_overrides.get("method_type") or "lora"
|
||||||
@@ -507,7 +519,7 @@ class PreTrainedPolicy(nn.Module, HubMixin, abc.ABC):
|
|||||||
|
|
||||||
def _apply_peft_cli_overrides(self, peft_config, cli_overrides: dict):
|
def _apply_peft_cli_overrides(self, peft_config, cli_overrides: dict):
|
||||||
"""Apply CLI overrides to an existing PEFT config."""
|
"""Apply CLI overrides to an existing PEFT config."""
|
||||||
from peft import PEFT_TYPE_TO_CONFIG_MAPPING, PeftType
|
require_package("peft", extra="peft")
|
||||||
|
|
||||||
# Get method type from existing config or CLI override
|
# Get method type from existing config or CLI override
|
||||||
method_type_str = cli_overrides.get("method_type")
|
method_type_str = cli_overrides.get("method_type")
|
||||||
|
|||||||
@@ -145,6 +145,9 @@ class SmolVLAPolicy(PreTrainedPolicy):
|
|||||||
config_class = SmolVLAConfig
|
config_class = SmolVLAConfig
|
||||||
name = "smolvla"
|
name = "smolvla"
|
||||||
|
|
||||||
|
def supports_rtc(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: SmolVLAConfig,
|
config: SmolVLAConfig,
|
||||||
|
|||||||
@@ -168,14 +168,23 @@ class SmolVLMWithExpertModel(nn.Module):
|
|||||||
last_layers.append(self.num_vlm_layers - 2)
|
last_layers.append(self.num_vlm_layers - 2)
|
||||||
frozen_layers = [
|
frozen_layers = [
|
||||||
"lm_head",
|
"lm_head",
|
||||||
"text_model.model.norm.weight",
|
"text_model.norm.weight",
|
||||||
]
|
]
|
||||||
for layer in last_layers:
|
for layer in last_layers:
|
||||||
frozen_layers.append(f"text_model.model.layers.{layer}.")
|
frozen_layers.append(f"text_model.layers.{layer}.")
|
||||||
|
|
||||||
|
unmatched_patterns = set(frozen_layers)
|
||||||
for name, params in self.vlm.named_parameters():
|
for name, params in self.vlm.named_parameters():
|
||||||
if any(k in name for k in frozen_layers):
|
matched_patterns = [k for k in frozen_layers if k in name]
|
||||||
|
if matched_patterns:
|
||||||
params.requires_grad = False
|
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
|
# To avoid unused params issue with distributed training
|
||||||
for name, params in self.lm_expert.named_parameters():
|
for name, params in self.lm_expert.named_parameters():
|
||||||
if "lm_head" in name:
|
if "lm_head" in name:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import torch
|
|||||||
from torch import nn
|
from torch import nn
|
||||||
|
|
||||||
from lerobot.configs import FeatureType, PolicyFeature, PreTrainedConfig
|
from lerobot.configs import FeatureType, PolicyFeature, PreTrainedConfig
|
||||||
from lerobot.types import PolicyAction, RobotAction, RobotObservation
|
from lerobot.lerobot_types import PolicyAction, RobotAction, RobotObservation
|
||||||
from lerobot.utils.constants import ACTION, OBS_STR
|
from lerobot.utils.constants import ACTION, OBS_STR
|
||||||
from lerobot.utils.feature_utils import build_dataset_frame
|
from lerobot.utils.feature_utils import build_dataset_frame
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from lerobot.configs.policies import PreTrainedConfig
|
from lerobot.configs.policies import PreTrainedConfig
|
||||||
from lerobot.configs.types import NormalizationMode
|
from lerobot.configs.types import FeatureType, NormalizationMode, PolicyFeature
|
||||||
from lerobot.optim.optimizers import AdamWConfig
|
from lerobot.optim.optimizers import AdamWConfig
|
||||||
from lerobot.optim.schedulers import CosineDecayWithWarmupSchedulerConfig
|
from lerobot.optim.schedulers import CosineDecayWithWarmupSchedulerConfig
|
||||||
|
from lerobot.utils.constants import OBS_STATE
|
||||||
|
|
||||||
|
|
||||||
@PreTrainedConfig.register_subclass("vla_jepa")
|
@PreTrainedConfig.register_subclass("vla_jepa")
|
||||||
@@ -122,6 +124,13 @@ class VLAJEPAConfig(PreTrainedConfig):
|
|||||||
if self.robot_state_feature is not None:
|
if self.robot_state_feature is not None:
|
||||||
self.state_dim = self.robot_state_feature.shape[0]
|
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:
|
def get_optimizer_preset(self) -> AdamWConfig:
|
||||||
return AdamWConfig(
|
return AdamWConfig(
|
||||||
lr=self.optimizer_lr,
|
lr=self.optimizer_lr,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from contextlib import nullcontext
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ from torch import Tensor, nn
|
|||||||
from lerobot.policies.pretrained import PreTrainedPolicy, T
|
from lerobot.policies.pretrained import PreTrainedPolicy, T
|
||||||
from lerobot.policies.utils import populate_queues
|
from lerobot.policies.utils import populate_queues
|
||||||
from lerobot.utils.constants import ACTION, OBS_STATE
|
from lerobot.utils.constants import ACTION, OBS_STATE
|
||||||
|
from lerobot.utils.device_utils import is_amp_available
|
||||||
from lerobot.utils.import_utils import _transformers_available, require_package
|
from lerobot.utils.import_utils import _transformers_available, require_package
|
||||||
|
|
||||||
if TYPE_CHECKING or _transformers_available:
|
if TYPE_CHECKING or _transformers_available:
|
||||||
@@ -39,6 +41,21 @@ from .configuration_vla_jepa import VLAJEPAConfig
|
|||||||
from .qwen_interface import Qwen3VLInterface
|
from .qwen_interface import Qwen3VLInterface
|
||||||
from .world_model import ActionConditionedVideoPredictor
|
from .world_model import ActionConditionedVideoPredictor
|
||||||
|
|
||||||
|
|
||||||
|
def _get_autocast_context(device_type: str, dtype: torch.dtype = torch.bfloat16):
|
||||||
|
"""Return an autocast context appropriate for the device.
|
||||||
|
|
||||||
|
MPS does not support ``torch.autocast`` at all. On CUDA devices
|
||||||
|
without bfloat16 support (compute capability < 8.0) we fall back to
|
||||||
|
float16.
|
||||||
|
"""
|
||||||
|
if not is_amp_available(device_type):
|
||||||
|
return nullcontext()
|
||||||
|
if device_type == "cuda" and dtype == torch.bfloat16 and not torch.cuda.is_bf16_supported():
|
||||||
|
dtype = torch.float16
|
||||||
|
return torch.autocast(device_type=device_type, dtype=dtype)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Native VLA-JEPA Model - follows original starVLA VLA_JEPA.py implementation
|
# Native VLA-JEPA Model - follows original starVLA VLA_JEPA.py implementation
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -183,7 +200,7 @@ class VLAJEPAModel(nn.Module):
|
|||||||
action_idx = action_mask.nonzero(as_tuple=True)
|
action_idx = action_mask.nonzero(as_tuple=True)
|
||||||
|
|
||||||
device_type = next(self.parameters()).device.type
|
device_type = next(self.parameters()).device.type
|
||||||
with torch.autocast(device_type=device_type, dtype=torch.bfloat16):
|
with _get_autocast_context(device_type, torch.bfloat16):
|
||||||
last_hidden = self._qwen_last_decoder_hidden(qwen_inputs) # [B, seq_len, H]
|
last_hidden = self._qwen_last_decoder_hidden(qwen_inputs) # [B, seq_len, H]
|
||||||
b, _, h = last_hidden.shape
|
b, _, h = last_hidden.shape
|
||||||
embodied_action_tokens = last_hidden[embodied_idx[0], embodied_idx[1], :].view(b, -1, h)
|
embodied_action_tokens = last_hidden[embodied_idx[0], embodied_idx[1], :].view(b, -1, h)
|
||||||
@@ -250,7 +267,7 @@ class VLAJEPAModel(nn.Module):
|
|||||||
) -> Tensor:
|
) -> Tensor:
|
||||||
"""Flow-matching action-head loss, repeated over `repeated_diffusion_steps`."""
|
"""Flow-matching action-head loss, repeated over `repeated_diffusion_steps`."""
|
||||||
device_type = next(self.parameters()).device.type
|
device_type = next(self.parameters()).device.type
|
||||||
with torch.autocast(device_type=device_type, dtype=torch.float32):
|
with _get_autocast_context(device_type, torch.float32):
|
||||||
r = self.config.repeated_diffusion_steps
|
r = self.config.repeated_diffusion_steps
|
||||||
horizon = self.config.chunk_size
|
horizon = self.config.chunk_size
|
||||||
actions_target = actions[:, -horizon:, :].to(torch.float32).repeat(r, 1, 1)
|
actions_target = actions[:, -horizon:, :].to(torch.float32).repeat(r, 1, 1)
|
||||||
@@ -399,7 +416,8 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
|||||||
state = batch.get(OBS_STATE)
|
state = batch.get(OBS_STATE)
|
||||||
if state is not None:
|
if state is not None:
|
||||||
if state.ndim > 2:
|
if state.ndim > 2:
|
||||||
state = state[:, -1, :]
|
# deltas are forward-looking here, so index 0 is the current observation, not -1.
|
||||||
|
state = state[:, 0, :]
|
||||||
inputs["state"] = (state.unsqueeze(1) if state.ndim == 2 else state).float() # [B, 1, dim]
|
inputs["state"] = (state.unsqueeze(1) if state.ndim == 2 else state).float() # [B, 1, dim]
|
||||||
|
|
||||||
return inputs
|
return inputs
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user