mirror of
https://github.com/huggingface/lerobot.git
synced 2026-06-01 19:31:25 +00:00
Compare commits
5 Commits
docs/compl
...
docs/add-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfb9133842 | ||
|
|
2ddb2a3553 | ||
|
|
71848ebb2e | ||
|
|
5c98e80430 | ||
|
|
f65f3f7a4a |
@@ -9,6 +9,8 @@
|
|||||||
- sections:
|
- sections:
|
||||||
- local: il_robots
|
- local: il_robots
|
||||||
title: Imitation Learning for Robots
|
title: Imitation Learning for Robots
|
||||||
|
- local: lelab
|
||||||
|
title: LeLab - Lerobot GUI
|
||||||
- local: bring_your_own_policies
|
- local: bring_your_own_policies
|
||||||
title: Adding a Policy
|
title: Adding a Policy
|
||||||
- local: integrate_hardware
|
- local: integrate_hardware
|
||||||
|
|||||||
26
docs/source/lelab.mdx
Normal file
26
docs/source/lelab.mdx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# LeLab - LeRobot Guide
|
||||||
|
|
||||||
|
LeLab is a graphical user interface built on top of the LeRobot library, designed to make robotics accessible without needing to memorize CLI commands. From a single app you can configure your robot, teleoperate it, collect datasets, train policies locally or on cloud GPUs via HF Jobs, and deploy trained models back onto your robot. It's the easiest way to go from an unboxed SO-101 to a working policy, and a great companion for anyone learning the LeRobot workflow. Source code and issues live on GitHub: [huggingface/leLab](https://github.com/huggingface/leLab).
|
||||||
|
|
||||||
|
🎥 _Video walkthrough coming soon._
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Requires [`uv`](https://docs.astral.sh/uv/getting-started/installation/). Install and launch in one command:
|
||||||
|
|
||||||
|
```
|
||||||
|
uv tool install git+https://github.com/huggingface/leLab.git && lelab
|
||||||
|
```
|
||||||
|
|
||||||
|
After install, run `lelab` from your terminal anytime to start the app.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Add robots** — Select arm type (leader/follower), calibrate each joint from the middle position, and attach cameras.
|
||||||
|
- **Teleoperation** — Control the follower arm with the leader and see a live 3D visualization of the arms.
|
||||||
|
- **Dataset recording** — Define a task description, number of episodes, and episode/reset durations. Press spacebar to advance between episodes. 30+ episodes recommended.
|
||||||
|
- **Local training** — Train a policy directly on your own machine with a selected dataset, policy type, batch size, and step count.
|
||||||
|
- **Cloud training with HF Jobs** — Train on powerful GPUs via [HF Jobs](https://huggingface.co/docs/huggingface_hub/en/guides/jobs) with transparent pricing. Run `hf auth login` first. See the [Compute HW Guide](hardware_guide.mdx) for hardware/batch size tips.
|
||||||
|
- **Training visualization** — Watch progress live in the app, with checkpoints saved automatically.
|
||||||
|
- **Run trained policies** — Pick any model from your jobs list and run inference on your robot with one click.
|
||||||
|
- **Use community datasets** — Provide any Hugging Face dataset ID to train on datasets you didn't record yourself.
|
||||||
@@ -255,8 +255,7 @@ def extract_path_fields_from_config(config_path: str, path_fields: list[str]) ->
|
|||||||
remaining = config_data[field]
|
remaining = config_data[field]
|
||||||
if remaining:
|
if remaining:
|
||||||
_config_yaml_overrides[field] = _flatten_to_cli_args(remaining)
|
_config_yaml_overrides[field] = _flatten_to_cli_args(remaining)
|
||||||
else:
|
del config_data[field]
|
||||||
del config_data[field]
|
|
||||||
modified = True
|
modified = True
|
||||||
|
|
||||||
if not modified:
|
if not modified:
|
||||||
@@ -311,7 +310,13 @@ def wrap(config_path: Path | None = None) -> Callable[[F], F]:
|
|||||||
cli_args = filter_arg("config_path", cli_args)
|
cli_args = filter_arg("config_path", cli_args)
|
||||||
cfg = argtype.from_pretrained(config_path_cli, cli_args=cli_args)
|
cfg = argtype.from_pretrained(config_path_cli, cli_args=cli_args)
|
||||||
else:
|
else:
|
||||||
cfg = draccus.parse(config_class=argtype, config_path=config_path, args=cli_args)
|
if config_path_cli:
|
||||||
|
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,
|
||||||
|
)
|
||||||
response = fn(cfg, *args, **kwargs)
|
response = fn(cfg, *args, **kwargs)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class Eagle25VLPreTrainedModel(PreTrainedModel):
|
|||||||
"SiglipEncoderLayer",
|
"SiglipEncoderLayer",
|
||||||
]
|
]
|
||||||
_skip_keys_device_placement = "past_key_values"
|
_skip_keys_device_placement = "past_key_values"
|
||||||
|
_supports_flash_attn = True
|
||||||
_supports_flash_attn_2 = True
|
_supports_flash_attn_2 = True
|
||||||
_supports_cache_class = True
|
_supports_cache_class = True
|
||||||
_supports_static_cache = True
|
_supports_static_cache = True
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ class Eagle25VLProcessor(ProcessorMixin):
|
|||||||
"videos_kwargs",
|
"videos_kwargs",
|
||||||
"text_kwargs",
|
"text_kwargs",
|
||||||
]
|
]
|
||||||
image_processor_class = "AutoImageProcessor"
|
|
||||||
tokenizer_class = "AutoTokenizer"
|
tokenizer_class = "AutoTokenizer"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@@ -206,7 +206,11 @@ def _build_eagle_processor(tokenizer_assets_repo: str = DEFAULT_TOKENIZER_ASSETS
|
|||||||
"Vendor files are copied during model creation. Create the policy/model first, "
|
"Vendor files are copied during model creation. Create the policy/model first, "
|
||||||
"or call ensure_eagle_cache_ready() before building processors."
|
"or call ensure_eagle_cache_ready() before building processors."
|
||||||
)
|
)
|
||||||
proc = AutoProcessor.from_pretrained(str(cache_dir), trust_remote_code=True, use_fast=True)
|
proc = AutoProcessor.from_pretrained(
|
||||||
|
str(cache_dir),
|
||||||
|
trust_remote_code=True,
|
||||||
|
fix_mistral_regex=False,
|
||||||
|
)
|
||||||
proc.tokenizer.padding_side = "left"
|
proc.tokenizer.padding_side = "left"
|
||||||
return proc
|
return proc
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
"""Tests for policy.path support in YAML config files (issue #2957)."""
|
"""Tests for policy.path support in YAML config files (issue #2957)."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from lerobot.configs import parser
|
||||||
from lerobot.configs.parser import (
|
from lerobot.configs.parser import (
|
||||||
_config_path_args,
|
_config_path_args,
|
||||||
_config_yaml_overrides,
|
_config_yaml_overrides,
|
||||||
@@ -16,7 +20,8 @@ from lerobot.configs.parser import (
|
|||||||
|
|
||||||
|
|
||||||
def test_extract_path_fields_from_yaml():
|
def test_extract_path_fields_from_yaml():
|
||||||
"""Test that policy.path is extracted from a YAML config and removed."""
|
"""Test that policy.path is extracted from a YAML config and the policy block
|
||||||
|
is removed entirely (siblings are captured separately as cli_overrides)."""
|
||||||
config = {
|
config = {
|
||||||
"dataset": {"repo_id": "lerobot/pusht"},
|
"dataset": {"repo_id": "lerobot/pusht"},
|
||||||
"policy": {"type": "smolvla", "path": "lerobot/smolvla_base", "push_to_hub": False},
|
"policy": {"type": "smolvla", "path": "lerobot/smolvla_base", "push_to_hub": False},
|
||||||
@@ -26,26 +31,33 @@ def test_extract_path_fields_from_yaml():
|
|||||||
config_path = f.name
|
config_path = f.name
|
||||||
|
|
||||||
_config_path_args.clear()
|
_config_path_args.clear()
|
||||||
|
_config_yaml_overrides.clear()
|
||||||
cleaned_path = extract_path_fields_from_config(config_path, ["policy"])
|
cleaned_path = extract_path_fields_from_config(config_path, ["policy"])
|
||||||
|
|
||||||
# Path should be extracted and stored
|
# Path should be extracted and stored
|
||||||
assert _config_path_args["policy"] == "lerobot/smolvla_base"
|
assert _config_path_args["policy"] == "lerobot/smolvla_base"
|
||||||
|
|
||||||
# Cleaned config should not have the path field
|
# Cleaned config should not have the policy block at all -- draccus must not
|
||||||
|
# try to decode it as PreTrainedConfig; the actual config comes from
|
||||||
|
# from_pretrained(path) with the captured overrides applied on top.
|
||||||
with open(cleaned_path) as f:
|
with open(cleaned_path) as f:
|
||||||
cleaned = yaml.safe_load(f)
|
cleaned = yaml.safe_load(f)
|
||||||
assert "path" not in cleaned["policy"]
|
assert "policy" not in cleaned
|
||||||
assert cleaned["policy"]["type"] == "smolvla"
|
|
||||||
assert cleaned["policy"]["push_to_hub"] is False
|
|
||||||
|
|
||||||
# Original dataset should be untouched
|
# Original dataset should be untouched
|
||||||
assert cleaned["dataset"]["repo_id"] == "lerobot/pusht"
|
assert cleaned["dataset"]["repo_id"] == "lerobot/pusht"
|
||||||
|
|
||||||
|
# Sibling overrides (excluding type/path) captured for from_pretrained.
|
||||||
|
overrides = get_yaml_overrides("policy")
|
||||||
|
assert any("push_to_hub=false" in o for o in overrides)
|
||||||
|
|
||||||
_config_path_args.clear()
|
_config_path_args.clear()
|
||||||
|
_config_yaml_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
def test_extract_path_fields_from_json():
|
def test_extract_path_fields_from_json():
|
||||||
"""Test that policy.path is extracted from a JSON config."""
|
"""Test that policy.path is extracted from a JSON config and the policy
|
||||||
|
block is removed entirely."""
|
||||||
config = {
|
config = {
|
||||||
"policy": {"type": "act", "path": "some/local/path"},
|
"policy": {"type": "act", "path": "some/local/path"},
|
||||||
}
|
}
|
||||||
@@ -54,15 +66,17 @@ def test_extract_path_fields_from_json():
|
|||||||
config_path = f.name
|
config_path = f.name
|
||||||
|
|
||||||
_config_path_args.clear()
|
_config_path_args.clear()
|
||||||
|
_config_yaml_overrides.clear()
|
||||||
cleaned_path = extract_path_fields_from_config(config_path, ["policy"])
|
cleaned_path = extract_path_fields_from_config(config_path, ["policy"])
|
||||||
|
|
||||||
assert _config_path_args["policy"] == "some/local/path"
|
assert _config_path_args["policy"] == "some/local/path"
|
||||||
|
|
||||||
with open(cleaned_path) as f:
|
with open(cleaned_path) as f:
|
||||||
cleaned = json.load(f)
|
cleaned = json.load(f)
|
||||||
assert "path" not in cleaned["policy"]
|
assert "policy" not in cleaned
|
||||||
|
|
||||||
_config_path_args.clear()
|
_config_path_args.clear()
|
||||||
|
_config_yaml_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
def test_extract_no_path_returns_original():
|
def test_extract_no_path_returns_original():
|
||||||
@@ -216,3 +230,91 @@ def test_flatten_nested_with_bools():
|
|||||||
args = _flatten_to_cli_args(d)
|
args = _flatten_to_cli_args(d)
|
||||||
assert "--optimizer.use_warmup=true" in args
|
assert "--optimizer.use_warmup=true" in args
|
||||||
assert "--optimizer.lr=0.01" in args
|
assert "--optimizer.lr=0.01" in args
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_removes_field_with_siblings_and_no_type():
|
||||||
|
"""Regression: when policy.path has siblings but no type:, the entire policy
|
||||||
|
block must still be removed from the cleaned config. Otherwise draccus tries
|
||||||
|
to decode the leftover dict as PreTrainedConfig and crashes on the missing
|
||||||
|
type discriminator.
|
||||||
|
"""
|
||||||
|
config = {
|
||||||
|
"dataset": {"repo_id": "lerobot/pusht"},
|
||||||
|
"policy": {
|
||||||
|
"path": "lerobot/smolvla_base",
|
||||||
|
"n_action_steps": 10,
|
||||||
|
"dtype": "bfloat16",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||||
|
yaml.dump(config, f)
|
||||||
|
config_path = f.name
|
||||||
|
|
||||||
|
_config_path_args.clear()
|
||||||
|
_config_yaml_overrides.clear()
|
||||||
|
cleaned_path = extract_path_fields_from_config(config_path, ["policy"])
|
||||||
|
|
||||||
|
with open(cleaned_path) as f:
|
||||||
|
cleaned = yaml.safe_load(f) or {}
|
||||||
|
assert "policy" not in cleaned, "policy block should be fully removed when path is present"
|
||||||
|
assert cleaned["dataset"]["repo_id"] == "lerobot/pusht"
|
||||||
|
assert _config_path_args["policy"] == "lerobot/smolvla_base"
|
||||||
|
overrides = get_yaml_overrides("policy")
|
||||||
|
assert any("n_action_steps=10" in o for o in overrides)
|
||||||
|
assert any("dtype=bfloat16" in o for o in overrides)
|
||||||
|
|
||||||
|
_config_path_args.clear()
|
||||||
|
_config_yaml_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _DummyNested:
|
||||||
|
foo: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _DummyConfig:
|
||||||
|
nested: _DummyNested = field(default_factory=_DummyNested)
|
||||||
|
other: str = "default"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __get_path_fields__(cls):
|
||||||
|
return ["nested"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrap_uses_cleaned_config_for_draccus_parse():
|
||||||
|
"""Regression: wrap() updates config_path_cli to point at the cleaned temp
|
||||||
|
file but must propagate that to the draccus.parse fallback branch. Without
|
||||||
|
the fix, cli_args still contains --config_path=<original> and draccus reads
|
||||||
|
the original YAML with `path:` still in it, crashing on the unknown field.
|
||||||
|
"""
|
||||||
|
config = {
|
||||||
|
"nested": {"path": "some/checkpoint", "foo": 42},
|
||||||
|
"other": "set-via-yaml",
|
||||||
|
}
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||||||
|
yaml.dump(config, f)
|
||||||
|
config_path = f.name
|
||||||
|
|
||||||
|
_config_path_args.clear()
|
||||||
|
_config_yaml_overrides.clear()
|
||||||
|
|
||||||
|
captured: dict = {}
|
||||||
|
|
||||||
|
@parser.wrap()
|
||||||
|
def main(cfg: _DummyConfig) -> _DummyConfig:
|
||||||
|
captured["cfg"] = cfg
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
with patch.object(sys, "argv", ["prog", f"--config_path={config_path}"]):
|
||||||
|
main()
|
||||||
|
|
||||||
|
assert captured["cfg"].other == "set-via-yaml"
|
||||||
|
assert _config_path_args["nested"] == "some/checkpoint"
|
||||||
|
# Cleaned config dropped `nested:` entirely; defaults stand for this wrapper
|
||||||
|
# class (a real PreTrainedConfig would now load the checkpoint and apply
|
||||||
|
# the captured yaml_overrides via from_pretrained()).
|
||||||
|
assert captured["cfg"].nested.foo == 0
|
||||||
|
|
||||||
|
_config_path_args.clear()
|
||||||
|
_config_yaml_overrides.clear()
|
||||||
|
|||||||
Reference in New Issue
Block a user