pixelkiln 0.83.0 → 0.85.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/PROVIDERS.md +3 -3
- package/dist/cli.js +957 -611
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +2 -0
- package/dist/client/gallery.js +3366 -3087
- package/dist/index.cjs +876 -530
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +792 -71
- package/dist/index.d.ts +792 -71
- package/dist/index.js +783 -437
- package/dist/index.js.map +1 -1
- package/docs/MANIFEST.md +10 -6
- package/docs/REVISIONS.md +66 -15
- package/docs/SCENARIO.md +174 -7
- package/package.json +1 -1
- package/schema/manifest.schema.json +22 -0
- package/skills/pixelkiln/references/pixellab-roadmap.md +10 -0
- package/skills/pixelkiln/references/pixellab.md +6 -3
- package/skills/pixelkiln/references/revisions.md +13 -7
- package/skills/pixelkiln/references/scenario.md +8 -4
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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(
|
|
1819
|
+
async request(path38, init) {
|
|
1759
1820
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
1760
|
-
const res = await this.http(`${BASE}${
|
|
1821
|
+
const res = await this.http(`${BASE}${path38}`, {
|
|
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"} ${
|
|
1831
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path38} \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"} ${
|
|
1837
|
+
throw new Error(`${init?.method ?? "GET"} ${path38} 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
|
|
2242
|
+
let path38;
|
|
2182
2243
|
let body;
|
|
2183
2244
|
if (args.mode === "v3") {
|
|
2184
|
-
|
|
2245
|
+
path38 = "/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
|
-
|
|
2259
|
+
path38 = "/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
|
-
|
|
2283
|
+
path38 = "/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
|
-
|
|
2297
|
+
path38 = 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(
|
|
2254
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
2314
|
+
const raw = await this.request(path38, { method: "POST", body: JSON.stringify(body) });
|
|
2315
|
+
return validateResponse(CharacterSubmitSchema, raw, path38.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(
|
|
3528
|
+
async function sha256File(path38) {
|
|
3441
3529
|
return new Promise((resolve, reject) => {
|
|
3442
3530
|
const hash = createHash("sha256");
|
|
3443
|
-
const stream = createReadStream(
|
|
3531
|
+
const stream = createReadStream(path38);
|
|
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(
|
|
5935
|
+
async call(path38, init = {}) {
|
|
5812
5936
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
5813
|
-
const response = await this.request(`${this.baseUrl}${
|
|
5937
|
+
const response = await this.request(`${this.baseUrl}${path38}`, {
|
|
5814
5938
|
...init,
|
|
5815
5939
|
headers: {
|
|
5816
5940
|
"Content-Type": "application/json",
|
|
@@ -6119,8 +6243,13 @@ function retroError(value) {
|
|
|
6119
6243
|
}
|
|
6120
6244
|
|
|
6121
6245
|
// src/providers/scenario.ts
|
|
6246
|
+
import { createHash as createHash2 } from "crypto";
|
|
6247
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
6248
|
+
import os2 from "os";
|
|
6249
|
+
import path2 from "path";
|
|
6122
6250
|
var DEFAULT_BASE_URL2 = "https://api.cloud.scenario.com/v1";
|
|
6123
6251
|
var SOURCE_PROTOCOL2 = "scenario:";
|
|
6252
|
+
var SUBMIT_RETRIES = 8;
|
|
6124
6253
|
var PROTECTED_PARAMETERS = /* @__PURE__ */ new Set([
|
|
6125
6254
|
"dryRun",
|
|
6126
6255
|
"height",
|
|
@@ -6167,7 +6296,11 @@ var ScenarioClient = class {
|
|
|
6167
6296
|
async submit(modelId, body, projectId) {
|
|
6168
6297
|
const response = await this.call(
|
|
6169
6298
|
`/generate/custom/${encodeURIComponent(modelId)}`,
|
|
6170
|
-
|
|
6299
|
+
// A 429 here means this account's concurrent-job limit refused the
|
|
6300
|
+
// request, so nothing was accepted or billed; wait it out instead of
|
|
6301
|
+
// failing the asset. The default four retries give up after about half
|
|
6302
|
+
// a minute, before a running job has finished.
|
|
6303
|
+
{ method: "POST", body: JSON.stringify(body), retries: SUBMIT_RETRIES },
|
|
6171
6304
|
{ projectId }
|
|
6172
6305
|
);
|
|
6173
6306
|
const job = response?.job;
|
|
@@ -6175,6 +6308,29 @@ var ScenarioClient = class {
|
|
|
6175
6308
|
if (!jobId) throw new Error("Scenario did not return an async job id");
|
|
6176
6309
|
return jobId;
|
|
6177
6310
|
}
|
|
6311
|
+
/**
|
|
6312
|
+
* Uploads one image (POST /assets) and returns its asset id, which a model's
|
|
6313
|
+
* reference input takes. Free, and idempotent per image: the id is kept by
|
|
6314
|
+
* content hash so a rerun does not upload the same bytes again.
|
|
6315
|
+
*/
|
|
6316
|
+
async uploadImage(image, projectId) {
|
|
6317
|
+
const bytes = Buffer.from(image.base64, "base64");
|
|
6318
|
+
const digest2 = sha256(bytes);
|
|
6319
|
+
const cached = readUploadCache()[uploadCacheKey(this.baseUrl, projectId, digest2)];
|
|
6320
|
+
if (cached) return cached;
|
|
6321
|
+
const response = await this.call(
|
|
6322
|
+
"/assets",
|
|
6323
|
+
{
|
|
6324
|
+
method: "POST",
|
|
6325
|
+
body: JSON.stringify({ image: image.base64, name: `pixelkiln-${digest2.slice(0, 12)}.${image.format === "png" ? "png" : "jpg"}` })
|
|
6326
|
+
},
|
|
6327
|
+
{ projectId }
|
|
6328
|
+
);
|
|
6329
|
+
const id = response?.asset?.id;
|
|
6330
|
+
if (typeof id !== "string" || !id) throw new Error("Scenario did not return an uploaded asset id");
|
|
6331
|
+
writeUploadCache(uploadCacheKey(this.baseUrl, projectId, digest2), id);
|
|
6332
|
+
return id;
|
|
6333
|
+
}
|
|
6178
6334
|
async job(id, projectId) {
|
|
6179
6335
|
const response = await this.call(`/jobs/${encodeURIComponent(id)}`, {}, { projectId });
|
|
6180
6336
|
const raw = response?.job;
|
|
@@ -6301,6 +6457,14 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
6301
6457
|
supports(generator) {
|
|
6302
6458
|
return generator === "map";
|
|
6303
6459
|
}
|
|
6460
|
+
/**
|
|
6461
|
+
* An edit is a generation with the parent as a reference image, which the
|
|
6462
|
+
* editing models (GPT Image, Gemini, FLUX) take. Masked inpainting and the
|
|
6463
|
+
* PixelLab-specific modes have no common Scenario input.
|
|
6464
|
+
*/
|
|
6465
|
+
supportsRevision(mode2) {
|
|
6466
|
+
return mode2 === "image-to-image";
|
|
6467
|
+
}
|
|
6304
6468
|
estimate(spec) {
|
|
6305
6469
|
const options = scenarioOptions(spec);
|
|
6306
6470
|
return {
|
|
@@ -6321,11 +6485,31 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
6321
6485
|
if (!Number.isInteger(count) || count < 1 || count > 4) {
|
|
6322
6486
|
throw new Error("Scenario numOutputs must be a whole number from 1 to 4");
|
|
6323
6487
|
}
|
|
6324
|
-
if (spec.width
|
|
6325
|
-
throw new Error("Scenario width and height must be
|
|
6488
|
+
if (!Number.isInteger(spec.width) || spec.width < 16 || spec.width > 4096 || !Number.isInteger(spec.height) || spec.height < 16 || spec.height > 4096) {
|
|
6489
|
+
throw new Error("Scenario width and height must be whole numbers from 16 to 4096");
|
|
6326
6490
|
}
|
|
6327
|
-
|
|
6328
|
-
|
|
6491
|
+
const revision = spec.revision;
|
|
6492
|
+
if (revision) {
|
|
6493
|
+
if (revision.sourceMembers?.length) {
|
|
6494
|
+
throw new Error(
|
|
6495
|
+
`Scenario edits one image; ${revision.sourceAssetId} is a ${revision.sourceMembers.length}-member set`
|
|
6496
|
+
);
|
|
6497
|
+
}
|
|
6498
|
+
if (revision.strength != null) {
|
|
6499
|
+
throw new Error("Scenario image-to-image takes no strength; use the model's own parameters");
|
|
6500
|
+
}
|
|
6501
|
+
}
|
|
6502
|
+
const parameter = referenceParameter(options);
|
|
6503
|
+
if ((styleImages.length || revision) && Object.hasOwn(options.parameters ?? {}, parameter)) {
|
|
6504
|
+
throw new Error(
|
|
6505
|
+
`Scenario parameters.${parameter} is the reference-image input and is filled from styleImages and revisions`
|
|
6506
|
+
);
|
|
6507
|
+
}
|
|
6508
|
+
if (styleImages.length + (revision ? 1 : 0) > 1 && options.referenceArray === false) {
|
|
6509
|
+
throw new Error(`Scenario ${parameter} takes one image; use one style image or a revision, not both`);
|
|
6510
|
+
}
|
|
6511
|
+
if (options.referenceParameter != null && (typeof options.referenceParameter !== "string" || !options.referenceParameter.trim())) {
|
|
6512
|
+
throw new Error("Scenario referenceParameter must be a non-empty string");
|
|
6329
6513
|
}
|
|
6330
6514
|
if (options.projectId != null && (typeof options.projectId !== "string" || !options.projectId.trim())) {
|
|
6331
6515
|
throw new Error("Scenario projectId must be a non-empty string");
|
|
@@ -6338,7 +6522,12 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
6338
6522
|
async submit(spec, styleImages) {
|
|
6339
6523
|
this.validate(spec, styleImages);
|
|
6340
6524
|
const options = scenarioOptions(spec);
|
|
6341
|
-
const
|
|
6525
|
+
const references = [];
|
|
6526
|
+
if (spec.revision) references.push(readRevisionSource(spec));
|
|
6527
|
+
for (const image of styleImages) references.push({ base64: image.base64, format: image.format });
|
|
6528
|
+
const referenceAssetIds = [];
|
|
6529
|
+
for (const image of references) referenceAssetIds.push(await this.client.uploadImage(image, options.projectId));
|
|
6530
|
+
const body = requestBody(spec, options, referenceAssetIds);
|
|
6342
6531
|
const quote = await this.client.quote(options.modelId, body, options.projectId);
|
|
6343
6532
|
if (quote.total > options.maxComputeUnits + Number.EPSILON) {
|
|
6344
6533
|
throw new Error(
|
|
@@ -6355,7 +6544,8 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
6355
6544
|
quotedComputeUnits: quote.total,
|
|
6356
6545
|
creativeUnitsCost: quote.creativeUnitsCost,
|
|
6357
6546
|
costDetails: quote.costDetails,
|
|
6358
|
-
ipDetectionCost: quote.ipDetectionCost
|
|
6547
|
+
ipDetectionCost: quote.ipDetectionCost,
|
|
6548
|
+
...referenceAssetIds.length ? { referenceAssetIds } : {}
|
|
6359
6549
|
}
|
|
6360
6550
|
};
|
|
6361
6551
|
}
|
|
@@ -6377,7 +6567,13 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
6377
6567
|
}
|
|
6378
6568
|
const assetIds = scenarioAssetIds(job);
|
|
6379
6569
|
if (!assetIds.length) return { status: "failed", error: "Scenario job returned no asset IDs" };
|
|
6380
|
-
|
|
6570
|
+
let assets;
|
|
6571
|
+
try {
|
|
6572
|
+
assets = await this.assets(assetIds, reference.projectId);
|
|
6573
|
+
} catch (error) {
|
|
6574
|
+
if (error instanceof UnsupportedScenarioOutput) return { status: "failed", error: error.message };
|
|
6575
|
+
throw error;
|
|
6576
|
+
}
|
|
6381
6577
|
if (assets.length > 1) {
|
|
6382
6578
|
return {
|
|
6383
6579
|
status: "review",
|
|
@@ -6432,6 +6628,32 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
6432
6628
|
return assets.sort((left, right) => left.outputIndex - right.outputIndex || (jobOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (jobOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER) || left.id.localeCompare(right.id));
|
|
6433
6629
|
}
|
|
6434
6630
|
};
|
|
6631
|
+
function uploadCachePath() {
|
|
6632
|
+
return process.env.SCENARIO_UPLOAD_CACHE ?? path2.join(os2.tmpdir(), "pixelkiln-scenario", "uploads.json");
|
|
6633
|
+
}
|
|
6634
|
+
function uploadCacheKey(baseUrl, projectId, digest2) {
|
|
6635
|
+
return createHash2("sha256").update(`${baseUrl}
|
|
6636
|
+
${projectId ?? ""}
|
|
6637
|
+
${digest2}`).digest("hex");
|
|
6638
|
+
}
|
|
6639
|
+
function readUploadCache() {
|
|
6640
|
+
try {
|
|
6641
|
+
const value = JSON.parse(readFileSync2(uploadCachePath(), "utf8"));
|
|
6642
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
6643
|
+
} catch {
|
|
6644
|
+
return {};
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
function writeUploadCache(key, assetId) {
|
|
6648
|
+
try {
|
|
6649
|
+
const file = uploadCachePath();
|
|
6650
|
+
mkdirSync2(path2.dirname(file), { recursive: true });
|
|
6651
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
6652
|
+
writeFileSync2(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
|
|
6653
|
+
renameSync(tmp, file);
|
|
6654
|
+
} catch {
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6435
6657
|
function scenarioOptions(spec) {
|
|
6436
6658
|
return spec.providerOptions;
|
|
6437
6659
|
}
|
|
@@ -6447,9 +6669,25 @@ function scenarioBaseUrl(value) {
|
|
|
6447
6669
|
}
|
|
6448
6670
|
return url.href.replace(/\/$/, "");
|
|
6449
6671
|
}
|
|
6450
|
-
function
|
|
6672
|
+
function referenceParameter(options) {
|
|
6673
|
+
return options.referenceParameter?.trim() || "referenceImages";
|
|
6674
|
+
}
|
|
6675
|
+
function readRevisionSource(spec) {
|
|
6676
|
+
const revision = spec.revision;
|
|
6677
|
+
if (!revision.sourceFile || !revision.sourceSha256 || !revision.sourceFormat) {
|
|
6678
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
|
|
6679
|
+
}
|
|
6680
|
+
const bytes = readFileSync2(revision.sourceFile);
|
|
6681
|
+
if (sha256(bytes) !== revision.sourceSha256) {
|
|
6682
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
|
|
6683
|
+
}
|
|
6684
|
+
return { base64: bytes.toString("base64"), format: revision.sourceFormat };
|
|
6685
|
+
}
|
|
6686
|
+
function requestBody(spec, options, referenceAssetIds = []) {
|
|
6687
|
+
const parameter = referenceParameter(options);
|
|
6451
6688
|
return {
|
|
6452
6689
|
...options.parameters ?? {},
|
|
6690
|
+
...referenceAssetIds.length ? { [parameter]: options.referenceArray === false ? referenceAssetIds[0] : referenceAssetIds } : {},
|
|
6453
6691
|
prompt: spec.prompt,
|
|
6454
6692
|
width: spec.width,
|
|
6455
6693
|
height: spec.height,
|
|
@@ -6539,9 +6777,11 @@ function scenarioAssetIds(job) {
|
|
|
6539
6777
|
function assetDownloadUrl(asset) {
|
|
6540
6778
|
return asset.originalFileUrl || asset.url;
|
|
6541
6779
|
}
|
|
6780
|
+
var UnsupportedScenarioOutput = class extends Error {
|
|
6781
|
+
};
|
|
6542
6782
|
function assertPngAsset(asset) {
|
|
6543
6783
|
if (asset.originalMimeType && asset.originalMimeType !== MediaType.PNG) {
|
|
6544
|
-
throw new
|
|
6784
|
+
throw new UnsupportedScenarioOutput(
|
|
6545
6785
|
`Scenario asset ${asset.id} is ${asset.originalMimeType}; PixelKiln's Scenario MVP requires PNG output`
|
|
6546
6786
|
);
|
|
6547
6787
|
}
|
|
@@ -6608,7 +6848,7 @@ function redactScenarioError(value, secrets) {
|
|
|
6608
6848
|
// src/providers/comfyui.ts
|
|
6609
6849
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6610
6850
|
import { readFile } from "fs/promises";
|
|
6611
|
-
import
|
|
6851
|
+
import path3 from "path";
|
|
6612
6852
|
var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
|
|
6613
6853
|
var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
|
|
6614
6854
|
"prompt",
|
|
@@ -6745,7 +6985,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
6745
6985
|
}
|
|
6746
6986
|
async resolveOptions(value, context) {
|
|
6747
6987
|
const options = parseOptions(value);
|
|
6748
|
-
const workflowPath =
|
|
6988
|
+
const workflowPath = path3.resolve(context.root, options.workflowFile);
|
|
6749
6989
|
let raw;
|
|
6750
6990
|
try {
|
|
6751
6991
|
raw = await readFile(workflowPath, "utf8");
|
|
@@ -6800,7 +7040,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
6800
7040
|
if (typeof item !== "string") {
|
|
6801
7041
|
throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
|
|
6802
7042
|
}
|
|
6803
|
-
const file =
|
|
7043
|
+
const file = path3.resolve(context.root, item);
|
|
6804
7044
|
let bytes;
|
|
6805
7045
|
try {
|
|
6806
7046
|
bytes = await readFile(file);
|
|
@@ -7512,7 +7752,7 @@ function canonical(value) {
|
|
|
7512
7752
|
}
|
|
7513
7753
|
|
|
7514
7754
|
// src/providers/fake.ts
|
|
7515
|
-
import { createHash as
|
|
7755
|
+
import { createHash as createHash3 } from "crypto";
|
|
7516
7756
|
var FAKE_PNG = Buffer.from(
|
|
7517
7757
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
|
7518
7758
|
"base64"
|
|
@@ -7614,7 +7854,7 @@ var FakeProvider = class {
|
|
|
7614
7854
|
}
|
|
7615
7855
|
async download(url) {
|
|
7616
7856
|
if (!url.startsWith("fake://")) throw new Error(`fake provider cannot download ${url}`);
|
|
7617
|
-
const salt =
|
|
7857
|
+
const salt = createHash3("sha256").update(url).digest();
|
|
7618
7858
|
return Buffer.concat([FAKE_PNG, salt.subarray(0, 4)]);
|
|
7619
7859
|
}
|
|
7620
7860
|
async balance() {
|
|
@@ -7763,7 +8003,7 @@ registerProvider({
|
|
|
7763
8003
|
// src/manifest.ts
|
|
7764
8004
|
import { readFile as readFile3 } from "fs/promises";
|
|
7765
8005
|
import { existsSync as existsSync4 } from "fs";
|
|
7766
|
-
import
|
|
8006
|
+
import path6 from "path";
|
|
7767
8007
|
|
|
7768
8008
|
// src/loop-directions.ts
|
|
7769
8009
|
var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
|
|
@@ -7852,12 +8092,12 @@ function isObject2(value) {
|
|
|
7852
8092
|
|
|
7853
8093
|
// src/outputs.ts
|
|
7854
8094
|
import { existsSync as existsSync3 } from "fs";
|
|
7855
|
-
import
|
|
8095
|
+
import path5 from "path";
|
|
7856
8096
|
|
|
7857
8097
|
// src/lock.ts
|
|
7858
8098
|
import { readFile as readFile2, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
|
|
7859
8099
|
import { existsSync as existsSync2 } from "fs";
|
|
7860
|
-
import
|
|
8100
|
+
import path4 from "path";
|
|
7861
8101
|
async function loadLock(lockPath) {
|
|
7862
8102
|
if (!existsSync2(lockPath)) return { version: 2, entries: {} };
|
|
7863
8103
|
try {
|
|
@@ -7874,7 +8114,7 @@ var saveQueues = /* @__PURE__ */ new Map();
|
|
|
7874
8114
|
var dirtyPatches = /* @__PURE__ */ new WeakMap();
|
|
7875
8115
|
var dirtyDeletes = /* @__PURE__ */ new WeakMap();
|
|
7876
8116
|
function saveLock(lockPath, lock) {
|
|
7877
|
-
const queueKey =
|
|
8117
|
+
const queueKey = path4.resolve(lockPath);
|
|
7878
8118
|
const previous = saveQueues.get(queueKey) ?? Promise.resolve();
|
|
7879
8119
|
const next = previous.catch(() => {
|
|
7880
8120
|
}).then(() => writeLockNow(lockPath, lock));
|
|
@@ -7886,7 +8126,7 @@ function saveLock(lockPath, lock) {
|
|
|
7886
8126
|
return next;
|
|
7887
8127
|
}
|
|
7888
8128
|
async function writeLockNow(lockPath, lock) {
|
|
7889
|
-
await mkdir(
|
|
8129
|
+
await mkdir(path4.dirname(path4.resolve(lockPath)), { recursive: true });
|
|
7890
8130
|
const release = await acquireFileLock(lockPath);
|
|
7891
8131
|
try {
|
|
7892
8132
|
await writeLockWhileHeld(lockPath, lock);
|
|
@@ -8031,22 +8271,22 @@ function totalSpend(lock, unit = "generations") {
|
|
|
8031
8271
|
|
|
8032
8272
|
// src/outputs.ts
|
|
8033
8273
|
function portableOutputPath(file, manifestDir) {
|
|
8034
|
-
const root =
|
|
8035
|
-
const absolute =
|
|
8036
|
-
const relative =
|
|
8037
|
-
const value =
|
|
8038
|
-
return value.split(
|
|
8274
|
+
const root = path5.resolve(manifestDir);
|
|
8275
|
+
const absolute = path5.isAbsolute(file) ? path5.normalize(file) : path5.resolve(root, file);
|
|
8276
|
+
const relative = path5.relative(root, absolute);
|
|
8277
|
+
const value = path5.isAbsolute(relative) ? absolute : relative;
|
|
8278
|
+
return value.split(path5.sep).join("/");
|
|
8039
8279
|
}
|
|
8040
8280
|
function resolveOutputPath(recordedPath, manifestDir) {
|
|
8041
|
-
if (
|
|
8042
|
-
if (
|
|
8043
|
-
return
|
|
8281
|
+
if (path5.isAbsolute(recordedPath)) return path5.normalize(recordedPath);
|
|
8282
|
+
if (path5.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
8283
|
+
return path5.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path5.sep));
|
|
8044
8284
|
}
|
|
8045
8285
|
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
8046
8286
|
return memberPath(spec.outFile, role, index, total, mediaType);
|
|
8047
8287
|
}
|
|
8048
8288
|
function memberPath(file, role, index, total, mediaType) {
|
|
8049
|
-
const originalExt =
|
|
8289
|
+
const originalExt = path5.extname(file);
|
|
8050
8290
|
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
8051
8291
|
const stem = originalExt ? file.slice(0, -originalExt.length) : file;
|
|
8052
8292
|
if (total === 1) return `${stem}${ext}`;
|
|
@@ -8060,10 +8300,10 @@ function isFrameSetEntry(entry) {
|
|
|
8060
8300
|
return entry.generator === "frames" && isMemberSetEntry(entry);
|
|
8061
8301
|
}
|
|
8062
8302
|
function sourceIsStem(source, entry, manifestDir) {
|
|
8063
|
-
return isMemberSetEntry(entry) && !existsSync3(
|
|
8303
|
+
return isMemberSetEntry(entry) && !existsSync3(path5.resolve(manifestDir, source));
|
|
8064
8304
|
}
|
|
8065
8305
|
function sourceOutputPath(source, entry, index, manifestDir) {
|
|
8066
|
-
const absolute =
|
|
8306
|
+
const absolute = path5.resolve(manifestDir, source);
|
|
8067
8307
|
if (!sourceIsStem(source, entry, manifestDir)) return absolute;
|
|
8068
8308
|
const output = entry.outputs[index];
|
|
8069
8309
|
return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
|
|
@@ -8150,7 +8390,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
|
8150
8390
|
id: spec.assetId,
|
|
8151
8391
|
index: 0,
|
|
8152
8392
|
path: spec.outFile,
|
|
8153
|
-
absolutePath:
|
|
8393
|
+
absolutePath: path5.resolve(spec.outFile),
|
|
8154
8394
|
sha256: ""
|
|
8155
8395
|
}];
|
|
8156
8396
|
}
|
|
@@ -8166,7 +8406,7 @@ function frameSetFps(entry) {
|
|
|
8166
8406
|
|
|
8167
8407
|
// src/manifest.ts
|
|
8168
8408
|
async function loadManifest(manifestPath) {
|
|
8169
|
-
const abs =
|
|
8409
|
+
const abs = path6.resolve(manifestPath);
|
|
8170
8410
|
if (!existsSync4(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
8171
8411
|
const expansion = expandLoopDirections(JSON.parse(await readFile3(abs, "utf8")));
|
|
8172
8412
|
if (expansion.issues.length) {
|
|
@@ -8259,7 +8499,7 @@ ${issues}`);
|
|
|
8259
8499
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
8260
8500
|
}
|
|
8261
8501
|
const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
|
|
8262
|
-
return { manifest: parsed.data, root:
|
|
8502
|
+
return { manifest: parsed.data, root: path6.dirname(abs), path: abs, ...loopFamilies };
|
|
8263
8503
|
}
|
|
8264
8504
|
function parentAssetId(asset) {
|
|
8265
8505
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
|
|
@@ -8360,7 +8600,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8360
8600
|
}
|
|
8361
8601
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
8362
8602
|
async function loadStyleImage(rel, what = "Style image") {
|
|
8363
|
-
const abs =
|
|
8603
|
+
const abs = path6.resolve(root, rel);
|
|
8364
8604
|
let hit = styleImageCache.get(abs);
|
|
8365
8605
|
if (!hit) {
|
|
8366
8606
|
if (!existsSync4(abs)) throw new Error(`${what} not found: ${abs}`);
|
|
@@ -8390,7 +8630,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8390
8630
|
styleImageDimensions.push(loadedImage);
|
|
8391
8631
|
}
|
|
8392
8632
|
const resolvedImages = style.styleImages.map((image) => {
|
|
8393
|
-
const hit = styleImageCache.get(
|
|
8633
|
+
const hit = styleImageCache.get(path6.resolve(root, image.path));
|
|
8394
8634
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
8395
8635
|
});
|
|
8396
8636
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
@@ -8491,9 +8731,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8491
8731
|
} else {
|
|
8492
8732
|
prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
8493
8733
|
}
|
|
8494
|
-
const relFile = asset.file ??
|
|
8495
|
-
const outFile =
|
|
8496
|
-
const qualityOutFile = style.quality ?
|
|
8734
|
+
const relFile = asset.file ?? path6.join(asset.category ?? "", `${assetId}.png`);
|
|
8735
|
+
const outFile = path6.resolve(root, style.outDir, relFile);
|
|
8736
|
+
const qualityOutFile = style.quality ? path6.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
8497
8737
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
8498
8738
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
8499
8739
|
const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
|
|
@@ -8588,7 +8828,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8588
8828
|
minGridConfidence: style.quality.minGridConfidence,
|
|
8589
8829
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
8590
8830
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
8591
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
8831
|
+
...style.quality.fixerPython ? { fixerPython: path6.resolve(root, style.quality.fixerPython) } : {},
|
|
8592
8832
|
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
8593
8833
|
}
|
|
8594
8834
|
} : {},
|
|
@@ -8724,7 +8964,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8724
8964
|
animation: sourceSpec.character.animation,
|
|
8725
8965
|
outfit: {
|
|
8726
8966
|
reference: {
|
|
8727
|
-
path:
|
|
8967
|
+
path: path6.resolve(root, asset.outfit.reference),
|
|
8728
8968
|
sha256: referenceImage.hash,
|
|
8729
8969
|
width: referenceImage.width,
|
|
8730
8970
|
height: referenceImage.height,
|
|
@@ -8746,23 +8986,23 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8746
8986
|
);
|
|
8747
8987
|
}
|
|
8748
8988
|
const sourceSpec = await finalize(asset.revision.from);
|
|
8749
|
-
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
8989
|
+
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path6.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
8750
8990
|
const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
|
|
8751
8991
|
const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
|
|
8752
8992
|
const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
|
|
8753
8993
|
const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
|
|
8754
|
-
const maskFile = asset.revision.mask ?
|
|
8994
|
+
const maskFile = asset.revision.mask ? path6.resolve(root, asset.revision.mask) : void 0;
|
|
8755
8995
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
8756
8996
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
8757
8997
|
throw new Error(
|
|
8758
8998
|
`Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
|
|
8759
8999
|
);
|
|
8760
9000
|
}
|
|
8761
|
-
const paletteImageFile = asset.revision.paletteImage ?
|
|
9001
|
+
const paletteImageFile = asset.revision.paletteImage ? path6.resolve(root, asset.revision.paletteImage) : void 0;
|
|
8762
9002
|
const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
|
|
8763
|
-
const lastFrameFile = asset.revision.lastFrame ?
|
|
9003
|
+
const lastFrameFile = asset.revision.lastFrame ? path6.resolve(root, asset.revision.lastFrame) : void 0;
|
|
8764
9004
|
const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
|
|
8765
|
-
const keypointsFile = asset.revision.keypointsFile ?
|
|
9005
|
+
const keypointsFile = asset.revision.keypointsFile ? path6.resolve(root, asset.revision.keypointsFile) : void 0;
|
|
8766
9006
|
const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
|
|
8767
9007
|
resolved.revision = {
|
|
8768
9008
|
mode: asset.revision.mode,
|
|
@@ -8816,6 +9056,10 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8816
9056
|
} : {},
|
|
8817
9057
|
...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
|
|
8818
9058
|
...asset.revision.description ? { description: asset.revision.description } : {},
|
|
9059
|
+
...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
|
|
9060
|
+
...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
|
|
9061
|
+
...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
|
|
9062
|
+
...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
|
|
8819
9063
|
...asset.revision.engine ? { engine: asset.revision.engine } : {}
|
|
8820
9064
|
};
|
|
8821
9065
|
}
|
|
@@ -8925,7 +9169,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
8925
9169
|
return specs;
|
|
8926
9170
|
}
|
|
8927
9171
|
function pngPath(file) {
|
|
8928
|
-
const extension =
|
|
9172
|
+
const extension = path6.extname(file);
|
|
8929
9173
|
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
8930
9174
|
}
|
|
8931
9175
|
async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
|
|
@@ -8935,12 +9179,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
|
|
|
8935
9179
|
if (!provider.supportsMapScene) {
|
|
8936
9180
|
throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
|
|
8937
9181
|
}
|
|
8938
|
-
const file =
|
|
9182
|
+
const file = path6.resolve(root, scene.image);
|
|
8939
9183
|
const image = await optionalRevisionImage(file, "scene image", "png");
|
|
8940
9184
|
const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
|
|
8941
9185
|
const placement = scene.placement;
|
|
8942
9186
|
if ("mask" in placement) {
|
|
8943
|
-
const maskFile =
|
|
9187
|
+
const maskFile = path6.resolve(root, placement.mask);
|
|
8944
9188
|
const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
|
|
8945
9189
|
if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
|
|
8946
9190
|
throw new Error(
|
|
@@ -9012,7 +9256,7 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
9012
9256
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
9013
9257
|
const out = [];
|
|
9014
9258
|
for (const img of style.styleImages) {
|
|
9015
|
-
const file =
|
|
9259
|
+
const file = path6.resolve(loaded.root, img.path);
|
|
9016
9260
|
const buf = await readFile3(file);
|
|
9017
9261
|
const metadata = imageMetadata(buf);
|
|
9018
9262
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
@@ -9043,7 +9287,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
9043
9287
|
};
|
|
9044
9288
|
if (asset.concept) {
|
|
9045
9289
|
const image = await files.load(asset.concept, "Concept image");
|
|
9046
|
-
shape.concept = { path:
|
|
9290
|
+
shape.concept = { path: path6.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
9047
9291
|
}
|
|
9048
9292
|
if (asset.reference) {
|
|
9049
9293
|
const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
|
|
@@ -9051,7 +9295,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
9051
9295
|
for (const [direction, rel] of Object.entries(byDirection)) {
|
|
9052
9296
|
const image = await files.load(rel, `Reference sprite (${direction})`);
|
|
9053
9297
|
shape.reference[direction] = {
|
|
9054
|
-
path:
|
|
9298
|
+
path: path6.resolve(files.root, rel),
|
|
9055
9299
|
sha256: image.hash,
|
|
9056
9300
|
width: image.width,
|
|
9057
9301
|
height: image.height,
|
|
@@ -9072,7 +9316,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
9072
9316
|
const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
|
|
9073
9317
|
const pose = async (rel, what) => {
|
|
9074
9318
|
const image = await files.load(rel, what);
|
|
9075
|
-
return { path:
|
|
9319
|
+
return { path: path6.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
9076
9320
|
};
|
|
9077
9321
|
shape.animation = {
|
|
9078
9322
|
mode: animationMode,
|
|
@@ -9103,7 +9347,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
9103
9347
|
const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
|
|
9104
9348
|
if (south) {
|
|
9105
9349
|
const image = await files.load(south, "Reference sprite");
|
|
9106
|
-
shape.reference = { path:
|
|
9350
|
+
shape.reference = { path: path6.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
9107
9351
|
}
|
|
9108
9352
|
}
|
|
9109
9353
|
if (asset.state) {
|
|
@@ -9131,7 +9375,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
9131
9375
|
}
|
|
9132
9376
|
const pose = async (rel, what) => {
|
|
9133
9377
|
const image = await files.load(rel, what);
|
|
9134
|
-
return { path:
|
|
9378
|
+
return { path: path6.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
9135
9379
|
};
|
|
9136
9380
|
shape.animation = {
|
|
9137
9381
|
mode: asset.animation.mode ?? "v3",
|
|
@@ -9151,11 +9395,11 @@ function characterDirections(directions) {
|
|
|
9151
9395
|
}
|
|
9152
9396
|
|
|
9153
9397
|
// src/project.ts
|
|
9154
|
-
import
|
|
9398
|
+
import path14 from "path";
|
|
9155
9399
|
|
|
9156
9400
|
// src/env.ts
|
|
9157
|
-
import { readFileSync as
|
|
9158
|
-
import
|
|
9401
|
+
import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
|
|
9402
|
+
import path7 from "path";
|
|
9159
9403
|
function loadEnvFiles(dir) {
|
|
9160
9404
|
const loaded = [];
|
|
9161
9405
|
for (const [file, vars] of envFiles(dir)) {
|
|
@@ -9169,10 +9413,10 @@ function loadEnvFiles(dir) {
|
|
|
9169
9413
|
function envFiles(dir) {
|
|
9170
9414
|
const out = [];
|
|
9171
9415
|
for (const name of [".env.local", ".env"]) {
|
|
9172
|
-
const file =
|
|
9416
|
+
const file = path7.join(dir, name);
|
|
9173
9417
|
if (!existsSync5(file)) continue;
|
|
9174
9418
|
try {
|
|
9175
|
-
out.push([file, parseEnv(
|
|
9419
|
+
out.push([file, parseEnv(readFileSync3(file, "utf8"))]);
|
|
9176
9420
|
} catch {
|
|
9177
9421
|
}
|
|
9178
9422
|
}
|
|
@@ -9199,11 +9443,11 @@ function parseEnv(contents) {
|
|
|
9199
9443
|
|
|
9200
9444
|
// src/pipeline/plan.ts
|
|
9201
9445
|
import { existsSync as existsSync8 } from "fs";
|
|
9202
|
-
import
|
|
9446
|
+
import path13 from "path";
|
|
9203
9447
|
|
|
9204
9448
|
// src/pipeline/mirror.ts
|
|
9205
9449
|
import { mkdir as mkdir2, readFile as readFile4, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
9206
|
-
import
|
|
9450
|
+
import path8 from "path";
|
|
9207
9451
|
|
|
9208
9452
|
// src/palette.ts
|
|
9209
9453
|
function normalizePalette(values) {
|
|
@@ -9342,7 +9586,7 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
|
|
|
9342
9586
|
for (const [index, member] of members.entries()) {
|
|
9343
9587
|
const bytes = flipHorizontal(await readFile4(member.file));
|
|
9344
9588
|
const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
|
|
9345
|
-
await mkdir2(
|
|
9589
|
+
await mkdir2(path8.dirname(target), { recursive: true });
|
|
9346
9590
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
9347
9591
|
await writeFile2(tmp, bytes);
|
|
9348
9592
|
await rename2(tmp, target);
|
|
@@ -9405,28 +9649,28 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
|
|
|
9405
9649
|
|
|
9406
9650
|
// src/pipeline/quality-profile.ts
|
|
9407
9651
|
import { existsSync as existsSync6 } from "fs";
|
|
9408
|
-
import
|
|
9652
|
+
import path11 from "path";
|
|
9409
9653
|
|
|
9410
9654
|
// src/pipeline/refine.ts
|
|
9411
|
-
import
|
|
9655
|
+
import path10 from "path";
|
|
9412
9656
|
import { execFile } from "child_process";
|
|
9413
9657
|
import { mkdtemp, readFile as readFile6, rm as rm3 } from "fs/promises";
|
|
9414
9658
|
import { tmpdir } from "os";
|
|
9415
9659
|
import { promisify } from "util";
|
|
9416
9660
|
|
|
9417
9661
|
// src/artifacts.ts
|
|
9418
|
-
import
|
|
9419
|
-
import { createHash as
|
|
9662
|
+
import path9 from "path";
|
|
9663
|
+
import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
|
|
9420
9664
|
import { mkdir as mkdir3, readFile as readFile5, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
9421
9665
|
var activeTransactions = /* @__PURE__ */ new Set();
|
|
9422
9666
|
function message(error) {
|
|
9423
9667
|
return error instanceof Error ? error.message : String(error);
|
|
9424
9668
|
}
|
|
9425
9669
|
function digest(data) {
|
|
9426
|
-
return
|
|
9670
|
+
return createHash4("sha256").update(data).digest("hex");
|
|
9427
9671
|
}
|
|
9428
9672
|
function portableRelative(from, to) {
|
|
9429
|
-
return
|
|
9673
|
+
return path9.relative(from, path9.resolve(to)).split(path9.sep).join("/") || ".";
|
|
9430
9674
|
}
|
|
9431
9675
|
function canonical2(value) {
|
|
9432
9676
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
@@ -9476,7 +9720,7 @@ function parseArtifactManifest(absolute, data) {
|
|
|
9476
9720
|
return raw;
|
|
9477
9721
|
}
|
|
9478
9722
|
async function readArtifactBundleManifest(manifestPath) {
|
|
9479
|
-
const absolute =
|
|
9723
|
+
const absolute = path9.resolve(manifestPath);
|
|
9480
9724
|
return parseArtifactManifest(absolute, await readFile5(absolute, "utf8"));
|
|
9481
9725
|
}
|
|
9482
9726
|
async function readOptional(file) {
|
|
@@ -9500,8 +9744,8 @@ function processIsAlive(pid) {
|
|
|
9500
9744
|
}
|
|
9501
9745
|
}
|
|
9502
9746
|
function validTemporaryPath(candidate, destination, type) {
|
|
9503
|
-
return
|
|
9504
|
-
`.${
|
|
9747
|
+
return path9.dirname(candidate) === path9.dirname(destination) && path9.basename(candidate).startsWith(
|
|
9748
|
+
`.${path9.basename(destination)}.pixelkiln-${type}-`
|
|
9505
9749
|
);
|
|
9506
9750
|
}
|
|
9507
9751
|
function parseTransaction(journal, data) {
|
|
@@ -9521,7 +9765,7 @@ async function removeTransactionFiles(journal) {
|
|
|
9521
9765
|
await rm2(transactionMarker(journal), { force: true });
|
|
9522
9766
|
}
|
|
9523
9767
|
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
9524
|
-
const journal =
|
|
9768
|
+
const journal = path9.resolve(recoveryFile);
|
|
9525
9769
|
const bytes = await readOptional(journal);
|
|
9526
9770
|
if (!bytes) {
|
|
9527
9771
|
await rm2(transactionMarker(journal), { force: true });
|
|
@@ -9537,7 +9781,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
9537
9781
|
);
|
|
9538
9782
|
}
|
|
9539
9783
|
for (const entry of transaction.entries) {
|
|
9540
|
-
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(
|
|
9784
|
+
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path9.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
|
|
9541
9785
|
throw new Error(
|
|
9542
9786
|
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
9543
9787
|
);
|
|
@@ -9583,7 +9827,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
9583
9827
|
await removeTransactionFiles(journal);
|
|
9584
9828
|
}
|
|
9585
9829
|
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
9586
|
-
const root =
|
|
9830
|
+
const root = path9.dirname(path9.resolve(manifestPath));
|
|
9587
9831
|
const sources = provenance.sources.map((source) => ({
|
|
9588
9832
|
...source,
|
|
9589
9833
|
path: portableRelative(root, source.path)
|
|
@@ -9613,11 +9857,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
|
9613
9857
|
];
|
|
9614
9858
|
}
|
|
9615
9859
|
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
9616
|
-
const absoluteManifest =
|
|
9860
|
+
const absoluteManifest = path9.resolve(manifestPath);
|
|
9617
9861
|
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
9618
9862
|
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
9619
9863
|
absoluteManifest,
|
|
9620
|
-
...outputs.map((output) =>
|
|
9864
|
+
...outputs.map((output) => path9.resolve(output.path))
|
|
9621
9865
|
]);
|
|
9622
9866
|
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
9623
9867
|
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
@@ -9635,13 +9879,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
9635
9879
|
}
|
|
9636
9880
|
}
|
|
9637
9881
|
if (!options.force) {
|
|
9638
|
-
const root =
|
|
9882
|
+
const root = path9.dirname(absoluteManifest);
|
|
9639
9883
|
const recorded = new Map(
|
|
9640
|
-
previous?.outputs.map((output) => [
|
|
9884
|
+
previous?.outputs.map((output) => [path9.resolve(root, output.path), output.sha256]) ?? []
|
|
9641
9885
|
);
|
|
9642
9886
|
const conflicts = [];
|
|
9643
9887
|
for (const output of outputs) {
|
|
9644
|
-
const destination =
|
|
9888
|
+
const destination = path9.resolve(output.path);
|
|
9645
9889
|
const current = await readOptional(destination);
|
|
9646
9890
|
if (!current || digest(current) === digest(output.data)) continue;
|
|
9647
9891
|
const expected = recorded.get(destination);
|
|
@@ -9663,15 +9907,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
|
|
|
9663
9907
|
);
|
|
9664
9908
|
}
|
|
9665
9909
|
async function verifyArtifactBundle(manifestPath) {
|
|
9666
|
-
const absolute =
|
|
9910
|
+
const absolute = path9.resolve(manifestPath);
|
|
9667
9911
|
const raw = await readArtifactBundleManifest(absolute);
|
|
9668
|
-
const root =
|
|
9912
|
+
const root = path9.dirname(absolute);
|
|
9669
9913
|
const changedSources = [];
|
|
9670
9914
|
const changedOutputs = [];
|
|
9671
9915
|
for (const source of raw.sources) {
|
|
9672
9916
|
let actual = null;
|
|
9673
9917
|
try {
|
|
9674
|
-
actual = digest(await readFile5(
|
|
9918
|
+
actual = digest(await readFile5(path9.resolve(root, source.path)));
|
|
9675
9919
|
} catch (error) {
|
|
9676
9920
|
if (!isCode(error, "ENOENT")) throw error;
|
|
9677
9921
|
}
|
|
@@ -9679,7 +9923,7 @@ async function verifyArtifactBundle(manifestPath) {
|
|
|
9679
9923
|
}
|
|
9680
9924
|
for (const output of raw.outputs) {
|
|
9681
9925
|
try {
|
|
9682
|
-
if (digest(await readFile5(
|
|
9926
|
+
if (digest(await readFile5(path9.resolve(root, output.path))) !== output.sha256) {
|
|
9683
9927
|
changedOutputs.push(output.path);
|
|
9684
9928
|
}
|
|
9685
9929
|
} catch (error) {
|
|
@@ -9735,7 +9979,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
9735
9979
|
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
9736
9980
|
const normalized = files.map((file) => {
|
|
9737
9981
|
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
9738
|
-
return { destination:
|
|
9982
|
+
return { destination: path9.resolve(file.path), data: Buffer.from(file.data) };
|
|
9739
9983
|
});
|
|
9740
9984
|
const destinations = /* @__PURE__ */ new Set();
|
|
9741
9985
|
for (const artifact of normalized) {
|
|
@@ -9765,21 +10009,21 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
9765
10009
|
if (!changed.length) return { changed: [], unchanged };
|
|
9766
10010
|
const token = randomUUID3();
|
|
9767
10011
|
for (const [index, artifact] of changed.entries()) {
|
|
9768
|
-
const basename =
|
|
9769
|
-
artifact.stage =
|
|
9770
|
-
|
|
10012
|
+
const basename = path9.basename(artifact.destination);
|
|
10013
|
+
artifact.stage = path9.join(
|
|
10014
|
+
path9.dirname(artifact.destination),
|
|
9771
10015
|
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
9772
10016
|
);
|
|
9773
10017
|
if (artifact.existed) {
|
|
9774
|
-
artifact.backup =
|
|
9775
|
-
|
|
10018
|
+
artifact.backup = path9.join(
|
|
10019
|
+
path9.dirname(artifact.destination),
|
|
9776
10020
|
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
9777
10021
|
);
|
|
9778
10022
|
}
|
|
9779
10023
|
}
|
|
9780
|
-
const journal = options.recoveryFile ?
|
|
10024
|
+
const journal = options.recoveryFile ? path9.resolve(options.recoveryFile) : null;
|
|
9781
10025
|
if (journal) {
|
|
9782
|
-
await mkdir3(
|
|
10026
|
+
await mkdir3(path9.dirname(journal), { recursive: true });
|
|
9783
10027
|
const transaction = {
|
|
9784
10028
|
format: "pixelkiln-artifact-transaction",
|
|
9785
10029
|
version: 1,
|
|
@@ -9804,7 +10048,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
9804
10048
|
}
|
|
9805
10049
|
try {
|
|
9806
10050
|
for (const [index, artifact] of changed.entries()) {
|
|
9807
|
-
await mkdir3(
|
|
10051
|
+
await mkdir3(path9.dirname(artifact.destination), { recursive: true });
|
|
9808
10052
|
await options.beforeStage?.(artifact.destination, index);
|
|
9809
10053
|
await writeFile3(artifact.stage, artifact.data, { flag: "wx" });
|
|
9810
10054
|
}
|
|
@@ -10070,10 +10314,10 @@ function sameFrameGrid(first, next) {
|
|
|
10070
10314
|
return next.detection.stepX === first.detection.stepX && next.detection.stepY === first.detection.stepY && next.detection.columns === first.detection.columns && next.detection.rows === first.detection.rows && phaseMatches(next.detection.phaseX, first.detection.phaseX, first.detection.stepX) && phaseMatches(next.detection.phaseY, first.detection.phaseY, first.detection.stepY) && next.audit.width === first.audit.width && next.audit.height === first.audit.height && next.sourceWidth === first.sourceWidth && next.sourceHeight === first.sourceHeight;
|
|
10071
10315
|
}
|
|
10072
10316
|
async function refineAsset(options) {
|
|
10073
|
-
const source =
|
|
10074
|
-
const output =
|
|
10317
|
+
const source = path10.resolve(options.source);
|
|
10318
|
+
const output = path10.resolve(options.output);
|
|
10075
10319
|
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
10076
|
-
const record =
|
|
10320
|
+
const record = path10.resolve(qualityRecordPath(output));
|
|
10077
10321
|
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
10078
10322
|
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
10079
10323
|
if (options.minTransparency !== void 0 && (!Number.isFinite(options.minTransparency) || options.minTransparency < 0 || options.minTransparency > 1)) throw new Error("minTransparency must be between 0 and 1.");
|
|
@@ -10096,8 +10340,8 @@ async function refineAsset(options) {
|
|
|
10096
10340
|
);
|
|
10097
10341
|
}
|
|
10098
10342
|
}
|
|
10099
|
-
const temp = await mkdtemp(
|
|
10100
|
-
const recovered =
|
|
10343
|
+
const temp = await mkdtemp(path10.join(tmpdir(), "pixelkiln-refine-"));
|
|
10344
|
+
const recovered = path10.join(temp, "native.png");
|
|
10101
10345
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
10102
10346
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
10103
10347
|
try {
|
|
@@ -10182,18 +10426,18 @@ async function refineFrameSet(options) {
|
|
|
10182
10426
|
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
10183
10427
|
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
10184
10428
|
}
|
|
10185
|
-
const output =
|
|
10186
|
-
const record =
|
|
10429
|
+
const output = path10.resolve(options.output);
|
|
10430
|
+
const record = path10.resolve(qualityRecordPath(output));
|
|
10187
10431
|
const palette = normalizeRefinementPalette(options.palette);
|
|
10188
10432
|
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
10189
10433
|
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
10190
10434
|
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
10191
10435
|
}
|
|
10192
|
-
const sourceSet = new Set(options.sources.map((source) =>
|
|
10436
|
+
const sourceSet = new Set(options.sources.map((source) => path10.resolve(source.path)));
|
|
10193
10437
|
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
10194
10438
|
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
10195
10439
|
}
|
|
10196
|
-
const temp = await mkdtemp(
|
|
10440
|
+
const temp = await mkdtemp(path10.join(tmpdir(), "pixelkiln-refine-frames-"));
|
|
10197
10441
|
try {
|
|
10198
10442
|
const frames = [];
|
|
10199
10443
|
const outputs = [];
|
|
@@ -10201,8 +10445,8 @@ async function refineFrameSet(options) {
|
|
|
10201
10445
|
const sourcePaths = [];
|
|
10202
10446
|
let firstOptions = null;
|
|
10203
10447
|
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
10204
|
-
const source =
|
|
10205
|
-
const temporaryOutput =
|
|
10448
|
+
const source = path10.resolve(sourceEntry.path);
|
|
10449
|
+
const temporaryOutput = path10.join(temp, `${String(index).padStart(2, "0")}.png`);
|
|
10206
10450
|
const refined = await refineAsset({
|
|
10207
10451
|
source,
|
|
10208
10452
|
output: temporaryOutput,
|
|
@@ -10277,7 +10521,7 @@ async function refineFrameSet(options) {
|
|
|
10277
10521
|
);
|
|
10278
10522
|
return {
|
|
10279
10523
|
sources: sourcePaths,
|
|
10280
|
-
outputs: outputs.map((item) =>
|
|
10524
|
+
outputs: outputs.map((item) => path10.resolve(item.path)),
|
|
10281
10525
|
record,
|
|
10282
10526
|
frames,
|
|
10283
10527
|
palette,
|
|
@@ -10288,12 +10532,12 @@ async function refineFrameSet(options) {
|
|
|
10288
10532
|
}
|
|
10289
10533
|
}
|
|
10290
10534
|
async function checkQualityRecord(recordPath) {
|
|
10291
|
-
const record =
|
|
10535
|
+
const record = path10.resolve(recordPath);
|
|
10292
10536
|
const manifest = await readArtifactBundleManifest(record);
|
|
10293
10537
|
const options = parseRecordOptions(manifest, record);
|
|
10294
10538
|
const verification = await verifyArtifactBundle(record);
|
|
10295
|
-
const sources = manifest.sources.map((source2) =>
|
|
10296
|
-
const outputs = manifest.outputs.map((output2) =>
|
|
10539
|
+
const sources = manifest.sources.map((source2) => path10.resolve(path10.dirname(record), source2.path));
|
|
10540
|
+
const outputs = manifest.outputs.map((output2) => path10.resolve(path10.dirname(record), output2.path));
|
|
10297
10541
|
const source = sources[0] ?? "";
|
|
10298
10542
|
const output = outputs[0] ?? "";
|
|
10299
10543
|
const expectedCount = options.frameSet?.count ?? 1;
|
|
@@ -10326,7 +10570,7 @@ async function checkQualityRecord(recordPath) {
|
|
|
10326
10570
|
async function approveQualityRecord(recordPath, approval) {
|
|
10327
10571
|
const reviewer = approval.reviewer.trim();
|
|
10328
10572
|
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
10329
|
-
const record =
|
|
10573
|
+
const record = path10.resolve(recordPath);
|
|
10330
10574
|
const manifest = await readArtifactBundleManifest(record);
|
|
10331
10575
|
const options = parseRecordOptions(manifest, record);
|
|
10332
10576
|
const verification = await verifyArtifactBundle(record);
|
|
@@ -10347,14 +10591,14 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
10347
10591
|
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
10348
10592
|
);
|
|
10349
10593
|
}
|
|
10350
|
-
const root =
|
|
10594
|
+
const root = path10.dirname(record);
|
|
10351
10595
|
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
10352
|
-
const outputPath =
|
|
10596
|
+
const outputPath = path10.resolve(root, entry.path);
|
|
10353
10597
|
return { path: outputPath, data: await readFile6(outputPath) };
|
|
10354
10598
|
}));
|
|
10355
10599
|
const sources = manifest.sources.map((source) => ({
|
|
10356
10600
|
...source,
|
|
10357
|
-
path:
|
|
10601
|
+
path: path10.resolve(root, source.path)
|
|
10358
10602
|
}));
|
|
10359
10603
|
const approved = {
|
|
10360
10604
|
...options,
|
|
@@ -10386,7 +10630,7 @@ async function qualitySource(spec, lock) {
|
|
|
10386
10630
|
if (spec.generator === "frames") {
|
|
10387
10631
|
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
10388
10632
|
}
|
|
10389
|
-
const source =
|
|
10633
|
+
const source = path11.resolve(spec.root, spec.source);
|
|
10390
10634
|
return existsSync6(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
10391
10635
|
}
|
|
10392
10636
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -10453,7 +10697,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
10453
10697
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
10454
10698
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
10455
10699
|
const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
|
|
10456
|
-
const contractChanged = JSON.stringify(check.sources.map((item) =>
|
|
10700
|
+
const contractChanged = JSON.stringify(check.sources.map((item) => path11.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path11.resolve(item))) || JSON.stringify(check.outputs.map((item) => path11.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path11.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
|
|
10457
10701
|
if (contractChanged) {
|
|
10458
10702
|
return {
|
|
10459
10703
|
key,
|
|
@@ -10617,7 +10861,7 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
|
|
|
10617
10861
|
|
|
10618
10862
|
// src/pipeline/revision.ts
|
|
10619
10863
|
import { existsSync as existsSync7 } from "fs";
|
|
10620
|
-
import
|
|
10864
|
+
import path12 from "path";
|
|
10621
10865
|
async function inspectRevisionReadiness(spec, lock) {
|
|
10622
10866
|
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
10623
10867
|
if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
|
|
@@ -10760,8 +11004,8 @@ async function inspectSourceMembers(spec, lock) {
|
|
|
10760
11004
|
if (parent.source) return { ready: true, reason: "declared source set is present" };
|
|
10761
11005
|
const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
|
|
10762
11006
|
if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
|
|
10763
|
-
const recorded = entry.outputs.map((_, index) =>
|
|
10764
|
-
const found = members.map((member) =>
|
|
11007
|
+
const recorded = entry.outputs.map((_, index) => path12.resolve(currentEntryOutputPath(entry, parent, index))).sort();
|
|
11008
|
+
const found = members.map((member) => path12.resolve(member.file)).sort();
|
|
10765
11009
|
if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
|
|
10766
11010
|
return {
|
|
10767
11011
|
ready: false,
|
|
@@ -10810,7 +11054,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
10810
11054
|
};
|
|
10811
11055
|
}
|
|
10812
11056
|
if (spec.source) {
|
|
10813
|
-
const source =
|
|
11057
|
+
const source = path12.resolve(spec.root, spec.source);
|
|
10814
11058
|
return existsSync7(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
|
|
10815
11059
|
}
|
|
10816
11060
|
const entry = lock.entries[key];
|
|
@@ -10889,7 +11133,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
10889
11133
|
state = "missing";
|
|
10890
11134
|
reason = "--force";
|
|
10891
11135
|
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
10892
|
-
if (existsSync8(
|
|
11136
|
+
if (existsSync8(path13.resolve(spec.root, spec.source))) {
|
|
10893
11137
|
state = "ok";
|
|
10894
11138
|
reason = `placed from ${spec.source}; not generated`;
|
|
10895
11139
|
} else {
|
|
@@ -11031,13 +11275,13 @@ function summarize(plan) {
|
|
|
11031
11275
|
|
|
11032
11276
|
// src/project.ts
|
|
11033
11277
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
11034
|
-
const absolute =
|
|
11035
|
-
const root =
|
|
11278
|
+
const absolute = path14.resolve(manifestPath);
|
|
11279
|
+
const root = path14.dirname(absolute);
|
|
11036
11280
|
if (opts.env !== false) {
|
|
11037
11281
|
loadEnvFiles(root);
|
|
11038
|
-
if (
|
|
11282
|
+
if (path14.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
11039
11283
|
}
|
|
11040
|
-
const lockPath =
|
|
11284
|
+
const lockPath = path14.resolve(opts.lockPath ?? path14.join(root, "pixelkiln.lock.json"));
|
|
11041
11285
|
const loaded = await loadManifest(absolute);
|
|
11042
11286
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
11043
11287
|
const lock = await loadLock(lockPath);
|
|
@@ -11104,7 +11348,7 @@ function shouldPersistSourceUrl(value) {
|
|
|
11104
11348
|
}
|
|
11105
11349
|
|
|
11106
11350
|
// src/recipes.ts
|
|
11107
|
-
import
|
|
11351
|
+
import path15 from "path";
|
|
11108
11352
|
import { existsSync as existsSync9 } from "fs";
|
|
11109
11353
|
import { readdir, readFile as readFile7 } from "fs/promises";
|
|
11110
11354
|
import { fileURLToPath } from "url";
|
|
@@ -11115,7 +11359,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
11115
11359
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
11116
11360
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
11117
11361
|
var PortablePathSchema = z4.string().min(1).refine((value) => {
|
|
11118
|
-
if (value.includes("\\") ||
|
|
11362
|
+
if (value.includes("\\") || path15.posix.isAbsolute(value)) return false;
|
|
11119
11363
|
const parts = value.split("/");
|
|
11120
11364
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
11121
11365
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -11291,7 +11535,7 @@ function message3(error) {
|
|
|
11291
11535
|
return error instanceof Error ? error.message : String(error);
|
|
11292
11536
|
}
|
|
11293
11537
|
async function readRecipeFile(recipePath, bundled) {
|
|
11294
|
-
const absolute =
|
|
11538
|
+
const absolute = path15.resolve(recipePath);
|
|
11295
11539
|
let raw;
|
|
11296
11540
|
try {
|
|
11297
11541
|
raw = JSON.parse(await readFile7(absolute, "utf8"));
|
|
@@ -11305,13 +11549,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
11305
11549
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
11306
11550
|
);
|
|
11307
11551
|
}
|
|
11308
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
11552
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path15.dirname(absolute), bundled };
|
|
11309
11553
|
}
|
|
11310
11554
|
async function walkRecipeFiles(root) {
|
|
11311
11555
|
if (!existsSync9(root)) return [];
|
|
11312
11556
|
const out = [];
|
|
11313
11557
|
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
11314
|
-
const resolved =
|
|
11558
|
+
const resolved = path15.join(root, entry.name);
|
|
11315
11559
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
11316
11560
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
11317
11561
|
}
|
|
@@ -11322,7 +11566,7 @@ function bundledRecipeRoot() {
|
|
|
11322
11566
|
}
|
|
11323
11567
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
11324
11568
|
const recipes = await Promise.all(
|
|
11325
|
-
(await walkRecipeFiles(
|
|
11569
|
+
(await walkRecipeFiles(path15.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
11326
11570
|
);
|
|
11327
11571
|
return recipes.sort(
|
|
11328
11572
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -11345,9 +11589,9 @@ function parseSelector(selector) {
|
|
|
11345
11589
|
return { id: match[1], version: match[2] };
|
|
11346
11590
|
}
|
|
11347
11591
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
11348
|
-
const local =
|
|
11592
|
+
const local = path15.resolve(target);
|
|
11349
11593
|
if (existsSync9(local)) {
|
|
11350
|
-
const recipePath =
|
|
11594
|
+
const recipePath = path15.basename(local) === RECIPE_FILE ? local : path15.join(local, RECIPE_FILE);
|
|
11351
11595
|
return readRecipeFile(recipePath, false);
|
|
11352
11596
|
}
|
|
11353
11597
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -11361,7 +11605,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
11361
11605
|
return matches[0];
|
|
11362
11606
|
}
|
|
11363
11607
|
async function verifyFile(root, file) {
|
|
11364
|
-
const absolute =
|
|
11608
|
+
const absolute = path15.resolve(root, ...file.path.split("/"));
|
|
11365
11609
|
if (!existsSync9(absolute)) {
|
|
11366
11610
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
11367
11611
|
}
|
|
@@ -11377,7 +11621,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
11377
11621
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
11378
11622
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
11379
11623
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
11380
|
-
const modelRoot = options.modelRoot ?
|
|
11624
|
+
const modelRoot = options.modelRoot ? path15.resolve(options.modelRoot) : null;
|
|
11381
11625
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
11382
11626
|
...await verifyFile(modelRoot, model),
|
|
11383
11627
|
source: model.source,
|
|
@@ -11411,7 +11655,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
11411
11655
|
};
|
|
11412
11656
|
}
|
|
11413
11657
|
function renderStyle(style, destination, cwd) {
|
|
11414
|
-
const portableDir =
|
|
11658
|
+
const portableDir = path15.relative(cwd, destination).split(path15.sep).join("/") || ".";
|
|
11415
11659
|
const replace = (value) => {
|
|
11416
11660
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
11417
11661
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -11423,19 +11667,19 @@ function renderStyle(style, destination, cwd) {
|
|
|
11423
11667
|
return replace(style);
|
|
11424
11668
|
}
|
|
11425
11669
|
async function installRecipe(target, options = {}) {
|
|
11426
|
-
const cwd =
|
|
11670
|
+
const cwd = path15.resolve(options.cwd ?? process.cwd());
|
|
11427
11671
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
11428
11672
|
const verification = await verifyRecipe(loaded.path);
|
|
11429
11673
|
if (!verification.ok) {
|
|
11430
11674
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
11431
11675
|
}
|
|
11432
|
-
const destination = options.out ?
|
|
11676
|
+
const destination = options.out ? path15.resolve(cwd, options.out) : path15.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
11433
11677
|
const sources = [
|
|
11434
11678
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
11435
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
11679
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path15.join(loaded.dir, ...file.path.split("/")) }))
|
|
11436
11680
|
];
|
|
11437
11681
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
11438
|
-
path:
|
|
11682
|
+
path: path15.join(destination, ...file.path.split("/")),
|
|
11439
11683
|
data: await readFile7(file.source)
|
|
11440
11684
|
})));
|
|
11441
11685
|
if (!options.force) {
|
|
@@ -11467,7 +11711,7 @@ async function installRecipe(target, options = {}) {
|
|
|
11467
11711
|
// src/pipeline/audit.ts
|
|
11468
11712
|
import { readFile as readFile8 } from "fs/promises";
|
|
11469
11713
|
import { existsSync as existsSync10 } from "fs";
|
|
11470
|
-
import
|
|
11714
|
+
import path16 from "path";
|
|
11471
11715
|
function paletteDistance(asset, reference) {
|
|
11472
11716
|
if (!asset.length || !reference.length) return 0;
|
|
11473
11717
|
let total = 0;
|
|
@@ -11532,7 +11776,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
11532
11776
|
let referenceFromStyleImages = false;
|
|
11533
11777
|
const refPalettes = [];
|
|
11534
11778
|
for (const img of style.styleImages) {
|
|
11535
|
-
const abs =
|
|
11779
|
+
const abs = path16.resolve(loaded.root, img.path);
|
|
11536
11780
|
if (!existsSync10(abs)) continue;
|
|
11537
11781
|
try {
|
|
11538
11782
|
refPalettes.push(extractPalette(decodePng(await readFile8(abs)), 12));
|
|
@@ -11613,16 +11857,16 @@ function hex(c) {
|
|
|
11613
11857
|
// src/pixellab-utils.ts
|
|
11614
11858
|
import { existsSync as existsSync11 } from "fs";
|
|
11615
11859
|
import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
11616
|
-
import
|
|
11860
|
+
import path17 from "path";
|
|
11617
11861
|
var UNZOOM_MIN_SIDE = 256;
|
|
11618
11862
|
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
11619
11863
|
function defaultUnzoomOut(input) {
|
|
11620
|
-
const ext =
|
|
11864
|
+
const ext = path17.extname(input);
|
|
11621
11865
|
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
11622
11866
|
}
|
|
11623
11867
|
async function unzoomFile(client2, input, opts = {}) {
|
|
11624
|
-
const out =
|
|
11625
|
-
if (
|
|
11868
|
+
const out = path17.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
11869
|
+
if (path17.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
11626
11870
|
if (existsSync11(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
11627
11871
|
const bytes = await readFile9(input);
|
|
11628
11872
|
const metadata = imageMetadata(bytes);
|
|
@@ -11637,7 +11881,7 @@ async function unzoomFile(client2, input, opts = {}) {
|
|
|
11637
11881
|
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
11638
11882
|
}
|
|
11639
11883
|
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
11640
|
-
await mkdir4(
|
|
11884
|
+
await mkdir4(path17.dirname(out), { recursive: true });
|
|
11641
11885
|
await writeFile4(out, res.png);
|
|
11642
11886
|
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
11643
11887
|
}
|
|
@@ -11645,7 +11889,7 @@ var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
|
11645
11889
|
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
11646
11890
|
var FONT_COST_GENERATIONS = 25;
|
|
11647
11891
|
function fontOutputPaths(out) {
|
|
11648
|
-
const base =
|
|
11892
|
+
const base = path17.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
11649
11893
|
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
11650
11894
|
}
|
|
11651
11895
|
async function generateFont(client2, opts) {
|
|
@@ -11679,7 +11923,7 @@ async function generateFont(client2, opts) {
|
|
|
11679
11923
|
client2.download(job.download_ttf_url),
|
|
11680
11924
|
client2.download(job.download_atlas_url)
|
|
11681
11925
|
]);
|
|
11682
|
-
await mkdir4(
|
|
11926
|
+
await mkdir4(path17.dirname(ttf), { recursive: true });
|
|
11683
11927
|
await writeFile4(ttf, ttfBytes);
|
|
11684
11928
|
await writeFile4(atlas, atlasBytes);
|
|
11685
11929
|
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
@@ -11694,7 +11938,7 @@ async function generateFont(client2, opts) {
|
|
|
11694
11938
|
}
|
|
11695
11939
|
|
|
11696
11940
|
// src/pipeline/quality-regression.ts
|
|
11697
|
-
import
|
|
11941
|
+
import path18 from "path";
|
|
11698
11942
|
import { existsSync as existsSync12 } from "fs";
|
|
11699
11943
|
import { readFile as readFile10 } from "fs/promises";
|
|
11700
11944
|
import { z as z5 } from "zod";
|
|
@@ -11702,7 +11946,7 @@ var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
|
11702
11946
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
11703
11947
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
11704
11948
|
var QualityPathSchema = z5.string().min(1).refine(
|
|
11705
|
-
(value) => !
|
|
11949
|
+
(value) => !path18.posix.isAbsolute(value) && !value.includes("\\"),
|
|
11706
11950
|
"expected a portable relative path"
|
|
11707
11951
|
);
|
|
11708
11952
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -11849,7 +12093,7 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
11849
12093
|
};
|
|
11850
12094
|
}
|
|
11851
12095
|
async function measureImageQuality(file) {
|
|
11852
|
-
const absolute =
|
|
12096
|
+
const absolute = path18.resolve(file);
|
|
11853
12097
|
let bytes;
|
|
11854
12098
|
try {
|
|
11855
12099
|
bytes = await readFile10(absolute);
|
|
@@ -11871,11 +12115,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
11871
12115
|
}
|
|
11872
12116
|
const ids = parsed.data.map((entry) => entry.id);
|
|
11873
12117
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
11874
|
-
const root =
|
|
12118
|
+
const root = path18.dirname(path18.resolve(inputsFilePath));
|
|
11875
12119
|
return parsed.data.map((entry) => ({
|
|
11876
12120
|
id: entry.id,
|
|
11877
|
-
file:
|
|
11878
|
-
...entry.record ? { record:
|
|
12121
|
+
file: path18.resolve(root, entry.path),
|
|
12122
|
+
...entry.record ? { record: path18.resolve(root, entry.record) } : {},
|
|
11879
12123
|
tolerances: QualityToleranceSchema.parse({
|
|
11880
12124
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
11881
12125
|
...entry.tolerances
|
|
@@ -11883,7 +12127,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
11883
12127
|
}));
|
|
11884
12128
|
}
|
|
11885
12129
|
function portableRelative2(from, to) {
|
|
11886
|
-
return
|
|
12130
|
+
return path18.relative(from, path18.resolve(to)).split(path18.sep).join("/") || ".";
|
|
11887
12131
|
}
|
|
11888
12132
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
11889
12133
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -11898,20 +12142,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
11898
12142
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
11899
12143
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
11900
12144
|
}
|
|
11901
|
-
const outputs = record.outputs.map((output) =>
|
|
11902
|
-
if (!outputs.includes(
|
|
12145
|
+
const outputs = record.outputs.map((output) => path18.resolve(path18.dirname(recordPath), output.path));
|
|
12146
|
+
if (!outputs.includes(path18.resolve(imagePath))) {
|
|
11903
12147
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
11904
12148
|
}
|
|
11905
12149
|
}
|
|
11906
12150
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
11907
12151
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
11908
|
-
const absolute =
|
|
12152
|
+
const absolute = path18.resolve(baselinePath);
|
|
11909
12153
|
for (const input of inputs) {
|
|
11910
|
-
if (
|
|
12154
|
+
if (path18.resolve(input.file) === absolute || input.record && path18.resolve(input.record) === absolute) {
|
|
11911
12155
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
11912
12156
|
}
|
|
11913
12157
|
}
|
|
11914
|
-
const root =
|
|
12158
|
+
const root = path18.dirname(absolute);
|
|
11915
12159
|
const cases = [];
|
|
11916
12160
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
11917
12161
|
const expected = await measureImageQuality(input.file);
|
|
@@ -11951,7 +12195,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
11951
12195
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
11952
12196
|
}
|
|
11953
12197
|
async function readQualityBaseline(baselinePath) {
|
|
11954
|
-
const absolute =
|
|
12198
|
+
const absolute = path18.resolve(baselinePath);
|
|
11955
12199
|
let raw;
|
|
11956
12200
|
try {
|
|
11957
12201
|
raw = JSON.parse(await readFile10(absolute, "utf8"));
|
|
@@ -12024,12 +12268,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
12024
12268
|
return { violations, warnings };
|
|
12025
12269
|
}
|
|
12026
12270
|
async function checkQualityBaseline(baselinePath) {
|
|
12027
|
-
const absolute =
|
|
12271
|
+
const absolute = path18.resolve(baselinePath);
|
|
12028
12272
|
const baseline = await readQualityBaseline(absolute);
|
|
12029
|
-
const root =
|
|
12273
|
+
const root = path18.dirname(absolute);
|
|
12030
12274
|
const cases = [];
|
|
12031
12275
|
for (const entry of baseline.cases) {
|
|
12032
|
-
const file =
|
|
12276
|
+
const file = path18.resolve(root, entry.file);
|
|
12033
12277
|
let actual = null;
|
|
12034
12278
|
const violations = [];
|
|
12035
12279
|
const warnings = [];
|
|
@@ -12042,7 +12286,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
12042
12286
|
violations.push(message4(error));
|
|
12043
12287
|
}
|
|
12044
12288
|
if (entry.record) {
|
|
12045
|
-
const recordPath =
|
|
12289
|
+
const recordPath = path18.resolve(root, entry.record.path);
|
|
12046
12290
|
try {
|
|
12047
12291
|
const recordHash = await sha256File(recordPath);
|
|
12048
12292
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -12080,7 +12324,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
12080
12324
|
// src/pipeline/cache-health.ts
|
|
12081
12325
|
import { existsSync as existsSync16 } from "fs";
|
|
12082
12326
|
import { readFile as readFile13, readdir as readdir2, rm as rm6 } from "fs/promises";
|
|
12083
|
-
import
|
|
12327
|
+
import path21 from "path";
|
|
12084
12328
|
|
|
12085
12329
|
// src/cache.ts
|
|
12086
12330
|
import { mkdir as mkdir5, readFile as readFile11, rename as rename4, rm as rm4, writeFile as writeFile5 } from "fs/promises";
|
|
@@ -12099,10 +12343,10 @@ function isSha256Hash(value) {
|
|
|
12099
12343
|
function parseCache(value) {
|
|
12100
12344
|
return HashCacheSchema.parse(value);
|
|
12101
12345
|
}
|
|
12102
|
-
async function loadCache(
|
|
12103
|
-
if (!existsSync13(
|
|
12346
|
+
async function loadCache(path38) {
|
|
12347
|
+
if (!existsSync13(path38)) return { version: 1, hashes: {} };
|
|
12104
12348
|
try {
|
|
12105
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(
|
|
12349
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(path38, "utf8")));
|
|
12106
12350
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
12107
12351
|
return {
|
|
12108
12352
|
version: 1,
|
|
@@ -12114,18 +12358,18 @@ async function loadCache(path36) {
|
|
|
12114
12358
|
return { version: 1, hashes: {} };
|
|
12115
12359
|
}
|
|
12116
12360
|
}
|
|
12117
|
-
async function saveCache(
|
|
12361
|
+
async function saveCache(path38, cache) {
|
|
12118
12362
|
const sorted = {};
|
|
12119
12363
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
12120
12364
|
const hash = cache.hashes[key];
|
|
12121
12365
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
12122
12366
|
sorted[key] = hash;
|
|
12123
12367
|
}
|
|
12124
|
-
await mkdir5(pathModule.dirname(pathModule.resolve(
|
|
12125
|
-
const tmp = `${
|
|
12368
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path38)), { recursive: true });
|
|
12369
|
+
const tmp = `${path38}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
12126
12370
|
try {
|
|
12127
12371
|
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
12128
|
-
await rename4(tmp,
|
|
12372
|
+
await rename4(tmp, path38);
|
|
12129
12373
|
} finally {
|
|
12130
12374
|
await rm4(tmp, { force: true });
|
|
12131
12375
|
}
|
|
@@ -12150,19 +12394,19 @@ function cachePathFor(lockPath) {
|
|
|
12150
12394
|
|
|
12151
12395
|
// src/pipeline/history.ts
|
|
12152
12396
|
import { existsSync as existsSync15 } from "fs";
|
|
12153
|
-
import
|
|
12397
|
+
import path20 from "path";
|
|
12154
12398
|
|
|
12155
12399
|
// src/pipeline/fetch.ts
|
|
12156
|
-
import { existsSync as existsSync14, readFileSync as
|
|
12400
|
+
import { existsSync as existsSync14, readFileSync as readFileSync4 } from "fs";
|
|
12157
12401
|
import { mkdir as mkdir6, readFile as readFile12, rename as rename5, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
12158
|
-
import
|
|
12402
|
+
import path19 from "path";
|
|
12159
12403
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
12160
12404
|
const log = opts.onProgress ?? (() => {
|
|
12161
12405
|
});
|
|
12162
12406
|
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
12163
12407
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
12164
12408
|
normalizeLockOutputPaths(lock, specs);
|
|
12165
|
-
const cacheDir = opts.cacheDir === false ? null :
|
|
12409
|
+
const cacheDir = opts.cacheDir === false ? null : path19.resolve(opts.cacheDir ?? path19.join(path19.dirname(lockPath), ".pixelkiln", "cache"));
|
|
12166
12410
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
12167
12411
|
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
12168
12412
|
if (specByKey.get(key)?.mirror) return false;
|
|
@@ -12173,7 +12417,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12173
12417
|
if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
|
|
12174
12418
|
return Boolean(opts.force) || e.outputs.every((output) => {
|
|
12175
12419
|
const file = resolveOutputPath(output.path, spec.root);
|
|
12176
|
-
return existsSync14(file) && sha256(
|
|
12420
|
+
return existsSync14(file) && sha256(readFileSync4(file)) === output.sha256;
|
|
12177
12421
|
});
|
|
12178
12422
|
}
|
|
12179
12423
|
if (!opts.repair) return false;
|
|
@@ -12181,7 +12425,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12181
12425
|
return e.outputs.some((output) => {
|
|
12182
12426
|
const file = resolveOutputPath(output.path, spec.root);
|
|
12183
12427
|
if (!existsSync14(file)) return true;
|
|
12184
|
-
const current = sha256(
|
|
12428
|
+
const current = sha256(readFileSync4(file));
|
|
12185
12429
|
if (current === output.sha256) return false;
|
|
12186
12430
|
return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
|
|
12187
12431
|
});
|
|
@@ -12264,7 +12508,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12264
12508
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
12265
12509
|
fresh = await provider.download(source.url);
|
|
12266
12510
|
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
12267
|
-
log(` current ${
|
|
12511
|
+
log(` current ${path19.relative(process.cwd(), target)}`);
|
|
12268
12512
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
12269
12513
|
continue;
|
|
12270
12514
|
}
|
|
@@ -12288,9 +12532,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12288
12532
|
}
|
|
12289
12533
|
if (reapply && !fresh) {
|
|
12290
12534
|
if (!recorded.raw) raw = await readFile12(target);
|
|
12291
|
-
log(` reapply ${
|
|
12535
|
+
log(` reapply ${path19.relative(process.cwd(), target)}`);
|
|
12292
12536
|
} else {
|
|
12293
|
-
log(` changed ${
|
|
12537
|
+
log(` changed ${path19.relative(process.cwd(), target)} (upstream)`);
|
|
12294
12538
|
}
|
|
12295
12539
|
} else {
|
|
12296
12540
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
@@ -12318,7 +12562,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12318
12562
|
}
|
|
12319
12563
|
}
|
|
12320
12564
|
log(
|
|
12321
|
-
` replace ${
|
|
12565
|
+
` replace ${path19.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
12322
12566
|
);
|
|
12323
12567
|
}
|
|
12324
12568
|
}
|
|
@@ -12327,7 +12571,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12327
12571
|
} else if (!raw && recorded && cacheDir) {
|
|
12328
12572
|
if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
|
|
12329
12573
|
if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
|
|
12330
|
-
if (final || raw) log(` cached ${
|
|
12574
|
+
if (final || raw) log(` cached ${path19.relative(process.cwd(), target)}`);
|
|
12331
12575
|
}
|
|
12332
12576
|
if (!final && !raw) {
|
|
12333
12577
|
if (!source.url) {
|
|
@@ -12354,11 +12598,11 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12354
12598
|
buf = processed.bytes;
|
|
12355
12599
|
if (processed.changed) {
|
|
12356
12600
|
rawHash = sha256(raw);
|
|
12357
|
-
log(` snapped ${
|
|
12601
|
+
log(` snapped ${path19.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
|
|
12358
12602
|
}
|
|
12359
12603
|
}
|
|
12360
12604
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
12361
|
-
await mkdir6(
|
|
12605
|
+
await mkdir6(path19.dirname(target), { recursive: true });
|
|
12362
12606
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
12363
12607
|
await writeFile6(tmp, buf);
|
|
12364
12608
|
outputs.push({
|
|
@@ -12372,7 +12616,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12372
12616
|
await saveLock(lockPath, lock);
|
|
12373
12617
|
await rename5(tmp, target);
|
|
12374
12618
|
wrote = true;
|
|
12375
|
-
log(` wrote ${
|
|
12619
|
+
log(` wrote ${path19.relative(process.cwd(), target)}`);
|
|
12376
12620
|
}
|
|
12377
12621
|
if (opts.refresh && !wrote) {
|
|
12378
12622
|
if (rotated) {
|
|
@@ -12416,7 +12660,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12416
12660
|
return result;
|
|
12417
12661
|
}
|
|
12418
12662
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
12419
|
-
const file =
|
|
12663
|
+
const file = path19.join(cacheDir, cacheFileName(hash, mediaType));
|
|
12420
12664
|
if (!existsSync14(file)) return null;
|
|
12421
12665
|
try {
|
|
12422
12666
|
const buf = await readFile12(file);
|
|
@@ -12430,7 +12674,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
12430
12674
|
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
12431
12675
|
const hash = knownHash ?? sha256(buf);
|
|
12432
12676
|
validateMedia(buf, mediaType);
|
|
12433
|
-
const file =
|
|
12677
|
+
const file = path19.join(cacheDir, cacheFileName(hash, mediaType));
|
|
12434
12678
|
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
12435
12679
|
await mkdir6(cacheDir, { recursive: true });
|
|
12436
12680
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
@@ -12560,7 +12804,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
12560
12804
|
if (!existsSync15(file)) continue;
|
|
12561
12805
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
12562
12806
|
throw new OverwriteRefusedError(
|
|
12563
|
-
`${
|
|
12807
|
+
`${path20.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
12564
12808
|
[file]
|
|
12565
12809
|
);
|
|
12566
12810
|
}
|
|
@@ -12613,10 +12857,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
12613
12857
|
// src/pipeline/cache-health.ts
|
|
12614
12858
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
12615
12859
|
if (options.prune && !existsSync16(lockPath)) {
|
|
12616
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
12860
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path21.resolve(lockPath)}`);
|
|
12617
12861
|
}
|
|
12618
|
-
const contentDir =
|
|
12619
|
-
const remotePath =
|
|
12862
|
+
const contentDir = path21.resolve(path21.dirname(lockPath), ".pixelkiln", "cache");
|
|
12863
|
+
const remotePath = path21.resolve(cachePathFor(lockPath));
|
|
12620
12864
|
const referenced = referencedHashes(lock);
|
|
12621
12865
|
let content = await inspectContentCache(contentDir, referenced);
|
|
12622
12866
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -12628,7 +12872,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
12628
12872
|
]);
|
|
12629
12873
|
for (const name of names) {
|
|
12630
12874
|
try {
|
|
12631
|
-
await rm6(
|
|
12875
|
+
await rm6(path21.join(contentDir, name), { force: true });
|
|
12632
12876
|
removed.contentFiles++;
|
|
12633
12877
|
} catch {
|
|
12634
12878
|
}
|
|
@@ -12676,7 +12920,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
12676
12920
|
continue;
|
|
12677
12921
|
}
|
|
12678
12922
|
report.files++;
|
|
12679
|
-
const file =
|
|
12923
|
+
const file = path21.join(contentDir, entry.name);
|
|
12680
12924
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
12681
12925
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
12682
12926
|
let bytes;
|
|
@@ -12743,20 +12987,20 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
12743
12987
|
}
|
|
12744
12988
|
|
|
12745
12989
|
// src/pipeline/pack.ts
|
|
12746
|
-
import { readFileSync as
|
|
12747
|
-
import
|
|
12990
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
12991
|
+
import path22 from "path";
|
|
12748
12992
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
12749
12993
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
12750
12994
|
if (!Array.isArray(raw) || !raw.length) {
|
|
12751
12995
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
12752
12996
|
}
|
|
12753
|
-
const baseDir =
|
|
12997
|
+
const baseDir = path22.dirname(path22.resolve(inputsFilePath));
|
|
12754
12998
|
return raw.map((entry, i) => {
|
|
12755
12999
|
const e = entry;
|
|
12756
13000
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
12757
13001
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
12758
13002
|
}
|
|
12759
|
-
return { id: e.id, path:
|
|
13003
|
+
return { id: e.id, path: path22.resolve(baseDir, e.path) };
|
|
12760
13004
|
});
|
|
12761
13005
|
}
|
|
12762
13006
|
function packSprites(inputs, options = {}) {
|
|
@@ -12777,7 +13021,7 @@ function packSprites(inputs, options = {}) {
|
|
|
12777
13021
|
for (const input of ordered) {
|
|
12778
13022
|
let sourceHash = null;
|
|
12779
13023
|
try {
|
|
12780
|
-
const bytes =
|
|
13024
|
+
const bytes = readFileSync5(input.path);
|
|
12781
13025
|
sourceHash = sha256(bytes);
|
|
12782
13026
|
const png = decodePng(bytes);
|
|
12783
13027
|
sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
|
|
@@ -12840,7 +13084,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
12840
13084
|
}
|
|
12841
13085
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
12842
13086
|
id,
|
|
12843
|
-
path:
|
|
13087
|
+
path: path22.resolve(source)
|
|
12844
13088
|
}));
|
|
12845
13089
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
12846
13090
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -12864,7 +13108,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
12864
13108
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
12865
13109
|
const source = sources[id];
|
|
12866
13110
|
if (source && outputs.length <= 1) {
|
|
12867
|
-
inputs.push({ id, path:
|
|
13111
|
+
inputs.push({ id, path: path22.resolve(manifestDir, source) });
|
|
12868
13112
|
continue;
|
|
12869
13113
|
}
|
|
12870
13114
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -12898,7 +13142,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
12898
13142
|
}
|
|
12899
13143
|
}
|
|
12900
13144
|
for (const [id, source] of Object.entries(sources)) {
|
|
12901
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
13145
|
+
if (!locked.has(id)) inputs.push({ id, path: path22.resolve(manifestDir, source) });
|
|
12902
13146
|
}
|
|
12903
13147
|
if (!inputs.length) {
|
|
12904
13148
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -12935,7 +13179,7 @@ function mountSprites(placements, options) {
|
|
|
12935
13179
|
for (const p of placements) {
|
|
12936
13180
|
let sourceHash = null;
|
|
12937
13181
|
try {
|
|
12938
|
-
const bytes =
|
|
13182
|
+
const bytes = readFileSync5(p.path);
|
|
12939
13183
|
sourceHash = sha256(bytes);
|
|
12940
13184
|
const png = decodePng(bytes);
|
|
12941
13185
|
if (png.width > cellWidth || png.height > cellHeight) {
|
|
@@ -13006,7 +13250,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
13006
13250
|
const source = sources[id];
|
|
13007
13251
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
13008
13252
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
13009
|
-
placements.push({ id, path:
|
|
13253
|
+
placements.push({ id, path: path22.resolve(manifestDir, source), cell });
|
|
13010
13254
|
continue;
|
|
13011
13255
|
}
|
|
13012
13256
|
if (source && entry) {
|
|
@@ -13039,7 +13283,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
13039
13283
|
`Nothing to mount for style "${styleId}": ${ids.length} asset(s) declare a cell, but none has generated output or a \`source\`. Run \`pixelkiln gen --style ${styleId}\`, or point each at a file.${detail}`
|
|
13040
13284
|
);
|
|
13041
13285
|
}
|
|
13042
|
-
const basePng = mount.base ?
|
|
13286
|
+
const basePng = mount.base ? readFileSync5(path22.resolve(manifestDir, mount.base)) : void 0;
|
|
13043
13287
|
const mounted = mountSprites(placements, {
|
|
13044
13288
|
cellWidth: mount.cellWidth,
|
|
13045
13289
|
cellHeight: mount.cellHeight,
|
|
@@ -13049,7 +13293,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
13049
13293
|
...mounted,
|
|
13050
13294
|
sources: mount.base ? [{
|
|
13051
13295
|
id: "$base",
|
|
13052
|
-
path:
|
|
13296
|
+
path: path22.resolve(manifestDir, mount.base),
|
|
13053
13297
|
sha256: sha256(basePng),
|
|
13054
13298
|
included: true
|
|
13055
13299
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -13641,6 +13885,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
13641
13885
|
...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
|
|
13642
13886
|
...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
|
|
13643
13887
|
...spec.revision.description ? { description: spec.revision.description } : {},
|
|
13888
|
+
...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
|
|
13889
|
+
...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
|
|
13890
|
+
...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
|
|
13891
|
+
...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
|
|
13644
13892
|
...spec.revision.engine ? { engine: spec.revision.engine } : {}
|
|
13645
13893
|
} : null,
|
|
13646
13894
|
outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
|
|
@@ -13857,7 +14105,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
13857
14105
|
// src/pipeline/doctor.ts
|
|
13858
14106
|
import { constants, existsSync as existsSync17 } from "fs";
|
|
13859
14107
|
import { access, stat as stat2 } from "fs/promises";
|
|
13860
|
-
import
|
|
14108
|
+
import path23 from "path";
|
|
13861
14109
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
13862
14110
|
const checks = [];
|
|
13863
14111
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -13868,8 +14116,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13868
14116
|
);
|
|
13869
14117
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
13870
14118
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
13871
|
-
|
|
13872
|
-
...spec.quality ? [
|
|
14119
|
+
path23.dirname(spec.outFile),
|
|
14120
|
+
...spec.quality ? [path23.dirname(spec.quality.outFile)] : []
|
|
13873
14121
|
]))];
|
|
13874
14122
|
const unwritable = [];
|
|
13875
14123
|
for (const dir of dirs) {
|
|
@@ -13892,7 +14140,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13892
14140
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
13893
14141
|
const output = entry.outputs[index];
|
|
13894
14142
|
const spec = specByKey.get(key);
|
|
13895
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
14143
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path23.dirname(lockPath));
|
|
13896
14144
|
const owner = outputOwners.get(absolute);
|
|
13897
14145
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
13898
14146
|
outputOwners.set(absolute, key);
|
|
@@ -13907,7 +14155,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13907
14155
|
} else {
|
|
13908
14156
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
13909
14157
|
}
|
|
13910
|
-
const cacheDir =
|
|
14158
|
+
const cacheDir = path23.join(path23.dirname(lockPath), ".pixelkiln", "cache");
|
|
13911
14159
|
const stranded = [];
|
|
13912
14160
|
const invalidState = [];
|
|
13913
14161
|
const staleMapUrls = [];
|
|
@@ -13923,7 +14171,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
13923
14171
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
13924
14172
|
let hasCache = false;
|
|
13925
14173
|
for (const output of entry.outputs) {
|
|
13926
|
-
const cached =
|
|
14174
|
+
const cached = path23.join(
|
|
13927
14175
|
cacheDir,
|
|
13928
14176
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
13929
14177
|
);
|
|
@@ -14023,7 +14271,7 @@ async function nearestExistingDirectory(start) {
|
|
|
14023
14271
|
if ((await stat2(current)).isDirectory()) return current;
|
|
14024
14272
|
} catch {
|
|
14025
14273
|
}
|
|
14026
|
-
const parent =
|
|
14274
|
+
const parent = path23.dirname(current);
|
|
14027
14275
|
if (parent === current) return current;
|
|
14028
14276
|
current = parent;
|
|
14029
14277
|
}
|
|
@@ -14032,7 +14280,7 @@ async function nearestExistingDirectory(start) {
|
|
|
14032
14280
|
// src/pipeline/adopt.ts
|
|
14033
14281
|
import { existsSync as existsSync18 } from "fs";
|
|
14034
14282
|
import { readFile as readFile14, writeFile as writeFile7 } from "fs/promises";
|
|
14035
|
-
import
|
|
14283
|
+
import path24 from "path";
|
|
14036
14284
|
async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
14037
14285
|
const log = opts.onProgress ?? (() => {
|
|
14038
14286
|
});
|
|
@@ -14171,7 +14419,7 @@ import { readFile as readFile17 } from "fs/promises";
|
|
|
14171
14419
|
// src/pipeline/init.ts
|
|
14172
14420
|
import { readdir as readdir3, readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
|
|
14173
14421
|
import { existsSync as existsSync19 } from "fs";
|
|
14174
|
-
import
|
|
14422
|
+
import path25 from "path";
|
|
14175
14423
|
function pngSize(buf) {
|
|
14176
14424
|
if (buf.length < 24) return null;
|
|
14177
14425
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -14185,7 +14433,7 @@ function slugify(value) {
|
|
|
14185
14433
|
async function walk(dir, exclude) {
|
|
14186
14434
|
const out = [];
|
|
14187
14435
|
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
14188
|
-
const full =
|
|
14436
|
+
const full = path25.join(dir, entry.name);
|
|
14189
14437
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
14190
14438
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
14191
14439
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -14204,9 +14452,9 @@ async function scanAssets(root, opts = {}) {
|
|
|
14204
14452
|
skipped.push(`${file} (not a readable PNG)`);
|
|
14205
14453
|
continue;
|
|
14206
14454
|
}
|
|
14207
|
-
const rel =
|
|
14208
|
-
const category =
|
|
14209
|
-
let id = slugify(
|
|
14455
|
+
const rel = path25.relative(root, file);
|
|
14456
|
+
const category = path25.dirname(rel) === "." ? "" : path25.dirname(rel);
|
|
14457
|
+
let id = slugify(path25.basename(rel));
|
|
14210
14458
|
if (seen.has(id)) {
|
|
14211
14459
|
const n = seen.get(id) + 1;
|
|
14212
14460
|
seen.set(id, n);
|
|
@@ -14929,15 +15177,15 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
14929
15177
|
}
|
|
14930
15178
|
|
|
14931
15179
|
// src/gallery/snapshot.ts
|
|
14932
|
-
import { createHash as
|
|
14933
|
-
import { existsSync as
|
|
14934
|
-
import { readFile as
|
|
14935
|
-
import
|
|
15180
|
+
import { createHash as createHash5 } from "crypto";
|
|
15181
|
+
import { existsSync as existsSync26 } from "fs";
|
|
15182
|
+
import { readFile as readFile24, stat as stat3 } from "fs/promises";
|
|
15183
|
+
import path33 from "path";
|
|
14936
15184
|
|
|
14937
15185
|
// src/workspace.ts
|
|
14938
15186
|
import { mkdir as mkdir7, readFile as readFile18, rename as rename6, rm as rm7, writeFile as writeFile9 } from "fs/promises";
|
|
14939
15187
|
import { existsSync as existsSync21 } from "fs";
|
|
14940
|
-
import
|
|
15188
|
+
import path26 from "path";
|
|
14941
15189
|
import { z as z7 } from "zod";
|
|
14942
15190
|
var WorkspaceProjectSchema = z7.object({
|
|
14943
15191
|
id: z7.string().min(1),
|
|
@@ -14979,7 +15227,7 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
14979
15227
|
version: 1,
|
|
14980
15228
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
14981
15229
|
};
|
|
14982
|
-
await mkdir7(
|
|
15230
|
+
await mkdir7(path26.dirname(path26.resolve(workspacePath)), { recursive: true });
|
|
14983
15231
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
14984
15232
|
try {
|
|
14985
15233
|
await writeFile9(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
@@ -14989,12 +15237,12 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
14989
15237
|
}
|
|
14990
15238
|
}
|
|
14991
15239
|
function toPortablePath(dir, absolute) {
|
|
14992
|
-
return
|
|
15240
|
+
return path26.relative(dir, absolute).split(path26.sep).join("/");
|
|
14993
15241
|
}
|
|
14994
15242
|
function resolveProject(dir, project) {
|
|
14995
15243
|
return {
|
|
14996
|
-
manifestPath:
|
|
14997
|
-
lockPath:
|
|
15244
|
+
manifestPath: path26.resolve(dir, project.manifest.split("/").join(path26.sep)),
|
|
15245
|
+
lockPath: path26.resolve(dir, project.lock.split("/").join(path26.sep))
|
|
14998
15246
|
};
|
|
14999
15247
|
}
|
|
15000
15248
|
function validateWorkspace(ws, dir) {
|
|
@@ -15007,7 +15255,7 @@ function validateWorkspace(ws, dir) {
|
|
|
15007
15255
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
15008
15256
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
15009
15257
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
15010
|
-
if (
|
|
15258
|
+
if (path26.isAbsolute(project.manifest) || path26.isAbsolute(project.lock)) {
|
|
15011
15259
|
diagnostics.push({
|
|
15012
15260
|
id: "absolute-path",
|
|
15013
15261
|
level: "warning",
|
|
@@ -15068,15 +15316,16 @@ function validateWorkspace(ws, dir) {
|
|
|
15068
15316
|
}
|
|
15069
15317
|
|
|
15070
15318
|
// src/gallery/edit.ts
|
|
15071
|
-
import { readFile as
|
|
15072
|
-
import
|
|
15073
|
-
import { z as
|
|
15319
|
+
import { readFile as readFile23, rename as rename9, writeFile as writeFile15 } from "fs/promises";
|
|
15320
|
+
import path32 from "path";
|
|
15321
|
+
import { z as z13 } from "zod";
|
|
15074
15322
|
|
|
15075
15323
|
// src/manifest-edit.ts
|
|
15076
15324
|
import { readFile as readFile19, rename as rename7, rm as rm8, writeFile as writeFile10 } from "fs/promises";
|
|
15077
|
-
import
|
|
15325
|
+
import path27 from "path";
|
|
15078
15326
|
import { z as z8 } from "zod";
|
|
15079
15327
|
var HexSha = z8.string().regex(/^[0-9a-f]{64}$/);
|
|
15328
|
+
var ProjectFileSchema = z8.string().min(1).refine((file) => !path27.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
|
|
15080
15329
|
var AssetPatchSchema = z8.object({
|
|
15081
15330
|
prompt: z8.string().optional(),
|
|
15082
15331
|
/** Per-style override for the style being viewed; null removes it. */
|
|
@@ -15125,10 +15374,11 @@ var NewAssetSchema = z8.object({
|
|
|
15125
15374
|
styles: z8.array(z8.string().min(1)).optional(),
|
|
15126
15375
|
/**
|
|
15127
15376
|
* A controlled regeneration of another asset in the same manifest,
|
|
15128
|
-
* instead of a fresh generation.
|
|
15129
|
-
* `
|
|
15130
|
-
*
|
|
15131
|
-
*
|
|
15377
|
+
* instead of a fresh generation. Every mode but `outpaint` can be
|
|
15378
|
+
* created here: `inpaint` names a mask file the gallery's mask editor
|
|
15379
|
+
* wrote first, `interpolate` names an existing image as its ending
|
|
15380
|
+
* keyframe, and `animate-skeleton` names the keypoints file the gallery
|
|
15381
|
+
* wrote first.
|
|
15132
15382
|
*/
|
|
15133
15383
|
revision: z8.discriminatedUnion("mode", [
|
|
15134
15384
|
z8.object({
|
|
@@ -15145,15 +15395,38 @@ var NewAssetSchema = z8.object({
|
|
|
15145
15395
|
dithering: RevisionDitheringSchema.optional(),
|
|
15146
15396
|
ditheringStrength: z8.number().min(0).max(10).optional()
|
|
15147
15397
|
}).strict(),
|
|
15398
|
+
z8.object({
|
|
15399
|
+
mode: z8.literal("inpaint"),
|
|
15400
|
+
from: z8.string().min(1),
|
|
15401
|
+
/** Black-and-white PNG the same size as the parent; white is repainted. */
|
|
15402
|
+
mask: ProjectFileSchema
|
|
15403
|
+
}).strict(),
|
|
15404
|
+
z8.object({
|
|
15405
|
+
mode: z8.literal("interpolate"),
|
|
15406
|
+
from: z8.string().min(1),
|
|
15407
|
+
/** The ending keyframe: an image already in the project, the parent's size. */
|
|
15408
|
+
lastFrame: ProjectFileSchema,
|
|
15409
|
+
fps: z8.number().int().min(1).max(60).optional()
|
|
15410
|
+
}).strict(),
|
|
15148
15411
|
z8.object({ mode: z8.literal("correct-pixelart"), from: z8.string().min(1), strength: z8.number().min(0).max(1).optional() }).strict(),
|
|
15412
|
+
z8.object({
|
|
15413
|
+
mode: z8.literal("remove-background"),
|
|
15414
|
+
from: z8.string().min(1),
|
|
15415
|
+
removalTask: z8.enum(["simple", "complex"]).optional(),
|
|
15416
|
+
/** A short description of the foreground, sent as PixelLab's text hint. */
|
|
15417
|
+
description: z8.string().min(1).optional()
|
|
15418
|
+
}).strict(),
|
|
15149
15419
|
z8.object({
|
|
15150
15420
|
mode: z8.enum(["animate", "animate-pixminimax"]),
|
|
15151
15421
|
from: z8.string().min(1),
|
|
15152
15422
|
frames: z8.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
|
|
15153
15423
|
fps: z8.number().int().min(1).max(60).optional(),
|
|
15154
15424
|
enhancePrompt: z8.boolean().optional(),
|
|
15155
|
-
|
|
15156
|
-
|
|
15425
|
+
driftThreshold: z8.number().min(0).optional(),
|
|
15426
|
+
/** `animate-pixminimax` only; the loader refuses these on `animate`. */
|
|
15427
|
+
direction: CharacterDirectionSchema.optional(),
|
|
15428
|
+
subjectDescription: z8.string().min(1).max(300).optional(),
|
|
15429
|
+
initialPose: z8.string().min(1).max(300).optional()
|
|
15157
15430
|
}).strict(),
|
|
15158
15431
|
z8.object({
|
|
15159
15432
|
mode: z8.literal("edit-animation"),
|
|
@@ -15471,7 +15744,7 @@ function applyEdit(raw, edit) {
|
|
|
15471
15744
|
}
|
|
15472
15745
|
}
|
|
15473
15746
|
async function withCandidate(manifestPath, edit, use, unchanged) {
|
|
15474
|
-
const absolute =
|
|
15747
|
+
const absolute = path27.resolve(manifestPath);
|
|
15475
15748
|
const original = await readFile19(absolute, "utf8");
|
|
15476
15749
|
if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
15477
15750
|
let raw;
|
|
@@ -15485,9 +15758,9 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
|
|
|
15485
15758
|
applyEdit(raw, edit);
|
|
15486
15759
|
const next = serializeLike(original, raw);
|
|
15487
15760
|
if (next === original) return unchanged(absolute, original);
|
|
15488
|
-
const tmp =
|
|
15489
|
-
|
|
15490
|
-
`.${
|
|
15761
|
+
const tmp = path27.join(
|
|
15762
|
+
path27.dirname(absolute),
|
|
15763
|
+
`.${path27.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
15491
15764
|
);
|
|
15492
15765
|
try {
|
|
15493
15766
|
await writeFile10(tmp, next);
|
|
@@ -15518,16 +15791,16 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
15518
15791
|
// src/pipeline/hand-edit.ts
|
|
15519
15792
|
import { existsSync as existsSync22 } from "fs";
|
|
15520
15793
|
import { copyFile, mkdir as mkdir8, readFile as readFile20, rename as rename8, rm as rm9, writeFile as writeFile11 } from "fs/promises";
|
|
15521
|
-
import
|
|
15794
|
+
import path28 from "path";
|
|
15522
15795
|
import { z as z9 } from "zod";
|
|
15523
15796
|
var HAND_EDIT_DIR = "edits";
|
|
15524
15797
|
function handEditPath(loaded, spec) {
|
|
15525
15798
|
const style = loaded.manifest.styles[spec.styleId];
|
|
15526
|
-
const outDir =
|
|
15527
|
-
const relative =
|
|
15528
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
15529
|
-
const file = inside ? relative :
|
|
15530
|
-
return
|
|
15799
|
+
const outDir = path28.resolve(loaded.root, style?.outDir ?? ".");
|
|
15800
|
+
const relative = path28.relative(outDir, spec.outFile);
|
|
15801
|
+
const inside = relative && !relative.startsWith("..") && !path28.isAbsolute(relative);
|
|
15802
|
+
const file = inside ? relative : path28.basename(spec.outFile);
|
|
15803
|
+
return path28.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
15531
15804
|
}
|
|
15532
15805
|
function handEditBases(spec, lock) {
|
|
15533
15806
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -15573,7 +15846,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
15573
15846
|
`${spec.styleId}/${spec.assetId} declares ${spec.source}, one committed file placed for the whole set; clear that source (pixelkiln edit detach) to hand-edit the members one by one`
|
|
15574
15847
|
);
|
|
15575
15848
|
}
|
|
15576
|
-
const existing = spec.source ?
|
|
15849
|
+
const existing = spec.source ? path28.resolve(loaded.root, spec.source) : null;
|
|
15577
15850
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
15578
15851
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
15579
15852
|
let created = false;
|
|
@@ -15584,7 +15857,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
15584
15857
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
15585
15858
|
);
|
|
15586
15859
|
}
|
|
15587
|
-
await mkdir8(
|
|
15860
|
+
await mkdir8(path28.dirname(member.path), { recursive: true });
|
|
15588
15861
|
await copyFile(member.base, member.path);
|
|
15589
15862
|
created = true;
|
|
15590
15863
|
}
|
|
@@ -15722,7 +15995,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
15722
15995
|
editor: input.editor,
|
|
15723
15996
|
protocol: input.protocol,
|
|
15724
15997
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
15725
|
-
project: projectPath ?
|
|
15998
|
+
project: projectPath ? path28.basename(projectPath) : null,
|
|
15726
15999
|
outputs: incoming.map((frame, index) => ({
|
|
15727
16000
|
role: frame.role,
|
|
15728
16001
|
basedOn: basedOnHash(entry, index),
|
|
@@ -15766,7 +16039,7 @@ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
|
|
|
15766
16039
|
// src/gallery/upload.ts
|
|
15767
16040
|
import { existsSync as existsSync23 } from "fs";
|
|
15768
16041
|
import { mkdir as mkdir9, writeFile as writeFile12 } from "fs/promises";
|
|
15769
|
-
import
|
|
16042
|
+
import path29 from "path";
|
|
15770
16043
|
import { z as z10 } from "zod";
|
|
15771
16044
|
var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
|
|
15772
16045
|
var UploadImageSchema = z10.object({
|
|
@@ -15779,12 +16052,12 @@ var UploadImageSchema = z10.object({
|
|
|
15779
16052
|
overwrite: z10.boolean().optional()
|
|
15780
16053
|
}).strict();
|
|
15781
16054
|
async function saveProjectImage(root, request) {
|
|
15782
|
-
const file =
|
|
15783
|
-
const relative =
|
|
15784
|
-
if (relative.startsWith("..") ||
|
|
16055
|
+
const file = path29.resolve(root, request.path);
|
|
16056
|
+
const relative = path29.relative(root, file);
|
|
16057
|
+
if (relative.startsWith("..") || path29.isAbsolute(relative)) {
|
|
15785
16058
|
throw new ManifestEditError("an upload must stay inside the project");
|
|
15786
16059
|
}
|
|
15787
|
-
const extension =
|
|
16060
|
+
const extension = path29.extname(file).toLowerCase();
|
|
15788
16061
|
const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
|
|
15789
16062
|
if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
|
|
15790
16063
|
const bytes = Buffer.from(request.base64, "base64");
|
|
@@ -15797,69 +16070,125 @@ async function saveProjectImage(root, request) {
|
|
|
15797
16070
|
throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
|
|
15798
16071
|
}
|
|
15799
16072
|
if (existsSync23(file) && !request.overwrite) {
|
|
15800
|
-
throw new ManifestEditError(`${relative.split(
|
|
16073
|
+
throw new ManifestEditError(`${relative.split(path29.sep).join("/")} already exists; choose another name, or replace it`);
|
|
15801
16074
|
}
|
|
15802
|
-
await mkdir9(
|
|
16075
|
+
await mkdir9(path29.dirname(file), { recursive: true });
|
|
15803
16076
|
await writeFile12(file, bytes);
|
|
15804
|
-
return relative.split(
|
|
16077
|
+
return relative.split(path29.sep).join("/");
|
|
15805
16078
|
}
|
|
15806
16079
|
|
|
15807
|
-
// src/gallery/
|
|
16080
|
+
// src/gallery/image-revision.ts
|
|
15808
16081
|
import { existsSync as existsSync24 } from "fs";
|
|
15809
|
-
import {
|
|
15810
|
-
import
|
|
16082
|
+
import { readFile as readFile21, rm as rm10, writeFile as writeFile13 } from "fs/promises";
|
|
16083
|
+
import path30 from "path";
|
|
15811
16084
|
import { z as z11 } from "zod";
|
|
15812
|
-
var
|
|
15813
|
-
|
|
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"),
|
|
16085
|
+
var InpaintRevisionRequestSchema = z11.object({
|
|
16086
|
+
action: z11.literal("create-inpaint-revision"),
|
|
15820
16087
|
project: z11.string().min(1).optional(),
|
|
15821
16088
|
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
|
|
15822
16089
|
styleId: z11.string().min(1),
|
|
15823
|
-
/** The sprite
|
|
16090
|
+
/** The sprite being repainted. */
|
|
15824
16091
|
from: z11.string().min(1),
|
|
15825
16092
|
assetId: z11.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
15826
|
-
/**
|
|
16093
|
+
/** What to paint into the masked area. */
|
|
15827
16094
|
prompt: z11.string().min(1),
|
|
16095
|
+
/** Manifest-relative `.png` destination for the mask. */
|
|
16096
|
+
maskPath: z11.string().min(1),
|
|
16097
|
+
maskBase64: z11.string().regex(/^[A-Za-z0-9+/]*={0,2}$/),
|
|
16098
|
+
/** Replace a file already at `maskPath`. */
|
|
16099
|
+
overwrite: z11.boolean().optional()
|
|
16100
|
+
}).strict();
|
|
16101
|
+
async function createInpaintRevision(manifestPath, request) {
|
|
16102
|
+
const root = path30.dirname(path30.resolve(manifestPath));
|
|
16103
|
+
if (path30.extname(request.maskPath).toLowerCase() !== ".png") {
|
|
16104
|
+
throw new ManifestEditError("the mask must be a .png path inside the project");
|
|
16105
|
+
}
|
|
16106
|
+
const bytes = Buffer.from(request.maskBase64, "base64");
|
|
16107
|
+
const metadata = imageMetadata(bytes);
|
|
16108
|
+
if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
|
|
16109
|
+
const file = path30.resolve(root, request.maskPath);
|
|
16110
|
+
const previous = existsSync24(file) ? await readFile21(file) : null;
|
|
16111
|
+
const mask = await saveProjectImage(root, {
|
|
16112
|
+
action: "upload-image",
|
|
16113
|
+
path: request.maskPath,
|
|
16114
|
+
base64: request.maskBase64,
|
|
16115
|
+
...request.overwrite ? { overwrite: true } : {}
|
|
16116
|
+
});
|
|
16117
|
+
try {
|
|
16118
|
+
await applyManifestEdit(manifestPath, {
|
|
16119
|
+
action: "add-asset",
|
|
16120
|
+
assetId: request.assetId,
|
|
16121
|
+
expectedSha256: request.expectedSha256,
|
|
16122
|
+
asset: {
|
|
16123
|
+
prompt: request.prompt,
|
|
16124
|
+
styles: [request.styleId],
|
|
16125
|
+
revision: { mode: "inpaint", from: request.from, mask }
|
|
16126
|
+
}
|
|
16127
|
+
});
|
|
16128
|
+
} catch (error) {
|
|
16129
|
+
if (previous) await writeFile13(file, previous);
|
|
16130
|
+
else await rm10(file, { force: true });
|
|
16131
|
+
throw error;
|
|
16132
|
+
}
|
|
16133
|
+
return mask;
|
|
16134
|
+
}
|
|
16135
|
+
|
|
16136
|
+
// src/gallery/skeleton.ts
|
|
16137
|
+
import { existsSync as existsSync25 } from "fs";
|
|
16138
|
+
import { mkdir as mkdir10, readFile as readFile22, rm as rm11, writeFile as writeFile14 } from "fs/promises";
|
|
16139
|
+
import path31 from "path";
|
|
16140
|
+
import { z as z12 } from "zod";
|
|
16141
|
+
var SkeletonEstimateRequestSchema = z12.object({
|
|
16142
|
+
project: z12.string().min(1).optional(),
|
|
16143
|
+
styleId: z12.string().min(1),
|
|
16144
|
+
assetId: z12.string().min(1),
|
|
16145
|
+
frames: z12.number().int().min(3).max(15).optional()
|
|
16146
|
+
}).strict();
|
|
16147
|
+
var SkeletonAnimationRequestSchema = z12.object({
|
|
16148
|
+
action: z12.literal("create-skeleton-animation"),
|
|
16149
|
+
project: z12.string().min(1).optional(),
|
|
16150
|
+
expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
|
|
16151
|
+
styleId: z12.string().min(1),
|
|
16152
|
+
/** The sprite the animation poses. */
|
|
16153
|
+
from: z12.string().min(1),
|
|
16154
|
+
assetId: z12.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
16155
|
+
/** The motion, sent as PixelLab's `action`. */
|
|
16156
|
+
prompt: z12.string().min(1),
|
|
15828
16157
|
direction: CharacterDirectionSchema,
|
|
15829
|
-
description:
|
|
15830
|
-
skeletonTemplate:
|
|
16158
|
+
description: z12.string().min(1).optional(),
|
|
16159
|
+
skeletonTemplate: z12.string().min(1).optional(),
|
|
15831
16160
|
/** Manifest-relative JSON path inside the project. */
|
|
15832
|
-
keypointsFile:
|
|
16161
|
+
keypointsFile: z12.string().min(1),
|
|
15833
16162
|
/** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
|
|
15834
16163
|
set: SkeletonSetSchema.optional(),
|
|
15835
16164
|
/** Replace an existing `keypointsFile` with `set`. */
|
|
15836
|
-
overwrite:
|
|
16165
|
+
overwrite: z12.boolean().optional()
|
|
15837
16166
|
}).strict();
|
|
15838
|
-
var SkeletonKeypointsUpdateSchema =
|
|
15839
|
-
action:
|
|
15840
|
-
project:
|
|
15841
|
-
styleId:
|
|
15842
|
-
assetId:
|
|
16167
|
+
var SkeletonKeypointsUpdateSchema = z12.object({
|
|
16168
|
+
action: z12.literal("update-skeleton-keypoints"),
|
|
16169
|
+
project: z12.string().min(1).optional(),
|
|
16170
|
+
styleId: z12.string().min(1),
|
|
16171
|
+
assetId: z12.string().min(1),
|
|
15843
16172
|
/** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
|
|
15844
|
-
expectedSha256:
|
|
16173
|
+
expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
|
|
15845
16174
|
set: SkeletonSetSchema
|
|
15846
16175
|
}).strict();
|
|
15847
16176
|
async function createSkeletonAnimation(manifestPath, request) {
|
|
15848
|
-
const root =
|
|
15849
|
-
const file =
|
|
15850
|
-
const relative =
|
|
15851
|
-
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") ||
|
|
16177
|
+
const root = path31.dirname(path31.resolve(manifestPath));
|
|
16178
|
+
const file = path31.resolve(root, request.keypointsFile);
|
|
16179
|
+
const relative = path31.relative(root, file);
|
|
16180
|
+
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path31.isAbsolute(relative)) {
|
|
15852
16181
|
throw new ManifestEditError("the keypoints file must be a .json path inside the project");
|
|
15853
16182
|
}
|
|
15854
|
-
const existed =
|
|
16183
|
+
const existed = existsSync25(file);
|
|
15855
16184
|
let previous = null;
|
|
15856
16185
|
if (request.set) {
|
|
15857
16186
|
if (existed && !request.overwrite) {
|
|
15858
16187
|
throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
|
|
15859
16188
|
}
|
|
15860
|
-
if (existed) previous = await
|
|
15861
|
-
await mkdir10(
|
|
15862
|
-
await
|
|
16189
|
+
if (existed) previous = await readFile22(file);
|
|
16190
|
+
await mkdir10(path31.dirname(file), { recursive: true });
|
|
16191
|
+
await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
15863
16192
|
} else if (!existed) {
|
|
15864
16193
|
throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
|
|
15865
16194
|
}
|
|
@@ -15874,7 +16203,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
15874
16203
|
revision: {
|
|
15875
16204
|
mode: "animate-skeleton",
|
|
15876
16205
|
from: request.from,
|
|
15877
|
-
keypointsFile: relative.split(
|
|
16206
|
+
keypointsFile: relative.split(path31.sep).join("/"),
|
|
15878
16207
|
direction: request.direction,
|
|
15879
16208
|
...request.description ? { description: request.description } : {},
|
|
15880
16209
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
@@ -15883,8 +16212,8 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
15883
16212
|
});
|
|
15884
16213
|
} catch (error) {
|
|
15885
16214
|
if (request.set) {
|
|
15886
|
-
if (previous) await
|
|
15887
|
-
else await
|
|
16215
|
+
if (previous) await writeFile14(file, previous);
|
|
16216
|
+
else await rm11(file, { force: true });
|
|
15888
16217
|
}
|
|
15889
16218
|
throw error;
|
|
15890
16219
|
}
|
|
@@ -15895,64 +16224,64 @@ async function updateSkeletonKeypoints(ctx, request) {
|
|
|
15895
16224
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
15896
16225
|
const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
|
|
15897
16226
|
if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
|
|
15898
|
-
const relative =
|
|
15899
|
-
if (relative.startsWith("..") ||
|
|
16227
|
+
const relative = path31.relative(ctx.loaded.root, file);
|
|
16228
|
+
if (relative.startsWith("..") || path31.isAbsolute(relative)) {
|
|
15900
16229
|
throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
|
|
15901
16230
|
}
|
|
15902
|
-
const current =
|
|
16231
|
+
const current = existsSync25(file) ? sha256(await readFile22(file)) : null;
|
|
15903
16232
|
if (current !== request.expectedSha256) {
|
|
15904
16233
|
throw Object.assign(
|
|
15905
|
-
new Error(`${relative.split(
|
|
16234
|
+
new Error(`${relative.split(path31.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
|
|
15906
16235
|
{ status: 409 }
|
|
15907
16236
|
);
|
|
15908
16237
|
}
|
|
15909
|
-
await
|
|
15910
|
-
return relative.split(
|
|
16238
|
+
await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
16239
|
+
return relative.split(path31.sep).join("/");
|
|
15911
16240
|
}
|
|
15912
16241
|
|
|
15913
16242
|
// src/gallery/edit.ts
|
|
15914
|
-
var Base64 =
|
|
15915
|
-
var HandEditRequestSchema =
|
|
15916
|
-
|
|
15917
|
-
action:
|
|
15918
|
-
project:
|
|
15919
|
-
assetId:
|
|
15920
|
-
styleId:
|
|
15921
|
-
expectedSha256:
|
|
16243
|
+
var Base64 = z13.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
16244
|
+
var HandEditRequestSchema = z13.discriminatedUnion("action", [
|
|
16245
|
+
z13.object({
|
|
16246
|
+
action: z13.literal("start-edit"),
|
|
16247
|
+
project: z13.string().min(1).optional(),
|
|
16248
|
+
assetId: z13.string().min(1),
|
|
16249
|
+
styleId: z13.string().min(1),
|
|
16250
|
+
expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/),
|
|
15922
16251
|
/** Also launch the author's editor on the file. */
|
|
15923
|
-
open:
|
|
16252
|
+
open: z13.boolean().optional()
|
|
15924
16253
|
}).strict(),
|
|
15925
|
-
|
|
15926
|
-
action:
|
|
15927
|
-
project:
|
|
15928
|
-
assetId:
|
|
15929
|
-
styleId:
|
|
15930
|
-
expectedSha256:
|
|
16254
|
+
z13.object({
|
|
16255
|
+
action: z13.literal("detach-edit"),
|
|
16256
|
+
project: z13.string().min(1).optional(),
|
|
16257
|
+
assetId: z13.string().min(1),
|
|
16258
|
+
styleId: z13.string().min(1),
|
|
16259
|
+
expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/)
|
|
15931
16260
|
}).strict(),
|
|
15932
16261
|
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
15933
|
-
|
|
15934
|
-
action:
|
|
15935
|
-
project:
|
|
15936
|
-
assetId:
|
|
15937
|
-
styleId:
|
|
15938
|
-
expectedSha256:
|
|
16262
|
+
z13.object({
|
|
16263
|
+
action: z13.literal("save-edit"),
|
|
16264
|
+
project: z13.string().min(1).optional(),
|
|
16265
|
+
assetId: z13.string().min(1),
|
|
16266
|
+
styleId: z13.string().min(1),
|
|
16267
|
+
expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/),
|
|
15939
16268
|
/** The flattened image; for a frame set, `frames` instead. */
|
|
15940
16269
|
png: Base64.min(1).optional(),
|
|
15941
16270
|
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
15942
|
-
frames:
|
|
16271
|
+
frames: z13.array(z13.object({ role: z13.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
15943
16272
|
/** The editor's layered project file, kept beside the edit. */
|
|
15944
16273
|
pxo: Base64.optional(),
|
|
15945
16274
|
/** Editor identity from the bridge's ready message. */
|
|
15946
|
-
editor:
|
|
15947
|
-
protocol:
|
|
16275
|
+
editor: z13.string().min(1).max(200),
|
|
16276
|
+
protocol: z13.number().int().positive()
|
|
15948
16277
|
}).strict()
|
|
15949
16278
|
]);
|
|
15950
16279
|
var UNDO_DEPTH = 20;
|
|
15951
|
-
var UndoRequestSchema =
|
|
15952
|
-
action:
|
|
15953
|
-
project:
|
|
16280
|
+
var UndoRequestSchema = z13.object({
|
|
16281
|
+
action: z13.literal("undo-edit"),
|
|
16282
|
+
project: z13.string().min(1).optional(),
|
|
15954
16283
|
/** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
|
|
15955
|
-
expectedSha256:
|
|
16284
|
+
expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/)
|
|
15956
16285
|
}).strict();
|
|
15957
16286
|
function createGalleryEditHandler(opts) {
|
|
15958
16287
|
const log = opts.onProgress ?? (() => {
|
|
@@ -15961,7 +16290,7 @@ function createGalleryEditHandler(opts) {
|
|
|
15961
16290
|
const history = /* @__PURE__ */ new Map();
|
|
15962
16291
|
const manifestOf = async (project) => {
|
|
15963
16292
|
try {
|
|
15964
|
-
return
|
|
16293
|
+
return path32.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
|
|
15965
16294
|
} catch {
|
|
15966
16295
|
return null;
|
|
15967
16296
|
}
|
|
@@ -15971,7 +16300,7 @@ function createGalleryEditHandler(opts) {
|
|
|
15971
16300
|
if (undo.success) {
|
|
15972
16301
|
const manifestPath2 = await manifestOf(undo.data.project);
|
|
15973
16302
|
if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
|
|
15974
|
-
const current = await
|
|
16303
|
+
const current = await readFile23(manifestPath2, "utf8");
|
|
15975
16304
|
if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
|
|
15976
16305
|
const stack = history.get(manifestPath2) ?? [];
|
|
15977
16306
|
const last = stack[stack.length - 1];
|
|
@@ -15981,17 +16310,17 @@ function createGalleryEditHandler(opts) {
|
|
|
15981
16310
|
throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
|
|
15982
16311
|
}
|
|
15983
16312
|
const tmp = manifestPath2 + ".pixelkiln-undo";
|
|
15984
|
-
await
|
|
16313
|
+
await writeFile15(tmp, last.before);
|
|
15985
16314
|
await rename9(tmp, manifestPath2);
|
|
15986
16315
|
stack.pop();
|
|
15987
|
-
log(` manifest edit undone in ${
|
|
16316
|
+
log(` manifest edit undone in ${path32.relative(process.cwd(), manifestPath2) || manifestPath2}`);
|
|
15988
16317
|
return opts.reload();
|
|
15989
16318
|
}
|
|
15990
16319
|
const manifestPath = await manifestOf(body?.project);
|
|
15991
|
-
const before = manifestPath ? await
|
|
16320
|
+
const before = manifestPath ? await readFile23(manifestPath, "utf8").catch(() => null) : null;
|
|
15992
16321
|
const build = await apply(body);
|
|
15993
16322
|
if (manifestPath && before !== null) {
|
|
15994
|
-
const after = await
|
|
16323
|
+
const after = await readFile23(manifestPath, "utf8").catch(() => null);
|
|
15995
16324
|
if (after !== null && after !== before) {
|
|
15996
16325
|
const stack = history.get(manifestPath) ?? [];
|
|
15997
16326
|
stack.push({ before, afterSha: sha256(after) });
|
|
@@ -16046,7 +16375,7 @@ function createEditApplier(opts, log) {
|
|
|
16046
16375
|
} catch (error) {
|
|
16047
16376
|
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
16048
16377
|
}
|
|
16049
|
-
const written = await saveProjectImage(
|
|
16378
|
+
const written = await saveProjectImage(path32.dirname(path32.resolve(manifestPath2)), upload.data);
|
|
16050
16379
|
log(` image added: ${written}`);
|
|
16051
16380
|
return opts.reload();
|
|
16052
16381
|
}
|
|
@@ -16082,6 +16411,23 @@ function createEditApplier(opts, log) {
|
|
|
16082
16411
|
"invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
16083
16412
|
);
|
|
16084
16413
|
}
|
|
16414
|
+
const inpaint = InpaintRevisionRequestSchema.safeParse(body);
|
|
16415
|
+
if (inpaint.success) {
|
|
16416
|
+
let manifestPath2;
|
|
16417
|
+
try {
|
|
16418
|
+
manifestPath2 = await opts.manifestFor(inpaint.data.project);
|
|
16419
|
+
} catch (error) {
|
|
16420
|
+
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
16421
|
+
}
|
|
16422
|
+
const mask = await createInpaintRevision(manifestPath2, inpaint.data);
|
|
16423
|
+
log(` manifest edited: added inpaint revision ${inpaint.data.assetId} (mask ${mask})`);
|
|
16424
|
+
return opts.reload();
|
|
16425
|
+
}
|
|
16426
|
+
if (body?.action === "create-inpaint-revision") {
|
|
16427
|
+
throw new ManifestEditError(
|
|
16428
|
+
"invalid inpaint revision: " + inpaint.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
16429
|
+
);
|
|
16430
|
+
}
|
|
16085
16431
|
const parsed = ManifestEditSchema.safeParse(body);
|
|
16086
16432
|
if (!parsed.success) {
|
|
16087
16433
|
throw new ManifestEditError(
|
|
@@ -16096,7 +16442,7 @@ function createEditApplier(opts, log) {
|
|
|
16096
16442
|
}
|
|
16097
16443
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
16098
16444
|
if (result.changed) {
|
|
16099
|
-
const relative =
|
|
16445
|
+
const relative = path32.relative(process.cwd(), result.manifestPath);
|
|
16100
16446
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
16101
16447
|
log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
|
|
16102
16448
|
}
|
|
@@ -16124,7 +16470,7 @@ function describeEdit(edit) {
|
|
|
16124
16470
|
// src/gallery/snapshot.ts
|
|
16125
16471
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
16126
16472
|
function galleryMediaId(absolutePath) {
|
|
16127
|
-
return
|
|
16473
|
+
return createHash5("sha256").update(path33.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
16128
16474
|
}
|
|
16129
16475
|
function galleryMediaRoute(id) {
|
|
16130
16476
|
return `/media/${id}`;
|
|
@@ -16143,9 +16489,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
16143
16489
|
return (entry.history ?? []).map((generation, i) => {
|
|
16144
16490
|
const outputs = generation.outputs.map((output) => {
|
|
16145
16491
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
16146
|
-
const file =
|
|
16492
|
+
const file = path33.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
16147
16493
|
let url = null;
|
|
16148
|
-
if (
|
|
16494
|
+
if (existsSync26(file)) {
|
|
16149
16495
|
const id = galleryMediaId(file);
|
|
16150
16496
|
media.set(id, { path: file, contentType: mediaType });
|
|
16151
16497
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -16177,7 +16523,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
16177
16523
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
16178
16524
|
async function samePixels(a, b) {
|
|
16179
16525
|
try {
|
|
16180
|
-
const [left, right] = await Promise.all([
|
|
16526
|
+
const [left, right] = await Promise.all([readFile24(a), readFile24(b)]);
|
|
16181
16527
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
16182
16528
|
const x = decodePng(left);
|
|
16183
16529
|
const y = decodePng(right);
|
|
@@ -16275,10 +16621,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
16275
16621
|
async function describeQuality(media, root, inspection) {
|
|
16276
16622
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
16277
16623
|
const outputs = await Promise.all(
|
|
16278
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
16624
|
+
outputPaths.map((file) => describeOutput(media, root, path33.resolve(root, file)))
|
|
16279
16625
|
);
|
|
16280
|
-
const recordPath =
|
|
16281
|
-
const recordExists =
|
|
16626
|
+
const recordPath = path33.resolve(root, inspection.record);
|
|
16627
|
+
const recordExists = existsSync26(recordPath);
|
|
16282
16628
|
let options = null;
|
|
16283
16629
|
let check = null;
|
|
16284
16630
|
if (recordExists) {
|
|
@@ -16297,7 +16643,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
16297
16643
|
return {
|
|
16298
16644
|
state: inspection.state,
|
|
16299
16645
|
reason: inspection.reason,
|
|
16300
|
-
output: portableOutputPath(
|
|
16646
|
+
output: portableOutputPath(path33.resolve(root, inspection.output), root),
|
|
16301
16647
|
record: portableOutputPath(recordPath, root),
|
|
16302
16648
|
recordExists,
|
|
16303
16649
|
outputs,
|
|
@@ -16335,9 +16681,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
16335
16681
|
const media = /* @__PURE__ */ new Map();
|
|
16336
16682
|
const plan = await buildPlan(specs, lock);
|
|
16337
16683
|
const items = [];
|
|
16338
|
-
const manifestText = await
|
|
16684
|
+
const manifestText = await readFile24(loaded.path, "utf8");
|
|
16339
16685
|
const manifestSha256 = sha256(manifestText);
|
|
16340
|
-
const cacheDir =
|
|
16686
|
+
const cacheDir = path33.resolve(path33.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
16341
16687
|
let rawStyles = {};
|
|
16342
16688
|
try {
|
|
16343
16689
|
const raw = JSON.parse(manifestText);
|
|
@@ -16361,12 +16707,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
16361
16707
|
)
|
|
16362
16708
|
);
|
|
16363
16709
|
if (spec.source) {
|
|
16364
|
-
const editPath =
|
|
16710
|
+
const editPath = path33.resolve(root, spec.source);
|
|
16365
16711
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
16366
16712
|
const editShas = await Promise.all(
|
|
16367
16713
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
16368
16714
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
16369
|
-
return { file, sha256:
|
|
16715
|
+
return { file, sha256: existsSync26(file) ? await sha256File(file) : null };
|
|
16370
16716
|
})
|
|
16371
16717
|
);
|
|
16372
16718
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -16374,7 +16720,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16374
16720
|
const companion = await readHandEditCompanion(editPath);
|
|
16375
16721
|
if (companion) {
|
|
16376
16722
|
const projectPath = handEditProjectPath(editPath);
|
|
16377
|
-
const project = companion.project &&
|
|
16723
|
+
const project = companion.project && existsSync26(projectPath) ? await fileInfo(projectPath) : null;
|
|
16378
16724
|
let projectUrl = null;
|
|
16379
16725
|
if (project) {
|
|
16380
16726
|
const id = galleryMediaId(projectPath);
|
|
@@ -16410,8 +16756,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
16410
16756
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
16411
16757
|
}
|
|
16412
16758
|
} else if (spec.source) {
|
|
16413
|
-
outputs = [await describeOutput(media, root,
|
|
16414
|
-
} else if (!entry &&
|
|
16759
|
+
outputs = [await describeOutput(media, root, path33.resolve(root, spec.source))];
|
|
16760
|
+
} else if (!entry && existsSync26(spec.outFile)) {
|
|
16415
16761
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
16416
16762
|
} else {
|
|
16417
16763
|
outputs = [];
|
|
@@ -16614,7 +16960,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16614
16960
|
id: loaded.manifest.name,
|
|
16615
16961
|
name: loaded.manifest.name,
|
|
16616
16962
|
manifest: loaded.path,
|
|
16617
|
-
lock:
|
|
16963
|
+
lock: path33.resolve(opts.lockPath),
|
|
16618
16964
|
root,
|
|
16619
16965
|
provider: loaded.manifest.provider,
|
|
16620
16966
|
account: null,
|
|
@@ -16640,7 +16986,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16640
16986
|
return { snapshot, media };
|
|
16641
16987
|
}
|
|
16642
16988
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
16643
|
-
const dir =
|
|
16989
|
+
const dir = path33.dirname(path33.resolve(opts.workspacePath));
|
|
16644
16990
|
const media = /* @__PURE__ */ new Map();
|
|
16645
16991
|
const projects = [];
|
|
16646
16992
|
const items = [];
|
|
@@ -16683,11 +17029,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16683
17029
|
id: project.id,
|
|
16684
17030
|
name: loaded.manifest.name,
|
|
16685
17031
|
manifest: manifestPath,
|
|
16686
|
-
lock:
|
|
17032
|
+
lock: path33.resolve(lockPath),
|
|
16687
17033
|
root: loaded.root,
|
|
16688
17034
|
provider: loaded.manifest.provider,
|
|
16689
17035
|
account: project.account ?? null,
|
|
16690
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
17036
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile24(manifestPath, "utf8")),
|
|
16691
17037
|
historyLimit: historyLimit(loaded.manifest),
|
|
16692
17038
|
entries: Object.keys(lock.entries).length,
|
|
16693
17039
|
items: projectItems.length,
|
|
@@ -16699,8 +17045,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16699
17045
|
id: project.id,
|
|
16700
17046
|
name: project.id,
|
|
16701
17047
|
manifest: manifestPath,
|
|
16702
|
-
lock:
|
|
16703
|
-
root:
|
|
17048
|
+
lock: path33.resolve(lockPath),
|
|
17049
|
+
root: path33.dirname(manifestPath),
|
|
16704
17050
|
provider: project.provider,
|
|
16705
17051
|
account: project.account ?? null,
|
|
16706
17052
|
manifestSha256: null,
|
|
@@ -16727,7 +17073,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16727
17073
|
version: 1,
|
|
16728
17074
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
16729
17075
|
project: null,
|
|
16730
|
-
workspace: { path:
|
|
17076
|
+
workspace: { path: path33.resolve(opts.workspacePath), dir, projects },
|
|
16731
17077
|
filter,
|
|
16732
17078
|
totals: {
|
|
16733
17079
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -16744,7 +17090,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16744
17090
|
}
|
|
16745
17091
|
|
|
16746
17092
|
// src/gallery/page.ts
|
|
16747
|
-
import { existsSync as
|
|
17093
|
+
import { existsSync as existsSync27, readFileSync as readFileSync6 } from "fs";
|
|
16748
17094
|
|
|
16749
17095
|
// src/gallery/client-bundle.ts
|
|
16750
17096
|
import { createRequire } from "module";
|
|
@@ -16784,8 +17130,8 @@ var client;
|
|
|
16784
17130
|
function clientAssets() {
|
|
16785
17131
|
const prebuilt = new URL("gallery.js", CLIENT_DIR);
|
|
16786
17132
|
client ??= {
|
|
16787
|
-
css:
|
|
16788
|
-
js:
|
|
17133
|
+
css: readFileSync6(new URL("gallery.css", CLIENT_DIR), "utf8"),
|
|
17134
|
+
js: existsSync27(prebuilt) ? readFileSync6(prebuilt, "utf8") : bundleGalleryClient()
|
|
16789
17135
|
};
|
|
16790
17136
|
return client;
|
|
16791
17137
|
}
|
|
@@ -16927,11 +17273,11 @@ var EDITOR_PIN = pin_default;
|
|
|
16927
17273
|
var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download";
|
|
16928
17274
|
|
|
16929
17275
|
// src/editor/install.ts
|
|
16930
|
-
import { createHash as
|
|
16931
|
-
import { existsSync as
|
|
16932
|
-
import { mkdir as mkdir11, readFile as
|
|
16933
|
-
import
|
|
16934
|
-
import
|
|
17276
|
+
import { createHash as createHash6 } from "crypto";
|
|
17277
|
+
import { existsSync as existsSync28 } from "fs";
|
|
17278
|
+
import { mkdir as mkdir11, readFile as readFile25, rename as rename10, rm as rm12, stat as stat4, writeFile as writeFile16 } from "fs/promises";
|
|
17279
|
+
import os3 from "os";
|
|
17280
|
+
import path34 from "path";
|
|
16935
17281
|
var EditorInstallError = class extends Error {
|
|
16936
17282
|
constructor(message6, file) {
|
|
16937
17283
|
super(message6);
|
|
@@ -16942,15 +17288,15 @@ var EditorInstallError = class extends Error {
|
|
|
16942
17288
|
};
|
|
16943
17289
|
function editorToolsRoot() {
|
|
16944
17290
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
16945
|
-
if (override) return
|
|
16946
|
-
if (process.platform === "darwin") return
|
|
17291
|
+
if (override) return path34.resolve(override);
|
|
17292
|
+
if (process.platform === "darwin") return path34.join(os3.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
16947
17293
|
if (process.platform === "win32") {
|
|
16948
|
-
return
|
|
17294
|
+
return path34.join(process.env.LOCALAPPDATA ?? path34.join(os3.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
16949
17295
|
}
|
|
16950
|
-
return
|
|
17296
|
+
return path34.join(process.env.XDG_CACHE_HOME ?? path34.join(os3.homedir(), ".cache"), "pixelkiln", "tools");
|
|
16951
17297
|
}
|
|
16952
17298
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
16953
|
-
return
|
|
17299
|
+
return path34.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
16954
17300
|
}
|
|
16955
17301
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
16956
17302
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -16960,7 +17306,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
16960
17306
|
}
|
|
16961
17307
|
async function sha256Of(file) {
|
|
16962
17308
|
try {
|
|
16963
|
-
return
|
|
17309
|
+
return createHash6("sha256").update(await readFile25(file)).digest("hex");
|
|
16964
17310
|
} catch {
|
|
16965
17311
|
return null;
|
|
16966
17312
|
}
|
|
@@ -16973,8 +17319,8 @@ async function editorStatus(opts = {}) {
|
|
|
16973
17319
|
let totalBytes = 0;
|
|
16974
17320
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
16975
17321
|
totalBytes += expected.bytes;
|
|
16976
|
-
const file =
|
|
16977
|
-
const info =
|
|
17322
|
+
const file = path34.join(dir, name);
|
|
17323
|
+
const info = existsSync28(file) ? await stat4(file).catch(() => null) : null;
|
|
16978
17324
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
16979
17325
|
installedBytes += expected.bytes;
|
|
16980
17326
|
} else {
|
|
@@ -17027,17 +17373,17 @@ async function installEditor(opts = {}) {
|
|
|
17027
17373
|
if (bytes.length !== expected.bytes) {
|
|
17028
17374
|
throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
|
|
17029
17375
|
}
|
|
17030
|
-
const digest2 =
|
|
17376
|
+
const digest2 = createHash6("sha256").update(bytes).digest("hex");
|
|
17031
17377
|
if (digest2 !== expected.sha256) {
|
|
17032
17378
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
17033
17379
|
}
|
|
17034
|
-
const final =
|
|
17380
|
+
const final = path34.join(dir, name);
|
|
17035
17381
|
const tmp = `${final}.${process.pid}.part`;
|
|
17036
17382
|
try {
|
|
17037
|
-
await
|
|
17383
|
+
await writeFile16(tmp, bytes);
|
|
17038
17384
|
await rename10(tmp, final);
|
|
17039
17385
|
} finally {
|
|
17040
|
-
await
|
|
17386
|
+
await rm12(tmp, { force: true });
|
|
17041
17387
|
}
|
|
17042
17388
|
fetchedBytes += bytes.length;
|
|
17043
17389
|
downloaded.push(name);
|
|
@@ -17077,12 +17423,12 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
17077
17423
|
".json": "application/json; charset=utf-8"
|
|
17078
17424
|
};
|
|
17079
17425
|
function editorContentType(name) {
|
|
17080
|
-
return EDITOR_CONTENT_TYPES[
|
|
17426
|
+
return EDITOR_CONTENT_TYPES[path34.extname(name).toLowerCase()] ?? null;
|
|
17081
17427
|
}
|
|
17082
17428
|
|
|
17083
17429
|
// src/gallery/editor.ts
|
|
17084
|
-
import { readFile as
|
|
17085
|
-
import
|
|
17430
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
17431
|
+
import path35 from "path";
|
|
17086
17432
|
function editorRoute(release, name = "index.html") {
|
|
17087
17433
|
return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
|
|
17088
17434
|
}
|
|
@@ -17155,7 +17501,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
17155
17501
|
if (!status.installed) return null;
|
|
17156
17502
|
let bytes;
|
|
17157
17503
|
try {
|
|
17158
|
-
bytes = await
|
|
17504
|
+
bytes = await readFile26(path35.join(dir, name));
|
|
17159
17505
|
} catch {
|
|
17160
17506
|
verified = null;
|
|
17161
17507
|
return null;
|
|
@@ -17172,7 +17518,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
17172
17518
|
// src/gallery/server.ts
|
|
17173
17519
|
import { randomBytes } from "crypto";
|
|
17174
17520
|
import { createServer as createServer2 } from "http";
|
|
17175
|
-
import { readFile as
|
|
17521
|
+
import { readFile as readFile27 } from "fs/promises";
|
|
17176
17522
|
var MAX_EDIT_BYTES = 48 * 1024 * 1024;
|
|
17177
17523
|
var EDITOR_CSP = [
|
|
17178
17524
|
"default-src 'self'",
|
|
@@ -17353,7 +17699,7 @@ async function serveGallery(opts) {
|
|
|
17353
17699
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
17354
17700
|
if (!asset) return fail(404, "no such review asset");
|
|
17355
17701
|
try {
|
|
17356
|
-
const bytes = await
|
|
17702
|
+
const bytes = await readFile27(asset.path);
|
|
17357
17703
|
res.writeHead(200, {
|
|
17358
17704
|
"Content-Type": asset.contentType,
|
|
17359
17705
|
"Cache-Control": "no-store",
|
|
@@ -17436,7 +17782,7 @@ async function serveGallery(opts) {
|
|
|
17436
17782
|
if (!asset) return fail(404, "no such gallery media");
|
|
17437
17783
|
let bytes;
|
|
17438
17784
|
try {
|
|
17439
|
-
bytes = await
|
|
17785
|
+
bytes = await readFile27(asset.path);
|
|
17440
17786
|
} catch {
|
|
17441
17787
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
17442
17788
|
}
|
|
@@ -17480,27 +17826,27 @@ async function serveGallery(opts) {
|
|
|
17480
17826
|
|
|
17481
17827
|
// src/gallery/generate.ts
|
|
17482
17828
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
17483
|
-
import { z as
|
|
17484
|
-
var GenerateRequestSchema =
|
|
17485
|
-
project:
|
|
17486
|
-
keys:
|
|
17829
|
+
import { z as z14 } from "zod";
|
|
17830
|
+
var GenerateRequestSchema = z14.object({
|
|
17831
|
+
project: z14.string().min(1).optional(),
|
|
17832
|
+
keys: z14.array(z14.string().min(1)).min(1).max(500),
|
|
17487
17833
|
/**
|
|
17488
17834
|
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
17489
17835
|
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
17490
17836
|
* the recorded bytes back over a modified file.
|
|
17491
17837
|
*/
|
|
17492
|
-
force:
|
|
17838
|
+
force: z14.boolean().optional(),
|
|
17493
17839
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
17494
|
-
resume:
|
|
17840
|
+
resume: z14.boolean().optional(),
|
|
17495
17841
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
17496
|
-
refresh:
|
|
17842
|
+
refresh: z14.boolean().optional(),
|
|
17497
17843
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
17498
|
-
restore:
|
|
17844
|
+
restore: z14.boolean().optional(),
|
|
17499
17845
|
/**
|
|
17500
17846
|
* Bring a replaced generation back: 1-based position in the record's
|
|
17501
17847
|
* history (newest first) or an output hash prefix. One key at a time.
|
|
17502
17848
|
*/
|
|
17503
|
-
revert:
|
|
17849
|
+
revert: z14.string().min(1).max(64).optional()
|
|
17504
17850
|
}).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
|
|
17505
17851
|
message: "resume, refresh, restore, and revert are exclusive"
|
|
17506
17852
|
}).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
|
|
@@ -17846,19 +18192,19 @@ function createGenerateHandlers(opts) {
|
|
|
17846
18192
|
}
|
|
17847
18193
|
|
|
17848
18194
|
// src/pipeline/salvage.ts
|
|
17849
|
-
import { readFile as
|
|
17850
|
-
import { existsSync as
|
|
17851
|
-
import
|
|
18195
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
18196
|
+
import { existsSync as existsSync29 } from "fs";
|
|
18197
|
+
import path36 from "path";
|
|
17852
18198
|
function providerClaimId(provider, objectId) {
|
|
17853
18199
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
17854
18200
|
}
|
|
17855
18201
|
async function loadClaims(lockPaths, opts = {}) {
|
|
17856
18202
|
const claimed = /* @__PURE__ */ new Set();
|
|
17857
18203
|
for (const p of lockPaths) {
|
|
17858
|
-
if (!
|
|
18204
|
+
if (!existsSync29(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
17859
18205
|
let parsed;
|
|
17860
18206
|
try {
|
|
17861
|
-
parsed = parseLock(JSON.parse(await
|
|
18207
|
+
parsed = parseLock(JSON.parse(await readFile28(p, "utf8")));
|
|
17862
18208
|
} catch {
|
|
17863
18209
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
17864
18210
|
}
|
|
@@ -17962,20 +18308,20 @@ function matchOrphanStyle(prompt, manifest) {
|
|
|
17962
18308
|
return matchStyleByPattern(prompt, manifest);
|
|
17963
18309
|
}
|
|
17964
18310
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
17965
|
-
const own =
|
|
18311
|
+
const own = path36.resolve(ownManifestPath);
|
|
17966
18312
|
const siblingManifestPaths = [
|
|
17967
18313
|
.../* @__PURE__ */ new Set([
|
|
17968
18314
|
...workspaceManifestPaths,
|
|
17969
|
-
...claimPaths.map((c) =>
|
|
18315
|
+
...claimPaths.map((c) => path36.join(path36.dirname(path36.resolve(c)), "pixelkiln.manifest.json"))
|
|
17970
18316
|
])
|
|
17971
18317
|
];
|
|
17972
18318
|
const siblings = [];
|
|
17973
18319
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
17974
|
-
if (
|
|
17975
|
-
if (!
|
|
18320
|
+
if (path36.resolve(siblingManifestPath) === own) continue;
|
|
18321
|
+
if (!existsSync29(siblingManifestPath)) continue;
|
|
17976
18322
|
try {
|
|
17977
18323
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
17978
|
-
siblings.push({ label:
|
|
18324
|
+
siblings.push({ label: path36.basename(path36.dirname(siblingManifestPath)), manifest });
|
|
17979
18325
|
} catch {
|
|
17980
18326
|
}
|
|
17981
18327
|
}
|
|
@@ -18095,8 +18441,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
18095
18441
|
}
|
|
18096
18442
|
|
|
18097
18443
|
// src/pick/salvage-server.ts
|
|
18098
|
-
import { mkdir as mkdir12, writeFile as
|
|
18099
|
-
import
|
|
18444
|
+
import { mkdir as mkdir12, writeFile as writeFile17, readFile as readFile29 } from "fs/promises";
|
|
18445
|
+
import path37 from "path";
|
|
18100
18446
|
|
|
18101
18447
|
// src/pick/salvage-sheet.ts
|
|
18102
18448
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -18262,7 +18608,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
18262
18608
|
});
|
|
18263
18609
|
const html = renderSalvageSheet(orphans, {
|
|
18264
18610
|
styleId: ctx.styleId,
|
|
18265
|
-
importDir:
|
|
18611
|
+
importDir: path37.relative(process.cwd(), ctx.importDir) || "."
|
|
18266
18612
|
});
|
|
18267
18613
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
18268
18614
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -18318,10 +18664,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
18318
18664
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
18319
18665
|
decodePng(buf);
|
|
18320
18666
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
18321
|
-
const rel =
|
|
18322
|
-
const outFile =
|
|
18323
|
-
await mkdir12(
|
|
18324
|
-
await
|
|
18667
|
+
const rel = path37.join("_salvaged", `${assetId}.png`);
|
|
18668
|
+
const outFile = path37.resolve(ctx.importDir, rel);
|
|
18669
|
+
await mkdir12(path37.dirname(outFile), { recursive: true });
|
|
18670
|
+
await writeFile17(outFile, buf);
|
|
18325
18671
|
ctx.manifest.assets[assetId] = {
|
|
18326
18672
|
prompt: orphan.prompt,
|
|
18327
18673
|
promptByStyle: {},
|
|
@@ -18353,7 +18699,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
18353
18699
|
sourceUrl: durableSource,
|
|
18354
18700
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
18355
18701
|
outputs: [{
|
|
18356
|
-
path: portableOutputPath(outFile,
|
|
18702
|
+
path: portableOutputPath(outFile, path37.dirname(ctx.manifestPath)),
|
|
18357
18703
|
sha256: sha256(buf)
|
|
18358
18704
|
}],
|
|
18359
18705
|
submittedAt: orphan.createdAt,
|
|
@@ -18377,9 +18723,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
18377
18723
|
}
|
|
18378
18724
|
}
|
|
18379
18725
|
await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
|
|
18380
|
-
const raw = JSON.parse(await
|
|
18726
|
+
const raw = JSON.parse(await readFile29(ctx.manifestPath, "utf8"));
|
|
18381
18727
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
18382
|
-
await
|
|
18728
|
+
await writeFile17(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
18383
18729
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
18384
18730
|
if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
|
|
18385
18731
|
return result;
|