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.js CHANGED
@@ -128,11 +128,45 @@ var CharacterAnimationSchema = z.object({
128
128
  /** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
129
129
  keepFirstFrame: z.boolean().default(true),
130
130
  /** `template` when a template is named, otherwise `v3`; `pro` for the sequential high-quality engine. */
131
- mode: CharacterAnimationModeSchema.optional()
131
+ mode: CharacterAnimationModeSchema.optional(),
132
+ /**
133
+ * v3 only: a manifest-relative image of the pose to start from, instead
134
+ * of the character's rotation for this direction. Up to 256px.
135
+ */
136
+ startFrame: z.string().min(1).optional(),
137
+ /**
138
+ * v3 only: a pose to animate toward. The loop then interpolates from the
139
+ * start frame (the rotation, or `startFrame`) to this image, which must
140
+ * be the same size as the start frame.
141
+ */
142
+ endFrame: z.string().min(1).optional(),
143
+ /** What is being animated, when the character's own description would mislead the model. */
144
+ subject: z.string().min(1).optional(),
145
+ /** Template mode only: style hints for this loop, over the character's own. */
146
+ outline: z.string().min(1).optional(),
147
+ shading: z.string().min(1).optional(),
148
+ detail: z.string().min(1).optional(),
149
+ /** v3 text loops only: let PixelLab expand the action into a fuller motion description first. */
150
+ enhancePrompt: z.boolean().optional()
132
151
  }).strict().superRefine((animation, context) => {
133
152
  if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
134
153
  context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
135
154
  }
155
+ const mode2 = animation.mode ?? (animation.template ? "template" : "v3");
156
+ for (const key of ["startFrame", "endFrame", "enhancePrompt"]) {
157
+ if (animation[key] !== void 0 && mode2 !== "v3") {
158
+ context.addIssue({
159
+ code: z.ZodIssueCode.custom,
160
+ message: `${key} is for v3 loops; a ${mode2} loop ${mode2 === "template" ? "starts from the character's rotation and follows its template" : "takes neither"}`,
161
+ path: [key]
162
+ });
163
+ }
164
+ }
165
+ for (const key of ["outline", "shading", "detail"]) {
166
+ if (animation[key] !== void 0 && mode2 !== "template") {
167
+ context.addIssue({ code: z.ZodIssueCode.custom, message: `${key} overrides apply to template loops only`, path: [key] });
168
+ }
169
+ }
136
170
  if (animation.template && animation.mode && animation.mode !== "template") {
137
171
  context.addIssue({
138
172
  code: z.ZodIssueCode.custom,
@@ -270,6 +304,8 @@ var StyleObjectSchema = z.object({
270
304
  textGuidanceScale: z.number().min(1).max(20).optional(),
271
305
  /** `character` only. Draw `standard` bases and every loop in isometric view. */
272
306
  isometric: z.boolean().optional(),
307
+ /** `character` only, `v3` bases: let PixelLab expand the prompt into a fuller one before drawing. */
308
+ enhancePrompt: z.boolean().optional(),
273
309
  /** Fixed seed for reproducibility where the endpoint supports it. */
274
310
  seed: z.number().int().optional(),
275
311
  /**
@@ -1194,7 +1230,8 @@ var PixelLabClient = class {
1194
1230
  ...args.outline ? { outline: args.outline } : {},
1195
1231
  ...args.detail ? { detail: args.detail } : {},
1196
1232
  ...args.seed != null ? { seed: args.seed } : {},
1197
- ...south ? { reference_image: encode(south) } : {}
1233
+ ...south ? { reference_image: encode(south) } : {},
1234
+ ...args.enhancePrompt ? { enhance_prompt: true } : {}
1198
1235
  };
1199
1236
  } else if (args.mode === "pro") {
1200
1237
  path33 = "/create-character-pro";
@@ -1251,6 +1288,7 @@ var PixelLabClient = class {
1251
1288
  * the action text; pro is the sequential engine. One job per direction.
1252
1289
  */
1253
1290
  async animateCharacter(args) {
1291
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1254
1292
  const raw = await this.request("/animate-character", {
1255
1293
  method: "POST",
1256
1294
  body: JSON.stringify({
@@ -1260,10 +1298,18 @@ var PixelLabClient = class {
1260
1298
  directions: args.directions,
1261
1299
  ...args.template ? { template_animation_id: args.template } : {},
1262
1300
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
1301
+ ...args.description ? { description: args.description } : {},
1263
1302
  ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
1264
1303
  ...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
1304
+ ...args.mode === "v3" && args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
1305
+ ...args.mode === "v3" && args.endFrame ? { end_frame: encode(args.endFrame) } : {},
1306
+ ...args.mode === "v3" && args.enhancePrompt ? { enhance_prompt: true } : {},
1265
1307
  ...args.mode === "template" && args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
1308
+ ...args.mode === "template" && args.outline ? { outline: args.outline } : {},
1309
+ ...args.mode === "template" && args.shading ? { shading: args.shading } : {},
1310
+ ...args.mode === "template" && args.detail ? { detail: args.detail } : {},
1266
1311
  ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
1312
+ ...args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {},
1267
1313
  ...args.seed != null ? { seed: args.seed } : {}
1268
1314
  })
1269
1315
  });
@@ -1860,6 +1906,34 @@ function skipSubBlocks(bytes, start) {
1860
1906
  function cacheFileName(hash, mediaType = MediaType.PNG) {
1861
1907
  return `${hash}${mediaExtension(mediaType)}`;
1862
1908
  }
1909
+ function imageMetadata(buf) {
1910
+ if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
1911
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
1912
+ }
1913
+ if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
1914
+ let offset = 2;
1915
+ while (offset + 3 < buf.length) {
1916
+ if (buf[offset] !== 255) return null;
1917
+ while (buf[offset] === 255) offset++;
1918
+ const marker = buf[offset++];
1919
+ if (marker == null || marker === 217 || marker === 218) break;
1920
+ if (marker === 1 || marker >= 208 && marker <= 215) continue;
1921
+ if (offset + 2 > buf.length) return null;
1922
+ const length = buf.readUInt16BE(offset);
1923
+ if (length < 2 || offset + length > buf.length) return null;
1924
+ const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
1925
+ if (isStartOfFrame) {
1926
+ if (length < 7) return null;
1927
+ return {
1928
+ width: buf.readUInt16BE(offset + 5),
1929
+ height: buf.readUInt16BE(offset + 3),
1930
+ format: "jpeg"
1931
+ };
1932
+ }
1933
+ offset += length;
1934
+ }
1935
+ return null;
1936
+ }
1863
1937
 
1864
1938
  // src/providers/pixellab.ts
1865
1939
  import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
@@ -1920,11 +1994,17 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
1920
1994
  parent: spec.character.parentAssetId,
1921
1995
  parentSha256: spec.character.parentSha256 ?? null,
1922
1996
  state: spec.character.state,
1923
- animation: spec.character.animation,
1997
+ // Pose images enter as their hashes, never their paths.
1998
+ animation: spec.character.animation ? {
1999
+ ...spec.character.animation,
2000
+ startFrame: spec.character.animation.startFrame?.sha256,
2001
+ endFrame: spec.character.animation.endFrame?.sha256
2002
+ } : void 0,
1924
2003
  // Absent keys keep the hashes of manifests that never set these.
1925
2004
  proportions: spec.character.proportions,
1926
2005
  textGuidanceScale: spec.character.textGuidanceScale,
1927
2006
  isometric: spec.character.isometric,
2007
+ enhancePrompt: spec.character.enhancePrompt,
1928
2008
  reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
1929
2009
  } : void 0,
1930
2010
  // A mirror's bytes come from its source's recorded outputs; the plan
@@ -2093,6 +2173,25 @@ var PixelLabProvider = class _PixelLabProvider {
2093
2173
  throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
2094
2174
  }
2095
2175
  }
2176
+ if (character.kind === "base" && character.enhancePrompt !== void 0 && character.mode !== "v3") {
2177
+ throw new Error(`${label}: enhancePrompt applies to v3 bases; the ${character.mode} engine does not take it`);
2178
+ }
2179
+ if (character.animation) {
2180
+ const { startFrame, endFrame } = character.animation;
2181
+ for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
2182
+ if (image && (image.width > 256 || image.height > 256)) {
2183
+ throw new Error(`${label}: ${name} is ${image.width}x${image.height}; PixelLab v3 takes up to 256px`);
2184
+ }
2185
+ }
2186
+ if (endFrame) {
2187
+ const start = startFrame ?? (character.parentFile && existsSync(character.parentFile) ? imageMetadata(readFileSync(character.parentFile)) : null);
2188
+ if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
2189
+ throw new Error(
2190
+ `${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`
2191
+ );
2192
+ }
2193
+ }
2194
+ }
2096
2195
  if (character.reference) {
2097
2196
  const given = Object.keys(character.reference);
2098
2197
  if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
@@ -2193,7 +2292,8 @@ var PixelLabProvider = class _PixelLabProvider {
2193
2292
  textGuidanceScale: character.textGuidanceScale,
2194
2293
  isometric: character.isometric,
2195
2294
  reference: character.reference ? readReference(spec, character.reference) : void 0,
2196
- styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0
2295
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
2296
+ enhancePrompt: character.enhancePrompt
2197
2297
  });
2198
2298
  return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
2199
2299
  }
@@ -2237,7 +2337,15 @@ var PixelLabProvider = class _PixelLabProvider {
2237
2337
  directions: [animation.direction],
2238
2338
  seed: spec.seed,
2239
2339
  textGuidanceScale: character.textGuidanceScale,
2240
- isometric: character.isometric
2340
+ isometric: character.isometric,
2341
+ startFrame: animation.startFrame ? readPose(spec, "start frame", animation.startFrame) : void 0,
2342
+ endFrame: animation.endFrame ? readPose(spec, "end frame", animation.endFrame) : void 0,
2343
+ description: animation.subject,
2344
+ outline: animation.outline,
2345
+ shading: animation.shading,
2346
+ detail: animation.detail,
2347
+ enhancePrompt: animation.enhancePrompt,
2348
+ paletteSwatchBase64: spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0
2241
2349
  });
2242
2350
  const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
2243
2351
  return {
@@ -2605,6 +2713,13 @@ function readReference(spec, reference) {
2605
2713
  }
2606
2714
  return images;
2607
2715
  }
2716
+ function readPose(spec, what, image) {
2717
+ const bytes = readFileSync(image.path);
2718
+ if (sha256(bytes) !== image.sha256) {
2719
+ throw new Error(`${spec.styleId}/${spec.assetId}: ${what} changed after the manifest was resolved: ${image.path}`);
2720
+ }
2721
+ return { base64: bytes.toString("base64"), format: image.format };
2722
+ }
2608
2723
  function rotationSources(character) {
2609
2724
  const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2610
2725
  const sources = [];
@@ -5435,34 +5550,6 @@ async function resolveStyleImages(loaded, styleId) {
5435
5550
  async function styleImagesBase64(loaded, styleId) {
5436
5551
  return (await resolveStyleImages(loaded, styleId)).map((img) => img.base64);
5437
5552
  }
5438
- function imageMetadata(buf) {
5439
- if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
5440
- return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
5441
- }
5442
- if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
5443
- let offset = 2;
5444
- while (offset + 3 < buf.length) {
5445
- if (buf[offset] !== 255) return null;
5446
- while (buf[offset] === 255) offset++;
5447
- const marker = buf[offset++];
5448
- if (marker == null || marker === 217 || marker === 218) break;
5449
- if (marker === 1 || marker >= 208 && marker <= 215) continue;
5450
- if (offset + 2 > buf.length) return null;
5451
- const length = buf.readUInt16BE(offset);
5452
- if (length < 2 || offset + length > buf.length) return null;
5453
- const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
5454
- if (isStartOfFrame) {
5455
- if (length < 7) return null;
5456
- return {
5457
- width: buf.readUInt16BE(offset + 5),
5458
- height: buf.readUInt16BE(offset + 3),
5459
- format: "jpeg"
5460
- };
5461
- }
5462
- offset += length;
5463
- }
5464
- return null;
5465
- }
5466
5553
  async function resolveCharacterShape(asset, style, kind, files) {
5467
5554
  const mode2 = style.mode ?? "standard";
5468
5555
  const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
@@ -5474,7 +5561,8 @@ async function resolveCharacterShape(asset, style, kind, files) {
5474
5561
  template: style.template ?? "mannequin",
5475
5562
  ...proportions !== void 0 ? { proportions } : {},
5476
5563
  ...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
5477
- ...style.isometric !== void 0 ? { isometric: style.isometric } : {}
5564
+ ...style.isometric !== void 0 ? { isometric: style.isometric } : {},
5565
+ ...style.enhancePrompt !== void 0 ? { enhancePrompt: style.enhancePrompt } : {}
5478
5566
  };
5479
5567
  if (asset.reference) {
5480
5568
  const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
@@ -5498,13 +5586,24 @@ async function resolveCharacterShape(asset, style, kind, files) {
5498
5586
  }
5499
5587
  if (asset.animation) {
5500
5588
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
5589
+ const pose = async (rel, what) => {
5590
+ const image = await files.load(rel, what);
5591
+ return { path: path5.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
5592
+ };
5501
5593
  shape.animation = {
5502
5594
  mode: animationMode,
5503
5595
  ...asset.animation.template ? { template: asset.animation.template } : {},
5504
5596
  direction: asset.animation.direction,
5505
5597
  frames: asset.animation.frames ?? 8,
5506
5598
  fps: asset.animation.fps,
5507
- keepFirstFrame: asset.animation.keepFirstFrame
5599
+ keepFirstFrame: asset.animation.keepFirstFrame,
5600
+ ...asset.animation.startFrame ? { startFrame: await pose(asset.animation.startFrame, "Animation start frame") } : {},
5601
+ ...asset.animation.endFrame ? { endFrame: await pose(asset.animation.endFrame, "Animation end frame") } : {},
5602
+ ...asset.animation.subject ? { subject: asset.animation.subject } : {},
5603
+ ...asset.animation.outline ? { outline: asset.animation.outline } : {},
5604
+ ...asset.animation.shading ? { shading: asset.animation.shading } : {},
5605
+ ...asset.animation.detail ? { detail: asset.animation.detail } : {},
5606
+ ...asset.animation.enhancePrompt !== void 0 ? { enhancePrompt: asset.animation.enhancePrompt } : {}
5508
5607
  };
5509
5608
  }
5510
5609
  return shape;