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.
Files changed (78) hide show
  1. spritegen_cli-0.1.0/.env.template +14 -0
  2. spritegen_cli-0.1.0/.gitattributes +8 -0
  3. spritegen_cli-0.1.0/.github/workflows/ci.yml +119 -0
  4. spritegen_cli-0.1.0/.github/workflows/release.yml +161 -0
  5. spritegen_cli-0.1.0/.gitignore +219 -0
  6. spritegen_cli-0.1.0/PKG-INFO +16 -0
  7. spritegen_cli-0.1.0/README.md +105 -0
  8. spritegen_cli-0.1.0/pyproject.toml +73 -0
  9. spritegen_cli-0.1.0/src/spritegen/__init__.py +3 -0
  10. spritegen_cli-0.1.0/src/spritegen/atlas.py +99 -0
  11. spritegen_cli-0.1.0/src/spritegen/cli.py +178 -0
  12. spritegen_cli-0.1.0/src/spritegen/clip.py +81 -0
  13. spritegen_cli-0.1.0/src/spritegen/drive.py +163 -0
  14. spritegen_cli-0.1.0/src/spritegen/endpoints.py +113 -0
  15. spritegen_cli-0.1.0/src/spritegen/fal.py +228 -0
  16. spritegen_cli-0.1.0/src/spritegen/imaging.py +219 -0
  17. spritegen_cli-0.1.0/src/spritegen/ledger.py +143 -0
  18. spritegen_cli-0.1.0/src/spritegen/matting.py +136 -0
  19. spritegen_cli-0.1.0/src/spritegen/migrate.py +162 -0
  20. spritegen_cli-0.1.0/src/spritegen/prompts.py +96 -0
  21. spritegen_cli-0.1.0/src/spritegen/rrdb.py +91 -0
  22. spritegen_cli-0.1.0/src/spritegen/settings.py +127 -0
  23. spritegen_cli-0.1.0/src/spritegen/sheet.py +370 -0
  24. spritegen_cli-0.1.0/src/spritegen/skill/__init__.py +303 -0
  25. spritegen_cli-0.1.0/src/spritegen/skill/files/SKILL.md +553 -0
  26. spritegen_cli-0.1.0/src/spritegen/stages/__init__.py +490 -0
  27. spritegen_cli-0.1.0/src/spritegen/stages/anchor.py +215 -0
  28. spritegen_cli-0.1.0/src/spritegen/stages/board.py +68 -0
  29. spritegen_cli-0.1.0/src/spritegen/stages/matte.py +209 -0
  30. spritegen_cli-0.1.0/src/spritegen/stages/motion.py +164 -0
  31. spritegen_cli-0.1.0/src/spritegen/stages/pose.py +172 -0
  32. spritegen_cli-0.1.0/src/spritegen/stages/video.py +196 -0
  33. spritegen_cli-0.1.0/src/spritegen/upscale.py +444 -0
  34. spritegen_cli-0.1.0/src/spritegen/workspace.py +852 -0
  35. spritegen_cli-0.1.0/tests/conftest.py +62 -0
  36. spritegen_cli-0.1.0/tests/helpers.py +67 -0
  37. spritegen_cli-0.1.0/tests/parity/expected/anchor_crop.pixelart.json +10 -0
  38. spritegen_cli-0.1.0/tests/parity/expected/anchor_crop.pixelart.png +0 -0
  39. spritegen_cli-0.1.0/tests/parity/expected/synthetic_chroma.cut.json +6 -0
  40. spritegen_cli-0.1.0/tests/parity/expected/synthetic_chroma.cut.png +0 -0
  41. spritegen_cli-0.1.0/tests/parity/expected/synthetic_matted.json +6 -0
  42. spritegen_cli-0.1.0/tests/parity/expected/synthetic_row.png +0 -0
  43. spritegen_cli-0.1.0/tests/parity/expected/synthetic_video_board.json +12 -0
  44. spritegen_cli-0.1.0/tests/parity/expected/synthetic_video_board.png +0 -0
  45. spritegen_cli-0.1.0/tests/parity/expected/walk_south_row.gif +0 -0
  46. spritegen_cli-0.1.0/tests/parity/expected/walk_south_row.png +0 -0
  47. spritegen_cli-0.1.0/tests/parity/input/anchor_crop.png +0 -0
  48. spritegen_cli-0.1.0/tests/parity/input/synthetic_board.png +0 -0
  49. spritegen_cli-0.1.0/tests/parity/input/synthetic_chroma.png +0 -0
  50. spritegen_cli-0.1.0/tests/parity/input/synthetic_matted.png +0 -0
  51. spritegen_cli-0.1.0/tests/parity/input/walk_south_board.png +0 -0
  52. spritegen_cli-0.1.0/tests/parity/manifest.json +181 -0
  53. spritegen_cli-0.1.0/tests/test_anchor.py +457 -0
  54. spritegen_cli-0.1.0/tests/test_atlas.py +106 -0
  55. spritegen_cli-0.1.0/tests/test_board_stage.py +254 -0
  56. spritegen_cli-0.1.0/tests/test_cli.py +67 -0
  57. spritegen_cli-0.1.0/tests/test_clip.py +142 -0
  58. spritegen_cli-0.1.0/tests/test_drive.py +179 -0
  59. spritegen_cli-0.1.0/tests/test_fal.py +285 -0
  60. spritegen_cli-0.1.0/tests/test_imaging.py +213 -0
  61. spritegen_cli-0.1.0/tests/test_ledger.py +232 -0
  62. spritegen_cli-0.1.0/tests/test_matte.py +318 -0
  63. spritegen_cli-0.1.0/tests/test_matting.py +248 -0
  64. spritegen_cli-0.1.0/tests/test_migrate.py +183 -0
  65. spritegen_cli-0.1.0/tests/test_motion.py +566 -0
  66. spritegen_cli-0.1.0/tests/test_parity.py +103 -0
  67. spritegen_cli-0.1.0/tests/test_pose.py +279 -0
  68. spritegen_cli-0.1.0/tests/test_prompts.py +88 -0
  69. spritegen_cli-0.1.0/tests/test_settings.py +165 -0
  70. spritegen_cli-0.1.0/tests/test_sheet.py +492 -0
  71. spritegen_cli-0.1.0/tests/test_show.py +147 -0
  72. spritegen_cli-0.1.0/tests/test_skill.py +245 -0
  73. spritegen_cli-0.1.0/tests/test_stages.py +86 -0
  74. spritegen_cli-0.1.0/tests/test_upscale.py +311 -0
  75. spritegen_cli-0.1.0/tests/test_video.py +414 -0
  76. spritegen_cli-0.1.0/tests/test_workspace.py +719 -0
  77. spritegen_cli-0.1.0/tests/tests_fal_doubles.py +68 -0
  78. 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.