pixelkiln 0.86.2 → 0.87.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
@@ -134,7 +134,7 @@ background. That distinction matters more than raw canvas size.
134
134
 
135
135
  | Asset type | PixelLab | Retro Diffusion | ComfyUI | Scenario |
136
136
  |---|---|---|---|---|
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. | Raw model output is opaque and anti-aliased (the BFL smoke keep used 19,619 colors), but a chain of render at four times the size, Pixelate to the target grid, Birefnet, and `refine` took a 960 px render of an alpine lodge to a 240×208 native grid at high confidence with 63 colors, for 18 CU (a 1 CU render can be nearly as good). Details and the Pixelate grid caveat are in [Set up Scenario](./docs/SCENARIO.md#large-and-illustrative-art-render-big-pixelate-refine). One sample per subject. |
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 10–25 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. | Raw model output is opaque and anti-aliased (the BFL smoke keep used 19,619 colors), but a chain of render at four times the size, Pixelate to the target grid, Birefnet, and `refine` took a 960 px render of an alpine lodge to a 240×208 native grid at high confidence with 63 colors, for 18 CU (a 1 CU render can be nearly as good). Details and the Pixelate grid caveat are in [Set up Scenario](./docs/SCENARIO.md#large-and-illustrative-art-render-big-pixelate-refine). One sample per subject. |
138
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. | GPT Image 2 at four times the target size, then Pixelate, turned a course-cover brief into a convincing pixel scene with 31 colors and legible title text. It has not been put through the shared benchmark. A large model canvas is not native pixel resolution: pixelate to the grid you need and check it with `refine`. |
139
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. |
140
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. |
@@ -206,20 +206,20 @@ conversion to dollars:
206
206
  |---|---:|
207
207
  | `map` | 1 generation |
208
208
  | `pixflux` | 1 generation |
209
- | `1dir` | 20–40 generations |
210
- | `imagePro` | 40 generations |
209
+ | `1dir` | 10–25 generations |
210
+ | `imagePro` | 10–25 generations by canvas (a flat 40 before October 2026) |
211
211
  | `imageProFlash` | 5–9 generations (provisional quote) |
212
- | `tiles` | 20–40 generations |
213
- | `terrain` | 20–40 generations (unmeasured) |
212
+ | `tiles` | 10–25 generations |
213
+ | `terrain` | 10–25 generations (unmeasured) |
214
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 |
217
- | `character` animation | 1 (template), 2–4 (`skeleton-v3`, budgeted at 4), 1 at 64px from text, 20–40 (pro) |
215
+ | `character` base | 1 (standard), 2–9 (v3), 10–25 (pro), 6–17 (pro-flash; rotations only from a reference) |
216
+ | `character` state or portrait | 10–25 generations |
217
+ | `character` animation | 1 (template), 2–4 (`skeleton-v3`, budgeted at 4), 1 at 64px from text, 10–25 (pro) |
218
218
  | `character` outfit | 20 generations (one 2-frame, 92×92 job) |
219
219
  | `objectPro` base | 6 at 64px (unmeasured) |
220
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) |
221
+ | `uiElement` | 10–25 generations (unmeasured) |
222
+ | `image-to-image` / `inpaint` revision | 10–25 generations; 5–9 with `engine: "pro-flash"` (provisional quote) |
223
223
  | `reduce-colors` / `correct-pixelart` revision | 0.1 generations (measured at 32×32) |
224
224
 
225
225
  Retro Diffusion publishes USD formulas and fixed prices. Examples relevant to
package/README.md CHANGED
@@ -55,7 +55,7 @@ provenance and no long-lived npm token.
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
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
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
+ | PixelLab utilities | `unzoom` returns upscaled reference art to its native grid, `pixelate` turns a photo or render into pixel art (Image to Pixel Art Pro Flash, 6 generations), `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. |
59
59
  | Existing-art onboarding | Manifest scaffolding, exact-hash account adoption, and prompt recovery. |
60
60
  | Recovery | Safe stale-output replacement, validated caches, durable references, resumable paid jobs, and per-asset generation history with free restores. |
61
61
  | Shared-account safety | Cross-project claim files or a registered workspace catalog, sibling-style exclusion, reviewed salvage, keep/discard tags, separate confirmed purge. |
@@ -255,9 +255,9 @@ Measured PixelLab economics vary by 40×:
255
255
  |---|---|---:|
256
256
  | Standalone arbitrary-size prop/icon | `map` (default) | 1 generation |
257
257
  | Exact closed palette | `pixflux` | 1 generation |
258
- | Candidate variety/reference anchoring/future animation | `1dir` | 20–40 generations |
259
- | Independent or connectable ground tiles | `tiles` | 20–40 generations |
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 |
258
+ | Candidate variety/reference anchoring/future animation | `1dir` | 10–25 generations |
259
+ | Independent or connectable ground tiles | `tiles` | 10–25 generations |
260
+ | A character in 4 or 8 directions, its poses, its loops | `character` | 1 per base, 10–25 per pose, 1 per template loop per direction; a `mirror` of a loop is free |
261
261
 
262
262
  Start with the required capability, not the most expensive endpoint. Forty `map`
263
263
  re-rolls cost the same as one 64×64 `1dir` call; conversely, `map` cannot replace a
package/dist/cli.js CHANGED
@@ -162,6 +162,7 @@ var COMMANDS = [
162
162
  "estimate-skeleton",
163
163
  "skeleton-preview",
164
164
  "unzoom",
165
+ "pixelate",
165
166
  "font",
166
167
  "help",
167
168
  "--help",
@@ -499,8 +500,12 @@ Commands
499
500
  add/remove/list/status/claims. Offline.
500
501
  unzoom PixelLab: shrink an upscaled image (--from) back to its native
501
502
  pixel grid before using it as a style or reference image.
503
+ pixelate PixelLab: convert a photo, painting, or render (--from) into
504
+ pixel art with Image to Pixel Art Pro Flash; PixelLab picks the
505
+ output size. Optional --description steers the style. 6
506
+ generations; asks first.
502
507
  font PixelLab: generate a pixel font (--description, --out); writes a
503
- .ttf and a glyph atlas PNG. 25 generations; asks first.
508
+ .ttf and a glyph atlas PNG. 15 generations; asks first.
504
509
 
505
510
  Options
506
511
  --columns <n> pack/export: sprites or tiles per row (default: near-square)
@@ -523,7 +528,8 @@ Options
523
528
  --model-root <dir> recipe verify: also hash required local model files
524
529
  --generation <n|hash> restore: the previous generation to bring back (1 = newest)
525
530
  --quantize <n> unzoom: 0 auto palette (default), -1 keep all colors, 2-256 exact
526
- --description <text> font: the style to draw, e.g. "warm orange arcade font"
531
+ --description <text> font: the style to draw, e.g. "warm orange arcade font";
532
+ pixelate: an optional style steer, e.g. "light dithering"
527
533
  --weight <w> font: Bold or Regular (default)
528
534
  --glyph-px <n> font: native glyph size, 8, 16 (default), 32, or 64
529
535
  --prune cache: remove invalid/unreferenced local cache data
@@ -583,6 +589,7 @@ Examples
583
589
  pixelkiln quality snapshot --inputs quality-inputs.json --out pixelkiln.quality.json
584
590
  pixelkiln quality check --from pixelkiln.quality.json
585
591
  pixelkiln unzoom --from refs/knight-512.png --out refs/knight.png
592
+ pixelkiln pixelate --from renders/lodge.png --out art/lodge.png
586
593
  pixelkiln font --description "warm orange arcade font" --weight Bold --out fonts/arcade
587
594
  pixelkiln workspace add ../other-game/pixelkiln.manifest.json
588
595
  pixelkiln workspace status --json
@@ -849,17 +856,24 @@ function candidateCount(size) {
849
856
  }
850
857
  function generationCost(width, height, generator = "map") {
851
858
  if (generator === "map" || generator === "pixflux") return 1;
852
- if (generator === "imagePro") return 40;
859
+ if (generator === "imagePro") return proTierCost(width, height);
853
860
  const px = width * height;
854
- if (px <= 1024) return 20;
855
- if (px <= 2048) return 25;
856
- return 40;
861
+ if (px <= 1024) return PRO_TIER_PRICES[0];
862
+ if (px <= 2048) return PRO_TIER_PRICES[1];
863
+ return PRO_TIER_PRICES[2];
864
+ }
865
+ var PRO_TIER_PRICES = [10, 15, 25];
866
+ function proTierCost(width, height) {
867
+ const px = width * height;
868
+ if (px <= 65536) return PRO_TIER_PRICES[0];
869
+ if (px <= 102400) return PRO_TIER_PRICES[1];
870
+ return PRO_TIER_PRICES[2];
857
871
  }
858
872
  function tilesCost(tileSize, variations) {
859
873
  const px = tileSize * tileSize * Math.max(1, variations);
860
- if (px <= 1024) return 20;
861
- if (px <= 2048) return 25;
862
- return 40;
874
+ if (px <= 1024) return PRO_TIER_PRICES[0];
875
+ if (px <= 2048) return PRO_TIER_PRICES[1];
876
+ return PRO_TIER_PRICES[2];
863
877
  }
864
878
  function isometricTileCost() {
865
879
  return 1;
@@ -1375,7 +1389,7 @@ var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-anim
1375
1389
  var StyleObjectSchema = z.object({
1376
1390
  /** Generation backend for this style. Omit to inherit the manifest default. */
1377
1391
  provider: z.string().min(1).optional(),
1378
- // `map` is the default because it is 20-40x cheaper and correct for any
1392
+ // `map` is the default because it is 10-25x cheaper and correct for any
1379
1393
  // asset that is not going to be rotated or animated.
1380
1394
  generator: GeneratorSchema.default("map"),
1381
1395
  /** Square edge length. The active provider owns its exact limit. */
@@ -6820,7 +6834,8 @@ var PixelLabClient = class {
6820
6834
  * A full-body sprite in, a bust portrait out (or the reverse, unused
6821
6835
  * today). Job-based like a state or `1dir`, not the couple-generations
6822
6836
  * "conversion" the name suggests: the smallest `resultSize` (16px) billed
6823
- * 20 generations live (docs/ENDPOINTS.md, "Characters, measured").
6837
+ * 20 generations live (docs/ENDPOINTS.md, "Characters, measured"), the
6838
+ * Pro floor before PixelLab's October 2026 cut put it at 10.
6824
6839
  */
6825
6840
  async createPortraitCharacterPro(args) {
6826
6841
  const raw = await this.request("/portrait-character-pro", {
@@ -6859,7 +6874,8 @@ var PixelLabClient = class {
6859
6874
  * everything else here, but the completed job returns the result frames
6860
6875
  * inline as base64 (`last_response.quantized_images`), not as storage
6861
6876
  * URLs — the one PixelLab character tool that works this way. A live
6862
- * 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md).
6877
+ * 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md), before
6878
+ * PixelLab's October 2026 Pro cut halved that floor to 10.
6863
6879
  */
6864
6880
  async transferOutfitV2(args) {
6865
6881
  const image = (img) => ({
@@ -6909,7 +6925,9 @@ var PixelLabClient = class {
6909
6925
  * far higher than this tiering assumes; 352x352 (123904px²), 384x384
6910
6926
  * (147456px²), and 512x512 (262144px², the tiering ceiling) all billed
6911
6927
  * 40. Both breakpoints are now tightly bracketed: 20->25 in
6912
- * (65536px², 82944px²], 25->40 in (102400px², 123904px²].
6928
+ * (65536px², 82944px²], 25->40 in (102400px², 123904px²]. Those prices
6929
+ * predate PixelLab's October 2026 Pro cut to 10/15/25; `proTierCost`
6930
+ * keeps the breakpoints with the new prices.
6913
6931
  * `editImagesV2` below returns the same fields under `images`, plural
6914
6932
  * and array-wrapped, not `image` — do not assume the two endpoints share
6915
6933
  * one response shape. See docs/ENDPOINTS.md and docs/REVISIONS.md for
@@ -6962,8 +6980,9 @@ var PixelLabClient = class {
6962
6980
  );
6963
6981
  }
6964
6982
  /**
6965
- * PixelLab's Pro image tier, `/generate-image-v2`: a flat 40 generations
6966
- * (docs/ENDPOINTS.md, "Single-image generators, measured") for real style
6983
+ * PixelLab's Pro image tier, `/generate-image-v2`: 10 to 25 generations by
6984
+ * canvas since PixelLab's October 2026 Pro cut, a flat 40 before
6985
+ * (docs/ENDPOINTS.md, "Single-image generators, measured"), for real style
6967
6986
  * transfer and non-square canvases up to 792x688, where `pixflux` is
6968
6987
  * limited to 400x400 and no style reference. Like a revision, this hands
6969
6988
  * back a plain background job with no resource of its own; unlike a
@@ -7049,6 +7068,24 @@ var PixelLabClient = class {
7049
7068
  "inpaint-image-pro-flash"
7050
7069
  );
7051
7070
  }
7071
+ /**
7072
+ * `/image-to-pixelart-pro-flash`: a photo, painting, or render in, pixel
7073
+ * art out, with no size to pass: PixelLab detects the native pixel scale
7074
+ * and picks the output size (a 256x256 render came back 108x109, on a
7075
+ * transparent background). A flat 6 generations, billed exactly that live
7076
+ * in October 2026. Sources over 2048px on the longer side are scaled down
7077
+ * upstream. The finished job carries the image at `last_response.images[0]`.
7078
+ */
7079
+ async imageToPixelartProFlash(args) {
7080
+ const body = { image: args.image };
7081
+ if (args.description) body.description = args.description;
7082
+ if (args.seed != null) body.seed = args.seed;
7083
+ return validateResponse(
7084
+ ProFlashImageSubmitSchema,
7085
+ await this.request("/image-to-pixelart-pro-flash", { method: "POST", body: JSON.stringify(body) }),
7086
+ "image-to-pixelart-pro-flash"
7087
+ );
7088
+ }
7052
7089
  /**
7053
7090
  * `/generate-ui-v2` ("Generate UI (Pro)"): one UI element (a button, a
7054
7091
  * health bar, an inventory slot, a dialogue box) from a description,
@@ -7443,7 +7480,7 @@ function characterCost(spec) {
7443
7480
  }
7444
7481
  if (character.kind === "state" || character.kind === "portrait") return generationCost(spec.width, spec.height, "1dir");
7445
7482
  if (character.kind === "outfit") {
7446
- return 20;
7483
+ return PRO_TIER_PRICES[0];
7447
7484
  }
7448
7485
  if (character.mode === "standard") return 1;
7449
7486
  if (character.mode === "v3") {
@@ -7695,7 +7732,7 @@ var PixelLabProvider = class _PixelLabProvider {
7695
7732
  if (spec.revision) {
7696
7733
  const width = spec.revision.sourceWidth ?? spec.width;
7697
7734
  const height = spec.revision.sourceHeight ?? spec.height;
7698
- return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
7735
+ return { unit: "generations", amount: proTierCost(width, height), candidates: 1 };
7699
7736
  }
7700
7737
  if (spec.generator === "tiles" || spec.generator === "terrain" || spec.generator === "isometricTile") {
7701
7738
  return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
@@ -17650,6 +17687,12 @@ async function currentArt(entry, spec) {
17650
17687
  }
17651
17688
  return { path: file, contentType, width, height };
17652
17689
  }
17690
+ async function cachedFrame(url) {
17691
+ if (!url.startsWith("file://")) return null;
17692
+ const file = url.slice("file://".length);
17693
+ const contentType = mediaTypeFromExtension(file) ?? detectMediaType(await readFile23(file).catch(() => Buffer.alloc(0))) ?? "application/octet-stream";
17694
+ return { path: file, contentType };
17695
+ }
17653
17696
  async function prepareReview(provider, lock, opts = {}) {
17654
17697
  const log2 = opts.onProgress ?? (() => {
17655
17698
  });
@@ -17659,6 +17702,7 @@ async function prepareReview(provider, lock, opts = {}) {
17659
17702
  );
17660
17703
  const reviewAssets = /* @__PURE__ */ new Map();
17661
17704
  const prefix = opts.routePrefix ?? "";
17705
+ const providerUrls = /* @__PURE__ */ new Map();
17662
17706
  const groups = [];
17663
17707
  for (const [key, entry] of Object.entries(lock.entries)) {
17664
17708
  if (entry.provider !== provider.id || selectedKeys && !selectedKeys.has(key)) continue;
@@ -17667,7 +17711,19 @@ async function prepareReview(provider, lock, opts = {}) {
17667
17711
  const spec = specByKey.get(key);
17668
17712
  const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec, metadata: entry.providerMetadata?.[provider.id] });
17669
17713
  if (state.status !== "review" && state.status !== "review-set" || (state.status === "review" ? !state.candidateUrls.length : !state.frameUrls.length)) continue;
17670
- const frameUrls = state.status === "review" ? state.candidateUrls : state.frameUrls;
17714
+ const urls = state.status === "review" ? state.candidateUrls : state.frameUrls;
17715
+ const frameUrls = [];
17716
+ const frameAssets = [];
17717
+ for (const [index, url] of urls.entries()) {
17718
+ const cached = await cachedFrame(url);
17719
+ if (!cached) {
17720
+ frameUrls.push(url);
17721
+ continue;
17722
+ }
17723
+ const route = `${prefix}/review-frame/${encodeURIComponent(String(groups.length))}/${index}`;
17724
+ frameAssets.push([route, cached]);
17725
+ frameUrls.push(route);
17726
+ }
17671
17727
  const sourceRoute = spec?.revision ? `${prefix}/revision-source/${encodeURIComponent(String(groups.length))}` : null;
17672
17728
  if (sourceRoute && spec?.revision) {
17673
17729
  reviewAssets.set(sourceRoute, {
@@ -17680,6 +17736,8 @@ async function prepareReview(provider, lock, opts = {}) {
17680
17736
  if (current && currentRoute) {
17681
17737
  reviewAssets.set(currentRoute, { path: current.path, contentType: current.contentType });
17682
17738
  }
17739
+ for (const [route, asset] of frameAssets) reviewAssets.set(route, asset);
17740
+ providerUrls.set(key, urls);
17683
17741
  groups.push({
17684
17742
  key,
17685
17743
  assetId: entry.assetId,
@@ -17762,7 +17820,7 @@ async function prepareReview(provider, lock, opts = {}) {
17762
17820
  status: "selected",
17763
17821
  objectId,
17764
17822
  candidateIndex: index,
17765
- sourceUrl: sourceUrl ?? group.frameUrls[index] ?? null,
17823
+ sourceUrl: sourceUrl ?? providerUrls.get(key)?.[index] ?? null,
17766
17824
  sourceUrls: [],
17767
17825
  provider: provider.id,
17768
17826
  providerMetadata: metadata ? {
@@ -20646,7 +20704,7 @@ async function unzoomFile(client2, input, opts = {}) {
20646
20704
  }
20647
20705
  var FONT_WEIGHTS = ["Bold", "Regular"];
20648
20706
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
20649
- var FONT_COST_GENERATIONS = 25;
20707
+ var FONT_COST_GENERATIONS = 15;
20650
20708
  function fontOutputPaths(out) {
20651
20709
  const base = path43.resolve(out.replace(/\.(ttf|png)$/i, ""));
20652
20710
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
@@ -20695,6 +20753,61 @@ async function generateFont(client2, opts) {
20695
20753
  await sleep3(interval);
20696
20754
  }
20697
20755
  }
20756
+ var PIXELATE_COST_GENERATIONS = 6;
20757
+ var PIXELATE_MAX_SIDE = 2048;
20758
+ function defaultPixelateOut(input) {
20759
+ const ext = path43.extname(input);
20760
+ return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
20761
+ }
20762
+ async function pixelateFile(client2, input, opts = {}) {
20763
+ const out = path43.resolve(opts.out ?? defaultPixelateOut(input));
20764
+ if (path43.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
20765
+ if (existsSync33(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
20766
+ const bytes = await readFile29(input);
20767
+ const metadata = imageMetadata(bytes);
20768
+ if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
20769
+ const { width, height, format } = metadata;
20770
+ const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
20771
+ const interval = opts.intervalMs ?? 5e3;
20772
+ const timeout = opts.timeoutMs ?? 10 * 6e4;
20773
+ const submitted = await client2.imageToPixelartProFlash({
20774
+ image: { base64: bytes.toString("base64"), format },
20775
+ description: opts.description,
20776
+ seed: opts.seed
20777
+ });
20778
+ const jobId = submitted.background_job_id;
20779
+ const started = Date.now();
20780
+ for (; ; ) {
20781
+ const job = await client2.getBackgroundJob(jobId);
20782
+ opts.onProgress?.(job.status);
20783
+ if (job.status === "failed") throw new Error(`PixelLab pixelate job ${jobId} failed upstream`);
20784
+ if (job.status === "completed") {
20785
+ const images = job.last_response?.images;
20786
+ const first = Array.isArray(images) ? images[0] : void 0;
20787
+ if (!first || typeof first.base64 !== "string" || !first.base64) {
20788
+ throw new Error(`PixelLab pixelate job ${jobId} completed without an image at last_response.images[0]`);
20789
+ }
20790
+ const png = Buffer.from(first.base64, "base64");
20791
+ const size = imageMetadata(png);
20792
+ await mkdir13(path43.dirname(out), { recursive: true });
20793
+ await writeFile18(out, png);
20794
+ return {
20795
+ jobId,
20796
+ out,
20797
+ original: { width, height },
20798
+ pixelated: { width: size?.width ?? Number(first.width), height: size?.height ?? Number(first.height) },
20799
+ imageId: submitted.image_id,
20800
+ usage: job.usage ?? null
20801
+ };
20802
+ }
20803
+ if (Date.now() - started > timeout) {
20804
+ throw new Error(
20805
+ `PixelLab pixelate job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
20806
+ );
20807
+ }
20808
+ await sleep3(interval);
20809
+ }
20810
+ }
20698
20811
 
