pixelkiln 0.64.0 → 0.66.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
@@ -1,7 +1,7 @@
1
1
  // src/types.ts
2
2
  import { z } from "zod";
3
3
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
4
- var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement"]);
4
+ var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement", "imageProFlash"]);
5
5
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
6
6
  var RevisionModeSchema = z.enum([
7
7
  "image-to-image",
@@ -186,6 +186,27 @@ var CharacterStateSchema = z.object({
186
186
  height: z.number().int().min(16).max(256)
187
187
  }).strict().optional()
188
188
  }).strict();
189
+ var CharacterPortraitSizeSchema = z.union([
190
+ z.literal(16),
191
+ z.literal(32),
192
+ z.literal(48),
193
+ z.literal(64),
194
+ z.literal(128),
195
+ z.literal(160)
196
+ ]);
197
+ var CharacterPortraitSchema = z.object({
198
+ /** The base or state to portray, in the same style. */
199
+ of: z.string().min(1),
200
+ size: CharacterPortraitSizeSchema.default(64)
201
+ }).strict();
202
+ var CharacterOutfitSchema = z.object({
203
+ /** The loop (a character animation asset) to re-clothe, in the same style. */
204
+ of: z.string().min(1),
205
+ /** Manifest-relative image of the outfit to transfer. */
206
+ reference: z.string().min(1),
207
+ /** Extra guidance PixelLab passes through as written, e.g. view/direction hints. */
208
+ additionalInstructions: z.string().max(2e3).optional()
209
+ }).strict();
189
210
  var CharacterAnimationSchema = z.object({
190
211
  /** The character to animate: a base or a state in the same style. */
191
212
  of: z.string().min(1),
@@ -307,8 +328,22 @@ var RevisionSchema = z.object({
307
328
  /** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
308
329
  direction: CharacterDirectionSchema.optional(),
309
330
  /** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
310
- enhancePrompt: z.boolean().optional()
331
+ enhancePrompt: z.boolean().optional(),
332
+ /**
333
+ * `image-to-image`/`inpaint` only: which provider engine draws the
334
+ * edit. `pro-flash` selects PixelLab's Pro Flash edit and inpaint tier
335
+ * (`/edit-image-pro-flash`, `/inpaint-image-pro-flash`) instead of the
336
+ * default Pro endpoints; omit it for the default.
337
+ */
338
+ engine: z.enum(["pro-flash"]).optional()
311
339
  }).strict().superRefine((revision, context) => {
340
+ if (revision.engine !== void 0 && revision.mode !== "image-to-image" && revision.mode !== "inpaint") {
341
+ context.addIssue({
342
+ code: z.ZodIssueCode.custom,
343
+ message: "engine applies to image-to-image and inpaint revisions only",
344
+ path: ["engine"]
345
+ });
346
+ }
312
347
  if (revision.mode === "inpaint" && !revision.mask) {
313
348
  context.addIssue({
314
349
  code: z.ZodIssueCode.custom,
@@ -597,8 +632,8 @@ var StyleObjectSchema = z.object({
597
632
  /** `character` only. Body template: `mannequin` (default), a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`), or `custom` for `pro-flash`. */
598
633
  template: z.string().min(1).optional(),
599
634
  /**
600
- * `character` only, `pro-flash` bases with a style image: which traits
601
- * the style image lends. Each defaults to true.
635
+ * `character` `pro-flash` bases and `imageProFlash` styles, with a
636
+ * style image: which traits the style image lends. Each defaults to true.
602
637
  */
603
638
  styleTraits: z.object({
604
639
  palette: z.boolean().optional(),
@@ -807,6 +842,10 @@ var AssetSchema = z.object({
807
842
  pieces: z.array(UiPieceSchema).min(1).optional(),
808
843
  /** `uiAsset` styles: named, auto-positioned UI element scaffolds. Combine with `pieces`; omit both for a default full-canvas panel. */
809
844
  elements: z.array(UiElementSchema).min(1).optional(),
845
+ /** `character` styles: this asset is a bust portrait of another character asset's south sprite. */
846
+ portrait: CharacterPortraitSchema.optional(),
847
+ /** `character` styles: this asset re-clothes an existing loop's frames with a reference outfit. */
848
+ outfit: CharacterOutfitSchema.optional(),
810
849
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
811
850
  proportions: CharacterProportionsSchema.optional(),
812
851
  /**
@@ -883,7 +922,7 @@ var AssetSchema = z.object({
883
922
  path: ["revision"]
884
923
  });
885
924
  }
886
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
925
+ const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch", asset.portrait && "portrait", asset.outfit && "outfit"].filter(Boolean);
887
926
  if (shapes.length > 1) {
888
927
  context.addIssue({
889
928
  code: z.ZodIssueCode.custom,
@@ -898,24 +937,24 @@ var AssetSchema = z.object({
898
937
  path: ["mirror"]
899
938
  });
900
939
  }
901
- if (asset.prompt === void 0 && !asset.mirror) {
940
+ if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit) {
902
941
  context.addIssue({
903
942
  code: z.ZodIssueCode.custom,
904
943
  message: "Required",
905
944
  path: ["prompt"]
906
945
  });
907
946
  }
908
- if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
947
+ if (asset.reference && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
909
948
  context.addIssue({
910
949
  code: z.ZodIssueCode.custom,
911
- message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
950
+ message: "a reference sprite belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
912
951
  path: ["reference"]
913
952
  });
914
953
  }
915
- if (asset.concept && (asset.state || asset.animation || asset.mirror)) {
954
+ if (asset.concept && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
916
955
  context.addIssue({
917
956
  code: z.ZodIssueCode.custom,
918
- message: "a concept image belongs on a base; a state, animation, or mirror takes its look from its parent",
957
+ message: "a concept image belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
919
958
  path: ["concept"]
920
959
  });
921
960
  }
@@ -1027,7 +1066,8 @@ var LockEntrySchema = z.object({
1027
1066
  fps: z.number().int().min(1).max(60).optional(),
1028
1067
  lastFrameSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1029
1068
  direction: CharacterDirectionSchema.optional(),
1030
- enhancePrompt: z.boolean().optional()
1069
+ enhancePrompt: z.boolean().optional(),
1070
+ engine: z.enum(["pro-flash"]).optional()
1031
1071
  }).strict().nullable().default(null),
1032
1072
  /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
1033
1073
  mirror: z.object({
@@ -1040,6 +1080,17 @@ var LockEntrySchema = z.object({
1040
1080
  leaderAssetId: z.string().min(1).optional(),
1041
1081
  index: z.number().int().min(1).optional()
1042
1082
  }).strict().nullable().default(null),
1083
+ /**
1084
+ * For an outfit: the loop it re-clothed, a hash over that loop's output
1085
+ * hashes when this was made, and the reference image's own hash. Tracked
1086
+ * the way `mirror` is, not through `specHash`: manifest resolution has no
1087
+ * lock to hash the parent's actual current frame bytes against.
1088
+ */
1089
+ outfit: z.object({
1090
+ sourceAssetId: z.string().min(1),
1091
+ sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
1092
+ referenceSha256: z.string().regex(/^[0-9a-f]{64}$/)
1093
+ }).strict().nullable().default(null),
1043
1094
  /**
1044
1095
  * Output hashes owned by the previous generation while its replacement is
1045
1096
  * pending. They authorize replacing only unchanged PixelKiln-owned files.
@@ -1393,6 +1444,14 @@ var CorrectPixelartResponseSchema = z2.object({
1393
1444
  images: z2.array(z2.object({ base64: z2.string().min(1) }).passthrough()).min(1),
1394
1445
  usage: z2.unknown().optional()
1395
1446
  }).passthrough();
1447
+ var ProFlashImageSubmitSchema = z2.object({
1448
+ background_job_id: z2.string().min(1),
1449
+ image_id: z2.string().min(1),
1450
+ source_image_id: z2.string().min(1),
1451
+ status: z2.string().default("processing"),
1452
+ estimated_generations: z2.number().nullable().optional(),
1453
+ usage: z2.unknown().optional()
1454
+ }).passthrough();
1396
1455
  var UnzoomResponseSchema = z2.object({
1397
1456
  image: z2.object({ base64: z2.string().min(1) }).passthrough(),
1398
1457
  original_size: z2.object({ width: z2.number().int(), height: z2.number().int() }).passthrough(),
@@ -1443,6 +1502,28 @@ var AnimateObjectSubmitSchema = z2.object({
1443
1502
  submissions: z2.array(DirectionSubmissionSchema),
1444
1503
  usage: UsageSchema
1445
1504
  }).passthrough();
1505
+ var PortraitSubmitSchema = z2.object({
1506
+ background_job_id: z2.string().min(1),
1507
+ status: z2.string().default("processing"),
1508
+ usage: UsageSchema
1509
+ }).passthrough();
1510
+ var PortraitJobSchema = z2.object({
1511
+ status: z2.string(),
1512
+ download_url: z2.string().nullable().optional(),
1513
+ width: z2.number().int().nullable().optional(),
1514
+ height: z2.number().int().nullable().optional()
1515
+ }).passthrough();
1516
+ var SetPortraitSchema = z2.object({
1517
+ character_id: z2.string().min(1),
1518
+ size: z2.number().int().positive(),
1519
+ url: z2.string().min(1),
1520
+ usage: UsageSchema
1521
+ }).passthrough();
1522
+ var TransferOutfitSubmitSchema = z2.object({
1523
+ background_job_id: z2.string().min(1),
1524
+ status: z2.string().default("processing"),
1525
+ usage: UsageSchema
1526
+ }).passthrough();
1446
1527
  var CharacterAnimationDirectionSchema = z2.object({
1447
1528
  direction: z2.string(),
1448
1529
  frame_count: z2.number().int().min(0),
@@ -1811,7 +1892,7 @@ var PixelLabClient = class {
1811
1892
  const body = {
1812
1893
  description: args.description,
1813
1894
  n_directions: args.directions,
1814
- ...args.reference ? { first_frame: encode(args.reference) } : { image_size: { width: args.size, height: args.size } },
1895
+ ...args.sourceImageId ? { source_image_id: args.sourceImageId } : args.reference ? { first_frame: encode(args.reference) } : { image_size: { width: args.size, height: args.size } },
1815
1896
  ...args.view ? { view: args.view } : {},
1816
1897
  ...args.seed != null ? { seed: args.seed } : {},
1817
1898
  ...args.styleReference && args.styleReferenceSize ? {
@@ -1958,8 +2039,9 @@ var PixelLabClient = class {
1958
2039
  description: args.description,
1959
2040
  template_id: args.template,
1960
2041
  n_directions: 8,
1961
- // The author's sprite sets the canvas; a size only applies to text creation.
1962
- ...south ? { first_frame: encode(south) } : { image_size: imageSize },
2042
+ // The author's sprite sets the canvas; a size only applies to text
2043
+ // creation. An owned copy of the same sprite is referenced by id.
2044
+ ...args.sourceImageId ? { source_image_id: args.sourceImageId } : south ? { first_frame: encode(south) } : { image_size: imageSize },
1963
2045
  ...args.view ? { view: args.view } : {},
1964
2046
  ...args.seed != null ? { seed: args.seed } : {},
1965
2047
  ...args.styleReference && args.styleReferenceSize ? {
@@ -2058,6 +2140,69 @@ var PixelLabClient = class {
2058
2140
  });
2059
2141
  return validateResponse(AnimateSubmitSchema, raw, "animate-character");
2060
2142
  }
2143
+ /**
2144
+ * A full-body sprite in, a bust portrait out (or the reverse, unused
2145
+ * today). Job-based like a state or `1dir`, not the couple-generations
2146
+ * "conversion" the name suggests: the smallest `resultSize` (16px) billed
2147
+ * 20 generations live (docs/ENDPOINTS.md, "Characters, measured").
2148
+ */
2149
+ async createPortraitCharacterPro(args) {
2150
+ const raw = await this.request("/portrait-character-pro", {
2151
+ method: "POST",
2152
+ body: JSON.stringify({
2153
+ direction: args.direction,
2154
+ image: { type: "base64", base64: args.image.base64, format: args.image.format },
2155
+ ...args.view ? { view: args.view } : {},
2156
+ ...args.resultSize != null ? { result_size: args.resultSize } : {},
2157
+ ...args.seed != null ? { seed: args.seed } : {}
2158
+ })
2159
+ });
2160
+ return validateResponse(PortraitSubmitSchema, raw, "portrait-character-pro");
2161
+ }
2162
+ /** Throws PixelLabError(423) while the job is still drawing, like tiles and map objects. */
2163
+ async getPortraitCharacterJob(jobId) {
2164
+ const raw = await this.request(`/portrait-character-pro/${encodeURIComponent(jobId)}`);
2165
+ return validateResponse(PortraitJobSchema, raw, "portrait-character-pro/{id}");
2166
+ }
2167
+ /**
2168
+ * Attaches a portrait image to a character record. Synchronous and free
2169
+ * (0 generations, live); PixelLab does not link this to
2170
+ * `createPortraitCharacterPro` automatically, and `GET /characters/{id}`
2171
+ * never grows a portrait field, so this response's `url` is the only place
2172
+ * it appears.
2173
+ */
2174
+ async setCharacterPortrait(characterId, image) {
2175
+ const raw = await this.request(`/characters/${encodeURIComponent(characterId)}/portrait`, {
2176
+ method: "POST",
2177
+ body: JSON.stringify({ image: { type: "base64", base64: image.base64, format: image.format } })
2178
+ });
2179
+ return validateResponse(SetPortraitSchema, raw, "characters/{id}/portrait");
2180
+ }
2181
+ /**
2182
+ * Applies a reference outfit to 2–16 existing frames. Job-based like
2183
+ * everything else here, but the completed job returns the result frames
2184
+ * inline as base64 (`last_response.quantized_images`), not as storage
2185
+ * URLs — the one PixelLab character tool that works this way. A live
2186
+ * 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md).
2187
+ */
2188
+ async transferOutfitV2(args) {
2189
+ const image = (img) => ({
2190
+ image: { type: "base64", base64: img.base64, format: img.format },
2191
+ size: { width: img.width, height: img.height }
2192
+ });
2193
+ const raw = await this.request("/transfer-outfit-v2", {
2194
+ method: "POST",
2195
+ body: JSON.stringify({
2196
+ reference_image: image(args.referenceImage),
2197
+ frames: args.frames.map(image),
2198
+ image_size: args.imageSize,
2199
+ ...args.seed != null ? { seed: args.seed } : {},
2200
+ ...args.noBackground !== void 0 ? { no_background: args.noBackground } : {},
2201
+ ...args.additionalInstructions ? { additional_instructions: args.additionalInstructions } : {}
2202
+ })
2203
+ });
2204
+ return validateResponse(TransferOutfitSubmitSchema, raw, "transfer-outfit-v2");
2205
+ }
2061
2206
  async getCharacter(characterId) {
2062
2207
  const raw = await this.request(`/characters/${encodeURIComponent(characterId)}`);
2063
2208
  return validateResponse(CharacterSchema, raw, "characters/{id}");
@@ -2168,6 +2313,66 @@ var PixelLabClient = class {
2168
2313
  "generate-image-v2"
2169
2314
  );
2170
2315
  }
2316
+ /**
2317
+ * `/create-image-pro-flash`: one native pixel-art image from a
2318
+ * description, optionally styled after a style image with per-trait
2319
+ * toggles, on the same Pro Flash model `character`/`objectPro` use for
2320
+ * their `pro-flash` engine. Sizes run 16 to 256 per side in multiples of 4.
2321
+ * A plain background job; `pollRevision`'s generic reader takes the
2322
+ * finished image. The response's `source_image_id` is what a Pro Flash
2323
+ * character or object can reuse instead of an upload.
2324
+ */
2325
+ async createImageProFlash(args) {
2326
+ const body = {
2327
+ description: args.description,
2328
+ image_size: { width: args.width, height: args.height }
2329
+ };
2330
+ if (args.noBackground != null) body.no_background = args.noBackground;
2331
+ if (args.seed != null) body.seed = args.seed;
2332
+ if (args.styleImage) {
2333
+ body.style_image = { image: args.styleImage.image, size: { width: args.styleImage.width, height: args.styleImage.height } };
2334
+ const traits = args.styleTraits;
2335
+ if (traits) {
2336
+ body.style_options = {
2337
+ ...traits.palette !== void 0 ? { color_palette: traits.palette } : {},
2338
+ ...traits.outline !== void 0 ? { outline: traits.outline } : {},
2339
+ ...traits.detail !== void 0 ? { detail: traits.detail } : {},
2340
+ ...traits.shading !== void 0 ? { shading: traits.shading } : {}
2341
+ };
2342
+ }
2343
+ }
2344
+ return validateResponse(
2345
+ ProFlashImageSubmitSchema,
2346
+ await this.request("/create-image-pro-flash", { method: "POST", body: JSON.stringify(body) }),
2347
+ "create-image-pro-flash"
2348
+ );
2349
+ }
2350
+ /** `/edit-image-pro-flash`, text method: edit one image at its own size. The canvas never grows or resizes. */
2351
+ async editImageProFlash(args) {
2352
+ const body = { image: args.image, method: "text", description: args.description };
2353
+ if (args.noBackground != null) body.no_background = args.noBackground;
2354
+ if (args.seed != null) body.seed = args.seed;
2355
+ return validateResponse(
2356
+ ProFlashImageSubmitSchema,
2357
+ await this.request("/edit-image-pro-flash", { method: "POST", body: JSON.stringify(body) }),
2358
+ "edit-image-pro-flash"
2359
+ );
2360
+ }
2361
+ /**
2362
+ * `/inpaint-image-pro-flash`: replace only the white pixels of a same-size
2363
+ * black/white mask, keeping everything else exactly. `Modify current
2364
+ * layer` (the endpoint's default) returns the full composite.
2365
+ */
2366
+ async inpaintImageProFlash(args) {
2367
+ const body = { image: args.image, mask_image: args.maskImage, description: args.description };
2368
+ if (args.noBackground != null) body.no_background = args.noBackground;
2369
+ if (args.seed != null) body.seed = args.seed;
2370
+ return validateResponse(
2371
+ ProFlashImageSubmitSchema,
2372
+ await this.request("/inpaint-image-pro-flash", { method: "POST", body: JSON.stringify(body) }),
2373
+ "inpaint-image-pro-flash"
2374
+ );
2375
+ }
2171
2376
  /**
2172
2377
  * `/generate-ui-v2` ("Generate UI (Pro)"): one UI element (a button, a
2173
2378
  * health bar, an inventory slot, a dialogue box) from a description,
@@ -3048,6 +3253,7 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3048
3253
  uiPieces: spec.uiPieces,
3049
3254
  uiElements: spec.uiElements,
3050
3255
  uiColorPalette: spec.uiColorPalette,
3256
+ styleTraits: spec.styleTraits,
3051
3257
  styleImages: styleImageHashes,
3052
3258
  // A character family: the engine and rotations of a base, the parent's
3053
3259
  // generated bytes for a state or animation (so a regenerated parent
@@ -3074,7 +3280,16 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3074
3280
  styleTraits: spec.character.styleTraits,
3075
3281
  reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0,
3076
3282
  concept: spec.character.concept?.sha256,
3077
- styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0
3283
+ styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0,
3284
+ // The parent's own bytes come from its recorded outputs; the plan
3285
+ // compares those directly (like a mirror), so only the choice of
3286
+ // parent, the reference image's bytes, and any extra guidance
3287
+ // are identity here.
3288
+ outfit: spec.character.outfit ? {
3289
+ of: spec.character.parentAssetId,
3290
+ reference: spec.character.outfit.reference.sha256,
3291
+ additionalInstructions: spec.character.outfit.additionalInstructions
3292
+ } : void 0
3078
3293
  } : void 0,
3079
3294
  // An objectPro family: same shape as character above, minus the
3080
3295
  // fields it has none of (mode/template/proportions/isometric/concept/
@@ -3115,7 +3330,8 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3115
3330
  fps: spec.revision.fps,
3116
3331
  lastFrameSha256: spec.revision.lastFrameSha256,
3117
3332
  direction: spec.revision.direction,
3118
- enhancePrompt: spec.revision.enhancePrompt
3333
+ enhancePrompt: spec.revision.enhancePrompt,
3334
+ engine: spec.revision.engine
3119
3335
  } : void 0
3120
3336
  })
3121
3337
  );
@@ -3155,7 +3371,10 @@ function characterCost(spec) {
3155
3371
  if (animation.mode === "v3") return Math.max(1, Math.ceil(px * animation.frames / 65536));
3156
3372
  return generationCost(spec.width, spec.height, "1dir");
3157
3373
  }
3158
- if (character.kind === "state") return generationCost(spec.width, spec.height, "1dir");
3374
+ if (character.kind === "state" || character.kind === "portrait") return generationCost(spec.width, spec.height, "1dir");
3375
+ if (character.kind === "outfit") {
3376
+ return 20;
3377
+ }
3159
3378
  if (character.mode === "standard") return 1;
3160
3379
  if (character.mode === "v3") return 1 + Math.max(1, Math.ceil(px * 8 / 65536));
3161
3380
  if (character.mode === "pro-flash") {
@@ -3168,8 +3387,11 @@ function proFlashCharacterCost(width, height, fromReference) {
3168
3387
  const side = Math.max(32, Math.ceil(Math.max(width, height) / 4) * 4);
3169
3388
  const rotations = Math.max(1, Math.ceil(side * side * 8 / 65536));
3170
3389
  if (fromReference) return rotations;
3171
- const image = side <= 96 ? 5 : side <= 208 ? 6 : 9;
3172
- return image + rotations;
3390
+ return proFlashImageCost(side, side) + rotations;
3391
+ }
3392
+ function proFlashImageCost(width, height) {
3393
+ const side = Math.max(width, height);
3394
+ return side <= 96 ? 5 : side <= 208 ? 6 : 9;
3173
3395
  }
3174
3396
  function characterAnimationName(spec) {
3175
3397
  return `pixelkiln:${spec.styleId}/${spec.assetId}`;
@@ -3245,7 +3467,7 @@ var PixelLabProvider = class _PixelLabProvider {
3245
3467
  return new _PixelLabProvider(new PixelLabClient("download-only"));
3246
3468
  }
3247
3469
  supports(generator) {
3248
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement";
3470
+ return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement" || generator === "imageProFlash";
3249
3471
  }
3250
3472
  /**
3251
3473
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
@@ -3272,15 +3494,18 @@ var PixelLabProvider = class _PixelLabProvider {
3272
3494
  return { spacingMs: 2500, maxInFlight: 8 };
3273
3495
  }
3274
3496
  /**
3275
- * Where synchronous pixflux results are parked between `submit` and `fetch`.
3497
+ * Where synchronous pixflux results (or an outfit transfer's inline
3498
+ * frames) are parked between `submit`/poll and `fetch`.
3276
3499
  *
3277
- * pixflux returns the PNG inline rather than a job id, but the pipeline is
3278
- * built around submit → poll → fetch running as separate commands. Writing
3279
- * the bytes to a known path keeps that model intact: the "job id" is the
3280
- * filename, polling is an existence check, and downloading is a file read.
3500
+ * pixflux returns the PNG inline rather than a job id, and an outfit
3501
+ * transfer's completed job returns its frames inline too, but the
3502
+ * pipeline is built around submit → poll → fetch running as separate
3503
+ * commands. Writing the bytes to a known path keeps that model intact:
3504
+ * the id is the filename, polling is an existence check, and downloading
3505
+ * is a file read.
3281
3506
  */
3282
- static cacheDir() {
3283
- const dir = path.join(os.tmpdir(), "pixelkiln-pixflux");
3507
+ static cacheDir(namespace = "pixflux") {
3508
+ const dir = path.join(os.tmpdir(), `pixelkiln-${namespace}`);
3284
3509
  mkdirSync(dir, { recursive: true });
3285
3510
  return dir;
3286
3511
  }
@@ -3304,6 +3529,13 @@ var PixelLabProvider = class _PixelLabProvider {
3304
3529
  const base = Math.max(1, Math.ceil(width * height * frames / 65536));
3305
3530
  return { unit: "generations", amount: spec.revision.enhancePrompt ? base + 0.05 : base, candidates: 1 };
3306
3531
  }
3532
+ if (spec.revision?.engine === "pro-flash") {
3533
+ return {
3534
+ unit: "generations",
3535
+ amount: proFlashImageCost(spec.revision.sourceWidth ?? spec.width, spec.revision.sourceHeight ?? spec.height),
3536
+ candidates: 1
3537
+ };
3538
+ }
3307
3539
  if (spec.revision) {
3308
3540
  const width = spec.revision.sourceWidth ?? spec.width;
3309
3541
  const height = spec.revision.sourceHeight ?? spec.height;
@@ -3318,6 +3550,9 @@ var PixelLabProvider = class _PixelLabProvider {
3318
3550
  if (spec.generator === "objectPro") {
3319
3551
  return { unit: "generations", amount: objectProCost(spec), candidates: 1 };
3320
3552
  }
3553
+ if (spec.generator === "imageProFlash") {
3554
+ return { unit: "generations", amount: proFlashImageCost(spec.width, spec.height), candidates: 1 };
3555
+ }
3321
3556
  return {
3322
3557
  unit: "generations",
3323
3558
  amount: generationCost(spec.width, spec.height, spec.generator),
@@ -3331,7 +3566,17 @@ var PixelLabProvider = class _PixelLabProvider {
3331
3566
  `PixelLab ${spec.revision.mode} reads one source image; ${spec.revision.sourceAssetId} is a ${members.length}-member set (${members[0].role}, ...). Revise one member through a declared source, or use reduce-colors, correct-pixelart, or edit-animation, which take the whole set`
3332
3567
  );
3333
3568
  }
3334
- if (spec.revision?.mode === "inpaint") {
3569
+ if (spec.revision?.engine === "pro-flash") {
3570
+ const { sourceWidth: width, sourceHeight: height, mode: mode2 } = spec.revision;
3571
+ if (width != null && height != null && (width < 32 || height < 32 || width > 256 || height > 256 || width % 4 || height % 4)) {
3572
+ throw new Error(
3573
+ `PixelLab pro-flash ${mode2} source is ${width}x${height}; the API takes 32 to 256 pixels per side, in multiples of 4`
3574
+ );
3575
+ }
3576
+ if (spec.revision.strength != null) {
3577
+ throw new Error(`PixelLab pro-flash ${mode2} takes no strength; the edit applies in full`);
3578
+ }
3579
+ } else if (spec.revision?.mode === "inpaint") {
3335
3580
  const { sourceWidth: width, sourceHeight: height } = spec.revision;
3336
3581
  if (width != null && height != null && (width < 32 || height < 32 || width > 512 || height > 512)) {
3337
3582
  throw new Error(`PixelLab inpaint source is ${width}x${height}; the API takes 32 to 512 pixels per side`);
@@ -3412,6 +3657,19 @@ var PixelLabProvider = class _PixelLabProvider {
3412
3657
  `PixelLab uiAsset is ${spec.width}x${spec.height}; the API takes 192 to 688 pixels per side (the exact ceiling also depends on aspect ratio \u2014 square tops out at 512, 16:9 at 688x384, 9:16 at 384x688, 4:3 at 600x448, 3:4 at 448x600)`
3413
3658
  );
3414
3659
  }
3660
+ if (spec.generator === "imageProFlash") {
3661
+ if (spec.width < 16 || spec.height < 16 || spec.width > 256 || spec.height > 256 || spec.width % 4 || spec.height % 4) {
3662
+ throw new Error(
3663
+ `PixelLab imageProFlash is ${spec.width}x${spec.height}; the API takes 16 to 256 pixels per side, in multiples of 4`
3664
+ );
3665
+ }
3666
+ if (styleImages.length > 1) {
3667
+ throw new Error("PixelLab imageProFlash takes one style image; list at most one styleImages entry");
3668
+ }
3669
+ if (spec.styleTraits && !styleImages.length) {
3670
+ throw new Error("PixelLab imageProFlash: styleTraits choose what a style image lends; add a styleImages entry or drop them");
3671
+ }
3672
+ }
3415
3673
  if (spec.generator === "uiElement" && (spec.width < 16 || spec.height < 16 || spec.width > 792 || spec.height > 688)) {
3416
3674
  throw new Error(
3417
3675
  `PixelLab uiElement is ${spec.width}x${spec.height}; the API takes 16 to 792 wide and 16 to 688 tall (the exact ceiling depends on aspect ratio \u2014 square tops out at 512, 16:9 at 688x384)`
@@ -3540,6 +3798,12 @@ var PixelLabProvider = class _PixelLabProvider {
3540
3798
  const character = spec.character;
3541
3799
  if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
3542
3800
  const label = `${spec.styleId}/${spec.assetId}`;
3801
+ if (character.kind === "outfit") {
3802
+ const reference = character.outfit.reference;
3803
+ if (reference.width < 32 || reference.width > 256 || reference.height < 32 || reference.height > 256) {
3804
+ throw new Error(`${label}: outfit reference is ${reference.width}x${reference.height}; PixelLab transfer-outfit-v2 takes 32 to 256px per side`);
3805
+ }
3806
+ }
3543
3807
  if (character.kind === "base" && character.proportions !== void 0) {
3544
3808
  if (character.mode !== "standard") {
3545
3809
  throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
@@ -3551,7 +3815,7 @@ var PixelLabProvider = class _PixelLabProvider {
3551
3815
  if (character.kind === "base" && character.enhancePrompt !== void 0 && character.mode !== "v3") {
3552
3816
  throw new Error(`${label}: enhancePrompt applies to v3 bases; the ${character.mode} engine does not take it`);
3553
3817
  }
3554
- if (character.animation) {
3818
+ if (character.animation && character.kind !== "outfit") {
3555
3819
  const { startFrame, endFrame } = character.animation;
3556
3820
  for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
3557
3821
  if (image && (image.width > 256 || image.height > 256)) {
@@ -3632,9 +3896,11 @@ var PixelLabProvider = class _PixelLabProvider {
3632
3896
  if (character.styleTraits && character.mode !== "pro-flash") {
3633
3897
  throw new Error(`${label}: styleTraits choose what a pro-flash style image lends; the ${character.mode} engine does not take them`);
3634
3898
  }
3635
- const maxSize = character.mode === "v3" || character.mode === "pro-flash" ? 256 : 128;
3636
- if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
3637
- throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
3899
+ if (character.kind !== "portrait") {
3900
+ const maxSize = character.mode === "v3" || character.mode === "pro-flash" ? 256 : 128;
3901
+ if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
3902
+ throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
3903
+ }
3638
3904
  }
3639
3905
  if (character.mode === "pro-flash" && character.kind === "base" && (spec.width % 4 || spec.height % 4)) {
3640
3906
  throw new Error(`PixelLab pro-flash characters are sized in multiples of 4 pixels; ${spec.width}x${spec.height} is not`);
@@ -3643,9 +3909,11 @@ var PixelLabProvider = class _PixelLabProvider {
3643
3909
  if (!templates.includes(character.template)) {
3644
3910
  throw new Error(`PixelLab ${character.mode} character template must be one of: ${templates.join(", ")}`);
3645
3911
  }
3646
- const views = character.mode === "standard" ? ["low top-down", "high top-down", "side", "perspective", "oblique"] : ["low top-down", "high top-down", "side"];
3912
+ const views = character.kind === "portrait" ? ["low top-down", "high top-down", "side"] : character.mode === "standard" ? ["low top-down", "high top-down", "side", "perspective", "oblique"] : ["low top-down", "high top-down", "side"];
3647
3913
  if (!views.includes(spec.view)) {
3648
- throw new Error(`PixelLab ${character.mode} character view must be one of: ${views.join(", ")}`);
3914
+ throw new Error(
3915
+ character.kind === "portrait" ? `PixelLab portrait view must be one of: ${views.join(", ")}` : `PixelLab ${character.mode} character view must be one of: ${views.join(", ")}`
3916
+ );
3649
3917
  }
3650
3918
  if (spec.view === "oblique" && character.directions !== 4) {
3651
3919
  throw new Error("PixelLab oblique characters are 4-direction only");
@@ -3653,7 +3921,7 @@ var PixelLabProvider = class _PixelLabProvider {
3653
3921
  if (character.kind !== "base" && !character.parentAssetId) {
3654
3922
  throw new Error(`${spec.styleId}/${spec.assetId} is a ${character.kind} with no parent`);
3655
3923
  }
3656
- if (character.animation) {
3924
+ if (character.animation && character.kind !== "outfit") {
3657
3925
  const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
3658
3926
  if (!allowed.includes(character.animation.direction)) {
3659
3927
  throw new Error(
@@ -3755,10 +4023,48 @@ var PixelLabProvider = class _PixelLabProvider {
3755
4023
  concept: character.concept ? readPose(spec, "concept image", character.concept) : void 0,
3756
4024
  styleCharacterId: character.styleAnchor ? requireStyleObjectId(spec, context) : void 0,
3757
4025
  styleReferenceSize: styleImage ? { width: styleImage.width, height: styleImage.height } : void 0,
3758
- styleTraits: character.styleTraits
4026
+ styleTraits: character.styleTraits,
4027
+ sourceImageId: character.mode === "pro-flash" ? context?.referenceSourceImageId : void 0
3759
4028
  });
3760
4029
  return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
3761
4030
  }
4031
+ if (character.kind === "outfit") {
4032
+ const parentOutputs = context?.parentOutputs ?? [];
4033
+ if (parentOutputs.length < 2) {
4034
+ throw new Error(
4035
+ `${spec.styleId}/${spec.assetId} is an outfit of ${character.parentAssetId}, which has ${parentOutputs.length} downloaded frame(s); PixelLab transfer-outfit-v2 needs 2 to 16`
4036
+ );
4037
+ }
4038
+ const frames = parentOutputs.slice(0, 16).map((output) => {
4039
+ const bytes = readFileSync(output.path);
4040
+ const dims = imageMetadata(bytes);
4041
+ if (!dims) throw new Error(`${spec.styleId}/${spec.assetId}: outfit source frame is not a readable PNG: ${output.path}`);
4042
+ return { base64: bytes.toString("base64"), format: "png", width: dims.width, height: dims.height };
4043
+ });
4044
+ const outfit = character.outfit;
4045
+ const referenceBytes = readFileSync(outfit.reference.path);
4046
+ if (sha256(referenceBytes) !== outfit.reference.sha256) {
4047
+ throw new Error(`${spec.styleId}/${spec.assetId}: outfit reference changed after the manifest was resolved: ${outfit.reference.path}`);
4048
+ }
4049
+ const res2 = await this.client.transferOutfitV2({
4050
+ referenceImage: { base64: referenceBytes.toString("base64"), format: "png", width: outfit.reference.width, height: outfit.reference.height },
4051
+ frames,
4052
+ imageSize: { width: spec.width, height: spec.height },
4053
+ seed: spec.seed,
4054
+ noBackground: spec.noBackground,
4055
+ additionalInstructions: outfit.additionalInstructions
4056
+ });
4057
+ return {
4058
+ jobId: res2.background_job_id,
4059
+ metadata: {
4060
+ character: {
4061
+ kind: "outfit",
4062
+ backgroundJobId: res2.background_job_id,
4063
+ frameRoles: parentOutputs.slice(0, 16).map((output) => output.role ?? null)
4064
+ }
4065
+ }
4066
+ };
4067
+ }
3762
4068
  const parentId = context?.parentObjectId;
3763
4069
  if (!parentId) {
3764
4070
  throw new Error(
@@ -3777,6 +4083,24 @@ var PixelLabProvider = class _PixelLabProvider {
3777
4083
  });
3778
4084
  return { jobId: res2.character_id, metadata: { character: { kind: "state", characterId: res2.character_id, parentCharacterId: parentId, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
3779
4085
  }
4086
+ if (character.kind === "portrait") {
4087
+ const parentFile = character.parentFile;
4088
+ const bytes = readFileSync(parentFile);
4089
+ if (character.parentSha256 && sha256(bytes) !== character.parentSha256) {
4090
+ throw new Error(`${spec.styleId}/${spec.assetId}: portrait source changed after the manifest was resolved: ${parentFile}`);
4091
+ }
4092
+ const res2 = await this.client.createPortraitCharacterPro({
4093
+ direction: "character_to_portrait",
4094
+ image: { base64: bytes.toString("base64"), format: "png" },
4095
+ view: spec.view,
4096
+ resultSize: character.portrait.size,
4097
+ seed: spec.seed
4098
+ });
4099
+ return {
4100
+ jobId: res2.background_job_id,
4101
+ metadata: { character: { kind: "portrait", parentCharacterId: parentId, backgroundJobId: res2.background_job_id } }
4102
+ };
4103
+ }
3780
4104
  const animation = character.animation;
3781
4105
  const name = characterAnimationName(spec);
3782
4106
  const replaced = context?.replacedMetadata?.character ?? {};
@@ -3835,7 +4159,8 @@ var PixelLabProvider = class _PixelLabProvider {
3835
4159
  reference: object.reference ? readPose(spec, "reference sprite", object.reference) : void 0,
3836
4160
  styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
3837
4161
  styleReferenceSize: styleImage ? { width: styleImage.width, height: styleImage.height } : void 0,
3838
- styleTraits: object.styleTraits
4162
+ styleTraits: object.styleTraits,
4163
+ sourceImageId: context?.referenceSourceImageId
3839
4164
  });
3840
4165
  return { jobId: res2.object_id, metadata: { objectPro: { kind: "base", objectId: res2.object_id, directions: object.directions, backgroundJobId: res2.background_job_id } } };
3841
4166
  }
@@ -3890,6 +4215,9 @@ var PixelLabProvider = class _PixelLabProvider {
3890
4215
  async pollCharacter(jobId, context) {
3891
4216
  const animation = decodeAnimationJob(jobId);
3892
4217
  if (animation) return this.pollCharacterAnimation(animation, context);
4218
+ const kind = context?.metadata?.character?.kind ?? context?.spec?.character?.kind;
4219
+ if (kind === "portrait") return this.pollCharacterPortrait(jobId, context);
4220
+ if (kind === "outfit") return this.pollCharacterOutfit(jobId, context);
3893
4221
  const character = await this.client.getCharacter(jobId);
3894
4222
  if (character.status === "failed") return { status: "failed", error: "character generation failed upstream" };
3895
4223
  if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
@@ -3915,6 +4243,66 @@ var PixelLabProvider = class _PixelLabProvider {
3915
4243
  billed: await this.billedForJob(backgroundJobId)
3916
4244
  };
3917
4245
  }
4246
+ /**
4247
+ * `jobId` here is `portrait-character-pro`'s own background job id, not a
4248
+ * character id: the endpoint converts an arbitrary image and never touches
4249
+ * the character record. Once the portrait itself is drawn, this also
4250
+ * attaches it to the parent's character record with `SetPortrait` — a
4251
+ * separate, free call PixelLab does not make automatically (docs/PIXELLAB.md).
4252
+ */
4253
+ async pollCharacterPortrait(jobId, context) {
4254
+ const meta = context?.metadata?.character;
4255
+ try {
4256
+ const job = await this.client.getPortraitCharacterJob(jobId);
4257
+ if (job.status === "failed") return { status: "failed", error: "portrait generation failed upstream" };
4258
+ if (job.status !== "completed" || !job.download_url) return { status: "processing" };
4259
+ if (meta?.parentCharacterId) {
4260
+ const bytes = await this.client.download(job.download_url);
4261
+ await this.client.setCharacterPortrait(meta.parentCharacterId, { base64: bytes.toString("base64"), format: "png" });
4262
+ }
4263
+ return {
4264
+ status: "ready",
4265
+ objectId: jobId,
4266
+ sourceUrl: job.download_url,
4267
+ sources: [{ url: job.download_url }],
4268
+ billed: await this.billedForJob(meta?.backgroundJobId)
4269
+ };
4270
+ } catch (err) {
4271
+ if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
4272
+ throw err;
4273
+ }
4274
+ }
4275
+ /**
4276
+ * No dedicated status endpoint exists for `transfer-outfit-v2`, unlike
4277
+ * portraits and tiles: the background job is the only place to poll. A
4278
+ * completed job returns its frames inline as base64
4279
+ * (`last_response.quantized_images`), not as storage URLs, so they are
4280
+ * cached locally the way a synchronous pixflux result is, and matched back
4281
+ * to the source frames' own roles recorded at submit time.
4282
+ */
4283
+ async pollCharacterOutfit(jobId, context) {
4284
+ const meta = context?.metadata?.character;
4285
+ const job = await this.client.getBackgroundJob(jobId);
4286
+ if (job.status === "failed") return { status: "failed", error: "outfit transfer failed upstream" };
4287
+ if (job.status !== "completed") return { status: "processing" };
4288
+ const images = job.last_response?.quantized_images;
4289
+ if (!images?.length) return { status: "failed", error: "outfit transfer completed with no result frames" };
4290
+ const dir = _PixelLabProvider.cacheDir("outfit");
4291
+ const sources = images.map((image, index) => {
4292
+ if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
4293
+ const role = meta?.frameRoles?.[index] ?? void 0;
4294
+ const file = path.join(dir, `${jobId}-${index}.png`);
4295
+ writeFileSync(file, Buffer.from(image.base64, "base64"));
4296
+ return { url: `file://${file}`, ...role ? { role } : {} };
4297
+ });
4298
+ return {
4299
+ status: "ready",
4300
+ objectId: jobId,
4301
+ sourceUrl: sources[0].url,
4302
+ sources,
4303
+ billed: await this.billedForJob(meta?.backgroundJobId)
4304
+ };
4305
+ }
3918
4306
  /**
3919
4307
  * The background job is the authoritative record of one direction: it
3920
4308
  * completes with the frame URLs, the animation id, and the direction, and
@@ -4071,6 +4459,19 @@ var PixelLabProvider = class _PixelLabProvider {
4071
4459
  });
4072
4460
  return { jobId: res2.ui_asset_id, metadata: { backgroundJobId: res2.background_job_id } };
4073
4461
  }
4462
+ if (spec.generator === "imageProFlash") {
4463
+ const style = styleImages[0];
4464
+ const res2 = await this.client.createImageProFlash({
4465
+ description: spec.prompt,
4466
+ width: spec.width,
4467
+ height: spec.height,
4468
+ noBackground: spec.noBackground,
4469
+ seed: spec.seed,
4470
+ styleImage: style ? { image: { base64: style.base64, format: style.format }, width: style.width, height: style.height } : void 0,
4471
+ styleTraits: spec.styleTraits
4472
+ });
4473
+ return { jobId: res2.background_job_id, metadata: { imageProFlash: proFlashRecord(res2) } };
4474
+ }
4074
4475
  if (spec.generator === "uiElement") {
4075
4476
  const concept = styleImages[0];
4076
4477
  const res2 = await this.client.generateUiV2({
@@ -4224,6 +4625,32 @@ var PixelLabProvider = class _PixelLabProvider {
4224
4625
  const width = revision.sourceWidth;
4225
4626
  const height = revision.sourceHeight;
4226
4627
  const roles = revision.sourceMembers?.map((member) => member.role);
4628
+ if (revision.engine === "pro-flash") {
4629
+ if (revision.mode === "inpaint") {
4630
+ if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
4631
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
4632
+ }
4633
+ const maskBytes = readFileSync(revision.maskFile);
4634
+ if (sha256(maskBytes) !== revision.maskSha256) {
4635
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
4636
+ }
4637
+ const res3 = await this.client.inpaintImageProFlash({
4638
+ image,
4639
+ maskImage: { base64: maskBytes.toString("base64"), format: revision.maskFormat },
4640
+ description: spec.prompt,
4641
+ noBackground: spec.noBackground,
4642
+ seed: spec.seed
4643
+ });
4644
+ return { jobId: res3.background_job_id, metadata: { imageProFlash: proFlashRecord(res3) } };
4645
+ }
4646
+ const res2 = await this.client.editImageProFlash({
4647
+ image,
4648
+ description: spec.prompt,
4649
+ noBackground: spec.noBackground,
4650
+ seed: spec.seed
4651
+ });
4652
+ return { jobId: res2.background_job_id, metadata: { imageProFlash: proFlashRecord(res2) } };
4653
+ }
4227
4654
  if (revision.mode === "inpaint") {
4228
4655
  if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
4229
4656
  throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
@@ -4388,6 +4815,7 @@ var PixelLabProvider = class _PixelLabProvider {
4388
4815
  return this.pollAnimateRevision(jobId, context);
4389
4816
  }
4390
4817
  if (context?.spec?.revision) return this.pollRevision(jobId);
4818
+ if (generator === "imageProFlash") return this.pollRevision(jobId);
4391
4819
  if (generator === "pixflux") {
4392
4820
  const file = path.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
4393
4821
  if (existsSync(file)) {
@@ -4968,6 +5396,13 @@ function readPose(spec, what, image) {
4968
5396
  }
4969
5397
  return { base64: bytes.toString("base64"), format: image.format };
4970
5398
  }
5399
+ function proFlashRecord(res) {
5400
+ return {
5401
+ imageId: res.image_id,
5402
+ sourceImageId: res.source_image_id,
5403
+ ...res.estimated_generations != null ? { estimatedGenerations: res.estimated_generations } : {}
5404
+ };
5405
+ }
4971
5406
  function editAnimationFrameCeiling(width, height) {
4972
5407
  const side = Math.max(width, height);
4973
5408
  if (side <= 64) return 16;
@@ -6171,6 +6606,11 @@ var ComfyUIProvider = class _ComfyUIProvider {
6171
6606
  throw new Error("ComfyUI providerOptions.frames requires generator: frames");
6172
6607
  }
6173
6608
  if (spec.revision) {
6609
+ if (spec.revision.engine) {
6610
+ throw new Error(
6611
+ `ComfyUI has no "${spec.revision.engine}" revision engine; that selects PixelLab's Pro Flash edit/inpaint endpoints. Drop engine to run the revision through this style's own workflow.`
6612
+ );
6613
+ }
6174
6614
  if (spec.revision.sourceMembers) {
6175
6615
  throw new Error(
6176
6616
  `ComfyUI revisions read one source image; ${spec.revision.sourceAssetId} is a ${spec.revision.sourceMembers.length}-member set (directions or frames)`
@@ -7513,7 +7953,7 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
7513
7953
  return { manifest: parsed.data, root: path5.dirname(abs), path: abs, ...loopFamilies };
7514
7954
  }
7515
7955
  function parentAssetId(asset) {
7516
- return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
7956
+ return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
7517
7957
  }
7518
7958
  function formatManifestIssues(issues) {
7519
7959
  return issues.flatMap((issue) => {
@@ -7680,9 +8120,9 @@ async function resolveSpecs(loaded, filter) {
7680
8120
  height = styleImageDimensions.length ? Math.max(...styleImageDimensions.map((img) => img.height)) : style.tileSize ?? 32;
7681
8121
  size = Math.max(width, height);
7682
8122
  } else if (generator === "character") {
7683
- size = asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
7684
- width = asset.state?.canvas?.width ?? size;
7685
- height = asset.state?.canvas?.height ?? size;
8123
+ size = asset.portrait ? asset.portrait.size : asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
8124
+ width = asset.portrait?.size ?? asset.state?.canvas?.width ?? size;
8125
+ height = asset.portrait?.size ?? asset.state?.canvas?.height ?? size;
7686
8126
  } else if (generator === "terrain") {
7687
8127
  size = style.terrainTileSize ?? 16;
7688
8128
  width = size;
@@ -7709,12 +8149,17 @@ async function resolveSpecs(loaded, filter) {
7709
8149
  `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
7710
8150
  );
7711
8151
  }
8152
+ if ((asset.portrait || asset.outfit) && generator !== "character") {
8153
+ throw new Error(
8154
+ `assets.${assetId}: ${asset.portrait ? "portrait" : "outfit"} needs a character style; "${styleId}" generates ${generator}`
8155
+ );
8156
+ }
7712
8157
  if ((asset.pieces || asset.elements) && generator !== "uiAsset") {
7713
8158
  throw new Error(
7714
8159
  `assets.${assetId}: ${asset.pieces ? "pieces" : "elements"} needs a uiAsset style; "${styleId}" generates ${generator}`
7715
8160
  );
7716
8161
  }
7717
- const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
8162
+ const characterKind = asset.animation ? "animation" : asset.state ? "state" : asset.portrait ? "portrait" : "base";
7718
8163
  const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
7719
8164
  let terrainLowerDescription;
7720
8165
  let terrainUpperDescription;
@@ -7763,6 +8208,7 @@ async function resolveSpecs(loaded, filter) {
7763
8208
  uiPieces: generator === "uiAsset" ? asset.pieces : void 0,
7764
8209
  uiElements: generator === "uiAsset" ? asset.elements : void 0,
7765
8210
  uiColorPalette: generator === "uiAsset" || generator === "uiElement" ? style.uiColorPalette : void 0,
8211
+ ...generator === "imageProFlash" && style.styleTraits ? { styleTraits: style.styleTraits } : {},
7766
8212
  palette: style.palette,
7767
8213
  enforcePalette: style.enforcePalette,
7768
8214
  noBackground: style.noBackground,
@@ -7806,7 +8252,9 @@ async function resolveSpecs(loaded, filter) {
7806
8252
  // its parent's pixels, not a new character: giving it a base shape
7807
8253
  // would have packing, the gallery, and adoption treat it as one.
7808
8254
  ...generator === "character" && !asset.revision ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
7809
- ...generator === "objectPro" && !asset.revision ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
8255
+ // A portrait needs a character style (refused above), so an
8256
+ // objectPro asset is only ever a base, state, or animation.
8257
+ ...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
7810
8258
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
7811
8259
  costUnit: "generations",
7812
8260
  candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
@@ -7917,7 +8365,7 @@ async function resolveSpecs(loaded, filter) {
7917
8365
  styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: existsSync4(file) ? await sha256File(file) : null }
7918
8366
  };
7919
8367
  }
7920
- const characterParent = asset.state?.of ?? asset.animation?.of;
8368
+ const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
7921
8369
  if (resolved.character && characterParent) {
7922
8370
  const parentSpec = await finalize(characterParent);
7923
8371
  const parentDirections = parentSpec.character?.directions ?? 8;
@@ -7950,6 +8398,38 @@ async function resolveSpecs(loaded, filter) {
7950
8398
  directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
7951
8399
  };
7952
8400
  }
8401
+ if (asset.outfit) {
8402
+ if (asset.outfit.of === assetId) throw new Error(`assets.${assetId}: an asset cannot re-clothe itself`);
8403
+ const sourceSpec = await finalize(asset.outfit.of);
8404
+ if (sourceSpec.character?.kind !== "animation") {
8405
+ throw new Error(`assets.${assetId}: outfit "${asset.outfit.of}" is not a character loop`);
8406
+ }
8407
+ const referenceImage = await loadStyleImage(asset.outfit.reference, "Outfit reference image");
8408
+ resolved.character = {
8409
+ kind: "outfit",
8410
+ mode: sourceSpec.character.mode,
8411
+ directions: sourceSpec.character.directions,
8412
+ template: sourceSpec.character.template,
8413
+ parentAssetId: asset.outfit.of,
8414
+ parentSpec: sourceSpec,
8415
+ animation: sourceSpec.character.animation,
8416
+ outfit: {
8417
+ reference: {
8418
+ path: path5.resolve(root, asset.outfit.reference),
8419
+ sha256: referenceImage.hash,
8420
+ width: referenceImage.width,
8421
+ height: referenceImage.height,
8422
+ format: referenceImage.format
8423
+ },
8424
+ ...asset.outfit.additionalInstructions ? { additionalInstructions: asset.outfit.additionalInstructions } : {}
8425
+ }
8426
+ };
8427
+ resolved.generator = sourceSpec.generator;
8428
+ resolved.width = sourceSpec.width;
8429
+ resolved.height = sourceSpec.height;
8430
+ resolved.size = sourceSpec.size;
8431
+ resolved.prompt = "";
8432
+ }
7953
8433
  if (asset.revision) {
7954
8434
  if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
7955
8435
  throw new Error(
@@ -8012,7 +8492,8 @@ async function resolveSpecs(loaded, filter) {
8012
8492
  lastFrameFormat: lastFrame?.format ?? null
8013
8493
  } : {},
8014
8494
  ...asset.revision.direction ? { direction: asset.revision.direction } : {},
8015
- ...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt }
8495
+ ...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt },
8496
+ ...asset.revision.engine ? { engine: asset.revision.engine } : {}
8016
8497
  };
8017
8498
  }
8018
8499
  if (asset.batch) {
@@ -8223,6 +8704,9 @@ async function resolveCharacterShape(asset, style, kind, files) {
8223
8704
  ...asset.state.canvas ? { canvas: asset.state.canvas } : {}
8224
8705
  };
8225
8706
  }
8707
+ if (asset.portrait) {
8708
+ shape.portrait = { size: asset.portrait.size };
8709
+ }
8226
8710
  if (asset.animation) {
8227
8711
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
8228
8712
  const pose = async (rel, what) => {
@@ -9775,6 +10259,7 @@ import { existsSync as existsSync7 } from "fs";
9775
10259
  import path11 from "path";
9776
10260
  async function inspectRevisionReadiness(spec, lock) {
9777
10261
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
10262
+ if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
9778
10263
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
9779
10264
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
9780
10265
  if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
@@ -9801,6 +10286,20 @@ async function inspectMirror(spec, lock, seen) {
9801
10286
  const dependency = await inspectParent(source, lock, seen);
9802
10287
  return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
9803
10288
  }
10289
+ async function inspectOutfit(spec, lock, seen) {
10290
+ const character = spec.character;
10291
+ const outfit = character.outfit;
10292
+ const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
10293
+ const dependency = await inspectParent(character.parentSpec, lock, seen);
10294
+ if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
10295
+ if (!existsSync7(outfit.reference.path)) {
10296
+ return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
10297
+ }
10298
+ if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
10299
+ return { ready: false, reason: "outfit reference changed after the manifest was resolved" };
10300
+ }
10301
+ return { ready: true, reason: `${label} is current` };
10302
+ }
9804
10303
  async function inspectCharacterParent(spec, lock, seen) {
9805
10304
  const character = spec.character;
9806
10305
  const parentSpec = character.parentSpec;
@@ -9916,7 +10415,10 @@ async function inspectParent(spec, lock, seen) {
9916
10415
  const inputs = await inspectRevision(spec, lock, nextSeen);
9917
10416
  if (!inputs.ready) return inputs;
9918
10417
  }
9919
- if (spec.character?.parentSpec) {
10418
+ if (spec.character?.kind === "outfit") {
10419
+ const inputs = await inspectOutfit(spec, lock, nextSeen);
10420
+ if (!inputs.ready) return inputs;
10421
+ } else if (spec.character?.parentSpec) {
9920
10422
  const inputs = await inspectCharacterParent(spec, lock, nextSeen);
9921
10423
  if (!inputs.ready) return inputs;
9922
10424
  }
@@ -10039,6 +10541,9 @@ async function buildPlan(specs, lock, opts = {}) {
10039
10541
  } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !existsSync8(currentEntryOutputPath(entry, spec, index)))) {
10040
10542
  state = "stale";
10041
10543
  reason = "mirror output missing on disk; flip it again (no generation cost)";
10544
+ } else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
10545
+ state = "stale";
10546
+ reason = `outfit source ${spec.character.parentAssetId} changed; re-clothe the loop again`;
10042
10547
  } else if (entry.status === "download-failed") {
10043
10548
  state = "recoverable";
10044
10549
  const force = entry.error?.includes("pass --force") ? " --force" : "";
@@ -12736,6 +13241,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12736
13241
  const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
12737
13242
  const submittedAt = resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString();
12738
13243
  const history = historyAfterReplacing(previousEntry, historyLimit(loaded.manifest), submittedAt ?? (/* @__PURE__ */ new Date()).toISOString());
13244
+ const outfitParentEntry = spec.character?.kind === "outfit" && spec.character.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
12739
13245
  upsert(lock, key, {
12740
13246
  styleId: spec.styleId,
12741
13247
  assetId: spec.assetId,
@@ -12759,7 +13265,13 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12759
13265
  ...spec.revision.fps == null ? {} : { fps: spec.revision.fps },
12760
13266
  ...spec.revision.lastFrameSha256 ? { lastFrameSha256: spec.revision.lastFrameSha256 } : {},
12761
13267
  ...spec.revision.direction ? { direction: spec.revision.direction } : {},
12762
- ...spec.revision.enhancePrompt == null ? {} : { enhancePrompt: spec.revision.enhancePrompt }
13268
+ ...spec.revision.enhancePrompt == null ? {} : { enhancePrompt: spec.revision.enhancePrompt },
13269
+ ...spec.revision.engine ? { engine: spec.revision.engine } : {}
13270
+ } : null,
13271
+ outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
13272
+ sourceAssetId: spec.character.parentAssetId,
13273
+ sourceSha256: mirrorSourceHash(outfitParentEntry),
13274
+ referenceSha256: spec.character.outfit.reference.sha256
12763
13275
  } : null,
12764
13276
  status: "pending",
12765
13277
  jobId: previousJobId ?? null,
@@ -12794,10 +13306,17 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12794
13306
  const anchor = spec.character?.styleAnchor;
12795
13307
  const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
12796
13308
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
13309
+ const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
13310
+ path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
13311
+ role: output.role
13312
+ })) : void 0;
13313
+ const referenceSourceImageId = ownedReferenceImageId(spec, lock, provider.id);
12797
13314
  const { jobId, metadata } = await provider.submit(spec, refs, {
12798
13315
  ...previousJobId ? { previousJobId } : {},
13316
+ ...referenceSourceImageId ? { referenceSourceImageId } : {},
12799
13317
  ...parentEntry?.objectId ? { parentObjectId: parentEntry.objectId } : {},
12800
13318
  ...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
13319
+ ...parentOutputs ? { parentOutputs } : {},
12801
13320
  ...replacedMetadata ? { replacedMetadata } : {},
12802
13321
  ...previousMetadata ? { previousMetadata } : {},
12803
13322
  checkpoint: async (checkpoint) => {
@@ -12861,6 +13380,17 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12861
13380
  }
12862
13381
  return { submitted, failed, spent, unit };
12863
13382
  }
13383
+ function ownedReferenceImageId(spec, lock, providerId) {
13384
+ const sha2562 = spec.character?.kind === "base" && spec.character.mode === "pro-flash" ? spec.character.reference?.south?.sha256 : spec.objectPro?.kind === "base" ? spec.objectPro.reference?.sha256 : void 0;
13385
+ if (!sha2562) return void 0;
13386
+ for (const entry of Object.values(lock.entries)) {
13387
+ if (entry.provider !== providerId || entry.generator !== "imageProFlash" || entry.status !== "downloaded") continue;
13388
+ if (entry.outputs.length !== 1 || entry.outputs[0].sha256 !== sha2562) continue;
13389
+ const recorded = entry.providerMetadata?.[providerId]?.imageProFlash;
13390
+ if (typeof recorded?.sourceImageId === "string" && recorded.sourceImageId) return recorded.sourceImageId;
13391
+ }
13392
+ return void 0;
13393
+ }
12864
13394
 
12865
13395
  // src/pipeline/poll.ts
12866
13396
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -14916,7 +15446,7 @@ function describeCharacter(spec, entry) {
14916
15446
  };
14917
15447
  }
14918
15448
  if (entry?.generator === "character") {
14919
- const kind = recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
15449
+ const kind = recordedCharacter?.kind === "portrait" ? "portrait" : recordedCharacter?.kind === "outfit" ? "outfit" : recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
14920
15450
  return {
14921
15451
  generator: "character",
14922
15452
  kind,
@@ -15277,7 +15807,9 @@ async function buildGallerySnapshot(opts) {
15277
15807
  characters: style?.generator === "character" || style?.generator === "objectPro" ? {
15278
15808
  bases: styleItems.filter((item) => item.character?.kind === "base").length,
15279
15809
  states: styleItems.filter((item) => item.character?.kind === "state").length,
15280
- animations: styleItems.filter((item) => item.character?.kind === "animation").length
15810
+ animations: styleItems.filter((item) => item.character?.kind === "animation").length,
15811
+ portraits: styleItems.filter((item) => item.character?.kind === "portrait").length,
15812
+ outfits: styleItems.filter((item) => item.character?.kind === "outfit").length
15281
15813
  } : null,
15282
15814
  tags: style?.tags ?? [],
15283
15815
  extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
@@ -17071,6 +17603,9 @@ export {
17071
17603
  CharacterAnimationSchema,
17072
17604
  CharacterDirectionSchema,
17073
17605
  CharacterModeSchema,
17606
+ CharacterOutfitSchema,
17607
+ CharacterPortraitSchema,
17608
+ CharacterPortraitSizeSchema,
17074
17609
  CharacterProportionsSchema,
17075
17610
  CharacterStateSchema,
17076
17611
  ComfyUIProvider,