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
- Download a fresh copy of the model and re-zip including the .pmx/.pmd at the expected location
- 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
- Validate archive contents before invoking the loader
- Extract nested zips before loading
- Ship pmx/pmd together with textures in one archive
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
- [mmd] gaze: no eye bone matched (左目/右目/両目); eye tracking…
- Archive entry not found
- Cannot find file
- Empty settings file
- Expected a JSON object in
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)