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 +459 -12
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +461 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +687 -65
- package/dist/index.d.ts +687 -65
- package/dist/index.js +460 -12
- package/dist/index.js.map +1 -1
- package/docs/ENDPOINTS.md +25 -7
- package/docs/MANIFEST.md +11 -2
- package/docs/REVISIONS.md +175 -9
- package/package.json +1 -1
- package/schema/manifest.schema.json +58 -1
- package/skills/pixelkiln/references/pixellab-roadmap.md +42 -35
- package/skills/pixelkiln/references/pixellab.md +18 -4
- package/skills/pixelkiln/references/revisions.md +30 -3
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([
|
|
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
|
-
|
|
4631
|
-
|
|
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
|
|
6278
|
-
*
|
|
6279
|
-
*
|
|
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,
|