pixelkiln 0.42.0 → 0.44.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,18 @@ 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
+ // Pose images enter as their hashes, never their paths.
669
+ animation: spec.character.animation ? {
670
+ ...spec.character.animation,
671
+ startFrame: spec.character.animation.startFrame?.sha256,
672
+ endFrame: spec.character.animation.endFrame?.sha256
673
+ } : void 0,
674
+ // Absent keys keep the hashes of manifests that never set these.
675
+ proportions: spec.character.proportions,
676
+ textGuidanceScale: spec.character.textGuidanceScale,
677
+ isometric: spec.character.isometric,
678
+ enhancePrompt: spec.character.enhancePrompt,
679
+ reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
669
680
  } : void 0,
670
681
  // A mirror's bytes come from its source's recorded outputs; the plan
671
682
  // compares those directly, so only the choice of source is identity.
@@ -783,6 +794,17 @@ var MIRRORED_DIRECTION = {
783
794
  "south-west": "south-east"
784
795
  };
785
796
  var CharacterModeSchema = z.enum(["standard", "v3", "pro"]);
797
+ var CHARACTER_PROPORTION_PRESETS = ["default", "chibi", "cartoon", "stylized", "realistic_male", "realistic_female", "heroic"];
798
+ var CharacterProportionsSchema = z.union([
799
+ z.enum(CHARACTER_PROPORTION_PRESETS),
800
+ z.object({
801
+ headSize: z.number().min(0.5).max(2).optional(),
802
+ armsLength: z.number().min(0.5).max(2).optional(),
803
+ legsLength: z.number().min(0.5).max(2).optional(),
804
+ shoulderWidth: z.number().min(0.5).max(2).optional(),
805
+ hipWidth: z.number().min(0.5).max(2).optional()
806
+ }).strict()
807
+ ]);
786
808
  var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
