pixelkiln 0.82.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 the
15454
- * gallery does not offer yet, so only `image-to-image` and
15455
- * `animate-skeleton` (whose keypoints file the gallery writes first) can
15456
- * be created here; hand-edit the manifest for the other modes.
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({
@@ -15462,6 +15596,52 @@ var NewAssetSchema = import_zod8.z.object({
15462
15596
  from: import_zod8.z.string().min(1),
15463
15597
  strength: import_zod8.z.number().min(0).max(1).optional()
15464
15598
  }).strict(),
15599
+ import_zod8.z.object({
15600
+ mode: import_zod8.z.literal("reduce-colors"),
15601
+ from: import_zod8.z.string().min(1),
15602
+ /** A target colour count; the gallery leaves PixelLab's default when this is omitted. */
15603
+ numColors: import_zod8.z.number().int().min(2).max(256).optional(),
15604
+ dithering: RevisionDitheringSchema.optional(),
15605
+ ditheringStrength: import_zod8.z.number().min(0).max(10).optional()
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(),
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(),
15628
+ import_zod8.z.object({
15629
+ mode: import_zod8.z.enum(["animate", "animate-pixminimax"]),
15630
+ from: import_zod8.z.string().min(1),
15631
+ frames: import_zod8.z.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
15632
+ fps: import_zod8.z.number().int().min(1).max(60).optional(),
15633
+ enhancePrompt: import_zod8.z.boolean().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()
15639
+ }).strict(),
15640
+ import_zod8.z.object({
15641
+ mode: import_zod8.z.literal("edit-animation"),
15642
+ from: import_zod8.z.string().min(1),
15643
+ fps: import_zod8.z.number().int().min(1).max(60).optional()
15644
+ }).strict(),
15465
15645
  import_zod8.z.object({
15466
15646
  mode: import_zod8.z.literal("animate-skeleton"),
15467
15647
  from: import_zod8.z.string().min(1),
@@ -16106,62 +16286,118 @@ async function saveProjectImage(root, request) {
16106
16286
  return relative.split(import_node_path29.default.sep).join("/");
16107
16287
  }
16108
16288
 
16109
- // src/gallery/skeleton.ts
16289
+ // src/gallery/image-revision.ts
16110
16290
  var import_node_fs26 = require("fs");
16111
16291
  var import_promises23 = require("fs/promises");
16112
16292
  var import_node_path30 = __toESM(require("path"), 1);
16113
16293
  var import_zod11 = require("zod");
16114
- var SkeletonEstimateRequestSchema = import_zod11.z.object({
16115
- project: import_zod11.z.string().min(1).optional(),
16116
- styleId: import_zod11.z.string().min(1),
16117
- assetId: import_zod11.z.string().min(1),
16118
- frames: import_zod11.z.number().int().min(3).max(15).optional()
16119
- }).strict();
16120
- var SkeletonAnimationRequestSchema = import_zod11.z.object({
16121
- action: import_zod11.z.literal("create-skeleton-animation"),
16294
+ var InpaintRevisionRequestSchema = import_zod11.z.object({
16295
+ action: import_zod11.z.literal("create-inpaint-revision"),
16122
16296
  project: import_zod11.z.string().min(1).optional(),
16123
16297
  expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/),
16124
16298
  styleId: import_zod11.z.string().min(1),
16125
- /** The sprite the animation poses. */
16299
+ /** The sprite being repainted. */
16126
16300
  from: import_zod11.z.string().min(1),
16127
16301
  assetId: import_zod11.z.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
16128
- /** The motion, sent as PixelLab's `action`. */
16302
+ /** What to paint into the masked area. */
16129
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),
16130
16366
  direction: CharacterDirectionSchema,
16131
- description: import_zod11.z.string().min(1).optional(),
16132
- 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(),
16133
16369
  /** Manifest-relative JSON path inside the project. */
16134
- keypointsFile: import_zod11.z.string().min(1),
16370
+ keypointsFile: import_zod12.z.string().min(1),
16135
16371
  /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
16136
16372
  set: SkeletonSetSchema.optional(),
16137
16373
  /** Replace an existing `keypointsFile` with `set`. */
16138
- overwrite: import_zod11.z.boolean().optional()
16374
+ overwrite: import_zod12.z.boolean().optional()
16139
16375
  }).strict();
16140
- var SkeletonKeypointsUpdateSchema = import_zod11.z.object({
16141
- action: import_zod11.z.literal("update-skeleton-keypoints"),
16142
- project: import_zod11.z.string().min(1).optional(),
16143
- styleId: import_zod11.z.string().min(1),
16144
- 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),
16145
16381
  /** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
16146
- expectedSha256: import_zod11.z.string().regex(/^[0-9a-f]{64}$/),
16382
+ expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/),
16147
16383
  set: SkeletonSetSchema
16148
16384
  }).strict();
16149
16385
  async function createSkeletonAnimation(manifestPath, request) {
16150
- const root = import_node_path30.default.dirname(import_node_path30.default.resolve(manifestPath));
16151
- const file = import_node_path30.default.resolve(root, request.keypointsFile);
16152
- const relative = import_node_path30.default.relative(root, file);
16153
- 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)) {
16154
16390
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
16155
16391
  }
16156
- const existed = (0, import_node_fs26.existsSync)(file);
16392
+ const existed = (0, import_node_fs27.existsSync)(file);
16157
16393
  let previous = null;
16158
16394
  if (request.set) {
16159
16395
  if (existed && !request.overwrite) {
16160
16396
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
16161
16397
  }
16162
- if (existed) previous = await (0, import_promises23.readFile)(file);
16163
- await (0, import_promises23.mkdir)(import_node_path30.default.dirname(file), { recursive: true });
16164
- 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");
16165
16401
  } else if (!existed) {
16166
16402
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
16167
16403
  }
@@ -16176,7 +16412,7 @@ async function createSkeletonAnimation(manifestPath, request) {
16176
16412
  revision: {
16177
16413
  mode: "animate-skeleton",
16178
16414
  from: request.from,
16179
- keypointsFile: relative.split(import_node_path30.default.sep).join("/"),
16415
+ keypointsFile: relative.split(import_node_path31.default.sep).join("/"),
16180
16416
  direction: request.direction,
16181
16417
  ...request.description ? { description: request.description } : {},
16182
16418
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -16185,8 +16421,8 @@ async function createSkeletonAnimation(manifestPath, request) {
16185
16421
  });
16186
16422
  } catch (error) {
16187
16423
  if (request.set) {
16188
- if (previous) await (0, import_promises23.writeFile)(file, previous);
16189
- 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 });
16190
16426
  }
16191
16427
  throw error;
16192
16428
  }
@@ -16197,64 +16433,64 @@ async function updateSkeletonKeypoints(ctx, request) {
16197
16433
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
16198
16434
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
16199
16435
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
16200
- const relative = import_node_path30.default.relative(ctx.loaded.root, file);
16201
- 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)) {
16202
16438
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
16203
16439
  }
16204
- 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;
16205
16441
  if (current !== request.expectedSha256) {
16206
16442
  throw Object.assign(
16207
- 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`),
16208
16444
  { status: 409 }
16209
16445
  );
16210
16446
  }
16211
- await (0, import_promises23.writeFile)(file, JSON.stringify(request.set, null, 2) + "\n");
16212
- 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("/");
16213
16449
  }
16214
16450
 
16215
16451
  // src/gallery/edit.ts
16216
- var Base64 = import_zod12.z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
16217
- var HandEditRequestSchema = import_zod12.z.discriminatedUnion("action", [
16218
- import_zod12.z.object({
16219
- action: import_zod12.z.literal("start-edit"),
16220
- project: import_zod12.z.string().min(1).optional(),
16221
- assetId: import_zod12.z.string().min(1),
16222
- styleId: import_zod12.z.string().min(1),
16223
- 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}$/),
16224
16460
  /** Also launch the author's editor on the file. */
16225
- open: import_zod12.z.boolean().optional()
16461
+ open: import_zod13.z.boolean().optional()
16226
16462
  }).strict(),
16227
- import_zod12.z.object({
16228
- action: import_zod12.z.literal("detach-edit"),
16229
- project: import_zod12.z.string().min(1).optional(),
16230
- assetId: import_zod12.z.string().min(1),
16231
- styleId: import_zod12.z.string().min(1),
16232
- 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}$/)
16233
16469
  }).strict(),
16234
16470
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
16235
- import_zod12.z.object({
16236
- action: import_zod12.z.literal("save-edit"),
16237
- project: import_zod12.z.string().min(1).optional(),
16238
- assetId: import_zod12.z.string().min(1),
16239
- styleId: import_zod12.z.string().min(1),
16240
- 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}$/),
16241
16477
  /** The flattened image; for a frame set, `frames` instead. */
16242
16478
  png: Base64.min(1).optional(),
16243
16479
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
16244
- 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(),
16245
16481
  /** The editor's layered project file, kept beside the edit. */
16246
16482
  pxo: Base64.optional(),
16247
16483
  /** Editor identity from the bridge's ready message. */
16248
- editor: import_zod12.z.string().min(1).max(200),
16249
- 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()
16250
16486
  }).strict()
16251
16487
  ]);
