pixelkiln 0.48.0 → 0.49.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 +199 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +199 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +199 -3
- package/dist/index.js.map +1 -1
- package/docs/ENDPOINTS.md +8 -3
- package/docs/PIXELLAB.md +3 -1
- package/docs/REVISIONS.md +31 -2
- package/package.json +1 -1
- package/skills/pixelkiln/SKILL.md +4 -0
- package/skills/pixelkiln/references/pixellab.md +72 -6
package/dist/cli.js
CHANGED
|
@@ -5125,6 +5125,10 @@ var TilesProSchema = z2.object({
|
|
|
5125
5125
|
kind: z2.string().nullable().default(null),
|
|
5126
5126
|
tile_rules: z2.record(z2.unknown()).nullable().optional()
|
|
5127
5127
|
}).passthrough();
|
|
5128
|
+
var RevisionJobSubmitSchema = z2.object({
|
|
5129
|
+
background_job_id: z2.string().min(1),
|
|
5130
|
+
status: z2.string().default("processing")
|
|
5131
|
+
}).passthrough();
|
|
5128
5132
|
var PixelLabObjectSchema = z2.object({
|
|
5129
5133
|
id: z2.string().min(1),
|
|
5130
5134
|
name: z2.string().nullable().default(null),
|
|
@@ -5596,6 +5600,59 @@ var PixelLabClient = class {
|
|
|
5596
5600
|
if (page.characters.length === 0 || offset >= page.total) return;
|
|
5597
5601
|
}
|
|
5598
5602
|
}
|
|
5603
|
+
/**
|
|
5604
|
+
* Masked inpaint, PixelLab's `/inpaint-v3` (the endpoint its own docs list
|
|
5605
|
+
* first in the Inpaint section, its convention for "reach for this by
|
|
5606
|
+
* default"). Unlike the rest of this client, this method's shape is taken
|
|
5607
|
+
* from the OpenAPI spec, not exercised against a live account: the request
|
|
5608
|
+
* side is exact (`InpaintV3Request`), but a completed job's `last_response`
|
|
5609
|
+
* has no documented example for this endpoint (the spec's only worked
|
|
5610
|
+
* example is a character job's shape). `pollRevision` in pixellab.ts reads
|
|
5611
|
+
* it defensively and fails loudly on an unrecognized shape rather than
|
|
5612
|
+
* guessing.
|
|
5613
|
+
*
|
|
5614
|
+
* `crop_to_mask` defaults true upstream (confirmed in the schema): PixelLab
|
|
5615
|
+
* otherwise blends generated pixels outside the mask edge to "fit
|
|
5616
|
+
* naturally," which is the opposite of what a mask boundary is for.
|
|
5617
|
+
*/
|
|
5618
|
+
async inpaintV3(args) {
|
|
5619
|
+
const size = { width: args.width, height: args.height };
|
|
5620
|
+
const body = {
|
|
5621
|
+
description: args.description,
|
|
5622
|
+
inpainting_image: { image: args.image, size },
|
|
5623
|
+
mask_image: { image: args.maskImage, size }
|
|
5624
|
+
};
|
|
5625
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
5626
|
+
if (args.cropToMask != null) body.crop_to_mask = args.cropToMask;
|
|
5627
|
+
if (args.seed != null) body.seed = args.seed;
|
|
5628
|
+
return validateResponse(
|
|
5629
|
+
RevisionJobSubmitSchema,
|
|
5630
|
+
await this.request("/inpaint-v3", { method: "POST", body: JSON.stringify(body) }),
|
|
5631
|
+
"inpaint-v3"
|
|
5632
|
+
);
|
|
5633
|
+
}
|
|
5634
|
+
/**
|
|
5635
|
+
* Whole-image edit with no mask, PixelLab's `/edit-images-v2`. Same
|
|
5636
|
+
* not-yet-live-verified caveat as `inpaintV3` above. `edit_images` takes an
|
|
5637
|
+
* array (the endpoint supports editing several images with one
|
|
5638
|
+
* instruction); pixelkiln's `revision` model is one parent per child, so
|
|
5639
|
+
* this always sends exactly one.
|
|
5640
|
+
*/
|
|
5641
|
+
async editImagesV2(args) {
|
|
5642
|
+
const body = {
|
|
5643
|
+
method: "edit_with_text",
|
|
5644
|
+
description: args.description,
|
|
5645
|
+
edit_images: [{ image: args.image, width: args.width, height: args.height }],
|
|
5646
|
+
image_size: { width: args.width, height: args.height }
|
|
5647
|
+
};
|
|
5648
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
5649
|
+
if (args.seed != null) body.seed = args.seed;
|
|
5650
|
+
return validateResponse(
|
|
5651
|
+
RevisionJobSubmitSchema,
|
|
5652
|
+
await this.request("/edit-images-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
5653
|
+
"edit-images-v2"
|
|
5654
|
+
);
|
|
5655
|
+
}
|
|
5599
5656
|
async getBackgroundJob(jobId) {
|
|
5600
5657
|
const raw = await this.request(`/background-jobs/${encodeURIComponent(jobId)}`);
|
|
5601
5658
|
return validateResponse(BackgroundJobSchema, raw, "background-jobs/{id}");
|
|
@@ -5736,6 +5793,15 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5736
5793
|
supports(generator) {
|
|
5737
5794
|
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "character";
|
|
5738
5795
|
}
|
|
5796
|
+
/**
|
|
5797
|
+
* `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
|
|
5798
|
+
* no mask) both exist on PixelLab. `outpaint` does not: there is no
|
|
5799
|
+
* canvas-expansion endpoint in the API, matching docs/REVISIONS.md's note
|
|
5800
|
+
* that no provider ships a tested outpaint path yet.
|
|
5801
|
+
*/
|
|
5802
|
+
supportsRevision(mode2) {
|
|
5803
|
+
return mode2 === "inpaint" || mode2 === "image-to-image";
|
|
5804
|
+
}
|
|
5739
5805
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
5740
5806
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
5741
5807
|
* Tier 2=10, Tier 3=20); 8 is the safe floor across every tier. */
|
|
@@ -5756,6 +5822,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5756
5822
|
return dir;
|
|
5757
5823
|
}
|
|
5758
5824
|
estimate(spec) {
|
|
5825
|
+
if (spec.revision) {
|
|
5826
|
+
const width = spec.revision.sourceWidth ?? spec.width;
|
|
5827
|
+
const height = spec.revision.sourceHeight ?? spec.height;
|
|
5828
|
+
return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
|
|
5829
|
+
}
|
|
5759
5830
|
if (spec.generator === "tiles") {
|
|
5760
5831
|
return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
|
|
5761
5832
|
}
|
|
@@ -5769,6 +5840,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5769
5840
|
};
|
|
5770
5841
|
}
|
|
5771
5842
|
validate(spec, styleImages) {
|
|
5843
|
+
if (spec.revision?.mode === "inpaint") {
|
|
5844
|
+
const { sourceWidth: width, sourceHeight: height } = spec.revision;
|
|
5845
|
+
if (width != null && height != null && (width < 32 || height < 32 || width > 512 || height > 512)) {
|
|
5846
|
+
throw new Error(`PixelLab inpaint source is ${width}x${height}; the API takes 32 to 512 pixels per side`);
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5772
5849
|
if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
|
|
5773
5850
|
throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
|
|
5774
5851
|
}
|
|
@@ -6118,6 +6195,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6118
6195
|
}
|
|
6119
6196
|
async submit(spec, styleImages, context) {
|
|
6120
6197
|
this.validate(spec, styleImages);
|
|
6198
|
+
if (spec.revision) return this.submitRevision(spec);
|
|
6121
6199
|
if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
|
|
6122
6200
|
if (spec.generator === "pixflux") {
|
|
6123
6201
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
@@ -6169,6 +6247,60 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6169
6247
|
});
|
|
6170
6248
|
return { jobId: res.object_id, metadata: { backgroundJobId: res.background_job_id } };
|
|
6171
6249
|
}
|
|
6250
|
+
/**
|
|
6251
|
+
* `inpaint` reads the mask as PixelLab's own convention: white marks the
|
|
6252
|
+
* area to generate, black the area to preserve (`InpaintV3Request`'s own
|
|
6253
|
+
* field description). Unlike ComfyUI's revision graph, there is no
|
|
6254
|
+
* configurable side to this — document it as fixed rather than leave an
|
|
6255
|
+
* agent to guess, the way docs/REVISIONS.md tells a ComfyUI author to test
|
|
6256
|
+
* their own graph.
|
|
6257
|
+
*/
|
|
6258
|
+
async submitRevision(spec) {
|
|
6259
|
+
const revision = spec.revision;
|
|
6260
|
+
if (!revision.sourceSha256 || !revision.sourceFormat || revision.sourceWidth == null || revision.sourceHeight == null) {
|
|
6261
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
|
|
6262
|
+
}
|
|
6263
|
+
const sourceBytes = readFileSync2(revision.sourceFile);
|
|
6264
|
+
if (sha256(sourceBytes) !== revision.sourceSha256) {
|
|
6265
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
|
|
6266
|
+
}
|
|
6267
|
+
const image = { base64: sourceBytes.toString("base64"), format: revision.sourceFormat };
|
|
6268
|
+
const width = revision.sourceWidth;
|
|
6269
|
+
const height = revision.sourceHeight;
|
|
6270
|
+
if (revision.mode === "inpaint") {
|
|
6271
|
+
if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
|
|
6272
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
|
|
6273
|
+
}
|
|
6274
|
+
const maskBytes = readFileSync2(revision.maskFile);
|
|
6275
|
+
if (sha256(maskBytes) !== revision.maskSha256) {
|
|
6276
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
|
|
6277
|
+
}
|
|
6278
|
+
const res2 = await this.client.inpaintV3({
|
|
6279
|
+
description: spec.prompt,
|
|
6280
|
+
image,
|
|
6281
|
+
width,
|
|
6282
|
+
height,
|
|
6283
|
+
maskImage: { base64: maskBytes.toString("base64"), format: revision.maskFormat },
|
|
6284
|
+
noBackground: spec.noBackground,
|
|
6285
|
+
seed: spec.seed
|
|
6286
|
+
});
|
|
6287
|
+
return { jobId: res2.background_job_id };
|
|
6288
|
+
}
|
|
6289
|
+
if (revision.strength != null) {
|
|
6290
|
+
throw new Error(
|
|
6291
|
+
`${spec.styleId}/${spec.assetId}: PixelLab image-to-image revisions take no strength; edit-images-v2 always applies the full instruction`
|
|
6292
|
+
);
|
|
6293
|
+
}
|
|
6294
|
+
const res = await this.client.editImagesV2({
|
|
6295
|
+
description: spec.prompt,
|
|
6296
|
+
image,
|
|
6297
|
+
width,
|
|
6298
|
+
height,
|
|
6299
|
+
noBackground: spec.noBackground,
|
|
6300
|
+
seed: spec.seed
|
|
6301
|
+
});
|
|
6302
|
+
return { jobId: res.background_job_id };
|
|
6303
|
+
}
|
|
6172
6304
|
/**
|
|
6173
6305
|
* What a background job actually billed, once its own record is still
|
|
6174
6306
|
* around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
|
|
@@ -6185,6 +6317,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6185
6317
|
}
|
|
6186
6318
|
}
|
|
6187
6319
|
async poll(jobId, generator, context) {
|
|
6320
|
+
if (context?.spec?.revision) return this.pollRevision(jobId);
|
|
6188
6321
|
if (generator === "pixflux") {
|
|
6189
6322
|
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
6190
6323
|
if (existsSync7(file)) {
|
|
@@ -6215,6 +6348,58 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6215
6348
|
etaSeconds: obj.eta_seconds ?? null
|
|
6216
6349
|
};
|
|
6217
6350
|
}
|
|
6351
|
+
/**
|
|
6352
|
+
* `/inpaint-v3` and `/edit-images-v2` both hand back a plain background
|
|
6353
|
+
* job with no resource of its own, polled generically at
|
|
6354
|
+
* `GET /background-jobs/{id}`. What a *completed* job's `last_response`
|
|
6355
|
+
* actually contains is not documented for either endpoint: the OpenAPI
|
|
6356
|
+
* spec's only worked example of that field is a character job's shape
|
|
6357
|
+
* (`character_id`, `uploaded_directions`, ...), not an inpaint or edit
|
|
6358
|
+
* job's. This reads every image shape seen elsewhere in this client
|
|
6359
|
+
* (a nested `{image: {base64, format}}`, a bare `{base64, format}`, or a
|
|
6360
|
+
* hosted URL under a handful of plausible keys) and fails loudly, naming
|
|
6361
|
+
* the keys it actually got, rather than guess wrong silently. Fixing a
|
|
6362
|
+
* real completed response into this list is a one-line change once one is
|
|
6363
|
+
* seen live.
|
|
6364
|
+
*/
|
|
6365
|
+
async pollRevision(jobId) {
|
|
6366
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
6367
|
+
if (job.status === "failed") return { status: "failed", error: "revision job failed upstream" };
|
|
6368
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
6369
|
+
const billed = billedFromUsage(job.usage);
|
|
6370
|
+
const done = job.last_response ?? {};
|
|
6371
|
+
const urlKeys = ["image_url", "download_url", "url", "output_url"];
|
|
6372
|
+
for (const key of urlKeys) {
|
|
6373
|
+
const url = done[key];
|
|
6374
|
+
if (typeof url === "string" && url) {
|
|
6375
|
+
return { status: "ready", objectId: jobId, sourceUrl: url, sources: [{ url }], billed };
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6378
|
+
const imageKeys = ["image", "output_image", "result_image", "edited_image"];
|
|
6379
|
+
for (const key of imageKeys) {
|
|
6380
|
+
const candidate = done[key];
|
|
6381
|
+
const base64 = extractBase64(candidate);
|
|
6382
|
+
if (base64) {
|
|
6383
|
+
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
6384
|
+
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
6385
|
+
const sourceUrl = `file://${file}`;
|
|
6386
|
+
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
6387
|
+
}
|
|
6388
|
+
}
|
|
6389
|
+
if (Array.isArray(done.images) && done.images.length) {
|
|
6390
|
+
const base64 = extractBase64(done.images[0]);
|
|
6391
|
+
if (base64) {
|
|
6392
|
+
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
6393
|
+
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
6394
|
+
const sourceUrl = `file://${file}`;
|
|
6395
|
+
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
6396
|
+
}
|
|
6397
|
+
}
|
|
6398
|
+
return {
|
|
6399
|
+
status: "failed",
|
|
6400
|
+
error: `Invalid PixelLab response for revision job ${jobId}: completed with no recognized image field (got: ${Object.keys(done).join(", ") || "no keys"}); update pollRevision in src/providers/pixellab.ts with the real shape`
|
|
6401
|
+
};
|
|
6402
|
+
}
|
|
6218
6403
|
/**
|
|
6219
6404
|
* Map objects need their own path because the `/map-objects/{id}` record is
|
|
6220
6405
|
* deleted upstream roughly 8 hours after creation while the image survives in
|
|
@@ -6406,6 +6591,15 @@ function firstUrl(urls) {
|
|
|
6406
6591
|
if (!urls) return null;
|
|
6407
6592
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
6408
6593
|
}
|
|
6594
|
+
function extractBase64(value) {
|
|
6595
|
+
if (typeof value === "string" && value) return value;
|
|
6596
|
+
if (value && typeof value === "object") {
|
|
6597
|
+
const obj = value;
|
|
6598
|
+
if (typeof obj.base64 === "string" && obj.base64) return obj.base64;
|
|
6599
|
+
if (obj.image) return extractBase64(obj.image);
|
|
6600
|
+
}
|
|
6601
|
+
return null;
|
|
6602
|
+
}
|
|
6409
6603
|
function readReference(spec, reference) {
|
|
6410
6604
|
const images = {};
|
|
6411
6605
|
for (const [direction, image] of Object.entries(reference)) {
|
|
@@ -10926,7 +11120,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
10926
11120
|
retiredAt: generation.retiredAt,
|
|
10927
11121
|
outputs,
|
|
10928
11122
|
cached: outputs.length > 0 && outputs.every((output) => output.url !== null),
|
|
10929
|
-
|
|
11123
|
+
// A revision's jobId is a plain background job id, not a PixelLab
|
|
11124
|
+
// account object: there is no `/create-object` page to point at.
|
|
11125
|
+
upstreamUrl: generation.provider === "pixellab" && !entry.revision ? pixelLabObjectUrl(generation.generator, generation.objectId) : null
|
|
10930
11126
|
};
|
|
10931
11127
|
});
|
|
10932
11128
|
}
|
|
@@ -11191,7 +11387,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11191
11387
|
editChanged,
|
|
11192
11388
|
editStatus,
|
|
11193
11389
|
editMeta,
|
|
11194
|
-
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11390
|
+
upstreamUrl: entry?.provider === "pixellab" && !spec.revision ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11195
11391
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
11196
11392
|
history: entry ? describeHistory(media, entry, cacheDir) : [],
|
|
11197
11393
|
tags: spec.tags,
|
|
@@ -11255,7 +11451,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11255
11451
|
editChanged: [],
|
|
11256
11452
|
editStatus: null,
|
|
11257
11453
|
editMeta: null,
|
|
11258
|
-
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11454
|
+
upstreamUrl: entry.provider === "pixellab" && !entry.revision ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11259
11455
|
refreshable: false,
|
|
11260
11456
|
history: describeHistory(media, entry, cacheDir),
|
|
11261
11457
|
tags: [],
|