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/cli.js +131 -46
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +6 -1
- package/dist/index.cjs +97 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +203 -6
- package/dist/index.d.ts +203 -6
- package/dist/index.js +97 -27
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +12 -6
- package/docs/ARTIFACTS.md +8 -1
- package/docs/CHARACTERS.md +5 -1
- package/docs/CLI.md +10 -1
- package/docs/ENDPOINTS.md +11 -1
- package/docs/GENERATORS.md +4 -0
- package/docs/PIXELLAB.md +15 -7
- package/package.json +1 -1
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
|
-
|
|
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
|
|
9229
|
-
const
|
|
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({
|
|
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:
|
|
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,
|