fix(smolvla2): instantiate CameraConfig subclasses from JSON dicts

``--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) <noreply@anthropic.com>
This commit is contained in:
Pepijn
2026-05-12 14:39:28 +02:00
parent e0fa957569
commit 41095e3cc3
@@ -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(<type>)`` (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