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.
@@ -1050,7 +1050,9 @@ function historySection(item) {
1050
1050
  else thumb.append(el('span', null, g.cached ? '' : 'not cached'));
1051
1051
  const facts = el('div', 'facts');
1052
1052
  const head = el('div');
1053
- head.append(el('b', null, '#' + g.index), document.createTextNode(' · ' + fmtWhen(g.downloadedAt || g.submittedAt) + ' · ' + fmtCost(g.costUnit, g.cost) +
1053
+ const billedNote = g.billed && (g.billed.unit !== g.costUnit || g.billed.amount !== g.cost)
1054
+ ? ' (billed ' + fmtCost(g.billed.unit, g.billed.amount) + ')' : '';
1055
+ head.append(el('b', null, '#' + g.index), document.createTextNode(' · ' + fmtWhen(g.downloadedAt || g.submittedAt) + ' · ' + fmtCost(g.costUnit, g.cost) + billedNote +
1054
1056
  (g.outputs.length > 1 ? ' · ' + g.outputs.length + ' files' : '')));
1055
1057
  facts.append(head);
1056
1058
  const prompt = el('div', 'prompt' + (g.promptDiffers ? ' differs' : ''), (g.promptDiffers ? 'prompt: ' : 'same prompt: ') + g.prompt);
@@ -1876,6 +1878,9 @@ function renderDrawer() {
1876
1878
  if (item.currentPrompt) row(dl, 'prompt now', el('div', 'prompt state-warn', item.currentPrompt));
1877
1879
  row(dl, 'size', item.width + ' × ' + item.height + ' px');
1878
1880
  if (item.status) row(dl, 'cost', fmtCost(item.costUnit, item.cost));
1881
+ if (item.billed && (item.billed.unit !== item.costUnit || item.billed.amount !== item.cost)) {
1882
+ row(dl, 'billed', fmtCost(item.billed.unit, item.billed.amount));
1883
+ }
1879
1884
  if (item.postprocess && item.postprocess.palette) {
1880
1885
  // What fetch did to the provider's bytes before writing them.
1881
1886
  const p = item.postprocess.palette;
package/dist/index.cjs CHANGED
@@ -967,6 +967,8 @@ var LockHistoryEntrySchema = import_zod.z.object({
967
967
  downloadedAt: import_zod.z.string().nullable().default(null),
968
968
  cost: import_zod.z.number().finite().nonnegative().default(0),
969
969
  costUnit: import_zod.z.string().min(1).default("generations"),
970
+ /** What the provider actually billed, read from the completed job; null when never read. */
971
+ billed: import_zod.z.object({ amount: import_zod.z.number(), unit: import_zod.z.string() }).nullable().default(null),
970
972
  provider: import_zod.z.string().default("pixellab"),
971
973
  postprocess: PostprocessSchema.optional(),
972
974
  /** When the replacement was submitted. */
@@ -1058,6 +1060,14 @@ var LockEntrySchema = import_zod.z.object({
1058
1060
  cost: import_zod.z.number().finite().nonnegative().default(0),
1059
1061
  /** Unit for `cost`. Defaults preserve pre-unit PixelLab lockfiles. */
1060
1062
  costUnit: import_zod.z.string().min(1).default("generations"),
1063
+ /**
1064
+ * What the provider actually billed, read from the completed job rather
1065
+ * than estimated at submit time. Null until a poll reads it, and for a
1066
+ * provider or generator `poll` does not read it from at all; `cost` above
1067
+ * is still what a wave budget spends against, since the real number is
1068
+ * only known after the fact.
1069
+ */
1070
+ billed: import_zod.z.object({ amount: import_zod.z.number(), unit: import_zod.z.string() }).nullable().default(null),
1061
1071
  /** Which provider produced this. Absent on entries written before providers. */
1062
1072
  provider: import_zod.z.string().default("pixellab"),
1063
1073
  /** Generations this entry replaced, newest first; see LockHistoryEntrySchema. */
@@ -1311,7 +1321,13 @@ var CharacterListSchema = import_zod2.z.object({ characters: import_zod2.z.array
1311
1321
  var BackgroundJobSchema = import_zod2.z.object({
1312
1322
  id: import_zod2.z.string(),
1313
1323
  status: import_zod2.z.string(),
1314
- last_response: import_zod2.z.record(import_zod2.z.unknown()).nullable().optional()
1324
+ last_response: import_zod2.z.record(import_zod2.z.unknown()).nullable().optional(),
1325
+ /**
1326
+ * The billed amount, confirmed live for a `map` object and a standard
1327
+ * character base (docs/ENDPOINTS.md, "Limits and billing"); duplicated at
1328
+ * `last_response.billing_usage` on the jobs observed so far.
1329
+ */
1330
+ usage: UsageSchema
1315
1331
  }).passthrough();
1316
1332
  var DeleteAnimationsSchema = import_zod2.z.object({ success: import_zod2.z.boolean(), deleted_count: import_zod2.z.number().int().default(0), error: import_zod2.z.string().nullable().optional() }).passthrough();
1317
1333
  function validateResponse(schema, raw, operation) {
@@ -2406,6 +2422,14 @@ function pixelLabObjectUrl(generator, objectId) {
2406
2422
  }
2407
2423
  return null;
2408
2424
  }
2425
+ function billedFromUsage(usage) {
2426
+ if (!usage) return null;
2427
+ if (usage.type === "usd" || usage.type === void 0 && typeof usage.usd === "number") {
2428
+ return typeof usage.usd === "number" ? { amount: usage.usd, unit: "usd" } : null;
2429
+ }
2430
+ if (typeof usage.generations === "number") return { amount: usage.generations, unit: "generations" };
2431
+ return null;
2432
+ }
2409
2433
  function characterCost(spec) {
2410
2434
  const character = spec.character;
2411
2435
  if (!character) return generationCost(spec.width, spec.height, "1dir");
@@ -2776,6 +2800,7 @@ var PixelLabProvider = class _PixelLabProvider {
2776
2800
  if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
2777
2801
  const sources = rotationSources(character);
2778
2802
  if (!sources.length) return { status: "failed", error: "character completed with no rotation images" };
2803
+ const backgroundJobId = context?.metadata?.character?.backgroundJobId;
2779
2804
  return {
2780
2805
  status: "ready",
2781
2806
  objectId: character.id,
@@ -2791,7 +2816,8 @@ var PixelLabProvider = class _PixelLabProvider {
2791
2816
  size: character.size,
2792
2817
  updatedAt: character.updated_at ?? null
2793
2818
  }
2794
- }
2819
+ },
2820
+ billed: await this.billedForJob(backgroundJobId)
2795
2821
  };
2796
2822
  }
2797
2823
  /**
@@ -2803,7 +2829,7 @@ var PixelLabProvider = class _PixelLabProvider {
2803
2829
  */
2804
2830
  async pollCharacterAnimation(job, context) {
2805
2831
  const fps = context?.spec?.character?.animation?.fps ?? 8;
2806
- const review = (frames, animationId, groupId) => ({
2832
+ const review = (frames, animationId, groupId, billed) => ({
2807
2833
  status: "review-set",
2808
2834
  objectId: `${job.characterId}#${groupId ?? animationId ?? job.name}`,
2809
2835
  frameUrls: frames,
@@ -2819,7 +2845,8 @@ var PixelLabProvider = class _PixelLabProvider {
2819
2845
  animationName: job.name,
2820
2846
  direction: job.direction
2821
2847
  }
2822
- }
2848
+ },
2849
+ billed
2823
2850
  });
2824
2851
  let cleanedUp = job.jobIds.length === 0;
2825
2852
  for (const id of job.jobIds) {
@@ -2839,13 +2866,13 @@ var PixelLabProvider = class _PixelLabProvider {
2839
2866
  if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
2840
2867
  const character2 = await this.client.getCharacter(job.characterId);
2841
2868
  const group = findAnimation(character2, job.name, job.direction, animationId);
2842
- return review(frames, animationId, group?.groupId ?? null);
2869
+ return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
2843
2870
  }
2844
2871
  }
2845
2872
  const recorded = context?.metadata?.character?.animationId ?? null;
2846
2873
  const character = await this.client.getCharacter(job.characterId);
2847
2874
  const found = findAnimation(character, job.name, job.direction, recorded);
2848
- if (found) return review(found.frames, found.animationId, found.groupId);
2875
+ if (found) return review(found.frames, found.animationId, found.groupId, null);
2849
2876
  return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
2850
2877
  }
2851
2878
  async submit(spec, styleImages, context) {
@@ -2878,7 +2905,7 @@ var PixelLabProvider = class _PixelLabProvider {
2878
2905
  // unlike 1dir's {type, base64, format} payload.
2879
2906
  styleImages: styleImages.map(({ base64, width, height }) => ({ base64, width, height }))
2880
2907
  });
2881
- return { jobId: res2.tile_id };
2908
+ return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
2882
2909
  }
2883
2910
  if (spec.generator === "1dir") {
2884
2911
  const res2 = await this.client.create1Direction({
@@ -2887,7 +2914,7 @@ var PixelLabProvider = class _PixelLabProvider {
2887
2914
  view: spec.view === "sidescroller" ? "sidescroller" : "top-down",
2888
2915
  styleImages
2889
2916
  });
2890
- return { jobId: res2.object_id };
2917
+ return { jobId: res2.object_id, metadata: { backgroundJobId: res2.background_job_id } };
2891
2918
  }
2892
2919
  const res = await this.client.createMapObject({
2893
2920
  description: spec.prompt,
@@ -2899,7 +2926,22 @@ var PixelLabProvider = class _PixelLabProvider {
2899
2926
  detail: spec.detail,
2900
2927
  seed: spec.seed
2901
2928
  });
2902
- return { jobId: res.object_id };
2929
+ return { jobId: res.object_id, metadata: { backgroundJobId: res.background_job_id } };
2930
+ }
2931
+ /**
2932
+ * What a background job actually billed, once its own record is still
2933
+ * around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
2934
+ * (a map object's record expires in hours; see `pollMap`) means "not
2935
+ * known", not a failure worth surfacing.
2936
+ */
2937
+ async billedForJob(backgroundJobId) {
2938
+ if (!backgroundJobId) return null;
2939
+ try {
2940
+ const job = await this.client.getBackgroundJob(backgroundJobId);
2941
+ return billedFromUsage(job.usage);
2942
+ } catch {
2943
+ return null;
2944
+ }
2903
2945
  }
2904
2946
  async poll(jobId, generator, context) {
2905
2947
  if (generator === "pixflux") {
@@ -2913,16 +2955,17 @@ var PixelLabProvider = class _PixelLabProvider {
2913
2955
  error: "pixflux result is no longer cached locally; re-run submit for this asset"
2914
2956
  };
2915
2957
  }
2916
- if (generator === "map") return this.pollMap(jobId);
2917
- if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature));
2958
+ if (generator === "map") return this.pollMap(jobId, context);
2959
+ if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature), context);
2918
2960
  if (generator === "character") return this.pollCharacter(jobId, context);
2961
+ const backgroundJobId = context?.metadata?.backgroundJobId;
2919
2962
  const obj = await this.client.getObject(jobId);
2920
2963
  if (obj.status === "review") {
2921
- return { status: "review", candidateUrls: obj.frame_urls ?? [] };
2964
+ return { status: "review", candidateUrls: obj.frame_urls ?? [], billed: await this.billedForJob(backgroundJobId) };
2922
2965
  }
2923
2966
  if (obj.status === "completed") {
2924
2967
  const url = firstUrl(obj.rotation_urls) ?? obj.preview_url ?? null;
2925
- return { status: "ready", objectId: obj.id, sourceUrl: url, sources: url ? [{ url }] : [] };
2968
+ return { status: "ready", objectId: obj.id, sourceUrl: url, sources: url ? [{ url }] : [], billed: await this.billedForJob(backgroundJobId) };
2926
2969
  }
2927
2970
  if (obj.status === "failed") return { status: "failed", error: "generation failed upstream" };
2928
2971
  return {
@@ -2938,7 +2981,8 @@ var PixelLabProvider = class _PixelLabProvider {
2938
2981
  * `/objects` four months on, with `/map-objects` returning 404 for the same
2939
2982
  * id. So a 404 here is not evidence the work is lost.
2940
2983
  */
2941
- async pollMap(jobId) {
2984
+ async pollMap(jobId, context) {
2985
+ const backgroundJobId = context?.metadata?.backgroundJobId;
2942
2986
  try {
2943
2987
  const obj = await this.client.getMapObject(jobId);
2944
2988
  if (obj.status === "completed" && obj.download_url) {
@@ -2946,7 +2990,8 @@ var PixelLabProvider = class _PixelLabProvider {
2946
2990
  status: "ready",
2947
2991
  objectId: jobId,
2948
2992
  sourceUrl: obj.download_url,
2949
- sources: [{ url: obj.download_url }]
2993
+ sources: [{ url: obj.download_url }],
2994
+ billed: await this.billedForJob(backgroundJobId)
2950
2995
  };
2951
2996
  }
2952
2997
  if (obj.status === "failed") return { status: "failed", error: "generation failed upstream" };
@@ -2957,7 +3002,7 @@ var PixelLabProvider = class _PixelLabProvider {
2957
3002
  const survivor = await this.client.getObject(jobId).catch(() => null);
2958
3003
  const url = firstUrl(survivor?.rotation_urls) ?? survivor?.preview_url ?? null;
2959
3004
  if (survivor?.status === "completed" && url) {
2960
- return { status: "ready", objectId: survivor.id, sourceUrl: url, sources: [{ url }] };
3005
+ return { status: "ready", objectId: survivor.id, sourceUrl: url, sources: [{ url }], billed: null };
2961
3006
  }
2962
3007
  return {
2963
3008
  status: "failed",
@@ -2970,12 +3015,14 @@ var PixelLabProvider = class _PixelLabProvider {
2970
3015
  * `storage_urls` once finished. There is no `status` field to read and no
2971
3016
  * progress percentage on offer, so "processing" here carries no ETA.
2972
3017
  */
2973
- async pollTiles(tileId, connectable) {
3018
+ async pollTiles(tileId, connectable, context) {
2974
3019
  try {
2975
3020
  const set = await this.client.getTilesPro(tileId);
2976
3021
  const tiles = tilesInIndexOrder(set.storage_urls);
2977
3022
  if (!tiles.length) return { status: "failed", error: "tiles job returned no storage urls" };
2978
- if (!connectable) return { status: "review", candidateUrls: tiles.map((tile) => tile.url) };
3023
+ const backgroundJobId = context?.metadata?.backgroundJobId;
3024
+ const billed = await this.billedForJob(backgroundJobId);
3025
+ if (!connectable) return { status: "review", candidateUrls: tiles.map((tile) => tile.url), billed };
2979
3026
  return {
2980
3027
  status: "ready",
2981
3028
  objectId: tileId,
@@ -2987,7 +3034,8 @@ var PixelLabProvider = class _PixelLabProvider {
2987
3034
  metadata: {
2988
3035
  tileKind: set.kind,
2989
3036
  ...set.tile_rules ? { tileRules: set.tile_rules } : {}
2990
- }
3037
+ },
3038
+ billed
2991
3039
  };
2992
3040
  } catch (err) {
2993
3041
  if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
@@ -9228,6 +9276,7 @@ function retireGeneration(entry, retiredAt) {
9228
9276
  downloadedAt: entry.downloadedAt,
9229
9277
  cost: entry.cost,
9230
9278
  costUnit: entry.costUnit,
9279
+ billed: entry.billed,
9231
9280
  provider: entry.provider,
9232
9281
  postprocess: entry.postprocess,
9233
9282
  retiredAt
@@ -9318,6 +9367,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
9318
9367
  downloadedAt: generation.downloadedAt,
9319
9368
  cost: generation.cost,
9320
9369
  costUnit: generation.costUnit,
9370
+ billed: generation.billed,
9321
9371
  provider: generation.provider,
9322
9372
  postprocess: generation.postprocess,
9323
9373
  history
@@ -9523,15 +9573,27 @@ function packSprites(inputs, options = {}) {
9523
9573
  const sheetH = rows * cellH;
9524
9574
  const rgba = Buffer.alloc(sheetW * sheetH * 4);
9525
9575
  const frames = [];
9576
+ const pivot = options.pivot ?? "top-left";
9526
9577
  sprites.forEach((sprite, i) => {
9527
- const ox = i % columns * cellW;
9528
- const oy = Math.floor(i / columns) * cellH;
9578
+ const cellOx = i % columns * cellW;
9579
+ const cellOy = Math.floor(i / columns) * cellH;
9580
+ const offsetX = pivot === "bottom-center" ? Math.floor((cellW - sprite.width) / 2) : 0;
9581
+ const offsetY = pivot === "bottom-center" ? cellH - sprite.height : 0;
9582
+ const ox = cellOx + offsetX;
9583
+ const oy = cellOy + offsetY;
9529
9584
  for (let y = 0; y < sprite.height; y++) {
9530
9585
  const src = y * sprite.width * 4;
9531
9586
  const dst = ((oy + y) * sheetW + ox) * 4;
9532
9587
  sprite.pixels.copy(rgba, dst, src, src + sprite.width * 4);
9533
9588
  }
9534
- frames.push({ id: sprite.id, x: ox, y: oy, width: sprite.width, height: sprite.height });
9589
+ frames.push({
9590
+ id: sprite.id,
9591
+ x: ox,
9592
+ y: oy,
9593
+ width: sprite.width,
9594
+ height: sprite.height,
9595
+ ...offsetX || offsetY ? { offsetX, offsetY } : {}
9596
+ });
9535
9597
  });
9536
9598
  return {
9537
9599
  png: encodeRgbaPng(sheetW, sheetH, rgba),
@@ -10048,12 +10110,15 @@ function renderAsepriteSheet(atlas, opts) {
10048
10110
  }
10049
10111
  const frames = {};
10050
10112
  for (const frame of atlas.frames) {
10113
+ const offsetX = frame.offsetX ?? 0;
10114
+ const offsetY = frame.offsetY ?? 0;
10115
+ const pivoted = offsetX !== 0 || offsetY !== 0;
10051
10116
  frames[frame.id] = {
10052
10117
  frame: { x: frame.x, y: frame.y, w: frame.width, h: frame.height },
10053
10118
  rotated: false,
10054
- trimmed: false,
10055
- spriteSourceSize: { x: 0, y: 0, w: frame.width, h: frame.height },
10056
- sourceSize: { w: frame.width, h: frame.height },
10119
+ trimmed: pivoted,
10120
+ spriteSourceSize: pivoted ? { x: offsetX, y: offsetY, w: frame.width, h: frame.height } : { x: 0, y: 0, w: frame.width, h: frame.height },
10121
+ sourceSize: pivoted ? { w: atlas.cell.width, h: atlas.cell.height } : { w: frame.width, h: frame.height },
10057
10122
  duration: durations.get(frame.id) ?? ASEPRITE_DEFAULT_DURATION_MS
10058
10123
  };
10059
10124
  }
@@ -10384,7 +10449,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
10384
10449
  ...entry.providerMetadata[provider.id],
10385
10450
  ...state.metadata
10386
10451
  }
10387
- } : entry.providerMetadata
10452
+ } : entry.providerMetadata,
10453
+ billed: state.billed ?? entry.billed
10388
10454
  });
10389
10455
  result.review++;
10390
10456
  log(
@@ -10404,7 +10470,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
10404
10470
  ...state.metadata
10405
10471
  }
10406
10472
  } : entry.providerMetadata,
10407
- error: null
10473
+ error: null,
10474
+ billed: state.billed ?? entry.billed
10408
10475
  });
10409
10476
  result.completed++;
10410
10477
  log(` ready ${key}`);
@@ -12263,6 +12330,7 @@ function describeHistory(media, entry, cacheDir) {
12263
12330
  height: generation.height,
12264
12331
  cost: generation.cost,
12265
12332
  costUnit: generation.costUnit,
12333
+ billed: generation.billed,
12266
12334
  submittedAt: generation.submittedAt,
12267
12335
  downloadedAt: generation.downloadedAt,
12268
12336
  retiredAt: generation.retiredAt,
@@ -12506,6 +12574,7 @@ async function buildGallerySnapshot(opts) {
12506
12574
  fps: quality?.frameSet?.fps ?? metadataFps(entry) ?? spec.quality?.fps ?? null,
12507
12575
  cost: entry?.cost ?? 0,
12508
12576
  costUnit: entry?.costUnit ?? spec.costUnit,
12577
+ billed: entry?.billed ?? null,
12509
12578
  estimatedCost: spec.cost,
12510
12579
  candidates: spec.candidates,
12511
12580
  submittedAt: entry?.submittedAt ?? null,
@@ -12569,6 +12638,7 @@ async function buildGallerySnapshot(opts) {
12569
12638
  fps: metadataFps(entry),
12570
12639
  cost: entry.cost,
12571
12640
  costUnit: entry.costUnit,
12641
+ billed: entry.billed,
12572
12642
  estimatedCost: null,
12573
12643
  candidates: null,
12574
12644
  submittedAt: entry.submittedAt,