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.cjs CHANGED
@@ -40,6 +40,9 @@ __export(index_exports, {
40
40
  CharacterAnimationSchema: () => CharacterAnimationSchema,
41
41
  CharacterDirectionSchema: () => CharacterDirectionSchema,
42
42
  CharacterModeSchema: () => CharacterModeSchema,
43
+ CharacterOutfitSchema: () => CharacterOutfitSchema,
44
+ CharacterPortraitSchema: () => CharacterPortraitSchema,
45
+ CharacterPortraitSizeSchema: () => CharacterPortraitSizeSchema,
43
46
  CharacterProportionsSchema: () => CharacterProportionsSchema,
44
47
  CharacterStateSchema: () => CharacterStateSchema,
45
48
  ComfyUIProvider: () => ComfyUIProvider,
@@ -319,7 +322,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
319
322
  // src/types.ts
320
323
  var import_zod = require("zod");
321
324
  var MediaTypeSchema = import_zod.z.enum(["image/png", "image/gif"]);
322
- var GeneratorSchema = import_zod.z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement"]);
325
+ var GeneratorSchema = import_zod.z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement", "imageProFlash"]);
323
326
  var GridConfidenceSchema = import_zod.z.enum(["low", "medium", "high"]);
324
327
  var RevisionModeSchema = import_zod.z.enum([
325
328
  "image-to-image",
@@ -504,6 +507,27 @@ var CharacterStateSchema = import_zod.z.object({
504
507
  height: import_zod.z.number().int().min(16).max(256)
505
508
  }).strict().optional()
506
509
  }).strict();
510
+ var CharacterPortraitSizeSchema = import_zod.z.union([
511
+ import_zod.z.literal(16),
512
+ import_zod.z.literal(32),
513
+ import_zod.z.literal(48),
514
+ import_zod.z.literal(64),
515
+ import_zod.z.literal(128),
516
+ import_zod.z.literal(160)
517
+ ]);
518
+ var CharacterPortraitSchema = import_zod.z.object({
519
+ /** The base or state to portray, in the same style. */
520
+ of: import_zod.z.string().min(1),
521
+ size: CharacterPortraitSizeSchema.default(64)
522
+ }).strict();
523
+ var CharacterOutfitSchema = import_zod.z.object({
524
+ /** The loop (a character animation asset) to re-clothe, in the same style. */
525
+ of: import_zod.z.string().min(1),
526
+ /** Manifest-relative image of the outfit to transfer. */
527
+ reference: import_zod.z.string().min(1),
528
+ /** Extra guidance PixelLab passes through as written, e.g. view/direction hints. */
529
+ additionalInstructions: import_zod.z.string().max(2e3).optional()
530
+ }).strict();
507
531
  var CharacterAnimationSchema = import_zod.z.object({
508
532
  /** The character to animate: a base or a state in the same style. */
509
533
  of: import_zod.z.string().min(1),
@@ -625,8 +649,22 @@ var RevisionSchema = import_zod.z.object({
625
649
  /** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
626
650
  direction: CharacterDirectionSchema.optional(),
627
651
  /** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
628
- enhancePrompt: import_zod.z.boolean().optional()
652
+ enhancePrompt: import_zod.z.boolean().optional(),
653
+ /**
654
+ * `image-to-image`/`inpaint` only: which provider engine draws the
655
+ * edit. `pro-flash` selects PixelLab's Pro Flash edit and inpaint tier
656
+ * (`/edit-image-pro-flash`, `/inpaint-image-pro-flash`) instead of the
657
+ * default Pro endpoints; omit it for the default.
658
+ */
659
+ engine: import_zod.z.enum(["pro-flash"]).optional()
629
660
  }).strict().superRefine((revision, context) => {
661
+ if (revision.engine !== void 0 && revision.mode !== "image-to-image" && revision.mode !== "inpaint") {
662
+ context.addIssue({
663
+ code: import_zod.z.ZodIssueCode.custom,
664
+ message: "engine applies to image-to-image and inpaint revisions only",
665
+ path: ["engine"]
666
+ });
667
+ }
630
668
  if (revision.mode === "inpaint" && !revision.mask) {
631
669
  context.addIssue({
632
670
  code: import_zod.z.ZodIssueCode.custom,
@@ -915,8 +953,8 @@ var StyleObjectSchema = import_zod.z.object({
915
953
  /** `character` only. Body template: `mannequin` (default), a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`), or `custom` for `pro-flash`. */
916
954
  template: import_zod.z.string().min(1).optional(),
917
955
  /**
918
- * `character` only, `pro-flash` bases with a style image: which traits
919
- * the style image lends. Each defaults to true.
956
+ * `character` `pro-flash` bases and `imageProFlash` styles, with a
957
+ * style image: which traits the style image lends. Each defaults to true.
920
958
  */
921
959
  styleTraits: import_zod.z.object({
922
960
  palette: import_zod.z.boolean().optional(),
@@ -1125,6 +1163,10 @@ var AssetSchema = import_zod.z.object({
1125
1163
  pieces: import_zod.z.array(UiPieceSchema).min(1).optional(),
1126
1164
  /** `uiAsset` styles: named, auto-positioned UI element scaffolds. Combine with `pieces`; omit both for a default full-canvas panel. */
1127
1165
  elements: import_zod.z.array(UiElementSchema).min(1).optional(),
1166
+ /** `character` styles: this asset is a bust portrait of another character asset's south sprite. */
1167
+ portrait: CharacterPortraitSchema.optional(),
1168
+ /** `character` styles: this asset re-clothes an existing loop's frames with a reference outfit. */
1169
+ outfit: CharacterOutfitSchema.optional(),
1128
1170
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1129
1171
  proportions: CharacterProportionsSchema.optional(),
1130
1172
  /**
@@ -1201,7 +1243,7 @@ var AssetSchema = import_zod.z.object({
1201
1243
  path: ["revision"]
1202
1244
  });
1203
1245
  }
1204
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
1246
+ 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);
1205
1247
  if (shapes.length > 1) {
1206
1248
  context.addIssue({
1207
1249
  code: import_zod.z.ZodIssueCode.custom,
@@ -1216,24 +1258,24 @@ var AssetSchema = import_zod.z.object({
1216
1258
  path: ["mirror"]
1217
1259
  });
1218
1260
  }
1219
- if (asset.prompt === void 0 && !asset.mirror) {
1261
+ if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit) {
1220
1262
  context.addIssue({
1221
1263
  code: import_zod.z.ZodIssueCode.custom,
1222
1264
  message: "Required",
1223
1265
  path: ["prompt"]
1224
1266
  });
1225
1267
  }
1226
- if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
1268
+ if (asset.reference && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
1227
1269
  context.addIssue({
1228
1270
  code: import_zod.z.ZodIssueCode.custom,
1229
- message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
1271
+ message: "a reference sprite belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
1230
1272
  path: ["reference"]
1231
1273
  });
1232
1274
  }
1233
- if (asset.concept && (asset.state || asset.animation || asset.mirror)) {
1275
+ if (asset.concept && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
1234
1276
  context.addIssue({
1235
1277
  code: import_zod.z.ZodIssueCode.custom,
1236
- message: "a concept image belongs on a base; a state, animation, or mirror takes its look from its parent",
1278
+ message: "a concept image belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
1237
1279
  path: ["concept"]
1238
1280
  });
1239
1281
  }
@@ -1345,7 +1387,8 @@ var LockEntrySchema = import_zod.z.object({
1345
1387
  fps: import_zod.z.number().int().min(1).max(60).optional(),
1346
1388
  lastFrameSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/).optional(),
1347
1389
  direction: CharacterDirectionSchema.optional(),
1348
- enhancePrompt: import_zod.z.boolean().optional()
1390
+ enhancePrompt: import_zod.z.boolean().optional(),
1391
+ engine: import_zod.z.enum(["pro-flash"]).optional()
1349
1392
  }).strict().nullable().default(null),
1350
1393
  /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
1351
1394
  mirror: import_zod.z.object({
@@ -1358,6 +1401,17 @@ var LockEntrySchema = import_zod.z.object({
1358
1401
  leaderAssetId: import_zod.z.string().min(1).optional(),
1359
1402
  index: import_zod.z.number().int().min(1).optional()
1360
1403
  }).strict().nullable().default(null),
1404
+ /**
1405
+ * For an outfit: the loop it re-clothed, a hash over that loop's output
1406
+ * hashes when this was made, and the reference image's own hash. Tracked
1407
+ * the way `mirror` is, not through `specHash`: manifest resolution has no
1408
+ * lock to hash the parent's actual current frame bytes against.
1409
+ */
1410
+ outfit: import_zod.z.object({
1411
+ sourceAssetId: import_zod.z.string().min(1),
1412
+ sourceSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/),
1413
+ referenceSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/)
1414
+ }).strict().nullable().default(null),
1361
1415
  /**
1362
1416
  * Output hashes owned by the previous generation while its replacement is
1363
1417
  * pending. They authorize replacing only unchanged PixelKiln-owned files.
@@ -1711,6 +1765,14 @@ var CorrectPixelartResponseSchema = import_zod2.z.object({
1711
1765
  images: import_zod2.z.array(import_zod2.z.object({ base64: import_zod2.z.string().min(1) }).passthrough()).min(1),
1712
1766
  usage: import_zod2.z.unknown().optional()
1713
1767
  }).passthrough();
1768
+ var ProFlashImageSubmitSchema = import_zod2.z.object({
1769
+ background_job_id: import_zod2.z.string().min(1),
1770
+ image_id: import_zod2.z.string().min(1),
1771
+ source_image_id: import_zod2.z.string().min(1),
1772
+ status: import_zod2.z.string().default("processing"),
1773
+ estimated_generations: import_zod2.z.number().nullable().optional(),
1774
+ usage: import_zod2.z.unknown().optional()
1775
+ }).passthrough();
1714
1776
  var UnzoomResponseSchema = import_zod2.z.object({
1715
1777
  image: import_zod2.z.object({ base64: import_zod2.z.string().min(1) }).passthrough(),
1716
1778
  original_size: import_zod2.z.object({ width: import_zod2.z.number().int(), height: import_zod2.z.number().int() }).passthrough(),
@@ -1761,6 +1823,28 @@ var AnimateObjectSubmitSchema = import_zod2.z.object({
1761
1823
  submissions: import_zod2.z.array(DirectionSubmissionSchema),
1762
1824
  usage: UsageSchema
1763
1825
  }).passthrough();
1826
+ var PortraitSubmitSchema = import_zod2.z.object({
1827
+ background_job_id: import_zod2.z.string().min(1),
1828
+ status: import_zod2.z.string().default("processing"),
1829
+ usage: UsageSchema
1830
+ }).passthrough();
1831
+ var PortraitJobSchema = import_zod2.z.object({
1832
+ status: import_zod2.z.string(),
1833
+ download_url: import_zod2.z.string().nullable().optional(),
1834
+ width: import_zod2.z.number().int().nullable().optional(),
1835
+ height: import_zod2.z.number().int().nullable().optional()
1836
+ }).passthrough();
1837
+ var SetPortraitSchema = import_zod2.z.object({
1838
+ character_id: import_zod2.z.string().min(1),
1839
+ size: import_zod2.z.number().int().positive(),
1840
+ url: import_zod2.z.string().min(1),
1841
+ usage: UsageSchema
1842
+ }).passthrough();
1843
+ var TransferOutfitSubmitSchema = import_zod2.z.object({
1844
+ background_job_id: import_zod2.z.string().min(1),
1845
+ status: import_zod2.z.string().default("processing"),
1846
+ usage: UsageSchema
1847
+ }).passthrough();
1764
1848
  var CharacterAnimationDirectionSchema = import_zod2.z.object({
1765
1849
  direction: import_zod2.z.string(),
1766
1850
  frame_count: import_zod2.z.number().int().min(0),
@@ -2129,7 +2213,7 @@ var PixelLabClient = class {
2129
2213
  const body = {
2130
2214
  description: args.description,
2131
2215
  n_directions: args.directions,
2132
- ...args.reference ? { first_frame: encode(args.reference) } : { image_size: { width: args.size, height: args.size } },
2216
+ ...args.sourceImageId ? { source_image_id: args.sourceImageId } : args.reference ? { first_frame: encode(args.reference) } : { image_size: { width: args.size, height: args.size } },
2133
2217
  ...args.view ? { view: args.view } : {},
2134
2218
  ...args.seed != null ? { seed: args.seed } : {},
2135
2219
  ...args.styleReference && args.styleReferenceSize ? {
@@ -2276,8 +2360,9 @@ var PixelLabClient = class {
2276
2360
  description: args.description,
2277
2361
  template_id: args.template,
2278
2362
  n_directions: 8,
2279
- // The author's sprite sets the canvas; a size only applies to text creation.
2280
- ...south ? { first_frame: encode(south) } : { image_size: imageSize },
2363
+ // The author's sprite sets the canvas; a size only applies to text
2364
+ // creation. An owned copy of the same sprite is referenced by id.
2365
+ ...args.sourceImageId ? { source_image_id: args.sourceImageId } : south ? { first_frame: encode(south) } : { image_size: imageSize },
2281
2366
  ...args.view ? { view: args.view } : {},
2282
2367
  ...args.seed != null ? { seed: args.seed } : {},
2283
2368
  ...args.styleReference && args.styleReferenceSize ? {
@@ -2376,6 +2461,69 @@ var PixelLabClient = class {
2376
2461
  });
2377
2462
  return validateResponse(AnimateSubmitSchema, raw, "animate-character");
2378
2463
  }
2464
+ /**
2465
+ * A full-body sprite in, a bust portrait out (or the reverse, unused
2466
+ * today). Job-based like a state or `1dir`, not the couple-generations
2467
+ * "conversion" the name suggests: the smallest `resultSize` (16px) billed
2468
+ * 20 generations live (docs/ENDPOINTS.md, "Characters, measured").
2469
+ */
2470
+ async createPortraitCharacterPro(args) {
2471
+ const raw = await this.request("/portrait-character-pro", {
2472
+ method: "POST",
2473
+ body: JSON.stringify({
2474
+ direction: args.direction,
2475
+ image: { type: "base64", base64: args.image.base64, format: args.image.format },
2476
+ ...args.view ? { view: args.view } : {},
2477
+ ...args.resultSize != null ? { result_size: args.resultSize } : {},
2478
+ ...args.seed != null ? { seed: args.seed } : {}
2479
+ })
2480
+ });
2481
+ return validateResponse(PortraitSubmitSchema, raw, "portrait-character-pro");
2482
+ }
2483
+ /** Throws PixelLabError(423) while the job is still drawing, like tiles and map objects. */
2484
+ async getPortraitCharacterJob(jobId) {
2485
+ const raw = await this.request(`/portrait-character-pro/${encodeURIComponent(jobId)}`);
2486
+ return validateResponse(PortraitJobSchema, raw, "portrait-character-pro/{id}");
2487
+ }
2488
+ /**
2489
+ * Attaches a portrait image to a character record. Synchronous and free
2490
+ * (0 generations, live); PixelLab does not link this to
2491
+ * `createPortraitCharacterPro` automatically, and `GET /characters/{id}`
2492
+ * never grows a portrait field, so this response's `url` is the only place
2493
+ * it appears.
2494
+ */
2495
+ async setCharacterPortrait(characterId, image) {
2496
+ const raw = await this.request(`/characters/${encodeURIComponent(characterId)}/portrait`, {
2497
+ method: "POST",
2498
+ body: JSON.stringify({ image: { type: "base64", base64: image.base64, format: image.format } })
2499
+ });
2500
+ return validateResponse(SetPortraitSchema, raw, "characters/{id}/portrait");
2501
+ }
2502
+ /**
2503
+ * Applies a reference outfit to 2–16 existing frames. Job-based like
2504
+ * everything else here, but the completed job returns the result frames
2505
+ * inline as base64 (`last_response.quantized_images`), not as storage
2506
+ * URLs — the one PixelLab character tool that works this way. A live
2507
+ * 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md).
2508
+ */
2509
+ async transferOutfitV2(args) {
2510
+ const image = (img) => ({
2511
+ image: { type: "base64", base64: img.base64, format: img.format },
2512
+ size: { width: img.width, height: img.height }
2513
+ });
2514
+ const raw = await this.request("/transfer-outfit-v2", {
2515
+ method: "POST",
2516
+ body: JSON.stringify({
2517
+ reference_image: image(args.referenceImage),
2518
+ frames: args.frames.map(image),
2519
+ image_size: args.imageSize,
2520
+ ...args.seed != null ? { seed: args.seed } : {},
2521
+ ...args.noBackground !== void 0 ? { no_background: args.noBackground } : {},
2522
+ ...args.additionalInstructions ? { additional_instructions: args.additionalInstructions } : {}
2523
+ })
2524
+ });
2525
+ return validateResponse(TransferOutfitSubmitSchema, raw, "transfer-outfit-v2");
2526
+ }
2379
2527
  async getCharacter(characterId) {
2380
2528
  const raw = await this.request(`/characters/${encodeURIComponent(characterId)}`);
2381
2529
  return validateResponse(CharacterSchema, raw, "characters/{id}");
@@ -2486,6 +2634,66 @@ var PixelLabClient = class {
2486
2634
  "generate-image-v2"
2487
2635
  );
2488
2636
  }
2637
+ /**
2638
+ * `/create-image-pro-flash`: one native pixel-art image from a
2639
+ * description, optionally styled after a style image with per-trait
2640
+ * toggles, on the same Pro Flash model `character`/`objectPro` use for
2641
+ * their `pro-flash` engine. Sizes run 16 to 256 per side in multiples of 4.
2642
+ * A plain background job; `pollRevision`'s generic reader takes the
2643
+ * finished image. The response's `source_image_id` is what a Pro Flash
2644
+ * character or object can reuse instead of an upload.
2645
+ */
2646
+ async createImageProFlash(args) {
2647
+ const body = {
2648
+ description: args.description,
2649
+ image_size: { width: args.width, height: args.height }
2650
+ };
2651
+ if (args.noBackground != null) body.no_background = args.noBackground;
2652
+ if (args.seed != null) body.seed = args.seed;
2653
+ if (args.styleImage) {
2654
+ body.style_image = { image: args.styleImage.image, size: { width: args.styleImage.width, height: args.styleImage.height } };
2655
+ const traits = args.styleTraits;
2656
+ if (traits) {
2657
+ body.style_options = {
2658
+ ...traits.palette !== void 0 ? { color_palette: traits.palette } : {},
2659
+ ...traits.outline !== void 0 ? { outline: traits.outline } : {},
2660
+ ...traits.detail !== void 0 ? { detail: traits.detail } : {},
2661
+ ...traits.shading !== void 0 ? { shading: traits.shading } : {}
2662
+ };
2663
+ }
2664
+ }
2665
+ return validateResponse(
2666
+ ProFlashImageSubmitSchema,
2667
+ await this.request("/create-image-pro-flash", { method: "POST", body: JSON.stringify(body) }),
2668
+ "create-image-pro-flash"
2669
+ );
2670
+ }
2671
+ /** `/edit-image-pro-flash`, text method: edit one image at its own size. The canvas never grows or resizes. */
2672
+ async editImageProFlash(args) {
2673
+ const body = { image: args.image, method: "text", description: args.description };
2674
+ if (args.noBackground != null) body.no_background = args.noBackground;
2675
+ if (args.seed != null) body.seed = args.seed;
2676
+ return validateResponse(
2677
+ ProFlashImageSubmitSchema,
2678
+ await this.request("/edit-image-pro-flash", { method: "POST", body: JSON.stringify(body) }),
2679
+ "edit-image-pro-flash"
2680
+ );
2681
+ }
2682
+ /**
2683
+ * `/inpaint-image-pro-flash`: replace only the white pixels of a same-size
2684
+ * black/white mask, keeping everything else exactly. `Modify current
2685
+ * layer` (the endpoint's default) returns the full composite.
2686
+ */
2687
+ async inpaintImageProFlash(args) {
2688
+ const body = { image: args.image, mask_image: args.maskImage, description: args.description };
2689
+ if (args.noBackground != null) body.no_background = args.noBackground;
2690
+ if (args.seed != null) body.seed = args.seed;
2691
+ return validateResponse(
2692
+ ProFlashImageSubmitSchema,
2693
+ await this.request("/inpaint-image-pro-flash", { method: "POST", body: JSON.stringify(body) }),
2694
+ "inpaint-image-pro-flash"
2695
+ );
2696
+ }
2489
2697
  /**
2490
2698
  * `/generate-ui-v2` ("Generate UI (Pro)"): one UI element (a button, a
2491
2699
  * health bar, an inventory slot, a dialogue box) from a description,
@@ -3366,6 +3574,7 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3366
3574
  uiPieces: spec.uiPieces,
3367
3575
  uiElements: spec.uiElements,
3368
3576
  uiColorPalette: spec.uiColorPalette,
3577
+ styleTraits: spec.styleTraits,
3369
3578
  styleImages: styleImageHashes,
3370
3579
  // A character family: the engine and rotations of a base, the parent's
3371
3580
  // generated bytes for a state or animation (so a regenerated parent
@@ -3392,7 +3601,16 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3392
3601
  styleTraits: spec.character.styleTraits,
3393
3602
  reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0,
3394
3603
  concept: spec.character.concept?.sha256,
3395
- styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0
3604
+ styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0,
3605
+ // The parent's own bytes come from its recorded outputs; the plan
3606
+ // compares those directly (like a mirror), so only the choice of
3607
+ // parent, the reference image's bytes, and any extra guidance
3608
+ // are identity here.
3609
+ outfit: spec.character.outfit ? {
3610
+ of: spec.character.parentAssetId,
3611
+ reference: spec.character.outfit.reference.sha256,
3612
+ additionalInstructions: spec.character.outfit.additionalInstructions
3613
+ } : void 0
3396
3614
  } : void 0,
3397
3615
  // An objectPro family: same shape as character above, minus the
3398
3616
  // fields it has none of (mode/template/proportions/isometric/concept/
@@ -3433,7 +3651,8 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3433
3651
  fps: spec.revision.fps,
3434
3652
  lastFrameSha256: spec.revision.lastFrameSha256,
3435
3653
  direction: spec.revision.direction,
3436
- enhancePrompt: spec.revision.enhancePrompt
3654
+ enhancePrompt: spec.revision.enhancePrompt,
3655
+ engine: spec.revision.engine
3437
3656
  } : void 0
3438
3657
  })
3439
3658
  );
@@ -3473,7 +3692,10 @@ function characterCost(spec) {
3473
3692
  if (animation.mode === "v3") return Math.max(1, Math.ceil(px * animation.frames / 65536));
3474
3693
  return generationCost(spec.width, spec.height, "1dir");
3475
3694
  }
3476
- if (character.kind === "state") return generationCost(spec.width, spec.height, "1dir");
3695
+ if (character.kind === "state" || character.kind === "portrait") return generationCost(spec.width, spec.height, "1dir");
3696
+ if (character.kind === "outfit") {
3697
+ return 20;
3698
+ }
3477
3699
  if (character.mode === "standard") return 1;
3478
3700
  if (character.mode === "v3") return 1 + Math.max(1, Math.ceil(px * 8 / 65536));
3479
3701
  if (character.mode === "pro-flash") {
@@ -3486,8 +3708,11 @@ function proFlashCharacterCost(width, height, fromReference) {
3486
3708
  const side = Math.max(32, Math.ceil(Math.max(width, height) / 4) * 4);
3487
3709
  const rotations = Math.max(1, Math.ceil(side * side * 8 / 65536));
3488
3710
  if (fromReference) return rotations;
3489
- const image = side <= 96 ? 5 : side <= 208 ? 6 : 9;
3490
- return image + rotations;
3711
+ return proFlashImageCost(side, side) + rotations;
3712
+ }
3713
+ function proFlashImageCost(width, height) {
3714
+ const side = Math.max(width, height);
3715
+ return side <= 96 ? 5 : side <= 208 ? 6 : 9;
3491
3716
  }
3492
3717
  function characterAnimationName(spec) {
3493
3718
  return `pixelkiln:${spec.styleId}/${spec.assetId}`;
@@ -3563,7 +3788,7 @@ var PixelLabProvider = class _PixelLabProvider {
3563
3788
  return new _PixelLabProvider(new PixelLabClient("download-only"));
3564
3789
  }
3565
3790
  supports(generator) {
3566
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement";
3791
+ 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";
3567
3792
  }
3568
3793
  /**
3569
3794
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
@@ -3590,15 +3815,18 @@ var PixelLabProvider = class _PixelLabProvider {
3590
3815
  return { spacingMs: 2500, maxInFlight: 8 };
3591
3816
  }
3592
3817
  /**
3593
- * Where synchronous pixflux results are parked between `submit` and `fetch`.
3818
+ * Where synchronous pixflux results (or an outfit transfer's inline
3819
+ * frames) are parked between `submit`/poll and `fetch`.
3594
3820
  *
3595
- * pixflux returns the PNG inline rather than a job id, but the pipeline is
3596
- * built around submit → poll → fetch running as separate commands. Writing
3597
- * the bytes to a known path keeps that model intact: the "job id" is the
3598
- * filename, polling is an existence check, and downloading is a file read.
3821
+ * pixflux returns the PNG inline rather than a job id, and an outfit
3822
+ * transfer's completed job returns its frames inline too, but the
3823
+ * pipeline is built around submit → poll → fetch running as separate
3824
+ * commands. Writing the bytes to a known path keeps that model intact:
3825
+ * the id is the filename, polling is an existence check, and downloading
3826
+ * is a file read.
3599
3827
  */
3600
- static cacheDir() {
3601
- const dir = import_node_path.default.join(import_node_os.default.tmpdir(), "pixelkiln-pixflux");
3828
+ static cacheDir(namespace = "pixflux") {
3829
+ const dir = import_node_path.default.join(import_node_os.default.tmpdir(), `pixelkiln-${namespace}`);
3602
3830
  (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
3603
3831
  return dir;
3604
3832
  }
@@ -3622,6 +3850,13 @@ var PixelLabProvider = class _PixelLabProvider {
3622
3850
  const base = Math.max(1, Math.ceil(width * height * frames / 65536));
3623
3851
  return { unit: "generations", amount: spec.revision.enhancePrompt ? base + 0.05 : base, candidates: 1 };
3624
3852
  }
3853
+ if (spec.revision?.engine === "pro-flash") {
3854
+ return {
3855
+ unit: "generations",
3856
+ amount: proFlashImageCost(spec.revision.sourceWidth ?? spec.width, spec.revision.sourceHeight ?? spec.height),
3857
+ candidates: 1
3858
+ };
3859
+ }
3625
3860
  if (spec.revision) {
3626
3861
  const width = spec.revision.sourceWidth ?? spec.width;
3627
3862
  const height = spec.revision.sourceHeight ?? spec.height;
@@ -3636,6 +3871,9 @@ var PixelLabProvider = class _PixelLabProvider {
3636
3871
  if (spec.generator === "objectPro") {
3637
3872
  return { unit: "generations", amount: objectProCost(spec), candidates: 1 };
3638
3873
  }
3874
+ if (spec.generator === "imageProFlash") {
3875
+ return { unit: "generations", amount: proFlashImageCost(spec.width, spec.height), candidates: 1 };
3876
+ }
3639
3877
  return {
3640
3878
  unit: "generations",
3641
3879
  amount: generationCost(spec.width, spec.height, spec.generator),
@@ -3649,7 +3887,17 @@ var PixelLabProvider = class _PixelLabProvider {
3649
3887
  `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`
3650
3888
  );
3651
3889
  }
3652
- if (spec.revision?.mode === "inpaint") {
3890
+ if (spec.revision?.engine === "pro-flash") {
3891
+ const { sourceWidth: width, sourceHeight: height, mode: mode2 } = spec.revision;
3892
+ if (width != null && height != null && (width < 32 || height < 32 || width > 256 || height > 256 || width % 4 || height % 4)) {
3893
+ throw new Error(
3894
+ `PixelLab pro-flash ${mode2} source is ${width}x${height}; the API takes 32 to 256 pixels per side, in multiples of 4`
3895
+ );
3896
+ }
3897
+ if (spec.revision.strength != null) {
3898
+ throw new Error(`PixelLab pro-flash ${mode2} takes no strength; the edit applies in full`);
3899
+ }
3900
+ } else if (spec.revision?.mode === "inpaint") {
3653
3901
  const { sourceWidth: width, sourceHeight: height } = spec.revision;
3654
3902
  if (width != null && height != null && (width < 32 || height < 32 || width > 512 || height > 512)) {
3655
3903
  throw new Error(`PixelLab inpaint source is ${width}x${height}; the API takes 32 to 512 pixels per side`);
@@ -3730,6 +3978,19 @@ var PixelLabProvider = class _PixelLabProvider {
3730
3978
  `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)`
3731
3979
  );
3732
3980
  }
3981
+ if (spec.generator === "imageProFlash") {
3982
+ if (spec.width < 16 || spec.height < 16 || spec.width > 256 || spec.height > 256 || spec.width % 4 || spec.height % 4) {
3983
+ throw new Error(
3984
+ `PixelLab imageProFlash is ${spec.width}x${spec.height}; the API takes 16 to 256 pixels per side, in multiples of 4`
3985
+ );
3986
+ }
3987
+ if (styleImages.length > 1) {
3988
+ throw new Error("PixelLab imageProFlash takes one style image; list at most one styleImages entry");
3989
+ }
3990
+ if (spec.styleTraits && !styleImages.length) {
3991
+ throw new Error("PixelLab imageProFlash: styleTraits choose what a style image lends; add a styleImages entry or drop them");
3992
+ }
3993
+ }
3733
3994
  if (spec.generator === "uiElement" && (spec.width < 16 || spec.height < 16 || spec.width > 792 || spec.height > 688)) {
3734
3995
  throw new Error(
3735
3996
  `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)`
@@ -3858,6 +4119,12 @@ var PixelLabProvider = class _PixelLabProvider {
3858
4119
  const character = spec.character;
3859
4120
  if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
3860
4121
  const label = `${spec.styleId}/${spec.assetId}`;
4122
+ if (character.kind === "outfit") {
4123
+ const reference = character.outfit.reference;
4124
+ if (reference.width < 32 || reference.width > 256 || reference.height < 32 || reference.height > 256) {
4125
+ throw new Error(`${label}: outfit reference is ${reference.width}x${reference.height}; PixelLab transfer-outfit-v2 takes 32 to 256px per side`);
4126
+ }
4127
+ }
3861
4128
  if (character.kind === "base" && character.proportions !== void 0) {
3862
4129
  if (character.mode !== "standard") {
3863
4130
  throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
@@ -3869,7 +4136,7 @@ var PixelLabProvider = class _PixelLabProvider {
3869
4136
  if (character.kind === "base" && character.enhancePrompt !== void 0 && character.mode !== "v3") {
3870
4137
  throw new Error(`${label}: enhancePrompt applies to v3 bases; the ${character.mode} engine does not take it`);
3871
4138
  }
3872
- if (character.animation) {
4139
+ if (character.animation && character.kind !== "outfit") {
3873
4140
  const { startFrame, endFrame } = character.animation;
3874
4141
  for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
3875
4142
  if (image && (image.width > 256 || image.height > 256)) {
@@ -3950,9 +4217,11 @@ var PixelLabProvider = class _PixelLabProvider {
3950
4217
  if (character.styleTraits && character.mode !== "pro-flash") {
3951
4218
  throw new Error(`${label}: styleTraits choose what a pro-flash style image lends; the ${character.mode} engine does not take them`);
3952
4219
  }
3953
- const maxSize = character.mode === "v3" || character.mode === "pro-flash" ? 256 : 128;
3954
- if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
3955
- throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
4220
+ if (character.kind !== "portrait") {
4221
+ const maxSize = character.mode === "v3" || character.mode === "pro-flash" ? 256 : 128;
4222
+ if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
4223
+ throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
4224
+ }
3956
4225
  }
3957
4226
  if (character.mode === "pro-flash" && character.kind === "base" && (spec.width % 4 || spec.height % 4)) {
3958
4227
  throw new Error(`PixelLab pro-flash characters are sized in multiples of 4 pixels; ${spec.width}x${spec.height} is not`);
@@ -3961,9 +4230,11 @@ var PixelLabProvider = class _PixelLabProvider {
3961
4230
  if (!templates.includes(character.template)) {
3962
4231
  throw new Error(`PixelLab ${character.mode} character template must be one of: ${templates.join(", ")}`);
3963
4232
  }
3964
- const views = character.mode === "standard" ? ["low top-down", "high top-down", "side", "perspective", "oblique"] : ["low top-down", "high top-down", "side"];
4233
+ 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"];
3965
4234
  if (!views.includes(spec.view)) {
3966
- throw new Error(`PixelLab ${character.mode} character view must be one of: ${views.join(", ")}`);
4235
+ throw new Error(
4236
+ character.kind === "portrait" ? `PixelLab portrait view must be one of: ${views.join(", ")}` : `PixelLab ${character.mode} character view must be one of: ${views.join(", ")}`
4237
+ );
3967
4238
  }
3968
4239
  if (spec.view === "oblique" && character.directions !== 4) {
3969
4240
  throw new Error("PixelLab oblique characters are 4-direction only");
@@ -3971,7 +4242,7 @@ var PixelLabProvider = class _PixelLabProvider {
3971
4242
  if (character.kind !== "base" && !character.parentAssetId) {
3972
4243
  throw new Error(`${spec.styleId}/${spec.assetId} is a ${character.kind} with no parent`);
3973
4244
  }
3974
- if (character.animation) {
4245
+ if (character.animation && character.kind !== "outfit") {
3975
4246
  const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
3976
4247
  if (!allowed.includes(character.animation.direction)) {
3977
4248
  throw new Error(
@@ -4073,10 +4344,48 @@ var PixelLabProvider = class _PixelLabProvider {
4073
4344
  concept: character.concept ? readPose(spec, "concept image", character.concept) : void 0,
4074
4345
  styleCharacterId: character.styleAnchor ? requireStyleObjectId(spec, context) : void 0,
4075
4346
  styleReferenceSize: styleImage ? { width: styleImage.width, height: styleImage.height } : void 0,
4076
- styleTraits: character.styleTraits
4347
+ styleTraits: character.styleTraits,
4348
+ sourceImageId: character.mode === "pro-flash" ? context?.referenceSourceImageId : void 0
4077
4349
  });
4078
4350
  return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
4079
4351
  }
4352
+ if (character.kind === "outfit") {
4353
+ const parentOutputs = context?.parentOutputs ?? [];
4354
+ if (parentOutputs.length < 2) {
4355
+ throw new Error(
4356
+ `${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`
4357
+ );
4358
+ }
4359
+ const frames = parentOutputs.slice(0, 16).map((output) => {
4360
+ const bytes = (0, import_node_fs2.readFileSync)(output.path);
4361
+ const dims = imageMetadata(bytes);
4362
+ if (!dims) throw new Error(`${spec.styleId}/${spec.assetId}: outfit source frame is not a readable PNG: ${output.path}`);
4363
+ return { base64: bytes.toString("base64"), format: "png", width: dims.width, height: dims.height };
4364
+ });
4365
+ const outfit = character.outfit;
4366
+ const referenceBytes = (0, import_node_fs2.readFileSync)(outfit.reference.path);
4367
+ if (sha256(referenceBytes) !== outfit.reference.sha256) {
4368
+ throw new Error(`${spec.styleId}/${spec.assetId}: outfit reference changed after the manifest was resolved: ${outfit.reference.path}`);
4369
+ }
4370
+ const res2 = await this.client.transferOutfitV2({
4371
+ referenceImage: { base64: referenceBytes.toString("base64"), format: "png", width: outfit.reference.width, height: outfit.reference.height },
4372
+ frames,
4373
+ imageSize: { width: spec.width, height: spec.height },
4374
+ seed: spec.seed,
4375
+ noBackground: spec.noBackground,
4376
+ additionalInstructions: outfit.additionalInstructions
4377
+ });
4378
+ return {
4379
+ jobId: res2.background_job_id,
4380
+ metadata: {
4381
+ character: {
4382
+ kind: "outfit",
4383
+ backgroundJobId: res2.background_job_id,
4384
+ frameRoles: parentOutputs.slice(0, 16).map((output) => output.role ?? null)
4385
+ }
4386
+ }
4387
+ };
4388
+ }
4080
4389
  const parentId = context?.parentObjectId;
4081
4390
  if (!parentId) {
4082
4391
  throw new Error(
@@ -4095,6 +4404,24 @@ var PixelLabProvider = class _PixelLabProvider {
4095
4404
  });
4096
4405
  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 } } };
4097
4406
  }
4407
+ if (character.kind === "portrait") {
4408
+ const parentFile = character.parentFile;
4409
+ const bytes = (0, import_node_fs2.readFileSync)(parentFile);
4410
+ if (character.parentSha256 && sha256(bytes) !== character.parentSha256) {
4411
+ throw new Error(`${spec.styleId}/${spec.assetId}: portrait source changed after the manifest was resolved: ${parentFile}`);
4412
+ }
4413
+ const res2 = await this.client.createPortraitCharacterPro({
4414
+ direction: "character_to_portrait",
4415
+ image: { base64: bytes.toString("base64"), format: "png" },
4416
+ view: spec.view,
4417
+ resultSize: character.portrait.size,
4418
+ seed: spec.seed
4419
+ });
4420
+ return {
4421
+ jobId: res2.background_job_id,
4422
+ metadata: { character: { kind: "portrait", parentCharacterId: parentId, backgroundJobId: res2.background_job_id } }
4423
+ };
4424
+ }
4098
4425
  const animation = character.animation;
4099
4426
  const name = characterAnimationName(spec);
4100
4427
  const replaced = context?.replacedMetadata?.character ?? {};
@@ -4153,7 +4480,8 @@ var PixelLabProvider = class _PixelLabProvider {
4153
4480
  reference: object.reference ? readPose(spec, "reference sprite", object.reference) : void 0,
4154
4481
  styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
4155
4482
  styleReferenceSize: styleImage ? { width: styleImage.width, height: styleImage.height } : void 0,
4156
- styleTraits: object.styleTraits
4483
+ styleTraits: object.styleTraits,
4484
+ sourceImageId: context?.referenceSourceImageId
4157
4485
  });
4158
4486
  return { jobId: res2.object_id, metadata: { objectPro: { kind: "base", objectId: res2.object_id, directions: object.directions, backgroundJobId: res2.background_job_id } } };
4159
4487
  }
@@ -4208,6 +4536,9 @@ var PixelLabProvider = class _PixelLabProvider {
4208
4536
  async pollCharacter(jobId, context) {
4209
4537
  const animation = decodeAnimationJob(jobId);
4210
4538
  if (animation) return this.pollCharacterAnimation(animation, context);
4539
+ const kind = context?.metadata?.character?.kind ?? context?.spec?.character?.kind;
4540
+ if (kind === "portrait") return this.pollCharacterPortrait(jobId, context);
4541
+ if (kind === "outfit") return this.pollCharacterOutfit(jobId, context);
4211
4542
  const character = await this.client.getCharacter(jobId);
4212
4543
  if (character.status === "failed") return { status: "failed", error: "character generation failed upstream" };
4213
4544
  if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
@@ -4233,6 +4564,66 @@ var PixelLabProvider = class _PixelLabProvider {
4233
4564
  billed: await this.billedForJob(backgroundJobId)
4234
4565
  };
4235
4566
  }
4567
+ /**
4568
+ * `jobId` here is `portrait-character-pro`'s own background job id, not a
4569
+ * character id: the endpoint converts an arbitrary image and never touches
4570
+ * the character record. Once the portrait itself is drawn, this also
4571
+ * attaches it to the parent's character record with `SetPortrait` — a
4572
+ * separate, free call PixelLab does not make automatically (docs/PIXELLAB.md).
4573
+ */
4574
+ async pollCharacterPortrait(jobId, context) {
4575
+ const meta = context?.metadata?.character;
4576
+ try {
4577
+ const job = await this.client.getPortraitCharacterJob(jobId);
4578
+ if (job.status === "failed") return { status: "failed", error: "portrait generation failed upstream" };
4579
+ if (job.status !== "completed" || !job.download_url) return { status: "processing" };
4580
+ if (meta?.parentCharacterId) {
4581
+ const bytes = await this.client.download(job.download_url);
4582
+ await this.client.setCharacterPortrait(meta.parentCharacterId, { base64: bytes.toString("base64"), format: "png" });
4583
+ }
4584
+ return {
4585
+ status: "ready",
4586
+ objectId: jobId,
4587
+ sourceUrl: job.download_url,
4588
+ sources: [{ url: job.download_url }],
4589
+ billed: await this.billedForJob(meta?.backgroundJobId)
4590
+ };
4591
+ } catch (err) {
4592
+ if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
4593
+ throw err;
4594
+ }
4595
+ }
4596
+ /**
4597
+ * No dedicated status endpoint exists for `transfer-outfit-v2`, unlike
4598
+ * portraits and tiles: the background job is the only place to poll. A
4599
+ * completed job returns its frames inline as base64
4600
+ * (`last_response.quantized_images`), not as storage URLs, so they are
4601
+ * cached locally the way a synchronous pixflux result is, and matched back
4602
+ * to the source frames' own roles recorded at submit time.
4603
+ */
4604
+ async pollCharacterOutfit(jobId, context) {
4605
+ const meta = context?.metadata?.character;
4606
+ const job = await this.client.getBackgroundJob(jobId);
4607
+ if (job.status === "failed") return { status: "failed", error: "outfit transfer failed upstream" };
4608
+ if (job.status !== "completed") return { status: "processing" };
4609
+ const images = job.last_response?.quantized_images;
4610
+ if (!images?.length) return { status: "failed", error: "outfit transfer completed with no result frames" };
4611
+ const dir = _PixelLabProvider.cacheDir("outfit");
4612
+ const sources = images.map((image, index) => {
4613
+ if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
4614
+ const role = meta?.frameRoles?.[index] ?? void 0;
4615
+ const file = import_node_path.default.join(dir, `${jobId}-${index}.png`);
4616
+ (0, import_node_fs2.writeFileSync)(file, Buffer.from(image.base64, "base64"));
4617
+ return { url: `file://${file}`, ...role ? { role } : {} };
4618
+ });
4619
+ return {
4620
+ status: "ready",
4621
+ objectId: jobId,
4622
+ sourceUrl: sources[0].url,
4623
+ sources,
4624
+ billed: await this.billedForJob(meta?.backgroundJobId)
4625
+ };
4626
+ }
4236
4627
  /**
4237
4628
  * The background job is the authoritative record of one direction: it
4238
4629
  * completes with the frame URLs, the animation id, and the direction, and
@@ -4389,6 +4780,19 @@ var PixelLabProvider = class _PixelLabProvider {
4389
4780
  });
4390
4781
  return { jobId: res2.ui_asset_id, metadata: { backgroundJobId: res2.background_job_id } };
4391
4782
  }
4783
+ if (spec.generator === "imageProFlash") {
4784
+ const style = styleImages[0];
4785
+ const res2 = await this.client.createImageProFlash({
4786
+ description: spec.prompt,
4787
+ width: spec.width,
4788
+ height: spec.height,
4789
+ noBackground: spec.noBackground,
4790
+ seed: spec.seed,
4791
+ styleImage: style ? { image: { base64: style.base64, format: style.format }, width: style.width, height: style.height } : void 0,
4792
+ styleTraits: spec.styleTraits
4793
+ });
4794
+ return { jobId: res2.background_job_id, metadata: { imageProFlash: proFlashRecord(res2) } };
4795
+ }
4392
4796
  if (spec.generator === "uiElement") {
4393
4797
  const concept = styleImages[0];
4394
4798
  const res2 = await this.client.generateUiV2({
@@ -4542,6 +4946,32 @@ var PixelLabProvider = class _PixelLabProvider {
4542
4946
  const width = revision.sourceWidth;
4543
4947
  const height = revision.sourceHeight;
4544
4948
  const roles = revision.sourceMembers?.map((member) => member.role);
4949
+ if (revision.engine === "pro-flash") {
4950
+ if (revision.mode === "inpaint") {
4951
+ if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
4952
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
4953
+ }
4954
+ const maskBytes = (0, import_node_fs2.readFileSync)(revision.maskFile);
4955
+ if (sha256(maskBytes) !== revision.maskSha256) {
4956
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
4957
+ }
4958
+ const res3 = await this.client.inpaintImageProFlash({
4959
+ image,
4960
+ maskImage: { base64: maskBytes.toString("base64"), format: revision.maskFormat },
4961
+ description: spec.prompt,
4962
+ noBackground: spec.noBackground,
4963
+ seed: spec.seed
4964
+ });
4965
+ return { jobId: res3.background_job_id, metadata: { imageProFlash: proFlashRecord(res3) } };
4966
+ }
4967
+ const res2 = await this.client.editImageProFlash({
4968
+ image,
4969
+ description: spec.prompt,
4970
+ noBackground: spec.noBackground,
4971
+ seed: spec.seed
4972
+ });
4973
+ return { jobId: res2.background_job_id, metadata: { imageProFlash: proFlashRecord(res2) } };
4974
+ }
4545
4975
  if (revision.mode === "inpaint") {
4546
4976
  if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
4547
4977
  throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
@@ -4706,6 +5136,7 @@ var PixelLabProvider = class _PixelLabProvider {
4706
5136
  return this.pollAnimateRevision(jobId, context);
4707
5137
  }
4708
5138
  if (context?.spec?.revision) return this.pollRevision(jobId);
5139
+ if (generator === "imageProFlash") return this.pollRevision(jobId);
4709
5140
  if (generator === "pixflux") {
4710
5141
  const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
4711
5142
  if ((0, import_node_fs2.existsSync)(file)) {
@@ -5286,6 +5717,13 @@ function readPose(spec, what, image) {
5286
5717
  }
5287
5718
  return { base64: bytes.toString("base64"), format: image.format };
5288
5719
  }
5720
+ function proFlashRecord(res) {
5721
+ return {
5722
+ imageId: res.image_id,
5723
+ sourceImageId: res.source_image_id,
5724
+ ...res.estimated_generations != null ? { estimatedGenerations: res.estimated_generations } : {}
5725
+ };
5726
+ }
5289
5727
  function editAnimationFrameCeiling(width, height) {
5290
5728
  const side = Math.max(width, height);
5291
5729
  if (side <= 64) return 16;
@@ -6489,6 +6927,11 @@ var ComfyUIProvider = class _ComfyUIProvider {
6489
6927
  throw new Error("ComfyUI providerOptions.frames requires generator: frames");
6490
6928
  }
6491
6929
  if (spec.revision) {
6930
+ if (spec.revision.engine) {
6931
+ throw new Error(
6932
+ `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.`
6933
+ );
6934
+ }
6492
6935
  if (spec.revision.sourceMembers) {
6493
6936
  throw new Error(
6494
6937
  `ComfyUI revisions read one source image; ${spec.revision.sourceAssetId} is a ${spec.revision.sourceMembers.length}-member set (directions or frames)`
@@ -7831,7 +8274,7 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
7831
8274
  return { manifest: parsed.data, root: import_node_path5.default.dirname(abs), path: abs, ...loopFamilies };
7832
8275
  }
7833
8276
  function parentAssetId(asset) {
7834
- return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
8277
+ return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
7835
8278
  }
7836
8279
  function formatManifestIssues(issues) {
7837
8280
  return issues.flatMap((issue) => {
@@ -7998,9 +8441,9 @@ async function resolveSpecs(loaded, filter) {
7998
8441
  height = styleImageDimensions.length ? Math.max(...styleImageDimensions.map((img) => img.height)) : style.tileSize ?? 32;
7999
8442
  size = Math.max(width, height);
8000
8443
  } else if (generator === "character") {
8001
- size = asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
8002
- width = asset.state?.canvas?.width ?? size;
8003
- height = asset.state?.canvas?.height ?? size;
8444
+ size = asset.portrait ? asset.portrait.size : asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
8445
+ width = asset.portrait?.size ?? asset.state?.canvas?.width ?? size;
8446
+ height = asset.portrait?.size ?? asset.state?.canvas?.height ?? size;
8004
8447
  } else if (generator === "terrain") {
8005
8448
  size = style.terrainTileSize ?? 16;
8006
8449
  width = size;
@@ -8027,12 +8470,17 @@ async function resolveSpecs(loaded, filter) {
8027
8470
  `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
8028
8471
  );
8029
8472
  }
8473
+ if ((asset.portrait || asset.outfit) && generator !== "character") {
8474
+ throw new Error(
8475
+ `assets.${assetId}: ${asset.portrait ? "portrait" : "outfit"} needs a character style; "${styleId}" generates ${generator}`
8476
+ );
8477
+ }
8030
8478
  if ((asset.pieces || asset.elements) && generator !== "uiAsset") {
8031
8479
  throw new Error(
8032
8480
  `assets.${assetId}: ${asset.pieces ? "pieces" : "elements"} needs a uiAsset style; "${styleId}" generates ${generator}`
8033
8481
  );
8034
8482
  }
8035
- const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
8483
+ const characterKind = asset.animation ? "animation" : asset.state ? "state" : asset.portrait ? "portrait" : "base";
8036
8484
  const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
8037
8485
  let terrainLowerDescription;
8038
8486
  let terrainUpperDescription;
@@ -8081,6 +8529,7 @@ async function resolveSpecs(loaded, filter) {
8081
8529
  uiPieces: generator === "uiAsset" ? asset.pieces : void 0,
8082
8530
  uiElements: generator === "uiAsset" ? asset.elements : void 0,
8083
8531
  uiColorPalette: generator === "uiAsset" || generator === "uiElement" ? style.uiColorPalette : void 0,
8532
+ ...generator === "imageProFlash" && style.styleTraits ? { styleTraits: style.styleTraits } : {},
8084
8533
  palette: style.palette,
8085
8534
  enforcePalette: style.enforcePalette,
8086
8535
  noBackground: style.noBackground,
@@ -8124,7 +8573,9 @@ async function resolveSpecs(loaded, filter) {
8124
8573
  // its parent's pixels, not a new character: giving it a base shape
8125
8574
  // would have packing, the gallery, and adoption treat it as one.
8126
8575
  ...generator === "character" && !asset.revision ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
8127
- ...generator === "objectPro" && !asset.revision ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
8576
+ // A portrait needs a character style (refused above), so an
8577
+ // objectPro asset is only ever a base, state, or animation.
8578
+ ...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
8128
8579
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
8129
8580
  costUnit: "generations",
8130
8581
  candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
@@ -8235,7 +8686,7 @@ async function resolveSpecs(loaded, filter) {
8235
8686
  styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: (0, import_node_fs5.existsSync)(file) ? await sha256File(file) : null }
8236
8687
  };
8237
8688
  }
8238
- const characterParent = asset.state?.of ?? asset.animation?.of;
8689
+ const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
8239
8690
  if (resolved.character && characterParent) {
8240
8691
  const parentSpec = await finalize(characterParent);
8241
8692
  const parentDirections = parentSpec.character?.directions ?? 8;
@@ -8268,6 +8719,38 @@ async function resolveSpecs(loaded, filter) {
8268
8719
  directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
8269
8720
  };
8270
8721
  }
8722
+ if (asset.outfit) {
8723
+ if (asset.outfit.of === assetId) throw new Error(`assets.${assetId}: an asset cannot re-clothe itself`);
8724
+ const sourceSpec = await finalize(asset.outfit.of);
8725
+ if (sourceSpec.character?.kind !== "animation") {
8726
+ throw new Error(`assets.${assetId}: outfit "${asset.outfit.of}" is not a character loop`);
8727
+ }
8728
+ const referenceImage = await loadStyleImage(asset.outfit.reference, "Outfit reference image");
8729
+ resolved.character = {
8730
+ kind: "outfit",
8731
+ mode: sourceSpec.character.mode,
8732
+ directions: sourceSpec.character.directions,
8733
+ template: sourceSpec.character.template,
8734
+ parentAssetId: asset.outfit.of,
8735
+ parentSpec: sourceSpec,
8736
+ animation: sourceSpec.character.animation,
8737
+ outfit: {
8738
+ reference: {
8739
+ path: import_node_path5.default.resolve(root, asset.outfit.reference),
8740
+ sha256: referenceImage.hash,
8741
+ width: referenceImage.width,
8742
+ height: referenceImage.height,
8743
+ format: referenceImage.format
8744
+ },
8745
+ ...asset.outfit.additionalInstructions ? { additionalInstructions: asset.outfit.additionalInstructions } : {}
8746
+ }
8747
+ };
8748
+ resolved.generator = sourceSpec.generator;
8749
+ resolved.width = sourceSpec.width;
8750
+ resolved.height = sourceSpec.height;
8751
+ resolved.size = sourceSpec.size;
8752
+ resolved.prompt = "";
8753
+ }
8271
8754
  if (asset.revision) {
8272
8755
  if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
8273
8756
  throw new Error(
@@ -8330,7 +8813,8 @@ async function resolveSpecs(loaded, filter) {
8330
8813
  lastFrameFormat: lastFrame?.format ?? null
8331
8814
  } : {},
8332
8815
  ...asset.revision.direction ? { direction: asset.revision.direction } : {},
8333
- ...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt }
8816
+ ...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt },
8817
+ ...asset.revision.engine ? { engine: asset.revision.engine } : {}
8334
8818
  };
8335
8819
  }
8336
8820
  if (asset.batch) {
@@ -8541,6 +9025,9 @@ async function resolveCharacterShape(asset, style, kind, files) {
8541
9025
  ...asset.state.canvas ? { canvas: asset.state.canvas } : {}
8542
9026
  };
8543
9027
  }
9028
+ if (asset.portrait) {
9029
+ shape.portrait = { size: asset.portrait.size };
9030
+ }
8544
9031
  if (asset.animation) {
8545
9032
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
8546
9033
  const pose = async (rel, what) => {
@@ -10093,6 +10580,7 @@ var import_node_fs8 = require("fs");
10093
10580
  var import_node_path11 = __toESM(require("path"), 1);
10094
10581
  async function inspectRevisionReadiness(spec, lock) {
10095
10582
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
10583
+ if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
10096
10584
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
10097
10585
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
10098
10586
  if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
@@ -10119,6 +10607,20 @@ async function inspectMirror(spec, lock, seen) {
10119
10607
  const dependency = await inspectParent(source, lock, seen);
10120
10608
  return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
10121
10609
  }
10610
+ async function inspectOutfit(spec, lock, seen) {
10611
+ const character = spec.character;
10612
+ const outfit = character.outfit;
10613
+ const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
10614
+ const dependency = await inspectParent(character.parentSpec, lock, seen);
10615
+ if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
10616
+ if (!(0, import_node_fs8.existsSync)(outfit.reference.path)) {
10617
+ return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
10618
+ }
10619
+ if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
10620
+ return { ready: false, reason: "outfit reference changed after the manifest was resolved" };
10621
+ }
10622
+ return { ready: true, reason: `${label} is current` };
10623
+ }
10122
10624
  async function inspectCharacterParent(spec, lock, seen) {
10123
10625
  const character = spec.character;
10124
10626
  const parentSpec = character.parentSpec;
@@ -10234,7 +10736,10 @@ async function inspectParent(spec, lock, seen) {
10234
10736
  const inputs = await inspectRevision(spec, lock, nextSeen);
10235
10737
  if (!inputs.ready) return inputs;
10236
10738
  }
10237
- if (spec.character?.parentSpec) {
10739
+ if (spec.character?.kind === "outfit") {
10740
+ const inputs = await inspectOutfit(spec, lock, nextSeen);
10741
+ if (!inputs.ready) return inputs;
10742
+ } else if (spec.character?.parentSpec) {
10238
10743
  const inputs = await inspectCharacterParent(spec, lock, nextSeen);
10239
10744
  if (!inputs.ready) return inputs;
10240
10745
  }
@@ -10357,6 +10862,9 @@ async function buildPlan(specs, lock, opts = {}) {
10357
10862
  } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
10358
10863
  state = "stale";
10359
10864
  reason = "mirror output missing on disk; flip it again (no generation cost)";
10865
+ } else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
10866
+ state = "stale";
10867
+ reason = `outfit source ${spec.character.parentAssetId} changed; re-clothe the loop again`;
10360
10868
  } else if (entry.status === "download-failed") {
10361
10869
  state = "recoverable";
10362
10870
  const force = entry.error?.includes("pass --force") ? " --force" : "";
@@ -13054,6 +13562,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13054
13562
  const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
13055
13563
  const submittedAt = resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString();
13056
13564
  const history = historyAfterReplacing(previousEntry, historyLimit(loaded.manifest), submittedAt ?? (/* @__PURE__ */ new Date()).toISOString());
13565
+ const outfitParentEntry = spec.character?.kind === "outfit" && spec.character.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
13057
13566
  upsert(lock, key, {
13058
13567
  styleId: spec.styleId,
13059
13568
  assetId: spec.assetId,
@@ -13077,7 +13586,13 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13077
13586
  ...spec.revision.fps == null ? {} : { fps: spec.revision.fps },
13078
13587
  ...spec.revision.lastFrameSha256 ? { lastFrameSha256: spec.revision.lastFrameSha256 } : {},
13079
13588
  ...spec.revision.direction ? { direction: spec.revision.direction } : {},
13080
- ...spec.revision.enhancePrompt == null ? {} : { enhancePrompt: spec.revision.enhancePrompt }
13589
+ ...spec.revision.enhancePrompt == null ? {} : { enhancePrompt: spec.revision.enhancePrompt },
13590
+ ...spec.revision.engine ? { engine: spec.revision.engine } : {}
13591
+ } : null,
13592
+ outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
13593
+ sourceAssetId: spec.character.parentAssetId,
13594
+ sourceSha256: mirrorSourceHash(outfitParentEntry),
13595
+ referenceSha256: spec.character.outfit.reference.sha256
13081
13596
  } : null,
13082
13597
  status: "pending",
13083
13598
  jobId: previousJobId ?? null,
@@ -13112,10 +13627,17 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13112
13627
  const anchor = spec.character?.styleAnchor;
13113
13628
  const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
13114
13629
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
13630
+ const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
13631
+ path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
13632
+ role: output.role
13633
+ })) : void 0;
13634
+ const referenceSourceImageId = ownedReferenceImageId(spec, lock, provider.id);
13115
13635
  const { jobId, metadata } = await provider.submit(spec, refs, {
13116
13636
  ...previousJobId ? { previousJobId } : {},
13637
+ ...referenceSourceImageId ? { referenceSourceImageId } : {},
13117
13638
  ...parentEntry?.objectId ? { parentObjectId: parentEntry.objectId } : {},
13118
13639
  ...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
13640
+ ...parentOutputs ? { parentOutputs } : {},
13119
13641
  ...replacedMetadata ? { replacedMetadata } : {},
13120
13642
  ...previousMetadata ? { previousMetadata } : {},
13121
13643
  checkpoint: async (checkpoint) => {
@@ -13179,6 +13701,17 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13179
13701
  }
13180
13702
  return { submitted, failed, spent, unit };
13181
13703
  }
13704
+ function ownedReferenceImageId(spec, lock, providerId) {
13705
+ 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;
13706
+ if (!sha2562) return void 0;
13707
+ for (const entry of Object.values(lock.entries)) {
13708
+ if (entry.provider !== providerId || entry.generator !== "imageProFlash" || entry.status !== "downloaded") continue;
13709
+ if (entry.outputs.length !== 1 || entry.outputs[0].sha256 !== sha2562) continue;
13710
+ const recorded = entry.providerMetadata?.[providerId]?.imageProFlash;
13711
+ if (typeof recorded?.sourceImageId === "string" && recorded.sourceImageId) return recorded.sourceImageId;
13712
+ }
13713
+ return void 0;
13714
+ }
13182
13715
 
13183
13716
  // src/pipeline/poll.ts
13184
13717
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -15234,7 +15767,7 @@ function describeCharacter(spec, entry) {
15234
15767
  };
15235
15768
  }
15236
15769
  if (entry?.generator === "character") {
15237
- const kind = recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
15770
+ const kind = recordedCharacter?.kind === "portrait" ? "portrait" : recordedCharacter?.kind === "outfit" ? "outfit" : recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
15238
15771
  return {
15239
15772
  generator: "character",
15240
15773
  kind,
@@ -15595,7 +16128,9 @@ async function buildGallerySnapshot(opts) {
15595
16128
  characters: style?.generator === "character" || style?.generator === "objectPro" ? {
15596
16129
  bases: styleItems.filter((item) => item.character?.kind === "base").length,
15597
16130
  states: styleItems.filter((item) => item.character?.kind === "state").length,
15598
- animations: styleItems.filter((item) => item.character?.kind === "animation").length
16131
+ animations: styleItems.filter((item) => item.character?.kind === "animation").length,
16132
+ portraits: styleItems.filter((item) => item.character?.kind === "portrait").length,
16133
+ outfits: styleItems.filter((item) => item.character?.kind === "outfit").length
15599
16134
  } : null,
15600
16135
  tags: style?.tags ?? [],
15601
16136
  extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
@@ -17390,6 +17925,9 @@ async function workspaceStatus(ws, dir) {
17390
17925
  CharacterAnimationSchema,
17391
17926
  CharacterDirectionSchema,
17392
17927
  CharacterModeSchema,
17928
+ CharacterOutfitSchema,
17929
+ CharacterPortraitSchema,
17930
+ CharacterPortraitSizeSchema,
17393
17931
  CharacterProportionsSchema,
17394
17932
  CharacterStateSchema,
17395
17933
  ComfyUIProvider,