Skip to content

v4.0.0 - Game features removed leaving only sprites, no picture shown again by default (`--fill-held off`) save meant holds, `inspect-holds` compares deliveries with hold lists, and `video-chain` plays keyed clip pieces as one timed move

Choose a tag to compare

@aldegad aldegad released this 06 Nov 07:27
· 16 commits to main since this release

Upgrading from 3.15.0: sprite-gen sound, sprite-gen game-pack and sprite-gen ui-kit are removed, with gen --layout-guide ui-kit and background-tile --axis xy. These game-making features were added for building games. Making games is outside the scope of this engine, which makes sprites; features that belong to sprites stay. Files an earlier version wrote (sound folders, game packs, UI kit folders) stay on disk; nothing reads them.

Upgrading from 3.15.0: no picture is shown again by default. Output changes where a picture was shown again: a loop whose last cells or wrap step showed one picture, a loop whose repairs left a run, an aligned set whose loops or alignment showed one, a stalled video-stop, a scene layer cut from a clip shown faster than its cells, and the cell-boundary frames of 24, 60 and 15 fps strips in a scene. Every output that showed no picture again is unchanged (the six cuts pinned before, a stop with no held cell, a set aligned with no held cell, pinned on commit 4f5ccd5, and scenes at 25, 20, 30, 12 and 48 fps), and so is every output cut with --fill-held off (except the scene boundary-frame fix, which --fill-held off does not undo). Metadata grows: the strip fps and holds; the loop report cut_args, held_fill.again, held_fill.wrap_held, held_fill.before.wrap and keep on kept runs; cycle_align.cut_again; the Grok report held; bake.json held; the scene report's inspection.repeats and fill_held.

Removed

Removed Use
sprite-gen sound (ElevenLabs sound effects and instrumental music with Opus and AAC exports, a SQLite request cache and a browser loop helper), the package sprite_gen.audio (sound, provider, cache, processing, manifest, playback and the browser helper sprite-gen-audio-v2.mjs, which the wheel no longer carries), docs/sound.md No replacement.
The ElevenLabs provider: sprite_gen.gen.address.ELEVENLABS and its entry in address.PROVIDERS, SPRITE_GEN_ELEVENLABS_BASE_URL, sprite_gen.gen.retry.ELEVENLABS_BUSY and the ElevenLabs rows of retry.UNTAKEN and retry.ACT_FIRST; ELEVENLABS_API_KEY is no longer read No replacement. sprite-gen capabilities no longer lists the sound verb or the elevenlabs provider; its providers are openai, xai and anthropic.
sprite-gen game-pack (game atlases with playback policies, sounds, version 2 images, tiles and UI nine-slice entries), sprite_gen.compose.game_pack, docs/game-pack.md compose-atlas and export-aseprite (Phaser and Flame) for engine exports.
sprite_gen.spec.assets.load_asset_states, which only game-pack called load_asset and PLAYBACK_FIELDS stay.
sprite-gen ui-kit (splitting a game UI kit sheet into nine-slice elements), sprite_gen.frames.ui_kit, docs/ui-kit.md No replacement. Its "Alpha cleanup" section moved to docs/sheet-slicing.md ("Alpha cleanup" under "Transparent sheets"); slice-sheet --alpha-floor and --alpha-edge are unchanged.
gen --layout-guide ui-kit, gen.ui_kit_guide_size and the ui_kit_guide argument of gen.still_prompt No replacement. The bare --layout-guide and --layout-guide figure are unchanged.
background-tile --axis xy (one tile that repeats both ways, made for game ground tiles), sprite_gen.background.tile.inspect_tile_xy and the cyclic seam it used No replacement. --axis x and --axis y are unchanged, and so are their reports.
  • Stays the same: non-looping playback (the video-loop once cut, the loop, hold_last and end_frame metadata, the play modes), effects (pixel-draw --kind effect, brightness alpha, lifetime cuts), video-stop, slice-sheet --scale sheet and --fit contain, the alpha haze cleanup, the refusal of a painted transparency checkerboard, logo, compose-atlas and export-aseprite.
  • Command line: sprite-gen sound, sprite-gen game-pack and sprite-gen ui-kit end as an unknown command with argparse exit code 2; gen --layout-guide ui-kit and background-tile --axis xy end as an invalid choice with argparse exit code 2.
  • Dependencies: the removed features declared no Python dependency. ffmpeg, ffprobe and the AAC encoder stay required by the video features that use them (face-bake, the video source reading, sprite-gen video); only the sound checks for them were removed.
  • Old changelog links: the links in the v2.45.0 section that pointed at docs/game-pack.md and docs/ui-kit.md now point at those files in the v2.45.0 tag on GitHub; their wording is unchanged.
  • Tests: Removed tests/audio/ (test_sound.py, render_playback.mjs, prepare_playback_fixture.py), tests/compose/test_game_pack.py, tests/compose/test_game_pack_audio.py, tests/compose/test_game_pack_v2.py, tests/frames/test_ui_kit.py and tests/background/test_tile_xy.py; the game pack test of tests/video/test_stop.py and of tests/frames/test_slice_sheet_scale.py, the load_asset_states test of tests/spec/test_assets.py, the three UI kit guide tests of tests/gen/test_layout_guide.py, and the ElevenLabs cases of tests/gen/test_provider_address.py and tests/workflow/test_capabilities.py. Changed: tests/gen/test_painted_checkerboard.py refuses the checkerboard through gen with the figure layout guide; tests/workflow/test_capabilities.py reads a flag's --no- alias off prepare and a required exclusive group off video-sway; tests/packaging/test_curation_asset_packaging.py checks that the wheel carries the face player sprite_gen/video/sprite-gen-face-v1.mjs.

