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

Thrown by loadMMDZip when detectMMDVariants finds no .pmx or .pmd entry among the archive's non-directory file paths. MMD models must ship the binary mesh in one of those two formats; a ZIP with only textures, motions, or a nested archive yields zero variants and fails before any blob URL materialization matters.

Solutions

  1. Download a fresh copy of the model and re-zip including the .pmx/.pmd at the expected location
  2. Pre-scan uploads before calling loadMMDZip: const hasModel = Object.keys(zip.files).some(n => /\.pm[xd]$/i.test(n))

Example fix

// before
const assets = await loadMMDZip(file) // throws 'MMD ZIP must contain a .pmx or .pmd model file'

// after
import JSZip from 'jszip'
const zip = await new JSZip().loadAsync(file)
const modelEntries = Object.keys(zip.files).filter(n => !zip.files[n].dir && /\.pm[xd]$/i.test(n))
if (modelEntries.length === 0) {
  throw new Error('This ZIP has no .pmx/.pmd model file. Zip the model folder that contains the .pmx (or .pmd) plus its textures.')
}
const assets = await loadMMDZip(file)
Defensive patterns

Strategy: validation

Validate before calling

import JSZip from 'jszip'
const zip = await new JSZip().loadAsync(file)
const paths = Object.keys(zip.files).filter(n => !zip.files[n].dir)
if (!paths.some(n => /\.pm[xd]$/i.test(n))) {
  throw new Error('No .pmx/.pmd in this ZIP — include the model file and re-zip')
}

Type guard

const hasMMDModelEntry = (names: string[]): boolean => names.some(n => /\.pm[xd]$/i.test(n))

Try / catch

catch (e) { if (e.message.includes('.pmx or .pmd')) { showUserError('This ZIP has no MMD model file'); return } throw e }

Prevention

When it happens

Trigger: Uploading a texture-only or VMD-motion-only ZIP; a .zip inside the .zip containing the actual model; the model file having an unusual extension case (.PMX passes only if the filter lowercases — verify with detectMMDVariants) or being replaced by a git-lfs pointer file that kept a different name.

Common situations: Users zipping the wrong folder (textures instead of model root); archives auto-converted by upload pipelines; downloading an 'all files' bundle where the .pmx sits in a subfolder the variant detector does not scan

Related errors


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

Appendix: 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 677329427f)