From 41095e3cc3e12c7f42fc8216fba349c060a3e2b6 Mon Sep 17 00:00:00 2001 From: Pepijn Date: Tue, 12 May 2026 14:39:28 +0200 Subject: [PATCH] fix(smolvla2): instantiate CameraConfig subclasses from JSON dicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``--robot.cameras`` parses the JSON into ``dict[str, dict]``, but ``RobotConfig`` expects ``dict[str, CameraConfig]`` — each inner value must be the actual ``CameraConfig`` subclass instance for the chosen backend (e.g. ``OpenCVCameraConfig``). Passing raw dicts blew up in ``RobotConfig.__post_init__`` with ``AttributeError: 'dict' object has no attribute 'width'`` when it iterated cameras and tried to read attributes. Look up the right subclass per-camera by its ``"type"`` field via ``CameraConfig.get_choice_class(...)`` (mirroring the lazy-import dance we already do for ``RobotConfig``: eagerly walk ``lerobot.cameras``'s submodules so the registry is populated before lookup). Construct an instance with the rest of the dict's fields. On an unknown camera type, raise a clean ``ValueError`` listing the available choices. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scripts/lerobot_smolvla2_runtime.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/lerobot/scripts/lerobot_smolvla2_runtime.py b/src/lerobot/scripts/lerobot_smolvla2_runtime.py index 1acb9ff25..00c63473c 100644 --- a/src/lerobot/scripts/lerobot_smolvla2_runtime.py +++ b/src/lerobot/scripts/lerobot_smolvla2_runtime.py @@ -494,11 +494,52 @@ def _build_robot( kwargs["id"] = robot_id if robot_cameras_json: try: - kwargs["cameras"] = json.loads(robot_cameras_json) + cameras_raw = json.loads(robot_cameras_json) except json.JSONDecodeError as exc: raise ValueError( f"--robot.cameras must be a JSON object, got {robot_cameras_json!r}: {exc}" ) from exc + # ``RobotConfig`` expects ``cameras: dict[str, CameraConfig]`` — + # each inner value must be an actual ``CameraConfig`` subclass + # instance, not a raw dict. Look up the matching subclass via + # ``CameraConfig.get_choice_class()`` (registered by + # ``@CameraConfig.register_subclass`` decorators on each camera + # backend's config) and instantiate it. Mirror the lazy-import + # pattern from above so the registry is populated. + import lerobot.cameras as _cameras_pkg # noqa: PLC0415 + from lerobot.cameras import CameraConfig # noqa: PLC0415 + + for _modinfo in pkgutil.iter_modules(_cameras_pkg.__path__): + if _modinfo.name.startswith("_"): + continue + try: + importlib.import_module(f"lerobot.cameras.{_modinfo.name}") + except Exception as exc: # noqa: BLE001 + logger.debug("could not import lerobot.cameras.%s: %s", _modinfo.name, exc) + + cameras: dict[str, Any] = {} + for cam_name, cam_dict in cameras_raw.items(): + if not isinstance(cam_dict, dict): + raise ValueError( + f"camera {cam_name!r} value must be a dict, got {cam_dict!r}" + ) + cam_dict = dict(cam_dict) # don't mutate caller's parsed JSON + cam_type = cam_dict.pop("type", None) + if cam_type is None: + raise ValueError( + f"camera {cam_name!r} is missing a 'type' field " + f"(e.g. 'opencv', 'intelrealsense')" + ) + try: + cam_cls = CameraConfig.get_choice_class(cam_type) + except KeyError as exc: + available = sorted(CameraConfig._choice_registry.keys()) + raise ValueError( + f"camera {cam_name!r}: unknown type {cam_type!r}. " + f"Available choices: {available}" + ) from exc + cameras[cam_name] = cam_cls(**cam_dict) + kwargs["cameras"] = cameras if robot_max_relative_target: # Accept either a bare float (uniform cap) or a JSON object # (per-motor cap). Matches ``RobotConfig.max_relative_target``'s