moeru-ai/airi · error · Error

MMD ZIP must contain a .pmx or .pmd model file

Error message

MMD ZIP must contain a .pmx or .pmd model file

What it means

`loadMMDZip(file)` loads the ZIP, lists non-directory entries, and runs `detectMMDVariants(paths)`. If `variants.length === 0` it throws `MMD ZIP must contain a .pmx or .pmd model file` — the archive has no recognizable MMD model mesh file at the top level of detection.

Source

Thrown at packages/stage-ui-mmd/src/utils/mmd-zip-loader.ts:120

    return byBasename.get(base) ?? requested
  }
}

/**
 * Loads an MMD model packaged as a ZIP (the common distribution format: a
 * `.pmx`/`.pmd` plus its texture/toon/sphere-map files) into blob URLs ready
 * for {@link createMMDLoaderContext}.
 *
 * Call `dispose()` on unmount or reload to revoke the blob URLs.
 */
export async function loadMMDZip(file: File | Blob | ArrayBuffer): Promise<MMDLoadedAssets> {
  const zip = new JSZip()
  const archive = await zip.loadAsync(file)

  const paths = Object.keys(archive.files).filter(name => !archive.files[name].dir)
  const variants = detectMMDVariants(paths)
  if (variants.length === 0)
    throw new Error('MMD ZIP must contain a .pmx or .pmd model file')

  const blobUrls: Record<string, string> = {}
  await Promise.all(paths.map(async (path) => {
    const entry = archive.files[path]
    if (!entry)
      return
    const blob = await entry.async('blob')
    blobUrls[path] = URL.createObjectURL(blob)
  }))

  const variant = variants[0]
  const modelBlobUrl = blobUrls[variant.modelPath]

  return {
    variant,
    variants,
    modelBlobUrl,
    blobUrls,

View on GitHub (pinned to 27111382b4)

Solutions

  1. Confirm the ZIP contains at least one `.pmx` or `.pmd` model file.
  2. Route the archive to the correct loader (Live2D for `.model3.json`, Spine for `.skel`+`.atlas`).
  3. Re-export from the MMD authoring tool ensuring the mesh file is included.
  4. Inspect `Object.keys(zip.files)` after `loadAsync` to see what was actually detected.

Example fix

// before
const assets = await loadMMDZip(blob)  // throws if no pmx/pmd

// after
const zip = await new JSZip().loadAsync(blob)
const hasModel = Object.keys(zip.files).some(n => n.endsWith('.pmx') || n.endsWith('.pmd'))
if (!hasModel) throw new Error('ZIP has no .pmx/.pmd model file')
const assets = await loadMMDZip(blob)
Defensive patterns

Strategy: validation

Validate before calling

const zip = await new JSZip().loadAsync(file)
const names = Object.keys(zip.files).filter(n => !zip.files[n].dir)
if (!names.some(n => n.endsWith('.pmx') || n.endsWith('.pmd')))
  throw new Error('ZIP has no .pmx/.pmd model file')
await loadMMDZip(file)

Type guard

function zipHasMmdModel(names: string[]): boolean {
  return names.some(n => n.endsWith('.pmx') || n.endsWith('.pmd'))
}

Try / catch

try {
  await loadMMDZip(blob)
} catch (e) {
  if (e instanceof Error && e.message.includes('must contain a .pmx or .pmd')) {
    // suggest the user pick an MMD archive
  } else throw e
}

Prevention

When it happens

Trigger: A ZIP that contains textures/audio but no `.pmx` or `.pmd`; a ZIP whose model file uses an unsupported extension (`.pmm`, `.vpd`, `.vmd` motion-only); a corrupt ZIP that `loadAsync` parsed but whose file list is incomplete; a Live2D or Spine archive loaded through the MMD path.

Common situations: User selected the wrong archive type (Live2D/Spine instead of MMD); motion-only MMD package; model file renamed to `.pmc`/typo; ZIP produced by a tool that nested the model in a way `detectMMDVariants` skips.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/9444b798134778ed. Report an issue: GitHub.