pixelkiln 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROVIDERS.md +20 -15
- package/README.md +10 -9
- package/dist/cli.js +1208 -571
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1204 -570
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2053 -229
- package/dist/index.d.ts +2053 -229
- package/dist/index.js +1198 -569
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +1 -1
- package/docs/ARCHITECTURE.md +30 -19
- package/docs/ARTIFACTS.md +9 -4
- package/docs/CLI.md +12 -8
- package/docs/COMFYUI.md +89 -10
- package/docs/GENERATORS.md +16 -0
- package/docs/GETTING_STARTED.md +7 -1
- package/docs/LIBRARY.md +18 -2
- package/docs/MANIFEST.md +76 -10
- package/docs/QUALITY.md +11 -3
- package/docs/README.md +2 -2
- package/package.json +1 -1
- package/schema/manifest.schema.json +414 -171
- package/schema/recipe.schema.json +7 -1
- package/skills/pixelkiln/SKILL.md +5 -0
- package/skills/pixelkiln/references/comfyui.md +11 -3
- package/skills/pixelkiln/references/quality.md +7 -2
package/dist/cli.js
CHANGED
|
@@ -759,6 +759,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
759
759
|
identity: {
|
|
760
760
|
outputNodeId: options.outputNodeId,
|
|
761
761
|
numImages: options.numImages,
|
|
762
|
+
...options.frames ? { frames: options.frames } : {},
|
|
762
763
|
bindings: options.bindings,
|
|
763
764
|
workflowSha256
|
|
764
765
|
}
|
|
@@ -783,56 +784,80 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
783
784
|
`ComfyUI providerInputs.${name} has no matching bindings.${name} target`
|
|
784
785
|
);
|
|
785
786
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
787
|
+
const resolveValue = async (item, index) => {
|
|
788
|
+
const label = `providerInputs.${name}${index == null ? "" : `[${index}]`}`;
|
|
789
|
+
if (isImageBinding(options.workflow, binding)) {
|
|
790
|
+
if (typeof item !== "string") {
|
|
791
|
+
throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
|
|
792
|
+
}
|
|
793
|
+
const file = path2.resolve(context.root, item);
|
|
794
|
+
let bytes;
|
|
795
|
+
try {
|
|
796
|
+
bytes = await readFile(file);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
throw new Error(
|
|
799
|
+
`ComfyUI ${label} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
const format = inputImageFormat(bytes);
|
|
803
|
+
if (!format) throw new Error(`ComfyUI ${label} is not a PNG or JPEG: ${file}`);
|
|
804
|
+
const hash = sha256(bytes);
|
|
805
|
+
return {
|
|
806
|
+
runtime: { kind: "image", path: file, sha256: hash, format },
|
|
807
|
+
stable: { kind: "image", sha256: hash, format }
|
|
808
|
+
};
|
|
789
809
|
}
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
try {
|
|
793
|
-
bytes = await readFile(file);
|
|
794
|
-
} catch (error) {
|
|
810
|
+
const current = options.workflow[binding.nodeId].inputs[binding.input];
|
|
811
|
+
if (typeof current !== "string" && typeof current !== "number" && typeof current !== "boolean") {
|
|
795
812
|
throw new Error(
|
|
796
|
-
`ComfyUI
|
|
813
|
+
`ComfyUI ${label} cannot replace non-scalar ${binding.nodeId}.${binding.input}`
|
|
797
814
|
);
|
|
798
815
|
}
|
|
799
|
-
|
|
800
|
-
if (!format) {
|
|
801
|
-
throw new Error(`ComfyUI providerInputs.${name} is not a PNG or JPEG: ${file}`);
|
|
802
|
-
}
|
|
803
|
-
const hash = sha256(bytes);
|
|
804
|
-
inputs[name] = { kind: "image", path: file, sha256: hash, format };
|
|
805
|
-
identity[name] = { kind: "image", sha256: hash, format };
|
|
806
|
-
} else {
|
|
807
|
-
const current = options.workflow[binding.nodeId].inputs[binding.input];
|
|
808
|
-
if (typeof current !== "string" && typeof current !== "number" && typeof current !== "boolean") {
|
|
816
|
+
if (typeof current !== typeof item) {
|
|
809
817
|
throw new Error(
|
|
810
|
-
`ComfyUI
|
|
818
|
+
`ComfyUI ${label} must be ${typeof current} to match ${binding.nodeId}.${binding.input}`
|
|
811
819
|
);
|
|
812
820
|
}
|
|
813
|
-
|
|
821
|
+
return { runtime: item, stable: item };
|
|
822
|
+
};
|
|
823
|
+
if (Array.isArray(input)) {
|
|
824
|
+
if (!options.frames || name !== options.frames.vary) {
|
|
814
825
|
throw new Error(
|
|
815
|
-
`ComfyUI providerInputs.${name}
|
|
826
|
+
`ComfyUI providerInputs.${name} is an array but frames.vary is ${options.frames ? `"${options.frames.vary}"` : "not configured"}`
|
|
816
827
|
);
|
|
817
828
|
}
|
|
818
|
-
|
|
819
|
-
|
|
829
|
+
const resolved = await Promise.all(input.map((item, index) => resolveValue(item, index)));
|
|
830
|
+
inputs[name] = resolved.map((item) => item.runtime);
|
|
831
|
+
identity[name] = resolved.map((item) => item.stable);
|
|
832
|
+
} else {
|
|
833
|
+
const resolved = await resolveValue(input);
|
|
834
|
+
inputs[name] = resolved.runtime;
|
|
835
|
+
identity[name] = resolved.stable;
|
|
820
836
|
}
|
|
821
837
|
}
|
|
822
838
|
return { inputs, identity };
|
|
823
839
|
}
|
|
824
840
|
supports(generator) {
|
|
825
|
-
return generator === "map";
|
|
841
|
+
return generator === "map" || generator === "frames";
|
|
826
842
|
}
|
|
827
843
|
supportsRevision(_mode) {
|
|
828
844
|
return true;
|
|
829
845
|
}
|
|
830
846
|
estimate(spec) {
|
|
831
|
-
return {
|
|
847
|
+
return {
|
|
848
|
+
unit: "free",
|
|
849
|
+
amount: 0,
|
|
850
|
+
candidates: spec.generator === "frames" ? 1 : resolvedOptions(spec).numImages
|
|
851
|
+
};
|
|
832
852
|
}
|
|
833
853
|
validate(spec, styleImages) {
|
|
834
854
|
const options = resolvedOptions(spec);
|
|
835
|
-
if (spec.source && !spec.revision)
|
|
855
|
+
if (spec.source && !spec.revision) {
|
|
856
|
+
if (spec.generator === "frames") {
|
|
857
|
+
throw new Error("ComfyUI frame sets require generated provider outputs, not asset.source");
|
|
858
|
+
}
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
836
861
|
if (spec.width < 16 || spec.height < 16 || spec.width > 4096 || spec.height > 4096) {
|
|
837
862
|
throw new Error("ComfyUI output dimensions must be between 16 and 4096 pixels");
|
|
838
863
|
}
|
|
@@ -854,6 +879,25 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
854
879
|
if (options.numImages > 1 && !options.bindings.batchSize) {
|
|
855
880
|
throw new Error("ComfyUI numImages greater than 1 requires bindings.batchSize");
|
|
856
881
|
}
|
|
882
|
+
if (spec.generator === "frames") {
|
|
883
|
+
if (!options.frames) throw new Error("ComfyUI frames generation requires providerOptions.comfyui.frames");
|
|
884
|
+
if (options.numImages !== 1) throw new Error("ComfyUI frames generation requires numImages: 1");
|
|
885
|
+
if (spec.revision) throw new Error("ComfyUI frame sets do not support asset revisions yet");
|
|
886
|
+
const values = spec.providerInputs?.[options.frames.vary];
|
|
887
|
+
if (!Array.isArray(values) || values.length < 2 || values.length > 64) {
|
|
888
|
+
throw new Error(
|
|
889
|
+
`ComfyUI frames.vary "${options.frames.vary}" requires a providerInputs array of 2\u201364 values`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (options.frames.seedStep !== 0 && (spec.seed == null || !options.bindings.seed)) {
|
|
893
|
+
throw new Error("ComfyUI frames.seedStep requires a style seed and bindings.seed");
|
|
894
|
+
}
|
|
895
|
+
if (spec.seed != null && !Number.isSafeInteger(spec.seed + (values.length - 1) * options.frames.seedStep)) {
|
|
896
|
+
throw new Error("ComfyUI frame seed schedule exceeds JavaScript's safe integer range");
|
|
897
|
+
}
|
|
898
|
+
} else if (options.frames) {
|
|
899
|
+
throw new Error("ComfyUI providerOptions.frames requires generator: frames");
|
|
900
|
+
}
|
|
857
901
|
if (spec.revision) {
|
|
858
902
|
if (!options.bindings.sourceImage) {
|
|
859
903
|
throw new Error("ComfyUI revisions require bindings.sourceImage");
|
|
@@ -878,30 +922,64 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
878
922
|
async submit(spec, styleImages) {
|
|
879
923
|
this.validate(spec, styleImages);
|
|
880
924
|
const options = resolvedOptions(spec);
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
if (options.bindings.width) setBinding(workflow, options.bindings.width, spec.width);
|
|
884
|
-
if (options.bindings.height) setBinding(workflow, options.bindings.height, spec.height);
|
|
885
|
-
if (options.bindings.batchSize) {
|
|
886
|
-
setBinding(workflow, options.bindings.batchSize, options.numImages);
|
|
887
|
-
}
|
|
888
|
-
if (spec.seed != null && options.bindings.seed) {
|
|
889
|
-
setBinding(workflow, options.bindings.seed, spec.seed);
|
|
890
|
-
}
|
|
891
|
-
for (const [name, input] of Object.entries(spec.providerInputs ?? {})) {
|
|
925
|
+
const uploadedImages = /* @__PURE__ */ new Map();
|
|
926
|
+
const bindInput = async (workflow2, name, input) => {
|
|
892
927
|
const binding = Object.hasOwn(options.bindings, name) ? options.bindings[name] : void 0;
|
|
893
928
|
if (!binding) throw new Error(`ComfyUI provider input "${name}" has no binding`);
|
|
894
|
-
if (isComfyImageInput(input)) {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
throw new Error(`ComfyUI provider input "${name}" changed before upload`);
|
|
898
|
-
}
|
|
899
|
-
const uploaded = await this.client.uploadImage(bytes, input.sha256, input.format);
|
|
900
|
-
setBinding(workflow, binding, uploaded);
|
|
901
|
-
} else {
|
|
902
|
-
setBinding(workflow, binding, input);
|
|
929
|
+
if (!isComfyImageInput(input)) {
|
|
930
|
+
setBinding(workflow2, binding, input);
|
|
931
|
+
return;
|
|
903
932
|
}
|
|
933
|
+
const bytes = await readFile(input.path);
|
|
934
|
+
if (sha256(bytes) !== input.sha256) {
|
|
935
|
+
throw new Error(`ComfyUI provider input "${name}" changed before upload`);
|
|
936
|
+
}
|
|
937
|
+
let uploaded = uploadedImages.get(input.sha256);
|
|
938
|
+
if (!uploaded) {
|
|
939
|
+
uploaded = await this.client.uploadImage(bytes, input.sha256, input.format);
|
|
940
|
+
uploadedImages.set(input.sha256, uploaded);
|
|
941
|
+
}
|
|
942
|
+
setBinding(workflow2, binding, uploaded);
|
|
943
|
+
};
|
|
944
|
+
const buildWorkflow = async (frameIndex) => {
|
|
945
|
+
const workflow2 = structuredClone(options.workflow);
|
|
946
|
+
setBinding(workflow2, options.bindings.prompt, spec.prompt);
|
|
947
|
+
if (options.bindings.width) setBinding(workflow2, options.bindings.width, spec.width);
|
|
948
|
+
if (options.bindings.height) setBinding(workflow2, options.bindings.height, spec.height);
|
|
949
|
+
if (options.bindings.batchSize) setBinding(workflow2, options.bindings.batchSize, options.numImages);
|
|
950
|
+
if (spec.seed != null && options.bindings.seed) {
|
|
951
|
+
const seed = frameIndex == null ? spec.seed : spec.seed + frameIndex * (options.frames?.seedStep ?? 0);
|
|
952
|
+
setBinding(workflow2, options.bindings.seed, seed);
|
|
953
|
+
}
|
|
954
|
+
for (const [name, input] of Object.entries(spec.providerInputs ?? {})) {
|
|
955
|
+
const selected = Array.isArray(input) ? frameIndex == null ? input : input[frameIndex] : input;
|
|
956
|
+
await bindInput(workflow2, name, selected);
|
|
957
|
+
}
|
|
958
|
+
return workflow2;
|
|
959
|
+
};
|
|
960
|
+
const inputs = providerInputProvenance(spec.providerInputs);
|
|
961
|
+
if (spec.generator === "frames") {
|
|
962
|
+
const frames = options.frames;
|
|
963
|
+
const values = spec.providerInputs[frames.vary];
|
|
964
|
+
const promptIds = [];
|
|
965
|
+
for (let index = 0; index < values.length; index++) {
|
|
966
|
+
promptIds.push(await this.client.submit(await buildWorkflow(index)));
|
|
967
|
+
}
|
|
968
|
+
return {
|
|
969
|
+
jobId: encodeFrameSetJob({ promptIds, outputNodeId: options.outputNodeId }),
|
|
970
|
+
metadata: {
|
|
971
|
+
inputs,
|
|
972
|
+
frameSet: {
|
|
973
|
+
count: values.length,
|
|
974
|
+
vary: frames.vary,
|
|
975
|
+
seedStep: frames.seedStep,
|
|
976
|
+
fps: spec.quality?.fps ?? 12,
|
|
977
|
+
promptIds
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
};
|
|
904
981
|
}
|
|
982
|
+
const workflow = await buildWorkflow();
|
|
905
983
|
if (spec.revision) {
|
|
906
984
|
if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
|
|
907
985
|
throw new Error("ComfyUI revision source is not ready");
|
|
@@ -936,7 +1014,6 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
936
1014
|
}
|
|
937
1015
|
}
|
|
938
1016
|
const promptId = await this.client.submit(workflow);
|
|
939
|
-
const inputs = providerInputProvenance(spec.providerInputs);
|
|
940
1017
|
const metadata = {
|
|
941
1018
|
...Object.keys(inputs).length ? { inputs } : {},
|
|
942
1019
|
...spec.revision ? {
|
|
@@ -953,7 +1030,47 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
953
1030
|
...Object.keys(metadata).length ? { metadata } : {}
|
|
954
1031
|
};
|
|
955
1032
|
}
|
|
956
|
-
async poll(jobId,
|
|
1033
|
+
async poll(jobId, generator, context) {
|
|
1034
|
+
if (generator === "frames") {
|
|
1035
|
+
const frameJob = decodeFrameSetJob(jobId);
|
|
1036
|
+
const images2 = [];
|
|
1037
|
+
for (let index = 0; index < frameJob.promptIds.length; index++) {
|
|
1038
|
+
const promptId2 = frameJob.promptIds[index];
|
|
1039
|
+
const history2 = await this.client.history(promptId2);
|
|
1040
|
+
const entry2 = historyEntry(history2, promptId2);
|
|
1041
|
+
if (!entry2) return { status: "processing" };
|
|
1042
|
+
const status2 = isObject(entry2.status) ? entry2.status : {};
|
|
1043
|
+
if (status2.status_str === "error") {
|
|
1044
|
+
return { status: "failed", error: `frame ${index}: ${historyError(status2)}` };
|
|
1045
|
+
}
|
|
1046
|
+
if (status2.completed === false) return { status: "processing" };
|
|
1047
|
+
const output = outputImages(entry2, frameJob.outputNodeId);
|
|
1048
|
+
if (output instanceof Error) {
|
|
1049
|
+
return { status: "failed", error: `frame ${index}: ${output.message}` };
|
|
1050
|
+
}
|
|
1051
|
+
if (output.length !== 1) {
|
|
1052
|
+
return {
|
|
1053
|
+
status: "failed",
|
|
1054
|
+
error: `frame ${index}: ComfyUI returned ${output.length} images, expected 1`
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
images2.push(output[0]);
|
|
1058
|
+
}
|
|
1059
|
+
const fps = context?.spec?.quality?.fps ?? 12;
|
|
1060
|
+
const sources = images2.map((image, index) => ({
|
|
1061
|
+
url: sourceRef(image),
|
|
1062
|
+
role: frameRole(index),
|
|
1063
|
+
mediaType: MediaType.PNG
|
|
1064
|
+
}));
|
|
1065
|
+
return {
|
|
1066
|
+
status: "review-set",
|
|
1067
|
+
objectId: jobId,
|
|
1068
|
+
frameUrls: images2.map((image) => this.client.viewUrl(image)),
|
|
1069
|
+
sources,
|
|
1070
|
+
fps,
|
|
1071
|
+
metadata: context?.spec ? comfyFrameMetadata(context.spec, frameJob, images2, fps) : { frameSet: { count: images2.length, fps, promptIds: frameJob.promptIds } }
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
957
1074
|
const { promptId, outputNodeId } = decodeJob(jobId);
|
|
958
1075
|
const history = await this.client.history(promptId);
|
|
959
1076
|
const entry = historyEntry(history, promptId);
|
|
@@ -1016,7 +1133,15 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
1016
1133
|
}
|
|
1017
1134
|
};
|
|
1018
1135
|
function parseOptions(value) {
|
|
1019
|
-
const allowed = /* @__PURE__ */ new Set([
|
|
1136
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1137
|
+
"workflowFile",
|
|
1138
|
+
"outputNodeId",
|
|
1139
|
+
"numImages",
|
|
1140
|
+
"frames",
|
|
1141
|
+
"bindings",
|
|
1142
|
+
"workflow",
|
|
1143
|
+
"workflowSha256"
|
|
1144
|
+
]);
|
|
1020
1145
|
const extra = Object.keys(value).filter((key) => !allowed.has(key));
|
|
1021
1146
|
if (extra.length) throw new Error(`Unknown ComfyUI option(s): ${extra.join(", ")}`);
|
|
1022
1147
|
const workflowFile = requiredString(value.workflowFile, "workflowFile");
|
|
@@ -1030,6 +1155,15 @@ function parseOptions(value) {
|
|
|
1030
1155
|
Object.entries(value.bindings).map(([name, binding]) => [name, parseBinding(binding, name)])
|
|
1031
1156
|
);
|
|
1032
1157
|
if (!bindings.prompt) throw new Error("ComfyUI bindings.prompt must be an object");
|
|
1158
|
+
const frames = value.frames == null ? void 0 : parseFrames(value.frames);
|
|
1159
|
+
if (frames) {
|
|
1160
|
+
if (BUILTIN_BINDINGS.has(frames.vary)) {
|
|
1161
|
+
throw new Error(`ComfyUI frames.vary must name a custom binding, not "${frames.vary}"`);
|
|
1162
|
+
}
|
|
1163
|
+
if (!Object.hasOwn(bindings, frames.vary)) {
|
|
1164
|
+
throw new Error(`ComfyUI frames.vary "${frames.vary}" has no matching binding`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1033
1167
|
const workflow = value.workflow == null ? void 0 : parseWorkflow(value.workflow, workflowFile);
|
|
1034
1168
|
const workflowSha256 = value.workflowSha256 == null ? void 0 : requiredString(value.workflowSha256, "workflowSha256");
|
|
1035
1169
|
return {
|
|
@@ -1037,10 +1171,22 @@ function parseOptions(value) {
|
|
|
1037
1171
|
outputNodeId,
|
|
1038
1172
|
numImages: Number(numImages),
|
|
1039
1173
|
bindings,
|
|
1174
|
+
...frames ? { frames } : {},
|
|
1040
1175
|
...workflow ? { workflow } : {},
|
|
1041
1176
|
...workflowSha256 ? { workflowSha256 } : {}
|
|
1042
1177
|
};
|
|
1043
1178
|
}
|
|
1179
|
+
function parseFrames(value) {
|
|
1180
|
+
if (!isObject(value)) throw new Error("ComfyUI frames must be an object");
|
|
1181
|
+
const extra = Object.keys(value).filter((key) => key !== "vary" && key !== "seedStep");
|
|
1182
|
+
if (extra.length) throw new Error(`Unknown ComfyUI frames field(s): ${extra.join(", ")}`);
|
|
1183
|
+
const vary = requiredString(value.vary, "frames.vary");
|
|
1184
|
+
const seedStep = value.seedStep ?? 0;
|
|
1185
|
+
if (!Number.isSafeInteger(seedStep) || Math.abs(Number(seedStep)) > 1e6) {
|
|
1186
|
+
throw new Error("ComfyUI frames.seedStep must be a safe integer from -1000000 to 1000000");
|
|
1187
|
+
}
|
|
1188
|
+
return { vary, seedStep: Number(seedStep) };
|
|
1189
|
+
}
|
|
1044
1190
|
function resolvedOptions(spec) {
|
|
1045
1191
|
const options = parseOptions(spec.providerOptions);
|
|
1046
1192
|
if (!options.workflow || !options.workflowSha256) {
|
|
@@ -1142,6 +1288,23 @@ function comfyMetadata(spec, promptId, outputNodeId, images) {
|
|
|
1142
1288
|
} : {}
|
|
1143
1289
|
};
|
|
1144
1290
|
}
|
|
1291
|
+
function comfyFrameMetadata(spec, job, images, fps) {
|
|
1292
|
+
const options = resolvedOptions(spec);
|
|
1293
|
+
return {
|
|
1294
|
+
workflowFile: options.workflowFile,
|
|
1295
|
+
workflowSha256: options.workflowSha256,
|
|
1296
|
+
outputNodeId: job.outputNodeId,
|
|
1297
|
+
inputs: providerInputProvenance(spec.providerInputs),
|
|
1298
|
+
frameSet: {
|
|
1299
|
+
count: images.length,
|
|
1300
|
+
vary: options.frames.vary,
|
|
1301
|
+
seedStep: options.frames.seedStep,
|
|
1302
|
+
fps,
|
|
1303
|
+
promptIds: job.promptIds,
|
|
1304
|
+
frames: images.map((image, index) => ({ role: frameRole(index), ...image }))
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1145
1308
|
function isImageBinding(workflow, binding) {
|
|
1146
1309
|
const node = workflow[binding.nodeId];
|
|
1147
1310
|
return binding.input === "image" && (node?.class_type === "LoadImage" || node?.class_type === "LoadImageMask");
|
|
@@ -1152,9 +1315,12 @@ function isComfyImageInput(value) {
|
|
|
1152
1315
|
function providerInputProvenance(inputs) {
|
|
1153
1316
|
return Object.fromEntries(Object.entries(inputs ?? {}).map(([name, input]) => [
|
|
1154
1317
|
name,
|
|
1155
|
-
|
|
1318
|
+
Array.isArray(input) ? { kind: "sequence", values: input.map(providerInputValueProvenance) } : providerInputValueProvenance(input)
|
|
1156
1319
|
]));
|
|
1157
1320
|
}
|
|
1321
|
+
function providerInputValueProvenance(input) {
|
|
1322
|
+
return isComfyImageInput(input) ? { kind: "image", sha256: input.sha256, format: input.format } : { kind: "value", value: input };
|
|
1323
|
+
}
|
|
1158
1324
|
function inputImageFormat(bytes) {
|
|
1159
1325
|
if (bytes.length >= 24 && bytes.readUInt32BE(0) === 2303741511 && bytes.readUInt32BE(4) === 218765834 && bytes.toString("ascii", 12, 16) === "IHDR" && bytes.readUInt32BE(16) > 0 && bytes.readUInt32BE(20) > 0) return "png";
|
|
1160
1326
|
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) return null;
|
|
@@ -1180,6 +1346,25 @@ function inputImageFormat(bytes) {
|
|
|
1180
1346
|
function encodeJob(promptId, outputNodeId) {
|
|
1181
1347
|
return `${promptId}#${encodeURIComponent(outputNodeId)}`;
|
|
1182
1348
|
}
|
|
1349
|
+
function frameRole(index) {
|
|
1350
|
+
return `frame-${String(index).padStart(2, "0")}`;
|
|
1351
|
+
}
|
|
1352
|
+
function encodeFrameSetJob(job) {
|
|
1353
|
+
return `frames:${Buffer.from(JSON.stringify(job)).toString("base64url")}`;
|
|
1354
|
+
}
|
|
1355
|
+
function decodeFrameSetJob(jobId) {
|
|
1356
|
+
if (!jobId.startsWith("frames:")) throw new Error(`Invalid ComfyUI frame-set job id "${jobId}"`);
|
|
1357
|
+
let value;
|
|
1358
|
+
try {
|
|
1359
|
+
value = JSON.parse(Buffer.from(jobId.slice(7), "base64url").toString("utf8"));
|
|
1360
|
+
} catch {
|
|
1361
|
+
throw new Error(`Invalid ComfyUI frame-set job id "${jobId}"`);
|
|
1362
|
+
}
|
|
1363
|
+
if (!isObject(value) || typeof value.outputNodeId !== "string" || !value.outputNodeId || !Array.isArray(value.promptIds) || value.promptIds.length < 2 || value.promptIds.length > 64 || !value.promptIds.every((id) => typeof id === "string" && id.length > 0)) {
|
|
1364
|
+
throw new Error(`Invalid ComfyUI frame-set job id "${jobId}"`);
|
|
1365
|
+
}
|
|
1366
|
+
return { outputNodeId: value.outputNodeId, promptIds: value.promptIds };
|
|
1367
|
+
}
|
|
1183
1368
|
function decodeJob(jobId) {
|
|
1184
1369
|
const split = jobId.lastIndexOf("#");
|
|
1185
1370
|
if (split < 1 || split === jobId.length - 1) throw new Error(`Invalid ComfyUI job id "${jobId}"`);
|
|
@@ -1613,7 +1798,7 @@ function clientFromEnv() {
|
|
|
1613
1798
|
// src/types.ts
|
|
1614
1799
|
import { z as z2 } from "zod";
|
|
1615
1800
|
var MediaTypeSchema = z2.enum(["image/png", "image/gif"]);
|
|
1616
|
-
var GeneratorSchema = z2.enum(["1dir", "map", "pixflux", "tiles", "animation"]);
|
|
1801
|
+
var GeneratorSchema = z2.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames"]);
|
|
1617
1802
|
var GridConfidenceSchema = z2.enum(["low", "medium", "high"]);
|
|
1618
1803
|
var RevisionModeSchema = z2.enum(["image-to-image", "inpaint", "outpaint"]);
|
|
1619
1804
|
function tileVariationCount(descriptions) {
|
|
@@ -1651,7 +1836,7 @@ var StyleImageSchema = z2.object({
|
|
|
1651
1836
|
path: z2.string()
|
|
1652
1837
|
});
|
|
1653
1838
|
var HexColorSchema = z2.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour");
|
|
1654
|
-
var
|
|
1839
|
+
var QualityProfileObjectSchema = z2.object({
|
|
1655
1840
|
/** Output root for approved refined art, relative to the manifest. */
|
|
1656
1841
|
outDir: z2.string().min(1),
|
|
1657
1842
|
/** Closed final palette applied after native-grid recovery, without dithering. */
|
|
@@ -1663,8 +1848,11 @@ var QualityProfileSchema = z2.object({
|
|
|
1663
1848
|
/** Pixel Art Fixer revision written into the quality record. */
|
|
1664
1849
|
fixerRevision: z2.string().min(1).optional(),
|
|
1665
1850
|
/** Python executable containing Pixel Art Fixer, relative to the manifest unless absolute. */
|
|
1666
|
-
fixerPython: z2.string().min(1).optional()
|
|
1667
|
-
|
|
1851
|
+
fixerPython: z2.string().min(1).optional(),
|
|
1852
|
+
/** Playback rate recorded for an ordered frame set. */
|
|
1853
|
+
fps: z2.number().int().min(1).max(60).optional()
|
|
1854
|
+
}).strict();
|
|
1855
|
+
var QualityProfileSchema = QualityProfileObjectSchema.refine(
|
|
1668
1856
|
(profile) => new Set(profile.palette.map((color) => color.replace(/^#/, "").toLowerCase())).size === profile.palette.length,
|
|
1669
1857
|
{ message: "quality palette colors must be unique", path: ["palette"] }
|
|
1670
1858
|
);
|
|
@@ -1693,7 +1881,7 @@ var RevisionSchema = z2.object({
|
|
|
1693
1881
|
});
|
|
1694
1882
|
}
|
|
1695
1883
|
});
|
|
1696
|
-
var
|
|
1884
|
+
var StyleObjectSchema = z2.object({
|
|
1697
1885
|
/** Generation backend for this style. Omit to inherit the manifest default. */
|
|
1698
1886
|
provider: z2.string().min(1).optional(),
|
|
1699
1887
|
// `map` is the default because it is 20-40x cheaper and correct for any
|
|
@@ -1808,7 +1996,8 @@ var StyleSchema = z2.object({
|
|
|
1808
1996
|
tags: z2.array(z2.string()).default([]),
|
|
1809
1997
|
/** Adapter-owned settings, keyed by provider id. */
|
|
1810
1998
|
providerOptions: z2.record(z2.record(z2.unknown())).default({})
|
|
1811
|
-
}).strict()
|
|
1999
|
+
}).strict();
|
|
2000
|
+
var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImages.length), {
|
|
1812
2001
|
message: "tileFeature and styleImages cannot be combined \u2014 a connectable set derives its own tile geometry, so remove one or the other",
|
|
1813
2002
|
path: ["tileFeature"]
|
|
1814
2003
|
}).superRefine((style, ctx) => {
|
|
@@ -1820,6 +2009,14 @@ var StyleSchema = z2.object({
|
|
|
1820
2009
|
});
|
|
1821
2010
|
}
|
|
1822
2011
|
});
|
|
2012
|
+
var InheritedStyleSchema = StyleObjectSchema.partial().extend({
|
|
2013
|
+
extends: z2.string().min(1),
|
|
2014
|
+
/** Children own their destination; sharing a parent's output is never implicit. */
|
|
2015
|
+
outDir: z2.string(),
|
|
2016
|
+
/** A child may override only part of the inherited final-art policy. */
|
|
2017
|
+
quality: QualityProfileObjectSchema.partial().optional()
|
|
2018
|
+
}).strict();
|
|
2019
|
+
var StyleInputSchema = z2.union([StyleSchema, InheritedStyleSchema]);
|
|
1823
2020
|
var AssetSchema = z2.object({
|
|
1824
2021
|
/** The subject. Style wrapping comes from the style's prefix/suffix. */
|
|
1825
2022
|
prompt: z2.string(),
|
|
@@ -1882,10 +2079,15 @@ var AssetSchema = z2.object({
|
|
|
1882
2079
|
promptByStyle: z2.record(z2.string()).default({}),
|
|
1883
2080
|
/**
|
|
1884
2081
|
* Named per-asset values consumed by the active provider's declared
|
|
1885
|
-
* bindings. Values are JSON scalars
|
|
1886
|
-
*
|
|
2082
|
+
* bindings. Values are JSON scalars, or a frame-set sequence of scalars;
|
|
2083
|
+
* adapters may interpret a string as a manifest-relative uploaded file.
|
|
1887
2084
|
*/
|
|
1888
|
-
providerInputs: z2.record(z2.union([
|
|
2085
|
+
providerInputs: z2.record(z2.union([
|
|
2086
|
+
z2.string(),
|
|
2087
|
+
z2.number().finite(),
|
|
2088
|
+
z2.boolean(),
|
|
2089
|
+
z2.array(z2.union([z2.string(), z2.number().finite(), z2.boolean()])).min(2).max(64)
|
|
2090
|
+
])).default({})
|
|
1889
2091
|
}).strict().superRefine((asset, context) => {
|
|
1890
2092
|
if (asset.source && asset.revision) {
|
|
1891
2093
|
context.addIssue({
|
|
@@ -1903,6 +2105,13 @@ var ManifestSchema = z2.object({
|
|
|
1903
2105
|
styles: z2.record(StyleSchema),
|
|
1904
2106
|
assets: z2.record(AssetSchema)
|
|
1905
2107
|
}).strict();
|
|
2108
|
+
var ManifestInputSchema = z2.object({
|
|
2109
|
+
$schema: z2.string().optional(),
|
|
2110
|
+
name: z2.string(),
|
|
2111
|
+
provider: z2.string().min(1).default("pixellab"),
|
|
2112
|
+
styles: z2.record(StyleInputSchema),
|
|
2113
|
+
assets: z2.record(AssetSchema)
|
|
2114
|
+
}).strict();
|
|
1906
2115
|
var LockEntrySchema = z2.object({
|
|
1907
2116
|
styleId: z2.string(),
|
|
1908
2117
|
assetId: z2.string(),
|
|
@@ -3195,136 +3404,483 @@ registerProvider({
|
|
|
3195
3404
|
});
|
|
3196
3405
|
|
|
3197
3406
|
// src/manifest.ts
|
|
3198
|
-
import { readFile as
|
|
3407
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
3408
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3409
|
+
import path6 from "path";
|
|
3410
|
+
|
|
3411
|
+
// src/outputs.ts
|
|
3412
|
+
import path5 from "path";
|
|
3413
|
+
|
|
3414
|
+
// src/lock.ts
|
|
3415
|
+
import { readFile as readFile2, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
|
|
3199
3416
|
import { existsSync as existsSync3 } from "fs";
|
|
3200
3417
|
import path4 from "path";
|
|
3201
|
-
async function
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
${
|
|
3209
|
-
|
|
3210
|
-
const styleIds = new Set(Object.keys(parsed.data.styles));
|
|
3211
|
-
const unknownReferences = [];
|
|
3212
|
-
for (const [assetId, asset] of Object.entries(parsed.data.assets)) {
|
|
3213
|
-
for (const styleId of asset.styles) {
|
|
3214
|
-
if (!styleIds.has(styleId)) unknownReferences.push(`assets.${assetId}.styles: unknown style "${styleId}"`);
|
|
3215
|
-
}
|
|
3216
|
-
for (const styleId of Object.keys(asset.promptByStyle)) {
|
|
3217
|
-
if (!styleIds.has(styleId)) {
|
|
3218
|
-
unknownReferences.push(`assets.${assetId}.promptByStyle: unknown style "${styleId}"`);
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
3221
|
-
if (asset.revision) {
|
|
3222
|
-
if (!parsed.data.assets[asset.revision.from]) {
|
|
3223
|
-
unknownReferences.push(
|
|
3224
|
-
`assets.${assetId}.revision.from: unknown asset "${asset.revision.from}"`
|
|
3225
|
-
);
|
|
3226
|
-
} else if (asset.revision.from === assetId) {
|
|
3227
|
-
unknownReferences.push(`assets.${assetId}.revision.from: an asset cannot revise itself`);
|
|
3228
|
-
}
|
|
3229
|
-
}
|
|
3418
|
+
async function loadLock(lockPath) {
|
|
3419
|
+
if (!existsSync3(lockPath)) return { version: 2, entries: {} };
|
|
3420
|
+
try {
|
|
3421
|
+
return parseLock(JSON.parse(await readFile2(lockPath, "utf8")));
|
|
3422
|
+
} catch (err) {
|
|
3423
|
+
throw new Error(
|
|
3424
|
+
`Lockfile at ${lockPath} is malformed:
|
|
3425
|
+
${err instanceof Error ? err.message : String(err)}`
|
|
3426
|
+
);
|
|
3230
3427
|
}
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
const parent = parsed.data.assets[assetId]?.revision?.from;
|
|
3244
|
-
if (parent && parent !== assetId && parsed.data.assets[parent]) {
|
|
3245
|
-
visitRevision(parent, [...chain, assetId]);
|
|
3246
|
-
}
|
|
3247
|
-
visiting.delete(assetId);
|
|
3248
|
-
visited.add(assetId);
|
|
3428
|
+
}
|
|
3429
|
+
var saveQueues = /* @__PURE__ */ new Map();
|
|
3430
|
+
var dirtyPatches = /* @__PURE__ */ new WeakMap();
|
|
3431
|
+
var dirtyDeletes = /* @__PURE__ */ new WeakMap();
|
|
3432
|
+
function saveLock(lockPath, lock) {
|
|
3433
|
+
const queueKey = path4.resolve(lockPath);
|
|
3434
|
+
const previous = saveQueues.get(queueKey) ?? Promise.resolve();
|
|
3435
|
+
const next = previous.catch(() => {
|
|
3436
|
+
}).then(() => writeLockNow(lockPath, lock));
|
|
3437
|
+
saveQueues.set(queueKey, next);
|
|
3438
|
+
const cleanup = () => {
|
|
3439
|
+
if (saveQueues.get(queueKey) === next) saveQueues.delete(queueKey);
|
|
3249
3440
|
};
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3441
|
+
void next.then(cleanup, cleanup);
|
|
3442
|
+
return next;
|
|
3443
|
+
}
|
|
3444
|
+
async function writeLockNow(lockPath, lock) {
|
|
3445
|
+
await mkdir(path4.dirname(path4.resolve(lockPath)), { recursive: true });
|
|
3446
|
+
const release = await acquireFileLock(lockPath);
|
|
3447
|
+
try {
|
|
3448
|
+
await writeLockWhileHeld(lockPath, lock);
|
|
3449
|
+
} finally {
|
|
3450
|
+
await release();
|
|
3254
3451
|
}
|
|
3255
|
-
return { manifest: parsed.data, root: path4.dirname(abs), path: abs };
|
|
3256
3452
|
}
|
|
3257
|
-
async function
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
let provider = providers.get(id);
|
|
3264
|
-
if (!provider) {
|
|
3265
|
-
provider = createProvider(id, "offline");
|
|
3266
|
-
providers.set(id, provider);
|
|
3267
|
-
}
|
|
3268
|
-
return provider;
|
|
3269
|
-
};
|
|
3270
|
-
const specs = [];
|
|
3271
|
-
const styleIds = Object.keys(manifest.styles).filter(
|
|
3272
|
-
(id) => !filter?.styles?.length || filter.styles.includes(id)
|
|
3273
|
-
);
|
|
3274
|
-
for (const unknownStyle of filter?.styles ?? []) {
|
|
3275
|
-
if (!manifest.styles[unknownStyle]) {
|
|
3453
|
+
async function writeLockWhileHeld(file, lock) {
|
|
3454
|
+
let disk = { version: 2, entries: {} };
|
|
3455
|
+
if (existsSync3(file)) {
|
|
3456
|
+
try {
|
|
3457
|
+
disk = parseLock(JSON.parse(await readFile2(file, "utf8")));
|
|
3458
|
+
} catch (err) {
|
|
3276
3459
|
throw new Error(
|
|
3277
|
-
`
|
|
3460
|
+
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
3278
3461
|
);
|
|
3279
3462
|
}
|
|
3280
3463
|
}
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3464
|
+
const dirty = dirtyPatches.get(lock) ?? /* @__PURE__ */ new Map();
|
|
3465
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
3466
|
+
for (const [key, patch] of dirty) snapshot.set(key, { ...patch });
|
|
3467
|
+
const merged = { ...disk.entries };
|
|
3468
|
+
for (const [key, patch] of snapshot) {
|
|
3469
|
+
const local = lock.entries[key];
|
|
3470
|
+
if (!local) continue;
|
|
3471
|
+
merged[key] = merged[key] ? { ...merged[key], ...patch } : local;
|
|
3285
3472
|
}
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
for (const assetId of [...requestedAssetIds]) {
|
|
3289
|
-
let current = manifest.assets[assetId]?.revision?.from;
|
|
3290
|
-
while (current && !resolutionAssetIds.has(current)) {
|
|
3291
|
-
resolutionAssetIds.add(current);
|
|
3292
|
-
current = manifest.assets[current]?.revision?.from;
|
|
3293
|
-
}
|
|
3473
|
+
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
3474
|
+
if (!merged[key]) merged[key] = entry;
|
|
3294
3475
|
}
|
|
3295
|
-
const
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3476
|
+
const deletes = dirtyDeletes.get(lock);
|
|
3477
|
+
const deleteSnapshot = deletes ? new Set(deletes) : /* @__PURE__ */ new Set();
|
|
3478
|
+
for (const key of deleteSnapshot) delete merged[key];
|
|
3479
|
+
const sorted = {};
|
|
3480
|
+
for (const key of Object.keys(merged).sort()) {
|
|
3481
|
+
sorted[key] = merged[key];
|
|
3482
|
+
}
|
|
3483
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3484
|
+
try {
|
|
3485
|
+
await writeFile(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
|
|
3486
|
+
await rename(tmp, file);
|
|
3487
|
+
} finally {
|
|
3488
|
+
await rm(tmp, { force: true });
|
|
3489
|
+
}
|
|
3490
|
+
const currentDirty = dirtyPatches.get(lock);
|
|
3491
|
+
if (currentDirty) {
|
|
3492
|
+
for (const [key, patch] of snapshot) {
|
|
3493
|
+
const current = currentDirty.get(key);
|
|
3494
|
+
if (!current) continue;
|
|
3495
|
+
for (const [field, value] of Object.entries(patch)) {
|
|
3496
|
+
if (Object.is(current[field], value)) {
|
|
3497
|
+
delete current[field];
|
|
3498
|
+
}
|
|
3306
3499
|
}
|
|
3307
|
-
|
|
3308
|
-
styleImageCache.set(abs, hit);
|
|
3500
|
+
if (Object.keys(current).length === 0) currentDirty.delete(key);
|
|
3309
3501
|
}
|
|
3310
|
-
return hit;
|
|
3311
3502
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
const
|
|
3503
|
+
const stillDirty = dirtyDeletes.get(lock);
|
|
3504
|
+
if (stillDirty) {
|
|
3505
|
+
for (const key of deleteSnapshot) stillDirty.delete(key);
|
|
3506
|
+
if (stillDirty.size === 0) dirtyDeletes.delete(lock);
|
|
3507
|
+
}
|
|
3508
|
+
const localAfterWrite = lock.entries;
|
|
3509
|
+
lock.entries = { ...merged };
|
|
3510
|
+
for (const [key, patch] of currentDirty ?? []) {
|
|
3511
|
+
const base = lock.entries[key] ?? localAfterWrite[key];
|
|
3512
|
+
if (base) lock.entries[key] = { ...base, ...patch };
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
function upsert(lock, key, patch) {
|
|
3516
|
+
dirtyDeletes.get(lock)?.delete(key);
|
|
3517
|
+
const existing = lock.entries[key];
|
|
3518
|
+
const next = { ...existing, ...patch };
|
|
3519
|
+
lock.entries[key] = next;
|
|
3520
|
+
let dirty = dirtyPatches.get(lock);
|
|
3521
|
+
if (!dirty) {
|
|
3522
|
+
dirty = /* @__PURE__ */ new Map();
|
|
3523
|
+
dirtyPatches.set(lock, dirty);
|
|
3524
|
+
}
|
|
3525
|
+
dirty.set(key, { ...dirty.get(key) ?? {}, ...patch });
|
|
3526
|
+
return next;
|
|
3527
|
+
}
|
|
3528
|
+
function remove(lock, key) {
|
|
3529
|
+
if (!lock.entries[key]) return false;
|
|
3530
|
+
delete lock.entries[key];
|
|
3531
|
+
const patches = dirtyPatches.get(lock);
|
|
3532
|
+
if (patches) {
|
|
3533
|
+
patches.delete(key);
|
|
3534
|
+
if (patches.size === 0) dirtyPatches.delete(lock);
|
|
3535
|
+
}
|
|
3536
|
+
let deletes = dirtyDeletes.get(lock);
|
|
3537
|
+
if (!deletes) {
|
|
3538
|
+
deletes = /* @__PURE__ */ new Set();
|
|
3539
|
+
dirtyDeletes.set(lock, deletes);
|
|
3540
|
+
}
|
|
3541
|
+
deletes.add(key);
|
|
3542
|
+
return true;
|
|
3543
|
+
}
|
|
3544
|
+
var FILE_LOCK_TIMEOUT_MS = 1e4;
|
|
3545
|
+
var STALE_FILE_LOCK_MS = 2 * 60 * 1e3;
|
|
3546
|
+
async function acquireFileLock(file) {
|
|
3547
|
+
const lockDir = `${file}.lock`;
|
|
3548
|
+
const deadline = Date.now() + FILE_LOCK_TIMEOUT_MS;
|
|
3549
|
+
for (; ; ) {
|
|
3550
|
+
try {
|
|
3551
|
+
await mkdir(lockDir);
|
|
3552
|
+
return async () => {
|
|
3553
|
+
await rmdir(lockDir).catch(() => {
|
|
3554
|
+
});
|
|
3555
|
+
};
|
|
3556
|
+
} catch (err) {
|
|
3557
|
+
const code = err.code;
|
|
3558
|
+
if (code !== "EEXIST") throw err;
|
|
3559
|
+
try {
|
|
3560
|
+
const age = Date.now() - (await stat(lockDir)).mtimeMs;
|
|
3561
|
+
if (age > STALE_FILE_LOCK_MS) {
|
|
3562
|
+
await rmdir(lockDir);
|
|
3563
|
+
continue;
|
|
3564
|
+
}
|
|
3565
|
+
} catch (lockErr) {
|
|
3566
|
+
if (lockErr.code === "ENOENT") continue;
|
|
3567
|
+
}
|
|
3568
|
+
if (Date.now() >= deadline) {
|
|
3569
|
+
throw new Error(`Timed out waiting for lockfile writer ${lockDir}`);
|
|
3570
|
+
}
|
|
3571
|
+
await new Promise((resolve) => setTimeout(resolve, 25 + Math.floor(Math.random() * 25)));
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
function spendByUnit(lock) {
|
|
3576
|
+
const totals = { generations: 0, usd: 0, free: 0 };
|
|
3577
|
+
for (const entry of Object.values(lock.entries)) {
|
|
3578
|
+
const unit = entry.costUnit ?? "generations";
|
|
3579
|
+
totals[unit] = (totals[unit] ?? 0) + (entry.cost ?? 0);
|
|
3580
|
+
}
|
|
3581
|
+
return totals;
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
// src/outputs.ts
|
|
3585
|
+
function portableOutputPath(file, manifestDir) {
|
|
3586
|
+
const root = path5.resolve(manifestDir);
|
|
3587
|
+
const absolute = path5.isAbsolute(file) ? path5.normalize(file) : path5.resolve(root, file);
|
|
3588
|
+
const relative = path5.relative(root, absolute);
|
|
3589
|
+
const value = path5.isAbsolute(relative) ? absolute : relative;
|
|
3590
|
+
return value.split(path5.sep).join("/");
|
|
3591
|
+
}
|
|
3592
|
+
function resolveOutputPath(recordedPath, manifestDir) {
|
|
3593
|
+
if (path5.isAbsolute(recordedPath)) return path5.normalize(recordedPath);
|
|
3594
|
+
if (path5.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
3595
|
+
return path5.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path5.sep));
|
|
3596
|
+
}
|
|
3597
|
+
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
3598
|
+
const originalExt = path5.extname(spec.outFile);
|
|
3599
|
+
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
3600
|
+
const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
|
|
3601
|
+
if (total === 1) return `${stem}${ext}`;
|
|
3602
|
+
const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
3603
|
+
return `${stem}-${safeRole}${ext}`;
|
|
3604
|
+
}
|
|
3605
|
+
function currentOutputPath(output, spec, index, total) {
|
|
3606
|
+
return expectedOutputPath(spec, output.role, index, total, output.mediaType);
|
|
3607
|
+
}
|
|
3608
|
+
function currentEntryOutputPath(entry, spec, index) {
|
|
3609
|
+
const output = entry.outputs[index];
|
|
3610
|
+
const sourceIndex = output.role ? (entry.sourceUrls ?? []).findIndex((source) => source.role === output.role) : -1;
|
|
3611
|
+
const position = sourceIndex >= 0 ? sourceIndex : index;
|
|
3612
|
+
const total = Math.max(entry.outputs.length, entry.sourceUrls?.length ?? 0);
|
|
3613
|
+
return currentOutputPath(output, spec, position, total);
|
|
3614
|
+
}
|
|
3615
|
+
function normalizeLockOutputPaths(lock, specs) {
|
|
3616
|
+
const byKey = new Map(specs.map((spec) => [lockKey(spec.styleId, spec.assetId), spec]));
|
|
3617
|
+
let changed = 0;
|
|
3618
|
+
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
3619
|
+
const spec = byKey.get(key);
|
|
3620
|
+
if (!spec) continue;
|
|
3621
|
+
let entryChanged = false;
|
|
3622
|
+
const outputs = entry.outputs.map((output, index) => {
|
|
3623
|
+
const absolute = currentEntryOutputPath(entry, spec, index);
|
|
3624
|
+
const portable = portableOutputPath(absolute, spec.root);
|
|
3625
|
+
if (portable === output.path) return output;
|
|
3626
|
+
changed++;
|
|
3627
|
+
entryChanged = true;
|
|
3628
|
+
return { ...output, path: portable };
|
|
3629
|
+
});
|
|
3630
|
+
if (entryChanged) upsert(lock, key, { outputs });
|
|
3631
|
+
}
|
|
3632
|
+
return changed;
|
|
3633
|
+
}
|
|
3634
|
+
function fallbackOutputRole(index) {
|
|
3635
|
+
return `output-${String(index).padStart(2, "0")}`;
|
|
3636
|
+
}
|
|
3637
|
+
function outputId(assetId, output, index, total) {
|
|
3638
|
+
if (total === 1) return assetId;
|
|
3639
|
+
return `${assetId}/${output.role ?? fallbackOutputRole(index)}`;
|
|
3640
|
+
}
|
|
3641
|
+
function resolveEntryOutputs(entry, assetId, manifestDir) {
|
|
3642
|
+
return entry.outputs.map((output, index) => ({
|
|
3643
|
+
...output,
|
|
3644
|
+
assetId,
|
|
3645
|
+
id: outputId(assetId, output, index, entry.outputs.length),
|
|
3646
|
+
index,
|
|
3647
|
+
absolutePath: resolveOutputPath(output.path, manifestDir)
|
|
3648
|
+
}));
|
|
3649
|
+
}
|
|
3650
|
+
function resolveSpecEntryOutputs(entry, spec) {
|
|
3651
|
+
return entry.outputs.map((output, index) => ({
|
|
3652
|
+
...output,
|
|
3653
|
+
assetId: spec.assetId,
|
|
3654
|
+
id: outputId(spec.assetId, output, index, entry.outputs.length),
|
|
3655
|
+
index,
|
|
3656
|
+
absolutePath: currentEntryOutputPath(entry, spec, index)
|
|
3657
|
+
}));
|
|
3658
|
+
}
|
|
3659
|
+
function selectEntryOutput(entry, role) {
|
|
3660
|
+
if (role) {
|
|
3661
|
+
const output = entry.outputs.find((candidate) => candidate.role === role);
|
|
3662
|
+
return output ? { ok: true, output } : { ok: false, reason: `output role "${role}" is not recorded` };
|
|
3663
|
+
}
|
|
3664
|
+
if (entry.outputs.length === 0) return { ok: false, reason: "no output recorded in the lockfile" };
|
|
3665
|
+
if (entry.outputs.length === 1) return { ok: true, output: entry.outputs[0] };
|
|
3666
|
+
const primary = entry.outputs.filter((output) => !output.role);
|
|
3667
|
+
if (primary.length === 1) return { ok: true, output: primary[0] };
|
|
3668
|
+
return {
|
|
3669
|
+
ok: false,
|
|
3670
|
+
reason: `${entry.outputs.length} outputs are recorded; set \`outputRole\` to choose one`
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
3674
|
+
const entry = lock?.entries[lockKey(spec.styleId, spec.assetId)];
|
|
3675
|
+
if (entry?.outputs.length) return resolveSpecEntryOutputs(entry, spec);
|
|
3676
|
+
return [{
|
|
3677
|
+
assetId: spec.assetId,
|
|
3678
|
+
id: spec.assetId,
|
|
3679
|
+
index: 0,
|
|
3680
|
+
path: spec.outFile,
|
|
3681
|
+
absolutePath: path5.resolve(spec.outFile),
|
|
3682
|
+
sha256: ""
|
|
3683
|
+
}];
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
// src/manifest.ts
|
|
3687
|
+
async function loadManifest(manifestPath) {
|
|
3688
|
+
const abs = path6.resolve(manifestPath);
|
|
3689
|
+
if (!existsSync4(abs)) throw new Error(`No manifest at ${abs}`);
|
|
3690
|
+
const input = ManifestInputSchema.safeParse(JSON.parse(await readFile3(abs, "utf8")));
|
|
3691
|
+
if (!input.success) {
|
|
3692
|
+
const issues = formatManifestIssues(input.error.issues);
|
|
3693
|
+
throw new Error(`Manifest at ${abs} is invalid:
|
|
3694
|
+
${issues}`);
|
|
3695
|
+
}
|
|
3696
|
+
let styles;
|
|
3697
|
+
try {
|
|
3698
|
+
styles = resolveStyleInheritance(input.data.styles);
|
|
3699
|
+
} catch (error) {
|
|
3700
|
+
throw new Error(
|
|
3701
|
+
`Manifest at ${abs} is invalid:
|
|
3702
|
+
${error instanceof Error ? error.message : String(error)}`
|
|
3703
|
+
);
|
|
3704
|
+
}
|
|
3705
|
+
const parsed = ManifestSchema.safeParse({ ...input.data, styles });
|
|
3706
|
+
if (!parsed.success) {
|
|
3707
|
+
const issues = formatManifestIssues(parsed.error.issues);
|
|
3708
|
+
throw new Error(`Manifest at ${abs} is invalid:
|
|
3709
|
+
${issues}`);
|
|
3710
|
+
}
|
|
3711
|
+
const styleIds = new Set(Object.keys(parsed.data.styles));
|
|
3712
|
+
const unknownReferences = [];
|
|
3713
|
+
for (const [assetId, asset] of Object.entries(parsed.data.assets)) {
|
|
3714
|
+
for (const styleId of asset.styles) {
|
|
3715
|
+
if (!styleIds.has(styleId)) unknownReferences.push(`assets.${assetId}.styles: unknown style "${styleId}"`);
|
|
3716
|
+
}
|
|
3717
|
+
for (const styleId of Object.keys(asset.promptByStyle)) {
|
|
3718
|
+
if (!styleIds.has(styleId)) {
|
|
3719
|
+
unknownReferences.push(`assets.${assetId}.promptByStyle: unknown style "${styleId}"`);
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
if (asset.revision) {
|
|
3723
|
+
if (!parsed.data.assets[asset.revision.from]) {
|
|
3724
|
+
unknownReferences.push(
|
|
3725
|
+
`assets.${assetId}.revision.from: unknown asset "${asset.revision.from}"`
|
|
3726
|
+
);
|
|
3727
|
+
} else if (asset.revision.from === assetId) {
|
|
3728
|
+
unknownReferences.push(`assets.${assetId}.revision.from: an asset cannot revise itself`);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
3733
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3734
|
+
const visitRevision = (assetId, chain) => {
|
|
3735
|
+
if (visited.has(assetId)) return;
|
|
3736
|
+
if (visiting.has(assetId)) {
|
|
3737
|
+
const start = chain.indexOf(assetId);
|
|
3738
|
+
unknownReferences.push(
|
|
3739
|
+
`assets.${assetId}.revision.from: revision cycle ${[...chain.slice(start), assetId].join(" -> ")}`
|
|
3740
|
+
);
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
visiting.add(assetId);
|
|
3744
|
+
const parent = parsed.data.assets[assetId]?.revision?.from;
|
|
3745
|
+
if (parent && parent !== assetId && parsed.data.assets[parent]) {
|
|
3746
|
+
visitRevision(parent, [...chain, assetId]);
|
|
3747
|
+
}
|
|
3748
|
+
visiting.delete(assetId);
|
|
3749
|
+
visited.add(assetId);
|
|
3750
|
+
};
|
|
3751
|
+
for (const assetId of Object.keys(parsed.data.assets)) visitRevision(assetId, []);
|
|
3752
|
+
if (unknownReferences.length) {
|
|
3753
|
+
throw new Error(`Manifest at ${abs} is invalid:
|
|
3754
|
+
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
3755
|
+
}
|
|
3756
|
+
return { manifest: parsed.data, root: path6.dirname(abs), path: abs };
|
|
3757
|
+
}
|
|
3758
|
+
function formatManifestIssues(issues) {
|
|
3759
|
+
return issues.flatMap((issue) => {
|
|
3760
|
+
if (issue.code === "invalid_union") {
|
|
3761
|
+
return issue.unionErrors.flatMap((error) => formatManifestIssues(error.issues).split("\n"));
|
|
3762
|
+
}
|
|
3763
|
+
return [` ${issue.path.join(".")}: ${issue.message}`];
|
|
3764
|
+
}).join("\n");
|
|
3765
|
+
}
|
|
3766
|
+
function resolveStyleInheritance(styles) {
|
|
3767
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
3768
|
+
const visit = (styleId, chain) => {
|
|
3769
|
+
const cached = resolved.get(styleId);
|
|
3770
|
+
if (cached) return cached;
|
|
3771
|
+
const cycleStart = chain.indexOf(styleId);
|
|
3772
|
+
if (cycleStart >= 0) {
|
|
3773
|
+
throw new Error(
|
|
3774
|
+
`styles.${styleId}.extends: inheritance cycle ${[...chain.slice(cycleStart), styleId].join(" -> ")}`
|
|
3775
|
+
);
|
|
3776
|
+
}
|
|
3777
|
+
const style = styles[styleId];
|
|
3778
|
+
if (!style) throw new Error(`styles.${styleId}: unknown style`);
|
|
3779
|
+
if (!("extends" in style)) {
|
|
3780
|
+
resolved.set(styleId, style);
|
|
3781
|
+
return style;
|
|
3782
|
+
}
|
|
3783
|
+
const parentId = style.extends;
|
|
3784
|
+
if (!Object.hasOwn(styles, parentId)) {
|
|
3785
|
+
throw new Error(`styles.${styleId}.extends: unknown style "${parentId}"`);
|
|
3786
|
+
}
|
|
3787
|
+
const parent = visit(parentId, [...chain, styleId]);
|
|
3788
|
+
const { extends: _parent, ...child } = style;
|
|
3789
|
+
const merged = {
|
|
3790
|
+
...parent,
|
|
3791
|
+
...child,
|
|
3792
|
+
quality: child.quality == null ? parent.quality : { ...parent.quality ?? {}, ...child.quality },
|
|
3793
|
+
providerOptions: mergeProviderOptions(parent.providerOptions, child.providerOptions)
|
|
3794
|
+
};
|
|
3795
|
+
const parsed = StyleSchema.safeParse(merged);
|
|
3796
|
+
if (!parsed.success) {
|
|
3797
|
+
const issues = parsed.error.issues.map((issue) => `styles.${styleId}.${issue.path.join(".")}: ${issue.message}`).join("; ");
|
|
3798
|
+
throw new Error(issues);
|
|
3799
|
+
}
|
|
3800
|
+
resolved.set(styleId, parsed.data);
|
|
3801
|
+
return parsed.data;
|
|
3802
|
+
};
|
|
3803
|
+
return Object.fromEntries(Object.keys(styles).map((styleId) => [styleId, visit(styleId, [])]));
|
|
3804
|
+
}
|
|
3805
|
+
function mergeProviderOptions(parent, child) {
|
|
3806
|
+
if (!child) return parent;
|
|
3807
|
+
const merged = { ...parent };
|
|
3808
|
+
for (const [provider, options] of Object.entries(child)) {
|
|
3809
|
+
merged[provider] = { ...parent[provider] ?? {}, ...options };
|
|
3810
|
+
}
|
|
3811
|
+
return merged;
|
|
3812
|
+
}
|
|
3813
|
+
async function resolveSpecs(loaded, filter) {
|
|
3814
|
+
const { manifest, root } = loaded;
|
|
3815
|
+
const providerOverride = filter?.provider;
|
|
3816
|
+
const providers = /* @__PURE__ */ new Map();
|
|
3817
|
+
const providerFor = (id) => {
|
|
3818
|
+
if (providerOverride) return providerOverride;
|
|
3819
|
+
let provider = providers.get(id);
|
|
3820
|
+
if (!provider) {
|
|
3821
|
+
provider = createProvider(id, "offline");
|
|
3822
|
+
providers.set(id, provider);
|
|
3823
|
+
}
|
|
3824
|
+
return provider;
|
|
3825
|
+
};
|
|
3826
|
+
const specs = [];
|
|
3827
|
+
const styleIds = Object.keys(manifest.styles).filter(
|
|
3828
|
+
(id) => !filter?.styles?.length || filter.styles.includes(id)
|
|
3829
|
+
);
|
|
3830
|
+
for (const unknownStyle of filter?.styles ?? []) {
|
|
3831
|
+
if (!manifest.styles[unknownStyle]) {
|
|
3832
|
+
throw new Error(
|
|
3833
|
+
`Unknown style "${unknownStyle}". Defined: ${Object.keys(manifest.styles).join(", ") || "(none)"}`
|
|
3834
|
+
);
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
for (const unknownAsset of filter?.assets ?? []) {
|
|
3838
|
+
if (!manifest.assets[unknownAsset]) {
|
|
3839
|
+
throw new Error(`Unknown asset "${unknownAsset}".`);
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
const requestedAssetIds = new Set(filter?.assets?.length ? filter.assets : Object.keys(manifest.assets));
|
|
3843
|
+
const resolutionAssetIds = new Set(requestedAssetIds);
|
|
3844
|
+
for (const assetId of [...requestedAssetIds]) {
|
|
3845
|
+
let current = manifest.assets[assetId]?.revision?.from;
|
|
3846
|
+
while (current && !resolutionAssetIds.has(current)) {
|
|
3847
|
+
resolutionAssetIds.add(current);
|
|
3848
|
+
current = manifest.assets[current]?.revision?.from;
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
const styleImageCache = /* @__PURE__ */ new Map();
|
|
3852
|
+
async function loadStyleImage(rel) {
|
|
3853
|
+
const abs = path6.resolve(root, rel);
|
|
3854
|
+
let hit = styleImageCache.get(abs);
|
|
3855
|
+
if (!hit) {
|
|
3856
|
+
if (!existsSync4(abs)) throw new Error(`Style image not found: ${abs}`);
|
|
3857
|
+
const buf = await readFile3(abs);
|
|
3858
|
+
const metadata = imageMetadata(buf);
|
|
3859
|
+
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
3860
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
3861
|
+
throw new Error(`Style image has invalid dimensions: ${abs}`);
|
|
3862
|
+
}
|
|
3863
|
+
hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
|
|
3864
|
+
styleImageCache.set(abs, hit);
|
|
3865
|
+
}
|
|
3866
|
+
return hit;
|
|
3867
|
+
}
|
|
3868
|
+
for (const styleId of styleIds) {
|
|
3869
|
+
const style = manifest.styles[styleId];
|
|
3870
|
+
const activeProvider = providerFor(style.provider ?? manifest.provider);
|
|
3871
|
+
const rawProviderOptions = style.providerOptions[activeProvider.id] ?? {};
|
|
3872
|
+
const optionResolution = activeProvider.resolveOptions ? await activeProvider.resolveOptions(rawProviderOptions, { root, styleId }) : { options: rawProviderOptions };
|
|
3873
|
+
const providerOptions = optionResolution.options;
|
|
3874
|
+
const providerOptionIdentity = optionResolution.identity ?? providerOptions;
|
|
3875
|
+
const styleImageHashes = [];
|
|
3876
|
+
const styleImageDimensions = [];
|
|
3321
3877
|
for (const img of style.styleImages) {
|
|
3322
3878
|
const loadedImage = await loadStyleImage(img.path);
|
|
3323
3879
|
styleImageHashes.push(loadedImage.hash);
|
|
3324
3880
|
styleImageDimensions.push(loadedImage);
|
|
3325
3881
|
}
|
|
3326
3882
|
const resolvedImages = style.styleImages.map((image) => {
|
|
3327
|
-
const hit = styleImageCache.get(
|
|
3883
|
+
const hit = styleImageCache.get(path6.resolve(root, image.path));
|
|
3328
3884
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
3329
3885
|
});
|
|
3330
3886
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
@@ -3345,6 +3901,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3345
3901
|
root,
|
|
3346
3902
|
styleId,
|
|
3347
3903
|
assetId,
|
|
3904
|
+
generator,
|
|
3348
3905
|
providerOptions
|
|
3349
3906
|
}) : { inputs: {} };
|
|
3350
3907
|
const providerInputs = inputResolution.inputs;
|
|
@@ -3368,9 +3925,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3368
3925
|
}
|
|
3369
3926
|
const subject = asset.promptByStyle[styleId] ?? asset.prompt;
|
|
3370
3927
|
const prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
3371
|
-
const relFile = asset.file ??
|
|
3372
|
-
const outFile =
|
|
3373
|
-
const qualityOutFile = style.quality ?
|
|
3928
|
+
const relFile = asset.file ?? path6.join(asset.category ?? "", `${assetId}.png`);
|
|
3929
|
+
const outFile = path6.resolve(root, style.outDir, relFile);
|
|
3930
|
+
const qualityOutFile = style.quality ? path6.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
3374
3931
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
3375
3932
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
3376
3933
|
const base = {
|
|
@@ -3421,7 +3978,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3421
3978
|
minGridConfidence: style.quality.minGridConfidence,
|
|
3422
3979
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
3423
3980
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
3424
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
3981
|
+
...style.quality.fixerPython ? { fixerPython: path6.resolve(root, style.quality.fixerPython) } : {},
|
|
3982
|
+
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
3425
3983
|
}
|
|
3426
3984
|
} : {},
|
|
3427
3985
|
tags,
|
|
@@ -3449,9 +4007,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3449
4007
|
);
|
|
3450
4008
|
}
|
|
3451
4009
|
const sourceSpec = await finalize(asset.revision.from);
|
|
3452
|
-
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
4010
|
+
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path6.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
3453
4011
|
const sourceImage = await optionalRevisionImage(sourceFile, "revision source");
|
|
3454
|
-
const maskFile = asset.revision.mask ?
|
|
4012
|
+
const maskFile = asset.revision.mask ? path6.resolve(root, asset.revision.mask) : void 0;
|
|
3455
4013
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
3456
4014
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
3457
4015
|
throw new Error(
|
|
@@ -3507,349 +4065,92 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3507
4065
|
for (const spec of specs) {
|
|
3508
4066
|
const key = `${spec.styleId}/${spec.assetId}`;
|
|
3509
4067
|
claimOutput(spec.outFile, key);
|
|
3510
|
-
if (spec.quality)
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
}
|
|
3528
|
-
|
|
3529
|
-
const style = loaded.manifest.styles[styleId];
|
|
3530
|
-
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
3531
|
-
const out = [];
|
|
3532
|
-
for (const img of style.styleImages) {
|
|
3533
|
-
const file = path4.resolve(loaded.root, img.path);
|
|
3534
|
-
const buf = await readFile2(file);
|
|
3535
|
-
const metadata = imageMetadata(buf);
|
|
3536
|
-
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
3537
|
-
if (metadata.width < 1 || metadata.height < 1) {
|
|
3538
|
-
throw new Error(`Style image has invalid dimensions: ${file}`);
|
|
3539
|
-
}
|
|
3540
|
-
out.push({ base64: buf.toString("base64"), ...metadata });
|
|
3541
|
-
}
|
|
3542
|
-
return out;
|
|
3543
|
-
}
|
|
3544
|
-
function imageMetadata(buf) {
|
|
3545
|
-
if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
|
|
3546
|
-
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
|
|
3547
|
-
}
|
|
3548
|
-
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
|
|
3549
|
-
let offset = 2;
|
|
3550
|
-
while (offset + 3 < buf.length) {
|
|
3551
|
-
if (buf[offset] !== 255) return null;
|
|
3552
|
-
while (buf[offset] === 255) offset++;
|
|
3553
|
-
const marker = buf[offset++];
|
|
3554
|
-
if (marker == null || marker === 217 || marker === 218) break;
|
|
3555
|
-
if (marker === 1 || marker >= 208 && marker <= 215) continue;
|
|
3556
|
-
if (offset + 2 > buf.length) return null;
|
|
3557
|
-
const length = buf.readUInt16BE(offset);
|
|
3558
|
-
if (length < 2 || offset + length > buf.length) return null;
|
|
3559
|
-
const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
|
|
3560
|
-
if (isStartOfFrame) {
|
|
3561
|
-
if (length < 7) return null;
|
|
3562
|
-
return {
|
|
3563
|
-
width: buf.readUInt16BE(offset + 5),
|
|
3564
|
-
height: buf.readUInt16BE(offset + 3),
|
|
3565
|
-
format: "jpeg"
|
|
3566
|
-
};
|
|
3567
|
-
}
|
|
3568
|
-
offset += length;
|
|
3569
|
-
}
|
|
3570
|
-
return null;
|
|
3571
|
-
}
|
|
3572
|
-
|
|
3573
|
-
// src/pipeline/pack.ts
|
|
3574
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
3575
|
-
import path7 from "path";
|
|
3576
|
-
|
|
3577
|
-
// src/outputs.ts
|
|
3578
|
-
import path6 from "path";
|
|
3579
|
-
|
|
3580
|
-
// src/lock.ts
|
|
3581
|
-
import { readFile as readFile3, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
|
|
3582
|
-
import { existsSync as existsSync4 } from "fs";
|
|
3583
|
-
import path5 from "path";
|
|
3584
|
-
async function loadLock(lockPath) {
|
|
3585
|
-
if (!existsSync4(lockPath)) return { version: 2, entries: {} };
|
|
3586
|
-
try {
|
|
3587
|
-
return parseLock(JSON.parse(await readFile3(lockPath, "utf8")));
|
|
3588
|
-
} catch (err) {
|
|
3589
|
-
throw new Error(
|
|
3590
|
-
`Lockfile at ${lockPath} is malformed:
|
|
3591
|
-
${err instanceof Error ? err.message : String(err)}`
|
|
3592
|
-
);
|
|
3593
|
-
}
|
|
3594
|
-
}
|
|
3595
|
-
var saveQueues = /* @__PURE__ */ new Map();
|
|
3596
|
-
var dirtyPatches = /* @__PURE__ */ new WeakMap();
|
|
3597
|
-
var dirtyDeletes = /* @__PURE__ */ new WeakMap();
|
|
3598
|
-
function saveLock(lockPath, lock) {
|
|
3599
|
-
const queueKey = path5.resolve(lockPath);
|
|
3600
|
-
const previous = saveQueues.get(queueKey) ?? Promise.resolve();
|
|
3601
|
-
const next = previous.catch(() => {
|
|
3602
|
-
}).then(() => writeLockNow(lockPath, lock));
|
|
3603
|
-
saveQueues.set(queueKey, next);
|
|
3604
|
-
const cleanup = () => {
|
|
3605
|
-
if (saveQueues.get(queueKey) === next) saveQueues.delete(queueKey);
|
|
3606
|
-
};
|
|
3607
|
-
void next.then(cleanup, cleanup);
|
|
3608
|
-
return next;
|
|
3609
|
-
}
|
|
3610
|
-
async function writeLockNow(lockPath, lock) {
|
|
3611
|
-
await mkdir(path5.dirname(path5.resolve(lockPath)), { recursive: true });
|
|
3612
|
-
const release = await acquireFileLock(lockPath);
|
|
3613
|
-
try {
|
|
3614
|
-
await writeLockWhileHeld(lockPath, lock);
|
|
3615
|
-
} finally {
|
|
3616
|
-
await release();
|
|
3617
|
-
}
|
|
3618
|
-
}
|
|
3619
|
-
async function writeLockWhileHeld(file, lock) {
|
|
3620
|
-
let disk = { version: 2, entries: {} };
|
|
3621
|
-
if (existsSync4(file)) {
|
|
3622
|
-
try {
|
|
3623
|
-
disk = parseLock(JSON.parse(await readFile3(file, "utf8")));
|
|
3624
|
-
} catch (err) {
|
|
3625
|
-
throw new Error(
|
|
3626
|
-
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
3627
|
-
);
|
|
3628
|
-
}
|
|
3629
|
-
}
|
|
3630
|
-
const dirty = dirtyPatches.get(lock) ?? /* @__PURE__ */ new Map();
|
|
3631
|
-
const snapshot = /* @__PURE__ */ new Map();
|
|
3632
|
-
for (const [key, patch] of dirty) snapshot.set(key, { ...patch });
|
|
3633
|
-
const merged = { ...disk.entries };
|
|
3634
|
-
for (const [key, patch] of snapshot) {
|
|
3635
|
-
const local = lock.entries[key];
|
|
3636
|
-
if (!local) continue;
|
|
3637
|
-
merged[key] = merged[key] ? { ...merged[key], ...patch } : local;
|
|
3638
|
-
}
|
|
3639
|
-
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
3640
|
-
if (!merged[key]) merged[key] = entry;
|
|
3641
|
-
}
|
|
3642
|
-
const deletes = dirtyDeletes.get(lock);
|
|
3643
|
-
const deleteSnapshot = deletes ? new Set(deletes) : /* @__PURE__ */ new Set();
|
|
3644
|
-
for (const key of deleteSnapshot) delete merged[key];
|
|
3645
|
-
const sorted = {};
|
|
3646
|
-
for (const key of Object.keys(merged).sort()) {
|
|
3647
|
-
sorted[key] = merged[key];
|
|
3648
|
-
}
|
|
3649
|
-
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3650
|
-
try {
|
|
3651
|
-
await writeFile(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
|
|
3652
|
-
await rename(tmp, file);
|
|
3653
|
-
} finally {
|
|
3654
|
-
await rm(tmp, { force: true });
|
|
3655
|
-
}
|
|
3656
|
-
const currentDirty = dirtyPatches.get(lock);
|
|
3657
|
-
if (currentDirty) {
|
|
3658
|
-
for (const [key, patch] of snapshot) {
|
|
3659
|
-
const current = currentDirty.get(key);
|
|
3660
|
-
if (!current) continue;
|
|
3661
|
-
for (const [field, value] of Object.entries(patch)) {
|
|
3662
|
-
if (Object.is(current[field], value)) {
|
|
3663
|
-
delete current[field];
|
|
3664
|
-
}
|
|
3665
|
-
}
|
|
3666
|
-
if (Object.keys(current).length === 0) currentDirty.delete(key);
|
|
3667
|
-
}
|
|
3668
|
-
}
|
|
3669
|
-
const stillDirty = dirtyDeletes.get(lock);
|
|
3670
|
-
if (stillDirty) {
|
|
3671
|
-
for (const key of deleteSnapshot) stillDirty.delete(key);
|
|
3672
|
-
if (stillDirty.size === 0) dirtyDeletes.delete(lock);
|
|
3673
|
-
}
|
|
3674
|
-
const localAfterWrite = lock.entries;
|
|
3675
|
-
lock.entries = { ...merged };
|
|
3676
|
-
for (const [key, patch] of currentDirty ?? []) {
|
|
3677
|
-
const base = lock.entries[key] ?? localAfterWrite[key];
|
|
3678
|
-
if (base) lock.entries[key] = { ...base, ...patch };
|
|
3679
|
-
}
|
|
3680
|
-
}
|
|
3681
|
-
function upsert(lock, key, patch) {
|
|
3682
|
-
dirtyDeletes.get(lock)?.delete(key);
|
|
3683
|
-
const existing = lock.entries[key];
|
|
3684
|
-
const next = { ...existing, ...patch };
|
|
3685
|
-
lock.entries[key] = next;
|
|
3686
|
-
let dirty = dirtyPatches.get(lock);
|
|
3687
|
-
if (!dirty) {
|
|
3688
|
-
dirty = /* @__PURE__ */ new Map();
|
|
3689
|
-
dirtyPatches.set(lock, dirty);
|
|
3690
|
-
}
|
|
3691
|
-
dirty.set(key, { ...dirty.get(key) ?? {}, ...patch });
|
|
3692
|
-
return next;
|
|
3693
|
-
}
|
|
3694
|
-
function remove(lock, key) {
|
|
3695
|
-
if (!lock.entries[key]) return false;
|
|
3696
|
-
delete lock.entries[key];
|
|
3697
|
-
const patches = dirtyPatches.get(lock);
|
|
3698
|
-
if (patches) {
|
|
3699
|
-
patches.delete(key);
|
|
3700
|
-
if (patches.size === 0) dirtyPatches.delete(lock);
|
|
3701
|
-
}
|
|
3702
|
-
let deletes = dirtyDeletes.get(lock);
|
|
3703
|
-
if (!deletes) {
|
|
3704
|
-
deletes = /* @__PURE__ */ new Set();
|
|
3705
|
-
dirtyDeletes.set(lock, deletes);
|
|
3706
|
-
}
|
|
3707
|
-
deletes.add(key);
|
|
3708
|
-
return true;
|
|
3709
|
-
}
|
|
3710
|
-
var FILE_LOCK_TIMEOUT_MS = 1e4;
|
|
3711
|
-
var STALE_FILE_LOCK_MS = 2 * 60 * 1e3;
|
|
3712
|
-
async function acquireFileLock(file) {
|
|
3713
|
-
const lockDir = `${file}.lock`;
|
|
3714
|
-
const deadline = Date.now() + FILE_LOCK_TIMEOUT_MS;
|
|
3715
|
-
for (; ; ) {
|
|
3716
|
-
try {
|
|
3717
|
-
await mkdir(lockDir);
|
|
3718
|
-
return async () => {
|
|
3719
|
-
await rmdir(lockDir).catch(() => {
|
|
3720
|
-
});
|
|
3721
|
-
};
|
|
3722
|
-
} catch (err) {
|
|
3723
|
-
const code = err.code;
|
|
3724
|
-
if (code !== "EEXIST") throw err;
|
|
3725
|
-
try {
|
|
3726
|
-
const age = Date.now() - (await stat(lockDir)).mtimeMs;
|
|
3727
|
-
if (age > STALE_FILE_LOCK_MS) {
|
|
3728
|
-
await rmdir(lockDir);
|
|
3729
|
-
continue;
|
|
3730
|
-
}
|
|
3731
|
-
} catch (lockErr) {
|
|
3732
|
-
if (lockErr.code === "ENOENT") continue;
|
|
3733
|
-
}
|
|
3734
|
-
if (Date.now() >= deadline) {
|
|
3735
|
-
throw new Error(`Timed out waiting for lockfile writer ${lockDir}`);
|
|
4068
|
+
if (spec.quality) {
|
|
4069
|
+
if (spec.generator === "frames") {
|
|
4070
|
+
const frameInputs = Object.values(spec.providerInputs ?? {}).find(Array.isArray);
|
|
4071
|
+
const count = frameInputs?.length ?? 0;
|
|
4072
|
+
for (let index = 0; index < count; index++) {
|
|
4073
|
+
const role = `frame-${String(index).padStart(2, "0")}`;
|
|
4074
|
+
claimOutput(
|
|
4075
|
+
expectedOutputPath(
|
|
4076
|
+
{ ...spec, outFile: spec.quality.outFile },
|
|
4077
|
+
role,
|
|
4078
|
+
index,
|
|
4079
|
+
count,
|
|
4080
|
+
MediaType.PNG
|
|
4081
|
+
),
|
|
4082
|
+
`${key} quality ${role}`
|
|
4083
|
+
);
|
|
4084
|
+
}
|
|
4085
|
+
} else {
|
|
4086
|
+
claimOutput(spec.quality.outFile, `${key} quality output`);
|
|
3736
4087
|
}
|
|
3737
|
-
await new Promise((resolve) => setTimeout(resolve, 25 + Math.floor(Math.random() * 25)));
|
|
3738
4088
|
}
|
|
3739
4089
|
}
|
|
4090
|
+
return specs;
|
|
3740
4091
|
}
|
|
3741
|
-
function
|
|
3742
|
-
const
|
|
3743
|
-
|
|
3744
|
-
const unit = entry.costUnit ?? "generations";
|
|
3745
|
-
totals[unit] = (totals[unit] ?? 0) + (entry.cost ?? 0);
|
|
3746
|
-
}
|
|
3747
|
-
return totals;
|
|
3748
|
-
}
|
|
3749
|
-
|
|
3750
|
-
// src/outputs.ts
|
|
3751
|
-
function portableOutputPath(file, manifestDir) {
|
|
3752
|
-
const root = path6.resolve(manifestDir);
|
|
3753
|
-
const absolute = path6.isAbsolute(file) ? path6.normalize(file) : path6.resolve(root, file);
|
|
3754
|
-
const relative = path6.relative(root, absolute);
|
|
3755
|
-
const value = path6.isAbsolute(relative) ? absolute : relative;
|
|
3756
|
-
return value.split(path6.sep).join("/");
|
|
3757
|
-
}
|
|
3758
|
-
function resolveOutputPath(recordedPath, manifestDir) {
|
|
3759
|
-
if (path6.isAbsolute(recordedPath)) return path6.normalize(recordedPath);
|
|
3760
|
-
if (path6.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
3761
|
-
return path6.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path6.sep));
|
|
3762
|
-
}
|
|
3763
|
-
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
3764
|
-
const originalExt = path6.extname(spec.outFile);
|
|
3765
|
-
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
3766
|
-
const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
|
|
3767
|
-
if (total === 1) return `${stem}${ext}`;
|
|
3768
|
-
const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
3769
|
-
return `${stem}-${safeRole}${ext}`;
|
|
3770
|
-
}
|
|
3771
|
-
function currentOutputPath(output, spec, index, total) {
|
|
3772
|
-
return expectedOutputPath(spec, output.role, index, total, output.mediaType);
|
|
3773
|
-
}
|
|
3774
|
-
function currentEntryOutputPath(entry, spec, index) {
|
|
3775
|
-
const output = entry.outputs[index];
|
|
3776
|
-
const sourceIndex = output.role ? (entry.sourceUrls ?? []).findIndex((source) => source.role === output.role) : -1;
|
|
3777
|
-
const position = sourceIndex >= 0 ? sourceIndex : index;
|
|
3778
|
-
const total = Math.max(entry.outputs.length, entry.sourceUrls?.length ?? 0);
|
|
3779
|
-
return currentOutputPath(output, spec, position, total);
|
|
4092
|
+
function pngPath(file) {
|
|
4093
|
+
const extension = path6.extname(file);
|
|
4094
|
+
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
3780
4095
|
}
|
|
3781
|
-
function
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
const outputs = entry.outputs.map((output, index) => {
|
|
3789
|
-
const absolute = currentEntryOutputPath(entry, spec, index);
|
|
3790
|
-
const portable = portableOutputPath(absolute, spec.root);
|
|
3791
|
-
if (portable === output.path) return output;
|
|
3792
|
-
changed++;
|
|
3793
|
-
entryChanged = true;
|
|
3794
|
-
return { ...output, path: portable };
|
|
3795
|
-
});
|
|
3796
|
-
if (entryChanged) upsert(lock, key, { outputs });
|
|
4096
|
+
async function optionalRevisionImage(file, label, requiredFormat) {
|
|
4097
|
+
if (!existsSync4(file)) return null;
|
|
4098
|
+
const bytes = await readFile3(file);
|
|
4099
|
+
const metadata = imageMetadata(bytes);
|
|
4100
|
+
if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
|
|
4101
|
+
if (requiredFormat && metadata.format !== requiredFormat) {
|
|
4102
|
+
throw new Error(`${label} must be ${requiredFormat.toUpperCase()}: ${file}`);
|
|
3797
4103
|
}
|
|
3798
|
-
return
|
|
3799
|
-
}
|
|
3800
|
-
function fallbackOutputRole(index) {
|
|
3801
|
-
return `output-${String(index).padStart(2, "0")}`;
|
|
3802
|
-
}
|
|
3803
|
-
function outputId(assetId, output, index, total) {
|
|
3804
|
-
if (total === 1) return assetId;
|
|
3805
|
-
return `${assetId}/${output.role ?? fallbackOutputRole(index)}`;
|
|
3806
|
-
}
|
|
3807
|
-
function resolveEntryOutputs(entry, assetId, manifestDir) {
|
|
3808
|
-
return entry.outputs.map((output, index) => ({
|
|
3809
|
-
...output,
|
|
3810
|
-
assetId,
|
|
3811
|
-
id: outputId(assetId, output, index, entry.outputs.length),
|
|
3812
|
-
index,
|
|
3813
|
-
absolutePath: resolveOutputPath(output.path, manifestDir)
|
|
3814
|
-
}));
|
|
3815
|
-
}
|
|
3816
|
-
function resolveSpecEntryOutputs(entry, spec) {
|
|
3817
|
-
return entry.outputs.map((output, index) => ({
|
|
3818
|
-
...output,
|
|
3819
|
-
assetId: spec.assetId,
|
|
3820
|
-
id: outputId(spec.assetId, output, index, entry.outputs.length),
|
|
3821
|
-
index,
|
|
3822
|
-
absolutePath: currentEntryOutputPath(entry, spec, index)
|
|
3823
|
-
}));
|
|
4104
|
+
return { hash: sha256(bytes), ...metadata };
|
|
3824
4105
|
}
|
|
3825
|
-
function
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
4106
|
+
async function resolveStyleImages(loaded, styleId) {
|
|
4107
|
+
const style = loaded.manifest.styles[styleId];
|
|
4108
|
+
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
4109
|
+
const out = [];
|
|
4110
|
+
for (const img of style.styleImages) {
|
|
4111
|
+
const file = path6.resolve(loaded.root, img.path);
|
|
4112
|
+
const buf = await readFile3(file);
|
|
4113
|
+
const metadata = imageMetadata(buf);
|
|
4114
|
+
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
4115
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
4116
|
+
throw new Error(`Style image has invalid dimensions: ${file}`);
|
|
4117
|
+
}
|
|
4118
|
+
out.push({ base64: buf.toString("base64"), ...metadata });
|
|
3829
4119
|
}
|
|
3830
|
-
|
|
3831
|
-
if (entry.outputs.length === 1) return { ok: true, output: entry.outputs[0] };
|
|
3832
|
-
const primary = entry.outputs.filter((output) => !output.role);
|
|
3833
|
-
if (primary.length === 1) return { ok: true, output: primary[0] };
|
|
3834
|
-
return {
|
|
3835
|
-
ok: false,
|
|
3836
|
-
reason: `${entry.outputs.length} outputs are recorded; set \`outputRole\` to choose one`
|
|
3837
|
-
};
|
|
4120
|
+
return out;
|
|
3838
4121
|
}
|
|
3839
|
-
function
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
4122
|
+
function imageMetadata(buf) {
|
|
4123
|
+
if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
|
|
4124
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
|
|
4125
|
+
}
|
|
4126
|
+
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
|
|
4127
|
+
let offset = 2;
|
|
4128
|
+
while (offset + 3 < buf.length) {
|
|
4129
|
+
if (buf[offset] !== 255) return null;
|
|
4130
|
+
while (buf[offset] === 255) offset++;
|
|
4131
|
+
const marker = buf[offset++];
|
|
4132
|
+
if (marker == null || marker === 217 || marker === 218) break;
|
|
4133
|
+
if (marker === 1 || marker >= 208 && marker <= 215) continue;
|
|
4134
|
+
if (offset + 2 > buf.length) return null;
|
|
4135
|
+
const length = buf.readUInt16BE(offset);
|
|
4136
|
+
if (length < 2 || offset + length > buf.length) return null;
|
|
4137
|
+
const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
|
|
4138
|
+
if (isStartOfFrame) {
|
|
4139
|
+
if (length < 7) return null;
|
|
4140
|
+
return {
|
|
4141
|
+
width: buf.readUInt16BE(offset + 5),
|
|
4142
|
+
height: buf.readUInt16BE(offset + 3),
|
|
4143
|
+
format: "jpeg"
|
|
4144
|
+
};
|
|
4145
|
+
}
|
|
4146
|
+
offset += length;
|
|
4147
|
+
}
|
|
4148
|
+
return null;
|
|
3850
4149
|
}
|
|
3851
4150
|
|
|
3852
4151
|
// src/pipeline/pack.ts
|
|
4152
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
4153
|
+
import path7 from "path";
|
|
3853
4154
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
3854
4155
|
if (!Array.isArray(raw) || !raw.length) {
|
|
3855
4156
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
@@ -3928,7 +4229,7 @@ function packSprites(inputs, options = {}) {
|
|
|
3928
4229
|
function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
3929
4230
|
if (options.sourceOverrides) {
|
|
3930
4231
|
if (options.outputRoles?.length) {
|
|
3931
|
-
throw new Error("Quality-profile packing does not accept --output-role; profiles own
|
|
4232
|
+
throw new Error("Quality-profile packing does not accept --output-role; profiles own final output selection.");
|
|
3932
4233
|
}
|
|
3933
4234
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
3934
4235
|
id,
|
|
@@ -4740,6 +5041,8 @@ function hex(c) {
|
|
|
4740
5041
|
|
|
4741
5042
|
// src/pipeline/refine.ts
|
|
4742
5043
|
var execFileAsync = promisify(execFile);
|
|
5044
|
+
var PIXEL_ART_FIXER_MIN_SIDE = 16;
|
|
5045
|
+
var PIXEL_ART_FIXER_MAX_PIXELS = 4e6;
|
|
4743
5046
|
var PIXEL_ART_FIXER_URL = "https://github.com/Retro-Diffusion/pixel-art-fixer";
|
|
4744
5047
|
var PIXEL_ART_FIXER_REVISION = "ef376e57e1c272633ca2dbf5f29ec3fcf6596465";
|
|
4745
5048
|
function message2(error) {
|
|
@@ -4787,25 +5090,33 @@ function firstJsonObject(text) {
|
|
|
4787
5090
|
}
|
|
4788
5091
|
var PYTHON_FIXER_BRIDGE = [
|
|
4789
5092
|
"import json, sys",
|
|
4790
|
-
"from
|
|
4791
|
-
"
|
|
4792
|
-
"
|
|
4793
|
-
"
|
|
5093
|
+
"from PIL import Image",
|
|
5094
|
+
"from pixelfixer.api import _load",
|
|
5095
|
+
"from pixelfixer.core import detect",
|
|
5096
|
+
"from pixelfixer.reconstruct import two_stage_pack",
|
|
5097
|
+
"rgba = _load(open(sys.argv[1], 'rb').read())",
|
|
5098
|
+
"result = detect(rgba, mode='full')",
|
|
5099
|
+
"Image.fromarray(two_stage_pack(rgba, result['cols'], result['rows'])).save(sys.argv[2], 'PNG')",
|
|
5100
|
+
"print(json.dumps({key: result[key] for key in ('cols', 'rows', 'step_x', 'step_y', 'phase_x', 'phase_y', 'consensus')}))"
|
|
4794
5101
|
].join("; ");
|
|
4795
5102
|
function parseDetection(raw) {
|
|
4796
5103
|
if (!raw || typeof raw !== "object") throw new Error("Pixel Art Fixer returned no detection result.");
|
|
4797
5104
|
const value = raw;
|
|
4798
5105
|
const stepX = Number(value.step_x);
|
|
4799
5106
|
const stepY = Number(value.step_y);
|
|
5107
|
+
const phaseX = Number(value.phase_x);
|
|
5108
|
+
const phaseY = Number(value.phase_y);
|
|
4800
5109
|
const columns = Number(value.cols);
|
|
4801
5110
|
const rows = Number(value.rows);
|
|
4802
5111
|
const consensus = value.consensus;
|
|
4803
|
-
if (!Number.isFinite(stepX) || stepX <= 0 || !Number.isFinite(stepY) || stepY <= 0 || !Number.isInteger(columns) || columns < 1 || !Number.isInteger(rows) || rows < 1 || typeof consensus !== "string" || !consensus) {
|
|
5112
|
+
if (!Number.isFinite(stepX) || stepX <= 0 || !Number.isFinite(stepY) || stepY <= 0 || !Number.isFinite(phaseX) || phaseX < 0 || phaseX > stepX || !Number.isFinite(phaseY) || phaseY < 0 || phaseY > stepY || !Number.isInteger(columns) || columns < 1 || !Number.isInteger(rows) || rows < 1 || typeof consensus !== "string" || !consensus) {
|
|
4804
5113
|
throw new Error("Pixel Art Fixer returned an invalid grid detection result.");
|
|
4805
5114
|
}
|
|
4806
5115
|
return {
|
|
4807
5116
|
stepX,
|
|
4808
5117
|
stepY,
|
|
5118
|
+
phaseX,
|
|
5119
|
+
phaseY,
|
|
4809
5120
|
columns,
|
|
4810
5121
|
rows,
|
|
4811
5122
|
consensus,
|
|
@@ -4916,6 +5227,23 @@ function auditRefinement(png, detection, palette, minTransparency, minGridConfid
|
|
|
4916
5227
|
function isGridConfidence(value) {
|
|
4917
5228
|
return value === "low" || value === "medium" || value === "high";
|
|
4918
5229
|
}
|
|
5230
|
+
function isDetection(value) {
|
|
5231
|
+
if (!value || typeof value !== "object") return false;
|
|
5232
|
+
const detection = value;
|
|
5233
|
+
return Number.isFinite(detection.stepX) && detection.stepX > 0 && Number.isFinite(detection.stepY) && detection.stepY > 0 && Number.isFinite(detection.phaseX) && detection.phaseX >= 0 && detection.phaseX <= detection.stepX && Number.isFinite(detection.phaseY) && detection.phaseY >= 0 && detection.phaseY <= detection.stepY && Number.isInteger(detection.columns) && detection.columns > 0 && Number.isInteger(detection.rows) && detection.rows > 0 && typeof detection.consensus === "string" && !!detection.consensus && isGridConfidence(detection.confidence);
|
|
5234
|
+
}
|
|
5235
|
+
function isAudit(value) {
|
|
5236
|
+
if (!value || typeof value !== "object") return false;
|
|
5237
|
+
const audit = value;
|
|
5238
|
+
return Number.isInteger(audit.width) && audit.width > 0 && Number.isInteger(audit.height) && audit.height > 0 && Number.isInteger(audit.colorCount) && audit.colorCount >= 0 && typeof audit.paletteConformant === "boolean" && Number.isFinite(audit.transparency) && audit.transparency >= 0 && audit.transparency <= 1 && !!audit.thresholds && Number.isInteger(audit.thresholds.maxColors) && audit.thresholds.maxColors > 0 && (audit.thresholds.minTransparency === null || Number.isFinite(audit.thresholds.minTransparency) && audit.thresholds.minTransparency >= 0 && audit.thresholds.minTransparency <= 1) && isGridConfidence(audit.thresholds.minGridConfidence) && typeof audit.safe === "boolean" && Array.isArray(audit.reasons) && audit.reasons.every((reason) => typeof reason === "string");
|
|
5239
|
+
}
|
|
5240
|
+
function validFrameSet(value) {
|
|
5241
|
+
if (!value || typeof value !== "object") return false;
|
|
5242
|
+
const frameSet = value;
|
|
5243
|
+
return Number.isInteger(frameSet.fps) && frameSet.fps >= 1 && frameSet.fps <= 60 && Number.isInteger(frameSet.count) && frameSet.count >= 2 && frameSet.count <= 64 && Array.isArray(frameSet.roles) && frameSet.roles.length === frameSet.count && new Set(frameSet.roles).size === frameSet.roles.length && frameSet.roles.every((role) => typeof role === "string" && !!role) && Array.isArray(frameSet.frames) && frameSet.frames.length === frameSet.count && frameSet.frames.every(
|
|
5244
|
+
(frame, index) => frame.role === frameSet.roles[index] && Number.isInteger(frame.sourceWidth) && frame.sourceWidth > 0 && Number.isInteger(frame.sourceHeight) && frame.sourceHeight > 0 && isDetection(frame.detection) && isAudit(frame.audit)
|
|
5245
|
+
);
|
|
5246
|
+
}
|
|
4919
5247
|
function parseRecordOptions(manifest, record) {
|
|
4920
5248
|
if (manifest.kind !== "refine") throw new Error(`${record} is not a PixelKiln refinement record.`);
|
|
4921
5249
|
const raw = manifest.options;
|
|
@@ -4925,11 +5253,26 @@ function parseRecordOptions(manifest, record) {
|
|
|
4925
5253
|
const palette = options.palette;
|
|
4926
5254
|
const audit = options.audit;
|
|
4927
5255
|
const review = options.review;
|
|
4928
|
-
if (options.schema !== "pixelkiln-quality" || options.version !== 1 || !grid || grid.tool !== "pixel-art-fixer" || grid.url !== PIXEL_ART_FIXER_URL || typeof grid.revision !== "string" || !grid.revision || grid.protocol !== "python-api-v1" && grid.protocol !== "python-cli-v1" || !isGridConfidence(grid.confidence) || typeof grid.consensus !== "string" || !Number.isFinite(grid.stepX) || !Number.isFinite(grid.stepY) || !Number.isInteger(grid.sourceWidth) || !Number.isInteger(grid.sourceHeight) || !Number.isInteger(grid.nativeWidth) || !Number.isInteger(grid.nativeHeight) || !palette || !Array.isArray(palette.colors) || palette.colors.length < 2 || !palette.colors.every((color) => typeof color === "string") || palette.dither !== "none" || palette.distance !== "redmean" || !
|
|
5256
|
+
if (options.schema !== "pixelkiln-quality" || options.version !== 1 || !grid || grid.tool !== "pixel-art-fixer" || grid.url !== PIXEL_ART_FIXER_URL || typeof grid.revision !== "string" || !grid.revision || grid.protocol !== "python-api-v1" && grid.protocol !== "python-core-v1" && grid.protocol !== "python-cli-v1" || !isGridConfidence(grid.confidence) || typeof grid.consensus !== "string" || !Number.isFinite(grid.stepX) || !Number.isFinite(grid.stepY) || (grid.phaseX !== void 0 || grid.phaseY !== void 0 || options.frameSet !== void 0) && (!Number.isFinite(grid.phaseX) || !Number.isFinite(grid.phaseY)) || !Number.isInteger(grid.sourceWidth) || !Number.isInteger(grid.sourceHeight) || !Number.isInteger(grid.nativeWidth) || !Number.isInteger(grid.nativeHeight) || !palette || !Array.isArray(palette.colors) || palette.colors.length < 2 || !palette.colors.every((color) => typeof color === "string") || palette.dither !== "none" || palette.distance !== "redmean" || !isAudit(audit) || !review || review.status !== "pending" && review.status !== "approved" || options.frameSet !== void 0 && !validFrameSet(options.frameSet)) {
|
|
4929
5257
|
throw new Error(`${record} has invalid quality metadata.`);
|
|
4930
5258
|
}
|
|
4931
5259
|
return options;
|
|
4932
5260
|
}
|
|
5261
|
+
function safeFrameRole(role, index) {
|
|
5262
|
+
const safe = role.trim().replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5263
|
+
return safe || `frame-${String(index).padStart(2, "0")}`;
|
|
5264
|
+
}
|
|
5265
|
+
function qualityFrameOutputPath(output, role, index) {
|
|
5266
|
+
if (!/\.png$/i.test(output)) throw new Error("Refined frame-set output must use a .png stem.");
|
|
5267
|
+
return output.replace(/\.png$/i, `-${safeFrameRole(role, index)}.png`);
|
|
5268
|
+
}
|
|
5269
|
+
function sameFrameGrid(first, next) {
|
|
5270
|
+
const phaseMatches = (a, b, step) => {
|
|
5271
|
+
const distance = Math.abs(a - b) % step;
|
|
5272
|
+
return Math.min(distance, step - distance) <= 0.01;
|
|
5273
|
+
};
|
|
5274
|
+
return next.detection.stepX === first.detection.stepX && next.detection.stepY === first.detection.stepY && next.detection.columns === first.detection.columns && next.detection.rows === first.detection.rows && phaseMatches(next.detection.phaseX, first.detection.phaseX, first.detection.stepX) && phaseMatches(next.detection.phaseY, first.detection.phaseY, first.detection.stepY) && next.audit.width === first.audit.width && next.audit.height === first.audit.height && next.sourceWidth === first.sourceWidth && next.sourceHeight === first.sourceHeight;
|
|
5275
|
+
}
|
|
4933
5276
|
async function refineAsset(options) {
|
|
4934
5277
|
const source = path10.resolve(options.source);
|
|
4935
5278
|
const output = path10.resolve(options.output);
|
|
@@ -4944,9 +5287,21 @@ async function refineAsset(options) {
|
|
|
4944
5287
|
} catch (error) {
|
|
4945
5288
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
4946
5289
|
}
|
|
5290
|
+
const compatibilityCommand = options.fixerCommand;
|
|
5291
|
+
if (!compatibilityCommand) {
|
|
5292
|
+
if (sourcePng.width < PIXEL_ART_FIXER_MIN_SIDE || sourcePng.height < PIXEL_ART_FIXER_MIN_SIDE) {
|
|
5293
|
+
throw new Error(
|
|
5294
|
+
`Pixel Art Fixer requires each source side to be at least ${PIXEL_ART_FIXER_MIN_SIDE}px; got ${sourcePng.width}x${sourcePng.height}.`
|
|
5295
|
+
);
|
|
5296
|
+
}
|
|
5297
|
+
if (sourcePng.width * sourcePng.height > PIXEL_ART_FIXER_MAX_PIXELS) {
|
|
5298
|
+
throw new Error(
|
|
5299
|
+
`Pixel Art Fixer accepts at most ${PIXEL_ART_FIXER_MAX_PIXELS.toLocaleString("en-US")} source pixels; got ${sourcePng.width}x${sourcePng.height}.`
|
|
5300
|
+
);
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
4947
5303
|
const temp = await mkdtemp(path10.join(tmpdir(), "pixelkiln-refine-"));
|
|
4948
5304
|
const recovered = path10.join(temp, "native.png");
|
|
4949
|
-
const compatibilityCommand = options.fixerCommand;
|
|
4950
5305
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
4951
5306
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
4952
5307
|
try {
|
|
@@ -4993,11 +5348,13 @@ async function refineAsset(options) {
|
|
|
4993
5348
|
tool: "pixel-art-fixer",
|
|
4994
5349
|
url: PIXEL_ART_FIXER_URL,
|
|
4995
5350
|
revision: options.fixerRevision ?? PIXEL_ART_FIXER_REVISION,
|
|
4996
|
-
protocol: compatibilityCommand ? "python-cli-v1" : "python-
|
|
5351
|
+
protocol: compatibilityCommand ? "python-cli-v1" : "python-core-v1",
|
|
4997
5352
|
confidence: detection.confidence,
|
|
4998
5353
|
consensus: detection.consensus,
|
|
4999
5354
|
stepX: detection.stepX,
|
|
5000
5355
|
stepY: detection.stepY,
|
|
5356
|
+
phaseX: detection.phaseX,
|
|
5357
|
+
phaseY: detection.phaseY,
|
|
5001
5358
|
sourceWidth: sourcePng.width,
|
|
5002
5359
|
sourceHeight: sourcePng.height,
|
|
5003
5360
|
nativeWidth: quantized.width,
|
|
@@ -5018,19 +5375,142 @@ async function refineAsset(options) {
|
|
|
5018
5375
|
await rm3(temp, { recursive: true, force: true });
|
|
5019
5376
|
}
|
|
5020
5377
|
}
|
|
5378
|
+
async function refineFrameSet(options) {
|
|
5379
|
+
if (!Number.isInteger(options.fps) || options.fps < 1 || options.fps > 60) {
|
|
5380
|
+
throw new Error("Frame-set fps must be an integer between 1 and 60.");
|
|
5381
|
+
}
|
|
5382
|
+
if (options.sources.length < 2 || options.sources.length > 64) {
|
|
5383
|
+
throw new Error(`A frame set must contain 2\u201364 ordered sources; got ${options.sources.length}.`);
|
|
5384
|
+
}
|
|
5385
|
+
const roles = options.sources.map((source) => source.role);
|
|
5386
|
+
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
5387
|
+
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
5388
|
+
}
|
|
5389
|
+
const output = path10.resolve(options.output);
|
|
5390
|
+
const record = path10.resolve(qualityRecordPath(output));
|
|
5391
|
+
const palette = normalizeRefinementPalette(options.palette);
|
|
5392
|
+
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
5393
|
+
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
5394
|
+
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
5395
|
+
}
|
|
5396
|
+
const sourceSet = new Set(options.sources.map((source) => path10.resolve(source.path)));
|
|
5397
|
+
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
5398
|
+
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
5399
|
+
}
|
|
5400
|
+
const temp = await mkdtemp(path10.join(tmpdir(), "pixelkiln-refine-frames-"));
|
|
5401
|
+
try {
|
|
5402
|
+
const frames = [];
|
|
5403
|
+
const outputs = [];
|
|
5404
|
+
const provenanceSources = [];
|
|
5405
|
+
const sourcePaths = [];
|
|
5406
|
+
let firstOptions = null;
|
|
5407
|
+
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
5408
|
+
const source = path10.resolve(sourceEntry.path);
|
|
5409
|
+
const temporaryOutput = path10.join(temp, `${String(index).padStart(2, "0")}.png`);
|
|
5410
|
+
const refined = await refineAsset({
|
|
5411
|
+
source,
|
|
5412
|
+
output: temporaryOutput,
|
|
5413
|
+
palette,
|
|
5414
|
+
fixerPython: options.fixerPython,
|
|
5415
|
+
fixerCommand: options.fixerCommand,
|
|
5416
|
+
fixerArgsPrefix: options.fixerArgsPrefix,
|
|
5417
|
+
fixerRevision: options.fixerRevision,
|
|
5418
|
+
minGridConfidence: options.minGridConfidence,
|
|
5419
|
+
minTransparency: options.minTransparency,
|
|
5420
|
+
force: true
|
|
5421
|
+
});
|
|
5422
|
+
const temporaryRecord = await readArtifactBundleManifest(refined.record);
|
|
5423
|
+
const frameOptions = parseRecordOptions(temporaryRecord, refined.record);
|
|
5424
|
+
firstOptions ??= frameOptions;
|
|
5425
|
+
frames.push({
|
|
5426
|
+
role: sourceEntry.role,
|
|
5427
|
+
sourceWidth: frameOptions.nativeGrid.sourceWidth,
|
|
5428
|
+
sourceHeight: frameOptions.nativeGrid.sourceHeight,
|
|
5429
|
+
detection: refined.detection,
|
|
5430
|
+
audit: refined.audit
|
|
5431
|
+
});
|
|
5432
|
+
sourcePaths.push(source);
|
|
5433
|
+
provenanceSources.push({
|
|
5434
|
+
id: sourceEntry.role,
|
|
5435
|
+
path: source,
|
|
5436
|
+
sha256: await sha256File(source),
|
|
5437
|
+
included: true
|
|
5438
|
+
});
|
|
5439
|
+
outputs.push({
|
|
5440
|
+
path: finalOutputs[index],
|
|
5441
|
+
data: await readFile6(temporaryOutput)
|
|
5442
|
+
});
|
|
5443
|
+
}
|
|
5444
|
+
const first = frames[0];
|
|
5445
|
+
const mismatch = frames.find((frame) => !sameFrameGrid(first, frame));
|
|
5446
|
+
if (mismatch) {
|
|
5447
|
+
throw new Error(
|
|
5448
|
+
`Frame-set native grid mismatch: ${first.role} detected ${first.detection.columns}x${first.detection.rows} at ${first.detection.stepX}x${first.detection.stepY}, phase ${first.detection.phaseX},${first.detection.phaseY}; but ${mismatch.role} detected ${mismatch.detection.columns}x${mismatch.detection.rows} at ${mismatch.detection.stepX}x${mismatch.detection.stepY}, phase ${mismatch.detection.phaseX},${mismatch.detection.phaseY}.`
|
|
5449
|
+
);
|
|
5450
|
+
}
|
|
5451
|
+
const reasons = frames.flatMap(
|
|
5452
|
+
(frame) => frame.audit.reasons.map((reason) => `${frame.role}: ${reason}`)
|
|
5453
|
+
);
|
|
5454
|
+
const audit = {
|
|
5455
|
+
width: first.audit.width,
|
|
5456
|
+
height: first.audit.height,
|
|
5457
|
+
colorCount: Math.max(...frames.map((frame) => frame.audit.colorCount)),
|
|
5458
|
+
paletteConformant: frames.every((frame) => frame.audit.paletteConformant),
|
|
5459
|
+
transparency: Math.min(...frames.map((frame) => frame.audit.transparency)),
|
|
5460
|
+
thresholds: first.audit.thresholds,
|
|
5461
|
+
safe: frames.every((frame) => frame.audit.safe) && reasons.length === 0,
|
|
5462
|
+
reasons
|
|
5463
|
+
};
|
|
5464
|
+
const baseOptions = firstOptions;
|
|
5465
|
+
const recordOptions = {
|
|
5466
|
+
...baseOptions,
|
|
5467
|
+
audit,
|
|
5468
|
+
review: { status: "pending" },
|
|
5469
|
+
frameSet: {
|
|
5470
|
+
fps: options.fps,
|
|
5471
|
+
count: frames.length,
|
|
5472
|
+
roles,
|
|
5473
|
+
frames
|
|
5474
|
+
}
|
|
5475
|
+
};
|
|
5476
|
+
await writeManagedArtifactBundle(
|
|
5477
|
+
record,
|
|
5478
|
+
outputs,
|
|
5479
|
+
{ kind: "refine", sources: provenanceSources, options: recordOptions },
|
|
5480
|
+
{ force: options.force }
|
|
5481
|
+
);
|
|
5482
|
+
return {
|
|
5483
|
+
sources: sourcePaths,
|
|
5484
|
+
outputs: outputs.map((item) => path10.resolve(item.path)),
|
|
5485
|
+
record,
|
|
5486
|
+
frames,
|
|
5487
|
+
palette,
|
|
5488
|
+
audit
|
|
5489
|
+
};
|
|
5490
|
+
} finally {
|
|
5491
|
+
await rm3(temp, { recursive: true, force: true });
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5021
5494
|
async function checkQualityRecord(recordPath) {
|
|
5022
5495
|
const record = path10.resolve(recordPath);
|
|
5023
5496
|
const manifest = await readArtifactBundleManifest(record);
|
|
5024
5497
|
const options = parseRecordOptions(manifest, record);
|
|
5025
5498
|
const verification = await verifyArtifactBundle(record);
|
|
5026
|
-
const
|
|
5027
|
-
const
|
|
5499
|
+
const sources = manifest.sources.map((source2) => path10.resolve(path10.dirname(record), source2.path));
|
|
5500
|
+
const outputs = manifest.outputs.map((output2) => path10.resolve(path10.dirname(record), output2.path));
|
|
5501
|
+
const source = sources[0] ?? "";
|
|
5502
|
+
const output = outputs[0] ?? "";
|
|
5503
|
+
const expectedCount = options.frameSet?.count ?? 1;
|
|
5028
5504
|
const reasons = [];
|
|
5029
5505
|
if (!verification.fingerprintValid) reasons.push("quality record fingerprint changed");
|
|
5030
5506
|
for (const source2 of verification.changedSources) reasons.push(`source changed: ${source2}`);
|
|
5031
5507
|
for (const changed of verification.changedOutputs) reasons.push(`output changed: ${changed}`);
|
|
5032
|
-
if (manifest.sources.length !==
|
|
5033
|
-
|
|
5508
|
+
if (manifest.sources.length !== expectedCount) {
|
|
5509
|
+
reasons.push(`quality record must contain exactly ${expectedCount} source(s)`);
|
|
5510
|
+
}
|
|
5511
|
+
if (manifest.outputs.length !== expectedCount) {
|
|
5512
|
+
reasons.push(`quality record must contain exactly ${expectedCount} output(s)`);
|
|
5513
|
+
}
|
|
5034
5514
|
if (!options.audit.safe) reasons.push(...options.audit.reasons.map((reason) => `audit: ${reason}`));
|
|
5035
5515
|
if (options.review.status !== "approved") reasons.push("human 1\xD7 review is pending");
|
|
5036
5516
|
return {
|
|
@@ -5041,6 +5521,8 @@ async function checkQualityRecord(recordPath) {
|
|
|
5041
5521
|
record,
|
|
5042
5522
|
source,
|
|
5043
5523
|
output,
|
|
5524
|
+
sources,
|
|
5525
|
+
outputs,
|
|
5044
5526
|
options,
|
|
5045
5527
|
reasons
|
|
5046
5528
|
};
|
|
@@ -5055,7 +5537,7 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5055
5537
|
if (!verification.current) {
|
|
5056
5538
|
const changes = [
|
|
5057
5539
|
...verification.changedSources.map((source) => `source ${source}`),
|
|
5058
|
-
...verification.changedOutputs.map((
|
|
5540
|
+
...verification.changedOutputs.map((output) => `output ${output}`),
|
|
5059
5541
|
...!verification.fingerprintValid ? ["quality metadata"] : []
|
|
5060
5542
|
];
|
|
5061
5543
|
throw new Error(`Cannot approve a stale refinement; changed: ${changes.join(", ")}. Run refine again.`);
|
|
@@ -5063,12 +5545,17 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5063
5545
|
if (!options.audit.safe) {
|
|
5064
5546
|
throw new Error(`Cannot approve a failed quality audit: ${options.audit.reasons.join("; ")}`);
|
|
5065
5547
|
}
|
|
5066
|
-
|
|
5067
|
-
|
|
5548
|
+
const expectedCount = options.frameSet?.count ?? 1;
|
|
5549
|
+
if (manifest.sources.length !== expectedCount || manifest.outputs.length !== expectedCount) {
|
|
5550
|
+
throw new Error(
|
|
5551
|
+
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
5552
|
+
);
|
|
5068
5553
|
}
|
|
5069
5554
|
const root = path10.dirname(record);
|
|
5070
|
-
const
|
|
5071
|
-
|
|
5555
|
+
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
5556
|
+
const outputPath = path10.resolve(root, entry.path);
|
|
5557
|
+
return { path: outputPath, data: await readFile6(outputPath) };
|
|
5558
|
+
}));
|
|
5072
5559
|
const sources = manifest.sources.map((source) => ({
|
|
5073
5560
|
...source,
|
|
5074
5561
|
path: path10.resolve(root, source.path)
|
|
@@ -5091,7 +5578,7 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5091
5578
|
};
|
|
5092
5579
|
await writeManagedArtifactBundle(
|
|
5093
5580
|
record,
|
|
5094
|
-
|
|
5581
|
+
outputs,
|
|
5095
5582
|
{ kind: "refine", sources, options: approved }
|
|
5096
5583
|
);
|
|
5097
5584
|
return checkQualityRecord(record);
|
|
@@ -5100,33 +5587,46 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5100
5587
|
// src/pipeline/quality-profile.ts
|
|
5101
5588
|
async function qualitySource(spec, lock) {
|
|
5102
5589
|
if (spec.source) {
|
|
5103
|
-
|
|
5104
|
-
|
|
5590
|
+
if (spec.generator === "frames") {
|
|
5591
|
+
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
5592
|
+
}
|
|
5593
|
+
const source = path11.resolve(spec.root, spec.source);
|
|
5594
|
+
return existsSync6(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
5105
5595
|
}
|
|
5106
5596
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
5107
|
-
if (!entry) return {
|
|
5597
|
+
if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
|
|
5108
5598
|
if (entry.specHash !== spec.specHash) {
|
|
5109
|
-
return {
|
|
5599
|
+
return { items: null, reason: "raw provider output does not match the current generation spec" };
|
|
5110
5600
|
}
|
|
5111
5601
|
if (entry.status !== "downloaded") {
|
|
5112
|
-
return {
|
|
5602
|
+
return { items: null, reason: `raw provider output is ${entry.status}, not downloaded` };
|
|
5113
5603
|
}
|
|
5114
|
-
|
|
5604
|
+
const frameSet = spec.generator === "frames";
|
|
5605
|
+
if (!frameSet && entry.outputs.length !== 1 || frameSet && entry.outputs.length < 2) {
|
|
5115
5606
|
return {
|
|
5116
|
-
|
|
5117
|
-
reason: `quality profiles require one PNG output; lock entry has ${entry.outputs.length}`
|
|
5607
|
+
items: null,
|
|
5608
|
+
reason: frameSet ? `frame-set quality requires at least two PNG outputs; lock entry has ${entry.outputs.length}` : `quality profiles require one PNG output; lock entry has ${entry.outputs.length}`
|
|
5118
5609
|
};
|
|
5119
5610
|
}
|
|
5120
|
-
const
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5611
|
+
const roles = /* @__PURE__ */ new Set();
|
|
5612
|
+
const items = [];
|
|
5613
|
+
for (const [index, output] of entry.outputs.entries()) {
|
|
5614
|
+
if (output.mediaType && output.mediaType !== MediaType.PNG) {
|
|
5615
|
+
return { items: null, reason: `quality profiles require PNG input; lock entry is ${output.mediaType}` };
|
|
5616
|
+
}
|
|
5617
|
+
const role = frameSet ? output.role : "source";
|
|
5618
|
+
if (frameSet && (!role || roles.has(role))) {
|
|
5619
|
+
return { items: null, reason: "frame-set quality requires a unique role for every ordered output" };
|
|
5620
|
+
}
|
|
5621
|
+
if (role) roles.add(role);
|
|
5622
|
+
const source = currentEntryOutputPath(entry, spec, index);
|
|
5623
|
+
if (!existsSync6(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
|
|
5624
|
+
if (await sha256File(source) !== output.sha256) {
|
|
5625
|
+
return { items: null, reason: "raw provider output was modified after download" };
|
|
5626
|
+
}
|
|
5627
|
+
items.push({ role, path: source });
|
|
5128
5628
|
}
|
|
5129
|
-
return {
|
|
5629
|
+
return { items };
|
|
5130
5630
|
}
|
|
5131
5631
|
async function inspectQualityProfile(spec, lock) {
|
|
5132
5632
|
if (!spec.quality) return null;
|
|
@@ -5134,16 +5634,20 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5134
5634
|
const output = spec.quality.outFile;
|
|
5135
5635
|
const record = qualityRecordPath(output);
|
|
5136
5636
|
const source = await qualitySource(spec, lock);
|
|
5137
|
-
if (!source.
|
|
5637
|
+
if (!source.items) {
|
|
5138
5638
|
return { key, state: "blocked", reason: source.reason, source: null, output, record };
|
|
5139
5639
|
}
|
|
5640
|
+
const sourcePaths = source.items.map((item) => item.path);
|
|
5641
|
+
const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
|
|
5140
5642
|
if (!existsSync6(record)) {
|
|
5141
5643
|
return {
|
|
5142
5644
|
key,
|
|
5143
5645
|
state: "needs-refinement",
|
|
5144
5646
|
reason: "no refinement record",
|
|
5145
|
-
source:
|
|
5647
|
+
source: sourcePaths[0],
|
|
5648
|
+
sources: sourcePaths,
|
|
5146
5649
|
output,
|
|
5650
|
+
outputs: expectedOutputs,
|
|
5147
5651
|
record
|
|
5148
5652
|
};
|
|
5149
5653
|
}
|
|
@@ -5152,14 +5656,17 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5152
5656
|
const expectedPalette = normalizeRefinementPalette(spec.quality.palette);
|
|
5153
5657
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
5154
5658
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
5155
|
-
const
|
|
5659
|
+
const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
|
|
5660
|
+
const contractChanged = JSON.stringify(check.sources.map((item) => path11.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path11.resolve(item))) || JSON.stringify(check.outputs.map((item) => path11.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path11.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
|
|
5156
5661
|
if (contractChanged) {
|
|
5157
5662
|
return {
|
|
5158
5663
|
key,
|
|
5159
5664
|
state: "needs-refinement",
|
|
5160
5665
|
reason: "quality profile changed",
|
|
5161
|
-
source:
|
|
5666
|
+
source: sourcePaths[0],
|
|
5667
|
+
sources: sourcePaths,
|
|
5162
5668
|
output,
|
|
5669
|
+
outputs: expectedOutputs,
|
|
5163
5670
|
record
|
|
5164
5671
|
};
|
|
5165
5672
|
}
|
|
@@ -5168,8 +5675,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5168
5675
|
key,
|
|
5169
5676
|
state: "needs-refinement",
|
|
5170
5677
|
reason: check.reasons.filter((reason) => reason !== "human 1\xD7 review is pending").join("; ") || "refinement record is stale",
|
|
5171
|
-
source:
|
|
5678
|
+
source: sourcePaths[0],
|
|
5679
|
+
sources: sourcePaths,
|
|
5172
5680
|
output,
|
|
5681
|
+
outputs: expectedOutputs,
|
|
5173
5682
|
record
|
|
5174
5683
|
};
|
|
5175
5684
|
}
|
|
@@ -5178,8 +5687,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5178
5687
|
key,
|
|
5179
5688
|
state: "needs-approval",
|
|
5180
5689
|
reason: "automated checks passed; human 1\xD7 review is pending",
|
|
5181
|
-
source:
|
|
5690
|
+
source: sourcePaths[0],
|
|
5691
|
+
sources: sourcePaths,
|
|
5182
5692
|
output,
|
|
5693
|
+
outputs: expectedOutputs,
|
|
5183
5694
|
record
|
|
5184
5695
|
};
|
|
5185
5696
|
}
|
|
@@ -5187,8 +5698,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5187
5698
|
key,
|
|
5188
5699
|
state: "approved",
|
|
5189
5700
|
reason: "refined output and human approval are current",
|
|
5190
|
-
source:
|
|
5701
|
+
source: sourcePaths[0],
|
|
5702
|
+
sources: sourcePaths,
|
|
5191
5703
|
output,
|
|
5704
|
+
outputs: expectedOutputs,
|
|
5192
5705
|
record
|
|
5193
5706
|
};
|
|
5194
5707
|
} catch (error) {
|
|
@@ -5196,8 +5709,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5196
5709
|
key,
|
|
5197
5710
|
state: "needs-refinement",
|
|
5198
5711
|
reason: `invalid refinement record: ${error instanceof Error ? error.message : String(error)}`,
|
|
5199
|
-
source:
|
|
5712
|
+
source: sourcePaths[0],
|
|
5713
|
+
sources: sourcePaths,
|
|
5200
5714
|
output,
|
|
5715
|
+
outputs: expectedOutputs,
|
|
5201
5716
|
record
|
|
5202
5717
|
};
|
|
5203
5718
|
}
|
|
@@ -5221,8 +5736,7 @@ async function refineQualityProfiles(specs, lock, options = {}) {
|
|
|
5221
5736
|
continue;
|
|
5222
5737
|
}
|
|
5223
5738
|
try {
|
|
5224
|
-
|
|
5225
|
-
source: inspection.source,
|
|
5739
|
+
const common = {
|
|
5226
5740
|
output: inspection.output,
|
|
5227
5741
|
palette: spec.quality.palette,
|
|
5228
5742
|
minGridConfidence: spec.quality.minGridConfidence,
|
|
@@ -5234,7 +5748,18 @@ async function refineQualityProfiles(specs, lock, options = {}) {
|
|
|
5234
5748
|
fixerCommand: options.fixerCommand,
|
|
5235
5749
|
fixerArgsPrefix: options.fixerArgsPrefix,
|
|
5236
5750
|
force: options.force
|
|
5237
|
-
}
|
|
5751
|
+
};
|
|
5752
|
+
if (spec.generator === "frames") {
|
|
5753
|
+
const frameSources = await qualitySource(spec, lock);
|
|
5754
|
+
if (!frameSources.items) throw new Error(frameSources.reason);
|
|
5755
|
+
await refineFrameSet({
|
|
5756
|
+
...common,
|
|
5757
|
+
sources: frameSources.items,
|
|
5758
|
+
fps: spec.quality.fps ?? 12
|
|
5759
|
+
});
|
|
5760
|
+
} else {
|
|
5761
|
+
await refineAsset({ ...common, source: inspection.source });
|
|
5762
|
+
}
|
|
5238
5763
|
result.processed++;
|
|
5239
5764
|
result.items.push(await inspectQualityProfile(spec, lock));
|
|
5240
5765
|
} catch (error) {
|
|
@@ -5248,7 +5773,7 @@ async function refineQualityProfiles(specs, lock, options = {}) {
|
|
|
5248
5773
|
}
|
|
5249
5774
|
return result;
|
|
5250
5775
|
}
|
|
5251
|
-
async function requireApprovedQualitySources(specs, lock, assetIds) {
|
|
5776
|
+
async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles) {
|
|
5252
5777
|
const configured = specs.filter(
|
|
5253
5778
|
(spec) => spec.quality && (!assetIds || assetIds.has(spec.assetId))
|
|
5254
5779
|
);
|
|
@@ -5257,8 +5782,33 @@ async function requireApprovedQualitySources(specs, lock, assetIds) {
|
|
|
5257
5782
|
const rejected = [];
|
|
5258
5783
|
for (const spec of configured) {
|
|
5259
5784
|
const inspection = await inspectQualityProfile(spec, lock);
|
|
5260
|
-
if (inspection.state === "approved")
|
|
5261
|
-
|
|
5785
|
+
if (inspection.state === "approved") {
|
|
5786
|
+
if (spec.generator === "frames") {
|
|
5787
|
+
const check = await checkQualityRecord(inspection.record);
|
|
5788
|
+
const roles = check.options.frameSet.roles;
|
|
5789
|
+
if (outputRoles) {
|
|
5790
|
+
const selectedRole = outputRoles[spec.assetId];
|
|
5791
|
+
if (!selectedRole) {
|
|
5792
|
+
throw new Error(
|
|
5793
|
+
`Mounting quality-approved frame set ${inspection.key} requires asset.outputRole.`
|
|
5794
|
+
);
|
|
5795
|
+
}
|
|
5796
|
+
const index = roles.indexOf(selectedRole);
|
|
5797
|
+
if (index < 0) {
|
|
5798
|
+
throw new Error(
|
|
5799
|
+
`Frame set ${inspection.key} has no approved output role "${selectedRole}".`
|
|
5800
|
+
);
|
|
5801
|
+
}
|
|
5802
|
+
sources[spec.assetId] = check.outputs[index];
|
|
5803
|
+
} else {
|
|
5804
|
+
for (const [index, output] of check.outputs.entries()) {
|
|
5805
|
+
sources[`${spec.assetId}/${roles[index]}`] = output;
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
} else {
|
|
5809
|
+
sources[spec.assetId] = inspection.output;
|
|
5810
|
+
}
|
|
5811
|
+
} else rejected.push(inspection);
|
|
5262
5812
|
}
|
|
5263
5813
|
if (rejected.length) {
|
|
5264
5814
|
const detail = rejected.slice(0, 5).map((item) => `${item.key}: ${item.state} (${item.reason})`).join("; ");
|
|
@@ -5611,7 +6161,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
5611
6161
|
jobId,
|
|
5612
6162
|
// A multi-candidate generator routes through review; record the parent
|
|
5613
6163
|
// so `pick` knows where to look.
|
|
5614
|
-
reviewObjectId: estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
6164
|
+
reviewObjectId: spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
5615
6165
|
status: "processing",
|
|
5616
6166
|
providerMetadata: metadata ? { ...lock.entries[key].providerMetadata, [provider.id]: metadata } : lock.entries[key].providerMetadata
|
|
5617
6167
|
});
|
|
@@ -5659,7 +6209,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
5659
6209
|
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
5660
6210
|
spec: currentSpec
|
|
5661
6211
|
});
|
|
5662
|
-
if (state.status === "review") {
|
|
6212
|
+
if (state.status === "review" || state.status === "review-set") {
|
|
5663
6213
|
upsert(lock, key, {
|
|
5664
6214
|
status: "review",
|
|
5665
6215
|
reviewObjectId: entry.jobId,
|
|
@@ -5672,7 +6222,9 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
5672
6222
|
} : entry.providerMetadata
|
|
5673
6223
|
});
|
|
5674
6224
|
result.review++;
|
|
5675
|
-
log2(
|
|
6225
|
+
log2(
|
|
6226
|
+
state.status === "review-set" ? ` review ${key} (${state.frameUrls.length} ordered frames)` : ` review ${key} (${state.candidateUrls.length} candidates)`
|
|
6227
|
+
);
|
|
5676
6228
|
} else if (state.status === "ready") {
|
|
5677
6229
|
const sourceUrls = state.sources?.length ? state.sources : state.sourceUrl ? [{ url: state.sourceUrl }] : [];
|
|
5678
6230
|
upsert(lock, key, {
|
|
@@ -6431,12 +6983,17 @@ function renderSheet(groups) {
|
|
|
6431
6983
|
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%);
|
|
6432
6984
|
background-size:12px 12px; background-position:0 0,6px 6px; }
|
|
6433
6985
|
.frames { display:flex; flex-wrap:wrap; gap:10px; max-width:100%; overflow-x:auto; }
|
|
6986
|
+
.loop { display:flex; align-items:flex-start; gap:12px; margin-bottom:12px; }
|
|
6987
|
+
.loop img { image-rendering:pixelated; border:2px solid var(--accent); padding:7px;
|
|
6988
|
+
background:var(--panel-deep); max-width:min(var(--preview),calc(100vw - 62px)); height:auto; }
|
|
6989
|
+
.loop-copy { color:var(--dim); font-size:11px; max-width:32ch; }
|
|
6434
6990
|
.cand { border:2px solid var(--line-strong); border-radius:0; padding:7px; background:var(--panel-deep);
|
|
6435
6991
|
cursor:pointer; display:flex; flex:0 0 auto; flex-direction:column; align-items:center; gap:5px;
|
|
6436
6992
|
position:relative; max-width:100%; }
|
|
6437
6993
|
.cand:hover { border-color:var(--dim); }
|
|
6438
6994
|
.cand.active { border-color:var(--dim); box-shadow:0 0 0 2px color-mix(in srgb, var(--dim) 20%, transparent); }
|
|
6439
6995
|
.cand.sel { border-color:var(--ok); box-shadow:0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }
|
|
6996
|
+
.cand.set-frame { cursor:pointer; }
|
|
6440
6997
|
.cand img { image-rendering:pixelated; display:block;
|
|
6441
6998
|
background-image:
|
|
6442
6999
|
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
|
|
@@ -6474,7 +7031,8 @@ function renderSheet(groups) {
|
|
|
6474
7031
|
</header>
|
|
6475
7032
|
<main id="root"></main>
|
|
6476
7033
|
<footer>
|
|
6477
|
-
Click a candidate to choose it
|
|
7034
|
+
Click a candidate to choose it, or any ordered frame to accept its whole set;
|
|
7035
|
+
click again to undo. In the focused row,
|
|
6478
7036
|
<kbd>\u2190</kbd>/<kbd>\u2192</kbd> browses every candidate and <kbd>Enter</kbd> chooses;
|
|
6479
7037
|
<kbd>1</kbd>\u2013<kbd>9</kbd> picks directly, <kbd>0</kbd> skips, and
|
|
6480
7038
|
<kbd>\u2191</kbd>/<kbd>\u2193</kbd> changes rows. Unchosen rows stay in review \u2014 nothing is
|
|
@@ -6522,12 +7080,34 @@ GROUPS.forEach((g, gi) => {
|
|
|
6522
7080
|
context.append(label, source);
|
|
6523
7081
|
review.insertBefore(context, frames);
|
|
6524
7082
|
}
|
|
7083
|
+
if (g.mode === 'frame-set') {
|
|
7084
|
+
const loop = document.createElement('div');
|
|
7085
|
+
loop.className = 'loop';
|
|
7086
|
+
const preview = document.createElement('img');
|
|
7087
|
+
preview.src = g.frameUrls[0];
|
|
7088
|
+
preview.width = displayWidth;
|
|
7089
|
+
preview.height = displayHeight;
|
|
7090
|
+
preview.alt = 'Animated frame-set preview';
|
|
7091
|
+
const copy = document.createElement('div');
|
|
7092
|
+
copy.className = 'loop-copy';
|
|
7093
|
+
copy.textContent = g.frameUrls.length + ' ordered frames \xB7 ' + (g.fps || 12) +
|
|
7094
|
+
' fps. Accepting keeps every frame; one bad frame means leave the set unchosen.';
|
|
7095
|
+
loop.append(preview, copy);
|
|
7096
|
+
review.insertBefore(loop, frames);
|
|
7097
|
+
let loopIndex = 0;
|
|
7098
|
+
setInterval(() => {
|
|
7099
|
+
loopIndex = (loopIndex + 1) % g.frameUrls.length;
|
|
7100
|
+
preview.src = g.frameUrls[loopIndex];
|
|
7101
|
+
}, Math.max(16, Math.round(1000 / (g.fps || 12))));
|
|
7102
|
+
}
|
|
6525
7103
|
g.frameUrls.forEach((url, i) => {
|
|
6526
7104
|
const c = document.createElement('button');
|
|
6527
7105
|
c.type = 'button';
|
|
6528
|
-
c.className = 'cand';
|
|
7106
|
+
c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '');
|
|
6529
7107
|
c.tabIndex = -1;
|
|
6530
|
-
c.setAttribute('aria-label',
|
|
7108
|
+
c.setAttribute('aria-label', g.mode === 'frame-set'
|
|
7109
|
+
? 'Accept ordered frame set from frame ' + (i + 1)
|
|
7110
|
+
: 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length);
|
|
6531
7111
|
|
|
6532
7112
|
const preview = document.createElement('img');
|
|
6533
7113
|
preview.className = 'preview';
|
|
@@ -6537,7 +7117,8 @@ GROUPS.forEach((g, gi) => {
|
|
|
6537
7117
|
preview.loading = 'lazy';
|
|
6538
7118
|
const index = document.createElement('span');
|
|
6539
7119
|
index.className = 'idx';
|
|
6540
|
-
index.textContent = String(i + 1) + ' \xB7 ' +
|
|
7120
|
+
index.textContent = (g.frameLabels?.[i] || String(i + 1)) + ' \xB7 ' +
|
|
7121
|
+
g.width + '\xD7' + g.height + ' \xB7 ' + scaleLabel;
|
|
6541
7122
|
c.append(preview, index);
|
|
6542
7123
|
if (displayScale > 1 && largestSide <= 96) {
|
|
6543
7124
|
const actual = document.createElement('img');
|
|
@@ -6578,6 +7159,14 @@ function activate(i, frames) {
|
|
|
6578
7159
|
}
|
|
6579
7160
|
|
|
6580
7161
|
function choose(gi, i, node, frames, groupEl) {
|
|
7162
|
+
if (GROUPS[gi].mode === 'frame-set') {
|
|
7163
|
+
const selected = picks.has(gi);
|
|
7164
|
+
frames.querySelectorAll('.cand').forEach(n => n.classList.toggle('sel', !selected));
|
|
7165
|
+
if (selected) { picks.delete(gi); groupEl.classList.remove('done'); }
|
|
7166
|
+
else { picks.set(gi, 0); groupEl.classList.add('done'); }
|
|
7167
|
+
refresh();
|
|
7168
|
+
return;
|
|
7169
|
+
}
|
|
6581
7170
|
const current = picks.get(gi);
|
|
6582
7171
|
frames.querySelectorAll('.cand').forEach(n => n.classList.remove('sel'));
|
|
6583
7172
|
if (current === i) { picks.delete(gi); groupEl.classList.remove('done'); }
|
|
@@ -6754,7 +7343,8 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
6754
7343
|
try {
|
|
6755
7344
|
const spec = specByKey.get(key);
|
|
6756
7345
|
const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec });
|
|
6757
|
-
if (state.status !== "review" || !state.candidateUrls.length) continue;
|
|
7346
|
+
if (state.status !== "review" && state.status !== "review-set" || (state.status === "review" ? !state.candidateUrls.length : !state.frameUrls.length)) continue;
|
|
7347
|
+
const frameUrls = state.status === "review" ? state.candidateUrls : state.frameUrls;
|
|
6758
7348
|
const sourceRoute = spec?.revision ? `/revision-source/${encodeURIComponent(String(groups.length))}` : null;
|
|
6759
7349
|
if (sourceRoute && spec?.revision) {
|
|
6760
7350
|
reviewAssets.set(sourceRoute, {
|
|
@@ -6768,9 +7358,14 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
6768
7358
|
styleId: entry.styleId,
|
|
6769
7359
|
prompt: entry.prompt,
|
|
6770
7360
|
reviewObjectId: entry.reviewObjectId,
|
|
6771
|
-
frameUrls
|
|
7361
|
+
frameUrls,
|
|
6772
7362
|
width: entry.width,
|
|
6773
7363
|
height: entry.height,
|
|
7364
|
+
...state.status === "review-set" ? {
|
|
7365
|
+
mode: "frame-set",
|
|
7366
|
+
frameLabels: state.sources.map((source, index) => source.role ?? `frame-${index}`),
|
|
7367
|
+
fps: state.fps
|
|
7368
|
+
} : { mode: "candidates" },
|
|
6774
7369
|
...sourceRoute && spec?.revision && spec.revision.sourceWidth && spec.revision.sourceHeight ? {
|
|
6775
7370
|
revision: {
|
|
6776
7371
|
mode: spec.revision.mode,
|
|
@@ -6813,6 +7408,31 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
6813
7408
|
const entry = lock.entries[key];
|
|
6814
7409
|
if (!group || !entry?.reviewObjectId) continue;
|
|
6815
7410
|
if (!Number.isInteger(index) || index < 0 || index >= group.frameUrls.length) continue;
|
|
7411
|
+
if (group.mode === "frame-set") {
|
|
7412
|
+
const spec = specByKey.get(key);
|
|
7413
|
+
const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec });
|
|
7414
|
+
if (state.status !== "review-set") {
|
|
7415
|
+
throw new Error(`Frame set ${key} is no longer ready for review`);
|
|
7416
|
+
}
|
|
7417
|
+
upsert(lock, key, {
|
|
7418
|
+
status: "selected",
|
|
7419
|
+
objectId: state.objectId,
|
|
7420
|
+
candidateIndex: null,
|
|
7421
|
+
sourceUrl: state.sources[0]?.url ?? null,
|
|
7422
|
+
sourceUrls: state.sources,
|
|
7423
|
+
provider: provider.id,
|
|
7424
|
+
providerMetadata: state.metadata ? {
|
|
7425
|
+
...entry.providerMetadata,
|
|
7426
|
+
[provider.id]: {
|
|
7427
|
+
...entry.providerMetadata[provider.id],
|
|
7428
|
+
...state.metadata
|
|
7429
|
+
}
|
|
7430
|
+
} : entry.providerMetadata
|
|
7431
|
+
});
|
|
7432
|
+
selected++;
|
|
7433
|
+
log2(` accepted ${key} \u2192 ${state.sources.length} ordered frames`);
|
|
7434
|
+
continue;
|
|
7435
|
+
}
|
|
6816
7436
|
const { objectId, sourceUrl, metadata } = await requireSelectCandidate(provider)(
|
|
6817
7437
|
entry.reviewObjectId,
|
|
6818
7438
|
index,
|
|
@@ -9522,9 +10142,15 @@ async function main() {
|
|
|
9522
10142
|
if (!args.reviewer?.trim()) {
|
|
9523
10143
|
throw new Error("refine approve needs --reviewer <name>.");
|
|
9524
10144
|
}
|
|
9525
|
-
|
|
10145
|
+
const pending = await checkQualityRecord(args.from);
|
|
10146
|
+
log(
|
|
10147
|
+
pending.options.frameSet ? ` Inspect all ${pending.options.frameSet.count} ordered frames at 1\xD7, integer zoom, and playback speed.` : ` Inspect the final PNG at 1\xD7 and integer zoom.`
|
|
10148
|
+
);
|
|
9526
10149
|
log(` Confirm crisp edges, readable forms, palette separation, alpha, and seams.`);
|
|
9527
|
-
if (!await confirm(
|
|
10150
|
+
if (!await confirm(
|
|
10151
|
+
pending.options.frameSet ? " Record this complete frame set as human-approved?" : " Record this asset as human-approved?",
|
|
10152
|
+
args.yes
|
|
10153
|
+
)) {
|
|
9528
10154
|
log(" approval not recorded");
|
|
9529
10155
|
return;
|
|
9530
10156
|
}
|
|
@@ -9533,7 +10159,11 @@ async function main() {
|
|
|
9533
10159
|
note: args.note
|
|
9534
10160
|
});
|
|
9535
10161
|
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
9536
|
-
else
|
|
10162
|
+
else {
|
|
10163
|
+
log(
|
|
10164
|
+
result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${path24.relative(process.cwd(), result2.record)} by ${args.reviewer.trim()}` : ` approved ${path24.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`
|
|
10165
|
+
);
|
|
10166
|
+
}
|
|
9537
10167
|
return;
|
|
9538
10168
|
}
|
|
9539
10169
|
const result = await checkQualityRecord(args.from);
|
|
@@ -10046,7 +10676,10 @@ async function main() {
|
|
|
10046
10676
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
10047
10677
|
];
|
|
10048
10678
|
const qualityRecords = qualitySources ? await Promise.all(
|
|
10049
|
-
|
|
10679
|
+
packagingSpecs.filter((spec) => spec.quality).map((spec) => provenanceFile(
|
|
10680
|
+
`$quality/${spec.assetId}`,
|
|
10681
|
+
qualityRecordPath(spec.quality.outFile)
|
|
10682
|
+
))
|
|
10050
10683
|
) : [];
|
|
10051
10684
|
await writeManagedArtifactBundle(`${base}.pixelkiln.json`, outputs, {
|
|
10052
10685
|
kind: "pack",
|
|
@@ -10098,7 +10731,8 @@ async function main() {
|
|
|
10098
10731
|
const qualitySources = await requireApprovedQualitySources(
|
|
10099
10732
|
packagingSpecs,
|
|
10100
10733
|
lock,
|
|
10101
|
-
new Set(Object.keys(cells))
|
|
10734
|
+
new Set(Object.keys(cells)),
|
|
10735
|
+
outputRoles
|
|
10102
10736
|
);
|
|
10103
10737
|
if (qualitySources) Object.assign(sources, qualitySources);
|
|
10104
10738
|
const { png, atlas, skipped, overBase, sources: artifactSources } = mountStyle(
|
|
@@ -10118,7 +10752,10 @@ async function main() {
|
|
|
10118
10752
|
{ path: metadata, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
10119
10753
|
];
|
|
10120
10754
|
const qualityRecords = qualitySources ? await Promise.all(
|
|
10121
|
-
Object.
|
|
10755
|
+
packagingSpecs.filter((spec) => spec.quality && Object.hasOwn(cells, spec.assetId)).map((spec) => provenanceFile(
|
|
10756
|
+
`$quality/${spec.assetId}`,
|
|
10757
|
+
qualityRecordPath(spec.quality.outFile)
|
|
10758
|
+
))
|
|
10122
10759
|
) : [];
|
|
10123
10760
|
await writeManagedArtifactBundle(companion, outputs, {
|
|
10124
10761
|
kind: "mount",
|