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.d.cts
CHANGED
|
@@ -8591,6 +8591,52 @@ declare class PixelLabClient {
|
|
|
8591
8591
|
total: number;
|
|
8592
8592
|
}>;
|
|
8593
8593
|
iterateCharacters(pageSize?: number): AsyncGenerator<PixelLabCharacter>;
|
|
8594
|
+
/**
|
|
8595
|
+
* Masked inpaint, PixelLab's `/inpaint-v3` (the endpoint its own docs list
|
|
8596
|
+
* first in the Inpaint section, its convention for "reach for this by
|
|
8597
|
+
* default"). Unlike the rest of this client, this method's shape is taken
|
|
8598
|
+
* from the OpenAPI spec, not exercised against a live account: the request
|
|
8599
|
+
* side is exact (`InpaintV3Request`), but a completed job's `last_response`
|
|
8600
|
+
* has no documented example for this endpoint (the spec's only worked
|
|
8601
|
+
* example is a character job's shape). `pollRevision` in pixellab.ts reads
|
|
8602
|
+
* it defensively and fails loudly on an unrecognized shape rather than
|
|
8603
|
+
* guessing.
|
|
8604
|
+
*
|
|
8605
|
+
* `crop_to_mask` defaults true upstream (confirmed in the schema): PixelLab
|
|
8606
|
+
* otherwise blends generated pixels outside the mask edge to "fit
|
|
8607
|
+
* naturally," which is the opposite of what a mask boundary is for.
|
|
8608
|
+
*/
|
|
8609
|
+
inpaintV3(args: {
|
|
8610
|
+
description: string;
|
|
8611
|
+
image: Base64Image;
|
|
8612
|
+
width: number;
|
|
8613
|
+
height: number;
|
|
8614
|
+
maskImage: Base64Image;
|
|
8615
|
+
noBackground?: boolean;
|
|
8616
|
+
cropToMask?: boolean;
|
|
8617
|
+
seed?: number;
|
|
8618
|
+
}): Promise<{
|
|
8619
|
+
background_job_id: string;
|
|
8620
|
+
status: string;
|
|
8621
|
+
}>;
|
|
8622
|
+
/**
|
|
8623
|
+
* Whole-image edit with no mask, PixelLab's `/edit-images-v2`. Same
|
|
8624
|
+
* not-yet-live-verified caveat as `inpaintV3` above. `edit_images` takes an
|
|
8625
|
+
* array (the endpoint supports editing several images with one
|
|
8626
|
+
* instruction); pixelkiln's `revision` model is one parent per child, so
|
|
8627
|
+
* this always sends exactly one.
|
|
8628
|
+
*/
|
|
8629
|
+
editImagesV2(args: {
|
|
8630
|
+
description: string;
|
|
8631
|
+
image: Base64Image;
|
|
8632
|
+
width: number;
|
|
8633
|
+
height: number;
|
|
8634
|
+
noBackground?: boolean;
|
|
8635
|
+
seed?: number;
|
|
8636
|
+
}): Promise<{
|
|
8637
|
+
background_job_id: string;
|
|
8638
|
+
status: string;
|
|
8639
|
+
}>;
|
|
8594
8640
|
getBackgroundJob(jobId: string): Promise<{
|
|
8595
8641
|
id: string;
|
|
8596
8642
|
status: string;
|
|
@@ -8637,6 +8683,13 @@ declare class PixelLabProvider implements Provider {
|
|
|
8637
8683
|
/** Capability and cost estimation only; makes no network request itself. */
|
|
8638
8684
|
static forOffline(): PixelLabProvider;
|
|
8639
8685
|
supports(generator: Generator): boolean;
|
|
8686
|
+
/**
|
|
8687
|
+
* `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
|
|
8688
|
+
* no mask) both exist on PixelLab. `outpaint` does not: there is no
|
|
8689
|
+
* canvas-expansion endpoint in the API, matching docs/REVISIONS.md's note
|
|
8690
|
+
* that no provider ships a tested outpaint path yet.
|
|
8691
|
+
*/
|
|
8692
|
+
supportsRevision(mode: RevisionMode): boolean;
|
|
8640
8693
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
8641
8694
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
8642
8695
|
* Tier 2=10, Tier 3=20); 8 is the safe floor across every tier. */
|
|
@@ -8673,6 +8726,15 @@ declare class PixelLabProvider implements Provider {
|
|
|
8673
8726
|
jobId: string;
|
|
8674
8727
|
metadata?: Record<string, unknown>;
|
|
8675
8728
|
}>;
|
|
8729
|
+
/**
|
|
8730
|
+
* `inpaint` reads the mask as PixelLab's own convention: white marks the
|
|
8731
|
+
* area to generate, black the area to preserve (`InpaintV3Request`'s own
|
|
8732
|
+
* field description). Unlike ComfyUI's revision graph, there is no
|
|
8733
|
+
* configurable side to this — document it as fixed rather than leave an
|
|
8734
|
+
* agent to guess, the way docs/REVISIONS.md tells a ComfyUI author to test
|
|
8735
|
+
* their own graph.
|
|
8736
|
+
*/
|
|
8737
|
+
private submitRevision;
|
|
8676
8738
|
/**
|
|
8677
8739
|
* What a background job actually billed, once its own record is still
|
|
8678
8740
|
* around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
|
|
@@ -8681,6 +8743,21 @@ declare class PixelLabProvider implements Provider {
|
|
|
8681
8743
|
*/
|
|
8682
8744
|
private billedForJob;
|
|
8683
8745
|
poll(jobId: string, generator: Generator, context?: PollContext): Promise<JobState>;
|
|
8746
|
+
/**
|
|
8747
|
+
* `/inpaint-v3` and `/edit-images-v2` both hand back a plain background
|
|
8748
|
+
* job with no resource of its own, polled generically at
|
|
8749
|
+
* `GET /background-jobs/{id}`. What a *completed* job's `last_response`
|
|
8750
|
+
* actually contains is not documented for either endpoint: the OpenAPI
|
|
8751
|
+
* spec's only worked example of that field is a character job's shape
|
|
8752
|
+
* (`character_id`, `uploaded_directions`, ...), not an inpaint or edit
|
|
8753
|
+
* job's. This reads every image shape seen elsewhere in this client
|
|
8754
|
+
* (a nested `{image: {base64, format}}`, a bare `{base64, format}`, or a
|
|
8755
|
+
* hosted URL under a handful of plausible keys) and fails loudly, naming
|
|
8756
|
+
* the keys it actually got, rather than guess wrong silently. Fixing a
|
|
8757
|
+
* real completed response into this list is a one-line change once one is
|
|
8758
|
+
* seen live.
|
|
8759
|
+
*/
|
|
8760
|
+
private pollRevision;
|
|
8684
8761
|
/**
|
|
8685
8762
|
* Map objects need their own path because the `/map-objects/{id}` record is
|
|
8686
8763
|
* deleted upstream roughly 8 hours after creation while the image survives in
|
package/dist/index.d.ts
CHANGED
|
@@ -8591,6 +8591,52 @@ declare class PixelLabClient {
|
|
|
8591
8591
|
total: number;
|
|
8592
8592
|
}>;
|
|
8593
8593
|
iterateCharacters(pageSize?: number): AsyncGenerator<PixelLabCharacter>;
|
|
8594
|
+
/**
|
|
8595
|
+
* Masked inpaint, PixelLab's `/inpaint-v3` (the endpoint its own docs list
|
|
8596
|
+
* first in the Inpaint section, its convention for "reach for this by
|
|
8597
|
+
* default"). Unlike the rest of this client, this method's shape is taken
|
|
8598
|
+
* from the OpenAPI spec, not exercised against a live account: the request
|
|
8599
|
+
* side is exact (`InpaintV3Request`), but a completed job's `last_response`
|
|
8600
|
+
* has no documented example for this endpoint (the spec's only worked
|
|
8601
|
+
* example is a character job's shape). `pollRevision` in pixellab.ts reads
|
|
8602
|
+
* it defensively and fails loudly on an unrecognized shape rather than
|
|
8603
|
+
* guessing.
|
|
8604
|
+
*
|
|
8605
|
+
* `crop_to_mask` defaults true upstream (confirmed in the schema): PixelLab
|
|
8606
|
+
* otherwise blends generated pixels outside the mask edge to "fit
|
|
8607
|
+
* naturally," which is the opposite of what a mask boundary is for.
|
|
8608
|
+
*/
|
|
8609
|
+
inpaintV3(args: {
|
|
8610
|
+
description: string;
|
|
8611
|
+
image: Base64Image;
|
|
8612
|
+
width: number;
|
|
8613
|
+
height: number;
|
|
8614
|
+
maskImage: Base64Image;
|
|
8615
|
+
noBackground?: boolean;
|
|
8616
|
+
cropToMask?: boolean;
|
|
8617
|
+
seed?: number;
|
|
8618
|
+
}): Promise<{
|
|
8619
|
+
background_job_id: string;
|
|
8620
|
+
status: string;
|
|
8621
|
+
}>;
|
|
8622
|
+
/**
|
|
8623
|
+
* Whole-image edit with no mask, PixelLab's `/edit-images-v2`. Same
|
|
8624
|
+
* not-yet-live-verified caveat as `inpaintV3` above. `edit_images` takes an
|
|
8625
|
+
* array (the endpoint supports editing several images with one
|
|
8626
|
+
* instruction); pixelkiln's `revision` model is one parent per child, so
|
|
8627
|
+
* this always sends exactly one.
|
|
8628
|
+
*/
|
|
8629
|
+
editImagesV2(args: {
|
|
8630
|
+
description: string;
|
|
8631
|
+
image: Base64Image;
|
|
8632
|
+
width: number;
|
|
8633
|
+
height: number;
|
|
8634
|
+
noBackground?: boolean;
|
|
8635
|
+
seed?: number;
|
|
8636
|
+
}): Promise<{
|
|
8637
|
+
background_job_id: string;
|
|
8638
|
+
status: string;
|
|
8639
|
+
}>;
|
|
8594
8640
|
getBackgroundJob(jobId: string): Promise<{
|
|
8595
8641
|
id: string;
|
|
8596
8642
|
status: string;
|
|
@@ -8637,6 +8683,13 @@ declare class PixelLabProvider implements Provider {
|
|
|
8637
8683
|
/** Capability and cost estimation only; makes no network request itself. */
|
|
8638
8684
|
static forOffline(): PixelLabProvider;
|
|
8639
8685
|
supports(generator: Generator): boolean;
|
|
8686
|
+
/**
|
|
8687
|
+
* `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
|
|
8688
|
+
* no mask) both exist on PixelLab. `outpaint` does not: there is no
|
|
8689
|
+
* canvas-expansion endpoint in the API, matching docs/REVISIONS.md's note
|
|
8690
|
+
* that no provider ships a tested outpaint path yet.
|
|
8691
|
+
*/
|
|
8692
|
+
supportsRevision(mode: RevisionMode): boolean;
|
|
8640
8693
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
8641
8694
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
8642
8695
|
* Tier 2=10, Tier 3=20); 8 is the safe floor across every tier. */
|
|
@@ -8673,6 +8726,15 @@ declare class PixelLabProvider implements Provider {
|
|
|
8673
8726
|
jobId: string;
|
|
8674
8727
|
metadata?: Record<string, unknown>;
|
|
8675
8728
|
}>;
|
|
8729
|
+
/**
|
|
8730
|
+
* `inpaint` reads the mask as PixelLab's own convention: white marks the
|
|
8731
|
+
* area to generate, black the area to preserve (`InpaintV3Request`'s own
|
|
8732
|
+
* field description). Unlike ComfyUI's revision graph, there is no
|
|
8733
|
+
* configurable side to this — document it as fixed rather than leave an
|
|
8734
|
+
* agent to guess, the way docs/REVISIONS.md tells a ComfyUI author to test
|
|
8735
|
+
* their own graph.
|
|
8736
|
+
*/
|
|
8737
|
+
private submitRevision;
|
|
8676
8738
|
/**
|
|
8677
8739
|
* What a background job actually billed, once its own record is still
|
|
8678
8740
|
* around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
|
|
@@ -8681,6 +8743,21 @@ declare class PixelLabProvider implements Provider {
|
|
|
8681
8743
|
*/
|
|
8682
8744
|
private billedForJob;
|
|
8683
8745
|
poll(jobId: string, generator: Generator, context?: PollContext): Promise<JobState>;
|
|
8746
|
+
/**
|
|
8747
|
+
* `/inpaint-v3` and `/edit-images-v2` both hand back a plain background
|
|
8748
|
+
* job with no resource of its own, polled generically at
|
|
8749
|
+
* `GET /background-jobs/{id}`. What a *completed* job's `last_response`
|
|
8750
|
+
* actually contains is not documented for either endpoint: the OpenAPI
|
|
8751
|
+
* spec's only worked example of that field is a character job's shape
|
|
8752
|
+
* (`character_id`, `uploaded_directions`, ...), not an inpaint or edit
|
|
8753
|
+
* job's. This reads every image shape seen elsewhere in this client
|
|
8754
|
+
* (a nested `{image: {base64, format}}`, a bare `{base64, format}`, or a
|
|
8755
|
+
* hosted URL under a handful of plausible keys) and fails loudly, naming
|
|
8756
|
+
* the keys it actually got, rather than guess wrong silently. Fixing a
|
|
8757
|
+
* real completed response into this list is a one-line change once one is
|
|
8758
|
+
* seen live.
|
|
8759
|
+
*/
|
|
8760
|
+
private pollRevision;
|
|
8684
8761
|
/**
|
|
8685
8762
|
* Map objects need their own path because the `/map-objects/{id}` record is
|
|
8686
8763
|
* deleted upstream roughly 8 hours after creation while the image survives in
|
package/dist/index.js
CHANGED
|
@@ -944,6 +944,10 @@ var TilesProSchema = z2.object({
|
|
|
944
944
|
kind: z2.string().nullable().default(null),
|
|
945
945
|
tile_rules: z2.record(z2.unknown()).nullable().optional()
|
|
946
946
|
}).passthrough();
|
|
947
|
+
var RevisionJobSubmitSchema = z2.object({
|
|
948
|
+
background_job_id: z2.string().min(1),
|
|
949
|
+
status: z2.string().default("processing")
|
|
950
|
+
}).passthrough();
|
|
947
951
|
var PixelLabObjectSchema = z2.object({
|
|
948
952
|
id: z2.string().min(1),
|
|
949
953
|
name: z2.string().nullable().default(null),
|
|
@@ -1415,6 +1419,59 @@ var PixelLabClient = class {
|
|
|
1415
1419
|
if (page.characters.length === 0 || offset >= page.total) return;
|
|
1416
1420
|
}
|
|
1417
1421
|
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Masked inpaint, PixelLab's `/inpaint-v3` (the endpoint its own docs list
|
|
1424
|
+
* first in the Inpaint section, its convention for "reach for this by
|
|
1425
|
+
* default"). Unlike the rest of this client, this method's shape is taken
|
|
1426
|
+
* from the OpenAPI spec, not exercised against a live account: the request
|
|
1427
|
+
* side is exact (`InpaintV3Request`), but a completed job's `last_response`
|
|
1428
|
+
* has no documented example for this endpoint (the spec's only worked
|
|
1429
|
+
* example is a character job's shape). `pollRevision` in pixellab.ts reads
|
|
1430
|
+
* it defensively and fails loudly on an unrecognized shape rather than
|
|
1431
|
+
* guessing.
|
|
1432
|
+
*
|
|
1433
|
+
* `crop_to_mask` defaults true upstream (confirmed in the schema): PixelLab
|
|
1434
|
+
* otherwise blends generated pixels outside the mask edge to "fit
|
|
1435
|
+
* naturally," which is the opposite of what a mask boundary is for.
|
|
1436
|
+
*/
|
|
1437
|
+
async inpaintV3(args) {
|
|
1438
|
+
const size = { width: args.width, height: args.height };
|
|
1439
|
+
const body = {
|
|
1440
|
+
description: args.description,
|
|
1441
|
+
inpainting_image: { image: args.image, size },
|
|
1442
|
+
mask_image: { image: args.maskImage, size }
|
|
1443
|
+
};
|
|
1444
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
1445
|
+
if (args.cropToMask != null) body.crop_to_mask = args.cropToMask;
|
|
1446
|
+
if (args.seed != null) body.seed = args.seed;
|
|
1447
|
+
return validateResponse(
|
|
1448
|
+
RevisionJobSubmitSchema,
|
|
1449
|
+
await this.request("/inpaint-v3", { method: "POST", body: JSON.stringify(body) }),
|
|
1450
|
+
"inpaint-v3"
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Whole-image edit with no mask, PixelLab's `/edit-images-v2`. Same
|
|
1455
|
+
* not-yet-live-verified caveat as `inpaintV3` above. `edit_images` takes an
|
|
1456
|
+
* array (the endpoint supports editing several images with one
|
|
1457
|
+
* instruction); pixelkiln's `revision` model is one parent per child, so
|
|
1458
|
+
* this always sends exactly one.
|
|
1459
|
+
*/
|
|
1460
|
+
async editImagesV2(args) {
|
|
1461
|
+
const body = {
|
|
1462
|
+
method: "edit_with_text",
|
|
1463
|
+
description: args.description,
|
|
1464
|
+
edit_images: [{ image: args.image, width: args.width, height: args.height }],
|
|
1465
|
+
image_size: { width: args.width, height: args.height }
|
|
1466
|
+
};
|
|
1467
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
1468
|
+
if (args.seed != null) body.seed = args.seed;
|
|
1469
|
+
return validateResponse(
|
|
1470
|
+
RevisionJobSubmitSchema,
|
|
1471
|
+
await this.request("/edit-images-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
1472
|
+
"edit-images-v2"
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1418
1475
|
async getBackgroundJob(jobId) {
|
|
1419
1476
|
const raw = await this.request(`/background-jobs/${encodeURIComponent(jobId)}`);
|
|
1420
1477
|
return validateResponse(BackgroundJobSchema, raw, "background-jobs/{id}");
|
|
@@ -2196,6 +2253,15 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2196
2253
|
supports(generator) {
|
|
2197
2254
|
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "character";
|
|
2198
2255
|
}
|
|
2256
|
+
/**
|
|
2257
|
+
* `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
|
|
2258
|
+
* no mask) both exist on PixelLab. `outpaint` does not: there is no
|
|
2259
|
+
* canvas-expansion endpoint in the API, matching docs/REVISIONS.md's note
|
|
2260
|
+
* that no provider ships a tested outpaint path yet.
|
|
2261
|
+
*/
|
|
2262
|
+
supportsRevision(mode2) {
|
|
2263
|
+
return mode2 === "inpaint" || mode2 === "image-to-image";
|
|
2264
|
+
}
|
|
2199
2265
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
2200
2266
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
2201
2267
|
* Tier 2=10, Tier 3=20); 8 is the safe floor across every tier. */
|
|
@@ -2216,6 +2282,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2216
2282
|
return dir;
|
|
2217
2283
|
}
|
|
2218
2284
|
estimate(spec) {
|
|
2285
|
+
if (spec.revision) {
|
|
2286
|
+
const width = spec.revision.sourceWidth ?? spec.width;
|
|
2287
|
+
const height = spec.revision.sourceHeight ?? spec.height;
|
|
2288
|
+
return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
|
|
2289
|
+
}
|
|
2219
2290
|
if (spec.generator === "tiles") {
|
|
2220
2291
|
return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
|
|
2221
2292
|
}
|
|
@@ -2229,6 +2300,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2229
2300
|
};
|
|
2230
2301
|
}
|
|
2231
2302
|
validate(spec, styleImages) {
|
|
2303
|
+
if (spec.revision?.mode === "inpaint") {
|
|
2304
|
+
const { sourceWidth: width, sourceHeight: height } = spec.revision;
|
|
2305
|
+
if (width != null && height != null && (width < 32 || height < 32 || width > 512 || height > 512)) {
|
|
2306
|
+
throw new Error(`PixelLab inpaint source is ${width}x${height}; the API takes 32 to 512 pixels per side`);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2232
2309
|
if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
|
|
2233
2310
|
throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
|
|
2234
2311
|
}
|
|
@@ -2578,6 +2655,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2578
2655
|
}
|
|
2579
2656
|
async submit(spec, styleImages, context) {
|
|
2580
2657
|
this.validate(spec, styleImages);
|
|
2658
|
+
if (spec.revision) return this.submitRevision(spec);
|
|
2581
2659
|
if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
|
|
2582
2660
|
if (spec.generator === "pixflux") {
|
|
2583
2661
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
@@ -2629,6 +2707,60 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2629
2707
|
});
|
|
2630
2708
|
return { jobId: res.object_id, metadata: { backgroundJobId: res.background_job_id } };
|
|
2631
2709
|
}
|
|
2710
|
+
/**
|
|
2711
|
+
* `inpaint` reads the mask as PixelLab's own convention: white marks the
|
|
2712
|
+
* area to generate, black the area to preserve (`InpaintV3Request`'s own
|
|
2713
|
+
* field description). Unlike ComfyUI's revision graph, there is no
|
|
2714
|
+
* configurable side to this — document it as fixed rather than leave an
|
|
2715
|
+
* agent to guess, the way docs/REVISIONS.md tells a ComfyUI author to test
|
|
2716
|
+
* their own graph.
|
|
2717
|
+
*/
|
|
2718
|
+
async submitRevision(spec) {
|
|
2719
|
+
const revision = spec.revision;
|
|
2720
|
+
if (!revision.sourceSha256 || !revision.sourceFormat || revision.sourceWidth == null || revision.sourceHeight == null) {
|
|
2721
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
|
|
2722
|
+
}
|
|
2723
|
+
const sourceBytes = readFileSync(revision.sourceFile);
|
|
2724
|
+
if (sha256(sourceBytes) !== revision.sourceSha256) {
|
|
2725
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
|
|
2726
|
+
}
|
|
2727
|
+
const image = { base64: sourceBytes.toString("base64"), format: revision.sourceFormat };
|
|
2728
|
+
const width = revision.sourceWidth;
|
|
2729
|
+
const height = revision.sourceHeight;
|
|
2730
|
+
if (revision.mode === "inpaint") {
|
|
2731
|
+
if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
|
|
2732
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
|
|
2733
|
+
}
|
|
2734
|
+
const maskBytes = readFileSync(revision.maskFile);
|
|
2735
|
+
if (sha256(maskBytes) !== revision.maskSha256) {
|
|
2736
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
|
|
2737
|
+
}
|
|
2738
|
+
const res2 = await this.client.inpaintV3({
|
|
2739
|
+
description: spec.prompt,
|
|
2740
|
+
image,
|
|
2741
|
+
width,
|
|
2742
|
+
height,
|
|
2743
|
+
maskImage: { base64: maskBytes.toString("base64"), format: revision.maskFormat },
|
|
2744
|
+
noBackground: spec.noBackground,
|
|
2745
|
+
seed: spec.seed
|
|
2746
|
+
});
|
|
2747
|
+
return { jobId: res2.background_job_id };
|
|
2748
|
+
}
|
|
2749
|
+
if (revision.strength != null) {
|
|
2750
|
+
throw new Error(
|
|
2751
|
+
`${spec.styleId}/${spec.assetId}: PixelLab image-to-image revisions take no strength; edit-images-v2 always applies the full instruction`
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
const res = await this.client.editImagesV2({
|
|
2755
|
+
description: spec.prompt,
|
|
2756
|
+
image,
|
|
2757
|
+
width,
|
|
2758
|
+
height,
|
|
2759
|
+
noBackground: spec.noBackground,
|
|
2760
|
+
seed: spec.seed
|
|
2761
|
+
});
|
|
2762
|
+
return { jobId: res.background_job_id };
|
|
2763
|
+
}
|
|
2632
2764
|
/**
|
|
2633
2765
|
* What a background job actually billed, once its own record is still
|
|
2634
2766
|
* around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
|
|
@@ -2645,6 +2777,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2645
2777
|
}
|
|
2646
2778
|
}
|
|
2647
2779
|
async poll(jobId, generator, context) {
|
|
2780
|
+
if (context?.spec?.revision) return this.pollRevision(jobId);
|
|
2648
2781
|
if (generator === "pixflux") {
|
|
2649
2782
|
const file = path.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
2650
2783
|
if (existsSync(file)) {
|
|
@@ -2675,6 +2808,58 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2675
2808
|
etaSeconds: obj.eta_seconds ?? null
|
|
2676
2809
|
};
|
|
2677
2810
|
}
|
|
2811
|
+
/**
|
|
2812
|
+
* `/inpaint-v3` and `/edit-images-v2` both hand back a plain background
|
|
2813
|
+
* job with no resource of its own, polled generically at
|
|
2814
|
+
* `GET /background-jobs/{id}`. What a *completed* job's `last_response`
|
|
2815
|
+
* actually contains is not documented for either endpoint: the OpenAPI
|
|
2816
|
+
* spec's only worked example of that field is a character job's shape
|
|
2817
|
+
* (`character_id`, `uploaded_directions`, ...), not an inpaint or edit
|
|
2818
|
+
* job's. This reads every image shape seen elsewhere in this client
|
|
2819
|
+
* (a nested `{image: {base64, format}}`, a bare `{base64, format}`, or a
|
|
2820
|
+
* hosted URL under a handful of plausible keys) and fails loudly, naming
|
|
2821
|
+
* the keys it actually got, rather than guess wrong silently. Fixing a
|
|
2822
|
+
* real completed response into this list is a one-line change once one is
|
|
2823
|
+
* seen live.
|
|
2824
|
+
*/
|
|
2825
|
+
async pollRevision(jobId) {
|
|
2826
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
2827
|
+
if (job.status === "failed") return { status: "failed", error: "revision job failed upstream" };
|
|
2828
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
2829
|
+
const billed = billedFromUsage(job.usage);
|
|
2830
|
+
const done = job.last_response ?? {};
|
|
2831
|
+
const urlKeys = ["image_url", "download_url", "url", "output_url"];
|
|
2832
|
+
for (const key of urlKeys) {
|
|
2833
|
+
const url = done[key];
|
|
2834
|
+
if (typeof url === "string" && url) {
|
|
2835
|
+
return { status: "ready", objectId: jobId, sourceUrl: url, sources: [{ url }], billed };
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
const imageKeys = ["image", "output_image", "result_image", "edited_image"];
|
|
2839
|
+
for (const key of imageKeys) {
|
|
2840
|
+
const candidate = done[key];
|
|
2841
|
+
const base64 = extractBase64(candidate);
|
|
2842
|
+
if (base64) {
|
|
2843
|
+
const file = path.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
2844
|
+
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
2845
|
+
const sourceUrl = `file://${file}`;
|
|
2846
|
+
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
if (Array.isArray(done.images) && done.images.length) {
|
|
2850
|
+
const base64 = extractBase64(done.images[0]);
|
|
2851
|
+
if (base64) {
|
|
2852
|
+
const file = path.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
2853
|
+
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
2854
|
+
const sourceUrl = `file://${file}`;
|
|
2855
|
+
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
return {
|
|
2859
|
+
status: "failed",
|
|
2860
|
+
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`
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2678
2863
|
/**
|
|
2679
2864
|
* Map objects need their own path because the `/map-objects/{id}` record is
|
|
2680
2865
|
* deleted upstream roughly 8 hours after creation while the image survives in
|
|
@@ -2866,6 +3051,15 @@ function firstUrl(urls) {
|
|
|
2866
3051
|
if (!urls) return null;
|
|
2867
3052
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
2868
3053
|
}
|
|
3054
|
+
function extractBase64(value) {
|
|
3055
|
+
if (typeof value === "string" && value) return value;
|
|
3056
|
+
if (value && typeof value === "object") {
|
|
3057
|
+
const obj = value;
|
|
3058
|
+
if (typeof obj.base64 === "string" && obj.base64) return obj.base64;
|
|
3059
|
+
if (obj.image) return extractBase64(obj.image);
|
|
3060
|
+
}
|
|
3061
|
+
return null;
|
|
3062
|
+
}
|
|
2869
3063
|
function readReference(spec, reference) {
|
|
2870
3064
|
const images = {};
|
|
2871
3065
|
for (const [direction, image] of Object.entries(reference)) {
|
|
@@ -12037,7 +12231,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
12037
12231
|
retiredAt: generation.retiredAt,
|
|
12038
12232
|
outputs,
|
|
12039
12233
|
cached: outputs.length > 0 && outputs.every((output) => output.url !== null),
|
|
12040
|
-
|
|
12234
|
+
// A revision's jobId is a plain background job id, not a PixelLab
|
|
12235
|
+
// account object: there is no `/create-object` page to point at.
|
|
12236
|
+
upstreamUrl: generation.provider === "pixellab" && !entry.revision ? pixelLabObjectUrl(generation.generator, generation.objectId) : null
|
|
12041
12237
|
};
|
|
12042
12238
|
});
|
|
12043
12239
|
}
|
|
@@ -12302,7 +12498,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
12302
12498
|
editChanged,
|
|
12303
12499
|
editStatus,
|
|
12304
12500
|
editMeta,
|
|
12305
|
-
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
12501
|
+
upstreamUrl: entry?.provider === "pixellab" && !spec.revision ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
12306
12502
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
12307
12503
|
history: entry ? describeHistory(media, entry, cacheDir) : [],
|
|
12308
12504
|
tags: spec.tags,
|
|
@@ -12366,7 +12562,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
12366
12562
|
editChanged: [],
|
|
12367
12563
|
editStatus: null,
|
|
12368
12564
|
editMeta: null,
|
|
12369
|
-
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
12565
|
+
upstreamUrl: entry.provider === "pixellab" && !entry.revision ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
12370
12566
|
refreshable: false,
|
|
12371
12567
|
history: describeHistory(media, entry, cacheDir),
|
|
12372
12568
|
tags: [],
|