Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/docs/docs/features/lerobot_recorder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# LeRobot Recorder
> LeRobot Dataset Recorder: Record LeRobot-format datasets directly during teleoperation

LeRobot Recorder is an enhanced feature provided by LeIsaac that allows users to record data directly in LeRobot format during teleoperation. This feature seamlessly integrates into the teleoperation workflow, generating LeRobot-standard datasets without requiring additional data conversion steps.

## How It Works

LeRobot Recorder replaces the default recorder manager with `LeRobotRecorderManager`. After each environment step:

1. **Data Collection**: Collects observations (including joint positions, camera images, etc.) and actions from the current step
2. **Format Conversion**: Converts data to LeRobot frame format via the `build_lerobot_frame` method
3. **Buffer Management**: Adds frame data to the LeRobot Dataset buffer
4. **Episode Processing**: When an episode ends, decides whether to save based on task success status:
- **Success**: Calls `flush()` to save the entire episode to the dataset
- **Failure**: Calls `clear()` to clear the buffer without saving data

LeIsaac automatically skips the first 5 frames of each episode to avoid instability from initial states affecting data quality.

## Usage

LeRobot Recorder requires the lerobot dependency. For installation instructions, refer to this [section](../getting_started/installation#optional-install-lerobot).

To record data in LeRobot format during teleoperation, use the following command:

```shell
python scripts/environments/teleoperation/teleop_se3_agent.py \
--task=LeIsaac-SO101-PickOrange-v0 \
--teleop_device=so101leader \
--port=/dev/ttyACM0 \
--num_envs=1 \
--device=cuda \
--enable_cameras \
--record \
--use_lerobot_recorder \
--lerobot_dataset_repo_id=EverNorif/test_lerobot_recorder \
--lerobot_dataset_fps=30
```

Simply enable `--use_lerobot_recorder` and specify the `repo_id` and `dataset_fps` to record data directly in LeRobot Dataset format during teleoperation.

## Parameters

- `--use_lerobot_recorder`: Enables the LeRobot format recorder
- `--lerobot_dataset_repo_id`: HuggingFace dataset repository ID (format: `username/repository_name`)
- `--lerobot_dataset_fps`: Dataset frame rate, typically set to 30 FPS

::::tip
Compared to recording as hdf5, LeRobot Recorder integration may cause slight delays in teleoperation. If you encounter this issue, consider not using `--use_lerobot_recorder`.
::::
12 changes: 12 additions & 0 deletions docs/docs/docs/getting_started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ If you are using a 50-series GPU, we recommend using IsaacSim 5.0+ and IsaacLab
| PyTorch | 2.5.1 | 2.7.0 | 2.7.0 |
::::

### [Optional] Install Lerobot

We also provide integration with LeRobot. In certain cases, you may need the lerobot dependency, such as for data conversion, lerobot dataset recorder, lerobot model inference, and envhub support. This is optional; you can install lerobot alongside leisaac when you need these features.

```bash
# Install with lerobot
pip install -e "source/leisaac[lerobot]"

# Fix numpy version
pip install numpy==1.26.0
```

## 2. Asset Preparation

We provide an example USD asset—a kitchen scene. Please download related scene [here](https://github.com/LightwheelAI/leisaac/releases/tag/v0.1.0) and extract it into the `assets` directory. The directory structure should look like this:
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/docs/getting_started/teleoperation.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ python scripts/environments/teleoperation/teleop_se3_agent.py \

- `--quality`: Whether to enable quality render mode.

- `--use_lerobot_recorder`: Whether to use lerobot recorder.

- `--lerobot_dataset_repo_id`: LeRobot dataset repository ID.

- `--lerobot_dataset_fps`: LeRobot dtaset frames per second.

</details>

::::tip
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/docs/tutorials/custom_task.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ class CustomTaskEnvCfg(SingleArmTaskEnvCfg):

terminations: TerminationsCfg = TerminationsCfg()

task_description: str = "pick up the red cube and place it into the box."

def __post_init__(self) -> None:
super().__post_init__()

Expand Down
6 changes: 6 additions & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ const sidebars = {
link: { type: 'doc', id: 'docs/features/envhub_support' },
items: [],
},
{
type: 'category',
label: 'LeRobot Recorder',
link: { type: 'doc', id: 'docs/features/lerobot_recorder' },
items: [],
},
],
},
'docs/trouble_shooting',
Expand Down
179 changes: 113 additions & 66 deletions scripts/environments/teleoperation/teleop_se3_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
if multiprocessing.get_start_method() != "spawn":
multiprocessing.set_start_method("spawn", force=True)
import argparse
import signal

