{"record":{"id":"ca4f5f0c61e1bcc0","repo":"nexu-io/open-design","slug":"state-row-needs-frame-count-frames-found-len","errorCode":null,"errorMessage":"{state} row needs {frame_count} frames, found {len(files)} under {root}","messagePattern":"(.+?) row needs (.+?) frames, found (.+?) under (.+?)","errorType":"exception","errorClass":"SystemExit","httpStatus":null,"severity":"error","filePath":"skills/hatch-pet/scripts/compose_atlas.py","lineNumber":101,"sourceCode":"            )\n        source = source.resize((ATLAS_WIDTH, ATLAS_HEIGHT), Image.Resampling.LANCZOS)\n\n    atlas = Image.new(\"RGBA\", (ATLAS_WIDTH, ATLAS_HEIGHT), (0, 0, 0, 0))\n    for _state, row, frame_count in ROW_SPECS:\n        for column in range(frame_count):\n            left = column * CELL_WIDTH\n            top = row * CELL_HEIGHT\n            cell = source.crop((left, top, left + CELL_WIDTH, top + CELL_HEIGHT))\n            atlas.alpha_composite(cell, (left, top))\n    return atlas\n\n\ndef compose_from_frames(root: Path) -> Image.Image:\n    atlas = Image.new(\"RGBA\", (ATLAS_WIDTH, ATLAS_HEIGHT), (0, 0, 0, 0))\n    for state, row, frame_count in ROW_SPECS:\n        files = find_row_frames(root, state, row)\n        if len(files) < frame_count:\n            raise SystemExit(\n                f\"{state} row needs {frame_count} frames, found {len(files)} under {root}\"\n            )\n        for column, frame_path in enumerate(files[:frame_count]):\n            with Image.open(frame_path) as frame:\n                paste_centered(atlas, frame, row, column)\n    return atlas\n\n\ndef save_outputs(atlas: Image.Image, output: Path, webp_output: Path | None) -> None:\n    output.parent.mkdir(parents=True, exist_ok=True)\n    atlas.save(output)\n    if webp_output is not None:\n        webp_output.parent.mkdir(parents=True, exist_ok=True)\n        atlas.save(webp_output, format=\"WEBP\", lossless=True, quality=100, method=6)\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=__doc__)","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/skills/hatch-pet/scripts/compose_atlas.py#L83-L119","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","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.","Confirm all frames use a recognized suffix (.png, .webp, .jpg, .jpeg)."],"exampleFix":"# before: frames-root/idle/ has only 3 pngs (needs 6)\n# after: add idle_004.png, idle_005.png, idle_006.png under frames-root/idle/","handlingStrategy":"validation","validationCode":"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)]\nSUFFIXES = {\".png\",\".webp\",\".jpg\",\".jpeg\"}\ndef assert_frames_complete(root):\n    from pathlib import Path\n    root = Path(root)\n    for state, row, count in ROW_SPECS:\n        sub = root / state\n        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]\n        if len(found) < count:\n            raise SystemExit(f\"{state} needs {count}, found {len(found)}\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Name frames with the state prefix and a zero-padded index (e.g. idle_001.png).","Keep all frames in recognized formats (.png/.webp/.jpg/.jpeg)."],"tags":["image-processing","hatch-pet","atlas"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}