This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
LeLab is a FastAPI + React web interface wrapping the LeRobot framework for the SO-101 leader/follower arm. It exposes teleoperation, dataset recording, calibration, replay, and training as HTTP/WebSocket endpoints, replacing LeRobot's CLI + keyboard-driven flows.
The frontend (React + Vite) lives in frontend/. The bundle in frontend/dist/ is not committed: setup.py builds it while building the wheel, with Node from the nodejs-wheel-binaries build requirement, and ships it as package data (frontend.__init__.py makes setuptools treat it as a package); lelab/server.py mounts it as StaticFiles at / so a single lelab process serves both API and UI on :8000. The frontend/ directory is also force-pushed to the LeLab HF Space by .github/workflows/sync_space.yml — that's a separate runtime; the Space builds the Dockerfile (which runs npm run build itself).
Install (editable, requires Python ≥3.10):
pip install -e .lerobot is pinned to a ref on huggingface/lerobot (currently v0.7.0, see pyproject.toml), installed from git rather than PyPI because the published wheels don't expose the lerobot-rollout script that lelab/rollout.py shells out to. Bump the ref when you want a newer upstream release; expect import-path drift (e.g. lerobot.configs.dataset, lerobot.utils.feature_utils, lerobot.common.control_utils) and CLI flag renames (v0.7.0 replaced --wandb.* with --trackio.*). tests/test_lerobot_cli.py runs the training and rollout argv LeLab builds through the installed LeRobot's parsers, so run the tests right after a bump. Since v0.7.0 the arms' motors go through LeRobot's rustypot-backed SerialMotorsBus: use its public methods (set_operating_mode, set_half_turn_homings, disconnect), not Feetech SDK internals.
Run servers (entry point defined in pyproject.toml):
lelab # uvicorn on :8000, serves built frontend at /, opens browser
lelab --dev # spawns Vite dev (:8080) + uvicorn --reload (:8000), opens browser to :8080Editable installs skip the frontend build: lelab --dev serves directly from Vite, and lelab --rebuild builds frontend/dist/ with your local Node.js 22 when you want to try the production bundle. The SO-101 meshes in frontend/public/ are kept out of LFS because installing from git does not fetch LFS files.
Run the Python tests with pytest (config in pyproject.toml; install dev deps via pip install -e ".[test]"). Tests live in tests/ and cover request schemas, pure helpers, and idle/mutex branches of the feature handlers — subprocess/thread happy paths and HF Jobs integration are deliberately not unit-tested. Lint with ruff check / ruff format (config in pyproject.toml). There is no Python build step; for end-to-end validation, run lelab and exercise endpoints (curl or via the frontend).
server.py is a thin FastAPI router. Each feature lives in its own module that owns its global state and exposes handle_* functions plus a Pydantic request model:
- record.py: dataset recording around LeRobot's
record_loop; frontend buttons set the sameeventsdict LeRobot's keyboard listener would. The worker is a daemon thread, so the server's shutdown hook callsstop_recording_and_wait()to let an active session finalize on Ctrl+C. - teleoperate.py: leader→follower teleoperation loop over LeRobot's SO-101 leader and follower.
- replay.py: plays a recorded episode's actions on the follower arm, an in-process take on
lerobot-replayso the dataset browser can stop it. - calibrate.py — step-by-step web calibration with a
CalibrationManagersingleton and_step_completethreading.Event. - train.py — wraps the LeRobot training CLI as a subprocess (psutil for lifecycle, queue for log streaming).
- datasets.py — dataset listing (local cache + Hub, merged under a
sourcefield) plus the episode-browsing handlers behind/dataset-episodes,/dataset-episode,/dataset-frame,/dataset-thumbnails,/dataset-motionand/dataset-video. - episode_media.py — reads the LeRobot v3.0 on-disk layout (
meta/episodes/*.parquet+videos/) and decodes frames with PyAV. Deliberately does not go throughLeRobotDataset, so browsing a dataset never builds a robot config or imports torch. Several episodes share one mp4, so locating an episode resolves both the file and thefrom_/to_timestampwindow inside it — which is why/dataset-videois addressed by chunk/file rather than by episode. Callensure_episode_index(repo_id)before opening a local dataset withLeRobotDataset: LeRobot writes the index only infinalize(), and without it reads the dataset as "not downloaded yet" and fetches it from the Hub, which 404s for a dataset that was never pushed. - utils/config.py — shared paths and persistence: calibration JSON, saved ports, saved config selections. Import shared constants from here, do not hardcode paths in feature modules.
Each feature module owns module-level globals (e.g. recording_active, teleoperation_active, current_robot) protected by threads/locks where needed. There's no shared session object. Recording, teleoperation, inference and replay all drive the follower, so each start handler checks the others' flags and refuses to run while one is active. Calibration is not part of that check.
server.py defines a single ConnectionManager with a background _broadcast_worker thread that drains a queue.Queue and forwards joint data to all /ws/joint-data clients via a thread-local asyncio loop. Feature modules get the manager passed in (e.g. handle_start_teleoperation(request, manager)) and call manager.broadcast_joint_data_sync(data) from their worker threads. Don't await from these threads — use the sync queue method.
All under ~/.cache/huggingface/lerobot/ (managed in utils/config.py):
calibration/teleoperators/so101_leader/*.json— leader calibration files (also called "teleop")calibration/robots/so101_follower/*.json— follower calibration files (also called "robot")ports/{leader,follower}_port.txt— last-used serial portssaved_configs/{leader,follower}_config.txt— last-selected config name
device_type in API requests is "teleop" or "robot" (mapped to leader/follower paths). robot_type in port endpoints is "leader" or "follower". Don't conflate the two vocabularies.
setup_calibration_files copies user-selected configs from LEADER_CONFIG_PATH / FOLLOWER_CONFIG_PATH into LeRobot's expected locations under ~/.cache/huggingface/lerobot/calibration/. Recording, teleoperation, and replay all call this before starting. New features that drive a robot must do the same.
Hardcoded for SO-101 leader/follower arms (so101_leader, so101_follower). Adding another robot type requires touching every feature module's config construction (search for SO101LeaderConfig / SO101FollowerConfig).
Phone-as-camera streaming requires HTTPS. See PHONE_CAMERA_SETUP.md and HTTPS_SETUP.md. Self-signed certs go in certs/. The default lelab-fullstack does not start with HTTPS — for phone cameras run uvicorn manually with --ssl-keyfile certs/key.pem --ssl-certfile certs/cert.pem.