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 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, and structural tile members | PNG stills/tiles/spritesheets or animated GIF |
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
- or when the measured one-generation `map`/`pixflux` routes fit the work. Choose
71
- Retro Diffusion when a native animation or spritesheet is required, an RD style
72
- is the desired look, or a USD quote is easier to budget. For a production batch,
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, but ComfyUI is
100
- the only built-in adapter that currently opts in. PixelLab, Retro Diffusion,
101
- and Scenario reject revision assets offline. See
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. Current PixelKiln routes top out at 400×400. | `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. |
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 are measurements from the endpoints PixelKiln currently
198
- uses; they are not a conversion to dollars:
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 | Measured cost |
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
- | `character` base | 1 (standard), 2–9 (v3), 20–40 (pro) |
207
- | `character` state | 20–40 generations |
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/inpaint lineage, 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 and loops as dependent assets generated in waves, free mirrored directions, account adoption, and SpriteFrames export. |
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
- `map` re-rolls cost the same as one 64×64 `1dir` call; conversely, `map` cannot
263
- replace a hard palette or reference-image constraint. See
264
- [Generator selection](./docs/GENERATORS.md) and the
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
- prices depend on the selected provider. Retro Diffusion also supports the
269
- provider-specific `animation` generator. ComfyUI supports `map` plus atomic
270
- still-image `frames` through an operator-supplied workflow. Scenario supports `map`
271
- with a required offline CU ceiling and a live quote before each paid call.
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/inpaint parents, masks, fail-closed readiness, provenance, and ComfyUI bindings. |
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
- these Markdown files by [`website/`](./website/README.md), so the site and the
389
- package share one source. Policies: [Contributing](./CONTRIBUTING.md),
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 existsSync33 } from "fs";
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 an image, direct
466
- and un-budgeted (like balance, not a generation). Prints the
467
- keypoints JSON, or writes it with --out \u2014 the way to bootstrap
468
- an animate-skeleton revision's keypointsFile.
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.string().min(1),
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 json = JSON.stringify(res.keypoints, null, 2);
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 ${res.keypoints.length} keypoints to ${args.out}`);
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 existsSync32 } from "fs";
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 (!existsSync32(lockPath)) {
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 (!existsSync32(workspacePath)) {
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 (!existsSync32(workspacePath)) {
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 (!existsSync33(url)) continue;
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
  };