spritegen-cli 0.1.0__py3-none-any.whl
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/__init__.py +3 -0
- spritegen/atlas.py +99 -0
- spritegen/cli.py +178 -0
- spritegen/clip.py +81 -0
- spritegen/drive.py +163 -0
- spritegen/endpoints.py +113 -0
- spritegen/fal.py +228 -0
- spritegen/imaging.py +219 -0
- spritegen/ledger.py +143 -0
- spritegen/matting.py +136 -0
- spritegen/migrate.py +162 -0
- spritegen/prompts.py +96 -0
- spritegen/rrdb.py +91 -0
- spritegen/settings.py +127 -0
- spritegen/sheet.py +370 -0
- spritegen/skill/__init__.py +303 -0
- spritegen/skill/files/SKILL.md +553 -0
- spritegen/stages/__init__.py +490 -0
- spritegen/stages/anchor.py +215 -0
- spritegen/stages/board.py +68 -0
- spritegen/stages/matte.py +209 -0
- spritegen/stages/motion.py +164 -0
- spritegen/stages/pose.py +172 -0
- spritegen/stages/video.py +196 -0
- spritegen/upscale.py +444 -0
- spritegen/workspace.py +852 -0
- spritegen_cli-0.1.0.dist-info/METADATA +16 -0
- spritegen_cli-0.1.0.dist-info/RECORD +30 -0
- spritegen_cli-0.1.0.dist-info/WHEEL +4 -0
- spritegen_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"""The stage registry — what exists, and what each one needs before it.
|
|
2
|
+
|
|
3
|
+
The pipeline is not a queue. `motion` and `video` do the same job by different routes:
|
|
4
|
+
one transfers movement from a driving clip, the other animates the anchor directly.
|
|
5
|
+
Both hand over a board, and `matte` takes either. That is why `requires` means *any
|
|
6
|
+
one of these* rather than *this one*.
|
|
7
|
+
|
|
8
|
+
Order is checked by `workspace`, reading from here. Each stage is implemented by the
|
|
9
|
+
module of the same name beside this file, resolved only when it is about to run: the
|
|
10
|
+
registry describes the pipeline without importing Pillow, numpy or fal_client.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from importlib import import_module
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Option:
|
|
22
|
+
"""One command-line option a stage takes.
|
|
23
|
+
|
|
24
|
+
Declared here rather than by the stage module so `--help` is complete without
|
|
25
|
+
importing five modules and their image libraries. The stage reads the value off the
|
|
26
|
+
parsed namespace; nothing about how it is parsed is the stage's business.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
flags: tuple[str, ...]
|
|
30
|
+
help: str
|
|
31
|
+
kind: str = "str"
|
|
32
|
+
default: object = None
|
|
33
|
+
choices: tuple = ()
|
|
34
|
+
action: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Stage:
|
|
39
|
+
"""One stage of the pipeline.
|
|
40
|
+
|
|
41
|
+
`requires` names the stages of which **one** is enough to be complete. Empty marks
|
|
42
|
+
the root.
|
|
43
|
+
|
|
44
|
+
`produces` is the **kind** of artifact, which is also the directory it goes in. Those
|
|
45
|
+
were one word for as long as a stage made one thing; they are not the same word any
|
|
46
|
+
more. `anchor` makes box art, an anchor or an icon depending on what it was asked
|
|
47
|
+
for, and `board` closes the same frames into as many art directions as anyone wants
|
|
48
|
+
— so the kind, and the directory, follow the options rather than the stage:
|
|
49
|
+
|
|
50
|
+
- `kind_option` names the option that decides the kind. Absent, the kind is
|
|
51
|
+
`produces` and nothing varies.
|
|
52
|
+
- `variant_option` names the option that decides a subdirectory under the kind. That
|
|
53
|
+
is a dimension rather than a step: `sheet/sharp/` and `sheet/pixel-art/` are the
|
|
54
|
+
same sprite closed two ways, and neither is the sheet.
|
|
55
|
+
|
|
56
|
+
Both name an option this stage declares, and `workspace.output_dir` is the only
|
|
57
|
+
place that turns them into a path.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
name: str
|
|
61
|
+
requires: tuple[str, ...]
|
|
62
|
+
produces: str
|
|
63
|
+
summary: str
|
|
64
|
+
paid: bool
|
|
65
|
+
options: tuple[Option, ...] = ()
|
|
66
|
+
kind_option: str | None = None
|
|
67
|
+
variant_option: str | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
STAGES: tuple[Stage, ...] = (
|
|
71
|
+
Stage(
|
|
72
|
+
name="anchor",
|
|
73
|
+
requires=(),
|
|
74
|
+
produces="anchor",
|
|
75
|
+
summary="generate a single image: an anchor, box art or an icon",
|
|
76
|
+
paid=True,
|
|
77
|
+
kind_option="kind",
|
|
78
|
+
options=(
|
|
79
|
+
Option(
|
|
80
|
+
("--kind",),
|
|
81
|
+
"what this image is, and the directory it goes in: anchor is the neutral "
|
|
82
|
+
"sprite every later stage animates, box-art is the illustration that "
|
|
83
|
+
"settles the look, icon is a single object",
|
|
84
|
+
default="anchor",
|
|
85
|
+
choices=("anchor", "box-art", "icon"),
|
|
86
|
+
),
|
|
87
|
+
Option(("--prompt",), "the prompt itself"),
|
|
88
|
+
Option(("--prompt-file",), "a file holding the prompt"),
|
|
89
|
+
Option(
|
|
90
|
+
("--ref",),
|
|
91
|
+
"a reference image; repeat it, and the prompt addresses each one in order",
|
|
92
|
+
action="append",
|
|
93
|
+
),
|
|
94
|
+
Option(
|
|
95
|
+
("--size",),
|
|
96
|
+
"WIDTHxHEIGHT, or a fal preset (square_hd, square, portrait_4_3, "
|
|
97
|
+
"portrait_16_9, landscape_4_3, landscape_16_9, auto)",
|
|
98
|
+
default="1024x1024",
|
|
99
|
+
),
|
|
100
|
+
Option(
|
|
101
|
+
("--quality",),
|
|
102
|
+
"the endpoint's tier",
|
|
103
|
+
default="low",
|
|
104
|
+
choices=("low", "medium", "high"),
|
|
105
|
+
),
|
|
106
|
+
Option(("--count",), "how many images to generate", kind="int", default=1),
|
|
107
|
+
Option(("--transparent",), "cut the chroma background to alpha", action="store_true"),
|
|
108
|
+
Option(
|
|
109
|
+
("--chroma",),
|
|
110
|
+
"the key colour, 6 hex digits; with --transparent the prompt asks for "
|
|
111
|
+
"this field and the cut then removes it",
|
|
112
|
+
default="00b140",
|
|
113
|
+
),
|
|
114
|
+
Option(("--tol",), "chroma tolerance", kind="float", default=60.0),
|
|
115
|
+
Option(("--feather",), "the alpha band at the edge", kind="float", default=24.0),
|
|
116
|
+
Option(
|
|
117
|
+
("--chroma-mode",),
|
|
118
|
+
"flood takes only what the border reaches; global takes the key anywhere",
|
|
119
|
+
default="flood",
|
|
120
|
+
choices=("flood", "global"),
|
|
121
|
+
),
|
|
122
|
+
Option(("--despill",), "pull the leaked key out of edge pixels", action="store_true"),
|
|
123
|
+
Option(
|
|
124
|
+
("--despill-radius",),
|
|
125
|
+
"how far from transparent to despill",
|
|
126
|
+
kind="int",
|
|
127
|
+
default=2,
|
|
128
|
+
),
|
|
129
|
+
Option(
|
|
130
|
+
("--pixelart",),
|
|
131
|
+
"recover the native pixel grid and rewrite on it",
|
|
132
|
+
action="store_true",
|
|
133
|
+
),
|
|
134
|
+
Option(("--colors",), "quantise to N colours with --pixelart", kind="int", default=0),
|
|
135
|
+
Option(
|
|
136
|
+
("--scale",),
|
|
137
|
+
"integer upscale after the grid is recovered",
|
|
138
|
+
kind="int",
|
|
139
|
+
default=1,
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
Stage(
|
|
144
|
+
name="motion",
|
|
145
|
+
requires=("anchor",),
|
|
146
|
+
produces="frames",
|
|
147
|
+
summary="transfer movement from a driving clip onto the anchor or a pose",
|
|
148
|
+
paid=True,
|
|
149
|
+
options=(
|
|
150
|
+
Option(
|
|
151
|
+
("--pose",),
|
|
152
|
+
"start the clip from this animation's pose instead of the anchor, and "
|
|
153
|
+
"end it on that pose's last frame",
|
|
154
|
+
),
|
|
155
|
+
Option(
|
|
156
|
+
("--sheet",),
|
|
157
|
+
"the reference sheet the movement is taken from; a game asset, not the "
|
|
158
|
+
"asset's own, which is why it is named here",
|
|
159
|
+
),
|
|
160
|
+
Option(("--row",), "which row of the sheet, one animation each", kind="int", default=0),
|
|
161
|
+
Option(
|
|
162
|
+
("--sheet-frames",),
|
|
163
|
+
"how many cells of that row (default: the whole row)",
|
|
164
|
+
kind="int",
|
|
165
|
+
default=0,
|
|
166
|
+
),
|
|
167
|
+
Option(("--clip",), "a driving clip to use instead of building one from a sheet"),
|
|
168
|
+
Option(
|
|
169
|
+
("--endpoint",),
|
|
170
|
+
"which motion endpoint",
|
|
171
|
+
default="wan-motion",
|
|
172
|
+
choices=("wan-motion", "wan-animate", "one-to-all"),
|
|
173
|
+
),
|
|
174
|
+
Option(("--prompt",), "an optional hint, where the endpoint takes one", default=""),
|
|
175
|
+
Option(("--target",), "the driving clip's square side", kind="int", default=720),
|
|
176
|
+
Option(("--drive-fps",), "the driving clip's frame rate", kind="int", default=12),
|
|
177
|
+
Option(("--seconds",), "how long the driving clip runs", kind="float", default=4.0),
|
|
178
|
+
Option(
|
|
179
|
+
("--frames",),
|
|
180
|
+
"how many frames to pull out of the result",
|
|
181
|
+
kind="int",
|
|
182
|
+
default=6,
|
|
183
|
+
),
|
|
184
|
+
Option(("--start",), "where extraction begins, 0..1", kind="float", default=0.0),
|
|
185
|
+
Option(("--end",), "where extraction ends, 0..1", kind="float", default=1.0),
|
|
186
|
+
Option(("--pick",), "exact frame indices, comma-separated"),
|
|
187
|
+
Option(("--cols",), "how wide to pack the board", kind="int", default=3),
|
|
188
|
+
Option(
|
|
189
|
+
("--set",),
|
|
190
|
+
"an endpoint option, NAME=VALUE; checked against that endpoint before "
|
|
191
|
+
"anything is spent",
|
|
192
|
+
action="append",
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
Stage(
|
|
197
|
+
name="pose",
|
|
198
|
+
requires=("anchor",),
|
|
199
|
+
produces="pose",
|
|
200
|
+
summary="the frame an animation starts from, and the one it ends on",
|
|
201
|
+
paid=True,
|
|
202
|
+
# Not `--name`: the asset is already the positional `name`, and argparse would
|
|
203
|
+
# write both to the same attribute — the pose would be generated for an asset
|
|
204
|
+
# called "attack".
|
|
205
|
+
variant_option="animation",
|
|
206
|
+
options=(
|
|
207
|
+
Option(
|
|
208
|
+
("--animation",),
|
|
209
|
+
"which animation this pose is for, and the directory it goes in "
|
|
210
|
+
"under pose/ — attack, cast, death",
|
|
211
|
+
default="",
|
|
212
|
+
),
|
|
213
|
+
Option(("--prompt",), "the starting pose"),
|
|
214
|
+
Option(("--prompt-file",), "a file holding it"),
|
|
215
|
+
Option(
|
|
216
|
+
("--end-prompt",),
|
|
217
|
+
"a different last frame; without it the last frame is the first, so the "
|
|
218
|
+
"cycle closes where it began",
|
|
219
|
+
),
|
|
220
|
+
Option(("--end-prompt-file",), "a file holding the last frame's prompt"),
|
|
221
|
+
Option(
|
|
222
|
+
("--no-end",),
|
|
223
|
+
"write no last frame at all, for a movement that does not return",
|
|
224
|
+
action="store_true",
|
|
225
|
+
),
|
|
226
|
+
Option(
|
|
227
|
+
("--ref",),
|
|
228
|
+
"an extra reference after the anchor, which is always the first",
|
|
229
|
+
action="append",
|
|
230
|
+
),
|
|
231
|
+
Option(("--size",), "WIDTHxHEIGHT, or a fal preset", default="1024x1024"),
|
|
232
|
+
Option(
|
|
233
|
+
("--quality",),
|
|
234
|
+
"the endpoint's tier",
|
|
235
|
+
default="low",
|
|
236
|
+
choices=("low", "medium", "high"),
|
|
237
|
+
),
|
|
238
|
+
Option(("--transparent",), "cut the chroma background to alpha", action="store_true"),
|
|
239
|
+
Option(("--chroma",), "the key colour, 6 hex digits", default="00b140"),
|
|
240
|
+
Option(("--tol",), "chroma tolerance", kind="float", default=60.0),
|
|
241
|
+
Option(("--feather",), "the alpha band at the edge", kind="float", default=24.0),
|
|
242
|
+
Option(
|
|
243
|
+
("--chroma-mode",),
|
|
244
|
+
"flood takes only what the border reaches; global takes the key anywhere",
|
|
245
|
+
default="flood",
|
|
246
|
+
choices=("flood", "global"),
|
|
247
|
+
),
|
|
248
|
+
Option(("--despill",), "pull the leaked key out of edge pixels", action="store_true"),
|
|
249
|
+
Option(
|
|
250
|
+
("--despill-radius",),
|
|
251
|
+
"how far from transparent to despill",
|
|
252
|
+
kind="int",
|
|
253
|
+
default=2,
|
|
254
|
+
),
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
Stage(
|
|
258
|
+
name="video",
|
|
259
|
+
requires=("anchor",),
|
|
260
|
+
produces="frames",
|
|
261
|
+
summary="animate the anchor or a pose directly, with no driving clip",
|
|
262
|
+
paid=True,
|
|
263
|
+
options=(
|
|
264
|
+
Option(
|
|
265
|
+
("--pose",),
|
|
266
|
+
"start the clip from this animation's pose instead of the anchor, and "
|
|
267
|
+
"end it on that pose's last frame",
|
|
268
|
+
),
|
|
269
|
+
Option(("--prompt",), "the movement to animate"),
|
|
270
|
+
Option(("--prompt-file",), "a file holding the prompt"),
|
|
271
|
+
Option(
|
|
272
|
+
("--model",),
|
|
273
|
+
"which video endpoint",
|
|
274
|
+
default="grok",
|
|
275
|
+
choices=("grok", "grok-i2v", "seedance", "kling"),
|
|
276
|
+
),
|
|
277
|
+
Option(
|
|
278
|
+
("--ref",),
|
|
279
|
+
"an extra reference for grok, after the anchor; the prompt addresses "
|
|
280
|
+
"each by <IMAGE_0>, <IMAGE_1> in order",
|
|
281
|
+
action="append",
|
|
282
|
+
),
|
|
283
|
+
Option(
|
|
284
|
+
("--loop",),
|
|
285
|
+
"send the anchor as the last frame too, so the cycle closes where it began",
|
|
286
|
+
action="store_true",
|
|
287
|
+
),
|
|
288
|
+
Option(("--duration",), "seconds of output", kind="int", default=4),
|
|
289
|
+
Option(
|
|
290
|
+
("--resolution",),
|
|
291
|
+
"clip resolution",
|
|
292
|
+
default="480p",
|
|
293
|
+
choices=("480p", "720p", "1080p"),
|
|
294
|
+
),
|
|
295
|
+
Option(
|
|
296
|
+
("--aspect",),
|
|
297
|
+
"clip aspect for grok",
|
|
298
|
+
default="1:1",
|
|
299
|
+
choices=("16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16"),
|
|
300
|
+
),
|
|
301
|
+
Option(
|
|
302
|
+
("--negative",),
|
|
303
|
+
"negative prompt, where the endpoint takes one",
|
|
304
|
+
default="blur, distort, low quality, camera movement",
|
|
305
|
+
),
|
|
306
|
+
Option(("--frames",), "how many frames to pull out", kind="int", default=6),
|
|
307
|
+
Option(
|
|
308
|
+
("--start",),
|
|
309
|
+
"where extraction begins, 0..1 of the clip; the first frame of an "
|
|
310
|
+
"image-to-video clip is the input image standing still",
|
|
311
|
+
kind="float",
|
|
312
|
+
default=0.0,
|
|
313
|
+
),
|
|
314
|
+
Option(("--end",), "where extraction ends, 0..1", kind="float", default=1.0),
|
|
315
|
+
Option(
|
|
316
|
+
("--pick",),
|
|
317
|
+
"exact frame indices, comma-separated; ignores frames, start and end",
|
|
318
|
+
),
|
|
319
|
+
Option(("--cols",), "how wide to pack the board", kind="int", default=3),
|
|
320
|
+
),
|
|
321
|
+
),
|
|
322
|
+
Stage(
|
|
323
|
+
name="matte",
|
|
324
|
+
requires=("motion", "video"),
|
|
325
|
+
produces="cutout",
|
|
326
|
+
summary="cut the frames' background by segmentation, locally or paid",
|
|
327
|
+
paid=True,
|
|
328
|
+
options=(
|
|
329
|
+
Option(
|
|
330
|
+
("--backend",),
|
|
331
|
+
"local runs BiRefNet here through rembg and costs nothing; fal is the "
|
|
332
|
+
"paid endpoint. Without this, whatever settings say (local by default)",
|
|
333
|
+
choices=("local", "fal"),
|
|
334
|
+
),
|
|
335
|
+
Option(
|
|
336
|
+
("--model",),
|
|
337
|
+
"the rembg weights, with --backend local; without it, what settings say",
|
|
338
|
+
),
|
|
339
|
+
Option(
|
|
340
|
+
("--variant",),
|
|
341
|
+
"birefnet weights at the paid endpoint; Matting gives continuous alpha, "
|
|
342
|
+
"which is what hair needs",
|
|
343
|
+
default="Matting",
|
|
344
|
+
choices=(
|
|
345
|
+
"General Use (Light)",
|
|
346
|
+
"General Use (Light 2K)",
|
|
347
|
+
"General Use (Heavy)",
|
|
348
|
+
"Matting",
|
|
349
|
+
"Portrait",
|
|
350
|
+
"General Use (Dynamic)",
|
|
351
|
+
),
|
|
352
|
+
),
|
|
353
|
+
Option(
|
|
354
|
+
("--resolution",),
|
|
355
|
+
"operating resolution",
|
|
356
|
+
default="2048x2048",
|
|
357
|
+
choices=("1024x1024", "2048x2048", "2304x2304"),
|
|
358
|
+
),
|
|
359
|
+
Option(
|
|
360
|
+
("--no-refine",),
|
|
361
|
+
"turn off edge colour decontamination, which is what kills the halo",
|
|
362
|
+
action="store_true",
|
|
363
|
+
),
|
|
364
|
+
Option(("--mask",), "write the alpha mask beside the cut", action="store_true"),
|
|
365
|
+
Option(
|
|
366
|
+
("--no-join",),
|
|
367
|
+
"matte each image on its own instead of joining them into one call",
|
|
368
|
+
action="store_true",
|
|
369
|
+
),
|
|
370
|
+
),
|
|
371
|
+
),
|
|
372
|
+
Stage(
|
|
373
|
+
name="board",
|
|
374
|
+
requires=("matte",),
|
|
375
|
+
produces="sheet",
|
|
376
|
+
summary="close the frames into a sheet row, in square cells",
|
|
377
|
+
paid=False,
|
|
378
|
+
variant_option="art",
|
|
379
|
+
options=(
|
|
380
|
+
Option(("--grid",), "the board's grid, COLSxROWS", default="3x2"),
|
|
381
|
+
Option(
|
|
382
|
+
("--frames",),
|
|
383
|
+
"how many cells to read (default: the whole grid)",
|
|
384
|
+
kind="int",
|
|
385
|
+
default=0,
|
|
386
|
+
),
|
|
387
|
+
Option(("--cell",), "the sheet cell, in pixels", kind="int", default=166),
|
|
388
|
+
Option(
|
|
389
|
+
("--scale",),
|
|
390
|
+
"the scale (default: the largest that fits — a whole factor while one "
|
|
391
|
+
"fits, and the exact fraction that brings a larger figure down)",
|
|
392
|
+
kind="float",
|
|
393
|
+
default=0.0,
|
|
394
|
+
),
|
|
395
|
+
Option(
|
|
396
|
+
("--fit",),
|
|
397
|
+
"bring a figure larger than the cell down to it; implied by "
|
|
398
|
+
"--art sharp, and off otherwise so an overflow stays visible",
|
|
399
|
+
action="store_true",
|
|
400
|
+
),
|
|
401
|
+
Option(
|
|
402
|
+
("--art",),
|
|
403
|
+
"the art direction to close this row in, and the directory under "
|
|
404
|
+
"sheet/ it goes in: sharp lands a crisp render on the cell, pixel-art "
|
|
405
|
+
"recovers a native grid the art already has, as-is does neither",
|
|
406
|
+
default="as-is",
|
|
407
|
+
choices=("as-is", "sharp", "pixel-art"),
|
|
408
|
+
),
|
|
409
|
+
Option(("--colors",), "quantise the whole row to N colours", kind="int", default=0),
|
|
410
|
+
Option(
|
|
411
|
+
("--alpha-floor",),
|
|
412
|
+
"alpha under this is background smear and goes to nothing, with --art sharp",
|
|
413
|
+
kind="int",
|
|
414
|
+
default=16,
|
|
415
|
+
),
|
|
416
|
+
Option(
|
|
417
|
+
("--alpha-ceil",),
|
|
418
|
+
"alpha over this is the figure and goes to solid, with --art sharp",
|
|
419
|
+
kind="int",
|
|
420
|
+
default=240,
|
|
421
|
+
),
|
|
422
|
+
Option(("--chroma",), "cut this key off the whole board first, 6 hex digits"),
|
|
423
|
+
Option(("--tol",), "chroma tolerance", kind="float", default=60.0),
|
|
424
|
+
Option(("--feather",), "the alpha band at the edge", kind="float", default=24.0),
|
|
425
|
+
Option(
|
|
426
|
+
("--chroma-mode",),
|
|
427
|
+
"flood takes only what the border reaches; global takes the key anywhere",
|
|
428
|
+
default="flood",
|
|
429
|
+
choices=("flood", "global"),
|
|
430
|
+
),
|
|
431
|
+
Option(("--no-gif",), "skip the looping preview", action="store_true"),
|
|
432
|
+
Option(("--fps",), "the preview's frames per second", kind="int", default=7),
|
|
433
|
+
),
|
|
434
|
+
),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
BY_NAME: dict[str, Stage] = {stage.name: stage for stage in STAGES}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def get(name: str) -> Stage:
|
|
441
|
+
"""The stage called `name`.
|
|
442
|
+
|
|
443
|
+
Fails with the full list rather than a KeyError: whoever mistyped wants to know
|
|
444
|
+
what exists, not which key was missing.
|
|
445
|
+
"""
|
|
446
|
+
try:
|
|
447
|
+
return BY_NAME[name]
|
|
448
|
+
except KeyError:
|
|
449
|
+
known = ", ".join(stage.name for stage in STAGES)
|
|
450
|
+
raise KeyError(f"unknown stage {name!r}; there are: {known}") from None
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def order() -> tuple[str, ...]:
|
|
454
|
+
"""Every stage, each after a prerequisite of its own.
|
|
455
|
+
|
|
456
|
+
This is presentation order, not a path to walk: `motion` and `video` are
|
|
457
|
+
alternatives and both appear.
|
|
458
|
+
"""
|
|
459
|
+
done: list[str] = []
|
|
460
|
+
pending = [stage.name for stage in STAGES]
|
|
461
|
+
while pending:
|
|
462
|
+
ready = [
|
|
463
|
+
name
|
|
464
|
+
for name in pending
|
|
465
|
+
if not BY_NAME[name].requires
|
|
466
|
+
or any(dep in done for dep in BY_NAME[name].requires)
|
|
467
|
+
]
|
|
468
|
+
if not ready:
|
|
469
|
+
raise RuntimeError(f"cycle in the stage registry, stuck on: {pending}")
|
|
470
|
+
for name in ready:
|
|
471
|
+
done.append(name)
|
|
472
|
+
pending.remove(name)
|
|
473
|
+
return tuple(done)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def implementation(name: str) -> ModuleType:
|
|
477
|
+
"""The module implementing `name`, imported now and not before.
|
|
478
|
+
|
|
479
|
+
A stage that has not been written yet fails here, naming the module that is
|
|
480
|
+
missing — not at package import, which would take the whole CLI down over one
|
|
481
|
+
stage.
|
|
482
|
+
"""
|
|
483
|
+
stage = get(name)
|
|
484
|
+
try:
|
|
485
|
+
return import_module(f"spritegen.stages.{stage.name}")
|
|
486
|
+
except ModuleNotFoundError as exc:
|
|
487
|
+
raise NotImplementedError(
|
|
488
|
+
f"stage {stage.name!r} has no implementation yet "
|
|
489
|
+
f"(spritegen/stages/{stage.name}.py)"
|
|
490
|
+
) from exc
|