pixelkiln 0.88.0 → 0.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +2017 -782
- package/dist/cli.js.map +1 -1
- package/dist/client/gallery.js +18 -7
- package/dist/index.cjs +1740 -588
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2239 -359
- package/dist/index.d.ts +2239 -359
- package/dist/index.js +1629 -503
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +77 -5
- package/docs/ENDPOINTS.md +132 -9
- package/docs/GENERATORS.md +88 -6
- package/docs/MANIFEST.md +27 -6
- package/docs/PIXELLAB.md +93 -7
- package/docs/REVISIONS.md +70 -5
- package/docs/TILES.md +30 -6
- package/package.json +1 -1
- package/schema/manifest.schema.json +147 -1
- package/schema/recipe.schema.json +39 -0
- package/skills/pixelkiln/SKILL.md +1 -1
- package/skills/pixelkiln/references/pixellab-roadmap.md +138 -12
- package/skills/pixelkiln/references/pixellab.md +117 -26
- package/skills/pixelkiln/references/revisions.md +14 -2
package/dist/index.js
CHANGED
|
@@ -14,8 +14,10 @@ var RevisionModeSchema = z.enum([
|
|
|
14
14
|
"animate-pixminimax",
|
|
15
15
|
"animate-skeleton",
|
|
16
16
|
"interpolate",
|
|
17
|
-
"edit-animation"
|
|
17
|
+
"edit-animation",
|
|
18
|
+
"normal-map"
|
|
18
19
|
]);
|
|
20
|
+
var NormalMapConventionSchema = z.enum(["opengl", "directx"]);
|
|
19
21
|
function tileVariationCount(descriptions) {
|
|
20
22
|
return Math.max(1, descriptions) * 4;
|
|
21
23
|
}
|
|
@@ -58,9 +60,16 @@ function tilesCost(tileSize, variations) {
|
|
|
58
60
|
function isometricTileCost() {
|
|
59
61
|
return 1;
|
|
60
62
|
}
|
|
61
|
-
function terrainTileCount(transitionSize) {
|
|
63
|
+
function terrainTileCount(transitionSize, mode2, orientation) {
|
|
64
|
+
if (orientation === "sidescroller") return 16;
|
|
65
|
+
if (mode2 === "pro") return transitionSize != null && transitionSize > 0.4 ? 25 : 16;
|
|
62
66
|
return transitionSize === 1 ? 25 : 16;
|
|
63
67
|
}
|
|
68
|
+
function terrainCost(mode2, tileSize, orientation) {
|
|
69
|
+
if (orientation === "sidescroller") return 3;
|
|
70
|
+
if (mode2 === "pro") return tileSize <= 32 ? 15 : 25;
|
|
71
|
+
return 4;
|
|
72
|
+
}
|
|
64
73
|
function parseTerrainDescriptions(prompt) {
|
|
65
74
|
const parts = prompt.split(/\d+\s*\)\s*\./).map((part) => part.trim()).filter(Boolean);
|
|
66
75
|
if (parts.length < 2) return null;
|
|
@@ -196,6 +205,17 @@ var CharacterStateSchema = z.object({
|
|
|
196
205
|
height: z.number().int().min(16).max(256)
|
|
197
206
|
}).strict().optional()
|
|
198
207
|
}).strict();
|
|
208
|
+
var TerrainSideSchema = z.enum(["lower", "upper"]);
|
|
209
|
+
var TerrainLinkSchema = z.object({
|
|
210
|
+
/** A terrain asset in the same style. */
|
|
211
|
+
of: z.string().min(1),
|
|
212
|
+
/** Which of that set's terrains: its `lower` or its `upper`. */
|
|
213
|
+
terrain: TerrainSideSchema
|
|
214
|
+
}).strict();
|
|
215
|
+
var TerrainConnectSchema = z.object({
|
|
216
|
+
lower: TerrainLinkSchema.optional(),
|
|
217
|
+
upper: TerrainLinkSchema.optional()
|
|
218
|
+
}).strict().refine((connect) => connect.lower || connect.upper, { message: "connect needs lower, upper, or both" });
|
|
199
219
|
var CharacterPortraitSizeSchema = z.union([
|
|
200
220
|
z.literal(16),
|
|
201
221
|
z.literal(32),
|
|
@@ -226,13 +246,38 @@ var AssetVfxSchema = z.object({
|
|
|
226
246
|
parts: z.array(VfxPartSchema).optional(),
|
|
227
247
|
sprite: z.string().min(1).optional(),
|
|
228
248
|
fps: z.number().int().min(1).max(60).optional(),
|
|
229
|
-
name: z.string().min(1).optional()
|
|
249
|
+
name: z.string().min(1).optional(),
|
|
250
|
+
description: z.string().trim().min(1).max(8e3).optional(),
|
|
251
|
+
artist: z.enum(["lite", "pro"]).optional()
|
|
230
252
|
}).strict().superRefine((vfx, context) => {
|
|
253
|
+
if (vfx.description !== void 0) {
|
|
254
|
+
for (const key of ["preset", "recipe", "parts", "fps"]) {
|
|
255
|
+
if (vfx[key] !== void 0) {
|
|
256
|
+
context.addIssue({
|
|
257
|
+
code: z.ZodIssueCode.custom,
|
|
258
|
+
message: `a vfx description goes to PixelLab's VFX artist, which takes no ${key}; use a description or the workbench's preset/recipe/parts/fps, not both`,
|
|
259
|
+
path: [key]
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (vfx.sprite && /^(character|object|gallery):/.test(vfx.sprite)) {
|
|
264
|
+
context.addIssue({
|
|
265
|
+
code: z.ZodIssueCode.custom,
|
|
266
|
+
message: "the VFX artist takes a sprite's pixels, not a PixelLab reference; point sprite at a PNG file (another asset's output works)",
|
|
267
|
+
path: ["sprite"]
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (vfx.name && vfx.name.length > 80) {
|
|
271
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "the VFX artist takes a name of at most 80 characters", path: ["name"] });
|
|
272
|
+
}
|
|
273
|
+
} else if (vfx.artist) {
|
|
274
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "artist picks who draws a vfx description; add a description", path: ["artist"] });
|
|
275
|
+
}
|
|
231
276
|
if (vfx.preset && vfx.recipe) {
|
|
232
277
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "start a vfx from a preset or a recipe, not both", path: ["recipe"] });
|
|
233
278
|
}
|
|
234
|
-
if (!vfx.preset && !vfx.recipe && !vfx.parts?.length) {
|
|
235
|
-
context.addIssue({ code: z.ZodIssueCode.custom, message: "a vfx needs a preset, a recipe, or at least one part", path: ["preset"] });
|
|
279
|
+
if (vfx.description === void 0 && !vfx.preset && !vfx.recipe && !vfx.parts?.length) {
|
|
280
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "a vfx needs a preset, a recipe, or at least one part, or a description", path: ["preset"] });
|
|
236
281
|
}
|
|
237
282
|
if (vfx.sprite && /^(https?|data):/i.test(vfx.sprite)) {
|
|
238
283
|
context.addIssue({
|
|
@@ -417,6 +462,8 @@ var RevisionSchema = z.object({
|
|
|
417
462
|
skeletonTemplate: z.string().min(1).optional(),
|
|
418
463
|
/** `remove-background` only: `simple` is faster and suits flat backgrounds; `complex` handles detailed edges. PixelLab defaults to `simple`. */
|
|
419
464
|
removalTask: z.enum(["simple", "complex"]).optional(),
|
|
465
|
+
/** `normal-map` only: `opengl` (green up; PixelLab's default) or `directx` (green down, for Unreal). */
|
|
466
|
+
convention: NormalMapConventionSchema.optional(),
|
|
420
467
|
/**
|
|
421
468
|
* `animate-skeleton`: what the character *looks like*, a noun
|
|
422
469
|
* phrase (colours, clothing, held items) — distinct from the asset's
|
|
@@ -563,13 +610,20 @@ var RevisionSchema = z.object({
|
|
|
563
610
|
path: ["removalTask"]
|
|
564
611
|
});
|
|
565
612
|
}
|
|
566
|
-
if (revision.strength !== void 0 && revision.mode === "remove-background") {
|
|
613
|
+
if (revision.strength !== void 0 && (revision.mode === "remove-background" || revision.mode === "normal-map")) {
|
|
567
614
|
context.addIssue({
|
|
568
615
|
code: z.ZodIssueCode.custom,
|
|
569
|
-
message:
|
|
616
|
+
message: `${revision.mode} revisions do not take a strength`,
|
|
570
617
|
path: ["strength"]
|
|
571
618
|
});
|
|
572
619
|
}
|
|
620
|
+
if (revision.convention !== void 0 && revision.mode !== "normal-map") {
|
|
621
|
+
context.addIssue({
|
|
622
|
+
code: z.ZodIssueCode.custom,
|
|
623
|
+
message: "convention applies to normal-map revisions only",
|
|
624
|
+
path: ["convention"]
|
|
625
|
+
});
|
|
626
|
+
}
|
|
573
627
|
if (revision.description !== void 0 && revision.mode !== "animate-skeleton" && revision.mode !== "remove-background") {
|
|
574
628
|
context.addIssue({
|
|
575
629
|
code: z.ZodIssueCode.custom,
|
|
@@ -588,7 +642,7 @@ var RevisionSchema = z.object({
|
|
|
588
642
|
}
|
|
589
643
|
});
|
|
590
644
|
var FRAME_SET_REVISION_MODES = ["animate", "animate-pixminimax", "animate-skeleton", "interpolate", "edit-animation"];
|
|
591
|
-
var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-animation"];
|
|
645
|
+
var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-animation", "normal-map"];
|
|
592
646
|
var StyleObjectSchema = z.object({
|
|
593
647
|
/** Generation backend for this style. Omit to inherit the manifest default. */
|
|
594
648
|
provider: z.string().min(1).optional(),
|
|
@@ -701,9 +755,10 @@ var StyleObjectSchema = z.object({
|
|
|
701
755
|
terrainTileSize: z.union([z.literal(16), z.literal(32), z.literal(64)]).optional(),
|
|
702
756
|
/**
|
|
703
757
|
* `terrain` generator only. `standard` is the classic Wang tileset
|
|
704
|
-
* pipeline; `pro` is a newer
|
|
705
|
-
*
|
|
706
|
-
* in place of
|
|
758
|
+
* pipeline; `pro` is a newer pipeline with its own shape controls
|
|
759
|
+
* (`terrainRaggedness`, `terrainRoundness`, `terrainBoundaryAmplitude`,
|
|
760
|
+
* `terrainStepSlope`, `terrainConnectDiagonals`) in place of
|
|
761
|
+
* `terrainShapeStyle`. The API default is `standard`.
|
|
707
762
|
*/
|
|
708
763
|
terrainMode: z.enum(["standard", "pro"]).optional(),
|
|
709
764
|
/**
|
|
@@ -715,31 +770,86 @@ var StyleObjectSchema = z.object({
|
|
|
715
770
|
/**
|
|
716
771
|
* `terrain` generator only, `terrainMode: "standard"`. Procedural
|
|
717
772
|
* boundary geometry: `square` or `round`, 16px or 32px tiles only.
|
|
718
|
-
* Rejected together with `terrainMode: "pro"`,
|
|
719
|
-
*
|
|
773
|
+
* Rejected together with `terrainMode: "pro"`, which has its own shape
|
|
774
|
+
* controls.
|
|
720
775
|
*/
|
|
721
776
|
terrainShapeStyle: z.enum(["square", "round"]).optional(),
|
|
722
|
-
/**
|
|
723
|
-
*
|
|
777
|
+
/**
|
|
778
|
+
* Deprecated: no effect. PixelLab's Pro pipeline no longer reads
|
|
779
|
+
* `spread_x`; the API keeps accepting it so existing calls work, and so
|
|
780
|
+
* does pixelkiln. Use `terrainBoundaryAmplitude` or `terrainStepSlope`.
|
|
781
|
+
*/
|
|
724
782
|
terrainSpreadX: z.number().min(0).max(1).optional(),
|
|
725
|
-
/**
|
|
726
|
-
*
|
|
783
|
+
/**
|
|
784
|
+
* Deprecated: no effect. PixelLab's Pro pipeline no longer reads
|
|
785
|
+
* `slope_size`; the API keeps accepting it so existing calls work, and
|
|
786
|
+
* so does pixelkiln. `terrainStepSlope` is the slope control now.
|
|
787
|
+
*/
|
|
727
788
|
terrainSlopeSize: z.number().min(0).max(1).optional(),
|
|
728
789
|
/** `terrain` generator only, `terrainMode: "pro"`. Terrain boundary
|
|
729
|
-
* noise (0 = smooth, 1 = rough)
|
|
790
|
+
* noise (0 = smooth, 1 = rough), half strength on tall sets
|
|
791
|
+
* (`terrainTransitionSize` above 0.4). API default 0. */
|
|
730
792
|
terrainRaggedness: z.number().min(0).max(1).optional(),
|
|
793
|
+
/**
|
|
794
|
+
* `terrain` generator only, `terrainMode: "pro"` or `terrainShapeStyle`.
|
|
795
|
+
* Corner shape of the border between the terrains: 0 square, 1 round,
|
|
796
|
+
* 2 corners cut at 45° (values between blend). Above 1 needs pro. The API
|
|
797
|
+
* default is 0.85 on pro and 0.6 with a shape style. With
|
|
798
|
+
* `terrainConnectDiagonals`, 2 is the fix for 45° paths and coastlines
|
|
799
|
+
* that otherwise break into one island per step.
|
|
800
|
+
*/
|
|
801
|
+
terrainRoundness: z.number().min(0).max(2).optional(),
|
|
802
|
+
/**
|
|
803
|
+
* `terrain` generator only, `terrainMode: "pro"` (any transition size)
|
|
804
|
+
* or `terrainShapeStyle` (transition size up to 0.5). How far the border
|
|
805
|
+
* meanders instead of running straight: 0 straight (the default) to 0.3.
|
|
806
|
+
* Tiles still connect seamlessly.
|
|
807
|
+
*/
|
|
808
|
+
terrainBoundaryAmplitude: z.number().min(0).max(0.3).optional(),
|
|
809
|
+
/**
|
|
810
|
+
* `terrain` generator only, with `terrainBoundaryAmplitude`. Picks the
|
|
811
|
+
* meander's shape: another number gives a different border at the same
|
|
812
|
+
* amplitude, the same number the same border. API default 0.
|
|
813
|
+
*/
|
|
814
|
+
terrainBoundarySeed: z.number().int().min(0).optional(),
|
|
815
|
+
/**
|
|
816
|
+
* `terrain` generator only, `terrainMode: "pro"` with
|
|
817
|
+
* `terrainTransitionSize: 0.25`. How the bank between the terrains
|
|
818
|
+
* falls: 0 a straight cliff (the default) to 2 a gentle slope spread
|
|
819
|
+
* over more ground.
|
|
820
|
+
*/
|
|
821
|
+
terrainStepSlope: z.number().min(0).max(2).optional(),
|
|
822
|
+
/**
|
|
823
|
+
* `terrain` generator only, `terrainMode: "pro"`. On the two diagonal
|
|
824
|
+
* tiles (upper terrain in opposite corners), true joins the upper
|
|
825
|
+
* corners through the middle into one area; false (the default) runs the
|
|
826
|
+
* lower terrain between them.
|
|
827
|
+
*/
|
|
828
|
+
terrainConnectDiagonals: z.boolean().optional(),
|
|
731
829
|
/**
|
|
732
830
|
* `terrain` generator only. Visual height of the step between lower and
|
|
733
831
|
* upper terrain. Without `terrainShapeStyle`, only 0, 0.25, 0.5, or 1 are
|
|
734
|
-
* accepted
|
|
735
|
-
* third "transition" value
|
|
736
|
-
*
|
|
737
|
-
*
|
|
832
|
+
* accepted. Standard switches to the 25-tile cliff layout at 1, where
|
|
833
|
+
* corners take a third "transition" value; Pro reads it as wall height
|
|
834
|
+
* and switches to the 25-tile tall-wall layout above 0.4, so 0.5 is
|
|
835
|
+
* already 25 tiles there. With `terrainShapeStyle`, any value from 0 to 1
|
|
836
|
+
* works, but above 0.5 switches to an extended 32-tile layout this
|
|
837
|
+
* adapter does not model. API default 0.
|
|
738
838
|
*/
|
|
739
839
|
terrainTransitionSize: z.number().min(0).max(1).optional(),
|
|
740
840
|
/** `terrain` generator only. Camera angle; the API default is `high
|
|
741
841
|
* top-down`. */
|
|
742
842
|
terrainView: z.enum(["low top-down", "high top-down"]).optional(),
|
|
843
|
+
/**
|
|
844
|
+
* `terrain` generator only. `top-down` (the default) is a two-terrain
|
|
845
|
+
* map set from `/create-tileset`; `sidescroller` is a platform set from
|
|
846
|
+
* `/create-tileset-sidescroller`, seen from the side: the prompt's 1).
|
|
847
|
+
* is the platform body and 2). the top layer on it (grass, snow, moss),
|
|
848
|
+
* and the set's other terrain is empty air. 16 or 32px tiles,
|
|
849
|
+
* `terrainTransitionSize` 0, 0.25, 0.5, or 1 (the top layer's height),
|
|
850
|
+
* and none of the top-down mode, view, or shape controls.
|
|
851
|
+
*/
|
|
852
|
+
terrainOrientation: z.enum(["top-down", "sidescroller"]).optional(),
|
|
743
853
|
/**
|
|
744
854
|
* `isometricTile` generator only. `/create-isometric-tile`'s own tile
|
|
745
855
|
* grid size, distinct from `size` (the generation canvas, 16-64px — the
|
|
@@ -819,7 +929,9 @@ var StyleObjectSchema = z.object({
|
|
|
819
929
|
/** Fixed seed for reproducibility where the endpoint supports it. */
|
|
820
930
|
seed: z.number().int().optional(),
|
|
821
931
|
/**
|
|
822
|
-
* Forced palette, as `#rrggbb` values. `pixflux`
|
|
932
|
+
* Forced palette, as `#rrggbb` values. Sent as a swatch to `pixflux`
|
|
933
|
+
* and the generators that take one, and to `imagePro` as its
|
|
934
|
+
* `target_palette.colors` (2 to 256 colours).
|
|
823
935
|
*
|
|
824
936
|
* Unlike prose, this is a hard constraint. The API is handed a swatch
|
|
825
937
|
* image and the output is limited to those colours. Verified: a four-colour
|
|
@@ -829,6 +941,10 @@ var StyleObjectSchema = z.object({
|
|
|
829
941
|
* chocolate bar.
|
|
830
942
|
*/
|
|
831
943
|
palette: z.array(HexColorSchema).default([]),
|
|
944
|
+
/** `imagePro` only: the palette's name ("Game Boy", "PICO-8"), which PixelLab uses to help the look. Needs `palette`. */
|
|
945
|
+
paletteName: z.string().min(1).max(100).optional(),
|
|
946
|
+
/** `imagePro` only: at most this many colours in each candidate, 2 to 64 (drawn from `palette` when one is set). */
|
|
947
|
+
maxColors: z.number().int().min(2).max(64).optional(),
|
|
832
948
|
/**
|
|
833
949
|
* Snap every visible pixel of the downloaded art to the nearest `palette`
|
|
834
950
|
* colour, without dithering. The palette is sent to providers that take
|
|
@@ -882,6 +998,22 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
|
|
|
882
998
|
path: ["quality"]
|
|
883
999
|
});
|
|
884
1000
|
}
|
|
1001
|
+
for (const field of ["paletteName", "maxColors"]) {
|
|
1002
|
+
if (style[field] !== void 0 && style.generator !== "imagePro") {
|
|
1003
|
+
ctx.addIssue({
|
|
1004
|
+
code: z.ZodIssueCode.custom,
|
|
1005
|
+
message: `${field} applies to the imagePro generator only`,
|
|
1006
|
+
path: [field]
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (style.paletteName !== void 0 && style.palette.length === 0) {
|
|
1011
|
+
ctx.addIssue({
|
|
1012
|
+
code: z.ZodIssueCode.custom,
|
|
1013
|
+
message: "paletteName names the palette's colours; set palette too",
|
|
1014
|
+
path: ["paletteName"]
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
885
1017
|
if (style.enforcePalette && style.palette.length < 2) {
|
|
886
1018
|
ctx.addIssue({
|
|
887
1019
|
code: z.ZodIssueCode.custom,
|
|
@@ -1007,6 +1139,8 @@ var AssetSchema = z.object({
|
|
|
1007
1139
|
scene: MapSceneSchema.optional(),
|
|
1008
1140
|
/** `vfx` styles only: the effect to render. See `AssetVfxSchema`. */
|
|
1009
1141
|
vfx: AssetVfxSchema.optional(),
|
|
1142
|
+
/** `terrain` styles only: share a terrain with another set. See `TerrainConnectSchema`. */
|
|
1143
|
+
connect: TerrainConnectSchema.optional(),
|
|
1010
1144
|
/** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
|
|
1011
1145
|
proportions: CharacterProportionsSchema.optional(),
|
|
1012
1146
|
/**
|
|
@@ -1083,7 +1217,7 @@ var AssetSchema = z.object({
|
|
|
1083
1217
|
path: ["revision"]
|
|
1084
1218
|
});
|
|
1085
1219
|
}
|
|
1086
|
-
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);
|
|
1220
|
+
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", asset.connect && "connect"].filter(Boolean);
|
|
1087
1221
|
if (shapes.length > 1) {
|
|
1088
1222
|
context.addIssue({
|
|
1089
1223
|
code: z.ZodIssueCode.custom,
|
|
@@ -1175,6 +1309,10 @@ var LockSourceSchema = z.object({
|
|
|
1175
1309
|
role: z.string().optional(),
|
|
1176
1310
|
mediaType: MediaTypeSchema.optional()
|
|
1177
1311
|
});
|
|
1312
|
+
var LockTerrainConnectSchema = z.record(
|
|
1313
|
+
TerrainSideSchema,
|
|
1314
|
+
z.object({ sourceAssetId: z.string().min(1), sourceObjectId: z.string().min(1) }).strict()
|
|
1315
|
+
);
|
|
1178
1316
|
var LockHistoryEntrySchema = z.object({
|
|
1179
1317
|
specHash: z.string(),
|
|
1180
1318
|
generator: GeneratorSchema,
|
|
@@ -1197,6 +1335,8 @@ var LockHistoryEntrySchema = z.object({
|
|
|
1197
1335
|
billed: z.object({ amount: z.number(), unit: z.string() }).nullable().default(null),
|
|
1198
1336
|
provider: z.string().default("pixellab"),
|
|
1199
1337
|
postprocess: PostprocessSchema.optional(),
|
|
1338
|
+
/** A connected terrain set's other sets; see LockEntrySchema.terrainConnect. */
|
|
1339
|
+
terrainConnect: LockTerrainConnectSchema.optional(),
|
|
1200
1340
|
/** When the replacement was submitted. */
|
|
1201
1341
|
retiredAt: z.string()
|
|
1202
1342
|
});
|
|
@@ -1235,6 +1375,7 @@ var LockEntrySchema = z.object({
|
|
|
1235
1375
|
skeletonTemplate: z.string().min(1).optional(),
|
|
1236
1376
|
description: z.string().min(1).optional(),
|
|
1237
1377
|
removalTask: z.enum(["simple", "complex"]).optional(),
|
|
1378
|
+
convention: NormalMapConventionSchema.optional(),
|
|
1238
1379
|
driftThreshold: z.number().min(0).optional(),
|
|
1239
1380
|
subjectDescription: z.string().min(1).max(300).optional(),
|
|
1240
1381
|
initialPose: z.string().min(1).max(300).optional(),
|
|
@@ -1262,6 +1403,14 @@ var LockEntrySchema = z.object({
|
|
|
1262
1403
|
sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
1263
1404
|
referenceSha256: z.string().regex(/^[0-9a-f]{64}$/)
|
|
1264
1405
|
}).strict().nullable().default(null),
|
|
1406
|
+
/**
|
|
1407
|
+
* For a connected terrain set: per side of this set, the set it shares
|
|
1408
|
+
* that terrain with and that set's provider id when this was submitted.
|
|
1409
|
+
* Tracked the way `outfit` is, not through `specHash`: the other set's
|
|
1410
|
+
* id lives in the lockfile, which manifest resolution does not read.
|
|
1411
|
+
* Absent (not null) otherwise, so other entries do not grow the key.
|
|
1412
|
+
*/
|
|
1413
|
+
terrainConnect: LockTerrainConnectSchema.optional(),
|
|
1265
1414
|
/**
|
|
1266
1415
|
* Output hashes owned by the previous generation while its replacement is
|
|
1267
1416
|
* pending. They authorize replacing only unchanged PixelKiln-owned files.
|
|
@@ -1586,7 +1735,16 @@ var TilesetGetSchema = z3.object({
|
|
|
1586
1735
|
terrain_types: z3.array(z3.string()),
|
|
1587
1736
|
tiles: z3.array(TilesetTileSchema).min(1)
|
|
1588
1737
|
}).passthrough(),
|
|
1589
|
-
usage: UsageSchema
|
|
1738
|
+
usage: UsageSchema,
|
|
1739
|
+
/**
|
|
1740
|
+
* `terrain_ids` names each terrain's base tile id: the id another set
|
|
1741
|
+
* sends as `lower_base_tile_id`/`upper_base_tile_id` to share that
|
|
1742
|
+
* terrain. A sidescroller set has only `lower` (its upper is empty air).
|
|
1743
|
+
*/
|
|
1744
|
+
metadata: z3.object({
|
|
1745
|
+
terrain_ids: z3.record(z3.string()).nullable().optional(),
|
|
1746
|
+
view: z3.string().nullable().optional()
|
|
1747
|
+
}).passthrough().nullable().optional()
|
|
1590
1748
|
}).passthrough();
|
|
1591
1749
|
var IsometricTileSubmitSchema = z3.object({
|
|
1592
1750
|
tile_id: z3.string().min(1),
|
|
@@ -1675,6 +1833,12 @@ var ProFlashImageSubmitSchema = z3.object({
|
|
|
1675
1833
|
estimated_generations: z3.number().nullable().optional(),
|
|
1676
1834
|
usage: z3.unknown().optional()
|
|
1677
1835
|
}).passthrough();
|
|
1836
|
+
var VfxSubmitSchema = z3.object({
|
|
1837
|
+
background_job_id: z3.string().min(1),
|
|
1838
|
+
status: z3.string().default("processing"),
|
|
1839
|
+
quoted_generations: z3.number().nullable().optional(),
|
|
1840
|
+
usage: z3.unknown().optional()
|
|
1841
|
+
}).passthrough();
|
|
1678
1842
|
var UnzoomResponseSchema = z3.object({
|
|
1679
1843
|
image: z3.object({ base64: z3.string().min(1) }).passthrough(),
|
|
1680
1844
|
original_size: z3.object({ width: z3.number().int(), height: z3.number().int() }).passthrough(),
|
|
@@ -2015,12 +2179,19 @@ var PixelLabClient = class {
|
|
|
2015
2179
|
upper_description: args.upperDescription
|
|
2016
2180
|
};
|
|
2017
2181
|
if (args.transitionDescription) body.transition_description = args.transitionDescription;
|
|
2182
|
+
if (args.lowerBaseTileId) body.lower_base_tile_id = args.lowerBaseTileId;
|
|
2183
|
+
if (args.upperBaseTileId) body.upper_base_tile_id = args.upperBaseTileId;
|
|
2018
2184
|
if (args.tileSize != null) body.tile_size = { width: args.tileSize, height: args.tileSize };
|
|
2019
2185
|
if (args.mode) body.mode = args.mode;
|
|
2020
2186
|
if (args.shapeStyle) body.shape_style = args.shapeStyle;
|
|
2021
2187
|
if (args.spreadX != null) body.spread_x = args.spreadX;
|
|
2022
2188
|
if (args.slopeSize != null) body.slope_size = args.slopeSize;
|
|
2023
2189
|
if (args.raggedness != null) body.raggedness = args.raggedness;
|
|
2190
|
+
if (args.roundness != null) body.roundness = args.roundness;
|
|
2191
|
+
if (args.boundaryAmplitude != null) body.boundary_amplitude = args.boundaryAmplitude;
|
|
2192
|
+
if (args.boundarySeed != null) body.boundary_seed = args.boundarySeed;
|
|
2193
|
+
if (args.stepSlope != null) body.step_slope = args.stepSlope;
|
|
2194
|
+
if (args.connectDiagonals != null) body.connect_diagonals = args.connectDiagonals;
|
|
2024
2195
|
if (args.transitionSize != null) body.transition_size = args.transitionSize;
|
|
2025
2196
|
if (args.view) body.view = args.view;
|
|
2026
2197
|
if (args.outline) body.outline = args.outline;
|
|
@@ -2033,14 +2204,48 @@ var PixelLabClient = class {
|
|
|
2033
2204
|
"create tileset"
|
|
2034
2205
|
);
|
|
2035
2206
|
}
|
|
2036
|
-
/**
|
|
2207
|
+
/**
|
|
2208
|
+
* `/create-tileset-sidescroller`: a platform set seen from the side. One
|
|
2209
|
+
* terrain (`lower_description`, the platform body) and the decorative
|
|
2210
|
+
* layer on top of it (`transition_description`); the other side of every
|
|
2211
|
+
* edge is empty air, so there is no upper description, mode, view, or
|
|
2212
|
+
* shape control. 16 or 32px tiles only. The result is read through
|
|
2213
|
+
* `getTileset` like a top-down set.
|
|
2214
|
+
*/
|
|
2215
|
+
async createTilesetSidescroller(args) {
|
|
2216
|
+
const body = { lower_description: args.lowerDescription };
|
|
2217
|
+
if (args.transitionDescription) body.transition_description = args.transitionDescription;
|
|
2218
|
+
if (args.lowerBaseTileId) body.lower_base_tile_id = args.lowerBaseTileId;
|
|
2219
|
+
if (args.tileSize != null) body.tile_size = { width: args.tileSize, height: args.tileSize };
|
|
2220
|
+
if (args.transitionSize != null) body.transition_size = args.transitionSize;
|
|
2221
|
+
if (args.outline) body.outline = args.outline;
|
|
2222
|
+
if (args.shading) body.shading = args.shading;
|
|
2223
|
+
if (args.detail) body.detail = args.detail;
|
|
2224
|
+
if (args.seed != null) body.seed = args.seed;
|
|
2225
|
+
return validateResponse(
|
|
2226
|
+
TilesetSubmitSchema,
|
|
2227
|
+
await this.request("/create-tileset-sidescroller", { method: "POST", body: JSON.stringify(body) }),
|
|
2228
|
+
"create sidescroller tileset"
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Throws PixelLabError(423) while the set is still drawing; see Tileset.
|
|
2233
|
+
* Serves top-down and sidescroller sets alike.
|
|
2234
|
+
*/
|
|
2037
2235
|
async getTileset(tilesetId) {
|
|
2038
2236
|
const res = await validateResponse(
|
|
2039
2237
|
TilesetGetSchema,
|
|
2040
2238
|
await this.request(`/tilesets/${tilesetId}`),
|
|
2041
2239
|
"get tileset"
|
|
2042
2240
|
);
|
|
2043
|
-
return {
|
|
2241
|
+
return {
|
|
2242
|
+
tileset: res.tileset,
|
|
2243
|
+
usage: res.usage ?? null,
|
|
2244
|
+
metadata: {
|
|
2245
|
+
terrainIds: res.metadata?.terrain_ids ?? {},
|
|
2246
|
+
view: res.metadata?.view ?? null
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2044
2249
|
}
|
|
2045
2250
|
/** A single standalone isometric tile — no connectable set, no candidates. */
|
|
2046
2251
|
async createIsometricTile(args) {
|
|
@@ -2577,6 +2782,13 @@ var PixelLabClient = class {
|
|
|
2577
2782
|
*
|
|
2578
2783
|
* Reference images and a style image (`reference_images`, `style_image` +
|
|
2579
2784
|
* `style_options`) exist on this endpoint but are not modeled here yet.
|
|
2785
|
+
*
|
|
2786
|
+
* `targetPalette` is the beta `target_palette` (PixelLab 0.4.130): with
|
|
2787
|
+
* `colors`, the image is drawn for them and every result pixel is one of
|
|
2788
|
+
* them; `name` ("PICO-8") helps the look; `maxColors` caps the colours in
|
|
2789
|
+
* each candidate. Its `smart` flag is not modeled: it is documented as
|
|
2790
|
+
* "single images only", and this endpoint returns a grid of candidates
|
|
2791
|
+
* below 171px.
|
|
2580
2792
|
*/
|
|
2581
2793
|
async createImagePro(args) {
|
|
2582
2794
|
const body = {
|
|
@@ -2585,6 +2797,14 @@ var PixelLabClient = class {
|
|
|
2585
2797
|
};
|
|
2586
2798
|
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
2587
2799
|
if (args.seed != null) body.seed = args.seed;
|
|
2800
|
+
const palette = args.targetPalette;
|
|
2801
|
+
if (palette && (palette.colors?.length || palette.maxColors != null)) {
|
|
2802
|
+
body.target_palette = {
|
|
2803
|
+
...palette.colors?.length ? { colors: palette.colors } : {},
|
|
2804
|
+
...palette.name ? { name: palette.name } : {},
|
|
2805
|
+
...palette.maxColors != null ? { max_colors: palette.maxColors } : {}
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2588
2808
|
return validateResponse(
|
|
2589
2809
|
RevisionJobSubmitSchema,
|
|
2590
2810
|
await this.request("/generate-image-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
@@ -2775,6 +2995,27 @@ var PixelLabClient = class {
|
|
|
2775
2995
|
);
|
|
2776
2996
|
return { png: Buffer.from(res.image.base64, "base64"), usage: res.usage };
|
|
2777
2997
|
}
|
|
2998
|
+
/**
|
|
2999
|
+
* `/normal-map` (PixelLab 0.4.130): the normal map of a sprite, so a game
|
|
3000
|
+
* engine can light it. RGB is the way each pixel faces, alpha is 255 where
|
|
3001
|
+
* the sprite shows and 0 elsewhere, at the input's size. An animation goes
|
|
3002
|
+
* as one horizontal strip of `frames` equal frames (up to 32, each 8 to 512
|
|
3003
|
+
* px per side) and comes back as a strip, planned once on the first frame
|
|
3004
|
+
* so the light stays steady. `opengl` (the default) is green up, for
|
|
3005
|
+
* Godot, Unity, and Phaser; `directx` is green down, for Unreal. A
|
|
3006
|
+
* background job whose map is `last_response.image`; 0.5 generation,
|
|
3007
|
+
* charged only when the map is delivered.
|
|
3008
|
+
*/
|
|
3009
|
+
async normalMap(args) {
|
|
3010
|
+
const body = { image: args.image };
|
|
3011
|
+
if (args.frames != null && args.frames !== 1) body.frames = args.frames;
|
|
3012
|
+
if (args.convention) body.convention = args.convention;
|
|
3013
|
+
return validateResponse(
|
|
3014
|
+
RevisionJobSubmitSchema,
|
|
3015
|
+
await this.request("/normal-map", { method: "POST", body: JSON.stringify(body) }),
|
|
3016
|
+
"normal-map"
|
|
3017
|
+
);
|
|
3018
|
+
}
|
|
2778
3019
|
/**
|
|
2779
3020
|
* `/unzoom`: recover native-resolution pixel art from an upscaled image (a
|
|
2780
3021
|
* 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
|
|
@@ -2865,6 +3106,28 @@ var PixelLabClient = class {
|
|
|
2865
3106
|
"generate-font-pro"
|
|
2866
3107
|
);
|
|
2867
3108
|
}
|
|
3109
|
+
/**
|
|
3110
|
+
* `POST /vfx`: PixelLab's effects artist builds an animated effect from a
|
|
3111
|
+
* description, around an optional sprite, and saves it to My effects. With
|
|
3112
|
+
* `effectId` it changes that saved effect in place; adding the effect's
|
|
3113
|
+
* `conversation` prices the change as a change. A background job whose
|
|
3114
|
+
* 202 carries `quoted_generations`; PixelLab bills on delivery, up to
|
|
3115
|
+
* twice the quote. A 503 (renderer busy) is retried by the shared policy.
|
|
3116
|
+
*/
|
|
3117
|
+
async createVfx(args) {
|
|
3118
|
+
const body = { description: args.description };
|
|
3119
|
+
if (args.artist) body.artist = args.artist;
|
|
3120
|
+
if (args.sprite) body.sprite = { type: "base64", base64: args.sprite.base64, format: args.sprite.format };
|
|
3121
|
+
if (args.effectId) body.effect_id = args.effectId;
|
|
3122
|
+
if (args.conversation) body.conversation = args.conversation;
|
|
3123
|
+
if (args.name) body.name = args.name;
|
|
3124
|
+
if (args.gamePack) body.game_pack = true;
|
|
3125
|
+
return validateResponse(
|
|
3126
|
+
VfxSubmitSchema,
|
|
3127
|
+
await this.request("/vfx", { method: "POST", body: JSON.stringify(body) }),
|
|
3128
|
+
"vfx"
|
|
3129
|
+
);
|
|
3130
|
+
}
|
|
2868
3131
|
/** `GET /generate-font-pro/{job_id}`: status, then no-auth atlas and `.ttf` URLs once completed. */
|
|
2869
3132
|
async getFontJob(jobId) {
|
|
2870
3133
|
const raw = await this.request(`/generate-font-pro/${encodeURIComponent(jobId)}`);
|
|
@@ -3464,6 +3727,41 @@ function parseHex(hex2) {
|
|
|
3464
3727
|
const n = parseInt(m[1], 16);
|
|
3465
3728
|
return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 };
|
|
3466
3729
|
}
|
|
3730
|
+
function joinStripPng(frames) {
|
|
3731
|
+
const decoded = frames.map((frame) => decodePng(frame));
|
|
3732
|
+
const first = decoded[0];
|
|
3733
|
+
if (!first) throw new Error("a strip needs at least one frame");
|
|
3734
|
+
const { width, height } = first;
|
|
3735
|
+
for (const [index, frame] of decoded.entries()) {
|
|
3736
|
+
if (frame.width !== width || frame.height !== height) {
|
|
3737
|
+
throw new Error(`strip frame ${index} is ${frame.width}x${frame.height}; frame 0 is ${width}x${height}`);
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
const stride = width * 4;
|
|
3741
|
+
const out = Buffer.alloc(stride * decoded.length * height);
|
|
3742
|
+
for (let y = 0; y < height; y++) {
|
|
3743
|
+
for (const [index, frame] of decoded.entries()) {
|
|
3744
|
+
frame.pixels.copy(out, (y * decoded.length + index) * stride, y * stride, (y + 1) * stride);
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
return encodeRgbaPng(width * decoded.length, height, out);
|
|
3748
|
+
}
|
|
3749
|
+
function splitStripPng(strip, frames) {
|
|
3750
|
+
const image = decodePng(strip);
|
|
3751
|
+
if (frames < 1 || image.width % frames !== 0) {
|
|
3752
|
+
throw new Error(`a ${image.width}x${image.height} strip does not split into ${frames} equal frames`);
|
|
3753
|
+
}
|
|
3754
|
+
const width = image.width / frames;
|
|
3755
|
+
const stride = width * 4;
|
|
3756
|
+
return Array.from({ length: frames }, (_, index) => {
|
|
3757
|
+
const out = Buffer.alloc(stride * image.height);
|
|
3758
|
+
for (let y = 0; y < image.height; y++) {
|
|
3759
|
+
const from = (y * image.width + index * width) * 4;
|
|
3760
|
+
image.pixels.copy(out, y * stride, from, from + stride);
|
|
3761
|
+
}
|
|
3762
|
+
return encodeRgbaPng(width, image.height, out);
|
|
3763
|
+
});
|
|
3764
|
+
}
|
|
3467
3765
|
|
|
3468
3766
|
// src/media.ts
|
|
3469
3767
|
var MediaType = {
|
|
@@ -3579,10 +3877,10 @@ function imageMetadata(buf) {
|
|
|
3579
3877
|
}
|
|
3580
3878
|
|
|
3581
3879
|
// src/providers/pixellab.ts
|
|
3582
|
-
import { mkdirSync, existsSync as
|
|
3880
|
+
import { mkdirSync, existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
|
|
3583
3881
|
import { randomUUID } from "crypto";
|
|
3584
3882
|
import os from "os";
|
|
3585
|
-
import
|
|
3883
|
+
import path3 from "path";
|
|
3586
3884
|
|
|
3587
3885
|
// src/hash.ts
|
|
3588
3886
|
import { createHash } from "crypto";
|
|
@@ -3618,6 +3916,9 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
3618
3916
|
detail: effect ? void 0 : spec.detail ?? null,
|
|
3619
3917
|
seed: effect ? void 0 : spec.seed ?? null,
|
|
3620
3918
|
palette: effect ? void 0 : spec.palette,
|
|
3919
|
+
// `imagePro` only, and absent unless the style sets them.
|
|
3920
|
+
paletteName: spec.paletteName,
|
|
3921
|
+
maxColors: spec.maxColors,
|
|
3621
3922
|
// `noBackground` only reaches the wire for pixflux and uiAsset; the
|
|
3622
3923
|
// tile fields are undefined for every other generator. `tileSize` is
|
|
3623
3924
|
// intentionally absent; width/height are derived from it, so it is
|
|
@@ -3632,6 +3933,25 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
3632
3933
|
uiPieces: spec.uiPieces,
|
|
3633
3934
|
uiElements: spec.uiElements,
|
|
3634
3935
|
uiColorPalette: spec.uiColorPalette,
|
|
3936
|
+
// The older terrain knobs (terrainMode, terrainTransitionSize, and the
|
|
3937
|
+
// rest) were never part of this hash, and adding them now would make
|
|
3938
|
+
// every existing terrain entry stale. The Pro shape controls added
|
|
3939
|
+
// after them are identity, each only when set.
|
|
3940
|
+
terrainRoundness: spec.terrainRoundness,
|
|
3941
|
+
terrainBoundaryAmplitude: spec.terrainBoundaryAmplitude,
|
|
3942
|
+
terrainBoundarySeed: spec.terrainBoundarySeed,
|
|
3943
|
+
terrainStepSlope: spec.terrainStepSlope,
|
|
3944
|
+
terrainConnectDiagonals: spec.terrainConnectDiagonals,
|
|
3945
|
+
// "top-down" is the default and draws the same set as leaving it out.
|
|
3946
|
+
terrainOrientation: spec.terrainOrientation === "sidescroller" ? spec.terrainOrientation : void 0,
|
|
3947
|
+
// Which set and which of its terrains, not that terrain's id: the id
|
|
3948
|
+
// is in the lockfile, and the plan compares it there (like a mirror).
|
|
3949
|
+
terrainConnect: spec.terrainConnect ? Object.fromEntries(
|
|
3950
|
+
["lower", "upper"].flatMap((side) => {
|
|
3951
|
+
const link = spec.terrainConnect[side];
|
|
3952
|
+
return link ? [[side, { of: link.assetId, terrain: link.terrain }]] : [];
|
|
3953
|
+
})
|
|
3954
|
+
) : void 0,
|
|
3635
3955
|
// Scene and mask enter as their bytes' hashes, never their paths.
|
|
3636
3956
|
scene: spec.scene ? {
|
|
3637
3957
|
image: spec.scene.sha256,
|
|
@@ -3693,13 +4013,17 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
3693
4013
|
reference: spec.objectPro.reference?.sha256
|
|
3694
4014
|
} : void 0,
|
|
3695
4015
|
// A recipe and a local sprite enter as their contents' hashes, never
|
|
3696
|
-
// their paths; the My effects name is not identity.
|
|
4016
|
+
// their paths; the My effects name is not identity. `description` and
|
|
4017
|
+
// `artist` exist only for the paid VFX artist and stay absent for a
|
|
4018
|
+
// workbench effect, so its hash is the one it always had.
|
|
3697
4019
|
vfx: spec.vfx ? {
|
|
3698
4020
|
preset: spec.vfx.preset,
|
|
3699
4021
|
recipe: spec.vfx.recipe?.sha256,
|
|
3700
4022
|
parts: spec.vfx.parts,
|
|
3701
4023
|
fps: spec.vfx.fps,
|
|
3702
|
-
sprite: spec.vfx.sprite ? "reference" in spec.vfx.sprite ? spec.vfx.sprite.reference : spec.vfx.sprite.sha256 : void 0
|
|
4024
|
+
sprite: spec.vfx.sprite ? "reference" in spec.vfx.sprite ? spec.vfx.sprite.reference : spec.vfx.sprite.sha256 : void 0,
|
|
4025
|
+
description: spec.vfx.description,
|
|
4026
|
+
artist: spec.vfx.description ? spec.vfx.artist : void 0
|
|
3703
4027
|
} : void 0,
|
|
3704
4028
|
// A mirror's bytes come from its source's recorded outputs; the plan
|
|
3705
4029
|
// compares those directly, so only the choice of source is identity.
|
|
@@ -3729,6 +4053,7 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
3729
4053
|
skeletonTemplate: spec.revision.skeletonTemplate,
|
|
3730
4054
|
description: spec.revision.description,
|
|
3731
4055
|
removalTask: spec.revision.removalTask,
|
|
4056
|
+
convention: spec.revision.convention,
|
|
3732
4057
|
driftThreshold: spec.revision.driftThreshold,
|
|
3733
4058
|
subjectDescription: spec.revision.subjectDescription,
|
|
3734
4059
|
initialPose: spec.revision.initialPose,
|
|
@@ -3875,7 +4200,8 @@ function vfxOutputPaths(out) {
|
|
|
3875
4200
|
gif: `${base}.gif`,
|
|
3876
4201
|
atlas: `${base}.json`,
|
|
3877
4202
|
provenance: `${base}.vfx.json`,
|
|
3878
|
-
gamePack: `${base}-game.zip
|
|
4203
|
+
gamePack: `${base}-game.zip`,
|
|
4204
|
+
normals: `${base}-normals.png`
|
|
3879
4205
|
};
|
|
3880
4206
|
}
|
|
3881
4207
|
function isVfxReference(sprite) {
|
|
@@ -3965,10 +4291,17 @@ function vfxRecipeSha256(recipe) {
|
|
|
3965
4291
|
const { name: _name, notes: _notes, ...rest } = recipe;
|
|
3966
4292
|
return sha256(JSON.stringify(rest));
|
|
3967
4293
|
}
|
|
4294
|
+
function vfxAtlasFrames(atlas) {
|
|
4295
|
+
const listed = atlas?.frames;
|
|
4296
|
+
const entries = Array.isArray(listed) ? listed : listed && typeof listed === "object" ? Object.values(listed) : null;
|
|
4297
|
+
if (!entries?.length) return null;
|
|
4298
|
+
const rects = entries.map((entry) => entry?.frame);
|
|
4299
|
+
return rects.every((rect) => rect && [rect.x, rect.y, rect.w, rect.h].every((n) => typeof n === "number")) ? rects : null;
|
|
4300
|
+
}
|
|
3968
4301
|
function sliceVfxSheet(sheet, facts, atlas) {
|
|
3969
4302
|
const image = decodePng(sheet);
|
|
3970
|
-
const listed = atlas
|
|
3971
|
-
const rects =
|
|
4303
|
+
const listed = vfxAtlasFrames(atlas);
|
|
4304
|
+
const rects = listed && listed.length === facts.frames ? listed : Array.from({ length: facts.frames }, (_, index) => {
|
|
3972
4305
|
const columns = Math.max(1, Math.floor(image.width / facts.width));
|
|
3973
4306
|
return { x: index % columns * facts.width, y: Math.floor(index / columns) * facts.height, w: facts.width, h: facts.height };
|
|
3974
4307
|
});
|
|
@@ -4068,6 +4401,477 @@ async function makeVfx(client2, opts) {
|
|
|
4068
4401
|
`);
|
|
4069
4402
|
return { ...exported, paths: written, recipe };
|
|
4070
4403
|
}
|
|
4404
|
+
var VFX_ARTISTS = ["lite", "pro"];
|
|
4405
|
+
var VFX_ARTIST_MAX_SPRITE = 512;
|
|
4406
|
+
function vfxArtistQuote(artist, sprite, change = false) {
|
|
4407
|
+
const table = {
|
|
4408
|
+
lite: { new: [1, 1, 3, 5], change: [1, 1, 2, 2] },
|
|
4409
|
+
pro: { new: [6, 8, 14, 28], change: [4, 4, 5, 8] }
|
|
4410
|
+
};
|
|
4411
|
+
const tier = sprite === "none" ? 0 : sprite === "unknown" ? 3 : sprite <= 128 ? 1 : sprite <= 256 ? 2 : 3;
|
|
4412
|
+
return table[artist][change ? "change" : "new"][tier];
|
|
4413
|
+
}
|
|
4414
|
+
function vfxArtistCeiling(quote) {
|
|
4415
|
+
return quote * 2;
|
|
4416
|
+
}
|
|
4417
|
+
function vfxArtistSprite(bytes, label) {
|
|
4418
|
+
const metadata = imageMetadata(bytes);
|
|
4419
|
+
if (metadata?.format !== "png") throw new Error(`${label}: PixelLab's VFX artist takes a PNG sprite`);
|
|
4420
|
+
const side = Math.max(metadata.width, metadata.height);
|
|
4421
|
+
if (side > VFX_ARTIST_MAX_SPRITE) {
|
|
4422
|
+
throw new Error(
|
|
4423
|
+
`${label} is ${metadata.width}x${metadata.height}; PixelLab's VFX artist takes up to ${VFX_ARTIST_MAX_SPRITE}x${VFX_ARTIST_MAX_SPRITE} (crop or pad it; never rescale pixel art)`
|
|
4424
|
+
);
|
|
4425
|
+
}
|
|
4426
|
+
return { image: { base64: bytes.toString("base64"), format: "png" }, side };
|
|
4427
|
+
}
|
|
4428
|
+
function vfxArtistFiles(raw) {
|
|
4429
|
+
const urlOf = (value) => {
|
|
4430
|
+
if (typeof value === "string") return /^https?:\/\//i.test(value) ? value : null;
|
|
4431
|
+
if (!value || typeof value !== "object") return null;
|
|
4432
|
+
for (const key of ["url", "download_url", "href"]) {
|
|
4433
|
+
const url = value[key];
|
|
4434
|
+
if (typeof url === "string" && /^https?:\/\//i.test(url)) return url;
|
|
4435
|
+
}
|
|
4436
|
+
return null;
|
|
4437
|
+
};
|
|
4438
|
+
const nameOf = (value, url, key) => {
|
|
4439
|
+
if (value && typeof value === "object") {
|
|
4440
|
+
for (const field of ["name", "filename", "file"]) {
|
|
4441
|
+
const name = value[field];
|
|
4442
|
+
if (typeof name === "string" && name) return name;
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
if (key && /\.[a-z0-9]+$/i.test(key)) return key;
|
|
4446
|
+
return decodeURIComponent(new URL(url).pathname.split("/").pop() ?? "");
|
|
4447
|
+
};
|
|
4448
|
+
const entries = Array.isArray(raw) ? raw.map((value) => [void 0, value]) : raw && typeof raw === "object" ? Object.entries(raw) : [];
|
|
4449
|
+
const files = [];
|
|
4450
|
+
for (const [key, value] of entries) {
|
|
4451
|
+
const url = urlOf(value);
|
|
4452
|
+
if (url) files.push({ name: nameOf(value, url, key), url });
|
|
4453
|
+
}
|
|
4454
|
+
return files;
|
|
4455
|
+
}
|
|
4456
|
+
function parseVfxArtistResult(done, jobId) {
|
|
4457
|
+
const files = vfxArtistFiles(done.files);
|
|
4458
|
+
const pick = (test, skip) => files.find((file) => test.test(file.name) && !skip?.test(file.name)) ?? null;
|
|
4459
|
+
const normals = /-normals?\.png$/i;
|
|
4460
|
+
const sheet = pick(/\.png$/i, normals);
|
|
4461
|
+
const effectId = typeof done.effect_id === "string" && done.effect_id ? done.effect_id : null;
|
|
4462
|
+
if (!effectId || !sheet) {
|
|
4463
|
+
throw new Error(
|
|
4464
|
+
`Invalid PixelLab response for vfx job ${jobId}: completed without ${effectId ? "a spritesheet among its files" : "an effect_id"} (last_response keys: ${Object.keys(done).join(", ") || "none"}; files: ${JSON.stringify(done.files ?? null).slice(0, 400)}); update parseVfxArtistResult in src/pixellab-vfx.ts with the real shape`
|
|
4465
|
+
);
|
|
4466
|
+
}
|
|
4467
|
+
const number = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
4468
|
+
const text = (value) => typeof value === "string" && value ? value : null;
|
|
4469
|
+
const canvas = done.canvas ?? {};
|
|
4470
|
+
const anchor = done.anchor;
|
|
4471
|
+
return {
|
|
4472
|
+
effectId,
|
|
4473
|
+
name: text(done.name),
|
|
4474
|
+
editUrl: text(done.edit_url),
|
|
4475
|
+
message: text(done.message),
|
|
4476
|
+
width: number(canvas.w),
|
|
4477
|
+
height: number(canvas.h),
|
|
4478
|
+
frames: number(done.frames),
|
|
4479
|
+
fps: number(done.fps),
|
|
4480
|
+
loop: typeof done.loop === "boolean" ? done.loop : null,
|
|
4481
|
+
anchor: anchor && number(anchor.x) !== null && number(anchor.y) !== null ? { kind: text(anchor.kind), x: anchor.x, y: anchor.y } : null,
|
|
4482
|
+
parts: done.parts ?? null,
|
|
4483
|
+
conversation: text(done.conversation),
|
|
4484
|
+
files,
|
|
4485
|
+
sheet,
|
|
4486
|
+
atlas: pick(/\.json$/i),
|
|
4487
|
+
gif: pick(/\.gif$/i),
|
|
4488
|
+
gamePack: pick(/\.zip$/i),
|
|
4489
|
+
normals: pick(normals)
|
|
4490
|
+
};
|
|
4491
|
+
}
|
|
4492
|
+
function vfxArtistFrameFacts(result, atlas) {
|
|
4493
|
+
const rects = vfxAtlasFrames(atlas);
|
|
4494
|
+
const frames = result.frames ?? rects?.length ?? null;
|
|
4495
|
+
const width = result.width ?? rects?.[0]?.w ?? null;
|
|
4496
|
+
const height = result.height ?? rects?.[0]?.h ?? null;
|
|
4497
|
+
if (!frames || !width || !height) {
|
|
4498
|
+
throw new Error(`PixelLab vfx effect ${result.effectId} came back with no frame size or count, in the result or its atlas`);
|
|
4499
|
+
}
|
|
4500
|
+
return { width, height, frames };
|
|
4501
|
+
}
|
|
4502
|
+
async function makeVfxWithArtist(client2, opts) {
|
|
4503
|
+
const paths = vfxOutputPaths(opts.out);
|
|
4504
|
+
if (!opts.force) {
|
|
4505
|
+
for (const file of [paths.sheet, paths.gif, paths.atlas, paths.provenance, ...opts.gamePack ? [paths.gamePack] : []]) {
|
|
4506
|
+
if (existsSync(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
const progress = opts.onProgress ?? (() => {
|
|
4510
|
+
});
|
|
4511
|
+
const sprite = opts.sprite ? vfxArtistSprite(await readFile(opts.sprite), opts.sprite).image : void 0;
|
|
4512
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
4513
|
+
const interval = opts.intervalMs ?? 6e3;
|
|
4514
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
4515
|
+
const submitted = await client2.createVfx({
|
|
4516
|
+
description: opts.description,
|
|
4517
|
+
artist: opts.artist,
|
|
4518
|
+
sprite,
|
|
4519
|
+
effectId: opts.effect,
|
|
4520
|
+
conversation: opts.effect ? opts.conversation : void 0,
|
|
4521
|
+
name: opts.name,
|
|
4522
|
+
gamePack: opts.gamePack
|
|
4523
|
+
});
|
|
4524
|
+
const jobId = submitted.background_job_id;
|
|
4525
|
+
progress(`submitted job ${jobId}, quoted ${submitted.quoted_generations ?? "?"} generations`);
|
|
4526
|
+
const started = Date.now();
|
|
4527
|
+
let job;
|
|
4528
|
+
for (; ; ) {
|
|
4529
|
+
job = await client2.getBackgroundJob(jobId);
|
|
4530
|
+
if (job.status === "completed") break;
|
|
4531
|
+
if (job.status === "failed") throw new Error(`PixelLab vfx job ${jobId} failed upstream${vfxJobFailure(job.last_response)}`);
|
|
4532
|
+
if (Date.now() - started > timeout) {
|
|
4533
|
+
throw new Error(
|
|
4534
|
+
`PixelLab vfx job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream (and bills when delivered), but this command has stopped waiting`
|
|
4535
|
+
);
|
|
4536
|
+
}
|
|
4537
|
+
progress(job.status);
|
|
4538
|
+
await sleep3(interval);
|
|
4539
|
+
}
|
|
4540
|
+
const result = parseVfxArtistResult(job.last_response ?? {}, jobId);
|
|
4541
|
+
await mkdir(path.dirname(paths.sheet), { recursive: true });
|
|
4542
|
+
const written = { sheet: paths.sheet, provenance: paths.provenance };
|
|
4543
|
+
await writeFile(paths.sheet, await client2.download(result.sheet.url));
|
|
4544
|
+
let atlas;
|
|
4545
|
+
if (result.atlas) {
|
|
4546
|
+
atlas = JSON.parse((await client2.download(result.atlas.url)).toString("utf8"));
|
|
4547
|
+
const meta = atlas.meta;
|
|
4548
|
+
if (meta) meta.image = path.basename(paths.sheet);
|
|
4549
|
+
await writeFile(paths.atlas, `${JSON.stringify(atlas, null, 2)}
|
|
4550
|
+
`);
|
|
4551
|
+
written.atlas = paths.atlas;
|
|
4552
|
+
}
|
|
4553
|
+
for (const [file, target, key] of [
|
|
4554
|
+
[result.gif, paths.gif, "gif"],
|
|
4555
|
+
[result.gamePack, paths.gamePack, "gamePack"],
|
|
4556
|
+
[result.normals, paths.normals, "normals"]
|
|
4557
|
+
]) {
|
|
4558
|
+
if (!file) continue;
|
|
4559
|
+
await writeFile(target, await client2.download(file.url));
|
|
4560
|
+
written[key] = target;
|
|
4561
|
+
}
|
|
4562
|
+
const facts = vfxArtistFrameFacts(result, atlas);
|
|
4563
|
+
const usage = job.usage ?? null;
|
|
4564
|
+
const provenance = {
|
|
4565
|
+
version: 1,
|
|
4566
|
+
provider: "pixellab",
|
|
4567
|
+
tool: "vfx",
|
|
4568
|
+
artist: opts.artist ?? "lite",
|
|
4569
|
+
description: opts.description,
|
|
4570
|
+
name: result.name,
|
|
4571
|
+
effectId: result.effectId,
|
|
4572
|
+
editUrl: result.editUrl,
|
|
4573
|
+
message: result.message,
|
|
4574
|
+
source: {
|
|
4575
|
+
...opts.sprite ? { sprite: path.relative(path.dirname(paths.provenance), path.resolve(opts.sprite)).split(path.sep).join("/") } : {},
|
|
4576
|
+
...opts.effect ? { changed: opts.effect } : {}
|
|
4577
|
+
},
|
|
4578
|
+
...facts,
|
|
4579
|
+
fps: result.fps,
|
|
4580
|
+
loop: result.loop,
|
|
4581
|
+
anchor: result.anchor,
|
|
4582
|
+
parts: result.parts,
|
|
4583
|
+
files: Object.fromEntries(
|
|
4584
|
+
["sheet", "gif", "atlas", "gamePack", "normals"].filter((key) => written[key]).map((key) => [key, path.basename(written[key])])
|
|
4585
|
+
),
|
|
4586
|
+
quotedGenerations: submitted.quoted_generations ?? null,
|
|
4587
|
+
usage,
|
|
4588
|
+
conversation: result.conversation,
|
|
4589
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4590
|
+
};
|
|
4591
|
+
await writeFile(paths.provenance, `${JSON.stringify(provenance, null, 2)}
|
|
4592
|
+
`);
|
|
4593
|
+
return {
|
|
4594
|
+
jobId,
|
|
4595
|
+
effectId: result.effectId,
|
|
4596
|
+
editUrl: result.editUrl,
|
|
4597
|
+
name: result.name,
|
|
4598
|
+
message: result.message,
|
|
4599
|
+
...facts,
|
|
4600
|
+
fps: result.fps,
|
|
4601
|
+
loop: result.loop,
|
|
4602
|
+
anchor: result.anchor,
|
|
4603
|
+
quotedGenerations: submitted.quoted_generations ?? null,
|
|
4604
|
+
usage,
|
|
4605
|
+
paths: written
|
|
4606
|
+
};
|
|
4607
|
+
}
|
|
4608
|
+
function vfxJobFailure(done) {
|
|
4609
|
+
for (const key of ["error", "detail", "message"]) {
|
|
4610
|
+
const value = done?.[key];
|
|
4611
|
+
if (typeof value === "string" && value) return `: ${value}`;
|
|
4612
|
+
}
|
|
4613
|
+
return "";
|
|
4614
|
+
}
|
|
4615
|
+
async function readVfxConversation(file, effectId) {
|
|
4616
|
+
if (!existsSync(file)) return void 0;
|
|
4617
|
+
try {
|
|
4618
|
+
const record = JSON.parse(await readFile(file, "utf8"));
|
|
4619
|
+
return record.effectId === effectId && typeof record.conversation === "string" ? record.conversation : void 0;
|
|
4620
|
+
} catch {
|
|
4621
|
+
return void 0;
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4625
|
+
// src/source-url.ts
|
|
4626
|
+
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
4627
|
+
"access_token",
|
|
4628
|
+
"apikey",
|
|
4629
|
+
"api_key",
|
|
4630
|
+
"auth",
|
|
4631
|
+
"authorization",
|
|
4632
|
+
"awsaccesskeyid",
|
|
4633
|
+
"googleaccessid",
|
|
4634
|
+
"key",
|
|
4635
|
+
"sig"
|
|
4636
|
+
]);
|
|
4637
|
+
function isSensitiveSourceUrl(value) {
|
|
4638
|
+
let parsed;
|
|
4639
|
+
try {
|
|
4640
|
+
parsed = new URL(value);
|
|
4641
|
+
} catch {
|
|
4642
|
+
return false;
|
|
4643
|
+
}
|
|
4644
|
+
if (parsed.username || parsed.password) return true;
|
|
4645
|
+
for (const key of parsed.searchParams.keys()) {
|
|
4646
|
+
const normalized = key.toLowerCase();
|
|
4647
|
+
if (SENSITIVE_QUERY_KEYS.has(normalized) || normalized.includes("credential") || normalized.includes("signature") || normalized.includes("security-token") || normalized.includes("token")) return true;
|
|
4648
|
+
}
|
|
4649
|
+
return false;
|
|
4650
|
+
}
|
|
4651
|
+
function shouldPersistSourceUrl(value) {
|
|
4652
|
+
let parsed;
|
|
4653
|
+
try {
|
|
4654
|
+
parsed = new URL(value);
|
|
4655
|
+
} catch {
|
|
4656
|
+
return true;
|
|
4657
|
+
}
|
|
4658
|
+
if (parsed.protocol === "data:" || parsed.protocol === "file:") return false;
|
|
4659
|
+
return !isSensitiveSourceUrl(value);
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
// src/pixellab-utils.ts
|
|
4663
|
+
import { existsSync as existsSync2 } from "fs";
|
|
4664
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
4665
|
+
import path2 from "path";
|
|
4666
|
+
var UNZOOM_MIN_SIDE = 256;
|
|
4667
|
+
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
4668
|
+
function defaultUnzoomOut(input) {
|
|
4669
|
+
const ext = path2.extname(input);
|
|
4670
|
+
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
4671
|
+
}
|
|
4672
|
+
async function unzoomFile(client2, input, opts = {}) {
|
|
4673
|
+
const out = path2.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
4674
|
+
if (path2.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
4675
|
+
if (existsSync2(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
4676
|
+
const bytes = await readFile2(input);
|
|
4677
|
+
const metadata = imageMetadata(bytes);
|
|
4678
|
+
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
4679
|
+
const { width, height, format } = metadata;
|
|
4680
|
+
if (width < UNZOOM_MIN_SIDE || height < UNZOOM_MIN_SIDE) {
|
|
4681
|
+
throw new Error(
|
|
4682
|
+
`${input} is ${width}x${height}; PixelLab's unzoom needs at least ${UNZOOM_MIN_SIDE}x${UNZOOM_MIN_SIDE} to find the pixel grid, so this is probably already at its native size`
|
|
4683
|
+
);
|
|
4684
|
+
}
|
|
4685
|
+
if (width * height > UNZOOM_MAX_AREA) {
|
|
4686
|
+
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
4687
|
+
}
|
|
4688
|
+
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
4689
|
+
await mkdir2(path2.dirname(out), { recursive: true });
|
|
4690
|
+
await writeFile2(out, res.png);
|
|
4691
|
+
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
4692
|
+
}
|
|
4693
|
+
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
4694
|
+
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
4695
|
+
var FONT_COST_GENERATIONS = 15;
|
|
4696
|
+
function fontOutputPaths(out) {
|
|
4697
|
+
const base = path2.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
4698
|
+
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
4699
|
+
}
|
|
4700
|
+
async function generateFont(client2, opts) {
|
|
4701
|
+
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
4702
|
+
if (!opts.force) {
|
|
4703
|
+
for (const file of [ttf, atlas]) {
|
|
4704
|
+
if (existsSync2(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
4708
|
+
const interval = opts.intervalMs ?? 5e3;
|
|
4709
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
4710
|
+
const submitted = await client2.generateFontPro({
|
|
4711
|
+
description: opts.description,
|
|
4712
|
+
weight: opts.weight,
|
|
4713
|
+
glyphPx: opts.glyphPx,
|
|
4714
|
+
seed: opts.seed,
|
|
4715
|
+
fontName: opts.fontName
|
|
4716
|
+
});
|
|
4717
|
+
const jobId = submitted.background_job_id;
|
|
4718
|
+
const started = Date.now();
|
|
4719
|
+
for (; ; ) {
|
|
4720
|
+
const job = await client2.getFontJob(jobId);
|
|
4721
|
+
opts.onProgress?.(job.status);
|
|
4722
|
+
if (job.status === "failed") throw new Error(`PixelLab font job ${jobId} failed upstream`);
|
|
4723
|
+
if (job.status === "completed") {
|
|
4724
|
+
if (!job.download_ttf_url || !job.download_atlas_url) {
|
|
4725
|
+
throw new Error(`PixelLab font job ${jobId} completed without both download URLs`);
|
|
4726
|
+
}
|
|
4727
|
+
const [ttfBytes, atlasBytes] = await Promise.all([
|
|
4728
|
+
client2.download(job.download_ttf_url),
|
|
4729
|
+
client2.download(job.download_atlas_url)
|
|
4730
|
+
]);
|
|
4731
|
+
await mkdir2(path2.dirname(ttf), { recursive: true });
|
|
4732
|
+
await writeFile2(ttf, ttfBytes);
|
|
4733
|
+
await writeFile2(atlas, atlasBytes);
|
|
4734
|
+
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
4735
|
+
}
|
|
4736
|
+
if (Date.now() - started > timeout) {
|
|
4737
|
+
throw new Error(
|
|
4738
|
+
`PixelLab font job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
4739
|
+
);
|
|
4740
|
+
}
|
|
4741
|
+
await sleep3(interval);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
var PIXELATE_COST_GENERATIONS = 6;
|
|
4745
|
+
var PIXELATE_MAX_SIDE = 2048;
|
|
4746
|
+
function defaultPixelateOut(input) {
|
|
4747
|
+
const ext = path2.extname(input);
|
|
4748
|
+
return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
|
|
4749
|
+
}
|
|
4750
|
+
async function pixelateFile(client2, input, opts = {}) {
|
|
4751
|
+
const out = path2.resolve(opts.out ?? defaultPixelateOut(input));
|
|
4752
|
+
if (path2.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
|
|
4753
|
+
if (existsSync2(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
4754
|
+
const bytes = await readFile2(input);
|
|
4755
|
+
const metadata = imageMetadata(bytes);
|
|
4756
|
+
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
4757
|
+
const { width, height, format } = metadata;
|
|
4758
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
4759
|
+
const interval = opts.intervalMs ?? 5e3;
|
|
4760
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
4761
|
+
const submitted = await client2.imageToPixelartProFlash({
|
|
4762
|
+
image: { base64: bytes.toString("base64"), format },
|
|
4763
|
+
description: opts.description,
|
|
4764
|
+
seed: opts.seed
|
|
4765
|
+
});
|
|
4766
|
+
const jobId = submitted.background_job_id;
|
|
4767
|
+
const started = Date.now();
|
|
4768
|
+
for (; ; ) {
|
|
4769
|
+
const job = await client2.getBackgroundJob(jobId);
|
|
4770
|
+
opts.onProgress?.(job.status);
|
|
4771
|
+
if (job.status === "failed") throw new Error(`PixelLab pixelate job ${jobId} failed upstream`);
|
|
4772
|
+
if (job.status === "completed") {
|
|
4773
|
+
const images = job.last_response?.images;
|
|
4774
|
+
const first = Array.isArray(images) ? images[0] : void 0;
|
|
4775
|
+
if (!first || typeof first.base64 !== "string" || !first.base64) {
|
|
4776
|
+
throw new Error(`PixelLab pixelate job ${jobId} completed without an image at last_response.images[0]`);
|
|
4777
|
+
}
|
|
4778
|
+
const png = Buffer.from(first.base64, "base64");
|
|
4779
|
+
const size = imageMetadata(png);
|
|
4780
|
+
await mkdir2(path2.dirname(out), { recursive: true });
|
|
4781
|
+
await writeFile2(out, png);
|
|
4782
|
+
return {
|
|
4783
|
+
jobId,
|
|
4784
|
+
out,
|
|
4785
|
+
original: { width, height },
|
|
4786
|
+
pixelated: { width: size?.width ?? Number(first.width), height: size?.height ?? Number(first.height) },
|
|
4787
|
+
imageId: submitted.image_id,
|
|
4788
|
+
usage: job.usage ?? null
|
|
4789
|
+
};
|
|
4790
|
+
}
|
|
4791
|
+
if (Date.now() - started > timeout) {
|
|
4792
|
+
throw new Error(
|
|
4793
|
+
`PixelLab pixelate job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
4794
|
+
);
|
|
4795
|
+
}
|
|
4796
|
+
await sleep3(interval);
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
var NORMAL_MAP_COST_GENERATIONS = 0.5;
|
|
4800
|
+
var NORMAL_MAP_MIN_SIDE = 8;
|
|
4801
|
+
var NORMAL_MAP_MAX_SIDE = 512;
|
|
4802
|
+
var NORMAL_MAP_MAX_FRAMES = 32;
|
|
4803
|
+
var NORMAL_MAP_CONVENTIONS = ["opengl", "directx"];
|
|
4804
|
+
function normalMapSizeProblem(width, height, frames = 1) {
|
|
4805
|
+
if (!Number.isInteger(frames) || frames < 1 || frames > NORMAL_MAP_MAX_FRAMES) {
|
|
4806
|
+
return `PixelLab's normal map takes 1 to ${NORMAL_MAP_MAX_FRAMES} frames, not ${frames}`;
|
|
4807
|
+
}
|
|
4808
|
+
if (width % frames !== 0) return `a ${width}px-wide strip does not split into ${frames} equal frames`;
|
|
4809
|
+
const frameWidth = width / frames;
|
|
4810
|
+
if (frameWidth < NORMAL_MAP_MIN_SIDE || height < NORMAL_MAP_MIN_SIDE || frameWidth > NORMAL_MAP_MAX_SIDE || height > NORMAL_MAP_MAX_SIDE) {
|
|
4811
|
+
return `${frames > 1 ? "each frame is " : "it is "}${frameWidth}x${height}; PixelLab's normal map takes ${NORMAL_MAP_MIN_SIDE} to ${NORMAL_MAP_MAX_SIDE} px per side`;
|
|
4812
|
+
}
|
|
4813
|
+
return null;
|
|
4814
|
+
}
|
|
4815
|
+
function defaultNormalMapOut(input) {
|
|
4816
|
+
const ext = path2.extname(input);
|
|
4817
|
+
return `${ext ? input.slice(0, -ext.length) : input}.normal.png`;
|
|
4818
|
+
}
|
|
4819
|
+
function normalMapResultPng(lastResponse) {
|
|
4820
|
+
const image = lastResponse?.image;
|
|
4821
|
+
const base64 = typeof image === "string" ? image : image?.base64;
|
|
4822
|
+
if (typeof base64 !== "string" || !base64) return null;
|
|
4823
|
+
return Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ""), "base64");
|
|
4824
|
+
}
|
|
4825
|
+
async function normalMapFile(client2, input, opts = {}) {
|
|
4826
|
+
const out = path2.resolve(opts.out ?? defaultNormalMapOut(input));
|
|
4827
|
+
if (path2.resolve(input) === out) throw new Error("normal-map will not overwrite its own input; pass a different --out");
|
|
4828
|
+
if (existsSync2(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
4829
|
+
const bytes = await readFile2(input);
|
|
4830
|
+
const metadata = imageMetadata(bytes);
|
|
4831
|
+
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
4832
|
+
if (metadata.format !== "png") throw new Error(`${input} is a JPEG; normal-map reads a PNG, whose alpha marks where the sprite is`);
|
|
4833
|
+
const { width, height } = metadata;
|
|
4834
|
+
const frames = opts.frames ?? 1;
|
|
4835
|
+
const problem = normalMapSizeProblem(width, height, frames);
|
|
4836
|
+
if (problem) throw new Error(`${input}: ${problem}`);
|
|
4837
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
4838
|
+
const interval = opts.intervalMs ?? 5e3;
|
|
4839
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
4840
|
+
const submitted = await client2.normalMap({
|
|
4841
|
+
image: { base64: bytes.toString("base64"), format: "png" },
|
|
4842
|
+
frames,
|
|
4843
|
+
convention: opts.convention
|
|
4844
|
+
});
|
|
4845
|
+
const jobId = submitted.background_job_id;
|
|
4846
|
+
const started = Date.now();
|
|
4847
|
+
for (; ; ) {
|
|
4848
|
+
const job = await client2.getBackgroundJob(jobId);
|
|
4849
|
+
opts.onProgress?.(job.status);
|
|
4850
|
+
if (job.status === "failed") throw new Error(`PixelLab normal-map job ${jobId} failed upstream`);
|
|
4851
|
+
if (job.status === "completed") {
|
|
4852
|
+
const png = normalMapResultPng(job.last_response);
|
|
4853
|
+
if (!png) throw new Error(`PixelLab normal-map job ${jobId} completed without an image at last_response.image`);
|
|
4854
|
+
await mkdir2(path2.dirname(out), { recursive: true });
|
|
4855
|
+
await writeFile2(out, png);
|
|
4856
|
+
const size = imageMetadata(png);
|
|
4857
|
+
return {
|
|
4858
|
+
jobId,
|
|
4859
|
+
out,
|
|
4860
|
+
width: size?.width ?? width,
|
|
4861
|
+
height: size?.height ?? height,
|
|
4862
|
+
frames,
|
|
4863
|
+
convention: opts.convention ?? "opengl",
|
|
4864
|
+
usage: job.usage ?? null
|
|
4865
|
+
};
|
|
4866
|
+
}
|
|
4867
|
+
if (Date.now() - started > timeout) {
|
|
4868
|
+
throw new Error(
|
|
4869
|
+
`PixelLab normal-map job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
4870
|
+
);
|
|
4871
|
+
}
|
|
4872
|
+
await sleep3(interval);
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4071
4875
|
|
|
4072
4876
|
// src/providers/pixellab.ts
|
|
4073
4877
|
function pixelLabObjectUrl(generator, objectId) {
|
|
@@ -4086,6 +4890,20 @@ function pixelLabObjectUrl(generator, objectId) {
|
|
|
4086
4890
|
if (generator === "vfx") return `https://pixellab.ai/create-vfx?effect=${encodeURIComponent(objectId)}`;
|
|
4087
4891
|
return null;
|
|
4088
4892
|
}
|
|
4893
|
+
function vfxArtistSpecQuote(spec) {
|
|
4894
|
+
const sprite = spec.vfx?.sprite;
|
|
4895
|
+
const side = !sprite ? "none" : "file" in sprite && sprite.width != null && sprite.height != null ? Math.max(sprite.width, sprite.height) : "unknown";
|
|
4896
|
+
return vfxArtistQuote(spec.vfx?.artist ?? "lite", side);
|
|
4897
|
+
}
|
|
4898
|
+
function workbenchVfxUrls(exported) {
|
|
4899
|
+
const sheetFile = exported.files.find((file) => /\.png$/i.test(file));
|
|
4900
|
+
const atlasFile = exported.files.find((file) => /\.json$/i.test(file));
|
|
4901
|
+
if (!sheetFile) throw new Error(`PixelLab's vfx export listed no spritesheet: ${exported.files.join(", ")}`);
|
|
4902
|
+
return {
|
|
4903
|
+
sheet: `${exported.downloadBase}${encodeURIComponent(sheetFile)}`,
|
|
4904
|
+
...atlasFile ? { atlas: `${exported.downloadBase}${encodeURIComponent(atlasFile)}` } : {}
|
|
4905
|
+
};
|
|
4906
|
+
}
|
|
4089
4907
|
function billedFromUsage(usage) {
|
|
4090
4908
|
if (!usage) return null;
|
|
4091
4909
|
if (usage.type === "usd" || usage.type === void 0 && typeof usage.usd === "number") {
|
|
@@ -4240,13 +5058,14 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4240
5058
|
* source from a supplied per-frame skeleton instead of a text description)
|
|
4241
5059
|
* — and `interpolate` (`/interpolation-v2`, the in-betweens
|
|
4242
5060
|
* from one keyframe to another) and `edit-animation`
|
|
4243
|
-
* (`/edit-animation-v2`, one text edit applied across a whole frame set)
|
|
5061
|
+
* (`/edit-animation-v2`, one text edit applied across a whole frame set),
|
|
5062
|
+
* and `normal-map` (`/normal-map`, lighting data for a sprite or a loop).
|
|
4244
5063
|
* `outpaint` does not: there is no canvas-expansion endpoint in the API,
|
|
4245
5064
|
* matching docs/REVISIONS.md's note that no provider ships a tested
|
|
4246
5065
|
* outpaint path yet.
|
|
4247
5066
|
*/
|
|
4248
5067
|
supportsRevision(mode2) {
|
|
4249
|
-
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "remove-background" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "animate-skeleton" || mode2 === "interpolate" || mode2 === "edit-animation";
|
|
5068
|
+
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "remove-background" || mode2 === "normal-map" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "animate-skeleton" || mode2 === "interpolate" || mode2 === "edit-animation";
|
|
4250
5069
|
}
|
|
4251
5070
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
4252
5071
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
@@ -4266,11 +5085,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4266
5085
|
* is a file read.
|
|
4267
5086
|
*/
|
|
4268
5087
|
static cacheDir(namespace = "pixflux") {
|
|
4269
|
-
const dir =
|
|
5088
|
+
const dir = path3.join(os.tmpdir(), `pixelkiln-${namespace}`);
|
|
4270
5089
|
mkdirSync(dir, { recursive: true });
|
|
4271
5090
|
return dir;
|
|
4272
5091
|
}
|
|
4273
5092
|
estimate(spec) {
|
|
5093
|
+
if (spec.vfx?.description) return { unit: "generations", amount: vfxArtistCeiling(vfxArtistSpecQuote(spec)), candidates: 1 };
|
|
4274
5094
|
if (spec.vfx) return { unit: "generations", amount: 0, candidates: 1 };
|
|
4275
5095
|
if (spec.batch?.role === "member") {
|
|
4276
5096
|
return { unit: "generations", amount: 0, candidates: 1 };
|
|
@@ -4278,6 +5098,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4278
5098
|
if (spec.revision?.mode === "remove-background") {
|
|
4279
5099
|
return { unit: "generations", amount: 1, candidates: 1 };
|
|
4280
5100
|
}
|
|
5101
|
+
if (spec.revision?.mode === "normal-map") {
|
|
5102
|
+
return { unit: "generations", amount: NORMAL_MAP_COST_GENERATIONS, candidates: 1 };
|
|
5103
|
+
}
|
|
4281
5104
|
if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
|
|
4282
5105
|
return { unit: "generations", amount: 0.1, candidates: 1 };
|
|
4283
5106
|
}
|
|
@@ -4332,12 +5155,40 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4332
5155
|
if (spec.generator === "vfx" && styleImages.length) {
|
|
4333
5156
|
throw new Error("PixelLab vfx takes no style images; an effect's look comes from its preset, parts, or recipe");
|
|
4334
5157
|
}
|
|
5158
|
+
const sprite = spec.vfx?.description ? spec.vfx.sprite : void 0;
|
|
5159
|
+
if (sprite && "reference" in sprite) {
|
|
5160
|
+
throw new Error("PixelLab's VFX artist takes a sprite's pixels, not a PixelLab reference; point vfx.sprite at a PNG file");
|
|
5161
|
+
}
|
|
5162
|
+
if (sprite?.width != null && sprite.height != null && Math.max(sprite.width, sprite.height) > VFX_ARTIST_MAX_SPRITE) {
|
|
5163
|
+
throw new Error(
|
|
5164
|
+
`vfx sprite ${sprite.file} is ${sprite.width}x${sprite.height}; PixelLab's VFX artist takes up to ${VFX_ARTIST_MAX_SPRITE}x${VFX_ARTIST_MAX_SPRITE} (crop or pad it; never rescale pixel art)`
|
|
5165
|
+
);
|
|
5166
|
+
}
|
|
4335
5167
|
const members = spec.revision?.sourceMembers;
|
|
4336
5168
|
if (spec.revision && members && !MEMBER_SET_REVISION_MODES.includes(spec.revision.mode)) {
|
|
4337
5169
|
throw new Error(
|
|
4338
|
-
`PixelLab ${spec.revision.mode} reads one source image; ${spec.revision.sourceAssetId} is a ${members.length}-member set (${members[0].role}, ...). Revise one member through a declared source, or use reduce-colors, correct-pixelart,
|
|
5170
|
+
`PixelLab ${spec.revision.mode} reads one source image; ${spec.revision.sourceAssetId} is a ${members.length}-member set (${members[0].role}, ...). Revise one member through a declared source, or use reduce-colors, correct-pixelart, edit-animation, or normal-map, which take the whole set`
|
|
4339
5171
|
);
|
|
4340
5172
|
}
|
|
5173
|
+
if (spec.revision?.mode === "normal-map") {
|
|
5174
|
+
const { sourceWidth: width, sourceHeight: height, sourceFormat, sourceAssetId } = spec.revision;
|
|
5175
|
+
if (members && !members.every((member) => member.role.startsWith("frame-"))) {
|
|
5176
|
+
throw new Error(
|
|
5177
|
+
`PixelLab normal-map reads one sprite or one animation loop; ${sourceAssetId} is a set of directions (${members.map((member) => member.role).join(", ")}). Make one normal-map revision per direction, each from an asset whose \`source\` is that direction's file`
|
|
5178
|
+
);
|
|
5179
|
+
}
|
|
5180
|
+
if (members && members.length > NORMAL_MAP_MAX_FRAMES) {
|
|
5181
|
+
throw new Error(
|
|
5182
|
+
`PixelLab normal-map takes at most ${NORMAL_MAP_MAX_FRAMES} frames in one strip; ${sourceAssetId} has ${members.length}`
|
|
5183
|
+
);
|
|
5184
|
+
}
|
|
5185
|
+
if (sourceFormat === "jpeg") {
|
|
5186
|
+
throw new Error(`PixelLab normal-map reads a PNG, whose alpha marks where the sprite is; ${sourceAssetId} is a JPEG`);
|
|
5187
|
+
}
|
|
5188
|
+
const frames = members?.length ?? 1;
|
|
5189
|
+
const problem = width != null && height != null ? normalMapSizeProblem(width * frames, height, frames) : null;
|
|
5190
|
+
if (problem) throw new Error(`PixelLab normal-map source ${sourceAssetId}: ${problem}`);
|
|
5191
|
+
}
|
|
4341
5192
|
if (spec.revision?.engine === "pro-flash") {
|
|
4342
5193
|
const { sourceWidth: width, sourceHeight: height, mode: mode2 } = spec.revision;
|
|
4343
5194
|
if (width != null && height != null && (width < 32 || height < 32 || width > 256 || height > 256 || width % 4 || height % 4)) {
|
|
@@ -4445,6 +5296,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4445
5296
|
`PixelLab imagePro is ${spec.width}x${spec.height}; the API takes 16 to 792 wide and 16 to 688 tall (the exact ceiling also depends on aspect ratio)`
|
|
4446
5297
|
);
|
|
4447
5298
|
}
|
|
5299
|
+
if (spec.generator === "imagePro" && !spec.revision && spec.palette.length && (spec.palette.length < 2 || spec.palette.length > 256)) {
|
|
5300
|
+
throw new Error(`PixelLab imagePro's target palette takes 2 to 256 colours; the style's palette has ${spec.palette.length}`);
|
|
5301
|
+
}
|
|
4448
5302
|
if (spec.generator === "uiAsset" && (spec.width < 192 || spec.height < 192 || spec.width > 688 || spec.height > 688)) {
|
|
4449
5303
|
throw new Error(
|
|
4450
5304
|
`PixelLab uiAsset is ${spec.width}x${spec.height}; the API takes 192 to 688 pixels per side (the exact ceiling also depends on aspect ratio \u2014 square tops out at 512, 16:9 at 688x384, 9:16 at 384x688, 4:3 at 600x448, 3:4 at 448x600)`
|
|
@@ -4511,24 +5365,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4511
5365
|
if (spec.detail) {
|
|
4512
5366
|
requirePixelLabOption("terrain", "detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
|
|
4513
5367
|
}
|
|
4514
|
-
if (spec.
|
|
4515
|
-
|
|
4516
|
-
"PixelLab terrain: terrainShapeStyle is standard-mode only; pro's own shape controls are terrainSpreadX/terrainSlopeSize/terrainRaggedness"
|
|
4517
|
-
);
|
|
4518
|
-
}
|
|
4519
|
-
if (spec.terrainTileSize === 64 && spec.terrainMode !== "pro") {
|
|
4520
|
-
throw new Error('PixelLab terrain: a 64px tile needs terrainMode: "pro"');
|
|
4521
|
-
}
|
|
4522
|
-
if (spec.terrainShapeStyle && spec.terrainTransitionSize != null && spec.terrainTransitionSize > 0.5) {
|
|
4523
|
-
throw new Error(
|
|
4524
|
-
"PixelLab terrain: terrainShapeStyle with terrainTransitionSize above 0.5 uses an extended 32-tile layout pixelkiln does not model; use 0, 0.25, or 0.5"
|
|
4525
|
-
);
|
|
4526
|
-
}
|
|
4527
|
-
if (!spec.terrainShapeStyle && spec.terrainTransitionSize != null && ![0, 0.25, 0.5, 1].includes(spec.terrainTransitionSize)) {
|
|
4528
|
-
throw new Error(
|
|
4529
|
-
"PixelLab terrain: terrainTransitionSize must be 0, 0.25, 0.5, or 1 unless terrainShapeStyle is set"
|
|
4530
|
-
);
|
|
4531
|
-
}
|
|
5368
|
+
if (spec.terrainOrientation === "sidescroller") this.validateSidescrollerTerrain(spec);
|
|
5369
|
+
else this.validateTopDownTerrain(spec);
|
|
4532
5370
|
}
|
|
4533
5371
|
if (spec.generator === "isometricTile") {
|
|
4534
5372
|
if (spec.outline) {
|
|
@@ -4587,6 +5425,87 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4587
5425
|
);
|
|
4588
5426
|
}
|
|
4589
5427
|
}
|
|
5428
|
+
/** `/create-tileset`'s own rules, beyond the enums both terrain endpoints share. */
|
|
5429
|
+
validateTopDownTerrain(spec) {
|
|
5430
|
+
if (spec.terrainShapeStyle && spec.terrainMode === "pro") {
|
|
5431
|
+
throw new Error(
|
|
5432
|
+
"PixelLab terrain: terrainShapeStyle is standard-mode only; pro's own shape controls are terrainRaggedness/terrainRoundness/terrainBoundaryAmplitude/terrainStepSlope"
|
|
5433
|
+
);
|
|
5434
|
+
}
|
|
5435
|
+
const pro = spec.terrainMode === "pro";
|
|
5436
|
+
if (spec.terrainRoundness != null && !pro && !spec.terrainShapeStyle) {
|
|
5437
|
+
throw new Error('PixelLab terrain: terrainRoundness needs terrainMode: "pro" or a terrainShapeStyle');
|
|
5438
|
+
}
|
|
5439
|
+
if (spec.terrainRoundness != null && spec.terrainRoundness > 1 && !pro) {
|
|
5440
|
+
throw new Error('PixelLab terrain: terrainRoundness above 1 (corners cut toward 45\xB0) needs terrainMode: "pro"');
|
|
5441
|
+
}
|
|
5442
|
+
if (spec.terrainBoundaryAmplitude != null && !pro && !spec.terrainShapeStyle) {
|
|
5443
|
+
throw new Error('PixelLab terrain: terrainBoundaryAmplitude needs terrainMode: "pro" or a terrainShapeStyle');
|
|
5444
|
+
}
|
|
5445
|
+
if (spec.terrainBoundarySeed != null && spec.terrainBoundaryAmplitude == null) {
|
|
5446
|
+
throw new Error("PixelLab terrain: terrainBoundarySeed shapes terrainBoundaryAmplitude's border; set both or neither");
|
|
5447
|
+
}
|
|
5448
|
+
if (spec.terrainStepSlope != null && !(pro && spec.terrainTransitionSize === 0.25)) {
|
|
5449
|
+
throw new Error('PixelLab terrain: terrainStepSlope needs terrainMode: "pro" with terrainTransitionSize 0.25');
|
|
5450
|
+
}
|
|
5451
|
+
if (spec.terrainConnectDiagonals != null && !pro) {
|
|
5452
|
+
throw new Error('PixelLab terrain: terrainConnectDiagonals needs terrainMode: "pro"');
|
|
5453
|
+
}
|
|
5454
|
+
if (spec.terrainTileSize === 64 && spec.terrainMode !== "pro") {
|
|
5455
|
+
throw new Error('PixelLab terrain: a 64px tile needs terrainMode: "pro"');
|
|
5456
|
+
}
|
|
5457
|
+
if (spec.terrainShapeStyle && spec.terrainTransitionSize != null && spec.terrainTransitionSize > 0.5) {
|
|
5458
|
+
throw new Error(
|
|
5459
|
+
"PixelLab terrain: terrainShapeStyle with terrainTransitionSize above 0.5 uses an extended 32-tile layout pixelkiln does not model; use 0, 0.25, or 0.5"
|
|
5460
|
+
);
|
|
5461
|
+
}
|
|
5462
|
+
if (!spec.terrainShapeStyle && spec.terrainTransitionSize != null && ![0, 0.25, 0.5, 1].includes(spec.terrainTransitionSize)) {
|
|
5463
|
+
throw new Error(
|
|
5464
|
+
"PixelLab terrain: terrainTransitionSize must be 0, 0.25, 0.5, or 1 unless terrainShapeStyle is set"
|
|
5465
|
+
);
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
5468
|
+
/**
|
|
5469
|
+
* `/create-tileset-sidescroller` takes a platform, its top layer, and the
|
|
5470
|
+
* layer's height, and nothing of the top-down endpoint's mode, camera, or
|
|
5471
|
+
* shape controls; sending one would be silently dropped, so it is refused.
|
|
5472
|
+
*/
|
|
5473
|
+
validateSidescrollerTerrain(spec) {
|
|
5474
|
+
const topDownOnly = {
|
|
5475
|
+
terrainMode: spec.terrainMode,
|
|
5476
|
+
terrainShapeStyle: spec.terrainShapeStyle,
|
|
5477
|
+
terrainView: spec.terrainView,
|
|
5478
|
+
terrainSpreadX: spec.terrainSpreadX,
|
|
5479
|
+
terrainSlopeSize: spec.terrainSlopeSize,
|
|
5480
|
+
terrainRaggedness: spec.terrainRaggedness,
|
|
5481
|
+
terrainRoundness: spec.terrainRoundness,
|
|
5482
|
+
terrainBoundaryAmplitude: spec.terrainBoundaryAmplitude,
|
|
5483
|
+
terrainBoundarySeed: spec.terrainBoundarySeed,
|
|
5484
|
+
terrainStepSlope: spec.terrainStepSlope,
|
|
5485
|
+
terrainConnectDiagonals: spec.terrainConnectDiagonals
|
|
5486
|
+
};
|
|
5487
|
+
const set = Object.entries(topDownOnly).filter(([, value]) => value != null).map(([field]) => field);
|
|
5488
|
+
if (set.length) {
|
|
5489
|
+
throw new Error(`PixelLab terrain: a sidescroller set takes no ${set.join(", ")}; those are top-down controls`);
|
|
5490
|
+
}
|
|
5491
|
+
if (spec.terrainTileSize !== 16 && spec.terrainTileSize !== 32) {
|
|
5492
|
+
throw new Error(`PixelLab terrain: a sidescroller set's tiles are 16 or 32px, got ${spec.terrainTileSize}`);
|
|
5493
|
+
}
|
|
5494
|
+
if (spec.terrainTransitionSize != null && ![0, 0.25, 0.5, 1].includes(spec.terrainTransitionSize)) {
|
|
5495
|
+
throw new Error("PixelLab terrain: a sidescroller set's terrainTransitionSize must be 0, 0.25, 0.5, or 1");
|
|
5496
|
+
}
|
|
5497
|
+
if (spec.terrainConnect?.upper) {
|
|
5498
|
+
throw new Error(
|
|
5499
|
+
"PixelLab terrain: a sidescroller set's upper terrain is empty air; only its lower (platform) terrain can connect"
|
|
5500
|
+
);
|
|
5501
|
+
}
|
|
5502
|
+
const link = spec.terrainConnect?.lower;
|
|
5503
|
+
if (link?.terrain === "upper") {
|
|
5504
|
+
throw new Error(
|
|
5505
|
+
`PixelLab terrain: ${link.assetId}'s upper terrain is empty air; connect to its lower (platform) terrain`
|
|
5506
|
+
);
|
|
5507
|
+
}
|
|
5508
|
+
}
|
|
4590
5509
|
validateCharacter(spec, styleImages = []) {
|
|
4591
5510
|
const character = spec.character;
|
|
4592
5511
|
if (!character) throw new Error(`${spec.styleId}/${spec.assetId} has no character shape`);
|
|
@@ -4616,7 +5535,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4616
5535
|
}
|
|
4617
5536
|
}
|
|
4618
5537
|
if (endFrame) {
|
|
4619
|
-
const start = startFrame ?? (character.parentFile &&
|
|
5538
|
+
const start = startFrame ?? (character.parentFile && existsSync3(character.parentFile) ? imageMetadata(readFileSync(character.parentFile)) : null);
|
|
4620
5539
|
if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
|
|
4621
5540
|
throw new Error(
|
|
4622
5541
|
`${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`
|
|
@@ -4774,7 +5693,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
4774
5693
|
}
|
|
4775
5694
|
}
|
|
4776
5695
|
if (endFrame) {
|
|
4777
|
-
const start = startFrame ?? (object.parentFile &&
|
|
5696
|
+
const start = startFrame ?? (object.parentFile && existsSync3(object.parentFile) ? imageMetadata(readFileSync(object.parentFile)) : null);
|
|
4778
5697
|
if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
|
|
4779
5698
|
throw new Error(
|
|
4780
5699
|
`${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`
|
|
@@ -5084,7 +6003,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5084
6003
|
const sources = images.map((image, index) => {
|
|
5085
6004
|
if (!image.base64) throw new Error(`outfit transfer frame ${index} has no image data`);
|
|
5086
6005
|
const role = meta?.frameRoles?.[index] ?? void 0;
|
|
5087
|
-
const file =
|
|
6006
|
+
const file = path3.join(dir, `${jobId}-${index}.png`);
|
|
5088
6007
|
writeFileSync(file, Buffer.from(image.base64, "base64"));
|
|
5089
6008
|
return { url: `file://${file}`, ...role ? { role } : {} };
|
|
5090
6009
|
});
|
|
@@ -5290,7 +6209,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5290
6209
|
seed: spec.seed
|
|
5291
6210
|
});
|
|
5292
6211
|
const jobId = randomUUID();
|
|
5293
|
-
writeFileSync(
|
|
6212
|
+
writeFileSync(path3.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
|
|
5294
6213
|
return { jobId };
|
|
5295
6214
|
}
|
|
5296
6215
|
if (spec.generator === "tiles") {
|
|
@@ -5319,17 +6238,46 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5319
6238
|
});
|
|
5320
6239
|
return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
|
|
5321
6240
|
}
|
|
6241
|
+
if (spec.generator === "terrain" && spec.terrainOrientation === "sidescroller") {
|
|
6242
|
+
const baseTileIds = await this.terrainBaseTileIds(context?.terrainLinks);
|
|
6243
|
+
const res2 = await this.client.createTilesetSidescroller({
|
|
6244
|
+
lowerDescription: spec.terrainLowerDescription,
|
|
6245
|
+
transitionDescription: spec.terrainTransitionDescription,
|
|
6246
|
+
lowerBaseTileId: baseTileIds.lower,
|
|
6247
|
+
tileSize: spec.terrainTileSize,
|
|
6248
|
+
transitionSize: spec.terrainTransitionSize,
|
|
6249
|
+
outline: spec.outline,
|
|
6250
|
+
shading: spec.shading,
|
|
6251
|
+
detail: spec.detail,
|
|
6252
|
+
seed: spec.seed
|
|
6253
|
+
});
|
|
6254
|
+
return {
|
|
6255
|
+
jobId: res2.tileset_id,
|
|
6256
|
+
metadata: {
|
|
6257
|
+
backgroundJobId: res2.background_job_id,
|
|
6258
|
+
...Object.keys(baseTileIds).length ? { baseTileIds } : {}
|
|
6259
|
+
}
|
|
6260
|
+
};
|
|
6261
|
+
}
|
|
5322
6262
|
if (spec.generator === "terrain") {
|
|
6263
|
+
const baseTileIds = await this.terrainBaseTileIds(context?.terrainLinks);
|
|
5323
6264
|
const res2 = await this.client.createTileset({
|
|
5324
6265
|
lowerDescription: spec.terrainLowerDescription,
|
|
5325
6266
|
upperDescription: spec.terrainUpperDescription,
|
|
5326
6267
|
transitionDescription: spec.terrainTransitionDescription,
|
|
6268
|
+
lowerBaseTileId: baseTileIds.lower,
|
|
6269
|
+
upperBaseTileId: baseTileIds.upper,
|
|
5327
6270
|
tileSize: spec.terrainTileSize,
|
|
5328
6271
|
mode: spec.terrainMode,
|
|
5329
6272
|
shapeStyle: spec.terrainShapeStyle,
|
|
5330
6273
|
spreadX: spec.terrainSpreadX,
|
|
5331
6274
|
slopeSize: spec.terrainSlopeSize,
|
|
5332
6275
|
raggedness: spec.terrainRaggedness,
|
|
6276
|
+
roundness: spec.terrainRoundness,
|
|
6277
|
+
boundaryAmplitude: spec.terrainBoundaryAmplitude,
|
|
6278
|
+
boundarySeed: spec.terrainBoundarySeed,
|
|
6279
|
+
stepSlope: spec.terrainStepSlope,
|
|
6280
|
+
connectDiagonals: spec.terrainConnectDiagonals,
|
|
5333
6281
|
transitionSize: spec.terrainTransitionSize,
|
|
5334
6282
|
view: spec.terrainView,
|
|
5335
6283
|
outline: spec.outline,
|
|
@@ -5337,7 +6285,13 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5337
6285
|
detail: spec.detail,
|
|
5338
6286
|
seed: spec.seed
|
|
5339
6287
|
});
|
|
5340
|
-
return {
|
|
6288
|
+
return {
|
|
6289
|
+
jobId: res2.tileset_id,
|
|
6290
|
+
metadata: {
|
|
6291
|
+
backgroundJobId: res2.background_job_id,
|
|
6292
|
+
...Object.keys(baseTileIds).length ? { baseTileIds } : {}
|
|
6293
|
+
}
|
|
6294
|
+
};
|
|
5341
6295
|
}
|
|
5342
6296
|
if (spec.generator === "isometricTile") {
|
|
5343
6297
|
const res2 = await this.client.createIsometricTile({
|
|
@@ -5359,7 +6313,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5359
6313
|
width: spec.width,
|
|
5360
6314
|
height: spec.height,
|
|
5361
6315
|
noBackground: spec.noBackground,
|
|
5362
|
-
seed: spec.seed
|
|
6316
|
+
seed: spec.seed,
|
|
6317
|
+
targetPalette: { colors: spec.palette, name: spec.paletteName, maxColors: spec.maxColors }
|
|
5363
6318
|
});
|
|
5364
6319
|
return { jobId: res2.background_job_id };
|
|
5365
6320
|
}
|
|
@@ -5502,6 +6457,14 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5502
6457
|
});
|
|
5503
6458
|
return this.cacheCleanupResult([res2.png], res2.usage, roles, revision.sourceFps);
|
|
5504
6459
|
}
|
|
6460
|
+
if (revision.mode === "normal-map") {
|
|
6461
|
+
const strip = roles ? { base64: joinStripPng(images.map((member) => Buffer.from(member.base64, "base64"))).toString("base64"), format: "png" } : image;
|
|
6462
|
+
const res2 = await this.client.normalMap({ image: strip, frames: roles?.length ?? 1, convention: revision.convention });
|
|
6463
|
+
return {
|
|
6464
|
+
jobId: res2.background_job_id,
|
|
6465
|
+
metadata: roles ? { revisionMembers: roles, ...revision.sourceFps != null ? { revisionFps: revision.sourceFps } : {} } : {}
|
|
6466
|
+
};
|
|
6467
|
+
}
|
|
5505
6468
|
if (revision.mode === "interpolate") {
|
|
5506
6469
|
const lastFrame = this.readLastFrame(spec);
|
|
5507
6470
|
if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
|
|
@@ -5619,11 +6582,11 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5619
6582
|
cacheCleanupResult(pngs, usage, roles, fps) {
|
|
5620
6583
|
const jobId = randomUUID();
|
|
5621
6584
|
if (!roles) {
|
|
5622
|
-
writeFileSync(
|
|
6585
|
+
writeFileSync(path3.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
|
|
5623
6586
|
return { jobId, metadata: { revisionUsage: usage } };
|
|
5624
6587
|
}
|
|
5625
6588
|
for (const [index, png] of pngs.entries()) {
|
|
5626
|
-
writeFileSync(
|
|
6589
|
+
writeFileSync(path3.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
|
|
5627
6590
|
}
|
|
5628
6591
|
return { jobId, metadata: { revisionUsage: usage, revisionMembers: roles, ...fps != null ? { revisionFps: fps } : {} } };
|
|
5629
6592
|
}
|
|
@@ -5647,6 +6610,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5647
6610
|
if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart" || revisionMode === "remove-background") {
|
|
5648
6611
|
return this.pollCachedRevision(jobId, context);
|
|
5649
6612
|
}
|
|
6613
|
+
if (revisionMode === "normal-map") return this.pollNormalMap(jobId, context);
|
|
5650
6614
|
if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "animate-skeleton" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
|
|
5651
6615
|
return this.pollAnimateRevision(jobId, context);
|
|
5652
6616
|
}
|
|
@@ -5654,8 +6618,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5654
6618
|
if (generator === "vfx") return this.pollVfx(jobId, context);
|
|
5655
6619
|
if (generator === "imageProFlash") return this.pollRevision(jobId);
|
|
5656
6620
|
if (generator === "pixflux") {
|
|
5657
|
-
const file =
|
|
5658
|
-
if (
|
|
6621
|
+
const file = path3.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
6622
|
+
if (existsSync3(file)) {
|
|
5659
6623
|
const sourceUrl = `file://${file}`;
|
|
5660
6624
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
|
|
5661
6625
|
}
|
|
@@ -5711,13 +6675,14 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5711
6675
|
*/
|
|
5712
6676
|
async submitVfx(spec, context) {
|
|
5713
6677
|
const vfx = spec.vfx;
|
|
6678
|
+
if (vfx.description) return this.submitVfxArtist(spec, context);
|
|
5714
6679
|
if (!this.mcp) throw new Error("PixelLab vfx renders over PixelLab's MCP server, which this provider was built without");
|
|
5715
6680
|
let sprite;
|
|
5716
6681
|
if (vfx.sprite && "reference" in vfx.sprite) {
|
|
5717
6682
|
sprite = vfx.sprite.reference;
|
|
5718
6683
|
} else if (vfx.sprite) {
|
|
5719
6684
|
const { file } = vfx.sprite;
|
|
5720
|
-
if (!
|
|
6685
|
+
if (!existsSync3(file)) throw new Error(`vfx sprite does not exist yet: ${file}`);
|
|
5721
6686
|
const bytes = readFileSync(file);
|
|
5722
6687
|
if (sha256(bytes) !== vfx.sprite.sha256) {
|
|
5723
6688
|
throw new Error(`vfx sprite changed after the manifest was resolved: ${file}; run the command again`);
|
|
@@ -5765,50 +6730,137 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5765
6730
|
// The effect exists from here on; record it before anything else can fail.
|
|
5766
6731
|
onExported: (done, effectName) => context?.checkpoint({ jobId, complete: true, metadata: { vfx: facts(done, effectName) } })
|
|
5767
6732
|
});
|
|
5768
|
-
await this.cacheVfxFrames(jobId, exported);
|
|
6733
|
+
await this.cacheVfxFrames(jobId, exported, workbenchVfxUrls(exported));
|
|
5769
6734
|
return { jobId, metadata: { vfx: { ...facts(exported, name), recipe } } };
|
|
5770
6735
|
}
|
|
6736
|
+
/**
|
|
6737
|
+
* Ask PixelLab's VFX artist (`POST /vfx`) for a `vfx` description: a
|
|
6738
|
+
* plain background job, polled by `pollVfxArtist`. Every submission is a
|
|
6739
|
+
* new effect. The manifest's description is the whole effect, not a change
|
|
6740
|
+
* to an earlier one, so it is never sent with `effect_id`; the effect this
|
|
6741
|
+
* asset made last time stays in My effects and its id is recorded here.
|
|
6742
|
+
*/
|
|
6743
|
+
async submitVfxArtist(spec, context) {
|
|
6744
|
+
const vfx = spec.vfx;
|
|
6745
|
+
let sprite;
|
|
6746
|
+
if (vfx.sprite && "reference" in vfx.sprite) {
|
|
6747
|
+
throw new Error("PixelLab's VFX artist takes a sprite's pixels, not a PixelLab reference; point vfx.sprite at a PNG file");
|
|
6748
|
+
} else if (vfx.sprite) {
|
|
6749
|
+
const { file } = vfx.sprite;
|
|
6750
|
+
if (!existsSync3(file)) throw new Error(`vfx sprite does not exist yet: ${file}`);
|
|
6751
|
+
const bytes = readFileSync(file);
|
|
6752
|
+
if (sha256(bytes) !== vfx.sprite.sha256) {
|
|
6753
|
+
throw new Error(`vfx sprite changed after the manifest was resolved: ${file}; run the command again`);
|
|
6754
|
+
}
|
|
6755
|
+
sprite = vfxArtistSprite(bytes, `vfx sprite ${file}`).image;
|
|
6756
|
+
}
|
|
6757
|
+
const previous = context?.replacedMetadata?.vfx ?? context?.lastGenerationMetadata?.vfx;
|
|
6758
|
+
const artist = vfx.artist ?? "lite";
|
|
6759
|
+
const submitted = await this.client.createVfx({ description: vfx.description, artist, sprite, name: vfx.name });
|
|
6760
|
+
return {
|
|
6761
|
+
jobId: submitted.background_job_id,
|
|
6762
|
+
metadata: {
|
|
6763
|
+
vfx: {
|
|
6764
|
+
artist,
|
|
6765
|
+
quotedGenerations: submitted.quoted_generations ?? null,
|
|
6766
|
+
...typeof previous?.effectId === "string" ? { previousEffect: previous.effectId } : {}
|
|
6767
|
+
}
|
|
6768
|
+
}
|
|
6769
|
+
};
|
|
6770
|
+
}
|
|
5771
6771
|
/** Cut an export's sheet into frames, with a sidecar `poll` reads without any lock metadata. */
|
|
5772
|
-
async cacheVfxFrames(jobId,
|
|
5773
|
-
const
|
|
5774
|
-
const
|
|
5775
|
-
|
|
5776
|
-
const sheet = await this.client.download(`${exported.downloadBase}${encodeURIComponent(sheetFile)}`);
|
|
5777
|
-
const atlas = atlasFile ? JSON.parse((await this.client.download(`${exported.downloadBase}${encodeURIComponent(atlasFile)}`)).toString("utf8")) : void 0;
|
|
5778
|
-
const frames = sliceVfxSheet(sheet, exported, atlas);
|
|
6772
|
+
async cacheVfxFrames(jobId, facts, urls, atlas) {
|
|
6773
|
+
const sheet = await this.client.download(urls.sheet);
|
|
6774
|
+
const frameAtlas = atlas ?? (urls.atlas ? JSON.parse((await this.client.download(urls.atlas)).toString("utf8")) : void 0);
|
|
6775
|
+
const frames = sliceVfxSheet(sheet, facts, frameAtlas);
|
|
5779
6776
|
const dir = _PixelLabProvider.cacheDir("vfx");
|
|
5780
|
-
frames.forEach((frame, index) => writeFileSync(
|
|
6777
|
+
frames.forEach((frame, index) => writeFileSync(path3.join(dir, `${jobId}-${index}.png`), frame));
|
|
5781
6778
|
writeFileSync(
|
|
5782
|
-
|
|
5783
|
-
JSON.stringify({ effectId:
|
|
6779
|
+
path3.join(dir, `${jobId}.json`),
|
|
6780
|
+
JSON.stringify({ effectId: facts.effectId, count: frames.length, fps: facts.fps, loop: facts.loop })
|
|
5784
6781
|
);
|
|
5785
6782
|
}
|
|
5786
6783
|
async pollVfx(jobId, context) {
|
|
5787
6784
|
const dir = _PixelLabProvider.cacheDir("vfx");
|
|
5788
|
-
const sidecar =
|
|
6785
|
+
const sidecar = path3.join(dir, `${jobId}.json`);
|
|
5789
6786
|
const cached = () => {
|
|
5790
|
-
if (!
|
|
6787
|
+
if (!existsSync3(sidecar)) return null;
|
|
5791
6788
|
const facts2 = JSON.parse(readFileSync(sidecar, "utf8"));
|
|
5792
|
-
const files2 = Array.from({ length: facts2.count }, (_, index) =>
|
|
5793
|
-
return files2.every((file) =>
|
|
6789
|
+
const files2 = Array.from({ length: facts2.count }, (_, index) => path3.join(dir, `${jobId}-${index}.png`));
|
|
6790
|
+
return files2.every((file) => existsSync3(file)) ? { facts: facts2, files: files2 } : null;
|
|
5794
6791
|
};
|
|
5795
6792
|
let found = cached();
|
|
5796
6793
|
const recorded = context?.metadata?.vfx;
|
|
5797
|
-
|
|
5798
|
-
|
|
6794
|
+
let artistDone;
|
|
6795
|
+
if (!found && recorded?.artist) {
|
|
6796
|
+
if (recorded.fileUrls?.sheet && recorded.effectId) {
|
|
6797
|
+
await this.cacheVfxFrames(jobId, recorded, {
|
|
6798
|
+
sheet: recorded.fileUrls.sheet,
|
|
6799
|
+
atlas: recorded.fileUrls.atlas
|
|
6800
|
+
});
|
|
6801
|
+
} else {
|
|
6802
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
6803
|
+
if (job.status === "failed") return { status: "failed", error: `vfx job failed upstream${vfxJobFailure(job.last_response)}` };
|
|
6804
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
6805
|
+
try {
|
|
6806
|
+
artistDone = await this.cacheVfxArtistResult(jobId, job.last_response ?? {}, recorded);
|
|
6807
|
+
} catch (error) {
|
|
6808
|
+
return { status: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
6809
|
+
}
|
|
6810
|
+
artistDone.billed = billedFromUsage(job.usage);
|
|
6811
|
+
}
|
|
6812
|
+
found = cached();
|
|
6813
|
+
} else if (!found && recorded?.downloadBase && Array.isArray(recorded.files)) {
|
|
6814
|
+
const exported = recorded;
|
|
6815
|
+
await this.cacheVfxFrames(jobId, exported, workbenchVfxUrls(exported));
|
|
5799
6816
|
found = cached();
|
|
5800
6817
|
}
|
|
5801
6818
|
if (!found) {
|
|
5802
|
-
return {
|
|
6819
|
+
return {
|
|
6820
|
+
status: "failed",
|
|
6821
|
+
error: recorded?.artist ? "vfx frames are no longer cached locally and their download links were not recorded; re-run submit for this asset (paid)" : "vfx frames are no longer cached locally; re-run submit for this asset (free)"
|
|
6822
|
+
};
|
|
5803
6823
|
}
|
|
5804
6824
|
const { facts, files } = found;
|
|
5805
6825
|
const sources = files.length === 1 ? [{ url: `file://${files[0]}` }] : files.map((file, index) => ({ url: `file://${file}`, role: `frame-${String(index).padStart(2, "0")}` }));
|
|
6826
|
+
const metadata = {
|
|
6827
|
+
...files.length > 1 ? { frameSet: { fps: facts.fps, count: files.length, loop: facts.loop } } : {},
|
|
6828
|
+
...artistDone ? { vfx: artistDone.vfx } : {}
|
|
6829
|
+
};
|
|
5806
6830
|
return {
|
|
5807
6831
|
status: "ready",
|
|
5808
6832
|
objectId: facts.effectId,
|
|
5809
6833
|
sourceUrl: sources[0].url,
|
|
5810
6834
|
sources,
|
|
5811
|
-
...
|
|
6835
|
+
...Object.keys(metadata).length ? { metadata } : {},
|
|
6836
|
+
billed: artistDone?.billed ?? null
|
|
6837
|
+
};
|
|
6838
|
+
}
|
|
6839
|
+
/**
|
|
6840
|
+
* A delivered VFX artist job: cut its sheet into the cache and return the
|
|
6841
|
+
* effect's facts, merged over what submit recorded (poll replaces the
|
|
6842
|
+
* whole `vfx` key). Download links are kept only when they carry no
|
|
6843
|
+
* credential, so a committed lockfile never holds a signed URL.
|
|
6844
|
+
*/
|
|
6845
|
+
async cacheVfxArtistResult(jobId, done, recorded) {
|
|
6846
|
+
const result = parseVfxArtistResult(done, jobId);
|
|
6847
|
+
const atlas = result.atlas ? JSON.parse((await this.client.download(result.atlas.url)).toString("utf8")) : void 0;
|
|
6848
|
+
const size = vfxArtistFrameFacts(result, atlas);
|
|
6849
|
+
const facts = { effectId: result.effectId, ...size, fps: result.fps ?? 12, loop: result.loop ?? false };
|
|
6850
|
+
await this.cacheVfxFrames(jobId, facts, { sheet: result.sheet.url }, atlas);
|
|
6851
|
+
const urls = [result.sheet.url, ...result.atlas ? [result.atlas.url] : []];
|
|
6852
|
+
return {
|
|
6853
|
+
vfx: {
|
|
6854
|
+
...recorded,
|
|
6855
|
+
...facts,
|
|
6856
|
+
editUrl: result.editUrl,
|
|
6857
|
+
name: result.name,
|
|
6858
|
+
message: result.message,
|
|
6859
|
+
anchor: result.anchor,
|
|
6860
|
+
parts: result.parts,
|
|
6861
|
+
files: result.files.map((file) => file.name),
|
|
6862
|
+
...urls.every(shouldPersistSourceUrl) ? { fileUrls: { sheet: result.sheet.url, ...result.atlas ? { atlas: result.atlas.url } : {} } } : {}
|
|
6863
|
+
},
|
|
5812
6864
|
billed: null
|
|
5813
6865
|
};
|
|
5814
6866
|
}
|
|
@@ -5821,10 +6873,10 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5821
6873
|
};
|
|
5822
6874
|
if (Array.isArray(roles) && roles.length) {
|
|
5823
6875
|
const sources = roles.map((role, index) => ({
|
|
5824
|
-
url: `file://${
|
|
6876
|
+
url: `file://${path3.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
|
|
5825
6877
|
role: String(role)
|
|
5826
6878
|
}));
|
|
5827
|
-
if (sources.some((source) => !
|
|
6879
|
+
if (sources.some((source) => !existsSync3(source.url.slice("file://".length)))) return gone;
|
|
5828
6880
|
const fps = context?.metadata?.revisionFps;
|
|
5829
6881
|
const frames = sources.every((source) => source.role.startsWith("frame-"));
|
|
5830
6882
|
return {
|
|
@@ -5836,11 +6888,58 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5836
6888
|
billed: billedFromUsage(usage)
|
|
5837
6889
|
};
|
|
5838
6890
|
}
|
|
5839
|
-
const file =
|
|
5840
|
-
if (!
|
|
6891
|
+
const file = path3.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
6892
|
+
if (!existsSync3(file)) return gone;
|
|
5841
6893
|
const sourceUrl = `file://${file}`;
|
|
5842
6894
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
|
|
5843
6895
|
}
|
|
6896
|
+
/**
|
|
6897
|
+
* A `normal-map` revision: a background job whose map is
|
|
6898
|
+
* `last_response.image`. A loop's map comes back as the strip it was sent
|
|
6899
|
+
* as, and is cut into one file per frame under the parent's roles. Nothing
|
|
6900
|
+
* downstream palette-snaps it: the manifest refuses a normal map in a
|
|
6901
|
+
* style with `enforcePalette` or `quality`.
|
|
6902
|
+
*/
|
|
6903
|
+
async pollNormalMap(jobId, context) {
|
|
6904
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
6905
|
+
if (job.status === "failed") return { status: "failed", error: "normal-map job failed upstream" };
|
|
6906
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
6907
|
+
const billed = billedFromUsage(job.usage);
|
|
6908
|
+
const png = normalMapResultPng(job.last_response);
|
|
6909
|
+
if (!png) {
|
|
6910
|
+
return {
|
|
6911
|
+
status: "failed",
|
|
6912
|
+
error: `Invalid PixelLab response for normal-map job ${jobId}: completed with no image at last_response.image (got: ${Object.keys(job.last_response ?? {}).join(", ") || "no keys"})`
|
|
6913
|
+
};
|
|
6914
|
+
}
|
|
6915
|
+
const roles = context?.metadata?.revisionMembers;
|
|
6916
|
+
if (Array.isArray(roles) && roles.length) {
|
|
6917
|
+
let frames;
|
|
6918
|
+
try {
|
|
6919
|
+
frames = splitStripPng(png, roles.length);
|
|
6920
|
+
} catch (error) {
|
|
6921
|
+
return { status: "failed", error: `normal-map job ${jobId}: ${error.message}` };
|
|
6922
|
+
}
|
|
6923
|
+
const sources = frames.map((frame, index) => {
|
|
6924
|
+
const file2 = path3.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
6925
|
+
writeFileSync(file2, frame);
|
|
6926
|
+
return { url: `file://${file2}`, role: String(roles[index]) };
|
|
6927
|
+
});
|
|
6928
|
+
const fps = context?.metadata?.revisionFps;
|
|
6929
|
+
return {
|
|
6930
|
+
status: "ready",
|
|
6931
|
+
objectId: jobId,
|
|
6932
|
+
sourceUrl: sources[0].url,
|
|
6933
|
+
sources,
|
|
6934
|
+
...typeof fps === "number" ? { metadata: { frameSet: { fps, count: sources.length } } } : {},
|
|
6935
|
+
billed
|
|
6936
|
+
};
|
|
6937
|
+
}
|
|
6938
|
+
const file = path3.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
6939
|
+
writeFileSync(file, png);
|
|
6940
|
+
const sourceUrl = `file://${file}`;
|
|
6941
|
+
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
6942
|
+
}
|
|
5844
6943
|
/**
|
|
5845
6944
|
* `animate`/`animate-pixminimax` revisions: a plain background job, like
|
|
5846
6945
|
* `pollRevision` above, but completing with an ORDERED FRAME LIST instead
|
|
@@ -5893,7 +6992,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5893
6992
|
}
|
|
5894
6993
|
const base64 = extractBase64(item);
|
|
5895
6994
|
if (base64) {
|
|
5896
|
-
const file =
|
|
6995
|
+
const file = path3.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
5897
6996
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
5898
6997
|
urls.push(`file://${file}`);
|
|
5899
6998
|
continue;
|
|
@@ -5944,7 +7043,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5944
7043
|
const candidate = done[key];
|
|
5945
7044
|
const base64 = extractBase64(candidate);
|
|
5946
7045
|
if (base64) {
|
|
5947
|
-
const file =
|
|
7046
|
+
const file = path3.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
5948
7047
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
5949
7048
|
const sourceUrl = `file://${file}`;
|
|
5950
7049
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
@@ -5953,7 +7052,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5953
7052
|
if (Array.isArray(done.images) && done.images.length) {
|
|
5954
7053
|
const base64 = extractBase64(done.images[0]);
|
|
5955
7054
|
if (base64) {
|
|
5956
|
-
const file =
|
|
7055
|
+
const file = path3.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
5957
7056
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
5958
7057
|
const sourceUrl = `file://${file}`;
|
|
5959
7058
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed };
|
|
@@ -5987,7 +7086,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
5987
7086
|
for (const [index, candidate] of done.images.entries()) {
|
|
5988
7087
|
const base64 = extractBase64(candidate);
|
|
5989
7088
|
if (!base64) continue;
|
|
5990
|
-
const file =
|
|
7089
|
+
const file = path3.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
5991
7090
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
5992
7091
|
urls.push(`file://${file}`);
|
|
5993
7092
|
}
|
|
@@ -6017,7 +7116,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6017
7116
|
for (const [index, candidate] of images.entries()) {
|
|
6018
7117
|
const base64 = extractBase64(candidate);
|
|
6019
7118
|
if (!base64) continue;
|
|
6020
|
-
const file =
|
|
7119
|
+
const file = path3.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
6021
7120
|
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
6022
7121
|
urls.push(`file://${file}`);
|
|
6023
7122
|
}
|
|
@@ -6096,6 +7195,26 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6096
7195
|
throw err;
|
|
6097
7196
|
}
|
|
6098
7197
|
}
|
|
7198
|
+
/**
|
|
7199
|
+
* The base tile id of each terrain a connected set shares: the other
|
|
7200
|
+
* set's recorded `terrainIds`, or, for a set downloaded before pixelkiln
|
|
7201
|
+
* recorded them, read from PixelLab's own record of it (a free GET).
|
|
7202
|
+
*/
|
|
7203
|
+
async terrainBaseTileIds(links) {
|
|
7204
|
+
const ids = {};
|
|
7205
|
+
for (const side of ["lower", "upper"]) {
|
|
7206
|
+
const link = links?.[side];
|
|
7207
|
+
if (!link) continue;
|
|
7208
|
+
const id = link.terrainId ?? (await this.client.getTileset(link.objectId)).metadata.terrainIds[link.terrain];
|
|
7209
|
+
if (!id) {
|
|
7210
|
+
throw new Error(
|
|
7211
|
+
`PixelLab terrain: tileset ${link.objectId} has no ${link.terrain} terrain id to connect this set's ${side} terrain to`
|
|
7212
|
+
);
|
|
7213
|
+
}
|
|
7214
|
+
ids[side] = id;
|
|
7215
|
+
}
|
|
7216
|
+
return ids;
|
|
7217
|
+
}
|
|
6099
7218
|
/**
|
|
6100
7219
|
* `/create-tileset` reports progress the same way `tiles` does (423 while
|
|
6101
7220
|
* drawing, 200 once finished), but each tile's image comes back embedded
|
|
@@ -6107,7 +7226,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6107
7226
|
*/
|
|
6108
7227
|
async pollTerrain(tilesetId, context) {
|
|
6109
7228
|
try {
|
|
6110
|
-
const { tileset, usage } = await this.client.getTileset(tilesetId);
|
|
7229
|
+
const { tileset, usage, metadata } = await this.client.getTileset(tilesetId);
|
|
7230
|
+
const sidescroller = metadata.view === "side" || context?.spec?.terrainOrientation === "sidescroller";
|
|
6111
7231
|
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
6112
7232
|
const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
|
|
6113
7233
|
const sources = [];
|
|
@@ -6115,7 +7235,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6115
7235
|
for (const [index, tile] of tileset.tiles.entries()) {
|
|
6116
7236
|
const slug2 = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
|
|
6117
7237
|
const role = `tile-${String(index).padStart(2, "0")}-${slug2}`;
|
|
6118
|
-
const file =
|
|
7238
|
+
const file = path3.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
|
|
6119
7239
|
writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
|
|
6120
7240
|
sources.push({ url: `file://${file}`, role });
|
|
6121
7241
|
terrainTiles.push({ role, corners: tile.corners, pattern4x4: tile.pattern_4x4 });
|
|
@@ -6125,7 +7245,13 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6125
7245
|
objectId: tilesetId,
|
|
6126
7246
|
sourceUrl: sources[0]?.url ?? null,
|
|
6127
7247
|
sources,
|
|
6128
|
-
metadata: {
|
|
7248
|
+
metadata: {
|
|
7249
|
+
terrainTypes: sidescroller ? ["lower", "upper"] : tileset.terrain_types,
|
|
7250
|
+
...sidescroller ? { providerTerrainTypes: tileset.terrain_types, view: "side" } : {},
|
|
7251
|
+
terrainTiles,
|
|
7252
|
+
// Each terrain's base tile id, which a connected set sends.
|
|
7253
|
+
...Object.keys(metadata.terrainIds).length ? { terrainIds: metadata.terrainIds } : {}
|
|
7254
|
+
},
|
|
6129
7255
|
billed
|
|
6130
7256
|
};
|
|
6131
7257
|
} catch (err) {
|
|
@@ -6145,7 +7271,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6145
7271
|
const { image, usage } = await this.client.getIsometricTile(tileId);
|
|
6146
7272
|
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
6147
7273
|
const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
|
|
6148
|
-
const file =
|
|
7274
|
+
const file = path3.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
|
|
6149
7275
|
writeFileSync(file, Buffer.from(image.base64, "base64"));
|
|
6150
7276
|
return {
|
|
6151
7277
|
status: "ready",
|
|
@@ -6189,8 +7315,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6189
7315
|
return { objectId: `${jobId}#${index}`, sourceUrl: url };
|
|
6190
7316
|
}
|
|
6191
7317
|
if (generator === "imagePro" || generator === "uiElement") {
|
|
6192
|
-
const file =
|
|
6193
|
-
if (!
|
|
7318
|
+
const file = path3.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
7319
|
+
if (!existsSync3(file)) throw new Error(`${generator} job ${jobId} has no cached candidate at index ${index}`);
|
|
6194
7320
|
return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
|
|
6195
7321
|
}
|
|
6196
7322
|
const promoted = await this.client.selectFrames(jobId, [index], commonTag);
|
|
@@ -6781,7 +7907,7 @@ function retroError(value) {
|
|
|
6781
7907
|
import { createHash as createHash2 } from "crypto";
|
|
6782
7908
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
6783
7909
|
import os2 from "os";
|
|
6784
|
-
import
|
|
7910
|
+
import path4 from "path";
|
|
6785
7911
|
var DEFAULT_BASE_URL2 = "https://api.cloud.scenario.com/v1";
|
|
6786
7912
|
var SOURCE_PROTOCOL2 = "scenario:";
|
|
6787
7913
|
var SUBMIT_RETRIES = 8;
|
|
@@ -7164,7 +8290,7 @@ var ScenarioProvider = class _ScenarioProvider {
|
|
|
7164
8290
|
}
|
|
7165
8291
|
};
|
|
7166
8292
|
function uploadCachePath() {
|
|
7167
|
-
return process.env.SCENARIO_UPLOAD_CACHE ??
|
|
8293
|
+
return process.env.SCENARIO_UPLOAD_CACHE ?? path4.join(os2.tmpdir(), "pixelkiln-scenario", "uploads.json");
|
|
7168
8294
|
}
|
|
7169
8295
|
function uploadCacheKey(baseUrl, projectId, digest2) {
|
|
7170
8296
|
return createHash2("sha256").update(`${baseUrl}
|
|
@@ -7182,7 +8308,7 @@ function readUploadCache() {
|
|
|
7182
8308
|
function writeUploadCache(key, assetId) {
|
|
7183
8309
|
try {
|
|
7184
8310
|
const file = uploadCachePath();
|
|
7185
|
-
mkdirSync2(
|
|
8311
|
+
mkdirSync2(path4.dirname(file), { recursive: true });
|
|
7186
8312
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
7187
8313
|
writeFileSync2(tmp, JSON.stringify({ ...readUploadCache(), [key]: assetId }));
|
|
7188
8314
|
renameSync(tmp, file);
|
|
@@ -7382,8 +8508,8 @@ function redactScenarioError(value, secrets) {
|
|
|
7382
8508
|
|
|
7383
8509
|
// src/providers/comfyui.ts
|
|
7384
8510
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7385
|
-
import { readFile as
|
|
7386
|
-
import
|
|
8511
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
8512
|
+
import path5 from "path";
|
|
7387
8513
|
var DEFAULT_BASE_URL3 = "http://127.0.0.1:8188";
|
|
7388
8514
|
var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
|
|
7389
8515
|
"prompt",
|
|
@@ -7520,10 +8646,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
7520
8646
|
}
|
|
7521
8647
|
async resolveOptions(value, context) {
|
|
7522
8648
|
const options = parseOptions(value);
|
|
7523
|
-
const workflowPath =
|
|
8649
|
+
const workflowPath = path5.resolve(context.root, options.workflowFile);
|
|
7524
8650
|
let raw;
|
|
7525
8651
|
try {
|
|
7526
|
-
raw = await
|
|
8652
|
+
raw = await readFile3(workflowPath, "utf8");
|
|
7527
8653
|
} catch (err) {
|
|
7528
8654
|
throw new Error(
|
|
7529
8655
|
`ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -7575,10 +8701,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
7575
8701
|
if (typeof item !== "string") {
|
|
7576
8702
|
throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
|
|
7577
8703
|
}
|
|
7578
|
-
const file =
|
|
8704
|
+
const file = path5.resolve(context.root, item);
|
|
7579
8705
|
let bytes;
|
|
7580
8706
|
try {
|
|
7581
|
-
bytes = await
|
|
8707
|
+
bytes = await readFile3(file);
|
|
7582
8708
|
} catch (error) {
|
|
7583
8709
|
throw new Error(
|
|
7584
8710
|
`ComfyUI ${label} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -7713,6 +8839,11 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
7713
8839
|
"ComfyUI has no binding for numColors/paletteImage/dithering \u2014 those are PixelLab's /reduce-colors parameters. A reduce-colors revision on ComfyUI is whatever your own workflow does with bindings.sourceImage; write that behavior into the graph directly."
|
|
7714
8840
|
);
|
|
7715
8841
|
}
|
|
8842
|
+
if (spec.revision.convention) {
|
|
8843
|
+
throw new Error(
|
|
8844
|
+
"ComfyUI has no binding for convention \u2014 that is PixelLab's /normal-map parameter. A normal-map revision on ComfyUI is whatever your own workflow does; build the green-channel convention into the graph."
|
|
8845
|
+
);
|
|
8846
|
+
}
|
|
7716
8847
|
if ((!options.bindings.width || !options.bindings.height) && spec.revision.sourceWidth != null && spec.revision.sourceHeight != null && (spec.width !== spec.revision.sourceWidth || spec.height !== spec.revision.sourceHeight)) {
|
|
7717
8848
|
throw new Error(
|
|
7718
8849
|
"ComfyUI revisions without width/height bindings must keep the source dimensions"
|
|
@@ -7735,7 +8866,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
7735
8866
|
setBinding(workflow2, binding, input);
|
|
7736
8867
|
return;
|
|
7737
8868
|
}
|
|
7738
|
-
const bytes = await
|
|
8869
|
+
const bytes = await readFile3(input.path);
|
|
7739
8870
|
if (sha256(bytes) !== input.sha256) {
|
|
7740
8871
|
throw new Error(`ComfyUI provider input "${name}" changed before upload`);
|
|
7741
8872
|
}
|
|
@@ -7802,7 +8933,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
7802
8933
|
if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
|
|
7803
8934
|
throw new Error("ComfyUI revision source is not ready");
|
|
7804
8935
|
}
|
|
7805
|
-
const sourceBytes = await
|
|
8936
|
+
const sourceBytes = await readFile3(spec.revision.sourceFile);
|
|
7806
8937
|
if (sha256(sourceBytes) !== spec.revision.sourceSha256) {
|
|
7807
8938
|
throw new Error("ComfyUI revision source changed before upload");
|
|
7808
8939
|
}
|
|
@@ -7816,7 +8947,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
7816
8947
|
if (!spec.revision.maskSha256 || !spec.revision.maskFormat) {
|
|
7817
8948
|
throw new Error("ComfyUI revision mask is not ready");
|
|
7818
8949
|
}
|
|
7819
|
-
const maskBytes = await
|
|
8950
|
+
const maskBytes = await readFile3(spec.revision.maskFile);
|
|
7820
8951
|
if (sha256(maskBytes) !== spec.revision.maskSha256) {
|
|
7821
8952
|
throw new Error("ComfyUI revision mask changed before upload");
|
|
7822
8953
|
}
|
|
@@ -8536,9 +9667,9 @@ registerProvider({
|
|
|
8536
9667
|
});
|
|
8537
9668
|
|
|
8538
9669
|
// src/manifest.ts
|
|
8539
|
-
import { readFile as
|
|
8540
|
-
import { existsSync as
|
|
8541
|
-
import
|
|
9670
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
9671
|
+
import { existsSync as existsSync6 } from "fs";
|
|
9672
|
+
import path8 from "path";
|
|
8542
9673
|
|
|
8543
9674
|
// src/loop-directions.ts
|
|
8544
9675
|
var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
|
|
@@ -8626,17 +9757,17 @@ function isObject2(value) {
|
|
|
8626
9757
|
}
|
|
8627
9758
|
|
|
8628
9759
|
// src/outputs.ts
|
|
8629
|
-
import { existsSync as
|
|
8630
|
-
import
|
|
9760
|
+
import { existsSync as existsSync5 } from "fs";
|
|
9761
|
+
import path7 from "path";
|
|
8631
9762
|
|
|
8632
9763
|
// src/lock.ts
|
|
8633
|
-
import { readFile as
|
|
8634
|
-
import { existsSync as
|
|
8635
|
-
import
|
|
9764
|
+
import { readFile as readFile4, writeFile as writeFile3, rename, mkdir as mkdir3, rmdir, rm, stat } from "fs/promises";
|
|
9765
|
+
import { existsSync as existsSync4 } from "fs";
|
|
9766
|
+
import path6 from "path";
|
|
8636
9767
|
async function loadLock(lockPath) {
|
|
8637
|
-
if (!
|
|
9768
|
+
if (!existsSync4(lockPath)) return { version: 2, entries: {} };
|
|
8638
9769
|
try {
|
|
8639
|
-
return parseLock(JSON.parse(await
|
|
9770
|
+
return parseLock(JSON.parse(await readFile4(lockPath, "utf8")));
|
|
8640
9771
|
} catch (err) {
|
|
8641
9772
|
throw new ProjectError(
|
|
8642
9773
|
`Lockfile at ${lockPath} is malformed:
|
|
@@ -8649,7 +9780,7 @@ var saveQueues = /* @__PURE__ */ new Map();
|
|
|
8649
9780
|
var dirtyPatches = /* @__PURE__ */ new WeakMap();
|
|
8650
9781
|
var dirtyDeletes = /* @__PURE__ */ new WeakMap();
|
|
8651
9782
|
function saveLock(lockPath, lock) {
|
|
8652
|
-
const queueKey =
|
|
9783
|
+
const queueKey = path6.resolve(lockPath);
|
|
8653
9784
|
const previous = saveQueues.get(queueKey) ?? Promise.resolve();
|
|
8654
9785
|
const next = previous.catch(() => {
|
|
8655
9786
|
}).then(() => writeLockNow(lockPath, lock));
|
|
@@ -8661,7 +9792,7 @@ function saveLock(lockPath, lock) {
|
|
|
8661
9792
|
return next;
|
|
8662
9793
|
}
|
|
8663
9794
|
async function writeLockNow(lockPath, lock) {
|
|
8664
|
-
await
|
|
9795
|
+
await mkdir3(path6.dirname(path6.resolve(lockPath)), { recursive: true });
|
|
8665
9796
|
const release = await acquireFileLock(lockPath);
|
|
8666
9797
|
try {
|
|
8667
9798
|
await writeLockWhileHeld(lockPath, lock);
|
|
@@ -8671,9 +9802,9 @@ async function writeLockNow(lockPath, lock) {
|
|
|
8671
9802
|
}
|
|
8672
9803
|
async function writeLockWhileHeld(file, lock) {
|
|
8673
9804
|
let disk = { version: 2, entries: {} };
|
|
8674
|
-
if (
|
|
9805
|
+
if (existsSync4(file)) {
|
|
8675
9806
|
try {
|
|
8676
|
-
disk = parseLock(JSON.parse(await
|
|
9807
|
+
disk = parseLock(JSON.parse(await readFile4(file, "utf8")));
|
|
8677
9808
|
} catch (err) {
|
|
8678
9809
|
throw new ProjectError(
|
|
8679
9810
|
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -8702,7 +9833,7 @@ async function writeLockWhileHeld(file, lock) {
|
|
|
8702
9833
|
}
|
|
8703
9834
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
8704
9835
|
try {
|
|
8705
|
-
await
|
|
9836
|
+
await writeFile3(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
|
|
8706
9837
|
await rename(tmp, file);
|
|
8707
9838
|
} finally {
|
|
8708
9839
|
await rm(tmp, { force: true });
|
|
@@ -8768,7 +9899,7 @@ async function acquireFileLock(file) {
|
|
|
8768
9899
|
const deadline = Date.now() + FILE_LOCK_TIMEOUT_MS;
|
|
8769
9900
|
for (; ; ) {
|
|
8770
9901
|
try {
|
|
8771
|
-
await
|
|
9902
|
+
await mkdir3(lockDir);
|
|
8772
9903
|
return async () => {
|
|
8773
9904
|
await rmdir(lockDir).catch(() => {
|
|
8774
9905
|
});
|
|
@@ -8806,22 +9937,22 @@ function totalSpend(lock, unit = "generations") {
|
|
|
8806
9937
|
|
|
8807
9938
|
// src/outputs.ts
|
|
8808
9939
|
function portableOutputPath(file, manifestDir) {
|
|
8809
|
-
const root =
|
|
8810
|
-
const absolute =
|
|
8811
|
-
const relative =
|
|
8812
|
-
const value =
|
|
8813
|
-
return value.split(
|
|
9940
|
+
const root = path7.resolve(manifestDir);
|
|
9941
|
+
const absolute = path7.isAbsolute(file) ? path7.normalize(file) : path7.resolve(root, file);
|
|
9942
|
+
const relative = path7.relative(root, absolute);
|
|
9943
|
+
const value = path7.isAbsolute(relative) ? absolute : relative;
|
|
9944
|
+
return value.split(path7.sep).join("/");
|
|
8814
9945
|
}
|
|
8815
9946
|
function resolveOutputPath(recordedPath, manifestDir) {
|
|
8816
|
-
if (
|
|
8817
|
-
if (
|
|
8818
|
-
return
|
|
9947
|
+
if (path7.isAbsolute(recordedPath)) return path7.normalize(recordedPath);
|
|
9948
|
+
if (path7.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
9949
|
+
return path7.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path7.sep));
|
|
8819
9950
|
}
|
|
8820
9951
|
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
8821
9952
|
return memberPath(spec.outFile, role, index, total, mediaType);
|
|
8822
9953
|
}
|
|
8823
9954
|
function memberPath(file, role, index, total, mediaType) {
|
|
8824
|
-
const originalExt =
|
|
9955
|
+
const originalExt = path7.extname(file);
|
|
8825
9956
|
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
8826
9957
|
const stem = originalExt ? file.slice(0, -originalExt.length) : file;
|
|
8827
9958
|
if (total === 1) return `${stem}${ext}`;
|
|
@@ -8835,10 +9966,10 @@ function isFrameSetEntry(entry) {
|
|
|
8835
9966
|
return entry.generator === "frames" && isMemberSetEntry(entry);
|
|
8836
9967
|
}
|
|
8837
9968
|
function sourceIsStem(source, entry, manifestDir) {
|
|
8838
|
-
return isMemberSetEntry(entry) && !
|
|
9969
|
+
return isMemberSetEntry(entry) && !existsSync5(path7.resolve(manifestDir, source));
|
|
8839
9970
|
}
|
|
8840
9971
|
function sourceOutputPath(source, entry, index, manifestDir) {
|
|
8841
|
-
const absolute =
|
|
9972
|
+
const absolute = path7.resolve(manifestDir, source);
|
|
8842
9973
|
if (!sourceIsStem(source, entry, manifestDir)) return absolute;
|
|
8843
9974
|
const output = entry.outputs[index];
|
|
8844
9975
|
return memberPath(absolute, output.role, index, entry.outputs.length, output.mediaType);
|
|
@@ -8925,7 +10056,7 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
|
8925
10056
|
id: spec.assetId,
|
|
8926
10057
|
index: 0,
|
|
8927
10058
|
path: spec.outFile,
|
|
8928
|
-
absolutePath:
|
|
10059
|
+
absolutePath: path7.resolve(spec.outFile),
|
|
8929
10060
|
sha256: ""
|
|
8930
10061
|
}];
|
|
8931
10062
|
}
|
|
@@ -8946,9 +10077,9 @@ function frameSetLoop(entry) {
|
|
|
8946
10077
|
|
|
8947
10078
|
// src/manifest.ts
|
|
8948
10079
|
async function loadManifest(manifestPath) {
|
|
8949
|
-
const abs =
|
|
8950
|
-
if (!
|
|
8951
|
-
const expansion = expandLoopDirections(JSON.parse(await
|
|
10080
|
+
const abs = path8.resolve(manifestPath);
|
|
10081
|
+
if (!existsSync6(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
10082
|
+
const expansion = expandLoopDirections(JSON.parse(await readFile5(abs, "utf8")));
|
|
8952
10083
|
if (expansion.issues.length) {
|
|
8953
10084
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
8954
10085
|
${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
|
|
@@ -9045,12 +10176,38 @@ ${issues}`);
|
|
|
9045
10176
|
visited.add(assetId);
|
|
9046
10177
|
};
|
|
9047
10178
|
for (const assetId of Object.keys(parsed.data.assets)) visitParents(assetId, []);
|
|
10179
|
+
const connectVisiting = [];
|
|
10180
|
+
const connectVisited = /* @__PURE__ */ new Set();
|
|
10181
|
+
const visitConnections = (assetId) => {
|
|
10182
|
+
if (connectVisited.has(assetId)) return;
|
|
10183
|
+
const cycleStart = connectVisiting.indexOf(assetId);
|
|
10184
|
+
if (cycleStart >= 0) {
|
|
10185
|
+
unknownReferences.push(
|
|
10186
|
+
`assets.${assetId}.connect: connection cycle ${[...connectVisiting.slice(cycleStart), assetId].join(" -> ")}`
|
|
10187
|
+
);
|
|
10188
|
+
return;
|
|
10189
|
+
}
|
|
10190
|
+
connectVisiting.push(assetId);
|
|
10191
|
+
for (const [side, link] of Object.entries(parsed.data.assets[assetId]?.connect ?? {})) {
|
|
10192
|
+
if (!link) continue;
|
|
10193
|
+
if (!parsed.data.assets[link.of]) {
|
|
10194
|
+
unknownReferences.push(`assets.${assetId}.connect.${side}.of: unknown asset "${link.of}"`);
|
|
10195
|
+
} else if (link.of === assetId) {
|
|
10196
|
+
unknownReferences.push(`assets.${assetId}.connect.${side}.of: a set cannot connect to itself`);
|
|
10197
|
+
} else {
|
|
10198
|
+
visitConnections(link.of);
|
|
10199
|
+
}
|
|
10200
|
+
}
|
|
10201
|
+
connectVisiting.pop();
|
|
10202
|
+
connectVisited.add(assetId);
|
|
10203
|
+
};
|
|
10204
|
+
for (const assetId of Object.keys(parsed.data.assets)) visitConnections(assetId);
|
|
9048
10205
|
if (unknownReferences.length) {
|
|
9049
10206
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
9050
10207
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
9051
10208
|
}
|
|
9052
10209
|
const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
|
|
9053
|
-
return { manifest: parsed.data, root:
|
|
10210
|
+
return { manifest: parsed.data, root: path8.dirname(abs), path: abs, ...loopFamilies };
|
|
9054
10211
|
}
|
|
9055
10212
|
function assetInStyle(asset, styleId, style) {
|
|
9056
10213
|
if (asset.styles.length) return asset.styles.includes(styleId);
|
|
@@ -9059,6 +10216,13 @@ function assetInStyle(asset, styleId, style) {
|
|
|
9059
10216
|
function parentAssetId(asset) {
|
|
9060
10217
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of ?? asset?.portrait?.of ?? asset?.outfit?.of;
|
|
9061
10218
|
}
|
|
10219
|
+
function dependencyAssetIds(asset) {
|
|
10220
|
+
const parent = parentAssetId(asset);
|
|
10221
|
+
return [
|
|
10222
|
+
...parent ? [parent] : [],
|
|
10223
|
+
...[asset?.connect?.lower?.of, asset?.connect?.upper?.of].filter((id) => Boolean(id))
|
|
10224
|
+
];
|
|
10225
|
+
}
|
|
9062
10226
|
function formatManifestIssues(issues) {
|
|
9063
10227
|
return issues.flatMap((issue) => {
|
|
9064
10228
|
if (issue.code === "invalid_union") {
|
|
@@ -9146,11 +10310,12 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9146
10310
|
}
|
|
9147
10311
|
const requestedAssetIds = new Set(assetFilter.length ? assetFilter : Object.keys(manifest.assets));
|
|
9148
10312
|
const resolutionAssetIds = new Set(requestedAssetIds);
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
resolutionAssetIds.
|
|
9153
|
-
|
|
10313
|
+
const pending = [...requestedAssetIds];
|
|
10314
|
+
while (pending.length) {
|
|
10315
|
+
for (const dependency of dependencyAssetIds(manifest.assets[pending.pop()])) {
|
|
10316
|
+
if (resolutionAssetIds.has(dependency) || !manifest.assets[dependency]) continue;
|
|
10317
|
+
resolutionAssetIds.add(dependency);
|
|
10318
|
+
pending.push(dependency);
|
|
9154
10319
|
}
|
|
9155
10320
|
}
|
|
9156
10321
|
const styleDependencies = /* @__PURE__ */ new Map();
|
|
@@ -9188,11 +10353,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9188
10353
|
const styleFinalizers = /* @__PURE__ */ new Map();
|
|
9189
10354
|
const styleImageCache = /* @__PURE__ */ new Map();
|
|
9190
10355
|
async function loadStyleImage(rel, what = "Style image") {
|
|
9191
|
-
const abs =
|
|
10356
|
+
const abs = path8.resolve(root, rel);
|
|
9192
10357
|
let hit = styleImageCache.get(abs);
|
|
9193
10358
|
if (!hit) {
|
|
9194
|
-
if (!
|
|
9195
|
-
const buf = await
|
|
10359
|
+
if (!existsSync6(abs)) throw new Error(`${what} not found: ${abs}`);
|
|
10360
|
+
const buf = await readFile5(abs);
|
|
9196
10361
|
const metadata = imageMetadata(buf);
|
|
9197
10362
|
if (!metadata) throw new Error(`${what} is not a readable PNG or JPEG: ${abs}`);
|
|
9198
10363
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -9218,7 +10383,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9218
10383
|
styleImageDimensions.push(loadedImage);
|
|
9219
10384
|
}
|
|
9220
10385
|
const resolvedImages = style.styleImages.map((image) => {
|
|
9221
|
-
const hit = styleImageCache.get(
|
|
10386
|
+
const hit = styleImageCache.get(path8.resolve(root, image.path));
|
|
9222
10387
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
9223
10388
|
});
|
|
9224
10389
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
@@ -9305,6 +10470,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9305
10470
|
`assets.${assetId}: ${asset.portrait ? "portrait" : "outfit"} needs a character style; "${styleId}" generates ${generator}`
|
|
9306
10471
|
);
|
|
9307
10472
|
}
|
|
10473
|
+
if (asset.connect && generator !== "terrain") {
|
|
10474
|
+
throw new Error(`assets.${assetId}: connect needs a terrain style; "${styleId}" generates ${generator}`);
|
|
10475
|
+
}
|
|
9308
10476
|
if ((asset.pieces || asset.elements) && generator !== "uiAsset") {
|
|
9309
10477
|
throw new Error(
|
|
9310
10478
|
`assets.${assetId}: ${asset.pieces ? "pieces" : "elements"} needs a uiAsset style; "${styleId}" generates ${generator}`
|
|
@@ -9316,7 +10484,18 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9316
10484
|
let terrainUpperDescription;
|
|
9317
10485
|
let terrainTransitionDescription;
|
|
9318
10486
|
let prompt;
|
|
9319
|
-
if (generator === "terrain") {
|
|
10487
|
+
if (generator === "terrain" && style.terrainOrientation === "sidescroller") {
|
|
10488
|
+
const parsed = parseTerrainDescriptions(subject);
|
|
10489
|
+
if (!parsed || parsed.transition) {
|
|
10490
|
+
throw new Error(
|
|
10491
|
+
`assets.${assetId}: a sidescroller terrain needs exactly "1). <platform body> 2). <top layer>" in its prompt (e.g. 1). stone brick texture 2). green grass texture)`
|
|
10492
|
+
);
|
|
10493
|
+
}
|
|
10494
|
+
const wrap = (text) => [style.promptPrefix, text, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
10495
|
+
terrainLowerDescription = wrap(parsed.lower);
|
|
10496
|
+
terrainTransitionDescription = wrap(parsed.upper);
|
|
10497
|
+
prompt = subject.trim();
|
|
10498
|
+
} else if (generator === "terrain") {
|
|
9320
10499
|
const parsed = parseTerrainDescriptions(subject);
|
|
9321
10500
|
if (!parsed) {
|
|
9322
10501
|
throw new Error(
|
|
@@ -9333,12 +10512,12 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9333
10512
|
} else {
|
|
9334
10513
|
prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
9335
10514
|
}
|
|
9336
|
-
const relFile = asset.file ??
|
|
9337
|
-
const outFile =
|
|
9338
|
-
const qualityOutFile = style.quality ?
|
|
10515
|
+
const relFile = asset.file ?? path8.join(asset.category ?? "", `${assetId}.png`);
|
|
10516
|
+
const outFile = path8.resolve(root, style.outDir, relFile);
|
|
10517
|
+
const qualityOutFile = style.quality ? path8.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
9339
10518
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
9340
10519
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
9341
|
-
const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
|
|
10520
|
+
const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize, style.terrainMode, style.terrainOrientation) : 0;
|
|
9342
10521
|
const base = {
|
|
9343
10522
|
styleId,
|
|
9344
10523
|
assetId,
|
|
@@ -9361,6 +10540,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9361
10540
|
uiColorPalette: generator === "uiAsset" || generator === "uiElement" ? style.uiColorPalette : void 0,
|
|
9362
10541
|
...generator === "imageProFlash" && style.styleTraits ? { styleTraits: style.styleTraits } : {},
|
|
9363
10542
|
palette: style.palette,
|
|
10543
|
+
...generator === "imagePro" && style.paletteName ? { paletteName: style.paletteName } : {},
|
|
10544
|
+
...generator === "imagePro" && style.maxColors != null ? { maxColors: style.maxColors } : {},
|
|
9364
10545
|
enforcePalette: style.enforcePalette,
|
|
9365
10546
|
noBackground: style.noBackground,
|
|
9366
10547
|
tileSize: generator === "tiles" ? tileSize : void 0,
|
|
@@ -9395,8 +10576,14 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9395
10576
|
terrainSpreadX: generator === "terrain" ? style.terrainSpreadX : void 0,
|
|
9396
10577
|
terrainSlopeSize: generator === "terrain" ? style.terrainSlopeSize : void 0,
|
|
9397
10578
|
terrainRaggedness: generator === "terrain" ? style.terrainRaggedness : void 0,
|
|
10579
|
+
terrainRoundness: generator === "terrain" ? style.terrainRoundness : void 0,
|
|
10580
|
+
terrainBoundaryAmplitude: generator === "terrain" ? style.terrainBoundaryAmplitude : void 0,
|
|
10581
|
+
terrainBoundarySeed: generator === "terrain" ? style.terrainBoundarySeed : void 0,
|
|
10582
|
+
terrainStepSlope: generator === "terrain" ? style.terrainStepSlope : void 0,
|
|
10583
|
+
terrainConnectDiagonals: generator === "terrain" ? style.terrainConnectDiagonals : void 0,
|
|
9398
10584
|
terrainTransitionSize: generator === "terrain" ? style.terrainTransitionSize : void 0,
|
|
9399
10585
|
terrainView: generator === "terrain" ? style.terrainView : void 0,
|
|
10586
|
+
terrainOrientation: generator === "terrain" ? style.terrainOrientation : void 0,
|
|
9400
10587
|
isometricTileSize: generator === "isometricTile" ? style.isometricTileSize : void 0,
|
|
9401
10588
|
isometricTileShape: generator === "isometricTile" ? style.isometricTileShape : void 0,
|
|
9402
10589
|
// A revision in a character or objectPro style is a provider edit of
|
|
@@ -9407,7 +10594,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9407
10594
|
// objectPro asset is only ever a base, state, or animation.
|
|
9408
10595
|
...generator === "objectPro" && !asset.revision && characterKind !== "portrait" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
9409
10596
|
...vfx ? { vfx } : {},
|
|
9410
|
-
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ?
|
|
10597
|
+
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? terrainCost(style.terrainMode, size, style.terrainOrientation) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
|
|
9411
10598
|
costUnit: "generations",
|
|
9412
10599
|
candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
|
|
9413
10600
|
};
|
|
@@ -9431,7 +10618,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9431
10618
|
minGridConfidence: style.quality.minGridConfidence,
|
|
9432
10619
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
9433
10620
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
9434
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
10621
|
+
...style.quality.fixerPython ? { fixerPython: path8.resolve(root, style.quality.fixerPython) } : {},
|
|
9435
10622
|
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
9436
10623
|
}
|
|
9437
10624
|
} : {},
|
|
@@ -9468,6 +10655,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9468
10655
|
if (sourceSpec.generator === "tiles" || sourceSpec.generator === "terrain") {
|
|
9469
10656
|
throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
|
|
9470
10657
|
}
|
|
10658
|
+
if (sourceSpec.revision?.mode === "normal-map") {
|
|
10659
|
+
throw new Error(
|
|
10660
|
+
`assets.${assetId}: a normal map cannot be mirrored by flipping its pixels; every red channel would still point the old way. Make a normal-map revision of the mirrored art instead`
|
|
10661
|
+
);
|
|
10662
|
+
}
|
|
9471
10663
|
if (sourceSpec.character?.kind === "animation") {
|
|
9472
10664
|
const facing = sourceSpec.character.animation.direction;
|
|
9473
10665
|
const mirrored = MIRRORED_DIRECTION[facing];
|
|
@@ -9514,7 +10706,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9514
10706
|
const file = memberPath(anchorSpec.outFile, "south", 0, anchorSpec.character.directions, MediaType.PNG);
|
|
9515
10707
|
resolved.character = {
|
|
9516
10708
|
...resolved.character,
|
|
9517
|
-
styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256:
|
|
10709
|
+
styleAnchor: { assetId: anchorId, spec: anchorSpec, file, sha256: existsSync6(file) ? await sha256File(file) : null }
|
|
9518
10710
|
};
|
|
9519
10711
|
}
|
|
9520
10712
|
const characterParent = asset.state?.of ?? asset.animation?.of ?? asset.portrait?.of;
|
|
@@ -9527,7 +10719,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9527
10719
|
parentAssetId: characterParent,
|
|
9528
10720
|
parentSpec,
|
|
9529
10721
|
parentFile,
|
|
9530
|
-
parentSha256:
|
|
10722
|
+
parentSha256: existsSync6(parentFile) ? await sha256File(parentFile) : null,
|
|
9531
10723
|
// A state and its animations keep the engine and rotations of the base.
|
|
9532
10724
|
mode: resolved.character.kind === "base" ? resolved.character.mode : parentSpec.character?.mode ?? resolved.character.mode,
|
|
9533
10725
|
directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
|
|
@@ -9545,7 +10737,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9545
10737
|
parentAssetId: characterParent,
|
|
9546
10738
|
parentSpec,
|
|
9547
10739
|
parentFile,
|
|
9548
|
-
parentSha256:
|
|
10740
|
+
parentSha256: existsSync6(parentFile) ? await sha256File(parentFile) : null,
|
|
9549
10741
|
// A state and its animations keep the base's own rotation count.
|
|
9550
10742
|
directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
|
|
9551
10743
|
};
|
|
@@ -9567,7 +10759,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9567
10759
|
animation: sourceSpec.character.animation,
|
|
9568
10760
|
outfit: {
|
|
9569
10761
|
reference: {
|
|
9570
|
-
path:
|
|
10762
|
+
path: path8.resolve(root, asset.outfit.reference),
|
|
9571
10763
|
sha256: referenceImage.hash,
|
|
9572
10764
|
width: referenceImage.width,
|
|
9573
10765
|
height: referenceImage.height,
|
|
@@ -9588,25 +10780,30 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9588
10780
|
`Provider "${activeProvider.id}" does not support ${asset.revision.mode} revisions`
|
|
9589
10781
|
);
|
|
9590
10782
|
}
|
|
10783
|
+
if (asset.revision.mode === "normal-map" && (style.enforcePalette || style.quality)) {
|
|
10784
|
+
throw new Error(
|
|
10785
|
+
`assets.${assetId}: a normal-map revision is lighting data, not art, and style "${styleId}" sets ${style.enforcePalette ? "enforcePalette" : "quality"}, which would recolour it. Put it in a style without enforcePalette or quality, and name the parent's style with revision.fromStyle`
|
|
10786
|
+
);
|
|
10787
|
+
}
|
|
9591
10788
|
const sourceStyleId = asset.revision.fromStyle && asset.revision.fromStyle !== styleId ? asset.revision.fromStyle : void 0;
|
|
9592
10789
|
const sourceSpec = sourceStyleId ? await styleFinalizers.get(sourceStyleId)(asset.revision.from) : await finalize(asset.revision.from);
|
|
9593
|
-
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
10790
|
+
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path8.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
9594
10791
|
const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
|
|
9595
10792
|
const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
|
|
9596
10793
|
const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
|
|
9597
10794
|
const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
|
|
9598
|
-
const maskFile = asset.revision.mask ?
|
|
10795
|
+
const maskFile = asset.revision.mask ? path8.resolve(root, asset.revision.mask) : void 0;
|
|
9599
10796
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
9600
10797
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
9601
10798
|
throw new Error(
|
|
9602
10799
|
`Revision mask for ${styleId}/${assetId} is ${maskImage.width}x${maskImage.height}; source ${asset.revision.from} is ${sourceImage.width}x${sourceImage.height}`
|
|
9603
10800
|
);
|
|
9604
10801
|
}
|
|
9605
|
-
const paletteImageFile = asset.revision.paletteImage ?
|
|
10802
|
+
const paletteImageFile = asset.revision.paletteImage ? path8.resolve(root, asset.revision.paletteImage) : void 0;
|
|
9606
10803
|
const paletteImage = paletteImageFile ? await optionalRevisionImage(paletteImageFile, "revision palette image", "png") : null;
|
|
9607
|
-
const lastFrameFile = asset.revision.lastFrame ?
|
|
10804
|
+
const lastFrameFile = asset.revision.lastFrame ? path8.resolve(root, asset.revision.lastFrame) : void 0;
|
|
9608
10805
|
const lastFrame = lastFrameFile ? await optionalRevisionImage(lastFrameFile, "revision last frame") : null;
|
|
9609
|
-
const keypointsFile = asset.revision.keypointsFile ?
|
|
10806
|
+
const keypointsFile = asset.revision.keypointsFile ? path8.resolve(root, asset.revision.keypointsFile) : void 0;
|
|
9610
10807
|
const keypoints = keypointsFile ? await optionalSkeletonSet(keypointsFile) : null;
|
|
9611
10808
|
resolved.revision = {
|
|
9612
10809
|
mode: asset.revision.mode,
|
|
@@ -9662,6 +10859,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9662
10859
|
...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
|
|
9663
10860
|
...asset.revision.description ? { description: asset.revision.description } : {},
|
|
9664
10861
|
...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
|
|
10862
|
+
...asset.revision.convention ? { convention: asset.revision.convention } : {},
|
|
9665
10863
|
...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
|
|
9666
10864
|
...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
|
|
9667
10865
|
...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
|
|
@@ -9718,6 +10916,24 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9718
10916
|
};
|
|
9719
10917
|
}
|
|
9720
10918
|
}
|
|
10919
|
+
if (asset.connect) {
|
|
10920
|
+
const links = {};
|
|
10921
|
+
for (const side of ["lower", "upper"]) {
|
|
10922
|
+
const link = asset.connect[side];
|
|
10923
|
+
if (!link) continue;
|
|
10924
|
+
if (!styleSpecs.has(link.of)) {
|
|
10925
|
+
throw new Error(
|
|
10926
|
+
`assets.${assetId}.connect.${side}.of: "${link.of}" is not in style "${styleId}"; connected terrain sets share one style`
|
|
10927
|
+
);
|
|
10928
|
+
}
|
|
10929
|
+
const linkedSpec = await finalize(link.of);
|
|
10930
|
+
if (linkedSpec.generator !== "terrain") {
|
|
10931
|
+
throw new Error(`assets.${assetId}.connect.${side}.of: "${link.of}" is not a terrain set`);
|
|
10932
|
+
}
|
|
10933
|
+
links[side] = { assetId: link.of, terrain: link.terrain, spec: linkedSpec };
|
|
10934
|
+
}
|
|
10935
|
+
resolved.terrainConnect = links;
|
|
10936
|
+
}
|
|
9721
10937
|
if (asset.scene) resolved.scene = await resolveMapScene(root, styleId, assetId, resolved.generator, asset.scene, activeProvider);
|
|
9722
10938
|
resolved.specHash = specHash(
|
|
9723
10939
|
resolved,
|
|
@@ -9776,7 +10992,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9776
10992
|
return specs;
|
|
9777
10993
|
}
|
|
9778
10994
|
function pngPath(file) {
|
|
9779
|
-
const extension =
|
|
10995
|
+
const extension = path8.extname(file);
|
|
9780
10996
|
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
9781
10997
|
}
|
|
9782
10998
|
async function resolveMapScene(root, styleId, assetId, generator, scene, provider) {
|
|
@@ -9786,12 +11002,12 @@ async function resolveMapScene(root, styleId, assetId, generator, scene, provide
|
|
|
9786
11002
|
if (!provider.supportsMapScene) {
|
|
9787
11003
|
throw new Error(`assets.${assetId}.scene: provider "${provider.id}" cannot draw into a scene; PixelLab's /map-objects can`);
|
|
9788
11004
|
}
|
|
9789
|
-
const file =
|
|
11005
|
+
const file = path8.resolve(root, scene.image);
|
|
9790
11006
|
const image = await optionalRevisionImage(file, "scene image", "png");
|
|
9791
11007
|
const base = { file, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null };
|
|
9792
11008
|
const placement = scene.placement;
|
|
9793
11009
|
if ("mask" in placement) {
|
|
9794
|
-
const maskFile =
|
|
11010
|
+
const maskFile = path8.resolve(root, placement.mask);
|
|
9795
11011
|
const mask = await optionalRevisionImage(maskFile, "scene mask", "png");
|
|
9796
11012
|
if (image && mask && (image.width !== mask.width || image.height !== mask.height)) {
|
|
9797
11013
|
throw new Error(
|
|
@@ -9809,20 +11025,20 @@ async function resolveVfxShape(root, assetId, vfx, defaultName) {
|
|
|
9809
11025
|
let recipe;
|
|
9810
11026
|
let recordedSprite;
|
|
9811
11027
|
if (vfx.recipe) {
|
|
9812
|
-
const file =
|
|
9813
|
-
if (!
|
|
11028
|
+
const file = path8.resolve(root, vfx.recipe);
|
|
11029
|
+
if (!existsSync6(file)) throw new Error(`assets.${assetId}.vfx.recipe: VFX recipe not found: ${file}`);
|
|
9814
11030
|
const read = await readVfxRecipe(file).catch((error) => {
|
|
9815
11031
|
throw new Error(`assets.${assetId}.vfx.recipe: ${error instanceof Error ? error.message : String(error)}`);
|
|
9816
11032
|
});
|
|
9817
11033
|
recipe = { file, sha256: vfxRecipeSha256(read.recipe), recipe: read.recipe };
|
|
9818
11034
|
recordedSprite = read.sprite;
|
|
9819
11035
|
}
|
|
9820
|
-
const spriteRef = vfx.sprite ? isVfxReference(vfx.sprite) ? vfx.sprite :
|
|
11036
|
+
const spriteRef = vfx.sprite ? isVfxReference(vfx.sprite) ? vfx.sprite : path8.resolve(root, vfx.sprite) : recordedSprite;
|
|
9821
11037
|
let sprite;
|
|
9822
11038
|
if (spriteRef && isVfxReference(spriteRef)) {
|
|
9823
11039
|
sprite = { reference: spriteRef };
|
|
9824
11040
|
} else if (spriteRef) {
|
|
9825
|
-
const image = await optionalRevisionImage(spriteRef, `assets.${assetId}.vfx.sprite
|
|
11041
|
+
const image = await optionalRevisionImage(spriteRef, `assets.${assetId}.vfx.sprite`, vfx.description ? "png" : void 0);
|
|
9826
11042
|
sprite = { file: spriteRef, sha256: image?.hash ?? null, width: image?.width ?? null, height: image?.height ?? null, format: image?.format ?? null };
|
|
9827
11043
|
}
|
|
9828
11044
|
return {
|
|
@@ -9831,12 +11047,14 @@ async function resolveVfxShape(root, assetId, vfx, defaultName) {
|
|
|
9831
11047
|
...vfx.parts?.length ? { parts: vfx.parts } : {},
|
|
9832
11048
|
...sprite ? { sprite } : {},
|
|
9833
11049
|
...vfx.fps !== void 0 ? { fps: vfx.fps } : {},
|
|
9834
|
-
|
|
11050
|
+
// The artist caps a name at 80 characters; a long default is cut, not refused.
|
|
11051
|
+
name: vfx.name ?? (vfx.description ? defaultName.slice(0, 80) : defaultName),
|
|
11052
|
+
...vfx.description ? { description: vfx.description, artist: vfx.artist ?? "lite" } : {}
|
|
9835
11053
|
};
|
|
9836
11054
|
}
|
|
9837
11055
|
async function optionalRevisionImage(file, label, requiredFormat) {
|
|
9838
|
-
if (!
|
|
9839
|
-
const bytes = await
|
|
11056
|
+
if (!existsSync6(file)) return null;
|
|
11057
|
+
const bytes = await readFile5(file);
|
|
9840
11058
|
const metadata = imageMetadata(bytes);
|
|
9841
11059
|
if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
|
|
9842
11060
|
if (requiredFormat && metadata.format !== requiredFormat) {
|
|
@@ -9845,8 +11063,8 @@ async function optionalRevisionImage(file, label, requiredFormat) {
|
|
|
9845
11063
|
return { hash: sha256(bytes), ...metadata };
|
|
9846
11064
|
}
|
|
9847
11065
|
async function optionalSkeletonSet(file) {
|
|
9848
|
-
if (!
|
|
9849
|
-
const bytes = await
|
|
11066
|
+
if (!existsSync6(file)) return null;
|
|
11067
|
+
const bytes = await readFile5(file);
|
|
9850
11068
|
let json;
|
|
9851
11069
|
try {
|
|
9852
11070
|
json = JSON.parse(bytes.toString("utf8"));
|
|
@@ -9856,17 +11074,17 @@ async function optionalSkeletonSet(file) {
|
|
|
9856
11074
|
return { hash: sha256(bytes), skeleton: parseSkeletonSet(json, file) };
|
|
9857
11075
|
}
|
|
9858
11076
|
async function revisionSourceMembers(stem, label) {
|
|
9859
|
-
if (
|
|
11077
|
+
if (existsSync6(stem)) return null;
|
|
9860
11078
|
const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
|
|
9861
11079
|
let roles = [];
|
|
9862
11080
|
for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
|
|
9863
|
-
if (set.every((direction) =>
|
|
11081
|
+
if (set.every((direction) => existsSync6(at(direction)))) {
|
|
9864
11082
|
roles = [...set];
|
|
9865
11083
|
break;
|
|
9866
11084
|
}
|
|
9867
11085
|
}
|
|
9868
11086
|
if (!roles.length) {
|
|
9869
|
-
for (let index = 0;
|
|
11087
|
+
for (let index = 0; existsSync6(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
|
|
9870
11088
|
roles.push(`frame-${String(index).padStart(2, "0")}`);
|
|
9871
11089
|
}
|
|
9872
11090
|
}
|
|
@@ -9892,8 +11110,8 @@ async function resolveStyleImages(loaded, styleId) {
|
|
|
9892
11110
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
9893
11111
|
const out = [];
|
|
9894
11112
|
for (const img of style.styleImages) {
|
|
9895
|
-
const file =
|
|
9896
|
-
const buf = await
|
|
11113
|
+
const file = path8.resolve(loaded.root, img.path);
|
|
11114
|
+
const buf = await readFile5(file);
|
|
9897
11115
|
const metadata = imageMetadata(buf);
|
|
9898
11116
|
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
9899
11117
|
if (metadata.width < 1 || metadata.height < 1) {
|
|
@@ -9923,7 +11141,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
9923
11141
|
};
|
|
9924
11142
|
if (asset.concept) {
|
|
9925
11143
|
const image = await files.load(asset.concept, "Concept image");
|
|
9926
|
-
shape.concept = { path:
|
|
11144
|
+
shape.concept = { path: path8.resolve(files.root, asset.concept), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
9927
11145
|
}
|
|
9928
11146
|
if (asset.reference) {
|
|
9929
11147
|
const byDirection = typeof asset.reference === "string" ? { south: asset.reference } : asset.reference;
|
|
@@ -9931,7 +11149,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
9931
11149
|
for (const [direction, rel] of Object.entries(byDirection)) {
|
|
9932
11150
|
const image = await files.load(rel, `Reference sprite (${direction})`);
|
|
9933
11151
|
shape.reference[direction] = {
|
|
9934
|
-
path:
|
|
11152
|
+
path: path8.resolve(files.root, rel),
|
|
9935
11153
|
sha256: image.hash,
|
|
9936
11154
|
width: image.width,
|
|
9937
11155
|
height: image.height,
|
|
@@ -9952,7 +11170,7 @@ async function resolveCharacterShape(asset, style, kind, files) {
|
|
|
9952
11170
|
const animationMode = asset.animation.mode ?? (asset.animation.template ? "template" : "v3");
|
|
9953
11171
|
const pose = async (rel, what) => {
|
|
9954
11172
|
const image = await files.load(rel, what);
|
|
9955
|
-
return { path:
|
|
11173
|
+
return { path: path8.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
9956
11174
|
};
|
|
9957
11175
|
shape.animation = {
|
|
9958
11176
|
mode: animationMode,
|
|
@@ -9983,7 +11201,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
9983
11201
|
const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
|
|
9984
11202
|
if (south) {
|
|
9985
11203
|
const image = await files.load(south, "Reference sprite");
|
|
9986
|
-
shape.reference = { path:
|
|
11204
|
+
shape.reference = { path: path8.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
9987
11205
|
}
|
|
9988
11206
|
}
|
|
9989
11207
|
if (asset.state) {
|
|
@@ -10011,7 +11229,7 @@ async function resolveObjectProShape(assetId, asset, style, kind, files) {
|
|
|
10011
11229
|
}
|
|
10012
11230
|
const pose = async (rel, what) => {
|
|
10013
11231
|
const image = await files.load(rel, what);
|
|
10014
|
-
return { path:
|
|
11232
|
+
return { path: path8.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
|
|
10015
11233
|
};
|
|
10016
11234
|
shape.animation = {
|
|
10017
11235
|
mode: asset.animation.mode ?? "v3",
|
|
@@ -10031,11 +11249,11 @@ function characterDirections(directions) {
|
|
|
10031
11249
|
}
|
|
10032
11250
|
|
|
10033
11251
|
// src/project.ts
|
|
10034
|
-
import
|
|
11252
|
+
import path16 from "path";
|
|
10035
11253
|
|
|
10036
11254
|
// src/env.ts
|
|
10037
|
-
import { readFileSync as readFileSync3, existsSync as
|
|
10038
|
-
import
|
|
11255
|
+
import { readFileSync as readFileSync3, existsSync as existsSync7 } from "fs";
|
|
11256
|
+
import path9 from "path";
|
|
10039
11257
|
function loadEnvFiles(dir) {
|
|
10040
11258
|
const loaded = [];
|
|
10041
11259
|
for (const [file, vars] of envFiles(dir)) {
|
|
@@ -10049,8 +11267,8 @@ function loadEnvFiles(dir) {
|
|
|
10049
11267
|
function envFiles(dir) {
|
|
10050
11268
|
const out = [];
|
|
10051
11269
|
for (const name of [".env.local", ".env"]) {
|
|
10052
|
-
const file =
|
|
10053
|
-
if (!
|
|
11270
|
+
const file = path9.join(dir, name);
|
|
11271
|
+
if (!existsSync7(file)) continue;
|
|
10054
11272
|
try {
|
|
10055
11273
|
out.push([file, parseEnv(readFileSync3(file, "utf8"))]);
|
|
10056
11274
|
} catch {
|
|
@@ -10078,12 +11296,12 @@ function parseEnv(contents) {
|
|
|
10078
11296
|
}
|
|
10079
11297
|
|
|
10080
11298
|
// src/pipeline/plan.ts
|
|
10081
|
-
import { existsSync as
|
|
10082
|
-
import
|
|
11299
|
+
import { existsSync as existsSync10 } from "fs";
|
|
11300
|
+
import path15 from "path";
|
|
10083
11301
|
|
|
10084
11302
|
// src/pipeline/mirror.ts
|
|
10085
|
-
import { mkdir as
|
|
10086
|
-
import
|
|
11303
|
+
import { mkdir as mkdir4, readFile as readFile6, rename as rename2, writeFile as writeFile4 } from "fs/promises";
|
|
11304
|
+
import path10 from "path";
|
|
10087
11305
|
|
|
10088
11306
|
// src/palette.ts
|
|
10089
11307
|
function normalizePalette(values) {
|
|
@@ -10220,11 +11438,11 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
|
|
|
10220
11438
|
const outputs = [];
|
|
10221
11439
|
const written = [];
|
|
10222
11440
|
for (const [index, member] of members.entries()) {
|
|
10223
|
-
const bytes = flipHorizontal(await
|
|
11441
|
+
const bytes = flipHorizontal(await readFile6(member.file));
|
|
10224
11442
|
const target = expectedOutputPath(spec, member.role, index, members.length, MediaType.PNG);
|
|
10225
|
-
await
|
|
11443
|
+
await mkdir4(path10.dirname(target), { recursive: true });
|
|
10226
11444
|
const tmp = `${target}.pixelkiln.tmp`;
|
|
10227
|
-
await
|
|
11445
|
+
await writeFile4(tmp, bytes);
|
|
10228
11446
|
await rename2(tmp, target);
|
|
10229
11447
|
written.push(target);
|
|
10230
11448
|
outputs.push({
|
|
@@ -10284,20 +11502,20 @@ async function deriveMirror(spec, lock, lockPath, opts = {}) {
|
|
|
10284
11502
|
}
|
|
10285
11503
|
|
|
10286
11504
|
// src/pipeline/quality-profile.ts
|
|
10287
|
-
import { existsSync as
|
|
10288
|
-
import
|
|
11505
|
+
import { existsSync as existsSync8 } from "fs";
|
|
11506
|
+
import path13 from "path";
|
|
10289
11507
|
|
|
10290
11508
|
// src/pipeline/refine.ts
|
|
10291
|
-
import
|
|
11509
|
+
import path12 from "path";
|
|
10292
11510
|
import { execFile } from "child_process";
|
|
10293
|
-
import { mkdtemp, readFile as
|
|
11511
|
+
import { mkdtemp, readFile as readFile8, rm as rm3 } from "fs/promises";
|
|
10294
11512
|
import { tmpdir } from "os";
|
|
10295
11513
|
import { promisify } from "util";
|
|
10296
11514
|
|
|
10297
11515
|
// src/artifacts.ts
|
|
10298
|
-
import
|
|
11516
|
+
import path11 from "path";
|
|
10299
11517
|
import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
|
|
10300
|
-
import { mkdir as
|
|
11518
|
+
import { mkdir as mkdir5, readFile as readFile7, rename as rename3, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
10301
11519
|
var activeTransactions = /* @__PURE__ */ new Set();
|
|
10302
11520
|
function message(error) {
|
|
10303
11521
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -10306,7 +11524,7 @@ function digest(data) {
|
|
|
10306
11524
|
return createHash4("sha256").update(data).digest("hex");
|
|
10307
11525
|
}
|
|
10308
11526
|
function portableRelative(from, to) {
|
|
10309
|
-
return
|
|
11527
|
+
return path11.relative(from, path11.resolve(to)).split(path11.sep).join("/") || ".";
|
|
10310
11528
|
}
|
|
10311
11529
|
function canonical2(value) {
|
|
10312
11530
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
@@ -10356,12 +11574,12 @@ function parseArtifactManifest(absolute, data) {
|
|
|
10356
11574
|
return raw;
|
|
10357
11575
|
}
|
|
10358
11576
|
async function readArtifactBundleManifest(manifestPath) {
|
|
10359
|
-
const absolute =
|
|
10360
|
-
return parseArtifactManifest(absolute, await
|
|
11577
|
+
const absolute = path11.resolve(manifestPath);
|
|
11578
|
+
return parseArtifactManifest(absolute, await readFile7(absolute, "utf8"));
|
|
10361
11579
|
}
|
|
10362
11580
|
async function readOptional(file) {
|
|
10363
11581
|
try {
|
|
10364
|
-
return await
|
|
11582
|
+
return await readFile7(file);
|
|
10365
11583
|
} catch (error) {
|
|
10366
11584
|
if (isCode(error, "ENOENT")) return null;
|
|
10367
11585
|
throw error;
|
|
@@ -10380,8 +11598,8 @@ function processIsAlive(pid) {
|
|
|
10380
11598
|
}
|
|
10381
11599
|
}
|
|
10382
11600
|
function validTemporaryPath(candidate, destination, type) {
|
|
10383
|
-
return
|
|
10384
|
-
`.${
|
|
11601
|
+
return path11.dirname(candidate) === path11.dirname(destination) && path11.basename(candidate).startsWith(
|
|
11602
|
+
`.${path11.basename(destination)}.pixelkiln-${type}-`
|
|
10385
11603
|
);
|
|
10386
11604
|
}
|
|
10387
11605
|
function parseTransaction(journal, data) {
|
|
@@ -10401,7 +11619,7 @@ async function removeTransactionFiles(journal) {
|
|
|
10401
11619
|
await rm2(transactionMarker(journal), { force: true });
|
|
10402
11620
|
}
|
|
10403
11621
|
async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
10404
|
-
const journal =
|
|
11622
|
+
const journal = path11.resolve(recoveryFile);
|
|
10405
11623
|
const bytes = await readOptional(journal);
|
|
10406
11624
|
if (!bytes) {
|
|
10407
11625
|
await rm2(transactionMarker(journal), { force: true });
|
|
@@ -10417,7 +11635,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
10417
11635
|
);
|
|
10418
11636
|
}
|
|
10419
11637
|
for (const entry of transaction.entries) {
|
|
10420
|
-
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(
|
|
11638
|
+
if (typeof entry.destination !== "string" || typeof entry.stage !== "string" || typeof entry.sha256 !== "string" || entry.backup !== void 0 && typeof entry.backup !== "string" || !allowedDestinations.has(path11.resolve(entry.destination)) || !validTemporaryPath(entry.stage, entry.destination, "stage") || entry.backup !== void 0 && !validTemporaryPath(entry.backup, entry.destination, "backup")) {
|
|
10421
11639
|
throw new Error(
|
|
10422
11640
|
`Refusing unsafe artifact recovery from ${journal}; its destinations or temporary paths do not match the current bundle.`
|
|
10423
11641
|
);
|
|
@@ -10463,7 +11681,7 @@ async function recoverArtifactTransaction(recoveryFile, allowedDestinations) {
|
|
|
10463
11681
|
await removeTransactionFiles(journal);
|
|
10464
11682
|
}
|
|
10465
11683
|
function createArtifactBundleManifest(manifestPath, outputs, provenance) {
|
|
10466
|
-
const root =
|
|
11684
|
+
const root = path11.dirname(path11.resolve(manifestPath));
|
|
10467
11685
|
const sources = provenance.sources.map((source) => ({
|
|
10468
11686
|
...source,
|
|
10469
11687
|
path: portableRelative(root, source.path)
|
|
@@ -10493,11 +11711,11 @@ function withArtifactManifest(manifestPath, outputs, provenance) {
|
|
|
10493
11711
|
];
|
|
10494
11712
|
}
|
|
10495
11713
|
async function writeManagedArtifactBundle(manifestPath, outputs, provenance, options = {}) {
|
|
10496
|
-
const absoluteManifest =
|
|
11714
|
+
const absoluteManifest = path11.resolve(manifestPath);
|
|
10497
11715
|
const recoveryFile = `${absoluteManifest}.transaction`;
|
|
10498
11716
|
const allowedDestinations = /* @__PURE__ */ new Set([
|
|
10499
11717
|
absoluteManifest,
|
|
10500
|
-
...outputs.map((output) =>
|
|
11718
|
+
...outputs.map((output) => path11.resolve(output.path))
|
|
10501
11719
|
]);
|
|
10502
11720
|
await recoverArtifactTransaction(recoveryFile, allowedDestinations);
|
|
10503
11721
|
const existingManifestBytes = await readOptional(absoluteManifest);
|
|
@@ -10515,13 +11733,13 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
|
|
|
10515
11733
|
}
|
|
10516
11734
|
}
|
|
10517
11735
|
if (!options.force) {
|
|
10518
|
-
const root =
|
|
11736
|
+
const root = path11.dirname(absoluteManifest);
|
|
10519
11737
|
const recorded = new Map(
|
|
10520
|
-
previous?.outputs.map((output) => [
|
|
11738
|
+
previous?.outputs.map((output) => [path11.resolve(root, output.path), output.sha256]) ?? []
|
|
10521
11739
|
);
|
|
10522
11740
|
const conflicts = [];
|
|
10523
11741
|
for (const output of outputs) {
|
|
10524
|
-
const destination =
|
|
11742
|
+
const destination = path11.resolve(output.path);
|
|
10525
11743
|
const current = await readOptional(destination);
|
|
10526
11744
|
if (!current || digest(current) === digest(output.data)) continue;
|
|
10527
11745
|
const expected = recorded.get(destination);
|
|
@@ -10543,15 +11761,15 @@ Pass { force: true } (CLI: --force) to take ownership and replace the complete b
|
|
|
10543
11761
|
);
|
|
10544
11762
|
}
|
|
10545
11763
|
async function verifyArtifactBundle(manifestPath) {
|
|
10546
|
-
const absolute =
|
|
11764
|
+
const absolute = path11.resolve(manifestPath);
|
|
10547
11765
|
const raw = await readArtifactBundleManifest(absolute);
|
|
10548
|
-
const root =
|
|
11766
|
+
const root = path11.dirname(absolute);
|
|
10549
11767
|
const changedSources = [];
|
|
10550
11768
|
const changedOutputs = [];
|
|
10551
11769
|
for (const source of raw.sources) {
|
|
10552
11770
|
let actual = null;
|
|
10553
11771
|
try {
|
|
10554
|
-
actual = digest(await
|
|
11772
|
+
actual = digest(await readFile7(path11.resolve(root, source.path)));
|
|
10555
11773
|
} catch (error) {
|
|
10556
11774
|
if (!isCode(error, "ENOENT")) throw error;
|
|
10557
11775
|
}
|
|
@@ -10559,7 +11777,7 @@ async function verifyArtifactBundle(manifestPath) {
|
|
|
10559
11777
|
}
|
|
10560
11778
|
for (const output of raw.outputs) {
|
|
10561
11779
|
try {
|
|
10562
|
-
if (digest(await
|
|
11780
|
+
if (digest(await readFile7(path11.resolve(root, output.path))) !== output.sha256) {
|
|
10563
11781
|
changedOutputs.push(output.path);
|
|
10564
11782
|
}
|
|
10565
11783
|
} catch (error) {
|
|
@@ -10615,7 +11833,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
10615
11833
|
if (!files.length) throw new Error("An artifact bundle must contain at least one file.");
|
|
10616
11834
|
const normalized = files.map((file) => {
|
|
10617
11835
|
if (!file.path.trim()) throw new Error("Artifact paths cannot be empty.");
|
|
10618
|
-
return { destination:
|
|
11836
|
+
return { destination: path11.resolve(file.path), data: Buffer.from(file.data) };
|
|
10619
11837
|
});
|
|
10620
11838
|
const destinations = /* @__PURE__ */ new Set();
|
|
10621
11839
|
for (const artifact of normalized) {
|
|
@@ -10631,7 +11849,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
10631
11849
|
const unchanged = [];
|
|
10632
11850
|
for (const artifact of normalized) {
|
|
10633
11851
|
try {
|
|
10634
|
-
const current = await
|
|
11852
|
+
const current = await readFile7(artifact.destination);
|
|
10635
11853
|
if (current.equals(artifact.data)) {
|
|
10636
11854
|
unchanged.push(artifact.destination);
|
|
10637
11855
|
} else {
|
|
@@ -10645,21 +11863,21 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
10645
11863
|
if (!changed.length) return { changed: [], unchanged };
|
|
10646
11864
|
const token = randomUUID3();
|
|
10647
11865
|
for (const [index, artifact] of changed.entries()) {
|
|
10648
|
-
const basename =
|
|
10649
|
-
artifact.stage =
|
|
10650
|
-
|
|
11866
|
+
const basename = path11.basename(artifact.destination);
|
|
11867
|
+
artifact.stage = path11.join(
|
|
11868
|
+
path11.dirname(artifact.destination),
|
|
10651
11869
|
`.${basename}.pixelkiln-stage-${token}-${index}`
|
|
10652
11870
|
);
|
|
10653
11871
|
if (artifact.existed) {
|
|
10654
|
-
artifact.backup =
|
|
10655
|
-
|
|
11872
|
+
artifact.backup = path11.join(
|
|
11873
|
+
path11.dirname(artifact.destination),
|
|
10656
11874
|
`.${basename}.pixelkiln-backup-${token}-${index}`
|
|
10657
11875
|
);
|
|
10658
11876
|
}
|
|
10659
11877
|
}
|
|
10660
|
-
const journal = options.recoveryFile ?
|
|
11878
|
+
const journal = options.recoveryFile ? path11.resolve(options.recoveryFile) : null;
|
|
10661
11879
|
if (journal) {
|
|
10662
|
-
await
|
|
11880
|
+
await mkdir5(path11.dirname(journal), { recursive: true });
|
|
10663
11881
|
const transaction = {
|
|
10664
11882
|
format: "pixelkiln-artifact-transaction",
|
|
10665
11883
|
version: 1,
|
|
@@ -10673,7 +11891,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
10673
11891
|
}))
|
|
10674
11892
|
};
|
|
10675
11893
|
try {
|
|
10676
|
-
await
|
|
11894
|
+
await writeFile5(journal, JSON.stringify(transaction, null, 2) + "\n", { flag: "wx" });
|
|
10677
11895
|
activeTransactions.add(journal);
|
|
10678
11896
|
} catch (error) {
|
|
10679
11897
|
throw new Error(
|
|
@@ -10684,9 +11902,9 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
10684
11902
|
}
|
|
10685
11903
|
try {
|
|
10686
11904
|
for (const [index, artifact] of changed.entries()) {
|
|
10687
|
-
await
|
|
11905
|
+
await mkdir5(path11.dirname(artifact.destination), { recursive: true });
|
|
10688
11906
|
await options.beforeStage?.(artifact.destination, index);
|
|
10689
|
-
await
|
|
11907
|
+
await writeFile5(artifact.stage, artifact.data, { flag: "wx" });
|
|
10690
11908
|
}
|
|
10691
11909
|
} catch (error) {
|
|
10692
11910
|
const cleanupErrors2 = await rollback(changed);
|
|
@@ -10709,7 +11927,7 @@ async function writeArtifactBundle(files, options = {}) {
|
|
|
10709
11927
|
artifact.promoted = true;
|
|
10710
11928
|
}
|
|
10711
11929
|
if (journal) {
|
|
10712
|
-
await
|
|
11930
|
+
await writeFile5(transactionMarker(journal), `${token}
|
|
10713
11931
|
`, { flag: "wx" });
|
|
10714
11932
|
durableCommit = true;
|
|
10715
11933
|
await options.afterCommit?.();
|
|
@@ -10950,16 +12168,16 @@ function sameFrameGrid(first, next) {
|
|
|
10950
12168
|
return next.detection.stepX === first.detection.stepX && next.detection.stepY === first.detection.stepY && next.detection.columns === first.detection.columns && next.detection.rows === first.detection.rows && phaseMatches(next.detection.phaseX, first.detection.phaseX, first.detection.stepX) && phaseMatches(next.detection.phaseY, first.detection.phaseY, first.detection.stepY) && next.audit.width === first.audit.width && next.audit.height === first.audit.height && next.sourceWidth === first.sourceWidth && next.sourceHeight === first.sourceHeight;
|
|
10951
12169
|
}
|
|
10952
12170
|
async function refineAsset(options) {
|
|
10953
|
-
const source =
|
|
10954
|
-
const output =
|
|
12171
|
+
const source = path12.resolve(options.source);
|
|
12172
|
+
const output = path12.resolve(options.output);
|
|
10955
12173
|
if (source === output) throw new Error("Refinement source and output must be different files.");
|
|
10956
|
-
const record =
|
|
12174
|
+
const record = path12.resolve(qualityRecordPath(output));
|
|
10957
12175
|
const palette = normalizePalette(options.palette).map((color) => color.hex);
|
|
10958
12176
|
const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
|
|
10959
12177
|
if (options.minTransparency !== void 0 && (!Number.isFinite(options.minTransparency) || options.minTransparency < 0 || options.minTransparency > 1)) throw new Error("minTransparency must be between 0 and 1.");
|
|
10960
12178
|
let sourcePng;
|
|
10961
12179
|
try {
|
|
10962
|
-
sourcePng = decodePng(await
|
|
12180
|
+
sourcePng = decodePng(await readFile8(source));
|
|
10963
12181
|
} catch (error) {
|
|
10964
12182
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
10965
12183
|
}
|
|
@@ -10976,8 +12194,8 @@ async function refineAsset(options) {
|
|
|
10976
12194
|
);
|
|
10977
12195
|
}
|
|
10978
12196
|
}
|
|
10979
|
-
const temp = await mkdtemp(
|
|
10980
|
-
const recovered =
|
|
12197
|
+
const temp = await mkdtemp(path12.join(tmpdir(), "pixelkiln-refine-"));
|
|
12198
|
+
const recovered = path12.join(temp, "native.png");
|
|
10981
12199
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
10982
12200
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
10983
12201
|
try {
|
|
@@ -10998,7 +12216,7 @@ async function refineAsset(options) {
|
|
|
10998
12216
|
);
|
|
10999
12217
|
}
|
|
11000
12218
|
const detection = parseDetection(firstJsonObject(stdout));
|
|
11001
|
-
const native = decodePng(await
|
|
12219
|
+
const native = decodePng(await readFile8(recovered));
|
|
11002
12220
|
const quantized = quantizeToPalette(native, palette);
|
|
11003
12221
|
const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
|
|
11004
12222
|
const audit = auditRefinement(
|
|
@@ -11062,18 +12280,18 @@ async function refineFrameSet(options) {
|
|
|
11062
12280
|
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
11063
12281
|
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
11064
12282
|
}
|
|
11065
|
-
const output =
|
|
11066
|
-
const record =
|
|
12283
|
+
const output = path12.resolve(options.output);
|
|
12284
|
+
const record = path12.resolve(qualityRecordPath(output));
|
|
11067
12285
|
const palette = normalizeRefinementPalette(options.palette);
|
|
11068
12286
|
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
11069
12287
|
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
11070
12288
|
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
11071
12289
|
}
|
|
11072
|
-
const sourceSet = new Set(options.sources.map((source) =>
|
|
12290
|
+
const sourceSet = new Set(options.sources.map((source) => path12.resolve(source.path)));
|
|
11073
12291
|
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
11074
12292
|
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
11075
12293
|
}
|
|
11076
|
-
const temp = await mkdtemp(
|
|
12294
|
+
const temp = await mkdtemp(path12.join(tmpdir(), "pixelkiln-refine-frames-"));
|
|
11077
12295
|
try {
|
|
11078
12296
|
const frames = [];
|
|
11079
12297
|
const outputs = [];
|
|
@@ -11081,8 +12299,8 @@ async function refineFrameSet(options) {
|
|
|
11081
12299
|
const sourcePaths = [];
|
|
11082
12300
|
let firstOptions = null;
|
|
11083
12301
|
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
11084
|
-
const source =
|
|
11085
|
-
const temporaryOutput =
|
|
12302
|
+
const source = path12.resolve(sourceEntry.path);
|
|
12303
|
+
const temporaryOutput = path12.join(temp, `${String(index).padStart(2, "0")}.png`);
|
|
11086
12304
|
const refined = await refineAsset({
|
|
11087
12305
|
source,
|
|
11088
12306
|
output: temporaryOutput,
|
|
@@ -11114,7 +12332,7 @@ async function refineFrameSet(options) {
|
|
|
11114
12332
|
});
|
|
11115
12333
|
outputs.push({
|
|
11116
12334
|
path: finalOutputs[index],
|
|
11117
|
-
data: await
|
|
12335
|
+
data: await readFile8(temporaryOutput)
|
|
11118
12336
|
});
|
|
11119
12337
|
}
|
|
11120
12338
|
const first = frames[0];
|
|
@@ -11157,7 +12375,7 @@ async function refineFrameSet(options) {
|
|
|
11157
12375
|
);
|
|
11158
12376
|
return {
|
|
11159
12377
|
sources: sourcePaths,
|
|
11160
|
-
outputs: outputs.map((item) =>
|
|
12378
|
+
outputs: outputs.map((item) => path12.resolve(item.path)),
|
|
11161
12379
|
record,
|
|
11162
12380
|
frames,
|
|
11163
12381
|
palette,
|
|
@@ -11168,12 +12386,12 @@ async function refineFrameSet(options) {
|
|
|
11168
12386
|
}
|
|
11169
12387
|
}
|
|
11170
12388
|
async function checkQualityRecord(recordPath) {
|
|
11171
|
-
const record =
|
|
12389
|
+
const record = path12.resolve(recordPath);
|
|
11172
12390
|
const manifest = await readArtifactBundleManifest(record);
|
|
11173
12391
|
const options = parseRecordOptions(manifest, record);
|
|
11174
12392
|
const verification = await verifyArtifactBundle(record);
|
|
11175
|
-
const sources = manifest.sources.map((source2) =>
|
|
11176
|
-
const outputs = manifest.outputs.map((output2) =>
|
|
12393
|
+
const sources = manifest.sources.map((source2) => path12.resolve(path12.dirname(record), source2.path));
|
|
12394
|
+
const outputs = manifest.outputs.map((output2) => path12.resolve(path12.dirname(record), output2.path));
|
|
11177
12395
|
const source = sources[0] ?? "";
|
|
11178
12396
|
const output = outputs[0] ?? "";
|
|
11179
12397
|
const expectedCount = options.frameSet?.count ?? 1;
|
|
@@ -11206,7 +12424,7 @@ async function checkQualityRecord(recordPath) {
|
|
|
11206
12424
|
async function approveQualityRecord(recordPath, approval) {
|
|
11207
12425
|
const reviewer = approval.reviewer.trim();
|
|
11208
12426
|
if (!reviewer) throw new Error("A human reviewer name is required.");
|
|
11209
|
-
const record =
|
|
12427
|
+
const record = path12.resolve(recordPath);
|
|
11210
12428
|
const manifest = await readArtifactBundleManifest(record);
|
|
11211
12429
|
const options = parseRecordOptions(manifest, record);
|
|
11212
12430
|
const verification = await verifyArtifactBundle(record);
|
|
@@ -11227,14 +12445,14 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
11227
12445
|
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
11228
12446
|
);
|
|
11229
12447
|
}
|
|
11230
|
-
const root =
|
|
12448
|
+
const root = path12.dirname(record);
|
|
11231
12449
|
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
11232
|
-
const outputPath =
|
|
11233
|
-
return { path: outputPath, data: await
|
|
12450
|
+
const outputPath = path12.resolve(root, entry.path);
|
|
12451
|
+
return { path: outputPath, data: await readFile8(outputPath) };
|
|
11234
12452
|
}));
|
|
11235
12453
|
const sources = manifest.sources.map((source) => ({
|
|
11236
12454
|
...source,
|
|
11237
|
-
path:
|
|
12455
|
+
path: path12.resolve(root, source.path)
|
|
11238
12456
|
}));
|
|
11239
12457
|
const approved = {
|
|
11240
12458
|
...options,
|
|
@@ -11266,8 +12484,8 @@ async function qualitySource(spec, lock) {
|
|
|
11266
12484
|
if (spec.generator === "frames") {
|
|
11267
12485
|
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
11268
12486
|
}
|
|
11269
|
-
const source =
|
|
11270
|
-
return
|
|
12487
|
+
const source = path13.resolve(spec.root, spec.source);
|
|
12488
|
+
return existsSync8(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
11271
12489
|
}
|
|
11272
12490
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
11273
12491
|
if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
|
|
@@ -11296,7 +12514,7 @@ async function qualitySource(spec, lock) {
|
|
|
11296
12514
|
}
|
|
11297
12515
|
if (role) roles.add(role);
|
|
11298
12516
|
const source = currentEntryOutputPath(entry, spec, index);
|
|
11299
|
-
if (!
|
|
12517
|
+
if (!existsSync8(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
|
|
11300
12518
|
if (await sha256File(source) !== output.sha256) {
|
|
11301
12519
|
return { items: null, reason: "raw provider output was modified after download" };
|
|
11302
12520
|
}
|
|
@@ -11315,7 +12533,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
11315
12533
|
}
|
|
11316
12534
|
const sourcePaths = source.items.map((item) => item.path);
|
|
11317
12535
|
const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
|
|
11318
|
-
if (!
|
|
12536
|
+
if (!existsSync8(record)) {
|
|
11319
12537
|
return {
|
|
11320
12538
|
key,
|
|
11321
12539
|
state: "needs-refinement",
|
|
@@ -11333,7 +12551,7 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
11333
12551
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
11334
12552
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
11335
12553
|
const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
|
|
11336
|
-
const contractChanged = JSON.stringify(check.sources.map((item) =>
|
|
12554
|
+
const contractChanged = JSON.stringify(check.sources.map((item) => path13.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path13.resolve(item))) || JSON.stringify(check.outputs.map((item) => path13.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path13.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
|
|
11337
12555
|
if (contractChanged) {
|
|
11338
12556
|
return {
|
|
11339
12557
|
key,
|
|
@@ -11496,8 +12714,8 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
|
|
|
11496
12714
|
}
|
|
11497
12715
|
|
|
11498
12716
|
// src/pipeline/revision.ts
|
|
11499
|
-
import { existsSync as
|
|
11500
|
-
import
|
|
12717
|
+
import { existsSync as existsSync9 } from "fs";
|
|
12718
|
+
import path14 from "path";
|
|
11501
12719
|
async function inspectRevisionReadiness(spec, lock) {
|
|
11502
12720
|
if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
|
|
11503
12721
|
if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
|
|
@@ -11506,6 +12724,7 @@ async function inspectRevisionReadiness(spec, lock) {
|
|
|
11506
12724
|
if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
|
|
11507
12725
|
if (spec.scene) return inspectScene(spec);
|
|
11508
12726
|
if (spec.vfx?.sprite && "file" in spec.vfx.sprite) return inspectVfxSprite(spec);
|
|
12727
|
+
if (spec.terrainConnect) return inspectTerrainConnect(spec, lock, /* @__PURE__ */ new Set());
|
|
11509
12728
|
if (!spec.revision) return null;
|
|
11510
12729
|
return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
|
|
11511
12730
|
}
|
|
@@ -11514,7 +12733,7 @@ async function inspectStyleAnchor(spec, lock, seen) {
|
|
|
11514
12733
|
const label = `style anchor ${spec.styleId}/${anchor.assetId}`;
|
|
11515
12734
|
const dependency = await inspectParent(anchor.spec, lock, seen);
|
|
11516
12735
|
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
11517
|
-
if (!anchor.sha256 || !
|
|
12736
|
+
if (!anchor.sha256 || !existsSync9(anchor.file)) return { ready: false, reason: `${label} has no generated south-facing file yet` };
|
|
11518
12737
|
if (await sha256File(anchor.file) !== anchor.sha256) {
|
|
11519
12738
|
return { ready: false, reason: `${label} changed after the manifest was resolved` };
|
|
11520
12739
|
}
|
|
@@ -11523,19 +12742,31 @@ async function inspectStyleAnchor(spec, lock, seen) {
|
|
|
11523
12742
|
}
|
|
11524
12743
|
return { ready: true, reason: `${label} is current` };
|
|
11525
12744
|
}
|
|
12745
|
+
async function inspectTerrainConnect(spec, lock, seen) {
|
|
12746
|
+
for (const link of [spec.terrainConnect.lower, spec.terrainConnect.upper]) {
|
|
12747
|
+
if (!link) continue;
|
|
12748
|
+
const label = `connected set ${spec.styleId}/${link.assetId}`;
|
|
12749
|
+
const dependency = await inspectParent(link.spec, lock, seen);
|
|
12750
|
+
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
12751
|
+
if (!lock.entries[lockKey(link.spec.styleId, link.spec.assetId)]?.objectId) {
|
|
12752
|
+
return { ready: false, reason: `${label} has no PixelLab tileset id recorded` };
|
|
12753
|
+
}
|
|
12754
|
+
}
|
|
12755
|
+
return { ready: true, reason: "connected sets are current" };
|
|
12756
|
+
}
|
|
11526
12757
|
async function inspectScene(spec) {
|
|
11527
12758
|
const scene = spec.scene;
|
|
11528
12759
|
const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
|
|
11529
12760
|
if (scene.placement.type === "mask") files.push({ label: "scene mask", file: scene.placement.file, sha256: scene.placement.sha256 });
|
|
11530
12761
|
for (const { label, file, sha256: sha2562 } of files) {
|
|
11531
|
-
if (!sha2562 || !
|
|
12762
|
+
if (!sha2562 || !existsSync9(file)) return { ready: false, reason: `${label} does not exist yet: ${file}` };
|
|
11532
12763
|
if (await sha256File(file) !== sha2562) return { ready: false, reason: `${label} changed after the manifest was resolved` };
|
|
11533
12764
|
}
|
|
11534
12765
|
return { ready: true, reason: "scene is current" };
|
|
11535
12766
|
}
|
|
11536
12767
|
async function inspectVfxSprite(spec) {
|
|
11537
12768
|
const sprite = spec.vfx.sprite;
|
|
11538
|
-
if (!sprite.sha256 || !
|
|
12769
|
+
if (!sprite.sha256 || !existsSync9(sprite.file)) return { ready: false, reason: `vfx sprite does not exist yet: ${sprite.file}` };
|
|
11539
12770
|
if (await sha256File(sprite.file) !== sprite.sha256) return { ready: false, reason: "vfx sprite changed after the manifest was resolved" };
|
|
11540
12771
|
return { ready: true, reason: "vfx sprite is current" };
|
|
11541
12772
|
}
|
|
@@ -11551,7 +12782,7 @@ async function inspectOutfit(spec, lock, seen) {
|
|
|
11551
12782
|
const label = `outfit source ${spec.styleId}/${character.parentAssetId}`;
|
|
11552
12783
|
const dependency = await inspectParent(character.parentSpec, lock, seen);
|
|
11553
12784
|
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
11554
|
-
if (!
|
|
12785
|
+
if (!existsSync9(outfit.reference.path)) {
|
|
11555
12786
|
return { ready: false, reason: `outfit reference is missing: ${outfit.reference.path}` };
|
|
11556
12787
|
}
|
|
11557
12788
|
if (await sha256File(outfit.reference.path) !== outfit.reference.sha256) {
|
|
@@ -11565,7 +12796,7 @@ async function inspectCharacterParent(spec, lock, seen) {
|
|
|
11565
12796
|
const label = `${character.kind} parent ${spec.styleId}/${character.parentAssetId}`;
|
|
11566
12797
|
const dependency = await inspectParent(parentSpec, lock, seen);
|
|
11567
12798
|
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
11568
|
-
if (!character.parentSha256 || !character.parentFile || !
|
|
12799
|
+
if (!character.parentSha256 || !character.parentFile || !existsSync9(character.parentFile)) {
|
|
11569
12800
|
return { ready: false, reason: `${label} has no generated south-facing file yet` };
|
|
11570
12801
|
}
|
|
11571
12802
|
if (await sha256File(character.parentFile) !== character.parentSha256) {
|
|
@@ -11583,7 +12814,7 @@ async function inspectObjectProParent(spec, lock, seen) {
|
|
|
11583
12814
|
const label = `${object.kind} parent ${spec.styleId}/${object.parentAssetId}`;
|
|
11584
12815
|
const dependency = await inspectParent(parentSpec, lock, seen);
|
|
11585
12816
|
if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
|
|
11586
|
-
if (!object.parentSha256 || !object.parentFile || !
|
|
12817
|
+
if (!object.parentSha256 || !object.parentFile || !existsSync9(object.parentFile)) {
|
|
11587
12818
|
return { ready: false, reason: `${label} has no generated south-facing file yet` };
|
|
11588
12819
|
}
|
|
11589
12820
|
if (await sha256File(object.parentFile) !== object.parentSha256) {
|
|
@@ -11609,7 +12840,7 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
11609
12840
|
const members = await inspectSourceMembers(spec, lock);
|
|
11610
12841
|
if (!members.ready) return members;
|
|
11611
12842
|
} else {
|
|
11612
|
-
if (!revision.sourceSha256 || !
|
|
12843
|
+
if (!revision.sourceSha256 || !existsSync9(revision.sourceFile)) {
|
|
11613
12844
|
return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
|
|
11614
12845
|
}
|
|
11615
12846
|
if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
|
|
@@ -11617,7 +12848,7 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
11617
12848
|
}
|
|
11618
12849
|
}
|
|
11619
12850
|
if (revision.maskFile) {
|
|
11620
|
-
if (!revision.maskSha256 || !
|
|
12851
|
+
if (!revision.maskSha256 || !existsSync9(revision.maskFile)) {
|
|
11621
12852
|
return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
|
|
11622
12853
|
}
|
|
11623
12854
|
if (await sha256File(revision.maskFile) !== revision.maskSha256) {
|
|
@@ -11625,7 +12856,7 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
11625
12856
|
}
|
|
11626
12857
|
}
|
|
11627
12858
|
if (revision.lastFrameFile) {
|
|
11628
|
-
if (!revision.lastFrameSha256 || !
|
|
12859
|
+
if (!revision.lastFrameSha256 || !existsSync9(revision.lastFrameFile)) {
|
|
11629
12860
|
return { ready: false, reason: `revision last frame is missing: ${revision.lastFrameFile}` };
|
|
11630
12861
|
}
|
|
11631
12862
|
if (await sha256File(revision.lastFrameFile) !== revision.lastFrameSha256) {
|
|
@@ -11638,7 +12869,7 @@ async function inspectSourceMembers(spec, lock) {
|
|
|
11638
12869
|
const revision = spec.revision;
|
|
11639
12870
|
const members = revision.sourceMembers;
|
|
11640
12871
|
for (const member of members) {
|
|
11641
|
-
if (!
|
|
12872
|
+
if (!existsSync9(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
|
|
11642
12873
|
if (await sha256File(member.file) !== member.sha256) {
|
|
11643
12874
|
return { ready: false, reason: `revision source member changed after the manifest was resolved: ${member.file}` };
|
|
11644
12875
|
}
|
|
@@ -11647,8 +12878,8 @@ async function inspectSourceMembers(spec, lock) {
|
|
|
11647
12878
|
if (parent.source) return { ready: true, reason: "declared source set is present" };
|
|
11648
12879
|
const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
|
|
11649
12880
|
if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
|
|
11650
|
-
const recorded = entry.outputs.map((_, index) =>
|
|
11651
|
-
const found = members.map((member) =>
|
|
12881
|
+
const recorded = entry.outputs.map((_, index) => path14.resolve(currentEntryOutputPath(entry, parent, index))).sort();
|
|
12882
|
+
const found = members.map((member) => path14.resolve(member.file)).sort();
|
|
11652
12883
|
if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
|
|
11653
12884
|
return {
|
|
11654
12885
|
ready: false,
|
|
@@ -11689,6 +12920,10 @@ async function inspectParent(spec, lock, seen) {
|
|
|
11689
12920
|
const inputs = await inspectObjectProParent(spec, lock, nextSeen);
|
|
11690
12921
|
if (!inputs.ready) return inputs;
|
|
11691
12922
|
}
|
|
12923
|
+
if (spec.terrainConnect) {
|
|
12924
|
+
const inputs = await inspectTerrainConnect(spec, lock, nextSeen);
|
|
12925
|
+
if (!inputs.ready) return inputs;
|
|
12926
|
+
}
|
|
11692
12927
|
if (spec.quality) {
|
|
11693
12928
|
const quality = await inspectQualityProfile(spec, lock);
|
|
11694
12929
|
return quality?.state === "approved" ? { ready: true, reason: "approved quality output is current" } : {
|
|
@@ -11697,8 +12932,8 @@ async function inspectParent(spec, lock, seen) {
|
|
|
11697
12932
|
};
|
|
11698
12933
|
}
|
|
11699
12934
|
if (spec.source) {
|
|
11700
|
-
const source =
|
|
11701
|
-
return
|
|
12935
|
+
const source = path14.resolve(spec.root, spec.source);
|
|
12936
|
+
return existsSync9(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
|
|
11702
12937
|
}
|
|
11703
12938
|
const entry = lock.entries[key];
|
|
11704
12939
|
if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
|
|
@@ -11712,7 +12947,7 @@ async function inspectParent(spec, lock, seen) {
|
|
|
11712
12947
|
for (let index = 0; index < entry.outputs.length; index++) {
|
|
11713
12948
|
const output = entry.outputs[index];
|
|
11714
12949
|
const file = currentEntryOutputPath(entry, spec, index);
|
|
11715
|
-
if (!
|
|
12950
|
+
if (!existsSync9(file)) return { ready: false, reason: `parent output is missing: ${file}` };
|
|
11716
12951
|
if (await sha256File(file) !== output.sha256) {
|
|
11717
12952
|
return { ready: false, reason: `parent output was modified: ${file}` };
|
|
11718
12953
|
}
|
|
@@ -11734,9 +12969,21 @@ function dependencies(spec) {
|
|
|
11734
12969
|
spec.revision?.sourceSpec,
|
|
11735
12970
|
spec.character?.parentSpec,
|
|
11736
12971
|
spec.character?.styleAnchor?.spec,
|
|
11737
|
-
spec.objectPro?.parentSpec
|
|
12972
|
+
spec.objectPro?.parentSpec,
|
|
12973
|
+
spec.terrainConnect?.lower?.spec,
|
|
12974
|
+
spec.terrainConnect?.upper?.spec
|
|
11738
12975
|
].filter((parent) => Boolean(parent));
|
|
11739
12976
|
}
|
|
12977
|
+
function changedTerrainConnection(spec, entry, lock) {
|
|
12978
|
+
for (const side of ["lower", "upper"]) {
|
|
12979
|
+
const link = spec.terrainConnect?.[side];
|
|
12980
|
+
if (!link) continue;
|
|
12981
|
+
const other = lock.entries[lockKey(link.spec.styleId, link.spec.assetId)];
|
|
12982
|
+
const recorded = entry.terrainConnect?.[side];
|
|
12983
|
+
if (!recorded || recorded.sourceObjectId !== other?.objectId) return link.assetId;
|
|
12984
|
+
}
|
|
12985
|
+
return null;
|
|
12986
|
+
}
|
|
11740
12987
|
function resumeCommandForStatus(status) {
|
|
11741
12988
|
if (status === "pending" || status === "processing") return "poll";
|
|
11742
12989
|
if (status === "review") return "pick";
|
|
@@ -11768,6 +13015,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
11768
13015
|
const entry = lock.entries[key];
|
|
11769
13016
|
let state;
|
|
11770
13017
|
let reason;
|
|
13018
|
+
const changedConnection = entry ? changedTerrainConnection(spec, entry, lock) : null;
|
|
11771
13019
|
const revision = await inspectRevisionReadiness(spec, lock);
|
|
11772
13020
|
if (revision && !revision.ready) {
|
|
11773
13021
|
state = "blocked";
|
|
@@ -11776,7 +13024,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
11776
13024
|
state = "missing";
|
|
11777
13025
|
reason = "--force";
|
|
11778
13026
|
} else if (!entry && spec.source && spec.generator !== "frames" && spec.generator !== "vfx") {
|
|
11779
|
-
if (
|
|
13027
|
+
if (existsSync10(path15.resolve(spec.root, spec.source))) {
|
|
11780
13028
|
state = "ok";
|
|
11781
13029
|
reason = `placed from ${spec.source}; not generated`;
|
|
11782
13030
|
} else {
|
|
@@ -11784,7 +13032,7 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
11784
13032
|
reason = `declared source is missing: ${spec.source}`;
|
|
11785
13033
|
}
|
|
11786
13034
|
} else if (!entry) {
|
|
11787
|
-
if (
|
|
13035
|
+
if (existsSync10(spec.outFile)) {
|
|
11788
13036
|
state = "untracked";
|
|
11789
13037
|
reason = "file on disk, no provenance recorded";
|
|
11790
13038
|
} else {
|
|
@@ -11797,12 +13045,15 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
11797
13045
|
} else if (spec.mirror && !mirrorCurrent(entry, lock.entries[lockKey(spec.styleId, spec.mirror.sourceAssetId)])) {
|
|
11798
13046
|
state = "stale";
|
|
11799
13047
|
reason = `mirror source ${spec.mirror.sourceAssetId} changed; flip it again (no generation cost)`;
|
|
11800
|
-
} else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !
|
|
13048
|
+
} else if (spec.mirror && entry.status === "downloaded" && entry.outputs.some((_output, index) => !existsSync10(currentEntryOutputPath(entry, spec, index)))) {
|
|
11801
13049
|
state = "stale";
|
|
11802
13050
|
reason = "mirror output missing on disk; flip it again (no generation cost)";
|
|
11803
13051
|
} else if (spec.character?.kind === "outfit" && entry.outfit?.sourceSha256 !== mirrorSourceHash(lock.entries[lockKey(spec.styleId, spec.character.parentAssetId)] ?? { outputs: [] })) {
|
|
11804
13052
|
state = "stale";
|
|
11805
13053
|
reason = `outfit source ${spec.character.parentAssetId} changed; re-clothe the loop again`;
|
|
13054
|
+
} else if (changedConnection) {
|
|
13055
|
+
state = "stale";
|
|
13056
|
+
reason = `connected set ${changedConnection} was regenerated; draw this set from its new terrain`;
|
|
11806
13057
|
} else if (entry.status === "download-failed") {
|
|
11807
13058
|
state = "recoverable";
|
|
11808
13059
|
const force = entry.error?.includes("pass --force") ? " --force" : "";
|
|
@@ -11825,9 +13076,9 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
11825
13076
|
} else if (entry.status !== "downloaded") {
|
|
11826
13077
|
state = "in-flight";
|
|
11827
13078
|
reason = `awaiting ${entry.status}`;
|
|
11828
|
-
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !
|
|
13079
|
+
} else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !existsSync10(currentEntryOutputPath(entry, spec, index)))) {
|
|
11829
13080
|
state = "orphaned";
|
|
11830
|
-
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !
|
|
13081
|
+
reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !existsSync10(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
|
|
11831
13082
|
} else if (await anyOutputModified(entry, spec)) {
|
|
11832
13083
|
state = "orphaned";
|
|
11833
13084
|
reason = "output modified since download";
|
|
@@ -11918,13 +13169,13 @@ function summarize(plan) {
|
|
|
11918
13169
|
|
|
11919
13170
|
// src/project.ts
|
|
11920
13171
|
async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
|
|
11921
|
-
const absolute =
|
|
11922
|
-
const root =
|
|
13172
|
+
const absolute = path16.resolve(manifestPath);
|
|
13173
|
+
const root = path16.dirname(absolute);
|
|
11923
13174
|
if (opts.env !== false) {
|
|
11924
13175
|
loadEnvFiles(root);
|
|
11925
|
-
if (
|
|
13176
|
+
if (path16.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
|
|
11926
13177
|
}
|
|
11927
|
-
const lockPath =
|
|
13178
|
+
const lockPath = path16.resolve(opts.lockPath ?? path16.join(root, "pixelkiln.lock.json"));
|
|
11928
13179
|
const loaded = await loadManifest(absolute);
|
|
11929
13180
|
const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
|
|
11930
13181
|
const lock = await loadLock(lockPath);
|
|
@@ -11953,47 +13204,10 @@ async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {})
|
|
|
11953
13204
|
};
|
|
11954
13205
|
}
|
|
11955
13206
|
|
|
11956
|
-
// src/source-url.ts
|
|
11957
|
-
var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
|
|
11958
|
-
"access_token",
|
|
11959
|
-
"apikey",
|
|
11960
|
-
"api_key",
|
|
11961
|
-
"auth",
|
|
11962
|
-
"authorization",
|
|
11963
|
-
"awsaccesskeyid",
|
|
11964
|
-
"googleaccessid",
|
|
11965
|
-
"key",
|
|
11966
|
-
"sig"
|
|
11967
|
-
]);
|
|
11968
|
-
function isSensitiveSourceUrl(value) {
|
|
11969
|
-
let parsed;
|
|
11970
|
-
try {
|
|
11971
|
-
parsed = new URL(value);
|
|
11972
|
-
} catch {
|
|
11973
|
-
return false;
|
|
11974
|
-
}
|
|
11975
|
-
if (parsed.username || parsed.password) return true;
|
|
11976
|
-
for (const key of parsed.searchParams.keys()) {
|
|
11977
|
-
const normalized = key.toLowerCase();
|
|
11978
|
-
if (SENSITIVE_QUERY_KEYS.has(normalized) || normalized.includes("credential") || normalized.includes("signature") || normalized.includes("security-token") || normalized.includes("token")) return true;
|
|
11979
|
-
}
|
|
11980
|
-
return false;
|
|
11981
|
-
}
|
|
11982
|
-
function shouldPersistSourceUrl(value) {
|
|
11983
|
-
let parsed;
|
|
11984
|
-
try {
|
|
11985
|
-
parsed = new URL(value);
|
|
11986
|
-
} catch {
|
|
11987
|
-
return true;
|
|
11988
|
-
}
|
|
11989
|
-
if (parsed.protocol === "data:" || parsed.protocol === "file:") return false;
|
|
11990
|
-
return !isSensitiveSourceUrl(value);
|
|
11991
|
-
}
|
|
11992
|
-
|
|
11993
13207
|
// src/recipes.ts
|
|
11994
|
-
import
|
|
11995
|
-
import { existsSync as
|
|
11996
|
-
import { readdir, readFile as
|
|
13208
|
+
import path17 from "path";
|
|
13209
|
+
import { existsSync as existsSync11 } from "fs";
|
|
13210
|
+
import { readdir, readFile as readFile9 } from "fs/promises";
|
|
11997
13211
|
import { fileURLToPath } from "url";
|
|
11998
13212
|
import { z as z4 } from "zod";
|
|
11999
13213
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
@@ -12002,7 +13216,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
12002
13216
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
12003
13217
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
12004
13218
|
var PortablePathSchema = z4.string().min(1).refine((value) => {
|
|
12005
|
-
if (value.includes("\\") ||
|
|
13219
|
+
if (value.includes("\\") || path17.posix.isAbsolute(value)) return false;
|
|
12006
13220
|
const parts = value.split("/");
|
|
12007
13221
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
12008
13222
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -12178,10 +13392,10 @@ function message3(error) {
|
|
|
12178
13392
|
return error instanceof Error ? error.message : String(error);
|
|
12179
13393
|
}
|
|
12180
13394
|
async function readRecipeFile(recipePath, bundled) {
|
|
12181
|
-
const absolute =
|
|
13395
|
+
const absolute = path17.resolve(recipePath);
|
|
12182
13396
|
let raw;
|
|
12183
13397
|
try {
|
|
12184
|
-
raw = JSON.parse(await
|
|
13398
|
+
raw = JSON.parse(await readFile9(absolute, "utf8"));
|
|
12185
13399
|
} catch (error) {
|
|
12186
13400
|
throw new Error(`Could not read recipe ${absolute}: ${message3(error)}`, { cause: error });
|
|
12187
13401
|
}
|
|
@@ -12192,13 +13406,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
12192
13406
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
12193
13407
|
);
|
|
12194
13408
|
}
|
|
12195
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
13409
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path17.dirname(absolute), bundled };
|
|
12196
13410
|
}
|
|
12197
13411
|
async function walkRecipeFiles(root) {
|
|
12198
|
-
if (!
|
|
13412
|
+
if (!existsSync11(root)) return [];
|
|
12199
13413
|
const out = [];
|
|
12200
13414
|
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
12201
|
-
const resolved =
|
|
13415
|
+
const resolved = path17.join(root, entry.name);
|
|
12202
13416
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
12203
13417
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
12204
13418
|
}
|
|
@@ -12209,7 +13423,7 @@ function bundledRecipeRoot() {
|
|
|
12209
13423
|
}
|
|
12210
13424
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
12211
13425
|
const recipes = await Promise.all(
|
|
12212
|
-
(await walkRecipeFiles(
|
|
13426
|
+
(await walkRecipeFiles(path17.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
12213
13427
|
);
|
|
12214
13428
|
return recipes.sort(
|
|
12215
13429
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -12232,9 +13446,9 @@ function parseSelector(selector) {
|
|
|
12232
13446
|
return { id: match[1], version: match[2] };
|
|
12233
13447
|
}
|
|
12234
13448
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
12235
|
-
const local =
|
|
12236
|
-
if (
|
|
12237
|
-
const recipePath =
|
|
13449
|
+
const local = path17.resolve(target);
|
|
13450
|
+
if (existsSync11(local)) {
|
|
13451
|
+
const recipePath = path17.basename(local) === RECIPE_FILE ? local : path17.join(local, RECIPE_FILE);
|
|
12238
13452
|
return readRecipeFile(recipePath, false);
|
|
12239
13453
|
}
|
|
12240
13454
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -12248,8 +13462,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
12248
13462
|
return matches[0];
|
|
12249
13463
|
}
|
|
12250
13464
|
async function verifyFile(root, file) {
|
|
12251
|
-
const absolute =
|
|
12252
|
-
if (!
|
|
13465
|
+
const absolute = path17.resolve(root, ...file.path.split("/"));
|
|
13466
|
+
if (!existsSync11(absolute)) {
|
|
12253
13467
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
12254
13468
|
}
|
|
12255
13469
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -12264,7 +13478,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12264
13478
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12265
13479
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
12266
13480
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
12267
|
-
const modelRoot = options.modelRoot ?
|
|
13481
|
+
const modelRoot = options.modelRoot ? path17.resolve(options.modelRoot) : null;
|
|
12268
13482
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
12269
13483
|
...await verifyFile(modelRoot, model),
|
|
12270
13484
|
source: model.source,
|
|
@@ -12298,7 +13512,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
12298
13512
|
};
|
|
12299
13513
|
}
|
|
12300
13514
|
function renderStyle(style, destination, cwd) {
|
|
12301
|
-
const portableDir =
|
|
13515
|
+
const portableDir = path17.relative(cwd, destination).split(path17.sep).join("/") || ".";
|
|
12302
13516
|
const replace = (value) => {
|
|
12303
13517
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
12304
13518
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -12310,25 +13524,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
12310
13524
|
return replace(style);
|
|
12311
13525
|
}
|
|
12312
13526
|
async function installRecipe(target, options = {}) {
|
|
12313
|
-
const cwd =
|
|
13527
|
+
const cwd = path17.resolve(options.cwd ?? process.cwd());
|
|
12314
13528
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
12315
13529
|
const verification = await verifyRecipe(loaded.path);
|
|
12316
13530
|
if (!verification.ok) {
|
|
12317
13531
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
12318
13532
|
}
|
|
12319
|
-
const destination = options.out ?
|
|
13533
|
+
const destination = options.out ? path17.resolve(cwd, options.out) : path17.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
12320
13534
|
const sources = [
|
|
12321
13535
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
12322
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
13536
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path17.join(loaded.dir, ...file.path.split("/")) }))
|
|
12323
13537
|
];
|
|
12324
13538
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
12325
|
-
path:
|
|
12326
|
-
data: await
|
|
13539
|
+
path: path17.join(destination, ...file.path.split("/")),
|
|
13540
|
+
data: await readFile9(file.source)
|
|
12327
13541
|
})));
|
|
12328
13542
|
if (!options.force) {
|
|
12329
13543
|
for (const file of files) {
|
|
12330
|
-
if (!
|
|
12331
|
-
const current = await
|
|
13544
|
+
if (!existsSync11(file.path)) continue;
|
|
13545
|
+
const current = await readFile9(file.path);
|
|
12332
13546
|
if (!current.equals(file.data)) {
|
|
12333
13547
|
throw new OverwriteRefusedError(
|
|
12334
13548
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -12352,9 +13566,9 @@ async function installRecipe(target, options = {}) {
|
|
|
12352
13566
|
}
|
|
12353
13567
|
|
|
12354
13568
|
// src/pipeline/audit.ts
|
|
12355
|
-
import { readFile as
|
|
12356
|
-
import { existsSync as
|
|
12357
|
-
import
|
|
13569
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
13570
|
+
import { existsSync as existsSync12 } from "fs";
|
|
13571
|
+
import path18 from "path";
|
|
12358
13572
|
function paletteDistance(asset, reference) {
|
|
12359
13573
|
if (!asset.length || !reference.length) return 0;
|
|
12360
13574
|
let total = 0;
|
|
@@ -12390,12 +13604,12 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
12390
13604
|
const unreadable = [];
|
|
12391
13605
|
for (const spec of mine) {
|
|
12392
13606
|
for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
|
|
12393
|
-
if (!
|
|
13607
|
+
if (!existsSync12(output.absolutePath)) {
|
|
12394
13608
|
missing.push(output.id);
|
|
12395
13609
|
continue;
|
|
12396
13610
|
}
|
|
12397
13611
|
try {
|
|
12398
|
-
const png = decodePng(await
|
|
13612
|
+
const png = decodePng(await readFile10(output.absolutePath));
|
|
12399
13613
|
const palette = extractPalette(png, 12);
|
|
12400
13614
|
assets.push({
|
|
12401
13615
|
assetId: spec.assetId,
|
|
@@ -12419,10 +13633,10 @@ async function auditStyle(loaded, specs, styleId, lock) {
|
|
|
12419
13633
|
let referenceFromStyleImages = false;
|
|
12420
13634
|
const refPalettes = [];
|
|
12421
13635
|
for (const img of style.styleImages) {
|
|
12422
|
-
const abs =
|
|
12423
|
-
if (!
|
|
13636
|
+
const abs = path18.resolve(loaded.root, img.path);
|
|
13637
|
+
if (!existsSync12(abs)) continue;
|
|
12424
13638
|
try {
|
|
12425
|
-
refPalettes.push(extractPalette(decodePng(await
|
|
13639
|
+
refPalettes.push(extractPalette(decodePng(await readFile10(abs)), 12));
|
|
12426
13640
|
} catch {
|
|
12427
13641
|
}
|
|
12428
13642
|
}
|
|
@@ -12497,144 +13711,6 @@ function hex(c) {
|
|
|
12497
13711
|
return `#${[c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
12498
13712
|
}
|
|
12499
13713
|
|
|
12500
|
-
// src/pixellab-utils.ts
|
|
12501
|
-
import { existsSync as existsSync12 } from "fs";
|
|
12502
|
-
import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
12503
|
-
import path18 from "path";
|
|
12504
|
-
var UNZOOM_MIN_SIDE = 256;
|
|
12505
|
-
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
12506
|
-
function defaultUnzoomOut(input) {
|
|
12507
|
-
const ext = path18.extname(input);
|
|
12508
|
-
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
12509
|
-
}
|
|
12510
|
-
async function unzoomFile(client2, input, opts = {}) {
|
|
12511
|
-
const out = path18.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
12512
|
-
if (path18.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
12513
|
-
if (existsSync12(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
12514
|
-
const bytes = await readFile10(input);
|
|
12515
|
-
const metadata = imageMetadata(bytes);
|
|
12516
|
-
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
12517
|
-
const { width, height, format } = metadata;
|
|
12518
|
-
if (width < UNZOOM_MIN_SIDE || height < UNZOOM_MIN_SIDE) {
|
|
12519
|
-
throw new Error(
|
|
12520
|
-
`${input} is ${width}x${height}; PixelLab's unzoom needs at least ${UNZOOM_MIN_SIDE}x${UNZOOM_MIN_SIDE} to find the pixel grid, so this is probably already at its native size`
|
|
12521
|
-
);
|
|
12522
|
-
}
|
|
12523
|
-
if (width * height > UNZOOM_MAX_AREA) {
|
|
12524
|
-
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
12525
|
-
}
|
|
12526
|
-
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
12527
|
-
await mkdir5(path18.dirname(out), { recursive: true });
|
|
12528
|
-
await writeFile5(out, res.png);
|
|
12529
|
-
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
12530
|
-
}
|
|
12531
|
-
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
12532
|
-
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
12533
|
-
var FONT_COST_GENERATIONS = 15;
|
|
12534
|
-
function fontOutputPaths(out) {
|
|
12535
|
-
const base = path18.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
12536
|
-
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
12537
|
-
}
|
|
12538
|
-
async function generateFont(client2, opts) {
|
|
12539
|
-
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
12540
|
-
if (!opts.force) {
|
|
12541
|
-
for (const file of [ttf, atlas]) {
|
|
12542
|
-
if (existsSync12(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
12543
|
-
}
|
|
12544
|
-
}
|
|
12545
|
-
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
12546
|
-
const interval = opts.intervalMs ?? 5e3;
|
|
12547
|
-
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
12548
|
-
const submitted = await client2.generateFontPro({
|
|
12549
|
-
description: opts.description,
|
|
12550
|
-
weight: opts.weight,
|
|
12551
|
-
glyphPx: opts.glyphPx,
|
|
12552
|
-
seed: opts.seed,
|
|
12553
|
-
fontName: opts.fontName
|
|
12554
|
-
});
|
|
12555
|
-
const jobId = submitted.background_job_id;
|
|
12556
|
-
const started = Date.now();
|
|
12557
|
-
for (; ; ) {
|
|
12558
|
-
const job = await client2.getFontJob(jobId);
|
|
12559
|
-
opts.onProgress?.(job.status);
|
|
12560
|
-
if (job.status === "failed") throw new Error(`PixelLab font job ${jobId} failed upstream`);
|
|
12561
|
-
if (job.status === "completed") {
|
|
12562
|
-
if (!job.download_ttf_url || !job.download_atlas_url) {
|
|
12563
|
-
throw new Error(`PixelLab font job ${jobId} completed without both download URLs`);
|
|
12564
|
-
}
|
|
12565
|
-
const [ttfBytes, atlasBytes] = await Promise.all([
|
|
12566
|
-
client2.download(job.download_ttf_url),
|
|
12567
|
-
client2.download(job.download_atlas_url)
|
|
12568
|
-
]);
|
|
12569
|
-
await mkdir5(path18.dirname(ttf), { recursive: true });
|
|
12570
|
-
await writeFile5(ttf, ttfBytes);
|
|
12571
|
-
await writeFile5(atlas, atlasBytes);
|
|
12572
|
-
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
12573
|
-
}
|
|
12574
|
-
if (Date.now() - started > timeout) {
|
|
12575
|
-
throw new Error(
|
|
12576
|
-
`PixelLab font job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
12577
|
-
);
|
|
12578
|
-
}
|
|
12579
|
-
await sleep3(interval);
|
|
12580
|
-
}
|
|
12581
|
-
}
|
|
12582
|
-
var PIXELATE_COST_GENERATIONS = 6;
|
|
12583
|
-
var PIXELATE_MAX_SIDE = 2048;
|
|
12584
|
-
function defaultPixelateOut(input) {
|
|
12585
|
-
const ext = path18.extname(input);
|
|
12586
|
-
return `${ext ? input.slice(0, -ext.length) : input}.pixel.png`;
|
|
12587
|
-
}
|
|
12588
|
-
async function pixelateFile(client2, input, opts = {}) {
|
|
12589
|
-
const out = path18.resolve(opts.out ?? defaultPixelateOut(input));
|
|
12590
|
-
if (path18.resolve(input) === out) throw new Error("pixelate will not overwrite its own input; pass a different --out");
|
|
12591
|
-
if (existsSync12(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
12592
|
-
const bytes = await readFile10(input);
|
|
12593
|
-
const metadata = imageMetadata(bytes);
|
|
12594
|
-
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
12595
|
-
const { width, height, format } = metadata;
|
|
12596
|
-
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
12597
|
-
const interval = opts.intervalMs ?? 5e3;
|
|
12598
|
-
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
12599
|
-
const submitted = await client2.imageToPixelartProFlash({
|
|
12600
|
-
image: { base64: bytes.toString("base64"), format },
|
|
12601
|
-
description: opts.description,
|
|
12602
|
-
seed: opts.seed
|
|
12603
|
-
});
|
|
12604
|
-
const jobId = submitted.background_job_id;
|
|
12605
|
-
const started = Date.now();
|
|
12606
|
-
for (; ; ) {
|
|
12607
|
-
const job = await client2.getBackgroundJob(jobId);
|
|
12608
|
-
opts.onProgress?.(job.status);
|
|
12609
|
-
if (job.status === "failed") throw new Error(`PixelLab pixelate job ${jobId} failed upstream`);
|
|
12610
|
-
if (job.status === "completed") {
|
|
12611
|
-
const images = job.last_response?.images;
|
|
12612
|
-
const first = Array.isArray(images) ? images[0] : void 0;
|
|
12613
|
-
if (!first || typeof first.base64 !== "string" || !first.base64) {
|
|
12614
|
-
throw new Error(`PixelLab pixelate job ${jobId} completed without an image at last_response.images[0]`);
|
|
12615
|
-
}
|
|
12616
|
-
const png = Buffer.from(first.base64, "base64");
|
|
12617
|
-
const size = imageMetadata(png);
|
|
12618
|
-
await mkdir5(path18.dirname(out), { recursive: true });
|
|
12619
|
-
await writeFile5(out, png);
|
|
12620
|
-
return {
|
|
12621
|
-
jobId,
|
|
12622
|
-
out,
|
|
12623
|
-
original: { width, height },
|
|
12624
|
-
pixelated: { width: size?.width ?? Number(first.width), height: size?.height ?? Number(first.height) },
|
|
12625
|
-
imageId: submitted.image_id,
|
|
12626
|
-
usage: job.usage ?? null
|
|
12627
|
-
};
|
|
12628
|
-
}
|
|
12629
|
-
if (Date.now() - started > timeout) {
|
|
12630
|
-
throw new Error(
|
|
12631
|
-
`PixelLab pixelate job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
12632
|
-
);
|
|
12633
|
-
}
|
|
12634
|
-
await sleep3(interval);
|
|
12635
|
-
}
|
|
12636
|
-
}
|
|
12637
|
-
|
|
12638
13714
|
// src/pipeline/quality-regression.ts
|
|
12639
13715
|
import path19 from "path";
|
|
12640
13716
|
import { existsSync as existsSync13 } from "fs";
|
|
@@ -13446,6 +14522,7 @@ function retireGeneration(entry, retiredAt) {
|
|
|
13446
14522
|
billed: entry.billed,
|
|
13447
14523
|
provider: entry.provider,
|
|
13448
14524
|
postprocess: entry.postprocess,
|
|
14525
|
+
...entry.terrainConnect ? { terrainConnect: entry.terrainConnect } : {},
|
|
13449
14526
|
retiredAt
|
|
13450
14527
|
};
|
|
13451
14528
|
}
|
|
@@ -13537,6 +14614,9 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
|
|
|
13537
14614
|
billed: generation.billed,
|
|
13538
14615
|
provider: generation.provider,
|
|
13539
14616
|
postprocess: generation.postprocess,
|
|
14617
|
+
// A connected set comes back with the connection it was drawn from, so
|
|
14618
|
+
// the plan compares the restored generation, not the one it replaced.
|
|
14619
|
+
terrainConnect: generation.terrainConnect,
|
|
13540
14620
|
history
|
|
13541
14621
|
});
|
|
13542
14622
|
await saveLock(lockPath, lock);
|
|
@@ -14557,6 +15637,22 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
14557
15637
|
const submittedAt = resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
14558
15638
|
const history = historyAfterReplacing(previousEntry, historyLimit(loaded.manifest), submittedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
14559
15639
|
const outfitParentEntry = spec.character?.kind === "outfit" && spec.character.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
|
|
15640
|
+
const terrainLinks = {};
|
|
15641
|
+
const terrainConnect = {};
|
|
15642
|
+
for (const side of ["lower", "upper"]) {
|
|
15643
|
+
const link = spec.terrainConnect?.[side];
|
|
15644
|
+
if (!link) continue;
|
|
15645
|
+
const other = lock.entries[lockKey(link.spec.styleId, link.spec.assetId)];
|
|
15646
|
+
const objectId = other.objectId;
|
|
15647
|
+
const terrainIds = other.providerMetadata?.[provider.id]?.terrainIds;
|
|
15648
|
+
const terrainId = terrainIds?.[link.terrain];
|
|
15649
|
+
terrainLinks[side] = {
|
|
15650
|
+
objectId,
|
|
15651
|
+
terrain: link.terrain,
|
|
15652
|
+
...typeof terrainId === "string" && terrainId ? { terrainId } : {}
|
|
15653
|
+
};
|
|
15654
|
+
terrainConnect[side] = { sourceAssetId: link.assetId, sourceObjectId: objectId };
|
|
15655
|
+
}
|
|
14560
15656
|
upsert(lock, key, {
|
|
14561
15657
|
styleId: spec.styleId,
|
|
14562
15658
|
assetId: spec.assetId,
|
|
@@ -14586,6 +15682,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
14586
15682
|
...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
|
|
14587
15683
|
...spec.revision.description ? { description: spec.revision.description } : {},
|
|
14588
15684
|
...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
|
|
15685
|
+
...spec.revision.convention ? { convention: spec.revision.convention } : {},
|
|
14589
15686
|
...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
|
|
14590
15687
|
...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
|
|
14591
15688
|
...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
|
|
@@ -14596,6 +15693,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
14596
15693
|
sourceSha256: mirrorSourceHash(outfitParentEntry),
|
|
14597
15694
|
referenceSha256: spec.character.outfit.reference.sha256
|
|
14598
15695
|
} : null,
|
|
15696
|
+
terrainConnect: spec.terrainConnect ? terrainConnect : void 0,
|
|
14599
15697
|
status: "pending",
|
|
14600
15698
|
jobId: previousJobId ?? null,
|
|
14601
15699
|
...previousJobId ? { submissionComplete: false } : { submissionComplete: void 0 },
|
|
@@ -14642,6 +15740,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
14642
15740
|
...parentEntry?.objectId ? { parentObjectId: parentEntry.objectId } : {},
|
|
14643
15741
|
...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
|
|
14644
15742
|
...parentOutputs ? { parentOutputs } : {},
|
|
15743
|
+
...spec.terrainConnect ? { terrainLinks } : {},
|
|
14645
15744
|
...replacedMetadata ? { replacedMetadata } : {},
|
|
14646
15745
|
...lastGenerationMetadata ? { lastGenerationMetadata } : {},
|
|
14647
15746
|
...previousMetadata ? { previousMetadata } : {},
|
|
@@ -16140,6 +17239,7 @@ var NewAssetSchema = z8.object({
|
|
|
16140
17239
|
/** A short description of the foreground, sent as PixelLab's text hint. */
|
|
16141
17240
|
description: z8.string().min(1).optional()
|
|
16142
17241
|
}).strict(),
|
|
17242
|
+
z8.object({ mode: z8.literal("normal-map"), from: z8.string().min(1), convention: NormalMapConventionSchema.optional() }).strict(),
|
|
16143
17243
|
z8.object({
|
|
16144
17244
|
mode: z8.enum(["animate", "animate-pixminimax"]),
|
|
16145
17245
|
from: z8.string().min(1),
|
|
@@ -19618,6 +20718,12 @@ export {
|
|
|
19618
20718
|
MapScenePlacementSchema,
|
|
19619
20719
|
MapSceneSchema,
|
|
19620
20720
|
MediaType,
|
|
20721
|
+
NORMAL_MAP_CONVENTIONS,
|
|
20722
|
+
NORMAL_MAP_COST_GENERATIONS,
|
|
20723
|
+
NORMAL_MAP_MAX_FRAMES,
|
|
20724
|
+
NORMAL_MAP_MAX_SIDE,
|
|
20725
|
+
NORMAL_MAP_MIN_SIDE,
|
|
20726
|
+
NormalMapConventionSchema,
|
|
19621
20727
|
OverwriteRefusedError,
|
|
19622
20728
|
PIXELATE_COST_GENERATIONS,
|
|
19623
20729
|
PIXELATE_MAX_SIDE,
|
|
@@ -19645,12 +20751,17 @@ export {
|
|
|
19645
20751
|
ScenarioProvider,
|
|
19646
20752
|
StyleInputSchema,
|
|
19647
20753
|
StyleSchema,
|
|
20754
|
+
TerrainConnectSchema,
|
|
20755
|
+
TerrainLinkSchema,
|
|
20756
|
+
TerrainSideSchema,
|
|
19648
20757
|
UNZOOM_MAX_AREA,
|
|
19649
20758
|
UNZOOM_MIN_SIDE,
|
|
19650
20759
|
UiElementSchema,
|
|
19651
20760
|
UiPieceSchema,
|
|
19652
20761
|
UnsupportedCapabilityError,
|
|
19653
20762
|
UsageError,
|
|
20763
|
+
VFX_ARTISTS,
|
|
20764
|
+
VFX_ARTIST_MAX_SPRITE,
|
|
19654
20765
|
VFX_TOOL,
|
|
19655
20766
|
VfxPartSchema,
|
|
19656
20767
|
WorkspaceProjectSchema,
|
|
@@ -19683,6 +20794,7 @@ export {
|
|
|
19683
20794
|
createProvider,
|
|
19684
20795
|
currentEntryOutputPath,
|
|
19685
20796
|
currentOutputPath,
|
|
20797
|
+
defaultNormalMapOut,
|
|
19686
20798
|
defaultOpenCommand,
|
|
19687
20799
|
defaultPixelateOut,
|
|
19688
20800
|
defaultUnzoomOut,
|
|
@@ -19744,6 +20856,7 @@ export {
|
|
|
19744
20856
|
loadWorkspace,
|
|
19745
20857
|
lockKey,
|
|
19746
20858
|
makeVfx,
|
|
20859
|
+
makeVfxWithArtist,
|
|
19747
20860
|
matchOrphanStyle,
|
|
19748
20861
|
mcpClientFromEnv,
|
|
19749
20862
|
measureBalanceChange,
|
|
@@ -19755,6 +20868,9 @@ export {
|
|
|
19755
20868
|
mergePalettes,
|
|
19756
20869
|
mountSprites,
|
|
19757
20870
|
mountStyle,
|
|
20871
|
+
normalMapFile,
|
|
20872
|
+
normalMapResultPng,
|
|
20873
|
+
normalMapSizeProblem,
|
|
19758
20874
|
normalizeLockOutputPaths,
|
|
19759
20875
|
normalizeRefinementPalette,
|
|
19760
20876
|
normalizeTerrainTiles,
|
|
@@ -19772,6 +20888,7 @@ export {
|
|
|
19772
20888
|
parseMcpBody,
|
|
19773
20889
|
parseProFlashQuote,
|
|
19774
20890
|
parseTerrainDescriptions,
|
|
20891
|
+
parseVfxArtistResult,
|
|
19775
20892
|
parseVfxExport,
|
|
19776
20893
|
parseVfxRecipe,
|
|
19777
20894
|
parseVfxStep,
|
|
@@ -19795,6 +20912,7 @@ export {
|
|
|
19795
20912
|
readHandEditCompanion,
|
|
19796
20913
|
readQualityBaseline,
|
|
19797
20914
|
readSavedVfxRecipe,
|
|
20915
|
+
readVfxConversation,
|
|
19798
20916
|
readVfxRecipe,
|
|
19799
20917
|
recipeDigest,
|
|
19800
20918
|
referencedHashes,
|
|
@@ -19857,6 +20975,7 @@ export {
|
|
|
19857
20975
|
submit,
|
|
19858
20976
|
summarize,
|
|
19859
20977
|
tagAdopted,
|
|
20978
|
+
terrainCost,
|
|
19860
20979
|
terrainTileCount,
|
|
19861
20980
|
tileFeatureOutputCount,
|
|
19862
20981
|
tileVariationCount,
|
|
@@ -19870,6 +20989,13 @@ export {
|
|
|
19870
20989
|
validateWorkspace,
|
|
19871
20990
|
verifyArtifactBundle,
|
|
19872
20991
|
verifyRecipe,
|
|
20992
|
+
vfxArtistCeiling,
|
|
20993
|
+
vfxArtistFiles,
|
|
20994
|
+
vfxArtistFrameFacts,
|
|
20995
|
+
vfxArtistQuote,
|
|
20996
|
+
vfxArtistSprite,
|
|
20997
|
+
vfxAtlasFrames,
|
|
20998
|
+
vfxJobFailure,
|
|
19873
20999
|
vfxOutputPaths,
|
|
19874
21000
|
vfxRecipeSha256,
|
|
19875
21001
|
withArtifactManifest,
|