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 built bundle in frontend/dist/ is committed and shipped inside the Python wheel 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 again, so committed dist/ doesn't matter to it).
Install (editable, requires Python ≥3.10):
pip install -e .lerobot is pinned to a release tag on huggingface/lerobot (currently v0.6.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 tag 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 adjust call sites accordingly.
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 :8080When frontend/** (excluding frontend/dist/**) changes on main, .github/workflows/build_frontend.yml auto-rebuilds frontend/dist/ and commits it back. You can still build locally before committing if you want to test the production bundle, but it's no longer required. lelab --dev serves directly from Vite, no rebuild needed.
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 (wraps
lerobot.record.record); patcheslerobot.common.utils.control_utilskeyboard listener so frontend buttons replace arrow-key controls. - teleoperate.py — leader→follower teleoperation (wraps
lerobot.teleoperate). - 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).
- 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 — features are mutually exclusive in practice (you can't teleoperate and record simultaneously) but this is not enforced in code.
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.