pixelkiln 0.41.0 → 0.43.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/README.md +1 -1
- package/dist/cli.js +954 -550
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +14 -0
- package/dist/index.cjs +940 -514
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1038 -29
- package/dist/index.d.ts +1038 -29
- package/dist/index.js +902 -480
- package/dist/index.js.map +1 -1
- package/docs/GENERATORS.md +14 -6
- package/docs/MANIFEST.md +94 -6
- package/package.json +1 -1
- package/schema/manifest.schema.json +188 -3
- package/schema/recipe.schema.json +55 -0
- package/skills/pixelkiln/references/pixellab.md +12 -1
package/dist/index.js
CHANGED
|
@@ -80,7 +80,28 @@ var CHARACTER_DIRECTIONS_8 = [
|
|
|
80
80
|
"south-west"
|
|
81
81
|
];
|
|
82
82
|
var CHARACTER_DIRECTIONS_4 = ["south", "west", "east", "north"];
|
|
83
|
+
var MIRRORED_DIRECTION = {
|
|
84
|
+
south: "south",
|
|
85
|
+
"south-east": "south-west",
|
|
86
|
+
east: "west",
|
|
87
|
+
"north-east": "north-west",
|
|
88
|
+
north: "north",
|
|
89
|
+
"north-west": "north-east",
|
|
90
|
+
west: "east",
|
|
91
|
+
"south-west": "south-east"
|
|
92
|
+
};
|
|
83
93
|
var CharacterModeSchema = z.enum(["standard", "v3", "pro"]);
|
|
94
|
+
var CHARACTER_PROPORTION_PRESETS = ["default", "chibi", "cartoon", "stylized", "realistic_male", "realistic_female", "heroic"];
|
|
95
|
+
var CharacterProportionsSchema = z.union([
|
|
96
|
+
z.enum(CHARACTER_PROPORTION_PRESETS),
|
|
97
|
+
z.object({
|
|
98
|
+
headSize: z.number().min(0.5).max(2).optional(),
|
|
99
|
+
armsLength: z.number().min(0.5).max(2).optional(),
|
|
100
|
+
legsLength: z.number().min(0.5).max(2).optional(),
|
|
101
|
+
shoulderWidth: z.number().min(0.5).max(2).optional(),
|
|
102
|
+
hipWidth: z.number().min(0.5).max(2).optional()
|
|
103
|
+
}).strict()
|
|
104
|
+
]);
|
|
84
105
|
var CharacterAnimationModeSchema = z.enum(["template", "v3", "pro"]);
|
|
85
106
|
var CharacterStateSchema = z.object({
|
|
86
107
|
/** The base character, or another state, in the same style. */
|
|
@@ -239,6 +260,16 @@ var StyleObjectSchema = z.object({
|
|
|
239
260
|
directions: z.union([z.literal(4), z.literal(8)]).optional(),
|
|
240
261
|
/** `character` only. Body template: `mannequin` (default) or a quadruped (`bear`, `cat`, `dog`, `horse`, `lion`). */
|
|
241
262
|
template: z.string().min(1).optional(),
|
|
263
|
+
/** `character` only, `standard` humanoid bases. A preset or multipliers; an asset may override it. */
|
|
264
|
+
proportions: CharacterProportionsSchema.optional(),
|
|
265
|
+
/**
|
|
266
|
+
* `character` only. How closely a `standard` base and a template loop
|
|
267
|
+
* follow their text, 1 to 20; PixelLab's default is 8. The v3 and pro
|
|
268
|
+
* engines do not take it.
|
|
269
|
+
*/
|
|
270
|
+
textGuidanceScale: z.number().min(1).max(20).optional(),
|
|
271
|
+
/** `character` only. Draw `standard` bases and every loop in isometric view. */
|
|
272
|
+
isometric: z.boolean().optional(),
|
|
242
273
|
/** Fixed seed for reproducibility where the endpoint supports it. */
|
|
243
274
|
seed: z.number().int().optional(),
|
|
244
275
|
/**
|
|
@@ -339,8 +370,8 @@ var InheritedStyleSchema = StyleObjectSchema.partial().extend({
|
|
|
339
370
|
}).strict();
|
|
340
371
|
var StyleInputSchema = z.union([StyleSchema, InheritedStyleSchema]);
|
|
341
372
|
var AssetSchema = z.object({
|
|
342
|
-
/** The subject. Style wrapping comes from the style's prefix/suffix. */
|
|
343
|
-
prompt: z.string(),
|
|
373
|
+
/** The subject. Style wrapping comes from the style's prefix/suffix. Only a `mirror` may leave it out. */
|
|
374
|
+
prompt: z.string().optional(),
|
|
344
375
|
/** Subdirectory under the style's outDir. Optional. */
|
|
345
376
|
category: z.string().optional(),
|
|
346
377
|
/** Overrides the style default. `map` generator only. */
|
|
@@ -398,6 +429,32 @@ var AssetSchema = z.object({
|
|
|
398
429
|
state: CharacterStateSchema.optional(),
|
|
399
430
|
/** `character` styles: this asset is a loop of another character asset in one direction. */
|
|
400
431
|
animation: CharacterAnimationSchema.optional(),
|
|
432
|
+
/** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
|
|
433
|
+
proportions: CharacterProportionsSchema.optional(),
|
|
434
|
+
/**
|
|
435
|
+
* `character` styles, bases only: the character's own sprite, which
|
|
436
|
+
* PixelLab rotates into the other directions instead of drawing from
|
|
437
|
+
* the prompt. A manifest-relative PNG or JPEG of the south-facing
|
|
438
|
+
* sprite, or an object keyed by direction (`{ "south": ..., "east":
|
|
439
|
+
* ... }`); quadrupeds in `standard` mode need south and east, and only
|
|
440
|
+
* `standard` takes more than south. `standard` wants each image at the
|
|
441
|
+
* style's size; v3 accepts up to 256px, pro up to 168px. The prompt
|
|
442
|
+
* still guides the result.
|
|
443
|
+
*/
|
|
444
|
+
reference: z.union([z.string().min(1), z.record(CharacterDirectionSchema, z.string().min(1))]).optional(),
|
|
445
|
+
/**
|
|
446
|
+
* Another asset of the same style flipped left to right, made locally
|
|
447
|
+
* from that asset's downloaded files at no generation cost.
|
|
448
|
+
*
|
|
449
|
+
* The saving is in character loops, where each direction is its own
|
|
450
|
+
* generation: a walk facing west mirrored is the walk facing east, and a
|
|
451
|
+
* diagonal mirrors to the other diagonal. South and north mirror onto
|
|
452
|
+
* themselves and are refused. A single image or a frame set from any
|
|
453
|
+
* generator can be mirrored too; a tile set cannot, since its edges
|
|
454
|
+
* carry meaning. Mirroring swaps handedness, so an asymmetric character
|
|
455
|
+
* (a sword hand, a parted fringe) comes out the other way round.
|
|
456
|
+
*/
|
|
457
|
+
mirror: z.string().min(1).optional(),
|
|
401
458
|
/**
|
|
402
459
|
* Generated output role to place in `cell` when this asset expands to
|
|
403
460
|
* several files. Omit for ordinary single-output assets. A structural set
|
|
@@ -437,7 +494,7 @@ var AssetSchema = z.object({
|
|
|
437
494
|
path: ["revision"]
|
|
438
495
|
});
|
|
439
496
|
}
|
|
440
|
-
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation"].filter(Boolean);
|
|
497
|
+
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror"].filter(Boolean);
|
|
441
498
|
if (shapes.length > 1) {
|
|
442
499
|
context.addIssue({
|
|
443
500
|
code: z.ZodIssueCode.custom,
|
|
@@ -445,6 +502,30 @@ var AssetSchema = z.object({
|
|
|
445
502
|
path: [shapes[1]]
|
|
446
503
|
});
|
|
447
504
|
}
|
|
505
|
+
if (asset.mirror && (asset.source || Object.keys(asset.sourceByStyle).length)) {
|
|
506
|
+
context.addIssue({
|
|
507
|
+
code: z.ZodIssueCode.custom,
|
|
508
|
+
message: "a mirror is made from another asset's files; it cannot also declare a source",
|
|
509
|
+
path: ["mirror"]
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
if (asset.prompt === void 0 && !asset.mirror) {
|
|
513
|
+
context.addIssue({
|
|
514
|
+
code: z.ZodIssueCode.custom,
|
|
515
|
+
message: "Required",
|
|
516
|
+
path: ["prompt"]
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (asset.reference && (asset.state || asset.animation || asset.mirror)) {
|
|
520
|
+
context.addIssue({
|
|
521
|
+
code: z.ZodIssueCode.custom,
|
|
522
|
+
message: "a reference sprite belongs on a base; a state, animation, or mirror takes its look from its parent",
|
|
523
|
+
path: ["reference"]
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (typeof asset.reference === "object" && !Object.keys(asset.reference).length) {
|
|
527
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "a reference needs at least a south image", path: ["reference"] });
|
|
528
|
+
}
|
|
448
529
|
});
|
|
449
530
|
var DEFAULT_HISTORY_LIMIT = 5;
|
|
450
531
|
var HistoryLimitSchema = z.number().int().min(0).max(100);
|
|
@@ -534,6 +615,11 @@ var LockEntrySchema = z.object({
|
|
|
534
615
|
maskSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
535
616
|
strength: z.number().min(0).max(1).optional()
|
|
536
617
|
}).strict().nullable().default(null),
|
|
618
|
+
/** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
|
|
619
|
+
mirror: z.object({
|
|
620
|
+
sourceAssetId: z.string().min(1),
|
|
621
|
+
sourceSha256: z.string().regex(/^[0-9a-f]{64}$/)
|
|
622
|
+
}).strict().nullable().default(null),
|
|
537
623
|
/**
|
|
538
624
|
* Output hashes owned by the previous generation while its replacement is
|
|
539
625
|
* pending. They authorize replacing only unchanged PixelKiln-owned files.
|
|
@@ -865,6 +951,17 @@ var PixelLabError = class extends ProviderError {
|
|
|
865
951
|
}
|
|
866
952
|
body;
|
|
867
953
|
};
|
|
954
|
+
function proportionsPayload(proportions) {
|
|
955
|
+
if (typeof proportions === "string") return { type: "preset", name: proportions };
|
|
956
|
+
return {
|
|
957
|
+
type: "custom",
|
|
958
|
+
...proportions.headSize !== void 0 ? { head_size: proportions.headSize } : {},
|
|
959
|
+
...proportions.armsLength !== void 0 ? { arms_length: proportions.armsLength } : {},
|
|
960
|
+
...proportions.legsLength !== void 0 ? { legs_length: proportions.legsLength } : {},
|
|
961
|
+
...proportions.shoulderWidth !== void 0 ? { shoulder_width: proportions.shoulderWidth } : {},
|
|
962
|
+
...proportions.hipWidth !== void 0 ? { hip_width: proportions.hipWidth } : {}
|
|
963
|
+
};
|
|
964
|
+
}
|
|
868
965
|
var PixelLabClient = class {
|
|
869
966
|
constructor(apiKey, timeoutMs = 12e4) {
|
|
870
967
|
this.apiKey = apiKey;
|
|
@@ -874,9 +971,9 @@ var PixelLabClient = class {
|
|
|
874
971
|
apiKey;
|
|
875
972
|
http;
|
|
876
973
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
877
|
-
async request(
|
|
974
|
+
async request(path33, init) {
|
|
878
975
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
879
|
-
const res = await this.http(`${BASE}${
|
|
976
|
+
const res = await this.http(`${BASE}${path33}`, {
|
|
880
977
|
...init,
|
|
881
978
|
headers: {
|
|
882
979
|
Authorization: auth,
|
|
@@ -886,13 +983,13 @@ var PixelLabClient = class {
|
|
|
886
983
|
});
|
|
887
984
|
const text = await res.text();
|
|
888
985
|
if (!res.ok) {
|
|
889
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
986
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path33} \u2192 ${res.status}`, res.status, text);
|
|
890
987
|
}
|
|
891
988
|
if (!text) return {};
|
|
892
989
|
try {
|
|
893
990
|
return JSON.parse(text);
|
|
894
991
|
} catch {
|
|
895
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
992
|
+
throw new Error(`${init?.method ?? "GET"} ${path33} returned invalid JSON`);
|
|
896
993
|
}
|
|
897
994
|
}
|
|
898
995
|
async balance() {
|
|
@@ -1073,14 +1170,21 @@ var PixelLabClient = class {
|
|
|
1073
1170
|
* A base character. Standard mode picks the 4- or 8-direction template
|
|
1074
1171
|
* endpoint; v3 and pro have their own and always draw 8. Every one answers
|
|
1075
1172
|
* at once with the character id and a background job.
|
|
1173
|
+
*
|
|
1174
|
+
* A `reference` is the author's own sprite: standard takes one per
|
|
1175
|
+
* direction and draws the rest, v3 rotates the south one, and pro
|
|
1176
|
+
* switches to its rotate method for it. A `styleReference` is pro's style
|
|
1177
|
+
* anchor; the other engines have no such input.
|
|
1076
1178
|
*/
|
|
1077
1179
|
async createCharacter(args) {
|
|
1078
1180
|
const imageSize = { width: args.size, height: args.size };
|
|
1079
1181
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
1080
|
-
|
|
1182
|
+
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
1183
|
+
const south = args.reference?.south;
|
|
1184
|
+
let path33;
|
|
1081
1185
|
let body;
|
|
1082
1186
|
if (args.mode === "v3") {
|
|
1083
|
-
|
|
1187
|
+
path33 = "/create-character-v3";
|
|
1084
1188
|
body = {
|
|
1085
1189
|
description: args.description,
|
|
1086
1190
|
image_size: imageSize,
|
|
@@ -1089,20 +1193,23 @@ var PixelLabClient = class {
|
|
|
1089
1193
|
...args.view ? { view: args.view } : {},
|
|
1090
1194
|
...args.outline ? { outline: args.outline } : {},
|
|
1091
1195
|
...args.detail ? { detail: args.detail } : {},
|
|
1092
|
-
...args.seed != null ? { seed: args.seed } : {}
|
|
1196
|
+
...args.seed != null ? { seed: args.seed } : {},
|
|
1197
|
+
...south ? { reference_image: encode(south) } : {}
|
|
1093
1198
|
};
|
|
1094
1199
|
} else if (args.mode === "pro") {
|
|
1095
|
-
|
|
1200
|
+
path33 = "/create-character-pro";
|
|
1096
1201
|
body = {
|
|
1097
1202
|
description: args.description,
|
|
1098
1203
|
image_size: imageSize,
|
|
1099
1204
|
template_id: args.template,
|
|
1100
1205
|
no_background: args.noBackground ?? true,
|
|
1206
|
+
method: south ? "rotate_character" : "create_with_style",
|
|
1101
1207
|
...args.view ? { view: args.view } : {},
|
|
1102
|
-
...args.seed != null ? { seed: args.seed } : {}
|
|
1208
|
+
...args.seed != null ? { seed: args.seed } : {},
|
|
1209
|
+
...south ? { reference_image: encode(south) } : args.styleReference ? { reference_image: encode(args.styleReference) } : {}
|
|
1103
1210
|
};
|
|
1104
1211
|
} else {
|
|
1105
|
-
|
|
1212
|
+
path33 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
1106
1213
|
body = {
|
|
1107
1214
|
description: args.description,
|
|
1108
1215
|
image_size: imageSize,
|
|
@@ -1112,11 +1219,15 @@ var PixelLabClient = class {
|
|
|
1112
1219
|
...args.shading ? { shading: args.shading } : {},
|
|
1113
1220
|
...args.detail ? { detail: args.detail } : {},
|
|
1114
1221
|
...args.seed != null ? { seed: args.seed } : {},
|
|
1222
|
+
...args.proportions !== void 0 ? { proportions: proportionsPayload(args.proportions) } : {},
|
|
1223
|
+
...args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
|
|
1224
|
+
...args.isometric !== void 0 ? { isometric: args.isometric } : {},
|
|
1225
|
+
...args.reference && Object.keys(args.reference).length ? { directions: Object.fromEntries(Object.entries(args.reference).flatMap(([direction, image]) => image ? [[direction, encode(image)]] : [])) } : {},
|
|
1115
1226
|
...palette
|
|
1116
1227
|
};
|
|
1117
1228
|
}
|
|
1118
|
-
const raw = await this.request(
|
|
1119
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
1229
|
+
const raw = await this.request(path33, { method: "POST", body: JSON.stringify(body) });
|
|
1230
|
+
return validateResponse(CharacterSubmitSchema, raw, path33.slice(1));
|
|
1120
1231
|
}
|
|
1121
1232
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
1122
1233
|
async createCharacterState(args) {
|
|
@@ -1151,6 +1262,8 @@ var PixelLabClient = class {
|
|
|
1151
1262
|
...args.actionDescription ? { action_description: args.actionDescription } : {},
|
|
1152
1263
|
...args.mode === "v3" && args.frameCount ? { frame_count: args.frameCount } : {},
|
|
1153
1264
|
...args.mode === "v3" && args.keepFirstFrame === false ? { keep_first_frame: false } : {},
|
|
1265
|
+
...args.mode === "template" && args.textGuidanceScale !== void 0 ? { text_guidance_scale: args.textGuidanceScale } : {},
|
|
1266
|
+
...args.isometric !== void 0 ? { isometric: args.isometric } : {},
|
|
1154
1267
|
...args.seed != null ? { seed: args.seed } : {}
|
|
1155
1268
|
})
|
|
1156
1269
|
});
|
|
@@ -1753,6 +1866,82 @@ import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
|
1753
1866
|
import { randomUUID } from "crypto";
|
|
1754
1867
|
import os from "os";
|
|
1755
1868
|
import path from "path";
|
|
1869
|
+
|
|
1870
|
+
// src/hash.ts
|
|
1871
|
+
import { createHash } from "crypto";
|
|
1872
|
+
import { createReadStream } from "fs";
|
|
1873
|
+
function sha256(data) {
|
|
1874
|
+
return createHash("sha256").update(data).digest("hex");
|
|
1875
|
+
}
|
|
1876
|
+
async function sha256File(path33) {
|
|
1877
|
+
return new Promise((resolve, reject) => {
|
|
1878
|
+
const hash = createHash("sha256");
|
|
1879
|
+
const stream = createReadStream(path33);
|
|
1880
|
+
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
1881
|
+
stream.on("error", reject);
|
|
1882
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
|
|
1886
|
+
return sha256(
|
|
1887
|
+
JSON.stringify({
|
|
1888
|
+
// Preserve every existing PixelLab hash while making a provider switch
|
|
1889
|
+
// invalidate the spec. Older manifests implicitly mean pixellab.
|
|
1890
|
+
provider: spec.provider === "pixellab" ? void 0 : spec.provider,
|
|
1891
|
+
providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
|
|
1892
|
+
providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
|
|
1893
|
+
generator: spec.generator,
|
|
1894
|
+
prompt: spec.prompt,
|
|
1895
|
+
width: spec.width,
|
|
1896
|
+
height: spec.height,
|
|
1897
|
+
view: spec.view,
|
|
1898
|
+
outline: spec.outline ?? null,
|
|
1899
|
+
shading: spec.shading ?? null,
|
|
1900
|
+
detail: spec.detail ?? null,
|
|
1901
|
+
seed: spec.seed ?? null,
|
|
1902
|
+
palette: spec.palette,
|
|
1903
|
+
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
1904
|
+
// undefined for every other generator. `tileSize` is intentionally
|
|
1905
|
+
// absent; width/height are derived from it, so it is already covered.
|
|
1906
|
+
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
1907
|
+
tileType: spec.tileType,
|
|
1908
|
+
tileView: spec.tileView,
|
|
1909
|
+
tileFeature: spec.tileFeature,
|
|
1910
|
+
outlineMode: spec.outlineMode,
|
|
1911
|
+
styleImages: styleImageHashes,
|
|
1912
|
+
// A character family: the engine and rotations of a base, the parent's
|
|
1913
|
+
// generated bytes for a state or animation (so a regenerated parent
|
|
1914
|
+
// makes its children stale), and each shape's own settings.
|
|
1915
|
+
character: spec.character ? {
|
|
1916
|
+
kind: spec.character.kind,
|
|
1917
|
+
mode: spec.character.mode,
|
|
1918
|
+
directions: spec.character.directions,
|
|
1919
|
+
template: spec.character.template,
|
|
1920
|
+
parent: spec.character.parentAssetId,
|
|
1921
|
+
parentSha256: spec.character.parentSha256 ?? null,
|
|
1922
|
+
state: spec.character.state,
|
|
1923
|
+
animation: spec.character.animation,
|
|
1924
|
+
// Absent keys keep the hashes of manifests that never set these.
|
|
1925
|
+
proportions: spec.character.proportions,
|
|
1926
|
+
textGuidanceScale: spec.character.textGuidanceScale,
|
|
1927
|
+
isometric: spec.character.isometric,
|
|
1928
|
+
reference: spec.character.reference ? Object.fromEntries(Object.entries(spec.character.reference).map(([direction, image]) => [direction, image.sha256])) : void 0
|
|
1929
|
+
} : void 0,
|
|
1930
|
+
// A mirror's bytes come from its source's recorded outputs; the plan
|
|
1931
|
+
// compares those directly, so only the choice of source is identity.
|
|
1932
|
+
mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
|
|
1933
|
+
revision: spec.revision ? {
|
|
1934
|
+
mode: spec.revision.mode,
|
|
1935
|
+
from: spec.revision.sourceAssetId,
|
|
1936
|
+
sourceSha256: spec.revision.sourceSha256,
|
|
1937
|
+
maskSha256: spec.revision.maskSha256,
|
|
1938
|
+
strength: spec.revision.strength
|
|
1939
|
+
} : void 0
|
|
1940
|
+
})
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// src/providers/pixellab.ts
|
|
1756
1945
|
function pixelLabObjectUrl(generator, objectId) {
|
|
1757
1946
|
if (!objectId) return null;
|
|
1758
1947
|
if (generator === "map" || generator === "1dir") {
|
|
@@ -1875,8 +2064,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1875
2064
|
"high detail"
|
|
1876
2065
|
]);
|
|
1877
2066
|
}
|
|
1878
|
-
if (spec.generator === "character") this.validateCharacter(spec);
|
|
1879
|
-
if ((spec.generator === "map" || spec.generator === "pixflux"
|
|
2067
|
+
if (spec.generator === "character") this.validateCharacter(spec, styleImages);
|
|
2068
|
+
if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
|
|
1880
2069
|
throw new Error(`PixelLab ${spec.generator} does not support style images`);
|
|
1881
2070
|
}
|
|
1882
2071
|
for (const image of styleImages) {
|
|
@@ -1892,9 +2081,61 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1892
2081
|
);
|
|
1893
2082
|
}
|
|
1894
2083
|
}
|
|
1895
|
-
validateCharacter(spec) {
|
|
2084
|
+
validateCharacter(spec, styleImages = []) {
|
|
1896
2085
|
const character = spec.character;
|
|
1897
2086
|
if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
|
|
2087
|
+
const label = `${spec.styleId}/${spec.assetId}`;
|
|
2088
|
+
if (character.kind === "base" && character.proportions !== void 0) {
|
|
2089
|
+
if (character.mode !== "standard") {
|
|
2090
|
+
throw new Error(`${label}: proportions apply to standard bases; the ${character.mode} engine has none to set`);
|
|
2091
|
+
}
|
|
2092
|
+
if (character.template !== "mannequin") {
|
|
2093
|
+
throw new Error(`${label}: proportions apply to the mannequin template; ${character.template} is a quadruped`);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
if (character.reference) {
|
|
2097
|
+
const given = Object.keys(character.reference);
|
|
2098
|
+
if (!character.reference.south) throw new Error(`${label}: a reference needs a south-facing sprite`);
|
|
2099
|
+
if (character.mode !== "standard") {
|
|
2100
|
+
if (given.length > 1) throw new Error(`${label}: PixelLab ${character.mode} rotates one south-facing reference; drop the other directions`);
|
|
2101
|
+
const limit = character.mode === "v3" ? 256 : 168;
|
|
2102
|
+
const south = character.reference.south;
|
|
2103
|
+
if (south.width > limit || south.height > limit) {
|
|
2104
|
+
throw new Error(`${label}: reference is ${south.width}x${south.height}; PixelLab ${character.mode} takes up to ${limit}px`);
|
|
2105
|
+
}
|
|
2106
|
+
} else {
|
|
2107
|
+
const allowed = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
2108
|
+
for (const direction of given) {
|
|
2109
|
+
if (!allowed.includes(direction)) {
|
|
2110
|
+
throw new Error(`${label}: reference "${direction}" is not one of this character's ${character.directions} directions`);
|
|
2111
|
+
}
|
|
2112
|
+
const image = character.reference[direction];
|
|
2113
|
+
if (image.width !== spec.width || image.height !== spec.height) {
|
|
2114
|
+
throw new Error(
|
|
2115
|
+
`${label}: reference (${direction}) is ${image.width}x${image.height}; standard mode wants each image at the style's size, ${spec.width}x${spec.height}`
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
if (character.template !== "mannequin" && !character.reference.east) {
|
|
2120
|
+
throw new Error(`${label}: a ${character.template} reference needs south and east images`);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
if (styleImages.length) {
|
|
2125
|
+
if (character.mode !== "pro") {
|
|
2126
|
+
throw new Error(
|
|
2127
|
+
`PixelLab ${character.mode} characters take no style images; give a base its own sprite with \`reference\`, or use mode pro for a style anchor`
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
if (styleImages.length > 1) throw new Error("PixelLab pro characters take one style image");
|
|
2131
|
+
if (character.kind === "base" && character.reference) {
|
|
2132
|
+
throw new Error(`${label}: a pro base rotating its own reference takes no style image`);
|
|
2133
|
+
}
|
|
2134
|
+
const image = styleImages[0];
|
|
2135
|
+
if (image.width > 168 || image.height > 168) {
|
|
2136
|
+
throw new Error(`PixelLab pro character style image is ${image.width}x${image.height}; the limit is 168px`);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
1898
2139
|
const maxSize = character.mode === "v3" ? 256 : 128;
|
|
1899
2140
|
if (spec.width < 16 || spec.height < 16 || spec.width > maxSize || spec.height > maxSize) {
|
|
1900
2141
|
throw new Error(`PixelLab ${character.mode} characters must be between 16 and ${maxSize} pixels`);
|
|
@@ -1930,10 +2171,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1930
2171
|
* asset so a later run can find it, and clears its own earlier take for
|
|
1931
2172
|
* that direction first, since PixelLab skips a direction that exists.
|
|
1932
2173
|
*/
|
|
1933
|
-
async submitCharacter(spec, context) {
|
|
2174
|
+
async submitCharacter(spec, styleImages, context) {
|
|
1934
2175
|
const character = spec.character;
|
|
1935
2176
|
if (character.kind === "base") {
|
|
1936
2177
|
const swatch = spec.palette.length && character.mode === "standard" ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
2178
|
+
const styleImage = styleImages[0];
|
|
1937
2179
|
const res2 = await this.client.createCharacter({
|
|
1938
2180
|
mode: character.mode,
|
|
1939
2181
|
description: spec.prompt,
|
|
@@ -1946,7 +2188,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1946
2188
|
detail: spec.detail,
|
|
1947
2189
|
seed: spec.seed,
|
|
1948
2190
|
noBackground: spec.noBackground,
|
|
1949
|
-
paletteSwatchBase64: swatch
|
|
2191
|
+
paletteSwatchBase64: swatch,
|
|
2192
|
+
proportions: character.proportions,
|
|
2193
|
+
textGuidanceScale: character.textGuidanceScale,
|
|
2194
|
+
isometric: character.isometric,
|
|
2195
|
+
reference: character.reference ? readReference(spec, character.reference) : void 0,
|
|
2196
|
+
styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0
|
|
1950
2197
|
});
|
|
1951
2198
|
return { jobId: res2.character_id, metadata: { character: { kind: "base", characterId: res2.character_id, mode: character.mode, directions: character.directions, backgroundJobId: res2.background_job_id } } };
|
|
1952
2199
|
}
|
|
@@ -1988,7 +2235,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
1988
2235
|
frameCount: animation.frames,
|
|
1989
2236
|
keepFirstFrame: animation.keepFirstFrame,
|
|
1990
2237
|
directions: [animation.direction],
|
|
1991
|
-
seed: spec.seed
|
|
2238
|
+
seed: spec.seed,
|
|
2239
|
+
textGuidanceScale: character.textGuidanceScale,
|
|
2240
|
+
isometric: character.isometric
|
|
1992
2241
|
});
|
|
1993
2242
|
const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
|
|
1994
2243
|
return {
|
|
@@ -2078,7 +2327,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
2078
2327
|
}
|
|
2079
2328
|
async submit(spec, styleImages, context) {
|
|
2080
2329
|
this.validate(spec, styleImages);
|
|
2081
|
-
if (spec.generator === "character") return this.submitCharacter(spec, context);
|
|
2330
|
+
if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
|
|
2082
2331
|
if (spec.generator === "pixflux") {
|
|
2083
2332
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
2084
2333
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -2345,6 +2594,17 @@ function firstUrl(urls) {
|
|
|
2345
2594
|
if (!urls) return null;
|
|
2346
2595
|
return Object.values(urls).find((u) => typeof u === "string") ?? null;
|
|
2347
2596
|
}
|
|
2597
|
+
function readReference(spec, reference) {
|
|
2598
|
+
const images = {};
|
|
2599
|
+
for (const [direction, image] of Object.entries(reference)) {
|
|
2600
|
+
const bytes = readFileSync(image.path);
|
|
2601
|
+
if (sha256(bytes) !== image.sha256) {
|
|
2602
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: reference (${direction}) changed after the manifest was resolved: ${image.path}`);
|
|
2603
|
+
}
|
|
2604
|
+
images[direction] = { base64: bytes.toString("base64"), format: image.format };
|
|
2605
|
+
}
|
|
2606
|
+
return images;
|
|
2607
|
+
}
|
|
2348
2608
|
function rotationSources(character) {
|
|
2349
2609
|
const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
2350
2610
|
const sources = [];
|
|
@@ -2415,9 +2675,9 @@ var RetroDiffusionClient = class {
|
|
|
2415
2675
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
2416
2676
|
return Buffer.from(await response.arrayBuffer());
|
|
2417
2677
|
}
|
|
2418
|
-
async call(
|
|
2678
|
+
async call(path33, init = {}) {
|
|
2419
2679
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
2420
|
-
const response = await this.request(`${this.baseUrl}${
|
|
2680
|
+
const response = await this.request(`${this.baseUrl}${path33}`, {
|
|
2421
2681
|
...init,
|
|
2422
2682
|
headers: {
|
|
2423
2683
|
"Content-Type": "application/json",
|
|
@@ -3216,74 +3476,6 @@ function redactScenarioError(value, secrets) {
|
|
|
3216
3476
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3217
3477
|
import { readFile } from "fs/promises";
|
|
3218
3478
|
import path2 from "path";
|
|
3219
|
-
|
|
3220
|
-
// src/hash.ts
|
|
3221
|
-
import { createHash } from "crypto";
|
|
3222
|
-
import { createReadStream } from "fs";
|
|
3223
|
-
function sha256(data) {
|
|
3224
|
-
return createHash("sha256").update(data).digest("hex");
|
|
3225
|
-
}
|
|
3226
|
-
async function sha256File(path32) {
|
|
3227
|
-
return new Promise((resolve, reject) => {
|
|
3228
|
-
const hash = createHash("sha256");
|
|
3229
|
-
const stream = createReadStream(path32);
|
|
3230
|
-
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
3231
|
-
stream.on("error", reject);
|
|
3232
|
-
stream.on("end", () => resolve(hash.digest("hex")));
|
|
3233
|
-
});
|
|
3234
|
-
}
|
|
3235
|
-
function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
|
|
3236
|
-
return sha256(
|
|
3237
|
-
JSON.stringify({
|
|
3238
|
-
// Preserve every existing PixelLab hash while making a provider switch
|
|
3239
|
-
// invalidate the spec. Older manifests implicitly mean pixellab.
|
|
3240
|
-
provider: spec.provider === "pixellab" ? void 0 : spec.provider,
|
|
3241
|
-
providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
|
|
3242
|
-
providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
|
|
3243
|
-
generator: spec.generator,
|
|
3244
|
-
prompt: spec.prompt,
|
|
3245
|
-
width: spec.width,
|
|
3246
|
-
height: spec.height,
|
|
3247
|
-
view: spec.view,
|
|
3248
|
-
outline: spec.outline ?? null,
|
|
3249
|
-
shading: spec.shading ?? null,
|
|
3250
|
-
detail: spec.detail ?? null,
|
|
3251
|
-
seed: spec.seed ?? null,
|
|
3252
|
-
palette: spec.palette,
|
|
3253
|
-
// `noBackground` only reaches the wire for pixflux; the tile fields are
|
|
3254
|
-
// undefined for every other generator. `tileSize` is intentionally
|
|
3255
|
-
// absent; width/height are derived from it, so it is already covered.
|
|
3256
|
-
noBackground: spec.generator === "pixflux" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
3257
|
-
tileType: spec.tileType,
|
|
3258
|
-
tileView: spec.tileView,
|
|
3259
|
-
tileFeature: spec.tileFeature,
|
|
3260
|
-
outlineMode: spec.outlineMode,
|
|
3261
|
-
styleImages: styleImageHashes,
|
|
3262
|
-
// A character family: the engine and rotations of a base, the parent's
|
|
3263
|
-
// generated bytes for a state or animation (so a regenerated parent
|
|
3264
|
-
// makes its children stale), and each shape's own settings.
|
|
3265
|
-
character: spec.character ? {
|
|
3266
|
-
kind: spec.character.kind,
|
|
3267
|
-
mode: spec.character.mode,
|
|
3268
|
-
directions: spec.character.directions,
|
|
3269
|
-
template: spec.character.template,
|
|
3270
|
-
parent: spec.character.parentAssetId,
|
|
3271
|
-
parentSha256: spec.character.parentSha256 ?? null,
|
|
3272
|
-
state: spec.character.state,
|
|
3273
|
-
animation: spec.character.animation
|
|
3274
|
-
} : void 0,
|
|
3275
|
-
revision: spec.revision ? {
|
|
3276
|
-
mode: spec.revision.mode,
|
|
3277
|
-
from: spec.revision.sourceAssetId,
|
|
3278
|
-
sourceSha256: spec.revision.sourceSha256,
|
|
3279
|
-
maskSha256: spec.revision.maskSha256,
|
|
3280
|
-
strength: spec.revision.strength
|
|
3281
|
-
} : void 0
|
|
3282
|
-
})
|
|
3283
|
-
);
|
|
3284
|
-
}
|
|
3285
|
-
|
|
3286
|
-
// src/providers/comfyui.ts
|
|
3287
3479
|
var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
|
|
3288
3480
|
var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
|
|
3289
3481
|
"prompt",
|
|
@@ -4234,6 +4426,18 @@ var FakeProvider = class {
|
|
|
4234
4426
|
const objectId = job.objectId ?? `obj-${jobId}`;
|
|
4235
4427
|
job.objectId = objectId;
|
|
4236
4428
|
this.register(objectId, job.spec);
|
|
4429
|
+
const character = job.spec.character;
|
|
4430
|
+
if (character) {
|
|
4431
|
+
const roles = character.kind === "animation" ? Array.from({ length: character.animation.frames }, (_, i) => `frame-${String(i).padStart(2, "0")}`) : character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
4432
|
+
const sources = roles.map((role) => ({ url: `fake://${objectId}/${role}.png`, role }));
|
|
4433
|
+
return {
|
|
4434
|
+
status: "ready",
|
|
4435
|
+
objectId,
|
|
4436
|
+
sourceUrl: sources[0].url,
|
|
4437
|
+
sources,
|
|
4438
|
+
...character.kind === "animation" ? { metadata: { frameSet: { fps: character.animation.fps, count: roles.length } } } : {}
|
|
4439
|
+
};
|
|
4440
|
+
}
|
|
4237
4441
|
const sourceUrl = `fake://${objectId}.png`;
|
|
4238
4442
|
return { status: "ready", objectId, sourceUrl, sources: [{ url: sourceUrl }] };
|
|
4239
4443
|
}
|
|
@@ -4898,16 +5102,16 @@ async function resolveSpecs(loaded, filter) {
|
|
|
4898
5102
|
}
|
|
4899
5103
|
}
|
|
4900
5104
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
4901
|
-
async function loadStyleImage(rel) {
|
|
5105
|
+
async function loadStyleImage(rel, what = "Style image") {
|
|
4902
5106
|
const abs = path5.resolve(root, rel);
|
|
4903
5107
|
let hit = styleImageCache.get(abs);
|
|
4904
5108
|
if (!hit) {
|
|
4905
|
-
if (!existsSync4(abs)) throw new Error(
|
|
5109
|
+
if (!existsSync4(abs)) throw new Error(`${what} not found: ${abs}`);
|
|
4906
5110
|
const buf = await readFile3(abs);
|
|
4907
5111
|
const metadata = imageMetadata(buf);
|
|
4908
|
-
if (!metadata) throw new Error(
|
|
5112
|
+
if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
|
|
4909
5113
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
4910
|
-
throw new Error(
|
|
5114
|
+
throw new Error(`${what} has invalid dimensions: ${abs}`);
|
|
4911
5115
|
}
|
|
4912
5116
|
hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
|
|
4913
5117
|
styleImageCache.set(abs, hit);
|
|
@@ -4982,7 +5186,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
4982
5186
|
);
|
|
4983
5187
|
}
|
|
4984
5188
|
const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
|
|
4985
|
-
const subject = asset.promptByStyle[styleId] ?? asset.prompt;
|
|
5189
|
+
const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
|
|
4986
5190
|
const prompt = generator === "character" && characterKind !== "base" ? subject.trim() : [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
4987
5191
|
const relFile = asset.file ?? path5.join(asset.category ?? "", `${assetId}.png`);
|
|
4988
5192
|
const outFile = path5.resolve(root, style.outDir, relFile);
|
|
@@ -5014,7 +5218,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
5014
5218
|
tileView: generator === "tiles" ? style.tileView : void 0,
|
|
5015
5219
|
tileFeature: generator === "tiles" ? style.tileFeature : void 0,
|
|
5016
5220
|
outlineMode: generator === "tiles" ? style.outlineMode : void 0,
|
|
5017
|
-
...generator === "character" ? { character: resolveCharacterShape(asset, style, characterKind) } : {},
|
|
5221
|
+
...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
5018
5222
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
|
|
5019
5223
|
costUnit: "generations",
|
|
5020
5224
|
candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
|
|
@@ -5057,11 +5261,42 @@ async function resolveSpecs(loaded, filter) {
|
|
|
5057
5261
|
const resolved = styleSpecs.get(assetId);
|
|
5058
5262
|
if (!resolved) {
|
|
5059
5263
|
throw new Error(
|
|
5060
|
-
`Asset "${assetId}" is not available in style "${styleId}"; revision parents must
|
|
5264
|
+
`Asset "${assetId}" is not available in style "${styleId}"; revision parents, character parents, and mirror sources must be in the same style.`
|
|
5061
5265
|
);
|
|
5062
5266
|
}
|
|
5063
5267
|
if (finalized.has(assetId)) return resolved;
|
|
5064
5268
|
const asset = manifest.assets[assetId];
|
|
5269
|
+
if (asset.mirror) {
|
|
5270
|
+
if (asset.mirror === assetId) throw new Error(`assets.${assetId}: an asset cannot mirror itself`);
|
|
5271
|
+
const sourceSpec = await finalize(asset.mirror);
|
|
5272
|
+
if (sourceSpec.generator === "tiles") {
|
|
5273
|
+
throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
|
|
5274
|
+
}
|
|
5275
|
+
if (sourceSpec.character?.kind === "animation") {
|
|
5276
|
+
const facing = sourceSpec.character.animation.direction;
|
|
5277
|
+
const mirrored = MIRRORED_DIRECTION[facing];
|
|
5278
|
+
if (mirrored === facing) {
|
|
5279
|
+
throw new Error(
|
|
5280
|
+
`assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
|
|
5281
|
+
);
|
|
5282
|
+
}
|
|
5283
|
+
resolved.character = { ...sourceSpec.character, animation: { ...sourceSpec.character.animation, direction: mirrored } };
|
|
5284
|
+
} else if (sourceSpec.character) {
|
|
5285
|
+
resolved.character = { ...sourceSpec.character };
|
|
5286
|
+
}
|
|
5287
|
+
resolved.mirror = { sourceAssetId: asset.mirror, sourceSpec };
|
|
5288
|
+
resolved.generator = sourceSpec.generator;
|
|
5289
|
+
resolved.width = sourceSpec.width;
|
|
5290
|
+
resolved.height = sourceSpec.height;
|
|
5291
|
+
resolved.size = sourceSpec.size;
|
|
5292
|
+
resolved.prompt = "";
|
|
5293
|
+
resolved.specHash = specHash(resolved, styleImageHashes, providerOptionIdentity, providerInputIdentities.get(assetId));
|
|
5294
|
+
resolved.cost = 0;
|
|
5295
|
+
resolved.costUnit = sourceSpec.costUnit;
|
|
5296
|
+
resolved.candidates = 1;
|
|
5297
|
+
finalized.add(assetId);
|
|
5298
|
+
return resolved;
|
|
5299
|
+
}
|
|
5065
5300
|
const characterParent = asset.state?.of ?? asset.animation?.of;
|
|
5066
5301
|
if (resolved.character && characterParent) {
|
|
5067
5302
|
const parentSpec = await finalize(characterParent);
|
|
@@ -5228,15 +5463,33 @@ function imageMetadata(buf) {
|
|
|
5228
5463
|
}
|
|
5229
5464
|
return null;
|
|
5230
5465
|
}
|
|
5231
|
-
function resolveCharacterShape(asset, style, kind) {
|
|
5466
|
+
async function resolveCharacterShape(asset, style, kind, files) {
|
|
5232
5467
|
const mode2 = style.mode ?? "standard";
|
|
5233
5468
|
const directions = mode2 === "standard" ? style.directions ?? 8 : 8;
|
|
5469
|
+
const proportions = asset.proportions ?? style.proportions;
|
|
5234
5470
|
const shape = {
|
|
5235
5471
|
kind,
|
|
5236
5472
|
mode: mode2,
|
|
5237
5473
|
directions,
|
|
5238
|
-
template: style.template ?? "mannequin"
|
|
5474
|
+
template: style.template ?? "mannequin",
|
|
5475
|
+
...proportions !== void 0 ? { proportions } : {},
|
|
5476
|
+
...style.textGuidanceScale !== void 0 ? { textGuidanceScale: style.textGuidanceScale } : {},
|
|
5477
|
+
...style.isometric !== void 0 ? { isometric: style.isometric } : {}
|
|
5239
5478
|
};
|
|
5479
|
+
if (asset.reference) {
|
|
5480
|
+
const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
|
|
5481
|
+
shape.reference = {};
|
|
5482
|
+
for (const [direction, rel] of Object.entries(byDirection)) {
|
|
5483
|
+
const image = await files.load(rel, `Reference sprite (${direction})`);
|
|
5484
|
+
shape.reference[direction] = {
|
|
5485
|
+
path: path5.resolve(files.root, rel),
|
|
5486
|
+
sha256: image.hash,
|
|
5487
|
+
width: image.width,
|
|
5488
|
+
height: image.height,
|
|
5489
|
+
format: image.format
|
|
5490
|
+
};
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5240
5493
|
if (asset.state) {
|
|
5241
5494
|
shape.state = {
|
|
5242
5495
|
paletteFromReference: asset.state.paletteFromReference,
|
|
@@ -5261,7 +5514,7 @@ function characterDirections(directions) {
|
|
|
5261
5514
|
}
|
|
5262
5515
|
|
|
5263
5516
|
// src/project.ts
|
|
5264
|
-
import
|
|
5517
|
+
import path13 from "path";
|
|
5265
5518
|
|
|
5266
5519
|
// src/env.ts
|
|
5267
5520
|
import { readFileSync as readFileSync2, existsSync as existsSync5 } from "fs";
|
|
@@ -5309,23 +5562,225 @@ function parseEnv(contents) {
|
|
|
5309
5562
|
|
|
5310
5563
|
// src/pipeline/plan.ts
|
|
5311
5564
|
import { existsSync as existsSync8 } from "fs";
|
|
5312
|
-
import
|
|
5565
|
+
import path12 from "path";
|
|
5566
|
+
|
|
5567
|
+
// src/pipeline/mirror.ts
|
|
5568
|
+
import { mkdir as mkdir2, readFile as readFile4, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
5569
|
+
import path7 from "path";
|
|
5570
|
+
|
|
5571
|
+
// src/palette.ts
|
|
5572
|
+
function normalizePalette(values) {
|
|
5573
|
+
const colors = /* @__PURE__ */ new Map();
|
|
5574
|
+
for (const value of values) {
|
|
5575
|
+
const rgb = parseHex(value);
|
|
5576
|
+
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
5577
|
+
if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
|
|
5578
|
+
}
|
|
5579
|
+
if (colors.size < 2 || colors.size > 256) {
|
|
5580
|
+
throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
5581
|
+
}
|
|
5582
|
+
return [...colors.values()];
|
|
5583
|
+
}
|
|
5584
|
+
function colorDistance(a, b) {
|
|
5585
|
+
const rmean = (a.r + b.r) / 2;
|
|
5586
|
+
const dr = a.r - b.r;
|
|
5587
|
+
const dg = a.g - b.g;
|
|
5588
|
+
const db = a.b - b.b;
|
|
5589
|
+
return Math.sqrt(
|
|
5590
|
+
(512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
|
|
5591
|
+
);
|
|
5592
|
+
}
|
|
5593
|
+
function quantize(png, palette) {
|
|
5594
|
+
const colors = normalizePalette(palette);
|
|
5595
|
+
const pixels = Buffer.from(png.pixels);
|
|
5596
|
+
const nearestOf = /* @__PURE__ */ new Map();
|
|
5597
|
+
let changed = 0;
|
|
5598
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
5599
|
+
const alpha = pixels[i + 3];
|
|
5600
|
+
if (alpha === 0) {
|
|
5601
|
+
if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
|
|
5602
|
+
pixels[i] = 0;
|
|
5603
|
+
pixels[i + 1] = 0;
|
|
5604
|
+
pixels[i + 2] = 0;
|
|
5605
|
+
continue;
|
|
5606
|
+
}
|
|
5607
|
+
const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
|
|
5608
|
+
let nearest = nearestOf.get(key);
|
|
5609
|
+
if (!nearest) {
|
|
5610
|
+
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
5611
|
+
nearest = colors[0];
|
|
5612
|
+
let distance = colorDistance(pixel, nearest);
|
|
5613
|
+
for (let j = 1; j < colors.length; j++) {
|
|
5614
|
+
const candidate = colors[j];
|
|
5615
|
+
const candidateDistance = colorDistance(pixel, candidate);
|
|
5616
|
+
if (candidateDistance < distance) {
|
|
5617
|
+
nearest = candidate;
|
|
5618
|
+
distance = candidateDistance;
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5621
|
+
nearestOf.set(key, nearest);
|
|
5622
|
+
}
|
|
5623
|
+
if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
|
|
5624
|
+
pixels[i] = nearest.r;
|
|
5625
|
+
pixels[i + 1] = nearest.g;
|
|
5626
|
+
pixels[i + 2] = nearest.b;
|
|
5627
|
+
}
|
|
5628
|
+
return { png: { width: png.width, height: png.height, pixels }, changed };
|
|
5629
|
+
}
|
|
5630
|
+
function quantizeToPalette(png, palette) {
|
|
5631
|
+
return quantize(png, palette).png;
|
|
5632
|
+
}
|
|
5633
|
+
|
|
5634
|
+
// src/pipeline/postprocess.ts
|
|
5635
|
+
function postprocessFor(spec) {
|
|
5636
|
+
if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
|
|
5637
|
+
return {
|
|
5638
|
+
palette: {
|
|
5639
|
+
colors: normalizePalette(spec.palette).map((color) => color.hex),
|
|
5640
|
+
dither: "none",
|
|
5641
|
+
distance: "redmean"
|
|
5642
|
+
}
|
|
5643
|
+
};
|
|
5644
|
+
}
|
|
5645
|
+
function samePostprocess(a, b) {
|
|
5646
|
+
const pa = a?.palette;
|
|
5647
|
+
const pb = b?.palette;
|
|
5648
|
+
if (!pa && !pb) return true;
|
|
5649
|
+
if (!pa || !pb) return false;
|
|
5650
|
+
return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
|
|
5651
|
+
}
|
|
5652
|
+
function postprocessCurrent(entry, spec) {
|
|
5653
|
+
return samePostprocess(entry.postprocess, postprocessFor(spec));
|
|
5654
|
+
}
|
|
5655
|
+
function applyPostprocess(bytes, mediaType, record) {
|
|
5656
|
+
if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
|
|
5657
|
+
const png = decodePng(bytes);
|
|
5658
|
+
const { png: snapped, changed } = quantize(png, record.palette.colors);
|
|
5659
|
+
if (!changed) return { bytes, changed: false };
|
|
5660
|
+
return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
|
|
5661
|
+
}
|
|
5662
|
+
|
|
5663
|
+
// src/pipeline/mirror.ts
|
|
5664
|
+
function mirrorSourceHash(source) {
|
|
5665
|
+
return sha256(JSON.stringify(source.outputs.map((output) => output.sha256)));
|
|
5666
|
+
}
|
|
5667
|
+
function mirrorCurrent(entry, source) {
|
|
5668
|
+
return Boolean(entry.mirror && source && entry.mirror.sourceSha256 === mirrorSourceHash(source));
|
|
5669
|
+
}
|
|
5670
|
+
function mirroredRole(role) {
|
|
5671
|
+
if (role && role in MIRRORED_DIRECTION) return MIRRORED_DIRECTION[role];
|
|
5672
|
+
return role;
|
|
5673
|
+
}
|
|
5674
|
+
function flipHorizontal(png) {
|
|
5675
|
+
const { width, height, pixels } = decodePng(png);
|
|
5676
|
+
const flipped = Buffer.alloc(pixels.length);
|
|
5677
|
+
const stride = width * 4;
|
|
5678
|
+
for (let y = 0; y < height; y++) {
|
|
5679
|
+
const row = y * stride;
|
|
5680
|
+
for (let x = 0; x < width; x++) {
|
|
5681
|
+
pixels.copy(flipped, row + (width - 1 - x) * 4, row + x * 4, row + x * 4 + 4);
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
return encodeRgbaPng(width, height, flipped);
|
|
5685
|
+
}
|
|
5686
|
+
async function deriveMirror(spec, lock, lockPath, opts = {}) {
|
|
5687
|
+
const log = opts.onProgress ?? (() => {
|
|
5688
|
+
});
|
|
5689
|
+
const mirror = spec.mirror;
|
|
5690
|
+
const key = lockKey(spec.styleId, spec.assetId);
|
|
5691
|
+
const sourceKey = lockKey(spec.styleId, mirror.sourceAssetId);
|
|
5692
|
+
const source = lock.entries[sourceKey];
|
|
5693
|
+
if (!source || source.status !== "downloaded" || !source.outputs.length) {
|
|
5694
|
+
throw new Error(`mirror source ${sourceKey} has no downloaded outputs`);
|
|
5695
|
+
}
|
|
5696
|
+
const members = source.outputs.map((output, index) => ({
|
|
5697
|
+
file: currentEntryOutputPath(source, mirror.sourceSpec, index),
|
|
5698
|
+
role: mirroredRole(output.role)
|
|
5699
|
+
}));
|
|
5700
|
+
if (members.every((member) => member.role && CHARACTER_DIRECTIONS_8.includes(member.role))) {
|
|
5701
|
+
members.sort((a, b) => CHARACTER_DIRECTIONS_8.indexOf(a.role) - CHARACTER_DIRECTIONS_8.indexOf(b.role));
|
|
5702
|
+
}
|
|
5703
|
+
const outputs = [];
|
|
5704
|
+
const written = [];
|
|
5705
|
+
for (const [index, member] of members.entries()) {
|
|
5706
|
+
const bytes = flipHorizontal(await readFile4(member.file));
|
|
5707
|
+
const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
|
|
5708
|
+
await mkdir2(path7.dirname(target), { recursive: true });
|
|
5709
|
+
const tmp = `${target}.pixelkiln.tmp`;
|
|
5710
|
+
await writeFile2(tmp, bytes);
|
|
5711
|
+
await rename2(tmp, target);
|
|
5712
|
+
written.push(target);
|
|
5713
|
+
outputs.push({
|
|
5714
|
+
path: portableOutputPath(target, spec.root),
|
|
5715
|
+
sha256: sha256(bytes),
|
|
5716
|
+
...member.role ? { role: member.role } : {},
|
|
5717
|
+
mediaType: MediaType.PNG
|
|
5718
|
+
});
|
|
5719
|
+
}
|
|
5720
|
+
const sourceMetadata = source.providerMetadata?.[source.provider] ?? {};
|
|
5721
|
+
const character = spec.character;
|
|
5722
|
+
const entry = upsert(lock, key, {
|
|
5723
|
+
styleId: spec.styleId,
|
|
5724
|
+
assetId: spec.assetId,
|
|
5725
|
+
specHash: spec.specHash,
|
|
5726
|
+
generator: spec.generator,
|
|
5727
|
+
tileFeature: null,
|
|
5728
|
+
prompt: "",
|
|
5729
|
+
width: spec.width,
|
|
5730
|
+
height: spec.height,
|
|
5731
|
+
revision: null,
|
|
5732
|
+
mirror: { sourceAssetId: mirror.sourceAssetId, sourceSha256: mirrorSourceHash(source) },
|
|
5733
|
+
supersededOutputs: [],
|
|
5734
|
+
jobId: null,
|
|
5735
|
+
submissionComplete: void 0,
|
|
5736
|
+
reviewObjectId: null,
|
|
5737
|
+
objectId: null,
|
|
5738
|
+
candidateIndex: null,
|
|
5739
|
+
status: "downloaded",
|
|
5740
|
+
error: null,
|
|
5741
|
+
outputs,
|
|
5742
|
+
providerMetadata: {
|
|
5743
|
+
[source.provider]: {
|
|
5744
|
+
...sourceMetadata.frameSet ? { frameSet: sourceMetadata.frameSet } : {},
|
|
5745
|
+
...character ? {
|
|
5746
|
+
character: {
|
|
5747
|
+
kind: character.kind,
|
|
5748
|
+
mirrorOf: mirror.sourceAssetId,
|
|
5749
|
+
...character.animation ? { direction: character.animation.direction } : {}
|
|
5750
|
+
}
|
|
5751
|
+
} : {}
|
|
5752
|
+
}
|
|
5753
|
+
},
|
|
5754
|
+
sourceUrl: null,
|
|
5755
|
+
sourceUrls: [],
|
|
5756
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5757
|
+
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5758
|
+
history: [],
|
|
5759
|
+
cost: 0,
|
|
5760
|
+
costUnit: spec.costUnit,
|
|
5761
|
+
provider: source.provider,
|
|
5762
|
+
postprocess: postprocessFor(spec)
|
|
5763
|
+
});
|
|
5764
|
+
await saveLock(lockPath, lock);
|
|
5765
|
+
log(` mirror ${key} \u2190 ${sourceKey} (${written.length} file${written.length === 1 ? "" : "s"} flipped, no generation cost)`);
|
|
5766
|
+
return entry;
|
|
5767
|
+
}
|
|
5313
5768
|
|
|
5314
5769
|
// src/pipeline/quality-profile.ts
|
|
5315
5770
|
import { existsSync as existsSync6 } from "fs";
|
|
5316
|
-
import
|
|
5771
|
+
import path10 from "path";
|
|
5317
5772
|
|
|
5318
5773
|
// src/pipeline/refine.ts
|
|
5319
|
-
import
|
|
5774
|
+
import path9 from "path";
|
|
5320
5775
|
import { execFile } from "child_process";
|
|
5321
|
-
import { mkdtemp, readFile as
|
|
5776
|
+
import { mkdtemp, readFile as readFile6, rm as rm3 } from "fs/promises";
|
|
5322
5777
|
import { tmpdir } from "os";
|
|
5323
5778
|
import { promisify } from "util";
|
|
5324
5779
|
|
|
5325
5780
|
// src/artifacts.ts
|
|
5326
|
-
import
|
|
5781
|
+
import path8 from "path";
|
|
5327
5782
|
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
5328
|
-
import { mkdir as
|
|
5783
|
+
import { mkdir as mkdir3, readFile as readFile5, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
5329
5784
|
var activeTransactions = /* @__PURE__ */ new Set();
|
|
5330
5785
|
function message(error) {
|
|
5331
5786
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -5334,7 +5789,7 @@ function digest(data) {
|
|
|
5334
5789
|
return createHash3("sha256").update(data).digest("hex");
|
|
5335
5790
|
}
|
|
5336
5791
|
function portableRelative(from, to) {
|
|
5337
|
-
return
|
|
5792
|
+
return path8.relative(from, path8.resolve(to)).split(path8.sep).join("/") || ".";
|
|
5338
5793
|
}
|
|
5339
5794
|
function canonical2(value) {
|
|
5340
5795
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
@@ -5384,12 +5839,12 @@ function parseArtifactManifest(absolute, data) {
|
|
|
5384
5839
|
return raw;
|
|
5385
5840
|
}
|
|
5386
5841
|
async function readArtifactBundleManifest(manifestPath) {
|
|
5387
|
-
const absolute =
|
|
5388
|
-
return parseArtifactManifest(absolute, await
|
|
5842
|
+
const absolute = path8.resolve(manifestPath);
|
|
5843
|
+
return parseArtifactManifest(absolute, await readFile5(absolute, "utf8"));
|
|
5389
5844
|
}
|
|
5390
5845
|
async function readOptional(file) {
|
|
5391
5846
|
try {
|
|
5392
|
-
return await
|
|
5847
|
+
return await readFile5(file);
|
|
5393
5848
|
} catch (error) {
|
|
5394
5849
|
if (isCode(error, "ENOENT")) return null;
|
|
5395
5850
|
throw error;
|
|
@@ -5408,8 +5863,8 @@ function processIsAlive(pid) {
|
|
|
5408
5863
|
}
|
|
5409
5864
|
}
|
|
5410
5865
|
function validTemporaryPath(candidate, destination, type) {
|
|
5411
|
-
return
|
|
5412
|
-
`.${
|
|
5866
|
+
return path8.dirname(candidate) === path8.dirname(destination) && path8.basename(candidate).startsWith(
|
|
5867
|
+
`.${path8.basename(destination)}.pixelkiln-${type}-`
|
|
5413
5868
|
);
|
|
5414
5869
|
}
|
|
5415
5870
|
function parseTransaction(journal, data) {
|
|
@@ -5429,7 +5884,7 @@ async function removeTransactionFiles(journal) {
|
|
|
5429
5884
|
await rm2(transactionMarker(journal), { force: true });
|
|
5430
5885
|
}
|
|
5431
5886
|
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
5432
|
-
const journal =
|
|
5887
|
+
const journal = path8.resolve(recoveryFile);
|
|
5433
5888
|
const bytes = await readOptional(journal);
|
|
5434
5889
|
if (!bytes) {
|
|
5435
5890
|
await rm2(transactionMarker(journal), { force: true });
|
|
@@ -5445,7 +5900,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
5445
5900
|
);
|
|
5446
5901
|
}
|
|
5447
5902
|
for (const entry of transaction.entries) {
|
|
5448
|
-
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(
|
|
5903
|
+
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path8.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
|
|
5449
5904
|
throw new Error(
|
|
5450
5905
|
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
5451
5906
|
);
|
|
@@ -5471,7 +5926,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
5471
5926
|
}
|
|
5472
5927
|
await remove2(entry.destination, errors);
|
|
5473
5928
|
try {
|
|
5474
|
-
await
|
|
5929
|
+
await rename3(entry.backup, entry.destination);
|
|
5475
5930
|
} catch (error) {
|
|
5476
5931
|
errors.push(
|
|
5477
5932
|
`could not restore ${entry.destination}; previous file remains at ${entry.backup}: ${message(error)}`
|
|
@@ -5491,7 +5946,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
5491
5946
|
await removeTransactionFiles(journal);
|
|
5492
5947
|
}
|
|
5493
5948
|
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
5494
|
-
const root =
|
|
5949
|
+
const root = path8.dirname(path8.resolve(manifestPath));
|
|
5495
5950
|
const sources = provenance.sources.map((source) => ({
|
|
5496
5951
|
...source,
|
|
5497
5952
|
path: portableRelative(root, source.path)
|
|
@@ -5521,11 +5976,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
|
5521
5976
|
];
|
|
5522
5977
|
}
|
|
5523
5978
|
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
5524
|
-
const absoluteManifest =
|
|
5979
|
+
const absoluteManifest = path8.resolve(manifestPath);
|
|
5525
5980
|
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
5526
5981
|
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
5527
5982
|
absoluteManifest,
|
|
5528
|
-
...outputs.map((output) =>
|
|
5983
|
+
...outputs.map((output) => path8.resolve(output.path))
|
|
5529
5984
|
]);
|
|
5530
5985
|
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
5531
5986
|
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
@@ -5543,13 +5998,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
5543
5998
|
}
|
|
5544
5999
|
}
|
|
5545
6000
|
if (!options.force) {
|
|
5546
|
-
const root =
|
|
6001
|
+
const root = path8.dirname(absoluteManifest);
|
|
5547
6002
|
const recorded = new Map(
|
|
5548
|
-
previous?.outputs.map((output) => [
|
|
6003
|
+
previous?.outputs.map((output) => [path8.resolve(root, output.path), output.sha256]) ?? []
|
|
5549
6004
|
);
|
|
5550
6005
|
const conflicts = [];
|
|
5551
6006
|
for (const output of outputs) {
|
|
5552
|
-
const destination =
|
|
6007
|
+
const destination = path8.resolve(output.path);
|
|
5553
6008
|
const current = await readOptional(destination);
|
|
5554
6009
|
if (!current || digest(current) === digest(output.data)) continue;
|
|
5555
6010
|
const expected = recorded.get(destination);
|
|
@@ -5571,15 +6026,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
|
|
|
5571
6026
|
);
|
|
5572
6027
|
}
|
|
5573
6028
|
async function verifyArtifactBundle(manifestPath) {
|
|
5574
|
-
const absolute =
|
|
6029
|
+
const absolute = path8.resolve(manifestPath);
|
|
5575
6030
|
const raw = await readArtifactBundleManifest(absolute);
|
|
5576
|
-
const root =
|
|
6031
|
+
const root = path8.dirname(absolute);
|
|
5577
6032
|
const changedSources = [];
|
|
5578
6033
|
const changedOutputs = [];
|
|
5579
6034
|
for (const source of raw.sources) {
|
|
5580
6035
|
let actual = null;
|
|
5581
6036
|
try {
|
|
5582
|
-
actual = digest(await
|
|
6037
|
+
actual = digest(await readFile5(path8.resolve(root, source.path)));
|
|
5583
6038
|
} catch (error) {
|
|
5584
6039
|
if (!isCode(error, "ENOENT")) throw error;
|
|
5585
6040
|
}
|
|
@@ -5587,7 +6042,7 @@ async function verifyArtifactBundle(manifestPath) {
|
|
|
5587
6042
|
}
|
|
5588
6043
|
for (const output of raw.outputs) {
|
|
5589
6044
|
try {
|
|
5590
|
-
if (digest(await
|
|
6045
|
+
if (digest(await readFile5(path8.resolve(root, output.path))) !== output.sha256) {
|
|
5591
6046
|
changedOutputs.push(output.path);
|
|
5592
6047
|
}
|
|
5593
6048
|
} catch (error) {
|
|
@@ -5626,7 +6081,7 @@ async function rollback(prepared) {
|
|
|
5626
6081
|
for (const artifact of [...prepared].reverse()) {
|
|
5627
6082
|
if (!artifact.backup || !artifact.backedUp) continue;
|
|
5628
6083
|
try {
|
|
5629
|
-
await
|
|
6084
|
+
await rename3(artifact.backup, artifact.destination);
|
|
5630
6085
|
artifact.backedUp = false;
|
|
5631
6086
|
} catch (error) {
|
|
5632
6087
|
errors.push(
|
|
@@ -5643,7 +6098,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
5643
6098
|
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
5644
6099
|
const normalized = files.map((file) => {
|
|
5645
6100
|
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
5646
|
-
return { destination:
|
|
6101
|
+
return { destination: path8.resolve(file.path), data: Buffer.from(file.data) };
|
|
5647
6102
|
});
|
|
5648
6103
|
const destinations = /* @__PURE__ */ new Set();
|
|
5649
6104
|
for (const artifact of normalized) {
|
|
@@ -5659,7 +6114,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
5659
6114
|
const unchanged = [];
|
|
5660
6115
|
for (const artifact of normalized) {
|
|
5661
6116
|
try {
|
|
5662
|
-
const current = await
|
|
6117
|
+
const current = await readFile5(artifact.destination);
|
|
5663
6118
|
if (current.equals(artifact.data)) {
|
|
5664
6119
|
unchanged.push(artifact.destination);
|
|
5665
6120
|
} else {
|
|
@@ -5673,21 +6128,21 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
5673
6128
|
if (!changed.length) return { changed: [], unchanged };
|
|
5674
6129
|
const token = randomUUID3();
|
|
5675
6130
|
for (const [index, artifact] of changed.entries()) {
|
|
5676
|
-
const basename =
|
|
5677
|
-
artifact.stage =
|
|
5678
|
-
|
|
6131
|
+
const basename = path8.basename(artifact.destination);
|
|
6132
|
+
artifact.stage = path8.join(
|
|
6133
|
+
path8.dirname(artifact.destination),
|
|
5679
6134
|
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
5680
6135
|
);
|
|
5681
6136
|
if (artifact.existed) {
|
|
5682
|
-
artifact.backup =
|
|
5683
|
-
|
|
6137
|
+
artifact.backup = path8.join(
|
|
6138
|
+
path8.dirname(artifact.destination),
|
|
5684
6139
|
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
5685
6140
|
);
|
|
5686
6141
|
}
|
|
5687
6142
|
}
|
|
5688
|
-
const journal = options.recoveryFile ?
|
|
6143
|
+
const journal = options.recoveryFile ? path8.resolve(options.recoveryFile) : null;
|
|
5689
6144
|
if (journal) {
|
|
5690
|
-
await
|
|
6145
|
+
await mkdir3(path8.dirname(journal), { recursive: true });
|
|
5691
6146
|
const transaction = {
|
|
5692
6147
|
format: "pixelkiln-artifact-transaction",
|
|
5693
6148
|
version: 1,
|
|
@@ -5701,7 +6156,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
5701
6156
|
}))
|
|
5702
6157
|
};
|
|
5703
6158
|
try {
|
|
5704
|
-
await
|
|
6159
|
+
await writeFile3(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
5705
6160
|
activeTransactions.add(journal);
|
|
5706
6161
|
} catch (error) {
|
|
5707
6162
|
throw new Error(
|
|
@@ -5712,9 +6167,9 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
5712
6167
|
}
|
|
5713
6168
|
try {
|
|
5714
6169
|
for (const [index, artifact] of changed.entries()) {
|
|
5715
|
-
await
|
|
6170
|
+
await mkdir3(path8.dirname(artifact.destination), { recursive: true });
|
|
5716
6171
|
await options.beforeStage?.(artifact.destination, index);
|
|
5717
|
-
await
|
|
6172
|
+
await writeFile3(artifact.stage, artifact.data, { flag: "wx" });
|
|
5718
6173
|
}
|
|
5719
6174
|
} catch (error) {
|
|
5720
6175
|
const cleanupErrors2 = await rollback(changed);
|
|
@@ -5727,17 +6182,17 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
5727
6182
|
try {
|
|
5728
6183
|
for (const artifact of changed) {
|
|
5729
6184
|
if (!artifact.existed) continue;
|
|
5730
|
-
await
|
|
6185
|
+
await rename3(artifact.destination, artifact.backup);
|
|
5731
6186
|
artifact.backedUp = true;
|
|
5732
6187
|
}
|
|
5733
6188
|
for (const [index, artifact] of changed.entries()) {
|
|
5734
6189
|
await options.beforePromote?.(artifact.destination, index);
|
|
5735
|
-
await
|
|
6190
|
+
await rename3(artifact.stage, artifact.destination);
|
|
5736
6191
|
artifact.stage = void 0;
|
|
5737
6192
|
artifact.promoted = true;
|
|
5738
6193
|
}
|
|
5739
6194
|
if (journal) {
|
|
5740
|
-
await
|
|
6195
|
+
await writeFile3(transactionMarker(journal), `${token}
|
|
5741
6196
|
`, { flag: "wx" });
|
|
5742
6197
|
durableCommit = true;
|
|
5743
6198
|
await options.afterCommit?.();
|
|
@@ -5779,69 +6234,6 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
5779
6234
|
};
|
|
5780
6235
|
}
|
|
5781
6236
|
|
|
5782
|
-
// src/palette.ts
|
|
5783
|
-
function normalizePalette(values) {
|
|
5784
|
-
const colors = /* @__PURE__ */ new Map();
|
|
5785
|
-
for (const value of values) {
|
|
5786
|
-
const rgb = parseHex(value);
|
|
5787
|
-
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
5788
|
-
if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
|
|
5789
|
-
}
|
|
5790
|
-
if (colors.size < 2 || colors.size > 256) {
|
|
5791
|
-
throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
5792
|
-
}
|
|
5793
|
-
return [...colors.values()];
|
|
5794
|
-
}
|
|
5795
|
-
function colorDistance(a, b) {
|
|
5796
|
-
const rmean = (a.r + b.r) / 2;
|
|
5797
|
-
const dr = a.r - b.r;
|
|
5798
|
-
const dg = a.g - b.g;
|
|
5799
|
-
const db = a.b - b.b;
|
|
5800
|
-
return Math.sqrt(
|
|
5801
|
-
(512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
|
|
5802
|
-
);
|
|
5803
|
-
}
|
|
5804
|
-
function quantize(png, palette) {
|
|
5805
|
-
const colors = normalizePalette(palette);
|
|
5806
|
-
const pixels = Buffer.from(png.pixels);
|
|
5807
|
-
const nearestOf = /* @__PURE__ */ new Map();
|
|
5808
|
-
let changed = 0;
|
|
5809
|
-
for (let i = 0; i < pixels.length; i += 4) {
|
|
5810
|
-
const alpha = pixels[i + 3];
|
|
5811
|
-
if (alpha === 0) {
|
|
5812
|
-
if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
|
|
5813
|
-
pixels[i] = 0;
|
|
5814
|
-
pixels[i + 1] = 0;
|
|
5815
|
-
pixels[i + 2] = 0;
|
|
5816
|
-
continue;
|
|
5817
|
-
}
|
|
5818
|
-
const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
|
|
5819
|
-
let nearest = nearestOf.get(key);
|
|
5820
|
-
if (!nearest) {
|
|
5821
|
-
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
5822
|
-
nearest = colors[0];
|
|
5823
|
-
let distance = colorDistance(pixel, nearest);
|
|
5824
|
-
for (let j = 1; j < colors.length; j++) {
|
|
5825
|
-
const candidate = colors[j];
|
|
5826
|
-
const candidateDistance = colorDistance(pixel, candidate);
|
|
5827
|
-
if (candidateDistance < distance) {
|
|
5828
|
-
nearest = candidate;
|
|
5829
|
-
distance = candidateDistance;
|
|
5830
|
-
}
|
|
5831
|
-
}
|
|
5832
|
-
nearestOf.set(key, nearest);
|
|
5833
|
-
}
|
|
5834
|
-
if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
|
|
5835
|
-
pixels[i] = nearest.r;
|
|
5836
|
-
pixels[i + 1] = nearest.g;
|
|
5837
|
-
pixels[i + 2] = nearest.b;
|
|
5838
|
-
}
|
|
5839
|
-
return { png: { width: png.width, height: png.height, pixels }, changed };
|
|
5840
|
-
}
|
|
5841
|
-
function quantizeToPalette(png, palette) {
|
|
5842
|
-
return quantize(png, palette).png;
|
|
5843
|
-
}
|
|
5844
|
-
|
|
5845
6237
|
// src/pipeline/refine.ts
|
|
5846
6238
|
var execFileAsync = promisify(execFile);
|
|
5847
6239
|
var PIXEL_ART_FIXER_MIN_SIDE = 16;
|
|
@@ -6041,16 +6433,16 @@ function sameFrameGrid(first, next) {
|
|
|
6041
6433
|
return next.detection.stepX === first.detection.stepX && next.detection.stepY === first.detection.stepY && next.detection.columns === first.detection.columns && next.detection.rows === first.detection.rows && phaseMatches(next.detection.phaseX, first.detection.phaseX, first.detection.stepX) && phaseMatches(next.detection.phaseY, first.detection.phaseY, first.detection.stepY) && next.audit.width === first.audit.width && next.audit.height === first.audit.height && next.sourceWidth === first.sourceWidth && next.sourceHeight === first.sourceHeight;
|
|
6042
6434
|
}
|
|
6043
6435
|
async function refineAsset(options) {
|
|
6044
|
-
const source =
|
|
6045
|
-
const output =
|
|
6436
|
+
const source = path9.resolve(options.source);
|
|
6437
|
+
const output = path9.resolve(options.output);
|
|
6046
6438
|
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
6047
|
-
const record =
|
|
6439
|
+
const record = path9.resolve(qualityRecordPath(output));
|
|
6048
6440
|
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
6049
6441
|
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
6050
6442
|
if (options.minTransparency !== void 0 && (!Number.isFinite(options.minTransparency) || options.minTransparency < 0 || options.minTransparency > 1)) throw new Error("minTransparency must be between 0 and 1.");
|
|
6051
6443
|
let sourcePng;
|
|
6052
6444
|
try {
|
|
6053
|
-
sourcePng = decodePng(await
|
|
6445
|
+
sourcePng = decodePng(await readFile6(source));
|
|
6054
6446
|
} catch (error) {
|
|
6055
6447
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
6056
6448
|
}
|
|
@@ -6067,8 +6459,8 @@ async function refineAsset(options) {
|
|
|
6067
6459
|
);
|
|
6068
6460
|
}
|
|
6069
6461
|
}
|
|
6070
|
-
const temp = await mkdtemp(
|
|
6071
|
-
const recovered =
|
|
6462
|
+
const temp = await mkdtemp(path9.join(tmpdir(), "pixelkiln-refine-"));
|
|
6463
|
+
const recovered = path9.join(temp, "native.png");
|
|
6072
6464
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
6073
6465
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
6074
6466
|
try {
|
|
@@ -6089,7 +6481,7 @@ async function refineAsset(options) {
|
|
|
6089
6481
|
);
|
|
6090
6482
|
}
|
|
6091
6483
|
const detection = parseDetection(firstJsonObject(stdout));
|
|
6092
|
-
const native = decodePng(await
|
|
6484
|
+
const native = decodePng(await readFile6(recovered));
|
|
6093
6485
|
const quantized = quantizeToPalette(native, palette);
|
|
6094
6486
|
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
6095
6487
|
const audit = auditRefinement(
|
|
@@ -6153,18 +6545,18 @@ async function refineFrameSet(options) {
|
|
|
6153
6545
|
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
6154
6546
|
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
6155
6547
|
}
|
|
6156
|
-
const output =
|
|
6157
|
-
const record =
|
|
6548
|
+
const output = path9.resolve(options.output);
|
|
6549
|
+
const record = path9.resolve(qualityRecordPath(output));
|
|
6158
6550
|
const palette = normalizeRefinementPalette(options.palette);
|
|
6159
6551
|
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
6160
6552
|
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
6161
6553
|
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
6162
6554
|
}
|
|
6163
|
-
const sourceSet = new Set(options.sources.map((source) =>
|
|
6555
|
+
const sourceSet = new Set(options.sources.map((source) => path9.resolve(source.path)));
|
|
6164
6556
|
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
6165
6557
|
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
6166
6558
|
}
|
|
6167
|
-
const temp = await mkdtemp(
|
|
6559
|
+
const temp = await mkdtemp(path9.join(tmpdir(), "pixelkiln-refine-frames-"));
|
|
6168
6560
|
try {
|
|
6169
6561
|
const frames = [];
|
|
6170
6562
|
const outputs = [];
|
|
@@ -6172,8 +6564,8 @@ async function refineFrameSet(options) {
|
|
|
6172
6564
|
const sourcePaths = [];
|
|
6173
6565
|
let firstOptions = null;
|
|
6174
6566
|
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
6175
|
-
const source =
|
|
6176
|
-
const temporaryOutput =
|
|
6567
|
+
const source = path9.resolve(sourceEntry.path);
|
|
6568
|
+
const temporaryOutput = path9.join(temp, `${String(index).padStart(2, "0")}.png`);
|
|
6177
6569
|
const refined = await refineAsset({
|
|
6178
6570
|
source,
|
|
6179
6571
|
output: temporaryOutput,
|
|
@@ -6205,7 +6597,7 @@ async function refineFrameSet(options) {
|
|
|
6205
6597
|
});
|
|
6206
6598
|
outputs.push({
|
|
6207
6599
|
path: finalOutputs[index],
|
|
6208
|
-
data: await
|
|
6600
|
+
data: await readFile6(temporaryOutput)
|
|
6209
6601
|
});
|
|
6210
6602
|
}
|
|
6211
6603
|
const first = frames[0];
|
|
@@ -6248,7 +6640,7 @@ async function refineFrameSet(options) {
|
|
|
6248
6640
|
);
|
|
6249
6641
|
return {
|
|
6250
6642
|
sources: sourcePaths,
|
|
6251
|
-
outputs: outputs.map((item) =>
|
|
6643
|
+
outputs: outputs.map((item) => path9.resolve(item.path)),
|
|
6252
6644
|
record,
|
|
6253
6645
|
frames,
|
|
6254
6646
|
palette,
|
|
@@ -6259,12 +6651,12 @@ async function refineFrameSet(options) {
|
|
|
6259
6651
|
}
|
|
6260
6652
|
}
|
|
6261
6653
|
async function checkQualityRecord(recordPath) {
|
|
6262
|
-
const record =
|
|
6654
|
+
const record = path9.resolve(recordPath);
|
|
6263
6655
|
const manifest = await readArtifactBundleManifest(record);
|
|
6264
6656
|
const options = parseRecordOptions(manifest, record);
|
|
6265
6657
|
const verification = await verifyArtifactBundle(record);
|
|
6266
|
-
const sources = manifest.sources.map((source2) =>
|
|
6267
|
-
const outputs = manifest.outputs.map((output2) =>
|
|
6658
|
+
const sources = manifest.sources.map((source2) => path9.resolve(path9.dirname(record), source2.path));
|
|
6659
|
+
const outputs = manifest.outputs.map((output2) => path9.resolve(path9.dirname(record), output2.path));
|
|
6268
6660
|
const source = sources[0] ?? "";
|
|
6269
6661
|
const output = outputs[0] ?? "";
|
|
6270
6662
|
const expectedCount = options.frameSet?.count ?? 1;
|
|
@@ -6297,7 +6689,7 @@ async function checkQualityRecord(recordPath) {
|
|
|
6297
6689
|
async function approveQualityRecord(recordPath, approval) {
|
|
6298
6690
|
const reviewer = approval.reviewer.trim();
|
|
6299
6691
|
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
6300
|
-
const record =
|
|
6692
|
+
const record = path9.resolve(recordPath);
|
|
6301
6693
|
const manifest = await readArtifactBundleManifest(record);
|
|
6302
6694
|
const options = parseRecordOptions(manifest, record);
|
|
6303
6695
|
const verification = await verifyArtifactBundle(record);
|
|
@@ -6318,14 +6710,14 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
6318
6710
|
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
6319
6711
|
);
|
|
6320
6712
|
}
|
|
6321
|
-
const root =
|
|
6713
|
+
const root = path9.dirname(record);
|
|
6322
6714
|
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
6323
|
-
const outputPath =
|
|
6324
|
-
return { path: outputPath, data: await
|
|
6715
|
+
const outputPath = path9.resolve(root, entry.path);
|
|
6716
|
+
return { path: outputPath, data: await readFile6(outputPath) };
|
|
6325
6717
|
}));
|
|
6326
6718
|
const sources = manifest.sources.map((source) => ({
|
|
6327
6719
|
...source,
|
|
6328
|
-
path:
|
|
6720
|
+
path: path9.resolve(root, source.path)
|
|
6329
6721
|
}));
|
|
6330
6722
|
const approved = {
|
|
6331
6723
|
...options,
|
|
@@ -6357,7 +6749,7 @@ async function qualitySource(spec, lock) {
|
|
|
6357
6749
|
if (spec.generator === "frames") {
|
|
6358
6750
|
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
6359
6751
|
}
|
|
6360
|
-
const source =
|
|
6752
|
+
const source = path10.resolve(spec.root, spec.source);
|
|
6361
6753
|
return existsSync6(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
6362
6754
|
}
|
|
6363
6755
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -6424,7 +6816,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
6424
6816
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
6425
6817
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
6426
6818
|
const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
|
|
6427
|
-
const contractChanged = JSON.stringify(check.sources.map((item) =>
|
|
6819
|
+
const contractChanged = JSON.stringify(check.sources.map((item) => path10.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path10.resolve(item))) || JSON.stringify(check.outputs.map((item) => path10.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path10.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
|
|
6428
6820
|
if (contractChanged) {
|
|
6429
6821
|
return {
|
|
6430
6822
|
key,
|
|
@@ -6586,43 +6978,21 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
|
|
|
6586
6978
|
return sources;
|
|
6587
6979
|
}
|
|
6588
6980
|
|
|
6589
|
-
// src/pipeline/postprocess.ts
|
|
6590
|
-
function postprocessFor(spec) {
|
|
6591
|
-
if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
|
|
6592
|
-
return {
|
|
6593
|
-
palette: {
|
|
6594
|
-
colors: normalizePalette(spec.palette).map((color) => color.hex),
|
|
6595
|
-
dither: "none",
|
|
6596
|
-
distance: "redmean"
|
|
6597
|
-
}
|
|
6598
|
-
};
|
|
6599
|
-
}
|
|
6600
|
-
function samePostprocess(a, b) {
|
|
6601
|
-
const pa = a?.palette;
|
|
6602
|
-
const pb = b?.palette;
|
|
6603
|
-
if (!pa && !pb) return true;
|
|
6604
|
-
if (!pa || !pb) return false;
|
|
6605
|
-
return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
|
|
6606
|
-
}
|
|
6607
|
-
function postprocessCurrent(entry, spec) {
|
|
6608
|
-
return samePostprocess(entry.postprocess, postprocessFor(spec));
|
|
6609
|
-
}
|
|
6610
|
-
function applyPostprocess(bytes, mediaType, record) {
|
|
6611
|
-
if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
|
|
6612
|
-
const png = decodePng(bytes);
|
|
6613
|
-
const { png: snapped, changed } = quantize(png, record.palette.colors);
|
|
6614
|
-
if (!changed) return { bytes, changed: false };
|
|
6615
|
-
return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
|
|
6616
|
-
}
|
|
6617
|
-
|
|
6618
6981
|
// src/pipeline/revision.ts
|
|
6619
6982
|
import { existsSync as existsSync7 } from "fs";
|
|
6620
|
-
import
|
|
6983
|
+
import path11 from "path";
|
|
6621
6984
|
async function inspectRevisionReadiness(spec, lock) {
|
|
6985
|
+
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
6622
6986
|
if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
|
|
6623
6987
|
if (!spec.revision) return null;
|
|
6624
6988
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
6625
6989
|
}
|
|
6990
|
+
async function inspectMirror(spec, lock, seen) {
|
|
6991
|
+
const source = spec.mirror.sourceSpec;
|
|
6992
|
+
const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
|
|
6993
|
+
const dependency = await inspectParent(source, lock, seen);
|
|
6994
|
+
return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
6995
|
+
}
|
|
6626
6996
|
async function inspectCharacterParent(spec, lock, seen) {
|
|
6627
6997
|
const character = spec.character;
|
|
6628
6998
|
const parentSpec = character.parentSpec;
|
|
@@ -6676,6 +7046,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
6676
7046
|
if (seen.has(key)) return { ready: false, reason: `revision dependency cycle at ${key}` };
|
|
6677
7047
|
const nextSeen = new Set(seen);
|
|
6678
7048
|
nextSeen.add(key);
|
|
7049
|
+
if (spec.mirror) {
|
|
7050
|
+
const inputs = await inspectMirror(spec, lock, nextSeen);
|
|
7051
|
+
if (!inputs.ready) return inputs;
|
|
7052
|
+
}
|
|
6679
7053
|
if (spec.revision) {
|
|
6680
7054
|
const inputs = await inspectRevision(spec, lock, nextSeen);
|
|
6681
7055
|
if (!inputs.ready) return inputs;
|
|
@@ -6692,7 +7066,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
6692
7066
|
};
|
|
6693
7067
|
}
|
|
6694
7068
|
if (spec.source) {
|
|
6695
|
-
const source =
|
|
7069
|
+
const source = path11.resolve(spec.root, spec.source);
|
|
6696
7070
|
return existsSync7(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
|
|
6697
7071
|
}
|
|
6698
7072
|
const entry = lock.entries[key];
|
|
@@ -6762,7 +7136,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6762
7136
|
state = "missing";
|
|
6763
7137
|
reason = "--force";
|
|
6764
7138
|
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
6765
|
-
if (existsSync8(
|
|
7139
|
+
if (existsSync8(path12.resolve(spec.root, spec.source))) {
|
|
6766
7140
|
state = "ok";
|
|
6767
7141
|
reason = `placed from ${spec.source}; not generated`;
|
|
6768
7142
|
} else {
|
|
@@ -6775,11 +7149,17 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6775
7149
|
reason = "file on disk, no provenance recorded";
|
|
6776
7150
|
} else {
|
|
6777
7151
|
state = "missing";
|
|
6778
|
-
reason = "not in lockfile";
|
|
7152
|
+
reason = spec.mirror ? `not made yet; flipped from ${spec.mirror.sourceAssetId} (no generation cost)` : "not in lockfile";
|
|
6779
7153
|
}
|
|
6780
7154
|
} else if (entry.specHash !== spec.specHash) {
|
|
6781
7155
|
state = "stale";
|
|
6782
7156
|
reason = "prompt, size, or style changed";
|
|
7157
|
+
} else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
|
|
7158
|
+
state = "stale";
|
|
7159
|
+
reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
|
|
7160
|
+
} else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !existsSync8(currentEntryOutputPath(entry, spec, index)))) {
|
|
7161
|
+
state = "stale";
|
|
7162
|
+
reason = "mirror output missing on disk; flip it again (no generation cost)";
|
|
6783
7163
|
} else if (entry.status === "download-failed") {
|
|
6784
7164
|
state = "recoverable";
|
|
6785
7165
|
const force = entry.error?.includes("pass --force") ? " --force" : "";
|
|
@@ -6808,6 +7188,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6808
7188
|
} else if (await anyOutputModified(entry, spec)) {
|
|
6809
7189
|
state = "orphaned";
|
|
6810
7190
|
reason = "output modified since download";
|
|
7191
|
+
} else if (spec.mirror && !postprocessCurrent(entry, spec)) {
|
|
7192
|
+
state = "stale";
|
|
7193
|
+
reason = "palette enforcement changed; flip the source again once it is refreshed (no generation cost)";
|
|
6811
7194
|
} else if (!postprocessCurrent(entry, spec)) {
|
|
6812
7195
|
state = "recoverable";
|
|
6813
7196
|
reason = spec.enforcePalette ? "palette enforcement turned on; run pixelkiln fetch to snap the file (no generation cost)" : "palette enforcement turned off; run pixelkiln fetch to restore the provider's bytes (no generation cost)";
|
|
@@ -6819,15 +7202,19 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6819
7202
|
items.push({ spec, key, state, reason, ...quality ? { quality } : {} });
|
|
6820
7203
|
}
|
|
6821
7204
|
const isActionable = (i) => i.state === "missing" || i.state === "stale" || i.state === "failed";
|
|
6822
|
-
const
|
|
7205
|
+
const stateOf = new Map(items.map((i) => [i.key, i.state]));
|
|
6823
7206
|
for (const item of items) {
|
|
6824
7207
|
if (!isActionable(item)) continue;
|
|
6825
|
-
const parent = item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
|
|
7208
|
+
const parent = item.spec.mirror?.sourceSpec ?? item.spec.revision?.sourceSpec ?? item.spec.character?.parentSpec;
|
|
6826
7209
|
if (!parent) continue;
|
|
6827
7210
|
const parentKey = lockKey(parent.styleId, parent.assetId);
|
|
6828
|
-
|
|
7211
|
+
const parentState = stateOf.get(parentKey);
|
|
7212
|
+
if (parentState && isActionable({ state: parentState })) {
|
|
6829
7213
|
item.state = "blocked";
|
|
6830
7214
|
item.reason = `parent ${parentKey} is being generated in this run; this follows in the next wave`;
|
|
7215
|
+
} else if (item.spec.mirror && (parentState === "recoverable" || parentState === "in-flight")) {
|
|
7216
|
+
item.state = "blocked";
|
|
7217
|
+
item.reason = `mirror source ${parentKey} is ${parentState}; this follows in the next wave`;
|
|
6831
7218
|
}
|
|
6832
7219
|
}
|
|
6833
7220
|
const actionable = items.filter(isActionable);
|
|
@@ -6885,13 +7272,13 @@ function summarize(plan) {
|
|
|
6885
7272
|
|
|
6886
7273
|
// src/project.ts
|
|
6887
7274
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
6888
|
-
const absolute =
|
|
6889
|
-
const root =
|
|
7275
|
+
const absolute = path13.resolve(manifestPath);
|
|
7276
|
+
const root = path13.dirname(absolute);
|
|
6890
7277
|
if (opts.env !== false) {
|
|
6891
7278
|
loadEnvFiles(root);
|
|
6892
|
-
if (
|
|
7279
|
+
if (path13.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
6893
7280
|
}
|
|
6894
|
-
const lockPath =
|
|
7281
|
+
const lockPath = path13.resolve(opts.lockPath ?? path13.join(root, "pixelkiln.lock.json"));
|
|
6895
7282
|
const loaded = await loadManifest(absolute);
|
|
6896
7283
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
6897
7284
|
const lock = await loadLock(lockPath);
|
|
@@ -6958,9 +7345,9 @@ function shouldPersistSourceUrl(value) {
|
|
|
6958
7345
|
}
|
|
6959
7346
|
|
|
6960
7347
|
// src/recipes.ts
|
|
6961
|
-
import
|
|
7348
|
+
import path14 from "path";
|
|
6962
7349
|
import { existsSync as existsSync9 } from "fs";
|
|
6963
|
-
import { readdir, readFile as
|
|
7350
|
+
import { readdir, readFile as readFile7 } from "fs/promises";
|
|
6964
7351
|
import { fileURLToPath } from "url";
|
|
6965
7352
|
import { z as z3 } from "zod";
|
|
6966
7353
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
@@ -6969,7 +7356,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
6969
7356
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
6970
7357
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
6971
7358
|
var PortablePathSchema = z3.string().min(1).refine((value) => {
|
|
6972
|
-
if (value.includes("\\") ||
|
|
7359
|
+
if (value.includes("\\") || path14.posix.isAbsolute(value)) return false;
|
|
6973
7360
|
const parts = value.split("/");
|
|
6974
7361
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
6975
7362
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -7145,10 +7532,10 @@ function message3(error) {
|
|
|
7145
7532
|
return error instanceof Error ? error.message : String(error);
|
|
7146
7533
|
}
|
|
7147
7534
|
async function readRecipeFile(recipePath, bundled) {
|
|
7148
|
-
const absolute =
|
|
7535
|
+
const absolute = path14.resolve(recipePath);
|
|
7149
7536
|
let raw;
|
|
7150
7537
|
try {
|
|
7151
|
-
raw = JSON.parse(await
|
|
7538
|
+
raw = JSON.parse(await readFile7(absolute, "utf8"));
|
|
7152
7539
|
} catch (error) {
|
|
7153
7540
|
throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
|
|
7154
7541
|
}
|
|
@@ -7159,13 +7546,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
7159
7546
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
7160
7547
|
);
|
|
7161
7548
|
}
|
|
7162
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
7549
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path14.dirname(absolute), bundled };
|
|
7163
7550
|
}
|
|
7164
7551
|
async function walkRecipeFiles(root) {
|
|
7165
7552
|
if (!existsSync9(root)) return [];
|
|
7166
7553
|
const out = [];
|
|
7167
7554
|
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
7168
|
-
const resolved =
|
|
7555
|
+
const resolved = path14.join(root, entry.name);
|
|
7169
7556
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
7170
7557
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
7171
7558
|
}
|
|
@@ -7176,7 +7563,7 @@ function bundledRecipeRoot() {
|
|
|
7176
7563
|
}
|
|
7177
7564
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
7178
7565
|
const recipes = await Promise.all(
|
|
7179
|
-
(await walkRecipeFiles(
|
|
7566
|
+
(await walkRecipeFiles(path14.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
7180
7567
|
);
|
|
7181
7568
|
return recipes.sort(
|
|
7182
7569
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -7199,9 +7586,9 @@ function parseSelector(selector) {
|
|
|
7199
7586
|
return { id: match[1], version: match[2] };
|
|
7200
7587
|
}
|
|
7201
7588
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
7202
|
-
const local =
|
|
7589
|
+
const local = path14.resolve(target);
|
|
7203
7590
|
if (existsSync9(local)) {
|
|
7204
|
-
const recipePath =
|
|
7591
|
+
const recipePath = path14.basename(local) === RECIPE_FILE ? local : path14.join(local, RECIPE_FILE);
|
|
7205
7592
|
return readRecipeFile(recipePath, false);
|
|
7206
7593
|
}
|
|
7207
7594
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -7215,7 +7602,7 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
7215
7602
|
return matches[0];
|
|
7216
7603
|
}
|
|
7217
7604
|
async function verifyFile(root, file) {
|
|
7218
|
-
const absolute =
|
|
7605
|
+
const absolute = path14.resolve(root, ...file.path.split("/"));
|
|
7219
7606
|
if (!existsSync9(absolute)) {
|
|
7220
7607
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
7221
7608
|
}
|
|
@@ -7231,7 +7618,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
7231
7618
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
7232
7619
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
7233
7620
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
7234
|
-
const modelRoot = options.modelRoot ?
|
|
7621
|
+
const modelRoot = options.modelRoot ? path14.resolve(options.modelRoot) : null;
|
|
7235
7622
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
7236
7623
|
...await verifyFile(modelRoot, model),
|
|
7237
7624
|
source: model.source,
|
|
@@ -7265,7 +7652,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
7265
7652
|
};
|
|
7266
7653
|
}
|
|
7267
7654
|
function renderStyle(style, destination, cwd) {
|
|
7268
|
-
const portableDir =
|
|
7655
|
+
const portableDir = path14.relative(cwd, destination).split(path14.sep).join("/") || ".";
|
|
7269
7656
|
const replace = (value) => {
|
|
7270
7657
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
7271
7658
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -7277,25 +7664,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
7277
7664
|
return replace(style);
|
|
7278
7665
|
}
|
|
7279
7666
|
async function installRecipe(target, options = {}) {
|
|
7280
|
-
const cwd =
|
|
7667
|
+
const cwd = path14.resolve(options.cwd ?? process.cwd());
|
|
7281
7668
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
7282
7669
|
const verification = await verifyRecipe(loaded.path);
|
|
7283
7670
|
if (!verification.ok) {
|
|
7284
7671
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
7285
7672
|
}
|
|
7286
|
-
const destination = options.out ?
|
|
7673
|
+
const destination = options.out ? path14.resolve(cwd, options.out) : path14.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
7287
7674
|
const sources = [
|
|
7288
7675
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
7289
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
7676
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path14.join(loaded.dir, ...file.path.split("/")) }))
|
|
7290
7677
|
];
|
|
7291
7678
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
7292
|
-
path:
|
|
7293
|
-
data: await
|
|
7679
|
+
path: path14.join(destination, ...file.path.split("/")),
|
|
7680
|
+
data: await readFile7(file.source)
|
|
7294
7681
|
})));
|
|
7295
7682
|
if (!options.force) {
|
|
7296
7683
|
for (const file of files) {
|
|
7297
7684
|
if (!existsSync9(file.path)) continue;
|
|
7298
|
-
const current = await
|
|
7685
|
+
const current = await readFile7(file.path);
|
|
7299
7686
|
if (!current.equals(file.data)) {
|
|
7300
7687
|
throw new OverwriteRefusedError(
|
|
7301
7688
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -7319,9 +7706,9 @@ async function installRecipe(target, options = {}) {
|
|
|
7319
7706
|
}
|
|
7320
7707
|
|
|
7321
7708
|
// src/pipeline/audit.ts
|
|
7322
|
-
import { readFile as
|
|
7709
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
7323
7710
|
import { existsSync as existsSync10 } from "fs";
|
|
7324
|
-
import
|
|
7711
|
+
import path15 from "path";
|
|
7325
7712
|
function paletteDistance(asset, reference) {
|
|
7326
7713
|
if (!asset.length || !reference.length) return 0;
|
|
7327
7714
|
let total = 0;
|
|
@@ -7362,7 +7749,7 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
7362
7749
|
continue;
|
|
7363
7750
|
}
|
|
7364
7751
|
try {
|
|
7365
|
-
const png = decodePng(await
|
|
7752
|
+
const png = decodePng(await readFile8(output.absolutePath));
|
|
7366
7753
|
const palette = extractPalette(png, 12);
|
|
7367
7754
|
assets.push({
|
|
7368
7755
|
assetId: spec.assetId,
|
|
@@ -7386,10 +7773,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
7386
7773
|
let referenceFromStyleImages = false;
|
|
7387
7774
|
const refPalettes = [];
|
|
7388
7775
|
for (const img of style.styleImages) {
|
|
7389
|
-
const abs =
|
|
7776
|
+
const abs = path15.resolve(loaded.root, img.path);
|
|
7390
7777
|
if (!existsSync10(abs)) continue;
|
|
7391
7778
|
try {
|
|
7392
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
7779
|
+
refPalettes.push(extractPalette(decodePng(await readFile8(abs)), 12));
|
|
7393
7780
|
} catch {
|
|
7394
7781
|
}
|
|
7395
7782
|
}
|
|
@@ -7465,15 +7852,15 @@ function hex(c) {
|
|
|
7465
7852
|
}
|
|
7466
7853
|
|
|
7467
7854
|
// src/pipeline/quality-regression.ts
|
|
7468
|
-
import
|
|
7855
|
+
import path16 from "path";
|
|
7469
7856
|
import { existsSync as existsSync11 } from "fs";
|
|
7470
|
-
import { readFile as
|
|
7857
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
7471
7858
|
import { z as z4 } from "zod";
|
|
7472
7859
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
7473
7860
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
7474
7861
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
7475
7862
|
var QualityPathSchema = z4.string().min(1).refine(
|
|
7476
|
-
(value) => !
|
|
7863
|
+
(value) => !path16.posix.isAbsolute(value) && !value.includes("\\"),
|
|
7477
7864
|
"expected a portable relative path"
|
|
7478
7865
|
);
|
|
7479
7866
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -7620,10 +8007,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
7620
8007
|
};
|
|
7621
8008
|
}
|
|
7622
8009
|
async function measureImageQuality(file) {
|
|
7623
|
-
const absolute =
|
|
8010
|
+
const absolute = path16.resolve(file);
|
|
7624
8011
|
let bytes;
|
|
7625
8012
|
try {
|
|
7626
|
-
bytes = await
|
|
8013
|
+
bytes = await readFile9(absolute);
|
|
7627
8014
|
} catch (error) {
|
|
7628
8015
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
7629
8016
|
}
|
|
@@ -7642,11 +8029,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
7642
8029
|
}
|
|
7643
8030
|
const ids = parsed.data.map((entry) => entry.id);
|
|
7644
8031
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
7645
|
-
const root =
|
|
8032
|
+
const root = path16.dirname(path16.resolve(inputsFilePath));
|
|
7646
8033
|
return parsed.data.map((entry) => ({
|
|
7647
8034
|
id: entry.id,
|
|
7648
|
-
file:
|
|
7649
|
-
...entry.record ? { record:
|
|
8035
|
+
file: path16.resolve(root, entry.path),
|
|
8036
|
+
...entry.record ? { record: path16.resolve(root, entry.record) } : {},
|
|
7650
8037
|
tolerances: QualityToleranceSchema.parse({
|
|
7651
8038
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
7652
8039
|
...entry.tolerances
|
|
@@ -7654,7 +8041,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
7654
8041
|
}));
|
|
7655
8042
|
}
|
|
7656
8043
|
function portableRelative2(from, to) {
|
|
7657
|
-
return
|
|
8044
|
+
return path16.relative(from, path16.resolve(to)).split(path16.sep).join("/") || ".";
|
|
7658
8045
|
}
|
|
7659
8046
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
7660
8047
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -7669,20 +8056,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
7669
8056
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
7670
8057
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
7671
8058
|
}
|
|
7672
|
-
const outputs = record.outputs.map((output) =>
|
|
7673
|
-
if (!outputs.includes(
|
|
8059
|
+
const outputs = record.outputs.map((output) => path16.resolve(path16.dirname(recordPath), output.path));
|
|
8060
|
+
if (!outputs.includes(path16.resolve(imagePath))) {
|
|
7674
8061
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
7675
8062
|
}
|
|
7676
8063
|
}
|
|
7677
8064
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
7678
8065
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
7679
|
-
const absolute =
|
|
8066
|
+
const absolute = path16.resolve(baselinePath);
|
|
7680
8067
|
for (const input of inputs) {
|
|
7681
|
-
if (
|
|
8068
|
+
if (path16.resolve(input.file) === absolute || input.record && path16.resolve(input.record) === absolute) {
|
|
7682
8069
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
7683
8070
|
}
|
|
7684
8071
|
}
|
|
7685
|
-
const root =
|
|
8072
|
+
const root = path16.dirname(absolute);
|
|
7686
8073
|
const cases = [];
|
|
7687
8074
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
7688
8075
|
const expected = await measureImageQuality(input.file);
|
|
@@ -7710,7 +8097,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
7710
8097
|
});
|
|
7711
8098
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
7712
8099
|
if (existsSync11(absolute) && !options.force) {
|
|
7713
|
-
const current = await
|
|
8100
|
+
const current = await readFile9(absolute);
|
|
7714
8101
|
if (!current.equals(data)) {
|
|
7715
8102
|
throw new OverwriteRefusedError(
|
|
7716
8103
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -7722,10 +8109,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
7722
8109
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
7723
8110
|
}
|
|
7724
8111
|
async function readQualityBaseline(baselinePath) {
|
|
7725
|
-
const absolute =
|
|
8112
|
+
const absolute = path16.resolve(baselinePath);
|
|
7726
8113
|
let raw;
|
|
7727
8114
|
try {
|
|
7728
|
-
raw = JSON.parse(await
|
|
8115
|
+
raw = JSON.parse(await readFile9(absolute, "utf8"));
|
|
7729
8116
|
} catch (error) {
|
|
7730
8117
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
7731
8118
|
}
|
|
@@ -7795,12 +8182,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
7795
8182
|
return { violations, warnings };
|
|
7796
8183
|
}
|
|
7797
8184
|
async function checkQualityBaseline(baselinePath) {
|
|
7798
|
-
const absolute =
|
|
8185
|
+
const absolute = path16.resolve(baselinePath);
|
|
7799
8186
|
const baseline = await readQualityBaseline(absolute);
|
|
7800
|
-
const root =
|
|
8187
|
+
const root = path16.dirname(absolute);
|
|
7801
8188
|
const cases = [];
|
|
7802
8189
|
for (const entry of baseline.cases) {
|
|
7803
|
-
const file =
|
|
8190
|
+
const file = path16.resolve(root, entry.file);
|
|
7804
8191
|
let actual = null;
|
|
7805
8192
|
const violations = [];
|
|
7806
8193
|
const warnings = [];
|
|
@@ -7813,7 +8200,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
7813
8200
|
violations.push(message4(error));
|
|
7814
8201
|
}
|
|
7815
8202
|
if (entry.record) {
|
|
7816
|
-
const recordPath =
|
|
8203
|
+
const recordPath = path16.resolve(root, entry.record.path);
|
|
7817
8204
|
try {
|
|
7818
8205
|
const recordHash = await sha256File(recordPath);
|
|
7819
8206
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -7850,11 +8237,11 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
7850
8237
|
|
|
7851
8238
|
// src/pipeline/cache-health.ts
|
|
7852
8239
|
import { existsSync as existsSync15 } from "fs";
|
|
7853
|
-
import { readFile as
|
|
7854
|
-
import
|
|
8240
|
+
import { readFile as readFile12, readdir as readdir2, rm as rm6 } from "fs/promises";
|
|
8241
|
+
import path19 from "path";
|
|
7855
8242
|
|
|
7856
8243
|
// src/cache.ts
|
|
7857
|
-
import { mkdir as
|
|
8244
|
+
import { mkdir as mkdir4, readFile as readFile10, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
7858
8245
|
import { existsSync as existsSync12 } from "fs";
|
|
7859
8246
|
import pathModule from "path";
|
|
7860
8247
|
import { z as z5 } from "zod";
|
|
@@ -7870,10 +8257,10 @@ function isSha256Hash(value) {
|
|
|
7870
8257
|
function parseCache(value) {
|
|
7871
8258
|
return HashCacheSchema.parse(value);
|
|
7872
8259
|
}
|
|
7873
|
-
async function loadCache(
|
|
7874
|
-
if (!existsSync12(
|
|
8260
|
+
async function loadCache(path33) {
|
|
8261
|
+
if (!existsSync12(path33)) return { version: 1, hashes: {} };
|
|
7875
8262
|
try {
|
|
7876
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await
|
|
8263
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile10(path33, "utf8")));
|
|
7877
8264
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
7878
8265
|
return {
|
|
7879
8266
|
version: 1,
|
|
@@ -7885,18 +8272,18 @@ async function loadCache(path32) {
|
|
|
7885
8272
|
return { version: 1, hashes: {} };
|
|
7886
8273
|
}
|
|
7887
8274
|
}
|
|
7888
|
-
async function saveCache(
|
|
8275
|
+
async function saveCache(path33, cache) {
|
|
7889
8276
|
const sorted = {};
|
|
7890
8277
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
7891
8278
|
const hash = cache.hashes[key];
|
|
7892
8279
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
7893
8280
|
sorted[key] = hash;
|
|
7894
8281
|
}
|
|
7895
|
-
await
|
|
7896
|
-
const tmp = `${
|
|
8282
|
+
await mkdir4(pathModule.dirname(pathModule.resolve(path33)), { recursive: true });
|
|
8283
|
+
const tmp = `${path33}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
7897
8284
|
try {
|
|
7898
|
-
await
|
|
7899
|
-
await
|
|
8285
|
+
await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
8286
|
+
await rename4(tmp, path33);
|
|
7900
8287
|
} finally {
|
|
7901
8288
|
await rm4(tmp, { force: true });
|
|
7902
8289
|
}
|
|
@@ -7921,21 +8308,22 @@ function cachePathFor(lockPath) {
|
|
|
7921
8308
|
|
|
7922
8309
|
// src/pipeline/history.ts
|
|
7923
8310
|
import { existsSync as existsSync14 } from "fs";
|
|
7924
|
-
import
|
|
8311
|
+
import path18 from "path";
|
|
7925
8312
|
|
|
7926
8313
|
// src/pipeline/fetch.ts
|
|
7927
8314
|
import { existsSync as existsSync13, readFileSync as readFileSync3 } from "fs";
|
|
7928
|
-
import { mkdir as
|
|
7929
|
-
import
|
|
8315
|
+
import { mkdir as mkdir5, readFile as readFile11, rename as rename5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
|
|
8316
|
+
import path17 from "path";
|
|
7930
8317
|
async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
7931
8318
|
const log = opts.onProgress ?? (() => {
|
|
7932
8319
|
});
|
|
7933
8320
|
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
7934
8321
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
7935
8322
|
normalizeLockOutputPaths(lock, specs);
|
|
7936
|
-
const cacheDir = opts.cacheDir === false ? null :
|
|
8323
|
+
const cacheDir = opts.cacheDir === false ? null : path17.resolve(opts.cacheDir ?? path17.join(path17.dirname(lockPath), ".pixelkiln", "cache"));
|
|
7937
8324
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
7938
8325
|
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
8326
|
+
if (specByKey.get(key)?.mirror) return false;
|
|
7939
8327
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
7940
8328
|
if (e.status !== "downloaded") return false;
|
|
7941
8329
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
@@ -8034,7 +8422,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8034
8422
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
8035
8423
|
fresh = await provider.download(source.url);
|
|
8036
8424
|
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
8037
|
-
log(` current ${
|
|
8425
|
+
log(` current ${path17.relative(process.cwd(), target)}`);
|
|
8038
8426
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
8039
8427
|
continue;
|
|
8040
8428
|
}
|
|
@@ -8048,7 +8436,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8048
8436
|
if (cacheDir) {
|
|
8049
8437
|
await cacheMedia(
|
|
8050
8438
|
cacheDir,
|
|
8051
|
-
await
|
|
8439
|
+
await readFile11(target),
|
|
8052
8440
|
recorded.mediaType ?? MediaType.PNG,
|
|
8053
8441
|
recorded.sha256
|
|
8054
8442
|
);
|
|
@@ -8057,10 +8445,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8057
8445
|
continue;
|
|
8058
8446
|
}
|
|
8059
8447
|
if (reapply && !fresh) {
|
|
8060
|
-
if (!recorded.raw) raw = await
|
|
8061
|
-
log(` reapply ${
|
|
8448
|
+
if (!recorded.raw) raw = await readFile11(target);
|
|
8449
|
+
log(` reapply ${path17.relative(process.cwd(), target)}`);
|
|
8062
8450
|
} else {
|
|
8063
|
-
log(` changed ${
|
|
8451
|
+
log(` changed ${path17.relative(process.cwd(), target)} (upstream)`);
|
|
8064
8452
|
}
|
|
8065
8453
|
} else {
|
|
8066
8454
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
@@ -8079,7 +8467,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8079
8467
|
}
|
|
8080
8468
|
}
|
|
8081
8469
|
log(
|
|
8082
|
-
` replace ${
|
|
8470
|
+
` replace ${path17.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
8083
8471
|
);
|
|
8084
8472
|
}
|
|
8085
8473
|
}
|
|
@@ -8088,7 +8476,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8088
8476
|
} else if (!raw && recorded && cacheDir) {
|
|
8089
8477
|
if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
|
|
8090
8478
|
if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
|
|
8091
|
-
if (final || raw) log(` cached ${
|
|
8479
|
+
if (final || raw) log(` cached ${path17.relative(process.cwd(), target)}`);
|
|
8092
8480
|
}
|
|
8093
8481
|
if (!final && !raw) {
|
|
8094
8482
|
if (!source.url) {
|
|
@@ -8115,13 +8503,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8115
8503
|
buf = processed.bytes;
|
|
8116
8504
|
if (processed.changed) {
|
|
8117
8505
|
rawHash = sha256(raw);
|
|
8118
|
-
log(` snapped ${
|
|
8506
|
+
log(` snapped ${path17.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
|
|
8119
8507
|
}
|
|
8120
8508
|
}
|
|
8121
8509
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
8122
|
-
await
|
|
8510
|
+
await mkdir5(path17.dirname(target), { recursive: true });
|
|
8123
8511
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
8124
|
-
await
|
|
8512
|
+
await writeFile5(tmp, buf);
|
|
8125
8513
|
outputs.push({
|
|
8126
8514
|
path: portableOutputPath(target, spec.root),
|
|
8127
8515
|
sha256: sha256(buf),
|
|
@@ -8131,9 +8519,9 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8131
8519
|
});
|
|
8132
8520
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
8133
8521
|
await saveLock(lockPath, lock);
|
|
8134
|
-
await
|
|
8522
|
+
await rename5(tmp, target);
|
|
8135
8523
|
wrote = true;
|
|
8136
|
-
log(` wrote ${
|
|
8524
|
+
log(` wrote ${path17.relative(process.cwd(), target)}`);
|
|
8137
8525
|
}
|
|
8138
8526
|
if (opts.refresh && !wrote) {
|
|
8139
8527
|
if (rotated) {
|
|
@@ -8177,10 +8565,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
8177
8565
|
return result;
|
|
8178
8566
|
}
|
|
8179
8567
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
8180
|
-
const file =
|
|
8568
|
+
const file = path17.join(cacheDir, cacheFileName(hash, mediaType));
|
|
8181
8569
|
if (!existsSync13(file)) return null;
|
|
8182
8570
|
try {
|
|
8183
|
-
const buf = await
|
|
8571
|
+
const buf = await readFile11(file);
|
|
8184
8572
|
if (sha256(buf) !== hash) return null;
|
|
8185
8573
|
validateMedia(buf, mediaType);
|
|
8186
8574
|
return buf;
|
|
@@ -8191,13 +8579,13 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
8191
8579
|
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
8192
8580
|
const hash = knownHash ?? sha256(buf);
|
|
8193
8581
|
validateMedia(buf, mediaType);
|
|
8194
|
-
const file =
|
|
8582
|
+
const file = path17.join(cacheDir, cacheFileName(hash, mediaType));
|
|
8195
8583
|
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
8196
|
-
await
|
|
8584
|
+
await mkdir5(cacheDir, { recursive: true });
|
|
8197
8585
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
8198
|
-
await
|
|
8586
|
+
await writeFile5(tmp, buf);
|
|
8199
8587
|
try {
|
|
8200
|
-
await
|
|
8588
|
+
await rename5(tmp, file);
|
|
8201
8589
|
} catch (err) {
|
|
8202
8590
|
if (!existsSync13(file)) throw err;
|
|
8203
8591
|
} finally {
|
|
@@ -8320,7 +8708,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
8320
8708
|
if (!existsSync14(file)) continue;
|
|
8321
8709
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
8322
8710
|
throw new OverwriteRefusedError(
|
|
8323
|
-
`${
|
|
8711
|
+
`${path18.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
8324
8712
|
[file]
|
|
8325
8713
|
);
|
|
8326
8714
|
}
|
|
@@ -8372,10 +8760,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
8372
8760
|
// src/pipeline/cache-health.ts
|
|
8373
8761
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
8374
8762
|
if (options.prune && !existsSync15(lockPath)) {
|
|
8375
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
8763
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path19.resolve(lockPath)}`);
|
|
8376
8764
|
}
|
|
8377
|
-
const contentDir =
|
|
8378
|
-
const remotePath =
|
|
8765
|
+
const contentDir = path19.resolve(path19.dirname(lockPath), ".pixelkiln", "cache");
|
|
8766
|
+
const remotePath = path19.resolve(cachePathFor(lockPath));
|
|
8379
8767
|
const referenced = referencedHashes(lock);
|
|
8380
8768
|
let content = await inspectContentCache(contentDir, referenced);
|
|
8381
8769
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -8387,7 +8775,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
8387
8775
|
]);
|
|
8388
8776
|
for (const name of names) {
|
|
8389
8777
|
try {
|
|
8390
|
-
await rm6(
|
|
8778
|
+
await rm6(path19.join(contentDir, name), { force: true });
|
|
8391
8779
|
removed.contentFiles++;
|
|
8392
8780
|
} catch {
|
|
8393
8781
|
}
|
|
@@ -8396,7 +8784,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
8396
8784
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
8397
8785
|
removed.resetRemoteHashCache = true;
|
|
8398
8786
|
} else if (remoteHashes.invalidIds.length) {
|
|
8399
|
-
const cache = parseCache(JSON.parse(await
|
|
8787
|
+
const cache = parseCache(JSON.parse(await readFile12(remotePath, "utf8")));
|
|
8400
8788
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
8401
8789
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
8402
8790
|
await saveCache(remotePath, cache);
|
|
@@ -8435,12 +8823,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
8435
8823
|
continue;
|
|
8436
8824
|
}
|
|
8437
8825
|
report.files++;
|
|
8438
|
-
const file =
|
|
8826
|
+
const file = path19.join(contentDir, entry.name);
|
|
8439
8827
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
8440
8828
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
8441
8829
|
let bytes;
|
|
8442
8830
|
try {
|
|
8443
|
-
bytes = await
|
|
8831
|
+
bytes = await readFile12(file);
|
|
8444
8832
|
report.bytes += bytes.length;
|
|
8445
8833
|
} catch (err) {
|
|
8446
8834
|
report.invalid.push({
|
|
@@ -8487,7 +8875,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
8487
8875
|
if (!report.exists) return report;
|
|
8488
8876
|
let cache;
|
|
8489
8877
|
try {
|
|
8490
|
-
cache = parseCache(JSON.parse(await
|
|
8878
|
+
cache = parseCache(JSON.parse(await readFile12(remotePath, "utf8")));
|
|
8491
8879
|
} catch (err) {
|
|
8492
8880
|
report.error = err instanceof Error ? err.message : String(err);
|
|
8493
8881
|
return report;
|
|
@@ -8503,19 +8891,19 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
8503
8891
|
|
|
8504
8892
|
// src/pipeline/pack.ts
|
|
8505
8893
|
import { readFileSync as readFileSync4 } from "fs";
|
|
8506
|
-
import
|
|
8894
|
+
import path20 from "path";
|
|
8507
8895
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
8508
8896
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
8509
8897
|
if (!Array.isArray(raw) || !raw.length) {
|
|
8510
8898
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
8511
8899
|
}
|
|
8512
|
-
const baseDir =
|
|
8900
|
+
const baseDir = path20.dirname(path20.resolve(inputsFilePath));
|
|
8513
8901
|
return raw.map((entry, i) => {
|
|
8514
8902
|
const e = entry;
|
|
8515
8903
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
8516
8904
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
8517
8905
|
}
|
|
8518
|
-
return { id: e.id, path:
|
|
8906
|
+
return { id: e.id, path: path20.resolve(baseDir, e.path) };
|
|
8519
8907
|
});
|
|
8520
8908
|
}
|
|
8521
8909
|
function packSprites(inputs, options = {}) {
|
|
@@ -8587,7 +8975,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
8587
8975
|
}
|
|
8588
8976
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
8589
8977
|
id,
|
|
8590
|
-
path:
|
|
8978
|
+
path: path20.resolve(source)
|
|
8591
8979
|
}));
|
|
8592
8980
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
8593
8981
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -8611,7 +8999,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
8611
8999
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
8612
9000
|
const source = sources[id];
|
|
8613
9001
|
if (source && outputs.length <= 1) {
|
|
8614
|
-
inputs.push({ id, path:
|
|
9002
|
+
inputs.push({ id, path: path20.resolve(manifestDir, source) });
|
|
8615
9003
|
continue;
|
|
8616
9004
|
}
|
|
8617
9005
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -8645,7 +9033,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
8645
9033
|
}
|
|
8646
9034
|
}
|
|
8647
9035
|
for (const [id, source] of Object.entries(sources)) {
|
|
8648
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
9036
|
+
if (!locked.has(id)) inputs.push({ id, path: path20.resolve(manifestDir, source) });
|
|
8649
9037
|
}
|
|
8650
9038
|
if (!inputs.length) {
|
|
8651
9039
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -8753,7 +9141,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
8753
9141
|
const source = sources[id];
|
|
8754
9142
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
8755
9143
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
8756
|
-
placements.push({ id, path:
|
|
9144
|
+
placements.push({ id, path: path20.resolve(manifestDir, source), cell });
|
|
8757
9145
|
continue;
|
|
8758
9146
|
}
|
|
8759
9147
|
if (source && entry) {
|
|
@@ -8786,7 +9174,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
8786
9174
|
`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}`
|
|
8787
9175
|
);
|
|
8788
9176
|
}
|
|
8789
|
-
const basePng = mount.base ? readFileSync4(
|
|
9177
|
+
const basePng = mount.base ? readFileSync4(path20.resolve(manifestDir, mount.base)) : void 0;
|
|
8790
9178
|
const mounted = mountSprites(placements, {
|
|
8791
9179
|
cellWidth: mount.cellWidth,
|
|
8792
9180
|
cellHeight: mount.cellHeight,
|
|
@@ -8796,7 +9184,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
8796
9184
|
...mounted,
|
|
8797
9185
|
sources: mount.base ? [{
|
|
8798
9186
|
id: "$base",
|
|
8799
|
-
path:
|
|
9187
|
+
path: path20.resolve(manifestDir, mount.base),
|
|
8800
9188
|
sha256: sha256(basePng),
|
|
8801
9189
|
included: true
|
|
8802
9190
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -9169,7 +9557,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
9169
9557
|
});
|
|
9170
9558
|
const estimates = new Map(items.map((item) => [
|
|
9171
9559
|
item.key,
|
|
9172
|
-
validateCostEstimate(provider.id, provider.estimate(item.spec))
|
|
9560
|
+
item.spec.mirror ? { amount: 0, unit: item.spec.costUnit, candidates: 1 } : validateCostEstimate(provider.id, provider.estimate(item.spec))
|
|
9173
9561
|
]));
|
|
9174
9562
|
const units = new Set([...estimates.values()].map((estimate) => estimate.unit));
|
|
9175
9563
|
if (units.size > 1) {
|
|
@@ -9219,6 +9607,34 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
9219
9607
|
}
|
|
9220
9608
|
for (const { spec, key } of items) {
|
|
9221
9609
|
const estimate = estimates.get(key);
|
|
9610
|
+
if (spec.mirror) {
|
|
9611
|
+
try {
|
|
9612
|
+
await requireRevisionReady(spec, lock);
|
|
9613
|
+
await deriveMirror(spec, lock, lockPath, { onProgress: log });
|
|
9614
|
+
submitted++;
|
|
9615
|
+
} catch (err) {
|
|
9616
|
+
const message6 = err instanceof Error ? err.message : String(err);
|
|
9617
|
+
failed++;
|
|
9618
|
+
upsert(lock, key, {
|
|
9619
|
+
styleId: spec.styleId,
|
|
9620
|
+
assetId: spec.assetId,
|
|
9621
|
+
specHash: spec.specHash,
|
|
9622
|
+
generator: spec.generator,
|
|
9623
|
+
prompt: "",
|
|
9624
|
+
width: spec.width,
|
|
9625
|
+
height: spec.height,
|
|
9626
|
+
status: "failed",
|
|
9627
|
+
error: message6,
|
|
9628
|
+
outputs: lock.entries[key]?.outputs ?? [],
|
|
9629
|
+
cost: 0,
|
|
9630
|
+
costUnit: spec.costUnit,
|
|
9631
|
+
provider: provider.id
|
|
9632
|
+
});
|
|
9633
|
+
await saveLock(lockPath, lock);
|
|
9634
|
+
log(` FAILED ${key}: ${message6}`);
|
|
9635
|
+
}
|
|
9636
|
+
continue;
|
|
9637
|
+
}
|
|
9222
9638
|
await waitForSlot();
|
|
9223
9639
|
const since = Date.now() - lastSubmitAt;
|
|
9224
9640
|
if (since < spacing) await sleep(spacing - since);
|
|
@@ -9430,7 +9846,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
9430
9846
|
// src/pipeline/doctor.ts
|
|
9431
9847
|
import { constants, existsSync as existsSync16 } from "fs";
|
|
9432
9848
|
import { access, stat as stat2 } from "fs/promises";
|
|
9433
|
-
import
|
|
9849
|
+
import path21 from "path";
|
|
9434
9850
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
9435
9851
|
const checks = [];
|
|
9436
9852
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -9441,8 +9857,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
9441
9857
|
);
|
|
9442
9858
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
9443
9859
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
9444
|
-
|
|
9445
|
-
...spec.quality ? [
|
|
9860
|
+
path21.dirname(spec.outFile),
|
|
9861
|
+
...spec.quality ? [path21.dirname(spec.quality.outFile)] : []
|
|
9446
9862
|
]))];
|
|
9447
9863
|
const unwritable = [];
|
|
9448
9864
|
for (const dir of dirs) {
|
|
@@ -9465,7 +9881,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
9465
9881
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
9466
9882
|
const output = entry.outputs[index];
|
|
9467
9883
|
const spec = specByKey.get(key);
|
|
9468
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
9884
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path21.dirname(lockPath));
|
|
9469
9885
|
const owner = outputOwners.get(absolute);
|
|
9470
9886
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
9471
9887
|
outputOwners.set(absolute, key);
|
|
@@ -9480,7 +9896,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
9480
9896
|
} else {
|
|
9481
9897
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
9482
9898
|
}
|
|
9483
|
-
const cacheDir =
|
|
9899
|
+
const cacheDir = path21.join(path21.dirname(lockPath), ".pixelkiln", "cache");
|
|
9484
9900
|
const stranded = [];
|
|
9485
9901
|
const invalidState = [];
|
|
9486
9902
|
const staleMapUrls = [];
|
|
@@ -9496,7 +9912,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
9496
9912
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
9497
9913
|
let hasCache = false;
|
|
9498
9914
|
for (const output of entry.outputs) {
|
|
9499
|
-
const cached =
|
|
9915
|
+
const cached = path21.join(
|
|
9500
9916
|
cacheDir,
|
|
9501
9917
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
9502
9918
|
);
|
|
@@ -9596,7 +10012,7 @@ async function nearestExistingDirectory(start) {
|
|
|
9596
10012
|
if ((await stat2(current)).isDirectory()) return current;
|
|
9597
10013
|
} catch {
|
|
9598
10014
|
}
|
|
9599
|
-
const parent =
|
|
10015
|
+
const parent = path21.dirname(current);
|
|
9600
10016
|
if (parent === current) return current;
|
|
9601
10017
|
current = parent;
|
|
9602
10018
|
}
|
|
@@ -9604,8 +10020,8 @@ async function nearestExistingDirectory(start) {
|
|
|
9604
10020
|
|
|
9605
10021
|
// src/pipeline/adopt.ts
|
|
9606
10022
|
import { existsSync as existsSync17 } from "fs";
|
|
9607
|
-
import { readFile as
|
|
9608
|
-
import
|
|
10023
|
+
import { readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
|
|
10024
|
+
import path22 from "path";
|
|
9609
10025
|
async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
9610
10026
|
const log = opts.onProgress ?? (() => {
|
|
9611
10027
|
});
|
|
@@ -9620,7 +10036,7 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
9620
10036
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
9621
10037
|
continue;
|
|
9622
10038
|
}
|
|
9623
|
-
const bytes = await
|
|
10039
|
+
const bytes = await readFile13(spec.outFile);
|
|
9624
10040
|
try {
|
|
9625
10041
|
decodePng(bytes);
|
|
9626
10042
|
} catch (err) {
|
|
@@ -9739,12 +10155,12 @@ async function tagAdopted(provider, specs, lock, opts = {}) {
|
|
|
9739
10155
|
|
|
9740
10156
|
// src/pick/server.ts
|
|
9741
10157
|
import { existsSync as existsSync19 } from "fs";
|
|
9742
|
-
import { readFile as
|
|
10158
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
9743
10159
|
|
|
9744
10160
|
// src/pipeline/init.ts
|
|
9745
|
-
import { readdir as readdir3, readFile as
|
|
10161
|
+
import { readdir as readdir3, readFile as readFile14, writeFile as writeFile7 } from "fs/promises";
|
|
9746
10162
|
import { existsSync as existsSync18 } from "fs";
|
|
9747
|
-
import
|
|
10163
|
+
import path23 from "path";
|
|
9748
10164
|
function pngSize(buf) {
|
|
9749
10165
|
if (buf.length < 24) return null;
|
|
9750
10166
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -9758,7 +10174,7 @@ function slugify(value) {
|
|
|
9758
10174
|
async function walk(dir, exclude) {
|
|
9759
10175
|
const out = [];
|
|
9760
10176
|
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
9761
|
-
const full =
|
|
10177
|
+
const full = path23.join(dir, entry.name);
|
|
9762
10178
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
9763
10179
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
9764
10180
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -9772,14 +10188,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
9772
10188
|
const skipped = [];
|
|
9773
10189
|
const seen = /* @__PURE__ */ new Map();
|
|
9774
10190
|
for (const file of files.sort()) {
|
|
9775
|
-
const size = pngSize(await
|
|
10191
|
+
const size = pngSize(await readFile14(file));
|
|
9776
10192
|
if (!size) {
|
|
9777
10193
|
skipped.push(`${file} (not a readable PNG)`);
|
|
9778
10194
|
continue;
|
|
9779
10195
|
}
|
|
9780
|
-
const rel =
|
|
9781
|
-
const category =
|
|
9782
|
-
let id = slugify(
|
|
10196
|
+
const rel = path23.relative(root, file);
|
|
10197
|
+
const category = path23.dirname(rel) === "." ? "" : path23.dirname(rel);
|
|
10198
|
+
let id = slugify(path23.basename(rel));
|
|
9783
10199
|
if (seen.has(id)) {
|
|
9784
10200
|
const n = seen.get(id) + 1;
|
|
9785
10201
|
seen.set(id, n);
|
|
@@ -10230,7 +10646,7 @@ document.getElementById('submit').onclick = async () => {
|
|
|
10230
10646
|
|
|
10231
10647
|
// src/pick/review-server.ts
|
|
10232
10648
|
import { createServer } from "http";
|
|
10233
|
-
import { readFile as
|
|
10649
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
10234
10650
|
function serveReviewPage(opts) {
|
|
10235
10651
|
const log = opts.onProgress ?? (() => {
|
|
10236
10652
|
});
|
|
@@ -10239,7 +10655,7 @@ function serveReviewPage(opts) {
|
|
|
10239
10655
|
if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
|
|
10240
10656
|
try {
|
|
10241
10657
|
const asset = opts.assets.get(req.url);
|
|
10242
|
-
const bytes = await
|
|
10658
|
+
const bytes = await readFile15(asset.path);
|
|
10243
10659
|
res.writeHead(200, {
|
|
10244
10660
|
"Content-Type": asset.contentType,
|
|
10245
10661
|
"Cache-Control": "no-store",
|
|
@@ -10321,7 +10737,7 @@ async function currentArt(entry, spec) {
|
|
|
10321
10737
|
let width = entry.width;
|
|
10322
10738
|
let height = entry.height;
|
|
10323
10739
|
if (contentType === "image/png") {
|
|
10324
|
-
const size = pngSize(await
|
|
10740
|
+
const size = pngSize(await readFile16(file));
|
|
10325
10741
|
if (size) ({ width, height } = size);
|
|
10326
10742
|
}
|
|
10327
10743
|
return { path: file, contentType, width, height };
|
|
@@ -10487,13 +10903,13 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
10487
10903
|
// src/gallery/snapshot.ts
|
|
10488
10904
|
import { createHash as createHash4 } from "crypto";
|
|
10489
10905
|
import { existsSync as existsSync22 } from "fs";
|
|
10490
|
-
import { readFile as
|
|
10491
|
-
import
|
|
10906
|
+
import { readFile as readFile20, stat as stat3 } from "fs/promises";
|
|
10907
|
+
import path28 from "path";
|
|
10492
10908
|
|
|
10493
10909
|
// src/workspace.ts
|
|
10494
|
-
import { mkdir as
|
|
10910
|
+
import { mkdir as mkdir6, readFile as readFile17, rename as rename6, rm as rm7, writeFile as writeFile8 } from "fs/promises";
|
|
10495
10911
|
import { existsSync as existsSync20 } from "fs";
|
|
10496
|
-
import
|
|
10912
|
+
import path24 from "path";
|
|
10497
10913
|
import { z as z6 } from "zod";
|
|
10498
10914
|
var WorkspaceProjectSchema = z6.object({
|
|
10499
10915
|
id: z6.string().min(1),
|
|
@@ -10521,7 +10937,7 @@ async function loadWorkspace(workspacePath) {
|
|
|
10521
10937
|
if (!existsSync20(workspacePath)) return { version: 1, projects: [] };
|
|
10522
10938
|
let raw;
|
|
10523
10939
|
try {
|
|
10524
|
-
raw = JSON.parse(await
|
|
10940
|
+
raw = JSON.parse(await readFile17(workspacePath, "utf8"));
|
|
10525
10941
|
} catch (err) {
|
|
10526
10942
|
throw new ProjectError(
|
|
10527
10943
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
@@ -10535,22 +10951,22 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
10535
10951
|
version: 1,
|
|
10536
10952
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
10537
10953
|
};
|
|
10538
|
-
await
|
|
10954
|
+
await mkdir6(path24.dirname(path24.resolve(workspacePath)), { recursive: true });
|
|
10539
10955
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
10540
10956
|
try {
|
|
10541
|
-
await
|
|
10542
|
-
await
|
|
10957
|
+
await writeFile8(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
10958
|
+
await rename6(tmp, workspacePath);
|
|
10543
10959
|
} finally {
|
|
10544
10960
|
await rm7(tmp, { force: true });
|
|
10545
10961
|
}
|
|
10546
10962
|
}
|
|
10547
10963
|
function toPortablePath(dir, absolute) {
|
|
10548
|
-
return
|
|
10964
|
+
return path24.relative(dir, absolute).split(path24.sep).join("/");
|
|
10549
10965
|
}
|
|
10550
10966
|
function resolveProject(dir, project) {
|
|
10551
10967
|
return {
|
|
10552
|
-
manifestPath:
|
|
10553
|
-
lockPath:
|
|
10968
|
+
manifestPath: path24.resolve(dir, project.manifest.split("/").join(path24.sep)),
|
|
10969
|
+
lockPath: path24.resolve(dir, project.lock.split("/").join(path24.sep))
|
|
10554
10970
|
};
|
|
10555
10971
|
}
|
|
10556
10972
|
function validateWorkspace(ws, dir) {
|
|
@@ -10563,7 +10979,7 @@ function validateWorkspace(ws, dir) {
|
|
|
10563
10979
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
10564
10980
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
10565
10981
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
10566
|
-
if (
|
|
10982
|
+
if (path24.isAbsolute(project.manifest) || path24.isAbsolute(project.lock)) {
|
|
10567
10983
|
diagnostics.push({
|
|
10568
10984
|
id: "absolute-path",
|
|
10569
10985
|
level: "warning",
|
|
@@ -10624,12 +11040,12 @@ function validateWorkspace(ws, dir) {
|
|
|
10624
11040
|
}
|
|
10625
11041
|
|
|
10626
11042
|
// src/gallery/edit.ts
|
|
10627
|
-
import
|
|
11043
|
+
import path27 from "path";
|
|
10628
11044
|
import { z as z9 } from "zod";
|
|
10629
11045
|
|
|
10630
11046
|
// src/manifest-edit.ts
|
|
10631
|
-
import { readFile as
|
|
10632
|
-
import
|
|
11047
|
+
import { readFile as readFile18, rename as rename7, rm as rm8, writeFile as writeFile9 } from "fs/promises";
|
|
11048
|
+
import path25 from "path";
|
|
10633
11049
|
import { z as z7 } from "zod";
|
|
10634
11050
|
var HexSha = z7.string().regex(/^[0-9a-f]{64}$/);
|
|
10635
11051
|
var AssetPatchSchema = z7.object({
|
|
@@ -10844,8 +11260,8 @@ function applyEdit(raw, edit) {
|
|
|
10844
11260
|
}
|
|
10845
11261
|
}
|
|
10846
11262
|
async function applyManifestEdit(manifestPath, edit) {
|
|
10847
|
-
const absolute =
|
|
10848
|
-
const original = await
|
|
11263
|
+
const absolute = path25.resolve(manifestPath);
|
|
11264
|
+
const original = await readFile18(absolute, "utf8");
|
|
10849
11265
|
const before = sha256(original);
|
|
10850
11266
|
if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
10851
11267
|
let raw;
|
|
@@ -10859,12 +11275,12 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
10859
11275
|
applyEdit(raw, edit);
|
|
10860
11276
|
const next = serializeLike(original, raw);
|
|
10861
11277
|
if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
|
|
10862
|
-
const tmp =
|
|
10863
|
-
|
|
10864
|
-
`.${
|
|
11278
|
+
const tmp = path25.join(
|
|
11279
|
+
path25.dirname(absolute),
|
|
11280
|
+
`.${path25.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
10865
11281
|
);
|
|
10866
11282
|
try {
|
|
10867
|
-
await
|
|
11283
|
+
await writeFile9(tmp, next);
|
|
10868
11284
|
try {
|
|
10869
11285
|
const loaded = await loadManifest(tmp);
|
|
10870
11286
|
await resolveSpecs(loaded);
|
|
@@ -10872,7 +11288,7 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
10872
11288
|
const message6 = error instanceof Error ? error.message : String(error);
|
|
10873
11289
|
throw new ManifestEditError(message6.replace(tmp, absolute));
|
|
10874
11290
|
}
|
|
10875
|
-
await
|
|
11291
|
+
await rename7(tmp, absolute);
|
|
10876
11292
|
} finally {
|
|
10877
11293
|
await rm8(tmp, { force: true });
|
|
10878
11294
|
}
|
|
@@ -10881,17 +11297,17 @@ async function applyManifestEdit(manifestPath, edit) {
|
|
|
10881
11297
|
|
|
10882
11298
|
// src/pipeline/hand-edit.ts
|
|
10883
11299
|
import { existsSync as existsSync21 } from "fs";
|
|
10884
|
-
import { copyFile, mkdir as
|
|
10885
|
-
import
|
|
11300
|
+
import { copyFile, mkdir as mkdir7, readFile as readFile19, rename as rename8, rm as rm9, writeFile as writeFile10 } from "fs/promises";
|
|
11301
|
+
import path26 from "path";
|
|
10886
11302
|
import { z as z8 } from "zod";
|
|
10887
11303
|
var HAND_EDIT_DIR = "edits";
|
|
10888
11304
|
function handEditPath(loaded, spec) {
|
|
10889
11305
|
const style = loaded.manifest.styles[spec.styleId];
|
|
10890
|
-
const outDir =
|
|
10891
|
-
const relative =
|
|
10892
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
10893
|
-
const file = inside ? relative :
|
|
10894
|
-
return
|
|
11306
|
+
const outDir = path26.resolve(loaded.root, style?.outDir ?? ".");
|
|
11307
|
+
const relative = path26.relative(outDir, spec.outFile);
|
|
11308
|
+
const inside = relative && !relative.startsWith("..") && !path26.isAbsolute(relative);
|
|
11309
|
+
const file = inside ? relative : path26.basename(spec.outFile);
|
|
11310
|
+
return path26.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
10895
11311
|
}
|
|
10896
11312
|
function handEditBases(spec, lock) {
|
|
10897
11313
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -10937,7 +11353,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
10937
11353
|
`${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`
|
|
10938
11354
|
);
|
|
10939
11355
|
}
|
|
10940
|
-
const existing = spec.source ?
|
|
11356
|
+
const existing = spec.source ? path26.resolve(loaded.root, spec.source) : null;
|
|
10941
11357
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
10942
11358
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
10943
11359
|
let created = false;
|
|
@@ -10948,7 +11364,7 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
10948
11364
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
10949
11365
|
);
|
|
10950
11366
|
}
|
|
10951
|
-
await
|
|
11367
|
+
await mkdir7(path26.dirname(member.path), { recursive: true });
|
|
10952
11368
|
await copyFile(member.base, member.path);
|
|
10953
11369
|
created = true;
|
|
10954
11370
|
}
|
|
@@ -11027,7 +11443,7 @@ function handEditProjectPath(editPath) {
|
|
|
11027
11443
|
async function readHandEditCompanion(editPath) {
|
|
11028
11444
|
let text;
|
|
11029
11445
|
try {
|
|
11030
|
-
text = await
|
|
11446
|
+
text = await readFile19(handEditCompanionPath(editPath), "utf8");
|
|
11031
11447
|
} catch {
|
|
11032
11448
|
return null;
|
|
11033
11449
|
}
|
|
@@ -11070,7 +11486,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
11070
11486
|
}
|
|
11071
11487
|
});
|
|
11072
11488
|
const bases = handEditBases(spec, lock);
|
|
11073
|
-
const expectedSize = bases?.[0] ? decodePng(await
|
|
11489
|
+
const expectedSize = bases?.[0] ? decodePng(await readFile19(bases[0].path)) : { width: spec.width, height: spec.height };
|
|
11074
11490
|
for (const [index, image] of decoded.entries()) {
|
|
11075
11491
|
if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
|
|
11076
11492
|
const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
|
|
@@ -11086,7 +11502,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
11086
11502
|
editor: input.editor,
|
|
11087
11503
|
protocol: input.protocol,
|
|
11088
11504
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
11089
|
-
project: projectPath ?
|
|
11505
|
+
project: projectPath ? path26.basename(projectPath) : null,
|
|
11090
11506
|
outputs: incoming.map((frame, index) => ({
|
|
11091
11507
|
role: frame.role,
|
|
11092
11508
|
basedOn: basedOnHash(entry, index),
|
|
@@ -11116,8 +11532,8 @@ function basedOnHash(entry, index) {
|
|
|
11116
11532
|
async function replaceFile(file, bytes) {
|
|
11117
11533
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
11118
11534
|
try {
|
|
11119
|
-
await
|
|
11120
|
-
await
|
|
11535
|
+
await writeFile10(tmp, bytes);
|
|
11536
|
+
await rename8(tmp, file);
|
|
11121
11537
|
} finally {
|
|
11122
11538
|
await rm9(tmp, { force: true });
|
|
11123
11539
|
}
|
|
@@ -11216,7 +11632,7 @@ function createGalleryEditHandler(opts) {
|
|
|
11216
11632
|
}
|
|
11217
11633
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
11218
11634
|
if (result.changed) {
|
|
11219
|
-
const relative =
|
|
11635
|
+
const relative = path27.relative(process.cwd(), result.manifestPath);
|
|
11220
11636
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
11221
11637
|
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;
|
|
11222
11638
|
log(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
|
|
@@ -11228,7 +11644,7 @@ function createGalleryEditHandler(opts) {
|
|
|
11228
11644
|
// src/gallery/snapshot.ts
|
|
11229
11645
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
11230
11646
|
function galleryMediaId(absolutePath) {
|
|
11231
|
-
return createHash4("sha256").update(
|
|
11647
|
+
return createHash4("sha256").update(path28.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
11232
11648
|
}
|
|
11233
11649
|
function galleryMediaRoute(id) {
|
|
11234
11650
|
return `/media/${id}`;
|
|
@@ -11247,7 +11663,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
11247
11663
|
return (entry.history ?? []).map((generation, i) => {
|
|
11248
11664
|
const outputs = generation.outputs.map((output) => {
|
|
11249
11665
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
11250
|
-
const file =
|
|
11666
|
+
const file = path28.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
11251
11667
|
let url = null;
|
|
11252
11668
|
if (existsSync22(file)) {
|
|
11253
11669
|
const id = galleryMediaId(file);
|
|
@@ -11278,7 +11694,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
11278
11694
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
11279
11695
|
async function samePixels(a, b) {
|
|
11280
11696
|
try {
|
|
11281
|
-
const [left, right] = await Promise.all([
|
|
11697
|
+
const [left, right] = await Promise.all([readFile20(a), readFile20(b)]);
|
|
11282
11698
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
11283
11699
|
const x = decodePng(left);
|
|
11284
11700
|
const y = decodePng(right);
|
|
@@ -11348,9 +11764,9 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
11348
11764
|
async function describeQuality(media, root, inspection) {
|
|
11349
11765
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
11350
11766
|
const outputs = await Promise.all(
|
|
11351
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
11767
|
+
outputPaths.map((file) => describeOutput(media, root, path28.resolve(root, file)))
|
|
11352
11768
|
);
|
|
11353
|
-
const recordPath =
|
|
11769
|
+
const recordPath = path28.resolve(root, inspection.record);
|
|
11354
11770
|
const recordExists = existsSync22(recordPath);
|
|
11355
11771
|
let options = null;
|
|
11356
11772
|
let check = null;
|
|
@@ -11370,7 +11786,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
11370
11786
|
return {
|
|
11371
11787
|
state: inspection.state,
|
|
11372
11788
|
reason: inspection.reason,
|
|
11373
|
-
output: portableOutputPath(
|
|
11789
|
+
output: portableOutputPath(path28.resolve(root, inspection.output), root),
|
|
11374
11790
|
record: portableOutputPath(recordPath, root),
|
|
11375
11791
|
recordExists,
|
|
11376
11792
|
outputs,
|
|
@@ -11407,9 +11823,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
11407
11823
|
const media = /* @__PURE__ */ new Map();
|
|
11408
11824
|
const plan = await buildPlan(specs, lock);
|
|
11409
11825
|
const items = [];
|
|
11410
|
-
const manifestText = await
|
|
11826
|
+
const manifestText = await readFile20(loaded.path, "utf8");
|
|
11411
11827
|
const manifestSha256 = sha256(manifestText);
|
|
11412
|
-
const cacheDir =
|
|
11828
|
+
const cacheDir = path28.resolve(path28.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
11413
11829
|
let rawStyles = {};
|
|
11414
11830
|
try {
|
|
11415
11831
|
const raw = JSON.parse(manifestText);
|
|
@@ -11433,7 +11849,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11433
11849
|
)
|
|
11434
11850
|
);
|
|
11435
11851
|
if (spec.source) {
|
|
11436
|
-
const editPath =
|
|
11852
|
+
const editPath = path28.resolve(root, spec.source);
|
|
11437
11853
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
11438
11854
|
const editShas = await Promise.all(
|
|
11439
11855
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
@@ -11482,7 +11898,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11482
11898
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
11483
11899
|
}
|
|
11484
11900
|
} else if (spec.source) {
|
|
11485
|
-
outputs = [await describeOutput(media, root,
|
|
11901
|
+
outputs = [await describeOutput(media, root, path28.resolve(root, spec.source))];
|
|
11486
11902
|
} else if (!entry && existsSync22(spec.outFile)) {
|
|
11487
11903
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
11488
11904
|
} else {
|
|
@@ -11522,6 +11938,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11522
11938
|
currentSpecHash: spec.specHash,
|
|
11523
11939
|
revision: entry?.revision ?? null,
|
|
11524
11940
|
revisionParentKey: spec.revision ? lockKey(spec.styleId, spec.revision.sourceAssetId) : entry?.revision ? lockKey(spec.styleId, entry.revision.sourceAssetId) : null,
|
|
11941
|
+
mirrorOfKey: spec.mirror ? lockKey(spec.styleId, spec.mirror.sourceAssetId) : entry?.mirror ? lockKey(spec.styleId, entry.mirror.sourceAssetId) : null,
|
|
11525
11942
|
character: describeCharacter(spec, entry),
|
|
11526
11943
|
outputs,
|
|
11527
11944
|
quality,
|
|
@@ -11584,6 +12001,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11584
12001
|
currentSpecHash: null,
|
|
11585
12002
|
revision: entry.revision,
|
|
11586
12003
|
revisionParentKey: entry.revision ? lockKey(entry.styleId, entry.revision.sourceAssetId) : null,
|
|
12004
|
+
mirrorOfKey: entry.mirror ? lockKey(entry.styleId, entry.mirror.sourceAssetId) : null,
|
|
11587
12005
|
character: describeCharacter(void 0, entry),
|
|
11588
12006
|
outputs,
|
|
11589
12007
|
quality: null,
|
|
@@ -11672,7 +12090,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11672
12090
|
id: loaded.manifest.name,
|
|
11673
12091
|
name: loaded.manifest.name,
|
|
11674
12092
|
manifest: loaded.path,
|
|
11675
|
-
lock:
|
|
12093
|
+
lock: path28.resolve(opts.lockPath),
|
|
11676
12094
|
root,
|
|
11677
12095
|
provider: loaded.manifest.provider,
|
|
11678
12096
|
account: null,
|
|
@@ -11698,7 +12116,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
11698
12116
|
return { snapshot, media };
|
|
11699
12117
|
}
|
|
11700
12118
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
11701
|
-
const dir =
|
|
12119
|
+
const dir = path28.dirname(path28.resolve(opts.workspacePath));
|
|
11702
12120
|
const media = /* @__PURE__ */ new Map();
|
|
11703
12121
|
const projects = [];
|
|
11704
12122
|
const items = [];
|
|
@@ -11741,11 +12159,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
11741
12159
|
id: project.id,
|
|
11742
12160
|
name: loaded.manifest.name,
|
|
11743
12161
|
manifest: manifestPath,
|
|
11744
|
-
lock:
|
|
12162
|
+
lock: path28.resolve(lockPath),
|
|
11745
12163
|
root: loaded.root,
|
|
11746
12164
|
provider: loaded.manifest.provider,
|
|
11747
12165
|
account: project.account ?? null,
|
|
11748
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
12166
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile20(manifestPath, "utf8")),
|
|
11749
12167
|
historyLimit: historyLimit(loaded.manifest),
|
|
11750
12168
|
entries: Object.keys(lock.entries).length,
|
|
11751
12169
|
items: projectItems.length,
|
|
@@ -11757,8 +12175,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
11757
12175
|
id: project.id,
|
|
11758
12176
|
name: project.id,
|
|
11759
12177
|
manifest: manifestPath,
|
|
11760
|
-
lock:
|
|
11761
|
-
root:
|
|
12178
|
+
lock: path28.resolve(lockPath),
|
|
12179
|
+
root: path28.dirname(manifestPath),
|
|
11762
12180
|
provider: project.provider,
|
|
11763
12181
|
account: project.account ?? null,
|
|
11764
12182
|
manifestSha256: null,
|
|
@@ -11785,7 +12203,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
11785
12203
|
version: 1,
|
|
11786
12204
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
11787
12205
|
project: null,
|
|
11788
|
-
workspace: { path:
|
|
12206
|
+
workspace: { path: path28.resolve(opts.workspacePath), dir, projects },
|
|
11789
12207
|
filter,
|
|
11790
12208
|
totals: {
|
|
11791
12209
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -11935,9 +12353,9 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
|
|
|
11935
12353
|
// src/editor/install.ts
|
|
11936
12354
|
import { createHash as createHash5 } from "crypto";
|
|
11937
12355
|
import { existsSync as existsSync23 } from "fs";
|
|
11938
|
-
import { mkdir as
|
|
12356
|
+
import { mkdir as mkdir8, readFile as readFile21, rename as rename9, rm as rm10, stat as stat4, writeFile as writeFile11 } from "fs/promises";
|
|
11939
12357
|
import os2 from "os";
|
|
11940
|
-
import
|
|
12358
|
+
import path29 from "path";
|
|
11941
12359
|
var EditorInstallError = class extends Error {
|
|
11942
12360
|
constructor(message6, file) {
|
|
11943
12361
|
super(message6);
|
|
@@ -11948,15 +12366,15 @@ var EditorInstallError = class extends Error {
|
|
|
11948
12366
|
};
|
|
11949
12367
|
function editorToolsRoot() {
|
|
11950
12368
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
11951
|
-
if (override) return
|
|
11952
|
-
if (process.platform === "darwin") return
|
|
12369
|
+
if (override) return path29.resolve(override);
|
|
12370
|
+
if (process.platform === "darwin") return path29.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
11953
12371
|
if (process.platform === "win32") {
|
|
11954
|
-
return
|
|
12372
|
+
return path29.join(process.env.LOCALAPPDATA ?? path29.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
11955
12373
|
}
|
|
11956
|
-
return
|
|
12374
|
+
return path29.join(process.env.XDG_CACHE_HOME ?? path29.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
|
|
11957
12375
|
}
|
|
11958
12376
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
11959
|
-
return
|
|
12377
|
+
return path29.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
11960
12378
|
}
|
|
11961
12379
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
11962
12380
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -11966,7 +12384,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
11966
12384
|
}
|
|
11967
12385
|
async function sha256Of(file) {
|
|
11968
12386
|
try {
|
|
11969
|
-
return createHash5("sha256").update(await
|
|
12387
|
+
return createHash5("sha256").update(await readFile21(file)).digest("hex");
|
|
11970
12388
|
} catch {
|
|
11971
12389
|
return null;
|
|
11972
12390
|
}
|
|
@@ -11979,7 +12397,7 @@ async function editorStatus(opts = {}) {
|
|
|
11979
12397
|
let totalBytes = 0;
|
|
11980
12398
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
11981
12399
|
totalBytes += expected.bytes;
|
|
11982
|
-
const file =
|
|
12400
|
+
const file = path29.join(dir, name);
|
|
11983
12401
|
const info = existsSync23(file) ? await stat4(file).catch(() => null) : null;
|
|
11984
12402
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
11985
12403
|
installedBytes += expected.bytes;
|
|
@@ -12009,7 +12427,7 @@ async function installEditor(opts = {}) {
|
|
|
12009
12427
|
);
|
|
12010
12428
|
}
|
|
12011
12429
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
12012
|
-
await
|
|
12430
|
+
await mkdir8(dir, { recursive: true });
|
|
12013
12431
|
const downloaded = [];
|
|
12014
12432
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
12015
12433
|
let fetchedBytes = before.installedBytes;
|
|
@@ -12037,11 +12455,11 @@ async function installEditor(opts = {}) {
|
|
|
12037
12455
|
if (digest2 !== expected.sha256) {
|
|
12038
12456
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
12039
12457
|
}
|
|
12040
|
-
const final =
|
|
12458
|
+
const final = path29.join(dir, name);
|
|
12041
12459
|
const tmp = `${final}.${process.pid}.part`;
|
|
12042
12460
|
try {
|
|
12043
|
-
await
|
|
12044
|
-
await
|
|
12461
|
+
await writeFile11(tmp, bytes);
|
|
12462
|
+
await rename9(tmp, final);
|
|
12045
12463
|
} finally {
|
|
12046
12464
|
await rm10(tmp, { force: true });
|
|
12047
12465
|
}
|
|
@@ -12083,12 +12501,12 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
12083
12501
|
".json": "application/json; charset=utf-8"
|
|
12084
12502
|
};
|
|
12085
12503
|
function editorContentType(name) {
|
|
12086
|
-
return EDITOR_CONTENT_TYPES[
|
|
12504
|
+
return EDITOR_CONTENT_TYPES[path29.extname(name).toLowerCase()] ?? null;
|
|
12087
12505
|
}
|
|
12088
12506
|
|
|
12089
12507
|
// src/gallery/editor.ts
|
|
12090
|
-
import { readFile as
|
|
12091
|
-
import
|
|
12508
|
+
import { readFile as readFile22 } from "fs/promises";
|
|
12509
|
+
import path30 from "path";
|
|
12092
12510
|
function editorRoute(release, name = "index.html") {
|
|
12093
12511
|
return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
|
|
12094
12512
|
}
|
|
@@ -12161,7 +12579,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
12161
12579
|
if (!status.installed) return null;
|
|
12162
12580
|
let bytes;
|
|
12163
12581
|
try {
|
|
12164
|
-
bytes = await
|
|
12582
|
+
bytes = await readFile22(path30.join(dir, name));
|
|
12165
12583
|
} catch {
|
|
12166
12584
|
verified = null;
|
|
12167
12585
|
return null;
|
|
@@ -12178,7 +12596,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
12178
12596
|
// src/gallery/server.ts
|
|
12179
12597
|
import { randomBytes } from "crypto";
|
|
12180
12598
|
import { createServer as createServer2 } from "http";
|
|
12181
|
-
import { readFile as
|
|
12599
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
12182
12600
|
var MAX_EDIT_BYTES = 48 * 1024 * 1024;
|
|
12183
12601
|
var EDITOR_CSP = [
|
|
12184
12602
|
"default-src 'self'",
|
|
@@ -12337,7 +12755,7 @@ async function serveGallery(opts) {
|
|
|
12337
12755
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
12338
12756
|
if (!asset) return fail(404, "no such review asset");
|
|
12339
12757
|
try {
|
|
12340
|
-
const bytes = await
|
|
12758
|
+
const bytes = await readFile23(asset.path);
|
|
12341
12759
|
res.writeHead(200, {
|
|
12342
12760
|
"Content-Type": asset.contentType,
|
|
12343
12761
|
"Cache-Control": "no-store",
|
|
@@ -12413,7 +12831,7 @@ async function serveGallery(opts) {
|
|
|
12413
12831
|
if (!asset) return fail(404, "no such gallery media");
|
|
12414
12832
|
let bytes;
|
|
12415
12833
|
try {
|
|
12416
|
-
bytes = await
|
|
12834
|
+
bytes = await readFile23(asset.path);
|
|
12417
12835
|
} catch {
|
|
12418
12836
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
12419
12837
|
}
|
|
@@ -12787,9 +13205,9 @@ function createGenerateHandlers(opts) {
|
|
|
12787
13205
|
}
|
|
12788
13206
|
|
|
12789
13207
|
// src/pipeline/salvage.ts
|
|
12790
|
-
import { readFile as
|
|
13208
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
12791
13209
|
import { existsSync as existsSync24 } from "fs";
|
|
12792
|
-
import
|
|
13210
|
+
import path31 from "path";
|
|
12793
13211
|
function providerClaimId(provider, objectId) {
|
|
12794
13212
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
12795
13213
|
}
|
|
@@ -12799,7 +13217,7 @@ async function loadClaims(lockPaths, opts = {}) {
|
|
|
12799
13217
|
if (!existsSync24(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
12800
13218
|
let parsed;
|
|
12801
13219
|
try {
|
|
12802
|
-
parsed = parseLock(JSON.parse(await
|
|
13220
|
+
parsed = parseLock(JSON.parse(await readFile24(p, "utf8")));
|
|
12803
13221
|
} catch {
|
|
12804
13222
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
12805
13223
|
}
|
|
@@ -12852,20 +13270,20 @@ function matchOrphanStyle(prompt, manifest) {
|
|
|
12852
13270
|
return matchStyleByPattern(prompt, manifest);
|
|
12853
13271
|
}
|
|
12854
13272
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
12855
|
-
const own =
|
|
13273
|
+
const own = path31.resolve(ownManifestPath);
|
|
12856
13274
|
const siblingManifestPaths = [
|
|
12857
13275
|
.../* @__PURE__ */ new Set([
|
|
12858
13276
|
...workspaceManifestPaths,
|
|
12859
|
-
...claimPaths.map((c) =>
|
|
13277
|
+
...claimPaths.map((c) => path31.join(path31.dirname(path31.resolve(c)), "pixelkiln.manifest.json"))
|
|
12860
13278
|
])
|
|
12861
13279
|
];
|
|
12862
13280
|
const siblings = [];
|
|
12863
13281
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
12864
|
-
if (
|
|
13282
|
+
if (path31.resolve(siblingManifestPath) === own) continue;
|
|
12865
13283
|
if (!existsSync24(siblingManifestPath)) continue;
|
|
12866
13284
|
try {
|
|
12867
13285
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
12868
|
-
siblings.push({ label:
|
|
13286
|
+
siblings.push({ label: path31.basename(path31.dirname(siblingManifestPath)), manifest });
|
|
12869
13287
|
} catch {
|
|
12870
13288
|
}
|
|
12871
13289
|
}
|
|
@@ -12981,8 +13399,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
12981
13399
|
}
|
|
12982
13400
|
|
|
12983
13401
|
// src/pick/salvage-server.ts
|
|
12984
|
-
import { mkdir as
|
|
12985
|
-
import
|
|
13402
|
+
import { mkdir as mkdir9, writeFile as writeFile12, readFile as readFile25 } from "fs/promises";
|
|
13403
|
+
import path32 from "path";
|
|
12986
13404
|
|
|
12987
13405
|
// src/pick/salvage-sheet.ts
|
|
12988
13406
|
var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -13147,7 +13565,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13147
13565
|
});
|
|
13148
13566
|
const html = renderSalvageSheet(orphans, {
|
|
13149
13567
|
styleId: ctx.styleId,
|
|
13150
|
-
importDir:
|
|
13568
|
+
importDir: path32.relative(process.cwd(), ctx.importDir) || "."
|
|
13151
13569
|
});
|
|
13152
13570
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
13153
13571
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -13176,10 +13594,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13176
13594
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
13177
13595
|
decodePng(buf);
|
|
13178
13596
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
13179
|
-
const rel =
|
|
13180
|
-
const outFile =
|
|
13181
|
-
await
|
|
13182
|
-
await
|
|
13597
|
+
const rel = path32.join("_salvaged", `${assetId}.png`);
|
|
13598
|
+
const outFile = path32.resolve(ctx.importDir, rel);
|
|
13599
|
+
await mkdir9(path32.dirname(outFile), { recursive: true });
|
|
13600
|
+
await writeFile12(outFile, buf);
|
|
13183
13601
|
ctx.manifest.assets[assetId] = {
|
|
13184
13602
|
prompt: orphan.prompt,
|
|
13185
13603
|
promptByStyle: {},
|
|
@@ -13211,7 +13629,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13211
13629
|
sourceUrl: durableSource,
|
|
13212
13630
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
13213
13631
|
outputs: [{
|
|
13214
|
-
path: portableOutputPath(outFile,
|
|
13632
|
+
path: portableOutputPath(outFile, path32.dirname(ctx.manifestPath)),
|
|
13215
13633
|
sha256: sha256(buf)
|
|
13216
13634
|
}],
|
|
13217
13635
|
submittedAt: orphan.createdAt,
|
|
@@ -13235,9 +13653,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13235
13653
|
}
|
|
13236
13654
|
}
|
|
13237
13655
|
await applyTags(provider, decisions, existingTags, { onProgress: log });
|
|
13238
|
-
const raw = JSON.parse(await
|
|
13656
|
+
const raw = JSON.parse(await readFile25(ctx.manifestPath, "utf8"));
|
|
13239
13657
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
13240
|
-
await
|
|
13658
|
+
await writeFile12(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
13241
13659
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
13242
13660
|
return result;
|
|
13243
13661
|
}
|
|
@@ -13348,10 +13766,12 @@ export {
|
|
|
13348
13766
|
BudgetError,
|
|
13349
13767
|
CHARACTER_DIRECTIONS_4,
|
|
13350
13768
|
CHARACTER_DIRECTIONS_8,
|
|
13769
|
+
CHARACTER_PROPORTION_PRESETS,
|
|
13351
13770
|
CharacterAnimationModeSchema,
|
|
13352
13771
|
CharacterAnimationSchema,
|
|
13353
13772
|
CharacterDirectionSchema,
|
|
13354
13773
|
CharacterModeSchema,
|
|
13774
|
+
CharacterProportionsSchema,
|
|
13355
13775
|
CharacterStateSchema,
|
|
13356
13776
|
ComfyUIProvider,
|
|
13357
13777
|
DEFAULT_HISTORY_LIMIT,
|
|
@@ -13379,6 +13799,7 @@ export {
|
|
|
13379
13799
|
MAX_DOWNLOAD_BYTES,
|
|
13380
13800
|
MAX_HAND_EDIT_BYTES,
|
|
13381
13801
|
MAX_RETRIES,
|
|
13802
|
+
MIRRORED_DIRECTION,
|
|
13382
13803
|
ManifestDriftError,
|
|
13383
13804
|
ManifestEditError,
|
|
13384
13805
|
ManifestEditSchema,
|
|
@@ -13517,6 +13938,7 @@ export {
|
|
|
13517
13938
|
portableOutputPath,
|
|
13518
13939
|
prepareReview,
|
|
13519
13940
|
primaryOutput,
|
|
13941
|
+
proportionsPayload,
|
|
13520
13942
|
providerCredentialEnvs,
|
|
13521
13943
|
providerFactory,
|
|
13522
13944
|
pushTags,
|