pixelkiln 0.82.0 → 0.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +733 -480
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +2 -0
- package/dist/client/gallery.js +3377 -3000
- package/dist/index.cjs +446 -193
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1283 -79
- package/dist/index.d.ts +1283 -79
- package/dist/index.js +442 -189
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +4 -2
- package/docs/CHARACTERS.md +4 -3
- package/docs/MANIFEST.md +10 -6
- package/docs/PIXELLAB.md +9 -1
- package/docs/REVISIONS.md +63 -9
- 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 +17 -11
- package/skills/pixelkiln/references/revisions.md +19 -10
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({
|
|
@@ -13957,6 +14087,52 @@ var NewAssetSchema = z6.object({
|
|
|
13957
14087
|
from: z6.string().min(1),
|
|
13958
14088
|
strength: z6.number().min(0).max(1).optional()
|
|
13959
14089
|
}).strict(),
|
|
14090
|
+
z6.object({
|
|
14091
|
+
mode: z6.literal("reduce-colors"),
|
|
14092
|
+
from: z6.string().min(1),
|
|
14093
|
+
/** A target colour count; the gallery leaves PixelLab's default when this is omitted. */
|
|
14094
|
+
numColors: z6.number().int().min(2).max(256).optional(),
|
|
14095
|
+
dithering: RevisionDitheringSchema.optional(),
|
|
14096
|
+
ditheringStrength: z6.number().min(0).max(10).optional()
|
|
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(),
|
|
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(),
|
|
14119
|
+
z6.object({
|
|
14120
|
+
mode: z6.enum(["animate", "animate-pixminimax"]),
|
|
14121
|
+
from: z6.string().min(1),
|
|
14122
|
+
frames: z6.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
|
|
14123
|
+
fps: z6.number().int().min(1).max(60).optional(),
|
|
14124
|
+
enhancePrompt: z6.boolean().optional(),
|
|
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()
|
|
14130
|
+
}).strict(),
|
|
14131
|
+
z6.object({
|
|
14132
|
+
mode: z6.literal("edit-animation"),
|
|
14133
|
+
from: z6.string().min(1),
|
|
14134
|
+
fps: z6.number().int().min(1).max(60).optional()
|
|
14135
|
+
}).strict(),
|
|
13960
14136
|
z6.object({
|
|
13961
14137
|
mode: z6.literal("animate-skeleton"),
|
|
13962
14138
|
from: z6.string().min(1),
|
|
@@ -14622,19 +14798,19 @@ async function runEdit(args) {
|
|
|
14622
14798
|
}
|
|
14623
14799
|
|
|
14624
14800
|
// src/cli/commands/gallery.ts
|
|
14625
|
-
import
|
|
14626
|
-
import { existsSync as
|
|
14801
|
+
import path35 from "path";
|
|
14802
|
+
import { existsSync as existsSync27 } from "fs";
|
|
14627
14803
|
|
|
14628
14804
|
// src/gallery/snapshot.ts
|
|
14629
14805
|
import { createHash as createHash3 } from "crypto";
|
|
14630
|
-
import { existsSync as
|
|
14631
|
-
import { readFile as
|
|
14632
|
-
import
|
|
14806
|
+
import { existsSync as existsSync22 } from "fs";
|
|
14807
|
+
import { readFile as readFile20, stat as stat2 } from "fs/promises";
|
|
14808
|
+
import path31 from "path";
|
|
14633
14809
|
|
|
14634
14810
|
// src/gallery/edit.ts
|
|
14635
|
-
import { readFile as
|
|
14636
|
-
import
|
|
14637
|
-
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";
|
|
14638
14814
|
|
|
14639
14815
|
// src/gallery/upload.ts
|
|
14640
14816
|
import { existsSync as existsSync18 } from "fs";
|
|
@@ -14677,50 +14853,106 @@ async function saveProjectImage(root, request) {
|
|
|
14677
14853
|
return relative.split(path26.sep).join("/");
|
|
14678
14854
|
}
|
|
14679
14855
|
|
|
14680
|
-
// src/gallery/
|
|
14856
|
+
// src/gallery/image-revision.ts
|
|
14681
14857
|
import { existsSync as existsSync19 } from "fs";
|
|
14682
|
-
import {
|
|
14858
|
+
import { readFile as readFile17, rm as rm9, writeFile as writeFile13 } from "fs/promises";
|
|
14683
14859
|
import path27 from "path";
|
|
14684
14860
|
import { z as z9 } from "zod";
|
|
14685
|
-
var
|
|
14686
|
-
|
|
14687
|
-
styleId: z9.string().min(1),
|
|
14688
|
-
assetId: z9.string().min(1),
|
|
14689
|
-
frames: z9.number().int().min(3).max(15).optional()
|
|
14690
|
-
}).strict();
|
|
14691
|
-
var SkeletonAnimationRequestSchema = z9.object({
|
|
14692
|
-
action: z9.literal("create-skeleton-animation"),
|
|
14861
|
+
var InpaintRevisionRequestSchema = z9.object({
|
|
14862
|
+
action: z9.literal("create-inpaint-revision"),
|
|
14693
14863
|
project: z9.string().min(1).optional(),
|
|
14694
14864
|
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
14695
14865
|
styleId: z9.string().min(1),
|
|
14696
|
-
/** The sprite
|
|
14866
|
+
/** The sprite being repainted. */
|
|
14697
14867
|
from: z9.string().min(1),
|
|
14698
14868
|
assetId: z9.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
14699
|
-
/**
|
|
14869
|
+
/** What to paint into the masked area. */
|
|
14700
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),
|
|
14701
14933
|
direction: CharacterDirectionSchema,
|
|
14702
|
-
description:
|
|
14703
|
-
skeletonTemplate:
|
|
14934
|
+
description: z10.string().min(1).optional(),
|
|
14935
|
+
skeletonTemplate: z10.string().min(1).optional(),
|
|
14704
14936
|
/** Manifest-relative JSON path inside the project. */
|
|
14705
|
-
keypointsFile:
|
|
14937
|
+
keypointsFile: z10.string().min(1),
|
|
14706
14938
|
/** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
|
|
14707
14939
|
set: SkeletonSetSchema.optional(),
|
|
14708
14940
|
/** Replace an existing `keypointsFile` with `set`. */
|
|
14709
|
-
overwrite:
|
|
14941
|
+
overwrite: z10.boolean().optional()
|
|
14710
14942
|
}).strict();
|
|
14711
|
-
var SkeletonKeypointsUpdateSchema =
|
|
14712
|
-
action:
|
|
14713
|
-
project:
|
|
14714
|
-
styleId:
|
|
14715
|
-
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),
|
|
14716
14948
|
/** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
|
|
14717
|
-
expectedSha256:
|
|
14949
|
+
expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
|
|
14718
14950
|
set: SkeletonSetSchema
|
|
14719
14951
|
}).strict();
|
|
14720
14952
|
var DEFAULT_ESTIMATE_LIMIT = 10;
|
|
14721
14953
|
function skeletonSourceFile(spec) {
|
|
14722
|
-
if (spec.source) return
|
|
14723
|
-
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;
|
|
14724
14956
|
return spec.outFile;
|
|
14725
14957
|
}
|
|
14726
14958
|
function createGallerySkeletonHandlers(opts) {
|
|
@@ -14741,10 +14973,10 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
14741
14973
|
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
|
|
14742
14974
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14743
14975
|
const file = skeletonSourceFile(spec);
|
|
14744
|
-
if (!
|
|
14745
|
-
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);
|
|
14746
14978
|
const metadata = imageMetadata(bytes);
|
|
14747
|
-
if (!metadata) throw new ManifestEditError(`${
|
|
14979
|
+
if (!metadata) throw new ManifestEditError(`${path28.basename(file)} is not a readable PNG or JPEG`);
|
|
14748
14980
|
const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
14749
14981
|
if (problem) throw new ManifestEditError(`${key} is ${problem}`);
|
|
14750
14982
|
if (used >= limit) {
|
|
@@ -14761,7 +14993,7 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
14761
14993
|
log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
|
|
14762
14994
|
return {
|
|
14763
14995
|
set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
|
|
14764
|
-
source:
|
|
14996
|
+
source: path28.relative(ctx.loaded.root, file).split(path28.sep).join("/"),
|
|
14765
14997
|
session: status()
|
|
14766
14998
|
};
|
|
14767
14999
|
}
|
|
@@ -14775,21 +15007,21 @@ function billedAmount(usage) {
|
|
|
14775
15007
|
return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
|
|
14776
15008
|
}
|
|
14777
15009
|
async function createSkeletonAnimation(manifestPath, request) {
|
|
14778
|
-
const root =
|
|
14779
|
-
const file =
|
|
14780
|
-
const relative =
|
|
14781
|
-
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)) {
|
|
14782
15014
|
throw new ManifestEditError("the keypoints file must be a .json path inside the project");
|
|
14783
15015
|
}
|
|
14784
|
-
const existed =
|
|
15016
|
+
const existed = existsSync20(file);
|
|
14785
15017
|
let previous = null;
|
|
14786
15018
|
if (request.set) {
|
|
14787
15019
|
if (existed && !request.overwrite) {
|
|
14788
15020
|
throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
|
|
14789
15021
|
}
|
|
14790
|
-
if (existed) previous = await
|
|
14791
|
-
await mkdir11(
|
|
14792
|
-
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");
|
|
14793
15025
|
} else if (!existed) {
|
|
14794
15026
|
throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
|
|
14795
15027
|
}
|
|
@@ -14804,7 +15036,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
14804
15036
|
revision: {
|
|
14805
15037
|
mode: "animate-skeleton",
|
|
14806
15038
|
from: request.from,
|
|
14807
|
-
keypointsFile: relative.split(
|
|
15039
|
+
keypointsFile: relative.split(path28.sep).join("/"),
|
|
14808
15040
|
direction: request.direction,
|
|
14809
15041
|
...request.description ? { description: request.description } : {},
|
|
14810
15042
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
@@ -14813,8 +15045,8 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
14813
15045
|
});
|
|
14814
15046
|
} catch (error) {
|
|
14815
15047
|
if (request.set) {
|
|
14816
|
-
if (previous) await
|
|
14817
|
-
else await
|
|
15048
|
+
if (previous) await writeFile14(file, previous);
|
|
15049
|
+
else await rm10(file, { force: true });
|
|
14818
15050
|
}
|
|
14819
15051
|
throw error;
|
|
14820
15052
|
}
|
|
@@ -14825,64 +15057,64 @@ async function updateSkeletonKeypoints(ctx, request) {
|
|
|
14825
15057
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14826
15058
|
const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
|
|
14827
15059
|
if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
|
|
14828
|
-
const relative =
|
|
14829
|
-
if (relative.startsWith("..") ||
|
|
15060
|
+
const relative = path28.relative(ctx.loaded.root, file);
|
|
15061
|
+
if (relative.startsWith("..") || path28.isAbsolute(relative)) {
|
|
14830
15062
|
throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
|
|
14831
15063
|
}
|
|
14832
|
-
const current =
|
|
15064
|
+
const current = existsSync20(file) ? sha256(await readFile18(file)) : null;
|
|
14833
15065
|
if (current !== request.expectedSha256) {
|
|
14834
15066
|
throw Object.assign(
|
|
14835
|
-
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`),
|
|
14836
15068
|
{ status: 409 }
|
|
14837
15069
|
);
|
|
14838
15070
|
}
|
|
14839
|
-
await
|
|
14840
|
-
return relative.split(
|
|
15071
|
+
await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
15072
|
+
return relative.split(path28.sep).join("/");
|
|
14841
15073
|
}
|
|
14842
15074
|
|
|
14843
15075
|
// src/gallery/edit.ts
|
|
14844
|
-
var Base64 =
|
|
14845
|
-
var HandEditRequestSchema =
|
|
14846
|
-
|
|
14847
|
-
action:
|
|
14848
|
-
project:
|
|
14849
|
-
assetId:
|
|
14850
|
-
styleId:
|
|
14851
|
-
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}$/),
|
|
14852
15084
|
/** Also launch the author's editor on the file. */
|
|
14853
|
-
open:
|
|
15085
|
+
open: z11.boolean().optional()
|
|
14854
15086
|
}).strict(),
|
|
14855
|
-
|
|
14856
|
-
action:
|
|
14857
|
-
project:
|
|
14858
|
-
assetId:
|
|
14859
|
-
styleId:
|
|
14860
|
-
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}$/)
|
|
14861
15093
|
}).strict(),
|
|
14862
15094
|
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
14863
|
-
|
|
14864
|
-
action:
|
|
14865
|
-
project:
|
|
14866
|
-
assetId:
|
|
14867
|
-
styleId:
|
|
14868
|
-
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}$/),
|
|
14869
15101
|
/** The flattened image; for a frame set, `frames` instead. */
|
|
14870
15102
|
png: Base64.min(1).optional(),
|
|
14871
15103
|
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
14872
|
-
frames:
|
|
15104
|
+
frames: z11.array(z11.object({ role: z11.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
14873
15105
|
/** The editor's layered project file, kept beside the edit. */
|
|
14874
15106
|
pxo: Base64.optional(),
|
|
14875
15107
|
/** Editor identity from the bridge's ready message. */
|
|
14876
|
-
editor:
|
|
14877
|
-
protocol:
|
|
15108
|
+
editor: z11.string().min(1).max(200),
|
|
15109
|
+
protocol: z11.number().int().positive()
|
|
14878
15110
|
}).strict()
|
|
14879
15111
|
]);
|
|
14880
15112
|
var UNDO_DEPTH = 20;
|
|
14881
|
-
var UndoRequestSchema =
|
|
14882
|
-
action:
|
|
14883
|
-
project:
|
|
15113
|
+
var UndoRequestSchema = z11.object({
|
|
15114
|
+
action: z11.literal("undo-edit"),
|
|
15115
|
+
project: z11.string().min(1).optional(),
|
|
14884
15116
|
/** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
|
|
14885
|
-
expectedSha256:
|
|
15117
|
+
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/)
|
|
14886
15118
|
}).strict();
|
|
14887
15119
|
function createGalleryEditHandler(opts) {
|
|
14888
15120
|
const log2 = opts.onProgress ?? (() => {
|
|
@@ -14891,7 +15123,7 @@ function createGalleryEditHandler(opts) {
|
|
|
14891
15123
|
const history = /* @__PURE__ */ new Map();
|
|
14892
15124
|
const manifestOf = async (project) => {
|
|
14893
15125
|
try {
|
|
14894
|
-
return
|
|
15126
|
+
return path29.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
|
|
14895
15127
|
} catch {
|
|
14896
15128
|
return null;
|
|
14897
15129
|
}
|
|
@@ -14901,7 +15133,7 @@ function createGalleryEditHandler(opts) {
|
|
|
14901
15133
|
if (undo.success) {
|
|
14902
15134
|
const manifestPath2 = await manifestOf(undo.data.project);
|
|
14903
15135
|
if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
|
|
14904
|
-
const current = await
|
|
15136
|
+
const current = await readFile19(manifestPath2, "utf8");
|
|
14905
15137
|
if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
|
|
14906
15138
|
const stack = history.get(manifestPath2) ?? [];
|
|
14907
15139
|
const last = stack[stack.length - 1];
|
|
@@ -14911,17 +15143,17 @@ function createGalleryEditHandler(opts) {
|
|
|
14911
15143
|
throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
|
|
14912
15144
|
}
|
|
14913
15145
|
const tmp = manifestPath2 + ".pixelkiln-undo";
|
|
14914
|
-
await
|
|
15146
|
+
await writeFile15(tmp, last.before);
|
|
14915
15147
|
await rename9(tmp, manifestPath2);
|
|
14916
15148
|
stack.pop();
|
|
14917
|
-
log2(` manifest edit undone in ${
|
|
15149
|
+
log2(` manifest edit undone in ${path29.relative(process.cwd(), manifestPath2) || manifestPath2}`);
|
|
14918
15150
|
return opts.reload();
|
|
14919
15151
|
}
|
|
14920
15152
|
const manifestPath = await manifestOf(body?.project);
|
|
14921
|
-
const before = manifestPath ? await
|
|
15153
|
+
const before = manifestPath ? await readFile19(manifestPath, "utf8").catch(() => null) : null;
|
|
14922
15154
|
const build = await apply(body);
|
|
14923
15155
|
if (manifestPath && before !== null) {
|
|
14924
|
-
const after = await
|
|
15156
|
+
const after = await readFile19(manifestPath, "utf8").catch(() => null);
|
|
14925
15157
|
if (after !== null && after !== before) {
|
|
14926
15158
|
const stack = history.get(manifestPath) ?? [];
|
|
14927
15159
|
stack.push({ before, afterSha: sha256(after) });
|
|
@@ -14976,7 +15208,7 @@ function createEditApplier(opts, log2) {
|
|
|
14976
15208
|
} catch (error) {
|
|
14977
15209
|
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
14978
15210
|
}
|
|
14979
|
-
const written = await saveProjectImage(
|
|
15211
|
+
const written = await saveProjectImage(path29.dirname(path29.resolve(manifestPath2)), upload.data);
|
|
14980
15212
|
log2(` image added: ${written}`);
|
|
14981
15213
|
return opts.reload();
|
|
14982
15214
|
}
|
|
@@ -15012,6 +15244,23 @@ function createEditApplier(opts, log2) {
|
|
|
15012
15244
|
"invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
15013
15245
|
);
|
|
15014
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
|
+
}
|
|
15015
15264
|
const parsed = ManifestEditSchema.safeParse(body);
|
|
15016
15265
|
if (!parsed.success) {
|
|
15017
15266
|
throw new ManifestEditError(
|
|
@@ -15026,7 +15275,7 @@ function createEditApplier(opts, log2) {
|
|
|
15026
15275
|
}
|
|
15027
15276
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
15028
15277
|
if (result.changed) {
|
|
15029
|
-
const relative =
|
|
15278
|
+
const relative = path29.relative(process.cwd(), result.manifestPath);
|
|
15030
15279
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
15031
15280
|
log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
|
|
15032
15281
|
}
|
|
@@ -15077,8 +15326,8 @@ function createGalleryPriceHandler(opts) {
|
|
|
15077
15326
|
}
|
|
15078
15327
|
|
|
15079
15328
|
// src/pipeline/history.ts
|
|
15080
|
-
import { existsSync as
|
|
15081
|
-
import
|
|
15329
|
+
import { existsSync as existsSync21 } from "fs";
|
|
15330
|
+
import path30 from "path";
|
|
15082
15331
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
15083
15332
|
function historyLimit(manifest, env = process.env) {
|
|
15084
15333
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -15165,10 +15414,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
15165
15414
|
}
|
|
15166
15415
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
15167
15416
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
15168
|
-
if (!
|
|
15417
|
+
if (!existsSync21(file)) continue;
|
|
15169
15418
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
15170
15419
|
throw new OverwriteRefusedError(
|
|
15171
|
-
`${
|
|
15420
|
+
`${path30.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
15172
15421
|
[file]
|
|
15173
15422
|
);
|
|
15174
15423
|
}
|
|
@@ -15221,7 +15470,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
15221
15470
|
// src/gallery/snapshot.ts
|
|
15222
15471
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
15223
15472
|
function galleryMediaId(absolutePath) {
|
|
15224
|
-
return createHash3("sha256").update(
|
|
15473
|
+
return createHash3("sha256").update(path31.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
15225
15474
|
}
|
|
15226
15475
|
function galleryMediaRoute(id) {
|
|
15227
15476
|
return `/media/${id}`;
|
|
@@ -15240,9 +15489,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
15240
15489
|
return (entry.history ?? []).map((generation, i) => {
|
|
15241
15490
|
const outputs = generation.outputs.map((output) => {
|
|
15242
15491
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
15243
|
-
const file =
|
|
15492
|
+
const file = path31.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
15244
15493
|
let url = null;
|
|
15245
|
-
if (
|
|
15494
|
+
if (existsSync22(file)) {
|
|
15246
15495
|
const id = galleryMediaId(file);
|
|
15247
15496
|
media.set(id, { path: file, contentType: mediaType });
|
|
15248
15497
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -15274,7 +15523,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
15274
15523
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
15275
15524
|
async function samePixels(a, b) {
|
|
15276
15525
|
try {
|
|
15277
|
-
const [left, right] = await Promise.all([
|
|
15526
|
+
const [left, right] = await Promise.all([readFile20(a), readFile20(b)]);
|
|
15278
15527
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
15279
15528
|
const x = decodePng(left);
|
|
15280
15529
|
const y = decodePng(right);
|
|
@@ -15372,10 +15621,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
15372
15621
|
async function describeQuality(media, root, inspection) {
|
|
15373
15622
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
15374
15623
|
const outputs = await Promise.all(
|
|
15375
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
15624
|
+
outputPaths.map((file) => describeOutput(media, root, path31.resolve(root, file)))
|
|
15376
15625
|
);
|
|
15377
|
-
const recordPath =
|
|
15378
|
-
const recordExists =
|
|
15626
|
+
const recordPath = path31.resolve(root, inspection.record);
|
|
15627
|
+
const recordExists = existsSync22(recordPath);
|
|
15379
15628
|
let options = null;
|
|
15380
15629
|
let check = null;
|
|
15381
15630
|
if (recordExists) {
|
|
@@ -15394,7 +15643,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
15394
15643
|
return {
|
|
15395
15644
|
state: inspection.state,
|
|
15396
15645
|
reason: inspection.reason,
|
|
15397
|
-
output: portableOutputPath(
|
|
15646
|
+
output: portableOutputPath(path31.resolve(root, inspection.output), root),
|
|
15398
15647
|
record: portableOutputPath(recordPath, root),
|
|
15399
15648
|
recordExists,
|
|
15400
15649
|
outputs,
|
|
@@ -15432,9 +15681,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
15432
15681
|
const media = /* @__PURE__ */ new Map();
|
|
15433
15682
|
const plan = await buildPlan(specs, lock);
|
|
15434
15683
|
const items = [];
|
|
15435
|
-
const manifestText = await
|
|
15684
|
+
const manifestText = await readFile20(loaded.path, "utf8");
|
|
15436
15685
|
const manifestSha256 = sha256(manifestText);
|
|
15437
|
-
const cacheDir =
|
|
15686
|
+
const cacheDir = path31.resolve(path31.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
15438
15687
|
let rawStyles = {};
|
|
15439
15688
|
try {
|
|
15440
15689
|
const raw = JSON.parse(manifestText);
|
|
@@ -15458,12 +15707,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
15458
15707
|
)
|
|
15459
15708
|
);
|
|
15460
15709
|
if (spec.source) {
|
|
15461
|
-
const editPath =
|
|
15710
|
+
const editPath = path31.resolve(root, spec.source);
|
|
15462
15711
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
15463
15712
|
const editShas = await Promise.all(
|
|
15464
15713
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
15465
15714
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
15466
|
-
return { file, sha256:
|
|
15715
|
+
return { file, sha256: existsSync22(file) ? await sha256File(file) : null };
|
|
15467
15716
|
})
|
|
15468
15717
|
);
|
|
15469
15718
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -15471,7 +15720,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15471
15720
|
const companion = await readHandEditCompanion(editPath);
|
|
15472
15721
|
if (companion) {
|
|
15473
15722
|
const projectPath = handEditProjectPath(editPath);
|
|
15474
|
-
const project = companion.project &&
|
|
15723
|
+
const project = companion.project && existsSync22(projectPath) ? await fileInfo(projectPath) : null;
|
|
15475
15724
|
let projectUrl = null;
|
|
15476
15725
|
if (project) {
|
|
15477
15726
|
const id = galleryMediaId(projectPath);
|
|
@@ -15507,8 +15756,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
15507
15756
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
15508
15757
|
}
|
|
15509
15758
|
} else if (spec.source) {
|
|
15510
|
-
outputs = [await describeOutput(media, root,
|
|
15511
|
-
} else if (!entry &&
|
|
15759
|
+
outputs = [await describeOutput(media, root, path31.resolve(root, spec.source))];
|
|
15760
|
+
} else if (!entry && existsSync22(spec.outFile)) {
|
|
15512
15761
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
15513
15762
|
} else {
|
|
15514
15763
|
outputs = [];
|
|
@@ -15711,7 +15960,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15711
15960
|
id: loaded.manifest.name,
|
|
15712
15961
|
name: loaded.manifest.name,
|
|
15713
15962
|
manifest: loaded.path,
|
|
15714
|
-
lock:
|
|
15963
|
+
lock: path31.resolve(opts.lockPath),
|
|
15715
15964
|
root,
|
|
15716
15965
|
provider: loaded.manifest.provider,
|
|
15717
15966
|
account: null,
|
|
@@ -15737,7 +15986,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15737
15986
|
return { snapshot, media };
|
|
15738
15987
|
}
|
|
15739
15988
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
15740
|
-
const dir =
|
|
15989
|
+
const dir = path31.dirname(path31.resolve(opts.workspacePath));
|
|
15741
15990
|
const media = /* @__PURE__ */ new Map();
|
|
15742
15991
|
const projects = [];
|
|
15743
15992
|
const items = [];
|
|
@@ -15780,11 +16029,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15780
16029
|
id: project.id,
|
|
15781
16030
|
name: loaded.manifest.name,
|
|
15782
16031
|
manifest: manifestPath,
|
|
15783
|
-
lock:
|
|
16032
|
+
lock: path31.resolve(lockPath),
|
|
15784
16033
|
root: loaded.root,
|
|
15785
16034
|
provider: loaded.manifest.provider,
|
|
15786
16035
|
account: project.account ?? null,
|
|
15787
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
16036
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile20(manifestPath, "utf8")),
|
|
15788
16037
|
historyLimit: historyLimit(loaded.manifest),
|
|
15789
16038
|
entries: Object.keys(lock.entries).length,
|
|
15790
16039
|
items: projectItems.length,
|
|
@@ -15796,8 +16045,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15796
16045
|
id: project.id,
|
|
15797
16046
|
name: project.id,
|
|
15798
16047
|
manifest: manifestPath,
|
|
15799
|
-
lock:
|
|
15800
|
-
root:
|
|
16048
|
+
lock: path31.resolve(lockPath),
|
|
16049
|
+
root: path31.dirname(manifestPath),
|
|
15801
16050
|
provider: project.provider,
|
|
15802
16051
|
account: project.account ?? null,
|
|
15803
16052
|
manifestSha256: null,
|
|
@@ -15824,7 +16073,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15824
16073
|
version: 1,
|
|
15825
16074
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
15826
16075
|
project: null,
|
|
15827
|
-
workspace: { path:
|
|
16076
|
+
workspace: { path: path31.resolve(opts.workspacePath), dir, projects },
|
|
15828
16077
|
filter,
|
|
15829
16078
|
totals: {
|
|
15830
16079
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -15843,10 +16092,10 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15843
16092
|
// src/gallery/server.ts
|
|
15844
16093
|
import { randomBytes } from "crypto";
|
|
15845
16094
|
import { createServer as createServer2 } from "http";
|
|
15846
|
-
import { readFile as
|
|
16095
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
15847
16096
|
|
|
15848
16097
|
// src/gallery/page.ts
|
|
15849
|
-
import { existsSync as
|
|
16098
|
+
import { existsSync as existsSync23, readFileSync as readFileSync4 } from "fs";
|
|
15850
16099
|
|
|
15851
16100
|
// src/gallery/client-bundle.ts
|
|
15852
16101
|
import { createRequire } from "module";
|
|
@@ -15887,7 +16136,7 @@ function clientAssets() {
|
|
|
15887
16136
|
const prebuilt = new URL("gallery.js", CLIENT_DIR);
|
|
15888
16137
|
client ??= {
|
|
15889
16138
|
css: readFileSync4(new URL("gallery.css", CLIENT_DIR), "utf8"),
|
|
15890
|
-
js:
|
|
16139
|
+
js: existsSync23(prebuilt) ? readFileSync4(prebuilt, "utf8") : bundleGalleryClient()
|
|
15891
16140
|
};
|
|
15892
16141
|
return client;
|
|
15893
16142
|
}
|
|
@@ -16158,7 +16407,7 @@ async function serveGallery(opts) {
|
|
|
16158
16407
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
16159
16408
|
if (!asset) return fail(404, "no such review asset");
|
|
16160
16409
|
try {
|
|
16161
|
-
const bytes = await
|
|
16410
|
+
const bytes = await readFile21(asset.path);
|
|
16162
16411
|
res.writeHead(200, {
|
|
16163
16412
|
"Content-Type": asset.contentType,
|
|
16164
16413
|
"Cache-Control": "no-store",
|
|
@@ -16241,7 +16490,7 @@ async function serveGallery(opts) {
|
|
|
16241
16490
|
if (!asset) return fail(404, "no such gallery media");
|
|
16242
16491
|
let bytes;
|
|
16243
16492
|
try {
|
|
16244
|
-
bytes = await
|
|
16493
|
+
bytes = await readFile21(asset.path);
|
|
16245
16494
|
} catch {
|
|
16246
16495
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
16247
16496
|
}
|
|
@@ -16285,7 +16534,7 @@ async function serveGallery(opts) {
|
|
|
16285
16534
|
|
|
16286
16535
|
// src/gallery/generate.ts
|
|
16287
16536
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
16288
|
-
import { z as
|
|
16537
|
+
import { z as z12 } from "zod";
|
|
16289
16538
|
|
|
16290
16539
|
// src/pipeline/poll.ts
|
|
16291
16540
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -16569,6 +16818,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
16569
16818
|
...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
|
|
16570
16819
|
...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
|
|
16571
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 } : {},
|
|
16572
16825
|
...spec.revision.engine ? { engine: spec.revision.engine } : {}
|
|
16573
16826
|
} : null,
|
|
16574
16827
|
outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
|
|
@@ -16696,13 +16949,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
|
|
|
16696
16949
|
}
|
|
16697
16950
|
|
|
16698
16951
|
// src/pick/server.ts
|
|
16699
|
-
import { existsSync as
|
|
16700
|
-
import { readFile as
|
|
16952
|
+
import { existsSync as existsSync25 } from "fs";
|
|
16953
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
16701
16954
|
|
|
16702
16955
|
// src/pipeline/init.ts
|
|
16703
|
-
import { readdir, readFile as
|
|
16704
|
-
import { existsSync as
|
|
16705
|
-
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";
|
|
16706
16959
|
function pngSize(buf) {
|
|
16707
16960
|
if (buf.length < 24) return null;
|
|
16708
16961
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -16716,7 +16969,7 @@ function slugify(value) {
|
|
|
16716
16969
|
async function walk(dir, exclude) {
|
|
16717
16970
|
const out = [];
|
|
16718
16971
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
16719
|
-
const full =
|
|
16972
|
+
const full = path32.join(dir, entry.name);
|
|
16720
16973
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
16721
16974
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
16722
16975
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -16730,14 +16983,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
16730
16983
|
const skipped = [];
|
|
16731
16984
|
const seen = /* @__PURE__ */ new Map();
|
|
16732
16985
|
for (const file of files.sort()) {
|
|
16733
|
-
const size = pngSize(await
|
|
16986
|
+
const size = pngSize(await readFile22(file));
|
|
16734
16987
|
if (!size) {
|
|
16735
16988
|
skipped.push(`${file} (not a readable PNG)`);
|
|
16736
16989
|
continue;
|
|
16737
16990
|
}
|
|
16738
|
-
const rel =
|
|
16739
|
-
const category =
|
|
16740
|
-
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));
|
|
16741
16994
|
if (seen.has(id)) {
|
|
16742
16995
|
const n = seen.get(id) + 1;
|
|
16743
16996
|
seen.set(id, n);
|
|
@@ -16795,10 +17048,10 @@ function mode(values) {
|
|
|
16795
17048
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
16796
17049
|
}
|
|
16797
17050
|
async function writeManifestFile(target, manifest) {
|
|
16798
|
-
if (
|
|
17051
|
+
if (existsSync24(target)) {
|
|
16799
17052
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
16800
17053
|
}
|
|
16801
|
-
await
|
|
17054
|
+
await writeFile16(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
16802
17055
|
}
|
|
16803
17056
|
|
|
16804
17057
|
// src/pick/sheet.ts
|
|
@@ -17211,13 +17464,13 @@ async function currentArt(entry, spec) {
|
|
|
17211
17464
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
17212
17465
|
if (!previous) return null;
|
|
17213
17466
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
17214
|
-
if (!
|
|
17467
|
+
if (!existsSync25(file)) return null;
|
|
17215
17468
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
17216
17469
|
if (!contentType) return null;
|
|
17217
17470
|
let width = entry.width;
|
|
17218
17471
|
let height = entry.height;
|
|
17219
17472
|
if (contentType === "image/png") {
|
|
17220
|
-
const size = pngSize(await
|
|
17473
|
+
const size = pngSize(await readFile23(file));
|
|
17221
17474
|
if (size) ({ width, height } = size);
|
|
17222
17475
|
}
|
|
17223
17476
|
return { path: file, contentType, width, height };
|
|
@@ -17385,26 +17638,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
17385
17638
|
}
|
|
17386
17639
|
|
|
17387
17640
|
// src/gallery/generate.ts
|
|
17388
|
-
var GenerateRequestSchema =
|
|
17389
|
-
project:
|
|
17390
|
-
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),
|
|
17391
17644
|
/**
|
|
17392
17645
|
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
17393
17646
|
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
17394
17647
|
* the recorded bytes back over a modified file.
|
|
17395
17648
|
*/
|
|
17396
|
-
force:
|
|
17649
|
+
force: z12.boolean().optional(),
|
|
17397
17650
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
17398
|
-
resume:
|
|
17651
|
+
resume: z12.boolean().optional(),
|
|
17399
17652
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
17400
|
-
refresh:
|
|
17653
|
+
refresh: z12.boolean().optional(),
|
|
17401
17654
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
17402
|
-
restore:
|
|
17655
|
+
restore: z12.boolean().optional(),
|
|
17403
17656
|
/**
|
|
17404
17657
|
* Bring a replaced generation back: 1-based position in the record's
|
|
17405
17658
|
* history (newest first) or an output hash prefix. One key at a time.
|
|
17406
17659
|
*/
|
|
17407
|
-
revert:
|
|
17660
|
+
revert: z12.string().min(1).max(64).optional()
|
|
17408
17661
|
}).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
|
|
17409
17662
|
message: "resume, refresh, restore, and revert are exclusive"
|
|
17410
17663
|
}).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
|
|
@@ -17750,15 +18003,15 @@ function createGenerateHandlers(opts) {
|
|
|
17750
18003
|
}
|
|
17751
18004
|
|
|
17752
18005
|
// src/gallery/editor.ts
|
|
17753
|
-
import { readFile as
|
|
17754
|
-
import
|
|
18006
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
18007
|
+
import path34 from "path";
|
|
17755
18008
|
|
|
17756
18009
|
// src/editor/install.ts
|
|
17757
18010
|
import { createHash as createHash4 } from "crypto";
|
|
17758
|
-
import { existsSync as
|
|
17759
|
-
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";
|
|
17760
18013
|
import os2 from "os";
|
|
17761
|
-
import
|
|
18014
|
+
import path33 from "path";
|
|
17762
18015
|
|
|
17763
18016
|
// tools/pixelorama-bridge/pin.json
|
|
17764
18017
|
var pin_default = {
|
|
@@ -17822,15 +18075,15 @@ var EditorInstallError = class extends Error {
|
|
|
17822
18075
|
};
|
|
17823
18076
|
function editorToolsRoot() {
|
|
17824
18077
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
17825
|
-
if (override) return
|
|
17826
|
-
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");
|
|
17827
18080
|
if (process.platform === "win32") {
|
|
17828
|
-
return
|
|
18081
|
+
return path33.join(process.env.LOCALAPPDATA ?? path33.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
17829
18082
|
}
|
|
17830
|
-
return
|
|
18083
|
+
return path33.join(process.env.XDG_CACHE_HOME ?? path33.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
17831
18084
|
}
|
|
17832
18085
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
17833
|
-
return
|
|
18086
|
+
return path33.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
17834
18087
|
}
|
|
17835
18088
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
17836
18089
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -17840,7 +18093,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
17840
18093
|
}
|
|
17841
18094
|
async function sha256Of(file) {
|
|
17842
18095
|
try {
|
|
17843
|
-
return createHash4("sha256").update(await
|
|
18096
|
+
return createHash4("sha256").update(await readFile24(file)).digest("hex");
|
|
17844
18097
|
} catch {
|
|
17845
18098
|
return null;
|
|
17846
18099
|
}
|
|
@@ -17853,8 +18106,8 @@ async function editorStatus(opts = {}) {
|
|
|
17853
18106
|
let totalBytes = 0;
|
|
17854
18107
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
17855
18108
|
totalBytes += expected.bytes;
|
|
17856
|
-
const file =
|
|
17857
|
-
const info =
|
|
18109
|
+
const file = path33.join(dir, name);
|
|
18110
|
+
const info = existsSync26(file) ? await stat3(file).catch(() => null) : null;
|
|
17858
18111
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
17859
18112
|
installedBytes += expected.bytes;
|
|
17860
18113
|
} else {
|
|
@@ -17911,13 +18164,13 @@ async function installEditor(opts = {}) {
|
|
|
17911
18164
|
if (digest2 !== expected.sha256) {
|
|
17912
18165
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
17913
18166
|
}
|
|
17914
|
-
const final =
|
|
18167
|
+
const final = path33.join(dir, name);
|
|
17915
18168
|
const tmp = `${final}.${process.pid}.part`;
|
|
17916
18169
|
try {
|
|
17917
|
-
await
|
|
18170
|
+
await writeFile17(tmp, bytes);
|
|
17918
18171
|
await rename10(tmp, final);
|
|
17919
18172
|
} finally {
|
|
17920
|
-
await
|
|
18173
|
+
await rm11(tmp, { force: true });
|
|
17921
18174
|
}
|
|
17922
18175
|
fetchedBytes += bytes.length;
|
|
17923
18176
|
downloaded.push(name);
|
|
@@ -17957,7 +18210,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
17957
18210
|
".json": "application/json; charset=utf-8"
|
|
17958
18211
|
};
|
|
17959
18212
|
function editorContentType(name) {
|
|
17960
|
-
return EDITOR_CONTENT_TYPES[
|
|
18213
|
+
return EDITOR_CONTENT_TYPES[path33.extname(name).toLowerCase()] ?? null;
|
|
17961
18214
|
}
|
|
17962
18215
|
|
|
17963
18216
|
// src/gallery/editor.ts
|
|
@@ -18033,7 +18286,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
18033
18286
|
if (!status.installed) return null;
|
|
18034
18287
|
let bytes;
|
|
18035
18288
|
try {
|
|
18036
|
-
bytes = await
|
|
18289
|
+
bytes = await readFile25(path34.join(dir, name));
|
|
18037
18290
|
} catch {
|
|
18038
18291
|
verified = null;
|
|
18039
18292
|
return null;
|
|
@@ -18073,7 +18326,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18073
18326
|
};
|
|
18074
18327
|
const loadProject = async (project) => {
|
|
18075
18328
|
const ctx = await access2.loadProject(project);
|
|
18076
|
-
loadProjectEnv(
|
|
18329
|
+
loadProjectEnv(path35.dirname(ctx.loaded.path));
|
|
18077
18330
|
return ctx;
|
|
18078
18331
|
};
|
|
18079
18332
|
const loadProjectEnv = (dir) => {
|
|
@@ -18082,7 +18335,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18082
18335
|
for (const [name, value] of Object.entries(declared)) {
|
|
18083
18336
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
18084
18337
|
throw new Error(
|
|
18085
|
-
`${
|
|
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.`
|
|
18086
18339
|
);
|
|
18087
18340
|
}
|
|
18088
18341
|
}
|
|
@@ -18090,7 +18343,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18090
18343
|
};
|
|
18091
18344
|
const quoters = /* @__PURE__ */ new Map();
|
|
18092
18345
|
const quoteFor = async (project) => {
|
|
18093
|
-
loadProjectEnv(
|
|
18346
|
+
loadProjectEnv(path35.dirname(path35.resolve(await access2.manifestFor(project))));
|
|
18094
18347
|
const key = process.env.PIXELLAB_API_KEY;
|
|
18095
18348
|
if (!key) return null;
|
|
18096
18349
|
let quoter = quoters.get(key);
|
|
@@ -18141,8 +18394,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18141
18394
|
}
|
|
18142
18395
|
async function runGallery(args) {
|
|
18143
18396
|
if (args.workspace) {
|
|
18144
|
-
const workspacePath =
|
|
18145
|
-
if (!
|
|
18397
|
+
const workspacePath = path35.resolve(args.workspace);
|
|
18398
|
+
if (!existsSync27(workspacePath)) {
|
|
18146
18399
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
18147
18400
|
}
|
|
18148
18401
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -18162,7 +18415,7 @@ async function runGallery(args) {
|
|
|
18162
18415
|
const registered = async (projectId) => {
|
|
18163
18416
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
18164
18417
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
18165
|
-
return resolveProject(
|
|
18418
|
+
return resolveProject(path35.dirname(workspacePath), project);
|
|
18166
18419
|
};
|
|
18167
18420
|
await serveUntilStopped(initial, build2, args, {
|
|
18168
18421
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -18187,7 +18440,7 @@ async function runGallery(args) {
|
|
|
18187
18440
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
18188
18441
|
};
|
|
18189
18442
|
await serveUntilStopped(await build(), reload, args, {
|
|
18190
|
-
manifestFor: () =>
|
|
18443
|
+
manifestFor: () => path35.resolve(args.manifest),
|
|
18191
18444
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
18192
18445
|
});
|
|
18193
18446
|
}
|
|
@@ -18496,18 +18749,18 @@ async function runRestoreGeneration(args) {
|
|
|
18496
18749
|
}
|
|
18497
18750
|
|
|
18498
18751
|
// src/cli/commands/housekeeping.ts
|
|
18499
|
-
import { existsSync as
|
|
18752
|
+
import { existsSync as existsSync29 } from "fs";
|
|
18500
18753
|
|
|
18501
18754
|
// src/pipeline/cache-health.ts
|
|
18502
|
-
import { existsSync as
|
|
18503
|
-
import { readFile as
|
|
18504
|
-
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";
|
|
18505
18758
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
18506
|
-
if (options.prune && !
|
|
18507
|
-
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)}`);
|
|
18508
18761
|
}
|
|
18509
|
-
const contentDir =
|
|
18510
|
-
const remotePath =
|
|
18762
|
+
const contentDir = path36.resolve(path36.dirname(lockPath), ".pixelkiln", "cache");
|
|
18763
|
+
const remotePath = path36.resolve(cachePathFor(lockPath));
|
|
18511
18764
|
const referenced = referencedHashes(lock);
|
|
18512
18765
|
let content = await inspectContentCache(contentDir, referenced);
|
|
18513
18766
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -18519,7 +18772,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18519
18772
|
]);
|
|
18520
18773
|
for (const name of names) {
|
|
18521
18774
|
try {
|
|
18522
|
-
await
|
|
18775
|
+
await rm12(path36.join(contentDir, name), { force: true });
|
|
18523
18776
|
removed.contentFiles++;
|
|
18524
18777
|
} catch {
|
|
18525
18778
|
}
|
|
@@ -18528,7 +18781,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18528
18781
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
18529
18782
|
removed.resetRemoteHashCache = true;
|
|
18530
18783
|
} else if (remoteHashes.invalidIds.length) {
|
|
18531
|
-
const cache = parseCache(JSON.parse(await
|
|
18784
|
+
const cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
|
|
18532
18785
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
18533
18786
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
18534
18787
|
await saveCache(remotePath, cache);
|
|
@@ -18546,7 +18799,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18546
18799
|
async function inspectContentCache(contentDir, referenced) {
|
|
18547
18800
|
const report = {
|
|
18548
18801
|
path: contentDir,
|
|
18549
|
-
exists:
|
|
18802
|
+
exists: existsSync28(contentDir),
|
|
18550
18803
|
files: 0,
|
|
18551
18804
|
bytes: 0,
|
|
18552
18805
|
valid: 0,
|
|
@@ -18567,12 +18820,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
18567
18820
|
continue;
|
|
18568
18821
|
}
|
|
18569
18822
|
report.files++;
|
|
18570
|
-
const file =
|
|
18823
|
+
const file = path36.join(contentDir, entry.name);
|
|
18571
18824
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
18572
18825
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
18573
18826
|
let bytes;
|
|
18574
18827
|
try {
|
|
18575
|
-
bytes = await
|
|
18828
|
+
bytes = await readFile26(file);
|
|
18576
18829
|
report.bytes += bytes.length;
|
|
18577
18830
|
} catch (err) {
|
|
18578
18831
|
report.invalid.push({
|
|
@@ -18610,7 +18863,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
18610
18863
|
async function inspectRemoteHashCache(remotePath) {
|
|
18611
18864
|
const report = {
|
|
18612
18865
|
path: remotePath,
|
|
18613
|
-
exists:
|
|
18866
|
+
exists: existsSync28(remotePath),
|
|
18614
18867
|
entries: 0,
|
|
18615
18868
|
valid: 0,
|
|
18616
18869
|
invalidIds: [],
|
|
@@ -18619,7 +18872,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
18619
18872
|
if (!report.exists) return report;
|
|
18620
18873
|
let cache;
|
|
18621
18874
|
try {
|
|
18622
|
-
cache = parseCache(JSON.parse(await
|
|
18875
|
+
cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
|
|
18623
18876
|
} catch (err) {
|
|
18624
18877
|
report.error = err instanceof Error ? err.message : String(err);
|
|
18625
18878
|
return report;
|
|
@@ -18683,7 +18936,7 @@ async function runAccept(args) {
|
|
|
18683
18936
|
let intact = true;
|
|
18684
18937
|
for (const output of entry.outputs) {
|
|
18685
18938
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
18686
|
-
if (!
|
|
18939
|
+
if (!existsSync29(file) || await sha256File(file) !== output.sha256) {
|
|
18687
18940
|
intact = false;
|
|
18688
18941
|
break;
|
|
18689
18942
|
}
|
|
@@ -18738,42 +18991,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
18738
18991
|
}
|
|
18739
18992
|
|
|
18740
18993
|
// src/cli/commands/init.ts
|
|
18741
|
-
import
|
|
18742
|
-
import { existsSync as
|
|
18994
|
+
import path37 from "path";
|
|
18995
|
+
import { existsSync as existsSync30 } from "fs";
|
|
18743
18996
|
async function runInit(args) {
|
|
18744
18997
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
18745
|
-
const root =
|
|
18746
|
-
if (!
|
|
18998
|
+
const root = path37.resolve(args.from);
|
|
18999
|
+
if (!existsSync30(root)) throw new Error(`No directory at ${root}`);
|
|
18747
19000
|
const generator = args.generator ?? "map";
|
|
18748
19001
|
if (generator !== "1dir" && generator !== "map") {
|
|
18749
19002
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
18750
19003
|
}
|
|
18751
|
-
const target =
|
|
19004
|
+
const target = path37.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
18752
19005
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
18753
19006
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
18754
19007
|
const manifest = buildManifest(
|
|
18755
|
-
args.name ??
|
|
19008
|
+
args.name ?? path37.basename(path37.dirname(target)),
|
|
18756
19009
|
args.styles[0] ?? "base",
|
|
18757
19010
|
generator,
|
|
18758
|
-
|
|
19011
|
+
path37.relative(path37.dirname(target), root) || ".",
|
|
18759
19012
|
assets
|
|
18760
19013
|
);
|
|
18761
19014
|
await writeManifestFile(target, manifest);
|
|
18762
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
19015
|
+
log(` scanned ${assets.length} PNG(s) under ${path37.relative(process.cwd(), root)}`);
|
|
18763
19016
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
18764
|
-
log(` wrote ${
|
|
19017
|
+
log(` wrote ${path37.relative(process.cwd(), target)}`);
|
|
18765
19018
|
log(`
|
|
18766
19019
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
18767
19020
|
log(` PixelLab account instead of inventing them:`);
|
|
18768
19021
|
log(`
|
|
18769
|
-
pixelkiln adopt --manifest ${
|
|
19022
|
+
pixelkiln adopt --manifest ${path37.relative(process.cwd(), target)} --write-prompts
|
|
18770
19023
|
`);
|
|
18771
19024
|
}
|
|
18772
19025
|
|
|
18773
19026
|
// src/pipeline/doctor.ts
|
|
18774
|
-
import { constants, existsSync as
|
|
19027
|
+
import { constants, existsSync as existsSync31 } from "fs";
|
|
18775
19028
|
import { access, stat as stat4 } from "fs/promises";
|
|
18776
|
-
import
|
|
19029
|
+
import path38 from "path";
|
|
18777
19030
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
18778
19031
|
const checks = [];
|
|
18779
19032
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -18784,8 +19037,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18784
19037
|
);
|
|
18785
19038
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
18786
19039
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
18787
|
-
|
|
18788
|
-
...spec.quality ? [
|
|
19040
|
+
path38.dirname(spec.outFile),
|
|
19041
|
+
...spec.quality ? [path38.dirname(spec.quality.outFile)] : []
|
|
18789
19042
|
]))];
|
|
18790
19043
|
const unwritable = [];
|
|
18791
19044
|
for (const dir of dirs) {
|
|
@@ -18808,7 +19061,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18808
19061
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
18809
19062
|
const output = entry.outputs[index];
|
|
18810
19063
|
const spec = specByKey.get(key);
|
|
18811
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
19064
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path38.dirname(lockPath));
|
|
18812
19065
|
const owner = outputOwners.get(absolute);
|
|
18813
19066
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
18814
19067
|
outputOwners.set(absolute, key);
|
|
@@ -18823,7 +19076,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18823
19076
|
} else {
|
|
18824
19077
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
18825
19078
|
}
|
|
18826
|
-
const cacheDir =
|
|
19079
|
+
const cacheDir = path38.join(path38.dirname(lockPath), ".pixelkiln", "cache");
|
|
18827
19080
|
const stranded = [];
|
|
18828
19081
|
const invalidState = [];
|
|
18829
19082
|
const staleMapUrls = [];
|
|
@@ -18839,11 +19092,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18839
19092
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
18840
19093
|
let hasCache = false;
|
|
18841
19094
|
for (const output of entry.outputs) {
|
|
18842
|
-
const cached =
|
|
19095
|
+
const cached = path38.join(
|
|
18843
19096
|
cacheDir,
|
|
18844
19097
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
18845
19098
|
);
|
|
18846
|
-
if (
|
|
19099
|
+
if (existsSync31(cached) && await sha256File(cached) === output.sha256) {
|
|
18847
19100
|
hasCache = true;
|
|
18848
19101
|
break;
|
|
18849
19102
|
}
|
|
@@ -18939,16 +19192,16 @@ async function nearestExistingDirectory(start) {
|
|
|
18939
19192
|
if ((await stat4(current)).isDirectory()) return current;
|
|
18940
19193
|
} catch {
|
|
18941
19194
|
}
|
|
18942
|
-
const parent =
|
|
19195
|
+
const parent = path38.dirname(current);
|
|
18943
19196
|
if (parent === current) return current;
|
|
18944
19197
|
current = parent;
|
|
18945
19198
|
}
|
|
18946
19199
|
}
|
|
18947
19200
|
|
|
18948
19201
|
// src/pipeline/audit.ts
|
|
18949
|
-
import { readFile as
|
|
18950
|
-
import { existsSync as
|
|
18951
|
-
import
|
|
19202
|
+
import { readFile as readFile27 } from "fs/promises";
|
|
19203
|
+
import { existsSync as existsSync32 } from "fs";
|
|
19204
|
+
import path39 from "path";
|
|
18952
19205
|
function paletteDistance(asset, reference) {
|
|
18953
19206
|
if (!asset.length || !reference.length) return 0;
|
|
18954
19207
|
let total = 0;
|
|
@@ -18984,12 +19237,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
18984
19237
|
const unreadable = [];
|
|
18985
19238
|
for (const spec of mine) {
|
|
18986
19239
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
18987
|
-
if (!
|
|
19240
|
+
if (!existsSync32(output.absolutePath)) {
|
|
18988
19241
|
missing.push(output.id);
|
|
18989
19242
|
continue;
|
|
18990
19243
|
}
|
|
18991
19244
|
try {
|
|
18992
|
-
const png = decodePng(await
|
|
19245
|
+
const png = decodePng(await readFile27(output.absolutePath));
|
|
18993
19246
|
const palette = extractPalette(png, 12);
|
|
18994
19247
|
assets.push({
|
|
18995
19248
|
assetId: spec.assetId,
|
|
@@ -19013,10 +19266,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
19013
19266
|
let referenceFromStyleImages = false;
|
|
19014
19267
|
const refPalettes = [];
|
|
19015
19268
|
for (const img of style.styleImages) {
|
|
19016
|
-
const abs =
|
|
19017
|
-
if (!
|
|
19269
|
+
const abs = path39.resolve(loaded.root, img.path);
|
|
19270
|
+
if (!existsSync32(abs)) continue;
|
|
19018
19271
|
try {
|
|
19019
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
19272
|
+
refPalettes.push(extractPalette(decodePng(await readFile27(abs)), 12));
|
|
19020
19273
|
} catch {
|
|
19021
19274
|
}
|
|
19022
19275
|
}
|
|
@@ -19244,24 +19497,24 @@ async function runAudit(args) {
|
|
|
19244
19497
|
}
|
|
19245
19498
|
|
|
19246
19499
|
// src/cli/commands/pack.ts
|
|
19247
|
-
import
|
|
19248
|
-
import { readFile as
|
|
19500
|
+
import path41 from "path";
|
|
19501
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
19249
19502
|
|
|
19250
19503
|
// src/pipeline/pack.ts
|
|
19251
19504
|
import { readFileSync as readFileSync5 } from "fs";
|
|
19252
|
-
import
|
|
19505
|
+
import path40 from "path";
|
|
19253
19506
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
19254
19507
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
19255
19508
|
if (!Array.isArray(raw) || !raw.length) {
|
|
19256
19509
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
19257
19510
|
}
|
|
19258
|
-
const baseDir =
|
|
19511
|
+
const baseDir = path40.dirname(path40.resolve(inputsFilePath));
|
|
19259
19512
|
return raw.map((entry, i) => {
|
|
19260
19513
|
const e = entry;
|
|
19261
19514
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
19262
19515
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
19263
19516
|
}
|
|
19264
|
-
return { id: e.id, path:
|
|
19517
|
+
return { id: e.id, path: path40.resolve(baseDir, e.path) };
|
|
19265
19518
|
});
|
|
19266
19519
|
}
|
|
19267
19520
|
function packSprites(inputs, options = {}) {
|
|
@@ -19345,7 +19598,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19345
19598
|
}
|
|
19346
19599
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
19347
19600
|
id,
|
|
19348
|
-
path:
|
|
19601
|
+
path: path40.resolve(source)
|
|
19349
19602
|
}));
|
|
19350
19603
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
19351
19604
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -19369,7 +19622,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19369
19622
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
19370
19623
|
const source = sources[id];
|
|
19371
19624
|
if (source && outputs.length <= 1) {
|
|
19372
|
-
inputs.push({ id, path:
|
|
19625
|
+
inputs.push({ id, path: path40.resolve(manifestDir, source) });
|
|
19373
19626
|
continue;
|
|
19374
19627
|
}
|
|
19375
19628
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -19403,7 +19656,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19403
19656
|
}
|
|
19404
19657
|
}
|
|
19405
19658
|
for (const [id, source] of Object.entries(sources)) {
|
|
19406
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
19659
|
+
if (!locked.has(id)) inputs.push({ id, path: path40.resolve(manifestDir, source) });
|
|
19407
19660
|
}
|
|
19408
19661
|
if (!inputs.length) {
|
|
19409
19662
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -19511,7 +19764,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19511
19764
|
const source = sources[id];
|
|
19512
19765
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
19513
19766
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
19514
|
-
placements.push({ id, path:
|
|
19767
|
+
placements.push({ id, path: path40.resolve(manifestDir, source), cell });
|
|
19515
19768
|
continue;
|
|
19516
19769
|
}
|
|
19517
19770
|
if (source && entry) {
|
|
@@ -19544,7 +19797,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19544
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}`
|
|
19545
19798
|
);
|
|
19546
19799
|
}
|
|
19547
|
-
const basePng = mount.base ? readFileSync5(
|
|
19800
|
+
const basePng = mount.base ? readFileSync5(path40.resolve(manifestDir, mount.base)) : void 0;
|
|
19548
19801
|
const mounted = mountSprites(placements, {
|
|
19549
19802
|
cellWidth: mount.cellWidth,
|
|
19550
19803
|
cellHeight: mount.cellHeight,
|
|
@@ -19554,7 +19807,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19554
19807
|
...mounted,
|
|
19555
19808
|
sources: mount.base ? [{
|
|
19556
19809
|
id: "$base",
|
|
19557
|
-
path:
|
|
19810
|
+
path: path40.resolve(manifestDir, mount.base),
|
|
19558
19811
|
sha256: sha256(basePng),
|
|
19559
19812
|
included: true
|
|
19560
19813
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -19958,12 +20211,12 @@ async function runPack(args) {
|
|
|
19958
20211
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
19959
20212
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
19960
20213
|
}
|
|
19961
|
-
const raw = JSON.parse(await
|
|
20214
|
+
const raw = JSON.parse(await readFile28(path41.resolve(args.inputs), "utf8"));
|
|
19962
20215
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
19963
20216
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
19964
20217
|
const format = sheetFormat(args);
|
|
19965
|
-
const base =
|
|
19966
|
-
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`) });
|
|
19967
20220
|
const outputs = [
|
|
19968
20221
|
{ path: `${base}.png`, data: png },
|
|
19969
20222
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19976,7 +20229,7 @@ async function runPack(args) {
|
|
|
19976
20229
|
log(
|
|
19977
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})`
|
|
19978
20231
|
);
|
|
19979
|
-
log(` ${
|
|
20232
|
+
log(` ${path41.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
19980
20233
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
19981
20234
|
return;
|
|
19982
20235
|
}
|
|
@@ -19984,7 +20237,7 @@ async function runPack(args) {
|
|
|
19984
20237
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
19985
20238
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
19986
20239
|
}
|
|
19987
|
-
const manifestDir =
|
|
20240
|
+
const manifestDir = path41.dirname(path41.resolve(args.manifest));
|
|
19988
20241
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
19989
20242
|
for (const styleId of styleIds) {
|
|
19990
20243
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -19999,9 +20252,9 @@ async function runPack(args) {
|
|
|
19999
20252
|
sourceOverrides: qualitySources,
|
|
20000
20253
|
sources: manifestSources(loaded.manifest, styleId)
|
|
20001
20254
|
});
|
|
20002
|
-
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`);
|
|
20003
20256
|
const format = sheetFormat(args);
|
|
20004
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
20257
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path41.basename(`${base}.png`) });
|
|
20005
20258
|
const outputs = [
|
|
20006
20259
|
{ path: `${base}.png`, data: png },
|
|
20007
20260
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -20033,13 +20286,13 @@ async function runPack(args) {
|
|
|
20033
20286
|
log(
|
|
20034
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})`
|
|
20035
20288
|
);
|
|
20036
|
-
log(` ${
|
|
20289
|
+
log(` ${path41.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
20037
20290
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20038
20291
|
}
|
|
20039
20292
|
}
|
|
20040
20293
|
async function runMount(args) {
|
|
20041
20294
|
const { loaded, lock } = await openProject2(args);
|
|
20042
|
-
const manifestDir =
|
|
20295
|
+
const manifestDir = path41.dirname(path41.resolve(args.manifest));
|
|
20043
20296
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
20044
20297
|
for (const styleId of styleIds) {
|
|
20045
20298
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -20078,9 +20331,9 @@ async function runMount(args) {
|
|
|
20078
20331
|
sources,
|
|
20079
20332
|
outputRoles
|
|
20080
20333
|
);
|
|
20081
|
-
const out =
|
|
20334
|
+
const out = path41.resolve(manifestDir, style.mount.out);
|
|
20082
20335
|
const format = sheetFormat(args);
|
|
20083
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
20336
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path41.basename(out) });
|
|
20084
20337
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
20085
20338
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
20086
20339
|
const outputs = [
|
|
@@ -20100,7 +20353,7 @@ async function runMount(args) {
|
|
|
20100
20353
|
await provenanceFile("$lock", args.lock),
|
|
20101
20354
|
...qualityRecords,
|
|
20102
20355
|
...artifactSources.filter(
|
|
20103
|
-
(source) => source.id !== "$base" ||
|
|
20356
|
+
(source) => source.id !== "$base" || path41.resolve(source.path) !== out
|
|
20104
20357
|
)
|
|
20105
20358
|
],
|
|
20106
20359
|
options: {
|
|
@@ -20114,14 +20367,14 @@ async function runMount(args) {
|
|
|
20114
20367
|
log(
|
|
20115
20368
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
20116
20369
|
);
|
|
20117
|
-
log(` ${
|
|
20370
|
+
log(` ${path41.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
20118
20371
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20119
20372
|
}
|
|
20120
20373
|
}
|
|
20121
20374
|
async function runExport(args) {
|
|
20122
20375
|
const { loaded, specs, lock } = await openProject2(args);
|
|
20123
20376
|
const format = args.format ?? "generic";
|
|
20124
|
-
const manifestDir =
|
|
20377
|
+
const manifestDir = path41.dirname(path41.resolve(args.manifest));
|
|
20125
20378
|
const selected = specs.filter((spec) => {
|
|
20126
20379
|
if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
|
|
20127
20380
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -20137,12 +20390,12 @@ async function runExport(args) {
|
|
|
20137
20390
|
for (const spec of selected) {
|
|
20138
20391
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
20139
20392
|
const style = loaded.manifest.styles[spec.styleId];
|
|
20140
|
-
const defaultBase =
|
|
20141
|
-
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;
|
|
20142
20395
|
const result = exportTileset(entry, spec, {
|
|
20143
20396
|
format,
|
|
20144
20397
|
manifestDir,
|
|
20145
|
-
imageName:
|
|
20398
|
+
imageName: path41.basename(`${base}.png`),
|
|
20146
20399
|
columns: args.columns
|
|
20147
20400
|
});
|
|
20148
20401
|
const outputs = [
|
|
@@ -20160,7 +20413,7 @@ async function runExport(args) {
|
|
|
20160
20413
|
asset: spec.assetId,
|
|
20161
20414
|
columns: args.columns ?? null,
|
|
20162
20415
|
format,
|
|
20163
|
-
image:
|
|
20416
|
+
image: path41.basename(`${base}.png`),
|
|
20164
20417
|
providerRules: result.generic.providerRules,
|
|
20165
20418
|
style: spec.styleId,
|
|
20166
20419
|
tileType: spec.tileType ?? null
|
|
@@ -20170,7 +20423,7 @@ async function runExport(args) {
|
|
|
20170
20423
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
20171
20424
|
);
|
|
20172
20425
|
log(
|
|
20173
|
-
` ${
|
|
20426
|
+
` ${path41.relative(process.cwd(), base)}.png + ${path41.basename(base)}${result.extension} + .pixelkiln.json`
|
|
20174
20427
|
);
|
|
20175
20428
|
}
|
|
20176
20429
|
}
|
|
@@ -20183,23 +20436,23 @@ function sheetFormat(args) {
|
|
|
20183
20436
|
}
|
|
20184
20437
|
|
|
20185
20438
|
// src/cli/commands/pixellab-utils.ts
|
|
20186
|
-
import
|
|
20439
|
+
import path43 from "path";
|
|
20187
20440
|
|
|
20188
20441
|
// src/pixellab-utils.ts
|
|
20189
|
-
import { existsSync as
|
|
20190
|
-
import { mkdir as mkdir13, readFile as
|
|
20191
|
-
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";
|
|
20192
20445
|
var UNZOOM_MIN_SIDE = 256;
|
|
20193
20446
|
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
20194
20447
|
function defaultUnzoomOut(input) {
|
|
20195
|
-
const ext =
|
|
20448
|
+
const ext = path42.extname(input);
|
|
20196
20449
|
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
20197
20450
|
}
|
|
20198
20451
|
async function unzoomFile(client2, input, opts = {}) {
|
|
20199
|
-
const out =
|
|
20200
|
-
if (
|
|
20201
|
-
if (
|
|
20202
|
-
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);
|
|
20203
20456
|
const metadata = imageMetadata(bytes);
|
|
20204
20457
|
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
20205
20458
|
const { width, height, format } = metadata;
|
|
@@ -20212,22 +20465,22 @@ async function unzoomFile(client2, input, opts = {}) {
|
|
|
20212
20465
|
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
20213
20466
|
}
|
|
20214
20467
|
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
20215
|
-
await mkdir13(
|
|
20216
|
-
await
|
|
20468
|
+
await mkdir13(path42.dirname(out), { recursive: true });
|
|
20469
|
+
await writeFile18(out, res.png);
|
|
20217
20470
|
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
20218
20471
|
}
|
|
20219
20472
|
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
20220
20473
|
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
20221
20474
|
var FONT_COST_GENERATIONS = 25;
|
|
20222
20475
|
function fontOutputPaths(out) {
|
|
20223
|
-
const base =
|
|
20476
|
+
const base = path42.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
20224
20477
|
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
20225
20478
|
}
|
|
20226
20479
|
async function generateFont(client2, opts) {
|
|
20227
20480
|
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
20228
20481
|
if (!opts.force) {
|
|
20229
20482
|
for (const file of [ttf, atlas]) {
|
|
20230
|
-
if (
|
|
20483
|
+
if (existsSync33(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
20231
20484
|
}
|
|
20232
20485
|
}
|
|
20233
20486
|
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -20254,9 +20507,9 @@ async function generateFont(client2, opts) {
|
|
|
20254
20507
|
client2.download(job.download_ttf_url),
|
|
20255
20508
|
client2.download(job.download_atlas_url)
|
|
20256
20509
|
]);
|
|
20257
|
-
await mkdir13(
|
|
20258
|
-
await
|
|
20259
|
-
await
|
|
20510
|
+
await mkdir13(path42.dirname(ttf), { recursive: true });
|
|
20511
|
+
await writeFile18(ttf, ttfBytes);
|
|
20512
|
+
await writeFile18(atlas, atlasBytes);
|
|
20260
20513
|
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
20261
20514
|
}
|
|
20262
20515
|
if (Date.now() - started > timeout) {
|
|
@@ -20270,7 +20523,7 @@ async function generateFont(client2, opts) {
|
|
|
20270
20523
|
|
|
20271
20524
|
// src/cli/commands/pixellab-utils.ts
|
|
20272
20525
|
function loadKeys(args) {
|
|
20273
|
-
loadEnvFiles(
|
|
20526
|
+
loadEnvFiles(path43.dirname(path43.resolve(args.manifest)));
|
|
20274
20527
|
loadEnvFiles(process.cwd());
|
|
20275
20528
|
}
|
|
20276
20529
|
async function runUnzoom(args) {
|
|
@@ -20336,19 +20589,19 @@ async function runFont(args) {
|
|
|
20336
20589
|
}
|
|
20337
20590
|
|
|
20338
20591
|
// src/cli/commands/quality.ts
|
|
20339
|
-
import
|
|
20340
|
-
import { readFile as
|
|
20592
|
+
import path45 from "path";
|
|
20593
|
+
import { readFile as readFile31 } from "fs/promises";
|
|
20341
20594
|
|
|
20342
20595
|
// src/pipeline/quality-regression.ts
|
|
20343
|
-
import
|
|
20344
|
-
import { existsSync as
|
|
20345
|
-
import { readFile as
|
|
20346
|
-
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";
|
|
20347
20600
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
20348
20601
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
20349
20602
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
20350
|
-
var QualityPathSchema =
|
|
20351
|
-
(value) => !
|
|
20603
|
+
var QualityPathSchema = z13.string().min(1).refine(
|
|
20604
|
+
(value) => !path44.posix.isAbsolute(value) && !value.includes("\\"),
|
|
20352
20605
|
"expected a portable relative path"
|
|
20353
20606
|
);
|
|
20354
20607
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -20361,53 +20614,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
20361
20614
|
maxEdgeContrastDrop: 0.03,
|
|
20362
20615
|
maxIsolatedPixelIncrease: 5e-3
|
|
20363
20616
|
};
|
|
20364
|
-
var QualityToleranceSchema =
|
|
20365
|
-
requireExactHash:
|
|
20366
|
-
maxColorCountIncrease:
|
|
20367
|
-
maxNewColors:
|
|
20368
|
-
maxTransparencyDelta:
|
|
20369
|
-
maxPartialAlphaIncrease:
|
|
20370
|
-
maxEdgeDensityDelta:
|
|
20371
|
-
maxEdgeContrastDrop:
|
|
20372
|
-
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)
|
|
20373
20626
|
}).strict();
|
|
20374
|
-
var ImageQualityMetricsSchema =
|
|
20375
|
-
sha256:
|
|
20376
|
-
width:
|
|
20377
|
-
height:
|
|
20378
|
-
colorCount:
|
|
20379
|
-
colors:
|
|
20380
|
-
transparency:
|
|
20381
|
-
partialAlpha:
|
|
20382
|
-
edgeDensity:
|
|
20383
|
-
meanEdgeContrast:
|
|
20384
|
-
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)
|
|
20385
20638
|
}).strict();
|
|
20386
|
-
var QualityBaselineCaseSchema =
|
|
20387
|
-
id:
|
|
20639
|
+
var QualityBaselineCaseSchema = z13.object({
|
|
20640
|
+
id: z13.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
20388
20641
|
file: QualityPathSchema,
|
|
20389
|
-
record:
|
|
20642
|
+
record: z13.object({
|
|
20390
20643
|
path: QualityPathSchema,
|
|
20391
|
-
sha256:
|
|
20644
|
+
sha256: z13.string().regex(SHA256_RE)
|
|
20392
20645
|
}).strict().optional(),
|
|
20393
20646
|
expected: ImageQualityMetricsSchema,
|
|
20394
20647
|
tolerances: QualityToleranceSchema
|
|
20395
20648
|
}).strict();
|
|
20396
|
-
var QualityBaselineSchema =
|
|
20397
|
-
$schema:
|
|
20398
|
-
format:
|
|
20399
|
-
schemaVersion:
|
|
20400
|
-
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)
|
|
20401
20654
|
}).strict().superRefine((baseline, context) => {
|
|
20402
20655
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
20403
20656
|
if (new Set(ids).size !== ids.length) {
|
|
20404
|
-
context.addIssue({ code:
|
|
20657
|
+
context.addIssue({ code: z13.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
20405
20658
|
}
|
|
20406
20659
|
});
|
|
20407
|
-
var QualityInputSchema =
|
|
20408
|
-
id:
|
|
20409
|
-
path:
|
|
20410
|
-
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(),
|
|
20411
20664
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
20412
20665
|
}).strict();
|
|
20413
20666
|
function message4(error) {
|
|
@@ -20495,10 +20748,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
20495
20748
|
};
|
|
20496
20749
|
}
|
|
20497
20750
|
async function measureImageQuality(file) {
|
|
20498
|
-
const absolute =
|
|
20751
|
+
const absolute = path44.resolve(file);
|
|
20499
20752
|
let bytes;
|
|
20500
20753
|
try {
|
|
20501
|
-
bytes = await
|
|
20754
|
+
bytes = await readFile30(absolute);
|
|
20502
20755
|
} catch (error) {
|
|
20503
20756
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
20504
20757
|
}
|
|
@@ -20509,7 +20762,7 @@ async function measureImageQuality(file) {
|
|
|
20509
20762
|
}
|
|
20510
20763
|
}
|
|
20511
20764
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
20512
|
-
const parsed =
|
|
20765
|
+
const parsed = z13.array(QualityInputSchema).min(1).safeParse(raw);
|
|
20513
20766
|
if (!parsed.success) {
|
|
20514
20767
|
throw new Error(
|
|
20515
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")
|
|
@@ -20517,11 +20770,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20517
20770
|
}
|
|
20518
20771
|
const ids = parsed.data.map((entry) => entry.id);
|
|
20519
20772
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
20520
|
-
const root =
|
|
20773
|
+
const root = path44.dirname(path44.resolve(inputsFilePath));
|
|
20521
20774
|
return parsed.data.map((entry) => ({
|
|
20522
20775
|
id: entry.id,
|
|
20523
|
-
file:
|
|
20524
|
-
...entry.record ? { record:
|
|
20776
|
+
file: path44.resolve(root, entry.path),
|
|
20777
|
+
...entry.record ? { record: path44.resolve(root, entry.record) } : {},
|
|
20525
20778
|
tolerances: QualityToleranceSchema.parse({
|
|
20526
20779
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
20527
20780
|
...entry.tolerances
|
|
@@ -20529,7 +20782,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20529
20782
|
}));
|
|
20530
20783
|
}
|
|
20531
20784
|
function portableRelative2(from, to) {
|
|
20532
|
-
return
|
|
20785
|
+
return path44.relative(from, path44.resolve(to)).split(path44.sep).join("/") || ".";
|
|
20533
20786
|
}
|
|
20534
20787
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
20535
20788
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -20544,20 +20797,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
20544
20797
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
20545
20798
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
20546
20799
|
}
|
|
20547
|
-
const outputs = record.outputs.map((output) =>
|
|
20548
|
-
if (!outputs.includes(
|
|
20800
|
+
const outputs = record.outputs.map((output) => path44.resolve(path44.dirname(recordPath), output.path));
|
|
20801
|
+
if (!outputs.includes(path44.resolve(imagePath))) {
|
|
20549
20802
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
20550
20803
|
}
|
|
20551
20804
|
}
|
|
20552
20805
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
20553
20806
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
20554
|
-
const absolute =
|
|
20807
|
+
const absolute = path44.resolve(baselinePath);
|
|
20555
20808
|
for (const input of inputs) {
|
|
20556
|
-
if (
|
|
20809
|
+
if (path44.resolve(input.file) === absolute || input.record && path44.resolve(input.record) === absolute) {
|
|
20557
20810
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
20558
20811
|
}
|
|
20559
20812
|
}
|
|
20560
|
-
const root =
|
|
20813
|
+
const root = path44.dirname(absolute);
|
|
20561
20814
|
const cases = [];
|
|
20562
20815
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
20563
20816
|
const expected = await measureImageQuality(input.file);
|
|
@@ -20584,8 +20837,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
20584
20837
|
cases
|
|
20585
20838
|
});
|
|
20586
20839
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
20587
|
-
if (
|
|
20588
|
-
const current = await
|
|
20840
|
+
if (existsSync34(absolute) && !options.force) {
|
|
20841
|
+
const current = await readFile30(absolute);
|
|
20589
20842
|
if (!current.equals(data)) {
|
|
20590
20843
|
throw new OverwriteRefusedError(
|
|
20591
20844
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -20597,10 +20850,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
20597
20850
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
20598
20851
|
}
|
|
20599
20852
|
async function readQualityBaseline(baselinePath) {
|
|
20600
|
-
const absolute =
|
|
20853
|
+
const absolute = path44.resolve(baselinePath);
|
|
20601
20854
|
let raw;
|
|
20602
20855
|
try {
|
|
20603
|
-
raw = JSON.parse(await
|
|
20856
|
+
raw = JSON.parse(await readFile30(absolute, "utf8"));
|
|
20604
20857
|
} catch (error) {
|
|
20605
20858
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
20606
20859
|
}
|
|
@@ -20670,12 +20923,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
20670
20923
|
return { violations, warnings };
|
|
20671
20924
|
}
|
|
20672
20925
|
async function checkQualityBaseline(baselinePath) {
|
|
20673
|
-
const absolute =
|
|
20926
|
+
const absolute = path44.resolve(baselinePath);
|
|
20674
20927
|
const baseline = await readQualityBaseline(absolute);
|
|
20675
|
-
const root =
|
|
20928
|
+
const root = path44.dirname(absolute);
|
|
20676
20929
|
const cases = [];
|
|
20677
20930
|
for (const entry of baseline.cases) {
|
|
20678
|
-
const file =
|
|
20931
|
+
const file = path44.resolve(root, entry.file);
|
|
20679
20932
|
let actual = null;
|
|
20680
20933
|
const violations = [];
|
|
20681
20934
|
const warnings = [];
|
|
@@ -20688,7 +20941,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
20688
20941
|
violations.push(message4(error));
|
|
20689
20942
|
}
|
|
20690
20943
|
if (entry.record) {
|
|
20691
|
-
const recordPath =
|
|
20944
|
+
const recordPath = path44.resolve(root, entry.record.path);
|
|
20692
20945
|
try {
|
|
20693
20946
|
const recordHash = await sha256File(recordPath);
|
|
20694
20947
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -20730,15 +20983,15 @@ async function runQuality(args) {
|
|
|
20730
20983
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
20731
20984
|
let raw;
|
|
20732
20985
|
try {
|
|
20733
|
-
raw = JSON.parse(await
|
|
20986
|
+
raw = JSON.parse(await readFile31(path45.resolve(args.inputs), "utf8"));
|
|
20734
20987
|
} catch (error) {
|
|
20735
20988
|
throw new Error(
|
|
20736
|
-
`Could not read quality inputs ${
|
|
20989
|
+
`Could not read quality inputs ${path45.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
20737
20990
|
{ cause: error }
|
|
20738
20991
|
);
|
|
20739
20992
|
}
|
|
20740
20993
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
20741
|
-
if (
|
|
20994
|
+
if (path45.resolve(args.inputs) === path45.resolve(args.out)) {
|
|
20742
20995
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
20743
20996
|
}
|
|
20744
20997
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -20751,7 +21004,7 @@ async function runQuality(args) {
|
|
|
20751
21004
|
}, null, 2));
|
|
20752
21005
|
} else {
|
|
20753
21006
|
log(
|
|
20754
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
21007
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path45.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
20755
21008
|
);
|
|
20756
21009
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
20757
21010
|
}
|
|
@@ -20777,54 +21030,54 @@ async function runQuality(args) {
|
|
|
20777
21030
|
}
|
|
20778
21031
|
|
|
20779
21032
|
// src/cli/commands/recipe.ts
|
|
20780
|
-
import
|
|
21033
|
+
import path47 from "path";
|
|
20781
21034
|
|
|
20782
21035
|
// src/recipes.ts
|
|
20783
|
-
import
|
|
20784
|
-
import { existsSync as
|
|
20785
|
-
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";
|
|
20786
21039
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
20787
|
-
import { z as
|
|
21040
|
+
import { z as z14 } from "zod";
|
|
20788
21041
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
20789
21042
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
20790
21043
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
20791
21044
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
20792
21045
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
20793
|
-
var PortablePathSchema =
|
|
20794
|
-
if (value.includes("\\") ||
|
|
21046
|
+
var PortablePathSchema = z14.string().min(1).refine((value) => {
|
|
21047
|
+
if (value.includes("\\") || path46.posix.isAbsolute(value)) return false;
|
|
20795
21048
|
const parts = value.split("/");
|
|
20796
21049
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
20797
21050
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
20798
|
-
var RecipeBindingSchema =
|
|
20799
|
-
nodeId:
|
|
20800
|
-
input:
|
|
21051
|
+
var RecipeBindingSchema = z14.object({
|
|
21052
|
+
nodeId: z14.string().min(1),
|
|
21053
|
+
input: z14.string().min(1)
|
|
20801
21054
|
}).strict();
|
|
20802
|
-
var RecipeSchema =
|
|
20803
|
-
$schema:
|
|
20804
|
-
format:
|
|
20805
|
-
schemaVersion:
|
|
20806
|
-
id:
|
|
20807
|
-
version:
|
|
20808
|
-
provider:
|
|
20809
|
-
summary:
|
|
20810
|
-
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({
|
|
20811
21064
|
path: PortablePathSchema,
|
|
20812
|
-
role:
|
|
20813
|
-
sha256:
|
|
21065
|
+
role: z14.enum(["workflow", "reference"]),
|
|
21066
|
+
sha256: z14.string().regex(SHA256_RE2)
|
|
20814
21067
|
}).strict()).default([]),
|
|
20815
|
-
models:
|
|
21068
|
+
models: z14.array(z14.object({
|
|
20816
21069
|
path: PortablePathSchema,
|
|
20817
|
-
sha256:
|
|
20818
|
-
source:
|
|
20819
|
-
license:
|
|
21070
|
+
sha256: z14.string().regex(SHA256_RE2),
|
|
21071
|
+
source: z14.string().url(),
|
|
21072
|
+
license: z14.string().min(1)
|
|
20820
21073
|
}).strict()).default([]),
|
|
20821
|
-
styleId:
|
|
21074
|
+
styleId: z14.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20822
21075
|
style: StyleSchema,
|
|
20823
|
-
workflow:
|
|
21076
|
+
workflow: z14.object({
|
|
20824
21077
|
path: PortablePathSchema,
|
|
20825
|
-
outputNodeId:
|
|
20826
|
-
numImages:
|
|
20827
|
-
bindings:
|
|
21078
|
+
outputNodeId: z14.string().min(1),
|
|
21079
|
+
numImages: z14.number().int().min(1).max(16),
|
|
21080
|
+
bindings: z14.object({
|
|
20828
21081
|
prompt: RecipeBindingSchema,
|
|
20829
21082
|
width: RecipeBindingSchema.optional(),
|
|
20830
21083
|
height: RecipeBindingSchema.optional(),
|
|
@@ -20835,67 +21088,67 @@ var RecipeSchema = z13.object({
|
|
|
20835
21088
|
strength: RecipeBindingSchema.optional()
|
|
20836
21089
|
}).strict()
|
|
20837
21090
|
}).strict().optional(),
|
|
20838
|
-
quality:
|
|
20839
|
-
stage:
|
|
20840
|
-
workingCanvas:
|
|
20841
|
-
width:
|
|
20842
|
-
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)
|
|
20843
21096
|
}).strict(),
|
|
20844
|
-
recommendedNativeSize:
|
|
20845
|
-
min:
|
|
20846
|
-
max:
|
|
21097
|
+
recommendedNativeSize: z14.object({
|
|
21098
|
+
min: z14.number().int().min(1),
|
|
21099
|
+
max: z14.number().int().min(1)
|
|
20847
21100
|
}).strict(),
|
|
20848
|
-
paletteColors:
|
|
20849
|
-
min:
|
|
20850
|
-
max:
|
|
21101
|
+
paletteColors: z14.object({
|
|
21102
|
+
min: z14.number().int().min(1),
|
|
21103
|
+
max: z14.number().int().min(1)
|
|
20851
21104
|
}).strict(),
|
|
20852
|
-
background:
|
|
20853
|
-
checks:
|
|
21105
|
+
background: z14.enum(["opaque", "transparent", "full-bleed"]),
|
|
21106
|
+
checks: z14.array(z14.enum([
|
|
20854
21107
|
"prompt-coverage",
|
|
20855
21108
|
"native-grid",
|
|
20856
21109
|
"final-palette",
|
|
20857
21110
|
"alpha-edge",
|
|
20858
21111
|
"human-1x"
|
|
20859
21112
|
])).min(1),
|
|
20860
|
-
notes:
|
|
21113
|
+
notes: z14.array(z14.string().min(1)).default([])
|
|
20861
21114
|
}).strict(),
|
|
20862
|
-
integrity:
|
|
20863
|
-
algorithm:
|
|
20864
|
-
digest:
|
|
21115
|
+
integrity: z14.object({
|
|
21116
|
+
algorithm: z14.literal("sha256"),
|
|
21117
|
+
digest: z14.string().regex(SHA256_RE2)
|
|
20865
21118
|
}).strict()
|
|
20866
21119
|
}).strict().superRefine((recipe, context) => {
|
|
20867
21120
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
20868
21121
|
context.addIssue({
|
|
20869
|
-
code:
|
|
21122
|
+
code: z14.ZodIssueCode.custom,
|
|
20870
21123
|
path: ["style", "provider"],
|
|
20871
21124
|
message: "must match the recipe provider"
|
|
20872
21125
|
});
|
|
20873
21126
|
}
|
|
20874
21127
|
const filePaths = recipe.files.map((file) => file.path);
|
|
20875
21128
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
20876
|
-
context.addIssue({ code:
|
|
21129
|
+
context.addIssue({ code: z14.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
20877
21130
|
}
|
|
20878
21131
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
20879
21132
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
20880
|
-
context.addIssue({ code:
|
|
21133
|
+
context.addIssue({ code: z14.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
20881
21134
|
}
|
|
20882
21135
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
20883
21136
|
context.addIssue({
|
|
20884
|
-
code:
|
|
21137
|
+
code: z14.ZodIssueCode.custom,
|
|
20885
21138
|
path: ["files"],
|
|
20886
21139
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
20887
21140
|
});
|
|
20888
21141
|
}
|
|
20889
21142
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
20890
21143
|
context.addIssue({
|
|
20891
|
-
code:
|
|
21144
|
+
code: z14.ZodIssueCode.custom,
|
|
20892
21145
|
path: ["quality", "recommendedNativeSize"],
|
|
20893
21146
|
message: "min cannot exceed max"
|
|
20894
21147
|
});
|
|
20895
21148
|
}
|
|
20896
21149
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
20897
21150
|
context.addIssue({
|
|
20898
|
-
code:
|
|
21151
|
+
code: z14.ZodIssueCode.custom,
|
|
20899
21152
|
path: ["quality", "paletteColors"],
|
|
20900
21153
|
message: "min cannot exceed max"
|
|
20901
21154
|
});
|
|
@@ -20904,7 +21157,7 @@ var RecipeSchema = z13.object({
|
|
|
20904
21157
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
20905
21158
|
)) {
|
|
20906
21159
|
context.addIssue({
|
|
20907
|
-
code:
|
|
21160
|
+
code: z14.ZodIssueCode.custom,
|
|
20908
21161
|
path: ["workflow", "path"],
|
|
20909
21162
|
message: "must name a file whose role is workflow"
|
|
20910
21163
|
});
|
|
@@ -20912,7 +21165,7 @@ var RecipeSchema = z13.object({
|
|
|
20912
21165
|
if (recipe.provider === "comfyui") {
|
|
20913
21166
|
if (!recipe.workflow) {
|
|
20914
21167
|
context.addIssue({
|
|
20915
|
-
code:
|
|
21168
|
+
code: z14.ZodIssueCode.custom,
|
|
20916
21169
|
path: ["workflow"],
|
|
20917
21170
|
message: "ComfyUI recipes require workflow metadata"
|
|
20918
21171
|
});
|
|
@@ -20921,7 +21174,7 @@ var RecipeSchema = z13.object({
|
|
|
20921
21174
|
const options = recipe.style.providerOptions.comfyui;
|
|
20922
21175
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
20923
21176
|
context.addIssue({
|
|
20924
|
-
code:
|
|
21177
|
+
code: z14.ZodIssueCode.custom,
|
|
20925
21178
|
path: ["style", "providerOptions", "comfyui"],
|
|
20926
21179
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
20927
21180
|
});
|
|
@@ -20935,7 +21188,7 @@ var RecipeSchema = z13.object({
|
|
|
20935
21188
|
};
|
|
20936
21189
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
20937
21190
|
context.addIssue({
|
|
20938
|
-
code:
|
|
21191
|
+
code: z14.ZodIssueCode.custom,
|
|
20939
21192
|
path: ["style", "providerOptions", "comfyui"],
|
|
20940
21193
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
20941
21194
|
});
|
|
@@ -20967,10 +21220,10 @@ function message5(error) {
|
|
|
20967
21220
|
return error instanceof Error ? error.message : String(error);
|
|
20968
21221
|
}
|
|
20969
21222
|
async function readRecipeFile(recipePath, bundled) {
|
|
20970
|
-
const absolute =
|
|
21223
|
+
const absolute = path46.resolve(recipePath);
|
|
20971
21224
|
let raw;
|
|
20972
21225
|
try {
|
|
20973
|
-
raw = JSON.parse(await
|
|
21226
|
+
raw = JSON.parse(await readFile32(absolute, "utf8"));
|
|
20974
21227
|
} catch (error) {
|
|
20975
21228
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
20976
21229
|
}
|
|
@@ -20981,13 +21234,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
20981
21234
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
20982
21235
|
);
|
|
20983
21236
|
}
|
|
20984
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
21237
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path46.dirname(absolute), bundled };
|
|
20985
21238
|
}
|
|
20986
21239
|
async function walkRecipeFiles(root) {
|
|
20987
|
-
if (!
|
|
21240
|
+
if (!existsSync35(root)) return [];
|
|
20988
21241
|
const out = [];
|
|
20989
21242
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
20990
|
-
const resolved =
|
|
21243
|
+
const resolved = path46.join(root, entry.name);
|
|
20991
21244
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
20992
21245
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
20993
21246
|
}
|
|
@@ -20998,7 +21251,7 @@ function bundledRecipeRoot() {
|
|
|
20998
21251
|
}
|
|
20999
21252
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
21000
21253
|
const recipes = await Promise.all(
|
|
21001
|
-
(await walkRecipeFiles(
|
|
21254
|
+
(await walkRecipeFiles(path46.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
21002
21255
|
);
|
|
21003
21256
|
return recipes.sort(
|
|
21004
21257
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -21021,9 +21274,9 @@ function parseSelector(selector) {
|
|
|
21021
21274
|
return { id: match[1], version: match[2] };
|
|
21022
21275
|
}
|
|
21023
21276
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
21024
|
-
const local =
|
|
21025
|
-
if (
|
|
21026
|
-
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);
|
|
21027
21280
|
return readRecipeFile(recipePath, false);
|
|
21028
21281
|
}
|
|
21029
21282
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -21037,8 +21290,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
21037
21290
|
return matches[0];
|
|
21038
21291
|
}
|
|
21039
21292
|
async function verifyFile(root, file) {
|
|
21040
|
-
const absolute =
|
|
21041
|
-
if (!
|
|
21293
|
+
const absolute = path46.resolve(root, ...file.path.split("/"));
|
|
21294
|
+
if (!existsSync35(absolute)) {
|
|
21042
21295
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
21043
21296
|
}
|
|
21044
21297
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -21053,7 +21306,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
21053
21306
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
21054
21307
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
21055
21308
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
21056
|
-
const modelRoot = options.modelRoot ?
|
|
21309
|
+
const modelRoot = options.modelRoot ? path46.resolve(options.modelRoot) : null;
|
|
21057
21310
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
21058
21311
|
...await verifyFile(modelRoot, model),
|
|
21059
21312
|
source: model.source,
|
|
@@ -21087,7 +21340,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
21087
21340
|
};
|
|
21088
21341
|
}
|
|
21089
21342
|
function renderStyle(style, destination, cwd) {
|
|
21090
|
-
const portableDir =
|
|
21343
|
+
const portableDir = path46.relative(cwd, destination).split(path46.sep).join("/") || ".";
|
|
21091
21344
|
const replace = (value) => {
|
|
21092
21345
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
21093
21346
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -21099,25 +21352,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
21099
21352
|
return replace(style);
|
|
21100
21353
|
}
|
|
21101
21354
|
async function installRecipe(target, options = {}) {
|
|
21102
|
-
const cwd =
|
|
21355
|
+
const cwd = path46.resolve(options.cwd ?? process.cwd());
|
|
21103
21356
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
21104
21357
|
const verification = await verifyRecipe(loaded.path);
|
|
21105
21358
|
if (!verification.ok) {
|
|
21106
21359
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
21107
21360
|
}
|
|
21108
|
-
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);
|
|
21109
21362
|
const sources = [
|
|
21110
21363
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
21111
|
-
...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("/")) }))
|
|
21112
21365
|
];
|
|
21113
21366
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
21114
|
-
path:
|
|
21115
|
-
data: await
|
|
21367
|
+
path: path46.join(destination, ...file.path.split("/")),
|
|
21368
|
+
data: await readFile32(file.source)
|
|
21116
21369
|
})));
|
|
21117
21370
|
if (!options.force) {
|
|
21118
21371
|
for (const file of files) {
|
|
21119
|
-
if (!
|
|
21120
|
-
const current = await
|
|
21372
|
+
if (!existsSync35(file.path)) continue;
|
|
21373
|
+
const current = await readFile32(file.path);
|
|
21121
21374
|
if (!current.equals(file.data)) {
|
|
21122
21375
|
throw new OverwriteRefusedError(
|
|
21123
21376
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -21211,14 +21464,14 @@ async function runRecipe(args) {
|
|
|
21211
21464
|
}, null, 2));
|
|
21212
21465
|
} else {
|
|
21213
21466
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
21214
|
-
log(` ${
|
|
21467
|
+
log(` ${path47.relative(process.cwd(), result.destination) || "."}`);
|
|
21215
21468
|
log(`
|
|
21216
21469
|
Add this entry under your manifest's styles object:`);
|
|
21217
21470
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
21218
21471
|
if (result.recipe.models.length) {
|
|
21219
21472
|
log(`
|
|
21220
21473
|
Models are not downloaded automatically. Verify them with:`);
|
|
21221
|
-
log(` pixelkiln recipe verify ${
|
|
21474
|
+
log(` pixelkiln recipe verify ${path47.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
21222
21475
|
}
|
|
21223
21476
|
}
|
|
21224
21477
|
return;
|
|
@@ -21226,7 +21479,7 @@ async function runRecipe(args) {
|
|
|
21226
21479
|
}
|
|
21227
21480
|
|
|
21228
21481
|
// src/cli/commands/refine.ts
|
|
21229
|
-
import
|
|
21482
|
+
import path48 from "path";
|
|
21230
21483
|
async function runRefine(args) {
|
|
21231
21484
|
if (args.from || args.subcommand === "approve") {
|
|
21232
21485
|
if (!args.from) {
|
|
@@ -21256,12 +21509,12 @@ async function runRefine(args) {
|
|
|
21256
21509
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
21257
21510
|
);
|
|
21258
21511
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
21259
|
-
log(` wrote ${
|
|
21260
|
-
log(` quality record: ${
|
|
21512
|
+
log(` wrote ${path48.relative(process.cwd(), result3.output)}`);
|
|
21513
|
+
log(` quality record: ${path48.relative(process.cwd(), result3.record)}`);
|
|
21261
21514
|
log(`
|
|
21262
21515
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
21263
21516
|
log(
|
|
21264
|
-
` pixelkiln refine approve --from ${
|
|
21517
|
+
` pixelkiln refine approve --from ${path48.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
21265
21518
|
);
|
|
21266
21519
|
}
|
|
21267
21520
|
return;
|
|
@@ -21289,7 +21542,7 @@ async function runRefine(args) {
|
|
|
21289
21542
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
21290
21543
|
else {
|
|
21291
21544
|
log(
|
|
21292
|
-
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()}`
|
|
21293
21546
|
);
|
|
21294
21547
|
}
|
|
21295
21548
|
return;
|
|
@@ -21298,7 +21551,7 @@ async function runRefine(args) {
|
|
|
21298
21551
|
if (args.json) {
|
|
21299
21552
|
log(JSON.stringify(result2, null, 2));
|
|
21300
21553
|
} else {
|
|
21301
|
-
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)}`);
|
|
21302
21555
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
21303
21556
|
}
|
|
21304
21557
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -21344,7 +21597,7 @@ async function runRefine(args) {
|
|
|
21344
21597
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
21345
21598
|
if (item.state === "needs-approval") {
|
|
21346
21599
|
log(
|
|
21347
|
-
` pixelkiln refine approve --from ${
|
|
21600
|
+
` pixelkiln refine approve --from ${path48.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
21348
21601
|
);
|
|
21349
21602
|
}
|
|
21350
21603
|
}
|
|
@@ -21355,9 +21608,9 @@ async function runRefine(args) {
|
|
|
21355
21608
|
}
|
|
21356
21609
|
|
|
21357
21610
|
// src/cli/commands/skeleton.ts
|
|
21358
|
-
import
|
|
21359
|
-
import { existsSync as
|
|
21360
|
-
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";
|
|
21361
21614
|
|
|
21362
21615
|
// src/skeleton-preview.ts
|
|
21363
21616
|
var RIGHT = [255, 140, 40, 255];
|
|
@@ -21468,12 +21721,12 @@ var Canvas = class {
|
|
|
21468
21721
|
async function runEstimateSkeleton(args) {
|
|
21469
21722
|
const image = args.target;
|
|
21470
21723
|
if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
|
|
21471
|
-
if (args.out &&
|
|
21724
|
+
if (args.out && existsSync36(path49.resolve(args.out)) && !args.force) {
|
|
21472
21725
|
throw new UsageError(`${args.out} already exists; pass --force to replace it`);
|
|
21473
21726
|
}
|
|
21474
|
-
loadEnvFiles(
|
|
21727
|
+
loadEnvFiles(path49.dirname(path49.resolve(args.manifest)));
|
|
21475
21728
|
loadEnvFiles(process.cwd());
|
|
21476
|
-
const bytes = await
|
|
21729
|
+
const bytes = await readFile33(path49.resolve(image));
|
|
21477
21730
|
const metadata = imageMetadata(bytes);
|
|
21478
21731
|
if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
|
|
21479
21732
|
const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
@@ -21485,7 +21738,7 @@ async function runEstimateSkeleton(args) {
|
|
|
21485
21738
|
const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
|
|
21486
21739
|
const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
|
|
21487
21740
|
if (args.out) {
|
|
21488
|
-
await
|
|
21741
|
+
await writeFile19(path49.resolve(args.out), json + "\n");
|
|
21489
21742
|
log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
|
|
21490
21743
|
log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
|
|
21491
21744
|
} else {
|
|
@@ -21498,8 +21751,8 @@ async function runSkeletonPreview(args) {
|
|
|
21498
21751
|
let sourceFile;
|
|
21499
21752
|
let label;
|
|
21500
21753
|
if (target.endsWith(".json")) {
|
|
21501
|
-
keypointsFile =
|
|
21502
|
-
sourceFile = args.from ?
|
|
21754
|
+
keypointsFile = path49.resolve(target);
|
|
21755
|
+
sourceFile = args.from ? path49.resolve(args.from) : void 0;
|
|
21503
21756
|
label = target;
|
|
21504
21757
|
} else {
|
|
21505
21758
|
const loaded = await loadManifest(args.manifest);
|
|
@@ -21511,31 +21764,31 @@ async function runSkeletonPreview(args) {
|
|
|
21511
21764
|
);
|
|
21512
21765
|
}
|
|
21513
21766
|
keypointsFile = spec.revision.keypointsFile;
|
|
21514
|
-
sourceFile = args.from ?
|
|
21767
|
+
sourceFile = args.from ? path49.resolve(args.from) : spec.revision.sourceFile;
|
|
21515
21768
|
label = `${spec.styleId}/${spec.assetId}`;
|
|
21516
21769
|
}
|
|
21517
|
-
if (!
|
|
21770
|
+
if (!existsSync36(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
|
|
21518
21771
|
let json;
|
|
21519
21772
|
try {
|
|
21520
|
-
json = JSON.parse(await
|
|
21773
|
+
json = JSON.parse(await readFile33(keypointsFile, "utf8"));
|
|
21521
21774
|
} catch (error) {
|
|
21522
21775
|
throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
21523
21776
|
}
|
|
21524
21777
|
const set = parseSkeletonSet(json, keypointsFile);
|
|
21525
21778
|
let source;
|
|
21526
21779
|
if (sourceFile) {
|
|
21527
|
-
if (!
|
|
21780
|
+
if (!existsSync36(sourceFile)) {
|
|
21528
21781
|
log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
|
|
21529
21782
|
} else {
|
|
21530
|
-
const bytes = await
|
|
21783
|
+
const bytes = await readFile33(sourceFile);
|
|
21531
21784
|
if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
|
|
21532
21785
|
source = decodePng(bytes);
|
|
21533
21786
|
}
|
|
21534
21787
|
}
|
|
21535
|
-
const out =
|
|
21536
|
-
await
|
|
21788
|
+
const out = path49.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
|
|
21789
|
+
await writeFile19(out, renderSkeletonSheet(set, { source }));
|
|
21537
21790
|
log(` ${label}: ${set.frames.length} frames after the starting pose`);
|
|
21538
|
-
const shown =
|
|
21791
|
+
const shown = path49.relative(process.cwd(), out);
|
|
21539
21792
|
log(` wrote ${shown.startsWith("..") ? out : shown}`);
|
|
21540
21793
|
}
|
|
21541
21794
|
|
|
@@ -21581,15 +21834,15 @@ async function runTools(args) {
|
|
|
21581
21834
|
}
|
|
21582
21835
|
|
|
21583
21836
|
// src/cli/commands/workspace.ts
|
|
21584
|
-
import
|
|
21585
|
-
import { existsSync as
|
|
21837
|
+
import path50 from "path";
|
|
21838
|
+
import { existsSync as existsSync37 } from "fs";
|
|
21586
21839
|
async function runWorkspace(args) {
|
|
21587
|
-
const workspacePath =
|
|
21588
|
-
const dir =
|
|
21840
|
+
const workspacePath = path50.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
21841
|
+
const dir = path50.dirname(workspacePath);
|
|
21589
21842
|
if (args.subcommand === "add") {
|
|
21590
|
-
const manifestPath =
|
|
21843
|
+
const manifestPath = path50.resolve(args.target);
|
|
21591
21844
|
const loadedTarget = await loadManifest(manifestPath);
|
|
21592
|
-
const lockPath = args.explicitLock ?
|
|
21845
|
+
const lockPath = args.explicitLock ? path50.resolve(args.explicitLock) : path50.join(path50.dirname(manifestPath), "pixelkiln.lock.json");
|
|
21593
21846
|
const ws = await loadWorkspace(workspacePath);
|
|
21594
21847
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
21595
21848
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -21609,10 +21862,10 @@ async function runWorkspace(args) {
|
|
|
21609
21862
|
...args.account ? { account: args.account } : {}
|
|
21610
21863
|
};
|
|
21611
21864
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
21612
|
-
log(` registered "${id}" in ${
|
|
21865
|
+
log(` registered "${id}" in ${path50.relative(process.cwd(), workspacePath)}`);
|
|
21613
21866
|
log(` manifest: ${project.manifest}`);
|
|
21614
21867
|
log(` lock: ${project.lock}`);
|
|
21615
|
-
if (!
|
|
21868
|
+
if (!existsSync37(lockPath)) {
|
|
21616
21869
|
log(
|
|
21617
21870
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
21618
21871
|
);
|
|
@@ -21621,7 +21874,7 @@ async function runWorkspace(args) {
|
|
|
21621
21874
|
}
|
|
21622
21875
|
if (args.subcommand === "remove") {
|
|
21623
21876
|
const ws = await loadWorkspace(workspacePath);
|
|
21624
|
-
const resolvedTarget =
|
|
21877
|
+
const resolvedTarget = path50.resolve(args.target);
|
|
21625
21878
|
const match = ws.projects.find(
|
|
21626
21879
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
21627
21880
|
);
|
|
@@ -21632,11 +21885,11 @@ async function runWorkspace(args) {
|
|
|
21632
21885
|
version: 1,
|
|
21633
21886
|
projects: ws.projects.filter((p) => p !== match)
|
|
21634
21887
|
});
|
|
21635
|
-
log(` removed "${match.id}" from ${
|
|
21888
|
+
log(` removed "${match.id}" from ${path50.relative(process.cwd(), workspacePath)}`);
|
|
21636
21889
|
return;
|
|
21637
21890
|
}
|
|
21638
21891
|
if (args.subcommand === "list") {
|
|
21639
|
-
if (!
|
|
21892
|
+
if (!existsSync37(workspacePath)) {
|
|
21640
21893
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
21641
21894
|
}
|
|
21642
21895
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -21644,9 +21897,9 @@ async function runWorkspace(args) {
|
|
|
21644
21897
|
if (args.json) {
|
|
21645
21898
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
21646
21899
|
} else if (!ws.projects.length) {
|
|
21647
|
-
log(` no projects registered in ${
|
|
21900
|
+
log(` no projects registered in ${path50.relative(process.cwd(), workspacePath)}`);
|
|
21648
21901
|
} else {
|
|
21649
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
21902
|
+
log(` ${ws.projects.length} project(s) in ${path50.relative(process.cwd(), workspacePath)}:`);
|
|
21650
21903
|
for (const p of ws.projects) {
|
|
21651
21904
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
21652
21905
|
}
|
|
@@ -21656,7 +21909,7 @@ async function runWorkspace(args) {
|
|
|
21656
21909
|
return;
|
|
21657
21910
|
}
|
|
21658
21911
|
if (args.subcommand === "status") {
|
|
21659
|
-
if (!
|
|
21912
|
+
if (!existsSync37(workspacePath)) {
|
|
21660
21913
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
21661
21914
|
}
|
|
21662
21915
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -21664,7 +21917,7 @@ async function runWorkspace(args) {
|
|
|
21664
21917
|
if (args.json) {
|
|
21665
21918
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
21666
21919
|
} else {
|
|
21667
|
-
log(` workspace: ${
|
|
21920
|
+
log(` workspace: ${path50.relative(process.cwd(), workspacePath)}`);
|
|
21668
21921
|
for (const p of report.projects) {
|
|
21669
21922
|
if (p.error) {
|
|
21670
21923
|
log(`
|
|
@@ -21718,8 +21971,8 @@ async function runHelp() {
|
|
|
21718
21971
|
async function runVersion() {
|
|
21719
21972
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
21720
21973
|
const url = new URL(up, import.meta.url);
|
|
21721
|
-
if (!
|
|
21722
|
-
const pkg = JSON.parse(await
|
|
21974
|
+
if (!existsSync38(url)) continue;
|
|
21975
|
+
const pkg = JSON.parse(await readFile34(url, "utf8"));
|
|
21723
21976
|
if (pkg.name === "pixelkiln") {
|
|
21724
21977
|
log(`${pkg.name} ${pkg.version}`);
|
|
21725
21978
|
return;
|