from isaaclab.app import AppLauncher

Expand Down Expand Up @@ -64,6 +65,9 @@

parser.add_argument("--recalibrate", action="store_true", help="recalibrate SO101-Leader or Bi-SO101Leader")
parser.add_argument("--quality", action="store_true", help="whether to enable quality render mode.")
parser.add_argument("--use_lerobot_recorder", action="store_true", help="whether to use lerobot recorder.")
parser.add_argument("--lerobot_dataset_repo_id", type=str, default=None, help="Lerobot Dataset repository ID.")
parser.add_argument("--lerobot_dataset_fps", type=int, default=30, help="Lerobot Dataset frames per second.")

# append AppLauncher cli args
AppLauncher.add_app_launcher_args(parser)
Expand Down Expand Up @@ -180,16 +184,22 @@ def main(): # noqa: C901
env_cfg.terminations.success = None
# recorder preprocess & manual success terminate preprocess
if args_cli.record:
if args_cli.resume:
env_cfg.recorders.dataset_export_mode = EnhanceDatasetExportMode.EXPORT_ALL_RESUME
assert os.path.exists(
args_cli.dataset_file
), "the dataset file does not exist, please don't use '--resume' if you want to record a new dataset"
if args_cli.use_lerobot_recorder:
if args_cli.resume:
env_cfg.recorders.dataset_export_mode = EnhanceDatasetExportMode.EXPORT_SUCCEEDED_ONLY_RESUME
else:
env_cfg.recorders.dataset_export_mode = DatasetExportMode.EXPORT_SUCCEEDED_ONLY
else:
env_cfg.recorders.dataset_export_mode = DatasetExportMode.EXPORT_ALL
assert not os.path.exists(
args_cli.dataset_file
), "the dataset file already exists, please use '--resume' to resume recording"
if args_cli.resume:
env_cfg.recorders.dataset_export_mode = EnhanceDatasetExportMode.EXPORT_ALL_RESUME
assert os.path.exists(
args_cli.dataset_file
), "the dataset file does not exist, please don't use '--resume' if you want to record a new dataset"
else:
env_cfg.recorders.dataset_export_mode = DatasetExportMode.EXPORT_ALL
assert not os.path.exists(
args_cli.dataset_file
), "the dataset file already exists, please use '--resume' to resume recording"
env_cfg.recorders.dataset_export_dir_path = output_dir
env_cfg.recorders.dataset_filename = output_file_name
if is_direct_env:
Expand All @@ -205,12 +215,26 @@ def main(): # noqa: C901

# create environment
env: ManagerBasedRLEnv | DirectRLEnv = gym.make(task_name, cfg=env_cfg).unwrapped
# replace the original recorder manager with the streaming recorder manager
# replace the original recorder manager with the streaming recorder manager or lerobot recorder manager
if args_cli.record:
del env.recorder_manager
env.recorder_manager = StreamingRecorderManager(env_cfg.recorders, env)
env.recorder_manager.flush_steps = 100
env.recorder_manager.compression = "lzf"
if args_cli.use_lerobot_recorder:
from leisaac.enhance.datasets.lerobot_dataset_handler import (
LeRobotDatasetCfg,
)
from leisaac.enhance.managers.lerobot_recorder_manager import (
LeRobotRecorderManager,
)

dataset_cfg = LeRobotDatasetCfg(
repo_id=args_cli.lerobot_dataset_repo_id,
fps=args_cli.lerobot_dataset_fps,
)
env.recorder_manager = LeRobotRecorderManager(env_cfg.recorders, dataset_cfg, env)
else:
env.recorder_manager = StreamingRecorderManager(env_cfg.recorders, env)
env.recorder_manager.flush_steps = 100
env.recorder_manager.compression = "lzf"

# create controller
if args_cli.teleop_device == "keyboard":
Expand Down Expand Up @@ -284,61 +308,84 @@ def reset_task_success():

start_record_state = False

# simulate environment
while simulation_app.is_running():
# run everything in inference mode
with torch.inference_mode():
if env.cfg.dynamic_reset_gripper_effort_limit:
dynamic_reset_gripper_effort_limit_sim(env, args_cli.teleop_device)
actions = teleop_interface.advance()
if should_reset_task_success:
print("Task Success!!!")
should_reset_task_success = False
if args_cli.record:
manual_terminate(env, True)
if should_reset_recording_instance:
env.reset()
should_reset_recording_instance = False
if start_record_state:
interrupted = False

