{"record":{"id":"83ea6c55417de225","repo":"nexu-io/open-design","slug":"avatarmode-mixed-requires-at-least-one-upload-path","errorCode":null,"errorMessage":"avatarMode=mixed requires at least one upload path","messagePattern":"avatarMode=mixed requires at least one upload path","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py","lineNumber":214,"sourceCode":"        side = message[\"side\"] or participant[\"side\"]\n        if side != participant[\"side\"]:\n            raise ValueError(f\"Participant {speaker} appears on both {participant['side']} and {side}; use one side per participant\")\n        avatar_key = participant[\"avatarKey\"]\n        if message[\"avatar\"] and message[\"avatar\"] != participant[\"avatarKey\"] and not configured_participant(speaker, config).get(\"preset\"):\n            raise ValueError(f\"Participant {speaker} has conflicting transcript avatar hints: {participant['avatarKey']} and {message['avatar']}; set a config preset to override\")\n        messages.append(\n            {\n                \"id\": f\"msg-{index + 1}\",\n                \"speaker\": speaker,\n                \"text\": message[\"text\"].strip(),\n                \"side\": side,\n                \"avatarKey\": avatar_key,\n                \"appearAt\": start + index * gap,\n                \"highlight\": bool(message[\"highlight\"]),\n            }\n        )\n    if config[\"avatarMode\"] == \"mixed\" and not any(participant.get(\"uploadPath\") for participant in participants.values()):\n        raise ValueError(\"avatarMode=mixed requires at least one upload path\")\n    duration = start + max(len(messages) - 1, 0) * gap + int(meta[\"hold\"])\n    runtime_output = DELIVERY_TO_OUTPUT[config[\"deliveryFormat\"]]\n    scene_config = {key: value for key, value in config.items() if key != \"participants\"}\n    return {\n        \"title\": meta[\"title\"],\n        \"timestamp\": meta[\"time\"],\n        \"durationInFrames\": duration,\n        \"timing\": {\"start\": start, \"gap\": gap, \"hold\": int(meta[\"hold\"])},\n        \"sceneConfig\": {**scene_config, \"output\": runtime_output},\n        \"participants\": list(participants.values()),\n        \"messages\": messages,\n    }\n\n\ndef slugify(text: str) -> str:\n    lowered = text.strip().lower()\n    return re.sub(r\"[^a-z0-9\\u4e00-\\u9fff]+\", \"-\", lowered).strip(\"-\") or \"speaker\"\n","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/skills/chat-motion-overlay/scripts/build_chat_overlay_spec.py#L196-L232","documentation":"Thrown at the end of build_spec() when avatarMode is 'mixed' but none of the resolved participants carry an uploadPath. Mixed mode's contract is that at least one participant uses an uploaded avatar while others may use presets; if all end up preset-based, the mode is wrong.","triggerScenarios":"avatarMode='mixed' with a config where no participant has uploadPath, or where upload-bearing participants never appear in the transcript so they are never instantiated. The run_test_matrix.py case 'invalid_mixed_without_upload' reproduces this.","commonSituations":"Setting avatarMode='mixed' as a 'safe default' without actually supplying an upload; configuring an upload participant under a speaker name that is misspelled relative to the transcript; deleting the only upload participant but leaving the mode as mixed.","solutions":["Give at least one participant (that actually appears in the transcript) an 'uploadPath' pointing at an existing image.","Switch avatarMode to 'preset' if no uploads are needed.","Switch avatarMode to 'upload' if every participant should have an uploadPath."],"exampleFix":"// before\n{\"avatarMode\": \"mixed\", \"participants\": {\"闺蜜\": {\"preset\": \"female-bunny-pink\"}, \"老婆\": {\"preset\": \"female-cat-orange\"}}}\n// after\n{\"avatarMode\": \"mixed\", \"participants\": {\"闺蜜\": {\"uploadPath\": \"/me.png\"}, \"老婆\": {\"preset\": \"female-cat-orange\"}}}","handlingStrategy":"validation","validationCode":"def mixed_needs_an_upload(config, transcript_speakers):\n    if config.get(\"avatarMode\") != \"mixed\":\n        return\n    has_upload = any(config.get(\"participants\", {}).get(s, {}).get(\"uploadPath\") for s in transcript_speakers)\n    if not has_upload:\n        raise ValueError(\"avatarMode=mixed requires at least one transcript speaker with uploadPath\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only choose 'mixed' when you genuinely mix presets and uploads.","Ensure the upload-bearing participant actually appears in 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"}