Skip to content
Open
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
19 changes: 16 additions & 3 deletions lelab/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from pydantic import BaseModel

from lerobot.configs.dataset import DatasetRecordConfig
from lerobot.configs.video import RGBEncoderConfig
from lerobot.datasets import LeRobotDataset
from lerobot.robots.so_follower import SO101FollowerConfig

Expand Down Expand Up @@ -183,6 +184,10 @@ def create_record_config(request: RecordingRequest) -> RecordConfig:
tags=with_lelab_tag(request.tags) if request.push_to_hub else None,
private=request.private,
streaming_encoding=request.streaming_encoding,
# libsvtav1 (lerobot's default) is CPU-heavy enough on this machine to starve
# the motor bus read loop mid-recording ("no status packet" ConnectionError,
# see 2026-07-15 crashes). h264/ultrafast measured ~11x faster to encode here.
rgb_encoder=RGBEncoderConfig(vcodec="h264", preset="ultrafast"),
)

# Create the main record config
Expand Down Expand Up @@ -309,11 +314,15 @@ def recording_worker():
"robot_type": getattr(dataset.meta, "robot_type", "Unknown robot"),
}
except Exception as e:
logger.exception("Recording session failed")
logger.exception("Recording session failed: %r", e)
current_phase = "error"
if recording_start_time:
session_end_elapsed_seconds = int(time.time() - recording_start_time)
last_recording_info = {"success": False, "error": str(e)}
last_recording_info = {
"success": False,
"error": str(e) or repr(e),
"dataset_repo_id": request.dataset_repo_id,
}
finally:
if current_phase != "error":
current_phase = "completed"
Expand Down Expand Up @@ -419,7 +428,11 @@ def handle_recording_status() -> dict[str, Any]:
"rerecord_episode": recording_active
and current_phase == "recording", # Only during recording phase
},
"message": "Recording session failed with error - check logs"
"message": (
f"Recording failed: {last_recording_info['error']}"
if last_recording_info and last_recording_info.get("error")
else "Recording session failed with error - check logs"
)
if current_phase == "error"
else (
"Recording session has ended - stop polling"
Expand Down
24 changes: 24 additions & 0 deletions lelab/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@
from starlette.responses import Response
from starlette.types import Scope

# Windows consoles often use a legacy code page (cp1252) that can't encode the
# emoji used in log/print output; an unhandled UnicodeEncodeError inside an
# endpoint then turns into a 500 and hides the real error from the frontend.
# Replace unencodable characters instead of crashing.
for _stream in (sys.stdout, sys.stderr):
if _stream is not None and hasattr(_stream, "reconfigure"):
with contextlib.suppress(Exception):
_stream.reconfigure(errors="replace")

from . import datasets as dataset_browser

# Import our custom calibration functionality
Expand Down Expand Up @@ -998,6 +1007,18 @@ def _windows_cameras() -> list[dict[str, Any]]:
frontend match each index to the browser's ``MediaDeviceInfo.label`` for the
live preview. Falls back to generic names if pygrabber is unavailable.
"""
# pygrabber uses COM, which must be initialized per-thread. FastAPI serves
# sync endpoints from a thread pool whose threads never call CoInitialize,
# so enumeration fails intermittently depending on which thread handles the
# request. Initialize COM here and balance with CoUninitialize.
import ctypes

com_initialized = False
try:
hr = ctypes.windll.ole32.CoInitialize(None)
com_initialized = hr in (0, 1) # S_OK or S_FALSE (already initialized)
except Exception:
pass
try:
from pygrabber.dshow_graph import FilterGraph

Expand All @@ -1007,6 +1028,9 @@ def _windows_cameras() -> list[dict[str, Any]]:
import cv2

return _generic_cv2_cameras(cv2.CAP_DSHOW)
finally:
if com_initialized:
ctypes.windll.ole32.CoUninitialize()
return [{"index": i, "name": name, "available": True} for i, name in enumerate(names)]


Expand Down