{"record":{"id":"c34855e0a5597371","repo":"nexu-io/open-design","slug":"avatarmode-upload-requires-uploadpath-for-particip","errorCode":null,"errorMessage":"avatarMode=upload requires uploadPath for participant {speaker}","messagePattern":"avatarMode=upload requires uploadPath for participant (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py","lineNumber":148,"sourceCode":"    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)]\n\n\ndef configured_participant(speaker: str, config: dict) -> dict:\n    return config.get(\"participants\", {}).get(speaker, {})\n\n\ndef build_spec(parsed: dict, config: dict) -> dict:\n    meta = parsed[\"metadata\"]\n    participants = {}\n    used_participant_ids = set()","sourceCodeStart":130,"sourceCodeEnd":166,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py#L130-L166","documentation":"Thrown by validate_config() when avatarMode is 'upload' but a configured participant lacks an 'uploadPath'. Upload mode requires every participant to supply a real avatar file; there is no preset fallback. It fires at config validation time, before the transcript is processed.","triggerScenarios":"Run build_chat_overlay_spec.py (or prepare_chat_overlay_bundle.py) with \"avatarMode\": \"upload\" and at least one participant entry missing the \"uploadPath\" key. The run_test_matrix.py case 'invalid_upload_missing_side' exercises exactly this path.","commonSituations":"Switching avatarMode to 'upload' without backfilling uploadPath for all speakers; a new speaker appears in the transcript but was not added to the participants config; mixing preset-style participant dicts into an upload-mode config.","solutions":["Add an \"uploadPath\" pointing at an existing image file for every participant named in the transcript.","If only some participants have uploads, switch avatarMode to 'mixed' (which permits a mix of preset and uploaded avatars).","Remove participants without uploads from the config and rely on auto preset assignment by switching to avatarMode='preset'."],"exampleFix":"// before\n{\"avatarMode\": \"upload\", \"participants\": {\"闺蜜\": {\"side\": \"left\", \"uploadPath\": \"/a.png\"}, \"老婆\": {\"side\": \"right\", \"preset\": \"female-cat-orange\"}}}\n// after\n{\"avatarMode\": \"upload\", \"participants\": {\"闺蜜\": {\"side\": \"left\", \"uploadPath\": \"/a.png\"}, \"老婆\": {\"side\": \"right\", \"uploadPath\": \"/b.png\"}}}","handlingStrategy":"validation","validationCode":"def validate_upload_mode(config: dict) -> None:\n    if config.get(\"avatarMode\") != \"upload\":\n        return\n    missing = [name for name, p in config.get(\"participants\", {}).items() if not p.get(\"uploadPath\")]\n    if missing:\n        raise ValueError(f\"avatarMode=upload needs uploadPath for: {missing}\")\n\nvalidate_upload_mode(config)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["If you only have uploads for some participants, use avatarMode='mixed' rather than 'upload'.","Add new speakers to config.participants at the same time you add them to the transcript."],"tags":["config-validation","chat-motion-overlay","avatar-mode"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}