pixelkiln 0.50.0 → 0.52.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 CHANGED
@@ -707,7 +707,7 @@ import path2 from "path";
707
707
  // src/types.ts
708
708
  import { z } from "zod";
709
709
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
710
- var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character"]);
710
+ var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro"]);
711
711
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
712
712
  var RevisionModeSchema = z.enum(["image-to-image", "inpaint", "outpaint"]);
713
713
  function tileVariationCount(descriptions) {
@@ -729,6 +729,7 @@ function candidateCount(size) {
729
729
  }
730
730
  function generationCost(width, height, generator = "map") {
731
731
  if (generator === "map" || generator === "pixflux") return 1;
732
+ if (generator === "imagePro") return 40;
732
733
  const px = width * height;
733
734
  if (px <= 1024) return 20;
734
735
  if (px <= 2048) return 25;
@@ -740,6 +741,14 @@ function tilesCost(tileSize, variations) {
740
741
  if (px <= 2048) return 25;
741
742
  return 40;
742
743
  }
744
+ function terrainTileCount(transitionSize) {
745
+ return transitionSize === 1 ? 25 : 16;
746
+ }
747
+ function parseTerrainDescriptions(prompt) {
748
+ const parts = prompt.split(/\d+\s*\)\s*\./).map((part) => part.trim()).filter(Boolean);
749
+ if (parts.length < 2) return null;
750
+ return { lower: parts[0], upper: parts[1], transition: parts[2] };
751
+ }
743
752
  var StyleImageSchema = z.object({
744
753
  /** Path to a PNG/JPEG, relative to the manifest; the active provider validates limits. */
745
754
  path: z.string()
@@ -1019,6 +1028,48 @@ var StyleObjectSchema = z.object({
1019
1028
  * rather than inheriting.
1020
1029
  */
1021
1030
  outlineMode: z.enum(["outline", "segmentation"]).optional(),
1031
+ /**
1032
+ * `terrain` generator only. Edge length of one tile in pixels. 16 or 32
1033
+ * work in both modes; 64 needs `terrainMode: "pro"`. The API default is
1034
+ * 16. See `parseTerrainDescriptions` for how an asset's `prompt` becomes
1035
+ * the lower/upper/transition terrain descriptions this endpoint wants.
1036
+ */
1037
+ terrainTileSize: z.union([z.literal(16), z.literal(32), z.literal(64)]).optional(),
1038
+ /**
1039
+ * `terrain` generator only. `standard` is the classic Wang tileset
1040
+ * pipeline; `pro` is a newer corner-pair pipeline with its own shape
1041
+ * controls (`terrainSpreadX`, `terrainSlopeSize`, `terrainRaggedness`)
1042
+ * in place of `terrainShapeStyle`. The API default is `standard`.
1043
+ */
1044
+ terrainMode: z.enum(["standard", "pro"]).optional(),
1045
+ /**
1046
+ * `terrain` generator only, `terrainMode: "standard"`. Procedural
1047
+ * boundary geometry: `square` or `round`, 16px or 32px tiles only.
1048
+ * Rejected together with `terrainMode: "pro"`, whose own shape controls
1049
+ * are `terrainSpreadX`/`terrainSlopeSize`/`terrainRaggedness`.
1050
+ */
1051
+ terrainShapeStyle: z.enum(["square", "round"]).optional(),
1052
+ /** `terrain` generator only, `terrainMode: "pro"`. Boundary spread
1053
+ * between terrains (0 = steep, 1 = gradual). API default 0.5. */
1054
+ terrainSpreadX: z.number().min(0).max(1).optional(),
1055
+ /** `terrain` generator only, `terrainMode: "pro"`. Slope on the N/W/E
1056
+ * sides as a fraction of wall height. API default 0. */
1057
+ terrainSlopeSize: z.number().min(0).max(1).optional(),
1058
+ /** `terrain` generator only, `terrainMode: "pro"`. Terrain boundary
1059
+ * noise (0 = smooth, 1 = rough). API default 0. */
1060
+ terrainRaggedness: z.number().min(0).max(1).optional(),
1061
+ /**
1062
+ * `terrain` generator only. Visual height of the step between lower and
1063
+ * upper terrain. Without `terrainShapeStyle`, only 0, 0.25, 0.5, or 1 are
1064
+ * accepted (1 switches to the 25-tile cliff layout, where corners take a
1065
+ * third "transition" value); with it, any value from 0 to 1 works, but
1066
+ * above 0.5 switches to an extended 32-tile layout this adapter does not
1067
+ * model. API default 0.
1068
+ */
1069
+ terrainTransitionSize: z.number().min(0).max(1).optional(),
1070
+ /** `terrain` generator only. Camera angle; the API default is `high
1071
+ * top-down`. */
1072
+ terrainView: z.enum(["low top-down", "high top-down"]).optional(),
1022
1073
  /**
1023
1074
  * `pixflux` only. Whether to strip the generated background.
1024
1075
  *
@@ -1132,7 +1183,7 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
1132
1183
  message: "tileFeature and styleImages cannot be combined; a connectable set derives its own tile geometry, so remove one or the other",
1133
1184
  path: ["tileFeature"]
1134
1185
  }).superRefine((style, ctx) => {
1135
- if (style.quality && (style.generator === "tiles" || style.generator === "animation")) {
1186
+ if (style.quality && (style.generator === "tiles" || style.generator === "terrain" || style.generator === "animation")) {
1136
1187
  ctx.addIssue({
1137
1188
  code: z.ZodIssueCode.custom,
1138
1189
  message: "quality profiles currently support single-image generators only",
@@ -1177,7 +1228,7 @@ var AssetSchema = z.object({
1177
1228
  prompt: z.string().optional(),
1178
1229
  /** Subdirectory under the style's outDir. Optional. */
1179
1230
  category: z.string().optional(),
1180
- /** Overrides the style default. `map` generator only. */
1231
+ /** Overrides the style default. `map`, `pixflux`, and `imagePro` only. */
1181
1232
  width: z.number().int().min(16).max(8192).optional(),
1182
1233
  height: z.number().int().min(16).max(8192).optional(),
1183
1234
  /** Overrides the style default. `1dir` generator only. */
@@ -5157,6 +5208,11 @@ var MapSubmitSchema = z2.object({
5157
5208
  object_id: z2.string().min(1),
5158
5209
  status: z2.string().default("processing")
5159
5210
  }).passthrough();
5211
+ var UsageSchema = z2.object({
5212
+ type: z2.string().optional(),
5213
+ usd: z2.number().nullable().optional(),
5214
+ generations: z2.number().nullable().optional()
5215
+ }).passthrough().nullable().optional();
5160
5216
  var TilesSubmitSchema = z2.object({
5161
5217
  tile_id: z2.string().min(1),
5162
5218
  background_job_id: z2.string().min(1),
@@ -5167,6 +5223,32 @@ var TilesProSchema = z2.object({
5167
5223
  kind: z2.string().nullable().default(null),
5168
5224
  tile_rules: z2.record(z2.unknown()).nullable().optional()
5169
5225
  }).passthrough();
5226
+ var TilesetSubmitSchema = z2.object({
5227
+ tileset_id: z2.string().min(1),
5228
+ background_job_id: z2.string().min(1),
5229
+ status: z2.literal("processing").default("processing")
5230
+ }).passthrough();
5231
+ var TilesetTileSchema = z2.object({
5232
+ id: z2.string().min(1),
5233
+ name: z2.string(),
5234
+ image: z2.object({ base64: z2.string().min(1), format: z2.string().default("png") }).passthrough(),
5235
+ corners: z2.object({ NW: z2.string(), NE: z2.string(), SW: z2.string(), SE: z2.string() }).passthrough(),
5236
+ pattern_4x4: z2.object({
5237
+ row_0: z2.array(z2.number()),
5238
+ row_1: z2.array(z2.number()),
5239
+ row_2: z2.array(z2.number()),
5240
+ row_3: z2.array(z2.number())
5241
+ }).passthrough()
5242
+ }).passthrough();
5243
+ var TilesetGetSchema = z2.object({
5244
+ tileset: z2.object({
5245
+ total_tiles: z2.number().int().min(1),
5246
+ tile_size: z2.object({ width: z2.number().int().positive(), height: z2.number().int().positive() }).passthrough(),
5247
+ terrain_types: z2.array(z2.string()),
5248
+ tiles: z2.array(TilesetTileSchema).min(1)
5249
+ }).passthrough(),
5250
+ usage: UsageSchema
5251
+ }).passthrough();
5170
5252
  var RevisionJobSubmitSchema = z2.object({
5171
5253
  background_job_id: z2.string().min(1),
5172
5254
  status: z2.string().default("processing")
@@ -5198,11 +5280,6 @@ var MapObjectSchema = z2.object({
5198
5280
  var ObjectListSchema = z2.object({ objects: z2.array(PixelLabObjectSchema), total: z2.number().int().min(0) }).passthrough();
5199
5281
  var PixfluxResponseSchema = z2.object({ image: z2.object({ base64: z2.string().min(1) }).passthrough(), usage: z2.unknown().optional() }).passthrough();
5200
5282
  var SelectFramesSchema = z2.object({ created_object_ids: z2.array(z2.string()) }).passthrough();
5201
- var UsageSchema = z2.object({
5202
- type: z2.string().optional(),
5203
- usd: z2.number().nullable().optional(),
5204
- generations: z2.number().nullable().optional()
5205
- }).passthrough().nullable().optional();
5206
5283
  var CharacterSubmitSchema = z2.object({
5207
5284
  background_job_id: z2.string().min(1),
5208
5285
  character_id: z2.string().min(1),
@@ -5418,6 +5495,45 @@ var PixelLabClient = class {
5418
5495
  "get tiles"
5419
5496
  );
5420
5497
  }
5498
+ /**
5499
+ * `/create-tileset`: two named terrain levels (`lower`/`upper`) and the
5500
+ * transition between them, laid out as a Wang corner set. Unlike
5501
+ * `/create-tiles-pro`, the descriptions are separate fields the API itself
5502
+ * places on the terrain vertex grid, not one prompt it splits by number.
5503
+ */
5504
+ async createTileset(args) {
5505
+ const body = {
5506
+ lower_description: args.lowerDescription,
5507
+ upper_description: args.upperDescription
5508
+ };
5509
+ if (args.transitionDescription) body.transition_description = args.transitionDescription;
5510
+ if (args.tileSize != null) body.tile_size = { width: args.tileSize, height: args.tileSize };
5511
+ if (args.mode) body.mode = args.mode;
5512
+ if (args.shapeStyle) body.shape_style = args.shapeStyle;
5513
+ if (args.spreadX != null) body.spread_x = args.spreadX;
5514
+ if (args.slopeSize != null) body.slope_size = args.slopeSize;
5515
+ if (args.raggedness != null) body.raggedness = args.raggedness;
5516
+ if (args.transitionSize != null) body.transition_size = args.transitionSize;
5517
+ if (args.view) body.view = args.view;
5518
+ if (args.outline) body.outline = args.outline;
5519
+ if (args.shading) body.shading = args.shading;
5520
+ if (args.detail) body.detail = args.detail;
5521
+ if (args.seed != null) body.seed = args.seed;
5522
+ return validateResponse(
5523
+ TilesetSubmitSchema,
5524
+ await this.request("/create-tileset", { method: "POST", body: JSON.stringify(body) }),
5525
+ "create tileset"
5526
+ );
5527
+ }
5528
+ /** Throws PixelLabError(423) while the set is still drawing; see Tileset. */
5529
+ async getTileset(tilesetId) {
5530
+ const res = await validateResponse(
5531
+ TilesetGetSchema,
5532
+ await this.request(`/tilesets/${tilesetId}`),
5533
+ "get tileset"
5534
+ );
5535
+ return { tileset: res.tileset, usage: res.usage ?? null };
5536
+ }
5421
5537
  /**
5422
5538
  * Synchronous single-image generation. Returns the PNG inline rather than a
5423
5539
  * job id, and is the only endpoint that honours a forced palette;
@@ -5718,6 +5834,34 @@ var PixelLabClient = class {
5718
5834
  "edit-images-v2"
5719
5835
  );
5720
5836
  }
5837
+ /**
5838
+ * PixelLab's Pro image tier, `/generate-image-v2`: a flat 40 generations
5839
+ * (docs/ENDPOINTS.md, "Single-image generators, measured") for real style
5840
+ * transfer and non-square canvases up to 792x688, where `pixflux` is
5841
+ * limited to 400x400 and no style reference. Like a revision, this hands
5842
+ * back a plain background job with no resource of its own; unlike a
5843
+ * revision, one call returns several candidate images to pick from (the
5844
+ * same 4/16/64-by-size tiering as `1dir`), not a single result, and its
5845
+ * completed shape has not been exercised live yet — `pollImagePro` in
5846
+ * pixellab.ts fails loudly rather than guess if `last_response.images`
5847
+ * turns out not to be the real field name.
5848
+ *
5849
+ * Reference images and a style image (`reference_images`, `style_image` +
5850
+ * `style_options`) exist on this endpoint but are not modeled here yet.
5851
+ */
5852
+ async createImagePro(args) {
5853
+ const body = {
5854
+ description: args.description,
5855
+ image_size: { width: args.width, height: args.height }
5856
+ };
5857
+ if (args.noBackground != null) body.no_background = args.noBackground;
5858
+ if (args.seed != null) body.seed = args.seed;
5859
+ return validateResponse(
5860
+ RevisionJobSubmitSchema,
5861
+ await this.request("/generate-image-v2", { method: "POST", body: JSON.stringify(body) }),
5862
+ "generate-image-v2"
5863
+ );
5864
+ }
5721
5865
  async getBackgroundJob(jobId) {
5722
5866
  const raw = await this.request(`/background-jobs/${encodeURIComponent(jobId)}`);
5723
5867
  return validateResponse(BackgroundJobSchema, raw, "background-jobs/{id}");
@@ -5856,7 +6000,7 @@ var PixelLabProvider = class _PixelLabProvider {
5856
6000
  return new _PixelLabProvider(new PixelLabClient("download-only"));
5857
6001
  }
5858
6002
  supports(generator) {
5859
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "character";
6003
+ return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character";
5860
6004
  }
5861
6005
  /**
5862
6006
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
@@ -5892,7 +6036,7 @@ var PixelLabProvider = class _PixelLabProvider {
5892
6036
  const height = spec.revision.sourceHeight ?? spec.height;
5893
6037
  return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
5894
6038
  }
5895
- if (spec.generator === "tiles") {
6039
+ if (spec.generator === "tiles" || spec.generator === "terrain") {
5896
6040
  return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
5897
6041
  }
5898
6042
  if (spec.generator === "character") {
@@ -5901,7 +6045,7 @@ var PixelLabProvider = class _PixelLabProvider {
5901
6045
  return {
5902
6046
  unit: "generations",
5903
6047
  amount: generationCost(spec.width, spec.height, spec.generator),
5904
- candidates: spec.generator === "1dir" ? candidateCount(spec.size) : 1
6048
+ candidates: spec.generator === "1dir" || spec.generator === "imagePro" ? candidateCount(spec.size) : 1
5905
6049
  };
5906
6050
  }
5907
6051
  validate(spec, styleImages) {
@@ -5917,6 +6061,11 @@ var PixelLabProvider = class _PixelLabProvider {
5917
6061
  if ((spec.generator === "map" || spec.generator === "pixflux") && (spec.width < 16 || spec.height < 16 || spec.width > 400 || spec.height > 400)) {
5918
6062
  throw new Error(`PixelLab ${spec.generator} dimensions must be between 16 and 400 pixels`);
5919
6063
  }
6064
+ if (spec.generator === "imagePro" && (spec.width < 16 || spec.height < 16 || spec.width > 792 || spec.height > 688)) {
6065
+ throw new Error(
6066
+ `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)`
6067
+ );
6068
+ }
5920
6069
  if (spec.generator === "map") {
5921
6070
  requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
5922
6071
  requirePixelLabOption("outline", spec.outline, [
@@ -5936,10 +6085,60 @@ var PixelLabProvider = class _PixelLabProvider {
5936
6085
  "high detail"
5937
6086
  ]);
5938
6087
  }
6088
+ if (spec.generator === "terrain") {
6089
+ if (spec.outline) {
6090
+ requirePixelLabOption("outline", spec.outline, [
6091
+ "single color black outline",
6092
+ "single color outline",
6093
+ "selective outline",
6094
+ "lineless"
6095
+ ]);
6096
+ }
6097
+ if (spec.shading) {
6098
+ requirePixelLabOption("shading", spec.shading, [
6099
+ "flat shading",
6100
+ "basic shading",
6101
+ "medium shading",
6102
+ "detailed shading",
6103
+ "highly detailed shading"
6104
+ ]);
6105
+ }
6106
+ if (spec.detail) {
6107
+ requirePixelLabOption("detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
6108
+ }
6109
+ if (spec.terrainShapeStyle && spec.terrainMode === "pro") {
6110
+ throw new Error(
6111
+ "PixelLab terrain: terrainShapeStyle is standard-mode only; pro's own shape controls are terrainSpreadX/terrainSlopeSize/terrainRaggedness"
6112
+ );
6113
+ }
6114
+ if (spec.terrainTileSize === 64 && spec.terrainMode !== "pro") {
6115
+ throw new Error('PixelLab terrain: a 64px tile needs terrainMode: "pro"');
6116
+ }
6117
+ if (spec.terrainShapeStyle && spec.terrainTransitionSize != null && spec.terrainTransitionSize > 0.5) {
6118
+ throw new Error(
6119
+ "PixelLab terrain: terrainShapeStyle with terrainTransitionSize above 0.5 uses an extended 32-tile layout pixelkiln does not model; use 0, 0.25, or 0.5"
6120
+ );
6121
+ }
6122
+ if (!spec.terrainShapeStyle && spec.terrainTransitionSize != null && ![0, 0.25, 0.5, 1].includes(spec.terrainTransitionSize)) {
6123
+ throw new Error(
6124
+ "PixelLab terrain: terrainTransitionSize must be 0, 0.25, 0.5, or 1 unless terrainShapeStyle is set"
6125
+ );
6126
+ }
6127
+ }
5939
6128
  if (spec.generator === "character") this.validateCharacter(spec, styleImages);
5940
6129
  if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
5941
6130
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
5942
6131
  }
6132
+ if (spec.generator === "terrain" && styleImages.length) {
6133
+ throw new Error(
6134
+ "PixelLab terrain does not support style images yet; use color_image/reference images directly against /create-tileset if this becomes a real need"
6135
+ );
6136
+ }
6137
+ if (spec.generator === "imagePro" && styleImages.length) {
6138
+ throw new Error(
6139
+ "PixelLab imagePro does not support style images yet; /generate-image-v2's own reference_images and style_image are not modeled here"
6140
+ );
6141
+ }
5943
6142
  for (const image of styleImages) {
5944
6143
  if (image.width > 256 || image.height > 256) {
5945
6144
  throw new Error(
@@ -6302,6 +6501,36 @@ var PixelLabProvider = class _PixelLabProvider {
6302
6501
  });
6303
6502
  return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
6304
6503
  }
6504
+ if (spec.generator === "terrain") {
6505
+ const res2 = await this.client.createTileset({
6506
+ lowerDescription: spec.terrainLowerDescription,
6507
+ upperDescription: spec.terrainUpperDescription,
6508
+ transitionDescription: spec.terrainTransitionDescription,
6509
+ tileSize: spec.terrainTileSize,
6510
+ mode: spec.terrainMode,
6511
+ shapeStyle: spec.terrainShapeStyle,
6512
+ spreadX: spec.terrainSpreadX,
6513
+ slopeSize: spec.terrainSlopeSize,
6514
+ raggedness: spec.terrainRaggedness,
6515
+ transitionSize: spec.terrainTransitionSize,
6516
+ view: spec.terrainView,
6517
+ outline: spec.outline,
6518
+ shading: spec.shading,
6519
+ detail: spec.detail,
6520
+ seed: spec.seed
6521
+ });
6522
+ return { jobId: res2.tileset_id, metadata: { backgroundJobId: res2.background_job_id } };
6523
+ }
6524
+ if (spec.generator === "imagePro") {
6525
+ const res2 = await this.client.createImagePro({
6526
+ description: spec.prompt,
6527
+ width: spec.width,
6528
+ height: spec.height,
6529
+ noBackground: spec.noBackground,
6530
+ seed: spec.seed
6531
+ });
6532
+ return { jobId: res2.background_job_id };
6533
+ }
6305
6534
  if (spec.generator === "1dir") {
6306
6535
  const res2 = await this.client.create1Direction({
6307
6536
  description: spec.prompt,
@@ -6407,6 +6636,8 @@ var PixelLabProvider = class _PixelLabProvider {
6407
6636
  }
6408
6637
  if (generator === "map") return this.pollMap(jobId, context);
6409
6638
  if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature), context);
6639
+ if (generator === "terrain") return this.pollTerrain(jobId, context);
6640
+ if (generator === "imagePro") return this.pollImagePro(jobId);
6410
6641
  if (generator === "character") return this.pollCharacter(jobId, context);
6411
6642
  const backgroundJobId = context?.metadata?.backgroundJobId;
6412
6643
  const obj = await this.client.getObject(jobId);
@@ -6477,6 +6708,40 @@ var PixelLabProvider = class _PixelLabProvider {
6477
6708
  error: `Invalid PixelLab response for revision job ${jobId}: completed with no recognized image field (got: ${Object.keys(done).join(", ") || "no keys"}); update pollRevision in src/providers/pixellab.ts with the real shape`
6478
6709
  };
6479
6710
  }
6711
+ /**
6712
+ * `/generate-image-v2` hands back a plain background job, the same as a
6713
+ * revision, but a completed one carries several candidate images to
6714
+ * review rather than a single result: the Python client's own usage
6715
+ * sample reads `response.images`, and `edit-images-v2` (a sibling Pro
6716
+ * endpoint) confirmed live that its own array lands at
6717
+ * `last_response.images` with each entry `extractBase64`-shaped, so this
6718
+ * assumes the same field name and shape here. That assumption is
6719
+ * UNVERIFIED for this specific endpoint — no live call has been made to
6720
+ * it — so an unrecognized response still fails loudly rather than
6721
+ * guessing, exactly like pollRevision.
6722
+ */
6723
+ async pollImagePro(jobId) {
6724
+ const job = await this.client.getBackgroundJob(jobId);
6725
+ if (job.status === "failed") return { status: "failed", error: "imagePro job failed upstream" };
6726
+ if (job.status !== "completed") return { status: "processing" };
6727
+ const billed = billedFromUsage(job.usage);
6728
+ const done = job.last_response ?? {};
6729
+ if (Array.isArray(done.images) && done.images.length) {
6730
+ const urls = [];
6731
+ for (const [index, candidate] of done.images.entries()) {
6732
+ const base64 = extractBase64(candidate);
6733
+ if (!base64) continue;
6734
+ const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
6735
+ writeFileSync(file, Buffer.from(base64, "base64"));
6736
+ urls.push(`file://${file}`);
6737
+ }
6738
+ if (urls.length) return { status: "review", candidateUrls: urls, billed };
6739
+ }
6740
+ return {
6741
+ status: "failed",
6742
+ error: `Invalid PixelLab response for imagePro job ${jobId}: completed with no recognized images array (got: ${Object.keys(done).join(", ") || "no keys"}); this endpoint's completed shape has not been exercised live yet -- update pollImagePro in src/providers/pixellab.ts with the real shape`
6743
+ };
6744
+ }
6480
6745
  /**
6481
6746
  * Map objects need their own path because the `/map-objects/{id}` record is
6482
6747
  * deleted upstream roughly 8 hours after creation while the image survives in
@@ -6545,6 +6810,43 @@ var PixelLabProvider = class _PixelLabProvider {
6545
6810
  throw err;
6546
6811
  }
6547
6812
  }
6813
+ /**
6814
+ * `/create-tileset` reports progress the same way `tiles` does (423 while
6815
+ * drawing, 200 once finished), but each tile's image comes back embedded
6816
+ * as base64 rather than a `storage_urls` link, so it is decoded straight
6817
+ * to a cache file the same way a revision result is (`pollRevision`). A
6818
+ * terrain set is always a connectable Wang tileset, never independent
6819
+ * candidates to review, so this goes straight to "ready" like a connectable
6820
+ * `tiles` set does.
6821
+ */
6822
+ async pollTerrain(tilesetId, context) {
6823
+ try {
6824
+ const { tileset, usage } = await this.client.getTileset(tilesetId);
6825
+ const backgroundJobId = context?.metadata?.backgroundJobId;
6826
+ const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
6827
+ const sources = [];
6828
+ const terrainTiles = [];
6829
+ for (const [index, tile] of tileset.tiles.entries()) {
6830
+ const slug = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
6831
+ const role = `tile-${String(index).padStart(2, "0")}-${slug}`;
6832
+ const file = path12.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
6833
+ writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
6834
+ sources.push({ url: `file://${file}`, role });
6835
+ terrainTiles.push({ role, corners: tile.corners, pattern4x4: tile.pattern_4x4 });
6836
+ }
6837
+ return {
6838
+ status: "ready",
6839
+ objectId: tilesetId,
6840
+ sourceUrl: sources[0]?.url ?? null,
6841
+ sources,
6842
+ metadata: { terrainTypes: tileset.terrain_types, terrainTiles },
6843
+ billed
6844
+ };
6845
+ } catch (err) {
6846
+ if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
6847
+ throw err;
6848
+ }
6849
+ }
6548
6850
  async selectCandidate(jobId, index, commonTag, generator) {
6549
6851
  if (generator === "tiles") {
6550
6852
  const set = await this.client.getTilesPro(jobId);
@@ -6552,6 +6854,11 @@ var PixelLabProvider = class _PixelLabProvider {
6552
6854
  if (!url) throw new Error(`tiles job ${jobId} has no variation at index ${index}`);
6553
6855
  return { objectId: `${jobId}#${index}`, sourceUrl: url };
6554
6856
  }
6857
+ if (generator === "imagePro") {
6858
+ const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
6859
+ if (!existsSync7(file)) throw new Error(`imagePro job ${jobId} has no cached candidate at index ${index}`);
6860
+ return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
6861
+ }
6555
6862
  const promoted = await this.client.selectFrames(jobId, [index], commonTag);
6556
6863
  const objectId = promoted.created_object_ids?.[0];
6557
6864
  if (!objectId) {
@@ -7895,6 +8202,10 @@ async function resolveSpecs(loaded, filter) {
7895
8202
  size = asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
7896
8203
  width = asset.state?.canvas?.width ?? size;
7897
8204
  height = asset.state?.canvas?.height ?? size;
8205
+ } else if (generator === "terrain") {
8206
+ size = style.terrainTileSize ?? 16;
8207
+ width = size;
8208
+ height = size;
7898
8209
  } else {
7899
8210
  width = asset.width ?? style.size ?? 64;
7900
8211
  height = asset.height ?? style.size ?? 64;
@@ -7907,12 +8218,33 @@ async function resolveSpecs(loaded, filter) {
7907
8218
  }
7908
8219
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
7909
8220
  const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
7910
- const prompt = generator === "character" && characterKind !== "base" ? subject.trim() : [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8221
+ let terrainLowerDescription;
8222
+ let terrainUpperDescription;
8223
+ let terrainTransitionDescription;
8224
+ let prompt;
8225
+ if (generator === "terrain") {
8226
+ const parsed = parseTerrainDescriptions(subject);
8227
+ if (!parsed) {
8228
+ throw new Error(
8229
+ `assets.${assetId}: terrain needs "1). <lower terrain> 2). <upper terrain>" in its prompt, optionally followed by "3). <transition>"`
8230
+ );
8231
+ }
8232
+ const wrap = (text) => [style.promptPrefix, text, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8233
+ terrainLowerDescription = wrap(parsed.lower);
8234
+ terrainUpperDescription = wrap(parsed.upper);
8235
+ terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
8236
+ prompt = subject.trim();
8237
+ } else if (generator === "character" && characterKind !== "base") {
8238
+ prompt = subject.trim();
8239
+ } else {
8240
+ prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8241
+ }
7911
8242
  const relFile = asset.file ?? path13.join(asset.category ?? "", `${assetId}.png`);
7912
8243
  const outFile = path13.resolve(root, style.outDir, relFile);
7913
8244
  const qualityOutFile = style.quality ? path13.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
7914
8245
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
7915
8246
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
8247
+ const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
7916
8248
  const base = {
7917
8249
  styleId,
7918
8250
  assetId,
@@ -7949,10 +8281,21 @@ async function resolveSpecs(loaded, filter) {
7949
8281
  buildingFloor2Description: generator === "tiles" ? style.buildingFloor2Description : void 0,
7950
8282
  buildingWallAngle: generator === "tiles" ? style.buildingWallAngle : void 0,
7951
8283
  outlineMode: generator === "tiles" ? style.outlineMode : void 0,
8284
+ terrainLowerDescription: generator === "terrain" ? terrainLowerDescription : void 0,
8285
+ terrainUpperDescription: generator === "terrain" ? terrainUpperDescription : void 0,
8286
+ terrainTransitionDescription: generator === "terrain" ? terrainTransitionDescription : void 0,
8287
+ terrainTileSize: generator === "terrain" ? size : void 0,
8288
+ terrainMode: generator === "terrain" ? style.terrainMode : void 0,
8289
+ terrainShapeStyle: generator === "terrain" ? style.terrainShapeStyle : void 0,
8290
+ terrainSpreadX: generator === "terrain" ? style.terrainSpreadX : void 0,
8291
+ terrainSlopeSize: generator === "terrain" ? style.terrainSlopeSize : void 0,
8292
+ terrainRaggedness: generator === "terrain" ? style.terrainRaggedness : void 0,
8293
+ terrainTransitionSize: generator === "terrain" ? style.terrainTransitionSize : void 0,
8294
+ terrainView: generator === "terrain" ? style.terrainView : void 0,
7952
8295
  ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
7953
- cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
8296
+ cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generationCost(width, height, generator),
7954
8297
  costUnit: "generations",
7955
- candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
8298
+ candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
7956
8299
  };
7957
8300
  const tags = [
7958
8301
  .../* @__PURE__ */ new Set([
@@ -8000,7 +8343,7 @@ async function resolveSpecs(loaded, filter) {
8000
8343
  if (asset.mirror) {
8001
8344
  if (asset.mirror === assetId) throw new Error(`assets.${assetId}: an asset cannot mirror itself`);
8002
8345
  const sourceSpec = await finalize(asset.mirror);
8003
- if (sourceSpec.generator === "tiles") {
8346
+ if (sourceSpec.generator === "tiles" || sourceSpec.generator === "terrain") {
8004
8347
  throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
8005
8348
  }
8006
8349
  if (sourceSpec.character?.kind === "animation") {