pixelkiln 0.36.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/README.md +1 -1
- package/dist/cli.js +637 -510
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +42 -5
- package/dist/index.cjs +414 -285
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +501 -20
- package/dist/index.d.ts +501 -20
- package/dist/index.js +410 -282
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +8 -1
- package/docs/CLI.md +13 -2
- package/docs/MANIFEST.md +50 -2
- package/docs/QUALITY.md +16 -0
- package/docs/RECIPES.md +6 -2
- package/package.json +1 -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
|
@@ -71,6 +71,7 @@ __export(index_exports, {
|
|
|
71
71
|
PixelLabClient: () => PixelLabClient,
|
|
72
72
|
PixelLabError: () => PixelLabError,
|
|
73
73
|
PixelLabProvider: () => PixelLabProvider,
|
|
74
|
+
PostprocessSchema: () => PostprocessSchema,
|
|
74
75
|
ProjectError: () => ProjectError,
|
|
75
76
|
ProviderError: () => ProviderError,
|
|
76
77
|
QualityBaselineSchema: () => QualityBaselineSchema,
|
|
@@ -450,6 +451,18 @@ var StyleObjectSchema = import_zod.z.object({
|
|
|
450
451
|
* chocolate bar.
|
|
451
452
|
*/
|
|
452
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),
|
|
453
466
|
/** Fail-closed native-grid, palette, and human-approval policy for derived art. */
|
|
454
467
|
quality: QualityProfileSchema.optional(),
|
|
455
468
|
/**
|
|
@@ -491,6 +504,13 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
|
|
|
491
504
|
path: ["quality"]
|
|
492
505
|
});
|
|
493
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
|
+
}
|
|
494
514
|
});
|
|
495
515
|
var InheritedStyleSchema = StyleObjectSchema.partial().extend({
|
|
496
516
|
extends: import_zod.z.string().min(1),
|
|
@@ -613,7 +633,21 @@ var LockOutputSchema = import_zod.z.object({
|
|
|
613
633
|
path: import_zod.z.string(),
|
|
614
634
|
sha256: import_zod.z.string(),
|
|
615
635
|
role: import_zod.z.string().optional(),
|
|
616
|
-
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()
|
|
617
651
|
});
|
|
618
652
|
var LockSourceSchema = import_zod.z.object({
|
|
619
653
|
url: import_zod.z.string(),
|
|
@@ -639,6 +673,7 @@ var LockHistoryEntrySchema = import_zod.z.object({
|
|
|
639
673
|
cost: import_zod.z.number().finite().nonnegative().default(0),
|
|
640
674
|
costUnit: import_zod.z.string().min(1).default("generations"),
|
|
641
675
|
provider: import_zod.z.string().default("pixellab"),
|
|
676
|
+
postprocess: PostprocessSchema.optional(),
|
|
642
677
|
/** When the replacement was submitted. */
|
|
643
678
|
retiredAt: import_zod.z.string()
|
|
644
679
|
});
|
|
@@ -710,14 +745,7 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
710
745
|
* `role` labels non-primary artifacts (e.g. "portrait", "spriteframes") so a
|
|
711
746
|
* consumer can find the one it wants without pattern-matching on paths.
|
|
712
747
|
*/
|
|
713
|
-
outputs: import_zod.z.array(
|
|
714
|
-
import_zod.z.object({
|
|
715
|
-
path: import_zod.z.string(),
|
|
716
|
-
sha256: import_zod.z.string(),
|
|
717
|
-
role: import_zod.z.string().optional(),
|
|
718
|
-
mediaType: MediaTypeSchema.optional()
|
|
719
|
-
})
|
|
720
|
-
).default([]),
|
|
748
|
+
outputs: import_zod.z.array(LockOutputSchema).default([]),
|
|
721
749
|
/**
|
|
722
750
|
* Provider-owned data retained for downstream consumers, namespaced by
|
|
723
751
|
* provider id. PixelLab connectable sets keep their exact `tileRules` here
|
|
@@ -733,7 +761,9 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
733
761
|
/** Which provider produced this. Absent on entries written before providers. */
|
|
734
762
|
provider: import_zod.z.string().default("pixellab"),
|
|
735
763
|
/** Generations this entry replaced, newest first; see LockHistoryEntrySchema. */
|
|
736
|
-
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()
|
|
737
767
|
});
|
|
738
768
|
var LockSchema = import_zod.z.object({
|
|
739
769
|
version: import_zod.z.literal(2),
|
|
@@ -4574,6 +4604,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
4574
4604
|
detail: style.detail,
|
|
4575
4605
|
seed: style.seed,
|
|
4576
4606
|
palette: style.palette,
|
|
4607
|
+
enforcePalette: style.enforcePalette,
|
|
4577
4608
|
noBackground: style.noBackground,
|
|
4578
4609
|
tileSize: generator === "tiles" ? tileSize : void 0,
|
|
4579
4610
|
tileType: generator === "tiles" ? style.tileType : void 0,
|
|
@@ -4778,7 +4809,7 @@ function imageMetadata(buf) {
|
|
|
4778
4809
|
}
|
|
4779
4810
|
|
|
4780
4811
|
// src/project.ts
|
|
4781
|
-
var
|
|
4812
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
4782
4813
|
|
|
4783
4814
|
// src/env.ts
|
|
4784
4815
|
var import_node_fs6 = require("fs");
|
|
@@ -4825,17 +4856,17 @@ function parseEnv(contents) {
|
|
|
4825
4856
|
}
|
|
4826
4857
|
|
|
4827
4858
|
// src/pipeline/plan.ts
|
|
4828
|
-
var
|
|
4829
|
-
var
|
|
4859
|
+
var import_node_fs9 = require("fs");
|
|
4860
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
4830
4861
|
|
|
4831
4862
|
// src/pipeline/quality-profile.ts
|
|
4832
|
-
var
|
|
4833
|
-
var
|
|
4863
|
+
var import_node_fs7 = require("fs");
|
|
4864
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
4834
4865
|
|
|
4835
4866
|
// src/pipeline/refine.ts
|
|
4836
|
-
var
|
|
4867
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
4837
4868
|
var import_node_child_process2 = require("child_process");
|
|
4838
|
-
var
|
|
4869
|
+
var import_promises5 = require("fs/promises");
|
|
4839
4870
|
var import_node_os2 = require("os");
|
|
4840
4871
|
var import_node_util = require("util");
|
|
4841
4872
|
|
|
@@ -5296,10 +5327,19 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
5296
5327
|
};
|
|
5297
5328
|
}
|
|
5298
5329
|
|
|
5299
|
-
// src/
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
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
|
+
}
|
|
5303
5343
|
function colorDistance(a, b) {
|
|
5304
5344
|
const rmean = (a.r + b.r) / 2;
|
|
5305
5345
|
const dr = a.r - b.r;
|
|
@@ -5309,146 +5349,45 @@ function colorDistance(a, b) {
|
|
|
5309
5349
|
(512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
|
|
5310
5350
|
);
|
|
5311
5351
|
}
|
|
5312
|
-
function
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
const totals = /* @__PURE__ */ new Map();
|
|
5326
|
-
for (const palette of palettes) {
|
|
5327
|
-
for (const color of palette) {
|
|
5328
|
-
const key = `${color.r},${color.g},${color.b}`;
|
|
5329
|
-
const existing = totals.get(key);
|
|
5330
|
-
if (existing) existing.weight += color.weight;
|
|
5331
|
-
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;
|
|
5332
5365
|
}
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
5347
|
-
if (!(0, import_node_fs7.existsSync)(output.absolutePath)) {
|
|
5348
|
-
missing.push(output.id);
|
|
5349
|
-
continue;
|
|
5350
|
-
}
|
|
5351
|
-
try {
|
|
5352
|
-
const png = decodePng(await (0, import_promises5.readFile)(output.absolutePath));
|
|
5353
|
-
const palette = extractPalette(png, 12);
|
|
5354
|
-
assets.push({
|
|
5355
|
-
assetId: spec.assetId,
|
|
5356
|
-
...output.role ? { outputRole: output.role } : {},
|
|
5357
|
-
id: output.id,
|
|
5358
|
-
file: output.absolutePath,
|
|
5359
|
-
width: png.width,
|
|
5360
|
-
height: png.height,
|
|
5361
|
-
paletteDistance: 0,
|
|
5362
|
-
// filled once the reference is known
|
|
5363
|
-
transparency: transparencyRatio(png),
|
|
5364
|
-
colorCount: countColors(png),
|
|
5365
|
-
palette
|
|
5366
|
-
});
|
|
5367
|
-
} catch (err) {
|
|
5368
|
-
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
|
+
}
|
|
5369
5379
|
}
|
|
5380
|
+
nearestOf.set(key, nearest);
|
|
5370
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;
|
|
5371
5386
|
}
|
|
5372
|
-
|
|
5373
|
-
let referenceFromStyleImages = false;
|
|
5374
|
-
const refPalettes = [];
|
|
5375
|
-
for (const img of style.styleImages) {
|
|
5376
|
-
const abs = import_node_path8.default.resolve(loaded.root, img.path);
|
|
5377
|
-
if (!(0, import_node_fs7.existsSync)(abs)) continue;
|
|
5378
|
-
try {
|
|
5379
|
-
refPalettes.push(extractPalette(decodePng(await (0, import_promises5.readFile)(abs)), 12));
|
|
5380
|
-
} catch {
|
|
5381
|
-
}
|
|
5382
|
-
}
|
|
5383
|
-
if (refPalettes.length) {
|
|
5384
|
-
reference = mergePalettes(refPalettes);
|
|
5385
|
-
referenceFromStyleImages = true;
|
|
5386
|
-
for (const asset of assets) {
|
|
5387
|
-
asset.paletteDistance = paletteDistance(asset.palette, reference);
|
|
5388
|
-
}
|
|
5389
|
-
} else {
|
|
5390
|
-
reference = mergePalettes(assets.map((a) => a.palette));
|
|
5391
|
-
for (const asset of assets) {
|
|
5392
|
-
const others = assets.filter((a) => a !== asset).map((a) => a.palette);
|
|
5393
|
-
asset.paletteDistance = others.length ? paletteDistance(asset.palette, mergePalettes(others)) : 0;
|
|
5394
|
-
}
|
|
5395
|
-
}
|
|
5396
|
-
assets.sort((a, b) => b.paletteDistance - a.paletteDistance);
|
|
5397
|
-
return { styleId, referenceFromStyleImages, reference, assets, missing, unreadable };
|
|
5398
|
-
}
|
|
5399
|
-
function countColors(png) {
|
|
5400
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5401
|
-
for (let i = 0; i < png.pixels.length; i += 4) {
|
|
5402
|
-
if (png.pixels[i + 3] < 128) continue;
|
|
5403
|
-
seen.add(png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2]);
|
|
5404
|
-
}
|
|
5405
|
-
return seen.size;
|
|
5406
|
-
}
|
|
5407
|
-
function outliers(audit, sigma = 1.5) {
|
|
5408
|
-
const n = audit.assets.length;
|
|
5409
|
-
if (n < 3) return [];
|
|
5410
|
-
const mean = audit.assets.reduce((s, a) => s + a.paletteDistance, 0) / n;
|
|
5411
|
-
const variance = audit.assets.reduce((s, a) => s + (a.paletteDistance - mean) ** 2, 0) / n;
|
|
5412
|
-
const sd = Math.sqrt(variance);
|
|
5413
|
-
if (sd === 0) return [];
|
|
5414
|
-
return audit.assets.filter((a) => a.paletteDistance > mean + sigma * sd);
|
|
5415
|
-
}
|
|
5416
|
-
function evaluateAudit(audit, thresholds = {}) {
|
|
5417
|
-
const resolved = { ...thresholds, sigma: thresholds.sigma ?? 1.5 };
|
|
5418
|
-
const relative = outliers(audit, resolved.sigma);
|
|
5419
|
-
const relativeIds = new Set(relative.map((asset) => asset.id));
|
|
5420
|
-
const violations = [];
|
|
5421
|
-
for (const asset of audit.assets) {
|
|
5422
|
-
const reasons = [];
|
|
5423
|
-
if (relativeIds.has(asset.id)) {
|
|
5424
|
-
reasons.push(`palette outlier beyond ${resolved.sigma} standard deviations`);
|
|
5425
|
-
}
|
|
5426
|
-
if (resolved.maxDistance !== void 0 && asset.paletteDistance > resolved.maxDistance) {
|
|
5427
|
-
reasons.push(
|
|
5428
|
-
`palette distance ${asset.paletteDistance.toFixed(1)} exceeds ${resolved.maxDistance}`
|
|
5429
|
-
);
|
|
5430
|
-
}
|
|
5431
|
-
if (resolved.minTransparency !== void 0 && asset.transparency < resolved.minTransparency) {
|
|
5432
|
-
reasons.push(
|
|
5433
|
-
`transparency ${(asset.transparency * 100).toFixed(1)}% is below ${(resolved.minTransparency * 100).toFixed(1)}%`
|
|
5434
|
-
);
|
|
5435
|
-
}
|
|
5436
|
-
if (resolved.maxColors !== void 0 && asset.colorCount > resolved.maxColors) {
|
|
5437
|
-
reasons.push(`color count ${asset.colorCount} exceeds ${resolved.maxColors}`);
|
|
5438
|
-
}
|
|
5439
|
-
if (reasons.length) violations.push({ id: asset.id, reasons });
|
|
5440
|
-
}
|
|
5441
|
-
return {
|
|
5442
|
-
safe: violations.length === 0 && audit.missing.length === 0 && audit.unreadable.length === 0,
|
|
5443
|
-
thresholds: resolved,
|
|
5444
|
-
outliers: [...relativeIds],
|
|
5445
|
-
violations,
|
|
5446
|
-
missing: [...audit.missing],
|
|
5447
|
-
unreadable: [...audit.unreadable]
|
|
5448
|
-
};
|
|
5387
|
+
return { png: { width: png.width, height: png.height, pixels }, changed };
|
|
5449
5388
|
}
|
|
5450
|
-
function
|
|
5451
|
-
return
|
|
5389
|
+
function quantizeToPalette(png, palette) {
|
|
5390
|
+
return quantize(png, palette).png;
|
|
5452
5391
|
}
|
|
5453
5392
|
|
|
5454
5393
|
// src/pipeline/refine.ts
|
|
@@ -5535,48 +5474,12 @@ function parseDetection(raw) {
|
|
|
5535
5474
|
confidence: confidenceFor(consensus)
|
|
5536
5475
|
};
|
|
5537
5476
|
}
|
|
5538
|
-
function normalizePalette(values) {
|
|
5539
|
-
const colors = /* @__PURE__ */ new Map();
|
|
5540
|
-
for (const value of values) {
|
|
5541
|
-
const rgb = parseHex(value);
|
|
5542
|
-
const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
|
|
5543
|
-
colors.set(hex2, { hex: hex2, ...rgb });
|
|
5544
|
-
}
|
|
5545
|
-
if (colors.size < 2 || colors.size > 256) {
|
|
5546
|
-
throw new Error(`A refinement palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
|
|
5547
|
-
}
|
|
5548
|
-
return [...colors.values()];
|
|
5549
|
-
}
|
|
5550
5477
|
function normalizeRefinementPalette(values) {
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
const pixels = Buffer.from(png.pixels);
|
|
5556
|
-
for (let i = 0; i < pixels.length; i += 4) {
|
|
5557
|
-
const alpha = pixels[i + 3];
|
|
5558
|
-
if (alpha === 0) {
|
|
5559
|
-
pixels[i] = 0;
|
|
5560
|
-
pixels[i + 1] = 0;
|
|
5561
|
-
pixels[i + 2] = 0;
|
|
5562
|
-
continue;
|
|
5563
|
-
}
|
|
5564
|
-
const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
|
|
5565
|
-
let nearest = colors[0];
|
|
5566
|
-
let distance = colorDistance(pixel, nearest);
|
|
5567
|
-
for (let j = 1; j < colors.length; j++) {
|
|
5568
|
-
const candidate = colors[j];
|
|
5569
|
-
const candidateDistance = colorDistance(pixel, candidate);
|
|
5570
|
-
if (candidateDistance < distance) {
|
|
5571
|
-
nearest = candidate;
|
|
5572
|
-
distance = candidateDistance;
|
|
5573
|
-
}
|
|
5574
|
-
}
|
|
5575
|
-
pixels[i] = nearest.r;
|
|
5576
|
-
pixels[i + 1] = nearest.g;
|
|
5577
|
-
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 });
|
|
5578
5482
|
}
|
|
5579
|
-
return { width: png.width, height: png.height, pixels };
|
|
5580
5483
|
}
|
|
5581
5484
|
function colorCount(png) {
|
|
5582
5485
|
const colors = /* @__PURE__ */ new Set();
|
|
@@ -5686,16 +5589,16 @@ function sameFrameGrid(first, next) {
|
|
|
5686
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;
|
|
5687
5590
|
}
|
|
5688
5591
|
async function refineAsset(options) {
|
|
5689
|
-
const source =
|
|
5690
|
-
const output =
|
|
5592
|
+
const source = import_node_path8.default.resolve(options.source);
|
|
5593
|
+
const output = import_node_path8.default.resolve(options.output);
|
|
5691
5594
|
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
5692
|
-
const record =
|
|
5595
|
+
const record = import_node_path8.default.resolve(qualityRecordPath(output));
|
|
5693
5596
|
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
5694
5597
|
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
5695
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.");
|
|
5696
5599
|
let sourcePng;
|
|
5697
5600
|
try {
|
|
5698
|
-
sourcePng = decodePng(await (0,
|
|
5601
|
+
sourcePng = decodePng(await (0, import_promises5.readFile)(source));
|
|
5699
5602
|
} catch (error) {
|
|
5700
5603
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
5701
5604
|
}
|
|
@@ -5712,8 +5615,8 @@ async function refineAsset(options) {
|
|
|
5712
5615
|
);
|
|
5713
5616
|
}
|
|
5714
5617
|
}
|
|
5715
|
-
const temp = await (0,
|
|
5716
|
-
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");
|
|
5717
5620
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
5718
5621
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
5719
5622
|
try {
|
|
@@ -5734,7 +5637,7 @@ async function refineAsset(options) {
|
|
|
5734
5637
|
);
|
|
5735
5638
|
}
|
|
5736
5639
|
const detection = parseDetection(firstJsonObject(stdout));
|
|
5737
|
-
const native = decodePng(await (0,
|
|
5640
|
+
const native = decodePng(await (0, import_promises5.readFile)(recovered));
|
|
5738
5641
|
const quantized = quantizeToPalette(native, palette);
|
|
5739
5642
|
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
5740
5643
|
const audit = auditRefinement(
|
|
@@ -5784,7 +5687,7 @@ async function refineAsset(options) {
|
|
|
5784
5687
|
);
|
|
5785
5688
|
return { source, output, record, detection, palette, audit };
|
|
5786
5689
|
} finally {
|
|
5787
|
-
await (0,
|
|
5690
|
+
await (0, import_promises5.rm)(temp, { recursive: true, force: true });
|
|
5788
5691
|
}
|
|
5789
5692
|
}
|
|
5790
5693
|
async function refineFrameSet(options) {
|
|
@@ -5798,18 +5701,18 @@ async function refineFrameSet(options) {
|
|
|
5798
5701
|
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
5799
5702
|
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
5800
5703
|
}
|
|
5801
|
-
const output =
|
|
5802
|
-
const record =
|
|
5704
|
+
const output = import_node_path8.default.resolve(options.output);
|
|
5705
|
+
const record = import_node_path8.default.resolve(qualityRecordPath(output));
|
|
5803
5706
|
const palette = normalizeRefinementPalette(options.palette);
|
|
5804
5707
|
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
5805
5708
|
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
5806
5709
|
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
5807
5710
|
}
|
|
5808
|
-
const sourceSet = new Set(options.sources.map((source) =>
|
|
5711
|
+
const sourceSet = new Set(options.sources.map((source) => import_node_path8.default.resolve(source.path)));
|
|
5809
5712
|
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
5810
5713
|
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
5811
5714
|
}
|
|
5812
|
-
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-"));
|
|
5813
5716
|
try {
|
|
5814
5717
|
const frames = [];
|
|
5815
5718
|
const outputs = [];
|
|
@@ -5817,8 +5720,8 @@ async function refineFrameSet(options) {
|
|
|
5817
5720
|
const sourcePaths = [];
|
|
5818
5721
|
let firstOptions = null;
|
|
5819
5722
|
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
5820
|
-
const source =
|
|
5821
|
-
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`);
|
|
5822
5725
|
const refined = await refineAsset({
|
|
5823
5726
|
source,
|
|
5824
5727
|
output: temporaryOutput,
|
|
@@ -5850,7 +5753,7 @@ async function refineFrameSet(options) {
|
|
|
5850
5753
|
});
|
|
5851
5754
|
outputs.push({
|
|
5852
5755
|
path: finalOutputs[index],
|
|
5853
|
-
data: await (0,
|
|
5756
|
+
data: await (0, import_promises5.readFile)(temporaryOutput)
|
|
5854
5757
|
});
|
|
5855
5758
|
}
|
|
5856
5759
|
const first = frames[0];
|
|
@@ -5893,23 +5796,23 @@ async function refineFrameSet(options) {
|
|
|
5893
5796
|
);
|
|
5894
5797
|
return {
|
|
5895
5798
|
sources: sourcePaths,
|
|
5896
|
-
outputs: outputs.map((item) =>
|
|
5799
|
+
outputs: outputs.map((item) => import_node_path8.default.resolve(item.path)),
|
|
5897
5800
|
record,
|
|
5898
5801
|
frames,
|
|
5899
5802
|
palette,
|
|
5900
5803
|
audit
|
|
5901
5804
|
};
|
|
5902
5805
|
} finally {
|
|
5903
|
-
await (0,
|
|
5806
|
+
await (0, import_promises5.rm)(temp, { recursive: true, force: true });
|
|
5904
5807
|
}
|
|
5905
5808
|
}
|
|
5906
5809
|
async function checkQualityRecord(recordPath) {
|
|
5907
|
-
const record =
|
|
5810
|
+
const record = import_node_path8.default.resolve(recordPath);
|
|
5908
5811
|
const manifest = await readArtifactBundleManifest(record);
|
|
5909
5812
|
const options = parseRecordOptions(manifest, record);
|
|
5910
5813
|
const verification = await verifyArtifactBundle(record);
|
|
5911
|
-
const sources = manifest.sources.map((source2) =>
|
|
5912
|
-
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));
|
|
5913
5816
|
const source = sources[0] ?? "";
|
|
5914
5817
|
const output = outputs[0] ?? "";
|
|
5915
5818
|
const expectedCount = options.frameSet?.count ?? 1;
|
|
@@ -5942,7 +5845,7 @@ async function checkQualityRecord(recordPath) {
|
|
|
5942
5845
|
async function approveQualityRecord(recordPath, approval) {
|
|
5943
5846
|
const reviewer = approval.reviewer.trim();
|
|
5944
5847
|
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
5945
|
-
const record =
|
|
5848
|
+
const record = import_node_path8.default.resolve(recordPath);
|
|
5946
5849
|
const manifest = await readArtifactBundleManifest(record);
|
|
5947
5850
|
const options = parseRecordOptions(manifest, record);
|
|
5948
5851
|
const verification = await verifyArtifactBundle(record);
|
|
@@ -5963,14 +5866,14 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5963
5866
|
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
5964
5867
|
);
|
|
5965
5868
|
}
|
|
5966
|
-
const root =
|
|
5869
|
+
const root = import_node_path8.default.dirname(record);
|
|
5967
5870
|
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
5968
|
-
const outputPath =
|
|
5969
|
-
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) };
|
|
5970
5873
|
}));
|
|
5971
5874
|
const sources = manifest.sources.map((source) => ({
|
|
5972
5875
|
...source,
|
|
5973
|
-
path:
|
|
5876
|
+
path: import_node_path8.default.resolve(root, source.path)
|
|
5974
5877
|
}));
|
|
5975
5878
|
const approved = {
|
|
5976
5879
|
...options,
|
|
@@ -6002,8 +5905,8 @@ async function qualitySource(spec, lock) {
|
|
|
6002
5905
|
if (spec.generator === "frames") {
|
|
6003
5906
|
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
6004
5907
|
}
|
|
6005
|
-
const source =
|
|
6006
|
-
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}` };
|
|
6007
5910
|
}
|
|
6008
5911
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
6009
5912
|
if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
|
|
@@ -6032,7 +5935,7 @@ async function qualitySource(spec, lock) {
|
|
|
6032
5935
|
}
|
|
6033
5936
|
if (role) roles.add(role);
|
|
6034
5937
|
const source = currentEntryOutputPath(entry, spec, index);
|
|
6035
|
-
if (!(0,
|
|
5938
|
+
if (!(0, import_node_fs7.existsSync)(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
|
|
6036
5939
|
if (await sha256File(source) !== output.sha256) {
|
|
6037
5940
|
return { items: null, reason: "raw provider output was modified after download" };
|
|
6038
5941
|
}
|
|
@@ -6051,7 +5954,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
6051
5954
|
}
|
|
6052
5955
|
const sourcePaths = source.items.map((item) => item.path);
|
|
6053
5956
|
const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
|
|
6054
|
-
if (!(0,
|
|
5957
|
+
if (!(0, import_node_fs7.existsSync)(record)) {
|
|
6055
5958
|
return {
|
|
6056
5959
|
key,
|
|
6057
5960
|
state: "needs-refinement",
|
|
@@ -6069,7 +5972,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
6069
5972
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
6070
5973
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
6071
5974
|
const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
|
|
6072
|
-
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);
|
|
6073
5976
|
if (contractChanged) {
|
|
6074
5977
|
return {
|
|
6075
5978
|
key,
|
|
@@ -6231,9 +6134,38 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
|
|
|
6231
6134
|
return sources;
|
|
6232
6135
|
}
|
|
6233
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
|
+
|
|
6234
6166
|
// src/pipeline/revision.ts
|
|
6235
|
-
var
|
|
6236
|
-
var
|
|
6167
|
+
var import_node_fs8 = require("fs");
|
|
6168
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
6237
6169
|
async function inspectRevisionReadiness(spec, lock) {
|
|
6238
6170
|
if (!spec.revision) return null;
|
|
6239
6171
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
@@ -6248,14 +6180,14 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
6248
6180
|
reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
|
|
6249
6181
|
};
|
|
6250
6182
|
}
|
|
6251
|
-
if (!revision.sourceSha256 || !(0,
|
|
6183
|
+
if (!revision.sourceSha256 || !(0, import_node_fs8.existsSync)(revision.sourceFile)) {
|
|
6252
6184
|
return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
|
|
6253
6185
|
}
|
|
6254
6186
|
if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
|
|
6255
6187
|
return { ready: false, reason: "revision source changed after the manifest was resolved" };
|
|
6256
6188
|
}
|
|
6257
6189
|
if (revision.maskFile) {
|
|
6258
|
-
if (!revision.maskSha256 || !(0,
|
|
6190
|
+
if (!revision.maskSha256 || !(0, import_node_fs8.existsSync)(revision.maskFile)) {
|
|
6259
6191
|
return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
|
|
6260
6192
|
}
|
|
6261
6193
|
if (await sha256File(revision.maskFile) !== revision.maskSha256) {
|
|
@@ -6285,8 +6217,8 @@ async function inspectParent(spec, lock, seen) {
|
|
|
6285
6217
|
};
|
|
6286
6218
|
}
|
|
6287
6219
|
if (spec.source) {
|
|
6288
|
-
const source =
|
|
6289
|
-
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}` };
|
|
6290
6222
|
}
|
|
6291
6223
|
const entry = lock.entries[key];
|
|
6292
6224
|
if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
|
|
@@ -6300,7 +6232,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
6300
6232
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
6301
6233
|
const output = entry.outputs[index];
|
|
6302
6234
|
const file = currentEntryOutputPath(entry, spec, index);
|
|
6303
|
-
if (!(0,
|
|
6235
|
+
if (!(0, import_node_fs8.existsSync)(file)) return { ready: false, reason: `parent output is missing: ${file}` };
|
|
6304
6236
|
if (await sha256File(file) !== output.sha256) {
|
|
6305
6237
|
return { ready: false, reason: `parent output was modified: ${file}` };
|
|
6306
6238
|
}
|
|
@@ -6329,7 +6261,7 @@ function resumeActions(specs, lock) {
|
|
|
6329
6261
|
const entry = lock.entries[key];
|
|
6330
6262
|
if (!entry || entry.specHash !== spec.specHash) continue;
|
|
6331
6263
|
if (entry.submissionComplete === false) continue;
|
|
6332
|
-
const command = resumeCommandForStatus(entry.status);
|
|
6264
|
+
const command = resumeCommandForStatus(entry.status) ?? (entry.status === "downloaded" && entry.outputs.length && !postprocessCurrent(entry, spec) ? "fetch" : null);
|
|
6333
6265
|
if (!command) continue;
|
|
6334
6266
|
if (command === "poll" && !entry.jobId) continue;
|
|
6335
6267
|
if (command === "pick" && !entry.reviewObjectId) continue;
|
|
@@ -6355,7 +6287,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6355
6287
|
state = "missing";
|
|
6356
6288
|
reason = "--force";
|
|
6357
6289
|
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
6358
|
-
if ((0,
|
|
6290
|
+
if ((0, import_node_fs9.existsSync)(import_node_path11.default.resolve(spec.root, spec.source))) {
|
|
6359
6291
|
state = "ok";
|
|
6360
6292
|
reason = `placed from ${spec.source}; not generated`;
|
|
6361
6293
|
} else {
|
|
@@ -6363,7 +6295,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6363
6295
|
reason = `declared source is missing: ${spec.source}`;
|
|
6364
6296
|
}
|
|
6365
6297
|
} else if (!entry) {
|
|
6366
|
-
if ((0,
|
|
6298
|
+
if ((0, import_node_fs9.existsSync)(spec.outFile)) {
|
|
6367
6299
|
state = "untracked";
|
|
6368
6300
|
reason = "file on disk, no provenance recorded";
|
|
6369
6301
|
} else {
|
|
@@ -6395,12 +6327,15 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6395
6327
|
} else if (entry.status !== "downloaded") {
|
|
6396
6328
|
state = "in-flight";
|
|
6397
6329
|
reason = `awaiting ${entry.status}`;
|
|
6398
|
-
} 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)))) {
|
|
6399
6331
|
state = "orphaned";
|
|
6400
|
-
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(", ")}`;
|
|
6401
6333
|
} else if (await anyOutputModified(entry, spec)) {
|
|
6402
6334
|
state = "orphaned";
|
|
6403
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)";
|
|
6404
6339
|
} else {
|
|
6405
6340
|
state = "ok";
|
|
6406
6341
|
reason = "up to date";
|
|
@@ -6463,13 +6398,13 @@ function summarize(plan) {
|
|
|
6463
6398
|
|
|
6464
6399
|
// src/project.ts
|
|
6465
6400
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
6466
|
-
const absolute =
|
|
6467
|
-
const root =
|
|
6401
|
+
const absolute = import_node_path12.default.resolve(manifestPath);
|
|
6402
|
+
const root = import_node_path12.default.dirname(absolute);
|
|
6468
6403
|
if (opts.env !== false) {
|
|
6469
6404
|
loadEnvFiles(root);
|
|
6470
|
-
if (
|
|
6405
|
+
if (import_node_path12.default.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
6471
6406
|
}
|
|
6472
|
-
const lockPath =
|
|
6407
|
+
const lockPath = import_node_path12.default.resolve(opts.lockPath ?? import_node_path12.default.join(root, "pixelkiln.lock.json"));
|
|
6473
6408
|
const loaded = await loadManifest(absolute);
|
|
6474
6409
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
6475
6410
|
const lock = await loadLock(lockPath);
|
|
@@ -6536,9 +6471,9 @@ function shouldPersistSourceUrl(value) {
|
|
|
6536
6471
|
}
|
|
6537
6472
|
|
|
6538
6473
|
// src/recipes.ts
|
|
6539
|
-
var
|
|
6540
|
-
var
|
|
6541
|
-
var
|
|
6474
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
6475
|
+
var import_node_fs10 = require("fs");
|
|
6476
|
+
var import_promises6 = require("fs/promises");
|
|
6542
6477
|
var import_node_url = require("url");
|
|
6543
6478
|
var import_zod3 = require("zod");
|
|
6544
6479
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
@@ -6547,7 +6482,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
6547
6482
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
6548
6483
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
6549
6484
|
var PortablePathSchema = import_zod3.z.string().min(1).refine((value) => {
|
|
6550
|
-
if (value.includes("\\") ||
|
|
6485
|
+
if (value.includes("\\") || import_node_path13.default.posix.isAbsolute(value)) return false;
|
|
6551
6486
|
const parts = value.split("/");
|
|
6552
6487
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
6553
6488
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -6723,10 +6658,10 @@ function message3(error) {
|
|
|
6723
6658
|
return error instanceof Error ? error.message : String(error);
|
|
6724
6659
|
}
|
|
6725
6660
|
async function readRecipeFile(recipePath, bundled) {
|
|
6726
|
-
const absolute =
|
|
6661
|
+
const absolute = import_node_path13.default.resolve(recipePath);
|
|
6727
6662
|
let raw;
|
|
6728
6663
|
try {
|
|
6729
|
-
raw = JSON.parse(await (0,
|
|
6664
|
+
raw = JSON.parse(await (0, import_promises6.readFile)(absolute, "utf8"));
|
|
6730
6665
|
} catch (error) {
|
|
6731
6666
|
throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
|
|
6732
6667
|
}
|
|
@@ -6737,13 +6672,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
6737
6672
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
6738
6673
|
);
|
|
6739
6674
|
}
|
|
6740
|
-
return { recipe: parsed.data, path: absolute, dir:
|
|
6675
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: import_node_path13.default.dirname(absolute), bundled };
|
|
6741
6676
|
}
|
|
6742
6677
|
async function walkRecipeFiles(root) {
|
|
6743
|
-
if (!(0,
|
|
6678
|
+
if (!(0, import_node_fs10.existsSync)(root)) return [];
|
|
6744
6679
|
const out = [];
|
|
6745
|
-
for (const entry of await (0,
|
|
6746
|
-
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);
|
|
6747
6682
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
6748
6683
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
6749
6684
|
}
|
|
@@ -6754,7 +6689,7 @@ function bundledRecipeRoot() {
|
|
|
6754
6689
|
}
|
|
6755
6690
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
6756
6691
|
const recipes = await Promise.all(
|
|
6757
|
-
(await walkRecipeFiles(
|
|
6692
|
+
(await walkRecipeFiles(import_node_path13.default.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
6758
6693
|
);
|
|
6759
6694
|
return recipes.sort(
|
|
6760
6695
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -6777,9 +6712,9 @@ function parseSelector(selector) {
|
|
|
6777
6712
|
return { id: match[1], version: match[2] };
|
|
6778
6713
|
}
|
|
6779
6714
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
6780
|
-
const local =
|
|
6781
|
-
if ((0,
|
|
6782
|
-
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);
|
|
6783
6718
|
return readRecipeFile(recipePath, false);
|
|
6784
6719
|
}
|
|
6785
6720
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -6793,8 +6728,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
6793
6728
|
return matches[0];
|
|
6794
6729
|
}
|
|
6795
6730
|
async function verifyFile(root, file) {
|
|
6796
|
-
const absolute =
|
|
6797
|
-
if (!(0,
|
|
6731
|
+
const absolute = import_node_path13.default.resolve(root, ...file.path.split("/"));
|
|
6732
|
+
if (!(0, import_node_fs10.existsSync)(absolute)) {
|
|
6798
6733
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
6799
6734
|
}
|
|
6800
6735
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -6807,9 +6742,9 @@ async function verifyFile(root, file) {
|
|
|
6807
6742
|
}
|
|
6808
6743
|
async function verifyRecipe(target, options = {}) {
|
|
6809
6744
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
6810
|
-
const actualIntegrity = recipeDigest(loaded.
|
|
6745
|
+
const actualIntegrity = recipeDigest(loaded.authored);
|
|
6811
6746
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
6812
|
-
const modelRoot = options.modelRoot ?
|
|
6747
|
+
const modelRoot = options.modelRoot ? import_node_path13.default.resolve(options.modelRoot) : null;
|
|
6813
6748
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
6814
6749
|
...await verifyFile(modelRoot, model),
|
|
6815
6750
|
source: model.source,
|
|
@@ -6843,7 +6778,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
6843
6778
|
};
|
|
6844
6779
|
}
|
|
6845
6780
|
function renderStyle(style, destination, cwd) {
|
|
6846
|
-
const portableDir =
|
|
6781
|
+
const portableDir = import_node_path13.default.relative(cwd, destination).split(import_node_path13.default.sep).join("/") || ".";
|
|
6847
6782
|
const replace = (value) => {
|
|
6848
6783
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
6849
6784
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -6855,25 +6790,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
6855
6790
|
return replace(style);
|
|
6856
6791
|
}
|
|
6857
6792
|
async function installRecipe(target, options = {}) {
|
|
6858
|
-
const cwd =
|
|
6793
|
+
const cwd = import_node_path13.default.resolve(options.cwd ?? process.cwd());
|
|
6859
6794
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
6860
6795
|
const verification = await verifyRecipe(loaded.path);
|
|
6861
6796
|
if (!verification.ok) {
|
|
6862
6797
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
6863
6798
|
}
|
|
6864
|
-
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);
|
|
6865
6800
|
const sources = [
|
|
6866
6801
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
6867
|
-
...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("/")) }))
|
|
6868
6803
|
];
|
|
6869
6804
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
6870
|
-
path:
|
|
6871
|
-
data: await (0,
|
|
6805
|
+
path: import_node_path13.default.join(destination, ...file.path.split("/")),
|
|
6806
|
+
data: await (0, import_promises6.readFile)(file.source)
|
|
6872
6807
|
})));
|
|
6873
6808
|
if (!options.force) {
|
|
6874
6809
|
for (const file of files) {
|
|
6875
|
-
if (!(0,
|
|
6876
|
-
const current = await (0,
|
|
6810
|
+
if (!(0, import_node_fs10.existsSync)(file.path)) continue;
|
|
6811
|
+
const current = await (0, import_promises6.readFile)(file.path);
|
|
6877
6812
|
if (!current.equals(file.data)) {
|
|
6878
6813
|
throw new OverwriteRefusedError(
|
|
6879
6814
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -6896,6 +6831,152 @@ async function installRecipe(target, options = {}) {
|
|
|
6896
6831
|
};
|
|
6897
6832
|
}
|
|
6898
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
|
+
|
|
6899
6980
|
// src/pipeline/quality-regression.ts
|
|
6900
6981
|
var import_node_path15 = __toESM(require("path"), 1);
|
|
6901
6982
|
var import_node_fs12 = require("fs");
|
|
@@ -7368,8 +7449,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7368
7449
|
if (e.status === "selected" || e.status === "download-failed") return true;
|
|
7369
7450
|
if (e.status !== "downloaded") return false;
|
|
7370
7451
|
if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
|
|
7371
|
-
if (!opts.repair) return false;
|
|
7372
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;
|
|
7373
7460
|
if (e.outputs.length === 0 || !spec) return true;
|
|
7374
7461
|
return e.outputs.some((output) => {
|
|
7375
7462
|
const file = resolveOutputPath(output.path, spec.root);
|
|
@@ -7408,6 +7495,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7408
7495
|
}
|
|
7409
7496
|
const outputs = [];
|
|
7410
7497
|
let wrote = false;
|
|
7498
|
+
const wanted = postprocessFor(spec);
|
|
7499
|
+
const reapply = entry.status === "downloaded" && !postprocessCurrent(entry, spec);
|
|
7411
7500
|
try {
|
|
7412
7501
|
for (let index = 0; index < sources.length; index++) {
|
|
7413
7502
|
const source = sources[index];
|
|
@@ -7415,20 +7504,24 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7415
7504
|
(o) => source.role ? o.role === source.role : !o.role && sources.length === 1
|
|
7416
7505
|
);
|
|
7417
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;
|
|
7418
7509
|
let fresh = null;
|
|
7419
7510
|
if (opts.refresh) {
|
|
7420
7511
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
7421
7512
|
fresh = await provider.download(source.url);
|
|
7422
|
-
if (recorded && sha256(fresh) ===
|
|
7513
|
+
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
7423
7514
|
log(` current ${import_node_path17.default.relative(process.cwd(), target)}`);
|
|
7424
7515
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
7425
7516
|
continue;
|
|
7426
7517
|
}
|
|
7427
7518
|
}
|
|
7519
|
+
let raw = null;
|
|
7520
|
+
let final = null;
|
|
7428
7521
|
if ((0, import_node_fs14.existsSync)(target)) {
|
|
7429
7522
|
const currentHash = await sha256File(target);
|
|
7430
7523
|
if (recorded && currentHash === recorded.sha256) {
|
|
7431
|
-
if (!fresh) {
|
|
7524
|
+
if (!fresh && !reapply) {
|
|
7432
7525
|
if (cacheDir) {
|
|
7433
7526
|
await cacheMedia(
|
|
7434
7527
|
cacheDir,
|
|
@@ -7440,7 +7533,12 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7440
7533
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
7441
7534
|
continue;
|
|
7442
7535
|
}
|
|
7443
|
-
|
|
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
|
+
}
|
|
7444
7542
|
} else {
|
|
7445
7543
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
7446
7544
|
(output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
|
|
@@ -7462,26 +7560,41 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7462
7560
|
);
|
|
7463
7561
|
}
|
|
7464
7562
|
}
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
if (
|
|
7468
|
-
|
|
7469
|
-
|
|
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) {
|
|
7470
7571
|
if (!source.url) {
|
|
7471
7572
|
throw new Error(`no source URL or cached bytes remain for ${source.role ?? "asset"}`);
|
|
7472
7573
|
}
|
|
7473
|
-
|
|
7574
|
+
raw = await provider.download(source.url);
|
|
7474
7575
|
}
|
|
7475
7576
|
let mediaType;
|
|
7577
|
+
const bytes = final ?? raw;
|
|
7476
7578
|
try {
|
|
7477
|
-
mediaType = validateMedia(
|
|
7579
|
+
mediaType = validateMedia(bytes, expectedMediaType);
|
|
7478
7580
|
} catch (err) {
|
|
7479
7581
|
const label = expectedMediaType === MediaType.GIF ? "GIF" : "PNG";
|
|
7480
|
-
const mismatch = detectMediaType(
|
|
7582
|
+
const mismatch = detectMediaType(bytes) !== expectedMediaType;
|
|
7481
7583
|
throw new Error(
|
|
7482
|
-
`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)}`)
|
|
7483
7585
|
);
|
|
7484
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
|
+
}
|
|
7485
7598
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
7486
7599
|
await (0, import_promises10.mkdir)(import_node_path17.default.dirname(target), { recursive: true });
|
|
7487
7600
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
@@ -7490,7 +7603,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7490
7603
|
path: portableOutputPath(target, spec.root),
|
|
7491
7604
|
sha256: sha256(buf),
|
|
7492
7605
|
...source.role ? { role: source.role } : {},
|
|
7493
|
-
mediaType
|
|
7606
|
+
mediaType,
|
|
7607
|
+
...rawHash ? { raw: rawHash } : {}
|
|
7494
7608
|
});
|
|
7495
7609
|
upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
|
|
7496
7610
|
await saveLock(lockPath, lock);
|
|
@@ -7514,7 +7628,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
7514
7628
|
sourceUrls: persistentSources,
|
|
7515
7629
|
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7516
7630
|
error: null,
|
|
7517
|
-
supersededOutputs: []
|
|
7631
|
+
supersededOutputs: [],
|
|
7632
|
+
postprocess: wanted
|
|
7518
7633
|
});
|
|
7519
7634
|
result.downloaded++;
|
|
7520
7635
|
} catch (err) {
|
|
@@ -7621,6 +7736,7 @@ function retireGeneration(entry, retiredAt) {
|
|
|
7621
7736
|
cost: entry.cost,
|
|
7622
7737
|
costUnit: entry.costUnit,
|
|
7623
7738
|
provider: entry.provider,
|
|
7739
|
+
postprocess: entry.postprocess,
|
|
7624
7740
|
retiredAt
|
|
7625
7741
|
};
|
|
7626
7742
|
}
|
|
@@ -7633,9 +7749,13 @@ function historyAfterReplacing(previous, limit, retiredAt) {
|
|
|
7633
7749
|
}
|
|
7634
7750
|
function referencedHashes(lock) {
|
|
7635
7751
|
const hashes = /* @__PURE__ */ new Set();
|
|
7752
|
+
const add = (output) => {
|
|
7753
|
+
hashes.add(output.sha256);
|
|
7754
|
+
if (output.raw) hashes.add(output.raw);
|
|
7755
|
+
};
|
|
7636
7756
|
for (const entry of Object.values(lock.entries)) {
|
|
7637
|
-
for (const output of entry.outputs)
|
|
7638
|
-
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);
|
|
7639
7759
|
}
|
|
7640
7760
|
return hashes;
|
|
7641
7761
|
}
|
|
@@ -7706,6 +7826,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
7706
7826
|
cost: generation.cost,
|
|
7707
7827
|
costUnit: generation.costUnit,
|
|
7708
7828
|
provider: generation.provider,
|
|
7829
|
+
postprocess: generation.postprocess,
|
|
7709
7830
|
history
|
|
7710
7831
|
});
|
|
7711
7832
|
await saveLock(lockPath, lock);
|
|
@@ -9062,6 +9183,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
9062
9183
|
promptSuffix: "",
|
|
9063
9184
|
styleImages: [],
|
|
9064
9185
|
palette: [],
|
|
9186
|
+
enforcePalette: false,
|
|
9065
9187
|
outDir,
|
|
9066
9188
|
tags: [name],
|
|
9067
9189
|
providerOptions: {}
|
|
@@ -9942,6 +10064,8 @@ var ManifestEditSchema = import_zod7.z.discriminatedUnion("action", [
|
|
|
9942
10064
|
promptSuffix: import_zod7.z.string().optional(),
|
|
9943
10065
|
/** `#rrggbb` values; null or empty clears the style's own palette. */
|
|
9944
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(),
|
|
9945
10069
|
/** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
|
|
9946
10070
|
view: import_zod7.z.string().max(64).nullable().optional(),
|
|
9947
10071
|
/** pixflux only; null clears the style's own value. */
|
|
@@ -10033,6 +10157,7 @@ function applyEdit(raw, edit) {
|
|
|
10033
10157
|
const colors = (patch2.palette ?? []).map((color) => "#" + color.replace(/^#/, "").toLowerCase());
|
|
10034
10158
|
setOrDelete(style, "palette", colors.length ? colors : null);
|
|
10035
10159
|
}
|
|
10160
|
+
if (patch2.enforcePalette !== void 0) setOrDelete(style, "enforcePalette", patch2.enforcePalette || null);
|
|
10036
10161
|
if (patch2.view !== void 0) setOrDelete(style, "view", patch2.view?.trim() || null);
|
|
10037
10162
|
if (patch2.noBackground !== void 0) setOrDelete(style, "noBackground", patch2.noBackground);
|
|
10038
10163
|
return;
|
|
@@ -10744,6 +10869,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10744
10869
|
outputs,
|
|
10745
10870
|
quality,
|
|
10746
10871
|
providerMetadata: entry?.providerMetadata ?? {},
|
|
10872
|
+
postprocess: entry?.postprocess ?? null,
|
|
10747
10873
|
asset,
|
|
10748
10874
|
source: spec.source ?? null,
|
|
10749
10875
|
edit,
|
|
@@ -10804,6 +10930,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10804
10930
|
outputs,
|
|
10805
10931
|
quality: null,
|
|
10806
10932
|
providerMetadata: entry.providerMetadata ?? {},
|
|
10933
|
+
postprocess: entry.postprocess ?? null,
|
|
10807
10934
|
asset: null,
|
|
10808
10935
|
source: null,
|
|
10809
10936
|
edit: null,
|
|
@@ -10852,6 +10979,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
10852
10979
|
promptPrefix: style?.promptPrefix ?? "",
|
|
10853
10980
|
promptSuffix: style?.promptSuffix ?? "",
|
|
10854
10981
|
palette: style?.palette ?? [],
|
|
10982
|
+
enforcePalette: style?.enforcePalette ?? false,
|
|
10855
10983
|
view: style?.view ?? null,
|
|
10856
10984
|
noBackground: style?.noBackground ?? true,
|
|
10857
10985
|
quality: Boolean(style?.quality),
|
|
@@ -12595,6 +12723,7 @@ async function workspaceStatus(ws, dir) {
|
|
|
12595
12723
|
PixelLabClient,
|
|
12596
12724
|
PixelLabError,
|
|
12597
12725
|
PixelLabProvider,
|
|
12726
|
+
PostprocessSchema,
|
|
12598
12727
|
ProjectError,
|
|
12599
12728
|
ProviderError,
|
|
12600
12729
|
QualityBaselineSchema,
|