diff --git a/docs/source/il_robots.mdx b/docs/source/il_robots.mdx index c0e6b80a5..53ae5af82 100644 --- a/docs/source/il_robots.mdx +++ b/docs/source/il_robots.mdx @@ -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). diff --git a/docs/source/inference.mdx b/docs/source/inference.mdx index 89260d9c1..4514e8b2d 100644 --- a/docs/source/inference.mdx +++ b/docs/source/inference.mdx @@ -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. | --- diff --git a/src/lerobot/common/control_utils.py b/src/lerobot/common/control_utils.py index 210ab4c8a..ddaf77d26 100644 --- a/src/lerobot/common/control_utils.py +++ b/src/lerobot/common/control_utils.py @@ -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. ######################################################################################## diff --git a/src/lerobot/rollout/__init__.py b/src/lerobot/rollout/__init__.py index 993f1e250..9808e3618 100644 --- a/src/lerobot/rollout/__init__.py +++ b/src/lerobot/rollout/__init__.py @@ -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", diff --git a/src/lerobot/rollout/configs.py b/src/lerobot/rollout/configs.py index a008165b4..7a565e7d6 100644 --- a/src/lerobot/rollout/configs.py +++ b/src/lerobot/rollout/configs.py @@ -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): diff --git a/src/lerobot/rollout/strategies/__init__.py b/src/lerobot/rollout/strategies/__init__.py index 57b2324b0..2501064bd 100644 --- a/src/lerobot/rollout/strategies/__init__.py +++ b/src/lerobot/rollout/strategies/__init__.py @@ -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", diff --git a/src/lerobot/rollout/strategies/legacy.py b/src/lerobot/rollout/strategies/episodic.py similarity index 94% rename from src/lerobot/rollout/strategies/legacy.py rename to src/lerobot/rollout/strategies/episodic.py index 4640dd6ee..42be39290 100644 --- a/src/lerobot/rollout/strategies/legacy.py +++ b/src/lerobot/rollout/strategies/episodic.py @@ -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") diff --git a/src/lerobot/rollout/strategies/factory.py b/src/lerobot/rollout/strategies/factory.py index be6878ba8..3e1ce3214 100644 --- a/src/lerobot/rollout/strategies/factory.py +++ b/src/lerobot/rollout/strategies/factory.py @@ -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" ) diff --git a/src/lerobot/scripts/lerobot_rollout.py b/src/lerobot/scripts/lerobot_rollout.py index d1bed58c7..82a858b9a 100644 --- a/src/lerobot/scripts/lerobot_rollout.py +++ b/src/lerobot/scripts/lerobot_rollout.py @@ -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" diff --git a/tests/test_rollout.py b/tests/test_rollout.py index 0edcb0a33..85a29ff4c 100644 --- a/tests/test_rollout.py +++ b/tests/test_rollout.py @@ -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():