pixelkiln 0.35.0 → 0.37.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/CONTRIBUTING.md +7 -0
- package/README.md +1 -1
- package/dist/cli.js +800 -589
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +42 -5
- package/dist/index.cjs +551 -323
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +616 -71
- package/dist/index.d.ts +616 -71
- package/dist/index.js +538 -320
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +8 -1
- package/docs/CLI.md +62 -2
- package/docs/LIBRARY.md +32 -0
- package/docs/MANIFEST.md +50 -2
- package/docs/QUALITY.md +16 -0
- package/docs/RECIPES.md +6 -2
- package/package.json +3 -1
- package/recipes/comfyui/pixel-art-xl-environment/1.0.0/pixelkiln.recipe.json +1 -1
- package/recipes/comfyui/pixel-art-xl-img2img/1.0.0/pixelkiln.recipe.json +1 -1
- package/schema/manifest.schema.json +8 -0
- package/schema/recipe.schema.json +4 -0
package/dist/index.js
CHANGED
|
@@ -170,6 +170,18 @@ var StyleObjectSchema = z.object({
|
|
|
170
170
|
* chocolate bar.
|
|
171
171
|
*/
|
|
172
172
|
palette: z.array(HexColorSchema).default([]),
|
|
173
|
+
/**
|
|
174
|
+
* Snap every visible pixel of the downloaded art to the nearest `palette`
|
|
175
|
+
* colour, without dithering. The palette is sent to providers that take
|
|
176
|
+
* one, and most honour it most of the time; this makes it a guarantee
|
|
177
|
+
* for every generator, including the ones with no palette parameter.
|
|
178
|
+
* Applied by `fetch` as the file is written and recorded on the lock
|
|
179
|
+
* entry, so a restore reproduces the same bytes. Turning it on or off
|
|
180
|
+
* does not change the spec hash: `plan` reports the entry as recoverable
|
|
181
|
+
* and the next `fetch` or `restore` re-applies it from the art already
|
|
182
|
+
* on disk or in the cache, spending nothing.
|
|
183
|
+
*/
|
|
184
|
+
enforcePalette: z.boolean().default(false),
|
|
173
185
|
/** Fail-closed native-grid, palette, and human-approval policy for derived art. */
|
|
174
186
|
quality: QualityProfileSchema.optional(),
|
|
175
187
|
/**
|
|
@@ -211,6 +223,13 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
|
|
|
211
223
|
path: ["quality"]
|
|
212
224
|
});
|
|
213
225
|
}
|
|
226
|
+
if (style.enforcePalette && style.palette.length < 2) {
|
|
227
|
+
ctx.addIssue({
|
|
228
|
+
code: z.ZodIssueCode.custom,
|
|
229
|
+
message: "enforcePalette needs a palette of at least two colours to snap to",
|
|
230
|
+
path: ["enforcePalette"]
|
|
231
|
+
});
|
|
232
|
+
}
|
|
214
233
|
});
|
|
215
234
|
var InheritedStyleSchema = StyleObjectSchema.partial().extend({
|
|
216
235
|
extends: z.string().min(1),
|
|
@@ -333,7 +352,21 @@ var LockOutputSchema = z.object({
|
|
|
333
352
|
path: z.string(),
|
|
334
353
|
sha256: z.string(),
|
|
335
354
|
role: z.string().optional(),
|
|
336
|
-
mediaType: MediaTypeSchema.optional()
|
|
355
|
+
mediaType: MediaTypeSchema.optional(),
|
|
356
|
+
/**
|
|
357
|
+
* Hash of the provider's bytes before post-processing changed them, when
|
|
358
|
+
* it did. The content cache keeps both, so the post-processing can be
|
|
359
|
+
* undone or redone without contacting the provider.
|
|
360
|
+
*/
|
|
361
|
+
raw: z.string().optional()
|
|
362
|
+
});
|
|
363
|
+
var PostprocessSchema = z.object({
|
|
364
|
+
palette: z.object({
|
|
365
|
+
/** Canonical lowercase `#rrggbb` values, in manifest order. */
|
|
366
|
+
colors: z.array(z.string()).min(2).max(256),
|
|
367
|
+
dither: z.literal("none"),
|
|
368
|
+
distance: z.literal("redmean")
|
|
369
|
+
}).optional()
|
|
337
370
|
});
|
|
338
371
|
var LockSourceSchema = z.object({
|
|
339
372
|
url: z.string(),
|
|
@@ -359,6 +392,7 @@ var LockHistoryEntrySchema = z.object({
|
|
|
359
392
|
cost: z.number().finite().nonnegative().default(0),
|
|
360
393
|
costUnit: z.string().min(1).default("generations"),
|
|
361
394
|
provider: z.string().default("pixellab"),
|
|
395
|
+
postprocess: PostprocessSchema.optional(),
|
|
362
396
|
/** When the replacement was submitted. */
|
|
363
397
|
retiredAt: z.string()
|
|
364
398
|
});
|
|
@@ -430,14 +464,7 @@ var LockEntrySchema = z.object({
|
|
|
430
464
|
* `role` labels non-primary artifacts (e.g. "portrait", "spriteframes") so a
|
|
431
465
|
* consumer can find the one it wants without pattern-matching on paths.
|
|
432
466
|
*/
|
|
433
|
-
outputs: z.array(
|
|
434
|
-
z.object({
|
|
435
|
-
path: z.string(),
|
|
436
|
-
sha256: z.string(),
|
|
437
|
-
role: z.string().optional(),
|
|
438
|
-
mediaType: MediaTypeSchema.optional()
|
|
439
|
-
})
|
|
440
|
-
).default([]),
|
|
467
|
+
outputs: z.array(LockOutputSchema).default([]),
|
|
441
468
|
/**
|
|
442
469
|
* Provider-owned data retained for downstream consumers, namespaced by
|
|
443
470
|
* provider id. PixelLab connectable sets keep their exact `tileRules` here
|
|
@@ -453,7 +480,9 @@ var LockEntrySchema = z.object({
|
|
|
453
480
|
/** Which provider produced this. Absent on entries written before providers. */
|
|
454
481
|
provider: z.string().default("pixellab"),
|
|
455
482
|
/** Generations this entry replaced, newest first; see LockHistoryEntrySchema. */
|
|
456
|
-
history: z.array(LockHistoryEntrySchema).optional()
|
|
483
|
+
history: z.array(LockHistoryEntrySchema).optional(),
|
|
484
|
+
/** Post-processing applied to the recorded outputs; absent when the bytes are the provider's. */
|
|
485
|
+
postprocess: PostprocessSchema.optional()
|
|
457
486
|
});
|
|
458
487
|
var LockSchema = z.object({
|
|
459
488
|
version: z.literal(2),
|
|
@@ -474,6 +503,72 @@ function lockKey(styleId, assetId) {
|
|
|
474
503
|
return `${styleId}/${assetId}`;
|
|
475
504
|
}
|
|
476
505
|
|
|
506
|
+
// src/errors.ts
|
|
507
|
+
var EXIT_CODES = {
|
|
508
|
+
usage: 2,
|
|
509
|
+
project: 3,
|
|
510
|
+
provider: 4,
|
|
511
|
+
"refused-overwrite": 5,
|
|
512
|
+
budget: 6,
|
|
513
|
+
capability: 7
|
|
514
|
+
};
|
|
515
|
+
var PixelKilnError = class extends Error {
|
|
516
|
+
code;
|
|
517
|
+
hint;
|
|
518
|
+
constructor(message6, code, opts = {}) {
|
|
519
|
+
super(message6, opts.cause === void 0 ? void 0 : { cause: opts.cause });
|
|
520
|
+
this.name = "PixelKilnError";
|
|
521
|
+
this.code = code;
|
|
522
|
+
if (opts.hint) this.hint = opts.hint;
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
var UsageError = class extends PixelKilnError {
|
|
526
|
+
constructor(message6, opts) {
|
|
527
|
+
super(message6, "usage", opts);
|
|
528
|
+
this.name = "UsageError";
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
var ProjectError = class extends PixelKilnError {
|
|
532
|
+
constructor(message6, opts) {
|
|
533
|
+
super(message6, "project", opts);
|
|
534
|
+
this.name = "ProjectError";
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
var ProviderError = class extends PixelKilnError {
|
|
538
|
+
/** Registry id of the provider that failed. */
|
|
539
|
+
provider;
|
|
540
|
+
/** HTTP status when the failure was a response, otherwise undefined. */
|
|
541
|
+
status;
|
|
542
|
+
constructor(provider, message6, opts = {}) {
|
|
543
|
+
super(message6, "provider", opts);
|
|
544
|
+
this.name = "ProviderError";
|
|
545
|
+
this.provider = provider;
|
|
546
|
+
if (opts.status !== void 0) this.status = opts.status;
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
var OverwriteRefusedError = class extends PixelKilnError {
|
|
550
|
+
/** The file(s) left untouched. */
|
|
551
|
+
files;
|
|
552
|
+
constructor(message6, files, opts) {
|
|
553
|
+
super(message6, "refused-overwrite", opts);
|
|
554
|
+
this.name = "OverwriteRefusedError";
|
|
555
|
+
this.files = files;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
var BudgetError = class extends PixelKilnError {
|
|
559
|
+
constructor(message6, opts) {
|
|
560
|
+
super(message6, "budget", opts);
|
|
561
|
+
this.name = "BudgetError";
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
function errorCode(err) {
|
|
565
|
+
return err instanceof PixelKilnError ? err.code : void 0;
|
|
566
|
+
}
|
|
567
|
+
function exitCodeFor(err) {
|
|
568
|
+
const code = errorCode(err);
|
|
569
|
+
return code ? EXIT_CODES[code] : 1;
|
|
570
|
+
}
|
|
571
|
+
|
|
477
572
|
// src/client.ts
|
|
478
573
|
import { z as z2 } from "zod";
|
|
479
574
|
|
|
@@ -590,14 +685,12 @@ function validateResponse(schema, raw, operation) {
|
|
|
590
685
|
const issues = parsed.error.issues.slice(0, 4).map((i) => `${i.path.join(".") || "response"}: ${i.message}`).join("; ");
|
|
591
686
|
throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
|
|
592
687
|
}
|
|
593
|
-
var PixelLabError = class extends
|
|
688
|
+
var PixelLabError = class extends ProviderError {
|
|
594
689
|
constructor(message6, status, body) {
|
|
595
|
-
super(message6);
|
|
596
|
-
this.status = status;
|
|
690
|
+
super("pixellab", message6, { status });
|
|
597
691
|
this.body = body;
|
|
598
692
|
this.name = "PixelLabError";
|
|
599
693
|
}
|
|
600
|
-
status;
|
|
601
694
|
body;
|
|
602
695
|
};
|
|
603
696
|
var PixelLabClient = class {
|
|
@@ -877,10 +970,11 @@ function measureBalanceChange(before, after) {
|
|
|
877
970
|
};
|
|
878
971
|
}
|
|
879
972
|
var DEFAULT_RATE_LIMIT = { spacingMs: 2500, maxInFlight: 8 };
|
|
880
|
-
var UnsupportedCapabilityError = class extends
|
|
973
|
+
var UnsupportedCapabilityError = class extends PixelKilnError {
|
|
881
974
|
constructor(providerId, capability) {
|
|
882
975
|
super(
|
|
883
|
-
`Provider "${providerId}" does not support ${capability}. That command is unavailable with this backend
|
|
976
|
+
`Provider "${providerId}" does not support ${capability}. That command is unavailable with this backend.`,
|
|
977
|
+
"capability"
|
|
884
978
|
);
|
|
885
979
|
this.name = "UnsupportedCapabilityError";
|
|
886
980
|
}
|
|
@@ -1705,7 +1799,7 @@ var RetroDiffusionClient = class {
|
|
|
1705
1799
|
/** Result URLs are signed; no token header, and the same retry policy as the API. */
|
|
1706
1800
|
async download(url) {
|
|
1707
1801
|
const response = await this.request(url);
|
|
1708
|
-
if (!response.ok) throw new
|
|
1802
|
+
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
1709
1803
|
return Buffer.from(await response.arrayBuffer());
|
|
1710
1804
|
}
|
|
1711
1805
|
async call(path32, init = {}) {
|
|
@@ -1728,8 +1822,10 @@ var RetroDiffusionClient = class {
|
|
|
1728
1822
|
if (!response.ok) {
|
|
1729
1823
|
const retry = response.headers.get("retry-after");
|
|
1730
1824
|
const detail = retroError(value);
|
|
1731
|
-
throw new
|
|
1732
|
-
|
|
1825
|
+
throw new ProviderError(
|
|
1826
|
+
"retrodiffusion",
|
|
1827
|
+
`Retro Diffusion request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : ""),
|
|
1828
|
+
{ status: response.status }
|
|
1733
1829
|
);
|
|
1734
1830
|
}
|
|
1735
1831
|
return value;
|
|
@@ -2121,7 +2217,7 @@ var ScenarioClient = class {
|
|
|
2121
2217
|
throw new Error("Scenario downloads must use HTTP or HTTPS");
|
|
2122
2218
|
}
|
|
2123
2219
|
const response = await this.request(parsed);
|
|
2124
|
-
if (!response.ok) throw new
|
|
2220
|
+
if (!response.ok) throw new ProviderError("scenario", `Scenario download failed (${response.status})`, { status: response.status });
|
|
2125
2221
|
const declared = Number(response.headers.get("content-length"));
|
|
2126
2222
|
if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) {
|
|
2127
2223
|
throw new Error(`Scenario download exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
|
|
@@ -2168,8 +2264,10 @@ var ScenarioClient = class {
|
|
|
2168
2264
|
if (!response.ok) {
|
|
2169
2265
|
const retry = response.headers.get("retry-after");
|
|
2170
2266
|
const detail = redactScenarioError(value, [this.apiKey, this.apiSecret, auth]);
|
|
2171
|
-
throw new
|
|
2172
|
-
|
|
2267
|
+
throw new ProviderError(
|
|
2268
|
+
"scenario",
|
|
2269
|
+
`Scenario request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : ""),
|
|
2270
|
+
{ status: response.status }
|
|
2173
2271
|
);
|
|
2174
2272
|
}
|
|
2175
2273
|
return value;
|
|
@@ -2619,8 +2717,10 @@ var ComfyUIClient = class {
|
|
|
2619
2717
|
value = null;
|
|
2620
2718
|
}
|
|
2621
2719
|
if (!response.ok) {
|
|
2622
|
-
throw new
|
|
2623
|
-
|
|
2720
|
+
throw new ProviderError(
|
|
2721
|
+
"comfyui",
|
|
2722
|
+
`ComfyUI image upload failed (${response.status})` + (apiError(value) ? `: ${apiError(value)}` : ""),
|
|
2723
|
+
{ status: response.status }
|
|
2624
2724
|
);
|
|
2625
2725
|
}
|
|
2626
2726
|
if (!isObject(value) || typeof value.name !== "string" || !value.name) {
|
|
@@ -2643,7 +2743,7 @@ var ComfyUIClient = class {
|
|
|
2643
2743
|
let target = source;
|
|
2644
2744
|
if (source.startsWith("comfyui://")) target = this.viewUrl(parseSource(source));
|
|
2645
2745
|
const response = await this.request(target);
|
|
2646
|
-
if (!response.ok) throw new
|
|
2746
|
+
if (!response.ok) throw new ProviderError("comfyui", `ComfyUI download failed (${response.status})`, { status: response.status });
|
|
2647
2747
|
return Buffer.from(await response.arrayBuffer());
|
|
2648
2748
|
}
|
|
2649
2749
|
endpoint(route) {
|
|
@@ -2668,8 +2768,10 @@ var ComfyUIClient = class {
|
|
|
2668
2768
|
}
|
|
2669
2769
|
if (!response.ok) {
|
|
2670
2770
|
const detail = apiError(value);
|
|
2671
|
-
throw new
|
|
2672
|
-
|
|
2771
|
+
throw new ProviderError(
|
|
2772
|
+
"comfyui",
|
|
2773
|
+
`ComfyUI request failed (${response.status}) at ${url.pathname}` + (detail ? `: ${detail}` : ""),
|
|
2774
|
+
{ status: response.status }
|
|
2673
2775
|
);
|
|
2674
2776
|
}
|
|
2675
2777
|
return value;
|
|
@@ -3659,9 +3761,10 @@ async function loadLock(lockPath) {
|
|
|
3659
3761
|
try {
|
|
3660
3762
|
return parseLock(JSON.parse(await readFile2(lockPath, "utf8")));
|
|
3661
3763
|
} catch (err) {
|
|
3662
|
-
throw new
|
|
3764
|
+
throw new ProjectError(
|
|
3663
3765
|
`Lockfile at ${lockPath} is malformed:
|
|
3664
|
-
${err instanceof Error ? err.message : String(err)}
|
|
3766
|
+
${err instanceof Error ? err.message : String(err)}`,
|
|
3767
|
+
{ cause: err }
|
|
3665
3768
|
);
|
|
3666
3769
|
}
|
|
3667
3770
|
}
|
|
@@ -3695,8 +3798,9 @@ async function writeLockWhileHeld(file, lock) {
|
|
|
3695
3798
|
try {
|
|
3696
3799
|
disk = parseLock(JSON.parse(await readFile2(file, "utf8")));
|
|
3697
3800
|
} catch (err) {
|
|
3698
|
-
throw new
|
|
3699
|
-
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}
|
|
3801
|
+
throw new ProjectError(
|
|
3802
|
+
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
3803
|
+
{ cause: err }
|
|
3700
3804
|
);
|
|
3701
3805
|
}
|
|
3702
3806
|
}
|
|
@@ -3952,18 +4056,18 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
|
3952
4056
|
// src/manifest.ts
|
|
3953
4057
|
async function loadManifest(manifestPath) {
|
|
3954
4058
|
const abs = path5.resolve(manifestPath);
|
|
3955
|
-
if (!existsSync4(abs)) throw new
|
|
4059
|
+
if (!existsSync4(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
3956
4060
|
const input = ManifestInputSchema.safeParse(JSON.parse(await readFile3(abs, "utf8")));
|
|
3957
4061
|
if (!input.success) {
|
|
3958
4062
|
const issues = formatManifestIssues(input.error.issues);
|
|
3959
|
-
throw new
|
|
4063
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
3960
4064
|
${issues}`);
|
|
3961
4065
|
}
|
|
3962
4066
|
let styles;
|
|
3963
4067
|
try {
|
|
3964
4068
|
styles = resolveStyleInheritance(input.data.styles);
|
|
3965
4069
|
} catch (error) {
|
|
3966
|
-
throw new
|
|
4070
|
+
throw new ProjectError(
|
|
3967
4071
|
`Manifest at ${abs} is invalid:
|
|
3968
4072
|
${error instanceof Error ? error.message : String(error)}`
|
|
3969
4073
|
);
|
|
@@ -3971,7 +4075,7 @@ ${issues}`);
|
|
|
3971
4075
|
const parsed = ManifestSchema.safeParse({ ...input.data, styles });
|
|
3972
4076
|
if (!parsed.success) {
|
|
3973
4077
|
const issues = formatManifestIssues(parsed.error.issues);
|
|
3974
|
-
throw new
|
|
4078
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
3975
4079
|
${issues}`);
|
|
3976
4080
|
}
|
|
3977
4081
|
const styleIds = new Set(Object.keys(parsed.data.styles));
|
|
@@ -4021,7 +4125,7 @@ ${issues}`);
|
|
|
4021
4125
|
};
|
|
4022
4126
|
for (const assetId of Object.keys(parsed.data.assets)) visitRevision(assetId, []);
|
|
4023
4127
|
if (unknownReferences.length) {
|
|
4024
|
-
throw new
|
|
4128
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
4025
4129
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
4026
4130
|
}
|
|
4027
4131
|
return { manifest: parsed.data, root: path5.dirname(abs), path: abs };
|
|
@@ -4100,14 +4204,14 @@ async function resolveSpecs(loaded, filter) {
|
|
|
4100
4204
|
);
|
|
4101
4205
|
for (const unknownStyle of filter?.styles ?? []) {
|
|
4102
4206
|
if (!manifest.styles[unknownStyle]) {
|
|
4103
|
-
throw new
|
|
4207
|
+
throw new UsageError(
|
|
4104
4208
|
`Unknown style "${unknownStyle}". Defined: ${Object.keys(manifest.styles).join(", ") || "(none)"}`
|
|
4105
4209
|
);
|
|
4106
4210
|
}
|
|
4107
4211
|
}
|
|
4108
4212
|
for (const unknownAsset of filter?.assets ?? []) {
|
|
4109
4213
|
if (!manifest.assets[unknownAsset]) {
|
|
4110
|
-
throw new
|
|
4214
|
+
throw new UsageError(`Unknown asset "${unknownAsset}".`);
|
|
4111
4215
|
}
|
|
4112
4216
|
}
|
|
4113
4217
|
const requestedAssetIds = new Set(filter?.assets?.length ? filter.assets : Object.keys(manifest.assets));
|
|
@@ -4219,6 +4323,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
4219
4323
|
detail: style.detail,
|
|
4220
4324
|
seed: style.seed,
|
|
4221
4325
|
palette: style.palette,
|
|
4326
|
+
enforcePalette: style.enforcePalette,
|
|
4222
4327
|
noBackground: style.noBackground,
|
|
4223
4328
|
tileSize: generator === "tiles" ? tileSize : void 0,
|
|
4224
4329
|
tileType: generator === "tiles" ? style.tileType : void 0,
|
|
@@ -4423,7 +4528,7 @@ function imageMetadata(buf) {
|
|
|
4423
4528
|
}
|
|
4424
4529
|
|
|
4425
4530
|
// src/project.ts
|
|
4426
|
-
import
|
|
4531
|
+
import path12 from "path";
|
|
4427
4532
|
|
|
4428
4533
|
// src/env.ts
|
|
4429
4534
|
import { readFileSync as readFileSync2, existsSync as existsSync5 } from "fs";
|
|
@@ -4470,17 +4575,17 @@ function parseEnv(contents) {
|
|
|
4470
4575
|
}
|
|
4471
4576
|
|
|
4472
4577
|
// src/pipeline/plan.ts
|
|
4473
|
-
import { existsSync as
|
|
4474
|
-
import
|
|
4578
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4579
|
+
import path11 from "path";
|
|
4475
4580
|
|
|
4476
4581
|
// src/pipeline/quality-profile.ts
|
|
4477
|
-
import { existsSync as
|
|
4478
|
-
import
|
|
4582
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4583
|
+
import path9 from "path";
|
|
4479
4584
|
|
|
4480
4585
|
// src/pipeline/refine.ts
|
|
4481
|
-
import
|
|
4586
|
+
import path8 from "path";
|
|
4482
4587
|
import { execFile } from "child_process";
|
|
4483
|
-
import { mkdtemp, readFile as
|
|
4588
|
+
import { mkdtemp, readFile as readFile5, rm as rm3 } from "fs/promises";
|
|
4484
4589
|
import { tmpdir } from "os";
|
|
4485
4590
|
import { promisify } from "util";
|
|
4486
4591
|
|
|
@@ -4718,10 +4823,11 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
4718
4823
|
if (!expected || digest(current) !== expected) conflicts.push(destination);
|
|
4719
4824
|
}
|
|
4720
4825
|
if (conflicts.length) {
|
|
4721
|
-
throw new
|
|
4826
|
+
throw new OverwriteRefusedError(
|
|
4722
4827
|
`Refusing to overwrite modified or unowned artifact file(s):
|
|
4723
4828
|
` + conflicts.map((file) => ` ${file}`).join("\n") + `
|
|
4724
|
-
Pass { force: true } (CLI: --force) to take ownership and replace the complete bundle
|
|
4829
|
+
Pass { force: true } (CLI: --force) to take ownership and replace the complete bundle.`,
|
|
4830
|
+
conflicts
|
|
4725
4831
|
);
|
|
4726
4832
|
}
|
|
4727
4833
|
}
|
|
@@ -4940,10 +5046,19 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
4940
5046
|
};
|
|
4941
5047
|
}
|
|
4942
5048
|
|
|
4943
|
-
// src/
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
5049
|
+
// src/palette.ts
|
|
5050
|
+
function normalizePalette(values) {
|
|
5051
|
+
const colors = /* @__PURE__ */ new Map();
|
|
5052
|
+
for (const value of values) {
|
|
5053
|
+
const rgb = parseHex(value);
|
|
5054
|
+
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
5055
|
+
if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
|
|
5056
|
+
}
|
|
5057
|
+
if (colors.size < 2 || colors.size > 256) {
|
|
5058
|
+
throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
5059
|
+
}
|
|
5060
|
+
return [...colors.values()];
|
|
5061
|
+
}
|
|
4947
5062
|
function colorDistance(a, b) {
|
|
4948
5063
|
const rmean = (a.r + b.r) / 2;
|
|
4949
5064
|
const dr = a.r - b.r;
|
|
@@ -4953,146 +5068,45 @@ function colorDistance(a, b) {
|
|
|
4953
5068
|
(512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
|
|
4954
5069
|
);
|
|
4955
5070
|
}
|
|
4956
|
-
function
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
const totals = /* @__PURE__ */ new Map();
|
|
4970
|
-
for (const palette of palettes) {
|
|
4971
|
-
for (const color of palette) {
|
|
4972
|
-
const key = `${color.r},${color.g},${color.b}`;
|
|
4973
|
-
const existing = totals.get(key);
|
|
4974
|
-
if (existing) existing.weight += color.weight;
|
|
4975
|
-
else totals.set(key, { ...color });
|
|
5071
|
+
function quantize(png, palette) {
|
|
5072
|
+
const colors = normalizePalette(palette);
|
|
5073
|
+
const pixels = Buffer.from(png.pixels);
|
|
5074
|
+
const nearestOf = /* @__PURE__ */ new Map();
|
|
5075
|
+
let changed = 0;
|
|
5076
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
5077
|
+
const alpha = pixels[i + 3];
|
|
5078
|
+
if (alpha === 0) {
|
|
5079
|
+
if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
|
|
5080
|
+
pixels[i] = 0;
|
|
5081
|
+
pixels[i + 1] = 0;
|
|
5082
|
+
pixels[i + 2] = 0;
|
|
5083
|
+
continue;
|
|
4976
5084
|
}
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
4991
|
-
if (!existsSync6(output.absolutePath)) {
|
|
4992
|
-
missing.push(output.id);
|
|
4993
|
-
continue;
|
|
4994
|
-
}
|
|
4995
|
-
try {
|
|
4996
|
-
const png = decodePng(await readFile5(output.absolutePath));
|
|
4997
|
-
const palette = extractPalette(png, 12);
|
|
4998
|
-
assets.push({
|
|
4999
|
-
assetId: spec.assetId,
|
|
5000
|
-
...output.role ? { outputRole: output.role } : {},
|
|
5001
|
-
id: output.id,
|
|
5002
|
-
file: output.absolutePath,
|
|
5003
|
-
width: png.width,
|
|
5004
|
-
height: png.height,
|
|
5005
|
-
paletteDistance: 0,
|
|
5006
|
-
// filled once the reference is known
|
|
5007
|
-
transparency: transparencyRatio(png),
|
|
5008
|
-
colorCount: countColors(png),
|
|
5009
|
-
palette
|
|
5010
|
-
});
|
|
5011
|
-
} catch (err) {
|
|
5012
|
-
unreadable.push(`${output.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5085
|
+
const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
|
|
5086
|
+
let nearest = nearestOf.get(key);
|
|
5087
|
+
if (!nearest) {
|
|
5088
|
+
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
5089
|
+
nearest = colors[0];
|
|
5090
|
+
let distance = colorDistance(pixel, nearest);
|
|
5091
|
+
for (let j = 1; j < colors.length; j++) {
|
|
5092
|
+
const candidate = colors[j];
|
|
5093
|
+
const candidateDistance = colorDistance(pixel, candidate);
|
|
5094
|
+
if (candidateDistance < distance) {
|
|
5095
|
+
nearest = candidate;
|
|
5096
|
+
distance = candidateDistance;
|
|
5097
|
+
}
|
|
5013
5098
|
}
|
|
5099
|
+
nearestOf.set(key, nearest);
|
|
5014
5100
|
}
|
|
5101
|
+
if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
|
|
5102
|
+
pixels[i] = nearest.r;
|
|
5103
|
+
pixels[i + 1] = nearest.g;
|
|
5104
|
+
pixels[i + 2] = nearest.b;
|
|
5015
5105
|
}
|
|
5016
|
-
|
|
5017
|
-
let referenceFromStyleImages = false;
|
|
5018
|
-
const refPalettes = [];
|
|
5019
|
-
for (const img of style.styleImages) {
|
|
5020
|
-
const abs = path8.resolve(loaded.root, img.path);
|
|
5021
|
-
if (!existsSync6(abs)) continue;
|
|
5022
|
-
try {
|
|
5023
|
-
refPalettes.push(extractPalette(decodePng(await readFile5(abs)), 12));
|
|
5024
|
-
} catch {
|
|
5025
|
-
}
|
|
5026
|
-
}
|
|
5027
|
-
if (refPalettes.length) {
|
|
5028
|
-
reference = mergePalettes(refPalettes);
|
|
5029
|
-
referenceFromStyleImages = true;
|
|
5030
|
-
for (const asset of assets) {
|
|
5031
|
-
asset.paletteDistance = paletteDistance(asset.palette, reference);
|
|
5032
|
-
}
|
|
5033
|
-
} else {
|
|
5034
|
-
reference = mergePalettes(assets.map((a) => a.palette));
|
|
5035
|
-
for (const asset of assets) {
|
|
5036
|
-
const others = assets.filter((a) => a !== asset).map((a) => a.palette);
|
|
5037
|
-
asset.paletteDistance = others.length ? paletteDistance(asset.palette, mergePalettes(others)) : 0;
|
|
5038
|
-
}
|
|
5039
|
-
}
|
|
5040
|
-
assets.sort((a, b) => b.paletteDistance - a.paletteDistance);
|
|
5041
|
-
return { styleId, referenceFromStyleImages, reference, assets, missing, unreadable };
|
|
5042
|
-
}
|
|
5043
|
-
function countColors(png) {
|
|
5044
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5045
|
-
for (let i = 0; i < png.pixels.length; i += 4) {
|
|
5046
|
-
if (png.pixels[i + 3] < 128) continue;
|
|
5047
|
-
seen.add(png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2]);
|
|
5048
|
-
}
|
|
5049
|
-
return seen.size;
|
|
5106
|
+
return { png: { width: png.width, height: png.height, pixels }, changed };
|
|
5050
5107
|
}
|
|
5051
|
-
function
|
|
5052
|
-
|
|
5053
|
-
if (n < 3) return [];
|
|
5054
|
-
const mean = audit.assets.reduce((s, a) => s + a.paletteDistance, 0) / n;
|
|
5055
|
-
const variance = audit.assets.reduce((s, a) => s + (a.paletteDistance - mean) ** 2, 0) / n;
|
|
5056
|
-
const sd = Math.sqrt(variance);
|
|
5057
|
-
if (sd === 0) return [];
|
|
5058
|
-
return audit.assets.filter((a) => a.paletteDistance > mean + sigma * sd);
|
|
5059
|
-
}
|
|
5060
|
-
function evaluateAudit(audit, thresholds = {}) {
|
|
5061
|
-
const resolved = { ...thresholds, sigma: thresholds.sigma ?? 1.5 };
|
|
5062
|
-
const relative = outliers(audit, resolved.sigma);
|
|
5063
|
-
const relativeIds = new Set(relative.map((asset) => asset.id));
|
|
5064
|
-
const violations = [];
|
|
5065
|
-
for (const asset of audit.assets) {
|
|
5066
|
-
const reasons = [];
|
|
5067
|
-
if (relativeIds.has(asset.id)) {
|
|
5068
|
-
reasons.push(`palette outlier beyond ${resolved.sigma} standard deviations`);
|
|
5069
|
-
}
|
|
5070
|
-
if (resolved.maxDistance !== void 0 && asset.paletteDistance > resolved.maxDistance) {
|
|
5071
|
-
reasons.push(
|
|
5072
|
-
`palette distance ${asset.paletteDistance.toFixed(1)} exceeds ${resolved.maxDistance}`
|
|
5073
|
-
);
|
|
5074
|
-
}
|
|
5075
|
-
if (resolved.minTransparency !== void 0 && asset.transparency < resolved.minTransparency) {
|
|
5076
|
-
reasons.push(
|
|
5077
|
-
`transparency ${(asset.transparency * 100).toFixed(1)}% is below ${(resolved.minTransparency * 100).toFixed(1)}%`
|
|
5078
|
-
);
|
|
5079
|
-
}
|
|
5080
|
-
if (resolved.maxColors !== void 0 && asset.colorCount > resolved.maxColors) {
|
|
5081
|
-
reasons.push(`color count ${asset.colorCount} exceeds ${resolved.maxColors}`);
|
|
5082
|
-
}
|
|
5083
|
-
if (reasons.length) violations.push({ id: asset.id, reasons });
|
|
5084
|
-
}
|
|
5085
|
-
return {
|
|
5086
|
-
safe: violations.length === 0 && audit.missing.length === 0 && audit.unreadable.length === 0,
|
|
5087
|
-
thresholds: resolved,
|
|
5088
|
-
outliers: [...relativeIds],
|
|
5089
|
-
violations,
|
|
5090
|
-
missing: [...audit.missing],
|
|
5091
|
-
unreadable: [...audit.unreadable]
|
|
5092
|
-
};
|
|
5093
|
-
}
|
|
5094
|
-
function hex(c) {
|
|
5095
|
-
return `#${[c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
5108
|
+
function quantizeToPalette(png, palette) {
|
|
5109
|
+
return quantize(png, palette).png;
|
|
5096
5110
|
}
|
|
5097
5111
|
|
|
5098
5112
|
// src/pipeline/refine.ts
|
|
@@ -5179,48 +5193,12 @@ function parseDetection(raw) {
|
|
|
5179
5193
|
confidence: confidenceFor(consensus)
|
|
5180
5194
|
};
|
|
5181
5195
|
}
|
|
5182
|
-
function normalizePalette(values) {
|
|
5183
|
-
const colors = /* @__PURE__ */ new Map();
|
|
5184
|
-
for (const value of values) {
|
|
5185
|
-
const rgb = parseHex(value);
|
|
5186
|
-
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
5187
|
-
colors.set(hex2, { hex: hex2, ...rgb });
|
|
5188
|
-
}
|
|
5189
|
-
if (colors.size < 2 || colors.size > 256) {
|
|
5190
|
-
throw new Error(`A refinement palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
5191
|
-
}
|
|
5192
|
-
return [...colors.values()];
|
|
5193
|
-
}
|
|
5194
5196
|
function normalizeRefinementPalette(values) {
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
const pixels = Buffer.from(png.pixels);
|
|
5200
|
-
for (let i = 0; i < pixels.length; i += 4) {
|
|
5201
|
-
const alpha = pixels[i + 3];
|
|
5202
|
-
if (alpha === 0) {
|
|
5203
|
-
pixels[i] = 0;
|
|
5204
|
-
pixels[i + 1] = 0;
|
|
5205
|
-
pixels[i + 2] = 0;
|
|
5206
|
-
continue;
|
|
5207
|
-
}
|
|
5208
|
-
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
5209
|
-
let nearest = colors[0];
|
|
5210
|
-
let distance = colorDistance(pixel, nearest);
|
|
5211
|
-
for (let j = 1; j < colors.length; j++) {
|
|
5212
|
-
const candidate = colors[j];
|
|
5213
|
-
const candidateDistance = colorDistance(pixel, candidate);
|
|
5214
|
-
if (candidateDistance < distance) {
|
|
5215
|
-
nearest = candidate;
|
|
5216
|
-
distance = candidateDistance;
|
|
5217
|
-
}
|
|
5218
|
-
}
|
|
5219
|
-
pixels[i] = nearest.r;
|
|
5220
|
-
pixels[i + 1] = nearest.g;
|
|
5221
|
-
pixels[i + 2] = nearest.b;
|
|
5197
|
+
try {
|
|
5198
|
+
return normalizePalette(values).map((color) => color.hex);
|
|
5199
|
+
} catch (err) {
|
|
5200
|
+
throw new Error(`A refinement palette must contain 2\u2013256 unique colors; got ${new Set(values.map((v) => v.toLowerCase())).size}.`, { cause: err });
|
|
5222
5201
|
}
|
|
5223
|
-
return { width: png.width, height: png.height, pixels };
|
|
5224
5202
|
}
|
|
5225
5203
|
function colorCount(png) {
|
|
5226
5204
|
const colors = /* @__PURE__ */ new Set();
|
|
@@ -5330,16 +5308,16 @@ function sameFrameGrid(first, next) {
|
|
|
5330
5308
|
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;
|
|
5331
5309
|
}
|
|
5332
5310
|
async function refineAsset(options) {
|
|
5333
|
-
const source =
|
|
5334
|
-
const output =
|
|
5311
|
+
const source = path8.resolve(options.source);
|
|
5312
|
+
const output = path8.resolve(options.output);
|
|
5335
5313
|
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
5336
|
-
const record =
|
|
5314
|
+
const record = path8.resolve(qualityRecordPath(output));
|
|
5337
5315
|
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
5338
5316
|
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
5339
5317
|
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.");
|
|
5340
5318
|
let sourcePng;
|
|
5341
5319
|
try {
|
|
5342
|
-
sourcePng = decodePng(await
|
|
5320
|
+
sourcePng = decodePng(await readFile5(source));
|
|
5343
5321
|
} catch (error) {
|
|
5344
5322
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
5345
5323
|
}
|
|
@@ -5356,8 +5334,8 @@ async function refineAsset(options) {
|
|
|
5356
5334
|
);
|
|
5357
5335
|
}
|
|
5358
5336
|
}
|
|
5359
|
-
const temp = await mkdtemp(
|
|
5360
|
-
const recovered =
|
|
5337
|
+
const temp = await mkdtemp(path8.join(tmpdir(), "pixelkiln-refine-"));
|
|
5338
|
+
const recovered = path8.join(temp, "native.png");
|
|
5361
5339
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
5362
5340
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
5363
5341
|
try {
|
|
@@ -5378,7 +5356,7 @@ async function refineAsset(options) {
|
|
|
5378
5356
|
);
|
|
5379
5357
|
}
|
|
5380
5358
|
const detection = parseDetection(firstJsonObject(stdout));
|
|
5381
|
-
const native = decodePng(await
|
|
5359
|
+
const native = decodePng(await readFile5(recovered));
|
|
5382
5360
|
const quantized = quantizeToPalette(native, palette);
|
|
5383
5361
|
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
5384
5362
|
const audit = auditRefinement(
|
|
@@ -5442,18 +5420,18 @@ async function refineFrameSet(options) {
|
|
|
5442
5420
|
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
5443
5421
|
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
5444
5422
|
}
|
|
5445
|
-
const output =
|
|
5446
|
-
const record =
|
|
5423
|
+
const output = path8.resolve(options.output);
|
|
5424
|
+
const record = path8.resolve(qualityRecordPath(output));
|
|
5447
5425
|
const palette = normalizeRefinementPalette(options.palette);
|
|
5448
5426
|
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
5449
5427
|
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
5450
5428
|
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
5451
5429
|
}
|
|
5452
|
-
const sourceSet = new Set(options.sources.map((source) =>
|
|
5430
|
+
const sourceSet = new Set(options.sources.map((source) => path8.resolve(source.path)));
|
|
5453
5431
|
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
5454
5432
|
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
5455
5433
|
}
|
|
5456
|
-
const temp = await mkdtemp(
|
|
5434
|
+
const temp = await mkdtemp(path8.join(tmpdir(), "pixelkiln-refine-frames-"));
|
|
5457
5435
|
try {
|
|
5458
5436
|
const frames = [];
|
|
5459
5437
|
const outputs = [];
|
|
@@ -5461,8 +5439,8 @@ async function refineFrameSet(options) {
|
|
|
5461
5439
|
const sourcePaths = [];
|
|
5462
5440
|
let firstOptions = null;
|
|
5463
5441
|
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
5464
|
-
const source =
|
|
5465
|
-
const temporaryOutput =
|
|
5442
|
+
const source = path8.resolve(sourceEntry.path);
|
|
5443
|
+
const temporaryOutput = path8.join(temp, `${String(index).padStart(2, "0")}.png`);
|
|
5466
5444
|
const refined = await refineAsset({
|
|
5467
5445
|
source,
|
|
5468
5446
|
output: temporaryOutput,
|
|
@@ -5494,7 +5472,7 @@ async function refineFrameSet(options) {
|
|
|
5494
5472
|
});
|
|
5495
5473
|
outputs.push({
|
|
5496
5474
|
path: finalOutputs[index],
|
|
5497
|
-
data: await
|
|
5475
|
+
data: await readFile5(temporaryOutput)
|
|
5498
5476
|
});
|
|
5499
5477
|
}
|
|
5500
5478
|
const first = frames[0];
|
|
@@ -5537,7 +5515,7 @@ async function refineFrameSet(options) {
|
|
|
5537
5515
|
);
|
|
5538
5516
|
return {
|
|
5539
5517
|
sources: sourcePaths,
|
|
5540
|
-
outputs: outputs.map((item) =>
|
|
5518
|
+
outputs: outputs.map((item) => path8.resolve(item.path)),
|
|
5541
5519
|
record,
|
|
5542
5520
|
frames,
|
|
5543
5521
|
palette,
|
|
@@ -5548,12 +5526,12 @@ async function refineFrameSet(options) {
|
|
|
5548
5526
|
}
|
|
5549
5527
|
}
|
|
5550
5528
|
async function checkQualityRecord(recordPath) {
|
|
5551
|
-
const record =
|
|
5529
|
+
const record = path8.resolve(recordPath);
|
|
5552
5530
|
const manifest = await readArtifactBundleManifest(record);
|
|
5553
5531
|
const options = parseRecordOptions(manifest, record);
|
|
5554
5532
|
const verification = await verifyArtifactBundle(record);
|
|
5555
|
-
const sources = manifest.sources.map((source2) =>
|
|
5556
|
-
const outputs = manifest.outputs.map((output2) =>
|
|
5533
|
+
const sources = manifest.sources.map((source2) => path8.resolve(path8.dirname(record), source2.path));
|
|
5534
|
+
const outputs = manifest.outputs.map((output2) => path8.resolve(path8.dirname(record), output2.path));
|
|
5557
5535
|
const source = sources[0] ?? "";
|
|
5558
5536
|
const output = outputs[0] ?? "";
|
|
5559
5537
|
const expectedCount = options.frameSet?.count ?? 1;
|
|
@@ -5586,7 +5564,7 @@ async function checkQualityRecord(recordPath) {
|
|
|
5586
5564
|
async function approveQualityRecord(recordPath, approval) {
|
|
5587
5565
|
const reviewer = approval.reviewer.trim();
|
|
5588
5566
|
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
5589
|
-
const record =
|
|
5567
|
+
const record = path8.resolve(recordPath);
|
|
5590
5568
|
const manifest = await readArtifactBundleManifest(record);
|
|
5591
5569
|
const options = parseRecordOptions(manifest, record);
|
|
5592
5570
|
const verification = await verifyArtifactBundle(record);
|
|
@@ -5607,14 +5585,14 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5607
5585
|
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
5608
5586
|
);
|
|
5609
5587
|
}
|
|
5610
|
-
const root =
|
|
5588
|
+
const root = path8.dirname(record);
|
|
5611
5589
|
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
5612
|
-
const outputPath =
|
|
5613
|
-
return { path: outputPath, data: await
|
|
5590
|
+
const outputPath = path8.resolve(root, entry.path);
|
|
5591
|
+
return { path: outputPath, data: await readFile5(outputPath) };
|
|
5614
5592
|
}));
|
|
5615
5593
|
const sources = manifest.sources.map((source) => ({
|
|
5616
5594
|
...source,
|
|
5617
|
-
path:
|
|
5595
|
+
path: path8.resolve(root, source.path)
|
|
5618
5596
|
}));
|
|
5619
5597
|
const approved = {
|
|
5620
5598
|
...options,
|
|
@@ -5646,8 +5624,8 @@ async function qualitySource(spec, lock) {
|
|
|
5646
5624
|
if (spec.generator === "frames") {
|
|
5647
5625
|
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
5648
5626
|
}
|
|
5649
|
-
const source =
|
|
5650
|
-
return
|
|
5627
|
+
const source = path9.resolve(spec.root, spec.source);
|
|
5628
|
+
return existsSync6(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
5651
5629
|
}
|
|
5652
5630
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
5653
5631
|
if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
|
|
@@ -5676,7 +5654,7 @@ async function qualitySource(spec, lock) {
|
|
|
5676
5654
|
}
|
|
5677
5655
|
if (role) roles.add(role);
|
|
5678
5656
|
const source = currentEntryOutputPath(entry, spec, index);
|
|
5679
|
-
if (!
|
|
5657
|
+
if (!existsSync6(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
|
|
5680
5658
|
if (await sha256File(source) !== output.sha256) {
|
|
5681
5659
|
return { items: null, reason: "raw provider output was modified after download" };
|
|
5682
5660
|
}
|
|
@@ -5695,7 +5673,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5695
5673
|
}
|
|
5696
5674
|
const sourcePaths = source.items.map((item) => item.path);
|
|
5697
5675
|
const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
|
|
5698
|
-
if (!
|
|
5676
|
+
if (!existsSync6(record)) {
|
|
5699
5677
|
return {
|
|
5700
5678
|
key,
|
|
5701
5679
|
state: "needs-refinement",
|
|
@@ -5713,7 +5691,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5713
5691
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
5714
5692
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
5715
5693
|
const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
|
|
5716
|
-
const contractChanged = JSON.stringify(check.sources.map((item) =>
|
|
5694
|
+
const contractChanged = JSON.stringify(check.sources.map((item) => path9.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path9.resolve(item))) || JSON.stringify(check.outputs.map((item) => path9.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path9.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);
|
|
5717
5695
|
if (contractChanged) {
|
|
5718
5696
|
return {
|
|
5719
5697
|
key,
|
|
@@ -5875,9 +5853,38 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
|
|
|
5875
5853
|
return sources;
|
|
5876
5854
|
}
|
|
5877
5855
|
|
|
5856
|
+
// src/pipeline/postprocess.ts
|
|
5857
|
+
function postprocessFor(spec) {
|
|
5858
|
+
if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
|
|
5859
|
+
return {
|
|
5860
|
+
palette: {
|
|
5861
|
+
colors: normalizePalette(spec.palette).map((color) => color.hex),
|
|
5862
|
+
dither: "none",
|
|
5863
|
+
distance: "redmean"
|
|
5864
|
+
}
|
|
5865
|
+
};
|
|
5866
|
+
}
|
|
5867
|
+
function samePostprocess(a, b) {
|
|
5868
|
+
const pa = a?.palette;
|
|
5869
|
+
const pb = b?.palette;
|
|
5870
|
+
if (!pa && !pb) return true;
|
|
5871
|
+
if (!pa || !pb) return false;
|
|
5872
|
+
return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
|
|
5873
|
+
}
|
|
5874
|
+
function postprocessCurrent(entry, spec) {
|
|
5875
|
+
return samePostprocess(entry.postprocess, postprocessFor(spec));
|
|
5876
|
+
}
|
|
5877
|
+
function applyPostprocess(bytes, mediaType, record) {
|
|
5878
|
+
if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
|
|
5879
|
+
const png = decodePng(bytes);
|
|
5880
|
+
const { png: snapped, changed } = quantize(png, record.palette.colors);
|
|
5881
|
+
if (!changed) return { bytes, changed: false };
|
|
5882
|
+
return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
|
|
5883
|
+
}
|
|
5884
|
+
|
|
5878
5885
|
// src/pipeline/revision.ts
|
|
5879
|
-
import { existsSync as
|
|
5880
|
-
import
|
|
5886
|
+
import { existsSync as existsSync7 } from "fs";
|
|
5887
|
+
import path10 from "path";
|
|
5881
5888
|
async function inspectRevisionReadiness(spec, lock) {
|
|
5882
5889
|
if (!spec.revision) return null;
|
|
5883
5890
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
@@ -5892,14 +5899,14 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
5892
5899
|
reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
|
|
5893
5900
|
};
|
|
5894
5901
|
}
|
|
5895
|
-
if (!revision.sourceSha256 || !
|
|
5902
|
+
if (!revision.sourceSha256 || !existsSync7(revision.sourceFile)) {
|
|
5896
5903
|
return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
|
|
5897
5904
|
}
|
|
5898
5905
|
if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
|
|
5899
5906
|
return { ready: false, reason: "revision source changed after the manifest was resolved" };
|
|
5900
5907
|
}
|
|
5901
5908
|
if (revision.maskFile) {
|
|
5902
|
-
if (!revision.maskSha256 || !
|
|
5909
|
+
if (!revision.maskSha256 || !existsSync7(revision.maskFile)) {
|
|
5903
5910
|
return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
|
|
5904
5911
|
}
|
|
5905
5912
|
if (await sha256File(revision.maskFile) !== revision.maskSha256) {
|
|
@@ -5929,8 +5936,8 @@ async function inspectParent(spec, lock, seen) {
|
|
|
5929
5936
|
};
|
|
5930
5937
|
}
|
|
5931
5938
|
if (spec.source) {
|
|
5932
|
-
const source =
|
|
5933
|
-
return
|
|
5939
|
+
const source = path10.resolve(spec.root, spec.source);
|
|
5940
|
+
return existsSync7(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
|
|
5934
5941
|
}
|
|
5935
5942
|
const entry = lock.entries[key];
|
|
5936
5943
|
if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
|
|
@@ -5944,7 +5951,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
5944
5951
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
5945
5952
|
const output = entry.outputs[index];
|
|
5946
5953
|
const file = currentEntryOutputPath(entry, spec, index);
|
|
5947
|
-
if (!
|
|
5954
|
+
if (!existsSync7(file)) return { ready: false, reason: `parent output is missing: ${file}` };
|
|
5948
5955
|
if (await sha256File(file) !== output.sha256) {
|
|
5949
5956
|
return { ready: false, reason: `parent output was modified: ${file}` };
|
|
5950
5957
|
}
|
|
@@ -5973,7 +5980,7 @@ function resumeActions(specs, lock) {
|
|
|
5973
5980
|
const entry = lock.entries[key];
|
|
5974
5981
|
if (!entry || entry.specHash !== spec.specHash) continue;
|
|
5975
5982
|
if (entry.submissionComplete === false) continue;
|
|
5976
|
-
const command = resumeCommandForStatus(entry.status);
|
|
5983
|
+
const command = resumeCommandForStatus(entry.status) ?? (entry.status === "downloaded" && entry.outputs.length && !postprocessCurrent(entry, spec) ? "fetch" : null);
|
|
5977
5984
|
if (!command) continue;
|
|
5978
5985
|
if (command === "poll" && !entry.jobId) continue;
|
|
5979
5986
|
if (command === "pick" && !entry.reviewObjectId) continue;
|
|
@@ -5999,7 +6006,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
5999
6006
|
state = "missing";
|
|
6000
6007
|
reason = "--force";
|
|
6001
6008
|
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
6002
|
-
if (
|
|
6009
|
+
if (existsSync8(path11.resolve(spec.root, spec.source))) {
|
|
6003
6010
|
state = "ok";
|
|
6004
6011
|
reason = `placed from ${spec.source}; not generated`;
|
|
6005
6012
|
} else {
|
|
@@ -6007,7 +6014,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6007
6014
|
reason = `declared source is missing: ${spec.source}`;
|
|
6008
6015
|
}
|
|
6009
6016
|
} else if (!entry) {
|
|
6010
|
-
if (
|
|
6017
|
+
if (existsSync8(spec.outFile)) {
|
|
6011
6018
|
state = "untracked";
|
|
6012
6019
|
reason = "file on disk, no provenance recorded";
|
|
6013
6020
|
} else {
|
|
@@ -6039,12 +6046,15 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6039
6046
|
} else if (entry.status !== "downloaded") {
|
|
6040
6047
|
state = "in-flight";
|
|
6041
6048
|
reason = `awaiting ${entry.status}`;
|
|
6042
|
-
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !
|
|
6049
|
+
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !existsSync8(currentEntryOutputPath(entry, spec, index)))) {
|
|
6043
6050
|
state = "orphaned";
|
|
6044
|
-
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !
|
|
6051
|
+
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !existsSync8(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
|
|
6045
6052
|
} else if (await anyOutputModified(entry, spec)) {
|
|
6046
6053
|
state = "orphaned";
|
|
6047
6054
|
reason = "output modified since download";
|
|
6055
|
+
} else if (!postprocessCurrent(entry, spec)) {
|
|
6056
|
+
state = "recoverable";
|
|
6057
|
+
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)";
|
|
6048
6058
|
} else {
|
|
6049
6059
|
state = "ok";
|
|
6050
6060
|
reason = "up to date";
|
|
@@ -6107,13 +6117,13 @@ function summarize(plan) {
|
|
|
6107
6117
|
|
|
6108
6118
|
// src/project.ts
|
|
6109
6119
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
6110
|
-
const absolute =
|
|
6111
|
-
const root =
|
|
6120
|
+
const absolute = path12.resolve(manifestPath);
|
|
6121
|
+
const root = path12.dirname(absolute);
|
|
6112
6122
|
if (opts.env !== false) {
|
|
6113
6123
|
loadEnvFiles(root);
|
|
6114
|
-
if (
|
|
6124
|
+
if (path12.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
6115
6125
|
}
|
|
6116
|
-
const lockPath =
|
|
6126
|
+
const lockPath = path12.resolve(opts.lockPath ?? path12.join(root, "pixelkiln.lock.json"));
|
|
6117
6127
|
const loaded = await loadManifest(absolute);
|
|
6118
6128
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
6119
6129
|
const lock = await loadLock(lockPath);
|
|
@@ -6180,9 +6190,9 @@ function shouldPersistSourceUrl(value) {
|
|
|
6180
6190
|
}
|
|
6181
6191
|
|
|
6182
6192
|
// src/recipes.ts
|
|
6183
|
-
import
|
|
6184
|
-
import { existsSync as
|
|
6185
|
-
import { readdir, readFile as
|
|
6193
|
+
import path13 from "path";
|
|
6194
|
+
import { existsSync as existsSync9 } from "fs";
|
|
6195
|
+
import { readdir, readFile as readFile6 } from "fs/promises";
|
|
6186
6196
|
import { fileURLToPath } from "url";
|
|
6187
6197
|
import { z as z3 } from "zod";
|
|
6188
6198
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
@@ -6191,7 +6201,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
6191
6201
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
6192
6202
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
6193
6203
|
var PortablePathSchema = z3.string().min(1).refine((value) => {
|
|
6194
|
-
if (value.includes("\\") ||
|
|
6204
|
+
if (value.includes("\\") || path13.posix.isAbsolute(value)) return false;
|
|
6195
6205
|
const parts = value.split("/");
|
|
6196
6206
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
6197
6207
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -6367,10 +6377,10 @@ function message3(error) {
|
|
|
6367
6377
|
return error instanceof Error ? error.message : String(error);
|
|
6368
6378
|
}
|
|
6369
6379
|
async function readRecipeFile(recipePath, bundled) {
|
|
6370
|
-
const absolute =
|
|
6380
|
+
const absolute = path13.resolve(recipePath);
|
|
6371
6381
|
let raw;
|
|
6372
6382
|
try {
|
|
6373
|
-
raw = JSON.parse(await
|
|
6383
|
+
raw = JSON.parse(await readFile6(absolute, "utf8"));
|
|
6374
6384
|
} catch (error) {
|
|
6375
6385
|
throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
|
|
6376
6386
|
}
|
|
@@ -6381,13 +6391,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
6381
6391
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
6382
6392
|
);
|
|
6383
6393
|
}
|
|
6384
|
-
return { recipe: parsed.data, path: absolute, dir:
|
|
6394
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path13.dirname(absolute), bundled };
|
|
6385
6395
|
}
|
|
6386
6396
|
async function walkRecipeFiles(root) {
|
|
6387
|
-
if (!
|
|
6397
|
+
if (!existsSync9(root)) return [];
|
|
6388
6398
|
const out = [];
|
|
6389
6399
|
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
6390
|
-
const resolved =
|
|
6400
|
+
const resolved = path13.join(root, entry.name);
|
|
6391
6401
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
6392
6402
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
6393
6403
|
}
|
|
@@ -6398,7 +6408,7 @@ function bundledRecipeRoot() {
|
|
|
6398
6408
|
}
|
|
6399
6409
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
6400
6410
|
const recipes = await Promise.all(
|
|
6401
|
-
(await walkRecipeFiles(
|
|
6411
|
+
(await walkRecipeFiles(path13.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
6402
6412
|
);
|
|
6403
6413
|
return recipes.sort(
|
|
6404
6414
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -6421,9 +6431,9 @@ function parseSelector(selector) {
|
|
|
6421
6431
|
return { id: match[1], version: match[2] };
|
|
6422
6432
|
}
|
|
6423
6433
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
6424
|
-
const local =
|
|
6425
|
-
if (
|
|
6426
|
-
const recipePath =
|
|
6434
|
+
const local = path13.resolve(target);
|
|
6435
|
+
if (existsSync9(local)) {
|
|
6436
|
+
const recipePath = path13.basename(local) === RECIPE_FILE ? local : path13.join(local, RECIPE_FILE);
|
|
6427
6437
|
return readRecipeFile(recipePath, false);
|
|
6428
6438
|
}
|
|
6429
6439
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -6437,8 +6447,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
6437
6447
|
return matches[0];
|
|
6438
6448
|
}
|
|
6439
6449
|
async function verifyFile(root, file) {
|
|
6440
|
-
const absolute =
|
|
6441
|
-
if (!
|
|
6450
|
+
const absolute = path13.resolve(root, ...file.path.split("/"));
|
|
6451
|
+
if (!existsSync9(absolute)) {
|
|
6442
6452
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
6443
6453
|
}
|
|
6444
6454
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -6451,9 +6461,9 @@ async function verifyFile(root, file) {
|
|
|
6451
6461
|
}
|
|
6452
6462
|
async function verifyRecipe(target, options = {}) {
|
|
6453
6463
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
6454
|
-
const actualIntegrity = recipeDigest(loaded.
|
|
6464
|
+
const actualIntegrity = recipeDigest(loaded.authored);
|
|
6455
6465
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
6456
|
-
const modelRoot = options.modelRoot ?
|
|
6466
|
+
const modelRoot = options.modelRoot ? path13.resolve(options.modelRoot) : null;
|
|
6457
6467
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
6458
6468
|
...await verifyFile(modelRoot, model),
|
|
6459
6469
|
source: model.source,
|
|
@@ -6487,7 +6497,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
6487
6497
|
};
|
|
6488
6498
|
}
|
|
6489
6499
|
function renderStyle(style, destination, cwd) {
|
|
6490
|
-
const portableDir =
|
|
6500
|
+
const portableDir = path13.relative(cwd, destination).split(path13.sep).join("/") || ".";
|
|
6491
6501
|
const replace = (value) => {
|
|
6492
6502
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
6493
6503
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -6499,28 +6509,29 @@ function renderStyle(style, destination, cwd) {
|
|
|
6499
6509
|
return replace(style);
|
|
6500
6510
|
}
|
|
6501
6511
|
async function installRecipe(target, options = {}) {
|
|
6502
|
-
const cwd =
|
|
6512
|
+
const cwd = path13.resolve(options.cwd ?? process.cwd());
|
|
6503
6513
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
6504
6514
|
const verification = await verifyRecipe(loaded.path);
|
|
6505
6515
|
if (!verification.ok) {
|
|
6506
6516
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
6507
6517
|
}
|
|
6508
|
-
const destination = options.out ?
|
|
6518
|
+
const destination = options.out ? path13.resolve(cwd, options.out) : path13.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
6509
6519
|
const sources = [
|
|
6510
6520
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
6511
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
6521
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path13.join(loaded.dir, ...file.path.split("/")) }))
|
|
6512
6522
|
];
|
|
6513
6523
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
6514
|
-
path:
|
|
6515
|
-
data: await
|
|
6524
|
+
path: path13.join(destination, ...file.path.split("/")),
|
|
6525
|
+
data: await readFile6(file.source)
|
|
6516
6526
|
})));
|
|
6517
6527
|
if (!options.force) {
|
|
6518
6528
|
for (const file of files) {
|
|
6519
|
-
if (!
|
|
6520
|
-
const current = await
|
|
6529
|
+
if (!existsSync9(file.path)) continue;
|
|
6530
|
+
const current = await readFile6(file.path);
|
|
6521
6531
|
if (!current.equals(file.data)) {
|
|
6522
|
-
throw new
|
|
6523
|
-
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files
|
|
6532
|
+
throw new OverwriteRefusedError(
|
|
6533
|
+
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
6534
|
+
[file.path]
|
|
6524
6535
|
);
|
|
6525
6536
|
}
|
|
6526
6537
|
}
|
|
@@ -6539,6 +6550,152 @@ async function installRecipe(target, options = {}) {
|
|
|
6539
6550
|
};
|
|
6540
6551
|
}
|
|
6541
6552
|
|
|
6553
|
+
// src/pipeline/audit.ts
|
|
6554
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
6555
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6556
|
+
import path14 from "path";
|
|
6557
|
+
function paletteDistance(asset, reference) {
|
|
6558
|
+
if (!asset.length || !reference.length) return 0;
|
|
6559
|
+
let total = 0;
|
|
6560
|
+
let weight = 0;
|
|
6561
|
+
for (const color of asset) {
|
|
6562
|
+
let nearest = Infinity;
|
|
6563
|
+
for (const ref of reference) nearest = Math.min(nearest, colorDistance(color, ref));
|
|
6564
|
+
total += nearest * color.weight;
|
|
6565
|
+
weight += color.weight;
|
|
6566
|
+
}
|
|
6567
|
+
return weight === 0 ? 0 : total / weight;
|
|
6568
|
+
}
|
|
6569
|
+
function mergePalettes(palettes, topN = 24) {
|
|
6570
|
+
const totals = /* @__PURE__ */ new Map();
|
|
6571
|
+
for (const palette of palettes) {
|
|
6572
|
+
for (const color of palette) {
|
|
6573
|
+
const key = `${color.r},${color.g},${color.b}`;
|
|
6574
|
+
const existing = totals.get(key);
|
|
6575
|
+
if (existing) existing.weight += color.weight;
|
|
6576
|
+
else totals.set(key, { ...color });
|
|
6577
|
+
}
|
|
6578
|
+
}
|
|
6579
|
+
const merged = [...totals.values()].sort((a, b) => b.weight - a.weight).slice(0, topN);
|
|
6580
|
+
const sum = merged.reduce((s, c) => s + c.weight, 0) || 1;
|
|
6581
|
+
return merged.map((c) => ({ ...c, weight: c.weight / sum }));
|
|
6582
|
+
}
|
|
6583
|
+
async function auditStyle(loaded, specs, styleId, lock) {
|
|
6584
|
+
const style = loaded.manifest.styles[styleId];
|
|
6585
|
+
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
6586
|
+
const mine = specs.filter((s) => s.styleId === styleId);
|
|
6587
|
+
const assets = [];
|
|
6588
|
+
const missing = [];
|
|
6589
|
+
const unreadable = [];
|
|
6590
|
+
for (const spec of mine) {
|
|
6591
|
+
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
6592
|
+
if (!existsSync10(output.absolutePath)) {
|
|
6593
|
+
missing.push(output.id);
|
|
6594
|
+
continue;
|
|
6595
|
+
}
|
|
6596
|
+
try {
|
|
6597
|
+
const png = decodePng(await readFile7(output.absolutePath));
|
|
6598
|
+
const palette = extractPalette(png, 12);
|
|
6599
|
+
assets.push({
|
|
6600
|
+
assetId: spec.assetId,
|
|
6601
|
+
...output.role ? { outputRole: output.role } : {},
|
|
6602
|
+
id: output.id,
|
|
6603
|
+
file: output.absolutePath,
|
|
6604
|
+
width: png.width,
|
|
6605
|
+
height: png.height,
|
|
6606
|
+
paletteDistance: 0,
|
|
6607
|
+
// filled once the reference is known
|
|
6608
|
+
transparency: transparencyRatio(png),
|
|
6609
|
+
colorCount: countColors(png),
|
|
6610
|
+
palette
|
|
6611
|
+
});
|
|
6612
|
+
} catch (err) {
|
|
6613
|
+
unreadable.push(`${output.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
let reference = [];
|
|
6618
|
+
let referenceFromStyleImages = false;
|
|
6619
|
+
const refPalettes = [];
|
|
6620
|
+
for (const img of style.styleImages) {
|
|
6621
|
+
const abs = path14.resolve(loaded.root, img.path);
|
|
6622
|
+
if (!existsSync10(abs)) continue;
|
|
6623
|
+
try {
|
|
6624
|
+
refPalettes.push(extractPalette(decodePng(await readFile7(abs)), 12));
|
|
6625
|
+
} catch {
|
|
6626
|
+
}
|
|
6627
|
+
}
|
|
6628
|
+
if (refPalettes.length) {
|
|
6629
|
+
reference = mergePalettes(refPalettes);
|
|
6630
|
+
referenceFromStyleImages = true;
|
|
6631
|
+
for (const asset of assets) {
|
|
6632
|
+
asset.paletteDistance = paletteDistance(asset.palette, reference);
|
|
6633
|
+
}
|
|
6634
|
+
} else {
|
|
6635
|
+
reference = mergePalettes(assets.map((a) => a.palette));
|
|
6636
|
+
for (const asset of assets) {
|
|
6637
|
+
const others = assets.filter((a) => a !== asset).map((a) => a.palette);
|
|
6638
|
+
asset.paletteDistance = others.length ? paletteDistance(asset.palette, mergePalettes(others)) : 0;
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
assets.sort((a, b) => b.paletteDistance - a.paletteDistance);
|
|
6642
|
+
return { styleId, referenceFromStyleImages, reference, assets, missing, unreadable };
|
|
6643
|
+
}
|
|
6644
|
+
function countColors(png) {
|
|
6645
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6646
|
+
for (let i = 0; i < png.pixels.length; i += 4) {
|
|
6647
|
+
if (png.pixels[i + 3] < 128) continue;
|
|
6648
|
+
seen.add(png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2]);
|
|
6649
|
+
}
|
|
6650
|
+
return seen.size;
|
|
6651
|
+
}
|
|
6652
|
+
function outliers(audit, sigma = 1.5) {
|
|
6653
|
+
const n = audit.assets.length;
|
|
6654
|
+
if (n < 3) return [];
|
|
6655
|
+
const mean = audit.assets.reduce((s, a) => s + a.paletteDistance, 0) / n;
|
|
6656
|
+
const variance = audit.assets.reduce((s, a) => s + (a.paletteDistance - mean) ** 2, 0) / n;
|
|
6657
|
+
const sd = Math.sqrt(variance);
|
|
6658
|
+
if (sd === 0) return [];
|
|
6659
|
+
return audit.assets.filter((a) => a.paletteDistance > mean + sigma * sd);
|
|
6660
|
+
}
|
|
6661
|
+
function evaluateAudit(audit, thresholds = {}) {
|
|
6662
|
+
const resolved = { ...thresholds, sigma: thresholds.sigma ?? 1.5 };
|
|
6663
|
+
const relative = outliers(audit, resolved.sigma);
|
|
6664
|
+
const relativeIds = new Set(relative.map((asset) => asset.id));
|
|
6665
|
+
const violations = [];
|
|
6666
|
+
for (const asset of audit.assets) {
|
|
6667
|
+
const reasons = [];
|
|
6668
|
+
if (relativeIds.has(asset.id)) {
|
|
6669
|
+
reasons.push(`palette outlier beyond ${resolved.sigma} standard deviations`);
|
|
6670
|
+
}
|
|
6671
|
+
if (resolved.maxDistance !== void 0 && asset.paletteDistance > resolved.maxDistance) {
|
|
6672
|
+
reasons.push(
|
|
6673
|
+
`palette distance ${asset.paletteDistance.toFixed(1)} exceeds ${resolved.maxDistance}`
|
|
6674
|
+
);
|
|
6675
|
+
}
|
|
6676
|
+
if (resolved.minTransparency !== void 0 && asset.transparency < resolved.minTransparency) {
|
|
6677
|
+
reasons.push(
|
|
6678
|
+
`transparency ${(asset.transparency * 100).toFixed(1)}% is below ${(resolved.minTransparency * 100).toFixed(1)}%`
|
|
6679
|
+
);
|
|
6680
|
+
}
|
|
6681
|
+
if (resolved.maxColors !== void 0 && asset.colorCount > resolved.maxColors) {
|
|
6682
|
+
reasons.push(`color count ${asset.colorCount} exceeds ${resolved.maxColors}`);
|
|
6683
|
+
}
|
|
6684
|
+
if (reasons.length) violations.push({ id: asset.id, reasons });
|
|
6685
|
+
}
|
|
6686
|
+
return {
|
|
6687
|
+
safe: violations.length === 0 && audit.missing.length === 0 && audit.unreadable.length === 0,
|
|
6688
|
+
thresholds: resolved,
|
|
6689
|
+
outliers: [...relativeIds],
|
|
6690
|
+
violations,
|
|
6691
|
+
missing: [...audit.missing],
|
|
6692
|
+
unreadable: [...audit.unreadable]
|
|
6693
|
+
};
|
|
6694
|
+
}
|
|
6695
|
+
function hex(c) {
|
|
6696
|
+
return `#${[c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
6697
|
+
}
|
|
6698
|
+
|
|
6542
6699
|
// src/pipeline/quality-regression.ts
|
|
6543
6700
|
import path15 from "path";
|
|
6544
6701
|
import { existsSync as existsSync11 } from "fs";
|
|
@@ -6787,7 +6944,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
6787
6944
|
if (existsSync11(absolute) && !options.force) {
|
|
6788
6945
|
const current = await readFile8(absolute);
|
|
6789
6946
|
if (!current.equals(data)) {
|
|
6790
|
-
throw new
|
|
6947
|
+
throw new OverwriteRefusedError(
|
|
6948
|
+
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
6949
|
+
[absolute]
|
|
6950
|
+
);
|
|
6791
6951
|
}
|
|
6792
6952
|
}
|
|
6793
6953
|
const result = await writeArtifactBundle([{ path: absolute, data }]);
|
|
@@ -7008,8 +7168,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7008
7168
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
7009
7169
|
if (e.status !== "downloaded") return false;
|
|
7010
7170
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
7011
|
-
if (!opts.repair) return false;
|
|
7012
7171
|
const spec = specByKey.get(key);
|
|
7172
|
+
if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
|
|
7173
|
+
return Boolean(opts.force) || e.outputs.every((output) => {
|
|
7174
|
+
const file = resolveOutputPath(output.path, spec.root);
|
|
7175
|
+
return existsSync13(file) && sha256(readFileSync3(file)) === output.sha256;
|
|
7176
|
+
});
|
|
7177
|
+
}
|
|
7178
|
+
if (!opts.repair) return false;
|
|
7013
7179
|
if (e.outputs.length === 0 || !spec) return true;
|
|
7014
7180
|
return e.outputs.some((output) => {
|
|
7015
7181
|
const file = resolveOutputPath(output.path, spec.root);
|
|
@@ -7048,6 +7214,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7048
7214
|
}
|
|
7049
7215
|
const outputs = [];
|
|
7050
7216
|
let wrote = false;
|
|
7217
|
+
const wanted = postprocessFor(spec);
|
|
7218
|
+
const reapply = entry.status === "downloaded" && !postprocessCurrent(entry, spec);
|
|
7051
7219
|
try {
|
|
7052
7220
|
for (let index = 0; index < sources.length; index++) {
|
|
7053
7221
|
const source = sources[index];
|
|
@@ -7055,20 +7223,24 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7055
7223
|
(o) => source.role ? o.role === source.role : !o.role && sources.length === 1
|
|
7056
7224
|
);
|
|
7057
7225
|
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
|
|
7226
|
+
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
7227
|
+
const recordedRaw = recorded?.raw ?? recorded?.sha256;
|
|
7058
7228
|
let fresh = null;
|
|
7059
7229
|
if (opts.refresh) {
|
|
7060
7230
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
7061
7231
|
fresh = await provider.download(source.url);
|
|
7062
|
-
if (recorded && sha256(fresh) ===
|
|
7232
|
+
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
7063
7233
|
log(` current ${path16.relative(process.cwd(), target)}`);
|
|
7064
7234
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
7065
7235
|
continue;
|
|
7066
7236
|
}
|
|
7067
7237
|
}
|
|
7238
|
+
let raw = null;
|
|
7239
|
+
let final = null;
|
|
7068
7240
|
if (existsSync13(target)) {
|
|
7069
7241
|
const currentHash = await sha256File(target);
|
|
7070
7242
|
if (recorded && currentHash === recorded.sha256) {
|
|
7071
|
-
if (!fresh) {
|
|
7243
|
+
if (!fresh && !reapply) {
|
|
7072
7244
|
if (cacheDir) {
|
|
7073
7245
|
await cacheMedia(
|
|
7074
7246
|
cacheDir,
|
|
@@ -7080,7 +7252,12 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7080
7252
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
7081
7253
|
continue;
|
|
7082
7254
|
}
|
|
7083
|
-
|
|
7255
|
+
if (reapply && !fresh) {
|
|
7256
|
+
if (!recorded.raw) raw = await readFile10(target);
|
|
7257
|
+
log(` reapply ${path16.relative(process.cwd(), target)}`);
|
|
7258
|
+
} else {
|
|
7259
|
+
log(` changed ${path16.relative(process.cwd(), target)} (upstream)`);
|
|
7260
|
+
}
|
|
7084
7261
|
} else {
|
|
7085
7262
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
7086
7263
|
(output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
|
|
@@ -7102,26 +7279,41 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7102
7279
|
);
|
|
7103
7280
|
}
|
|
7104
7281
|
}
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
if (
|
|
7108
|
-
|
|
7109
|
-
|
|
7282
|
+
if (fresh) {
|
|
7283
|
+
raw = fresh;
|
|
7284
|
+
} else if (!raw && recorded && cacheDir) {
|
|
7285
|
+
if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
|
|
7286
|
+
if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
|
|
7287
|
+
if (final || raw) log(` cached ${path16.relative(process.cwd(), target)}`);
|
|
7288
|
+
}
|
|
7289
|
+
if (!final && !raw) {
|
|
7110
7290
|
if (!source.url) {
|
|
7111
7291
|
throw new Error(`no source URL or cached bytes remain for ${source.role ?? "asset"}`);
|
|
7112
7292
|
}
|
|
7113
|
-
|
|
7293
|
+
raw = await provider.download(source.url);
|
|
7114
7294
|
}
|
|
7115
7295
|
let mediaType;
|
|
7296
|
+
const bytes = final ?? raw;
|
|
7116
7297
|
try {
|
|
7117
|
-
mediaType = validateMedia(
|
|
7298
|
+
mediaType = validateMedia(bytes, expectedMediaType);
|
|
7118
7299
|
} catch (err) {
|
|
7119
7300
|
const label = expectedMediaType === MediaType.GIF ? "GIF" : "PNG";
|
|
7120
|
-
const mismatch = detectMediaType(
|
|
7301
|
+
const mismatch = detectMediaType(bytes) !== expectedMediaType;
|
|
7121
7302
|
throw new Error(
|
|
7122
|
-
`response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${
|
|
7303
|
+
`response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${bytes.length} bytes)` : `: ${err instanceof Error ? err.message : String(err)}`)
|
|
7123
7304
|
);
|
|
7124
7305
|
}
|
|
7306
|
+
let rawHash = final ? recorded?.raw : void 0;
|
|
7307
|
+
let buf = bytes;
|
|
7308
|
+
if (!final) {
|
|
7309
|
+
if (cacheDir) await cacheMedia(cacheDir, raw, mediaType);
|
|
7310
|
+
const processed = applyPostprocess(raw, mediaType, wanted);
|
|
7311
|
+
buf = processed.bytes;
|
|
7312
|
+
if (processed.changed) {
|
|
7313
|
+
rawHash = sha256(raw);
|
|
7314
|
+
log(` snapped ${path16.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
|
|
7315
|
+
}
|
|
7316
|
+
}
|
|
7125
7317
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
7126
7318
|
await mkdir4(path16.dirname(target), { recursive: true });
|
|
7127
7319
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
@@ -7130,7 +7322,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7130
7322
|
path: portableOutputPath(target, spec.root),
|
|
7131
7323
|
sha256: sha256(buf),
|
|
7132
7324
|
...source.role ? { role: source.role } : {},
|
|
7133
|
-
mediaType
|
|
7325
|
+
mediaType,
|
|
7326
|
+
...rawHash ? { raw: rawHash } : {}
|
|
7134
7327
|
});
|
|
7135
7328
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
7136
7329
|
await saveLock(lockPath, lock);
|
|
@@ -7154,7 +7347,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7154
7347
|
sourceUrls: persistentSources,
|
|
7155
7348
|
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7156
7349
|
error: null,
|
|
7157
|
-
supersededOutputs: []
|
|
7350
|
+
supersededOutputs: [],
|
|
7351
|
+
postprocess: wanted
|
|
7158
7352
|
});
|
|
7159
7353
|
result.downloaded++;
|
|
7160
7354
|
} catch (err) {
|
|
@@ -7261,6 +7455,7 @@ function retireGeneration(entry, retiredAt) {
|
|
|
7261
7455
|
cost: entry.cost,
|
|
7262
7456
|
costUnit: entry.costUnit,
|
|
7263
7457
|
provider: entry.provider,
|
|
7458
|
+
postprocess: entry.postprocess,
|
|
7264
7459
|
retiredAt
|
|
7265
7460
|
};
|
|
7266
7461
|
}
|
|
@@ -7273,9 +7468,13 @@ function historyAfterReplacing(previous, limit, retiredAt) {
|
|
|
7273
7468
|
}
|
|
7274
7469
|
function referencedHashes(lock) {
|
|
7275
7470
|
const hashes = /* @__PURE__ */ new Set();
|
|
7471
|
+
const add = (output) => {
|
|
7472
|
+
hashes.add(output.sha256);
|
|
7473
|
+
if (output.raw) hashes.add(output.raw);
|
|
7474
|
+
};
|
|
7276
7475
|
for (const entry of Object.values(lock.entries)) {
|
|
7277
|
-
for (const output of entry.outputs)
|
|
7278
|
-
for (const generation of entry.history ?? []) for (const output of generation.outputs)
|
|
7476
|
+
for (const output of entry.outputs) add(output);
|
|
7477
|
+
for (const generation of entry.history ?? []) for (const output of generation.outputs) add(output);
|
|
7279
7478
|
}
|
|
7280
7479
|
return hashes;
|
|
7281
7480
|
}
|
|
@@ -7312,8 +7511,9 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
7312
7511
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
7313
7512
|
if (!existsSync14(file)) continue;
|
|
7314
7513
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
7315
|
-
throw new
|
|
7316
|
-
`${path17.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it
|
|
7514
|
+
throw new OverwriteRefusedError(
|
|
7515
|
+
`${path17.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
7516
|
+
[file]
|
|
7317
7517
|
);
|
|
7318
7518
|
}
|
|
7319
7519
|
}
|
|
@@ -7345,6 +7545,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
7345
7545
|
cost: generation.cost,
|
|
7346
7546
|
costUnit: generation.costUnit,
|
|
7347
7547
|
provider: generation.provider,
|
|
7548
|
+
postprocess: generation.postprocess,
|
|
7348
7549
|
history
|
|
7349
7550
|
});
|
|
7350
7551
|
await saveLock(lockPath, lock);
|
|
@@ -8063,7 +8264,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
8063
8264
|
if (opts.budget != null) {
|
|
8064
8265
|
const cost = [...estimates.values()].reduce((sum, estimate) => sum + estimate.amount, 0);
|
|
8065
8266
|
if (cost > opts.budget) {
|
|
8066
|
-
throw new
|
|
8267
|
+
throw new BudgetError(
|
|
8067
8268
|
`This run would spend ${formatCost(unit, cost)} but the budget is ${formatCost(unit, opts.budget)}. Narrow it with --only/--style, or raise --budget.`
|
|
8068
8269
|
);
|
|
8069
8270
|
}
|
|
@@ -8701,6 +8902,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
8701
8902
|
promptSuffix: "",
|
|
8702
8903
|
styleImages: [],
|
|
8703
8904
|
palette: [],
|
|
8905
|
+
enforcePalette: false,
|
|
8704
8906
|
outDir,
|
|
8705
8907
|
tags: [name],
|
|
8706
8908
|
providerOptions: {}
|
|
@@ -9390,7 +9592,7 @@ var WorkspaceSchema = z6.object({
|
|
|
9390
9592
|
function parseWorkspace(raw) {
|
|
9391
9593
|
const parsed = WorkspaceSchema.safeParse(raw);
|
|
9392
9594
|
if (parsed.success) return parsed.data;
|
|
9393
|
-
throw new
|
|
9595
|
+
throw new ProjectError(
|
|
9394
9596
|
`Workspace catalog is not valid v1:
|
|
9395
9597
|
${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
|
|
9396
9598
|
);
|
|
@@ -9401,7 +9603,7 @@ async function loadWorkspace(workspacePath) {
|
|
|
9401
9603
|
try {
|
|
9402
9604
|
raw = JSON.parse(await readFile16(workspacePath, "utf8"));
|
|
9403
9605
|
} catch (err) {
|
|
9404
|
-
throw new
|
|
9606
|
+
throw new ProjectError(
|
|
9405
9607
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
9406
9608
|
${err instanceof Error ? err.message : String(err)}`
|
|
9407
9609
|
);
|
|
@@ -9581,6 +9783,8 @@ var ManifestEditSchema = z7.discriminatedUnion("action", [
|
|
|
9581
9783
|
promptSuffix: z7.string().optional(),
|
|
9582
9784
|
/** `#rrggbb` values; null or empty clears the style's own palette. */
|
|
9583
9785
|
palette: z7.array(z7.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
|
|
9786
|
+
/** Snap downloaded art to the palette; false clears the style's own value. */
|
|
9787
|
+
enforcePalette: z7.boolean().optional(),
|
|
9584
9788
|
/** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
|
|
9585
9789
|
view: z7.string().max(64).nullable().optional(),
|
|
9586
9790
|
/** pixflux only; null clears the style's own value. */
|
|
@@ -9672,6 +9876,7 @@ function applyEdit(raw, edit) {
|
|
|
9672
9876
|
const colors = (patch2.palette ?? []).map((color) => "#" + color.replace(/^#/, "").toLowerCase());
|
|
9673
9877
|
setOrDelete(style, "palette", colors.length ? colors : null);
|
|
9674
9878
|
}
|
|
9879
|
+
if (patch2.enforcePalette !== void 0) setOrDelete(style, "enforcePalette", patch2.enforcePalette || null);
|
|
9675
9880
|
if (patch2.view !== void 0) setOrDelete(style, "view", patch2.view?.trim() || null);
|
|
9676
9881
|
if (patch2.noBackground !== void 0) setOrDelete(style, "noBackground", patch2.noBackground);
|
|
9677
9882
|
return;
|
|
@@ -10383,6 +10588,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10383
10588
|
outputs,
|
|
10384
10589
|
quality,
|
|
10385
10590
|
providerMetadata: entry?.providerMetadata ?? {},
|
|
10591
|
+
postprocess: entry?.postprocess ?? null,
|
|
10386
10592
|
asset,
|
|
10387
10593
|
source: spec.source ?? null,
|
|
10388
10594
|
edit,
|
|
@@ -10443,6 +10649,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10443
10649
|
outputs,
|
|
10444
10650
|
quality: null,
|
|
10445
10651
|
providerMetadata: entry.providerMetadata ?? {},
|
|
10652
|
+
postprocess: entry.postprocess ?? null,
|
|
10446
10653
|
asset: null,
|
|
10447
10654
|
source: null,
|
|
10448
10655
|
edit: null,
|
|
@@ -10491,6 +10698,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10491
10698
|
promptPrefix: style?.promptPrefix ?? "",
|
|
10492
10699
|
promptSuffix: style?.promptSuffix ?? "",
|
|
10493
10700
|
palette: style?.palette ?? [],
|
|
10701
|
+
enforcePalette: style?.enforcePalette ?? false,
|
|
10494
10702
|
view: style?.view ?? null,
|
|
10495
10703
|
noBackground: style?.noBackground ?? true,
|
|
10496
10704
|
quality: Boolean(style?.quality),
|
|
@@ -12193,6 +12401,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
12193
12401
|
}
|
|
12194
12402
|
export {
|
|
12195
12403
|
AssetSchema,
|
|
12404
|
+
BudgetError,
|
|
12196
12405
|
ComfyUIProvider,
|
|
12197
12406
|
DEFAULT_HISTORY_LIMIT,
|
|
12198
12407
|
DEFAULT_QUALITY_TOLERANCES,
|
|
@@ -12200,6 +12409,7 @@ export {
|
|
|
12200
12409
|
DEFAULT_TIMEOUT_MS,
|
|
12201
12410
|
EDITOR_PIN,
|
|
12202
12411
|
EDITOR_RELEASE_BASE,
|
|
12412
|
+
EXIT_CODES,
|
|
12203
12413
|
EditorInstallError,
|
|
12204
12414
|
FAKE_PNG,
|
|
12205
12415
|
FakeProvider,
|
|
@@ -12224,11 +12434,16 @@ export {
|
|
|
12224
12434
|
ManifestInputSchema,
|
|
12225
12435
|
ManifestSchema,
|
|
12226
12436
|
MediaType,
|
|
12437
|
+
OverwriteRefusedError,
|
|
12227
12438
|
PIXEL_ART_FIXER_REVISION,
|
|
12228
12439
|
PIXEL_ART_FIXER_URL,
|
|
12440
|
+
PixelKilnError,
|
|
12229
12441
|
PixelLabClient,
|
|
12230
12442
|
PixelLabError,
|
|
12231
12443
|
PixelLabProvider,
|
|
12444
|
+
PostprocessSchema,
|
|
12445
|
+
ProjectError,
|
|
12446
|
+
ProviderError,
|
|
12232
12447
|
QualityBaselineSchema,
|
|
12233
12448
|
QualityProfileSchema,
|
|
12234
12449
|
QualityToleranceSchema,
|
|
@@ -12240,6 +12455,7 @@ export {
|
|
|
12240
12455
|
StyleInputSchema,
|
|
12241
12456
|
StyleSchema,
|
|
12242
12457
|
UnsupportedCapabilityError,
|
|
12458
|
+
UsageError,
|
|
12243
12459
|
WorkspaceProjectSchema,
|
|
12244
12460
|
WorkspaceSchema,
|
|
12245
12461
|
adopt,
|
|
@@ -12277,7 +12493,9 @@ export {
|
|
|
12277
12493
|
editorRoute,
|
|
12278
12494
|
editorStatus,
|
|
12279
12495
|
editorToolsRoot,
|
|
12496
|
+
errorCode,
|
|
12280
12497
|
evaluateAudit,
|
|
12498
|
+
exitCodeFor,
|
|
12281
12499
|
expectedOutputPath,
|
|
12282
12500
|
exportTileset,
|
|
12283
12501
|
fallbackOutputRole,
|