{"record":{"id":"8dc7a589c21828fb","repo":"moeru-ai/airi","slug":"ziploader-failed-to-extract-cdi-exp-metadata","errorCode":null,"errorMessage":"[ZipLoader] Failed to extract CDI/EXP metadata:","messagePattern":"\\[ZipLoader\\] Failed to extract CDI/EXP metadata:","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/stage-ui-live2d/src/utils/live2d-zip-loader.ts","lineNumber":74,"sourceCode":"    // Find and collect expression files\n    const expPaths = filePaths.filter(f => f.toLowerCase().endsWith('.exp3.json'))\n    if (expPaths.length > 0) {\n      const expFiles: Array<{ name: string, fileName: string, data: unknown }> = []\n      for (const expPath of expPaths) {\n        const expText = await reader.file(expPath)!.async('text')\n        const baseName = expPath.split('/').pop()?.replace('.exp3.json', '') || expPath\n        expFiles.push({\n          name: baseName,\n          fileName: expPath,\n          data: JSON.parse(expText),\n        })\n      }\n      metadataSettings._expFiles = expFiles\n      console.info('[ZipLoader] Extracted', expFiles.length, 'expression files')\n    }\n  }\n  catch (e) {\n    console.warn('[ZipLoader] Failed to extract CDI/EXP metadata:', e)\n  }\n\n  return settings\n}\n\n/**\n * Normalizes Live2D model settings JSON before upstream path resolution.\n *\n * Before:\n * - `{ \"FileReferences\": { \"Physics\": null } }`\n *\n * After:\n * - `{ \"FileReferences\": {} }`\n */\nfunction sanitizeModelSettingsText(text: string): string {\n  const json = JSON.parse(text) as Record<string, unknown>\n  const refs = json.FileReferences\n","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/moeru-ai/airi/blob/9c213115f8bd0fff9e6eabab02b077ac32da21be/packages/stage-ui-live2d/src/utils/live2d-zip-loader.ts#L56-L92","documentation":"The custom ZipLoader.createSettings in live2d-zip-loader scans the model archive for .cdi3.json and .exp3.json entries, reads them, and JSON.parse's their contents into settings._cdiData / settings._expFiles. The whole extraction is wrapped in try/catch: any failure (missing-but-referenced entry, unreadable file, invalid JSON, a non-null assertion hitting a null file) is warned here and the settings are returned without the metadata — the model still loads, minus expressions/CDI data.","triggerScenarios":"An archive whose cdi3/exp3 entries contain malformed JSON, are zero-byte, have paths with legacy codepage encoding that don't match, or where reader.file(path) returns null for a path found by suffix but not actually present as a file.","commonSituations":"Hand-repacked or VTube-Studio-exported zips with corrupted expression JSON; case-mismatched file names; mojibake entry names on non-ASCII expression files.","solutions":["Open the model zip and validate every .cdi3.json / .exp3.json parses as JSON (e.g. with a quick node script or unzip + jq)","This is non-fatal: if expressions are optional for your use case, proceed — the model renders without them","Re-export the model from Live2D Cubism with expression files properly included, or fix the broken entries and re-zip preserving UTF-8 names"],"exampleFix":"// before (inside createSettings)\nconst expText = await reader.file(expPath)!.async('text')\nexpFiles.push({ name: baseName, fileName: expPath, data: JSON.parse(expText) })\n\n// after (per-file tolerance: one bad entry doesn't drop all metadata)\nconst entry = reader.file(expPath)\nif (!entry)\n  continue\ntry {\n  const parsed = JSON.parse(await entry.async('text'))\n  expFiles.push({ name: baseName, fileName: expPath, data: parsed })\n}\ncatch (e) {\n  console.warn('[ZipLoader] Skipping bad expression file:', expPath, e)\n}","handlingStrategy":"fallback","validationCode":"// validate before relying on expression metadata\nfor (const path of Object.keys(zip.files).filter(f => f.toLowerCase().endsWith('.exp3.json'))) {\n  const entry = zip.file(path)\n  if (!entry)\n    continue\n  JSON.parse(await entry.async('text')) // throws early on corrupted entries\n}","typeGuard":"function isExpMetadata(value: unknown): value is Array<{ name: string, fileName: string, data: unknown }> {\n  return Array.isArray(value) && value.every(item => typeof item?.name === 'string' && typeof item?.fileName === 'string')\n}","tryCatchPattern":null,"preventionTips":["Validate model archives at import time (the repo's validateLive2DZip) so corrupted exp/cdi entries are rejected with a clear message","Treat expression metadata as optional: design features to degrade without it","Preserve UTF-8 entry names when re-zipping models"],"tags":["live2d","zip","json-parse","model-metadata","expressions"],"backgroundTag":"model-metadata-parse-failed","analyzedSha":"9c213115f8bd0fff9e6eabab02b077ac32da21be","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}