pixelkiln 0.47.1 → 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 +305 -42
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +6 -1
- package/dist/index.cjs +275 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +246 -1
- package/dist/index.d.ts +246 -1
- package/dist/index.js +275 -24
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +12 -6
- package/docs/CLI.md +10 -1
- package/docs/ENDPOINTS.md +19 -4
- package/docs/GENERATORS.md +4 -0
- package/docs/PIXELLAB.md +8 -2
- 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
|
@@ -1374,6 +1374,8 @@ var LockHistoryEntrySchema = z.object({
|
|
|
1374
1374
|
downloadedAt: z.string().nullable().default(null),
|
|
1375
1375
|
cost: z.number().finite().nonnegative().default(0),
|
|
1376
1376
|
costUnit: z.string().min(1).default("generations"),
|
|
1377
|
+
/** What the provider actually billed, read from the completed job; null when never read. */
|
|
1378
|
+
billed: z.object({ amount: z.number(), unit: z.string() }).nullable().default(null),
|
|
1377
1379
|
provider: z.string().default("pixellab"),
|
|
1378
1380
|
postprocess: PostprocessSchema.optional(),
|
|
1379
1381
|
/** When the replacement was submitted. */
|
|
@@ -1465,6 +1467,14 @@ var LockEntrySchema = z.object({
|
|
|
1465
1467
|
cost: z.number().finite().nonnegative().default(0),
|
|
1466
1468
|
/** Unit for `cost`. Defaults preserve pre-unit PixelLab lockfiles. */
|
|
1467
1469
|
costUnit: z.string().min(1).default("generations"),
|
|
1470
|
+
/**
|
|
1471
|
+
* What the provider actually billed, read from the completed job rather
|
|
1472
|
+
* than estimated at submit time. Null until a poll reads it, and for a
|
|
1473
|
+
* provider or generator `poll` does not read it from at all; `cost` above
|
|
1474
|
+
* is still what a wave budget spends against, since the real number is
|
|
1475
|
+
* only known after the fact.
|
|
1476
|
+
*/
|
|
1477
|
+
billed: z.object({ amount: z.number(), unit: z.string() }).nullable().default(null),
|
|
1468
1478
|
/** Which provider produced this. Absent on entries written before providers. */
|
|
1469
1479
|
provider: z.string().default("pixellab"),
|
|
1470
1480
|
/** Generations this entry replaced, newest first; see LockHistoryEntrySchema. */
|
|
@@ -5115,6 +5125,10 @@ var TilesProSchema = z2.object({
|
|
|
5115
5125
|
kind: z2.string().nullable().default(null),
|
|
5116
5126
|
tile_rules: z2.record(z2.unknown()).nullable().optional()
|
|
5117
5127
|
}).passthrough();
|
|
5128
|
+
var RevisionJobSubmitSchema = z2.object({
|
|
5129
|
+
background_job_id: z2.string().min(1),
|
|
5130
|
+
status: z2.string().default("processing")
|
|
5131
|
+
}).passthrough();
|
|
5118
5132
|
var PixelLabObjectSchema = z2.object({
|
|
5119
5133
|
id: z2.string().min(1),
|
|
5120
5134
|
name: z2.string().nullable().default(null),
|
|
@@ -5193,7 +5207,13 @@ var CharacterListSchema = z2.object({ characters: z2.array(CharacterSchema), tot
|
|
|
5193
5207
|
var BackgroundJobSchema = z2.object({
|
|
5194
5208
|
id: z2.string(),
|
|
5195
5209
|
status: z2.string(),
|
|
5196
|
-
last_response: z2.record(z2.unknown()).nullable().optional()
|
|
5210
|
+
last_response: z2.record(z2.unknown()).nullable().optional(),
|
|
5211
|
+
/**
|
|
5212
|
+
* The billed amount, confirmed live for a `map` object and a standard
|
|
5213
|
+
* character base (docs/ENDPOINTS.md, "Limits and billing"); duplicated at
|
|
5214
|
+
* `last_response.billing_usage` on the jobs observed so far.
|
|
5215
|
+
*/
|
|
5216
|
+
usage: UsageSchema
|
|
5197
5217
|
}).passthrough();
|
|
5198
5218
|
var DeleteAnimationsSchema = z2.object({ success: z2.boolean(), deleted_count: z2.number().int().default(0), error: z2.string().nullable().optional() }).passthrough();
|
|
5199
5219
|
function validateResponse(schema, raw, operation) {
|
|
@@ -5580,6 +5600,59 @@ var PixelLabClient = class {
|
|
|
5580
5600
|
if (page.characters.length === 0 || offset >= page.total) return;
|
|
5581
5601
|
}
|
|
5582
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
|
+
}
|
|
5583
5656
|
async getBackgroundJob(jobId) {
|
|
5584
5657
|
const raw = await this.request(`/background-jobs/${encodeURIComponent(jobId)}`);
|
|
5585
5658
|
return validateResponse(BackgroundJobSchema, raw, "background-jobs/{id}");
|
|
@@ -5647,6 +5720,14 @@ function pixelLabObjectUrl(generator, objectId) {
|
|
|
5647
5720
|
}
|
|
5648
5721
|
return null;
|
|
5649
5722
|
}
|
|
5723
|
+
function billedFromUsage(usage) {
|
|
5724
|
+
if (!usage) return null;
|
|
5725
|
+
if (usage.type === "usd" || usage.type === void 0 && typeof usage.usd === "number") {
|
|
5726
|
+
return typeof usage.usd === "number" ? { amount: usage.usd, unit: "usd" } : null;
|
|
5727
|
+
}
|
|
5728
|
+
if (typeof usage.generations === "number") return { amount: usage.generations, unit: "generations" };
|
|
5729
|
+
return null;
|
|
5730
|
+
}
|
|
5650
5731
|
function characterCost(spec) {
|
|
5651
5732
|
const character = spec.character;
|
|
5652
5733
|
if (!character) return generationCost(spec.width, spec.height, "1dir");
|
|
@@ -5712,6 +5793,15 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5712
5793
|
supports(generator) {
|
|
5713
5794
|
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "character";
|
|
5714
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
|
+
}
|
|
5715
5805
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
5716
5806
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
5717
5807
|
* Tier 2=10, Tier 3=20); 8 is the safe floor across every tier. */
|
|
@@ -5732,6 +5822,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5732
5822
|
return dir;
|
|
5733
5823
|
}
|
|
5734
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
|
+
}
|
|
5735
5830
|
if (spec.generator === "tiles") {
|
|
5736
5831
|
return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
|
|
5737
5832
|
}
|
|
@@ -5745,6 +5840,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5745
5840
|
};
|
|
5746
5841
|
}
|
|
5747
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
|
+
}
|
|
5748
5849
|
if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
|
|
5749
5850
|
throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
|
|
5750
5851
|
}
|
|
@@ -6017,6 +6118,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6017
6118
|
if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
|
|
6018
6119
|
const sources = rotationSources(character);
|
|
6019
6120
|
if (!sources.length) return { status: "failed", error: "character completed with no rotation images" };
|
|
6121
|
+
const backgroundJobId = context?.metadata?.character?.backgroundJobId;
|
|
6020
6122
|
return {
|
|
6021
6123
|
status: "ready",
|
|
6022
6124
|
objectId: character.id,
|
|
@@ -6032,7 +6134,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6032
6134
|
size: character.size,
|
|
6033
6135
|
updatedAt: character.updated_at ?? null
|
|
6034
6136
|
}
|
|
6035
|
-
}
|
|
6137
|
+
},
|
|
6138
|
+
billed: await this.billedForJob(backgroundJobId)
|
|
6036
6139
|
};
|
|
6037
6140
|
}
|
|
6038
6141
|
/**
|
|
@@ -6044,7 +6147,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6044
6147
|
*/
|
|
6045
6148
|
async pollCharacterAnimation(job, context) {
|
|
6046
6149
|
const fps = context?.spec?.character?.animation?.fps ?? 8;
|
|
6047
|
-
const review = (frames, animationId, groupId) => ({
|
|
6150
|
+
const review = (frames, animationId, groupId, billed) => ({
|
|
6048
6151
|
status: "review-set",
|
|
6049
6152
|
objectId: `${job.characterId}#${groupId ?? animationId ?? job.name}`,
|
|
6050
6153
|
frameUrls: frames,
|
|
@@ -6060,7 +6163,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6060
6163
|
animationName: job.name,
|
|
6061
6164
|
direction: job.direction
|
|
6062
6165
|
}
|
|
6063
|
-
}
|
|
6166
|
+
},
|
|
6167
|
+
billed
|
|
6064
6168
|
});
|
|
6065
6169
|
let cleanedUp = job.jobIds.length === 0;
|
|
6066
6170
|
for (const id of job.jobIds) {
|
|
@@ -6080,17 +6184,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6080
6184
|
if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
|
|
6081
6185
|
const character2 = await this.client.getCharacter(job.characterId);
|
|
6082
6186
|
const group = findAnimation(character2, job.name, job.direction, animationId);
|
|
6083
|
-
return review(frames, animationId, group?.groupId ?? null);
|
|
6187
|
+
return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
|
|
6084
6188
|
}
|
|
6085
6189
|
}
|
|
6086
6190
|
const recorded = context?.metadata?.character?.animationId ?? null;
|
|
6087
6191
|
const character = await this.client.getCharacter(job.characterId);
|
|
6088
6192
|
const found = findAnimation(character, job.name, job.direction, recorded);
|
|
6089
|
-
if (found) return review(found.frames, found.animationId, found.groupId);
|
|
6193
|
+
if (found) return review(found.frames, found.animationId, found.groupId, null);
|
|
6090
6194
|
return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
|
|
6091
6195
|
}
|
|
6092
6196
|
async submit(spec, styleImages, context) {
|
|
6093
6197
|
this.validate(spec, styleImages);
|
|
6198
|
+
if (spec.revision) return this.submitRevision(spec);
|
|
6094
6199
|
if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
|
|
6095
6200
|
if (spec.generator === "pixflux") {
|
|
6096
6201
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
@@ -6119,7 +6224,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6119
6224
|
// unlike 1dir's {type, base64, format} payload.
|
|
6120
6225
|
styleImages: styleImages.map(({ base64, width, height }) => ({ base64, width, height }))
|
|
6121
6226
|
});
|
|
6122
|
-
return { jobId: res2.tile_id };
|
|
6227
|
+
return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
|
|
6123
6228
|
}
|
|
6124
6229
|
if (spec.generator === "1dir") {
|
|
6125
6230
|
const res2 = await this.client.create1Direction({
|
|
@@ -6128,7 +6233,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6128
6233
|
view: spec.view === "sidescroller" ? "sidescroller" : "top-down",
|
|
6129
6234
|
styleImages
|
|
6130
6235
|
});
|
|
6131
|
-
return { jobId: res2.object_id };
|
|
6236
|
+
return { jobId: res2.object_id, metadata: { backgroundJobId: res2.background_job_id } };
|
|
6132
6237
|
}
|
|
6133
6238
|
const res = await this.client.createMapObject({
|
|
6134
6239
|
description: spec.prompt,
|
|
@@ -6140,9 +6245,79 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6140
6245
|
detail: spec.detail,
|
|
6141
6246
|
seed: spec.seed
|
|
6142
6247
|
});
|
|
6143
|
-
return { jobId: res.object_id };
|
|
6248
|
+
return { jobId: res.object_id, metadata: { backgroundJobId: res.background_job_id } };
|
|
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
|
+
}
|
|
6304
|
+
/**
|
|
6305
|
+
* What a background job actually billed, once its own record is still
|
|
6306
|
+
* around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
|
|
6307
|
+
* (a map object's record expires in hours; see `pollMap`) means "not
|
|
6308
|
+
* known", not a failure worth surfacing.
|
|
6309
|
+
*/
|
|
6310
|
+
async billedForJob(backgroundJobId) {
|
|
6311
|
+
if (!backgroundJobId) return null;
|
|
6312
|
+
try {
|
|
6313
|
+
const job = await this.client.getBackgroundJob(backgroundJobId);
|
|
6314
|
+
return billedFromUsage(job.usage);
|
|
6315
|
+
} catch {
|
|
6316
|
+
return null;
|
|
6317
|
+
}
|
|
6144
6318
|
}
|
|
6145
6319
|
async poll(jobId, generator, context) {
|
|
6320
|
+
if (context?.spec?.revision) return this.pollRevision(jobId);
|
|
6146
6321
|
if (generator === "pixflux") {
|
|
6147
6322
|
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
6148
6323
|
if (existsSync7(file)) {
|
|
@@ -6154,16 +6329,17 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6154
6329
|
error: "pixflux result is no longer cached locally; re-run submit for this asset"
|
|
6155
6330
|
};
|
|
6156
6331
|
}
|
|
6157
|
-
if (generator === "map") return this.pollMap(jobId);
|
|
6158
|
-
if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature));
|
|
6332
|
+
if (generator === "map") return this.pollMap(jobId, context);
|
|
6333
|
+
if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature), context);
|
|
6159
6334
|
if (generator === "character") return this.pollCharacter(jobId, context);
|
|
6335
|
+
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
6160
6336
|
const obj = await this.client.getObject(jobId);
|
|
6161
6337
|
if (obj.status === "review") {
|
|
6162
|
-
return { status: "review", candidateUrls: obj.frame_urls ?? [] };
|
|
6338
|
+
return { status: "review", candidateUrls: obj.frame_urls ?? [], billed: await this.billedForJob(backgroundJobId) };
|
|
6163
6339
|
}
|
|
6164
6340
|
if (obj.status === "completed") {
|
|
6165
6341
|
const url = firstUrl(obj.rotation_urls) ?? obj.preview_url ?? null;
|
|
6166
|
-
return { status: "ready", objectId: obj.id, sourceUrl: url, sources: url ? [{ url }] : [] };
|
|
6342
|
+
return { status: "ready", objectId: obj.id, sourceUrl: url, sources: url ? [{ url }] : [], billed: await this.billedForJob(backgroundJobId) };
|
|
6167
6343
|
}
|
|
6168
6344
|
if (obj.status === "failed") return { status: "failed", error: "generation failed upstream" };
|
|
6169
6345
|
return {
|
|
@@ -6172,6 +6348,58 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6172
6348
|
etaSeconds: obj.eta_seconds ?? null
|
|
6173
6349
|
};
|
|
6174
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
|
+
}
|
|
6175
6403
|
/**
|
|
6176
6404
|
* Map objects need their own path because the `/map-objects/{id}` record is
|
|
6177
6405
|
* deleted upstream roughly 8 hours after creation while the image survives in
|
|
@@ -6179,7 +6407,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6179
6407
|
* `/objects` four months on, with `/map-objects` returning 404 for the same
|
|
6180
6408
|
* id. So a 404 here is not evidence the work is lost.
|
|
6181
6409
|
*/
|
|
6182
|
-
async pollMap(jobId) {
|
|
6410
|
+
async pollMap(jobId, context) {
|
|
6411
|
+
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
6183
6412
|
try {
|
|
6184
6413
|
const obj = await this.client.getMapObject(jobId);
|
|
6185
6414
|
if (obj.status === "completed" && obj.download_url) {
|
|
@@ -6187,7 +6416,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6187
6416
|
status: "ready",
|
|
6188
6417
|
objectId: jobId,
|
|
6189
6418
|
sourceUrl: obj.download_url,
|
|
6190
|
-
sources: [{ url: obj.download_url }]
|
|
6419
|
+
sources: [{ url: obj.download_url }],
|
|
6420
|
+
billed: await this.billedForJob(backgroundJobId)
|
|
6191
6421
|
};
|
|
6192
6422
|
}
|
|
6193
6423
|
if (obj.status === "failed") return { status: "failed", error: "generation failed upstream" };
|
|
@@ -6198,7 +6428,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6198
6428
|
const survivor = await this.client.getObject(jobId).catch(() => null);
|
|
6199
6429
|
const url = firstUrl(survivor?.rotation_urls) ?? survivor?.preview_url ?? null;
|
|
6200
6430
|
if (survivor?.status === "completed" && url) {
|
|
6201
|
-
return { status: "ready", objectId: survivor.id, sourceUrl: url, sources: [{ url }] };
|
|
6431
|
+
return { status: "ready", objectId: survivor.id, sourceUrl: url, sources: [{ url }], billed: null };
|
|
6202
6432
|
}
|
|
6203
6433
|
return {
|
|
6204
6434
|
status: "failed",
|
|
@@ -6211,12 +6441,14 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6211
6441
|
* `storage_urls` once finished. There is no `status` field to read and no
|
|
6212
6442
|
* progress percentage on offer, so "processing" here carries no ETA.
|
|
6213
6443
|
*/
|
|
6214
|
-
async pollTiles(tileId, connectable) {
|
|
6444
|
+
async pollTiles(tileId, connectable, context) {
|
|
6215
6445
|
try {
|
|
6216
6446
|
const set = await this.client.getTilesPro(tileId);
|
|
6217
6447
|
const tiles = tilesInIndexOrder(set.storage_urls);
|
|
6218
6448
|
if (!tiles.length) return { status: "failed", error: "tiles job returned no storage urls" };
|
|
6219
|
-
|
|
6449
|
+
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
6450
|
+
const billed = await this.billedForJob(backgroundJobId);
|
|
6451
|
+
if (!connectable) return { status: "review", candidateUrls: tiles.map((tile) => tile.url), billed };
|
|
6220
6452
|
return {
|
|
6221
6453
|
status: "ready",
|
|
6222
6454
|
objectId: tileId,
|
|
@@ -6228,7 +6460,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6228
6460
|
metadata: {
|
|
6229
6461
|
tileKind: set.kind,
|
|
6230
6462
|
...set.tile_rules ? { tileRules: set.tile_rules } : {}
|
|
6231
|
-
}
|
|
6463
|
+
},
|
|
6464
|
+
billed
|
|
6232
6465
|
};
|
|
6233
6466
|
} catch (err) {
|
|
6234
6467
|
if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
|
|
@@ -6358,6 +6591,15 @@ function firstUrl(urls) {
|
|
|
6358
6591
|
if (!urls) return null;
|
|
6359
6592
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
6360
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
|
+
}
|
|
6361
6603
|
function readReference(spec, reference) {
|
|
6362
6604
|
const images = {};
|
|
6363
6605
|
for (const [direction, image] of Object.entries(reference)) {
|
|
@@ -10722,6 +10964,7 @@ function retireGeneration(entry, retiredAt) {
|
|
|
10722
10964
|
downloadedAt: entry.downloadedAt,
|
|
10723
10965
|
cost: entry.cost,
|
|
10724
10966
|
costUnit: entry.costUnit,
|
|
10967
|
+
billed: entry.billed,
|
|
10725
10968
|
provider: entry.provider,
|
|
10726
10969
|
postprocess: entry.postprocess,
|
|
10727
10970
|
retiredAt
|
|
@@ -10812,6 +11055,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
10812
11055
|
downloadedAt: generation.downloadedAt,
|
|
10813
11056
|
cost: generation.cost,
|
|
10814
11057
|
costUnit: generation.costUnit,
|
|
11058
|
+
billed: generation.billed,
|
|
10815
11059
|
provider: generation.provider,
|
|
10816
11060
|
postprocess: generation.postprocess,
|
|
10817
11061
|
history
|
|
@@ -10870,12 +11114,15 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
10870
11114
|
height: generation.height,
|
|
10871
11115
|
cost: generation.cost,
|
|
10872
11116
|
costUnit: generation.costUnit,
|
|
11117
|
+
billed: generation.billed,
|
|
10873
11118
|
submittedAt: generation.submittedAt,
|
|
10874
11119
|
downloadedAt: generation.downloadedAt,
|
|
10875
11120
|
retiredAt: generation.retiredAt,
|
|
10876
11121
|
outputs,
|
|
10877
11122
|
cached: outputs.length > 0 && outputs.every((output) => output.url !== null),
|
|
10878
|
-
|
|
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
|
|
10879
11126
|
};
|
|
10880
11127
|
});
|
|
10881
11128
|
}
|
|
@@ -11113,6 +11360,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11113
11360
|
fps: quality?.frameSet?.fps ?? metadataFps(entry) ?? spec.quality?.fps ?? null,
|
|
11114
11361
|
cost: entry?.cost ?? 0,
|
|
11115
11362
|
costUnit: entry?.costUnit ?? spec.costUnit,
|
|
11363
|
+
billed: entry?.billed ?? null,
|
|
11116
11364
|
estimatedCost: spec.cost,
|
|
11117
11365
|
candidates: spec.candidates,
|
|
11118
11366
|
submittedAt: entry?.submittedAt ?? null,
|
|
@@ -11139,7 +11387,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11139
11387
|
editChanged,
|
|
11140
11388
|
editStatus,
|
|
11141
11389
|
editMeta,
|
|
11142
|
-
upstreamUrl: entry?.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11390
|
+
upstreamUrl: entry?.provider === "pixellab" && !spec.revision ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11143
11391
|
refreshable: Boolean(entry && entry.status === "downloaded" && entry.outputs.length && (entry.sourceUrls?.length || entry.sourceUrl)),
|
|
11144
11392
|
history: entry ? describeHistory(media, entry, cacheDir) : [],
|
|
11145
11393
|
tags: spec.tags,
|
|
@@ -11176,6 +11424,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11176
11424
|
fps: metadataFps(entry),
|
|
11177
11425
|
cost: entry.cost,
|
|
11178
11426
|
costUnit: entry.costUnit,
|
|
11427
|
+
billed: entry.billed,
|
|
11179
11428
|
estimatedCost: null,
|
|
11180
11429
|
candidates: null,
|
|
11181
11430
|
submittedAt: entry.submittedAt,
|
|
@@ -11202,7 +11451,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11202
11451
|
editChanged: [],
|
|
11203
11452
|
editStatus: null,
|
|
11204
11453
|
editMeta: null,
|
|
11205
|
-
upstreamUrl: entry.provider === "pixellab" ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11454
|
+
upstreamUrl: entry.provider === "pixellab" && !entry.revision ? pixelLabObjectUrl(entry.generator, entry.objectId) : null,
|
|
11206
11455
|
refreshable: false,
|
|
11207
11456
|
history: describeHistory(media, entry, cacheDir),
|
|
11208
11457
|
tags: [],
|
|
@@ -11818,7 +12067,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
11818
12067
|
...entry.providerMetadata[provider.id],
|
|
11819
12068
|
...state.metadata
|
|
11820
12069
|
}
|
|
11821
|
-
} : entry.providerMetadata
|
|
12070
|
+
} : entry.providerMetadata,
|
|
12071
|
+
billed: state.billed ?? entry.billed
|
|
11822
12072
|
});
|
|
11823
12073
|
result.review++;
|
|
11824
12074
|
log2(
|
|
@@ -11838,7 +12088,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
11838
12088
|
...state.metadata
|
|
11839
12089
|
}
|
|
11840
12090
|
} : entry.providerMetadata,
|
|
11841
|
-
error: null
|
|
12091
|
+
error: null,
|
|
12092
|
+
billed: state.billed ?? entry.billed
|
|
11842
12093
|
});
|
|
11843
12094
|
result.completed++;
|
|
11844
12095
|
log2(` ready ${key}`);
|
|
@@ -13746,6 +13997,10 @@ async function runLifecycle(args, wave, spent) {
|
|
|
13746
13997
|
}
|
|
13747
13998
|
|
|
13748
13999
|
// src/cli/commands/history.ts
|
|
14000
|
+
function billedNote(cost, costUnit, billed) {
|
|
14001
|
+
if (!billed || billed.unit === costUnit && billed.amount === cost) return "";
|
|
14002
|
+
return ` (billed ${formatCost(billed.unit, billed.amount)})`;
|
|
14003
|
+
}
|
|
13749
14004
|
async function runHistory(args) {
|
|
13750
14005
|
const { loaded, specs, lock } = await openProject2(args);
|
|
13751
14006
|
const limit = historyLimit(loaded.manifest);
|
|
@@ -13756,7 +14011,7 @@ async function runHistory(args) {
|
|
|
13756
14011
|
limit,
|
|
13757
14012
|
assets: selected.map(({ spec, entry }) => ({
|
|
13758
14013
|
key: lockKey(spec.styleId, spec.assetId),
|
|
13759
|
-
current: entry ? { objectId: entry.objectId, outputs: entry.outputs, downloadedAt: entry.downloadedAt, cost: entry.cost, costUnit: entry.costUnit } : null,
|
|
14014
|
+
current: entry ? { objectId: entry.objectId, outputs: entry.outputs, downloadedAt: entry.downloadedAt, cost: entry.cost, costUnit: entry.costUnit, billed: entry.billed } : null,
|
|
13760
14015
|
history: entry?.history ?? []
|
|
13761
14016
|
}))
|
|
13762
14017
|
}, null, 2));
|
|
@@ -13770,10 +14025,10 @@ async function runHistory(args) {
|
|
|
13770
14025
|
const key = lockKey(spec.styleId, spec.assetId);
|
|
13771
14026
|
log(`
|
|
13772
14027
|
${key}`);
|
|
13773
|
-
log(` current ${entry.outputs[0]?.sha256.slice(0, 12) ?? "\u2014"} ${entry.downloadedAt ?? ""} ${entry.objectId ?? ""}`);
|
|
14028
|
+
log(` current ${entry.outputs[0]?.sha256.slice(0, 12) ?? "\u2014"} ${entry.downloadedAt ?? ""} ${entry.objectId ?? ""}${billedNote(entry.cost, entry.costUnit, entry.billed)}`);
|
|
13774
14029
|
for (const [i, generation] of entry.history.entries()) {
|
|
13775
14030
|
const changed = generation.prompt !== entry.prompt ? " (different prompt)" : "";
|
|
13776
|
-
log(` #${String(i + 1).padEnd(2)} ${generation.outputs[0]?.sha256.slice(0, 12) ?? "\u2014"} ${generation.downloadedAt ?? ""} ${generation.objectId ?? ""}${changed}`);
|
|
14031
|
+
log(` #${String(i + 1).padEnd(2)} ${generation.outputs[0]?.sha256.slice(0, 12) ?? "\u2014"} ${generation.downloadedAt ?? ""} ${generation.objectId ?? ""}${changed}${billedNote(generation.cost, generation.costUnit, generation.billed)}`);
|
|
13777
14032
|
}
|
|
13778
14033
|
}
|
|
13779
14034
|
if (!shown) log(` no asset has a previous generation recorded${selected.length ? "" : " (nothing selected)"}`);
|
|
@@ -14468,21 +14723,29 @@ async function runPlan(args) {
|
|
|
14468
14723
|
actionable: group.actionable.map((item) => item.key)
|
|
14469
14724
|
})),
|
|
14470
14725
|
actionable: plan.actionable.map((i) => i.key),
|
|
14471
|
-
items: plan.items.map(({ key, state, reason, quality, spec }) =>
|
|
14472
|
-
key
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
|
|
14480
|
-
|
|
14481
|
-
|
|
14482
|
-
|
|
14483
|
-
|
|
14484
|
-
|
|
14485
|
-
|
|
14726
|
+
items: plan.items.map(({ key, state, reason, quality, spec }) => {
|
|
14727
|
+
const entry = lock.entries[key];
|
|
14728
|
+
const billedDiffers = entry?.billed && (entry.billed.unit !== entry.costUnit || entry.billed.amount !== entry.cost);
|
|
14729
|
+
return {
|
|
14730
|
+
key,
|
|
14731
|
+
state,
|
|
14732
|
+
reason,
|
|
14733
|
+
...spec.revision ? {
|
|
14734
|
+
revision: {
|
|
14735
|
+
mode: spec.revision.mode,
|
|
14736
|
+
from: spec.revision.sourceAssetId,
|
|
14737
|
+
sourceSha256: spec.revision.sourceSha256,
|
|
14738
|
+
...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
|
|
14739
|
+
...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
|
|
14740
|
+
}
|
|
14741
|
+
} : {},
|
|
14742
|
+
...quality ? { quality: { state: quality.state, reason: quality.reason } } : {},
|
|
14743
|
+
// The estimate a wave budget spent against; present alongside the
|
|
14744
|
+
// provider's actual charge only when `poll` has read one and it
|
|
14745
|
+
// differs, since that is the only time the gap is worth a look.
|
|
14746
|
+
...billedDiffers ? { cost: entry.cost, costUnit: entry.costUnit, billed: entry.billed } : {}
|
|
14747
|
+
};
|
|
14748
|
+
})
|
|
14486
14749
|
}, null, 2));
|
|
14487
14750
|
} else {
|
|
14488
14751
|
printPlan(plan);
|