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/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AssetBatchSchema: () => AssetBatchSchema,
33
34
  AssetSchema: () => AssetSchema,
34
35
  BudgetError: () => BudgetError,
35
36
  CHARACTER_DIRECTIONS_4: () => CHARACTER_DIRECTIONS_4,
@@ -96,6 +97,8 @@ __export(index_exports, {
96
97
  ScenarioProvider: () => ScenarioProvider,
97
98
  StyleInputSchema: () => StyleInputSchema,
98
99
  StyleSchema: () => StyleSchema,
100
+ UiElementSchema: () => UiElementSchema,
101
+ UiPieceSchema: () => UiPieceSchema,
99
102
  UnsupportedCapabilityError: () => UnsupportedCapabilityError,
100
103
  UsageError: () => UsageError,
101
104
  WorkspaceProjectSchema: () => WorkspaceProjectSchema,
@@ -305,7 +308,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
305
308
  // src/types.ts
306
309
  var import_zod = require("zod");
307
310
  var MediaTypeSchema = import_zod.z.enum(["image/png", "image/gif"]);
308
- var GeneratorSchema = import_zod.z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro"]);
311
+ var GeneratorSchema = import_zod.z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset"]);
309
312
  var GridConfidenceSchema = import_zod.z.enum(["low", "medium", "high"]);
310
313
  var RevisionModeSchema = import_zod.z.enum([
311
314
  "image-to-image",
@@ -427,6 +430,56 @@ var CharacterProportionsSchema = import_zod.z.union([
427
430
  }).strict()
428
431
  ]);
429
432
  var CharacterAnimationModeSchema = import_zod.z.enum(["template", "v3", "pro"]);
433
+ var UiPieceSchema = import_zod.z.discriminatedUnion("kind", [
434
+ import_zod.z.object({
435
+ id: import_zod.z.string().min(1),
436
+ kind: import_zod.z.literal("rounded_rect"),
437
+ label: import_zod.z.string().optional(),
438
+ x: import_zod.z.number(),
439
+ y: import_zod.z.number(),
440
+ w: import_zod.z.number(),
441
+ h: import_zod.z.number(),
442
+ radius: import_zod.z.number().min(0).optional()
443
+ }).strict(),
444
+ import_zod.z.object({
445
+ id: import_zod.z.string().min(1),
446
+ kind: import_zod.z.literal("circle"),
447
+ label: import_zod.z.string().optional(),
448
+ x: import_zod.z.number(),
449
+ y: import_zod.z.number(),
450
+ r: import_zod.z.number()
451
+ }).strict(),
452
+ import_zod.z.object({
453
+ id: import_zod.z.string().min(1),
454
+ kind: import_zod.z.literal("polygon"),
455
+ label: import_zod.z.string().optional(),
456
+ x: import_zod.z.number(),
457
+ y: import_zod.z.number(),
458
+ r: import_zod.z.number(),
459
+ sides: import_zod.z.number().int().min(3),
460
+ phase: import_zod.z.number().optional()
461
+ }).strict()
462
+ ]);
463
+ var UiElementSchema = import_zod.z.enum([
464
+ "button",
465
+ "icon_button",
466
+ "toolbar",
467
+ "tab",
468
+ "panel",
469
+ "window",
470
+ "health_bar",
471
+ "avatar",
472
+ "triangle",
473
+ "pentagon",
474
+ "hexagon",
475
+ "octagon"
476
+ ]);
477
+ var AssetBatchSchema = import_zod.z.object({
478
+ /** The batch leader: another `1dir` asset in the same style. */
479
+ of: import_zod.z.string().min(1),
480
+ /** 1-based slot in the batch (the leader is implicitly slot 0). Unique and contiguous among siblings. */
481
+ index: import_zod.z.number().int().min(1)
482
+ }).strict();
430
483
  var CharacterStateSchema = import_zod.z.object({
431
484
  /** The base character, or another state, in the same style. */
432
485
  of: import_zod.z.string().min(1),
@@ -723,6 +776,12 @@ var StyleObjectSchema = import_zod.z.object({
723
776
  * in place of `terrainShapeStyle`. The API default is `standard`.
724
777
  */
725
778
  terrainMode: import_zod.z.enum(["standard", "pro"]).optional(),
779
+ /**
780
+ * `uiAsset` generator only. A natural-language palette hint sent as-is
781
+ * (e.g. "brown and gold"), distinct from `palette`'s hex-color array —
782
+ * this is a prompt-level steer, not a local quantization target.
783
+ */
784
+ uiColorPalette: import_zod.z.string().max(200).optional(),
726
785
  /**
727
786
  * `terrain` generator only, `terrainMode: "standard"`. Procedural
728
787
  * boundary geometry: `square` or `round`, 16px or 32px tiles only.
@@ -1000,6 +1059,16 @@ var AssetSchema = import_zod.z.object({
1000
1059
  * `prompt` instead.
1001
1060
  */
1002
1061
  animation: CharacterAnimationSchema.optional(),
1062
+ /**
1063
+ * `1dir` styles only: this asset rides along on another `1dir` asset's
1064
+ * batch submission instead of generating on its own. See
1065
+ * `AssetBatchSchema`.
1066
+ */
1067
+ batch: AssetBatchSchema.optional(),
1068
+ /** `uiAsset` styles: precise shape regions the panel is composited from. Combine with `elements`; omit both for a default full-canvas panel. */
1069
+ pieces: import_zod.z.array(UiPieceSchema).min(1).optional(),
1070
+ /** `uiAsset` styles: named, auto-positioned UI element scaffolds. Combine with `pieces`; omit both for a default full-canvas panel. */
1071
+ elements: import_zod.z.array(UiElementSchema).min(1).optional(),
1003
1072
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1004
1073
  proportions: CharacterProportionsSchema.optional(),
1005
1074
  /**
@@ -1076,7 +1145,7 @@ var AssetSchema = import_zod.z.object({
1076
1145
  path: ["revision"]
1077
1146
  });
1078
1147
  }
1079
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
1148
+ const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
1080
1149
  if (shapes.length > 1) {
1081
1150
  context.addIssue({
1082
1151
  code: import_zod.z.ZodIssueCode.custom,
@@ -1227,6 +1296,12 @@ var LockEntrySchema = import_zod.z.object({
1227
1296
  sourceAssetId: import_zod.z.string().min(1),
1228
1297
  sourceSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/)
1229
1298
  }).strict().nullable().default(null),
1299
+ /** For a `1dir` batch member: which leader and slot it rode along on. */
1300
+ batch: import_zod.z.object({
1301
+ role: import_zod.z.enum(["leader", "member"]),
1302
+ leaderAssetId: import_zod.z.string().min(1).optional(),
1303
+ index: import_zod.z.number().int().min(1).optional()
1304
+ }).strict().nullable().default(null),
1230
1305
  /**
1231
1306
  * Output hashes owned by the previous generation while its replacement is
1232
1307
  * pending. They authorize replacing only unchanged PixelKiln-owned files.
@@ -1511,6 +1586,19 @@ var IsometricTileGetSchema = import_zod2.z.object({
1511
1586
  image: import_zod2.z.object({ base64: import_zod2.z.string().min(1), format: import_zod2.z.string().default("png") }).passthrough(),
1512
1587
  usage: UsageSchema
1513
1588
  }).passthrough();
1589
+ var UiAssetSubmitSchema = import_zod2.z.object({
1590
+ ui_asset_id: import_zod2.z.string().min(1),
1591
+ background_job_id: import_zod2.z.string().min(1),
1592
+ status: import_zod2.z.string().default("processing"),
1593
+ usage: UsageSchema
1594
+ }).passthrough();
1595
+ var UiAssetGetSchema = import_zod2.z.object({
1596
+ id: import_zod2.z.string().min(1),
1597
+ status: import_zod2.z.string().nullable().default(null),
1598
+ image_url: import_zod2.z.string().nullable().optional(),
1599
+ progress_percent: import_zod2.z.number().nullable().optional(),
1600
+ eta_seconds: import_zod2.z.number().nullable().optional()
1601
+ }).passthrough();
1514
1602
  var RevisionJobSubmitSchema = import_zod2.z.object({
1515
1603
  background_job_id: import_zod2.z.string().min(1),
1516
1604
  status: import_zod2.z.string().default("processing")
@@ -1871,6 +1959,48 @@ var PixelLabClient = class {
1871
1959
  );
1872
1960
  return { image: res.image, usage: res.usage ?? null };
1873
1961
  }
1962
+ /**
1963
+ * `/create-ui-asset`: one composited panel image from a shape template —
1964
+ * `pieces` (precise rect/circle/polygon regions on a virtual 0–512
1965
+ * editor canvas) and/or named `elements` (auto-positioned scaffolds:
1966
+ * button, icon_button, toolbar, tab, panel, window, health_bar, avatar,
1967
+ * triangle, pentagon, hexagon, octagon), or neither for a default
1968
+ * full-canvas rounded-rect panel. Unlike every other generator here, the
1969
+ * result is one flat image with no per-piece sub-regions or nine-slice
1970
+ * metadata returned — cropping a `pieces` layout into separate files
1971
+ * would be pixelkiln's own local work, not modeled yet.
1972
+ */
1973
+ async createUiAsset(args) {
1974
+ const body = {
1975
+ description: args.description,
1976
+ image_size: { width: args.width, height: args.height }
1977
+ };
1978
+ if (args.pieces?.length) body.pieces = args.pieces;
1979
+ if (args.elements?.length) body.elements = args.elements;
1980
+ if (args.styleImage) body.style_image = args.styleImage;
1981
+ if (args.colorPalette) body.color_palette = args.colorPalette;
1982
+ if (args.noBackground != null) body.no_background = args.noBackground;
1983
+ if (args.seed != null) body.seed = args.seed;
1984
+ const res = await validateResponse(
1985
+ UiAssetSubmitSchema,
1986
+ await this.request("/create-ui-asset", { method: "POST", body: JSON.stringify(body) }),
1987
+ "create ui asset"
1988
+ );
1989
+ return { ui_asset_id: res.ui_asset_id, background_job_id: res.background_job_id, usage: res.usage };
1990
+ }
1991
+ async getUiAsset(uiAssetId) {
1992
+ const res = await validateResponse(
1993
+ UiAssetGetSchema,
1994
+ await this.request(`/ui-assets/${encodeURIComponent(uiAssetId)}`),
1995
+ "get ui asset"
1996
+ );
1997
+ return {
1998
+ status: res.status,
1999
+ imageUrl: res.image_url ?? null,
2000
+ progressPercent: res.progress_percent ?? null,
2001
+ etaSeconds: res.eta_seconds ?? null
2002
+ };
2003
+ }
1874
2004
  /**
1875
2005
  * Synchronous single-image generation. Returns the PNG inline rather than a
1876
2006
  * job id, and is the only endpoint that honours a forced palette;
@@ -2277,14 +2407,15 @@ var PixelLabClient = class {
2277
2407
  /**
2278
2408
  * `/reduce-colors`, PixelLab's "Cleanup" tier: quantize one image onto a
2279
2409
  * smaller palette, synchronously — no `background_job_id`, the result
2280
- * comes back in this same response, like `createImagePixflux`. Verified
2281
- * against the live OpenAPI schema, not exercised against a live account:
2282
- * the schema's own response example is `usage: {type: "usd", usd: 0.02}`,
2283
- * which — going by this codebase's own repeated experience with PixelLab's
2284
- * documented-vs-billed cost mismatches (`isometricTile`, `objectPro`) —
2285
- * should not be trusted over a real call. `numColors` and `paletteImage`
2286
- * are mutually exclusive upstream; the manifest schema already enforces
2287
- * that before this is ever called.
2410
+ * comes back in this same response, like `createImagePixflux`. The
2411
+ * schema's own response example is dollar-denominated (`usage: {type:
2412
+ * "usd", usd: 0.02}`), which — matching this codebase's repeated
2413
+ * experience with PixelLab's documented-vs-billed cost mismatches
2414
+ * (`isometricTile`, `objectPro`) — did not hold: confirmed live against a
2415
+ * Tier 2 account at exactly 0.1 generations for a 32x32 source
2416
+ * (docs/REVISIONS.md). `numColors` and `paletteImage` are mutually
2417
+ * exclusive upstream; the manifest schema already enforces that before
2418
+ * this is ever called.
2288
2419
  */
2289
2420
  async reduceColors(args) {
2290
2421
  const body = { images: [args.image] };
@@ -2307,8 +2438,9 @@ var PixelLabClient = class {
2307
2438
  /**
2308
2439
  * `/correct-pixelart`, PixelLab's "Cleanup" tier: sharpen edges and drop
2309
2440
  * stray pixels without resizing, synchronously — same shape as
2310
- * `reduceColors` above, no background job. Cost is likewise unverified
2311
- * against a live account (schema example: `usage: {type: "usd", usd: 0.02}`).
2441
+ * `reduceColors` above, no background job. Cost is likewise confirmed
2442
+ * live at 0.1 generations for a 32x32 source, not the schema's own
2443
+ * dollar-denominated example (`usage: {type: "usd", usd: 0.02}`).
2312
2444
  */
2313
2445
  async correctPixelart(args) {
2314
2446
  const body = { images: [args.image] };
@@ -3006,16 +3138,20 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3006
3138
  detail: spec.detail ?? null,
3007
3139
  seed: spec.seed ?? null,
3008
3140
  palette: spec.palette,
3009
- // `noBackground` only reaches the wire for pixflux; the tile fields are
3010
- // undefined for every other generator. `tileSize` is intentionally
3011
- // absent; width/height are derived from it, so it is already covered.
3012
- noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
3141
+ // `noBackground` only reaches the wire for pixflux and uiAsset; the
3142
+ // tile fields are undefined for every other generator. `tileSize` is
3143
+ // intentionally absent; width/height are derived from it, so it is
3144
+ // already covered.
3145
+ noBackground: spec.generator === "pixflux" || spec.generator === "uiAsset" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
3013
3146
  tileType: spec.tileType,
3014
3147
  tileView: spec.tileView,
3015
3148
  tileFeature: spec.tileFeature,
3016
3149
  outlineMode: spec.outlineMode,
3017
3150
  isometricTileSize: spec.isometricTileSize,
3018
3151
  isometricTileShape: spec.isometricTileShape,
3152
+ uiPieces: spec.uiPieces,
3153
+ uiElements: spec.uiElements,
3154
+ uiColorPalette: spec.uiColorPalette,
3019
3155
  styleImages: styleImageHashes,
3020
3156
  // A character family: the engine and rotations of a base, the parent's
3021
3157
  // generated bytes for a state or animation (so a regenerated parent
@@ -3064,6 +3200,11 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3064
3200
  // A mirror's bytes come from its source's recorded outputs; the plan
3065
3201
  // compares those directly, so only the choice of source is identity.
3066
3202
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
3203
+ // The whole group's ordered descriptions, not just this asset's own —
3204
+ // any sibling's prompt changing, or a member being added or removed,
3205
+ // must mark every member stale together, since they all ride on one
3206
+ // submitted job with no way to amend it after the fact.
3207
+ batch: spec.batch ? { role: spec.batch.role, itemDescriptions: spec.batch.itemDescriptions, index: spec.batch.index } : void 0,
3067
3208
  revision: spec.revision ? {
3068
3209
  mode: spec.revision.mode,
3069
3210
  from: spec.revision.sourceAssetId,
@@ -3208,7 +3349,7 @@ var PixelLabProvider = class _PixelLabProvider {
3208
3349
  return new _PixelLabProvider(new PixelLabClient("download-only"));
3209
3350
  }
3210
3351
  supports(generator) {
3211
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro";
3352
+ return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset";
3212
3353
  }
3213
3354
  /**
3214
3355
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
@@ -3245,6 +3386,9 @@ var PixelLabProvider = class _PixelLabProvider {
3245
3386
  return dir;
3246
3387
  }
3247
3388
  estimate(spec) {
3389
+ if (spec.batch?.role === "member") {
3390
+ return { unit: "generations", amount: 0, candidates: 1 };
3391
+ }
3248
3392
  if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
3249
3393
  return { unit: "generations", amount: 0.1, candidates: 1 };
3250
3394
  }
@@ -3321,6 +3465,11 @@ var PixelLabProvider = class _PixelLabProvider {
3321
3465
  `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)`
3322
3466
  );
3323
3467
  }
3468
+ if (spec.generator === "uiAsset" && (spec.width < 192 || spec.height < 192 || spec.width > 688 || spec.height > 688)) {
3469
+ throw new Error(
3470
+ `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)`
3471
+ );
3472
+ }
3324
3473
  if (spec.generator === "map") {
3325
3474
  requirePixelLabOption("map", "view", spec.view, ["low top-down", "high top-down", "side"]);
3326
3475
  requirePixelLabOption("map", "outline", spec.outline, [
@@ -3958,6 +4107,20 @@ var PixelLabProvider = class _PixelLabProvider {
3958
4107
  if (spec.revision) return this.submitRevision(spec);
3959
4108
  if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
3960
4109
  if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
4110
+ if (spec.generator === "uiAsset") {
4111
+ const res2 = await this.client.createUiAsset({
4112
+ description: spec.prompt,
4113
+ width: spec.width,
4114
+ height: spec.height,
4115
+ pieces: spec.uiPieces,
4116
+ elements: spec.uiElements,
4117
+ styleImage: styleImages[0] ? { base64: styleImages[0].base64, format: styleImages[0].format } : void 0,
4118
+ colorPalette: spec.uiColorPalette,
4119
+ noBackground: spec.noBackground,
4120
+ seed: spec.seed
4121
+ });
4122
+ return { jobId: res2.ui_asset_id, metadata: { backgroundJobId: res2.background_job_id } };
4123
+ }
3961
4124
  if (spec.generator === "pixflux") {
3962
4125
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
3963
4126
  const { png } = await this.client.createImagePixflux({
@@ -4043,11 +4206,21 @@ var PixelLabProvider = class _PixelLabProvider {
4043
4206
  return { jobId: res2.background_job_id };
4044
4207
  }
4045
4208
  if (spec.generator === "1dir") {
4209
+ if (spec.batch?.role === "member") {
4210
+ throw new Error(`${spec.styleId}/${spec.assetId}: a batch member cannot submit on its own`);
4211
+ }
4046
4212
  const res2 = await this.client.create1Direction({
4047
4213
  description: spec.prompt,
4048
4214
  size: spec.size,
4049
4215
  view: spec.view === "sidescroller" ? "sidescroller" : "top-down",
4050
- styleImages
4216
+ styleImages,
4217
+ // The leader's own subject is item_descriptions[0], so `description`
4218
+ // matches it exactly. Confirmed live: item_descriptions[0] owns
4219
+ // candidate slot 0 (a 3-item batch returned exactly the declared
4220
+ // items at frames 0/1/2, in order; docs/GENERATORS.md#1dir), so
4221
+ // sending the same text as `description` too is redundant but
4222
+ // harmless, not a hedge against ambiguity.
4223
+ itemDescriptions: spec.batch?.role === "leader" ? spec.batch.itemDescriptions : void 0
4051
4224
  });
4052
4225
  return { jobId: res2.object_id, metadata: { backgroundJobId: res2.background_job_id } };
4053
4226
  }
@@ -4224,6 +4397,7 @@ var PixelLabProvider = class _PixelLabProvider {
4224
4397
  if (generator === "imagePro") return this.pollImagePro(jobId);
4225
4398
  if (generator === "character") return this.pollCharacter(jobId, context);
4226
4399
  if (generator === "objectPro") return this.pollObjectPro(jobId, context);
4400
+ if (generator === "uiAsset") return this.pollUiAsset(jobId, context);
4227
4401
  const backgroundJobId = context?.metadata?.backgroundJobId;
4228
4402
  const obj = await this.client.getObject(jobId);
4229
4403
  if (obj.status === "review") {
@@ -4543,6 +4717,29 @@ var PixelLabProvider = class _PixelLabProvider {
4543
4717
  throw err;
4544
4718
  }
4545
4719
  }
4720
+ /**
4721
+ * `/ui-assets/{id}` reports progress with its own `status` field
4722
+ * ("processing"/"completed"/"failed") rather than a 423, and its result is
4723
+ * one flat composited image at a hosted `image_url` — no per-piece
4724
+ * sub-images, no nine-slice metadata, confirmed absent from the response
4725
+ * schema. There is nothing to review: one job is one ready image, like
4726
+ * `isometricTile`.
4727
+ */
4728
+ async pollUiAsset(uiAssetId, context) {
4729
+ const asset = await this.client.getUiAsset(uiAssetId);
4730
+ if (asset.status === "failed") return { status: "failed", error: "UI asset generation failed upstream" };
4731
+ if (asset.status !== "completed" || !asset.imageUrl) {
4732
+ return { status: "processing", progressPercent: asset.progressPercent, etaSeconds: asset.etaSeconds };
4733
+ }
4734
+ const backgroundJobId = context?.metadata?.backgroundJobId;
4735
+ return {
4736
+ status: "ready",
4737
+ objectId: uiAssetId,
4738
+ sourceUrl: asset.imageUrl,
4739
+ sources: [{ url: asset.imageUrl }],
4740
+ billed: await this.billedForJob(backgroundJobId)
4741
+ };
4742
+ }
4546
4743
  async selectCandidate(jobId, index, commonTag, generator) {
4547
4744
  if (generator === "tiles") {
4548
4745
  const set = await this.client.getTilesPro(jobId);
@@ -7323,6 +7520,10 @@ async function resolveSpecs(loaded, filter) {
7323
7520
  size = asset.size ?? style.size ?? 64;
7324
7521
  width = size;
7325
7522
  height = size;
7523
+ } else if (generator === "uiAsset") {
7524
+ width = asset.width ?? style.size ?? 256;
7525
+ height = asset.height ?? style.size ?? 256;
7526
+ size = Math.max(width, height);
7326
7527
  } else {
7327
7528
  width = asset.width ?? style.size ?? 64;
7328
7529
  height = asset.height ?? style.size ?? 64;
@@ -7333,6 +7534,11 @@ async function resolveSpecs(loaded, filter) {
7333
7534
  `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
7334
7535
  );
7335
7536
  }
7537
+ if ((asset.pieces || asset.elements) && generator !== "uiAsset") {
7538
+ throw new Error(
7539
+ `assets.${assetId}: ${asset.pieces ? "pieces" : "elements"} needs a uiAsset style; "${styleId}" generates ${generator}`
7540
+ );
7541
+ }
7336
7542
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
7337
7543
  const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
7338
7544
  let terrainLowerDescription;
@@ -7379,6 +7585,9 @@ async function resolveSpecs(loaded, filter) {
7379
7585
  shading: style.shading,
7380
7586
  detail: style.detail,
7381
7587
  seed: style.seed,
7588
+ uiPieces: generator === "uiAsset" ? asset.pieces : void 0,
7589
+ uiElements: generator === "uiAsset" ? asset.elements : void 0,
7590
+ uiColorPalette: generator === "uiAsset" ? style.uiColorPalette : void 0,
7382
7591
  palette: style.palette,
7383
7592
  enforcePalette: style.enforcePalette,
7384
7593
  noBackground: style.noBackground,
@@ -7457,6 +7666,14 @@ async function resolveSpecs(loaded, filter) {
7457
7666
  };
7458
7667
  styleSpecs.set(assetId, resolved);
7459
7668
  }
7669
+ const batchMembersByLeader = /* @__PURE__ */ new Map();
7670
+ for (const id of styleSpecs.keys()) {
7671
+ const batch = manifest.assets[id].batch;
7672
+ if (!batch) continue;
7673
+ const members = batchMembersByLeader.get(batch.of) ?? [];
7674
+ members.push({ id, index: batch.index });
7675
+ batchMembersByLeader.set(batch.of, members);
7676
+ }
7460
7677
  const finalized = /* @__PURE__ */ new Set();
7461
7678
  const finalize = async (assetId) => {
7462
7679
  const resolved = styleSpecs.get(assetId);
@@ -7615,6 +7832,56 @@ async function resolveSpecs(loaded, filter) {
7615
7832
  ...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt }
7616
7833
  };
7617
7834
  }
7835
+ if (asset.batch) {
7836
+ if (asset.batch.of === assetId) throw new Error(`assets.${assetId}: a batch cannot name itself as its own leader`);
7837
+ const leaderSpec = await finalize(asset.batch.of);
7838
+ if (!leaderSpec.batch || leaderSpec.batch.role !== "leader") {
7839
+ throw new Error(`assets.${assetId}: ${asset.batch.of} is not a 1dir batch leader`);
7840
+ }
7841
+ if (resolved.size !== leaderSpec.size) {
7842
+ throw new Error(
7843
+ `assets.${assetId}: batch members share the leader's canvas size; ${asset.batch.of} is ${leaderSpec.size}px, this is ${resolved.size}px`
7844
+ );
7845
+ }
7846
+ resolved.batch = {
7847
+ role: "member",
7848
+ itemDescriptions: leaderSpec.batch.itemDescriptions,
7849
+ index: asset.batch.index,
7850
+ leaderAssetId: asset.batch.of,
7851
+ leaderSpec
7852
+ };
7853
+ } else {
7854
+ const members = batchMembersByLeader.get(assetId);
7855
+ if (members) {
7856
+ if (style.generator !== "1dir") {
7857
+ throw new Error(`assets.${assetId}: only a 1dir asset can lead a batch`);
7858
+ }
7859
+ const sorted = [...members].sort((a, b) => a.index - b.index);
7860
+ const seen = /* @__PURE__ */ new Set();
7861
+ for (const { index } of sorted) {
7862
+ if (seen.has(index)) throw new Error(`assets.${assetId}: two batch members both claim index ${index}`);
7863
+ seen.add(index);
7864
+ }
7865
+ const expectedIndices = sorted.map((_, i) => i + 1).join(",");
7866
+ if (sorted.map((m) => m.index).join(",") !== expectedIndices) {
7867
+ throw new Error(
7868
+ `assets.${assetId}: batch member indices must be 1..${sorted.length} with no gaps; got ${sorted.map((m) => m.index).join(",")}`
7869
+ );
7870
+ }
7871
+ const total = 1 + sorted.length;
7872
+ const limit = candidateCount(resolved.size);
7873
+ if (total > limit) {
7874
+ throw new Error(
7875
+ `assets.${assetId}: a ${resolved.size}px batch holds at most ${limit} items (1 leader + ${limit - 1} members); this one declares ${total}`
7876
+ );
7877
+ }
7878
+ resolved.batch = {
7879
+ role: "leader",
7880
+ itemDescriptions: [resolved.prompt, ...sorted.map(({ id }) => styleSpecs.get(id).prompt)],
7881
+ memberAssetIds: sorted.map((m) => m.id)
7882
+ };
7883
+ }
7884
+ }
7618
7885
  resolved.specHash = specHash(
7619
7886
  resolved,
7620
7887
  styleImageHashes,
@@ -12000,6 +12267,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12000
12267
  for (const styleId of new Set(items.map((i) => i.spec.styleId))) {
12001
12268
  styleImages.set(styleId, await resolveStyleImages(loaded, styleId));
12002
12269
  }
12270
+ const byKey = new Map(items.map((item) => [item.key, item]));
12003
12271
  let submitted = 0;
12004
12272
  let failed = 0;
12005
12273
  let spent = 0;
@@ -12029,8 +12297,62 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12029
12297
  await pruneInFlight();
12030
12298
  }
12031
12299
  }
12300
+ function writeBatchMembers(leaderSpec, leaderKey, error) {
12301
+ if (leaderSpec.batch?.role !== "leader") return;
12302
+ const leaderEntry = lock.entries[leaderKey];
12303
+ for (const memberAssetId of leaderSpec.batch.memberAssetIds ?? []) {
12304
+ const memberKey = lockKey(leaderSpec.styleId, memberAssetId);
12305
+ const memberItem = byKey.get(memberKey);
12306
+ if (!memberItem) {
12307
+ throw new Error(
12308
+ `${leaderKey}: batch member "${memberAssetId}" must be submitted in the same run as its leader; run without --only, or include every sibling`
12309
+ );
12310
+ }
12311
+ const memberSpec = memberItem.spec;
12312
+ const memberEstimate = estimates.get(memberKey);
12313
+ const previousMemberEntry = lock.entries[memberKey];
12314
+ const memberSubmittedAt = leaderEntry.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString();
12315
+ upsert(lock, memberKey, {
12316
+ styleId: memberSpec.styleId,
12317
+ assetId: memberSpec.assetId,
12318
+ specHash: memberSpec.specHash,
12319
+ generator: memberSpec.generator,
12320
+ prompt: memberSpec.prompt,
12321
+ width: memberSpec.width,
12322
+ height: memberSpec.height,
12323
+ batch: { role: "member", leaderAssetId: leaderSpec.assetId, index: memberSpec.batch.index },
12324
+ status: error ? "failed" : "processing",
12325
+ error,
12326
+ jobId: error ? null : leaderEntry.jobId,
12327
+ submissionComplete: error ? void 0 : true,
12328
+ reviewObjectId: error ? null : leaderEntry.reviewObjectId,
12329
+ objectId: null,
12330
+ candidateIndex: null,
12331
+ outputs: [],
12332
+ supersededOutputs: previousMemberEntry?.outputs.length ? previousMemberEntry.outputs : previousMemberEntry?.supersededOutputs ?? [],
12333
+ providerMetadata: leaderEntry.providerMetadata,
12334
+ sourceUrl: null,
12335
+ sourceUrls: [],
12336
+ submittedAt: memberSubmittedAt,
12337
+ history: historyAfterReplacing(previousMemberEntry, historyLimit(loaded.manifest), memberSubmittedAt),
12338
+ cost: memberEstimate.amount,
12339
+ costUnit: memberEstimate.unit,
12340
+ provider: provider.id,
12341
+ downloadedAt: null
12342
+ });
12343
+ if (error) failed++;
12344
+ else {
12345
+ submitted++;
12346
+ spent += memberEstimate.amount;
12347
+ log(` ${memberKey} \u2192 ${leaderEntry.jobId} (rides on ${leaderKey}'s batch)`);
12348
+ }
12349
+ }
12350
+ }
12032
12351
  for (const { spec, key } of items) {
12033
12352
  const estimate = estimates.get(key);
12353
+ if (spec.batch?.role === "member") {
12354
+ continue;
12355
+ }
12034
12356
  if (spec.mirror) {
12035
12357
  try {
12036
12358
  await requireRevisionReady(spec, lock);
@@ -12063,6 +12385,14 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12063
12385
  const since = Date.now() - lastSubmitAt;
12064
12386
  if (since < spacing) await sleep(spacing - since);
12065
12387
  await requireRevisionReady(spec, lock);
12388
+ if (spec.batch?.role === "leader") {
12389
+ const missing = (spec.batch.memberAssetIds ?? []).filter((id) => !byKey.has(lockKey(spec.styleId, id)));
12390
+ if (missing.length) {
12391
+ throw new Error(
12392
+ `${key}: batch member(s) ${missing.join(", ")} must be submitted in the same run as this leader; run without --only, or include every sibling`
12393
+ );
12394
+ }
12395
+ }
12066
12396
  const previousEntry = lock.entries[key];
12067
12397
  const resumesCheckpoint = Boolean(
12068
12398
  previousEntry?.specHash === spec.specHash && previousEntry.provider === provider.id && previousEntry.generator === spec.generator && previousEntry.submissionComplete === false && previousEntry.jobId
@@ -12168,6 +12498,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12168
12498
  inFlight.set(jobId, spec);
12169
12499
  submitted++;
12170
12500
  spent += estimate.amount;
12501
+ writeBatchMembers(spec, key, null);
12171
12502
  log(
12172
12503
  ` ${key} \u2192 ${jobId} (${spec.width}x${spec.height}` + (estimate.candidates > 1 ? `, ${estimate.candidates} ${spec.tileFeature ? "outputs" : "candidates"}` : "") + `, ${estimate.amount})`
12173
12504
  );
@@ -12179,6 +12510,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12179
12510
  inFlight.set(entry.jobId, spec);
12180
12511
  submitted++;
12181
12512
  spent += estimate.amount;
12513
+ writeBatchMembers(spec, key, null);
12182
12514
  log(` ${key} \u2192 ${entry.jobId} (recovered from completed checkpoint)`);
12183
12515
  } else {
12184
12516
  failed++;
@@ -12187,6 +12519,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12187
12519
  error: message6,
12188
12520
  cost: entry.jobId ? estimate.amount : 0
12189
12521
  });
12522
+ writeBatchMembers(spec, key, message6);
12190
12523
  log(` FAILED ${key}: ${message6}`);
12191
12524
  }
12192
12525
  }
@@ -12778,6 +13111,9 @@ function renderSheet(groups, options = {}) {
12778
13111
  .cand.active { border-color:var(--dim); box-shadow:0 0 0 2px color-mix(in srgb, var(--dim) 20%, transparent); }
12779
13112
  .cand.sel { border-color:var(--ok); box-shadow:0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }
12780
13113
  .cand.set-frame { cursor:pointer; }
13114
+ .cand.recommended:not(.sel) { border-color:var(--accent); }
13115
+ .cand.recommended .badge { position:absolute; top:-1px; left:-1px; font-size:9.5px; font-weight:650;
13116
+ text-transform:uppercase; letter-spacing:.03em; color:var(--bg); background:var(--accent); padding:1px 5px; }
12781
13117
  .cand img { image-rendering:pixelated; display:block;
12782
13118
  background-image:
12783
13119
  linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
@@ -12935,14 +13271,17 @@ GROUPS.forEach((g, gi) => {
12935
13271
  }).observe(loop);
12936
13272
  }
12937
13273
  }
13274
+ const recommended = g.recommendedIndex ?? 0;
12938
13275
  g.frameUrls.forEach((url, i) => {
12939
13276
  const c = document.createElement('button');
12940
13277
  c.type = 'button';
12941
- c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '');
13278
+ c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '') +
13279
+ (g.recommendedIndex != null && i === recommended ? ' recommended' : '');
12942
13280
  c.tabIndex = -1;
12943
- c.setAttribute('aria-label', g.mode === 'frame-set'
13281
+ c.setAttribute('aria-label', (g.mode === 'frame-set'
12944
13282
  ? 'Accept ordered frame set from frame ' + (i + 1)
12945
- : 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length);
13283
+ : 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length) +
13284
+ (g.recommendedIndex != null && i === recommended ? ' (this asset\u2019s declared slot)' : ''));
12946
13285
 
12947
13286
  const preview = document.createElement('img');
12948
13287
  preview.className = 'preview';
@@ -12955,6 +13294,12 @@ GROUPS.forEach((g, gi) => {
12955
13294
  index.textContent = (g.frameLabels?.[i] || String(i + 1)) + ' \xB7 ' +
12956
13295
  g.width + '\xD7' + g.height + ' \xB7 ' + scaleLabel;
12957
13296
  c.append(preview, index);
13297
+ if (g.recommendedIndex != null && i === recommended) {
13298
+ const badge = document.createElement('span');
13299
+ badge.className = 'badge';
13300
+ badge.textContent = 'declared';
13301
+ c.append(badge);
13302
+ }
12958
13303
  if (displayScale > 1 && largestSide <= 96) {
12959
13304
  const actual = document.createElement('img');
12960
13305
  actual.className = 'actual';
@@ -12979,6 +13324,7 @@ GROUPS.forEach((g, gi) => {
12979
13324
  });
12980
13325
  frames.dataset.active = '0';
12981
13326
  frames.querySelector('.cand')?.classList.add('active');
13327
+ if (recommended > 0) activate(recommended, frames);
12982
13328
  root.appendChild(el);
12983
13329
  });
12984
13330
 
@@ -13234,7 +13580,11 @@ async function prepareReview(provider, lock, opts = {}) {
13234
13580
  height: spec.revision.sourceHeight
13235
13581
  }
13236
13582
  } : {},
13237
- ...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {}
13583
+ ...current && currentRoute ? { current: { url: currentRoute, width: current.width, height: current.height } } : {},
13584
+ // A batch member's declared slot in the shared candidate set — the
13585
+ // leader's own slot 0 is already the sheet's ordinary default, so
13586
+ // only a member (whose slot is 1+) needs to say so explicitly.
13587
+ ...entry.batch?.index != null ? { recommendedIndex: entry.batch.index } : {}
13238
13588
  });
13239
13589
  } catch (err) {
13240
13590
  log(` could not load candidates for ${key}: ${err instanceof Error ? err.message : String(err)}`);
@@ -16282,6 +16632,7 @@ async function workspaceStatus(ws, dir) {
16282
16632
  }
16283
16633
  // Annotate the CommonJS export names for ESM import in node:
16284
16634
  0 && (module.exports = {
16635
+ AssetBatchSchema,
16285
16636
  AssetSchema,
16286
16637
  BudgetError,
16287
16638
  CHARACTER_DIRECTIONS_4,
@@ -16348,6 +16699,8 @@ async function workspaceStatus(ws, dir) {
16348
16699
  ScenarioProvider,
16349
16700
  StyleInputSchema,
16350
16701
  StyleSchema,
16702
+ UiElementSchema,
16703
+ UiPieceSchema,
16351
16704
  UnsupportedCapabilityError,
16352
16705
  UsageError,
16353
16706
  WorkspaceProjectSchema,