moeru-ai/airi · error · Error
Spine ZIP must contain a .atlas (or .atlas.txt) file
Error message
Spine ZIP must contain a .atlas (or .atlas.txt) file
What it means
`detectAllSpineLayouts(entries, atlasText)` first filters `entries` for atlas candidates via `isAtlasPath`. If there are zero `.atlas` / `.atlas.txt` files it throws `Spine ZIP must contain a .atlas (or .atlas.txt) file` before attempting skeleton matching. An atlas file is a hard prerequisite for Spine loading.
Source
Thrown at packages/stage-ui-spine/src/utils/spine-zip-loader.ts:119
* 3. Return the first matched pair as the primary layout.
*/
export function detectSpineLayout(entries: Record<string, string>, atlasText: Record<string, string>): SpineModelLayout {
const variants = detectAllSpineLayouts(entries, atlasText)
if (variants.length === 0)
throw new Error('Spine ZIP must contain a .skel or .json skeleton file paired with a .atlas')
return variants[0].layout
}
/**
* Detects all skeleton+atlas pairs in a ZIP, returning them as named
* variants. Useful for ZIPs containing multiple outfits/characters in
* separate folders.
*/
export function detectAllSpineLayouts(entries: Record<string, string>, atlasText: Record<string, string>): SpineModelVariant[] {
const allFiles = Object.keys(entries)
const atlasCandidates = allFiles.filter(isAtlasPath)
if (atlasCandidates.length === 0)
throw new Error('Spine ZIP must contain a .atlas (or .atlas.txt) file')
const variants: SpineModelVariant[] = []
const usedAtlases = new Set<string>()
// First pass: match each atlas with a same-basename skeleton.
for (const candidate of atlasCandidates) {
const baseName = stripExt(stripExt(basename(candidate)))
const dir = dirname(candidate)
const binaryPath = `${dir}${baseName}${SKELETON_BINARY_EXT}`
const jsonPath = `${dir}${baseName}${SKELETON_JSON_EXT}`
let skeletonPath: string | undefined
let skeletonFormat: SpineModelLayout['skeletonFormat'] = 'binary'
if (entries[binaryPath] !== undefined) {
skeletonPath = binaryPath
skeletonFormat = 'binary'View on GitHub (pinned to 27111382b4)
Solutions
- Ensure the archive contains a `.atlas` (or `.atlas.txt`) file alongside the skeleton.
- Re-export from Spine enabling 'Create atlas' in the texture packer settings.
- If the atlas was renamed, restore the `.atlas` / `.atlas.txt` extension.
- Confirm the archive is actually a Spine export before routing here.
Example fix
// before
const variants = detectAllSpineLayouts(entries, atlasText) // throws, no .atlas
// after
const atlasFiles = Object.keys(entries).filter(isAtlasPath)
if (!atlasFiles.length) throw new Error('Archive is missing a .atlas file; re-export with texture packer')
const variants = detectAllSpineLayouts(entries, atlasText) Defensive patterns
Strategy: validation
Validate before calling
const atlasFiles = Object.keys(entries).filter(isAtlasPath)
if (!atlasFiles.length)
throw new Error('Archive is missing a .atlas file')
detectAllSpineLayouts(entries, atlasText) Type guard
function zipHasAtlas(entries: Record<string, string>): boolean {
return Object.keys(entries).some(isAtlasPath)
} Try / catch
try {
detectAllSpineLayouts(entries, atlasText)
} catch (e) {
if (e instanceof Error && e.message.includes('.atlas (or .atlas.txt)')) {
// tell user the atlas is missing; re-export
} else throw e
} Prevention
- Ensure Spine export produces a .atlas / .atlas.txt alongside the skeleton.
- Confirm the archive is a Spine export before routing here.
- Watch for renamed atlas files.
When it happens
Trigger: An archive with a `.skel`/`.json` skeleton and textures but no `.atlas`; the atlas file renamed to `.txt` without the `.atlas.txt` dual extension; only a `.json` atlas reference that is not a real atlas; an archive produced without the texture-pack atlas export.
Common situations: Spine export configured to skip the atlas; the atlas file lost in transfer; a non-Spine archive (Live2D/MMD) loaded through the Spine path; case mismatch where `isAtlasPath` did not recognize the extension.
Related errors
- Spine ZIP must contain a .skel or .json skeleton file paired
- Spine ZIP must contain at least one skeleton+atlas pair
- Empty settings file: ${url}
- Expected exactly one moc file, got ${mocFiles.length} ${file
- MMD ZIP must contain a .pmx or .pmd model file
AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12).
Data as JSON: /api/errors/d85b7d415d533f3e.
Report an issue: GitHub.