pixelkiln 0.43.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,11 +665,17 @@ 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,
669
674
  // Absent keys keep the hashes of manifests that never set these.
670
675
  proportions: spec.character.proportions,
671
676
  textGuidanceScale: spec.character.textGuidanceScale,
672
677
  isometric: spec.character.isometric,
678
+ enhancePrompt: spec.character.enhancePrompt,
673
679
  reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
674
680
  } : void 0,
675
681
  // A mirror's bytes come from its source's recorded outputs; the plan
@@ -825,11 +831,45 @@ var CharacterAnimationSchema = z.object({
825
831
  /** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
826
832
  keepFirstFrame: z.boolean().default(true),
827
833
  /** `template` when a template is named, otherwise `v3`; `pro` for the sequential high-quality engine. */
828
- 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()
829
854
  }).strict().superRefine((animation, context) => {
830
855
  if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
831
856
  context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
832
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
+ }
833
873
  if (animation.template && animation.mode && animation.mode !== "template") {
834
874
  context.addIssue({
835
875
  code: z.ZodIssueCode.custom,
@@ -967,6 +1007,8 @@ var StyleObjectSchema = z.object({
967
1007
  textGuidanceScale: z.number().min(1).max(20).optional(),
968
1008
  /** `character` only. Draw `standard` bases and every loop in isometric view. */
969
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(),
970
1012
  /** Fixed seed for reproducibility where the endpoint supports it. */
971
1013
  seed: z.number().int().optional(),
972
1014
  /**
@@ -2014,6 +2056,34 @@ function skipSubBlocks(bytes, start) {
2014
2056
  function cacheFileName(hash, mediaType = MediaType.PNG) {
2015
2057
  return `${hash}${mediaExtension(mediaType)}`;
2016
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
+ }
2017
2087
 
2018
2088
  // src/outputs.ts
2019
2089
  import { existsSync as existsSync2 } from "fs";
@@ -5308,7 +5378,8 @@ var PixelLabClient = class {
5308
5378
  ...args.outline ? { outline: args.outline } : {},
5309
5379
  ...args.detail ? { detail: args.detail } : {},
5310
5380
  ...args.seed != null ? { seed: args.seed } : {},
5311
- ...south ? { reference_image: encode(south) } : {}
5381
+ ...south ? { reference_image: encode(south) } : {},
5382
+ ...args.enhancePrompt ? { enhance_prompt: true } : {}
5312
5383
  };
5313
5384
  } else if (args.mode === "pro") {
5314
5385
  path45 = "/create-character-pro";
@@ -5365,6 +5436,7 @@ var PixelLabClient = class {
5365
5436
  * the action text; pro is the sequential engine. One job per direction.
5366
5437
  */
5367
5438
  async animateCharacter(args) {
5439
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
5368
5440
  const raw = await this.request("/animate-character", {
5369
5441
  method: "POST",
5370
5442
  body: JSON.stringify({
@@ -5374,10 +5446,18 @@ var PixelLabClient = class {
5374
5446
  directions: args.directions,
5375
5447
  ...args.template ? { template_animation_id: args.template } : {},
5376
5448
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
5449
+ ...args.description ? { description: args.description } : {},
5377
5450
  ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
5378
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 } : {},
5379
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 } : {},
5380
5459
  ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
5460
+ ...args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {},
5381
5461
  ...args.seed != null ? { seed: args.seed } : {}
5382
5462
  })
5383
5463
  });
@@ -5603,6 +5683,25 @@ var PixelLabProvider = class _PixelLabProvider {
5603
5683
  throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
5604
5684
  }
5605
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
+ }
5606
5705
  if (character.reference) {
5607
5706
  const given = Object.keys(character.reference);
5608
5707
  if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
@@ -5703,7 +5802,8 @@ var PixelLabProvider = class _PixelLabProvider {
5703
5802
  textGuidanceScale: character.textGuidanceScale,
5704
5803
  isometric: character.isometric,
5705
5804
  reference: character.reference ? readReference(spec, character.reference) : void 0,
5706
- styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0
5805
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
5806
+ enhancePrompt: character.enhancePrompt
5707
5807
  });
5708
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 } } };
5709
5809
  }
@@ -5747,7 +5847,15 @@ var PixelLabProvider = class _PixelLabProvider {
5747
5847
  directions: [animation.direction],
5748
5848
  seed: spec.seed,
5749
5849
  textGuidanceScale: character.textGuidanceScale,
5750
- isometric: character.isometric
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
5751
5859
  });
5752
5860
  const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
5753
5861
  return {
@@ -6115,6 +6223,13 @@ function readReference(spec, reference) {
6115
6223
  }
6116
6224
  return images;
6117
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
+ }
6118
6233
  function rotationSources(character) {
6119
6234
  const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
6120
6235
  const sources = [];
@@ -7564,34 +7679,6 @@ async function resolveStyleImages(loaded, styleId) {
7564
7679
  }
7565
7680
  return out;
7566
7681
  }
7567
- function imageMetadata(buf) {
7568
- if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
7569
- return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
7570
- }
7571
- if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
7572
- let offset = 2;
7573
- while (offset + 3 < buf.length) {
7574
- if (buf[offset] !== 255) return null;
7575
- while (buf[offset] === 255) offset++;
7576
- const marker = buf[offset++];
7577
- if (marker == null || marker === 217 || marker === 218) break;
7578
- if (marker === 1 || marker >= 208 && marker <= 215) continue;
7579
- if (offset + 2 > buf.length) return null;
7580
- const length = buf.readUInt16BE(offset);
7581
- if (length < 2 || offset + length > buf.length) return null;
7582
- const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
7583
- if (isStartOfFrame) {
7584
- if (length < 7) return null;
7585
- return {
7586
- width: buf.readUInt16BE(offset + 5),
7587
- height: buf.readUInt16BE(offset + 3),
7588
- format: "jpeg"
7589
- };
7590
- }
7591
- offset += length;
7592
- }
7593
- return null;
7594
- }
7595
7682
  async function resolveCharacterShape(asset, style, kind, files) {
7596
7683
  const mode2 = style.mode ?? "standard";
7597
7684
  const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
@@ -7603,7 +7690,8 @@ async function resolveCharacterShape(asset, style, kind, files) {
7603
7690
  template: style.template ?? "mannequin",
7604
7691
  ...proportions !== void 0 ? { proportions } : {},
7605
7692
  ...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
7606
- ...style.isometric !== void 0 ? { isometric: style.isometric } : {}
7693
+ ...style.isometric !== void 0 ? { isometric: style.isometric } : {},
7694
+ ...style.enhancePrompt !== void 0 ? { enhancePrompt: style.enhancePrompt } : {}
7607
7695
  };
7608
7696
  if (asset.reference) {
7609
7697
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -7627,13 +7715,24 @@ async function resolveCharacterShape(asset, style, kind, files) {
7627
7715
  }
7628
7716
  if (asset.animation) {
7629
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
+ };
7630
7722
  shape.animation = {
7631
7723
  mode: animationMode,
7632
7724
  ...asset.animation.template ? { template: asset.animation.template } : {},
7633
7725
  direction: asset.animation.direction,
7634
7726
  frames: asset.animation.frames ?? 8,
7635
7727
  fps: asset.animation.fps,
7636
- 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 } : {}
7637
7736
  };
7638
7737
  }
7639
7738
  return shape;