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.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AssetSchema: () => AssetSchema,
|
|
34
|
+
BudgetError: () => BudgetError,
|
|
34
35
|
ComfyUIProvider: () => ComfyUIProvider,
|
|
35
36
|
DEFAULT_HISTORY_LIMIT: () => DEFAULT_HISTORY_LIMIT,
|
|
36
37
|
DEFAULT_QUALITY_TOLERANCES: () => DEFAULT_QUALITY_TOLERANCES,
|
|
@@ -38,6 +39,7 @@ __export(index_exports, {
|
|
|
38
39
|
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
39
40
|
EDITOR_PIN: () => EDITOR_PIN,
|
|
40
41
|
EDITOR_RELEASE_BASE: () => EDITOR_RELEASE_BASE,
|
|
42
|
+
EXIT_CODES: () => EXIT_CODES,
|
|
41
43
|
EditorInstallError: () => EditorInstallError,
|
|
42
44
|
FAKE_PNG: () => FAKE_PNG,
|
|
43
45
|
FakeProvider: () => FakeProvider,
|
|
@@ -62,11 +64,16 @@ __export(index_exports, {
|
|
|
62
64
|
ManifestInputSchema: () => ManifestInputSchema,
|
|
63
65
|
ManifestSchema: () => ManifestSchema,
|
|
64
66
|
MediaType: () => MediaType,
|
|
67
|
+
OverwriteRefusedError: () => OverwriteRefusedError,
|
|
65
68
|
PIXEL_ART_FIXER_REVISION: () => PIXEL_ART_FIXER_REVISION,
|
|
66
69
|
PIXEL_ART_FIXER_URL: () => PIXEL_ART_FIXER_URL,
|
|
70
|
+
PixelKilnError: () => PixelKilnError,
|
|
67
71
|
PixelLabClient: () => PixelLabClient,
|
|
68
72
|
PixelLabError: () => PixelLabError,
|
|
69
73
|
PixelLabProvider: () => PixelLabProvider,
|
|
74
|
+
PostprocessSchema: () => PostprocessSchema,
|
|
75
|
+
ProjectError: () => ProjectError,
|
|
76
|
+
ProviderError: () => ProviderError,
|
|
70
77
|
QualityBaselineSchema: () => QualityBaselineSchema,
|
|
71
78
|
QualityProfileSchema: () => QualityProfileSchema,
|
|
72
79
|
QualityToleranceSchema: () => QualityToleranceSchema,
|
|
@@ -78,6 +85,7 @@ __export(index_exports, {
|
|
|
78
85
|
StyleInputSchema: () => StyleInputSchema,
|
|
79
86
|
StyleSchema: () => StyleSchema,
|
|
80
87
|
UnsupportedCapabilityError: () => UnsupportedCapabilityError,
|
|
88
|
+
UsageError: () => UsageError,
|
|
81
89
|
WorkspaceProjectSchema: () => WorkspaceProjectSchema,
|
|
82
90
|
WorkspaceSchema: () => WorkspaceSchema,
|
|
83
91
|
adopt: () => adopt,
|
|
@@ -115,7 +123,9 @@ __export(index_exports, {
|
|
|
115
123
|
editorRoute: () => editorRoute,
|
|
116
124
|
editorStatus: () => editorStatus,
|
|
117
125
|
editorToolsRoot: () => editorToolsRoot,
|
|
126
|
+
errorCode: () => errorCode,
|
|
118
127
|
evaluateAudit: () => evaluateAudit,
|
|
128
|
+
exitCodeFor: () => exitCodeFor,
|
|
119
129
|
expectedOutputPath: () => expectedOutputPath,
|
|
120
130
|
exportTileset: () => exportTileset,
|
|
121
131
|
fallbackOutputRole: () => fallbackOutputRole,
|
|
@@ -441,6 +451,18 @@ var StyleObjectSchema = import_zod.z.object({
|
|
|
441
451
|
* chocolate bar.
|
|
442
452
|
*/
|
|
443
453
|
palette: import_zod.z.array(HexColorSchema).default([]),
|
|
454
|
+
/**
|
|
455
|
+
* Snap every visible pixel of the downloaded art to the nearest `palette`
|
|
456
|
+
* colour, without dithering. The palette is sent to providers that take
|
|
457
|
+
* one, and most honour it most of the time; this makes it a guarantee
|
|
458
|
+
* for every generator, including the ones with no palette parameter.
|
|
459
|
+
* Applied by `fetch` as the file is written and recorded on the lock
|
|
460
|
+
* entry, so a restore reproduces the same bytes. Turning it on or off
|
|
461
|
+
* does not change the spec hash: `plan` reports the entry as recoverable
|
|
462
|
+
* and the next `fetch` or `restore` re-applies it from the art already
|
|
463
|
+
* on disk or in the cache, spending nothing.
|
|
464
|
+
*/
|
|
465
|
+
enforcePalette: import_zod.z.boolean().default(false),
|
|
444
466
|
/** Fail-closed native-grid, palette, and human-approval policy for derived art. */
|
|
445
467
|
quality: QualityProfileSchema.optional(),
|
|
446
468
|
/**
|
|
@@ -482,6 +504,13 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
|
|
|
482
504
|
path: ["quality"]
|
|
483
505
|
});
|
|
484
506
|
}
|
|
507
|
+
if (style.enforcePalette && style.palette.length < 2) {
|
|
508
|
+
ctx.addIssue({
|
|
509
|
+
code: import_zod.z.ZodIssueCode.custom,
|
|
510
|
+
message: "enforcePalette needs a palette of at least two colours to snap to",
|
|
511
|
+
path: ["enforcePalette"]
|
|
512
|
+
});
|
|
513
|
+
}
|
|
485
514
|
});
|
|
486
515
|
var InheritedStyleSchema = StyleObjectSchema.partial().extend({
|
|
487
516
|
extends: import_zod.z.string().min(1),
|
|
@@ -604,7 +633,21 @@ var LockOutputSchema = import_zod.z.object({
|
|
|
604
633
|
path: import_zod.z.string(),
|
|
605
634
|
sha256: import_zod.z.string(),
|
|
606
635
|
role: import_zod.z.string().optional(),
|
|
607
|
-
mediaType: MediaTypeSchema.optional()
|
|
636
|
+
mediaType: MediaTypeSchema.optional(),
|
|
637
|
+
/**
|
|
638
|
+
* Hash of the provider's bytes before post-processing changed them, when
|
|
639
|
+
* it did. The content cache keeps both, so the post-processing can be
|
|
640
|
+
* undone or redone without contacting the provider.
|
|
641
|
+
*/
|
|
642
|
+
raw: import_zod.z.string().optional()
|
|
643
|
+
});
|
|
644
|
+
var PostprocessSchema = import_zod.z.object({
|
|
645
|
+
palette: import_zod.z.object({
|
|
646
|
+
/** Canonical lowercase `#rrggbb` values, in manifest order. */
|
|
647
|
+
colors: import_zod.z.array(import_zod.z.string()).min(2).max(256),
|
|
648
|
+
dither: import_zod.z.literal("none"),
|
|
649
|
+
distance: import_zod.z.literal("redmean")
|
|
650
|
+
}).optional()
|
|
608
651
|
});
|
|
609
652
|
var LockSourceSchema = import_zod.z.object({
|
|
610
653
|
url: import_zod.z.string(),
|
|
@@ -630,6 +673,7 @@ var LockHistoryEntrySchema = import_zod.z.object({
|
|
|
630
673
|
cost: import_zod.z.number().finite().nonnegative().default(0),
|
|
631
674
|
costUnit: import_zod.z.string().min(1).default("generations"),
|
|
632
675
|
provider: import_zod.z.string().default("pixellab"),
|
|
676
|
+
postprocess: PostprocessSchema.optional(),
|
|
633
677
|
/** When the replacement was submitted. */
|
|
634
678
|
retiredAt: import_zod.z.string()
|
|
635
679
|
});
|
|
@@ -701,14 +745,7 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
701
745
|
* `role` labels non-primary artifacts (e.g. "portrait", "spriteframes") so a
|
|
702
746
|
* consumer can find the one it wants without pattern-matching on paths.
|
|
703
747
|
*/
|
|
704
|
-
outputs: import_zod.z.array(
|
|
705
|
-
import_zod.z.object({
|
|
706
|
-
path: import_zod.z.string(),
|
|
707
|
-
sha256: import_zod.z.string(),
|
|
708
|
-
role: import_zod.z.string().optional(),
|
|
709
|
-
mediaType: MediaTypeSchema.optional()
|
|
710
|
-
})
|
|
711
|
-
).default([]),
|
|
748
|
+
outputs: import_zod.z.array(LockOutputSchema).default([]),
|
|
712
749
|
/**
|
|
713
750
|
* Provider-owned data retained for downstream consumers, namespaced by
|
|
714
751
|
* provider id. PixelLab connectable sets keep their exact `tileRules` here
|
|
@@ -724,7 +761,9 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
724
761
|
/** Which provider produced this. Absent on entries written before providers. */
|
|
725
762
|
provider: import_zod.z.string().default("pixellab"),
|
|
726
763
|
/** Generations this entry replaced, newest first; see LockHistoryEntrySchema. */
|
|
727
|
-
history: import_zod.z.array(LockHistoryEntrySchema).optional()
|
|
764
|
+
history: import_zod.z.array(LockHistoryEntrySchema).optional(),
|
|
765
|
+
/** Post-processing applied to the recorded outputs; absent when the bytes are the provider's. */
|
|
766
|
+
postprocess: PostprocessSchema.optional()
|
|
728
767
|
});
|
|
729
768
|
var LockSchema = import_zod.z.object({
|
|
730
769
|
version: import_zod.z.literal(2),
|
|
@@ -745,6 +784,72 @@ function lockKey(styleId, assetId) {
|
|
|
745
784
|
return `${styleId}/${assetId}`;
|
|
746
785
|
}
|
|
747
786
|
|
|
787
|
+
// src/errors.ts
|
|
788
|
+
var EXIT_CODES = {
|
|
789
|
+
usage: 2,
|
|
790
|
+
project: 3,
|
|
791
|
+
provider: 4,
|
|
792
|
+
"refused-overwrite": 5,
|
|
793
|
+
budget: 6,
|
|
794
|
+
capability: 7
|
|
795
|
+
};
|
|
796
|
+
var PixelKilnError = class extends Error {
|
|
797
|
+
code;
|
|
798
|
+
hint;
|
|
799
|
+
constructor(message6, code, opts = {}) {
|
|
800
|
+
super(message6, opts.cause === void 0 ? void 0 : { cause: opts.cause });
|
|
801
|
+
this.name = "PixelKilnError";
|
|
802
|
+
this.code = code;
|
|
803
|
+
if (opts.hint) this.hint = opts.hint;
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
var UsageError = class extends PixelKilnError {
|
|
807
|
+
constructor(message6, opts) {
|
|
808
|
+
super(message6, "usage", opts);
|
|
809
|
+
this.name = "UsageError";
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
var ProjectError = class extends PixelKilnError {
|
|
813
|
+
constructor(message6, opts) {
|
|
814
|
+
super(message6, "project", opts);
|
|
815
|
+
this.name = "ProjectError";
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
var ProviderError = class extends PixelKilnError {
|
|
819
|
+
/** Registry id of the provider that failed. */
|
|
820
|
+
provider;
|
|
821
|
+
/** HTTP status when the failure was a response, otherwise undefined. */
|
|
822
|
+
status;
|
|
823
|
+
constructor(provider, message6, opts = {}) {
|
|
824
|
+
super(message6, "provider", opts);
|
|
825
|
+
this.name = "ProviderError";
|
|
826
|
+
this.provider = provider;
|
|
827
|
+
if (opts.status !== void 0) this.status = opts.status;
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
var OverwriteRefusedError = class extends PixelKilnError {
|
|
831
|
+
/** The file(s) left untouched. */
|
|
832
|
+
files;
|
|
833
|
+
constructor(message6, files, opts) {
|
|
834
|
+
super(message6, "refused-overwrite", opts);
|
|
835
|
+
this.name = "OverwriteRefusedError";
|
|
836
|
+
this.files = files;
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
var BudgetError = class extends PixelKilnError {
|
|
840
|
+
constructor(message6, opts) {
|
|
841
|
+
super(message6, "budget", opts);
|
|
842
|
+
this.name = "BudgetError";
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
function errorCode(err) {
|
|
846
|
+
return err instanceof PixelKilnError ? err.code : void 0;
|
|
847
|
+
}
|
|
848
|
+
function exitCodeFor(err) {
|
|
849
|
+
const code = errorCode(err);
|
|
850
|
+
return code ? EXIT_CODES[code] : 1;
|
|
851
|
+
}
|
|
852
|
+
|
|
748
853
|
// src/client.ts
|
|
749
854
|
var import_zod2 = require("zod");
|
|
750
855
|
|
|
@@ -861,14 +966,12 @@ function validateResponse(schema, raw, operation) {
|
|
|
861
966
|
const issues = parsed.error.issues.slice(0, 4).map((i) => `${i.path.join(".") || "response"}: ${i.message}`).join("; ");
|
|
862
967
|
throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
|
|
863
968
|
}
|
|
864
|
-
var PixelLabError = class extends
|
|
969
|
+
var PixelLabError = class extends ProviderError {
|
|
865
970
|
constructor(message6, status, body) {
|
|
866
|
-
super(message6);
|
|
867
|
-
this.status = status;
|
|
971
|
+
super("pixellab", message6, { status });
|
|
868
972
|
this.body = body;
|
|
869
973
|
this.name = "PixelLabError";
|
|
870
974
|
}
|
|
871
|
-
status;
|
|
872
975
|
body;
|
|
873
976
|
};
|
|
874
977
|
var PixelLabClient = class {
|
|
@@ -1148,10 +1251,11 @@ function measureBalanceChange(before, after) {
|
|
|
1148
1251
|
};
|
|
1149
1252
|
}
|
|
1150
1253
|
var DEFAULT_RATE_LIMIT = { spacingMs: 2500, maxInFlight: 8 };
|
|
1151
|
-
var UnsupportedCapabilityError = class extends
|
|
1254
|
+
var UnsupportedCapabilityError = class extends PixelKilnError {
|
|
1152
1255
|
constructor(providerId, capability) {
|
|
1153
1256
|
super(
|
|
1154
|
-
`Provider "${providerId}" does not support ${capability}. That command is unavailable with this backend
|
|
1257
|
+
`Provider "${providerId}" does not support ${capability}. That command is unavailable with this backend.`,
|
|
1258
|
+
"capability"
|
|
1155
1259
|
);
|
|
1156
1260
|
this.name = "UnsupportedCapabilityError";
|
|
1157
1261
|
}
|
|
@@ -1976,7 +2080,7 @@ var RetroDiffusionClient = class {
|
|
|
1976
2080
|
/** Result URLs are signed; no token header, and the same retry policy as the API. */
|
|
1977
2081
|
async download(url) {
|
|
1978
2082
|
const response = await this.request(url);
|
|
1979
|
-
if (!response.ok) throw new
|
|
2083
|
+
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
1980
2084
|
return Buffer.from(await response.arrayBuffer());
|
|
1981
2085
|
}
|
|
1982
2086
|
async call(path32, init = {}) {
|
|
@@ -1999,8 +2103,10 @@ var RetroDiffusionClient = class {
|
|
|
1999
2103
|
if (!response.ok) {
|
|
2000
2104
|
const retry = response.headers.get("retry-after");
|
|
2001
2105
|
const detail = retroError(value);
|
|
2002
|
-
throw new
|
|
2003
|
-
|
|
2106
|
+
throw new ProviderError(
|
|
2107
|
+
"retrodiffusion",
|
|
2108
|
+
`Retro Diffusion request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : ""),
|
|
2109
|
+
{ status: response.status }
|
|
2004
2110
|
);
|
|
2005
2111
|
}
|
|
2006
2112
|
return value;
|
|
@@ -2392,7 +2498,7 @@ var ScenarioClient = class {
|
|
|
2392
2498
|
throw new Error("Scenario downloads must use HTTP or HTTPS");
|
|
2393
2499
|
}
|
|
2394
2500
|
const response = await this.request(parsed);
|
|
2395
|
-
if (!response.ok) throw new
|
|
2501
|
+
if (!response.ok) throw new ProviderError("scenario", `Scenario download failed (${response.status})`, { status: response.status });
|
|
2396
2502
|
const declared = Number(response.headers.get("content-length"));
|
|
2397
2503
|
if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) {
|
|
2398
2504
|
throw new Error(`Scenario download exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
|
|
@@ -2439,8 +2545,10 @@ var ScenarioClient = class {
|
|
|
2439
2545
|
if (!response.ok) {
|
|
2440
2546
|
const retry = response.headers.get("retry-after");
|
|
2441
2547
|
const detail = redactScenarioError(value, [this.apiKey, this.apiSecret, auth]);
|
|
2442
|
-
throw new
|
|
2443
|
-
|
|
2548
|
+
throw new ProviderError(
|
|
2549
|
+
"scenario",
|
|
2550
|
+
`Scenario request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : ""),
|
|
2551
|
+
{ status: response.status }
|
|
2444
2552
|
);
|
|
2445
2553
|
}
|
|
2446
2554
|
return value;
|
|
@@ -2890,8 +2998,10 @@ var ComfyUIClient = class {
|
|
|
2890
2998
|
value = null;
|
|
2891
2999
|
}
|
|
2892
3000
|
if (!response.ok) {
|
|
2893
|
-
throw new
|
|
2894
|
-
|
|
3001
|
+
throw new ProviderError(
|
|
3002
|
+
"comfyui",
|
|
3003
|
+
`ComfyUI image upload failed (${response.status})` + (apiError(value) ? `: ${apiError(value)}` : ""),
|
|
3004
|
+
{ status: response.status }
|
|
2895
3005
|
);
|
|
2896
3006
|
}
|
|
2897
3007
|
if (!isObject(value) || typeof value.name !== "string" || !value.name) {
|
|
@@ -2914,7 +3024,7 @@ var ComfyUIClient = class {
|
|
|
2914
3024
|
let target = source;
|
|
2915
3025
|
if (source.startsWith("comfyui://")) target = this.viewUrl(parseSource(source));
|
|
2916
3026
|
const response = await this.request(target);
|
|
2917
|
-
if (!response.ok) throw new
|
|
3027
|
+
if (!response.ok) throw new ProviderError("comfyui", `ComfyUI download failed (${response.status})`, { status: response.status });
|
|
2918
3028
|
return Buffer.from(await response.arrayBuffer());
|
|
2919
3029
|
}
|
|
2920
3030
|
endpoint(route) {
|
|
@@ -2939,8 +3049,10 @@ var ComfyUIClient = class {
|
|
|
2939
3049
|
}
|
|
2940
3050
|
if (!response.ok) {
|
|
2941
3051
|
const detail = apiError(value);
|
|
2942
|
-
throw new
|
|
2943
|
-
|
|
3052
|
+
throw new ProviderError(
|
|
3053
|
+
"comfyui",
|
|
3054
|
+
`ComfyUI request failed (${response.status}) at ${url.pathname}` + (detail ? `: ${detail}` : ""),
|
|
3055
|
+
{ status: response.status }
|
|
2944
3056
|
);
|
|
2945
3057
|
}
|
|
2946
3058
|
return value;
|
|
@@ -3930,9 +4042,10 @@ async function loadLock(lockPath) {
|
|
|
3930
4042
|
try {
|
|
3931
4043
|
return parseLock(JSON.parse(await (0, import_promises2.readFile)(lockPath, "utf8")));
|
|
3932
4044
|
} catch (err) {
|
|
3933
|
-
throw new
|
|
4045
|
+
throw new ProjectError(
|
|
3934
4046
|
`Lockfile at ${lockPath} is malformed:
|
|
3935
|
-
${err instanceof Error ? err.message : String(err)}
|
|
4047
|
+
${err instanceof Error ? err.message : String(err)}`,
|
|
4048
|
+
{ cause: err }
|
|
3936
4049
|
);
|
|
3937
4050
|
}
|
|
3938
4051
|
}
|
|
@@ -3966,8 +4079,9 @@ async function writeLockWhileHeld(file, lock) {
|
|
|
3966
4079
|
try {
|
|
3967
4080
|
disk = parseLock(JSON.parse(await (0, import_promises2.readFile)(file, "utf8")));
|
|
3968
4081
|
} catch (err) {
|
|
3969
|
-
throw new
|
|
3970
|
-
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}
|
|
4082
|
+
throw new ProjectError(
|
|
4083
|
+
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
4084
|
+
{ cause: err }
|
|
3971
4085
|
);
|
|
3972
4086
|
}
|
|
3973
4087
|
}
|
|
@@ -4223,18 +4337,18 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
|
4223
4337
|
// src/manifest.ts
|
|
4224
4338
|
async function loadManifest(manifestPath) {
|
|
4225
4339
|
const abs = import_node_path5.default.resolve(manifestPath);
|
|
4226
|
-
if (!(0, import_node_fs5.existsSync)(abs)) throw new
|
|
4340
|
+
if (!(0, import_node_fs5.existsSync)(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
4227
4341
|
const input = ManifestInputSchema.safeParse(JSON.parse(await (0, import_promises3.readFile)(abs, "utf8")));
|
|
4228
4342
|
if (!input.success) {
|
|
4229
4343
|
const issues = formatManifestIssues(input.error.issues);
|
|
4230
|
-
throw new
|
|
4344
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
4231
4345
|
${issues}`);
|
|
4232
4346
|
}
|
|
4233
4347
|
let styles;
|
|
4234
4348
|
try {
|
|
4235
4349
|
styles = resolveStyleInheritance(input.data.styles);
|
|
4236
4350
|
} catch (error) {
|
|
4237
|
-
throw new
|
|
4351
|
+
throw new ProjectError(
|
|
4238
4352
|
`Manifest at ${abs} is invalid:
|
|
4239
4353
|
${error instanceof Error ? error.message : String(error)}`
|
|
4240
4354
|
);
|
|
@@ -4242,7 +4356,7 @@ ${issues}`);
|
|
|
4242
4356
|
const parsed = ManifestSchema.safeParse({ ...input.data, styles });
|
|
4243
4357
|
if (!parsed.success) {
|
|
4244
4358
|
const issues = formatManifestIssues(parsed.error.issues);
|
|
4245
|
-
throw new
|
|
4359
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
4246
4360
|
${issues}`);
|
|
4247
4361
|
}
|
|
4248
4362
|
const styleIds = new Set(Object.keys(parsed.data.styles));
|
|
@@ -4292,7 +4406,7 @@ ${issues}`);
|
|
|
4292
4406
|
};
|
|
4293
4407
|
for (const assetId of Object.keys(parsed.data.assets)) visitRevision(assetId, []);
|
|
4294
4408
|
if (unknownReferences.length) {
|
|
4295
|
-
throw new
|
|
4409
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
4296
4410
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
4297
4411
|
}
|
|
4298
4412
|
return { manifest: parsed.data, root: import_node_path5.default.dirname(abs), path: abs };
|
|
@@ -4371,14 +4485,14 @@ async function resolveSpecs(loaded, filter) {
|
|
|
4371
4485
|
);
|
|
4372
4486
|
for (const unknownStyle of filter?.styles ?? []) {
|
|
4373
4487
|
if (!manifest.styles[unknownStyle]) {
|
|
4374
|
-
throw new
|
|
4488
|
+
throw new UsageError(
|
|
4375
4489
|
`Unknown style "${unknownStyle}". Defined: ${Object.keys(manifest.styles).join(", ") || "(none)"}`
|
|
4376
4490
|
);
|
|
4377
4491
|
}
|
|
4378
4492
|
}
|
|
4379
4493
|
for (const unknownAsset of filter?.assets ?? []) {
|
|
4380
4494
|
if (!manifest.assets[unknownAsset]) {
|
|
4381
|
-
throw new
|
|
4495
|
+
throw new UsageError(`Unknown asset "${unknownAsset}".`);
|
|
4382
4496
|
}
|
|
4383
4497
|
}
|
|
4384
4498
|
const requestedAssetIds = new Set(filter?.assets?.length ? filter.assets : Object.keys(manifest.assets));
|
|
@@ -4490,6 +4604,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
4490
4604
|
detail: style.detail,
|
|
4491
4605
|
seed: style.seed,
|
|
4492
4606
|
palette: style.palette,
|
|
4607
|
+
enforcePalette: style.enforcePalette,
|
|
4493
4608
|
noBackground: style.noBackground,
|
|
4494
4609
|
tileSize: generator === "tiles" ? tileSize : void 0,
|
|
4495
4610
|
tileType: generator === "tiles" ? style.tileType : void 0,
|
|
@@ -4694,7 +4809,7 @@ function imageMetadata(buf) {
|
|
|
4694
4809
|
}
|
|
4695
4810
|
|
|
4696
4811
|
// src/project.ts
|
|
4697
|
-
var
|
|
4812
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
4698
4813
|
|
|
4699
4814
|
// src/env.ts
|
|
4700
4815
|
var import_node_fs6 = require("fs");
|
|
@@ -4741,17 +4856,17 @@ function parseEnv(contents) {
|
|
|
4741
4856
|
}
|
|
4742
4857
|
|
|
4743
4858
|
// src/pipeline/plan.ts
|
|
4744
|
-
var
|
|
4745
|
-
var
|
|
4859
|
+
var import_node_fs9 = require("fs");
|
|
4860
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
4746
4861
|
|
|
4747
4862
|
// src/pipeline/quality-profile.ts
|
|
4748
|
-
var
|
|
4749
|
-
var
|
|
4863
|
+
var import_node_fs7 = require("fs");
|
|
4864
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
4750
4865
|
|
|
4751
4866
|
// src/pipeline/refine.ts
|
|
4752
|
-
var
|
|
4867
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
4753
4868
|
var import_node_child_process2 = require("child_process");
|
|
4754
|
-
var
|
|
4869
|
+
var import_promises5 = require("fs/promises");
|
|
4755
4870
|
var import_node_os2 = require("os");
|
|
4756
4871
|
var import_node_util = require("util");
|
|
4757
4872
|
|
|
@@ -4989,10 +5104,11 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
4989
5104
|
if (!expected || digest(current) !== expected) conflicts.push(destination);
|
|
4990
5105
|
}
|
|
4991
5106
|
if (conflicts.length) {
|
|
4992
|
-
throw new
|
|
5107
|
+
throw new OverwriteRefusedError(
|
|
4993
5108
|
`Refusing to overwrite modified or unowned artifact file(s):
|
|
4994
5109
|
` + conflicts.map((file) => ` ${file}`).join("\n") + `
|
|
4995
|
-
Pass { force: true } (CLI: --force) to take ownership and replace the complete bundle
|
|
5110
|
+
Pass { force: true } (CLI: --force) to take ownership and replace the complete bundle.`,
|
|
5111
|
+
conflicts
|
|
4996
5112
|
);
|
|
4997
5113
|
}
|
|
4998
5114
|
}
|
|
@@ -5211,10 +5327,19 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
5211
5327
|
};
|
|
5212
5328
|
}
|
|
5213
5329
|
|
|
5214
|
-
// src/
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5330
|
+
// src/palette.ts
|
|
5331
|
+
function normalizePalette(values) {
|
|
5332
|
+
const colors = /* @__PURE__ */ new Map();
|
|
5333
|
+
for (const value of values) {
|
|
5334
|
+
const rgb = parseHex(value);
|
|
5335
|
+
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
5336
|
+
if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
|
|
5337
|
+
}
|
|
5338
|
+
if (colors.size < 2 || colors.size > 256) {
|
|
5339
|
+
throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
5340
|
+
}
|
|
5341
|
+
return [...colors.values()];
|
|
5342
|
+
}
|
|
5218
5343
|
function colorDistance(a, b) {
|
|
5219
5344
|
const rmean = (a.r + b.r) / 2;
|
|
5220
5345
|
const dr = a.r - b.r;
|
|
@@ -5224,146 +5349,45 @@ function colorDistance(a, b) {
|
|
|
5224
5349
|
(512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
|
|
5225
5350
|
);
|
|
5226
5351
|
}
|
|
5227
|
-
function
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
const totals = /* @__PURE__ */ new Map();
|
|
5241
|
-
for (const palette of palettes) {
|
|
5242
|
-
for (const color of palette) {
|
|
5243
|
-
const key = `${color.r},${color.g},${color.b}`;
|
|
5244
|
-
const existing = totals.get(key);
|
|
5245
|
-
if (existing) existing.weight += color.weight;
|
|
5246
|
-
else totals.set(key, { ...color });
|
|
5352
|
+
function quantize(png, palette) {
|
|
5353
|
+
const colors = normalizePalette(palette);
|
|
5354
|
+
const pixels = Buffer.from(png.pixels);
|
|
5355
|
+
const nearestOf = /* @__PURE__ */ new Map();
|
|
5356
|
+
let changed = 0;
|
|
5357
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
5358
|
+
const alpha = pixels[i + 3];
|
|
5359
|
+
if (alpha === 0) {
|
|
5360
|
+
if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
|
|
5361
|
+
pixels[i] = 0;
|
|
5362
|
+
pixels[i + 1] = 0;
|
|
5363
|
+
pixels[i + 2] = 0;
|
|
5364
|
+
continue;
|
|
5247
5365
|
}
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
5262
|
-
if (!(0, import_node_fs7.existsSync)(output.absolutePath)) {
|
|
5263
|
-
missing.push(output.id);
|
|
5264
|
-
continue;
|
|
5265
|
-
}
|
|
5266
|
-
try {
|
|
5267
|
-
const png = decodePng(await (0, import_promises5.readFile)(output.absolutePath));
|
|
5268
|
-
const palette = extractPalette(png, 12);
|
|
5269
|
-
assets.push({
|
|
5270
|
-
assetId: spec.assetId,
|
|
5271
|
-
...output.role ? { outputRole: output.role } : {},
|
|
5272
|
-
id: output.id,
|
|
5273
|
-
file: output.absolutePath,
|
|
5274
|
-
width: png.width,
|
|
5275
|
-
height: png.height,
|
|
5276
|
-
paletteDistance: 0,
|
|
5277
|
-
// filled once the reference is known
|
|
5278
|
-
transparency: transparencyRatio(png),
|
|
5279
|
-
colorCount: countColors(png),
|
|
5280
|
-
palette
|
|
5281
|
-
});
|
|
5282
|
-
} catch (err) {
|
|
5283
|
-
unreadable.push(`${output.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5366
|
+
const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
|
|
5367
|
+
let nearest = nearestOf.get(key);
|
|
5368
|
+
if (!nearest) {
|
|
5369
|
+
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
5370
|
+
nearest = colors[0];
|
|
5371
|
+
let distance = colorDistance(pixel, nearest);
|
|
5372
|
+
for (let j = 1; j < colors.length; j++) {
|
|
5373
|
+
const candidate = colors[j];
|
|
5374
|
+
const candidateDistance = colorDistance(pixel, candidate);
|
|
5375
|
+
if (candidateDistance < distance) {
|
|
5376
|
+
nearest = candidate;
|
|
5377
|
+
distance = candidateDistance;
|
|
5378
|
+
}
|
|
5284
5379
|
}
|
|
5380
|
+
nearestOf.set(key, nearest);
|
|
5285
5381
|
}
|
|
5382
|
+
if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
|
|
5383
|
+
pixels[i] = nearest.r;
|
|
5384
|
+
pixels[i + 1] = nearest.g;
|
|
5385
|
+
pixels[i + 2] = nearest.b;
|
|
5286
5386
|
}
|
|
5287
|
-
|
|
5288
|
-
let referenceFromStyleImages = false;
|
|
5289
|
-
const refPalettes = [];
|
|
5290
|
-
for (const img of style.styleImages) {
|
|
5291
|
-
const abs = import_node_path8.default.resolve(loaded.root, img.path);
|
|
5292
|
-
if (!(0, import_node_fs7.existsSync)(abs)) continue;
|
|
5293
|
-
try {
|
|
5294
|
-
refPalettes.push(extractPalette(decodePng(await (0, import_promises5.readFile)(abs)), 12));
|
|
5295
|
-
} catch {
|
|
5296
|
-
}
|
|
5297
|
-
}
|
|
5298
|
-
if (refPalettes.length) {
|
|
5299
|
-
reference = mergePalettes(refPalettes);
|
|
5300
|
-
referenceFromStyleImages = true;
|
|
5301
|
-
for (const asset of assets) {
|
|
5302
|
-
asset.paletteDistance = paletteDistance(asset.palette, reference);
|
|
5303
|
-
}
|
|
5304
|
-
} else {
|
|
5305
|
-
reference = mergePalettes(assets.map((a) => a.palette));
|
|
5306
|
-
for (const asset of assets) {
|
|
5307
|
-
const others = assets.filter((a) => a !== asset).map((a) => a.palette);
|
|
5308
|
-
asset.paletteDistance = others.length ? paletteDistance(asset.palette, mergePalettes(others)) : 0;
|
|
5309
|
-
}
|
|
5310
|
-
}
|
|
5311
|
-
assets.sort((a, b) => b.paletteDistance - a.paletteDistance);
|
|
5312
|
-
return { styleId, referenceFromStyleImages, reference, assets, missing, unreadable };
|
|
5313
|
-
}
|
|
5314
|
-
function countColors(png) {
|
|
5315
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5316
|
-
for (let i = 0; i < png.pixels.length; i += 4) {
|
|
5317
|
-
if (png.pixels[i + 3] < 128) continue;
|
|
5318
|
-
seen.add(png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2]);
|
|
5319
|
-
}
|
|
5320
|
-
return seen.size;
|
|
5387
|
+
return { png: { width: png.width, height: png.height, pixels }, changed };
|
|
5321
5388
|
}
|
|
5322
|
-
function
|
|
5323
|
-
|
|
5324
|
-
if (n < 3) return [];
|
|
5325
|
-
const mean = audit.assets.reduce((s, a) => s + a.paletteDistance, 0) / n;
|
|
5326
|
-
const variance = audit.assets.reduce((s, a) => s + (a.paletteDistance - mean) ** 2, 0) / n;
|
|
5327
|
-
const sd = Math.sqrt(variance);
|
|
5328
|
-
if (sd === 0) return [];
|
|
5329
|
-
return audit.assets.filter((a) => a.paletteDistance > mean + sigma * sd);
|
|
5330
|
-
}
|
|
5331
|
-
function evaluateAudit(audit, thresholds = {}) {
|
|
5332
|
-
const resolved = { ...thresholds, sigma: thresholds.sigma ?? 1.5 };
|
|
5333
|
-
const relative = outliers(audit, resolved.sigma);
|
|
5334
|
-
const relativeIds = new Set(relative.map((asset) => asset.id));
|
|
5335
|
-
const violations = [];
|
|
5336
|
-
for (const asset of audit.assets) {
|
|
5337
|
-
const reasons = [];
|
|
5338
|
-
if (relativeIds.has(asset.id)) {
|
|
5339
|
-
reasons.push(`palette outlier beyond ${resolved.sigma} standard deviations`);
|
|
5340
|
-
}
|
|
5341
|
-
if (resolved.maxDistance !== void 0 && asset.paletteDistance > resolved.maxDistance) {
|
|
5342
|
-
reasons.push(
|
|
5343
|
-
`palette distance ${asset.paletteDistance.toFixed(1)} exceeds ${resolved.maxDistance}`
|
|
5344
|
-
);
|
|
5345
|
-
}
|
|
5346
|
-
if (resolved.minTransparency !== void 0 && asset.transparency < resolved.minTransparency) {
|
|
5347
|
-
reasons.push(
|
|
5348
|
-
`transparency ${(asset.transparency * 100).toFixed(1)}% is below ${(resolved.minTransparency * 100).toFixed(1)}%`
|
|
5349
|
-
);
|
|
5350
|
-
}
|
|
5351
|
-
if (resolved.maxColors !== void 0 && asset.colorCount > resolved.maxColors) {
|
|
5352
|
-
reasons.push(`color count ${asset.colorCount} exceeds ${resolved.maxColors}`);
|
|
5353
|
-
}
|
|
5354
|
-
if (reasons.length) violations.push({ id: asset.id, reasons });
|
|
5355
|
-
}
|
|
5356
|
-
return {
|
|
5357
|
-
safe: violations.length === 0 && audit.missing.length === 0 && audit.unreadable.length === 0,
|
|
5358
|
-
thresholds: resolved,
|
|
5359
|
-
outliers: [...relativeIds],
|
|
5360
|
-
violations,
|
|
5361
|
-
missing: [...audit.missing],
|
|
5362
|
-
unreadable: [...audit.unreadable]
|
|
5363
|
-
};
|
|
5364
|
-
}
|
|
5365
|
-
function hex(c) {
|
|
5366
|
-
return `#${[c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
5389
|
+
function quantizeToPalette(png, palette) {
|
|
5390
|
+
return quantize(png, palette).png;
|
|
5367
5391
|
}
|
|
5368
5392
|
|
|
5369
5393
|
// src/pipeline/refine.ts
|
|
@@ -5450,48 +5474,12 @@ function parseDetection(raw) {
|
|
|
5450
5474
|
confidence: confidenceFor(consensus)
|
|
5451
5475
|
};
|
|
5452
5476
|
}
|
|
5453
|
-
function normalizePalette(values) {
|
|
5454
|
-
const colors = /* @__PURE__ */ new Map();
|
|
5455
|
-
for (const value of values) {
|
|
5456
|
-
const rgb = parseHex(value);
|
|
5457
|
-
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
5458
|
-
colors.set(hex2, { hex: hex2, ...rgb });
|
|
5459
|
-
}
|
|
5460
|
-
if (colors.size < 2 || colors.size > 256) {
|
|
5461
|
-
throw new Error(`A refinement palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
5462
|
-
}
|
|
5463
|
-
return [...colors.values()];
|
|
5464
|
-
}
|
|
5465
5477
|
function normalizeRefinementPalette(values) {
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
const pixels = Buffer.from(png.pixels);
|
|
5471
|
-
for (let i = 0; i < pixels.length; i += 4) {
|
|
5472
|
-
const alpha = pixels[i + 3];
|
|
5473
|
-
if (alpha === 0) {
|
|
5474
|
-
pixels[i] = 0;
|
|
5475
|
-
pixels[i + 1] = 0;
|
|
5476
|
-
pixels[i + 2] = 0;
|
|
5477
|
-
continue;
|
|
5478
|
-
}
|
|
5479
|
-
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
5480
|
-
let nearest = colors[0];
|
|
5481
|
-
let distance = colorDistance(pixel, nearest);
|
|
5482
|
-
for (let j = 1; j < colors.length; j++) {
|
|
5483
|
-
const candidate = colors[j];
|
|
5484
|
-
const candidateDistance = colorDistance(pixel, candidate);
|
|
5485
|
-
if (candidateDistance < distance) {
|
|
5486
|
-
nearest = candidate;
|
|
5487
|
-
distance = candidateDistance;
|
|
5488
|
-
}
|
|
5489
|
-
}
|
|
5490
|
-
pixels[i] = nearest.r;
|
|
5491
|
-
pixels[i + 1] = nearest.g;
|
|
5492
|
-
pixels[i + 2] = nearest.b;
|
|
5478
|
+
try {
|
|
5479
|
+
return normalizePalette(values).map((color) => color.hex);
|
|
5480
|
+
} catch (err) {
|
|
5481
|
+
throw new Error(`A refinement palette must contain 2\u2013256 unique colors; got ${new Set(values.map((v) => v.toLowerCase())).size}.`, { cause: err });
|
|
5493
5482
|
}
|
|
5494
|
-
return { width: png.width, height: png.height, pixels };
|
|
5495
5483
|
}
|
|
5496
5484
|
function colorCount(png) {
|
|
5497
5485
|
const colors = /* @__PURE__ */ new Set();
|
|
@@ -5601,16 +5589,16 @@ function sameFrameGrid(first, next) {
|
|
|
5601
5589
|
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;
|
|
5602
5590
|
}
|
|
5603
5591
|
async function refineAsset(options) {
|
|
5604
|
-
const source =
|
|
5605
|
-
const output =
|
|
5592
|
+
const source = import_node_path8.default.resolve(options.source);
|
|
5593
|
+
const output = import_node_path8.default.resolve(options.output);
|
|
5606
5594
|
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
5607
|
-
const record =
|
|
5595
|
+
const record = import_node_path8.default.resolve(qualityRecordPath(output));
|
|
5608
5596
|
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
5609
5597
|
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
5610
5598
|
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.");
|
|
5611
5599
|
let sourcePng;
|
|
5612
5600
|
try {
|
|
5613
|
-
sourcePng = decodePng(await (0,
|
|
5601
|
+
sourcePng = decodePng(await (0, import_promises5.readFile)(source));
|
|
5614
5602
|
} catch (error) {
|
|
5615
5603
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
5616
5604
|
}
|
|
@@ -5627,8 +5615,8 @@ async function refineAsset(options) {
|
|
|
5627
5615
|
);
|
|
5628
5616
|
}
|
|
5629
5617
|
}
|
|
5630
|
-
const temp = await (0,
|
|
5631
|
-
const recovered =
|
|
5618
|
+
const temp = await (0, import_promises5.mkdtemp)(import_node_path8.default.join((0, import_node_os2.tmpdir)(), "pixelkiln-refine-"));
|
|
5619
|
+
const recovered = import_node_path8.default.join(temp, "native.png");
|
|
5632
5620
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
5633
5621
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
5634
5622
|
try {
|
|
@@ -5649,7 +5637,7 @@ async function refineAsset(options) {
|
|
|
5649
5637
|
);
|
|
5650
5638
|
}
|
|
5651
5639
|
const detection = parseDetection(firstJsonObject(stdout));
|
|
5652
|
-
const native = decodePng(await (0,
|
|
5640
|
+
const native = decodePng(await (0, import_promises5.readFile)(recovered));
|
|
5653
5641
|
const quantized = quantizeToPalette(native, palette);
|
|
5654
5642
|
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
5655
5643
|
const audit = auditRefinement(
|
|
@@ -5699,7 +5687,7 @@ async function refineAsset(options) {
|
|
|
5699
5687
|
);
|
|
5700
5688
|
return { source, output, record, detection, palette, audit };
|
|
5701
5689
|
} finally {
|
|
5702
|
-
await (0,
|
|
5690
|
+
await (0, import_promises5.rm)(temp, { recursive: true, force: true });
|
|
5703
5691
|
}
|
|
5704
5692
|
}
|
|
5705
5693
|
async function refineFrameSet(options) {
|
|
@@ -5713,18 +5701,18 @@ async function refineFrameSet(options) {
|
|
|
5713
5701
|
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
5714
5702
|
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
5715
5703
|
}
|
|
5716
|
-
const output =
|
|
5717
|
-
const record =
|
|
5704
|
+
const output = import_node_path8.default.resolve(options.output);
|
|
5705
|
+
const record = import_node_path8.default.resolve(qualityRecordPath(output));
|
|
5718
5706
|
const palette = normalizeRefinementPalette(options.palette);
|
|
5719
5707
|
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
5720
5708
|
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
5721
5709
|
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
5722
5710
|
}
|
|
5723
|
-
const sourceSet = new Set(options.sources.map((source) =>
|
|
5711
|
+
const sourceSet = new Set(options.sources.map((source) => import_node_path8.default.resolve(source.path)));
|
|
5724
5712
|
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
5725
5713
|
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
5726
5714
|
}
|
|
5727
|
-
const temp = await (0,
|
|
5715
|
+
const temp = await (0, import_promises5.mkdtemp)(import_node_path8.default.join((0, import_node_os2.tmpdir)(), "pixelkiln-refine-frames-"));
|
|
5728
5716
|
try {
|
|
5729
5717
|
const frames = [];
|
|
5730
5718
|
const outputs = [];
|
|
@@ -5732,8 +5720,8 @@ async function refineFrameSet(options) {
|
|
|
5732
5720
|
const sourcePaths = [];
|
|
5733
5721
|
let firstOptions = null;
|
|
5734
5722
|
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
5735
|
-
const source =
|
|
5736
|
-
const temporaryOutput =
|
|
5723
|
+
const source = import_node_path8.default.resolve(sourceEntry.path);
|
|
5724
|
+
const temporaryOutput = import_node_path8.default.join(temp, `${String(index).padStart(2, "0")}.png`);
|
|
5737
5725
|
const refined = await refineAsset({
|
|
5738
5726
|
source,
|
|
5739
5727
|
output: temporaryOutput,
|
|
@@ -5765,7 +5753,7 @@ async function refineFrameSet(options) {
|
|
|
5765
5753
|
});
|
|
5766
5754
|
outputs.push({
|
|
5767
5755
|
path: finalOutputs[index],
|
|
5768
|
-
data: await (0,
|
|
5756
|
+
data: await (0, import_promises5.readFile)(temporaryOutput)
|
|
5769
5757
|
});
|
|
5770
5758
|
}
|
|
5771
5759
|
const first = frames[0];
|
|
@@ -5808,23 +5796,23 @@ async function refineFrameSet(options) {
|
|
|
5808
5796
|
);
|
|
5809
5797
|
return {
|
|
5810
5798
|
sources: sourcePaths,
|
|
5811
|
-
outputs: outputs.map((item) =>
|
|
5799
|
+
outputs: outputs.map((item) => import_node_path8.default.resolve(item.path)),
|
|
5812
5800
|
record,
|
|
5813
5801
|
frames,
|
|
5814
5802
|
palette,
|
|
5815
5803
|
audit
|
|
5816
5804
|
};
|
|
5817
5805
|
} finally {
|
|
5818
|
-
await (0,
|
|
5806
|
+
await (0, import_promises5.rm)(temp, { recursive: true, force: true });
|
|
5819
5807
|
}
|
|
5820
5808
|
}
|
|
5821
5809
|
async function checkQualityRecord(recordPath) {
|
|
5822
|
-
const record =
|
|
5810
|
+
const record = import_node_path8.default.resolve(recordPath);
|
|
5823
5811
|
const manifest = await readArtifactBundleManifest(record);
|
|
5824
5812
|
const options = parseRecordOptions(manifest, record);
|
|
5825
5813
|
const verification = await verifyArtifactBundle(record);
|
|
5826
|
-
const sources = manifest.sources.map((source2) =>
|
|
5827
|
-
const outputs = manifest.outputs.map((output2) =>
|
|
5814
|
+
const sources = manifest.sources.map((source2) => import_node_path8.default.resolve(import_node_path8.default.dirname(record), source2.path));
|
|
5815
|
+
const outputs = manifest.outputs.map((output2) => import_node_path8.default.resolve(import_node_path8.default.dirname(record), output2.path));
|
|
5828
5816
|
const source = sources[0] ?? "";
|
|
5829
5817
|
const output = outputs[0] ?? "";
|
|
5830
5818
|
const expectedCount = options.frameSet?.count ?? 1;
|
|
@@ -5857,7 +5845,7 @@ async function checkQualityRecord(recordPath) {
|
|
|
5857
5845
|
async function approveQualityRecord(recordPath, approval) {
|
|
5858
5846
|
const reviewer = approval.reviewer.trim();
|
|
5859
5847
|
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
5860
|
-
const record =
|
|
5848
|
+
const record = import_node_path8.default.resolve(recordPath);
|
|
5861
5849
|
const manifest = await readArtifactBundleManifest(record);
|
|
5862
5850
|
const options = parseRecordOptions(manifest, record);
|
|
5863
5851
|
const verification = await verifyArtifactBundle(record);
|
|
@@ -5878,14 +5866,14 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5878
5866
|
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
5879
5867
|
);
|
|
5880
5868
|
}
|
|
5881
|
-
const root =
|
|
5869
|
+
const root = import_node_path8.default.dirname(record);
|
|
5882
5870
|
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
5883
|
-
const outputPath =
|
|
5884
|
-
return { path: outputPath, data: await (0,
|
|
5871
|
+
const outputPath = import_node_path8.default.resolve(root, entry.path);
|
|
5872
|
+
return { path: outputPath, data: await (0, import_promises5.readFile)(outputPath) };
|
|
5885
5873
|
}));
|
|
5886
5874
|
const sources = manifest.sources.map((source) => ({
|
|
5887
5875
|
...source,
|
|
5888
|
-
path:
|
|
5876
|
+
path: import_node_path8.default.resolve(root, source.path)
|
|
5889
5877
|
}));
|
|
5890
5878
|
const approved = {
|
|
5891
5879
|
...options,
|
|
@@ -5917,8 +5905,8 @@ async function qualitySource(spec, lock) {
|
|
|
5917
5905
|
if (spec.generator === "frames") {
|
|
5918
5906
|
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
5919
5907
|
}
|
|
5920
|
-
const source =
|
|
5921
|
-
return (0,
|
|
5908
|
+
const source = import_node_path9.default.resolve(spec.root, spec.source);
|
|
5909
|
+
return (0, import_node_fs7.existsSync)(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
5922
5910
|
}
|
|
5923
5911
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
5924
5912
|
if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
|
|
@@ -5947,7 +5935,7 @@ async function qualitySource(spec, lock) {
|
|
|
5947
5935
|
}
|
|
5948
5936
|
if (role) roles.add(role);
|
|
5949
5937
|
const source = currentEntryOutputPath(entry, spec, index);
|
|
5950
|
-
if (!(0,
|
|
5938
|
+
if (!(0, import_node_fs7.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
|
|
5951
5939
|
if (await sha256File(source) !== output.sha256) {
|
|
5952
5940
|
return { items: null, reason: "raw provider output was modified after download" };
|
|
5953
5941
|
}
|
|
@@ -5966,7 +5954,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5966
5954
|
}
|
|
5967
5955
|
const sourcePaths = source.items.map((item) => item.path);
|
|
5968
5956
|
const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
|
|
5969
|
-
if (!(0,
|
|
5957
|
+
if (!(0, import_node_fs7.existsSync)(record)) {
|
|
5970
5958
|
return {
|
|
5971
5959
|
key,
|
|
5972
5960
|
state: "needs-refinement",
|
|
@@ -5984,7 +5972,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5984
5972
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
5985
5973
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
5986
5974
|
const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
|
|
5987
|
-
const contractChanged = JSON.stringify(check.sources.map((item) =>
|
|
5975
|
+
const contractChanged = JSON.stringify(check.sources.map((item) => import_node_path9.default.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => import_node_path9.default.resolve(item))) || JSON.stringify(check.outputs.map((item) => import_node_path9.default.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => import_node_path9.default.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);
|
|
5988
5976
|
if (contractChanged) {
|
|
5989
5977
|
return {
|
|
5990
5978
|
key,
|
|
@@ -6146,9 +6134,38 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
|
|
|
6146
6134
|
return sources;
|
|
6147
6135
|
}
|
|
6148
6136
|
|
|
6137
|
+
// src/pipeline/postprocess.ts
|
|
6138
|
+
function postprocessFor(spec) {
|
|
6139
|
+
if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
|
|
6140
|
+
return {
|
|
6141
|
+
palette: {
|
|
6142
|
+
colors: normalizePalette(spec.palette).map((color) => color.hex),
|
|
6143
|
+
dither: "none",
|
|
6144
|
+
distance: "redmean"
|
|
6145
|
+
}
|
|
6146
|
+
};
|
|
6147
|
+
}
|
|
6148
|
+
function samePostprocess(a, b) {
|
|
6149
|
+
const pa = a?.palette;
|
|
6150
|
+
const pb = b?.palette;
|
|
6151
|
+
if (!pa && !pb) return true;
|
|
6152
|
+
if (!pa || !pb) return false;
|
|
6153
|
+
return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
|
|
6154
|
+
}
|
|
6155
|
+
function postprocessCurrent(entry, spec) {
|
|
6156
|
+
return samePostprocess(entry.postprocess, postprocessFor(spec));
|
|
6157
|
+
}
|
|
6158
|
+
function applyPostprocess(bytes, mediaType, record) {
|
|
6159
|
+
if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
|
|
6160
|
+
const png = decodePng(bytes);
|
|
6161
|
+
const { png: snapped, changed } = quantize(png, record.palette.colors);
|
|
6162
|
+
if (!changed) return { bytes, changed: false };
|
|
6163
|
+
return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
|
|
6164
|
+
}
|
|
6165
|
+
|
|
6149
6166
|
// src/pipeline/revision.ts
|
|
6150
|
-
var
|
|
6151
|
-
var
|
|
6167
|
+
var import_node_fs8 = require("fs");
|
|
6168
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
6152
6169
|
async function inspectRevisionReadiness(spec, lock) {
|
|
6153
6170
|
if (!spec.revision) return null;
|
|
6154
6171
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
@@ -6163,14 +6180,14 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
6163
6180
|
reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
|
|
6164
6181
|
};
|
|
6165
6182
|
}
|
|
6166
|
-
if (!revision.sourceSha256 || !(0,
|
|
6183
|
+
if (!revision.sourceSha256 || !(0, import_node_fs8.existsSync)(revision.sourceFile)) {
|
|
6167
6184
|
return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
|
|
6168
6185
|
}
|
|
6169
6186
|
if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
|
|
6170
6187
|
return { ready: false, reason: "revision source changed after the manifest was resolved" };
|
|
6171
6188
|
}
|
|
6172
6189
|
if (revision.maskFile) {
|
|
6173
|
-
if (!revision.maskSha256 || !(0,
|
|
6190
|
+
if (!revision.maskSha256 || !(0, import_node_fs8.existsSync)(revision.maskFile)) {
|
|
6174
6191
|
return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
|
|
6175
6192
|
}
|
|
6176
6193
|
if (await sha256File(revision.maskFile) !== revision.maskSha256) {
|
|
@@ -6200,8 +6217,8 @@ async function inspectParent(spec, lock, seen) {
|
|
|
6200
6217
|
};
|
|
6201
6218
|
}
|
|
6202
6219
|
if (spec.source) {
|
|
6203
|
-
const source =
|
|
6204
|
-
return (0,
|
|
6220
|
+
const source = import_node_path10.default.resolve(spec.root, spec.source);
|
|
6221
|
+
return (0, import_node_fs8.existsSync)(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
|
|
6205
6222
|
}
|
|
6206
6223
|
const entry = lock.entries[key];
|
|
6207
6224
|
if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
|
|
@@ -6215,7 +6232,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
6215
6232
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
6216
6233
|
const output = entry.outputs[index];
|
|
6217
6234
|
const file = currentEntryOutputPath(entry, spec, index);
|
|
6218
|
-
if (!(0,
|
|
6235
|
+
if (!(0, import_node_fs8.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
|
|
6219
6236
|
if (await sha256File(file) !== output.sha256) {
|
|
6220
6237
|
return { ready: false, reason: `parent output was modified: ${file}` };
|
|
6221
6238
|
}
|
|
@@ -6244,7 +6261,7 @@ function resumeActions(specs, lock) {
|
|
|
6244
6261
|
const entry = lock.entries[key];
|
|
6245
6262
|
if (!entry || entry.specHash !== spec.specHash) continue;
|
|
6246
6263
|
if (entry.submissionComplete === false) continue;
|
|
6247
|
-
const command = resumeCommandForStatus(entry.status);
|
|
6264
|
+
const command = resumeCommandForStatus(entry.status) ?? (entry.status === "downloaded" && entry.outputs.length && !postprocessCurrent(entry, spec) ? "fetch" : null);
|
|
6248
6265
|
if (!command) continue;
|
|
6249
6266
|
if (command === "poll" && !entry.jobId) continue;
|
|
6250
6267
|
if (command === "pick" && !entry.reviewObjectId) continue;
|
|
@@ -6270,7 +6287,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6270
6287
|
state = "missing";
|
|
6271
6288
|
reason = "--force";
|
|
6272
6289
|
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
6273
|
-
if ((0,
|
|
6290
|
+
if ((0, import_node_fs9.existsSync)(import_node_path11.default.resolve(spec.root, spec.source))) {
|
|
6274
6291
|
state = "ok";
|
|
6275
6292
|
reason = `placed from ${spec.source}; not generated`;
|
|
6276
6293
|
} else {
|
|
@@ -6278,7 +6295,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6278
6295
|
reason = `declared source is missing: ${spec.source}`;
|
|
6279
6296
|
}
|
|
6280
6297
|
} else if (!entry) {
|
|
6281
|
-
if ((0,
|
|
6298
|
+
if ((0, import_node_fs9.existsSync)(spec.outFile)) {
|
|
6282
6299
|
state = "untracked";
|
|
6283
6300
|
reason = "file on disk, no provenance recorded";
|
|
6284
6301
|
} else {
|
|
@@ -6310,12 +6327,15 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6310
6327
|
} else if (entry.status !== "downloaded") {
|
|
6311
6328
|
state = "in-flight";
|
|
6312
6329
|
reason = `awaiting ${entry.status}`;
|
|
6313
|
-
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0,
|
|
6330
|
+
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index)))) {
|
|
6314
6331
|
state = "orphaned";
|
|
6315
|
-
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0,
|
|
6332
|
+
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !(0, import_node_fs9.existsSync)(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
|
|
6316
6333
|
} else if (await anyOutputModified(entry, spec)) {
|
|
6317
6334
|
state = "orphaned";
|
|
6318
6335
|
reason = "output modified since download";
|
|
6336
|
+
} else if (!postprocessCurrent(entry, spec)) {
|
|
6337
|
+
state = "recoverable";
|
|
6338
|
+
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)";
|
|
6319
6339
|
} else {
|
|
6320
6340
|
state = "ok";
|
|
6321
6341
|
reason = "up to date";
|
|
@@ -6378,13 +6398,13 @@ function summarize(plan) {
|
|
|
6378
6398
|
|
|
6379
6399
|
// src/project.ts
|
|
6380
6400
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
6381
|
-
const absolute =
|
|
6382
|
-
const root =
|
|
6401
|
+
const absolute = import_node_path12.default.resolve(manifestPath);
|
|
6402
|
+
const root = import_node_path12.default.dirname(absolute);
|
|
6383
6403
|
if (opts.env !== false) {
|
|
6384
6404
|
loadEnvFiles(root);
|
|
6385
|
-
if (
|
|
6405
|
+
if (import_node_path12.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
6386
6406
|
}
|
|
6387
|
-
const lockPath =
|
|
6407
|
+
const lockPath = import_node_path12.default.resolve(opts.lockPath ?? import_node_path12.default.join(root, "pixelkiln.lock.json"));
|
|
6388
6408
|
const loaded = await loadManifest(absolute);
|
|
6389
6409
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
6390
6410
|
const lock = await loadLock(lockPath);
|
|
@@ -6451,9 +6471,9 @@ function shouldPersistSourceUrl(value) {
|
|
|
6451
6471
|
}
|
|
6452
6472
|
|
|
6453
6473
|
// src/recipes.ts
|
|
6454
|
-
var
|
|
6455
|
-
var
|
|
6456
|
-
var
|
|
6474
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
6475
|
+
var import_node_fs10 = require("fs");
|
|
6476
|
+
var import_promises6 = require("fs/promises");
|
|
6457
6477
|
var import_node_url = require("url");
|
|
6458
6478
|
var import_zod3 = require("zod");
|
|
6459
6479
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
@@ -6462,7 +6482,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
6462
6482
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
6463
6483
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
6464
6484
|
var PortablePathSchema = import_zod3.z.string().min(1).refine((value) => {
|
|
6465
|
-
if (value.includes("\\") ||
|
|
6485
|
+
if (value.includes("\\") || import_node_path13.default.posix.isAbsolute(value)) return false;
|
|
6466
6486
|
const parts = value.split("/");
|
|
6467
6487
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
6468
6488
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -6638,10 +6658,10 @@ function message3(error) {
|
|
|
6638
6658
|
return error instanceof Error ? error.message : String(error);
|
|
6639
6659
|
}
|
|
6640
6660
|
async function readRecipeFile(recipePath, bundled) {
|
|
6641
|
-
const absolute =
|
|
6661
|
+
const absolute = import_node_path13.default.resolve(recipePath);
|
|
6642
6662
|
let raw;
|
|
6643
6663
|
try {
|
|
6644
|
-
raw = JSON.parse(await (0,
|
|
6664
|
+
raw = JSON.parse(await (0, import_promises6.readFile)(absolute, "utf8"));
|
|
6645
6665
|
} catch (error) {
|
|
6646
6666
|
throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
|
|
6647
6667
|
}
|
|
@@ -6652,13 +6672,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
6652
6672
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
6653
6673
|
);
|
|
6654
6674
|
}
|
|
6655
|
-
return { recipe: parsed.data, path: absolute, dir:
|
|
6675
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path13.default.dirname(absolute), bundled };
|
|
6656
6676
|
}
|
|
6657
6677
|
async function walkRecipeFiles(root) {
|
|
6658
|
-
if (!(0,
|
|
6678
|
+
if (!(0, import_node_fs10.existsSync)(root)) return [];
|
|
6659
6679
|
const out = [];
|
|
6660
|
-
for (const entry of await (0,
|
|
6661
|
-
const resolved =
|
|
6680
|
+
for (const entry of await (0, import_promises6.readdir)(root, { withFileTypes: true })) {
|
|
6681
|
+
const resolved = import_node_path13.default.join(root, entry.name);
|
|
6662
6682
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
6663
6683
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
6664
6684
|
}
|
|
@@ -6669,7 +6689,7 @@ function bundledRecipeRoot() {
|
|
|
6669
6689
|
}
|
|
6670
6690
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
6671
6691
|
const recipes = await Promise.all(
|
|
6672
|
-
(await walkRecipeFiles(
|
|
6692
|
+
(await walkRecipeFiles(import_node_path13.default.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
6673
6693
|
);
|
|
6674
6694
|
return recipes.sort(
|
|
6675
6695
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -6692,9 +6712,9 @@ function parseSelector(selector) {
|
|
|
6692
6712
|
return { id: match[1], version: match[2] };
|
|
6693
6713
|
}
|
|
6694
6714
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
6695
|
-
const local =
|
|
6696
|
-
if ((0,
|
|
6697
|
-
const recipePath =
|
|
6715
|
+
const local = import_node_path13.default.resolve(target);
|
|
6716
|
+
if ((0, import_node_fs10.existsSync)(local)) {
|
|
6717
|
+
const recipePath = import_node_path13.default.basename(local) === RECIPE_FILE ? local : import_node_path13.default.join(local, RECIPE_FILE);
|
|
6698
6718
|
return readRecipeFile(recipePath, false);
|
|
6699
6719
|
}
|
|
6700
6720
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -6708,8 +6728,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
6708
6728
|
return matches[0];
|
|
6709
6729
|
}
|
|
6710
6730
|
async function verifyFile(root, file) {
|
|
6711
|
-
const absolute =
|
|
6712
|
-
if (!(0,
|
|
6731
|
+
const absolute = import_node_path13.default.resolve(root, ...file.path.split("/"));
|
|
6732
|
+
if (!(0, import_node_fs10.existsSync)(absolute)) {
|
|
6713
6733
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
6714
6734
|
}
|
|
6715
6735
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -6722,9 +6742,9 @@ async function verifyFile(root, file) {
|
|
|
6722
6742
|
}
|
|
6723
6743
|
async function verifyRecipe(target, options = {}) {
|
|
6724
6744
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
6725
|
-
const actualIntegrity = recipeDigest(loaded.
|
|
6745
|
+
const actualIntegrity = recipeDigest(loaded.authored);
|
|
6726
6746
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
6727
|
-
const modelRoot = options.modelRoot ?
|
|
6747
|
+
const modelRoot = options.modelRoot ? import_node_path13.default.resolve(options.modelRoot) : null;
|
|
6728
6748
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
6729
6749
|
...await verifyFile(modelRoot, model),
|
|
6730
6750
|
source: model.source,
|
|
@@ -6758,7 +6778,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
6758
6778
|
};
|
|
6759
6779
|
}
|
|
6760
6780
|
function renderStyle(style, destination, cwd) {
|
|
6761
|
-
const portableDir =
|
|
6781
|
+
const portableDir = import_node_path13.default.relative(cwd, destination).split(import_node_path13.default.sep).join("/") || ".";
|
|
6762
6782
|
const replace = (value) => {
|
|
6763
6783
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
6764
6784
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -6770,28 +6790,29 @@ function renderStyle(style, destination, cwd) {
|
|
|
6770
6790
|
return replace(style);
|
|
6771
6791
|
}
|
|
6772
6792
|
async function installRecipe(target, options = {}) {
|
|
6773
|
-
const cwd =
|
|
6793
|
+
const cwd = import_node_path13.default.resolve(options.cwd ?? process.cwd());
|
|
6774
6794
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
6775
6795
|
const verification = await verifyRecipe(loaded.path);
|
|
6776
6796
|
if (!verification.ok) {
|
|
6777
6797
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
6778
6798
|
}
|
|
6779
|
-
const destination = options.out ?
|
|
6799
|
+
const destination = options.out ? import_node_path13.default.resolve(cwd, options.out) : import_node_path13.default.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
6780
6800
|
const sources = [
|
|
6781
6801
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
6782
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
6802
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: import_node_path13.default.join(loaded.dir, ...file.path.split("/")) }))
|
|
6783
6803
|
];
|
|
6784
6804
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
6785
|
-
path:
|
|
6786
|
-
data: await (0,
|
|
6805
|
+
path: import_node_path13.default.join(destination, ...file.path.split("/")),
|
|
6806
|
+
data: await (0, import_promises6.readFile)(file.source)
|
|
6787
6807
|
})));
|
|
6788
6808
|
if (!options.force) {
|
|
6789
6809
|
for (const file of files) {
|
|
6790
|
-
if (!(0,
|
|
6791
|
-
const current = await (0,
|
|
6810
|
+
if (!(0, import_node_fs10.existsSync)(file.path)) continue;
|
|
6811
|
+
const current = await (0, import_promises6.readFile)(file.path);
|
|
6792
6812
|
if (!current.equals(file.data)) {
|
|
6793
|
-
throw new
|
|
6794
|
-
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files
|
|
6813
|
+
throw new OverwriteRefusedError(
|
|
6814
|
+
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
6815
|
+
[file.path]
|
|
6795
6816
|
);
|
|
6796
6817
|
}
|
|
6797
6818
|
}
|
|
@@ -6810,6 +6831,152 @@ async function installRecipe(target, options = {}) {
|
|
|
6810
6831
|
};
|
|
6811
6832
|
}
|
|
6812
6833
|
|
|
6834
|
+
// src/pipeline/audit.ts
|
|
6835
|
+
var import_promises7 = require("fs/promises");
|
|
6836
|
+
var import_node_fs11 = require("fs");
|
|
6837
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
6838
|
+
function paletteDistance(asset, reference) {
|
|
6839
|
+
if (!asset.length || !reference.length) return 0;
|
|
6840
|
+
let total = 0;
|
|
6841
|
+
let weight = 0;
|
|
6842
|
+
for (const color of asset) {
|
|
6843
|
+
let nearest = Infinity;
|
|
6844
|
+
for (const ref of reference) nearest = Math.min(nearest, colorDistance(color, ref));
|
|
6845
|
+
total += nearest * color.weight;
|
|
6846
|
+
weight += color.weight;
|
|
6847
|
+
}
|
|
6848
|
+
return weight === 0 ? 0 : total / weight;
|
|
6849
|
+
}
|
|
6850
|
+
function mergePalettes(palettes, topN = 24) {
|
|
6851
|
+
const totals = /* @__PURE__ */ new Map();
|
|
6852
|
+
for (const palette of palettes) {
|
|
6853
|
+
for (const color of palette) {
|
|
6854
|
+
const key = `${color.r},${color.g},${color.b}`;
|
|
6855
|
+
const existing = totals.get(key);
|
|
6856
|
+
if (existing) existing.weight += color.weight;
|
|
6857
|
+
else totals.set(key, { ...color });
|
|
6858
|
+
}
|
|
6859
|
+
}
|
|
6860
|
+
const merged = [...totals.values()].sort((a, b) => b.weight - a.weight).slice(0, topN);
|
|
6861
|
+
const sum = merged.reduce((s, c) => s + c.weight, 0) || 1;
|
|
6862
|
+
return merged.map((c) => ({ ...c, weight: c.weight / sum }));
|
|
6863
|
+
}
|
|
6864
|
+
async function auditStyle(loaded, specs, styleId, lock) {
|
|
6865
|
+
const style = loaded.manifest.styles[styleId];
|
|
6866
|
+
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
6867
|
+
const mine = specs.filter((s) => s.styleId === styleId);
|
|
6868
|
+
const assets = [];
|
|
6869
|
+
const missing = [];
|
|
6870
|
+
const unreadable = [];
|
|
6871
|
+
for (const spec of mine) {
|
|
6872
|
+
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
6873
|
+
if (!(0, import_node_fs11.existsSync)(output.absolutePath)) {
|
|
6874
|
+
missing.push(output.id);
|
|
6875
|
+
continue;
|
|
6876
|
+
}
|
|
6877
|
+
try {
|
|
6878
|
+
const png = decodePng(await (0, import_promises7.readFile)(output.absolutePath));
|
|
6879
|
+
const palette = extractPalette(png, 12);
|
|
6880
|
+
assets.push({
|
|
6881
|
+
assetId: spec.assetId,
|
|
6882
|
+
...output.role ? { outputRole: output.role } : {},
|
|
6883
|
+
id: output.id,
|
|
6884
|
+
file: output.absolutePath,
|
|
6885
|
+
width: png.width,
|
|
6886
|
+
height: png.height,
|
|
6887
|
+
paletteDistance: 0,
|
|
6888
|
+
// filled once the reference is known
|
|
6889
|
+
transparency: transparencyRatio(png),
|
|
6890
|
+
colorCount: countColors(png),
|
|
6891
|
+
palette
|
|
6892
|
+
});
|
|
6893
|
+
} catch (err) {
|
|
6894
|
+
unreadable.push(`${output.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6895
|
+
}
|
|
6896
|
+
}
|
|
6897
|
+
}
|
|
6898
|
+
let reference = [];
|
|
6899
|
+
let referenceFromStyleImages = false;
|
|
6900
|
+
const refPalettes = [];
|
|
6901
|
+
for (const img of style.styleImages) {
|
|
6902
|
+
const abs = import_node_path14.default.resolve(loaded.root, img.path);
|
|
6903
|
+
if (!(0, import_node_fs11.existsSync)(abs)) continue;
|
|
6904
|
+
try {
|
|
6905
|
+
refPalettes.push(extractPalette(decodePng(await (0, import_promises7.readFile)(abs)), 12));
|
|
6906
|
+
} catch {
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
if (refPalettes.length) {
|
|
6910
|
+
reference = mergePalettes(refPalettes);
|
|
6911
|
+
referenceFromStyleImages = true;
|
|
6912
|
+
for (const asset of assets) {
|
|
6913
|
+
asset.paletteDistance = paletteDistance(asset.palette, reference);
|
|
6914
|
+
}
|
|
6915
|
+
} else {
|
|
6916
|
+
reference = mergePalettes(assets.map((a) => a.palette));
|
|
6917
|
+
for (const asset of assets) {
|
|
6918
|
+
const others = assets.filter((a) => a !== asset).map((a) => a.palette);
|
|
6919
|
+
asset.paletteDistance = others.length ? paletteDistance(asset.palette, mergePalettes(others)) : 0;
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
assets.sort((a, b) => b.paletteDistance - a.paletteDistance);
|
|
6923
|
+
return { styleId, referenceFromStyleImages, reference, assets, missing, unreadable };
|
|
6924
|
+
}
|
|
6925
|
+
function countColors(png) {
|
|
6926
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6927
|
+
for (let i = 0; i < png.pixels.length; i += 4) {
|
|
6928
|
+
if (png.pixels[i + 3] < 128) continue;
|
|
6929
|
+
seen.add(png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2]);
|
|
6930
|
+
}
|
|
6931
|
+
return seen.size;
|
|
6932
|
+
}
|
|
6933
|
+
function outliers(audit, sigma = 1.5) {
|
|
6934
|
+
const n = audit.assets.length;
|
|
6935
|
+
if (n < 3) return [];
|
|
6936
|
+
const mean = audit.assets.reduce((s, a) => s + a.paletteDistance, 0) / n;
|
|
6937
|
+
const variance = audit.assets.reduce((s, a) => s + (a.paletteDistance - mean) ** 2, 0) / n;
|
|
6938
|
+
const sd = Math.sqrt(variance);
|
|
6939
|
+
if (sd === 0) return [];
|
|
6940
|
+
return audit.assets.filter((a) => a.paletteDistance > mean + sigma * sd);
|
|
6941
|
+
}
|
|
6942
|
+
function evaluateAudit(audit, thresholds = {}) {
|
|
6943
|
+
const resolved = { ...thresholds, sigma: thresholds.sigma ?? 1.5 };
|
|
6944
|
+
const relative = outliers(audit, resolved.sigma);
|
|
6945
|
+
const relativeIds = new Set(relative.map((asset) => asset.id));
|
|
6946
|
+
const violations = [];
|
|
6947
|
+
for (const asset of audit.assets) {
|
|
6948
|
+
const reasons = [];
|
|
6949
|
+
if (relativeIds.has(asset.id)) {
|
|
6950
|
+
reasons.push(`palette outlier beyond ${resolved.sigma} standard deviations`);
|
|
6951
|
+
}
|
|
6952
|
+
if (resolved.maxDistance !== void 0 && asset.paletteDistance > resolved.maxDistance) {
|
|
6953
|
+
reasons.push(
|
|
6954
|
+
`palette distance ${asset.paletteDistance.toFixed(1)} exceeds ${resolved.maxDistance}`
|
|
6955
|
+
);
|
|
6956
|
+
}
|
|
6957
|
+
if (resolved.minTransparency !== void 0 && asset.transparency < resolved.minTransparency) {
|
|
6958
|
+
reasons.push(
|
|
6959
|
+
`transparency ${(asset.transparency * 100).toFixed(1)}% is below ${(resolved.minTransparency * 100).toFixed(1)}%`
|
|
6960
|
+
);
|
|
6961
|
+
}
|
|
6962
|
+
if (resolved.maxColors !== void 0 && asset.colorCount > resolved.maxColors) {
|
|
6963
|
+
reasons.push(`color count ${asset.colorCount} exceeds ${resolved.maxColors}`);
|
|
6964
|
+
}
|
|
6965
|
+
if (reasons.length) violations.push({ id: asset.id, reasons });
|
|
6966
|
+
}
|
|
6967
|
+
return {
|
|
6968
|
+
safe: violations.length === 0 && audit.missing.length === 0 && audit.unreadable.length === 0,
|
|
6969
|
+
thresholds: resolved,
|
|
6970
|
+
outliers: [...relativeIds],
|
|
6971
|
+
violations,
|
|
6972
|
+
missing: [...audit.missing],
|
|
6973
|
+
unreadable: [...audit.unreadable]
|
|
6974
|
+
};
|
|
6975
|
+
}
|
|
6976
|
+
function hex(c) {
|
|
6977
|
+
return `#${[c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
6978
|
+
}
|
|
6979
|
+
|
|
6813
6980
|
// src/pipeline/quality-regression.ts
|
|
6814
6981
|
var import_node_path15 = __toESM(require("path"), 1);
|
|
6815
6982
|
var import_node_fs12 = require("fs");
|
|
@@ -7058,7 +7225,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
7058
7225
|
if ((0, import_node_fs12.existsSync)(absolute) && !options.force) {
|
|
7059
7226
|
const current = await (0, import_promises8.readFile)(absolute);
|
|
7060
7227
|
if (!current.equals(data)) {
|
|
7061
|
-
throw new
|
|
7228
|
+
throw new OverwriteRefusedError(
|
|
7229
|
+
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
7230
|
+
[absolute]
|
|
7231
|
+
);
|
|
7062
7232
|
}
|
|
7063
7233
|
}
|
|
7064
7234
|
const result = await writeArtifactBundle([{ path: absolute, data }]);
|
|
@@ -7279,8 +7449,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7279
7449
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
7280
7450
|
if (e.status !== "downloaded") return false;
|
|
7281
7451
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
7282
|
-
if (!opts.repair) return false;
|
|
7283
7452
|
const spec = specByKey.get(key);
|
|
7453
|
+
if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
|
|
7454
|
+
return Boolean(opts.force) || e.outputs.every((output) => {
|
|
7455
|
+
const file = resolveOutputPath(output.path, spec.root);
|
|
7456
|
+
return (0, import_node_fs14.existsSync)(file) && sha256((0, import_node_fs14.readFileSync)(file)) === output.sha256;
|
|
7457
|
+
});
|
|
7458
|
+
}
|
|
7459
|
+
if (!opts.repair) return false;
|
|
7284
7460
|
if (e.outputs.length === 0 || !spec) return true;
|
|
7285
7461
|
return e.outputs.some((output) => {
|
|
7286
7462
|
const file = resolveOutputPath(output.path, spec.root);
|
|
@@ -7319,6 +7495,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7319
7495
|
}
|
|
7320
7496
|
const outputs = [];
|
|
7321
7497
|
let wrote = false;
|
|
7498
|
+
const wanted = postprocessFor(spec);
|
|
7499
|
+
const reapply = entry.status === "downloaded" && !postprocessCurrent(entry, spec);
|
|
7322
7500
|
try {
|
|
7323
7501
|
for (let index = 0; index < sources.length; index++) {
|
|
7324
7502
|
const source = sources[index];
|
|
@@ -7326,20 +7504,24 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7326
7504
|
(o) => source.role ? o.role === source.role : !o.role && sources.length === 1
|
|
7327
7505
|
);
|
|
7328
7506
|
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
|
|
7507
|
+
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
7508
|
+
const recordedRaw = recorded?.raw ?? recorded?.sha256;
|
|
7329
7509
|
let fresh = null;
|
|
7330
7510
|
if (opts.refresh) {
|
|
7331
7511
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
7332
7512
|
fresh = await provider.download(source.url);
|
|
7333
|
-
if (recorded && sha256(fresh) ===
|
|
7513
|
+
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
7334
7514
|
log(` current ${import_node_path17.default.relative(process.cwd(), target)}`);
|
|
7335
7515
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
7336
7516
|
continue;
|
|
7337
7517
|
}
|
|
7338
7518
|
}
|
|
7519
|
+
let raw = null;
|
|
7520
|
+
let final = null;
|
|
7339
7521
|
if ((0, import_node_fs14.existsSync)(target)) {
|
|
7340
7522
|
const currentHash = await sha256File(target);
|
|
7341
7523
|
if (recorded && currentHash === recorded.sha256) {
|
|
7342
|
-
if (!fresh) {
|
|
7524
|
+
if (!fresh && !reapply) {
|
|
7343
7525
|
if (cacheDir) {
|
|
7344
7526
|
await cacheMedia(
|
|
7345
7527
|
cacheDir,
|
|
@@ -7351,7 +7533,12 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7351
7533
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
7352
7534
|
continue;
|
|
7353
7535
|
}
|
|
7354
|
-
|
|
7536
|
+
if (reapply && !fresh) {
|
|
7537
|
+
if (!recorded.raw) raw = await (0, import_promises10.readFile)(target);
|
|
7538
|
+
log(` reapply ${import_node_path17.default.relative(process.cwd(), target)}`);
|
|
7539
|
+
} else {
|
|
7540
|
+
log(` changed ${import_node_path17.default.relative(process.cwd(), target)} (upstream)`);
|
|
7541
|
+
}
|
|
7355
7542
|
} else {
|
|
7356
7543
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
7357
7544
|
(output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
|
|
@@ -7373,26 +7560,41 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7373
7560
|
);
|
|
7374
7561
|
}
|
|
7375
7562
|
}
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
if (
|
|
7379
|
-
|
|
7380
|
-
|
|
7563
|
+
if (fresh) {
|
|
7564
|
+
raw = fresh;
|
|
7565
|
+
} else if (!raw && recorded && cacheDir) {
|
|
7566
|
+
if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
|
|
7567
|
+
if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
|
|
7568
|
+
if (final || raw) log(` cached ${import_node_path17.default.relative(process.cwd(), target)}`);
|
|
7569
|
+
}
|
|
7570
|
+
if (!final && !raw) {
|
|
7381
7571
|
if (!source.url) {
|
|
7382
7572
|
throw new Error(`no source URL or cached bytes remain for ${source.role ?? "asset"}`);
|
|
7383
7573
|
}
|
|
7384
|
-
|
|
7574
|
+
raw = await provider.download(source.url);
|
|
7385
7575
|
}
|
|
7386
7576
|
let mediaType;
|
|
7577
|
+
const bytes = final ?? raw;
|
|
7387
7578
|
try {
|
|
7388
|
-
mediaType = validateMedia(
|
|
7579
|
+
mediaType = validateMedia(bytes, expectedMediaType);
|
|
7389
7580
|
} catch (err) {
|
|
7390
7581
|
const label = expectedMediaType === MediaType.GIF ? "GIF" : "PNG";
|
|
7391
|
-
const mismatch = detectMediaType(
|
|
7582
|
+
const mismatch = detectMediaType(bytes) !== expectedMediaType;
|
|
7392
7583
|
throw new Error(
|
|
7393
|
-
`response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${
|
|
7584
|
+
`response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${bytes.length} bytes)` : `: ${err instanceof Error ? err.message : String(err)}`)
|
|
7394
7585
|
);
|
|
7395
7586
|
}
|
|
7587
|
+
let rawHash = final ? recorded?.raw : void 0;
|
|
7588
|
+
let buf = bytes;
|
|
7589
|
+
if (!final) {
|
|
7590
|
+
if (cacheDir) await cacheMedia(cacheDir, raw, mediaType);
|
|
7591
|
+
const processed = applyPostprocess(raw, mediaType, wanted);
|
|
7592
|
+
buf = processed.bytes;
|
|
7593
|
+
if (processed.changed) {
|
|
7594
|
+
rawHash = sha256(raw);
|
|
7595
|
+
log(` snapped ${import_node_path17.default.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
|
|
7596
|
+
}
|
|
7597
|
+
}
|
|
7396
7598
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
7397
7599
|
await (0, import_promises10.mkdir)(import_node_path17.default.dirname(target), { recursive: true });
|
|
7398
7600
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
@@ -7401,7 +7603,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7401
7603
|
path: portableOutputPath(target, spec.root),
|
|
7402
7604
|
sha256: sha256(buf),
|
|
7403
7605
|
...source.role ? { role: source.role } : {},
|
|
7404
|
-
mediaType
|
|
7606
|
+
mediaType,
|
|
7607
|
+
...rawHash ? { raw: rawHash } : {}
|
|
7405
7608
|
});
|
|
7406
7609
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
7407
7610
|
await saveLock(lockPath, lock);
|
|
@@ -7425,7 +7628,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7425
7628
|
sourceUrls: persistentSources,
|
|
7426
7629
|
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7427
7630
|
error: null,
|
|
7428
|
-
supersededOutputs: []
|
|
7631
|
+
supersededOutputs: [],
|
|
7632
|
+
postprocess: wanted
|
|
7429
7633
|
});
|
|
7430
7634
|
result.downloaded++;
|
|
7431
7635
|
} catch (err) {
|
|
@@ -7532,6 +7736,7 @@ function retireGeneration(entry, retiredAt) {
|
|
|
7532
7736
|
cost: entry.cost,
|
|
7533
7737
|
costUnit: entry.costUnit,
|
|
7534
7738
|
provider: entry.provider,
|
|
7739
|
+
postprocess: entry.postprocess,
|
|
7535
7740
|
retiredAt
|
|
7536
7741
|
};
|
|
7537
7742
|
}
|
|
@@ -7544,9 +7749,13 @@ function historyAfterReplacing(previous, limit, retiredAt) {
|
|
|
7544
7749
|
}
|
|
7545
7750
|
function referencedHashes(lock) {
|
|
7546
7751
|
const hashes = /* @__PURE__ */ new Set();
|
|
7752
|
+
const add = (output) => {
|
|
7753
|
+
hashes.add(output.sha256);
|
|
7754
|
+
if (output.raw) hashes.add(output.raw);
|
|
7755
|
+
};
|
|
7547
7756
|
for (const entry of Object.values(lock.entries)) {
|
|
7548
|
-
for (const output of entry.outputs)
|
|
7549
|
-
for (const generation of entry.history ?? []) for (const output of generation.outputs)
|
|
7757
|
+
for (const output of entry.outputs) add(output);
|
|
7758
|
+
for (const generation of entry.history ?? []) for (const output of generation.outputs) add(output);
|
|
7550
7759
|
}
|
|
7551
7760
|
return hashes;
|
|
7552
7761
|
}
|
|
@@ -7583,8 +7792,9 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
7583
7792
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
7584
7793
|
if (!(0, import_node_fs15.existsSync)(file)) continue;
|
|
7585
7794
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
7586
|
-
throw new
|
|
7587
|
-
`${import_node_path18.default.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it
|
|
7795
|
+
throw new OverwriteRefusedError(
|
|
7796
|
+
`${import_node_path18.default.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
7797
|
+
[file]
|
|
7588
7798
|
);
|
|
7589
7799
|
}
|
|
7590
7800
|
}
|
|
@@ -7616,6 +7826,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
7616
7826
|
cost: generation.cost,
|
|
7617
7827
|
costUnit: generation.costUnit,
|
|
7618
7828
|
provider: generation.provider,
|
|
7829
|
+
postprocess: generation.postprocess,
|
|
7619
7830
|
history
|
|
7620
7831
|
});
|
|
7621
7832
|
await saveLock(lockPath, lock);
|
|
@@ -8334,7 +8545,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
8334
8545
|
if (opts.budget != null) {
|
|
8335
8546
|
const cost = [...estimates.values()].reduce((sum, estimate) => sum + estimate.amount, 0);
|
|
8336
8547
|
if (cost > opts.budget) {
|
|
8337
|
-
throw new
|
|
8548
|
+
throw new BudgetError(
|
|
8338
8549
|
`This run would spend ${formatCost(unit, cost)} but the budget is ${formatCost(unit, opts.budget)}. Narrow it with --only/--style, or raise --budget.`
|
|
8339
8550
|
);
|
|
8340
8551
|
}
|
|
@@ -8972,6 +9183,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
8972
9183
|
promptSuffix: "",
|
|
8973
9184
|
styleImages: [],
|
|
8974
9185
|
palette: [],
|
|
9186
|
+
enforcePalette: false,
|
|
8975
9187
|
outDir,
|
|
8976
9188
|
tags: [name],
|
|
8977
9189
|
providerOptions: {}
|
|
@@ -9661,7 +9873,7 @@ var WorkspaceSchema = import_zod6.z.object({
|
|
|
9661
9873
|
function parseWorkspace(raw) {
|
|
9662
9874
|
const parsed = WorkspaceSchema.safeParse(raw);
|
|
9663
9875
|
if (parsed.success) return parsed.data;
|
|
9664
|
-
throw new
|
|
9876
|
+
throw new ProjectError(
|
|
9665
9877
|
`Workspace catalog is not valid v1:
|
|
9666
9878
|
${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
|
|
9667
9879
|
);
|
|
@@ -9672,7 +9884,7 @@ async function loadWorkspace(workspacePath) {
|
|
|
9672
9884
|
try {
|
|
9673
9885
|
raw = JSON.parse(await (0, import_promises17.readFile)(workspacePath, "utf8"));
|
|
9674
9886
|
} catch (err) {
|
|
9675
|
-
throw new
|
|
9887
|
+
throw new ProjectError(
|
|
9676
9888
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
9677
9889
|
${err instanceof Error ? err.message : String(err)}`
|
|
9678
9890
|
);
|
|
@@ -9852,6 +10064,8 @@ var ManifestEditSchema = import_zod7.z.discriminatedUnion("action", [
|
|
|
9852
10064
|
promptSuffix: import_zod7.z.string().optional(),
|
|
9853
10065
|
/** `#rrggbb` values; null or empty clears the style's own palette. */
|
|
9854
10066
|
palette: import_zod7.z.array(import_zod7.z.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
|
|
10067
|
+
/** Snap downloaded art to the palette; false clears the style's own value. */
|
|
10068
|
+
enforcePalette: import_zod7.z.boolean().optional(),
|
|
9855
10069
|
/** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
|
|
9856
10070
|
view: import_zod7.z.string().max(64).nullable().optional(),
|
|
9857
10071
|
/** pixflux only; null clears the style's own value. */
|
|
@@ -9943,6 +10157,7 @@ function applyEdit(raw, edit) {
|
|
|
9943
10157
|
const colors = (patch2.palette ?? []).map((color) => "#" + color.replace(/^#/, "").toLowerCase());
|
|
9944
10158
|
setOrDelete(style, "palette", colors.length ? colors : null);
|
|
9945
10159
|
}
|
|
10160
|
+
if (patch2.enforcePalette !== void 0) setOrDelete(style, "enforcePalette", patch2.enforcePalette || null);
|
|
9946
10161
|
if (patch2.view !== void 0) setOrDelete(style, "view", patch2.view?.trim() || null);
|
|
9947
10162
|
if (patch2.noBackground !== void 0) setOrDelete(style, "noBackground", patch2.noBackground);
|
|
9948
10163
|
return;
|
|
@@ -10654,6 +10869,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10654
10869
|
outputs,
|
|
10655
10870
|
quality,
|
|
10656
10871
|
providerMetadata: entry?.providerMetadata ?? {},
|
|
10872
|
+
postprocess: entry?.postprocess ?? null,
|
|
10657
10873
|
asset,
|
|
10658
10874
|
source: spec.source ?? null,
|
|
10659
10875
|
edit,
|
|
@@ -10714,6 +10930,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10714
10930
|
outputs,
|
|
10715
10931
|
quality: null,
|
|
10716
10932
|
providerMetadata: entry.providerMetadata ?? {},
|
|
10933
|
+
postprocess: entry.postprocess ?? null,
|
|
10717
10934
|
asset: null,
|
|
10718
10935
|
source: null,
|
|
10719
10936
|
edit: null,
|
|
@@ -10762,6 +10979,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10762
10979
|
promptPrefix: style?.promptPrefix ?? "",
|
|
10763
10980
|
promptSuffix: style?.promptSuffix ?? "",
|
|
10764
10981
|
palette: style?.palette ?? [],
|
|
10982
|
+
enforcePalette: style?.enforcePalette ?? false,
|
|
10765
10983
|
view: style?.view ?? null,
|
|
10766
10984
|
noBackground: style?.noBackground ?? true,
|
|
10767
10985
|
quality: Boolean(style?.quality),
|
|
@@ -12465,6 +12683,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
12465
12683
|
// Annotate the CommonJS export names for ESM import in node:
|
|
12466
12684
|
0 && (module.exports = {
|
|
12467
12685
|
AssetSchema,
|
|
12686
|
+
BudgetError,
|
|
12468
12687
|
ComfyUIProvider,
|
|
12469
12688
|
DEFAULT_HISTORY_LIMIT,
|
|
12470
12689
|
DEFAULT_QUALITY_TOLERANCES,
|
|
@@ -12472,6 +12691,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
12472
12691
|
DEFAULT_TIMEOUT_MS,
|
|
12473
12692
|
EDITOR_PIN,
|
|
12474
12693
|
EDITOR_RELEASE_BASE,
|
|
12694
|
+
EXIT_CODES,
|
|
12475
12695
|
EditorInstallError,
|
|
12476
12696
|
FAKE_PNG,
|
|
12477
12697
|
FakeProvider,
|
|
@@ -12496,11 +12716,16 @@ async function workspaceStatus(ws, dir) {
|
|
|
12496
12716
|
ManifestInputSchema,
|
|
12497
12717
|
ManifestSchema,
|
|
12498
12718
|
MediaType,
|
|
12719
|
+
OverwriteRefusedError,
|
|
12499
12720
|
PIXEL_ART_FIXER_REVISION,
|
|
12500
12721
|
PIXEL_ART_FIXER_URL,
|
|
12722
|
+
PixelKilnError,
|
|
12501
12723
|
PixelLabClient,
|
|
12502
12724
|
PixelLabError,
|
|
12503
12725
|
PixelLabProvider,
|
|
12726
|
+
PostprocessSchema,
|
|
12727
|
+
ProjectError,
|
|
12728
|
+
ProviderError,
|
|
12504
12729
|
QualityBaselineSchema,
|
|
12505
12730
|
QualityProfileSchema,
|
|
12506
12731
|
QualityToleranceSchema,
|
|
@@ -12512,6 +12737,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
12512
12737
|
StyleInputSchema,
|
|
12513
12738
|
StyleSchema,
|
|
12514
12739
|
UnsupportedCapabilityError,
|
|
12740
|
+
UsageError,
|
|
12515
12741
|
WorkspaceProjectSchema,
|
|
12516
12742
|
WorkspaceSchema,
|
|
12517
12743
|
adopt,
|
|
@@ -12549,7 +12775,9 @@ async function workspaceStatus(ws, dir) {
|
|
|
12549
12775
|
editorRoute,
|
|
12550
12776
|
editorStatus,
|
|
12551
12777
|
editorToolsRoot,
|
|
12778
|
+
errorCode,
|
|
12552
12779
|
evaluateAudit,
|
|
12780
|
+
exitCodeFor,
|
|
12553
12781
|
expectedOutputPath,
|
|
12554
12782
|
exportTileset,
|
|
12555
12783
|
fallbackOutputRole,
|