From 8d7969e7cb8b5c433872a7a9a85d132100cced8a Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Sat, 14 Jun 2025 14:23:07 +0200 Subject: [PATCH] fix(record): no teleop arg in reset environment (#1294) --- docs/source/il_robots.mdx | 4 ++++ docs/source/smolvla.mdx | 4 ++++ lerobot/record.py | 33 ++++++++++++++++++++++----------- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/docs/source/il_robots.mdx b/docs/source/il_robots.mdx index d13e431c..ddfcd98d 100644 --- a/docs/source/il_robots.mdx +++ b/docs/source/il_robots.mdx @@ -303,6 +303,10 @@ python -m lerobot.record \ --display_data=false \ --dataset.repo_id=$HF_USER/eval_so100 \ --dataset.single_task="Put lego brick into the transparent box" \ + # <- Teleop optional if you want to teleoperate in between episodes \ + # --teleop.type=so100_leader \ + # --teleop.port=/dev/ttyACM0 \ + # --teleop.id=my_awesome_leader_arm \ --policy.path=${HF_USER}/my_policy ``` diff --git a/docs/source/smolvla.mdx b/docs/source/smolvla.mdx index 58340baa..1d6596f6 100644 --- a/docs/source/smolvla.mdx +++ b/docs/source/smolvla.mdx @@ -87,6 +87,10 @@ python -m lerobot.record \ --dataset.repo_id=${HF_USER}/eval_DATASET_NAME_test \ # <- This will be the dataset name on HF Hub --dataset.episode_time_s=50 \ --dataset.num_episodes=10 \ + # <- Teleop optional if you want to teleoperate in between episodes \ + # --teleop.type=so100_leader \ + # --teleop.port=/dev/ttyACM0 \ + # --teleop.id=my_red_leader_arm \ --policy.path=HF_USER/FINETUNE_MODEL_NAME # <- Use your fine-tuned model ``` diff --git a/lerobot/record.py b/lerobot/record.py index 884a3fcd..acc844ff 100644 --- a/lerobot/record.py +++ b/lerobot/record.py @@ -23,12 +23,15 @@ python -m lerobot.record \ --robot.port=/dev/tty.usbmodem58760431541 \ --robot.cameras="{laptop: {type: opencv, camera_index: 0, width: 640, height: 480}}" \ --robot.id=black \ - --teleop.type=so100_leader \ - --teleop.port=/dev/tty.usbmodem58760431551 \ - --teleop.id=blue \ --dataset.repo_id=aliberts/record-test \ --dataset.num_episodes=2 \ - --dataset.single_task="Grab the cube" + --dataset.single_task="Grab the cube" \ + # <- Teleop optional if you want to teleoperate to record or in between episodes with a policy \ + # --teleop.type=so100_leader \ + # --teleop.port=/dev/tty.usbmodem58760431551 \ + # --teleop.id=blue \ + # <- Policy optional if you want to record with a policy \ + # --policy.path=${HF_USER}/my_policy \ ``` """ @@ -139,9 +142,6 @@ class RecordConfig: resume: bool = False def __post_init__(self): - if self.teleop is not None and self.policy is not None: - raise ValueError("Choose either a policy or a teleoperator to control the robot") - # HACK: We parse again the cli args here to get the pretrained path if there was one. policy_path = parser.get_path_arg("policy") if policy_path: @@ -149,6 +149,9 @@ class RecordConfig: self.policy = PreTrainedConfig.from_pretrained(policy_path, cli_overrides=cli_overrides) self.policy.pretrained_path = policy_path + if self.teleop is None and self.policy is None: + raise ValueError("Choose a policy, a teleoperator or both to control the robot") + @classmethod def __get_path_fields__(cls) -> list[str]: """This enables the parser to load config from the policy using `--policy.path=local/dir`""" @@ -179,6 +182,10 @@ def record_loop( while timestamp < control_time_s: start_loop_t = time.perf_counter() + if events["exit_early"]: + events["exit_early"] = False + break + observation = robot.get_observation() if policy is not None or dataset is not None: @@ -194,8 +201,15 @@ def record_loop( robot_type=robot.robot_type, ) action = {key: action_values[i].item() for i, key in enumerate(robot.action_features)} - else: + elif policy is None and teleop is not None: action = teleop.get_action() + else: + logging.info( + "No policy or teleoperator provided, skipping action generation." + "This is likely to happen when resetting the environment without a teleop device." + "The robot won't be at its rest position at the start of the next episode." + ) + continue # Action can eventually be clipped using `max_relative_target`, # so action actually sent is saved in the dataset. @@ -220,9 +234,6 @@ def record_loop( busy_wait(1 / fps - dt_s) timestamp = time.perf_counter() - start_episode_t - if events["exit_early"]: - events["exit_early"] = False - break @parser.wrap()