pixelkiln 0.61.0 → 0.63.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/dist/cli.js +952 -214
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1034 -368
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +559 -210
- package/dist/index.d.ts +559 -210
- package/dist/index.js +997 -342
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +39 -0
- package/docs/CLI.md +63 -3
- package/docs/ENDPOINTS.md +32 -6
- package/docs/GENERATORS.md +45 -0
- package/docs/MANIFEST.md +7 -7
- package/docs/PIXELLAB.md +6 -3
- package/docs/REVISIONS.md +128 -19
- package/package.json +1 -1
- package/schema/manifest.schema.json +24 -3
- package/schema/recipe.schema.json +2 -1
- package/skills/pixelkiln/SKILL.md +3 -1
- package/skills/pixelkiln/references/pixellab-roadmap.md +47 -20
- package/skills/pixelkiln/references/pixellab.md +24 -8
- package/skills/pixelkiln/references/revisions.md +26 -5
package/dist/cli.js
CHANGED
|
@@ -67,8 +67,8 @@ function exitCodeFor(err) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// src/cli/main.ts
|
|
70
|
-
import { existsSync as
|
|
71
|
-
import { readFile as
|
|
70
|
+
import { existsSync as existsSync33 } from "fs";
|
|
71
|
+
import { readFile as readFile30 } from "fs/promises";
|
|
72
72
|
|
|
73
73
|
// src/cli/args.ts
|
|
74
74
|
import path from "path";
|
|
@@ -103,7 +103,11 @@ var VALUE_FLAGS = [
|
|
|
103
103
|
"--reviewer",
|
|
104
104
|
"--note",
|
|
105
105
|
"--model-root",
|
|
106
|
-
"--generation"
|
|
106
|
+
"--generation",
|
|
107
|
+
"--quantize",
|
|
108
|
+
"--description",
|
|
109
|
+
"--weight",
|
|
110
|
+
"--glyph-px"
|
|
107
111
|
];
|
|
108
112
|
var BOOL_FLAGS = [
|
|
109
113
|
"--force",
|
|
@@ -153,6 +157,8 @@ var COMMANDS = [
|
|
|
153
157
|
"refine",
|
|
154
158
|
"recipe",
|
|
155
159
|
"workspace",
|
|
160
|
+
"unzoom",
|
|
161
|
+
"font",
|
|
156
162
|
"help",
|
|
157
163
|
"--help",
|
|
158
164
|
"-h",
|
|
@@ -406,7 +412,11 @@ function parseArgs(argv) {
|
|
|
406
412
|
workspace: get("--workspace"),
|
|
407
413
|
target,
|
|
408
414
|
provider: get("--provider"),
|
|
409
|
-
account: get("--account")
|
|
415
|
+
account: get("--account"),
|
|
416
|
+
quantize: numberOption("--quantize", { min: -1, max: 256, integer: true }),
|
|
417
|
+
description: get("--description"),
|
|
418
|
+
weight: get("--weight"),
|
|
419
|
+
glyphPx: numberOption("--glyph-px", { min: 8, max: 64, integer: true })
|
|
410
420
|
};
|
|
411
421
|
}
|
|
412
422
|
|
|
@@ -461,6 +471,10 @@ Commands
|
|
|
461
471
|
verified against pinned hashes. Offline once installed.
|
|
462
472
|
workspace Register sibling projects and derive account-wide claims/status.
|
|
463
473
|
add/remove/list/status/claims. Offline.
|
|
474
|
+
unzoom PixelLab: shrink an upscaled image (--from) back to its native
|
|
475
|
+
pixel grid before using it as a style or reference image.
|
|
476
|
+
font PixelLab: generate a pixel font (--description, --out); writes a
|
|
477
|
+
.ttf and a glyph atlas PNG. 25 generations; asks first.
|
|
464
478
|
|
|
465
479
|
Options
|
|
466
480
|
--columns <n> pack/export: sprites or tiles per row (default: near-square)
|
|
@@ -482,6 +496,10 @@ Options
|
|
|
482
496
|
--note <text> refine approve: optional review note
|
|
483
497
|
--model-root <dir> recipe verify: also hash required local model files
|
|
484
498
|
--generation <n|hash> restore: the previous generation to bring back (1 = newest)
|
|
499
|
+
--quantize <n> unzoom: 0 auto palette (default), -1 keep all colors, 2-256 exact
|
|
500
|
+
--description <text> font: the style to draw, e.g. "warm orange arcade font"
|
|
501
|
+
--weight <w> font: Bold or Regular (default)
|
|
502
|
+
--glyph-px <n> font: native glyph size, 8, 16 (default), 32, or 64
|
|
485
503
|
--prune cache: remove invalid/unreferenced local cache data
|
|
486
504
|
--manifest <path> Default: pixelkiln.manifest.json
|
|
487
505
|
--lock <path> Default: pixelkiln.lock.json beside the manifest
|
|
@@ -537,6 +555,8 @@ Examples
|
|
|
537
555
|
pixelkiln recipe verify pixelkiln-recipes/comfyui/pixel-art-xl-environment/1.0.0 --model-root /path/to/ComfyUI/models
|
|
538
556
|
pixelkiln quality snapshot --inputs quality-inputs.json --out pixelkiln.quality.json
|
|
539
557
|
pixelkiln quality check --from pixelkiln.quality.json
|
|
558
|
+
pixelkiln unzoom --from refs/knight-512.png --out refs/knight.png
|
|
559
|
+
pixelkiln font --description "warm orange arcade font" --weight Bold --out fonts/arcade
|
|
540
560
|
pixelkiln workspace add ../other-game/pixelkiln.manifest.json
|
|
541
561
|
pixelkiln workspace status --json
|
|
542
562
|
pixelkiln salvage --workspace pixelkiln.workspace.json
|
|
@@ -618,10 +638,10 @@ import { createReadStream } from "fs";
|
|
|
618
638
|
function sha256(data) {
|
|
619
639
|
return createHash("sha256").update(data).digest("hex");
|
|
620
640
|
}
|
|
621
|
-
async function sha256File(
|
|
641
|
+
async function sha256File(path47) {
|
|
622
642
|
return new Promise((resolve, reject) => {
|
|
623
643
|
const hash = createHash("sha256");
|
|
624
|
-
const stream = createReadStream(
|
|
644
|
+
const stream = createReadStream(path47);
|
|
625
645
|
stream.on("data", (chunk2) => hash.update(chunk2));
|
|
626
646
|
stream.on("error", reject);
|
|
627
647
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -649,7 +669,7 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
|
|
|
649
669
|
// tile fields are undefined for every other generator. `tileSize` is
|
|
650
670
|
// intentionally absent; width/height are derived from it, so it is
|
|
651
671
|
// already covered.
|
|
652
|
-
noBackground: spec.generator === "pixflux" || spec.generator === "uiAsset" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
672
|
+
noBackground: spec.generator === "pixflux" || spec.generator === "uiAsset" || spec.generator === "uiElement" || spec.provider !== "pixellab" ? spec.noBackground : void 0,
|
|
653
673
|
tileType: spec.tileType,
|
|
654
674
|
tileView: spec.tileView,
|
|
655
675
|
tileFeature: spec.tileFeature,
|
|
@@ -744,7 +764,7 @@ import path2 from "path";
|
|
|
744
764
|
// src/types.ts
|
|
745
765
|
import { z } from "zod";
|
|
746
766
|
var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
|
|
747
|
-
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset"]);
|
|
767
|
+
var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro", "uiAsset", "uiElement"]);
|
|
748
768
|
var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
|
|
749
769
|
var RevisionModeSchema = z.enum([
|
|
750
770
|
"image-to-image",
|
|
@@ -753,7 +773,9 @@ var RevisionModeSchema = z.enum([
|
|
|
753
773
|
"reduce-colors",
|
|
754
774
|
"correct-pixelart",
|
|
755
775
|
"animate",
|
|
756
|
-
"animate-pixminimax"
|
|
776
|
+
"animate-pixminimax",
|
|
777
|
+
"interpolate",
|
|
778
|
+
"edit-animation"
|
|
757
779
|
]);
|
|
758
780
|
function tileVariationCount(descriptions) {
|
|
759
781
|
return Math.max(1, descriptions) * 4;
|
|
@@ -934,6 +956,14 @@ var CharacterAnimationSchema = z.object({
|
|
|
934
956
|
template: z.string().min(1).optional(),
|
|
935
957
|
/** Direction to animate. */
|
|
936
958
|
direction: CharacterDirectionSchema.default("south"),
|
|
959
|
+
/**
|
|
960
|
+
* Shorthand for one loop in several directions, instead of `direction`.
|
|
961
|
+
* Expanded when the manifest loads into one loop per named direction
|
|
962
|
+
* (`<id>.<direction>`) plus a mirror for each direction whose flip is
|
|
963
|
+
* not named, exactly as if each had been written out. See
|
|
964
|
+
* `src/loop-directions.ts`.
|
|
965
|
+
*/
|
|
966
|
+
directions: z.array(CharacterDirectionSchema).min(1).optional(),
|
|
937
967
|
/** Frames to generate without a template (v3). Even, 4 to 16. */
|
|
938
968
|
frames: z.number().int().min(4).max(16).optional(),
|
|
939
969
|
/** Playback rate recorded with the frames; PixelLab does not store one. */
|
|
@@ -962,6 +992,13 @@ var CharacterAnimationSchema = z.object({
|
|
|
962
992
|
/** v3 text loops only: let PixelLab expand the action into a fuller motion description first. */
|
|
963
993
|
enhancePrompt: z.boolean().optional()
|
|
964
994
|
}).strict().superRefine((animation, context) => {
|
|
995
|
+
if (animation.directions !== void 0) {
|
|
996
|
+
context.addIssue({
|
|
997
|
+
code: z.ZodIssueCode.custom,
|
|
998
|
+
message: "directions is expanded when the manifest loads; validate the loaded manifest, not the raw file",
|
|
999
|
+
path: ["directions"]
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
965
1002
|
if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
|
|
966
1003
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
967
1004
|
}
|
|
@@ -1022,9 +1059,13 @@ var RevisionSchema = z.object({
|
|
|
1022
1059
|
* the provider layer, since it differs by mode).
|
|
1023
1060
|
*/
|
|
1024
1061
|
frames: z.number().int().min(4).max(40).optional(),
|
|
1025
|
-
/** `animate`/`animate-pixminimax` only: playback rate recorded with the frames; PixelLab does not store one. */
|
|
1062
|
+
/** `animate`/`animate-pixminimax`/`interpolate`/`edit-animation` only: playback rate recorded with the frames; PixelLab does not store one. */
|
|
1026
1063
|
fps: z.number().int().min(1).max(60).optional(),
|
|
1027
|
-
/**
|
|
1064
|
+
/**
|
|
1065
|
+
* `animate`/`animate-pixminimax`: manifest-relative image pinning where
|
|
1066
|
+
* the motion ends (interpolation instead of open-ended animation).
|
|
1067
|
+
* `interpolate`: required, the ending keyframe; the parent is the start.
|
|
1068
|
+
*/
|
|
1028
1069
|
lastFrame: z.string().min(1).optional(),
|
|
1029
1070
|
/** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
|
|
1030
1071
|
direction: CharacterDirectionSchema.optional(),
|
|
@@ -1068,14 +1109,15 @@ var RevisionSchema = z.object({
|
|
|
1068
1109
|
path: ["strength"]
|
|
1069
1110
|
});
|
|
1070
1111
|
}
|
|
1071
|
-
|
|
1112
|
+
const generative = revision.mode === "animate" || revision.mode === "animate-pixminimax" || revision.mode === "interpolate" || revision.mode === "edit-animation";
|
|
1113
|
+
if (revision.strength !== void 0 && generative) {
|
|
1072
1114
|
context.addIssue({
|
|
1073
1115
|
code: z.ZodIssueCode.custom,
|
|
1074
|
-
message: `${revision.mode} revisions do not take a strength; use enhancePrompt`,
|
|
1116
|
+
message: revision.mode === "animate" || revision.mode === "animate-pixminimax" ? `${revision.mode} revisions do not take a strength; use enhancePrompt` : `${revision.mode} revisions do not take a strength`,
|
|
1075
1117
|
path: ["strength"]
|
|
1076
1118
|
});
|
|
1077
1119
|
}
|
|
1078
|
-
for (const field of ["frames", "
|
|
1120
|
+
for (const field of ["frames", "enhancePrompt"]) {
|
|
1079
1121
|
if (revision[field] !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
|
|
1080
1122
|
context.addIssue({
|
|
1081
1123
|
code: z.ZodIssueCode.custom,
|
|
@@ -1084,6 +1126,27 @@ var RevisionSchema = z.object({
|
|
|
1084
1126
|
});
|
|
1085
1127
|
}
|
|
1086
1128
|
}
|
|
1129
|
+
if (revision.lastFrame !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax" && revision.mode !== "interpolate") {
|
|
1130
|
+
context.addIssue({
|
|
1131
|
+
code: z.ZodIssueCode.custom,
|
|
1132
|
+
message: "lastFrame applies to animate/animate-pixminimax/interpolate revisions only",
|
|
1133
|
+
path: ["lastFrame"]
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
if (revision.mode === "interpolate" && revision.lastFrame === void 0) {
|
|
1137
|
+
context.addIssue({
|
|
1138
|
+
code: z.ZodIssueCode.custom,
|
|
1139
|
+
message: "interpolate revisions require lastFrame: the ending keyframe to interpolate toward",
|
|
1140
|
+
path: ["lastFrame"]
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
if (revision.fps !== void 0 && !generative) {
|
|
1144
|
+
context.addIssue({
|
|
1145
|
+
code: z.ZodIssueCode.custom,
|
|
1146
|
+
message: "fps applies to animate/animate-pixminimax/interpolate/edit-animation revisions only",
|
|
1147
|
+
path: ["fps"]
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1087
1150
|
if (revision.direction !== void 0 && revision.mode !== "animate-pixminimax") {
|
|
1088
1151
|
context.addIssue({
|
|
1089
1152
|
code: z.ZodIssueCode.custom,
|
|
@@ -1095,6 +1158,8 @@ var RevisionSchema = z.object({
|
|
|
1095
1158
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
1096
1159
|
}
|
|
1097
1160
|
});
|
|
1161
|
+
var FRAME_SET_REVISION_MODES = ["animate", "animate-pixminimax", "interpolate", "edit-animation"];
|
|
1162
|
+
var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-animation"];
|
|
1098
1163
|
var StyleObjectSchema = z.object({
|
|
1099
1164
|
/** Generation backend for this style. Omit to inherit the manifest default. */
|
|
1100
1165
|
provider: z.string().min(1).optional(),
|
|
@@ -1213,7 +1278,7 @@ var StyleObjectSchema = z.object({
|
|
|
1213
1278
|
*/
|
|
1214
1279
|
terrainMode: z.enum(["standard", "pro"]).optional(),
|
|
1215
1280
|
/**
|
|
1216
|
-
* `uiAsset`
|
|
1281
|
+
* `uiAsset` and `uiElement` generators only. A natural-language palette hint sent as-is
|
|
1217
1282
|
* (e.g. "brown and gold"), distinct from `palette`'s hex-color array —
|
|
1218
1283
|
* this is a prompt-level steer, not a local quantization target.
|
|
1219
1284
|
*/
|
|
@@ -4087,11 +4152,16 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
4087
4152
|
reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
|
|
4088
4153
|
};
|
|
4089
4154
|
}
|
|
4090
|
-
if (
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4155
|
+
if (revision.sourceMembers) {
|
|
4156
|
+
const members = await inspectSourceMembers(spec, lock);
|
|
4157
|
+
if (!members.ready) return members;
|
|
4158
|
+
} else {
|
|
4159
|
+
if (!revision.sourceSha256 || !existsSync4(revision.sourceFile)) {
|
|
4160
|
+
return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
|
|
4161
|
+
}
|
|
4162
|
+
if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
|
|
4163
|
+
return { ready: false, reason: "revision source changed after the manifest was resolved" };
|
|
4164
|
+
}
|
|
4095
4165
|
}
|
|
4096
4166
|
if (revision.maskFile) {
|
|
4097
4167
|
if (!revision.maskSha256 || !existsSync4(revision.maskFile)) {
|
|
@@ -4101,8 +4171,39 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
4101
4171
|
return { ready: false, reason: "revision mask changed after the manifest was resolved" };
|
|
4102
4172
|
}
|
|
4103
4173
|
}
|
|
4174
|
+
if (revision.lastFrameFile) {
|
|
4175
|
+
if (!revision.lastFrameSha256 || !existsSync4(revision.lastFrameFile)) {
|
|
4176
|
+
return { ready: false, reason: `revision last frame is missing: ${revision.lastFrameFile}` };
|
|
4177
|
+
}
|
|
4178
|
+
if (await sha256File(revision.lastFrameFile) !== revision.lastFrameSha256) {
|
|
4179
|
+
return { ready: false, reason: "revision last frame changed after the manifest was resolved" };
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4104
4182
|
return { ready: true, reason: "revision inputs are current" };
|
|
4105
4183
|
}
|
|
4184
|
+
async function inspectSourceMembers(spec, lock) {
|
|
4185
|
+
const revision = spec.revision;
|
|
4186
|
+
const members = revision.sourceMembers;
|
|
4187
|
+
for (const member of members) {
|
|
4188
|
+
if (!existsSync4(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
|
|
4189
|
+
if (await sha256File(member.file) !== member.sha256) {
|
|
4190
|
+
return { ready: false, reason: `revision source member changed after the manifest was resolved: ${member.file}` };
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
const parent = revision.sourceSpec;
|
|
4194
|
+
if (parent.source) return { ready: true, reason: "declared source set is present" };
|
|
4195
|
+
const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
|
|
4196
|
+
if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
|
|
4197
|
+
const recorded = entry.outputs.map((_, index) => path8.resolve(currentEntryOutputPath(entry, parent, index))).sort();
|
|
4198
|
+
const found = members.map((member) => path8.resolve(member.file)).sort();
|
|
4199
|
+
if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
|
|
4200
|
+
return {
|
|
4201
|
+
ready: false,
|
|
4202
|
+
reason: `revision source ${parent.assetId} records ${recorded.length} output(s) but ${found.length} member file(s) were found beside it; remove files left over from an earlier generation, or re-fetch the parent`
|
|
4203
|
+
};
|
|
4204
|
+
}
|
|
4205
|
+
return { ready: true, reason: "revision source set is current" };
|
|
4206
|
+
}
|
|
4106
4207
|
async function requireRevisionReady(spec, lock) {
|
|
4107
4208
|
const inspection = await inspectRevisionReadiness(spec, lock);
|
|
4108
4209
|
if (inspection && !inspection.ready) throw new Error(inspection.reason);
|
|
@@ -4808,14 +4909,15 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4808
4909
|
}
|
|
4809
4910
|
/**
|
|
4810
4911
|
* Everything else resolves generically to whatever the user's own
|
|
4811
|
-
* workflow does with `bindings.sourceImage`.
|
|
4812
|
-
*
|
|
4813
|
-
*
|
|
4814
|
-
*
|
|
4815
|
-
*
|
|
4912
|
+
* workflow does with `bindings.sourceImage`. The frame-set modes
|
|
4913
|
+
* (`animate`, `animate-pixminimax`, `interpolate`, `edit-animation`) are
|
|
4914
|
+
* the exception: they produce an ordered frame set, and this adapter's
|
|
4915
|
+
* revision path always writes a single output image (see `submit`/`fetch`
|
|
4916
|
+
* below) — a real structural gap, not a missing binding, so they are
|
|
4917
|
+
* rejected here rather than only failing once `validate` runs.
|
|
4816
4918
|
*/
|
|
4817
4919
|
supportsRevision(mode2) {
|
|
4818
|
-
return mode2
|
|
4920
|
+
return !FRAME_SET_REVISION_MODES.includes(mode2);
|
|
4819
4921
|
}
|
|
4820
4922
|
estimate(spec) {
|
|
4821
4923
|
return {
|
|
@@ -4868,6 +4970,11 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4868
4970
|
throw new Error("ComfyUI providerOptions.frames requires generator: frames");
|
|
4869
4971
|
}
|
|
4870
4972
|
if (spec.revision) {
|
|
4973
|
+
if (spec.revision.sourceMembers) {
|
|
4974
|
+
throw new Error(
|
|
4975
|
+
`ComfyUI revisions read one source image; ${spec.revision.sourceAssetId} is a ${spec.revision.sourceMembers.length}-member set (directions or frames)`
|
|
4976
|
+
);
|
|
4977
|
+
}
|
|
4871
4978
|
if (!options.bindings.sourceImage) {
|
|
4872
4979
|
throw new Error("ComfyUI revisions require bindings.sourceImage");
|
|
4873
4980
|
}
|
|
@@ -5603,6 +5710,21 @@ var CorrectPixelartResponseSchema = z2.object({
|
|
|
5603
5710
|
images: z2.array(z2.object({ base64: z2.string().min(1) }).passthrough()).min(1),
|
|
5604
5711
|
usage: z2.unknown().optional()
|
|
5605
5712
|
}).passthrough();
|
|
5713
|
+
var UnzoomResponseSchema = z2.object({
|
|
5714
|
+
image: z2.object({ base64: z2.string().min(1) }).passthrough(),
|
|
5715
|
+
original_size: z2.object({ width: z2.number().int(), height: z2.number().int() }).passthrough(),
|
|
5716
|
+
unzoomed_size: z2.object({ width: z2.number().int(), height: z2.number().int() }).passthrough(),
|
|
5717
|
+
zoom_factor_detected: z2.number(),
|
|
5718
|
+
usage: z2.unknown().optional()
|
|
5719
|
+
}).passthrough();
|
|
5720
|
+
var FontJobSchema = z2.object({
|
|
5721
|
+
job_id: z2.string().min(1),
|
|
5722
|
+
status: z2.string(),
|
|
5723
|
+
glyph_px: z2.number().int().nullable().optional(),
|
|
5724
|
+
download_atlas_url: z2.string().nullable().optional(),
|
|
5725
|
+
download_ttf_url: z2.string().nullable().optional(),
|
|
5726
|
+
usage: z2.unknown().optional()
|
|
5727
|
+
}).passthrough();
|
|
5606
5728
|
var SelectFramesSchema = z2.object({ created_object_ids: z2.array(z2.string()) }).passthrough();
|
|
5607
5729
|
var CharacterSubmitSchema = z2.object({
|
|
5608
5730
|
background_job_id: z2.string().min(1),
|
|
@@ -5687,6 +5809,17 @@ function validateResponse(schema, raw, operation) {
|
|
|
5687
5809
|
const issues = parsed.error.issues.slice(0, 4).map((i) => `${i.path.join(".") || "response"}: ${i.message}`).join("; ");
|
|
5688
5810
|
throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
|
|
5689
5811
|
}
|
|
5812
|
+
function cleanupImages(args, operation) {
|
|
5813
|
+
const images = args.images ?? (args.image ? [args.image] : []);
|
|
5814
|
+
if (!images.length) throw new Error(`${operation} needs at least one image`);
|
|
5815
|
+
return images;
|
|
5816
|
+
}
|
|
5817
|
+
function cleanupResults(images, sent, operation) {
|
|
5818
|
+
if (images.length !== sent) {
|
|
5819
|
+
throw new Error(`Invalid PixelLab response for ${operation}: sent ${sent} image(s), got ${images.length} back`);
|
|
5820
|
+
}
|
|
5821
|
+
return images.map((image) => Buffer.from(image.base64, "base64"));
|
|
5822
|
+
}
|
|
5690
5823
|
var PixelLabError = class extends ProviderError {
|
|
5691
5824
|
constructor(message6, status, body) {
|
|
5692
5825
|
super("pixellab", message6, { status });
|
|
@@ -5715,9 +5848,9 @@ var PixelLabClient = class {
|
|
|
5715
5848
|
apiKey;
|
|
5716
5849
|
http;
|
|
5717
5850
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
5718
|
-
async request(
|
|
5851
|
+
async request(path47, init) {
|
|
5719
5852
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
5720
|
-
const res = await this.http(`${BASE}${
|
|
5853
|
+
const res = await this.http(`${BASE}${path47}`, {
|
|
5721
5854
|
...init,
|
|
5722
5855
|
headers: {
|
|
5723
5856
|
Authorization: auth,
|
|
@@ -5727,13 +5860,13 @@ var PixelLabClient = class {
|
|
|
5727
5860
|
});
|
|
5728
5861
|
const text = await res.text();
|
|
5729
5862
|
if (!res.ok) {
|
|
5730
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
5863
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path47} \u2192 ${res.status}`, res.status, text);
|
|
5731
5864
|
}
|
|
5732
5865
|
if (!text) return {};
|
|
5733
5866
|
try {
|
|
5734
5867
|
return JSON.parse(text);
|
|
5735
5868
|
} catch {
|
|
5736
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
5869
|
+
throw new Error(`${init?.method ?? "GET"} ${path47} returned invalid JSON`);
|
|
5737
5870
|
}
|
|
5738
5871
|
}
|
|
5739
5872
|
async balance() {
|
|
@@ -6119,10 +6252,10 @@ var PixelLabClient = class {
|
|
|
6119
6252
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
6120
6253
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
6121
6254
|
const south = args.reference?.south;
|
|
6122
|
-
let
|
|
6255
|
+
let path47;
|
|
6123
6256
|
let body;
|
|
6124
6257
|
if (args.mode === "v3") {
|
|
6125
|
-
|
|
6258
|
+
path47 = "/create-character-v3";
|
|
6126
6259
|
body = {
|
|
6127
6260
|
description: args.description,
|
|
6128
6261
|
image_size: imageSize,
|
|
@@ -6136,7 +6269,7 @@ var PixelLabClient = class {
|
|
|
6136
6269
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
6137
6270
|
};
|
|
6138
6271
|
} else if (args.mode === "pro-flash") {
|
|
6139
|
-
|
|
6272
|
+
path47 = "/create-character-pro-flash";
|
|
6140
6273
|
const traits = args.styleTraits;
|
|
6141
6274
|
body = {
|
|
6142
6275
|
description: args.description,
|
|
@@ -6159,7 +6292,7 @@ var PixelLabClient = class {
|
|
|
6159
6292
|
} : {}
|
|
6160
6293
|
};
|
|
6161
6294
|
} else if (args.mode === "pro") {
|
|
6162
|
-
|
|
6295
|
+
path47 = "/create-character-pro";
|
|
6163
6296
|
body = {
|
|
6164
6297
|
description: args.description,
|
|
6165
6298
|
image_size: imageSize,
|
|
@@ -6173,7 +6306,7 @@ var PixelLabClient = class {
|
|
|
6173
6306
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
6174
6307
|
};
|
|
6175
6308
|
} else {
|
|
6176
|
-
|
|
6309
|
+
path47 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
6177
6310
|
body = {
|
|
6178
6311
|
description: args.description,
|
|
6179
6312
|
image_size: imageSize,
|
|
@@ -6190,8 +6323,8 @@ var PixelLabClient = class {
|
|
|
6190
6323
|
...palette
|
|
6191
6324
|
};
|
|
6192
6325
|
}
|
|
6193
|
-
const raw = await this.request(
|
|
6194
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
6326
|
+
const raw = await this.request(path47, { method: "POST", body: JSON.stringify(body) });
|
|
6327
|
+
return validateResponse(CharacterSubmitSchema, raw, path47.slice(1));
|
|
6195
6328
|
}
|
|
6196
6329
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
6197
6330
|
async createCharacterState(args) {
|
|
@@ -6353,20 +6486,52 @@ var PixelLabClient = class {
|
|
|
6353
6486
|
);
|
|
6354
6487
|
}
|
|
6355
6488
|
/**
|
|
6356
|
-
* `/
|
|
6357
|
-
*
|
|
6358
|
-
*
|
|
6359
|
-
*
|
|
6360
|
-
*
|
|
6361
|
-
*
|
|
6362
|
-
*
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6489
|
+
* `/generate-ui-v2` ("Generate UI (Pro)"): one UI element (a button, a
|
|
6490
|
+
* health bar, an inventory slot, a dialogue box) from a description,
|
|
6491
|
+
* guided by an optional concept image and a natural-language color
|
|
6492
|
+
* palette. A plain background job; unlike `/create-ui-asset` there is no
|
|
6493
|
+
* piece/element layout, and sizes start at 16px instead of 192. Its
|
|
6494
|
+
* completed shape has not been exercised live; `pollUiElement` reads it
|
|
6495
|
+
* defensively.
|
|
6496
|
+
*/
|
|
6497
|
+
async generateUiV2(args) {
|
|
6498
|
+
const body = {
|
|
6499
|
+
description: args.description,
|
|
6500
|
+
image_size: { width: args.width, height: args.height }
|
|
6501
|
+
};
|
|
6502
|
+
if (args.conceptImage) {
|
|
6503
|
+
body.concept_image = {
|
|
6504
|
+
image: args.conceptImage.image,
|
|
6505
|
+
size: { width: args.conceptImage.width, height: args.conceptImage.height }
|
|
6506
|
+
};
|
|
6507
|
+
}
|
|
6508
|
+
if (args.colorPalette) body.color_palette = args.colorPalette;
|
|
6509
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
6510
|
+
if (args.seed != null) body.seed = args.seed;
|
|
6511
|
+
return validateResponse(
|
|
6512
|
+
RevisionJobSubmitSchema,
|
|
6513
|
+
await this.request("/generate-ui-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
6514
|
+
"generate-ui-v2"
|
|
6515
|
+
);
|
|
6516
|
+
}
|
|
6517
|
+
/**
|
|
6518
|
+
* `/reduce-colors`, PixelLab's "Cleanup" tier: quantize one image, or
|
|
6519
|
+
* several same-size frames together onto ONE shared palette, synchronously
|
|
6520
|
+
* — no `background_job_id`, the result comes back in this same response,
|
|
6521
|
+
* like `createImagePixflux`. Passing every frame of an animation (or every
|
|
6522
|
+
* direction of a character) in one call is the endpoint's whole point:
|
|
6523
|
+
* frames quantized separately drift onto different palettes. The schema's
|
|
6524
|
+
* own response example is dollar-denominated (`usage: {type: "usd", usd:
|
|
6525
|
+
* 0.02}`), which — matching this codebase's repeated experience with
|
|
6526
|
+
* PixelLab's documented-vs-billed cost mismatches (`isometricTile`,
|
|
6527
|
+
* `objectPro`) — did not hold: confirmed live against a Tier 2 account at
|
|
6528
|
+
* exactly 0.1 generations for a 32x32 source (docs/REVISIONS.md).
|
|
6529
|
+
* `numColors` and `paletteImage` are mutually exclusive upstream; the
|
|
6530
|
+
* manifest schema already enforces that before this is ever called.
|
|
6367
6531
|
*/
|
|
6368
6532
|
async reduceColors(args) {
|
|
6369
|
-
const
|
|
6533
|
+
const images = cleanupImages(args, "reduce-colors");
|
|
6534
|
+
const body = { images };
|
|
6370
6535
|
if (args.numColors != null) body.num_colors = args.numColors;
|
|
6371
6536
|
if (args.paletteImage) body.palette_image = args.paletteImage;
|
|
6372
6537
|
if (args.dithering) body.dithering = args.dithering;
|
|
@@ -6376,8 +6541,10 @@ var PixelLabClient = class {
|
|
|
6376
6541
|
await this.request("/reduce-colors", { method: "POST", body: JSON.stringify(body) }),
|
|
6377
6542
|
"reduce-colors"
|
|
6378
6543
|
);
|
|
6544
|
+
const pngs = cleanupResults(res.images, images.length, "reduce-colors");
|
|
6379
6545
|
return {
|
|
6380
|
-
png:
|
|
6546
|
+
png: pngs[0],
|
|
6547
|
+
pngs,
|
|
6381
6548
|
paletteStripPng: Buffer.from(res.palette.base64, "base64"),
|
|
6382
6549
|
nColors: res.n_colors,
|
|
6383
6550
|
usage: res.usage
|
|
@@ -6386,19 +6553,117 @@ var PixelLabClient = class {
|
|
|
6386
6553
|
/**
|
|
6387
6554
|
* `/correct-pixelart`, PixelLab's "Cleanup" tier: sharpen edges and drop
|
|
6388
6555
|
* stray pixels without resizing, synchronously — same shape as
|
|
6389
|
-
* `reduceColors` above, no background job
|
|
6390
|
-
*
|
|
6556
|
+
* `reduceColors` above, no background job, and likewise corrects several
|
|
6557
|
+
* same-size frames together when given `images`. Cost is confirmed live
|
|
6558
|
+
* at 0.1 generations for a 32x32 source, not the schema's own
|
|
6391
6559
|
* dollar-denominated example (`usage: {type: "usd", usd: 0.02}`).
|
|
6392
6560
|
*/
|
|
6393
6561
|
async correctPixelart(args) {
|
|
6394
|
-
const
|
|
6562
|
+
const images = cleanupImages(args, "correct-pixelart");
|
|
6563
|
+
const body = { images };
|
|
6395
6564
|
if (args.strength != null) body.strength = args.strength;
|
|
6396
6565
|
const res = validateResponse(
|
|
6397
6566
|
CorrectPixelartResponseSchema,
|
|
6398
6567
|
await this.request("/correct-pixelart", { method: "POST", body: JSON.stringify(body) }),
|
|
6399
6568
|
"correct-pixelart"
|
|
6400
6569
|
);
|
|
6401
|
-
|
|
6570
|
+
const pngs = cleanupResults(res.images, images.length, "correct-pixelart");
|
|
6571
|
+
return { png: pngs[0], pngs, usage: res.usage };
|
|
6572
|
+
}
|
|
6573
|
+
/**
|
|
6574
|
+
* `/unzoom`: recover native-resolution pixel art from an upscaled image (a
|
|
6575
|
+
* 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
|
|
6576
|
+
* PixelLab's own API overview names this "the most common cause of
|
|
6577
|
+
* disappointing output": run it on outside artwork before it becomes a
|
|
6578
|
+
* style or reference image. The input must be at least 256x256 and at
|
|
6579
|
+
* most 2048x2048 in area, and the result is opaque — transparency is
|
|
6580
|
+
* composited onto white before the grid is detected. `quantize` is 0 to
|
|
6581
|
+
* auto-detect a palette, -1 to keep every color, or 2-256 for exactly
|
|
6582
|
+
* that many.
|
|
6583
|
+
*/
|
|
6584
|
+
async unzoom(args) {
|
|
6585
|
+
const body = { image: args.image };
|
|
6586
|
+
if (args.quantize != null) body.quantize = args.quantize;
|
|
6587
|
+
const res = validateResponse(
|
|
6588
|
+
UnzoomResponseSchema,
|
|
6589
|
+
await this.request("/unzoom", { method: "POST", body: JSON.stringify(body) }),
|
|
6590
|
+
"unzoom"
|
|
6591
|
+
);
|
|
6592
|
+
return {
|
|
6593
|
+
png: Buffer.from(res.image.base64, "base64"),
|
|
6594
|
+
originalSize: { width: res.original_size.width, height: res.original_size.height },
|
|
6595
|
+
unzoomedSize: { width: res.unzoomed_size.width, height: res.unzoomed_size.height },
|
|
6596
|
+
zoomFactor: res.zoom_factor_detected,
|
|
6597
|
+
usage: res.usage
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6600
|
+
/**
|
|
6601
|
+
* `/interpolation-v2` ("Interpolate (Pro)"): generate the in-between
|
|
6602
|
+
* frames from one keyframe to another, guided by a short `action`. Unlike
|
|
6603
|
+
* `last_frame` on `/animate-with-text-v3`, both ends are required and the
|
|
6604
|
+
* frame count is the model's own choice (its docs say "typically 4-8").
|
|
6605
|
+
* Output frames are 16 to 128 pixels per side. A plain background job;
|
|
6606
|
+
* its completed shape is read defensively by `pollAnimateRevision`.
|
|
6607
|
+
*/
|
|
6608
|
+
async interpolationV2(args) {
|
|
6609
|
+
const size = { width: args.width, height: args.height };
|
|
6610
|
+
const body = {
|
|
6611
|
+
start_image: { image: args.startImage, size },
|
|
6612
|
+
end_image: { image: args.endImage, size },
|
|
6613
|
+
action: args.action,
|
|
6614
|
+
image_size: size
|
|
6615
|
+
};
|
|
6616
|
+
if (args.seed != null) body.seed = args.seed;
|
|
6617
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
6618
|
+
return validateResponse(
|
|
6619
|
+
RevisionJobSubmitSchema,
|
|
6620
|
+
await this.request("/interpolation-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
6621
|
+
"interpolation-v2"
|
|
6622
|
+
);
|
|
6623
|
+
}
|
|
6624
|
+
/**
|
|
6625
|
+
* `/edit-animation-v2` ("Edit animation (Pro)"): apply one text edit
|
|
6626
|
+
* across every frame of an existing animation at once, so the change
|
|
6627
|
+
* stays consistent frame to frame. 2 to 16 frames, but the real ceiling
|
|
6628
|
+
* shrinks with frame size because every frame is packed into one grid:
|
|
6629
|
+
* 16 at up to 64px, 9 at 65-80px, 4 at 81-256px.
|
|
6630
|
+
*/
|
|
6631
|
+
async editAnimationV2(args) {
|
|
6632
|
+
const size = { width: args.width, height: args.height };
|
|
6633
|
+
const body = {
|
|
6634
|
+
description: args.description,
|
|
6635
|
+
frames: args.frames.map((image) => ({ image, size })),
|
|
6636
|
+
image_size: size
|
|
6637
|
+
};
|
|
6638
|
+
if (args.seed != null) body.seed = args.seed;
|
|
6639
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
6640
|
+
return validateResponse(
|
|
6641
|
+
RevisionJobSubmitSchema,
|
|
6642
|
+
await this.request("/edit-animation-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
6643
|
+
"edit-animation-v2"
|
|
6644
|
+
);
|
|
6645
|
+
}
|
|
6646
|
+
/**
|
|
6647
|
+
* `/generate-font-pro`: an 80-glyph pixel font (A-Z, a-z, 0-9, common
|
|
6648
|
+
* game-UI punctuation) from a style description, delivered as a glyph
|
|
6649
|
+
* atlas PNG plus a ready-to-use `.ttf`. Documented at 25 subscription
|
|
6650
|
+
* generations. Poll with `getFontJob`.
|
|
6651
|
+
*/
|
|
6652
|
+
async generateFontPro(args) {
|
|
6653
|
+
const body = { description: args.description, weight: args.weight };
|
|
6654
|
+
if (args.glyphPx != null) body.glyph_px = args.glyphPx;
|
|
6655
|
+
if (args.seed != null) body.seed = args.seed;
|
|
6656
|
+
if (args.fontName) body.font_name = args.fontName;
|
|
6657
|
+
return validateResponse(
|
|
6658
|
+
RevisionJobSubmitSchema,
|
|
6659
|
+
await this.request("/generate-font-pro", { method: "POST", body: JSON.stringify(body) }),
|
|
6660
|
+
"generate-font-pro"
|
|
6661
|
+
);
|
|
6662
|
+
}
|
|
6663
|
+
/** `GET /generate-font-pro/{job_id}`: status, then no-auth atlas and `.ttf` URLs once completed. */
|
|
6664
|
+
async getFontJob(jobId) {
|
|
6665
|
+
const raw = await this.request(`/generate-font-pro/${encodeURIComponent(jobId)}`);
|
|
6666
|
+
return validateResponse(FontJobSchema, raw, "generate-font-pro/{job_id}");
|
|
6402
6667
|
}
|
|
6403
6668
|
/**
|
|
6404
6669
|
* `/animate-with-text-v3`: animate a loose image from a text description,
|
|
@@ -6619,7 +6884,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6619
6884
|
return new _PixelLabProvider(new PixelLabClient("download-only"));
|
|
6620
6885
|
}
|
|
6621
6886
|
supports(generator) {
|
|
6622
|
-
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset";
|
|
6887
|
+
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement";
|
|
6623
6888
|
}
|
|
6624
6889
|
/**
|
|
6625
6890
|
* `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
|
|
@@ -6629,12 +6894,15 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6629
6894
|
* rather than a described edit — and `animate`/`animate-pixminimax`
|
|
6630
6895
|
* (`/animate-with-text-v3`, `/animate-pixminimax`), which animate any loose
|
|
6631
6896
|
* image from a text description with no character/object resource
|
|
6632
|
-
* required
|
|
6633
|
-
*
|
|
6634
|
-
*
|
|
6897
|
+
* required — and `interpolate` (`/interpolation-v2`, the in-betweens
|
|
6898
|
+
* from one keyframe to another) and `edit-animation`
|
|
6899
|
+
* (`/edit-animation-v2`, one text edit applied across a whole frame set).
|
|
6900
|
+
* `outpaint` does not: there is no canvas-expansion endpoint in the API,
|
|
6901
|
+
* matching docs/REVISIONS.md's note that no provider ships a tested
|
|
6902
|
+
* outpaint path yet.
|
|
6635
6903
|
*/
|
|
6636
6904
|
supportsRevision(mode2) {
|
|
6637
|
-
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax";
|
|
6905
|
+
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "interpolate" || mode2 === "edit-animation";
|
|
6638
6906
|
}
|
|
6639
6907
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
6640
6908
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
@@ -6662,6 +6930,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6662
6930
|
if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
|
|
6663
6931
|
return { unit: "generations", amount: 0.1, candidates: 1 };
|
|
6664
6932
|
}
|
|
6933
|
+
if (spec.revision?.mode === "interpolate" || spec.revision?.mode === "edit-animation") {
|
|
6934
|
+
const width = spec.revision.sourceWidth ?? spec.width;
|
|
6935
|
+
const height = spec.revision.sourceHeight ?? spec.height;
|
|
6936
|
+
const side = spec.revision.mode === "edit-animation" ? Math.ceil(Math.sqrt(Math.max(1, spec.revision.sourceMembers?.length ?? 1))) : 1;
|
|
6937
|
+
return { unit: "generations", amount: generationCost(width * side, height * side, "1dir"), candidates: 1 };
|
|
6938
|
+
}
|
|
6665
6939
|
if (spec.revision?.mode === "animate" || spec.revision?.mode === "animate-pixminimax") {
|
|
6666
6940
|
const width = spec.revision.sourceWidth ?? spec.width;
|
|
6667
6941
|
const height = spec.revision.sourceHeight ?? spec.height;
|
|
@@ -6690,6 +6964,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6690
6964
|
};
|
|
6691
6965
|
}
|
|
6692
6966
|
validate(spec, styleImages) {
|
|
6967
|
+
const members = spec.revision?.sourceMembers;
|
|
6968
|
+
if (spec.revision && members && !MEMBER_SET_REVISION_MODES.includes(spec.revision.mode)) {
|
|
6969
|
+
throw new Error(
|
|
6970
|
+
`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, or edit-animation, which take the whole set`
|
|
6971
|
+
);
|
|
6972
|
+
}
|
|
6693
6973
|
if (spec.revision?.mode === "inpaint") {
|
|
6694
6974
|
const { sourceWidth: width, sourceHeight: height } = spec.revision;
|
|
6695
6975
|
if (width != null && height != null && (width < 32 || height < 32 || width > 512 || height > 512)) {
|
|
@@ -6698,9 +6978,10 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6698
6978
|
}
|
|
6699
6979
|
if (spec.revision?.mode === "reduce-colors") {
|
|
6700
6980
|
const { sourceWidth: width, sourceHeight: height } = spec.revision;
|
|
6701
|
-
|
|
6981
|
+
const count = members?.length ?? 1;
|
|
6982
|
+
if (width != null && height != null && width * height * count > 512 * 512) {
|
|
6702
6983
|
throw new Error(
|
|
6703
|
-
`PixelLab reduce-colors source is ${width}x${height} (${width * height}px\xB2); the API takes at most 512x512 worth of pixels per call`
|
|
6984
|
+
`PixelLab reduce-colors source is ${count > 1 ? `${count} frames of ` : ""}${width}x${height} (${width * height * count}px\xB2 in all); the API takes at most 512x512 worth of pixels per call`
|
|
6704
6985
|
);
|
|
6705
6986
|
}
|
|
6706
6987
|
}
|
|
@@ -6724,6 +7005,36 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6724
7005
|
);
|
|
6725
7006
|
}
|
|
6726
7007
|
}
|
|
7008
|
+
if (spec.revision?.mode === "interpolate") {
|
|
7009
|
+
const { sourceWidth: width, sourceHeight: height, lastFrameWidth, lastFrameHeight } = spec.revision;
|
|
7010
|
+
if (width != null && height != null && (width < 16 || height < 16 || width > 128 || height > 128)) {
|
|
7011
|
+
throw new Error(`PixelLab interpolate keyframes are ${width}x${height}; the API takes 16 to 128 pixels per side`);
|
|
7012
|
+
}
|
|
7013
|
+
if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
|
|
7014
|
+
throw new Error(
|
|
7015
|
+
`PixelLab interpolate lastFrame is ${lastFrameWidth}x${lastFrameHeight}; the start keyframe is ${width}x${height} \u2014 they must match`
|
|
7016
|
+
);
|
|
7017
|
+
}
|
|
7018
|
+
}
|
|
7019
|
+
if (spec.revision?.mode === "edit-animation") {
|
|
7020
|
+
const { sourceWidth: width, sourceHeight: height, sourceAssetId } = spec.revision;
|
|
7021
|
+
if (width != null && height != null) {
|
|
7022
|
+
if (!members || members.length < 2) {
|
|
7023
|
+
throw new Error(
|
|
7024
|
+
`PixelLab edit-animation needs a frame set to edit; ${sourceAssetId} is a single image (revise an animation, a character's directions, or another frame-set revision)`
|
|
7025
|
+
);
|
|
7026
|
+
}
|
|
7027
|
+
if (width < 16 || height < 16 || width > 256 || height > 256) {
|
|
7028
|
+
throw new Error(`PixelLab edit-animation frames are ${width}x${height}; the API takes 16 to 256 pixels per side`);
|
|
7029
|
+
}
|
|
7030
|
+
const ceiling = editAnimationFrameCeiling(width, height);
|
|
7031
|
+
if (members.length > ceiling) {
|
|
7032
|
+
throw new Error(
|
|
7033
|
+
`PixelLab edit-animation takes at most ${ceiling} frames at ${width}x${height} (every frame is packed into one grid); ${sourceAssetId} has ${members.length}`
|
|
7034
|
+
);
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
}
|
|
6727
7038
|
if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
|
|
6728
7039
|
throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
|
|
6729
7040
|
}
|
|
@@ -6740,6 +7051,14 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6740
7051
|
`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)`
|
|
6741
7052
|
);
|
|
6742
7053
|
}
|
|
7054
|
+
if (spec.generator === "uiElement" && (spec.width < 16 || spec.height < 16 || spec.width > 792 || spec.height > 688)) {
|
|
7055
|
+
throw new Error(
|
|
7056
|
+
`PixelLab uiElement is ${spec.width}x${spec.height}; the API takes 16 to 792 wide and 16 to 688 tall (the exact ceiling depends on aspect ratio \u2014 square tops out at 512, 16:9 at 688x384)`
|
|
7057
|
+
);
|
|
7058
|
+
}
|
|
7059
|
+
if (spec.generator === "uiElement" && styleImages.length > 1) {
|
|
7060
|
+
throw new Error("PixelLab uiElement takes one concept image; list at most one styleImages entry");
|
|
7061
|
+
}
|
|
6743
7062
|
if (spec.generator === "map") {
|
|
6744
7063
|
requirePixelLabOption("map", "view", spec.view, ["low top-down", "high top-down", "side"]);
|
|
6745
7064
|
requirePixelLabOption("map", "outline", spec.outline, [
|
|
@@ -6823,8 +7142,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6823
7142
|
throw new Error(`PixelLab isometricTile: size must be 16 to 64 pixels, got ${spec.width}`);
|
|
6824
7143
|
}
|
|
6825
7144
|
}
|
|
6826
|
-
if (spec.generator === "character") this.validateCharacter(spec, styleImages);
|
|
6827
|
-
if (spec.generator === "objectPro") this.validateObjectPro(spec, styleImages);
|
|
7145
|
+
if (spec.generator === "character" && !spec.revision) this.validateCharacter(spec, styleImages);
|
|
7146
|
+
if (spec.generator === "objectPro" && !spec.revision) this.validateObjectPro(spec, styleImages);
|
|
6828
7147
|
if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
|
|
6829
7148
|
throw new Error(`PixelLab ${spec.generator} does not support style images`);
|
|
6830
7149
|
}
|
|
@@ -7391,6 +7710,19 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7391
7710
|
});
|
|
7392
7711
|
return { jobId: res2.ui_asset_id, metadata: { backgroundJobId: res2.background_job_id } };
|
|
7393
7712
|
}
|
|
7713
|
+
if (spec.generator === "uiElement") {
|
|
7714
|
+
const concept = styleImages[0];
|
|
7715
|
+
const res2 = await this.client.generateUiV2({
|
|
7716
|
+
description: spec.prompt,
|
|
7717
|
+
width: spec.width,
|
|
7718
|
+
height: spec.height,
|
|
7719
|
+
conceptImage: concept ? { image: { base64: concept.base64, format: concept.format }, width: concept.width, height: concept.height } : void 0,
|
|
7720
|
+
colorPalette: spec.uiColorPalette,
|
|
7721
|
+
noBackground: spec.noBackground,
|
|
7722
|
+
seed: spec.seed
|
|
7723
|
+
});
|
|
7724
|
+
return { jobId: res2.background_job_id };
|
|
7725
|
+
}
|
|
7394
7726
|
if (spec.generator === "pixflux") {
|
|
7395
7727
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
7396
7728
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -7519,13 +7851,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7519
7851
|
if (!revision.sourceSha256 || !revision.sourceFormat || revision.sourceWidth == null || revision.sourceHeight == null) {
|
|
7520
7852
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
|
|
7521
7853
|
}
|
|
7522
|
-
const
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7854
|
+
const sourceFiles = revision.sourceMembers ?? [{ file: revision.sourceFile, sha256: revision.sourceSha256, format: revision.sourceFormat }];
|
|
7855
|
+
const images = sourceFiles.map((member) => {
|
|
7856
|
+
const bytes = readFileSync2(member.file);
|
|
7857
|
+
if (sha256(bytes) !== member.sha256) {
|
|
7858
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
|
|
7859
|
+
}
|
|
7860
|
+
return { base64: bytes.toString("base64"), format: member.format };
|
|
7861
|
+
});
|
|
7862
|
+
const image = images[0];
|
|
7527
7863
|
const width = revision.sourceWidth;
|
|
7528
7864
|
const height = revision.sourceHeight;
|
|
7865
|
+
const roles = revision.sourceMembers?.map((member) => member.role);
|
|
7529
7866
|
if (revision.mode === "inpaint") {
|
|
7530
7867
|
if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
|
|
7531
7868
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
|
|
@@ -7558,34 +7895,45 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7558
7895
|
paletteImage = { base64: paletteBytes.toString("base64"), format: revision.paletteImageFormat };
|
|
7559
7896
|
}
|
|
7560
7897
|
const res2 = await this.client.reduceColors({
|
|
7561
|
-
|
|
7898
|
+
images,
|
|
7562
7899
|
numColors: revision.numColors,
|
|
7563
7900
|
paletteImage,
|
|
7564
7901
|
dithering: revision.dithering,
|
|
7565
7902
|
ditheringStrength: revision.ditheringStrength
|
|
7566
7903
|
});
|
|
7567
|
-
|
|
7568
|
-
writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), res2.png);
|
|
7569
|
-
return { jobId, metadata: { revisionUsage: res2.usage } };
|
|
7904
|
+
return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
|
|
7570
7905
|
}
|
|
7571
7906
|
if (revision.mode === "correct-pixelart") {
|
|
7572
|
-
const res2 = await this.client.correctPixelart({
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7907
|
+
const res2 = await this.client.correctPixelart({ images, strength: revision.strength });
|
|
7908
|
+
return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
|
|
7909
|
+
}
|
|
7910
|
+
if (revision.mode === "interpolate") {
|
|
7911
|
+
const lastFrame = this.readLastFrame(spec);
|
|
7912
|
+
if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
|
|
7913
|
+
const res2 = await this.client.interpolationV2({
|
|
7914
|
+
startImage: image,
|
|
7915
|
+
endImage: lastFrame,
|
|
7916
|
+
width,
|
|
7917
|
+
height,
|
|
7918
|
+
action: spec.prompt,
|
|
7919
|
+
seed: spec.seed,
|
|
7920
|
+
noBackground: spec.noBackground
|
|
7921
|
+
});
|
|
7922
|
+
return { jobId: res2.background_job_id };
|
|
7923
|
+
}
|
|
7924
|
+
if (revision.mode === "edit-animation") {
|
|
7925
|
+
const res2 = await this.client.editAnimationV2({
|
|
7926
|
+
frames: images,
|
|
7927
|
+
width,
|
|
7928
|
+
height,
|
|
7929
|
+
description: spec.prompt,
|
|
7930
|
+
seed: spec.seed,
|
|
7931
|
+
noBackground: spec.noBackground
|
|
7932
|
+
});
|
|
7933
|
+
return { jobId: res2.background_job_id };
|
|
7576
7934
|
}
|
|
7577
7935
|
if (revision.mode === "animate" || revision.mode === "animate-pixminimax") {
|
|
7578
|
-
|
|
7579
|
-
if (revision.lastFrameFile) {
|
|
7580
|
-
if (!revision.lastFrameSha256 || !revision.lastFrameFormat) {
|
|
7581
|
-
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame is not ready`);
|
|
7582
|
-
}
|
|
7583
|
-
const lastFrameBytes = readFileSync2(revision.lastFrameFile);
|
|
7584
|
-
if (sha256(lastFrameBytes) !== revision.lastFrameSha256) {
|
|
7585
|
-
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame changed after the manifest was resolved`);
|
|
7586
|
-
}
|
|
7587
|
-
lastFrame = { base64: lastFrameBytes.toString("base64"), format: revision.lastFrameFormat };
|
|
7588
|
-
}
|
|
7936
|
+
const lastFrame = this.readLastFrame(spec);
|
|
7589
7937
|
if (revision.mode === "animate") {
|
|
7590
7938
|
const res3 = await this.client.animateWithTextV3({
|
|
7591
7939
|
firstFrame: image,
|
|
@@ -7625,6 +7973,36 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7625
7973
|
});
|
|
7626
7974
|
return { jobId: res.background_job_id };
|
|
7627
7975
|
}
|
|
7976
|
+
/** A revision's pinned ending frame, proven to be the bytes the spec hashed; undefined when none is declared. */
|
|
7977
|
+
readLastFrame(spec) {
|
|
7978
|
+
const revision = spec.revision;
|
|
7979
|
+
if (!revision.lastFrameFile) return void 0;
|
|
7980
|
+
if (!revision.lastFrameSha256 || !revision.lastFrameFormat) {
|
|
7981
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame is not ready`);
|
|
7982
|
+
}
|
|
7983
|
+
const bytes = readFileSync2(revision.lastFrameFile);
|
|
7984
|
+
if (sha256(bytes) !== revision.lastFrameSha256) {
|
|
7985
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame changed after the manifest was resolved`);
|
|
7986
|
+
}
|
|
7987
|
+
return { base64: bytes.toString("base64"), format: revision.lastFrameFormat };
|
|
7988
|
+
}
|
|
7989
|
+
/**
|
|
7990
|
+
* Park a synchronous Cleanup-tier result the way pixflux parks its own:
|
|
7991
|
+
* one `<id>.png` for a single image, or `<id>-<n>.png` per member with the
|
|
7992
|
+
* member roles recorded so `pollCachedRevision` hands them back under the
|
|
7993
|
+
* same names the parent's set used.
|
|
7994
|
+
*/
|
|
7995
|
+
cacheCleanupResult(pngs, usage, roles, fps) {
|
|
7996
|
+
const jobId = randomUUID3();
|
|
7997
|
+
if (!roles) {
|
|
7998
|
+
writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
|
|
7999
|
+
return { jobId, metadata: { revisionUsage: usage } };
|
|
8000
|
+
}
|
|
8001
|
+
for (const [index, png] of pngs.entries()) {
|
|
8002
|
+
writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
|
|
8003
|
+
}
|
|
8004
|
+
return { jobId, metadata: { revisionUsage: usage, revisionMembers: roles, ...fps != null ? { revisionFps: fps } : {} } };
|
|
8005
|
+
}
|
|
7628
8006
|
/**
|
|
7629
8007
|
* What a background job actually billed, once its own record is still
|
|
7630
8008
|
* around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
|
|
@@ -7645,7 +8023,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7645
8023
|
if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
|
|
7646
8024
|
return this.pollCachedRevision(jobId, context);
|
|
7647
8025
|
}
|
|
7648
|
-
if (revisionMode === "animate" || revisionMode === "animate-pixminimax") {
|
|
8026
|
+
if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
|
|
7649
8027
|
return this.pollAnimateRevision(jobId, context);
|
|
7650
8028
|
}
|
|
7651
8029
|
if (context?.spec?.revision) return this.pollRevision(jobId);
|
|
@@ -7668,6 +8046,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7668
8046
|
if (generator === "character") return this.pollCharacter(jobId, context);
|
|
7669
8047
|
if (generator === "objectPro") return this.pollObjectPro(jobId, context);
|
|
7670
8048
|
if (generator === "uiAsset") return this.pollUiAsset(jobId, context);
|
|
8049
|
+
if (generator === "uiElement") return this.pollUiElement(jobId);
|
|
7671
8050
|
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
7672
8051
|
const obj = await this.client.getObject(jobId);
|
|
7673
8052
|
if (obj.status === "review") {
|
|
@@ -7694,15 +8073,32 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7694
8073
|
* work.
|
|
7695
8074
|
*/
|
|
7696
8075
|
async pollCachedRevision(jobId, context) {
|
|
7697
|
-
const
|
|
7698
|
-
|
|
8076
|
+
const usage = context?.metadata?.revisionUsage;
|
|
8077
|
+
const roles = context?.metadata?.revisionMembers;
|
|
8078
|
+
const gone = {
|
|
8079
|
+
status: "failed",
|
|
8080
|
+
error: "revision result is no longer cached locally; re-run submit for this asset"
|
|
8081
|
+
};
|
|
8082
|
+
if (Array.isArray(roles) && roles.length) {
|
|
8083
|
+
const sources = roles.map((role, index) => ({
|
|
8084
|
+
url: `file://${path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
|
|
8085
|
+
role: String(role)
|
|
8086
|
+
}));
|
|
8087
|
+
if (sources.some((source) => !existsSync7(source.url.slice("file://".length)))) return gone;
|
|
8088
|
+
const fps = context?.metadata?.revisionFps;
|
|
8089
|
+
const frames = sources.every((source) => source.role.startsWith("frame-"));
|
|
7699
8090
|
return {
|
|
7700
|
-
status: "
|
|
7701
|
-
|
|
8091
|
+
status: "ready",
|
|
8092
|
+
objectId: jobId,
|
|
8093
|
+
sourceUrl: sources[0].url,
|
|
8094
|
+
sources,
|
|
8095
|
+
...frames && typeof fps === "number" ? { metadata: { frameSet: { fps, count: sources.length } } } : {},
|
|
8096
|
+
billed: billedFromUsage(usage)
|
|
7702
8097
|
};
|
|
7703
8098
|
}
|
|
8099
|
+
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
8100
|
+
if (!existsSync7(file)) return gone;
|
|
7704
8101
|
const sourceUrl = `file://${file}`;
|
|
7705
|
-
const usage = context?.metadata?.revisionUsage;
|
|
7706
8102
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
|
|
7707
8103
|
}
|
|
7708
8104
|
/**
|
|
@@ -7726,16 +8122,21 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7726
8122
|
if (job.status !== "completed") return { status: "processing" };
|
|
7727
8123
|
const billed = billedFromUsage(job.usage);
|
|
7728
8124
|
const done = job.last_response ?? {};
|
|
7729
|
-
const
|
|
7730
|
-
const
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
frameUrls,
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
8125
|
+
const revision = context?.spec?.revision;
|
|
8126
|
+
const fps = revision?.fps ?? revision?.sourceFps ?? 8;
|
|
8127
|
+
const memberRoles = revision?.mode === "edit-animation" ? revision.sourceMembers?.map((member) => member.role) : void 0;
|
|
8128
|
+
const review = (frameUrls) => {
|
|
8129
|
+
const roles = memberRoles?.length === frameUrls.length ? memberRoles : frameUrls.map((_, index) => `frame-${String(index).padStart(2, "0")}`);
|
|
8130
|
+
return {
|
|
8131
|
+
status: "review-set",
|
|
8132
|
+
objectId: jobId,
|
|
8133
|
+
frameUrls,
|
|
8134
|
+
sources: frameUrls.map((url, index) => ({ url, role: roles[index] })),
|
|
8135
|
+
fps,
|
|
8136
|
+
metadata: roles.every((role) => role.startsWith("frame-")) ? { frameSet: { fps, count: frameUrls.length } } : {},
|
|
8137
|
+
billed
|
|
8138
|
+
};
|
|
8139
|
+
};
|
|
7739
8140
|
for (const key of ["frame_urls", "frames", "images"]) {
|
|
7740
8141
|
const list = done[key];
|
|
7741
8142
|
if (!Array.isArray(list) || !list.length) continue;
|
|
@@ -7767,7 +8168,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7767
8168
|
}
|
|
7768
8169
|
return {
|
|
7769
8170
|
status: "failed",
|
|
7770
|
-
error: `Invalid PixelLab response for animate job ${jobId}: completed with no recognized frame list (got: ${Object.keys(done).join(", ") || "no keys"}); update pollAnimateRevision in src/providers/pixellab.ts with the real shape`
|
|
8171
|
+
error: `Invalid PixelLab response for ${revision?.mode ?? "animate"} job ${jobId}: completed with no recognized frame list (got: ${Object.keys(done).join(", ") || "no keys"}); update pollAnimateRevision in src/providers/pixellab.ts with the real shape`
|
|
7771
8172
|
};
|
|
7772
8173
|
}
|
|
7773
8174
|
/**
|
|
@@ -7857,6 +8258,36 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7857
8258
|
error: `Invalid PixelLab response for imagePro job ${jobId}: completed with no recognized images array (got: ${Object.keys(done).join(", ") || "no keys"}); this endpoint's completed shape has not been exercised live yet -- update pollImagePro in src/providers/pixellab.ts with the real shape`
|
|
7858
8259
|
};
|
|
7859
8260
|
}
|
|
8261
|
+
/**
|
|
8262
|
+
* `/generate-ui-v2` completes a plain background job whose
|
|
8263
|
+
* `last_response.images` (per its own docs and Python client) holds the
|
|
8264
|
+
* result. One image is the expected case and goes straight to selected;
|
|
8265
|
+
* should the endpoint ever return several, they go to review like
|
|
8266
|
+
* imagePro's candidates, decoded to the same local cache. The shape has
|
|
8267
|
+
* not been observed live, so anything else fails naming the keys received.
|
|
8268
|
+
*/
|
|
8269
|
+
async pollUiElement(jobId) {
|
|
8270
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
8271
|
+
if (job.status === "failed") return { status: "failed", error: "UI element job failed upstream" };
|
|
8272
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
8273
|
+
const billed = billedFromUsage(job.usage);
|
|
8274
|
+
const done = job.last_response ?? {};
|
|
8275
|
+
const images = Array.isArray(done.images) ? done.images : done.image ? [done.image] : [];
|
|
8276
|
+
const urls = [];
|
|
8277
|
+
for (const [index, candidate] of images.entries()) {
|
|
8278
|
+
const base64 = extractBase64(candidate);
|
|
8279
|
+
if (!base64) continue;
|
|
8280
|
+
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
8281
|
+
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
8282
|
+
urls.push(`file://${file}`);
|
|
8283
|
+
}
|
|
8284
|
+
if (urls.length === 1) return { status: "ready", objectId: jobId, sourceUrl: urls[0], sources: [{ url: urls[0] }], billed };
|
|
8285
|
+
if (urls.length > 1) return { status: "review", candidateUrls: urls, billed };
|
|
8286
|
+
return {
|
|
8287
|
+
status: "failed",
|
|
8288
|
+
error: `Invalid PixelLab response for uiElement job ${jobId}: completed with no recognized image (got: ${Object.keys(done).join(", ") || "no keys"}); update pollUiElement in src/providers/pixellab.ts with the real shape`
|
|
8289
|
+
};
|
|
8290
|
+
}
|
|
7860
8291
|
/**
|
|
7861
8292
|
* Map objects need their own path because the `/map-objects/{id}` record is
|
|
7862
8293
|
* deleted upstream roughly 8 hours after creation while the image survives in
|
|
@@ -8017,9 +8448,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8017
8448
|
if (!url) throw new Error(`tiles job ${jobId} has no variation at index ${index}`);
|
|
8018
8449
|
return { objectId: `${jobId}#${index}`, sourceUrl: url };
|
|
8019
8450
|
}
|
|
8020
|
-
if (generator === "imagePro") {
|
|
8451
|
+
if (generator === "imagePro" || generator === "uiElement") {
|
|
8021
8452
|
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
8022
|
-
if (!existsSync7(file)) throw new Error(
|
|
8453
|
+
if (!existsSync7(file)) throw new Error(`${generator} job ${jobId} has no cached candidate at index ${index}`);
|
|
8023
8454
|
return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
|
|
8024
8455
|
}
|
|
8025
8456
|
const promoted = await this.client.selectFrames(jobId, [index], commonTag);
|
|
@@ -8173,6 +8604,12 @@ function readPose(spec, what, image) {
|
|
|
8173
8604
|
}
|
|
8174
8605
|
return { base64: bytes.toString("base64"), format: image.format };
|
|
8175
8606
|
}
|
|
8607
|
+
function editAnimationFrameCeiling(width, height) {
|
|
8608
|
+
const side = Math.max(width, height);
|
|
8609
|
+
if (side <= 64) return 16;
|
|
8610
|
+
if (side <= 80) return 9;
|
|
8611
|
+
return 4;
|
|
8612
|
+
}
|
|
8176
8613
|
function rotationSources(character) {
|
|
8177
8614
|
const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
8178
8615
|
const sources = [];
|
|
@@ -8265,9 +8702,9 @@ var RetroDiffusionClient = class {
|
|
|
8265
8702
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
8266
8703
|
return Buffer.from(await response.arrayBuffer());
|
|
8267
8704
|
}
|
|
8268
|
-
async call(
|
|
8705
|
+
async call(path47, init = {}) {
|
|
8269
8706
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
8270
|
-
const response = await this.request(`${this.baseUrl}${
|
|
8707
|
+
const response = await this.request(`${this.baseUrl}${path47}`, {
|
|
8271
8708
|
...init,
|
|
8272
8709
|
headers: {
|
|
8273
8710
|
"Content-Type": "application/json",
|
|
@@ -9127,10 +9564,102 @@ registerProvider({
|
|
|
9127
9564
|
import { readFile as readFile6 } from "fs/promises";
|
|
9128
9565
|
import { existsSync as existsSync8 } from "fs";
|
|
9129
9566
|
import path13 from "path";
|
|
9567
|
+
|
|
9568
|
+
// src/loop-directions.ts
|
|
9569
|
+
var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
|
|
9570
|
+
var MIRROR_FIELDS = ["category", "tags", "styles"];
|
|
9571
|
+
function loopDirectionId(shorthandId, direction) {
|
|
9572
|
+
return `${shorthandId}.${direction}`;
|
|
9573
|
+
}
|
|
9574
|
+
function expandLoopDirections(raw) {
|
|
9575
|
+
const families = {};
|
|
9576
|
+
const issues = [];
|
|
9577
|
+
if (!isObject2(raw) || !isObject2(raw.assets)) return { raw, families, issues };
|
|
9578
|
+
const assets = raw.assets;
|
|
9579
|
+
if (!Object.values(assets).some((asset) => isObject2(asset) && isObject2(asset.animation) && "directions" in asset.animation)) {
|
|
9580
|
+
return { raw, families, issues };
|
|
9581
|
+
}
|
|
9582
|
+
const expanded = {};
|
|
9583
|
+
const claimed = new Set(Object.keys(assets));
|
|
9584
|
+
for (const [id, asset] of Object.entries(assets)) {
|
|
9585
|
+
if (!isObject2(asset) || !isObject2(asset.animation) || !("directions" in asset.animation)) {
|
|
9586
|
+
expanded[id] = asset;
|
|
9587
|
+
continue;
|
|
9588
|
+
}
|
|
9589
|
+
const where = `assets.${id}.animation.directions`;
|
|
9590
|
+
const { directions: rawDirections, ...animation } = asset.animation;
|
|
9591
|
+
const parsed = CharacterDirectionSchema.array().min(1).safeParse(rawDirections);
|
|
9592
|
+
if (!parsed.success) {
|
|
9593
|
+
issues.push(`${where}: expected a non-empty list of directions (${CharacterDirectionSchema.options.join(", ")})`);
|
|
9594
|
+
continue;
|
|
9595
|
+
}
|
|
9596
|
+
const directions = parsed.data;
|
|
9597
|
+
const duplicate = directions.find((direction, index) => directions.indexOf(direction) !== index);
|
|
9598
|
+
if (duplicate) {
|
|
9599
|
+
issues.push(`${where}: "${duplicate}" is listed twice`);
|
|
9600
|
+
continue;
|
|
9601
|
+
}
|
|
9602
|
+
if ("direction" in animation) {
|
|
9603
|
+
issues.push(`assets.${id}.animation: direction and directions are mutually exclusive; directions names every loop to make`);
|
|
9604
|
+
continue;
|
|
9605
|
+
}
|
|
9606
|
+
const single = SINGLE_OUTPUT_FIELDS.filter((field) => field in asset);
|
|
9607
|
+
if (single.length) {
|
|
9608
|
+
issues.push(
|
|
9609
|
+
`assets.${id}: ${single.join(", ")} name${single.length === 1 ? "s" : ""} one output, but directions makes ${directions.length} loops; declare the loops explicitly to set ${single.length === 1 ? "it" : "them"} per direction`
|
|
9610
|
+
);
|
|
9611
|
+
continue;
|
|
9612
|
+
}
|
|
9613
|
+
const loops = directions.map((direction) => ({ id: loopDirectionId(id, direction), direction }));
|
|
9614
|
+
const named = new Set(directions);
|
|
9615
|
+
const mirrors = [];
|
|
9616
|
+
for (const { id: loopId, direction } of loops) {
|
|
9617
|
+
const flipped = MIRRORED_DIRECTION[direction];
|
|
9618
|
+
if (flipped === direction || named.has(flipped)) continue;
|
|
9619
|
+
named.add(flipped);
|
|
9620
|
+
mirrors.push({ id: loopDirectionId(id, flipped), of: loopId });
|
|
9621
|
+
}
|
|
9622
|
+
const ids = [...loops.map((loop) => loop.id), ...mirrors.map((mirror) => mirror.id)];
|
|
9623
|
+
const collision = ids.find((expandedId) => claimed.has(expandedId));
|
|
9624
|
+
if (collision) {
|
|
9625
|
+
issues.push(`${where}: expands to "${collision}", which is already declared`);
|
|
9626
|
+
continue;
|
|
9627
|
+
}
|
|
9628
|
+
for (const expandedId of ids) claimed.add(expandedId);
|
|
9629
|
+
for (const loop of loops) {
|
|
9630
|
+
expanded[loop.id] = { ...asset, animation: { ...animation, direction: loop.direction } };
|
|
9631
|
+
}
|
|
9632
|
+
for (const mirror of mirrors) {
|
|
9633
|
+
const inherited = {};
|
|
9634
|
+
for (const field of MIRROR_FIELDS) if (field in asset) inherited[field] = asset[field];
|
|
9635
|
+
expanded[mirror.id] = { mirror: mirror.of, ...inherited };
|
|
9636
|
+
}
|
|
9637
|
+
families[id] = ids;
|
|
9638
|
+
}
|
|
9639
|
+
return { raw: { ...raw, assets: expanded }, families, issues };
|
|
9640
|
+
}
|
|
9641
|
+
function expandAssetFilter(ids, families) {
|
|
9642
|
+
if (!families) return [...ids];
|
|
9643
|
+
return [...new Set(ids.flatMap((id) => families[id] ?? [id]))];
|
|
9644
|
+
}
|
|
9645
|
+
function loopShorthandOf(assetId, families) {
|
|
9646
|
+
if (!families) return void 0;
|
|
9647
|
+
return Object.keys(families).find((id) => families[id].includes(assetId));
|
|
9648
|
+
}
|
|
9649
|
+
function isObject2(value) {
|
|
9650
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9651
|
+
}
|
|
9652
|
+
|
|
9653
|
+
// src/manifest.ts
|
|
9130
9654
|
async function loadManifest(manifestPath) {
|
|
9131
9655
|
const abs = path13.resolve(manifestPath);
|
|
9132
9656
|
if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
|
|
9133
|
-
const
|
|
9657
|
+
const expansion = expandLoopDirections(JSON.parse(await readFile6(abs, "utf8")));
|
|
9658
|
+
if (expansion.issues.length) {
|
|
9659
|
+
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
9660
|
+
${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
|
|
9661
|
+
}
|
|
9662
|
+
const input = ManifestInputSchema.safeParse(expansion.raw);
|
|
9134
9663
|
if (!input.success) {
|
|
9135
9664
|
const issues = formatManifestIssues(input.error.issues);
|
|
9136
9665
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
@@ -9215,7 +9744,8 @@ ${issues}`);
|
|
|
9215
9744
|
throw new ProjectError(`Manifest at ${abs} is invalid:
|
|
9216
9745
|
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
9217
9746
|
}
|
|
9218
|
-
|
|
9747
|
+
const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
|
|
9748
|
+
return { manifest: parsed.data, root: path13.dirname(abs), path: abs, ...loopFamilies };
|
|
9219
9749
|
}
|
|
9220
9750
|
function parentAssetId(asset) {
|
|
9221
9751
|
return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
|
|
@@ -9299,12 +9829,13 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9299
9829
|
);
|
|
9300
9830
|
}
|
|
9301
9831
|
}
|
|
9302
|
-
|
|
9832
|
+
const assetFilter = expandAssetFilter(filter?.assets ?? [], loaded.loopFamilies);
|
|
9833
|
+
for (const unknownAsset of assetFilter) {
|
|
9303
9834
|
if (!manifest.assets[unknownAsset]) {
|
|
9304
9835
|
throw new UsageError(`Unknown asset "${unknownAsset}".`);
|
|
9305
9836
|
}
|
|
9306
9837
|
}
|
|
9307
|
-
const requestedAssetIds = new Set(
|
|
9838
|
+
const requestedAssetIds = new Set(assetFilter.length ? assetFilter : Object.keys(manifest.assets));
|
|
9308
9839
|
const resolutionAssetIds = new Set(requestedAssetIds);
|
|
9309
9840
|
for (const assetId of [...requestedAssetIds]) {
|
|
9310
9841
|
let current = parentAssetId(manifest.assets[assetId]);
|
|
@@ -9399,7 +9930,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9399
9930
|
size = asset.size ?? style.size ?? 64;
|
|
9400
9931
|
width = size;
|
|
9401
9932
|
height = size;
|
|
9402
|
-
} else if (generator === "uiAsset") {
|
|
9933
|
+
} else if (generator === "uiAsset" || generator === "uiElement") {
|
|
9403
9934
|
width = asset.width ?? style.size ?? 256;
|
|
9404
9935
|
height = asset.height ?? style.size ?? 256;
|
|
9405
9936
|
size = Math.max(width, height);
|
|
@@ -9466,7 +9997,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9466
9997
|
seed: style.seed,
|
|
9467
9998
|
uiPieces: generator === "uiAsset" ? asset.pieces : void 0,
|
|
9468
9999
|
uiElements: generator === "uiAsset" ? asset.elements : void 0,
|
|
9469
|
-
uiColorPalette: generator === "uiAsset" ? style.uiColorPalette : void 0,
|
|
10000
|
+
uiColorPalette: generator === "uiAsset" || generator === "uiElement" ? style.uiColorPalette : void 0,
|
|
9470
10001
|
palette: style.palette,
|
|
9471
10002
|
enforcePalette: style.enforcePalette,
|
|
9472
10003
|
noBackground: style.noBackground,
|
|
@@ -9506,8 +10037,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9506
10037
|
terrainView: generator === "terrain" ? style.terrainView : void 0,
|
|
9507
10038
|
isometricTileSize: generator === "isometricTile" ? style.isometricTileSize : void 0,
|
|
9508
10039
|
isometricTileShape: generator === "isometricTile" ? style.isometricTileShape : void 0,
|
|
9509
|
-
|
|
9510
|
-
|
|
10040
|
+
// A revision in a character or objectPro style is a provider edit of
|
|
10041
|
+
// its parent's pixels, not a new character: giving it a base shape
|
|
10042
|
+
// would have packing, the gallery, and adoption treat it as one.
|
|
10043
|
+
...generator === "character" && !asset.revision ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
10044
|
+
...generator === "objectPro" && !asset.revision ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
9511
10045
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
|
|
9512
10046
|
costUnit: "generations",
|
|
9513
10047
|
candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
|
|
@@ -9658,8 +10192,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9658
10192
|
);
|
|
9659
10193
|
}
|
|
9660
10194
|
const sourceSpec = await finalize(asset.revision.from);
|
|
9661
|
-
const
|
|
9662
|
-
const
|
|
10195
|
+
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path13.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
10196
|
+
const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
|
|
10197
|
+
const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
|
|
10198
|
+
const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
|
|
10199
|
+
const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
|
|
9663
10200
|
const maskFile = asset.revision.mask ? path13.resolve(root, asset.revision.mask) : void 0;
|
|
9664
10201
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
9665
10202
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
@@ -9680,6 +10217,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9680
10217
|
sourceHeight: sourceImage?.height ?? null,
|
|
9681
10218
|
sourceFormat: sourceImage?.format ?? null,
|
|
9682
10219
|
sourceSpec,
|
|
10220
|
+
...sourceMembers ? { sourceMembers } : {},
|
|
10221
|
+
...sourceMembers && sourceFps != null ? { sourceFps } : {},
|
|
9683
10222
|
...maskFile ? {
|
|
9684
10223
|
maskFile,
|
|
9685
10224
|
maskSha256: maskImage?.hash ?? null,
|
|
@@ -9829,6 +10368,38 @@ async function optionalRevisionImage(file, label, requiredFormat) {
|
|
|
9829
10368
|
}
|
|
9830
10369
|
return { hash: sha256(bytes), ...metadata };
|
|
9831
10370
|
}
|
|
10371
|
+
async function revisionSourceMembers(stem, label) {
|
|
10372
|
+
if (existsSync8(stem)) return null;
|
|
10373
|
+
const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
|
|
10374
|
+
let roles = [];
|
|
10375
|
+
for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
|
|
10376
|
+
if (set.every((direction) => existsSync8(at(direction)))) {
|
|
10377
|
+
roles = [...set];
|
|
10378
|
+
break;
|
|
10379
|
+
}
|
|
10380
|
+
}
|
|
10381
|
+
if (!roles.length) {
|
|
10382
|
+
for (let index = 0; existsSync8(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
|
|
10383
|
+
roles.push(`frame-${String(index).padStart(2, "0")}`);
|
|
10384
|
+
}
|
|
10385
|
+
}
|
|
10386
|
+
if (!roles.length) return null;
|
|
10387
|
+
const members = [];
|
|
10388
|
+
for (const role of roles) {
|
|
10389
|
+
const file = at(role);
|
|
10390
|
+
const image = await optionalRevisionImage(file, `${label} (${role})`);
|
|
10391
|
+
if (!image) return null;
|
|
10392
|
+
members.push({ role, file, sha256: image.hash, width: image.width, height: image.height, format: image.format });
|
|
10393
|
+
}
|
|
10394
|
+
const first = members[0];
|
|
10395
|
+
const odd = members.find((member) => member.width !== first.width || member.height !== first.height);
|
|
10396
|
+
if (odd) {
|
|
10397
|
+
throw new Error(
|
|
10398
|
+
`${label} is a set whose members differ in size (${first.role} is ${first.width}x${first.height}, ${odd.role} is ${odd.width}x${odd.height}); a revision reads every member as one same-size batch`
|
|
10399
|
+
);
|
|
10400
|
+
}
|
|
10401
|
+
return members;
|
|
10402
|
+
}
|
|
9832
10403
|
async function resolveStyleImages(loaded, styleId) {
|
|
9833
10404
|
const style = loaded.manifest.styles[styleId];
|
|
9834
10405
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
@@ -10342,10 +10913,10 @@ function isSha256Hash(value) {
|
|
|
10342
10913
|
function parseCache(value) {
|
|
10343
10914
|
return HashCacheSchema.parse(value);
|
|
10344
10915
|
}
|
|
10345
|
-
async function loadCache(
|
|
10346
|
-
if (!existsSync10(
|
|
10916
|
+
async function loadCache(path47) {
|
|
10917
|
+
if (!existsSync10(path47)) return { version: 1, hashes: {} };
|
|
10347
10918
|
try {
|
|
10348
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
10919
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path47, "utf8")));
|
|
10349
10920
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
10350
10921
|
return {
|
|
10351
10922
|
version: 1,
|
|
@@ -10357,18 +10928,18 @@ async function loadCache(path45) {
|
|
|
10357
10928
|
return { version: 1, hashes: {} };
|
|
10358
10929
|
}
|
|
10359
10930
|
}
|
|
10360
|
-
async function saveCache(
|
|
10931
|
+
async function saveCache(path47, cache) {
|
|
10361
10932
|
const sorted = {};
|
|
10362
10933
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
10363
10934
|
const hash = cache.hashes[key];
|
|
10364
10935
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
10365
10936
|
sorted[key] = hash;
|
|
10366
10937
|
}
|
|
10367
|
-
await mkdir5(pathModule.dirname(pathModule.resolve(
|
|
10368
|
-
const tmp = `${
|
|
10938
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path47)), { recursive: true });
|
|
10939
|
+
const tmp = `${path47}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
10369
10940
|
try {
|
|
10370
10941
|
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
10371
|
-
await rename5(tmp,
|
|
10942
|
+
await rename5(tmp, path47);
|
|
10372
10943
|
} finally {
|
|
10373
10944
|
await rm5(tmp, { force: true });
|
|
10374
10945
|
}
|
|
@@ -12271,11 +12842,21 @@ function assetStyles(raw, asset) {
|
|
|
12271
12842
|
const own = Array.isArray(asset.styles) ? asset.styles : [];
|
|
12272
12843
|
return own.length ? own : Object.keys(raw.styles ?? {});
|
|
12273
12844
|
}
|
|
12845
|
+
function declaredAssets(raw) {
|
|
12846
|
+
const expanded = expandLoopDirections(raw).raw;
|
|
12847
|
+
return new Set(Object.keys(expanded.assets ?? {}));
|
|
12848
|
+
}
|
|
12849
|
+
function undeclaredAsset(raw, assetId) {
|
|
12850
|
+
const shorthand = loopShorthandOf(assetId, expandLoopDirections(raw).families);
|
|
12851
|
+
return new ManifestEditError(
|
|
12852
|
+
shorthand ? `asset "${assetId}" is expanded from "${shorthand}"'s animation.directions; edit "${shorthand}" (it applies to every direction), or write this direction out as its own asset to change it alone` : `asset "${assetId}" is not declared by the manifest`
|
|
12853
|
+
);
|
|
12854
|
+
}
|
|
12274
12855
|
function applyEdit(raw, edit) {
|
|
12275
12856
|
raw.assets ??= {};
|
|
12276
12857
|
if (edit.action === "set-source" || edit.action === "clear-source") {
|
|
12277
12858
|
const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
12278
|
-
if (!asset2) throw
|
|
12859
|
+
if (!asset2) throw undeclaredAsset(raw, edit.assetId);
|
|
12279
12860
|
if (!raw.styles || !Object.hasOwn(raw.styles, edit.styleId)) {
|
|
12280
12861
|
throw new ManifestEditError(`unknown style "${edit.styleId}"`);
|
|
12281
12862
|
}
|
|
@@ -12327,7 +12908,7 @@ function applyEdit(raw, edit) {
|
|
|
12327
12908
|
return;
|
|
12328
12909
|
}
|
|
12329
12910
|
if (edit.action === "add-asset") {
|
|
12330
|
-
if (
|
|
12911
|
+
if (declaredAssets(raw).has(edit.assetId)) {
|
|
12331
12912
|
throw new ManifestEditError(`asset "${edit.assetId}" already exists`);
|
|
12332
12913
|
}
|
|
12333
12914
|
const { styles, tags, ...rest } = edit.asset;
|
|
@@ -12341,20 +12922,20 @@ function applyEdit(raw, edit) {
|
|
|
12341
12922
|
}
|
|
12342
12923
|
asset2.styles = styles;
|
|
12343
12924
|
}
|
|
12344
|
-
if (edit.asset.revision && !
|
|
12925
|
+
if (edit.asset.revision && !declaredAssets(raw).has(edit.asset.revision.from)) {
|
|
12345
12926
|
throw new ManifestEditError(`revision parent "${edit.asset.revision.from}" is not declared by the manifest`);
|
|
12346
12927
|
}
|
|
12347
|
-
if (edit.asset.state && !
|
|
12928
|
+
if (edit.asset.state && !declaredAssets(raw).has(edit.asset.state.of)) {
|
|
12348
12929
|
throw new ManifestEditError(`state parent "${edit.asset.state.of}" is not declared by the manifest`);
|
|
12349
12930
|
}
|
|
12350
|
-
if (edit.asset.animation && !
|
|
12931
|
+
if (edit.asset.animation && !declaredAssets(raw).has(edit.asset.animation.of)) {
|
|
12351
12932
|
throw new ManifestEditError(`animation parent "${edit.asset.animation.of}" is not declared by the manifest`);
|
|
12352
12933
|
}
|
|
12353
12934
|
raw.assets[edit.assetId] = asset2;
|
|
12354
12935
|
return;
|
|
12355
12936
|
}
|
|
12356
12937
|
const asset = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
|
|
12357
|
-
if (!asset) throw
|
|
12938
|
+
if (!asset) throw undeclaredAsset(raw, edit.assetId);
|
|
12358
12939
|
const { patch } = edit;
|
|
12359
12940
|
if (patch.prompt !== void 0) asset.prompt = patch.prompt;
|
|
12360
12941
|
if (patch.promptForStyle) {
|
|
@@ -13001,6 +13582,7 @@ async function samePixels(a, b) {
|
|
|
13001
13582
|
var metadataFps = (entry) => entry ? frameSetFps(entry) : null;
|
|
13002
13583
|
function describeCharacter(spec, entry) {
|
|
13003
13584
|
const recordedCharacter = entry?.providerMetadata?.[entry.provider]?.character;
|
|
13585
|
+
if (spec?.revision) return null;
|
|
13004
13586
|
if (spec?.character) {
|
|
13005
13587
|
const character = spec.character;
|
|
13006
13588
|
return {
|
|
@@ -13141,6 +13723,7 @@ async function describeQuality(media, root, inspection) {
|
|
|
13141
13723
|
}
|
|
13142
13724
|
async function buildGallerySnapshot(opts) {
|
|
13143
13725
|
const { loaded, specs, lock } = opts;
|
|
13726
|
+
const filter = opts.filter?.assets?.length ? { ...opts.filter, assets: expandAssetFilter(opts.filter.assets, loaded.loopFamilies) } : opts.filter;
|
|
13144
13727
|
const root = loaded.root;
|
|
13145
13728
|
const media = /* @__PURE__ */ new Map();
|
|
13146
13729
|
const plan = await buildPlan(specs, lock);
|
|
@@ -13285,7 +13868,7 @@ async function buildGallerySnapshot(opts) {
|
|
|
13285
13868
|
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
13286
13869
|
if (shown.has(key)) continue;
|
|
13287
13870
|
if (isDeclared(loaded, entry.styleId, entry.assetId)) continue;
|
|
13288
|
-
if (!matchesFilter(
|
|
13871
|
+
if (!matchesFilter(filter, entry.styleId, entry.assetId)) continue;
|
|
13289
13872
|
const outputs = await Promise.all(
|
|
13290
13873
|
entry.outputs.map(
|
|
13291
13874
|
(output) => describeOutput(media, root, resolveOutputPath(output.path, root), output)
|
|
@@ -13451,7 +14034,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
|
|
|
13451
14034
|
try {
|
|
13452
14035
|
const loaded = await loadManifest(manifestPath);
|
|
13453
14036
|
const styleFilter = filter.styles.filter((id) => loaded.manifest.styles[id]);
|
|
13454
|
-
const assetFilter = filter.assets.filter((id) => loaded.manifest.assets[id]);
|
|
14037
|
+
const assetFilter = expandAssetFilter(filter.assets, loaded.loopFamilies).filter((id) => loaded.manifest.assets[id]);
|
|
13455
14038
|
const excluded = filter.styles.length > 0 && styleFilter.length === 0 || filter.assets.length > 0 && assetFilter.length === 0;
|
|
13456
14039
|
const lock = await loadLock(lockPath);
|
|
13457
14040
|
let projectItems = [];
|
|
@@ -17723,20 +18306,173 @@ function sheetFormat(args) {
|
|
|
17723
18306
|
return format;
|
|
17724
18307
|
}
|
|
17725
18308
|
|
|
17726
|
-
// src/cli/commands/
|
|
18309
|
+
// src/cli/commands/pixellab-utils.ts
|
|
17727
18310
|
import path40 from "path";
|
|
17728
|
-
import { readFile as readFile27 } from "fs/promises";
|
|
17729
18311
|
|
|
17730
|
-
// src/
|
|
17731
|
-
import path39 from "path";
|
|
18312
|
+
// src/pixellab-utils.ts
|
|
17732
18313
|
import { existsSync as existsSync29 } from "fs";
|
|
17733
|
-
import { readFile as readFile26 } from "fs/promises";
|
|
18314
|
+
import { mkdir as mkdir11, readFile as readFile26, writeFile as writeFile14 } from "fs/promises";
|
|
18315
|
+
import path39 from "path";
|
|
18316
|
+
var UNZOOM_MIN_SIDE = 256;
|
|
18317
|
+
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
18318
|
+
function defaultUnzoomOut(input) {
|
|
18319
|
+
const ext = path39.extname(input);
|
|
18320
|
+
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
18321
|
+
}
|
|
18322
|
+
async function unzoomFile(client2, input, opts = {}) {
|
|
18323
|
+
const out = path39.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
18324
|
+
if (path39.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
18325
|
+
if (existsSync29(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
18326
|
+
const bytes = await readFile26(input);
|
|
18327
|
+
const metadata = imageMetadata(bytes);
|
|
18328
|
+
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
18329
|
+
const { width, height, format } = metadata;
|
|
18330
|
+
if (width < UNZOOM_MIN_SIDE || height < UNZOOM_MIN_SIDE) {
|
|
18331
|
+
throw new Error(
|
|
18332
|
+
`${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`
|
|
18333
|
+
);
|
|
18334
|
+
}
|
|
18335
|
+
if (width * height > UNZOOM_MAX_AREA) {
|
|
18336
|
+
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
18337
|
+
}
|
|
18338
|
+
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
18339
|
+
await mkdir11(path39.dirname(out), { recursive: true });
|
|
18340
|
+
await writeFile14(out, res.png);
|
|
18341
|
+
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
18342
|
+
}
|
|
18343
|
+
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
18344
|
+
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
18345
|
+
var FONT_COST_GENERATIONS = 25;
|
|
18346
|
+
function fontOutputPaths(out) {
|
|
18347
|
+
const base = path39.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
18348
|
+
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
18349
|
+
}
|
|
18350
|
+
async function generateFont(client2, opts) {
|
|
18351
|
+
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
18352
|
+
if (!opts.force) {
|
|
18353
|
+
for (const file of [ttf, atlas]) {
|
|
18354
|
+
if (existsSync29(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
18355
|
+
}
|
|
18356
|
+
}
|
|
18357
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
18358
|
+
const interval = opts.intervalMs ?? 5e3;
|
|
18359
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
18360
|
+
const submitted = await client2.generateFontPro({
|
|
18361
|
+
description: opts.description,
|
|
18362
|
+
weight: opts.weight,
|
|
18363
|
+
glyphPx: opts.glyphPx,
|
|
18364
|
+
seed: opts.seed,
|
|
18365
|
+
fontName: opts.fontName
|
|
18366
|
+
});
|
|
18367
|
+
const jobId = submitted.background_job_id;
|
|
18368
|
+
const started = Date.now();
|
|
18369
|
+
for (; ; ) {
|
|
18370
|
+
const job = await client2.getFontJob(jobId);
|
|
18371
|
+
opts.onProgress?.(job.status);
|
|
18372
|
+
if (job.status === "failed") throw new Error(`PixelLab font job ${jobId} failed upstream`);
|
|
18373
|
+
if (job.status === "completed") {
|
|
18374
|
+
if (!job.download_ttf_url || !job.download_atlas_url) {
|
|
18375
|
+
throw new Error(`PixelLab font job ${jobId} completed without both download URLs`);
|
|
18376
|
+
}
|
|
18377
|
+
const [ttfBytes, atlasBytes] = await Promise.all([
|
|
18378
|
+
client2.download(job.download_ttf_url),
|
|
18379
|
+
client2.download(job.download_atlas_url)
|
|
18380
|
+
]);
|
|
18381
|
+
await mkdir11(path39.dirname(ttf), { recursive: true });
|
|
18382
|
+
await writeFile14(ttf, ttfBytes);
|
|
18383
|
+
await writeFile14(atlas, atlasBytes);
|
|
18384
|
+
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
18385
|
+
}
|
|
18386
|
+
if (Date.now() - started > timeout) {
|
|
18387
|
+
throw new Error(
|
|
18388
|
+
`PixelLab font job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
18389
|
+
);
|
|
18390
|
+
}
|
|
18391
|
+
await sleep3(interval);
|
|
18392
|
+
}
|
|
18393
|
+
}
|
|
18394
|
+
|
|
18395
|
+
// src/cli/commands/pixellab-utils.ts
|
|
18396
|
+
function loadKeys(args) {
|
|
18397
|
+
loadEnvFiles(path40.dirname(path40.resolve(args.manifest)));
|
|
18398
|
+
loadEnvFiles(process.cwd());
|
|
18399
|
+
}
|
|
18400
|
+
async function runUnzoom(args) {
|
|
18401
|
+
if (!args.from) throw new UsageError("unzoom needs --from <image>: the upscaled PNG or JPEG to shrink back to its native grid");
|
|
18402
|
+
const out = args.out ?? defaultUnzoomOut(args.from);
|
|
18403
|
+
if (args.dryRun) {
|
|
18404
|
+
log(` would unzoom ${args.from} \u2192 ${out}${args.quantize === void 0 ? "" : ` (quantize ${args.quantize})`}; nothing sent`);
|
|
18405
|
+
return;
|
|
18406
|
+
}
|
|
18407
|
+
loadKeys(args);
|
|
18408
|
+
const res = await unzoomFile(clientFromEnv(), args.from, { out, quantize: args.quantize, force: args.force });
|
|
18409
|
+
if (args.json) {
|
|
18410
|
+
log(JSON.stringify({ version: 1, ...res }, null, 2));
|
|
18411
|
+
return;
|
|
18412
|
+
}
|
|
18413
|
+
log(
|
|
18414
|
+
` ${res.original.width}x${res.original.height} \u2192 ${res.unzoomed.width}x${res.unzoomed.height} (each art pixel was ${res.zoomFactor}x${res.zoomFactor}); wrote ${res.out}`
|
|
18415
|
+
);
|
|
18416
|
+
log(" the result is opaque: PixelLab composites transparency onto white first");
|
|
18417
|
+
}
|
|
18418
|
+
async function runFont(args) {
|
|
18419
|
+
if (!args.description) throw new UsageError('font needs --description "<style>", e.g. "warm orange arcade font"');
|
|
18420
|
+
if (!args.out) throw new UsageError("font needs --out <base>: <base>.ttf and <base>.png are written");
|
|
18421
|
+
const weight = args.weight ?? "Regular";
|
|
18422
|
+
if (!FONT_WEIGHTS.includes(weight)) {
|
|
18423
|
+
throw new UsageError(`--weight must be ${FONT_WEIGHTS.join(" or ")}, got "${args.weight}"`);
|
|
18424
|
+
}
|
|
18425
|
+
const glyphPx = args.glyphPx ?? 16;
|
|
18426
|
+
if (!FONT_GLYPH_SIZES.includes(glyphPx)) {
|
|
18427
|
+
throw new UsageError(`--glyph-px must be ${FONT_GLYPH_SIZES.join(", ")}, got "${args.glyphPx}"`);
|
|
18428
|
+
}
|
|
18429
|
+
const { ttf, atlas } = fontOutputPaths(args.out);
|
|
18430
|
+
log(` font: "${args.description}", ${weight}, ${glyphPx}px glyphs \u2192 ${ttf} + ${atlas}`);
|
|
18431
|
+
log(` cost: ${FONT_COST_GENERATIONS} generations (PixelLab's documented price)`);
|
|
18432
|
+
if (args.budget !== void 0 && args.budget < FONT_COST_GENERATIONS) {
|
|
18433
|
+
throw new Error(`--budget ${args.budget} is below this font's ${FONT_COST_GENERATIONS}-generation cost`);
|
|
18434
|
+
}
|
|
18435
|
+
if (args.dryRun) {
|
|
18436
|
+
log(" dry run: nothing sent");
|
|
18437
|
+
return;
|
|
18438
|
+
}
|
|
18439
|
+
if (!await confirm(` Spend ${FONT_COST_GENERATIONS} generations on this font?`, args.yes)) return;
|
|
18440
|
+
loadKeys(args);
|
|
18441
|
+
let last = "";
|
|
18442
|
+
const res = await generateFont(clientFromEnv(), {
|
|
18443
|
+
description: args.description,
|
|
18444
|
+
weight,
|
|
18445
|
+
glyphPx,
|
|
18446
|
+
fontName: args.name,
|
|
18447
|
+
out: args.out,
|
|
18448
|
+
force: args.force,
|
|
18449
|
+
onProgress: (status) => {
|
|
18450
|
+
if (status !== last) log(` ${status}\u2026`);
|
|
18451
|
+
last = status;
|
|
18452
|
+
}
|
|
18453
|
+
});
|
|
18454
|
+
if (args.json) {
|
|
18455
|
+
log(JSON.stringify({ version: 1, ...res }, null, 2));
|
|
18456
|
+
return;
|
|
18457
|
+
}
|
|
18458
|
+
log(` wrote ${res.ttf}`);
|
|
18459
|
+
log(` wrote ${res.atlas}`);
|
|
18460
|
+
}
|
|
18461
|
+
|
|
18462
|
+
// src/cli/commands/quality.ts
|
|
18463
|
+
import path42 from "path";
|
|
18464
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
18465
|
+
|
|
18466
|
+
// src/pipeline/quality-regression.ts
|
|
18467
|
+
import path41 from "path";
|
|
18468
|
+
import { existsSync as existsSync30 } from "fs";
|
|
18469
|
+
import { readFile as readFile27 } from "fs/promises";
|
|
17734
18470
|
import { z as z9 } from "zod";
|
|
17735
18471
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
17736
18472
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
17737
18473
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
17738
18474
|
var QualityPathSchema = z9.string().min(1).refine(
|
|
17739
|
-
(value) => !
|
|
18475
|
+
(value) => !path41.posix.isAbsolute(value) && !value.includes("\\"),
|
|
17740
18476
|
"expected a portable relative path"
|
|
17741
18477
|
);
|
|
17742
18478
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -17883,10 +18619,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
17883
18619
|
};
|
|
17884
18620
|
}
|
|
17885
18621
|
async function measureImageQuality(file) {
|
|
17886
|
-
const absolute =
|
|
18622
|
+
const absolute = path41.resolve(file);
|
|
17887
18623
|
let bytes;
|
|
17888
18624
|
try {
|
|
17889
|
-
bytes = await
|
|
18625
|
+
bytes = await readFile27(absolute);
|
|
17890
18626
|
} catch (error) {
|
|
17891
18627
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
17892
18628
|
}
|
|
@@ -17905,11 +18641,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
17905
18641
|
}
|
|
17906
18642
|
const ids = parsed.data.map((entry) => entry.id);
|
|
17907
18643
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
17908
|
-
const root =
|
|
18644
|
+
const root = path41.dirname(path41.resolve(inputsFilePath));
|
|
17909
18645
|
return parsed.data.map((entry) => ({
|
|
17910
18646
|
id: entry.id,
|
|
17911
|
-
file:
|
|
17912
|
-
...entry.record ? { record:
|
|
18647
|
+
file: path41.resolve(root, entry.path),
|
|
18648
|
+
...entry.record ? { record: path41.resolve(root, entry.record) } : {},
|
|
17913
18649
|
tolerances: QualityToleranceSchema.parse({
|
|
17914
18650
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
17915
18651
|
...entry.tolerances
|
|
@@ -17917,7 +18653,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
17917
18653
|
}));
|
|
17918
18654
|
}
|
|
17919
18655
|
function portableRelative2(from, to) {
|
|
17920
|
-
return
|
|
18656
|
+
return path41.relative(from, path41.resolve(to)).split(path41.sep).join("/") || ".";
|
|
17921
18657
|
}
|
|
17922
18658
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
17923
18659
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -17932,20 +18668,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
17932
18668
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
17933
18669
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
17934
18670
|
}
|
|
17935
|
-
const outputs = record.outputs.map((output) =>
|
|
17936
|
-
if (!outputs.includes(
|
|
18671
|
+
const outputs = record.outputs.map((output) => path41.resolve(path41.dirname(recordPath), output.path));
|
|
18672
|
+
if (!outputs.includes(path41.resolve(imagePath))) {
|
|
17937
18673
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
17938
18674
|
}
|
|
17939
18675
|
}
|
|
17940
18676
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
17941
18677
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
17942
|
-
const absolute =
|
|
18678
|
+
const absolute = path41.resolve(baselinePath);
|
|
17943
18679
|
for (const input of inputs) {
|
|
17944
|
-
if (
|
|
18680
|
+
if (path41.resolve(input.file) === absolute || input.record && path41.resolve(input.record) === absolute) {
|
|
17945
18681
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
17946
18682
|
}
|
|
17947
18683
|
}
|
|
17948
|
-
const root =
|
|
18684
|
+
const root = path41.dirname(absolute);
|
|
17949
18685
|
const cases = [];
|
|
17950
18686
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
17951
18687
|
const expected = await measureImageQuality(input.file);
|
|
@@ -17972,8 +18708,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
17972
18708
|
cases
|
|
17973
18709
|
});
|
|
17974
18710
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
17975
|
-
if (
|
|
17976
|
-
const current = await
|
|
18711
|
+
if (existsSync30(absolute) && !options.force) {
|
|
18712
|
+
const current = await readFile27(absolute);
|
|
17977
18713
|
if (!current.equals(data)) {
|
|
17978
18714
|
throw new OverwriteRefusedError(
|
|
17979
18715
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -17985,10 +18721,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
17985
18721
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
17986
18722
|
}
|
|
17987
18723
|
async function readQualityBaseline(baselinePath) {
|
|
17988
|
-
const absolute =
|
|
18724
|
+
const absolute = path41.resolve(baselinePath);
|
|
17989
18725
|
let raw;
|
|
17990
18726
|
try {
|
|
17991
|
-
raw = JSON.parse(await
|
|
18727
|
+
raw = JSON.parse(await readFile27(absolute, "utf8"));
|
|
17992
18728
|
} catch (error) {
|
|
17993
18729
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
17994
18730
|
}
|
|
@@ -18058,12 +18794,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
18058
18794
|
return { violations, warnings };
|
|
18059
18795
|
}
|
|
18060
18796
|
async function checkQualityBaseline(baselinePath) {
|
|
18061
|
-
const absolute =
|
|
18797
|
+
const absolute = path41.resolve(baselinePath);
|
|
18062
18798
|
const baseline = await readQualityBaseline(absolute);
|
|
18063
|
-
const root =
|
|
18799
|
+
const root = path41.dirname(absolute);
|
|
18064
18800
|
const cases = [];
|
|
18065
18801
|
for (const entry of baseline.cases) {
|
|
18066
|
-
const file =
|
|
18802
|
+
const file = path41.resolve(root, entry.file);
|
|
18067
18803
|
let actual = null;
|
|
18068
18804
|
const violations = [];
|
|
18069
18805
|
const warnings = [];
|
|
@@ -18076,7 +18812,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
18076
18812
|
violations.push(message4(error));
|
|
18077
18813
|
}
|
|
18078
18814
|
if (entry.record) {
|
|
18079
|
-
const recordPath =
|
|
18815
|
+
const recordPath = path41.resolve(root, entry.record.path);
|
|
18080
18816
|
try {
|
|
18081
18817
|
const recordHash = await sha256File(recordPath);
|
|
18082
18818
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -18118,15 +18854,15 @@ async function runQuality(args) {
|
|
|
18118
18854
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
18119
18855
|
let raw;
|
|
18120
18856
|
try {
|
|
18121
|
-
raw = JSON.parse(await
|
|
18857
|
+
raw = JSON.parse(await readFile28(path42.resolve(args.inputs), "utf8"));
|
|
18122
18858
|
} catch (error) {
|
|
18123
18859
|
throw new Error(
|
|
18124
|
-
`Could not read quality inputs ${
|
|
18860
|
+
`Could not read quality inputs ${path42.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
18125
18861
|
{ cause: error }
|
|
18126
18862
|
);
|
|
18127
18863
|
}
|
|
18128
18864
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
18129
|
-
if (
|
|
18865
|
+
if (path42.resolve(args.inputs) === path42.resolve(args.out)) {
|
|
18130
18866
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
18131
18867
|
}
|
|
18132
18868
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -18139,7 +18875,7 @@ async function runQuality(args) {
|
|
|
18139
18875
|
}, null, 2));
|
|
18140
18876
|
} else {
|
|
18141
18877
|
log(
|
|
18142
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
18878
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path42.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
18143
18879
|
);
|
|
18144
18880
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
18145
18881
|
}
|
|
@@ -18165,12 +18901,12 @@ async function runQuality(args) {
|
|
|
18165
18901
|
}
|
|
18166
18902
|
|
|
18167
18903
|
// src/cli/commands/recipe.ts
|
|
18168
|
-
import
|
|
18904
|
+
import path44 from "path";
|
|
18169
18905
|
|
|
18170
18906
|
// src/recipes.ts
|
|
18171
|
-
import
|
|
18172
|
-
import { existsSync as
|
|
18173
|
-
import { readdir as readdir3, readFile as
|
|
18907
|
+
import path43 from "path";
|
|
18908
|
+
import { existsSync as existsSync31 } from "fs";
|
|
18909
|
+
import { readdir as readdir3, readFile as readFile29 } from "fs/promises";
|
|
18174
18910
|
import { fileURLToPath } from "url";
|
|
18175
18911
|
import { z as z10 } from "zod";
|
|
18176
18912
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
@@ -18179,7 +18915,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
18179
18915
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
18180
18916
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
18181
18917
|
var PortablePathSchema = z10.string().min(1).refine((value) => {
|
|
18182
|
-
if (value.includes("\\") ||
|
|
18918
|
+
if (value.includes("\\") || path43.posix.isAbsolute(value)) return false;
|
|
18183
18919
|
const parts = value.split("/");
|
|
18184
18920
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
18185
18921
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -18355,10 +19091,10 @@ function message5(error) {
|
|
|
18355
19091
|
return error instanceof Error ? error.message : String(error);
|
|
18356
19092
|
}
|
|
18357
19093
|
async function readRecipeFile(recipePath, bundled) {
|
|
18358
|
-
const absolute =
|
|
19094
|
+
const absolute = path43.resolve(recipePath);
|
|
18359
19095
|
let raw;
|
|
18360
19096
|
try {
|
|
18361
|
-
raw = JSON.parse(await
|
|
19097
|
+
raw = JSON.parse(await readFile29(absolute, "utf8"));
|
|
18362
19098
|
} catch (error) {
|
|
18363
19099
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
18364
19100
|
}
|
|
@@ -18369,13 +19105,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
18369
19105
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
18370
19106
|
);
|
|
18371
19107
|
}
|
|
18372
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
19108
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path43.dirname(absolute), bundled };
|
|
18373
19109
|
}
|
|
18374
19110
|
async function walkRecipeFiles(root) {
|
|
18375
|
-
if (!
|
|
19111
|
+
if (!existsSync31(root)) return [];
|
|
18376
19112
|
const out = [];
|
|
18377
19113
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
18378
|
-
const resolved =
|
|
19114
|
+
const resolved = path43.join(root, entry.name);
|
|
18379
19115
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
18380
19116
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
18381
19117
|
}
|
|
@@ -18386,7 +19122,7 @@ function bundledRecipeRoot() {
|
|
|
18386
19122
|
}
|
|
18387
19123
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
18388
19124
|
const recipes = await Promise.all(
|
|
18389
|
-
(await walkRecipeFiles(
|
|
19125
|
+
(await walkRecipeFiles(path43.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
18390
19126
|
);
|
|
18391
19127
|
return recipes.sort(
|
|
18392
19128
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -18409,9 +19145,9 @@ function parseSelector(selector) {
|
|
|
18409
19145
|
return { id: match[1], version: match[2] };
|
|
18410
19146
|
}
|
|
18411
19147
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
18412
|
-
const local =
|
|
18413
|
-
if (
|
|
18414
|
-
const recipePath =
|
|
19148
|
+
const local = path43.resolve(target);
|
|
19149
|
+
if (existsSync31(local)) {
|
|
19150
|
+
const recipePath = path43.basename(local) === RECIPE_FILE ? local : path43.join(local, RECIPE_FILE);
|
|
18415
19151
|
return readRecipeFile(recipePath, false);
|
|
18416
19152
|
}
|
|
18417
19153
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -18425,8 +19161,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
18425
19161
|
return matches[0];
|
|
18426
19162
|
}
|
|
18427
19163
|
async function verifyFile(root, file) {
|
|
18428
|
-
const absolute =
|
|
18429
|
-
if (!
|
|
19164
|
+
const absolute = path43.resolve(root, ...file.path.split("/"));
|
|
19165
|
+
if (!existsSync31(absolute)) {
|
|
18430
19166
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
18431
19167
|
}
|
|
18432
19168
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -18441,7 +19177,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
18441
19177
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
18442
19178
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
18443
19179
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
18444
|
-
const modelRoot = options.modelRoot ?
|
|
19180
|
+
const modelRoot = options.modelRoot ? path43.resolve(options.modelRoot) : null;
|
|
18445
19181
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
18446
19182
|
...await verifyFile(modelRoot, model),
|
|
18447
19183
|
source: model.source,
|
|
@@ -18475,7 +19211,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
18475
19211
|
};
|
|
18476
19212
|
}
|
|
18477
19213
|
function renderStyle(style, destination, cwd) {
|
|
18478
|
-
const portableDir =
|
|
19214
|
+
const portableDir = path43.relative(cwd, destination).split(path43.sep).join("/") || ".";
|
|
18479
19215
|
const replace = (value) => {
|
|
18480
19216
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
18481
19217
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -18487,25 +19223,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
18487
19223
|
return replace(style);
|
|
18488
19224
|
}
|
|
18489
19225
|
async function installRecipe(target, options = {}) {
|
|
18490
|
-
const cwd =
|
|
19226
|
+
const cwd = path43.resolve(options.cwd ?? process.cwd());
|
|
18491
19227
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
18492
19228
|
const verification = await verifyRecipe(loaded.path);
|
|
18493
19229
|
if (!verification.ok) {
|
|
18494
19230
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
18495
19231
|
}
|
|
18496
|
-
const destination = options.out ?
|
|
19232
|
+
const destination = options.out ? path43.resolve(cwd, options.out) : path43.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
18497
19233
|
const sources = [
|
|
18498
19234
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
18499
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
19235
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path43.join(loaded.dir, ...file.path.split("/")) }))
|
|
18500
19236
|
];
|
|
18501
19237
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
18502
|
-
path:
|
|
18503
|
-
data: await
|
|
19238
|
+
path: path43.join(destination, ...file.path.split("/")),
|
|
19239
|
+
data: await readFile29(file.source)
|
|
18504
19240
|
})));
|
|
18505
19241
|
if (!options.force) {
|
|
18506
19242
|
for (const file of files) {
|
|
18507
|
-
if (!
|
|
18508
|
-
const current = await
|
|
19243
|
+
if (!existsSync31(file.path)) continue;
|
|
19244
|
+
const current = await readFile29(file.path);
|
|
18509
19245
|
if (!current.equals(file.data)) {
|
|
18510
19246
|
throw new OverwriteRefusedError(
|
|
18511
19247
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -18599,14 +19335,14 @@ async function runRecipe(args) {
|
|
|
18599
19335
|
}, null, 2));
|
|
18600
19336
|
} else {
|
|
18601
19337
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
18602
|
-
log(` ${
|
|
19338
|
+
log(` ${path44.relative(process.cwd(), result.destination) || "."}`);
|
|
18603
19339
|
log(`
|
|
18604
19340
|
Add this entry under your manifest's styles object:`);
|
|
18605
19341
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
18606
19342
|
if (result.recipe.models.length) {
|
|
18607
19343
|
log(`
|
|
18608
19344
|
Models are not downloaded automatically. Verify them with:`);
|
|
18609
|
-
log(` pixelkiln recipe verify ${
|
|
19345
|
+
log(` pixelkiln recipe verify ${path44.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
18610
19346
|
}
|
|
18611
19347
|
}
|
|
18612
19348
|
return;
|
|
@@ -18614,7 +19350,7 @@ async function runRecipe(args) {
|
|
|
18614
19350
|
}
|
|
18615
19351
|
|
|
18616
19352
|
// src/cli/commands/refine.ts
|
|
18617
|
-
import
|
|
19353
|
+
import path45 from "path";
|
|
18618
19354
|
async function runRefine(args) {
|
|
18619
19355
|
if (args.from || args.subcommand === "approve") {
|
|
18620
19356
|
if (!args.from) {
|
|
@@ -18644,12 +19380,12 @@ async function runRefine(args) {
|
|
|
18644
19380
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
18645
19381
|
);
|
|
18646
19382
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
18647
|
-
log(` wrote ${
|
|
18648
|
-
log(` quality record: ${
|
|
19383
|
+
log(` wrote ${path45.relative(process.cwd(), result3.output)}`);
|
|
19384
|
+
log(` quality record: ${path45.relative(process.cwd(), result3.record)}`);
|
|
18649
19385
|
log(`
|
|
18650
19386
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
18651
19387
|
log(
|
|
18652
|
-
` pixelkiln refine approve --from ${
|
|
19388
|
+
` pixelkiln refine approve --from ${path45.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
18653
19389
|
);
|
|
18654
19390
|
}
|
|
18655
19391
|
return;
|
|
@@ -18677,7 +19413,7 @@ async function runRefine(args) {
|
|
|
18677
19413
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
18678
19414
|
else {
|
|
18679
19415
|
log(
|
|
18680
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
19416
|
+
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${path45.relative(process.cwd(), result3.record)} by ${args.reviewer.trim()}` : ` approved ${path45.relative(process.cwd(), result3.output)} by ${args.reviewer.trim()}`
|
|
18681
19417
|
);
|
|
18682
19418
|
}
|
|
18683
19419
|
return;
|
|
@@ -18686,7 +19422,7 @@ async function runRefine(args) {
|
|
|
18686
19422
|
if (args.json) {
|
|
18687
19423
|
log(JSON.stringify(result2, null, 2));
|
|
18688
19424
|
} else {
|
|
18689
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
19425
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path45.relative(process.cwd(), result2.output || result2.record)}`);
|
|
18690
19426
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
18691
19427
|
}
|
|
18692
19428
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -18732,7 +19468,7 @@ async function runRefine(args) {
|
|
|
18732
19468
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
18733
19469
|
if (item.state === "needs-approval") {
|
|
18734
19470
|
log(
|
|
18735
|
-
` pixelkiln refine approve --from ${
|
|
19471
|
+
` pixelkiln refine approve --from ${path45.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
18736
19472
|
);
|
|
18737
19473
|
}
|
|
18738
19474
|
}
|
|
@@ -18784,15 +19520,15 @@ async function runTools(args) {
|
|
|
18784
19520
|
}
|
|
18785
19521
|
|
|
18786
19522
|
// src/cli/commands/workspace.ts
|
|
18787
|
-
import
|
|
18788
|
-
import { existsSync as
|
|
19523
|
+
import path46 from "path";
|
|
19524
|
+
import { existsSync as existsSync32 } from "fs";
|
|
18789
19525
|
async function runWorkspace(args) {
|
|
18790
|
-
const workspacePath =
|
|
18791
|
-
const dir =
|
|
19526
|
+
const workspacePath = path46.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
19527
|
+
const dir = path46.dirname(workspacePath);
|
|
18792
19528
|
if (args.subcommand === "add") {
|
|
18793
|
-
const manifestPath =
|
|
19529
|
+
const manifestPath = path46.resolve(args.target);
|
|
18794
19530
|
const loadedTarget = await loadManifest(manifestPath);
|
|
18795
|
-
const lockPath = args.explicitLock ?
|
|
19531
|
+
const lockPath = args.explicitLock ? path46.resolve(args.explicitLock) : path46.join(path46.dirname(manifestPath), "pixelkiln.lock.json");
|
|
18796
19532
|
const ws = await loadWorkspace(workspacePath);
|
|
18797
19533
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
18798
19534
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -18812,10 +19548,10 @@ async function runWorkspace(args) {
|
|
|
18812
19548
|
...args.account ? { account: args.account } : {}
|
|
18813
19549
|
};
|
|
18814
19550
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
18815
|
-
log(` registered "${id}" in ${
|
|
19551
|
+
log(` registered "${id}" in ${path46.relative(process.cwd(), workspacePath)}`);
|
|
18816
19552
|
log(` manifest: ${project.manifest}`);
|
|
18817
19553
|
log(` lock: ${project.lock}`);
|
|
18818
|
-
if (!
|
|
19554
|
+
if (!existsSync32(lockPath)) {
|
|
18819
19555
|
log(
|
|
18820
19556
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
18821
19557
|
);
|
|
@@ -18824,7 +19560,7 @@ async function runWorkspace(args) {
|
|
|
18824
19560
|
}
|
|
18825
19561
|
if (args.subcommand === "remove") {
|
|
18826
19562
|
const ws = await loadWorkspace(workspacePath);
|
|
18827
|
-
const resolvedTarget =
|
|
19563
|
+
const resolvedTarget = path46.resolve(args.target);
|
|
18828
19564
|
const match = ws.projects.find(
|
|
18829
19565
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
18830
19566
|
);
|
|
@@ -18835,11 +19571,11 @@ async function runWorkspace(args) {
|
|
|
18835
19571
|
version: 1,
|
|
18836
19572
|
projects: ws.projects.filter((p) => p !== match)
|
|
18837
19573
|
});
|
|
18838
|
-
log(` removed "${match.id}" from ${
|
|
19574
|
+
log(` removed "${match.id}" from ${path46.relative(process.cwd(), workspacePath)}`);
|
|
18839
19575
|
return;
|
|
18840
19576
|
}
|
|
18841
19577
|
if (args.subcommand === "list") {
|
|
18842
|
-
if (!
|
|
19578
|
+
if (!existsSync32(workspacePath)) {
|
|
18843
19579
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
18844
19580
|
}
|
|
18845
19581
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -18847,9 +19583,9 @@ async function runWorkspace(args) {
|
|
|
18847
19583
|
if (args.json) {
|
|
18848
19584
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
18849
19585
|
} else if (!ws.projects.length) {
|
|
18850
|
-
log(` no projects registered in ${
|
|
19586
|
+
log(` no projects registered in ${path46.relative(process.cwd(), workspacePath)}`);
|
|
18851
19587
|
} else {
|
|
18852
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
19588
|
+
log(` ${ws.projects.length} project(s) in ${path46.relative(process.cwd(), workspacePath)}:`);
|
|
18853
19589
|
for (const p of ws.projects) {
|
|
18854
19590
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
18855
19591
|
}
|
|
@@ -18859,7 +19595,7 @@ async function runWorkspace(args) {
|
|
|
18859
19595
|
return;
|
|
18860
19596
|
}
|
|
18861
19597
|
if (args.subcommand === "status") {
|
|
18862
|
-
if (!
|
|
19598
|
+
if (!existsSync32(workspacePath)) {
|
|
18863
19599
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
18864
19600
|
}
|
|
18865
19601
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -18867,7 +19603,7 @@ async function runWorkspace(args) {
|
|
|
18867
19603
|
if (args.json) {
|
|
18868
19604
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
18869
19605
|
} else {
|
|
18870
|
-
log(` workspace: ${
|
|
19606
|
+
log(` workspace: ${path46.relative(process.cwd(), workspacePath)}`);
|
|
18871
19607
|
for (const p of report.projects) {
|
|
18872
19608
|
if (p.error) {
|
|
18873
19609
|
log(`
|
|
@@ -18921,8 +19657,8 @@ async function runHelp() {
|
|
|
18921
19657
|
async function runVersion() {
|
|
18922
19658
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
18923
19659
|
const url = new URL(up, import.meta.url);
|
|
18924
|
-
if (!
|
|
18925
|
-
const pkg = JSON.parse(await
|
|
19660
|
+
if (!existsSync33(url)) continue;
|
|
19661
|
+
const pkg = JSON.parse(await readFile30(url, "utf8"));
|
|
18926
19662
|
if (pkg.name === "pixelkiln") {
|
|
18927
19663
|
log(`${pkg.name} ${pkg.version}`);
|
|
18928
19664
|
return;
|
|
@@ -18965,7 +19701,9 @@ var RUNNERS = {
|
|
|
18965
19701
|
quality: runQuality,
|
|
18966
19702
|
refine: runRefine,
|
|
18967
19703
|
recipe: runRecipe,
|
|
18968
|
-
workspace: runWorkspace
|
|
19704
|
+
workspace: runWorkspace,
|
|
19705
|
+
unzoom: runUnzoom,
|
|
19706
|
+
font: runFont
|
|
18969
19707
|
};
|
|
18970
19708
|
async function main(argv = process.argv.slice(2)) {
|
|
18971
19709
|
const args = parseArgs(argv);
|