pixelkiln 0.42.0 → 0.43.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
@@ -665,7 +665,12 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
665
665
  parent: spec.character.parentAssetId,
666
666
  parentSha256: spec.character.parentSha256 ?? null,
667
667
  state: spec.character.state,
668
- animation: spec.character.animation
668
+ animation: spec.character.animation,
669
+ // Absent keys keep the hashes of manifests that never set these.
670
+ proportions: spec.character.proportions,
671
+ textGuidanceScale: spec.character.textGuidanceScale,
672
+ isometric: spec.character.isometric,
673
+ reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
669
674
  } : void 0,
670
675
  // A mirror's bytes come from its source's recorded outputs; the plan
671
676
  // compares those directly, so only the choice of source is identity.
@@ -783,6 +788,17 @@ var MIRRORED_DIRECTION = {
783
788
  "south-west": "south-east"
784
789
  };
785
790
  var CharacterModeSchema = z.enum(["standard", "v3", "pro"]);
791
+ var CHARACTER_PROPORTION_PRESETS = ["default", "chibi", "cartoon", "stylized", "realistic_male", "realistic_female", "heroic"];
792
+ var CharacterProportionsSchema = z.union([
793
+ z.enum(CHARACTER_PROPORTION_PRESETS),
794
+ z.object({
795
+ headSize: z.number().min(0.5).max(2).optional(),
796
+ armsLength: z.number().min(0.5).max(2).optional(),
797
+ legsLength: z.number().min(0.5).max(2).optional(),
798
+ shoulderWidth: z.number().min(0.5).max(2).optional(),
799
+ hipWidth: z.number().min(0.5).max(2).optional()
800
+ }).strict()
801
+ ]);
786
802
  var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
