{"record":{"id":"13dec3d5d78171c6","repo":"nexu-io/open-design","slug":"avatarmode-preset-does-not-allow-uploadpath-for-pa","errorCode":null,"errorMessage":"avatarMode=preset does not allow uploadPath for participant {speaker}","messagePattern":"avatarMode=preset does not allow uploadPath for participant (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py","lineNumber":146,"sourceCode":"    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)]\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\"]","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py#L128-L164","documentation":"Thrown by validate_config() in build_chat_overlay_spec.py when avatarMode is 'preset' but a participant also supplies an 'uploadPath'. Preset mode draws avatars exclusively from the built-in PRESET_KEYS library, so an upload path is contradictory. It is a config-level guard that fires before any transcript parsing or spec building.","triggerScenarios":"Run build_chat_overlay_spec.py --config config.json (or prepare_chat_overlay_bundle.py, which calls load_config) where the JSON has \"avatarMode\": \"preset\" and any entry under \"participants\" contains an \"uploadPath\" key with a truthy value.","commonSituations":"Copying a config block from an 'upload' or 'mixed' example and forgetting to switch avatarMode; iterating on configs and leaving a stale uploadPath; misunderstanding that preset mode forbids uploads entirely rather than ignoring them.","solutions":["Set \"avatarMode\" to \"upload\" or \"mixed\" in the config if you intend to use the uploaded avatar.","Remove the \"uploadPath\" key from every participant entry when you want to keep avatarMode='preset'.","Audit the participants dict: only 'side', 'preset', and (for upload/mixed) 'uploadPath' are honored; drop stray uploadAsset/uploadPath you did not mean to set."],"exampleFix":"// before\n{\"avatarMode\": \"preset\", \"participants\": {\"老婆\": {\"side\": \"right\", \"preset\": \"female-cat-orange\", \"uploadPath\": \"/abs/me.png\"}}}\n// after\n{\"avatarMode\": \"preset\", \"participants\": {\"老婆\": {\"side\": \"right\", \"preset\": \"female-cat-orange\"}}}","handlingStrategy":"validation","validationCode":"def preset_mode_has_no_upload_paths(config: dict) -> None:\n    if config.get(\"avatarMode\") != \"preset\":\n        return\n    offenders = [name for name, p in config.get(\"participants\", {}).items() if p.get(\"uploadPath\")]\n    if offenders:\n        raise ValueError(f\"avatarMode=preset forbids uploadPath; offenders: {offenders}\")\n\npreset_mode_has_no_upload_paths(config)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep a single config schema/source of truth and validate avatarMode vs uploadPath presence in a preflight step.","When reusing a config, switch avatarMode and participants together; never one without the other."],"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"}