pixelkiln 0.47.0 → 0.48.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.js CHANGED
@@ -668,6 +668,8 @@ var LockHistoryEntrySchema = z.object({
668
668
  downloadedAt: z.string().nullable().default(null),
669
669
  cost: z.number().finite().nonnegative().default(0),
670
670
  costUnit: z.string().min(1).default("generations"),
671
+ /** What the provider actually billed, read from the completed job; null when never read. */
672
+ billed: z.object({ amount: z.number(), unit: z.string() }).nullable().default(null),
671
673
  provider: z.string().default("pixellab"),
672
674
  postprocess: PostprocessSchema.optional(),
673
675
  /** When the replacement was submitted. */
@@ -759,6 +761,14 @@ var LockEntrySchema = z.object({
759
761
  cost: z.number().finite().nonnegative().default(0),
760
762
  /** Unit for `cost`. Defaults preserve pre-unit PixelLab lockfiles. */
761
763
  costUnit: z.string().min(1).default("generations"),
764
+ /**
765
+ * What the provider actually billed, read from the completed job rather
766
+ * than estimated at submit time. Null until a poll reads it, and for a
767
+ * provider or generator `poll` does not read it from at all; `cost` above
768
+ * is still what a wave budget spends against, since the real number is
769
+ * only known after the fact.
770
+ */
771
+ billed: z.object({ amount: z.number(), unit: z.string() }).nullable().default(null),
762
772
  /** Which provider produced this. Absent on entries written before providers. */
763
773
  provider: z.string().default("pixellab"),
764
774
  /** Generations this entry replaced, newest first; see LockHistoryEntrySchema. */
@@ -1012,7 +1022,13 @@ var CharacterListSchema = z2.object({ characters: z2.array(CharacterSchema), tot
1012
1022
  var BackgroundJobSchema = z2.object({
1013
1023
  id: z2.string(),
1014
1024
  status: z2.string(),
1015
- last_response: z2.record(z2.unknown()).nullable().optional()
1025
+ last_response: z2.record(z2.unknown()).nullable().optional(),
1026
+ /**
1027
+ * The billed amount, confirmed live for a `map` object and a standard
1028
+ * character base (docs/ENDPOINTS.md, "Limits and billing"); duplicated at
1029
+ * `last_response.billing_usage` on the jobs observed so far.
1030
+ */
1031
+ usage: UsageSchema
1016
1032
  }).passthrough();
1017
1033
  var DeleteAnimationsSchema = z2.object({ success: z2.boolean(), deleted_count: z2.number().int().default(0), error: z2.string().nullable().optional() }).passthrough();
1018
1034
  function validateResponse(schema, raw, operation) {
@@ -2107,6 +2123,14 @@ function pixelLabObjectUrl(generator, objectId) {
2107
2123
  }
2108
2124
  return null;
2109
2125
  }
2126
+ function billedFromUsage(usage) {
2127
+ if (!usage) return null;
2128
+ if (usage.type === "usd" || usage.type === void 0 && typeof usage.usd === "number") {
2129
+ return typeof usage.usd === "number" ? { amount: usage.usd, unit: "usd" } : null;
2130
+ }
2131
+ if (typeof usage.generations === "number") return { amount: usage.generations, unit: "generations" };
2132
+ return null;
2133
+ }
2110
2134
  function characterCost(spec) {
2111
2135
  const character = spec.character;
2112
2136
  if (!character) return generationCost(spec.width, spec.height, "1dir");
@@ -2477,6 +2501,7 @@ var PixelLabProvider = class _PixelLabProvider {
2477
2501
  if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
2478
2502
  const sources = rotationSources(character);
2479
2503
  if (!sources.length) return { status: "failed", error: "character completed with no rotation images" };
2504
+ const backgroundJobId = context?.metadata?.character?.backgroundJobId;
2480
2505
  return {
2481
2506
  status: "ready",
2482
2507
  objectId: character.id,
@@ -2492,7 +2517,8 @@ var PixelLabProvider = class _PixelLabProvider {
2492
2517
  size: character.size,
2493
2518
  updatedAt: character.updated_at ?? null
2494
2519
  }
2495
- }
2520
+ },
2521
+ billed: await this.billedForJob(backgroundJobId)
2496
2522
  };
2497
2523
  }
2498
2524
  /**
@@ -2504,7 +2530,7 @@ var PixelLabProvider = class _PixelLabProvider {
2504
2530
  */
2505
2531
  async pollCharacterAnimation(job, context) {
2506
2532
  const fps = context?.spec?.character?.animation?.fps ?? 8;
2507
- const review = (frames, animationId, groupId) => ({
2533
+ const review = (frames, animationId, groupId, billed) => ({
2508
2534
  status: "review-set",
2509
2535
  objectId: `${job.characterId}#${groupId ?? animationId ?? job.name}`,
2510
2536
  frameUrls: frames,
@@ -2520,7 +2546,8 @@ var PixelLabProvider = class _PixelLabProvider {
2520
2546
  animationName: job.name,
2521
2547
  direction: job.direction
2522
2548
  }
2523
- }
2549
+ },
2550
+ billed
2524
2551
  });
2525
2552
  let cleanedUp = job.jobIds.length === 0;
2526
2553
  for (const id of job.jobIds) {
@@ -2540,13 +2567,13 @@ var PixelLabProvider = class _PixelLabProvider {
2540
2567
  if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
2541
2568
  const character2 = await this.client.getCharacter(job.characterId);
2542
2569
  const group = findAnimation(character2, job.name, job.direction, animationId);
2543
- return review(frames, animationId, group?.groupId ?? null);
2570
+ return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
2544
2571
  }
2545
2572
  }
2546
2573
  const recorded = context?.metadata?.character?.animationId ?? null;
2547
2574
  const character = await this.client.getCharacter(job.characterId);
2548
2575
  const found = findAnimation(character, job.name, job.direction, recorded);
2549
- if (found) return review(found.frames, found.animationId, found.groupId);
2576
+ if (found) return review(found.frames, found.animationId, found.groupId, null);
2550
2577
  return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
2551
2578
  }
2552
2579
  async submit(spec, styleImages, context) {
@@ -2579,7 +2606,7 @@ var PixelLabProvider = class _PixelLabProvider {
2579
2606
  // unlike 1dir's {type, base64, format} payload.
2580
2607
  styleImages: styleImages.map(({ base64, width, height }) => ({ base64, width, height }))
2581
2608
  });
2582
- return { jobId: res2.tile_id };
2609
+ return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
2583
2610
  }
2584
2611
  if (spec.generator === "1dir") {
2585
2612
  const res2 = await this.client.create1Direction({
@@ -2588,7 +2615,7 @@ var PixelLabProvider = class _PixelLabProvider {
2588
2615
  view: spec.view === "sidescroller" ? "sidescroller" : "top-down",
2589
2616
  styleImages
2590
2617
  });
2591
- return { jobId: res2.object_id };
2618
+ return { jobId: res2.object_id, metadata: { backgroundJobId: res2.background_job_id } };
2592
2619
  }
2593
2620
  const res = await this.client.createMapObject({
2594
2621
  description: spec.prompt,
@@ -2600,7 +2627,22 @@ var PixelLabProvider = class _PixelLabProvider {
2600
2627
  detail: spec.detail,
2601
2628
  seed: spec.seed
2602
2629
  });
2603
- return { jobId: res.object_id };
2630
+ return { jobId: res.object_id, metadata: { backgroundJobId: res.background_job_id } };
2631
+ }
2632
+ /**
2633
+ * What a background job actually billed, once its own record is still
2634
+ * around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
2635
+ * (a map object's record expires in hours; see `pollMap`) means "not
2636
+ * known", not a failure worth surfacing.
2637
+ */
2638
+ async billedForJob(backgroundJobId) {
2639
+ if (!backgroundJobId) return null;
2640
+ try {
2641
+ const job = await this.client.getBackgroundJob(backgroundJobId);
2642
+ return billedFromUsage(job.usage);
2643
+ } catch {
2644
+ return null;
2645
+ }
2604
2646
  }
2605
2647
  async poll(jobId, generator, context) {
2606
2648
  if (generator === "pixflux") {
@@ -2614,16 +2656,17 @@ var PixelLabProvider = class _PixelLabProvider {
2614
2656
  error: "pixflux result is no longer cached locally; re-run submit for this asset"
2615
2657
  };
2616
2658
  }
2617
- if (generator === "map") return this.pollMap(jobId);
2618
- if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature));
2659
+ if (generator === "map") return this.pollMap(jobId, context);
2660
+ if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature), context);
2619
2661
  if (generator === "character") return this.pollCharacter(jobId, context);
2662
+ const backgroundJobId = context?.metadata?.backgroundJobId;
2620
2663
  const obj = await this.client.getObject(jobId);
2621
2664
  if (obj.status === "review") {
2622
- return { status: "review", candidateUrls: obj.frame_urls ?? [] };
2665
+ return { status: "review", candidateUrls: obj.frame_urls ?? [], billed: await this.billedForJob(backgroundJobId) };
2623
2666
  }
2624
2667
  if (obj.status === "completed") {
2625
2668
  const url = firstUrl(obj.rotation_urls) ?? obj.preview_url ?? null;
2626
- return { status: "ready", objectId: obj.id, sourceUrl: url, sources: url ? [{ url }] : [] };
2669
+ return { status: "ready", objectId: obj.id, sourceUrl: url, sources: url ? [{ url }] : [], billed: await this.billedForJob(backgroundJobId) };
2627
2670
  }
2628
2671
  if (obj.status === "failed") return { status: "failed", error: "generation failed upstream" };
2629
2672
  return {
@@ -2639,7 +2682,8 @@ var PixelLabProvider = class _PixelLabProvider {
2639
2682
  * `/objects` four months on, with `/map-objects` returning 404 for the same
2640
2683
  * id. So a 404 here is not evidence the work is lost.
2641
2684
  */
2642
- async pollMap(jobId) {
2685
+ async pollMap(jobId, context) {
2686
+ const backgroundJobId = context?.metadata?.backgroundJobId;
2643
2687
  try {
2644
2688
  const obj = await this.client.getMapObject(jobId);
2645
2689
  if (obj.status === "completed" && obj.download_url) {
@@ -2647,7 +2691,8 @@ var PixelLabProvider = class _PixelLabProvider {
2647
2691
  status: "ready",
2648
2692
  objectId: jobId,
2649
2693
  sourceUrl: obj.download_url,
2650
- sources: [{ url: obj.download_url }]
2694
+ sources: [{ url: obj.download_url }],
2695
+ billed: await this.billedForJob(backgroundJobId)
2651
2696
  };
2652
2697
  }
2653
2698
  if (obj.status === "failed") return { status: "failed", error: "generation failed upstream" };
@@ -2658,7 +2703,7 @@ var PixelLabProvider = class _PixelLabProvider {
2658
2703
  const survivor = await this.client.getObject(jobId).catch(() => null);
2659
2704
  const url = firstUrl(survivor?.rotation_urls) ?? survivor?.preview_url ?? null;
2660
2705
  if (survivor?.status === "completed" && url) {
2661
- return { status: "ready", objectId: survivor.id, sourceUrl: url, sources: [{ url }] };
2706
+ return { status: "ready", objectId: survivor.id, sourceUrl: url, sources: [{ url }], billed: null };
2662
2707
  }
2663
2708
  return {
2664
2709
  status: "failed",
@@ -2671,12 +2716,14 @@ var PixelLabProvider = class _PixelLabProvider {
2671
2716
  * `storage_urls` once finished. There is no `status` field to read and no
2672
2717
  * progress percentage on offer, so "processing" here carries no ETA.
2673
2718
  */
2674
- async pollTiles(tileId, connectable) {
2719
+ async pollTiles(tileId, connectable, context) {
2675
2720
  try {
2676
2721
  const set = await this.client.getTilesPro(tileId);
2677
2722
  const tiles = tilesInIndexOrder(set.storage_urls);
2678
2723
  if (!tiles.length) return { status: "failed", error: "tiles job returned no storage urls" };
2679
- if (!connectable) return { status: "review", candidateUrls: tiles.map((tile) => tile.url) };
2724
+ const backgroundJobId = context?.metadata?.backgroundJobId;
2725
+ const billed = await this.billedForJob(backgroundJobId);
2726
+ if (!connectable) return { status: "review", candidateUrls: tiles.map((tile) => tile.url), billed };
2680
2727
  return {
2681
2728
  status: "ready",
2682
2729
  objectId: tileId,
@@ -2688,7 +2735,8 @@ var PixelLabProvider = class _PixelLabProvider {
2688
2735
  metadata: {
2689
2736
  tileKind: set.kind,
2690
2737
  ...set.tile_rules ? { tileRules: set.tile_rules } : {}
2691
- }
2738
+ },
2739
+ billed
2692
2740
  };
2693
2741
  } catch (err) {
2694
2742
  if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
@@ -8929,6 +8977,7 @@ function retireGeneration(entry, retiredAt) {
8929
8977
  downloadedAt: entry.downloadedAt,
8930
8978
  cost: entry.cost,
8931
8979
  costUnit: entry.costUnit,
8980
+ billed: entry.billed,
8932
8981
  provider: entry.provider,
8933
8982
  postprocess: entry.postprocess,
8934
8983
  retiredAt
@@ -9019,6 +9068,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
9019
9068
  downloadedAt: generation.downloadedAt,
9020
9069
  cost: generation.cost,
9021
9070
  costUnit: generation.costUnit,
9071
+ billed: generation.billed,
9022
9072
  provider: generation.provider,
9023
9073
  postprocess: generation.postprocess,
9024
9074
  history
@@ -9224,15 +9274,27 @@ function packSprites(inputs, options = {}) {
9224
9274
  const sheetH = rows * cellH;
9225
9275
  const rgba = Buffer.alloc(sheetW * sheetH * 4);
9226
9276
  const frames = [];
9277
+ const pivot = options.pivot ?? "top-left";
9227
9278
  sprites.forEach((sprite, i) => {
9228
- const ox = i % columns * cellW;
9229
- const oy = Math.floor(i / columns) * cellH;
9279
+ const cellOx = i % columns * cellW;
9280
+ const cellOy = Math.floor(i / columns) * cellH;
9281
+ const offsetX = pivot === "bottom-center" ? Math.floor((cellW - sprite.width) / 2) : 0;
9282
+ const offsetY = pivot === "bottom-center" ? cellH - sprite.height : 0;
9283
+ const ox = cellOx + offsetX;
9284
+ const oy = cellOy + offsetY;
9230
9285
  for (let y = 0; y < sprite.height; y++) {
9231
9286
  const src = y * sprite.width * 4;
9232
9287
  const dst = ((oy + y) * sheetW + ox) * 4;
9233
9288
  sprite.pixels.copy(rgba, dst, src, src + sprite.width * 4);
9234
9289
  }
9235
- frames.push({ id: sprite.id, x: ox, y: oy, width: sprite.width, height: sprite.height });
9290
+ frames.push({
9291
+ id: sprite.id,
9292
+ x: ox,
9293
+ y: oy,
9294
+ width: sprite.width,
9295
+ height: sprite.height,
9296
+ ...offsetX || offsetY ? { offsetX, offsetY } : {}
9297
+ });
9236
9298
  });
9237
9299
  return {
9238
9300
  png: encodeRgbaPng(sheetW, sheetH, rgba),
@@ -9749,12 +9811,15 @@ function renderAsepriteSheet(atlas, opts) {
9749
9811
  }
9750
9812
  const frames = {};
9751
9813
  for (const frame of atlas.frames) {
9814
+ const offsetX = frame.offsetX ?? 0;
9815
+ const offsetY = frame.offsetY ?? 0;
9816
+ const pivoted = offsetX !== 0 || offsetY !== 0;
9752
9817
  frames[frame.id] = {
9753
9818
  frame: { x: frame.x, y: frame.y, w: frame.width, h: frame.height },
9754
9819
  rotated: false,
9755
- trimmed: false,
9756
- spriteSourceSize: { x: 0, y: 0, w: frame.width, h: frame.height },
9757
- sourceSize: { w: frame.width, h: frame.height },
9820
+ trimmed: pivoted,
9821
+ spriteSourceSize: pivoted ? { x: offsetX, y: offsetY, w: frame.width, h: frame.height } : { x: 0, y: 0, w: frame.width, h: frame.height },
9822
+ sourceSize: pivoted ? { w: atlas.cell.width, h: atlas.cell.height } : { w: frame.width, h: frame.height },
9758
9823
  duration: durations.get(frame.id) ?? ASEPRITE_DEFAULT_DURATION_MS
9759
9824
  };
9760
9825
  }
@@ -10085,7 +10150,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
10085
10150
  ...entry.providerMetadata[provider.id],
10086
10151
  ...state.metadata
10087
10152
  }
10088
- } : entry.providerMetadata
10153
+ } : entry.providerMetadata,
10154
+ billed: state.billed ?? entry.billed
10089
10155
  });
10090
10156
  result.review++;
10091
10157
  log(
@@ -10105,7 +10171,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
10105
10171
  ...state.metadata
10106
10172
  }
10107
10173
  } : entry.providerMetadata,
10108
- error: null
10174
+ error: null,
10175
+ billed: state.billed ?? entry.billed
10109
10176
  });
10110
10177
  result.completed++;
10111
10178
  log(` ready ${key}`);
@@ -11964,6 +12031,7 @@ function describeHistory(media, entry, cacheDir) {
11964
12031
  height: generation.height,
11965
12032
  cost: generation.cost,
11966
12033
  costUnit: generation.costUnit,
12034
+ billed: generation.billed,
11967
12035
  submittedAt: generation.submittedAt,
11968
12036
  downloadedAt: generation.downloadedAt,
11969
12037
  retiredAt: generation.retiredAt,
@@ -12207,6 +12275,7 @@ async function buildGallerySnapshot(opts) {
12207
12275
  fps: quality?.frameSet?.fps ?? metadataFps(entry) ?? spec.quality?.fps ?? null,
12208
12276
  cost: entry?.cost ?? 0,
12209
12277
  costUnit: entry?.costUnit ?? spec.costUnit,
12278
+ billed: entry?.billed ?? null,
12210
12279
  estimatedCost: spec.cost,
12211
12280
  candidates: spec.candidates,
12212
12281
  submittedAt: entry?.submittedAt ?? null,
@@ -12270,6 +12339,7 @@ async function buildGallerySnapshot(opts) {
12270
12339
  fps: metadataFps(entry),
12271
12340
  cost: entry.cost,
12272
12341
  costUnit: entry.costUnit,
12342
+ billed: entry.billed,
12273
12343
  estimatedCost: null,
12274
12344
  candidates: null,
12275
12345
  submittedAt: entry.submittedAt,