pixelkiln 0.61.0 → 0.62.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 +820 -202
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +904 -356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +444 -164
- package/dist/index.d.ts +444 -164
- package/dist/index.js +867 -330
- package/dist/index.js.map +1 -1
- package/docs/CLI.md +63 -3
- package/docs/ENDPOINTS.md +32 -6
- package/docs/GENERATORS.md +45 -0
- package/docs/MANIFEST.md +6 -6
- package/docs/PIXELLAB.md +6 -3
- package/docs/REVISIONS.md +128 -19
- package/package.json +1 -1
- package/schema/manifest.schema.json +7 -3
- package/schema/recipe.schema.json +2 -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;
|
|
@@ -1022,9 +1044,13 @@ var RevisionSchema = z.object({
|
|
|
1022
1044
|
* the provider layer, since it differs by mode).
|
|
1023
1045
|
*/
|
|
1024
1046
|
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. */
|
|
1047
|
+
/** `animate`/`animate-pixminimax`/`interpolate`/`edit-animation` only: playback rate recorded with the frames; PixelLab does not store one. */
|
|
1026
1048
|
fps: z.number().int().min(1).max(60).optional(),
|
|
1027
|
-
/**
|
|
1049
|
+
/**
|
|
1050
|
+
* `animate`/`animate-pixminimax`: manifest-relative image pinning where
|
|
1051
|
+
* the motion ends (interpolation instead of open-ended animation).
|
|
1052
|
+
* `interpolate`: required, the ending keyframe; the parent is the start.
|
|
1053
|
+
*/
|
|
1028
1054
|
lastFrame: z.string().min(1).optional(),
|
|
1029
1055
|
/** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
|
|
1030
1056
|
direction: CharacterDirectionSchema.optional(),
|
|
@@ -1068,14 +1094,15 @@ var RevisionSchema = z.object({
|
|
|
1068
1094
|
path: ["strength"]
|
|
1069
1095
|
});
|
|
1070
1096
|
}
|
|
1071
|
-
|
|
1097
|
+
const generative = revision.mode === "animate" || revision.mode === "animate-pixminimax" || revision.mode === "interpolate" || revision.mode === "edit-animation";
|
|
1098
|
+
if (revision.strength !== void 0 && generative) {
|
|
1072
1099
|
context.addIssue({
|
|
1073
1100
|
code: z.ZodIssueCode.custom,
|
|
1074
|
-
message: `${revision.mode} revisions do not take a strength; use enhancePrompt`,
|
|
1101
|
+
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
1102
|
path: ["strength"]
|
|
1076
1103
|
});
|
|
1077
1104
|
}
|
|
1078
|
-
for (const field of ["frames", "
|
|
1105
|
+
for (const field of ["frames", "enhancePrompt"]) {
|
|
1079
1106
|
if (revision[field] !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
|
|
1080
1107
|
context.addIssue({
|
|
1081
1108
|
code: z.ZodIssueCode.custom,
|
|
@@ -1084,6 +1111,27 @@ var RevisionSchema = z.object({
|
|
|
1084
1111
|
});
|
|
1085
1112
|
}
|
|
1086
1113
|
}
|
|
1114
|
+
if (revision.lastFrame !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax" && revision.mode !== "interpolate") {
|
|
1115
|
+
context.addIssue({
|
|
1116
|
+
code: z.ZodIssueCode.custom,
|
|
1117
|
+
message: "lastFrame applies to animate/animate-pixminimax/interpolate revisions only",
|
|
1118
|
+
path: ["lastFrame"]
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
if (revision.mode === "interpolate" && revision.lastFrame === void 0) {
|
|
1122
|
+
context.addIssue({
|
|
1123
|
+
code: z.ZodIssueCode.custom,
|
|
1124
|
+
message: "interpolate revisions require lastFrame: the ending keyframe to interpolate toward",
|
|
1125
|
+
path: ["lastFrame"]
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
if (revision.fps !== void 0 && !generative) {
|
|
1129
|
+
context.addIssue({
|
|
1130
|
+
code: z.ZodIssueCode.custom,
|
|
1131
|
+
message: "fps applies to animate/animate-pixminimax/interpolate/edit-animation revisions only",
|
|
1132
|
+
path: ["fps"]
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1087
1135
|
if (revision.direction !== void 0 && revision.mode !== "animate-pixminimax") {
|
|
1088
1136
|
context.addIssue({
|
|
1089
1137
|
code: z.ZodIssueCode.custom,
|
|
@@ -1095,6 +1143,8 @@ var RevisionSchema = z.object({
|
|
|
1095
1143
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
|
|
1096
1144
|
}
|
|
1097
1145
|
});
|
|
1146
|
+
var FRAME_SET_REVISION_MODES = ["animate", "animate-pixminimax", "interpolate", "edit-animation"];
|
|
1147
|
+
var MEMBER_SET_REVISION_MODES = ["reduce-colors", "correct-pixelart", "edit-animation"];
|
|
1098
1148
|
var StyleObjectSchema = z.object({
|
|
1099
1149
|
/** Generation backend for this style. Omit to inherit the manifest default. */
|
|
1100
1150
|
provider: z.string().min(1).optional(),
|
|
@@ -1213,7 +1263,7 @@ var StyleObjectSchema = z.object({
|
|
|
1213
1263
|
*/
|
|
1214
1264
|
terrainMode: z.enum(["standard", "pro"]).optional(),
|
|
1215
1265
|
/**
|
|
1216
|
-
* `uiAsset`
|
|
1266
|
+
* `uiAsset` and `uiElement` generators only. A natural-language palette hint sent as-is
|
|
1217
1267
|
* (e.g. "brown and gold"), distinct from `palette`'s hex-color array —
|
|
1218
1268
|
* this is a prompt-level steer, not a local quantization target.
|
|
1219
1269
|
*/
|
|
@@ -4087,11 +4137,16 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
4087
4137
|
reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
|
|
4088
4138
|
};
|
|
4089
4139
|
}
|
|
4090
|
-
if (
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4140
|
+
if (revision.sourceMembers) {
|
|
4141
|
+
const members = await inspectSourceMembers(spec, lock);
|
|
4142
|
+
if (!members.ready) return members;
|
|
4143
|
+
} else {
|
|
4144
|
+
if (!revision.sourceSha256 || !existsSync4(revision.sourceFile)) {
|
|
4145
|
+
return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
|
|
4146
|
+
}
|
|
4147
|
+
if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
|
|
4148
|
+
return { ready: false, reason: "revision source changed after the manifest was resolved" };
|
|
4149
|
+
}
|
|
4095
4150
|
}
|
|
4096
4151
|
if (revision.maskFile) {
|
|
4097
4152
|
if (!revision.maskSha256 || !existsSync4(revision.maskFile)) {
|
|
@@ -4101,8 +4156,39 @@ async function inspectRevision(spec, lock, seen) {
|
|
|
4101
4156
|
return { ready: false, reason: "revision mask changed after the manifest was resolved" };
|
|
4102
4157
|
}
|
|
4103
4158
|
}
|
|
4159
|
+
if (revision.lastFrameFile) {
|
|
4160
|
+
if (!revision.lastFrameSha256 || !existsSync4(revision.lastFrameFile)) {
|
|
4161
|
+
return { ready: false, reason: `revision last frame is missing: ${revision.lastFrameFile}` };
|
|
4162
|
+
}
|
|
4163
|
+
if (await sha256File(revision.lastFrameFile) !== revision.lastFrameSha256) {
|
|
4164
|
+
return { ready: false, reason: "revision last frame changed after the manifest was resolved" };
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4104
4167
|
return { ready: true, reason: "revision inputs are current" };
|
|
4105
4168
|
}
|
|
4169
|
+
async function inspectSourceMembers(spec, lock) {
|
|
4170
|
+
const revision = spec.revision;
|
|
4171
|
+
const members = revision.sourceMembers;
|
|
4172
|
+
for (const member of members) {
|
|
4173
|
+
if (!existsSync4(member.file)) return { ready: false, reason: `revision source member is missing: ${member.file}` };
|
|
4174
|
+
if (await sha256File(member.file) !== member.sha256) {
|
|
4175
|
+
return { ready: false, reason: `revision source member changed after the manifest was resolved: ${member.file}` };
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
const parent = revision.sourceSpec;
|
|
4179
|
+
if (parent.source) return { ready: true, reason: "declared source set is present" };
|
|
4180
|
+
const entry = lock.entries[lockKey(parent.styleId, parent.assetId)];
|
|
4181
|
+
if (!entry) return { ready: false, reason: "revision source is not in the lockfile" };
|
|
4182
|
+
const recorded = entry.outputs.map((_, index) => path8.resolve(currentEntryOutputPath(entry, parent, index))).sort();
|
|
4183
|
+
const found = members.map((member) => path8.resolve(member.file)).sort();
|
|
4184
|
+
if (recorded.length !== found.length || recorded.some((file, index) => file !== found[index])) {
|
|
4185
|
+
return {
|
|
4186
|
+
ready: false,
|
|
4187
|
+
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`
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
return { ready: true, reason: "revision source set is current" };
|
|
4191
|
+
}
|
|
4106
4192
|
async function requireRevisionReady(spec, lock) {
|
|
4107
4193
|
const inspection = await inspectRevisionReadiness(spec, lock);
|
|
4108
4194
|
if (inspection && !inspection.ready) throw new Error(inspection.reason);
|
|
@@ -4808,14 +4894,15 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4808
4894
|
}
|
|
4809
4895
|
/**
|
|
4810
4896
|
* Everything else resolves generically to whatever the user's own
|
|
4811
|
-
* workflow does with `bindings.sourceImage`.
|
|
4812
|
-
*
|
|
4813
|
-
*
|
|
4814
|
-
*
|
|
4815
|
-
*
|
|
4897
|
+
* workflow does with `bindings.sourceImage`. The frame-set modes
|
|
4898
|
+
* (`animate`, `animate-pixminimax`, `interpolate`, `edit-animation`) are
|
|
4899
|
+
* the exception: they produce an ordered frame set, and this adapter's
|
|
4900
|
+
* revision path always writes a single output image (see `submit`/`fetch`
|
|
4901
|
+
* below) — a real structural gap, not a missing binding, so they are
|
|
4902
|
+
* rejected here rather than only failing once `validate` runs.
|
|
4816
4903
|
*/
|
|
4817
4904
|
supportsRevision(mode2) {
|
|
4818
|
-
return mode2
|
|
4905
|
+
return !FRAME_SET_REVISION_MODES.includes(mode2);
|
|
4819
4906
|
}
|
|
4820
4907
|
estimate(spec) {
|
|
4821
4908
|
return {
|
|
@@ -4868,6 +4955,11 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
4868
4955
|
throw new Error("ComfyUI providerOptions.frames requires generator: frames");
|
|
4869
4956
|
}
|
|
4870
4957
|
if (spec.revision) {
|
|
4958
|
+
if (spec.revision.sourceMembers) {
|
|
4959
|
+
throw new Error(
|
|
4960
|
+
`ComfyUI revisions read one source image; ${spec.revision.sourceAssetId} is a ${spec.revision.sourceMembers.length}-member set (directions or frames)`
|
|
4961
|
+
);
|
|
4962
|
+
}
|
|
4871
4963
|
if (!options.bindings.sourceImage) {
|
|
4872
4964
|
throw new Error("ComfyUI revisions require bindings.sourceImage");
|
|
4873
4965
|
}
|
|
@@ -5603,6 +5695,21 @@ var CorrectPixelartResponseSchema = z2.object({
|
|
|
5603
5695
|
images: z2.array(z2.object({ base64: z2.string().min(1) }).passthrough()).min(1),
|
|
5604
5696
|
usage: z2.unknown().optional()
|
|
5605
5697
|
}).passthrough();
|
|
5698
|
+
var UnzoomResponseSchema = z2.object({
|
|
5699
|
+
image: z2.object({ base64: z2.string().min(1) }).passthrough(),
|
|
5700
|
+
original_size: z2.object({ width: z2.number().int(), height: z2.number().int() }).passthrough(),
|
|
5701
|
+
unzoomed_size: z2.object({ width: z2.number().int(), height: z2.number().int() }).passthrough(),
|
|
5702
|
+
zoom_factor_detected: z2.number(),
|
|
5703
|
+
usage: z2.unknown().optional()
|
|
5704
|
+
}).passthrough();
|
|
5705
|
+
var FontJobSchema = z2.object({
|
|
5706
|
+
job_id: z2.string().min(1),
|
|
5707
|
+
status: z2.string(),
|
|
5708
|
+
glyph_px: z2.number().int().nullable().optional(),
|
|
5709
|
+
download_atlas_url: z2.string().nullable().optional(),
|
|
5710
|
+
download_ttf_url: z2.string().nullable().optional(),
|
|
5711
|
+
usage: z2.unknown().optional()
|
|
5712
|
+
}).passthrough();
|
|
5606
5713
|
var SelectFramesSchema = z2.object({ created_object_ids: z2.array(z2.string()) }).passthrough();
|
|
5607
5714
|
var CharacterSubmitSchema = z2.object({
|
|
5608
5715
|
background_job_id: z2.string().min(1),
|
|
@@ -5687,6 +5794,17 @@ function validateResponse(schema, raw, operation) {
|
|
|
5687
5794
|
const issues = parsed.error.issues.slice(0, 4).map((i) => `${i.path.join(".") || "response"}: ${i.message}`).join("; ");
|
|
5688
5795
|
throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
|
|
5689
5796
|
}
|
|
5797
|
+
function cleanupImages(args, operation) {
|
|
5798
|
+
const images = args.images ?? (args.image ? [args.image] : []);
|
|
5799
|
+
if (!images.length) throw new Error(`${operation} needs at least one image`);
|
|
5800
|
+
return images;
|
|
5801
|
+
}
|
|
5802
|
+
function cleanupResults(images, sent, operation) {
|
|
5803
|
+
if (images.length !== sent) {
|
|
5804
|
+
throw new Error(`Invalid PixelLab response for ${operation}: sent ${sent} image(s), got ${images.length} back`);
|
|
5805
|
+
}
|
|
5806
|
+
return images.map((image) => Buffer.from(image.base64, "base64"));
|
|
5807
|
+
}
|
|
5690
5808
|
var PixelLabError = class extends ProviderError {
|
|
5691
5809
|
constructor(message6, status, body) {
|
|
5692
5810
|
super("pixellab", message6, { status });
|
|
@@ -5715,9 +5833,9 @@ var PixelLabClient = class {
|
|
|
5715
5833
|
apiKey;
|
|
5716
5834
|
http;
|
|
5717
5835
|
/** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
|
|
5718
|
-
async request(
|
|
5836
|
+
async request(path47, init) {
|
|
5719
5837
|
const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
|
|
5720
|
-
const res = await this.http(`${BASE}${
|
|
5838
|
+
const res = await this.http(`${BASE}${path47}`, {
|
|
5721
5839
|
...init,
|
|
5722
5840
|
headers: {
|
|
5723
5841
|
Authorization: auth,
|
|
@@ -5727,13 +5845,13 @@ var PixelLabClient = class {
|
|
|
5727
5845
|
});
|
|
5728
5846
|
const text = await res.text();
|
|
5729
5847
|
if (!res.ok) {
|
|
5730
|
-
throw new PixelLabError(`${init?.method ?? "GET"} ${
|
|
5848
|
+
throw new PixelLabError(`${init?.method ?? "GET"} ${path47} \u2192 ${res.status}`, res.status, text);
|
|
5731
5849
|
}
|
|
5732
5850
|
if (!text) return {};
|
|
5733
5851
|
try {
|
|
5734
5852
|
return JSON.parse(text);
|
|
5735
5853
|
} catch {
|
|
5736
|
-
throw new Error(`${init?.method ?? "GET"} ${
|
|
5854
|
+
throw new Error(`${init?.method ?? "GET"} ${path47} returned invalid JSON`);
|
|
5737
5855
|
}
|
|
5738
5856
|
}
|
|
5739
5857
|
async balance() {
|
|
@@ -6119,10 +6237,10 @@ var PixelLabClient = class {
|
|
|
6119
6237
|
const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
|
|
6120
6238
|
const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
|
|
6121
6239
|
const south = args.reference?.south;
|
|
6122
|
-
let
|
|
6240
|
+
let path47;
|
|
6123
6241
|
let body;
|
|
6124
6242
|
if (args.mode === "v3") {
|
|
6125
|
-
|
|
6243
|
+
path47 = "/create-character-v3";
|
|
6126
6244
|
body = {
|
|
6127
6245
|
description: args.description,
|
|
6128
6246
|
image_size: imageSize,
|
|
@@ -6136,7 +6254,7 @@ var PixelLabClient = class {
|
|
|
6136
6254
|
...args.enhancePrompt ? { enhance_prompt: true } : {}
|
|
6137
6255
|
};
|
|
6138
6256
|
} else if (args.mode === "pro-flash") {
|
|
6139
|
-
|
|
6257
|
+
path47 = "/create-character-pro-flash";
|
|
6140
6258
|
const traits = args.styleTraits;
|
|
6141
6259
|
body = {
|
|
6142
6260
|
description: args.description,
|
|
@@ -6159,7 +6277,7 @@ var PixelLabClient = class {
|
|
|
6159
6277
|
} : {}
|
|
6160
6278
|
};
|
|
6161
6279
|
} else if (args.mode === "pro") {
|
|
6162
|
-
|
|
6280
|
+
path47 = "/create-character-pro";
|
|
6163
6281
|
body = {
|
|
6164
6282
|
description: args.description,
|
|
6165
6283
|
image_size: imageSize,
|
|
@@ -6173,7 +6291,7 @@ var PixelLabClient = class {
|
|
|
6173
6291
|
...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
|
|
6174
6292
|
};
|
|
6175
6293
|
} else {
|
|
6176
|
-
|
|
6294
|
+
path47 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
|
|
6177
6295
|
body = {
|
|
6178
6296
|
description: args.description,
|
|
6179
6297
|
image_size: imageSize,
|
|
@@ -6190,8 +6308,8 @@ var PixelLabClient = class {
|
|
|
6190
6308
|
...palette
|
|
6191
6309
|
};
|
|
6192
6310
|
}
|
|
6193
|
-
const raw = await this.request(
|
|
6194
|
-
return validateResponse(CharacterSubmitSchema, raw,
|
|
6311
|
+
const raw = await this.request(path47, { method: "POST", body: JSON.stringify(body) });
|
|
6312
|
+
return validateResponse(CharacterSubmitSchema, raw, path47.slice(1));
|
|
6195
6313
|
}
|
|
6196
6314
|
/** A text edit of an existing character, applied to every direction and stored as a sibling. */
|
|
6197
6315
|
async createCharacterState(args) {
|
|
@@ -6353,20 +6471,52 @@ var PixelLabClient = class {
|
|
|
6353
6471
|
);
|
|
6354
6472
|
}
|
|
6355
6473
|
/**
|
|
6356
|
-
* `/
|
|
6357
|
-
*
|
|
6358
|
-
*
|
|
6359
|
-
*
|
|
6360
|
-
*
|
|
6361
|
-
*
|
|
6362
|
-
*
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6474
|
+
* `/generate-ui-v2` ("Generate UI (Pro)"): one UI element (a button, a
|
|
6475
|
+
* health bar, an inventory slot, a dialogue box) from a description,
|
|
6476
|
+
* guided by an optional concept image and a natural-language color
|
|
6477
|
+
* palette. A plain background job; unlike `/create-ui-asset` there is no
|
|
6478
|
+
* piece/element layout, and sizes start at 16px instead of 192. Its
|
|
6479
|
+
* completed shape has not been exercised live; `pollUiElement` reads it
|
|
6480
|
+
* defensively.
|
|
6481
|
+
*/
|
|
6482
|
+
async generateUiV2(args) {
|
|
6483
|
+
const body = {
|
|
6484
|
+
description: args.description,
|
|
6485
|
+
image_size: { width: args.width, height: args.height }
|
|
6486
|
+
};
|
|
6487
|
+
if (args.conceptImage) {
|
|
6488
|
+
body.concept_image = {
|
|
6489
|
+
image: args.conceptImage.image,
|
|
6490
|
+
size: { width: args.conceptImage.width, height: args.conceptImage.height }
|
|
6491
|
+
};
|
|
6492
|
+
}
|
|
6493
|
+
if (args.colorPalette) body.color_palette = args.colorPalette;
|
|
6494
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
6495
|
+
if (args.seed != null) body.seed = args.seed;
|
|
6496
|
+
return validateResponse(
|
|
6497
|
+
RevisionJobSubmitSchema,
|
|
6498
|
+
await this.request("/generate-ui-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
6499
|
+
"generate-ui-v2"
|
|
6500
|
+
);
|
|
6501
|
+
}
|
|
6502
|
+
/**
|
|
6503
|
+
* `/reduce-colors`, PixelLab's "Cleanup" tier: quantize one image, or
|
|
6504
|
+
* several same-size frames together onto ONE shared palette, synchronously
|
|
6505
|
+
* — no `background_job_id`, the result comes back in this same response,
|
|
6506
|
+
* like `createImagePixflux`. Passing every frame of an animation (or every
|
|
6507
|
+
* direction of a character) in one call is the endpoint's whole point:
|
|
6508
|
+
* frames quantized separately drift onto different palettes. The schema's
|
|
6509
|
+
* own response example is dollar-denominated (`usage: {type: "usd", usd:
|
|
6510
|
+
* 0.02}`), which — matching this codebase's repeated experience with
|
|
6511
|
+
* PixelLab's documented-vs-billed cost mismatches (`isometricTile`,
|
|
6512
|
+
* `objectPro`) — did not hold: confirmed live against a Tier 2 account at
|
|
6513
|
+
* exactly 0.1 generations for a 32x32 source (docs/REVISIONS.md).
|
|
6514
|
+
* `numColors` and `paletteImage` are mutually exclusive upstream; the
|
|
6515
|
+
* manifest schema already enforces that before this is ever called.
|
|
6367
6516
|
*/
|
|
6368
6517
|
async reduceColors(args) {
|
|
6369
|
-
const
|
|
6518
|
+
const images = cleanupImages(args, "reduce-colors");
|
|
6519
|
+
const body = { images };
|
|
6370
6520
|
if (args.numColors != null) body.num_colors = args.numColors;
|
|
6371
6521
|
if (args.paletteImage) body.palette_image = args.paletteImage;
|
|
6372
6522
|
if (args.dithering) body.dithering = args.dithering;
|
|
@@ -6376,8 +6526,10 @@ var PixelLabClient = class {
|
|
|
6376
6526
|
await this.request("/reduce-colors", { method: "POST", body: JSON.stringify(body) }),
|
|
6377
6527
|
"reduce-colors"
|
|
6378
6528
|
);
|
|
6529
|
+
const pngs = cleanupResults(res.images, images.length, "reduce-colors");
|
|
6379
6530
|
return {
|
|
6380
|
-
png:
|
|
6531
|
+
png: pngs[0],
|
|
6532
|
+
pngs,
|
|
6381
6533
|
paletteStripPng: Buffer.from(res.palette.base64, "base64"),
|
|
6382
6534
|
nColors: res.n_colors,
|
|
6383
6535
|
usage: res.usage
|
|
@@ -6386,19 +6538,117 @@ var PixelLabClient = class {
|
|
|
6386
6538
|
/**
|
|
6387
6539
|
* `/correct-pixelart`, PixelLab's "Cleanup" tier: sharpen edges and drop
|
|
6388
6540
|
* stray pixels without resizing, synchronously — same shape as
|
|
6389
|
-
* `reduceColors` above, no background job
|
|
6390
|
-
*
|
|
6541
|
+
* `reduceColors` above, no background job, and likewise corrects several
|
|
6542
|
+
* same-size frames together when given `images`. Cost is confirmed live
|
|
6543
|
+
* at 0.1 generations for a 32x32 source, not the schema's own
|
|
6391
6544
|
* dollar-denominated example (`usage: {type: "usd", usd: 0.02}`).
|
|
6392
6545
|
*/
|
|
6393
6546
|
async correctPixelart(args) {
|
|
6394
|
-
const
|
|
6547
|
+
const images = cleanupImages(args, "correct-pixelart");
|
|
6548
|
+
const body = { images };
|
|
6395
6549
|
if (args.strength != null) body.strength = args.strength;
|
|
6396
6550
|
const res = validateResponse(
|
|
6397
6551
|
CorrectPixelartResponseSchema,
|
|
6398
6552
|
await this.request("/correct-pixelart", { method: "POST", body: JSON.stringify(body) }),
|
|
6399
6553
|
"correct-pixelart"
|
|
6400
6554
|
);
|
|
6401
|
-
|
|
6555
|
+
const pngs = cleanupResults(res.images, images.length, "correct-pixelart");
|
|
6556
|
+
return { png: pngs[0], pngs, usage: res.usage };
|
|
6557
|
+
}
|
|
6558
|
+
/**
|
|
6559
|
+
* `/unzoom`: recover native-resolution pixel art from an upscaled image (a
|
|
6560
|
+
* 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
|
|
6561
|
+
* PixelLab's own API overview names this "the most common cause of
|
|
6562
|
+
* disappointing output": run it on outside artwork before it becomes a
|
|
6563
|
+
* style or reference image. The input must be at least 256x256 and at
|
|
6564
|
+
* most 2048x2048 in area, and the result is opaque — transparency is
|
|
6565
|
+
* composited onto white before the grid is detected. `quantize` is 0 to
|
|
6566
|
+
* auto-detect a palette, -1 to keep every color, or 2-256 for exactly
|
|
6567
|
+
* that many.
|
|
6568
|
+
*/
|
|
6569
|
+
async unzoom(args) {
|
|
6570
|
+
const body = { image: args.image };
|
|
6571
|
+
if (args.quantize != null) body.quantize = args.quantize;
|
|
6572
|
+
const res = validateResponse(
|
|
6573
|
+
UnzoomResponseSchema,
|
|
6574
|
+
await this.request("/unzoom", { method: "POST", body: JSON.stringify(body) }),
|
|
6575
|
+
"unzoom"
|
|
6576
|
+
);
|
|
6577
|
+
return {
|
|
6578
|
+
png: Buffer.from(res.image.base64, "base64"),
|
|
6579
|
+
originalSize: { width: res.original_size.width, height: res.original_size.height },
|
|
6580
|
+
unzoomedSize: { width: res.unzoomed_size.width, height: res.unzoomed_size.height },
|
|
6581
|
+
zoomFactor: res.zoom_factor_detected,
|
|
6582
|
+
usage: res.usage
|
|
6583
|
+
};
|
|
6584
|
+
}
|
|
6585
|
+
/**
|
|
6586
|
+
* `/interpolation-v2` ("Interpolate (Pro)"): generate the in-between
|
|
6587
|
+
* frames from one keyframe to another, guided by a short `action`. Unlike
|
|
6588
|
+
* `last_frame` on `/animate-with-text-v3`, both ends are required and the
|
|
6589
|
+
* frame count is the model's own choice (its docs say "typically 4-8").
|
|
6590
|
+
* Output frames are 16 to 128 pixels per side. A plain background job;
|
|
6591
|
+
* its completed shape is read defensively by `pollAnimateRevision`.
|
|
6592
|
+
*/
|
|
6593
|
+
async interpolationV2(args) {
|
|
6594
|
+
const size = { width: args.width, height: args.height };
|
|
6595
|
+
const body = {
|
|
6596
|
+
start_image: { image: args.startImage, size },
|
|
6597
|
+
end_image: { image: args.endImage, size },
|
|
6598
|
+
action: args.action,
|
|
6599
|
+
image_size: size
|
|
6600
|
+
};
|
|
6601
|
+
if (args.seed != null) body.seed = args.seed;
|
|
6602
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
6603
|
+
return validateResponse(
|
|
6604
|
+
RevisionJobSubmitSchema,
|
|
6605
|
+
await this.request("/interpolation-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
6606
|
+
"interpolation-v2"
|
|
6607
|
+
);
|
|
6608
|
+
}
|
|
6609
|
+
/**
|
|
6610
|
+
* `/edit-animation-v2` ("Edit animation (Pro)"): apply one text edit
|
|
6611
|
+
* across every frame of an existing animation at once, so the change
|
|
6612
|
+
* stays consistent frame to frame. 2 to 16 frames, but the real ceiling
|
|
6613
|
+
* shrinks with frame size because every frame is packed into one grid:
|
|
6614
|
+
* 16 at up to 64px, 9 at 65-80px, 4 at 81-256px.
|
|
6615
|
+
*/
|
|
6616
|
+
async editAnimationV2(args) {
|
|
6617
|
+
const size = { width: args.width, height: args.height };
|
|
6618
|
+
const body = {
|
|
6619
|
+
description: args.description,
|
|
6620
|
+
frames: args.frames.map((image) => ({ image, size })),
|
|
6621
|
+
image_size: size
|
|
6622
|
+
};
|
|
6623
|
+
if (args.seed != null) body.seed = args.seed;
|
|
6624
|
+
if (args.noBackground != null) body.no_background = args.noBackground;
|
|
6625
|
+
return validateResponse(
|
|
6626
|
+
RevisionJobSubmitSchema,
|
|
6627
|
+
await this.request("/edit-animation-v2", { method: "POST", body: JSON.stringify(body) }),
|
|
6628
|
+
"edit-animation-v2"
|
|
6629
|
+
);
|
|
6630
|
+
}
|
|
6631
|
+
/**
|
|
6632
|
+
* `/generate-font-pro`: an 80-glyph pixel font (A-Z, a-z, 0-9, common
|
|
6633
|
+
* game-UI punctuation) from a style description, delivered as a glyph
|
|
6634
|
+
* atlas PNG plus a ready-to-use `.ttf`. Documented at 25 subscription
|
|
6635
|
+
* generations. Poll with `getFontJob`.
|
|
6636
|
+
*/
|
|
6637
|
+
async generateFontPro(args) {
|
|
6638
|
+
const body = { description: args.description, weight: args.weight };
|
|
6639
|
+
if (args.glyphPx != null) body.glyph_px = args.glyphPx;
|
|
6640
|
+
if (args.seed != null) body.seed = args.seed;
|
|
6641
|
+
if (args.fontName) body.font_name = args.fontName;
|
|
6642
|
+
return validateResponse(
|
|
6643
|
+
RevisionJobSubmitSchema,
|
|
6644
|
+
await this.request("/generate-font-pro", { method: "POST", body: JSON.stringify(body) }),
|
|
6645
|
+
"generate-font-pro"
|
|
6646
|
+
);
|
|
6647
|
+
}
|
|
6648
|
+
/** `GET /generate-font-pro/{job_id}`: status, then no-auth atlas and `.ttf` URLs once completed. */
|
|
6649
|
+
async getFontJob(jobId) {
|
|
6650
|
+
const raw = await this.request(`/generate-font-pro/${encodeURIComponent(jobId)}`);
|
|
6651
|
+
return validateResponse(FontJobSchema, raw, "generate-font-pro/{job_id}");
|
|
6402
6652
|
}
|
|
6403
6653
|
/**
|
|
6404
6654
|
* `/animate-with-text-v3`: animate a loose image from a text description,
|
|
@@ -6619,7 +6869,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6619
6869
|
return new _PixelLabProvider(new PixelLabClient("download-only"));
|
|
6620
6870
|
}
|
|
6621
6871
|
supports(generator) {
|
|
6622
|
-
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset";
|
|
6872
|
+
return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro" || generator === "uiAsset" || generator === "uiElement";
|
|
6623
6873
|
}
|
|
6624
6874
|
/**
|
|
6625
6875
|
* `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
|
|
@@ -6629,12 +6879,15 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6629
6879
|
* rather than a described edit — and `animate`/`animate-pixminimax`
|
|
6630
6880
|
* (`/animate-with-text-v3`, `/animate-pixminimax`), which animate any loose
|
|
6631
6881
|
* image from a text description with no character/object resource
|
|
6632
|
-
* required
|
|
6633
|
-
*
|
|
6634
|
-
*
|
|
6882
|
+
* required — and `interpolate` (`/interpolation-v2`, the in-betweens
|
|
6883
|
+
* from one keyframe to another) and `edit-animation`
|
|
6884
|
+
* (`/edit-animation-v2`, one text edit applied across a whole frame set).
|
|
6885
|
+
* `outpaint` does not: there is no canvas-expansion endpoint in the API,
|
|
6886
|
+
* matching docs/REVISIONS.md's note that no provider ships a tested
|
|
6887
|
+
* outpaint path yet.
|
|
6635
6888
|
*/
|
|
6636
6889
|
supportsRevision(mode2) {
|
|
6637
|
-
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax";
|
|
6890
|
+
return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "interpolate" || mode2 === "edit-animation";
|
|
6638
6891
|
}
|
|
6639
6892
|
/** PixelLab's own constraints: submissions must be >2s apart, and
|
|
6640
6893
|
* background jobs in flight are capped by subscription tier (Tier 1=8,
|
|
@@ -6662,6 +6915,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6662
6915
|
if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
|
|
6663
6916
|
return { unit: "generations", amount: 0.1, candidates: 1 };
|
|
6664
6917
|
}
|
|
6918
|
+
if (spec.revision?.mode === "interpolate" || spec.revision?.mode === "edit-animation") {
|
|
6919
|
+
const width = spec.revision.sourceWidth ?? spec.width;
|
|
6920
|
+
const height = spec.revision.sourceHeight ?? spec.height;
|
|
6921
|
+
const side = spec.revision.mode === "edit-animation" ? Math.ceil(Math.sqrt(Math.max(1, spec.revision.sourceMembers?.length ?? 1))) : 1;
|
|
6922
|
+
return { unit: "generations", amount: generationCost(width * side, height * side, "1dir"), candidates: 1 };
|
|
6923
|
+
}
|
|
6665
6924
|
if (spec.revision?.mode === "animate" || spec.revision?.mode === "animate-pixminimax") {
|
|
6666
6925
|
const width = spec.revision.sourceWidth ?? spec.width;
|
|
6667
6926
|
const height = spec.revision.sourceHeight ?? spec.height;
|
|
@@ -6690,6 +6949,12 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6690
6949
|
};
|
|
6691
6950
|
}
|
|
6692
6951
|
validate(spec, styleImages) {
|
|
6952
|
+
const members = spec.revision?.sourceMembers;
|
|
6953
|
+
if (spec.revision && members && !MEMBER_SET_REVISION_MODES.includes(spec.revision.mode)) {
|
|
6954
|
+
throw new Error(
|
|
6955
|
+
`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`
|
|
6956
|
+
);
|
|
6957
|
+
}
|
|
6693
6958
|
if (spec.revision?.mode === "inpaint") {
|
|
6694
6959
|
const { sourceWidth: width, sourceHeight: height } = spec.revision;
|
|
6695
6960
|
if (width != null && height != null && (width < 32 || height < 32 || width > 512 || height > 512)) {
|
|
@@ -6698,9 +6963,10 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6698
6963
|
}
|
|
6699
6964
|
if (spec.revision?.mode === "reduce-colors") {
|
|
6700
6965
|
const { sourceWidth: width, sourceHeight: height } = spec.revision;
|
|
6701
|
-
|
|
6966
|
+
const count = members?.length ?? 1;
|
|
6967
|
+
if (width != null && height != null && width * height * count > 512 * 512) {
|
|
6702
6968
|
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`
|
|
6969
|
+
`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
6970
|
);
|
|
6705
6971
|
}
|
|
6706
6972
|
}
|
|
@@ -6724,6 +6990,36 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6724
6990
|
);
|
|
6725
6991
|
}
|
|
6726
6992
|
}
|
|
6993
|
+
if (spec.revision?.mode === "interpolate") {
|
|
6994
|
+
const { sourceWidth: width, sourceHeight: height, lastFrameWidth, lastFrameHeight } = spec.revision;
|
|
6995
|
+
if (width != null && height != null && (width < 16 || height < 16 || width > 128 || height > 128)) {
|
|
6996
|
+
throw new Error(`PixelLab interpolate keyframes are ${width}x${height}; the API takes 16 to 128 pixels per side`);
|
|
6997
|
+
}
|
|
6998
|
+
if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
|
|
6999
|
+
throw new Error(
|
|
7000
|
+
`PixelLab interpolate lastFrame is ${lastFrameWidth}x${lastFrameHeight}; the start keyframe is ${width}x${height} \u2014 they must match`
|
|
7001
|
+
);
|
|
7002
|
+
}
|
|
7003
|
+
}
|
|
7004
|
+
if (spec.revision?.mode === "edit-animation") {
|
|
7005
|
+
const { sourceWidth: width, sourceHeight: height, sourceAssetId } = spec.revision;
|
|
7006
|
+
if (width != null && height != null) {
|
|
7007
|
+
if (!members || members.length < 2) {
|
|
7008
|
+
throw new Error(
|
|
7009
|
+
`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)`
|
|
7010
|
+
);
|
|
7011
|
+
}
|
|
7012
|
+
if (width < 16 || height < 16 || width > 256 || height > 256) {
|
|
7013
|
+
throw new Error(`PixelLab edit-animation frames are ${width}x${height}; the API takes 16 to 256 pixels per side`);
|
|
7014
|
+
}
|
|
7015
|
+
const ceiling = editAnimationFrameCeiling(width, height);
|
|
7016
|
+
if (members.length > ceiling) {
|
|
7017
|
+
throw new Error(
|
|
7018
|
+
`PixelLab edit-animation takes at most ${ceiling} frames at ${width}x${height} (every frame is packed into one grid); ${sourceAssetId} has ${members.length}`
|
|
7019
|
+
);
|
|
7020
|
+
}
|
|
7021
|
+
}
|
|
7022
|
+
}
|
|
6727
7023
|
if (spec.generator === "1dir" && (spec.width < 32 || spec.width > 256)) {
|
|
6728
7024
|
throw new Error("PixelLab 1dir dimensions must be between 32 and 256 pixels");
|
|
6729
7025
|
}
|
|
@@ -6740,6 +7036,14 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6740
7036
|
`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
7037
|
);
|
|
6742
7038
|
}
|
|
7039
|
+
if (spec.generator === "uiElement" && (spec.width < 16 || spec.height < 16 || spec.width > 792 || spec.height > 688)) {
|
|
7040
|
+
throw new Error(
|
|
7041
|
+
`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)`
|
|
7042
|
+
);
|
|
7043
|
+
}
|
|
7044
|
+
if (spec.generator === "uiElement" && styleImages.length > 1) {
|
|
7045
|
+
throw new Error("PixelLab uiElement takes one concept image; list at most one styleImages entry");
|
|
7046
|
+
}
|
|
6743
7047
|
if (spec.generator === "map") {
|
|
6744
7048
|
requirePixelLabOption("map", "view", spec.view, ["low top-down", "high top-down", "side"]);
|
|
6745
7049
|
requirePixelLabOption("map", "outline", spec.outline, [
|
|
@@ -6823,8 +7127,8 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
6823
7127
|
throw new Error(`PixelLab isometricTile: size must be 16 to 64 pixels, got ${spec.width}`);
|
|
6824
7128
|
}
|
|
6825
7129
|
}
|
|
6826
|
-
if (spec.generator === "character") this.validateCharacter(spec, styleImages);
|
|
6827
|
-
if (spec.generator === "objectPro") this.validateObjectPro(spec, styleImages);
|
|
7130
|
+
if (spec.generator === "character" && !spec.revision) this.validateCharacter(spec, styleImages);
|
|
7131
|
+
if (spec.generator === "objectPro" && !spec.revision) this.validateObjectPro(spec, styleImages);
|
|
6828
7132
|
if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
|
|
6829
7133
|
throw new Error(`PixelLab ${spec.generator} does not support style images`);
|
|
6830
7134
|
}
|
|
@@ -7391,6 +7695,19 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7391
7695
|
});
|
|
7392
7696
|
return { jobId: res2.ui_asset_id, metadata: { backgroundJobId: res2.background_job_id } };
|
|
7393
7697
|
}
|
|
7698
|
+
if (spec.generator === "uiElement") {
|
|
7699
|
+
const concept = styleImages[0];
|
|
7700
|
+
const res2 = await this.client.generateUiV2({
|
|
7701
|
+
description: spec.prompt,
|
|
7702
|
+
width: spec.width,
|
|
7703
|
+
height: spec.height,
|
|
7704
|
+
conceptImage: concept ? { image: { base64: concept.base64, format: concept.format }, width: concept.width, height: concept.height } : void 0,
|
|
7705
|
+
colorPalette: spec.uiColorPalette,
|
|
7706
|
+
noBackground: spec.noBackground,
|
|
7707
|
+
seed: spec.seed
|
|
7708
|
+
});
|
|
7709
|
+
return { jobId: res2.background_job_id };
|
|
7710
|
+
}
|
|
7394
7711
|
if (spec.generator === "pixflux") {
|
|
7395
7712
|
const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
|
|
7396
7713
|
const { png } = await this.client.createImagePixflux({
|
|
@@ -7519,13 +7836,18 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7519
7836
|
if (!revision.sourceSha256 || !revision.sourceFormat || revision.sourceWidth == null || revision.sourceHeight == null) {
|
|
7520
7837
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision source is not ready`);
|
|
7521
7838
|
}
|
|
7522
|
-
const
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7839
|
+
const sourceFiles = revision.sourceMembers ?? [{ file: revision.sourceFile, sha256: revision.sourceSha256, format: revision.sourceFormat }];
|
|
7840
|
+
const images = sourceFiles.map((member) => {
|
|
7841
|
+
const bytes = readFileSync2(member.file);
|
|
7842
|
+
if (sha256(bytes) !== member.sha256) {
|
|
7843
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision source changed after the manifest was resolved`);
|
|
7844
|
+
}
|
|
7845
|
+
return { base64: bytes.toString("base64"), format: member.format };
|
|
7846
|
+
});
|
|
7847
|
+
const image = images[0];
|
|
7527
7848
|
const width = revision.sourceWidth;
|
|
7528
7849
|
const height = revision.sourceHeight;
|
|
7850
|
+
const roles = revision.sourceMembers?.map((member) => member.role);
|
|
7529
7851
|
if (revision.mode === "inpaint") {
|
|
7530
7852
|
if (!revision.maskFile || !revision.maskSha256 || !revision.maskFormat) {
|
|
7531
7853
|
throw new Error(`${spec.styleId}/${spec.assetId}: revision mask is not ready`);
|
|
@@ -7558,34 +7880,45 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7558
7880
|
paletteImage = { base64: paletteBytes.toString("base64"), format: revision.paletteImageFormat };
|
|
7559
7881
|
}
|
|
7560
7882
|
const res2 = await this.client.reduceColors({
|
|
7561
|
-
|
|
7883
|
+
images,
|
|
7562
7884
|
numColors: revision.numColors,
|
|
7563
7885
|
paletteImage,
|
|
7564
7886
|
dithering: revision.dithering,
|
|
7565
7887
|
ditheringStrength: revision.ditheringStrength
|
|
7566
7888
|
});
|
|
7567
|
-
|
|
7568
|
-
writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), res2.png);
|
|
7569
|
-
return { jobId, metadata: { revisionUsage: res2.usage } };
|
|
7889
|
+
return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
|
|
7570
7890
|
}
|
|
7571
7891
|
if (revision.mode === "correct-pixelart") {
|
|
7572
|
-
const res2 = await this.client.correctPixelart({
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7892
|
+
const res2 = await this.client.correctPixelart({ images, strength: revision.strength });
|
|
7893
|
+
return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
|
|
7894
|
+
}
|
|
7895
|
+
if (revision.mode === "interpolate") {
|
|
7896
|
+
const lastFrame = this.readLastFrame(spec);
|
|
7897
|
+
if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
|
|
7898
|
+
const res2 = await this.client.interpolationV2({
|
|
7899
|
+
startImage: image,
|
|
7900
|
+
endImage: lastFrame,
|
|
7901
|
+
width,
|
|
7902
|
+
height,
|
|
7903
|
+
action: spec.prompt,
|
|
7904
|
+
seed: spec.seed,
|
|
7905
|
+
noBackground: spec.noBackground
|
|
7906
|
+
});
|
|
7907
|
+
return { jobId: res2.background_job_id };
|
|
7908
|
+
}
|
|
7909
|
+
if (revision.mode === "edit-animation") {
|
|
7910
|
+
const res2 = await this.client.editAnimationV2({
|
|
7911
|
+
frames: images,
|
|
7912
|
+
width,
|
|
7913
|
+
height,
|
|
7914
|
+
description: spec.prompt,
|
|
7915
|
+
seed: spec.seed,
|
|
7916
|
+
noBackground: spec.noBackground
|
|
7917
|
+
});
|
|
7918
|
+
return { jobId: res2.background_job_id };
|
|
7576
7919
|
}
|
|
7577
7920
|
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
|
-
}
|
|
7921
|
+
const lastFrame = this.readLastFrame(spec);
|
|
7589
7922
|
if (revision.mode === "animate") {
|
|
7590
7923
|
const res3 = await this.client.animateWithTextV3({
|
|
7591
7924
|
firstFrame: image,
|
|
@@ -7625,6 +7958,36 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7625
7958
|
});
|
|
7626
7959
|
return { jobId: res.background_job_id };
|
|
7627
7960
|
}
|
|
7961
|
+
/** A revision's pinned ending frame, proven to be the bytes the spec hashed; undefined when none is declared. */
|
|
7962
|
+
readLastFrame(spec) {
|
|
7963
|
+
const revision = spec.revision;
|
|
7964
|
+
if (!revision.lastFrameFile) return void 0;
|
|
7965
|
+
if (!revision.lastFrameSha256 || !revision.lastFrameFormat) {
|
|
7966
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame is not ready`);
|
|
7967
|
+
}
|
|
7968
|
+
const bytes = readFileSync2(revision.lastFrameFile);
|
|
7969
|
+
if (sha256(bytes) !== revision.lastFrameSha256) {
|
|
7970
|
+
throw new Error(`${spec.styleId}/${spec.assetId}: revision last frame changed after the manifest was resolved`);
|
|
7971
|
+
}
|
|
7972
|
+
return { base64: bytes.toString("base64"), format: revision.lastFrameFormat };
|
|
7973
|
+
}
|
|
7974
|
+
/**
|
|
7975
|
+
* Park a synchronous Cleanup-tier result the way pixflux parks its own:
|
|
7976
|
+
* one `<id>.png` for a single image, or `<id>-<n>.png` per member with the
|
|
7977
|
+
* member roles recorded so `pollCachedRevision` hands them back under the
|
|
7978
|
+
* same names the parent's set used.
|
|
7979
|
+
*/
|
|
7980
|
+
cacheCleanupResult(pngs, usage, roles, fps) {
|
|
7981
|
+
const jobId = randomUUID3();
|
|
7982
|
+
if (!roles) {
|
|
7983
|
+
writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), pngs[0]);
|
|
7984
|
+
return { jobId, metadata: { revisionUsage: usage } };
|
|
7985
|
+
}
|
|
7986
|
+
for (const [index, png] of pngs.entries()) {
|
|
7987
|
+
writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`), png);
|
|
7988
|
+
}
|
|
7989
|
+
return { jobId, metadata: { revisionUsage: usage, revisionMembers: roles, ...fps != null ? { revisionFps: fps } : {} } };
|
|
7990
|
+
}
|
|
7628
7991
|
/**
|
|
7629
7992
|
* What a background job actually billed, once its own record is still
|
|
7630
7993
|
* around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
|
|
@@ -7645,7 +8008,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7645
8008
|
if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
|
|
7646
8009
|
return this.pollCachedRevision(jobId, context);
|
|
7647
8010
|
}
|
|
7648
|
-
if (revisionMode === "animate" || revisionMode === "animate-pixminimax") {
|
|
8011
|
+
if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
|
|
7649
8012
|
return this.pollAnimateRevision(jobId, context);
|
|
7650
8013
|
}
|
|
7651
8014
|
if (context?.spec?.revision) return this.pollRevision(jobId);
|
|
@@ -7668,6 +8031,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7668
8031
|
if (generator === "character") return this.pollCharacter(jobId, context);
|
|
7669
8032
|
if (generator === "objectPro") return this.pollObjectPro(jobId, context);
|
|
7670
8033
|
if (generator === "uiAsset") return this.pollUiAsset(jobId, context);
|
|
8034
|
+
if (generator === "uiElement") return this.pollUiElement(jobId);
|
|
7671
8035
|
const backgroundJobId = context?.metadata?.backgroundJobId;
|
|
7672
8036
|
const obj = await this.client.getObject(jobId);
|
|
7673
8037
|
if (obj.status === "review") {
|
|
@@ -7694,15 +8058,32 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7694
8058
|
* work.
|
|
7695
8059
|
*/
|
|
7696
8060
|
async pollCachedRevision(jobId, context) {
|
|
7697
|
-
const
|
|
7698
|
-
|
|
8061
|
+
const usage = context?.metadata?.revisionUsage;
|
|
8062
|
+
const roles = context?.metadata?.revisionMembers;
|
|
8063
|
+
const gone = {
|
|
8064
|
+
status: "failed",
|
|
8065
|
+
error: "revision result is no longer cached locally; re-run submit for this asset"
|
|
8066
|
+
};
|
|
8067
|
+
if (Array.isArray(roles) && roles.length) {
|
|
8068
|
+
const sources = roles.map((role, index) => ({
|
|
8069
|
+
url: `file://${path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`)}`,
|
|
8070
|
+
role: String(role)
|
|
8071
|
+
}));
|
|
8072
|
+
if (sources.some((source) => !existsSync7(source.url.slice("file://".length)))) return gone;
|
|
8073
|
+
const fps = context?.metadata?.revisionFps;
|
|
8074
|
+
const frames = sources.every((source) => source.role.startsWith("frame-"));
|
|
7699
8075
|
return {
|
|
7700
|
-
status: "
|
|
7701
|
-
|
|
8076
|
+
status: "ready",
|
|
8077
|
+
objectId: jobId,
|
|
8078
|
+
sourceUrl: sources[0].url,
|
|
8079
|
+
sources,
|
|
8080
|
+
...frames && typeof fps === "number" ? { metadata: { frameSet: { fps, count: sources.length } } } : {},
|
|
8081
|
+
billed: billedFromUsage(usage)
|
|
7702
8082
|
};
|
|
7703
8083
|
}
|
|
8084
|
+
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
|
|
8085
|
+
if (!existsSync7(file)) return gone;
|
|
7704
8086
|
const sourceUrl = `file://${file}`;
|
|
7705
|
-
const usage = context?.metadata?.revisionUsage;
|
|
7706
8087
|
return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }], billed: billedFromUsage(usage) };
|
|
7707
8088
|
}
|
|
7708
8089
|
/**
|
|
@@ -7726,16 +8107,21 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7726
8107
|
if (job.status !== "completed") return { status: "processing" };
|
|
7727
8108
|
const billed = billedFromUsage(job.usage);
|
|
7728
8109
|
const done = job.last_response ?? {};
|
|
7729
|
-
const
|
|
7730
|
-
const
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
frameUrls,
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
8110
|
+
const revision = context?.spec?.revision;
|
|
8111
|
+
const fps = revision?.fps ?? revision?.sourceFps ?? 8;
|
|
8112
|
+
const memberRoles = revision?.mode === "edit-animation" ? revision.sourceMembers?.map((member) => member.role) : void 0;
|
|
8113
|
+
const review = (frameUrls) => {
|
|
8114
|
+
const roles = memberRoles?.length === frameUrls.length ? memberRoles : frameUrls.map((_, index) => `frame-${String(index).padStart(2, "0")}`);
|
|
8115
|
+
return {
|
|
8116
|
+
status: "review-set",
|
|
8117
|
+
objectId: jobId,
|
|
8118
|
+
frameUrls,
|
|
8119
|
+
sources: frameUrls.map((url, index) => ({ url, role: roles[index] })),
|
|
8120
|
+
fps,
|
|
8121
|
+
metadata: roles.every((role) => role.startsWith("frame-")) ? { frameSet: { fps, count: frameUrls.length } } : {},
|
|
8122
|
+
billed
|
|
8123
|
+
};
|
|
8124
|
+
};
|
|
7739
8125
|
for (const key of ["frame_urls", "frames", "images"]) {
|
|
7740
8126
|
const list = done[key];
|
|
7741
8127
|
if (!Array.isArray(list) || !list.length) continue;
|
|
@@ -7767,7 +8153,7 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7767
8153
|
}
|
|
7768
8154
|
return {
|
|
7769
8155
|
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`
|
|
8156
|
+
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
8157
|
};
|
|
7772
8158
|
}
|
|
7773
8159
|
/**
|
|
@@ -7857,6 +8243,36 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
7857
8243
|
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
8244
|
};
|
|
7859
8245
|
}
|
|
8246
|
+
/**
|
|
8247
|
+
* `/generate-ui-v2` completes a plain background job whose
|
|
8248
|
+
* `last_response.images` (per its own docs and Python client) holds the
|
|
8249
|
+
* result. One image is the expected case and goes straight to selected;
|
|
8250
|
+
* should the endpoint ever return several, they go to review like
|
|
8251
|
+
* imagePro's candidates, decoded to the same local cache. The shape has
|
|
8252
|
+
* not been observed live, so anything else fails naming the keys received.
|
|
8253
|
+
*/
|
|
8254
|
+
async pollUiElement(jobId) {
|
|
8255
|
+
const job = await this.client.getBackgroundJob(jobId);
|
|
8256
|
+
if (job.status === "failed") return { status: "failed", error: "UI element job failed upstream" };
|
|
8257
|
+
if (job.status !== "completed") return { status: "processing" };
|
|
8258
|
+
const billed = billedFromUsage(job.usage);
|
|
8259
|
+
const done = job.last_response ?? {};
|
|
8260
|
+
const images = Array.isArray(done.images) ? done.images : done.image ? [done.image] : [];
|
|
8261
|
+
const urls = [];
|
|
8262
|
+
for (const [index, candidate] of images.entries()) {
|
|
8263
|
+
const base64 = extractBase64(candidate);
|
|
8264
|
+
if (!base64) continue;
|
|
8265
|
+
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
8266
|
+
writeFileSync(file, Buffer.from(base64, "base64"));
|
|
8267
|
+
urls.push(`file://${file}`);
|
|
8268
|
+
}
|
|
8269
|
+
if (urls.length === 1) return { status: "ready", objectId: jobId, sourceUrl: urls[0], sources: [{ url: urls[0] }], billed };
|
|
8270
|
+
if (urls.length > 1) return { status: "review", candidateUrls: urls, billed };
|
|
8271
|
+
return {
|
|
8272
|
+
status: "failed",
|
|
8273
|
+
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`
|
|
8274
|
+
};
|
|
8275
|
+
}
|
|
7860
8276
|
/**
|
|
7861
8277
|
* Map objects need their own path because the `/map-objects/{id}` record is
|
|
7862
8278
|
* deleted upstream roughly 8 hours after creation while the image survives in
|
|
@@ -8017,9 +8433,9 @@ var PixelLabProvider = class _PixelLabProvider {
|
|
|
8017
8433
|
if (!url) throw new Error(`tiles job ${jobId} has no variation at index ${index}`);
|
|
8018
8434
|
return { objectId: `${jobId}#${index}`, sourceUrl: url };
|
|
8019
8435
|
}
|
|
8020
|
-
if (generator === "imagePro") {
|
|
8436
|
+
if (generator === "imagePro" || generator === "uiElement") {
|
|
8021
8437
|
const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}-${index}.png`);
|
|
8022
|
-
if (!existsSync7(file)) throw new Error(
|
|
8438
|
+
if (!existsSync7(file)) throw new Error(`${generator} job ${jobId} has no cached candidate at index ${index}`);
|
|
8023
8439
|
return { objectId: `${jobId}#${index}`, sourceUrl: `file://${file}` };
|
|
8024
8440
|
}
|
|
8025
8441
|
const promoted = await this.client.selectFrames(jobId, [index], commonTag);
|
|
@@ -8173,6 +8589,12 @@ function readPose(spec, what, image) {
|
|
|
8173
8589
|
}
|
|
8174
8590
|
return { base64: bytes.toString("base64"), format: image.format };
|
|
8175
8591
|
}
|
|
8592
|
+
function editAnimationFrameCeiling(width, height) {
|
|
8593
|
+
const side = Math.max(width, height);
|
|
8594
|
+
if (side <= 64) return 16;
|
|
8595
|
+
if (side <= 80) return 9;
|
|
8596
|
+
return 4;
|
|
8597
|
+
}
|
|
8176
8598
|
function rotationSources(character) {
|
|
8177
8599
|
const order = character.directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
|
|
8178
8600
|
const sources = [];
|
|
@@ -8265,9 +8687,9 @@ var RetroDiffusionClient = class {
|
|
|
8265
8687
|
if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
|
|
8266
8688
|
return Buffer.from(await response.arrayBuffer());
|
|
8267
8689
|
}
|
|
8268
|
-
async call(
|
|
8690
|
+
async call(path47, init = {}) {
|
|
8269
8691
|
if (!this.token) throw new Error("RD_API_KEY is not set");
|
|
8270
|
-
const response = await this.request(`${this.baseUrl}${
|
|
8692
|
+
const response = await this.request(`${this.baseUrl}${path47}`, {
|
|
8271
8693
|
...init,
|
|
8272
8694
|
headers: {
|
|
8273
8695
|
"Content-Type": "application/json",
|
|
@@ -9399,7 +9821,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9399
9821
|
size = asset.size ?? style.size ?? 64;
|
|
9400
9822
|
width = size;
|
|
9401
9823
|
height = size;
|
|
9402
|
-
} else if (generator === "uiAsset") {
|
|
9824
|
+
} else if (generator === "uiAsset" || generator === "uiElement") {
|
|
9403
9825
|
width = asset.width ?? style.size ?? 256;
|
|
9404
9826
|
height = asset.height ?? style.size ?? 256;
|
|
9405
9827
|
size = Math.max(width, height);
|
|
@@ -9466,7 +9888,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9466
9888
|
seed: style.seed,
|
|
9467
9889
|
uiPieces: generator === "uiAsset" ? asset.pieces : void 0,
|
|
9468
9890
|
uiElements: generator === "uiAsset" ? asset.elements : void 0,
|
|
9469
|
-
uiColorPalette: generator === "uiAsset" ? style.uiColorPalette : void 0,
|
|
9891
|
+
uiColorPalette: generator === "uiAsset" || generator === "uiElement" ? style.uiColorPalette : void 0,
|
|
9470
9892
|
palette: style.palette,
|
|
9471
9893
|
enforcePalette: style.enforcePalette,
|
|
9472
9894
|
noBackground: style.noBackground,
|
|
@@ -9506,8 +9928,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9506
9928
|
terrainView: generator === "terrain" ? style.terrainView : void 0,
|
|
9507
9929
|
isometricTileSize: generator === "isometricTile" ? style.isometricTileSize : void 0,
|
|
9508
9930
|
isometricTileShape: generator === "isometricTile" ? style.isometricTileShape : void 0,
|
|
9509
|
-
|
|
9510
|
-
|
|
9931
|
+
// A revision in a character or objectPro style is a provider edit of
|
|
9932
|
+
// its parent's pixels, not a new character: giving it a base shape
|
|
9933
|
+
// would have packing, the gallery, and adoption treat it as one.
|
|
9934
|
+
...generator === "character" && !asset.revision ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
9935
|
+
...generator === "objectPro" && !asset.revision ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
|
|
9511
9936
|
cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
|
|
9512
9937
|
costUnit: "generations",
|
|
9513
9938
|
candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
|
|
@@ -9658,8 +10083,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9658
10083
|
);
|
|
9659
10084
|
}
|
|
9660
10085
|
const sourceSpec = await finalize(asset.revision.from);
|
|
9661
|
-
const
|
|
9662
|
-
const
|
|
10086
|
+
const sourceStem = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path13.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
10087
|
+
const sourceMembers = await revisionSourceMembers(sourceStem, `revision source ${asset.revision.from}`);
|
|
10088
|
+
const sourceFile = sourceMembers?.[0]?.file ?? sourceStem;
|
|
10089
|
+
const sourceImage = sourceMembers ? { ...sourceMembers[0], hash: sha256(JSON.stringify(sourceMembers.map((member) => member.sha256))) } : await optionalRevisionImage(sourceFile, "revision source");
|
|
10090
|
+
const sourceFps = sourceSpec.character?.animation?.fps ?? sourceSpec.objectPro?.animation?.fps ?? sourceSpec.revision?.fps ?? sourceSpec.revision?.sourceFps;
|
|
9663
10091
|
const maskFile = asset.revision.mask ? path13.resolve(root, asset.revision.mask) : void 0;
|
|
9664
10092
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
9665
10093
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
@@ -9680,6 +10108,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
9680
10108
|
sourceHeight: sourceImage?.height ?? null,
|
|
9681
10109
|
sourceFormat: sourceImage?.format ?? null,
|
|
9682
10110
|
sourceSpec,
|
|
10111
|
+
...sourceMembers ? { sourceMembers } : {},
|
|
10112
|
+
...sourceMembers && sourceFps != null ? { sourceFps } : {},
|
|
9683
10113
|
...maskFile ? {
|
|
9684
10114
|
maskFile,
|
|
9685
10115
|
maskSha256: maskImage?.hash ?? null,
|
|
@@ -9829,6 +10259,38 @@ async function optionalRevisionImage(file, label, requiredFormat) {
|
|
|
9829
10259
|
}
|
|
9830
10260
|
return { hash: sha256(bytes), ...metadata };
|
|
9831
10261
|
}
|
|
10262
|
+
async function revisionSourceMembers(stem, label) {
|
|
10263
|
+
if (existsSync8(stem)) return null;
|
|
10264
|
+
const at = (role) => memberPath(stem, role, 0, 2, MediaType.PNG);
|
|
10265
|
+
let roles = [];
|
|
10266
|
+
for (const set of [CHARACTER_DIRECTIONS_8, CHARACTER_DIRECTIONS_4]) {
|
|
10267
|
+
if (set.every((direction) => existsSync8(at(direction)))) {
|
|
10268
|
+
roles = [...set];
|
|
10269
|
+
break;
|
|
10270
|
+
}
|
|
10271
|
+
}
|
|
10272
|
+
if (!roles.length) {
|
|
10273
|
+
for (let index = 0; existsSync8(at(`frame-${String(index).padStart(2, "0")}`)); index++) {
|
|
10274
|
+
roles.push(`frame-${String(index).padStart(2, "0")}`);
|
|
10275
|
+
}
|
|
10276
|
+
}
|
|
10277
|
+
if (!roles.length) return null;
|
|
10278
|
+
const members = [];
|
|
10279
|
+
for (const role of roles) {
|
|
10280
|
+
const file = at(role);
|
|
10281
|
+
const image = await optionalRevisionImage(file, `${label} (${role})`);
|
|
10282
|
+
if (!image) return null;
|
|
10283
|
+
members.push({ role, file, sha256: image.hash, width: image.width, height: image.height, format: image.format });
|
|
10284
|
+
}
|
|
10285
|
+
const first = members[0];
|
|
10286
|
+
const odd = members.find((member) => member.width !== first.width || member.height !== first.height);
|
|
10287
|
+
if (odd) {
|
|
10288
|
+
throw new Error(
|
|
10289
|
+
`${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`
|
|
10290
|
+
);
|
|
10291
|
+
}
|
|
10292
|
+
return members;
|
|
10293
|
+
}
|
|
9832
10294
|
async function resolveStyleImages(loaded, styleId) {
|
|
9833
10295
|
const style = loaded.manifest.styles[styleId];
|
|
9834
10296
|
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
@@ -10342,10 +10804,10 @@ function isSha256Hash(value) {
|
|
|
10342
10804
|
function parseCache(value) {
|
|
10343
10805
|
return HashCacheSchema.parse(value);
|
|
10344
10806
|
}
|
|
10345
|
-
async function loadCache(
|
|
10346
|
-
if (!existsSync10(
|
|
10807
|
+
async function loadCache(path47) {
|
|
10808
|
+
if (!existsSync10(path47)) return { version: 1, hashes: {} };
|
|
10347
10809
|
try {
|
|
10348
|
-
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(
|
|
10810
|
+
const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path47, "utf8")));
|
|
10349
10811
|
if (!parsed.success) return { version: 1, hashes: {} };
|
|
10350
10812
|
return {
|
|
10351
10813
|
version: 1,
|
|
@@ -10357,18 +10819,18 @@ async function loadCache(path45) {
|
|
|
10357
10819
|
return { version: 1, hashes: {} };
|
|
10358
10820
|
}
|
|
10359
10821
|
}
|
|
10360
|
-
async function saveCache(
|
|
10822
|
+
async function saveCache(path47, cache) {
|
|
10361
10823
|
const sorted = {};
|
|
10362
10824
|
for (const key of Object.keys(cache.hashes).sort()) {
|
|
10363
10825
|
const hash = cache.hashes[key];
|
|
10364
10826
|
if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
|
|
10365
10827
|
sorted[key] = hash;
|
|
10366
10828
|
}
|
|
10367
|
-
await mkdir5(pathModule.dirname(pathModule.resolve(
|
|
10368
|
-
const tmp = `${
|
|
10829
|
+
await mkdir5(pathModule.dirname(pathModule.resolve(path47)), { recursive: true });
|
|
10830
|
+
const tmp = `${path47}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
10369
10831
|
try {
|
|
10370
10832
|
await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
|
|
10371
|
-
await rename5(tmp,
|
|
10833
|
+
await rename5(tmp, path47);
|
|
10372
10834
|
} finally {
|
|
10373
10835
|
await rm5(tmp, { force: true });
|
|
10374
10836
|
}
|
|
@@ -13001,6 +13463,7 @@ async function samePixels(a, b) {
|
|
|
13001
13463
|
var metadataFps = (entry) => entry ? frameSetFps(entry) : null;
|
|
13002
13464
|
function describeCharacter(spec, entry) {
|
|
13003
13465
|
const recordedCharacter = entry?.providerMetadata?.[entry.provider]?.character;
|
|
13466
|
+
if (spec?.revision) return null;
|
|
13004
13467
|
if (spec?.character) {
|
|
13005
13468
|
const character = spec.character;
|
|
13006
13469
|
return {
|
|
@@ -17723,20 +18186,173 @@ function sheetFormat(args) {
|
|
|
17723
18186
|
return format;
|
|
17724
18187
|
}
|
|
17725
18188
|
|
|
17726
|
-
// src/cli/commands/
|
|
18189
|
+
// src/cli/commands/pixellab-utils.ts
|
|
17727
18190
|
import path40 from "path";
|
|
17728
|
-
import { readFile as readFile27 } from "fs/promises";
|
|
17729
18191
|
|
|
17730
|
-
// src/
|
|
17731
|
-
import path39 from "path";
|
|
18192
|
+
// src/pixellab-utils.ts
|
|
17732
18193
|
import { existsSync as existsSync29 } from "fs";
|
|
17733
|
-
import { readFile as readFile26 } from "fs/promises";
|
|
18194
|
+
import { mkdir as mkdir11, readFile as readFile26, writeFile as writeFile14 } from "fs/promises";
|
|
18195
|
+
import path39 from "path";
|
|
18196
|
+
var UNZOOM_MIN_SIDE = 256;
|
|
18197
|
+
var UNZOOM_MAX_AREA = 2048 * 2048;
|
|
18198
|
+
function defaultUnzoomOut(input) {
|
|
18199
|
+
const ext = path39.extname(input);
|
|
18200
|
+
return `${ext ? input.slice(0, -ext.length) : input}.unzoomed.png`;
|
|
18201
|
+
}
|
|
18202
|
+
async function unzoomFile(client2, input, opts = {}) {
|
|
18203
|
+
const out = path39.resolve(opts.out ?? defaultUnzoomOut(input));
|
|
18204
|
+
if (path39.resolve(input) === out) throw new Error("unzoom will not overwrite its own input; pass a different --out");
|
|
18205
|
+
if (existsSync29(out) && !opts.force) throw new Error(`${out} already exists; pass --force to replace it`);
|
|
18206
|
+
const bytes = await readFile26(input);
|
|
18207
|
+
const metadata = imageMetadata(bytes);
|
|
18208
|
+
if (!metadata) throw new Error(`${input} is not a readable PNG or JPEG`);
|
|
18209
|
+
const { width, height, format } = metadata;
|
|
18210
|
+
if (width < UNZOOM_MIN_SIDE || height < UNZOOM_MIN_SIDE) {
|
|
18211
|
+
throw new Error(
|
|
18212
|
+
`${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`
|
|
18213
|
+
);
|
|
18214
|
+
}
|
|
18215
|
+
if (width * height > UNZOOM_MAX_AREA) {
|
|
18216
|
+
throw new Error(`${input} is ${width}x${height}; PixelLab's unzoom takes at most 2048x2048 worth of pixels`);
|
|
18217
|
+
}
|
|
18218
|
+
const res = await client2.unzoom({ image: { base64: bytes.toString("base64"), format }, quantize: opts.quantize });
|
|
18219
|
+
await mkdir11(path39.dirname(out), { recursive: true });
|
|
18220
|
+
await writeFile14(out, res.png);
|
|
18221
|
+
return { out, original: res.originalSize, unzoomed: res.unzoomedSize, zoomFactor: res.zoomFactor, usage: res.usage };
|
|
18222
|
+
}
|
|
18223
|
+
var FONT_WEIGHTS = ["Bold", "Regular"];
|
|
18224
|
+
var FONT_GLYPH_SIZES = [8, 16, 32, 64];
|
|
18225
|
+
var FONT_COST_GENERATIONS = 25;
|
|
18226
|
+
function fontOutputPaths(out) {
|
|
18227
|
+
const base = path39.resolve(out.replace(/\.(ttf|png)$/i, ""));
|
|
18228
|
+
return { ttf: `${base}.ttf`, atlas: `${base}.png` };
|
|
18229
|
+
}
|
|
18230
|
+
async function generateFont(client2, opts) {
|
|
18231
|
+
const { ttf, atlas } = fontOutputPaths(opts.out);
|
|
18232
|
+
if (!opts.force) {
|
|
18233
|
+
for (const file of [ttf, atlas]) {
|
|
18234
|
+
if (existsSync29(file)) throw new Error(`${file} already exists; pass --force to replace it`);
|
|
18235
|
+
}
|
|
18236
|
+
}
|
|
18237
|
+
const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
18238
|
+
const interval = opts.intervalMs ?? 5e3;
|
|
18239
|
+
const timeout = opts.timeoutMs ?? 10 * 6e4;
|
|
18240
|
+
const submitted = await client2.generateFontPro({
|
|
18241
|
+
description: opts.description,
|
|
18242
|
+
weight: opts.weight,
|
|
18243
|
+
glyphPx: opts.glyphPx,
|
|
18244
|
+
seed: opts.seed,
|
|
18245
|
+
fontName: opts.fontName
|
|
18246
|
+
});
|
|
18247
|
+
const jobId = submitted.background_job_id;
|
|
18248
|
+
const started = Date.now();
|
|
18249
|
+
for (; ; ) {
|
|
18250
|
+
const job = await client2.getFontJob(jobId);
|
|
18251
|
+
opts.onProgress?.(job.status);
|
|
18252
|
+
if (job.status === "failed") throw new Error(`PixelLab font job ${jobId} failed upstream`);
|
|
18253
|
+
if (job.status === "completed") {
|
|
18254
|
+
if (!job.download_ttf_url || !job.download_atlas_url) {
|
|
18255
|
+
throw new Error(`PixelLab font job ${jobId} completed without both download URLs`);
|
|
18256
|
+
}
|
|
18257
|
+
const [ttfBytes, atlasBytes] = await Promise.all([
|
|
18258
|
+
client2.download(job.download_ttf_url),
|
|
18259
|
+
client2.download(job.download_atlas_url)
|
|
18260
|
+
]);
|
|
18261
|
+
await mkdir11(path39.dirname(ttf), { recursive: true });
|
|
18262
|
+
await writeFile14(ttf, ttfBytes);
|
|
18263
|
+
await writeFile14(atlas, atlasBytes);
|
|
18264
|
+
return { jobId, ttf, atlas, glyphPx: job.glyph_px ?? null, usage: job.usage };
|
|
18265
|
+
}
|
|
18266
|
+
if (Date.now() - started > timeout) {
|
|
18267
|
+
throw new Error(
|
|
18268
|
+
`PixelLab font job ${jobId} is still ${job.status} after ${Math.round(timeout / 1e3)} s; it keeps running upstream, but this command has stopped waiting`
|
|
18269
|
+
);
|
|
18270
|
+
}
|
|
18271
|
+
await sleep3(interval);
|
|
18272
|
+
}
|
|
18273
|
+
}
|
|
18274
|
+
|
|
18275
|
+
// src/cli/commands/pixellab-utils.ts
|
|
18276
|
+
function loadKeys(args) {
|
|
18277
|
+
loadEnvFiles(path40.dirname(path40.resolve(args.manifest)));
|
|
18278
|
+
loadEnvFiles(process.cwd());
|
|
18279
|
+
}
|
|
18280
|
+
async function runUnzoom(args) {
|
|
18281
|
+
if (!args.from) throw new UsageError("unzoom needs --from <image>: the upscaled PNG or JPEG to shrink back to its native grid");
|
|
18282
|
+
const out = args.out ?? defaultUnzoomOut(args.from);
|
|
18283
|
+
if (args.dryRun) {
|
|
18284
|
+
log(` would unzoom ${args.from} \u2192 ${out}${args.quantize === void 0 ? "" : ` (quantize ${args.quantize})`}; nothing sent`);
|
|
18285
|
+
return;
|
|
18286
|
+
}
|
|
18287
|
+
loadKeys(args);
|
|
18288
|
+
const res = await unzoomFile(clientFromEnv(), args.from, { out, quantize: args.quantize, force: args.force });
|
|
18289
|
+
if (args.json) {
|
|
18290
|
+
log(JSON.stringify({ version: 1, ...res }, null, 2));
|
|
18291
|
+
return;
|
|
18292
|
+
}
|
|
18293
|
+
log(
|
|
18294
|
+
` ${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}`
|
|
18295
|
+
);
|
|
18296
|
+
log(" the result is opaque: PixelLab composites transparency onto white first");
|
|
18297
|
+
}
|
|
18298
|
+
async function runFont(args) {
|
|
18299
|
+
if (!args.description) throw new UsageError('font needs --description "<style>", e.g. "warm orange arcade font"');
|
|
18300
|
+
if (!args.out) throw new UsageError("font needs --out <base>: <base>.ttf and <base>.png are written");
|
|
18301
|
+
const weight = args.weight ?? "Regular";
|
|
18302
|
+
if (!FONT_WEIGHTS.includes(weight)) {
|
|
18303
|
+
throw new UsageError(`--weight must be ${FONT_WEIGHTS.join(" or ")}, got "${args.weight}"`);
|
|
18304
|
+
}
|
|
18305
|
+
const glyphPx = args.glyphPx ?? 16;
|
|
18306
|
+
if (!FONT_GLYPH_SIZES.includes(glyphPx)) {
|
|
18307
|
+
throw new UsageError(`--glyph-px must be ${FONT_GLYPH_SIZES.join(", ")}, got "${args.glyphPx}"`);
|
|
18308
|
+
}
|
|
18309
|
+
const { ttf, atlas } = fontOutputPaths(args.out);
|
|
18310
|
+
log(` font: "${args.description}", ${weight}, ${glyphPx}px glyphs \u2192 ${ttf} + ${atlas}`);
|
|
18311
|
+
log(` cost: ${FONT_COST_GENERATIONS} generations (PixelLab's documented price)`);
|
|
18312
|
+
if (args.budget !== void 0 && args.budget < FONT_COST_GENERATIONS) {
|
|
18313
|
+
throw new Error(`--budget ${args.budget} is below this font's ${FONT_COST_GENERATIONS}-generation cost`);
|
|
18314
|
+
}
|
|
18315
|
+
if (args.dryRun) {
|
|
18316
|
+
log(" dry run: nothing sent");
|
|
18317
|
+
return;
|
|
18318
|
+
}
|
|
18319
|
+
if (!await confirm(` Spend ${FONT_COST_GENERATIONS} generations on this font?`, args.yes)) return;
|
|
18320
|
+
loadKeys(args);
|
|
18321
|
+
let last = "";
|
|
18322
|
+
const res = await generateFont(clientFromEnv(), {
|
|
18323
|
+
description: args.description,
|
|
18324
|
+
weight,
|
|
18325
|
+
glyphPx,
|
|
18326
|
+
fontName: args.name,
|
|
18327
|
+
out: args.out,
|
|
18328
|
+
force: args.force,
|
|
18329
|
+
onProgress: (status) => {
|
|
18330
|
+
if (status !== last) log(` ${status}\u2026`);
|
|
18331
|
+
last = status;
|
|
18332
|
+
}
|
|
18333
|
+
});
|
|
18334
|
+
if (args.json) {
|
|
18335
|
+
log(JSON.stringify({ version: 1, ...res }, null, 2));
|
|
18336
|
+
return;
|
|
18337
|
+
}
|
|
18338
|
+
log(` wrote ${res.ttf}`);
|
|
18339
|
+
log(` wrote ${res.atlas}`);
|
|
18340
|
+
}
|
|
18341
|
+
|
|
18342
|
+
// src/cli/commands/quality.ts
|
|
18343
|
+
import path42 from "path";
|
|
18344
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
18345
|
+
|
|
18346
|
+
// src/pipeline/quality-regression.ts
|
|
18347
|
+
import path41 from "path";
|
|
18348
|
+
import { existsSync as existsSync30 } from "fs";
|
|
18349
|
+
import { readFile as readFile27 } from "fs/promises";
|
|
17734
18350
|
import { z as z9 } from "zod";
|
|
17735
18351
|
var SHA256_RE = /^[0-9a-f]{64}$/;
|
|
17736
18352
|
var HEX_RE = /^#[0-9a-f]{6}$/;
|
|
17737
18353
|
var MAX_REDMEAN_DISTANCE = 765;
|
|
17738
18354
|
var QualityPathSchema = z9.string().min(1).refine(
|
|
17739
|
-
(value) => !
|
|
18355
|
+
(value) => !path41.posix.isAbsolute(value) && !value.includes("\\"),
|
|
17740
18356
|
"expected a portable relative path"
|
|
17741
18357
|
);
|
|
17742
18358
|
var DEFAULT_QUALITY_TOLERANCES = {
|
|
@@ -17883,10 +18499,10 @@ function measureDecodedImageQuality(png, digest2) {
|
|
|
17883
18499
|
};
|
|
17884
18500
|
}
|
|
17885
18501
|
async function measureImageQuality(file) {
|
|
17886
|
-
const absolute =
|
|
18502
|
+
const absolute = path41.resolve(file);
|
|
17887
18503
|
let bytes;
|
|
17888
18504
|
try {
|
|
17889
|
-
bytes = await
|
|
18505
|
+
bytes = await readFile27(absolute);
|
|
17890
18506
|
} catch (error) {
|
|
17891
18507
|
throw new Error(`Cannot read quality image ${absolute}: ${message4(error)}`, { cause: error });
|
|
17892
18508
|
}
|
|
@@ -17905,11 +18521,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
17905
18521
|
}
|
|
17906
18522
|
const ids = parsed.data.map((entry) => entry.id);
|
|
17907
18523
|
if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
|
|
17908
|
-
const root =
|
|
18524
|
+
const root = path41.dirname(path41.resolve(inputsFilePath));
|
|
17909
18525
|
return parsed.data.map((entry) => ({
|
|
17910
18526
|
id: entry.id,
|
|
17911
|
-
file:
|
|
17912
|
-
...entry.record ? { record:
|
|
18527
|
+
file: path41.resolve(root, entry.path),
|
|
18528
|
+
...entry.record ? { record: path41.resolve(root, entry.record) } : {},
|
|
17913
18529
|
tolerances: QualityToleranceSchema.parse({
|
|
17914
18530
|
...DEFAULT_QUALITY_TOLERANCES,
|
|
17915
18531
|
...entry.tolerances
|
|
@@ -17917,7 +18533,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
|
|
|
17917
18533
|
}));
|
|
17918
18534
|
}
|
|
17919
18535
|
function portableRelative2(from, to) {
|
|
17920
|
-
return
|
|
18536
|
+
return path41.relative(from, path41.resolve(to)).split(path41.sep).join("/") || ".";
|
|
17921
18537
|
}
|
|
17922
18538
|
async function verifyRecordForImage(recordPath, imagePath) {
|
|
17923
18539
|
const verification = await verifyArtifactBundle(recordPath);
|
|
@@ -17932,20 +18548,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
|
|
|
17932
18548
|
if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
|
|
17933
18549
|
throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
|
|
17934
18550
|
}
|
|
17935
|
-
const outputs = record.outputs.map((output) =>
|
|
17936
|
-
if (!outputs.includes(
|
|
18551
|
+
const outputs = record.outputs.map((output) => path41.resolve(path41.dirname(recordPath), output.path));
|
|
18552
|
+
if (!outputs.includes(path41.resolve(imagePath))) {
|
|
17937
18553
|
throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
|
|
17938
18554
|
}
|
|
17939
18555
|
}
|
|
17940
18556
|
async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
17941
18557
|
if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
|
|
17942
|
-
const absolute =
|
|
18558
|
+
const absolute = path41.resolve(baselinePath);
|
|
17943
18559
|
for (const input of inputs) {
|
|
17944
|
-
if (
|
|
18560
|
+
if (path41.resolve(input.file) === absolute || input.record && path41.resolve(input.record) === absolute) {
|
|
17945
18561
|
throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
|
|
17946
18562
|
}
|
|
17947
18563
|
}
|
|
17948
|
-
const root =
|
|
18564
|
+
const root = path41.dirname(absolute);
|
|
17949
18565
|
const cases = [];
|
|
17950
18566
|
for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
17951
18567
|
const expected = await measureImageQuality(input.file);
|
|
@@ -17972,8 +18588,8 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
17972
18588
|
cases
|
|
17973
18589
|
});
|
|
17974
18590
|
const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
|
|
17975
|
-
if (
|
|
17976
|
-
const current = await
|
|
18591
|
+
if (existsSync30(absolute) && !options.force) {
|
|
18592
|
+
const current = await readFile27(absolute);
|
|
17977
18593
|
if (!current.equals(data)) {
|
|
17978
18594
|
throw new OverwriteRefusedError(
|
|
17979
18595
|
`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
|
|
@@ -17985,10 +18601,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
|
|
|
17985
18601
|
return { baseline, path: absolute, changed: result.changed.length > 0 };
|
|
17986
18602
|
}
|
|
17987
18603
|
async function readQualityBaseline(baselinePath) {
|
|
17988
|
-
const absolute =
|
|
18604
|
+
const absolute = path41.resolve(baselinePath);
|
|
17989
18605
|
let raw;
|
|
17990
18606
|
try {
|
|
17991
|
-
raw = JSON.parse(await
|
|
18607
|
+
raw = JSON.parse(await readFile27(absolute, "utf8"));
|
|
17992
18608
|
} catch (error) {
|
|
17993
18609
|
throw new Error(`Could not read quality baseline ${absolute}: ${message4(error)}`, { cause: error });
|
|
17994
18610
|
}
|
|
@@ -18058,12 +18674,12 @@ function regressionViolations(expected, actual, tolerances) {
|
|
|
18058
18674
|
return { violations, warnings };
|
|
18059
18675
|
}
|
|
18060
18676
|
async function checkQualityBaseline(baselinePath) {
|
|
18061
|
-
const absolute =
|
|
18677
|
+
const absolute = path41.resolve(baselinePath);
|
|
18062
18678
|
const baseline = await readQualityBaseline(absolute);
|
|
18063
|
-
const root =
|
|
18679
|
+
const root = path41.dirname(absolute);
|
|
18064
18680
|
const cases = [];
|
|
18065
18681
|
for (const entry of baseline.cases) {
|
|
18066
|
-
const file =
|
|
18682
|
+
const file = path41.resolve(root, entry.file);
|
|
18067
18683
|
let actual = null;
|
|
18068
18684
|
const violations = [];
|
|
18069
18685
|
const warnings = [];
|
|
@@ -18076,7 +18692,7 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
18076
18692
|
violations.push(message4(error));
|
|
18077
18693
|
}
|
|
18078
18694
|
if (entry.record) {
|
|
18079
|
-
const recordPath =
|
|
18695
|
+
const recordPath = path41.resolve(root, entry.record.path);
|
|
18080
18696
|
try {
|
|
18081
18697
|
const recordHash = await sha256File(recordPath);
|
|
18082
18698
|
if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
|
|
@@ -18118,15 +18734,15 @@ async function runQuality(args) {
|
|
|
18118
18734
|
if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
|
|
18119
18735
|
let raw;
|
|
18120
18736
|
try {
|
|
18121
|
-
raw = JSON.parse(await
|
|
18737
|
+
raw = JSON.parse(await readFile28(path42.resolve(args.inputs), "utf8"));
|
|
18122
18738
|
} catch (error) {
|
|
18123
18739
|
throw new Error(
|
|
18124
|
-
`Could not read quality inputs ${
|
|
18740
|
+
`Could not read quality inputs ${path42.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
18125
18741
|
{ cause: error }
|
|
18126
18742
|
);
|
|
18127
18743
|
}
|
|
18128
18744
|
const inputs = resolveQualityInputs(raw, args.inputs);
|
|
18129
|
-
if (
|
|
18745
|
+
if (path42.resolve(args.inputs) === path42.resolve(args.out)) {
|
|
18130
18746
|
throw new Error("quality snapshot --out must not overwrite its --inputs file.");
|
|
18131
18747
|
}
|
|
18132
18748
|
const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
|
|
@@ -18139,7 +18755,7 @@ async function runQuality(args) {
|
|
|
18139
18755
|
}, null, 2));
|
|
18140
18756
|
} else {
|
|
18141
18757
|
log(
|
|
18142
|
-
` ${result.changed ? "wrote" : "unchanged"} ${
|
|
18758
|
+
` ${result.changed ? "wrote" : "unchanged"} ${path42.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
|
|
18143
18759
|
);
|
|
18144
18760
|
log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
|
|
18145
18761
|
}
|
|
@@ -18165,12 +18781,12 @@ async function runQuality(args) {
|
|
|
18165
18781
|
}
|
|
18166
18782
|
|
|
18167
18783
|
// src/cli/commands/recipe.ts
|
|
18168
|
-
import
|
|
18784
|
+
import path44 from "path";
|
|
18169
18785
|
|
|
18170
18786
|
// src/recipes.ts
|
|
18171
|
-
import
|
|
18172
|
-
import { existsSync as
|
|
18173
|
-
import { readdir as readdir3, readFile as
|
|
18787
|
+
import path43 from "path";
|
|
18788
|
+
import { existsSync as existsSync31 } from "fs";
|
|
18789
|
+
import { readdir as readdir3, readFile as readFile29 } from "fs/promises";
|
|
18174
18790
|
import { fileURLToPath } from "url";
|
|
18175
18791
|
import { z as z10 } from "zod";
|
|
18176
18792
|
var SHA256_RE2 = /^[0-9a-f]{64}$/;
|
|
@@ -18179,7 +18795,7 @@ var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
|
18179
18795
|
var RECIPE_FILE = "pixelkiln.recipe.json";
|
|
18180
18796
|
var RECIPE_DIR_TOKEN = "{{recipeDir}}";
|
|
18181
18797
|
var PortablePathSchema = z10.string().min(1).refine((value) => {
|
|
18182
|
-
if (value.includes("\\") ||
|
|
18798
|
+
if (value.includes("\\") || path43.posix.isAbsolute(value)) return false;
|
|
18183
18799
|
const parts = value.split("/");
|
|
18184
18800
|
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
18185
18801
|
}, "expected a portable relative path without '.', '..', or backslashes");
|
|
@@ -18355,10 +18971,10 @@ function message5(error) {
|
|
|
18355
18971
|
return error instanceof Error ? error.message : String(error);
|
|
18356
18972
|
}
|
|
18357
18973
|
async function readRecipeFile(recipePath, bundled) {
|
|
18358
|
-
const absolute =
|
|
18974
|
+
const absolute = path43.resolve(recipePath);
|
|
18359
18975
|
let raw;
|
|
18360
18976
|
try {
|
|
18361
|
-
raw = JSON.parse(await
|
|
18977
|
+
raw = JSON.parse(await readFile29(absolute, "utf8"));
|
|
18362
18978
|
} catch (error) {
|
|
18363
18979
|
throw new Error(`Could not read recipe ${absolute}: ${message5(error)}`, { cause: error });
|
|
18364
18980
|
}
|
|
@@ -18369,13 +18985,13 @@ async function readRecipeFile(recipePath, bundled) {
|
|
|
18369
18985
|
` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
|
|
18370
18986
|
);
|
|
18371
18987
|
}
|
|
18372
|
-
return { recipe: parsed.data, authored: raw, path: absolute, dir:
|
|
18988
|
+
return { recipe: parsed.data, authored: raw, path: absolute, dir: path43.dirname(absolute), bundled };
|
|
18373
18989
|
}
|
|
18374
18990
|
async function walkRecipeFiles(root) {
|
|
18375
|
-
if (!
|
|
18991
|
+
if (!existsSync31(root)) return [];
|
|
18376
18992
|
const out = [];
|
|
18377
18993
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
18378
|
-
const resolved =
|
|
18994
|
+
const resolved = path43.join(root, entry.name);
|
|
18379
18995
|
if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
|
|
18380
18996
|
else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
|
|
18381
18997
|
}
|
|
@@ -18386,7 +19002,7 @@ function bundledRecipeRoot() {
|
|
|
18386
19002
|
}
|
|
18387
19003
|
async function listBundledRecipes(root = bundledRecipeRoot()) {
|
|
18388
19004
|
const recipes = await Promise.all(
|
|
18389
|
-
(await walkRecipeFiles(
|
|
19005
|
+
(await walkRecipeFiles(path43.resolve(root))).map((file) => readRecipeFile(file, true))
|
|
18390
19006
|
);
|
|
18391
19007
|
return recipes.sort(
|
|
18392
19008
|
(a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
|
|
@@ -18409,9 +19025,9 @@ function parseSelector(selector) {
|
|
|
18409
19025
|
return { id: match[1], version: match[2] };
|
|
18410
19026
|
}
|
|
18411
19027
|
async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
18412
|
-
const local =
|
|
18413
|
-
if (
|
|
18414
|
-
const recipePath =
|
|
19028
|
+
const local = path43.resolve(target);
|
|
19029
|
+
if (existsSync31(local)) {
|
|
19030
|
+
const recipePath = path43.basename(local) === RECIPE_FILE ? local : path43.join(local, RECIPE_FILE);
|
|
18415
19031
|
return readRecipeFile(recipePath, false);
|
|
18416
19032
|
}
|
|
18417
19033
|
if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
|
|
@@ -18425,8 +19041,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
|
|
|
18425
19041
|
return matches[0];
|
|
18426
19042
|
}
|
|
18427
19043
|
async function verifyFile(root, file) {
|
|
18428
|
-
const absolute =
|
|
18429
|
-
if (!
|
|
19044
|
+
const absolute = path43.resolve(root, ...file.path.split("/"));
|
|
19045
|
+
if (!existsSync31(absolute)) {
|
|
18430
19046
|
return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
|
|
18431
19047
|
}
|
|
18432
19048
|
const actualSha256 = await sha256File(absolute);
|
|
@@ -18441,7 +19057,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
18441
19057
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
18442
19058
|
const actualIntegrity = recipeDigest(loaded.authored);
|
|
18443
19059
|
const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
|
|
18444
|
-
const modelRoot = options.modelRoot ?
|
|
19060
|
+
const modelRoot = options.modelRoot ? path43.resolve(options.modelRoot) : null;
|
|
18445
19061
|
const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
|
|
18446
19062
|
...await verifyFile(modelRoot, model),
|
|
18447
19063
|
source: model.source,
|
|
@@ -18475,7 +19091,7 @@ async function verifyRecipe(target, options = {}) {
|
|
|
18475
19091
|
};
|
|
18476
19092
|
}
|
|
18477
19093
|
function renderStyle(style, destination, cwd) {
|
|
18478
|
-
const portableDir =
|
|
19094
|
+
const portableDir = path43.relative(cwd, destination).split(path43.sep).join("/") || ".";
|
|
18479
19095
|
const replace = (value) => {
|
|
18480
19096
|
if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
|
|
18481
19097
|
if (Array.isArray(value)) return value.map(replace);
|
|
@@ -18487,25 +19103,25 @@ function renderStyle(style, destination, cwd) {
|
|
|
18487
19103
|
return replace(style);
|
|
18488
19104
|
}
|
|
18489
19105
|
async function installRecipe(target, options = {}) {
|
|
18490
|
-
const cwd =
|
|
19106
|
+
const cwd = path43.resolve(options.cwd ?? process.cwd());
|
|
18491
19107
|
const loaded = await resolveRecipe(target, options.bundledRoot);
|
|
18492
19108
|
const verification = await verifyRecipe(loaded.path);
|
|
18493
19109
|
if (!verification.ok) {
|
|
18494
19110
|
throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
|
|
18495
19111
|
}
|
|
18496
|
-
const destination = options.out ?
|
|
19112
|
+
const destination = options.out ? path43.resolve(cwd, options.out) : path43.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
|
|
18497
19113
|
const sources = [
|
|
18498
19114
|
{ path: RECIPE_FILE, source: loaded.path },
|
|
18499
|
-
...loaded.recipe.files.map((file) => ({ path: file.path, source:
|
|
19115
|
+
...loaded.recipe.files.map((file) => ({ path: file.path, source: path43.join(loaded.dir, ...file.path.split("/")) }))
|
|
18500
19116
|
];
|
|
18501
19117
|
const files = await Promise.all(sources.map(async (file) => ({
|
|
18502
|
-
path:
|
|
18503
|
-
data: await
|
|
19118
|
+
path: path43.join(destination, ...file.path.split("/")),
|
|
19119
|
+
data: await readFile29(file.source)
|
|
18504
19120
|
})));
|
|
18505
19121
|
if (!options.force) {
|
|
18506
19122
|
for (const file of files) {
|
|
18507
|
-
if (!
|
|
18508
|
-
const current = await
|
|
19123
|
+
if (!existsSync31(file.path)) continue;
|
|
19124
|
+
const current = await readFile29(file.path);
|
|
18509
19125
|
if (!current.equals(file.data)) {
|
|
18510
19126
|
throw new OverwriteRefusedError(
|
|
18511
19127
|
`Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
|
|
@@ -18599,14 +19215,14 @@ async function runRecipe(args) {
|
|
|
18599
19215
|
}, null, 2));
|
|
18600
19216
|
} else {
|
|
18601
19217
|
log(` installed ${result.recipe.id}@${result.recipe.version}`);
|
|
18602
|
-
log(` ${
|
|
19218
|
+
log(` ${path44.relative(process.cwd(), result.destination) || "."}`);
|
|
18603
19219
|
log(`
|
|
18604
19220
|
Add this entry under your manifest's styles object:`);
|
|
18605
19221
|
log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
|
|
18606
19222
|
if (result.recipe.models.length) {
|
|
18607
19223
|
log(`
|
|
18608
19224
|
Models are not downloaded automatically. Verify them with:`);
|
|
18609
|
-
log(` pixelkiln recipe verify ${
|
|
19225
|
+
log(` pixelkiln recipe verify ${path44.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
|
|
18610
19226
|
}
|
|
18611
19227
|
}
|
|
18612
19228
|
return;
|
|
@@ -18614,7 +19230,7 @@ async function runRecipe(args) {
|
|
|
18614
19230
|
}
|
|
18615
19231
|
|
|
18616
19232
|
// src/cli/commands/refine.ts
|
|
18617
|
-
import
|
|
19233
|
+
import path45 from "path";
|
|
18618
19234
|
async function runRefine(args) {
|
|
18619
19235
|
if (args.from || args.subcommand === "approve") {
|
|
18620
19236
|
if (!args.from) {
|
|
@@ -18644,12 +19260,12 @@ async function runRefine(args) {
|
|
|
18644
19260
|
` recovered ${result3.detection.columns}x${result3.detection.rows} native grid (${result3.detection.confidence}: ${result3.detection.consensus})`
|
|
18645
19261
|
);
|
|
18646
19262
|
log(` enforced ${result3.palette.length}-color palette without dithering`);
|
|
18647
|
-
log(` wrote ${
|
|
18648
|
-
log(` quality record: ${
|
|
19263
|
+
log(` wrote ${path45.relative(process.cwd(), result3.output)}`);
|
|
19264
|
+
log(` quality record: ${path45.relative(process.cwd(), result3.record)}`);
|
|
18649
19265
|
log(`
|
|
18650
19266
|
Automated checks passed. Human 1\xD7 review is still required:`);
|
|
18651
19267
|
log(
|
|
18652
|
-
` pixelkiln refine approve --from ${
|
|
19268
|
+
` pixelkiln refine approve --from ${path45.relative(process.cwd(), result3.record)} --reviewer "Your Name"`
|
|
18653
19269
|
);
|
|
18654
19270
|
}
|
|
18655
19271
|
return;
|
|
@@ -18677,7 +19293,7 @@ async function runRefine(args) {
|
|
|
18677
19293
|
if (args.json) log(JSON.stringify(result3, null, 2));
|
|
18678
19294
|
else {
|
|
18679
19295
|
log(
|
|
18680
|
-
result3.options.frameSet ? ` approved ${result3.outputs.length} frames in ${
|
|
19296
|
+
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
19297
|
);
|
|
18682
19298
|
}
|
|
18683
19299
|
return;
|
|
@@ -18686,7 +19302,7 @@ async function runRefine(args) {
|
|
|
18686
19302
|
if (args.json) {
|
|
18687
19303
|
log(JSON.stringify(result2, null, 2));
|
|
18688
19304
|
} else {
|
|
18689
|
-
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${
|
|
19305
|
+
log(` ${result2.safe ? "release-ready" : "not release-ready"}: ${path45.relative(process.cwd(), result2.output || result2.record)}`);
|
|
18690
19306
|
for (const reason of result2.reasons) log(` ${reason}`);
|
|
18691
19307
|
}
|
|
18692
19308
|
if (!result2.safe) process.exitCode = 1;
|
|
@@ -18732,7 +19348,7 @@ async function runRefine(args) {
|
|
|
18732
19348
|
log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
|
|
18733
19349
|
if (item.state === "needs-approval") {
|
|
18734
19350
|
log(
|
|
18735
|
-
` pixelkiln refine approve --from ${
|
|
19351
|
+
` pixelkiln refine approve --from ${path45.relative(process.cwd(), item.record)} --reviewer "Your Name"`
|
|
18736
19352
|
);
|
|
18737
19353
|
}
|
|
18738
19354
|
}
|
|
@@ -18784,15 +19400,15 @@ async function runTools(args) {
|
|
|
18784
19400
|
}
|
|
18785
19401
|
|
|
18786
19402
|
// src/cli/commands/workspace.ts
|
|
18787
|
-
import
|
|
18788
|
-
import { existsSync as
|
|
19403
|
+
import path46 from "path";
|
|
19404
|
+
import { existsSync as existsSync32 } from "fs";
|
|
18789
19405
|
async function runWorkspace(args) {
|
|
18790
|
-
const workspacePath =
|
|
18791
|
-
const dir =
|
|
19406
|
+
const workspacePath = path46.resolve(args.workspace ?? "pixelkiln.workspace.json");
|
|
19407
|
+
const dir = path46.dirname(workspacePath);
|
|
18792
19408
|
if (args.subcommand === "add") {
|
|
18793
|
-
const manifestPath =
|
|
19409
|
+
const manifestPath = path46.resolve(args.target);
|
|
18794
19410
|
const loadedTarget = await loadManifest(manifestPath);
|
|
18795
|
-
const lockPath = args.explicitLock ?
|
|
19411
|
+
const lockPath = args.explicitLock ? path46.resolve(args.explicitLock) : path46.join(path46.dirname(manifestPath), "pixelkiln.lock.json");
|
|
18796
19412
|
const ws = await loadWorkspace(workspacePath);
|
|
18797
19413
|
const id = args.name ?? loadedTarget.manifest.name;
|
|
18798
19414
|
if (ws.projects.some((p) => p.id === id)) {
|
|
@@ -18812,10 +19428,10 @@ async function runWorkspace(args) {
|
|
|
18812
19428
|
...args.account ? { account: args.account } : {}
|
|
18813
19429
|
};
|
|
18814
19430
|
await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
|
|
18815
|
-
log(` registered "${id}" in ${
|
|
19431
|
+
log(` registered "${id}" in ${path46.relative(process.cwd(), workspacePath)}`);
|
|
18816
19432
|
log(` manifest: ${project.manifest}`);
|
|
18817
19433
|
log(` lock: ${project.lock}`);
|
|
18818
|
-
if (!
|
|
19434
|
+
if (!existsSync32(lockPath)) {
|
|
18819
19435
|
log(
|
|
18820
19436
|
` warning: no lockfile there yet; this project contributes no claims until one is generated`
|
|
18821
19437
|
);
|
|
@@ -18824,7 +19440,7 @@ async function runWorkspace(args) {
|
|
|
18824
19440
|
}
|
|
18825
19441
|
if (args.subcommand === "remove") {
|
|
18826
19442
|
const ws = await loadWorkspace(workspacePath);
|
|
18827
|
-
const resolvedTarget =
|
|
19443
|
+
const resolvedTarget = path46.resolve(args.target);
|
|
18828
19444
|
const match = ws.projects.find(
|
|
18829
19445
|
(p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
|
|
18830
19446
|
);
|
|
@@ -18835,11 +19451,11 @@ async function runWorkspace(args) {
|
|
|
18835
19451
|
version: 1,
|
|
18836
19452
|
projects: ws.projects.filter((p) => p !== match)
|
|
18837
19453
|
});
|
|
18838
|
-
log(` removed "${match.id}" from ${
|
|
19454
|
+
log(` removed "${match.id}" from ${path46.relative(process.cwd(), workspacePath)}`);
|
|
18839
19455
|
return;
|
|
18840
19456
|
}
|
|
18841
19457
|
if (args.subcommand === "list") {
|
|
18842
|
-
if (!
|
|
19458
|
+
if (!existsSync32(workspacePath)) {
|
|
18843
19459
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
18844
19460
|
}
|
|
18845
19461
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -18847,9 +19463,9 @@ async function runWorkspace(args) {
|
|
|
18847
19463
|
if (args.json) {
|
|
18848
19464
|
log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
|
|
18849
19465
|
} else if (!ws.projects.length) {
|
|
18850
|
-
log(` no projects registered in ${
|
|
19466
|
+
log(` no projects registered in ${path46.relative(process.cwd(), workspacePath)}`);
|
|
18851
19467
|
} else {
|
|
18852
|
-
log(` ${ws.projects.length} project(s) in ${
|
|
19468
|
+
log(` ${ws.projects.length} project(s) in ${path46.relative(process.cwd(), workspacePath)}:`);
|
|
18853
19469
|
for (const p of ws.projects) {
|
|
18854
19470
|
log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
|
|
18855
19471
|
}
|
|
@@ -18859,7 +19475,7 @@ async function runWorkspace(args) {
|
|
|
18859
19475
|
return;
|
|
18860
19476
|
}
|
|
18861
19477
|
if (args.subcommand === "status") {
|
|
18862
|
-
if (!
|
|
19478
|
+
if (!existsSync32(workspacePath)) {
|
|
18863
19479
|
throw new Error(`Workspace catalog not found: ${workspacePath}`);
|
|
18864
19480
|
}
|
|
18865
19481
|
const ws = await loadWorkspace(workspacePath);
|
|
@@ -18867,7 +19483,7 @@ async function runWorkspace(args) {
|
|
|
18867
19483
|
if (args.json) {
|
|
18868
19484
|
log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
|
|
18869
19485
|
} else {
|
|
18870
|
-
log(` workspace: ${
|
|
19486
|
+
log(` workspace: ${path46.relative(process.cwd(), workspacePath)}`);
|
|
18871
19487
|
for (const p of report.projects) {
|
|
18872
19488
|
if (p.error) {
|
|
18873
19489
|
log(`
|
|
@@ -18921,8 +19537,8 @@ async function runHelp() {
|
|
|
18921
19537
|
async function runVersion() {
|
|
18922
19538
|
for (const up of ["../package.json", "../../package.json"]) {
|
|
18923
19539
|
const url = new URL(up, import.meta.url);
|
|
18924
|
-
if (!
|
|
18925
|
-
const pkg = JSON.parse(await
|
|
19540
|
+
if (!existsSync33(url)) continue;
|
|
19541
|
+
const pkg = JSON.parse(await readFile30(url, "utf8"));
|
|
18926
19542
|
if (pkg.name === "pixelkiln") {
|
|
18927
19543
|
log(`${pkg.name} ${pkg.version}`);
|
|
18928
19544
|
return;
|
|
@@ -18965,7 +19581,9 @@ var RUNNERS = {
|
|
|
18965
19581
|
quality: runQuality,
|
|
18966
19582
|
refine: runRefine,
|
|
18967
19583
|
recipe: runRecipe,
|
|
18968
|
-
workspace: runWorkspace
|
|
19584
|
+
workspace: runWorkspace,
|
|
19585
|
+
unzoom: runUnzoom,
|
|
19586
|
+
font: runFont
|
|
18969
19587
|
};
|
|
18970
19588
|
async function main(argv = process.argv.slice(2)) {
|
|
18971
19589
|
const args = parseArgs(argv);
|