def signal_handler(signum, frame):
"""Handle SIGINT (Ctrl+C) signal."""
nonlocal interrupted
interrupted = True
print("\n[INFO] KeyboardInterrupt (Ctrl+C) detected. Cleaning up resources...")

original_sigint_handler = signal.signal(signal.SIGINT, signal_handler)

try:
while simulation_app.is_running() and not interrupted:
# run everything in inference mode
with torch.inference_mode():
if env.cfg.dynamic_reset_gripper_effort_limit:
dynamic_reset_gripper_effort_limit_sim(env, args_cli.teleop_device)
actions = teleop_interface.advance()
if should_reset_task_success:
print("Task Success!!!")
should_reset_task_success = False
if args_cli.record:
print("Stop Recording!!!")
start_record_state = False
if args_cli.record:
manual_terminate(env, False)
# print out the current demo count if it has changed
if (
args_cli.record
and env.recorder_manager.exported_successful_episode_count + resume_recorded_demo_count
> current_recorded_demo_count
):
current_recorded_demo_count = (
env.recorder_manager.exported_successful_episode_count + resume_recorded_demo_count
)
print(f"Recorded {current_recorded_demo_count} successful demonstrations.")
if (
args_cli.record
and args_cli.num_demos > 0
and env.recorder_manager.exported_successful_episode_count + resume_recorded_demo_count
>= args_cli.num_demos
):
print(f"All {args_cli.num_demos} demonstrations recorded. Exiting the app.")
break

elif actions is None:
env.render()
# apply actions
else:
if not start_record_state:
manual_terminate(env, True)
if should_reset_recording_instance:
env.reset()
should_reset_recording_instance = False
if start_record_state:
if args_cli.record:
print("Stop Recording!!!")
start_record_state = False
if args_cli.record:
print("Start Recording!!!")
start_record_state = True
env.step(actions)
if rate_limiter:
rate_limiter.sleep(env)

# close the simulator
env.close()
simulation_app.close()
manual_terminate(env, False)
# print out the current demo count if it has changed
if (
args_cli.record
and env.recorder_manager.exported_successful_episode_count + resume_recorded_demo_count
> current_recorded_demo_count
):
current_recorded_demo_count = (
env.recorder_manager.exported_successful_episode_count + resume_recorded_demo_count
)
print(f"Recorded {current_recorded_demo_count} successful demonstrations.")
if (
args_cli.record
and args_cli.num_demos > 0
and env.recorder_manager.exported_successful_episode_count + resume_recorded_demo_count
>= args_cli.num_demos
):
print(f"All {args_cli.num_demos} demonstrations recorded. Exiting the app.")
break

elif actions is None:
env.render()
# apply actions
else:
if not start_record_state:
if args_cli.record:
print("Start Recording!!!")
start_record_state = True
env.step(actions)
if rate_limiter:
rate_limiter.sleep(env)
if interrupted:
break
except Exception as e:
import traceback

print(f"\n[ERROR] An error occurred: {e}\n")
traceback.print_exc()
print("[INFO] Cleaning up resources...")
finally:
# Restore original signal handler
signal.signal(signal.SIGINT, original_sigint_handler)
# finalize the recorder manager
if args_cli.record and hasattr(env.recorder_manager, "finalize"):
env.recorder_manager.finalize()
# close the simulator
env.close()
simulation_app.close()


if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions source/leisaac/leisaac/devices/lerobot/bi_so101_leader.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ def __init__(

# use left so101 leader as the main device to store state
print("Connecting to left_so101_leader...")
self.left_so101_leader = SO101Leader(env, left_port, recalibrate, "left_so101_leader.json", verbose=False)
self.left_so101_leader = SO101Leader(env, left_port, recalibrate, "left_so101_leader.json")
print("Connecting to right_so101_leader...")
self.right_so101_leader = SO101Leader(env, right_port, recalibrate, "right_so101_leader.json", verbose=False)
self.right_so101_leader = SO101Leader(env, right_port, recalibrate, "right_so101_leader.json")

self.left_so101_leader._stop_keyboard_listener()
self.right_so101_leader._stop_keyboard_listener()
Expand Down
Loading