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/cli.js CHANGED
@@ -679,6 +679,7 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
679
679
  uiPieces: spec.uiPieces,
680
680
  uiElements: spec.uiElements,
681
681
  uiColorPalette: spec.uiColorPalette,
682
+ styleTraits: spec.styleTraits,
682
683
  styleImages: styleImageHashes,
683
684
  // A character family: the engine and rotations of a base, the parent's
684
685
  // generated bytes for a state or animation (so a regenerated parent
@@ -705,7 +706,16 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
705
706
  styleTraits: spec.character.styleTraits,
706
707
  reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0,
707
708
  concept: spec.character.concept?.sha256,
708
- styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0
709
+ styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0,
710
+ // The parent's own bytes come from its recorded outputs; the plan
711
+ // compares those directly (like a mirror), so only the choice of
712
+ // parent, the reference image's bytes, and any extra guidance
713
+ // are identity here.
714
+ outfit: spec.character.outfit ? {
715
+ of: spec.character.parentAssetId,
716
+ reference: spec.character.outfit.reference.sha256,
717
+ additionalInstructions: spec.character.outfit.additionalInstructions
718
+ } : void 0
709
719
  } : void 0,
710
720
  // An objectPro family: same shape as character above, minus the
711
721
  // fields it has none of (mode/template/proportions/isometric/concept/
@@ -746,7 +756,8 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
746
756
  fps: spec.revision.fps,
747
757
  lastFrameSha256: spec.revision.lastFrameSha256,
748
758
  direction: spec.revision.direction,
749
- enhancePrompt: spec.revision.enhancePrompt
759
+ enhancePrompt: spec.revision.enhancePrompt,
760
+ engine: spec.revision.engine
750
761
  } : void 0
751
762
  })
752
763
  );
@@ -764,7 +775,7 @@ import path2 from "path";
764
775
  // src/types.ts
765
776
  import { z } from "zod";
766
777
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
767
- var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement"]);
778
+ var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement", "imageProFlash"]);
768
779
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
769
780
  var RevisionModeSchema = z.enum([
770
781
  "image-to-image",
@@ -949,6 +960,27 @@ var CharacterStateSchema = z.object({
949
960
  height: z.number().int().min(16).max(256)
950
961
  }).strict().optional()
951
962
  }).strict();
