nexu-io/open-design · error · SystemExit

{state} row needs {frame_count} frames, found {len(files)} u

Error message

{state} row needs {frame_count} frames, found {len(files)} under {root}

What it means

Thrown by compose_from_frames() when the number of frame images found for a given animation state/row is less than that row's required frame_count (defined in ROW_SPECS). Each state has a fixed slot count (e.g. idle=6, running-right=8); too few frames would leave blank cells and break the animation, so the script aborts.

Source

Thrown at skills/hatch-pet/scripts/compose_atlas.py:101

            )
        source = source.resize((ATLAS_WIDTH, ATLAS_HEIGHT), Image.Resampling.LANCZOS)

    atlas = Image.new("RGBA", (ATLAS_WIDTH, ATLAS_HEIGHT), (0, 0, 0, 0))
    for _state, row, frame_count in ROW_SPECS:
        for column in range(frame_count):
            left = column * CELL_WIDTH
            top = row * CELL_HEIGHT
            cell = source.crop((left, top, left + CELL_WIDTH, top + CELL_HEIGHT))
            atlas.alpha_composite(cell, (left, top))
    return atlas


def compose_from_frames(root: Path) -> Image.Image:
    atlas = Image.new("RGBA", (ATLAS_WIDTH, ATLAS_HEIGHT), (0, 0, 0, 0))
    for state, row, frame_count in ROW_SPECS:
        files = find_row_frames(root, state, row)
        if len(files) < frame_count:
            raise SystemExit(
                f"{state} row needs {frame_count} frames, found {len(files)} under {root}"
            )
        for column, frame_path in enumerate(files[:frame_count]):
            with Image.open(frame_path) as frame:
                paste_centered(atlas, frame, row, column)
    return atlas


def save_outputs(atlas: Image.Image, output: Path, webp_output: Path | None) -> None:
    output.parent.mkdir(parents=True, exist_ok=True)
    atlas.save(output)
    if webp_output is not None:
        webp_output.parent.mkdir(parents=True, exist_ok=True)
        atlas.save(webp_output, format="WEBP", lossless=True, quality=100, method=6)


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)

View on GitHub (pinned to 5be4028344)

Solutions

  1. Ensure each state has at least its required frame count: idle=6, running-right=8, running-left=8, waving=4, jumping=5, failed=8, waiting=6, running=6, review=6.
  2. Name frames so find_row_frames matches them: a per-state subfolder named after the state, or filenames like <state>_001.png / <state>-001.png.
  3. Confirm all frames use a recognized suffix (.png, .webp, .jpg, .jpeg).

Example fix

# before: frames-root/idle/ has only 3 pngs (needs 6)
# after: add idle_004.png, idle_005.png, idle_006.png under frames-root/idle/
Defensive patterns

Strategy: validation

Validate before calling

ROW_SPECS = [("idle",0,6),("running-right",1,8),("running-left",2,8),("waving",3,4),("jumping",4,5),("failed",5,8),("waiting",6,6),("running",7,6),("review",8,6)]
SUFFIXES = {".png",".webp",".jpg",".jpeg"}
def assert_frames_complete(root):
    from pathlib import Path
    root = Path(root)
    for state, row, count in ROW_SPECS:
        sub = root / state
        found = [p for p in sub.iterdir() if p.suffix.lower() in SUFFIXES] if sub.is_dir() else [p for p in root.glob(f"{state}_*") if p.suffix.lower() in SUFFIXES]
        if len(found) < count:
            raise SystemExit(f"{state} needs {count}, found {len(found)}")

Prevention

When it happens

Trigger: Run compose_atlas.py --frames-root <dir> where one of the state subdirectories (or matching glob patterns like idle_*, running-right_*, row-N) contains fewer images than ROW_SPECS demands for that state.

Common situations: Incomplete frame export (a render job finished early); files named with a pattern the finder does not recognize (see find_row_frames candidates); mixing extensions not in IMAGE_SUFFIXES (.bmp, .tiff); frames split across multiple folders.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/ca4f5f0c61e1bcc0. Report an issue: GitHub.