pixelkiln 0.83.0 → 0.84.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
@@ -334,6 +334,7 @@ var RevisionModeSchema = import_zod.z.enum([
334
334
  "outpaint",
335
335
  "reduce-colors",
336
336
  "correct-pixelart",
337
+ "remove-background",
337
338
  "animate",
338
339
  "animate-pixminimax",
339
340
  "animate-skeleton",
@@ -665,7 +666,22 @@ var RevisionSchema = import_zod.z.object({
665
666
  * `interpolate`: required, the ending keyframe; the parent is the start.
666
667
  */
667
668
  lastFrame: import_zod.z.string().min(1).optional(),
668
- /** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
669
+ /**
670
+ * `animate`/`animate-pixminimax` only: colour de-flicker sensitivity.
671
+ * Frames whose foreground drifts from the first frame beyond this are
672
+ * corrected toward it; 0 corrects every frame and higher values correct
673
+ * fewer. Omit for PixelLab's default.
674
+ */
675
+ driftThreshold: import_zod.z.number().min(0).optional(),
676
+ /**
677
+ * `animate-pixminimax` only: one noun phrase for what the parent shows.
678
+ * PixelLab reads it off the image when omitted, and can decline some
679
+ * images, failing the job until it is sent.
680
+ */
681
+ subjectDescription: import_zod.z.string().min(1).max(300).optional(),
682
+ /** `animate-pixminimax` only: the pose the parent is in, read off the image by PixelLab when omitted. */
683
+ initialPose: import_zod.z.string().min(1).max(300).optional(),
684
+ /** `animate-pixminimax` only: facing direction, one of the model's caption fields, so it also holds the sprite's facing. */
669
685
  direction: CharacterDirectionSchema.optional(),
670
686
  /** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
671
687
  enhancePrompt: import_zod.z.boolean().optional(),
@@ -685,12 +701,15 @@ var RevisionSchema = import_zod.z.object({
685
701
  * same `mannequin`/`bear`/`cat`/`dog`/`horse`/`lion` vocabulary.
686
702
  */
687
703
  skeletonTemplate: import_zod.z.string().min(1).optional(),
704
+ /** `remove-background` only: `simple` is faster and suits flat backgrounds; `complex` handles detailed edges. PixelLab defaults to `simple`. */
705
+ removalTask: import_zod.z.enum(["simple", "complex"]).optional(),
688
706
  /**
689
- * `animate-skeleton` only: what the character *looks like*, a noun
707
+ * `animate-skeleton`: what the character *looks like*, a noun
690
708
  * phrase (colours, clothing, held items) — distinct from the asset's
691
709
  * own `prompt`, which this mode sends as the motion's short label
692
710
  * (PixelLab's `action`), the same `prompt`-as-motion convention
693
- * `animate`/`animate-pixminimax` already use.
711
+ * `animate`/`animate-pixminimax` already use. `remove-background`: a
712
+ * short description of the foreground, which PixelLab uses as a hint.
694
713
  */
695
714
  description: import_zod.z.string().min(1).optional(),
696
715
  /**
@@ -807,7 +826,44 @@ var RevisionSchema = import_zod.z.object({
807
826
  path: ["keypointsFile"]
808
827
  });
809
828
  }
810
- for (const field of ["keypointsFile", "skeletonTemplate", "description"]) {
829
+ if (revision.driftThreshold !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
830
+ context.addIssue({
831
+ code: import_zod.z.ZodIssueCode.custom,
832
+ message: "driftThreshold applies to animate/animate-pixminimax revisions only",
833
+ path: ["driftThreshold"]
834
+ });
835
+ }
836
+ for (const field of ["subjectDescription", "initialPose"]) {
837
+ if (revision[field] !== void 0 && revision.mode !== "animate-pixminimax") {
838
+ context.addIssue({
839
+ code: import_zod.z.ZodIssueCode.custom,
840
+ message: `${field} applies to animate-pixminimax revisions only`,
841
+ path: [field]
842
+ });
843
+ }
844
+ }
845
+ if (revision.removalTask !== void 0 && revision.mode !== "remove-background") {
846
+ context.addIssue({
847
+ code: import_zod.z.ZodIssueCode.custom,
848
+ message: "removalTask applies to remove-background revisions only",
849
+ path: ["removalTask"]
850
+ });
851
+ }
852
+ if (revision.strength !== void 0 && revision.mode === "remove-background") {
853
+ context.addIssue({
854
+ code: import_zod.z.ZodIssueCode.custom,
855
+ message: "remove-background revisions do not take a strength",
856
+ path: ["strength"]
857
+ });
858
+ }
859
+ if (revision.description !== void 0 && revision.mode !== "animate-skeleton" && revision.mode !== "remove-background") {
860
+ context.addIssue({
861
+ code: import_zod.z.ZodIssueCode.custom,
862
+ message: "description applies to animate-skeleton/remove-background revisions only",
863
+ path: ["description"]
864
+ });
865
+ }
866
+ for (const field of ["keypointsFile", "skeletonTemplate"]) {
811
867
  if (revision[field] !== void 0 && revision.mode !== "animate-skeleton") {
812
868
  context.addIssue({
813
869
  code: import_zod.z.ZodIssueCode.custom,
@@ -1461,6 +1517,10 @@ var LockEntrySchema = import_zod.z.object({
1461
1517
  keypointsSha256: import_zod.z.string().regex(/^[0-9a-f]{64}$/).optional(),
1462
1518
  skeletonTemplate: import_zod.z.string().min(1).optional(),
1463
1519
  description: import_zod.z.string().min(1).optional(),
1520
+ removalTask: import_zod.z.enum(["simple", "complex"]).optional(),
1521
+ driftThreshold: import_zod.z.number().min(0).optional(),
1522
+ subjectDescription: import_zod.z.string().min(1).max(300).optional(),
1523
+ initialPose: import_zod.z.string().min(1).max(300).optional(),
1464
1524
  engine: import_zod.z.enum(["pro-flash"]).optional()
1465
1525
  }).strict().nullable().default(null),
1466
1526
  /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
@@ -1889,6 +1949,7 @@ var CorrectPixelartResponseSchema = import_zod3.z.object({
1889
1949
  images: import_zod3.z.array(import_zod3.z.object({ base64: import_zod3.z.string().min(1) }).passthrough()).min(1),
1890
1950
  usage: import_zod3.z.unknown().optional()
1891
1951
  }).passthrough();
1952
+ var RemoveBackgroundResponseSchema = import_zod3.z.object({ image: import_zod3.z.object({ base64: import_zod3.z.string().min(1) }).passthrough(), usage: import_zod3.z.unknown().optional() }).passthrough();
1892
1953
  var ProFlashImageSubmitSchema = import_zod3.z.object({
1893
1954
  background_job_id: import_zod3.z.string().min(1),
1894
1955
  image_id: import_zod3.z.string().min(1),
@@ -2080,9 +2141,9 @@ var PixelLabClient = class {
2080
2141
  apiKey;
2081
2142
  http;
2082
2143
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
2083
- async request(path36, init) {
2144
+ async request(path37, init) {
2084
2145
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
2085
- const res = await this.http(`${BASE}${path36}`, {
2146
+ const res = await this.http(`${BASE}${path37}`, {
2086
2147
  ...init,
2087
2148
  headers: {
2088
2149
  Authorization: auth,
@@ -2092,13 +2153,13 @@ var PixelLabClient = class {
2092
2153
  });
2093
2154
  const text = await res.text();
2094
2155
  if (!res.ok) {
2095
- throw new PixelLabError(`${init?.method ?? "GET"} ${path36} \u2192 ${res.status}`, res.status, text);
2156
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path37} \u2192 ${res.status}`, res.status, text);
2096
2157
  }
2097
2158
  if (!text) return {};
2098
2159
  try {
2099
2160
  return JSON.parse(text);
2100
2161
  } catch {
2101
- throw new Error(`${init?.method ?? "GET"} ${path36} returned invalid JSON`);
2162
+ throw new Error(`${init?.method ?? "GET"} ${path37} returned invalid JSON`);
2102
2163
  }
2103
2164
  }
2104
2165
  async balance() {
@@ -2503,10 +2564,10 @@ var PixelLabClient = class {
2503
2564
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2504
2565
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2505
2566
  const south = args.reference?.south;
2506
- let path36;
2567
+ let path37;
2507
2568
  let body;
2508
2569
  if (args.mode === "v3") {
2509
- path36 = "/create-character-v3";
2570
+ path37 = "/create-character-v3";
2510
2571
  body = {
2511
2572
  description: args.description,
2512
2573
  image_size: imageSize,
@@ -2520,7 +2581,7 @@ var PixelLabClient = class {
2520
2581
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2521
2582
  };
2522
2583
  } else if (args.mode === "pro-flash") {
2523
- path36 = "/create-character-pro-flash";
2584
+ path37 = "/create-character-pro-flash";
2524
2585
  const traits = args.styleTraits;
2525
2586
  body = {
2526
2587
  description: args.description,
@@ -2544,7 +2605,7 @@ var PixelLabClient = class {
2544
2605
  } : {}
2545
2606
  };
2546
2607
  } else if (args.mode === "pro") {
2547
- path36 = "/create-character-pro";
2608
+ path37 = "/create-character-pro";
2548
2609
  body = {
2549
2610
  description: args.description,
2550
2611
  image_size: imageSize,
@@ -2558,7 +2619,7 @@ var PixelLabClient = class {
2558
2619
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2559
2620
  };
2560
2621
  } else {
2561
- path36 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2622
+ path37 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2562
2623
  body = {
2563
2624
  description: args.description,
2564
2625
  image_size: imageSize,
@@ -2575,8 +2636,8 @@ var PixelLabClient = class {
2575
2636
  ...palette
2576
2637
  };
2577
2638
  }
2578
- const raw = await this.request(path36, { method: "POST", body: JSON.stringify(body) });
2579
- return validateResponse(CharacterSubmitSchema, raw, path36.slice(1));
2639
+ const raw = await this.request(path37, { method: "POST", body: JSON.stringify(body) });
2640
+ return validateResponse(CharacterSubmitSchema, raw, path37.slice(1));
2580
2641
  }
2581
2642
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2582
2643
  async createCharacterState(args) {
@@ -2953,6 +3014,27 @@ var PixelLabClient = class {
2953
3014
  const pngs = cleanupResults(res.images, images.length, "correct-pixelart");
2954
3015
  return { png: pngs[0], pngs, usage: res.usage };
2955
3016
  }
3017
+ /**
3018
+ * `/remove-background`: a transparent PNG from one pixel-art image,
3019
+ * synchronously, like the rest of the Cleanup tier. The API takes at most
3020
+ * 400x400 worth of area, one image per call. `simple` is faster and suits
3021
+ * flat backgrounds; `complex` is slower and handles detailed edges. The
3022
+ * schema's response example is dollar-denominated (`usd: 0.01`); the
3023
+ * subscription cost measured 1 generation (docs/ENDPOINTS.md), and the
3024
+ * default `simple` task is the one measured.
3025
+ */
3026
+ async removeBackground(args) {
3027
+ const body = { image: args.image, image_size: { width: args.width, height: args.height } };
3028
+ if (args.task) body.background_removal_task = args.task === "complex" ? "remove_complex_background" : "remove_simple_background";
3029
+ if (args.text) body.text = args.text;
3030
+ if (args.seed != null) body.seed = args.seed;
3031
+ const res = validateResponse(
3032
+ RemoveBackgroundResponseSchema,
3033
+ await this.request("/remove-background", { method: "POST", body: JSON.stringify(body) }),
3034
+ "remove-background"
3035
+ );
3036
+ return { png: Buffer.from(res.image.base64, "base64"), usage: res.usage };
3037
+ }
2956
3038
  /**
2957
3039
  * `/unzoom`: recover native-resolution pixel art from an upscaled image (a
2958
3040
  * 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
@@ -3061,6 +3143,7 @@ var PixelLabClient = class {
3061
3143
  */
3062
3144
  async animateWithTextV3(args) {
3063
3145
  const body = { first_frame: args.firstFrame, action: args.action };
3146
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
3064
3147
  if (args.lastFrame) body.last_frame = args.lastFrame;
3065
3148
  if (args.frameCount != null) body.frame_count = args.frameCount;
3066
3149
  if (args.seed != null) body.seed = args.seed;
@@ -3080,6 +3163,10 @@ var PixelLabClient = class {
3080
3163
  */
3081
3164
  async animatePixminimax(args) {
3082
3165
  const body = { first_frame: args.firstFrame, description: args.description };
3166
+ if (args.view) body.view = args.view;
3167
+ if (args.subjectDescription) body.subject_description = args.subjectDescription;
3168
+ if (args.initialPose) body.initial_pose = args.initialPose;
3169
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
3083
3170
  if (args.lastFrame) body.last_frame = args.lastFrame;
3084
3171
  if (args.frameCount != null) body.frame_count = args.frameCount;
3085
3172
  if (args.seed != null) body.seed = args.seed;
@@ -3115,6 +3202,7 @@ var PixelLabClient = class {
3115
3202
  keypoints: args.keypoints,
3116
3203
  direction: args.direction
3117
3204
  };
3205
+ if (args.view) body.view = args.view;
3118
3206
  if (args.templateId) body.template_id = args.templateId;
3119
3207
  if (args.action) body.action = args.action;
3120
3208
  if (args.description) body.description = args.description;
@@ -3762,10 +3850,10 @@ var import_node_fs = require("fs");
3762
3850
  function sha256(data) {
3763
3851
  return (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
3764
3852
  }
3765
- async function sha256File(path36) {
3853
+ async function sha256File(path37) {
3766
3854
  return new Promise((resolve, reject) => {
3767
3855
  const hash = (0, import_node_crypto.createHash)("sha256");
3768
- const stream = (0, import_node_fs.createReadStream)(path36);
3856
+ const stream = (0, import_node_fs.createReadStream)(path37);
3769
3857
  stream.on("data", (chunk2) => hash.update(chunk2));
3770
3858
  stream.on("error", reject);
3771
3859
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -3889,6 +3977,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3889
3977
  keypointsSha256: spec.revision.keypointsSha256,
3890
3978
  skeletonTemplate: spec.revision.skeletonTemplate,
3891
3979
  description: spec.revision.description,
3980
+ removalTask: spec.revision.removalTask,
3981
+ driftThreshold: spec.revision.driftThreshold,
3982
+ subjectDescription: spec.revision.subjectDescription,
3983
+ initialPose: spec.revision.initialPose,
3892
3984
  engine: spec.revision.engine
3893
3985
  } : void 0
3894
3986
  })
@@ -4064,7 +4156,7 @@ var PixelLabProvider = class _PixelLabProvider {
4064
4156
  * outpaint path yet.
4065
4157
  */
4066
4158
  supportsRevision(mode2) {
4067
- return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "animate-skeleton" || mode2 === "interpolate" || mode2 === "edit-animation";
4159
+ return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "remove-background" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "animate-skeleton" || mode2 === "interpolate" || mode2 === "edit-animation";
4068
4160
  }
4069
4161
  /** PixelLab's own constraints: submissions must be >2s apart, and
4070
4162
  * background jobs in flight are capped by subscription tier (Tier 1=8,
@@ -4092,6 +4184,9 @@ var PixelLabProvider = class _PixelLabProvider {
4092
4184
  if (spec.batch?.role === "member") {
4093
4185
  return { unit: "generations", amount: 0, candidates: 1 };
4094
4186
  }
4187
+ if (spec.revision?.mode === "remove-background") {
4188
+ return { unit: "generations", amount: 1, candidates: 1 };
4189
+ }
4095
4190
  if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
4096
4191
  return { unit: "generations", amount: 0.1, candidates: 1 };
4097
4192
  }
@@ -4180,6 +4275,12 @@ var PixelLabProvider = class _PixelLabProvider {
4180
4275
  throw new Error(`PixelLab correct-pixelart source is ${width}x${height}; the API takes at most 1024 pixels per side`);
4181
4276
  }
4182
4277
  }
4278
+ if (spec.revision?.mode === "remove-background") {
4279
+ const { sourceWidth: width, sourceHeight: height } = spec.revision;
4280
+ if (width != null && height != null && (width > 400 || height > 400)) {
4281
+ throw new Error(`PixelLab remove-background source is ${width}x${height}; the API takes at most 400x400`);
4282
+ }
4283
+ }
4183
4284
  if (spec.revision?.mode === "animate-skeleton") {
4184
4285
  const { sourceWidth: width, sourceHeight: height, frames, skeletonTemplate } = spec.revision;
4185
4286
  if (width != null && height != null && (width > 256 || height > 256)) {
@@ -4200,6 +4301,9 @@ var PixelLabProvider = class _PixelLabProvider {
4200
4301
  if (mode2 === "animate" && frames != null && frames > 16) {
4201
4302
  throw new Error(`PixelLab animate takes 4 to 16 frames; ${frames} is too many (use animate-pixminimax for up to 40)`);
4202
4303
  }
4304
+ if (mode2 === "animate-pixminimax" && frames != null && frames % 4 !== 0) {
4305
+ throw new Error(`PixelLab animate-pixminimax takes a multiple of 4 frames (4 to 40); ${frames} is not one`);
4306
+ }
4203
4307
  if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
4204
4308
  throw new Error(
4205
4309
  `PixelLab ${mode2} lastFrame is ${lastFrameWidth}x${lastFrameHeight}; source is ${width}x${height} \u2014 they must match`
@@ -5291,6 +5395,18 @@ var PixelLabProvider = class _PixelLabProvider {
5291
5395
  const res2 = await this.client.correctPixelart({ images, strength: revision.strength });
5292
5396
  return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
5293
5397
  }
5398
+ if (revision.mode === "remove-background") {
5399
+ if (width == null || height == null) throw new Error(`${spec.styleId}/${spec.assetId}: remove-background needs the source's size`);
5400
+ const res2 = await this.client.removeBackground({
5401
+ image,
5402
+ width,
5403
+ height,
5404
+ task: revision.removalTask,
5405
+ text: revision.description,
5406
+ seed: spec.seed
5407
+ });
5408
+ return this.cacheCleanupResult([res2.png], res2.usage, roles, revision.sourceFps);
5409
+ }
5294
5410
  if (revision.mode === "interpolate") {
5295
5411
  const lastFrame = this.readLastFrame(spec);
5296
5412
  if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
@@ -5326,7 +5442,8 @@ var PixelLabProvider = class _PixelLabProvider {
5326
5442
  frameCount: revision.frames,
5327
5443
  seed: spec.seed,
5328
5444
  noBackground: spec.noBackground,
5329
- enhancePrompt: revision.enhancePrompt
5445
+ enhancePrompt: revision.enhancePrompt,
5446
+ driftThreshold: revision.driftThreshold
5330
5447
  });
5331
5448
  return { jobId: res3.background_job_id };
5332
5449
  }
@@ -5338,7 +5455,11 @@ var PixelLabProvider = class _PixelLabProvider {
5338
5455
  seed: spec.seed,
5339
5456
  noBackground: spec.noBackground,
5340
5457
  enhancePrompt: revision.enhancePrompt,
5341
- direction: revision.direction
5458
+ direction: revision.direction,
5459
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
5460
+ subjectDescription: revision.subjectDescription,
5461
+ initialPose: revision.initialPose,
5462
+ driftThreshold: revision.driftThreshold
5342
5463
  });
5343
5464
  return { jobId: res2.background_job_id };
5344
5465
  }
@@ -5355,6 +5476,9 @@ var PixelLabProvider = class _PixelLabProvider {
5355
5476
  firstFrameKeypoints: revision.skeleton.firstFrameKeypoints,
5356
5477
  keypoints: revision.skeleton.frames,
5357
5478
  direction: revision.direction,
5479
+ // The style's camera, when it is one this endpoint knows; its enum is
5480
+ // narrower than a style's free-string `view`.
5481
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
5358
5482
  templateId: revision.skeletonTemplate,
5359
5483
  action: spec.prompt,
5360
5484
  description: revision.description,
@@ -5425,7 +5549,7 @@ var PixelLabProvider = class _PixelLabProvider {
5425
5549
  }
5426
5550
  async poll(jobId, generator, context) {
5427
5551
  const revisionMode = context?.spec?.revision?.mode;
5428
- if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
5552
+ if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart" || revisionMode === "remove-background") {
5429
5553
  return this.pollCachedRevision(jobId, context);
5430
5554
  }
5431
5555
  if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "animate-skeleton" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
@@ -6133,9 +6257,9 @@ var RetroDiffusionClient = class {
6133
6257
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
6134
6258
  return Buffer.from(await response.arrayBuffer());
6135
6259
  }
6136
- async call(path36, init = {}) {
6260
+ async call(path37, init = {}) {
6137
6261
  if (!this.token) throw new Error("RD_API_KEY is not set");
6138
- const response = await this.request(`${this.baseUrl}${path36}`, {
6262
+ const response = await this.request(`${this.baseUrl}${path37}`, {
6139
6263
  ...init,
6140
6264
  headers: {
6141
6265
  "Content-Type": "application/json",
@@ -9141,6 +9265,10 @@ async function resolveSpecs(loaded, filter) {
9141
9265
  } : {},
9142
9266
  ...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
9143
9267
  ...asset.revision.description ? { description: asset.revision.description } : {},
9268
+ ...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
9269
+ ...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
9270
+ ...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
9271
+ ...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
9144
9272
  ...asset.revision.engine ? { engine: asset.revision.engine } : {}
9145
9273
  };
9146
9274
  }
@@ -12424,10 +12552,10 @@ function isSha256Hash(value) {
12424
12552
  function parseCache(value) {
12425
12553
  return HashCacheSchema.parse(value);
12426
12554
  }
12427
- async function loadCache(path36) {
12428
- if (!(0, import_node_fs14.existsSync)(path36)) return { version: 1, hashes: {} };
12555
+ async function loadCache(path37) {
12556
+ if (!(0, import_node_fs14.existsSync)(path37)) return { version: 1, hashes: {} };
12429
12557
  try {
12430
- const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path36, "utf8")));
12558
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await (0, import_promises11.readFile)(path37, "utf8")));
12431
12559
  if (!parsed.success) return { version: 1, hashes: {} };
12432
12560
  return {
12433
12561
  version: 1,
@@ -12439,18 +12567,18 @@ async function loadCache(path36) {
12439
12567
  return { version: 1, hashes: {} };
12440
12568
  }
12441
12569
  }
12442
- async function saveCache(path36, cache) {
12570
+ async function saveCache(path37, cache) {
12443
12571
  const sorted = {};
12444
12572
  for (const key of Object.keys(cache.hashes).sort()) {
12445
12573
  const hash = cache.hashes[key];
12446
12574
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12447
12575
  sorted[key] = hash;
12448
12576
  }
12449
- await (0, import_promises11.mkdir)(import_node_path18.default.dirname(import_node_path18.default.resolve(path36)), { recursive: true });
12450
- const tmp = `${path36}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12577
+ await (0, import_promises11.mkdir)(import_node_path18.default.dirname(import_node_path18.default.resolve(path37)), { recursive: true });
12578
+ const tmp = `${path37}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12451
12579
  try {
12452
12580
  await (0, import_promises11.writeFile)(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12453
- await (0, import_promises11.rename)(tmp, path36);
12581
+ await (0, import_promises11.rename)(tmp, path37);
12454
12582
  } finally {
12455
12583
  await (0, import_promises11.rm)(tmp, { force: true });
12456
12584
  }
@@ -13966,6 +14094,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13966
14094
  ...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
13967
14095
  ...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
13968
14096
  ...spec.revision.description ? { description: spec.revision.description } : {},
14097
+ ...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
14098
+ ...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
14099
+ ...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
14100
+ ...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
13969
14101
  ...spec.revision.engine ? { engine: spec.revision.engine } : {}
13970
14102
  } : null,
13971
14103
  outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
@@ -15255,9 +15387,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
15255
15387
 
15256
15388
  // src/gallery/snapshot.ts
15257
15389
  var import_node_crypto6 = require("crypto");
15258
- var import_node_fs27 = require("fs");
15259
- var import_promises25 = require("fs/promises");
15260
- var import_node_path32 = __toESM(require("path"), 1);
15390
+ var import_node_fs28 = require("fs");
15391
+ var import_promises26 = require("fs/promises");
15392
+ var import_node_path33 = __toESM(require("path"), 1);
15261
15393
 
15262
15394
  // src/workspace.ts
15263
15395
  var import_promises19 = require("fs/promises");
@@ -15393,15 +15525,16 @@ function validateWorkspace(ws, dir) {
15393
15525
  }
15394
15526
 
15395
15527
  // src/gallery/edit.ts
15396
- var import_promises24 = require("fs/promises");
15397
- var import_node_path31 = __toESM(require("path"), 1);
15398
- var import_zod12 = require("zod");
15528
+ var import_promises25 = require("fs/promises");
15529
+ var import_node_path32 = __toESM(require("path"), 1);
15530
+ var import_zod13 = require("zod");
15399
15531
 
15400
15532
  // src/manifest-edit.ts
15401
15533
  var import_promises20 = require("fs/promises");
15402
15534
  var import_node_path27 = __toESM(require("path"), 1);
15403
15535
  var import_zod8 = require("zod");
15404
15536
  var HexSha = import_zod8.z.string().regex(/^[0-9a-f]{64}$/);
15537
+ var ProjectFileSchema = import_zod8.z.string().min(1).refine((file) => !import_node_path27.default.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
15405
15538
  var AssetPatchSchema = import_zod8.z.object({
15406
15539
  prompt: import_zod8.z.string().optional(),
15407
15540
  /** Per-style override for the style being viewed; null removes it. */
@@ -15450,10 +15583,11 @@ var NewAssetSchema = import_zod8.z.object({
15450
15583
  styles: import_zod8.z.array(import_zod8.z.string().min(1)).optional(),
15451
15584
  /**
15452
15585
  * A controlled regeneration of another asset in the same manifest,
15453
- * instead of a fresh generation. `inpaint` needs a mask upload and
15454
- * `interpolate` an ending-keyframe image the gallery does not offer, so
15455
- * those two are hand-edited; every other mode can be created here
15456
- * (`animate-skeleton` after the gallery writes its keypoints file).
15586
+ * instead of a fresh generation. Every mode but `outpaint` can be
15587
+ * created here: `inpaint` names a mask file the gallery's mask editor
15588
+ * wrote first, `interpolate` names an existing image as its ending
15589
+ * keyframe, and `animate-skeleton` names the keypoints file the gallery
15590
+ * wrote first.
15457
15591
  */
15458
15592
  revision: import_zod8.z.discriminatedUnion("mode", [
15459
15593
  import_zod8.z.object({
@@ -15470,15 +15604,38 @@ var NewAssetSchema = import_zod8.z.object({
15470
15604
  dithering: RevisionDitheringSchema.optional(),
15471
15605
  ditheringStrength: import_zod8.z.number().min(0).max(10).optional()
15472
15606
  }).strict(),
15607
+ import_zod8.z.object({
15608
+ mode: import_zod8.z.literal("inpaint"),
15609
+ from: import_zod8.z.string().min(1),
15610
+ /** Black-and-white PNG the same size as the parent; white is repainted. */
15611
+ mask: ProjectFileSchema
15612
+ }).strict(),
15613
+ import_zod8.z.object({
15614
+ mode: import_zod8.z.literal("interpolate"),
15615
+ from: import_zod8.z.string().min(1),
15616
+ /** The ending keyframe: an image already in the project, the parent's size. */
15617
+ lastFrame: ProjectFileSchema,
15618
+ fps: import_zod8.z.number().int().min(1).max(60).optional()
15619
+ }).strict(),
15473
15620
  import_zod8.z.object({ mode: import_zod8.z.literal("correct-pixelart"), from: import_zod8.z.string().min(1), strength: import_zod8.z.number().min(0).max(1).optional() }).strict(),
15621
+ import_zod8.z.object({
15622
+ mode: import_zod8.z.literal("remove-background"),
15623
+ from: import_zod8.z.string().min(1),
15624
+ removalTask: import_zod8.z.enum(["simple", "complex"]).optional(),
15625
+ /** A short description of the foreground, sent as PixelLab's text hint. */
15626
+ description: import_zod8.z.string().min(1).optional()
15627
+ }).strict(),
15474
15628
  import_zod8.z.object({
15475
15629
  mode: import_zod8.z.enum(["animate", "animate-pixminimax"]),
15476
15630
  from: import_zod8.z.string().min(1),
15477
15631
  frames: import_zod8.z.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
15478
15632
  fps: import_zod8.z.number().int().min(1).max(60).optional(),
15479
15633
  enhancePrompt: import_zod8.z.boolean().optional(),
15480
- /** `animate-pixminimax` only; the loader refuses it on `animate`. */
15481
- direction: CharacterDirectionSchema.optional()
15634
+ driftThreshold: import_zod8.z.number().min(0).optional(),
15635
+ /** `animate-pixminimax` only; the loader refuses these on `animate`. */
15636
+ direction: CharacterDirectionSchema.optional(),
15637
+ subjectDescription: import_zod8.z.string().min(1).max(300).optional(),
15638
+ initialPose: import_zod8.z.string().min(1).max(300).optional()
15482
15639
  }).strict(),
15483
15640
  import_zod8.z.object({
15484
15641
  mode: import_zod8.z.literal("edit-animation"),
@@ -16129,62 +16286,118 @@ async function saveProjectImage(root, request) {
16129
16286
  return relative.split(import_node_path29.default.sep).join("/");
16130
16287
  }
16131
16288
 
16132
- // src/gallery/skeleton.ts
16289
+ // src/gallery/image-revision.ts
16133
16290
  var import_node_fs26 = require("fs");
16134
16291
  var import_promises23 = require("fs/promises");
16135
16292
  var import_node_path30 = __toESM(require("path"), 1);
16136
16293
  var import_zod11 = require("zod");
16137
- var SkeletonEstimateRequestSchema = import_zod11.z.object({
16138
- project: import_zod11.z.string().min(1).optional(),
16139
- styleId: import_zod11.z.string().min(1),
16140
- assetId: import_zod11.z.string().min(1),
16141
- frames: import_zod11.z.number().int().min(3).max(15).optional()
16142
- }).strict();
16143
- var SkeletonAnimationRequestSchema = import_zod11.z.object({
16144
- action: import_zod11.z.literal("create-skeleton-animation"),
16294
+ var InpaintRevisionRequestSchema = import_zod11.z.object({
16295
+ action: import_zod11.z.literal("create-inpaint-revision"),
16145
16296
  project: import_zod11.z.string().min(1).optional(),
16146
16297
  expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/),
16147
16298
  styleId: import_zod11.z.string().min(1),
16148
- /** The sprite the animation poses. */
16299
+ /** The sprite being repainted. */
16149
16300
  from: import_zod11.z.string().min(1),
16150
16301
  assetId: import_zod11.z.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
16151
- /** The motion, sent as PixelLab's `action`. */
16302
+ /** What to paint into the masked area. */
16152
16303
  prompt: import_zod11.z.string().min(1),
16304
+ /** Manifest-relative `.png` destination for the mask. */
16305
+ maskPath: import_zod11.z.string().min(1),
16306
+ maskBase64: import_zod11.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/),
16307
+ /** Replace a file already at `maskPath`. */
16308
+ overwrite: import_zod11.z.boolean().optional()
16309
+ }).strict();
16310
+ async function createInpaintRevision(manifestPath, request) {
16311
+ const root = import_node_path30.default.dirname(import_node_path30.default.resolve(manifestPath));
16312
+ if (import_node_path30.default.extname(request.maskPath).toLowerCase() !== ".png") {
16313
+ throw new ManifestEditError("the mask must be a .png path inside the project");
16314
+ }
16315
+ const bytes = Buffer.from(request.maskBase64, "base64");
16316
+ const metadata = imageMetadata(bytes);
16317
+ if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
16318
+ const file = import_node_path30.default.resolve(root, request.maskPath);
16319
+ const previous = (0, import_node_fs26.existsSync)(file) ? await (0, import_promises23.readFile)(file) : null;
16320
+ const mask = await saveProjectImage(root, {
16321
+ action: "upload-image",
16322
+ path: request.maskPath,
16323
+ base64: request.maskBase64,
16324
+ ...request.overwrite ? { overwrite: true } : {}
16325
+ });
16326
+ try {
16327
+ await applyManifestEdit(manifestPath, {
16328
+ action: "add-asset",
16329
+ assetId: request.assetId,
16330
+ expectedSha256: request.expectedSha256,
16331
+ asset: {
16332
+ prompt: request.prompt,
16333
+ styles: [request.styleId],
16334
+ revision: { mode: "inpaint", from: request.from, mask }
16335
+ }
16336
+ });
16337
+ } catch (error) {
16338
+ if (previous) await (0, import_promises23.writeFile)(file, previous);
16339
+ else await (0, import_promises23.rm)(file, { force: true });
16340
+ throw error;
16341
+ }
16342
+ return mask;
16343
+ }
16344
+
16345
+ // src/gallery/skeleton.ts
16346
+ var import_node_fs27 = require("fs");
16347
+ var import_promises24 = require("fs/promises");
16348
+ var import_node_path31 = __toESM(require("path"), 1);
16349
+ var import_zod12 = require("zod");
16350
+ var SkeletonEstimateRequestSchema = import_zod12.z.object({
16351
+ project: import_zod12.z.string().min(1).optional(),
16352
+ styleId: import_zod12.z.string().min(1),
16353
+ assetId: import_zod12.z.string().min(1),
16354
+ frames: import_zod12.z.number().int().min(3).max(15).optional()
16355
+ }).strict();
16356
+ var SkeletonAnimationRequestSchema = import_zod12.z.object({
16357
+ action: import_zod12.z.literal("create-skeleton-animation"),
16358
+ project: import_zod12.z.string().min(1).optional(),
16359
+ expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/),
16360
+ styleId: import_zod12.z.string().min(1),
16361
+ /** The sprite the animation poses. */
16362
+ from: import_zod12.z.string().min(1),
16363
+ assetId: import_zod12.z.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
16364
+ /** The motion, sent as PixelLab's `action`. */
16365
+ prompt: import_zod12.z.string().min(1),
16153
16366
  direction: CharacterDirectionSchema,
16154
- description: import_zod11.z.string().min(1).optional(),
16155
- skeletonTemplate: import_zod11.z.string().min(1).optional(),
16367
+ description: import_zod12.z.string().min(1).optional(),
16368
+ skeletonTemplate: import_zod12.z.string().min(1).optional(),
16156
16369
  /** Manifest-relative JSON path inside the project. */
16157
- keypointsFile: import_zod11.z.string().min(1),
16370
+ keypointsFile: import_zod12.z.string().min(1),
16158
16371
  /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
16159
16372
  set: SkeletonSetSchema.optional(),
16160
16373
  /** Replace an existing `keypointsFile` with `set`. */
16161
- overwrite: import_zod11.z.boolean().optional()
16374
+ overwrite: import_zod12.z.boolean().optional()
16162
16375
  }).strict();
16163
- var SkeletonKeypointsUpdateSchema = import_zod11.z.object({
16164
- action: import_zod11.z.literal("update-skeleton-keypoints"),
16165
- project: import_zod11.z.string().min(1).optional(),
16166
- styleId: import_zod11.z.string().min(1),
16167
- assetId: import_zod11.z.string().min(1),
16376
+ var SkeletonKeypointsUpdateSchema = import_zod12.z.object({
16377
+ action: import_zod12.z.literal("update-skeleton-keypoints"),
16378
+ project: import_zod12.z.string().min(1).optional(),
16379
+ styleId: import_zod12.z.string().min(1),
16380
+ assetId: import_zod12.z.string().min(1),
16168
16381
  /** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
16169
- expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/),
16382
+ expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/),
16170
16383
  set: SkeletonSetSchema
16171
16384
  }).strict();
16172
16385
  async function createSkeletonAnimation(manifestPath, request) {
16173
- const root = import_node_path30.default.dirname(import_node_path30.default.resolve(manifestPath));
16174
- const file = import_node_path30.default.resolve(root, request.keypointsFile);
16175
- const relative = import_node_path30.default.relative(root, file);
16176
- if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || import_node_path30.default.isAbsolute(relative)) {
16386
+ const root = import_node_path31.default.dirname(import_node_path31.default.resolve(manifestPath));
16387
+ const file = import_node_path31.default.resolve(root, request.keypointsFile);
16388
+ const relative = import_node_path31.default.relative(root, file);
16389
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || import_node_path31.default.isAbsolute(relative)) {
16177
16390
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
16178
16391
  }
16179
- const existed = (0, import_node_fs26.existsSync)(file);
16392
+ const existed = (0, import_node_fs27.existsSync)(file);
16180
16393
  let previous = null;
16181
16394
  if (request.set) {
16182
16395
  if (existed && !request.overwrite) {
16183
16396
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
16184
16397
  }
16185
- if (existed) previous = await (0, import_promises23.readFile)(file);
16186
- await (0, import_promises23.mkdir)(import_node_path30.default.dirname(file), { recursive: true });
16187
- await (0, import_promises23.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16398
+ if (existed) previous = await (0, import_promises24.readFile)(file);
16399
+ await (0, import_promises24.mkdir)(import_node_path31.default.dirname(file), { recursive: true });
16400
+ await (0, import_promises24.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16188
16401
  } else if (!existed) {
16189
16402
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
16190
16403
  }
@@ -16199,7 +16412,7 @@ async function createSkeletonAnimation(manifestPath, request) {
16199
16412
  revision: {
16200
16413
  mode: "animate-skeleton",
16201
16414
  from: request.from,
16202
- keypointsFile: relative.split(import_node_path30.default.sep).join("/"),
16415
+ keypointsFile: relative.split(import_node_path31.default.sep).join("/"),
16203
16416
  direction: request.direction,
16204
16417
  ...request.description ? { description: request.description } : {},
16205
16418
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -16208,8 +16421,8 @@ async function createSkeletonAnimation(manifestPath, request) {
16208
16421
  });
16209
16422
  } catch (error) {
16210
16423
  if (request.set) {
16211
- if (previous) await (0, import_promises23.writeFile)(file, previous);
16212
- else await (0, import_promises23.rm)(file, { force: true });
16424
+ if (previous) await (0, import_promises24.writeFile)(file, previous);
16425
+ else await (0, import_promises24.rm)(file, { force: true });
16213
16426
  }
16214
16427
  throw error;
16215
16428
  }
@@ -16220,64 +16433,64 @@ async function updateSkeletonKeypoints(ctx, request) {
16220
16433
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
16221
16434
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
16222
16435
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
16223
- const relative = import_node_path30.default.relative(ctx.loaded.root, file);
16224
- if (relative.startsWith("..") || import_node_path30.default.isAbsolute(relative)) {
16436
+ const relative = import_node_path31.default.relative(ctx.loaded.root, file);
16437
+ if (relative.startsWith("..") || import_node_path31.default.isAbsolute(relative)) {
16225
16438
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
16226
16439
  }
16227
- const current = (0, import_node_fs26.existsSync)(file) ? sha256(await (0, import_promises23.readFile)(file)) : null;
16440
+ const current = (0, import_node_fs27.existsSync)(file) ? sha256(await (0, import_promises24.readFile)(file)) : null;
16228
16441
  if (current !== request.expectedSha256) {
16229
16442
  throw Object.assign(
16230
- new Error(`${relative.split(import_node_path30.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16443
+ new Error(`${relative.split(import_node_path31.default.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16231
16444
  { status: 409 }
16232
16445
  );
16233
16446
  }
16234
- await (0, import_promises23.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16235
- return relative.split(import_node_path30.default.sep).join("/");
16447
+ await (0, import_promises24.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16448
+ return relative.split(import_node_path31.default.sep).join("/");
16236
16449
  }
16237
16450
 
16238
16451
  // src/gallery/edit.ts
16239
- var Base64 = import_zod12.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
16240
- var HandEditRequestSchema = import_zod12.z.discriminatedUnion("action", [
16241
- import_zod12.z.object({
16242
- action: import_zod12.z.literal("start-edit"),
16243
- project: import_zod12.z.string().min(1).optional(),
16244
- assetId: import_zod12.z.string().min(1),
16245
- styleId: import_zod12.z.string().min(1),
16246
- expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/),
16452
+ var Base64 = import_zod13.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
16453
+ var HandEditRequestSchema = import_zod13.z.discriminatedUnion("action", [
16454
+ import_zod13.z.object({
16455
+ action: import_zod13.z.literal("start-edit"),
16456
+ project: import_zod13.z.string().min(1).optional(),
16457
+ assetId: import_zod13.z.string().min(1),
16458
+ styleId: import_zod13.z.string().min(1),
16459
+ expectedSha256: import_zod13.z.string().regex(/^[0-9a-f]{64}$/),
16247
16460
  /** Also launch the author's editor on the file. */
16248
- open: import_zod12.z.boolean().optional()
16461
+ open: import_zod13.z.boolean().optional()
16249
16462
  }).strict(),
16250
- import_zod12.z.object({
16251
- action: import_zod12.z.literal("detach-edit"),
16252
- project: import_zod12.z.string().min(1).optional(),
16253
- assetId: import_zod12.z.string().min(1),
16254
- styleId: import_zod12.z.string().min(1),
16255
- expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/)
16463
+ import_zod13.z.object({
16464
+ action: import_zod13.z.literal("detach-edit"),
16465
+ project: import_zod13.z.string().min(1).optional(),
16466
+ assetId: import_zod13.z.string().min(1),
16467
+ styleId: import_zod13.z.string().min(1),
16468
+ expectedSha256: import_zod13.z.string().regex(/^[0-9a-f]{64}$/)
16256
16469
  }).strict(),
16257
16470
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
16258
- import_zod12.z.object({
16259
- action: import_zod12.z.literal("save-edit"),
16260
- project: import_zod12.z.string().min(1).optional(),
16261
- assetId: import_zod12.z.string().min(1),
16262
- styleId: import_zod12.z.string().min(1),
16263
- expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/),
16471
+ import_zod13.z.object({
16472
+ action: import_zod13.z.literal("save-edit"),
16473
+ project: import_zod13.z.string().min(1).optional(),
16474
+ assetId: import_zod13.z.string().min(1),
16475
+ styleId: import_zod13.z.string().min(1),
16476
+ expectedSha256: import_zod13.z.string().regex(/^[0-9a-f]{64}$/),
16264
16477
  /** The flattened image; for a frame set, `frames` instead. */
16265
16478
  png: Base64.min(1).optional(),
16266
16479
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
16267
- frames: import_zod12.z.array(import_zod12.z.object({ role: import_zod12.z.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
16480
+ frames: import_zod13.z.array(import_zod13.z.object({ role: import_zod13.z.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
16268
16481
  /** The editor's layered project file, kept beside the edit. */
16269
16482
  pxo: Base64.optional(),
16270
16483
  /** Editor identity from the bridge's ready message. */
16271
- editor: import_zod12.z.string().min(1).max(200),
16272
- protocol: import_zod12.z.number().int().positive()
16484
+ editor: import_zod13.z.string().min(1).max(200),
16485
+ protocol: import_zod13.z.number().int().positive()
16273
16486
  }).strict()
16274
16487
  ]);
16275
16488
  var UNDO_DEPTH = 20;
16276
- var UndoRequestSchema = import_zod12.z.object({
16277
- action: import_zod12.z.literal("undo-edit"),
16278
- project: import_zod12.z.string().min(1).optional(),
16489
+ var UndoRequestSchema = import_zod13.z.object({
16490
+ action: import_zod13.z.literal("undo-edit"),
16491
+ project: import_zod13.z.string().min(1).optional(),
16279
16492
  /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
16280
- expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/)
16493
+ expectedSha256: import_zod13.z.string().regex(/^[0-9a-f]{64}$/)
16281
16494
  }).strict();
16282
16495
  function createGalleryEditHandler(opts) {
16283
16496
  const log = opts.onProgress ?? (() => {
@@ -16286,7 +16499,7 @@ function createGalleryEditHandler(opts) {
16286
16499
  const history = /* @__PURE__ */ new Map();
16287
16500
  const manifestOf = async (project) => {
16288
16501
  try {
16289
- return import_node_path31.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16502
+ return import_node_path32.default.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16290
16503
  } catch {
16291
16504
  return null;
16292
16505
  }
@@ -16296,7 +16509,7 @@ function createGalleryEditHandler(opts) {
16296
16509
  if (undo.success) {
16297
16510
  const manifestPath2 = await manifestOf(undo.data.project);
16298
16511
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
16299
- const current = await (0, import_promises24.readFile)(manifestPath2, "utf8");
16512
+ const current = await (0, import_promises25.readFile)(manifestPath2, "utf8");
16300
16513
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
16301
16514
  const stack = history.get(manifestPath2) ?? [];
16302
16515
  const last = stack[stack.length - 1];
@@ -16306,17 +16519,17 @@ function createGalleryEditHandler(opts) {
16306
16519
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
16307
16520
  }
16308
16521
  const tmp = manifestPath2 + ".pixelkiln-undo";
16309
- await (0, import_promises24.writeFile)(tmp, last.before);
16310
- await (0, import_promises24.rename)(tmp, manifestPath2);
16522
+ await (0, import_promises25.writeFile)(tmp, last.before);
16523
+ await (0, import_promises25.rename)(tmp, manifestPath2);
16311
16524
  stack.pop();
16312
- log(` manifest edit undone in ${import_node_path31.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16525
+ log(` manifest edit undone in ${import_node_path32.default.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16313
16526
  return opts.reload();
16314
16527
  }
16315
16528
  const manifestPath = await manifestOf(body?.project);
16316
- const before = manifestPath ? await (0, import_promises24.readFile)(manifestPath, "utf8").catch(() => null) : null;
16529
+ const before = manifestPath ? await (0, import_promises25.readFile)(manifestPath, "utf8").catch(() => null) : null;
16317
16530
  const build = await apply(body);
16318
16531
  if (manifestPath && before !== null) {
16319
- const after = await (0, import_promises24.readFile)(manifestPath, "utf8").catch(() => null);
16532
+ const after = await (0, import_promises25.readFile)(manifestPath, "utf8").catch(() => null);
16320
16533
  if (after !== null && after !== before) {
16321
16534
  const stack = history.get(manifestPath) ?? [];
16322
16535
  stack.push({ before, afterSha: sha256(after) });
@@ -16371,7 +16584,7 @@ function createEditApplier(opts, log) {
16371
16584
  } catch (error) {
16372
16585
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16373
16586
  }
16374
- const written = await saveProjectImage(import_node_path31.default.dirname(import_node_path31.default.resolve(manifestPath2)), upload.data);
16587
+ const written = await saveProjectImage(import_node_path32.default.dirname(import_node_path32.default.resolve(manifestPath2)), upload.data);
16375
16588
  log(` image added: ${written}`);
16376
16589
  return opts.reload();
16377
16590
  }
@@ -16407,6 +16620,23 @@ function createEditApplier(opts, log) {
16407
16620
  "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16408
16621
  );
16409
16622
  }
16623
+ const inpaint = InpaintRevisionRequestSchema.safeParse(body);
16624
+ if (inpaint.success) {
16625
+ let manifestPath2;
16626
+ try {
16627
+ manifestPath2 = await opts.manifestFor(inpaint.data.project);
16628
+ } catch (error) {
16629
+ throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16630
+ }
16631
+ const mask = await createInpaintRevision(manifestPath2, inpaint.data);
16632
+ log(` manifest edited: added inpaint revision ${inpaint.data.assetId} (mask ${mask})`);
16633
+ return opts.reload();
16634
+ }
16635
+ if (body?.action === "create-inpaint-revision") {
16636
+ throw new ManifestEditError(
16637
+ "invalid inpaint revision: " + inpaint.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16638
+ );
16639
+ }
16410
16640
  const parsed = ManifestEditSchema.safeParse(body);
16411
16641
  if (!parsed.success) {
16412
16642
  throw new ManifestEditError(
@@ -16421,7 +16651,7 @@ function createEditApplier(opts, log) {
16421
16651
  }
16422
16652
  const result = await applyManifestEdit(manifestPath, parsed.data);
16423
16653
  if (result.changed) {
16424
- const relative = import_node_path31.default.relative(process.cwd(), result.manifestPath);
16654
+ const relative = import_node_path32.default.relative(process.cwd(), result.manifestPath);
16425
16655
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
16426
16656
  log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
16427
16657
  }
@@ -16449,7 +16679,7 @@ function describeEdit(edit) {
16449
16679
  // src/gallery/snapshot.ts
16450
16680
  var PROJECT_FILE_TYPE = "application/octet-stream";
16451
16681
  function galleryMediaId(absolutePath) {
16452
- return (0, import_node_crypto6.createHash)("sha256").update(import_node_path32.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16682
+ return (0, import_node_crypto6.createHash)("sha256").update(import_node_path33.default.resolve(absolutePath)).digest("hex").slice(0, 24);
16453
16683
  }
16454
16684
  function galleryMediaRoute(id) {
16455
16685
  return `/media/${id}`;
@@ -16468,9 +16698,9 @@ function describeHistory(media, entry, cacheDir) {
16468
16698
  return (entry.history ?? []).map((generation, i) => {
16469
16699
  const outputs = generation.outputs.map((output) => {
16470
16700
  const mediaType = output.mediaType ?? MediaType.PNG;
16471
- const file = import_node_path32.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16701
+ const file = import_node_path33.default.join(cacheDir, cacheFileName(output.sha256, mediaType));
16472
16702
  let url = null;
16473
- if ((0, import_node_fs27.existsSync)(file)) {
16703
+ if ((0, import_node_fs28.existsSync)(file)) {
16474
16704
  const id = galleryMediaId(file);
16475
16705
  media.set(id, { path: file, contentType: mediaType });
16476
16706
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -16502,7 +16732,7 @@ function describeHistory(media, entry, cacheDir) {
16502
16732
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16503
16733
  async function samePixels(a, b) {
16504
16734
  try {
16505
- const [left, right] = await Promise.all([(0, import_promises25.readFile)(a), (0, import_promises25.readFile)(b)]);
16735
+ const [left, right] = await Promise.all([(0, import_promises26.readFile)(a), (0, import_promises26.readFile)(b)]);
16506
16736
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16507
16737
  const x = decodePng(left);
16508
16738
  const y = decodePng(right);
@@ -16568,7 +16798,7 @@ function describeCharacter(spec, entry) {
16568
16798
  }
16569
16799
  async function fileInfo(absolutePath) {
16570
16800
  try {
16571
- const info = await (0, import_promises25.stat)(absolutePath);
16801
+ const info = await (0, import_promises26.stat)(absolutePath);
16572
16802
  return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
16573
16803
  } catch {
16574
16804
  return null;
@@ -16600,10 +16830,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
16600
16830
  async function describeQuality(media, root, inspection) {
16601
16831
  const outputPaths = inspection.outputs ?? [inspection.output];
16602
16832
  const outputs = await Promise.all(
16603
- outputPaths.map((file) => describeOutput(media, root, import_node_path32.default.resolve(root, file)))
16833
+ outputPaths.map((file) => describeOutput(media, root, import_node_path33.default.resolve(root, file)))
16604
16834
  );
16605
- const recordPath = import_node_path32.default.resolve(root, inspection.record);
16606
- const recordExists = (0, import_node_fs27.existsSync)(recordPath);
16835
+ const recordPath = import_node_path33.default.resolve(root, inspection.record);
16836
+ const recordExists = (0, import_node_fs28.existsSync)(recordPath);
16607
16837
  let options = null;
16608
16838
  let check = null;
16609
16839
  if (recordExists) {
@@ -16622,7 +16852,7 @@ async function describeQuality(media, root, inspection) {
16622
16852
  return {
16623
16853
  state: inspection.state,
16624
16854
  reason: inspection.reason,
16625
- output: portableOutputPath(import_node_path32.default.resolve(root, inspection.output), root),
16855
+ output: portableOutputPath(import_node_path33.default.resolve(root, inspection.output), root),
16626
16856
  record: portableOutputPath(recordPath, root),
16627
16857
  recordExists,
16628
16858
  outputs,
@@ -16660,9 +16890,9 @@ async function buildGallerySnapshot(opts) {
16660
16890
  const media = /* @__PURE__ */ new Map();
16661
16891
  const plan = await buildPlan(specs, lock);
16662
16892
  const items = [];
16663
- const manifestText = await (0, import_promises25.readFile)(loaded.path, "utf8");
16893
+ const manifestText = await (0, import_promises26.readFile)(loaded.path, "utf8");
16664
16894
  const manifestSha256 = sha256(manifestText);
16665
- const cacheDir = import_node_path32.default.resolve(import_node_path32.default.dirname(opts.lockPath), ".pixelkiln", "cache");
16895
+ const cacheDir = import_node_path33.default.resolve(import_node_path33.default.dirname(opts.lockPath), ".pixelkiln", "cache");
16666
16896
  let rawStyles = {};
16667
16897
  try {
16668
16898
  const raw = JSON.parse(manifestText);
@@ -16686,12 +16916,12 @@ async function buildGallerySnapshot(opts) {
16686
16916
  )
16687
16917
  );
16688
16918
  if (spec.source) {
16689
- const editPath = import_node_path32.default.resolve(root, spec.source);
16919
+ const editPath = import_node_path33.default.resolve(root, spec.source);
16690
16920
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
16691
16921
  const editShas = await Promise.all(
16692
16922
  Array.from({ length: memberCount }, async (_, index) => {
16693
16923
  const file = sourceOutputPath(spec.source, entry, index, root);
16694
- return { file, sha256: (0, import_node_fs27.existsSync)(file) ? await sha256File(file) : null };
16924
+ return { file, sha256: (0, import_node_fs28.existsSync)(file) ? await sha256File(file) : null };
16695
16925
  })
16696
16926
  );
16697
16927
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -16699,7 +16929,7 @@ async function buildGallerySnapshot(opts) {
16699
16929
  const companion = await readHandEditCompanion(editPath);
16700
16930
  if (companion) {
16701
16931
  const projectPath = handEditProjectPath(editPath);
16702
- const project = companion.project && (0, import_node_fs27.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
16932
+ const project = companion.project && (0, import_node_fs28.existsSync)(projectPath) ? await fileInfo(projectPath) : null;
16703
16933
  let projectUrl = null;
16704
16934
  if (project) {
16705
16935
  const id = galleryMediaId(projectPath);
@@ -16735,8 +16965,8 @@ async function buildGallerySnapshot(opts) {
16735
16965
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
16736
16966
  }
16737
16967
  } else if (spec.source) {
16738
- outputs = [await describeOutput(media, root, import_node_path32.default.resolve(root, spec.source))];
16739
- } else if (!entry && (0, import_node_fs27.existsSync)(spec.outFile)) {
16968
+ outputs = [await describeOutput(media, root, import_node_path33.default.resolve(root, spec.source))];
16969
+ } else if (!entry && (0, import_node_fs28.existsSync)(spec.outFile)) {
16740
16970
  outputs = [await describeOutput(media, root, spec.outFile)];
16741
16971
  } else {
16742
16972
  outputs = [];
@@ -16939,7 +17169,7 @@ async function buildGallerySnapshot(opts) {
16939
17169
  id: loaded.manifest.name,
16940
17170
  name: loaded.manifest.name,
16941
17171
  manifest: loaded.path,
16942
- lock: import_node_path32.default.resolve(opts.lockPath),
17172
+ lock: import_node_path33.default.resolve(opts.lockPath),
16943
17173
  root,
16944
17174
  provider: loaded.manifest.provider,
16945
17175
  account: null,
@@ -16965,7 +17195,7 @@ async function buildGallerySnapshot(opts) {
16965
17195
  return { snapshot, media };
16966
17196
  }
16967
17197
  async function buildWorkspaceGallerySnapshot(opts) {
16968
- const dir = import_node_path32.default.dirname(import_node_path32.default.resolve(opts.workspacePath));
17198
+ const dir = import_node_path33.default.dirname(import_node_path33.default.resolve(opts.workspacePath));
16969
17199
  const media = /* @__PURE__ */ new Map();
16970
17200
  const projects = [];
16971
17201
  const items = [];
@@ -17008,11 +17238,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
17008
17238
  id: project.id,
17009
17239
  name: loaded.manifest.name,
17010
17240
  manifest: manifestPath,
17011
- lock: import_node_path32.default.resolve(lockPath),
17241
+ lock: import_node_path33.default.resolve(lockPath),
17012
17242
  root: loaded.root,
17013
17243
  provider: loaded.manifest.provider,
17014
17244
  account: project.account ?? null,
17015
- manifestSha256: manifestSha256 ?? sha256(await (0, import_promises25.readFile)(manifestPath, "utf8")),
17245
+ manifestSha256: manifestSha256 ?? sha256(await (0, import_promises26.readFile)(manifestPath, "utf8")),
17016
17246
  historyLimit: historyLimit(loaded.manifest),
17017
17247
  entries: Object.keys(lock.entries).length,
17018
17248
  items: projectItems.length,
@@ -17024,8 +17254,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
17024
17254
  id: project.id,
17025
17255
  name: project.id,
17026
17256
  manifest: manifestPath,
17027
- lock: import_node_path32.default.resolve(lockPath),
17028
- root: import_node_path32.default.dirname(manifestPath),
17257
+ lock: import_node_path33.default.resolve(lockPath),
17258
+ root: import_node_path33.default.dirname(manifestPath),
17029
17259
  provider: project.provider,
17030
17260
  account: project.account ?? null,
17031
17261
  manifestSha256: null,
@@ -17052,7 +17282,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17052
17282
  version: 1,
17053
17283
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
17054
17284
  project: null,
17055
- workspace: { path: import_node_path32.default.resolve(opts.workspacePath), dir, projects },
17285
+ workspace: { path: import_node_path33.default.resolve(opts.workspacePath), dir, projects },
17056
17286
  filter,
17057
17287
  totals: {
17058
17288
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -17069,7 +17299,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17069
17299
  }
17070
17300
 
17071
17301
  // src/gallery/page.ts
17072
- var import_node_fs28 = require("fs");
17302
+ var import_node_fs29 = require("fs");
17073
17303
 
17074
17304
  // src/gallery/client-bundle.ts
17075
17305
  var import_node_module = require("module");
@@ -17109,8 +17339,8 @@ var client;
17109
17339
  function clientAssets() {
17110
17340
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
17111
17341
  client ??= {
17112
- css: (0, import_node_fs28.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
17113
- js: (0, import_node_fs28.existsSync)(prebuilt) ? (0, import_node_fs28.readFileSync)(prebuilt, "utf8") : bundleGalleryClient()
17342
+ css: (0, import_node_fs29.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
17343
+ js: (0, import_node_fs29.existsSync)(prebuilt) ? (0, import_node_fs29.readFileSync)(prebuilt, "utf8") : bundleGalleryClient()
17114
17344
  };
17115
17345
  return client;
17116
17346
  }
@@ -17253,10 +17483,10 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
17253
17483
 
17254
17484
  // src/editor/install.ts
17255
17485
  var import_node_crypto7 = require("crypto");
17256
- var import_node_fs29 = require("fs");
17257
- var import_promises26 = require("fs/promises");
17486
+ var import_node_fs30 = require("fs");
17487
+ var import_promises27 = require("fs/promises");
17258
17488
  var import_node_os3 = __toESM(require("os"), 1);
17259
- var import_node_path33 = __toESM(require("path"), 1);
17489
+ var import_node_path34 = __toESM(require("path"), 1);
17260
17490
  var EditorInstallError = class extends Error {
17261
17491
  constructor(message6, file) {
17262
17492
  super(message6);
@@ -17267,15 +17497,15 @@ var EditorInstallError = class extends Error {
17267
17497
  };
17268
17498
  function editorToolsRoot() {
17269
17499
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17270
- if (override) return import_node_path33.default.resolve(override);
17271
- if (process.platform === "darwin") return import_node_path33.default.join(import_node_os3.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
17500
+ if (override) return import_node_path34.default.resolve(override);
17501
+ if (process.platform === "darwin") return import_node_path34.default.join(import_node_os3.default.homedir(), "Library", "Caches", "pixelkiln", "tools");
17272
17502
  if (process.platform === "win32") {
17273
- return import_node_path33.default.join(process.env.LOCALAPPDATA ?? import_node_path33.default.join(import_node_os3.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17503
+ return import_node_path34.default.join(process.env.LOCALAPPDATA ?? import_node_path34.default.join(import_node_os3.default.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17274
17504
  }
17275
- return import_node_path33.default.join(process.env.XDG_CACHE_HOME ?? import_node_path33.default.join(import_node_os3.default.homedir(), ".cache"), "pixelkiln", "tools");
17505
+ return import_node_path34.default.join(process.env.XDG_CACHE_HOME ?? import_node_path34.default.join(import_node_os3.default.homedir(), ".cache"), "pixelkiln", "tools");
17276
17506
  }
17277
17507
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17278
- return import_node_path33.default.join(root, "pixelorama", pin.release ?? "unpublished");
17508
+ return import_node_path34.default.join(root, "pixelorama", pin.release ?? "unpublished");
17279
17509
  }
17280
17510
  function editorBaseUrl(pin = EDITOR_PIN) {
17281
17511
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17285,7 +17515,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17285
17515
  }
17286
17516
  async function sha256Of(file) {
17287
17517
  try {
17288
- return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises26.readFile)(file)).digest("hex");
17518
+ return (0, import_node_crypto7.createHash)("sha256").update(await (0, import_promises27.readFile)(file)).digest("hex");
17289
17519
  } catch {
17290
17520
  return null;
17291
17521
  }
@@ -17298,8 +17528,8 @@ async function editorStatus(opts = {}) {
17298
17528
  let totalBytes = 0;
17299
17529
  for (const [name, expected] of Object.entries(pin.files)) {
17300
17530
  totalBytes += expected.bytes;
17301
- const file = import_node_path33.default.join(dir, name);
17302
- const info = (0, import_node_fs29.existsSync)(file) ? await (0, import_promises26.stat)(file).catch(() => null) : null;
17531
+ const file = import_node_path34.default.join(dir, name);
17532
+ const info = (0, import_node_fs30.existsSync)(file) ? await (0, import_promises27.stat)(file).catch(() => null) : null;
17303
17533
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17304
17534
  installedBytes += expected.bytes;
17305
17535
  } else {
@@ -17328,7 +17558,7 @@ async function installEditor(opts = {}) {
17328
17558
  );
17329
17559
  }
17330
17560
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
17331
- await (0, import_promises26.mkdir)(dir, { recursive: true });
17561
+ await (0, import_promises27.mkdir)(dir, { recursive: true });
17332
17562
  const downloaded = [];
17333
17563
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
17334
17564
  let fetchedBytes = before.installedBytes;
@@ -17356,13 +17586,13 @@ async function installEditor(opts = {}) {
17356
17586
  if (digest2 !== expected.sha256) {
17357
17587
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17358
17588
  }
17359
- const final = import_node_path33.default.join(dir, name);
17589
+ const final = import_node_path34.default.join(dir, name);
17360
17590
  const tmp = `${final}.${process.pid}.part`;
17361
17591
  try {
17362
- await (0, import_promises26.writeFile)(tmp, bytes);
17363
- await (0, import_promises26.rename)(tmp, final);
17592
+ await (0, import_promises27.writeFile)(tmp, bytes);
17593
+ await (0, import_promises27.rename)(tmp, final);
17364
17594
  } finally {
17365
- await (0, import_promises26.rm)(tmp, { force: true });
17595
+ await (0, import_promises27.rm)(tmp, { force: true });
17366
17596
  }
17367
17597
  fetchedBytes += bytes.length;
17368
17598
  downloaded.push(name);
@@ -17402,12 +17632,12 @@ var EDITOR_CONTENT_TYPES = {
17402
17632
  ".json": "application/json; charset=utf-8"
17403
17633
  };
17404
17634
  function editorContentType(name) {
17405
- return EDITOR_CONTENT_TYPES[import_node_path33.default.extname(name).toLowerCase()] ?? null;
17635
+ return EDITOR_CONTENT_TYPES[import_node_path34.default.extname(name).toLowerCase()] ?? null;
17406
17636
  }
17407
17637
 
17408
17638
  // src/gallery/editor.ts
17409
- var import_promises27 = require("fs/promises");
17410
- var import_node_path34 = __toESM(require("path"), 1);
17639
+ var import_promises28 = require("fs/promises");
17640
+ var import_node_path35 = __toESM(require("path"), 1);
17411
17641
  function editorRoute(release, name = "index.html") {
17412
17642
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
17413
17643
  }
@@ -17480,7 +17710,7 @@ function createGalleryEditorHandlers(opts = {}) {
17480
17710
  if (!status.installed) return null;
17481
17711
  let bytes;
17482
17712
  try {
17483
- bytes = await (0, import_promises27.readFile)(import_node_path34.default.join(dir, name));
17713
+ bytes = await (0, import_promises28.readFile)(import_node_path35.default.join(dir, name));
17484
17714
  } catch {
17485
17715
  verified = null;
17486
17716
  return null;
@@ -17497,7 +17727,7 @@ function createGalleryEditorHandlers(opts = {}) {
17497
17727
  // src/gallery/server.ts
17498
17728
  var import_node_crypto8 = require("crypto");
17499
17729
  var import_node_http2 = require("http");
17500
- var import_promises28 = require("fs/promises");
17730
+ var import_promises29 = require("fs/promises");
17501
17731
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
17502
17732
  var EDITOR_CSP = [
17503
17733
  "default-src 'self'",
@@ -17678,7 +17908,7 @@ async function serveGallery(opts) {
17678
17908
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17679
17909
  if (!asset) return fail(404, "no such review asset");
17680
17910
  try {
17681
- const bytes = await (0, import_promises28.readFile)(asset.path);
17911
+ const bytes = await (0, import_promises29.readFile)(asset.path);
17682
17912
  res.writeHead(200, {
17683
17913
  "Content-Type": asset.contentType,
17684
17914
  "Cache-Control": "no-store",
@@ -17761,7 +17991,7 @@ async function serveGallery(opts) {
17761
17991
  if (!asset) return fail(404, "no such gallery media");
17762
17992
  let bytes;
17763
17993
  try {
17764
- bytes = await (0, import_promises28.readFile)(asset.path);
17994
+ bytes = await (0, import_promises29.readFile)(asset.path);
17765
17995
  } catch {
17766
17996
  return fail(404, "gallery media is no longer on disk; refresh the page");
17767
17997
  }
@@ -17805,27 +18035,27 @@ async function serveGallery(opts) {
17805
18035
 
17806
18036
  // src/gallery/generate.ts
17807
18037
  var import_node_crypto9 = require("crypto");
17808
- var import_zod13 = require("zod");
17809
- var GenerateRequestSchema = import_zod13.z.object({
17810
- project: import_zod13.z.string().min(1).optional(),
17811
- keys: import_zod13.z.array(import_zod13.z.string().min(1)).min(1).max(500),
18038
+ var import_zod14 = require("zod");
18039
+ var GenerateRequestSchema = import_zod14.z.object({
18040
+ project: import_zod14.z.string().min(1).optional(),
18041
+ keys: import_zod14.z.array(import_zod14.z.string().min(1)).min(1).max(500),
17812
18042
  /**
17813
18043
  * Regenerate work that is up to date, as `gen --force` does, and let the
17814
18044
  * fetch replace a locally modified or untracked file. With `restore`, put
17815
18045
  * the recorded bytes back over a modified file.
17816
18046
  */
17817
- force: import_zod13.z.boolean().optional(),
18047
+ force: import_zod14.z.boolean().optional(),
17818
18048
  /** Advance in-flight, review, or selected work without submitting anything. */
17819
- resume: import_zod13.z.boolean().optional(),
18049
+ resume: import_zod14.z.boolean().optional(),
17820
18050
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
17821
- refresh: import_zod13.z.boolean().optional(),
18051
+ refresh: import_zod14.z.boolean().optional(),
17822
18052
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
17823
- restore: import_zod13.z.boolean().optional(),
18053
+ restore: import_zod14.z.boolean().optional(),
17824
18054
  /**
17825
18055
  * Bring a replaced generation back: 1-based position in the record's
17826
18056
  * history (newest first) or an output hash prefix. One key at a time.
17827
18057
  */
17828
- revert: import_zod13.z.string().min(1).max(64).optional()
18058
+ revert: import_zod14.z.string().min(1).max(64).optional()
17829
18059
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
17830
18060
  message: "resume, refresh, restore, and revert are exclusive"
17831
18061
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -18171,19 +18401,19 @@ function createGenerateHandlers(opts) {
18171
18401
  }
18172
18402
 
18173
18403
  // src/pipeline/salvage.ts
18174
- var import_promises29 = require("fs/promises");
18175
- var import_node_fs30 = require("fs");
18176
- var import_node_path35 = __toESM(require("path"), 1);
18404
+ var import_promises30 = require("fs/promises");
18405
+ var import_node_fs31 = require("fs");
18406
+ var import_node_path36 = __toESM(require("path"), 1);
18177
18407
  function providerClaimId(provider, objectId) {
18178
18408
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
18179
18409
  }
18180
18410
  async function loadClaims(lockPaths, opts = {}) {
18181
18411
  const claimed = /* @__PURE__ */ new Set();
18182
18412
  for (const p of lockPaths) {
18183
- if (!(0, import_node_fs30.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
18413
+ if (!(0, import_node_fs31.existsSync)(p)) throw new Error(`Claim lockfile not found: ${p}`);
18184
18414
  let parsed;
18185
18415
  try {
18186
- parsed = parseLock(JSON.parse(await (0, import_promises29.readFile)(p, "utf8")));
18416
+ parsed = parseLock(JSON.parse(await (0, import_promises30.readFile)(p, "utf8")));
18187
18417
  } catch {
18188
18418
  throw new Error(`Claim lockfile is malformed: ${p}`);
18189
18419
  }
@@ -18287,20 +18517,20 @@ function matchOrphanStyle(prompt, manifest) {
18287
18517
  return matchStyleByPattern(prompt, manifest);
18288
18518
  }
18289
18519
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
18290
- const own = import_node_path35.default.resolve(ownManifestPath);
18520
+ const own = import_node_path36.default.resolve(ownManifestPath);
18291
18521
  const siblingManifestPaths = [
18292
18522
  .../* @__PURE__ */ new Set([
18293
18523
  ...workspaceManifestPaths,
18294
- ...claimPaths.map((c) => import_node_path35.default.join(import_node_path35.default.dirname(import_node_path35.default.resolve(c)), "pixelkiln.manifest.json"))
18524
+ ...claimPaths.map((c) => import_node_path36.default.join(import_node_path36.default.dirname(import_node_path36.default.resolve(c)), "pixelkiln.manifest.json"))
18295
18525
  ])
18296
18526
  ];
18297
18527
  const siblings = [];
18298
18528
  for (const siblingManifestPath of siblingManifestPaths) {
18299
- if (import_node_path35.default.resolve(siblingManifestPath) === own) continue;
18300
- if (!(0, import_node_fs30.existsSync)(siblingManifestPath)) continue;
18529
+ if (import_node_path36.default.resolve(siblingManifestPath) === own) continue;
18530
+ if (!(0, import_node_fs31.existsSync)(siblingManifestPath)) continue;
18301
18531
  try {
18302
18532
  const { manifest } = await loadManifest(siblingManifestPath);
18303
- siblings.push({ label: import_node_path35.default.basename(import_node_path35.default.dirname(siblingManifestPath)), manifest });
18533
+ siblings.push({ label: import_node_path36.default.basename(import_node_path36.default.dirname(siblingManifestPath)), manifest });
18304
18534
  } catch {
18305
18535
  }
18306
18536
  }
@@ -18420,8 +18650,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18420
18650
  }
18421
18651
 
18422
18652
  // src/pick/salvage-server.ts
18423
- var import_promises30 = require("fs/promises");
18424
- var import_node_path36 = __toESM(require("path"), 1);
18653
+ var import_promises31 = require("fs/promises");
18654
+ var import_node_path37 = __toESM(require("path"), 1);
18425
18655
 
18426
18656
  // src/pick/salvage-sheet.ts
18427
18657
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -18587,7 +18817,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18587
18817
  });
18588
18818
  const html = renderSalvageSheet(orphans, {
18589
18819
  styleId: ctx.styleId,
18590
- importDir: import_node_path36.default.relative(process.cwd(), ctx.importDir) || "."
18820
+ importDir: import_node_path37.default.relative(process.cwd(), ctx.importDir) || "."
18591
18821
  });
18592
18822
  const byId = new Map(orphans.map((o) => [o.id, o]));
18593
18823
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -18643,10 +18873,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18643
18873
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
18644
18874
  decodePng(buf);
18645
18875
  const assetId = idFromPrompt(orphan.prompt, taken);
18646
- const rel = import_node_path36.default.join("_salvaged", `${assetId}.png`);
18647
- const outFile = import_node_path36.default.resolve(ctx.importDir, rel);
18648
- await (0, import_promises30.mkdir)(import_node_path36.default.dirname(outFile), { recursive: true });
18649
- await (0, import_promises30.writeFile)(outFile, buf);
18876
+ const rel = import_node_path37.default.join("_salvaged", `${assetId}.png`);
18877
+ const outFile = import_node_path37.default.resolve(ctx.importDir, rel);
18878
+ await (0, import_promises31.mkdir)(import_node_path37.default.dirname(outFile), { recursive: true });
18879
+ await (0, import_promises31.writeFile)(outFile, buf);
18650
18880
  ctx.manifest.assets[assetId] = {
18651
18881
  prompt: orphan.prompt,
18652
18882
  promptByStyle: {},
@@ -18678,7 +18908,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18678
18908
  sourceUrl: durableSource,
18679
18909
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
18680
18910
  outputs: [{
18681
- path: portableOutputPath(outFile, import_node_path36.default.dirname(ctx.manifestPath)),
18911
+ path: portableOutputPath(outFile, import_node_path37.default.dirname(ctx.manifestPath)),
18682
18912
  sha256: sha256(buf)
18683
18913
  }],
18684
18914
  submittedAt: orphan.createdAt,
@@ -18702,9 +18932,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18702
18932
  }
18703
18933
  }
18704
18934
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18705
- const raw = JSON.parse(await (0, import_promises30.readFile)(ctx.manifestPath, "utf8"));
18935
+ const raw = JSON.parse(await (0, import_promises31.readFile)(ctx.manifestPath, "utf8"));
18706
18936
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
18707
- await (0, import_promises30.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18937
+ await (0, import_promises31.writeFile)(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18708
18938
  await saveLock(ctx.lockPath, ctx.lock);
18709
18939
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18710
18940
  return result;