moeru-ai/airi · error · Error
Textures not found
Error message
Textures not found
What it means
Thrown by createFakeSettings when the uploaded files contain no .png entries. After pinning the single moc file, the synthesized Cubism4ModelSettings needs a Textures array; the heuristic simply collects all files ending in '.png'. An archive whose textures are .webp/.jpg, or where textures are missing entirely, fails here.
Solutions
- Re-export or re-package the model keeping the original .png texture atlases (the fake-settings path only recognizes .png)
- Include the real .model3.json so FileReferences.Textures from the editor export is used rather than the png-only heuristic
- Pre-scan uploads: if no file ends with '.png', reject with guidance before calling the loader
- Check for nested zips and extract them first so texture paths are visible to the loader
Example fix
// before
const settings = FileLoader.createSettings(files) // 'Textures not found'
// after
const hasPng = files.some(f => f.name.toLowerCase().endsWith('.png'))
if (!hasPng) {
throw new Error('No .png textures found. Keep original PNG texture files, or include the model\'s .model3.json so texture paths are read from it.')
} Defensive patterns
Strategy: validation
Validate before calling
const hasPng = files.some(f => f.name.toLowerCase().endsWith('.png'))
if (!hasPng) throw new Error('Model needs its original .png texture atlases') Try / catch
catch (e) { if (e.message === 'Textures not found') { showUserError('No PNG textures in this archive; re-export with original textures'); return } throw e } Prevention
- Don't convert Live2D texture atlases to webp/jpg
- Include the settings file so texture paths come from FileReferences
- Check nested zips before loading
When it happens
Trigger: Uploading a ZIP whose textures were converted to webp/jpg (breaking the .endsWith('.png') filter); textures inside a nested zip so their paths never appear in the flat file list; an archive with the moc but textures accidentally excluded; folder upload filtered to only some file types.
Common situations: Optimization pipelines that recompress texture atlases to webp; texture folders named with uppercase .PNG (case-sensitive endsWith); partial downloads of large model archives.
Related errors
- Archive entry not found
- Cannot find file
- Empty settings file
- Expected a JSON object in
- Expected exactly one moc file, got
AI-assisted analysis of moeru-ai/airi@9c213115f8 (2026-08-18).
Data as JSON: /api/errors/6677f695f2a20da5.
Report an issue: GitHub.
Appendix: source
Thrown at packages/stage-ui-live2d/src/utils/live2d-zip-loader.ts:176
}
// copy and modified from https://github.com/guansss/live2d-viewer-web/blob/f6060b2ce52c2e26b6b61fa903c837fe343f72d1/src/app/upload.ts#L81-L142
function createFakeSettings(files: string[]): ModelSettings {
const mocFiles = files.filter(file => isMocFile(file))
if (mocFiles.length !== 1) {
const fileList = mocFiles.length ? `(${mocFiles.map(f => `"${f}"`).join(',')})` : ''
throw new Error(`Expected exactly one moc file, got ${mocFiles.length} ${fileList}`)
}
const mocFile = mocFiles[0]
const modelName = basename(mocFile).replace(/\.moc3?/, '')
const textures = files.filter(f => f.endsWith('.png'))
if (!textures.length) {
throw new Error('Textures not found')
}
const motions = files.filter(f => f.endsWith('.mtn') || f.endsWith('.motion3.json'))
const physics = files.find(f => f.includes('physics'))
const pose = files.find(f => f.includes('pose'))
const settings = new Cubism4ModelSettings({
url: `${modelName}.model3.json`,
Version: 3,
FileReferences: {
Moc: mocFile,
Textures: textures,
Physics: physics,
Pose: pose,
Motions: motions.length
? {
'': motions.map(motion => ({ File: motion })),
}View on GitHub (pinned to 9c213115f8)