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.
@@ -5014,7 +5014,7 @@
5014
5014
  var ENGINES = [
5015
5015
  { id: "v3", label: "v3: PixelLab's recommended base engine (2 to 9 by size; 1 from a sprite)", min: 32, max: 256, directions: [8] },
5016
5016
  { id: "pro-flash", label: "pro-flash: the newest image model draws the south sprite (6 to 17 by size)", min: 16, max: 256, directions: [8] },
5017
- { id: "pro", label: "pro: reference-based, 20 to 40 by size", min: 32, max: 168, directions: [8] },
5017
+ { id: "pro", label: "pro: reference-based, 10 to 25 by size", min: 32, max: 168, directions: [8] },
5018
5018
  { id: "standard", label: "standard: skeleton template, 1 generation", min: 16, max: 256, directions: [8, 4] }
5019
5019
  ];
5020
5020
  var BODIES = ["mannequin", "bear", "cat", "dog", "horse", "lion"];
@@ -5026,7 +5026,7 @@
5026
5026
  { id: "skeleton-v3", label: "skeleton-v3: a template, posed by the skeleton model (steadiest; 2-4 per direction)" },
5027
5027
  { id: "template", label: "template: a template, redrawn (1 per direction)" },
5028
5028
  { id: "v3", label: "v3: described in words" },
5029
- { id: "pro", label: "pro: described in words, sequential (20-40 per direction)" }
5029
+ { id: "pro", label: "pro: described in words, sequential (10-25 per direction)" }
5030
5030
  ];
5031
5031
  var templated = (mode) => mode === "skeleton-v3" || mode === "template";
5032
5032
  var KNOWN_TEMPLATES = [
@@ -5172,7 +5172,7 @@
5172
5172
  portraitSize.value = "64";
5173
5173
  const portraitSection = el("section", "studio-part");
5174
5174
  const portraitCheck = el("label", "field check");
5175
- portraitCheck.append(portrait, el("span", null, "add a bust portrait (20 to 40 generations)"));
5175
+ portraitCheck.append(portrait, el("span", null, "add a bust portrait (10 to 25 generations)"));
5176
5176
  portraitSection.append(el("h3", null, "Portrait"), portraitCheck, field("portrait size", portraitSize));
5177
5177
  const priceBox = el("section", "studio-part studio-price");
5178
5178
  const actions = el("div", "actions");
package/dist/index.cjs CHANGED
@@ -86,8 +86,11 @@ __export(index_exports, {
86
86
  MapSceneSchema: () => MapSceneSchema,
87
87
  MediaType: () => MediaType,
88
88
  OverwriteRefusedError: () => OverwriteRefusedError,
89
+ PIXELATE_COST_GENERATIONS: () => PIXELATE_COST_GENERATIONS,
90
+ PIXELATE_MAX_SIDE: () => PIXELATE_MAX_SIDE,
89
91
  PIXEL_ART_FIXER_REVISION: () => PIXEL_ART_FIXER_REVISION,
90
92
  PIXEL_ART_FIXER_URL: () => PIXEL_ART_FIXER_URL,
93
+ PRO_TIER_PRICES: () => PRO_TIER_PRICES,
91
94
  PixelKilnError: () => PixelKilnError,
92
95
  PixelLabClient: () => PixelLabClient,
93
96
  PixelLabError: () => PixelLabError,
@@ -143,6 +146,7 @@ __export(index_exports, {
143
146
  currentEntryOutputPath: () => currentEntryOutputPath,
144
147
  currentOutputPath: () => currentOutputPath,
145
148
  defaultOpenCommand: () => defaultOpenCommand,
149
+ defaultPixelateOut: () => defaultPixelateOut,
146
150
  defaultUnzoomOut: () => defaultUnzoomOut,
147
151
  detachHandEdit: () => detachHandEdit,
148
152
  detectMediaType: () => detectMediaType,
@@ -225,11 +229,13 @@ __export(index_exports, {
225
229
  parseTerrainDescriptions: () => parseTerrainDescriptions,
226
230
  parseWorkspace: () => parseWorkspace,
227
231
  pickHistory: () => pickHistory,
232
+ pixelateFile: () => pixelateFile,
228
233
  pngSize: () => pngSize,
229
234
  poll: () => poll,
230
235
  portableOutputPath: () => portableOutputPath,
231
236
  prepareReview: () => prepareReview,
232
237
  primaryOutput: () => primaryOutput,
238
+ proTierCost: () => proTierCost,
233
239
  proportionsPayload: () => proportionsPayload,
234
240
  providerCredentialEnvs: () => providerCredentialEnvs,
235
241
  providerFactory: () => providerFactory,
@@ -360,17 +366,24 @@ function candidateCount(size) {
360
366
  }
361
367
  function generationCost(width, height, generator = "map") {
362
368
  if (generator === "map" || generator === "pixflux") return 1;
363
- if (generator === "imagePro") return 40;
369
+ if (generator === "imagePro") return proTierCost(width, height);
364
370
  const px = width * height;
365
- if (px <= 1024) return 20;
366
- if (px <= 2048) return 25;
367
- return 40;
371
+ if (px <= 1024) return PRO_TIER_PRICES[0];
372
+ if (px <= 2048) return PRO_TIER_PRICES[1];
373
+ return PRO_TIER_PRICES[2];
374
+ }
375
+ var PRO_TIER_PRICES = [10, 15, 25];
376
+ function proTierCost(width, height) {
377
+ const px = width * height;
378
+ if (px <= 65536) return PRO_TIER_PRICES[0];
379
+ if (px <= 102400) return PRO_TIER_PRICES[1];
380
+ return PRO_TIER_PRICES[2];
368
381
  }
369
382
  function tilesCost(tileSize, variations) {
370
383
  const px = tileSize * tileSize * Math.max(1, variations);
371
- if (px <= 1024) return 20;
372
- if (px <= 2048) return 25;
373
- return 40;
384
+ if (px <= 1024) return PRO_TIER_PRICES[0];
385
+ if (px <= 2048) return PRO_TIER_PRICES[1];
386
+ return PRO_TIER_PRICES[2];
374
387
  }
375
388
  function isometricTileCost() {
376
389
  return 1;
@@ -886,7 +899,7 @@ var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-anim
886
899
  var StyleObjectSchema = import_zod.z.object({
887
900
  /** Generation backend for this style. Omit to inherit the manifest default. */
888
901
  provider: import_zod.z.string().min(1).optional(),
889
- // `map` is the default because it is 20-40x cheaper and correct for any
902
+ // `map` is the default because it is 10-25x cheaper and correct for any
890
903
  // asset that is not going to be rotated or animated.
891
904
  generator: GeneratorSchema.default("map"),
892
905
  /** Square edge length. The active provider owns its exact limit. */
@@ -2709,7 +2722,8 @@ var PixelLabClient = class {
2709
2722
  * A full-body sprite in, a bust portrait out (or the reverse, unused
2710
2723
  * today). Job-based like a state or `1dir`, not the couple-generations
2711
2724
  * "conversion" the name suggests: the smallest `resultSize` (16px) billed
2712
- * 20 generations live (docs/ENDPOINTS.md, "Characters, measured").
2725
+ * 20 generations live (docs/ENDPOINTS.md, "Characters, measured"), the
2726
+ * Pro floor before PixelLab's October 2026 cut put it at 10.
2713
2727
  */
2714
2728
  async createPortraitCharacterPro(args) {
2715
2729
  const raw = await this.request("/portrait-character-pro", {
@@ -2748,7 +2762,8 @@ var PixelLabClient = class {
2748
2762
  * everything else here, but the completed job returns the result frames
2749
2763
  * inline as base64 (`last_response.quantized_images`), not as storage
2750
2764
  * URLs — the one PixelLab character tool that works this way. A live
2751
- * 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md).
2765
+ * 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md), before
2766
+ * PixelLab's October 2026 Pro cut halved that floor to 10.
2752
2767
  */
2753
2768
  async transferOutfitV2(args) {
2754
2769
  const image = (img) => ({
@@ -2798,7 +2813,9 @@ var PixelLabClient = class {
2798
2813
  * far higher than this tiering assumes; 352x352 (123904px²), 384x384
2799
2814
  * (147456px²), and 512x512 (262144px², the tiering ceiling) all billed
2800
2815
  * 40. Both breakpoints are now tightly bracketed: 20->25 in
2801
- * (65536px², 82944px²], 25->40 in (102400px², 123904px²].
2816
+ * (65536px², 82944px²], 25->40 in (102400px², 123904px²]. Those prices
2817
+ * predate PixelLab's October 2026 Pro cut to 10/15/25; `proTierCost`
2818
+ * keeps the breakpoints with the new prices.
2802
2819
  * `editImagesV2` below returns the same fields under `images`, plural
2803
2820
  * and array-wrapped, not `image` — do not assume the two endpoints share
2804
2821
  * one response shape. See docs/ENDPOINTS.md and docs/REVISIONS.md for
@@ -2851,8 +2868,9 @@ var PixelLabClient = class {
2851
2868
  );
2852
2869
  }
2853
2870
  /**
2854
- * PixelLab's Pro image tier, `/generate-image-v2`: a flat 40 generations
2855
- * (docs/ENDPOINTS.md, "Single-image generators, measured") for real style
2871
+ * PixelLab's Pro image tier, `/generate-image-v2`: 10 to 25 generations by
2872
+ * canvas since PixelLab's October 2026 Pro cut, a flat 40 before
2873
+ * (docs/ENDPOINTS.md, "Single-image generators, measured"), for real style
2856
2874
  * transfer and non-square canvases up to 792x688, where `pixflux` is
2857
2875
  * limited to 400x400 and no style reference. Like a revision, this hands
2858
2876
  * back a plain background job with no resource of its own; unlike a
@@ -2938,6 +2956,24 @@ var PixelLabClient = class {
2938
2956
  "inpaint-image-pro-flash"
2939
2957
  );
2940
2958
  }
2959
+ /**
2960
+ * `/image-to-pixelart-pro-flash`: a photo, painting, or render in, pixel
2961
+ * art out, with no size to pass: PixelLab detects the native pixel scale
2962
+ * and picks the output size (a 256x256 render came back 108x109, on a
2963
+ * transparent background). A flat 6 generations, billed exactly that live
2964
+ * in October 2026. Sources over 2048px on the longer side are scaled down
2965
+ * upstream. The finished job carries the image at `last_response.images[0]`.
2966
+ */
2967
+ async imageToPixelartProFlash(args) {
2968
+ const body = { image: args.image };
2969
+ if (args.description) body.description = args.description;
2970
+ if (args.seed != null) body.seed = args.seed;
2971
+ return validateResponse(
2972
+ ProFlashImageSubmitSchema,
2973
+ await this.request("/image-to-pixelart-pro-flash", { method: "POST", body: JSON.stringify(body) }),
2974
+ "image-to-pixelart-pro-flash"
2975
+ );
2976
+ }
2941
2977
  /**
2942
2978
  * `/generate-ui-v2` ("Generate UI (Pro)"): one UI element (a button, a
2943
2979
  * health bar, an inventory slot, a dialogue box) from a description,
@@ -4034,7 +4070,7 @@ function characterCost(spec) {
4034
4070
  }
4035
4071
  if (character.kind === "state" || character.kind === "portrait") return generationCost(spec.width, spec.height, "1dir");
4036
4072
  if (character.kind === "outfit") {
4037
- return 20;
4073
+ return PRO_TIER_PRICES[0];
4038
4074
  }
4039
4075
  if (character.mode === "standard") return 1;
4040
4076
  if (character.mode === "v3") {
@@ -4227,7 +4263,7 @@ var PixelLabProvider = class _PixelLabProvider {
4227
4263
  if (spec.revision) {
4228
4264
  const width = spec.revision.sourceWidth ?? spec.width;
4229
4265
  const height = spec.revision.sourceHeight ?? spec.height;
4230
- return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
4266
+ return { unit: "generations", amount: proTierCost(width, height), candidates: 1 };
4231
4267
  }
4232
4268
  if (spec.generator === "tiles" || spec.generator === "terrain" || spec.generator === "isometricTile") {
4233
4269
  return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
@@ -12270,7 +12306,7 @@ async function unzoomFile(client2, input, opts = {}) {
12270
12306
  }
12271
12307
  var FONT_WEIGHTS = ["Bold", "Regular"];
12272
12308
  var FONT_GLYPH_SIZES = [8, 16, 32, 64];
12273
- var FONT_COST_GENERATIONS = 25;
12309
+ var FONT_COST_GENERATIONS = 15;
12274
12310
  function fontOutputPaths(out) {
12275
12311
  const base = import_node_path17.default.resolve(out.replace(/\.(ttf|png)$/i, ""));
12276
12312
  return { ttf: `${base}.ttf`, atlas: `${base}.png` };
@@ -12319,6 +12355,61 @@ async function generateFont(client2, opts) {
12319
12355
  await sleep3(interval);
12320
12356
  }
12321
12357
  }
12358
+ var PIXELATE_COST_GENERATIONS = 6;
12359
+ var PIXELATE_MAX_SIDE = 2048;
12360
+ function defaultPixelateOut(input) {
12361
+ const ext = import_node_path17.default.extname(input);
12362
+ return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
12363
+ }
12364
+ async function pixelateFile(client2, input, opts = {}) {
12365
+ const out = import_node_path17.default.resolve(opts.out ?? defaultPixelateOut(input));
12366
+ if (import_node_path17.default.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
12367
+ if ((0, import_node_fs13.existsSync)(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
12368
+ const bytes = await (0, import_promises9.readFile)(input);
12369
+ const metadata = imageMetadata(bytes);
12370
+ if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
12371
+ const { width, height, format } = metadata;
12372
+ const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
12373
+ const interval = opts.intervalMs ?? 5e3;
12374
+ const timeout = opts.timeoutMs ?? 10 * 6e4;
12375
+ const submitted = await client2.imageToPixelartProFlash({
12376
+ image: { base64: bytes.toString("base64"), format },
12377
+ description: opts.description,
12378
+ seed: opts.seed
12379
+ });
12380
+ const jobId = submitted.background_job_id;
12381
+ const started = Date.now();
12382
+ for (; ; ) {
12383
+ const job = await client2.getBackgroundJob(jobId);
12384
+ opts.onProgress?.(job.status);
12385
+ if (job.status === "failed") throw new Error(`PixelLab pixelate job ${jobId} failed upstream`);
12386
+ if (job.status === "completed") {
12387
+ const images = job.last_response?.images;
12388
+ const first = Array.isArray(images) ? images[0] : void 0;
12389
+ if (!first || typeof first.base64 !== "string" || !first.base64) {
12390
+ throw new Error(`PixelLab pixelate job ${jobId} completed without an image at last_response.images[0]`);
12391
+ }
12392
+ const png = Buffer.from(first.base64, "base64");
12393
+ const size = imageMetadata(png);
12394
+ await (0, import_promises9.mkdir)(import_node_path17.default.dirname(out), { recursive: true });
12395
+ await (0, import_promises9.writeFile)(out, png);
12396
+ return {
12397
+ jobId,
12398
+ out,
12399
+ original: { width, height },
12400
+ pixelated: { width: size?.width ?? Number(first.width), height: size?.height ?? Number(first.height) },
12401
+ imageId: submitted.image_id,
12402
+ usage: job.usage ?? null
12403
+ };
12404
+ }
12405
+ if (Date.now() - started > timeout) {
12406
+ throw new Error(
12407
+ `PixelLab pixelate job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
12408
+ );
12409
+ }
12410
+ await sleep3(interval);
12411
+ }
12412
+ }
12322
12413
 
12323
12414
  // src/pipeline/quality-regression.ts
12324
12415
  var import_node_path18 = __toESM(require("path"), 1);
@@ -15398,6 +15489,12 @@ async function currentArt(entry, spec) {
15398
15489
  }
15399
15490
  return { path: file, contentType, width, height };
15400
15491
  }
15492
+ async function cachedFrame(url) {
15493
+ if (!url.startsWith("file://")) return null;
15494
+ const file = url.slice("file://".length);
15495
+ const contentType = mediaTypeFromExtension(file) ?? detectMediaType(await (0, import_promises18.readFile)(file).catch(() => Buffer.alloc(0))) ?? "application/octet-stream";
15496
+ return { path: file, contentType };
15497
+ }
15401
15498
  async function prepareReview(provider, lock, opts = {}) {
15402
15499
  const log = opts.onProgress ?? (() => {
15403
15500
  });
@@ -15407,6 +15504,7 @@ async function prepareReview(provider, lock, opts = {}) {
15407
15504
  );
15408
15505
  const reviewAssets = /* @__PURE__ */ new Map();
15409
15506
  const prefix = opts.routePrefix ?? "";
15507
+ const providerUrls = /* @__PURE__ */ new Map();
15410
15508
  const groups = [];
15411
15509
  for (const [key, entry] of Object.entries(lock.entries)) {
15412
15510
  if (entry.provider !== provider.id || selectedKeys && !selectedKeys.has(key)) continue;
@@ -15415,7 +15513,19 @@ async function prepareReview(provider, lock, opts = {}) {
15415
15513
  const spec = specByKey.get(key);
15416
15514
  const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec, metadata: entry.providerMetadata?.[provider.id] });
15417
15515
  if (state.status !== "review" && state.status !== "review-set" || (state.status === "review" ? !state.candidateUrls.length : !state.frameUrls.length)) continue;
15418
- const frameUrls = state.status === "review" ? state.candidateUrls : state.frameUrls;
15516
+ const urls = state.status === "review" ? state.candidateUrls : state.frameUrls;
15517
+ const frameUrls = [];
15518
+ const frameAssets = [];
15519
+ for (const [index, url] of urls.entries()) {
15520
+ const cached = await cachedFrame(url);
15521
+ if (!cached) {
15522
+ frameUrls.push(url);
15523
+ continue;
15524
+ }
15525
+ const route = `${prefix}/review-frame/${encodeURIComponent(String(groups.length))}/${index}`;
15526
+ frameAssets.push([route, cached]);
15527
+ frameUrls.push(route);
15528
+ }
15419
15529
  const sourceRoute = spec?.revision ? `${prefix}/revision-source/${encodeURIComponent(String(groups.length))}` : null;
15420
15530
  if (sourceRoute && spec?.revision) {
15421
15531
  reviewAssets.set(sourceRoute, {
@@ -15428,6 +15538,8 @@ async function prepareReview(provider, lock, opts = {}) {
15428
15538
  if (current && currentRoute) {
15429
15539
  reviewAssets.set(currentRoute, { path: current.path, contentType: current.contentType });
15430
15540
  }
15541
+ for (const [route, asset] of frameAssets) reviewAssets.set(route, asset);
15542
+ providerUrls.set(key, urls);
15431
15543
  groups.push({
15432
15544
  key,
15433
15545
  assetId: entry.assetId,
@@ -15510,7 +15622,7 @@ async function prepareReview(provider, lock, opts = {}) {
15510
15622
  status: "selected",
15511
15623
  objectId,
15512
15624
  candidateIndex: index,
15513
- sourceUrl: sourceUrl ?? group.frameUrls[index] ?? null,
15625
+ sourceUrl: sourceUrl ?? providerUrls.get(key)?.[index] ?? null,
15514
15626
  sourceUrls: [],
15515
15627
  provider: provider.id,
15516
15628
  providerMetadata: metadata ? {
@@ -19274,8 +19386,11 @@ async function workspaceStatus(ws, dir) {
19274
19386
  MapSceneSchema,
19275
19387
  MediaType,
19276
19388
  OverwriteRefusedError,
19389
+ PIXELATE_COST_GENERATIONS,
19390
+ PIXELATE_MAX_SIDE,
19277
19391
  PIXEL_ART_FIXER_REVISION,
19278
19392
  PIXEL_ART_FIXER_URL,
19393
+ PRO_TIER_PRICES,
19279
19394
  PixelKilnError,
19280
19395
  PixelLabClient,
19281
19396
  PixelLabError,
@@ -19331,6 +19446,7 @@ async function workspaceStatus(ws, dir) {
19331
19446
  currentEntryOutputPath,
19332
19447
  currentOutputPath,
19333
19448
  defaultOpenCommand,
19449
+ defaultPixelateOut,
19334
19450
  defaultUnzoomOut,
19335
19451
  detachHandEdit,
19336
19452
  detectMediaType,
@@ -19413,11 +19529,13 @@ async function workspaceStatus(ws, dir) {
19413
19529
  parseTerrainDescriptions,
19414
19530
  parseWorkspace,
19415
19531
  pickHistory,
19532
+ pixelateFile,
19416
19533
  pngSize,
19417
19534
  poll,
19418
19535
  portableOutputPath,
19419
19536
  prepareReview,
19420
19537
  primaryOutput,
19538
+ proTierCost,
19421
19539
  proportionsPayload,
19422
19540
  providerCredentialEnvs,
19423
19541
  providerFactory,