pixelkiln 0.57.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
@@ -712,7 +712,12 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
712
712
  numColors: spec.revision.numColors,
713
713
  paletteImageSha256: spec.revision.paletteImageSha256,
714
714
  dithering: spec.revision.dithering,
715
- ditheringStrength: spec.revision.ditheringStrength
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
716
721
  } : void 0
717
722
  })
718
723
  );
@@ -732,7 +737,15 @@ import { z } from "zod";
732
737
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
733
738
  var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro"]);
734
739
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
735
- var RevisionModeSchema = z.enum(["image-to-image", "inpaint", "outpaint", "reduce-colors", "correct-pixelart"]);
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
+ ]);
736
749
  function tileVariationCount(descriptions) {
737
750
  return Math.max(1, descriptions) * 4;
738
751
  }
@@ -943,7 +956,21 @@ var RevisionSchema = z.object({
943
956
  /** `reduce-colors` only: ordered dithering matrix size. */
944
957
  dithering: RevisionDitheringSchema.optional(),
945
958
  /** `reduce-colors` only: dithering intensity; ignored when `dithering` is "none" or unset. */
946
- ditheringStrength: z.number().min(0).max(10).optional()
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()
947
974
  }).strict().superRefine((revision, context) => {
948
975
  if (revision.mode === "inpaint" && !revision.mask) {
949
976
  context.addIssue({
@@ -982,6 +1009,32 @@ var RevisionSchema = z.object({
982
1009
  path: ["strength"]
983
1010
  });
984
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
+ }
985
1038
  });
986
1039
  var StyleObjectSchema = z.object({
987
1040
  /** Generation backend for this style. Omit to inherit the manifest default. */
@@ -1592,7 +1645,12 @@ var LockEntrySchema = z.object({
1592
1645
  numColors: z.number().int().min(2).max(256).optional(),
1593
1646
  paletteImageSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1594
1647
  dithering: RevisionDitheringSchema.optional(),
1595
- ditheringStrength: z.number().min(0).max(10).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()
1596
1654
  }).strict().nullable().default(null),
1597
1655
  /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
1598
1656
  mirror: z.object({
@@ -4667,8 +4725,16 @@ var ComfyUIProvider = class _ComfyUIProvider {
4667
4725
  supports(generator) {
4668
4726
  return generator === "map" || generator === "frames";
4669
4727
  }
4670
- supportsRevision(_mode) {
4671
- 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";
4672
4738
  }
4673
4739
  estimate(spec) {
4674
4740
  return {
@@ -6196,6 +6262,50 @@ var PixelLabClient = class {
6196
6262
  );
6197
6263
  return { png: Buffer.from(res.images[0].base64, "base64"), usage: res.usage };
6198
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
+ }
6199
6309
  async getBackgroundJob(jobId) {
6200
6310
  const raw = await this.request(`/background-jobs/${encodeURIComponent(jobId)}`);
6201
6311
  return validateResponse(BackgroundJobSchema, raw, "background-jobs/{id}");
@@ -6378,12 +6488,15 @@ var PixelLabProvider = class _PixelLabProvider {
6378
6488
  * no mask) both exist on PixelLab, and so do `reduce-colors`
6379
6489
  * (`/reduce-colors`) and `correct-pixelart` (`/correct-pixelart`) — PixelLab's
6380
6490
  * "Cleanup" tier, a mechanical post-process on the source's own pixels
6381
- * rather than a described edit. `outpaint` does not: there is no
6382
- * canvas-expansion endpoint in the API, matching docs/REVISIONS.md's note
6383
- * that no provider ships a tested outpaint path yet.
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.
6384
6497
  */
6385
6498
  supportsRevision(mode2) {
6386
- return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart";
6499
+ return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax";
6387
6500
  }
6388
6501
  /** PixelLab's own constraints: submissions must be >2s apart, and
6389
6502
  * background jobs in flight are capped by subscription tier (Tier 1=8,
@@ -6408,6 +6521,13 @@ var PixelLabProvider = class _PixelLabProvider {
6408
6521
  if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
6409
6522
  return { unit: "generations", amount: 0.1, candidates: 1 };
6410
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
+ }
6411
6531
  if (spec.revision) {
6412
6532
  const width = spec.revision.sourceWidth ?? spec.width;
6413
6533
  const height = spec.revision.sourceHeight ?? spec.height;
@@ -6449,6 +6569,20 @@ var PixelLabProvider = class _PixelLabProvider {
6449
6569
  throw new Error(`PixelLab correct-pixelart source is ${width}x${height}; the API takes at most 1024 pixels per side`);
6450
6570
  }
6451
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
+ }
6452
6586
  if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
6453
6587
  throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
6454
6588
  }
@@ -7270,6 +7404,42 @@ var PixelLabProvider = class _PixelLabProvider {
7270
7404
  writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), res2.png);
7271
7405
  return { jobId, metadata: { revisionUsage: res2.usage } };
7272
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
+ }
7273
7443
  if (revision.strength != null) {
7274
7444
  throw new Error(
7275
7445
  `${spec.styleId}/${spec.assetId}: PixelLab image-to-image revisions take no strength; edit-images-v2 always applies the full instruction`
@@ -7305,6 +7475,9 @@ var PixelLabProvider = class _PixelLabProvider {
7305
7475
  if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
7306
7476
  return this.pollCachedRevision(jobId, context);
7307
7477
  }
7478
+ if (revisionMode === "animate" || revisionMode === "animate-pixminimax") {
7479
+ return this.pollAnimateRevision(jobId, context);
7480
+ }
7308
7481
  if (context?.spec?.revision) return this.pollRevision(jobId);
7309
7482
  if (generator === "pixflux") {
7310
7483
  const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
@@ -7361,6 +7534,71 @@ var PixelLabProvider = class _PixelLabProvider {
7361
7534
  const usage = context?.metadata?.revisionUsage;
7362
7535
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
7363
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
+ }
7364
7602
  /**
7365
7603
  * `/inpaint-v3` and `/edit-images-v2` both hand back a plain background
7366
7604
  * job with no resource of its own, polled generically at
@@ -9217,6 +9455,8 @@ async function resolveSpecs(loaded, filter) {
9217
9455
  }
9218
9456
  const paletteImageFile = asset.revision.paletteImage ? path13.resolve(root, asset.revision.paletteImage) : void 0;
9219
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;
9220
9460
  resolved.revision = {
9221
9461
  mode: asset.revision.mode,
9222
9462
  sourceAssetId: asset.revision.from,
@@ -9243,7 +9483,18 @@ async function resolveSpecs(loaded, filter) {
9243
9483
  paletteImageFormat: paletteImage?.format ?? null
9244
9484
  } : {},
9245
9485
  ...asset.revision.dithering ? { dithering: asset.revision.dithering } : {},
9246
- ...asset.revision.ditheringStrength == null ? {} : { ditheringStrength: asset.revision.ditheringStrength }
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 }
9247
9498
  };
9248
9499
  }
9249
9500
  resolved.specHash = specHash(
@@ -13603,7 +13854,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13603
13854
  ...spec.revision.numColors == null ? {} : { numColors: spec.revision.numColors },
13604
13855
  ...spec.revision.paletteImageSha256 ? { paletteImageSha256: spec.revision.paletteImageSha256 } : {},
13605
13856
  ...spec.revision.dithering ? { dithering: spec.revision.dithering } : {},
13606
- ...spec.revision.ditheringStrength == null ? {} : { ditheringStrength: spec.revision.ditheringStrength }
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 }
13607
13863
  } : null,
13608
13864
  status: "pending",
13609
13865
  jobId: previousJobId ?? null,