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/index.cjs CHANGED
@@ -34,10 +34,12 @@ __export(index_exports, {
34
34
  BudgetError: () => BudgetError,
35
35
  CHARACTER_DIRECTIONS_4: () => CHARACTER_DIRECTIONS_4,
36
36
  CHARACTER_DIRECTIONS_8: () => CHARACTER_DIRECTIONS_8,
37
+ CHARACTER_PROPORTION_PRESETS: () => CHARACTER_PROPORTION_PRESETS,
37
38
  CharacterAnimationModeSchema: () => CharacterAnimationModeSchema,
38
39
  CharacterAnimationSchema: () => CharacterAnimationSchema,
39
40
  CharacterDirectionSchema: () => CharacterDirectionSchema,
40
41
  CharacterModeSchema: () => CharacterModeSchema,
42
+ CharacterProportionsSchema: () => CharacterProportionsSchema,
41
43
  CharacterStateSchema: () => CharacterStateSchema,
42
44
  ComfyUIProvider: () => ComfyUIProvider,
43
45
  DEFAULT_HISTORY_LIMIT: () => DEFAULT_HISTORY_LIMIT,
@@ -204,6 +206,7 @@ __export(index_exports, {
204
206
  portableOutputPath: () => portableOutputPath,
205
207
  prepareReview: () => prepareReview,
206
208
  primaryOutput: () => primaryOutput,
209
+ proportionsPayload: () => proportionsPayload,
207
210
  providerCredentialEnvs: () => providerCredentialEnvs,
208
211
  providerFactory: () => providerFactory,
209
212
  pushTags: () => pushTags,
@@ -387,6 +390,17 @@ var MIRRORED_DIRECTION = {
387
390
  "south-west": "south-east"
388
391
  };
389
392
  var CharacterModeSchema = import_zod.z.enum(["standard", "v3", "pro"]);
393
+ var CHARACTER_PROPORTION_PRESETS = ["default", "chibi", "cartoon", "stylized", "realistic_male", "realistic_female", "heroic"];
394
+ var CharacterProportionsSchema = import_zod.z.union([
395
+ import_zod.z.enum(CHARACTER_PROPORTION_PRESETS),
396
+ import_zod.z.object({
397
+ headSize: import_zod.z.number().min(0.5).max(2).optional(),
398
+ armsLength: import_zod.z.number().min(0.5).max(2).optional(),
399
+ legsLength: import_zod.z.number().min(0.5).max(2).optional(),
400
+ shoulderWidth: import_zod.z.number().min(0.5).max(2).optional(),
401
+ hipWidth: import_zod.z.number().min(0.5).max(2).optional()
402
+ }).strict()
403
+ ]);
390
404
  var CharacterAnimationModeSchema = import_zod.z.enum(["template", "v3", "pro"]);
391
405
  var CharacterStateSchema = import_zod.z.object({
392
406
  /** The base character, or another state, in the same style. */
@@ -413,11 +427,45 @@ var CharacterAnimationSchema = import_zod.z.object({
413
427
  /** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
414
428
  keepFirstFrame: import_zod.z.boolean().default(true),
415
429
  /** `template` when a template is named, otherwise `v3`; `pro` for the sequential high-quality engine. */
416
- 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()
417
450
  }).strict().superRefine((animation, context) => {
418
451
  if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
419
452
  context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
420
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
+ }
421
469
  if (animation.template && animation.mode && animation.mode !== "template") {
422
470
  context.addIssue({
423
471
  code: import_zod.z.ZodIssueCode.custom,
@@ -545,6 +593,18 @@ var StyleObjectSchema = import_zod.z.object({
545
593
  directions: import_zod.z.union([import_zod.z.literal(4), import_zod.z.literal(8)]).optional(),
546
594
  /** `character` only. Body template: `mannequin` (default) or a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`). */
547
595
  template: import_zod.z.string().min(1).optional(),
596
+ /** `character` only, `standard` humanoid bases. A preset or multipliers; an asset may override it. */
597
+ proportions: CharacterProportionsSchema.optional(),
598
+ /**
599
+ * `character` only. How closely a `standard` base and a template loop
600
+ * follow their text, 1 to 20; PixelLab's default is 8. The v3 and pro
601
+ * engines do not take it.
602
+ */
603
+ textGuidanceScale: import_zod.z.number().min(1).max(20).optional(),
604
+ /** `character` only. Draw `standard` bases and every loop in isometric view. */
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(),
548
608
  /** Fixed seed for reproducibility where the endpoint supports it. */
549
609
  seed: import_zod.z.number().int().optional(),
550
610
  /**
@@ -704,6 +764,19 @@ var AssetSchema = import_zod.z.object({
704
764
  state: CharacterStateSchema.optional(),
705
765
  /** `character` styles: this asset is a loop of another character asset in one direction. */
706
766
  animation: CharacterAnimationSchema.optional(),
767
+ /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
768
+ proportions: CharacterProportionsSchema.optional(),
769
+ /**
770
+ * `character` styles, bases only: the character's own sprite, which
771
+ * PixelLab rotates into the other directions instead of drawing from
772
+ * the prompt. A manifest-relative PNG or JPEG of the south-facing
773
+ * sprite, or an object keyed by direction (`{ "south": ..., "east":
774
+ * ... }`); quadrupeds in `standard` mode need south and east, and only
775
+ * `standard` takes more than south. `standard` wants each image at the
776
+ * style's size; v3 accepts up to 256px, pro up to 168px. The prompt
777
+ * still guides the result.
778
+ */
779
+ reference: import_zod.z.union([import_zod.z.string().min(1), import_zod.z.record(CharacterDirectionSchema, import_zod.z.string().min(1))]).optional(),
707
780
  /**
708
781
  * Another asset of the same style flipped left to right, made locally
709
782
  * from that asset's downloaded files at no generation cost.
@@ -778,6 +851,16 @@ var AssetSchema = import_zod.z.object({
778
851
  path: ["prompt"]
779
852
  });
780
853
  }
854
+ if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
855
+ context.addIssue({
856
+ code: import_zod.z.ZodIssueCode.custom,
857
+ message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
858
+ path: ["reference"]
859
+ });
860
+ }
861
+ if (typeof asset.reference === "object" && !Object.keys(asset.reference).length) {
862
+ context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "a reference needs at least a south image", path: ["reference"] });
863
+ }
781
864
  });
782
865
  var DEFAULT_HISTORY_LIMIT = 5;
783
866
  var HistoryLimitSchema = import_zod.z.number().int().min(0).max(100);
@@ -1203,6 +1286,17 @@ var PixelLabError = class extends ProviderError {
1203
1286
  }
1204
1287
  body;
1205
1288
  };
1289
+ function proportionsPayload(proportions) {
1290
+ if (typeof proportions === "string") return { type: "preset", name: proportions };
1291
+ return {
1292
+ type: "custom",
1293
+ ...proportions.headSize !== void 0 ? { head_size: proportions.headSize } : {},
1294
+ ...proportions.armsLength !== void 0 ? { arms_length: proportions.armsLength } : {},
1295
+ ...proportions.legsLength !== void 0 ? { legs_length: proportions.legsLength } : {},
1296
+ ...proportions.shoulderWidth !== void 0 ? { shoulder_width: proportions.shoulderWidth } : {},
1297
+ ...proportions.hipWidth !== void 0 ? { hip_width: proportions.hipWidth } : {}
1298
+ };
1299
+ }
1206
1300
  var PixelLabClient = class {
1207
1301
  constructor(apiKey, timeoutMs = 12e4) {
1208
1302
  this.apiKey = apiKey;
@@ -1411,10 +1505,17 @@ var PixelLabClient = class {
1411
1505
  * A base character. Standard mode picks the 4- or 8-direction template
1412
1506
  * endpoint; v3 and pro have their own and always draw 8. Every one answers
1413
1507
  * at once with the character id and a background job.
1508
+ *
1509
+ * A `reference` is the author's own sprite: standard takes one per
1510
+ * direction and draws the rest, v3 rotates the south one, and pro
1511
+ * switches to its rotate method for it. A `styleReference` is pro's style
1512
+ * anchor; the other engines have no such input.
1414
1513
  */
1415
1514
  async createCharacter(args) {
1416
1515
  const imageSize = { width: args.size, height: args.size };
1417
1516
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
1517
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1518
+ const south = args.reference?.south;
1418
1519
  let path33;
1419
1520
  let body;
1420
1521
  if (args.mode === "v3") {
@@ -1427,7 +1528,9 @@ var PixelLabClient = class {
1427
1528
  ...args.view ? { view: args.view } : {},
1428
1529
  ...args.outline ? { outline: args.outline } : {},
1429
1530
  ...args.detail ? { detail: args.detail } : {},
1430
- ...args.seed != null ? { seed: args.seed } : {}
1531
+ ...args.seed != null ? { seed: args.seed } : {},
1532
+ ...south ? { reference_image: encode(south) } : {},
1533
+ ...args.enhancePrompt ? { enhance_prompt: true } : {}
1431
1534
  };
1432
1535
  } else if (args.mode === "pro") {
1433
1536
  path33 = "/create-character-pro";
@@ -1436,8 +1539,10 @@ var PixelLabClient = class {
1436
1539
  image_size: imageSize,
1437
1540
  template_id: args.template,
1438
1541
  no_background: args.noBackground ?? true,
1542
+ method: south ? "rotate_character" : "create_with_style",
1439
1543
  ...args.view ? { view: args.view } : {},
1440
- ...args.seed != null ? { seed: args.seed } : {}
1544
+ ...args.seed != null ? { seed: args.seed } : {},
1545
+ ...south ? { reference_image: encode(south) } : args.styleReference ? { reference_image: encode(args.styleReference) } : {}
1441
1546
  };
1442
1547
  } else {
1443
1548
  path33 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
@@ -1450,6 +1555,10 @@ var PixelLabClient = class {
1450
1555
  ...args.shading ? { shading: args.shading } : {},
1451
1556
  ...args.detail ? { detail: args.detail } : {},
1452
1557
  ...args.seed != null ? { seed: args.seed } : {},
1558
+ ...args.proportions !== void 0 ? { proportions: proportionsPayload(args.proportions) } : {},
1559
+ ...args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
1560
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
1561
+ ...args.reference && Object.keys(args.reference).length ? { directions: Object.fromEntries(Object.entries(args.reference).flatMap(([direction, image]) => image ? [[direction, encode(image)]] : [])) } : {},
1453
1562
  ...palette
1454
1563
  };
1455
1564
  }
@@ -1478,6 +1587,7 @@ var PixelLabClient = class {
1478
1587
  * the action text; pro is the sequential engine. One job per direction.
1479
1588
  */
1480
1589
  async animateCharacter(args) {
1590
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1481
1591
  const raw = await this.request("/animate-character", {
1482
1592
  method: "POST",
1483
1593
  body: JSON.stringify({
@@ -1487,8 +1597,18 @@ var PixelLabClient = class {
1487
1597
  directions: args.directions,
1488
1598
  ...args.template ? { template_animation_id: args.template } : {},
1489
1599
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
1600
+ ...args.description ? { description: args.description } : {},
1490
1601
  ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
1491
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 } : {},
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 } : {},
1610
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
1611
+ ...args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {},
1492
1612
  ...args.seed != null ? { seed: args.seed } : {}
1493
1613
  })
1494
1614
  });
@@ -2085,12 +2205,122 @@ function skipSubBlocks(bytes, start) {
2085
2205
  function cacheFileName(hash, mediaType = MediaType.PNG) {
2086
2206
  return `${hash}${mediaExtension(mediaType)}`;
2087
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
+ }
2088
2236
 
2089
2237
  // src/providers/pixellab.ts
2090
- var import_node_fs = require("fs");
2091
- var import_node_crypto = require("crypto");
2238
+ var import_node_fs2 = require("fs");
2239
+ var import_node_crypto2 = require("crypto");
2092
2240
  var import_node_os = __toESM(require("os"), 1);
2093
2241
  var import_node_path = __toESM(require("path"), 1);
2242
+
2243
+ // src/hash.ts
2244
+ var import_node_crypto = require("crypto");
2245
+ var import_node_fs = require("fs");
2246
+ function sha256(data) {
2247
+ return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
2248
+ }
2249
+ async function sha256File(path33) {
2250
+ return new Promise((resolve, reject) => {
2251
+ const hash = (0, import_node_crypto.createHash)("sha256");
2252
+ const stream = (0, import_node_fs.createReadStream)(path33);
2253
+ stream.on("data", (chunk2) => hash.update(chunk2));
2254
+ stream.on("error", reject);
2255
+ stream.on("end", () => resolve(hash.digest("hex")));
2256
+ });
2257
+ }
2258
+ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
2259
+ return sha256(
2260
+ JSON.stringify({
2261
+ // Preserve every existing PixelLab hash while making a provider switch
2262
+ // invalidate the spec. Older manifests implicitly mean pixellab.
2263
+ provider: spec.provider === "pixellab" ? void 0 : spec.provider,
2264
+ providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
2265
+ providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
2266
+ generator: spec.generator,
2267
+ prompt: spec.prompt,
2268
+ width: spec.width,
2269
+ height: spec.height,
2270
+ view: spec.view,
2271
+ outline: spec.outline ?? null,
2272
+ shading: spec.shading ?? null,
2273
+ detail: spec.detail ?? null,
2274
+ seed: spec.seed ?? null,
2275
+ palette: spec.palette,
2276
+ // `noBackground` only reaches the wire for pixflux; the tile fields are
2277
+ // undefined for every other generator. `tileSize` is intentionally
2278
+ // absent; width/height are derived from it, so it is already covered.
2279
+ noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
2280
+ tileType: spec.tileType,
2281
+ tileView: spec.tileView,
2282
+ tileFeature: spec.tileFeature,
2283
+ outlineMode: spec.outlineMode,
2284
+ styleImages: styleImageHashes,
2285
+ // A character family: the engine and rotations of a base, the parent's
2286
+ // generated bytes for a state or animation (so a regenerated parent
2287
+ // makes its children stale), and each shape's own settings.
2288
+ character: spec.character ? {
2289
+ kind: spec.character.kind,
2290
+ mode: spec.character.mode,
2291
+ directions: spec.character.directions,
2292
+ template: spec.character.template,
2293
+ parent: spec.character.parentAssetId,
2294
+ parentSha256: spec.character.parentSha256 ?? null,
2295
+ state: spec.character.state,
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,
2302
+ // Absent keys keep the hashes of manifests that never set these.
2303
+ proportions: spec.character.proportions,
2304
+ textGuidanceScale: spec.character.textGuidanceScale,
2305
+ isometric: spec.character.isometric,
2306
+ enhancePrompt: spec.character.enhancePrompt,
2307
+ reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
2308
+ } : void 0,
2309
+ // A mirror's bytes come from its source's recorded outputs; the plan
2310
+ // compares those directly, so only the choice of source is identity.
2311
+ mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
2312
+ revision: spec.revision ? {
2313
+ mode: spec.revision.mode,
2314
+ from: spec.revision.sourceAssetId,
2315
+ sourceSha256: spec.revision.sourceSha256,
2316
+ maskSha256: spec.revision.maskSha256,
2317
+ strength: spec.revision.strength
2318
+ } : void 0
2319
+ })
2320
+ );
2321
+ }
2322
+
2323
+ // src/providers/pixellab.ts
2094
2324
  function pixelLabObjectUrl(generator, objectId) {
2095
2325
  if (!objectId) return null;
2096
2326
  if (generator === "map" || generator === "1dir") {
@@ -2171,7 +2401,7 @@ var PixelLabProvider = class _PixelLabProvider {
2171
2401
  */
2172
2402
  static cacheDir() {
2173
2403
  const dir = import_node_path.default.join(import_node_os.default.tmpdir(), "pixelkiln-pixflux");
2174
- (0, import_node_fs.mkdirSync)(dir, { recursive: true });
2404
+ (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
2175
2405
  return dir;
2176
2406
  }
2177
2407
  estimate(spec) {
@@ -2213,8 +2443,8 @@ var PixelLabProvider = class _PixelLabProvider {
2213
2443
  "high detail"
2214
2444
  ]);
2215
2445
  }
2216
- if (spec.generator === "character") this.validateCharacter(spec);
2217
- if ((spec.generator === "map" || spec.generator === "pixflux" || spec.generator === "character") && styleImages.length) {
2446
+ if (spec.generator === "character") this.validateCharacter(spec, styleImages);
2447
+ if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
2218
2448
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
2219
2449
  }
2220
2450
  for (const image of styleImages) {
@@ -2230,9 +2460,80 @@ var PixelLabProvider = class _PixelLabProvider {
2230
2460
  );
2231
2461
  }
2232
2462
  }
2233
- validateCharacter(spec) {
2463
+ validateCharacter(spec, styleImages = []) {
2234
2464
  const character = spec.character;
2235
2465
  if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
2466
+ const label = `${spec.styleId}/${spec.assetId}`;
2467
+ if (character.kind === "base" && character.proportions !== void 0) {
2468
+ if (character.mode !== "standard") {
2469
+ throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
2470
+ }
2471
+ if (character.template !== "mannequin") {
2472
+ throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
2473
+ }
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
+ }
2494
+ if (character.reference) {
2495
+ const given = Object.keys(character.reference);
2496
+ if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
2497
+ if (character.mode !== "standard") {
2498
+ if (given.length > 1) throw new Error(`${label}: PixelLab ${character.mode} rotates one south-facing reference; drop the other directions`);
2499
+ const limit = character.mode === "v3" ? 256 : 168;
2500
+ const south = character.reference.south;
2501
+ if (south.width > limit || south.height > limit) {
2502
+ throw new Error(`${label}: reference is ${south.width}x${south.height}; PixelLab ${character.mode} takes up to ${limit}px`);
2503
+ }
2504
+ } else {
2505
+ const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2506
+ for (const direction of given) {
2507
+ if (!allowed.includes(direction)) {
2508
+ throw new Error(`${label}: reference "${direction}" is not one of this character's ${character.directions} directions`);
2509
+ }
2510
+ const image = character.reference[direction];
2511
+ if (image.width !== spec.width || image.height !== spec.height) {
2512
+ throw new Error(
2513
+ `${label}: reference (${direction}) is ${image.width}x${image.height}; standard mode wants each image at the style's size, ${spec.width}x${spec.height}`
2514
+ );
2515
+ }
2516
+ }
2517
+ if (character.template !== "mannequin" && !character.reference.east) {
2518
+ throw new Error(`${label}: a ${character.template} reference needs south and east images`);
2519
+ }
2520
+ }
2521
+ }
2522
+ if (styleImages.length) {
2523
+ if (character.mode !== "pro") {
2524
+ throw new Error(
2525
+ `PixelLab ${character.mode} characters take no style images; give a base its own sprite with \`reference\`, or use mode pro for a style anchor`
2526
+ );
2527
+ }
2528
+ if (styleImages.length > 1) throw new Error("PixelLab pro characters take one style image");
2529
+ if (character.kind === "base" && character.reference) {
2530
+ throw new Error(`${label}: a pro base rotating its own reference takes no style image`);
2531
+ }
2532
+ const image = styleImages[0];
2533
+ if (image.width > 168 || image.height > 168) {
2534
+ throw new Error(`PixelLab pro character style image is ${image.width}x${image.height}; the limit is 168px`);
2535
+ }
2536
+ }
2236
2537
  const maxSize = character.mode === "v3" ? 256 : 128;
2237
2538
  if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
2238
2539
  throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
@@ -2268,10 +2569,11 @@ var PixelLabProvider = class _PixelLabProvider {
2268
2569
  * asset so a later run can find it, and clears its own earlier take for
2269
2570
  * that direction first, since PixelLab skips a direction that exists.
2270
2571
  */
2271
- async submitCharacter(spec, context) {
2572
+ async submitCharacter(spec, styleImages, context) {
2272
2573
  const character = spec.character;
2273
2574
  if (character.kind === "base") {
2274
2575
  const swatch = spec.palette.length && character.mode === "standard" ? paletteSwatch(spec.palette).toString("base64") : void 0;
2576
+ const styleImage = styleImages[0];
2275
2577
  const res2 = await this.client.createCharacter({
2276
2578
  mode: character.mode,
2277
2579
  description: spec.prompt,
@@ -2284,7 +2586,13 @@ var PixelLabProvider = class _PixelLabProvider {
2284
2586
  detail: spec.detail,
2285
2587
  seed: spec.seed,
2286
2588
  noBackground: spec.noBackground,
2287
- paletteSwatchBase64: swatch
2589
+ paletteSwatchBase64: swatch,
2590
+ proportions: character.proportions,
2591
+ textGuidanceScale: character.textGuidanceScale,
2592
+ isometric: character.isometric,
2593
+ reference: character.reference ? readReference(spec, character.reference) : void 0,
2594
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
2595
+ enhancePrompt: character.enhancePrompt
2288
2596
  });
2289
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 } } };
2290
2598
  }
@@ -2326,7 +2634,17 @@ var PixelLabProvider = class _PixelLabProvider {
2326
2634
  frameCount: animation.frames,
2327
2635
  keepFirstFrame: animation.keepFirstFrame,
2328
2636
  directions: [animation.direction],
2329
- seed: spec.seed
2637
+ seed: spec.seed,
2638
+ textGuidanceScale: character.textGuidanceScale,
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
2330
2648
  });
2331
2649
  const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
2332
2650
  return {
@@ -2416,7 +2734,7 @@ var PixelLabProvider = class _PixelLabProvider {
2416
2734
  }
2417
2735
  async submit(spec, styleImages, context) {
2418
2736
  this.validate(spec, styleImages);
2419
- if (spec.generator === "character") return this.submitCharacter(spec, context);
2737
+ if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
2420
2738
  if (spec.generator === "pixflux") {
2421
2739
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
2422
2740
  const { png } = await this.client.createImagePixflux({
@@ -2427,8 +2745,8 @@ var PixelLabProvider = class _PixelLabProvider {
2427
2745
  paletteSwatchBase64: swatch,
2428
2746
  seed: spec.seed
2429
2747
  });
2430
- const jobId = (0, import_node_crypto.randomUUID)();
2431
- (0, import_node_fs.writeFileSync)(import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
2748
+ const jobId = (0, import_node_crypto2.randomUUID)();
2749
+ (0, import_node_fs2.writeFileSync)(import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
2432
2750
  return { jobId };
2433
2751
  }
2434
2752
  if (spec.generator === "tiles") {
@@ -2470,7 +2788,7 @@ var PixelLabProvider = class _PixelLabProvider {
2470
2788
  async poll(jobId, generator, context) {
2471
2789
  if (generator === "pixflux") {
2472
2790
  const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
2473
- if ((0, import_node_fs.existsSync)(file)) {
2791
+ if ((0, import_node_fs2.existsSync)(file)) {
2474
2792
  const sourceUrl = `file://${file}`;
2475
2793
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
2476
2794
  }
@@ -2576,7 +2894,7 @@ var PixelLabProvider = class _PixelLabProvider {
2576
2894
  return { objectId, sourceUrl: firstUrl(obj?.rotation_urls) ?? obj?.preview_url ?? null };
2577
2895
  }
2578
2896
  async download(url) {
2579
- if (url.startsWith("file://")) return (0, import_node_fs.readFileSync)(url.slice("file://".length));
2897
+ if (url.startsWith("file://")) return (0, import_node_fs2.readFileSync)(url.slice("file://".length));
2580
2898
  return this.client.download(url);
2581
2899
  }
2582
2900
  async balance() {
@@ -2683,6 +3001,24 @@ function firstUrl(urls) {
2683
3001
  if (!urls) return null;
2684
3002
  return Object.values(urls).find((u) => typeof u === "string") ?? null;
2685
3003
  }
3004
+ function readReference(spec, reference) {
3005
+ const images = {};
3006
+ for (const [direction, image] of Object.entries(reference)) {
3007
+ const bytes = (0, import_node_fs2.readFileSync)(image.path);
3008
+ if (sha256(bytes) !== image.sha256) {
3009
+ throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
3010
+ }
3011
+ images[direction] = { base64: bytes.toString("base64"), format: image.format };
3012
+ }
3013
+ return images;
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
+ }
2686
3022
  function rotationSources(character) {
2687
3023
  const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2688
3024
  const sources = [];
@@ -3554,77 +3890,6 @@ function redactScenarioError(value, secrets) {
3554
3890
  var import_node_crypto3 = require("crypto");
3555
3891
  var import_promises = require("fs/promises");
3556
3892
  var import_node_path2 = __toESM(require("path"), 1);
3557
-
3558
- // src/hash.ts
3559
- var import_node_crypto2 = require("crypto");
3560
- var import_node_fs2 = require("fs");
3561
- function sha256(data) {
3562
- return (0, import_node_crypto2.createHash)("sha256").update(data).digest("hex");
3563
- }
3564
- async function sha256File(path33) {
3565
- return new Promise((resolve, reject) => {
3566
- const hash = (0, import_node_crypto2.createHash)("sha256");
3567
- const stream = (0, import_node_fs2.createReadStream)(path33);
3568
- stream.on("data", (chunk2) => hash.update(chunk2));
3569
- stream.on("error", reject);
3570
- stream.on("end", () => resolve(hash.digest("hex")));
3571
- });
3572
- }
3573
- function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
3574
- return sha256(
3575
- JSON.stringify({
3576
- // Preserve every existing PixelLab hash while making a provider switch
3577
- // invalidate the spec. Older manifests implicitly mean pixellab.
3578
- provider: spec.provider === "pixellab" ? void 0 : spec.provider,
3579
- providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
3580
- providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
3581
- generator: spec.generator,
3582
- prompt: spec.prompt,
3583
- width: spec.width,
3584
- height: spec.height,
3585
- view: spec.view,
3586
- outline: spec.outline ?? null,
3587
- shading: spec.shading ?? null,
3588
- detail: spec.detail ?? null,
3589
- seed: spec.seed ?? null,
3590
- palette: spec.palette,
3591
- // `noBackground` only reaches the wire for pixflux; the tile fields are
3592
- // undefined for every other generator. `tileSize` is intentionally
3593
- // absent; width/height are derived from it, so it is already covered.
3594
- noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
3595
- tileType: spec.tileType,
3596
- tileView: spec.tileView,
3597
- tileFeature: spec.tileFeature,
3598
- outlineMode: spec.outlineMode,
3599
- styleImages: styleImageHashes,
3600
- // A character family: the engine and rotations of a base, the parent's
3601
- // generated bytes for a state or animation (so a regenerated parent
3602
- // makes its children stale), and each shape's own settings.
3603
- character: spec.character ? {
3604
- kind: spec.character.kind,
3605
- mode: spec.character.mode,
3606
- directions: spec.character.directions,
3607
- template: spec.character.template,
3608
- parent: spec.character.parentAssetId,
3609
- parentSha256: spec.character.parentSha256 ?? null,
3610
- state: spec.character.state,
3611
- animation: spec.character.animation
3612
- } : void 0,
3613
- // A mirror's bytes come from its source's recorded outputs; the plan
3614
- // compares those directly, so only the choice of source is identity.
3615
- mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
3616
- revision: spec.revision ? {
3617
- mode: spec.revision.mode,
3618
- from: spec.revision.sourceAssetId,
3619
- sourceSha256: spec.revision.sourceSha256,
3620
- maskSha256: spec.revision.maskSha256,
3621
- strength: spec.revision.strength
3622
- } : void 0
3623
- })
3624
- );
3625
- }
3626
-
3627
- // src/providers/comfyui.ts
3628
3893
  var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
3629
3894
  var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
3630
3895
  "prompt",
@@ -5251,16 +5516,16 @@ async function resolveSpecs(loaded, filter) {
5251
5516
  }
5252
5517
  }
5253
5518
  const styleImageCache = /* @__PURE__ */ new Map();
5254
- async function loadStyleImage(rel) {
5519
+ async function loadStyleImage(rel, what = "Style image") {
5255
5520
  const abs = import_node_path5.default.resolve(root, rel);
5256
5521
  let hit = styleImageCache.get(abs);
5257
5522
  if (!hit) {
5258
- if (!(0, import_node_fs5.existsSync)(abs)) throw new Error(`Style image not found: ${abs}`);
5523
+ if (!(0, import_node_fs5.existsSync)(abs)) throw new Error(`${what} not found: ${abs}`);
5259
5524
  const buf = await (0, import_promises3.readFile)(abs);
5260
5525
  const metadata = imageMetadata(buf);
5261
- if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
5526
+ if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
5262
5527
  if (metadata.width < 1 || metadata.height < 1) {
5263
- throw new Error(`Style image has invalid dimensions: ${abs}`);
5528
+ throw new Error(`${what} has invalid dimensions: ${abs}`);
5264
5529
  }
5265
5530
  hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
5266
5531
  styleImageCache.set(abs, hit);
@@ -5367,7 +5632,7 @@ async function resolveSpecs(loaded, filter) {
5367
5632
  tileView: generator === "tiles" ? style.tileView : void 0,
5368
5633
  tileFeature: generator === "tiles" ? style.tileFeature : void 0,
5369
5634
  outlineMode: generator === "tiles" ? style.outlineMode : void 0,
5370
- ...generator === "character" ? { character: resolveCharacterShape(asset, style, characterKind) } : {},
5635
+ ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
5371
5636
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
5372
5637
  costUnit: "generations",
5373
5638
  candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
@@ -5584,43 +5849,34 @@ async function resolveStyleImages(loaded, styleId) {
5584
5849
  async function styleImagesBase64(loaded, styleId) {
5585
5850
  return (await resolveStyleImages(loaded, styleId)).map((img) => img.base64);
5586
5851
  }
5587
- function imageMetadata(buf) {
5588
- if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
5589
- return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
5590
- }
5591
- if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
5592
- let offset = 2;
5593
- while (offset + 3 < buf.length) {
5594
- if (buf[offset] !== 255) return null;
5595
- while (buf[offset] === 255) offset++;
5596
- const marker = buf[offset++];
5597
- if (marker == null || marker === 217 || marker === 218) break;
5598
- if (marker === 1 || marker >= 208 && marker <= 215) continue;
5599
- if (offset + 2 > buf.length) return null;
5600
- const length = buf.readUInt16BE(offset);
5601
- if (length < 2 || offset + length > buf.length) return null;
5602
- const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
5603
- if (isStartOfFrame) {
5604
- if (length < 7) return null;
5605
- return {
5606
- width: buf.readUInt16BE(offset + 5),
5607
- height: buf.readUInt16BE(offset + 3),
5608
- format: "jpeg"
5609
- };
5610
- }
5611
- offset += length;
5612
- }
5613
- return null;
5614
- }
5615
- function resolveCharacterShape(asset, style, kind) {
5852
+ async function resolveCharacterShape(asset, style, kind, files) {
5616
5853
  const mode2 = style.mode ?? "standard";
5617
5854
  const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
5855
+ const proportions = asset.proportions ?? style.proportions;
5618
5856
  const shape = {
5619
5857
  kind,
5620
5858
  mode: mode2,
5621
5859
  directions,
5622
- template: style.template ?? "mannequin"
5860
+ template: style.template ?? "mannequin",
5861
+ ...proportions !== void 0 ? { proportions } : {},
5862
+ ...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
5863
+ ...style.isometric !== void 0 ? { isometric: style.isometric } : {},
5864
+ ...style.enhancePrompt !== void 0 ? { enhancePrompt: style.enhancePrompt } : {}
5623
5865
  };
5866
+ if (asset.reference) {
5867
+ const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
5868
+ shape.reference = {};
5869
+ for (const [direction, rel] of Object.entries(byDirection)) {
5870
+ const image = await files.load(rel, `Reference sprite (${direction})`);
5871
+ shape.reference[direction] = {
5872
+ path: import_node_path5.default.resolve(files.root, rel),
5873
+ sha256: image.hash,
5874
+ width: image.width,
5875
+ height: image.height,
5876
+ format: image.format
5877
+ };
5878
+ }
5879
+ }
5624
5880
  if (asset.state) {
5625
5881
  shape.state = {
5626
5882
  paletteFromReference: asset.state.paletteFromReference,
@@ -5629,13 +5885,24 @@ function resolveCharacterShape(asset, style, kind) {
5629
5885
  }
5630
5886
  if (asset.animation) {
5631
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
+ };
5632
5892
  shape.animation = {
5633
5893
  mode: animationMode,
5634
5894
  ...asset.animation.template ? { template: asset.animation.template } : {},
5635
5895
  direction: asset.animation.direction,
5636
5896
  frames: asset.animation.frames ?? 8,
5637
5897
  fps: asset.animation.fps,
5638
- 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 } : {}
5639
5906
  };
5640
5907
  }
5641
5908
  return shape;
@@ -13898,10 +14165,12 @@ async function workspaceStatus(ws, dir) {
13898
14165
  BudgetError,
13899
14166
  CHARACTER_DIRECTIONS_4,
13900
14167
  CHARACTER_DIRECTIONS_8,
14168
+ CHARACTER_PROPORTION_PRESETS,
13901
14169
  CharacterAnimationModeSchema,
13902
14170
  CharacterAnimationSchema,
13903
14171
  CharacterDirectionSchema,
13904
14172
  CharacterModeSchema,
14173
+ CharacterProportionsSchema,
13905
14174
  CharacterStateSchema,
13906
14175
  ComfyUIProvider,
13907
14176
  DEFAULT_HISTORY_LIMIT,
@@ -14068,6 +14337,7 @@ async function workspaceStatus(ws, dir) {
14068
14337
  portableOutputPath,
14069
14338
  prepareReview,
14070
14339
  primaryOutput,
14340
+ proportionsPayload,
14071
14341
  providerCredentialEnvs,
14072
14342
  providerFactory,
14073
14343
  pushTags,