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