787
803
  var CharacterStateSchema = z.object({
788
804
  /** The base character, or another state, in the same style. */
@@ -941,6 +957,16 @@ var StyleObjectSchema = z.object({
941
957
  directions: z.union([z.literal(4), z.literal(8)]).optional(),
942
958
  /** `character` only. Body template: `mannequin` (default) or a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`). */
943
959
  template: z.string().min(1).optional(),
960
+ /** `character` only, `standard` humanoid bases. A preset or multipliers; an asset may override it. */
961
+ proportions: CharacterProportionsSchema.optional(),
962
+ /**
963
+ * `character` only. How closely a `standard` base and a template loop
964
+ * follow their text, 1 to 20; PixelLab's default is 8. The v3 and pro
965
+ * engines do not take it.
966
+ */
967
+ textGuidanceScale: z.number().min(1).max(20).optional(),
968
+ /** `character` only. Draw `standard` bases and every loop in isometric view. */
969
+ isometric: z.boolean().optional(),
944
970
  /** Fixed seed for reproducibility where the endpoint supports it. */
945
971
  seed: z.number().int().optional(),
946
972
  /**
@@ -1100,6 +1126,19 @@ var AssetSchema = z.object({
1100
1126
  state: CharacterStateSchema.optional(),
1101
1127
  /** `character` styles: this asset is a loop of another character asset in one direction. */
1102
1128
  animation: CharacterAnimationSchema.optional(),
1129
+ /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1130
+ proportions: CharacterProportionsSchema.optional(),
1131
+ /**
1132
+ * `character` styles, bases only: the character's own sprite, which
1133
+ * PixelLab rotates into the other directions instead of drawing from
1134
+ * the prompt. A manifest-relative PNG or JPEG of the south-facing
1135
+ * sprite, or an object keyed by direction (`{ "south": ..., "east":
1136
+ * ... }`); quadrupeds in `standard` mode need south and east, and only
1137
+ * `standard` takes more than south. `standard` wants each image at the
1138
+ * style's size; v3 accepts up to 256px, pro up to 168px. The prompt
1139
+ * still guides the result.
1140
+ */
1141
+ reference: z.union([z.string().min(1), z.record(CharacterDirectionSchema, z.string().min(1))]).optional(),
1103
1142
  /**
1104
1143
  * Another asset of the same style flipped left to right, made locally
1105
1144
  * from that asset's downloaded files at no generation cost.
@@ -1174,6 +1213,16 @@ var AssetSchema = z.object({
1174
1213
  path: ["prompt"]
1175
1214
  });
1176
1215
  }
1216
+ if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
1217
+ context.addIssue({
1218
+ code: z.ZodIssueCode.custom,
1219
+ message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
1220
+ path: ["reference"]
1221
+ });
1222
+ }
1223
+ if (typeof asset.reference === "object" && !Object.keys(asset.reference).length) {
1224
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "a reference needs at least a south image", path: ["reference"] });
1225
+ }
1177
1226
  });
1178
1227
  var DEFAULT_HISTORY_LIMIT = 5;
1179
1228
  var HistoryLimitSchema = z.number().int().min(0).max(100);
@@ -5016,6 +5065,17 @@ var PixelLabError = class extends ProviderError {
5016
5065
  }
5017
5066
  body;
5018
5067
  };
5068
+ function proportionsPayload(proportions) {
5069
+ if (typeof proportions === "string") return { type: "preset", name: proportions };
5070
+ return {
5071
+ type: "custom",
5072
+ ...proportions.headSize !== void 0 ? { head_size: proportions.headSize } : {},
5073
+ ...proportions.armsLength !== void 0 ? { arms_length: proportions.armsLength } : {},
5074
+ ...proportions.legsLength !== void 0 ? { legs_length: proportions.legsLength } : {},
5075
+ ...proportions.shoulderWidth !== void 0 ? { shoulder_width: proportions.shoulderWidth } : {},
5076
+ ...proportions.hipWidth !== void 0 ? { hip_width: proportions.hipWidth } : {}
5077
+ };
5078
+ }
5019
5079
  var PixelLabClient = class {
5020
5080
  constructor(apiKey, timeoutMs = 12e4) {
5021
5081
  this.apiKey = apiKey;
@@ -5224,10 +5284,17 @@ var PixelLabClient = class {
5224
5284
  * A base character. Standard mode picks the 4- or 8-direction template
5225
5285
  * endpoint; v3 and pro have their own and always draw 8. Every one answers
5226
5286
  * at once with the character id and a background job.
5287
+ *
5288
+ * A `reference` is the author's own sprite: standard takes one per
5289
+ * direction and draws the rest, v3 rotates the south one, and pro
5290
+ * switches to its rotate method for it. A `styleReference` is pro's style
5291
+ * anchor; the other engines have no such input.
5227
5292
  */
5228
5293
  async createCharacter(args) {
5229
5294
  const imageSize = { width: args.size, height: args.size };
5230
5295
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
5296
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
5297
+ const south = args.reference?.south;
5231
5298
  let path45;
5232
5299
  let body;
5233
5300
  if (args.mode === "v3") {
@@ -5240,7 +5307,8 @@ var PixelLabClient = class {
5240
5307
  ...args.view ? { view: args.view } : {},
5241
5308
  ...args.outline ? { outline: args.outline } : {},
5242
5309
  ...args.detail ? { detail: args.detail } : {},
5243
- ...args.seed != null ? { seed: args.seed } : {}
5310
+ ...args.seed != null ? { seed: args.seed } : {},
5311
+ ...south ? { reference_image: encode(south) } : {}
5244
5312
  };
5245
5313
  } else if (args.mode === "pro") {
5246
5314
  path45 = "/create-character-pro";
@@ -5249,8 +5317,10 @@ var PixelLabClient = class {
5249
5317
  image_size: imageSize,
5250
5318
  template_id: args.template,
5251
5319
  no_background: args.noBackground ?? true,
5320
+ method: south ? "rotate_character" : "create_with_style",
5252
5321
  ...args.view ? { view: args.view } : {},
5253
- ...args.seed != null ? { seed: args.seed } : {}
5322
+ ...args.seed != null ? { seed: args.seed } : {},
5323
+ ...south ? { reference_image: encode(south) } : args.styleReference ? { reference_image: encode(args.styleReference) } : {}
5254
5324
  };
5255
5325
  } else {
5256
5326
  path45 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
@@ -5263,6 +5333,10 @@ var PixelLabClient = class {
5263
5333
  ...args.shading ? { shading: args.shading } : {},
5264
5334
  ...args.detail ? { detail: args.detail } : {},
5265
5335
  ...args.seed != null ? { seed: args.seed } : {},
5336
+ ...args.proportions !== void 0 ? { proportions: proportionsPayload(args.proportions) } : {},
5337
+ ...args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
5338
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
5339
+ ...args.reference && Object.keys(args.reference).length ? { directions: Object.fromEntries(Object.entries(args.reference).flatMap(([direction, image]) => image ? [[direction, encode(image)]] : [])) } : {},
5266
5340
  ...palette
5267
5341
  };
5268
5342
  }
@@ -5302,6 +5376,8 @@ var PixelLabClient = class {
5302
5376
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
5303
5377
  ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
5304
5378
  ...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
5379
+ ...args.mode === "template" && args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
5380
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
5305
5381
  ...args.seed != null ? { seed: args.seed } : {}
5306
5382
  })
5307
5383
  });
@@ -5498,8 +5574,8 @@ var PixelLabProvider = class _PixelLabProvider {
5498
5574
  "high detail"
5499
5575
  ]);
5500
5576
  }
