pixelkiln 0.87.0 → 0.88.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/PROVIDERS.md +2 -1
- package/README.md +4 -4
- package/dist/cli.js +1247 -593
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +1 -1
- package/dist/index.cjs +1257 -647
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +786 -101
- package/dist/index.d.ts +786 -101
- package/dist/index.js +1161 -574
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +69 -5
- package/docs/ENDPOINTS.md +63 -9
- package/docs/GENERATORS.md +72 -2
- package/docs/MANIFEST.md +3 -2
- package/docs/PIXELLAB.md +1 -0
- package/package.json +1 -1
- package/schema/manifest.schema.json +47 -2
- package/schema/recipe.schema.json +2 -1
- package/skills/pixelkiln/SKILL.md +8 -3
- package/skills/pixelkiln/references/pixellab-roadmap.md +16 -15
- package/skills/pixelkiln/references/pixellab.md +11 -3
package/dist/cli.js
CHANGED
|
@@ -67,8 +67,8 @@ function exitCodeFor(err) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// src/cli/main.ts
|
|
70
|
-
import { existsSync as
|
|
71
|
-
import { readFile as
|
|
70
|
+
import { existsSync as existsSync39 } from "fs";
|
|
71
|
+
import { readFile as readFile35 } from "fs/promises";
|
|
72
72
|
|
|
73
73
|
// src/cli/args.ts
|
|
74
74
|
import path from "path";
|
|
@@ -109,7 +109,13 @@ var VALUE_FLAGS = [
|
|
|
109
109
|
"--weight",
|
|
110
110
|
"--glyph-px",
|
|
111
111
|
"--frames",
|
|
112
|
-
"--estimate-limit"
|
|
112
|
+
"--estimate-limit",
|
|
113
|
+
"--preset",
|
|
114
|
+
"--recipe",
|
|
115
|
+
"--effect",
|
|
116
|
+
"--part",
|
|
117
|
+
"--sprite",
|
|
118
|
+
"--fps"
|
|
113
119
|
];
|
|
114
120
|
var BOOL_FLAGS = [
|
|
115
121
|
"--force",
|
|
@@ -126,7 +132,8 @@ var BOOL_FLAGS = [
|
|
|
126
132
|
"--prune",
|
|
127
133
|
"--edit",
|
|
128
134
|
"--refresh",
|
|
129
|
-
"--no-editor"
|
|
135
|
+
"--no-editor",
|
|
136
|
+
"--game-pack"
|
|
130
137
|
];
|
|
131
138
|
var COMMANDS = [
|
|
132
139
|
"init",
|
|
@@ -163,6 +170,7 @@ var COMMANDS = [
|
|
|
163
170
|
"skeleton-preview",
|
|
164
171
|
"unzoom",
|
|
165
172
|
"pixelate",
|
|
173
|
+
"vfx",
|
|
166
174
|
"font",
|
|
167
175
|
"help",
|
|
168
176
|
"--help",
|
|
@@ -177,6 +185,7 @@ var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
|
|
|
177
185
|
var EDIT_SUBCOMMANDS = ["start", "detach"];
|
|
178
186
|
var TOOLS_SUBCOMMANDS = ["status", "install"];
|
|
179
187
|
var TOOLS = ["editor"];
|
|
188
|
+
var VFX_SUBCOMMANDS = ["make", "presets", "parts"];
|
|
180
189
|
function parseArgs(argv) {
|
|
181
190
|
const [command = "help"] = argv;
|
|
182
191
|
if (!COMMANDS.includes(command)) {
|
|
@@ -266,6 +275,12 @@ function parseArgs(argv) {
|
|
|
266
275
|
throw new UsageError("skeleton-preview needs a keypoints file or an animate-skeleton asset id");
|
|
267
276
|
}
|
|
268
277
|
rest = rest.slice(1);
|
|
278
|
+
} else if (command === "vfx") {
|
|
279
|
+
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "make" : rest[0];
|
|
280
|
+
if (!VFX_SUBCOMMANDS.includes(subcommand)) {
|
|
281
|
+
throw new UsageError(`Unknown vfx subcommand "${subcommand}". Known: ${VFX_SUBCOMMANDS.join(", ")}`);
|
|
282
|
+
}
|
|
283
|
+
if (rest[0] === subcommand) rest = rest.slice(1);
|
|
269
284
|
} else if (command === "refine") {
|
|
270
285
|
subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
|
|
271
286
|
if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
|
|
@@ -433,7 +448,14 @@ function parseArgs(argv) {
|
|
|
433
448
|
weight: get("--weight"),
|
|
434
449
|
glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true }),
|
|
435
450
|
frames: numberOption("--frames", { min: 3, max: 15, integer: true }),
|
|
436
|
-
estimateLimit: numberOption("--estimate-limit", { min: 0, max: 1e3, integer: true })
|
|
451
|
+
estimateLimit: numberOption("--estimate-limit", { min: 0, max: 1e3, integer: true }),
|
|
452
|
+
preset: get("--preset"),
|
|
453
|
+
recipe: get("--recipe"),
|
|
454
|
+
effect: get("--effect"),
|
|
455
|
+
parts: list("--part"),
|
|
456
|
+
sprite: get("--sprite"),
|
|
457
|
+
fps: numberOption("--fps", { min: 1, max: 60, integer: true }),
|
|
458
|
+
gamePack: rest.includes("--game-pack")
|
|
437
459
|
};
|
|
438
460
|
}
|
|
439
461
|
|
|
@@ -504,6 +526,10 @@ Commands
|
|
|
504
526
|
pixel art with Image to Pixel Art Pro Flash; PixelLab picks the
|
|
505
527
|
output size. Optional --description steers the style. 6
|
|
506
528
|
generations; asks first.
|
|
529
|
+
vfx PixelLab: build an animated pixel-art effect with the VFX
|
|
530
|
+
workbench (free) from a --preset, a --recipe file, a saved
|
|
531
|
+
--effect, and/or --part; writes <out>.png/.gif/.json and a
|
|
532
|
+
.vfx.json recipe record. vfx presets / vfx parts list options.
|
|
507
533
|
font PixelLab: generate a pixel font (--description, --out); writes a
|
|
508
534
|
.ttf and a glyph atlas PNG. 15 generations; asks first.
|
|
509
535
|
|
|
@@ -531,6 +557,13 @@ Options
|
|
|
531
557
|
--description <text> font: the style to draw, e.g. "warm orange arcade font";
|
|
532
558
|
pixelate: an optional style steer, e.g. "light dithering"
|
|
533
559
|
--weight <w> font: Bold or Regular (default)
|
|
560
|
+
--preset <name> vfx: start from a whole-effect preset (vfx presets lists them)
|
|
561
|
+
--recipe <file> vfx: render a recipe or an earlier run's .vfx.json record
|
|
562
|
+
--effect <id> vfx: start from a saved My effects id and export over it
|
|
563
|
+
--part <name> vfx: add a part (repeatable; vfx parts lists them)
|
|
564
|
+
--sprite <ref> vfx: build around a sprite: a file, a URL, or character:<id>:<dir>
|
|
565
|
+
--fps <n> vfx: frames per second, 1-60
|
|
566
|
+
--game-pack vfx: also download the game-pack ZIP
|
|
534
567
|
--glyph-px <n> font: native glyph size, 8, 16 (default), 32, or 64
|
|
535
568
|
--prune cache: remove invalid/unreferenced local cache data
|
|
536
569
|
--manifest <path> Default: pixelkiln.manifest.json
|
|
@@ -590,6 +623,7 @@ Examples
|
|
|
590
623
|
pixelkiln quality check --from pixelkiln.quality.json
|
|
591
624
|
pixelkiln unzoom --from refs/knight-512.png --out refs/knight.png
|
|
592
625
|
pixelkiln pixelate --from renders/lodge.png --out art/lodge.png
|
|
626
|
+
pixelkiln vfx --preset explosion --part scorch_mark --out fx/explosion --game-pack
|
|
593
627
|
pixelkiln font --description "warm orange arcade font" --weight Bold --out fonts/arcade
|
|
594
628
|
pixelkiln workspace add ../other-game/pixelkiln.manifest.json
|
|
595
629
|
pixelkiln workspace status --json
|
|
@@ -672,16 +706,17 @@ import { createReadStream } from "fs";
|
|
|
672
706
|
function sha256(data) {
|
|
673
707
|
return createHash("sha256").update(data).digest("hex");
|
|
674
708
|
}
|
|
675
|
-
async function sha256File(
|
|
709
|
+
async function sha256File(path53) {
|
|
676
710
|
return new Promise((resolve, reject) => {
|
|
677
711
|
const hash = createHash("sha256");
|
|
678
|
-
const stream = createReadStream(
|
|
712
|
+
const stream = createReadStream(path53);
|
|
679
713
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
680
714
|
stream.on("error", reject);
|
|
681
715
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
682
716
|
});
|
|
683
717
|
}
|
|
684
718
|
function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
|
|
719
|
+
const effect = Boolean(spec.vfx);
|
|
685
720
|
return sha256(
|
|
686
721
|
JSON.stringify({
|
|
687
722
|
// Preserve every existing PixelLab hash while making a provider switch
|
|
@@ -690,15 +725,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
690
725
|
providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
|
|
691
726
|
providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
|
|
692
727
|
generator: spec.generator,
|
|
693
|
-
prompt: spec.prompt,
|
|
694
|
-
width: spec.width,
|
|
695
|
-
height: spec.height,
|
|
696
|
-
view: spec.view,
|
|
697
|
-
outline: spec.outline ?? null,
|
|
698
|
-
shading: spec.shading ?? null,
|
|
699
|
-
detail: spec.detail ?? null,
|
|
700
|
-
seed: spec.seed ?? null,
|
|
701
|
-
palette: spec.palette,
|
|
728
|
+
prompt: effect ? void 0 : spec.prompt,
|
|
729
|
+
width: effect ? void 0 : spec.width,
|
|
730
|
+
height: effect ? void 0 : spec.height,
|
|
731
|
+
view: effect ? void 0 : spec.view,
|
|
732
|
+
outline: effect ? void 0 : spec.outline ?? null,
|
|
733
|
+
shading: effect ? void 0 : spec.shading ?? null,
|
|
734
|
+
detail: effect ? void 0 : spec.detail ?? null,
|
|
735
|
+
seed: effect ? void 0 : spec.seed ?? null,
|
|
736
|
+
palette: effect ? void 0 : spec.palette,
|
|
702
737
|
// `noBackground` only reaches the wire for pixflux and uiAsset; the
|
|
703
738
|
// tile fields are undefined for every other generator. `tileSize` is
|
|
704
739
|
// intentionally absent; width/height are derived from it, so it is
|
|
@@ -773,6 +808,15 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
773
808
|
styleTraits: spec.objectPro.styleTraits,
|
|
774
809
|
reference: spec.objectPro.reference?.sha256
|
|
775
810
|
} : void 0,
|
|
811
|
+
// A recipe and a local sprite enter as their contents' hashes, never
|
|
812
|
+
// their paths; the My effects name is not identity.
|
|
813
|
+
vfx: spec.vfx ? {
|
|
814
|
+
preset: spec.vfx.preset,
|
|
815
|
+
recipe: spec.vfx.recipe?.sha256,
|
|
816
|
+
parts: spec.vfx.parts,
|
|
817
|
+
fps: spec.vfx.fps,
|
|
818
|
+
sprite: spec.vfx.sprite ? "reference" in spec.vfx.sprite ? spec.vfx.sprite.reference : spec.vfx.sprite.sha256 : void 0
|
|
819
|
+
} : void 0,
|
|
776
820
|
// A mirror's bytes come from its source's recorded outputs; the plan
|
|
777
821
|
// compares those directly, so only the choice of source is identity.
|
|
778
822
|
mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
|
|
@@ -822,7 +866,7 @@ import path2 from "path";
|
|
|
822
866
|
// src/types.ts
|
|
823
867
|
import { z } from "zod";
|
|
824
868
|
var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
|
|
825
|
-
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement", "imageProFlash"]);
|
|
869
|
+
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement", "imageProFlash", "vfx"]);
|
|
826
870
|
var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
|
|
827
871
|
var RevisionModeSchema = z.enum([
|
|
828
872
|
"image-to-image",
|
|
@@ -855,6 +899,7 @@ function candidateCount(size) {
|
|
|
855
899
|
return 1;
|
|
856
900
|
}
|
|
857
901
|
function generationCost(width, height, generator = "map") {
|
|
902
|
+
if (generator === "vfx") return 0;
|
|
858
903
|
if (generator === "map" || generator === "pixflux") return 1;
|
|
859
904
|
if (generator === "imagePro") return proTierCost(width, height);
|
|
860
905
|
const px = width * height;
|
|
@@ -1039,6 +1084,29 @@ var MapSceneSchema = z.object({
|
|
|
1039
1084
|
image: z.string().min(1),
|
|
1040
1085
|
placement: MapScenePlacementSchema.default({ oval: 0.3 })
|
|
1041
1086
|
}).strict();
|
|
1087
|
+
var VfxPartSchema = z.object({ name: z.string().min(1) }).catchall(z.unknown());
|
|
1088
|
+
var AssetVfxSchema = z.object({
|
|
1089
|
+
preset: z.string().min(1).optional(),
|
|
1090
|
+
recipe: z.string().min(1).optional(),
|
|
1091
|
+
parts: z.array(VfxPartSchema).optional(),
|
|
1092
|
+
sprite: z.string().min(1).optional(),
|
|
1093
|
+
fps: z.number().int().min(1).max(60).optional(),
|
|
1094
|
+
name: z.string().min(1).optional()
|
|
1095
|
+
}).strict().superRefine((vfx, context) => {
|
|
1096
|
+
if (vfx.preset && vfx.recipe) {
|
|
1097
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "start a vfx from a preset or a recipe, not both", path: ["recipe"] });
|
|
1098
|
+
}
|
|
1099
|
+
if (!vfx.preset && !vfx.recipe && !vfx.parts?.length) {
|
|
1100
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "a vfx needs a preset, a recipe, or at least one part", path: ["preset"] });
|
|
1101
|
+
}
|
|
1102
|
+
if (vfx.sprite && /^(https?|data):/i.test(vfx.sprite)) {
|
|
1103
|
+
context.addIssue({
|
|
1104
|
+
code: z.ZodIssueCode.custom,
|
|
1105
|
+
message: "a vfx sprite is a manifest-relative image or a character:/object:/gallery: reference; a URL has no content to track",
|
|
1106
|
+
path: ["sprite"]
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1042
1110
|
var CharacterOutfitSchema = z.object({
|
|
1043
1111
|
/** The loop (a character animation asset) to re-clothe, in the same style. */
|
|
1044
1112
|
of: z.string().min(1),
|
|
@@ -1672,7 +1740,7 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
|
|
|
1672
1740
|
message: "tileFeature and styleImages cannot be combined; a connectable set derives its own tile geometry, so remove one or the other",
|
|
1673
1741
|
path: ["tileFeature"]
|
|
1674
1742
|
}).superRefine((style, ctx) => {
|
|
1675
|
-
if (style.quality && (style.generator === "tiles" || style.generator === "terrain" || style.generator === "animation")) {
|
|
1743
|
+
if (style.quality && (style.generator === "tiles" || style.generator === "terrain" || style.generator === "animation" || style.generator === "vfx")) {
|
|
1676
1744
|
ctx.addIssue({
|
|
1677
1745
|
code: z.ZodIssueCode.custom,
|
|
1678
1746
|
message: "quality profiles currently support single-image generators only",
|
|
@@ -1802,6 +1870,8 @@ var AssetSchema = z.object({
|
|
|
1802
1870
|
outfit: CharacterOutfitSchema.optional(),
|
|
1803
1871
|
/** `map` styles only: draw this object in the style of a scene image. See `MapSceneSchema`. */
|
|
1804
1872
|
scene: MapSceneSchema.optional(),
|
|
1873
|
+
/** `vfx` styles only: the effect to render. See `AssetVfxSchema`. */
|
|
1874
|
+
vfx: AssetVfxSchema.optional(),
|
|
1805
1875
|
/** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
|
|
1806
1876
|
proportions: CharacterProportionsSchema.optional(),
|
|
1807
1877
|
/**
|
|
@@ -1878,7 +1948,7 @@ var AssetSchema = z.object({
|
|
|
1878
1948
|
path: ["revision"]
|
|
1879
1949
|
});
|
|
1880
1950
|
}
|
|
1881
|
-
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch", asset.portrait && "portrait", asset.outfit && "outfit", asset.scene && "scene"].filter(Boolean);
|
|
1951
|
+
const shapes = [asset.revision && "revision", asset.state && "state", asset.animation && "animation", asset.mirror && "mirror", asset.batch && "batch", asset.portrait && "portrait", asset.outfit && "outfit", asset.scene && "scene", asset.vfx && "vfx"].filter(Boolean);
|
|
1882
1952
|
if (shapes.length > 1) {
|
|
1883
1953
|
context.addIssue({
|
|
1884
1954
|
code: z.ZodIssueCode.custom,
|
|
@@ -1893,7 +1963,7 @@ var AssetSchema = z.object({
|
|
|
1893
1963
|
path: ["mirror"]
|
|
1894
1964
|
});
|
|
1895
1965
|
}
|
|
1896
|
-
if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit && !asset.animation?.template) {
|
|
1966
|
+
if (asset.prompt === void 0 && !asset.mirror && !asset.portrait && !asset.outfit && !asset.animation?.template && !asset.vfx) {
|
|
1897
1967
|
context.addIssue({
|
|
1898
1968
|
code: z.ZodIssueCode.custom,
|
|
1899
1969
|
message: "Required",
|
|
@@ -2918,6 +2988,11 @@ function frameSetFps(entry) {
|
|
|
2918
2988
|
}
|
|
2919
2989
|
return null;
|
|
2920
2990
|
}
|
|
2991
|
+
function frameSetLoop(entry) {
|
|
2992
|
+
const frameSet = entry.providerMetadata?.[entry.provider]?.frameSet;
|
|
2993
|
+
const loop = frameSet && typeof frameSet === "object" ? frameSet.loop : void 0;
|
|
2994
|
+
return typeof loop === "boolean" ? loop : null;
|
|
2995
|
+
}
|
|
2921
2996
|
|
|
2922
2997
|
// src/palette.ts
|
|
2923
2998
|
function normalizePalette(values) {
|
|
@@ -4339,6 +4414,7 @@ async function inspectRevisionReadiness(spec, lock) {
|
|
|
4339
4414
|
if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
|
|
4340
4415
|
if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
|
|
4341
4416
|
if (spec.scene) return inspectScene(spec);
|
|
4417
|
+
if (spec.vfx?.sprite && "file" in spec.vfx.sprite) return inspectVfxSprite(spec);
|
|
4342
4418
|
if (!spec.revision) return null;
|
|
4343
4419
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
4344
4420
|
}
|
|
@@ -4366,6 +4442,12 @@ async function inspectScene(spec) {
|
|
|
4366
4442
|
}
|
|
4367
4443
|
return { ready: true, reason: "scene is current" };
|
|
4368
4444
|
}
|
|
4445
|
+
async function inspectVfxSprite(spec) {
|
|
4446
|
+
const sprite = spec.vfx.sprite;
|
|
4447
|
+
if (!sprite.sha256 || !existsSync4(sprite.file)) return { ready: false, reason: `vfx sprite does not exist yet: ${sprite.file}` };
|
|
4448
|
+
if (await sha256File(sprite.file) !== sprite.sha256) return { ready: false, reason: "vfx sprite changed after the manifest was resolved" };
|
|
4449
|
+
return { ready: true, reason: "vfx sprite is current" };
|
|
4450
|
+
}
|
|
4369
4451
|
async function inspectMirror(spec, lock, seen) {
|
|
4370
4452
|
const source = spec.mirror.sourceSpec;
|
|
4371
4453
|
const label = `mirror source ${spec.styleId}/${spec.mirror.sourceAssetId}`;
|
|
@@ -4602,7 +4684,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
4602
4684
|
} else if (opts.force) {
|
|
4603
4685
|
state = "missing";
|
|
4604
4686
|
reason = "--force";
|
|
4605
|
-
} else if (!entry && spec.source && spec.generator !== "frames") {
|
|
4687
|
+
} else if (!entry && spec.source && spec.generator !== "frames" && spec.generator !== "vfx") {
|
|
4606
4688
|
if (existsSync5(path9.resolve(spec.root, spec.source))) {
|
|
4607
4689
|
state = "ok";
|
|
4608
4690
|
reason = `placed from ${spec.source}; not generated`;
|
|
@@ -4842,8 +4924,8 @@ async function confirm(question, auto) {
|
|
|
4842
4924
|
}
|
|
4843
4925
|
|
|
4844
4926
|
// src/cli/commands/account.ts
|
|
4845
|
-
import
|
|
4846
|
-
import { existsSync as
|
|
4927
|
+
import path24 from "path";
|
|
4928
|
+
import { existsSync as existsSync17 } from "fs";
|
|
4847
4929
|
|
|
4848
4930
|
// src/env.ts
|
|
4849
4931
|
import { readFileSync, existsSync as existsSync6 } from "fs";
|
|
@@ -5855,10 +5937,10 @@ function canonical2(value) {
|
|
|
5855
5937
|
}
|
|
5856
5938
|
|
|
5857
5939
|
// src/providers/pixellab.ts
|
|
5858
|
-
import { mkdirSync, existsSync as
|
|
5940
|
+
import { mkdirSync, existsSync as existsSync8, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
5859
5941
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5860
5942
|
import os from "os";
|
|
5861
|
-
import
|
|
5943
|
+
import path13 from "path";
|
|
5862
5944
|
|
|
5863
5945
|
// src/client.ts
|
|
5864
5946
|
import { z as z3 } from "zod";
|
|
@@ -6275,9 +6357,9 @@ var PixelLabClient = class {
|
|
|
6275
6357
|
apiKey;
|
|
6276
6358
|
http;
|
|
6277
6359
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
6278
|
-
async request(
|
|
6360
|
+
async request(path53, init) {
|
|
6279
6361
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
6280
|
-
const res = await this.http(`${BASE}${
|
|
6362
|
+
const res = await this.http(`${BASE}${path53}`, {
|
|
6281
6363
|
...init,
|
|
6282
6364
|
headers: {
|
|
6283
6365
|
Authorization: auth,
|
|
@@ -6287,13 +6369,13 @@ var PixelLabClient = class {
|
|
|
6287
6369
|
});
|
|
6288
6370
|
const text = await res.text();
|
|
6289
6371
|
if (!res.ok) {
|
|
6290
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
6372
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path53} \u2192 ${res.status}`, res.status, text);
|
|
6291
6373
|
}
|
|
6292
6374
|
if (!text) return {};
|
|
6293
6375
|
try {
|
|
6294
6376
|
return JSON.parse(text);
|
|
6295
6377
|
} catch {
|
|
6296
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
6378
|
+
throw new Error(`${init?.method ?? "GET"} ${path53} returned invalid JSON`);
|
|
6297
6379
|
}
|
|
6298
6380
|
}
|
|
6299
6381
|
async balance() {
|
|
@@ -6698,10 +6780,10 @@ var PixelLabClient = class {
|
|
|
6698
6780
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
6699
6781
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
6700
6782
|
const south = args.reference?.south;
|
|
6701
|
-
let
|
|
6783
|
+
let path53;
|
|
6702
6784
|
let body;
|
|
6703
6785
|
if (args.mode === "v3") {
|
|
6704
|
-
|
|
6786
|
+
path53 = "/create-character-v3";
|
|
6705
6787
|
body = {
|
|
6706
6788
|
description: args.description,
|
|
6707
6789
|
image_size: imageSize,
|
|
@@ -6715,7 +6797,7 @@ var PixelLabClient = class {
|
|
|
6715
6797
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
6716
6798
|
};
|
|
6717
6799
|
} else if (args.mode === "pro-flash") {
|
|
6718
|
-
|
|
6800
|
+
path53 = "/create-character-pro-flash";
|
|
6719
6801
|
const traits = args.styleTraits;
|
|
6720
6802
|
body = {
|
|
6721
6803
|
description: args.description,
|
|
@@ -6739,7 +6821,7 @@ var PixelLabClient = class {
|
|
|
6739
6821
|
} : {}
|
|
6740
6822
|
};
|
|
6741
6823
|
} else if (args.mode === "pro") {
|
|
6742
|
-
|
|
6824
|
+
path53 = "/create-character-pro";
|
|
6743
6825
|
body = {
|
|
6744
6826
|
description: args.description,
|
|
6745
6827
|
image_size: imageSize,
|
|
@@ -6753,7 +6835,7 @@ var PixelLabClient = class {
|
|
|
6753
6835
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
6754
6836
|
};
|
|
6755
6837
|
} else {
|
|
6756
|
-
|
|
6838
|
+
path53 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
6757
6839
|
body = {
|
|
6758
6840
|
description: args.description,
|
|
6759
6841
|
image_size: imageSize,
|
|
@@ -6770,8 +6852,8 @@ var PixelLabClient = class {
|
|
|
6770
6852
|
...palette
|
|
6771
6853
|
};
|
|
6772
6854
|
}
|
|
6773
|
-
const raw = await this.request(
|
|
6774
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
6855
|
+
const raw = await this.request(path53, { method: "POST", body: JSON.stringify(body) });
|
|
6856
|
+
return validateResponse(CharacterSubmitSchema, raw, path53.slice(1));
|
|
6775
6857
|
}
|
|
6776
6858
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
6777
6859
|
async createCharacterState(args) {
|
|
@@ -7071,8 +7153,8 @@ var PixelLabClient = class {
|
|
|
7071
7153
|
/**
|
|
7072
7154
|
* `/image-to-pixelart-pro-flash`: a photo, painting, or render in, pixel
|
|
7073
7155
|
* art out, with no size to pass: PixelLab detects the native pixel scale
|
|
7074
|
-
* and picks the output size (a 256x256 render came back 108x109
|
|
7075
|
-
*
|
|
7156
|
+
* and picks the output size (a 256x256 render came back 108x109). The
|
|
7157
|
+
* result is opaque, on a near-white background. A flat 6 generations, billed exactly that live
|
|
7076
7158
|
* in October 2026. Sources over 2048px on the longer side are scaled down
|
|
7077
7159
|
* upstream. The finished job carries the image at `last_response.images[0]`.
|
|
7078
7160
|
*/
|
|
@@ -7443,6 +7525,337 @@ function clientFromEnv() {
|
|
|
7443
7525
|
return new PixelLabClient(key);
|
|
7444
7526
|
}
|
|
7445
7527
|
|
|
7528
|
+
// src/pixellab-mcp.ts
|
|
7529
|
+
var PIXELLAB_MCP_URL = "https://api.pixellab.ai/mcp";
|
|
7530
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
7531
|
+
function parseMcpBody(body) {
|
|
7532
|
+
const trimmed = body.trim();
|
|
7533
|
+
if (trimmed.startsWith("{")) return JSON.parse(trimmed);
|
|
7534
|
+
let last;
|
|
7535
|
+
for (const line of body.split(/\r?\n/)) {
|
|
7536
|
+
if (!line.startsWith("data:")) continue;
|
|
7537
|
+
const payload = line.slice(5).trim();
|
|
7538
|
+
if (!payload) continue;
|
|
7539
|
+
const message6 = JSON.parse(payload);
|
|
7540
|
+
if ("id" in message6) last = message6;
|
|
7541
|
+
}
|
|
7542
|
+
if (!last) throw new Error(`PixelLab MCP reply had no JSON-RPC response: ${body.slice(0, 200)}`);
|
|
7543
|
+
return last;
|
|
7544
|
+
}
|
|
7545
|
+
function isBusyReply(text) {
|
|
7546
|
+
return /\bbusy\b.*\bretry\b/i.test(text);
|
|
7547
|
+
}
|
|
7548
|
+
var PixelLabMcpClient = class {
|
|
7549
|
+
constructor(apiKey, opts = {}) {
|
|
7550
|
+
this.apiKey = apiKey;
|
|
7551
|
+
if (!apiKey) throw new Error("PIXELLAB_API_KEY is required");
|
|
7552
|
+
this.url = opts.url ?? PIXELLAB_MCP_URL;
|
|
7553
|
+
this.http = fetchWithRetry(opts.fetch, { timeoutMs: opts.timeoutMs ?? 18e4 });
|
|
7554
|
+
this.busyRetries = opts.busyRetries ?? 4;
|
|
7555
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
7556
|
+
}
|
|
7557
|
+
apiKey;
|
|
7558
|
+
http;
|
|
7559
|
+
url;
|
|
7560
|
+
busyRetries;
|
|
7561
|
+
sleep;
|
|
7562
|
+
nextId = 1;
|
|
7563
|
+
/**
|
|
7564
|
+
* Call one tool and return its text and images. A tool-level error
|
|
7565
|
+
* (`isError`) throws with the tool's own message, after a few spaced
|
|
7566
|
+
* retries when the tool only said it was busy.
|
|
7567
|
+
*/
|
|
7568
|
+
async callTool(name, args) {
|
|
7569
|
+
for (let attempt = 0; ; attempt++) {
|
|
7570
|
+
const reply = await this.callOnce(name, args);
|
|
7571
|
+
if (!reply.isError) return { text: reply.text, images: reply.images };
|
|
7572
|
+
if (attempt < this.busyRetries && isBusyReply(reply.text)) {
|
|
7573
|
+
await this.sleep(2e3 * 2 ** attempt);
|
|
7574
|
+
continue;
|
|
7575
|
+
}
|
|
7576
|
+
throw new Error(`PixelLab ${name}: ${reply.text || "the tool reported an error"}`);
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
async callOnce(name, args) {
|
|
7580
|
+
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
7581
|
+
const id = this.nextId++;
|
|
7582
|
+
const res = await this.http(this.url, {
|
|
7583
|
+
method: "POST",
|
|
7584
|
+
headers: {
|
|
7585
|
+
Authorization: auth,
|
|
7586
|
+
"Content-Type": "application/json",
|
|
7587
|
+
Accept: "application/json, text/event-stream",
|
|
7588
|
+
"MCP-Protocol-Version": PROTOCOL_VERSION
|
|
7589
|
+
},
|
|
7590
|
+
body: JSON.stringify({ jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: args } })
|
|
7591
|
+
});
|
|
7592
|
+
const body = await res.text();
|
|
7593
|
+
if (!res.ok) throw new PixelLabError(`MCP ${name} \u2192 ${res.status}`, res.status, body);
|
|
7594
|
+
const message6 = parseMcpBody(body);
|
|
7595
|
+
const error = message6.error;
|
|
7596
|
+
if (error) throw new Error(`PixelLab MCP ${name} failed: ${error.message ?? JSON.stringify(error)}`);
|
|
7597
|
+
const result = message6.result ?? {};
|
|
7598
|
+
const text = (result.content ?? []).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
7599
|
+
const images = (result.content ?? []).filter((block) => block.type === "image" && typeof block.data === "string").map((block) => ({ mimeType: String(block.mimeType ?? "image/png"), data: block.data }));
|
|
7600
|
+
return { text, images, isError: Boolean(result.isError) };
|
|
7601
|
+
}
|
|
7602
|
+
};
|
|
7603
|
+
function mcpClientFromEnv() {
|
|
7604
|
+
const key = process.env.PIXELLAB_API_KEY;
|
|
7605
|
+
if (!key) {
|
|
7606
|
+
throw new Error(
|
|
7607
|
+
"PIXELLAB_API_KEY is not set.\nLooked in the environment, and in .env.local / .env beside the manifest and in the current directory."
|
|
7608
|
+
);
|
|
7609
|
+
}
|
|
7610
|
+
return new PixelLabMcpClient(key);
|
|
7611
|
+
}
|
|
7612
|
+
|
|
7613
|
+
// src/pixellab-vfx.ts
|
|
7614
|
+
import { existsSync as existsSync7 } from "fs";
|
|
7615
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
7616
|
+
import path12 from "path";
|
|
7617
|
+
var VFX_TOOL = "vfx_workbench";
|
|
7618
|
+
var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
7619
|
+
function parseVfxStep(text) {
|
|
7620
|
+
const match = new RegExp(`effect step: (${UUID})`).exec(text);
|
|
7621
|
+
if (!match) throw new Error(`PixelLab vfx_workbench returned no step id:
|
|
7622
|
+
${text.slice(0, 600)}`);
|
|
7623
|
+
return match[1];
|
|
7624
|
+
}
|
|
7625
|
+
function parseVfxRecipe(text) {
|
|
7626
|
+
const lines = text.split(/\r?\n/);
|
|
7627
|
+
const at = lines.findIndex((line) => line.trim() === "working recipe:");
|
|
7628
|
+
const json = at >= 0 ? lines[at + 1]?.trim() : void 0;
|
|
7629
|
+
if (!json?.startsWith("{")) return null;
|
|
7630
|
+
return JSON.parse(json);
|
|
7631
|
+
}
|
|
7632
|
+
function parseVfxExport(text) {
|
|
7633
|
+
const effectId = new RegExp(`effect id (${UUID})`).exec(text)?.[1];
|
|
7634
|
+
const download = /download (https:\/\/\S+\/): (.+)$/m.exec(text);
|
|
7635
|
+
const size = /(\d+)x(\d+), (\d+) frames @ ([\d.]+) fps, ([^;\n]+)/.exec(text);
|
|
7636
|
+
if (!effectId || !download || !size) {
|
|
7637
|
+
throw new Error(`PixelLab vfx_workbench export reply was not understood:
|
|
7638
|
+
${text.slice(0, 600)}`);
|
|
7639
|
+
}
|
|
7640
|
+
const anchor = /anchor[^\n]*? at (-?\d+),\s*(-?\d+)/.exec(text);
|
|
7641
|
+
return {
|
|
7642
|
+
effectId,
|
|
7643
|
+
editUrl: /(https:\/\/pixellab\.ai\/create-vfx\?effect=[0-9a-f-]+)/.exec(text)?.[1] ?? null,
|
|
7644
|
+
width: Number(size[1]),
|
|
7645
|
+
height: Number(size[2]),
|
|
7646
|
+
frames: Number(size[3]),
|
|
7647
|
+
fps: Number(size[4]),
|
|
7648
|
+
loop: !/plays once/.test(size[5]),
|
|
7649
|
+
anchor: anchor ? { x: Number(anchor[1]), y: Number(anchor[2]) } : null,
|
|
7650
|
+
downloadBase: download[1],
|
|
7651
|
+
files: download[2].split(",").map((file) => file.trim()).filter(Boolean),
|
|
7652
|
+
imageId: new RegExp(`image id: (${UUID})`).exec(text)?.[1] ?? null
|
|
7653
|
+
};
|
|
7654
|
+
}
|
|
7655
|
+
async function describeVfx(client2, topic) {
|
|
7656
|
+
return (await client2.callTool(VFX_TOOL, { argv: ["describe", topic] })).text;
|
|
7657
|
+
}
|
|
7658
|
+
function vfxOutputPaths(out) {
|
|
7659
|
+
const base = path12.resolve(out.replace(/\.(png|gif|json|zip)$/i, "").replace(/\.vfx$/i, ""));
|
|
7660
|
+
return {
|
|
7661
|
+
sheet: `${base}.png`,
|
|
7662
|
+
gif: `${base}.gif`,
|
|
7663
|
+
atlas: `${base}.json`,
|
|
7664
|
+
provenance: `${base}.vfx.json`,
|
|
7665
|
+
gamePack: `${base}-game.zip`
|
|
7666
|
+
};
|
|
7667
|
+
}
|
|
7668
|
+
function isVfxReference(sprite) {
|
|
7669
|
+
return /^(character|object|gallery):/.test(sprite) || /^(https?|data):/.test(sprite);
|
|
7670
|
+
}
|
|
7671
|
+
async function resolveVfxSprite(sprite) {
|
|
7672
|
+
if (isVfxReference(sprite)) return sprite;
|
|
7673
|
+
const bytes = await readFile6(sprite);
|
|
7674
|
+
const metadata = imageMetadata(bytes);
|
|
7675
|
+
if (!metadata) throw new Error(`${sprite} is not a readable PNG or JPEG`);
|
|
7676
|
+
return `data:image/${metadata.format};base64,${bytes.toString("base64")}`;
|
|
7677
|
+
}
|
|
7678
|
+
async function readVfxRecipe(file) {
|
|
7679
|
+
const parsed = JSON.parse(await readFile6(file, "utf8"));
|
|
7680
|
+
const record = parsed.tool === VFX_TOOL && parsed.recipe && typeof parsed.recipe === "object";
|
|
7681
|
+
const recipe = record ? parsed.recipe : parsed;
|
|
7682
|
+
if (!Array.isArray(recipe.layers)) throw new Error(`${file} is not a VFX recipe: it has no "layers" list`);
|
|
7683
|
+
const recorded = record ? parsed.source?.sprite : void 0;
|
|
7684
|
+
if (typeof recorded !== "string") return { recipe };
|
|
7685
|
+
const sprite = isVfxReference(recorded) ? recorded : path12.resolve(path12.dirname(path12.resolve(file)), recorded);
|
|
7686
|
+
return { recipe, sprite };
|
|
7687
|
+
}
|
|
7688
|
+
async function defaultDownload(url) {
|
|
7689
|
+
const http = fetchWithRetry();
|
|
7690
|
+
const res = await http(url);
|
|
7691
|
+
if (!res.ok) throw new Error(`download ${url} \u2192 ${res.status}`);
|
|
7692
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
7693
|
+
if (buf.length > MAX_DOWNLOAD_BYTES) throw new Error(`download ${url} exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
|
|
7694
|
+
return buf;
|
|
7695
|
+
}
|
|
7696
|
+
function localPathFor(file, paths) {
|
|
7697
|
+
if (/-game\.zip$/i.test(file)) return paths.gamePack;
|
|
7698
|
+
if (/\.gif$/i.test(file)) return paths.gif;
|
|
7699
|
+
if (/\.json$/i.test(file)) return paths.atlas;
|
|
7700
|
+
if (/\.png$/i.test(file)) return paths.sheet;
|
|
7701
|
+
return null;
|
|
7702
|
+
}
|
|
7703
|
+
async function runVfxWorkbench(client2, opts) {
|
|
7704
|
+
const starts = [opts.preset, opts.recipe, opts.effect].filter((value) => value !== void 0).length;
|
|
7705
|
+
if (starts > 1) throw new Error("Start a VFX from one of a preset, a recipe, or a saved effect, not several");
|
|
7706
|
+
if (starts === 0 && !opts.parts?.length) {
|
|
7707
|
+
throw new Error("A VFX needs something to start from: a preset, a recipe, a saved effect, or at least one part");
|
|
7708
|
+
}
|
|
7709
|
+
const progress = opts.onProgress ?? (() => {
|
|
7710
|
+
});
|
|
7711
|
+
const call = async (argv) => (await client2.callTool(VFX_TOOL, { argv, ...opts.notes ? { notes: opts.notes } : {} })).text;
|
|
7712
|
+
const startArgv = ["start"];
|
|
7713
|
+
if (opts.sprite) startArgv.push("--sprite", opts.sprite);
|
|
7714
|
+
if (opts.preset) startArgv.push("--preset", opts.preset);
|
|
7715
|
+
if (opts.effect) startArgv.push("--effect", opts.effect);
|
|
7716
|
+
progress(opts.preset ? `starting from preset ${opts.preset}` : opts.effect ? `starting from saved effect ${opts.effect}` : "starting");
|
|
7717
|
+
const started = await call(startArgv);
|
|
7718
|
+
let step = parseVfxStep(started);
|
|
7719
|
+
const savedName = opts.effect ? parseVfxRecipe(started)?.name : opts.recipe?.name;
|
|
7720
|
+
const name = opts.name ?? (typeof savedName === "string" ? savedName : void 0) ?? opts.defaultName;
|
|
7721
|
+
if (opts.recipe) {
|
|
7722
|
+
progress("rendering the recipe");
|
|
7723
|
+
step = parseVfxStep(await call([step, "render", JSON.stringify({ recipe: opts.recipe })]));
|
|
7724
|
+
}
|
|
7725
|
+
if (opts.parts?.length) {
|
|
7726
|
+
progress(`adding ${opts.parts.map((part) => part.name).join(", ")}`);
|
|
7727
|
+
step = parseVfxStep(await call([step, "add_part", JSON.stringify({ parts: opts.parts })]));
|
|
7728
|
+
}
|
|
7729
|
+
if (opts.fps !== void 0) {
|
|
7730
|
+
progress(`setting ${opts.fps} fps`);
|
|
7731
|
+
step = parseVfxStep(await call([step, "render", JSON.stringify({ patch: [{ op: "set", path: "fps", value: opts.fps }] })]));
|
|
7732
|
+
}
|
|
7733
|
+
const exportArgv = [step, "export", "--name", name];
|
|
7734
|
+
if (opts.replace) exportArgv.push("--replace", opts.replace);
|
|
7735
|
+
if (opts.gamePack) exportArgv.push("--game-pack");
|
|
7736
|
+
progress(opts.gamePack ? "exporting with a game pack" : "exporting");
|
|
7737
|
+
const exported = parseVfxExport(await call(exportArgv));
|
|
7738
|
+
await opts.onExported?.(exported, name);
|
|
7739
|
+
progress("reading the saved recipe");
|
|
7740
|
+
const recipe = parseVfxRecipe(await call(["start", "--effect", exported.effectId]));
|
|
7741
|
+
return { exported, recipe, name };
|
|
7742
|
+
}
|
|
7743
|
+
async function readSavedVfxRecipe(client2, effectId) {
|
|
7744
|
+
try {
|
|
7745
|
+
return parseVfxRecipe((await client2.callTool(VFX_TOOL, { argv: ["start", "--effect", effectId] })).text);
|
|
7746
|
+
} catch (error) {
|
|
7747
|
+
if (error instanceof Error && /no saved effect/i.test(error.message)) return null;
|
|
7748
|
+
throw error;
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7751
|
+
function vfxRecipeSha256(recipe) {
|
|
7752
|
+
const { name: _name, notes: _notes, ...rest } = recipe;
|
|
7753
|
+
return sha256(JSON.stringify(rest));
|
|
7754
|
+
}
|
|
7755
|
+
function sliceVfxSheet(sheet, facts, atlas) {
|
|
7756
|
+
const image = decodePng(sheet);
|
|
7757
|
+
const listed = atlas?.frames;
|
|
7758
|
+
const rects = Array.isArray(listed) && listed.length === facts.frames ? listed.map((entry) => entry.frame) : Array.from({ length: facts.frames }, (_, index) => {
|
|
7759
|
+
const columns = Math.max(1, Math.floor(image.width / facts.width));
|
|
7760
|
+
return { x: index % columns * facts.width, y: Math.floor(index / columns) * facts.height, w: facts.width, h: facts.height };
|
|
7761
|
+
});
|
|
7762
|
+
return rects.map((rect, index) => {
|
|
7763
|
+
if (rect.x < 0 || rect.y < 0 || rect.x + rect.w > image.width || rect.y + rect.h > image.height) {
|
|
7764
|
+
throw new Error(
|
|
7765
|
+
`VFX frame ${index + 1} (${rect.w}x${rect.h} at ${rect.x},${rect.y}) falls outside the ${image.width}x${image.height} sheet`
|
|
7766
|
+
);
|
|
7767
|
+
}
|
|
7768
|
+
const out = Buffer.alloc(rect.w * rect.h * 4);
|
|
7769
|
+
for (let y = 0; y < rect.h; y++) {
|
|
7770
|
+
const from = ((rect.y + y) * image.width + rect.x) * 4;
|
|
7771
|
+
image.pixels.copy(out, y * rect.w * 4, from, from + rect.w * 4);
|
|
7772
|
+
}
|
|
7773
|
+
return encodeRgbaPng(rect.w, rect.h, out);
|
|
7774
|
+
});
|
|
7775
|
+
}
|
|
7776
|
+
async function makeVfx(client2, opts) {
|
|
7777
|
+
const starts = [opts.preset, opts.recipe, opts.effect].filter((value) => value !== void 0).length;
|
|
7778
|
+
if (starts > 1) throw new Error("Start a VFX from one of a preset, a recipe, or a saved effect, not several");
|
|
7779
|
+
if (starts === 0 && !opts.parts?.length) {
|
|
7780
|
+
throw new Error("A VFX needs something to start from: a preset, a recipe, a saved effect, or at least one part");
|
|
7781
|
+
}
|
|
7782
|
+
const paths = vfxOutputPaths(opts.out);
|
|
7783
|
+
const wanted = [paths.sheet, paths.gif, paths.atlas, paths.provenance, ...opts.gamePack ? [paths.gamePack] : []];
|
|
7784
|
+
if (!opts.force) {
|
|
7785
|
+
for (const file of wanted) {
|
|
7786
|
+
if (existsSync7(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
7787
|
+
}
|
|
7788
|
+
}
|
|
7789
|
+
const progress = opts.onProgress ?? (() => {
|
|
7790
|
+
});
|
|
7791
|
+
const { exported, recipe, name } = await runVfxWorkbench(client2, {
|
|
7792
|
+
preset: opts.preset,
|
|
7793
|
+
recipe: opts.recipe,
|
|
7794
|
+
effect: opts.effect,
|
|
7795
|
+
parts: opts.parts,
|
|
7796
|
+
sprite: opts.sprite ? await resolveVfxSprite(opts.sprite) : void 0,
|
|
7797
|
+
fps: opts.fps,
|
|
7798
|
+
name: opts.name,
|
|
7799
|
+
defaultName: opts.preset ?? path12.basename(paths.sheet, ".png"),
|
|
7800
|
+
replace: opts.replace ?? opts.effect,
|
|
7801
|
+
gamePack: opts.gamePack,
|
|
7802
|
+
notes: "pixelkiln vfx",
|
|
7803
|
+
onProgress: progress
|
|
7804
|
+
});
|
|
7805
|
+
const download = opts.download ?? defaultDownload;
|
|
7806
|
+
const written = { sheet: paths.sheet, gif: paths.gif, atlas: paths.atlas, provenance: paths.provenance };
|
|
7807
|
+
await mkdir4(path12.dirname(paths.sheet), { recursive: true });
|
|
7808
|
+
for (const file of exported.files) {
|
|
7809
|
+
const target = localPathFor(file, paths);
|
|
7810
|
+
if (!target) continue;
|
|
7811
|
+
let bytes = await download(`${exported.downloadBase}${encodeURIComponent(file)}`);
|
|
7812
|
+
if (target === paths.atlas) {
|
|
7813
|
+
const atlas = JSON.parse(bytes.toString("utf8"));
|
|
7814
|
+
if (atlas.meta) atlas.meta.image = path12.basename(paths.sheet);
|
|
7815
|
+
bytes = Buffer.from(`${JSON.stringify(atlas, null, 2)}
|
|
7816
|
+
`);
|
|
7817
|
+
}
|
|
7818
|
+
await writeFile4(target, bytes);
|
|
7819
|
+
if (target === paths.gamePack) written.gamePack = target;
|
|
7820
|
+
}
|
|
7821
|
+
for (const file of [paths.sheet, paths.gif, paths.atlas]) {
|
|
7822
|
+
if (!existsSync7(file)) throw new Error(`PixelLab's export listed no file for ${path12.basename(file)}: ${exported.files.join(", ")}`);
|
|
7823
|
+
}
|
|
7824
|
+
const spriteRecord = !opts.sprite || /^data:/.test(opts.sprite) ? void 0 : isVfxReference(opts.sprite) ? opts.sprite : path12.relative(path12.dirname(paths.provenance), path12.resolve(opts.sprite)).split(path12.sep).join("/");
|
|
7825
|
+
const provenance = {
|
|
7826
|
+
version: 1,
|
|
7827
|
+
provider: "pixellab",
|
|
7828
|
+
tool: VFX_TOOL,
|
|
7829
|
+
name,
|
|
7830
|
+
effectId: exported.effectId,
|
|
7831
|
+
editUrl: exported.editUrl,
|
|
7832
|
+
source: {
|
|
7833
|
+
...opts.preset ? { preset: opts.preset } : {},
|
|
7834
|
+
...opts.effect ? { effect: opts.effect } : {},
|
|
7835
|
+
...opts.recipe ? { recipeFile: true } : {},
|
|
7836
|
+
...opts.parts?.length ? { parts: opts.parts } : {},
|
|
7837
|
+
...spriteRecord ? { sprite: spriteRecord } : {}
|
|
7838
|
+
},
|
|
7839
|
+
width: exported.width,
|
|
7840
|
+
height: exported.height,
|
|
7841
|
+
frames: exported.frames,
|
|
7842
|
+
fps: exported.fps,
|
|
7843
|
+
loop: exported.loop,
|
|
7844
|
+
anchor: exported.anchor,
|
|
7845
|
+
files: {
|
|
7846
|
+
sheet: path12.basename(paths.sheet),
|
|
7847
|
+
gif: path12.basename(paths.gif),
|
|
7848
|
+
atlas: path12.basename(paths.atlas),
|
|
7849
|
+
...written.gamePack ? { gamePack: path12.basename(written.gamePack) } : {}
|
|
7850
|
+
},
|
|
7851
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7852
|
+
recipe
|
|
7853
|
+
};
|
|
7854
|
+
await writeFile4(paths.provenance, `${JSON.stringify(provenance, null, 2)}
|
|
7855
|
+
`);
|
|
7856
|
+
return { ...exported, paths: written, recipe };
|
|
7857
|
+
}
|
|
7858
|
+
|
|
7446
7859
|
// src/providers/pixellab.ts
|
|
7447
7860
|
function pixelLabObjectUrl(generator, objectId) {
|
|
7448
7861
|
if (!objectId) return null;
|
|
@@ -7457,6 +7870,7 @@ function pixelLabObjectUrl(generator, objectId) {
|
|
|
7457
7870
|
const characterId = objectId.split("#")[0];
|
|
7458
7871
|
return characterId ? `https://www.pixellab.ai/create-character/${encodeURIComponent(characterId)}` : null;
|
|
7459
7872
|
}
|
|
7873
|
+
if (generator === "vfx") return `https://pixellab.ai/create-vfx?effect=${encodeURIComponent(objectId)}`;
|
|
7460
7874
|
return null;
|
|
7461
7875
|
}
|
|
7462
7876
|
function billedFromUsage(usage) {
|
|
@@ -7632,13 +8046,20 @@ function decodeObjectAnimationJob(jobId) {
|
|
|
7632
8046
|
return null;
|
|
7633
8047
|
}
|
|
7634
8048
|
var PixelLabProvider = class _PixelLabProvider {
|
|
7635
|
-
|
|
8049
|
+
/**
|
|
8050
|
+
* `mcp` reaches the tools PixelLab offers only over MCP (the VFX
|
|
8051
|
+
* workbench). Offline and download-only providers have none, and a `vfx`
|
|
8052
|
+
* submission through one says so.
|
|
8053
|
+
*/
|
|
8054
|
+
constructor(client2, mcp) {
|
|
7636
8055
|
this.client = client2;
|
|
8056
|
+
this.mcp = mcp;
|
|
7637
8057
|
}
|
|
7638
8058
|
client;
|
|
8059
|
+
mcp;
|
|
7639
8060
|
id = "pixellab";
|
|
7640
8061
|
static fromEnv() {
|
|
7641
|
-
return new _PixelLabProvider(clientFromEnv());
|
|
8062
|
+
return new _PixelLabProvider(clientFromEnv(), mcpClientFromEnv());
|
|
7642
8063
|
}
|
|
7643
8064
|
/** Public storage URLs and the local content cache do not require API auth. */
|
|
7644
8065
|
static forDownloads() {
|
|
@@ -7649,7 +8070,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7649
8070
|
return new _PixelLabProvider(new PixelLabClient("download-only"));
|
|
7650
8071
|
}
|
|
7651
8072
|
supports(generator) {
|
|
7652
|
-
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement" || generator === "imageProFlash";
|
|
8073
|
+
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement" || generator === "imageProFlash" || generator === "vfx";
|
|
7653
8074
|
}
|
|
7654
8075
|
/** `/map-objects` style matching: `background_image` plus `inpainting`. */
|
|
7655
8076
|
supportsMapScene = true;
|
|
@@ -7691,11 +8112,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7691
8112
|
* is a file read.
|
|
7692
8113
|
*/
|
|
7693
8114
|
static cacheDir(namespace = "pixflux") {
|
|
7694
|
-
const dir =
|
|
8115
|
+
const dir = path13.join(os.tmpdir(), `pixelkiln-${namespace}`);
|
|
7695
8116
|
mkdirSync(dir, { recursive: true });
|
|
7696
8117
|
return dir;
|
|
7697
8118
|
}
|
|
7698
8119
|
estimate(spec) {
|
|
8120
|
+
if (spec.vfx) return { unit: "generations", amount: 0, candidates: 1 };
|
|
7699
8121
|
if (spec.batch?.role === "member") {
|
|
7700
8122
|
return { unit: "generations", amount: 0, candidates: 1 };
|
|
7701
8123
|
}
|
|
@@ -7753,6 +8175,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7753
8175
|
};
|
|
7754
8176
|
}
|
|
7755
8177
|
validate(spec, styleImages) {
|
|
8178
|
+
if (spec.generator === "vfx" && styleImages.length) {
|
|
8179
|
+
throw new Error("PixelLab vfx takes no style images; an effect's look comes from its preset, parts, or recipe");
|
|
8180
|
+
}
|
|
7756
8181
|
const members = spec.revision?.sourceMembers;
|
|
7757
8182
|
if (spec.revision && members && !MEMBER_SET_REVISION_MODES.includes(spec.revision.mode)) {
|
|
7758
8183
|
throw new Error(
|
|
@@ -8037,7 +8462,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8037
8462
|
}
|
|
8038
8463
|
}
|
|
8039
8464
|
if (endFrame) {
|
|
8040
|
-
const start = startFrame ?? (character.parentFile &&
|
|
8465
|
+
const start = startFrame ?? (character.parentFile && existsSync8(character.parentFile) ? imageMetadata(readFileSync2(character.parentFile)) : null);
|
|
8041
8466
|
if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
|
|
8042
8467
|
throw new Error(
|
|
8043
8468
|
`${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "character's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
|
|
@@ -8195,7 +8620,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8195
8620
|
}
|
|
8196
8621
|
}
|
|
8197
8622
|
if (endFrame) {
|
|
8198
|
-
const start = startFrame ?? (object.parentFile &&
|
|
8623
|
+
const start = startFrame ?? (object.parentFile && existsSync8(object.parentFile) ? imageMetadata(readFileSync2(object.parentFile)) : null);
|
|
8199
8624
|
if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
|
|
8200
8625
|
throw new Error(
|
|
8201
8626
|
`${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "object's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
|
|
@@ -8505,7 +8930,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8505
8930
|
const sources = images.map((image, index) => {
|
|
8506
8931
|
if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
|
|
8507
8932
|
const role = meta?.frameRoles?.[index] ?? void 0;
|
|
8508
|
-
const file =
|
|
8933
|
+
const file = path13.join(dir, `${jobId}-${index}.png`);
|
|
8509
8934
|
writeFileSync(file, Buffer.from(image.base64, "base64"));
|
|
8510
8935
|
return { url: `file://${file}`, ...role ? { role } : {} };
|
|
8511
8936
|
});
|
|
@@ -8657,6 +9082,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8657
9082
|
async submit(spec, styleImages, context) {
|
|
8658
9083
|
this.validate(spec, styleImages);
|
|
8659
9084
|
if (spec.revision) return this.submitRevision(spec);
|
|
9085
|
+
if (spec.vfx) return this.submitVfx(spec, context);
|
|
8660
9086
|
if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
|
|
8661
9087
|
if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
|
|
8662
9088
|
if (spec.generator === "uiAsset") {
|
|
@@ -8710,7 +9136,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8710
9136
|
seed: spec.seed
|
|
8711
9137
|
});
|
|
8712
9138
|
const jobId = randomUUID3();
|
|
8713
|
-
writeFileSync(
|
|
9139
|
+
writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
|
|
8714
9140
|
return { jobId };
|
|
8715
9141
|
}
|
|
8716
9142
|
if (spec.generator === "tiles") {
|
|
@@ -9039,11 +9465,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9039
9465
|
cacheCleanupResult(pngs, usage, roles, fps) {
|
|
9040
9466
|
const jobId = randomUUID3();
|
|
9041
9467
|
if (!roles) {
|
|
9042
|
-
writeFileSync(
|
|
9468
|
+
writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
|
|
9043
9469
|
return { jobId, metadata: { revisionUsage: usage } };
|
|
9044
9470
|
}
|
|
9045
9471
|
for (const [index, png] of pngs.entries()) {
|
|
9046
|
-
writeFileSync(
|
|
9472
|
+
writeFileSync(path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
|
|
9047
9473
|
}
|
|
9048
9474
|
return { jobId, metadata: { revisionUsage: usage, revisionMembers: roles, ...fps != null ? { revisionFps: fps } : {} } };
|
|
9049
9475
|
}
|
|
@@ -9071,10 +9497,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9071
9497
|
return this.pollAnimateRevision(jobId, context);
|
|
9072
9498
|
}
|
|
9073
9499
|
if (context?.spec?.revision) return this.pollRevision(jobId);
|
|
9500
|
+
if (generator === "vfx") return this.pollVfx(jobId, context);
|
|
9074
9501
|
if (generator === "imageProFlash") return this.pollRevision(jobId);
|
|
9075
9502
|
if (generator === "pixflux") {
|
|
9076
|
-
const file =
|
|
9077
|
-
if (
|
|
9503
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
9504
|
+
if (existsSync8(file)) {
|
|
9078
9505
|
const sourceUrl = `file://${file}`;
|
|
9079
9506
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
|
|
9080
9507
|
}
|
|
@@ -9117,6 +9544,120 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9117
9544
|
* background job to ask afterwards the way `billedForJob` does for async
|
|
9118
9545
|
* work.
|
|
9119
9546
|
*/
|
|
9547
|
+
/**
|
|
9548
|
+
* Render a `vfx` asset with the VFX workbench, which is synchronous and
|
|
9549
|
+
* free. The frames are cut from the exported sheet and parked in the
|
|
9550
|
+
* cache like pixflux's bytes; the export's download folder is recorded so
|
|
9551
|
+
* `poll` can cut them again if the cache is cleared.
|
|
9552
|
+
*
|
|
9553
|
+
* A regeneration exports over the effect this asset rendered last time, so
|
|
9554
|
+
* My effects keeps one entry per asset, unless that effect has changed on
|
|
9555
|
+
* pixellab.ai since (someone moved its sliders) or is gone. Then a new
|
|
9556
|
+
* effect is saved and the edited one is left alone.
|
|
9557
|
+
*/
|
|
9558
|
+
async submitVfx(spec, context) {
|
|
9559
|
+
const vfx = spec.vfx;
|
|
9560
|
+
if (!this.mcp) throw new Error("PixelLab vfx renders over PixelLab's MCP server, which this provider was built without");
|
|
9561
|
+
let sprite;
|
|
9562
|
+
if (vfx.sprite && "reference" in vfx.sprite) {
|
|
9563
|
+
sprite = vfx.sprite.reference;
|
|
9564
|
+
} else if (vfx.sprite) {
|
|
9565
|
+
const { file } = vfx.sprite;
|
|
9566
|
+
if (!existsSync8(file)) throw new Error(`vfx sprite does not exist yet: ${file}`);
|
|
9567
|
+
const bytes = readFileSync2(file);
|
|
9568
|
+
if (sha256(bytes) !== vfx.sprite.sha256) {
|
|
9569
|
+
throw new Error(`vfx sprite changed after the manifest was resolved: ${file}; run the command again`);
|
|
9570
|
+
}
|
|
9571
|
+
const format = imageMetadata(bytes)?.format ?? "png";
|
|
9572
|
+
sprite = `data:image/${format};base64,${bytes.toString("base64")}`;
|
|
9573
|
+
}
|
|
9574
|
+
const previous = context?.replacedMetadata?.vfx ?? context?.lastGenerationMetadata?.vfx;
|
|
9575
|
+
let replace;
|
|
9576
|
+
let newEffect;
|
|
9577
|
+
if (previous?.effectId) {
|
|
9578
|
+
const upstream = await readSavedVfxRecipe(this.mcp, previous.effectId);
|
|
9579
|
+
if (!upstream) newEffect = `effect ${previous.effectId} is no longer in My effects`;
|
|
9580
|
+
else if (!previous.recipe || vfxRecipeSha256(upstream) !== vfxRecipeSha256(previous.recipe)) {
|
|
9581
|
+
newEffect = `effect ${previous.effectId} changed on pixellab.ai since it was rendered; it was kept`;
|
|
9582
|
+
} else replace = previous.effectId;
|
|
9583
|
+
}
|
|
9584
|
+
const jobId = randomUUID3();
|
|
9585
|
+
const facts = (exported2, name2) => ({
|
|
9586
|
+
effectId: exported2.effectId,
|
|
9587
|
+
editUrl: exported2.editUrl,
|
|
9588
|
+
name: name2,
|
|
9589
|
+
imageId: exported2.imageId,
|
|
9590
|
+
width: exported2.width,
|
|
9591
|
+
height: exported2.height,
|
|
9592
|
+
frames: exported2.frames,
|
|
9593
|
+
fps: exported2.fps,
|
|
9594
|
+
loop: exported2.loop,
|
|
9595
|
+
anchor: exported2.anchor,
|
|
9596
|
+
downloadBase: exported2.downloadBase,
|
|
9597
|
+
files: exported2.files,
|
|
9598
|
+
replaced: replace ?? null,
|
|
9599
|
+
...newEffect ? { newEffect } : {}
|
|
9600
|
+
});
|
|
9601
|
+
const { exported, recipe, name } = await runVfxWorkbench(this.mcp, {
|
|
9602
|
+
preset: vfx.preset,
|
|
9603
|
+
recipe: vfx.recipe?.recipe,
|
|
9604
|
+
parts: vfx.parts,
|
|
9605
|
+
sprite,
|
|
9606
|
+
fps: vfx.fps,
|
|
9607
|
+
name: vfx.name,
|
|
9608
|
+
defaultName: vfx.name,
|
|
9609
|
+
replace,
|
|
9610
|
+
notes: `pixelkiln ${spec.styleId}/${spec.assetId}`,
|
|
9611
|
+
// The effect exists from here on; record it before anything else can fail.
|
|
9612
|
+
onExported: (done, effectName) => context?.checkpoint({ jobId, complete: true, metadata: { vfx: facts(done, effectName) } })
|
|
9613
|
+
});
|
|
9614
|
+
await this.cacheVfxFrames(jobId, exported);
|
|
9615
|
+
return { jobId, metadata: { vfx: { ...facts(exported, name), recipe } } };
|
|
9616
|
+
}
|
|
9617
|
+
/** Cut an export's sheet into frames, with a sidecar `poll` reads without any lock metadata. */
|
|
9618
|
+
async cacheVfxFrames(jobId, exported) {
|
|
9619
|
+
const sheetFile = exported.files.find((file) => /\.png$/i.test(file));
|
|
9620
|
+
const atlasFile = exported.files.find((file) => /\.json$/i.test(file));
|
|
9621
|
+
if (!sheetFile) throw new Error(`PixelLab's vfx export listed no spritesheet: ${exported.files.join(", ")}`);
|
|
9622
|
+
const sheet = await this.client.download(`${exported.downloadBase}${encodeURIComponent(sheetFile)}`);
|
|
9623
|
+
const atlas = atlasFile ? JSON.parse((await this.client.download(`${exported.downloadBase}${encodeURIComponent(atlasFile)}`)).toString("utf8")) : void 0;
|
|
9624
|
+
const frames = sliceVfxSheet(sheet, exported, atlas);
|
|
9625
|
+
const dir = _PixelLabProvider.cacheDir("vfx");
|
|
9626
|
+
frames.forEach((frame, index) => writeFileSync(path13.join(dir, `${jobId}-${index}.png`), frame));
|
|
9627
|
+
writeFileSync(
|
|
9628
|
+
path13.join(dir, `${jobId}.json`),
|
|
9629
|
+
JSON.stringify({ effectId: exported.effectId, count: frames.length, fps: exported.fps, loop: exported.loop })
|
|
9630
|
+
);
|
|
9631
|
+
}
|
|
9632
|
+
async pollVfx(jobId, context) {
|
|
9633
|
+
const dir = _PixelLabProvider.cacheDir("vfx");
|
|
9634
|
+
const sidecar = path13.join(dir, `${jobId}.json`);
|
|
9635
|
+
const cached = () => {
|
|
9636
|
+
if (!existsSync8(sidecar)) return null;
|
|
9637
|
+
const facts2 = JSON.parse(readFileSync2(sidecar, "utf8"));
|
|
9638
|
+
const files2 = Array.from({ length: facts2.count }, (_, index) => path13.join(dir, `${jobId}-${index}.png`));
|
|
9639
|
+
return files2.every((file) => existsSync8(file)) ? { facts: facts2, files: files2 } : null;
|
|
9640
|
+
};
|
|
9641
|
+
let found = cached();
|
|
9642
|
+
const recorded = context?.metadata?.vfx;
|
|
9643
|
+
if (!found && recorded?.downloadBase && Array.isArray(recorded.files)) {
|
|
9644
|
+
await this.cacheVfxFrames(jobId, recorded);
|
|
9645
|
+
found = cached();
|
|
9646
|
+
}
|
|
9647
|
+
if (!found) {
|
|
9648
|
+
return { status: "failed", error: "vfx frames are no longer cached locally; re-run submit for this asset (free)" };
|
|
9649
|
+
}
|
|
9650
|
+
const { facts, files } = found;
|
|
9651
|
+
const sources = files.length === 1 ? [{ url: `file://${files[0]}` }] : files.map((file, index) => ({ url: `file://${file}`, role: `frame-${String(index).padStart(2, "0")}` }));
|
|
9652
|
+
return {
|
|
9653
|
+
status: "ready",
|
|
9654
|
+
objectId: facts.effectId,
|
|
9655
|
+
sourceUrl: sources[0].url,
|
|
9656
|
+
sources,
|
|
9657
|
+
...files.length > 1 ? { metadata: { frameSet: { fps: facts.fps, count: files.length, loop: facts.loop } } } : {},
|
|
9658
|
+
billed: null
|
|
9659
|
+
};
|
|
9660
|
+
}
|
|
9120
9661
|
async pollCachedRevision(jobId, context) {
|
|
9121
9662
|
const usage = context?.metadata?.revisionUsage;
|
|
9122
9663
|
const roles = context?.metadata?.revisionMembers;
|
|
@@ -9126,10 +9667,10 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9126
9667
|
};
|
|
9127
9668
|
if (Array.isArray(roles) && roles.length) {
|
|
9128
9669
|
const sources = roles.map((role, index) => ({
|
|
9129
|
-
url: `file://${
|
|
9670
|
+
url: `file://${path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
|
|
9130
9671
|
role: String(role)
|
|
9131
9672
|
}));
|
|
9132
|
-
if (sources.some((source) => !
|
|
9673
|
+
if (sources.some((source) => !existsSync8(source.url.slice("file://".length)))) return gone;
|
|
9133
9674
|
const fps = context?.metadata?.revisionFps;
|
|
9134
9675
|
const frames = sources.every((source) => source.role.startsWith("frame-"));
|
|
9135
9676
|
return {
|
|
@@ -9141,8 +9682,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9141
9682
|
billed: billedFromUsage(usage)
|
|
9142
9683
|
};
|
|
9143
9684
|
}
|
|
9144
|
-
const file =
|
|
9145
|
-
if (!
|
|
9685
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
9686
|
+
if (!existsSync8(file)) return gone;
|
|
9146
9687
|
const sourceUrl = `file://${file}`;
|
|
9147
9688
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
|
|
9148
9689
|
}
|
|
@@ -9198,7 +9739,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9198
9739
|
}
|
|
9199
9740
|
const base64 = extractBase64(item);
|
|
9200
9741
|
if (base64) {
|
|
9201
|
-
const file =
|
|
9742
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
9202
9743
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
9203
9744
|
urls.push(`file://${file}`);
|
|
9204
9745
|
continue;
|
|
@@ -9249,7 +9790,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9249
9790
|
const candidate = done[key];
|
|
9250
9791
|
const base64 = extractBase64(candidate);
|
|
9251
9792
|
if (base64) {
|
|
9252
|
-
const file =
|
|
9793
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
9253
9794
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
9254
9795
|
const sourceUrl = `file://${file}`;
|
|
9255
9796
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
@@ -9258,7 +9799,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9258
9799
|
if (Array.isArray(done.images) && done.images.length) {
|
|
9259
9800
|
const base64 = extractBase64(done.images[0]);
|
|
9260
9801
|
if (base64) {
|
|
9261
|
-
const file =
|
|
9802
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
9262
9803
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
9263
9804
|
const sourceUrl = `file://${file}`;
|
|
9264
9805
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
@@ -9292,7 +9833,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9292
9833
|
for (const [index, candidate] of done.images.entries()) {
|
|
9293
9834
|
const base64 = extractBase64(candidate);
|
|
9294
9835
|
if (!base64) continue;
|
|
9295
|
-
const file =
|
|
9836
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
9296
9837
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
9297
9838
|
urls.push(`file://${file}`);
|
|
9298
9839
|
}
|
|
@@ -9322,7 +9863,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9322
9863
|
for (const [index, candidate] of images.entries()) {
|
|
9323
9864
|
const base64 = extractBase64(candidate);
|
|
9324
9865
|
if (!base64) continue;
|
|
9325
|
-
const file =
|
|
9866
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
9326
9867
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
9327
9868
|
urls.push(`file://${file}`);
|
|
9328
9869
|
}
|
|
@@ -9420,7 +9961,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9420
9961
|
for (const [index, tile] of tileset.tiles.entries()) {
|
|
9421
9962
|
const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
|
|
9422
9963
|
const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
|
|
9423
|
-
const file =
|
|
9964
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
|
|
9424
9965
|
writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
|
|
9425
9966
|
sources.push({ url: `file://${file}`, role });
|
|
9426
9967
|
terrainTiles.push({ role, corners: tile.corners, pattern4x4: tile.pattern_4x4 });
|
|
@@ -9450,7 +9991,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9450
9991
|
const { image, usage } = await this.client.getIsometricTile(tileId);
|
|
9451
9992
|
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
9452
9993
|
const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
|
|
9453
|
-
const file =
|
|
9994
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
|
|
9454
9995
|
writeFileSync(file, Buffer.from(image.base64, "base64"));
|
|
9455
9996
|
return {
|
|
9456
9997
|
status: "ready",
|
|
@@ -9494,8 +10035,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
9494
10035
|
return { objectId: `${jobId}#${index}`, sourceUrl: url };
|
|
9495
10036
|
}
|
|
9496
10037
|
if (generator === "imagePro" || generator === "uiElement") {
|
|
9497
|
-
const file =
|
|
9498
|
-
if (!
|
|
10038
|
+
const file = path13.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
10039
|
+
if (!existsSync8(file)) throw new Error(`${generator} job ${jobId} has no cached candidate at index ${index}`);
|
|
9499
10040
|
return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
|
|
9500
10041
|
}
|
|
9501
10042
|
const promoted = await this.client.selectFrames(jobId, [index], commonTag);
|
|
@@ -9772,9 +10313,9 @@ var RetroDiffusionClient = class {
|
|
|
9772
10313
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
9773
10314
|
return Buffer.from(await response.arrayBuffer());
|
|
9774
10315
|
}
|
|
9775
|
-
async call(
|
|
10316
|
+
async call(path53, init = {}) {
|
|
9776
10317
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
9777
|
-
const response = await this.request(`${this.baseUrl}${
|
|
10318
|
+
const response = await this.request(`${this.baseUrl}${path53}`, {
|
|
9778
10319
|
...init,
|
|
9779
10320
|
headers: {
|
|
9780
10321
|
"Content-Type": "application/json",
|
|
@@ -10086,7 +10627,7 @@ function retroError(value) {
|
|
|
10086
10627
|
import { createHash as createHash3 } from "crypto";
|
|
10087
10628
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
10088
10629
|
import os2 from "os";
|
|
10089
|
-
import
|
|
10630
|
+
import path14 from "path";
|
|
10090
10631
|
var DEFAULT_BASE_URL3 = "https://api.cloud.scenario.com/v1";
|
|
10091
10632
|
var SOURCE_PROTOCOL2 = "scenario:";
|
|
10092
10633
|
var SUBMIT_RETRIES = 8;
|
|
@@ -10469,7 +11010,7 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
10469
11010
|
}
|
|
10470
11011
|
};
|
|
10471
11012
|
function uploadCachePath() {
|
|
10472
|
-
return process.env.SCENARIO_UPLOAD_CACHE ??
|
|
11013
|
+
return process.env.SCENARIO_UPLOAD_CACHE ?? path14.join(os2.tmpdir(), "pixelkiln-scenario", "uploads.json");
|
|
10473
11014
|
}
|
|
10474
11015
|
function uploadCacheKey(baseUrl, projectId, digest2) {
|
|
10475
11016
|
return createHash3("sha256").update(`${baseUrl}
|
|
@@ -10487,7 +11028,7 @@ function readUploadCache() {
|
|
|
10487
11028
|
function writeUploadCache(key, assetId) {
|
|
10488
11029
|
try {
|
|
10489
11030
|
const file = uploadCachePath();
|
|
10490
|
-
mkdirSync2(
|
|
11031
|
+
mkdirSync2(path14.dirname(file), { recursive: true });
|
|
10491
11032
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
10492
11033
|
writeFileSync2(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
|
|
10493
11034
|
renameSync(tmp, file);
|
|
@@ -10747,9 +11288,9 @@ registerProvider({
|
|
|
10747
11288
|
});
|
|
10748
11289
|
|
|
10749
11290
|
// src/manifest.ts
|
|
10750
|
-
import { readFile as
|
|
10751
|
-
import { existsSync as
|
|
10752
|
-
import
|
|
11291
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
11292
|
+
import { existsSync as existsSync9 } from "fs";
|
|
11293
|
+
import path15 from "path";
|
|
10753
11294
|
|
|
10754
11295
|
// src/loop-directions.ts
|
|
10755
11296
|
var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
|
|
@@ -10838,9 +11379,9 @@ function isObject2(value) {
|
|
|
10838
11379
|
|
|
10839
11380
|
// src/manifest.ts
|
|
10840
11381
|
async function loadManifest(manifestPath) {
|
|
10841
|
-
const abs =
|
|
10842
|
-
if (!
|
|
10843
|
-
const expansion = expandLoopDirections(JSON.parse(await
|
|
11382
|
+
const abs = path15.resolve(manifestPath);
|
|
11383
|
+
if (!existsSync9(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
11384
|
+
const expansion = expandLoopDirections(JSON.parse(await readFile7(abs, "utf8")));
|
|
10844
11385
|
if (expansion.issues.length) {
|
|
10845
11386
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
10846
11387
|
${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
|
|
@@ -10895,7 +11436,7 @@ ${issues}`);
|
|
|
10895
11436
|
if (fromStyle) {
|
|
10896
11437
|
if (!styleIds.has(fromStyle)) {
|
|
10897
11438
|
unknownReferences.push(`assets.${assetId}.revision.fromStyle: unknown style "${fromStyle}"`);
|
|
10898
|
-
} else if (parent && parent
|
|
11439
|
+
} else if (parent && !assetInStyle(parent, fromStyle, parsed.data.styles[fromStyle])) {
|
|
10899
11440
|
unknownReferences.push(
|
|
10900
11441
|
`assets.${assetId}.revision.fromStyle: "${asset.revision.from}" is not in style "${fromStyle}"`
|
|
10901
11442
|
);
|
|
@@ -10942,7 +11483,11 @@ ${issues}`);
|
|
|
10942
11483
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
10943
11484
|
}
|
|
10944
11485
|
const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
|
|
10945
|
-
return { manifest: parsed.data, root:
|
|
11486
|
+
return { manifest: parsed.data, root: path15.dirname(abs), path: abs, ...loopFamilies };
|
|
11487
|
+
}
|
|
11488
|
+
function assetInStyle(asset, styleId, style) {
|
|
11489
|
+
if (asset.styles.length) return asset.styles.includes(styleId);
|
|
11490
|
+
return style?.generator === "vfx" === Boolean(asset.vfx);
|
|
10946
11491
|
}
|
|
10947
11492
|
function parentAssetId(asset) {
|
|
10948
11493
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
|
|
@@ -11046,8 +11591,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11046
11591
|
const needed = /* @__PURE__ */ new Set();
|
|
11047
11592
|
for (const assetId of resolutionAssetIds) {
|
|
11048
11593
|
const asset = manifest.assets[assetId];
|
|
11049
|
-
if (asset
|
|
11050
|
-
const fromStyle = asset
|
|
11594
|
+
if (!asset || !assetInStyle(asset, styleId, manifest.styles[styleId])) continue;
|
|
11595
|
+
const fromStyle = asset.revision?.fromStyle;
|
|
11051
11596
|
if (fromStyle && fromStyle !== styleId) needed.add(fromStyle);
|
|
11052
11597
|
}
|
|
11053
11598
|
styleDependencies.set(styleId, needed);
|
|
@@ -11076,11 +11621,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11076
11621
|
const styleFinalizers = /* @__PURE__ */ new Map();
|
|
11077
11622
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
11078
11623
|
async function loadStyleImage(rel, what = "Style image") {
|
|
11079
|
-
const abs =
|
|
11624
|
+
const abs = path15.resolve(root, rel);
|
|
11080
11625
|
let hit = styleImageCache.get(abs);
|
|
11081
11626
|
if (!hit) {
|
|
11082
|
-
if (!
|
|
11083
|
-
const buf = await
|
|
11627
|
+
if (!existsSync9(abs)) throw new Error(`${what} not found: ${abs}`);
|
|
11628
|
+
const buf = await readFile7(abs);
|
|
11084
11629
|
const metadata = imageMetadata(buf);
|
|
11085
11630
|
if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
|
|
11086
11631
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -11106,14 +11651,14 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11106
11651
|
styleImageDimensions.push(loadedImage);
|
|
11107
11652
|
}
|
|
11108
11653
|
const resolvedImages = style.styleImages.map((image) => {
|
|
11109
|
-
const hit = styleImageCache.get(
|
|
11654
|
+
const hit = styleImageCache.get(path15.resolve(root, image.path));
|
|
11110
11655
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
11111
11656
|
});
|
|
11112
11657
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
11113
11658
|
const providerInputIdentities = /* @__PURE__ */ new Map();
|
|
11114
11659
|
for (const [assetId, asset] of Object.entries(manifest.assets)) {
|
|
11115
11660
|
if (!resolutionAssetIds.has(assetId)) continue;
|
|
11116
|
-
if (asset
|
|
11661
|
+
if (!assetInStyle(asset, styleId, style)) continue;
|
|
11117
11662
|
const generator = style.generator;
|
|
11118
11663
|
if (!activeProvider.supports(generator)) {
|
|
11119
11664
|
throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
|
|
@@ -11133,6 +11678,15 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11133
11678
|
const providerInputs = inputResolution.inputs;
|
|
11134
11679
|
const providerInputIdentity = inputResolution.identity ?? providerInputs;
|
|
11135
11680
|
providerInputIdentities.set(assetId, providerInputIdentity);
|
|
11681
|
+
if (asset.vfx && generator !== "vfx") {
|
|
11682
|
+
throw new Error(`assets.${assetId}: vfx needs a vfx style; "${styleId}" generates ${generator}`);
|
|
11683
|
+
}
|
|
11684
|
+
if (generator === "vfx" && !asset.vfx && !asset.mirror && !asset.revision) {
|
|
11685
|
+
throw new Error(
|
|
11686
|
+
`assets.${assetId}: style "${styleId}" renders effects; give the asset a vfx block, or make it a mirror or revision of one`
|
|
11687
|
+
);
|
|
11688
|
+
}
|
|
11689
|
+
const vfx = generator === "vfx" && asset.vfx ? await resolveVfxShape(root, assetId, asset.vfx, `pixelkiln:${styleId}/${assetId}`) : void 0;
|
|
11136
11690
|
let width;
|
|
11137
11691
|
let height;
|
|
11138
11692
|
let size;
|
|
@@ -11160,6 +11714,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11160
11714
|
size = asset.size ?? style.size ?? 64;
|
|
11161
11715
|
width = size;
|
|
11162
11716
|
height = size;
|
|
11717
|
+
} else if (generator === "vfx") {
|
|
11718
|
+
const canvas = vfx?.recipe?.recipe.canvas;
|
|
11719
|
+
width = typeof canvas?.w === "number" ? canvas.w : asset.width ?? style.size ?? 64;
|
|
11720
|
+
height = typeof canvas?.h === "number" ? canvas.h : asset.height ?? style.size ?? 64;
|
|
11721
|
+
size = Math.max(width, height);
|
|
11163
11722
|
} else if (generator === "uiAsset" || generator === "uiElement") {
|
|
11164
11723
|
width = asset.width ?? style.size ?? 256;
|
|
11165
11724
|
height = asset.height ?? style.size ?? 256;
|
|
@@ -11202,14 +11761,14 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11202
11761
|
terrainUpperDescription = wrap(parsed.upper);
|
|
11203
11762
|
terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
|
|
11204
11763
|
prompt = subject.trim();
|
|
11205
|
-
} else if ((generator === "character" || generator === "objectPro") && characterKind !== "base") {
|
|
11764
|
+
} else if ((generator === "character" || generator === "objectPro") && characterKind !== "base" || vfx) {
|
|
11206
11765
|
prompt = subject.trim();
|
|
11207
11766
|
} else {
|
|
11208
11767
|
prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
11209
11768
|
}
|
|
11210
|
-
const relFile = asset.file ??
|
|
11211
|
-
const outFile =
|
|
11212
|
-
const qualityOutFile = style.quality ?
|
|
11769
|
+
const relFile = asset.file ?? path15.join(asset.category ?? "", `${assetId}.png`);
|
|
11770
|
+
const outFile = path15.resolve(root, style.outDir, relFile);
|
|
11771
|
+
const qualityOutFile = style.quality ? path15.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
11213
11772
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
11214
11773
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
11215
11774
|
const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
|
|
@@ -11280,6 +11839,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11280
11839
|
// A portrait needs a character style (refused above), so an
|
|
11281
11840
|
// objectPro asset is only ever a base, state, or animation.
|
|
11282
11841
|
...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
11842
|
+
...vfx ? { vfx } : {},
|
|
11283
11843
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
|
|
11284
11844
|
costUnit: "generations",
|
|
11285
11845
|
candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
|
|
@@ -11304,7 +11864,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11304
11864
|
minGridConfidence: style.quality.minGridConfidence,
|
|
11305
11865
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
11306
11866
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
11307
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
11867
|
+
...style.quality.fixerPython ? { fixerPython: path15.resolve(root, style.quality.fixerPython) } : {},
|
|
11308
11868
|
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
11309
11869
|
}
|
|
11310
11870
|
} : {},
|
|
@@ -11387,7 +11947,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11387
11947
|
const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
|
|
11388
11948
|
resolved.character = {
|
|
11389
11949
|
...resolved.character,
|
|
11390
|
-
styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256:
|
|
11950
|
+
styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: existsSync9(file) ? await sha256File(file) : null }
|
|
11391
11951
|
};
|
|
11392
11952
|
}
|
|
11393
11953
|
const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
|
|
@@ -11400,7 +11960,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11400
11960
|
parentAssetId: characterParent,
|
|
11401
11961
|
parentSpec,
|
|
11402
11962
|
parentFile,
|
|
11403
|
-
parentSha256:
|
|
11963
|
+
parentSha256: existsSync9(parentFile) ? await sha256File(parentFile) : null,
|
|
11404
11964
|
// A state and its animations keep the engine and rotations of the base.
|
|
11405
11965
|
mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
|
|
11406
11966
|
directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
|
|
@@ -11418,7 +11978,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11418
11978
|
parentAssetId: characterParent,
|
|
11419
11979
|
parentSpec,
|
|
11420
11980
|
parentFile,
|
|
11421
|
-
parentSha256:
|
|
11981
|
+
parentSha256: existsSync9(parentFile) ? await sha256File(parentFile) : null,
|
|
11422
11982
|
// A state and its animations keep the base's own rotation count.
|
|
11423
11983
|
directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
|
|
11424
11984
|
};
|
|
@@ -11440,7 +12000,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11440
12000
|
animation: sourceSpec.character.animation,
|
|
11441
12001
|
outfit: {
|
|
11442
12002
|
reference: {
|
|
11443
|
-
path:
|
|
12003
|
+
path: path15.resolve(root, asset.outfit.reference),
|
|
11444
12004
|
sha256: referenceImage.hash,
|
|
11445
12005
|
width: referenceImage.width,
|
|
11446
12006
|
height: referenceImage.height,
|
|
@@ -11463,23 +12023,23 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11463
12023
|
}
|
|
11464
12024
|
const sourceStyleId = asset.revision.fromStyle && asset.revision.fromStyle !== styleId ? asset.revision.fromStyle : void 0;
|
|
11465
12025
|
const sourceSpec = sourceStyleId ? await styleFinalizers.get(sourceStyleId)(asset.revision.from) : await finalize(asset.revision.from);
|
|
11466
|
-
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
12026
|
+
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path15.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
11467
12027
|
const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
|
|
11468
12028
|
const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
|
|
11469
12029
|
const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
|
|
11470
12030
|
const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
|
|
11471
|
-
const maskFile = asset.revision.mask ?
|
|
12031
|
+
const maskFile = asset.revision.mask ? path15.resolve(root, asset.revision.mask) : void 0;
|
|
11472
12032
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
11473
12033
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
11474
12034
|
throw new Error(
|
|
11475
12035
|
`Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
|
|
11476
12036
|
);
|
|
11477
12037
|
}
|
|
11478
|
-
const paletteImageFile = asset.revision.paletteImage ?
|
|
12038
|
+
const paletteImageFile = asset.revision.paletteImage ? path15.resolve(root, asset.revision.paletteImage) : void 0;
|
|
11479
12039
|
const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
|
|
11480
|
-
const lastFrameFile = asset.revision.lastFrame ?
|
|
12040
|
+
const lastFrameFile = asset.revision.lastFrame ? path15.resolve(root, asset.revision.lastFrame) : void 0;
|
|
11481
12041
|
const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
|
|
11482
|
-
const keypointsFile = asset.revision.keypointsFile ?
|
|
12042
|
+
const keypointsFile = asset.revision.keypointsFile ? path15.resolve(root, asset.revision.keypointsFile) : void 0;
|
|
11483
12043
|
const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
|
|
11484
12044
|
resolved.revision = {
|
|
11485
12045
|
mode: asset.revision.mode,
|
|
@@ -11649,7 +12209,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
11649
12209
|
return specs;
|
|
11650
12210
|
}
|
|
11651
12211
|
function pngPath(file) {
|
|
11652
|
-
const extension =
|
|
12212
|
+
const extension = path15.extname(file);
|
|
11653
12213
|
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
11654
12214
|
}
|
|
11655
12215
|
async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
|
|
@@ -11659,12 +12219,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
|
|
|
11659
12219
|
if (!provider.supportsMapScene) {
|
|
11660
12220
|
throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
|
|
11661
12221
|
}
|
|
11662
|
-
const file =
|
|
12222
|
+
const file = path15.resolve(root, scene.image);
|
|
11663
12223
|
const image = await optionalRevisionImage(file, "scene image", "png");
|
|
11664
12224
|
const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
|
|
11665
12225
|
const placement = scene.placement;
|
|
11666
12226
|
if ("mask" in placement) {
|
|
11667
|
-
const maskFile =
|
|
12227
|
+
const maskFile = path15.resolve(root, placement.mask);
|
|
11668
12228
|
const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
|
|
11669
12229
|
if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
|
|
11670
12230
|
throw new Error(
|
|
@@ -11678,9 +12238,38 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
|
|
|
11678
12238
|
}
|
|
11679
12239
|
return "oval" in placement ? { ...base, placement: { type: "oval", fraction: placement.oval } } : { ...base, placement: { type: "rectangle", fraction: placement.rectangle } };
|
|
11680
12240
|
}
|
|
12241
|
+
async function resolveVfxShape(root, assetId, vfx, defaultName) {
|
|
12242
|
+
let recipe;
|
|
12243
|
+
let recordedSprite;
|
|
12244
|
+
if (vfx.recipe) {
|
|
12245
|
+
const file = path15.resolve(root, vfx.recipe);
|
|
12246
|
+
if (!existsSync9(file)) throw new Error(`assets.${assetId}.vfx.recipe: VFX recipe not found: ${file}`);
|
|
12247
|
+
const read = await readVfxRecipe(file).catch((error) => {
|
|
12248
|
+
throw new Error(`assets.${assetId}.vfx.recipe: ${error instanceof Error ? error.message : String(error)}`);
|
|
12249
|
+
});
|
|
12250
|
+
recipe = { file, sha256: vfxRecipeSha256(read.recipe), recipe: read.recipe };
|
|
12251
|
+
recordedSprite = read.sprite;
|
|
12252
|
+
}
|
|
12253
|
+
const spriteRef = vfx.sprite ? isVfxReference(vfx.sprite) ? vfx.sprite : path15.resolve(root, vfx.sprite) : recordedSprite;
|
|
12254
|
+
let sprite;
|
|
12255
|
+
if (spriteRef && isVfxReference(spriteRef)) {
|
|
12256
|
+
sprite = { reference: spriteRef };
|
|
12257
|
+
} else if (spriteRef) {
|
|
12258
|
+
const image = await optionalRevisionImage(spriteRef, `assets.${assetId}.vfx.sprite`);
|
|
12259
|
+
sprite = { file: spriteRef, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null, format: image?.format ?? null };
|
|
12260
|
+
}
|
|
12261
|
+
return {
|
|
12262
|
+
...vfx.preset ? { preset: vfx.preset } : {},
|
|
12263
|
+
...recipe ? { recipe } : {},
|
|
12264
|
+
...vfx.parts?.length ? { parts: vfx.parts } : {},
|
|
12265
|
+
...sprite ? { sprite } : {},
|
|
12266
|
+
...vfx.fps !== void 0 ? { fps: vfx.fps } : {},
|
|
12267
|
+
name: vfx.name ?? defaultName
|
|
12268
|
+
};
|
|
12269
|
+
}
|
|
11681
12270
|
async function optionalRevisionImage(file, label, requiredFormat) {
|
|
11682
|
-
if (!
|
|
11683
|
-
const bytes = await
|
|
12271
|
+
if (!existsSync9(file)) return null;
|
|
12272
|
+
const bytes = await readFile7(file);
|
|
11684
12273
|
const metadata = imageMetadata(bytes);
|
|
11685
12274
|
if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
|
|
11686
12275
|
if (requiredFormat && metadata.format !== requiredFormat) {
|
|
@@ -11689,8 +12278,8 @@ async function optionalRevisionImage(file, label, requiredFormat) {
|
|
|
11689
12278
|
return { hash: sha256(bytes), ...metadata };
|
|
11690
12279
|
}
|
|
11691
12280
|
async function optionalSkeletonSet(file) {
|
|
11692
|
-
if (!
|
|
11693
|
-
const bytes = await
|
|
12281
|
+
if (!existsSync9(file)) return null;
|
|
12282
|
+
const bytes = await readFile7(file);
|
|
11694
12283
|
let json;
|
|
11695
12284
|
try {
|
|
11696
12285
|
json = JSON.parse(bytes.toString("utf8"));
|
|
@@ -11700,17 +12289,17 @@ async function optionalSkeletonSet(file) {
|
|
|
11700
12289
|
return { hash: sha256(bytes), skeleton: parseSkeletonSet(json, file) };
|
|
11701
12290
|
}
|
|
11702
12291
|
async function revisionSourceMembers(stem, label) {
|
|
11703
|
-
if (
|
|
12292
|
+
if (existsSync9(stem)) return null;
|
|
11704
12293
|
const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
|
|
11705
12294
|
let roles = [];
|
|
11706
12295
|
for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
|
|
11707
|
-
if (set.every((direction) =>
|
|
12296
|
+
if (set.every((direction) => existsSync9(at(direction)))) {
|
|
11708
12297
|
roles = [...set];
|
|
11709
12298
|
break;
|
|
11710
12299
|
}
|
|
11711
12300
|
}
|
|
11712
12301
|
if (!roles.length) {
|
|
11713
|
-
for (let index = 0;
|
|
12302
|
+
for (let index = 0; existsSync9(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
|
|
11714
12303
|
roles.push(`frame-${String(index).padStart(2, "0")}`);
|
|
11715
12304
|
}
|
|
11716
12305
|
}
|
|
@@ -11736,8 +12325,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
11736
12325
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
11737
12326
|
const out = [];
|
|
11738
12327
|
for (const img of style.styleImages) {
|
|
11739
|
-
const file =
|
|
11740
|
-
const buf = await
|
|
12328
|
+
const file = path15.resolve(loaded.root, img.path);
|
|
12329
|
+
const buf = await readFile7(file);
|
|
11741
12330
|
const metadata = imageMetadata(buf);
|
|
11742
12331
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
11743
12332
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -11764,7 +12353,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
11764
12353
|
};
|
|
11765
12354
|
if (asset.concept) {
|
|
11766
12355
|
const image = await files.load(asset.concept, "Concept image");
|
|
11767
|
-
shape.concept = { path:
|
|
12356
|
+
shape.concept = { path: path15.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11768
12357
|
}
|
|
11769
12358
|
if (asset.reference) {
|
|
11770
12359
|
const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
|
|
@@ -11772,7 +12361,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
11772
12361
|
for (const [direction, rel] of Object.entries(byDirection)) {
|
|
11773
12362
|
const image = await files.load(rel, `Reference sprite (${direction})`);
|
|
11774
12363
|
shape.reference[direction] = {
|
|
11775
|
-
path:
|
|
12364
|
+
path: path15.resolve(files.root, rel),
|
|
11776
12365
|
sha256: image.hash,
|
|
11777
12366
|
width: image.width,
|
|
11778
12367
|
height: image.height,
|
|
@@ -11793,7 +12382,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
11793
12382
|
const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
|
|
11794
12383
|
const pose = async (rel, what) => {
|
|
11795
12384
|
const image = await files.load(rel, what);
|
|
11796
|
-
return { path:
|
|
12385
|
+
return { path: path15.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11797
12386
|
};
|
|
11798
12387
|
shape.animation = {
|
|
11799
12388
|
mode: animationMode,
|
|
@@ -11824,7 +12413,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
11824
12413
|
const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
|
|
11825
12414
|
if (south) {
|
|
11826
12415
|
const image = await files.load(south, "Reference sprite");
|
|
11827
|
-
shape.reference = { path:
|
|
12416
|
+
shape.reference = { path: path15.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11828
12417
|
}
|
|
11829
12418
|
}
|
|
11830
12419
|
if (asset.state) {
|
|
@@ -11852,7 +12441,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
11852
12441
|
}
|
|
11853
12442
|
const pose = async (rel, what) => {
|
|
11854
12443
|
const image = await files.load(rel, what);
|
|
11855
|
-
return { path:
|
|
12444
|
+
return { path: path15.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
11856
12445
|
};
|
|
11857
12446
|
shape.animation = {
|
|
11858
12447
|
mode: asset.animation.mode ?? "v3",
|
|
@@ -11869,9 +12458,9 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
11869
12458
|
}
|
|
11870
12459
|
|
|
11871
12460
|
// src/pipeline/fetch.ts
|
|
11872
|
-
import { existsSync as
|
|
11873
|
-
import { mkdir as
|
|
11874
|
-
import
|
|
12461
|
+
import { existsSync as existsSync10, readFileSync as readFileSync4 } from "fs";
|
|
12462
|
+
import { mkdir as mkdir5, readFile as readFile8, rename as rename4, rm as rm4, writeFile as writeFile5 } from "fs/promises";
|
|
12463
|
+
import path16 from "path";
|
|
11875
12464
|
|
|
11876
12465
|
// src/source-url.ts
|
|
11877
12466
|
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -11917,7 +12506,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11917
12506
|
const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
|
|
11918
12507
|
const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
11919
12508
|
normalizeLockOutputPaths(lock, specs);
|
|
11920
|
-
const cacheDir = opts.cacheDir === false ? null :
|
|
12509
|
+
const cacheDir = opts.cacheDir === false ? null : path16.resolve(opts.cacheDir ?? path16.join(path16.dirname(lockPath), ".pixelkiln", "cache"));
|
|
11921
12510
|
const pending = Object.entries(lock.entries).filter(([key, e]) => {
|
|
11922
12511
|
if (e.provider !== provider.id || !specByKey.has(key)) return false;
|
|
11923
12512
|
if (specByKey.get(key)?.mirror) return false;
|
|
@@ -11928,14 +12517,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
11928
12517
|
if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
|
|
11929
12518
|
return Boolean(opts.force) || e.outputs.every((output) => {
|
|
11930
12519
|
const file = resolveOutputPath(output.path, spec.root);
|
|
11931
|
-
return
|
|
12520
|
+
return existsSync10(file) && sha256(readFileSync4(file)) === output.sha256;
|
|
11932
12521
|
});
|
|
11933
12522
|
}
|
|
11934
12523
|
if (!opts.repair) return false;
|
|
11935
12524
|
if (e.outputs.length === 0 || !spec) return true;
|
|
11936
12525
|
return e.outputs.some((output) => {
|
|
11937
12526
|
const file = resolveOutputPath(output.path, spec.root);
|
|
11938
|
-
if (!
|
|
12527
|
+
if (!existsSync10(file)) return true;
|
|
11939
12528
|
const current = sha256(readFileSync4(file));
|
|
11940
12529
|
if (current === output.sha256) return false;
|
|
11941
12530
|
return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
|
|
@@ -12019,21 +12608,21 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12019
12608
|
if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
|
|
12020
12609
|
fresh = await provider.download(source.url);
|
|
12021
12610
|
if (recorded && sha256(fresh) === recordedRaw && !reapply) {
|
|
12022
|
-
log2(` current ${
|
|
12611
|
+
log2(` current ${path16.relative(process.cwd(), target)}`);
|
|
12023
12612
|
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
12024
12613
|
continue;
|
|
12025
12614
|
}
|
|
12026
12615
|
}
|
|
12027
12616
|
let raw = null;
|
|
12028
12617
|
let final = null;
|
|
12029
|
-
if (
|
|
12618
|
+
if (existsSync10(target)) {
|
|
12030
12619
|
const currentHash = await sha256File(target);
|
|
12031
12620
|
if (recorded && currentHash === recorded.sha256) {
|
|
12032
12621
|
if (!fresh && !reapply) {
|
|
12033
12622
|
if (cacheDir) {
|
|
12034
12623
|
await cacheMedia(
|
|
12035
12624
|
cacheDir,
|
|
12036
|
-
await
|
|
12625
|
+
await readFile8(target),
|
|
12037
12626
|
recorded.mediaType ?? MediaType.PNG,
|
|
12038
12627
|
recorded.sha256
|
|
12039
12628
|
);
|
|
@@ -12042,10 +12631,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12042
12631
|
continue;
|
|
12043
12632
|
}
|
|
12044
12633
|
if (reapply && !fresh) {
|
|
12045
|
-
if (!recorded.raw) raw = await
|
|
12046
|
-
log2(` reapply ${
|
|
12634
|
+
if (!recorded.raw) raw = await readFile8(target);
|
|
12635
|
+
log2(` reapply ${path16.relative(process.cwd(), target)}`);
|
|
12047
12636
|
} else {
|
|
12048
|
-
log2(` changed ${
|
|
12637
|
+
log2(` changed ${path16.relative(process.cwd(), target)} (upstream)`);
|
|
12049
12638
|
}
|
|
12050
12639
|
} else {
|
|
12051
12640
|
const superseded = (entry.supersededOutputs ?? []).find(
|
|
@@ -12066,14 +12655,14 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12066
12655
|
}
|
|
12067
12656
|
if (outgoing && cacheDir) {
|
|
12068
12657
|
try {
|
|
12069
|
-
const oldBytes = await
|
|
12658
|
+
const oldBytes = await readFile8(target);
|
|
12070
12659
|
const oldMediaType = detectMediaType(oldBytes);
|
|
12071
12660
|
if (oldMediaType) await cacheMedia(cacheDir, oldBytes, oldMediaType, currentHash);
|
|
12072
12661
|
} catch {
|
|
12073
12662
|
}
|
|
12074
12663
|
}
|
|
12075
12664
|
log2(
|
|
12076
|
-
` replace ${
|
|
12665
|
+
` replace ${path16.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
12077
12666
|
);
|
|
12078
12667
|
}
|
|
12079
12668
|
}
|
|
@@ -12082,7 +12671,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12082
12671
|
} else if (!raw && recorded && cacheDir) {
|
|
12083
12672
|
if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
|
|
12084
12673
|
if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
|
|
12085
|
-
if (final || raw) log2(` cached ${
|
|
12674
|
+
if (final || raw) log2(` cached ${path16.relative(process.cwd(), target)}`);
|
|
12086
12675
|
}
|
|
12087
12676
|
if (!final && !raw) {
|
|
12088
12677
|
if (!source.url) {
|
|
@@ -12109,13 +12698,13 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12109
12698
|
buf = processed.bytes;
|
|
12110
12699
|
if (processed.changed) {
|
|
12111
12700
|
rawHash = sha256(raw);
|
|
12112
|
-
log2(` snapped ${
|
|
12701
|
+
log2(` snapped ${path16.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
|
|
12113
12702
|
}
|
|
12114
12703
|
}
|
|
12115
12704
|
if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
|
|
12116
|
-
await
|
|
12705
|
+
await mkdir5(path16.dirname(target), { recursive: true });
|
|
12117
12706
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
12118
|
-
await
|
|
12707
|
+
await writeFile5(tmp, buf);
|
|
12119
12708
|
outputs.push({
|
|
12120
12709
|
path: portableOutputPath(target, spec.root),
|
|
12121
12710
|
sha256: sha256(buf),
|
|
@@ -12127,7 +12716,7 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12127
12716
|
await saveLock(lockPath, lock);
|
|
12128
12717
|
await rename4(tmp, target);
|
|
12129
12718
|
wrote = true;
|
|
12130
|
-
log2(` wrote ${
|
|
12719
|
+
log2(` wrote ${path16.relative(process.cwd(), target)}`);
|
|
12131
12720
|
}
|
|
12132
12721
|
if (opts.refresh && !wrote) {
|
|
12133
12722
|
if (rotated) {
|
|
@@ -12171,10 +12760,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12171
12760
|
return result;
|
|
12172
12761
|
}
|
|
12173
12762
|
async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
12174
|
-
const file =
|
|
12175
|
-
if (!
|
|
12763
|
+
const file = path16.join(cacheDir, cacheFileName(hash, mediaType));
|
|
12764
|
+
if (!existsSync10(file)) return null;
|
|
12176
12765
|
try {
|
|
12177
|
-
const buf = await
|
|
12766
|
+
const buf = await readFile8(file);
|
|
12178
12767
|
if (sha256(buf) !== hash) return null;
|
|
12179
12768
|
validateMedia(buf, mediaType);
|
|
12180
12769
|
return buf;
|
|
@@ -12185,15 +12774,15 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
|
|
|
12185
12774
|
async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
|
|
12186
12775
|
const hash = knownHash ?? sha256(buf);
|
|
12187
12776
|
validateMedia(buf, mediaType);
|
|
12188
|
-
const file =
|
|
12777
|
+
const file = path16.join(cacheDir, cacheFileName(hash, mediaType));
|
|
12189
12778
|
if (await readCachedMedia(cacheDir, hash, mediaType)) return;
|
|
12190
|
-
await
|
|
12779
|
+
await mkdir5(cacheDir, { recursive: true });
|
|
12191
12780
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
12192
|
-
await
|
|
12781
|
+
await writeFile5(tmp, buf);
|
|
12193
12782
|
try {
|
|
12194
12783
|
await rename4(tmp, file);
|
|
12195
12784
|
} catch (err) {
|
|
12196
|
-
if (!
|
|
12785
|
+
if (!existsSync10(file)) throw err;
|
|
12197
12786
|
} finally {
|
|
12198
12787
|
await rm4(tmp, { force: true });
|
|
12199
12788
|
}
|
|
@@ -12213,7 +12802,7 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
12213
12802
|
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
12214
12803
|
const spec = specByKey.get(key);
|
|
12215
12804
|
if (!spec || entry.provider !== provider.id || !entry.objectId) continue;
|
|
12216
|
-
if (entry.generator === "pixflux") continue;
|
|
12805
|
+
if (entry.generator === "pixflux" || entry.generator === "vfx") continue;
|
|
12217
12806
|
if (!provider.setTags) return tagged;
|
|
12218
12807
|
try {
|
|
12219
12808
|
await provider.setTags(entry.objectId, spec.tags, entry.generator);
|
|
@@ -12226,13 +12815,13 @@ async function pushTags(provider, specs, lock, opts = {}) {
|
|
|
12226
12815
|
}
|
|
12227
12816
|
|
|
12228
12817
|
// src/pipeline/adopt.ts
|
|
12229
|
-
import { existsSync as
|
|
12230
|
-
import { readFile as
|
|
12231
|
-
import
|
|
12818
|
+
import { existsSync as existsSync12 } from "fs";
|
|
12819
|
+
import { readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
|
|
12820
|
+
import path17 from "path";
|
|
12232
12821
|
|
|
12233
12822
|
// src/cache.ts
|
|
12234
|
-
import { mkdir as
|
|
12235
|
-
import { existsSync as
|
|
12823
|
+
import { mkdir as mkdir6, readFile as readFile9, rename as rename5, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
12824
|
+
import { existsSync as existsSync11 } from "fs";
|
|
12236
12825
|
import pathModule from "path";
|
|
12237
12826
|
import { z as z4 } from "zod";
|
|
12238
12827
|
var HashCacheSchema = z4.object({
|
|
@@ -12247,10 +12836,10 @@ function isSha256Hash(value) {
|
|
|
12247
12836
|
function parseCache(value) {
|
|
12248
12837
|
return HashCacheSchema.parse(value);
|
|
12249
12838
|
}
|
|
12250
|
-
async function loadCache(
|
|
12251
|
-
if (!
|
|
12839
|
+
async function loadCache(path53) {
|
|
12840
|
+
if (!existsSync11(path53)) return { version: 1, hashes: {} };
|
|
12252
12841
|
try {
|
|
12253
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await
|
|
12842
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile9(path53, "utf8")));
|
|
12254
12843
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
12255
12844
|
return {
|
|
12256
12845
|
version: 1,
|
|
@@ -12262,18 +12851,18 @@ async function loadCache(path52) {
|
|
|
12262
12851
|
return { version: 1, hashes: {} };
|
|
12263
12852
|
}
|
|
12264
12853
|
}
|
|
12265
|
-
async function saveCache(
|
|
12854
|
+
async function saveCache(path53, cache) {
|
|
12266
12855
|
const sorted = {};
|
|
12267
12856
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
12268
12857
|
const hash = cache.hashes[key];
|
|
12269
12858
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
12270
12859
|
sorted[key] = hash;
|
|
12271
12860
|
}
|
|
12272
|
-
await
|
|
12273
|
-
const tmp = `${
|
|
12861
|
+
await mkdir6(pathModule.dirname(pathModule.resolve(path53)), { recursive: true });
|
|
12862
|
+
const tmp = `${path53}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
12274
12863
|
try {
|
|
12275
|
-
await
|
|
12276
|
-
await rename5(tmp,
|
|
12864
|
+
await writeFile6(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
12865
|
+
await rename5(tmp, path53);
|
|
12277
12866
|
} finally {
|
|
12278
12867
|
await rm5(tmp, { force: true });
|
|
12279
12868
|
}
|
|
@@ -12307,11 +12896,11 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
|
|
|
12307
12896
|
const localByHash = /* @__PURE__ */ new Map();
|
|
12308
12897
|
const unmatchedLocal = [];
|
|
12309
12898
|
for (const spec of specs) {
|
|
12310
|
-
if (!
|
|
12899
|
+
if (!existsSync12(spec.outFile)) {
|
|
12311
12900
|
unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
|
|
12312
12901
|
continue;
|
|
12313
12902
|
}
|
|
12314
|
-
const bytes = await
|
|
12903
|
+
const bytes = await readFile10(spec.outFile);
|
|
12315
12904
|
try {
|
|
12316
12905
|
decodePng(bytes);
|
|
12317
12906
|
} catch (err) {
|
|
@@ -12431,7 +13020,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
|
12431
13020
|
const log2 = opts.onProgress ?? (() => {
|
|
12432
13021
|
});
|
|
12433
13022
|
const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
|
|
12434
|
-
const raw = JSON.parse(await
|
|
13023
|
+
const raw = JSON.parse(await readFile10(manifestPath, "utf8"));
|
|
12435
13024
|
const best = /* @__PURE__ */ new Map();
|
|
12436
13025
|
for (const entry of Object.values(lock.entries)) {
|
|
12437
13026
|
if (opts.provider && entry.provider !== opts.provider) continue;
|
|
@@ -12453,7 +13042,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
|
|
|
12453
13042
|
stillEmpty.push(id);
|
|
12454
13043
|
}
|
|
12455
13044
|
}
|
|
12456
|
-
await
|
|
13045
|
+
await writeFile7(manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
12457
13046
|
log2(` filled ${filled} prompt(s) from adopted objects`);
|
|
12458
13047
|
return { filled, stillEmpty };
|
|
12459
13048
|
}
|
|
@@ -12464,9 +13053,9 @@ function formatUnmatchedRemote(objects, limit = 15) {
|
|
|
12464
13053
|
}
|
|
12465
13054
|
|
|
12466
13055
|
// src/pipeline/adopt-characters.ts
|
|
12467
|
-
import { existsSync as
|
|
12468
|
-
import { mkdir as
|
|
12469
|
-
import
|
|
13056
|
+
import { existsSync as existsSync13 } from "fs";
|
|
13057
|
+
import { mkdir as mkdir7, readFile as readFile11, writeFile as writeFile8 } from "fs/promises";
|
|
13058
|
+
import path18 from "path";
|
|
12470
13059
|
async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
12471
13060
|
const log2 = opts.onProgress ?? (() => {
|
|
12472
13061
|
});
|
|
@@ -12517,12 +13106,12 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12517
13106
|
const bytes = await download(source.url);
|
|
12518
13107
|
const hash = sha256(bytes);
|
|
12519
13108
|
const file = expectedOutputPath(spec, source.role, index, sources.length);
|
|
12520
|
-
if (
|
|
12521
|
-
if (sha256(await
|
|
13109
|
+
if (existsSync13(file)) {
|
|
13110
|
+
if (sha256(await readFile11(file)) !== hash) result.differing.push(path18.relative(process.cwd(), file));
|
|
12522
13111
|
} else {
|
|
12523
|
-
await
|
|
12524
|
-
await
|
|
12525
|
-
result.written.push(
|
|
13112
|
+
await mkdir7(path18.dirname(file), { recursive: true });
|
|
13113
|
+
await writeFile8(file, bytes);
|
|
13114
|
+
result.written.push(path18.relative(process.cwd(), file));
|
|
12526
13115
|
}
|
|
12527
13116
|
outputs.push({ path: portableOutputPath(file, spec.root), sha256: hash, ...source.role ? { role: source.role } : {} });
|
|
12528
13117
|
}
|
|
@@ -12560,11 +13149,11 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12560
13149
|
if (!match) throw new Error(`remoteId ${spec.remoteId} is not a character on this account`);
|
|
12561
13150
|
} else {
|
|
12562
13151
|
const south = expectedOutputPath(spec, "south", 0, spec.character.directions);
|
|
12563
|
-
if (!
|
|
12564
|
-
result.unmatched.push(`${key}: no remoteId and no ${
|
|
13152
|
+
if (!existsSync13(south)) {
|
|
13153
|
+
result.unmatched.push(`${key}: no remoteId and no ${path18.relative(process.cwd(), south)} to match by`);
|
|
12565
13154
|
return false;
|
|
12566
13155
|
}
|
|
12567
|
-
const localHash = sha256(await
|
|
13156
|
+
const localHash = sha256(await readFile11(south));
|
|
12568
13157
|
for (const candidate of remote) {
|
|
12569
13158
|
if (claimed.has(candidate.id)) continue;
|
|
12570
13159
|
if (await southHash(candidate) === localHash) {
|
|
@@ -12573,7 +13162,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12573
13162
|
}
|
|
12574
13163
|
}
|
|
12575
13164
|
if (!match) {
|
|
12576
|
-
result.unmatched.push(`${key}: no character's south rotation has the bytes of ${
|
|
13165
|
+
result.unmatched.push(`${key}: no character's south rotation has the bytes of ${path18.relative(process.cwd(), south)}`);
|
|
12577
13166
|
return false;
|
|
12578
13167
|
}
|
|
12579
13168
|
}
|
|
@@ -12640,15 +13229,15 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12640
13229
|
if (!match) throw new Error(`remoteId ${spec.remoteId} names no ${animation.direction} animation of character ${parentId}`);
|
|
12641
13230
|
} else {
|
|
12642
13231
|
const first = expectedOutputPath(spec, "frame-00", 0, 2);
|
|
12643
|
-
if (!
|
|
13232
|
+
if (!existsSync13(first)) {
|
|
12644
13233
|
const byName = candidates.find((a) => a.name === characterAnimationName(spec));
|
|
12645
13234
|
if (!byName) {
|
|
12646
|
-
result.unmatched.push(`${key}: no remoteId and no ${
|
|
13235
|
+
result.unmatched.push(`${key}: no remoteId and no ${path18.relative(process.cwd(), first)} to match by`);
|
|
12647
13236
|
return false;
|
|
12648
13237
|
}
|
|
12649
13238
|
match = byName;
|
|
12650
13239
|
} else {
|
|
12651
|
-
const localHash = sha256(await
|
|
13240
|
+
const localHash = sha256(await readFile11(first));
|
|
12652
13241
|
for (const candidate of candidates) {
|
|
12653
13242
|
if (sha256(await download(candidate.frames[0])) === localHash) {
|
|
12654
13243
|
match = candidate;
|
|
@@ -12656,7 +13245,7 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12656
13245
|
}
|
|
12657
13246
|
}
|
|
12658
13247
|
if (!match) {
|
|
12659
|
-
result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${
|
|
13248
|
+
result.unmatched.push(`${key}: no ${animation.direction} animation of character ${parentId} starts with the bytes of ${path18.relative(process.cwd(), first)}`);
|
|
12660
13249
|
return false;
|
|
12661
13250
|
}
|
|
12662
13251
|
}
|
|
@@ -12716,19 +13305,19 @@ async function adoptCharacters(provider, specs, lock, lockPath, opts) {
|
|
|
12716
13305
|
}
|
|
12717
13306
|
|
|
12718
13307
|
// src/pipeline/salvage.ts
|
|
12719
|
-
import { readFile as
|
|
12720
|
-
import { existsSync as
|
|
12721
|
-
import
|
|
13308
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
13309
|
+
import { existsSync as existsSync14 } from "fs";
|
|
13310
|
+
import path19 from "path";
|
|
12722
13311
|
function providerClaimId(provider, objectId) {
|
|
12723
13312
|
return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
|
|
12724
13313
|
}
|
|
12725
13314
|
async function loadClaims(lockPaths, opts = {}) {
|
|
12726
13315
|
const claimed = /* @__PURE__ */ new Set();
|
|
12727
13316
|
for (const p of lockPaths) {
|
|
12728
|
-
if (!
|
|
13317
|
+
if (!existsSync14(p)) throw new Error(`Claim lockfile not found: ${p}`);
|
|
12729
13318
|
let parsed;
|
|
12730
13319
|
try {
|
|
12731
|
-
parsed = parseLock(JSON.parse(await
|
|
13320
|
+
parsed = parseLock(JSON.parse(await readFile12(p, "utf8")));
|
|
12732
13321
|
} catch {
|
|
12733
13322
|
throw new Error(`Claim lockfile is malformed: ${p}`);
|
|
12734
13323
|
}
|
|
@@ -12895,20 +13484,20 @@ function matchStyleByPattern(prompt, manifest) {
|
|
|
12895
13484
|
return null;
|
|
12896
13485
|
}
|
|
12897
13486
|
async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
|
|
12898
|
-
const own =
|
|
13487
|
+
const own = path19.resolve(ownManifestPath);
|
|
12899
13488
|
const siblingManifestPaths = [
|
|
12900
13489
|
.../* @__PURE__ */ new Set([
|
|
12901
13490
|
...workspaceManifestPaths,
|
|
12902
|
-
...claimPaths.map((c) =>
|
|
13491
|
+
...claimPaths.map((c) => path19.join(path19.dirname(path19.resolve(c)), "pixelkiln.manifest.json"))
|
|
12903
13492
|
])
|
|
12904
13493
|
];
|
|
12905
13494
|
const siblings = [];
|
|
12906
13495
|
for (const siblingManifestPath of siblingManifestPaths) {
|
|
12907
|
-
if (
|
|
12908
|
-
if (!
|
|
13496
|
+
if (path19.resolve(siblingManifestPath) === own) continue;
|
|
13497
|
+
if (!existsSync14(siblingManifestPath)) continue;
|
|
12909
13498
|
try {
|
|
12910
13499
|
const { manifest } = await loadManifest(siblingManifestPath);
|
|
12911
|
-
siblings.push({ label:
|
|
13500
|
+
siblings.push({ label: path19.basename(path19.dirname(siblingManifestPath)), manifest });
|
|
12912
13501
|
} catch {
|
|
12913
13502
|
}
|
|
12914
13503
|
}
|
|
@@ -13028,9 +13617,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
|
|
|
13028
13617
|
}
|
|
13029
13618
|
|
|
13030
13619
|
// src/workspace.ts
|
|
13031
|
-
import { mkdir as
|
|
13032
|
-
import { existsSync as
|
|
13033
|
-
import
|
|
13620
|
+
import { mkdir as mkdir8, readFile as readFile13, rename as rename6, rm as rm6, writeFile as writeFile9 } from "fs/promises";
|
|
13621
|
+
import { existsSync as existsSync15 } from "fs";
|
|
13622
|
+
import path20 from "path";
|
|
13034
13623
|
import { z as z5 } from "zod";
|
|
13035
13624
|
var WorkspaceProjectSchema = z5.object({
|
|
13036
13625
|
id: z5.string().min(1),
|
|
@@ -13055,10 +13644,10 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
|
|
|
13055
13644
|
);
|
|
13056
13645
|
}
|
|
13057
13646
|
async function loadWorkspace(workspacePath) {
|
|
13058
|
-
if (!
|
|
13647
|
+
if (!existsSync15(workspacePath)) return { version: 1, projects: [] };
|
|
13059
13648
|
let raw;
|
|
13060
13649
|
try {
|
|
13061
|
-
raw = JSON.parse(await
|
|
13650
|
+
raw = JSON.parse(await readFile13(workspacePath, "utf8"));
|
|
13062
13651
|
} catch (err) {
|
|
13063
13652
|
throw new ProjectError(
|
|
13064
13653
|
`Workspace catalog at ${workspacePath} is malformed:
|
|
@@ -13072,22 +13661,22 @@ async function saveWorkspace(workspacePath, ws) {
|
|
|
13072
13661
|
version: 1,
|
|
13073
13662
|
projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
|
|
13074
13663
|
};
|
|
13075
|
-
await
|
|
13664
|
+
await mkdir8(path20.dirname(path20.resolve(workspacePath)), { recursive: true });
|
|
13076
13665
|
const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
13077
13666
|
try {
|
|
13078
|
-
await
|
|
13667
|
+
await writeFile9(tmp, JSON.stringify(sorted, null, 2) + "\n");
|
|
13079
13668
|
await rename6(tmp, workspacePath);
|
|
13080
13669
|
} finally {
|
|
13081
13670
|
await rm6(tmp, { force: true });
|
|
13082
13671
|
}
|
|
13083
13672
|
}
|
|
13084
13673
|
function toPortablePath(dir, absolute) {
|
|
13085
|
-
return
|
|
13674
|
+
return path20.relative(dir, absolute).split(path20.sep).join("/");
|
|
13086
13675
|
}
|
|
13087
13676
|
function resolveProject(dir, project) {
|
|
13088
13677
|
return {
|
|
13089
|
-
manifestPath:
|
|
13090
|
-
lockPath:
|
|
13678
|
+
manifestPath: path20.resolve(dir, project.manifest.split("/").join(path20.sep)),
|
|
13679
|
+
lockPath: path20.resolve(dir, project.lock.split("/").join(path20.sep))
|
|
13091
13680
|
};
|
|
13092
13681
|
}
|
|
13093
13682
|
function validateWorkspace(ws, dir) {
|
|
@@ -13100,21 +13689,21 @@ function validateWorkspace(ws, dir) {
|
|
|
13100
13689
|
const { manifestPath, lockPath } = resolveProject(dir, project);
|
|
13101
13690
|
lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
|
|
13102
13691
|
manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
|
|
13103
|
-
if (
|
|
13692
|
+
if (path20.isAbsolute(project.manifest) || path20.isAbsolute(project.lock)) {
|
|
13104
13693
|
diagnostics.push({
|
|
13105
13694
|
id: "absolute-path",
|
|
13106
13695
|
level: "warning",
|
|
13107
13696
|
message: `project "${project.id}" stores an absolute path; the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
|
|
13108
13697
|
});
|
|
13109
13698
|
}
|
|
13110
|
-
if (!
|
|
13699
|
+
if (!existsSync15(manifestPath)) {
|
|
13111
13700
|
diagnostics.push({
|
|
13112
13701
|
id: "missing-manifest",
|
|
13113
13702
|
level: "error",
|
|
13114
13703
|
message: `project "${project.id}" manifest not found: ${manifestPath}`
|
|
13115
13704
|
});
|
|
13116
13705
|
}
|
|
13117
|
-
if (!
|
|
13706
|
+
if (!existsSync15(lockPath)) {
|
|
13118
13707
|
diagnostics.push({
|
|
13119
13708
|
id: "missing-lock",
|
|
13120
13709
|
level: "error",
|
|
@@ -13161,8 +13750,8 @@ function validateWorkspace(ws, dir) {
|
|
|
13161
13750
|
}
|
|
13162
13751
|
|
|
13163
13752
|
// src/pick/salvage-server.ts
|
|
13164
|
-
import { mkdir as
|
|
13165
|
-
import
|
|
13753
|
+
import { mkdir as mkdir9, writeFile as writeFile10, readFile as readFile15 } from "fs/promises";
|
|
13754
|
+
import path21 from "path";
|
|
13166
13755
|
|
|
13167
13756
|
// src/pick/salvage-sheet.ts
|
|
13168
13757
|
var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
@@ -13323,7 +13912,7 @@ refresh();
|
|
|
13323
13912
|
|
|
13324
13913
|
// src/pick/review-server.ts
|
|
13325
13914
|
import { createServer } from "http";
|
|
13326
|
-
import { readFile as
|
|
13915
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
13327
13916
|
|
|
13328
13917
|
// src/open.ts
|
|
13329
13918
|
import { spawn } from "child_process";
|
|
@@ -13350,7 +13939,7 @@ function serveReviewPage(opts) {
|
|
|
13350
13939
|
if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
|
|
13351
13940
|
try {
|
|
13352
13941
|
const asset = opts.assets.get(req.url);
|
|
13353
|
-
const bytes = await
|
|
13942
|
+
const bytes = await readFile14(asset.path);
|
|
13354
13943
|
res.writeHead(200, {
|
|
13355
13944
|
"Content-Type": asset.contentType,
|
|
13356
13945
|
"Cache-Control": "no-store",
|
|
@@ -13427,7 +14016,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13427
14016
|
});
|
|
13428
14017
|
const html = renderSalvageSheet(orphans, {
|
|
13429
14018
|
styleId: ctx.styleId,
|
|
13430
|
-
importDir:
|
|
14019
|
+
importDir: path21.relative(process.cwd(), ctx.importDir) || "."
|
|
13431
14020
|
});
|
|
13432
14021
|
const byId = new Map(orphans.map((o) => [o.id, o]));
|
|
13433
14022
|
const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
|
|
@@ -13483,10 +14072,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13483
14072
|
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
|
|
13484
14073
|
decodePng(buf);
|
|
13485
14074
|
const assetId = idFromPrompt(orphan.prompt, taken);
|
|
13486
|
-
const rel =
|
|
13487
|
-
const outFile =
|
|
13488
|
-
await
|
|
13489
|
-
await
|
|
14075
|
+
const rel = path21.join("_salvaged", `${assetId}.png`);
|
|
14076
|
+
const outFile = path21.resolve(ctx.importDir, rel);
|
|
14077
|
+
await mkdir9(path21.dirname(outFile), { recursive: true });
|
|
14078
|
+
await writeFile10(outFile, buf);
|
|
13490
14079
|
ctx.manifest.assets[assetId] = {
|
|
13491
14080
|
prompt: orphan.prompt,
|
|
13492
14081
|
promptByStyle: {},
|
|
@@ -13518,7 +14107,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13518
14107
|
sourceUrl: durableSource,
|
|
13519
14108
|
sourceUrls: durableSource ? [{ url: durableSource }] : [],
|
|
13520
14109
|
outputs: [{
|
|
13521
|
-
path: portableOutputPath(outFile,
|
|
14110
|
+
path: portableOutputPath(outFile, path21.dirname(ctx.manifestPath)),
|
|
13522
14111
|
sha256: sha256(buf)
|
|
13523
14112
|
}],
|
|
13524
14113
|
submittedAt: orphan.createdAt,
|
|
@@ -13542,9 +14131,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13542
14131
|
}
|
|
13543
14132
|
}
|
|
13544
14133
|
await applyTags(provider, decisions, existingTags, { onProgress: log2, characters: characterMembers });
|
|
13545
|
-
const raw = JSON.parse(await
|
|
14134
|
+
const raw = JSON.parse(await readFile15(ctx.manifestPath, "utf8"));
|
|
13546
14135
|
for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
|
|
13547
|
-
await
|
|
14136
|
+
await writeFile10(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
|
|
13548
14137
|
await saveLock(ctx.lockPath, ctx.lock);
|
|
13549
14138
|
if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
|
|
13550
14139
|
return result;
|
|
@@ -13553,19 +14142,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
13553
14142
|
}
|
|
13554
14143
|
|
|
13555
14144
|
// src/cli/project.ts
|
|
13556
|
-
import
|
|
13557
|
-
import { existsSync as
|
|
14145
|
+
import path23 from "path";
|
|
14146
|
+
import { existsSync as existsSync16 } from "fs";
|
|
13558
14147
|
|
|
13559
14148
|
// src/project.ts
|
|
13560
|
-
import
|
|
14149
|
+
import path22 from "path";
|
|
13561
14150
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
13562
|
-
const absolute =
|
|
13563
|
-
const root =
|
|
14151
|
+
const absolute = path22.resolve(manifestPath);
|
|
14152
|
+
const root = path22.dirname(absolute);
|
|
13564
14153
|
if (opts.env !== false) {
|
|
13565
14154
|
loadEnvFiles(root);
|
|
13566
|
-
if (
|
|
14155
|
+
if (path22.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
13567
14156
|
}
|
|
13568
|
-
const lockPath =
|
|
14157
|
+
const lockPath = path22.resolve(opts.lockPath ?? path22.join(root, "pixelkiln.lock.json"));
|
|
13569
14158
|
const loaded = await loadManifest(absolute);
|
|
13570
14159
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
13571
14160
|
const lock = await loadLock(lockPath);
|
|
@@ -13696,18 +14285,18 @@ async function workspaceStatus(ws, dir) {
|
|
|
13696
14285
|
|
|
13697
14286
|
// src/cli/project.ts
|
|
13698
14287
|
async function provenanceFile(id, file) {
|
|
13699
|
-
const absolute =
|
|
14288
|
+
const absolute = path23.resolve(file);
|
|
13700
14289
|
return {
|
|
13701
14290
|
id,
|
|
13702
14291
|
path: absolute,
|
|
13703
|
-
sha256:
|
|
14292
|
+
sha256: existsSync16(absolute) ? await sha256File(absolute) : null,
|
|
13704
14293
|
included: true
|
|
13705
14294
|
};
|
|
13706
14295
|
}
|
|
13707
14296
|
function manifestSources(manifest, styleId) {
|
|
13708
14297
|
const sources = {};
|
|
13709
14298
|
for (const [assetId, asset] of Object.entries(manifest.assets)) {
|
|
13710
|
-
if (asset
|
|
14299
|
+
if (!assetInStyle(asset, styleId, manifest.styles[styleId])) continue;
|
|
13711
14300
|
const source = asset.sourceByStyle[styleId] ?? asset.source;
|
|
13712
14301
|
if (source) sources[assetId] = source;
|
|
13713
14302
|
}
|
|
@@ -13772,10 +14361,10 @@ function budgetsForPlan(plan, args) {
|
|
|
13772
14361
|
return out;
|
|
13773
14362
|
}
|
|
13774
14363
|
async function requireCompleteWorkspaceClaims(workspacePath) {
|
|
13775
|
-
if (!
|
|
14364
|
+
if (!existsSync16(workspacePath)) {
|
|
13776
14365
|
throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
|
|
13777
14366
|
}
|
|
13778
|
-
const dir =
|
|
14367
|
+
const dir = path23.dirname(path23.resolve(workspacePath));
|
|
13779
14368
|
const ws = await loadWorkspace(workspacePath);
|
|
13780
14369
|
const diagnostics = validateWorkspace(ws, dir);
|
|
13781
14370
|
const errors = diagnostics.filter((d) => d.level === "error");
|
|
@@ -13798,9 +14387,9 @@ async function openAccountProject(args) {
|
|
|
13798
14387
|
return project;
|
|
13799
14388
|
}
|
|
13800
14389
|
async function open(args) {
|
|
13801
|
-
if (!
|
|
14390
|
+
if (!existsSync16(path23.resolve(args.manifest))) {
|
|
13802
14391
|
throw new ProjectError(
|
|
13803
|
-
`No manifest at ${
|
|
14392
|
+
`No manifest at ${path23.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
|
|
13804
14393
|
);
|
|
13805
14394
|
}
|
|
13806
14395
|
const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
|
|
@@ -13842,7 +14431,7 @@ function providerCache(mode2) {
|
|
|
13842
14431
|
|
|
13843
14432
|
// src/cli/commands/account.ts
|
|
13844
14433
|
async function runBalance(args) {
|
|
13845
|
-
loadEnvFiles(
|
|
14434
|
+
loadEnvFiles(path24.dirname(path24.resolve(args.manifest)));
|
|
13846
14435
|
loadEnvFiles(process.cwd());
|
|
13847
14436
|
const loaded = await loadManifest(args.manifest);
|
|
13848
14437
|
const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
|
|
@@ -13915,12 +14504,12 @@ async function runAdopt(args) {
|
|
|
13915
14504
|
tagged ${n} object(s) upstream`);
|
|
13916
14505
|
}
|
|
13917
14506
|
if (args.writePrompts) {
|
|
13918
|
-
const { filled, stillEmpty } = await writePromptsBack(
|
|
14507
|
+
const { filled, stillEmpty } = await writePromptsBack(path24.resolve(args.manifest), lock, {
|
|
13919
14508
|
onProgress: log,
|
|
13920
14509
|
provider: provider.id,
|
|
13921
14510
|
assetIds: specs.map((spec) => spec.assetId)
|
|
13922
14511
|
});
|
|
13923
|
-
log(` recovered ${filled} prompt(s) into ${
|
|
14512
|
+
log(` recovered ${filled} prompt(s) into ${path24.relative(process.cwd(), args.manifest)}`);
|
|
13924
14513
|
const reloaded = await loadManifest(args.manifest);
|
|
13925
14514
|
const rebased = (await resolveSpecs(reloaded, {
|
|
13926
14515
|
styles: args.styles,
|
|
@@ -13950,12 +14539,12 @@ async function runSalvage2(args) {
|
|
|
13950
14539
|
const provider = providerFor(accountProvider);
|
|
13951
14540
|
const jsonMode = args.dryRun && args.json;
|
|
13952
14541
|
const diag = jsonMode ? (msg = "") => console.error(msg) : log;
|
|
13953
|
-
const ownLock =
|
|
14542
|
+
const ownLock = path24.resolve(args.lock);
|
|
13954
14543
|
let workspaceProjects = [];
|
|
13955
14544
|
let workspaceDir = "";
|
|
13956
14545
|
if (args.workspace) {
|
|
13957
|
-
const workspacePath =
|
|
13958
|
-
workspaceDir =
|
|
14546
|
+
const workspacePath = path24.resolve(args.workspace);
|
|
14547
|
+
workspaceDir = path24.dirname(workspacePath);
|
|
13959
14548
|
const complete = await requireCompleteWorkspaceClaims(workspacePath);
|
|
13960
14549
|
workspaceProjects = complete.ws.projects;
|
|
13961
14550
|
for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
|
|
@@ -13964,12 +14553,12 @@ async function runSalvage2(args) {
|
|
|
13964
14553
|
const lockPaths = [
|
|
13965
14554
|
.../* @__PURE__ */ new Set([
|
|
13966
14555
|
...workspaceLockPaths,
|
|
13967
|
-
...
|
|
13968
|
-
...args.claims.map((c) =>
|
|
14556
|
+
...existsSync17(ownLock) ? [ownLock] : [],
|
|
14557
|
+
...args.claims.map((c) => path24.resolve(c))
|
|
13969
14558
|
])
|
|
13970
14559
|
];
|
|
13971
14560
|
diag(` claim set (${lockPaths.length} lockfile(s)):`);
|
|
13972
|
-
for (const p of lockPaths) diag(` ${
|
|
14561
|
+
for (const p of lockPaths) diag(` ${path24.relative(process.cwd(), p)}`);
|
|
13973
14562
|
if (!args.claims.length && !args.workspace) {
|
|
13974
14563
|
diag(
|
|
13975
14564
|
`
|
|
@@ -13977,7 +14566,7 @@ async function runSalvage2(args) {
|
|
|
13977
14566
|
pass every other project's lockfile via --claims a.json,b.json, or
|
|
13978
14567
|
register every project in a workspace catalog and pass --workspace.`
|
|
13979
14568
|
);
|
|
13980
|
-
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath ===
|
|
14569
|
+
} else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path24.resolve(args.manifest))) {
|
|
13981
14570
|
diag(
|
|
13982
14571
|
`
|
|
13983
14572
|
This project's manifest is not registered in the workspace catalog. Its own
|
|
@@ -14096,7 +14685,7 @@ async function runSalvage2(args) {
|
|
|
14096
14685
|
manifestPath: loaded.path,
|
|
14097
14686
|
manifest: accountManifest,
|
|
14098
14687
|
styleId,
|
|
14099
|
-
importDir:
|
|
14688
|
+
importDir: path24.resolve(loaded.root, style.outDir),
|
|
14100
14689
|
lock,
|
|
14101
14690
|
lockPath: args.lock
|
|
14102
14691
|
},
|
|
@@ -14223,20 +14812,20 @@ async function runTag(args) {
|
|
|
14223
14812
|
}
|
|
14224
14813
|
|
|
14225
14814
|
// src/cli/commands/edit.ts
|
|
14226
|
-
import
|
|
14815
|
+
import path27 from "path";
|
|
14227
14816
|
|
|
14228
14817
|
// src/pipeline/hand-edit.ts
|
|
14229
|
-
import { existsSync as
|
|
14230
|
-
import { copyFile, mkdir as
|
|
14231
|
-
import
|
|
14818
|
+
import { existsSync as existsSync18 } from "fs";
|
|
14819
|
+
import { copyFile, mkdir as mkdir10, readFile as readFile17, rename as rename8, rm as rm8, writeFile as writeFile12 } from "fs/promises";
|
|
14820
|
+
import path26 from "path";
|
|
14232
14821
|
import { z as z7 } from "zod";
|
|
14233
14822
|
|
|
14234
14823
|
// src/manifest-edit.ts
|
|
14235
|
-
import { readFile as
|
|
14236
|
-
import
|
|
14824
|
+
import { readFile as readFile16, rename as rename7, rm as rm7, writeFile as writeFile11 } from "fs/promises";
|
|
14825
|
+
import path25 from "path";
|
|
14237
14826
|
import { z as z6 } from "zod";
|
|
14238
14827
|
var HexSha = z6.string().regex(/^[0-9a-f]{64}$/);
|
|
14239
|
-
var ProjectFileSchema = z6.string().min(1).refine((file) => !
|
|
14828
|
+
var ProjectFileSchema = z6.string().min(1).refine((file) => !path25.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
|
|
14240
14829
|
var AssetPatchSchema = z6.object({
|
|
14241
14830
|
prompt: z6.string().optional(),
|
|
14242
14831
|
/** Per-style override for the style being viewed; null removes it. */
|
|
@@ -14655,8 +15244,8 @@ function applyEdit(raw, edit) {
|
|
|
14655
15244
|
}
|
|
14656
15245
|
}
|
|
14657
15246
|
async function withCandidate(manifestPath, edit, use, unchanged) {
|
|
14658
|
-
const absolute =
|
|
14659
|
-
const original = await
|
|
15247
|
+
const absolute = path25.resolve(manifestPath);
|
|
15248
|
+
const original = await readFile16(absolute, "utf8");
|
|
14660
15249
|
if (sha256(original) !== edit.expectedSha256) throw new ManifestDriftError(absolute);
|
|
14661
15250
|
let raw;
|
|
14662
15251
|
try {
|
|
@@ -14669,12 +15258,12 @@ async function withCandidate(manifestPath, edit, use, unchanged) {
|
|
|
14669
15258
|
applyEdit(raw, edit);
|
|
14670
15259
|
const next = serializeLike(original, raw);
|
|
14671
15260
|
if (next === original) return unchanged(absolute, original);
|
|
14672
|
-
const tmp =
|
|
14673
|
-
|
|
14674
|
-
`.${
|
|
15261
|
+
const tmp = path25.join(
|
|
15262
|
+
path25.dirname(absolute),
|
|
15263
|
+
`.${path25.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
|
|
14675
15264
|
);
|
|
14676
15265
|
try {
|
|
14677
|
-
await
|
|
15266
|
+
await writeFile11(tmp, next);
|
|
14678
15267
|
let specs;
|
|
14679
15268
|
try {
|
|
14680
15269
|
specs = await resolveSpecs(await loadManifest(tmp));
|
|
@@ -14744,11 +15333,11 @@ async function priceManifestEdit(manifestPath, edit, opts = {}) {
|
|
|
14744
15333
|
var HAND_EDIT_DIR = "edits";
|
|
14745
15334
|
function handEditPath(loaded, spec) {
|
|
14746
15335
|
const style = loaded.manifest.styles[spec.styleId];
|
|
14747
|
-
const outDir =
|
|
14748
|
-
const relative =
|
|
14749
|
-
const inside = relative && !relative.startsWith("..") && !
|
|
14750
|
-
const file = inside ? relative :
|
|
14751
|
-
return
|
|
15336
|
+
const outDir = path26.resolve(loaded.root, style?.outDir ?? ".");
|
|
15337
|
+
const relative = path26.relative(outDir, spec.outFile);
|
|
15338
|
+
const inside = relative && !relative.startsWith("..") && !path26.isAbsolute(relative);
|
|
15339
|
+
const file = inside ? relative : path26.basename(spec.outFile);
|
|
15340
|
+
return path26.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
|
|
14752
15341
|
}
|
|
14753
15342
|
function handEditBases(spec, lock) {
|
|
14754
15343
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -14761,10 +15350,10 @@ function handEditBases(spec, lock) {
|
|
|
14761
15350
|
}));
|
|
14762
15351
|
if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
|
|
14763
15352
|
if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
|
|
14764
|
-
if (!members.every((member) =>
|
|
15353
|
+
if (!members.every((member) => existsSync18(member.path))) return null;
|
|
14765
15354
|
return members.map(({ role, path: file }) => ({ role, path: file }));
|
|
14766
15355
|
}
|
|
14767
|
-
return
|
|
15356
|
+
return existsSync18(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
|
|
14768
15357
|
}
|
|
14769
15358
|
function handEditMembers(loaded, spec, lock, editPath) {
|
|
14770
15359
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
@@ -14790,18 +15379,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
|
|
|
14790
15379
|
`${spec.styleId}/${spec.assetId} declares ${spec.source}, one committed file placed for the whole set; clear that source (pixelkiln edit detach) to hand-edit the members one by one`
|
|
14791
15380
|
);
|
|
14792
15381
|
}
|
|
14793
|
-
const existing = spec.source ?
|
|
15382
|
+
const existing = spec.source ? path26.resolve(loaded.root, spec.source) : null;
|
|
14794
15383
|
const editPath = existing ?? handEditPath(loaded, spec);
|
|
14795
15384
|
const members = handEditMembers(loaded, spec, lock, editPath);
|
|
14796
15385
|
let created = false;
|
|
14797
15386
|
for (const member of members) {
|
|
14798
|
-
if (
|
|
15387
|
+
if (existsSync18(member.path)) continue;
|
|
14799
15388
|
if (!member.base) {
|
|
14800
15389
|
throw new ManifestEditError(
|
|
14801
15390
|
`${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
|
|
14802
15391
|
);
|
|
14803
15392
|
}
|
|
14804
|
-
await
|
|
15393
|
+
await mkdir10(path26.dirname(member.path), { recursive: true });
|
|
14805
15394
|
await copyFile(member.base, member.path);
|
|
14806
15395
|
created = true;
|
|
14807
15396
|
}
|
|
@@ -14880,7 +15469,7 @@ function handEditProjectPath(editPath) {
|
|
|
14880
15469
|
async function readHandEditCompanion(editPath) {
|
|
14881
15470
|
let text;
|
|
14882
15471
|
try {
|
|
14883
|
-
text = await
|
|
15472
|
+
text = await readFile17(handEditCompanionPath(editPath), "utf8");
|
|
14884
15473
|
} catch {
|
|
14885
15474
|
return null;
|
|
14886
15475
|
}
|
|
@@ -14923,7 +15512,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
14923
15512
|
}
|
|
14924
15513
|
});
|
|
14925
15514
|
const bases = handEditBases(spec, lock);
|
|
14926
|
-
const expectedSize = bases?.[0] ? decodePng(await
|
|
15515
|
+
const expectedSize = bases?.[0] ? decodePng(await readFile17(bases[0].path)) : { width: spec.width, height: spec.height };
|
|
14927
15516
|
for (const [index, image] of decoded.entries()) {
|
|
14928
15517
|
if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
|
|
14929
15518
|
const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
|
|
@@ -14939,7 +15528,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
|
|
|
14939
15528
|
editor: input.editor,
|
|
14940
15529
|
protocol: input.protocol,
|
|
14941
15530
|
savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
14942
|
-
project: projectPath ?
|
|
15531
|
+
project: projectPath ? path26.basename(projectPath) : null,
|
|
14943
15532
|
outputs: incoming.map((frame, index) => ({
|
|
14944
15533
|
role: frame.role,
|
|
14945
15534
|
basedOn: basedOnHash(entry, index),
|
|
@@ -14969,7 +15558,7 @@ function basedOnHash(entry, index) {
|
|
|
14969
15558
|
async function replaceFile(file, bytes) {
|
|
14970
15559
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
14971
15560
|
try {
|
|
14972
|
-
await
|
|
15561
|
+
await writeFile12(tmp, bytes);
|
|
14973
15562
|
await rename8(tmp, file);
|
|
14974
15563
|
} finally {
|
|
14975
15564
|
await rm8(tmp, { force: true });
|
|
@@ -14999,7 +15588,7 @@ async function runEdit(args) {
|
|
|
14999
15588
|
const set = started.members.length > 1;
|
|
15000
15589
|
const kind = spec.generator === "frames" ? "frames" : "members";
|
|
15001
15590
|
log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
|
|
15002
|
-
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${
|
|
15591
|
+
if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path27.relative(process.cwd(), member.path)}`);
|
|
15003
15592
|
if (args.noOpen) {
|
|
15004
15593
|
log(` edit ${set ? "them" : "it"} with your own tool, then run pixelkiln plan; mount and pack place ${set ? "them" : "it"} in place of the generated art`);
|
|
15005
15594
|
} else {
|
|
@@ -15009,24 +15598,24 @@ async function runEdit(args) {
|
|
|
15009
15598
|
}
|
|
15010
15599
|
|
|
15011
15600
|
// src/cli/commands/gallery.ts
|
|
15012
|
-
import
|
|
15013
|
-
import { existsSync as
|
|
15601
|
+
import path37 from "path";
|
|
15602
|
+
import { existsSync as existsSync28 } from "fs";
|
|
15014
15603
|
|
|
15015
15604
|
// src/gallery/snapshot.ts
|
|
15016
15605
|
import { createHash as createHash4 } from "crypto";
|
|
15017
|
-
import { existsSync as
|
|
15018
|
-
import { readFile as
|
|
15019
|
-
import
|
|
15606
|
+
import { existsSync as existsSync23 } from "fs";
|
|
15607
|
+
import { readFile as readFile21, stat as stat2 } from "fs/promises";
|
|
15608
|
+
import path33 from "path";
|
|
15020
15609
|
|
|
15021
15610
|
// src/gallery/edit.ts
|
|
15022
|
-
import { readFile as
|
|
15023
|
-
import
|
|
15611
|
+
import { readFile as readFile20, rename as rename9, writeFile as writeFile16 } from "fs/promises";
|
|
15612
|
+
import path31 from "path";
|
|
15024
15613
|
import { z as z11 } from "zod";
|
|
15025
15614
|
|
|
15026
15615
|
// src/gallery/upload.ts
|
|
15027
|
-
import { existsSync as
|
|
15028
|
-
import { mkdir as
|
|
15029
|
-
import
|
|
15616
|
+
import { existsSync as existsSync19 } from "fs";
|
|
15617
|
+
import { mkdir as mkdir11, writeFile as writeFile13 } from "fs/promises";
|
|
15618
|
+
import path28 from "path";
|
|
15030
15619
|
import { z as z8 } from "zod";
|
|
15031
15620
|
var MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
|
|
15032
15621
|
var UploadImageSchema = z8.object({
|
|
@@ -15039,12 +15628,12 @@ var UploadImageSchema = z8.object({
|
|
|
15039
15628
|
overwrite: z8.boolean().optional()
|
|
15040
15629
|
}).strict();
|
|
15041
15630
|
async function saveProjectImage(root, request) {
|
|
15042
|
-
const file =
|
|
15043
|
-
const relative =
|
|
15044
|
-
if (relative.startsWith("..") ||
|
|
15631
|
+
const file = path28.resolve(root, request.path);
|
|
15632
|
+
const relative = path28.relative(root, file);
|
|
15633
|
+
if (relative.startsWith("..") || path28.isAbsolute(relative)) {
|
|
15045
15634
|
throw new ManifestEditError("an upload must stay inside the project");
|
|
15046
15635
|
}
|
|
15047
|
-
const extension =
|
|
15636
|
+
const extension = path28.extname(file).toLowerCase();
|
|
15048
15637
|
const expected = extension === ".png" ? "png" : extension === ".jpg" || extension === ".jpeg" ? "jpeg" : null;
|
|
15049
15638
|
if (!expected) throw new ManifestEditError("an upload must be a .png, .jpg, or .jpeg path");
|
|
15050
15639
|
const bytes = Buffer.from(request.base64, "base64");
|
|
@@ -15056,18 +15645,18 @@ async function saveProjectImage(root, request) {
|
|
|
15056
15645
|
if (metadata.format !== expected) {
|
|
15057
15646
|
throw new ManifestEditError(`the upload is a ${metadata.format.toUpperCase()}; name it ${metadata.format === "png" ? ".png" : ".jpg"}`);
|
|
15058
15647
|
}
|
|
15059
|
-
if (
|
|
15060
|
-
throw new ManifestEditError(`${relative.split(
|
|
15648
|
+
if (existsSync19(file) && !request.overwrite) {
|
|
15649
|
+
throw new ManifestEditError(`${relative.split(path28.sep).join("/")} already exists; choose another name, or replace it`);
|
|
15061
15650
|
}
|
|
15062
|
-
await
|
|
15063
|
-
await
|
|
15064
|
-
return relative.split(
|
|
15651
|
+
await mkdir11(path28.dirname(file), { recursive: true });
|
|
15652
|
+
await writeFile13(file, bytes);
|
|
15653
|
+
return relative.split(path28.sep).join("/");
|
|
15065
15654
|
}
|
|
15066
15655
|
|
|
15067
15656
|
// src/gallery/image-revision.ts
|
|
15068
|
-
import { existsSync as
|
|
15069
|
-
import { readFile as
|
|
15070
|
-
import
|
|
15657
|
+
import { existsSync as existsSync20 } from "fs";
|
|
15658
|
+
import { readFile as readFile18, rm as rm9, writeFile as writeFile14 } from "fs/promises";
|
|
15659
|
+
import path29 from "path";
|
|
15071
15660
|
import { z as z9 } from "zod";
|
|
15072
15661
|
var InpaintRevisionRequestSchema = z9.object({
|
|
15073
15662
|
action: z9.literal("create-inpaint-revision"),
|
|
@@ -15086,15 +15675,15 @@ var InpaintRevisionRequestSchema = z9.object({
|
|
|
15086
15675
|
overwrite: z9.boolean().optional()
|
|
15087
15676
|
}).strict();
|
|
15088
15677
|
async function createInpaintRevision(manifestPath, request) {
|
|
15089
|
-
const root =
|
|
15090
|
-
if (
|
|
15678
|
+
const root = path29.dirname(path29.resolve(manifestPath));
|
|
15679
|
+
if (path29.extname(request.maskPath).toLowerCase() !== ".png") {
|
|
15091
15680
|
throw new ManifestEditError("the mask must be a .png path inside the project");
|
|
15092
15681
|
}
|
|
15093
15682
|
const bytes = Buffer.from(request.maskBase64, "base64");
|
|
15094
15683
|
const metadata = imageMetadata(bytes);
|
|
15095
15684
|
if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
|
|
15096
|
-
const file =
|
|
15097
|
-
const previous =
|
|
15685
|
+
const file = path29.resolve(root, request.maskPath);
|
|
15686
|
+
const previous = existsSync20(file) ? await readFile18(file) : null;
|
|
15098
15687
|
const mask = await saveProjectImage(root, {
|
|
15099
15688
|
action: "upload-image",
|
|
15100
15689
|
path: request.maskPath,
|
|
@@ -15113,7 +15702,7 @@ async function createInpaintRevision(manifestPath, request) {
|
|
|
15113
15702
|
}
|
|
15114
15703
|
});
|
|
15115
15704
|
} catch (error) {
|
|
15116
|
-
if (previous) await
|
|
15705
|
+
if (previous) await writeFile14(file, previous);
|
|
15117
15706
|
else await rm9(file, { force: true });
|
|
15118
15707
|
throw error;
|
|
15119
15708
|
}
|
|
@@ -15121,9 +15710,9 @@ async function createInpaintRevision(manifestPath, request) {
|
|
|
15121
15710
|
}
|
|
15122
15711
|
|
|
15123
15712
|
// src/gallery/skeleton.ts
|
|
15124
|
-
import { existsSync as
|
|
15125
|
-
import { mkdir as
|
|
15126
|
-
import
|
|
15713
|
+
import { existsSync as existsSync21 } from "fs";
|
|
15714
|
+
import { mkdir as mkdir12, readFile as readFile19, rm as rm10, writeFile as writeFile15 } from "fs/promises";
|
|
15715
|
+
import path30 from "path";
|
|
15127
15716
|
import { z as z10 } from "zod";
|
|
15128
15717
|
var SkeletonEstimateRequestSchema = z10.object({
|
|
15129
15718
|
project: z10.string().min(1).optional(),
|
|
@@ -15162,8 +15751,8 @@ var SkeletonKeypointsUpdateSchema = z10.object({
|
|
|
15162
15751
|
}).strict();
|
|
15163
15752
|
var DEFAULT_ESTIMATE_LIMIT = 10;
|
|
15164
15753
|
function skeletonSourceFile(spec) {
|
|
15165
|
-
if (spec.source) return
|
|
15166
|
-
if (spec.quality &&
|
|
15754
|
+
if (spec.source) return path30.resolve(spec.root, spec.source);
|
|
15755
|
+
if (spec.quality && existsSync21(spec.quality.outFile)) return spec.quality.outFile;
|
|
15167
15756
|
return spec.outFile;
|
|
15168
15757
|
}
|
|
15169
15758
|
function createGallerySkeletonHandlers(opts) {
|
|
@@ -15184,10 +15773,10 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
15184
15773
|
const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === key);
|
|
15185
15774
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
15186
15775
|
const file = skeletonSourceFile(spec);
|
|
15187
|
-
if (!
|
|
15188
|
-
const bytes = await
|
|
15776
|
+
if (!existsSync21(file)) throw new ManifestEditError(`${key} has no image on disk to estimate from`);
|
|
15777
|
+
const bytes = await readFile19(file);
|
|
15189
15778
|
const metadata = imageMetadata(bytes);
|
|
15190
|
-
if (!metadata) throw new ManifestEditError(`${
|
|
15779
|
+
if (!metadata) throw new ManifestEditError(`${path30.basename(file)} is not a readable PNG or JPEG`);
|
|
15191
15780
|
const problem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
15192
15781
|
if (problem) throw new ManifestEditError(`${key} is ${problem}`);
|
|
15193
15782
|
if (used >= limit) {
|
|
@@ -15204,7 +15793,7 @@ function createGallerySkeletonHandlers(opts) {
|
|
|
15204
15793
|
log2(` estimated a skeleton for ${key} (PixelLab estimate-skeleton, ${used}/${limit} this session${billed ? `, billed ${billed.amount} ${billed.unit}` : ""})`);
|
|
15205
15794
|
return {
|
|
15206
15795
|
set: scaffoldSkeletonSet(res.keypoints, request.frames ?? DEFAULT_SCAFFOLD_FRAMES),
|
|
15207
|
-
source:
|
|
15796
|
+
source: path30.relative(ctx.loaded.root, file).split(path30.sep).join("/"),
|
|
15208
15797
|
session: status()
|
|
15209
15798
|
};
|
|
15210
15799
|
}
|
|
@@ -15218,21 +15807,21 @@ function billedAmount(usage) {
|
|
|
15218
15807
|
return typeof amount === "number" && Number.isFinite(amount) ? { unit: type, amount } : null;
|
|
15219
15808
|
}
|
|
15220
15809
|
async function createSkeletonAnimation(manifestPath, request) {
|
|
15221
|
-
const root =
|
|
15222
|
-
const file =
|
|
15223
|
-
const relative =
|
|
15224
|
-
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") ||
|
|
15810
|
+
const root = path30.dirname(path30.resolve(manifestPath));
|
|
15811
|
+
const file = path30.resolve(root, request.keypointsFile);
|
|
15812
|
+
const relative = path30.relative(root, file);
|
|
15813
|
+
if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path30.isAbsolute(relative)) {
|
|
15225
15814
|
throw new ManifestEditError("the keypoints file must be a .json path inside the project");
|
|
15226
15815
|
}
|
|
15227
|
-
const existed =
|
|
15816
|
+
const existed = existsSync21(file);
|
|
15228
15817
|
let previous = null;
|
|
15229
15818
|
if (request.set) {
|
|
15230
15819
|
if (existed && !request.overwrite) {
|
|
15231
15820
|
throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
|
|
15232
15821
|
}
|
|
15233
|
-
if (existed) previous = await
|
|
15234
|
-
await
|
|
15235
|
-
await
|
|
15822
|
+
if (existed) previous = await readFile19(file);
|
|
15823
|
+
await mkdir12(path30.dirname(file), { recursive: true });
|
|
15824
|
+
await writeFile15(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
15236
15825
|
} else if (!existed) {
|
|
15237
15826
|
throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
|
|
15238
15827
|
}
|
|
@@ -15247,7 +15836,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
15247
15836
|
revision: {
|
|
15248
15837
|
mode: "animate-skeleton",
|
|
15249
15838
|
from: request.from,
|
|
15250
|
-
keypointsFile: relative.split(
|
|
15839
|
+
keypointsFile: relative.split(path30.sep).join("/"),
|
|
15251
15840
|
direction: request.direction,
|
|
15252
15841
|
...request.description ? { description: request.description } : {},
|
|
15253
15842
|
...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
|
|
@@ -15256,7 +15845,7 @@ async function createSkeletonAnimation(manifestPath, request) {
|
|
|
15256
15845
|
});
|
|
15257
15846
|
} catch (error) {
|
|
15258
15847
|
if (request.set) {
|
|
15259
|
-
if (previous) await
|
|
15848
|
+
if (previous) await writeFile15(file, previous);
|
|
15260
15849
|
else await rm10(file, { force: true });
|
|
15261
15850
|
}
|
|
15262
15851
|
throw error;
|
|
@@ -15268,19 +15857,19 @@ async function updateSkeletonKeypoints(ctx, request) {
|
|
|
15268
15857
|
if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
|
|
15269
15858
|
const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
|
|
15270
15859
|
if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
|
|
15271
|
-
const relative =
|
|
15272
|
-
if (relative.startsWith("..") ||
|
|
15860
|
+
const relative = path30.relative(ctx.loaded.root, file);
|
|
15861
|
+
if (relative.startsWith("..") || path30.isAbsolute(relative)) {
|
|
15273
15862
|
throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
|
|
15274
15863
|
}
|
|
15275
|
-
const current =
|
|
15864
|
+
const current = existsSync21(file) ? sha256(await readFile19(file)) : null;
|
|
15276
15865
|
if (current !== request.expectedSha256) {
|
|
15277
15866
|
throw Object.assign(
|
|
15278
|
-
new Error(`${relative.split(
|
|
15867
|
+
new Error(`${relative.split(path30.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
|
|
15279
15868
|
{ status: 409 }
|
|
15280
15869
|
);
|
|
15281
15870
|
}
|
|
15282
|
-
await
|
|
15283
|
-
return relative.split(
|
|
15871
|
+
await writeFile15(file, JSON.stringify(request.set, null, 2) + "\n");
|
|
15872
|
+
return relative.split(path30.sep).join("/");
|
|
15284
15873
|
}
|
|
15285
15874
|
|
|
15286
15875
|
// src/gallery/edit.ts
|
|
@@ -15334,7 +15923,7 @@ function createGalleryEditHandler(opts) {
|
|
|
15334
15923
|
const history = /* @__PURE__ */ new Map();
|
|
15335
15924
|
const manifestOf = async (project) => {
|
|
15336
15925
|
try {
|
|
15337
|
-
return
|
|
15926
|
+
return path31.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
|
|
15338
15927
|
} catch {
|
|
15339
15928
|
return null;
|
|
15340
15929
|
}
|
|
@@ -15344,7 +15933,7 @@ function createGalleryEditHandler(opts) {
|
|
|
15344
15933
|
if (undo.success) {
|
|
15345
15934
|
const manifestPath2 = await manifestOf(undo.data.project);
|
|
15346
15935
|
if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
|
|
15347
|
-
const current = await
|
|
15936
|
+
const current = await readFile20(manifestPath2, "utf8");
|
|
15348
15937
|
if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
|
|
15349
15938
|
const stack = history.get(manifestPath2) ?? [];
|
|
15350
15939
|
const last = stack[stack.length - 1];
|
|
@@ -15354,17 +15943,17 @@ function createGalleryEditHandler(opts) {
|
|
|
15354
15943
|
throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
|
|
15355
15944
|
}
|
|
15356
15945
|
const tmp = manifestPath2 + ".pixelkiln-undo";
|
|
15357
|
-
await
|
|
15946
|
+
await writeFile16(tmp, last.before);
|
|
15358
15947
|
await rename9(tmp, manifestPath2);
|
|
15359
15948
|
stack.pop();
|
|
15360
|
-
log2(` manifest edit undone in ${
|
|
15949
|
+
log2(` manifest edit undone in ${path31.relative(process.cwd(), manifestPath2) || manifestPath2}`);
|
|
15361
15950
|
return opts.reload();
|
|
15362
15951
|
}
|
|
15363
15952
|
const manifestPath = await manifestOf(body?.project);
|
|
15364
|
-
const before = manifestPath ? await
|
|
15953
|
+
const before = manifestPath ? await readFile20(manifestPath, "utf8").catch(() => null) : null;
|
|
15365
15954
|
const build = await apply(body);
|
|
15366
15955
|
if (manifestPath && before !== null) {
|
|
15367
|
-
const after = await
|
|
15956
|
+
const after = await readFile20(manifestPath, "utf8").catch(() => null);
|
|
15368
15957
|
if (after !== null && after !== before) {
|
|
15369
15958
|
const stack = history.get(manifestPath) ?? [];
|
|
15370
15959
|
stack.push({ before, afterSha: sha256(after) });
|
|
@@ -15419,7 +16008,7 @@ function createEditApplier(opts, log2) {
|
|
|
15419
16008
|
} catch (error) {
|
|
15420
16009
|
throw new ManifestEditError(error instanceof Error ? error.message : String(error));
|
|
15421
16010
|
}
|
|
15422
|
-
const written = await saveProjectImage(
|
|
16011
|
+
const written = await saveProjectImage(path31.dirname(path31.resolve(manifestPath2)), upload.data);
|
|
15423
16012
|
log2(` image added: ${written}`);
|
|
15424
16013
|
return opts.reload();
|
|
15425
16014
|
}
|
|
@@ -15486,7 +16075,7 @@ function createEditApplier(opts, log2) {
|
|
|
15486
16075
|
}
|
|
15487
16076
|
const result = await applyManifestEdit(manifestPath, parsed.data);
|
|
15488
16077
|
if (result.changed) {
|
|
15489
|
-
const relative =
|
|
16078
|
+
const relative = path31.relative(process.cwd(), result.manifestPath);
|
|
15490
16079
|
const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
|
|
15491
16080
|
log2(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
|
|
15492
16081
|
}
|
|
@@ -15537,8 +16126,8 @@ function createGalleryPriceHandler(opts) {
|
|
|
15537
16126
|
}
|
|
15538
16127
|
|
|
15539
16128
|
// src/pipeline/history.ts
|
|
15540
|
-
import { existsSync as
|
|
15541
|
-
import
|
|
16129
|
+
import { existsSync as existsSync22 } from "fs";
|
|
16130
|
+
import path32 from "path";
|
|
15542
16131
|
var HISTORY_ENV = "PIXELKILN_HISTORY";
|
|
15543
16132
|
function historyLimit(manifest, env = process.env) {
|
|
15544
16133
|
if (manifest?.history !== void 0) return manifest.history;
|
|
@@ -15625,10 +16214,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
15625
16214
|
}
|
|
15626
16215
|
for (let i = 0; i < entry.outputs.length; i++) {
|
|
15627
16216
|
const file = currentEntryOutputPath(entry, spec, i);
|
|
15628
|
-
if (!
|
|
16217
|
+
if (!existsSync22(file)) continue;
|
|
15629
16218
|
if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
|
|
15630
16219
|
throw new OverwriteRefusedError(
|
|
15631
|
-
`${
|
|
16220
|
+
`${path32.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
|
|
15632
16221
|
[file]
|
|
15633
16222
|
);
|
|
15634
16223
|
}
|
|
@@ -15681,15 +16270,20 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
15681
16270
|
// src/gallery/snapshot.ts
|
|
15682
16271
|
var PROJECT_FILE_TYPE = "application/octet-stream";
|
|
15683
16272
|
function galleryMediaId(absolutePath) {
|
|
15684
|
-
return createHash4("sha256").update(
|
|
16273
|
+
return createHash4("sha256").update(path33.resolve(absolutePath)).digest("hex").slice(0, 24);
|
|
15685
16274
|
}
|
|
15686
16275
|
function galleryMediaRoute(id) {
|
|
15687
16276
|
return `/media/${id}`;
|
|
15688
16277
|
}
|
|
16278
|
+
function renderedSize(entry) {
|
|
16279
|
+
const vfx = entry?.providerMetadata?.[entry.provider]?.vfx;
|
|
16280
|
+
return typeof vfx?.width === "number" && typeof vfx.height === "number" ? { width: vfx.width, height: vfx.height } : null;
|
|
16281
|
+
}
|
|
15689
16282
|
function isDeclared(loaded, styleId, assetId) {
|
|
15690
16283
|
const asset = loaded.manifest.assets[assetId];
|
|
15691
|
-
|
|
15692
|
-
|
|
16284
|
+
const style = loaded.manifest.styles[styleId];
|
|
16285
|
+
if (!asset || !style) return false;
|
|
16286
|
+
return assetInStyle(asset, styleId, style);
|
|
15693
16287
|
}
|
|
15694
16288
|
function matchesFilter(filter, styleId, assetId) {
|
|
15695
16289
|
if (filter?.styles?.length && !filter.styles.includes(styleId)) return false;
|
|
@@ -15700,9 +16294,9 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
15700
16294
|
return (entry.history ?? []).map((generation, i) => {
|
|
15701
16295
|
const outputs = generation.outputs.map((output) => {
|
|
15702
16296
|
const mediaType = output.mediaType ?? MediaType.PNG;
|
|
15703
|
-
const file =
|
|
16297
|
+
const file = path33.join(cacheDir, cacheFileName(output.sha256, mediaType));
|
|
15704
16298
|
let url = null;
|
|
15705
|
-
if (
|
|
16299
|
+
if (existsSync23(file)) {
|
|
15706
16300
|
const id = galleryMediaId(file);
|
|
15707
16301
|
media.set(id, { path: file, contentType: mediaType });
|
|
15708
16302
|
url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
|
|
@@ -15734,7 +16328,7 @@ function describeHistory(media, entry, cacheDir) {
|
|
|
15734
16328
|
var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
|
|
15735
16329
|
async function samePixels(a, b) {
|
|
15736
16330
|
try {
|
|
15737
|
-
const [left, right] = await Promise.all([
|
|
16331
|
+
const [left, right] = await Promise.all([readFile21(a), readFile21(b)]);
|
|
15738
16332
|
if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
|
|
15739
16333
|
const x = decodePng(left);
|
|
15740
16334
|
const y = decodePng(right);
|
|
@@ -15832,10 +16426,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
|
|
|
15832
16426
|
async function describeQuality(media, root, inspection) {
|
|
15833
16427
|
const outputPaths = inspection.outputs ?? [inspection.output];
|
|
15834
16428
|
const outputs = await Promise.all(
|
|
15835
|
-
outputPaths.map((file) => describeOutput(media, root,
|
|
16429
|
+
outputPaths.map((file) => describeOutput(media, root, path33.resolve(root, file)))
|
|
15836
16430
|
);
|
|
15837
|
-
const recordPath =
|
|
15838
|
-
const recordExists =
|
|
16431
|
+
const recordPath = path33.resolve(root, inspection.record);
|
|
16432
|
+
const recordExists = existsSync23(recordPath);
|
|
15839
16433
|
let options = null;
|
|
15840
16434
|
let check = null;
|
|
15841
16435
|
if (recordExists) {
|
|
@@ -15854,7 +16448,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
15854
16448
|
return {
|
|
15855
16449
|
state: inspection.state,
|
|
15856
16450
|
reason: inspection.reason,
|
|
15857
|
-
output: portableOutputPath(
|
|
16451
|
+
output: portableOutputPath(path33.resolve(root, inspection.output), root),
|
|
15858
16452
|
record: portableOutputPath(recordPath, root),
|
|
15859
16453
|
recordExists,
|
|
15860
16454
|
outputs,
|
|
@@ -15892,9 +16486,9 @@ async function buildGallerySnapshot(opts) {
|
|
|
15892
16486
|
const media = /* @__PURE__ */ new Map();
|
|
15893
16487
|
const plan = await buildPlan(specs, lock);
|
|
15894
16488
|
const items = [];
|
|
15895
|
-
const manifestText = await
|
|
16489
|
+
const manifestText = await readFile21(loaded.path, "utf8");
|
|
15896
16490
|
const manifestSha256 = sha256(manifestText);
|
|
15897
|
-
const cacheDir =
|
|
16491
|
+
const cacheDir = path33.resolve(path33.dirname(opts.lockPath), ".pixelkiln", "cache");
|
|
15898
16492
|
let rawStyles = {};
|
|
15899
16493
|
try {
|
|
15900
16494
|
const raw = JSON.parse(manifestText);
|
|
@@ -15918,12 +16512,12 @@ async function buildGallerySnapshot(opts) {
|
|
|
15918
16512
|
)
|
|
15919
16513
|
);
|
|
15920
16514
|
if (spec.source) {
|
|
15921
|
-
const editPath =
|
|
16515
|
+
const editPath = path33.resolve(root, spec.source);
|
|
15922
16516
|
const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
|
|
15923
16517
|
const editShas = await Promise.all(
|
|
15924
16518
|
Array.from({ length: memberCount }, async (_, index) => {
|
|
15925
16519
|
const file = sourceOutputPath(spec.source, entry, index, root);
|
|
15926
|
-
return { file, sha256:
|
|
16520
|
+
return { file, sha256: existsSync23(file) ? await sha256File(file) : null };
|
|
15927
16521
|
})
|
|
15928
16522
|
);
|
|
15929
16523
|
edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
|
|
@@ -15931,7 +16525,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
15931
16525
|
const companion = await readHandEditCompanion(editPath);
|
|
15932
16526
|
if (companion) {
|
|
15933
16527
|
const projectPath = handEditProjectPath(editPath);
|
|
15934
|
-
const project = companion.project &&
|
|
16528
|
+
const project = companion.project && existsSync23(projectPath) ? await fileInfo(projectPath) : null;
|
|
15935
16529
|
let projectUrl = null;
|
|
15936
16530
|
if (project) {
|
|
15937
16531
|
const id = galleryMediaId(projectPath);
|
|
@@ -15967,8 +16561,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
15967
16561
|
editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
|
|
15968
16562
|
}
|
|
15969
16563
|
} else if (spec.source) {
|
|
15970
|
-
outputs = [await describeOutput(media, root,
|
|
15971
|
-
} else if (!entry &&
|
|
16564
|
+
outputs = [await describeOutput(media, root, path33.resolve(root, spec.source))];
|
|
16565
|
+
} else if (!entry && existsSync23(spec.outFile)) {
|
|
15972
16566
|
outputs = [await describeOutput(media, root, spec.outFile)];
|
|
15973
16567
|
} else {
|
|
15974
16568
|
outputs = [];
|
|
@@ -15989,8 +16583,8 @@ async function buildGallerySnapshot(opts) {
|
|
|
15989
16583
|
generator: entry?.generator ?? spec.generator,
|
|
15990
16584
|
prompt: entry?.prompt ?? spec.prompt,
|
|
15991
16585
|
currentPrompt: entry && entry.prompt !== spec.prompt ? spec.prompt : null,
|
|
15992
|
-
width: entry?.width ?? spec.width,
|
|
15993
|
-
height: entry?.height ?? spec.height,
|
|
16586
|
+
width: renderedSize(entry)?.width ?? entry?.width ?? spec.width,
|
|
16587
|
+
height: renderedSize(entry)?.height ?? entry?.height ?? spec.height,
|
|
15994
16588
|
tileFeature: entry?.tileFeature ?? spec.tileFeature ?? null,
|
|
15995
16589
|
fps: quality?.frameSet?.fps ?? metadataFps(entry) ?? spec.quality?.fps ?? null,
|
|
15996
16590
|
cost: entry?.cost ?? 0,
|
|
@@ -16171,7 +16765,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16171
16765
|
id: loaded.manifest.name,
|
|
16172
16766
|
name: loaded.manifest.name,
|
|
16173
16767
|
manifest: loaded.path,
|
|
16174
|
-
lock:
|
|
16768
|
+
lock: path33.resolve(opts.lockPath),
|
|
16175
16769
|
root,
|
|
16176
16770
|
provider: loaded.manifest.provider,
|
|
16177
16771
|
account: null,
|
|
@@ -16197,7 +16791,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
16197
16791
|
return { snapshot, media };
|
|
16198
16792
|
}
|
|
16199
16793
|
async function buildWorkspaceGallerySnapshot(opts) {
|
|
16200
|
-
const dir =
|
|
16794
|
+
const dir = path33.dirname(path33.resolve(opts.workspacePath));
|
|
16201
16795
|
const media = /* @__PURE__ */ new Map();
|
|
16202
16796
|
const projects = [];
|
|
16203
16797
|
const items = [];
|
|
@@ -16240,11 +16834,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16240
16834
|
id: project.id,
|
|
16241
16835
|
name: loaded.manifest.name,
|
|
16242
16836
|
manifest: manifestPath,
|
|
16243
|
-
lock:
|
|
16837
|
+
lock: path33.resolve(lockPath),
|
|
16244
16838
|
root: loaded.root,
|
|
16245
16839
|
provider: loaded.manifest.provider,
|
|
16246
16840
|
account: project.account ?? null,
|
|
16247
|
-
manifestSha256: manifestSha256 ?? sha256(await
|
|
16841
|
+
manifestSha256: manifestSha256 ?? sha256(await readFile21(manifestPath, "utf8")),
|
|
16248
16842
|
historyLimit: historyLimit(loaded.manifest),
|
|
16249
16843
|
entries: Object.keys(lock.entries).length,
|
|
16250
16844
|
items: projectItems.length,
|
|
@@ -16256,8 +16850,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16256
16850
|
id: project.id,
|
|
16257
16851
|
name: project.id,
|
|
16258
16852
|
manifest: manifestPath,
|
|
16259
|
-
lock:
|
|
16260
|
-
root:
|
|
16853
|
+
lock: path33.resolve(lockPath),
|
|
16854
|
+
root: path33.dirname(manifestPath),
|
|
16261
16855
|
provider: project.provider,
|
|
16262
16856
|
account: project.account ?? null,
|
|
16263
16857
|
manifestSha256: null,
|
|
@@ -16284,7 +16878,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16284
16878
|
version: 1,
|
|
16285
16879
|
generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
16286
16880
|
project: null,
|
|
16287
|
-
workspace: { path:
|
|
16881
|
+
workspace: { path: path33.resolve(opts.workspacePath), dir, projects },
|
|
16288
16882
|
filter,
|
|
16289
16883
|
totals: {
|
|
16290
16884
|
entries: projects.reduce((sum, project) => sum + project.entries, 0),
|
|
@@ -16303,10 +16897,10 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
16303
16897
|
// src/gallery/server.ts
|
|
16304
16898
|
import { randomBytes } from "crypto";
|
|
16305
16899
|
import { createServer as createServer2 } from "http";
|
|
16306
|
-
import { readFile as
|
|
16900
|
+
import { readFile as readFile22 } from "fs/promises";
|
|
16307
16901
|
|
|
16308
16902
|
// src/gallery/page.ts
|
|
16309
|
-
import { existsSync as
|
|
16903
|
+
import { existsSync as existsSync24, readFileSync as readFileSync5 } from "fs";
|
|
16310
16904
|
|
|
16311
16905
|
// src/gallery/client-bundle.ts
|
|
16312
16906
|
import { createRequire } from "module";
|
|
@@ -16347,7 +16941,7 @@ function clientAssets() {
|
|
|
16347
16941
|
const prebuilt = new URL("gallery.js", CLIENT_DIR);
|
|
16348
16942
|
client ??= {
|
|
16349
16943
|
css: readFileSync5(new URL("gallery.css", CLIENT_DIR), "utf8"),
|
|
16350
|
-
js:
|
|
16944
|
+
js: existsSync24(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
|
|
16351
16945
|
};
|
|
16352
16946
|
return client;
|
|
16353
16947
|
}
|
|
@@ -16618,7 +17212,7 @@ async function serveGallery(opts) {
|
|
|
16618
17212
|
const asset = reviewAssets.get(jobId)?.get(url2.pathname);
|
|
16619
17213
|
if (!asset) return fail(404, "no such review asset");
|
|
16620
17214
|
try {
|
|
16621
|
-
const bytes = await
|
|
17215
|
+
const bytes = await readFile22(asset.path);
|
|
16622
17216
|
res.writeHead(200, {
|
|
16623
17217
|
"Content-Type": asset.contentType,
|
|
16624
17218
|
"Cache-Control": "no-store",
|
|
@@ -16701,7 +17295,7 @@ async function serveGallery(opts) {
|
|
|
16701
17295
|
if (!asset) return fail(404, "no such gallery media");
|
|
16702
17296
|
let bytes;
|
|
16703
17297
|
try {
|
|
16704
|
-
bytes = await
|
|
17298
|
+
bytes = await readFile22(asset.path);
|
|
16705
17299
|
} catch {
|
|
16706
17300
|
return fail(404, "gallery media is no longer on disk; refresh the page");
|
|
16707
17301
|
}
|
|
@@ -17074,6 +17668,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
17074
17668
|
const anchor = spec.character?.styleAnchor;
|
|
17075
17669
|
const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
|
|
17076
17670
|
const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
|
|
17671
|
+
const lastGeneration = previousEntry?.status === "downloaded" ? previousEntry : previousEntry?.history?.[0];
|
|
17672
|
+
const lastGenerationMetadata = lastGeneration?.providerMetadata?.[provider.id];
|
|
17077
17673
|
const parentOutputs = spec.character?.kind === "outfit" && parentEntry ? parentEntry.outputs.map((output, index) => ({
|
|
17078
17674
|
path: currentEntryOutputPath(parentEntry, spec.character.parentSpec, index),
|
|
17079
17675
|
role: output.role
|
|
@@ -17086,6 +17682,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
17086
17682
|
...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
|
|
17087
17683
|
...parentOutputs ? { parentOutputs } : {},
|
|
17088
17684
|
...replacedMetadata ? { replacedMetadata } : {},
|
|
17685
|
+
...lastGenerationMetadata ? { lastGenerationMetadata } : {},
|
|
17089
17686
|
...previousMetadata ? { previousMetadata } : {},
|
|
17090
17687
|
checkpoint: async (checkpoint) => {
|
|
17091
17688
|
if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
|
|
@@ -17161,13 +17758,13 @@ function ownedReferenceImageId(spec, lock, providerId) {
|
|
|
17161
17758
|
}
|
|
17162
17759
|
|
|
17163
17760
|
// src/pick/server.ts
|
|
17164
|
-
import { existsSync as
|
|
17165
|
-
import { readFile as
|
|
17761
|
+
import { existsSync as existsSync26 } from "fs";
|
|
17762
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
17166
17763
|
|
|
17167
17764
|
// src/pipeline/init.ts
|
|
17168
|
-
import { readdir, readFile as
|
|
17169
|
-
import { existsSync as
|
|
17170
|
-
import
|
|
17765
|
+
import { readdir, readFile as readFile23, writeFile as writeFile17 } from "fs/promises";
|
|
17766
|
+
import { existsSync as existsSync25 } from "fs";
|
|
17767
|
+
import path34 from "path";
|
|
17171
17768
|
function pngSize(buf) {
|
|
17172
17769
|
if (buf.length < 24) return null;
|
|
17173
17770
|
const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
|
|
@@ -17181,7 +17778,7 @@ function slugify(value) {
|
|
|
17181
17778
|
async function walk(dir, exclude) {
|
|
17182
17779
|
const out = [];
|
|
17183
17780
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
17184
|
-
const full =
|
|
17781
|
+
const full = path34.join(dir, entry.name);
|
|
17185
17782
|
if (exclude.some((x) => full.includes(x))) continue;
|
|
17186
17783
|
if (entry.isDirectory()) out.push(...await walk(full, exclude));
|
|
17187
17784
|
else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
|
|
@@ -17195,14 +17792,14 @@ async function scanAssets(root, opts = {}) {
|
|
|
17195
17792
|
const skipped = [];
|
|
17196
17793
|
const seen = /* @__PURE__ */ new Map();
|
|
17197
17794
|
for (const file of files.sort()) {
|
|
17198
|
-
const size = pngSize(await
|
|
17795
|
+
const size = pngSize(await readFile23(file));
|
|
17199
17796
|
if (!size) {
|
|
17200
17797
|
skipped.push(`${file} (not a readable PNG)`);
|
|
17201
17798
|
continue;
|
|
17202
17799
|
}
|
|
17203
|
-
const rel =
|
|
17204
|
-
const category =
|
|
17205
|
-
let id = slugify(
|
|
17800
|
+
const rel = path34.relative(root, file);
|
|
17801
|
+
const category = path34.dirname(rel) === "." ? "" : path34.dirname(rel);
|
|
17802
|
+
let id = slugify(path34.basename(rel));
|
|
17206
17803
|
if (seen.has(id)) {
|
|
17207
17804
|
const n = seen.get(id) + 1;
|
|
17208
17805
|
seen.set(id, n);
|
|
@@ -17260,10 +17857,10 @@ function mode(values) {
|
|
|
17260
17857
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
|
|
17261
17858
|
}
|
|
17262
17859
|
async function writeManifestFile(target, manifest) {
|
|
17263
|
-
if (
|
|
17860
|
+
if (existsSync25(target)) {
|
|
17264
17861
|
throw new Error(`${target} already exists; refusing to overwrite an existing manifest.`);
|
|
17265
17862
|
}
|
|
17266
|
-
await
|
|
17863
|
+
await writeFile17(target, JSON.stringify(manifest, null, 2) + "\n");
|
|
17267
17864
|
}
|
|
17268
17865
|
|
|
17269
17866
|
// src/pick/sheet.ts
|
|
@@ -17676,13 +18273,13 @@ async function currentArt(entry, spec) {
|
|
|
17676
18273
|
const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
|
|
17677
18274
|
if (!previous) return null;
|
|
17678
18275
|
const file = resolveOutputPath(previous.path, spec.root);
|
|
17679
|
-
if (!
|
|
18276
|
+
if (!existsSync26(file)) return null;
|
|
17680
18277
|
const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
|
|
17681
18278
|
if (!contentType) return null;
|
|
17682
18279
|
let width = entry.width;
|
|
17683
18280
|
let height = entry.height;
|
|
17684
18281
|
if (contentType === "image/png") {
|
|
17685
|
-
const size = pngSize(await
|
|
18282
|
+
const size = pngSize(await readFile24(file));
|
|
17686
18283
|
if (size) ({ width, height } = size);
|
|
17687
18284
|
}
|
|
17688
18285
|
return { path: file, contentType, width, height };
|
|
@@ -17690,7 +18287,7 @@ async function currentArt(entry, spec) {
|
|
|
17690
18287
|
async function cachedFrame(url) {
|
|
17691
18288
|
if (!url.startsWith("file://")) return null;
|
|
17692
18289
|
const file = url.slice("file://".length);
|
|
17693
|
-
const contentType = mediaTypeFromExtension(file) ?? detectMediaType(await
|
|
18290
|
+
const contentType = mediaTypeFromExtension(file) ?? detectMediaType(await readFile24(file).catch(() => Buffer.alloc(0))) ?? "application/octet-stream";
|
|
17694
18291
|
return { path: file, contentType };
|
|
17695
18292
|
}
|
|
17696
18293
|
async function prepareReview(provider, lock, opts = {}) {
|
|
@@ -18236,15 +18833,15 @@ function createGenerateHandlers(opts) {
|
|
|
18236
18833
|
}
|
|
18237
18834
|
|
|
18238
18835
|
// src/gallery/editor.ts
|
|
18239
|
-
import { readFile as
|
|
18240
|
-
import
|
|
18836
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
18837
|
+
import path36 from "path";
|
|
18241
18838
|
|
|
18242
18839
|
// src/editor/install.ts
|
|
18243
18840
|
import { createHash as createHash5 } from "crypto";
|
|
18244
|
-
import { existsSync as
|
|
18245
|
-
import { mkdir as
|
|
18841
|
+
import { existsSync as existsSync27 } from "fs";
|
|
18842
|
+
import { mkdir as mkdir13, readFile as readFile25, rename as rename10, rm as rm11, stat as stat3, writeFile as writeFile18 } from "fs/promises";
|
|
18246
18843
|
import os3 from "os";
|
|
18247
|
-
import
|
|
18844
|
+
import path35 from "path";
|
|
18248
18845
|
|
|
18249
18846
|
// tools/pixelorama-bridge/pin.json
|
|
18250
18847
|
var pin_default = {
|
|
@@ -18308,15 +18905,15 @@ var EditorInstallError = class extends Error {
|
|
|
18308
18905
|
};
|
|
18309
18906
|
function editorToolsRoot() {
|
|
18310
18907
|
const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
|
|
18311
|
-
if (override) return
|
|
18312
|
-
if (process.platform === "darwin") return
|
|
18908
|
+
if (override) return path35.resolve(override);
|
|
18909
|
+
if (process.platform === "darwin") return path35.join(os3.homedir(), "Library", "Caches", "pixelkiln", "tools");
|
|
18313
18910
|
if (process.platform === "win32") {
|
|
18314
|
-
return
|
|
18911
|
+
return path35.join(process.env.LOCALAPPDATA ?? path35.join(os3.homedir(), "AppData", "Local"), "pixelkiln", "tools");
|
|
18315
18912
|
}
|
|
18316
|
-
return
|
|
18913
|
+
return path35.join(process.env.XDG_CACHE_HOME ?? path35.join(os3.homedir(), ".cache"), "pixelkiln", "tools");
|
|
18317
18914
|
}
|
|
18318
18915
|
function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
|
|
18319
|
-
return
|
|
18916
|
+
return path35.join(root, "pixelorama", pin.release ?? "unpublished");
|
|
18320
18917
|
}
|
|
18321
18918
|
function editorBaseUrl(pin = EDITOR_PIN) {
|
|
18322
18919
|
const override = process.env.PIXELKILN_EDITOR_URL?.trim();
|
|
@@ -18326,7 +18923,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
|
|
|
18326
18923
|
}
|
|
18327
18924
|
async function sha256Of(file) {
|
|
18328
18925
|
try {
|
|
18329
|
-
return createHash5("sha256").update(await
|
|
18926
|
+
return createHash5("sha256").update(await readFile25(file)).digest("hex");
|
|
18330
18927
|
} catch {
|
|
18331
18928
|
return null;
|
|
18332
18929
|
}
|
|
@@ -18339,8 +18936,8 @@ async function editorStatus(opts = {}) {
|
|
|
18339
18936
|
let totalBytes = 0;
|
|
18340
18937
|
for (const [name, expected] of Object.entries(pin.files)) {
|
|
18341
18938
|
totalBytes += expected.bytes;
|
|
18342
|
-
const file =
|
|
18343
|
-
const info =
|
|
18939
|
+
const file = path35.join(dir, name);
|
|
18940
|
+
const info = existsSync27(file) ? await stat3(file).catch(() => null) : null;
|
|
18344
18941
|
if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
|
|
18345
18942
|
installedBytes += expected.bytes;
|
|
18346
18943
|
} else {
|
|
@@ -18369,7 +18966,7 @@ async function installEditor(opts = {}) {
|
|
|
18369
18966
|
);
|
|
18370
18967
|
}
|
|
18371
18968
|
if (!baseUrl) throw new EditorInstallError("no download location for the editor build");
|
|
18372
|
-
await
|
|
18969
|
+
await mkdir13(dir, { recursive: true });
|
|
18373
18970
|
const downloaded = [];
|
|
18374
18971
|
const skipped = Object.keys(pin.files).filter((name) => !before.missing.includes(name));
|
|
18375
18972
|
let fetchedBytes = before.installedBytes;
|
|
@@ -18397,10 +18994,10 @@ async function installEditor(opts = {}) {
|
|
|
18397
18994
|
if (digest2 !== expected.sha256) {
|
|
18398
18995
|
throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
|
|
18399
18996
|
}
|
|
18400
|
-
const final =
|
|
18997
|
+
const final = path35.join(dir, name);
|
|
18401
18998
|
const tmp = `${final}.${process.pid}.part`;
|
|
18402
18999
|
try {
|
|
18403
|
-
await
|
|
19000
|
+
await writeFile18(tmp, bytes);
|
|
18404
19001
|
await rename10(tmp, final);
|
|
18405
19002
|
} finally {
|
|
18406
19003
|
await rm11(tmp, { force: true });
|
|
@@ -18443,7 +19040,7 @@ var EDITOR_CONTENT_TYPES = {
|
|
|
18443
19040
|
".json": "application/json; charset=utf-8"
|
|
18444
19041
|
};
|
|
18445
19042
|
function editorContentType(name) {
|
|
18446
|
-
return EDITOR_CONTENT_TYPES[
|
|
19043
|
+
return EDITOR_CONTENT_TYPES[path35.extname(name).toLowerCase()] ?? null;
|
|
18447
19044
|
}
|
|
18448
19045
|
|
|
18449
19046
|
// src/gallery/editor.ts
|
|
@@ -18519,7 +19116,7 @@ function createGalleryEditorHandlers(opts = {}) {
|
|
|
18519
19116
|
if (!status.installed) return null;
|
|
18520
19117
|
let bytes;
|
|
18521
19118
|
try {
|
|
18522
|
-
bytes = await
|
|
19119
|
+
bytes = await readFile26(path36.join(dir, name));
|
|
18523
19120
|
} catch {
|
|
18524
19121
|
verified = null;
|
|
18525
19122
|
return null;
|
|
@@ -18559,7 +19156,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18559
19156
|
};
|
|
18560
19157
|
const loadProject = async (project) => {
|
|
18561
19158
|
const ctx = await access2.loadProject(project);
|
|
18562
|
-
loadProjectEnv(
|
|
19159
|
+
loadProjectEnv(path37.dirname(ctx.loaded.path));
|
|
18563
19160
|
return ctx;
|
|
18564
19161
|
};
|
|
18565
19162
|
const loadProjectEnv = (dir) => {
|
|
@@ -18568,7 +19165,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18568
19165
|
for (const [name, value] of Object.entries(declared)) {
|
|
18569
19166
|
if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
|
|
18570
19167
|
throw new Error(
|
|
18571
|
-
`${
|
|
19168
|
+
`${path37.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
|
|
18572
19169
|
);
|
|
18573
19170
|
}
|
|
18574
19171
|
}
|
|
@@ -18576,7 +19173,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18576
19173
|
};
|
|
18577
19174
|
const quoters = /* @__PURE__ */ new Map();
|
|
18578
19175
|
const quoteFor = async (project) => {
|
|
18579
|
-
loadProjectEnv(
|
|
19176
|
+
loadProjectEnv(path37.dirname(path37.resolve(await access2.manifestFor(project))));
|
|
18580
19177
|
const key = process.env.PIXELLAB_API_KEY;
|
|
18581
19178
|
if (!key) return null;
|
|
18582
19179
|
let quoter = quoters.get(key);
|
|
@@ -18627,8 +19224,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
|
|
|
18627
19224
|
}
|
|
18628
19225
|
async function runGallery(args) {
|
|
18629
19226
|
if (args.workspace) {
|
|
18630
|
-
const workspacePath =
|
|
18631
|
-
if (!
|
|
19227
|
+
const workspacePath = path37.resolve(args.workspace);
|
|
19228
|
+
if (!existsSync28(workspacePath)) {
|
|
18632
19229
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
18633
19230
|
}
|
|
18634
19231
|
const filter2 = { styles: args.styles, assets: args.assets };
|
|
@@ -18648,7 +19245,7 @@ async function runGallery(args) {
|
|
|
18648
19245
|
const registered = async (projectId) => {
|
|
18649
19246
|
const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
|
|
18650
19247
|
if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
|
|
18651
|
-
return resolveProject(
|
|
19248
|
+
return resolveProject(path37.dirname(workspacePath), project);
|
|
18652
19249
|
};
|
|
18653
19250
|
await serveUntilStopped(initial, build2, args, {
|
|
18654
19251
|
manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
|
|
@@ -18673,7 +19270,7 @@ async function runGallery(args) {
|
|
|
18673
19270
|
return buildGallerySnapshot({ ...fresh, filter });
|
|
18674
19271
|
};
|
|
18675
19272
|
await serveUntilStopped(await build(), reload, args, {
|
|
18676
|
-
manifestFor: () =>
|
|
19273
|
+
manifestFor: () => path37.resolve(args.manifest),
|
|
18677
19274
|
loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
|
|
18678
19275
|
});
|
|
18679
19276
|
}
|
|
@@ -18916,7 +19513,7 @@ async function runLifecycle(args, wave, spent) {
|
|
|
18916
19513
|
log(` Packaging stays blocked until each refined PNG has a current human approval.`);
|
|
18917
19514
|
}
|
|
18918
19515
|
}
|
|
18919
|
-
return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.character?.styleAnchor || spec.revision || spec.mirror);
|
|
19516
|
+
return args.command === "gen" && submitted > 0 && total.downloaded > 0 && !total.failed && specs.some((spec) => spec.character?.parentSpec || spec.character?.styleAnchor || spec.revision || spec.mirror || spec.scene || spec.vfx?.sprite && "file" in spec.vfx.sprite);
|
|
18920
19517
|
}
|
|
18921
19518
|
return false;
|
|
18922
19519
|
}
|
|
@@ -18982,18 +19579,18 @@ async function runRestoreGeneration(args) {
|
|
|
18982
19579
|
}
|
|
18983
19580
|
|
|
18984
19581
|
// src/cli/commands/housekeeping.ts
|
|
18985
|
-
import { existsSync as
|
|
19582
|
+
import { existsSync as existsSync30 } from "fs";
|
|
18986
19583
|
|
|
18987
19584
|
// src/pipeline/cache-health.ts
|
|
18988
|
-
import { existsSync as
|
|
18989
|
-
import { readFile as
|
|
18990
|
-
import
|
|
19585
|
+
import { existsSync as existsSync29 } from "fs";
|
|
19586
|
+
import { readFile as readFile27, readdir as readdir2, rm as rm12 } from "fs/promises";
|
|
19587
|
+
import path38 from "path";
|
|
18991
19588
|
async function inspectCaches(lock, lockPath, options = {}) {
|
|
18992
|
-
if (options.prune && !
|
|
18993
|
-
throw new Error(`Refusing to prune without an existing lockfile at ${
|
|
19589
|
+
if (options.prune && !existsSync29(lockPath)) {
|
|
19590
|
+
throw new Error(`Refusing to prune without an existing lockfile at ${path38.resolve(lockPath)}`);
|
|
18994
19591
|
}
|
|
18995
|
-
const contentDir =
|
|
18996
|
-
const remotePath =
|
|
19592
|
+
const contentDir = path38.resolve(path38.dirname(lockPath), ".pixelkiln", "cache");
|
|
19593
|
+
const remotePath = path38.resolve(cachePathFor(lockPath));
|
|
18997
19594
|
const referenced = referencedHashes(lock);
|
|
18998
19595
|
let content = await inspectContentCache(contentDir, referenced);
|
|
18999
19596
|
let remoteHashes = await inspectRemoteHashCache(remotePath);
|
|
@@ -19005,7 +19602,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
19005
19602
|
]);
|
|
19006
19603
|
for (const name of names) {
|
|
19007
19604
|
try {
|
|
19008
|
-
await rm12(
|
|
19605
|
+
await rm12(path38.join(contentDir, name), { force: true });
|
|
19009
19606
|
removed.contentFiles++;
|
|
19010
19607
|
} catch {
|
|
19011
19608
|
}
|
|
@@ -19014,7 +19611,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
19014
19611
|
await saveCache(remotePath, { version: 1, hashes: {} });
|
|
19015
19612
|
removed.resetRemoteHashCache = true;
|
|
19016
19613
|
} else if (remoteHashes.invalidIds.length) {
|
|
19017
|
-
const cache = parseCache(JSON.parse(await
|
|
19614
|
+
const cache = parseCache(JSON.parse(await readFile27(remotePath, "utf8")));
|
|
19018
19615
|
for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
|
|
19019
19616
|
removed.remoteHashEntries = remoteHashes.invalidIds.length;
|
|
19020
19617
|
await saveCache(remotePath, cache);
|
|
@@ -19032,7 +19629,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
|
|
|
19032
19629
|
async function inspectContentCache(contentDir, referenced) {
|
|
19033
19630
|
const report = {
|
|
19034
19631
|
path: contentDir,
|
|
19035
|
-
exists:
|
|
19632
|
+
exists: existsSync29(contentDir),
|
|
19036
19633
|
files: 0,
|
|
19037
19634
|
bytes: 0,
|
|
19038
19635
|
valid: 0,
|
|
@@ -19053,12 +19650,12 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
19053
19650
|
continue;
|
|
19054
19651
|
}
|
|
19055
19652
|
report.files++;
|
|
19056
|
-
const file =
|
|
19653
|
+
const file = path38.join(contentDir, entry.name);
|
|
19057
19654
|
const mediaType = mediaTypeFromExtension(entry.name);
|
|
19058
19655
|
const expected = mediaType ? entry.name.slice(0, -4) : "";
|
|
19059
19656
|
let bytes;
|
|
19060
19657
|
try {
|
|
19061
|
-
bytes = await
|
|
19658
|
+
bytes = await readFile27(file);
|
|
19062
19659
|
report.bytes += bytes.length;
|
|
19063
19660
|
} catch (err) {
|
|
19064
19661
|
report.invalid.push({
|
|
@@ -19096,7 +19693,7 @@ async function inspectContentCache(contentDir, referenced) {
|
|
|
19096
19693
|
async function inspectRemoteHashCache(remotePath) {
|
|
19097
19694
|
const report = {
|
|
19098
19695
|
path: remotePath,
|
|
19099
|
-
exists:
|
|
19696
|
+
exists: existsSync29(remotePath),
|
|
19100
19697
|
entries: 0,
|
|
19101
19698
|
valid: 0,
|
|
19102
19699
|
invalidIds: [],
|
|
@@ -19105,7 +19702,7 @@ async function inspectRemoteHashCache(remotePath) {
|
|
|
19105
19702
|
if (!report.exists) return report;
|
|
19106
19703
|
let cache;
|
|
19107
19704
|
try {
|
|
19108
|
-
cache = parseCache(JSON.parse(await
|
|
19705
|
+
cache = parseCache(JSON.parse(await readFile27(remotePath, "utf8")));
|
|
19109
19706
|
} catch (err) {
|
|
19110
19707
|
report.error = err instanceof Error ? err.message : String(err);
|
|
19111
19708
|
return report;
|
|
@@ -19169,7 +19766,7 @@ async function runAccept(args) {
|
|
|
19169
19766
|
let intact = true;
|
|
19170
19767
|
for (const output of entry.outputs) {
|
|
19171
19768
|
const file = resolveOutputPath(output.path, item.spec.root);
|
|
19172
|
-
if (!
|
|
19769
|
+
if (!existsSync30(file) || await sha256File(file) !== output.sha256) {
|
|
19173
19770
|
intact = false;
|
|
19174
19771
|
break;
|
|
19175
19772
|
}
|
|
@@ -19224,42 +19821,42 @@ This drops their provenance from the lockfile. The art on disk is`);
|
|
|
19224
19821
|
}
|
|
19225
19822
|
|
|
19226
19823
|
// src/cli/commands/init.ts
|
|
19227
|
-
import
|
|
19228
|
-
import { existsSync as
|
|
19824
|
+
import path39 from "path";
|
|
19825
|
+
import { existsSync as existsSync31 } from "fs";
|
|
19229
19826
|
async function runInit(args) {
|
|
19230
19827
|
if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
|
|
19231
|
-
const root =
|
|
19232
|
-
if (!
|
|
19828
|
+
const root = path39.resolve(args.from);
|
|
19829
|
+
if (!existsSync31(root)) throw new Error(`No directory at ${root}`);
|
|
19233
19830
|
const generator = args.generator ?? "map";
|
|
19234
19831
|
if (generator !== "1dir" && generator !== "map") {
|
|
19235
19832
|
throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
|
|
19236
19833
|
}
|
|
19237
|
-
const target =
|
|
19834
|
+
const target = path39.resolve(args.out ?? "pixelkiln.manifest.json");
|
|
19238
19835
|
const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
|
|
19239
19836
|
if (!assets.length) throw new Error(`No PNGs found under ${root}`);
|
|
19240
19837
|
const manifest = buildManifest(
|
|
19241
|
-
args.name ??
|
|
19838
|
+
args.name ?? path39.basename(path39.dirname(target)),
|
|
19242
19839
|
args.styles[0] ?? "base",
|
|
19243
19840
|
generator,
|
|
19244
|
-
|
|
19841
|
+
path39.relative(path39.dirname(target), root) || ".",
|
|
19245
19842
|
assets
|
|
19246
19843
|
);
|
|
19247
19844
|
await writeManifestFile(target, manifest);
|
|
19248
|
-
log(` scanned ${assets.length} PNG(s) under ${
|
|
19845
|
+
log(` scanned ${assets.length} PNG(s) under ${path39.relative(process.cwd(), root)}`);
|
|
19249
19846
|
if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
|
|
19250
|
-
log(` wrote ${
|
|
19847
|
+
log(` wrote ${path39.relative(process.cwd(), target)}`);
|
|
19251
19848
|
log(`
|
|
19252
19849
|
Prompts are intentionally empty. To recover the real ones from your`);
|
|
19253
19850
|
log(` PixelLab account instead of inventing them:`);
|
|
19254
19851
|
log(`
|
|
19255
|
-
pixelkiln adopt --manifest ${
|
|
19852
|
+
pixelkiln adopt --manifest ${path39.relative(process.cwd(), target)} --write-prompts
|
|
19256
19853
|
`);
|
|
19257
19854
|
}
|
|
19258
19855
|
|
|
19259
19856
|
// src/pipeline/doctor.ts
|
|
19260
|
-
import { constants, existsSync as
|
|
19857
|
+
import { constants, existsSync as existsSync32 } from "fs";
|
|
19261
19858
|
import { access, stat as stat4 } from "fs/promises";
|
|
19262
|
-
import
|
|
19859
|
+
import path40 from "path";
|
|
19263
19860
|
async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
19264
19861
|
const checks = [];
|
|
19265
19862
|
const add = (id, level, message6) => checks.push({ id, level, message: message6 });
|
|
@@ -19270,8 +19867,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
19270
19867
|
);
|
|
19271
19868
|
add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
|
|
19272
19869
|
const dirs = [...new Set(specs.flatMap((spec) => [
|
|
19273
|
-
|
|
19274
|
-
...spec.quality ? [
|
|
19870
|
+
path40.dirname(spec.outFile),
|
|
19871
|
+
...spec.quality ? [path40.dirname(spec.quality.outFile)] : []
|
|
19275
19872
|
]))];
|
|
19276
19873
|
const unwritable = [];
|
|
19277
19874
|
for (const dir of dirs) {
|
|
@@ -19294,7 +19891,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
19294
19891
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
19295
19892
|
const output = entry.outputs[index];
|
|
19296
19893
|
const spec = specByKey.get(key);
|
|
19297
|
-
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path,
|
|
19894
|
+
const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path40.dirname(lockPath));
|
|
19298
19895
|
const owner = outputOwners.get(absolute);
|
|
19299
19896
|
if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
|
|
19300
19897
|
outputOwners.set(absolute, key);
|
|
@@ -19309,7 +19906,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
19309
19906
|
} else {
|
|
19310
19907
|
add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
|
|
19311
19908
|
}
|
|
19312
|
-
const cacheDir =
|
|
19909
|
+
const cacheDir = path40.join(path40.dirname(lockPath), ".pixelkiln", "cache");
|
|
19313
19910
|
const stranded = [];
|
|
19314
19911
|
const invalidState = [];
|
|
19315
19912
|
const staleMapUrls = [];
|
|
@@ -19325,11 +19922,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
19325
19922
|
const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
|
|
19326
19923
|
let hasCache = false;
|
|
19327
19924
|
for (const output of entry.outputs) {
|
|
19328
|
-
const cached =
|
|
19925
|
+
const cached = path40.join(
|
|
19329
19926
|
cacheDir,
|
|
19330
19927
|
cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
|
|
19331
19928
|
);
|
|
19332
|
-
if (
|
|
19929
|
+
if (existsSync32(cached) && await sha256File(cached) === output.sha256) {
|
|
19333
19930
|
hasCache = true;
|
|
19334
19931
|
break;
|
|
19335
19932
|
}
|
|
@@ -19425,16 +20022,16 @@ async function nearestExistingDirectory(start) {
|
|
|
19425
20022
|
if ((await stat4(current)).isDirectory()) return current;
|
|
19426
20023
|
} catch {
|
|
19427
20024
|
}
|
|
19428
|
-
const parent =
|
|
20025
|
+
const parent = path40.dirname(current);
|
|
19429
20026
|
if (parent === current) return current;
|
|
19430
20027
|
current = parent;
|
|
19431
20028
|
}
|
|
19432
20029
|
}
|
|
19433
20030
|
|
|
19434
20031
|
// src/pipeline/audit.ts
|
|
19435
|
-
import { readFile as
|
|
19436
|
-
import { existsSync as
|
|
19437
|
-
import
|
|
20032
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
20033
|
+
import { existsSync as existsSync33 } from "fs";
|
|
20034
|
+
import path41 from "path";
|
|
19438
20035
|
function paletteDistance(asset, reference) {
|
|
19439
20036
|
if (!asset.length || !reference.length) return 0;
|
|
19440
20037
|
let total = 0;
|
|
@@ -19470,12 +20067,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
19470
20067
|
const unreadable = [];
|
|
19471
20068
|
for (const spec of mine) {
|
|
19472
20069
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
19473
|
-
if (!
|
|
20070
|
+
if (!existsSync33(output.absolutePath)) {
|
|
19474
20071
|
missing.push(output.id);
|
|
19475
20072
|
continue;
|
|
19476
20073
|
}
|
|
19477
20074
|
try {
|
|
19478
|
-
const png = decodePng(await
|
|
20075
|
+
const png = decodePng(await readFile28(output.absolutePath));
|
|
19479
20076
|
const palette = extractPalette(png, 12);
|
|
19480
20077
|
assets.push({
|
|
19481
20078
|
assetId: spec.assetId,
|
|
@@ -19499,10 +20096,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
19499
20096
|
let referenceFromStyleImages = false;
|
|
19500
20097
|
const refPalettes = [];
|
|
19501
20098
|
for (const img of style.styleImages) {
|
|
19502
|
-
const abs =
|
|
19503
|
-
if (!
|
|
20099
|
+
const abs = path41.resolve(loaded.root, img.path);
|
|
20100
|
+
if (!existsSync33(abs)) continue;
|
|
19504
20101
|
try {
|
|
19505
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
20102
|
+
refPalettes.push(extractPalette(decodePng(await readFile28(abs)), 12));
|
|
19506
20103
|
} catch {
|
|
19507
20104
|
}
|
|
19508
20105
|
}
|
|
@@ -19730,24 +20327,24 @@ async function runAudit(args) {
|
|
|
19730
20327
|
}
|
|
19731
20328
|
|
|
19732
20329
|
// src/cli/commands/pack.ts
|
|
19733
|
-
import
|
|
19734
|
-
import { readFile as
|
|
20330
|
+
import path43 from "path";
|
|
20331
|
+
import { readFile as readFile29 } from "fs/promises";
|
|
19735
20332
|
|
|
19736
20333
|
// src/pipeline/pack.ts
|
|
19737
20334
|
import { readFileSync as readFileSync6 } from "fs";
|
|
19738
|
-
import
|
|
20335
|
+
import path42 from "path";
|
|
19739
20336
|
var DEFAULT_FRAME_SET_FPS = 12;
|
|
19740
20337
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
19741
20338
|
if (!Array.isArray(raw) || !raw.length) {
|
|
19742
20339
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
19743
20340
|
}
|
|
19744
|
-
const baseDir =
|
|
20341
|
+
const baseDir = path42.dirname(path42.resolve(inputsFilePath));
|
|
19745
20342
|
return raw.map((entry, i) => {
|
|
19746
20343
|
const e = entry;
|
|
19747
20344
|
if (typeof e.id !== "string" || typeof e.path !== "string") {
|
|
19748
20345
|
throw new Error(`--inputs[${i}] needs string "id" and "path"`);
|
|
19749
20346
|
}
|
|
19750
|
-
return { id: e.id, path:
|
|
20347
|
+
return { id: e.id, path: path42.resolve(baseDir, e.path) };
|
|
19751
20348
|
});
|
|
19752
20349
|
}
|
|
19753
20350
|
function packSprites(inputs, options = {}) {
|
|
@@ -19831,7 +20428,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19831
20428
|
}
|
|
19832
20429
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
19833
20430
|
id,
|
|
19834
|
-
path:
|
|
20431
|
+
path: path42.resolve(source)
|
|
19835
20432
|
}));
|
|
19836
20433
|
if (!inputs2.length) throw new Error(`No approved quality outputs for style "${styleId}".`);
|
|
19837
20434
|
const packed2 = packSprites(inputs2, options);
|
|
@@ -19855,7 +20452,7 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19855
20452
|
let outputs = resolveEntryOutputs(entry, id, manifestDir);
|
|
19856
20453
|
const source = sources[id];
|
|
19857
20454
|
if (source && outputs.length <= 1) {
|
|
19858
|
-
inputs.push({ id, path:
|
|
20455
|
+
inputs.push({ id, path: path42.resolve(manifestDir, source) });
|
|
19859
20456
|
continue;
|
|
19860
20457
|
}
|
|
19861
20458
|
if (source && sourceIsStem(source, entry, manifestDir)) {
|
|
@@ -19884,12 +20481,13 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
|
19884
20481
|
id,
|
|
19885
20482
|
kind: isFrames ? "frames" : "members",
|
|
19886
20483
|
...fps ? { fps } : {},
|
|
20484
|
+
...isFrames && frameSetLoop(entry) === false ? { loop: false } : {},
|
|
19887
20485
|
frames: selected.map((output) => output.id)
|
|
19888
20486
|
});
|
|
19889
20487
|
}
|
|
19890
20488
|
}
|
|
19891
20489
|
for (const [id, source] of Object.entries(sources)) {
|
|
19892
|
-
if (!locked.has(id)) inputs.push({ id, path:
|
|
20490
|
+
if (!locked.has(id)) inputs.push({ id, path: path42.resolve(manifestDir, source) });
|
|
19893
20491
|
}
|
|
19894
20492
|
if (!inputs.length) {
|
|
19895
20493
|
throw new Error(`No readable sprites for style "${styleId}"; ${noOutput.length} skipped.`);
|
|
@@ -19997,7 +20595,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
19997
20595
|
const source = sources[id];
|
|
19998
20596
|
const entry = lock.entries[`${styleId}/${id}`];
|
|
19999
20597
|
if (source && !(entry && sourceIsStem(source, entry, manifestDir))) {
|
|
20000
|
-
placements.push({ id, path:
|
|
20598
|
+
placements.push({ id, path: path42.resolve(manifestDir, source), cell });
|
|
20001
20599
|
continue;
|
|
20002
20600
|
}
|
|
20003
20601
|
if (source && entry) {
|
|
@@ -20030,7 +20628,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
20030
20628
|
`Nothing to mount for style "${styleId}": ${ids.length} asset(s) declare a cell, but none has generated output or a \`source\`. Run \`pixelkiln gen --style ${styleId}\`, or point each at a file.${detail}`
|
|
20031
20629
|
);
|
|
20032
20630
|
}
|
|
20033
|
-
const basePng = mount.base ? readFileSync6(
|
|
20631
|
+
const basePng = mount.base ? readFileSync6(path42.resolve(manifestDir, mount.base)) : void 0;
|
|
20034
20632
|
const mounted = mountSprites(placements, {
|
|
20035
20633
|
cellWidth: mount.cellWidth,
|
|
20036
20634
|
cellHeight: mount.cellHeight,
|
|
@@ -20040,7 +20638,7 @@ function mountStyle(lock, styleId, manifestDir, mount, cells, sources = {}, outp
|
|
|
20040
20638
|
...mounted,
|
|
20041
20639
|
sources: mount.base ? [{
|
|
20042
20640
|
id: "$base",
|
|
20043
|
-
path:
|
|
20641
|
+
path: path42.resolve(manifestDir, mount.base),
|
|
20044
20642
|
sha256: sha256(basePng),
|
|
20045
20643
|
included: true
|
|
20046
20644
|
}, ...mounted.sources] : mounted.sources,
|
|
@@ -20422,7 +21020,7 @@ function renderGodotSpriteFrames(atlas, opts) {
|
|
|
20422
21020
|
);
|
|
20423
21021
|
};
|
|
20424
21022
|
for (const set of atlas.sets ?? []) {
|
|
20425
|
-
if (set.kind === "frames") animation(set.id, set.frames, set.fps ?? GODOT_DEFAULT_SPEED, true);
|
|
21023
|
+
if (set.kind === "frames") animation(set.id, set.frames, set.fps ?? GODOT_DEFAULT_SPEED, set.loop ?? true);
|
|
20426
21024
|
else for (const id of set.frames) animation(id, [id], GODOT_DEFAULT_SPEED, false);
|
|
20427
21025
|
}
|
|
20428
21026
|
for (const frame of atlas.frames) {
|
|
@@ -20444,12 +21042,12 @@ async function runPack(args) {
|
|
|
20444
21042
|
if (args.primaryOnly || args.outputRoles.length) {
|
|
20445
21043
|
throw new Error("--primary-only and --output-role require manifest-driven pack");
|
|
20446
21044
|
}
|
|
20447
|
-
const raw = JSON.parse(await
|
|
21045
|
+
const raw = JSON.parse(await readFile29(path43.resolve(args.inputs), "utf8"));
|
|
20448
21046
|
const inputs = resolvePackInputs(raw, args.inputs);
|
|
20449
21047
|
const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
|
|
20450
21048
|
const format = sheetFormat(args);
|
|
20451
|
-
const base =
|
|
20452
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
21049
|
+
const base = path43.resolve(args.out.replace(/\.(?:png|json|tres)$/i, ""));
|
|
21050
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path43.basename(`${base}.png`) });
|
|
20453
21051
|
const outputs = [
|
|
20454
21052
|
{ path: `${base}.png`, data: png },
|
|
20455
21053
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -20462,7 +21060,7 @@ async function runPack(args) {
|
|
|
20462
21060
|
log(
|
|
20463
21061
|
` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s), ${(png.length / 1024).toFixed(1)} KB (${format})`
|
|
20464
21062
|
);
|
|
20465
|
-
log(` ${
|
|
21063
|
+
log(` ${path43.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
20466
21064
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20467
21065
|
return;
|
|
20468
21066
|
}
|
|
@@ -20470,7 +21068,7 @@ async function runPack(args) {
|
|
|
20470
21068
|
if (args.primaryOnly && args.outputRoles.length) {
|
|
20471
21069
|
throw new Error("pack accepts either --primary-only or --output-role, not both");
|
|
20472
21070
|
}
|
|
20473
|
-
const manifestDir =
|
|
21071
|
+
const manifestDir = path43.dirname(path43.resolve(args.manifest));
|
|
20474
21072
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
20475
21073
|
for (const styleId of styleIds) {
|
|
20476
21074
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -20485,9 +21083,9 @@ async function runPack(args) {
|
|
|
20485
21083
|
sourceOverrides: qualitySources,
|
|
20486
21084
|
sources: manifestSources(loaded.manifest, styleId)
|
|
20487
21085
|
});
|
|
20488
|
-
const base = args.out ?
|
|
21086
|
+
const base = args.out ? path43.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path43.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
|
|
20489
21087
|
const format = sheetFormat(args);
|
|
20490
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
21088
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path43.basename(`${base}.png`) });
|
|
20491
21089
|
const outputs = [
|
|
20492
21090
|
{ path: `${base}.png`, data: png },
|
|
20493
21091
|
{ path: `${base}${extension}`, data: document }
|
|
@@ -20519,13 +21117,13 @@ async function runPack(args) {
|
|
|
20519
21117
|
log(
|
|
20520
21118
|
` ${styleId}: ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)` + (atlas.sets?.length ? `, ${atlas.sets.length} set(s)` : "") + ` (${format})`
|
|
20521
21119
|
);
|
|
20522
|
-
log(` ${
|
|
21120
|
+
log(` ${path43.relative(process.cwd(), base)}.png + ${extension} + .pixelkiln.json`);
|
|
20523
21121
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20524
21122
|
}
|
|
20525
21123
|
}
|
|
20526
21124
|
async function runMount(args) {
|
|
20527
21125
|
const { loaded, lock } = await openProject2(args);
|
|
20528
|
-
const manifestDir =
|
|
21126
|
+
const manifestDir = path43.dirname(path43.resolve(args.manifest));
|
|
20529
21127
|
const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
|
|
20530
21128
|
for (const styleId of styleIds) {
|
|
20531
21129
|
const style = loaded.manifest.styles[styleId];
|
|
@@ -20564,9 +21162,9 @@ async function runMount(args) {
|
|
|
20564
21162
|
sources,
|
|
20565
21163
|
outputRoles
|
|
20566
21164
|
);
|
|
20567
|
-
const out =
|
|
21165
|
+
const out = path43.resolve(manifestDir, style.mount.out);
|
|
20568
21166
|
const format = sheetFormat(args);
|
|
20569
|
-
const { extension, document } = renderSheetDocument(format, atlas, { imageName:
|
|
21167
|
+
const { extension, document } = renderSheetDocument(format, atlas, { imageName: path43.basename(out) });
|
|
20570
21168
|
const metadata = out.replace(/\.png$/, "") + extension;
|
|
20571
21169
|
const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
|
|
20572
21170
|
const outputs = [
|
|
@@ -20586,7 +21184,7 @@ async function runMount(args) {
|
|
|
20586
21184
|
await provenanceFile("$lock", args.lock),
|
|
20587
21185
|
...qualityRecords,
|
|
20588
21186
|
...artifactSources.filter(
|
|
20589
|
-
(source) => source.id !== "$base" ||
|
|
21187
|
+
(source) => source.id !== "$base" || path43.resolve(source.path) !== out
|
|
20590
21188
|
)
|
|
20591
21189
|
],
|
|
20592
21190
|
options: {
|
|
@@ -20600,14 +21198,14 @@ async function runMount(args) {
|
|
|
20600
21198
|
log(
|
|
20601
21199
|
` ${styleId}: ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
|
|
20602
21200
|
);
|
|
20603
|
-
log(` ${
|
|
21201
|
+
log(` ${path43.relative(process.cwd(), out)} + atlas/provenance JSON`);
|
|
20604
21202
|
for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
|
|
20605
21203
|
}
|
|
20606
21204
|
}
|
|
20607
21205
|
async function runExport(args) {
|
|
20608
21206
|
const { loaded, specs, lock } = await openProject2(args);
|
|
20609
21207
|
const format = args.format ?? "generic";
|
|
20610
|
-
const manifestDir =
|
|
21208
|
+
const manifestDir = path43.dirname(path43.resolve(args.manifest));
|
|
20611
21209
|
const selected = specs.filter((spec) => {
|
|
20612
21210
|
if (spec.generator !== "tiles" && spec.generator !== "terrain") return false;
|
|
20613
21211
|
if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
|
|
@@ -20623,12 +21221,12 @@ async function runExport(args) {
|
|
|
20623
21221
|
for (const spec of selected) {
|
|
20624
21222
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
20625
21223
|
const style = loaded.manifest.styles[spec.styleId];
|
|
20626
|
-
const defaultBase =
|
|
20627
|
-
const base = args.out ?
|
|
21224
|
+
const defaultBase = path43.resolve(manifestDir, style.outDir, `${spec.assetId}-tileset`);
|
|
21225
|
+
const base = args.out ? path43.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
|
|
20628
21226
|
const result = exportTileset(entry, spec, {
|
|
20629
21227
|
format,
|
|
20630
21228
|
manifestDir,
|
|
20631
|
-
imageName:
|
|
21229
|
+
imageName: path43.basename(`${base}.png`),
|
|
20632
21230
|
columns: args.columns
|
|
20633
21231
|
});
|
|
20634
21232
|
const outputs = [
|
|
@@ -20646,7 +21244,7 @@ async function runExport(args) {
|
|
|
20646
21244
|
asset: spec.assetId,
|
|
20647
21245
|
columns: args.columns ?? null,
|
|
20648
21246
|
format,
|
|
20649
|
-
image:
|
|
21247
|
+
image: path43.basename(`${base}.png`),
|
|
20650
21248
|
providerRules: result.generic.providerRules,
|
|
20651
21249
|
style: spec.styleId,
|
|
20652
21250
|
tileType: spec.tileType ?? null
|
|
@@ -20656,7 +21254,7 @@ async function runExport(args) {
|
|
|
20656
21254
|
` ${spec.styleId}/${spec.assetId}: ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
|
|
20657
21255
|
);
|
|
20658
21256
|
log(
|
|
20659
|
-
` ${
|
|
21257
|
+
` ${path43.relative(process.cwd(), base)}.png + ${path43.basename(base)}${result.extension} + .pixelkiln.json`
|
|
20660
21258
|
);
|
|
20661
21259
|
}
|
|
20662
21260
|
}
|
|
@@ -20669,23 +21267,23 @@ function sheetFormat(args) {
|
|
|
20669
21267
|
}
|
|
20670
21268
|
|
|
20671
21269
|
// src/cli/commands/pixellab-utils.ts
|
|
20672
|
-
import
|
|
21270
|
+
import path45 from "path";
|
|
20673
21271
|
|
|
20674
21272
|
// src/pixellab-utils.ts
|
|
20675
|
-
import { existsSync as
|
|
20676
|
-
import { mkdir as
|
|
20677
|
-
import
|
|
21273
|
+
import { existsSync as existsSync34 } from "fs";
|
|
21274
|
+
import { mkdir as mkdir14, readFile as readFile30, writeFile as writeFile19 } from "fs/promises";
|
|
21275
|
+
import path44 from "path";
|
|
20678
21276
|
var UNZOOM_MIN_SIDE = 256;
|
|
20679
21277
|
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
20680
21278
|
function defaultUnzoomOut(input) {
|
|
20681
|
-
const ext =
|
|
21279
|
+
const ext = path44.extname(input);
|
|
20682
21280
|
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
20683
21281
|
}
|
|
20684
21282
|
async function unzoomFile(client2, input, opts = {}) {
|
|
20685
|
-
const out =
|
|
20686
|
-
if (
|
|
20687
|
-
if (
|
|
20688
|
-
const bytes = await
|
|
21283
|
+
const out = path44.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
21284
|
+
if (path44.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
21285
|
+
if (existsSync34(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
21286
|
+
const bytes = await readFile30(input);
|
|
20689
21287
|
const metadata = imageMetadata(bytes);
|
|
20690
21288
|
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
20691
21289
|
const { width, height, format } = metadata;
|
|
@@ -20698,22 +21296,22 @@ async function unzoomFile(client2, input, opts = {}) {
|
|
|
20698
21296
|
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
20699
21297
|
}
|
|
20700
21298
|
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
20701
|
-
await
|
|
20702
|
-
await
|
|
21299
|
+
await mkdir14(path44.dirname(out), { recursive: true });
|
|
21300
|
+
await writeFile19(out, res.png);
|
|
20703
21301
|
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
20704
21302
|
}
|
|
20705
21303
|
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
20706
21304
|
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
20707
21305
|
var FONT_COST_GENERATIONS = 15;
|
|
20708
21306
|
function fontOutputPaths(out) {
|
|
20709
|
-
const base =
|
|
21307
|
+
const base = path44.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
20710
21308
|
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
20711
21309
|
}
|
|
20712
21310
|
async function generateFont(client2, opts) {
|
|
20713
21311
|
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
20714
21312
|
if (!opts.force) {
|
|
20715
21313
|
for (const file of [ttf, atlas]) {
|
|
20716
|
-
if (
|
|
21314
|
+
if (existsSync34(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
20717
21315
|
}
|
|
20718
21316
|
}
|
|
20719
21317
|
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -20740,9 +21338,9 @@ async function generateFont(client2, opts) {
|
|
|
20740
21338
|
client2.download(job.download_ttf_url),
|
|
20741
21339
|
client2.download(job.download_atlas_url)
|
|
20742
21340
|
]);
|
|
20743
|
-
await
|
|
20744
|
-
await
|
|
20745
|
-
await
|
|
21341
|
+
await mkdir14(path44.dirname(ttf), { recursive: true });
|
|
21342
|
+
await writeFile19(ttf, ttfBytes);
|
|
21343
|
+
await writeFile19(atlas, atlasBytes);
|
|
20746
21344
|
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
20747
21345
|
}
|
|
20748
21346
|
if (Date.now() - started > timeout) {
|
|
@@ -20756,14 +21354,14 @@ async function generateFont(client2, opts) {
|
|
|
20756
21354
|
var PIXELATE_COST_GENERATIONS = 6;
|
|
20757
21355
|
var PIXELATE_MAX_SIDE = 2048;
|
|
20758
21356
|
function defaultPixelateOut(input) {
|
|
20759
|
-
const ext =
|
|
21357
|
+
const ext = path44.extname(input);
|
|
20760
21358
|
return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
|
|
20761
21359
|
}
|
|
20762
21360
|
async function pixelateFile(client2, input, opts = {}) {
|
|
20763
|
-
const out =
|
|
20764
|
-
if (
|
|
20765
|
-
if (
|
|
20766
|
-
const bytes = await
|
|
21361
|
+
const out = path44.resolve(opts.out ?? defaultPixelateOut(input));
|
|
21362
|
+
if (path44.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
|
|
21363
|
+
if (existsSync34(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
21364
|
+
const bytes = await readFile30(input);
|
|
20767
21365
|
const metadata = imageMetadata(bytes);
|
|
20768
21366
|
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
20769
21367
|
const { width, height, format } = metadata;
|
|
@@ -20789,8 +21387,8 @@ async function pixelateFile(client2, input, opts = {}) {
|
|
|
20789
21387
|
}
|
|
20790
21388
|
const png = Buffer.from(first.base64, "base64");
|
|
20791
21389
|
const size = imageMetadata(png);
|
|
20792
|
-
await
|
|
20793
|
-
await
|
|
21390
|
+
await mkdir14(path44.dirname(out), { recursive: true });
|
|
21391
|
+
await writeFile19(out, png);
|
|
20794
21392
|
return {
|
|
20795
21393
|
jobId,
|
|
20796
21394
|
out,
|
|
@@ -20811,7 +21409,7 @@ async function pixelateFile(client2, input, opts = {}) {
|
|
|
20811
21409
|
|
|
20812
21410
|
// src/cli/commands/pixellab-utils.ts
|
|
20813
21411
|
function loadKeys(args) {
|
|
20814
|
-
loadEnvFiles(
|
|
21412
|
+
loadEnvFiles(path45.dirname(path45.resolve(args.manifest)));
|
|
20815
21413
|
loadEnvFiles(process.cwd());
|
|
20816
21414
|
}
|
|
20817
21415
|
async function runUnzoom(args) {
|
|
@@ -20908,21 +21506,76 @@ async function runPixelate(args) {
|
|
|
20908
21506
|
` ${res.original.width}x${res.original.height}${scaled} \u2192 ${res.pixelated.width}x${res.pixelated.height}; wrote ${res.out}`
|
|
20909
21507
|
);
|
|
20910
21508
|
}
|
|
21509
|
+
async function runVfx(args) {
|
|
21510
|
+
if (args.subcommand === "presets" || args.subcommand === "parts") {
|
|
21511
|
+
if (args.dryRun) {
|
|
21512
|
+
log(` would list PixelLab's VFX ${args.subcommand}; nothing sent`);
|
|
21513
|
+
return;
|
|
21514
|
+
}
|
|
21515
|
+
loadKeys(args);
|
|
21516
|
+
log(await describeVfx(mcpClientFromEnv(), args.subcommand));
|
|
21517
|
+
return;
|
|
21518
|
+
}
|
|
21519
|
+
if (!args.out) throw new UsageError("vfx needs --out <base>: <base>.png, .gif, .json, and .vfx.json are written");
|
|
21520
|
+
const starts = [args.preset, args.recipe, args.effect].filter(Boolean);
|
|
21521
|
+
if (starts.length > 1) throw new UsageError("vfx takes one of --preset, --recipe, or --effect, not several");
|
|
21522
|
+
if (starts.length === 0 && args.parts.length === 0) {
|
|
21523
|
+
throw new UsageError("vfx needs something to start from: --preset <name>, --recipe <file>, --effect <id>, or --part <name>");
|
|
21524
|
+
}
|
|
21525
|
+
const paths = vfxOutputPaths(args.out);
|
|
21526
|
+
const fromRecipe = args.recipe ? await readVfxRecipe(args.recipe) : void 0;
|
|
21527
|
+
const sprite = args.sprite ?? fromRecipe?.sprite;
|
|
21528
|
+
const recipeName = typeof fromRecipe?.recipe.name === "string" ? fromRecipe.recipe.name : void 0;
|
|
21529
|
+
const name = args.name ?? (args.effect ? void 0 : recipeName ?? args.preset ?? path45.basename(paths.sheet, ".png"));
|
|
21530
|
+
const from = args.preset ? `preset ${args.preset}` : args.recipe ? `recipe ${args.recipe}` : args.effect ? `saved effect ${args.effect}` : "nothing";
|
|
21531
|
+
log(
|
|
21532
|
+
` vfx ${name ? `"${name}"` : "(keeping its saved name)"} from ${from}${args.parts.length ? ` + ${args.parts.join(", ")}` : ""}${sprite ? ` around ${sprite}` : ""} \u2192 ${paths.sheet} (+ .gif, .json, .vfx.json${args.gamePack ? ", -game.zip" : ""})`
|
|
21533
|
+
);
|
|
21534
|
+
log(" cost: free on a PixelLab subscription (a renderer, not a generation); saved to your My effects");
|
|
21535
|
+
if (args.dryRun) {
|
|
21536
|
+
log(" dry run: nothing sent");
|
|
21537
|
+
return;
|
|
21538
|
+
}
|
|
21539
|
+
if (fromRecipe?.sprite && !args.sprite) log(` using the sprite the record names: ${fromRecipe.sprite}`);
|
|
21540
|
+
loadKeys(args);
|
|
21541
|
+
const res = await makeVfx(mcpClientFromEnv(), {
|
|
21542
|
+
preset: args.preset,
|
|
21543
|
+
recipe: fromRecipe?.recipe,
|
|
21544
|
+
effect: args.effect,
|
|
21545
|
+
parts: args.parts.map((part) => ({ name: part })),
|
|
21546
|
+
sprite,
|
|
21547
|
+
fps: args.fps,
|
|
21548
|
+
name: args.name,
|
|
21549
|
+
out: args.out,
|
|
21550
|
+
gamePack: args.gamePack,
|
|
21551
|
+
force: args.force,
|
|
21552
|
+
onProgress: (message6) => log(` ${message6}\u2026`)
|
|
21553
|
+
});
|
|
21554
|
+
if (args.json) {
|
|
21555
|
+
log(JSON.stringify({ version: 1, ...res }, null, 2));
|
|
21556
|
+
return;
|
|
21557
|
+
}
|
|
21558
|
+
log(` ${res.width}x${res.height}, ${res.frames} frames @ ${res.fps} fps, ${res.loop ? "loops" : "plays once"}`);
|
|
21559
|
+
for (const file of [res.paths.sheet, res.paths.gif, res.paths.atlas, res.paths.gamePack, res.paths.provenance]) {
|
|
21560
|
+
if (file) log(` wrote ${file}`);
|
|
21561
|
+
}
|
|
21562
|
+
if (res.editUrl) log(` tweak it with sliders: ${res.editUrl} (then: pixelkiln vfx --effect ${res.effectId} --out ${args.out} --force)`);
|
|
21563
|
+
}
|
|
20911
21564
|
|
|
20912
21565
|
// src/cli/commands/quality.ts
|
|
20913
|
-
import
|
|
20914
|
-
import { readFile as
|
|
21566
|
+
import path47 from "path";
|
|
21567
|
+
import { readFile as readFile32 } from "fs/promises";
|
|
20915
21568
|
|
|
20916
21569
|
// src/pipeline/quality-regression.ts
|
|
20917
|
-
import
|
|
20918
|
-
import { existsSync as
|
|
20919
|
-
import { readFile as
|
|
21570
|
+
import path46 from "path";
|
|
21571
|
+
import { existsSync as existsSync35 } from "fs";
|
|
21572
|
+
import { readFile as readFile31 } from "fs/promises";
|
|
20920
21573
|
import { z as z13 } from "zod";
|
|
20921
21574
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
20922
21575
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
20923
21576
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
20924
21577
|
var QualityPathSchema = z13.string().min(1).refine(
|
|
20925
|
-
(value) => !
|
|
21578
|
+
(value) => !path46.posix.isAbsolute(value) && !value.includes("\\"),
|
|
20926
21579
|
"expected a portable relative path"
|
|
20927
21580
|
);
|
|
20928
21581
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -21069,10 +21722,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
21069
21722
|
};
|
|
21070
21723
|
}
|
|
21071
21724
|
async function measureImageQuality(file) {
|
|
21072
|
-
const absolute =
|
|
21725
|
+
const absolute = path46.resolve(file);
|
|
21073
21726
|
let bytes;
|
|
21074
21727
|
try {
|
|
21075
|
-
bytes = await
|
|
21728
|
+
bytes = await readFile31(absolute);
|
|
21076
21729
|
} catch (error) {
|
|
21077
21730
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
21078
21731
|
}
|
|
@@ -21091,11 +21744,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
21091
21744
|
}
|
|
21092
21745
|
const ids = parsed.data.map((entry) => entry.id);
|
|
21093
21746
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
21094
|
-
const root =
|
|
21747
|
+
const root = path46.dirname(path46.resolve(inputsFilePath));
|
|
21095
21748
|
return parsed.data.map((entry) => ({
|
|
21096
21749
|
id: entry.id,
|
|
21097
|
-
file:
|
|
21098
|
-
...entry.record ? { record:
|
|
21750
|
+
file: path46.resolve(root, entry.path),
|
|
21751
|
+
...entry.record ? { record: path46.resolve(root, entry.record) } : {},
|
|
21099
21752
|
tolerances: QualityToleranceSchema.parse({
|
|
21100
21753
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
21101
21754
|
...entry.tolerances
|
|
@@ -21103,7 +21756,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
21103
21756
|
}));
|
|
21104
21757
|
}
|
|
21105
21758
|
function portableRelative2(from, to) {
|
|
21106
|
-
return
|
|
21759
|
+
return path46.relative(from, path46.resolve(to)).split(path46.sep).join("/") || ".";
|
|
21107
21760
|
}
|
|
21108
21761
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
21109
21762
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -21118,20 +21771,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
21118
21771
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
21119
21772
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
21120
21773
|
}
|
|
21121
|
-
const outputs = record.outputs.map((output) =>
|
|
21122
|
-
if (!outputs.includes(
|
|
21774
|
+
const outputs = record.outputs.map((output) => path46.resolve(path46.dirname(recordPath), output.path));
|
|
21775
|
+
if (!outputs.includes(path46.resolve(imagePath))) {
|
|
21123
21776
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
21124
21777
|
}
|
|
21125
21778
|
}
|
|
21126
21779
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
21127
21780
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
21128
|
-
const absolute =
|
|
21781
|
+
const absolute = path46.resolve(baselinePath);
|
|
21129
21782
|
for (const input of inputs) {
|
|
21130
|
-
if (
|
|
21783
|
+
if (path46.resolve(input.file) === absolute || input.record && path46.resolve(input.record) === absolute) {
|
|
21131
21784
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
21132
21785
|
}
|
|
21133
21786
|
}
|
|
21134
|
-
const root =
|
|
21787
|
+
const root = path46.dirname(absolute);
|
|
21135
21788
|
const cases = [];
|
|
21136
21789
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
21137
21790
|
const expected = await measureImageQuality(input.file);
|
|
@@ -21158,8 +21811,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
21158
21811
|
cases
|
|
21159
21812
|
});
|
|
21160
21813
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
21161
|
-
if (
|
|
21162
|
-
const current = await
|
|
21814
|
+
if (existsSync35(absolute) && !options.force) {
|
|
21815
|
+
const current = await readFile31(absolute);
|
|
21163
21816
|
if (!current.equals(data)) {
|
|
21164
21817
|
throw new OverwriteRefusedError(
|
|
21165
21818
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -21171,10 +21824,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
21171
21824
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
21172
21825
|
}
|
|
21173
21826
|
async function readQualityBaseline(baselinePath) {
|
|
21174
|
-
const absolute =
|
|
21827
|
+
const absolute = path46.resolve(baselinePath);
|
|
21175
21828
|
let raw;
|
|
21176
21829
|
try {
|
|
21177
|
-
raw = JSON.parse(await
|
|
21830
|
+
raw = JSON.parse(await readFile31(absolute, "utf8"));
|
|
21178
21831
|
} catch (error) {
|
|
21179
21832
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
21180
21833
|
}
|
|
@@ -21244,12 +21897,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
21244
21897
|
return { violations, warnings };
|
|
21245
21898
|
}
|
|
21246
21899
|
async function checkQualityBaseline(baselinePath) {
|
|
21247
|
-
const absolute =
|
|
21900
|
+
const absolute = path46.resolve(baselinePath);
|
|
21248
21901
|
const baseline = await readQualityBaseline(absolute);
|
|
21249
|
-
const root =
|
|
21902
|
+
const root = path46.dirname(absolute);
|
|
21250
21903
|
const cases = [];
|
|
21251
21904
|
for (const entry of baseline.cases) {
|
|
21252
|
-
const file =
|
|
21905
|
+
const file = path46.resolve(root, entry.file);
|
|
21253
21906
|
let actual = null;
|
|
21254
21907
|
const violations = [];
|
|
21255
21908
|
const warnings = [];
|
|
@@ -21262,7 +21915,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
21262
21915
|
violations.push(message4(error));
|
|
21263
21916
|
}
|
|
21264
21917
|
if (entry.record) {
|
|
21265
|
-
const recordPath =
|
|
21918
|
+
const recordPath = path46.resolve(root, entry.record.path);
|
|
21266
21919
|
try {
|
|
21267
21920
|
const recordHash = await sha256File(recordPath);
|
|
21268
21921
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -21304,15 +21957,15 @@ async function runQuality(args) {
|
|
|
21304
21957
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
21305
21958
|
let raw;
|
|
21306
21959
|
try {
|
|
21307
|
-
raw = JSON.parse(await
|
|
21960
|
+
raw = JSON.parse(await readFile32(path47.resolve(args.inputs), "utf8"));
|
|
21308
21961
|
} catch (error) {
|
|
21309
21962
|
throw new Error(
|
|
21310
|
-
`Could not read quality inputs ${
|
|
21963
|
+
`Could not read quality inputs ${path47.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
21311
21964
|
{ cause: error }
|
|
21312
21965
|
);
|
|
21313
21966
|
}
|
|
21314
21967
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
21315
|
-
if (
|
|
21968
|
+
if (path47.resolve(args.inputs) === path47.resolve(args.out)) {
|
|
21316
21969
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
21317
21970
|
}
|
|
21318
21971
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -21325,7 +21978,7 @@ async function runQuality(args) {
|
|
|
21325
21978
|
}, null, 2));
|
|
21326
21979
|
} else {
|
|
21327
21980
|
log(
|
|
21328
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
21981
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path47.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
21329
21982
|
);
|
|
21330
21983
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
21331
21984
|
}
|
|
@@ -21351,12 +22004,12 @@ async function runQuality(args) {
|
|
|
21351
22004
|
}
|
|
21352
22005
|
|
|
21353
22006
|
// src/cli/commands/recipe.ts
|
|
21354
|
-
import
|
|
22007
|
+
import path49 from "path";
|
|
21355
22008
|
|
|
21356
22009
|
// src/recipes.ts
|
|
21357
|
-
import
|
|
21358
|
-
import { existsSync as
|
|
21359
|
-
import { readdir as readdir3, readFile as
|
|
22010
|
+
import path48 from "path";
|
|
22011
|
+
import { existsSync as existsSync36 } from "fs";
|
|
22012
|
+
import { readdir as readdir3, readFile as readFile33 } from "fs/promises";
|
|
21360
22013
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
21361
22014
|
import { z as z14 } from "zod";
|
|
21362
22015
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
@@ -21365,7 +22018,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
21365
22018
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
21366
22019
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
21367
22020
|
var PortablePathSchema = z14.string().min(1).refine((value) => {
|
|
21368
|
-
if (value.includes("\\") ||
|
|
22021
|
+
if (value.includes("\\") || path48.posix.isAbsolute(value)) return false;
|
|
21369
22022
|
const parts = value.split("/");
|
|
21370
22023
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
21371
22024
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -21541,10 +22194,10 @@ function message5(error) {
|
|
|
21541
22194
|
return error instanceof Error ? error.message : String(error);
|
|
21542
22195
|
}
|
|
21543
22196
|
async function readRecipeFile(recipePath, bundled) {
|
|
21544
|
-
const absolute =
|
|
22197
|
+
const absolute = path48.resolve(recipePath);
|
|
21545
22198
|
let raw;
|
|
21546
22199
|
try {
|
|
21547
|
-
raw = JSON.parse(await
|
|
22200
|
+
raw = JSON.parse(await readFile33(absolute, "utf8"));
|
|
21548
22201
|
} catch (error) {
|
|
21549
22202
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
21550
22203
|
}
|
|
@@ -21555,13 +22208,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
21555
22208
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
21556
22209
|
);
|
|
21557
22210
|
}
|
|
21558
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
22211
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path48.dirname(absolute), bundled };
|
|
21559
22212
|
}
|
|
21560
22213
|
async function walkRecipeFiles(root) {
|
|
21561
|
-
if (!
|
|
22214
|
+
if (!existsSync36(root)) return [];
|
|
21562
22215
|
const out = [];
|
|
21563
22216
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
21564
|
-
const resolved =
|
|
22217
|
+
const resolved = path48.join(root, entry.name);
|
|
21565
22218
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
21566
22219
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
21567
22220
|
}
|
|
@@ -21572,7 +22225,7 @@ function bundledRecipeRoot() {
|
|
|
21572
22225
|
}
|
|
21573
22226
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
21574
22227
|
const recipes = await Promise.all(
|
|
21575
|
-
(await walkRecipeFiles(
|
|
22228
|
+
(await walkRecipeFiles(path48.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
21576
22229
|
);
|
|
21577
22230
|
return recipes.sort(
|
|
21578
22231
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -21595,9 +22248,9 @@ function parseSelector(selector) {
|
|
|
21595
22248
|
return { id: match[1], version: match[2] };
|
|
21596
22249
|
}
|
|
21597
22250
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
21598
|
-
const local =
|
|
21599
|
-
if (
|
|
21600
|
-
const recipePath =
|
|
22251
|
+
const local = path48.resolve(target);
|
|
22252
|
+
if (existsSync36(local)) {
|
|
22253
|
+
const recipePath = path48.basename(local) === RECIPE_FILE ? local : path48.join(local, RECIPE_FILE);
|
|
21601
22254
|
return readRecipeFile(recipePath, false);
|
|
21602
22255
|
}
|
|
21603
22256
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -21611,8 +22264,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
21611
22264
|
return matches[0];
|
|
21612
22265
|
}
|
|
21613
22266
|
async function verifyFile(root, file) {
|
|
21614
|
-
const absolute =
|
|
21615
|
-
if (!
|
|
22267
|
+
const absolute = path48.resolve(root, ...file.path.split("/"));
|
|
22268
|
+
if (!existsSync36(absolute)) {
|
|
21616
22269
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
21617
22270
|
}
|
|
21618
22271
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -21627,7 +22280,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
21627
22280
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
21628
22281
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
21629
22282
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
21630
|
-
const modelRoot = options.modelRoot ?
|
|
22283
|
+
const modelRoot = options.modelRoot ? path48.resolve(options.modelRoot) : null;
|
|
21631
22284
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
21632
22285
|
...await verifyFile(modelRoot, model),
|
|
21633
22286
|
source: model.source,
|
|
@@ -21661,7 +22314,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
21661
22314
|
};
|
|
21662
22315
|
}
|
|
21663
22316
|
function renderStyle(style, destination, cwd) {
|
|
21664
|
-
const portableDir =
|
|
22317
|
+
const portableDir = path48.relative(cwd, destination).split(path48.sep).join("/") || ".";
|
|
21665
22318
|
const replace = (value) => {
|
|
21666
22319
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
21667
22320
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -21673,25 +22326,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
21673
22326
|
return replace(style);
|
|
21674
22327
|
}
|
|
21675
22328
|
async function installRecipe(target, options = {}) {
|
|
21676
|
-
const cwd =
|
|
22329
|
+
const cwd = path48.resolve(options.cwd ?? process.cwd());
|
|
21677
22330
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
21678
22331
|
const verification = await verifyRecipe(loaded.path);
|
|
21679
22332
|
if (!verification.ok) {
|
|
21680
22333
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
21681
22334
|
}
|
|
21682
|
-
const destination = options.out ?
|
|
22335
|
+
const destination = options.out ? path48.resolve(cwd, options.out) : path48.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
21683
22336
|
const sources = [
|
|
21684
22337
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
21685
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
22338
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path48.join(loaded.dir, ...file.path.split("/")) }))
|
|
21686
22339
|
];
|
|
21687
22340
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
21688
|
-
path:
|
|
21689
|
-
data: await
|
|
22341
|
+
path: path48.join(destination, ...file.path.split("/")),
|
|
22342
|
+
data: await readFile33(file.source)
|
|
21690
22343
|
})));
|
|
21691
22344
|
if (!options.force) {
|
|
21692
22345
|
for (const file of files) {
|
|
21693
|
-
if (!
|
|
21694
|
-
const current = await
|
|
22346
|
+
if (!existsSync36(file.path)) continue;
|
|
22347
|
+
const current = await readFile33(file.path);
|
|
21695
22348
|
if (!current.equals(file.data)) {
|
|
21696
22349
|
throw new OverwriteRefusedError(
|
|
21697
22350
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -21785,14 +22438,14 @@ async function runRecipe(args) {
|
|
|
21785
22438
|
}, null, 2));
|
|
21786
22439
|
} else {
|
|
21787
22440
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
21788
|
-
log(` ${
|
|
22441
|
+
log(` ${path49.relative(process.cwd(), result.destination) || "."}`);
|
|
21789
22442
|
log(`
|
|
21790
22443
|
Add this entry under your manifest's styles object:`);
|
|
21791
22444
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
21792
22445
|
if (result.recipe.models.length) {
|
|
21793
22446
|
log(`
|
|
21794
22447
|
Models are not downloaded automatically. Verify them with:`);
|
|
21795
|
-
log(` pixelkiln recipe verify ${
|
|
22448
|
+
log(` pixelkiln recipe verify ${path49.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
21796
22449
|
}
|
|
21797
22450
|
}
|
|
21798
22451
|
return;
|
|
@@ -21800,7 +22453,7 @@ async function runRecipe(args) {
|
|
|
21800
22453
|
}
|
|
21801
22454
|
|
|
21802
22455
|
// src/cli/commands/refine.ts
|
|
21803
|
-
import
|
|
22456
|
+
import path50 from "path";
|
|
21804
22457
|
async function runRefine(args) {
|
|
21805
22458
|
if (args.from || args.subcommand === "approve") {
|
|
21806
22459
|
if (!args.from) {
|
|
@@ -21830,12 +22483,12 @@ async function runRefine(args) {
|
|
|
21830
22483
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
21831
22484
|
);
|
|
21832
22485
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
21833
|
-
log(` wrote ${
|
|
21834
|
-
log(` quality record: ${
|
|
22486
|
+
log(` wrote ${path50.relative(process.cwd(), result3.output)}`);
|
|
22487
|
+
log(` quality record: ${path50.relative(process.cwd(), result3.record)}`);
|
|
21835
22488
|
log(`
|
|
21836
22489
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
21837
22490
|
log(
|
|
21838
|
-
` pixelkiln refine approve --from ${
|
|
22491
|
+
` pixelkiln refine approve --from ${path50.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
21839
22492
|
);
|
|
21840
22493
|
}
|
|
21841
22494
|
return;
|
|
@@ -21863,7 +22516,7 @@ async function runRefine(args) {
|
|
|
21863
22516
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
21864
22517
|
else {
|
|
21865
22518
|
log(
|
|
21866
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
22519
|
+
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path50.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path50.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
|
|
21867
22520
|
);
|
|
21868
22521
|
}
|
|
21869
22522
|
return;
|
|
@@ -21872,7 +22525,7 @@ async function runRefine(args) {
|
|
|
21872
22525
|
if (args.json) {
|
|
21873
22526
|
log(JSON.stringify(result2, null, 2));
|
|
21874
22527
|
} else {
|
|
21875
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
22528
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path50.relative(process.cwd(), result2.output || result2.record)}`);
|
|
21876
22529
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
21877
22530
|
}
|
|
21878
22531
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -21918,7 +22571,7 @@ async function runRefine(args) {
|
|
|
21918
22571
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
21919
22572
|
if (item.state === "needs-approval") {
|
|
21920
22573
|
log(
|
|
21921
|
-
` pixelkiln refine approve --from ${
|
|
22574
|
+
` pixelkiln refine approve --from ${path50.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
21922
22575
|
);
|
|
21923
22576
|
}
|
|
21924
22577
|
}
|
|
@@ -21929,9 +22582,9 @@ async function runRefine(args) {
|
|
|
21929
22582
|
}
|
|
21930
22583
|
|
|
21931
22584
|
// src/cli/commands/skeleton.ts
|
|
21932
|
-
import
|
|
21933
|
-
import { existsSync as
|
|
21934
|
-
import { readFile as
|
|
22585
|
+
import path51 from "path";
|
|
22586
|
+
import { existsSync as existsSync37 } from "fs";
|
|
22587
|
+
import { readFile as readFile34, writeFile as writeFile20 } from "fs/promises";
|
|
21935
22588
|
|
|
21936
22589
|
// src/skeleton-preview.ts
|
|
21937
22590
|
var RIGHT = [255, 140, 40, 255];
|
|
@@ -22042,12 +22695,12 @@ var Canvas = class {
|
|
|
22042
22695
|
async function runEstimateSkeleton(args) {
|
|
22043
22696
|
const image = args.target;
|
|
22044
22697
|
if (!image) throw new UsageError("estimate-skeleton needs an image path: pixelkiln estimate-skeleton <image> [--out keypoints.json]");
|
|
22045
|
-
if (args.out &&
|
|
22698
|
+
if (args.out && existsSync37(path51.resolve(args.out)) && !args.force) {
|
|
22046
22699
|
throw new UsageError(`${args.out} already exists; pass --force to replace it`);
|
|
22047
22700
|
}
|
|
22048
|
-
loadEnvFiles(
|
|
22701
|
+
loadEnvFiles(path51.dirname(path51.resolve(args.manifest)));
|
|
22049
22702
|
loadEnvFiles(process.cwd());
|
|
22050
|
-
const bytes = await
|
|
22703
|
+
const bytes = await readFile34(path51.resolve(image));
|
|
22051
22704
|
const metadata = imageMetadata(bytes);
|
|
22052
22705
|
if (!metadata) throw new UsageError(`${image} is not a readable PNG or JPEG`);
|
|
22053
22706
|
const sizeProblem = estimateSkeletonSizeProblem(metadata.width, metadata.height);
|
|
@@ -22059,7 +22712,7 @@ async function runEstimateSkeleton(args) {
|
|
|
22059
22712
|
const frames = args.frames ?? DEFAULT_SCAFFOLD_FRAMES;
|
|
22060
22713
|
const json = JSON.stringify(scaffoldSkeletonSet(res.keypoints, frames), null, 2);
|
|
22061
22714
|
if (args.out) {
|
|
22062
|
-
await
|
|
22715
|
+
await writeFile20(path51.resolve(args.out), json + "\n");
|
|
22063
22716
|
log(` wrote ${args.out}: the estimated pose and ${frames} frames to edit`);
|
|
22064
22717
|
log(` preview it with: pixelkiln skeleton-preview ${args.out} --from ${image}`);
|
|
22065
22718
|
} else {
|
|
@@ -22072,8 +22725,8 @@ async function runSkeletonPreview(args) {
|
|
|
22072
22725
|
let sourceFile;
|
|
22073
22726
|
let label;
|
|
22074
22727
|
if (target.endsWith(".json")) {
|
|
22075
|
-
keypointsFile =
|
|
22076
|
-
sourceFile = args.from ?
|
|
22728
|
+
keypointsFile = path51.resolve(target);
|
|
22729
|
+
sourceFile = args.from ? path51.resolve(args.from) : void 0;
|
|
22077
22730
|
label = target;
|
|
22078
22731
|
} else {
|
|
22079
22732
|
const loaded = await loadManifest(args.manifest);
|
|
@@ -22085,31 +22738,31 @@ async function runSkeletonPreview(args) {
|
|
|
22085
22738
|
);
|
|
22086
22739
|
}
|
|
22087
22740
|
keypointsFile = spec.revision.keypointsFile;
|
|
22088
|
-
sourceFile = args.from ?
|
|
22741
|
+
sourceFile = args.from ? path51.resolve(args.from) : spec.revision.sourceFile;
|
|
22089
22742
|
label = `${spec.styleId}/${spec.assetId}`;
|
|
22090
22743
|
}
|
|
22091
|
-
if (!
|
|
22744
|
+
if (!existsSync37(keypointsFile)) throw new UsageError(`Keypoints file does not exist yet: ${keypointsFile}`);
|
|
22092
22745
|
let json;
|
|
22093
22746
|
try {
|
|
22094
|
-
json = JSON.parse(await
|
|
22747
|
+
json = JSON.parse(await readFile34(keypointsFile, "utf8"));
|
|
22095
22748
|
} catch (error) {
|
|
22096
22749
|
throw new UsageError(`${keypointsFile} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
22097
22750
|
}
|
|
22098
22751
|
const set = parseSkeletonSet(json, keypointsFile);
|
|
22099
22752
|
let source;
|
|
22100
22753
|
if (sourceFile) {
|
|
22101
|
-
if (!
|
|
22754
|
+
if (!existsSync37(sourceFile)) {
|
|
22102
22755
|
log(` source image does not exist yet, drawing on a plain ground: ${sourceFile}`);
|
|
22103
22756
|
} else {
|
|
22104
|
-
const bytes = await
|
|
22757
|
+
const bytes = await readFile34(sourceFile);
|
|
22105
22758
|
if (imageMetadata(bytes)?.format !== "png") throw new UsageError(`${sourceFile} must be a PNG to preview over`);
|
|
22106
22759
|
source = decodePng(bytes);
|
|
22107
22760
|
}
|
|
22108
22761
|
}
|
|
22109
|
-
const out =
|
|
22110
|
-
await
|
|
22762
|
+
const out = path51.resolve(args.out ?? keypointsFile.replace(/\.json$/, "") + ".preview.png");
|
|
22763
|
+
await writeFile20(out, renderSkeletonSheet(set, { source }));
|
|
22111
22764
|
log(` ${label}: ${set.frames.length} frames after the starting pose`);
|
|
22112
|
-
const shown =
|
|
22765
|
+
const shown = path51.relative(process.cwd(), out);
|
|
22113
22766
|
log(` wrote ${shown.startsWith("..") ? out : shown}`);
|
|
22114
22767
|
}
|
|
22115
22768
|
|
|
@@ -22155,15 +22808,15 @@ async function runTools(args) {
|
|
|
22155
22808
|
}
|
|
22156
22809
|
|
|
22157
22810
|
// src/cli/commands/workspace.ts
|
|
22158
|
-
import
|
|
22159
|
-
import { existsSync as
|
|
22811
|
+
import path52 from "path";
|
|
22812
|
+
import { existsSync as existsSync38 } from "fs";
|
|
22160
22813
|
async function runWorkspace(args) {
|
|
22161
|
-
const workspacePath =
|
|
22162
|
-
const dir =
|
|
22814
|
+
const workspacePath = path52.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
22815
|
+
const dir = path52.dirname(workspacePath);
|
|
22163
22816
|
if (args.subcommand === "add") {
|
|
22164
|
-
const manifestPath =
|
|
22817
|
+
const manifestPath = path52.resolve(args.target);
|
|
22165
22818
|
const loadedTarget = await loadManifest(manifestPath);
|
|
22166
|
-
const lockPath = args.explicitLock ?
|
|
22819
|
+
const lockPath = args.explicitLock ? path52.resolve(args.explicitLock) : path52.join(path52.dirname(manifestPath), "pixelkiln.lock.json");
|
|
22167
22820
|
const ws = await loadWorkspace(workspacePath);
|
|
22168
22821
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
22169
22822
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -22183,10 +22836,10 @@ async function runWorkspace(args) {
|
|
|
22183
22836
|
...args.account ? { account: args.account } : {}
|
|
22184
22837
|
};
|
|
22185
22838
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
22186
|
-
log(` registered "${id}" in ${
|
|
22839
|
+
log(` registered "${id}" in ${path52.relative(process.cwd(), workspacePath)}`);
|
|
22187
22840
|
log(` manifest: ${project.manifest}`);
|
|
22188
22841
|
log(` lock: ${project.lock}`);
|
|
22189
|
-
if (!
|
|
22842
|
+
if (!existsSync38(lockPath)) {
|
|
22190
22843
|
log(
|
|
22191
22844
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
22192
22845
|
);
|
|
@@ -22195,7 +22848,7 @@ async function runWorkspace(args) {
|
|
|
22195
22848
|
}
|
|
22196
22849
|
if (args.subcommand === "remove") {
|
|
22197
22850
|
const ws = await loadWorkspace(workspacePath);
|
|
22198
|
-
const resolvedTarget =
|
|
22851
|
+
const resolvedTarget = path52.resolve(args.target);
|
|
22199
22852
|
const match = ws.projects.find(
|
|
22200
22853
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
22201
22854
|
);
|
|
@@ -22206,11 +22859,11 @@ async function runWorkspace(args) {
|
|
|
22206
22859
|
version: 1,
|
|
22207
22860
|
projects: ws.projects.filter((p) => p !== match)
|
|
22208
22861
|
});
|
|
22209
|
-
log(` removed "${match.id}" from ${
|
|
22862
|
+
log(` removed "${match.id}" from ${path52.relative(process.cwd(), workspacePath)}`);
|
|
22210
22863
|
return;
|
|
22211
22864
|
}
|
|
22212
22865
|
if (args.subcommand === "list") {
|
|
22213
|
-
if (!
|
|
22866
|
+
if (!existsSync38(workspacePath)) {
|
|
22214
22867
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
22215
22868
|
}
|
|
22216
22869
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -22218,9 +22871,9 @@ async function runWorkspace(args) {
|
|
|
22218
22871
|
if (args.json) {
|
|
22219
22872
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
22220
22873
|
} else if (!ws.projects.length) {
|
|
22221
|
-
log(` no projects registered in ${
|
|
22874
|
+
log(` no projects registered in ${path52.relative(process.cwd(), workspacePath)}`);
|
|
22222
22875
|
} else {
|
|
22223
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
22876
|
+
log(` ${ws.projects.length} project(s) in ${path52.relative(process.cwd(), workspacePath)}:`);
|
|
22224
22877
|
for (const p of ws.projects) {
|
|
22225
22878
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
22226
22879
|
}
|
|
@@ -22230,7 +22883,7 @@ async function runWorkspace(args) {
|
|
|
22230
22883
|
return;
|
|
22231
22884
|
}
|
|
22232
22885
|
if (args.subcommand === "status") {
|
|
22233
|
-
if (!
|
|
22886
|
+
if (!existsSync38(workspacePath)) {
|
|
22234
22887
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
22235
22888
|
}
|
|
22236
22889
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -22238,7 +22891,7 @@ async function runWorkspace(args) {
|
|
|
22238
22891
|
if (args.json) {
|
|
22239
22892
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
22240
22893
|
} else {
|
|
22241
|
-
log(` workspace: ${
|
|
22894
|
+
log(` workspace: ${path52.relative(process.cwd(), workspacePath)}`);
|
|
22242
22895
|
for (const p of report.projects) {
|
|
22243
22896
|
if (p.error) {
|
|
22244
22897
|
log(`
|
|
@@ -22292,8 +22945,8 @@ async function runHelp() {
|
|
|
22292
22945
|
async function runVersion() {
|
|
22293
22946
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
22294
22947
|
const url = new URL(up, import.meta.url);
|
|
22295
|
-
if (!
|
|
22296
|
-
const pkg = JSON.parse(await
|
|
22948
|
+
if (!existsSync39(url)) continue;
|
|
22949
|
+
const pkg = JSON.parse(await readFile35(url, "utf8"));
|
|
22297
22950
|
if (pkg.name === "pixelkiln") {
|
|
22298
22951
|
log(`${pkg.name} ${pkg.version}`);
|
|
22299
22952
|
return;
|
|
@@ -22341,6 +22994,7 @@ var RUNNERS = {
|
|
|
22341
22994
|
"skeleton-preview": runSkeletonPreview,
|
|
22342
22995
|
unzoom: runUnzoom,
|
|
22343
22996
|
pixelate: runPixelate,
|
|
22997
|
+
vfx: runVfx,
|
|
22344
22998
|
font: runFont
|
|
22345
22999
|
};
|
|
22346
23000
|
async function main(argv = process.argv.slice(2)) {
|