787
809
  var CharacterStateSchema = z.object({
788
810
  /** The base character, or another state, in the same style. */
@@ -809,11 +831,45 @@ var CharacterAnimationSchema = z.object({
809
831
  /** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
810
832
  keepFirstFrame: z.boolean().default(true),
811
833
  /** `template` when a template is named, otherwise `v3`; `pro` for the sequential high-quality engine. */
812
- mode: CharacterAnimationModeSchema.optional()
834
+ mode: CharacterAnimationModeSchema.optional(),
835
+ /**
836
+ * v3 only: a manifest-relative image of the pose to start from, instead
837
+ * of the character's rotation for this direction. Up to 256px.
838
+ */
839
+ startFrame: z.string().min(1).optional(),
840
+ /**
841
+ * v3 only: a pose to animate toward. The loop then interpolates from the
842
+ * start frame (the rotation, or `startFrame`) to this image, which must
843
+ * be the same size as the start frame.
844
+ */
845
+ endFrame: z.string().min(1).optional(),
846
+ /** What is being animated, when the character's own description would mislead the model. */
847
+ subject: z.string().min(1).optional(),
848
+ /** Template mode only: style hints for this loop, over the character's own. */
849
+ outline: z.string().min(1).optional(),
850
+ shading: z.string().min(1).optional(),
851
+ detail: z.string().min(1).optional(),
852
+ /** v3 text loops only: let PixelLab expand the action into a fuller motion description first. */
853
+ enhancePrompt: z.boolean().optional()
813
854
  }).strict().superRefine((animation, context) => {
814
855
  if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
815
856
  context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
816
857
  }
858
+ const mode2 = animation.mode ?? (animation.template ? "template" : "v3");
859
+ for (const key of ["startFrame", "endFrame", "enhancePrompt"]) {
860
+ if (animation[key] !== void 0 && mode2 !== "v3") {
861
+ context.addIssue({
862
+ code: z.ZodIssueCode.custom,
863
+ message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" ? "starts from the character's rotation and follows its template" : "takes neither"}`,
864
+ path: [key]
865
+ });
866
+ }
867
+ }
868
+ for (const key of ["outline", "shading", "detail"]) {
869
+ if (animation[key] !== void 0 && mode2 !== "template") {
870
+ context.addIssue({ code: z.ZodIssueCode.custom, message: `${key} overrides apply to template loops only`, path: [key] });
871
+ }
872
+ }
817
873
  if (animation.template && animation.mode && animation.mode !== "template") {
818
874
  context.addIssue({
819
875
  code: z.ZodIssueCode.custom,
@@ -941,6 +997,18 @@ var StyleObjectSchema = z.object({
941
997
  directions: z.union([z.literal(4), z.literal(8)]).optional(),
942
998
  /** `character` only. Body template: `mannequin` (default) or a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`). */
943
999
  template: z.string().min(1).optional(),
1000
+ /** `character` only, `standard` humanoid bases. A preset or multipliers; an asset may override it. */
1001
+ proportions: CharacterProportionsSchema.optional(),
1002
+ /**
1003
+ * `character` only. How closely a `standard` base and a template loop
1004
+ * follow their text, 1 to 20; PixelLab's default is 8. The v3 and pro
1005
+ * engines do not take it.
1006
+ */
1007
+ textGuidanceScale: z.number().min(1).max(20).optional(),
1008
+ /** `character` only. Draw `standard` bases and every loop in isometric view. */
1009
+ isometric: z.boolean().optional(),
1010
+ /** `character` only, `v3` bases: let PixelLab expand the prompt into a fuller one before drawing. */
1011
+ enhancePrompt: z.boolean().optional(),
944
1012
  /** Fixed seed for reproducibility where the endpoint supports it. */
945
1013
  seed: z.number().int().optional(),
946
1014
  /**
@@ -1100,6 +1168,19 @@ var AssetSchema = z.object({
1100
1168
  state: CharacterStateSchema.optional(),
1101
1169
  /** `character` styles: this asset is a loop of another character asset in one direction. */
1102
1170
  animation: CharacterAnimationSchema.optional(),
1171
+ /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1172
+ proportions: CharacterProportionsSchema.optional(),
1173
+ /**
1174
+ * `character` styles, bases only: the character's own sprite, which
1175
+ * PixelLab rotates into the other directions instead of drawing from
1176
+ * the prompt. A manifest-relative PNG or JPEG of the south-facing
1177
+ * sprite, or an object keyed by direction (`{ "south": ..., "east":
1178
+ * ... }`); quadrupeds in `standard` mode need south and east, and only
1179
+ * `standard` takes more than south. `standard` wants each image at the
1180
+ * style's size; v3 accepts up to 256px, pro up to 168px. The prompt
1181
+ * still guides the result.
1182
+ */
1183
+ reference: z.union([z.string().min(1), z.record(CharacterDirectionSchema, z.string().min(1))]).optional(),
1103
1184
  /**
1104
1185
  * Another asset of the same style flipped left to right, made locally
1105
1186
  * from that asset's downloaded files at no generation cost.
@@ -1174,6 +1255,16 @@ var AssetSchema = z.object({
1174
1255
  path: ["prompt"]
1175
1256
  });
1176
1257
  }
1258
+ if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
1259
+ context.addIssue({
1260
+ code: z.ZodIssueCode.custom,
1261
+ message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
1262
+ path: ["reference"]
1263
+ });
1264
+ }
1265
+ if (typeof asset.reference === "object" && !Object.keys(asset.reference).length) {
1266
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "a reference needs at least a south image", path: ["reference"] });
1267
+ }
1177
1268
  });
1178
1269
  var DEFAULT_HISTORY_LIMIT = 5;
1179
1270
  var HistoryLimitSchema = z.number().int().min(0).max(100);
@@ -1965,6 +2056,34 @@ function skipSubBlocks(bytes, start) {
1965
2056
  function cacheFileName(hash, mediaType = MediaType.PNG) {
1966
2057
  return `${hash}${mediaExtension(mediaType)}`;
1967
2058
  }
2059
+ function imageMetadata(buf) {
2060
+ if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
2061
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
2062
+ }
2063
+ if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
2064
+ let offset = 2;
2065
+ while (offset + 3 < buf.length) {
2066
+ if (buf[offset] !== 255) return null;
2067
+ while (buf[offset] === 255) offset++;
2068
+ const marker = buf[offset++];
2069
+ if (marker == null || marker === 217 || marker === 218) break;
2070
+ if (marker === 1 || marker >= 208 && marker <= 215) continue;
2071
+ if (offset + 2 > buf.length) return null;
2072
+ const length = buf.readUInt16BE(offset);
2073
+ if (length < 2 || offset + length > buf.length) return null;
2074
+ const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
2075
+ if (isStartOfFrame) {
2076
+ if (length < 7) return null;
2077
+ return {
2078
+ width: buf.readUInt16BE(offset + 5),
2079
+ height: buf.readUInt16BE(offset + 3),
2080
+ format: "jpeg"
2081
+ };
2082
+ }
2083
+ offset += length;
2084
+ }
2085
+ return null;
2086
+ }
1968
2087
 
1969
2088
  // src/outputs.ts
1970
2089
  import { existsSync as existsSync2 } from "fs";
@@ -5016,6 +5135,17 @@ var PixelLabError = class extends ProviderError {
5016
5135
  }
5017
5136
  body;
5018
5137
  };
5138
+ function proportionsPayload(proportions) {
5139
+ if (typeof proportions === "string") return { type: "preset", name: proportions };
5140
+ return {
5141
+ type: "custom",
5142
+ ...proportions.headSize !== void 0 ? { head_size: proportions.headSize } : {},
5143
+ ...proportions.armsLength !== void 0 ? { arms_length: proportions.armsLength } : {},
5144
+ ...proportions.legsLength !== void 0 ? { legs_length: proportions.legsLength } : {},
5145
+ ...proportions.shoulderWidth !== void 0 ? { shoulder_width: proportions.shoulderWidth } : {},
5146
+ ...proportions.hipWidth !== void 0 ? { hip_width: proportions.hipWidth } : {}
5147
+ };
5148
+ }
5019
5149
  var PixelLabClient = class {
5020
5150
  constructor(apiKey, timeoutMs = 12e4) {
5021
5151
  this.apiKey = apiKey;
@@ -5224,10 +5354,17 @@ var PixelLabClient = class {
5224
5354
  * A base character. Standard mode picks the 4- or 8-direction template
5225
5355
  * endpoint; v3 and pro have their own and always draw 8. Every one answers
5226
5356
  * at once with the character id and a background job.
5357
+ *
5358
+ * A `reference` is the author's own sprite: standard takes one per
5359
+ * direction and draws the rest, v3 rotates the south one, and pro
5360
+ * switches to its rotate method for it. A `styleReference` is pro's style
5361
+ * anchor; the other engines have no such input.
5227
5362
  */
5228
5363
  async createCharacter(args) {
5229
5364
  const imageSize = { width: args.size, height: args.size };
5230
5365
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
5366
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
5367
+ const south = args.reference?.south;
5231
5368
  let path45;
5232
5369
  let body;
5233
5370
  if (args.mode === "v3") {
@@ -5240,7 +5377,9 @@ var PixelLabClient = class {
5240
5377
  ...args.view ? { view: args.view } : {},
5241
5378
  ...args.outline ? { outline: args.outline } : {},
5242
5379
  ...args.detail ? { detail: args.detail } : {},
5243
- ...args.seed != null ? { seed: args.seed } : {}
5380
+ ...args.seed != null ? { seed: args.seed } : {},
5381
+ ...south ? { reference_image: encode(south) } : {},
5382
+ ...args.enhancePrompt ? { enhance_prompt: true } : {}
5244
5383
  };
5245
5384
  } else if (args.mode === "pro") {
5246
5385
  path45 = "/create-character-pro";
@@ -5249,8 +5388,10 @@ var PixelLabClient = class {
5249
5388
  image_size: imageSize,
5250
5389
  template_id: args.template,
5251
5390
  no_background: args.noBackground ?? true,
5391
+ method: south ? "rotate_character" : "create_with_style",
5252
5392
  ...args.view ? { view: args.view } : {},
5253
- ...args.seed != null ? { seed: args.seed } : {}
5393
+ ...args.seed != null ? { seed: args.seed } : {},
5394
+ ...south ? { reference_image: encode(south) } : args.styleReference ? { reference_image: encode(args.styleReference) } : {}
5254
5395
  };
5255
5396
  } else {
5256
5397
  path45 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
@@ -5263,6 +5404,10 @@ var PixelLabClient = class {
5263
5404
  ...args.shading ? { shading: args.shading } : {},
5264
5405
  ...args.detail ? { detail: args.detail } : {},
5265
5406
  ...args.seed != null ? { seed: args.seed } : {},
5407
+ ...args.proportions !== void 0 ? { proportions: proportionsPayload(args.proportions) } : {},
5408
+ ...args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
5409
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
5410
+ ...args.reference && Object.keys(args.reference).length ? { directions: Object.fromEntries(Object.entries(args.reference).flatMap(([direction, image]) => image ? [[direction, encode(image)]] : [])) } : {},
5266
5411
  ...palette
5267
5412
  };
5268
5413
  }
@@ -5291,6 +5436,7 @@ var PixelLabClient = class {
5291
5436
  * the action text; pro is the sequential engine. One job per direction.
5292
5437
  */
5293
5438
  async animateCharacter(args) {
5439
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
5294
5440
  const raw = await this.request("/animate-character", {
5295
5441
  method: "POST",
5296
5442
  body: JSON.stringify({
@@ -5300,8 +5446,18 @@ var PixelLabClient = class {
5300
5446
  directions: args.directions,
5301
5447
  ...args.template ? { template_animation_id: args.template } : {},
5302
5448
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
5449
+ ...args.description ? { description: args.description } : {},
5303
5450
  ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
5304
5451
  ...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
5452
+ ...args.mode === "v3" && args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
5453
+ ...args.mode === "v3" && args.endFrame ? { end_frame: encode(args.endFrame) } : {},
5454
+ ...args.mode === "v3" && args.enhancePrompt ? { enhance_prompt: true } : {},
5455
+ ...args.mode === "template" && args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
5456
+ ...args.mode === "template" && args.outline ? { outline: args.outline } : {},
5457
+ ...args.mode === "template" && args.shading ? { shading: args.shading } : {},
5458
+ ...args.mode === "template" && args.detail ? { detail: args.detail } : {},
5459
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
5460
+ ...args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {},
5305
5461
  ...args.seed != null ? { seed: args.seed } : {}
5306
5462
  })
5307
5463
  });
@@ -5498,8 +5654,8 @@ var PixelLabProvider = class _PixelLabProvider {
5498
5654
  "high detail"
5499
5655
  ]);
5500
5656
  }
5501
- if (spec.generator === "character") this.validateCharacter(spec);
5502
- if ((spec.generator === "map" || spec.generator === "pixflux" || spec.generator === "character") && styleImages.length) {
5657
+ if (spec.generator === "character") this.validateCharacter(spec, styleImages);
5658
+ if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
5503
5659
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
5504
5660
  }
5505
5661
  for (const image of styleImages) {
@@ -5515,9 +5671,80 @@ var PixelLabProvider = class _PixelLabProvider {
5515
5671
  );
5516
5672
  }
5517
5673
  }
5518
- validateCharacter(spec) {
5674
+ validateCharacter(spec, styleImages = []) {
5519
5675
  const character = spec.character;
5520
5676
  if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
5677
+ const label = `${spec.styleId}/${spec.assetId}`;
5678
+ if (character.kind === "base" && character.proportions !== void 0) {
5679
+ if (character.mode !== "standard") {
5680
+ throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
5681
+ }
5682
+ if (character.template !== "mannequin") {
5683
+ throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
5684
+ }
5685
+ }
5686
+ if (character.kind === "base" && character.enhancePrompt !== void 0 && character.mode !== "v3") {
5687
+ throw new Error(`${label}: enhancePrompt applies to v3 bases; the ${character.mode} engine does not take it`);
5688
+ }
5689
+ if (character.animation) {
5690
+ const { startFrame, endFrame } = character.animation;
5691
+ for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
5692
+ if (image && (image.width > 256 || image.height > 256)) {
5693
+ throw new Error(`${label}: ${name} is ${image.width}x${image.height}; PixelLab v3 takes up to 256px`);
5694
+ }
5695
+ }
5696
+ if (endFrame) {
5697
+ const start = startFrame ?? (character.parentFile && existsSync7(character.parentFile) ? imageMetadata(readFileSync2(character.parentFile)) : null);
5698
+ if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
5699
+ throw new Error(
5700
+ `${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "character's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
5701
+ );
5702
+ }
5703
+ }
5704
+ }
5705
+ if (character.reference) {
5706
+ const given = Object.keys(character.reference);
5707
+ if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
5708
+ if (character.mode !== "standard") {
5709
+ if (given.length > 1) throw new Error(`${label}: PixelLab ${character.mode} rotates one south-facing reference; drop the other directions`);
5710
+ const limit = character.mode === "v3" ? 256 : 168;
5711
+ const south = character.reference.south;
5712
+ if (south.width > limit || south.height > limit) {
5713
+ throw new Error(`${label}: reference is ${south.width}x${south.height}; PixelLab ${character.mode} takes up to ${limit}px`);
5714
+ }
5715
+ } else {
5716
+ const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
5717
+ for (const direction of given) {
5718
+ if (!allowed.includes(direction)) {
5719
+ throw new Error(`${label}: reference "${direction}" is not one of this character's ${character.directions} directions`);
5720
+ }
5721
+ const image = character.reference[direction];
5722
+ if (image.width !== spec.width || image.height !== spec.height) {
5723
+ throw new Error(
5724
+ `${label}: reference (${direction}) is ${image.width}x${image.height}; standard mode wants each image at the style's size, ${spec.width}x${spec.height}`
5725
+ );
5726
+ }
5727
+ }
5728
+ if (character.template !== "mannequin" && !character.reference.east) {
5729
+ throw new Error(`${label}: a ${character.template} reference needs south and east images`);
5730
+ }
5731
+ }
5732
+ }
5733
+ if (styleImages.length) {
5734
+ if (character.mode !== "pro") {
5735
+ throw new Error(
5736
+ `PixelLab ${character.mode} characters take no style images; give a base its own sprite with \`reference\`, or use mode pro for a style anchor`
5737
+ );
5738
+ }
5739
+ if (styleImages.length > 1) throw new Error("PixelLab pro characters take one style image");
5740
+ if (character.kind === "base" && character.reference) {
5741
+ throw new Error(`${label}: a pro base rotating its own reference takes no style image`);
5742
+ }
5743
+ const image = styleImages[0];
5744
+ if (image.width > 168 || image.height > 168) {
5745
+ throw new Error(`PixelLab pro character style image is ${image.width}x${image.height}; the limit is 168px`);
5746
+ }
5747
+ }
5521
5748
  const maxSize = character.mode === "v3" ? 256 : 128;
5522
5749
  if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
5523
5750
  throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
@@ -5553,10 +5780,11 @@ var PixelLabProvider = class _PixelLabProvider {
5553
5780
  * asset so a later run can find it, and clears its own earlier take for
5554
5781
  * that direction first, since PixelLab skips a direction that exists.
5555
5782
  */
5556
- async submitCharacter(spec, context) {
5783
+ async submitCharacter(spec, styleImages, context) {
5557
5784
  const character = spec.character;
5558
5785
  if (character.kind === "base") {
5559
5786
  const swatch = spec.palette.length && character.mode === "standard" ? paletteSwatch(spec.palette).toString("base64") : void 0;
5787
+ const styleImage = styleImages[0];
5560
5788
  const res2 = await this.client.createCharacter({
5561
5789
  mode: character.mode,
5562
5790
  description: spec.prompt,
@@ -5569,7 +5797,13 @@ var PixelLabProvider = class _PixelLabProvider {
5569
5797
  detail: spec.detail,
5570
5798
  seed: spec.seed,
5571
5799
  noBackground: spec.noBackground,
5572
- paletteSwatchBase64: swatch
5800
+ paletteSwatchBase64: swatch,
5801
+ proportions: character.proportions,
5802
+ textGuidanceScale: character.textGuidanceScale,
5803
+ isometric: character.isometric,
5804
+ reference: character.reference ? readReference(spec, character.reference) : void 0,
5805
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
5806
+ enhancePrompt: character.enhancePrompt
5573
5807
  });
5574
5808
  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
5809
  }
@@ -5611,7 +5845,17 @@ var PixelLabProvider = class _PixelLabProvider {
5611
5845
  frameCount: animation.frames,
5612
5846
  keepFirstFrame: animation.keepFirstFrame,
5613
5847
  directions: [animation.direction],
5614
- seed: spec.seed
5848
+ seed: spec.seed,
5849
+ textGuidanceScale: character.textGuidanceScale,
5850
+ isometric: character.isometric,
5851
+ startFrame: animation.startFrame ? readPose(spec, "start frame", animation.startFrame) : void 0,
5852
+ endFrame: animation.endFrame ? readPose(spec, "end frame", animation.endFrame) : void 0,
5853
+ description: animation.subject,
5854
+ outline: animation.outline,
5855
+ shading: animation.shading,
5856
+ detail: animation.detail,
5857
+ enhancePrompt: animation.enhancePrompt,
5858
+ paletteSwatchBase64: spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0
5615
5859
  });
5616
5860
  const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
5617
5861
  return {
@@ -5701,7 +5945,7 @@ var PixelLabProvider = class _PixelLabProvider {
5701
5945
  }
5702
5946
  async submit(spec, styleImages, context) {
5703
5947
  this.validate(spec, styleImages);
5704
- if (spec.generator === "character") return this.submitCharacter(spec, context);
5948
+ if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
5705
5949
  if (spec.generator === "pixflux") {
5706
5950
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
5707
5951
  const { png } = await this.client.createImagePixflux({
@@ -5968,6 +6212,24 @@ function firstUrl(urls) {
5968
6212
  if (!urls) return null;
5969
6213
  return Object.values(urls).find((u) => typeof u === "string") ?? null;
5970
6214
  }
6215
+ function readReference(spec, reference) {
6216
+ const images = {};
6217
+ for (const [direction, image] of Object.entries(reference)) {
6218
+ const bytes = readFileSync2(image.path);
6219
+ if (sha256(bytes) !== image.sha256) {
6220
+ throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
6221
+ }
6222
+ images[direction] = { base64: bytes.toString("base64"), format: image.format };
6223
+ }
6224
+ return images;
6225
+ }
6226
+ function readPose(spec, what, image) {
6227
+ const bytes = readFileSync2(image.path);
6228
+ if (sha256(bytes) !== image.sha256) {
6229
+ throw new Error(`${spec.styleId}/${spec.assetId}: ${what} changed after the manifest was resolved: ${image.path}`);
6230
+ }
6231
+ return { base64: bytes.toString("base64"), format: image.format };
6232
+ }
5971
6233
  function rotationSources(character) {
5972
6234
  const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
5973
6235
  const sources = [];
@@ -7087,16 +7349,16 @@ async function resolveSpecs(loaded, filter) {
7087
7349
  }
7088
7350
  }
7089
7351
  const styleImageCache = /* @__PURE__ */ new Map();
7090
- async function loadStyleImage(rel) {
7352
+ async function loadStyleImage(rel, what = "Style image") {
7091
7353
  const abs = path13.resolve(root, rel);
7092
7354
  let hit = styleImageCache.get(abs);
7093
7355
  if (!hit) {
7094
- if (!existsSync8(abs)) throw new Error(`Style image not found: ${abs}`);
7356
+ if (!existsSync8(abs)) throw new Error(`${what} not found: ${abs}`);
7095
7357
  const buf = await readFile6(abs);
7096
7358
  const metadata = imageMetadata(buf);
7097
- if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
7359
+ if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
7098
7360
  if (metadata.width < 1 || metadata.height < 1) {
7099
- throw new Error(`Style image has invalid dimensions: ${abs}`);
7361
+ throw new Error(`${what} has invalid dimensions: ${abs}`);
7100
7362
  }
7101
7363
  hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
7102
7364
  styleImageCache.set(abs, hit);
@@ -7203,7 +7465,7 @@ async function resolveSpecs(loaded, filter) {
7203
7465
  tileView: generator === "tiles" ? style.tileView : void 0,
7204
7466
  tileFeature: generator === "tiles" ? style.tileFeature : void 0,
7205
7467
  outlineMode: generator === "tiles" ? style.outlineMode : void 0,
7206
- ...generator === "character" ? { character: resolveCharacterShape(asset, style, characterKind) } : {},
7468
+ ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
7207
7469
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
7208
7470
  costUnit: "generations",
7209
7471
  candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
@@ -7417,43 +7679,34 @@ async function resolveStyleImages(loaded, styleId) {
7417
7679
  }
7418
7680
  return out;
7419
7681
  }
7420
- function imageMetadata(buf) {
7421
- if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
7422
- return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
7423
- }
7424
- if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
7425
- let offset = 2;
7426
- while (offset + 3 < buf.length) {
7427
- if (buf[offset] !== 255) return null;
7428
- while (buf[offset] === 255) offset++;
7429
- const marker = buf[offset++];
7430
- if (marker == null || marker === 217 || marker === 218) break;
7431
- if (marker === 1 || marker >= 208 && marker <= 215) continue;
7432
- if (offset + 2 > buf.length) return null;
7433
- const length = buf.readUInt16BE(offset);
7434
- if (length < 2 || offset + length > buf.length) return null;
7435
- const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
7436
- if (isStartOfFrame) {
7437
- if (length < 7) return null;
7438
- return {
7439
- width: buf.readUInt16BE(offset + 5),
7440
- height: buf.readUInt16BE(offset + 3),
7441
- format: "jpeg"
7442
- };
7443
- }
7444
- offset += length;
7445
- }
7446
- return null;
7447
- }
7448
- function resolveCharacterShape(asset, style, kind) {
7682
+ async function resolveCharacterShape(asset, style, kind, files) {
7449
7683
  const mode2 = style.mode ?? "standard";
7450
7684
  const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
7685
+ const proportions = asset.proportions ?? style.proportions;
7451
7686
  const shape = {
7452
7687
  kind,
7453
7688
  mode: mode2,
7454
7689
  directions,
7455
- template: style.template ?? "mannequin"
7690
+ template: style.template ?? "mannequin",
7691
+ ...proportions !== void 0 ? { proportions } : {},
7692
+ ...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
7693
+ ...style.isometric !== void 0 ? { isometric: style.isometric } : {},
7694
+ ...style.enhancePrompt !== void 0 ? { enhancePrompt: style.enhancePrompt } : {}
7456
7695
  };
7696
+ if (asset.reference) {
7697
+ const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
7698
+ shape.reference = {};
7699
+ for (const [direction, rel] of Object.entries(byDirection)) {
7700
+ const image = await files.load(rel, `Reference sprite (${direction})`);
7701
+ shape.reference[direction] = {
7702
+ path: path13.resolve(files.root, rel),
7703
+ sha256: image.hash,
7704
+ width: image.width,
7705
+ height: image.height,
7706
+ format: image.format
7707
+ };
7708
+ }
7709
+ }
7457
7710
  if (asset.state) {
7458
7711
  shape.state = {
7459
7712
  paletteFromReference: asset.state.paletteFromReference,
@@ -7462,13 +7715,24 @@ function resolveCharacterShape(asset, style, kind) {
7462
7715
  }
7463
7716
  if (asset.animation) {
7464
7717
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
7718
+ const pose = async (rel, what) => {
7719
+ const image = await files.load(rel, what);
7720
+ return { path: path13.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
7721
+ };
7465
7722
  shape.animation = {
7466
7723
  mode: animationMode,
7467
7724
  ...asset.animation.template ? { template: asset.animation.template } : {},
7468
7725
  direction: asset.animation.direction,
7469
7726
  frames: asset.animation.frames ?? 8,
7470
7727
  fps: asset.animation.fps,
7471
- keepFirstFrame: asset.animation.keepFirstFrame
7728
+ keepFirstFrame: asset.animation.keepFirstFrame,
7729
+ ...asset.animation.startFrame ? { startFrame: await pose(asset.animation.startFrame, "Animation start frame") } : {},
7730
+ ...asset.animation.endFrame ? { endFrame: await pose(asset.animation.endFrame, "Animation end frame") } : {},
7731
+ ...asset.animation.subject ? { subject: asset.animation.subject } : {},
7732
+ ...asset.animation.outline ? { outline: asset.animation.outline } : {},
7733
+ ...asset.animation.shading ? { shading: asset.animation.shading } : {},
7734
+ ...asset.animation.detail ? { detail: asset.animation.detail } : {},
7735
+ ...asset.animation.enhancePrompt !== void 0 ? { enhancePrompt: asset.animation.enhancePrompt } : {}
7472
7736
  };
7473
7737
  }
7474
7738
  return shape;