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/index.cjs
CHANGED
|
@@ -1243,6 +1243,10 @@ var TilesProSchema = import_zod2.z.object({
|
|
|
1243
1243
|
kind: import_zod2.z.string().nullable().default(null),
|
|
1244
1244
|
tile_rules: import_zod2.z.record(import_zod2.z.unknown()).nullable().optional()
|
|
1245
1245
|
}).passthrough();
|
|
1246
|
+
var RevisionJobSubmitSchema = import_zod2.z.object({
|
|
1247
|
+
background_job_id: import_zod2.z.string().min(1),
|
|
1248
|
+
status: import_zod2.z.string().default("processing")
|
|
1249
|
+
}).passthrough();
|
|
1246
1250
|
var PixelLabObjectSchema = import_zod2.z.object({
|
|
1247
1251
|
id: import_zod2.z.string().min(1),
|
|
1248
1252
|
name: import_zod2.z.string().nullable().default(null),
|
|
@@ -1714,6 +1718,59 @@ var PixelLabClient = class {
|
|
|
1714
1718
|
if (page.characters.length === 0 || offset >= page.total) return;
|
|
1715
1719
|
}
|
|
1716
1720
|
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Masked inpaint, PixelLab's `/inpaint-v3` (the endpoint its own docs list
|
|
1723
|
+
* first in the Inpaint section, its convention for "reach for this by
|
|
1724
|
+
* default"). Unlike the rest of this client, this method's shape is taken
|
|
1725
|
+
* from the OpenAPI spec, not exercised against a live account: the request
|
|
1726
|
+
* side is exact (`InpaintV3Request`), but a completed job's `last_response`
|
|
1727
|
+
* has no documented example for this endpoint (the spec's only worked
|
|
1728
|
+
* example is a character job's shape). `pollRevision` in pixellab.ts reads
|
|
1729
|
+
* it defensively and fails loudly on an unrecognized shape rather than
|
|
1730
|
+
* guessing.
|
|
1731
|
+
*
|
|
1732
|
+
* `crop_to_mask` defaults true upstream (confirmed in the schema): PixelLab
|
|
1733
|
+
* otherwise blends generated pixels outside the mask edge to "fit
|
|
1734
|
+
* naturally," which is the opposite of what a mask boundary is for.
|
|
1735
|
+
*/
|
|
1736
|
+
async inpaintV3(args) {
|
|
1737
|
+
const size = { width: args.width, height: args.height };
|
|
1738
|
+
const body = {
|
|
1739
|
+
description: args.description,
|
|
1740
|
+
inpainting_image: { image: args.image, size },
|
|
1741
|
+
mask_image: { image: args.maskImage, size }
|
|
1742
|
+
};
|
|
1743
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
1744
|
+
if (args.cropToMask != null) body.crop_to_mask = args.cropToMask;
|
|
1745
|
+
if (args.seed != null) body.seed = args.seed;
|
|
1746
|
+
return validateResponse(
|
|
1747
|
+
RevisionJobSubmitSchema,
|
|
1748
|
+
await this.request("/inpaint-v3", { method: "POST", body: JSON.stringify(body) }),
|
|
1749
|
+
"inpaint-v3"
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Whole-image edit with no mask, PixelLab's `/edit-images-v2`. Same
|
|
1754
|
+
* not-yet-live-verified caveat as `inpaintV3` above. `edit_images` takes an
|
|
1755
|
+
* array (the endpoint supports editing several images with one
|
|
1756
|
+
* instruction); pixelkiln's `revision` model is one parent per child, so
|
|
1757
|
+
* this always sends exactly one.
|
|
1758
|
+
*/
|
|
1759
|
+
async editImagesV2(args) {
|
|
1760
|
+
const body = {
|
|
1761
|
+
method: "edit_with_text",
|
|
1762
|
+
description: args.description,
|
|
1763
|
+
edit_images: [{ image: args.image, width: args.width, height: args.height }],
|
|
1764
|
+
image_size: { width: args.width, height: args.height }
|
|
1765
|
+
};
|
|
1766
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
1767
|
+
if (args.seed != null) body.seed = args.seed;
|
|
1768
|
+
return validateResponse(
|
|
1769
|
+
RevisionJobSubmitSchema,
|
|
1770
|
+
await this.request("/edit-images-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
1771
|
+
"edit-images-v2"
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1717
1774
|
async getBackgroundJob(jobId) {
|
|
1718
1775
|
const raw = await this.request(`/background-jobs/${encodeURIComponent(jobId)}`);
|
|
1719
1776
|
return validateResponse(BackgroundJobSchema, raw, "background-jobs/{id}");
|
|
@@ -2495,6 +2552,15 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2495
2552
|
supports(generator) {
|
|
2496
2553
|
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "character";
|
|
2497
2554
|
}
|
|
2555
|
+
/**
|
|
2556
|
+
* `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
|
|
2557
|
+
* no mask) both exist on PixelLab. `outpaint` does not: there is no
|
|
2558
|
+
* canvas-expansion endpoint in the API, matching docs/REVISIONS.md's note
|
|
2559
|
+
* that no provider ships a tested outpaint path yet.
|
|
2560
|
+
*/
|
|
2561
|
+
supportsRevision(mode2) {
|
|
2562
|
+
return mode2 === "inpaint" || mode2 === "image-to-image";
|
|
2563
|
+
}
|
|
2498
2564
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
2499
2565
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
2500
2566
|
* Tier 2=10, Tier 3=20); 8 is the safe floor across every tier. */
|
|
@@ -2515,6 +2581,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2515
2581
|
return dir;
|
|
2516
2582
|
}
|
|
2517
2583
|
estimate(spec) {
|
|
2584
|
+
if (spec.revision) {
|
|
2585
|
+
const width = spec.revision.sourceWidth ?? spec.width;
|
|
2586
|
+
const height = spec.revision.sourceHeight ?? spec.height;
|
|
2587
|
+
return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
|
|
2588
|
+
}
|
|
2518
2589
|
if (spec.generator === "tiles") {
|
|
2519
2590
|
return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
|
|
2520
2591
|
}
|
|
@@ -2528,6 +2599,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2528
2599
|
};
|
|
2529
2600
|
}
|
|
2530
2601
|
validate(spec, styleImages) {
|
|
2602
|
+
if (spec.revision?.mode === "inpaint") {
|
|
2603
|
+
const { sourceWidth: width, sourceHeight: height } = spec.revision;
|
|
2604
|
+
if (width != null && height != null && (width < 32 || height < 32 || width > 512 || height > 512)) {
|
|
2605
|
+
throw new Error(`PixelLab inpaint source is ${width}x${height}; the API takes 32 to 512 pixels per side`);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2531
2608
|
if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
|
|
2532
2609
|
throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
|
|
2533
2610
|
}
|
|
@@ -2877,6 +2954,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2877
2954
|
}
|
|
2878
2955
|
async submit(spec, styleImages, context) {
|
|
2879
2956
|
this.validate(spec, styleImages);
|
|
2957
|
+
if (spec.revision) return this.submitRevision(spec);
|
|
2880
2958
|
if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
|
|
2881
2959
|
if (spec.generator === "pixflux") {
|
|
2882
2960
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
@@ -2928,6 +3006,60 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2928
3006
|
});
|
|
2929
3007
|
return { jobId: res.object_id, metadata: { backgroundJobId: res.background_job_id } };
|
|
2930
3008
|
}
|
|
3009
|
+
/**
|
|
3010
|
+
* `inpaint` reads the mask as PixelLab's own convention: white marks the
|
|
3011
|
+
* area to generate, black the area to preserve (`InpaintV3Request`'s own
|
|
3012
|
+
* field description). Unlike ComfyUI's revision graph, there is no
|
|
3013
|
+
* configurable side to this — document it as fixed rather than leave an
|
|
3014
|
+
* agent to guess, the way docs/REVISIONS.md tells a ComfyUI author to test
|
|
3015
|
+
* their own graph.
|
|
3016
|
+
*/
|
|
3017
|
+
async submitRevision(spec) {
|
|
3018
|
+
const revision = spec.revision;
|
|
3019
|
+
if (!revision.sourceSha256 || !revision.sourceFormat || revision.sourceWidth == null || revision.sourceHeight == null) {
|
|
3020
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
|
|
3021
|
+
}
|
|
3022
|
+
const sourceBytes = (0, import_node_fs2.readFileSync)(revision.sourceFile);
|
|
3023
|
+
if (sha256(sourceBytes) !== revision.sourceSha256) {
|
|
3024
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
|
|
3025
|
+
}
|
|
3026
|
+
const image = { base64: sourceBytes.toString("base64"), format: revision.sourceFormat };
|
|
3027
|
+
const width = revision.sourceWidth;
|
|
3028
|
+
const height = revision.sourceHeight;
|
|
3029
|
+
if (revision.mode === "inpaint") {
|
|
3030
|
+
if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
|
|
3031
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
|
|
3032
|
+
}
|
|
3033
|
+
const maskBytes = (0, import_node_fs2.readFileSync)(revision.maskFile);
|
|
3034
|
+
if (sha256(maskBytes) !== revision.maskSha256) {
|
|
3035
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
|
|
3036
|
+
}
|
|
3037
|
+
const res2 = await this.client.inpaintV3({
|
|
3038
|
+
description: spec.prompt,
|
|
3039
|
+
image,
|
|
3040
|
+
width,
|
|
3041
|
+
height,
|
|
3042
|
+
maskImage: { base64: maskBytes.toString("base64"), format: revision.maskFormat },
|
|
3043
|
+
noBackground: spec.noBackground,
|
|
3044
|
+
seed: spec.seed
|
|
3045
|
+
});
|
|
3046
|
+
return { jobId: res2.background_job_id };
|
|
3047
|
+
}
|
|
3048
|
+
if (revision.strength != null) {
|
|
3049
|
+
throw new Error(
|
|
3050
|
+
`${spec.styleId}/${spec.assetId}: PixelLab image-to-image revisions take no strength; edit-images-v2 always applies the full instruction`
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
const res = await this.client.editImagesV2({
|
|
3054
|
+
description: spec.prompt,
|
|
3055
|
+
image,
|
|
3056
|
+
width,
|
|
3057
|
+
height,
|
|
3058
|
+
noBackground: spec.noBackground,
|
|
3059
|
+
seed: spec.seed
|
|
3060
|
+
});
|
|
3061
|
+
return { jobId: res.background_job_id };
|
|
3062
|
+
}
|
|
2931
3063
|
/**
|
|
2932
3064
|
* What a background job actually billed, once its own record is still
|
|
2933
3065
|
* around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
|
|
@@ -2944,6 +3076,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2944
3076
|
}
|
|
2945
3077
|
}
|
|
2946
3078
|
async poll(jobId, generator, context) {
|
|
3079
|
+
if (context?.spec?.revision) return this.pollRevision(jobId);
|
|
2947
3080
|
if (generator === "pixflux") {
|
|
2948
3081
|
const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
2949
3082
|
if ((0, import_node_fs2.existsSync)(file)) {
|
|
@@ -2974,6 +3107,58 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2974
3107
|
etaSeconds: obj.eta_seconds ?? null
|
|
2975
3108
|
};
|
|
2976
3109
|
}
|
|
3110
|
+
/**
|
|
3111
|
+
* `/inpaint-v3` and `/edit-images-v2` both hand back a plain background
|
|
3112
|
+
* job with no resource of its own, polled generically at
|
|
3113
|
+
* `GET /background-jobs/{id}`. What a *completed* job's `last_response`
|
|
3114
|
+
* actually contains is not documented for either endpoint: the OpenAPI
|
|
3115
|
+
* spec's only worked example of that field is a character job's shape
|
|
3116
|
+
* (`character_id`, `uploaded_directions`, ...), not an inpaint or edit
|
|
3117
|
+
* job's. This reads every image shape seen elsewhere in this client
|
|
3118
|
+
* (a nested `{image: {base64, format}}`, a bare `{base64, format}`, or a
|
|
3119
|
+
* hosted URL under a handful of plausible keys) and fails loudly, naming
|
|
3120
|
+
* the keys it actually got, rather than guess wrong silently. Fixing a
|
|
3121
|
+
* real completed response into this list is a one-line change once one is
|
|
3122
|
+
* seen live.
|
|
3123
|
+
*/
|
|
3124
|
+
async pollRevision(jobId) {
|
|
3125
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
3126
|
+
if (job.status === "failed") return { status: "failed", error: "revision job failed upstream" };
|
|
3127
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
3128
|
+
const billed = billedFromUsage(job.usage);
|
|
3129
|
+
const done = job.last_response ?? {};
|
|
3130
|
+
const urlKeys = ["image_url", "download_url", "url", "output_url"];
|
|
3131
|
+
for (const key of urlKeys) {
|
|
3132
|
+
const url = done[key];
|
|
3133
|
+
if (typeof url === "string" && url) {
|
|
3134
|
+
return { status: "ready", objectId: jobId, sourceUrl: url, sources: [{ url }], billed };
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
const imageKeys = ["image", "output_image", "result_image", "edited_image"];
|
|
3138
|
+
for (const key of imageKeys) {
|
|
3139
|
+
const candidate = done[key];
|
|
3140
|
+
const base64 = extractBase64(candidate);
|
|
3141
|
+
if (base64) {
|
|
3142
|
+
const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
3143
|
+
(0, import_node_fs2.writeFileSync)(file, Buffer.from(base64, "base64"));
|
|
3144
|
+
const sourceUrl = `file://${file}`;
|
|
3145
|
+
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
if (Array.isArray(done.images) && done.images.length) {
|
|
3149
|
+
const base64 = extractBase64(done.images[0]);
|
|
3150
|
+
if (base64) {
|
|
3151
|
+
const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
3152
|
+
(0, import_node_fs2.writeFileSync)(file, Buffer.from(base64, "base64"));
|
|
3153
|
+
const sourceUrl = `file://${file}`;
|
|
3154
|
+
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
return {
|
|
3158
|
+
status: "failed",
|
|
3159
|
+
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`
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
2977
3162
|
/**
|
|
2978
3163
|
* Map objects need their own path because the `/map-objects/{id}` record is
|
|
2979
3164
|
* deleted upstream roughly 8 hours after creation while the image survives in
|
|
@@ -3165,6 +3350,15 @@ function firstUrl(urls) {
|
|
|
3165
3350
|
if (!urls) return null;
|
|
3166
3351
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
3167
3352
|
}
|
|
3353
|
+
function extractBase64(value) {
|
|
3354
|
+
if (typeof value === "string" && value) return value;
|
|
3355
|
+
if (value && typeof value === "object") {
|
|
3356
|
+
const obj = value;
|
|
3357
|
+
if (typeof obj.base64 === "string" && obj.base64) return obj.base64;
|
|
3358
|
+
if (obj.image) return extractBase64(obj.image);
|
|
3359
|
+
}
|
|
3360
|
+
return null;
|
|
3361
|
+
}
|
|
3168
3362
|
function readReference(spec, reference) {
|
|
3169
3363
|
const images = {};
|
|
3170
3364
|
for (const [direction, image] of Object.entries(reference)) {
|
|
@@ -12336,7 +12530,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
12336
12530
|
retiredAt: generation.retiredAt,
|
|
12337
12531
|
outputs,
|
|
12338
12532
|
cached: outputs.length > 0 && outputs.every((output) => output.url !== null),
|
|
12339
|
-
|
|
12533
|
+
// A revision's jobId is a plain background job id, not a PixelLab
|
|
12534
|
+
// account object: there is no `/create-object` page to point at.
|
|
12535
|
+
upstreamUrl: generation.provider === "pixellab" && !entry.revision ? pixelLabObjectUrl(generation.generator, generation.objectId) : null
|
|
12340
12536
|
};
|
|
12341
12537
|
});
|
|
12342
12538
|
}
|
|
@@ -12601,7 +12797,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
12601
12797
|
editChanged,
|
|
12602
12798
|
editStatus,
|
|
12603
12799
|
editMeta,
|
|
12604
|
-
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
12800
|
+
upstreamUrl: entry?.provider === "pixellab" && !spec.revision ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
12605
12801
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
12606
12802
|
history: entry ? describeHistory(media, entry, cacheDir) : [],
|
|
12607
12803
|
tags: spec.tags,
|
|
@@ -12665,7 +12861,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
12665
12861
|
editChanged: [],
|
|
12666
12862
|
editStatus: null,
|
|
12667
12863
|
editMeta: null,
|
|
12668
|
-
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
12864
|
+
upstreamUrl: entry.provider === "pixellab" && !entry.revision ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
12669
12865
|
refreshable: false,
|
|
12670
12866
|
history: describeHistory(media, entry, cacheDir),
|
|
12671
12867
|
tags: [],
|