pixelkiln 0.83.0 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROVIDERS.md +3 -3
- package/dist/cli.js +957 -611
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.css +2 -0
- package/dist/client/gallery.js +3366 -3087
- package/dist/index.cjs +876 -530
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +792 -71
- package/dist/index.d.ts +792 -71
- package/dist/index.js +783 -437
- package/dist/index.js.map +1 -1
- package/docs/MANIFEST.md +10 -6
- package/docs/REVISIONS.md +66 -15
- package/docs/SCENARIO.md +174 -7
- package/package.json +1 -1
- package/schema/manifest.schema.json +22 -0
- package/skills/pixelkiln/references/pixellab-roadmap.md +10 -0
- package/skills/pixelkiln/references/pixellab.md +6 -3
- package/skills/pixelkiln/references/revisions.md +13 -7
- package/skills/pixelkiln/references/scenario.md +8 -4
package/dist/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(path52) {
|
|
669
669
|
return new Promise((resolve, reject) => {
|
|
670
670
|
const hash = createHash("sha256");
|
|
671
|
-
const stream = createReadStream(
|
|
671
|
+
const stream = createReadStream(path52);
|
|
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. */
|
|
@@ -4754,7 +4818,7 @@ async function confirm(question, auto) {
|
|
|
4754
4818
|
}
|
|
4755
4819
|
|
|
4756
4820
|
// src/cli/commands/account.ts
|
|
4757
|
-
import
|
|
4821
|
+
import path23 from "path";
|
|
4758
4822
|
import { existsSync as existsSync16 } from "fs";
|
|
4759
4823
|
|
|
4760
4824
|
// src/env.ts
|
|
@@ -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(path52, 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}${path52}`, {
|
|
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"} ${path52} \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"} ${path52} 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 path52;
|
|
6613
6678
|
let body;
|
|
6614
6679
|
if (args.mode === "v3") {
|
|
6615
|
-
|
|
6680
|
+
path52 = "/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
|
+
path52 = "/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
|
+
path52 = "/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
|
+
path52 = 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(path52, { method: "POST", body: JSON.stringify(body) });
|
|
6750
|
+
return validateResponse(CharacterSubmitSchema, raw, path52.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(path52, 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}${path52}`, {
|
|
9607
9731
|
...init,
|
|
9608
9732
|
headers: {
|
|
9609
9733
|
"Content-Type": "application/json",
|
|
@@ -9912,8 +10036,13 @@ function retroError(value) {
|
|
|
9912
10036
|
}
|
|
9913
10037
|
|
|
9914
10038
|
// src/providers/scenario.ts
|
|
10039
|
+
import { createHash as createHash3 } from "crypto";
|
|
10040
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
10041
|
+
import os2 from "os";
|
|
10042
|
+
import path13 from "path";
|
|
9915
10043
|
var DEFAULT_BASE_URL3 = "https://api.cloud.scenario.com/v1";
|
|
9916
10044
|
var SOURCE_PROTOCOL2 = "scenario:";
|
|
10045
|
+
var SUBMIT_RETRIES = 8;
|
|
9917
10046
|
var PROTECTED_PARAMETERS = /* @__PURE__ */ new Set([
|
|
9918
10047
|
"dryRun",
|
|
9919
10048
|
"height",
|
|
@@ -9960,7 +10089,11 @@ var ScenarioClient = class {
|
|
|
9960
10089
|
async submit(modelId, body, projectId) {
|
|
9961
10090
|
const response = await this.call(
|
|
9962
10091
|
`/generate/custom/${encodeURIComponent(modelId)}`,
|
|
9963
|
-
|
|
10092
|
+
// A 429 here means this account's concurrent-job limit refused the
|
|
10093
|
+
// request, so nothing was accepted or billed; wait it out instead of
|
|
10094
|
+
// failing the asset. The default four retries give up after about half
|
|
10095
|
+
// a minute, before a running job has finished.
|
|
10096
|
+
{ method: "POST", body: JSON.stringify(body), retries: SUBMIT_RETRIES },
|
|
9964
10097
|
{ projectId }
|
|
9965
10098
|
);
|
|
9966
10099
|
const job = response?.job;
|
|
@@ -9968,6 +10101,29 @@ var ScenarioClient = class {
|
|
|
9968
10101
|
if (!jobId) throw new Error("Scenario did not return an async job id");
|
|
9969
10102
|
return jobId;
|
|
9970
10103
|
}
|
|
10104
|
+
/**
|
|
10105
|
+
* Uploads one image (POST /assets) and returns its asset id, which a model's
|
|
10106
|
+
* reference input takes. Free, and idempotent per image: the id is kept by
|
|
10107
|
+
* content hash so a rerun does not upload the same bytes again.
|
|
10108
|
+
*/
|
|
10109
|
+
async uploadImage(image, projectId) {
|
|
10110
|
+
const bytes = Buffer.from(image.base64, "base64");
|
|
10111
|
+
const digest2 = sha256(bytes);
|
|
10112
|
+
const cached = readUploadCache()[uploadCacheKey(this.baseUrl, projectId, digest2)];
|
|
10113
|
+
if (cached) return cached;
|
|
10114
|
+
const response = await this.call(
|
|
10115
|
+
"/assets",
|
|
10116
|
+
{
|
|
10117
|
+
method: "POST",
|
|
10118
|
+
body: JSON.stringify({ image: image.base64, name: `pixelkiln-${digest2.slice(0, 12)}.${image.format === "png" ? "png" : "jpg"}` })
|
|
10119
|
+
},
|
|
10120
|
+
{ projectId }
|
|
10121
|
+
);
|
|
10122
|
+
const id = response?.asset?.id;
|
|
10123
|
+
if (typeof id !== "string" || !id) throw new Error("Scenario did not return an uploaded asset id");
|
|
10124
|
+
writeUploadCache(uploadCacheKey(this.baseUrl, projectId, digest2), id);
|
|
10125
|
+
return id;
|
|
10126
|
+
}
|
|
9971
10127
|
async job(id, projectId) {
|
|
9972
10128
|
const response = await this.call(`/jobs/${encodeURIComponent(id)}`, {}, { projectId });
|
|
9973
10129
|
const raw = response?.job;
|
|
@@ -10094,6 +10250,14 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
10094
10250
|
supports(generator) {
|
|
10095
10251
|
return generator === "map";
|
|
10096
10252
|
}
|
|
10253
|
+
/**
|
|
10254
|
+
* An edit is a generation with the parent as a reference image, which the
|
|
10255
|
+
* editing models (GPT Image, Gemini, FLUX) take. Masked inpainting and the
|
|
10256
|
+
* PixelLab-specific modes have no common Scenario input.
|
|
10257
|
+
*/
|
|
10258
|
+
supportsRevision(mode2) {
|
|
10259
|
+
return mode2 === "image-to-image";
|
|
10260
|
+
}
|
|
10097
10261
|
estimate(spec) {
|
|
10098
10262
|
const options = scenarioOptions(spec);
|
|
10099
10263
|
return {
|
|
@@ -10114,11 +10278,31 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
10114
10278
|
if (!Number.isInteger(count) || count < 1 || count > 4) {
|
|
10115
10279
|
throw new Error("Scenario numOutputs must be a whole number from 1 to 4");
|
|
10116
10280
|
}
|
|
10117
|
-
if (spec.width
|
|
10118
|
-
throw new Error("Scenario width and height must be
|
|
10281
|
+
if (!Number.isInteger(spec.width) || spec.width < 16 || spec.width > 4096 || !Number.isInteger(spec.height) || spec.height < 16 || spec.height > 4096) {
|
|
10282
|
+
throw new Error("Scenario width and height must be whole numbers from 16 to 4096");
|
|
10119
10283
|
}
|
|
10120
|
-
|
|
10121
|
-
|
|
10284
|
+
const revision = spec.revision;
|
|
10285
|
+
if (revision) {
|
|
10286
|
+
if (revision.sourceMembers?.length) {
|
|
10287
|
+
throw new Error(
|
|
10288
|
+
`Scenario edits one image; ${revision.sourceAssetId} is a ${revision.sourceMembers.length}-member set`
|
|
10289
|
+
);
|
|
10290
|
+
}
|
|
10291
|
+
if (revision.strength != null) {
|
|
10292
|
+
throw new Error("Scenario image-to-image takes no strength; use the model's own parameters");
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10295
|
+
const parameter = referenceParameter(options);
|
|
10296
|
+
if ((styleImages.length || revision) && Object.hasOwn(options.parameters ?? {}, parameter)) {
|
|
10297
|
+
throw new Error(
|
|
10298
|
+
`Scenario parameters.${parameter} is the reference-image input and is filled from styleImages and revisions`
|
|
10299
|
+
);
|
|
10300
|
+
}
|
|
10301
|
+
if (styleImages.length + (revision ? 1 : 0) > 1 && options.referenceArray === false) {
|
|
10302
|
+
throw new Error(`Scenario ${parameter} takes one image; use one style image or a revision, not both`);
|
|
10303
|
+
}
|
|
10304
|
+
if (options.referenceParameter != null && (typeof options.referenceParameter !== "string" || !options.referenceParameter.trim())) {
|
|
10305
|
+
throw new Error("Scenario referenceParameter must be a non-empty string");
|
|
10122
10306
|
}
|
|
10123
10307
|
if (options.projectId != null && (typeof options.projectId !== "string" || !options.projectId.trim())) {
|
|
10124
10308
|
throw new Error("Scenario projectId must be a non-empty string");
|
|
@@ -10131,7 +10315,12 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
10131
10315
|
async submit(spec, styleImages) {
|
|
10132
10316
|
this.validate(spec, styleImages);
|
|
10133
10317
|
const options = scenarioOptions(spec);
|
|
10134
|
-
const
|
|
10318
|
+
const references = [];
|
|
10319
|
+
if (spec.revision) references.push(readRevisionSource(spec));
|
|
10320
|
+
for (const image of styleImages) references.push({ base64: image.base64, format: image.format });
|
|
10321
|
+
const referenceAssetIds = [];
|
|
10322
|
+
for (const image of references) referenceAssetIds.push(await this.client.uploadImage(image, options.projectId));
|
|
10323
|
+
const body = requestBody(spec, options, referenceAssetIds);
|
|
10135
10324
|
const quote = await this.client.quote(options.modelId, body, options.projectId);
|
|
10136
10325
|
if (quote.total > options.maxComputeUnits + Number.EPSILON) {
|
|
10137
10326
|
throw new Error(
|
|
@@ -10148,7 +10337,8 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
10148
10337
|
quotedComputeUnits: quote.total,
|
|
10149
10338
|
creativeUnitsCost: quote.creativeUnitsCost,
|
|
10150
10339
|
costDetails: quote.costDetails,
|
|
10151
|
-
ipDetectionCost: quote.ipDetectionCost
|
|
10340
|
+
ipDetectionCost: quote.ipDetectionCost,
|
|
10341
|
+
...referenceAssetIds.length ? { referenceAssetIds } : {}
|
|
10152
10342
|
}
|
|
10153
10343
|
};
|
|
10154
10344
|
}
|
|
@@ -10170,7 +10360,13 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
10170
10360
|
}
|
|
10171
10361
|
const assetIds = scenarioAssetIds(job);
|
|
10172
10362
|
if (!assetIds.length) return { status: "failed", error: "Scenario job returned no asset IDs" };
|
|
10173
|
-
|
|
10363
|
+
let assets;
|
|
10364
|
+
try {
|
|
10365
|
+
assets = await this.assets(assetIds, reference.projectId);
|
|
10366
|
+
} catch (error) {
|
|
10367
|
+
if (error instanceof UnsupportedScenarioOutput) return { status: "failed", error: error.message };
|
|
10368
|
+
throw error;
|
|
10369
|
+
}
|
|
10174
10370
|
if (assets.length > 1) {
|
|
10175
10371
|
return {
|
|
10176
10372
|
status: "review",
|
|
@@ -10225,6 +10421,32 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
10225
10421
|
return assets.sort((left, right) => left.outputIndex - right.outputIndex || (jobOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (jobOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER) || left.id.localeCompare(right.id));
|
|
10226
10422
|
}
|
|
10227
10423
|
};
|
|
10424
|
+
function uploadCachePath() {
|
|
10425
|
+
return process.env.SCENARIO_UPLOAD_CACHE ?? path13.join(os2.tmpdir(), "pixelkiln-scenario", "uploads.json");
|
|
10426
|
+
}
|
|
10427
|
+
function uploadCacheKey(baseUrl, projectId, digest2) {
|
|
10428
|
+
return createHash3("sha256").update(`${baseUrl}
|
|
10429
|
+
${projectId ?? ""}
|
|
10430
|
+
${digest2}`).digest("hex");
|
|
10431
|
+
}
|
|
10432
|
+
function readUploadCache() {
|
|
10433
|
+
try {
|
|
10434
|
+
const value = JSON.parse(readFileSync3(uploadCachePath(), "utf8"));
|
|
10435
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
10436
|
+
} catch {
|
|
10437
|
+
return {};
|
|
10438
|
+
}
|
|
10439
|
+
}
|
|
10440
|
+
function writeUploadCache(key, assetId) {
|
|
10441
|
+
try {
|
|
10442
|
+
const file = uploadCachePath();
|
|
10443
|
+
mkdirSync2(path13.dirname(file), { recursive: true });
|
|
10444
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
10445
|
+
writeFileSync2(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
|
|
10446
|
+
renameSync(tmp, file);
|
|
10447
|
+
} catch {
|
|
10448
|
+
}
|
|
10449
|
+
}
|
|
10228
10450
|
function scenarioOptions(spec) {
|
|
10229
10451
|
return spec.providerOptions;
|
|
10230
10452
|
}
|
|
@@ -10240,9 +10462,25 @@ function scenarioBaseUrl(value) {
|
|
|
10240
10462
|
}
|
|
10241
10463
|
return url.href.replace(/\/$/, "");
|
|
10242
10464
|
}
|
|
10243
|
-
function
|
|
10465
|
+
function referenceParameter(options) {
|
|
10466
|
+
return options.referenceParameter?.trim() || "referenceImages";
|
|
10467
|
+
}
|
|
10468
|
+
function readRevisionSource(spec) {
|
|
10469
|
+
const revision = spec.revision;
|
|
10470
|
+
if (!revision.sourceFile || !revision.sourceSha256 || !revision.sourceFormat) {
|
|
10471
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
|
|
10472
|
+
}
|
|
10473
|
+
const bytes = readFileSync3(revision.sourceFile);
|
|
10474
|
+
if (sha256(bytes) !== revision.sourceSha256) {
|
|
10475
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
|
|
10476
|
+
}
|
|
10477
|
+
return { base64: bytes.toString("base64"), format: revision.sourceFormat };
|
|
10478
|
+
}
|
|
10479
|
+
function requestBody(spec, options, referenceAssetIds = []) {
|
|
10480
|
+
const parameter = referenceParameter(options);
|
|
10244
10481
|
return {
|
|
10245
10482
|
...options.parameters ?? {},
|
|
10483
|
+
...referenceAssetIds.length ? { [parameter]: options.referenceArray === false ? referenceAssetIds[0] : referenceAssetIds } : {},
|
|
10246
10484
|
prompt: spec.prompt,
|
|
10247
10485
|
width: spec.width,
|
|
10248
10486
|
height: spec.height,
|
|
@@ -10332,9 +10570,11 @@ function scenarioAssetIds(job) {
|
|
|
10332
10570
|
function assetDownloadUrl(asset) {
|
|
10333
10571
|
return asset.originalFileUrl || asset.url;
|
|
10334
10572
|
}
|
|
10573
|
+
var UnsupportedScenarioOutput = class extends Error {
|
|
10574
|
+
};
|
|
10335
10575
|
function assertPngAsset(asset) {
|
|
10336
10576
|
if (asset.originalMimeType && asset.originalMimeType !== MediaType.PNG) {
|
|
10337
|
-
throw new
|
|
10577
|
+
throw new UnsupportedScenarioOutput(
|
|
10338
10578
|
`Scenario asset ${asset.id} is ${asset.originalMimeType}; PixelKiln's Scenario MVP requires PNG output`
|
|
10339
10579
|
);
|
|
10340
10580
|
}
|
|
@@ -10462,7 +10702,7 @@ registerProvider({
|
|
|
10462
10702
|
// src/manifest.ts
|
|
10463
10703
|
import { readFile as readFile6 } from "fs/promises";
|
|
10464
10704
|
import { existsSync as existsSync8 } from "fs";
|
|
10465
|
-
import
|
|
10705
|
+
import path14 from "path";
|
|
10466
10706
|
|
|
10467
10707
|
// src/loop-directions.ts
|
|
10468
10708
|
var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
|
|
@@ -10551,7 +10791,7 @@ function isObject2(value) {
|
|
|
10551
10791
|
|
|
10552
10792
|
// src/manifest.ts
|
|
10553
10793
|
async function loadManifest(manifestPath) {
|
|
10554
|
-
const abs =
|
|
10794
|
+
const abs = path14.resolve(manifestPath);
|
|
10555
10795
|
if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
10556
10796
|
const expansion = expandLoopDirections(JSON.parse(await readFile6(abs, "utf8")));
|
|
10557
10797
|
if (expansion.issues.length) {
|
|
@@ -10644,7 +10884,7 @@ ${issues}`);
|
|
|
10644
10884
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
10645
10885
|
}
|
|
10646
10886
|
const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
|
|
10647
|
-
return { manifest: parsed.data, root:
|
|
10887
|
+
return { manifest: parsed.data, root: path14.dirname(abs), path: abs, ...loopFamilies };
|
|
10648
10888
|
}
|
|
10649
10889
|
function parentAssetId(asset) {
|
|
10650
10890
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
|
|
@@ -10745,7 +10985,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10745
10985
|
}
|
|
10746
10986
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
10747
10987
|
async function loadStyleImage(rel, what = "Style image") {
|
|
10748
|
-
const abs =
|
|
10988
|
+
const abs = path14.resolve(root, rel);
|
|
10749
10989
|
let hit = styleImageCache.get(abs);
|
|
10750
10990
|
if (!hit) {
|
|
10751
10991
|
if (!existsSync8(abs)) throw new Error(`${what} not found: ${abs}`);
|
|
@@ -10775,7 +11015,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10775
11015
|
styleImageDimensions.push(loadedImage);
|
|
10776
11016
|
}
|
|
10777
11017
|
const resolvedImages = style.styleImages.map((image) => {
|
|
10778
|
-
const hit = styleImageCache.get(
|
|
11018
|
+
const hit = styleImageCache.get(path14.resolve(root, image.path));
|
|
10779
11019
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
10780
11020
|
});
|
|
10781
11021
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
@@ -10876,9 +11116,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10876
11116
|
} else {
|
|
10877
11117
|
prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
10878
11118
|
}
|
|
10879
|
-
const relFile = asset.file ??
|
|
10880
|
-
const outFile =
|
|
10881
|
-
const qualityOutFile = style.quality ?
|
|
11119
|
+
const relFile = asset.file ?? path14.join(asset.category ?? "", `${assetId}.png`);
|
|
11120
|
+
const outFile = path14.resolve(root, style.outDir, relFile);
|
|
11121
|
+
const qualityOutFile = style.quality ? path14.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
10882
11122
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
10883
11123
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
10884
11124
|
const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
|
|
@@ -10973,7 +11213,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
10973
11213
|
minGridConfidence: style.quality.minGridConfidence,
|
|
10974
11214
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
10975
11215
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
10976
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
11216
|
+
...style.quality.fixerPython ? { fixerPython: path14.resolve(root, style.quality.fixerPython) } : {},
|
|
10977
11217
|
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
10978
11218
|
}
|
|
10979
11219
|
} : {},
|
|
@@ -11109,7 +11349,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11109
11349
|
animation: sourceSpec.character.animation,
|
|
11110
11350
|
outfit: {
|
|
11111
11351
|
reference: {
|
|
11112
|
-
path:
|
|
11352
|
+
path: path14.resolve(root, asset.outfit.reference),
|
|
11113
11353
|
sha256: referenceImage.hash,
|
|
11114
11354
|
width: referenceImage.width,
|
|
11115
11355
|
height: referenceImage.height,
|
|
@@ -11131,23 +11371,23 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11131
11371
|
);
|
|
11132
11372
|
}
|
|
11133
11373
|
const sourceSpec = await finalize(asset.revision.from);
|
|
11134
|
-
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
11374
|
+
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path14.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
11135
11375
|
const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
|
|
11136
11376
|
const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
|
|
11137
11377
|
const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
|
|
11138
11378
|
const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
|
|
11139
|
-
const maskFile = asset.revision.mask ?
|
|
11379
|
+
const maskFile = asset.revision.mask ? path14.resolve(root, asset.revision.mask) : void 0;
|
|
11140
11380
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
11141
11381
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
11142
11382
|
throw new Error(
|
|
11143
11383
|
`Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
|
|
11144
11384
|
);
|
|
11145
11385
|
}
|
|
11146
|
-
const paletteImageFile = asset.revision.paletteImage ?
|
|
11386
|
+
const paletteImageFile = asset.revision.paletteImage ? path14.resolve(root, asset.revision.paletteImage) : void 0;
|
|
11147
11387
|
const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
|
|
11148
|
-
const lastFrameFile = asset.revision.lastFrame ?
|
|
11388
|
+
const lastFrameFile = asset.revision.lastFrame ? path14.resolve(root, asset.revision.lastFrame) : void 0;
|
|
11149
11389
|
const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
|
|
11150
|
-
const keypointsFile = asset.revision.keypointsFile ?
|
|
11390
|
+
const keypointsFile = asset.revision.keypointsFile ? path14.resolve(root, asset.revision.keypointsFile) : void 0;
|
|
11151
11391
|
const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
|
|
11152
11392
|
resolved.revision = {
|
|
11153
11393
|
mode: asset.revision.mode,
|
|
@@ -11201,6 +11441,10 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11201
11441
|
} : {},
|
|
11202
11442
|
...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
|
|
11203
11443
|
...asset.revision.description ? { description: asset.revision.description } : {},
|
|
11444
|
+
...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
|
|
11445
|
+
...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
|
|
11446
|
+
...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
|
|
11447
|
+
...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
|
|
11204
11448
|
...asset.revision.engine ? { engine: asset.revision.engine } : {}
|
|
11205
11449
|
};
|
|
11206
11450
|
}
|
|
@@ -11310,7 +11554,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11310
11554
|
return specs;
|
|
11311
11555
|
}
|
|
11312
11556
|
function pngPath(file) {
|
|
11313
|
-
const extension =
|
|
11557
|
+
const extension = path14.extname(file);
|
|
11314
11558
|
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
11315
11559
|
}
|
|
11316
11560
|
async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
|
|
@@ -11320,12 +11564,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
|
|
|
11320
11564
|
if (!provider.supportsMapScene) {
|
|
11321
11565
|
throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
|
|
11322
11566
|
}
|
|
11323
|
-
const file =
|
|
11567
|
+
const file = path14.resolve(root, scene.image);
|
|
11324
11568
|
const image = await optionalRevisionImage(file, "scene image", "png");
|
|
11325
11569
|
const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
|
|
11326
11570
|
const placement = scene.placement;
|
|
11327
11571
|
if ("mask" in placement) {
|
|
11328
|
-
const maskFile =
|
|
11572
|
+
const maskFile = path14.resolve(root, placement.mask);
|
|
11329
11573
|
const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
|
|
11330
11574
|
if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
|
|
11331
11575
|
throw new Error(
|
|
@@ -11397,7 +11641,7 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
11397
11641
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
11398
11642
|
const out = [];
|
|
11399
11643
|
for (const img of style.styleImages) {
|
|
11400
|
-
const file =
|
|
11644
|
+
const file = path14.resolve(loaded.root, img.path);
|
|
11401
11645
|
const buf = await readFile6(file);
|
|
11402
11646
|
const metadata = imageMetadata(buf);
|
|
11403
11647
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
@@ -11425,7 +11669,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
11425
11669
|
};
|
|
11426
11670
|
if (asset.concept) {
|
|
11427
11671
|
const image = await files.load(asset.concept, "Concept image");
|
|
11428
|
-
shape.concept = { path:
|
|
11672
|
+
shape.concept = { path: path14.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11429
11673
|
}
|
|
11430
11674
|
if (asset.reference) {
|
|
11431
11675
|
const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
|
|
@@ -11433,7 +11677,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
11433
11677
|
for (const [direction, rel] of Object.entries(byDirection)) {
|
|
11434
11678
|
const image = await files.load(rel, `Reference sprite (${direction})`);
|
|
11435
11679
|
shape.reference[direction] = {
|
|
11436
|
-
path:
|
|
11680
|
+
path: path14.resolve(files.root, rel),
|
|
11437
11681
|
sha256: image.hash,
|
|
11438
11682
|
width: image.width,
|
|
11439
11683
|
height: image.height,
|
|
@@ -11454,7 +11698,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
11454
11698
|
const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
|
|
11455
11699
|
const pose = async (rel, what) => {
|
|
11456
11700
|
const image = await files.load(rel, what);
|
|
11457
|
-
return { path:
|
|
11701
|
+
return { path: path14.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11458
11702
|
};
|
|
11459
11703
|
shape.animation = {
|
|
11460
11704
|
mode: animationMode,
|
|
@@ -11485,7 +11729,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
11485
11729
|
const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
|
|
11486
11730
|
if (south) {
|
|
11487
11731
|
const image = await files.load(south, "Reference sprite");
|
|
11488
|
-
shape.reference = { path:
|
|
11732
|
+
shape.reference = { path: path14.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11489
11733
|
}
|
|
11490
11734
|
}
|
|
11491
11735
|
if (asset.state) {
|
|
@@ -11513,7 +11757,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
11513
11757
|
}
|
|
11514
11758
|
const pose = async (rel, what) => {
|
|
11515
11759
|
const image = await files.load(rel, what);
|
|
11516
|
-
return { path:
|
|
11760
|
+
return { path: path14.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11517
11761
|
};
|
|
11518
11762
|
shape.animation = {
|
|
11519
11763
|
mode: asset.animation.mode ?? "v3",
|
|
@@ -11530,9 +11774,9 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
11530
11774
|
}
|
|
11531
11775
|
|
|
11532
11776
|
// src/pipeline/fetch.ts
|
|
11533
|
-
import { existsSync as existsSync9, readFileSync as
|
|
11777
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
|
|
11534
11778
|
import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
11535
|
-
import
|
|
11779
|
+
import path15 from "path";
|
|
11536
11780
|
|
|
11537
11781
|
// src/source-url.ts
|
|
11538
11782
|
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -11578,7 +11822,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11578
11822
|
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
11579
11823
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
11580
11824
|
normalizeLockOutputPaths(lock, specs);
|
|
11581
|
-
const cacheDir = opts.cacheDir === false ? null :
|
|
11825
|
+
const cacheDir = opts.cacheDir === false ? null : path15.resolve(opts.cacheDir ?? path15.join(path15.dirname(lockPath), ".pixelkiln", "cache"));
|
|
11582
11826
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
11583
11827
|
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
11584
11828
|
if (specByKey.get(key)?.mirror) return false;
|
|
@@ -11589,7 +11833,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11589
11833
|
if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
|
|
11590
11834
|
return Boolean(opts.force) || e.outputs.every((output) => {
|
|
11591
11835
|
const file = resolveOutputPath(output.path, spec.root);
|
|
11592
|
-
return existsSync9(file) && sha256(
|
|
11836
|
+
return existsSync9(file) && sha256(readFileSync4(file)) === output.sha256;
|
|
11593
11837
|
});
|
|
11594
11838
|
}
|
|
11595
11839
|
if (!opts.repair) return false;
|
|
@@ -11597,7 +11841,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11597
11841
|
return e.outputs.some((output) => {
|
|
11598
11842
|
const file = resolveOutputPath(output.path, spec.root);
|
|
11599
11843
|
if (!existsSync9(file)) return true;
|
|
11600
|
-
const current = sha256(
|
|
11844
|
+
const current = sha256(readFileSync4(file));
|
|
11601
11845
|
if (current === output.sha256) return false;
|
|
11602
11846
|
return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
|
|
11603
11847
|
});
|
|
@@ -11680,7 +11924,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11680
11924
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
11681
11925
|
fresh = await provider.download(source.url);
|
|
11682
11926
|
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
11683
|
-
log2(` current ${
|
|
11927
|
+
log2(` current ${path15.relative(process.cwd(), target)}`);
|
|
11684
11928
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
11685
11929
|
continue;
|
|
11686
11930
|
}
|
|
@@ -11704,9 +11948,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11704
11948
|
}
|
|
11705
11949
|
if (reapply && !fresh) {
|
|
11706
11950
|
if (!recorded.raw) raw = await readFile7(target);
|
|
11707
|
-
log2(` reapply ${
|
|
11951
|
+
log2(` reapply ${path15.relative(process.cwd(), target)}`);
|
|
11708
11952
|
} else {
|
|
11709
|
-
log2(` changed ${
|
|
11953
|
+
log2(` changed ${path15.relative(process.cwd(), target)} (upstream)`);
|
|
11710
11954
|
}
|
|
11711
11955
|
} else {
|
|
11712
11956
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
@@ -11734,7 +11978,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11734
11978
|
}
|
|
11735
11979
|
}
|
|
11736
11980
|
log2(
|
|
11737
|
-
` replace ${
|
|
11981
|
+
` replace ${path15.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
11738
11982
|
);
|
|
11739
11983
|
}
|
|
11740
11984
|
}
|
|
@@ -11743,7 +11987,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11743
11987
|
} else if (!raw && recorded && cacheDir) {
|
|
11744
11988
|
if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
|
|
11745
11989
|
if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
|
|
11746
|
-
if (final || raw) log2(` cached ${
|
|
11990
|
+
if (final || raw) log2(` cached ${path15.relative(process.cwd(), target)}`);
|
|
11747
11991
|
}
|
|
11748
11992
|
if (!final && !raw) {
|
|
11749
11993
|
if (!source.url) {
|
|
@@ -11770,11 +12014,11 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11770
12014
|
buf = processed.bytes;
|
|
11771
12015
|
if (processed.changed) {
|
|
11772
12016
|
rawHash = sha256(raw);
|
|
11773
|
-
log2(` snapped ${
|
|
12017
|
+
log2(` snapped ${path15.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
|
|
11774
12018
|
}
|
|
11775
12019
|
}
|
|
11776
12020
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
11777
|
-
await mkdir4(
|
|
12021
|
+
await mkdir4(path15.dirname(target), { recursive: true });
|
|
11778
12022
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
11779
12023
|
await writeFile4(tmp, buf);
|
|
11780
12024
|
outputs.push({
|
|
@@ -11788,7 +12032,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11788
12032
|
await saveLock(lockPath, lock);
|
|
11789
12033
|
await rename4(tmp, target);
|
|
11790
12034
|
wrote = true;
|
|
11791
|
-
log2(` wrote ${
|
|
12035
|
+
log2(` wrote ${path15.relative(process.cwd(), target)}`);
|
|
11792
12036
|
}
|
|
11793
12037
|
if (opts.refresh && !wrote) {
|
|
11794
12038
|
if (rotated) {
|
|
@@ -11832,7 +12076,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11832
12076
|
return result;
|
|
11833
12077
|
}
|
|
11834
12078
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
11835
|
-
const file =
|
|
12079
|
+
const file = path15.join(cacheDir, cacheFileName(hash, mediaType));
|
|
11836
12080
|
if (!existsSync9(file)) return null;
|
|
11837
12081
|
try {
|
|
11838
12082
|
const buf = await readFile7(file);
|
|
@@ -11846,7 +12090,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
11846
12090
|
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
11847
12091
|
const hash = knownHash ?? sha256(buf);
|
|
11848
12092
|
validateMedia(buf, mediaType);
|
|
11849
|
-
const file =
|
|
12093
|
+
const file = path15.join(cacheDir, cacheFileName(hash, mediaType));
|
|
11850
12094
|
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
11851
12095
|
await mkdir4(cacheDir, { recursive: true });
|
|
11852
12096
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
@@ -11889,7 +12133,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
11889
12133
|
// src/pipeline/adopt.ts
|
|
11890
12134
|
import { existsSync as existsSync11 } from "fs";
|
|
11891
12135
|
import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
|
|
11892
|
-
import
|
|
12136
|
+
import path16 from "path";
|
|
11893
12137
|
|
|
11894
12138
|
// src/cache.ts
|
|
11895
12139
|
import { mkdir as mkdir5, readFile as readFile8, rename as rename5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
|
|
@@ -11908,10 +12152,10 @@ function isSha256Hash(value) {
|
|
|
11908
12152
|
function parseCache(value) {
|
|
11909
12153
|
return HashCacheSchema.parse(value);
|
|
11910
12154
|
}
|
|
11911
|
-
async function loadCache(
|
|
11912
|
-
if (!existsSync10(
|
|
12155
|
+
async function loadCache(path52) {
|
|
12156
|
+
if (!existsSync10(path52)) return { version: 1, hashes: {} };
|
|
11913
12157
|
try {
|
|
11914
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
12158
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path52, "utf8")));
|
|
11915
12159
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
11916
12160
|
return {
|
|
11917
12161
|
version: 1,
|
|
@@ -11923,18 +12167,18 @@ async function loadCache(path50) {
|
|
|
11923
12167
|
return { version: 1, hashes: {} };
|
|
11924
12168
|
}
|
|
11925
12169
|
}
|
|
11926
|
-
async function saveCache(
|
|
12170
|
+
async function saveCache(path52, cache) {
|
|
11927
12171
|
const sorted = {};
|
|
11928
12172
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
11929
12173
|
const hash = cache.hashes[key];
|
|
11930
12174
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
11931
12175
|
sorted[key] = hash;
|
|
11932
12176
|
}
|
|
11933
|
-
await mkdir5(pathModule.dirname(pathModule.resolve(
|
|
11934
|
-
const tmp = `${
|
|
12177
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path52)), { recursive: true });
|
|
12178
|
+
const tmp = `${path52}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
11935
12179
|
try {
|
|
11936
12180
|
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
11937
|
-
await rename5(tmp,
|
|
12181
|
+
await rename5(tmp, path52);
|
|
11938
12182
|
} finally {
|
|
11939
12183
|
await rm5(tmp, { force: true });
|
|
11940
12184
|
}
|
|
@@ -12127,7 +12371,7 @@ function formatUnmatchedRemote(objects, limit = 15) {
|
|
|
12127
12371
|
// src/pipeline/adopt-characters.ts
|
|
12128
12372
|
import { existsSync as existsSync12 } from "fs";
|
|
12129
12373
|
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
|
|
12130
|
-
import
|
|
12374
|
+
import path17 from "path";
|
|
12131
12375
|
async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
12132
12376
|
const log2 = opts.onProgress ?? (() => {
|
|
12133
12377
|
});
|
|
@@ -12179,11 +12423,11 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12179
12423
|
const hash = sha256(bytes);
|
|
12180
12424
|
const file = expectedOutputPath(spec, source.role, index, sources.length);
|
|
12181
12425
|
if (existsSync12(file)) {
|
|
12182
|
-
if (sha256(await readFile10(file)) !== hash) result.differing.push(
|
|
12426
|
+
if (sha256(await readFile10(file)) !== hash) result.differing.push(path17.relative(process.cwd(), file));
|
|
12183
12427
|
} else {
|
|
12184
|
-
await mkdir6(
|
|
12428
|
+
await mkdir6(path17.dirname(file), { recursive: true });
|
|
12185
12429
|
await writeFile7(file, bytes);
|
|
12186
|
-
result.written.push(
|
|
12430
|
+
result.written.push(path17.relative(process.cwd(), file));
|
|
12187
12431
|
}
|
|
12188
12432
|
outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
|
|
12189
12433
|
}
|
|
@@ -12222,7 +12466,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12222
12466
|
} else {
|
|
12223
12467
|
const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
|
|
12224
12468
|
if (!existsSync12(south)) {
|
|
12225
|
-
result.unmatched.push(`${key}: no remoteId and no ${
|
|
12469
|
+
result.unmatched.push(`${key}: no remoteId and no ${path17.relative(process.cwd(), south)} to match by`);
|
|
12226
12470
|
return false;
|
|
12227
12471
|
}
|
|
12228
12472
|
const localHash = sha256(await readFile10(south));
|
|
@@ -12234,7 +12478,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12234
12478
|
}
|
|
12235
12479
|
}
|
|
12236
12480
|
if (!match) {
|
|
12237
|
-
result.unmatched.push(`${key}: no character's south rotation has the bytes of ${
|
|
12481
|
+
result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path17.relative(process.cwd(), south)}`);
|
|
12238
12482
|
return false;
|
|
12239
12483
|
}
|
|
12240
12484
|
}
|
|
@@ -12304,7 +12548,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12304
12548
|
if (!existsSync12(first)) {
|
|
12305
12549
|
const byName = candidates.find((a) => a.name === characterAnimationName(spec));
|
|
12306
12550
|
if (!byName) {
|
|
12307
|
-
result.unmatched.push(`${key}: no remoteId and no ${
|
|
12551
|
+
result.unmatched.push(`${key}: no remoteId and no ${path17.relative(process.cwd(), first)} to match by`);
|
|
12308
12552
|
return false;
|
|
12309
12553
|
}
|
|
12310
12554
|
match = byName;
|
|
@@ -12317,7 +12561,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12317
12561
|
}
|
|
12318
12562
|
}
|
|
12319
12563
|
if (!match) {
|
|
12320
|
-
result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${
|
|
12564
|
+
result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path17.relative(process.cwd(), first)}`);
|
|
12321
12565
|
return false;
|
|
12322
12566
|
}
|
|
12323
12567
|
}
|
|
@@ -12379,7 +12623,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12379
12623
|
// src/pipeline/salvage.ts
|
|
12380
12624
|
import { readFile as readFile11 } from "fs/promises";
|
|
12381
12625
|
import { existsSync as existsSync13 } from "fs";
|
|
12382
|
-
import
|
|
12626
|
+
import path18 from "path";
|
|
12383
12627
|
function providerClaimId(provider, objectId) {
|
|
12384
12628
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
12385
12629
|
}
|
|
@@ -12556,20 +12800,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
12556
12800
|
return null;
|
|
12557
12801
|
}
|
|
12558
12802
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
12559
|
-
const own =
|
|
12803
|
+
const own = path18.resolve(ownManifestPath);
|
|
12560
12804
|
const siblingManifestPaths = [
|
|
12561
12805
|
.../* @__PURE__ */ new Set([
|
|
12562
12806
|
...workspaceManifestPaths,
|
|
12563
|
-
...claimPaths.map((c) =>
|
|
12807
|
+
...claimPaths.map((c) => path18.join(path18.dirname(path18.resolve(c)), "pixelkiln.manifest.json"))
|
|
12564
12808
|
])
|
|
12565
12809
|
];
|
|
12566
12810
|
const siblings = [];
|
|
12567
12811
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
12568
|
-
if (
|
|
12812
|
+
if (path18.resolve(siblingManifestPath) === own) continue;
|
|
12569
12813
|
if (!existsSync13(siblingManifestPath)) continue;
|
|
12570
12814
|
try {
|
|
12571
12815
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
12572
|
-
siblings.push({ label:
|
|
12816
|
+
siblings.push({ label: path18.basename(path18.dirname(siblingManifestPath)), manifest });
|
|
12573
12817
|
} catch {
|
|
12574
12818
|
}
|
|
12575
12819
|
}
|
|
@@ -12691,7 +12935,7 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
12691
12935
|
// src/workspace.ts
|
|
12692
12936
|
import { mkdir as mkdir7, readFile as readFile12, rename as rename6, rm as rm6, writeFile as writeFile8 } from "fs/promises";
|
|
12693
12937
|
import { existsSync as existsSync14 } from "fs";
|
|
12694
|
-
import
|
|
12938
|
+
import path19 from "path";
|
|
12695
12939
|
import { z as z5 } from "zod";
|
|
12696
12940
|
var WorkspaceProjectSchema = z5.object({
|
|
12697
12941
|
id: z5.string().min(1),
|
|
@@ -12733,7 +12977,7 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
12733
12977
|
version: 1,
|
|
12734
12978
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
12735
12979
|
};
|
|
12736
|
-
await mkdir7(
|
|
12980
|
+
await mkdir7(path19.dirname(path19.resolve(workspacePath)), { recursive: true });
|
|
12737
12981
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
12738
12982
|
try {
|
|
12739
12983
|
await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
@@ -12743,12 +12987,12 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
12743
12987
|
}
|
|
12744
12988
|
}
|
|
12745
12989
|
function toPortablePath(dir, absolute) {
|
|
12746
|
-
return
|
|
12990
|
+
return path19.relative(dir, absolute).split(path19.sep).join("/");
|
|
12747
12991
|
}
|
|
12748
12992
|
function resolveProject(dir, project) {
|
|
12749
12993
|
return {
|
|
12750
|
-
manifestPath:
|
|
12751
|
-
lockPath:
|
|
12994
|
+
manifestPath: path19.resolve(dir, project.manifest.split("/").join(path19.sep)),
|
|
12995
|
+
lockPath: path19.resolve(dir, project.lock.split("/").join(path19.sep))
|
|
12752
12996
|
};
|
|
12753
12997
|
}
|
|
12754
12998
|
function validateWorkspace(ws, dir) {
|
|
@@ -12761,7 +13005,7 @@ function validateWorkspace(ws, dir) {
|
|
|
12761
13005
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
12762
13006
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
12763
13007
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
12764
|
-
if (
|
|
13008
|
+
if (path19.isAbsolute(project.manifest) || path19.isAbsolute(project.lock)) {
|
|
12765
13009
|
diagnostics.push({
|
|
12766
13010
|
id: "absolute-path",
|
|
12767
13011
|
level: "warning",
|
|
@@ -12823,7 +13067,7 @@ function validateWorkspace(ws, dir) {
|
|
|
12823
13067
|
|
|
12824
13068
|
// src/pick/salvage-server.ts
|
|
12825
13069
|
import { mkdir as mkdir8, writeFile as writeFile9, readFile as readFile14 } from "fs/promises";
|
|
12826
|
-
import
|
|
13070
|
+
import path20 from "path";
|
|
12827
13071
|
|
|
12828
13072
|
// src/pick/salvage-sheet.ts
|
|
12829
13073
|
var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -13088,7 +13332,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13088
13332
|
});
|
|
13089
13333
|
const html = renderSalvageSheet(orphans, {
|
|
13090
13334
|
styleId: ctx.styleId,
|
|
13091
|
-
importDir:
|
|
13335
|
+
importDir: path20.relative(process.cwd(), ctx.importDir) || "."
|
|
13092
13336
|
});
|
|
13093
13337
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
13094
13338
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -13144,9 +13388,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13144
13388
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
13145
13389
|
decodePng(buf);
|
|
13146
13390
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
13147
|
-
const rel =
|
|
13148
|
-
const outFile =
|
|
13149
|
-
await mkdir8(
|
|
13391
|
+
const rel = path20.join("_salvaged", `${assetId}.png`);
|
|
13392
|
+
const outFile = path20.resolve(ctx.importDir, rel);
|
|
13393
|
+
await mkdir8(path20.dirname(outFile), { recursive: true });
|
|
13150
13394
|
await writeFile9(outFile, buf);
|
|
13151
13395
|
ctx.manifest.assets[assetId] = {
|
|
13152
13396
|
prompt: orphan.prompt,
|
|
@@ -13179,7 +13423,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13179
13423
|
sourceUrl: durableSource,
|
|
13180
13424
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
13181
13425
|
outputs: [{
|
|
13182
|
-
path: portableOutputPath(outFile,
|
|
13426
|
+
path: portableOutputPath(outFile, path20.dirname(ctx.manifestPath)),
|
|
13183
13427
|
sha256: sha256(buf)
|
|
13184
13428
|
}],
|
|
13185
13429
|
submittedAt: orphan.createdAt,
|
|
@@ -13214,19 +13458,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13214
13458
|
}
|
|
13215
13459
|
|
|
13216
13460
|
// src/cli/project.ts
|
|
13217
|
-
import
|
|
13461
|
+
import path22 from "path";
|
|
13218
13462
|
import { existsSync as existsSync15 } from "fs";
|
|
13219
13463
|
|
|
13220
13464
|
// src/project.ts
|
|
13221
|
-
import
|
|
13465
|
+
import path21 from "path";
|
|
13222
13466
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
13223
|
-
const absolute =
|
|
13224
|
-
const root =
|
|
13467
|
+
const absolute = path21.resolve(manifestPath);
|
|
13468
|
+
const root = path21.dirname(absolute);
|
|
13225
13469
|
if (opts.env !== false) {
|
|
13226
13470
|
loadEnvFiles(root);
|
|
13227
|
-
if (
|
|
13471
|
+
if (path21.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
13228
13472
|
}
|
|
13229
|
-
const lockPath =
|
|
13473
|
+
const lockPath = path21.resolve(opts.lockPath ?? path21.join(root, "pixelkiln.lock.json"));
|
|
13230
13474
|
const loaded = await loadManifest(absolute);
|
|
13231
13475
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
13232
13476
|
const lock = await loadLock(lockPath);
|
|
@@ -13357,7 +13601,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
13357
13601
|
|
|
13358
13602
|
// src/cli/project.ts
|
|
13359
13603
|
async function provenanceFile(id, file) {
|
|
13360
|
-
const absolute =
|
|
13604
|
+
const absolute = path22.resolve(file);
|
|
13361
13605
|
return {
|
|
13362
13606
|
id,
|
|
13363
13607
|
path: absolute,
|
|
@@ -13436,7 +13680,7 @@ async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
|
13436
13680
|
if (!existsSync15(workspacePath)) {
|
|
13437
13681
|
throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
|
|
13438
13682
|
}
|
|
13439
|
-
const dir =
|
|
13683
|
+
const dir = path22.dirname(path22.resolve(workspacePath));
|
|
13440
13684
|
const ws = await loadWorkspace(workspacePath);
|
|
13441
13685
|
const diagnostics = validateWorkspace(ws, dir);
|
|
13442
13686
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -13459,9 +13703,9 @@ async function openAccountProject(args) {
|
|
|
13459
13703
|
return project;
|
|
13460
13704
|
}
|
|
13461
13705
|
async function open(args) {
|
|
13462
|
-
if (!existsSync15(
|
|
13706
|
+
if (!existsSync15(path22.resolve(args.manifest))) {
|
|
13463
13707
|
throw new ProjectError(
|
|
13464
|
-
`No manifest at ${
|
|
13708
|
+
`No manifest at ${path22.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
13465
13709
|
);
|
|
13466
13710
|
}
|
|
13467
13711
|
const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
|
|
@@ -13503,7 +13747,7 @@ function providerCache(mode2) {
|
|
|
13503
13747
|
|
|
13504
13748
|
// src/cli/commands/account.ts
|
|
13505
13749
|
async function runBalance(args) {
|
|
13506
|
-
loadEnvFiles(
|
|
13750
|
+
loadEnvFiles(path23.dirname(path23.resolve(args.manifest)));
|
|
13507
13751
|
loadEnvFiles(process.cwd());
|
|
13508
13752
|
const loaded = await loadManifest(args.manifest);
|
|
13509
13753
|
const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
|
|
@@ -13576,12 +13820,12 @@ async function runAdopt(args) {
|
|
|
13576
13820
|
tagged ${n} object(s) upstream`);
|
|
13577
13821
|
}
|
|
13578
13822
|
if (args.writePrompts) {
|
|
13579
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
13823
|
+
const { filled, stillEmpty } = await writePromptsBack(path23.resolve(args.manifest), lock, {
|
|
13580
13824
|
onProgress: log,
|
|
13581
13825
|
provider: provider.id,
|
|
13582
13826
|
assetIds: specs.map((spec) => spec.assetId)
|
|
13583
13827
|
});
|
|
13584
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
13828
|
+
log(` recovered ${filled} prompt(s) into ${path23.relative(process.cwd(), args.manifest)}`);
|
|
13585
13829
|
const reloaded = await loadManifest(args.manifest);
|
|
13586
13830
|
const rebased = (await resolveSpecs(reloaded, {
|
|
13587
13831
|
styles: args.styles,
|
|
@@ -13611,12 +13855,12 @@ async function runSalvage2(args) {
|
|
|
13611
13855
|
const provider = providerFor(accountProvider);
|
|
13612
13856
|
const jsonMode = args.dryRun && args.json;
|
|
13613
13857
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
13614
|
-
const ownLock =
|
|
13858
|
+
const ownLock = path23.resolve(args.lock);
|
|
13615
13859
|
let workspaceProjects = [];
|
|
13616
13860
|
let workspaceDir = "";
|
|
13617
13861
|
if (args.workspace) {
|
|
13618
|
-
const workspacePath =
|
|
13619
|
-
workspaceDir =
|
|
13862
|
+
const workspacePath = path23.resolve(args.workspace);
|
|
13863
|
+
workspaceDir = path23.dirname(workspacePath);
|
|
13620
13864
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
13621
13865
|
workspaceProjects = complete.ws.projects;
|
|
13622
13866
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -13626,11 +13870,11 @@ async function runSalvage2(args) {
|
|
|
13626
13870
|
.../* @__PURE__ */ new Set([
|
|
13627
13871
|
...workspaceLockPaths,
|
|
13628
13872
|
...existsSync16(ownLock) ? [ownLock] : [],
|
|
13629
|
-
...args.claims.map((c) =>
|
|
13873
|
+
...args.claims.map((c) => path23.resolve(c))
|
|
13630
13874
|
])
|
|
13631
13875
|
];
|
|
13632
13876
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
13633
|
-
for (const p of lockPaths) diag(` ${
|
|
13877
|
+
for (const p of lockPaths) diag(` ${path23.relative(process.cwd(), p)}`);
|
|
13634
13878
|
if (!args.claims.length && !args.workspace) {
|
|
13635
13879
|
diag(
|
|
13636
13880
|
`
|
|
@@ -13638,7 +13882,7 @@ async function runSalvage2(args) {
|
|
|
13638
13882
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
13639
13883
|
register every project in a workspace catalog and pass --workspace.`
|
|
13640
13884
|
);
|
|
13641
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
13885
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path23.resolve(args.manifest))) {
|
|
13642
13886
|
diag(
|
|
13643
13887
|
`
|
|
13644
13888
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -13757,7 +14001,7 @@ async function runSalvage2(args) {
|
|
|
13757
14001
|
manifestPath: loaded.path,
|
|
13758
14002
|
manifest: accountManifest,
|
|
13759
14003
|
styleId,
|
|
13760
|
-
importDir:
|
|
14004
|
+
importDir: path23.resolve(loaded.root, style.outDir),
|
|
13761
14005
|
lock,
|
|
13762
14006
|
lockPath: args.lock
|
|
13763
14007
|
},
|
|
@@ -13884,19 +14128,20 @@ async function runTag(args) {
|
|
|
13884
14128
|
}
|
|
13885
14129
|
|
|
13886
14130
|
// src/cli/commands/edit.ts
|
|
13887
|
-
import
|
|
14131
|
+
import path26 from "path";
|
|
13888
14132
|
|
|
13889
14133
|
// src/pipeline/hand-edit.ts
|
|
13890
14134
|
import { existsSync as existsSync17 } from "fs";
|
|
13891
14135
|
import { copyFile, mkdir as mkdir9, readFile as readFile16, rename as rename8, rm as rm8, writeFile as writeFile11 } from "fs/promises";
|
|
13892
|
-
import
|
|
14136
|
+
import path25 from "path";
|
|
13893
14137
|
import { z as z7 } from "zod";
|
|
13894
14138
|
|
|
13895
14139
|
// src/manifest-edit.ts
|
|
13896
14140
|
import { readFile as readFile15, rename as rename7, rm as rm7, writeFile as writeFile10 } from "fs/promises";
|
|
13897
|
-
import
|
|
14141
|
+
import path24 from "path";
|
|
13898
14142
|
import { z as z6 } from "zod";
|
|
13899
14143
|
var HexSha = z6.string().regex(/^[0-9a-f]{64}$/);
|
|
14144
|
+
var ProjectFileSchema = z6.string().min(1).refine((file) => !path24.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
|
|
13900
14145
|
var AssetPatchSchema = z6.object({
|
|
13901
14146
|
prompt: z6.string().optional(),
|
|
13902
14147
|
/** Per-style override for the style being viewed; null removes it. */
|
|
@@ -13945,10 +14190,11 @@ var NewAssetSchema = z6.object({
|
|
|
13945
14190
|
styles: z6.array(z6.string().min(1)).optional(),
|
|
13946
14191
|
/**
|
|
13947
14192
|
* A controlled regeneration of another asset in the same manifest,
|
|
13948
|
-
* instead of a fresh generation.
|
|
13949
|
-
* `
|
|
13950
|
-
*
|
|
13951
|
-
*
|
|
14193
|
+
* instead of a fresh generation. Every mode but `outpaint` can be
|
|
14194
|
+
* created here: `inpaint` names a mask file the gallery's mask editor
|
|
14195
|
+
* wrote first, `interpolate` names an existing image as its ending
|
|
14196
|
+
* keyframe, and `animate-skeleton` names the keypoints file the gallery
|
|
14197
|
+
* wrote first.
|
|
13952
14198
|
*/
|
|
13953
14199
|
revision: z6.discriminatedUnion("mode", [
|
|
13954
14200
|
z6.object({
|
|
@@ -13965,15 +14211,38 @@ var NewAssetSchema = z6.object({
|
|
|
13965
14211
|
dithering: RevisionDitheringSchema.optional(),
|
|
13966
14212
|
ditheringStrength: z6.number().min(0).max(10).optional()
|
|
13967
14213
|
}).strict(),
|
|
14214
|
+
z6.object({
|
|
14215
|
+
mode: z6.literal("inpaint"),
|
|
14216
|
+
from: z6.string().min(1),
|
|
14217
|
+
/** Black-and-white PNG the same size as the parent; white is repainted. */
|
|
14218
|
+
mask: ProjectFileSchema
|
|
14219
|
+
}).strict(),
|
|
14220
|
+
z6.object({
|
|
14221
|
+
mode: z6.literal("interpolate"),
|
|
14222
|
+
from: z6.string().min(1),
|
|
14223
|
+
/** The ending keyframe: an image already in the project, the parent's size. */
|
|
14224
|
+
lastFrame: ProjectFileSchema,
|
|
14225
|
+
fps: z6.number().int().min(1).max(60).optional()
|
|
14226
|
+
}).strict(),
|
|
13968
14227
|
z6.object({ mode: z6.literal("correct-pixelart"), from: z6.string().min(1), strength: z6.number().min(0).max(1).optional() }).strict(),
|
|
14228
|
+
z6.object({
|
|
14229
|
+
mode: z6.literal("remove-background"),
|
|
14230
|
+
from: z6.string().min(1),
|
|
14231
|
+
removalTask: z6.enum(["simple", "complex"]).optional(),
|
|
14232
|
+
/** A short description of the foreground, sent as PixelLab's text hint. */
|
|
14233
|
+
description: z6.string().min(1).optional()
|
|
14234
|
+
}).strict(),
|
|
13969
14235
|
z6.object({
|
|
13970
14236
|
mode: z6.enum(["animate", "animate-pixminimax"]),
|
|
13971
14237
|
from: z6.string().min(1),
|
|
13972
14238
|
frames: z6.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
|
|
13973
14239
|
fps: z6.number().int().min(1).max(60).optional(),
|
|
13974
14240
|
enhancePrompt: z6.boolean().optional(),
|
|
13975
|
-
|
|
13976
|
-
|
|
14241
|
+
driftThreshold: z6.number().min(0).optional(),
|
|
14242
|
+
/** `animate-pixminimax` only; the loader refuses these on `animate`. */
|
|
14243
|
+
direction: CharacterDirectionSchema.optional(),
|
|
14244
|
+
subjectDescription: z6.string().min(1).max(300).optional(),
|
|
14245
|
+
initialPose: z6.string().min(1).max(300).optional()
|
|
13977
14246
|
}).strict(),
|
|
13978
14247
|
z6.object({
|
|
13979
14248
|
mode: z6.literal("edit-animation"),
|
|
@@ -14291,7 +14560,7 @@ function applyEdit(raw, edit) {
|
|
|
14291
14560
|
}
|
|
14292
14561
|
}
|
|
14293
14562
|
async function withCandidate(manifestPath, edit, use, unchanged) {
|
|
14294
|
-
const absolute =
|
|
14563
|
+
const absolute = path24.resolve(manifestPath);
|
|
14295
14564
|
const original = await readFile15(absolute, "utf8");
|
|
14296
14565
|
if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
14297
14566
|
let raw;
|
|
@@ -14305,9 +14574,9 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
|
|
|
14305
14574
|
applyEdit(raw, edit);
|
|
14306
14575
|
const next = serializeLike(original, raw);
|
|
14307
14576
|
if (next === original) return unchanged(absolute, original);
|
|
14308
|
-
const tmp =
|
|
14309
|
-
|
|
14310
|
-
`.${
|
|
14577
|
+
const tmp = path24.join(
|
|
14578
|
+
path24.dirname(absolute),
|
|
14579
|
+
`.${path24.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
14311
14580
|
);
|
|
14312
14581
|
try {
|
|
14313
14582
|
await writeFile10(tmp, next);
|
|
@@ -14380,11 +14649,11 @@ async function priceManifestEdit(manifestPath, edit, opts = {}) {
|
|
|
14380
14649
|
var HAND_EDIT_DIR = "edits";
|
|
14381
14650
|
function handEditPath(loaded, spec) {
|
|
14382
14651
|
const style = loaded.manifest.styles[spec.styleId];
|
|
14383
|
-
const outDir =
|
|
14384
|
-
const relative =
|
|
14385
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
14386
|
-
const file = inside ? relative :
|
|
14387
|
-
return
|
|
14652
|
+
const outDir = path25.resolve(loaded.root, style?.outDir ?? ".");
|
|
14653
|
+
const relative = path25.relative(outDir, spec.outFile);
|
|
14654
|
+
const inside = relative && !relative.startsWith("..") && !path25.isAbsolute(relative);
|
|
14655
|
+
const file = inside ? relative : path25.basename(spec.outFile);
|
|
14656
|
+
return path25.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
14388
14657
|
}
|
|
14389
14658
|
function handEditBases(spec, lock) {
|
|
14390
14659
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -14426,7 +14695,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
14426
14695
|
`${spec.styleId}/${spec.assetId} declares ${spec.source}, one committed file placed for the whole set; clear that source (pixelkiln edit detach) to hand-edit the members one by one`
|
|
14427
14696
|
);
|
|
14428
14697
|
}
|
|
14429
|
-
const existing = spec.source ?
|
|
14698
|
+
const existing = spec.source ? path25.resolve(loaded.root, spec.source) : null;
|
|
14430
14699
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
14431
14700
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
14432
14701
|
let created = false;
|
|
@@ -14437,7 +14706,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
14437
14706
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
14438
14707
|
);
|
|
14439
14708
|
}
|
|
14440
|
-
await mkdir9(
|
|
14709
|
+
await mkdir9(path25.dirname(member.path), { recursive: true });
|
|
14441
14710
|
await copyFile(member.base, member.path);
|
|
14442
14711
|
created = true;
|
|
14443
14712
|
}
|
|
@@ -14575,7 +14844,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
14575
14844
|
editor: input.editor,
|
|
14576
14845
|
protocol: input.protocol,
|
|
14577
14846
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
14578
|
-
project: projectPath ?
|
|
14847
|
+
project: projectPath ? path25.basename(projectPath) : null,
|
|
14579
14848
|
outputs: incoming.map((frame, index) => ({
|
|
14580
14849
|
role: frame.role,
|
|
14581
14850
|
basedOn: basedOnHash(entry, index),
|
|
@@ -14635,7 +14904,7 @@ async function runEdit(args) {
|
|
|
14635
14904
|
const set = started.members.length > 1;
|
|
14636
14905
|
const kind = spec.generator === "frames" ? "frames" : "members";
|
|
14637
14906
|
log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
14638
|
-
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${
|
|
14907
|
+
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path26.relative(process.cwd(), member.path)}`);
|
|
14639
14908
|
if (args.noOpen) {
|
|
14640
14909
|
log(` edit ${set ? "them" : "it"} with your own tool, then run pixelkiln plan; mount and pack place ${set ? "them" : "it"} in place of the generated art`);
|
|
14641
14910
|
} else {
|
|
@@ -14645,24 +14914,24 @@ async function runEdit(args) {
|
|
|
14645
14914
|
}
|
|
14646
14915
|
|
|
14647
14916
|
// src/cli/commands/gallery.ts
|
|
14648
|
-
import
|
|
14649
|
-
import { existsSync as
|
|
14917
|
+
import path36 from "path";
|
|
14918
|
+
import { existsSync as existsSync27 } from "fs";
|
|
14650
14919
|
|
|
14651
14920
|
// src/gallery/snapshot.ts
|
|
14652
|
-
import { createHash as
|
|
14653
|
-
import { existsSync as
|
|
14654
|
-
import { readFile as
|
|
14655
|
-
import
|
|
14921
|
+
import { createHash as createHash4 } from "crypto";
|
|
14922
|
+
import { existsSync as existsSync22 } from "fs";
|
|
14923
|
+
import { readFile as readFile20, stat as stat2 } from "fs/promises";
|
|
14924
|
+
import path32 from "path";
|
|
14656
14925
|
|
|
14657
14926
|
// src/gallery/edit.ts
|
|
14658
|
-
import { readFile as
|
|
14659
|
-
import
|
|
14660
|
-
import { z as
|
|
14927
|
+
import { readFile as readFile19, rename as rename9, writeFile as writeFile15 } from "fs/promises";
|
|
14928
|
+
import path30 from "path";
|
|
14929
|
+
import { z as z11 } from "zod";
|
|
14661
14930
|
|
|
14662
14931
|
// src/gallery/upload.ts
|
|
14663
14932
|
import { existsSync as existsSync18 } from "fs";
|
|
14664
14933
|
import { mkdir as mkdir10, writeFile as writeFile12 } from "fs/promises";
|
|
14665
|
-
import
|
|
14934
|
+
import path27 from "path";
|
|
14666
14935
|
import { z as z8 } from "zod";
|
|
14667
14936
|
var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
|
|
14668
14937
|
var UploadImageSchema = z8.object({
|
|
@@ -14675,12 +14944,12 @@ var UploadImageSchema = z8.object({
|
|
|
14675
14944
|
overwrite: z8.boolean().optional()
|
|
14676
14945
|
}).strict();
|
|
14677
14946
|
async function saveProjectImage(root, request) {
|
|
14678
|
-
const file =
|
|
14679
|
-
const relative =
|
|
14680
|
-
if (relative.startsWith("..") ||
|
|
14947
|
+
const file = path27.resolve(root, request.path);
|
|
14948
|
+
const relative = path27.relative(root, file);
|
|
14949
|
+
if (relative.startsWith("..") || path27.isAbsolute(relative)) {
|
|
14681
14950
|
throw new ManifestEditError("an upload must stay inside the project");
|
|
14682
14951
|
}
|
|
14683
|
-
const extension =
|
|
14952
|
+
const extension = path27.extname(file).toLowerCase();
|
|
14684
14953
|
const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
|
|
14685
14954
|
if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
|
|
14686
14955
|
const bytes = Buffer.from(request.base64, "base64");
|
|
@@ -14693,57 +14962,113 @@ async function saveProjectImage(root, request) {
|
|
|
14693
14962
|
throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
|
|
14694
14963
|
}
|
|
14695
14964
|
if (existsSync18(file) && !request.overwrite) {
|
|
14696
|
-
throw new ManifestEditError(`${relative.split(
|
|
14965
|
+
throw new ManifestEditError(`${relative.split(path27.sep).join("/")} already exists; choose another name, or replace it`);
|
|
14697
14966
|
}
|
|
14698
|
-
await mkdir10(
|
|
14967
|
+
await mkdir10(path27.dirname(file), { recursive: true });
|
|
14699
14968
|
await writeFile12(file, bytes);
|
|
14700
|
-
return relative.split(
|
|
14969
|
+
return relative.split(path27.sep).join("/");
|
|
14701
14970
|
}
|
|
14702
14971
|
|
|
14703
|
-
// src/gallery/
|
|
14972
|
+
// src/gallery/image-revision.ts
|
|
14704
14973
|
import { existsSync as existsSync19 } from "fs";
|
|
14705
|
-
import {
|
|
14706
|
-
import
|
|
14974
|
+
import { readFile as readFile17, rm as rm9, writeFile as writeFile13 } from "fs/promises";
|
|
14975
|
+
import path28 from "path";
|
|
14707
14976
|
import { z as z9 } from "zod";
|
|
14708
|
-
var
|
|
14709
|
-
|
|
14710
|
-
styleId: z9.string().min(1),
|
|
14711
|
-
assetId: z9.string().min(1),
|
|
14712
|
-
frames: z9.number().int().min(3).max(15).optional()
|
|
14713
|
-
}).strict();
|
|
14714
|
-
var SkeletonAnimationRequestSchema = z9.object({
|
|
14715
|
-
action: z9.literal("create-skeleton-animation"),
|
|
14977
|
+
var InpaintRevisionRequestSchema = z9.object({
|
|
14978
|
+
action: z9.literal("create-inpaint-revision"),
|
|
14716
14979
|
project: z9.string().min(1).optional(),
|
|
14717
14980
|
expectedSha256: z9.string().regex(/^[0-9a-f]{64}$/),
|
|
14718
14981
|
styleId: z9.string().min(1),
|
|
14719
|
-
/** The sprite
|
|
14982
|
+
/** The sprite being repainted. */
|
|
14720
14983
|
from: z9.string().min(1),
|
|
14721
14984
|
assetId: z9.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
14722
|
-
/**
|
|
14985
|
+
/** What to paint into the masked area. */
|
|
14723
14986
|
prompt: z9.string().min(1),
|
|
14987
|
+
/** Manifest-relative `.png` destination for the mask. */
|
|
14988
|
+
maskPath: z9.string().min(1),
|
|
14989
|
+
maskBase64: z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/),
|
|
14990
|
+
/** Replace a file already at `maskPath`. */
|
|
14991
|
+
overwrite: z9.boolean().optional()
|
|
14992
|
+
}).strict();
|
|
14993
|
+
async function createInpaintRevision(manifestPath, request) {
|
|
14994
|
+
const root = path28.dirname(path28.resolve(manifestPath));
|
|
14995
|
+
if (path28.extname(request.maskPath).toLowerCase() !== ".png") {
|
|
14996
|
+
throw new ManifestEditError("the mask must be a .png path inside the project");
|
|
14997
|
+
}
|
|
14998
|
+
const bytes = Buffer.from(request.maskBase64, "base64");
|
|
14999
|
+
const metadata = imageMetadata(bytes);
|
|
15000
|
+
if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
|
|
15001
|
+
const file = path28.resolve(root, request.maskPath);
|
|
15002
|
+
const previous = existsSync19(file) ? await readFile17(file) : null;
|
|
15003
|
+
const mask = await saveProjectImage(root, {
|
|
15004
|
+
action: "upload-image",
|
|
15005
|
+
path: request.maskPath,
|
|
15006
|
+
base64: request.maskBase64,
|
|
15007
|
+
...request.overwrite ? { overwrite: true } : {}
|
|
15008
|
+
});
|
|
15009
|
+
try {
|
|
15010
|
+
await applyManifestEdit(manifestPath, {
|
|
15011
|
+
action: "add-asset",
|
|
15012
|
+
assetId: request.assetId,
|
|
15013
|
+
expectedSha256: request.expectedSha256,
|
|
15014
|
+
asset: {
|
|
15015
|
+
prompt: request.prompt,
|
|
15016
|
+
styles: [request.styleId],
|
|
15017
|
+
revision: { mode: "inpaint", from: request.from, mask }
|
|
15018
|
+
}
|
|
15019
|
+
});
|
|
15020
|
+
} catch (error) {
|
|
15021
|
+
if (previous) await writeFile13(file, previous);
|
|
15022
|
+
else await rm9(file, { force: true });
|
|
15023
|
+
throw error;
|
|
15024
|
+
}
|
|
15025
|
+
return mask;
|
|
15026
|
+
}
|
|
15027
|
+
|
|
15028
|
+
// src/gallery/skeleton.ts
|
|
15029
|
+
import { existsSync as existsSync20 } from "fs";
|
|
15030
|
+
import { mkdir as mkdir11, readFile as readFile18, rm as rm10, writeFile as writeFile14 } from "fs/promises";
|
|
15031
|
+
import path29 from "path";
|
|
15032
|
+
import { z as z10 } from "zod";
|
|
15033
|
+
var SkeletonEstimateRequestSchema = z10.object({
|
|
15034
|
+
project: z10.string().min(1).optional(),
|
|
15035
|
+
styleId: z10.string().min(1),
|
|
15036
|
+
assetId: z10.string().min(1),
|
|
15037
|
+
frames: z10.number().int().min(3).max(15).optional()
|
|
15038
|
+
}).strict();
|
|
15039
|
+
var SkeletonAnimationRequestSchema = z10.object({
|
|
15040
|
+
action: z10.literal("create-skeleton-animation"),
|
|
15041
|
+
project: z10.string().min(1).optional(),
|
|
15042
|
+
expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
|
|
15043
|
+
styleId: z10.string().min(1),
|
|
15044
|
+
/** The sprite the animation poses. */
|
|
15045
|
+
from: z10.string().min(1),
|
|
15046
|
+
assetId: z10.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
|
|
15047
|
+
/** The motion, sent as PixelLab's `action`. */
|
|
15048
|
+
prompt: z10.string().min(1),
|
|
14724
15049
|
direction: CharacterDirectionSchema,
|
|
14725
|
-
description:
|
|
14726
|
-
skeletonTemplate:
|
|
15050
|
+
description: z10.string().min(1).optional(),
|
|
15051
|
+
skeletonTemplate: z10.string().min(1).optional(),
|
|
14727
15052
|
/** Manifest-relative JSON path inside the project. */
|
|
14728
|
-
keypointsFile:
|
|
15053
|
+
keypointsFile: z10.string().min(1),
|
|
14729
15054
|
/** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
|
|
14730
15055
|
set: SkeletonSetSchema.optional(),
|
|
14731
15056
|
/** Replace an existing `keypointsFile` with `set`. */
|
|
14732
|
-
overwrite:
|
|
15057
|
+
overwrite: z10.boolean().optional()
|
|
14733
15058
|
}).strict();
|
|
14734
|
-
var SkeletonKeypointsUpdateSchema =
|
|
14735
|
-
action:
|
|
14736
|
-
project:
|
|
14737
|
-
styleId:
|
|
14738
|
-
assetId:
|
|
15059
|
+
var SkeletonKeypointsUpdateSchema = z10.object({
|
|
15060
|
+
action: z10.literal("update-skeleton-keypoints"),
|
|
15061
|
+
project: z10.string().min(1).optional(),
|
|
15062
|
+
styleId: z10.string().min(1),
|
|
15063
|
+
assetId: z10.string().min(1),
|
|
14739
15064
|
/** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
|
|
14740
|
-
expectedSha256:
|
|
15065
|
+
expectedSha256: z10.string().regex(/^[0-9a-f]{64}$/),
|
|
14741
15066
|
set: SkeletonSetSchema
|
|
14742
15067
|
}).strict();
|
|
14743
15068
|
var DEFAULT_ESTIMATE_LIMIT = 10;
|
|
14744
15069
|
function skeletonSourceFile(spec) {
|
|
14745
|
-
if (spec.source) return
|
|
14746
|
-
if (spec.quality &&
|
|
15070
|
+
if (spec.source) return path29.resolve(spec.root, spec.source);
|
|
15071
|
+
if (spec.quality && existsSync20(spec.quality.outFile)) return spec.quality.outFile;
|
|
14747
15072
|
return spec.outFile;
|
|
14748
15073
|
}
|
|
14749
15074
|
function createGallerySkeletonHandlers(opts) {
|
|
@@ -14764,10 +15089,10 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
14764
15089
|
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
|
|
14765
15090
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14766
15091
|
const file = skeletonSourceFile(spec);
|
|
14767
|
-
if (!
|
|
14768
|
-
const bytes = await
|
|
15092
|
+
if (!existsSync20(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
|
|
15093
|
+
const bytes = await readFile18(file);
|
|
14769
15094
|
const metadata = imageMetadata(bytes);
|
|
14770
|
-
if (!metadata) throw new ManifestEditError(`${
|
|
15095
|
+
if (!metadata) throw new ManifestEditError(`${path29.basename(file)} is not a readable PNG or JPEG`);
|
|
14771
15096
|
const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
14772
15097
|
if (problem) throw new ManifestEditError(`${key} is ${problem}`);
|
|
14773
15098
|
if (used >= limit) {
|
|
@@ -14784,7 +15109,7 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
14784
15109
|
log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
|
|
14785
15110
|
return {
|
|
14786
15111
|
set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
|
|
14787
|
-
source:
|
|
15112
|
+
source: path29.relative(ctx.loaded.root, file).split(path29.sep).join("/"),
|
|
14788
15113
|
session: status()
|
|
14789
15114
|
};
|
|
14790
15115
|
}
|
|
@@ -14798,21 +15123,21 @@ function billedAmount(usage) {
|
|
|
14798
15123
|
return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
|
|
14799
15124
|
}
|
|
14800
15125
|
async function createSkeletonAnimation(manifestPath, request) {
|
|
14801
|
-
const root =
|
|
14802
|
-
const file =
|
|
14803
|
-
const relative =
|
|
14804
|
-
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") ||
|
|
15126
|
+
const root = path29.dirname(path29.resolve(manifestPath));
|
|
15127
|
+
const file = path29.resolve(root, request.keypointsFile);
|
|
15128
|
+
const relative = path29.relative(root, file);
|
|
15129
|
+
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path29.isAbsolute(relative)) {
|
|
14805
15130
|
throw new ManifestEditError("the keypoints file must be a .json path inside the project");
|
|
14806
15131
|
}
|
|
14807
|
-
const existed =
|
|
15132
|
+
const existed = existsSync20(file);
|
|
14808
15133
|
let previous = null;
|
|
14809
15134
|
if (request.set) {
|
|
14810
15135
|
if (existed && !request.overwrite) {
|
|
14811
15136
|
throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
|
|
14812
15137
|
}
|
|
14813
|
-
if (existed) previous = await
|
|
14814
|
-
await mkdir11(
|
|
14815
|
-
await
|
|
15138
|
+
if (existed) previous = await readFile18(file);
|
|
15139
|
+
await mkdir11(path29.dirname(file), { recursive: true });
|
|
15140
|
+
await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
14816
15141
|
} else if (!existed) {
|
|
14817
15142
|
throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
|
|
14818
15143
|
}
|
|
@@ -14827,7 +15152,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
14827
15152
|
revision: {
|
|
14828
15153
|
mode: "animate-skeleton",
|
|
14829
15154
|
from: request.from,
|
|
14830
|
-
keypointsFile: relative.split(
|
|
15155
|
+
keypointsFile: relative.split(path29.sep).join("/"),
|
|
14831
15156
|
direction: request.direction,
|
|
14832
15157
|
...request.description ? { description: request.description } : {},
|
|
14833
15158
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
@@ -14836,8 +15161,8 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
14836
15161
|
});
|
|
14837
15162
|
} catch (error) {
|
|
14838
15163
|
if (request.set) {
|
|
14839
|
-
if (previous) await
|
|
14840
|
-
else await
|
|
15164
|
+
if (previous) await writeFile14(file, previous);
|
|
15165
|
+
else await rm10(file, { force: true });
|
|
14841
15166
|
}
|
|
14842
15167
|
throw error;
|
|
14843
15168
|
}
|
|
@@ -14848,64 +15173,64 @@ async function updateSkeletonKeypoints(ctx, request) {
|
|
|
14848
15173
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
14849
15174
|
const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
|
|
14850
15175
|
if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
|
|
14851
|
-
const relative =
|
|
14852
|
-
if (relative.startsWith("..") ||
|
|
15176
|
+
const relative = path29.relative(ctx.loaded.root, file);
|
|
15177
|
+
if (relative.startsWith("..") || path29.isAbsolute(relative)) {
|
|
14853
15178
|
throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
|
|
14854
15179
|
}
|
|
14855
|
-
const current =
|
|
15180
|
+
const current = existsSync20(file) ? sha256(await readFile18(file)) : null;
|
|
14856
15181
|
if (current !== request.expectedSha256) {
|
|
14857
15182
|
throw Object.assign(
|
|
14858
|
-
new Error(`${relative.split(
|
|
15183
|
+
new Error(`${relative.split(path29.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
|
|
14859
15184
|
{ status: 409 }
|
|
14860
15185
|
);
|
|
14861
15186
|
}
|
|
14862
|
-
await
|
|
14863
|
-
return relative.split(
|
|
15187
|
+
await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
15188
|
+
return relative.split(path29.sep).join("/");
|
|
14864
15189
|
}
|
|
14865
15190
|
|
|
14866
15191
|
// src/gallery/edit.ts
|
|
14867
|
-
var Base64 =
|
|
14868
|
-
var HandEditRequestSchema =
|
|
14869
|
-
|
|
14870
|
-
action:
|
|
14871
|
-
project:
|
|
14872
|
-
assetId:
|
|
14873
|
-
styleId:
|
|
14874
|
-
expectedSha256:
|
|
15192
|
+
var Base64 = z11.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
|
|
15193
|
+
var HandEditRequestSchema = z11.discriminatedUnion("action", [
|
|
15194
|
+
z11.object({
|
|
15195
|
+
action: z11.literal("start-edit"),
|
|
15196
|
+
project: z11.string().min(1).optional(),
|
|
15197
|
+
assetId: z11.string().min(1),
|
|
15198
|
+
styleId: z11.string().min(1),
|
|
15199
|
+
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
|
|
14875
15200
|
/** Also launch the author's editor on the file. */
|
|
14876
|
-
open:
|
|
15201
|
+
open: z11.boolean().optional()
|
|
14877
15202
|
}).strict(),
|
|
14878
|
-
|
|
14879
|
-
action:
|
|
14880
|
-
project:
|
|
14881
|
-
assetId:
|
|
14882
|
-
styleId:
|
|
14883
|
-
expectedSha256:
|
|
15203
|
+
z11.object({
|
|
15204
|
+
action: z11.literal("detach-edit"),
|
|
15205
|
+
project: z11.string().min(1).optional(),
|
|
15206
|
+
assetId: z11.string().min(1),
|
|
15207
|
+
styleId: z11.string().min(1),
|
|
15208
|
+
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/)
|
|
14884
15209
|
}).strict(),
|
|
14885
15210
|
/** From the in-browser editor: the page holds the bytes, the server validates and writes. */
|
|
14886
|
-
|
|
14887
|
-
action:
|
|
14888
|
-
project:
|
|
14889
|
-
assetId:
|
|
14890
|
-
styleId:
|
|
14891
|
-
expectedSha256:
|
|
15211
|
+
z11.object({
|
|
15212
|
+
action: z11.literal("save-edit"),
|
|
15213
|
+
project: z11.string().min(1).optional(),
|
|
15214
|
+
assetId: z11.string().min(1),
|
|
15215
|
+
styleId: z11.string().min(1),
|
|
15216
|
+
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
|
|
14892
15217
|
/** The flattened image; for a frame set, `frames` instead. */
|
|
14893
15218
|
png: Base64.min(1).optional(),
|
|
14894
15219
|
/** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
|
|
14895
|
-
frames:
|
|
15220
|
+
frames: z11.array(z11.object({ role: z11.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
|
|
14896
15221
|
/** The editor's layered project file, kept beside the edit. */
|
|
14897
15222
|
pxo: Base64.optional(),
|
|
14898
15223
|
/** Editor identity from the bridge's ready message. */
|
|
14899
|
-
editor:
|
|
14900
|
-
protocol:
|
|
15224
|
+
editor: z11.string().min(1).max(200),
|
|
15225
|
+
protocol: z11.number().int().positive()
|
|
14901
15226
|
}).strict()
|
|
14902
15227
|
]);
|
|
14903
15228
|
var UNDO_DEPTH = 20;
|
|
14904
|
-
var UndoRequestSchema =
|
|
14905
|
-
action:
|
|
14906
|
-
project:
|
|
15229
|
+
var UndoRequestSchema = z11.object({
|
|
15230
|
+
action: z11.literal("undo-edit"),
|
|
15231
|
+
project: z11.string().min(1).optional(),
|
|
14907
15232
|
/** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
|
|
14908
|
-
expectedSha256:
|
|
15233
|
+
expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/)
|
|
14909
15234
|
}).strict();
|
|
14910
15235
|
function createGalleryEditHandler(opts) {
|
|
14911
15236
|
const log2 = opts.onProgress ?? (() => {
|
|
@@ -14914,7 +15239,7 @@ function createGalleryEditHandler(opts) {
|
|
|
14914
15239
|
const history = /* @__PURE__ */ new Map();
|
|
14915
15240
|
const manifestOf = async (project) => {
|
|
14916
15241
|
try {
|
|
14917
|
-
return
|
|
15242
|
+
return path30.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
|
|
14918
15243
|
} catch {
|
|
14919
15244
|
return null;
|
|
14920
15245
|
}
|
|
@@ -14924,7 +15249,7 @@ function createGalleryEditHandler(opts) {
|
|
|
14924
15249
|
if (undo.success) {
|
|
14925
15250
|
const manifestPath2 = await manifestOf(undo.data.project);
|
|
14926
15251
|
if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
|
|
14927
|
-
const current = await
|
|
15252
|
+
const current = await readFile19(manifestPath2, "utf8");
|
|
14928
15253
|
if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
|
|
14929
15254
|
const stack = history.get(manifestPath2) ?? [];
|
|
14930
15255
|
const last = stack[stack.length - 1];
|
|
@@ -14934,17 +15259,17 @@ function createGalleryEditHandler(opts) {
|
|
|
14934
15259
|
throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
|
|
14935
15260
|
}
|
|
14936
15261
|
const tmp = manifestPath2 + ".pixelkiln-undo";
|
|
14937
|
-
await
|
|
15262
|
+
await writeFile15(tmp, last.before);
|
|
14938
15263
|
await rename9(tmp, manifestPath2);
|
|
14939
15264
|
stack.pop();
|
|
14940
|
-
log2(` manifest edit undone in ${
|
|
15265
|
+
log2(` manifest edit undone in ${path30.relative(process.cwd(), manifestPath2) || manifestPath2}`);
|
|
14941
15266
|
return opts.reload();
|
|
14942
15267
|
}
|
|
14943
15268
|
const manifestPath = await manifestOf(body?.project);
|
|
14944
|
-
const before = manifestPath ? await
|
|
15269
|
+
const before = manifestPath ? await readFile19(manifestPath, "utf8").catch(() => null) : null;
|
|
14945
15270
|
const build = await apply(body);
|
|
14946
15271
|
if (manifestPath && before !== null) {
|
|
14947
|
-
const after = await
|
|
15272
|
+
const after = await readFile19(manifestPath, "utf8").catch(() => null);
|
|
14948
15273
|
if (after !== null && after !== before) {
|
|
14949
15274
|
const stack = history.get(manifestPath) ?? [];
|
|
14950
15275
|
stack.push({ before, afterSha: sha256(after) });
|
|
@@ -14999,7 +15324,7 @@ function createEditApplier(opts, log2) {
|
|
|
14999
15324
|
} catch (error) {
|
|
15000
15325
|
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
15001
15326
|
}
|
|
15002
|
-
const written = await saveProjectImage(
|
|
15327
|
+
const written = await saveProjectImage(path30.dirname(path30.resolve(manifestPath2)), upload.data);
|
|
15003
15328
|
log2(` image added: ${written}`);
|
|
15004
15329
|
return opts.reload();
|
|
15005
15330
|
}
|
|
@@ -15035,6 +15360,23 @@ function createEditApplier(opts, log2) {
|
|
|
15035
15360
|
"invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
15036
15361
|
);
|
|
15037
15362
|
}
|
|
15363
|
+
const inpaint = InpaintRevisionRequestSchema.safeParse(body);
|
|
15364
|
+
if (inpaint.success) {
|
|
15365
|
+
let manifestPath2;
|
|
15366
|
+
try {
|
|
15367
|
+
manifestPath2 = await opts.manifestFor(inpaint.data.project);
|
|
15368
|
+
} catch (error) {
|
|
15369
|
+
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
15370
|
+
}
|
|
15371
|
+
const mask = await createInpaintRevision(manifestPath2, inpaint.data);
|
|
15372
|
+
log2(` manifest edited: added inpaint revision ${inpaint.data.assetId} (mask ${mask})`);
|
|
15373
|
+
return opts.reload();
|
|
15374
|
+
}
|
|
15375
|
+
if (body?.action === "create-inpaint-revision") {
|
|
15376
|
+
throw new ManifestEditError(
|
|
15377
|
+
"invalid inpaint revision: " + inpaint.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
15378
|
+
);
|
|
15379
|
+
}
|
|
15038
15380
|
const parsed = ManifestEditSchema.safeParse(body);
|
|
15039
15381
|
if (!parsed.success) {
|
|
15040
15382
|
throw new ManifestEditError(
|
|
@@ -15049,7 +15391,7 @@ function createEditApplier(opts, log2) {
|
|
|
15049
15391
|
}
|
|
15050
15392
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
15051
15393
|
if (result.changed) {
|
|
15052
|
-
const relative =
|
|
15394
|
+
const relative = path30.relative(process.cwd(), result.manifestPath);
|
|
15053
15395
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
15054
15396
|
log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
|
|
15055
15397
|
}
|
|
@@ -15100,8 +15442,8 @@ function createGalleryPriceHandler(opts) {
|
|
|
15100
15442
|
}
|
|
15101
15443
|
|
|
15102
15444
|
// src/pipeline/history.ts
|
|
15103
|
-
import { existsSync as
|
|
15104
|
-
import
|
|
15445
|
+
import { existsSync as existsSync21 } from "fs";
|
|
15446
|
+
import path31 from "path";
|
|
15105
15447
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
15106
15448
|
function historyLimit(manifest, env = process.env) {
|
|
15107
15449
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -15188,10 +15530,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
15188
15530
|
}
|
|
15189
15531
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
15190
15532
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
15191
|
-
if (!
|
|
15533
|
+
if (!existsSync21(file)) continue;
|
|
15192
15534
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
15193
15535
|
throw new OverwriteRefusedError(
|
|
15194
|
-
`${
|
|
15536
|
+
`${path31.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
15195
15537
|
[file]
|
|
15196
15538
|
);
|
|
15197
15539
|
}
|
|
@@ -15244,7 +15586,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
15244
15586
|
// src/gallery/snapshot.ts
|
|
15245
15587
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
15246
15588
|
function galleryMediaId(absolutePath) {
|
|
15247
|
-
return
|
|
15589
|
+
return createHash4("sha256").update(path32.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
15248
15590
|
}
|
|
15249
15591
|
function galleryMediaRoute(id) {
|
|
15250
15592
|
return `/media/${id}`;
|
|
@@ -15263,9 +15605,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
15263
15605
|
return (entry.history ?? []).map((generation, i) => {
|
|
15264
15606
|
const outputs = generation.outputs.map((output) => {
|
|
15265
15607
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
15266
|
-
const file =
|
|
15608
|
+
const file = path32.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
15267
15609
|
let url = null;
|
|
15268
|
-
if (
|
|
15610
|
+
if (existsSync22(file)) {
|
|
15269
15611
|
const id = galleryMediaId(file);
|
|
15270
15612
|
media.set(id, { path: file, contentType: mediaType });
|
|
15271
15613
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -15297,7 +15639,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
15297
15639
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
15298
15640
|
async function samePixels(a, b) {
|
|
15299
15641
|
try {
|
|
15300
|
-
const [left, right] = await Promise.all([
|
|
15642
|
+
const [left, right] = await Promise.all([readFile20(a), readFile20(b)]);
|
|
15301
15643
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
15302
15644
|
const x = decodePng(left);
|
|
15303
15645
|
const y = decodePng(right);
|
|
@@ -15395,10 +15737,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
15395
15737
|
async function describeQuality(media, root, inspection) {
|
|
15396
15738
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
15397
15739
|
const outputs = await Promise.all(
|
|
15398
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
15740
|
+
outputPaths.map((file) => describeOutput(media, root, path32.resolve(root, file)))
|
|
15399
15741
|
);
|
|
15400
|
-
const recordPath =
|
|
15401
|
-
const recordExists =
|
|
15742
|
+
const recordPath = path32.resolve(root, inspection.record);
|
|
15743
|
+
const recordExists = existsSync22(recordPath);
|
|
15402
15744
|
let options = null;
|
|
15403
15745
|
let check = null;
|
|
15404
15746
|
if (recordExists) {
|
|
@@ -15417,7 +15759,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
15417
15759
|
return {
|
|
15418
15760
|
state: inspection.state,
|
|
15419
15761
|
reason: inspection.reason,
|
|
15420
|
-
output: portableOutputPath(
|
|
15762
|
+
output: portableOutputPath(path32.resolve(root, inspection.output), root),
|
|
15421
15763
|
record: portableOutputPath(recordPath, root),
|
|
15422
15764
|
recordExists,
|
|
15423
15765
|
outputs,
|
|
@@ -15455,9 +15797,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
15455
15797
|
const media = /* @__PURE__ */ new Map();
|
|
15456
15798
|
const plan = await buildPlan(specs, lock);
|
|
15457
15799
|
const items = [];
|
|
15458
|
-
const manifestText = await
|
|
15800
|
+
const manifestText = await readFile20(loaded.path, "utf8");
|
|
15459
15801
|
const manifestSha256 = sha256(manifestText);
|
|
15460
|
-
const cacheDir =
|
|
15802
|
+
const cacheDir = path32.resolve(path32.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
15461
15803
|
let rawStyles = {};
|
|
15462
15804
|
try {
|
|
15463
15805
|
const raw = JSON.parse(manifestText);
|
|
@@ -15481,12 +15823,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
15481
15823
|
)
|
|
15482
15824
|
);
|
|
15483
15825
|
if (spec.source) {
|
|
15484
|
-
const editPath =
|
|
15826
|
+
const editPath = path32.resolve(root, spec.source);
|
|
15485
15827
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
15486
15828
|
const editShas = await Promise.all(
|
|
15487
15829
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
15488
15830
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
15489
|
-
return { file, sha256:
|
|
15831
|
+
return { file, sha256: existsSync22(file) ? await sha256File(file) : null };
|
|
15490
15832
|
})
|
|
15491
15833
|
);
|
|
15492
15834
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -15494,7 +15836,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15494
15836
|
const companion = await readHandEditCompanion(editPath);
|
|
15495
15837
|
if (companion) {
|
|
15496
15838
|
const projectPath = handEditProjectPath(editPath);
|
|
15497
|
-
const project = companion.project &&
|
|
15839
|
+
const project = companion.project && existsSync22(projectPath) ? await fileInfo(projectPath) : null;
|
|
15498
15840
|
let projectUrl = null;
|
|
15499
15841
|
if (project) {
|
|
15500
15842
|
const id = galleryMediaId(projectPath);
|
|
@@ -15530,8 +15872,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
15530
15872
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
15531
15873
|
}
|
|
15532
15874
|
} else if (spec.source) {
|
|
15533
|
-
outputs = [await describeOutput(media, root,
|
|
15534
|
-
} else if (!entry &&
|
|
15875
|
+
outputs = [await describeOutput(media, root, path32.resolve(root, spec.source))];
|
|
15876
|
+
} else if (!entry && existsSync22(spec.outFile)) {
|
|
15535
15877
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
15536
15878
|
} else {
|
|
15537
15879
|
outputs = [];
|
|
@@ -15734,7 +16076,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15734
16076
|
id: loaded.manifest.name,
|
|
15735
16077
|
name: loaded.manifest.name,
|
|
15736
16078
|
manifest: loaded.path,
|
|
15737
|
-
lock:
|
|
16079
|
+
lock: path32.resolve(opts.lockPath),
|
|
15738
16080
|
root,
|
|
15739
16081
|
provider: loaded.manifest.provider,
|
|
15740
16082
|
account: null,
|
|
@@ -15760,7 +16102,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15760
16102
|
return { snapshot, media };
|
|
15761
16103
|
}
|
|
15762
16104
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
15763
|
-
const dir =
|
|
16105
|
+
const dir = path32.dirname(path32.resolve(opts.workspacePath));
|
|
15764
16106
|
const media = /* @__PURE__ */ new Map();
|
|
15765
16107
|
const projects = [];
|
|
15766
16108
|
const items = [];
|
|
@@ -15803,11 +16145,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15803
16145
|
id: project.id,
|
|
15804
16146
|
name: loaded.manifest.name,
|
|
15805
16147
|
manifest: manifestPath,
|
|
15806
|
-
lock:
|
|
16148
|
+
lock: path32.resolve(lockPath),
|
|
15807
16149
|
root: loaded.root,
|
|
15808
16150
|
provider: loaded.manifest.provider,
|
|
15809
16151
|
account: project.account ?? null,
|
|
15810
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
16152
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile20(manifestPath, "utf8")),
|
|
15811
16153
|
historyLimit: historyLimit(loaded.manifest),
|
|
15812
16154
|
entries: Object.keys(lock.entries).length,
|
|
15813
16155
|
items: projectItems.length,
|
|
@@ -15819,8 +16161,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15819
16161
|
id: project.id,
|
|
15820
16162
|
name: project.id,
|
|
15821
16163
|
manifest: manifestPath,
|
|
15822
|
-
lock:
|
|
15823
|
-
root:
|
|
16164
|
+
lock: path32.resolve(lockPath),
|
|
16165
|
+
root: path32.dirname(manifestPath),
|
|
15824
16166
|
provider: project.provider,
|
|
15825
16167
|
account: project.account ?? null,
|
|
15826
16168
|
manifestSha256: null,
|
|
@@ -15847,7 +16189,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15847
16189
|
version: 1,
|
|
15848
16190
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
15849
16191
|
project: null,
|
|
15850
|
-
workspace: { path:
|
|
16192
|
+
workspace: { path: path32.resolve(opts.workspacePath), dir, projects },
|
|
15851
16193
|
filter,
|
|
15852
16194
|
totals: {
|
|
15853
16195
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -15866,10 +16208,10 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
15866
16208
|
// src/gallery/server.ts
|
|
15867
16209
|
import { randomBytes } from "crypto";
|
|
15868
16210
|
import { createServer as createServer2 } from "http";
|
|
15869
|
-
import { readFile as
|
|
16211
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
15870
16212
|
|
|
15871
16213
|
// src/gallery/page.ts
|
|
15872
|
-
import { existsSync as
|
|
16214
|
+
import { existsSync as existsSync23, readFileSync as readFileSync5 } from "fs";
|
|
15873
16215
|
|
|
15874
16216
|
// src/gallery/client-bundle.ts
|
|
15875
16217
|
import { createRequire } from "module";
|
|
@@ -15909,8 +16251,8 @@ var client;
|
|
|
15909
16251
|
function clientAssets() {
|
|
15910
16252
|
const prebuilt = new URL("gallery.js", CLIENT_DIR);
|
|
15911
16253
|
client ??= {
|
|
15912
|
-
css:
|
|
15913
|
-
js:
|
|
16254
|
+
css: readFileSync5(new URL("gallery.css", CLIENT_DIR), "utf8"),
|
|
16255
|
+
js: existsSync23(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
|
|
15914
16256
|
};
|
|
15915
16257
|
return client;
|
|
15916
16258
|
}
|
|
@@ -16181,7 +16523,7 @@ async function serveGallery(opts) {
|
|
|
16181
16523
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
16182
16524
|
if (!asset) return fail(404, "no such review asset");
|
|
16183
16525
|
try {
|
|
16184
|
-
const bytes = await
|
|
16526
|
+
const bytes = await readFile21(asset.path);
|
|
16185
16527
|
res.writeHead(200, {
|
|
16186
16528
|
"Content-Type": asset.contentType,
|
|
16187
16529
|
"Cache-Control": "no-store",
|
|
@@ -16264,7 +16606,7 @@ async function serveGallery(opts) {
|
|
|
16264
16606
|
if (!asset) return fail(404, "no such gallery media");
|
|
16265
16607
|
let bytes;
|
|
16266
16608
|
try {
|
|
16267
|
-
bytes = await
|
|
16609
|
+
bytes = await readFile21(asset.path);
|
|
16268
16610
|
} catch {
|
|
16269
16611
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
16270
16612
|
}
|
|
@@ -16308,7 +16650,7 @@ async function serveGallery(opts) {
|
|
|
16308
16650
|
|
|
16309
16651
|
// src/gallery/generate.ts
|
|
16310
16652
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
16311
|
-
import { z as
|
|
16653
|
+
import { z as z12 } from "zod";
|
|
16312
16654
|
|
|
16313
16655
|
// src/pipeline/poll.ts
|
|
16314
16656
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -16592,6 +16934,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
16592
16934
|
...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
|
|
16593
16935
|
...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
|
|
16594
16936
|
...spec.revision.description ? { description: spec.revision.description } : {},
|
|
16937
|
+
...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
|
|
16938
|
+
...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
|
|
16939
|
+
...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
|
|
16940
|
+
...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
|
|
16595
16941
|
...spec.revision.engine ? { engine: spec.revision.engine } : {}
|
|
16596
16942
|
} : null,
|
|
16597
16943
|
outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
|
|
@@ -16719,13 +17065,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
|
|
|
16719
17065
|
}
|
|
16720
17066
|
|
|
16721
17067
|
// src/pick/server.ts
|
|
16722
|
-
import { existsSync as
|
|
16723
|
-
import { readFile as
|
|
17068
|
+
import { existsSync as existsSync25 } from "fs";
|
|
17069
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
16724
17070
|
|
|
16725
17071
|
// src/pipeline/init.ts
|
|
16726
|
-
import { readdir, readFile as
|
|
16727
|
-
import { existsSync as
|
|
16728
|
-
import
|
|
17072
|
+
import { readdir, readFile as readFile22, writeFile as writeFile16 } from "fs/promises";
|
|
17073
|
+
import { existsSync as existsSync24 } from "fs";
|
|
17074
|
+
import path33 from "path";
|
|
16729
17075
|
function pngSize(buf) {
|
|
16730
17076
|
if (buf.length < 24) return null;
|
|
16731
17077
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -16739,7 +17085,7 @@ function slugify(value) {
|
|
|
16739
17085
|
async function walk(dir, exclude) {
|
|
16740
17086
|
const out = [];
|
|
16741
17087
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
16742
|
-
const full =
|
|
17088
|
+
const full = path33.join(dir, entry.name);
|
|
16743
17089
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
16744
17090
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
16745
17091
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -16753,14 +17099,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
16753
17099
|
const skipped = [];
|
|
16754
17100
|
const seen = /* @__PURE__ */ new Map();
|
|
16755
17101
|
for (const file of files.sort()) {
|
|
16756
|
-
const size = pngSize(await
|
|
17102
|
+
const size = pngSize(await readFile22(file));
|
|
16757
17103
|
if (!size) {
|
|
16758
17104
|
skipped.push(`${file} (not a readable PNG)`);
|
|
16759
17105
|
continue;
|
|
16760
17106
|
}
|
|
16761
|
-
const rel =
|
|
16762
|
-
const category =
|
|
16763
|
-
let id = slugify(
|
|
17107
|
+
const rel = path33.relative(root, file);
|
|
17108
|
+
const category = path33.dirname(rel) === "." ? "" : path33.dirname(rel);
|
|
17109
|
+
let id = slugify(path33.basename(rel));
|
|
16764
17110
|
if (seen.has(id)) {
|
|
16765
17111
|
const n = seen.get(id) + 1;
|
|
16766
17112
|
seen.set(id, n);
|
|
@@ -16818,10 +17164,10 @@ function mode(values) {
|
|
|
16818
17164
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
16819
17165
|
}
|
|
16820
17166
|
async function writeManifestFile(target, manifest) {
|
|
16821
|
-
if (
|
|
17167
|
+
if (existsSync24(target)) {
|
|
16822
17168
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
16823
17169
|
}
|
|
16824
|
-
await
|
|
17170
|
+
await writeFile16(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
16825
17171
|
}
|
|
16826
17172
|
|
|
16827
17173
|
// src/pick/sheet.ts
|
|
@@ -17234,13 +17580,13 @@ async function currentArt(entry, spec) {
|
|
|
17234
17580
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
17235
17581
|
if (!previous) return null;
|
|
17236
17582
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
17237
|
-
if (!
|
|
17583
|
+
if (!existsSync25(file)) return null;
|
|
17238
17584
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
17239
17585
|
if (!contentType) return null;
|
|
17240
17586
|
let width = entry.width;
|
|
17241
17587
|
let height = entry.height;
|
|
17242
17588
|
if (contentType === "image/png") {
|
|
17243
|
-
const size = pngSize(await
|
|
17589
|
+
const size = pngSize(await readFile23(file));
|
|
17244
17590
|
if (size) ({ width, height } = size);
|
|
17245
17591
|
}
|
|
17246
17592
|
return { path: file, contentType, width, height };
|
|
@@ -17408,26 +17754,26 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
17408
17754
|
}
|
|
17409
17755
|
|
|
17410
17756
|
// src/gallery/generate.ts
|
|
17411
|
-
var GenerateRequestSchema =
|
|
17412
|
-
project:
|
|
17413
|
-
keys:
|
|
17757
|
+
var GenerateRequestSchema = z12.object({
|
|
17758
|
+
project: z12.string().min(1).optional(),
|
|
17759
|
+
keys: z12.array(z12.string().min(1)).min(1).max(500),
|
|
17414
17760
|
/**
|
|
17415
17761
|
* Regenerate work that is up to date, as `gen --force` does, and let the
|
|
17416
17762
|
* fetch replace a locally modified or untracked file. With `restore`, put
|
|
17417
17763
|
* the recorded bytes back over a modified file.
|
|
17418
17764
|
*/
|
|
17419
|
-
force:
|
|
17765
|
+
force: z12.boolean().optional(),
|
|
17420
17766
|
/** Advance in-flight, review, or selected work without submitting anything. */
|
|
17421
|
-
resume:
|
|
17767
|
+
resume: z12.boolean().optional(),
|
|
17422
17768
|
/** Re-download downloaded outputs and replace files whose object changed upstream. */
|
|
17423
|
-
refresh:
|
|
17769
|
+
refresh: z12.boolean().optional(),
|
|
17424
17770
|
/** Put recorded outputs back on disk from the cache or the provider, without generating. */
|
|
17425
|
-
restore:
|
|
17771
|
+
restore: z12.boolean().optional(),
|
|
17426
17772
|
/**
|
|
17427
17773
|
* Bring a replaced generation back: 1-based position in the record's
|
|
17428
17774
|
* history (newest first) or an output hash prefix. One key at a time.
|
|
17429
17775
|
*/
|
|
17430
|
-
revert:
|
|
17776
|
+
revert: z12.string().min(1).max(64).optional()
|
|
17431
17777
|
}).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
|
|
17432
17778
|
message: "resume, refresh, restore, and revert are exclusive"
|
|
17433
17779
|
}).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
|
|
@@ -17773,15 +18119,15 @@ function createGenerateHandlers(opts) {
|
|
|
17773
18119
|
}
|
|
17774
18120
|
|
|
17775
18121
|
// src/gallery/editor.ts
|
|
17776
|
-
import { readFile as
|
|
17777
|
-
import
|
|
18122
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
18123
|
+
import path35 from "path";
|
|
17778
18124
|
|
|
17779
18125
|
// src/editor/install.ts
|
|
17780
|
-
import { createHash as
|
|
17781
|
-
import { existsSync as
|
|
17782
|
-
import { mkdir as mkdir12, readFile as
|
|
17783
|
-
import
|
|
17784
|
-
import
|
|
18126
|
+
import { createHash as createHash5 } from "crypto";
|
|
18127
|
+
import { existsSync as existsSync26 } from "fs";
|
|
18128
|
+
import { mkdir as mkdir12, readFile as readFile24, rename as rename10, rm as rm11, stat as stat3, writeFile as writeFile17 } from "fs/promises";
|
|
18129
|
+
import os3 from "os";
|
|
18130
|
+
import path34 from "path";
|
|
17785
18131
|
|
|
17786
18132
|
// tools/pixelorama-bridge/pin.json
|
|
17787
18133
|
var pin_default = {
|
|
@@ -17845,15 +18191,15 @@ var EditorInstallError = class extends Error {
|
|
|
17845
18191
|
};
|
|
17846
18192
|
function editorToolsRoot() {
|
|
17847
18193
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
17848
|
-
if (override) return
|
|
17849
|
-
if (process.platform === "darwin") return
|
|
18194
|
+
if (override) return path34.resolve(override);
|
|
18195
|
+
if (process.platform === "darwin") return path34.join(os3.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
17850
18196
|
if (process.platform === "win32") {
|
|
17851
|
-
return
|
|
18197
|
+
return path34.join(process.env.LOCALAPPDATA ?? path34.join(os3.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
17852
18198
|
}
|
|
17853
|
-
return
|
|
18199
|
+
return path34.join(process.env.XDG_CACHE_HOME ?? path34.join(os3.homedir(), ".cache"), "pixelkiln", "tools");
|
|
17854
18200
|
}
|
|
17855
18201
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
17856
|
-
return
|
|
18202
|
+
return path34.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
17857
18203
|
}
|
|
17858
18204
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
17859
18205
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -17863,7 +18209,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
17863
18209
|
}
|
|
17864
18210
|
async function sha256Of(file) {
|
|
17865
18211
|
try {
|
|
17866
|
-
return
|
|
18212
|
+
return createHash5("sha256").update(await readFile24(file)).digest("hex");
|
|
17867
18213
|
} catch {
|
|
17868
18214
|
return null;
|
|
17869
18215
|
}
|
|
@@ -17876,8 +18222,8 @@ async function editorStatus(opts = {}) {
|
|
|
17876
18222
|
let totalBytes = 0;
|
|
17877
18223
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
17878
18224
|
totalBytes += expected.bytes;
|
|
17879
|
-
const file =
|
|
17880
|
-
const info =
|
|
18225
|
+
const file = path34.join(dir, name);
|
|
18226
|
+
const info = existsSync26(file) ? await stat3(file).catch(() => null) : null;
|
|
17881
18227
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
17882
18228
|
installedBytes += expected.bytes;
|
|
17883
18229
|
} else {
|
|
@@ -17930,17 +18276,17 @@ async function installEditor(opts = {}) {
|
|
|
17930
18276
|
if (bytes.length !== expected.bytes) {
|
|
17931
18277
|
throw new EditorInstallError(`${name}: expected ${expected.bytes} bytes, got ${bytes.length}`, name);
|
|
17932
18278
|
}
|
|
17933
|
-
const digest2 =
|
|
18279
|
+
const digest2 = createHash5("sha256").update(bytes).digest("hex");
|
|
17934
18280
|
if (digest2 !== expected.sha256) {
|
|
17935
18281
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
17936
18282
|
}
|
|
17937
|
-
const final =
|
|
18283
|
+
const final = path34.join(dir, name);
|
|
17938
18284
|
const tmp = `${final}.${process.pid}.part`;
|
|
17939
18285
|
try {
|
|
17940
|
-
await
|
|
18286
|
+
await writeFile17(tmp, bytes);
|
|
17941
18287
|
await rename10(tmp, final);
|
|
17942
18288
|
} finally {
|
|
17943
|
-
await
|
|
18289
|
+
await rm11(tmp, { force: true });
|
|
17944
18290
|
}
|
|
17945
18291
|
fetchedBytes += bytes.length;
|
|
17946
18292
|
downloaded.push(name);
|
|
@@ -17980,7 +18326,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
17980
18326
|
".json": "application/json; charset=utf-8"
|
|
17981
18327
|
};
|
|
17982
18328
|
function editorContentType(name) {
|
|
17983
|
-
return EDITOR_CONTENT_TYPES[
|
|
18329
|
+
return EDITOR_CONTENT_TYPES[path34.extname(name).toLowerCase()] ?? null;
|
|
17984
18330
|
}
|
|
17985
18331
|
|
|
17986
18332
|
// src/gallery/editor.ts
|
|
@@ -18056,7 +18402,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
18056
18402
|
if (!status.installed) return null;
|
|
18057
18403
|
let bytes;
|
|
18058
18404
|
try {
|
|
18059
|
-
bytes = await
|
|
18405
|
+
bytes = await readFile25(path35.join(dir, name));
|
|
18060
18406
|
} catch {
|
|
18061
18407
|
verified = null;
|
|
18062
18408
|
return null;
|
|
@@ -18096,7 +18442,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18096
18442
|
};
|
|
18097
18443
|
const loadProject = async (project) => {
|
|
18098
18444
|
const ctx = await access2.loadProject(project);
|
|
18099
|
-
loadProjectEnv(
|
|
18445
|
+
loadProjectEnv(path36.dirname(ctx.loaded.path));
|
|
18100
18446
|
return ctx;
|
|
18101
18447
|
};
|
|
18102
18448
|
const loadProjectEnv = (dir) => {
|
|
@@ -18105,7 +18451,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18105
18451
|
for (const [name, value] of Object.entries(declared)) {
|
|
18106
18452
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
18107
18453
|
throw new Error(
|
|
18108
|
-
`${
|
|
18454
|
+
`${path36.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
|
|
18109
18455
|
);
|
|
18110
18456
|
}
|
|
18111
18457
|
}
|
|
@@ -18113,7 +18459,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18113
18459
|
};
|
|
18114
18460
|
const quoters = /* @__PURE__ */ new Map();
|
|
18115
18461
|
const quoteFor = async (project) => {
|
|
18116
|
-
loadProjectEnv(
|
|
18462
|
+
loadProjectEnv(path36.dirname(path36.resolve(await access2.manifestFor(project))));
|
|
18117
18463
|
const key = process.env.PIXELLAB_API_KEY;
|
|
18118
18464
|
if (!key) return null;
|
|
18119
18465
|
let quoter = quoters.get(key);
|
|
@@ -18164,8 +18510,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18164
18510
|
}
|
|
18165
18511
|
async function runGallery(args) {
|
|
18166
18512
|
if (args.workspace) {
|
|
18167
|
-
const workspacePath =
|
|
18168
|
-
if (!
|
|
18513
|
+
const workspacePath = path36.resolve(args.workspace);
|
|
18514
|
+
if (!existsSync27(workspacePath)) {
|
|
18169
18515
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
18170
18516
|
}
|
|
18171
18517
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -18185,7 +18531,7 @@ async function runGallery(args) {
|
|
|
18185
18531
|
const registered = async (projectId) => {
|
|
18186
18532
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
18187
18533
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
18188
|
-
return resolveProject(
|
|
18534
|
+
return resolveProject(path36.dirname(workspacePath), project);
|
|
18189
18535
|
};
|
|
18190
18536
|
await serveUntilStopped(initial, build2, args, {
|
|
18191
18537
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -18210,7 +18556,7 @@ async function runGallery(args) {
|
|
|
18210
18556
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
18211
18557
|
};
|
|
18212
18558
|
await serveUntilStopped(await build(), reload, args, {
|
|
18213
|
-
manifestFor: () =>
|
|
18559
|
+
manifestFor: () => path36.resolve(args.manifest),
|
|
18214
18560
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
18215
18561
|
});
|
|
18216
18562
|
}
|
|
@@ -18519,18 +18865,18 @@ async function runRestoreGeneration(args) {
|
|
|
18519
18865
|
}
|
|
18520
18866
|
|
|
18521
18867
|
// src/cli/commands/housekeeping.ts
|
|
18522
|
-
import { existsSync as
|
|
18868
|
+
import { existsSync as existsSync29 } from "fs";
|
|
18523
18869
|
|
|
18524
18870
|
// src/pipeline/cache-health.ts
|
|
18525
|
-
import { existsSync as
|
|
18526
|
-
import { readFile as
|
|
18527
|
-
import
|
|
18871
|
+
import { existsSync as existsSync28 } from "fs";
|
|
18872
|
+
import { readFile as readFile26, readdir as readdir2, rm as rm12 } from "fs/promises";
|
|
18873
|
+
import path37 from "path";
|
|
18528
18874
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
18529
|
-
if (options.prune && !
|
|
18530
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
18875
|
+
if (options.prune && !existsSync28(lockPath)) {
|
|
18876
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path37.resolve(lockPath)}`);
|
|
18531
18877
|
}
|
|
18532
|
-
const contentDir =
|
|
18533
|
-
const remotePath =
|
|
18878
|
+
const contentDir = path37.resolve(path37.dirname(lockPath), ".pixelkiln", "cache");
|
|
18879
|
+
const remotePath = path37.resolve(cachePathFor(lockPath));
|
|
18534
18880
|
const referenced = referencedHashes(lock);
|
|
18535
18881
|
let content = await inspectContentCache(contentDir, referenced);
|
|
18536
18882
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -18542,7 +18888,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18542
18888
|
]);
|
|
18543
18889
|
for (const name of names) {
|
|
18544
18890
|
try {
|
|
18545
|
-
await
|
|
18891
|
+
await rm12(path37.join(contentDir, name), { force: true });
|
|
18546
18892
|
removed.contentFiles++;
|
|
18547
18893
|
} catch {
|
|
18548
18894
|
}
|
|
@@ -18551,7 +18897,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18551
18897
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
18552
18898
|
removed.resetRemoteHashCache = true;
|
|
18553
18899
|
} else if (remoteHashes.invalidIds.length) {
|
|
18554
|
-
const cache = parseCache(JSON.parse(await
|
|
18900
|
+
const cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
|
|
18555
18901
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
18556
18902
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
18557
18903
|
await saveCache(remotePath, cache);
|
|
@@ -18569,7 +18915,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
18569
18915
|
async function inspectContentCache(contentDir, referenced) {
|
|
18570
18916
|
const report = {
|
|
18571
18917
|
path: contentDir,
|
|
18572
|
-
exists:
|
|
18918
|
+
exists: existsSync28(contentDir),
|
|
18573
18919
|
files: 0,
|
|
18574
18920
|
bytes: 0,
|
|
18575
18921
|
valid: 0,
|
|
@@ -18590,12 +18936,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
18590
18936
|
continue;
|
|
18591
18937
|
}
|
|
18592
18938
|
report.files++;
|
|
18593
|
-
const file =
|
|
18939
|
+
const file = path37.join(contentDir, entry.name);
|
|
18594
18940
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
18595
18941
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
18596
18942
|
let bytes;
|
|
18597
18943
|
try {
|
|
18598
|
-
bytes = await
|
|
18944
|
+
bytes = await readFile26(file);
|
|
18599
18945
|
report.bytes += bytes.length;
|
|
18600
18946
|
} catch (err) {
|
|
18601
18947
|
report.invalid.push({
|
|
@@ -18633,7 +18979,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
18633
18979
|
async function inspectRemoteHashCache(remotePath) {
|
|
18634
18980
|
const report = {
|
|
18635
18981
|
path: remotePath,
|
|
18636
|
-
exists:
|
|
18982
|
+
exists: existsSync28(remotePath),
|
|
18637
18983
|
entries: 0,
|
|
18638
18984
|
valid: 0,
|
|
18639
18985
|
invalidIds: [],
|
|
@@ -18642,7 +18988,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
18642
18988
|
if (!report.exists) return report;
|
|
18643
18989
|
let cache;
|
|
18644
18990
|
try {
|
|
18645
|
-
cache = parseCache(JSON.parse(await
|
|
18991
|
+
cache = parseCache(JSON.parse(await readFile26(remotePath, "utf8")));
|
|
18646
18992
|
} catch (err) {
|
|
18647
18993
|
report.error = err instanceof Error ? err.message : String(err);
|
|
18648
18994
|
return report;
|
|
@@ -18706,7 +19052,7 @@ async function runAccept(args) {
|
|
|
18706
19052
|
let intact = true;
|
|
18707
19053
|
for (const output of entry.outputs) {
|
|
18708
19054
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
18709
|
-
if (!
|
|
19055
|
+
if (!existsSync29(file) || await sha256File(file) !== output.sha256) {
|
|
18710
19056
|
intact = false;
|
|
18711
19057
|
break;
|
|
18712
19058
|
}
|
|
@@ -18761,42 +19107,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
18761
19107
|
}
|
|
18762
19108
|
|
|
18763
19109
|
// src/cli/commands/init.ts
|
|
18764
|
-
import
|
|
18765
|
-
import { existsSync as
|
|
19110
|
+
import path38 from "path";
|
|
19111
|
+
import { existsSync as existsSync30 } from "fs";
|
|
18766
19112
|
async function runInit(args) {
|
|
18767
19113
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
18768
|
-
const root =
|
|
18769
|
-
if (!
|
|
19114
|
+
const root = path38.resolve(args.from);
|
|
19115
|
+
if (!existsSync30(root)) throw new Error(`No directory at ${root}`);
|
|
18770
19116
|
const generator = args.generator ?? "map";
|
|
18771
19117
|
if (generator !== "1dir" && generator !== "map") {
|
|
18772
19118
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
18773
19119
|
}
|
|
18774
|
-
const target =
|
|
19120
|
+
const target = path38.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
18775
19121
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
18776
19122
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
18777
19123
|
const manifest = buildManifest(
|
|
18778
|
-
args.name ??
|
|
19124
|
+
args.name ?? path38.basename(path38.dirname(target)),
|
|
18779
19125
|
args.styles[0] ?? "base",
|
|
18780
19126
|
generator,
|
|
18781
|
-
|
|
19127
|
+
path38.relative(path38.dirname(target), root) || ".",
|
|
18782
19128
|
assets
|
|
18783
19129
|
);
|
|
18784
19130
|
await writeManifestFile(target, manifest);
|
|
18785
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
19131
|
+
log(` scanned ${assets.length} PNG(s) under ${path38.relative(process.cwd(), root)}`);
|
|
18786
19132
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
18787
|
-
log(` wrote ${
|
|
19133
|
+
log(` wrote ${path38.relative(process.cwd(), target)}`);
|
|
18788
19134
|
log(`
|
|
18789
19135
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
18790
19136
|
log(` PixelLab account instead of inventing them:`);
|
|
18791
19137
|
log(`
|
|
18792
|
-
pixelkiln adopt --manifest ${
|
|
19138
|
+
pixelkiln adopt --manifest ${path38.relative(process.cwd(), target)} --write-prompts
|
|
18793
19139
|
`);
|
|
18794
19140
|
}
|
|
18795
19141
|
|
|
18796
19142
|
// src/pipeline/doctor.ts
|
|
18797
|
-
import { constants, existsSync as
|
|
19143
|
+
import { constants, existsSync as existsSync31 } from "fs";
|
|
18798
19144
|
import { access, stat as stat4 } from "fs/promises";
|
|
18799
|
-
import
|
|
19145
|
+
import path39 from "path";
|
|
18800
19146
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
18801
19147
|
const checks = [];
|
|
18802
19148
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -18807,8 +19153,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18807
19153
|
);
|
|
18808
19154
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
18809
19155
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
18810
|
-
|
|
18811
|
-
...spec.quality ? [
|
|
19156
|
+
path39.dirname(spec.outFile),
|
|
19157
|
+
...spec.quality ? [path39.dirname(spec.quality.outFile)] : []
|
|
18812
19158
|
]))];
|
|
18813
19159
|
const unwritable = [];
|
|
18814
19160
|
for (const dir of dirs) {
|
|
@@ -18831,7 +19177,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18831
19177
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
18832
19178
|
const output = entry.outputs[index];
|
|
18833
19179
|
const spec = specByKey.get(key);
|
|
18834
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
19180
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path39.dirname(lockPath));
|
|
18835
19181
|
const owner = outputOwners.get(absolute);
|
|
18836
19182
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
18837
19183
|
outputOwners.set(absolute, key);
|
|
@@ -18846,7 +19192,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18846
19192
|
} else {
|
|
18847
19193
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
18848
19194
|
}
|
|
18849
|
-
const cacheDir =
|
|
19195
|
+
const cacheDir = path39.join(path39.dirname(lockPath), ".pixelkiln", "cache");
|
|
18850
19196
|
const stranded = [];
|
|
18851
19197
|
const invalidState = [];
|
|
18852
19198
|
const staleMapUrls = [];
|
|
@@ -18862,11 +19208,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
18862
19208
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
18863
19209
|
let hasCache = false;
|
|
18864
19210
|
for (const output of entry.outputs) {
|
|
18865
|
-
const cached =
|
|
19211
|
+
const cached = path39.join(
|
|
18866
19212
|
cacheDir,
|
|
18867
19213
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
18868
19214
|
);
|
|
18869
|
-
if (
|
|
19215
|
+
if (existsSync31(cached) && await sha256File(cached) === output.sha256) {
|
|
18870
19216
|
hasCache = true;
|
|
18871
19217
|
break;
|
|
18872
19218
|
}
|
|
@@ -18962,16 +19308,16 @@ async function nearestExistingDirectory(start) {
|
|
|
18962
19308
|
if ((await stat4(current)).isDirectory()) return current;
|
|
18963
19309
|
} catch {
|
|
18964
19310
|
}
|
|
18965
|
-
const parent =
|
|
19311
|
+
const parent = path39.dirname(current);
|
|
18966
19312
|
if (parent === current) return current;
|
|
18967
19313
|
current = parent;
|
|
18968
19314
|
}
|
|
18969
19315
|
}
|
|
18970
19316
|
|
|
18971
19317
|
// src/pipeline/audit.ts
|
|
18972
|
-
import { readFile as
|
|
18973
|
-
import { existsSync as
|
|
18974
|
-
import
|
|
19318
|
+
import { readFile as readFile27 } from "fs/promises";
|
|
19319
|
+
import { existsSync as existsSync32 } from "fs";
|
|
19320
|
+
import path40 from "path";
|
|
18975
19321
|
function paletteDistance(asset, reference) {
|
|
18976
19322
|
if (!asset.length || !reference.length) return 0;
|
|
18977
19323
|
let total = 0;
|
|
@@ -19007,12 +19353,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
19007
19353
|
const unreadable = [];
|
|
19008
19354
|
for (const spec of mine) {
|
|
19009
19355
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
19010
|
-
if (!
|
|
19356
|
+
if (!existsSync32(output.absolutePath)) {
|
|
19011
19357
|
missing.push(output.id);
|
|
19012
19358
|
continue;
|
|
19013
19359
|
}
|
|
19014
19360
|
try {
|
|
19015
|
-
const png = decodePng(await
|
|
19361
|
+
const png = decodePng(await readFile27(output.absolutePath));
|
|
19016
19362
|
const palette = extractPalette(png, 12);
|
|
19017
19363
|
assets.push({
|
|
19018
19364
|
assetId: spec.assetId,
|
|
@@ -19036,10 +19382,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
19036
19382
|
let referenceFromStyleImages = false;
|
|
19037
19383
|
const refPalettes = [];
|
|
19038
19384
|
for (const img of style.styleImages) {
|
|
19039
|
-
const abs =
|
|
19040
|
-
if (!
|
|
19385
|
+
const abs = path40.resolve(loaded.root, img.path);
|
|
19386
|
+
if (!existsSync32(abs)) continue;
|
|
19041
19387
|
try {
|
|
19042
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
19388
|
+
refPalettes.push(extractPalette(decodePng(await readFile27(abs)), 12));
|
|
19043
19389
|
} catch {
|
|
19044
19390
|
}
|
|
19045
19391
|
}
|
|
@@ -19267,24 +19613,24 @@ async function runAudit(args) {
|
|
|
19267
19613
|
}
|
|
19268
19614
|
|
|
19269
19615
|
// src/cli/commands/pack.ts
|
|
19270
|
-
import
|
|
19271
|
-
import { readFile as
|
|
19616
|
+
import path42 from "path";
|
|
19617
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
19272
19618
|
|
|
19273
19619
|
// src/pipeline/pack.ts
|
|
19274
|
-
import { readFileSync as
|
|
19275
|
-
import
|
|
19620
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
19621
|
+
import path41 from "path";
|
|
19276
19622
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
19277
19623
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
19278
19624
|
if (!Array.isArray(raw) || !raw.length) {
|
|
19279
19625
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
19280
19626
|
}
|
|
19281
|
-
const baseDir =
|
|
19627
|
+
const baseDir = path41.dirname(path41.resolve(inputsFilePath));
|
|
19282
19628
|
return raw.map((entry, i) => {
|
|
19283
19629
|
const e = entry;
|
|
19284
19630
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
19285
19631
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
19286
19632
|
}
|
|
19287
|
-
return { id: e.id, path:
|
|
19633
|
+
return { id: e.id, path: path41.resolve(baseDir, e.path) };
|
|
19288
19634
|
});
|
|
19289
19635
|
}
|
|
19290
19636
|
function packSprites(inputs, options = {}) {
|
|
@@ -19305,7 +19651,7 @@ function packSprites(inputs, options = {}) {
|
|
|
19305
19651
|
for (const input of ordered) {
|
|
19306
19652
|
let sourceHash = null;
|
|
19307
19653
|
try {
|
|
19308
|
-
const bytes =
|
|
19654
|
+
const bytes = readFileSync6(input.path);
|
|
19309
19655
|
sourceHash = sha256(bytes);
|
|
19310
19656
|
const png = decodePng(bytes);
|
|
19311
19657
|
sprites.push({ id: input.id, width: png.width, height: png.height, pixels: png.pixels });
|
|
@@ -19368,7 +19714,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19368
19714
|
}
|
|
19369
19715
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
19370
19716
|
id,
|
|
19371
|
-
path:
|
|
19717
|
+
path: path41.resolve(source)
|
|
19372
19718
|
}));
|
|
19373
19719
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
19374
19720
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -19392,7 +19738,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19392
19738
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
19393
19739
|
const source = sources[id];
|
|
19394
19740
|
if (source && outputs.length <= 1) {
|
|
19395
|
-
inputs.push({ id, path:
|
|
19741
|
+
inputs.push({ id, path: path41.resolve(manifestDir, source) });
|
|
19396
19742
|
continue;
|
|
19397
19743
|
}
|
|
19398
19744
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -19426,7 +19772,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19426
19772
|
}
|
|
19427
19773
|
}
|
|
19428
19774
|
for (const [id, source] of Object.entries(sources)) {
|
|
19429
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
19775
|
+
if (!locked.has(id)) inputs.push({ id, path: path41.resolve(manifestDir, source) });
|
|
19430
19776
|
}
|
|
19431
19777
|
if (!inputs.length) {
|
|
19432
19778
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -19463,7 +19809,7 @@ function mountSprites(placements, options) {
|
|
|
19463
19809
|
for (const p of placements) {
|
|
19464
19810
|
let sourceHash = null;
|
|
19465
19811
|
try {
|
|
19466
|
-
const bytes =
|
|
19812
|
+
const bytes = readFileSync6(p.path);
|
|
19467
19813
|
sourceHash = sha256(bytes);
|
|
19468
19814
|
const png = decodePng(bytes);
|
|
19469
19815
|
if (png.width > cellWidth || png.height > cellHeight) {
|
|
@@ -19534,7 +19880,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19534
19880
|
const source = sources[id];
|
|
19535
19881
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
19536
19882
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
19537
|
-
placements.push({ id, path:
|
|
19883
|
+
placements.push({ id, path: path41.resolve(manifestDir, source), cell });
|
|
19538
19884
|
continue;
|
|
19539
19885
|
}
|
|
19540
19886
|
if (source && entry) {
|
|
@@ -19567,7 +19913,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19567
19913
|
`Nothing to mount for style "${styleId}": ${ids.length} asset(s) declare a cell, but none has generated output or a \`source\`. Run \`pixelkiln gen --style ${styleId}\`, or point each at a file.${detail}`
|
|
19568
19914
|
);
|
|
19569
19915
|
}
|
|
19570
|
-
const basePng = mount.base ?
|
|
19916
|
+
const basePng = mount.base ? readFileSync6(path41.resolve(manifestDir, mount.base)) : void 0;
|
|
19571
19917
|
const mounted = mountSprites(placements, {
|
|
19572
19918
|
cellWidth: mount.cellWidth,
|
|
19573
19919
|
cellHeight: mount.cellHeight,
|
|
@@ -19577,7 +19923,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19577
19923
|
...mounted,
|
|
19578
19924
|
sources: mount.base ? [{
|
|
19579
19925
|
id: "$base",
|
|
19580
|
-
path:
|
|
19926
|
+
path: path41.resolve(manifestDir, mount.base),
|
|
19581
19927
|
sha256: sha256(basePng),
|
|
19582
19928
|
included: true
|
|
19583
19929
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -19981,12 +20327,12 @@ async function runPack(args) {
|
|
|
19981
20327
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
19982
20328
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
19983
20329
|
}
|
|
19984
|
-
const raw = JSON.parse(await
|
|
20330
|
+
const raw = JSON.parse(await readFile28(path42.resolve(args.inputs), "utf8"));
|
|
19985
20331
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
19986
20332
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
19987
20333
|
const format = sheetFormat(args);
|
|
19988
|
-
const base =
|
|
19989
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
20334
|
+
const base = path42.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
20335
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path42.basename(`${base}.png`) });
|
|
19990
20336
|
const outputs = [
|
|
19991
20337
|
{ path: `${base}.png`, data: png },
|
|
19992
20338
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -19999,7 +20345,7 @@ async function runPack(args) {
|
|
|
19999
20345
|
log(
|
|
20000
20346
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
|
|
20001
20347
|
);
|
|
20002
|
-
log(` ${
|
|
20348
|
+
log(` ${path42.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
20003
20349
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20004
20350
|
return;
|
|
20005
20351
|
}
|
|
@@ -20007,7 +20353,7 @@ async function runPack(args) {
|
|
|
20007
20353
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
20008
20354
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
20009
20355
|
}
|
|
20010
|
-
const manifestDir =
|
|
20356
|
+
const manifestDir = path42.dirname(path42.resolve(args.manifest));
|
|
20011
20357
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
20012
20358
|
for (const styleId of styleIds) {
|
|
20013
20359
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -20022,9 +20368,9 @@ async function runPack(args) {
|
|
|
20022
20368
|
sourceOverrides: qualitySources,
|
|
20023
20369
|
sources: manifestSources(loaded.manifest, styleId)
|
|
20024
20370
|
});
|
|
20025
|
-
const base = args.out ?
|
|
20371
|
+
const base = args.out ? path42.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path42.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
20026
20372
|
const format = sheetFormat(args);
|
|
20027
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
20373
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path42.basename(`${base}.png`) });
|
|
20028
20374
|
const outputs = [
|
|
20029
20375
|
{ path: `${base}.png`, data: png },
|
|
20030
20376
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -20056,13 +20402,13 @@ async function runPack(args) {
|
|
|
20056
20402
|
log(
|
|
20057
20403
|
` ${styleId}: ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)` + (atlas.sets?.length ? `, ${atlas.sets.length} set(s)` : "") + ` (${format})`
|
|
20058
20404
|
);
|
|
20059
|
-
log(` ${
|
|
20405
|
+
log(` ${path42.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
20060
20406
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20061
20407
|
}
|
|
20062
20408
|
}
|
|
20063
20409
|
async function runMount(args) {
|
|
20064
20410
|
const { loaded, lock } = await openProject2(args);
|
|
20065
|
-
const manifestDir =
|
|
20411
|
+
const manifestDir = path42.dirname(path42.resolve(args.manifest));
|
|
20066
20412
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
20067
20413
|
for (const styleId of styleIds) {
|
|
20068
20414
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -20101,9 +20447,9 @@ async function runMount(args) {
|
|
|
20101
20447
|
sources,
|
|
20102
20448
|
outputRoles
|
|
20103
20449
|
);
|
|
20104
|
-
const out =
|
|
20450
|
+
const out = path42.resolve(manifestDir, style.mount.out);
|
|
20105
20451
|
const format = sheetFormat(args);
|
|
20106
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
20452
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path42.basename(out) });
|
|
20107
20453
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
20108
20454
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
20109
20455
|
const outputs = [
|
|
@@ -20123,7 +20469,7 @@ async function runMount(args) {
|
|
|
20123
20469
|
await provenanceFile("$lock", args.lock),
|
|
20124
20470
|
...qualityRecords,
|
|
20125
20471
|
...artifactSources.filter(
|
|
20126
|
-
(source) => source.id !== "$base" ||
|
|
20472
|
+
(source) => source.id !== "$base" || path42.resolve(source.path) !== out
|
|
20127
20473
|
)
|
|
20128
20474
|
],
|
|
20129
20475
|
options: {
|
|
@@ -20137,14 +20483,14 @@ async function runMount(args) {
|
|
|
20137
20483
|
log(
|
|
20138
20484
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
20139
20485
|
);
|
|
20140
|
-
log(` ${
|
|
20486
|
+
log(` ${path42.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
20141
20487
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20142
20488
|
}
|
|
20143
20489
|
}
|
|
20144
20490
|
async function runExport(args) {
|
|
20145
20491
|
const { loaded, specs, lock } = await openProject2(args);
|
|
20146
20492
|
const format = args.format ?? "generic";
|
|
20147
|
-
const manifestDir =
|
|
20493
|
+
const manifestDir = path42.dirname(path42.resolve(args.manifest));
|
|
20148
20494
|
const selected = specs.filter((spec) => {
|
|
20149
20495
|
if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
|
|
20150
20496
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -20160,12 +20506,12 @@ async function runExport(args) {
|
|
|
20160
20506
|
for (const spec of selected) {
|
|
20161
20507
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
20162
20508
|
const style = loaded.manifest.styles[spec.styleId];
|
|
20163
|
-
const defaultBase =
|
|
20164
|
-
const base = args.out ?
|
|
20509
|
+
const defaultBase = path42.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
|
|
20510
|
+
const base = args.out ? path42.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
20165
20511
|
const result = exportTileset(entry, spec, {
|
|
20166
20512
|
format,
|
|
20167
20513
|
manifestDir,
|
|
20168
|
-
imageName:
|
|
20514
|
+
imageName: path42.basename(`${base}.png`),
|
|
20169
20515
|
columns: args.columns
|
|
20170
20516
|
});
|
|
20171
20517
|
const outputs = [
|
|
@@ -20183,7 +20529,7 @@ async function runExport(args) {
|
|
|
20183
20529
|
asset: spec.assetId,
|
|
20184
20530
|
columns: args.columns ?? null,
|
|
20185
20531
|
format,
|
|
20186
|
-
image:
|
|
20532
|
+
image: path42.basename(`${base}.png`),
|
|
20187
20533
|
providerRules: result.generic.providerRules,
|
|
20188
20534
|
style: spec.styleId,
|
|
20189
20535
|
tileType: spec.tileType ?? null
|
|
@@ -20193,7 +20539,7 @@ async function runExport(args) {
|
|
|
20193
20539
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
20194
20540
|
);
|
|
20195
20541
|
log(
|
|
20196
|
-
` ${
|
|
20542
|
+
` ${path42.relative(process.cwd(), base)}.png + ${path42.basename(base)}${result.extension} + .pixelkiln.json`
|
|
20197
20543
|
);
|
|
20198
20544
|
}
|
|
20199
20545
|
}
|
|
@@ -20206,23 +20552,23 @@ function sheetFormat(args) {
|
|
|
20206
20552
|
}
|
|
20207
20553
|
|
|
20208
20554
|
// src/cli/commands/pixellab-utils.ts
|
|
20209
|
-
import
|
|
20555
|
+
import path44 from "path";
|
|
20210
20556
|
|
|
20211
20557
|
// src/pixellab-utils.ts
|
|
20212
|
-
import { existsSync as
|
|
20213
|
-
import { mkdir as mkdir13, readFile as
|
|
20214
|
-
import
|
|
20558
|
+
import { existsSync as existsSync33 } from "fs";
|
|
20559
|
+
import { mkdir as mkdir13, readFile as readFile29, writeFile as writeFile18 } from "fs/promises";
|
|
20560
|
+
import path43 from "path";
|
|
20215
20561
|
var UNZOOM_MIN_SIDE = 256;
|
|
20216
20562
|
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
20217
20563
|
function defaultUnzoomOut(input) {
|
|
20218
|
-
const ext =
|
|
20564
|
+
const ext = path43.extname(input);
|
|
20219
20565
|
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
20220
20566
|
}
|
|
20221
20567
|
async function unzoomFile(client2, input, opts = {}) {
|
|
20222
|
-
const out =
|
|
20223
|
-
if (
|
|
20224
|
-
if (
|
|
20225
|
-
const bytes = await
|
|
20568
|
+
const out = path43.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
20569
|
+
if (path43.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
20570
|
+
if (existsSync33(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
20571
|
+
const bytes = await readFile29(input);
|
|
20226
20572
|
const metadata = imageMetadata(bytes);
|
|
20227
20573
|
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
20228
20574
|
const { width, height, format } = metadata;
|
|
@@ -20235,22 +20581,22 @@ async function unzoomFile(client2, input, opts = {}) {
|
|
|
20235
20581
|
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
20236
20582
|
}
|
|
20237
20583
|
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
20238
|
-
await mkdir13(
|
|
20239
|
-
await
|
|
20584
|
+
await mkdir13(path43.dirname(out), { recursive: true });
|
|
20585
|
+
await writeFile18(out, res.png);
|
|
20240
20586
|
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
20241
20587
|
}
|
|
20242
20588
|
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
20243
20589
|
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
20244
20590
|
var FONT_COST_GENERATIONS = 25;
|
|
20245
20591
|
function fontOutputPaths(out) {
|
|
20246
|
-
const base =
|
|
20592
|
+
const base = path43.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
20247
20593
|
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
20248
20594
|
}
|
|
20249
20595
|
async function generateFont(client2, opts) {
|
|
20250
20596
|
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
20251
20597
|
if (!opts.force) {
|
|
20252
20598
|
for (const file of [ttf, atlas]) {
|
|
20253
|
-
if (
|
|
20599
|
+
if (existsSync33(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
20254
20600
|
}
|
|
20255
20601
|
}
|
|
20256
20602
|
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -20277,9 +20623,9 @@ async function generateFont(client2, opts) {
|
|
|
20277
20623
|
client2.download(job.download_ttf_url),
|
|
20278
20624
|
client2.download(job.download_atlas_url)
|
|
20279
20625
|
]);
|
|
20280
|
-
await mkdir13(
|
|
20281
|
-
await
|
|
20282
|
-
await
|
|
20626
|
+
await mkdir13(path43.dirname(ttf), { recursive: true });
|
|
20627
|
+
await writeFile18(ttf, ttfBytes);
|
|
20628
|
+
await writeFile18(atlas, atlasBytes);
|
|
20283
20629
|
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
20284
20630
|
}
|
|
20285
20631
|
if (Date.now() - started > timeout) {
|
|
@@ -20293,7 +20639,7 @@ async function generateFont(client2, opts) {
|
|
|
20293
20639
|
|
|
20294
20640
|
// src/cli/commands/pixellab-utils.ts
|
|
20295
20641
|
function loadKeys(args) {
|
|
20296
|
-
loadEnvFiles(
|
|
20642
|
+
loadEnvFiles(path44.dirname(path44.resolve(args.manifest)));
|
|
20297
20643
|
loadEnvFiles(process.cwd());
|
|
20298
20644
|
}
|
|
20299
20645
|
async function runUnzoom(args) {
|
|
@@ -20359,19 +20705,19 @@ async function runFont(args) {
|
|
|
20359
20705
|
}
|
|
20360
20706
|
|
|
20361
20707
|
// src/cli/commands/quality.ts
|
|
20362
|
-
import
|
|
20363
|
-
import { readFile as
|
|
20708
|
+
import path46 from "path";
|
|
20709
|
+
import { readFile as readFile31 } from "fs/promises";
|
|
20364
20710
|
|
|
20365
20711
|
// src/pipeline/quality-regression.ts
|
|
20366
|
-
import
|
|
20367
|
-
import { existsSync as
|
|
20368
|
-
import { readFile as
|
|
20369
|
-
import { z as
|
|
20712
|
+
import path45 from "path";
|
|
20713
|
+
import { existsSync as existsSync34 } from "fs";
|
|
20714
|
+
import { readFile as readFile30 } from "fs/promises";
|
|
20715
|
+
import { z as z13 } from "zod";
|
|
20370
20716
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
20371
20717
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
20372
20718
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
20373
|
-
var QualityPathSchema =
|
|
20374
|
-
(value) => !
|
|
20719
|
+
var QualityPathSchema = z13.string().min(1).refine(
|
|
20720
|
+
(value) => !path45.posix.isAbsolute(value) && !value.includes("\\"),
|
|
20375
20721
|
"expected a portable relative path"
|
|
20376
20722
|
);
|
|
20377
20723
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -20384,53 +20730,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
|
|
|
20384
20730
|
maxEdgeContrastDrop: 0.03,
|
|
20385
20731
|
maxIsolatedPixelIncrease: 5e-3
|
|
20386
20732
|
};
|
|
20387
|
-
var QualityToleranceSchema =
|
|
20388
|
-
requireExactHash:
|
|
20389
|
-
maxColorCountIncrease:
|
|
20390
|
-
maxNewColors:
|
|
20391
|
-
maxTransparencyDelta:
|
|
20392
|
-
maxPartialAlphaIncrease:
|
|
20393
|
-
maxEdgeDensityDelta:
|
|
20394
|
-
maxEdgeContrastDrop:
|
|
20395
|
-
maxIsolatedPixelIncrease:
|
|
20733
|
+
var QualityToleranceSchema = z13.object({
|
|
20734
|
+
requireExactHash: z13.boolean(),
|
|
20735
|
+
maxColorCountIncrease: z13.number().int().min(0),
|
|
20736
|
+
maxNewColors: z13.number().int().min(0),
|
|
20737
|
+
maxTransparencyDelta: z13.number().min(0).max(1),
|
|
20738
|
+
maxPartialAlphaIncrease: z13.number().min(0).max(1),
|
|
20739
|
+
maxEdgeDensityDelta: z13.number().min(0).max(1),
|
|
20740
|
+
maxEdgeContrastDrop: z13.number().min(0).max(1),
|
|
20741
|
+
maxIsolatedPixelIncrease: z13.number().min(0).max(1)
|
|
20396
20742
|
}).strict();
|
|
20397
|
-
var ImageQualityMetricsSchema =
|
|
20398
|
-
sha256:
|
|
20399
|
-
width:
|
|
20400
|
-
height:
|
|
20401
|
-
colorCount:
|
|
20402
|
-
colors:
|
|
20403
|
-
transparency:
|
|
20404
|
-
partialAlpha:
|
|
20405
|
-
edgeDensity:
|
|
20406
|
-
meanEdgeContrast:
|
|
20407
|
-
isolatedPixelRatio:
|
|
20743
|
+
var ImageQualityMetricsSchema = z13.object({
|
|
20744
|
+
sha256: z13.string().regex(SHA256_RE),
|
|
20745
|
+
width: z13.number().int().min(1),
|
|
20746
|
+
height: z13.number().int().min(1),
|
|
20747
|
+
colorCount: z13.number().int().min(0),
|
|
20748
|
+
colors: z13.array(z13.string().regex(HEX_RE)),
|
|
20749
|
+
transparency: z13.number().min(0).max(1),
|
|
20750
|
+
partialAlpha: z13.number().min(0).max(1),
|
|
20751
|
+
edgeDensity: z13.number().min(0).max(1),
|
|
20752
|
+
meanEdgeContrast: z13.number().min(0).max(1),
|
|
20753
|
+
isolatedPixelRatio: z13.number().min(0).max(1)
|
|
20408
20754
|
}).strict();
|
|
20409
|
-
var QualityBaselineCaseSchema =
|
|
20410
|
-
id:
|
|
20755
|
+
var QualityBaselineCaseSchema = z13.object({
|
|
20756
|
+
id: z13.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
20411
20757
|
file: QualityPathSchema,
|
|
20412
|
-
record:
|
|
20758
|
+
record: z13.object({
|
|
20413
20759
|
path: QualityPathSchema,
|
|
20414
|
-
sha256:
|
|
20760
|
+
sha256: z13.string().regex(SHA256_RE)
|
|
20415
20761
|
}).strict().optional(),
|
|
20416
20762
|
expected: ImageQualityMetricsSchema,
|
|
20417
20763
|
tolerances: QualityToleranceSchema
|
|
20418
20764
|
}).strict();
|
|
20419
|
-
var QualityBaselineSchema =
|
|
20420
|
-
$schema:
|
|
20421
|
-
format:
|
|
20422
|
-
schemaVersion:
|
|
20423
|
-
cases:
|
|
20765
|
+
var QualityBaselineSchema = z13.object({
|
|
20766
|
+
$schema: z13.string().url().optional(),
|
|
20767
|
+
format: z13.literal("pixelkiln-quality-baseline"),
|
|
20768
|
+
schemaVersion: z13.literal(1),
|
|
20769
|
+
cases: z13.array(QualityBaselineCaseSchema).min(1)
|
|
20424
20770
|
}).strict().superRefine((baseline, context) => {
|
|
20425
20771
|
const ids = baseline.cases.map((entry) => entry.id);
|
|
20426
20772
|
if (new Set(ids).size !== ids.length) {
|
|
20427
|
-
context.addIssue({ code:
|
|
20773
|
+
context.addIssue({ code: z13.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
|
|
20428
20774
|
}
|
|
20429
20775
|
});
|
|
20430
|
-
var QualityInputSchema =
|
|
20431
|
-
id:
|
|
20432
|
-
path:
|
|
20433
|
-
record:
|
|
20776
|
+
var QualityInputSchema = z13.object({
|
|
20777
|
+
id: z13.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
|
|
20778
|
+
path: z13.string().min(1),
|
|
20779
|
+
record: z13.string().min(1).optional(),
|
|
20434
20780
|
tolerances: QualityToleranceSchema.partial().optional()
|
|
20435
20781
|
}).strict();
|
|
20436
20782
|
function message4(error) {
|
|
@@ -20518,10 +20864,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
20518
20864
|
};
|
|
20519
20865
|
}
|
|
20520
20866
|
async function measureImageQuality(file) {
|
|
20521
|
-
const absolute =
|
|
20867
|
+
const absolute = path45.resolve(file);
|
|
20522
20868
|
let bytes;
|
|
20523
20869
|
try {
|
|
20524
|
-
bytes = await
|
|
20870
|
+
bytes = await readFile30(absolute);
|
|
20525
20871
|
} catch (error) {
|
|
20526
20872
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
20527
20873
|
}
|
|
@@ -20532,7 +20878,7 @@ async function measureImageQuality(file) {
|
|
|
20532
20878
|
}
|
|
20533
20879
|
}
|
|
20534
20880
|
function resolveQualityInputs(raw, inputsFilePath) {
|
|
20535
|
-
const parsed =
|
|
20881
|
+
const parsed = z13.array(QualityInputSchema).min(1).safeParse(raw);
|
|
20536
20882
|
if (!parsed.success) {
|
|
20537
20883
|
throw new Error(
|
|
20538
20884
|
"--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
@@ -20540,11 +20886,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20540
20886
|
}
|
|
20541
20887
|
const ids = parsed.data.map((entry) => entry.id);
|
|
20542
20888
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
20543
|
-
const root =
|
|
20889
|
+
const root = path45.dirname(path45.resolve(inputsFilePath));
|
|
20544
20890
|
return parsed.data.map((entry) => ({
|
|
20545
20891
|
id: entry.id,
|
|
20546
|
-
file:
|
|
20547
|
-
...entry.record ? { record:
|
|
20892
|
+
file: path45.resolve(root, entry.path),
|
|
20893
|
+
...entry.record ? { record: path45.resolve(root, entry.record) } : {},
|
|
20548
20894
|
tolerances: QualityToleranceSchema.parse({
|
|
20549
20895
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
20550
20896
|
...entry.tolerances
|
|
@@ -20552,7 +20898,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
20552
20898
|
}));
|
|
20553
20899
|
}
|
|
20554
20900
|
function portableRelative2(from, to) {
|
|
20555
|
-
return
|
|
20901
|
+
return path45.relative(from, path45.resolve(to)).split(path45.sep).join("/") || ".";
|
|
20556
20902
|
}
|
|
20557
20903
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
20558
20904
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -20567,20 +20913,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
20567
20913
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
20568
20914
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
20569
20915
|
}
|
|
20570
|
-
const outputs = record.outputs.map((output) =>
|
|
20571
|
-
if (!outputs.includes(
|
|
20916
|
+
const outputs = record.outputs.map((output) => path45.resolve(path45.dirname(recordPath), output.path));
|
|
20917
|
+
if (!outputs.includes(path45.resolve(imagePath))) {
|
|
20572
20918
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
20573
20919
|
}
|
|
20574
20920
|
}
|
|
20575
20921
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
20576
20922
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
20577
|
-
const absolute =
|
|
20923
|
+
const absolute = path45.resolve(baselinePath);
|
|
20578
20924
|
for (const input of inputs) {
|
|
20579
|
-
if (
|
|
20925
|
+
if (path45.resolve(input.file) === absolute || input.record && path45.resolve(input.record) === absolute) {
|
|
20580
20926
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
20581
20927
|
}
|
|
20582
20928
|
}
|
|
20583
|
-
const root =
|
|
20929
|
+
const root = path45.dirname(absolute);
|
|
20584
20930
|
const cases = [];
|
|
20585
20931
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
20586
20932
|
const expected = await measureImageQuality(input.file);
|
|
@@ -20607,8 +20953,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
20607
20953
|
cases
|
|
20608
20954
|
});
|
|
20609
20955
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
20610
|
-
if (
|
|
20611
|
-
const current = await
|
|
20956
|
+
if (existsSync34(absolute) && !options.force) {
|
|
20957
|
+
const current = await readFile30(absolute);
|
|
20612
20958
|
if (!current.equals(data)) {
|
|
20613
20959
|
throw new OverwriteRefusedError(
|
|
20614
20960
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -20620,10 +20966,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
20620
20966
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
20621
20967
|
}
|
|
20622
20968
|
async function readQualityBaseline(baselinePath) {
|
|
20623
|
-
const absolute =
|
|
20969
|
+
const absolute = path45.resolve(baselinePath);
|
|
20624
20970
|
let raw;
|
|
20625
20971
|
try {
|
|
20626
|
-
raw = JSON.parse(await
|
|
20972
|
+
raw = JSON.parse(await readFile30(absolute, "utf8"));
|
|
20627
20973
|
} catch (error) {
|
|
20628
20974
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
20629
20975
|
}
|
|
@@ -20693,12 +21039,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
20693
21039
|
return { violations, warnings };
|
|
20694
21040
|
}
|
|
20695
21041
|
async function checkQualityBaseline(baselinePath) {
|
|
20696
|
-
const absolute =
|
|
21042
|
+
const absolute = path45.resolve(baselinePath);
|
|
20697
21043
|
const baseline = await readQualityBaseline(absolute);
|
|
20698
|
-
const root =
|
|
21044
|
+
const root = path45.dirname(absolute);
|
|
20699
21045
|
const cases = [];
|
|
20700
21046
|
for (const entry of baseline.cases) {
|
|
20701
|
-
const file =
|
|
21047
|
+
const file = path45.resolve(root, entry.file);
|
|
20702
21048
|
let actual = null;
|
|
20703
21049
|
const violations = [];
|
|
20704
21050
|
const warnings = [];
|
|
@@ -20711,7 +21057,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
20711
21057
|
violations.push(message4(error));
|
|
20712
21058
|
}
|
|
20713
21059
|
if (entry.record) {
|
|
20714
|
-
const recordPath =
|
|
21060
|
+
const recordPath = path45.resolve(root, entry.record.path);
|
|
20715
21061
|
try {
|
|
20716
21062
|
const recordHash = await sha256File(recordPath);
|
|
20717
21063
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -20753,15 +21099,15 @@ async function runQuality(args) {
|
|
|
20753
21099
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
20754
21100
|
let raw;
|
|
20755
21101
|
try {
|
|
20756
|
-
raw = JSON.parse(await
|
|
21102
|
+
raw = JSON.parse(await readFile31(path46.resolve(args.inputs), "utf8"));
|
|
20757
21103
|
} catch (error) {
|
|
20758
21104
|
throw new Error(
|
|
20759
|
-
`Could not read quality inputs ${
|
|
21105
|
+
`Could not read quality inputs ${path46.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
20760
21106
|
{ cause: error }
|
|
20761
21107
|
);
|
|
20762
21108
|
}
|
|
20763
21109
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
20764
|
-
if (
|
|
21110
|
+
if (path46.resolve(args.inputs) === path46.resolve(args.out)) {
|
|
20765
21111
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
20766
21112
|
}
|
|
20767
21113
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -20774,7 +21120,7 @@ async function runQuality(args) {
|
|
|
20774
21120
|
}, null, 2));
|
|
20775
21121
|
} else {
|
|
20776
21122
|
log(
|
|
20777
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
21123
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path46.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
20778
21124
|
);
|
|
20779
21125
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
20780
21126
|
}
|
|
@@ -20800,54 +21146,54 @@ async function runQuality(args) {
|
|
|
20800
21146
|
}
|
|
20801
21147
|
|
|
20802
21148
|
// src/cli/commands/recipe.ts
|
|
20803
|
-
import
|
|
21149
|
+
import path48 from "path";
|
|
20804
21150
|
|
|
20805
21151
|
// src/recipes.ts
|
|
20806
|
-
import
|
|
20807
|
-
import { existsSync as
|
|
20808
|
-
import { readdir as readdir3, readFile as
|
|
21152
|
+
import path47 from "path";
|
|
21153
|
+
import { existsSync as existsSync35 } from "fs";
|
|
21154
|
+
import { readdir as readdir3, readFile as readFile32 } from "fs/promises";
|
|
20809
21155
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
20810
|
-
import { z as
|
|
21156
|
+
import { z as z14 } from "zod";
|
|
20811
21157
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
20812
21158
|
var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
|
|
20813
21159
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
20814
21160
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
20815
21161
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
20816
|
-
var PortablePathSchema =
|
|
20817
|
-
if (value.includes("\\") ||
|
|
21162
|
+
var PortablePathSchema = z14.string().min(1).refine((value) => {
|
|
21163
|
+
if (value.includes("\\") || path47.posix.isAbsolute(value)) return false;
|
|
20818
21164
|
const parts = value.split("/");
|
|
20819
21165
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
20820
21166
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
20821
|
-
var RecipeBindingSchema =
|
|
20822
|
-
nodeId:
|
|
20823
|
-
input:
|
|
21167
|
+
var RecipeBindingSchema = z14.object({
|
|
21168
|
+
nodeId: z14.string().min(1),
|
|
21169
|
+
input: z14.string().min(1)
|
|
20824
21170
|
}).strict();
|
|
20825
|
-
var RecipeSchema =
|
|
20826
|
-
$schema:
|
|
20827
|
-
format:
|
|
20828
|
-
schemaVersion:
|
|
20829
|
-
id:
|
|
20830
|
-
version:
|
|
20831
|
-
provider:
|
|
20832
|
-
summary:
|
|
20833
|
-
files:
|
|
21171
|
+
var RecipeSchema = z14.object({
|
|
21172
|
+
$schema: z14.string().url().optional(),
|
|
21173
|
+
format: z14.literal("pixelkiln-recipe"),
|
|
21174
|
+
schemaVersion: z14.literal(1),
|
|
21175
|
+
id: z14.string().regex(RECIPE_ID_RE, "expected provider/name"),
|
|
21176
|
+
version: z14.string().regex(VERSION_RE, "expected x.y.z"),
|
|
21177
|
+
provider: z14.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
21178
|
+
summary: z14.string().min(1).max(240),
|
|
21179
|
+
files: z14.array(z14.object({
|
|
20834
21180
|
path: PortablePathSchema,
|
|
20835
|
-
role:
|
|
20836
|
-
sha256:
|
|
21181
|
+
role: z14.enum(["workflow", "reference"]),
|
|
21182
|
+
sha256: z14.string().regex(SHA256_RE2)
|
|
20837
21183
|
}).strict()).default([]),
|
|
20838
|
-
models:
|
|
21184
|
+
models: z14.array(z14.object({
|
|
20839
21185
|
path: PortablePathSchema,
|
|
20840
|
-
sha256:
|
|
20841
|
-
source:
|
|
20842
|
-
license:
|
|
21186
|
+
sha256: z14.string().regex(SHA256_RE2),
|
|
21187
|
+
source: z14.string().url(),
|
|
21188
|
+
license: z14.string().min(1)
|
|
20843
21189
|
}).strict()).default([]),
|
|
20844
|
-
styleId:
|
|
21190
|
+
styleId: z14.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
20845
21191
|
style: StyleSchema,
|
|
20846
|
-
workflow:
|
|
21192
|
+
workflow: z14.object({
|
|
20847
21193
|
path: PortablePathSchema,
|
|
20848
|
-
outputNodeId:
|
|
20849
|
-
numImages:
|
|
20850
|
-
bindings:
|
|
21194
|
+
outputNodeId: z14.string().min(1),
|
|
21195
|
+
numImages: z14.number().int().min(1).max(16),
|
|
21196
|
+
bindings: z14.object({
|
|
20851
21197
|
prompt: RecipeBindingSchema,
|
|
20852
21198
|
width: RecipeBindingSchema.optional(),
|
|
20853
21199
|
height: RecipeBindingSchema.optional(),
|
|
@@ -20858,67 +21204,67 @@ var RecipeSchema = z13.object({
|
|
|
20858
21204
|
strength: RecipeBindingSchema.optional()
|
|
20859
21205
|
}).strict()
|
|
20860
21206
|
}).strict().optional(),
|
|
20861
|
-
quality:
|
|
20862
|
-
stage:
|
|
20863
|
-
workingCanvas:
|
|
20864
|
-
width:
|
|
20865
|
-
height:
|
|
21207
|
+
quality: z14.object({
|
|
21208
|
+
stage: z14.enum(["composition-source", "production-candidate"]),
|
|
21209
|
+
workingCanvas: z14.object({
|
|
21210
|
+
width: z14.number().int().min(16),
|
|
21211
|
+
height: z14.number().int().min(16)
|
|
20866
21212
|
}).strict(),
|
|
20867
|
-
recommendedNativeSize:
|
|
20868
|
-
min:
|
|
20869
|
-
max:
|
|
21213
|
+
recommendedNativeSize: z14.object({
|
|
21214
|
+
min: z14.number().int().min(1),
|
|
21215
|
+
max: z14.number().int().min(1)
|
|
20870
21216
|
}).strict(),
|
|
20871
|
-
paletteColors:
|
|
20872
|
-
min:
|
|
20873
|
-
max:
|
|
21217
|
+
paletteColors: z14.object({
|
|
21218
|
+
min: z14.number().int().min(1),
|
|
21219
|
+
max: z14.number().int().min(1)
|
|
20874
21220
|
}).strict(),
|
|
20875
|
-
background:
|
|
20876
|
-
checks:
|
|
21221
|
+
background: z14.enum(["opaque", "transparent", "full-bleed"]),
|
|
21222
|
+
checks: z14.array(z14.enum([
|
|
20877
21223
|
"prompt-coverage",
|
|
20878
21224
|
"native-grid",
|
|
20879
21225
|
"final-palette",
|
|
20880
21226
|
"alpha-edge",
|
|
20881
21227
|
"human-1x"
|
|
20882
21228
|
])).min(1),
|
|
20883
|
-
notes:
|
|
21229
|
+
notes: z14.array(z14.string().min(1)).default([])
|
|
20884
21230
|
}).strict(),
|
|
20885
|
-
integrity:
|
|
20886
|
-
algorithm:
|
|
20887
|
-
digest:
|
|
21231
|
+
integrity: z14.object({
|
|
21232
|
+
algorithm: z14.literal("sha256"),
|
|
21233
|
+
digest: z14.string().regex(SHA256_RE2)
|
|
20888
21234
|
}).strict()
|
|
20889
21235
|
}).strict().superRefine((recipe, context) => {
|
|
20890
21236
|
if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
|
|
20891
21237
|
context.addIssue({
|
|
20892
|
-
code:
|
|
21238
|
+
code: z14.ZodIssueCode.custom,
|
|
20893
21239
|
path: ["style", "provider"],
|
|
20894
21240
|
message: "must match the recipe provider"
|
|
20895
21241
|
});
|
|
20896
21242
|
}
|
|
20897
21243
|
const filePaths = recipe.files.map((file) => file.path);
|
|
20898
21244
|
if (new Set(filePaths).size !== filePaths.length) {
|
|
20899
|
-
context.addIssue({ code:
|
|
21245
|
+
context.addIssue({ code: z14.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
|
|
20900
21246
|
}
|
|
20901
21247
|
const modelPaths = recipe.models.map((model) => model.path);
|
|
20902
21248
|
if (new Set(modelPaths).size !== modelPaths.length) {
|
|
20903
|
-
context.addIssue({ code:
|
|
21249
|
+
context.addIssue({ code: z14.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
|
|
20904
21250
|
}
|
|
20905
21251
|
if (filePaths.includes(RECIPE_FILE)) {
|
|
20906
21252
|
context.addIssue({
|
|
20907
|
-
code:
|
|
21253
|
+
code: z14.ZodIssueCode.custom,
|
|
20908
21254
|
path: ["files"],
|
|
20909
21255
|
message: `${RECIPE_FILE} cannot hash itself`
|
|
20910
21256
|
});
|
|
20911
21257
|
}
|
|
20912
21258
|
if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
|
|
20913
21259
|
context.addIssue({
|
|
20914
|
-
code:
|
|
21260
|
+
code: z14.ZodIssueCode.custom,
|
|
20915
21261
|
path: ["quality", "recommendedNativeSize"],
|
|
20916
21262
|
message: "min cannot exceed max"
|
|
20917
21263
|
});
|
|
20918
21264
|
}
|
|
20919
21265
|
if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
|
|
20920
21266
|
context.addIssue({
|
|
20921
|
-
code:
|
|
21267
|
+
code: z14.ZodIssueCode.custom,
|
|
20922
21268
|
path: ["quality", "paletteColors"],
|
|
20923
21269
|
message: "min cannot exceed max"
|
|
20924
21270
|
});
|
|
@@ -20927,7 +21273,7 @@ var RecipeSchema = z13.object({
|
|
|
20927
21273
|
(file) => file.path === recipe.workflow.path && file.role === "workflow"
|
|
20928
21274
|
)) {
|
|
20929
21275
|
context.addIssue({
|
|
20930
|
-
code:
|
|
21276
|
+
code: z14.ZodIssueCode.custom,
|
|
20931
21277
|
path: ["workflow", "path"],
|
|
20932
21278
|
message: "must name a file whose role is workflow"
|
|
20933
21279
|
});
|
|
@@ -20935,7 +21281,7 @@ var RecipeSchema = z13.object({
|
|
|
20935
21281
|
if (recipe.provider === "comfyui") {
|
|
20936
21282
|
if (!recipe.workflow) {
|
|
20937
21283
|
context.addIssue({
|
|
20938
|
-
code:
|
|
21284
|
+
code: z14.ZodIssueCode.custom,
|
|
20939
21285
|
path: ["workflow"],
|
|
20940
21286
|
message: "ComfyUI recipes require workflow metadata"
|
|
20941
21287
|
});
|
|
@@ -20944,7 +21290,7 @@ var RecipeSchema = z13.object({
|
|
|
20944
21290
|
const options = recipe.style.providerOptions.comfyui;
|
|
20945
21291
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
20946
21292
|
context.addIssue({
|
|
20947
|
-
code:
|
|
21293
|
+
code: z14.ZodIssueCode.custom,
|
|
20948
21294
|
path: ["style", "providerOptions", "comfyui"],
|
|
20949
21295
|
message: "ComfyUI recipes require ComfyUI provider options"
|
|
20950
21296
|
});
|
|
@@ -20958,7 +21304,7 @@ var RecipeSchema = z13.object({
|
|
|
20958
21304
|
};
|
|
20959
21305
|
if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
|
|
20960
21306
|
context.addIssue({
|
|
20961
|
-
code:
|
|
21307
|
+
code: z14.ZodIssueCode.custom,
|
|
20962
21308
|
path: ["style", "providerOptions", "comfyui"],
|
|
20963
21309
|
message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
|
|
20964
21310
|
});
|
|
@@ -20990,10 +21336,10 @@ function message5(error) {
|
|
|
20990
21336
|
return error instanceof Error ? error.message : String(error);
|
|
20991
21337
|
}
|
|
20992
21338
|
async function readRecipeFile(recipePath, bundled) {
|
|
20993
|
-
const absolute =
|
|
21339
|
+
const absolute = path47.resolve(recipePath);
|
|
20994
21340
|
let raw;
|
|
20995
21341
|
try {
|
|
20996
|
-
raw = JSON.parse(await
|
|
21342
|
+
raw = JSON.parse(await readFile32(absolute, "utf8"));
|
|
20997
21343
|
} catch (error) {
|
|
20998
21344
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
20999
21345
|
}
|
|
@@ -21004,13 +21350,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
21004
21350
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
21005
21351
|
);
|
|
21006
21352
|
}
|
|
21007
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
21353
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path47.dirname(absolute), bundled };
|
|
21008
21354
|
}
|
|
21009
21355
|
async function walkRecipeFiles(root) {
|
|
21010
|
-
if (!
|
|
21356
|
+
if (!existsSync35(root)) return [];
|
|
21011
21357
|
const out = [];
|
|
21012
21358
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
21013
|
-
const resolved =
|
|
21359
|
+
const resolved = path47.join(root, entry.name);
|
|
21014
21360
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
21015
21361
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
21016
21362
|
}
|
|
@@ -21021,7 +21367,7 @@ function bundledRecipeRoot() {
|
|
|
21021
21367
|
}
|
|
21022
21368
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
21023
21369
|
const recipes = await Promise.all(
|
|
21024
|
-
(await walkRecipeFiles(
|
|
21370
|
+
(await walkRecipeFiles(path47.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
21025
21371
|
);
|
|
21026
21372
|
return recipes.sort(
|
|
21027
21373
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -21044,9 +21390,9 @@ function parseSelector(selector) {
|
|
|
21044
21390
|
return { id: match[1], version: match[2] };
|
|
21045
21391
|
}
|
|
21046
21392
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
21047
|
-
const local =
|
|
21048
|
-
if (
|
|
21049
|
-
const recipePath =
|
|
21393
|
+
const local = path47.resolve(target);
|
|
21394
|
+
if (existsSync35(local)) {
|
|
21395
|
+
const recipePath = path47.basename(local) === RECIPE_FILE ? local : path47.join(local, RECIPE_FILE);
|
|
21050
21396
|
return readRecipeFile(recipePath, false);
|
|
21051
21397
|
}
|
|
21052
21398
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -21060,8 +21406,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
21060
21406
|
return matches[0];
|
|
21061
21407
|
}
|
|
21062
21408
|
async function verifyFile(root, file) {
|
|
21063
|
-
const absolute =
|
|
21064
|
-
if (!
|
|
21409
|
+
const absolute = path47.resolve(root, ...file.path.split("/"));
|
|
21410
|
+
if (!existsSync35(absolute)) {
|
|
21065
21411
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
21066
21412
|
}
|
|
21067
21413
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -21076,7 +21422,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
21076
21422
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
21077
21423
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
21078
21424
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
21079
|
-
const modelRoot = options.modelRoot ?
|
|
21425
|
+
const modelRoot = options.modelRoot ? path47.resolve(options.modelRoot) : null;
|
|
21080
21426
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
21081
21427
|
...await verifyFile(modelRoot, model),
|
|
21082
21428
|
source: model.source,
|
|
@@ -21110,7 +21456,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
21110
21456
|
};
|
|
21111
21457
|
}
|
|
21112
21458
|
function renderStyle(style, destination, cwd) {
|
|
21113
|
-
const portableDir =
|
|
21459
|
+
const portableDir = path47.relative(cwd, destination).split(path47.sep).join("/") || ".";
|
|
21114
21460
|
const replace = (value) => {
|
|
21115
21461
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
21116
21462
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -21122,25 +21468,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
21122
21468
|
return replace(style);
|
|
21123
21469
|
}
|
|
21124
21470
|
async function installRecipe(target, options = {}) {
|
|
21125
|
-
const cwd =
|
|
21471
|
+
const cwd = path47.resolve(options.cwd ?? process.cwd());
|
|
21126
21472
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
21127
21473
|
const verification = await verifyRecipe(loaded.path);
|
|
21128
21474
|
if (!verification.ok) {
|
|
21129
21475
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
21130
21476
|
}
|
|
21131
|
-
const destination = options.out ?
|
|
21477
|
+
const destination = options.out ? path47.resolve(cwd, options.out) : path47.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
21132
21478
|
const sources = [
|
|
21133
21479
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
21134
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
21480
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path47.join(loaded.dir, ...file.path.split("/")) }))
|
|
21135
21481
|
];
|
|
21136
21482
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
21137
|
-
path:
|
|
21138
|
-
data: await
|
|
21483
|
+
path: path47.join(destination, ...file.path.split("/")),
|
|
21484
|
+
data: await readFile32(file.source)
|
|
21139
21485
|
})));
|
|
21140
21486
|
if (!options.force) {
|
|
21141
21487
|
for (const file of files) {
|
|
21142
|
-
if (!
|
|
21143
|
-
const current = await
|
|
21488
|
+
if (!existsSync35(file.path)) continue;
|
|
21489
|
+
const current = await readFile32(file.path);
|
|
21144
21490
|
if (!current.equals(file.data)) {
|
|
21145
21491
|
throw new OverwriteRefusedError(
|
|
21146
21492
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -21234,14 +21580,14 @@ async function runRecipe(args) {
|
|
|
21234
21580
|
}, null, 2));
|
|
21235
21581
|
} else {
|
|
21236
21582
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
21237
|
-
log(` ${
|
|
21583
|
+
log(` ${path48.relative(process.cwd(), result.destination) || "."}`);
|
|
21238
21584
|
log(`
|
|
21239
21585
|
Add this entry under your manifest's styles object:`);
|
|
21240
21586
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
21241
21587
|
if (result.recipe.models.length) {
|
|
21242
21588
|
log(`
|
|
21243
21589
|
Models are not downloaded automatically. Verify them with:`);
|
|
21244
|
-
log(` pixelkiln recipe verify ${
|
|
21590
|
+
log(` pixelkiln recipe verify ${path48.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
21245
21591
|
}
|
|
21246
21592
|
}
|
|
21247
21593
|
return;
|
|
@@ -21249,7 +21595,7 @@ async function runRecipe(args) {
|
|
|
21249
21595
|
}
|
|
21250
21596
|
|
|
21251
21597
|
// src/cli/commands/refine.ts
|
|
21252
|
-
import
|
|
21598
|
+
import path49 from "path";
|
|
21253
21599
|
async function runRefine(args) {
|
|
21254
21600
|
if (args.from || args.subcommand === "approve") {
|
|
21255
21601
|
if (!args.from) {
|
|
@@ -21279,12 +21625,12 @@ async function runRefine(args) {
|
|
|
21279
21625
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
21280
21626
|
);
|
|
21281
21627
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
21282
|
-
log(` wrote ${
|
|
21283
|
-
log(` quality record: ${
|
|
21628
|
+
log(` wrote ${path49.relative(process.cwd(), result3.output)}`);
|
|
21629
|
+
log(` quality record: ${path49.relative(process.cwd(), result3.record)}`);
|
|
21284
21630
|
log(`
|
|
21285
21631
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
21286
21632
|
log(
|
|
21287
|
-
` pixelkiln refine approve --from ${
|
|
21633
|
+
` pixelkiln refine approve --from ${path49.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
21288
21634
|
);
|
|
21289
21635
|
}
|
|
21290
21636
|
return;
|
|
@@ -21312,7 +21658,7 @@ async function runRefine(args) {
|
|
|
21312
21658
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
21313
21659
|
else {
|
|
21314
21660
|
log(
|
|
21315
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
21661
|
+
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path49.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path49.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
|
|
21316
21662
|
);
|
|
21317
21663
|
}
|
|
21318
21664
|
return;
|
|
@@ -21321,7 +21667,7 @@ async function runRefine(args) {
|
|
|
21321
21667
|
if (args.json) {
|
|
21322
21668
|
log(JSON.stringify(result2, null, 2));
|
|
21323
21669
|
} else {
|
|
21324
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
21670
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path49.relative(process.cwd(), result2.output || result2.record)}`);
|
|
21325
21671
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
21326
21672
|
}
|
|
21327
21673
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -21367,7 +21713,7 @@ async function runRefine(args) {
|
|
|
21367
21713
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
21368
21714
|
if (item.state === "needs-approval") {
|
|
21369
21715
|
log(
|
|
21370
|
-
` pixelkiln refine approve --from ${
|
|
21716
|
+
` pixelkiln refine approve --from ${path49.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
21371
21717
|
);
|
|
21372
21718
|
}
|
|
21373
21719
|
}
|
|
@@ -21378,9 +21724,9 @@ async function runRefine(args) {
|
|
|
21378
21724
|
}
|
|
21379
21725
|
|
|
21380
21726
|
// src/cli/commands/skeleton.ts
|
|
21381
|
-
import
|
|
21382
|
-
import { existsSync as
|
|
21383
|
-
import { readFile as
|
|
21727
|
+
import path50 from "path";
|
|
21728
|
+
import { existsSync as existsSync36 } from "fs";
|
|
21729
|
+
import { readFile as readFile33, writeFile as writeFile19 } from "fs/promises";
|
|
21384
21730
|
|
|
21385
21731
|
// src/skeleton-preview.ts
|
|
21386
21732
|
var RIGHT = [255, 140, 40, 255];
|
|
@@ -21491,12 +21837,12 @@ var Canvas = class {
|
|
|
21491
21837
|
async function runEstimateSkeleton(args) {
|
|
21492
21838
|
const image = args.target;
|
|
21493
21839
|
if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
|
|
21494
|
-
if (args.out &&
|
|
21840
|
+
if (args.out && existsSync36(path50.resolve(args.out)) && !args.force) {
|
|
21495
21841
|
throw new UsageError(`${args.out} already exists; pass --force to replace it`);
|
|
21496
21842
|
}
|
|
21497
|
-
loadEnvFiles(
|
|
21843
|
+
loadEnvFiles(path50.dirname(path50.resolve(args.manifest)));
|
|
21498
21844
|
loadEnvFiles(process.cwd());
|
|
21499
|
-
const bytes = await
|
|
21845
|
+
const bytes = await readFile33(path50.resolve(image));
|
|
21500
21846
|
const metadata = imageMetadata(bytes);
|
|
21501
21847
|
if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
|
|
21502
21848
|
const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
@@ -21508,7 +21854,7 @@ async function runEstimateSkeleton(args) {
|
|
|
21508
21854
|
const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
|
|
21509
21855
|
const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
|
|
21510
21856
|
if (args.out) {
|
|
21511
|
-
await
|
|
21857
|
+
await writeFile19(path50.resolve(args.out), json + "\n");
|
|
21512
21858
|
log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
|
|
21513
21859
|
log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
|
|
21514
21860
|
} else {
|
|
@@ -21521,8 +21867,8 @@ async function runSkeletonPreview(args) {
|
|
|
21521
21867
|
let sourceFile;
|
|
21522
21868
|
let label;
|
|
21523
21869
|
if (target.endsWith(".json")) {
|
|
21524
|
-
keypointsFile =
|
|
21525
|
-
sourceFile = args.from ?
|
|
21870
|
+
keypointsFile = path50.resolve(target);
|
|
21871
|
+
sourceFile = args.from ? path50.resolve(args.from) : void 0;
|
|
21526
21872
|
label = target;
|
|
21527
21873
|
} else {
|
|
21528
21874
|
const loaded = await loadManifest(args.manifest);
|
|
@@ -21534,31 +21880,31 @@ async function runSkeletonPreview(args) {
|
|
|
21534
21880
|
);
|
|
21535
21881
|
}
|
|
21536
21882
|
keypointsFile = spec.revision.keypointsFile;
|
|
21537
|
-
sourceFile = args.from ?
|
|
21883
|
+
sourceFile = args.from ? path50.resolve(args.from) : spec.revision.sourceFile;
|
|
21538
21884
|
label = `${spec.styleId}/${spec.assetId}`;
|
|
21539
21885
|
}
|
|
21540
|
-
if (!
|
|
21886
|
+
if (!existsSync36(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
|
|
21541
21887
|
let json;
|
|
21542
21888
|
try {
|
|
21543
|
-
json = JSON.parse(await
|
|
21889
|
+
json = JSON.parse(await readFile33(keypointsFile, "utf8"));
|
|
21544
21890
|
} catch (error) {
|
|
21545
21891
|
throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
21546
21892
|
}
|
|
21547
21893
|
const set = parseSkeletonSet(json, keypointsFile);
|
|
21548
21894
|
let source;
|
|
21549
21895
|
if (sourceFile) {
|
|
21550
|
-
if (!
|
|
21896
|
+
if (!existsSync36(sourceFile)) {
|
|
21551
21897
|
log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
|
|
21552
21898
|
} else {
|
|
21553
|
-
const bytes = await
|
|
21899
|
+
const bytes = await readFile33(sourceFile);
|
|
21554
21900
|
if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
|
|
21555
21901
|
source = decodePng(bytes);
|
|
21556
21902
|
}
|
|
21557
21903
|
}
|
|
21558
|
-
const out =
|
|
21559
|
-
await
|
|
21904
|
+
const out = path50.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
|
|
21905
|
+
await writeFile19(out, renderSkeletonSheet(set, { source }));
|
|
21560
21906
|
log(` ${label}: ${set.frames.length} frames after the starting pose`);
|
|
21561
|
-
const shown =
|
|
21907
|
+
const shown = path50.relative(process.cwd(), out);
|
|
21562
21908
|
log(` wrote ${shown.startsWith("..") ? out : shown}`);
|
|
21563
21909
|
}
|
|
21564
21910
|
|
|
@@ -21604,15 +21950,15 @@ async function runTools(args) {
|
|
|
21604
21950
|
}
|
|
21605
21951
|
|
|
21606
21952
|
// src/cli/commands/workspace.ts
|
|
21607
|
-
import
|
|
21608
|
-
import { existsSync as
|
|
21953
|
+
import path51 from "path";
|
|
21954
|
+
import { existsSync as existsSync37 } from "fs";
|
|
21609
21955
|
async function runWorkspace(args) {
|
|
21610
|
-
const workspacePath =
|
|
21611
|
-
const dir =
|
|
21956
|
+
const workspacePath = path51.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
21957
|
+
const dir = path51.dirname(workspacePath);
|
|
21612
21958
|
if (args.subcommand === "add") {
|
|
21613
|
-
const manifestPath =
|
|
21959
|
+
const manifestPath = path51.resolve(args.target);
|
|
21614
21960
|
const loadedTarget = await loadManifest(manifestPath);
|
|
21615
|
-
const lockPath = args.explicitLock ?
|
|
21961
|
+
const lockPath = args.explicitLock ? path51.resolve(args.explicitLock) : path51.join(path51.dirname(manifestPath), "pixelkiln.lock.json");
|
|
21616
21962
|
const ws = await loadWorkspace(workspacePath);
|
|
21617
21963
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
21618
21964
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -21632,10 +21978,10 @@ async function runWorkspace(args) {
|
|
|
21632
21978
|
...args.account ? { account: args.account } : {}
|
|
21633
21979
|
};
|
|
21634
21980
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
21635
|
-
log(` registered "${id}" in ${
|
|
21981
|
+
log(` registered "${id}" in ${path51.relative(process.cwd(), workspacePath)}`);
|
|
21636
21982
|
log(` manifest: ${project.manifest}`);
|
|
21637
21983
|
log(` lock: ${project.lock}`);
|
|
21638
|
-
if (!
|
|
21984
|
+
if (!existsSync37(lockPath)) {
|
|
21639
21985
|
log(
|
|
21640
21986
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
21641
21987
|
);
|
|
@@ -21644,7 +21990,7 @@ async function runWorkspace(args) {
|
|
|
21644
21990
|
}
|
|
21645
21991
|
if (args.subcommand === "remove") {
|
|
21646
21992
|
const ws = await loadWorkspace(workspacePath);
|
|
21647
|
-
const resolvedTarget =
|
|
21993
|
+
const resolvedTarget = path51.resolve(args.target);
|
|
21648
21994
|
const match = ws.projects.find(
|
|
21649
21995
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
21650
21996
|
);
|
|
@@ -21655,11 +22001,11 @@ async function runWorkspace(args) {
|
|
|
21655
22001
|
version: 1,
|
|
21656
22002
|
projects: ws.projects.filter((p) => p !== match)
|
|
21657
22003
|
});
|
|
21658
|
-
log(` removed "${match.id}" from ${
|
|
22004
|
+
log(` removed "${match.id}" from ${path51.relative(process.cwd(), workspacePath)}`);
|
|
21659
22005
|
return;
|
|
21660
22006
|
}
|
|
21661
22007
|
if (args.subcommand === "list") {
|
|
21662
|
-
if (!
|
|
22008
|
+
if (!existsSync37(workspacePath)) {
|
|
21663
22009
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
21664
22010
|
}
|
|
21665
22011
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -21667,9 +22013,9 @@ async function runWorkspace(args) {
|
|
|
21667
22013
|
if (args.json) {
|
|
21668
22014
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
21669
22015
|
} else if (!ws.projects.length) {
|
|
21670
|
-
log(` no projects registered in ${
|
|
22016
|
+
log(` no projects registered in ${path51.relative(process.cwd(), workspacePath)}`);
|
|
21671
22017
|
} else {
|
|
21672
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
22018
|
+
log(` ${ws.projects.length} project(s) in ${path51.relative(process.cwd(), workspacePath)}:`);
|
|
21673
22019
|
for (const p of ws.projects) {
|
|
21674
22020
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
21675
22021
|
}
|
|
@@ -21679,7 +22025,7 @@ async function runWorkspace(args) {
|
|
|
21679
22025
|
return;
|
|
21680
22026
|
}
|
|
21681
22027
|
if (args.subcommand === "status") {
|
|
21682
|
-
if (!
|
|
22028
|
+
if (!existsSync37(workspacePath)) {
|
|
21683
22029
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
21684
22030
|
}
|
|
21685
22031
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -21687,7 +22033,7 @@ async function runWorkspace(args) {
|
|
|
21687
22033
|
if (args.json) {
|
|
21688
22034
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
21689
22035
|
} else {
|
|
21690
|
-
log(` workspace: ${
|
|
22036
|
+
log(` workspace: ${path51.relative(process.cwd(), workspacePath)}`);
|
|
21691
22037
|
for (const p of report.projects) {
|
|
21692
22038
|
if (p.error) {
|
|
21693
22039
|
log(`
|
|
@@ -21741,8 +22087,8 @@ async function runHelp() {
|
|
|
21741
22087
|
async function runVersion() {
|
|
21742
22088
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
21743
22089
|
const url = new URL(up, import.meta.url);
|
|
21744
|
-
if (!
|
|
21745
|
-
const pkg = JSON.parse(await
|
|
22090
|
+
if (!existsSync38(url)) continue;
|
|
22091
|
+
const pkg = JSON.parse(await readFile34(url, "utf8"));
|
|
21746
22092
|
if (pkg.name === "pixelkiln") {
|
|
21747
22093
|
log(`${pkg.name} ${pkg.version}`);
|
|
21748
22094
|
return;
|