16252
16488
  var UNDO_DEPTH = 20;
16253
- var UndoRequestSchema = import_zod12.z.object({
16254
- action: import_zod12.z.literal("undo-edit"),
16255
- 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(),
16256
16492
  /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
16257
- expectedSha256: import_zod12.z.string().regex(/^[0-9a-f]{64}$/)
16493
+ expectedSha256: import_zod13.z.string().regex(/^[0-9a-f]{64}$/)
16258
16494
  }).strict();
16259
16495
  function createGalleryEditHandler(opts) {
16260
16496
  const log = opts.onProgress ?? (() => {
@@ -16263,7 +16499,7 @@ function createGalleryEditHandler(opts) {
16263
16499
  const history = /* @__PURE__ */ new Map();
16264
16500
  const manifestOf = async (project) => {
16265
16501
  try {
16266
- 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));
16267
16503
  } catch {
16268
16504
  return null;
16269
16505
  }
@@ -16273,7 +16509,7 @@ function createGalleryEditHandler(opts) {
16273
16509
  if (undo.success) {
16274
16510
  const manifestPath2 = await manifestOf(undo.data.project);
16275
16511
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
16276
- const current = await (0, import_promises24.readFile)(manifestPath2, "utf8");
16512
+ const current = await (0, import_promises25.readFile)(manifestPath2, "utf8");
16277
16513
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
16278
16514
  const stack = history.get(manifestPath2) ?? [];
16279
16515
  const last = stack[stack.length - 1];
@@ -16283,17 +16519,17 @@ function createGalleryEditHandler(opts) {
16283
16519
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
16284
16520
  }
16285
16521
  const tmp = manifestPath2 + ".pixelkiln-undo";
16286
- await (0, import_promises24.writeFile)(tmp, last.before);
16287
- await (0, import_promises24.rename)(tmp, manifestPath2);
16522
+ await (0, import_promises25.writeFile)(tmp, last.before);
16523
+ await (0, import_promises25.rename)(tmp, manifestPath2);
16288
16524
  stack.pop();
16289
- 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}`);
16290
16526
  return opts.reload();
16291
16527
  }
16292
16528
  const manifestPath = await manifestOf(body?.project);
16293
- 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;
16294
16530
  const build = await apply(body);
16295
16531
  if (manifestPath && before !== null) {
16296
- const after = await (0, import_promises24.readFile)(manifestPath, "utf8").catch(() => null);
16532
+ const after = await (0, import_promises25.readFile)(manifestPath, "utf8").catch(() => null);
16297
16533
  if (after !== null && after !== before) {
16298
16534
  const stack = history.get(manifestPath) ?? [];
16299
16535
  stack.push({ before, afterSha: sha256(after) });
@@ -16348,7 +16584,7 @@ function createEditApplier(opts, log) {
16348
16584
  } catch (error) {
16349
16585
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16350
16586
  }
16351
- 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);
16352
16588
  log(` image added: ${written}`);
16353
16589
  return opts.reload();
16354
16590
  }
@@ -16384,6 +16620,23 @@ function createEditApplier(opts, log) {
16384
16620
  "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16385
16621
  );
16386
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
+ }
16387
16640
  const parsed = ManifestEditSchema.safeParse(body);
16388
16641
  if (!parsed.success) {
16389
16642
  throw new ManifestEditError(
@@ -16398,7 +16651,7 @@ function createEditApplier(opts, log) {
16398
16651
  }
16399
16652
  const result = await applyManifestEdit(manifestPath, parsed.data);
16400
16653
  if (result.changed) {
16401
- const relative = import_node_path31.default.relative(process.cwd(), result.manifestPath);
16654
+ const relative = import_node_path32.default.relative(process.cwd(), result.manifestPath);
16402
16655
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
16403
16656
  log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
16404
16657
  }
@@ -16426,7 +16679,7 @@ function describeEdit(edit) {
16426
16679
  // src/gallery/snapshot.ts
16427
16680
  var PROJECT_FILE_TYPE = "application/octet-stream";
16428
16681
  function galleryMediaId(absolutePath) {
16429
- 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);
16430
16683
  }
16431
16684
  function galleryMediaRoute(id) {
16432
16685
  return `/media/${id}`;
@@ -16445,9 +16698,9 @@ function describeHistory(media, entry, cacheDir) {
16445
16698
  return (entry.history ?? []).map((generation, i) => {
16446
16699
  const outputs = generation.outputs.map((output) => {
16447
16700
  const mediaType = output.mediaType ?? MediaType.PNG;
16448
- 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));
16449
16702
  let url = null;
16450
- if ((0, import_node_fs27.existsSync)(file)) {
16703
+ if ((0, import_node_fs28.existsSync)(file)) {
16451
16704
  const id = galleryMediaId(file);
16452
16705
  media.set(id, { path: file, contentType: mediaType });
16453
16706
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -16479,7 +16732,7 @@ function describeHistory(media, entry, cacheDir) {
16479
16732
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16480
16733
  async function samePixels(a, b) {
16481
16734
  try {
16482
- 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)]);
16483
16736
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16484
16737
  const x = decodePng(left);
16485
16738
  const y = decodePng(right);
@@ -16545,7 +16798,7 @@ function describeCharacter(spec, entry) {
16545
16798
  }
16546
16799
  async function fileInfo(absolutePath) {
16547
16800
  try {
16548
- const info = await (0, import_promises25.stat)(absolutePath);
16801
+ const info = await (0, import_promises26.stat)(absolutePath);
16549
16802
  return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
16550
16803
  } catch {
16551
16804
  return null;
@@ -16577,10 +16830,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
16577
16830
  async function describeQuality(media, root, inspection) {
16578
16831
  const outputPaths = inspection.outputs ?? [inspection.output];
16579
16832
  const outputs = await Promise.all(
16580
- 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)))
16581
16834
  );
16582
- const recordPath = import_node_path32.default.resolve(root, inspection.record);
16583
- 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);
16584
16837
  let options = null;
16585
16838
  let check = null;
16586
16839
  if (recordExists) {
@@ -16599,7 +16852,7 @@ async function describeQuality(media, root, inspection) {
16599
16852
  return {
16600
16853
  state: inspection.state,
16601
16854
  reason: inspection.reason,
16602
- output: portableOutputPath(import_node_path32.default.resolve(root, inspection.output), root),
16855
+ output: portableOutputPath(import_node_path33.default.resolve(root, inspection.output), root),
16603
16856
  record: portableOutputPath(recordPath, root),
16604
16857
  recordExists,
16605
16858
  outputs,
@@ -16637,9 +16890,9 @@ async function buildGallerySnapshot(opts) {
16637
16890
  const media = /* @__PURE__ */ new Map();
16638
16891
  const plan = await buildPlan(specs, lock);
16639
16892
  const items = [];
16640
- const manifestText = await (0, import_promises25.readFile)(loaded.path, "utf8");
16893
+ const manifestText = await (0, import_promises26.readFile)(loaded.path, "utf8");
16641
16894
  const manifestSha256 = sha256(manifestText);
16642
- 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");
16643
16896
  let rawStyles = {};
16644
16897
  try {
16645
16898
  const raw = JSON.parse(manifestText);
@@ -16663,12 +16916,12 @@ async function buildGallerySnapshot(opts) {
16663
16916
  )
16664
16917
  );
16665
16918
  if (spec.source) {
16666
- const editPath = import_node_path32.default.resolve(root, spec.source);
16919
+ const editPath = import_node_path33.default.resolve(root, spec.source);
16667
16920
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
16668
16921
  const editShas = await Promise.all(
16669
16922
  Array.from({ length: memberCount }, async (_, index) => {
16670
16923
  const file = sourceOutputPath(spec.source, entry, index, root);
16671
- 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 };
16672
16925
  })
16673
16926
  );
16674
16927
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -16676,7 +16929,7 @@ async function buildGallerySnapshot(opts) {
16676
16929
  const companion = await readHandEditCompanion(editPath);
16677
16930
  if (companion) {
16678
16931
  const projectPath = handEditProjectPath(editPath);
16679
- 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;
16680
16933
  let projectUrl = null;
16681
16934
  if (project) {
16682
16935
  const id = galleryMediaId(projectPath);
@@ -16712,8 +16965,8 @@ async function buildGallerySnapshot(opts) {
16712
16965
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
16713
16966
  }
16714
16967
  } else if (spec.source) {
16715
- outputs = [await describeOutput(media, root, import_node_path32.default.resolve(root, spec.source))];
16716
- } 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)) {
16717
16970
  outputs = [await describeOutput(media, root, spec.outFile)];
16718
16971
  } else {
16719
16972
  outputs = [];
@@ -16916,7 +17169,7 @@ async function buildGallerySnapshot(opts) {
16916
17169
  id: loaded.manifest.name,
16917
17170
  name: loaded.manifest.name,
16918
17171
  manifest: loaded.path,
16919
- lock: import_node_path32.default.resolve(opts.lockPath),
17172
+ lock: import_node_path33.default.resolve(opts.lockPath),
16920
17173
  root,
16921
17174
  provider: loaded.manifest.provider,
16922
17175
  account: null,
@@ -16942,7 +17195,7 @@ async function buildGallerySnapshot(opts) {
16942
17195
  return { snapshot, media };
16943
17196
  }
16944
17197
  async function buildWorkspaceGallerySnapshot(opts) {
16945
- 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));
16946
17199
  const media = /* @__PURE__ */ new Map();
16947
17200
  const projects = [];
16948
17201
  const items = [];
@@ -16985,11 +17238,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
16985
17238
  id: project.id,
16986
17239
  name: loaded.manifest.name,
16987
17240
  manifest: manifestPath,
16988
- lock: import_node_path32.default.resolve(lockPath),
17241
+ lock: import_node_path33.default.resolve(lockPath),
16989
17242
  root: loaded.root,
16990
17243
  provider: loaded.manifest.provider,
16991
17244
  account: project.account ?? null,
16992
- manifestSha256: manifestSha256 ?? sha256(await (0, import_promises25.readFile)(manifestPath, "utf8")),
17245
+ manifestSha256: manifestSha256 ?? sha256(await (0, import_promises26.readFile)(manifestPath, "utf8")),
16993
17246
  historyLimit: historyLimit(loaded.manifest),
16994
17247
  entries: Object.keys(lock.entries).length,
16995
17248
  items: projectItems.length,
@@ -17001,8 +17254,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
17001
17254
  id: project.id,
17002
17255
  name: project.id,
17003
17256
  manifest: manifestPath,
17004
- lock: import_node_path32.default.resolve(lockPath),
17005
- root: import_node_path32.default.dirname(manifestPath),
17257
+ lock: import_node_path33.default.resolve(lockPath),
17258
+ root: import_node_path33.default.dirname(manifestPath),
17006
17259
  provider: project.provider,
17007
17260
  account: project.account ?? null,
17008
17261
  manifestSha256: null,
@@ -17029,7 +17282,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17029
17282
  version: 1,
17030
17283
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
17031
17284
  project: null,
17032
- workspace: { path: import_node_path32.default.resolve(opts.workspacePath), dir, projects },
17285
+ workspace: { path: import_node_path33.default.resolve(opts.workspacePath), dir, projects },
17033
17286
  filter,
17034
17287
  totals: {
17035
17288
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -17046,7 +17299,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
17046
17299
  }
17047
17300
 
17048
17301
  // src/gallery/page.ts
17049
- var import_node_fs28 = require("fs");
17302
+ var import_node_fs29 = require("fs");
17050
17303
 
17051
17304
  // src/gallery/client-bundle.ts
17052
17305
  var import_node_module = require("module");
@@ -17086,8 +17339,8 @@ var client;
17086
17339
  function clientAssets() {
17087
17340
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
17088
17341
  client ??= {
17089
- css: (0, import_node_fs28.readFileSync)(new URL("gallery.css", CLIENT_DIR), "utf8"),
17090
- 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()
17091
17344
  };
17092
17345
  return client;
17093
17346
  }
@@ -17230,10 +17483,10 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
17230
17483
 
17231
17484
  // src/editor/install.ts
17232
17485
  var import_node_crypto7 = require("crypto");
17233
- var import_node_fs29 = require("fs");
17234
- var import_promises26 = require("fs/promises");
17486
+ var import_node_fs30 = require("fs");
17487
+ var import_promises27 = require("fs/promises");
17235
17488
  var import_node_os3 = __toESM(require("os"), 1);
17236
- var import_node_path33 = __toESM(require("path"), 1);
17489
+ var import_node_path34 = __toESM(require("path"), 1);
17237
17490
  var EditorInstallError = class extends Error {
17238
17491
  constructor(message6, file) {
17239
17492
  super(message6);
@@ -17244,15 +17497,15 @@ var EditorInstallError = class extends Error {
17244
17497
  };
17245
17498
  function editorToolsRoot() {
17246
17499
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
17247
- if (override) return import_node_path33.default.resolve(override);
17248
- 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");
17249
17502
  if (process.platform === "win32") {
17250
- 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");
17251
17504
  }
17252
- 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");
17253
17506
  }
17254
17507
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
17255
- return import_node_path33.default.join(root, "pixelorama", pin.release ?? "unpublished");
17508
+ return import_node_path34.default.join(root, "pixelorama", pin.release ?? "unpublished");
17256
17509
  }
17257
17510
  function editorBaseUrl(pin = EDITOR_PIN) {
17258
17511
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -17262,7 +17515,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
17262
17515
  }
17263
17516
  async function sha256Of(file) {
17264
17517
  try {
17265
- 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");
17266
17519
  } catch {
17267
17520
  return null;
17268
17521
  }
@@ -17275,8 +17528,8 @@ async function editorStatus(opts = {}) {
17275
17528
  let totalBytes = 0;
17276
17529
  for (const [name, expected] of Object.entries(pin.files)) {
17277
17530
  totalBytes += expected.bytes;
17278
- const file = import_node_path33.default.join(dir, name);
17279
- 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;
17280
17533
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
17281
17534
  installedBytes += expected.bytes;
17282
17535
  } else {
@@ -17305,7 +17558,7 @@ async function installEditor(opts = {}) {
17305
17558
  );
17306
17559
  }
17307
17560
  if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
17308
- await (0, import_promises26.mkdir)(dir, { recursive: true });
17561
+ await (0, import_promises27.mkdir)(dir, { recursive: true });
17309
17562
  const downloaded = [];
17310
17563
  const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
17311
17564
  let fetchedBytes = before.installedBytes;
@@ -17333,13 +17586,13 @@ async function installEditor(opts = {}) {
17333
17586
  if (digest2 !== expected.sha256) {
17334
17587
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17335
17588
  }
17336
- const final = import_node_path33.default.join(dir, name);
17589
+ const final = import_node_path34.default.join(dir, name);
17337
17590
  const tmp = `${final}.${process.pid}.part`;
17338
17591
  try {
17339
- await (0, import_promises26.writeFile)(tmp, bytes);
17340
- await (0, import_promises26.rename)(tmp, final);
17592
+ await (0, import_promises27.writeFile)(tmp, bytes);
17593
+ await (0, import_promises27.rename)(tmp, final);
17341
17594
  } finally {
17342
- await (0, import_promises26.rm)(tmp, { force: true });
17595
+ await (0, import_promises27.rm)(tmp, { force: true });
17343
17596
  }
17344
17597
  fetchedBytes += bytes.length;
17345
17598
  downloaded.push(name);
@@ -17379,12 +17632,12 @@ var EDITOR_CONTENT_TYPES = {
17379
17632
  ".json": "application/json; charset=utf-8"
17380
17633
  };
17381
17634
  function editorContentType(name) {
17382
- 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;
17383
17636
  }
17384
17637
 
17385
17638
  // src/gallery/editor.ts
17386
- var import_promises27 = require("fs/promises");
17387
- var import_node_path34 = __toESM(require("path"), 1);
17639
+ var import_promises28 = require("fs/promises");
17640
+ var import_node_path35 = __toESM(require("path"), 1);
17388
17641
  function editorRoute(release, name = "index.html") {
17389
17642
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
17390
17643
  }
@@ -17457,7 +17710,7 @@ function createGalleryEditorHandlers(opts = {}) {
17457
17710
  if (!status.installed) return null;
17458
17711
  let bytes;
17459
17712
  try {
17460
- 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));
17461
17714
  } catch {
17462
17715
  verified = null;
17463
17716
  return null;
@@ -17474,7 +17727,7 @@ function createGalleryEditorHandlers(opts = {}) {
17474
17727
  // src/gallery/server.ts
17475
17728
  var import_node_crypto8 = require("crypto");
17476
17729
  var import_node_http2 = require("http");
17477
- var import_promises28 = require("fs/promises");
17730
+ var import_promises29 = require("fs/promises");
17478
17731
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
17479
17732
  var EDITOR_CSP = [
17480
17733
  "default-src 'self'",
@@ -17655,7 +17908,7 @@ async function serveGallery(opts) {
17655
17908
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17656
17909
  if (!asset) return fail(404, "no such review asset");
17657
17910
  try {
17658
- const bytes = await (0, import_promises28.readFile)(asset.path);
17911
+ const bytes = await (0, import_promises29.readFile)(asset.path);
17659
17912
  res.writeHead(200, {
17660
17913
  "Content-Type": asset.contentType,
17661
17914
  "Cache-Control": "no-store",
@@ -17738,7 +17991,7 @@ async function serveGallery(opts) {
17738
17991
  if (!asset) return fail(404, "no such gallery media");
17739
17992
  let bytes;
17740
17993
  try {
17741
- bytes = await (0, import_promises28.readFile)(asset.path);
17994
+ bytes = await (0, import_promises29.readFile)(asset.path);
17742
17995
  } catch {
17743
17996
  return fail(404, "gallery media is no longer on disk; refresh the page");
17744
17997
  }
@@ -17782,27 +18035,27 @@ async function serveGallery(opts) {
17782
18035
 
17783
18036
  // src/gallery/generate.ts
17784
18037
  var import_node_crypto9 = require("crypto");
17785
- var import_zod13 = require("zod");
17786
- var GenerateRequestSchema = import_zod13.z.object({
17787
- project: import_zod13.z.string().min(1).optional(),
17788
- 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),
17789
18042
  /**
17790
18043
  * Regenerate work that is up to date, as `gen --force` does, and let the
17791
18044
  * fetch replace a locally modified or untracked file. With `restore`, put
17792
18045
  * the recorded bytes back over a modified file.
17793
18046
  */
17794
- force: import_zod13.z.boolean().optional(),
18047
+ force: import_zod14.z.boolean().optional(),
17795
18048
  /** Advance in-flight, review, or selected work without submitting anything. */
17796
- resume: import_zod13.z.boolean().optional(),
18049
+ resume: import_zod14.z.boolean().optional(),
17797
18050
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
17798
- refresh: import_zod13.z.boolean().optional(),
18051
+ refresh: import_zod14.z.boolean().optional(),
17799
18052
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
17800
- restore: import_zod13.z.boolean().optional(),
18053
+ restore: import_zod14.z.boolean().optional(),
17801
18054
  /**
17802
18055
  * Bring a replaced generation back: 1-based position in the record's
17803
18056
  * history (newest first) or an output hash prefix. One key at a time.
17804
18057
  */
17805
- revert: import_zod13.z.string().min(1).max(64).optional()
18058
+ revert: import_zod14.z.string().min(1).max(64).optional()
17806
18059
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
17807
18060
  message: "resume, refresh, restore, and revert are exclusive"
17808
18061
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -18148,19 +18401,19 @@ function createGenerateHandlers(opts) {
18148
18401
  }
18149
18402
 
18150
18403
  // src/pipeline/salvage.ts
18151
- var import_promises29 = require("fs/promises");
18152
- var import_node_fs30 = require("fs");
18153
- 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);
18154
18407
  function providerClaimId(provider, objectId) {
18155
18408
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
18156
18409
  }
18157
18410
  async function loadClaims(lockPaths, opts = {}) {
18158
18411
  const claimed = /* @__PURE__ */ new Set();
18159
18412
  for (const p of lockPaths) {
18160
- 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}`);
18161
18414
  let parsed;
18162
18415
  try {
18163
- parsed = parseLock(JSON.parse(await (0, import_promises29.readFile)(p, "utf8")));
18416
+ parsed = parseLock(JSON.parse(await (0, import_promises30.readFile)(p, "utf8")));
18164
18417
  } catch {
18165
18418
  throw new Error(`Claim lockfile is malformed: ${p}`);
18166
18419
  }
@@ -18264,20 +18517,20 @@ function matchOrphanStyle(prompt, manifest) {
18264
18517
  return matchStyleByPattern(prompt, manifest);
18265
18518
  }
18266
18519
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
18267
- const own = import_node_path35.default.resolve(ownManifestPath);
18520
+ const own = import_node_path36.default.resolve(ownManifestPath);
18268
18521
  const siblingManifestPaths = [
18269
18522
  .../* @__PURE__ */ new Set([
18270
18523
  ...workspaceManifestPaths,
18271
- ...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"))
18272
18525
  ])
18273
18526
  ];
18274
18527
  const siblings = [];
18275
18528
  for (const siblingManifestPath of siblingManifestPaths) {
18276
- if (import_node_path35.default.resolve(siblingManifestPath) === own) continue;
18277
- 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;
18278
18531
  try {
18279
18532
  const { manifest } = await loadManifest(siblingManifestPath);
18280
- 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 });
18281
18534
  } catch {
18282
18535
  }
18283
18536
  }
@@ -18397,8 +18650,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18397
18650
  }
