* renaming to episodic

* changing semantics of the docstring
* fixing leader - follower handover disable torque
* adding optionnal config to disable handover
This commit is contained in:
Maximellerbach
2026-06-05 11:45:58 +02:00
parent eb21cd2b26
commit 8b1ffbe5c8
10 changed files with 58 additions and 51 deletions
+1 -1
View File
@@ -647,6 +647,6 @@ The `--strategy.type` flag selects the execution mode:
- `sentry`: Continuous recording with auto-upload (useful for large-scale evaluation)
- `highlight`: Ring buffer recording with keystroke save (useful for capturing interesting events)
- `dagger`: Human-in-the-loop data collection (see [HIL Data Collection](./hil_data_collection))
- `legacy`: Episode-oriented policy recording with reset phases, mirrors the old `lerobot-record` inference path
- `episodic`: Episode-oriented policy recording with reset phases between episodes
All strategies support `--inference.type=rtc` for smooth execution with slow VLA models (Pi0, Pi0.5, SmolVLA).
+11 -9
View File
@@ -157,13 +157,13 @@ Foot pedal input is also supported via `--strategy.input_device=pedal`. Configur
| `--strategy.input_device` | Input device: `keyboard` or `pedal` (default: keyboard) |
| `--teleop.type` | **Required.** Teleoperator type |
### Legacy (`--strategy.type=legacy`)
### Episodic (`--strategy.type=episodic`)
Episode-oriented recording that mirrors the old `lerobot-record` inference path. The policy drives the robot for each episode; an optional teleoperator can drive the robot during the reset phase between episodes.
Episode-oriented recording that mirrors the behavior of `lerobot-record`. The policy drives the robot for each episode; an optional teleoperator can drive the robot during the reset phase between episodes.
```bash
lerobot-rollout \
--strategy.type=legacy \
--strategy.type=episodic \
--policy.path=${HF_USER}/my_policy \
--robot.type=so100_follower \
--robot.port=/dev/ttyACM0 \
@@ -186,12 +186,14 @@ Teleop is optional — if omitted the robot holds its position during the reset
| `←` (left) | Discard episode and re-record it |
| `ESC` | Stop the recording session |
| Flag | Description |
| -------------------------- | ------------------------------------------------------- |
| `--dataset.num_episodes` | Number of episodes to record |
| `--dataset.episode_time_s` | Duration of each recording episode 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 |
| Flag | Description |
| -------------------------------------------- | -------------------------------------------------------------------------- |
| `--dataset.num_episodes` | Number of episodes to record |
| `--dataset.episode_time_s` | Duration of each recording episode 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 |
| `--strategy.reset_to_initial_position` | Whether to reset the robot to its initial position between episodes |
| `--strategy.smooth_leader_follower_handover` | Whether to turn on or off the leader -> follower smooth handover behavior. |
---
+2
View File
@@ -248,6 +248,8 @@ def sanity_check_dataset_robot_compatibility(
########################################################################################
# Teleoperator smooth handover helpers
# NOTE(Maxime): These functions use minimal type hints to maintain compatibility with utils
# being a root module.
########################################################################################
+4 -4
View File
@@ -23,8 +23,8 @@ from .configs import (
DAggerKeyboardConfig,
DAggerPedalConfig,
DAggerStrategyConfig,
EpisodicStrategyConfig,
HighlightStrategyConfig,
LegacyStrategyConfig,
RolloutConfig,
RolloutStrategyConfig,
SentryStrategyConfig,
@@ -50,8 +50,8 @@ from .inference import (
from .strategies import (
BaseStrategy,
DAggerStrategy,
EpisodicStrategy,
HighlightStrategy,
LegacyStrategy,
RolloutStrategy,
SentryStrategy,
create_strategy,
@@ -68,8 +68,8 @@ __all__ = [
"HardwareContext",
"HighlightStrategy",
"HighlightStrategyConfig",
"LegacyStrategy",
"LegacyStrategyConfig",
"EpisodicStrategy",
"EpisodicStrategyConfig",
"InferenceEngine",
"InferenceEngineConfig",
"PolicyContext",
+11 -7
View File
@@ -121,16 +121,16 @@ class DAggerPedalConfig:
upload: str = "KEY_C"
@RolloutStrategyConfig.register_subclass("legacy")
@RolloutStrategyConfig.register_subclass("episodic")
@dataclass
class LegacyStrategyConfig(RolloutStrategyConfig):
"""Episode-oriented recording that mirrors the pre-rollout lerobot-record behavior.
class EpisodicStrategyConfig(RolloutStrategyConfig):
"""Episode-oriented recording that mirrors the behavior of ``lerobot-record``.
Records ``dataset.num_episodes`` episodes of maximum ``dataset.episode_time_s`` each.
After each episode, runs ``dataset.reset_time_s`` seconds of reset time.
Keyboard controls (same as lerobot-record):
Right arrow — end episode early
Keyboard controls:
Right arrow — end current episode or reset phase early
Left arrow — discard current episode and re-record
Escape — stop recording session
@@ -143,7 +143,11 @@ class LegacyStrategyConfig(RolloutStrategyConfig):
# When True (default), moves the robot back to the joint positions captured at startup.
# Otherwise, leave the robot in its current position.
reset_to_initial_position: bool = True
pass
# Whether to turn on or off the leader -> follower smooth handover behavior.
# When False, fallback to follower -> leader handover.
# Note that leader -> follower handover is only supported when the leader has `send_feedback` capability.
smooth_leader_follower_handover: bool = True
@RolloutStrategyConfig.register_subclass("dagger")
@@ -259,7 +263,7 @@ class RolloutConfig:
SentryStrategyConfig,
HighlightStrategyConfig,
DAggerStrategyConfig,
LegacyStrategyConfig,
EpisodicStrategyConfig,
),
)
if needs_dataset and (self.dataset is None or not self.dataset.repo_id):
+2 -2
View File
@@ -17,9 +17,9 @@
from .base import BaseStrategy
from .core import RolloutStrategy, estimate_max_episode_seconds, safe_push_to_hub, send_next_action
from .dagger import DAggerEvents, DAggerPhase, DAggerStrategy
from .episodic import EpisodicStrategy
from .factory import create_strategy
from .highlight import HighlightStrategy
from .legacy import LegacyStrategy
from .sentry import SentryStrategy
__all__ = [
@@ -28,7 +28,7 @@ __all__ = [
"DAggerPhase",
"DAggerStrategy",
"HighlightStrategy",
"LegacyStrategy",
"EpisodicStrategy",
"RolloutStrategy",
"SentryStrategy",
"create_strategy",
@@ -12,17 +12,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Legacy rollout strategy: policy-driven multi-episode recording with reset phases.
Mirrors the pre-rollout ``lerobot-record`` inference path:
"""Episodic rollout strategy: mirrors the behavior of ``lerobot-record``.
- Policy drives the robot during each recording episode.
- An optional teleoperator can drive the robot during reset phases so the
operator can bring the environment back to its starting configuration.
If no teleop is connected the robot stays in its current position.
- Keyboard controls (identical to the old lerobot-record):
- Keyboard controls:
Right arrow end the current episode early
Right arrow end the current episode or reset phase early
Left arrow discard the current episode and re-record it
Escape stop the recording session
@@ -49,15 +47,15 @@ from lerobot.utils.robot_utils import precise_sleep
from lerobot.utils.utils import log_say
from lerobot.utils.visualization_utils import log_rerun_data
from ..configs import LegacyStrategyConfig
from ..configs import EpisodicStrategyConfig
from ..context import RolloutContext
from .core import RolloutStrategy, safe_push_to_hub, send_next_action
logger = logging.getLogger(__name__)
class LegacyStrategy(RolloutStrategy):
"""Policy-driven multi-episode recording (mirrors old lerobot-record inference path).
class EpisodicStrategy(RolloutStrategy):
"""Policy-driven multi-episode recording, mirrors the behavior of ``lerobot-record``.
Each recording episode runs the policy for maximum ``dataset.episode_time_s``
seconds, recording every frame. A reset phase of ``dataset.reset_time_s``
@@ -69,14 +67,14 @@ class LegacyStrategy(RolloutStrategy):
the start of each recording episode.
Keyboard events:
right arrow exit current episode early
right arrow end current episode or reset phase early
left arrow discard & re-record current episode
ESC stop the session
"""
config: LegacyStrategyConfig
config: EpisodicStrategyConfig
def __init__(self, config: LegacyStrategyConfig) -> None:
def __init__(self, config: EpisodicStrategyConfig) -> None:
super().__init__(config)
self._listener = None
self._events: dict | None = None
@@ -85,7 +83,7 @@ class LegacyStrategy(RolloutStrategy):
"""Start the inference engine and attach the keyboard listener."""
self._init_engine(ctx)
self._listener, self._events = init_keyboard_listener()
logger.info("Legacy strategy ready")
logger.info("Episodic strategy ready")
def run(self, ctx: RolloutContext) -> None:
"""Main multi-episode recording loop."""
@@ -150,7 +148,8 @@ class LegacyStrategy(RolloutStrategy):
current_pos = {k: v for k, v in obs.items() if k.endswith(".pos")}
if teleop_supports_feedback(teleop):
logger.info("Smooth handover: moving leader arm to follower position")
teleop_smooth_move_to(teleop, current_pos, duration_s=1)
teleop_smooth_move_to(teleop, current_pos, duration_s=2)
teleop.disable_torque()
else:
logger.info("Smooth handover: sliding follower to teleop position")
teleop_action = teleop.get_action()
@@ -191,7 +190,7 @@ class LegacyStrategy(RolloutStrategy):
# Save any frames buffered in the current episode so an unexpected
# exception or KeyboardInterrupt does not silently drop recorded data.
# suppress: save_episode raises if the buffer is empty (nothing to lose).
logger.info("Legacy control loop ended — saving any in-progress episode")
logger.info("Episodic control loop ended — saving any in-progress episode")
with contextlib.suppress(Exception):
dataset.save_episode()
@@ -330,4 +329,4 @@ class LegacyStrategy(RolloutStrategy):
return_to_initial_position=cfg.return_to_initial_position,
)
log_say("Exiting", play_sounds)
logger.info("Legacy strategy teardown complete")
logger.info("Episodic strategy teardown complete")
+4 -4
View File
@@ -21,8 +21,8 @@ from typing import TYPE_CHECKING
from .base import BaseStrategy
from .core import RolloutStrategy
from .dagger import DAggerStrategy
from .episodic import EpisodicStrategy
from .highlight import HighlightStrategy
from .legacy import LegacyStrategy
from .sentry import SentryStrategy
if TYPE_CHECKING:
@@ -43,8 +43,8 @@ def create_strategy(config: RolloutStrategyConfig) -> RolloutStrategy:
return HighlightStrategy(config)
if config.type == "dagger":
return DAggerStrategy(config)
if config.type == "legacy":
return LegacyStrategy(config)
if config.type == "episodic":
return EpisodicStrategy(config)
raise ValueError(
f"Unknown strategy type '{config.type}'. Available: base, sentry, highlight, dagger, legacy"
f"Unknown strategy type '{config.type}'. Available: base, sentry, highlight, dagger, episodic"
)
+4 -4
View File
@@ -25,7 +25,7 @@ Strategies
--strategy.type=sentry Continuous recording with auto-upload
--strategy.type=highlight Ring buffer + keystroke save
--strategy.type=dagger Human-in-the-loop (DAgger / RaC)
--strategy.type=legacy Episode oriented recording, mirrors old lerobot-record
--strategy.type=episodic Episode-oriented recording with reset phases
Inference backends
------------------
@@ -112,15 +112,15 @@ Usage examples
--display_data=true \\
--use_torch_compile=true
# Legacy mode — episode-oriented recording, mirrors old lerobot-record
# Episodic mode — episode-oriented recording with reset phases
lerobot-rollout \\
--strategy.type=legacy \\
--strategy.type=episodic \\
--policy.path=user/my_policy \\
--robot.type=so100_follower \\
--robot.port=/dev/ttyACM0 \\
--teleop.type=so100_leader \\
--teleop.port=/dev/ttyACM1 \\
--dataset.repo_id=user/rollout_legacy_data \\
--dataset.repo_id=user/rollout_episodic_data \\
--dataset.num_episodes=20 \\
--dataset.single_task="Grab the cube"
+5 -5
View File
@@ -59,8 +59,8 @@ def test_strategy_config_types():
from lerobot.rollout import (
BaseStrategyConfig,
DAggerStrategyConfig,
EpisodicStrategyConfig,
HighlightStrategyConfig,
LegacyStrategyConfig,
SentryStrategyConfig,
)
@@ -68,7 +68,7 @@ def test_strategy_config_types():
assert SentryStrategyConfig().type == "sentry"
assert HighlightStrategyConfig().type == "highlight"
assert DAggerStrategyConfig().type == "dagger"
assert LegacyStrategyConfig().type == "legacy"
assert EpisodicStrategyConfig().type == "episodic"
def test_dagger_config_invalid_input_device():
@@ -205,8 +205,8 @@ def test_create_strategy_dispatches():
BaseStrategyConfig,
DAggerStrategy,
DAggerStrategyConfig,
LegacyStrategy,
LegacyStrategyConfig,
EpisodicStrategy,
EpisodicStrategyConfig,
SentryStrategy,
SentryStrategyConfig,
create_strategy,
@@ -215,7 +215,7 @@ def test_create_strategy_dispatches():
assert isinstance(create_strategy(BaseStrategyConfig()), BaseStrategy)
assert isinstance(create_strategy(SentryStrategyConfig()), SentryStrategy)
assert isinstance(create_strategy(DAggerStrategyConfig()), DAggerStrategy)
assert isinstance(create_strategy(LegacyStrategyConfig()), LegacyStrategy)
assert isinstance(create_strategy(EpisodicStrategyConfig()), EpisodicStrategy)
def test_create_strategy_unknown_raises():