Skip to content

feat(datasets): replay an episode on the follower arm - #129

Open
nicolas-rabault wants to merge 2 commits into
mainfrom
feat/replay-on-robot
Open

nicolas-rabault wants to merge 2 commits into
mainfrom
feat/replay-on-robot

Conversation

@nicolas-rabault

@nicolas-rabault nicolas-rabault commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

What

  • Play on robot in the episode viewer, shown only when a calibrated robot is selected. It replays the episode's recorded actions on the follower at the recording's rate and starts the footage from the first frame at 1x alongside. While the arm moves the button becomes Stop robot, and reopening the viewer picks a running replay back up so it stays stoppable.
  • lelab/replay.py: an in-process take on lerobot-replay, built like teleoperation. The follower connects synchronously so a failure reaches the user, the worker owns the disconnect and the active flag, and a threading.Event stops it. Actions are read from the episode's data files through episode_media.
  • Routes: POST /start-replay, POST /stop-replay, GET /replay-status.
  • Recording, teleoperation, inference and replay all drive the follower, so each now checks and claims it under one shared follower_lock and refuses to start while another runs. The CLAUDE.md state model says so.

Behaviour to know

Same as lerobot-replay: the arm moves to the episode's first pose at full speed (no ramp), and torque is released when the replay ends or is stopped.

Testing

  • pytest 278 passed (mutual exclusion in both directions, connect failure, a stop that times out, stop while idle), tsc shows only the 5 errors already on main, eslint clean on the changed files.
  • vitest: ReplayButton tests cover restoring a running replay without a selected robot, starting, Stop holding until the status shows the arm released, a failed poll and a refused start. Reintroducing either regression from the review fails them.
  • Worker run against a fake follower: the right episode, frames in order at the dataset fps, stop mid-replay.
  • One real replay on an SO-101 follower: connect, calibration, playback, disconnect.
  • Button states checked in the browser with the replay endpoints intercepted.

Copilot AI balanced review requested due to automatic review settings October 7, 2026 14:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Cross-mode starts remain racy, and several paths can remove the Stop control while replay still owns the arm.

Review effort: Balanced
Findings: 2 High severity · 1 Medium severity · 1 Low severity

Open (4)
What changed in this PR

Adds episode replay on the SO-101 follower arm from the dataset viewer.

Changes:

  • Adds replay backend lifecycle, API routes, and cross-mode checks.
  • Adds replay controls with synchronized footage playback.
  • Adds tests, documentation, and rebuilt frontend assets.
File Description
lelab/​replay.py Implements replay lifecycle and hardware control.
lelab/​server.py Exposes replay endpoints.
lelab/​record.py Checks replay state before recording.
lelab/​teleoperate.py Checks replay state before teleoperation.
lelab/​rollout.py Checks replay state before inference.
tests/​test_replay.py Tests schemas, exclusion, and failure paths.
frontend/​src/​components/​dataset/​ReplayButton.tsx Adds replay UI and status polling.
frontend/​src/​components/​dataset/​EpisodeViewer.tsx Integrates replay with footage playback.
frontend/​src/​components/​dataset/​EpisodeViewer.test.tsx Mocks the new replay control.
frontend/​dist/​index.html References rebuilt assets.
frontend/​dist/​assets/​index-BhoUX80z.css Removes the previous stylesheet.
frontend/​dist/​assets/​index-Bg74I3Au.css Adds the rebuilt stylesheet.
CLAUDE.md Documents replay and mutual exclusion.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lelab/replay.py Outdated
Comment on lines +67 to +70
with _state_lock:
if replay_active:
return {"success": False, "message": "A replay is already running"}
if _record.recording_active:
Comment thread lelab/replay.py
Comment on lines +133 to +137
worker.join(timeout=5.0)
if worker.is_alive():
logger.warning("Replay worker did not exit within 5s")

return {"success": True, "message": "Replay stopped"}
return () => clearInterval(timer);
}, [phase, baseUrl, fetchWithHeaders, toast]);

if (!robot?.is_clean) return null;
Comment on lines +42 to +45
useEffect(() => {
apiRequest<ReplayStatus>(baseUrl, fetchWithHeaders, "/replay-status")
.then((status) => {
if (status.replay_active) setPhase("playing");
The episode viewer gets a "Play on robot" button, shown only once a
calibrated robot is selected. It sends the episode's recorded actions to
the follower at the recording's rate and starts the footage from the
first frame at 1x so both can be compared; while the arm moves the
button becomes "Stop robot", and it picks a running replay back up when
the viewer is opened again.

replay.py is an in-process take on lerobot-replay, built like
teleoperation: the follower connects synchronously so a failure reaches
the user, the actions are read from the episode's data files, and the
worker owns the disconnect and the active flag. Recording,
teleoperation, inference and replay all drive the follower, so each
refuses to start while another runs.
…lease

Recording, teleoperation, inference and replay each checked the others'
flags under their own lock, so two concurrent starts could both claim
the follower. They now check and claim under a shared follower_lock.

Stopping a replay reports a failure when the worker has not released
the arm within 5s, and the button only turns back to Play once the
status poll sees the arm released. A running replay keeps its Stop
button even when no calibrated robot is selected anymore.

ReplayButton gets component tests for restoring a running replay,
starting, stopping, a failed poll and a refused start.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants