pixelkiln 0.56.0 → 0.58.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/dist/cli.js CHANGED
@@ -708,7 +708,16 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
708
708
  from: spec.revision.sourceAssetId,
709
709
  sourceSha256: spec.revision.sourceSha256,
710
710
  maskSha256: spec.revision.maskSha256,
711
- strength: spec.revision.strength
711
+ strength: spec.revision.strength,
712
+ numColors: spec.revision.numColors,
713
+ paletteImageSha256: spec.revision.paletteImageSha256,
714
+ dithering: spec.revision.dithering,
715
+ ditheringStrength: spec.revision.ditheringStrength,
716
+ frames: spec.revision.frames,
717
+ fps: spec.revision.fps,
718
+ lastFrameSha256: spec.revision.lastFrameSha256,
719
+ direction: spec.revision.direction,
720
+ enhancePrompt: spec.revision.enhancePrompt
712
721
  } : void 0
713
722
  })
714
723
  );
@@ -728,7 +737,15 @@ import { z } from "zod";
728
737
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
729
738
  var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro"]);
730
739
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
731
- var RevisionModeSchema = z.enum(["image-to-image", "inpaint", "outpaint"]);
740
+ var RevisionModeSchema = z.enum([
741
+ "image-to-image",
742
+ "inpaint",
743
+ "outpaint",
744
+ "reduce-colors",
745
+ "correct-pixelart",
746
+ "animate",
747
+ "animate-pixminimax"
748
+ ]);
732
749
  function tileVariationCount(descriptions) {
733
750
  return Math.max(1, descriptions) * 4;
734
751
  }
@@ -922,6 +939,7 @@ var CharacterAnimationSchema = z.object({
922
939
  context.addIssue({ code: z.ZodIssueCode.custom, message: "template mode needs a template", path: ["template"] });
923
940
  }
924
941
  });
942
+ var RevisionDitheringSchema = z.enum(["none", "2x2", "4x4", "8x8"]);
925
943
  var RevisionSchema = z.object({
926
944
  /** What kind of controlled change the provider workflow performs. */
927
945
  mode: RevisionModeSchema,
@@ -930,7 +948,29 @@ var RevisionSchema = z.object({
930
948
  /** Manifest-relative black/white PNG. Required only for masked inpainting. */
931
949
  mask: z.string().min(1).optional(),
932
950
  /** Provider-neutral edit strength. The active adapter must bind it explicitly. */
933
- strength: z.number().min(0).max(1).optional()
951
+ strength: z.number().min(0).max(1).optional(),
952
+ /** `reduce-colors` only: target color count. Mutually exclusive with `paletteImage`. */
953
+ numColors: z.number().int().min(2).max(256).optional(),
954
+ /** `reduce-colors` only: manifest-relative image whose colors become the palette. Mutually exclusive with `numColors`. */
955
+ paletteImage: z.string().min(1).optional(),
956
+ /** `reduce-colors` only: ordered dithering matrix size. */
957
+ dithering: RevisionDitheringSchema.optional(),
958
+ /** `reduce-colors` only: dithering intensity; ignored when `dithering` is "none" or unset. */
959
+ ditheringStrength: z.number().min(0).max(10).optional(),
960
+ /**
961
+ * `animate`/`animate-pixminimax` only: frames to generate, even, 4 to 40
962
+ * (the provider-specific ceiling — 16 for `animate` — is enforced at
963
+ * the provider layer, since it differs by mode).
964
+ */
965
+ frames: z.number().int().min(4).max(40).optional(),
966
+ /** `animate`/`animate-pixminimax` only: playback rate recorded with the frames; PixelLab does not store one. */
967
+ fps: z.number().int().min(1).max(60).optional(),
968
+ /** `animate`/`animate-pixminimax` only: manifest-relative image pinning where the motion ends (interpolation instead of open-ended animation). */
969
+ lastFrame: z.string().min(1).optional(),
970
+ /** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
971
+ direction: CharacterDirectionSchema.optional(),
972
+ /** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
973
+ enhancePrompt: z.boolean().optional()
934
974
  }).strict().superRefine((revision, context) => {
935
975
  if (revision.mode === "inpaint" && !revision.mask) {
936
976
  context.addIssue({
@@ -946,6 +986,55 @@ var RevisionSchema = z.object({
946
986
  path: ["mask"]
947
987
  });
948
988
  }
989
+ if (revision.numColors !== void 0 && revision.paletteImage !== void 0) {
990
+ context.addIssue({
991
+ code: z.ZodIssueCode.custom,
992
+ message: "numColors and paletteImage are mutually exclusive",
993
+ path: ["numColors"]
994
+ });
995
+ }
996
+ for (const field of ["numColors", "paletteImage", "dithering", "ditheringStrength"]) {
997
+ if (revision[field] !== void 0 && revision.mode !== "reduce-colors") {
998
+ context.addIssue({
999
+ code: z.ZodIssueCode.custom,
1000
+ message: `${field} applies to reduce-colors revisions only`,
1001
+ path: [field]
1002
+ });
1003
+ }
1004
+ }
1005
+ if (revision.strength !== void 0 && revision.mode === "reduce-colors") {
1006
+ context.addIssue({
1007
+ code: z.ZodIssueCode.custom,
1008
+ message: "reduce-colors revisions do not take a strength; use dithering/ditheringStrength",
1009
+ path: ["strength"]
1010
+ });
1011
+ }
1012
+ if (revision.strength !== void 0 && (revision.mode === "animate" || revision.mode === "animate-pixminimax")) {
1013
+ context.addIssue({
1014
+ code: z.ZodIssueCode.custom,
1015
+ message: `${revision.mode} revisions do not take a strength; use enhancePrompt`,
1016
+ path: ["strength"]
1017
+ });
1018
+ }
1019
+ for (const field of ["frames", "fps", "lastFrame", "enhancePrompt"]) {
1020
+ if (revision[field] !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
1021
+ context.addIssue({
1022
+ code: z.ZodIssueCode.custom,
1023
+ message: `${field} applies to animate/animate-pixminimax revisions only`,
1024
+ path: [field]
1025
+ });
1026
+ }
1027
+ }
1028
+ if (revision.direction !== void 0 && revision.mode !== "animate-pixminimax") {
1029
+ context.addIssue({
1030
+ code: z.ZodIssueCode.custom,
1031
+ message: "direction applies to animate-pixminimax revisions only",
1032
+ path: ["direction"]
1033
+ });
1034
+ }
1035
+ if (revision.frames !== void 0 && revision.frames % 2 !== 0) {
1036
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
1037
+ }
949
1038
  });
950
1039
  var StyleObjectSchema = z.object({
951
1040
  /** Generation backend for this style. Omit to inherit the manifest default. */
@@ -1552,7 +1641,16 @@ var LockEntrySchema = z.object({
1552
1641
  sourceAssetId: z.string().min(1),
1553
1642
  sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
1554
1643
  maskSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1555
- strength: z.number().min(0).max(1).optional()
1644
+ strength: z.number().min(0).max(1).optional(),
1645
+ numColors: z.number().int().min(2).max(256).optional(),
1646
+ paletteImageSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1647
+ dithering: RevisionDitheringSchema.optional(),
1648
+ ditheringStrength: z.number().min(0).max(10).optional(),
1649
+ frames: z.number().int().min(4).max(40).optional(),
1650
+ fps: z.number().int().min(1).max(60).optional(),
1651
+ lastFrameSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1652
+ direction: CharacterDirectionSchema.optional(),
1653
+ enhancePrompt: z.boolean().optional()
1556
1654
  }).strict().nullable().default(null),
1557
1655
  /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
1558
1656
  mirror: z.object({
@@ -4627,8 +4725,16 @@ var ComfyUIProvider = class _ComfyUIProvider {
4627
4725
  supports(generator) {
4628
4726
  return generator === "map" || generator === "frames";
4629
4727
  }
4630
- supportsRevision(_mode) {
4631
- return true;
4728
+ /**
4729
+ * Everything else resolves generically to whatever the user's own
4730
+ * workflow does with `bindings.sourceImage`. `animate`/`animate-pixminimax`
4731
+ * are the one exception: they produce an ordered frame set, and this
4732
+ * adapter's revision path always writes a single output image (see
4733
+ * `submit`/`fetch` below) — a real structural gap, not a missing binding,
4734
+ * so it is rejected here rather than only failing once `validate` runs.
4735
+ */
4736
+ supportsRevision(mode2) {
4737
+ return mode2 !== "animate" && mode2 !== "animate-pixminimax";
4632
4738
  }
4633
4739
  estimate(spec) {
4634
4740
  return {
@@ -4690,6 +4796,11 @@ var ComfyUIProvider = class _ComfyUIProvider {
4690
4796
  if (spec.revision.strength != null && !options.bindings.strength) {
4691
4797
  throw new Error("ComfyUI revision strength requires bindings.strength");
4692
4798
  }
4799
+ if (spec.revision.numColors != null || spec.revision.paletteImageFile || spec.revision.dithering) {
4800
+ throw new Error(
4801
+ "ComfyUI has no binding for numColors/paletteImage/dithering \u2014 those are PixelLab's /reduce-colors parameters. A reduce-colors revision on ComfyUI is whatever your own workflow does with bindings.sourceImage; write that behavior into the graph directly."
4802
+ );
4803
+ }
4693
4804
  if ((!options.bindings.width || !options.bindings.height) && spec.revision.sourceWidth != null && spec.revision.sourceHeight != null && (spec.width !== spec.revision.sourceWidth || spec.height !== spec.revision.sourceHeight)) {
4694
4805
  throw new Error(
4695
4806
  "ComfyUI revisions without width/height bindings must keep the source dimensions"
@@ -5388,6 +5499,16 @@ var MapObjectSchema = z2.object({
5388
5499
  }).passthrough();
5389
5500
  var ObjectListSchema = z2.object({ objects: z2.array(PixelLabObjectSchema), total: z2.number().int().min(0) }).passthrough();
5390
5501
  var PixfluxResponseSchema = z2.object({ image: z2.object({ base64: z2.string().min(1) }).passthrough(), usage: z2.unknown().optional() }).passthrough();
5502
+ var ReduceColorsResponseSchema = z2.object({
5503
+ images: z2.array(z2.object({ base64: z2.string().min(1) }).passthrough()).min(1),
5504
+ palette: z2.object({ base64: z2.string().min(1) }).passthrough(),
5505
+ n_colors: z2.number().int(),
5506
+ usage: z2.unknown().optional()
5507
+ }).passthrough();
5508
+ var CorrectPixelartResponseSchema = z2.object({
5509
+ images: z2.array(z2.object({ base64: z2.string().min(1) }).passthrough()).min(1),
5510
+ usage: z2.unknown().optional()
5511
+ }).passthrough();
5391
5512
  var SelectFramesSchema = z2.object({ created_object_ids: z2.array(z2.string()) }).passthrough();
5392
5513
  var CharacterSubmitSchema = z2.object({
5393
5514
  background_job_id: z2.string().min(1),
@@ -6095,6 +6216,96 @@ var PixelLabClient = class {
6095
6216
  "generate-image-v2"
6096
6217
  );
6097
6218
  }
6219
+ /**
6220
+ * `/reduce-colors`, PixelLab's "Cleanup" tier: quantize one image onto a
6221
+ * smaller palette, synchronously — no `background_job_id`, the result
6222
+ * comes back in this same response, like `createImagePixflux`. Verified
6223
+ * against the live OpenAPI schema, not exercised against a live account:
6224
+ * the schema's own response example is `usage: {type: "usd", usd: 0.02}`,
6225
+ * which — going by this codebase's own repeated experience with PixelLab's
6226
+ * documented-vs-billed cost mismatches (`isometricTile`, `objectPro`) —
6227
+ * should not be trusted over a real call. `numColors` and `paletteImage`
6228
+ * are mutually exclusive upstream; the manifest schema already enforces
6229
+ * that before this is ever called.
6230
+ */
6231
+ async reduceColors(args) {
6232
+ const body = { images: [args.image] };
6233
+ if (args.numColors != null) body.num_colors = args.numColors;
6234
+ if (args.paletteImage) body.palette_image = args.paletteImage;
6235
+ if (args.dithering) body.dithering = args.dithering;
6236
+ if (args.ditheringStrength != null) body.dithering_strength = args.ditheringStrength;
6237
+ const res = validateResponse(
6238
+ ReduceColorsResponseSchema,
6239
+ await this.request("/reduce-colors", { method: "POST", body: JSON.stringify(body) }),
6240
+ "reduce-colors"
6241
+ );
6242
+ return {
6243
+ png: Buffer.from(res.images[0].base64, "base64"),
6244
+ paletteStripPng: Buffer.from(res.palette.base64, "base64"),
6245
+ nColors: res.n_colors,
6246
+ usage: res.usage
6247
+ };
6248
+ }
6249
+ /**
6250
+ * `/correct-pixelart`, PixelLab's "Cleanup" tier: sharpen edges and drop
6251
+ * stray pixels without resizing, synchronously — same shape as
6252
+ * `reduceColors` above, no background job. Cost is likewise unverified
6253
+ * against a live account (schema example: `usage: {type: "usd", usd: 0.02}`).
6254
+ */
6255
+ async correctPixelart(args) {
6256
+ const body = { images: [args.image] };
6257
+ if (args.strength != null) body.strength = args.strength;
6258
+ const res = validateResponse(
6259
+ CorrectPixelartResponseSchema,
6260
+ await this.request("/correct-pixelart", { method: "POST", body: JSON.stringify(body) }),
6261
+ "correct-pixelart"
6262
+ );
6263
+ return { png: Buffer.from(res.images[0].base64, "base64"), usage: res.usage };
6264
+ }
6265
+ /**
6266
+ * `/animate-with-text-v3`: animate a loose image from a text description,
6267
+ * no PixelLab character/object resource required — unlike `animateCharacter`
6268
+ * / `animateObject`, `firstFrame` is whatever bytes the caller has on hand.
6269
+ * A plain background job, like every other PixelLab async submission;
6270
+ * unlike `inpaintV3`/`editImagesV2`, its completed shape has not been
6271
+ * exercised against a live account (request/response fields here come from
6272
+ * the live OpenAPI document, not an observed call — `pollAnimateRevision`
6273
+ * in pixellab.ts checks several plausible field names for the frame list
6274
+ * defensively, the same as `pollRevision` already does for image edits).
6275
+ */
6276
+ async animateWithTextV3(args) {
6277
+ const body = { first_frame: args.firstFrame, action: args.action };
6278
+ if (args.lastFrame) body.last_frame = args.lastFrame;
6279
+ if (args.frameCount != null) body.frame_count = args.frameCount;
6280
+ if (args.seed != null) body.seed = args.seed;
6281
+ if (args.noBackground != null) body.no_background = args.noBackground;
6282
+ if (args.enhancePrompt != null) body.enhance_prompt = args.enhancePrompt;
6283
+ return validateResponse(
6284
+ RevisionJobSubmitSchema,
6285
+ await this.request("/animate-with-text-v3", { method: "POST", body: JSON.stringify(body) }),
6286
+ "animate-with-text-v3"
6287
+ );
6288
+ }
6289
+ /**
6290
+ * `/animate-pixminimax`, beta (tier 1 subscription or higher): PixMiniMax's
6291
+ * richer take on the same idea — `direction` and `enhancePrompt` steer
6292
+ * facing for aimed motions. Same unverified-completed-shape caveat as
6293
+ * `animateWithTextV3` above.
6294
+ */
6295
+ async animatePixminimax(args) {
6296
+ const body = { first_frame: args.firstFrame, description: args.description };
6297
+ if (args.lastFrame) body.last_frame = args.lastFrame;
6298
+ if (args.frameCount != null) body.frame_count = args.frameCount;
6299
+ if (args.seed != null) body.seed = args.seed;
6300
+ if (args.noBackground != null) body.no_background = args.noBackground;
6301
+ if (args.enhancePrompt != null) body.enhance_prompt = args.enhancePrompt;
6302
+ if (args.direction) body.direction = args.direction;
6303
+ return validateResponse(
6304
+ RevisionJobSubmitSchema,
6305
+ await this.request("/animate-pixminimax", { method: "POST", body: JSON.stringify(body) }),
6306
+ "animate-pixminimax"
6307
+ );
6308
+ }
6098
6309
  async getBackgroundJob(jobId) {
6099
6310
  const raw = await this.request(`/background-jobs/${encodeURIComponent(jobId)}`);
6100
6311
  return validateResponse(BackgroundJobSchema, raw, "background-jobs/{id}");
@@ -6274,12 +6485,18 @@ var PixelLabProvider = class _PixelLabProvider {
6274
6485
  }
6275
6486
  /**
6276
6487
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
6277
- * no mask) both exist on PixelLab. `outpaint` does not: there is no
6278
- * canvas-expansion endpoint in the API, matching docs/REVISIONS.md's note
6279
- * that no provider ships a tested outpaint path yet.
6488
+ * no mask) both exist on PixelLab, and so do `reduce-colors`
6489
+ * (`/reduce-colors`) and `correct-pixelart` (`/correct-pixelart`) — PixelLab's
6490
+ * "Cleanup" tier, a mechanical post-process on the source's own pixels
6491
+ * rather than a described edit — and `animate`/`animate-pixminimax`
6492
+ * (`/animate-with-text-v3`, `/animate-pixminimax`), which animate any loose
6493
+ * image from a text description with no character/object resource
6494
+ * required. `outpaint` does not: there is no canvas-expansion endpoint in
6495
+ * the API, matching docs/REVISIONS.md's note that no provider ships a
6496
+ * tested outpaint path yet.
6280
6497
  */
6281
6498
  supportsRevision(mode2) {
6282
- return mode2 === "inpaint" || mode2 === "image-to-image";
6499
+ return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax";
6283
6500
  }
6284
6501
  /** PixelLab's own constraints: submissions must be >2s apart, and
6285
6502
  * background jobs in flight are capped by subscription tier (Tier 1=8,
@@ -6301,6 +6518,16 @@ var PixelLabProvider = class _PixelLabProvider {
6301
6518
  return dir;
6302
6519
  }
6303
6520
  estimate(spec) {
6521
+ if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
6522
+ return { unit: "generations", amount: 0.1, candidates: 1 };
6523
+ }
6524
+ if (spec.revision?.mode === "animate" || spec.revision?.mode === "animate-pixminimax") {
6525
+ const width = spec.revision.sourceWidth ?? spec.width;
6526
+ const height = spec.revision.sourceHeight ?? spec.height;
6527
+ const frames = spec.revision.frames ?? 8;
6528
+ const base = Math.max(1, Math.ceil(width * height * frames / 65536));
6529
+ return { unit: "generations", amount: spec.revision.enhancePrompt ? base + 0.05 : base, candidates: 1 };
6530
+ }
6304
6531
  if (spec.revision) {
6305
6532
  const width = spec.revision.sourceWidth ?? spec.width;
6306
6533
  const height = spec.revision.sourceHeight ?? spec.height;
@@ -6328,6 +6555,34 @@ var PixelLabProvider = class _PixelLabProvider {
6328
6555
  throw new Error(`PixelLab inpaint source is ${width}x${height}; the API takes 32 to 512 pixels per side`);
6329
6556
  }
6330
6557
  }
6558
+ if (spec.revision?.mode === "reduce-colors") {
6559
+ const { sourceWidth: width, sourceHeight: height } = spec.revision;
6560
+ if (width != null && height != null && width * height > 512 * 512) {
6561
+ throw new Error(
6562
+ `PixelLab reduce-colors source is ${width}x${height} (${width * height}px\xB2); the API takes at most 512x512 worth of pixels per call`
6563
+ );
6564
+ }
6565
+ }
6566
+ if (spec.revision?.mode === "correct-pixelart") {
6567
+ const { sourceWidth: width, sourceHeight: height } = spec.revision;
6568
+ if (width != null && height != null && (width > 1024 || height > 1024)) {
6569
+ throw new Error(`PixelLab correct-pixelart source is ${width}x${height}; the API takes at most 1024 pixels per side`);
6570
+ }
6571
+ }
6572
+ if (spec.revision?.mode === "animate" || spec.revision?.mode === "animate-pixminimax") {
6573
+ const { sourceWidth: width, sourceHeight: height, mode: mode2, frames, lastFrameWidth, lastFrameHeight } = spec.revision;
6574
+ if (width != null && height != null && (width > 256 || height > 256)) {
6575
+ throw new Error(`PixelLab ${mode2} source is ${width}x${height}; the API takes at most 256 pixels per side`);
6576
+ }
6577
+ if (mode2 === "animate" && frames != null && frames > 16) {
6578
+ throw new Error(`PixelLab animate takes 4 to 16 frames; ${frames} is too many (use animate-pixminimax for up to 40)`);
6579
+ }
6580
+ if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
6581
+ throw new Error(
6582
+ `PixelLab ${mode2} lastFrame is ${lastFrameWidth}x${lastFrameHeight}; source is ${width}x${height} \u2014 they must match`
6583
+ );
6584
+ }
6585
+ }
6331
6586
  if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
6332
6587
  throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
6333
6588
  }
@@ -7120,6 +7375,71 @@ var PixelLabProvider = class _PixelLabProvider {
7120
7375
  });
7121
7376
  return { jobId: res2.background_job_id };
7122
7377
  }
7378
+ if (revision.mode === "reduce-colors") {
7379
+ let paletteImage;
7380
+ if (revision.paletteImageFile) {
7381
+ if (!revision.paletteImageSha256 || !revision.paletteImageFormat) {
7382
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision palette image is not ready`);
7383
+ }
7384
+ const paletteBytes = readFileSync2(revision.paletteImageFile);
7385
+ if (sha256(paletteBytes) !== revision.paletteImageSha256) {
7386
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision palette image changed after the manifest was resolved`);
7387
+ }
7388
+ paletteImage = { base64: paletteBytes.toString("base64"), format: revision.paletteImageFormat };
7389
+ }
7390
+ const res2 = await this.client.reduceColors({
7391
+ image,
7392
+ numColors: revision.numColors,
7393
+ paletteImage,
7394
+ dithering: revision.dithering,
7395
+ ditheringStrength: revision.ditheringStrength
7396
+ });
7397
+ const jobId = randomUUID3();
7398
+ writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), res2.png);
7399
+ return { jobId, metadata: { revisionUsage: res2.usage } };
7400
+ }
7401
+ if (revision.mode === "correct-pixelart") {
7402
+ const res2 = await this.client.correctPixelart({ image, strength: revision.strength });
7403
+ const jobId = randomUUID3();
7404
+ writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), res2.png);
7405
+ return { jobId, metadata: { revisionUsage: res2.usage } };
7406
+ }
7407
+ if (revision.mode === "animate" || revision.mode === "animate-pixminimax") {
7408
+ let lastFrame;
7409
+ if (revision.lastFrameFile) {
7410
+ if (!revision.lastFrameSha256 || !revision.lastFrameFormat) {
7411
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame is not ready`);
7412
+ }
7413
+ const lastFrameBytes = readFileSync2(revision.lastFrameFile);
7414
+ if (sha256(lastFrameBytes) !== revision.lastFrameSha256) {
7415
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame changed after the manifest was resolved`);
7416
+ }
7417
+ lastFrame = { base64: lastFrameBytes.toString("base64"), format: revision.lastFrameFormat };
7418
+ }
7419
+ if (revision.mode === "animate") {
7420
+ const res3 = await this.client.animateWithTextV3({
7421
+ firstFrame: image,
7422
+ lastFrame,
7423
+ action: spec.prompt,
7424
+ frameCount: revision.frames,
7425
+ seed: spec.seed,
7426
+ noBackground: spec.noBackground,
7427
+ enhancePrompt: revision.enhancePrompt
7428
+ });
7429
+ return { jobId: res3.background_job_id };
7430
+ }
7431
+ const res2 = await this.client.animatePixminimax({
7432
+ firstFrame: image,
7433
+ lastFrame,
7434
+ description: spec.prompt,
7435
+ frameCount: revision.frames,
7436
+ seed: spec.seed,
7437
+ noBackground: spec.noBackground,
7438
+ enhancePrompt: revision.enhancePrompt,
7439
+ direction: revision.direction
7440
+ });
7441
+ return { jobId: res2.background_job_id };
7442
+ }
7123
7443
  if (revision.strength != null) {
7124
7444
  throw new Error(
7125
7445
  `${spec.styleId}/${spec.assetId}: PixelLab image-to-image revisions take no strength; edit-images-v2 always applies the full instruction`
@@ -7151,6 +7471,13 @@ var PixelLabProvider = class _PixelLabProvider {
7151
7471
  }
7152
7472
  }
7153
7473
  async poll(jobId, generator, context) {
7474
+ const revisionMode = context?.spec?.revision?.mode;
7475
+ if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
7476
+ return this.pollCachedRevision(jobId, context);
7477
+ }
7478
+ if (revisionMode === "animate" || revisionMode === "animate-pixminimax") {
7479
+ return this.pollAnimateRevision(jobId, context);
7480
+ }
7154
7481
  if (context?.spec?.revision) return this.pollRevision(jobId);
7155
7482
  if (generator === "pixflux") {
7156
7483
  const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
@@ -7186,6 +7513,92 @@ var PixelLabProvider = class _PixelLabProvider {
7186
7513
  etaSeconds: obj.eta_seconds ?? null
7187
7514
  };
7188
7515
  }
7516
+ /**
7517
+ * `reduce-colors` and `correct-pixelart` results are already on disk by
7518
+ * the time `poll` is first called — `submitRevision` wrote them
7519
+ * synchronously, the same way `pixflux`'s poll branch above checks its own
7520
+ * cache file rather than a provider resource. The billed amount, if any,
7521
+ * travels through `context.metadata.revisionUsage` since there is no
7522
+ * background job to ask afterwards the way `billedForJob` does for async
7523
+ * work.
7524
+ */
7525
+ async pollCachedRevision(jobId, context) {
7526
+ const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
7527
+ if (!existsSync7(file)) {
7528
+ return {
7529
+ status: "failed",
7530
+ error: "revision result is no longer cached locally; re-run submit for this asset"
7531
+ };
7532
+ }
7533
+ const sourceUrl = `file://${file}`;
7534
+ const usage = context?.metadata?.revisionUsage;
7535
+ return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
7536
+ }
7537
+ /**
7538
+ * `animate`/`animate-pixminimax` revisions: a plain background job, like
7539
+ * `pollRevision` above, but completing with an ORDERED FRAME LIST instead
7540
+ * of a single image — the same `review-set` shape `pollCharacterAnimation`
7541
+ * returns, minus any character/object resource to look the result up on
7542
+ * (there is none; this operates on a loose image). Unlike every other
7543
+ * PixelLab call this adapter makes, the completed shape for
7544
+ * `/animate-with-text-v3`/`/animate-pixminimax` has never been exercised
7545
+ * against a live account — the checks below are an informed guess (hosted
7546
+ * URLs under a plausible key name, the same `storage_urls.frames` shape
7547
+ * character animations use, or inline base64 per frame, decoded to a local
7548
+ * cache file the same way `pollRevision`'s single-image case already is)
7549
+ * rather than an observed shape, and fail loudly, naming the keys actually
7550
+ * received, for whatever the real shape turns out to be.
7551
+ */
7552
+ async pollAnimateRevision(jobId, context) {
7553
+ const job = await this.client.getBackgroundJob(jobId);
7554
+ if (job.status === "failed") return { status: "failed", error: "animation job failed upstream" };
7555
+ if (job.status !== "completed") return { status: "processing" };
7556
+ const billed = billedFromUsage(job.usage);
7557
+ const done = job.last_response ?? {};
7558
+ const fps = context?.spec?.revision?.fps ?? 8;
7559
+ const review = (frameUrls) => ({
7560
+ status: "review-set",
7561
+ objectId: jobId,
7562
+ frameUrls,
7563
+ sources: frameUrls.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` })),
7564
+ fps,
7565
+ metadata: { frameSet: { fps, count: frameUrls.length } },
7566
+ billed
7567
+ });
7568
+ for (const key of ["frame_urls", "frames", "images"]) {
7569
+ const list = done[key];
7570
+ if (!Array.isArray(list) || !list.length) continue;
7571
+ const urls = [];
7572
+ for (const [index, item] of list.entries()) {
7573
+ if (typeof item === "string" && item) {
7574
+ urls.push(item);
7575
+ continue;
7576
+ }
7577
+ const url = item?.url;
7578
+ if (typeof url === "string" && url) {
7579
+ urls.push(url);
7580
+ continue;
7581
+ }
7582
+ const base64 = extractBase64(item);
7583
+ if (base64) {
7584
+ const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
7585
+ writeFileSync(file, Buffer.from(base64, "base64"));
7586
+ urls.push(`file://${file}`);
7587
+ continue;
7588
+ }
7589
+ break;
7590
+ }
7591
+ if (urls.length === list.length) return review(urls);
7592
+ }
7593
+ const storageFrames = done.storage_urls?.frames;
7594
+ if (Array.isArray(storageFrames) && storageFrames.length && storageFrames.every((f) => typeof f === "string")) {
7595
+ return review(storageFrames);
7596
+ }
7597
+ return {
7598
+ status: "failed",
7599
+ error: `Invalid PixelLab response for animate job ${jobId}: completed with no recognized frame list (got: ${Object.keys(done).join(", ") || "no keys"}); update pollAnimateRevision in src/providers/pixellab.ts with the real shape`
7600
+ };
7601
+ }
7189
7602
  /**
7190
7603
  * `/inpaint-v3` and `/edit-images-v2` both hand back a plain background
7191
7604
  * job with no resource of its own, polled generically at
@@ -9040,6 +9453,10 @@ async function resolveSpecs(loaded, filter) {
9040
9453
  `Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
9041
9454
  );
9042
9455
  }
9456
+ const paletteImageFile = asset.revision.paletteImage ? path13.resolve(root, asset.revision.paletteImage) : void 0;
9457
+ const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
9458
+ const lastFrameFile = asset.revision.lastFrame ? path13.resolve(root, asset.revision.lastFrame) : void 0;
9459
+ const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
9043
9460
  resolved.revision = {
9044
9461
  mode: asset.revision.mode,
9045
9462
  sourceAssetId: asset.revision.from,
@@ -9056,7 +9473,28 @@ async function resolveSpecs(loaded, filter) {
9056
9473
  maskHeight: maskImage?.height ?? null,
9057
9474
  maskFormat: maskImage?.format ?? null
9058
9475
  } : {},
9059
- ...asset.revision.strength == null ? {} : { strength: asset.revision.strength }
9476
+ ...asset.revision.strength == null ? {} : { strength: asset.revision.strength },
9477
+ ...asset.revision.numColors == null ? {} : { numColors: asset.revision.numColors },
9478
+ ...paletteImageFile ? {
9479
+ paletteImageFile,
9480
+ paletteImageSha256: paletteImage?.hash ?? null,
9481
+ paletteImageWidth: paletteImage?.width ?? null,
9482
+ paletteImageHeight: paletteImage?.height ?? null,
9483
+ paletteImageFormat: paletteImage?.format ?? null
9484
+ } : {},
9485
+ ...asset.revision.dithering ? { dithering: asset.revision.dithering } : {},
9486
+ ...asset.revision.ditheringStrength == null ? {} : { ditheringStrength: asset.revision.ditheringStrength },
9487
+ ...asset.revision.frames == null ? {} : { frames: asset.revision.frames },
9488
+ ...asset.revision.fps == null ? {} : { fps: asset.revision.fps },
9489
+ ...lastFrameFile ? {
9490
+ lastFrameFile,
9491
+ lastFrameSha256: lastFrame?.hash ?? null,
9492
+ lastFrameWidth: lastFrame?.width ?? null,
9493
+ lastFrameHeight: lastFrame?.height ?? null,
9494
+ lastFrameFormat: lastFrame?.format ?? null
9495
+ } : {},
9496
+ ...asset.revision.direction ? { direction: asset.revision.direction } : {},
9497
+ ...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt }
9060
9498
  };
9061
9499
  }
9062
9500
  resolved.specHash = specHash(
@@ -13412,7 +13850,16 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13412
13850
  sourceAssetId: spec.revision.sourceAssetId,
13413
13851
  sourceSha256: spec.revision.sourceSha256,
13414
13852
  ...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
13415
- ...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
13853
+ ...spec.revision.strength == null ? {} : { strength: spec.revision.strength },
13854
+ ...spec.revision.numColors == null ? {} : { numColors: spec.revision.numColors },
13855
+ ...spec.revision.paletteImageSha256 ? { paletteImageSha256: spec.revision.paletteImageSha256 } : {},
13856
+ ...spec.revision.dithering ? { dithering: spec.revision.dithering } : {},
13857
+ ...spec.revision.ditheringStrength == null ? {} : { ditheringStrength: spec.revision.ditheringStrength },
13858
+ ...spec.revision.frames == null ? {} : { frames: spec.revision.frames },
13859
+ ...spec.revision.fps == null ? {} : { fps: spec.revision.fps },
13860
+ ...spec.revision.lastFrameSha256 ? { lastFrameSha256: spec.revision.lastFrameSha256 } : {},
13861
+ ...spec.revision.direction ? { direction: spec.revision.direction } : {},
13862
+ ...spec.revision.enhancePrompt == null ? {} : { enhancePrompt: spec.revision.enhancePrompt }
13416
13863
  } : null,
13417
13864
  status: "pending",
13418
13865
  jobId: previousJobId ?? null,