18398
18651
 
18399
18652
  // src/pick/salvage-server.ts
18400
- var import_promises30 = require("fs/promises");
18401
- var import_node_path36 = __toESM(require("path"), 1);
18653
+ var import_promises31 = require("fs/promises");
18654
+ var import_node_path37 = __toESM(require("path"), 1);
18402
18655
 
18403
18656
  // src/pick/salvage-sheet.ts
18404
18657
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -18564,7 +18817,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18564
18817
  });
18565
18818
  const html = renderSalvageSheet(orphans, {
18566
18819
  styleId: ctx.styleId,
18567
- importDir: import_node_path36.default.relative(process.cwd(), ctx.importDir) || "."
18820
+ importDir: import_node_path37.default.relative(process.cwd(), ctx.importDir) || "."
18568
18821
  });
18569
18822
  const byId = new Map(orphans.map((o) => [o.id, o]));
18570
18823
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -18620,10 +18873,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18620
18873
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
18621
18874
  decodePng(buf);
18622
18875
  const assetId = idFromPrompt(orphan.prompt, taken);
18623
- const rel = import_node_path36.default.join("_salvaged", `${assetId}.png`);
18624
- const outFile = import_node_path36.default.resolve(ctx.importDir, rel);
18625
- await (0, import_promises30.mkdir)(import_node_path36.default.dirname(outFile), { recursive: true });
18626
- 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);
18627
18880
  ctx.manifest.assets[assetId] = {
18628
18881
  prompt: orphan.prompt,
18629
18882
  promptByStyle: {},
@@ -18655,7 +18908,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18655
18908
  sourceUrl: durableSource,
18656
18909
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
18657
18910
  outputs: [{
18658
- path: portableOutputPath(outFile, import_node_path36.default.dirname(ctx.manifestPath)),
18911
+ path: portableOutputPath(outFile, import_node_path37.default.dirname(ctx.manifestPath)),
18659
18912
  sha256: sha256(buf)
18660
18913
  }],
18661
18914
  submittedAt: orphan.createdAt,
@@ -18679,9 +18932,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18679
18932
  }
18680
18933
  }
18681
18934
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18682
- 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"));
18683
18936
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
18684
- 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");
18685
18938
  await saveLock(ctx.lockPath, ctx.lock);
18686
18939
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18687
18940
  return result;