pixelkiln 0.61.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +952 -214
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1034 -368
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +559 -210
- package/dist/index.d.ts +559 -210
- package/dist/index.js +997 -342
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +39 -0
- package/docs/CLI.md +63 -3
- package/docs/ENDPOINTS.md +32 -6
- package/docs/GENERATORS.md +45 -0
- package/docs/MANIFEST.md +7 -7
- package/docs/PIXELLAB.md +6 -3
- package/docs/REVISIONS.md +128 -19
- package/package.json +1 -1
- package/schema/manifest.schema.json +24 -3
- package/schema/recipe.schema.json +2 -1
- package/skills/pixelkiln/SKILL.md +3 -1
- package/skills/pixelkiln/references/pixellab-roadmap.md +47 -20
- package/skills/pixelkiln/references/pixellab.md +24 -8
- package/skills/pixelkiln/references/revisions.md +26 -5
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/types.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
|
|
4
|
-
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset"]);
|
|
4
|
+
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement"]);
|
|
5
5
|
var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
|
|
6
6
|
var RevisionModeSchema = z.enum([
|
|
7
7
|
"image-to-image",
|
|
@@ -10,7 +10,9 @@ var RevisionModeSchema = z.enum([
|
|
|
10
10
|
"reduce-colors",
|
|
11
11
|
"correct-pixelart",
|
|
12
12
|
"animate",
|
|
13
|
-
"animate-pixminimax"
|
|
13
|
+
"animate-pixminimax",
|
|
14
|
+
"interpolate",
|
|
15
|
+
"edit-animation"
|
|
14
16
|
]);
|
|
15
17
|
function tileVariationCount(descriptions) {
|
|
16
18
|
return Math.max(1, descriptions) * 4;
|
|
@@ -191,6 +193,14 @@ var CharacterAnimationSchema = z.object({
|
|
|
191
193
|
template: z.string().min(1).optional(),
|
|
192
194
|
/** Direction to animate. */
|
|
193
195
|
direction: CharacterDirectionSchema.default("south"),
|
|
196
|
+
/**
|
|
197
|
+
* Shorthand for one loop in several directions, instead of `direction`.
|
|
198
|
+
* Expanded when the manifest loads into one loop per named direction
|
|
199
|
+
* (`<id>.<direction>`) plus a mirror for each direction whose flip is
|
|
200
|
+
* not named, exactly as if each had been written out. See
|
|
201
|
+
* `src/loop-directions.ts`.
|
|
202
|
+
*/
|
|
203
|
+
directions: z.array(CharacterDirectionSchema).min(1).optional(),
|
|
194
204
|
/** Frames to generate without a template (v3). Even, 4 to 16. */
|
|
195
205
|
frames: z.number().int().min(4).max(16).optional(),
|
|
196
206
|
/** Playback rate recorded with the frames; PixelLab does not store one. */
|
|
@@ -219,6 +229,13 @@ var CharacterAnimationSchema = z.object({
|
|
|
219
229
|
/** v3 text loops only: let PixelLab expand the action into a fuller motion description first. */
|
|
220
230
|
enhancePrompt: z.boolean().optional()
|
|
221
231
|
}).strict().superRefine((animation, context) => {
|
|
232
|
+
if (animation.directions !== void 0) {
|
|
233
|
+
context.addIssue({
|
|
234
|
+
code: z.ZodIssueCode.custom,
|
|
235
|
+
message: "directions is expanded when the manifest loads; validate the loaded manifest, not the raw file",
|
|
236
|
+
path: ["directions"]
|
|
237
|
+
});
|
|
238
|
+
}
|
|
222
239
|
if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
|
|
223
240
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
224
241
|
}
|
|
@@ -279,9 +296,13 @@ var RevisionSchema = z.object({
|
|
|
279
296
|
* the provider layer, since it differs by mode).
|
|
280
297
|
*/
|
|
281
298
|
frames: z.number().int().min(4).max(40).optional(),
|
|
282
|
-
/** `animate`/`animate-pixminimax` only: playback rate recorded with the frames; PixelLab does not store one. */
|
|
299
|
+
/** `animate`/`animate-pixminimax`/`interpolate`/`edit-animation` only: playback rate recorded with the frames; PixelLab does not store one. */
|
|
283
300
|
fps: z.number().int().min(1).max(60).optional(),
|
|
284
|
-
/**
|
|
301
|
+
/**
|
|
302
|
+
* `animate`/`animate-pixminimax`: manifest-relative image pinning where
|
|
303
|
+
* the motion ends (interpolation instead of open-ended animation).
|
|
304
|
+
* `interpolate`: required, the ending keyframe; the parent is the start.
|
|
305
|
+
*/
|
|
285
306
|
lastFrame: z.string().min(1).optional(),
|
|
286
307
|
/** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
|
|
287
308
|
direction: CharacterDirectionSchema.optional(),
|
|
@@ -325,14 +346,15 @@ var RevisionSchema = z.object({
|
|
|
325
346
|
path: ["strength"]
|
|
326
347
|
});
|
|
327
348
|
}
|
|
328
|
-
|
|
349
|
+
const generative = revision.mode === "animate" || revision.mode === "animate-pixminimax" || revision.mode === "interpolate" || revision.mode === "edit-animation";
|
|
350
|
+
if (revision.strength !== void 0 && generative) {
|
|
329
351
|
context.addIssue({
|
|
330
352
|
code: z.ZodIssueCode.custom,
|
|
331
|
-
message: `${revision.mode} revisions do not take a strength; use enhancePrompt`,
|
|
353
|
+
message: revision.mode === "animate" || revision.mode === "animate-pixminimax" ? `${revision.mode} revisions do not take a strength; use enhancePrompt` : `${revision.mode} revisions do not take a strength`,
|
|
332
354
|
path: ["strength"]
|
|
333
355
|
});
|
|
334
356
|
}
|
|
335
|
-
for (const field of ["frames", "
|
|
357
|
+
for (const field of ["frames", "enhancePrompt"]) {
|
|
336
358
|
if (revision[field] !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
|
|
337
359
|
context.addIssue({
|
|
338
360
|
code: z.ZodIssueCode.custom,
|
|
@@ -341,6 +363,27 @@ var RevisionSchema = z.object({
|
|
|
341
363
|
});
|
|
342
364
|
}
|
|
343
365
|
}
|
|
366
|
+
if (revision.lastFrame !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax" && revision.mode !== "interpolate") {
|
|
367
|
+
context.addIssue({
|
|
368
|
+
code: z.ZodIssueCode.custom,
|
|
369
|
+
message: "lastFrame applies to animate/animate-pixminimax/interpolate revisions only",
|
|
370
|
+
path: ["lastFrame"]
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (revision.mode === "interpolate" && revision.lastFrame === void 0) {
|
|
374
|
+
context.addIssue({
|
|
375
|
+
code: z.ZodIssueCode.custom,
|
|
376
|
+
message: "interpolate revisions require lastFrame: the ending keyframe to interpolate toward",
|
|
377
|
+
path: ["lastFrame"]
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (revision.fps !== void 0 && !generative) {
|
|
381
|
+
context.addIssue({
|
|
382
|
+
code: z.ZodIssueCode.custom,
|
|
383
|
+
message: "fps applies to animate/animate-pixminimax/interpolate/edit-animation revisions only",
|
|
384
|
+
path: ["fps"]
|
|
385
|
+
});
|
|
386
|
+
}
|
|
344
387
|
if (revision.direction !== void 0 && revision.mode !== "animate-pixminimax") {
|
|
345
388
|
context.addIssue({
|
|
346
389
|
code: z.ZodIssueCode.custom,
|
|
@@ -352,6 +395,8 @@ var RevisionSchema = z.object({
|
|
|
352
395
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
353
396
|
}
|
|
354
397
|
});
|
|
398
|
+
var FRAME_SET_REVISION_MODES = ["animate", "animate-pixminimax", "interpolate", "edit-animation"];
|
|
399
|
+
var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-animation"];
|
|
355
400
|
var StyleObjectSchema = z.object({
|
|
356
401
|
/** Generation backend for this style. Omit to inherit the manifest default. */
|
|
357
402
|
provider: z.string().min(1).optional(),
|
|
@@ -470,7 +515,7 @@ var StyleObjectSchema = z.object({
|
|
|
470
515
|
*/
|
|
471
516
|
terrainMode: z.enum(["standard", "pro"]).optional(),
|
|
472
517
|
/**
|
|
473
|
-
* `uiAsset`
|
|
518
|
+
* `uiAsset` and `uiElement` generators only. A natural-language palette hint sent as-is
|
|
474
519
|
* (e.g. "brown and gold"), distinct from `palette`'s hex-color array —
|
|
475
520
|
* this is a prompt-level steer, not a local quantization target.
|
|
476
521
|
*/
|
|
@@ -1348,6 +1393,21 @@ var CorrectPixelartResponseSchema = z2.object({
|
|
|
1348
1393
|
images: z2.array(z2.object({ base64: z2.string().min(1) }).passthrough()).min(1),
|
|
1349
1394
|
usage: z2.unknown().optional()
|
|
1350
1395
|
}).passthrough();
|
|
1396
|
+
var UnzoomResponseSchema = z2.object({
|
|
1397
|
+
image: z2.object({ base64: z2.string().min(1) }).passthrough(),
|
|
1398
|
+
original_size: z2.object({ width: z2.number().int(), height: z2.number().int() }).passthrough(),
|
|
1399
|
+
unzoomed_size: z2.object({ width: z2.number().int(), height: z2.number().int() }).passthrough(),
|
|
1400
|
+
zoom_factor_detected: z2.number(),
|
|
1401
|
+
usage: z2.unknown().optional()
|
|
1402
|
+
}).passthrough();
|
|
1403
|
+
var FontJobSchema = z2.object({
|
|
1404
|
+
job_id: z2.string().min(1),
|
|
1405
|
+
status: z2.string(),
|
|
1406
|
+
glyph_px: z2.number().int().nullable().optional(),
|
|
1407
|
+
download_atlas_url: z2.string().nullable().optional(),
|
|
1408
|
+
download_ttf_url: z2.string().nullable().optional(),
|
|
1409
|
+
usage: z2.unknown().optional()
|
|
1410
|
+
}).passthrough();
|
|
1351
1411
|
var SelectFramesSchema = z2.object({ created_object_ids: z2.array(z2.string()) }).passthrough();
|
|
1352
1412
|
var CharacterSubmitSchema = z2.object({
|
|
1353
1413
|
background_job_id: z2.string().min(1),
|
|
@@ -1432,6 +1492,17 @@ function validateResponse(schema, raw, operation) {
|
|
|
1432
1492
|
const issues = parsed.error.issues.slice(0, 4).map((i) => `${i.path.join(".") || "response"}: ${i.message}`).join("; ");
|
|
1433
1493
|
throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
|
|
1434
1494
|
}
|
|
1495
|
+
function cleanupImages(args, operation) {
|
|
1496
|
+
const images = args.images ?? (args.image ? [args.image] : []);
|
|
1497
|
+
if (!images.length) throw new Error(`${operation} needs at least one image`);
|
|
1498
|
+
return images;
|
|
1499
|
+
}
|
|
1500
|
+
function cleanupResults(images, sent, operation) {
|
|
1501
|
+
if (images.length !== sent) {
|
|
1502
|
+
throw new Error(`Invalid PixelLab response for ${operation}: sent ${sent} image(s), got ${images.length} back`);
|
|
1503
|
+
}
|
|
1504
|
+
return images.map((image) => Buffer.from(image.base64, "base64"));
|
|
1505
|
+
}
|
|
1435
1506
|
var PixelLabError = class extends ProviderError {
|
|
1436
1507
|
constructor(message6, status, body) {
|
|
1437
1508
|
super("pixellab", message6, { status });
|
|
@@ -1460,9 +1531,9 @@ var PixelLabClient = class {
|
|
|
1460
1531
|
apiKey;
|
|
1461
1532
|
http;
|
|
1462
1533
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
1463
|
-
async request(
|
|
1534
|
+
async request(path34, init) {
|
|
1464
1535
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
1465
|
-
const res = await this.http(`${BASE}${
|
|
1536
|
+
const res = await this.http(`${BASE}${path34}`, {
|
|
1466
1537
|
...init,
|
|
1467
1538
|
headers: {
|
|
1468
1539
|
Authorization: auth,
|
|
@@ -1472,13 +1543,13 @@ var PixelLabClient = class {
|
|
|
1472
1543
|
});
|
|
1473
1544
|
const text = await res.text();
|
|
1474
1545
|
if (!res.ok) {
|
|
1475
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
1546
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path34} \u2192 ${res.status}`, res.status, text);
|
|
1476
1547
|
}
|
|
1477
1548
|
if (!text) return {};
|
|
1478
1549
|
try {
|
|
1479
1550
|
return JSON.parse(text);
|
|
1480
1551
|
} catch {
|
|
1481
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
1552
|
+
throw new Error(`${init?.method ?? "GET"} ${path34} returned invalid JSON`);
|
|
1482
1553
|
}
|
|
1483
1554
|
}
|
|
1484
1555
|
async balance() {
|
|
@@ -1864,10 +1935,10 @@ var PixelLabClient = class {
|
|
|
1864
1935
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
1865
1936
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
1866
1937
|
const south = args.reference?.south;
|
|
1867
|
-
let
|
|
1938
|
+
let path34;
|
|
1868
1939
|
let body;
|
|
1869
1940
|
if (args.mode === "v3") {
|
|
1870
|
-
|
|
1941
|
+
path34 = "/create-character-v3";
|
|
1871
1942
|
body = {
|
|
1872
1943
|
description: args.description,
|
|
1873
1944
|
image_size: imageSize,
|
|
@@ -1881,7 +1952,7 @@ var PixelLabClient = class {
|
|
|
1881
1952
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
1882
1953
|
};
|
|
1883
1954
|
} else if (args.mode === "pro-flash") {
|
|
1884
|
-
|
|
1955
|
+
path34 = "/create-character-pro-flash";
|
|
1885
1956
|
const traits = args.styleTraits;
|
|
1886
1957
|
body = {
|
|
1887
1958
|
description: args.description,
|
|
@@ -1904,7 +1975,7 @@ var PixelLabClient = class {
|
|
|
1904
1975
|
} : {}
|
|
1905
1976
|
};
|
|
1906
1977
|
} else if (args.mode === "pro") {
|
|
1907
|
-
|
|
1978
|
+
path34 = "/create-character-pro";
|
|
1908
1979
|
body = {
|
|
1909
1980
|
description: args.description,
|
|
1910
1981
|
image_size: imageSize,
|
|
@@ -1918,7 +1989,7 @@ var PixelLabClient = class {
|
|
|
1918
1989
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
1919
1990
|
};
|
|
1920
1991
|
} else {
|
|
1921
|
-
|
|
1992
|
+
path34 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
1922
1993
|
body = {
|
|
1923
1994
|
description: args.description,
|
|
1924
1995
|
image_size: imageSize,
|
|
@@ -1935,8 +2006,8 @@ var PixelLabClient = class {
|
|
|
1935
2006
|
...palette
|
|
1936
2007
|
};
|
|
1937
2008
|
}
|
|
1938
|
-
const raw = await this.request(
|
|
1939
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
2009
|
+
const raw = await this.request(path34, { method: "POST", body: JSON.stringify(body) });
|
|
2010
|
+
return validateResponse(CharacterSubmitSchema, raw, path34.slice(1));
|
|
1940
2011
|
}
|
|
1941
2012
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
1942
2013
|
async createCharacterState(args) {
|
|
@@ -2098,20 +2169,52 @@ var PixelLabClient = class {
|
|
|
2098
2169
|
);
|
|
2099
2170
|
}
|
|
2100
2171
|
/**
|
|
2101
|
-
* `/
|
|
2102
|
-
*
|
|
2103
|
-
*
|
|
2104
|
-
*
|
|
2105
|
-
*
|
|
2106
|
-
*
|
|
2107
|
-
*
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2172
|
+
* `/generate-ui-v2` ("Generate UI (Pro)"): one UI element (a button, a
|
|
2173
|
+
* health bar, an inventory slot, a dialogue box) from a description,
|
|
2174
|
+
* guided by an optional concept image and a natural-language color
|
|
2175
|
+
* palette. A plain background job; unlike `/create-ui-asset` there is no
|
|
2176
|
+
* piece/element layout, and sizes start at 16px instead of 192. Its
|
|
2177
|
+
* completed shape has not been exercised live; `pollUiElement` reads it
|
|
2178
|
+
* defensively.
|
|
2179
|
+
*/
|
|
2180
|
+
async generateUiV2(args) {
|
|
2181
|
+
const body = {
|
|
2182
|
+
description: args.description,
|
|
2183
|
+
image_size: { width: args.width, height: args.height }
|
|
2184
|
+
};
|
|
2185
|
+
if (args.conceptImage) {
|
|
2186
|
+
body.concept_image = {
|
|
2187
|
+
image: args.conceptImage.image,
|
|
2188
|
+
size: { width: args.conceptImage.width, height: args.conceptImage.height }
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
if (args.colorPalette) body.color_palette = args.colorPalette;
|
|
2192
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
2193
|
+
if (args.seed != null) body.seed = args.seed;
|
|
2194
|
+
return validateResponse(
|
|
2195
|
+
RevisionJobSubmitSchema,
|
|
2196
|
+
await this.request("/generate-ui-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
2197
|
+
"generate-ui-v2"
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* `/reduce-colors`, PixelLab's "Cleanup" tier: quantize one image, or
|
|
2202
|
+
* several same-size frames together onto ONE shared palette, synchronously
|
|
2203
|
+
* — no `background_job_id`, the result comes back in this same response,
|
|
2204
|
+
* like `createImagePixflux`. Passing every frame of an animation (or every
|
|
2205
|
+
* direction of a character) in one call is the endpoint's whole point:
|
|
2206
|
+
* frames quantized separately drift onto different palettes. The schema's
|
|
2207
|
+
* own response example is dollar-denominated (`usage: {type: "usd", usd:
|
|
2208
|
+
* 0.02}`), which — matching this codebase's repeated experience with
|
|
2209
|
+
* PixelLab's documented-vs-billed cost mismatches (`isometricTile`,
|
|
2210
|
+
* `objectPro`) — did not hold: confirmed live against a Tier 2 account at
|
|
2211
|
+
* exactly 0.1 generations for a 32x32 source (docs/REVISIONS.md).
|
|
2212
|
+
* `numColors` and `paletteImage` are mutually exclusive upstream; the
|
|
2213
|
+
* manifest schema already enforces that before this is ever called.
|
|
2112
2214
|
*/
|
|
2113
2215
|
async reduceColors(args) {
|
|
2114
|
-
const
|
|
2216
|
+
const images = cleanupImages(args, "reduce-colors");
|
|
2217
|
+
const body = { images };
|
|
2115
2218
|
if (args.numColors != null) body.num_colors = args.numColors;
|
|
2116
2219
|
if (args.paletteImage) body.palette_image = args.paletteImage;
|
|
2117
2220
|
if (args.dithering) body.dithering = args.dithering;
|
|
@@ -2121,8 +2224,10 @@ var PixelLabClient = class {
|
|
|
2121
2224
|
await this.request("/reduce-colors", { method: "POST", body: JSON.stringify(body) }),
|
|
2122
2225
|
"reduce-colors"
|
|
2123
2226
|
);
|
|
2227
|
+
const pngs = cleanupResults(res.images, images.length, "reduce-colors");
|
|
2124
2228
|
return {
|
|
2125
|
-
png:
|
|
2229
|
+
png: pngs[0],
|
|
2230
|
+
pngs,
|
|
2126
2231
|
paletteStripPng: Buffer.from(res.palette.base64, "base64"),
|
|
2127
2232
|
nColors: res.n_colors,
|
|
2128
2233
|
usage: res.usage
|
|
@@ -2131,19 +2236,117 @@ var PixelLabClient = class {
|
|
|
2131
2236
|
/**
|
|
2132
2237
|
* `/correct-pixelart`, PixelLab's "Cleanup" tier: sharpen edges and drop
|
|
2133
2238
|
* stray pixels without resizing, synchronously — same shape as
|
|
2134
|
-
* `reduceColors` above, no background job
|
|
2135
|
-
*
|
|
2239
|
+
* `reduceColors` above, no background job, and likewise corrects several
|
|
2240
|
+
* same-size frames together when given `images`. Cost is confirmed live
|
|
2241
|
+
* at 0.1 generations for a 32x32 source, not the schema's own
|
|
2136
2242
|
* dollar-denominated example (`usage: {type: "usd", usd: 0.02}`).
|
|
2137
2243
|
*/
|
|
2138
2244
|
async correctPixelart(args) {
|
|
2139
|
-
const
|
|
2245
|
+
const images = cleanupImages(args, "correct-pixelart");
|
|
2246
|
+
const body = { images };
|
|
2140
2247
|
if (args.strength != null) body.strength = args.strength;
|
|
2141
2248
|
const res = validateResponse(
|
|
2142
2249
|
CorrectPixelartResponseSchema,
|
|
2143
2250
|
await this.request("/correct-pixelart", { method: "POST", body: JSON.stringify(body) }),
|
|
2144
2251
|
"correct-pixelart"
|
|
2145
2252
|
);
|
|
2146
|
-
|
|
2253
|
+
const pngs = cleanupResults(res.images, images.length, "correct-pixelart");
|
|
2254
|
+
return { png: pngs[0], pngs, usage: res.usage };
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* `/unzoom`: recover native-resolution pixel art from an upscaled image (a
|
|
2258
|
+
* 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
|
|
2259
|
+
* PixelLab's own API overview names this "the most common cause of
|
|
2260
|
+
* disappointing output": run it on outside artwork before it becomes a
|
|
2261
|
+
* style or reference image. The input must be at least 256x256 and at
|
|
2262
|
+
* most 2048x2048 in area, and the result is opaque — transparency is
|
|
2263
|
+
* composited onto white before the grid is detected. `quantize` is 0 to
|
|
2264
|
+
* auto-detect a palette, -1 to keep every color, or 2-256 for exactly
|
|
2265
|
+
* that many.
|
|
2266
|
+
*/
|
|
2267
|
+
async unzoom(args) {
|
|
2268
|
+
const body = { image: args.image };
|
|
2269
|
+
if (args.quantize != null) body.quantize = args.quantize;
|
|
2270
|
+
const res = validateResponse(
|
|
2271
|
+
UnzoomResponseSchema,
|
|
2272
|
+
await this.request("/unzoom", { method: "POST", body: JSON.stringify(body) }),
|
|
2273
|
+
"unzoom"
|
|
2274
|
+
);
|
|
2275
|
+
return {
|
|
2276
|
+
png: Buffer.from(res.image.base64, "base64"),
|
|
2277
|
+
originalSize: { width: res.original_size.width, height: res.original_size.height },
|
|
2278
|
+
unzoomedSize: { width: res.unzoomed_size.width, height: res.unzoomed_size.height },
|
|
2279
|
+
zoomFactor: res.zoom_factor_detected,
|
|
2280
|
+
usage: res.usage
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* `/interpolation-v2` ("Interpolate (Pro)"): generate the in-between
|
|
2285
|
+
* frames from one keyframe to another, guided by a short `action`. Unlike
|
|
2286
|
+
* `last_frame` on `/animate-with-text-v3`, both ends are required and the
|
|
2287
|
+
* frame count is the model's own choice (its docs say "typically 4-8").
|
|
2288
|
+
* Output frames are 16 to 128 pixels per side. A plain background job;
|
|
2289
|
+
* its completed shape is read defensively by `pollAnimateRevision`.
|
|
2290
|
+
*/
|
|
2291
|
+
async interpolationV2(args) {
|
|
2292
|
+
const size = { width: args.width, height: args.height };
|
|
2293
|
+
const body = {
|
|
2294
|
+
start_image: { image: args.startImage, size },
|
|
2295
|
+
end_image: { image: args.endImage, size },
|
|
2296
|
+
action: args.action,
|
|
2297
|
+
image_size: size
|
|
2298
|
+
};
|
|
2299
|
+
if (args.seed != null) body.seed = args.seed;
|
|
2300
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
2301
|
+
return validateResponse(
|
|
2302
|
+
RevisionJobSubmitSchema,
|
|
2303
|
+
await this.request("/interpolation-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
2304
|
+
"interpolation-v2"
|
|
2305
|
+
);
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* `/edit-animation-v2` ("Edit animation (Pro)"): apply one text edit
|
|
2309
|
+
* across every frame of an existing animation at once, so the change
|
|
2310
|
+
* stays consistent frame to frame. 2 to 16 frames, but the real ceiling
|
|
2311
|
+
* shrinks with frame size because every frame is packed into one grid:
|
|
2312
|
+
* 16 at up to 64px, 9 at 65-80px, 4 at 81-256px.
|
|
2313
|
+
*/
|
|
2314
|
+
async editAnimationV2(args) {
|
|
2315
|
+
const size = { width: args.width, height: args.height };
|
|
2316
|
+
const body = {
|
|
2317
|
+
description: args.description,
|
|
2318
|
+
frames: args.frames.map((image) => ({ image, size })),
|
|
2319
|
+
image_size: size
|
|
2320
|
+
};
|
|
2321
|
+
if (args.seed != null) body.seed = args.seed;
|
|
2322
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
2323
|
+
return validateResponse(
|
|
2324
|
+
RevisionJobSubmitSchema,
|
|
2325
|
+
await this.request("/edit-animation-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
2326
|
+
"edit-animation-v2"
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* `/generate-font-pro`: an 80-glyph pixel font (A-Z, a-z, 0-9, common
|
|
2331
|
+
* game-UI punctuation) from a style description, delivered as a glyph
|
|
2332
|
+
* atlas PNG plus a ready-to-use `.ttf`. Documented at 25 subscription
|
|
2333
|
+
* generations. Poll with `getFontJob`.
|
|
2334
|
+
*/
|
|
2335
|
+
async generateFontPro(args) {
|
|
2336
|
+
const body = { description: args.description, weight: args.weight };
|
|
2337
|
+
if (args.glyphPx != null) body.glyph_px = args.glyphPx;
|
|
2338
|
+
if (args.seed != null) body.seed = args.seed;
|
|
2339
|
+
if (args.fontName) body.font_name = args.fontName;
|
|
2340
|
+
return validateResponse(
|
|
2341
|
+
RevisionJobSubmitSchema,
|
|
2342
|
+
await this.request("/generate-font-pro", { method: "POST", body: JSON.stringify(body) }),
|
|
2343
|
+
"generate-font-pro"
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
/** `GET /generate-font-pro/{job_id}`: status, then no-auth atlas and `.ttf` URLs once completed. */
|
|
2347
|
+
async getFontJob(jobId) {
|
|
2348
|
+
const raw = await this.request(`/generate-font-pro/${encodeURIComponent(jobId)}`);
|
|
2349
|
+
return validateResponse(FontJobSchema, raw, "generate-font-pro/{job_id}");
|
|
2147
2350
|
}
|
|
2148
2351
|
/**
|
|
2149
2352
|
* `/animate-with-text-v3`: animate a loose image from a text description,
|
|
@@ -2804,10 +3007,10 @@ import { createReadStream } from "fs";
|
|
|
2804
3007
|
function sha256(data) {
|
|
2805
3008
|
return createHash("sha256").update(data).digest("hex");
|
|
2806
3009
|
}
|
|
2807
|
-
async function sha256File(
|
|
3010
|
+
async function sha256File(path34) {
|
|
2808
3011
|
return new Promise((resolve, reject) => {
|
|
2809
3012
|
const hash = createHash("sha256");
|
|
2810
|
-
const stream = createReadStream(
|
|
3013
|
+
const stream = createReadStream(path34);
|
|
2811
3014
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
2812
3015
|
stream.on("error", reject);
|
|
2813
3016
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -2835,7 +3038,7 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
2835
3038
|
// tile fields are undefined for every other generator. `tileSize` is
|
|
2836
3039
|
// intentionally absent; width/height are derived from it, so it is
|
|
2837
3040
|
// already covered.
|
|
2838
|
-
noBackground: spec.generator === "pixflux" || spec.generator === "uiAsset" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
3041
|
+
noBackground: spec.generator === "pixflux" || spec.generator === "uiAsset" || spec.generator === "uiElement" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
2839
3042
|
tileType: spec.tileType,
|
|
2840
3043
|
tileView: spec.tileView,
|
|
2841
3044
|
tileFeature: spec.tileFeature,
|
|
@@ -3042,7 +3245,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3042
3245
|
return new _PixelLabProvider(new PixelLabClient("download-only"));
|
|
3043
3246
|
}
|
|
3044
3247
|
supports(generator) {
|
|
3045
|
-
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset";
|
|
3248
|
+
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement";
|
|
3046
3249
|
}
|
|
3047
3250
|
/**
|
|
3048
3251
|
* `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
|
|
@@ -3052,12 +3255,15 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3052
3255
|
* rather than a described edit — and `animate`/`animate-pixminimax`
|
|
3053
3256
|
* (`/animate-with-text-v3`, `/animate-pixminimax`), which animate any loose
|
|
3054
3257
|
* image from a text description with no character/object resource
|
|
3055
|
-
* required
|
|
3056
|
-
*
|
|
3057
|
-
*
|
|
3258
|
+
* required — and `interpolate` (`/interpolation-v2`, the in-betweens
|
|
3259
|
+
* from one keyframe to another) and `edit-animation`
|
|
3260
|
+
* (`/edit-animation-v2`, one text edit applied across a whole frame set).
|
|
3261
|
+
* `outpaint` does not: there is no canvas-expansion endpoint in the API,
|
|
3262
|
+
* matching docs/REVISIONS.md's note that no provider ships a tested
|
|
3263
|
+
* outpaint path yet.
|
|
3058
3264
|
*/
|
|
3059
3265
|
supportsRevision(mode2) {
|
|
3060
|
-
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax";
|
|
3266
|
+
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "interpolate" || mode2 === "edit-animation";
|
|
3061
3267
|
}
|
|
3062
3268
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
3063
3269
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
@@ -3085,6 +3291,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3085
3291
|
if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
|
|
3086
3292
|
return { unit: "generations", amount: 0.1, candidates: 1 };
|
|
3087
3293
|
}
|
|
3294
|
+
if (spec.revision?.mode === "interpolate" || spec.revision?.mode === "edit-animation") {
|
|
3295
|
+
const width = spec.revision.sourceWidth ?? spec.width;
|
|
3296
|
+
const height = spec.revision.sourceHeight ?? spec.height;
|
|
3297
|
+
const side = spec.revision.mode === "edit-animation" ? Math.ceil(Math.sqrt(Math.max(1, spec.revision.sourceMembers?.length ?? 1))) : 1;
|
|
3298
|
+
return { unit: "generations", amount: generationCost(width * side, height * side, "1dir"), candidates: 1 };
|
|
3299
|
+
}
|
|
3088
3300
|
if (spec.revision?.mode === "animate" || spec.revision?.mode === "animate-pixminimax") {
|
|
3089
3301
|
const width = spec.revision.sourceWidth ?? spec.width;
|
|
3090
3302
|
const height = spec.revision.sourceHeight ?? spec.height;
|
|
@@ -3113,6 +3325,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3113
3325
|
};
|
|
3114
3326
|
}
|
|
3115
3327
|
validate(spec, styleImages) {
|
|
3328
|
+
const members = spec.revision?.sourceMembers;
|
|
3329
|
+
if (spec.revision && members && !MEMBER_SET_REVISION_MODES.includes(spec.revision.mode)) {
|
|
3330
|
+
throw new Error(
|
|
3331
|
+
`PixelLab ${spec.revision.mode} reads one source image; ${spec.revision.sourceAssetId} is a ${members.length}-member set (${members[0].role}, ...). Revise one member through a declared source, or use reduce-colors, correct-pixelart, or edit-animation, which take the whole set`
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3116
3334
|
if (spec.revision?.mode === "inpaint") {
|
|
3117
3335
|
const { sourceWidth: width, sourceHeight: height } = spec.revision;
|
|
3118
3336
|
if (width != null && height != null && (width < 32 || height < 32 || width > 512 || height > 512)) {
|
|
@@ -3121,9 +3339,10 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3121
3339
|
}
|
|
3122
3340
|
if (spec.revision?.mode === "reduce-colors") {
|
|
3123
3341
|
const { sourceWidth: width, sourceHeight: height } = spec.revision;
|
|
3124
|
-
|
|
3342
|
+
const count = members?.length ?? 1;
|
|
3343
|
+
if (width != null && height != null && width * height * count > 512 * 512) {
|
|
3125
3344
|
throw new Error(
|
|
3126
|
-
`PixelLab reduce-colors source is ${width}x${height} (${width * height}px\xB2); the API takes at most 512x512 worth of pixels per call`
|
|
3345
|
+
`PixelLab reduce-colors source is ${count > 1 ? `${count} frames of ` : ""}${width}x${height} (${width * height * count}px\xB2 in all); the API takes at most 512x512 worth of pixels per call`
|
|
3127
3346
|
);
|
|
3128
3347
|
}
|
|
3129
3348
|
}
|
|
@@ -3147,6 +3366,36 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3147
3366
|
);
|
|
3148
3367
|
}
|
|
3149
3368
|
}
|
|
3369
|
+
if (spec.revision?.mode === "interpolate") {
|
|
3370
|
+
const { sourceWidth: width, sourceHeight: height, lastFrameWidth, lastFrameHeight } = spec.revision;
|
|
3371
|
+
if (width != null && height != null && (width < 16 || height < 16 || width > 128 || height > 128)) {
|
|
3372
|
+
throw new Error(`PixelLab interpolate keyframes are ${width}x${height}; the API takes 16 to 128 pixels per side`);
|
|
3373
|
+
}
|
|
3374
|
+
if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
|
|
3375
|
+
throw new Error(
|
|
3376
|
+
`PixelLab interpolate lastFrame is ${lastFrameWidth}x${lastFrameHeight}; the start keyframe is ${width}x${height} \u2014 they must match`
|
|
3377
|
+
);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
if (spec.revision?.mode === "edit-animation") {
|
|
3381
|
+
const { sourceWidth: width, sourceHeight: height, sourceAssetId } = spec.revision;
|
|
3382
|
+
if (width != null && height != null) {
|
|
3383
|
+
if (!members || members.length < 2) {
|
|
3384
|
+
throw new Error(
|
|
3385
|
+
`PixelLab edit-animation needs a frame set to edit; ${sourceAssetId} is a single image (revise an animation, a character's directions, or another frame-set revision)`
|
|
3386
|
+
);
|
|
3387
|
+
}
|
|
3388
|
+
if (width < 16 || height < 16 || width > 256 || height > 256) {
|
|
3389
|
+
throw new Error(`PixelLab edit-animation frames are ${width}x${height}; the API takes 16 to 256 pixels per side`);
|
|
3390
|
+
}
|
|
3391
|
+
const ceiling = editAnimationFrameCeiling(width, height);
|
|
3392
|
+
if (members.length > ceiling) {
|
|
3393
|
+
throw new Error(
|
|
3394
|
+
`PixelLab edit-animation takes at most ${ceiling} frames at ${width}x${height} (every frame is packed into one grid); ${sourceAssetId} has ${members.length}`
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3150
3399
|
if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
|
|
3151
3400
|
throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
|
|
3152
3401
|
}
|
|
@@ -3163,6 +3412,14 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3163
3412
|
`PixelLab uiAsset is ${spec.width}x${spec.height}; the API takes 192 to 688 pixels per side (the exact ceiling also depends on aspect ratio \u2014 square tops out at 512, 16:9 at 688x384, 9:16 at 384x688, 4:3 at 600x448, 3:4 at 448x600)`
|
|
3164
3413
|
);
|
|
3165
3414
|
}
|
|
3415
|
+
if (spec.generator === "uiElement" && (spec.width < 16 || spec.height < 16 || spec.width > 792 || spec.height > 688)) {
|
|
3416
|
+
throw new Error(
|
|
3417
|
+
`PixelLab uiElement is ${spec.width}x${spec.height}; the API takes 16 to 792 wide and 16 to 688 tall (the exact ceiling depends on aspect ratio \u2014 square tops out at 512, 16:9 at 688x384)`
|
|
3418
|
+
);
|
|
3419
|
+
}
|
|
3420
|
+
if (spec.generator === "uiElement" && styleImages.length > 1) {
|
|
3421
|
+
throw new Error("PixelLab uiElement takes one concept image; list at most one styleImages entry");
|
|
3422
|
+
}
|
|
3166
3423
|
if (spec.generator === "map") {
|
|
3167
3424
|
requirePixelLabOption("map", "view", spec.view, ["low top-down", "high top-down", "side"]);
|
|
3168
3425
|
requirePixelLabOption("map", "outline", spec.outline, [
|
|
@@ -3246,8 +3503,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3246
3503
|
throw new Error(`PixelLab isometricTile: size must be 16 to 64 pixels, got ${spec.width}`);
|
|
3247
3504
|
}
|
|
3248
3505
|
}
|
|
3249
|
-
if (spec.generator === "character") this.validateCharacter(spec, styleImages);
|
|
3250
|
-
if (spec.generator === "objectPro") this.validateObjectPro(spec, styleImages);
|
|
3506
|
+
if (spec.generator === "character" && !spec.revision) this.validateCharacter(spec, styleImages);
|
|
3507
|
+
if (spec.generator === "objectPro" && !spec.revision) this.validateObjectPro(spec, styleImages);
|
|
3251
3508
|
if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
|
|
3252
3509
|
throw new Error(`PixelLab ${spec.generator} does not support style images`);
|
|
3253
3510
|
}
|
|
@@ -3814,6 +4071,19 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3814
4071
|
});
|
|
3815
4072
|
return { jobId: res2.ui_asset_id, metadata: { backgroundJobId: res2.background_job_id } };
|
|
3816
4073
|
}
|
|
4074
|
+
if (spec.generator === "uiElement") {
|
|
4075
|
+
const concept = styleImages[0];
|
|
4076
|
+
const res2 = await this.client.generateUiV2({
|
|
4077
|
+
description: spec.prompt,
|
|
4078
|
+
width: spec.width,
|
|
4079
|
+
height: spec.height,
|
|
4080
|
+
conceptImage: concept ? { image: { base64: concept.base64, format: concept.format }, width: concept.width, height: concept.height } : void 0,
|
|
4081
|
+
colorPalette: spec.uiColorPalette,
|
|
4082
|
+
noBackground: spec.noBackground,
|
|
4083
|
+
seed: spec.seed
|
|
4084
|
+
});
|
|
4085
|
+
return { jobId: res2.background_job_id };
|
|
4086
|
+
}
|
|
3817
4087
|
if (spec.generator === "pixflux") {
|
|
3818
4088
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
3819
4089
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -3942,13 +4212,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3942
4212
|
if (!revision.sourceSha256 || !revision.sourceFormat || revision.sourceWidth == null || revision.sourceHeight == null) {
|
|
3943
4213
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
|
|
3944
4214
|
}
|
|
3945
|
-
const
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
4215
|
+
const sourceFiles = revision.sourceMembers ?? [{ file: revision.sourceFile, sha256: revision.sourceSha256, format: revision.sourceFormat }];
|
|
4216
|
+
const images = sourceFiles.map((member) => {
|
|
4217
|
+
const bytes = readFileSync(member.file);
|
|
4218
|
+
if (sha256(bytes) !== member.sha256) {
|
|
4219
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
|
|
4220
|
+
}
|
|
4221
|
+
return { base64: bytes.toString("base64"), format: member.format };
|
|
4222
|
+
});
|
|
4223
|
+
const image = images[0];
|
|
3950
4224
|
const width = revision.sourceWidth;
|
|
3951
4225
|
const height = revision.sourceHeight;
|
|
4226
|
+
const roles = revision.sourceMembers?.map((member) => member.role);
|
|
3952
4227
|
if (revision.mode === "inpaint") {
|
|
3953
4228
|
if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
|
|
3954
4229
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
|
|
@@ -3981,34 +4256,45 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
3981
4256
|
paletteImage = { base64: paletteBytes.toString("base64"), format: revision.paletteImageFormat };
|
|
3982
4257
|
}
|
|
3983
4258
|
const res2 = await this.client.reduceColors({
|
|
3984
|
-
|
|
4259
|
+
images,
|
|
3985
4260
|
numColors: revision.numColors,
|
|
3986
4261
|
paletteImage,
|
|
3987
4262
|
dithering: revision.dithering,
|
|
3988
4263
|
ditheringStrength: revision.ditheringStrength
|
|
3989
4264
|
});
|
|
3990
|
-
|
|
3991
|
-
writeFileSync(path.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), res2.png);
|
|
3992
|
-
return { jobId, metadata: { revisionUsage: res2.usage } };
|
|
4265
|
+
return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
|
|
3993
4266
|
}
|
|
3994
4267
|
if (revision.mode === "correct-pixelart") {
|
|
3995
|
-
const res2 = await this.client.correctPixelart({
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
4268
|
+
const res2 = await this.client.correctPixelart({ images, strength: revision.strength });
|
|
4269
|
+
return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
|
|
4270
|
+
}
|
|
4271
|
+
if (revision.mode === "interpolate") {
|
|
4272
|
+
const lastFrame = this.readLastFrame(spec);
|
|
4273
|
+
if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
|
|
4274
|
+
const res2 = await this.client.interpolationV2({
|
|
4275
|
+
startImage: image,
|
|
4276
|
+
endImage: lastFrame,
|
|
4277
|
+
width,
|
|
4278
|
+
height,
|
|
4279
|
+
action: spec.prompt,
|
|
4280
|
+
seed: spec.seed,
|
|
4281
|
+
noBackground: spec.noBackground
|
|
4282
|
+
});
|
|
4283
|
+
return { jobId: res2.background_job_id };
|
|
4284
|
+
}
|
|
4285
|
+
if (revision.mode === "edit-animation") {
|
|
4286
|
+
const res2 = await this.client.editAnimationV2({
|
|
4287
|
+
frames: images,
|
|
4288
|
+
width,
|
|
4289
|
+
height,
|
|
4290
|
+
description: spec.prompt,
|
|
4291
|
+
seed: spec.seed,
|
|
4292
|
+
noBackground: spec.noBackground
|
|
4293
|
+
});
|
|
4294
|
+
return { jobId: res2.background_job_id };
|
|
3999
4295
|
}
|
|
4000
4296
|
if (revision.mode === "animate" || revision.mode === "animate-pixminimax") {
|
|
4001
|
-
|
|
4002
|
-
if (revision.lastFrameFile) {
|
|
4003
|
-
if (!revision.lastFrameSha256 || !revision.lastFrameFormat) {
|
|
4004
|
-
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame is not ready`);
|
|
4005
|
-
}
|
|
4006
|
-
const lastFrameBytes = readFileSync(revision.lastFrameFile);
|
|
4007
|
-
if (sha256(lastFrameBytes) !== revision.lastFrameSha256) {
|
|
4008
|
-
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame changed after the manifest was resolved`);
|
|
4009
|
-
}
|
|
4010
|
-
lastFrame = { base64: lastFrameBytes.toString("base64"), format: revision.lastFrameFormat };
|
|
4011
|
-
}
|
|
4297
|
+
const lastFrame = this.readLastFrame(spec);
|
|
4012
4298
|
if (revision.mode === "animate") {
|
|
4013
4299
|
const res3 = await this.client.animateWithTextV3({
|
|
4014
4300
|
firstFrame: image,
|
|
@@ -4048,6 +4334,36 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4048
4334
|
});
|
|
4049
4335
|
return { jobId: res.background_job_id };
|
|
4050
4336
|
}
|
|
4337
|
+
/** A revision's pinned ending frame, proven to be the bytes the spec hashed; undefined when none is declared. */
|
|
4338
|
+
readLastFrame(spec) {
|
|
4339
|
+
const revision = spec.revision;
|
|
4340
|
+
if (!revision.lastFrameFile) return void 0;
|
|
4341
|
+
if (!revision.lastFrameSha256 || !revision.lastFrameFormat) {
|
|
4342
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame is not ready`);
|
|
4343
|
+
}
|
|
4344
|
+
const bytes = readFileSync(revision.lastFrameFile);
|
|
4345
|
+
if (sha256(bytes) !== revision.lastFrameSha256) {
|
|
4346
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame changed after the manifest was resolved`);
|
|
4347
|
+
}
|
|
4348
|
+
return { base64: bytes.toString("base64"), format: revision.lastFrameFormat };
|
|
4349
|
+
}
|
|
4350
|
+
/**
|
|
4351
|
+
* Park a synchronous Cleanup-tier result the way pixflux parks its own:
|
|
4352
|
+
* one `<id>.png` for a single image, or `<id>-<n>.png` per member with the
|
|
4353
|
+
* member roles recorded so `pollCachedRevision` hands them back under the
|
|
4354
|
+
* same names the parent's set used.
|
|
4355
|
+
*/
|
|
4356
|
+
cacheCleanupResult(pngs, usage, roles, fps) {
|
|
4357
|
+
const jobId = randomUUID();
|
|
4358
|
+
if (!roles) {
|
|
4359
|
+
writeFileSync(path.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
|
|
4360
|
+
return { jobId, metadata: { revisionUsage: usage } };
|
|
4361
|
+
}
|
|
4362
|
+
for (const [index, png] of pngs.entries()) {
|
|
4363
|
+
writeFileSync(path.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
|
|
4364
|
+
}
|
|
4365
|
+
return { jobId, metadata: { revisionUsage: usage, revisionMembers: roles, ...fps != null ? { revisionFps: fps } : {} } };
|
|
4366
|
+
}
|
|
4051
4367
|
/**
|
|
4052
4368
|
* What a background job actually billed, once its own record is still
|
|
4053
4369
|
* around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
|
|
@@ -4068,7 +4384,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4068
4384
|
if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
|
|
4069
4385
|
return this.pollCachedRevision(jobId, context);
|
|
4070
4386
|
}
|
|
4071
|
-
if (revisionMode === "animate" || revisionMode === "animate-pixminimax") {
|
|
4387
|
+
if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
|
|
4072
4388
|
return this.pollAnimateRevision(jobId, context);
|
|
4073
4389
|
}
|
|
4074
4390
|
if (context?.spec?.revision) return this.pollRevision(jobId);
|
|
@@ -4091,6 +4407,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4091
4407
|
if (generator === "character") return this.pollCharacter(jobId, context);
|
|
4092
4408
|
if (generator === "objectPro") return this.pollObjectPro(jobId, context);
|
|
4093
4409
|
if (generator === "uiAsset") return this.pollUiAsset(jobId, context);
|
|
4410
|
+
if (generator === "uiElement") return this.pollUiElement(jobId);
|
|
4094
4411
|
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
4095
4412
|
const obj = await this.client.getObject(jobId);
|
|
4096
4413
|
if (obj.status === "review") {
|
|
@@ -4117,15 +4434,32 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4117
4434
|
* work.
|
|
4118
4435
|
*/
|
|
4119
4436
|
async pollCachedRevision(jobId, context) {
|
|
4120
|
-
const
|
|
4121
|
-
|
|
4437
|
+
const usage = context?.metadata?.revisionUsage;
|
|
4438
|
+
const roles = context?.metadata?.revisionMembers;
|
|
4439
|
+
const gone = {
|
|
4440
|
+
status: "failed",
|
|
4441
|
+
error: "revision result is no longer cached locally; re-run submit for this asset"
|
|
4442
|
+
};
|
|
4443
|
+
if (Array.isArray(roles) && roles.length) {
|
|
4444
|
+
const sources = roles.map((role, index) => ({
|
|
4445
|
+
url: `file://${path.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
|
|
4446
|
+
role: String(role)
|
|
4447
|
+
}));
|
|
4448
|
+
if (sources.some((source) => !existsSync(source.url.slice("file://".length)))) return gone;
|
|
4449
|
+
const fps = context?.metadata?.revisionFps;
|
|
4450
|
+
const frames = sources.every((source) => source.role.startsWith("frame-"));
|
|
4122
4451
|
return {
|
|
4123
|
-
status: "
|
|
4124
|
-
|
|
4452
|
+
status: "ready",
|
|
4453
|
+
objectId: jobId,
|
|
4454
|
+
sourceUrl: sources[0].url,
|
|
4455
|
+
sources,
|
|
4456
|
+
...frames && typeof fps === "number" ? { metadata: { frameSet: { fps, count: sources.length } } } : {},
|
|
4457
|
+
billed: billedFromUsage(usage)
|
|
4125
4458
|
};
|
|
4126
4459
|
}
|
|
4460
|
+
const file = path.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
4461
|
+
if (!existsSync(file)) return gone;
|
|
4127
4462
|
const sourceUrl = `file://${file}`;
|
|
4128
|
-
const usage = context?.metadata?.revisionUsage;
|
|
4129
4463
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
|
|
4130
4464
|
}
|
|
4131
4465
|
/**
|
|
@@ -4149,16 +4483,21 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4149
4483
|
if (job.status !== "completed") return { status: "processing" };
|
|
4150
4484
|
const billed = billedFromUsage(job.usage);
|
|
4151
4485
|
const done = job.last_response ?? {};
|
|
4152
|
-
const
|
|
4153
|
-
const
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
frameUrls,
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4486
|
+
const revision = context?.spec?.revision;
|
|
4487
|
+
const fps = revision?.fps ?? revision?.sourceFps ?? 8;
|
|
4488
|
+
const memberRoles = revision?.mode === "edit-animation" ? revision.sourceMembers?.map((member) => member.role) : void 0;
|
|
4489
|
+
const review = (frameUrls) => {
|
|
4490
|
+
const roles = memberRoles?.length === frameUrls.length ? memberRoles : frameUrls.map((_, index) => `frame-${String(index).padStart(2, "0")}`);
|
|
4491
|
+
return {
|
|
4492
|
+
status: "review-set",
|
|
4493
|
+
objectId: jobId,
|
|
4494
|
+
frameUrls,
|
|
4495
|
+
sources: frameUrls.map((url, index) => ({ url, role: roles[index] })),
|
|
4496
|
+
fps,
|
|
4497
|
+
metadata: roles.every((role) => role.startsWith("frame-")) ? { frameSet: { fps, count: frameUrls.length } } : {},
|
|
4498
|
+
billed
|
|
4499
|
+
};
|
|
4500
|
+
};
|
|
4162
4501
|
for (const key of ["frame_urls", "frames", "images"]) {
|
|
4163
4502
|
const list = done[key];
|
|
4164
4503
|
if (!Array.isArray(list) || !list.length) continue;
|
|
@@ -4190,7 +4529,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4190
4529
|
}
|
|
4191
4530
|
return {
|
|
4192
4531
|
status: "failed",
|
|
4193
|
-
error: `Invalid PixelLab response for animate job ${jobId}: completed with no recognized frame list (got: ${Object.keys(done).join(", ") || "no keys"}); update pollAnimateRevision in src/providers/pixellab.ts with the real shape`
|
|
4532
|
+
error: `Invalid PixelLab response for ${revision?.mode ?? "animate"} job ${jobId}: completed with no recognized frame list (got: ${Object.keys(done).join(", ") || "no keys"}); update pollAnimateRevision in src/providers/pixellab.ts with the real shape`
|
|
4194
4533
|
};
|
|
4195
4534
|
}
|
|
4196
4535
|
/**
|
|
@@ -4280,6 +4619,36 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4280
4619
|
error: `Invalid PixelLab response for imagePro job ${jobId}: completed with no recognized images array (got: ${Object.keys(done).join(", ") || "no keys"}); this endpoint's completed shape has not been exercised live yet -- update pollImagePro in src/providers/pixellab.ts with the real shape`
|
|
4281
4620
|
};
|
|
4282
4621
|
}
|
|
4622
|
+
/**
|
|
4623
|
+
* `/generate-ui-v2` completes a plain background job whose
|
|
4624
|
+
* `last_response.images` (per its own docs and Python client) holds the
|
|
4625
|
+
* result. One image is the expected case and goes straight to selected;
|
|
4626
|
+
* should the endpoint ever return several, they go to review like
|
|
4627
|
+
* imagePro's candidates, decoded to the same local cache. The shape has
|
|
4628
|
+
* not been observed live, so anything else fails naming the keys received.
|
|
4629
|
+
*/
|
|
4630
|
+
async pollUiElement(jobId) {
|
|
4631
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
4632
|
+
if (job.status === "failed") return { status: "failed", error: "UI element job failed upstream" };
|
|
4633
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
4634
|
+
const billed = billedFromUsage(job.usage);
|
|
4635
|
+
const done = job.last_response ?? {};
|
|
4636
|
+
const images = Array.isArray(done.images) ? done.images : done.image ? [done.image] : [];
|
|
4637
|
+
const urls = [];
|
|
4638
|
+
for (const [index, candidate] of images.entries()) {
|
|
4639
|
+
const base64 = extractBase64(candidate);
|
|
4640
|
+
if (!base64) continue;
|
|
4641
|
+
const file = path.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
4642
|
+
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
4643
|
+
urls.push(`file://${file}`);
|
|
4644
|
+
}
|
|
4645
|
+
if (urls.length === 1) return { status: "ready", objectId: jobId, sourceUrl: urls[0], sources: [{ url: urls[0] }], billed };
|
|
4646
|
+
if (urls.length > 1) return { status: "review", candidateUrls: urls, billed };
|
|
4647
|
+
return {
|
|
4648
|
+
status: "failed",
|
|
4649
|
+
error: `Invalid PixelLab response for uiElement job ${jobId}: completed with no recognized image (got: ${Object.keys(done).join(", ") || "no keys"}); update pollUiElement in src/providers/pixellab.ts with the real shape`
|
|
4650
|
+
};
|
|
4651
|
+
}
|
|
4283
4652
|
/**
|
|
4284
4653
|
* Map objects need their own path because the `/map-objects/{id}` record is
|
|
4285
4654
|
* deleted upstream roughly 8 hours after creation while the image survives in
|
|
@@ -4440,9 +4809,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4440
4809
|
if (!url) throw new Error(`tiles job ${jobId} has no variation at index ${index}`);
|
|
4441
4810
|
return { objectId: `${jobId}#${index}`, sourceUrl: url };
|
|
4442
4811
|
}
|
|
4443
|
-
if (generator === "imagePro") {
|
|
4812
|
+
if (generator === "imagePro" || generator === "uiElement") {
|
|
4444
4813
|
const file = path.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
4445
|
-
if (!existsSync(file)) throw new Error(
|
|
4814
|
+
if (!existsSync(file)) throw new Error(`${generator} job ${jobId} has no cached candidate at index ${index}`);
|
|
4446
4815
|
return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
|
|
4447
4816
|
}
|
|
4448
4817
|
const promoted = await this.client.selectFrames(jobId, [index], commonTag);
|
|
@@ -4596,6 +4965,12 @@ function readPose(spec, what, image) {
|
|
|
4596
4965
|
}
|
|
4597
4966
|
return { base64: bytes.toString("base64"), format: image.format };
|
|
4598
4967
|
}
|
|
4968
|
+
function editAnimationFrameCeiling(width, height) {
|
|
4969
|
+
const side = Math.max(width, height);
|
|
4970
|
+
if (side <= 64) return 16;
|
|
4971
|
+
if (side <= 80) return 9;
|
|
4972
|
+
return 4;
|
|
4973
|
+
}
|
|
4599
4974
|
function rotationSources(character) {
|
|
4600
4975
|
const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
4601
4976
|
const sources = [];
|
|
@@ -4688,9 +5063,9 @@ var RetroDiffusionClient = class {
|
|
|
4688
5063
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
4689
5064
|
return Buffer.from(await response.arrayBuffer());
|
|
4690
5065
|
}
|
|
4691
|
-
async call(
|
|
5066
|
+
async call(path34, init = {}) {
|
|
4692
5067
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
4693
|
-
const response = await this.request(`${this.baseUrl}${
|
|
5068
|
+
const response = await this.request(`${this.baseUrl}${path34}`, {
|
|
4694
5069
|
...init,
|
|
4695
5070
|
headers: {
|
|
4696
5071
|
"Content-Type": "application/json",
|
|
@@ -5732,14 +6107,15 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
5732
6107
|
}
|
|
5733
6108
|
/**
|
|
5734
6109
|
* Everything else resolves generically to whatever the user's own
|
|
5735
|
-
* workflow does with `bindings.sourceImage`.
|
|
5736
|
-
*
|
|
5737
|
-
*
|
|
5738
|
-
*
|
|
5739
|
-
*
|
|
6110
|
+
* workflow does with `bindings.sourceImage`. The frame-set modes
|
|
6111
|
+
* (`animate`, `animate-pixminimax`, `interpolate`, `edit-animation`) are
|
|
6112
|
+
* the exception: they produce an ordered frame set, and this adapter's
|
|
6113
|
+
* revision path always writes a single output image (see `submit`/`fetch`
|
|
6114
|
+
* below) — a real structural gap, not a missing binding, so they are
|
|
6115
|
+
* rejected here rather than only failing once `validate` runs.
|
|
5740
6116
|
*/
|
|
5741
6117
|
supportsRevision(mode2) {
|
|
5742
|
-
return mode2
|
|
6118
|
+
return !FRAME_SET_REVISION_MODES.includes(mode2);
|
|
5743
6119
|
}
|
|
5744
6120
|
estimate(spec) {
|
|
5745
6121
|
return {
|
|
@@ -5792,6 +6168,11 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
5792
6168
|
throw new Error("ComfyUI providerOptions.frames requires generator: frames");
|
|
5793
6169
|
}
|
|
5794
6170
|
if (spec.revision) {
|
|
6171
|
+
if (spec.revision.sourceMembers) {
|
|
6172
|
+
throw new Error(
|
|
6173
|
+
`ComfyUI revisions read one source image; ${spec.revision.sourceAssetId} is a ${spec.revision.sourceMembers.length}-member set (directions or frames)`
|
|
6174
|
+
);
|
|
6175
|
+
}
|
|
5795
6176
|
if (!options.bindings.sourceImage) {
|
|
5796
6177
|
throw new Error("ComfyUI revisions require bindings.sourceImage");
|
|
5797
6178
|
}
|
|
@@ -6626,6 +7007,91 @@ import { readFile as readFile3 } from "fs/promises";
|
|
|
6626
7007
|
import { existsSync as existsSync4 } from "fs";
|
|
6627
7008
|
import path5 from "path";
|
|
6628
7009
|
|
|
7010
|
+
// src/loop-directions.ts
|
|
7011
|
+
var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
|
|
7012
|
+
var MIRROR_FIELDS = ["category", "tags", "styles"];
|
|
7013
|
+
function loopDirectionId(shorthandId, direction) {
|
|
7014
|
+
return `${shorthandId}.${direction}`;
|
|
7015
|
+
}
|
|
7016
|
+
function expandLoopDirections(raw) {
|
|
7017
|
+
const families = {};
|
|
7018
|
+
const issues = [];
|
|
7019
|
+
if (!isObject2(raw) || !isObject2(raw.assets)) return { raw, families, issues };
|
|
7020
|
+
const assets = raw.assets;
|
|
7021
|
+
if (!Object.values(assets).some((asset) => isObject2(asset) && isObject2(asset.animation) && "directions" in asset.animation)) {
|
|
7022
|
+
return { raw, families, issues };
|
|
7023
|
+
}
|
|
7024
|
+
const expanded = {};
|
|
7025
|
+
const claimed = new Set(Object.keys(assets));
|
|
7026
|
+
for (const [id, asset] of Object.entries(assets)) {
|
|
7027
|
+
if (!isObject2(asset) || !isObject2(asset.animation) || !("directions" in asset.animation)) {
|
|
7028
|
+
expanded[id] = asset;
|
|
7029
|
+
continue;
|
|
7030
|
+
}
|
|
7031
|
+
const where = `assets.${id}.animation.directions`;
|
|
7032
|
+
const { directions: rawDirections, ...animation } = asset.animation;
|
|
7033
|
+
const parsed = CharacterDirectionSchema.array().min(1).safeParse(rawDirections);
|
|
7034
|
+
if (!parsed.success) {
|
|
7035
|
+
issues.push(`${where}: expected a non-empty list of directions (${CharacterDirectionSchema.options.join(", ")})`);
|
|
7036
|
+
continue;
|
|
7037
|
+
}
|
|
7038
|
+
const directions = parsed.data;
|
|
7039
|
+
const duplicate = directions.find((direction, index) => directions.indexOf(direction) !== index);
|
|
7040
|
+
if (duplicate) {
|
|
7041
|
+
issues.push(`${where}: "${duplicate}" is listed twice`);
|
|
7042
|
+
continue;
|
|
7043
|
+
}
|
|
7044
|
+
if ("direction" in animation) {
|
|
7045
|
+
issues.push(`assets.${id}.animation: direction and directions are mutually exclusive; directions names every loop to make`);
|
|
7046
|
+
continue;
|
|
7047
|
+
}
|
|
7048
|
+
const single = SINGLE_OUTPUT_FIELDS.filter((field) => field in asset);
|
|
7049
|
+
if (single.length) {
|
|
7050
|
+
issues.push(
|
|
7051
|
+
`assets.${id}: ${single.join(", ")} name${single.length === 1 ? "s" : ""} one output, but directions makes ${directions.length} loops; declare the loops explicitly to set ${single.length === 1 ? "it" : "them"} per direction`
|
|
7052
|
+
);
|
|
7053
|
+
continue;
|
|
7054
|
+
}
|
|
7055
|
+
const loops = directions.map((direction) => ({ id: loopDirectionId(id, direction), direction }));
|
|
7056
|
+
const named = new Set(directions);
|
|
7057
|
+
const mirrors = [];
|
|
7058
|
+
for (const { id: loopId, direction } of loops) {
|
|
7059
|
+
const flipped = MIRRORED_DIRECTION[direction];
|
|
7060
|
+
if (flipped === direction || named.has(flipped)) continue;
|
|
7061
|
+
named.add(flipped);
|
|
7062
|
+
mirrors.push({ id: loopDirectionId(id, flipped), of: loopId });
|
|
7063
|
+
}
|
|
7064
|
+
const ids = [...loops.map((loop) => loop.id), ...mirrors.map((mirror) => mirror.id)];
|
|
7065
|
+
const collision = ids.find((expandedId) => claimed.has(expandedId));
|
|
7066
|
+
if (collision) {
|
|
7067
|
+
issues.push(`${where}: expands to "${collision}", which is already declared`);
|
|
7068
|
+
continue;
|
|
7069
|
+
}
|
|
7070
|
+
for (const expandedId of ids) claimed.add(expandedId);
|
|
7071
|
+
for (const loop of loops) {
|
|
7072
|
+
expanded[loop.id] = { ...asset, animation: { ...animation, direction: loop.direction } };
|
|
7073
|
+
}
|
|
7074
|
+
for (const mirror of mirrors) {
|
|
7075
|
+
const inherited = {};
|
|
7076
|
+
for (const field of MIRROR_FIELDS) if (field in asset) inherited[field] = asset[field];
|
|
7077
|
+
expanded[mirror.id] = { mirror: mirror.of, ...inherited };
|
|
7078
|
+
}
|
|
7079
|
+
families[id] = ids;
|
|
7080
|
+
}
|
|
7081
|
+
return { raw: { ...raw, assets: expanded }, families, issues };
|
|
7082
|
+
}
|
|
7083
|
+
function expandAssetFilter(ids, families) {
|
|
7084
|
+
if (!families) return [...ids];
|
|
7085
|
+
return [...new Set(ids.flatMap((id) => families[id] ?? [id]))];
|
|
7086
|
+
}
|
|
7087
|
+
function loopShorthandOf(assetId, families) {
|
|
7088
|
+
if (!families) return void 0;
|
|
7089
|
+
return Object.keys(families).find((id) => families[id].includes(assetId));
|
|
7090
|
+
}
|
|
7091
|
+
function isObject2(value) {
|
|
7092
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7093
|
+
}
|
|
7094
|
+
|
|
6629
7095
|
// src/outputs.ts
|
|
6630
7096
|
import { existsSync as existsSync3 } from "fs";
|
|
6631
7097
|
import path4 from "path";
|
|
@@ -6944,7 +7410,12 @@ function frameSetFps(entry) {
|
|
|
6944
7410
|
async function loadManifest(manifestPath) {
|
|
6945
7411
|
const abs = path5.resolve(manifestPath);
|
|
6946
7412
|
if (!existsSync4(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
6947
|
-
const
|
|
7413
|
+
const expansion = expandLoopDirections(JSON.parse(await readFile3(abs, "utf8")));
|
|
7414
|
+
if (expansion.issues.length) {
|
|
7415
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
7416
|
+
${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
|
|
7417
|
+
}
|
|
7418
|
+
const input = ManifestInputSchema.safeParse(expansion.raw);
|
|
6948
7419
|
if (!input.success) {
|
|
6949
7420
|
const issues = formatManifestIssues(input.error.issues);
|
|
6950
7421
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
@@ -7029,7 +7500,8 @@ ${issues}`);
|
|
|
7029
7500
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
7030
7501
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
7031
7502
|
}
|
|
7032
|
-
|
|
7503
|
+
const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
|
|
7504
|
+
return { manifest: parsed.data, root: path5.dirname(abs), path: abs, ...loopFamilies };
|
|
7033
7505
|
}
|
|
7034
7506
|
function parentAssetId(asset) {
|
|
7035
7507
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
|
|
@@ -7113,12 +7585,13 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7113
7585
|
);
|
|
7114
7586
|
}
|
|
7115
7587
|
}
|
|
7116
|
-
|
|
7588
|
+
const assetFilter = expandAssetFilter(filter?.assets ?? [], loaded.loopFamilies);
|
|
7589
|
+
for (const unknownAsset of assetFilter) {
|
|
7117
7590
|
if (!manifest.assets[unknownAsset]) {
|
|
7118
7591
|
throw new UsageError(`Unknown asset "${unknownAsset}".`);
|
|
7119
7592
|
}
|
|
7120
7593
|
}
|
|
7121
|
-
const requestedAssetIds = new Set(
|
|
7594
|
+
const requestedAssetIds = new Set(assetFilter.length ? assetFilter : Object.keys(manifest.assets));
|
|
7122
7595
|
const resolutionAssetIds = new Set(requestedAssetIds);
|
|
7123
7596
|
for (const assetId of [...requestedAssetIds]) {
|
|
7124
7597
|
let current = parentAssetId(manifest.assets[assetId]);
|
|
@@ -7213,7 +7686,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7213
7686
|
size = asset.size ?? style.size ?? 64;
|
|
7214
7687
|
width = size;
|
|
7215
7688
|
height = size;
|
|
7216
|
-
} else if (generator === "uiAsset") {
|
|
7689
|
+
} else if (generator === "uiAsset" || generator === "uiElement") {
|
|
7217
7690
|
width = asset.width ?? style.size ?? 256;
|
|
7218
7691
|
height = asset.height ?? style.size ?? 256;
|
|
7219
7692
|
size = Math.max(width, height);
|
|
@@ -7280,7 +7753,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7280
7753
|
seed: style.seed,
|
|
7281
7754
|
uiPieces: generator === "uiAsset" ? asset.pieces : void 0,
|
|
7282
7755
|
uiElements: generator === "uiAsset" ? asset.elements : void 0,
|
|
7283
|
-
uiColorPalette: generator === "uiAsset" ? style.uiColorPalette : void 0,
|
|
7756
|
+
uiColorPalette: generator === "uiAsset" || generator === "uiElement" ? style.uiColorPalette : void 0,
|
|
7284
7757
|
palette: style.palette,
|
|
7285
7758
|
enforcePalette: style.enforcePalette,
|
|
7286
7759
|
noBackground: style.noBackground,
|
|
@@ -7320,8 +7793,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7320
7793
|
terrainView: generator === "terrain" ? style.terrainView : void 0,
|
|
7321
7794
|
isometricTileSize: generator === "isometricTile" ? style.isometricTileSize : void 0,
|
|
7322
7795
|
isometricTileShape: generator === "isometricTile" ? style.isometricTileShape : void 0,
|
|
7323
|
-
|
|
7324
|
-
|
|
7796
|
+
// A revision in a character or objectPro style is a provider edit of
|
|
7797
|
+
// its parent's pixels, not a new character: giving it a base shape
|
|
7798
|
+
// would have packing, the gallery, and adoption treat it as one.
|
|
7799
|
+
...generator === "character" && !asset.revision ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
7800
|
+
...generator === "objectPro" && !asset.revision ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
7325
7801
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
|
|
7326
7802
|
costUnit: "generations",
|
|
7327
7803
|
candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
|
|
@@ -7472,8 +7948,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7472
7948
|
);
|
|
7473
7949
|
}
|
|
7474
7950
|
const sourceSpec = await finalize(asset.revision.from);
|
|
7475
|
-
const
|
|
7476
|
-
const
|
|
7951
|
+
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path5.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
7952
|
+
const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
|
|
7953
|
+
const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
|
|
7954
|
+
const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
|
|
7955
|
+
const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
|
|
7477
7956
|
const maskFile = asset.revision.mask ? path5.resolve(root, asset.revision.mask) : void 0;
|
|
7478
7957
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
7479
7958
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
@@ -7494,6 +7973,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
7494
7973
|
sourceHeight: sourceImage?.height ?? null,
|
|
7495
7974
|
sourceFormat: sourceImage?.format ?? null,
|
|
7496
7975
|
sourceSpec,
|
|
7976
|
+
...sourceMembers ? { sourceMembers } : {},
|
|
7977
|
+
...sourceMembers && sourceFps != null ? { sourceFps } : {},
|
|
7497
7978
|
...maskFile ? {
|
|
7498
7979
|
maskFile,
|
|
7499
7980
|
maskSha256: maskImage?.hash ?? null,
|
|
@@ -7643,6 +8124,38 @@ async function optionalRevisionImage(file, label, requiredFormat) {
|
|
|
7643
8124
|
}
|
|
7644
8125
|
return { hash: sha256(bytes), ...metadata };
|
|
7645
8126
|
}
|
|
8127
|
+
async function revisionSourceMembers(stem, label) {
|
|
8128
|
+
if (existsSync4(stem)) return null;
|
|
8129
|
+
const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
|
|
8130
|
+
let roles = [];
|
|
8131
|
+
for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
|
|
8132
|
+
if (set.every((direction) => existsSync4(at(direction)))) {
|
|
8133
|
+
roles = [...set];
|
|
8134
|
+
break;
|
|
8135
|
+
}
|
|
8136
|
+
}
|
|
8137
|
+
if (!roles.length) {
|
|
8138
|
+
for (let index = 0; existsSync4(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
|
|
8139
|
+
roles.push(`frame-${String(index).padStart(2, "0")}`);
|
|
8140
|
+
}
|
|
8141
|
+
}
|
|
8142
|
+
if (!roles.length) return null;
|
|
8143
|
+
const members = [];
|
|
8144
|
+
for (const role of roles) {
|
|
8145
|
+
const file = at(role);
|
|
8146
|
+
const image = await optionalRevisionImage(file, `${label} (${role})`);
|
|
8147
|
+
if (!image) return null;
|
|
8148
|
+
members.push({ role, file, sha256: image.hash, width: image.width, height: image.height, format: image.format });
|
|
8149
|
+
}
|
|
8150
|
+
const first = members[0];
|
|
8151
|
+
const odd = members.find((member) => member.width !== first.width || member.height !== first.height);
|
|
8152
|
+
if (odd) {
|
|
8153
|
+
throw new Error(
|
|
8154
|
+
`${label} is a set whose members differ in size (${first.role} is ${first.width}x${first.height}, ${odd.role} is ${odd.width}x${odd.height}); a revision reads every member as one same-size batch`
|
|
8155
|
+
);
|
|
8156
|
+
}
|
|
8157
|
+
return members;
|
|
8158
|
+
}
|
|
7646
8159
|
async function resolveStyleImages(loaded, styleId) {
|
|
7647
8160
|
const style = loaded.manifest.styles[styleId];
|
|
7648
8161
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
@@ -9325,11 +9838,16 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
9325
9838
|
reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
|
|
9326
9839
|
};
|
|
9327
9840
|
}
|
|
9328
|
-
if (
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9841
|
+
if (revision.sourceMembers) {
|
|
9842
|
+
const members = await inspectSourceMembers(spec, lock);
|
|
9843
|
+
if (!members.ready) return members;
|
|
9844
|
+
} else {
|
|
9845
|
+
if (!revision.sourceSha256 || !existsSync7(revision.sourceFile)) {
|
|
9846
|
+
return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
|
|
9847
|
+
}
|
|
9848
|
+
if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
|
|
9849
|
+
return { ready: false, reason: "revision source changed after the manifest was resolved" };
|
|
9850
|
+
}
|
|
9333
9851
|
}
|
|
9334
9852
|
if (revision.maskFile) {
|
|
9335
9853
|
if (!revision.maskSha256 || !existsSync7(revision.maskFile)) {
|
|
@@ -9339,8 +9857,39 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
9339
9857
|
return { ready: false, reason: "revision mask changed after the manifest was resolved" };
|
|
9340
9858
|
}
|
|
9341
9859
|
}
|
|
9860
|
+
if (revision.lastFrameFile) {
|
|
9861
|
+
if (!revision.lastFrameSha256 || !existsSync7(revision.lastFrameFile)) {
|
|
9862
|
+
return { ready: false, reason: `revision last frame is missing: ${revision.lastFrameFile}` };
|
|
9863
|
+
}
|
|
9864
|
+
if (await sha256File(revision.lastFrameFile) !== revision.lastFrameSha256) {
|
|
9865
|
+
return { ready: false, reason: "revision last frame changed after the manifest was resolved" };
|
|
9866
|
+
}
|
|
9867
|
+
}
|
|
9342
9868
|
return { ready: true, reason: "revision inputs are current" };
|
|
9343
9869
|
}
|
|
9870
|
+
async function inspectSourceMembers(spec, lock) {
|
|
9871
|
+
const revision = spec.revision;
|
|
9872
|
+
const members = revision.sourceMembers;
|
|
9873
|
+
for (const member of members) {
|
|
9874
|
+
if (!existsSync7(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
|
|
9875
|
+
if (await sha256File(member.file) !== member.sha256) {
|
|
9876
|
+
return { ready: false, reason: `revision source member changed after the manifest was resolved: ${member.file}` };
|
|
9877
|
+
}
|
|
9878
|
+
}
|
|
9879
|
+
const parent = revision.sourceSpec;
|
|
9880
|
+
if (parent.source) return { ready: true, reason: "declared source set is present" };
|
|
9881
|
+
const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
|
|
9882
|
+
if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
|
|
9883
|
+
const recorded = entry.outputs.map((_, index) => path11.resolve(currentEntryOutputPath(entry, parent, index))).sort();
|
|
9884
|
+
const found = members.map((member) => path11.resolve(member.file)).sort();
|
|
9885
|
+
if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
|
|
9886
|
+
return {
|
|
9887
|
+
ready: false,
|
|
9888
|
+
reason: `revision source ${parent.assetId} records ${recorded.length} output(s) but ${found.length} member file(s) were found beside it; remove files left over from an earlier generation, or re-fetch the parent`
|
|
9889
|
+
};
|
|
9890
|
+
}
|
|
9891
|
+
return { ready: true, reason: "revision source set is current" };
|
|
9892
|
+
}
|
|
9344
9893
|
async function requireRevisionReady(spec, lock) {
|
|
9345
9894
|
const inspection = await inspectRevisionReadiness(spec, lock);
|
|
9346
9895
|
if (inspection && !inspection.ready) throw new Error(inspection.reason);
|
|
@@ -10175,16 +10724,99 @@ function hex(c) {
|
|
|
10175
10724
|
return `#${[c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
10176
10725
|
}
|
|
10177
10726
|
|
|
10178
|
-
// src/
|
|
10179
|
-
import path16 from "path";
|
|
10727
|
+
// src/pixellab-utils.ts
|
|
10180
10728
|
import { existsSync as existsSync11 } from "fs";
|
|
10181
|
-
import { readFile as readFile9 } from "fs/promises";
|
|
10729
|
+
import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
10730
|
+
import path16 from "path";
|
|
10731
|
+
var UNZOOM_MIN_SIDE = 256;
|
|
10732
|
+
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
10733
|
+
function defaultUnzoomOut(input) {
|
|
10734
|
+
const ext = path16.extname(input);
|
|
10735
|
+
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
10736
|
+
}
|
|
10737
|
+
async function unzoomFile(client2, input, opts = {}) {
|
|
10738
|
+
const out = path16.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
10739
|
+
if (path16.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
10740
|
+
if (existsSync11(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
10741
|
+
const bytes = await readFile9(input);
|
|
10742
|
+
const metadata = imageMetadata(bytes);
|
|
10743
|
+
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
10744
|
+
const { width, height, format } = metadata;
|
|
10745
|
+
if (width < UNZOOM_MIN_SIDE || height < UNZOOM_MIN_SIDE) {
|
|
10746
|
+
throw new Error(
|
|
10747
|
+
`${input} is ${width}x${height}; PixelLab's unzoom needs at least ${UNZOOM_MIN_SIDE}x${UNZOOM_MIN_SIDE} to find the pixel grid, so this is probably already at its native size`
|
|
10748
|
+
);
|
|
10749
|
+
}
|
|
10750
|
+
if (width * height > UNZOOM_MAX_AREA) {
|
|
10751
|
+
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
10752
|
+
}
|
|
10753
|
+
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
10754
|
+
await mkdir4(path16.dirname(out), { recursive: true });
|
|
10755
|
+
await writeFile4(out, res.png);
|
|
10756
|
+
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
10757
|
+
}
|
|
10758
|
+
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
10759
|
+
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
10760
|
+
var FONT_COST_GENERATIONS = 25;
|
|
10761
|
+
function fontOutputPaths(out) {
|
|
10762
|
+
const base = path16.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
10763
|
+
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
10764
|
+
}
|
|
10765
|
+
async function generateFont(client2, opts) {
|
|
10766
|
+
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
10767
|
+
if (!opts.force) {
|
|
10768
|
+
for (const file of [ttf, atlas]) {
|
|
10769
|
+
if (existsSync11(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
10770
|
+
}
|
|
10771
|
+
}
|
|
10772
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
10773
|
+
const interval = opts.intervalMs ?? 5e3;
|
|
10774
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
10775
|
+
const submitted = await client2.generateFontPro({
|
|
10776
|
+
description: opts.description,
|
|
10777
|
+
weight: opts.weight,
|
|
10778
|
+
glyphPx: opts.glyphPx,
|
|
10779
|
+
seed: opts.seed,
|
|
10780
|
+
fontName: opts.fontName
|
|
10781
|
+
});
|
|
10782
|
+
const jobId = submitted.background_job_id;
|
|
10783
|
+
const started = Date.now();
|
|
10784
|
+
for (; ; ) {
|
|
10785
|
+
const job = await client2.getFontJob(jobId);
|
|
10786
|
+
opts.onProgress?.(job.status);
|
|
10787
|
+
if (job.status === "failed") throw new Error(`PixelLab font job ${jobId} failed upstream`);
|
|
10788
|
+
if (job.status === "completed") {
|
|
10789
|
+
if (!job.download_ttf_url || !job.download_atlas_url) {
|
|
10790
|
+
throw new Error(`PixelLab font job ${jobId} completed without both download URLs`);
|
|
10791
|
+
}
|
|
10792
|
+
const [ttfBytes, atlasBytes] = await Promise.all([
|
|
10793
|
+
client2.download(job.download_ttf_url),
|
|
10794
|
+
client2.download(job.download_atlas_url)
|
|
10795
|
+
]);
|
|
10796
|
+
await mkdir4(path16.dirname(ttf), { recursive: true });
|
|
10797
|
+
await writeFile4(ttf, ttfBytes);
|
|
10798
|
+
await writeFile4(atlas, atlasBytes);
|
|
10799
|
+
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
10800
|
+
}
|
|
10801
|
+
if (Date.now() - started > timeout) {
|
|
10802
|
+
throw new Error(
|
|
10803
|
+
`PixelLab font job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
10804
|
+
);
|
|
10805
|
+
}
|
|
10806
|
+
await sleep3(interval);
|
|
10807
|
+
}
|
|
10808
|
+
}
|
|
10809
|
+
|
|
10810
|
+
// src/pipeline/quality-regression.ts
|
|
10811
|
+
import path17 from "path";
|
|
10812
|
+
import { existsSync as existsSync12 } from "fs";
|
|
10813
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
10182
10814
|
import { z as z4 } from "zod";
|
|
10183
10815
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
10184
10816
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
10185
10817
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
10186
10818
|
var QualityPathSchema = z4.string().min(1).refine(
|
|
10187
|
-
(value) => !
|
|
10819
|
+
(value) => !path17.posix.isAbsolute(value) && !value.includes("\\"),
|
|
10188
10820
|
"expected a portable relative path"
|
|
10189
10821
|
);
|
|
10190
10822
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -10331,10 +10963,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
10331
10963
|
};
|
|
10332
10964
|
}
|
|
10333
10965
|
async function measureImageQuality(file) {
|
|
10334
|
-
const absolute =
|
|
10966
|
+
const absolute = path17.resolve(file);
|
|
10335
10967
|
let bytes;
|
|
10336
10968
|
try {
|
|
10337
|
-
bytes = await
|
|
10969
|
+
bytes = await readFile10(absolute);
|
|
10338
10970
|
} catch (error) {
|
|
10339
10971
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
10340
10972
|
}
|
|
@@ -10353,11 +10985,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
10353
10985
|
}
|
|
10354
10986
|
const ids = parsed.data.map((entry) => entry.id);
|
|
10355
10987
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
10356
|
-
const root =
|
|
10988
|
+
const root = path17.dirname(path17.resolve(inputsFilePath));
|
|
10357
10989
|
return parsed.data.map((entry) => ({
|
|
10358
10990
|
id: entry.id,
|
|
10359
|
-
file:
|
|
10360
|
-
...entry.record ? { record:
|
|
10991
|
+
file: path17.resolve(root, entry.path),
|
|
10992
|
+
...entry.record ? { record: path17.resolve(root, entry.record) } : {},
|
|
10361
10993
|
tolerances: QualityToleranceSchema.parse({
|
|
10362
10994
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
10363
10995
|
...entry.tolerances
|
|
@@ -10365,7 +10997,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
10365
10997
|
}));
|
|
10366
10998
|
}
|
|
10367
10999
|
function portableRelative2(from, to) {
|
|
10368
|
-
return
|
|
11000
|
+
return path17.relative(from, path17.resolve(to)).split(path17.sep).join("/") || ".";
|
|
10369
11001
|
}
|
|
10370
11002
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
10371
11003
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -10380,20 +11012,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
10380
11012
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
10381
11013
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
10382
11014
|
}
|
|
10383
|
-
const outputs = record.outputs.map((output) =>
|
|
10384
|
-
if (!outputs.includes(
|
|
11015
|
+
const outputs = record.outputs.map((output) => path17.resolve(path17.dirname(recordPath), output.path));
|
|
11016
|
+
if (!outputs.includes(path17.resolve(imagePath))) {
|
|
10385
11017
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
10386
11018
|
}
|
|
10387
11019
|
}
|
|
10388
11020
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
10389
11021
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
10390
|
-
const absolute =
|
|
11022
|
+
const absolute = path17.resolve(baselinePath);
|
|
10391
11023
|
for (const input of inputs) {
|
|
10392
|
-
if (
|
|
11024
|
+
if (path17.resolve(input.file) === absolute || input.record && path17.resolve(input.record) === absolute) {
|
|
10393
11025
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
10394
11026
|
}
|
|
10395
11027
|
}
|
|
10396
|
-
const root =
|
|
11028
|
+
const root = path17.dirname(absolute);
|
|
10397
11029
|
const cases = [];
|
|
10398
11030
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
10399
11031
|
const expected = await measureImageQuality(input.file);
|
|
@@ -10420,8 +11052,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
10420
11052
|
cases
|
|
10421
11053
|
});
|
|
10422
11054
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
10423
|
-
if (
|
|
10424
|
-
const current = await
|
|
11055
|
+
if (existsSync12(absolute) && !options.force) {
|
|
11056
|
+
const current = await readFile10(absolute);
|
|
10425
11057
|
if (!current.equals(data)) {
|
|
10426
11058
|
throw new OverwriteRefusedError(
|
|
10427
11059
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -10433,10 +11065,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
10433
11065
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
10434
11066
|
}
|
|
10435
11067
|
async function readQualityBaseline(baselinePath) {
|
|
10436
|
-
const absolute =
|
|
11068
|
+
const absolute = path17.resolve(baselinePath);
|
|
10437
11069
|
let raw;
|
|
10438
11070
|
try {
|
|
10439
|
-
raw = JSON.parse(await
|
|
11071
|
+
raw = JSON.parse(await readFile10(absolute, "utf8"));
|
|
10440
11072
|
} catch (error) {
|
|
10441
11073
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
10442
11074
|
}
|
|
@@ -10506,12 +11138,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
10506
11138
|
return { violations, warnings };
|
|
10507
11139
|
}
|
|
10508
11140
|
async function checkQualityBaseline(baselinePath) {
|
|
10509
|
-
const absolute =
|
|
11141
|
+
const absolute = path17.resolve(baselinePath);
|
|
10510
11142
|
const baseline = await readQualityBaseline(absolute);
|
|
10511
|
-
const root =
|
|
11143
|
+
const root = path17.dirname(absolute);
|
|
10512
11144
|
const cases = [];
|
|
10513
11145
|
for (const entry of baseline.cases) {
|
|
10514
|
-
const file =
|
|
11146
|
+
const file = path17.resolve(root, entry.file);
|
|
10515
11147
|
let actual = null;
|
|
10516
11148
|
const violations = [];
|
|
10517
11149
|
const warnings = [];
|
|
@@ -10524,7 +11156,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
10524
11156
|
violations.push(message4(error));
|
|
10525
11157
|
}
|
|
10526
11158
|
if (entry.record) {
|
|
10527
|
-
const recordPath =
|
|
11159
|
+
const recordPath = path17.resolve(root, entry.record.path);
|
|
10528
11160
|
try {
|
|
10529
11161
|
const recordHash = await sha256File(recordPath);
|
|
10530
11162
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -10560,13 +11192,13 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
10560
11192
|
}
|
|
10561
11193
|
|
|
10562
11194
|
// src/pipeline/cache-health.ts
|
|
10563
|
-
import { existsSync as
|
|
10564
|
-
import { readFile as
|
|
10565
|
-
import
|
|
11195
|
+
import { existsSync as existsSync16 } from "fs";
|
|
11196
|
+
import { readFile as readFile13, readdir as readdir2, rm as rm6 } from "fs/promises";
|
|
11197
|
+
import path20 from "path";
|
|
10566
11198
|
|
|
10567
11199
|
// src/cache.ts
|
|
10568
|
-
import { mkdir as
|
|
10569
|
-
import { existsSync as
|
|
11200
|
+
import { mkdir as mkdir5, readFile as readFile11, rename as rename4, rm as rm4, writeFile as writeFile5 } from "fs/promises";
|
|
11201
|
+
import { existsSync as existsSync13 } from "fs";
|
|
10570
11202
|
import pathModule from "path";
|
|
10571
11203
|
import { z as z5 } from "zod";
|
|
10572
11204
|
var HashCacheSchema = z5.object({
|
|
@@ -10581,10 +11213,10 @@ function isSha256Hash(value) {
|
|
|
10581
11213
|
function parseCache(value) {
|
|
10582
11214
|
return HashCacheSchema.parse(value);
|
|
10583
11215
|
}
|
|
10584
|
-
async function loadCache(
|
|
10585
|
-
if (!
|
|
11216
|
+
async function loadCache(path34) {
|
|
11217
|
+
if (!existsSync13(path34)) return { version: 1, hashes: {} };
|
|
10586
11218
|
try {
|
|
10587
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await
|
|
11219
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(path34, "utf8")));
|
|
10588
11220
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
10589
11221
|
return {
|
|
10590
11222
|
version: 1,
|
|
@@ -10596,18 +11228,18 @@ async function loadCache(path33) {
|
|
|
10596
11228
|
return { version: 1, hashes: {} };
|
|
10597
11229
|
}
|
|
10598
11230
|
}
|
|
10599
|
-
async function saveCache(
|
|
11231
|
+
async function saveCache(path34, cache) {
|
|
10600
11232
|
const sorted = {};
|
|
10601
11233
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
10602
11234
|
const hash = cache.hashes[key];
|
|
10603
11235
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
10604
11236
|
sorted[key] = hash;
|
|
10605
11237
|
}
|
|
10606
|
-
await
|
|
10607
|
-
const tmp = `${
|
|
11238
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path34)), { recursive: true });
|
|
11239
|
+
const tmp = `${path34}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
10608
11240
|
try {
|
|
10609
|
-
await
|
|
10610
|
-
await rename4(tmp,
|
|
11241
|
+
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
11242
|
+
await rename4(tmp, path34);
|
|
10611
11243
|
} finally {
|
|
10612
11244
|
await rm4(tmp, { force: true });
|
|
10613
11245
|
}
|
|
@@ -10631,20 +11263,20 @@ function cachePathFor(lockPath) {
|
|
|
10631
11263
|
}
|
|
10632
11264
|
|
|
10633
11265
|
// src/pipeline/history.ts
|
|
10634
|
-
import { existsSync as
|
|
10635
|
-
import
|
|
11266
|
+
import { existsSync as existsSync15 } from "fs";
|
|
11267
|
+
import path19 from "path";
|
|
10636
11268
|
|
|
10637
11269
|
// src/pipeline/fetch.ts
|
|
10638
|
-
import { existsSync as
|
|
10639
|
-
import { mkdir as
|
|
10640
|
-
import
|
|
11270
|
+
import { existsSync as existsSync14, readFileSync as readFileSync3 } from "fs";
|
|
11271
|
+
import { mkdir as mkdir6, readFile as readFile12, rename as rename5, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
11272
|
+
import path18 from "path";
|
|
10641
11273
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
10642
11274
|
const log = opts.onProgress ?? (() => {
|
|
10643
11275
|
});
|
|
10644
11276
|
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
10645
11277
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
10646
11278
|
normalizeLockOutputPaths(lock, specs);
|
|
10647
|
-
const cacheDir = opts.cacheDir === false ? null :
|
|
11279
|
+
const cacheDir = opts.cacheDir === false ? null : path18.resolve(opts.cacheDir ?? path18.join(path18.dirname(lockPath), ".pixelkiln", "cache"));
|
|
10648
11280
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
10649
11281
|
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
10650
11282
|
if (specByKey.get(key)?.mirror) return false;
|
|
@@ -10655,14 +11287,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
10655
11287
|
if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
|
|
10656
11288
|
return Boolean(opts.force) || e.outputs.every((output) => {
|
|
10657
11289
|
const file = resolveOutputPath(output.path, spec.root);
|
|
10658
|
-
return
|
|
11290
|
+
return existsSync14(file) && sha256(readFileSync3(file)) === output.sha256;
|
|
10659
11291
|
});
|
|
10660
11292
|
}
|
|
10661
11293
|
if (!opts.repair) return false;
|
|
10662
11294
|
if (e.outputs.length === 0 || !spec) return true;
|
|
10663
11295
|
return e.outputs.some((output) => {
|
|
10664
11296
|
const file = resolveOutputPath(output.path, spec.root);
|
|
10665
|
-
if (!
|
|
11297
|
+
if (!existsSync14(file)) return true;
|
|
10666
11298
|
const current = sha256(readFileSync3(file));
|
|
10667
11299
|
if (current === output.sha256) return false;
|
|
10668
11300
|
return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
|
|
@@ -10746,21 +11378,21 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
10746
11378
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
10747
11379
|
fresh = await provider.download(source.url);
|
|
10748
11380
|
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
10749
|
-
log(` current ${
|
|
11381
|
+
log(` current ${path18.relative(process.cwd(), target)}`);
|
|
10750
11382
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
10751
11383
|
continue;
|
|
10752
11384
|
}
|
|
10753
11385
|
}
|
|
10754
11386
|
let raw = null;
|
|
10755
11387
|
let final = null;
|
|
10756
|
-
if (
|
|
11388
|
+
if (existsSync14(target)) {
|
|
10757
11389
|
const currentHash = await sha256File(target);
|
|
10758
11390
|
if (recorded && currentHash === recorded.sha256) {
|
|
10759
11391
|
if (!fresh && !reapply) {
|
|
10760
11392
|
if (cacheDir) {
|
|
10761
11393
|
await cacheMedia(
|
|
10762
11394
|
cacheDir,
|
|
10763
|
-
await
|
|
11395
|
+
await readFile12(target),
|
|
10764
11396
|
recorded.mediaType ?? MediaType.PNG,
|
|
10765
11397
|
recorded.sha256
|
|
10766
11398
|
);
|
|
@@ -10769,10 +11401,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
10769
11401
|
continue;
|
|
10770
11402
|
}
|
|
10771
11403
|
if (reapply && !fresh) {
|
|
10772
|
-
if (!recorded.raw) raw = await
|
|
10773
|
-
log(` reapply ${
|
|
11404
|
+
if (!recorded.raw) raw = await readFile12(target);
|
|
11405
|
+
log(` reapply ${path18.relative(process.cwd(), target)}`);
|
|
10774
11406
|
} else {
|
|
10775
|
-
log(` changed ${
|
|
11407
|
+
log(` changed ${path18.relative(process.cwd(), target)} (upstream)`);
|
|
10776
11408
|
}
|
|
10777
11409
|
} else {
|
|
10778
11410
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
@@ -10793,14 +11425,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
10793
11425
|
}
|
|
10794
11426
|
if (outgoing && cacheDir) {
|
|
10795
11427
|
try {
|
|
10796
|
-
const oldBytes = await
|
|
11428
|
+
const oldBytes = await readFile12(target);
|
|
10797
11429
|
const oldMediaType = detectMediaType(oldBytes);
|
|
10798
11430
|
if (oldMediaType) await cacheMedia(cacheDir, oldBytes, oldMediaType, currentHash);
|
|
10799
11431
|
} catch {
|
|
10800
11432
|
}
|
|
10801
11433
|
}
|
|
10802
11434
|
log(
|
|
10803
|
-
` replace ${
|
|
11435
|
+
` replace ${path18.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
10804
11436
|
);
|
|
10805
11437
|
}
|
|
10806
11438
|
}
|
|
@@ -10809,7 +11441,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
10809
11441
|
} else if (!raw && recorded && cacheDir) {
|
|
10810
11442
|
if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
|
|
10811
11443
|
if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
|
|
10812
|
-
if (final || raw) log(` cached ${
|
|
11444
|
+
if (final || raw) log(` cached ${path18.relative(process.cwd(), target)}`);
|
|
10813
11445
|
}
|
|
10814
11446
|
if (!final && !raw) {
|
|
10815
11447
|
if (!source.url) {
|
|
@@ -10836,13 +11468,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
10836
11468
|
buf = processed.bytes;
|
|
10837
11469
|
if (processed.changed) {
|
|
10838
11470
|
rawHash = sha256(raw);
|
|
10839
|
-
log(` snapped ${
|
|
11471
|
+
log(` snapped ${path18.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
|
|
10840
11472
|
}
|
|
10841
11473
|
}
|
|
10842
11474
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
10843
|
-
await
|
|
11475
|
+
await mkdir6(path18.dirname(target), { recursive: true });
|
|
10844
11476
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
10845
|
-
await
|
|
11477
|
+
await writeFile6(tmp, buf);
|
|
10846
11478
|
outputs.push({
|
|
10847
11479
|
path: portableOutputPath(target, spec.root),
|
|
10848
11480
|
sha256: sha256(buf),
|
|
@@ -10854,7 +11486,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
10854
11486
|
await saveLock(lockPath, lock);
|
|
10855
11487
|
await rename5(tmp, target);
|
|
10856
11488
|
wrote = true;
|
|
10857
|
-
log(` wrote ${
|
|
11489
|
+
log(` wrote ${path18.relative(process.cwd(), target)}`);
|
|
10858
11490
|
}
|
|
10859
11491
|
if (opts.refresh && !wrote) {
|
|
10860
11492
|
if (rotated) {
|
|
@@ -10898,10 +11530,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
10898
11530
|
return result;
|
|
10899
11531
|
}
|
|
10900
11532
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
10901
|
-
const file =
|
|
10902
|
-
if (!
|
|
11533
|
+
const file = path18.join(cacheDir, cacheFileName(hash, mediaType));
|
|
11534
|
+
if (!existsSync14(file)) return null;
|
|
10903
11535
|
try {
|
|
10904
|
-
const buf = await
|
|
11536
|
+
const buf = await readFile12(file);
|
|
10905
11537
|
if (sha256(buf) !== hash) return null;
|
|
10906
11538
|
validateMedia(buf, mediaType);
|
|
10907
11539
|
return buf;
|
|
@@ -10912,15 +11544,15 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
10912
11544
|
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
10913
11545
|
const hash = knownHash ?? sha256(buf);
|
|
10914
11546
|
validateMedia(buf, mediaType);
|
|
10915
|
-
const file =
|
|
11547
|
+
const file = path18.join(cacheDir, cacheFileName(hash, mediaType));
|
|
10916
11548
|
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
10917
|
-
await
|
|
11549
|
+
await mkdir6(cacheDir, { recursive: true });
|
|
10918
11550
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
10919
|
-
await
|
|
11551
|
+
await writeFile6(tmp, buf);
|
|
10920
11552
|
try {
|
|
10921
11553
|
await rename5(tmp, file);
|
|
10922
11554
|
} catch (err) {
|
|
10923
|
-
if (!
|
|
11555
|
+
if (!existsSync14(file)) throw err;
|
|
10924
11556
|
} finally {
|
|
10925
11557
|
await rm5(tmp, { force: true });
|
|
10926
11558
|
}
|
|
@@ -11039,10 +11671,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
11039
11671
|
}
|
|
11040
11672
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
11041
11673
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
11042
|
-
if (!
|
|
11674
|
+
if (!existsSync15(file)) continue;
|
|
11043
11675
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
11044
11676
|
throw new OverwriteRefusedError(
|
|
11045
|
-
`${
|
|
11677
|
+
`${path19.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
11046
11678
|
[file]
|
|
11047
11679
|
);
|
|
11048
11680
|
}
|
|
@@ -11094,11 +11726,11 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
11094
11726
|
|
|
11095
11727
|
// src/pipeline/cache-health.ts
|
|
11096
11728
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
11097
|
-
if (options.prune && !
|
|
11098
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
11729
|
+
if (options.prune && !existsSync16(lockPath)) {
|
|
11730
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path20.resolve(lockPath)}`);
|
|
11099
11731
|
}
|
|
11100
|
-
const contentDir =
|
|
11101
|
-
const remotePath =
|
|
11732
|
+
const contentDir = path20.resolve(path20.dirname(lockPath), ".pixelkiln", "cache");
|
|
11733
|
+
const remotePath = path20.resolve(cachePathFor(lockPath));
|
|
11102
11734
|
const referenced = referencedHashes(lock);
|
|
11103
11735
|
let content = await inspectContentCache(contentDir, referenced);
|
|
11104
11736
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -11110,7 +11742,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11110
11742
|
]);
|
|
11111
11743
|
for (const name of names) {
|
|
11112
11744
|
try {
|
|
11113
|
-
await rm6(
|
|
11745
|
+
await rm6(path20.join(contentDir, name), { force: true });
|
|
11114
11746
|
removed.contentFiles++;
|
|
11115
11747
|
} catch {
|
|
11116
11748
|
}
|
|
@@ -11119,7 +11751,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11119
11751
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
11120
11752
|
removed.resetRemoteHashCache = true;
|
|
11121
11753
|
} else if (remoteHashes.invalidIds.length) {
|
|
11122
|
-
const cache = parseCache(JSON.parse(await
|
|
11754
|
+
const cache = parseCache(JSON.parse(await readFile13(remotePath, "utf8")));
|
|
11123
11755
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
11124
11756
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
11125
11757
|
await saveCache(remotePath, cache);
|
|
@@ -11137,7 +11769,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
11137
11769
|
async function inspectContentCache(contentDir, referenced) {
|
|
11138
11770
|
const report = {
|
|
11139
11771
|
path: contentDir,
|
|
11140
|
-
exists:
|
|
11772
|
+
exists: existsSync16(contentDir),
|
|
11141
11773
|
files: 0,
|
|
11142
11774
|
bytes: 0,
|
|
11143
11775
|
valid: 0,
|
|
@@ -11158,12 +11790,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
11158
11790
|
continue;
|
|
11159
11791
|
}
|
|
11160
11792
|
report.files++;
|
|
11161
|
-
const file =
|
|
11793
|
+
const file = path20.join(contentDir, entry.name);
|
|
11162
11794
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
11163
11795
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
11164
11796
|
let bytes;
|
|
11165
11797
|
try {
|
|
11166
|
-
bytes = await
|
|
11798
|
+
bytes = await readFile13(file);
|
|
11167
11799
|
report.bytes += bytes.length;
|
|
11168
11800
|
} catch (err) {
|
|
11169
11801
|
report.invalid.push({
|
|
@@ -11201,7 +11833,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
11201
11833
|
async function inspectRemoteHashCache(remotePath) {
|
|
11202
11834
|
const report = {
|
|
11203
11835
|
path: remotePath,
|
|
11204
|
-
exists:
|
|
11836
|
+
exists: existsSync16(remotePath),
|
|
11205
11837
|
entries: 0,
|
|
11206
11838
|
valid: 0,
|
|
11207
11839
|
invalidIds: [],
|
|
@@ -11210,7 +11842,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
11210
11842
|
if (!report.exists) return report;
|
|
11211
11843
|
let cache;
|
|
11212
11844
|
try {
|
|
11213
|
-
cache = parseCache(JSON.parse(await
|
|
11845
|
+
cache = parseCache(JSON.parse(await readFile13(remotePath, "utf8")));
|
|
11214
11846
|
} catch (err) {
|
|
11215
11847
|
report.error = err instanceof Error ? err.message : String(err);
|
|
11216
11848
|
return report;
|
|
@@ -11226,19 +11858,19 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
11226
11858
|
|
|
11227
11859
|
// src/pipeline/pack.ts
|
|
11228
11860
|
import { readFileSync as readFileSync4 } from "fs";
|
|
11229
|
-
import
|
|
11861
|
+
import path21 from "path";
|
|
11230
11862
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
11231
11863
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
11232
11864
|
if (!Array.isArray(raw) || !raw.length) {
|
|
11233
11865
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
11234
11866
|
}
|
|
11235
|
-
const baseDir =
|
|
11867
|
+
const baseDir = path21.dirname(path21.resolve(inputsFilePath));
|
|
11236
11868
|
return raw.map((entry, i) => {
|
|
11237
11869
|
const e = entry;
|
|
11238
11870
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
11239
11871
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
11240
11872
|
}
|
|
11241
|
-
return { id: e.id, path:
|
|
11873
|
+
return { id: e.id, path: path21.resolve(baseDir, e.path) };
|
|
11242
11874
|
});
|
|
11243
11875
|
}
|
|
11244
11876
|
function packSprites(inputs, options = {}) {
|
|
@@ -11322,7 +11954,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
11322
11954
|
}
|
|
11323
11955
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
11324
11956
|
id,
|
|
11325
|
-
path:
|
|
11957
|
+
path: path21.resolve(source)
|
|
11326
11958
|
}));
|
|
11327
11959
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
11328
11960
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -11346,7 +11978,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
11346
11978
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
11347
11979
|
const source = sources[id];
|
|
11348
11980
|
if (source && outputs.length <= 1) {
|
|
11349
|
-
inputs.push({ id, path:
|
|
11981
|
+
inputs.push({ id, path: path21.resolve(manifestDir, source) });
|
|
11350
11982
|
continue;
|
|
11351
11983
|
}
|
|
11352
11984
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -11380,7 +12012,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
11380
12012
|
}
|
|
11381
12013
|
}
|
|
11382
12014
|
for (const [id, source] of Object.entries(sources)) {
|
|
11383
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
12015
|
+
if (!locked.has(id)) inputs.push({ id, path: path21.resolve(manifestDir, source) });
|
|
11384
12016
|
}
|
|
11385
12017
|
if (!inputs.length) {
|
|
11386
12018
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -11488,7 +12120,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
11488
12120
|
const source = sources[id];
|
|
11489
12121
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
11490
12122
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
11491
|
-
placements.push({ id, path:
|
|
12123
|
+
placements.push({ id, path: path21.resolve(manifestDir, source), cell });
|
|
11492
12124
|
continue;
|
|
11493
12125
|
}
|
|
11494
12126
|
if (source && entry) {
|
|
@@ -11521,7 +12153,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
11521
12153
|
`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}`
|
|
11522
12154
|
);
|
|
11523
12155
|
}
|
|
11524
|
-
const basePng = mount.base ? readFileSync4(
|
|
12156
|
+
const basePng = mount.base ? readFileSync4(path21.resolve(manifestDir, mount.base)) : void 0;
|
|
11525
12157
|
const mounted = mountSprites(placements, {
|
|
11526
12158
|
cellWidth: mount.cellWidth,
|
|
11527
12159
|
cellHeight: mount.cellHeight,
|
|
@@ -11531,7 +12163,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
11531
12163
|
...mounted,
|
|
11532
12164
|
sources: mount.base ? [{
|
|
11533
12165
|
id: "$base",
|
|
11534
|
-
path:
|
|
12166
|
+
path: path21.resolve(manifestDir, mount.base),
|
|
11535
12167
|
sha256: sha256(basePng),
|
|
11536
12168
|
included: true
|
|
11537
12169
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -12309,9 +12941,9 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
12309
12941
|
}
|
|
12310
12942
|
|
|
12311
12943
|
// src/pipeline/doctor.ts
|
|
12312
|
-
import { constants, existsSync as
|
|
12944
|
+
import { constants, existsSync as existsSync17 } from "fs";
|
|
12313
12945
|
import { access, stat as stat2 } from "fs/promises";
|
|
12314
|
-
import
|
|
12946
|
+
import path22 from "path";
|
|
12315
12947
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
12316
12948
|
const checks = [];
|
|
12317
12949
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -12322,8 +12954,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12322
12954
|
);
|
|
12323
12955
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
12324
12956
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
12325
|
-
|
|
12326
|
-
...spec.quality ? [
|
|
12957
|
+
path22.dirname(spec.outFile),
|
|
12958
|
+
...spec.quality ? [path22.dirname(spec.quality.outFile)] : []
|
|
12327
12959
|
]))];
|
|
12328
12960
|
const unwritable = [];
|
|
12329
12961
|
for (const dir of dirs) {
|
|
@@ -12346,7 +12978,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12346
12978
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
12347
12979
|
const output = entry.outputs[index];
|
|
12348
12980
|
const spec = specByKey.get(key);
|
|
12349
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
12981
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path22.dirname(lockPath));
|
|
12350
12982
|
const owner = outputOwners.get(absolute);
|
|
12351
12983
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
12352
12984
|
outputOwners.set(absolute, key);
|
|
@@ -12361,7 +12993,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12361
12993
|
} else {
|
|
12362
12994
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
12363
12995
|
}
|
|
12364
|
-
const cacheDir =
|
|
12996
|
+
const cacheDir = path22.join(path22.dirname(lockPath), ".pixelkiln", "cache");
|
|
12365
12997
|
const stranded = [];
|
|
12366
12998
|
const invalidState = [];
|
|
12367
12999
|
const staleMapUrls = [];
|
|
@@ -12377,11 +13009,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
12377
13009
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
12378
13010
|
let hasCache = false;
|
|
12379
13011
|
for (const output of entry.outputs) {
|
|
12380
|
-
const cached =
|
|
13012
|
+
const cached = path22.join(
|
|
12381
13013
|
cacheDir,
|
|
12382
13014
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
12383
13015
|
);
|
|
12384
|
-
if (
|
|
13016
|
+
if (existsSync17(cached) && await sha256File(cached) === output.sha256) {
|
|
12385
13017
|
hasCache = true;
|
|
12386
13018
|
break;
|
|
12387
13019
|
}
|
|
@@ -12477,16 +13109,16 @@ async function nearestExistingDirectory(start) {
|
|
|
12477
13109
|
if ((await stat2(current)).isDirectory()) return current;
|
|
12478
13110
|
} catch {
|
|
12479
13111
|
}
|
|
12480
|
-
const parent =
|
|
13112
|
+
const parent = path22.dirname(current);
|
|
12481
13113
|
if (parent === current) return current;
|
|
12482
13114
|
current = parent;
|
|
12483
13115
|
}
|
|
12484
13116
|
}
|
|
12485
13117
|
|
|
12486
13118
|
// src/pipeline/adopt.ts
|
|
12487
|
-
import { existsSync as
|
|
12488
|
-
import { readFile as
|
|
12489
|
-
import
|
|
13119
|
+
import { existsSync as existsSync18 } from "fs";
|
|
13120
|
+
import { readFile as readFile14, writeFile as writeFile7 } from "fs/promises";
|
|
13121
|
+
import path23 from "path";
|
|
12490
13122
|
async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
12491
13123
|
const log = opts.onProgress ?? (() => {
|
|
12492
13124
|
});
|
|
@@ -12497,11 +13129,11 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12497
13129
|
const localByHash = /* @__PURE__ */ new Map();
|
|
12498
13130
|
const unmatchedLocal = [];
|
|
12499
13131
|
for (const spec of specs) {
|
|
12500
|
-
if (!
|
|
13132
|
+
if (!existsSync18(spec.outFile)) {
|
|
12501
13133
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
12502
13134
|
continue;
|
|
12503
13135
|
}
|
|
12504
|
-
const bytes = await
|
|
13136
|
+
const bytes = await readFile14(spec.outFile);
|
|
12505
13137
|
try {
|
|
12506
13138
|
decodePng(bytes);
|
|
12507
13139
|
} catch (err) {
|
|
@@ -12619,13 +13251,13 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
|
|
|
12619
13251
|
}
|
|
12620
13252
|
|
|
12621
13253
|
// src/pick/server.ts
|
|
12622
|
-
import { existsSync as
|
|
12623
|
-
import { readFile as
|
|
13254
|
+
import { existsSync as existsSync20 } from "fs";
|
|
13255
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
12624
13256
|
|
|
12625
13257
|
// src/pipeline/init.ts
|
|
12626
|
-
import { readdir as readdir3, readFile as
|
|
12627
|
-
import { existsSync as
|
|
12628
|
-
import
|
|
13258
|
+
import { readdir as readdir3, readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
|
|
13259
|
+
import { existsSync as existsSync19 } from "fs";
|
|
13260
|
+
import path24 from "path";
|
|
12629
13261
|
function pngSize(buf) {
|
|
12630
13262
|
if (buf.length < 24) return null;
|
|
12631
13263
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -12639,7 +13271,7 @@ function slugify(value) {
|
|
|
12639
13271
|
async function walk(dir, exclude) {
|
|
12640
13272
|
const out = [];
|
|
12641
13273
|
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
12642
|
-
const full =
|
|
13274
|
+
const full = path24.join(dir, entry.name);
|
|
12643
13275
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
12644
13276
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
12645
13277
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -12653,14 +13285,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
12653
13285
|
const skipped = [];
|
|
12654
13286
|
const seen = /* @__PURE__ */ new Map();
|
|
12655
13287
|
for (const file of files.sort()) {
|
|
12656
|
-
const size = pngSize(await
|
|
13288
|
+
const size = pngSize(await readFile15(file));
|
|
12657
13289
|
if (!size) {
|
|
12658
13290
|
skipped.push(`${file} (not a readable PNG)`);
|
|
12659
13291
|
continue;
|
|
12660
13292
|
}
|
|
12661
|
-
const rel =
|
|
12662
|
-
const category =
|
|
12663
|
-
let id = slugify(
|
|
13293
|
+
const rel = path24.relative(root, file);
|
|
13294
|
+
const category = path24.dirname(rel) === "." ? "" : path24.dirname(rel);
|
|
13295
|
+
let id = slugify(path24.basename(rel));
|
|
12664
13296
|
if (seen.has(id)) {
|
|
12665
13297
|
const n = seen.get(id) + 1;
|
|
12666
13298
|
seen.set(id, n);
|
|
@@ -13124,7 +13756,7 @@ document.getElementById('submit').onclick = async () => {
|
|
|
13124
13756
|
|
|
13125
13757
|
// src/pick/review-server.ts
|
|
13126
13758
|
import { createServer } from "http";
|
|
13127
|
-
import { readFile as
|
|
13759
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
13128
13760
|
function serveReviewPage(opts) {
|
|
13129
13761
|
const log = opts.onProgress ?? (() => {
|
|
13130
13762
|
});
|
|
@@ -13133,7 +13765,7 @@ function serveReviewPage(opts) {
|
|
|
13133
13765
|
if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
|
|
13134
13766
|
try {
|
|
13135
13767
|
const asset = opts.assets.get(req.url);
|
|
13136
|
-
const bytes = await
|
|
13768
|
+
const bytes = await readFile16(asset.path);
|
|
13137
13769
|
res.writeHead(200, {
|
|
13138
13770
|
"Content-Type": asset.contentType,
|
|
13139
13771
|
"Cache-Control": "no-store",
|
|
@@ -13209,13 +13841,13 @@ async function currentArt(entry, spec) {
|
|
|
13209
13841
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
13210
13842
|
if (!previous) return null;
|
|
13211
13843
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
13212
|
-
if (!
|
|
13844
|
+
if (!existsSync20(file)) return null;
|
|
13213
13845
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
13214
13846
|
if (!contentType) return null;
|
|
13215
13847
|
let width = entry.width;
|
|
13216
13848
|
let height = entry.height;
|
|
13217
13849
|
if (contentType === "image/png") {
|
|
13218
|
-
const size = pngSize(await
|
|
13850
|
+
const size = pngSize(await readFile17(file));
|
|
13219
13851
|
if (size) ({ width, height } = size);
|
|
13220
13852
|
}
|
|
13221
13853
|
return { path: file, contentType, width, height };
|
|
@@ -13384,14 +14016,14 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
13384
14016
|
|
|
13385
14017
|
// src/gallery/snapshot.ts
|
|
13386
14018
|
import { createHash as createHash4 } from "crypto";
|
|
13387
|
-
import { existsSync as
|
|
13388
|
-
import { readFile as
|
|
13389
|
-
import
|
|
14019
|
+
import { existsSync as existsSync23 } from "fs";
|
|
14020
|
+
import { readFile as readFile21, stat as stat3 } from "fs/promises";
|
|
14021
|
+
import path29 from "path";
|
|
13390
14022
|
|
|
13391
14023
|
// src/workspace.ts
|
|
13392
|
-
import { mkdir as
|
|
13393
|
-
import { existsSync as
|
|
13394
|
-
import
|
|
14024
|
+
import { mkdir as mkdir7, readFile as readFile18, rename as rename6, rm as rm7, writeFile as writeFile9 } from "fs/promises";
|
|
14025
|
+
import { existsSync as existsSync21 } from "fs";
|
|
14026
|
+
import path25 from "path";
|
|
13395
14027
|
import { z as z6 } from "zod";
|
|
13396
14028
|
var WorkspaceProjectSchema = z6.object({
|
|
13397
14029
|
id: z6.string().min(1),
|
|
@@ -13416,10 +14048,10 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
|
|
|
13416
14048
|
);
|
|
13417
14049
|
}
|
|
13418
14050
|
async function loadWorkspace(workspacePath) {
|
|
13419
|
-
if (!
|
|
14051
|
+
if (!existsSync21(workspacePath)) return { version: 1, projects: [] };
|
|
13420
14052
|
let raw;
|
|
13421
14053
|
try {
|
|
13422
|
-
raw = JSON.parse(await
|
|
14054
|
+
raw = JSON.parse(await readFile18(workspacePath, "utf8"));
|
|
13423
14055
|
} catch (err) {
|
|
13424
14056
|
throw new ProjectError(
|
|
13425
14057
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
@@ -13433,22 +14065,22 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
13433
14065
|
version: 1,
|
|
13434
14066
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
13435
14067
|
};
|
|
13436
|
-
await
|
|
14068
|
+
await mkdir7(path25.dirname(path25.resolve(workspacePath)), { recursive: true });
|
|
13437
14069
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
13438
14070
|
try {
|
|
13439
|
-
await
|
|
14071
|
+
await writeFile9(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
13440
14072
|
await rename6(tmp, workspacePath);
|
|
13441
14073
|
} finally {
|
|
13442
14074
|
await rm7(tmp, { force: true });
|
|
13443
14075
|
}
|
|
13444
14076
|
}
|
|
13445
14077
|
function toPortablePath(dir, absolute) {
|
|
13446
|
-
return
|
|
14078
|
+
return path25.relative(dir, absolute).split(path25.sep).join("/");
|
|
13447
14079
|
}
|
|
13448
14080
|
function resolveProject(dir, project) {
|
|
13449
14081
|
return {
|
|
13450
|
-
manifestPath:
|
|
13451
|
-
lockPath:
|
|
14082
|
+
manifestPath: path25.resolve(dir, project.manifest.split("/").join(path25.sep)),
|
|
14083
|
+
lockPath: path25.resolve(dir, project.lock.split("/").join(path25.sep))
|
|
13452
14084
|
};
|
|
13453
14085
|
}
|
|
13454
14086
|
function validateWorkspace(ws, dir) {
|
|
@@ -13461,21 +14093,21 @@ function validateWorkspace(ws, dir) {
|
|
|
13461
14093
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
13462
14094
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
13463
14095
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
13464
|
-
if (
|
|
14096
|
+
if (path25.isAbsolute(project.manifest) || path25.isAbsolute(project.lock)) {
|
|
13465
14097
|
diagnostics.push({
|
|
13466
14098
|
id: "absolute-path",
|
|
13467
14099
|
level: "warning",
|
|
13468
14100
|
message: `project "${project.id}" stores an absolute path; the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
13469
14101
|
});
|
|
13470
14102
|
}
|
|
13471
|
-
if (!
|
|
14103
|
+
if (!existsSync21(manifestPath)) {
|
|
13472
14104
|
diagnostics.push({
|
|
13473
14105
|
id: "missing-manifest",
|
|
13474
14106
|
level: "error",
|
|
13475
14107
|
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
13476
14108
|
});
|
|
13477
14109
|
}
|
|
13478
|
-
if (!
|
|
14110
|
+
if (!existsSync21(lockPath)) {
|
|
13479
14111
|
diagnostics.push({
|
|
13480
14112
|
id: "missing-lock",
|
|
13481
14113
|
level: "error",
|
|
@@ -13522,12 +14154,12 @@ function validateWorkspace(ws, dir) {
|
|
|
13522
14154
|
}
|
|
13523
14155
|
|
|
13524
14156
|
// src/gallery/edit.ts
|
|
13525
|
-
import
|
|
14157
|
+
import path28 from "path";
|
|
13526
14158
|
import { z as z9 } from "zod";
|
|
13527
14159
|
|
|
13528
14160
|
// src/manifest-edit.ts
|
|
13529
|
-
import { readFile as
|
|
13530
|
-
import
|
|
14161
|
+
import { readFile as readFile19, rename as rename7, rm as rm8, writeFile as writeFile10 } from "fs/promises";
|
|
14162
|
+
import path26 from "path";
|
|
13531
14163
|
import { z as z7 } from "zod";
|
|
13532
14164
|
var HexSha = z7.string().regex(/^[0-9a-f]{64}$/);
|
|
13533
14165
|
var AssetPatchSchema = z7.object({
|
|
@@ -13683,11 +14315,21 @@ function assetStyles(raw, asset) {
|
|
|
13683
14315
|
const own = Array.isArray(asset.styles) ? asset.styles : [];
|
|
13684
14316
|
return own.length ? own : Object.keys(raw.styles ?? {});
|
|
13685
14317
|
}
|
|
14318
|
+
function declaredAssets(raw) {
|
|
14319
|
+
const expanded = expandLoopDirections(raw).raw;
|
|
14320
|
+
return new Set(Object.keys(expanded.assets ?? {}));
|
|
14321
|
+
}
|
|
14322
|
+
function undeclaredAsset(raw, assetId) {
|
|
14323
|
+
const shorthand = loopShorthandOf(assetId, expandLoopDirections(raw).families);
|
|
14324
|
+
return new ManifestEditError(
|
|
14325
|
+
shorthand ? `asset "${assetId}" is expanded from "${shorthand}"'s animation.directions; edit "${shorthand}" (it applies to every direction), or write this direction out as its own asset to change it alone` : `asset "${assetId}" is not declared by the manifest`
|
|
14326
|
+
);
|
|
14327
|
+
}
|
|
13686
14328
|
function applyEdit(raw, edit) {
|
|
13687
14329
|
raw.assets ??= {};
|
|
13688
14330
|
if (edit.action === "set-source" || edit.action === "clear-source") {
|
|
13689
14331
|
const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
13690
|
-
if (!asset2) throw
|
|
14332
|
+
if (!asset2) throw undeclaredAsset(raw, edit.assetId);
|
|
13691
14333
|
if (!raw.styles || !Object.hasOwn(raw.styles, edit.styleId)) {
|
|
13692
14334
|
throw new ManifestEditError(`unknown style "${edit.styleId}"`);
|
|
13693
14335
|
}
|
|
@@ -13739,7 +14381,7 @@ function applyEdit(raw, edit) {
|
|
|
13739
14381
|
return;
|
|
13740
14382
|
}
|
|
13741
14383
|
if (edit.action === "add-asset") {
|
|
13742
|
-
if (
|
|
14384
|
+
if (declaredAssets(raw).has(edit.assetId)) {
|
|
13743
14385
|
throw new ManifestEditError(`asset "${edit.assetId}" already exists`);
|
|
13744
14386
|
}
|
|
13745
14387
|
const { styles, tags, ...rest } = edit.asset;
|
|
@@ -13753,20 +14395,20 @@ function applyEdit(raw, edit) {
|
|
|
13753
14395
|
}
|
|
13754
14396
|
asset2.styles = styles;
|
|
13755
14397
|
}
|
|
13756
|
-
if (edit.asset.revision && !
|
|
14398
|
+
if (edit.asset.revision && !declaredAssets(raw).has(edit.asset.revision.from)) {
|
|
13757
14399
|
throw new ManifestEditError(`revision parent "${edit.asset.revision.from}" is not declared by the manifest`);
|
|
13758
14400
|
}
|
|
13759
|
-
if (edit.asset.state && !
|
|
14401
|
+
if (edit.asset.state && !declaredAssets(raw).has(edit.asset.state.of)) {
|
|
13760
14402
|
throw new ManifestEditError(`state parent "${edit.asset.state.of}" is not declared by the manifest`);
|
|
13761
14403
|
}
|
|
13762
|
-
if (edit.asset.animation && !
|
|
14404
|
+
if (edit.asset.animation && !declaredAssets(raw).has(edit.asset.animation.of)) {
|
|
13763
14405
|
throw new ManifestEditError(`animation parent "${edit.asset.animation.of}" is not declared by the manifest`);
|
|
13764
14406
|
}
|
|
13765
14407
|
raw.assets[edit.assetId] = asset2;
|
|
13766
14408
|
return;
|
|
13767
14409
|
}
|
|
13768
14410
|
const asset = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
13769
|
-
if (!asset) throw
|
|
14411
|
+
if (!asset) throw undeclaredAsset(raw, edit.assetId);
|
|
13770
14412
|
const { patch } = edit;
|
|
13771
14413
|
if (patch.prompt !== void 0) asset.prompt = patch.prompt;
|
|
13772
14414
|
if (patch.promptForStyle) {
|
|
@@ -13790,8 +14432,8 @@ function applyEdit(raw, edit) {
|
|
|
13790
14432
|
}
|
|
13791
14433
|
}
|
|
13792
14434
|
async function applyManifestEdit(manifestPath, edit) {
|
|
13793
|
-
const absolute =
|
|
13794
|
-
const original = await
|
|
14435
|
+
const absolute = path26.resolve(manifestPath);
|
|
14436
|
+
const original = await readFile19(absolute, "utf8");
|
|
13795
14437
|
const before = sha256(original);
|
|
13796
14438
|
if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
13797
14439
|
let raw;
|
|
@@ -13805,12 +14447,12 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
13805
14447
|
applyEdit(raw, edit);
|
|
13806
14448
|
const next = serializeLike(original, raw);
|
|
13807
14449
|
if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
|
|
13808
|
-
const tmp =
|
|
13809
|
-
|
|
13810
|
-
`.${
|
|
14450
|
+
const tmp = path26.join(
|
|
14451
|
+
path26.dirname(absolute),
|
|
14452
|
+
`.${path26.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
13811
14453
|
);
|
|
13812
14454
|
try {
|
|
13813
|
-
await
|
|
14455
|
+
await writeFile10(tmp, next);
|
|
13814
14456
|
try {
|
|
13815
14457
|
const loaded = await loadManifest(tmp);
|
|
13816
14458
|
await resolveSpecs(loaded);
|
|
@@ -13826,18 +14468,18 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
13826
14468
|
}
|
|
13827
14469
|
|
|
13828
14470
|
// src/pipeline/hand-edit.ts
|
|
13829
|
-
import { existsSync as
|
|
13830
|
-
import { copyFile, mkdir as
|
|
13831
|
-
import
|
|
14471
|
+
import { existsSync as existsSync22 } from "fs";
|
|
14472
|
+
import { copyFile, mkdir as mkdir8, readFile as readFile20, rename as rename8, rm as rm9, writeFile as writeFile11 } from "fs/promises";
|
|
14473
|
+
import path27 from "path";
|
|
13832
14474
|
import { z as z8 } from "zod";
|
|
13833
14475
|
var HAND_EDIT_DIR = "edits";
|
|
13834
14476
|
function handEditPath(loaded, spec) {
|
|
13835
14477
|
const style = loaded.manifest.styles[spec.styleId];
|
|
13836
|
-
const outDir =
|
|
13837
|
-
const relative =
|
|
13838
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
13839
|
-
const file = inside ? relative :
|
|
13840
|
-
return
|
|
14478
|
+
const outDir = path27.resolve(loaded.root, style?.outDir ?? ".");
|
|
14479
|
+
const relative = path27.relative(outDir, spec.outFile);
|
|
14480
|
+
const inside = relative && !relative.startsWith("..") && !path27.isAbsolute(relative);
|
|
14481
|
+
const file = inside ? relative : path27.basename(spec.outFile);
|
|
14482
|
+
return path27.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
13841
14483
|
}
|
|
13842
14484
|
function handEditBases(spec, lock) {
|
|
13843
14485
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -13850,10 +14492,10 @@ function handEditBases(spec, lock) {
|
|
|
13850
14492
|
}));
|
|
13851
14493
|
if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
|
|
13852
14494
|
if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
|
|
13853
|
-
if (!members.every((member) =>
|
|
14495
|
+
if (!members.every((member) => existsSync22(member.path))) return null;
|
|
13854
14496
|
return members.map(({ role, path: file }) => ({ role, path: file }));
|
|
13855
14497
|
}
|
|
13856
|
-
return
|
|
14498
|
+
return existsSync22(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
|
|
13857
14499
|
}
|
|
13858
14500
|
function handEditBase(spec, lock) {
|
|
13859
14501
|
const bases = handEditBases(spec, lock);
|
|
@@ -13883,18 +14525,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
13883
14525
|
`${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`
|
|
13884
14526
|
);
|
|
13885
14527
|
}
|
|
13886
|
-
const existing = spec.source ?
|
|
14528
|
+
const existing = spec.source ? path27.resolve(loaded.root, spec.source) : null;
|
|
13887
14529
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
13888
14530
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
13889
14531
|
let created = false;
|
|
13890
14532
|
for (const member of members) {
|
|
13891
|
-
if (
|
|
14533
|
+
if (existsSync22(member.path)) continue;
|
|
13892
14534
|
if (!member.base) {
|
|
13893
14535
|
throw new ManifestEditError(
|
|
13894
14536
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
13895
14537
|
);
|
|
13896
14538
|
}
|
|
13897
|
-
await
|
|
14539
|
+
await mkdir8(path27.dirname(member.path), { recursive: true });
|
|
13898
14540
|
await copyFile(member.base, member.path);
|
|
13899
14541
|
created = true;
|
|
13900
14542
|
}
|
|
@@ -13973,7 +14615,7 @@ function handEditProjectPath(editPath) {
|
|
|
13973
14615
|
async function readHandEditCompanion(editPath) {
|
|
13974
14616
|
let text;
|
|
13975
14617
|
try {
|
|
13976
|
-
text = await
|
|
14618
|
+
text = await readFile20(handEditCompanionPath(editPath), "utf8");
|
|
13977
14619
|
} catch {
|
|
13978
14620
|
return null;
|
|
13979
14621
|
}
|
|
@@ -14016,7 +14658,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
14016
14658
|
}
|
|
14017
14659
|
});
|
|
14018
14660
|
const bases = handEditBases(spec, lock);
|
|
14019
|
-
const expectedSize = bases?.[0] ? decodePng(await
|
|
14661
|
+
const expectedSize = bases?.[0] ? decodePng(await readFile20(bases[0].path)) : { width: spec.width, height: spec.height };
|
|
14020
14662
|
for (const [index, image] of decoded.entries()) {
|
|
14021
14663
|
if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
|
|
14022
14664
|
const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
|
|
@@ -14032,7 +14674,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
14032
14674
|
editor: input.editor,
|
|
14033
14675
|
protocol: input.protocol,
|
|
14034
14676
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
14035
|
-
project: projectPath ?
|
|
14677
|
+
project: projectPath ? path27.basename(projectPath) : null,
|
|
14036
14678
|
outputs: incoming.map((frame, index) => ({
|
|
14037
14679
|
role: frame.role,
|
|
14038
14680
|
basedOn: basedOnHash(entry, index),
|
|
@@ -14062,7 +14704,7 @@ function basedOnHash(entry, index) {
|
|
|
14062
14704
|
async function replaceFile(file, bytes) {
|
|
14063
14705
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
14064
14706
|
try {
|
|
14065
|
-
await
|
|
14707
|
+
await writeFile11(tmp, bytes);
|
|
14066
14708
|
await rename8(tmp, file);
|
|
14067
14709
|
} finally {
|
|
14068
14710
|
await rm9(tmp, { force: true });
|
|
@@ -14162,7 +14804,7 @@ function createGalleryEditHandler(opts) {
|
|
|
14162
14804
|
}
|
|
14163
14805
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
14164
14806
|
if (result.changed) {
|
|
14165
|
-
const relative =
|
|
14807
|
+
const relative = path28.relative(process.cwd(), result.manifestPath);
|
|
14166
14808
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
14167
14809
|
const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.action === "set-source" || parsed.data.action === "clear-source" ? `${parsed.data.styleId}/${parsed.data.assetId} source` : parsed.data.assetId;
|
|
14168
14810
|
log(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
@@ -14174,7 +14816,7 @@ function createGalleryEditHandler(opts) {
|
|
|
14174
14816
|
// src/gallery/snapshot.ts
|
|
14175
14817
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
14176
14818
|
function galleryMediaId(absolutePath) {
|
|
14177
|
-
return createHash4("sha256").update(
|
|
14819
|
+
return createHash4("sha256").update(path29.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
14178
14820
|
}
|
|
14179
14821
|
function galleryMediaRoute(id) {
|
|
14180
14822
|
return `/media/${id}`;
|
|
@@ -14193,9 +14835,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
14193
14835
|
return (entry.history ?? []).map((generation, i) => {
|
|
14194
14836
|
const outputs = generation.outputs.map((output) => {
|
|
14195
14837
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
14196
|
-
const file =
|
|
14838
|
+
const file = path29.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
14197
14839
|
let url = null;
|
|
14198
|
-
if (
|
|
14840
|
+
if (existsSync23(file)) {
|
|
14199
14841
|
const id = galleryMediaId(file);
|
|
14200
14842
|
media.set(id, { path: file, contentType: mediaType });
|
|
14201
14843
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -14227,7 +14869,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
14227
14869
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
14228
14870
|
async function samePixels(a, b) {
|
|
14229
14871
|
try {
|
|
14230
|
-
const [left, right] = await Promise.all([
|
|
14872
|
+
const [left, right] = await Promise.all([readFile21(a), readFile21(b)]);
|
|
14231
14873
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
14232
14874
|
const x = decodePng(left);
|
|
14233
14875
|
const y = decodePng(right);
|
|
@@ -14239,6 +14881,7 @@ async function samePixels(a, b) {
|
|
|
14239
14881
|
var metadataFps = (entry) => entry ? frameSetFps(entry) : null;
|
|
14240
14882
|
function describeCharacter(spec, entry) {
|
|
14241
14883
|
const recordedCharacter = entry?.providerMetadata?.[entry.provider]?.character;
|
|
14884
|
+
if (spec?.revision) return null;
|
|
14242
14885
|
if (spec?.character) {
|
|
14243
14886
|
const character = spec.character;
|
|
14244
14887
|
return {
|
|
@@ -14324,10 +14967,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
14324
14967
|
async function describeQuality(media, root, inspection) {
|
|
14325
14968
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
14326
14969
|
const outputs = await Promise.all(
|
|
14327
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
14970
|
+
outputPaths.map((file) => describeOutput(media, root, path29.resolve(root, file)))
|
|
14328
14971
|
);
|
|
14329
|
-
const recordPath =
|
|
14330
|
-
const recordExists =
|
|
14972
|
+
const recordPath = path29.resolve(root, inspection.record);
|
|
14973
|
+
const recordExists = existsSync23(recordPath);
|
|
14331
14974
|
let options = null;
|
|
14332
14975
|
let check = null;
|
|
14333
14976
|
if (recordExists) {
|
|
@@ -14346,7 +14989,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
14346
14989
|
return {
|
|
14347
14990
|
state: inspection.state,
|
|
14348
14991
|
reason: inspection.reason,
|
|
14349
|
-
output: portableOutputPath(
|
|
14992
|
+
output: portableOutputPath(path29.resolve(root, inspection.output), root),
|
|
14350
14993
|
record: portableOutputPath(recordPath, root),
|
|
14351
14994
|
recordExists,
|
|
14352
14995
|
outputs,
|
|
@@ -14379,13 +15022,14 @@ async function describeQuality(media, root, inspection) {
|
|
|
14379
15022
|
}
|
|
14380
15023
|
async function buildGallerySnapshot(opts) {
|
|
14381
15024
|
const { loaded, specs, lock } = opts;
|
|
15025
|
+
const filter = opts.filter?.assets?.length ? { ...opts.filter, assets: expandAssetFilter(opts.filter.assets, loaded.loopFamilies) } : opts.filter;
|
|
14382
15026
|
const root = loaded.root;
|
|
14383
15027
|
const media = /* @__PURE__ */ new Map();
|
|
14384
15028
|
const plan = await buildPlan(specs, lock);
|
|
14385
15029
|
const items = [];
|
|
14386
|
-
const manifestText = await
|
|
15030
|
+
const manifestText = await readFile21(loaded.path, "utf8");
|
|
14387
15031
|
const manifestSha256 = sha256(manifestText);
|
|
14388
|
-
const cacheDir =
|
|
15032
|
+
const cacheDir = path29.resolve(path29.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
14389
15033
|
let rawStyles = {};
|
|
14390
15034
|
try {
|
|
14391
15035
|
const raw = JSON.parse(manifestText);
|
|
@@ -14409,12 +15053,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
14409
15053
|
)
|
|
14410
15054
|
);
|
|
14411
15055
|
if (spec.source) {
|
|
14412
|
-
const editPath =
|
|
15056
|
+
const editPath = path29.resolve(root, spec.source);
|
|
14413
15057
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
14414
15058
|
const editShas = await Promise.all(
|
|
14415
15059
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
14416
15060
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
14417
|
-
return { file, sha256:
|
|
15061
|
+
return { file, sha256: existsSync23(file) ? await sha256File(file) : null };
|
|
14418
15062
|
})
|
|
14419
15063
|
);
|
|
14420
15064
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -14422,7 +15066,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
14422
15066
|
const companion = await readHandEditCompanion(editPath);
|
|
14423
15067
|
if (companion) {
|
|
14424
15068
|
const projectPath = handEditProjectPath(editPath);
|
|
14425
|
-
const project = companion.project &&
|
|
15069
|
+
const project = companion.project && existsSync23(projectPath) ? await fileInfo(projectPath) : null;
|
|
14426
15070
|
let projectUrl = null;
|
|
14427
15071
|
if (project) {
|
|
14428
15072
|
const id = galleryMediaId(projectPath);
|
|
@@ -14458,8 +15102,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
14458
15102
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
14459
15103
|
}
|
|
14460
15104
|
} else if (spec.source) {
|
|
14461
|
-
outputs = [await describeOutput(media, root,
|
|
14462
|
-
} else if (!entry &&
|
|
15105
|
+
outputs = [await describeOutput(media, root, path29.resolve(root, spec.source))];
|
|
15106
|
+
} else if (!entry && existsSync23(spec.outFile)) {
|
|
14463
15107
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
14464
15108
|
} else {
|
|
14465
15109
|
outputs = [];
|
|
@@ -14523,7 +15167,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
14523
15167
|
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
14524
15168
|
if (shown.has(key)) continue;
|
|
14525
15169
|
if (isDeclared(loaded, entry.styleId, entry.assetId)) continue;
|
|
14526
|
-
if (!matchesFilter(
|
|
15170
|
+
if (!matchesFilter(filter, entry.styleId, entry.assetId)) continue;
|
|
14527
15171
|
const outputs = await Promise.all(
|
|
14528
15172
|
entry.outputs.map(
|
|
14529
15173
|
(output) => describeOutput(media, root, resolveOutputPath(output.path, root), output)
|
|
@@ -14652,7 +15296,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
14652
15296
|
id: loaded.manifest.name,
|
|
14653
15297
|
name: loaded.manifest.name,
|
|
14654
15298
|
manifest: loaded.path,
|
|
14655
|
-
lock:
|
|
15299
|
+
lock: path29.resolve(opts.lockPath),
|
|
14656
15300
|
root,
|
|
14657
15301
|
provider: loaded.manifest.provider,
|
|
14658
15302
|
account: null,
|
|
@@ -14678,7 +15322,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
14678
15322
|
return { snapshot, media };
|
|
14679
15323
|
}
|
|
14680
15324
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
14681
|
-
const dir =
|
|
15325
|
+
const dir = path29.dirname(path29.resolve(opts.workspacePath));
|
|
14682
15326
|
const media = /* @__PURE__ */ new Map();
|
|
14683
15327
|
const projects = [];
|
|
14684
15328
|
const items = [];
|
|
@@ -14689,7 +15333,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
14689
15333
|
try {
|
|
14690
15334
|
const loaded = await loadManifest(manifestPath);
|
|
14691
15335
|
const styleFilter = filter.styles.filter((id) => loaded.manifest.styles[id]);
|
|
14692
|
-
const assetFilter = filter.assets.filter((id) => loaded.manifest.assets[id]);
|
|
15336
|
+
const assetFilter = expandAssetFilter(filter.assets, loaded.loopFamilies).filter((id) => loaded.manifest.assets[id]);
|
|
14693
15337
|
const excluded = filter.styles.length > 0 && styleFilter.length === 0 || filter.assets.length > 0 && assetFilter.length === 0;
|
|
14694
15338
|
const lock = await loadLock(lockPath);
|
|
14695
15339
|
let projectItems = [];
|
|
@@ -14721,11 +15365,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
14721
15365
|
id: project.id,
|
|
14722
15366
|
name: loaded.manifest.name,
|
|
14723
15367
|
manifest: manifestPath,
|
|
14724
|
-
lock:
|
|
15368
|
+
lock: path29.resolve(lockPath),
|
|
14725
15369
|
root: loaded.root,
|
|
14726
15370
|
provider: loaded.manifest.provider,
|
|
14727
15371
|
account: project.account ?? null,
|
|
14728
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
15372
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile21(manifestPath, "utf8")),
|
|
14729
15373
|
historyLimit: historyLimit(loaded.manifest),
|
|
14730
15374
|
entries: Object.keys(lock.entries).length,
|
|
14731
15375
|
items: projectItems.length,
|
|
@@ -14737,8 +15381,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
14737
15381
|
id: project.id,
|
|
14738
15382
|
name: project.id,
|
|
14739
15383
|
manifest: manifestPath,
|
|
14740
|
-
lock:
|
|
14741
|
-
root:
|
|
15384
|
+
lock: path29.resolve(lockPath),
|
|
15385
|
+
root: path29.dirname(manifestPath),
|
|
14742
15386
|
provider: project.provider,
|
|
14743
15387
|
account: project.account ?? null,
|
|
14744
15388
|
manifestSha256: null,
|
|
@@ -14765,7 +15409,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
14765
15409
|
version: 1,
|
|
14766
15410
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
14767
15411
|
project: null,
|
|
14768
|
-
workspace: { path:
|
|
15412
|
+
workspace: { path: path29.resolve(opts.workspacePath), dir, projects },
|
|
14769
15413
|
filter,
|
|
14770
15414
|
totals: {
|
|
14771
15415
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -14914,10 +15558,10 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
|
|
|
14914
15558
|
|
|
14915
15559
|
// src/editor/install.ts
|
|
14916
15560
|
import { createHash as createHash5 } from "crypto";
|
|
14917
|
-
import { existsSync as
|
|
14918
|
-
import { mkdir as
|
|
15561
|
+
import { existsSync as existsSync24 } from "fs";
|
|
15562
|
+
import { mkdir as mkdir9, readFile as readFile22, rename as rename9, rm as rm10, stat as stat4, writeFile as writeFile12 } from "fs/promises";
|
|
14919
15563
|
import os2 from "os";
|
|
14920
|
-
import
|
|
15564
|
+
import path30 from "path";
|
|
14921
15565
|
var EditorInstallError = class extends Error {
|
|
14922
15566
|
constructor(message6, file) {
|
|
14923
15567
|
super(message6);
|
|
@@ -14928,15 +15572,15 @@ var EditorInstallError = class extends Error {
|
|
|
14928
15572
|
};
|
|
14929
15573
|
function editorToolsRoot() {
|
|
14930
15574
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
14931
|
-
if (override) return
|
|
14932
|
-
if (process.platform === "darwin") return
|
|
15575
|
+
if (override) return path30.resolve(override);
|
|
15576
|
+
if (process.platform === "darwin") return path30.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
14933
15577
|
if (process.platform === "win32") {
|
|
14934
|
-
return
|
|
15578
|
+
return path30.join(process.env.LOCALAPPDATA ?? path30.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
14935
15579
|
}
|
|
14936
|
-
return
|
|
15580
|
+
return path30.join(process.env.XDG_CACHE_HOME ?? path30.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
14937
15581
|
}
|
|
14938
15582
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
14939
|
-
return
|
|
15583
|
+
return path30.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
14940
15584
|
}
|
|
14941
15585
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
14942
15586
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -14946,7 +15590,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
14946
15590
|
}
|
|
14947
15591
|
async function sha256Of(file) {
|
|
14948
15592
|
try {
|
|
14949
|
-
return createHash5("sha256").update(await
|
|
15593
|
+
return createHash5("sha256").update(await readFile22(file)).digest("hex");
|
|
14950
15594
|
} catch {
|
|
14951
15595
|
return null;
|
|
14952
15596
|
}
|
|
@@ -14959,8 +15603,8 @@ async function editorStatus(opts = {}) {
|
|
|
14959
15603
|
let totalBytes = 0;
|
|
14960
15604
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
14961
15605
|
totalBytes += expected.bytes;
|
|
14962
|
-
const file =
|
|
14963
|
-
const info =
|
|
15606
|
+
const file = path30.join(dir, name);
|
|
15607
|
+
const info = existsSync24(file) ? await stat4(file).catch(() => null) : null;
|
|
14964
15608
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
14965
15609
|
installedBytes += expected.bytes;
|
|
14966
15610
|
} else {
|
|
@@ -14989,7 +15633,7 @@ async function installEditor(opts = {}) {
|
|
|
14989
15633
|
);
|
|
14990
15634
|
}
|
|
14991
15635
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
14992
|
-
await
|
|
15636
|
+
await mkdir9(dir, { recursive: true });
|
|
14993
15637
|
const downloaded = [];
|
|
14994
15638
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
14995
15639
|
let fetchedBytes = before.installedBytes;
|
|
@@ -15017,10 +15661,10 @@ async function installEditor(opts = {}) {
|
|
|
15017
15661
|
if (digest2 !== expected.sha256) {
|
|
15018
15662
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
15019
15663
|
}
|
|
15020
|
-
const final =
|
|
15664
|
+
const final = path30.join(dir, name);
|
|
15021
15665
|
const tmp = `${final}.${process.pid}.part`;
|
|
15022
15666
|
try {
|
|
15023
|
-
await
|
|
15667
|
+
await writeFile12(tmp, bytes);
|
|
15024
15668
|
await rename9(tmp, final);
|
|
15025
15669
|
} finally {
|
|
15026
15670
|
await rm10(tmp, { force: true });
|
|
@@ -15063,12 +15707,12 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
15063
15707
|
".json": "application/json; charset=utf-8"
|
|
15064
15708
|
};
|
|
15065
15709
|
function editorContentType(name) {
|
|
15066
|
-
return EDITOR_CONTENT_TYPES[
|
|
15710
|
+
return EDITOR_CONTENT_TYPES[path30.extname(name).toLowerCase()] ?? null;
|
|
15067
15711
|
}
|
|
15068
15712
|
|
|
15069
15713
|
// src/gallery/editor.ts
|
|
15070
|
-
import { readFile as
|
|
15071
|
-
import
|
|
15714
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
15715
|
+
import path31 from "path";
|
|
15072
15716
|
function editorRoute(release, name = "index.html") {
|
|
15073
15717
|
return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
|
|
15074
15718
|
}
|
|
@@ -15141,7 +15785,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
15141
15785
|
if (!status.installed) return null;
|
|
15142
15786
|
let bytes;
|
|
15143
15787
|
try {
|
|
15144
|
-
bytes = await
|
|
15788
|
+
bytes = await readFile23(path31.join(dir, name));
|
|
15145
15789
|
} catch {
|
|
15146
15790
|
verified = null;
|
|
15147
15791
|
return null;
|
|
@@ -15158,7 +15802,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
15158
15802
|
// src/gallery/server.ts
|
|
15159
15803
|
import { randomBytes } from "crypto";
|
|
15160
15804
|
import { createServer as createServer2 } from "http";
|
|
15161
|
-
import { readFile as
|
|
15805
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
15162
15806
|
var MAX_EDIT_BYTES = 48 * 1024 * 1024;
|
|
15163
15807
|
var EDITOR_CSP = [
|
|
15164
15808
|
"default-src 'self'",
|
|
@@ -15317,7 +15961,7 @@ async function serveGallery(opts) {
|
|
|
15317
15961
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
15318
15962
|
if (!asset) return fail(404, "no such review asset");
|
|
15319
15963
|
try {
|
|
15320
|
-
const bytes = await
|
|
15964
|
+
const bytes = await readFile24(asset.path);
|
|
15321
15965
|
res.writeHead(200, {
|
|
15322
15966
|
"Content-Type": asset.contentType,
|
|
15323
15967
|
"Cache-Control": "no-store",
|
|
@@ -15393,7 +16037,7 @@ async function serveGallery(opts) {
|
|
|
15393
16037
|
if (!asset) return fail(404, "no such gallery media");
|
|
15394
16038
|
let bytes;
|
|
15395
16039
|
try {
|
|
15396
|
-
bytes = await
|
|
16040
|
+
bytes = await readFile24(asset.path);
|
|
15397
16041
|
} catch {
|
|
15398
16042
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
15399
16043
|
}
|
|
@@ -15767,19 +16411,19 @@ function createGenerateHandlers(opts) {
|
|
|
15767
16411
|
}
|
|
15768
16412
|
|
|
15769
16413
|
// src/pipeline/salvage.ts
|
|
15770
|
-
import { readFile as
|
|
15771
|
-
import { existsSync as
|
|
15772
|
-
import
|
|
16414
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
16415
|
+
import { existsSync as existsSync25 } from "fs";
|
|
16416
|
+
import path32 from "path";
|
|
15773
16417
|
function providerClaimId(provider, objectId) {
|
|
15774
16418
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
15775
16419
|
}
|
|
15776
16420
|
async function loadClaims(lockPaths, opts = {}) {
|
|
15777
16421
|
const claimed = /* @__PURE__ */ new Set();
|
|
15778
16422
|
for (const p of lockPaths) {
|
|
15779
|
-
if (!
|
|
16423
|
+
if (!existsSync25(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
15780
16424
|
let parsed;
|
|
15781
16425
|
try {
|
|
15782
|
-
parsed = parseLock(JSON.parse(await
|
|
16426
|
+
parsed = parseLock(JSON.parse(await readFile25(p, "utf8")));
|
|
15783
16427
|
} catch {
|
|
15784
16428
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
15785
16429
|
}
|
|
@@ -15832,20 +16476,20 @@ function matchOrphanStyle(prompt, manifest) {
|
|
|
15832
16476
|
return matchStyleByPattern(prompt, manifest);
|
|
15833
16477
|
}
|
|
15834
16478
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
15835
|
-
const own =
|
|
16479
|
+
const own = path32.resolve(ownManifestPath);
|
|
15836
16480
|
const siblingManifestPaths = [
|
|
15837
16481
|
.../* @__PURE__ */ new Set([
|
|
15838
16482
|
...workspaceManifestPaths,
|
|
15839
|
-
...claimPaths.map((c) =>
|
|
16483
|
+
...claimPaths.map((c) => path32.join(path32.dirname(path32.resolve(c)), "pixelkiln.manifest.json"))
|
|
15840
16484
|
])
|
|
15841
16485
|
];
|
|
15842
16486
|
const siblings = [];
|
|
15843
16487
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
15844
|
-
if (
|
|
15845
|
-
if (!
|
|
16488
|
+
if (path32.resolve(siblingManifestPath) === own) continue;
|
|
16489
|
+
if (!existsSync25(siblingManifestPath)) continue;
|
|
15846
16490
|
try {
|
|
15847
16491
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
15848
|
-
siblings.push({ label:
|
|
16492
|
+
siblings.push({ label: path32.basename(path32.dirname(siblingManifestPath)), manifest });
|
|
15849
16493
|
} catch {
|
|
15850
16494
|
}
|
|
15851
16495
|
}
|
|
@@ -15961,8 +16605,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
15961
16605
|
}
|
|
15962
16606
|
|
|
15963
16607
|
// src/pick/salvage-server.ts
|
|
15964
|
-
import { mkdir as
|
|
15965
|
-
import
|
|
16608
|
+
import { mkdir as mkdir10, writeFile as writeFile13, readFile as readFile26 } from "fs/promises";
|
|
16609
|
+
import path33 from "path";
|
|
15966
16610
|
|
|
15967
16611
|
// src/pick/salvage-sheet.ts
|
|
15968
16612
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -16127,7 +16771,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16127
16771
|
});
|
|
16128
16772
|
const html = renderSalvageSheet(orphans, {
|
|
16129
16773
|
styleId: ctx.styleId,
|
|
16130
|
-
importDir:
|
|
16774
|
+
importDir: path33.relative(process.cwd(), ctx.importDir) || "."
|
|
16131
16775
|
});
|
|
16132
16776
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
16133
16777
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -16156,10 +16800,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16156
16800
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
16157
16801
|
decodePng(buf);
|
|
16158
16802
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
16159
|
-
const rel =
|
|
16160
|
-
const outFile =
|
|
16161
|
-
await
|
|
16162
|
-
await
|
|
16803
|
+
const rel = path33.join("_salvaged", `${assetId}.png`);
|
|
16804
|
+
const outFile = path33.resolve(ctx.importDir, rel);
|
|
16805
|
+
await mkdir10(path33.dirname(outFile), { recursive: true });
|
|
16806
|
+
await writeFile13(outFile, buf);
|
|
16163
16807
|
ctx.manifest.assets[assetId] = {
|
|
16164
16808
|
prompt: orphan.prompt,
|
|
16165
16809
|
promptByStyle: {},
|
|
@@ -16191,7 +16835,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16191
16835
|
sourceUrl: durableSource,
|
|
16192
16836
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
16193
16837
|
outputs: [{
|
|
16194
|
-
path: portableOutputPath(outFile,
|
|
16838
|
+
path: portableOutputPath(outFile, path33.dirname(ctx.manifestPath)),
|
|
16195
16839
|
sha256: sha256(buf)
|
|
16196
16840
|
}],
|
|
16197
16841
|
submittedAt: orphan.createdAt,
|
|
@@ -16215,9 +16859,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
16215
16859
|
}
|
|
16216
16860
|
}
|
|
16217
16861
|
await applyTags(provider, decisions, existingTags, { onProgress: log });
|
|
16218
|
-
const raw = JSON.parse(await
|
|
16862
|
+
const raw = JSON.parse(await readFile26(ctx.manifestPath, "utf8"));
|
|
16219
16863
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
16220
|
-
await
|
|
16864
|
+
await writeFile13(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
16221
16865
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
16222
16866
|
return result;
|
|
16223
16867
|
}
|
|
@@ -16346,6 +16990,10 @@ export {
|
|
|
16346
16990
|
EXIT_CODES,
|
|
16347
16991
|
EditorInstallError,
|
|
16348
16992
|
FAKE_PNG,
|
|
16993
|
+
FONT_COST_GENERATIONS,
|
|
16994
|
+
FONT_GLYPH_SIZES,
|
|
16995
|
+
FONT_WEIGHTS,
|
|
16996
|
+
FRAME_SET_REVISION_MODES,
|
|
16349
16997
|
FakeProvider,
|
|
16350
16998
|
GenerateRequestError,
|
|
16351
16999
|
GenerateRequestSchema,
|
|
@@ -16362,6 +17010,7 @@ export {
|
|
|
16362
17010
|
MAX_DOWNLOAD_BYTES,
|
|
16363
17011
|
MAX_HAND_EDIT_BYTES,
|
|
16364
17012
|
MAX_RETRIES,
|
|
17013
|
+
MEMBER_SET_REVISION_MODES,
|
|
16365
17014
|
MIRRORED_DIRECTION,
|
|
16366
17015
|
ManifestDriftError,
|
|
16367
17016
|
ManifestEditError,
|
|
@@ -16391,6 +17040,8 @@ export {
|
|
|
16391
17040
|
ScenarioProvider,
|
|
16392
17041
|
StyleInputSchema,
|
|
16393
17042
|
StyleSchema,
|
|
17043
|
+
UNZOOM_MAX_AREA,
|
|
17044
|
+
UNZOOM_MIN_SIDE,
|
|
16394
17045
|
UiElementSchema,
|
|
16395
17046
|
UiPieceSchema,
|
|
16396
17047
|
UnsupportedCapabilityError,
|
|
@@ -16425,6 +17076,7 @@ export {
|
|
|
16425
17076
|
currentEntryOutputPath,
|
|
16426
17077
|
currentOutputPath,
|
|
16427
17078
|
defaultOpenCommand,
|
|
17079
|
+
defaultUnzoomOut,
|
|
16428
17080
|
detachHandEdit,
|
|
16429
17081
|
detectMediaType,
|
|
16430
17082
|
doctor,
|
|
@@ -16442,10 +17094,12 @@ export {
|
|
|
16442
17094
|
fetchAssets,
|
|
16443
17095
|
fetchWithRetry,
|
|
16444
17096
|
findOrphans,
|
|
17097
|
+
fontOutputPaths,
|
|
16445
17098
|
formatCost,
|
|
16446
17099
|
frameSetFps,
|
|
16447
17100
|
galleryMediaId,
|
|
16448
17101
|
galleryMediaRoute,
|
|
17102
|
+
generateFont,
|
|
16449
17103
|
generationCost,
|
|
16450
17104
|
groupOrphansByStyle,
|
|
16451
17105
|
handEditBase,
|
|
@@ -16581,6 +17235,7 @@ export {
|
|
|
16581
17235
|
tilesCost,
|
|
16582
17236
|
toPortablePath,
|
|
16583
17237
|
totalSpend,
|
|
17238
|
+
unzoomFile,
|
|
16584
17239
|
upsert,
|
|
16585
17240
|
validateCostEstimate,
|
|
16586
17241
|
validateMedia,
|