Skip to content

feat: resume recording into an existing dataset across sessions - #74

Open
ZouzouWP wants to merge 1 commit into
huggingface:mainfrom
ZouzouWP:feat/dataset-resume
Open

feat: resume recording into an existing dataset across sessions#74
ZouzouWP wants to merge 1 commit into
huggingface:mainfrom
ZouzouWP:feat/dataset-resume

Conversation

@ZouzouWP

@ZouzouWP ZouzouWP commented Jul 17, 2026

Copy link
Copy Markdown

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 current lerobot. 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

  • 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.
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
Loading

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.

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
nicolas-rabault self-requested a review August 3, 2026 15:22

@nicolas-rabault nicolas-rabault left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Very nice!

One blocker inline: the resume path doesn't call repair_local_dataset(), which silently
corrupts an already-pushed dataset.

Comment thread lelab/record.py
Comment on lines +605 to +615
# 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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

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