Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions lelab/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@

logger = logging.getLogger(__name__)

LOCAL_DATASET_NAMESPACE = "local"

# Global variables for recording state
recording_active = False
recording_thread: threading.Thread | None = None
Expand Down Expand Up @@ -91,6 +93,22 @@ class DatasetInfoRequest(BaseModel):
dataset_repo_id: str


def _normalize_dataset_repo_id(repo_id: str) -> str:
"""Return a LeRobot-compatible dataset ID while preserving Hub IDs.

The landing page deliberately accepts a bare dataset name while the user is
not authenticated. LeRobot still requires an ``owner/name`` ID for local
recordings, so local-only recordings use a reserved local namespace.
"""
if "/" in repo_id:
namespace, name = repo_id.split("/", 1)
name = re.sub(r"[^A-Za-z0-9._-]", "_", name)
return f"{namespace}/{name}"

name = re.sub(r"[^A-Za-z0-9._-]", "_", repo_id)
return f"{LOCAL_DATASET_NAMESPACE}/{name}"


def _platform_backend():
"""Pin the OpenCV backend per-platform so the index→camera mapping matches
what the /available-cameras thumbnails were captured with. cv2.CAP_ANY can
Expand Down Expand Up @@ -247,12 +265,7 @@ def handle_start_recording(request: RecordingRequest) -> dict[str, Any]:
# recording over an invalid character. HF repo names allow only
# [A-Za-z0-9._-]; everything else becomes "_".
if request.dataset_repo_id:
if "/" in request.dataset_repo_id:
namespace, name = request.dataset_repo_id.split("/", 1)
name = re.sub(r"[^A-Za-z0-9._-]", "_", name)
request.dataset_repo_id = f"{namespace}/{name}"
else:
request.dataset_repo_id = re.sub(r"[^A-Za-z0-9._-]", "_", request.dataset_repo_id)
request.dataset_repo_id = _normalize_dataset_repo_id(request.dataset_repo_id)
# Stamp the repo_id with a timestamp (matches lerobot-record CLI behavior),
# so each session lands in a unique directory and the frontend gets the
# final id back in the response and status payload.
Expand Down
12 changes: 12 additions & 0 deletions tests/test_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ def test_recording_request_rejects_missing_required_fields() -> None:
RecordingRequest()


def test_normalize_dataset_repo_id_adds_local_namespace_for_bare_name() -> None:
from lelab.record import _normalize_dataset_repo_id

assert _normalize_dataset_repo_id("test recording") == "local/test_recording"


def test_normalize_dataset_repo_id_preserves_hub_namespace() -> None:
from lelab.record import _normalize_dataset_repo_id

assert _normalize_dataset_repo_id("samuel-hills/test recording") == "samuel-hills/test_recording"


def test_recording_status_handler_exposes_state_fields() -> None:
from lelab.record import handle_recording_status

Expand Down
Loading