moeru-ai/airi · error · Error
Spine ZIP must contain at least one skeleton+atlas pair
Error message
Spine ZIP must contain at least one skeleton+atlas pair
What it means
After loading the ZIP and reading all atlas texts, `loadSpineZip` runs `detectAllSpineLayouts`. If it returns zero variants it throws `Spine ZIP must contain at least one skeleton+atlas pair`. This is the loader-level guard (vs. `detectSpineLayout` which throws on the first-pair path) — it means atlas files may exist but none could be paired with a same-basename skeleton.
Source
Thrown at packages/stage-ui-spine/src/utils/spine-zip-loader.ts:249
const entries: Record<string, string> = {}
const atlasTexts: Record<string, string> = {}
const blobUrls: Record<string, string> = {}
// Pass 1: inventory file paths and read atlas text bodies.
await Promise.all(Object.keys(archive.files).map(async (name) => {
const entry = archive.files[name]
if (entry.dir)
return
entries[name] = name
if (isAtlasPath(name))
atlasTexts[name] = await entry.async('string')
}))
const variants = detectAllSpineLayouts(entries, atlasTexts)
if (variants.length === 0)
throw new Error('Spine ZIP must contain at least one skeleton+atlas pair')
const layout = variants[0].layout
// Pass 2: materialize assets for ALL variants.
// NOTICE:
// Spine's Downloader has a heuristic for rawDataUris: if the value doesn't
// contain ".", it treats it as a data: URI and calls atob(). In Electron,
// blob URLs are `blob:null/<uuid>` (no dots), so the Downloader fails.
// Even with data: URIs, Spine's atob-based decode can corrupt binary data.
// We store raw decoded data (Uint8Array / string) alongside blob URLs and
// monkey-patch the Downloader's download methods to serve from memory.
// Removal condition: Spine ships a Blob-aware or buffer-aware loader.
const rawData: Record<string, Uint8Array | string> = {}
// Collect all unique paths across all variants.
const allTexturePaths = new Set<string>()
const allSkeletonPaths = new Set<string>()
const allAtlasPaths = new Set<string>()
for (const v of variants) {View on GitHub (pinned to 27111382b4)
Solutions
- Keep each model's `.atlas` and `.skel`/`.json` in the same folder with matching basenames.
- Re-export from Spine with a consistent layout, or move files in the archive so basenames and folders align.
- Call `detectAllSpineLayouts` yourself to see what pairings are possible and adjust.
- Verify the skeleton file is present and not corrupted.
Example fix
// before const assets = await loadSpineZip(blob) // throws: no pair // after // ensure hero.atlas and hero.skel share a folder and basename // assets/hero.atlas + assets/hero.skel const assets = await loadSpineZip(blob)
Defensive patterns
Strategy: validation
Validate before calling
const variants = detectAllSpineLayouts(entries, atlasTexts)
if (!variants.length) throw new Error('No usable skeleton+atlas pair in archive')
await loadSpineZip(blob) Type guard
function hasSkeletonAtlasPair(entries: Record<string, string>, atlasText: Record<string, string>): boolean {
return detectAllSpineLayouts(entries, atlasText).length > 0
} Try / catch
try {
await loadSpineZip(blob)
} catch (e) {
if (e instanceof Error && e.message.includes('skeleton+atlas pair')) {
// list entries, re-prompt user
} else throw e
} Prevention
- Keep each model's atlas and skeleton in the same folder with matching basenames.
- Avoid splitting assets and skeletons across directories.
- Re-export from Spine with a consistent layout.
When it happens
Trigger: An archive that has `.atlas` files but no `.skel`/`.json` with a matching basename in the same folder; atlas and skeleton in different directories (`assets/hero.atlas` + `skeletons/hero.skel`); atlas with one basename and skeleton with another; only textures present.
Common situations: Folder reorganization that separated atlas from skeleton; renamed skeleton or atlas; Spine export that placed assets and skeleton in different subfolders; partial download missing the skeleton.
Related errors
- Spine ZIP must contain a .skel or .json skeleton file paired
- Spine ZIP must contain a .atlas (or .atlas.txt) file
- 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/22a5a95a3692b191.
Report an issue: GitHub.