{"record":{"id":"a74551b1b1fa85f4","repo":"nexu-io/open-design","slug":"unsupported-side-for-participant-speaker-side","errorCode":null,"errorMessage":"Unsupported side for participant {speaker}: {side}","messagePattern":"Unsupported side for participant (.+?): (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py","lineNumber":138,"sourceCode":"\ndef validate_config(config: dict) -> None:\n    if config[\"container\"] not in ALLOWED_CONTAINERS:\n        raise ValueError(f\"Unsupported container: {config['container']}\")\n    if config[\"avatarMode\"] not in ALLOWED_AVATAR_MODES:\n        raise ValueError(f\"Unsupported avatarMode: {config['avatarMode']}\")\n    if config[\"deviceFrame\"] not in ALLOWED_DEVICE_FRAMES:\n        raise ValueError(f\"Unsupported deviceFrame: {config['deviceFrame']}\")\n    if config[\"nicknameMode\"] not in ALLOWED_NICKNAME_MODES:\n        raise ValueError(f\"Unsupported nicknameMode: {config['nicknameMode']}\")\n    if config[\"deliveryFormat\"] not in ALLOWED_DELIVERY_FORMATS:\n        raise ValueError(f\"Unsupported deliveryFormat: {config['deliveryFormat']}\")\n    if config[\"container\"] == \"none\" and config[\"deviceFrame\"] == \"iphone-dynamic-island\":\n        raise ValueError(\"container=none does not support deviceFrame=iphone-dynamic-island; use deviceFrame=none or choose an app container\")\n\n    for speaker, participant in config.get(\"participants\", {}).items():\n        side = participant.get(\"side\")\n        if side and side not in {\"left\", \"right\"}:\n            raise ValueError(f\"Unsupported side for participant {speaker}: {side}\")\n        preset_key = participant.get(\"preset\")\n        if preset_key and preset_key not in PRESET_KEYS:\n            raise ValueError(f\"Unsupported preset for participant {speaker}: {preset_key}\")\n        upload_path = participant.get(\"uploadPath\")\n        if participant.get(\"uploadAsset\"):\n            raise ValueError(f\"Participant {speaker} config must use uploadPath, not uploadAsset\")\n        if config[\"avatarMode\"] == \"preset\" and upload_path:\n            raise ValueError(f\"avatarMode=preset does not allow uploadPath for participant {speaker}\")\n        if config[\"avatarMode\"] == \"upload\" and not upload_path:\n            raise ValueError(f\"avatarMode=upload requires uploadPath for participant {speaker}\")\n\n\ndef auto_avatar_for_participant(participant_index: int, used_avatar_keys: set[str]) -> str:\n    preferred = [*PRESET_KEYS[participant_index:], *PRESET_KEYS[:participant_index]]\n    for avatar_key in preferred:\n        if avatar_key not in used_avatar_keys:\n            return avatar_key\n    return PRESET_KEYS[participant_index % len(PRESET_KEYS)]","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py#L120-L156","documentation":"Raised by validate_config during participant iteration when a participant's `side` field is present and not in {'left','right'}. Note the transcript-level SIDE_MAP also accepts Chinese tokens (左/右), but the config-level participant side check is stricter: only the English 'left'/'right' strings are accepted in participant config.","triggerScenarios":"User config defines `\"participants\": {\"Alice\": {\"side\": \"l\"}}`, or `{\"side\": \"top\"}`, `{\"side\": \"left-side\"}`, `{\"side\": \"左\"}` (Chinese token not accepted at config layer), or a typo. Any truthy non-{left,right} value triggers it; null/missing side is allowed (the `if side and ...` guard skips empty).","commonSituations":"Abbreviating side ('l'/'r'); using the Chinese token that works in transcripts but not in config; wanting a centered speaker; copy-paste introducing extra text; boolean or numeric side.","solutions":["Set each participant's side to exactly 'left' or 'right' (lowercase English).","To leave side unspecified (auto), omit the field or set it to null — the `if side and ...` guard only validates non-empty values.","If you used Chinese tokens from a transcript, translate them: 左 -> left, 右 -> right.","Pre-validate: `for p in c.get('participants',{}).values(): assert not p.get('side') or p['side'] in {'left','right'}`."],"exampleFix":"// before\n{\"participants\": {\"Alice\": {\"side\": \"l\"}}}\n# -> ValueError: Unsupported side for participant Alice: l\n\n// after\n{\"participants\": {\"Alice\": {\"side\": \"left\"}}}","handlingStrategy":"validation","validationCode":"for speaker, participant in config.get(\"participants\", {}).items():\n    side = participant.get(\"side\")\n    if side and side not in {\"left\", \"right\"}:\n        raise SystemExit(\n            f\"Participant {speaker} side must be 'left' or 'right'; got {side!r}. \"\n            \"(Omit side to leave it auto.)\"\n        )","typeGuard":"def is_participant_side(value) -> bool:\n    return not value or value in {\"left\", \"right\"}","tryCatchPattern":"try:\n    validate_config(config)\nexcept ValueError as exc:\n    raise SystemExit(f\"Config error: {exc}\") from exc","preventionTips":["Use only lowercase 'left'/'right' in participant config (Chinese tokens are transcript-only).","Omit side to leave it auto-assigned rather than guessing.","Validate all participants in one pass before rendering.","Document the layer difference: transcript SIDE_MAP accepts 左/右, participant config does not."],"tags":["config","validation","chat-overlay","participants"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}