{"record":{"id":"1d80107f062fab96","repo":"calesthio/OpenMontage","slug":"face-choose-items-must-be-strings-or-objects","errorCode":null,"errorMessage":"face_choose items must be strings or objects","messagePattern":"face_choose items must be strings or objects","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"tools/avatar/kling_lip_sync.py","lineNumber":410,"sourceCode":"        if inputs.get(\"face_choose\"):\n            raw = inputs[\"face_choose\"]\n            if isinstance(raw, dict):\n                raw = [raw]\n            if not isinstance(raw, list):\n                raise ValueError(\"face_choose must be a list of face choice objects\")\n            normalized: list[dict[str, Any]] = []\n            for item in raw:\n                if isinstance(item, str):\n                    normalized.append({\"face_id\": item})\n                elif isinstance(item, dict):\n                    if not (item.get(\"face_id\") or item.get(\"id\")):\n                        raise ValueError(\"face_choose items must include face_id\")\n                    record = dict(item)\n                    if \"face_id\" not in record and record.get(\"id\"):\n                        record[\"face_id\"] = record.pop(\"id\")\n                    normalized.append(record)\n                else:\n                    raise ValueError(\"face_choose items must be strings or objects\")\n            return normalized\n        if inputs.get(\"face_id\"):\n            return [{\"face_id\": str(inputs[\"face_id\"])}]\n        return []\n\n    def _face_selection(\n        self,\n        faces: list[dict[str, Any]],\n        inputs: dict[str, Any],\n    ) -> tuple[list[dict[str, Any]], dict[str, Any]]:\n        explicit = self._normalize_face_choose(inputs)\n        if explicit:\n            return explicit, {\n                \"selection_method\": \"user_selected\",\n                \"selection_reason\": \"face_choose or face_id was provided\",\n                \"selected_face\": explicit,\n            }\n        if len(faces) == 1:","sourceCodeStart":392,"sourceCodeEnd":428,"githubUrl":"https://github.com/calesthio/OpenMontage/blob/95e1c3d0ab93482159818560f6a8c8e866b9139f/tools/avatar/kling_lip_sync.py#L392-L428","documentation":"Raised by KlingLipSyncTool._normalize_face_choose when an entry in the face_choose list is neither a string nor a dict. The tool accepts face_choose as a single dict, a list of face-id strings, or a list of objects carrying face_id/id plus per-face timing fields; any other element type (int, None, tuple, nested list) is rejected before the Kling API is called.","triggerScenarios":"Calling kling_lip_sync with inputs like face_choose=[123, \"abc\"], face_choose=[None], or face_choose=[[\"face1\"]] (a nested list). The loop in _normalize_face_choose hits the else branch for the first non-str/non-dict element and raises ValueError immediately.","commonSituations":"Passing face indexes instead of face ids (face_choose=[0, 1]), copying a JSON payload where face ids were numbers, or a template/agent emitting null entries for undetected faces.","solutions":["Use face-id strings: face_choose=[\"face_1\", \"face_2\"]","Or use objects with face_id or id keys: face_choose=[{\"face_id\": \"face_1\", \"sound_insert_time\": 0}]","If you only have numeric face indexes, map them to the face_id values returned by identify_face first","A single dict is also accepted and auto-wrapped: face_choose={\"face_id\": \"face_1\"}"],"exampleFix":"# before\ninputs = {\"face_choose\": [0, 1]}\n\n# after\ninputs = {\"face_choose\": [\"face_1\", \"face_2\"]}","handlingStrategy":"validation","validationCode":"def valid_face_choose(fc):\n    if fc is None:\n        return True\n    if isinstance(fc, dict):\n        fc = [fc]\n    if not isinstance(fc, list):\n        return False\n    return all(isinstance(i, str) or (isinstance(i, dict) and (i.get(\"face_id\") or i.get(\"id\"))) for i in fc)\n\nif not valid_face_choose(inputs.get(\"face_choose\")):\n    raise ValueError(\"face_choose must contain face-id strings or objects with face_id\")","typeGuard":"def is_face_choose_item(v) -> bool:\n    return isinstance(v, str) or (isinstance(v, dict) and bool(v.get(\"face_id\") or v.get(\"id\")))","tryCatchPattern":"try:\n    tool.run(inputs)\nexcept ValueError as e:\n    if \"face_choose\" in str(e):\n        fix_face_choose_and_retry(inputs)  # normalize items to {\"face_id\": str}\n    raise","preventionTips":["Always build face_choose entries as {\"face_id\": <id-from-identify_face>}","Never put numeric indexes or nulls into face_choose","Validate the list shape before calling the tool"],"tags":["kling","lip-sync","input-validation","face-choose"],"backgroundTag":null,"analyzedSha":"95e1c3d0ab93482159818560f6a8c8e866b9139f","analyzedAt":"2026-08-15T06:31:20.014Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}