No picture shown again by default

The clip is a walk 24 frames long whose loop comes back to its first picture and stays there: when cut to 26 cells, its last two cells (24 and 25) show the same picture as cell 0, so three cells in a row across the loop's wrap show one picture and the foot stops there on every loop. Both panels cut the same frames with rife-v4.25 and default settings. The left panel is the cut made by 3.15.0, labelled "3.15.0 poses 25/26", which kept those two last cells as filmed because a run that ended on a loop's last cell was not filled and the step across the wrap was not read, while the right panel is the cut made by 4.0.0, labelled "4.0.0 poses 26/26", where 4.0.0 reads the loop as a ring and makes cells 24 and 25 again with RIFE between cell 23 and cell 0 (cell 0 unchanged), each cell RIFE made is outlined in blue on the right, and the foot keeps moving through the wrap.

Two panels side by side, looping at 24 fps, show 26 cells of a synthetic test figure from the repository's tests (a blue torso, a skin-coloured head, a dark foot going round a circle) walking on a white background, with no real footage.

  • Changed: video-loop reads a loop as a ring, so a run that ends on the loop's last cell and a cut one cell too long are made again (held_fill.wrap_same, held_fill.before.wrap, held_fill.wrap_held). Before, a run that ended on a loop's last cell was kept as filmed and the last cell and the first were never read as a run. Now the run that ends on the last cell is made again between its first cell and the loop's first cell (its next pose is the first cell, across the wrap); the first cell is never changed. When the last cell (or last cells) shows the picture of the first cell, the wrap steps from a picture to the same picture: those last cells are made again between the cell before them and the first cell, and the first cell stays, read on the cut's frames as filmed (held_fill.wrap_same). A run joined across the wrap (cells at the end plus cells at the start, all one picture) longer than HOLD_MAX 3 cells in total is a hold the motion means: both parts are kept as filmed. A frame made at the wrap is judged as every frame the fill makes; refused, the cells are kept as filmed and a warning: line names them and the faults. held_fill.before.wrap records the run across the wrap (start, length, joined, filled, made), held_fill.wrap_held says the cut as filmed showed one picture across its wrap, and a loop's last cell that shows its first cell's picture is not counted as a pose.
  • New: the cut is read again after its repairs (held_fill.again). The fill runs where it ran before: after the anchor and source_projection, before the ghost screen, the wrap blend, the part pop and the jump repair. Once the repairs have run (the jump repair, the ghost RIFE frames, the eased retime, the overlapped wrap, the wrap blend, the part pop, the seam bridge), the cut is read again for held cells, and a run a repair left (one the first fill did not see) is made again the same way, judged the same way, with the same keeps. It is recorded as held_fill.again in the loop report and the strip metadata (before, made, frames, cells, poses, held_runs); its warning: lines start with read again after the repair:. A filmed ghost the repair gave way to the frame beside it (ghost_given_way) is not tried again; its own warning already names it. The repairs themselves are unchanged: they keep the cut's cell count and beat and are not a fill. --fill-held off turns the read-again off too. Measured on the tests' synthetic walker without a staff: its repairs left four runs and one wrap step showing a picture again (42/48 poses/cells); read again, it is 48/48.
  • New: a run kept as the motion means it carries keep, held_fill.KEEPS names the kinds, and the strip metadata gains holds and fps (held_fill.intended, held_fill.strip_holds). The rules for a hold the motion means are unchanged: a run longer than HOLD_MAX 3 cells (motion-hold); every run of a cut drawn on twos or threes (drawn); the opening pose (opening) and the closing pose (closing) of a cut played once (one-shot, once, once-return); hold_last; atlas clones. Each run kept for one of these reasons carries keep (its kind) in held_fill.before.held_runs, and held_fill.KEEPS names the kinds: motion-hold, drawn, opening, closing. The strip metadata (<name>.strip.json) gains holds: a list of {from, to, kind, reason}, one per kept run, as cells of the strip (from and to both included), the same list form a renderer's hold list uses; inspect-holds and scene-render read it as holds meant. held_fill.intended(record) gives the runs, held_fill.strip_holds maps them onto the strip's cells. The strip metadata also gains fps, the strip's exact rate (cells over the cycle's seconds, rounded to 6 places), next to delay_ms, which stays rounded to hundredths.
  • New: the loop report records cut_args, and loop.cut_again cuts the same frames again. cut_args is every argument the cut was asked with. loop.cut_again(report, out_dir, report_path=..., **changes) cuts the same frames again with those arguments and the changes given (for example fill_held="off"), with no provider call. It refuses a report from before cut_args was recorded, and frames that moved.
  • Changed: video-cycle-align (and video-set --align-cycles auto) aligns as filmed and fills at the end (cycle_align.cut_again, cycle.source/, --fill-held); new video-cycle-align --out-fps. The alignment makes its frames between from frames as filmed, never from frames the fill made. A loop never aligned whose held cells video-loop made again (held_fill.made, or held_fill.again.made) is cut again from its frames with --fill-held off, using its report's cut_args (no provider call); that cut is kept as cycle.source/ (its frames, strip and strip record) and aligned, and cycle_align.cut_again records report, fill_held (off), cells, made_before. If it cannot be cut again (no report beside it, a report from before cuts recorded their arguments, frames moved, the cut again took another window of frames), the alignment stops, names the loop and why, and says: cut it again with video-loop --fill-held off and align that. Then each loop as aligned is read for held cells and filled at the end (--fill-held auto, the default), as video-loop fills a loop: its first cell kept, a loop drawn on twos or threes and a hold longer than three cells kept, each frame made judged with the held fault among the others; this also catches a frame the alignment's own resampling took twice (--between nearest, or a frame made refused and the nearer source frame taken). The strip record's and the report row's held_fill say what was done; holds lists what was kept. --fill-held off delivers the aligned loops as they come out of the alignment. --out-fps F gives every loop of the set at F, above the loops' own rate, once aligned: each cell at its own time, made between the cells beside it, never a cell shown again, every loop the same number of cells; the strip metadata says out_fps and source_fps; a rate not above the loops' own is refused; a loop already aligned at a higher rate is aligned again from its cycle.source/ at its source rate. A set whose loops show no held cell is aligned as before, pinned by a picture digest measured on the engine before this change (commit 4f5ccd5).
  • Changed: video-set cuts the walks and runs it aligns with --fill-held off and lets the alignment fill them; an item no alignment took is cut again filled (loop.cut_again.why not aligned); video-set --out-fps with alignment is no longer refused. The walks and runs a set aligns (filmed in two or more directions, --align-cycles auto) are cut with --fill-held off; the alignment fills them at the end. A standalone video-loop and every other item still fill as before. An item cut to be aligned that no alignment took (for example the alignment was skipped) is cut again with the fill on (loop.cut_again), so what the set delivers is filled. The aligned walks and runs are given --out-fps once aligned (video-cycle-align --out-fps), the rest by video-loop --out-fps. Each item's loop.poses and loop.held_fill are those of the loop as delivered (after the alignment where it ran).
  • Changed: an effect's lifetime is named, not filled. A lifetime cut is read for held cells and each run is named in a warning: line (cells A-B of the effect's lifetime show one picture, played as filmed, an effect's lifetime is not filled). No frame is made. Measured on four synthetic effects (a slash, sparks, a burst, a flame; glowing on black, keyed by brightness alpha, 24 fps), three stalls each, filled with rife-v4.25: 12 frames were made and the judge refused none, all on the first try; two were broken (the sparks smeared into one grey mass, the flame double-exposed) and a third left a flash that should have faded as a grey disc. The ghost screen read none of the four (drawn part-covered), so the judge cannot tell a bad frame of an effect. Filling would deliver those frames, so the lifetime stays a warning.
  • Changed: video-stop fills held cells by default (--fill-held auto); its sidecar records held_fill and holds. Before, a stop was read for held cells and recorded held_fill with mode off; it never made a frame. Now each held run's cells after its first are made again with RIFE between the run's first cell and the next pose, as video-loop makes a cut played once. The opening pose, the closing still, a run longer than three cells and a clip drawn on twos or threes are kept. --fill-held off plays every frame as filmed, as before, with a warning line for each held run. A frame between refused keeps its run as filmed with a warning; with no RIFE installed every held cell is played as filmed with a warning naming the install command and held_fill.unmade. A RIFE that is found but fails stops the stop (report status: failed) with: fix RIFE, or pass --fill-held off. A stop with no held cell is the same pictures as before.
  • New: video, video-extend, video-edit, video-resume (Grok clips) and face-bake read for a frame shown again and warn; the report gains held, bake.json gains held. The published mp4 is read for a frame shown again (frames decoded by ffmpeg, the held_fill.same_steps rule; an opaque clip's background, the most common colour of a 4 px border when it covers half the border, is left out). A run that is not an opening, a closing or a run longer than three frames prints video: warning: clip.mp4 shows one picture on frames A-B .... Nothing is filled: the clip is kept as the provider made it; video-loop makes such cells again when the clip is cut. Without ffmpeg the report says read: false and a warning line, and the clip is published as before. face-bake reads the baked body cells; a run within a clip, across the loop's wrap or where a clip comes back to the loop prints face-bake: warning: frames A-B show one body picture .... Runs the clip means (longer than three frames, the opening or closing of a clip played once, cells a body row of the pack lists as holds) are recorded without a warning. No frame is made and the verdict does not change.
  • Fixed: scene-render and scene-inspect read a strip's cells at delay_ms, rounded to hundredths, so a scene frame on a cell boundary showed the cell before it (spec.assets.frame_seconds, SNAP_MS). delay_ms is 41.67 ms for 24 fps. Where the rounded delay is longer than the exact one (24, 60 and 15 fps strips), a 24 fps strip in a 24 fps scene showed its first cell twice, and in a 20 fps scene frames 5, 10 and 15 showed cells 5, 11 and 17 where 0.25 s x 24 = 6, 12 and 18 are right. The cells are now read at the strip's fps where its sidecar gives one, and a strip from before (no fps) whose delay_ms is within 0.01 ms of a whole rate is read at that whole rate. Those boundary frames change; --fill-held off does not undo this. Strips at 25, 20, 30, 12 and 48 fps render byte for byte as before (pinned).
  • New: scene-render --fill-held auto gives a layer cut from a filmed clip at the scene's times; the report gains inspection.repeats and fill_held; new scene-render --between-cache DIR. By default, a layer whose asset is a cut of a filmed clip (a video-loop or video-stop strip, its sidecar names source_cut) and would show a cell again at the scene's frames (a 24 fps cut in a 60 fps scene, a playback_rate under 1) is given at the scene's times: each frame the cell its time falls on, or RIFE's frame between the two cells either side at its own time, judged as video-loop judges a frame made for a held cell. Held on purpose stays held: before the layer's start, a one-shot's end and hold_last, a run its strip lists in holds, and every layer that is not a video cut (a still, an atlas row, a pixel drawing), which holds its cells as drawn. A layer that shows no cell again keeps its cells as its timing picks them (at the strip's exact rate) and makes no frame; every strip's cells are now read at its exact rate, so a frame on a cell boundary of a 24, 60 or 15 fps strip can change (a 24 fps strip in a 20 fps scene showed frames 5, 10 and 15 as cells 5, 11 and 17 before, and now as cells 6, 12 and 18); strips at 25, 20, 30, 12 and 48 fps render byte for byte as before (pinned in tests). A frame between refused shows the nearer cell again and is named; with no RIFE installed the cells are given as cut and a warning names the frames and the install command; a RIFE that fails stops the render, naming --fill-held off. --fill-held off gives every layer's cells as its timing picks them. inspection.repeats is each layer's table of scene frames that show the cell the frame before shows: each run, the cell, whether it is held on purpose and why; fill_held is in the report. --between-cache DIR keeps the frames made between cells across renders, under the bytes of the two cells, the time and the RIFE that made them, and takes them from there on the next render; the report's between_cache counts hits and misses. Nothing is kept without it.
  • New: sprite-gen inspect-holds (qa) reads a delivery for frames that show a figure's picture again and compares them with hold lists (--holds, --gate, --key, --box, --log, --log-figure). It reads frames with alpha, a keyed clip with --key, a figure's box with --box, or a renderer's log of cells with --log and --log-figure, with the same rule as held_fill.same_steps on the figure's pixels. It compares them with hold lists (--holds, a strip's holds, the list form a renderer uses) and reports listed and unlisted holds; --gate exits 1 where any hold is on no list. A render that draws a figure at a new scale or sub-pixel place on every frame is read by its --log. Its full documentation is docs/asset-tools.md "Frames shown again".
  • Fixed: inspect-holds reads a frame's largest part as a figure however few pixels it has, reads nothing in a --box that holds no pixel, exits 2 where no frame was read against the frame before, and reports steps and inspected; the Grok clip reading of video, video-extend, video-edit and video-resume does not read a clip whose every frame is the canvas alone. A part is a speck, and is not read, where it holds under 2 % of the pixels of the frame's largest part, or under 64 px; the 64 px floor applied to every part, the frame's largest included, so a frame whose only figure had under 64 px read no figure at all: a 7 x 9 block (63 px) shown on three identical frames of a 24 x 24 clear canvas read an empty table and --gate exited 0 with nothing on stderr, although its frames 1 and 2 show the picture of the frame before, where an 8 x 8 block (64 px) read 2 unlisted holds and --gate exited 1. Now the 64 px floor is never more than half the frame's largest part, so the largest part is always a figure and a part half its size or more is a figure too (two small figures in one frame are both read); both blocks read 2 unlisted holds (frames 1-2) and --gate exits 1, and where a frame's largest part has 128 px or more the rule is exactly what it was. Dots of 1, 2 and 3 px far from the figure, the same on every frame, are still not read. With --box, two frames whose box held no pixel with alpha over 8 (clear there, or past the frame's edge) read as one picture shown again, so a box over clear frames, or past the frame's edge, read a hold on every frame after the first; now such a frame shows no figure there and two such frames are no picture shown again. The report gains each figure's steps (its frames read against the frame before; on a loop read across its wrap, the wrap is one step) and inspected (the number of figures read on one step or more); the other fields are unchanged, exit 0, 1 and 2 keep their meanings, and a reading that reads no frame against the frame before is now one of the errors (exit 2). A reading that reads no frame against the frame before (no frame: an empty folder, --frames that starts past the last frame of a folder, a video, a strip or a log, a log of no line; no figure on any frame: clear frames, frames whose every pixel has alpha 8 or under, a key that leaves nothing, a box that holds nothing, a log whose --log-figure keys name no cell on any line; no figure shown on two frames in a row, for example one frame) is an error: it exits 2 with or without --gate, its report is still printed on stdout and written to --report with inspected 0, and one line on stderr says why. Before, most of these passed --gate (exit 0, an empty table, nothing on stderr); an empty folder and a log of no line exited 2 but printed no report; a strip given --frames past its last cell was refused with the wrong reason (as opaque frames); a box over nothing failed the gate on frames that showed nothing. In Python, inspect(...) and read(...) in sprite_gen.qa.holds raise NothingRead, a ValueError whose report attribute is the report, with inspected 0. The Grok clip reading of video, video-extend, video-edit and video-resume read a clip whose every frame is the canvas alone (no pixel left once the canvas colour is taken out) as one run of all its frames, recorded as a hold the motion means, and said nothing; now such a clip is not read, its report's held is read false with frames, background and a why, the warning line says its frames were not read for a picture shown again, and the clip is still published.
  • New: sprite-gen video-chain plays pieces of keyed clips as one move at the caller's time (--piece, --bridge, --out-fps, --places, --holds, --refuse, --fill-held, --gate). It joins keyed clips of matching canvas, key and rate, making frames between join points or retimed places with RIFE. A run of frames showing one picture, unless a list of holds meant names it, is made again between the poses beside it, and any unlisted frame showing the picture before it fails the run. Pieces that disagree in size, key, or rate, opaque frames, overlapping paths, or places off the chain refuse the run before anything is touched. By default every fault the judge finds refuses a frame; --refuse names those that do; which to leave out is the caller's to decide by looking at the material; held, the bar a frame made off its time must stand under and the final reading refuse whatever it says; a fault left out stays on the frame's record and a warning: line; the judge and its bars do not change. held_fill gains FAULTS and a refuse argument on at_times and fill, loop.fill_cut gains refuse, frames gains KEYS_READ and key_kind, a held_fill try gains judged and, with refuse, kept_faults, and the video-frames report gains key_read.
  • Python callers. held_fill: new wrap_same, KEEPS, intended, again, strip_holds; premultiplied (was _premultiplied); fill(..., wrap=, drawn_on=); reading(..., wrap=). loop: new cut_again; fill_cut, fill_lines, out_fps_lines (were _fill_held, _fill_lines, _out_fps_lines). align: new filled, unfilled_source, CUT_AGAIN_DIR; align_set(..., fill_held=, out_fps=). batch: new held_summary, loop_summary, cut_again_unaligned; align_gaits(..., fill_held=, out_fps=). spec.assets: new frame_seconds, SNAP_MS, FrameSequence.index_at. scene.render.Renderer(..., fill_held=, interpolate=, between_cache=). New module sprite_gen.qa.holds. stop.make_stop(..., fill_held=, interpolate=).
  • Tests: tests/video/test_no_repeat_default.py, tests/scene/test_scene_fill_held.py, tests/qa/test_inspect_holds.py, tests/gen/test_video_held.py, tests/video/test_face_bake_held.py (new); tests/video/test_held_fill.py, tests/video/test_stop.py, tests/video/test_face_layer.py, tests/video/test_root_motion.py, tests/video/test_loop_prop_seam_held.py, tests/video/test_follow_read_on.py, tests/video/test_motion_anchor.py (changed). test_no_repeat_default.py covers a loop's last cells, a cut one cell too long, the read again after the repairs, the strip's holds, cut_args and cut_again, the lifetime warning, the alignment's end fill, its cut again with the fill off and its refusal, --out-fps once aligned, a set with no held cell aligned as before, and video-set cutting aligned items as filmed and cutting again the ones no alignment took. test_held_fill.py's wrap tests: a run at a loop's wrap is made, a last cell showing the first again is made and the first kept, a hold across the wrap longer than three cells kept whole, a cut played once reads no wrap. test_stop.py: the stop fills by default; with no held cell and with --fill-held off it is the pictures pinned before. test_face_layer.py, test_root_motion.py and test_loop_prop_seam_held.py: the strip metadata grows fps and holds, the old pins otherwise unchanged. test_motion_anchor.py: its anchored cells 5 to 7 differ by one pixel, so the test cuts with --fill-held off to check the anchor's own cells. No-repeat outputs keep their pictures: the six cuts pinned before, a stop with no held cell, a set aligned with no held cell (pinned on commit 4f5ccd5) and scenes at 25, 20, 30, 12 and 48 fps. Two alignment tests now reach the RIFE binary through the alignment's fill at the end: test_cycle_align_aligns_a_two_step_loop_as_the_one_cycle_it_is in test_loop_prop_seam_held.py and test_the_first_alignment_records_the_cut_it_was_made_from_and_later_ones_keep_it in test_follow_read_on.py. Like the other tests the default fill sends to the binary often (the rule in docs/release.md), each now has a timeout of its own, @pytest.mark.timeout(900), and pins the binary's calls by model with as_pinned (exactly for rife-v4.6, between a floor and a top for rife-v4.25), and the second test is now marked slow, so the fast bundle (a plain pytest) leaves it out.

Install or update:

pip install --upgrade "sprite-gen @ git+https://github.com/aldegad/sprite-gen.git@v4.0.0"

The wheel, the source archive and SHA256SUMS are attached. Full notes in CHANGELOG.md.