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.js CHANGED
@@ -9,6 +9,7 @@ var RevisionModeSchema = z.enum([
9
9
  "outpaint",
10
10
  "reduce-colors",
11
11
  "correct-pixelart",
12
+ "remove-background",
12
13
  "animate",
13
14
  "animate-pixminimax",
14
15
  "animate-skeleton",
@@ -340,7 +341,22 @@ var RevisionSchema = z.object({
340
341
  * `interpolate`: required, the ending keyframe; the parent is the start.
341
342
  */
342
343
  lastFrame: z.string().min(1).optional(),
343
- /** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
344
+ /**
345
+ * `animate`/`animate-pixminimax` only: colour de-flicker sensitivity.
346
+ * Frames whose foreground drifts from the first frame beyond this are
347
+ * corrected toward it; 0 corrects every frame and higher values correct
348
+ * fewer. Omit for PixelLab's default.
349
+ */
350
+ driftThreshold: z.number().min(0).optional(),
351
+ /**
352
+ * `animate-pixminimax` only: one noun phrase for what the parent shows.
353
+ * PixelLab reads it off the image when omitted, and can decline some
354
+ * images, failing the job until it is sent.
355
+ */
356
+ subjectDescription: z.string().min(1).max(300).optional(),
357
+ /** `animate-pixminimax` only: the pose the parent is in, read off the image by PixelLab when omitted. */
358
+ initialPose: z.string().min(1).max(300).optional(),
359
+ /** `animate-pixminimax` only: facing direction, one of the model's caption fields, so it also holds the sprite's facing. */
344
360
  direction: CharacterDirectionSchema.optional(),
345
361
  /** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
346
362
  enhancePrompt: z.boolean().optional(),
@@ -360,12 +376,15 @@ var RevisionSchema = z.object({
360
376
  * same `mannequin`/`bear`/`cat`/`dog`/`horse`/`lion` vocabulary.
361
377
  */
362
378
  skeletonTemplate: z.string().min(1).optional(),
379
+ /** `remove-background` only: `simple` is faster and suits flat backgrounds; `complex` handles detailed edges. PixelLab defaults to `simple`. */
380
+ removalTask: z.enum(["simple", "complex"]).optional(),
363
381
  /**
364
- * `animate-skeleton` only: what the character *looks like*, a noun
382
+ * `animate-skeleton`: what the character *looks like*, a noun
365
383
  * phrase (colours, clothing, held items) — distinct from the asset's
366
384
  * own `prompt`, which this mode sends as the motion's short label
367
385
  * (PixelLab's `action`), the same `prompt`-as-motion convention
368
- * `animate`/`animate-pixminimax` already use.
386
+ * `animate`/`animate-pixminimax` already use. `remove-background`: a
387
+ * short description of the foreground, which PixelLab uses as a hint.
369
388
  */
370
389
  description: z.string().min(1).optional(),
371
390
  /**
@@ -482,7 +501,44 @@ var RevisionSchema = z.object({
482
501
  path: ["keypointsFile"]
483
502
  });
484
503
  }
485
- for (const field of ["keypointsFile", "skeletonTemplate", "description"]) {
504
+ if (revision.driftThreshold !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
505
+ context.addIssue({
506
+ code: z.ZodIssueCode.custom,
507
+ message: "driftThreshold applies to animate/animate-pixminimax revisions only",
508
+ path: ["driftThreshold"]
509
+ });
510
+ }
511
+ for (const field of ["subjectDescription", "initialPose"]) {
512
+ if (revision[field] !== void 0 && revision.mode !== "animate-pixminimax") {
513
+ context.addIssue({
514
+ code: z.ZodIssueCode.custom,
515
+ message: `${field} applies to animate-pixminimax revisions only`,
516
+ path: [field]
517
+ });
518
+ }
519
+ }
520
+ if (revision.removalTask !== void 0 && revision.mode !== "remove-background") {
521
+ context.addIssue({
522
+ code: z.ZodIssueCode.custom,
523
+ message: "removalTask applies to remove-background revisions only",
524
+ path: ["removalTask"]
525
+ });
526
+ }
527
+ if (revision.strength !== void 0 && revision.mode === "remove-background") {
528
+ context.addIssue({
529
+ code: z.ZodIssueCode.custom,
530
+ message: "remove-background revisions do not take a strength",
531
+ path: ["strength"]
532
+ });
533
+ }
534
+ if (revision.description !== void 0 && revision.mode !== "animate-skeleton" && revision.mode !== "remove-background") {
535
+ context.addIssue({
536
+ code: z.ZodIssueCode.custom,
537
+ message: "description applies to animate-skeleton/remove-background revisions only",
538
+ path: ["description"]
539
+ });
540
+ }
541
+ for (const field of ["keypointsFile", "skeletonTemplate"]) {
486
542
  if (revision[field] !== void 0 && revision.mode !== "animate-skeleton") {
487
543
  context.addIssue({
488
544
  code: z.ZodIssueCode.custom,
@@ -1136,6 +1192,10 @@ var LockEntrySchema = z.object({
1136
1192
  keypointsSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1137
1193
  skeletonTemplate: z.string().min(1).optional(),
1138
1194
  description: z.string().min(1).optional(),
1195
+ removalTask: z.enum(["simple", "complex"]).optional(),
1196
+ driftThreshold: z.number().min(0).optional(),
1197
+ subjectDescription: z.string().min(1).max(300).optional(),
1198
+ initialPose: z.string().min(1).max(300).optional(),
1139
1199
  engine: z.enum(["pro-flash"]).optional()
1140
1200
  }).strict().nullable().default(null),
1141
1201
  /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
@@ -1564,6 +1624,7 @@ var CorrectPixelartResponseSchema = z3.object({
1564
1624
  images: z3.array(z3.object({ base64: z3.string().min(1) }).passthrough()).min(1),
1565
1625
  usage: z3.unknown().optional()
1566
1626
  }).passthrough();
1627
+ var RemoveBackgroundResponseSchema = z3.object({ image: z3.object({ base64: z3.string().min(1) }).passthrough(), usage: z3.unknown().optional() }).passthrough();
1567
1628
  var ProFlashImageSubmitSchema = z3.object({
1568
1629
  background_job_id: z3.string().min(1),
1569
1630
  image_id: z3.string().min(1),
@@ -1755,9 +1816,9 @@ var PixelLabClient = class {
1755
1816
  apiKey;
1756
1817
  http;
1757
1818
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
1758
- async request(path36, init) {
1819
+ async request(path37, init) {
1759
1820
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
1760
- const res = await this.http(`${BASE}${path36}`, {
1821
+ const res = await this.http(`${BASE}${path37}`, {
1761
1822
  ...init,
1762
1823
  headers: {
1763
1824
  Authorization: auth,
@@ -1767,13 +1828,13 @@ var PixelLabClient = class {
1767
1828
  });
1768
1829
  const text = await res.text();
1769
1830
  if (!res.ok) {
1770
- throw new PixelLabError(`${init?.method ?? "GET"} ${path36} \u2192 ${res.status}`, res.status, text);
1831
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path37} \u2192 ${res.status}`, res.status, text);
1771
1832
  }
1772
1833
  if (!text) return {};
1773
1834
  try {
1774
1835
  return JSON.parse(text);
1775
1836
  } catch {
1776
- throw new Error(`${init?.method ?? "GET"} ${path36} returned invalid JSON`);
1837
+ throw new Error(`${init?.method ?? "GET"} ${path37} returned invalid JSON`);
1777
1838
  }
1778
1839
  }
1779
1840
  async balance() {
@@ -2178,10 +2239,10 @@ var PixelLabClient = class {
2178
2239
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2179
2240
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2180
2241
  const south = args.reference?.south;
2181
- let path36;
2242
+ let path37;
2182
2243
  let body;
2183
2244
  if (args.mode === "v3") {
2184
- path36 = "/create-character-v3";
2245
+ path37 = "/create-character-v3";
2185
2246
  body = {
2186
2247
  description: args.description,
2187
2248
  image_size: imageSize,
@@ -2195,7 +2256,7 @@ var PixelLabClient = class {
2195
2256
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2196
2257
  };
2197
2258
  } else if (args.mode === "pro-flash") {
2198
- path36 = "/create-character-pro-flash";
2259
+ path37 = "/create-character-pro-flash";
2199
2260
  const traits = args.styleTraits;
2200
2261
  body = {
2201
2262
  description: args.description,
@@ -2219,7 +2280,7 @@ var PixelLabClient = class {
2219
2280
  } : {}
2220
2281
  };
2221
2282
  } else if (args.mode === "pro") {
2222
- path36 = "/create-character-pro";
2283
+ path37 = "/create-character-pro";
2223
2284
  body = {
2224
2285
  description: args.description,
2225
2286
  image_size: imageSize,
@@ -2233,7 +2294,7 @@ var PixelLabClient = class {
2233
2294
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2234
2295
  };
2235
2296
  } else {
2236
- path36 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2297
+ path37 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2237
2298
  body = {
2238
2299
  description: args.description,
2239
2300
  image_size: imageSize,
@@ -2250,8 +2311,8 @@ var PixelLabClient = class {
2250
2311
  ...palette
2251
2312
  };
2252
2313
  }
2253
- const raw = await this.request(path36, { method: "POST", body: JSON.stringify(body) });
2254
- return validateResponse(CharacterSubmitSchema, raw, path36.slice(1));
2314
+ const raw = await this.request(path37, { method: "POST", body: JSON.stringify(body) });
2315
+ return validateResponse(CharacterSubmitSchema, raw, path37.slice(1));
2255
2316
  }
2256
2317
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2257
2318
  async createCharacterState(args) {
@@ -2628,6 +2689,27 @@ var PixelLabClient = class {
2628
2689
  const pngs = cleanupResults(res.images, images.length, "correct-pixelart");
2629
2690
  return { png: pngs[0], pngs, usage: res.usage };
2630
2691
  }
2692
+ /**
2693
+ * `/remove-background`: a transparent PNG from one pixel-art image,
2694
+ * synchronously, like the rest of the Cleanup tier. The API takes at most
2695
+ * 400x400 worth of area, one image per call. `simple` is faster and suits
2696
+ * flat backgrounds; `complex` is slower and handles detailed edges. The
2697
+ * schema's response example is dollar-denominated (`usd: 0.01`); the
2698
+ * subscription cost measured 1 generation (docs/ENDPOINTS.md), and the
2699
+ * default `simple` task is the one measured.
2700
+ */
2701
+ async removeBackground(args) {
2702
+ const body = { image: args.image, image_size: { width: args.width, height: args.height } };
2703
+ if (args.task) body.background_removal_task = args.task === "complex" ? "remove_complex_background" : "remove_simple_background";
2704
+ if (args.text) body.text = args.text;
2705
+ if (args.seed != null) body.seed = args.seed;
2706
+ const res = validateResponse(
2707
+ RemoveBackgroundResponseSchema,
2708
+ await this.request("/remove-background", { method: "POST", body: JSON.stringify(body) }),
2709
+ "remove-background"
2710
+ );
2711
+ return { png: Buffer.from(res.image.base64, "base64"), usage: res.usage };
2712
+ }
2631
2713
  /**
2632
2714
  * `/unzoom`: recover native-resolution pixel art from an upscaled image (a
2633
2715
  * 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
@@ -2736,6 +2818,7 @@ var PixelLabClient = class {
2736
2818
  */
2737
2819
  async animateWithTextV3(args) {
2738
2820
  const body = { first_frame: args.firstFrame, action: args.action };
2821
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
2739
2822
  if (args.lastFrame) body.last_frame = args.lastFrame;
2740
2823
  if (args.frameCount != null) body.frame_count = args.frameCount;
2741
2824
  if (args.seed != null) body.seed = args.seed;
@@ -2755,6 +2838,10 @@ var PixelLabClient = class {
2755
2838
  */
2756
2839
  async animatePixminimax(args) {
2757
2840
  const body = { first_frame: args.firstFrame, description: args.description };
2841
+ if (args.view) body.view = args.view;
2842
+ if (args.subjectDescription) body.subject_description = args.subjectDescription;
2843
+ if (args.initialPose) body.initial_pose = args.initialPose;
2844
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
2758
2845
  if (args.lastFrame) body.last_frame = args.lastFrame;
2759
2846
  if (args.frameCount != null) body.frame_count = args.frameCount;
2760
2847
  if (args.seed != null) body.seed = args.seed;
@@ -2790,6 +2877,7 @@ var PixelLabClient = class {
2790
2877
  keypoints: args.keypoints,
2791
2878
  direction: args.direction
2792
2879
  };
2880
+ if (args.view) body.view = args.view;
2793
2881
  if (args.templateId) body.template_id = args.templateId;
2794
2882
  if (args.action) body.action = args.action;
2795
2883
  if (args.description) body.description = args.description;
@@ -3437,10 +3525,10 @@ import { createReadStream } from "fs";
3437
3525
  function sha256(data) {
3438
3526
  return createHash("sha256").update(data).digest("hex");
3439
3527
  }
3440
- async function sha256File(path36) {
3528
+ async function sha256File(path37) {
3441
3529
  return new Promise((resolve, reject) => {
3442
3530
  const hash = createHash("sha256");
3443
- const stream = createReadStream(path36);
3531
+ const stream = createReadStream(path37);
3444
3532
  stream.on("data", (chunk2) => hash.update(chunk2));
3445
3533
  stream.on("error", reject);
3446
3534
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -3564,6 +3652,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3564
3652
  keypointsSha256: spec.revision.keypointsSha256,
3565
3653
  skeletonTemplate: spec.revision.skeletonTemplate,
3566
3654
  description: spec.revision.description,
3655
+ removalTask: spec.revision.removalTask,
3656
+ driftThreshold: spec.revision.driftThreshold,
3657
+ subjectDescription: spec.revision.subjectDescription,
3658
+ initialPose: spec.revision.initialPose,
3567
3659
  engine: spec.revision.engine
3568
3660
  } : void 0
3569
3661
  })
@@ -3739,7 +3831,7 @@ var PixelLabProvider = class _PixelLabProvider {
3739
3831
  * outpaint path yet.
3740
3832
  */
3741
3833
  supportsRevision(mode2) {
3742
- 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";
3834
+ 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";
3743
3835
  }
3744
3836
  /** PixelLab's own constraints: submissions must be >2s apart, and
3745
3837
  * background jobs in flight are capped by subscription tier (Tier 1=8,
@@ -3767,6 +3859,9 @@ var PixelLabProvider = class _PixelLabProvider {
3767
3859
  if (spec.batch?.role === "member") {
3768
3860
  return { unit: "generations", amount: 0, candidates: 1 };
3769
3861
  }
3862
+ if (spec.revision?.mode === "remove-background") {
3863
+ return { unit: "generations", amount: 1, candidates: 1 };
3864
+ }
3770
3865
  if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
3771
3866
  return { unit: "generations", amount: 0.1, candidates: 1 };
3772
3867
  }
@@ -3855,6 +3950,12 @@ var PixelLabProvider = class _PixelLabProvider {
3855
3950
  throw new Error(`PixelLab correct-pixelart source is ${width}x${height}; the API takes at most 1024 pixels per side`);
3856
3951
  }
3857
3952
  }
3953
+ if (spec.revision?.mode === "remove-background") {
3954
+ const { sourceWidth: width, sourceHeight: height } = spec.revision;
3955
+ if (width != null && height != null && (width > 400 || height > 400)) {
3956
+ throw new Error(`PixelLab remove-background source is ${width}x${height}; the API takes at most 400x400`);
3957
+ }
3958
+ }
3858
3959
  if (spec.revision?.mode === "animate-skeleton") {
3859
3960
  const { sourceWidth: width, sourceHeight: height, frames, skeletonTemplate } = spec.revision;
3860
3961
  if (width != null && height != null && (width > 256 || height > 256)) {
@@ -3875,6 +3976,9 @@ var PixelLabProvider = class _PixelLabProvider {
3875
3976
  if (mode2 === "animate" && frames != null && frames > 16) {
3876
3977
  throw new Error(`PixelLab animate takes 4 to 16 frames; ${frames} is too many (use animate-pixminimax for up to 40)`);
3877
3978
  }
3979
+ if (mode2 === "animate-pixminimax" && frames != null && frames % 4 !== 0) {
3980
+ throw new Error(`PixelLab animate-pixminimax takes a multiple of 4 frames (4 to 40); ${frames} is not one`);
3981
+ }
3878
3982
  if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
3879
3983
  throw new Error(
3880
3984
  `PixelLab ${mode2} lastFrame is ${lastFrameWidth}x${lastFrameHeight}; source is ${width}x${height} \u2014 they must match`
@@ -4966,6 +5070,18 @@ var PixelLabProvider = class _PixelLabProvider {
4966
5070
  const res2 = await this.client.correctPixelart({ images, strength: revision.strength });
4967
5071
  return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
4968
5072
  }
5073
+ if (revision.mode === "remove-background") {
5074
+ if (width == null || height == null) throw new Error(`${spec.styleId}/${spec.assetId}: remove-background needs the source's size`);
5075
+ const res2 = await this.client.removeBackground({
5076
+ image,
5077
+ width,
5078
+ height,
5079
+ task: revision.removalTask,
5080
+ text: revision.description,
5081
+ seed: spec.seed
5082
+ });
5083
+ return this.cacheCleanupResult([res2.png], res2.usage, roles, revision.sourceFps);
5084
+ }
4969
5085
  if (revision.mode === "interpolate") {
4970
5086
  const lastFrame = this.readLastFrame(spec);
4971
5087
  if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
@@ -5001,7 +5117,8 @@ var PixelLabProvider = class _PixelLabProvider {
5001
5117
  frameCount: revision.frames,
5002
5118
  seed: spec.seed,
5003
5119
  noBackground: spec.noBackground,
5004
- enhancePrompt: revision.enhancePrompt
5120
+ enhancePrompt: revision.enhancePrompt,
5121
+ driftThreshold: revision.driftThreshold
5005
5122
  });
5006
5123
  return { jobId: res3.background_job_id };
5007
5124
  }
@@ -5013,7 +5130,11 @@ var PixelLabProvider = class _PixelLabProvider {
5013
5130
  seed: spec.seed,
5014
5131
  noBackground: spec.noBackground,
5015
5132
  enhancePrompt: revision.enhancePrompt,
5016
- direction: revision.direction
5133
+ direction: revision.direction,
5134
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
5135
+ subjectDescription: revision.subjectDescription,
5136
+ initialPose: revision.initialPose,
5137
+ driftThreshold: revision.driftThreshold
5017
5138
  });
5018
5139
  return { jobId: res2.background_job_id };
5019
5140
  }
@@ -5030,6 +5151,9 @@ var PixelLabProvider = class _PixelLabProvider {
5030
5151
  firstFrameKeypoints: revision.skeleton.firstFrameKeypoints,
5031
5152
  keypoints: revision.skeleton.frames,
5032
5153
  direction: revision.direction,
5154
+ // The style's camera, when it is one this endpoint knows; its enum is
5155
+ // narrower than a style's free-string `view`.
5156
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
5033
5157
  templateId: revision.skeletonTemplate,
5034
5158
  action: spec.prompt,
5035
5159
  description: revision.description,
@@ -5100,7 +5224,7 @@ var PixelLabProvider = class _PixelLabProvider {
5100
5224
  }
5101
5225
  async poll(jobId, generator, context) {
5102
5226
  const revisionMode = context?.spec?.revision?.mode;
5103
- if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
5227
+ if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart" || revisionMode === "remove-background") {
5104
5228
  return this.pollCachedRevision(jobId, context);
5105
5229
  }
5106
5230
  if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "animate-skeleton" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
@@ -5808,9 +5932,9 @@ var RetroDiffusionClient = class {
5808
5932
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
5809
5933
  return Buffer.from(await response.arrayBuffer());
5810
5934
  }
5811
- async call(path36, init = {}) {
5935
+ async call(path37, init = {}) {
5812
5936
  if (!this.token) throw new Error("RD_API_KEY is not set");
5813
- const response = await this.request(`${this.baseUrl}${path36}`, {
5937
+ const response = await this.request(`${this.baseUrl}${path37}`, {
5814
5938
  ...init,
5815
5939
  headers: {
5816
5940
  "Content-Type": "application/json",
@@ -8816,6 +8940,10 @@ async function resolveSpecs(loaded, filter) {
8816
8940
  } : {},
8817
8941
  ...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
8818
8942
  ...asset.revision.description ? { description: asset.revision.description } : {},
8943
+ ...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
8944
+ ...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
8945
+ ...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
8946
+ ...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
8819
8947
  ...asset.revision.engine ? { engine: asset.revision.engine } : {}
8820
8948
  };
8821
8949
  }
@@ -12099,10 +12227,10 @@ function isSha256Hash(value) {
12099
12227
  function parseCache(value) {
12100
12228
  return HashCacheSchema.parse(value);
12101
12229
  }
12102
- async function loadCache(path36) {
12103
- if (!existsSync13(path36)) return { version: 1, hashes: {} };
12230
+ async function loadCache(path37) {
12231
+ if (!existsSync13(path37)) return { version: 1, hashes: {} };
12104
12232
  try {
12105
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(path36, "utf8")));
12233
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(path37, "utf8")));
12106
12234
  if (!parsed.success) return { version: 1, hashes: {} };
12107
12235
  return {
12108
12236
  version: 1,
@@ -12114,18 +12242,18 @@ async function loadCache(path36) {
12114
12242
  return { version: 1, hashes: {} };
12115
12243
  }
12116
12244
  }
12117
- async function saveCache(path36, cache) {
12245
+ async function saveCache(path37, cache) {
12118
12246
  const sorted = {};
12119
12247
  for (const key of Object.keys(cache.hashes).sort()) {
12120
12248
  const hash = cache.hashes[key];
12121
12249
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12122
12250
  sorted[key] = hash;
12123
12251
  }
12124
- await mkdir5(pathModule.dirname(pathModule.resolve(path36)), { recursive: true });
12125
- const tmp = `${path36}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12252
+ await mkdir5(pathModule.dirname(pathModule.resolve(path37)), { recursive: true });
12253
+ const tmp = `${path37}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12126
12254
  try {
12127
12255
  await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12128
- await rename4(tmp, path36);
12256
+ await rename4(tmp, path37);
12129
12257
  } finally {
12130
12258
  await rm4(tmp, { force: true });
12131
12259
  }
@@ -13641,6 +13769,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13641
13769
  ...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
13642
13770
  ...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
13643
13771
  ...spec.revision.description ? { description: spec.revision.description } : {},
13772
+ ...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
13773
+ ...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
13774
+ ...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
13775
+ ...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
13644
13776
  ...spec.revision.engine ? { engine: spec.revision.engine } : {}
13645
13777
  } : null,
13646
13778
  outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
@@ -14930,9 +15062,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
14930
15062
 
14931
15063
  // src/gallery/snapshot.ts
14932
15064
  import { createHash as createHash4 } from "crypto";
14933
- import { existsSync as existsSync25 } from "fs";
14934
- import { readFile as readFile23, stat as stat3 } from "fs/promises";
14935
- import path31 from "path";
15065
+ import { existsSync as existsSync26 } from "fs";
15066
+ import { readFile as readFile24, stat as stat3 } from "fs/promises";
15067
+ import path32 from "path";
14936
15068
 
14937
15069
  // src/workspace.ts
14938
15070
  import { mkdir as mkdir7, readFile as readFile18, rename as rename6, rm as rm7, writeFile as writeFile9 } from "fs/promises";
@@ -15068,15 +15200,16 @@ function validateWorkspace(ws, dir) {
15068
15200
  }
15069
15201
 
15070
15202
  // src/gallery/edit.ts
15071
- import { readFile as readFile22, rename as rename9, writeFile as writeFile14 } from "fs/promises";
15072
- import path30 from "path";
15073
- import { z as z12 } from "zod";
15203
+ import { readFile as readFile23, rename as rename9, writeFile as writeFile15 } from "fs/promises";
15204
+ import path31 from "path";
15205
+ import { z as z13 } from "zod";
15074
15206
 
15075
15207
  // src/manifest-edit.ts
15076
15208
  import { readFile as readFile19, rename as rename7, rm as rm8, writeFile as writeFile10 } from "fs/promises";
15077
15209
  import path26 from "path";
15078
15210
  import { z as z8 } from "zod";
15079
15211
  var HexSha = z8.string().regex(/^[0-9a-f]{64}$/);
15212
+ var ProjectFileSchema = z8.string().min(1).refine((file) => !path26.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
15080
15213
  var AssetPatchSchema = z8.object({
15081
15214
  prompt: z8.string().optional(),
15082
15215
  /** Per-style override for the style being viewed; null removes it. */
@@ -15125,10 +15258,11 @@ var NewAssetSchema = z8.object({
15125
15258
  styles: z8.array(z8.string().min(1)).optional(),
15126
15259
  /**
15127
15260
  * A controlled regeneration of another asset in the same manifest,
15128
- * instead of a fresh generation. `inpaint` needs a mask upload and
15129
- * `interpolate` an ending-keyframe image the gallery does not offer, so
15130
- * those two are hand-edited; every other mode can be created here
15131
- * (`animate-skeleton` after the gallery writes its keypoints file).
15261
+ * instead of a fresh generation. Every mode but `outpaint` can be
15262
+ * created here: `inpaint` names a mask file the gallery's mask editor
15263
+ * wrote first, `interpolate` names an existing image as its ending
15264
+ * keyframe, and `animate-skeleton` names the keypoints file the gallery
15265
+ * wrote first.
15132
15266
  */
15133
15267
  revision: z8.discriminatedUnion("mode", [
15134
15268
  z8.object({
@@ -15145,15 +15279,38 @@ var NewAssetSchema = z8.object({
15145
15279
  dithering: RevisionDitheringSchema.optional(),
15146
15280
  ditheringStrength: z8.number().min(0).max(10).optional()
15147
15281
  }).strict(),
15282
+ z8.object({
15283
+ mode: z8.literal("inpaint"),
15284
+ from: z8.string().min(1),
15285
+ /** Black-and-white PNG the same size as the parent; white is repainted. */
15286
+ mask: ProjectFileSchema
15287
+ }).strict(),
15288
+ z8.object({
15289
+ mode: z8.literal("interpolate"),
15290
+ from: z8.string().min(1),
15291
+ /** The ending keyframe: an image already in the project, the parent's size. */
15292
+ lastFrame: ProjectFileSchema,
15293
+ fps: z8.number().int().min(1).max(60).optional()
15294
+ }).strict(),
15148
15295
  z8.object({ mode: z8.literal("correct-pixelart"), from: z8.string().min(1), strength: z8.number().min(0).max(1).optional() }).strict(),
15296
+ z8.object({
15297
+ mode: z8.literal("remove-background"),
15298
+ from: z8.string().min(1),
15299
+ removalTask: z8.enum(["simple", "complex"]).optional(),
15300
+ /** A short description of the foreground, sent as PixelLab's text hint. */
15301
+ description: z8.string().min(1).optional()
15302
+ }).strict(),
15149
15303
  z8.object({
15150
15304
  mode: z8.enum(["animate", "animate-pixminimax"]),
15151
15305
  from: z8.string().min(1),
15152
15306
  frames: z8.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
15153
15307
  fps: z8.number().int().min(1).max(60).optional(),
15154
15308
  enhancePrompt: z8.boolean().optional(),
15155
- /** `animate-pixminimax` only; the loader refuses it on `animate`. */
15156
- direction: CharacterDirectionSchema.optional()
15309
+ driftThreshold: z8.number().min(0).optional(),
15310
+ /** `animate-pixminimax` only; the loader refuses these on `animate`. */
15311
+ direction: CharacterDirectionSchema.optional(),
15312
+ subjectDescription: z8.string().min(1).max(300).optional(),
15313
+ initialPose: z8.string().min(1).max(300).optional()
15157
15314
  }).strict(),
15158
15315
  z8.object({
15159
15316
  mode: z8.literal("edit-animation"),
@@ -15804,62 +15961,118 @@ async function saveProjectImage(root, request) {
15804
15961
  return relative.split(path28.sep).join("/");
15805
15962
  }
15806
15963
 
15807
- // src/gallery/skeleton.ts
15964
+ // src/gallery/image-revision.ts
15808
15965
  import { existsSync as existsSync24 } from "fs";
15809
- import { mkdir as mkdir10, readFile as readFile21, rm as rm10, writeFile as writeFile13 } from "fs/promises";
15966
+ import { readFile as readFile21, rm as rm10, writeFile as writeFile13 } from "fs/promises";
15810
15967
  import path29 from "path";
15811
15968
  import { z as z11 } from "zod";
15812
- var SkeletonEstimateRequestSchema = z11.object({
15813
- project: z11.string().min(1).optional(),
15814
- styleId: z11.string().min(1),
15815
- assetId: z11.string().min(1),
15816
- frames: z11.number().int().min(3).max(15).optional()
15817
- }).strict();
15818
- var SkeletonAnimationRequestSchema = z11.object({
15819
- action: z11.literal("create-skeleton-animation"),
15969
+ var InpaintRevisionRequestSchema = z11.object({
15970
+ action: z11.literal("create-inpaint-revision"),
15820
15971
  project: z11.string().min(1).optional(),
15821
15972
  expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
15822
15973
  styleId: z11.string().min(1),
15823
- /** The sprite the animation poses. */
15974
+ /** The sprite being repainted. */
15824
15975
  from: z11.string().min(1),
15825
15976
  assetId: z11.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
15826
- /** The motion, sent as PixelLab's `action`. */
15977
+ /** What to paint into the masked area. */
15827
15978
  prompt: z11.string().min(1),
15979
+ /** Manifest-relative `.png` destination for the mask. */
15980
+ maskPath: z11.string().min(1),
15981
+ maskBase64: z11.string().regex(/^[A-Za-z0-9+/]*={0,2}$/),
15982
+ /** Replace a file already at `maskPath`. */
15983
+ overwrite: z11.boolean().optional()
15984
+ }).strict();
15985
+ async function createInpaintRevision(manifestPath, request) {
15986
+ const root = path29.dirname(path29.resolve(manifestPath));
15987
+ if (path29.extname(request.maskPath).toLowerCase() !== ".png") {
15988
+ throw new ManifestEditError("the mask must be a .png path inside the project");
15989
+ }
15990
+ const bytes = Buffer.from(request.maskBase64, "base64");
15991
+ const metadata = imageMetadata(bytes);
15992
+ if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
15993
+ const file = path29.resolve(root, request.maskPath);
15994
+ const previous = existsSync24(file) ? await readFile21(file) : null;
15995
+ const mask = await saveProjectImage(root, {
15996
+ action: "upload-image",
15997
+ path: request.maskPath,
15998
+ base64: request.maskBase64,
15999
+ ...request.overwrite ? { overwrite: true } : {}
16000
+ });
16001
+ try {
16002
+ await applyManifestEdit(manifestPath, {
16003
+ action: "add-asset",
16004
+ assetId: request.assetId,
16005
+ expectedSha256: request.expectedSha256,
16006
+ asset: {
16007
+ prompt: request.prompt,
16008
+ styles: [request.styleId],
16009
+ revision: { mode: "inpaint", from: request.from, mask }
16010
+ }
16011
+ });
16012
+ } catch (error) {
16013
+ if (previous) await writeFile13(file, previous);
16014
+ else await rm10(file, { force: true });
16015
+ throw error;
16016
+ }
16017
+ return mask;
16018
+ }
16019
+
16020
+ // src/gallery/skeleton.ts
16021
+ import { existsSync as existsSync25 } from "fs";
16022
+ import { mkdir as mkdir10, readFile as readFile22, rm as rm11, writeFile as writeFile14 } from "fs/promises";
16023
+ import path30 from "path";
16024
+ import { z as z12 } from "zod";
16025
+ var SkeletonEstimateRequestSchema = z12.object({
16026
+ project: z12.string().min(1).optional(),
16027
+ styleId: z12.string().min(1),
16028
+ assetId: z12.string().min(1),
16029
+ frames: z12.number().int().min(3).max(15).optional()
16030
+ }).strict();
16031
+ var SkeletonAnimationRequestSchema = z12.object({
16032
+ action: z12.literal("create-skeleton-animation"),
16033
+ project: z12.string().min(1).optional(),
16034
+ expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
16035
+ styleId: z12.string().min(1),
16036
+ /** The sprite the animation poses. */
16037
+ from: z12.string().min(1),
16038
+ assetId: z12.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
16039
+ /** The motion, sent as PixelLab's `action`. */
16040
+ prompt: z12.string().min(1),
15828
16041
  direction: CharacterDirectionSchema,
15829
- description: z11.string().min(1).optional(),
15830
- skeletonTemplate: z11.string().min(1).optional(),
16042
+ description: z12.string().min(1).optional(),
16043
+ skeletonTemplate: z12.string().min(1).optional(),
15831
16044
  /** Manifest-relative JSON path inside the project. */
15832
- keypointsFile: z11.string().min(1),
16045
+ keypointsFile: z12.string().min(1),
15833
16046
  /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
15834
16047
  set: SkeletonSetSchema.optional(),
15835
16048
  /** Replace an existing `keypointsFile` with `set`. */
15836
- overwrite: z11.boolean().optional()
16049
+ overwrite: z12.boolean().optional()
15837
16050
  }).strict();
15838
- var SkeletonKeypointsUpdateSchema = z11.object({
15839
- action: z11.literal("update-skeleton-keypoints"),
15840
- project: z11.string().min(1).optional(),
15841
- styleId: z11.string().min(1),
15842
- assetId: z11.string().min(1),
16051
+ var SkeletonKeypointsUpdateSchema = z12.object({
16052
+ action: z12.literal("update-skeleton-keypoints"),
16053
+ project: z12.string().min(1).optional(),
16054
+ styleId: z12.string().min(1),
16055
+ assetId: z12.string().min(1),
15843
16056
  /** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
15844
- expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
16057
+ expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
15845
16058
  set: SkeletonSetSchema
15846
16059
  }).strict();
15847
16060
  async function createSkeletonAnimation(manifestPath, request) {
15848
- const root = path29.dirname(path29.resolve(manifestPath));
15849
- const file = path29.resolve(root, request.keypointsFile);
15850
- const relative = path29.relative(root, file);
15851
- if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path29.isAbsolute(relative)) {
16061
+ const root = path30.dirname(path30.resolve(manifestPath));
16062
+ const file = path30.resolve(root, request.keypointsFile);
16063
+ const relative = path30.relative(root, file);
16064
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path30.isAbsolute(relative)) {
15852
16065
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
15853
16066
  }
15854
- const existed = existsSync24(file);
16067
+ const existed = existsSync25(file);
15855
16068
  let previous = null;
15856
16069
  if (request.set) {
15857
16070
  if (existed && !request.overwrite) {
15858
16071
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
15859
16072
  }
15860
- if (existed) previous = await readFile21(file);
15861
- await mkdir10(path29.dirname(file), { recursive: true });
15862
- await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
16073
+ if (existed) previous = await readFile22(file);
16074
+ await mkdir10(path30.dirname(file), { recursive: true });
16075
+ await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
15863
16076
  } else if (!existed) {
15864
16077
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
15865
16078
  }
@@ -15874,7 +16087,7 @@ async function createSkeletonAnimation(manifestPath, request) {
15874
16087
  revision: {
15875
16088
  mode: "animate-skeleton",
15876
16089
  from: request.from,
15877
- keypointsFile: relative.split(path29.sep).join("/"),
16090
+ keypointsFile: relative.split(path30.sep).join("/"),
15878
16091
  direction: request.direction,
15879
16092
  ...request.description ? { description: request.description } : {},
15880
16093
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -15883,8 +16096,8 @@ async function createSkeletonAnimation(manifestPath, request) {
15883
16096
  });
15884
16097
  } catch (error) {
15885
16098
  if (request.set) {
15886
- if (previous) await writeFile13(file, previous);
15887
- else await rm10(file, { force: true });
16099
+ if (previous) await writeFile14(file, previous);
16100
+ else await rm11(file, { force: true });
15888
16101
  }
15889
16102
  throw error;
15890
16103
  }
@@ -15895,64 +16108,64 @@ async function updateSkeletonKeypoints(ctx, request) {
15895
16108
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
15896
16109
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
15897
16110
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
15898
- const relative = path29.relative(ctx.loaded.root, file);
15899
- if (relative.startsWith("..") || path29.isAbsolute(relative)) {
16111
+ const relative = path30.relative(ctx.loaded.root, file);
16112
+ if (relative.startsWith("..") || path30.isAbsolute(relative)) {
15900
16113
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
15901
16114
  }
15902
- const current = existsSync24(file) ? sha256(await readFile21(file)) : null;
16115
+ const current = existsSync25(file) ? sha256(await readFile22(file)) : null;
15903
16116
  if (current !== request.expectedSha256) {
15904
16117
  throw Object.assign(
15905
- new Error(`${relative.split(path29.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16118
+ new Error(`${relative.split(path30.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
15906
16119
  { status: 409 }
15907
16120
  );
15908
16121
  }
15909
- await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
15910
- return relative.split(path29.sep).join("/");
16122
+ await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
16123
+ return relative.split(path30.sep).join("/");
15911
16124
  }
15912
16125
 
15913
16126
  // src/gallery/edit.ts
15914
- var Base64 = z12.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
15915
- var HandEditRequestSchema = z12.discriminatedUnion("action", [
15916
- z12.object({
15917
- action: z12.literal("start-edit"),
15918
- project: z12.string().min(1).optional(),
15919
- assetId: z12.string().min(1),
15920
- styleId: z12.string().min(1),
15921
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
16127
+ var Base64 = z13.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
16128
+ var HandEditRequestSchema = z13.discriminatedUnion("action", [
16129
+ z13.object({
16130
+ action: z13.literal("start-edit"),
16131
+ project: z13.string().min(1).optional(),
16132
+ assetId: z13.string().min(1),
16133
+ styleId: z13.string().min(1),
16134
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/),
15922
16135
  /** Also launch the author's editor on the file. */
15923
- open: z12.boolean().optional()
16136
+ open: z13.boolean().optional()
15924
16137
  }).strict(),
15925
- z12.object({
15926
- action: z12.literal("detach-edit"),
15927
- project: z12.string().min(1).optional(),
15928
- assetId: z12.string().min(1),
15929
- styleId: z12.string().min(1),
15930
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/)
16138
+ z13.object({
16139
+ action: z13.literal("detach-edit"),
16140
+ project: z13.string().min(1).optional(),
16141
+ assetId: z13.string().min(1),
16142
+ styleId: z13.string().min(1),
16143
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/)
15931
16144
  }).strict(),
15932
16145
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
15933
- z12.object({
15934
- action: z12.literal("save-edit"),
15935
- project: z12.string().min(1).optional(),
15936
- assetId: z12.string().min(1),
15937
- styleId: z12.string().min(1),
15938
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
16146
+ z13.object({
16147
+ action: z13.literal("save-edit"),
16148
+ project: z13.string().min(1).optional(),
16149
+ assetId: z13.string().min(1),
16150
+ styleId: z13.string().min(1),
16151
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/),
15939
16152
  /** The flattened image; for a frame set, `frames` instead. */
15940
16153
  png: Base64.min(1).optional(),
15941
16154
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
15942
- frames: z12.array(z12.object({ role: z12.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
16155
+ frames: z13.array(z13.object({ role: z13.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
15943
16156
  /** The editor's layered project file, kept beside the edit. */
15944
16157
  pxo: Base64.optional(),
15945
16158
  /** Editor identity from the bridge's ready message. */
15946
- editor: z12.string().min(1).max(200),
15947
- protocol: z12.number().int().positive()
16159
+ editor: z13.string().min(1).max(200),
16160
+ protocol: z13.number().int().positive()
15948
16161
  }).strict()
15949
16162
  ]);
15950
16163
  var UNDO_DEPTH = 20;
15951
- var UndoRequestSchema = z12.object({
15952
- action: z12.literal("undo-edit"),
15953
- project: z12.string().min(1).optional(),
16164
+ var UndoRequestSchema = z13.object({
16165
+ action: z13.literal("undo-edit"),
16166
+ project: z13.string().min(1).optional(),
15954
16167
  /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
15955
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/)
16168
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/)
15956
16169
  }).strict();
15957
16170
  function createGalleryEditHandler(opts) {
15958
16171
  const log = opts.onProgress ?? (() => {
@@ -15961,7 +16174,7 @@ function createGalleryEditHandler(opts) {
15961
16174
  const history = /* @__PURE__ */ new Map();
15962
16175
  const manifestOf = async (project) => {
15963
16176
  try {
15964
- return path30.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16177
+ return path31.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
15965
16178
  } catch {
15966
16179
  return null;
15967
16180
  }
@@ -15971,7 +16184,7 @@ function createGalleryEditHandler(opts) {
15971
16184
  if (undo.success) {
15972
16185
  const manifestPath2 = await manifestOf(undo.data.project);
15973
16186
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
15974
- const current = await readFile22(manifestPath2, "utf8");
16187
+ const current = await readFile23(manifestPath2, "utf8");
15975
16188
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
15976
16189
  const stack = history.get(manifestPath2) ?? [];
15977
16190
  const last = stack[stack.length - 1];
@@ -15981,17 +16194,17 @@ function createGalleryEditHandler(opts) {
15981
16194
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
15982
16195
  }
15983
16196
  const tmp = manifestPath2 + ".pixelkiln-undo";
15984
- await writeFile14(tmp, last.before);
16197
+ await writeFile15(tmp, last.before);
15985
16198
  await rename9(tmp, manifestPath2);
15986
16199
  stack.pop();
15987
- log(` manifest edit undone in ${path30.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16200
+ log(` manifest edit undone in ${path31.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15988
16201
  return opts.reload();
15989
16202
  }
15990
16203
  const manifestPath = await manifestOf(body?.project);
15991
- const before = manifestPath ? await readFile22(manifestPath, "utf8").catch(() => null) : null;
16204
+ const before = manifestPath ? await readFile23(manifestPath, "utf8").catch(() => null) : null;
15992
16205
  const build = await apply(body);
15993
16206
  if (manifestPath && before !== null) {
15994
- const after = await readFile22(manifestPath, "utf8").catch(() => null);
16207
+ const after = await readFile23(manifestPath, "utf8").catch(() => null);
15995
16208
  if (after !== null && after !== before) {
15996
16209
  const stack = history.get(manifestPath) ?? [];
15997
16210
  stack.push({ before, afterSha: sha256(after) });
@@ -16046,7 +16259,7 @@ function createEditApplier(opts, log) {
16046
16259
  } catch (error) {
16047
16260
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16048
16261
  }
16049
- const written = await saveProjectImage(path30.dirname(path30.resolve(manifestPath2)), upload.data);
16262
+ const written = await saveProjectImage(path31.dirname(path31.resolve(manifestPath2)), upload.data);
16050
16263
  log(` image added: ${written}`);
16051
16264
  return opts.reload();
16052
16265
  }
@@ -16082,6 +16295,23 @@ function createEditApplier(opts, log) {
16082
16295
  "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16083
16296
  );
16084
16297
  }
16298
+ const inpaint = InpaintRevisionRequestSchema.safeParse(body);
16299
+ if (inpaint.success) {
16300
+ let manifestPath2;
16301
+ try {
16302
+ manifestPath2 = await opts.manifestFor(inpaint.data.project);
16303
+ } catch (error) {
16304
+ throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16305
+ }
16306
+ const mask = await createInpaintRevision(manifestPath2, inpaint.data);
16307
+ log(` manifest edited: added inpaint revision ${inpaint.data.assetId} (mask ${mask})`);
16308
+ return opts.reload();
16309
+ }
16310
+ if (body?.action === "create-inpaint-revision") {
16311
+ throw new ManifestEditError(
16312
+ "invalid inpaint revision: " + inpaint.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16313
+ );
16314
+ }
16085
16315
  const parsed = ManifestEditSchema.safeParse(body);
16086
16316
  if (!parsed.success) {
16087
16317
  throw new ManifestEditError(
@@ -16096,7 +16326,7 @@ function createEditApplier(opts, log) {
16096
16326
  }
16097
16327
  const result = await applyManifestEdit(manifestPath, parsed.data);
16098
16328
  if (result.changed) {
16099
- const relative = path30.relative(process.cwd(), result.manifestPath);
16329
+ const relative = path31.relative(process.cwd(), result.manifestPath);
16100
16330
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
16101
16331
  log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
16102
16332
  }
@@ -16124,7 +16354,7 @@ function describeEdit(edit) {
16124
16354
  // src/gallery/snapshot.ts
16125
16355
  var PROJECT_FILE_TYPE = "application/octet-stream";
16126
16356
  function galleryMediaId(absolutePath) {
16127
- return createHash4("sha256").update(path31.resolve(absolutePath)).digest("hex").slice(0, 24);
16357
+ return createHash4("sha256").update(path32.resolve(absolutePath)).digest("hex").slice(0, 24);
16128
16358
  }
16129
16359
  function galleryMediaRoute(id) {
16130
16360
  return `/media/${id}`;
@@ -16143,9 +16373,9 @@ function describeHistory(media, entry, cacheDir) {
16143
16373
  return (entry.history ?? []).map((generation, i) => {
16144
16374
  const outputs = generation.outputs.map((output) => {
16145
16375
  const mediaType = output.mediaType ?? MediaType.PNG;
16146
- const file = path31.join(cacheDir, cacheFileName(output.sha256, mediaType));
16376
+ const file = path32.join(cacheDir, cacheFileName(output.sha256, mediaType));
16147
16377
  let url = null;
16148
- if (existsSync25(file)) {
16378
+ if (existsSync26(file)) {
16149
16379
  const id = galleryMediaId(file);
16150
16380
  media.set(id, { path: file, contentType: mediaType });
16151
16381
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -16177,7 +16407,7 @@ function describeHistory(media, entry, cacheDir) {
16177
16407
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16178
16408
  async function samePixels(a, b) {
16179
16409
  try {
16180
- const [left, right] = await Promise.all([readFile23(a), readFile23(b)]);
16410
+ const [left, right] = await Promise.all([readFile24(a), readFile24(b)]);
16181
16411
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16182
16412
  const x = decodePng(left);
16183
16413
  const y = decodePng(right);
@@ -16275,10 +16505,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
16275
16505
  async function describeQuality(media, root, inspection) {
16276
16506
  const outputPaths = inspection.outputs ?? [inspection.output];
16277
16507
  const outputs = await Promise.all(
16278
- outputPaths.map((file) => describeOutput(media, root, path31.resolve(root, file)))
16508
+ outputPaths.map((file) => describeOutput(media, root, path32.resolve(root, file)))
16279
16509
  );
16280
- const recordPath = path31.resolve(root, inspection.record);
16281
- const recordExists = existsSync25(recordPath);
16510
+ const recordPath = path32.resolve(root, inspection.record);
16511
+ const recordExists = existsSync26(recordPath);
16282
16512
  let options = null;
16283
16513
  let check = null;
16284
16514
  if (recordExists) {
@@ -16297,7 +16527,7 @@ async function describeQuality(media, root, inspection) {
16297
16527
  return {
16298
16528
  state: inspection.state,
16299
16529
  reason: inspection.reason,
16300
- output: portableOutputPath(path31.resolve(root, inspection.output), root),
16530
+ output: portableOutputPath(path32.resolve(root, inspection.output), root),
16301
16531
  record: portableOutputPath(recordPath, root),
16302
16532
  recordExists,
16303
16533
  outputs,
@@ -16335,9 +16565,9 @@ async function buildGallerySnapshot(opts) {
16335
16565
  const media = /* @__PURE__ */ new Map();
16336
16566
  const plan = await buildPlan(specs, lock);
16337
16567
  const items = [];
16338
- const manifestText = await readFile23(loaded.path, "utf8");
16568
+ const manifestText = await readFile24(loaded.path, "utf8");
16339
16569
  const manifestSha256 = sha256(manifestText);
16340
- const cacheDir = path31.resolve(path31.dirname(opts.lockPath), ".pixelkiln", "cache");
16570
+ const cacheDir = path32.resolve(path32.dirname(opts.lockPath), ".pixelkiln", "cache");
16341
16571
  let rawStyles = {};
16342
16572
  try {
16343
16573
  const raw = JSON.parse(manifestText);
@@ -16361,12 +16591,12 @@ async function buildGallerySnapshot(opts) {
16361
16591
  )
16362
16592
  );
16363
16593
  if (spec.source) {
16364
- const editPath = path31.resolve(root, spec.source);
16594
+ const editPath = path32.resolve(root, spec.source);
16365
16595
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
16366
16596
  const editShas = await Promise.all(
16367
16597
  Array.from({ length: memberCount }, async (_, index) => {
16368
16598
  const file = sourceOutputPath(spec.source, entry, index, root);
16369
- return { file, sha256: existsSync25(file) ? await sha256File(file) : null };
16599
+ return { file, sha256: existsSync26(file) ? await sha256File(file) : null };
16370
16600
  })
16371
16601
  );
16372
16602
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -16374,7 +16604,7 @@ async function buildGallerySnapshot(opts) {
16374
16604
  const companion = await readHandEditCompanion(editPath);
16375
16605
  if (companion) {
16376
16606
  const projectPath = handEditProjectPath(editPath);
16377
- const project = companion.project && existsSync25(projectPath) ? await fileInfo(projectPath) : null;
16607
+ const project = companion.project && existsSync26(projectPath) ? await fileInfo(projectPath) : null;
16378
16608
  let projectUrl = null;
16379
16609
  if (project) {
16380
16610
  const id = galleryMediaId(projectPath);
@@ -16410,8 +16640,8 @@ async function buildGallerySnapshot(opts) {
16410
16640
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
16411
16641
  }
16412
16642
  } else if (spec.source) {
16413
- outputs = [await describeOutput(media, root, path31.resolve(root, spec.source))];
16414
- } else if (!entry && existsSync25(spec.outFile)) {
16643
+ outputs = [await describeOutput(media, root, path32.resolve(root, spec.source))];
16644
+ } else if (!entry && existsSync26(spec.outFile)) {
16415
16645
  outputs = [await describeOutput(media, root, spec.outFile)];
16416
16646
  } else {
16417
16647
  outputs = [];
@@ -16614,7 +16844,7 @@ async function buildGallerySnapshot(opts) {
16614
16844
  id: loaded.manifest.name,
16615
16845
  name: loaded.manifest.name,
16616
16846
  manifest: loaded.path,
16617
- lock: path31.resolve(opts.lockPath),
16847
+ lock: path32.resolve(opts.lockPath),
16618
16848
  root,
16619
16849
  provider: loaded.manifest.provider,
16620
16850
  account: null,
@@ -16640,7 +16870,7 @@ async function buildGallerySnapshot(opts) {
16640
16870
  return { snapshot, media };
16641
16871
  }
16642
16872
  async function buildWorkspaceGallerySnapshot(opts) {
16643
- const dir = path31.dirname(path31.resolve(opts.workspacePath));
16873
+ const dir = path32.dirname(path32.resolve(opts.workspacePath));
16644
16874
  const media = /* @__PURE__ */ new Map();
16645
16875
  const projects = [];
16646
16876
  const items = [];
@@ -16683,11 +16913,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
16683
16913
  id: project.id,
16684
16914
  name: loaded.manifest.name,
16685
16915
  manifest: manifestPath,
16686
- lock: path31.resolve(lockPath),
16916
+ lock: path32.resolve(lockPath),
16687
16917
  root: loaded.root,
16688
16918
  provider: loaded.manifest.provider,
16689
16919
  account: project.account ?? null,
16690
- manifestSha256: manifestSha256 ?? sha256(await readFile23(manifestPath, "utf8")),
16920
+ manifestSha256: manifestSha256 ?? sha256(await readFile24(manifestPath, "utf8")),
16691
16921
  historyLimit: historyLimit(loaded.manifest),
16692
16922
  entries: Object.keys(lock.entries).length,
16693
16923
  items: projectItems.length,
@@ -16699,8 +16929,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
16699
16929
  id: project.id,
16700
16930
  name: project.id,
16701
16931
  manifest: manifestPath,
16702
- lock: path31.resolve(lockPath),
16703
- root: path31.dirname(manifestPath),
16932
+ lock: path32.resolve(lockPath),
16933
+ root: path32.dirname(manifestPath),
16704
16934
  provider: project.provider,
16705
16935
  account: project.account ?? null,
16706
16936
  manifestSha256: null,
@@ -16727,7 +16957,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16727
16957
  version: 1,
16728
16958
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
16729
16959
  project: null,
16730
- workspace: { path: path31.resolve(opts.workspacePath), dir, projects },
16960
+ workspace: { path: path32.resolve(opts.workspacePath), dir, projects },
16731
16961
  filter,
16732
16962
  totals: {
16733
16963
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -16744,7 +16974,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16744
16974
  }
16745
16975
 
16746
16976
  // src/gallery/page.ts
16747
- import { existsSync as existsSync26, readFileSync as readFileSync5 } from "fs";
16977
+ import { existsSync as existsSync27, readFileSync as readFileSync5 } from "fs";
16748
16978
 
16749
16979
  // src/gallery/client-bundle.ts
16750
16980
  import { createRequire } from "module";
@@ -16785,7 +17015,7 @@ function clientAssets() {
16785
17015
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
16786
17016
  client ??= {
16787
17017
  css: readFileSync5(new URL("gallery.css", CLIENT_DIR), "utf8"),
16788
- js: existsSync26(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
17018
+ js: existsSync27(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
16789
17019
  };
16790
17020
  return client;
16791
17021
  }
@@ -16928,10 +17158,10 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
16928
17158
 
16929
17159
  // src/editor/install.ts
16930
17160
  import { createHash as createHash5 } from "crypto";
16931
- import { existsSync as existsSync27 } from "fs";
16932
- import { mkdir as mkdir11, readFile as readFile24, rename as rename10, rm as rm11, stat as stat4, writeFile as writeFile15 } from "fs/promises";
17161
+ import { existsSync as existsSync28 } from "fs";
17162
+ import { mkdir as mkdir11, readFile as readFile25, rename as rename10, rm as rm12, stat as stat4, writeFile as writeFile16 } from "fs/promises";
16933
17163
  import os2 from "os";
16934
- import path32 from "path";
17164
+ import path33 from "path";
16935
17165
  var EditorInstallError = class extends Error {
16936
17166
  constructor(message6, file) {
16937
17167
  super(message6);
@@ -16942,15 +17172,15 @@ var EditorInstallError = class extends Error {
16942
17172
  };
16943
17173
  function editorToolsRoot() {
16944
17174
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
16945
- if (override) return path32.resolve(override);
16946
- if (process.platform === "darwin") return path32.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
17175
+ if (override) return path33.resolve(override);
17176
+ if (process.platform === "darwin") return path33.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
16947
17177
  if (process.platform === "win32") {
16948
- return path32.join(process.env.LOCALAPPDATA ?? path32.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17178
+ return path33.join(process.env.LOCALAPPDATA ?? path33.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
16949
17179
  }
16950
- return path32.join(process.env.XDG_CACHE_HOME ?? path32.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
17180
+ return path33.join(process.env.XDG_CACHE_HOME ?? path33.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
16951
17181
  }
16952
17182
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
16953
- return path32.join(root, "pixelorama", pin.release ?? "unpublished");
17183
+ return path33.join(root, "pixelorama", pin.release ?? "unpublished");
16954
17184
  }
16955
17185
  function editorBaseUrl(pin = EDITOR_PIN) {
16956
17186
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -16960,7 +17190,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
16960
17190
  }
16961
17191
  async function sha256Of(file) {
16962
17192
  try {
16963
- return createHash5("sha256").update(await readFile24(file)).digest("hex");
17193
+ return createHash5("sha256").update(await readFile25(file)).digest("hex");
16964
17194
  } catch {
16965
17195
  return null;
16966
17196
  }
@@ -16973,8 +17203,8 @@ async function editorStatus(opts = {}) {
16973
17203
  let totalBytes = 0;
16974
17204
  for (const [name, expected] of Object.entries(pin.files)) {
16975
17205
  totalBytes += expected.bytes;
16976
- const file = path32.join(dir, name);
16977
- const info = existsSync27(file) ? await stat4(file).catch(() => null) : null;
17206
+ const file = path33.join(dir, name);
17207
+ const info = existsSync28(file) ? await stat4(file).catch(() => null) : null;
16978
17208
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
16979
17209
  installedBytes += expected.bytes;
16980
17210
  } else {
@@ -17031,13 +17261,13 @@ async function installEditor(opts = {}) {
17031
17261
  if (digest2 !== expected.sha256) {
17032
17262
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17033
17263
  }
17034
- const final = path32.join(dir, name);
17264
+ const final = path33.join(dir, name);
17035
17265
  const tmp = `${final}.${process.pid}.part`;
17036
17266
  try {
17037
- await writeFile15(tmp, bytes);
17267
+ await writeFile16(tmp, bytes);
17038
17268
  await rename10(tmp, final);
17039
17269
  } finally {
17040
- await rm11(tmp, { force: true });
17270
+ await rm12(tmp, { force: true });
17041
17271
  }
17042
17272
  fetchedBytes += bytes.length;
17043
17273
  downloaded.push(name);
@@ -17077,12 +17307,12 @@ var EDITOR_CONTENT_TYPES = {
17077
17307
  ".json": "application/json; charset=utf-8"
17078
17308
  };
17079
17309
  function editorContentType(name) {
17080
- return EDITOR_CONTENT_TYPES[path32.extname(name).toLowerCase()] ?? null;
17310
+ return EDITOR_CONTENT_TYPES[path33.extname(name).toLowerCase()] ?? null;
17081
17311
  }
17082
17312
 
17083
17313
  // src/gallery/editor.ts
17084
- import { readFile as readFile25 } from "fs/promises";
17085
- import path33 from "path";
17314
+ import { readFile as readFile26 } from "fs/promises";
17315
+ import path34 from "path";
17086
17316
  function editorRoute(release, name = "index.html") {
17087
17317
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
17088
17318
  }
@@ -17155,7 +17385,7 @@ function createGalleryEditorHandlers(opts = {}) {
17155
17385
  if (!status.installed) return null;
17156
17386
  let bytes;
17157
17387
  try {
17158
- bytes = await readFile25(path33.join(dir, name));
17388
+ bytes = await readFile26(path34.join(dir, name));
17159
17389
  } catch {
17160
17390
  verified = null;
17161
17391
  return null;
@@ -17172,7 +17402,7 @@ function createGalleryEditorHandlers(opts = {}) {
17172
17402
  // src/gallery/server.ts
17173
17403
  import { randomBytes } from "crypto";
17174
17404
  import { createServer as createServer2 } from "http";
17175
- import { readFile as readFile26 } from "fs/promises";
17405
+ import { readFile as readFile27 } from "fs/promises";
17176
17406
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
17177
17407
  var EDITOR_CSP = [
17178
17408
  "default-src 'self'",
@@ -17353,7 +17583,7 @@ async function serveGallery(opts) {
17353
17583
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17354
17584
  if (!asset) return fail(404, "no such review asset");
17355
17585
  try {
17356
- const bytes = await readFile26(asset.path);
17586
+ const bytes = await readFile27(asset.path);
17357
17587
  res.writeHead(200, {
17358
17588
  "Content-Type": asset.contentType,
17359
17589
  "Cache-Control": "no-store",
@@ -17436,7 +17666,7 @@ async function serveGallery(opts) {
17436
17666
  if (!asset) return fail(404, "no such gallery media");
17437
17667
  let bytes;
17438
17668
  try {
17439
- bytes = await readFile26(asset.path);
17669
+ bytes = await readFile27(asset.path);
17440
17670
  } catch {
17441
17671
  return fail(404, "gallery media is no longer on disk; refresh the page");
17442
17672
  }
@@ -17480,27 +17710,27 @@ async function serveGallery(opts) {
17480
17710
 
17481
17711
  // src/gallery/generate.ts
17482
17712
  import { randomBytes as randomBytes2 } from "crypto";
17483
- import { z as z13 } from "zod";
17484
- var GenerateRequestSchema = z13.object({
17485
- project: z13.string().min(1).optional(),
17486
- keys: z13.array(z13.string().min(1)).min(1).max(500),
17713
+ import { z as z14 } from "zod";
17714
+ var GenerateRequestSchema = z14.object({
17715
+ project: z14.string().min(1).optional(),
17716
+ keys: z14.array(z14.string().min(1)).min(1).max(500),
17487
17717
  /**
17488
17718
  * Regenerate work that is up to date, as `gen --force` does, and let the
17489
17719
  * fetch replace a locally modified or untracked file. With `restore`, put
17490
17720
  * the recorded bytes back over a modified file.
17491
17721
  */
17492
- force: z13.boolean().optional(),
17722
+ force: z14.boolean().optional(),
17493
17723
  /** Advance in-flight, review, or selected work without submitting anything. */
17494
- resume: z13.boolean().optional(),
17724
+ resume: z14.boolean().optional(),
17495
17725
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
17496
- refresh: z13.boolean().optional(),
17726
+ refresh: z14.boolean().optional(),
17497
17727
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
17498
- restore: z13.boolean().optional(),
17728
+ restore: z14.boolean().optional(),
17499
17729
  /**
17500
17730
  * Bring a replaced generation back: 1-based position in the record's
17501
17731
  * history (newest first) or an output hash prefix. One key at a time.
17502
17732
  */
17503
- revert: z13.string().min(1).max(64).optional()
17733
+ revert: z14.string().min(1).max(64).optional()
17504
17734
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
17505
17735
  message: "resume, refresh, restore, and revert are exclusive"
17506
17736
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -17846,19 +18076,19 @@ function createGenerateHandlers(opts) {
17846
18076
  }
17847
18077
 
17848
18078
  // src/pipeline/salvage.ts
17849
- import { readFile as readFile27 } from "fs/promises";
17850
- import { existsSync as existsSync28 } from "fs";
17851
- import path34 from "path";
18079
+ import { readFile as readFile28 } from "fs/promises";
18080
+ import { existsSync as existsSync29 } from "fs";
18081
+ import path35 from "path";
17852
18082
  function providerClaimId(provider, objectId) {
17853
18083
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
17854
18084
  }
17855
18085
  async function loadClaims(lockPaths, opts = {}) {
17856
18086
  const claimed = /* @__PURE__ */ new Set();
17857
18087
  for (const p of lockPaths) {
17858
- if (!existsSync28(p)) throw new Error(`Claim lockfile not found: ${p}`);
18088
+ if (!existsSync29(p)) throw new Error(`Claim lockfile not found: ${p}`);
17859
18089
  let parsed;
17860
18090
  try {
17861
- parsed = parseLock(JSON.parse(await readFile27(p, "utf8")));
18091
+ parsed = parseLock(JSON.parse(await readFile28(p, "utf8")));
17862
18092
  } catch {
17863
18093
  throw new Error(`Claim lockfile is malformed: ${p}`);
17864
18094
  }
@@ -17962,20 +18192,20 @@ function matchOrphanStyle(prompt, manifest) {
17962
18192
  return matchStyleByPattern(prompt, manifest);
17963
18193
  }
17964
18194
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
17965
- const own = path34.resolve(ownManifestPath);
18195
+ const own = path35.resolve(ownManifestPath);
17966
18196
  const siblingManifestPaths = [
17967
18197
  .../* @__PURE__ */ new Set([
17968
18198
  ...workspaceManifestPaths,
17969
- ...claimPaths.map((c) => path34.join(path34.dirname(path34.resolve(c)), "pixelkiln.manifest.json"))
18199
+ ...claimPaths.map((c) => path35.join(path35.dirname(path35.resolve(c)), "pixelkiln.manifest.json"))
17970
18200
  ])
17971
18201
  ];
17972
18202
  const siblings = [];
17973
18203
  for (const siblingManifestPath of siblingManifestPaths) {
17974
- if (path34.resolve(siblingManifestPath) === own) continue;
17975
- if (!existsSync28(siblingManifestPath)) continue;
18204
+ if (path35.resolve(siblingManifestPath) === own) continue;
18205
+ if (!existsSync29(siblingManifestPath)) continue;
17976
18206
  try {
17977
18207
  const { manifest } = await loadManifest(siblingManifestPath);
17978
- siblings.push({ label: path34.basename(path34.dirname(siblingManifestPath)), manifest });
18208
+ siblings.push({ label: path35.basename(path35.dirname(siblingManifestPath)), manifest });
17979
18209
  } catch {
17980
18210
  }
17981
18211
  }
@@ -18095,8 +18325,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18095
18325
  }
18096
18326
 
18097
18327
  // src/pick/salvage-server.ts
18098
- import { mkdir as mkdir12, writeFile as writeFile16, readFile as readFile28 } from "fs/promises";
18099
- import path35 from "path";
18328
+ import { mkdir as mkdir12, writeFile as writeFile17, readFile as readFile29 } from "fs/promises";
18329
+ import path36 from "path";
18100
18330
 
18101
18331
  // src/pick/salvage-sheet.ts
18102
18332
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -18262,7 +18492,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18262
18492
  });
18263
18493
  const html = renderSalvageSheet(orphans, {
18264
18494
  styleId: ctx.styleId,
18265
- importDir: path35.relative(process.cwd(), ctx.importDir) || "."
18495
+ importDir: path36.relative(process.cwd(), ctx.importDir) || "."
18266
18496
  });
18267
18497
  const byId = new Map(orphans.map((o) => [o.id, o]));
18268
18498
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -18318,10 +18548,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18318
18548
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
18319
18549
  decodePng(buf);
18320
18550
  const assetId = idFromPrompt(orphan.prompt, taken);
18321
- const rel = path35.join("_salvaged", `${assetId}.png`);
18322
- const outFile = path35.resolve(ctx.importDir, rel);
18323
- await mkdir12(path35.dirname(outFile), { recursive: true });
18324
- await writeFile16(outFile, buf);
18551
+ const rel = path36.join("_salvaged", `${assetId}.png`);
18552
+ const outFile = path36.resolve(ctx.importDir, rel);
18553
+ await mkdir12(path36.dirname(outFile), { recursive: true });
18554
+ await writeFile17(outFile, buf);
18325
18555
  ctx.manifest.assets[assetId] = {
18326
18556
  prompt: orphan.prompt,
18327
18557
  promptByStyle: {},
@@ -18353,7 +18583,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18353
18583
  sourceUrl: durableSource,
18354
18584
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
18355
18585
  outputs: [{
18356
- path: portableOutputPath(outFile, path35.dirname(ctx.manifestPath)),
18586
+ path: portableOutputPath(outFile, path36.dirname(ctx.manifestPath)),
18357
18587
  sha256: sha256(buf)
18358
18588
  }],
18359
18589
  submittedAt: orphan.createdAt,
@@ -18377,9 +18607,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18377
18607
  }
18378
18608
  }
18379
18609
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18380
- const raw = JSON.parse(await readFile28(ctx.manifestPath, "utf8"));
18610
+ const raw = JSON.parse(await readFile29(ctx.manifestPath, "utf8"));
18381
18611
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
18382
- await writeFile16(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18612
+ await writeFile17(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18383
18613
  await saveLock(ctx.lockPath, ctx.lock);
18384
18614
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18385
18615
  return result;