963
+ var CharacterPortraitSizeSchema = z.union([
964
+ z.literal(16),
965
+ z.literal(32),
966
+ z.literal(48),
967
+ z.literal(64),
968
+ z.literal(128),
969
+ z.literal(160)
970
+ ]);
971
+ var CharacterPortraitSchema = z.object({
972
+ /** The base or state to portray, in the same style. */
973
+ of: z.string().min(1),
974
+ size: CharacterPortraitSizeSchema.default(64)
975
+ }).strict();
976
+ var CharacterOutfitSchema = z.object({
977
+ /** The loop (a character animation asset) to re-clothe, in the same style. */
978
+ of: z.string().min(1),
979
+ /** Manifest-relative image of the outfit to transfer. */
980
+ reference: z.string().min(1),
981
+ /** Extra guidance PixelLab passes through as written, e.g. view/direction hints. */
982
+ additionalInstructions: z.string().max(2e3).optional()
983
+ }).strict();
952
984
  var CharacterAnimationSchema = z.object({
953
985
  /** The character to animate: a base or a state in the same style. */
954
986
  of: z.string().min(1),
@@ -1070,8 +1102,22 @@ var RevisionSchema = z.object({
1070
1102
  /** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
1071
1103
  direction: CharacterDirectionSchema.optional(),
1072
1104
  /** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
1073
- enhancePrompt: z.boolean().optional()
1105
+ enhancePrompt: z.boolean().optional(),
1106
+ /**
1107
+ * `image-to-image`/`inpaint` only: which provider engine draws the
1108
+ * edit. `pro-flash` selects PixelLab's Pro Flash edit and inpaint tier
1109
+ * (`/edit-image-pro-flash`, `/inpaint-image-pro-flash`) instead of the
1110
+ * default Pro endpoints; omit it for the default.
1111
+ */
1112
+ engine: z.enum(["pro-flash"]).optional()
1074
1113
  }).strict().superRefine((revision, context) => {
1114
+ if (revision.engine !== void 0 && revision.mode !== "image-to-image" && revision.mode !== "inpaint") {
1115
+ context.addIssue({
1116
+ code: z.ZodIssueCode.custom,
1117
+ message: "engine applies to image-to-image and inpaint revisions only",
1118
+ path: ["engine"]
1119
+ });
1120
+ }
1075
1121
  if (revision.mode === "inpaint" && !revision.mask) {
1076
1122
  context.addIssue({
1077
1123
  code: z.ZodIssueCode.custom,
@@ -1360,8 +1406,8 @@ var StyleObjectSchema = z.object({
1360
1406
  /** `character` only. Body template: `mannequin` (default), a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`), or `custom` for `pro-flash`. */
1361
1407
  template: z.string().min(1).optional(),
1362
1408
  /**
1363
- * `character` only, `pro-flash` bases with a style image: which traits
1364
- * the style image lends. Each defaults to true.
1409
+ * `character` `pro-flash` bases and `imageProFlash` styles, with a
1410
+ * style image: which traits the style image lends. Each defaults to true.
1365
1411
  */
1366
1412
  styleTraits: z.object({
1367
1413
  palette: z.boolean().optional(),
@@ -1570,6 +1616,10 @@ var AssetSchema = z.object({
1570
1616
  pieces: z.array(UiPieceSchema).min(1).optional(),
1571
1617
  /** `uiAsset` styles: named, auto-positioned UI element scaffolds. Combine with `pieces`; omit both for a default full-canvas panel. */
1572
1618
  elements: z.array(UiElementSchema).min(1).optional(),
1619
+ /** `character` styles: this asset is a bust portrait of another character asset's south sprite. */
1620
+ portrait: CharacterPortraitSchema.optional(),
1621
+ /** `character` styles: this asset re-clothes an existing loop's frames with a reference outfit. */
1622
+ outfit: CharacterOutfitSchema.optional(),
1573
1623
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1574
1624
  proportions: CharacterProportionsSchema.optional(),
1575
1625
  /**
@@ -1646,7 +1696,7 @@ var AssetSchema = z.object({
1646
1696
  path: ["revision"]
1647
1697
  });
1648
1698
  }
1649
- const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch"].filter(Boolean);
1699
+ 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);
1650
1700
  if (shapes.length > 1) {
1651
1701
  context.addIssue({
1652
1702
  code: z.ZodIssueCode.custom,
@@ -1661,24 +1711,24 @@ var AssetSchema = z.object({
1661
1711
  path: ["mirror"]
1662
1712
  });
1663
1713
  }
1664
- if (asset.prompt === void 0 && !asset.mirror) {
1714
+ if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit) {
1665
1715
  context.addIssue({
1666
1716
  code: z.ZodIssueCode.custom,
1667
1717
  message: "Required",
1668
1718
  path: ["prompt"]
1669
1719
  });
1670
1720
  }
1671
- if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
1721
+ if (asset.reference && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
1672
1722
  context.addIssue({
1673
1723
  code: z.ZodIssueCode.custom,
1674
- message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
1724
+ message: "a reference sprite belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
1675
1725
  path: ["reference"]
1676
1726
  });
1677
1727
  }
1678
- if (asset.concept && (asset.state || asset.animation || asset.mirror)) {
1728
+ if (asset.concept && (asset.state || asset.animation || asset.mirror || asset.portrait || asset.outfit)) {
1679
1729
  context.addIssue({
1680
1730
  code: z.ZodIssueCode.custom,
1681
- message: "a concept image belongs on a base; a state, animation, or mirror takes its look from its parent",
1731
+ message: "a concept image belongs on a base; a state, animation, mirror, portrait, or outfit takes its look from its parent",
1682
1732
  path: ["concept"]
1683
1733
  });
1684
1734
  }
@@ -1790,7 +1840,8 @@ var LockEntrySchema = z.object({
1790
1840
  fps: z.number().int().min(1).max(60).optional(),
1791
1841
  lastFrameSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1792
1842
  direction: CharacterDirectionSchema.optional(),
1793
- enhancePrompt: z.boolean().optional()
1843
+ enhancePrompt: z.boolean().optional(),
1844
+ engine: z.enum(["pro-flash"]).optional()
1794
1845
  }).strict().nullable().default(null),
1795
1846
  /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
1796
1847
  mirror: z.object({
@@ -1803,6 +1854,17 @@ var LockEntrySchema = z.object({
1803
1854
  leaderAssetId: z.string().min(1).optional(),
1804
1855
  index: z.number().int().min(1).optional()
1805
1856
  }).strict().nullable().default(null),
1857
+ /**
1858
+ * For an outfit: the loop it re-clothed, a hash over that loop's output
1859
+ * hashes when this was made, and the reference image's own hash. Tracked
1860
+ * the way `mirror` is, not through `specHash`: manifest resolution has no
1861
+ * lock to hash the parent's actual current frame bytes against.
1862
+ */
1863
+ outfit: z.object({
1864
+ sourceAssetId: z.string().min(1),
1865
+ sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
1866
+ referenceSha256: z.string().regex(/^[0-9a-f]{64}$/)
1867
+ }).strict().nullable().default(null),
1806
1868
  /**
1807
1869
  * Output hashes owned by the previous generation while its replacement is
1808
1870
  * pending. They authorize replacing only unchanged PixelKiln-owned files.
@@ -4080,6 +4142,7 @@ import { existsSync as existsSync4 } from "fs";
4080
4142
  import path8 from "path";
4081
4143
  async function inspectRevisionReadiness(spec, lock) {
4082
4144
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
4145
+ if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
4083
4146
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
4084
4147
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
4085
4148
  if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
@@ -4106,6 +4169,20 @@ async function inspectMirror(spec, lock, seen) {
4106
4169
  const dependency = await inspectParent(source, lock, seen);
4107
4170
  return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
4108
4171
  }
4172
+ async function inspectOutfit(spec, lock, seen) {
4173
+ const character = spec.character;
4174
+ const outfit = character.outfit;
4175
+ const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
4176
+ const dependency = await inspectParent(character.parentSpec, lock, seen);
4177
+ if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
4178
+ if (!existsSync4(outfit.reference.path)) {
4179
+ return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
4180
+ }
4181
+ if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
4182
+ return { ready: false, reason: "outfit reference changed after the manifest was resolved" };
4183
+ }
4184
+ return { ready: true, reason: `${label} is current` };
4185
+ }
4109
4186
  async function inspectCharacterParent(spec, lock, seen) {
4110
4187
  const character = spec.character;
4111
4188
  const parentSpec = character.parentSpec;
@@ -4221,7 +4298,10 @@ async function inspectParent(spec, lock, seen) {
4221
4298
  const inputs = await inspectRevision(spec, lock, nextSeen);
4222
4299
  if (!inputs.ready) return inputs;
4223
4300
  }
4224
- if (spec.character?.parentSpec) {
4301
+ if (spec.character?.kind === "outfit") {
4302
+ const inputs = await inspectOutfit(spec, lock, nextSeen);
4303
+ if (!inputs.ready) return inputs;
4304
+ } else if (spec.character?.parentSpec) {
4225
4305
  const inputs = await inspectCharacterParent(spec, lock, nextSeen);
4226
4306
  if (!inputs.ready) return inputs;
4227
4307
  }
@@ -4344,6 +4424,9 @@ async function buildPlan(specs, lock, opts = {}) {
4344
4424
  } else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !existsSync5(currentEntryOutputPath(entry, spec, index)))) {
4345
4425
  state = "stale";
4346
4426
  reason = "mirror output missing on disk; flip it again (no generation cost)";
4427
+ } else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
4428
+ state = "stale";
4429
+ reason = `outfit source ${spec.character.parentAssetId} changed; re-clothe the loop again`;
4347
4430
  } else if (entry.status === "download-failed") {
4348
4431
  state = "recoverable";
4349
4432
  const force = entry.error?.includes("pass --force") ? " --force" : "";
@@ -4970,6 +5053,11 @@ var ComfyUIProvider = class _ComfyUIProvider {
4970
5053
  throw new Error("ComfyUI providerOptions.frames requires generator: frames");
4971
5054
  }
4972
5055
  if (spec.revision) {
5056
+ if (spec.revision.engine) {
5057
+ throw new Error(
5058
+ `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.`
5059
+ );
5060
+ }
4973
5061
  if (spec.revision.sourceMembers) {
4974
5062
  throw new Error(
4975
5063
  `ComfyUI revisions read one source image; ${spec.revision.sourceAssetId} is a ${spec.revision.sourceMembers.length}-member set (directions or frames)`
@@ -5710,6 +5798,14 @@ var CorrectPixelartResponseSchema = z2.object({
5710
5798
  images: z2.array(z2.object({ base64: z2.string().min(1) }).passthrough()).min(1),
5711
5799
  usage: z2.unknown().optional()
5712
5800
  }).passthrough();
5801
+ var ProFlashImageSubmitSchema = z2.object({
5802
+ background_job_id: z2.string().min(1),
5803
+ image_id: z2.string().min(1),
5804
+ source_image_id: z2.string().min(1),
5805
+ status: z2.string().default("processing"),
5806
+ estimated_generations: z2.number().nullable().optional(),
5807
+ usage: z2.unknown().optional()
5808
+ }).passthrough();
5713
5809
  var UnzoomResponseSchema = z2.object({
5714
5810
  image: z2.object({ base64: z2.string().min(1) }).passthrough(),
5715
5811
  original_size: z2.object({ width: z2.number().int(), height: z2.number().int() }).passthrough(),
@@ -5760,6 +5856,28 @@ var AnimateObjectSubmitSchema = z2.object({
5760
5856
  submissions: z2.array(DirectionSubmissionSchema),
5761
5857
  usage: UsageSchema
5762
5858
  }).passthrough();
5859
+ var PortraitSubmitSchema = z2.object({
5860
+ background_job_id: z2.string().min(1),
5861
+ status: z2.string().default("processing"),
5862
+ usage: UsageSchema
5863
+ }).passthrough();
5864
+ var PortraitJobSchema = z2.object({
5865
+ status: z2.string(),
5866
+ download_url: z2.string().nullable().optional(),
5867
+ width: z2.number().int().nullable().optional(),
5868
+ height: z2.number().int().nullable().optional()
5869
+ }).passthrough();
5870
+ var SetPortraitSchema = z2.object({
5871
+ character_id: z2.string().min(1),
5872
+ size: z2.number().int().positive(),
5873
+ url: z2.string().min(1),
5874
+ usage: UsageSchema
5875
+ }).passthrough();
5876
+ var TransferOutfitSubmitSchema = z2.object({
5877
+ background_job_id: z2.string().min(1),
5878
+ status: z2.string().default("processing"),
5879
+ usage: UsageSchema
5880
+ }).passthrough();
5763
5881
  var CharacterAnimationDirectionSchema = z2.object({
5764
5882
  direction: z2.string(),
5765
5883
  frame_count: z2.number().int().min(0),
@@ -6128,7 +6246,7 @@ var PixelLabClient = class {
6128
6246
  const body = {
6129
6247
  description: args.description,
6130
6248
  n_directions: args.directions,
6131
- ...args.reference ? { first_frame: encode(args.reference) } : { image_size: { width: args.size, height: args.size } },
6249
+ ...args.sourceImageId ? { source_image_id: args.sourceImageId } : args.reference ? { first_frame: encode(args.reference) } : { image_size: { width: args.size, height: args.size } },
6132
6250
  ...args.view ? { view: args.view } : {},
6133
6251
  ...args.seed != null ? { seed: args.seed } : {},
6134
6252
  ...args.styleReference && args.styleReferenceSize ? {
@@ -6275,8 +6393,9 @@ var PixelLabClient = class {
6275
6393
  description: args.description,
6276
6394
  template_id: args.template,
6277
6395
  n_directions: 8,
6278
- // The author's sprite sets the canvas; a size only applies to text creation.
6279
- ...south ? { first_frame: encode(south) } : { image_size: imageSize },
6396
+ // The author's sprite sets the canvas; a size only applies to text
6397
+ // creation. An owned copy of the same sprite is referenced by id.
6398
+ ...args.sourceImageId ? { source_image_id: args.sourceImageId } : south ? { first_frame: encode(south) } : { image_size: imageSize },
6280
6399
  ...args.view ? { view: args.view } : {},
6281
6400
  ...args.seed != null ? { seed: args.seed } : {},
6282
6401
  ...args.styleReference && args.styleReferenceSize ? {
@@ -6375,6 +6494,69 @@ var PixelLabClient = class {
6375
6494
  });
6376
6495
  return validateResponse(AnimateSubmitSchema, raw, "animate-character");
6377
6496
  }
6497
+ /**
6498
+ * A full-body sprite in, a bust portrait out (or the reverse, unused
6499
+ * today). Job-based like a state or `1dir`, not the couple-generations
6500
+ * "conversion" the name suggests: the smallest `resultSize` (16px) billed
6501
+ * 20 generations live (docs/ENDPOINTS.md, "Characters, measured").
6502
+ */
6503
+ async createPortraitCharacterPro(args) {
6504
+ const raw = await this.request("/portrait-character-pro", {
6505
+ method: "POST",
6506
+ body: JSON.stringify({
6507
+ direction: args.direction,
6508
+ image: { type: "base64", base64: args.image.base64, format: args.image.format },
6509
+ ...args.view ? { view: args.view } : {},
6510
+ ...args.resultSize != null ? { result_size: args.resultSize } : {},
6511
+ ...args.seed != null ? { seed: args.seed } : {}
6512
+ })
6513
+ });
6514
+ return validateResponse(PortraitSubmitSchema, raw, "portrait-character-pro");
6515
+ }
6516
+ /** Throws PixelLabError(423) while the job is still drawing, like tiles and map objects. */
6517
+ async getPortraitCharacterJob(jobId) {
6518
+ const raw = await this.request(`/portrait-character-pro/${encodeURIComponent(jobId)}`);
6519
+ return validateResponse(PortraitJobSchema, raw, "portrait-character-pro/{id}");
6520
+ }
6521
+ /**
6522
+ * Attaches a portrait image to a character record. Synchronous and free
6523
+ * (0 generations, live); PixelLab does not link this to
6524
+ * `createPortraitCharacterPro` automatically, and `GET /characters/{id}`
6525
+ * never grows a portrait field, so this response's `url` is the only place
6526
+ * it appears.
6527
+ */
6528
+ async setCharacterPortrait(characterId, image) {
6529
+ const raw = await this.request(`/characters/${encodeURIComponent(characterId)}/portrait`, {
6530
+ method: "POST",
6531
+ body: JSON.stringify({ image: { type: "base64", base64: image.base64, format: image.format } })
6532
+ });
6533
+ return validateResponse(SetPortraitSchema, raw, "characters/{id}/portrait");
6534
+ }
6535
+ /**
6536
+ * Applies a reference outfit to 2–16 existing frames. Job-based like
6537
+ * everything else here, but the completed job returns the result frames
6538
+ * inline as base64 (`last_response.quantized_images`), not as storage
6539
+ * URLs — the one PixelLab character tool that works this way. A live
6540
+ * 2-frame, 92x92 job billed 20 generations (docs/ENDPOINTS.md).
6541
+ */
6542
+ async transferOutfitV2(args) {
6543
+ const image = (img) => ({
6544
+ image: { type: "base64", base64: img.base64, format: img.format },
6545
+ size: { width: img.width, height: img.height }
6546
+ });
6547
+ const raw = await this.request("/transfer-outfit-v2", {
6548
+ method: "POST",
6549
+ body: JSON.stringify({
6550
+ reference_image: image(args.referenceImage),
6551
+ frames: args.frames.map(image),
6552
+ image_size: args.imageSize,
6553
+ ...args.seed != null ? { seed: args.seed } : {},
6554
+ ...args.noBackground !== void 0 ? { no_background: args.noBackground } : {},
6555
+ ...args.additionalInstructions ? { additional_instructions: args.additionalInstructions } : {}
6556
+ })
6557
+ });
6558
+ return validateResponse(TransferOutfitSubmitSchema, raw, "transfer-outfit-v2");
6559
+ }
6378
6560
  async getCharacter(characterId) {
6379
6561
  const raw = await this.request(`/characters/${encodeURIComponent(characterId)}`);
6380
6562
  return validateResponse(CharacterSchema, raw, "characters/{id}");
@@ -6485,6 +6667,66 @@ var PixelLabClient = class {
6485
6667
  "generate-image-v2"
6486
6668
  );
6487
6669
  }
6670
+ /**
6671
+ * `/create-image-pro-flash`: one native pixel-art image from a
6672
+ * description, optionally styled after a style image with per-trait
6673
+ * toggles, on the same Pro Flash model `character`/`objectPro` use for
6674
+ * their `pro-flash` engine. Sizes run 16 to 256 per side in multiples of 4.
6675
+ * A plain background job; `pollRevision`'s generic reader takes the
6676
+ * finished image. The response's `source_image_id` is what a Pro Flash
6677
+ * character or object can reuse instead of an upload.
6678
+ */
6679
+ async createImageProFlash(args) {
6680
+ const body = {
6681
+ description: args.description,
6682
+ image_size: { width: args.width, height: args.height }
6683
+ };
6684
+ if (args.noBackground != null) body.no_background = args.noBackground;
6685
+ if (args.seed != null) body.seed = args.seed;
6686
+ if (args.styleImage) {
6687
+ body.style_image = { image: args.styleImage.image, size: { width: args.styleImage.width, height: args.styleImage.height } };
6688
+ const traits = args.styleTraits;
6689
+ if (traits) {
6690
+ body.style_options = {
6691
+ ...traits.palette !== void 0 ? { color_palette: traits.palette } : {},
6692
+ ...traits.outline !== void 0 ? { outline: traits.outline } : {},
6693
+ ...traits.detail !== void 0 ? { detail: traits.detail } : {},
6694
+ ...traits.shading !== void 0 ? { shading: traits.shading } : {}
6695
+ };
6696
+ }
6697
+ }
6698
+ return validateResponse(
6699
+ ProFlashImageSubmitSchema,
6700
+ await this.request("/create-image-pro-flash", { method: "POST", body: JSON.stringify(body) }),
6701
+ "create-image-pro-flash"
6702
+ );
6703
+ }
6704
+ /** `/edit-image-pro-flash`, text method: edit one image at its own size. The canvas never grows or resizes. */
6705
+ async editImageProFlash(args) {
6706
+ const body = { image: args.image, method: "text", description: args.description };
6707
+ if (args.noBackground != null) body.no_background = args.noBackground;
6708
+ if (args.seed != null) body.seed = args.seed;
6709
+ return validateResponse(
6710
+ ProFlashImageSubmitSchema,
6711
+ await this.request("/edit-image-pro-flash", { method: "POST", body: JSON.stringify(body) }),
6712
+ "edit-image-pro-flash"
6713
+ );
6714
+ }
6715
+ /**
6716
+ * `/inpaint-image-pro-flash`: replace only the white pixels of a same-size
6717
+ * black/white mask, keeping everything else exactly. `Modify current
6718
+ * layer` (the endpoint's default) returns the full composite.
6719
+ */
6720
+ async inpaintImageProFlash(args) {
6721
+ const body = { image: args.image, mask_image: args.maskImage, description: args.description };
6722
+ if (args.noBackground != null) body.no_background = args.noBackground;
6723
+ if (args.seed != null) body.seed = args.seed;
6724
+ return validateResponse(
6725
+ ProFlashImageSubmitSchema,
6726
+ await this.request("/inpaint-image-pro-flash", { method: "POST", body: JSON.stringify(body) }),
6727
+ "inpaint-image-pro-flash"
6728
+ );
6729
+ }
6488
6730
  /**
6489
6731
  * `/generate-ui-v2` ("Generate UI (Pro)"): one UI element (a button, a
6490
6732
  * health bar, an inventory slot, a dialogue box) from a description,
@@ -6794,7 +7036,10 @@ function characterCost(spec) {
6794
7036
  if (animation.mode === "v3") return Math.max(1, Math.ceil(px * animation.frames / 65536));
6795
7037
  return generationCost(spec.width, spec.height, "1dir");
6796
7038
  }
6797
- if (character.kind === "state") return generationCost(spec.width, spec.height, "1dir");
7039
+ if (character.kind === "state" || character.kind === "portrait") return generationCost(spec.width, spec.height, "1dir");
7040
+ if (character.kind === "outfit") {
7041
+ return 20;
7042
+ }
6798
7043
  if (character.mode === "standard") return 1;
6799
7044
  if (character.mode === "v3") return 1 + Math.max(1, Math.ceil(px * 8 / 65536));
6800
7045
  if (character.mode === "pro-flash") {
@@ -6807,8 +7052,11 @@ function proFlashCharacterCost(width, height, fromReference) {
6807
7052
  const side = Math.max(32, Math.ceil(Math.max(width, height) / 4) * 4);
6808
7053
  const rotations = Math.max(1, Math.ceil(side * side * 8 / 65536));
6809
7054
  if (fromReference) return rotations;
6810
- const image = side <= 96 ? 5 : side <= 208 ? 6 : 9;
6811
- return image + rotations;
7055
+ return proFlashImageCost(side, side) + rotations;
7056
+ }
7057
+ function proFlashImageCost(width, height) {
7058
+ const side = Math.max(width, height);
7059
+ return side <= 96 ? 5 : side <= 208 ? 6 : 9;
6812
7060
  }
6813
7061
  function characterAnimationName(spec) {
6814
7062
  return `pixelkiln:${spec.styleId}/${spec.assetId}`;
@@ -6884,7 +7132,7 @@ var PixelLabProvider = class _PixelLabProvider {
6884
7132
  return new _PixelLabProvider(new PixelLabClient("download-only"));
6885
7133
  }
6886
7134
  supports(generator) {
6887
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement";
7135
+ 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";
6888
7136
  }
6889
7137
  /**
6890
7138
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
@@ -6911,15 +7159,18 @@ var PixelLabProvider = class _PixelLabProvider {
6911
7159
  return { spacingMs: 2500, maxInFlight: 8 };
6912
7160
  }
6913
7161
  /**
6914
- * Where synchronous pixflux results are parked between `submit` and `fetch`.
7162
+ * Where synchronous pixflux results (or an outfit transfer's inline
7163
+ * frames) are parked between `submit`/poll and `fetch`.
6915
7164
  *
6916
- * pixflux returns the PNG inline rather than a job id, but the pipeline is
6917
- * built around submit → poll → fetch running as separate commands. Writing
6918
- * the bytes to a known path keeps that model intact: the "job id" is the
6919
- * filename, polling is an existence check, and downloading is a file read.
7165
+ * pixflux returns the PNG inline rather than a job id, and an outfit
7166
+ * transfer's completed job returns its frames inline too, but the
7167
+ * pipeline is built around submit → poll → fetch running as separate
7168
+ * commands. Writing the bytes to a known path keeps that model intact:
7169
+ * the id is the filename, polling is an existence check, and downloading
7170
+ * is a file read.
6920
7171
  */
6921
- static cacheDir() {
6922
- const dir = path12.join(os.tmpdir(), "pixelkiln-pixflux");
7172
+ static cacheDir(namespace = "pixflux") {
7173
+ const dir = path12.join(os.tmpdir(), `pixelkiln-${namespace}`);
6923
7174
  mkdirSync(dir, { recursive: true });
6924
7175
  return dir;
6925
7176
  }
@@ -6943,6 +7194,13 @@ var PixelLabProvider = class _PixelLabProvider {
6943
7194
  const base = Math.max(1, Math.ceil(width * height * frames / 65536));
6944
7195
  return { unit: "generations", amount: spec.revision.enhancePrompt ? base + 0.05 : base, candidates: 1 };
6945
7196
  }
7197
+ if (spec.revision?.engine === "pro-flash") {
7198
+ return {
7199
+ unit: "generations",
7200
+ amount: proFlashImageCost(spec.revision.sourceWidth ?? spec.width, spec.revision.sourceHeight ?? spec.height),
7201
+ candidates: 1
7202
+ };
7203
+ }
6946
7204
  if (spec.revision) {
6947
7205
  const width = spec.revision.sourceWidth ?? spec.width;
6948
7206
  const height = spec.revision.sourceHeight ?? spec.height;
@@ -6957,6 +7215,9 @@ var PixelLabProvider = class _PixelLabProvider {
6957
7215
  if (spec.generator === "objectPro") {
6958
7216
  return { unit: "generations", amount: objectProCost(spec), candidates: 1 };
6959
7217
  }
7218
+ if (spec.generator === "imageProFlash") {
7219
+ return { unit: "generations", amount: proFlashImageCost(spec.width, spec.height), candidates: 1 };
7220
+ }
6960
7221
  return {
6961
7222
  unit: "generations",
6962
7223
  amount: generationCost(spec.width, spec.height, spec.generator),
@@ -6970,7 +7231,17 @@ var PixelLabProvider = class _PixelLabProvider {
6970
7231
  `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`
6971
7232
  );
6972
7233
  }
6973
- if (spec.revision?.mode === "inpaint") {
7234
+ if (spec.revision?.engine === "pro-flash") {
7235
+ const { sourceWidth: width, sourceHeight: height, mode: mode2 } = spec.revision;
7236
+ if (width != null && height != null && (width < 32 || height < 32 || width > 256 || height > 256 || width % 4 || height % 4)) {
7237
+ throw new Error(
7238
+ `PixelLab pro-flash ${mode2} source is ${width}x${height}; the API takes 32 to 256 pixels per side, in multiples of 4`
7239
+ );
7240
+ }
7241
+ if (spec.revision.strength != null) {
7242
+ throw new Error(`PixelLab pro-flash ${mode2} takes no strength; the edit applies in full`);
7243
+ }
7244
+ } else if (spec.revision?.mode === "inpaint") {
6974
7245
  const { sourceWidth: width, sourceHeight: height } = spec.revision;
6975
7246
  if (width != null && height != null && (width < 32 || height < 32 || width > 512 || height > 512)) {
6976
7247
  throw new Error(`PixelLab inpaint source is ${width}x${height}; the API takes 32 to 512 pixels per side`);
@@ -7051,6 +7322,19 @@ var PixelLabProvider = class _PixelLabProvider {
7051
7322
  `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)`
7052
7323
  );
7053
7324
  }
7325
+ if (spec.generator === "imageProFlash") {
7326
+ if (spec.width < 16 || spec.height < 16 || spec.width > 256 || spec.height > 256 || spec.width % 4 || spec.height % 4) {
7327
+ throw new Error(
7328
+ `PixelLab imageProFlash is ${spec.width}x${spec.height}; the API takes 16 to 256 pixels per side, in multiples of 4`
7329
+ );
7330
+ }
7331
+ if (styleImages.length > 1) {
7332
+ throw new Error("PixelLab imageProFlash takes one style image; list at most one styleImages entry");
7333
+ }
7334
+ if (spec.styleTraits && !styleImages.length) {
7335
+ throw new Error("PixelLab imageProFlash: styleTraits choose what a style image lends; add a styleImages entry or drop them");
7336
+ }
7337
+ }
7054
7338
  if (spec.generator === "uiElement" && (spec.width < 16 || spec.height < 16 || spec.width > 792 || spec.height > 688)) {
7055
7339
  throw new Error(
7056
7340
  `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)`
@@ -7179,6 +7463,12 @@ var PixelLabProvider = class _PixelLabProvider {
7179
7463
  const character = spec.character;
7180
7464
  if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
7181
7465
  const label = `${spec.styleId}/${spec.assetId}`;
7466
+ if (character.kind === "outfit") {
7467
+ const reference = character.outfit.reference;
7468
+ if (reference.width < 32 || reference.width > 256 || reference.height < 32 || reference.height > 256) {
7469
+ throw new Error(`${label}: outfit reference is ${reference.width}x${reference.height}; PixelLab transfer-outfit-v2 takes 32 to 256px per side`);
7470
+ }
7471
+ }
7182
7472
  if (character.kind === "base" && character.proportions !== void 0) {
7183
7473
  if (character.mode !== "standard") {
7184
7474
  throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
@@ -7190,7 +7480,7 @@ var PixelLabProvider = class _PixelLabProvider {
7190
7480
  if (character.kind === "base" && character.enhancePrompt !== void 0 && character.mode !== "v3") {
7191
7481
  throw new Error(`${label}: enhancePrompt applies to v3 bases; the ${character.mode} engine does not take it`);
7192
7482
  }
7193
- if (character.animation) {
7483
+ if (character.animation && character.kind !== "outfit") {
7194
7484
  const { startFrame, endFrame } = character.animation;
7195
7485
  for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
7196
7486
  if (image && (image.width > 256 || image.height > 256)) {
@@ -7271,9 +7561,11 @@ var PixelLabProvider = class _PixelLabProvider {
7271
7561
  if (character.styleTraits && character.mode !== "pro-flash") {
7272
7562
  throw new Error(`${label}: styleTraits choose what a pro-flash style image lends; the ${character.mode} engine does not take them`);
7273
7563
  }
7274
- const maxSize = character.mode === "v3" || character.mode === "pro-flash" ? 256 : 128;
7275
- if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
7276
- throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
7564
+ if (character.kind !== "portrait") {
7565
+ const maxSize = character.mode === "v3" || character.mode === "pro-flash" ? 256 : 128;
7566
+ if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
7567
+ throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
7568
+ }
7277
7569
  }
7278
7570
  if (character.mode === "pro-flash" && character.kind === "base" && (spec.width % 4 || spec.height % 4)) {
7279
7571
  throw new Error(`PixelLab pro-flash characters are sized in multiples of 4 pixels; ${spec.width}x${spec.height} is not`);
@@ -7282,9 +7574,11 @@ var PixelLabProvider = class _PixelLabProvider {
7282
7574
  if (!templates.includes(character.template)) {
7283
7575
  throw new Error(`PixelLab ${character.mode} character template must be one of: ${templates.join(", ")}`);
7284
7576
  }
7285
- const views = character.mode === "standard" ? ["low top-down", "high top-down", "side", "perspective", "oblique"] : ["low top-down", "high top-down", "side"];
7577
+ 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"];
7286
7578
  if (!views.includes(spec.view)) {
7287
- throw new Error(`PixelLab ${character.mode} character view must be one of: ${views.join(", ")}`);
7579
+ throw new Error(
7580
+ character.kind === "portrait" ? `PixelLab portrait view must be one of: ${views.join(", ")}` : `PixelLab ${character.mode} character view must be one of: ${views.join(", ")}`
7581
+ );
7288
7582
  }
7289
7583
  if (spec.view === "oblique" && character.directions !== 4) {
7290
7584
  throw new Error("PixelLab oblique characters are 4-direction only");
@@ -7292,7 +7586,7 @@ var PixelLabProvider = class _PixelLabProvider {
7292
7586
  if (character.kind !== "base" && !character.parentAssetId) {
7293
7587
  throw new Error(`${spec.styleId}/${spec.assetId} is a ${character.kind} with no parent`);
7294
7588
  }
7295
- if (character.animation) {
7589
+ if (character.animation && character.kind !== "outfit") {
7296
7590
  const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
7297
7591
  if (!allowed.includes(character.animation.direction)) {
7298
7592
  throw new Error(
@@ -7394,10 +7688,48 @@ var PixelLabProvider = class _PixelLabProvider {
7394
7688
  concept: character.concept ? readPose(spec, "concept image", character.concept) : void 0,
7395
7689
  styleCharacterId: character.styleAnchor ? requireStyleObjectId(spec, context) : void 0,
7396
7690
  styleReferenceSize: styleImage ? { width: styleImage.width, height: styleImage.height } : void 0,
7397
- styleTraits: character.styleTraits
7691
+ styleTraits: character.styleTraits,
7692
+ sourceImageId: character.mode === "pro-flash" ? context?.referenceSourceImageId : void 0
7398
7693
  });
7399
7694
  return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
7400
7695
  }
7696
+ if (character.kind === "outfit") {
7697
+ const parentOutputs = context?.parentOutputs ?? [];
7698
+ if (parentOutputs.length < 2) {
7699
+ throw new Error(
7700
+ `${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`
7701
+ );
7702
+ }
7703
+ const frames = parentOutputs.slice(0, 16).map((output) => {
7704
+ const bytes = readFileSync2(output.path);
7705
+ const dims = imageMetadata(bytes);
7706
+ if (!dims) throw new Error(`${spec.styleId}/${spec.assetId}: outfit source frame is not a readable PNG: ${output.path}`);
7707
+ return { base64: bytes.toString("base64"), format: "png", width: dims.width, height: dims.height };
7708
+ });
7709
+ const outfit = character.outfit;
7710
+ const referenceBytes = readFileSync2(outfit.reference.path);
7711
+ if (sha256(referenceBytes) !== outfit.reference.sha256) {
7712
+ throw new Error(`${spec.styleId}/${spec.assetId}: outfit reference changed after the manifest was resolved: ${outfit.reference.path}`);
7713
+ }
7714
+ const res2 = await this.client.transferOutfitV2({
7715
+ referenceImage: { base64: referenceBytes.toString("base64"), format: "png", width: outfit.reference.width, height: outfit.reference.height },
7716
+ frames,
7717
+ imageSize: { width: spec.width, height: spec.height },
7718
+ seed: spec.seed,
7719
+ noBackground: spec.noBackground,
7720
+ additionalInstructions: outfit.additionalInstructions
7721
+ });
7722
+ return {
7723
+ jobId: res2.background_job_id,
7724
+ metadata: {
7725
+ character: {
7726
+ kind: "outfit",
7727
+ backgroundJobId: res2.background_job_id,
7728
+ frameRoles: parentOutputs.slice(0, 16).map((output) => output.role ?? null)
7729
+ }
7730
+ }
7731
+ };
7732
+ }
7401
7733
  const parentId = context?.parentObjectId;
7402
7734
  if (!parentId) {
7403
7735
  throw new Error(
@@ -7416,6 +7748,24 @@ var PixelLabProvider = class _PixelLabProvider {
7416
7748
  });
7417
7749
  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 } } };
7418
7750
  }
7751
+ if (character.kind === "portrait") {
7752
+ const parentFile = character.parentFile;
7753
+ const bytes = readFileSync2(parentFile);
7754
+ if (character.parentSha256 && sha256(bytes) !== character.parentSha256) {
7755
+ throw new Error(`${spec.styleId}/${spec.assetId}: portrait source changed after the manifest was resolved: ${parentFile}`);
7756
+ }
7757
+ const res2 = await this.client.createPortraitCharacterPro({
7758
+ direction: "character_to_portrait",
7759
+ image: { base64: bytes.toString("base64"), format: "png" },
7760
+ view: spec.view,
7761
+ resultSize: character.portrait.size,
7762
+ seed: spec.seed
7763
+ });
7764
+ return {
7765
+ jobId: res2.background_job_id,
7766
+ metadata: { character: { kind: "portrait", parentCharacterId: parentId, backgroundJobId: res2.background_job_id } }
7767
+ };
7768
+ }
7419
7769
  const animation = character.animation;
7420
7770
  const name = characterAnimationName(spec);
7421
7771
  const replaced = context?.replacedMetadata?.character ?? {};
@@ -7474,7 +7824,8 @@ var PixelLabProvider = class _PixelLabProvider {
7474
7824
  reference: object.reference ? readPose(spec, "reference sprite", object.reference) : void 0,
7475
7825
  styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
7476
7826
  styleReferenceSize: styleImage ? { width: styleImage.width, height: styleImage.height } : void 0,
7477
- styleTraits: object.styleTraits
7827
+ styleTraits: object.styleTraits,
7828
+ sourceImageId: context?.referenceSourceImageId
7478
7829
  });
7479
7830
  return { jobId: res2.object_id, metadata: { objectPro: { kind: "base", objectId: res2.object_id, directions: object.directions, backgroundJobId: res2.background_job_id } } };
7480
7831
  }
@@ -7529,6 +7880,9 @@ var PixelLabProvider = class _PixelLabProvider {
7529
7880
  async pollCharacter(jobId, context) {
7530
7881
  const animation = decodeAnimationJob(jobId);
7531
7882
  if (animation) return this.pollCharacterAnimation(animation, context);
7883
+ const kind = context?.metadata?.character?.kind ?? context?.spec?.character?.kind;
7884
+ if (kind === "portrait") return this.pollCharacterPortrait(jobId, context);
7885
+ if (kind === "outfit") return this.pollCharacterOutfit(jobId, context);
7532
7886
  const character = await this.client.getCharacter(jobId);
7533
7887
  if (character.status === "failed") return { status: "failed", error: "character generation failed upstream" };
7534
7888
  if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
@@ -7554,6 +7908,66 @@ var PixelLabProvider = class _PixelLabProvider {
7554
7908
  billed: await this.billedForJob(backgroundJobId)
7555
7909
  };
7556
7910
  }
7911
+ /**
7912
+ * `jobId` here is `portrait-character-pro`'s own background job id, not a
7913
+ * character id: the endpoint converts an arbitrary image and never touches
7914
+ * the character record. Once the portrait itself is drawn, this also
7915
+ * attaches it to the parent's character record with `SetPortrait` — a
7916
+ * separate, free call PixelLab does not make automatically (docs/PIXELLAB.md).
7917
+ */
7918
+ async pollCharacterPortrait(jobId, context) {
7919
+ const meta = context?.metadata?.character;
7920
+ try {
7921
+ const job = await this.client.getPortraitCharacterJob(jobId);
7922
+ if (job.status === "failed") return { status: "failed", error: "portrait generation failed upstream" };
7923
+ if (job.status !== "completed" || !job.download_url) return { status: "processing" };
7924
+ if (meta?.parentCharacterId) {
7925
+ const bytes = await this.client.download(job.download_url);
7926
+ await this.client.setCharacterPortrait(meta.parentCharacterId, { base64: bytes.toString("base64"), format: "png" });
7927
+ }
7928
+ return {
7929
+ status: "ready",
7930
+ objectId: jobId,
7931
+ sourceUrl: job.download_url,
7932
+ sources: [{ url: job.download_url }],
7933
+ billed: await this.billedForJob(meta?.backgroundJobId)
7934
+ };
7935
+ } catch (err) {
7936
+ if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
7937
+ throw err;
7938
+ }
7939
+ }
7940
+ /**
7941
+ * No dedicated status endpoint exists for `transfer-outfit-v2`, unlike
7942
+ * portraits and tiles: the background job is the only place to poll. A
7943
+ * completed job returns its frames inline as base64
7944
+ * (`last_response.quantized_images`), not as storage URLs, so they are
7945
+ * cached locally the way a synchronous pixflux result is, and matched back
7946
+ * to the source frames' own roles recorded at submit time.
7947
+ */
7948
+ async pollCharacterOutfit(jobId, context) {
7949
+ const meta = context?.metadata?.character;
7950
+ const job = await this.client.getBackgroundJob(jobId);
7951
+ if (job.status === "failed") return { status: "failed", error: "outfit transfer failed upstream" };
7952
+ if (job.status !== "completed") return { status: "processing" };
7953
+ const images = job.last_response?.quantized_images;
7954
+ if (!images?.length) return { status: "failed", error: "outfit transfer completed with no result frames" };
7955
+ const dir = _PixelLabProvider.cacheDir("outfit");
7956
+ const sources = images.map((image, index) => {
7957
+ if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
7958
+ const role = meta?.frameRoles?.[index] ?? void 0;
7959
+ const file = path12.join(dir, `${jobId}-${index}.png`);
7960
+ writeFileSync(file, Buffer.from(image.base64, "base64"));
7961
+ return { url: `file://${file}`, ...role ? { role } : {} };
7962
+ });
7963
+ return {
7964
+ status: "ready",
7965
+ objectId: jobId,
7966
+ sourceUrl: sources[0].url,
7967
+ sources,
7968
+ billed: await this.billedForJob(meta?.backgroundJobId)
7969
+ };
7970
+ }
7557
7971
  /**
7558
7972
  * The background job is the authoritative record of one direction: it
7559
7973
  * completes with the frame URLs, the animation id, and the direction, and
@@ -7710,6 +8124,19 @@ var PixelLabProvider = class _PixelLabProvider {
7710
8124
  });
7711
8125
  return { jobId: res2.ui_asset_id, metadata: { backgroundJobId: res2.background_job_id } };
7712
8126
  }
8127
+ if (spec.generator === "imageProFlash") {
8128
+ const style = styleImages[0];
8129
+ const res2 = await this.client.createImageProFlash({
8130
+ description: spec.prompt,
8131
+ width: spec.width,
8132
+ height: spec.height,
8133
+ noBackground: spec.noBackground,
8134
+ seed: spec.seed,
8135
+ styleImage: style ? { image: { base64: style.base64, format: style.format }, width: style.width, height: style.height } : void 0,
8136
+ styleTraits: spec.styleTraits
8137
+ });
8138
+ return { jobId: res2.background_job_id, metadata: { imageProFlash: proFlashRecord(res2) } };
8139
+ }
7713
8140
  if (spec.generator === "uiElement") {
7714
8141
  const concept = styleImages[0];
7715
8142
  const res2 = await this.client.generateUiV2({
@@ -7863,6 +8290,32 @@ var PixelLabProvider = class _PixelLabProvider {
7863
8290
  const width = revision.sourceWidth;
7864
8291
  const height = revision.sourceHeight;
7865
8292
  const roles = revision.sourceMembers?.map((member) => member.role);
8293
+ if (revision.engine === "pro-flash") {
8294
+ if (revision.mode === "inpaint") {
8295
+ if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
8296
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
8297
+ }
8298
+ const maskBytes = readFileSync2(revision.maskFile);
8299
+ if (sha256(maskBytes) !== revision.maskSha256) {
8300
+ throw new Error(`${spec.styleId}/${spec.assetId}: revision mask changed after the manifest was resolved`);
8301
+ }
8302
+ const res3 = await this.client.inpaintImageProFlash({
8303
+ image,
8304
+ maskImage: { base64: maskBytes.toString("base64"), format: revision.maskFormat },
8305
+ description: spec.prompt,
8306
+ noBackground: spec.noBackground,
8307
+ seed: spec.seed
8308
+ });
8309
+ return { jobId: res3.background_job_id, metadata: { imageProFlash: proFlashRecord(res3) } };
8310
+ }
8311
+ const res2 = await this.client.editImageProFlash({
8312
+ image,
8313
+ description: spec.prompt,
8314
+ noBackground: spec.noBackground,
8315
+ seed: spec.seed
8316
+ });
8317
+ return { jobId: res2.background_job_id, metadata: { imageProFlash: proFlashRecord(res2) } };
8318
+ }
7866
8319
  if (revision.mode === "inpaint") {
7867
8320
  if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
7868
8321
  throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
@@ -8027,6 +8480,7 @@ var PixelLabProvider = class _PixelLabProvider {
8027
8480
  return this.pollAnimateRevision(jobId, context);
8028
8481
  }
8029
8482
  if (context?.spec?.revision) return this.pollRevision(jobId);
8483
+ if (generator === "imageProFlash") return this.pollRevision(jobId);
8030
8484
  if (generator === "pixflux") {
8031
8485
  const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
8032
8486
  if (existsSync7(file)) {
@@ -8607,6 +9061,13 @@ function readPose(spec, what, image) {
8607
9061
  }
8608
9062
  return { base64: bytes.toString("base64"), format: image.format };
8609
9063
  }
9064
+ function proFlashRecord(res) {
9065
+ return {
9066
+ imageId: res.image_id,
9067
+ sourceImageId: res.source_image_id,
9068
+ ...res.estimated_generations != null ? { estimatedGenerations: res.estimated_generations } : {}
9069
+ };
9070
+ }
8610
9071
  function editAnimationFrameCeiling(width, height) {
8611
9072
  const side = Math.max(width, height);
8612
9073
  if (side <= 64) return 16;
@@ -9751,7 +10212,7 @@ ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
9751
10212
  return { manifest: parsed.data, root: path13.dirname(abs), path: abs, ...loopFamilies };
9752
10213
  }
9753
10214
  function parentAssetId(asset) {
9754
- return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
10215
+ return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
9755
10216
  }
9756
10217
  function formatManifestIssues(issues) {
9757
10218
  return issues.flatMap((issue) => {
@@ -9918,9 +10379,9 @@ async function resolveSpecs(loaded, filter) {
9918
10379
  height = styleImageDimensions.length ? Math.max(...styleImageDimensions.map((img) => img.height)) : style.tileSize ?? 32;
9919
10380
  size = Math.max(width, height);
9920
10381
  } else if (generator === "character") {
9921
- size = asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
9922
- width = asset.state?.canvas?.width ?? size;
9923
- height = asset.state?.canvas?.height ?? size;
10382
+ size = asset.portrait ? asset.portrait.size : asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
10383
+ width = asset.portrait?.size ?? asset.state?.canvas?.width ?? size;
10384
+ height = asset.portrait?.size ?? asset.state?.canvas?.height ?? size;
9924
10385
  } else if (generator === "terrain") {
9925
10386
  size = style.terrainTileSize ?? 16;
9926
10387
  width = size;
@@ -9947,12 +10408,17 @@ async function resolveSpecs(loaded, filter) {
9947
10408
  `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
9948
10409
  );
9949
10410
  }
10411
+ if ((asset.portrait || asset.outfit) && generator !== "character") {
10412
+ throw new Error(
10413
+ `assets.${assetId}: ${asset.portrait ? "portrait" : "outfit"} needs a character style; "${styleId}" generates ${generator}`
10414
+ );
10415
+ }
9950
10416
  if ((asset.pieces || asset.elements) && generator !== "uiAsset") {
9951
10417
  throw new Error(
9952
10418
  `assets.${assetId}: ${asset.pieces ? "pieces" : "elements"} needs a uiAsset style; "${styleId}" generates ${generator}`
9953
10419
  );
9954
10420
  }
9955
- const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
10421
+ const characterKind = asset.animation ? "animation" : asset.state ? "state" : asset.portrait ? "portrait" : "base";
9956
10422
  const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
9957
10423
  let terrainLowerDescription;
9958
10424
  let terrainUpperDescription;
@@ -10001,6 +10467,7 @@ async function resolveSpecs(loaded, filter) {
10001
10467
  uiPieces: generator === "uiAsset" ? asset.pieces : void 0,
10002
10468
  uiElements: generator === "uiAsset" ? asset.elements : void 0,
10003
10469
  uiColorPalette: generator === "uiAsset" || generator === "uiElement" ? style.uiColorPalette : void 0,
10470
+ ...generator === "imageProFlash" && style.styleTraits ? { styleTraits: style.styleTraits } : {},
10004
10471
  palette: style.palette,
10005
10472
  enforcePalette: style.enforcePalette,
10006
10473
  noBackground: style.noBackground,
@@ -10044,7 +10511,9 @@ async function resolveSpecs(loaded, filter) {
10044
10511
  // its parent's pixels, not a new character: giving it a base shape
10045
10512
  // would have packing, the gallery, and adoption treat it as one.
10046
10513
  ...generator === "character" && !asset.revision ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
10047
- ...generator === "objectPro" && !asset.revision ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
10514
+ // A portrait needs a character style (refused above), so an
10515
+ // objectPro asset is only ever a base, state, or animation.
10516
+ ...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
10048
10517
  cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
10049
10518
  costUnit: "generations",
10050
10519
  candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
@@ -10155,7 +10624,7 @@ async function resolveSpecs(loaded, filter) {
10155
10624
  styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: existsSync8(file) ? await sha256File(file) : null }
10156
10625
  };
10157
10626
  }
10158
- const characterParent = asset.state?.of ?? asset.animation?.of;
10627
+ const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
10159
10628
  if (resolved.character && characterParent) {
10160
10629
  const parentSpec = await finalize(characterParent);
10161
10630
  const parentDirections = parentSpec.character?.directions ?? 8;
@@ -10188,6 +10657,38 @@ async function resolveSpecs(loaded, filter) {
10188
10657
  directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
10189
10658
  };
10190
10659
  }
10660
+ if (asset.outfit) {
10661
+ if (asset.outfit.of === assetId) throw new Error(`assets.${assetId}: an asset cannot re-clothe itself`);
10662
+ const sourceSpec = await finalize(asset.outfit.of);
10663
+ if (sourceSpec.character?.kind !== "animation") {
10664
+ throw new Error(`assets.${assetId}: outfit "${asset.outfit.of}" is not a character loop`);
10665
+ }
10666
+ const referenceImage = await loadStyleImage(asset.outfit.reference, "Outfit reference image");
10667
+ resolved.character = {
10668
+ kind: "outfit",
10669
+ mode: sourceSpec.character.mode,
10670
+ directions: sourceSpec.character.directions,
10671
+ template: sourceSpec.character.template,
10672
+ parentAssetId: asset.outfit.of,
10673
+ parentSpec: sourceSpec,
10674
+ animation: sourceSpec.character.animation,
10675
+ outfit: {
10676
+ reference: {
10677
+ path: path13.resolve(root, asset.outfit.reference),
10678
+ sha256: referenceImage.hash,
10679
+ width: referenceImage.width,
10680
+ height: referenceImage.height,
10681
+ format: referenceImage.format
10682
+ },
10683
+ ...asset.outfit.additionalInstructions ? { additionalInstructions: asset.outfit.additionalInstructions } : {}
10684
+ }
10685
+ };
10686
+ resolved.generator = sourceSpec.generator;
10687
+ resolved.width = sourceSpec.width;
10688
+ resolved.height = sourceSpec.height;
10689
+ resolved.size = sourceSpec.size;
10690
+ resolved.prompt = "";
10691
+ }
10191
10692
  if (asset.revision) {
10192
10693
  if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
10193
10694
  throw new Error(
@@ -10250,7 +10751,8 @@ async function resolveSpecs(loaded, filter) {
10250
10751
  lastFrameFormat: lastFrame?.format ?? null
10251
10752
  } : {},
10252
10753
  ...asset.revision.direction ? { direction: asset.revision.direction } : {},
10253
- ...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt }
10754
+ ...asset.revision.enhancePrompt == null ? {} : { enhancePrompt: asset.revision.enhancePrompt },
10755
+ ...asset.revision.engine ? { engine: asset.revision.engine } : {}
10254
10756
  };
10255
10757
  }
10256
10758
  if (asset.batch) {
@@ -10458,6 +10960,9 @@ async function resolveCharacterShape(asset, style, kind, files) {
10458
10960
  ...asset.state.canvas ? { canvas: asset.state.canvas } : {}
10459
10961
  };
10460
10962
  }
10963
+ if (asset.portrait) {
10964
+ shape.portrait = { size: asset.portrait.size };
10965
+ }
10461
10966
  if (asset.animation) {
10462
10967
  const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
10463
10968
  const pose = async (rel, what) => {
@@ -13824,7 +14329,7 @@ function describeCharacter(spec, entry) {
13824
14329
  };
13825
14330
  }
13826
14331
  if (entry?.generator === "character") {
13827
- const kind = recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
14332
+ const kind = recordedCharacter?.kind === "portrait" ? "portrait" : recordedCharacter?.kind === "outfit" ? "outfit" : recordedCharacter?.kind === "animation" || entry.outputs.some((o) => o.role?.startsWith("frame-")) ? "animation" : "base";
13828
14333
  return {
13829
14334
  generator: "character",
13830
14335
  kind,
@@ -14185,7 +14690,9 @@ async function buildGallerySnapshot(opts) {
14185
14690
  characters: style?.generator === "character" || style?.generator === "objectPro" ? {
14186
14691
  bases: styleItems.filter((item) => item.character?.kind === "base").length,
14187
14692
  states: styleItems.filter((item) => item.character?.kind === "state").length,
14188
- animations: styleItems.filter((item) => item.character?.kind === "animation").length
14693
+ animations: styleItems.filter((item) => item.character?.kind === "animation").length,
14694
+ portraits: styleItems.filter((item) => item.character?.kind === "portrait").length,
14695
+ outfits: styleItems.filter((item) => item.character?.kind === "outfit").length
14189
14696
  } : null,
14190
14697
  tags: style?.tags ?? [],
14191
14698
  extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
@@ -14962,6 +15469,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
14962
15469
  const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
14963
15470
  const submittedAt = resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString();
14964
15471
  const history = historyAfterReplacing(previousEntry, historyLimit(loaded.manifest), submittedAt ?? (/* @__PURE__ */ new Date()).toISOString());
15472
+ const outfitParentEntry = spec.character?.kind === "outfit" && spec.character.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
14965
15473
  upsert(lock, key, {
14966
15474
  styleId: spec.styleId,
14967
15475
  assetId: spec.assetId,
@@ -14985,7 +15493,13 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
14985
15493
  ...spec.revision.fps == null ? {} : { fps: spec.revision.fps },
14986
15494
  ...spec.revision.lastFrameSha256 ? { lastFrameSha256: spec.revision.lastFrameSha256 } : {},
14987
15495
  ...spec.revision.direction ? { direction: spec.revision.direction } : {},
14988
- ...spec.revision.enhancePrompt == null ? {} : { enhancePrompt: spec.revision.enhancePrompt }
15496
+ ...spec.revision.enhancePrompt == null ? {} : { enhancePrompt: spec.revision.enhancePrompt },
15497
+ ...spec.revision.engine ? { engine: spec.revision.engine } : {}
15498
+ } : null,
15499
+ outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
15500
+ sourceAssetId: spec.character.parentAssetId,
15501
+ sourceSha256: mirrorSourceHash(outfitParentEntry),
15502
+ referenceSha256: spec.character.outfit.reference.sha256
14989
15503
  } : null,
14990
15504
  status: "pending",
14991
15505
  jobId: previousJobId ?? null,
@@ -15020,10 +15534,17 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
15020
15534
  const anchor = spec.character?.styleAnchor;
15021
15535
  const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
15022
15536
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
15537
+ const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
15538
+ path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
15539
+ role: output.role
15540
+ })) : void 0;
15541
+ const referenceSourceImageId = ownedReferenceImageId(spec, lock, provider.id);
15023
15542
  const { jobId, metadata } = await provider.submit(spec, refs, {
15024
15543
  ...previousJobId ? { previousJobId } : {},
15544
+ ...referenceSourceImageId ? { referenceSourceImageId } : {},
15025
15545
  ...parentEntry?.objectId ? { parentObjectId: parentEntry.objectId } : {},
15026
15546
  ...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
15547
+ ...parentOutputs ? { parentOutputs } : {},
15027
15548
  ...replacedMetadata ? { replacedMetadata } : {},
15028
15549
  ...previousMetadata ? { previousMetadata } : {},
15029
15550
  checkpoint: async (checkpoint) => {
@@ -15087,6 +15608,17 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
15087
15608
  }
15088
15609
  return { submitted, failed, spent, unit };
15089
15610
  }
15611
+ function ownedReferenceImageId(spec, lock, providerId) {
15612
+ 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;
15613
+ if (!sha2562) return void 0;
15614
+ for (const entry of Object.values(lock.entries)) {
15615
+ if (entry.provider !== providerId || entry.generator !== "imageProFlash" || entry.status !== "downloaded") continue;
15616
+ if (entry.outputs.length !== 1 || entry.outputs[0].sha256 !== sha2562) continue;
15617
+ const recorded = entry.providerMetadata?.[providerId]?.imageProFlash;
15618
+ if (typeof recorded?.sourceImageId === "string" && recorded.sourceImageId) return recorded.sourceImageId;
15619
+ }
15620
+ return void 0;
15621
+ }
15090
15622
 
15091
15623
  // src/pick/server.ts
15092
15624
  import { existsSync as existsSync21 } from "fs";