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 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
- upstreamUrl: generation.provider === "pixellab" ? pixelLabObjectUrl(generation.generator, generation.objectId) : null
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: [],