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.js CHANGED
@@ -91,6 +91,17 @@ var MIRRORED_DIRECTION = {
91
91
  "south-west": "south-east"
92
92
  };
93
93
  var CharacterModeSchema = z.enum(["standard", "v3", "pro"]);
94
+ var CHARACTER_PROPORTION_PRESETS = ["default", "chibi", "cartoon", "stylized", "realistic_male", "realistic_female", "heroic"];
95
+ var CharacterProportionsSchema = z.union([
96
+ z.enum(CHARACTER_PROPORTION_PRESETS),
97
+ z.object({
98
+ headSize: z.number().min(0.5).max(2).optional(),
99
+ armsLength: z.number().min(0.5).max(2).optional(),
100
+ legsLength: z.number().min(0.5).max(2).optional(),
101
+ shoulderWidth: z.number().min(0.5).max(2).optional(),
102
+ hipWidth: z.number().min(0.5).max(2).optional()
103
+ }).strict()
104
+ ]);
94
105
  var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
95
106
  var CharacterStateSchema = z.object({
96
107
  /** The base character, or another state, in the same style. */
@@ -117,11 +128,45 @@ var CharacterAnimationSchema = z.object({
117
128
  /** v3 only: keep the resting pose as frame 0, so `frames` generated frames land as `frames + 1` files. */
118
129
  keepFirstFrame: z.boolean().default(true),
119
130
  /** `template` when a template is named, otherwise `v3`; `pro` for the sequential high-quality engine. */
120
- 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()
121
151
  }).strict().superRefine((animation, context) => {
122
152
  if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
123
153
  context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
124
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
+ }
125
170
  if (animation.template && animation.mode && animation.mode !== "template") {
126
171
  context.addIssue({
127
172
  code: z.ZodIssueCode.custom,
@@ -249,6 +294,18 @@ var StyleObjectSchema = z.object({
249
294
  directions: z.union([z.literal(4), z.literal(8)]).optional(),
250
295
  /** `character` only. Body template: `mannequin` (default) or a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`). */
251
296
  template: z.string().min(1).optional(),
297
+ /** `character` only, `standard` humanoid bases. A preset or multipliers; an asset may override it. */
298
+ proportions: CharacterProportionsSchema.optional(),
299
+ /**
300
+ * `character` only. How closely a `standard` base and a template loop
301
+ * follow their text, 1 to 20; PixelLab's default is 8. The v3 and pro
302
+ * engines do not take it.
303
+ */
304
+ textGuidanceScale: z.number().min(1).max(20).optional(),
305
+ /** `character` only. Draw `standard` bases and every loop in isometric view. */
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(),
252
309
  /** Fixed seed for reproducibility where the endpoint supports it. */
253
310
  seed: z.number().int().optional(),
254
311
  /**
@@ -408,6 +465,19 @@ var AssetSchema = z.object({
408
465
  state: CharacterStateSchema.optional(),
409
466
  /** `character` styles: this asset is a loop of another character asset in one direction. */
410
467
  animation: CharacterAnimationSchema.optional(),
468
+ /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
469
+ proportions: CharacterProportionsSchema.optional(),
470
+ /**
471
+ * `character` styles, bases only: the character's own sprite, which
472
+ * PixelLab rotates into the other directions instead of drawing from
473
+ * the prompt. A manifest-relative PNG or JPEG of the south-facing
474
+ * sprite, or an object keyed by direction (`{ "south": ..., "east":
475
+ * ... }`); quadrupeds in `standard` mode need south and east, and only
476
+ * `standard` takes more than south. `standard` wants each image at the
477
+ * style's size; v3 accepts up to 256px, pro up to 168px. The prompt
478
+ * still guides the result.
479
+ */
480
+ reference: z.union([z.string().min(1), z.record(CharacterDirectionSchema, z.string().min(1))]).optional(),
411
481
  /**
412
482
  * Another asset of the same style flipped left to right, made locally
413
483
  * from that asset's downloaded files at no generation cost.
@@ -482,6 +552,16 @@ var AssetSchema = z.object({
482
552
  path: ["prompt"]
483
553
  });
484
554
  }
555
+ if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
556
+ context.addIssue({
557
+ code: z.ZodIssueCode.custom,
558
+ message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
559
+ path: ["reference"]
560
+ });
561
+ }
562
+ if (typeof asset.reference === "object" && !Object.keys(asset.reference).length) {
563
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "a reference needs at least a south image", path: ["reference"] });
564
+ }
485
565
  });
486
566
  var DEFAULT_HISTORY_LIMIT = 5;
487
567
  var HistoryLimitSchema = z.number().int().min(0).max(100);
@@ -907,6 +987,17 @@ var PixelLabError = class extends ProviderError {
907
987
  }
908
988
  body;
909
989
  };
990
+ function proportionsPayload(proportions) {
991
+ if (typeof proportions === "string") return { type: "preset", name: proportions };
992
+ return {
993
+ type: "custom",
994
+ ...proportions.headSize !== void 0 ? { head_size: proportions.headSize } : {},
995
+ ...proportions.armsLength !== void 0 ? { arms_length: proportions.armsLength } : {},
996
+ ...proportions.legsLength !== void 0 ? { legs_length: proportions.legsLength } : {},
997
+ ...proportions.shoulderWidth !== void 0 ? { shoulder_width: proportions.shoulderWidth } : {},
998
+ ...proportions.hipWidth !== void 0 ? { hip_width: proportions.hipWidth } : {}
999
+ };
1000
+ }
910
1001
  var PixelLabClient = class {
911
1002
  constructor(apiKey, timeoutMs = 12e4) {
912
1003
  this.apiKey = apiKey;
@@ -1115,10 +1206,17 @@ var PixelLabClient = class {
1115
1206
  * A base character. Standard mode picks the 4- or 8-direction template
1116
1207
  * endpoint; v3 and pro have their own and always draw 8. Every one answers
1117
1208
  * at once with the character id and a background job.
1209
+ *
1210
+ * A `reference` is the author's own sprite: standard takes one per
1211
+ * direction and draws the rest, v3 rotates the south one, and pro
1212
+ * switches to its rotate method for it. A `styleReference` is pro's style
1213
+ * anchor; the other engines have no such input.
1118
1214
  */
1119
1215
  async createCharacter(args) {
1120
1216
  const imageSize = { width: args.size, height: args.size };
1121
1217
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
1218
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1219
+ const south = args.reference?.south;
1122
1220
  let path33;
1123
1221
  let body;
1124
1222
  if (args.mode === "v3") {
@@ -1131,7 +1229,9 @@ var PixelLabClient = class {
1131
1229
  ...args.view ? { view: args.view } : {},
1132
1230
  ...args.outline ? { outline: args.outline } : {},
1133
1231
  ...args.detail ? { detail: args.detail } : {},
1134
- ...args.seed != null ? { seed: args.seed } : {}
1232
+ ...args.seed != null ? { seed: args.seed } : {},
1233
+ ...south ? { reference_image: encode(south) } : {},
1234
+ ...args.enhancePrompt ? { enhance_prompt: true } : {}
1135
1235
  };
1136
1236
  } else if (args.mode === "pro") {
1137
1237
  path33 = "/create-character-pro";
@@ -1140,8 +1240,10 @@ var PixelLabClient = class {
1140
1240
  image_size: imageSize,
1141
1241
  template_id: args.template,
1142
1242
  no_background: args.noBackground ?? true,
1243
+ method: south ? "rotate_character" : "create_with_style",
1143
1244
  ...args.view ? { view: args.view } : {},
1144
- ...args.seed != null ? { seed: args.seed } : {}
1245
+ ...args.seed != null ? { seed: args.seed } : {},
1246
+ ...south ? { reference_image: encode(south) } : args.styleReference ? { reference_image: encode(args.styleReference) } : {}
1145
1247
  };
1146
1248
  } else {
1147
1249
  path33 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
@@ -1154,6 +1256,10 @@ var PixelLabClient = class {
1154
1256
  ...args.shading ? { shading: args.shading } : {},
1155
1257
  ...args.detail ? { detail: args.detail } : {},
1156
1258
  ...args.seed != null ? { seed: args.seed } : {},
1259
+ ...args.proportions !== void 0 ? { proportions: proportionsPayload(args.proportions) } : {},
1260
+ ...args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
1261
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
1262
+ ...args.reference && Object.keys(args.reference).length ? { directions: Object.fromEntries(Object.entries(args.reference).flatMap(([direction, image]) => image ? [[direction, encode(image)]] : [])) } : {},
1157
1263
  ...palette
1158
1264
  };
1159
1265
  }
@@ -1182,6 +1288,7 @@ var PixelLabClient = class {
1182
1288
  * the action text; pro is the sequential engine. One job per direction.
1183
1289
  */
1184
1290
  async animateCharacter(args) {
1291
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1185
1292
  const raw = await this.request("/animate-character", {
1186
1293
  method: "POST",
1187
1294
  body: JSON.stringify({
@@ -1191,8 +1298,18 @@ var PixelLabClient = class {
1191
1298
  directions: args.directions,
1192
1299
  ...args.template ? { template_animation_id: args.template } : {},
1193
1300
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
1301
+ ...args.description ? { description: args.description } : {},
1194
1302
  ...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
1195
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 } : {},
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 } : {},
1311
+ ...args.isometric !== void 0 ? { isometric: args.isometric } : {},
1312
+ ...args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {},
1196
1313
  ...args.seed != null ? { seed: args.seed } : {}
1197
1314
  })
1198
1315
  });
@@ -1789,12 +1906,122 @@ function skipSubBlocks(bytes, start) {
1789
1906
  function cacheFileName(hash, mediaType = MediaType.PNG) {
1790
1907
  return `${hash}${mediaExtension(mediaType)}`;
1791
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
+ }
1792
1937
 
1793
1938
  // src/providers/pixellab.ts
1794
1939
  import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
1795
1940
  import { randomUUID } from "crypto";
1796
1941
  import os from "os";
1797
1942
  import path from "path";
1943
+
1944
+ // src/hash.ts
1945
+ import { createHash } from "crypto";
1946
+ import { createReadStream } from "fs";
1947
+ function sha256(data) {
1948
+ return createHash("sha256").update(data).digest("hex");
1949
+ }
1950
+ async function sha256File(path33) {
1951
+ return new Promise((resolve, reject) => {
1952
+ const hash = createHash("sha256");
1953
+ const stream = createReadStream(path33);
1954
+ stream.on("data", (chunk2) => hash.update(chunk2));
1955
+ stream.on("error", reject);
1956
+ stream.on("end", () => resolve(hash.digest("hex")));
1957
+ });
1958
+ }
1959
+ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
1960
+ return sha256(
1961
+ JSON.stringify({
1962
+ // Preserve every existing PixelLab hash while making a provider switch
1963
+ // invalidate the spec. Older manifests implicitly mean pixellab.
1964
+ provider: spec.provider === "pixellab" ? void 0 : spec.provider,
1965
+ providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
1966
+ providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
1967
+ generator: spec.generator,
1968
+ prompt: spec.prompt,
1969
+ width: spec.width,
1970
+ height: spec.height,
1971
+ view: spec.view,
1972
+ outline: spec.outline ?? null,
1973
+ shading: spec.shading ?? null,
1974
+ detail: spec.detail ?? null,
1975
+ seed: spec.seed ?? null,
1976
+ palette: spec.palette,
1977
+ // `noBackground` only reaches the wire for pixflux; the tile fields are
1978
+ // undefined for every other generator. `tileSize` is intentionally
1979
+ // absent; width/height are derived from it, so it is already covered.
1980
+ noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
1981
+ tileType: spec.tileType,
1982
+ tileView: spec.tileView,
1983
+ tileFeature: spec.tileFeature,
1984
+ outlineMode: spec.outlineMode,
1985
+ styleImages: styleImageHashes,
1986
+ // A character family: the engine and rotations of a base, the parent's
1987
+ // generated bytes for a state or animation (so a regenerated parent
1988
+ // makes its children stale), and each shape's own settings.
1989
+ character: spec.character ? {
1990
+ kind: spec.character.kind,
1991
+ mode: spec.character.mode,
1992
+ directions: spec.character.directions,
1993
+ template: spec.character.template,
1994
+ parent: spec.character.parentAssetId,
1995
+ parentSha256: spec.character.parentSha256 ?? null,
1996
+ state: spec.character.state,
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,
2003
+ // Absent keys keep the hashes of manifests that never set these.
2004
+ proportions: spec.character.proportions,
2005
+ textGuidanceScale: spec.character.textGuidanceScale,
2006
+ isometric: spec.character.isometric,
2007
+ enhancePrompt: spec.character.enhancePrompt,
2008
+ reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
2009
+ } : void 0,
2010
+ // A mirror's bytes come from its source's recorded outputs; the plan
2011
+ // compares those directly, so only the choice of source is identity.
2012
+ mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
2013
+ revision: spec.revision ? {
2014
+ mode: spec.revision.mode,
2015
+ from: spec.revision.sourceAssetId,
2016
+ sourceSha256: spec.revision.sourceSha256,
2017
+ maskSha256: spec.revision.maskSha256,
2018
+ strength: spec.revision.strength
2019
+ } : void 0
2020
+ })
2021
+ );
2022
+ }
2023
+
2024
+ // src/providers/pixellab.ts
1798
2025
  function pixelLabObjectUrl(generator, objectId) {
1799
2026
  if (!objectId) return null;
1800
2027
  if (generator === "map" || generator === "1dir") {
@@ -1917,8 +2144,8 @@ var PixelLabProvider = class _PixelLabProvider {
1917
2144
  "high detail"
1918
2145
  ]);
1919
2146
  }
1920
- if (spec.generator === "character") this.validateCharacter(spec);
1921
- if ((spec.generator === "map" || spec.generator === "pixflux" || spec.generator === "character") && styleImages.length) {
2147
+ if (spec.generator === "character") this.validateCharacter(spec, styleImages);
2148
+ if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
1922
2149
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
1923
2150
  }
1924
2151
  for (const image of styleImages) {
@@ -1934,9 +2161,80 @@ var PixelLabProvider = class _PixelLabProvider {
1934
2161
  );
1935
2162
  }
1936
2163
  }
1937
- validateCharacter(spec) {
2164
+ validateCharacter(spec, styleImages = []) {
1938
2165
  const character = spec.character;
1939
2166
  if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
2167
+ const label = `${spec.styleId}/${spec.assetId}`;
2168
+ if (character.kind === "base" && character.proportions !== void 0) {
2169
+ if (character.mode !== "standard") {
2170
+ throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
2171
+ }
2172
+ if (character.template !== "mannequin") {
2173
+ throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
2174
+ }
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
+ }
2195
+ if (character.reference) {
2196
+ const given = Object.keys(character.reference);
2197
+ if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
2198
+ if (character.mode !== "standard") {
2199
+ if (given.length > 1) throw new Error(`${label}: PixelLab ${character.mode} rotates one south-facing reference; drop the other directions`);
2200
+ const limit = character.mode === "v3" ? 256 : 168;
2201
+ const south = character.reference.south;
2202
+ if (south.width > limit || south.height > limit) {
2203
+ throw new Error(`${label}: reference is ${south.width}x${south.height}; PixelLab ${character.mode} takes up to ${limit}px`);
2204
+ }
2205
+ } else {
2206
+ const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2207
+ for (const direction of given) {
2208
+ if (!allowed.includes(direction)) {
2209
+ throw new Error(`${label}: reference "${direction}" is not one of this character's ${character.directions} directions`);
2210
+ }
2211
+ const image = character.reference[direction];
2212
+ if (image.width !== spec.width || image.height !== spec.height) {
2213
+ throw new Error(
2214
+ `${label}: reference (${direction}) is ${image.width}x${image.height}; standard mode wants each image at the style's size, ${spec.width}x${spec.height}`
2215
+ );
2216
+ }
2217
+ }
2218
+ if (character.template !== "mannequin" && !character.reference.east) {
2219
+ throw new Error(`${label}: a ${character.template} reference needs south and east images`);
2220
+ }
2221
+ }
2222
+ }
2223
+ if (styleImages.length) {
2224
+ if (character.mode !== "pro") {
2225
+ throw new Error(
2226
+ `PixelLab ${character.mode} characters take no style images; give a base its own sprite with \`reference\`, or use mode pro for a style anchor`
2227
+ );
2228
+ }
2229
+ if (styleImages.length > 1) throw new Error("PixelLab pro characters take one style image");
2230
+ if (character.kind === "base" && character.reference) {
2231
+ throw new Error(`${label}: a pro base rotating its own reference takes no style image`);
2232
+ }
2233
+ const image = styleImages[0];
2234
+ if (image.width > 168 || image.height > 168) {
2235
+ throw new Error(`PixelLab pro character style image is ${image.width}x${image.height}; the limit is 168px`);
2236
+ }
2237
+ }
1940
2238
  const maxSize = character.mode === "v3" ? 256 : 128;
1941
2239
  if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
1942
2240
  throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
@@ -1972,10 +2270,11 @@ var PixelLabProvider = class _PixelLabProvider {
1972
2270
  * asset so a later run can find it, and clears its own earlier take for
1973
2271
  * that direction first, since PixelLab skips a direction that exists.
1974
2272
  */
1975
- async submitCharacter(spec, context) {
2273
+ async submitCharacter(spec, styleImages, context) {
1976
2274
  const character = spec.character;
1977
2275
  if (character.kind === "base") {
1978
2276
  const swatch = spec.palette.length && character.mode === "standard" ? paletteSwatch(spec.palette).toString("base64") : void 0;
2277
+ const styleImage = styleImages[0];
1979
2278
  const res2 = await this.client.createCharacter({
1980
2279
  mode: character.mode,
1981
2280
  description: spec.prompt,
@@ -1988,7 +2287,13 @@ var PixelLabProvider = class _PixelLabProvider {
1988
2287
  detail: spec.detail,
1989
2288
  seed: spec.seed,
1990
2289
  noBackground: spec.noBackground,
1991
- paletteSwatchBase64: swatch
2290
+ paletteSwatchBase64: swatch,
2291
+ proportions: character.proportions,
2292
+ textGuidanceScale: character.textGuidanceScale,
2293
+ isometric: character.isometric,
2294
+ reference: character.reference ? readReference(spec, character.reference) : void 0,
2295
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
2296
+ enhancePrompt: character.enhancePrompt
1992
2297
  });
1993
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 } } };
1994
2299
  }
@@ -2030,7 +2335,17 @@ var PixelLabProvider = class _PixelLabProvider {
2030
2335
  frameCount: animation.frames,
2031
2336
  keepFirstFrame: animation.keepFirstFrame,
2032
2337
  directions: [animation.direction],
2033
- seed: spec.seed
2338
+ seed: spec.seed,
2339
+ textGuidanceScale: character.textGuidanceScale,
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
2034
2349
  });
2035
2350
  const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
2036
2351
  return {
@@ -2120,7 +2435,7 @@ var PixelLabProvider = class _PixelLabProvider {
2120
2435
  }
2121
2436
  async submit(spec, styleImages, context) {
2122
2437
  this.validate(spec, styleImages);
2123
- if (spec.generator === "character") return this.submitCharacter(spec, context);
2438
+ if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
2124
2439
  if (spec.generator === "pixflux") {
2125
2440
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
2126
2441
  const { png } = await this.client.createImagePixflux({
@@ -2387,6 +2702,24 @@ function firstUrl(urls) {
2387
2702
  if (!urls) return null;
2388
2703
  return Object.values(urls).find((u) => typeof u === "string") ?? null;
2389
2704
  }
2705
+ function readReference(spec, reference) {
2706
+ const images = {};
2707
+ for (const [direction, image] of Object.entries(reference)) {
2708
+ const bytes = readFileSync(image.path);
2709
+ if (sha256(bytes) !== image.sha256) {
2710
+ throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
2711
+ }
2712
+ images[direction] = { base64: bytes.toString("base64"), format: image.format };
2713
+ }
2714
+ return images;
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
+ }
2390
2723
  function rotationSources(character) {
2391
2724
  const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
2392
2725
  const sources = [];
@@ -3258,77 +3591,6 @@ function redactScenarioError(value, secrets) {
3258
3591
  import { randomUUID as randomUUID2 } from "crypto";
3259
3592
  import { readFile } from "fs/promises";
3260
3593
  import path2 from "path";
3261
-
3262
- // src/hash.ts
3263
- import { createHash } from "crypto";
3264
- import { createReadStream } from "fs";
3265
- function sha256(data) {
3266
- return createHash("sha256").update(data).digest("hex");
3267
- }
3268
- async function sha256File(path33) {
3269
- return new Promise((resolve, reject) => {
3270
- const hash = createHash("sha256");
3271
- const stream = createReadStream(path33);
3272
- stream.on("data", (chunk2) => hash.update(chunk2));
3273
- stream.on("error", reject);
3274
- stream.on("end", () => resolve(hash.digest("hex")));
3275
- });
3276
- }
3277
- function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
3278
- return sha256(
3279
- JSON.stringify({
3280
- // Preserve every existing PixelLab hash while making a provider switch
3281
- // invalidate the spec. Older manifests implicitly mean pixellab.
3282
- provider: spec.provider === "pixellab" ? void 0 : spec.provider,
3283
- providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
3284
- providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
3285
- generator: spec.generator,
3286
- prompt: spec.prompt,
3287
- width: spec.width,
3288
- height: spec.height,
3289
- view: spec.view,
3290
- outline: spec.outline ?? null,
3291
- shading: spec.shading ?? null,
3292
- detail: spec.detail ?? null,
3293
- seed: spec.seed ?? null,
3294
- palette: spec.palette,
3295
- // `noBackground` only reaches the wire for pixflux; the tile fields are
3296
- // undefined for every other generator. `tileSize` is intentionally
3297
- // absent; width/height are derived from it, so it is already covered.
3298
- noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
3299
- tileType: spec.tileType,
3300
- tileView: spec.tileView,
3301
- tileFeature: spec.tileFeature,
3302
- outlineMode: spec.outlineMode,
3303
- styleImages: styleImageHashes,
3304
- // A character family: the engine and rotations of a base, the parent's
3305
- // generated bytes for a state or animation (so a regenerated parent
3306
- // makes its children stale), and each shape's own settings.
3307
- character: spec.character ? {
3308
- kind: spec.character.kind,
3309
- mode: spec.character.mode,
3310
- directions: spec.character.directions,
3311
- template: spec.character.template,
3312
- parent: spec.character.parentAssetId,
3313
- parentSha256: spec.character.parentSha256 ?? null,
3314
- state: spec.character.state,
3315
- animation: spec.character.animation
3316
- } : void 0,
3317
- // A mirror's bytes come from its source's recorded outputs; the plan
3318
- // compares those directly, so only the choice of source is identity.
3319
- mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
3320
- revision: spec.revision ? {
3321
- mode: spec.revision.mode,
3322
- from: spec.revision.sourceAssetId,
3323
- sourceSha256: spec.revision.sourceSha256,
3324
- maskSha256: spec.revision.maskSha256,
3325
- strength: spec.revision.strength
3326
- } : void 0
3327
- })
3328
- );
3329
- }
3330
-
3331
- // src/providers/comfyui.ts
3332
3594
  var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
3333
3595
  var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
3334
3596
  "prompt",
@@ -4955,16 +5217,16 @@ async function resolveSpecs(loaded, filter) {
4955
5217
  }
4956
5218
  }
4957
5219
  const styleImageCache = /* @__PURE__ */ new Map();
4958
- async function loadStyleImage(rel) {
5220
+ async function loadStyleImage(rel, what = "Style image") {
4959
5221
  const abs = path5.resolve(root, rel);
4960
5222
  let hit = styleImageCache.get(abs);
4961
5223
  if (!hit) {
4962
- if (!existsSync4(abs)) throw new Error(`Style image not found: ${abs}`);
5224
+ if (!existsSync4(abs)) throw new Error(`${what} not found: ${abs}`);
4963
5225
  const buf = await readFile3(abs);
4964
5226
  const metadata = imageMetadata(buf);
4965
- if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
5227
+ if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
4966
5228
  if (metadata.width < 1 || metadata.height < 1) {
4967
- throw new Error(`Style image has invalid dimensions: ${abs}`);
5229
+ throw new Error(`${what} has invalid dimensions: ${abs}`);
4968
5230
  }
4969
5231
  hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
4970
5232
  styleImageCache.set(abs, hit);
@@ -5071,7 +5333,7 @@ async function resolveSpecs(loaded, filter) {
5071
5333
  tileView: generator === "tiles" ? style.tileView : void 0,
5072
5334
  tileFeature: generator === "tiles" ? style.tileFeature : void 0,
5073
5335
  outlineMode: generator === "tiles" ? style.outlineMode : void 0,
5074
- ...generator === "character" ? { character: resolveCharacterShape(asset, style, characterKind) } : {},
5336
+ ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
5075
5337
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
5076
5338
  costUnit: "generations",
5077
5339
  candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
@@ -5288,43 +5550,34 @@ async function resolveStyleImages(loaded, styleId) {
5288
5550
  async function styleImagesBase64(loaded, styleId) {
5289
5551
  return (await resolveStyleImages(loaded, styleId)).map((img) => img.base64);
5290
5552
  }
5291
- function imageMetadata(buf) {
5292
- if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
5293
- return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
5294
- }
5295
- if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
5296
- let offset = 2;
5297
- while (offset + 3 < buf.length) {
5298
- if (buf[offset] !== 255) return null;
5299
- while (buf[offset] === 255) offset++;
5300
- const marker = buf[offset++];
5301
- if (marker == null || marker === 217 || marker === 218) break;
5302
- if (marker === 1 || marker >= 208 && marker <= 215) continue;
5303
- if (offset + 2 > buf.length) return null;
5304
- const length = buf.readUInt16BE(offset);
5305
- if (length < 2 || offset + length > buf.length) return null;
5306
- const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
5307
- if (isStartOfFrame) {
5308
- if (length < 7) return null;
5309
- return {
5310
- width: buf.readUInt16BE(offset + 5),
5311
- height: buf.readUInt16BE(offset + 3),
5312
- format: "jpeg"
5313
- };
5314
- }
5315
- offset += length;
5316
- }
5317
- return null;
5318
- }
5319
- function resolveCharacterShape(asset, style, kind) {
5553
+ async function resolveCharacterShape(asset, style, kind, files) {
5320
5554
  const mode2 = style.mode ?? "standard";
5321
5555
  const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
5556
+ const proportions = asset.proportions ?? style.proportions;
5322
5557
  const shape = {
5323
5558
  kind,
5324
5559
  mode: mode2,
5325
5560
  directions,
5326
- template: style.template ?? "mannequin"
5561
+ template: style.template ?? "mannequin",
5562
+ ...proportions !== void 0 ? { proportions } : {},
5563
+ ...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
5564
+ ...style.isometric !== void 0 ? { isometric: style.isometric } : {},
5565
+ ...style.enhancePrompt !== void 0 ? { enhancePrompt: style.enhancePrompt } : {}
5327
5566
  };
5567
+ if (asset.reference) {
5568
+ const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
5569
+ shape.reference = {};
5570
+ for (const [direction, rel] of Object.entries(byDirection)) {
5571
+ const image = await files.load(rel, `Reference sprite (${direction})`);
5572
+ shape.reference[direction] = {
5573
+ path: path5.resolve(files.root, rel),
5574
+ sha256: image.hash,
5575
+ width: image.width,
5576
+ height: image.height,
5577
+ format: image.format
5578
+ };
5579
+ }
5580
+ }
5328
5581
  if (asset.state) {
5329
5582
  shape.state = {
5330
5583
  paletteFromReference: asset.state.paletteFromReference,
@@ -5333,13 +5586,24 @@ function resolveCharacterShape(asset, style, kind) {
5333
5586
  }
5334
5587
  if (asset.animation) {
5335
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
+ };
5336
5593
  shape.animation = {
5337
5594
  mode: animationMode,
5338
5595
  ...asset.animation.template ? { template: asset.animation.template } : {},
5339
5596
  direction: asset.animation.direction,
5340
5597
  frames: asset.animation.frames ?? 8,
5341
5598
  fps: asset.animation.fps,
5342
- 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 } : {}
5343
5607
  };
5344
5608
  }
5345
5609
  return shape;
@@ -13601,10 +13865,12 @@ export {
13601
13865
  BudgetError,
13602
13866
  CHARACTER_DIRECTIONS_4,
13603
13867
  CHARACTER_DIRECTIONS_8,
13868
+ CHARACTER_PROPORTION_PRESETS,
13604
13869
  CharacterAnimationModeSchema,
13605
13870
  CharacterAnimationSchema,
13606
13871
  CharacterDirectionSchema,
13607
13872
  CharacterModeSchema,
13873
+ CharacterProportionsSchema,
13608
13874
  CharacterStateSchema,
13609
13875
  ComfyUIProvider,
13610
13876
  DEFAULT_HISTORY_LIMIT,
@@ -13771,6 +14037,7 @@ export {
13771
14037
  portableOutputPath,
13772
14038
  prepareReview,
13773
14039
  primaryOutput,
14040
+ proportionsPayload,
13774
14041
  providerCredentialEnvs,
13775
14042
  providerFactory,
13776
14043
  pushTags,