pixelkiln 0.83.0 → 0.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +712 -482
- 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 +425 -195
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +767 -71
- package/dist/index.d.ts +767 -71
- package/dist/index.js +421 -191
- package/dist/index.js.map +1 -1
- package/docs/MANIFEST.md +10 -6
- package/docs/REVISIONS.md +62 -13
- 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/dist/cli.js
CHANGED
|
@@ -67,8 +67,8 @@ function exitCodeFor(err) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// src/cli/main.ts
|
|
70
|
-
import { existsSync as
|
|
71
|
-
import { readFile as
|
|
70
|
+
import { existsSync as existsSync38 } from "fs";
|
|
71
|
+
import { readFile as readFile34 } from "fs/promises";
|
|
72
72
|
|
|
73
73
|
// src/cli/args.ts
|
|
74
74
|
import path from "path";
|
|
@@ -665,10 +665,10 @@ import { createReadStream } from "fs";
|
|
|
665
665
|
function sha256(data) {
|
|
666
666
|
return createHash("sha256").update(data).digest("hex");
|
|
667
667
|
}
|
|
668
|
-
async function sha256File(
|
|
668
|
+
async function sha256File(path51) {
|
|
669
669
|
return new Promise((resolve, reject) => {
|
|
670
670
|
const hash = createHash("sha256");
|
|
671
|
-
const stream = createReadStream(
|
|
671
|
+
const stream = createReadStream(path51);
|
|
672
672
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
673
673
|
stream.on("error", reject);
|
|
674
674
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -792,6 +792,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
792
792
|
keypointsSha256: spec.revision.keypointsSha256,
|
|
793
793
|
skeletonTemplate: spec.revision.skeletonTemplate,
|
|
794
794
|
description: spec.revision.description,
|
|
795
|
+
removalTask: spec.revision.removalTask,
|
|
796
|
+
driftThreshold: spec.revision.driftThreshold,
|
|
797
|
+
subjectDescription: spec.revision.subjectDescription,
|
|
798
|
+
initialPose: spec.revision.initialPose,
|
|
795
799
|
engine: spec.revision.engine
|
|
796
800
|
} : void 0
|
|
797
801
|
})
|
|
@@ -818,6 +822,7 @@ var RevisionModeSchema = z.enum([
|
|
|
818
822
|
"outpaint",
|
|
819
823
|
"reduce-colors",
|
|
820
824
|
"correct-pixelart",
|
|
825
|
+
"remove-background",
|
|
821
826
|
"animate",
|
|
822
827
|
"animate-pixminimax",
|
|
823
828
|
"animate-skeleton",
|
|
@@ -1149,7 +1154,22 @@ var RevisionSchema = z.object({
|
|
|
1149
1154
|
* `interpolate`: required, the ending keyframe; the parent is the start.
|
|
1150
1155
|
*/
|
|
1151
1156
|
lastFrame: z.string().min(1).optional(),
|
|
1152
|
-
/**
|
|
1157
|
+
/**
|
|
1158
|
+
* `animate`/`animate-pixminimax` only: colour de-flicker sensitivity.
|
|
1159
|
+
* Frames whose foreground drifts from the first frame beyond this are
|
|
1160
|
+
* corrected toward it; 0 corrects every frame and higher values correct
|
|
1161
|
+
* fewer. Omit for PixelLab's default.
|
|
1162
|
+
*/
|
|
1163
|
+
driftThreshold: z.number().min(0).optional(),
|
|
1164
|
+
/**
|
|
1165
|
+
* `animate-pixminimax` only: one noun phrase for what the parent shows.
|
|
1166
|
+
* PixelLab reads it off the image when omitted, and can decline some
|
|
1167
|
+
* images, failing the job until it is sent.
|
|
1168
|
+
*/
|
|
1169
|
+
subjectDescription: z.string().min(1).max(300).optional(),
|
|
1170
|
+
/** `animate-pixminimax` only: the pose the parent is in, read off the image by PixelLab when omitted. */
|
|
1171
|
+
initialPose: z.string().min(1).max(300).optional(),
|
|
1172
|
+
/** `animate-pixminimax` only: facing direction, one of the model's caption fields, so it also holds the sprite's facing. */
|
|
1153
1173
|
direction: CharacterDirectionSchema.optional(),
|
|
1154
1174
|
/** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
|
|
1155
1175
|
enhancePrompt: z.boolean().optional(),
|
|
@@ -1169,12 +1189,15 @@ var RevisionSchema = z.object({
|
|
|
1169
1189
|
* same `mannequin`/`bear`/`cat`/`dog`/`horse`/`lion` vocabulary.
|
|
1170
1190
|
*/
|
|
1171
1191
|
skeletonTemplate: z.string().min(1).optional(),
|
|
1192
|
+
/** `remove-background` only: `simple` is faster and suits flat backgrounds; `complex` handles detailed edges. PixelLab defaults to `simple`. */
|
|
1193
|
+
removalTask: z.enum(["simple", "complex"]).optional(),
|
|
1172
1194
|
/**
|
|
1173
|
-
* `animate-skeleton
|
|
1195
|
+
* `animate-skeleton`: what the character *looks like*, a noun
|
|
1174
1196
|
* phrase (colours, clothing, held items) — distinct from the asset's
|
|
1175
1197
|
* own `prompt`, which this mode sends as the motion's short label
|
|
1176
1198
|
* (PixelLab's `action`), the same `prompt`-as-motion convention
|
|
1177
|
-
* `animate`/`animate-pixminimax` already use.
|
|
1199
|
+
* `animate`/`animate-pixminimax` already use. `remove-background`: a
|
|
1200
|
+
* short description of the foreground, which PixelLab uses as a hint.
|
|
1178
1201
|
*/
|
|
1179
1202
|
description: z.string().min(1).optional(),
|
|
1180
1203
|
/**
|
|
@@ -1291,7 +1314,44 @@ var RevisionSchema = z.object({
|
|
|
1291
1314
|
path: ["keypointsFile"]
|
|
1292
1315
|
});
|
|
1293
1316
|
}
|
|
1294
|
-
|
|
1317
|
+
if (revision.driftThreshold !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
|
|
1318
|
+
context.addIssue({
|
|
1319
|
+
code: z.ZodIssueCode.custom,
|
|
1320
|
+
message: "driftThreshold applies to animate/animate-pixminimax revisions only",
|
|
1321
|
+
path: ["driftThreshold"]
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
for (const field of ["subjectDescription", "initialPose"]) {
|
|
1325
|
+
if (revision[field] !== void 0 && revision.mode !== "animate-pixminimax") {
|
|
1326
|
+
context.addIssue({
|
|
1327
|
+
code: z.ZodIssueCode.custom,
|
|
1328
|
+
message: `${field} applies to animate-pixminimax revisions only`,
|
|
1329
|
+
path: [field]
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (revision.removalTask !== void 0 && revision.mode !== "remove-background") {
|
|
1334
|
+
context.addIssue({
|
|
1335
|
+
code: z.ZodIssueCode.custom,
|
|
1336
|
+
message: "removalTask applies to remove-background revisions only",
|
|
1337
|
+
path: ["removalTask"]
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
if (revision.strength !== void 0 && revision.mode === "remove-background") {
|
|
1341
|
+
context.addIssue({
|
|
1342
|
+
code: z.ZodIssueCode.custom,
|
|
1343
|
+
message: "remove-background revisions do not take a strength",
|
|
1344
|
+
path: ["strength"]
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
if (revision.description !== void 0 && revision.mode !== "animate-skeleton" && revision.mode !== "remove-background") {
|
|
1348
|
+
context.addIssue({
|
|
1349
|
+
code: z.ZodIssueCode.custom,
|
|
1350
|
+
message: "description applies to animate-skeleton/remove-background revisions only",
|
|
1351
|
+
path: ["description"]
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
for (const field of ["keypointsFile", "skeletonTemplate"]) {
|
|
1295
1355
|
if (revision[field] !== void 0 && revision.mode !== "animate-skeleton") {
|
|
1296
1356
|
context.addIssue({
|
|
1297
1357
|
code: z.ZodIssueCode.custom,
|
|
@@ -1945,6 +2005,10 @@ var LockEntrySchema = z.object({
|
|
|
1945
2005
|
keypointsSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
1946
2006
|
skeletonTemplate: z.string().min(1).optional(),
|
|
1947
2007
|
description: z.string().min(1).optional(),
|
|
2008
|
+
removalTask: z.enum(["simple", "complex"]).optional(),
|
|
2009
|
+
driftThreshold: z.number().min(0).optional(),
|
|
2010
|
+
subjectDescription: z.string().min(1).max(300).optional(),
|
|
2011
|
+
initialPose: z.string().min(1).max(300).optional(),
|
|
1948
2012
|
engine: z.enum(["pro-flash"]).optional()
|
|
1949
2013
|
}).strict().nullable().default(null),
|
|
1950
2014
|
/** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
|
|
@@ -5995,6 +6059,7 @@ var CorrectPixelartResponseSchema = z3.object({
|
|
|
5995
6059
|
images: z3.array(z3.object({ base64: z3.string().min(1) }).passthrough()).min(1),
|
|
5996
6060
|
usage: z3.unknown().optional()
|
|
5997
6061
|
}).passthrough();
|
|
6062
|
+
var RemoveBackgroundResponseSchema = z3.object({ image: z3.object({ base64: z3.string().min(1) }).passthrough(), usage: z3.unknown().optional() }).passthrough();
|
|
5998
6063
|
var ProFlashImageSubmitSchema = z3.object({
|
|
5999
6064
|
background_job_id: z3.string().min(1),
|
|
6000
6065
|
image_id: z3.string().min(1),
|
|
@@ -6186,9 +6251,9 @@ var PixelLabClient = class {
|
|
|
6186
6251
|
apiKey;
|
|
6187
6252
|
http;
|
|
6188
6253
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
6189
|
-
async request(
|
|
6254
|
+
async request(path51, init) {
|
|
6190
6255
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
6191
|
-
const res = await this.http(`${BASE}${
|
|
6256
|
+
const res = await this.http(`${BASE}${path51}`, {
|
|
6192
6257
|
...init,
|
|
6193
6258
|
headers: {
|
|
6194
6259
|
Authorization: auth,
|
|
@@ -6198,13 +6263,13 @@ var PixelLabClient = class {
|
|
|
6198
6263
|
});
|
|
6199
6264
|
const text = await res.text();
|
|
6200
6265
|
if (!res.ok) {
|
|
6201
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
6266
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path51} \u2192 ${res.status}`, res.status, text);
|
|
6202
6267
|
}
|
|
6203
6268
|
if (!text) return {};
|
|
6204
6269
|
try {
|
|
6205
6270
|
return JSON.parse(text);
|
|
6206
6271
|
} catch {
|
|
6207
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
6272
|
+
throw new Error(`${init?.method ?? "GET"} ${path51} returned invalid JSON`);
|
|
6208
6273
|
}
|
|
6209
6274
|
}
|
|
6210
6275
|
async balance() {
|
|
@@ -6609,10 +6674,10 @@ var PixelLabClient = class {
|
|
|
6609
6674
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
6610
6675
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
6611
6676
|
const south = args.reference?.south;
|
|
6612
|
-
let
|
|
6677
|
+
let path51;
|
|
6613
6678
|
let body;
|
|
6614
6679
|
if (args.mode === "v3") {
|
|
6615
|
-
|
|
6680
|
+
path51 = "/create-character-v3";
|
|
6616
6681
|
body = {
|
|
6617
6682
|
description: args.description,
|
|
6618
6683
|
image_size: imageSize,
|
|
@@ -6626,7 +6691,7 @@ var PixelLabClient = class {
|
|
|
6626
6691
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
6627
6692
|
};
|
|
6628
6693
|
} else if (args.mode === "pro-flash") {
|
|
6629
|
-
|
|
6694
|
+
path51 = "/create-character-pro-flash";
|
|
6630
6695
|
const traits = args.styleTraits;
|
|
6631
6696
|
body = {
|
|
6632
6697
|
description: args.description,
|
|
@@ -6650,7 +6715,7 @@ var PixelLabClient = class {
|
|
|
6650
6715
|
} : {}
|
|
6651
6716
|
};
|
|
6652
6717
|
} else if (args.mode === "pro") {
|
|
6653
|
-
|
|
6718
|
+
path51 = "/create-character-pro";
|
|
6654
6719
|
body = {
|
|
6655
6720
|
description: args.description,
|
|
6656
6721
|
image_size: imageSize,
|
|
@@ -6664,7 +6729,7 @@ var PixelLabClient = class {
|
|
|
6664
6729
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
6665
6730
|
};
|
|
6666
6731
|
} else {
|
|
6667
|
-
|
|
6732
|
+
path51 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
6668
6733
|
body = {
|
|
6669
6734
|
description: args.description,
|
|
6670
6735
|
image_size: imageSize,
|
|
@@ -6681,8 +6746,8 @@ var PixelLabClient = class {
|
|
|
6681
6746
|
...palette
|
|
6682
6747
|
};
|
|
6683
6748
|
}
|
|
6684
|
-
const raw = await this.request(
|
|
6685
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
6749
|
+
const raw = await this.request(path51, { method: "POST", body: JSON.stringify(body) });
|
|
6750
|
+
return validateResponse(CharacterSubmitSchema, raw, path51.slice(1));
|
|
6686
6751
|
}
|
|
6687
6752
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
6688
6753
|
async createCharacterState(args) {
|
|
@@ -7059,6 +7124,27 @@ var PixelLabClient = class {
|
|
|
7059
7124
|
const pngs = cleanupResults(res.images, images.length, "correct-pixelart");
|
|
7060
7125
|
return { png: pngs[0], pngs, usage: res.usage };
|
|
7061
7126
|
}
|
|
7127
|
+
/**
|
|
7128
|
+
* `/remove-background`: a transparent PNG from one pixel-art image,
|
|
7129
|
+
* synchronously, like the rest of the Cleanup tier. The API takes at most
|
|
7130
|
+
* 400x400 worth of area, one image per call. `simple` is faster and suits
|
|
7131
|
+
* flat backgrounds; `complex` is slower and handles detailed edges. The
|
|
7132
|
+
* schema's response example is dollar-denominated (`usd: 0.01`); the
|
|
7133
|
+
* subscription cost measured 1 generation (docs/ENDPOINTS.md), and the
|
|
7134
|
+
* default `simple` task is the one measured.
|
|
7135
|
+
*/
|
|
7136
|
+
async removeBackground(args) {
|
|
7137
|
+
const body = { image: args.image, image_size: { width: args.width, height: args.height } };
|
|
7138
|
+
if (args.task) body.background_removal_task = args.task === "complex" ? "remove_complex_background" : "remove_simple_background";
|
|
7139
|
+
if (args.text) body.text = args.text;
|
|
7140
|
+
if (args.seed != null) body.seed = args.seed;
|
|
7141
|
+
const res = validateResponse(
|
|
7142
|
+
RemoveBackgroundResponseSchema,
|
|
7143
|
+
await this.request("/remove-background", { method: "POST", body: JSON.stringify(body) }),
|
|
7144
|
+
"remove-background"
|
|
7145
|
+
);
|
|
7146
|
+
return { png: Buffer.from(res.image.base64, "base64"), usage: res.usage };
|
|
7147
|
+
}
|
|
7062
7148
|
/**
|
|
7063
7149
|
* `/unzoom`: recover native-resolution pixel art from an upscaled image (a
|
|
7064
7150
|
* 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
|
|
@@ -7167,6 +7253,7 @@ var PixelLabClient = class {
|
|
|
7167
7253
|
*/
|
|
7168
7254
|
async animateWithTextV3(args) {
|
|
7169
7255
|
const body = { first_frame: args.firstFrame, action: args.action };
|
|
7256
|
+
if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
|
|
7170
7257
|
if (args.lastFrame) body.last_frame = args.lastFrame;
|
|
7171
7258
|
if (args.frameCount != null) body.frame_count = args.frameCount;
|
|
7172
7259
|
if (args.seed != null) body.seed = args.seed;
|
|
@@ -7186,6 +7273,10 @@ var PixelLabClient = class {
|
|
|
7186
7273
|
*/
|
|
7187
7274
|
async animatePixminimax(args) {
|
|
7188
7275
|
const body = { first_frame: args.firstFrame, description: args.description };
|
|
7276
|
+
if (args.view) body.view = args.view;
|
|
7277
|
+
if (args.subjectDescription) body.subject_description = args.subjectDescription;
|
|
7278
|
+
if (args.initialPose) body.initial_pose = args.initialPose;
|
|
7279
|
+
if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
|
|
7189
7280
|
if (args.lastFrame) body.last_frame = args.lastFrame;
|
|
7190
7281
|
if (args.frameCount != null) body.frame_count = args.frameCount;
|
|
7191
7282
|
if (args.seed != null) body.seed = args.seed;
|
|
@@ -7221,6 +7312,7 @@ var PixelLabClient = class {
|
|
|
7221
7312
|
keypoints: args.keypoints,
|
|
7222
7313
|
direction: args.direction
|
|
7223
7314
|
};
|
|
7315
|
+
if (args.view) body.view = args.view;
|
|
7224
7316
|
if (args.templateId) body.template_id = args.templateId;
|
|
7225
7317
|
if (args.action) body.action = args.action;
|
|
7226
7318
|
if (args.description) body.description = args.description;
|
|
@@ -7532,7 +7624,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7532
7624
|
* outpaint path yet.
|
|
7533
7625
|
*/
|
|
7534
7626
|
supportsRevision(mode2) {
|
|
7535
|
-
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";
|
|
7627
|
+
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";
|
|
7536
7628
|
}
|
|
7537
7629
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
7538
7630
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
@@ -7560,6 +7652,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7560
7652
|
if (spec.batch?.role === "member") {
|
|
7561
7653
|
return { unit: "generations", amount: 0, candidates: 1 };
|
|
7562
7654
|
}
|
|
7655
|
+
if (spec.revision?.mode === "remove-background") {
|
|
7656
|
+
return { unit: "generations", amount: 1, candidates: 1 };
|
|
7657
|
+
}
|
|
7563
7658
|
if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
|
|
7564
7659
|
return { unit: "generations", amount: 0.1, candidates: 1 };
|
|
7565
7660
|
}
|
|
@@ -7648,6 +7743,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7648
7743
|
throw new Error(`PixelLab correct-pixelart source is ${width}x${height}; the API takes at most 1024 pixels per side`);
|
|
7649
7744
|
}
|
|
7650
7745
|
}
|
|
7746
|
+
if (spec.revision?.mode === "remove-background") {
|
|
7747
|
+
const { sourceWidth: width, sourceHeight: height } = spec.revision;
|
|
7748
|
+
if (width != null && height != null && (width > 400 || height > 400)) {
|
|
7749
|
+
throw new Error(`PixelLab remove-background source is ${width}x${height}; the API takes at most 400x400`);
|
|
7750
|
+
}
|
|
7751
|
+
}
|
|
7651
7752
|
if (spec.revision?.mode === "animate-skeleton") {
|
|
7652
7753
|
const { sourceWidth: width, sourceHeight: height, frames, skeletonTemplate } = spec.revision;
|
|
7653
7754
|
if (width != null && height != null && (width > 256 || height > 256)) {
|
|
@@ -7668,6 +7769,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7668
7769
|
if (mode2 === "animate" && frames != null && frames > 16) {
|
|
7669
7770
|
throw new Error(`PixelLab animate takes 4 to 16 frames; ${frames} is too many (use animate-pixminimax for up to 40)`);
|
|
7670
7771
|
}
|
|
7772
|
+
if (mode2 === "animate-pixminimax" && frames != null && frames % 4 !== 0) {
|
|
7773
|
+
throw new Error(`PixelLab animate-pixminimax takes a multiple of 4 frames (4 to 40); ${frames} is not one`);
|
|
7774
|
+
}
|
|
7671
7775
|
if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
|
|
7672
7776
|
throw new Error(
|
|
7673
7777
|
`PixelLab ${mode2} lastFrame is ${lastFrameWidth}x${lastFrameHeight}; source is ${width}x${height} \u2014 they must match`
|
|
@@ -8759,6 +8863,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8759
8863
|
const res2 = await this.client.correctPixelart({ images, strength: revision.strength });
|
|
8760
8864
|
return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
|
|
8761
8865
|
}
|
|
8866
|
+
if (revision.mode === "remove-background") {
|
|
8867
|
+
if (width == null || height == null) throw new Error(`${spec.styleId}/${spec.assetId}: remove-background needs the source's size`);
|
|
8868
|
+
const res2 = await this.client.removeBackground({
|
|
8869
|
+
image,
|
|
8870
|
+
width,
|
|
8871
|
+
height,
|
|
8872
|
+
task: revision.removalTask,
|
|
8873
|
+
text: revision.description,
|
|
8874
|
+
seed: spec.seed
|
|
8875
|
+
});
|
|
8876
|
+
return this.cacheCleanupResult([res2.png], res2.usage, roles, revision.sourceFps);
|
|
8877
|
+
}
|
|
8762
8878
|
if (revision.mode === "interpolate") {
|
|
8763
8879
|
const lastFrame = this.readLastFrame(spec);
|
|
8764
8880
|
if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
|
|
@@ -8794,7 +8910,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8794
8910
|
frameCount: revision.frames,
|
|
8795
8911
|
seed: spec.seed,
|
|
8796
8912
|
noBackground: spec.noBackground,
|
|
8797
|
-
enhancePrompt: revision.enhancePrompt
|
|
8913
|
+
enhancePrompt: revision.enhancePrompt,
|
|
8914
|
+
driftThreshold: revision.driftThreshold
|
|
8798
8915
|
});
|
|
8799
8916
|
return { jobId: res3.background_job_id };
|
|
8800
8917
|
}
|
|
@@ -8806,7 +8923,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8806
8923
|
seed: spec.seed,
|
|
8807
8924
|
noBackground: spec.noBackground,
|
|
8808
8925
|
enhancePrompt: revision.enhancePrompt,
|
|
8809
|
-
direction: revision.direction
|
|
8926
|
+
direction: revision.direction,
|
|
8927
|
+
...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
|
|
8928
|
+
subjectDescription: revision.subjectDescription,
|
|
8929
|
+
initialPose: revision.initialPose,
|
|
8930
|
+
driftThreshold: revision.driftThreshold
|
|
8810
8931
|
});
|
|
8811
8932
|
return { jobId: res2.background_job_id };
|
|
8812
8933
|
}
|
|
@@ -8823,6 +8944,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8823
8944
|
firstFrameKeypoints: revision.skeleton.firstFrameKeypoints,
|
|
8824
8945
|
keypoints: revision.skeleton.frames,
|
|
8825
8946
|
direction: revision.direction,
|
|
8947
|
+
// The style's camera, when it is one this endpoint knows; its enum is
|
|
8948
|
+
// narrower than a style's free-string `view`.
|
|
8949
|
+
...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
|
|
8826
8950
|
templateId: revision.skeletonTemplate,
|
|
8827
8951
|
action: spec.prompt,
|
|
8828
8952
|
description: revision.description,
|
|
@@ -8893,7 +9017,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8893
9017
|
}
|
|
8894
9018
|
async poll(jobId, generator, context) {
|
|
8895
9019
|
const revisionMode = context?.spec?.revision?.mode;
|
|
8896
|
-
if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
|
|
9020
|
+
if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart" || revisionMode === "remove-background") {
|
|
8897
9021
|
return this.pollCachedRevision(jobId, context);
|
|
8898
9022
|
}
|
|
8899
9023
|
if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "animate-skeleton" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
|
|
@@ -9601,9 +9725,9 @@ var RetroDiffusionClient = class {
|
|
|
9601
9725
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
9602
9726
|
return Buffer.from(await response.arrayBuffer());
|
|
9603
9727
|
}
|
|
9604
|
-
async call(
|
|
9728
|
+
async call(path51, init = {}) {
|
|
9605
9729
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
9606
|
-
const response = await this.request(`${this.baseUrl}${
|
|
9730
|
+
const response = await this.request(`${this.baseUrl}${path51}`, {
|
|
9607
9731
|
...init,
|
|
9608
9732
|
headers: {
|
|
9609
9733
|
"Content-Type": "application/json",
|
|
@@ -11201,6 +11325,10 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11201
11325
|
} : {},
|
|
11202
11326
|
...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
|
|
11203
11327
|
...asset.revision.description ? { description: asset.revision.description } : {},
|
|
11328
|
+
...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
|
|
11329
|
+
...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
|
|
11330
|
+
...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
|
|
11331
|
+
...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
|
|
11204
11332
|
...asset.revision.engine ? { engine: asset.revision.engine } : {}
|
|
11205
11333
|
};
|
|
11206
11334
|
}
|
|
@@ -11908,10 +12036,10 @@ function isSha256Hash(value) {
|
|
|
11908
12036
|
function parseCache(value) {
|
|
11909
12037
|
return HashCacheSchema.parse(value);
|
|
11910
12038
|
}
|
|
11911
|
-
async function loadCache(
|
|
11912
|
-
if (!existsSync10(
|
|
12039
|
+
async function loadCache(path51) {
|
|
12040
|
+
if (!existsSync10(path51)) return { version: 1, hashes: {} };
|
|
11913
12041
|
try {
|
|
11914
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
12042
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path51, "utf8")));
|
|
11915
12043
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
11916
12044
|
return {
|
|
11917
12045
|
version: 1,
|
|
@@ -11923,18 +12051,18 @@ async function loadCache(path50) {
|
|
|
11923
12051
|
return { version: 1, hashes: {} };
|
|
11924
12052
|
}
|
|
11925
12053
|
}
|
|
11926
|
-
async function saveCache(
|
|
12054
|
+
async function saveCache(path51, cache) {
|
|
11927
12055
|
const sorted = {};
|
|
11928
12056
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
11929
12057
|
const hash = cache.hashes[key];
|
|
11930
12058
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
11931
12059
|
sorted[key] = hash;
|
|
11932
12060
|
}
|
|
11933
|
-
await mkdir5(pathModule.dirname(pathModule.resolve(
|
|
11934
|
-
const tmp = `${
|
|
12061
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path51)), { recursive: true });
|
|
12062
|
+
const tmp = `${path51}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
11935
12063
|
try {
|
|
11936
12064
|
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
11937
|
-
await rename5(tmp,
|
|
12065
|
+
await rename5(tmp, path51);
|
|
11938
12066
|
} finally {
|
|
11939
12067
|
await rm5(tmp, { force: true });
|
|
11940
12068
|
}
|
|
@@ -13897,6 +14025,7 @@ import { readFile as readFile15, rename as rename7, rm as rm7, writeFile as writ
|
|
|
13897
14025
|
import path23 from "path";
|
|
13898
14026
|
import { z as z6 } from "zod";
|
|
13899
14027
|
var HexSha = z6.string().regex(/^[0-9a-f]{64}$/);
|
|
14028
|
+
var ProjectFileSchema = z6.string().min(1).refine((file) => !path23.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
|
|
13900
14029
|
var AssetPatchSchema = z6.object({
|
|
13901
14030
|
prompt: z6.string().optional(),
|
|
13902
14031
|
/** Per-style override for the style being viewed; null removes it. */
|
|
@@ -13945,10 +14074,11 @@ var NewAssetSchema = z6.object({
|
|
|
13945
14074
|
styles: z6.array(z6.string().min(1)).optional(),
|
|
13946
14075
|
/**
|
|
13947
14076
|
* A controlled regeneration of another asset in the same manifest,
|
|
13948
|
-
* instead of a fresh generation.
|
|
13949
|
-
* `
|
|
13950
|
-
*
|
|
13951
|
-
*
|
|
14077
|
+
* instead of a fresh generation. Every mode but `outpaint` can be
|
|
14078
|
+
* created here: `inpaint` names a mask file the gallery's mask editor
|
|
14079
|
+
* wrote first, `interpolate` names an existing image as its ending
|
|
14080
|
+
* keyframe, and `animate-skeleton` names the keypoints file the gallery
|
|
14081
|
+
* wrote first.
|
|
13952
14082
|
*/
|
|
13953
14083
|
revision: z6.discriminatedUnion("mode", [
|
|
13954
14084
|
z6.object({
|
|
@@ -13965,15 +14095,38 @@ var NewAssetSchema = z6.object({
|
|
|
13965
14095
|
dithering: RevisionDitheringSchema.optional(),
|
|
13966
14096
|
ditheringStrength: z6.number().min(0).max(10).optional()
|
|
13967
14097
|
}).strict(),
|
|
14098
|
+
z6.object({
|
|
14099
|
+
mode: z6.literal("inpaint"),
|
|
14100
|
+
from: z6.string().min(1),
|
|
14101
|
+
/** Black-and-white PNG the same size as the parent; white is repainted. */
|
|
14102
|
+
mask: ProjectFileSchema
|
|
14103
|
+
}).strict(),
|
|
14104
|
+
z6.object({
|
|
14105
|
+
mode: z6.literal("interpolate"),
|
|
14106
|
+
from: z6.string().min(1),
|
|
14107
|
+
/** The ending keyframe: an image already in the project, the parent's size. */
|
|
14108
|
+
lastFrame: ProjectFileSchema,
|
|
14109
|
+
fps: z6.number().int().min(1).max(60).optional()
|
|
14110
|
+
}).strict(),
|
|
13968
14111
|
z6.object({ mode: z6.literal("correct-pixelart"), from: z6.string().min(1), strength: z6.number().min(0).max(1).optional() }).strict(),
|
|
14112
|
+
z6.object({
|
|
14113
|
+
mode: z6.literal("remove-background"),
|
|
14114
|
+
from: z6.string().min(1),
|
|
14115
|
+
removalTask: z6.enum(["simple", "complex"]).optional(),
|
|
14116
|
+
/** A short description of the foreground, sent as PixelLab's text hint. */
|
|
14117
|
+
description: z6.string().min(1).optional()
|
|
14118
|
+
}).strict(),
|
|
13969
14119
|
z6.object({
|
|
13970
14120
|
mode: z6.enum(["animate", "animate-pixminimax"]),
|
|
13971
14121
|
from: z6.string().min(1),
|
|
13972
14122
|
frames: z6.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
|
|
13973
14123
|
fps: z6.number().int().min(1).max(60).optional(),
|
|
13974
14124
|
enhancePrompt: z6.boolean().optional(),
|
|
13975
|
-
|
|
13976
|
-
|
|
14125
|
+
driftThreshold: z6.number().min(0).optional(),
|
|
14126
|
+
/** `animate-pixminimax` only; the loader refuses these on `animate`. */
|
|
14127
|
+
direction: CharacterDirectionSchema.optional(),
|
|
14128
|
+
subjectDescription: z6.string().min(1).max(300).optional(),
|
|
14129
|
+
initialPose: z6.string().min(1).max(300).optional()
|
|
13977
14130
|
}).strict(),
|
|
13978
14131
|
z6.object({
|
|
13979
14132
|
mode: z6.literal("edit-animation"),
|
|
@@ -14645,19 +14798,19 @@ async function runEdit(args) {
|
|
|
14645
14798
|
}
|
|
14646
14799
|
|
|
14647
14800
|
// src/cli/commands/gallery.ts
|
|
14648
|
-
import
|
|
14649
|
-
import { existsSync as
|
|
14801
|
+
import path35 from "path";
|
|
14802
|
+
import { existsSync as existsSync27 } from "fs";
|
|
14650
14803
|
|
|
14651
14804
|
// src/gallery/snapshot.ts
|
|
14652
14805
|
import { createHash as createHash3 } from "crypto";
|
|
14653
|
-
import { existsSync as
|
|
14654
|
-
import { readFile as
|
|
14655
|
-
import
|
|
14806
|
+
import { existsSync as existsSync22 } from "fs";
|
|
14807
|
+
import { readFile as readFile20, stat as stat2 } from "fs/promises";
|
|
14808
|
+
import path31 from "path";
|
|
14656
14809
|
|
|
14657
14810
|
// src/gallery/edit.ts
|
|
14658
|
-
import { readFile as
|
|
14659
|
-
import
|
|
14660
|
-
import { z as
|
|
14811
|
+
import { readFile as readFile19, rename as rename9, writeFile as writeFile15 } from "fs/promises";
|
|
14812
|
+
import path29 from "path";
|
|
14813
|
+
import { z as z11 } from "zod";
|
|
14661
14814
|
|
|
14662
14815
|
// src/gallery/upload.ts
|
|
14663
14816
|
import { existsSync as existsSync18 } from "fs";
|
|
@@ -14700,50 +14853,106 @@ async function saveProjectImage(root, request) {
|
|
|
14700
14853
|
return relative.split(path26.sep).join("/");
|
|
14701
14854
|
}
|
|
14702
14855
|
|
|
14703
|
-
// src/gallery/
|
|
14856
|
+
// src/gallery/image-revision.ts
|
|
14704
14857
|
import { existsSync as existsSync19 } from "fs";
|
|
14705
|
-
import {
|
|
14858
|
+
import { readFile as readFile17, rm as rm9, writeFile as writeFile13 } from "fs/promises";
|
|
14706
14859
|
import path27 from "path";
|
|
14707
14860
|
import { z as z9 } from "zod";
|
|
14708
|
-
var
|
|
14709
|
-
|
|
14710
|
-
styleId: z9.string().min(1),
|
|
14711
|
-
assetId: z9.string().min(1),
|
|
14712
|
-
frames: z9.number().int().min(3).max(15).optional()
|
|
14713
|
-
}).strict();
|
|
14714
|
-
var SkeletonAnimationRequestSchema = z9.object({
|
|
14715
|
-
action: z9.literal("create-skeleton-animation"),
|
|
14861
|
+
var InpaintRevisionRequestSchema = z9.object({
|
|
14862
|
+
action: z9.literal("create-inpaint-revision"),
|
|
14716
14863
|
project: z9.string().min(1).optional(),
|
|
14717
14864
|
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
14718
14865
|
styleId: z9.string().min(1),
|
|
14719
|
-
/** The sprite
|
|
14866
|
+
/** The sprite being repainted. */
|
|
14720
14867
|
from: z9.string().min(1),
|
|
14721
14868
|
assetId: z9.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
14722
|
-
/**
|
|
14869
|
+
/** What to paint into the masked area. */
|
|
14723
14870
|
prompt: z9.string().min(1),
|
|
14871
|
+
/** Manifest-relative `.png` destination for the mask. */
|
|
14872
|
+
maskPath: z9.string().min(1),
|
|
14873
|
+
maskBase64: z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/),
|
|
14874
|
+
/** Replace a file already at `maskPath`. */
|
|
14875
|
+
overwrite: z9.boolean().optional()
|
|
14876
|
+
}).strict();
|
|
14877
|
+
async function createInpaintRevision(manifestPath, request) {
|
|
14878
|
+
const root = path27.dirname(path27.resolve(manifestPath));
|
|
14879
|
+
if (path27.extname(request.maskPath).toLowerCase() !== ".png") {
|
|
14880
|
+
throw new ManifestEditError("the mask must be a .png path inside the project");
|
|
14881
|
+
}
|
|
14882
|
+
const bytes = Buffer.from(request.maskBase64, "base64");
|
|
14883
|
+
const metadata = imageMetadata(bytes);
|
|
14884
|
+
if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
|
|
14885
|
+
const file = path27.resolve(root, request.maskPath);
|
|
14886
|
+
const previous = existsSync19(file) ? await readFile17(file) : null;
|
|
14887
|
+
const mask = await saveProjectImage(root, {
|
|
14888
|
+
action: "upload-image",
|
|
14889
|
+
path: request.maskPath,
|
|
14890
|
+
base64: request.maskBase64,
|
|
14891
|
+
...request.overwrite ? { overwrite: true } : {}
|
|
14892
|
+
});
|
|
14893
|
+
try {
|
|
14894
|
+
await applyManifestEdit(manifestPath, {
|
|
14895
|
+
action: "add-asset",
|
|
14896
|
+
assetId: request.assetId,
|
|
14897
|
+
expectedSha256: request.expectedSha256,
|
|
14898
|
+
asset: {
|
|
14899
|
+
prompt: request.prompt,
|
|
14900
|
+
styles: [request.styleId],
|
|
14901
|
+
revision: { mode: "inpaint", from: request.from, mask }
|
|
14902
|
+
}
|
|
14903
|
+
});
|
|
14904
|
+
} catch (error) {
|
|
14905
|
+
if (previous) await writeFile13(file, previous);
|
|
14906
|
+
else await rm9(file, { force: true });
|
|
14907
|
+
throw error;
|
|
14908
|
+
}
|
|
14909
|
+
return mask;
|
|
14910
|
+
}
|
|
14911
|
+
|
|
14912
|
+
// src/gallery/skeleton.ts
|
|
14913
|
+
import { existsSync as existsSync20 } from "fs";
|
|
14914
|
+
import { mkdir as mkdir11, readFile as readFile18, rm as rm10, writeFile as writeFile14 } from "fs/promises";
|
|
14915
|
+
import path28 from "path";
|
|
14916
|
+
import { z as z10 } from "zod";
|
|
14917
|
+
var SkeletonEstimateRequestSchema = z10.object({
|
|
14918
|
+
project: z10.string().min(1).optional(),
|
|
14919
|
+
styleId: z10.string().min(1),
|
|
14920
|
+
assetId: z10.string().min(1),
|
|
14921
|
+
frames: z10.number().int().min(3).max(15).optional()
|
|
14922
|
+
}).strict();
|
|
14923
|
+
var SkeletonAnimationRequestSchema = z10.object({
|
|
14924
|
+
action: z10.literal("create-skeleton-animation"),
|
|
14925
|
+
project: z10.string().min(1).optional(),
|
|
14926
|
+
expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
|
|
14927
|
+
styleId: z10.string().min(1),
|
|
14928
|
+
/** The sprite the animation poses. */
|
|
14929
|
+
from: z10.string().min(1),
|
|
14930
|
+
assetId: z10.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
14931
|
+
/** The motion, sent as PixelLab's `action`. */
|
|
14932
|
+
prompt: z10.string().min(1),
|
|
14724
14933
|
direction: CharacterDirectionSchema,
|
|
14725
|
-
description:
|
|
14726
|
-
skeletonTemplate:
|
|
14934
|
+
description: z10.string().min(1).optional(),
|
|
14935
|
+
skeletonTemplate: z10.string().min(1).optional(),
|
|
14727
14936
|
/** Manifest-relative JSON path inside the project. */
|
|
14728
|
-
keypointsFile:
|
|
14937
|
+
keypointsFile: z10.string().min(1),
|
|
14729
14938
|
/** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
|
|
14730
14939
|
set: SkeletonSetSchema.optional(),
|
|
14731
14940
|
/** Replace an existing `keypointsFile` with `set`. */
|
|
14732
|
-
overwrite:
|
|
14941
|
+
overwrite: z10.boolean().optional()
|
|
14733
14942
|
}).strict();
|
|
14734
|
-
var SkeletonKeypointsUpdateSchema =
|
|
14735
|
-
action:
|
|
14736
|
-
project:
|
|
14737
|
-
styleId:
|
|
14738
|
-
assetId:
|
|
14943
|
+
var SkeletonKeypointsUpdateSchema = z10.object({
|
|
14944
|
+
action: z10.literal("update-skeleton-keypoints"),
|
|
14945
|
+
project: z10.string().min(1).optional(),
|
|
14946
|
+
styleId: z10.string().min(1),
|
|
14947
|
+
assetId: z10.string().min(1),
|
|
14739
14948
|
/** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
|
|
14740
|
-
expectedSha256:
|
|
14949
|
+
expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
|
|
14741
14950
|
set: SkeletonSetSchema
|
|
14742
14951
|
}).strict();
|
|
14743
14952
|
var DEFAULT_ESTIMATE_LIMIT = 10;
|
|
14744
14953
|
function skeletonSourceFile(spec) {
|
|
14745
|
-
if (spec.source) return
|
|
14746
|
-
if (spec.quality &&
|
|
14954
|
+
if (spec.source) return path28.resolve(spec.root, spec.source);
|
|
14955
|
+
if (spec.quality && existsSync20(spec.quality.outFile)) return spec.quality.outFile;
|
|
14747
14956
|
return spec.outFile;
|
|
14748
14957
|
}
|
|
14749
14958
|
function createGallerySkeletonHandlers(opts) {
|
|
@@ -14764,10 +14973,10 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
14764
14973
|
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
|
|
14765
14974
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14766
14975
|
const file = skeletonSourceFile(spec);
|
|
14767
|
-
if (!
|
|
14768
|
-
const bytes = await
|
|
14976
|
+
if (!existsSync20(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
|
|
14977
|
+
const bytes = await readFile18(file);
|
|
14769
14978
|
const metadata = imageMetadata(bytes);
|
|
14770
|
-
if (!metadata) throw new ManifestEditError(`${
|
|
14979
|
+
if (!metadata) throw new ManifestEditError(`${path28.basename(file)} is not a readable PNG or JPEG`);
|
|
14771
14980
|
const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
14772
14981
|
if (problem) throw new ManifestEditError(`${key} is ${problem}`);
|
|
14773
14982
|
if (used >= limit) {
|
|
@@ -14784,7 +14993,7 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
14784
14993
|
log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
|
|
14785
14994
|
return {
|
|
14786
14995
|
set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
|
|
14787
|
-
source:
|
|
14996
|
+
source: path28.relative(ctx.loaded.root, file).split(path28.sep).join("/"),
|
|
14788
14997
|
session: status()
|
|
14789
14998
|
};
|
|
14790
14999
|
}
|
|
@@ -14798,21 +15007,21 @@ function billedAmount(usage) {
|
|
|
14798
15007
|
return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
|
|
14799
15008
|
}
|
|
14800
15009
|
async function createSkeletonAnimation(manifestPath, request) {
|
|
14801
|
-
const root =
|
|
14802
|
-
const file =
|
|
14803
|
-
const relative =
|
|
14804
|
-
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") ||
|
|
15010
|
+
const root = path28.dirname(path28.resolve(manifestPath));
|
|
15011
|
+
const file = path28.resolve(root, request.keypointsFile);
|
|
15012
|
+
const relative = path28.relative(root, file);
|
|
15013
|
+
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path28.isAbsolute(relative)) {
|
|
14805
15014
|
throw new ManifestEditError("the keypoints file must be a .json path inside the project");
|
|
14806
15015
|
}
|
|
14807
|
-
const existed =
|
|
15016
|
+
const existed = existsSync20(file);
|
|
14808
15017
|
let previous = null;
|
|
14809
15018
|
if (request.set) {
|
|
14810
15019
|
if (existed && !request.overwrite) {
|
|
14811
15020
|
throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
|
|
14812
15021
|
}
|
|
14813
|
-
if (existed) previous = await
|
|
14814
|
-
await mkdir11(
|
|
14815
|
-
await
|
|
15022
|
+
if (existed) previous = await readFile18(file);
|
|
15023
|
+
await mkdir11(path28.dirname(file), { recursive: true });
|
|
15024
|
+
await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
14816
15025
|
} else if (!existed) {
|
|
14817
15026
|
throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
|
|
14818
15027
|
}
|
|
@@ -14827,7 +15036,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
14827
15036
|
revision: {
|
|
14828
15037
|
mode: "animate-skeleton",
|
|
14829
15038
|
from: request.from,
|
|
14830
|
-
keypointsFile: relative.split(
|
|
15039
|
+
keypointsFile: relative.split(path28.sep).join("/"),
|
|
14831
15040
|
direction: request.direction,
|
|
14832
15041
|
...request.description ? { description: request.description } : {},
|
|
14833
15042
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
@@ -14836,8 +15045,8 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
14836
15045
|
});
|
|
14837
15046
|
} catch (error) {
|
|
14838
15047
|
if (request.set) {
|
|
14839
|
-
if (previous) await
|
|
14840
|
-
else await
|
|
15048
|
+
if (previous) await writeFile14(file, previous);
|
|
15049
|
+
else await rm10(file, { force: true });
|
|
14841
15050
|
}
|
|
14842
15051
|
throw error;
|
|
14843
15052
|
}
|
|
@@ -14848,64 +15057,64 @@ async function updateSkeletonKeypoints(ctx, request) {
|
|
|
14848
15057
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14849
15058
|
const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
|
|
14850
15059
|
if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
|
|
14851
|
-
const relative =
|
|
14852
|
-
if (relative.startsWith("..") ||
|
|
15060
|
+
const relative = path28.relative(ctx.loaded.root, file);
|
|
15061
|
+
if (relative.startsWith("..") || path28.isAbsolute(relative)) {
|
|
14853
15062
|
throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
|
|
14854
15063
|
}
|
|
14855
|
-
const current =
|
|
15064
|
+
const current = existsSync20(file) ? sha256(await readFile18(file)) : null;
|
|
14856
15065
|
if (current !== request.expectedSha256) {
|
|
14857
15066
|
throw Object.assign(
|
|
14858
|
-
new Error(`${relative.split(
|
|
15067
|
+
new Error(`${relative.split(path28.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
|
|
14859
15068
|
{ status: 409 }
|
|
14860
15069
|
);
|
|
14861
15070
|
}
|
|
14862
|
-
await
|
|
14863
|
-
return relative.split(
|
|
15071
|
+
await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
15072
|
+
return relative.split(path28.sep).join("/");
|
|
14864
15073
|
}
|
|
14865
15074
|
|
|
14866
15075
|
// src/gallery/edit.ts
|
|
14867
|
-
var Base64 =
|
|
14868
|
-
var HandEditRequestSchema =
|
|
14869
|
-
|
|
14870
|
-
action:
|
|
14871
|
-
project:
|
|
14872
|
-
assetId:
|
|
14873
|
-
styleId:
|
|
14874
|
-
expectedSha256:
|
|
15076
|
+
var Base64 = z11.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
15077
|
+
var HandEditRequestSchema = z11.discriminatedUnion("action", [
|
|
15078
|
+
z11.object({
|
|
15079
|
+
action: z11.literal("start-edit"),
|
|
15080
|
+
project: z11.string().min(1).optional(),
|
|
15081
|
+
assetId: z11.string().min(1),
|
|
15082
|
+
styleId: z11.string().min(1),
|
|
15083
|
+
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
|
|
14875
15084
|
/** Also launch the author's editor on the file. */
|
|
14876
|
-
open:
|
|
15085
|
+
open: z11.boolean().optional()
|
|
14877
15086
|
}).strict(),
|
|
14878
|
-
|
|
14879
|
-
action:
|
|
14880
|
-
project:
|
|
14881
|
-
assetId:
|
|
14882
|
-
styleId:
|
|
14883
|
-
expectedSha256:
|
|
15087
|
+
z11.object({
|
|
15088
|
+
action: z11.literal("detach-edit"),
|
|
15089
|
+
project: z11.string().min(1).optional(),
|
|
15090
|
+
assetId: z11.string().min(1),
|
|
15091
|
+
styleId: z11.string().min(1),
|
|
15092
|
+
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/)
|
|
14884
15093
|
}).strict(),
|
|
14885
15094
|
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
14886
|
-
|
|
14887
|
-
action:
|
|
14888
|
-
project:
|
|
14889
|
-
assetId:
|
|
14890
|
-
styleId:
|
|
14891
|
-
expectedSha256:
|
|
15095
|
+
z11.object({
|
|
15096
|
+
action: z11.literal("save-edit"),
|
|
15097
|
+
project: z11.string().min(1).optional(),
|
|
15098
|
+
assetId: z11.string().min(1),
|
|
15099
|
+
styleId: z11.string().min(1),
|
|
15100
|
+
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
|
|
14892
15101
|
/** The flattened image; for a frame set, `frames` instead. */
|
|
14893
15102
|
png: Base64.min(1).optional(),
|
|
14894
15103
|
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
14895
|
-
frames:
|
|
15104
|
+
frames: z11.array(z11.object({ role: z11.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
14896
15105
|
/** The editor's layered project file, kept beside the edit. */
|
|
14897
15106
|
pxo: Base64.optional(),
|
|
14898
15107
|
/** Editor identity from the bridge's ready message. */
|
|
14899
|
-
editor:
|
|
14900
|
-
protocol:
|
|
15108
|
+
editor: z11.string().min(1).max(200),
|
|
15109
|
+
protocol: z11.number().int().positive()
|
|
14901
15110
|
}).strict()
|
|
14902
15111
|
]);
|
|
14903
15112
|
var UNDO_DEPTH = 20;
|
|
14904
|
-
var UndoRequestSchema =
|
|
14905
|
-
action:
|
|
14906
|
-
project:
|
|
15113
|
+
var UndoRequestSchema = z11.object({
|
|
15114
|
+
action: z11.literal("undo-edit"),
|
|
15115
|
+
project: z11.string().min(1).optional(),
|
|
14907
15116
|
/** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
|
|
14908
|
-
expectedSha256:
|
|
15117
|
+
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/)
|
|
14909
15118
|
}).strict();
|
|
14910
15119
|
function createGalleryEditHandler(opts) {
|
|
14911
15120
|
const log2 = opts.onProgress ?? (() => {
|
|
@@ -14914,7 +15123,7 @@ function createGalleryEditHandler(opts) {
|
|
|
14914
15123
|
const history = /* @__PURE__ */ new Map();
|
|
14915
15124
|
const manifestOf = async (project) => {
|
|
14916
15125
|
try {
|
|
14917
|
-
return
|
|
15126
|
+
return path29.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
|
|
14918
15127
|
} catch {
|
|
14919
15128
|
return null;
|
|
14920
15129
|
}
|
|
@@ -14924,7 +15133,7 @@ function createGalleryEditHandler(opts) {
|
|
|
14924
15133
|
if (undo.success) {
|
|
14925
15134
|
const manifestPath2 = await manifestOf(undo.data.project);
|
|
14926
15135
|
if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
|
|
14927
|
-
const current = await
|
|
15136
|
+
const current = await readFile19(manifestPath2, "utf8");
|
|
14928
15137
|
if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
|
|
14929
15138
|
const stack = history.get(manifestPath2) ?? [];
|
|
14930
15139
|
const last = stack[stack.length - 1];
|
|
@@ -14934,17 +15143,17 @@ function createGalleryEditHandler(opts) {
|
|
|
14934
15143
|
throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
|
|
14935
15144
|
}
|
|
14936
15145
|
const tmp = manifestPath2 + ".pixelkiln-undo";
|
|
14937
|
-
await
|
|
15146
|
+
await writeFile15(tmp, last.before);
|
|
14938
15147
|
await rename9(tmp, manifestPath2);
|
|
14939
15148
|
stack.pop();
|
|
14940
|
-
log2(` manifest edit undone in ${
|
|
15149
|
+
log2(` manifest edit undone in ${path29.relative(process.cwd(), manifestPath2) || manifestPath2}`);
|
|
14941
15150
|
return opts.reload();
|
|
14942
15151
|
}
|
|
14943
15152
|
const manifestPath = await manifestOf(body?.project);
|
|
14944
|
-
const before = manifestPath ? await
|
|
15153
|
+
const before = manifestPath ? await readFile19(manifestPath, "utf8").catch(() => null) : null;
|
|
14945
15154
|
const build = await apply(body);
|
|
14946
15155
|
if (manifestPath && before !== null) {
|
|
14947
|
-
const after = await
|
|
15156
|
+
const after = await readFile19(manifestPath, "utf8").catch(() => null);
|
|
14948
15157
|
if (after !== null && after !== before) {
|
|
14949
15158
|
const stack = history.get(manifestPath) ?? [];
|
|
14950
15159
|
stack.push({ before, afterSha: sha256(after) });
|
|
@@ -14999,7 +15208,7 @@ function createEditApplier(opts, log2) {
|
|
|
14999
15208
|
} catch (error) {
|
|
15000
15209
|
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
15001
15210
|
}
|
|
15002
|
-
const written = await saveProjectImage(
|
|
15211
|
+
const written = await saveProjectImage(path29.dirname(path29.resolve(manifestPath2)), upload.data);
|
|
15003
15212
|
log2(` image added: ${written}`);
|
|
15004
15213
|
return opts.reload();
|
|
15005
15214
|
}
|
|
@@ -15035,6 +15244,23 @@ function createEditApplier(opts, log2) {
|
|
|
15035
15244
|
"invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
15036
15245
|
);
|
|
15037
15246
|
}
|
|
15247
|
+
const inpaint = InpaintRevisionRequestSchema.safeParse(body);
|
|
15248
|
+
if (inpaint.success) {
|
|
15249
|
+
let manifestPath2;
|
|
15250
|
+
try {
|
|
15251
|
+
manifestPath2 = await opts.manifestFor(inpaint.data.project);
|
|
15252
|
+
} catch (error) {
|
|
15253
|
+
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
15254
|
+
}
|
|
15255
|
+
const mask = await createInpaintRevision(manifestPath2, inpaint.data);
|
|
15256
|
+
log2(` manifest edited: added inpaint revision ${inpaint.data.assetId} (mask ${mask})`);
|
|
15257
|
+
return opts.reload();
|
|
15258
|
+
}
|
|
15259
|
+
if (body?.action === "create-inpaint-revision") {
|
|
15260
|
+
throw new ManifestEditError(
|
|
15261
|
+
"invalid inpaint revision: " + inpaint.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
15262
|
+
);
|
|
15263
|
+
}
|
|
15038
15264
|
const parsed = ManifestEditSchema.safeParse(body);
|
|
15039
15265
|
if (!parsed.success) {
|
|
15040
15266
|
throw new ManifestEditError(
|
|
@@ -15049,7 +15275,7 @@ function createEditApplier(opts, log2) {
|
|
|
15049
15275
|
}
|
|
15050
15276
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
15051
15277
|
if (result.changed) {
|
|
15052
|
-
const relative =
|
|
15278
|
+
const relative = path29.relative(process.cwd(), result.manifestPath);
|
|
15053
15279
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
15054
15280
|
log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
|
|
15055
15281
|
}
|
|
@@ -15100,8 +15326,8 @@ function createGalleryPriceHandler(opts) {
|
|
|
15100
15326
|
}
|
|
15101
15327
|
|
|
15102
15328
|
// src/pipeline/history.ts
|
|
15103
|
-
import { existsSync as
|
|
15104
|
-
import
|
|
15329
|
+
import { existsSync as existsSync21 } from "fs";
|
|
15330
|
+
import path30 from "path";
|
|
15105
15331
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
15106
15332
|
function historyLimit(manifest, env = process.env) {
|
|
15107
15333
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -15188,10 +15414,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
15188
15414
|
}
|
|
15189
15415
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
15190
15416
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
15191
|
-
if (!
|
|
15417
|
+
if (!existsSync21(file)) continue;
|
|
15192
15418
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
15193
15419
|
throw new OverwriteRefusedError(
|
|
15194
|
-
`${
|
|
15420
|
+
`${path30.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
15195
15421
|
[file]
|
|
15196
15422
|
);
|
|
15197
15423
|
}
|
|
@@ -15244,7 +15470,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
15244
15470
|
// src/gallery/snapshot.ts
|
|
15245
15471
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
15246
15472
|
function galleryMediaId(absolutePath) {
|
|
15247
|
-
return createHash3("sha256").update(
|
|
15473
|
+
return createHash3("sha256").update(path31.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
15248
15474
|
}
|
|
15249
15475
|
function galleryMediaRoute(id) {
|
|
15250
15476
|
return `/media/${id}`;
|
|
@@ -15263,9 +15489,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
15263
15489
|
return (entry.history ?? []).map((generation, i) => {
|
|
15264
15490
|
const outputs = generation.outputs.map((output) => {
|
|
15265
15491
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
15266
|
-
const file =
|
|
15492
|
+
const file = path31.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
15267
15493
|
let url = null;
|
|
15268
|
-
if (
|
|
15494
|
+
if (existsSync22(file)) {
|
|
15269
15495
|
const id = galleryMediaId(file);
|
|
15270
15496
|
media.set(id, { path: file, contentType: mediaType });
|
|
15271
15497
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -15297,7 +15523,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
15297
15523
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
15298
15524
|
async function samePixels(a, b) {
|
|
15299
15525
|
try {
|
|
15300
|
-
const [left, right] = await Promise.all([
|
|
15526
|
+
const [left, right] = await Promise.all([readFile20(a), readFile20(b)]);
|
|
15301
15527
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
15302
15528
|
const x = decodePng(left);
|
|
15303
15529
|
const y = decodePng(right);
|
|
@@ -15395,10 +15621,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
15395
15621
|
async function describeQuality(media, root, inspection) {
|
|
15396
15622
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
15397
15623
|
const outputs = await Promise.all(
|
|
15398
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
15624
|
+
outputPaths.map((file) => describeOutput(media, root, path31.resolve(root, file)))
|
|
15399
15625
|
);
|
|
15400
|
-
const recordPath =
|
|
15401
|
-
const recordExists =
|
|
15626
|
+
const recordPath = path31.resolve(root, inspection.record);
|
|
15627
|
+
const recordExists = existsSync22(recordPath);
|
|
15402
15628
|
let options = null;
|
|
15403
15629
|
let check = null;
|
|
15404
15630
|
if (recordExists) {
|
|
@@ -15417,7 +15643,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
15417
15643
|
return {
|
|
15418
15644
|
state: inspection.state,
|
|
15419
15645
|
reason: inspection.reason,
|
|
15420
|
-
output: portableOutputPath(
|
|
15646
|
+
output: portableOutputPath(path31.resolve(root, inspection.output), root),
|
|
15421
15647
|
record: portableOutputPath(recordPath, root),
|
|
15422
15648
|
recordExists,
|
|
15423
15649
|
outputs,
|
|
@@ -15455,9 +15681,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
15455
15681
|
const media = /* @__PURE__ */ new Map();
|
|
15456
15682
|
const plan = await buildPlan(specs, lock);
|
|
15457
15683
|
const items = [];
|
|
15458
|
-
const manifestText = await
|
|
15684
|
+
const manifestText = await readFile20(loaded.path, "utf8");
|
|
15459
15685
|
const manifestSha256 = sha256(manifestText);
|
|
15460
|
-
const cacheDir =
|
|
15686
|
+
const cacheDir = path31.resolve(path31.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
15461
15687
|
let rawStyles = {};
|
|
15462
15688
|
try {
|
|
15463
15689
|
const raw = JSON.parse(manifestText);
|
|
@@ -15481,12 +15707,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
15481
15707
|
)
|
|
15482
15708
|
);
|
|
15483
15709
|
if (spec.source) {
|
|
15484
|
-
const editPath =
|
|
15710
|
+
const editPath = path31.resolve(root, spec.source);
|
|
15485
15711
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
15486
15712
|
const editShas = await Promise.all(
|
|
15487
15713
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
15488
15714
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
15489
|
-
return { file, sha256:
|
|
15715
|
+
return { file, sha256: existsSync22(file) ? await sha256File(file) : null };
|
|
15490
15716
|
})
|
|
15491
15717
|
);
|
|
15492
15718
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -15494,7 +15720,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15494
15720
|
const companion = await readHandEditCompanion(editPath);
|
|
15495
15721
|
if (companion) {
|
|
15496
15722
|
const projectPath = handEditProjectPath(editPath);
|
|
15497
|
-
const project = companion.project &&
|
|
15723
|
+
const project = companion.project && existsSync22(projectPath) ? await fileInfo(projectPath) : null;
|
|
15498
15724
|
let projectUrl = null;
|
|
15499
15725
|
if (project) {
|
|
15500
15726
|
const id = galleryMediaId(projectPath);
|
|
@@ -15530,8 +15756,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
15530
15756
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
15531
15757
|
}
|
|
15532
15758
|
} else if (spec.source) {
|
|
15533
|
-
outputs = [await describeOutput(media, root,
|
|
15534
|
-
} else if (!entry &&
|
|
15759
|
+
outputs = [await describeOutput(media, root, path31.resolve(root, spec.source))];
|
|
15760
|
+
} else if (!entry && existsSync22(spec.outFile)) {
|
|
15535
15761
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
15536
15762
|
} else {
|
|
15537
15763
|
outputs = [];
|
|
@@ -15734,7 +15960,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15734
15960
|
id: loaded.manifest.name,
|
|
15735
15961
|
name: loaded.manifest.name,
|
|
15736
15962
|
manifest: loaded.path,
|
|
15737
|
-
lock:
|
|
15963
|
+
lock: path31.resolve(opts.lockPath),
|
|
15738
15964
|
root,
|
|
15739
15965
|
provider: loaded.manifest.provider,
|
|
15740
15966
|
account: null,
|
|
@@ -15760,7 +15986,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15760
15986
|
return { snapshot, media };
|
|
15761
15987
|
}
|
|
15762
15988
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
15763
|
-
const dir =
|
|
15989
|
+
const dir = path31.dirname(path31.resolve(opts.workspacePath));
|
|
15764
15990
|
const media = /* @__PURE__ */ new Map();
|
|
15765
15991
|
const projects = [];
|
|
15766
15992
|
const items = [];
|
|
@@ -15803,11 +16029,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15803
16029
|
id: project.id,
|
|
15804
16030
|
name: loaded.manifest.name,
|
|
15805
16031
|
manifest: manifestPath,
|
|
15806
|
-
lock:
|
|
16032
|
+
lock: path31.resolve(lockPath),
|
|
15807
16033
|
root: loaded.root,
|
|
15808
16034
|
provider: loaded.manifest.provider,
|
|
15809
16035
|
account: project.account ?? null,
|
|
15810
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
16036
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile20(manifestPath, "utf8")),
|
|
15811
16037
|
historyLimit: historyLimit(loaded.manifest),
|
|
15812
16038
|
entries: Object.keys(lock.entries).length,
|
|
15813
16039
|
items: projectItems.length,
|
|
@@ -15819,8 +16045,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15819
16045
|
id: project.id,
|
|
15820
16046
|
name: project.id,
|
|
15821
16047
|
manifest: manifestPath,
|
|
15822
|
-
lock:
|
|
15823
|
-
root:
|
|
16048
|
+
lock: path31.resolve(lockPath),
|
|
16049
|
+
root: path31.dirname(manifestPath),
|
|
15824
16050
|
provider: project.provider,
|
|
15825
16051
|
account: project.account ?? null,
|
|
15826
16052
|
manifestSha256: null,
|
|
@@ -15847,7 +16073,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15847
16073
|
version: 1,
|
|
15848
16074
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
15849
16075
|
project: null,
|
|
15850
|
-
workspace: { path:
|
|
16076
|
+
workspace: { path: path31.resolve(opts.workspacePath), dir, projects },
|
|
15851
16077
|
filter,
|
|
15852
16078
|
totals: {
|
|
15853
16079
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -15866,10 +16092,10 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15866
16092
|
// src/gallery/server.ts
|
|
15867
16093
|
import { randomBytes } from "crypto";
|
|
15868
16094
|
import { createServer as createServer2 } from "http";
|
|
15869
|
-
import { readFile as
|
|
16095
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
15870
16096
|
|
|
15871
16097
|
// src/gallery/page.ts
|
|
15872
|
-
import { existsSync as
|
|
16098
|
+
import { existsSync as existsSync23, readFileSync as readFileSync4 } from "fs";
|
|
15873
16099
|
|
|
15874
16100
|
// src/gallery/client-bundle.ts
|
|
15875
16101
|
import { createRequire } from "module";
|
|
@@ -15910,7 +16136,7 @@ function clientAssets() {
|
|
|
15910
16136
|
const prebuilt = new URL("gallery.js", CLIENT_DIR);
|
|
15911
16137
|
client ??= {
|
|
15912
16138
|
css: readFileSync4(new URL("gallery.css", CLIENT_DIR), "utf8"),
|
|
15913
|
-
js:
|
|
16139
|
+
js: existsSync23(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
|
|
15914
16140
|
};
|
|
15915
16141
|
return client;
|
|
15916
16142
|
}
|
|
@@ -16181,7 +16407,7 @@ async function serveGallery(opts) {
|
|
|
16181
16407
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
16182
16408
|
if (!asset) return fail(404, "no such review asset");
|
|
16183
16409
|
try {
|
|
16184
|
-
const bytes = await
|
|
16410
|
+
const bytes = await readFile21(asset.path);
|
|
16185
16411
|
res.writeHead(200, {
|
|
16186
16412
|
"Content-Type": asset.contentType,
|
|
16187
16413
|
"Cache-Control": "no-store",
|
|
@@ -16264,7 +16490,7 @@ async function serveGallery(opts) {
|
|
|
16264
16490
|
if (!asset) return fail(404, "no such gallery media");
|
|
16265
16491
|
let bytes;
|
|
16266
16492
|
try {
|
|
16267
|
-
bytes = await
|
|
16493
|
+
bytes = await readFile21(asset.path);
|
|
16268
16494
|
} catch {
|
|
16269
16495
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
16270
16496
|
}
|
|
@@ -16308,7 +16534,7 @@ async function serveGallery(opts) {
|
|
|
16308
16534
|
|
|
16309
16535
|
// src/gallery/generate.ts
|
|
16310
16536
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
16311
|
-
import { z as
|
|
16537
|
+
import { z as z12 } from "zod";
|
|
16312
16538
|
|
|
16313
16539
|
// src/pipeline/poll.ts
|
|
16314
16540
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -16592,6 +16818,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
16592
16818
|
...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
|
|
16593
16819
|
...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
|
|
16594
16820
|
...spec.revision.description ? { description: spec.revision.description } : {},
|
|
16821
|
+
...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
|
|
16822
|
+
...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
|
|
16823
|
+
...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
|
|
16824
|
+
...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
|
|
16595
16825
|
...spec.revision.engine ? { engine: spec.revision.engine } : {}
|
|
16596
16826
|
} : null,
|
|
16597
16827
|
outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
|
|
@@ -16719,13 +16949,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
|
|
|
16719
16949
|
}
|
|
16720
16950
|
|
|
16721
16951
|
// src/pick/server.ts
|
|
16722
|
-
import { existsSync as
|
|
16723
|
-
import { readFile as
|
|
16952
|
+
import { existsSync as existsSync25 } from "fs";
|
|
16953
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
16724
16954
|
|
|
16725
16955
|
// src/pipeline/init.ts
|
|
16726
|
-
import { readdir, readFile as
|
|
16727
|
-
import { existsSync as
|
|
16728
|
-
import
|
|
16956
|
+
import { readdir, readFile as readFile22, writeFile as writeFile16 } from "fs/promises";
|
|
16957
|
+
import { existsSync as existsSync24 } from "fs";
|
|
16958
|
+
import path32 from "path";
|
|
16729
16959
|
function pngSize(buf) {
|
|
16730
16960
|
if (buf.length < 24) return null;
|
|
16731
16961
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -16739,7 +16969,7 @@ function slugify(value) {
|
|
|
16739
16969
|
async function walk(dir, exclude) {
|
|
16740
16970
|
const out = [];
|
|
16741
16971
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
16742
|
-
const full =
|
|
16972
|
+
const full = path32.join(dir, entry.name);
|
|
16743
16973
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
16744
16974
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
16745
16975
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -16753,14 +16983,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
16753
16983
|
const skipped = [];
|
|
16754
16984
|
const seen = /* @__PURE__ */ new Map();
|
|
16755
16985
|
for (const file of files.sort()) {
|
|
16756
|
-
const size = pngSize(await
|
|
16986
|
+
const size = pngSize(await readFile22(file));
|
|
16757
16987
|
if (!size) {
|
|
16758
16988
|
skipped.push(`${file} (not a readable PNG)`);
|
|
16759
16989
|
continue;
|
|
16760
16990
|
}
|
|
16761
|
-
const rel =
|
|
16762
|
-
const category =
|
|
16763
|
-
let id = slugify(
|
|
16991
|
+
const rel = path32.relative(root, file);
|
|
16992
|
+
const category = path32.dirname(rel) === "." ? "" : path32.dirname(rel);
|
|
16993
|
+
let id = slugify(path32.basename(rel));
|
|
16764
16994
|
if (seen.has(id)) {
|
|
16765
16995
|
const n = seen.get(id) + 1;
|
|
16766
16996
|
seen.set(id, n);
|
|
@@ -16818,10 +17048,10 @@ function mode(values) {
|
|
|
16818
17048
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
16819
17049
|
}
|
|
16820
17050
|
async function writeManifestFile(target, manifest) {
|
|
16821
|
-
if (
|
|
17051
|
+
if (existsSync24(target)) {
|
|
16822
17052
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
16823
17053
|
}
|
|
16824
|
-
await
|
|
17054
|
+
await writeFile16(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
16825
17055
|
}
|
|
16826
17056
|
|
|
16827
17057
|
// src/pick/sheet.ts
|
|
@@ -17234,13 +17464,13 @@ async function currentArt(entry, spec) {
|
|
|
17234
17464
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
17235
17465
|
if (!previous) return null;
|
|
17236
17466
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
17237
|
-
if (!
|
|
17467
|
+
if (!existsSync25(file)) return null;
|
|
17238
17468
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
17239
17469
|
if (!contentType) return null;
|
|
17240
17470
|
let width = entry.width;
|
|
17241
17471
|
let height = entry.height;
|
|
17242
17472
|
if (contentType === "image/png") {
|
|
17243
|
-
const size = pngSize(await
|
|
17473
|
+
const size = pngSize(await readFile23(file));
|
|
17244
17474
|
if (size) ({ width, height } = size);
|
|
17245
17475
|
}
|
|
17246
17476
|
return { path: file, contentType, width, height };
|
|
@@ -17408,26 +17638,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
17408
17638
|
}
|
|
17409
17639
|
|
|
17410
17640
|
// src/gallery/generate.ts
|
|
17411
|
-
var GenerateRequestSchema =
|
|
17412
|
-
project:
|
|
17413
|
-
keys:
|
|
17641
|
+
var GenerateRequestSchema = z12.object({
|
|
17642
|
+
project: z12.string().min(1).optional(),
|
|
17643
|
+
keys: z12.array(z12.string().min(1)).min(1).max(500),
|
|
17414
17644
|
/**
|
|
17415
17645
|
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
17416
17646
|
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
17417
17647
|
* the recorded bytes back over a modified file.
|
|
17418
17648
|
*/
|
|
17419
|
-
force:
|
|
17649
|
+
force: z12.boolean().optional(),
|
|
17420
17650
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
17421
|
-
resume:
|
|
17651
|
+
resume: z12.boolean().optional(),
|
|
17422
17652
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
17423
|
-
refresh:
|
|
17653
|
+
refresh: z12.boolean().optional(),
|
|
17424
17654
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
17425
|
-
restore:
|
|
17655
|
+
restore: z12.boolean().optional(),
|
|
17426
17656
|
/**
|
|
17427
17657
|
* Bring a replaced generation back: 1-based position in the record's
|
|
17428
17658
|
* history (newest first) or an output hash prefix. One key at a time.
|
|
17429
17659
|
*/
|
|
17430
|
-
revert:
|
|
17660
|
+
revert: z12.string().min(1).max(64).optional()
|
|
17431
17661
|
}).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
|
|
17432
17662
|
message: "resume, refresh, restore, and revert are exclusive"
|
|
17433
17663
|
}).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
|
|
@@ -17773,15 +18003,15 @@ function createGenerateHandlers(opts) {
|
|
|
17773
18003
|
}
|
|
17774
18004
|
|
|
17775
18005
|
// src/gallery/editor.ts
|
|
17776
|
-
import { readFile as
|
|
17777
|
-
import
|
|
18006
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
18007
|
+
import path34 from "path";
|
|
17778
18008
|
|
|
17779
18009
|
// src/editor/install.ts
|
|
17780
18010
|
import { createHash as createHash4 } from "crypto";
|
|
17781
|
-
import { existsSync as
|
|
17782
|
-
import { mkdir as mkdir12, readFile as
|
|
18011
|
+
import { existsSync as existsSync26 } from "fs";
|
|
18012
|
+
import { mkdir as mkdir12, readFile as readFile24, rename as rename10, rm as rm11, stat as stat3, writeFile as writeFile17 } from "fs/promises";
|
|
17783
18013
|
import os2 from "os";
|
|
17784
|
-
import
|
|
18014
|
+
import path33 from "path";
|
|
17785
18015
|
|
|
17786
18016
|
// tools/pixelorama-bridge/pin.json
|
|
17787
18017
|
var pin_default = {
|
|
@@ -17845,15 +18075,15 @@ var EditorInstallError = class extends Error {
|
|
|
17845
18075
|
};
|
|
17846
18076
|
function editorToolsRoot() {
|
|
17847
18077
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
17848
|
-
if (override) return
|
|
17849
|
-
if (process.platform === "darwin") return
|
|
18078
|
+
if (override) return path33.resolve(override);
|
|
18079
|
+
if (process.platform === "darwin") return path33.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
17850
18080
|
if (process.platform === "win32") {
|
|
17851
|
-
return
|
|
18081
|
+
return path33.join(process.env.LOCALAPPDATA ?? path33.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
17852
18082
|
}
|
|
17853
|
-
return
|
|
18083
|
+
return path33.join(process.env.XDG_CACHE_HOME ?? path33.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
17854
18084
|
}
|
|
17855
18085
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
17856
|
-
return
|
|
18086
|
+
return path33.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
17857
18087
|
}
|
|
17858
18088
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
17859
18089
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -17863,7 +18093,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
17863
18093
|
}
|
|
17864
18094
|
async function sha256Of(file) {
|
|
17865
18095
|
try {
|
|
17866
|
-
return createHash4("sha256").update(await
|
|
18096
|
+
return createHash4("sha256").update(await readFile24(file)).digest("hex");
|
|
17867
18097
|
} catch {
|
|
17868
18098
|
return null;
|
|
17869
18099
|
}
|
|
@@ -17876,8 +18106,8 @@ async function editorStatus(opts = {}) {
|
|
|
17876
18106
|
let totalBytes = 0;
|
|
17877
18107
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
17878
18108
|
totalBytes += expected.bytes;
|
|
17879
|
-
const file =
|
|
17880
|
-
const info =
|
|
18109
|
+
const file = path33.join(dir, name);
|
|
18110
|
+
const info = existsSync26(file) ? await stat3(file).catch(() => null) : null;
|
|
17881
18111
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
17882
18112
|
installedBytes += expected.bytes;
|
|
17883
18113
|
} else {
|
|
@@ -17934,13 +18164,13 @@ async function installEditor(opts = {}) {
|
|
|
17934
18164
|
if (digest2 !== expected.sha256) {
|
|
17935
18165
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
17936
18166
|
}
|
|
17937
|
-
const final =
|
|
18167
|
+
const final = path33.join(dir, name);
|
|
17938
18168
|
const tmp = `${final}.${process.pid}.part`;
|
|
17939
18169
|
try {
|
|
17940
|
-
await
|
|
18170
|
+
await writeFile17(tmp, bytes);
|
|
17941
18171
|
await rename10(tmp, final);
|
|
17942
18172
|
} finally {
|
|
17943
|
-
await
|
|
18173
|
+
await rm11(tmp, { force: true });
|
|
17944
18174
|
}
|
|
17945
18175
|
fetchedBytes += bytes.length;
|
|
17946
18176
|
downloaded.push(name);
|
|
@@ -17980,7 +18210,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
17980
18210
|
".json": "application/json; charset=utf-8"
|
|
17981
18211
|
};
|
|
17982
18212
|
function editorContentType(name) {
|
|
17983
|
-
return EDITOR_CONTENT_TYPES[
|
|
18213
|
+
return EDITOR_CONTENT_TYPES[path33.extname(name).toLowerCase()] ?? null;
|
|
17984
18214
|
}
|
|
17985
18215
|
|
|
17986
18216
|
// src/gallery/editor.ts
|
|
@@ -18056,7 +18286,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
18056
18286
|
if (!status.installed) return null;
|
|
18057
18287
|
let bytes;
|
|
18058
18288
|
try {
|
|
18059
|
-
bytes = await
|
|
18289
|
+
bytes = await readFile25(path34.join(dir, name));
|
|
18060
18290
|
} catch {
|
|
18061
18291
|
verified = null;
|
|
18062
18292
|
return null;
|
|
@@ -18096,7 +18326,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18096
18326
|
};
|
|
18097
18327
|
const loadProject = async (project) => {
|
|
18098
18328
|
const ctx = await access2.loadProject(project);
|
|
18099
|
-
loadProjectEnv(
|
|
18329
|
+
loadProjectEnv(path35.dirname(ctx.loaded.path));
|
|
18100
18330
|
return ctx;
|
|
18101
18331
|
};
|
|
18102
18332
|
const loadProjectEnv = (dir) => {
|
|
@@ -18105,7 +18335,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18105
18335
|
for (const [name, value] of Object.entries(declared)) {
|
|
18106
18336
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
18107
18337
|
throw new Error(
|
|
18108
|
-
`${
|
|
18338
|
+
`${path35.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
|
|
18109
18339
|
);
|
|
18110
18340
|
}
|
|
18111
18341
|
}
|
|
@@ -18113,7 +18343,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18113
18343
|
};
|
|
18114
18344
|
const quoters = /* @__PURE__ */ new Map();
|
|
18115
18345
|
const quoteFor = async (project) => {
|
|
18116
|
-
loadProjectEnv(
|
|
18346
|
+
loadProjectEnv(path35.dirname(path35.resolve(await access2.manifestFor(project))));
|
|
18117
18347
|
const key = process.env.PIXELLAB_API_KEY;
|
|
18118
18348
|
if (!key) return null;
|
|
18119
18349
|
let quoter = quoters.get(key);
|
|
@@ -18164,8 +18394,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18164
18394
|
}
|
|
18165
18395
|
async function runGallery(args) {
|
|
18166
18396
|
if (args.workspace) {
|
|
18167
|
-
const workspacePath =
|
|
18168
|
-
if (!
|
|
18397
|
+
const workspacePath = path35.resolve(args.workspace);
|
|
18398
|
+
if (!existsSync27(workspacePath)) {
|
|
18169
18399
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
18170
18400
|
}
|
|
18171
18401
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -18185,7 +18415,7 @@ async function runGallery(args) {
|
|
|
18185
18415
|
const registered = async (projectId) => {
|
|
18186
18416
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
18187
18417
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
18188
|
-
return resolveProject(
|
|
18418
|
+
return resolveProject(path35.dirname(workspacePath), project);
|
|
18189
18419
|
};
|
|
18190
18420
|
await serveUntilStopped(initial, build2, args, {
|
|
18191
18421
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -18210,7 +18440,7 @@ async function runGallery(args) {
|
|
|
18210
18440
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
18211
18441
|
};
|
|
18212
18442
|
await serveUntilStopped(await build(), reload, args, {
|
|
18213
|
-
manifestFor: () =>
|
|
18443
|
+
manifestFor: () => path35.resolve(args.manifest),
|
|
18214
18444
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
18215
18445
|
});
|
|
18216
18446
|
}
|
|
@@ -18519,18 +18749,18 @@ async function runRestoreGeneration(args) {
|
|
|
18519
18749
|
}
|
|
18520
18750
|
|
|
18521
18751
|
// src/cli/commands/housekeeping.ts
|
|
18522
|
-
import { existsSync as
|
|
18752
|
+
import { existsSync as existsSync29 } from "fs";
|
|
18523
18753
|
|
|
18524
18754
|
// src/pipeline/cache-health.ts
|
|
18525
|
-
import { existsSync as
|
|
18526
|
-
import { readFile as
|
|
18527
|
-
import
|
|
18755
|
+
import { existsSync as existsSync28 } from "fs";
|
|
18756
|
+
import { readFile as readFile26, readdir as readdir2, rm as rm12 } from "fs/promises";
|
|
18757
|
+
import path36 from "path";
|
|
18528
18758
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
18529
|
-
if (options.prune && !
|
|
18530
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
18759
|
+
if (options.prune && !existsSync28(lockPath)) {
|
|
18760
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path36.resolve(lockPath)}`);
|
|
18531
18761
|
}
|
|
18532
|
-
const contentDir =
|
|
18533
|
-
const remotePath =
|
|
18762
|
+
const contentDir = path36.resolve(path36.dirname(lockPath), ".pixelkiln", "cache");
|
|
18763
|
+
const remotePath = path36.resolve(cachePathFor(lockPath));
|
|
18534
18764
|
const referenced = referencedHashes(lock);
|
|
18535
18765
|
let content = await inspectContentCache(contentDir, referenced);
|
|
18536
18766
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -18542,7 +18772,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18542
18772
|
]);
|
|
18543
18773
|
for (const name of names) {
|
|
18544
18774
|
try {
|
|
18545
|
-
await
|
|
18775
|
+
await rm12(path36.join(contentDir, name), { force: true });
|
|
18546
18776
|
removed.contentFiles++;
|
|
18547
18777
|
} catch {
|
|
18548
18778
|
}
|
|
@@ -18551,7 +18781,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18551
18781
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
18552
18782
|
removed.resetRemoteHashCache = true;
|
|
18553
18783
|
} else if (remoteHashes.invalidIds.length) {
|
|
18554
|
-
const cache = parseCache(JSON.parse(await
|
|
18784
|
+
const cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
|
|
18555
18785
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
18556
18786
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
18557
18787
|
await saveCache(remotePath, cache);
|
|
@@ -18569,7 +18799,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18569
18799
|
async function inspectContentCache(contentDir, referenced) {
|
|
18570
18800
|
const report = {
|
|
18571
18801
|
path: contentDir,
|
|
18572
|
-
exists:
|
|
18802
|
+
exists: existsSync28(contentDir),
|
|
18573
18803
|
files: 0,
|
|
18574
18804
|
bytes: 0,
|
|
18575
18805
|
valid: 0,
|
|
@@ -18590,12 +18820,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
18590
18820
|
continue;
|
|
18591
18821
|
}
|
|
18592
18822
|
report.files++;
|
|
18593
|
-
const file =
|
|
18823
|
+
const file = path36.join(contentDir, entry.name);
|
|
18594
18824
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
18595
18825
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
18596
18826
|
let bytes;
|
|
18597
18827
|
try {
|
|
18598
|
-
bytes = await
|
|
18828
|
+
bytes = await readFile26(file);
|
|
18599
18829
|
report.bytes += bytes.length;
|
|
18600
18830
|
} catch (err) {
|
|
18601
18831
|
report.invalid.push({
|
|
@@ -18633,7 +18863,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
18633
18863
|
async function inspectRemoteHashCache(remotePath) {
|
|
18634
18864
|
const report = {
|
|
18635
18865
|
path: remotePath,
|
|
18636
|
-
exists:
|
|
18866
|
+
exists: existsSync28(remotePath),
|
|
18637
18867
|
entries: 0,
|
|
18638
18868
|
valid: 0,
|
|
18639
18869
|
invalidIds: [],
|
|
@@ -18642,7 +18872,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
18642
18872
|
if (!report.exists) return report;
|
|
18643
18873
|
let cache;
|
|
18644
18874
|
try {
|
|
18645
|
-
cache = parseCache(JSON.parse(await
|
|
18875
|
+
cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
|
|
18646
18876
|
} catch (err) {
|
|
18647
18877
|
report.error = err instanceof Error ? err.message : String(err);
|
|
18648
18878
|
return report;
|
|
@@ -18706,7 +18936,7 @@ async function runAccept(args) {
|
|
|
18706
18936
|
let intact = true;
|
|
18707
18937
|
for (const output of entry.outputs) {
|
|
18708
18938
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
18709
|
-
if (!
|
|
18939
|
+
if (!existsSync29(file) || await sha256File(file) !== output.sha256) {
|
|
18710
18940
|
intact = false;
|
|
18711
18941
|
break;
|
|
18712
18942
|
}
|
|
@@ -18761,42 +18991,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
18761
18991
|
}
|
|
18762
18992
|
|
|
18763
18993
|
// src/cli/commands/init.ts
|
|
18764
|
-
import
|
|
18765
|
-
import { existsSync as
|
|
18994
|
+
import path37 from "path";
|
|
18995
|
+
import { existsSync as existsSync30 } from "fs";
|
|
18766
18996
|
async function runInit(args) {
|
|
18767
18997
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
18768
|
-
const root =
|
|
18769
|
-
if (!
|
|
18998
|
+
const root = path37.resolve(args.from);
|
|
18999
|
+
if (!existsSync30(root)) throw new Error(`No directory at ${root}`);
|
|
18770
19000
|
const generator = args.generator ?? "map";
|
|
18771
19001
|
if (generator !== "1dir" && generator !== "map") {
|
|
18772
19002
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
18773
19003
|
}
|
|
18774
|
-
const target =
|
|
19004
|
+
const target = path37.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
18775
19005
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
18776
19006
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
18777
19007
|
const manifest = buildManifest(
|
|
18778
|
-
args.name ??
|
|
19008
|
+
args.name ?? path37.basename(path37.dirname(target)),
|
|
18779
19009
|
args.styles[0] ?? "base",
|
|
18780
19010
|
generator,
|
|
18781
|
-
|
|
19011
|
+
path37.relative(path37.dirname(target), root) || ".",
|
|
18782
19012
|
assets
|
|
18783
19013
|
);
|
|
18784
19014
|
await writeManifestFile(target, manifest);
|
|
18785
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
19015
|
+
log(` scanned ${assets.length} PNG(s) under ${path37.relative(process.cwd(), root)}`);
|
|
18786
19016
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
18787
|
-
log(` wrote ${
|
|
19017
|
+
log(` wrote ${path37.relative(process.cwd(), target)}`);
|
|
18788
19018
|
log(`
|
|
18789
19019
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
18790
19020
|
log(` PixelLab account instead of inventing them:`);
|
|
18791
19021
|
log(`
|
|
18792
|
-
pixelkiln adopt --manifest ${
|
|
19022
|
+
pixelkiln adopt --manifest ${path37.relative(process.cwd(), target)} --write-prompts
|
|
18793
19023
|
`);
|
|
18794
19024
|
}
|
|
18795
19025
|
|
|
18796
19026
|
// src/pipeline/doctor.ts
|
|
18797
|
-
import { constants, existsSync as
|
|
19027
|
+
import { constants, existsSync as existsSync31 } from "fs";
|
|
18798
19028
|
import { access, stat as stat4 } from "fs/promises";
|
|
18799
|
-
import
|
|
19029
|
+
import path38 from "path";
|
|
18800
19030
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
18801
19031
|
const checks = [];
|
|
18802
19032
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -18807,8 +19037,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18807
19037
|
);
|
|
18808
19038
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
18809
19039
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
18810
|
-
|
|
18811
|
-
...spec.quality ? [
|
|
19040
|
+
path38.dirname(spec.outFile),
|
|
19041
|
+
...spec.quality ? [path38.dirname(spec.quality.outFile)] : []
|
|
18812
19042
|
]))];
|
|
18813
19043
|
const unwritable = [];
|
|
18814
19044
|
for (const dir of dirs) {
|
|
@@ -18831,7 +19061,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18831
19061
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
18832
19062
|
const output = entry.outputs[index];
|
|
18833
19063
|
const spec = specByKey.get(key);
|
|
18834
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
19064
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path38.dirname(lockPath));
|
|
18835
19065
|
const owner = outputOwners.get(absolute);
|
|
18836
19066
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
18837
19067
|
outputOwners.set(absolute, key);
|
|
@@ -18846,7 +19076,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18846
19076
|
} else {
|
|
18847
19077
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
18848
19078
|
}
|
|
18849
|
-
const cacheDir =
|
|
19079
|
+
const cacheDir = path38.join(path38.dirname(lockPath), ".pixelkiln", "cache");
|
|
18850
19080
|
const stranded = [];
|
|
18851
19081
|
const invalidState = [];
|
|
18852
19082
|
const staleMapUrls = [];
|
|
@@ -18862,11 +19092,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18862
19092
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
18863
19093
|
let hasCache = false;
|
|
18864
19094
|
for (const output of entry.outputs) {
|
|
18865
|
-
const cached =
|
|
19095
|
+
const cached = path38.join(
|
|
18866
19096
|
cacheDir,
|
|
18867
19097
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
18868
19098
|
);
|
|
18869
|
-
if (
|
|
19099
|
+
if (existsSync31(cached) && await sha256File(cached) === output.sha256) {
|
|
18870
19100
|
hasCache = true;
|
|
18871
19101
|
break;
|
|
18872
19102
|
}
|
|
@@ -18962,16 +19192,16 @@ async function nearestExistingDirectory(start) {
|
|
|
18962
19192
|
if ((await stat4(current)).isDirectory()) return current;
|
|
18963
19193
|
} catch {
|
|
18964
19194
|
}
|
|
18965
|
-
const parent =
|
|
19195
|
+
const parent = path38.dirname(current);
|
|
18966
19196
|
if (parent === current) return current;
|
|
18967
19197
|
current = parent;
|
|
18968
19198
|
}
|
|
18969
19199
|
}
|
|
18970
19200
|
|
|
18971
19201
|
// src/pipeline/audit.ts
|
|
18972
|
-
import { readFile as
|
|
18973
|
-
import { existsSync as
|
|
18974
|
-
import
|
|
19202
|
+
import { readFile as readFile27 } from "fs/promises";
|
|
19203
|
+
import { existsSync as existsSync32 } from "fs";
|
|
19204
|
+
import path39 from "path";
|
|
18975
19205
|
function paletteDistance(asset, reference) {
|
|
18976
19206
|
if (!asset.length || !reference.length) return 0;
|
|
18977
19207
|
let total = 0;
|
|
@@ -19007,12 +19237,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
19007
19237
|
const unreadable = [];
|
|
19008
19238
|
for (const spec of mine) {
|
|
19009
19239
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
19010
|
-
if (!
|
|
19240
|
+
if (!existsSync32(output.absolutePath)) {
|
|
19011
19241
|
missing.push(output.id);
|
|
19012
19242
|
continue;
|
|
19013
19243
|
}
|
|
19014
19244
|
try {
|
|
19015
|
-
const png = decodePng(await
|
|
19245
|
+
const png = decodePng(await readFile27(output.absolutePath));
|
|
19016
19246
|
const palette = extractPalette(png, 12);
|
|
19017
19247
|
assets.push({
|
|
19018
19248
|
assetId: spec.assetId,
|
|
@@ -19036,10 +19266,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
19036
19266
|
let referenceFromStyleImages = false;
|
|
19037
19267
|
const refPalettes = [];
|
|
19038
19268
|
for (const img of style.styleImages) {
|
|
19039
|
-
const abs =
|
|
19040
|
-
if (!
|
|
19269
|
+
const abs = path39.resolve(loaded.root, img.path);
|
|
19270
|
+
if (!existsSync32(abs)) continue;
|
|
19041
19271
|
try {
|
|
19042
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
19272
|
+
refPalettes.push(extractPalette(decodePng(await readFile27(abs)), 12));
|
|
19043
19273
|
} catch {
|
|
19044
19274
|
}
|
|
19045
19275
|
}
|
|
@@ -19267,24 +19497,24 @@ async function runAudit(args) {
|
|
|
19267
19497
|
}
|
|
19268
19498
|
|
|
19269
19499
|
// src/cli/commands/pack.ts
|
|
19270
|
-
import
|
|
19271
|
-
import { readFile as
|
|
19500
|
+
import path41 from "path";
|
|
19501
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
19272
19502
|
|
|
19273
19503
|
// src/pipeline/pack.ts
|
|
19274
19504
|
import { readFileSync as readFileSync5 } from "fs";
|
|
19275
|
-
import
|
|
19505
|
+
import path40 from "path";
|
|
19276
19506
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
19277
19507
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
19278
19508
|
if (!Array.isArray(raw) || !raw.length) {
|
|
19279
19509
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
19280
19510
|
}
|
|
19281
|
-
const baseDir =
|
|
19511
|
+
const baseDir = path40.dirname(path40.resolve(inputsFilePath));
|
|
19282
19512
|
return raw.map((entry, i) => {
|
|
19283
19513
|
const e = entry;
|
|
19284
19514
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
19285
19515
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
19286
19516
|
}
|
|
19287
|
-
return { id: e.id, path:
|
|
19517
|
+
return { id: e.id, path: path40.resolve(baseDir, e.path) };
|
|
19288
19518
|
});
|
|
19289
19519
|
}
|
|
19290
19520
|
function packSprites(inputs, options = {}) {
|
|
@@ -19368,7 +19598,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19368
19598
|
}
|
|
19369
19599
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
19370
19600
|
id,
|
|
19371
|
-
path:
|
|
19601
|
+
path: path40.resolve(source)
|
|
19372
19602
|
}));
|
|
19373
19603
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
19374
19604
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -19392,7 +19622,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19392
19622
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
19393
19623
|
const source = sources[id];
|
|
19394
19624
|
if (source && outputs.length <= 1) {
|
|
19395
|
-
inputs.push({ id, path:
|
|
19625
|
+
inputs.push({ id, path: path40.resolve(manifestDir, source) });
|
|
19396
19626
|
continue;
|
|
19397
19627
|
}
|
|
19398
19628
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -19426,7 +19656,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19426
19656
|
}
|
|
19427
19657
|
}
|
|
19428
19658
|
for (const [id, source] of Object.entries(sources)) {
|
|
19429
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
19659
|
+
if (!locked.has(id)) inputs.push({ id, path: path40.resolve(manifestDir, source) });
|
|
19430
19660
|
}
|
|
19431
19661
|
if (!inputs.length) {
|
|
19432
19662
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -19534,7 +19764,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19534
19764
|
const source = sources[id];
|
|
19535
19765
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
19536
19766
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
19537
|
-
placements.push({ id, path:
|
|
19767
|
+
placements.push({ id, path: path40.resolve(manifestDir, source), cell });
|
|
19538
19768
|
continue;
|
|
19539
19769
|
}
|
|
19540
19770
|
if (source && entry) {
|
|
@@ -19567,7 +19797,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19567
19797
|
`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}`
|
|
19568
19798
|
);
|
|
19569
19799
|
}
|
|
19570
|
-
const basePng = mount.base ? readFileSync5(
|
|
19800
|
+
const basePng = mount.base ? readFileSync5(path40.resolve(manifestDir, mount.base)) : void 0;
|
|
19571
19801
|
const mounted = mountSprites(placements, {
|
|
19572
19802
|
cellWidth: mount.cellWidth,
|
|
19573
19803
|
cellHeight: mount.cellHeight,
|
|
@@ -19577,7 +19807,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19577
19807
|
...mounted,
|
|
19578
19808
|
sources: mount.base ? [{
|
|
19579
19809
|
id: "$base",
|
|
19580
|
-
path:
|
|
19810
|
+
path: path40.resolve(manifestDir, mount.base),
|
|
19581
19811
|
sha256: sha256(basePng),
|
|
19582
19812
|
included: true
|
|
19583
19813
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -19981,12 +20211,12 @@ async function runPack(args) {
|
|
|
19981
20211
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
19982
20212
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
19983
20213
|
}
|
|
19984
|
-
const raw = JSON.parse(await
|
|
20214
|
+
const raw = JSON.parse(await readFile28(path41.resolve(args.inputs), "utf8"));
|
|
19985
20215
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
19986
20216
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
19987
20217
|
const format = sheetFormat(args);
|
|
19988
|
-
const base =
|
|
19989
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
20218
|
+
const base = path41.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
20219
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path41.basename(`${base}.png`) });
|
|
19990
20220
|
const outputs = [
|
|
19991
20221
|
{ path: `${base}.png`, data: png },
|
|
19992
20222
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19999,7 +20229,7 @@ async function runPack(args) {
|
|
|
19999
20229
|
log(
|
|
20000
20230
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
|
|
20001
20231
|
);
|
|
20002
|
-
log(` ${
|
|
20232
|
+
log(` ${path41.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
20003
20233
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20004
20234
|
return;
|
|
20005
20235
|
}
|
|
@@ -20007,7 +20237,7 @@ async function runPack(args) {
|
|
|
20007
20237
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
20008
20238
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
20009
20239
|
}
|
|
20010
|
-
const manifestDir =
|
|
20240
|
+
const manifestDir = path41.dirname(path41.resolve(args.manifest));
|
|
20011
20241
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
20012
20242
|
for (const styleId of styleIds) {
|
|
20013
20243
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -20022,9 +20252,9 @@ async function runPack(args) {
|
|
|
20022
20252
|
sourceOverrides: qualitySources,
|
|
20023
20253
|
sources: manifestSources(loaded.manifest, styleId)
|
|
20024
20254
|
});
|
|
20025
|
-
const base = args.out ?
|
|
20255
|
+
const base = args.out ? path41.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path41.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
20026
20256
|
const format = sheetFormat(args);
|
|
20027
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
20257
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path41.basename(`${base}.png`) });
|
|
20028
20258
|
const outputs = [
|
|
20029
20259
|
{ path: `${base}.png`, data: png },
|
|
20030
20260
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -20056,13 +20286,13 @@ async function runPack(args) {
|
|
|
20056
20286
|
log(
|
|
20057
20287
|
` ${styleId}: ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)` + (atlas.sets?.length ? `, ${atlas.sets.length} set(s)` : "") + ` (${format})`
|
|
20058
20288
|
);
|
|
20059
|
-
log(` ${
|
|
20289
|
+
log(` ${path41.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
20060
20290
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20061
20291
|
}
|
|
20062
20292
|
}
|
|
20063
20293
|
async function runMount(args) {
|
|
20064
20294
|
const { loaded, lock } = await openProject2(args);
|
|
20065
|
-
const manifestDir =
|
|
20295
|
+
const manifestDir = path41.dirname(path41.resolve(args.manifest));
|
|
20066
20296
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
20067
20297
|
for (const styleId of styleIds) {
|
|
20068
20298
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -20101,9 +20331,9 @@ async function runMount(args) {
|
|
|
20101
20331
|
sources,
|
|
20102
20332
|
outputRoles
|
|
20103
20333
|
);
|
|
20104
|
-
const out =
|
|
20334
|
+
const out = path41.resolve(manifestDir, style.mount.out);
|
|
20105
20335
|
const format = sheetFormat(args);
|
|
20106
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
20336
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path41.basename(out) });
|
|
20107
20337
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
20108
20338
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
20109
20339
|
const outputs = [
|
|
@@ -20123,7 +20353,7 @@ async function runMount(args) {
|
|
|
20123
20353
|
await provenanceFile("$lock", args.lock),
|
|
20124
20354
|
...qualityRecords,
|
|
20125
20355
|
...artifactSources.filter(
|
|
20126
|
-
(source) => source.id !== "$base" ||
|
|
20356
|
+
(source) => source.id !== "$base" || path41.resolve(source.path) !== out
|
|
20127
20357
|
)
|
|
20128
20358
|
],
|
|
20129
20359
|
options: {
|
|
@@ -20137,14 +20367,14 @@ async function runMount(args) {
|
|
|
20137
20367
|
log(
|
|
20138
20368
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
20139
20369
|
);
|
|
20140
|
-
log(` ${
|
|
20370
|
+
log(` ${path41.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
20141
20371
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20142
20372
|
}
|
|
20143
20373
|
}
|
|
20144
20374
|
async function runExport(args) {
|
|
20145
20375
|
const { loaded, specs, lock } = await openProject2(args);
|
|
20146
20376
|
const format = args.format ?? "generic";
|
|
20147
|
-
const manifestDir =
|
|
20377
|
+
const manifestDir = path41.dirname(path41.resolve(args.manifest));
|
|
20148
20378
|
const selected = specs.filter((spec) => {
|
|
20149
20379
|
if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
|
|
20150
20380
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -20160,12 +20390,12 @@ async function runExport(args) {
|
|
|
20160
20390
|
for (const spec of selected) {
|
|
20161
20391
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
20162
20392
|
const style = loaded.manifest.styles[spec.styleId];
|
|
20163
|
-
const defaultBase =
|
|
20164
|
-
const base = args.out ?
|
|
20393
|
+
const defaultBase = path41.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
|
|
20394
|
+
const base = args.out ? path41.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
20165
20395
|
const result = exportTileset(entry, spec, {
|
|
20166
20396
|
format,
|
|
20167
20397
|
manifestDir,
|
|
20168
|
-
imageName:
|
|
20398
|
+
imageName: path41.basename(`${base}.png`),
|
|
20169
20399
|
columns: args.columns
|
|
20170
20400
|
});
|
|
20171
20401
|
const outputs = [
|
|
@@ -20183,7 +20413,7 @@ async function runExport(args) {
|
|
|
20183
20413
|
asset: spec.assetId,
|
|
20184
20414
|
columns: args.columns ?? null,
|
|
20185
20415
|
format,
|
|
20186
|
-
image:
|
|
20416
|
+
image: path41.basename(`${base}.png`),
|
|
20187
20417
|
providerRules: result.generic.providerRules,
|
|
20188
20418
|
style: spec.styleId,
|
|
20189
20419
|
tileType: spec.tileType ?? null
|
|
@@ -20193,7 +20423,7 @@ async function runExport(args) {
|
|
|
20193
20423
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
20194
20424
|
);
|
|
20195
20425
|
log(
|
|
20196
|
-
` ${
|
|
20426
|
+
` ${path41.relative(process.cwd(), base)}.png + ${path41.basename(base)}${result.extension} + .pixelkiln.json`
|
|
20197
20427
|
);
|
|
20198
20428
|
}
|
|
20199
20429
|
}
|
|
@@ -20206,23 +20436,23 @@ function sheetFormat(args) {
|
|
|
20206
20436
|
}
|
|
20207
20437
|
|
|
20208
20438
|
// src/cli/commands/pixellab-utils.ts
|
|
20209
|
-
import
|
|
20439
|
+
import path43 from "path";
|
|
20210
20440
|
|
|
20211
20441
|
// src/pixellab-utils.ts
|
|
20212
|
-
import { existsSync as
|
|
20213
|
-
import { mkdir as mkdir13, readFile as
|
|
20214
|
-
import
|
|
20442
|
+
import { existsSync as existsSync33 } from "fs";
|
|
20443
|
+
import { mkdir as mkdir13, readFile as readFile29, writeFile as writeFile18 } from "fs/promises";
|
|
20444
|
+
import path42 from "path";
|
|
20215
20445
|
var UNZOOM_MIN_SIDE = 256;
|
|
20216
20446
|
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
20217
20447
|
function defaultUnzoomOut(input) {
|
|
20218
|
-
const ext =
|
|
20448
|
+
const ext = path42.extname(input);
|
|
20219
20449
|
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
20220
20450
|
}
|
|
20221
20451
|
async function unzoomFile(client2, input, opts = {}) {
|
|
20222
|
-
const out =
|
|
20223
|
-
if (
|
|
20224
|
-
if (
|
|
20225
|
-
const bytes = await
|
|
20452
|
+
const out = path42.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
20453
|
+
if (path42.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
20454
|
+
if (existsSync33(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
20455
|
+
const bytes = await readFile29(input);
|
|
20226
20456
|
const metadata = imageMetadata(bytes);
|
|
20227
20457
|
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
20228
20458
|
const { width, height, format } = metadata;
|
|
@@ -20235,22 +20465,22 @@ async function unzoomFile(client2, input, opts = {}) {
|
|
|
20235
20465
|
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
20236
20466
|
}
|
|
20237
20467
|
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
20238
|
-
await mkdir13(
|
|
20239
|
-
await
|
|
20468
|
+
await mkdir13(path42.dirname(out), { recursive: true });
|
|
20469
|
+
await writeFile18(out, res.png);
|
|
20240
20470
|
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
20241
20471
|
}
|
|
20242
20472
|
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
20243
20473
|
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
20244
20474
|
var FONT_COST_GENERATIONS = 25;
|
|
20245
20475
|
function fontOutputPaths(out) {
|
|
20246
|
-
const base =
|
|
20476
|
+
const base = path42.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
20247
20477
|
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
20248
20478
|
}
|
|
20249
20479
|
async function generateFont(client2, opts) {
|
|
20250
20480
|
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
20251
20481
|
if (!opts.force) {
|
|
20252
20482
|
for (const file of [ttf, atlas]) {
|
|
20253
|
-
if (
|
|
20483
|
+
if (existsSync33(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
20254
20484
|
}
|
|
20255
20485
|
}
|
|
20256
20486
|
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -20277,9 +20507,9 @@ async function generateFont(client2, opts) {
|
|
|
20277
20507
|
client2.download(job.download_ttf_url),
|
|
20278
20508
|
client2.download(job.download_atlas_url)
|
|
20279
20509
|
]);
|
|
20280
|
-
await mkdir13(
|
|
20281
|
-
await
|
|
20282
|
-
await
|
|
20510
|
+
await mkdir13(path42.dirname(ttf), { recursive: true });
|
|
20511
|
+
await writeFile18(ttf, ttfBytes);
|
|
20512
|
+
await writeFile18(atlas, atlasBytes);
|
|
20283
20513
|
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
20284
20514
|
}
|
|
20285
20515
|
if (Date.now() - started > timeout) {
|
|
@@ -20293,7 +20523,7 @@ async function generateFont(client2, opts) {
|
|
|
20293
20523
|
|
|
20294
20524
|
// src/cli/commands/pixellab-utils.ts
|
|
20295
20525
|
function loadKeys(args) {
|
|
20296
|
-
loadEnvFiles(
|
|
20526
|
+
loadEnvFiles(path43.dirname(path43.resolve(args.manifest)));
|
|
20297
20527
|
loadEnvFiles(process.cwd());
|
|
20298
20528
|
}
|
|
20299
20529
|
async function runUnzoom(args) {
|
|
@@ -20359,19 +20589,19 @@ async function runFont(args) {
|
|
|
20359
20589
|
}
|
|
20360
20590
|
|
|
20361
20591
|
// src/cli/commands/quality.ts
|
|
20362
|
-
import
|
|
20363
|
-
import { readFile as
|
|
20592
|
+
import path45 from "path";
|
|
20593
|
+
import { readFile as readFile31 } from "fs/promises";
|
|
20364
20594
|
|
|
20365
20595
|
// src/pipeline/quality-regression.ts
|
|
20366
|
-
import
|
|
20367
|
-
import { existsSync as
|
|
20368
|
-
import { readFile as
|
|
20369
|
-
import { z as
|
|
20596
|
+
import path44 from "path";
|
|
20597
|
+
import { existsSync as existsSync34 } from "fs";
|
|
20598
|
+
import { readFile as readFile30 } from "fs/promises";
|
|
20599
|
+
import { z as z13 } from "zod";
|
|
20370
20600
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
20371
20601
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
20372
20602
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
20373
|
-
var QualityPathSchema =
|
|
20374
|
-
(value) => !
|
|
20603
|
+
var QualityPathSchema = z13.string().min(1).refine(
|
|
20604
|
+
(value) => !path44.posix.isAbsolute(value) && !value.includes("\\"),
|
|
20375
20605
|
"expected a portable relative path"
|
|
20376
20606
|
);
|
|
20377
20607
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -20384,53 +20614,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
20384
20614
|
maxEdgeContrastDrop: 0.03,
|
|
20385
20615
|
maxIsolatedPixelIncrease: 5e-3
|
|
20386
20616
|
};
|
|
20387
|
-
var QualityToleranceSchema =
|
|
20388
|
-
requireExactHash:
|
|
20389
|
-
maxColorCountIncrease:
|
|
20390
|
-
maxNewColors:
|
|
20391
|
-
maxTransparencyDelta:
|
|
20392
|
-
maxPartialAlphaIncrease:
|
|
20393
|
-
maxEdgeDensityDelta:
|
|
20394
|
-
maxEdgeContrastDrop:
|
|
20395
|
-
maxIsolatedPixelIncrease:
|
|
20617
|
+
var QualityToleranceSchema = z13.object({
|
|
20618
|
+
requireExactHash: z13.boolean(),
|
|
20619
|
+
maxColorCountIncrease: z13.number().int().min(0),
|
|
20620
|
+
maxNewColors: z13.number().int().min(0),
|
|
20621
|
+
maxTransparencyDelta: z13.number().min(0).max(1),
|
|
20622
|
+
maxPartialAlphaIncrease: z13.number().min(0).max(1),
|
|
20623
|
+
maxEdgeDensityDelta: z13.number().min(0).max(1),
|
|
20624
|
+
maxEdgeContrastDrop: z13.number().min(0).max(1),
|
|
20625
|
+
maxIsolatedPixelIncrease: z13.number().min(0).max(1)
|
|
20396
20626
|
}).strict();
|
|
20397
|
-
var ImageQualityMetricsSchema =
|
|
20398
|
-
sha256:
|
|
20399
|
-
width:
|
|
20400
|
-
height:
|
|
20401
|
-
colorCount:
|
|
20402
|
-
colors:
|
|
20403
|
-
transparency:
|
|
20404
|
-
partialAlpha:
|
|
20405
|
-
edgeDensity:
|
|
20406
|
-
meanEdgeContrast:
|
|
20407
|
-
isolatedPixelRatio:
|
|
20627
|
+
var ImageQualityMetricsSchema = z13.object({
|
|
20628
|
+
sha256: z13.string().regex(SHA256_RE),
|
|
20629
|
+
width: z13.number().int().min(1),
|
|
20630
|
+
height: z13.number().int().min(1),
|
|
20631
|
+
colorCount: z13.number().int().min(0),
|
|
20632
|
+
colors: z13.array(z13.string().regex(HEX_RE)),
|
|
20633
|
+
transparency: z13.number().min(0).max(1),
|
|
20634
|
+
partialAlpha: z13.number().min(0).max(1),
|
|
20635
|
+
edgeDensity: z13.number().min(0).max(1),
|
|
20636
|
+
meanEdgeContrast: z13.number().min(0).max(1),
|
|
20637
|
+
isolatedPixelRatio: z13.number().min(0).max(1)
|
|
20408
20638
|
}).strict();
|
|
20409
|
-
var QualityBaselineCaseSchema =
|
|
20410
|
-
id:
|
|
20639
|
+
var QualityBaselineCaseSchema = z13.object({
|
|
20640
|
+
id: z13.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
20411
20641
|
file: QualityPathSchema,
|
|
20412
|
-
record:
|
|
20642
|
+
record: z13.object({
|
|
20413
20643
|
path: QualityPathSchema,
|
|
20414
|
-
sha256:
|
|
20644
|
+
sha256: z13.string().regex(SHA256_RE)
|
|
20415
20645
|
}).strict().optional(),
|
|
20416
20646
|
expected: ImageQualityMetricsSchema,
|
|
20417
20647
|
tolerances: QualityToleranceSchema
|
|
20418
20648
|
}).strict();
|
|
20419
|
-
var QualityBaselineSchema =
|
|
20420
|
-
$schema:
|
|
20421
|
-
format:
|
|
20422
|
-
schemaVersion:
|
|
20423
|
-
cases:
|
|
20649
|
+
var QualityBaselineSchema = z13.object({
|
|
20650
|
+
$schema: z13.string().url().optional(),
|
|
20651
|
+
format: z13.literal("pixelkiln-quality-baseline"),
|
|
20652
|
+
schemaVersion: z13.literal(1),
|
|
20653
|
+
cases: z13.array(QualityBaselineCaseSchema).min(1)
|
|
20424
20654
|
}).strict().superRefine((baseline, context) => {
|
|
20425
20655
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
20426
20656
|
if (new Set(ids).size !== ids.length) {
|
|
20427
|
-
context.addIssue({ code:
|
|
20657
|
+
context.addIssue({ code: z13.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
20428
20658
|
}
|
|
20429
20659
|
});
|
|
20430
|
-
var QualityInputSchema =
|
|
20431
|
-
id:
|
|
20432
|
-
path:
|
|
20433
|
-
record:
|
|
20660
|
+
var QualityInputSchema = z13.object({
|
|
20661
|
+
id: z13.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
20662
|
+
path: z13.string().min(1),
|
|
20663
|
+
record: z13.string().min(1).optional(),
|
|
20434
20664
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
20435
20665
|
}).strict();
|
|
20436
20666
|
function message4(error) {
|
|
@@ -20518,10 +20748,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
20518
20748
|
};
|
|
20519
20749
|
}
|
|
20520
20750
|
async function measureImageQuality(file) {
|
|
20521
|
-
const absolute =
|
|
20751
|
+
const absolute = path44.resolve(file);
|
|
20522
20752
|
let bytes;
|
|
20523
20753
|
try {
|
|
20524
|
-
bytes = await
|
|
20754
|
+
bytes = await readFile30(absolute);
|
|
20525
20755
|
} catch (error) {
|
|
20526
20756
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
20527
20757
|
}
|
|
@@ -20532,7 +20762,7 @@ async function measureImageQuality(file) {
|
|
|
20532
20762
|
}
|
|
20533
20763
|
}
|
|
20534
20764
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
20535
|
-
const parsed =
|
|
20765
|
+
const parsed = z13.array(QualityInputSchema).min(1).safeParse(raw);
|
|
20536
20766
|
if (!parsed.success) {
|
|
20537
20767
|
throw new Error(
|
|
20538
20768
|
"--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
@@ -20540,11 +20770,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20540
20770
|
}
|
|
20541
20771
|
const ids = parsed.data.map((entry) => entry.id);
|
|
20542
20772
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
20543
|
-
const root =
|
|
20773
|
+
const root = path44.dirname(path44.resolve(inputsFilePath));
|
|
20544
20774
|
return parsed.data.map((entry) => ({
|
|
20545
20775
|
id: entry.id,
|
|
20546
|
-
file:
|
|
20547
|
-
...entry.record ? { record:
|
|
20776
|
+
file: path44.resolve(root, entry.path),
|
|
20777
|
+
...entry.record ? { record: path44.resolve(root, entry.record) } : {},
|
|
20548
20778
|
tolerances: QualityToleranceSchema.parse({
|
|
20549
20779
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
20550
20780
|
...entry.tolerances
|
|
@@ -20552,7 +20782,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20552
20782
|
}));
|
|
20553
20783
|
}
|
|
20554
20784
|
function portableRelative2(from, to) {
|
|
20555
|
-
return
|
|
20785
|
+
return path44.relative(from, path44.resolve(to)).split(path44.sep).join("/") || ".";
|
|
20556
20786
|
}
|
|
20557
20787
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
20558
20788
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -20567,20 +20797,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
20567
20797
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
20568
20798
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
20569
20799
|
}
|
|
20570
|
-
const outputs = record.outputs.map((output) =>
|
|
20571
|
-
if (!outputs.includes(
|
|
20800
|
+
const outputs = record.outputs.map((output) => path44.resolve(path44.dirname(recordPath), output.path));
|
|
20801
|
+
if (!outputs.includes(path44.resolve(imagePath))) {
|
|
20572
20802
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
20573
20803
|
}
|
|
20574
20804
|
}
|
|
20575
20805
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
20576
20806
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
20577
|
-
const absolute =
|
|
20807
|
+
const absolute = path44.resolve(baselinePath);
|
|
20578
20808
|
for (const input of inputs) {
|
|
20579
|
-
if (
|
|
20809
|
+
if (path44.resolve(input.file) === absolute || input.record && path44.resolve(input.record) === absolute) {
|
|
20580
20810
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
20581
20811
|
}
|
|
20582
20812
|
}
|
|
20583
|
-
const root =
|
|
20813
|
+
const root = path44.dirname(absolute);
|
|
20584
20814
|
const cases = [];
|
|
20585
20815
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
20586
20816
|
const expected = await measureImageQuality(input.file);
|
|
@@ -20607,8 +20837,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
20607
20837
|
cases
|
|
20608
20838
|
});
|
|
20609
20839
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
20610
|
-
if (
|
|
20611
|
-
const current = await
|
|
20840
|
+
if (existsSync34(absolute) && !options.force) {
|
|
20841
|
+
const current = await readFile30(absolute);
|
|
20612
20842
|
if (!current.equals(data)) {
|
|
20613
20843
|
throw new OverwriteRefusedError(
|
|
20614
20844
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -20620,10 +20850,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
20620
20850
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
20621
20851
|
}
|
|
20622
20852
|
async function readQualityBaseline(baselinePath) {
|
|
20623
|
-
const absolute =
|
|
20853
|
+
const absolute = path44.resolve(baselinePath);
|
|
20624
20854
|
let raw;
|
|
20625
20855
|
try {
|
|
20626
|
-
raw = JSON.parse(await
|
|
20856
|
+
raw = JSON.parse(await readFile30(absolute, "utf8"));
|
|
20627
20857
|
} catch (error) {
|
|
20628
20858
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
20629
20859
|
}
|
|
@@ -20693,12 +20923,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
20693
20923
|
return { violations, warnings };
|
|
20694
20924
|
}
|
|
20695
20925
|
async function checkQualityBaseline(baselinePath) {
|
|
20696
|
-
const absolute =
|
|
20926
|
+
const absolute = path44.resolve(baselinePath);
|
|
20697
20927
|
const baseline = await readQualityBaseline(absolute);
|
|
20698
|
-
const root =
|
|
20928
|
+
const root = path44.dirname(absolute);
|
|
20699
20929
|
const cases = [];
|
|
20700
20930
|
for (const entry of baseline.cases) {
|
|
20701
|
-
const file =
|
|
20931
|
+
const file = path44.resolve(root, entry.file);
|
|
20702
20932
|
let actual = null;
|
|
20703
20933
|
const violations = [];
|
|
20704
20934
|
const warnings = [];
|
|
@@ -20711,7 +20941,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
20711
20941
|
violations.push(message4(error));
|
|
20712
20942
|
}
|
|
20713
20943
|
if (entry.record) {
|
|
20714
|
-
const recordPath =
|
|
20944
|
+
const recordPath = path44.resolve(root, entry.record.path);
|
|
20715
20945
|
try {
|
|
20716
20946
|
const recordHash = await sha256File(recordPath);
|
|
20717
20947
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -20753,15 +20983,15 @@ async function runQuality(args) {
|
|
|
20753
20983
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
20754
20984
|
let raw;
|
|
20755
20985
|
try {
|
|
20756
|
-
raw = JSON.parse(await
|
|
20986
|
+
raw = JSON.parse(await readFile31(path45.resolve(args.inputs), "utf8"));
|
|
20757
20987
|
} catch (error) {
|
|
20758
20988
|
throw new Error(
|
|
20759
|
-
`Could not read quality inputs ${
|
|
20989
|
+
`Could not read quality inputs ${path45.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
20760
20990
|
{ cause: error }
|
|
20761
20991
|
);
|
|
20762
20992
|
}
|
|
20763
20993
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
20764
|
-
if (
|
|
20994
|
+
if (path45.resolve(args.inputs) === path45.resolve(args.out)) {
|
|
20765
20995
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
20766
20996
|
}
|
|
20767
20997
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -20774,7 +21004,7 @@ async function runQuality(args) {
|
|
|
20774
21004
|
}, null, 2));
|
|
20775
21005
|
} else {
|
|
20776
21006
|
log(
|
|
20777
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
21007
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path45.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
20778
21008
|
);
|
|
20779
21009
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
20780
21010
|
}
|
|
@@ -20800,54 +21030,54 @@ async function runQuality(args) {
|
|
|
20800
21030
|
}
|
|
20801
21031
|
|
|
20802
21032
|
// src/cli/commands/recipe.ts
|
|
20803
|
-
import
|
|
21033
|
+
import path47 from "path";
|
|
20804
21034
|
|
|
20805
21035
|
// src/recipes.ts
|
|
20806
|
-
import
|
|
20807
|
-
import { existsSync as
|
|
20808
|
-
import { readdir as readdir3, readFile as
|
|
21036
|
+
import path46 from "path";
|
|
21037
|
+
import { existsSync as existsSync35 } from "fs";
|
|
21038
|
+
import { readdir as readdir3, readFile as readFile32 } from "fs/promises";
|
|
20809
21039
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
20810
|
-
import { z as
|
|
21040
|
+
import { z as z14 } from "zod";
|
|
20811
21041
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
20812
21042
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
20813
21043
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
20814
21044
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
20815
21045
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
20816
|
-
var PortablePathSchema =
|
|
20817
|
-
if (value.includes("\\") ||
|
|
21046
|
+
var PortablePathSchema = z14.string().min(1).refine((value) => {
|
|
21047
|
+
if (value.includes("\\") || path46.posix.isAbsolute(value)) return false;
|
|
20818
21048
|
const parts = value.split("/");
|
|
20819
21049
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
20820
21050
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
20821
|
-
var RecipeBindingSchema =
|
|
20822
|
-
nodeId:
|
|
20823
|
-
input:
|
|
21051
|
+
var RecipeBindingSchema = z14.object({
|
|
21052
|
+
nodeId: z14.string().min(1),
|
|
21053
|
+
input: z14.string().min(1)
|
|
20824
21054
|
}).strict();
|
|
20825
|
-
var RecipeSchema =
|
|
20826
|
-
$schema:
|
|
20827
|
-
format:
|
|
20828
|
-
schemaVersion:
|
|
20829
|
-
id:
|
|
20830
|
-
version:
|
|
20831
|
-
provider:
|
|
20832
|
-
summary:
|
|
20833
|
-
files:
|
|
21055
|
+
var RecipeSchema = z14.object({
|
|
21056
|
+
$schema: z14.string().url().optional(),
|
|
21057
|
+
format: z14.literal("pixelkiln-recipe"),
|
|
21058
|
+
schemaVersion: z14.literal(1),
|
|
21059
|
+
id: z14.string().regex(RECIPE_ID_RE, "expected provider/name"),
|
|
21060
|
+
version: z14.string().regex(VERSION_RE, "expected x.y.z"),
|
|
21061
|
+
provider: z14.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
21062
|
+
summary: z14.string().min(1).max(240),
|
|
21063
|
+
files: z14.array(z14.object({
|
|
20834
21064
|
path: PortablePathSchema,
|
|
20835
|
-
role:
|
|
20836
|
-
sha256:
|
|
21065
|
+
role: z14.enum(["workflow", "reference"]),
|
|
21066
|
+
sha256: z14.string().regex(SHA256_RE2)
|
|
20837
21067
|
}).strict()).default([]),
|
|
20838
|
-
models:
|
|
21068
|
+
models: z14.array(z14.object({
|
|
20839
21069
|
path: PortablePathSchema,
|
|
20840
|
-
sha256:
|
|
20841
|
-
source:
|
|
20842
|
-
license:
|
|
21070
|
+
sha256: z14.string().regex(SHA256_RE2),
|
|
21071
|
+
source: z14.string().url(),
|
|
21072
|
+
license: z14.string().min(1)
|
|
20843
21073
|
}).strict()).default([]),
|
|
20844
|
-
styleId:
|
|
21074
|
+
styleId: z14.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20845
21075
|
style: StyleSchema,
|
|
20846
|
-
workflow:
|
|
21076
|
+
workflow: z14.object({
|
|
20847
21077
|
path: PortablePathSchema,
|
|
20848
|
-
outputNodeId:
|
|
20849
|
-
numImages:
|
|
20850
|
-
bindings:
|
|
21078
|
+
outputNodeId: z14.string().min(1),
|
|
21079
|
+
numImages: z14.number().int().min(1).max(16),
|
|
21080
|
+
bindings: z14.object({
|
|
20851
21081
|
prompt: RecipeBindingSchema,
|
|
20852
21082
|
width: RecipeBindingSchema.optional(),
|
|
20853
21083
|
height: RecipeBindingSchema.optional(),
|
|
@@ -20858,67 +21088,67 @@ var RecipeSchema = z13.object({
|
|
|
20858
21088
|
strength: RecipeBindingSchema.optional()
|
|
20859
21089
|
}).strict()
|
|
20860
21090
|
}).strict().optional(),
|
|
20861
|
-
quality:
|
|
20862
|
-
stage:
|
|
20863
|
-
workingCanvas:
|
|
20864
|
-
width:
|
|
20865
|
-
height:
|
|
21091
|
+
quality: z14.object({
|
|
21092
|
+
stage: z14.enum(["composition-source", "production-candidate"]),
|
|
21093
|
+
workingCanvas: z14.object({
|
|
21094
|
+
width: z14.number().int().min(16),
|
|
21095
|
+
height: z14.number().int().min(16)
|
|
20866
21096
|
}).strict(),
|
|
20867
|
-
recommendedNativeSize:
|
|
20868
|
-
min:
|
|
20869
|
-
max:
|
|
21097
|
+
recommendedNativeSize: z14.object({
|
|
21098
|
+
min: z14.number().int().min(1),
|
|
21099
|
+
max: z14.number().int().min(1)
|
|
20870
21100
|
}).strict(),
|
|
20871
|
-
paletteColors:
|
|
20872
|
-
min:
|
|
20873
|
-
max:
|
|
21101
|
+
paletteColors: z14.object({
|
|
21102
|
+
min: z14.number().int().min(1),
|
|
21103
|
+
max: z14.number().int().min(1)
|
|
20874
21104
|
}).strict(),
|
|
20875
|
-
background:
|
|
20876
|
-
checks:
|
|
21105
|
+
background: z14.enum(["opaque", "transparent", "full-bleed"]),
|
|
21106
|
+
checks: z14.array(z14.enum([
|
|
20877
21107
|
"prompt-coverage",
|
|
20878
21108
|
"native-grid",
|
|
20879
21109
|
"final-palette",
|
|
20880
21110
|
"alpha-edge",
|
|
20881
21111
|
"human-1x"
|
|
20882
21112
|
])).min(1),
|
|
20883
|
-
notes:
|
|
21113
|
+
notes: z14.array(z14.string().min(1)).default([])
|
|
20884
21114
|
}).strict(),
|
|
20885
|
-
integrity:
|
|
20886
|
-
algorithm:
|
|
20887
|
-
digest:
|
|
21115
|
+
integrity: z14.object({
|
|
21116
|
+
algorithm: z14.literal("sha256"),
|
|
21117
|
+
digest: z14.string().regex(SHA256_RE2)
|
|
20888
21118
|
}).strict()
|
|
20889
21119
|
}).strict().superRefine((recipe, context) => {
|
|
20890
21120
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
20891
21121
|
context.addIssue({
|
|
20892
|
-
code:
|
|
21122
|
+
code: z14.ZodIssueCode.custom,
|
|
20893
21123
|
path: ["style", "provider"],
|
|
20894
21124
|
message: "must match the recipe provider"
|
|
20895
21125
|
});
|
|
20896
21126
|
}
|
|
20897
21127
|
const filePaths = recipe.files.map((file) => file.path);
|
|
20898
21128
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
20899
|
-
context.addIssue({ code:
|
|
21129
|
+
context.addIssue({ code: z14.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
20900
21130
|
}
|
|
20901
21131
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
20902
21132
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
20903
|
-
context.addIssue({ code:
|
|
21133
|
+
context.addIssue({ code: z14.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
20904
21134
|
}
|
|
20905
21135
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
20906
21136
|
context.addIssue({
|
|
20907
|
-
code:
|
|
21137
|
+
code: z14.ZodIssueCode.custom,
|
|
20908
21138
|
path: ["files"],
|
|
20909
21139
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
20910
21140
|
});
|
|
20911
21141
|
}
|
|
20912
21142
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
20913
21143
|
context.addIssue({
|
|
20914
|
-
code:
|
|
21144
|
+
code: z14.ZodIssueCode.custom,
|
|
20915
21145
|
path: ["quality", "recommendedNativeSize"],
|
|
20916
21146
|
message: "min cannot exceed max"
|
|
20917
21147
|
});
|
|
20918
21148
|
}
|
|
20919
21149
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
20920
21150
|
context.addIssue({
|
|
20921
|
-
code:
|
|
21151
|
+
code: z14.ZodIssueCode.custom,
|
|
20922
21152
|
path: ["quality", "paletteColors"],
|
|
20923
21153
|
message: "min cannot exceed max"
|
|
20924
21154
|
});
|
|
@@ -20927,7 +21157,7 @@ var RecipeSchema = z13.object({
|
|
|
20927
21157
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
20928
21158
|
)) {
|
|
20929
21159
|
context.addIssue({
|
|
20930
|
-
code:
|
|
21160
|
+
code: z14.ZodIssueCode.custom,
|
|
20931
21161
|
path: ["workflow", "path"],
|
|
20932
21162
|
message: "must name a file whose role is workflow"
|
|
20933
21163
|
});
|
|
@@ -20935,7 +21165,7 @@ var RecipeSchema = z13.object({
|
|
|
20935
21165
|
if (recipe.provider === "comfyui") {
|
|
20936
21166
|
if (!recipe.workflow) {
|
|
20937
21167
|
context.addIssue({
|
|
20938
|
-
code:
|
|
21168
|
+
code: z14.ZodIssueCode.custom,
|
|
20939
21169
|
path: ["workflow"],
|
|
20940
21170
|
message: "ComfyUI recipes require workflow metadata"
|
|
20941
21171
|
});
|
|
@@ -20944,7 +21174,7 @@ var RecipeSchema = z13.object({
|
|
|
20944
21174
|
const options = recipe.style.providerOptions.comfyui;
|
|
20945
21175
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
20946
21176
|
context.addIssue({
|
|
20947
|
-
code:
|
|
21177
|
+
code: z14.ZodIssueCode.custom,
|
|
20948
21178
|
path: ["style", "providerOptions", "comfyui"],
|
|
20949
21179
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
20950
21180
|
});
|
|
@@ -20958,7 +21188,7 @@ var RecipeSchema = z13.object({
|
|
|
20958
21188
|
};
|
|
20959
21189
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
20960
21190
|
context.addIssue({
|
|
20961
|
-
code:
|
|
21191
|
+
code: z14.ZodIssueCode.custom,
|
|
20962
21192
|
path: ["style", "providerOptions", "comfyui"],
|
|
20963
21193
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
20964
21194
|
});
|
|
@@ -20990,10 +21220,10 @@ function message5(error) {
|
|
|
20990
21220
|
return error instanceof Error ? error.message : String(error);
|
|
20991
21221
|
}
|
|
20992
21222
|
async function readRecipeFile(recipePath, bundled) {
|
|
20993
|
-
const absolute =
|
|
21223
|
+
const absolute = path46.resolve(recipePath);
|
|
20994
21224
|
let raw;
|
|
20995
21225
|
try {
|
|
20996
|
-
raw = JSON.parse(await
|
|
21226
|
+
raw = JSON.parse(await readFile32(absolute, "utf8"));
|
|
20997
21227
|
} catch (error) {
|
|
20998
21228
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
20999
21229
|
}
|
|
@@ -21004,13 +21234,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
21004
21234
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
21005
21235
|
);
|
|
21006
21236
|
}
|
|
21007
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
21237
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path46.dirname(absolute), bundled };
|
|
21008
21238
|
}
|
|
21009
21239
|
async function walkRecipeFiles(root) {
|
|
21010
|
-
if (!
|
|
21240
|
+
if (!existsSync35(root)) return [];
|
|
21011
21241
|
const out = [];
|
|
21012
21242
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
21013
|
-
const resolved =
|
|
21243
|
+
const resolved = path46.join(root, entry.name);
|
|
21014
21244
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
21015
21245
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
21016
21246
|
}
|
|
@@ -21021,7 +21251,7 @@ function bundledRecipeRoot() {
|
|
|
21021
21251
|
}
|
|
21022
21252
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
21023
21253
|
const recipes = await Promise.all(
|
|
21024
|
-
(await walkRecipeFiles(
|
|
21254
|
+
(await walkRecipeFiles(path46.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
21025
21255
|
);
|
|
21026
21256
|
return recipes.sort(
|
|
21027
21257
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -21044,9 +21274,9 @@ function parseSelector(selector) {
|
|
|
21044
21274
|
return { id: match[1], version: match[2] };
|
|
21045
21275
|
}
|
|
21046
21276
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
21047
|
-
const local =
|
|
21048
|
-
if (
|
|
21049
|
-
const recipePath =
|
|
21277
|
+
const local = path46.resolve(target);
|
|
21278
|
+
if (existsSync35(local)) {
|
|
21279
|
+
const recipePath = path46.basename(local) === RECIPE_FILE ? local : path46.join(local, RECIPE_FILE);
|
|
21050
21280
|
return readRecipeFile(recipePath, false);
|
|
21051
21281
|
}
|
|
21052
21282
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -21060,8 +21290,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
21060
21290
|
return matches[0];
|
|
21061
21291
|
}
|
|
21062
21292
|
async function verifyFile(root, file) {
|
|
21063
|
-
const absolute =
|
|
21064
|
-
if (!
|
|
21293
|
+
const absolute = path46.resolve(root, ...file.path.split("/"));
|
|
21294
|
+
if (!existsSync35(absolute)) {
|
|
21065
21295
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
21066
21296
|
}
|
|
21067
21297
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -21076,7 +21306,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
21076
21306
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
21077
21307
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
21078
21308
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
21079
|
-
const modelRoot = options.modelRoot ?
|
|
21309
|
+
const modelRoot = options.modelRoot ? path46.resolve(options.modelRoot) : null;
|
|
21080
21310
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
21081
21311
|
...await verifyFile(modelRoot, model),
|
|
21082
21312
|
source: model.source,
|
|
@@ -21110,7 +21340,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
21110
21340
|
};
|
|
21111
21341
|
}
|
|
21112
21342
|
function renderStyle(style, destination, cwd) {
|
|
21113
|
-
const portableDir =
|
|
21343
|
+
const portableDir = path46.relative(cwd, destination).split(path46.sep).join("/") || ".";
|
|
21114
21344
|
const replace = (value) => {
|
|
21115
21345
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
21116
21346
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -21122,25 +21352,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
21122
21352
|
return replace(style);
|
|
21123
21353
|
}
|
|
21124
21354
|
async function installRecipe(target, options = {}) {
|
|
21125
|
-
const cwd =
|
|
21355
|
+
const cwd = path46.resolve(options.cwd ?? process.cwd());
|
|
21126
21356
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
21127
21357
|
const verification = await verifyRecipe(loaded.path);
|
|
21128
21358
|
if (!verification.ok) {
|
|
21129
21359
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
21130
21360
|
}
|
|
21131
|
-
const destination = options.out ?
|
|
21361
|
+
const destination = options.out ? path46.resolve(cwd, options.out) : path46.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
21132
21362
|
const sources = [
|
|
21133
21363
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
21134
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
21364
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path46.join(loaded.dir, ...file.path.split("/")) }))
|
|
21135
21365
|
];
|
|
21136
21366
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
21137
|
-
path:
|
|
21138
|
-
data: await
|
|
21367
|
+
path: path46.join(destination, ...file.path.split("/")),
|
|
21368
|
+
data: await readFile32(file.source)
|
|
21139
21369
|
})));
|
|
21140
21370
|
if (!options.force) {
|
|
21141
21371
|
for (const file of files) {
|
|
21142
|
-
if (!
|
|
21143
|
-
const current = await
|
|
21372
|
+
if (!existsSync35(file.path)) continue;
|
|
21373
|
+
const current = await readFile32(file.path);
|
|
21144
21374
|
if (!current.equals(file.data)) {
|
|
21145
21375
|
throw new OverwriteRefusedError(
|
|
21146
21376
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -21234,14 +21464,14 @@ async function runRecipe(args) {
|
|
|
21234
21464
|
}, null, 2));
|
|
21235
21465
|
} else {
|
|
21236
21466
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
21237
|
-
log(` ${
|
|
21467
|
+
log(` ${path47.relative(process.cwd(), result.destination) || "."}`);
|
|
21238
21468
|
log(`
|
|
21239
21469
|
Add this entry under your manifest's styles object:`);
|
|
21240
21470
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
21241
21471
|
if (result.recipe.models.length) {
|
|
21242
21472
|
log(`
|
|
21243
21473
|
Models are not downloaded automatically. Verify them with:`);
|
|
21244
|
-
log(` pixelkiln recipe verify ${
|
|
21474
|
+
log(` pixelkiln recipe verify ${path47.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
21245
21475
|
}
|
|
21246
21476
|
}
|
|
21247
21477
|
return;
|
|
@@ -21249,7 +21479,7 @@ async function runRecipe(args) {
|
|
|
21249
21479
|
}
|
|
21250
21480
|
|
|
21251
21481
|
// src/cli/commands/refine.ts
|
|
21252
|
-
import
|
|
21482
|
+
import path48 from "path";
|
|
21253
21483
|
async function runRefine(args) {
|
|
21254
21484
|
if (args.from || args.subcommand === "approve") {
|
|
21255
21485
|
if (!args.from) {
|
|
@@ -21279,12 +21509,12 @@ async function runRefine(args) {
|
|
|
21279
21509
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
21280
21510
|
);
|
|
21281
21511
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
21282
|
-
log(` wrote ${
|
|
21283
|
-
log(` quality record: ${
|
|
21512
|
+
log(` wrote ${path48.relative(process.cwd(), result3.output)}`);
|
|
21513
|
+
log(` quality record: ${path48.relative(process.cwd(), result3.record)}`);
|
|
21284
21514
|
log(`
|
|
21285
21515
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
21286
21516
|
log(
|
|
21287
|
-
` pixelkiln refine approve --from ${
|
|
21517
|
+
` pixelkiln refine approve --from ${path48.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
21288
21518
|
);
|
|
21289
21519
|
}
|
|
21290
21520
|
return;
|
|
@@ -21312,7 +21542,7 @@ async function runRefine(args) {
|
|
|
21312
21542
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
21313
21543
|
else {
|
|
21314
21544
|
log(
|
|
21315
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
21545
|
+
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path48.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path48.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
|
|
21316
21546
|
);
|
|
21317
21547
|
}
|
|
21318
21548
|
return;
|
|
@@ -21321,7 +21551,7 @@ async function runRefine(args) {
|
|
|
21321
21551
|
if (args.json) {
|
|
21322
21552
|
log(JSON.stringify(result2, null, 2));
|
|
21323
21553
|
} else {
|
|
21324
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
21554
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path48.relative(process.cwd(), result2.output || result2.record)}`);
|
|
21325
21555
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
21326
21556
|
}
|
|
21327
21557
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -21367,7 +21597,7 @@ async function runRefine(args) {
|
|
|
21367
21597
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
21368
21598
|
if (item.state === "needs-approval") {
|
|
21369
21599
|
log(
|
|
21370
|
-
` pixelkiln refine approve --from ${
|
|
21600
|
+
` pixelkiln refine approve --from ${path48.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
21371
21601
|
);
|
|
21372
21602
|
}
|
|
21373
21603
|
}
|
|
@@ -21378,9 +21608,9 @@ async function runRefine(args) {
|
|
|
21378
21608
|
}
|
|
21379
21609
|
|
|
21380
21610
|
// src/cli/commands/skeleton.ts
|
|
21381
|
-
import
|
|
21382
|
-
import { existsSync as
|
|
21383
|
-
import { readFile as
|
|
21611
|
+
import path49 from "path";
|
|
21612
|
+
import { existsSync as existsSync36 } from "fs";
|
|
21613
|
+
import { readFile as readFile33, writeFile as writeFile19 } from "fs/promises";
|
|
21384
21614
|
|
|
21385
21615
|
// src/skeleton-preview.ts
|
|
21386
21616
|
var RIGHT = [255, 140, 40, 255];
|
|
@@ -21491,12 +21721,12 @@ var Canvas = class {
|
|
|
21491
21721
|
async function runEstimateSkeleton(args) {
|
|
21492
21722
|
const image = args.target;
|
|
21493
21723
|
if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
|
|
21494
|
-
if (args.out &&
|
|
21724
|
+
if (args.out && existsSync36(path49.resolve(args.out)) && !args.force) {
|
|
21495
21725
|
throw new UsageError(`${args.out} already exists; pass --force to replace it`);
|
|
21496
21726
|
}
|
|
21497
|
-
loadEnvFiles(
|
|
21727
|
+
loadEnvFiles(path49.dirname(path49.resolve(args.manifest)));
|
|
21498
21728
|
loadEnvFiles(process.cwd());
|
|
21499
|
-
const bytes = await
|
|
21729
|
+
const bytes = await readFile33(path49.resolve(image));
|
|
21500
21730
|
const metadata = imageMetadata(bytes);
|
|
21501
21731
|
if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
|
|
21502
21732
|
const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
@@ -21508,7 +21738,7 @@ async function runEstimateSkeleton(args) {
|
|
|
21508
21738
|
const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
|
|
21509
21739
|
const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
|
|
21510
21740
|
if (args.out) {
|
|
21511
|
-
await
|
|
21741
|
+
await writeFile19(path49.resolve(args.out), json + "\n");
|
|
21512
21742
|
log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
|
|
21513
21743
|
log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
|
|
21514
21744
|
} else {
|
|
@@ -21521,8 +21751,8 @@ async function runSkeletonPreview(args) {
|
|
|
21521
21751
|
let sourceFile;
|
|
21522
21752
|
let label;
|
|
21523
21753
|
if (target.endsWith(".json")) {
|
|
21524
|
-
keypointsFile =
|
|
21525
|
-
sourceFile = args.from ?
|
|
21754
|
+
keypointsFile = path49.resolve(target);
|
|
21755
|
+
sourceFile = args.from ? path49.resolve(args.from) : void 0;
|
|
21526
21756
|
label = target;
|
|
21527
21757
|
} else {
|
|
21528
21758
|
const loaded = await loadManifest(args.manifest);
|
|
@@ -21534,31 +21764,31 @@ async function runSkeletonPreview(args) {
|
|
|
21534
21764
|
);
|
|
21535
21765
|
}
|
|
21536
21766
|
keypointsFile = spec.revision.keypointsFile;
|
|
21537
|
-
sourceFile = args.from ?
|
|
21767
|
+
sourceFile = args.from ? path49.resolve(args.from) : spec.revision.sourceFile;
|
|
21538
21768
|
label = `${spec.styleId}/${spec.assetId}`;
|
|
21539
21769
|
}
|
|
21540
|
-
if (!
|
|
21770
|
+
if (!existsSync36(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
|
|
21541
21771
|
let json;
|
|
21542
21772
|
try {
|
|
21543
|
-
json = JSON.parse(await
|
|
21773
|
+
json = JSON.parse(await readFile33(keypointsFile, "utf8"));
|
|
21544
21774
|
} catch (error) {
|
|
21545
21775
|
throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
21546
21776
|
}
|
|
21547
21777
|
const set = parseSkeletonSet(json, keypointsFile);
|
|
21548
21778
|
let source;
|
|
21549
21779
|
if (sourceFile) {
|
|
21550
|
-
if (!
|
|
21780
|
+
if (!existsSync36(sourceFile)) {
|
|
21551
21781
|
log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
|
|
21552
21782
|
} else {
|
|
21553
|
-
const bytes = await
|
|
21783
|
+
const bytes = await readFile33(sourceFile);
|
|
21554
21784
|
if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
|
|
21555
21785
|
source = decodePng(bytes);
|
|
21556
21786
|
}
|
|
21557
21787
|
}
|
|
21558
|
-
const out =
|
|
21559
|
-
await
|
|
21788
|
+
const out = path49.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
|
|
21789
|
+
await writeFile19(out, renderSkeletonSheet(set, { source }));
|
|
21560
21790
|
log(` ${label}: ${set.frames.length} frames after the starting pose`);
|
|
21561
|
-
const shown =
|
|
21791
|
+
const shown = path49.relative(process.cwd(), out);
|
|
21562
21792
|
log(` wrote ${shown.startsWith("..") ? out : shown}`);
|
|
21563
21793
|
}
|
|
21564
21794
|
|
|
@@ -21604,15 +21834,15 @@ async function runTools(args) {
|
|
|
21604
21834
|
}
|
|
21605
21835
|
|
|
21606
21836
|
// src/cli/commands/workspace.ts
|
|
21607
|
-
import
|
|
21608
|
-
import { existsSync as
|
|
21837
|
+
import path50 from "path";
|
|
21838
|
+
import { existsSync as existsSync37 } from "fs";
|
|
21609
21839
|
async function runWorkspace(args) {
|
|
21610
|
-
const workspacePath =
|
|
21611
|
-
const dir =
|
|
21840
|
+
const workspacePath = path50.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
21841
|
+
const dir = path50.dirname(workspacePath);
|
|
21612
21842
|
if (args.subcommand === "add") {
|
|
21613
|
-
const manifestPath =
|
|
21843
|
+
const manifestPath = path50.resolve(args.target);
|
|
21614
21844
|
const loadedTarget = await loadManifest(manifestPath);
|
|
21615
|
-
const lockPath = args.explicitLock ?
|
|
21845
|
+
const lockPath = args.explicitLock ? path50.resolve(args.explicitLock) : path50.join(path50.dirname(manifestPath), "pixelkiln.lock.json");
|
|
21616
21846
|
const ws = await loadWorkspace(workspacePath);
|
|
21617
21847
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
21618
21848
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -21632,10 +21862,10 @@ async function runWorkspace(args) {
|
|
|
21632
21862
|
...args.account ? { account: args.account } : {}
|
|
21633
21863
|
};
|
|
21634
21864
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
21635
|
-
log(` registered "${id}" in ${
|
|
21865
|
+
log(` registered "${id}" in ${path50.relative(process.cwd(), workspacePath)}`);
|
|
21636
21866
|
log(` manifest: ${project.manifest}`);
|
|
21637
21867
|
log(` lock: ${project.lock}`);
|
|
21638
|
-
if (!
|
|
21868
|
+
if (!existsSync37(lockPath)) {
|
|
21639
21869
|
log(
|
|
21640
21870
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
21641
21871
|
);
|
|
@@ -21644,7 +21874,7 @@ async function runWorkspace(args) {
|
|
|
21644
21874
|
}
|
|
21645
21875
|
if (args.subcommand === "remove") {
|
|
21646
21876
|
const ws = await loadWorkspace(workspacePath);
|
|
21647
|
-
const resolvedTarget =
|
|
21877
|
+
const resolvedTarget = path50.resolve(args.target);
|
|
21648
21878
|
const match = ws.projects.find(
|
|
21649
21879
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
21650
21880
|
);
|
|
@@ -21655,11 +21885,11 @@ async function runWorkspace(args) {
|
|
|
21655
21885
|
version: 1,
|
|
21656
21886
|
projects: ws.projects.filter((p) => p !== match)
|
|
21657
21887
|
});
|
|
21658
|
-
log(` removed "${match.id}" from ${
|
|
21888
|
+
log(` removed "${match.id}" from ${path50.relative(process.cwd(), workspacePath)}`);
|
|
21659
21889
|
return;
|
|
21660
21890
|
}
|
|
21661
21891
|
if (args.subcommand === "list") {
|
|
21662
|
-
if (!
|
|
21892
|
+
if (!existsSync37(workspacePath)) {
|
|
21663
21893
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
21664
21894
|
}
|
|
21665
21895
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -21667,9 +21897,9 @@ async function runWorkspace(args) {
|
|
|
21667
21897
|
if (args.json) {
|
|
21668
21898
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
21669
21899
|
} else if (!ws.projects.length) {
|
|
21670
|
-
log(` no projects registered in ${
|
|
21900
|
+
log(` no projects registered in ${path50.relative(process.cwd(), workspacePath)}`);
|
|
21671
21901
|
} else {
|
|
21672
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
21902
|
+
log(` ${ws.projects.length} project(s) in ${path50.relative(process.cwd(), workspacePath)}:`);
|
|
21673
21903
|
for (const p of ws.projects) {
|
|
21674
21904
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
21675
21905
|
}
|
|
@@ -21679,7 +21909,7 @@ async function runWorkspace(args) {
|
|
|
21679
21909
|
return;
|
|
21680
21910
|
}
|
|
21681
21911
|
if (args.subcommand === "status") {
|
|
21682
|
-
if (!
|
|
21912
|
+
if (!existsSync37(workspacePath)) {
|
|
21683
21913
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
21684
21914
|
}
|
|
21685
21915
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -21687,7 +21917,7 @@ async function runWorkspace(args) {
|
|
|
21687
21917
|
if (args.json) {
|
|
21688
21918
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
21689
21919
|
} else {
|
|
21690
|
-
log(` workspace: ${
|
|
21920
|
+
log(` workspace: ${path50.relative(process.cwd(), workspacePath)}`);
|
|
21691
21921
|
for (const p of report.projects) {
|
|
21692
21922
|
if (p.error) {
|
|
21693
21923
|
log(`
|
|
@@ -21741,8 +21971,8 @@ async function runHelp() {
|
|
|
21741
21971
|
async function runVersion() {
|
|
21742
21972
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
21743
21973
|
const url = new URL(up, import.meta.url);
|
|
21744
|
-
if (!
|
|
21745
|
-
const pkg = JSON.parse(await
|
|
21974
|
+
if (!existsSync38(url)) continue;
|
|
21975
|
+
const pkg = JSON.parse(await readFile34(url, "utf8"));
|
|
21746
21976
|
if (pkg.name === "pixelkiln") {
|
|
21747
21977
|
log(`${pkg.name} ${pkg.version}`);
|
|
21748
21978
|
return;
|