pixelkiln 0.68.0 → 0.69.0
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.
- package/PROVIDERS.md +30 -15
- package/README.md +18 -18
- package/dist/cli.js +250 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +31 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -58
- package/dist/index.d.ts +73 -58
- package/dist/index.js +31 -3
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +1 -1
- package/docs/CHARACTERS.md +4 -1
- package/docs/CLI.md +60 -25
- package/docs/GENERATORS.md +10 -1
- package/docs/MANIFEST.md +4 -2
- package/docs/PIXELLAB.md +14 -3
- package/docs/README.md +2 -2
- package/docs/REVISIONS.md +10 -4
- package/package.json +1 -1
- package/skills/pixelkiln/SKILL.md +10 -4
- package/skills/pixelkiln/references/pixellab-roadmap.md +52 -80
- package/skills/pixelkiln/references/pixellab.md +23 -11
- package/skills/pixelkiln/references/revisions.md +22 -4
package/PROVIDERS.md
CHANGED
|
@@ -57,19 +57,20 @@ the providers offer directly.
|
|
|
57
57
|
| Decision | PixelLab | Retro Diffusion |
|
|
58
58
|
|---|---|---|
|
|
59
59
|
| Best fit today | Existing PixelKiln projects, account reconciliation, and live-tested generation | Native pixel-art styles, USD budgets, animation, and alternate tileset workflows |
|
|
60
|
-
| PixelKiln generators | `map`, `pixflux`, `1dir`, `tiles`, `character` | `map` and `pixflux` stills, `tiles`, `animation` |
|
|
61
|
-
| Output | PNG stills, candidates,
|
|
60
|
+
| PixelKiln generators | `map`, `pixflux`, `1dir`, `imagePro`, `imageProFlash`, `tiles`, `terrain`, `isometricTile`, `character`, `objectPro`, `uiAsset`, `uiElement` | `map` and `pixflux` stills, `tiles`, `animation` |
|
|
61
|
+
| Output | PNG stills, candidates, structural tile members, and ordered frame sets | PNG stills/tiles/spritesheets or animated GIF |
|
|
62
62
|
| Candidate review | Yes; count varies by generator and size | Yes for 1–16 still candidates; animations and tilesets currently use one result |
|
|
63
|
-
| References | `1dir` and tile style modes | Up to nine for supported RD Pro/user still styles; constrained inputs for animation and tiles |
|
|
63
|
+
| References | `1dir` and tile style modes; a character or `objectPro` reference sprite; a `pro` character concept image; one `imageProFlash` style image; one `uiElement` concept image | Up to nine for supported RD Pro/user still styles; constrained inputs for animation and tiles |
|
|
64
64
|
| Cost model | Subscription generations | Prepaid USD balance |
|
|
65
65
|
| Cost safety | Offline estimate plus hard generation budget | Offline estimate, hard USD budget, then a free authoritative quote before submission |
|
|
66
66
|
| Account lifecycle | Balance, list, adopt, salvage, tag, and confirmed purge | Balance only in the current adapter |
|
|
67
67
|
| Live confidence | Full paid generation workflows exercised | RD Fast and RD Plus single-candidate stills exercised end to end; multi-candidate, tileset, GIF, and spritesheet runs pending |
|
|
68
68
|
|
|
69
69
|
Choose PixelLab when mature account-object recovery and reconciliation matter,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
when the measured one-generation `map`/`pixflux` routes fit the work, or when an
|
|
71
|
+
animation belongs to a character, an object, or an existing image. Choose
|
|
72
|
+
Retro Diffusion when a GIF or spritesheet from one prompt fits, an RD style is
|
|
73
|
+
the desired look, or a USD quote is easier to budget. For a production batch,
|
|
73
74
|
run one representative asset through the selected provider before expanding the
|
|
74
75
|
scope.
|
|
75
76
|
|
|
@@ -96,9 +97,12 @@ the same graph and still differ because their checkpoint bytes, custom nodes,
|
|
|
96
97
|
or sampler settings differ. Commit the workflow and record the model stack used
|
|
97
98
|
to test it.
|
|
98
99
|
|
|
99
|
-
The revision manifest and dependency gate are provider-neutral
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
The revision manifest and dependency gate are provider-neutral. ComfyUI and
|
|
101
|
+
PixelLab opt in; Retro Diffusion and Scenario reject revision assets offline.
|
|
102
|
+
PixelLab covers `image-to-image`, `inpaint`, `reduce-colors`,
|
|
103
|
+
`correct-pixelart`, `animate`, `animate-pixminimax`, `animate-skeleton`,
|
|
104
|
+
`interpolate`, and `edit-animation`, but not `outpaint`. ComfyUI covers the
|
|
105
|
+
single-image modes through its workflow bindings. See
|
|
102
106
|
[Controlled asset revisions](./docs/REVISIONS.md) before using the bundled
|
|
103
107
|
square image-to-image graph.
|
|
104
108
|
|
|
@@ -131,7 +135,7 @@ background. That distinction matters more than raw canvas size.
|
|
|
131
135
|
| Asset type | PixelLab | Retro Diffusion | ComfyUI | Scenario |
|
|
132
136
|
|---|---|---|---|---|
|
|
133
137
|
| Isolated house, building, mountain, or landmark | Start with `map`: arbitrary dimensions up to 400×400 and a measured one-generation cost. Live benchmark outputs had opaque backgrounds, so plan for cleanup. Use `1dir` only when references or candidate variety justify 20–40 generations and a square canvas. | Start with `rd_plus__topdown_asset`, `rd_plus__isometric_asset`, or `rd_tile__scene_object`, depending on perspective. `rd_tile__scene_object` is intended for 64–384px objects placed on tile maps. | Choose a checkpoint or LoRA trained for the intended perspective, then keep background removal or segmentation in the workflow. For the tested Pixel Art XL stack, target 48–128px native components even though the adapter accepts larger working canvases. | The live BFL smoke produced a readable 512px keep, but it was opaque and used 19,619 colors. Use that profile for concepts or refinement input, not a finished limited-palette asset. A project-specific model may improve consistency but needs its own smoke. |
|
|
134
|
-
| Full scenic background | Use `pixflux` with `noBackground: false` when an exact palette matters, or `map` for a simple scene
|
|
138
|
+
| Full scenic background | Use `pixflux` with `noBackground: false` when an exact palette matters, or `map` for a simple scene; both top out at 400×400. `imagePro` reaches 792 wide or 688 tall for a flat 40 generations. | `rd_plus__environment` targets one-point-perspective scenes; `rd_plus__topdown_map` targets 3/4 top-down maps. These styles support up to 384×384. | Use composition controls only to establish the scene. Recover and review native components, then compose them at 1× with one grid and palette. A large model canvas is not a large native pixel-art canvas. | The BFL profile accepts canvases up to 2048px, but no Scenario scenic brief has passed the shared benchmark. Treat that as model-canvas capacity, not native pixel resolution. Start with one 512px concept before raising size or steps. |
|
|
135
139
|
| Style consistency across a set | `1dir` accepts a style reference and returns size-dependent candidates, but it is more expensive and capped at the square-object range. | RD Pro accepts up to nine references and has stronger prompt following, but its common styles top out at 256×256 and cost $0.18 per image. Environment-specific RD Plus styles trade references for a larger 384px canvas. | LoRAs, reference adapters, ControlNet, and shared latent settings can live in the committed workflow. Reproducibility also depends on external model and custom-node versions. | Scenario's project models and LoRAs are the main reason to use it for a set. PixelKiln can pin the model ID and parameters, but its first live run covers only the public BFL profile. |
|
|
136
140
|
| Very large final scene | Generate reusable objects, terrain, and background layers separately; assemble them deterministically and integer-upscale the result. | Use the same layered approach. The API has a 512px overall ceiling, but the useful environment and scene-object styles currently cap at 384px. | The graph can tile, upscale, or composite beyond hosted-provider limits, but memory and seam quality become workflow concerns. Prefer reusable layers unless the scene truly needs one render. | Scenario can request a larger raster from a compatible model, but the same rule applies: generate reusable layers at their useful native detail, compose at 1×, and integer-upscale only the final scene. |
|
|
137
141
|
|
|
@@ -194,18 +198,29 @@ not share one runtime.
|
|
|
194
198
|
The services use different billing units, so PixelKiln never adds their costs
|
|
195
199
|
together.
|
|
196
200
|
|
|
197
|
-
PixelLab figures below
|
|
198
|
-
|
|
201
|
+
PixelLab figures below come from the endpoints PixelKiln currently uses. Rows
|
|
202
|
+
marked unmeasured or quoted are `plan` estimates, not bills. None is a
|
|
203
|
+
conversion to dollars:
|
|
199
204
|
|
|
200
|
-
| PixelLab route |
|
|
205
|
+
| PixelLab route | Cost |
|
|
201
206
|
|---|---:|
|
|
202
207
|
| `map` | 1 generation |
|
|
203
208
|
| `pixflux` | 1 generation |
|
|
204
209
|
| `1dir` | 20–40 generations |
|
|
210
|
+
| `imagePro` | 40 generations |
|
|
211
|
+
| `imageProFlash` | 5–9 generations (provisional quote) |
|
|
205
212
|
| `tiles` | 20–40 generations |
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
213
|
+
| `terrain` | 20–40 generations (unmeasured) |
|
|
214
|
+
| `isometricTile` | 1 generation (measured once) |
|
|
215
|
+
| `character` base | 1 (standard), 2–9 (v3), 20–40 (pro), 6–17 (pro-flash; rotations only from a reference) |
|
|
216
|
+
| `character` state or portrait | 20–40 generations |
|
|
208
217
|
| `character` animation | 1 (template), 1 at 64px from text, 20–40 (pro) |
|
|
218
|
+
| `character` outfit | 20 generations (one 2-frame, 92×92 job) |
|
|
219
|
+
| `objectPro` base | 6 at 64px (unmeasured) |
|
|
220
|
+
| `uiAsset` | 20 generations (measured once, at 256×192) |
|
|
221
|
+
| `uiElement` | 20–40 generations (unmeasured) |
|
|
222
|
+
| `image-to-image` / `inpaint` revision | 20–40 generations; 5–9 with `engine: "pro-flash"` (provisional quote) |
|
|
223
|
+
| `reduce-colors` / `correct-pixelart` revision | 0.1 generations (measured at 32×32) |
|
|
209
224
|
|
|
210
225
|
Retro Diffusion publishes USD formulas and fixed prices. Examples relevant to
|
|
211
226
|
PixelKiln include RD Fast from about $0.015 per image, RD Plus from about $0.025,
|
package/README.md
CHANGED
|
@@ -53,8 +53,9 @@ provenance and no long-lived npm token.
|
|
|
53
53
|
| Plan and budget | Offline manifest/lock/disk diff, provider-grouped estimates, keyed mixed-provider budget ceilings, JSON/CI gate. |
|
|
54
54
|
| Generate and review | Resumable submit/poll/pick/fetch pipeline, exact next-step hints, candidate or atomic frame-set review, and a provenance gallery that can edit intent, generate under a budget, and compare records. |
|
|
55
55
|
| Hand edits | Touch-ups in your own editor or a pinned in-browser Pixelorama, kept beside the generated art with the generation still the record; frame and tile sets member by member. |
|
|
56
|
-
| Controlled inputs | Hashed image-to-image
|
|
57
|
-
| Characters | A base in 4 or 8 directions from a prompt or your own sprite (four PixelLab engines), poses and
|
|
56
|
+
| Controlled inputs | Hashed revision lineage (image-to-image and inpaint; on PixelLab also palette reduction, pixel cleanup, animation, skeleton posing, interpolation, and animation edits), fail-closed parent approval, source-versus-candidate review, and content-addressed per-asset ComfyUI bindings. |
|
|
57
|
+
| Characters | A base in 4 or 8 directions from a prompt or your own sprite (four PixelLab engines), poses, loops, portraits, and outfit transfers as dependent assets generated in waves, free mirrored directions (one `directions` list declares a loop and mirrors the rest), account adoption and salvage, and SpriteFrames export. |
|
|
58
|
+
| PixelLab utilities | `unzoom` returns upscaled reference art to its native grid, `font` generates a `.ttf` and glyph atlas, `estimate-skeleton` starts a keypoints file for skeleton animation, and `skeleton-preview` draws its poses over the sprite for free. |
|
|
58
59
|
| Existing-art onboarding | Manifest scaffolding, exact-hash account adoption, and prompt recovery. |
|
|
59
60
|
| Recovery | Safe stale-output replacement, validated caches, durable references, resumable paid jobs, and per-asset generation history with free restores. |
|
|
60
61
|
| Shared-account safety | Cross-project claim files or a registered workspace catalog, sibling-style exclusion, reviewed salvage, keep/discard tags, separate confirmed purge. |
|
|
@@ -258,18 +259,17 @@ Measured PixelLab economics vary by 40×:
|
|
|
258
259
|
| Independent or connectable ground tiles | `tiles` | 20–40 generations |
|
|
259
260
|
| A character in 4 or 8 directions, its poses, its loops | `character` | 1 per base, 20–40 per pose, 1 per template loop per direction; a `mirror` of a loop is free |
|
|
260
261
|
|
|
261
|
-
Start with the required capability, not the most expensive endpoint. Forty
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
[measured endpoint reference](./docs/ENDPOINTS.md).
|
|
262
|
+
Start with the required capability, not the most expensive endpoint. Forty `map`
|
|
263
|
+
re-rolls cost the same as one 64×64 `1dir` call; conversely, `map` cannot replace a
|
|
264
|
+
hard palette or reference-image constraint. PixelLab has seven more generators
|
|
265
|
+
(`imagePro`, `imageProFlash`, `terrain`, `isometricTile`, `objectPro`, `uiAsset`,
|
|
266
|
+
`uiElement`); see [Generator selection](./docs/GENERATORS.md) and the [measured endpoint reference](./docs/ENDPOINTS.md).
|
|
266
267
|
|
|
267
|
-
Generator names describe PixelKiln workflows; their exact capabilities and
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
Compare the adapters in the [provider comparison](./PROVIDERS.md).
|
|
268
|
+
Generator names describe PixelKiln workflows; their exact capabilities and prices
|
|
269
|
+
depend on the selected provider. Retro Diffusion also supports the provider-specific
|
|
270
|
+
`animation` generator. ComfyUI supports `map` plus atomic still-image `frames` through
|
|
271
|
+
an operator-supplied workflow. Scenario supports `map` with a required offline CU
|
|
272
|
+
ceiling and a live quote before each paid call. Compare the adapters in the [provider comparison](./PROVIDERS.md).
|
|
273
273
|
|
|
274
274
|
## Derived artifacts
|
|
275
275
|
|
|
@@ -368,12 +368,13 @@ verification. See [Library API](./docs/LIBRARY.md).
|
|
|
368
368
|
| [Set up ComfyUI](./docs/COMFYUI.md) | Self-hosted stills, revisions, ordered frame sets, per-asset inputs, and quality limits. |
|
|
369
369
|
| [Set up Scenario](./docs/SCENARIO.md) | Experimental hosted models, two-part credentials, CU preflight, review, and durable downloads. |
|
|
370
370
|
| [Versioned recipes](./docs/RECIPES.md) | Pinned workflow packs, model hashes, manifest templates, and quality contracts. |
|
|
371
|
-
| [Controlled revisions](./docs/REVISIONS.md) | Image-to-image
|
|
371
|
+
| [Controlled revisions](./docs/REVISIONS.md) | Image-to-image, inpaint, palette cleanup, animation, and interpolation from a parent; masks, fail-closed readiness, provenance, and ComfyUI bindings. |
|
|
372
372
|
| [CLI reference](./docs/CLI.md) | Every command, flag, JSON mode, and exit contract. |
|
|
373
373
|
| [Manifest reference](./docs/MANIFEST.md) | Style/asset fields, quality profiles, and generator constraints. |
|
|
374
374
|
| [Mixed-provider projects](./docs/MIXED_PROVIDERS.md) | Per-style routing, provider-keyed budgets, recovery, and account commands. |
|
|
375
375
|
| [Agent workflows](./docs/AGENTS.md) | Official skill install, operating model, and provider-aware safety. |
|
|
376
376
|
| [Generators](./docs/GENERATORS.md) | Capability choice, measured costs, palettes, style references, and tiles. |
|
|
377
|
+
| [Characters](./docs/CHARACTERS.md) | Bases, states, loops, mirrors, portraits, outfits, the `directions` shorthand, and per-engine costs. |
|
|
377
378
|
| [Environment provider benchmark](./docs/PROVIDER_BENCHMARK.md) | Thirty provider outputs plus native-grid and final-palette results comparing large scenes, transparency, palette size, and file readiness. |
|
|
378
379
|
| [Derived artifacts](./docs/ARTIFACTS.md) | Refine, pack, mount, export, provenance, ownership, transactions, and recovery. |
|
|
379
380
|
| [Recovery](./docs/RECOVERY.md) | Restore, caches, adopt, salvage, claims, and purge safety. |
|
|
@@ -384,10 +385,9 @@ verification. See [Library API](./docs/LIBRARY.md).
|
|
|
384
385
|
| [Endpoint research](./docs/ENDPOINTS.md) | Measured PixelLab API behavior and recipes. |
|
|
385
386
|
| [Provider comparison](./PROVIDERS.md) | Provider selection, costs, supported workflows, confidence, and limitations. |
|
|
386
387
|
|
|
387
|
-
The [public documentation site](https://pixelkiln.griffen.codes/docs) is built from
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
[Security](./SECURITY.md), [provider comparison](./PROVIDERS.md).
|
|
388
|
+
The [public documentation site](https://pixelkiln.griffen.codes/docs) is built from these Markdown
|
|
389
|
+
files by [`website/`](./website/README.md), so the site and the package share one source.
|
|
390
|
+
Policies: [Contributing](./CONTRIBUTING.md), [Security](./SECURITY.md).
|
|
391
391
|
|
|
392
392
|
## Scope
|
|
393
393
|
|
package/dist/cli.js
CHANGED
|
@@ -67,7 +67,7 @@ function exitCodeFor(err) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// src/cli/main.ts
|
|
70
|
-
import { existsSync as
|
|
70
|
+
import { existsSync as existsSync34 } from "fs";
|
|
71
71
|
import { readFile as readFile31 } from "fs/promises";
|
|
72
72
|
|
|
73
73
|
// src/cli/args.ts
|
|
@@ -107,7 +107,8 @@ var VALUE_FLAGS = [
|
|
|
107
107
|
"--quantize",
|
|
108
108
|
"--description",
|
|
109
109
|
"--weight",
|
|
110
|
-
"--glyph-px"
|
|
110
|
+
"--glyph-px",
|
|
111
|
+
"--frames"
|
|
111
112
|
];
|
|
112
113
|
var BOOL_FLAGS = [
|
|
113
114
|
"--force",
|
|
@@ -158,6 +159,7 @@ var COMMANDS = [
|
|
|
158
159
|
"recipe",
|
|
159
160
|
"workspace",
|
|
160
161
|
"estimate-skeleton",
|
|
162
|
+
"skeleton-preview",
|
|
161
163
|
"unzoom",
|
|
162
164
|
"font",
|
|
163
165
|
"help",
|
|
@@ -256,6 +258,12 @@ function parseArgs(argv) {
|
|
|
256
258
|
target = rest[0]?.startsWith("-") ? void 0 : rest[0];
|
|
257
259
|
if (target === void 0) throw new UsageError("estimate-skeleton needs an image path");
|
|
258
260
|
rest = rest.slice(1);
|
|
261
|
+
} else if (command === "skeleton-preview") {
|
|
262
|
+
target = rest[0]?.startsWith("-") ? void 0 : rest[0];
|
|
263
|
+
if (target === void 0) {
|
|
264
|
+
throw new UsageError("skeleton-preview needs a keypoints file or an animate-skeleton asset id");
|
|
265
|
+
}
|
|
266
|
+
rest = rest.slice(1);
|
|
259
267
|
} else if (command === "refine") {
|
|
260
268
|
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
|
|
261
269
|
if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
|
|
@@ -421,7 +429,8 @@ function parseArgs(argv) {
|
|
|
421
429
|
quantize: numberOption("--quantize", { min: -1, max: 256, integer: true }),
|
|
422
430
|
description: get("--description"),
|
|
423
431
|
weight: get("--weight"),
|
|
424
|
-
glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true })
|
|
432
|
+
glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true }),
|
|
433
|
+
frames: numberOption("--frames", { min: 3, max: 15, integer: true })
|
|
425
434
|
};
|
|
426
435
|
}
|
|
427
436
|
|
|
@@ -461,11 +470,16 @@ Commands
|
|
|
461
470
|
first. Local art is untouched.
|
|
462
471
|
tag Push manifest tags to the objects upstream (free).
|
|
463
472
|
balance Show the provider's remaining balance.
|
|
464
|
-
estimate-skeleton <image> [--out <file>]
|
|
465
|
-
PixelLab only: derive an 18-joint skeleton from
|
|
466
|
-
and un-budgeted (like balance, not a generation).
|
|
467
|
-
|
|
468
|
-
|
|
473
|
+
estimate-skeleton <image> [--out <file>] [--frames <n>] [--force]
|
|
474
|
+
PixelLab only: derive an 18-joint skeleton from a square 16-256px
|
|
475
|
+
image, direct and un-budgeted (like balance, not a generation).
|
|
476
|
+
Prints, or writes with --out, a ready keypoints file for an
|
|
477
|
+
animate-skeleton revision: the estimated pose plus --frames copies
|
|
478
|
+
(default 4) to edit. --out refuses to replace a file without --force.
|
|
479
|
+
skeleton-preview <keypoints.json | asset> [--from <image>] [--out <file>]
|
|
480
|
+
Draw every pose of a keypoints file over its source image as one
|
|
481
|
+
PNG, locally and free. Given an animate-skeleton asset id, reads
|
|
482
|
+
its keypointsFile and source from the manifest.
|
|
469
483
|
status Summarise the lockfile.
|
|
470
484
|
edit Hand-edit an asset in your own editor: copies the generated PNG to
|
|
471
485
|
<outDir>/edits/, declares it as the asset's source, and opens it.
|
|
@@ -5756,14 +5770,61 @@ import { z as z3 } from "zod";
|
|
|
5756
5770
|
|
|
5757
5771
|
// src/skeleton.ts
|
|
5758
5772
|
import { z as z2 } from "zod";
|
|
5773
|
+
var SKELETON_LABELS = [
|
|
5774
|
+
"NOSE",
|
|
5775
|
+
"NECK",
|
|
5776
|
+
"RIGHT SHOULDER",
|
|
5777
|
+
"RIGHT ELBOW",
|
|
5778
|
+
"RIGHT ARM",
|
|
5779
|
+
"LEFT SHOULDER",
|
|
5780
|
+
"LEFT ELBOW",
|
|
5781
|
+
"LEFT ARM",
|
|
5782
|
+
"RIGHT HIP",
|
|
5783
|
+
"RIGHT KNEE",
|
|
5784
|
+
"RIGHT LEG",
|
|
5785
|
+
"LEFT HIP",
|
|
5786
|
+
"LEFT KNEE",
|
|
5787
|
+
"LEFT LEG",
|
|
5788
|
+
"RIGHT EYE",
|
|
5789
|
+
"LEFT EYE",
|
|
5790
|
+
"RIGHT EAR",
|
|
5791
|
+
"LEFT EAR"
|
|
5792
|
+
];
|
|
5793
|
+
var SKELETON_BONES = [
|
|
5794
|
+
["NOSE", "NECK"],
|
|
5795
|
+
["NECK", "RIGHT SHOULDER"],
|
|
5796
|
+
["RIGHT SHOULDER", "RIGHT ELBOW"],
|
|
5797
|
+
["RIGHT ELBOW", "RIGHT ARM"],
|
|
5798
|
+
["NECK", "LEFT SHOULDER"],
|
|
5799
|
+
["LEFT SHOULDER", "LEFT ELBOW"],
|
|
5800
|
+
["LEFT ELBOW", "LEFT ARM"],
|
|
5801
|
+
["NECK", "RIGHT HIP"],
|
|
5802
|
+
["RIGHT HIP", "RIGHT KNEE"],
|
|
5803
|
+
["RIGHT KNEE", "RIGHT LEG"],
|
|
5804
|
+
["NECK", "LEFT HIP"],
|
|
5805
|
+
["LEFT HIP", "LEFT KNEE"],
|
|
5806
|
+
["LEFT KNEE", "LEFT LEG"],
|
|
5807
|
+
["NOSE", "RIGHT EYE"],
|
|
5808
|
+
["RIGHT EYE", "RIGHT EAR"],
|
|
5809
|
+
["NOSE", "LEFT EYE"],
|
|
5810
|
+
["LEFT EYE", "LEFT EAR"]
|
|
5811
|
+
];
|
|
5759
5812
|
var SkeletonKeypointSchema = z2.object({
|
|
5760
|
-
label: z2.
|
|
5813
|
+
label: z2.enum(SKELETON_LABELS),
|
|
5761
5814
|
x: z2.number().min(0).max(1),
|
|
5762
5815
|
y: z2.number().min(0).max(1),
|
|
5763
5816
|
z_index: z2.number(),
|
|
5764
5817
|
depth: z2.number().optional()
|
|
5765
5818
|
}).strict();
|
|
5766
|
-
var SkeletonFrameSchema = z2.array(SkeletonKeypointSchema).length(18)
|
|
5819
|
+
var SkeletonFrameSchema = z2.array(SkeletonKeypointSchema).length(18).superRefine((frame, context) => {
|
|
5820
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5821
|
+
for (const [index, joint] of frame.entries()) {
|
|
5822
|
+
if (seen.has(joint.label)) {
|
|
5823
|
+
context.addIssue({ code: z2.ZodIssueCode.custom, message: `"${joint.label}" appears twice`, path: [index, "label"] });
|
|
5824
|
+
}
|
|
5825
|
+
seen.add(joint.label);
|
|
5826
|
+
}
|
|
5827
|
+
});
|
|
5767
5828
|
var SkeletonSetSchema = z2.object({
|
|
5768
5829
|
firstFrameKeypoints: SkeletonFrameSchema,
|
|
5769
5830
|
frames: z2.array(SkeletonFrameSchema).min(3).max(15)
|
|
@@ -5776,6 +5837,11 @@ function parseSkeletonSet(json, file) {
|
|
|
5776
5837
|
}
|
|
5777
5838
|
return result.data;
|
|
5778
5839
|
}
|
|
5840
|
+
var DEFAULT_SCAFFOLD_FRAMES = 4;
|
|
5841
|
+
function scaffoldSkeletonSet(keypoints, frames = DEFAULT_SCAFFOLD_FRAMES) {
|
|
5842
|
+
const copy = () => keypoints.map((joint) => ({ ...joint }));
|
|
5843
|
+
return { firstFrameKeypoints: copy(), frames: Array.from({ length: frames }, copy) };
|
|
5844
|
+
}
|
|
5779
5845
|
|
|
5780
5846
|
// src/client.ts
|
|
5781
5847
|
var BASE = process.env.PIXELLAB_API_BASE ?? "https://api.pixellab.ai/v2";
|
|
@@ -7128,7 +7194,7 @@ var PixelLabClient = class {
|
|
|
7128
7194
|
* account (see `EstimateSkeletonResponseSchema`'s own caveat).
|
|
7129
7195
|
*/
|
|
7130
7196
|
async estimateSkeleton(args) {
|
|
7131
|
-
const body = { image: args.image };
|
|
7197
|
+
const body = { image: { type: "base64", base64: args.image.base64, format: args.image.format } };
|
|
7132
7198
|
const res = validateResponse(
|
|
7133
7199
|
EstimateSkeletonResponseSchema,
|
|
7134
7200
|
await this.request("/estimate-skeleton", { method: "POST", body: JSON.stringify(body) }),
|
|
@@ -20537,27 +20603,192 @@ async function runRefine(args) {
|
|
|
20537
20603
|
|
|
20538
20604
|
// src/cli/commands/skeleton.ts
|
|
20539
20605
|
import path46 from "path";
|
|
20606
|
+
import { existsSync as existsSync32 } from "fs";
|
|
20540
20607
|
import { readFile as readFile30, writeFile as writeFile15 } from "fs/promises";
|
|
20608
|
+
|
|
20609
|
+
// src/skeleton-preview.ts
|
|
20610
|
+
var RIGHT = [255, 140, 40, 255];
|
|
20611
|
+
var LEFT = [60, 150, 255, 255];
|
|
20612
|
+
var CENTER = [240, 240, 240, 255];
|
|
20613
|
+
var GROUND = [28, 28, 34, 255];
|
|
20614
|
+
var OUTLINE = [0, 0, 0, 255];
|
|
20615
|
+
var GUTTER = 4;
|
|
20616
|
+
var MIN_CELL = 192;
|
|
20617
|
+
function renderSkeletonSheet(set, options = {}) {
|
|
20618
|
+
const poses = [set.firstFrameKeypoints, ...set.frames];
|
|
20619
|
+
const source = options.source;
|
|
20620
|
+
const scale = source ? Math.max(1, Math.ceil(MIN_CELL / Math.max(source.width, source.height))) : 1;
|
|
20621
|
+
const cellW = source ? source.width * scale : options.cellSize ?? MIN_CELL;
|
|
20622
|
+
const cellH = source ? source.height * scale : options.cellSize ?? MIN_CELL;
|
|
20623
|
+
const columns = Math.min(options.columns ?? 8, poses.length);
|
|
20624
|
+
const rows = Math.ceil(poses.length / columns);
|
|
20625
|
+
const width = columns * cellW + (columns + 1) * GUTTER;
|
|
20626
|
+
const height = rows * cellH + (rows + 1) * GUTTER;
|
|
20627
|
+
const canvas = new Canvas(width, height);
|
|
20628
|
+
canvas.fill(0, 0, width, height, [12, 12, 16, 255]);
|
|
20629
|
+
poses.forEach((pose, index) => {
|
|
20630
|
+
const left = GUTTER + index % columns * (cellW + GUTTER);
|
|
20631
|
+
const top = GUTTER + Math.floor(index / columns) * (cellH + GUTTER);
|
|
20632
|
+
canvas.fill(left, top, cellW, cellH, GROUND);
|
|
20633
|
+
if (source) canvas.drawImage(source, left, top, scale, index === 0 ? 1 : 0.45);
|
|
20634
|
+
drawPose(canvas, pose, left, top, cellW, cellH, Math.max(1, Math.round(Math.min(cellW, cellH) / 128)));
|
|
20635
|
+
});
|
|
20636
|
+
return encodeRgbaPng(width, height, canvas.data);
|
|
20637
|
+
}
|
|
20638
|
+
function drawPose(canvas, pose, left, top, w, h, stroke) {
|
|
20639
|
+
const at = new Map(pose.map((joint) => [joint.label, joint]));
|
|
20640
|
+
const point = (joint) => [left + joint.x * (w - 1), top + joint.y * (h - 1)];
|
|
20641
|
+
for (const outline of [true, false]) {
|
|
20642
|
+
for (const [from, to] of SKELETON_BONES) {
|
|
20643
|
+
const a = at.get(from);
|
|
20644
|
+
const b = at.get(to);
|
|
20645
|
+
if (!a || !b) continue;
|
|
20646
|
+
const [x0, y0] = point(a);
|
|
20647
|
+
const [x1, y1] = point(b);
|
|
20648
|
+
canvas.line(x0, y0, x1, y1, outline ? OUTLINE : colorOf(to), outline ? stroke + 2 : stroke);
|
|
20649
|
+
}
|
|
20650
|
+
for (const joint of pose) {
|
|
20651
|
+
const [x, y] = point(joint);
|
|
20652
|
+
canvas.dot(x, y, outline ? stroke + 3 : stroke + 1, outline ? OUTLINE : colorOf(joint.label));
|
|
20653
|
+
}
|
|
20654
|
+
}
|
|
20655
|
+
}
|
|
20656
|
+
function colorOf(label) {
|
|
20657
|
+
if (label.startsWith("RIGHT")) return RIGHT;
|
|
20658
|
+
if (label.startsWith("LEFT")) return LEFT;
|
|
20659
|
+
return CENTER;
|
|
20660
|
+
}
|
|
20661
|
+
var Canvas = class {
|
|
20662
|
+
constructor(width, height) {
|
|
20663
|
+
this.width = width;
|
|
20664
|
+
this.height = height;
|
|
20665
|
+
this.data = Buffer.alloc(width * height * 4);
|
|
20666
|
+
}
|
|
20667
|
+
width;
|
|
20668
|
+
height;
|
|
20669
|
+
data;
|
|
20670
|
+
set(x, y, [r, g, b, a]) {
|
|
20671
|
+
const px = Math.round(x);
|
|
20672
|
+
const py = Math.round(y);
|
|
20673
|
+
if (px < 0 || py < 0 || px >= this.width || py >= this.height) return;
|
|
20674
|
+
const i = (py * this.width + px) * 4;
|
|
20675
|
+
this.data[i] = r;
|
|
20676
|
+
this.data[i + 1] = g;
|
|
20677
|
+
this.data[i + 2] = b;
|
|
20678
|
+
this.data[i + 3] = a;
|
|
20679
|
+
}
|
|
20680
|
+
fill(x, y, w, h, color) {
|
|
20681
|
+
for (let dy = 0; dy < h; dy++) for (let dx = 0; dx < w; dx++) this.set(x + dx, y + dy, color);
|
|
20682
|
+
}
|
|
20683
|
+
/** Nearest-neighbour upscale, composited over what is there, at `strength`. */
|
|
20684
|
+
drawImage(image, left, top, scale, strength) {
|
|
20685
|
+
for (let y = 0; y < image.height; y++) {
|
|
20686
|
+
for (let x = 0; x < image.width; x++) {
|
|
20687
|
+
const i = (y * image.width + x) * 4;
|
|
20688
|
+
const alpha = image.pixels[i + 3] / 255 * strength;
|
|
20689
|
+
if (alpha === 0) continue;
|
|
20690
|
+
const src = [image.pixels[i], image.pixels[i + 1], image.pixels[i + 2]];
|
|
20691
|
+
for (let dy = 0; dy < scale; dy++) {
|
|
20692
|
+
for (let dx = 0; dx < scale; dx++) {
|
|
20693
|
+
const o = ((top + y * scale + dy) * this.width + left + x * scale + dx) * 4;
|
|
20694
|
+
for (let c = 0; c < 3; c++) this.data[o + c] = Math.round(src[c] * alpha + this.data[o + c] * (1 - alpha));
|
|
20695
|
+
}
|
|
20696
|
+
}
|
|
20697
|
+
}
|
|
20698
|
+
}
|
|
20699
|
+
}
|
|
20700
|
+
line(x0, y0, x1, y1, color, stroke) {
|
|
20701
|
+
const steps = Math.max(1, Math.ceil(Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0))));
|
|
20702
|
+
for (let step = 0; step <= steps; step++) {
|
|
20703
|
+
const t = step / steps;
|
|
20704
|
+
this.dot(x0 + (x1 - x0) * t, y0 + (y1 - y0) * t, stroke, color);
|
|
20705
|
+
}
|
|
20706
|
+
}
|
|
20707
|
+
/** A filled square `size` wide centred on (x, y): crisp at pixel-art scale. */
|
|
20708
|
+
dot(x, y, size, color) {
|
|
20709
|
+
const half = Math.floor(size / 2);
|
|
20710
|
+
for (let dy = -half; dy < size - half; dy++) for (let dx = -half; dx < size - half; dx++) this.set(x + dx, y + dy, color);
|
|
20711
|
+
}
|
|
20712
|
+
};
|
|
20713
|
+
|
|
20714
|
+
// src/cli/commands/skeleton.ts
|
|
20715
|
+
var ESTIMATE_SKELETON_SIZES = [16, 32, 64, 128, 256];
|
|
20541
20716
|
async function runEstimateSkeleton(args) {
|
|
20542
20717
|
const image = args.target;
|
|
20543
20718
|
if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
|
|
20719
|
+
if (args.out && existsSync32(path46.resolve(args.out)) && !args.force) {
|
|
20720
|
+
throw new UsageError(`${args.out} already exists; pass --force to replace it`);
|
|
20721
|
+
}
|
|
20544
20722
|
loadEnvFiles(path46.dirname(path46.resolve(args.manifest)));
|
|
20545
20723
|
loadEnvFiles(process.cwd());
|
|
20546
20724
|
const bytes = await readFile30(path46.resolve(image));
|
|
20547
20725
|
const metadata = imageMetadata(bytes);
|
|
20548
20726
|
if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
|
|
20727
|
+
if (metadata.width !== metadata.height || !ESTIMATE_SKELETON_SIZES.includes(metadata.width)) {
|
|
20728
|
+
throw new UsageError(
|
|
20729
|
+
`${image} is ${metadata.width}x${metadata.height}; estimate-skeleton takes a square image of ${ESTIMATE_SKELETON_SIZES.join(", ")} pixels`
|
|
20730
|
+
);
|
|
20731
|
+
}
|
|
20549
20732
|
const client2 = clientFromEnv();
|
|
20550
20733
|
const res = await client2.estimateSkeleton({
|
|
20551
20734
|
image: { base64: bytes.toString("base64"), format: metadata.format }
|
|
20552
20735
|
});
|
|
20553
|
-
const
|
|
20736
|
+
const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
|
|
20737
|
+
const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
|
|
20554
20738
|
if (args.out) {
|
|
20555
20739
|
await writeFile15(path46.resolve(args.out), json + "\n");
|
|
20556
|
-
log(` wrote ${
|
|
20740
|
+
log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
|
|
20741
|
+
log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
|
|
20557
20742
|
} else {
|
|
20558
20743
|
log(json);
|
|
20559
20744
|
}
|
|
20560
20745
|
}
|
|
20746
|
+
async function runSkeletonPreview(args) {
|
|
20747
|
+
const target = args.target;
|
|
20748
|
+
let keypointsFile;
|
|
20749
|
+
let sourceFile;
|
|
20750
|
+
let label;
|
|
20751
|
+
if (target.endsWith(".json")) {
|
|
20752
|
+
keypointsFile = path46.resolve(target);
|
|
20753
|
+
sourceFile = args.from ? path46.resolve(args.from) : void 0;
|
|
20754
|
+
label = target;
|
|
20755
|
+
} else {
|
|
20756
|
+
const loaded = await loadManifest(args.manifest);
|
|
20757
|
+
const specs = (await resolveSpecs(loaded, { styles: args.styles, assets: [target] })).filter((spec2) => spec2.assetId === target);
|
|
20758
|
+
const spec = specs.find((candidate) => candidate.revision?.mode === "animate-skeleton");
|
|
20759
|
+
if (!spec) {
|
|
20760
|
+
throw new UsageError(
|
|
20761
|
+
specs.length ? `${target} is not an animate-skeleton revision; pass a keypoints file instead` : `No asset "${target}" in ${args.manifest}`
|
|
20762
|
+
);
|
|
20763
|
+
}
|
|
20764
|
+
keypointsFile = spec.revision.keypointsFile;
|
|
20765
|
+
sourceFile = args.from ? path46.resolve(args.from) : spec.revision.sourceFile;
|
|
20766
|
+
label = `${spec.styleId}/${spec.assetId}`;
|
|
20767
|
+
}
|
|
20768
|
+
if (!existsSync32(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
|
|
20769
|
+
let json;
|
|
20770
|
+
try {
|
|
20771
|
+
json = JSON.parse(await readFile30(keypointsFile, "utf8"));
|
|
20772
|
+
} catch (error) {
|
|
20773
|
+
throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
20774
|
+
}
|
|
20775
|
+
const set = parseSkeletonSet(json, keypointsFile);
|
|
20776
|
+
let source;
|
|
20777
|
+
if (sourceFile) {
|
|
20778
|
+
if (!existsSync32(sourceFile)) {
|
|
20779
|
+
log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
|
|
20780
|
+
} else {
|
|
20781
|
+
const bytes = await readFile30(sourceFile);
|
|
20782
|
+
if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
|
|
20783
|
+
source = decodePng(bytes);
|
|
20784
|
+
}
|
|
20785
|
+
}
|
|
20786
|
+
const out = path46.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
|
|
20787
|
+
await writeFile15(out, renderSkeletonSheet(set, { source }));
|
|
20788
|
+
log(` ${label}: ${set.frames.length} frames after the starting pose`);
|
|
20789
|
+
const shown = path46.relative(process.cwd(), out);
|
|
20790
|
+
log(` wrote ${shown.startsWith("..") ? out : shown}`);
|
|
20791
|
+
}
|
|
20561
20792
|
|
|
20562
20793
|
// src/cli/commands/tools.ts
|
|
20563
20794
|
async function runTools(args) {
|
|
@@ -20602,7 +20833,7 @@ async function runTools(args) {
|
|
|
20602
20833
|
|
|
20603
20834
|
// src/cli/commands/workspace.ts
|
|
20604
20835
|
import path47 from "path";
|
|
20605
|
-
import { existsSync as
|
|
20836
|
+
import { existsSync as existsSync33 } from "fs";
|
|
20606
20837
|
async function runWorkspace(args) {
|
|
20607
20838
|
const workspacePath = path47.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
20608
20839
|
const dir = path47.dirname(workspacePath);
|
|
@@ -20632,7 +20863,7 @@ async function runWorkspace(args) {
|
|
|
20632
20863
|
log(` registered "${id}" in ${path47.relative(process.cwd(), workspacePath)}`);
|
|
20633
20864
|
log(` manifest: ${project.manifest}`);
|
|
20634
20865
|
log(` lock: ${project.lock}`);
|
|
20635
|
-
if (!
|
|
20866
|
+
if (!existsSync33(lockPath)) {
|
|
20636
20867
|
log(
|
|
20637
20868
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
20638
20869
|
);
|
|
@@ -20656,7 +20887,7 @@ async function runWorkspace(args) {
|
|
|
20656
20887
|
return;
|
|
20657
20888
|
}
|
|
20658
20889
|
if (args.subcommand === "list") {
|
|
20659
|
-
if (!
|
|
20890
|
+
if (!existsSync33(workspacePath)) {
|
|
20660
20891
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
20661
20892
|
}
|
|
20662
20893
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -20676,7 +20907,7 @@ async function runWorkspace(args) {
|
|
|
20676
20907
|
return;
|
|
20677
20908
|
}
|
|
20678
20909
|
if (args.subcommand === "status") {
|
|
20679
|
-
if (!
|
|
20910
|
+
if (!existsSync33(workspacePath)) {
|
|
20680
20911
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
20681
20912
|
}
|
|
20682
20913
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -20738,7 +20969,7 @@ async function runHelp() {
|
|
|
20738
20969
|
async function runVersion() {
|
|
20739
20970
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
20740
20971
|
const url = new URL(up, import.meta.url);
|
|
20741
|
-
if (!
|
|
20972
|
+
if (!existsSync34(url)) continue;
|
|
20742
20973
|
const pkg = JSON.parse(await readFile31(url, "utf8"));
|
|
20743
20974
|
if (pkg.name === "pixelkiln") {
|
|
20744
20975
|
log(`${pkg.name} ${pkg.version}`);
|
|
@@ -20784,6 +21015,7 @@ var RUNNERS = {
|
|
|
20784
21015
|
recipe: runRecipe,
|
|
20785
21016
|
workspace: runWorkspace,
|
|
20786
21017
|
"estimate-skeleton": runEstimateSkeleton,
|
|
21018
|
+
"skeleton-preview": runSkeletonPreview,
|
|
20787
21019
|
unzoom: runUnzoom,
|
|
20788
21020
|
font: runFont
|
|
20789
21021
|
};
|