5501
- if (spec.generator === "character") this.validateCharacter(spec);
5502
- if ((spec.generator === "map" || spec.generator === "pixflux" || spec.generator === "character") && styleImages.length) {
5577
+ if (spec.generator === "character") this.validateCharacter(spec, styleImages);
5578
+ if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
5503
5579
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
5504
5580
  }
5505
5581
  for (const image of styleImages) {
@@ -5515,9 +5591,61 @@ var PixelLabProvider = class _PixelLabProvider {
5515
5591
  );
5516
5592
  }
5517
5593
  }
5518
- validateCharacter(spec) {
5594
+ validateCharacter(spec, styleImages = []) {
5519
5595
  const character = spec.character;
5520
5596
  if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
5597
+ const label = `${spec.styleId}/${spec.assetId}`;
5598
+ if (character.kind === "base" && character.proportions !== void 0) {
5599
+ if (character.mode !== "standard") {
5600
+ throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
5601
+ }
5602
+ if (character.template !== "mannequin") {
5603
+ throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
5604
+ }
5605
+ }
5606
+ if (character.reference) {
5607
+ const given = Object.keys(character.reference);
5608
+ if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
5609
+ if (character.mode !== "standard") {
5610
+ if (given.length > 1) throw new Error(`${label}: PixelLab ${character.mode} rotates one south-facing reference; drop the other directions`);
5611
+ const limit = character.mode === "v3" ? 256 : 168;
5612
+ const south = character.reference.south;
5613
+ if (south.width > limit || south.height > limit) {
5614
+ throw new Error(`${label}: reference is ${south.width}x${south.height}; PixelLab ${character.mode} takes up to ${limit}px`);
5615
+ }
5616
+ } else {
5617
+ const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
5618
+ for (const direction of given) {
5619
+ if (!allowed.includes(direction)) {
5620
+ throw new Error(`${label}: reference "${direction}" is not one of this character's ${character.directions} directions`);
5621
+ }
5622
+ const image = character.reference[direction];
5623
+ if (image.width !== spec.width || image.height !== spec.height) {
5624
+ throw new Error(
5625
+ `${label}: reference (${direction}) is ${image.width}x${image.height}; standard mode wants each image at the style's size, ${spec.width}x${spec.height}`
5626
+ );
5627
+ }
5628
+ }
5629
+ if (character.template !== "mannequin" && !character.reference.east) {
5630
+ throw new Error(`${label}: a ${character.template} reference needs south and east images`);
5631
+ }
5632
+ }
5633
+ }
5634
+ if (styleImages.length) {
5635
+ if (character.mode !== "pro") {
5636
+ throw new Error(
5637
+ `PixelLab ${character.mode} characters take no style images; give a base its own sprite with \`reference\`, or use mode pro for a style anchor`
5638
+ );
5639
+ }
5640
+ if (styleImages.length > 1) throw new Error("PixelLab pro characters take one style image");
5641
+ if (character.kind === "base" && character.reference) {
5642
+ throw new Error(`${label}: a pro base rotating its own reference takes no style image`);
5643
+ }
5644
+ const image = styleImages[0];
5645
+ if (image.width > 168 || image.height > 168) {
5646
+ throw new Error(`PixelLab pro character style image is ${image.width}x${image.height}; the limit is 168px`);
5647
+ }
5648
+ }
5521
5649
  const maxSize = character.mode === "v3" ? 256 : 128;
5522
5650
  if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
5523
5651
  throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
@@ -5553,10 +5681,11 @@ var PixelLabProvider = class _PixelLabProvider {
5553
5681
  * asset so a later run can find it, and clears its own earlier take for
5554
5682
  * that direction first, since PixelLab skips a direction that exists.
5555
5683
  */
5556
- async submitCharacter(spec, context) {
5684
+ async submitCharacter(spec, styleImages, context) {
5557
5685
  const character = spec.character;
5558
5686
  if (character.kind === "base") {
5559
5687
  const swatch = spec.palette.length && character.mode === "standard" ? paletteSwatch(spec.palette).toString("base64") : void 0;
5688
+ const styleImage = styleImages[0];
5560
5689
  const res2 = await this.client.createCharacter({
5561
5690
  mode: character.mode,
5562
5691
  description: spec.prompt,
@@ -5569,7 +5698,12 @@ var PixelLabProvider = class _PixelLabProvider {
5569
5698
  detail: spec.detail,
5570
5699
  seed: spec.seed,
5571
5700
  noBackground: spec.noBackground,
5572
- paletteSwatchBase64: swatch
5701
+ paletteSwatchBase64: swatch,
5702
+ proportions: character.proportions,
5703
+ textGuidanceScale: character.textGuidanceScale,
5704
+ isometric: character.isometric,
5705
+ reference: character.reference ? readReference(spec, character.reference) : void 0,
5706
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0
5573
5707
  });
5574
5708
  return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
5575
5709
  }
@@ -5611,7 +5745,9 @@ var PixelLabProvider = class _PixelLabProvider {
5611
5745
  frameCount: animation.frames,
5612
5746
  keepFirstFrame: animation.keepFirstFrame,
5613
5747
  directions: [animation.direction],
5614
- seed: spec.seed
5748
+ seed: spec.seed,
5749
+ textGuidanceScale: character.textGuidanceScale,
5750
+ isometric: character.isometric
5615
5751
  });
5616
5752
  const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
5617
5753
  return {
@@ -5701,7 +5837,7 @@ var PixelLabProvider = class _PixelLabProvider {
5701
5837
  }
5702
5838
  async submit(spec, styleImages, context) {
5703
5839
  this.validate(spec, styleImages);
5704
- if (spec.generator === "character") return this.submitCharacter(spec, context);
5840
+ if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
5705
5841
  if (spec.generator === "pixflux") {
5706
5842
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
5707
5843
  const { png } = await this.client.createImagePixflux({
@@ -5968,6 +6104,17 @@ function firstUrl(urls) {
5968
6104
  if (!urls) return null;
5969
6105
  return Object.values(urls).find((u) => typeof u === "string") ?? null;
5970
6106
  }
6107
+ function readReference(spec, reference) {
6108
+ const images = {};
6109
+ for (const [direction, image] of Object.entries(reference)) {
6110
+ const bytes = readFileSync2(image.path);
6111
+ if (sha256(bytes) !== image.sha256) {
6112
+ throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
6113
+ }
6114
+ images[direction] = { base64: bytes.toString("base64"), format: image.format };
6115
+ }
6116
+ return images;
6117
+ }
5971
6118
  function rotationSources(character) {
5972
6119
  const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
5973
6120
  const sources = [];
@@ -7087,16 +7234,16 @@ async function resolveSpecs(loaded, filter) {
7087
7234
  }
7088
7235
  }
7089
7236
  const styleImageCache = /* @__PURE__ */ new Map();
7090
- async function loadStyleImage(rel) {
7237
+ async function loadStyleImage(rel, what = "Style image") {
7091
7238
  const abs = path13.resolve(root, rel);
7092
7239
  let hit = styleImageCache.get(abs);
7093
7240
  if (!hit) {
7094
- if (!existsSync8(abs)) throw new Error(`Style image not found: ${abs}`);
7241
+ if (!existsSync8(abs)) throw new Error(`${what} not found: ${abs}`);
7095
7242
  const buf = await readFile6(abs);
7096
7243
  const metadata = imageMetadata(buf);
7097
- if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
7244
+ if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
7098
7245
  if (metadata.width < 1 || metadata.height < 1) {
7099
- throw new Error(`Style image has invalid dimensions: ${abs}`);
7246
+ throw new Error(`${what} has invalid dimensions: ${abs}`);
7100
7247
  }
7101
7248
  hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
7102
7249
  styleImageCache.set(abs, hit);
@@ -7203,7 +7350,7 @@ async function resolveSpecs(loaded, filter) {
7203
7350
  tileView: generator === "tiles" ? style.tileView : void 0,
7204
7351
  tileFeature: generator === "tiles" ? style.tileFeature : void 0,
7205
7352
  outlineMode: generator === "tiles" ? style.outlineMode : void 0,
7206
- ...generator === "character" ? { character: resolveCharacterShape(asset, style, characterKind) } : {},
7353
+ ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
7207
7354
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
7208
7355
  costUnit: "generations",
7209
7356
  candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
@@ -7445,15 +7592,33 @@ function imageMetadata(buf) {
7445
7592
  }
7446
7593
  return null;
7447
7594
  }
7448
- function resolveCharacterShape(asset, style, kind) {
7595
+ async function resolveCharacterShape(asset, style, kind, files) {
7449
7596
  const mode2 = style.mode ?? "standard";
7450
7597
  const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
7598
+ const proportions = asset.proportions ?? style.proportions;
7451
7599
  const shape = {
7452
7600
  kind,
7453
7601
  mode: mode2,
7454
7602
  directions,
7455
- template: style.template ?? "mannequin"
7603
+ template: style.template ?? "mannequin",
7604
+ ...proportions !== void 0 ? { proportions } : {},
7605
+ ...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
7606
+ ...style.isometric !== void 0 ? { isometric: style.isometric } : {}
7456
7607
  };
7608
+ if (asset.reference) {
7609
+ const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
7610
+ shape.reference = {};
7611
+ for (const [direction, rel] of Object.entries(byDirection)) {
7612
+ const image = await files.load(rel, `Reference sprite (${direction})`);
7613
+ shape.reference[direction] = {
7614
+ path: path13.resolve(files.root, rel),
7615
+ sha256: image.hash,
7616
+ width: image.width,
7617
+ height: image.height,
7618
+ format: image.format
7619
+ };
7620
+ }
7621
+ }
7457
7622
  if (asset.state) {
7458
7623
  shape.state = {
7459
7624
  paletteFromReference: asset.state.paletteFromReference,