pixelkiln 0.58.0 → 0.60.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 +369 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +375 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1265 -145
- package/dist/index.d.ts +1265 -145
- package/dist/index.js +372 -22
- package/dist/index.js.map +1 -1
- package/docs/ENDPOINTS.md +104 -15
- package/docs/GENERATORS.md +104 -0
- package/docs/MANIFEST.md +4 -1
- package/docs/REVISIONS.md +21 -21
- package/package.json +1 -1
- package/schema/manifest.schema.json +174 -2
- package/schema/recipe.schema.json +6 -1
- package/skills/pixelkiln/references/pixellab-roadmap.md +69 -29
- package/skills/pixelkiln/references/pixellab.md +65 -4
- package/skills/pixelkiln/references/revisions.md +3 -2
package/dist/cli.js
CHANGED
|
@@ -645,16 +645,20 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
645
645
|
detail: spec.detail ?? null,
|
|
646
646
|
seed: spec.seed ?? null,
|
|
647
647
|
palette: spec.palette,
|
|
648
|
-
// `noBackground` only reaches the wire for pixflux; the
|
|
649
|
-
// undefined for every other generator. `tileSize` is
|
|
650
|
-
// absent; width/height are derived from it, so it is
|
|
651
|
-
|
|
648
|
+
// `noBackground` only reaches the wire for pixflux and uiAsset; the
|
|
649
|
+
// tile fields are undefined for every other generator. `tileSize` is
|
|
650
|
+
// intentionally absent; width/height are derived from it, so it is
|
|
651
|
+
// already covered.
|
|
652
|
+
noBackground: spec.generator === "pixflux" || spec.generator === "uiAsset" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
652
653
|
tileType: spec.tileType,
|
|
653
654
|
tileView: spec.tileView,
|
|
654
655
|
tileFeature: spec.tileFeature,
|
|
655
656
|
outlineMode: spec.outlineMode,
|
|
656
657
|
isometricTileSize: spec.isometricTileSize,
|
|
657
658
|
isometricTileShape: spec.isometricTileShape,
|
|
659
|
+
uiPieces: spec.uiPieces,
|
|
660
|
+
uiElements: spec.uiElements,
|
|
661
|
+
uiColorPalette: spec.uiColorPalette,
|
|
658
662
|
styleImages: styleImageHashes,
|
|
659
663
|
// A character family: the engine and rotations of a base, the parent's
|
|
660
664
|
// generated bytes for a state or animation (so a regenerated parent
|
|
@@ -703,6 +707,11 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
703
707
|
// A mirror's bytes come from its source's recorded outputs; the plan
|
|
704
708
|
// compares those directly, so only the choice of source is identity.
|
|
705
709
|
mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
|
|
710
|
+
// The whole group's ordered descriptions, not just this asset's own —
|
|
711
|
+
// any sibling's prompt changing, or a member being added or removed,
|
|
712
|
+
// must mark every member stale together, since they all ride on one
|
|
713
|
+
// submitted job with no way to amend it after the fact.
|
|
714
|
+
batch: spec.batch ? { role: spec.batch.role, itemDescriptions: spec.batch.itemDescriptions, index: spec.batch.index } : void 0,
|
|
706
715
|
revision: spec.revision ? {
|
|
707
716
|
mode: spec.revision.mode,
|
|
708
717
|
from: spec.revision.sourceAssetId,
|
|
@@ -735,7 +744,7 @@ import path2 from "path";
|
|
|
735
744
|
// src/types.ts
|
|
736
745
|
import { z } from "zod";
|
|
737
746
|
var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
|
|
738
|
-
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro"]);
|
|
747
|
+
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset"]);
|
|
739
748
|
var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
|
|
740
749
|
var RevisionModeSchema = z.enum([
|
|
741
750
|
"image-to-image",
|
|
@@ -857,6 +866,56 @@ var CharacterProportionsSchema = z.union([
|
|
|
857
866
|
}).strict()
|
|
858
867
|
]);
|
|
859
868
|
var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
|
|
869
|
+
var UiPieceSchema = z.discriminatedUnion("kind", [
|
|
870
|
+
z.object({
|
|
871
|
+
id: z.string().min(1),
|
|
872
|
+
kind: z.literal("rounded_rect"),
|
|
873
|
+
label: z.string().optional(),
|
|
874
|
+
x: z.number(),
|
|
875
|
+
y: z.number(),
|
|
876
|
+
w: z.number(),
|
|
877
|
+
h: z.number(),
|
|
878
|
+
radius: z.number().min(0).optional()
|
|
879
|
+
}).strict(),
|
|
880
|
+
z.object({
|
|
881
|
+
id: z.string().min(1),
|
|
882
|
+
kind: z.literal("circle"),
|
|
883
|
+
label: z.string().optional(),
|
|
884
|
+
x: z.number(),
|
|
885
|
+
y: z.number(),
|
|
886
|
+
r: z.number()
|
|
887
|
+
}).strict(),
|
|
888
|
+
z.object({
|
|
889
|
+
id: z.string().min(1),
|
|
890
|
+
kind: z.literal("polygon"),
|
|
891
|
+
label: z.string().optional(),
|
|
892
|
+
x: z.number(),
|
|
893
|
+
y: z.number(),
|
|
894
|
+
r: z.number(),
|
|
895
|
+
sides: z.number().int().min(3),
|
|
896
|
+
phase: z.number().optional()
|
|
897
|
+
}).strict()
|
|
898
|
+
]);
|
|
899
|
+
var UiElementSchema = z.enum([
|
|
900
|
+
"button",
|
|
901
|
+
"icon_button",
|
|
902
|
+
"toolbar",
|
|
903
|
+
"tab",
|
|
904
|
+
"panel",
|
|
905
|
+
"window",
|
|
906
|
+
"health_bar",
|
|
907
|
+
"avatar",
|
|
908
|
+
"triangle",
|
|
909
|
+
"pentagon",
|
|
910
|
+
"hexagon",
|
|
911
|
+
"octagon"
|
|
912
|
+
]);
|
|
913
|
+
var AssetBatchSchema = z.object({
|
|
914
|
+
/** The batch leader: another `1dir` asset in the same style. */
|
|
915
|
+
of: z.string().min(1),
|
|
916
|
+
/** 1-based slot in the batch (the leader is implicitly slot 0). Unique and contiguous among siblings. */
|
|
917
|
+
index: z.number().int().min(1)
|
|
918
|
+
}).strict();
|
|
860
919
|
var CharacterStateSchema = z.object({
|
|
861
920
|
/** The base character, or another state, in the same style. */
|
|
862
921
|
of: z.string().min(1),
|
|
@@ -1153,6 +1212,12 @@ var StyleObjectSchema = z.object({
|
|
|
1153
1212
|
* in place of `terrainShapeStyle`. The API default is `standard`.
|
|
1154
1213
|
*/
|
|
1155
1214
|
terrainMode: z.enum(["standard", "pro"]).optional(),
|
|
1215
|
+
/**
|
|
1216
|
+
* `uiAsset` generator only. A natural-language palette hint sent as-is
|
|
1217
|
+
* (e.g. "brown and gold"), distinct from `palette`'s hex-color array —
|
|
1218
|
+
* this is a prompt-level steer, not a local quantization target.
|
|
1219
|
+
*/
|
|
1220
|
+
uiColorPalette: z.string().max(200).optional(),
|
|
1156
1221
|
/**
|
|
1157
1222
|
* `terrain` generator only, `terrainMode: "standard"`. Procedural
|
|
1158
1223
|
* boundary geometry: `square` or `round`, 16px or 32px tiles only.
|
|
@@ -1430,6 +1495,16 @@ var AssetSchema = z.object({
|
|
|
1430
1495
|
* `prompt` instead.
|
|
1431
1496
|
*/
|
|
1432
1497
|
animation: CharacterAnimationSchema.optional(),
|
|
1498
|
+
/**
|
|
1499
|
+
* `1dir` styles only: this asset rides along on another `1dir` asset's
|
|
1500
|
+
* batch submission instead of generating on its own. See
|
|
1501
|
+
* `AssetBatchSchema`.
|
|
1502
|
+
*/
|
|
1503
|
+
batch: AssetBatchSchema.optional(),
|
|
1504
|
+
/** `uiAsset` styles: precise shape regions the panel is composited from. Combine with `elements`; omit both for a default full-canvas panel. */
|
|
1505
|
+
pieces: z.array(UiPieceSchema).min(1).optional(),
|
|
1506
|
+
/** `uiAsset` styles: named, auto-positioned UI element scaffolds. Combine with `pieces`; omit both for a default full-canvas panel. */
|
|
1507
|
+
elements: z.array(UiElementSchema).min(1).optional(),
|
|
1433
1508
|
/** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
|
|
1434
1509
|
proportions: CharacterProportionsSchema.optional(),
|
|
1435
1510
|
/**
|
|
@@ -1506,7 +1581,7 @@ var AssetSchema = z.object({
|
|
|
1506
1581
|
path: ["revision"]
|
|
1507
1582
|
});
|
|
1508
1583
|
}
|
|
1509
|
-
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
|
|
1584
|
+
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
|
|
1510
1585
|
if (shapes.length > 1) {
|
|
1511
1586
|
context.addIssue({
|
|
1512
1587
|
code: z.ZodIssueCode.custom,
|
|
@@ -1657,6 +1732,12 @@ var LockEntrySchema = z.object({
|
|
|
1657
1732
|
sourceAssetId: z.string().min(1),
|
|
1658
1733
|
sourceSha256: z.string().regex(/^[0-9a-f]{64}$/)
|
|
1659
1734
|
}).strict().nullable().default(null),
|
|
1735
|
+
/** For a `1dir` batch member: which leader and slot it rode along on. */
|
|
1736
|
+
batch: z.object({
|
|
1737
|
+
role: z.enum(["leader", "member"]),
|
|
1738
|
+
leaderAssetId: z.string().min(1).optional(),
|
|
1739
|
+
index: z.number().int().min(1).optional()
|
|
1740
|
+
}).strict().nullable().default(null),
|
|
1660
1741
|
/**
|
|
1661
1742
|
* Output hashes owned by the previous generation while its replacement is
|
|
1662
1743
|
* pending. They authorize replacing only unchanged PixelKiln-owned files.
|
|
@@ -5453,6 +5534,19 @@ var IsometricTileGetSchema = z2.object({
|
|
|
5453
5534
|
image: z2.object({ base64: z2.string().min(1), format: z2.string().default("png") }).passthrough(),
|
|
5454
5535
|
usage: UsageSchema
|
|
5455
5536
|
}).passthrough();
|
|
5537
|
+
var UiAssetSubmitSchema = z2.object({
|
|
5538
|
+
ui_asset_id: z2.string().min(1),
|
|
5539
|
+
background_job_id: z2.string().min(1),
|
|
5540
|
+
status: z2.string().default("processing"),
|
|
5541
|
+
usage: UsageSchema
|
|
5542
|
+
}).passthrough();
|
|
5543
|
+
var UiAssetGetSchema = z2.object({
|
|
5544
|
+
id: z2.string().min(1),
|
|
5545
|
+
status: z2.string().nullable().default(null),
|
|
5546
|
+
image_url: z2.string().nullable().optional(),
|
|
5547
|
+
progress_percent: z2.number().nullable().optional(),
|
|
5548
|
+
eta_seconds: z2.number().nullable().optional()
|
|
5549
|
+
}).passthrough();
|
|
5456
5550
|
var RevisionJobSubmitSchema = z2.object({
|
|
5457
5551
|
background_job_id: z2.string().min(1),
|
|
5458
5552
|
status: z2.string().default("processing")
|
|
@@ -5813,6 +5907,48 @@ var PixelLabClient = class {
|
|
|
5813
5907
|
);
|
|
5814
5908
|
return { image: res.image, usage: res.usage ?? null };
|
|
5815
5909
|
}
|
|
5910
|
+
/**
|
|
5911
|
+
* `/create-ui-asset`: one composited panel image from a shape template —
|
|
5912
|
+
* `pieces` (precise rect/circle/polygon regions on a virtual 0–512
|
|
5913
|
+
* editor canvas) and/or named `elements` (auto-positioned scaffolds:
|
|
5914
|
+
* button, icon_button, toolbar, tab, panel, window, health_bar, avatar,
|
|
5915
|
+
* triangle, pentagon, hexagon, octagon), or neither for a default
|
|
5916
|
+
* full-canvas rounded-rect panel. Unlike every other generator here, the
|
|
5917
|
+
* result is one flat image with no per-piece sub-regions or nine-slice
|
|
5918
|
+
* metadata returned — cropping a `pieces` layout into separate files
|
|
5919
|
+
* would be pixelkiln's own local work, not modeled yet.
|
|
5920
|
+
*/
|
|
5921
|
+
async createUiAsset(args) {
|
|
5922
|
+
const body = {
|
|
5923
|
+
description: args.description,
|
|
5924
|
+
image_size: { width: args.width, height: args.height }
|
|
5925
|
+
};
|
|
5926
|
+
if (args.pieces?.length) body.pieces = args.pieces;
|
|
5927
|
+
if (args.elements?.length) body.elements = args.elements;
|
|
5928
|
+
if (args.styleImage) body.style_image = args.styleImage;
|
|
5929
|
+
if (args.colorPalette) body.color_palette = args.colorPalette;
|
|
5930
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
5931
|
+
if (args.seed != null) body.seed = args.seed;
|
|
5932
|
+
const res = await validateResponse(
|
|
5933
|
+
UiAssetSubmitSchema,
|
|
5934
|
+
await this.request("/create-ui-asset", { method: "POST", body: JSON.stringify(body) }),
|
|
5935
|
+
"create ui asset"
|
|
5936
|
+
);
|
|
5937
|
+
return { ui_asset_id: res.ui_asset_id, background_job_id: res.background_job_id, usage: res.usage };
|
|
5938
|
+
}
|
|
5939
|
+
async getUiAsset(uiAssetId) {
|
|
5940
|
+
const res = await validateResponse(
|
|
5941
|
+
UiAssetGetSchema,
|
|
5942
|
+
await this.request(`/ui-assets/${encodeURIComponent(uiAssetId)}`),
|
|
5943
|
+
"get ui asset"
|
|
5944
|
+
);
|
|
5945
|
+
return {
|
|
5946
|
+
status: res.status,
|
|
5947
|
+
imageUrl: res.image_url ?? null,
|
|
5948
|
+
progressPercent: res.progress_percent ?? null,
|
|
5949
|
+
etaSeconds: res.eta_seconds ?? null
|
|
5950
|
+
};
|
|
5951
|
+
}
|
|
5816
5952
|
/**
|
|
5817
5953
|
* Synchronous single-image generation. Returns the PNG inline rather than a
|
|
5818
5954
|
* job id, and is the only endpoint that honours a forced palette;
|
|
@@ -6219,14 +6355,15 @@ var PixelLabClient = class {
|
|
|
6219
6355
|
/**
|
|
6220
6356
|
* `/reduce-colors`, PixelLab's "Cleanup" tier: quantize one image onto a
|
|
6221
6357
|
* smaller palette, synchronously — no `background_job_id`, the result
|
|
6222
|
-
* comes back in this same response, like `createImagePixflux`.
|
|
6223
|
-
*
|
|
6224
|
-
*
|
|
6225
|
-
*
|
|
6226
|
-
*
|
|
6227
|
-
*
|
|
6228
|
-
*
|
|
6229
|
-
*
|
|
6358
|
+
* comes back in this same response, like `createImagePixflux`. The
|
|
6359
|
+
* schema's own response example is dollar-denominated (`usage: {type:
|
|
6360
|
+
* "usd", usd: 0.02}`), which — matching this codebase's repeated
|
|
6361
|
+
* experience with PixelLab's documented-vs-billed cost mismatches
|
|
6362
|
+
* (`isometricTile`, `objectPro`) — did not hold: confirmed live against a
|
|
6363
|
+
* Tier 2 account at exactly 0.1 generations for a 32x32 source
|
|
6364
|
+
* (docs/REVISIONS.md). `numColors` and `paletteImage` are mutually
|
|
6365
|
+
* exclusive upstream; the manifest schema already enforces that before
|
|
6366
|
+
* this is ever called.
|
|
6230
6367
|
*/
|
|
6231
6368
|
async reduceColors(args) {
|
|
6232
6369
|
const body = { images: [args.image] };
|
|
@@ -6249,8 +6386,9 @@ var PixelLabClient = class {
|
|
|
6249
6386
|
/**
|
|
6250
6387
|
* `/correct-pixelart`, PixelLab's "Cleanup" tier: sharpen edges and drop
|
|
6251
6388
|
* stray pixels without resizing, synchronously — same shape as
|
|
6252
|
-
* `reduceColors` above, no background job. Cost is likewise
|
|
6253
|
-
*
|
|
6389
|
+
* `reduceColors` above, no background job. Cost is likewise confirmed
|
|
6390
|
+
* live at 0.1 generations for a 32x32 source, not the schema's own
|
|
6391
|
+
* dollar-denominated example (`usage: {type: "usd", usd: 0.02}`).
|
|
6254
6392
|
*/
|
|
6255
6393
|
async correctPixelart(args) {
|
|
6256
6394
|
const body = { images: [args.image] };
|
|
@@ -6481,7 +6619,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6481
6619
|
return new _PixelLabProvider(new PixelLabClient("download-only"));
|
|
6482
6620
|
}
|
|
6483
6621
|
supports(generator) {
|
|
6484
|
-
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro";
|
|
6622
|
+
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset";
|
|
6485
6623
|
}
|
|
6486
6624
|
/**
|
|
6487
6625
|
* `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
|
|
@@ -6518,6 +6656,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6518
6656
|
return dir;
|
|
6519
6657
|
}
|
|
6520
6658
|
estimate(spec) {
|
|
6659
|
+
if (spec.batch?.role === "member") {
|
|
6660
|
+
return { unit: "generations", amount: 0, candidates: 1 };
|
|
6661
|
+
}
|
|
6521
6662
|
if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
|
|
6522
6663
|
return { unit: "generations", amount: 0.1, candidates: 1 };
|
|
6523
6664
|
}
|
|
@@ -6594,6 +6735,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6594
6735
|
`PixelLab imagePro is ${spec.width}x${spec.height}; the API takes 16 to 792 wide and 16 to 688 tall (the exact ceiling also depends on aspect ratio)`
|
|
6595
6736
|
);
|
|
6596
6737
|
}
|
|
6738
|
+
if (spec.generator === "uiAsset" && (spec.width < 192 || spec.height < 192 || spec.width > 688 || spec.height > 688)) {
|
|
6739
|
+
throw new Error(
|
|
6740
|
+
`PixelLab uiAsset is ${spec.width}x${spec.height}; the API takes 192 to 688 pixels per side (the exact ceiling also depends on aspect ratio \u2014 square tops out at 512, 16:9 at 688x384, 9:16 at 384x688, 4:3 at 600x448, 3:4 at 448x600)`
|
|
6741
|
+
);
|
|
6742
|
+
}
|
|
6597
6743
|
if (spec.generator === "map") {
|
|
6598
6744
|
requirePixelLabOption("map", "view", spec.view, ["low top-down", "high top-down", "side"]);
|
|
6599
6745
|
requirePixelLabOption("map", "outline", spec.outline, [
|
|
@@ -7231,6 +7377,20 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7231
7377
|
if (spec.revision) return this.submitRevision(spec);
|
|
7232
7378
|
if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
|
|
7233
7379
|
if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
|
|
7380
|
+
if (spec.generator === "uiAsset") {
|
|
7381
|
+
const res2 = await this.client.createUiAsset({
|
|
7382
|
+
description: spec.prompt,
|
|
7383
|
+
width: spec.width,
|
|
7384
|
+
height: spec.height,
|
|
7385
|
+
pieces: spec.uiPieces,
|
|
7386
|
+
elements: spec.uiElements,
|
|
7387
|
+
styleImage: styleImages[0] ? { base64: styleImages[0].base64, format: styleImages[0].format } : void 0,
|
|
7388
|
+
colorPalette: spec.uiColorPalette,
|
|
7389
|
+
noBackground: spec.noBackground,
|
|
7390
|
+
seed: spec.seed
|
|
7391
|
+
});
|
|
7392
|
+
return { jobId: res2.ui_asset_id, metadata: { backgroundJobId: res2.background_job_id } };
|
|
7393
|
+
}
|
|
7234
7394
|
if (spec.generator === "pixflux") {
|
|
7235
7395
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
7236
7396
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -7316,11 +7476,21 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7316
7476
|
return { jobId: res2.background_job_id };
|
|
7317
7477
|
}
|
|
7318
7478
|
if (spec.generator === "1dir") {
|
|
7479
|
+
if (spec.batch?.role === "member") {
|
|
7480
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: a batch member cannot submit on its own`);
|
|
7481
|
+
}
|
|
7319
7482
|
const res2 = await this.client.create1Direction({
|
|
7320
7483
|
description: spec.prompt,
|
|
7321
7484
|
size: spec.size,
|
|
7322
7485
|
view: spec.view === "sidescroller" ? "sidescroller" : "top-down",
|
|
7323
|
-
styleImages
|
|
7486
|
+
styleImages,
|
|
7487
|
+
// The leader's own subject is item_descriptions[0], so `description`
|
|
7488
|
+
// matches it exactly. Confirmed live: item_descriptions[0] owns
|
|
7489
|
+
// candidate slot 0 (a 3-item batch returned exactly the declared
|
|
7490
|
+
// items at frames 0/1/2, in order; docs/GENERATORS.md#1dir), so
|
|
7491
|
+
// sending the same text as `description` too is redundant but
|
|
7492
|
+
// harmless, not a hedge against ambiguity.
|
|
7493
|
+
itemDescriptions: spec.batch?.role === "leader" ? spec.batch.itemDescriptions : void 0
|
|
7324
7494
|
});
|
|
7325
7495
|
return { jobId: res2.object_id, metadata: { backgroundJobId: res2.background_job_id } };
|
|
7326
7496
|
}
|
|
@@ -7497,6 +7667,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7497
7667
|
if (generator === "imagePro") return this.pollImagePro(jobId);
|
|
7498
7668
|
if (generator === "character") return this.pollCharacter(jobId, context);
|
|
7499
7669
|
if (generator === "objectPro") return this.pollObjectPro(jobId, context);
|
|
7670
|
+
if (generator === "uiAsset") return this.pollUiAsset(jobId, context);
|
|
7500
7671
|
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
7501
7672
|
const obj = await this.client.getObject(jobId);
|
|
7502
7673
|
if (obj.status === "review") {
|
|
@@ -7816,6 +7987,29 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7816
7987
|
throw err;
|
|
7817
7988
|
}
|
|
7818
7989
|
}
|
|
7990
|
+
/**
|
|
7991
|
+
* `/ui-assets/{id}` reports progress with its own `status` field
|
|
7992
|
+
* ("processing"/"completed"/"failed") rather than a 423, and its result is
|
|
7993
|
+
* one flat composited image at a hosted `image_url` — no per-piece
|
|
7994
|
+
* sub-images, no nine-slice metadata, confirmed absent from the response
|
|
7995
|
+
* schema. There is nothing to review: one job is one ready image, like
|
|
7996
|
+
* `isometricTile`.
|
|
7997
|
+
*/
|
|
7998
|
+
async pollUiAsset(uiAssetId, context) {
|
|
7999
|
+
const asset = await this.client.getUiAsset(uiAssetId);
|
|
8000
|
+
if (asset.status === "failed") return { status: "failed", error: "UI asset generation failed upstream" };
|
|
8001
|
+
if (asset.status !== "completed" || !asset.imageUrl) {
|
|
8002
|
+
return { status: "processing", progressPercent: asset.progressPercent, etaSeconds: asset.etaSeconds };
|
|
8003
|
+
}
|
|
8004
|
+
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
8005
|
+
return {
|
|
8006
|
+
status: "ready",
|
|
8007
|
+
objectId: uiAssetId,
|
|
8008
|
+
sourceUrl: asset.imageUrl,
|
|
8009
|
+
sources: [{ url: asset.imageUrl }],
|
|
8010
|
+
billed: await this.billedForJob(backgroundJobId)
|
|
8011
|
+
};
|
|
8012
|
+
}
|
|
7819
8013
|
async selectCandidate(jobId, index, commonTag, generator) {
|
|
7820
8014
|
if (generator === "tiles") {
|
|
7821
8015
|
const set = await this.client.getTilesPro(jobId);
|
|
@@ -9205,6 +9399,10 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9205
9399
|
size = asset.size ?? style.size ?? 64;
|
|
9206
9400
|
width = size;
|
|
9207
9401
|
height = size;
|
|
9402
|
+
} else if (generator === "uiAsset") {
|
|
9403
|
+
width = asset.width ?? style.size ?? 256;
|
|
9404
|
+
height = asset.height ?? style.size ?? 256;
|
|
9405
|
+
size = Math.max(width, height);
|
|
9208
9406
|
} else {
|
|
9209
9407
|
width = asset.width ?? style.size ?? 64;
|
|
9210
9408
|
height = asset.height ?? style.size ?? 64;
|
|
@@ -9215,6 +9413,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9215
9413
|
`assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
|
|
9216
9414
|
);
|
|
9217
9415
|
}
|
|
9416
|
+
if ((asset.pieces || asset.elements) && generator !== "uiAsset") {
|
|
9417
|
+
throw new Error(
|
|
9418
|
+
`assets.${assetId}: ${asset.pieces ? "pieces" : "elements"} needs a uiAsset style; "${styleId}" generates ${generator}`
|
|
9419
|
+
);
|
|
9420
|
+
}
|
|
9218
9421
|
const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
|
|
9219
9422
|
const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
|
|
9220
9423
|
let terrainLowerDescription;
|
|
@@ -9261,6 +9464,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9261
9464
|
shading: style.shading,
|
|
9262
9465
|
detail: style.detail,
|
|
9263
9466
|
seed: style.seed,
|
|
9467
|
+
uiPieces: generator === "uiAsset" ? asset.pieces : void 0,
|
|
9468
|
+
uiElements: generator === "uiAsset" ? asset.elements : void 0,
|
|
9469
|
+
uiColorPalette: generator === "uiAsset" ? style.uiColorPalette : void 0,
|
|
9264
9470
|
palette: style.palette,
|
|
9265
9471
|
enforcePalette: style.enforcePalette,
|
|
9266
9472
|
noBackground: style.noBackground,
|
|
@@ -9339,6 +9545,14 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9339
9545
|
};
|
|
9340
9546
|
styleSpecs.set(assetId, resolved);
|
|
9341
9547
|
}
|
|
9548
|
+
const batchMembersByLeader = /* @__PURE__ */ new Map();
|
|
9549
|
+
for (const id of styleSpecs.keys()) {
|
|
9550
|
+
const batch = manifest.assets[id].batch;
|
|
9551
|
+
if (!batch) continue;
|
|
9552
|
+
const members = batchMembersByLeader.get(batch.of) ?? [];
|
|
9553
|
+
members.push({ id, index: batch.index });
|
|
9554
|
+
batchMembersByLeader.set(batch.of, members);
|
|
9555
|
+
}
|
|
9342
9556
|
const finalized = /* @__PURE__ */ new Set();
|
|
9343
9557
|
const finalize = async (assetId) => {
|
|
9344
9558
|
const resolved = styleSpecs.get(assetId);
|
|
@@ -9497,6 +9711,56 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9497
9711
|
...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt }
|
|
9498
9712
|
};
|
|
9499
9713
|
}
|
|
9714
|
+
if (asset.batch) {
|
|
9715
|
+
if (asset.batch.of === assetId) throw new Error(`assets.${assetId}: a batch cannot name itself as its own leader`);
|
|
9716
|
+
const leaderSpec = await finalize(asset.batch.of);
|
|
9717
|
+
if (!leaderSpec.batch || leaderSpec.batch.role !== "leader") {
|
|
9718
|
+
throw new Error(`assets.${assetId}: ${asset.batch.of} is not a 1dir batch leader`);
|
|
9719
|
+
}
|
|
9720
|
+
if (resolved.size !== leaderSpec.size) {
|
|
9721
|
+
throw new Error(
|
|
9722
|
+
`assets.${assetId}: batch members share the leader's canvas size; ${asset.batch.of} is ${leaderSpec.size}px, this is ${resolved.size}px`
|
|
9723
|
+
);
|
|
9724
|
+
}
|
|
9725
|
+
resolved.batch = {
|
|
9726
|
+
role: "member",
|
|
9727
|
+
itemDescriptions: leaderSpec.batch.itemDescriptions,
|
|
9728
|
+
index: asset.batch.index,
|
|
9729
|
+
leaderAssetId: asset.batch.of,
|
|
9730
|
+
leaderSpec
|
|
9731
|
+
};
|
|
9732
|
+
} else {
|
|
9733
|
+
const members = batchMembersByLeader.get(assetId);
|
|
9734
|
+
if (members) {
|
|
9735
|
+
if (style.generator !== "1dir") {
|
|
9736
|
+
throw new Error(`assets.${assetId}: only a 1dir asset can lead a batch`);
|
|
9737
|
+
}
|
|
9738
|
+
const sorted = [...members].sort((a, b) => a.index - b.index);
|
|
9739
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9740
|
+
for (const { index } of sorted) {
|
|
9741
|
+
if (seen.has(index)) throw new Error(`assets.${assetId}: two batch members both claim index ${index}`);
|
|
9742
|
+
seen.add(index);
|
|
9743
|
+
}
|
|
9744
|
+
const expectedIndices = sorted.map((_, i) => i + 1).join(",");
|
|
9745
|
+
if (sorted.map((m) => m.index).join(",") !== expectedIndices) {
|
|
9746
|
+
throw new Error(
|
|
9747
|
+
`assets.${assetId}: batch member indices must be 1..${sorted.length} with no gaps; got ${sorted.map((m) => m.index).join(",")}`
|
|
9748
|
+
);
|
|
9749
|
+
}
|
|
9750
|
+
const total = 1 + sorted.length;
|
|
9751
|
+
const limit = candidateCount(resolved.size);
|
|
9752
|
+
if (total > limit) {
|
|
9753
|
+
throw new Error(
|
|
9754
|
+
`assets.${assetId}: a ${resolved.size}px batch holds at most ${limit} items (1 leader + ${limit - 1} members); this one declares ${total}`
|
|
9755
|
+
);
|
|
9756
|
+
}
|
|
9757
|
+
resolved.batch = {
|
|
9758
|
+
role: "leader",
|
|
9759
|
+
itemDescriptions: [resolved.prompt, ...sorted.map(({ id }) => styleSpecs.get(id).prompt)],
|
|
9760
|
+
memberAssetIds: sorted.map((m) => m.id)
|
|
9761
|
+
};
|
|
9762
|
+
}
|
|
9763
|
+
}
|
|
9500
9764
|
resolved.specHash = specHash(
|
|
9501
9765
|
resolved,
|
|
9502
9766
|
styleImageHashes,
|
|
@@ -13764,6 +14028,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13764
14028
|
for (const styleId of new Set(items.map((i) => i.spec.styleId))) {
|
|
13765
14029
|
styleImages.set(styleId, await resolveStyleImages(loaded, styleId));
|
|
13766
14030
|
}
|
|
14031
|
+
const byKey = new Map(items.map((item) => [item.key, item]));
|
|
13767
14032
|
let submitted = 0;
|
|
13768
14033
|
let failed = 0;
|
|
13769
14034
|
let spent = 0;
|
|
@@ -13793,8 +14058,62 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13793
14058
|
await pruneInFlight();
|
|
13794
14059
|
}
|
|
13795
14060
|
}
|
|
14061
|
+
function writeBatchMembers(leaderSpec, leaderKey, error) {
|
|
14062
|
+
if (leaderSpec.batch?.role !== "leader") return;
|
|
14063
|
+
const leaderEntry = lock.entries[leaderKey];
|
|
14064
|
+
for (const memberAssetId of leaderSpec.batch.memberAssetIds ?? []) {
|
|
14065
|
+
const memberKey = lockKey(leaderSpec.styleId, memberAssetId);
|
|
14066
|
+
const memberItem = byKey.get(memberKey);
|
|
14067
|
+
if (!memberItem) {
|
|
14068
|
+
throw new Error(
|
|
14069
|
+
`${leaderKey}: batch member "${memberAssetId}" must be submitted in the same run as its leader; run without --only, or include every sibling`
|
|
14070
|
+
);
|
|
14071
|
+
}
|
|
14072
|
+
const memberSpec = memberItem.spec;
|
|
14073
|
+
const memberEstimate = estimates.get(memberKey);
|
|
14074
|
+
const previousMemberEntry = lock.entries[memberKey];
|
|
14075
|
+
const memberSubmittedAt = leaderEntry.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
14076
|
+
upsert(lock, memberKey, {
|
|
14077
|
+
styleId: memberSpec.styleId,
|
|
14078
|
+
assetId: memberSpec.assetId,
|
|
14079
|
+
specHash: memberSpec.specHash,
|
|
14080
|
+
generator: memberSpec.generator,
|
|
14081
|
+
prompt: memberSpec.prompt,
|
|
14082
|
+
width: memberSpec.width,
|
|
14083
|
+
height: memberSpec.height,
|
|
14084
|
+
batch: { role: "member", leaderAssetId: leaderSpec.assetId, index: memberSpec.batch.index },
|
|
14085
|
+
status: error ? "failed" : "processing",
|
|
14086
|
+
error,
|
|
14087
|
+
jobId: error ? null : leaderEntry.jobId,
|
|
14088
|
+
submissionComplete: error ? void 0 : true,
|
|
14089
|
+
reviewObjectId: error ? null : leaderEntry.reviewObjectId,
|
|
14090
|
+
objectId: null,
|
|
14091
|
+
candidateIndex: null,
|
|
14092
|
+
outputs: [],
|
|
14093
|
+
supersededOutputs: previousMemberEntry?.outputs.length ? previousMemberEntry.outputs : previousMemberEntry?.supersededOutputs ?? [],
|
|
14094
|
+
providerMetadata: leaderEntry.providerMetadata,
|
|
14095
|
+
sourceUrl: null,
|
|
14096
|
+
sourceUrls: [],
|
|
14097
|
+
submittedAt: memberSubmittedAt,
|
|
14098
|
+
history: historyAfterReplacing(previousMemberEntry, historyLimit(loaded.manifest), memberSubmittedAt),
|
|
14099
|
+
cost: memberEstimate.amount,
|
|
14100
|
+
costUnit: memberEstimate.unit,
|
|
14101
|
+
provider: provider.id,
|
|
14102
|
+
downloadedAt: null
|
|
14103
|
+
});
|
|
14104
|
+
if (error) failed++;
|
|
14105
|
+
else {
|
|
14106
|
+
submitted++;
|
|
14107
|
+
spent += memberEstimate.amount;
|
|
14108
|
+
log2(` ${memberKey} \u2192 ${leaderEntry.jobId} (rides on ${leaderKey}'s batch)`);
|
|
14109
|
+
}
|
|
14110
|
+
}
|
|
14111
|
+
}
|
|
13796
14112
|
for (const { spec, key } of items) {
|
|
13797
14113
|
const estimate = estimates.get(key);
|
|
14114
|
+
if (spec.batch?.role === "member") {
|
|
14115
|
+
continue;
|
|
14116
|
+
}
|
|
13798
14117
|
if (spec.mirror) {
|
|
13799
14118
|
try {
|
|
13800
14119
|
await requireRevisionReady(spec, lock);
|
|
@@ -13827,6 +14146,14 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13827
14146
|
const since = Date.now() - lastSubmitAt;
|
|
13828
14147
|
if (since < spacing) await sleep2(spacing - since);
|
|
13829
14148
|
await requireRevisionReady(spec, lock);
|
|
14149
|
+
if (spec.batch?.role === "leader") {
|
|
14150
|
+
const missing = (spec.batch.memberAssetIds ?? []).filter((id) => !byKey.has(lockKey(spec.styleId, id)));
|
|
14151
|
+
if (missing.length) {
|
|
14152
|
+
throw new Error(
|
|
14153
|
+
`${key}: batch member(s) ${missing.join(", ")} must be submitted in the same run as this leader; run without --only, or include every sibling`
|
|
14154
|
+
);
|
|
14155
|
+
}
|
|
14156
|
+
}
|
|
13830
14157
|
const previousEntry = lock.entries[key];
|
|
13831
14158
|
const resumesCheckpoint = Boolean(
|
|
13832
14159
|
previousEntry?.specHash === spec.specHash && previousEntry.provider === provider.id && previousEntry.generator === spec.generator && previousEntry.submissionComplete === false && previousEntry.jobId
|
|
@@ -13932,6 +14259,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13932
14259
|
inFlight.set(jobId, spec);
|
|
13933
14260
|
submitted++;
|
|
13934
14261
|
spent += estimate.amount;
|
|
14262
|
+
writeBatchMembers(spec, key, null);
|
|
13935
14263
|
log2(
|
|
13936
14264
|
` ${key} \u2192 ${jobId} (${spec.width}x${spec.height}` + (estimate.candidates > 1 ? `, ${estimate.candidates} ${spec.tileFeature ? "outputs" : "candidates"}` : "") + `, ${estimate.amount})`
|
|
13937
14265
|
);
|
|
@@ -13943,6 +14271,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13943
14271
|
inFlight.set(entry.jobId, spec);
|
|
13944
14272
|
submitted++;
|
|
13945
14273
|
spent += estimate.amount;
|
|
14274
|
+
writeBatchMembers(spec, key, null);
|
|
13946
14275
|
log2(` ${key} \u2192 ${entry.jobId} (recovered from completed checkpoint)`);
|
|
13947
14276
|
} else {
|
|
13948
14277
|
failed++;
|
|
@@ -13951,6 +14280,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13951
14280
|
error: message6,
|
|
13952
14281
|
cost: entry.jobId ? estimate.amount : 0
|
|
13953
14282
|
});
|
|
14283
|
+
writeBatchMembers(spec, key, message6);
|
|
13954
14284
|
log2(` FAILED ${key}: ${message6}`);
|
|
13955
14285
|
}
|
|
13956
14286
|
}
|
|
@@ -14151,6 +14481,9 @@ function renderSheet(groups, options = {}) {
|
|
|
14151
14481
|
.cand.active { border-color:var(--dim); box-shadow:0 0 0 2px color-mix(in srgb, var(--dim) 20%, transparent); }
|
|
14152
14482
|
.cand.sel { border-color:var(--ok); box-shadow:0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }
|
|
14153
14483
|
.cand.set-frame { cursor:pointer; }
|
|
14484
|
+
.cand.recommended:not(.sel) { border-color:var(--accent); }
|
|
14485
|
+
.cand.recommended .badge { position:absolute; top:-1px; left:-1px; font-size:9.5px; font-weight:650;
|
|
14486
|
+
text-transform:uppercase; letter-spacing:.03em; color:var(--bg); background:var(--accent); padding:1px 5px; }
|
|
14154
14487
|
.cand img { image-rendering:pixelated; display:block;
|
|
14155
14488
|
background-image:
|
|
14156
14489
|
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
|
|
@@ -14308,14 +14641,17 @@ GROUPS.forEach((g, gi) => {
|
|
|
14308
14641
|
}).observe(loop);
|
|
14309
14642
|
}
|
|
14310
14643
|
}
|
|
14644
|
+
const recommended = g.recommendedIndex ?? 0;
|
|
14311
14645
|
g.frameUrls.forEach((url, i) => {
|
|
14312
14646
|
const c = document.createElement('button');
|
|
14313
14647
|
c.type = 'button';
|
|
14314
|
-
c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '')
|
|
14648
|
+
c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '') +
|
|
14649
|
+
(g.recommendedIndex != null && i === recommended ? ' recommended' : '');
|
|
14315
14650
|
c.tabIndex = -1;
|
|
14316
|
-
c.setAttribute('aria-label', g.mode === 'frame-set'
|
|
14651
|
+
c.setAttribute('aria-label', (g.mode === 'frame-set'
|
|
14317
14652
|
? 'Accept ordered frame set from frame ' + (i + 1)
|
|
14318
|
-
: 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length)
|
|
14653
|
+
: 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length) +
|
|
14654
|
+
(g.recommendedIndex != null && i === recommended ? ' (this asset\u2019s declared slot)' : ''));
|
|
14319
14655
|
|
|
14320
14656
|
const preview = document.createElement('img');
|
|
14321
14657
|
preview.className = 'preview';
|
|
@@ -14328,6 +14664,12 @@ GROUPS.forEach((g, gi) => {
|
|
|
14328
14664
|
index.textContent = (g.frameLabels?.[i] || String(i + 1)) + ' \xB7 ' +
|
|
14329
14665
|
g.width + '\xD7' + g.height + ' \xB7 ' + scaleLabel;
|
|
14330
14666
|
c.append(preview, index);
|
|
14667
|
+
if (g.recommendedIndex != null && i === recommended) {
|
|
14668
|
+
const badge = document.createElement('span');
|
|
14669
|
+
badge.className = 'badge';
|
|
14670
|
+
badge.textContent = 'declared';
|
|
14671
|
+
c.append(badge);
|
|
14672
|
+
}
|
|
14331
14673
|
if (displayScale > 1 && largestSide <= 96) {
|
|
14332
14674
|
const actual = document.createElement('img');
|
|
14333
14675
|
actual.className = 'actual';
|
|
@@ -14352,6 +14694,7 @@ GROUPS.forEach((g, gi) => {
|
|
|
14352
14694
|
});
|
|
14353
14695
|
frames.dataset.active = '0';
|
|
14354
14696
|
frames.querySelector('.cand')?.classList.add('active');
|
|
14697
|
+
if (recommended > 0) activate(recommended, frames);
|
|
14355
14698
|
root.appendChild(el);
|
|
14356
14699
|
});
|
|
14357
14700
|
|
|
@@ -14526,7 +14869,11 @@ async function prepareReview(provider, lock, opts = {}) {
|
|
|
14526
14869
|
height: spec.revision.sourceHeight
|
|
14527
14870
|
}
|
|
14528
14871
|
} : {},
|
|
14529
|
-
...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {}
|
|
14872
|
+
...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {},
|
|
14873
|
+
// A batch member's declared slot in the shared candidate set — the
|
|
14874
|
+
// leader's own slot 0 is already the sheet's ordinary default, so
|
|
14875
|
+
// only a member (whose slot is 1+) needs to say so explicitly.
|
|
14876
|
+
...entry.batch?.index != null ? { recommendedIndex: entry.batch.index } : {}
|
|
14530
14877
|
});
|
|
14531
14878
|
} catch (err) {
|
|
14532
14879
|
log2(` could not load candidates for ${key}: ${err instanceof Error ? err.message : String(err)}`);
|