stablyai/orca · error · Error

Animation " " declares frame durations but frames.

Error message

Animation "${name}" declares ${anim.frameDurationsMs.length} frame durations but ${anim.frames} frames.

What it means

Thrown at pet.ts:357-360 when an animation provides frameDurationsMs (optional per pet.ts:107) but its array length does not equal anim.frames. Each frame must have exactly one duration, so a mismatch would either drop frames or index out of range during playback.

Solutions

  1. Make frameDurationsMs have exactly anim.frames entries (one duration per frame).
  2. If you do not need per-frame timing, omit frameDurationsMs and rely on fps.
  3. Re-export the bundle after fixing pet.json.

Example fix

// before — 6 frames, 3 durations
{ "animations": { "idle": { "row": 0, "frames": 6, "frameDurationsMs": [100,200,300] } } }
// after
{ "animations": { "idle": { "row": 0, "frames": 6, "frameDurationsMs": [100,100,100,100,100,100] } } }
Defensive patterns

Strategy: validation

Validate before calling

function validateFrameDurations(animations: Record<string, {frames:number;frameDurationsMs?:number[]}>) {
  for (const [name, a] of Object.entries(animations)) {
    if (a.frameDurationsMs && a.frameDurationsMs.length !== a.frames) {
      throw new Error(`animation ${name}: ${a.frameDurationsMs.length} durations vs ${a.frames} frames`)
    }
  }
}

Try / catch

try { await importPetBundle(p) }
catch (e) { if (e instanceof Error && /frame durations but/.test(e.message)) { /* align durations array to frame count */ } else throw e }

Prevention

When it happens

Trigger: An animation sets frames: 6 but frameDurationsMs: [100, 200, 300] (3 entries). Runs only when frameDurationsMs is present (line 357 guards with &&).

Common situations: Author added/removed a frame but forgot to update the durations array; durations were copied from a template with a different frame count.

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/b04f872eb68b31b6. Report an issue: GitHub.

Appendix: source

Thrown at src/main/ipc/pet.ts:358

      if (dims.width % fw !== 0 || dims.height % fh !== 0) {
        throw new Error(
          `Spritesheet ${dims.width}×${dims.height} is not a clean multiple of frame ${fw}×${fh}.`
        )
      }
      const columns = dims.width / fw
      const rows = dims.height / fh
      if (manifest.animations) {
        for (const [name, anim] of Object.entries(manifest.animations)) {
          if (anim.row >= rows) {
            throw new Error(`Animation "${name}" references row ${anim.row} but sheet has ${rows}.`)
          }
          if (anim.frames > columns) {
            throw new Error(
              `Animation "${name}" has ${anim.frames} frames but sheet only has ${columns} columns.`
            )
          }
          if (anim.frameDurationsMs && anim.frameDurationsMs.length !== anim.frames) {
            throw new Error(
              `Animation "${name}" declares ${anim.frameDurationsMs.length} frame durations but ${anim.frames} frames.`
            )
          }
        }
        if (manifest.defaultAnimation && !manifest.animations[manifest.defaultAnimation]) {
          throw new Error(`defaultAnimation "${manifest.defaultAnimation}" not in animations.`)
        }
      }
      sprite = {
        frameWidth: fw,
        frameHeight: fh,
        columns,
        rows,
        sheetWidth: dims.width,
        sheetHeight: dims.height,
        fps: manifest.fps ?? 8,
        defaultAnimation: manifest.defaultAnimation,
        animations: manifest.animations

View on GitHub (pinned to 1136503c6a)