20699
20812
  // src/cli/commands/pixellab-utils.ts
20700
20813
  function loadKeys(args) {
@@ -20762,6 +20875,39 @@ async function runFont(args) {
20762
20875
  log(` wrote ${res.ttf}`);
20763
20876
  log(` wrote ${res.atlas}`);
20764
20877
  }
20878
+ async function runPixelate(args) {
20879
+ if (!args.from) throw new UsageError("pixelate needs --from <image>: the photo, painting, or render to convert");
20880
+ const out = args.out ?? defaultPixelateOut(args.from);
20881
+ log(` pixelate ${args.from} \u2192 ${out}${args.description ? ` ("${args.description}")` : ""}; PixelLab picks the output size`);
20882
+ log(` cost: ${PIXELATE_COST_GENERATIONS} generations (Image to Pixel Art Pro Flash, flat)`);
20883
+ if (args.budget !== void 0 && args.budget < PIXELATE_COST_GENERATIONS) {
20884
+ throw new Error(`--budget ${args.budget} is below this conversion's ${PIXELATE_COST_GENERATIONS}-generation cost`);
20885
+ }
20886
+ if (args.dryRun) {
20887
+ log(" dry run: nothing sent");
20888
+ return;
20889
+ }
20890
+ if (!await confirm(` Spend ${PIXELATE_COST_GENERATIONS} generations on this conversion?`, args.yes)) return;
20891
+ loadKeys(args);
20892
+ let last = "";
20893
+ const res = await pixelateFile(clientFromEnv(), args.from, {
20894
+ out,
20895
+ description: args.description,
20896
+ force: args.force,
20897
+ onProgress: (status) => {
20898
+ if (status !== last) log(` ${status}\u2026`);
20899
+ last = status;
20900
+ }
20901
+ });
20902
+ if (args.json) {
20903
+ log(JSON.stringify({ version: 1, ...res }, null, 2));
20904
+ return;
20905
+ }
20906
+ const scaled = Math.max(res.original.width, res.original.height) > PIXELATE_MAX_SIDE ? " (scaled down upstream first)" : "";
20907
+ log(
20908
+ ` ${res.original.width}x${res.original.height}${scaled} \u2192 ${res.pixelated.width}x${res.pixelated.height}; wrote ${res.out}`
20909
+ );
20910
+ }
20765
20911
 
20766
20912
  // src/cli/commands/quality.ts
20767
20913
  import path46 from "path";
@@ -22194,6 +22340,7 @@ var RUNNERS = {
22194
22340
  "estimate-skeleton": runEstimateSkeleton,
22195
22341
  "skeleton-preview": runSkeletonPreview,
22196
22342
  unzoom: runUnzoom,
22343
+ pixelate: runPixelate,
22197
22344
  font: runFont
22198
22345
  };
22199
22346
  async function main(argv = process.argv.slice(2)) {