moeru-ai/airi · warning

[ZipLoader] Failed to extract CDI/EXP metadata:

Error message

[ZipLoader] Failed to extract CDI/EXP metadata:

What it means

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.

Source

Thrown at packages/stage-ui-live2d/src/utils/live2d-zip-loader.ts:74

    // Find and collect expression files
    const expPaths = filePaths.filter(f => f.toLowerCase().endsWith('.exp3.json'))
    if (expPaths.length > 0) {
      const expFiles: Array<{ name: string, fileName: string, data: unknown }> = []
      for (const expPath of expPaths) {
        const expText = await reader.file(expPath)!.async('text')
        const baseName = expPath.split('/').pop()?.replace('.exp3.json', '') || expPath
        expFiles.push({
          name: baseName,
          fileName: expPath,
          data: JSON.parse(expText),
        })
      }
      metadataSettings._expFiles = expFiles
      console.info('[ZipLoader] Extracted', expFiles.length, 'expression files')
    }
  }
  catch (e) {
    console.warn('[ZipLoader] Failed to extract CDI/EXP metadata:', e)
  }

  return settings
}

/**
 * Normalizes Live2D model settings JSON before upstream path resolution.
 *
 * Before:
 * - `{ "FileReferences": { "Physics": null } }`
 *
 * After:
 * - `{ "FileReferences": {} }`
 */
function sanitizeModelSettingsText(text: string): string {
  const json = JSON.parse(text) as Record<string, unknown>
  const refs = json.FileReferences

View on GitHub (pinned to 9c213115f8)

Solutions

  1. Open the model zip and validate every .cdi3.json / .exp3.json parses as JSON (e.g. with a quick node script or unzip + jq)
  2. This is non-fatal: if expressions are optional for your use case, proceed — the model renders without them
  3. Re-export the model from Live2D Cubism with expression files properly included, or fix the broken entries and re-zip preserving UTF-8 names

Example fix

// before (inside createSettings)
const expText = await reader.file(expPath)!.async('text')
expFiles.push({ name: baseName, fileName: expPath, data: JSON.parse(expText) })

// after (per-file tolerance: one bad entry doesn't drop all metadata)
const entry = reader.file(expPath)
if (!entry)
  continue
try {
  const parsed = JSON.parse(await entry.async('text'))
  expFiles.push({ name: baseName, fileName: expPath, data: parsed })
}
catch (e) {
  console.warn('[ZipLoader] Skipping bad expression file:', expPath, e)
}
Defensive patterns

Strategy: fallback

Validate before calling

// validate before relying on expression metadata
for (const path of Object.keys(zip.files).filter(f => f.toLowerCase().endsWith('.exp3.json'))) {
  const entry = zip.file(path)
  if (!entry)
    continue
  JSON.parse(await entry.async('text')) // throws early on corrupted entries
}

Type guard

function isExpMetadata(value: unknown): value is Array<{ name: string, fileName: string, data: unknown }> {
  return Array.isArray(value) && value.every(item => typeof item?.name === 'string' && typeof item?.fileName === 'string')
}

Prevention

When it happens

Trigger: 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.

Common situations: Hand-repacked or VTube-Studio-exported zips with corrupted expression JSON; case-mismatched file names; mojibake entry names on non-ASCII expression files.

Related errors


AI-assisted analysis of moeru-ai/airi@9c213115f8 (2026-08-18). Data as JSON: /api/errors/8dc7a589c21828fb. Report an issue: GitHub.