Sprite Gen
sprite-gen builds generic game sprite atlases with a component-row pipeline:
sprite-request.json -> layout guides + prompts -> image-gen state rows
-> chroma alpha -> connected components -> transparent cells
-> sprite-sheet-alpha.png + manifest.json.frame_layout
Use only the component-row pipeline. Do not treat one-shot master sheets, fixed-grid atlas cutting, local drawing, or static fallback as a successful sprite result.
ํ์ ๊ฒ์ดํธ โ AI raw ๋ ์ต์ข
์์
์ด ์๋๋ค (BLOCKING)
์ด ์คํฌ์ ๋ชจ๋ ์ฐ์ถ๋ฌผ์ ์๋ ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ํต๊ณผํด์ผ ํ๋ค. ํ๋๋ผ๋ ์ด๊ธฐ๋ฉด ๊ทธ ๊ฒฐ๊ณผ๋ฌผ์ ์คํจ๋ก ๋ณด๊ณ ํ๋ค:
๋ฆฌ๋ค์ ๊ฒ์ดํธ โ ์ดํ/ํค๋ฅผ ๋ฐ๊ฟ ๋ (BLOCKING)
์คํค๋ง ํคยท์๋ณ์ยท๋ผ๋ฒจ์ ๊ฑธ์ณ ์ดํ๋ฅผ ๋ฐ๊พธ๋ ์์
(pixel_perfect โ pixel_unfake ๋ฅ)์
์ผ๊ด ์นํ์ผ๋ก ์์ํ์ง ์๋๋ค. ์นํ์ ์ด๋ฆ์ ๋ฐ๊พธ์ง๋ง ๊ณ์ฝ์ ์ธต์๋ก ์กด์ฌํ๋ค:
์๋ณ์ โ ํค ๋ฌธ์์ด โ ์ฌ์ฉ์ ๋ผ๋ฒจ(en+ko) โ ๋ฌธ์ ์์ โ --help ๋ฌธ๊ตฌ โ ํ
์คํธ ํ๋์ค
์์๊ฐ ์ ํด์ ธ ์๋ค (ํ๊ท 2026-07-25/26, plan sprite-gen/pixel-unfake-rename โ ์ด ์์๋ฅผ
๊ฑฐ๊พธ๋ก ํด์ ๊ฒ์ฆ์ ๋ฆฌ์ ํธ 3๋ผ์ด๋๊ฐ ๋ฌ๋ค):
Base Lock Gate (Stage 0, BLOCKING)
Identity ownership in the row pipeline:
identity truth = accepted idle anchor
motion truth = layout guide + paired/basis row when needed
base truth = used only to create idle anchors, then removed from row inputs
The full reference-ownership flow (base โ idle anchors โ base ํ๊ธฐ โ basis/paired rows) and the base re-attach ban live in docs/architecture.md ยง5.
A weak idle anchor poisons every state โ proportions, style, and identity drift compound across all rows. Before any row generation, answer the gate question y/n:
Is there an image good enough to lock as the canonical base idle?
The base idle locks only when all of these hold:
- Full body, nothing cropped (head to feet inside frame).
- The final proportions and style the user asked for are already correct in this image (for example SD / chibi head-to-body ratio, pixel look, outline weight). The base defines the target โ do not plan to "fix it later" in the rows.
- For a pixel-art run (
fit.pixel_unfake): the base itself is true pixel art โ a uniform pixel-block grid is measurably present and edges are hard (no anti-aliased fringe). The style contract delegates style authority to this image, so a non-pixel base structurally produces a non-pixel row.
- Identity matches the character sheet / reference (face, hair, markings, palette, props).
- One clear single idle pose, facing the intended camera, readable silhouette at small size.
- Background is a flat clean chroma-ready fill (or trivially keyable).
If the answer is n: generate/iterate base candidates, review each against the criteria above, and re-gate. Do not run prepare_sprite_run.py until a base is locked. "Good enough for now" is not a pass โ drift only grows once the rows start. When the answer is y, that exact file becomes the accepted idle anchor for its direction; keep the original generation so the lock decision is auditable, but do not attach it again after the idle anchors have replaced it as row identity truth.
์คํ ์ธํฐํ๋ฆฌํฐ โ ์ ์ญ python3 ๋ ์ด ์คํฌ์ ์ธํฐํ๋ฆฌํฐ๊ฐ ์๋๋ค (BLOCKING)
์ด ์คํฌ์ ๋ชจ๋ ๋ช
๋ น์ ๋ ํฌ ๋ฃจํธ์ venv ์ธํฐํ๋ฆฌํฐ๋ก ์คํํ๋ค:
export SPRITE_GEN_ROOT=/path/to/sprite-gen
$SPRITE_GEN_ROOT/.venv/bin/python <script.py> ...
-
๋ถํธ์คํธ๋ฉ์ README quickstartยทCI ์ ๊ฐ์ ํ ์ค์ด๋ค โ python3 -m venv .venv && .venv/bin/pip install -e ..
.venv ๊ฐ ์์ผ๋ฉด ๋ง๋ ๋ค ์คํํ๋ค. ๋ค๋ฅธ ๊ฒฝ๋ก์ ๋ง๋ค์๋ค๋ฉด ๊ทธ ์ธํฐํ๋ฆฌํฐ์ ์ ๋๊ฒฝ๋ก๋ก ๋ฐ๊ฟ ์ด๋ค โ
๋ฐ๋๋ฉด ์ ๋๋ ๊ฒ์ ๊ฒฝ๋ก๊ฐ ์๋๋ผ "์ ์ญ python3 ๋ฅผ ์ฐ์ง ์๋๋ค" ๋ ๊ท์น์ด๋ค.
-
์ด์ : ์์กด(Pillow, NumPy)์ SSoT ๋ pyproject.toml ์ด๊ณ , ๊ทธ๊ฒ์ ์ค๋ฌผ๋ก ๋ง๋๋ ๊ณณ์ ์ด venv ํ๋๋ค.
์ ์ญ python3 ๋ $PATH ๊ฐ ๊ทธ๋ ๊ฐ๋ฆฌํค๋ ์๋ฌด ์ธํฐํ๋ฆฌํฐ์ด๊ณ (macOS ์์๋ ๋ณดํต homebrew CPython,
PEP 668 EXTERNALLY-MANAGED), ๊ฑฐ๊ธฐ ๋ ํจํค์ง๋ ์์ผ๋ก ๋ฃ์ ๊ฒ์ด๋ผ ์ ์ธ๊ณผ ์ค๋ฌผ์ด ๊ฐ๋ฆฐ๋ค. ์ค์ ๋ก
๊ทธ๋ ๊ฒ ๊ฐ๋ ธ๋ค: homebrew python3 ์๋ Pillow ๋ง ์๊ณ NumPy ๊ฐ ์์ด์, ํ ๊ฐ๊ฐ ๊น๋ ค ์๋ค๋ ์ด์ ๋ก
๋ค ๊น๋ฆฐ ๊ฒ์ฒ๋ผ ๋ณด์ด๋ ์ํ์๋ค.
-
ํด๋ฐฑ ๊ธ์ง: ".venv ์์ผ๋ฉด ๊ทธ๊ฑฐ, ์์ผ๋ฉด python3" ๊ฐ์ ํด์์ ๋์ง ์๋๋ค (์์น 6).
์์ผ๋ฉด ๋ง๋ค๊ฑฐ๋ ์๋ํ๊ฒ ์คํจํ๋ค โ ์กฐ์ฉํ ๋ค๋ฅธ ์ธํฐํ๋ฆฌํฐ๋ก ๋๋ ๊ฒฝ๋ก๋ ์๋ค.
-
NumPy ๊ฐ ์๋ ์ธํฐํ๋ฆฌํฐ์์๋ ์๋ฌด๊ฒ๋ ์์ํ์ง ์๋๋ค: ์ง์
์ ์ ํจํค์ง import ์์ ์
๋ฉ์ถ๊ณ , ์คํํ ์ธํฐํ๋ฆฌํฐ ๊ฒฝ๋ก์ ์ ๋ถํธ์คํธ๋ฉ ๋ช
๋ น์ ๊ทธ๋๋ก ์ฐ๋๋ค. ์ถ์ถ ๊ฒฝ๋ก๋ ๋ฐ์ดํธ ๋์ผ
๊ณ์ฝ์ ์ง๊ณ ์์ด์ ์์ ํ์ด์ฌ ํด๋ฐฑ์ ์๋ค โ ๋๋ฆฌ๊ฒ๋ผ๋ ๋๋ ๋ ๋ฒ์งธ ๊ตฌํ์ ๋๋ฉด ๊ฐ์ ์ง๋ฌธ์
๋ต์ด ๋์ด ๋๋ค. (๊ฒ์ดํธ sprite_gen/_deps.py, ์ ๊ธ tests/test_numpy_dependency_gate.py)
-
์์ ํ๋ก์ธ์ค๋ ์์ํ๋ค: heal_run ๊ณผ ํ๋ ์ด์
์๋ฒ๋ ์์์ sys.executable ๋ก ๋์ด๋ค.
์ฆ ๋ถ๋ชจ๋ฅผ ์ณ์ ์ธํฐํ๋ฆฌํฐ๋ก ๋์ฐ๋ฉด ๊ทธ ์๋๋ ์๋์ผ๋ก ์ณ๊ณ , ๋ฐ๋๋ก ํ๋ ์ด์
์๋ฒ๋ฅผ ์ ์ญ python3 ๋ก
๋์ฐ๋ฉด ๊ทธ ์๋ฒ๊ฐ ๋ถ๋ฅด๋ ์ฌ์ถ์ถยทcompose ๊ฐ ์ ๋ถ ๊ฐ์ด ํ๋ฆฐ๋ค. ๊ณ ์น ๊ณณ์ ๋์ฐ๋ ์๊ฐ ํ ๊ณณ์ด๋ค.
-
sprite-gen <tool> ์ ์ค์ฌํ๋ ์ฝ์ ์คํฌ๋ฆฝํธ๋ค (anchor, cutout, curation,
recolor, recolor-palette, migrate-breathe, migrate-request โฆ). pip install ์ด venv ์ bin/ ์ ์จ ๋ฃ๊ณ ๊ทธ shebang ์ด ๋ฐ๋ก ๊ทธ venv ์
์ธํฐํ๋ฆฌํฐ๋ฅผ ๊ฐ๋ฆฌํค๋ฏ๋ก, ์ด ํ์์ ์ธํฐํ๋ฆฌํฐ๋ฅผ ๊ณ ๋ฅด๋ ๋ฌธ์ ์์ฒด๊ฐ ์๋ค:
$SPRITE_GEN_ROOT/.venv/bin/sprite-gen <tool> ...
- ์ฌ๊ธฐ์๋ ์ ๋๊ฒฝ๋ก๋ค โ ๋งจ
sprite-gen ์ด PATH ์ ์๋ ๊ฑด venv ๋ฅผ ํ์ฑํํ๊ฑฐ๋ ๊ทธ ํ๊ฒฝ์
์ค์นํ ์
ธ ์์์๋ฟ์ด๋ค. SKILL.mdยทdocs/*.md ๋ ํ์ฑํ ์๋ ์
ธ์์ ์ฝํ๋ฏ๋ก ๋งจ python3 ์
๊ฐ์ ์ด์ ๋ก ๋งจ sprite-gen ๋ ์ฐ์ง ์๋๋ค (README quickstart ๋ ํ์ฑํ๊ฐ ์์ ์์ด ์์ธ).
Script Map
Scripts are explicit pipeline commands, not hidden imports. One job each (stage detail: docs/architecture.md ยง2):
prepare_sprite_run.py โ write sprite-request.json, per-state layout guides, prompts, and empty raw/ + frames/ from request truth.
extract_sprite_row_frames.py โ read raw/<state>.png strips: chroma removal โ connected components โ transparent frame cells + frames/frames-manifest.json.
- ์์ด์ ํธ ์ฃผ๋ ํธํก (์ฌ์ฉ์๊ฐ "์จ์ฌ๊ธฐ ์ ์ฉํด์ ๋ฝ์์ค" ๋ผ๊ณ ๋ง ํด๋ ๋จ): ํธํก์ ์ฌ์ด๋์นด ํ๋๋ผ ๋ทฐ ์์ด๋ ์ผค ์ ์๋ค โ (1)
states.<state>.breathe = {"depth": 0.06, "breaths": 1, "lag": 0.1} ๋ง ์ฐ๋ฉด ๋๋ค. ๊ฒฝ๊ณ๋ ์ ์ธํ์ง ์๋๋ค โ sprite_gen/anatomy.py ๊ฐ ๊ฒ์ถํ๋ค. ํ๋ ์ดํฐ๋ฅผ ๊ฑฐ์น๋ฉด ๊ทธ ๊ฒฐ๊ณผ๊ฐ ์ฌ์ด๋์นด anatomy ์ ์ผ๋ ค์ง๊ณ (GET /api/breathe-anatomy), ๋ทฐ ์์ด ์์ด์ ํธ๊ฐ breathe ๋ง ์ด ๋ฐ์ anatomy ๊ฐ ๋น์ด ์์ด ๊ตฝ๊ธฐ๊ฐ ๋งค๋ฒ ๋ค์ ์ฐ๋ค โ ๊ตฝ๊ธฐ๋ ์ฌ์ด๋์นด์ ์ฐ์ง ์๋๋ค. ์ด๋ ์ชฝ์ด๋ ๋์ํ๋ค. ๊ตฝ๊ธฐ๋ ์ผ๋ฆฐ ๊ฐ์ ์ ๋ขฐํ์ง ์๋๋ค โ ์ธ์ ๋ ์๊ธฐ ๊ธฐ์ค ํ๋ ์์์ ๋ค์ ์ฐ๋ค (์ผ๋ฆฐ ๊ฐ์ ํ๋ ์ดํฐ ํ๋ฆฌ๋ทฐ์ฉ ์บ์๋ค). ์ฌ์ด๋์นด์ ์ด๊ธ๋๋ฉด manifest ์ sidecar_drift ๋ก ๊ฐ์ ์ค์ด ๋ณด๊ณ ํ๋ค. ๊ทธ ์บ์๊ฐ ์์ง ์ ํจํ์ง๋ ๊ธฐ์ค ํ๋ ์์ ์
๋ ฅ ์ง๋ฌธ(์๋ณธ ํ์ผ ์คํฌํยทํฝ์
ํธ์งยท๋ณํยท๋ณ์ข
)์ผ๋ก ํ์ ํ๊ณ , ์ด๊ธ๋๋ฉด ํ๋ ์ดํฐ๊ฐ ํ๋ฆฌ๋ทฐยท์์ ๋ด๋ณด๋ด๊ธฐ๋ฅผ ๊ฑฐ๋ถํ๋ฉฐ ๊ฐฑ์ ํ๋ผ๊ณ ์๋ฆฐ๋ค โ ์กฐ์ฉํ ๋ก์ ์ซ์๋ก ๊ทธ๋ฆฌ์ง ์๋๋ค. ์ฌ๋์ด ํน์ ํ์ ๊ณ ์ ํ๊ณ ์ถ์ ๋๋ง rigid_row ๋ฅผ ์ค๋ค. (์์ด์ ํธ ์ง์ ์ฐ๊ธฐ๋ load_curationโstamp_curation ๋์ฅ ๊ฒฝ๋ก ํ์, ์ด๋ฆฐ ํญ์ ์๋ก๊ณ ์นจ ์๋ด โ ํจ์ ์์ธ: docs/troubleshooting.md), (2) compose_sprite_gif.py/compose_sprite_atlas.py ๊ฐ ์๋์ผ๋ก ๊ตฝ๋๋ค. ๊ฒ์ฆ: gif-manifest ์ breathe.phases. ๊ตฌ splits/amplitude/subpixel ์ ์๋ํ๊ฒ ๊ฑฐ๋ถ๋๋ค โ ์ฎ๊ธฐ๋ ค๋ฉด sprite-gen migrate-breathe <run-dir> --apply.
- ์ ์ง ์์ธ(sit/lie/carry_idle ๋ฑ) ํ ๋ ์ํผ โ ์ ์ง 1์ปท + ๋งํฌ ๋ณต์ @ 4fps + ํ๋ฆฌ์ ํธํก(breaths 3) + ๋ ๋ณด์ด๋ ๋ฐฉํฅ๋ง ๊น๋นก์. ๋ณต์ ์ =
recommended_breathe_frames(breathe) โ 1 (ํธํก๋น SMOOTH_CYCLE_FRAMES=6 ํ๋ ์ ํ๋ณด; breaths 3 โ ์ด 18์ปท) โ ์งง์ ๋ฃจํ(์ 11์ปท)์ ๋ค์ ํธํก์ ์ฐ๊ฒจ๋ฃ์ผ๋ฉด 1px ์์์ด ๋งค ํ๋ ์ ํ ๊ธํด ์ง๋์ผ๋ก ์ฝํ๋ ๊ฑธ ๋ง๋๋ค (maintainer 2026-07-24). ๊น๋นก์์ ์ํ์ค ๋ ๊ทผ์ฒ(๋งจ ๋ ์๋, ๋ค์ ๋๋ฌ rest โฅ2)์ ๋ฐฐ์นํด ๋ฃจํ ์ด์์ ์ ์ ๋ค์ ๋ ์ค๋
์ ์์ค๋ค. ์ค์ธก ๋์ถ ๊ทผ๊ฑฐยท์๋ ์ ์ฉ ์ ์ฐจยทํ๋ ์ ๊ฒ์ดํธ: docs/static-pose-recipe.md (maintainer ํ์ 2026-07-19, ์ด์ง ๊ฒ์ดํธ 2026-07-24).
- ํธํก(idle breathing)์ ํ์ฒ๋ฆฌ ๋ ์ด์ด๋ค (maintainer ํ์ 2026-07-18) โ ์คํฌ๋ฆฝํธ๊ฐ ์๋๋ผ curation.json ์ฌ์ด๋์นด ๋ก ์ ์ธํ๊ณ , compose/GIF ๊ฐ ์ฌ์ ์ํ์ค ์์ ๊ฒฐ์ ๋ก (๋ดํฌ ์ํ, )์ผ๋ก ๊ตฝ๋๋ค. ๊น๋นก์ ํ๋ ์๋ ๊ทธ๋๋ก ์จ์ฐ๋ค (ํ๋ ์ ์ ํ๊ณผ ์ง๊ต).
Workflow
-
Pass the Base Lock Gate above. Do not start step 1 until a base idle is locked (y).
-
Prepare the run:
$SPRITE_GEN_ROOT/.venv/bin/python $SPRITE_GEN_ROOT/scripts/prepare_sprite_run.py \
--out-dir <target>/assets/generated/sprites/<character-id> \
--character-id <character-id> \
--base-image /absolute/path/to/base.png \
--description "<short identity note>" \
--force
For hatch-pet-style locomotion, add the cell gate explicitly: --cell-width 192 --cell-height 208.
๋ฐฉํฅ ์๋ ์บ๋ฆญํฐ(ํด๋จธ๋
ธ์ด๋ 4/8๋ฐฉํฅ)๋ ๋ฐฉํฅ ๊ณ์ฝ์ ํจ๊ป ์ ์ธํ๋ค: --directions down,side,up --mirror left=side.
๋ฐฉํฅ ๊ณ์ฝ ๋ฐ์ ํ์ผ์ ํ์๋
ธ๋ฏธ(raw/<dir>/<pose>.png, frames/<dir>/<pose>/, ๊ฐ์ด๋/ํ๋กฌํํธ ๋์ผ)๋ก
๋๋๋ค โ ์์ธ๊ฐ ๋์ด๋ flat ํด๋๊ฐ ๋น๋ํด์ง์ง ์๋๋ค. ๊ฒฝ๋ก ๋ฆฌ์กธ๋ฒ SSoT ๋ sprite_gen/layout.py,
์ถ์ถ๋ ํ๋ ์์ ๊ฒฝ๋ก๋ frames-manifest row.files ๊ฐ SSoT ๋ค (run-contract ยง2).
base = down ์ ๋ฉด ๊ธฐ๋ณธ์์ธ ํ๋์ด๊ณ , prepare ๊ฐ ๋ฐฉํฅ ์ต์ปค(<dir>_idle) ์ฌ๋กฏ์ ํฉ์ฑํ๊ณ ์์ฑ ์ฒด์ธ SSoT
(references/generation-plan.json โ 1๋จ๊ณ ์ต์ปค๋ base ๊ธฐ๋ฐ, 2๋จ๊ณ ํ์ ์๊ธฐ ๋ฐฉํฅ ์ต์ปค ๊ธฐ๋ฐ, ๋ฏธ๋ฌ ๋ฐฉํฅ์
์์ฑ ์๋ต ๊ณ์ฝ)๋ฅผ ๊ธฐ๋กํ๋ค. ์์ธ์ ์ข์ฐ ์ฌ์์ฑ ๊ท์น: docs/directional-anchor-workflow.md "Prepare ์ค์บํด๋ฉ".
This writes:
sprite-request.json
base-source.<ext>
references/layout-guides/<state>.png
prompts/<state>.txt
raw/
frames/
- Generate one image per state with the engine's own
gen command (generation is engine-owned; the image-gen skill is now a thin shuttle over this โ docs/gen.md):
$SPRITE_GEN_ROOT/.venv/bin/python $SPRITE_GEN_ROOT/scripts/generate_sprite_image.py \
--provider codex \
--prompt-file <run>/prompts/<state>.txt \
--out <run>/raw/<state>.png \
--ref <run>/base-source.<ext> --ref <run>/references/layout-guides/<state>.png
Use prompts/<state>.txt as the prompt; save the selected image as raw/<state>.png. --provider is optional โ the default is codex (SPRITE_GEN_DEFAULT_PROVIDER env overrides it; an observable grok fallback kicks in only if codex is unavailable). Pass --provider grok explicitly for the faster backend; codex adheres tighter to negative constraints. Default policy: docs/gen.md. Keep the request chroma key on the background (extraction removes it). Reference attachment rules:
์์ฑ ๋์์ฑ (maintainer ํ์ 2026-07-19): ์ฌ๋ฌ ํ์ ๋ฝ๋ ๋ฐฐ์น๋ 4๋์๋ก ๋๋ฆฐ๋ค โ
sprite-gen gen ํธ์ถ์ ์ต๋ 4๊ฐ ๋ณ๋ ฌ (codex ์ค์ธก 4๋ณ๋ ฌ๊น์ง ์ค๋กํ ์์; grok ๋ 4,
์ฌ์ฉ์ ๊ด์ธก์ 6๊น์ง ๊ฐ๋ฅํ๋ ๊ธฐ๋ณธ์ 4). 1๊ฐ์ฉ ์ง๋ ฌ์ ๋ฉํฐ-ํ ๋ฐฐ์น์์ ์ํฐํจํด.
run-dir ์ฐ๊ธฐ๋ runio.py ๋ฝ์ด ์งํค๋ฏ๋ก ์์ฑ(๊ฐ์ ๋ค๋ฅธ raw/<state>.png ์ถ๋ ฅ)์
์์ ํ๊ฒ ๋ณ๋ ฌํ๋๋ค. ์ด ๊ท์น์ ์ง์นจ์ด๋ค โ ์ค์ผ์คํธ๋ ์ด์
์คํฌ๋ฆฝํธ๋ฅผ ์งค ๋
ThreadPoolExecutor(max_workers=4) ๊ธ์ผ๋ก ๋ฐ์ํ๋ผ.
Generation providers are engine backends, not user-facing agents. Selecting
grok launches a headless grok -p process owned by GrokProvider; it does not
require or route through a separate user-facing skill/task. Spawning a visible
worker/agent is the caller's orchestrator concern โ out of this engine's scope.
Command chain: docs/gen.md.
- Simple/default states (before direction-anchor mode exists): attach exactly two references โ
base-source.<ext> (canonical identity) + references/layout-guides/<state>.png (layout only).
- Direction-anchor mode: do not attach
base-source.<ext> to action rows. Attach the accepted target-direction anchor (a single-pose single image โ never a multi-frame idle row) + the state layout guide; for a paired row also attach the basis row as timing/scale/motion reference only. Never choose the anchor crop by hand โ ask the pipeline, right before each generation:
$SPRITE_GEN_ROOT/.venv/bin/python -m sprite_gen.cli anchor \
--run-dir <run> --for-state <state>
It returns references/anchors/<dir>-anchor-x8.png for an action row (the curated anchor frame โ pixel edits, transforms, deletions and reordering all baked, upscaled ร8 NEAREST) and base-source.<ext> for an anchor row or a non-direction run. The file is a derived cache, so re-run it every time; which frame is the anchor is the human's call (--pick <state>#<index>, or the pin button in the curation view) and defaults to the anchor row's curated sequence head. Chain details: docs/directional-anchor-workflow.md.
- Hatch-pet-style locomotion may attach additional references only when they are part of the row plan, recorded in
qa-notes.md: original sheet / canonical base (identity support only), a previous gait row such as raw/running-right.png (motion rhythm only), or an accepted motion-QA artifact (gait readability support only).
- Extract frames:
$SPRITE_GEN_ROOT/.venv/bin/python $SPRITE_GEN_ROOT/scripts/extract_sprite_row_frames.py \
--run-dir <target>/assets/generated/sprites/<character-id>
This removes the request chroma key, finds connected sprite components, fits each pose into a fresh transparent request-sized cell, and writes frames/<state>/frame-N.png plus frames/frames-manifest.json.
3.5. (Optional) Curate frames in the webview:
$SPRITE_GEN_ROOT/.venv/bin/sprite-gen curation \
--run-dir <target>/assets/generated/sprites/<character-id>
Standalone local webview: side-by-side frame compare, select/reject, drag-to-reorder play sequence, non-destructive per-frame transform saved to curation.json (originals never rewritten; no sidecar = all frames in order, an explicit default). Usage detail, finished-sheet editing via unpack_atlas_run.py, and the standalone image-candidate curation path: docs/curation.md.
- Compose the runtime atlas:
$SPRITE_GEN_ROOT/.venv/bin/python $SPRITE_GEN_ROOT/scripts/compose_sprite_atlas.py \
--run-dir <target>/assets/generated/sprites/<character-id>
This writes:
sprite-sheet-alpha.png
sprite-sheet-alpha.report.json
manifest.json
manifest.json.frame_layout is the runtime SSoT. Game code must consume rectangles from the manifest and must not recover frame rectangles from alpha content at runtime.
4.5. (Optional) Bake palette-swap colourways of the finished atlas:
$SPRITE_GEN_ROOT/.venv/bin/sprite-gen recolor-palette \
--base <run>/sprite-sheet-alpha.png --out <run>/palette.draft.json
$SPRITE_GEN_ROOT/.venv/bin/sprite-gen recolor \
--run-dir <run> --spec <run>/recolor.spec.json
Exact RGB match by default (dot art); opt-in match: "tolerance" for soft edges. Same input โ same output bytes. The report names every unused map source and every unmapped passthrough colour โ nothing outside the map vanishes quietly. Spec schema, report fields, and curation-view adopt flow: docs/recolor.md.
4.6. (Optional, rig runs only) Bake the declared composite stacks:
$SPRITE_GEN_ROOT/.venv/bin/sprite-gen compose-layers \
--run-dir <target>/assets/generated/sprites/<character-id>
Only for a run whose sprite-request.json declares rig / states.<state>.track / layers โ every other run is untouched by this feature and this step is skipped. It stacks the curated rows (integer pivot translation + alpha masks, no resampling) into <run>/layers/<name>.png + <name>.manifest.json + layers.report.json, so the same run bakes the same bytes every time. --names a,b bakes a subset and leaves the rest of layers/ alone. Declaration schema, landmark rules, track kinds, and what prepare carries: docs/layer-tracks.md.
- Launch the curation webview automatically (default closing step):
$SPRITE_GEN_ROOT/.venv/bin/sprite-gen curation \
--run-dir <target>/assets/generated/sprites/<character-id> &
After the atlas composes (and QA previews exist), launch the webview in the background and report the printed URL โ finishing a run means handing the human the open webview, not just file paths. Multi-agent launch rules (per-launch free port, one webview per run dir, .sprite-gen.lock, --no-open for headless): docs/curation.md. Skip the auto-launch only for an explicitly unattended batch run.
SSoT
Every run starts with sprite-request.json. It owns the numeric recipe used by prompts and scripts:
{
"version": 1,
"kind": "sprite-gen-request",
"engine": "component-row",
"character": { "id": "demo-hero", "description": "same character as the base image" },
"cell": { "shape": "square", "size": 256, "safe_margin": 24 },
"chroma_key": { "name": "magenta", "hex": "#FF00FF", "rgb": [255, 0,
256 is a default variable, not a hidden constant. Change it through the request, then regenerate guides, prompts, extraction, and atlas from the same request.
When safe_margin is omitted, the default is proportional: 9.4% of the cell dimension per axis, floored (256 โ 24px, 128 โ 12px, rect 192ร208 โ 18/19px). An explicit request/CLI value is absolute and wins as-is.
ํ
์ดํฌ(takes) โ ๊ฐ์ ์ํ์ ํ๋ณด/๋ณด๊ฐ ์คํธ๋ฆฝ์ ์๋ ๋ณํฉ์ด ์๋๋ผ request ๋ก ์ ์ธํ๋ค:
"states": { "down_idle": { "frames": 4, ..., "takes": [{ "label": "blink", "frames": 4 }] } }
raw/<...>.takes/<label>.png. ์ถ์ถ์ด primary ๋ค์ ์ด์ด๋ถ์ฌ ํ ํ์ ํ๋ ์ ํ์ ๋ง๋ค๊ณ
manifest labels("blink#0"โฆ)๋ก ํ๋ ์ด์
๋ทฐ์ ํ์๋๋ค. ๊ณ์ฝ ์์ธ: docs/run-contract.md ยง2.
์ค์๊ฐ ๊ณ์ฝ โ frames/ ๋ (raw + request + ์์ง)์ ํ์ ์บ์๋ค. ํ๋ ์ด์
๋ทฐยทcomposeยท
๋ค์ด๋ก๋๊ฐ ์ง์
์ heal_run ์ผ๋ก stale ํ์ ์๋ ์ฌ์ ๋ํ๋ฏ๋ก "์ฌ์ถ์ถ" ์ ๋ณ๋ ์คํ
์ผ๋ก
์ง์ํ ํ์๊ฐ ์๋ค (raw ์๋ ํ์ ๋ณด์กด + ๊ด์ธก ๋
ธํธ). ์บ์ ํค = ํ๋ณ engine_revision.
Optional fit object (opt-in; absent means legacy behavior), exposed by prepare_sprite_run.py as --fit-* flags:
"fit": { "resample": "kcentroid", "align_x": "foot-centroid", "align_y": "bottom" } โ pixel-art-aware downscale and jitter-free frame alignment. align_x: "alpha-centroid" (opt-in, perfectpixel-studio port) aligns the fringe-insensitive alpha-weighted centroid per frame โ the strongest anti-jitter anchor for walk/run rows.
"fit": { "pixel_unfake": true, "logical_height": 64, ... } โ true pixel-unfake extraction with no non-integer resampling (per-frame pitch detection โ grid snap โ kCentroid โ run-wide shared palette โ integer NEAREST). Fully deterministic code, applied at the row-extraction stage only; the style SSoT is the attached base/anchor reference, never prompt text.
- Parameter reference, stage ownership, the pixel-density reference rule, and the before/after plain-twin + curator toggle:
docs/pixel-unfake.md.
Rectangular generation cells are allowed when the target motion benefits from hatch-pet-style row proportions:
"cell": { "shape": "rect", "width": 192, "height": 208, "safe_margin_x": 18, "safe_margin_y": 16 }
The generated row uses the request cell shape. The final atlas is still consumed through manifest.json.frame_layout; runtime code must not assume square cells.
Prompt Contract
The generated row prompt must come from prompts/<state>.txt. Do not hand-write frame counts into a separate prompt. The prompt requires:
- exact state frame count from
sprite-request.json
- one complete full-body pose per invisible request-sized slot
- safe margin from
sprite-request.json
- same locked anchor identity across every frame
- motion-only row responsibility: the row should solve limb/body timing, not rediscover character details
- flat chroma-key background from
sprite-request.json
- no shadows, glows, smears, speed lines, dust, scenery, text, UI, frame numbers, guide boxes, or detached effects
If image generation produces guide boxes, visible labels, overlapping poses, backgrounds, cropped bodies, or identity drift, regenerate the row. Do not repair bad visual generation by drawing or tiling sprites locally.
Output Contract
Install from curated/, never from frames/. frames/ is pre-curation โ the human's
picks, pixel edits and transforms live in curation.json and are applied downstream. Copying
frames/ into an app silently ships the un-edited image and nothing fails. Stills โ
export_curated_pngs.py then curated/. Animation โ the composed atlas + manifest.
Contract: docs/run-contract.md ยง2-c.
One worker owns exactly one character folder. The canonical run-dir folder tree โ every input/output file and which ones drive the curation view โ is owned by docs/run-contract.md ยง2. Do not let multiple workers write the same character folder. The curation.json sidecar schema (selected/order/transforms/pixel_unfake) and its folder-collision rule: docs/curation.md.
Runtime Contract
manifest.json must contain:
game_input: "sprite-sheet-alpha.png"
degraded_static_fallback: false
animation.rows.<state> with frames, fps, durations_ms, and loop
frame_layout.rows.<state>[i] absolute atlas rectangles
Runtime must sample only the active rectangle. Rendering the whole atlas on one plane, guessing a grid, or showing a raw chroma row is a failed integration.
Frame timing and cell reuse (2026-07-16, Aseprite-JSON ๊ณผ ๋ํ ํจํด):
frame_layout.rows.<state> ๋ ์ฌ์(์ธ์คํด์ค) ์์ ๊ทธ๋๋ก์ด๋ฉฐ, ๊ฐ์ ๊ทธ๋ฆผ์ผ๋ก
๊ตฌ์์ง๋ ๋ณต์ ์ธ์คํด์ค๋ ๊ฐ์ rect ๊ฐ ๋ฐ๋ณต๋๋ค โ ํ
์ค์ฒ ์นธ์ ๊ณ ์ ๊ตฝ๊ธฐ๋น
ํ๋๋ง ์ด๋ค. ์๋น์๋ ์ง๊ธ์ฒ๋ผ ํ๋ ์ ์ธ๋ฑ์ค โ rect ์ํ๋ง๋ง ํ๋ฉด ๋๋ค.
animation.rows.<state>.durations_ms[i] ๊ฐ ํ๋ ์๋ณ ํ์ ์๊ฐ์ SSoT ๋ค
(ํ์ฌ๋ fps ๋ฑ๊ฐ๊ฒฉ์ผ๋ก ์ฑ์์ง). ๋ฐฐ์ด์ด ์์ผ๋ฉด fps ๋์ ์ด๊ฒ์ ๋ฐ๋ฅธ๋ค โ
๋ฃจํ๋๋ ์ด/ํ๋ ํ๋ ์์ ๋ง์ง๋ง ํ๋ ์ ๋ณต์ (rect ์ฌ์ฌ์ฉ, ํ
์ค์ฒ ๋น์ฉ 0)๋
duration ์ฐ์ฅ์ผ๋ก ํํํ๋ค.
Static fallback is allowed only as explicit survival output when generation is blocked. It is not a sprite-gen pass and must not create sprite-sheet-alpha.png.
QA
Automated checks (must all pass before reporting done):
frames/frames-manifest.json.ok is true
sprite-sheet-alpha.report.json.ok is true
- every state has the declared frame count
- no frame is empty or near-opaque background
- no frame has excessive edge pixels or chroma-adjacent pixels
- browser screenshots pass
scripts/check_visible_magenta.py when used in a game
Automatic correction-loop dry run:
$SPRITE_GEN_ROOT/.venv/bin/python $SPRITE_GEN_ROOT/scripts/run_correction_loop.py \
--run-dir <target>/assets/generated/sprites/<character-id> \
--states <state> \
--dry-run
This writes correction-loop.report.json, per-attempt inspect.json, score.json,
and correction-hints.txt. A real regeneration loop must pass an explicit
provider command; there is no silent fallback generator.
Use --min-attempts 2 for a live E2E that must exercise at least one provider
regeneration even when the seed candidate already clears the score gate.
Motion Continuity (BLOCKING)
Static identity QA is not enough โ a row can have the right frame count, clean alpha, and consistent identity and still animate as garbage. Build the previews and review motion as motion:
$SPRITE_GEN_ROOT/.venv/bin/python $SPRITE_GEN_ROOT/scripts/preview_animation.py \
--run-dir <target>/assets/generated/sprites/<character-id>
The full verdict criteria (cyclic locomotion, loop seam, non-loop gestures, humanoid per-frame anatomy review, independent second opinion) live in docs/qa-motion.md. If a row fails motion continuity, regenerate that row โ do not repair motion by drawing or re-timing frames locally. Record the per-state motion verdict in qa-notes.md.
Report:
sprite_gen_done=<character-id>
folder=<absolute folder path>
engine=component-row
files=sprite-request,raw,frames,atlas,manifest
qa_note=<one sentence>
Docs Topology
Leaf docs are one link deep from this hub. The tree groups them by the concern
you are in โ walk down the branch that matches your task, don't scan the flat
list. Each doc owns its tables; SKILL.md and the others point rather than restate.
sprite-gen (this SKILL.md = behavior contract + hub)
โ
โโ CONTRACT & STRUCTURE โโ "what files exist and what each stage promises"
โ โโ docs/run-contract.md # pipeline stage I/O table ยท canonical run-dir folder tree ยท
โ โ # curation-view display contract ยท run_revision/HTTP-409 ยท
โ โ # per-state salvage + stale backup ยท --pngs-dir import rule
โ โโ docs/architecture.md # how scripts realize the contract: stages ยท cell geometry ยท
โ # idle-anchor ownership flow ยท extraction internals (SKILL wins on conflict)
โ
โโ REQUEST AUTHORING โโ "fill sprite-request.json before generating"
โ โโ docs/states-and-frames.md # which states ยท frame counts (4/5/6/8/9/12) ยท Quick Path JSON
โ โโ docs/subject-profiles.md # "subject": character|effect ยท sparse-floor ํ๋กํ ยท
โ โ # ์ดํํธ ๋ฒ ์คํธ/์์คํธ ํ๋ํฐ์ค (์ค์ธก ๋ฐฐํฐ๋ฆฌ ๊ทผ๊ฑฐ)
โ โโ docs/pixel-unfake.md # fit / pixel_unfake params ยท plain-twin curator toggle ยท density refs
โ โโ docs/chroma-alpha.md # chroma key branch table ยท --chroma-key auto ยท alpha cleanup
โ
โโ GENERATION โโ "raw/<state>.png from prompts (the one AI step)"
โ โโ docs/gen.md # sprite-gen gen provider CLI ยท verified PNG/report ยท image-gen shuttle
โ โโ docs/frame-interpolation.md # generative in-between (codex/grok) โ take raw ยท auth prereqs ยท RIFE retire rationale
โ โโ docs/seamless-video-loop.md # non-looping AI video clip โ seamless loop: flow-matched cut + RIFE seam bridge
โ
โโ CURATION โโ "human/agent picks, edits, and downloads via the webview"
โ โโ docs/curation.md # webview ยท curation.json schema (selected/order/transforms/
โ # deleted/clones/revision/recolor.picked) ยท per-state salvage ยท
โ # frame CLONES ยท standalone image-candidate path ยท finished-sheet
โ # re-edit (unpack)
โ
โโ COLOURWAYS โโ "bake N palette-swapped sheets from one base atlas"
โ โโ docs/recolor.md # recolor / recolor-palette CLI ยท spec + report schema ยท exact vs
โ # tolerance match ยท variants/ layout ยท curation blink-compare + adopt
โ
โโ LAYER TRACKS โโ "compose rows onto each other instead of generating every combination"
โ โโ docs/layer-tracks.md # rig profiles + integer landmarks ยท track kinds (base /
โ # action_overlay / prop_effect / full_body_override) ยท
โ # composite stack ยท manifest rig block ยท layers/ artifact tree ยท
โ # compose-layers CLI + prepare ์ ๋ ์ด์ด ํค ๋ฐ์
/๋๋กญ ๊ณ ์ง
โ
โโ ENGINE EXPORT โโ "adapt one composed atlas to existing game-engine loaders"
โ โโ docs/engine-export.md # Aseprite JSON mapping ยท Phaser tags ยท Flame per-state hash
โ
โโ SPECIALIZED INPUTS โโ "not the plain animation-row path"
โ โโ docs/directional-anchor-workflow.md # directional / 45ยฐ anchor chains ยท hatch-pet locomotion
โ โโ docs/sheet-slicing.md # multi-figure variant sheet โ per-cell standing cuts (็ซใก็ตต, not rows)
โ
โโ QA โโ "verify motion as motion before reporting done"
โ โโ docs/qa-motion.md # Motion Continuity verdict criteria (BLOCKING)
โ โโ docs/locomotion-curation.md # motion-phase guides ยท manual selected cycles ยท clean GIF export
โ
โโ TROUBLESHOOTING โโ "์กฐ์ฉํ ์ด์ํ ๋ ๋จผ์ ๋ณผ ํ"
โโ docs/troubleshooting.md # ์ฌ์ด๋์นด ์คํ
์ผ ๊ฐ๋/๋์ฅ ๊ฒฝ๋ก ยท ๋-์์ฑ์ ์ถฉ๋ ยท
# provider ๋ฌด์ถ๋ ฅ ํ(env ์์) ยท ์ธ๋ก ์คํธ๋ฆฝ ์ ๋ฉธ ยท ffmpeg 500
Concept taxonomy (which doc owns each term, so agents don't guess):
sprite-request.json, cell, states, takes โ run-contract.md ยง2 ยท states-and-frames.md
run_revision, state_revision, per-state salvage, curation.stale-*.json โ curation.py + curation.md
curation.json fields (selected/order/deleted/transforms/pixels/clones/pixel_unfake/revision/recolor.picked) โ curation.md
- frame clones (duplicate instances,
source_frame_index) โ curation.md + compose consumers
frame_layout, manifest.json runtime contract โ run-contract.md + this SKILL.md "Runtime Contract"
- Aseprite-compatible Phaser / Flame JSON export โ
docs/engine-export.md
- pixel-unfake
fit, .plain.png/orig/ twins โ pixel-unfake.md
- recolor spec / report /
variants/ bake + colourway adopt โ recolor.md
rig profiles / landmarks, row track kinds, composite layers stack + layers/ bake (sprite-gen compose-layers) โ docs/layer-tracks.md (sprite_gen/compose/layers.py validates the declaration, sprite_gen/compose/compose_layers.py bakes it)
- webview interactions (title-drag reorder, ๋ฃ๊ธฐ/๋นผ๊ธฐ toggle, 2-tier card, custom
data-tip tooltip, recolor blink-compare) โ sprite_gen/curator/ (๋๋ฉ์ธ ๋ถํ src/*.js โ ๋ก๋ ์์ SSoT ๋ index.html โ + curator.css), described in curation.md + recolor.md