feat: resume recording into an existing dataset across sessions - #74
feat: resume recording into an existing dataset across sessions#74ZouzouWP wants to merge 1 commit into
Conversation
Recording a full dataset rarely happens in one sitting. Before this, adding more episodes to an existing dataset later meant starting a brand new one and merging them by hand — nothing stopped you from silently mixing sessions with different FPS, cameras, or task wording into a broken combined dataset.
## What changed
- `DatasetPicker` lists existing local/Hub datasets with their episode count, and offers a "Resume" action next to "Create new".
- Resuming pre-fills FPS, cameras, robot type, and the last task description from the dataset's own metadata (via a new `tasks` field on `/dataset-info`), and **blocks recording** if the current camera/robot setup doesn't match what the dataset was originally recorded with — this is exactly the mismatch that used to fail silently.
- An optional episode-count target (stored in `localStorage`, not the dataset itself) drives a progress bar across sessions, e.g. "14 / 45 episodes".
- Backend fix: `LeRobotDataset.resume()` was being called with `root=None`, which current `lerobot` rejects. Now resolves the same default local directory `LeRobotDataset.create()` uses.
```mermaid
sequenceDiagram
participant U as User
participant L as Landing page
participant API as LeLab backend
participant DS as LeRobotDataset
U->>L: Click "Resume" on a dataset
L->>API: POST /dataset-info {repo_id}
API->>DS: LeRobotDataset(repo_id)
DS-->>API: fps, cameras, robot_type, tasks
API-->>L: pre-fill form
Note over L: Blocks start if current setup mismatches
U->>L: Confirm, start recording
L->>API: POST /start-recording {resume: true, repo_id}
API->>DS: LeRobotDataset.resume(repo_id, root=resolved_root)
DS-->>API: existing episodes preserved
API-->>L: new episodes appended after the last one
```
## Testing
Validated end-to-end on the physical SO-101: episodes appended to a 5-episode dataset across two separate sessions on different days, episode count and video/frame continuity confirmed on the Hub afterward.
nicolas-rabault
left a comment
There was a problem hiding this comment.
Very nice!
One blocker inline: the resume path doesn't call repair_local_dataset(), which silently
corrupts an already-pushed dataset.
| # LeRobotDataset.resume() refuses root=None (it would write into the | ||
| # revision-safe Hub snapshot cache), so resolve the same default local | ||
| # directory that LeRobotDataset.create() uses. | ||
| resume_root = cfg.dataset.root | ||
| if resume_root is None: | ||
| from lerobot.utils.constants import HF_LEROBOT_HOME | ||
|
|
||
| resume_root = HF_LEROBOT_HOME / cfg.dataset.repo_id | ||
| dataset = LeRobotDataset.resume( | ||
| cfg.dataset.repo_id, | ||
| root=cfg.dataset.root, | ||
| root=resume_root, |
There was a problem hiding this comment.
repair_local_dataset() is missing here, and this is the exact case lelab/dataset_repair.py exists for. A recording interrupted before finalize() has no meta/episodes/, so load_episodes() raises FileNotFoundError, LeRobotDatasetMetadata.__init__ swallows it and calls _pull_from_repo(allow_patterns="meta/").
Because you now pass an explicit root, that snapshot downloads into the dataset directory itself rather than the revision-safe cache. For a dataset already pushed to the Hub, the local meta/ is overwritten by the older pushed copy and the writer starts appending from the stale total_episodes and total_frames, so new episodes are numbered over frames that already exist. Nothing raises. For a dataset never pushed it is a 404 partway through starting.
repair_local_dataset(cfg.dataset.repo_id) immediately before the resume() call fixes both, and matches what handle_get_dataset_info and handle_upload_dataset already do. It is a no-op when the dataset is already readable, so it costs nothing on the clean path.
Worth knowing while you are in here: handle_get_dataset_info returns the cached last_recording_info verbatim when the repo id matches, and after a failed session that cache is {"success": false, ...}. So the pre-fill request the modal makes never reaches the repair either. Fixing it here covers both, since this is the path that actually writes.
Related issue
Relates to issues #11, PR #14 already proposes an implementation via a dedicated Edit Dataset page; this PR takes a different approach — resuming directly from the existing dataset-picker/create flow via a new "Resume" action, plus a fix for
LeRobotDataset.resume(root=None)being rejected by currentlerobot. Happy to reconcile with #14's design if that's the preferred direction — flagging the overlap so it's visible rather than duplicated silently.Recording a full dataset rarely happens in one sitting. Before this, adding more episodes to an existing dataset later meant starting a brand new one and merging them by hand — nothing stopped you from silently mixing sessions with different FPS, cameras, or task wording into a broken combined dataset.
What changed
DatasetPickerlists existing local/Hub datasets with their episode count, and offers a "Resume" action next to "Create new".tasksfield on/dataset-info), and blocks recording if the current camera/robot setup doesn't match what the dataset was originally recorded with — this is exactly the mismatch that used to fail silently.localStorage, not the dataset itself) drives a progress bar across sessions, e.g. "14 / 45 episodes".LeRobotDataset.resume()was being called withroot=None, which currentlerobotrejects. Now resolves the same default local directoryLeRobotDataset.create()uses.sequenceDiagram participant U as User participant L as Landing page participant API as LeLab backend participant DS as LeRobotDataset U->>L: Click "Resume" on a dataset L->>API: POST /dataset-info {repo_id} API->>DS: LeRobotDataset(repo_id) DS-->>API: fps, cameras, robot_type, tasks API-->>L: pre-fill form Note over L: Blocks start if current setup mismatches U->>L: Confirm, start recording L->>API: POST /start-recording {resume: true, repo_id} API->>DS: LeRobotDataset.resume(repo_id, root=resolved_root) DS-->>API: existing episodes preserved API-->>L: new episodes appended after the last oneTesting
Validated end-to-end on the physical SO-101: episodes appended to a 5-episode dataset across two separate sessions on different days, episode count and video/frame continuity confirmed on the Hub afterward.