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/index.cjs CHANGED
@@ -427,11 +427,45 @@ var CharacterAnimationSchema = import_zod.z.object({
427
427
  /** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
428
428
  keepFirstFrame: import_zod.z.boolean().default(true),
429
429
  /** `template` when a template is named, otherwise `v3`; `pro` for the sequential high-quality engine. */
430
- mode: CharacterAnimationModeSchema.optional()
430
+ mode: CharacterAnimationModeSchema.optional(),
431
+ /**
432
+ * v3 only: a manifest-relative image of the pose to start from, instead
433
+ * of the character's rotation for this direction. Up to 256px.
434
+ */
435
+ startFrame: import_zod.z.string().min(1).optional(),
436
+ /**
437
+ * v3 only: a pose to animate toward. The loop then interpolates from the
438
+ * start frame (the rotation, or `startFrame`) to this image, which must
439
+ * be the same size as the start frame.
440
+ */
441
+ endFrame: import_zod.z.string().min(1).optional(),
442
+ /** What is being animated, when the character's own description would mislead the model. */
443
+ subject: import_zod.z.string().min(1).optional(),
444
+ /** Template mode only: style hints for this loop, over the character's own. */
445
+ outline: import_zod.z.string().min(1).optional(),
446
+ shading: import_zod.z.string().min(1).optional(),
447
+ detail: import_zod.z.string().min(1).optional(),
448
+ /** v3 text loops only: let PixelLab expand the action into a fuller motion description first. */
449
+ enhancePrompt: import_zod.z.boolean().optional()
431
450
  }).strict().superRefine((animation, context) => {
432
451
  if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
433
452
  context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
434
453
  }
454
+ const mode2 = animation.mode ?? (animation.template ? "template" : "v3");
455
+ for (const key of ["startFrame", "endFrame", "enhancePrompt"]) {
456
+ if (animation[key] !== void 0 && mode2 !== "v3") {
457
+ context.addIssue({
458
+ code: import_zod.z.ZodIssueCode.custom,
459
+ message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" ? "starts from the character's rotation and follows its template" : "takes neither"}`,
460
+ path: [key]
461
+ });
462
+ }
463
+ }
464
+ for (const key of ["outline", "shading", "detail"]) {
465
+ if (animation[key] !== void 0 && mode2 !== "template") {
466
+ context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: `${key} overrides apply to template loops only`, path: [key] });
467
+ }
468
+ }
435
469
  if (animation.template && animation.mode && animation.mode !== "template") {
436
470
  context.addIssue({
437
471
  code: import_zod.z.ZodIssueCode.custom,
@@ -569,6 +603,8 @@ var StyleObjectSchema = import_zod.z.object({
569
603
  textGuidanceScale: import_zod.z.number().min(1).max(20).optional(),
570
604
  /** `character` only. Draw `standard` bases and every loop in isometric view. */
571
605
  isometric: import_zod.z.boolean().optional(),
606
+ /** `character` only, `v3` bases: let PixelLab expand the prompt into a fuller one before drawing. */
607
+ enhancePrompt: import_zod.z.boolean().optional(),
572
608
  /** Fixed seed for reproducibility where the endpoint supports it. */
573
609
  seed: import_zod.z.number().int().optional(),
574
610
  /**
@@ -1493,7 +1529,8 @@ var PixelLabClient = class {
1493
1529
  ...args.outline ? { outline: args.outline } : {},
1494
1530
  ...args.detail ? { detail: args.detail } : {},
1495
1531
  ...args.seed != null ? { seed: args.seed } : {},
1496
- ...south ? { reference_image: encode(south) } : {}
1532
+ ...south ? { reference_image: encode(south) } : {},
1533
+ ...args.enhancePrompt ? { enhance_prompt: true } : {}
1497
1534
  };
1498
1535
  } else if (args.mode === "pro") {
1499
1536
  path33 = "/create-character-pro";
@@ -1550,6 +1587,7 @@ var PixelLabClient = class {
1550
1587
  * the action text; pro is the sequential engine. One job per direction.
1551
1588
  */
1552
1589
  async animateCharacter(args) {
1590
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1553
1591
  const raw = await this.request("/animate-character", {
1554
1592
  method: "POST",
1555
1593
  body: JSON.stringify({
@@ -1559,10 +1597,18 @@ var PixelLabClient = class {
1559
1597
  directions: args.directions,
1560
1598
  ...args.template ? { template_animation_id: args.template } : {},
1561
1599
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
1600
+ ...args.description ? { description: args.description } : {},
1562
1601
  ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
1563
1602
  ...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
1603
+ ...args.mode === "v3" && args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
1604
+ ...args.mode === "v3" && args.endFrame ? { end_frame: encode(args.endFrame) } : {},
1605
+ ...args.mode === "v3" && args.enhancePrompt ? { enhance_prompt: true } : {},
1564
1606
  ...args.mode === "template" && args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
1607
+ ...args.mode === "template" && args.outline ? { outline: args.outline } : {},
1608
+ ...args.mode === "template" && args.shading ? { shading: args.shading } : {},
1609
+ ...args.mode === "template" && args.detail ? { detail: args.detail } : {},
1565
1610
  ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
1611
+ ...args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {},
1566
1612
  ...args.seed != null ? { seed: args.seed } : {}
1567
1613
  })
1568
1614
  });
@@ -2159,6 +2205,34 @@ function skipSubBlocks(bytes, start) {
2159
2205
  function cacheFileName(hash, mediaType = MediaType.PNG) {
2160
2206
  return `${hash}${mediaExtension(mediaType)}`;
2161
2207
  }
2208
+ function imageMetadata(buf) {
2209
+ if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
2210
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
2211
+ }
2212
+ if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
2213
+ let offset = 2;
2214
+ while (offset + 3 < buf.length) {
2215
+ if (buf[offset] !== 255) return null;
2216
+ while (buf[offset] === 255) offset++;
2217
+ const marker = buf[offset++];
2218
+ if (marker == null || marker === 217 || marker === 218) break;
2219
+ if (marker === 1 || marker >= 208 && marker <= 215) continue;
2220
+ if (offset + 2 > buf.length) return null;
2221
+ const length = buf.readUInt16BE(offset);
2222
+ if (length < 2 || offset + length > buf.length) return null;
2223
+ const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
2224
+ if (isStartOfFrame) {
2225
+ if (length < 7) return null;
2226
+ return {
2227
+ width: buf.readUInt16BE(offset + 5),
2228
+ height: buf.readUInt16BE(offset + 3),
2229
+ format: "jpeg"
2230
+ };
2231
+ }
2232
+ offset += length;
2233
+ }
2234
+ return null;
2235
+ }
2162
2236
 
2163
2237
  // src/providers/pixellab.ts
2164
2238
  var import_node_fs2 = require("fs");
@@ -2219,11 +2293,17 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
2219
2293
  parent: spec.character.parentAssetId,
2220
2294
  parentSha256: spec.character.parentSha256 ?? null,
2221
2295
  state: spec.character.state,
2222
- animation: spec.character.animation,
2296
+ // Pose images enter as their hashes, never their paths.
2297
+ animation: spec.character.animation ? {
2298
+ ...spec.character.animation,
2299
+ startFrame: spec.character.animation.startFrame?.sha256,
2300
+ endFrame: spec.character.animation.endFrame?.sha256
2301
+ } : void 0,
2223
2302
  // Absent keys keep the hashes of manifests that never set these.
2224
2303
  proportions: spec.character.proportions,
2225
2304
  textGuidanceScale: spec.character.textGuidanceScale,
2226
2305
  isometric: spec.character.isometric,
2306
+ enhancePrompt: spec.character.enhancePrompt,
2227
2307
  reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
2228
2308
  } : void 0,
2229
2309
  // A mirror's bytes come from its source's recorded outputs; the plan
@@ -2392,6 +2472,25 @@ var PixelLabProvider = class _PixelLabProvider {
2392
2472
  throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
2393
2473
  }
2394
2474
  }
2475
+ if (character.kind === "base" && character.enhancePrompt !== void 0 && character.mode !== "v3") {
2476
+ throw new Error(`${label}: enhancePrompt applies to v3 bases; the ${character.mode} engine does not take it`);
2477
+ }
2478
+ if (character.animation) {
2479
+ const { startFrame, endFrame } = character.animation;
2480
+ for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
2481
+ if (image && (image.width > 256 || image.height > 256)) {
2482
+ throw new Error(`${label}: ${name} is ${image.width}x${image.height}; PixelLab v3 takes up to 256px`);
2483
+ }
2484
+ }
2485
+ if (endFrame) {
2486
+ const start = startFrame ?? (character.parentFile && (0, import_node_fs2.existsSync)(character.parentFile) ? imageMetadata((0, import_node_fs2.readFileSync)(character.parentFile)) : null);
2487
+ if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
2488
+ throw new Error(
2489
+ `${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`
2490
+ );
2491
+ }
2492
+ }
2493
+ }
2395
2494
  if (character.reference) {
2396
2495
  const given = Object.keys(character.reference);
2397
2496
  if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
@@ -2492,7 +2591,8 @@ var PixelLabProvider = class _PixelLabProvider {
2492
2591
  textGuidanceScale: character.textGuidanceScale,
2493
2592
  isometric: character.isometric,
2494
2593
  reference: character.reference ? readReference(spec, character.reference) : void 0,
2495
- styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0
2594
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
2595
+ enhancePrompt: character.enhancePrompt
2496
2596
  });
2497
2597
  return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
2498
2598
  }
@@ -2536,7 +2636,15 @@ var PixelLabProvider = class _PixelLabProvider {
2536
2636
  directions: [animation.direction],
2537
2637
  seed: spec.seed,
2538
2638
  textGuidanceScale: character.textGuidanceScale,
2539
- isometric: character.isometric
2639
+ isometric: character.isometric,
2640
+ startFrame: animation.startFrame ? readPose(spec, "start frame", animation.startFrame) : void 0,
2641
+ endFrame: animation.endFrame ? readPose(spec, "end frame", animation.endFrame) : void 0,
2642
+ description: animation.subject,
2643
+ outline: animation.outline,
2644
+ shading: animation.shading,
2645
+ detail: animation.detail,
2646
+ enhancePrompt: animation.enhancePrompt,
2647
+ paletteSwatchBase64: spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0
2540
2648
  });
2541
2649
  const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
2542
2650
  return {
@@ -2904,6 +3012,13 @@ function readReference(spec, reference) {
2904
3012
  }
2905
3013
  return images;
2906
3014
  }
3015
+ function readPose(spec, what, image) {
3016
+ const bytes = (0, import_node_fs2.readFileSync)(image.path);
3017
+ if (sha256(bytes) !== image.sha256) {
3018
+ throw new Error(`${spec.styleId}/${spec.assetId}: ${what} changed after the manifest was resolved: ${image.path}`);
3019
+ }
3020
+ return { base64: bytes.toString("base64"), format: image.format };
3021
+ }
2907
3022
  function rotationSources(character) {
2908
3023
  const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2909
3024
  const sources = [];
@@ -5734,34 +5849,6 @@ async function resolveStyleImages(loaded, styleId) {
5734
5849
  async function styleImagesBase64(loaded, styleId) {
5735
5850
  return (await resolveStyleImages(loaded, styleId)).map((img) => img.base64);
5736
5851
  }
5737
- function imageMetadata(buf) {
5738
- if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
5739
- return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
5740
- }
5741
- if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
5742
- let offset = 2;
5743
- while (offset + 3 < buf.length) {
5744
- if (buf[offset] !== 255) return null;
5745
- while (buf[offset] === 255) offset++;
5746
- const marker = buf[offset++];
5747
- if (marker == null || marker === 217 || marker === 218) break;
5748
- if (marker === 1 || marker >= 208 && marker <= 215) continue;
5749
- if (offset + 2 > buf.length) return null;
5750
- const length = buf.readUInt16BE(offset);
5751
- if (length < 2 || offset + length > buf.length) return null;
5752
- const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
5753
- if (isStartOfFrame) {
5754
- if (length < 7) return null;
5755
- return {
5756
- width: buf.readUInt16BE(offset + 5),
5757
- height: buf.readUInt16BE(offset + 3),
5758
- format: "jpeg"
5759
- };
5760
- }
5761
- offset += length;
5762
- }
5763
- return null;
5764
- }
5765
5852
  async function resolveCharacterShape(asset, style, kind, files) {
5766
5853
  const mode2 = style.mode ?? "standard";
5767
5854
  const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
@@ -5773,7 +5860,8 @@ async function resolveCharacterShape(asset, style, kind, files) {
5773
5860
  template: style.template ?? "mannequin",
5774
5861
  ...proportions !== void 0 ? { proportions } : {},
5775
5862
  ...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
5776
- ...style.isometric !== void 0 ? { isometric: style.isometric } : {}
5863
+ ...style.isometric !== void 0 ? { isometric: style.isometric } : {},
5864
+ ...style.enhancePrompt !== void 0 ? { enhancePrompt: style.enhancePrompt } : {}
5777
5865
  };
5778
5866
  if (asset.reference) {
5779
5867
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -5797,13 +5885,24 @@ async function resolveCharacterShape(asset, style, kind, files) {
5797
5885
  }
5798
5886
  if (asset.animation) {
5799
5887
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
5888
+ const pose = async (rel, what) => {
5889
+ const image = await files.load(rel, what);
5890
+ return { path: import_node_path5.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
5891
+ };
5800
5892
  shape.animation = {
5801
5893
  mode: animationMode,
5802
5894
  ...asset.animation.template ? { template: asset.animation.template } : {},
5803
5895
  direction: asset.animation.direction,
5804
5896
  frames: asset.animation.frames ?? 8,
5805
5897
  fps: asset.animation.fps,
5806
- keepFirstFrame: asset.animation.keepFirstFrame
5898
+ keepFirstFrame: asset.animation.keepFirstFrame,
5899
+ ...asset.animation.startFrame ? { startFrame: await pose(asset.animation.startFrame, "Animation start frame") } : {},
5900
+ ...asset.animation.endFrame ? { endFrame: await pose(asset.animation.endFrame, "Animation end frame") } : {},
5901
+ ...asset.animation.subject ? { subject: asset.animation.subject } : {},
5902
+ ...asset.animation.outline ? { outline: asset.animation.outline } : {},
5903
+ ...asset.animation.shading ? { shading: asset.animation.shading } : {},
5904
+ ...asset.animation.detail ? { detail: asset.animation.detail } : {},
5905
+ ...asset.animation.enhancePrompt !== void 0 ? { enhancePrompt: asset.animation.enhancePrompt } : {}
5807
5906
  };
5808
5907
  }
5809
5908
  return shape;