spritegen-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spritegen_cli-0.1.0/.env.template +14 -0
- spritegen_cli-0.1.0/.gitattributes +8 -0
- spritegen_cli-0.1.0/.github/workflows/ci.yml +119 -0
- spritegen_cli-0.1.0/.github/workflows/release.yml +161 -0
- spritegen_cli-0.1.0/.gitignore +219 -0
- spritegen_cli-0.1.0/PKG-INFO +16 -0
- spritegen_cli-0.1.0/README.md +105 -0
- spritegen_cli-0.1.0/pyproject.toml +73 -0
- spritegen_cli-0.1.0/src/spritegen/__init__.py +3 -0
- spritegen_cli-0.1.0/src/spritegen/atlas.py +99 -0
- spritegen_cli-0.1.0/src/spritegen/cli.py +178 -0
- spritegen_cli-0.1.0/src/spritegen/clip.py +81 -0
- spritegen_cli-0.1.0/src/spritegen/drive.py +163 -0
- spritegen_cli-0.1.0/src/spritegen/endpoints.py +113 -0
- spritegen_cli-0.1.0/src/spritegen/fal.py +228 -0
- spritegen_cli-0.1.0/src/spritegen/imaging.py +219 -0
- spritegen_cli-0.1.0/src/spritegen/ledger.py +143 -0
- spritegen_cli-0.1.0/src/spritegen/matting.py +136 -0
- spritegen_cli-0.1.0/src/spritegen/migrate.py +162 -0
- spritegen_cli-0.1.0/src/spritegen/prompts.py +96 -0
- spritegen_cli-0.1.0/src/spritegen/rrdb.py +91 -0
- spritegen_cli-0.1.0/src/spritegen/settings.py +127 -0
- spritegen_cli-0.1.0/src/spritegen/sheet.py +370 -0
- spritegen_cli-0.1.0/src/spritegen/skill/__init__.py +303 -0
- spritegen_cli-0.1.0/src/spritegen/skill/files/SKILL.md +553 -0
- spritegen_cli-0.1.0/src/spritegen/stages/__init__.py +490 -0
- spritegen_cli-0.1.0/src/spritegen/stages/anchor.py +215 -0
- spritegen_cli-0.1.0/src/spritegen/stages/board.py +68 -0
- spritegen_cli-0.1.0/src/spritegen/stages/matte.py +209 -0
- spritegen_cli-0.1.0/src/spritegen/stages/motion.py +164 -0
- spritegen_cli-0.1.0/src/spritegen/stages/pose.py +172 -0
- spritegen_cli-0.1.0/src/spritegen/stages/video.py +196 -0
- spritegen_cli-0.1.0/src/spritegen/upscale.py +444 -0
- spritegen_cli-0.1.0/src/spritegen/workspace.py +852 -0
- spritegen_cli-0.1.0/tests/conftest.py +62 -0
- spritegen_cli-0.1.0/tests/helpers.py +67 -0
- spritegen_cli-0.1.0/tests/parity/expected/anchor_crop.pixelart.json +10 -0
- spritegen_cli-0.1.0/tests/parity/expected/anchor_crop.pixelart.png +0 -0
- spritegen_cli-0.1.0/tests/parity/expected/synthetic_chroma.cut.json +6 -0
- spritegen_cli-0.1.0/tests/parity/expected/synthetic_chroma.cut.png +0 -0
- spritegen_cli-0.1.0/tests/parity/expected/synthetic_matted.json +6 -0
- spritegen_cli-0.1.0/tests/parity/expected/synthetic_row.png +0 -0
- spritegen_cli-0.1.0/tests/parity/expected/synthetic_video_board.json +12 -0
- spritegen_cli-0.1.0/tests/parity/expected/synthetic_video_board.png +0 -0
- spritegen_cli-0.1.0/tests/parity/expected/walk_south_row.gif +0 -0
- spritegen_cli-0.1.0/tests/parity/expected/walk_south_row.png +0 -0
- spritegen_cli-0.1.0/tests/parity/input/anchor_crop.png +0 -0
- spritegen_cli-0.1.0/tests/parity/input/synthetic_board.png +0 -0
- spritegen_cli-0.1.0/tests/parity/input/synthetic_chroma.png +0 -0
- spritegen_cli-0.1.0/tests/parity/input/synthetic_matted.png +0 -0
- spritegen_cli-0.1.0/tests/parity/input/walk_south_board.png +0 -0
- spritegen_cli-0.1.0/tests/parity/manifest.json +181 -0
- spritegen_cli-0.1.0/tests/test_anchor.py +457 -0
- spritegen_cli-0.1.0/tests/test_atlas.py +106 -0
- spritegen_cli-0.1.0/tests/test_board_stage.py +254 -0
- spritegen_cli-0.1.0/tests/test_cli.py +67 -0
- spritegen_cli-0.1.0/tests/test_clip.py +142 -0
- spritegen_cli-0.1.0/tests/test_drive.py +179 -0
- spritegen_cli-0.1.0/tests/test_fal.py +285 -0
- spritegen_cli-0.1.0/tests/test_imaging.py +213 -0
- spritegen_cli-0.1.0/tests/test_ledger.py +232 -0
- spritegen_cli-0.1.0/tests/test_matte.py +318 -0
- spritegen_cli-0.1.0/tests/test_matting.py +248 -0
- spritegen_cli-0.1.0/tests/test_migrate.py +183 -0
- spritegen_cli-0.1.0/tests/test_motion.py +566 -0
- spritegen_cli-0.1.0/tests/test_parity.py +103 -0
- spritegen_cli-0.1.0/tests/test_pose.py +279 -0
- spritegen_cli-0.1.0/tests/test_prompts.py +88 -0
- spritegen_cli-0.1.0/tests/test_settings.py +165 -0
- spritegen_cli-0.1.0/tests/test_sheet.py +492 -0
- spritegen_cli-0.1.0/tests/test_show.py +147 -0
- spritegen_cli-0.1.0/tests/test_skill.py +245 -0
- spritegen_cli-0.1.0/tests/test_stages.py +86 -0
- spritegen_cli-0.1.0/tests/test_upscale.py +311 -0
- spritegen_cli-0.1.0/tests/test_video.py +414 -0
- spritegen_cli-0.1.0/tests/test_workspace.py +719 -0
- spritegen_cli-0.1.0/tests/tests_fal_doubles.py +68 -0
- spritegen_cli-0.1.0/uv.lock +2320 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# spritegen environment. Copy to .env and fill in.
|
|
2
|
+
# The CLI reads .env from the working directory on start-up; an exported
|
|
3
|
+
# variable wins over the file, so a one-off key still beats a stale one here.
|
|
4
|
+
# .env is gitignored; this template is not — never put a real key here.
|
|
5
|
+
|
|
6
|
+
# Required by every paid stage (anchor, motion, video, matte).
|
|
7
|
+
# Get one at https://fal.ai/dashboard/keys
|
|
8
|
+
FAL_KEY=
|
|
9
|
+
|
|
10
|
+
# Optional. Where asset directories live. Default: assets/ under the working directory.
|
|
11
|
+
#SPRITEGEN_ASSETS=
|
|
12
|
+
|
|
13
|
+
# Optional. Extra hosts a download may come from, comma-separated.
|
|
14
|
+
#SPRITEGEN_ALLOWED_HOSTS=
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# The last matching line wins, so the general rule comes first and the exception after it.
|
|
2
|
+
* text=auto eol=lf
|
|
3
|
+
|
|
4
|
+
# Parity fixtures are hashed byte for byte. A file git converts on checkout is a
|
|
5
|
+
# different file on Windows than on Linux, and the digest recorded in manifest.json then
|
|
6
|
+
# only matches on the machine that captured it — which is exactly what CI caught: the
|
|
7
|
+
# manifest held the Windows working-tree bytes, and the repository's own bytes failed.
|
|
8
|
+
tests/parity/** -text
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Every push and every pull request. The suite is a few seconds of pure Python — there
|
|
4
|
+
# is no GPU here and no paid call can be made, because `tests/conftest.py` closes every
|
|
5
|
+
# route out of the process.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: ["main"]
|
|
9
|
+
pull_request:
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
# A second push to the same branch cancels the first. The last commit is the one whose
|
|
14
|
+
# answer anybody wants.
|
|
15
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
16
|
+
cancel-in-progress: true
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
lint:
|
|
23
|
+
name: lint
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v7
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
|
|
30
|
+
- name: Install uv
|
|
31
|
+
# Pinned to the patch, not a floating major: astral-sh/setup-uv publishes no
|
|
32
|
+
# `v10` tag, unlike actions/*, so `@v10` does not resolve at all.
|
|
33
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.13"
|
|
36
|
+
enable-cache: true
|
|
37
|
+
|
|
38
|
+
- name: Install
|
|
39
|
+
run: uv sync --frozen
|
|
40
|
+
|
|
41
|
+
- name: Ruff
|
|
42
|
+
run: uv run ruff check src tests
|
|
43
|
+
|
|
44
|
+
test:
|
|
45
|
+
name: ${{ matrix.os }}
|
|
46
|
+
runs-on: ${{ matrix.os }}
|
|
47
|
+
strategy:
|
|
48
|
+
fail-fast: false
|
|
49
|
+
matrix:
|
|
50
|
+
# All three, one Python. The risk this matrix is for is the operating system:
|
|
51
|
+
# this tool builds paths out of names that arrive from a command line and out of
|
|
52
|
+
# a state file, and `workspace.check_name` and `workspace.inside` guard cases —
|
|
53
|
+
# reserved device names, drive-relative paths, 8.3 aliases, reparse points — that
|
|
54
|
+
# exist on one of them only. It has already earned its keep: the parity fixtures
|
|
55
|
+
# turned out to be pinning the PNG encoder, and a `.env` test was reading the
|
|
56
|
+
# runner's temporary-directory layout. Both were green on Windows alone.
|
|
57
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
58
|
+
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v7
|
|
61
|
+
with:
|
|
62
|
+
persist-credentials: false
|
|
63
|
+
|
|
64
|
+
- name: Install uv
|
|
65
|
+
# Pinned to the patch, not a floating major: astral-sh/setup-uv publishes no
|
|
66
|
+
# `v10` tag, unlike actions/*, so `@v10` does not resolve at all.
|
|
67
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
68
|
+
with:
|
|
69
|
+
python-version: "3.13"
|
|
70
|
+
enable-cache: true
|
|
71
|
+
|
|
72
|
+
- name: Install
|
|
73
|
+
# Without the extras, deliberately. `torch` with CUDA is some two gigabytes and
|
|
74
|
+
# `rembg` brings onnxruntime, and every test that touches either one asserts the
|
|
75
|
+
# refusal a *missing* extra produces — installing them here would turn those
|
|
76
|
+
# tests into skips and lose the coverage silently. That the extras still resolve
|
|
77
|
+
# is what `uv lock --check` answers, in the job below.
|
|
78
|
+
run: uv sync --frozen
|
|
79
|
+
|
|
80
|
+
- name: Test
|
|
81
|
+
# The coverage floor lives in pyproject.toml, so it is the same number here and
|
|
82
|
+
# on anybody's machine. It sits below the real figure on purpose: `rrdb.py` and
|
|
83
|
+
# most of `upscale.py` and `matting.py` cannot run without the extras that are
|
|
84
|
+
# deliberately not installed, and pretending otherwise would mean either two
|
|
85
|
+
# gigabytes per job or tests that assert nothing.
|
|
86
|
+
run: uv run pytest -q --cov
|
|
87
|
+
|
|
88
|
+
package:
|
|
89
|
+
name: package
|
|
90
|
+
runs-on: ubuntu-latest
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/checkout@v7
|
|
93
|
+
with:
|
|
94
|
+
persist-credentials: false
|
|
95
|
+
|
|
96
|
+
- name: Install uv
|
|
97
|
+
# Pinned to the patch, not a floating major: astral-sh/setup-uv publishes no
|
|
98
|
+
# `v10` tag, unlike actions/*, so `@v10` does not resolve at all.
|
|
99
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
100
|
+
with:
|
|
101
|
+
python-version: "3.13"
|
|
102
|
+
enable-cache: true
|
|
103
|
+
|
|
104
|
+
- name: The lock agrees with pyproject.toml
|
|
105
|
+
# Not under UV_FROZEN. That variable turns this into a validity check and prints
|
|
106
|
+
# a warning saying so, which is the one thing this step is not for: a lock that
|
|
107
|
+
# is valid but stale against pyproject.toml is exactly the drift being looked
|
|
108
|
+
# for. `--frozen` belongs on the syncs, and only there.
|
|
109
|
+
run: uv lock --check
|
|
110
|
+
|
|
111
|
+
- name: Build
|
|
112
|
+
run: uv build
|
|
113
|
+
|
|
114
|
+
- name: The console script runs out of the built wheel, with nothing else supplied
|
|
115
|
+
# `--no-project` so it resolves the wheel rather than the checkout beside it: the
|
|
116
|
+
# thing under test is what would be installed, not what is on disk here. Nothing
|
|
117
|
+
# is added to the command, and that is the assertion — a wheel that needs a
|
|
118
|
+
# hand-passed git URL to start is a wheel nobody can install.
|
|
119
|
+
run: uv run --isolated --no-project --with dist/*.whl spritegen --version
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Tag-triggered. `v0.2.0` builds the distributions, checks the tag agrees with the
|
|
4
|
+
# version in pyproject.toml, smoke-tests both, and attaches them to a GitHub Release.
|
|
5
|
+
#
|
|
6
|
+
# ---------------------------------------------------------------------------------
|
|
7
|
+
# The two things that used to block PyPI are gone: the package is `spritegen-cli`, and
|
|
8
|
+
# `pixelfixer` is no longer a declared dependency — it is on no registry, and a direct
|
|
9
|
+
# URL in `dependencies` or an extra makes a distribution PyPI refuses, so it became an
|
|
10
|
+
# optional install that `imaging.require_pixelfixer` names. The built wheel now installs
|
|
11
|
+
# from its metadata alone, which the `package` job in ci.yml asserts on every commit.
|
|
12
|
+
#
|
|
13
|
+
# Publishing is driven by one secret, `PUBLISH_TOKEN`, on the `pypi` environment:
|
|
14
|
+
#
|
|
15
|
+
# 1. On GitHub, create the `pypi` environment. Add a required reviewer there if
|
|
16
|
+
# publishing should stop for a human before it happens.
|
|
17
|
+
# 2. Put a PyPI API token in it as `PUBLISH_TOKEN`, scoped to this project rather than
|
|
18
|
+
# to the whole account — a project-scoped token can only ever publish this package.
|
|
19
|
+
#
|
|
20
|
+
# **The secret is the switch.** Without it the job runs, says nothing was published, and
|
|
21
|
+
# succeeds; the distributions are on the GitHub Release either way. There is no second
|
|
22
|
+
# on/off variable to forget, or to disagree with the secret.
|
|
23
|
+
#
|
|
24
|
+
# A long-lived token is a credential sitting in a repository until somebody rotates it.
|
|
25
|
+
# PyPI's trusted publishing avoids that — an OIDC exchange per run, nothing stored — and
|
|
26
|
+
# is worth moving to. It needs `permissions: id-token: write` here and the publisher
|
|
27
|
+
# registered on PyPI, and then `PUBLISH_TOKEN` can be deleted.
|
|
28
|
+
# ---------------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
on:
|
|
31
|
+
push:
|
|
32
|
+
tags:
|
|
33
|
+
- "v[0-9]+.[0-9]+.[0-9]+"
|
|
34
|
+
- "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
|
35
|
+
- "v[0-9]+.[0-9]+.[0-9]+[ab][0-9]+"
|
|
36
|
+
workflow_dispatch:
|
|
37
|
+
|
|
38
|
+
permissions:
|
|
39
|
+
contents: read
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
build:
|
|
43
|
+
name: build and verify
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v7
|
|
47
|
+
with:
|
|
48
|
+
persist-credentials: false
|
|
49
|
+
|
|
50
|
+
- name: Install uv
|
|
51
|
+
# Pinned to the patch, not a floating major: astral-sh/setup-uv publishes no
|
|
52
|
+
# `v10` tag, unlike actions/*, so `@v10` does not resolve at all.
|
|
53
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
54
|
+
with:
|
|
55
|
+
python-version: "3.13"
|
|
56
|
+
enable-cache: false
|
|
57
|
+
|
|
58
|
+
- name: The tag agrees with the version
|
|
59
|
+
# The version lives in pyproject.toml and the tag is typed by hand, so they drift
|
|
60
|
+
# the first time somebody is in a hurry — and the failure is silent: v0.2.0 ships
|
|
61
|
+
# a distribution that calls itself 0.1.0, and the index rejects it later as a
|
|
62
|
+
# duplicate for reasons nobody can see from the tag.
|
|
63
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
64
|
+
run: |
|
|
65
|
+
tagged="${GITHUB_REF_NAME#v}"
|
|
66
|
+
declared="$(uv version --short)"
|
|
67
|
+
if [ "$tagged" != "$declared" ]; then
|
|
68
|
+
echo "tag $GITHUB_REF_NAME says $tagged; pyproject.toml says $declared" >&2
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
echo "both say $declared"
|
|
72
|
+
|
|
73
|
+
- name: The lock agrees with pyproject.toml
|
|
74
|
+
run: uv lock --check
|
|
75
|
+
|
|
76
|
+
- name: Lint and test
|
|
77
|
+
# A tag is not a promise that CI ran on that commit — it can be pushed to any
|
|
78
|
+
# commit, including one that never opened a pull request.
|
|
79
|
+
run: |
|
|
80
|
+
uv sync --frozen
|
|
81
|
+
uv run ruff check src tests
|
|
82
|
+
uv run pytest -q --cov
|
|
83
|
+
|
|
84
|
+
- name: Build
|
|
85
|
+
run: uv build
|
|
86
|
+
|
|
87
|
+
- name: Smoke test the wheel
|
|
88
|
+
# Nothing supplied but the wheel. What is being tested is that somebody who runs
|
|
89
|
+
# `pip install spritegen-cli` gets something that starts.
|
|
90
|
+
run: uv run --isolated --no-project --with dist/*.whl spritegen --version
|
|
91
|
+
|
|
92
|
+
- name: Smoke test the source distribution
|
|
93
|
+
run: uv run --isolated --no-project --with dist/*.tar.gz spritegen --version
|
|
94
|
+
|
|
95
|
+
- name: Upload the distributions
|
|
96
|
+
uses: actions/upload-artifact@v7
|
|
97
|
+
with:
|
|
98
|
+
name: dist
|
|
99
|
+
path: dist/
|
|
100
|
+
|
|
101
|
+
github-release:
|
|
102
|
+
name: github release
|
|
103
|
+
needs: [build]
|
|
104
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
105
|
+
runs-on: ubuntu-latest
|
|
106
|
+
permissions:
|
|
107
|
+
contents: write
|
|
108
|
+
steps:
|
|
109
|
+
- uses: actions/download-artifact@v8
|
|
110
|
+
with:
|
|
111
|
+
name: dist
|
|
112
|
+
path: dist/
|
|
113
|
+
|
|
114
|
+
- name: Publish the release with the distributions attached
|
|
115
|
+
env:
|
|
116
|
+
GH_TOKEN: ${{ github.token }}
|
|
117
|
+
REPO: ${{ github.repository }}
|
|
118
|
+
run: |
|
|
119
|
+
gh release create "$GITHUB_REF_NAME" dist/* \
|
|
120
|
+
--repo "$REPO" \
|
|
121
|
+
--title "$GITHUB_REF_NAME" \
|
|
122
|
+
--generate-notes
|
|
123
|
+
|
|
124
|
+
pypi:
|
|
125
|
+
name: pypi
|
|
126
|
+
needs: [build]
|
|
127
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
128
|
+
runs-on: ubuntu-latest
|
|
129
|
+
# The secret lives on this environment rather than on the repository, which is what
|
|
130
|
+
# keeps it out of reach of a workflow on a fork or a branch — and it is where a
|
|
131
|
+
# required reviewer goes, if publishing should stop for a human first.
|
|
132
|
+
environment:
|
|
133
|
+
name: pypi
|
|
134
|
+
steps:
|
|
135
|
+
- name: Install uv
|
|
136
|
+
# Pinned to the patch, not a floating major: astral-sh/setup-uv publishes no
|
|
137
|
+
# `v10` tag, unlike actions/*, so `@v10` does not resolve at all.
|
|
138
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
139
|
+
with:
|
|
140
|
+
python-version: "3.13"
|
|
141
|
+
enable-cache: false
|
|
142
|
+
|
|
143
|
+
- uses: actions/download-artifact@v8
|
|
144
|
+
with:
|
|
145
|
+
name: dist
|
|
146
|
+
path: dist/
|
|
147
|
+
|
|
148
|
+
- name: Publish
|
|
149
|
+
# **The secret is the switch.** There is no separate on/off variable to forget or
|
|
150
|
+
# to disagree with: no `PUBLISH_TOKEN`, no publish, and the run says so instead of
|
|
151
|
+
# failing on an empty credential. Setting the secret is the act that turns this
|
|
152
|
+
# on, and unsetting it is the act that turns it off.
|
|
153
|
+
env:
|
|
154
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
|
155
|
+
run: |
|
|
156
|
+
if [ -z "${UV_PUBLISH_TOKEN}" ]; then
|
|
157
|
+
echo "no PUBLISH_TOKEN on the pypi environment; nothing was published." >&2
|
|
158
|
+
echo "The distributions are on the GitHub Release for this tag." >&2
|
|
159
|
+
exit 0
|
|
160
|
+
fi
|
|
161
|
+
uv publish
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
219
|
+
assets/
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: spritegen-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sprite generation CLI — a few AI-generated images into a game-ready character sheet
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: av>=12.0
|
|
7
|
+
Requires-Dist: fal-client>=0.5
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: numpy>=1.26
|
|
10
|
+
Requires-Dist: opencv-python>=5.0.0.93
|
|
11
|
+
Requires-Dist: pillow>=10.0
|
|
12
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
13
|
+
Provides-Extra: matte
|
|
14
|
+
Requires-Dist: rembg>=2.0; extra == 'matte'
|
|
15
|
+
Provides-Extra: upscale
|
|
16
|
+
Requires-Dist: torch>=2.0; extra == 'upscale'
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# spritegen
|
|
2
|
+
|
|
3
|
+
A sprite generation CLI: a handful of AI-generated images in, the frames of a game-ready
|
|
4
|
+
character sheet out, one named stage at a time.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
pip install spritegen-cli # the package; the command is `spritegen`
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Three things it can do are optional installs, because each is large and none is needed by
|
|
11
|
+
the rest:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
pip install "spritegen-cli[upscale]" # Real-ESRGAN locally, so a paid call need not buy resolution
|
|
15
|
+
pip install "spritegen-cli[matte]" # BiRefNet locally, so cutting a background costs nothing
|
|
16
|
+
pip install "pixelfixer @ git+https://github.com/Retro-Diffusion/pixel-art-fixer.git#subdirectory=python"
|
|
17
|
+
# the pixel-art direction, which recovers a native grid
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`pixelfixer` is on no registry, so it cannot be an extra: a direct URL in a distribution's
|
|
21
|
+
metadata is one PyPI refuses. Every command says which install it needs when it needs one.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
spritegen new <character>
|
|
25
|
+
spritegen anchor <character> # the character, one image
|
|
26
|
+
spritegen pose <character> # where an animation starts, when the anchor is not it
|
|
27
|
+
spritegen motion <character> # movement from a driving clip
|
|
28
|
+
spritegen video <character> # or animate the anchor directly
|
|
29
|
+
spritegen matte <character> # cut the background
|
|
30
|
+
spritegen board <character> # close it into a sheet row, 166 px cells
|
|
31
|
+
spritegen upscale <character> # enlarge an artifact locally, for nothing
|
|
32
|
+
spritegen show <character> # what it holds, and what comes next
|
|
33
|
+
spritegen status
|
|
34
|
+
spritegen cost
|
|
35
|
+
spritegen init # write the Claude skill for all of this
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Every artifact of one character lives under `assets/<character>/`, and no stage takes an
|
|
39
|
+
input or output path — that is what lets `status` say what comes next, and what lets a
|
|
40
|
+
stage refuse to run out of order.
|
|
41
|
+
|
|
42
|
+
`motion` is the one that answers the problem the rest of this exists for. Generating a
|
|
43
|
+
walk frame by frame moves the character instead of animating it — measured on the purple
|
|
44
|
+
warrior sheet, the head-and-torso region shifts 0.25 between steps against the
|
|
45
|
+
reference's 0.11, because the model redraws the character in every cell. A pose-driven
|
|
46
|
+
endpoint cannot: identity comes from the anchor, movement from a clip built out of a
|
|
47
|
+
reference sheet the game already ships.
|
|
48
|
+
|
|
49
|
+
Three endpoints do that job, and `--endpoint` picks one:
|
|
50
|
+
|
|
51
|
+
| | |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `wan-motion` (default) | retargets the driving skeleton to the character's proportions |
|
|
54
|
+
| `wan-animate` | Wan 2.2 Animate, the full model |
|
|
55
|
+
| `one-to-all` | weighs identity against pose, as two separate numbers |
|
|
56
|
+
|
|
57
|
+
`init` spends nothing and is about the tool rather than about a character: it writes
|
|
58
|
+
`.claude/skills/spritegen/SKILL.md` into the first `.claude` at or above the working
|
|
59
|
+
directory — how to run the tool, the two routes through it, the prompt skeletons, and
|
|
60
|
+
every option of every stage. The prose is authored at `src/spritegen/skill/files/SKILL.md`;
|
|
61
|
+
the stage and endpoint tables are read out of the registry when the file is written, so a
|
|
62
|
+
new option shows up in the skill without anyone remembering to describe it twice.
|
|
63
|
+
|
|
64
|
+
Most stages spend money. Each paid call appends one line to that asset's `ledger.jsonl`
|
|
65
|
+
with the endpoint, the exact payload, the URLs returned and the sha256 of every file
|
|
66
|
+
written; `--dry-run` prints the payload and spends nothing.
|
|
67
|
+
|
|
68
|
+
## Running it
|
|
69
|
+
|
|
70
|
+
Needs [uv](https://docs.astral.sh/uv/) and `FAL_KEY`. Export it, or copy
|
|
71
|
+
`.env.template` to `.env` and put it there — the CLI reads a `.env` from the working
|
|
72
|
+
directory on start-up, and an exported variable wins over the file.
|
|
73
|
+
|
|
74
|
+
One dependency is not on PyPI: `pixelfixer`, from
|
|
75
|
+
[Retro-Diffusion/pixel-art-fixer](https://github.com/Retro-Diffusion/pixel-art-fixer),
|
|
76
|
+
pinned to a commit in `uv.lock`. It recovers the native pixel grid an image already has
|
|
77
|
+
and rewrites the image on it — the step that makes a 720p render into pixel art, and the
|
|
78
|
+
reason `uv sync` needs git.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
uv sync
|
|
82
|
+
uv run spritegen --help
|
|
83
|
+
uv run pytest
|
|
84
|
+
uv run ruff check .
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Where the spec lives
|
|
88
|
+
|
|
89
|
+
`specs/sprite-cli/`, in the workspace this tool was specified in, alongside the reference
|
|
90
|
+
sheet it drives from. spritegen is a standalone CLI: it knows nothing about the game whose
|
|
91
|
+
sprites it happens to have been built for, and nothing in it should.
|
|
92
|
+
|
|
93
|
+
## Releasing
|
|
94
|
+
|
|
95
|
+
Tag it. `git tag v0.2.0 && git push --tags` checks the tag agrees with the version in
|
|
96
|
+
`pyproject.toml`, re-runs lint and tests, builds, smoke-tests both distributions with
|
|
97
|
+
nothing supplied but the distribution itself, and attaches them to a GitHub Release.
|
|
98
|
+
|
|
99
|
+
Publishing to PyPI is driven by one secret, `PUBLISH_TOKEN`, on the `pypi` environment —
|
|
100
|
+
a project-scoped PyPI API token. **The secret is the switch**: without it the run says
|
|
101
|
+
nothing was published and succeeds, and the distributions are on the Release either way.
|
|
102
|
+
|
|
103
|
+
A long-lived token is a credential sitting in the repository until somebody rotates it.
|
|
104
|
+
Trusted publishing avoids that entirely and is worth moving to; the release workflow's
|
|
105
|
+
header says what it takes.
|