pixelkiln 0.18.0 → 0.19.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 +6 -6
- package/dist/cli.js +1125 -575
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1115 -574
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +195 -69
- package/dist/index.d.ts +195 -69
- package/dist/index.js +1112 -573
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +1 -1
- package/docs/ARCHITECTURE.md +15 -10
- 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 +2 -1
- package/docs/LIBRARY.md +13 -2
- package/docs/MANIFEST.md +24 -10
- package/docs/QUALITY.md +11 -3
- package/docs/README.md +2 -2
- package/package.json +1 -1
- package/schema/manifest.schema.json +25 -1
- package/schema/recipe.schema.json +7 -1
- package/skills/pixelkiln/SKILL.md +2 -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) {
|
|
@@ -1663,7 +1848,9 @@ 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()
|
|
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()
|
|
1667
1854
|
}).strict().refine(
|
|
1668
1855
|
(profile) => new Set(profile.palette.map((color) => color.replace(/^#/, "").toLowerCase())).size === profile.palette.length,
|
|
1669
1856
|
{ message: "quality palette colors must be unique", path: ["palette"] }
|
|
@@ -1882,10 +2069,15 @@ var AssetSchema = z2.object({
|
|
|
1882
2069
|
promptByStyle: z2.record(z2.string()).default({}),
|
|
1883
2070
|
/**
|
|
1884
2071
|
* Named per-asset values consumed by the active provider's declared
|
|
1885
|
-
* bindings. Values are JSON scalars
|
|
1886
|
-
*
|
|
2072
|
+
* bindings. Values are JSON scalars, or a frame-set sequence of scalars;
|
|
2073
|
+
* adapters may interpret a string as a manifest-relative uploaded file.
|
|
1887
2074
|
*/
|
|
1888
|
-
providerInputs: z2.record(z2.union([
|
|
2075
|
+
providerInputs: z2.record(z2.union([
|
|
2076
|
+
z2.string(),
|
|
2077
|
+
z2.number().finite(),
|
|
2078
|
+
z2.boolean(),
|
|
2079
|
+
z2.array(z2.union([z2.string(), z2.number().finite(), z2.boolean()])).min(2).max(64)
|
|
2080
|
+
])).default({})
|
|
1889
2081
|
}).strict().superRefine((asset, context) => {
|
|
1890
2082
|
if (asset.source && asset.revision) {
|
|
1891
2083
|
context.addIssue({
|
|
@@ -3195,138 +3387,415 @@ registerProvider({
|
|
|
3195
3387
|
});
|
|
3196
3388
|
|
|
3197
3389
|
// src/manifest.ts
|
|
3198
|
-
import { readFile as
|
|
3390
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
3391
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3392
|
+
import path6 from "path";
|
|
3393
|
+
|
|
3394
|
+
// src/outputs.ts
|
|
3395
|
+
import path5 from "path";
|
|
3396
|
+
|
|
3397
|
+
// src/lock.ts
|
|
3398
|
+
import { readFile as readFile2, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
|
|
3199
3399
|
import { existsSync as existsSync3 } from "fs";
|
|
3200
3400
|
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
|
-
}
|
|
3401
|
+
async function loadLock(lockPath) {
|
|
3402
|
+
if (!existsSync3(lockPath)) return { version: 2, entries: {} };
|
|
3403
|
+
try {
|
|
3404
|
+
return parseLock(JSON.parse(await readFile2(lockPath, "utf8")));
|
|
3405
|
+
} catch (err) {
|
|
3406
|
+
throw new Error(
|
|
3407
|
+
`Lockfile at ${lockPath} is malformed:
|
|
3408
|
+
${err instanceof Error ? err.message : String(err)}`
|
|
3409
|
+
);
|
|
3230
3410
|
}
|
|
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);
|
|
3411
|
+
}
|
|
3412
|
+
var saveQueues = /* @__PURE__ */ new Map();
|
|
3413
|
+
var dirtyPatches = /* @__PURE__ */ new WeakMap();
|
|
3414
|
+
var dirtyDeletes = /* @__PURE__ */ new WeakMap();
|
|
3415
|
+
function saveLock(lockPath, lock) {
|
|
3416
|
+
const queueKey = path4.resolve(lockPath);
|
|
3417
|
+
const previous = saveQueues.get(queueKey) ?? Promise.resolve();
|
|
3418
|
+
const next = previous.catch(() => {
|
|
3419
|
+
}).then(() => writeLockNow(lockPath, lock));
|
|
3420
|
+
saveQueues.set(queueKey, next);
|
|
3421
|
+
const cleanup = () => {
|
|
3422
|
+
if (saveQueues.get(queueKey) === next) saveQueues.delete(queueKey);
|
|
3249
3423
|
};
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3424
|
+
void next.then(cleanup, cleanup);
|
|
3425
|
+
return next;
|
|
3426
|
+
}
|
|
3427
|
+
async function writeLockNow(lockPath, lock) {
|
|
3428
|
+
await mkdir(path4.dirname(path4.resolve(lockPath)), { recursive: true });
|
|
3429
|
+
const release = await acquireFileLock(lockPath);
|
|
3430
|
+
try {
|
|
3431
|
+
await writeLockWhileHeld(lockPath, lock);
|
|
3432
|
+
} finally {
|
|
3433
|
+
await release();
|
|
3254
3434
|
}
|
|
3255
|
-
return { manifest: parsed.data, root: path4.dirname(abs), path: abs };
|
|
3256
3435
|
}
|
|
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]) {
|
|
3436
|
+
async function writeLockWhileHeld(file, lock) {
|
|
3437
|
+
let disk = { version: 2, entries: {} };
|
|
3438
|
+
if (existsSync3(file)) {
|
|
3439
|
+
try {
|
|
3440
|
+
disk = parseLock(JSON.parse(await readFile2(file, "utf8")));
|
|
3441
|
+
} catch (err) {
|
|
3276
3442
|
throw new Error(
|
|
3277
|
-
`
|
|
3443
|
+
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
3278
3444
|
);
|
|
3279
3445
|
}
|
|
3280
3446
|
}
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3447
|
+
const dirty = dirtyPatches.get(lock) ?? /* @__PURE__ */ new Map();
|
|
3448
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
3449
|
+
for (const [key, patch] of dirty) snapshot.set(key, { ...patch });
|
|
3450
|
+
const merged = { ...disk.entries };
|
|
3451
|
+
for (const [key, patch] of snapshot) {
|
|
3452
|
+
const local = lock.entries[key];
|
|
3453
|
+
if (!local) continue;
|
|
3454
|
+
merged[key] = merged[key] ? { ...merged[key], ...patch } : local;
|
|
3285
3455
|
}
|
|
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
|
-
}
|
|
3456
|
+
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
3457
|
+
if (!merged[key]) merged[key] = entry;
|
|
3294
3458
|
}
|
|
3295
|
-
const
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3459
|
+
const deletes = dirtyDeletes.get(lock);
|
|
3460
|
+
const deleteSnapshot = deletes ? new Set(deletes) : /* @__PURE__ */ new Set();
|
|
3461
|
+
for (const key of deleteSnapshot) delete merged[key];
|
|
3462
|
+
const sorted = {};
|
|
3463
|
+
for (const key of Object.keys(merged).sort()) {
|
|
3464
|
+
sorted[key] = merged[key];
|
|
3465
|
+
}
|
|
3466
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3467
|
+
try {
|
|
3468
|
+
await writeFile(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
|
|
3469
|
+
await rename(tmp, file);
|
|
3470
|
+
} finally {
|
|
3471
|
+
await rm(tmp, { force: true });
|
|
3472
|
+
}
|
|
3473
|
+
const currentDirty = dirtyPatches.get(lock);
|
|
3474
|
+
if (currentDirty) {
|
|
3475
|
+
for (const [key, patch] of snapshot) {
|
|
3476
|
+
const current = currentDirty.get(key);
|
|
3477
|
+
if (!current) continue;
|
|
3478
|
+
for (const [field, value] of Object.entries(patch)) {
|
|
3479
|
+
if (Object.is(current[field], value)) {
|
|
3480
|
+
delete current[field];
|
|
3481
|
+
}
|
|
3306
3482
|
}
|
|
3307
|
-
|
|
3308
|
-
styleImageCache.set(abs, hit);
|
|
3483
|
+
if (Object.keys(current).length === 0) currentDirty.delete(key);
|
|
3309
3484
|
}
|
|
3310
|
-
return hit;
|
|
3311
3485
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
const
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3486
|
+
const stillDirty = dirtyDeletes.get(lock);
|
|
3487
|
+
if (stillDirty) {
|
|
3488
|
+
for (const key of deleteSnapshot) stillDirty.delete(key);
|
|
3489
|
+
if (stillDirty.size === 0) dirtyDeletes.delete(lock);
|
|
3490
|
+
}
|
|
3491
|
+
const localAfterWrite = lock.entries;
|
|
3492
|
+
lock.entries = { ...merged };
|
|
3493
|
+
for (const [key, patch] of currentDirty ?? []) {
|
|
3494
|
+
const base = lock.entries[key] ?? localAfterWrite[key];
|
|
3495
|
+
if (base) lock.entries[key] = { ...base, ...patch };
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
function upsert(lock, key, patch) {
|
|
3499
|
+
dirtyDeletes.get(lock)?.delete(key);
|
|
3500
|
+
const existing = lock.entries[key];
|
|
3501
|
+
const next = { ...existing, ...patch };
|
|
3502
|
+
lock.entries[key] = next;
|
|
3503
|
+
let dirty = dirtyPatches.get(lock);
|
|
3504
|
+
if (!dirty) {
|
|
3505
|
+
dirty = /* @__PURE__ */ new Map();
|
|
3506
|
+
dirtyPatches.set(lock, dirty);
|
|
3507
|
+
}
|
|
3508
|
+
dirty.set(key, { ...dirty.get(key) ?? {}, ...patch });
|
|
3509
|
+
return next;
|
|
3510
|
+
}
|
|
3511
|
+
function remove(lock, key) {
|
|
3512
|
+
if (!lock.entries[key]) return false;
|
|
3513
|
+
delete lock.entries[key];
|
|
3514
|
+
const patches = dirtyPatches.get(lock);
|
|
3515
|
+
if (patches) {
|
|
3516
|
+
patches.delete(key);
|
|
3517
|
+
if (patches.size === 0) dirtyPatches.delete(lock);
|
|
3518
|
+
}
|
|
3519
|
+
let deletes = dirtyDeletes.get(lock);
|
|
3520
|
+
if (!deletes) {
|
|
3521
|
+
deletes = /* @__PURE__ */ new Set();
|
|
3522
|
+
dirtyDeletes.set(lock, deletes);
|
|
3523
|
+
}
|
|
3524
|
+
deletes.add(key);
|
|
3525
|
+
return true;
|
|
3526
|
+
}
|
|
3527
|
+
var FILE_LOCK_TIMEOUT_MS = 1e4;
|
|
3528
|
+
var STALE_FILE_LOCK_MS = 2 * 60 * 1e3;
|
|
3529
|
+
async function acquireFileLock(file) {
|
|
3530
|
+
const lockDir = `${file}.lock`;
|
|
3531
|
+
const deadline = Date.now() + FILE_LOCK_TIMEOUT_MS;
|
|
3532
|
+
for (; ; ) {
|
|
3533
|
+
try {
|
|
3534
|
+
await mkdir(lockDir);
|
|
3535
|
+
return async () => {
|
|
3536
|
+
await rmdir(lockDir).catch(() => {
|
|
3537
|
+
});
|
|
3538
|
+
};
|
|
3539
|
+
} catch (err) {
|
|
3540
|
+
const code = err.code;
|
|
3541
|
+
if (code !== "EEXIST") throw err;
|
|
3542
|
+
try {
|
|
3543
|
+
const age = Date.now() - (await stat(lockDir)).mtimeMs;
|
|
3544
|
+
if (age > STALE_FILE_LOCK_MS) {
|
|
3545
|
+
await rmdir(lockDir);
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
} catch (lockErr) {
|
|
3549
|
+
if (lockErr.code === "ENOENT") continue;
|
|
3550
|
+
}
|
|
3551
|
+
if (Date.now() >= deadline) {
|
|
3552
|
+
throw new Error(`Timed out waiting for lockfile writer ${lockDir}`);
|
|
3553
|
+
}
|
|
3554
|
+
await new Promise((resolve) => setTimeout(resolve, 25 + Math.floor(Math.random() * 25)));
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
function spendByUnit(lock) {
|
|
3559
|
+
const totals = { generations: 0, usd: 0, free: 0 };
|
|
3560
|
+
for (const entry of Object.values(lock.entries)) {
|
|
3561
|
+
const unit = entry.costUnit ?? "generations";
|
|
3562
|
+
totals[unit] = (totals[unit] ?? 0) + (entry.cost ?? 0);
|
|
3563
|
+
}
|
|
3564
|
+
return totals;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
// src/outputs.ts
|
|
3568
|
+
function portableOutputPath(file, manifestDir) {
|
|
3569
|
+
const root = path5.resolve(manifestDir);
|
|
3570
|
+
const absolute = path5.isAbsolute(file) ? path5.normalize(file) : path5.resolve(root, file);
|
|
3571
|
+
const relative = path5.relative(root, absolute);
|
|
3572
|
+
const value = path5.isAbsolute(relative) ? absolute : relative;
|
|
3573
|
+
return value.split(path5.sep).join("/");
|
|
3574
|
+
}
|
|
3575
|
+
function resolveOutputPath(recordedPath, manifestDir) {
|
|
3576
|
+
if (path5.isAbsolute(recordedPath)) return path5.normalize(recordedPath);
|
|
3577
|
+
if (path5.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
3578
|
+
return path5.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path5.sep));
|
|
3579
|
+
}
|
|
3580
|
+
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
3581
|
+
const originalExt = path5.extname(spec.outFile);
|
|
3582
|
+
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
3583
|
+
const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
|
|
3584
|
+
if (total === 1) return `${stem}${ext}`;
|
|
3585
|
+
const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
3586
|
+
return `${stem}-${safeRole}${ext}`;
|
|
3587
|
+
}
|
|
3588
|
+
function currentOutputPath(output, spec, index, total) {
|
|
3589
|
+
return expectedOutputPath(spec, output.role, index, total, output.mediaType);
|
|
3590
|
+
}
|
|
3591
|
+
function currentEntryOutputPath(entry, spec, index) {
|
|
3592
|
+
const output = entry.outputs[index];
|
|
3593
|
+
const sourceIndex = output.role ? (entry.sourceUrls ?? []).findIndex((source) => source.role === output.role) : -1;
|
|
3594
|
+
const position = sourceIndex >= 0 ? sourceIndex : index;
|
|
3595
|
+
const total = Math.max(entry.outputs.length, entry.sourceUrls?.length ?? 0);
|
|
3596
|
+
return currentOutputPath(output, spec, position, total);
|
|
3597
|
+
}
|
|
3598
|
+
function normalizeLockOutputPaths(lock, specs) {
|
|
3599
|
+
const byKey = new Map(specs.map((spec) => [lockKey(spec.styleId, spec.assetId), spec]));
|
|
3600
|
+
let changed = 0;
|
|
3601
|
+
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
3602
|
+
const spec = byKey.get(key);
|
|
3603
|
+
if (!spec) continue;
|
|
3604
|
+
let entryChanged = false;
|
|
3605
|
+
const outputs = entry.outputs.map((output, index) => {
|
|
3606
|
+
const absolute = currentEntryOutputPath(entry, spec, index);
|
|
3607
|
+
const portable = portableOutputPath(absolute, spec.root);
|
|
3608
|
+
if (portable === output.path) return output;
|
|
3609
|
+
changed++;
|
|
3610
|
+
entryChanged = true;
|
|
3611
|
+
return { ...output, path: portable };
|
|
3612
|
+
});
|
|
3613
|
+
if (entryChanged) upsert(lock, key, { outputs });
|
|
3614
|
+
}
|
|
3615
|
+
return changed;
|
|
3616
|
+
}
|
|
3617
|
+
function fallbackOutputRole(index) {
|
|
3618
|
+
return `output-${String(index).padStart(2, "0")}`;
|
|
3619
|
+
}
|
|
3620
|
+
function outputId(assetId, output, index, total) {
|
|
3621
|
+
if (total === 1) return assetId;
|
|
3622
|
+
return `${assetId}/${output.role ?? fallbackOutputRole(index)}`;
|
|
3623
|
+
}
|
|
3624
|
+
function resolveEntryOutputs(entry, assetId, manifestDir) {
|
|
3625
|
+
return entry.outputs.map((output, index) => ({
|
|
3626
|
+
...output,
|
|
3627
|
+
assetId,
|
|
3628
|
+
id: outputId(assetId, output, index, entry.outputs.length),
|
|
3629
|
+
index,
|
|
3630
|
+
absolutePath: resolveOutputPath(output.path, manifestDir)
|
|
3631
|
+
}));
|
|
3632
|
+
}
|
|
3633
|
+
function resolveSpecEntryOutputs(entry, spec) {
|
|
3634
|
+
return entry.outputs.map((output, index) => ({
|
|
3635
|
+
...output,
|
|
3636
|
+
assetId: spec.assetId,
|
|
3637
|
+
id: outputId(spec.assetId, output, index, entry.outputs.length),
|
|
3638
|
+
index,
|
|
3639
|
+
absolutePath: currentEntryOutputPath(entry, spec, index)
|
|
3640
|
+
}));
|
|
3641
|
+
}
|
|
3642
|
+
function selectEntryOutput(entry, role) {
|
|
3643
|
+
if (role) {
|
|
3644
|
+
const output = entry.outputs.find((candidate) => candidate.role === role);
|
|
3645
|
+
return output ? { ok: true, output } : { ok: false, reason: `output role "${role}" is not recorded` };
|
|
3646
|
+
}
|
|
3647
|
+
if (entry.outputs.length === 0) return { ok: false, reason: "no output recorded in the lockfile" };
|
|
3648
|
+
if (entry.outputs.length === 1) return { ok: true, output: entry.outputs[0] };
|
|
3649
|
+
const primary = entry.outputs.filter((output) => !output.role);
|
|
3650
|
+
if (primary.length === 1) return { ok: true, output: primary[0] };
|
|
3651
|
+
return {
|
|
3652
|
+
ok: false,
|
|
3653
|
+
reason: `${entry.outputs.length} outputs are recorded; set \`outputRole\` to choose one`
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
function resolveSpecOutputs(spec, lock, manifestDir) {
|
|
3657
|
+
const entry = lock?.entries[lockKey(spec.styleId, spec.assetId)];
|
|
3658
|
+
if (entry?.outputs.length) return resolveSpecEntryOutputs(entry, spec);
|
|
3659
|
+
return [{
|
|
3660
|
+
assetId: spec.assetId,
|
|
3661
|
+
id: spec.assetId,
|
|
3662
|
+
index: 0,
|
|
3663
|
+
path: spec.outFile,
|
|
3664
|
+
absolutePath: path5.resolve(spec.outFile),
|
|
3665
|
+
sha256: ""
|
|
3666
|
+
}];
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// src/manifest.ts
|
|
3670
|
+
async function loadManifest(manifestPath) {
|
|
3671
|
+
const abs = path6.resolve(manifestPath);
|
|
3672
|
+
if (!existsSync4(abs)) throw new Error(`No manifest at ${abs}`);
|
|
3673
|
+
const parsed = ManifestSchema.safeParse(JSON.parse(await readFile3(abs, "utf8")));
|
|
3674
|
+
if (!parsed.success) {
|
|
3675
|
+
const issues = parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
3676
|
+
throw new Error(`Manifest at ${abs} is invalid:
|
|
3677
|
+
${issues}`);
|
|
3678
|
+
}
|
|
3679
|
+
const styleIds = new Set(Object.keys(parsed.data.styles));
|
|
3680
|
+
const unknownReferences = [];
|
|
3681
|
+
for (const [assetId, asset] of Object.entries(parsed.data.assets)) {
|
|
3682
|
+
for (const styleId of asset.styles) {
|
|
3683
|
+
if (!styleIds.has(styleId)) unknownReferences.push(`assets.${assetId}.styles: unknown style "${styleId}"`);
|
|
3684
|
+
}
|
|
3685
|
+
for (const styleId of Object.keys(asset.promptByStyle)) {
|
|
3686
|
+
if (!styleIds.has(styleId)) {
|
|
3687
|
+
unknownReferences.push(`assets.${assetId}.promptByStyle: unknown style "${styleId}"`);
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
if (asset.revision) {
|
|
3691
|
+
if (!parsed.data.assets[asset.revision.from]) {
|
|
3692
|
+
unknownReferences.push(
|
|
3693
|
+
`assets.${assetId}.revision.from: unknown asset "${asset.revision.from}"`
|
|
3694
|
+
);
|
|
3695
|
+
} else if (asset.revision.from === assetId) {
|
|
3696
|
+
unknownReferences.push(`assets.${assetId}.revision.from: an asset cannot revise itself`);
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
3701
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3702
|
+
const visitRevision = (assetId, chain) => {
|
|
3703
|
+
if (visited.has(assetId)) return;
|
|
3704
|
+
if (visiting.has(assetId)) {
|
|
3705
|
+
const start = chain.indexOf(assetId);
|
|
3706
|
+
unknownReferences.push(
|
|
3707
|
+
`assets.${assetId}.revision.from: revision cycle ${[...chain.slice(start), assetId].join(" -> ")}`
|
|
3708
|
+
);
|
|
3709
|
+
return;
|
|
3710
|
+
}
|
|
3711
|
+
visiting.add(assetId);
|
|
3712
|
+
const parent = parsed.data.assets[assetId]?.revision?.from;
|
|
3713
|
+
if (parent && parent !== assetId && parsed.data.assets[parent]) {
|
|
3714
|
+
visitRevision(parent, [...chain, assetId]);
|
|
3715
|
+
}
|
|
3716
|
+
visiting.delete(assetId);
|
|
3717
|
+
visited.add(assetId);
|
|
3718
|
+
};
|
|
3719
|
+
for (const assetId of Object.keys(parsed.data.assets)) visitRevision(assetId, []);
|
|
3720
|
+
if (unknownReferences.length) {
|
|
3721
|
+
throw new Error(`Manifest at ${abs} is invalid:
|
|
3722
|
+
${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
|
|
3723
|
+
}
|
|
3724
|
+
return { manifest: parsed.data, root: path6.dirname(abs), path: abs };
|
|
3725
|
+
}
|
|
3726
|
+
async function resolveSpecs(loaded, filter) {
|
|
3727
|
+
const { manifest, root } = loaded;
|
|
3728
|
+
const providerOverride = filter?.provider;
|
|
3729
|
+
const providers = /* @__PURE__ */ new Map();
|
|
3730
|
+
const providerFor = (id) => {
|
|
3731
|
+
if (providerOverride) return providerOverride;
|
|
3732
|
+
let provider = providers.get(id);
|
|
3733
|
+
if (!provider) {
|
|
3734
|
+
provider = createProvider(id, "offline");
|
|
3735
|
+
providers.set(id, provider);
|
|
3736
|
+
}
|
|
3737
|
+
return provider;
|
|
3738
|
+
};
|
|
3739
|
+
const specs = [];
|
|
3740
|
+
const styleIds = Object.keys(manifest.styles).filter(
|
|
3741
|
+
(id) => !filter?.styles?.length || filter.styles.includes(id)
|
|
3742
|
+
);
|
|
3743
|
+
for (const unknownStyle of filter?.styles ?? []) {
|
|
3744
|
+
if (!manifest.styles[unknownStyle]) {
|
|
3745
|
+
throw new Error(
|
|
3746
|
+
`Unknown style "${unknownStyle}". Defined: ${Object.keys(manifest.styles).join(", ") || "(none)"}`
|
|
3747
|
+
);
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
for (const unknownAsset of filter?.assets ?? []) {
|
|
3751
|
+
if (!manifest.assets[unknownAsset]) {
|
|
3752
|
+
throw new Error(`Unknown asset "${unknownAsset}".`);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
const requestedAssetIds = new Set(filter?.assets?.length ? filter.assets : Object.keys(manifest.assets));
|
|
3756
|
+
const resolutionAssetIds = new Set(requestedAssetIds);
|
|
3757
|
+
for (const assetId of [...requestedAssetIds]) {
|
|
3758
|
+
let current = manifest.assets[assetId]?.revision?.from;
|
|
3759
|
+
while (current && !resolutionAssetIds.has(current)) {
|
|
3760
|
+
resolutionAssetIds.add(current);
|
|
3761
|
+
current = manifest.assets[current]?.revision?.from;
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
const styleImageCache = /* @__PURE__ */ new Map();
|
|
3765
|
+
async function loadStyleImage(rel) {
|
|
3766
|
+
const abs = path6.resolve(root, rel);
|
|
3767
|
+
let hit = styleImageCache.get(abs);
|
|
3768
|
+
if (!hit) {
|
|
3769
|
+
if (!existsSync4(abs)) throw new Error(`Style image not found: ${abs}`);
|
|
3770
|
+
const buf = await readFile3(abs);
|
|
3771
|
+
const metadata = imageMetadata(buf);
|
|
3772
|
+
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
|
|
3773
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
3774
|
+
throw new Error(`Style image has invalid dimensions: ${abs}`);
|
|
3775
|
+
}
|
|
3776
|
+
hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
|
|
3777
|
+
styleImageCache.set(abs, hit);
|
|
3778
|
+
}
|
|
3779
|
+
return hit;
|
|
3780
|
+
}
|
|
3781
|
+
for (const styleId of styleIds) {
|
|
3782
|
+
const style = manifest.styles[styleId];
|
|
3783
|
+
const activeProvider = providerFor(style.provider ?? manifest.provider);
|
|
3784
|
+
const rawProviderOptions = style.providerOptions[activeProvider.id] ?? {};
|
|
3785
|
+
const optionResolution = activeProvider.resolveOptions ? await activeProvider.resolveOptions(rawProviderOptions, { root, styleId }) : { options: rawProviderOptions };
|
|
3786
|
+
const providerOptions = optionResolution.options;
|
|
3787
|
+
const providerOptionIdentity = optionResolution.identity ?? providerOptions;
|
|
3788
|
+
const styleImageHashes = [];
|
|
3789
|
+
const styleImageDimensions = [];
|
|
3790
|
+
for (const img of style.styleImages) {
|
|
3791
|
+
const loadedImage = await loadStyleImage(img.path);
|
|
3792
|
+
styleImageHashes.push(loadedImage.hash);
|
|
3793
|
+
styleImageDimensions.push(loadedImage);
|
|
3794
|
+
}
|
|
3795
|
+
const resolvedImages = style.styleImages.map((image) => {
|
|
3796
|
+
const hit = styleImageCache.get(path6.resolve(root, image.path));
|
|
3797
|
+
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
3798
|
+
});
|
|
3330
3799
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
3331
3800
|
const providerInputIdentities = /* @__PURE__ */ new Map();
|
|
3332
3801
|
for (const [assetId, asset] of Object.entries(manifest.assets)) {
|
|
@@ -3345,6 +3814,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3345
3814
|
root,
|
|
3346
3815
|
styleId,
|
|
3347
3816
|
assetId,
|
|
3817
|
+
generator,
|
|
3348
3818
|
providerOptions
|
|
3349
3819
|
}) : { inputs: {} };
|
|
3350
3820
|
const providerInputs = inputResolution.inputs;
|
|
@@ -3368,9 +3838,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3368
3838
|
}
|
|
3369
3839
|
const subject = asset.promptByStyle[styleId] ?? asset.prompt;
|
|
3370
3840
|
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 ?
|
|
3841
|
+
const relFile = asset.file ?? path6.join(asset.category ?? "", `${assetId}.png`);
|
|
3842
|
+
const outFile = path6.resolve(root, style.outDir, relFile);
|
|
3843
|
+
const qualityOutFile = style.quality ? path6.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
|
|
3374
3844
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
3375
3845
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
3376
3846
|
const base = {
|
|
@@ -3421,7 +3891,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3421
3891
|
minGridConfidence: style.quality.minGridConfidence,
|
|
3422
3892
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
3423
3893
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
3424
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
3894
|
+
...style.quality.fixerPython ? { fixerPython: path6.resolve(root, style.quality.fixerPython) } : {},
|
|
3895
|
+
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
3425
3896
|
}
|
|
3426
3897
|
} : {},
|
|
3427
3898
|
tags,
|
|
@@ -3449,9 +3920,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3449
3920
|
);
|
|
3450
3921
|
}
|
|
3451
3922
|
const sourceSpec = await finalize(asset.revision.from);
|
|
3452
|
-
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
3923
|
+
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path6.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
3453
3924
|
const sourceImage = await optionalRevisionImage(sourceFile, "revision source");
|
|
3454
|
-
const maskFile = asset.revision.mask ?
|
|
3925
|
+
const maskFile = asset.revision.mask ? path6.resolve(root, asset.revision.mask) : void 0;
|
|
3455
3926
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
3456
3927
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
3457
3928
|
throw new Error(
|
|
@@ -3507,349 +3978,92 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3507
3978
|
for (const spec of specs) {
|
|
3508
3979
|
const key = `${spec.styleId}/${spec.assetId}`;
|
|
3509
3980
|
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}`);
|
|
3981
|
+
if (spec.quality) {
|
|
3982
|
+
if (spec.generator === "frames") {
|
|
3983
|
+
const frameInputs = Object.values(spec.providerInputs ?? {}).find(Array.isArray);
|
|
3984
|
+
const count = frameInputs?.length ?? 0;
|
|
3985
|
+
for (let index = 0; index < count; index++) {
|
|
3986
|
+
const role = `frame-${String(index).padStart(2, "0")}`;
|
|
3987
|
+
claimOutput(
|
|
3988
|
+
expectedOutputPath(
|
|
3989
|
+
{ ...spec, outFile: spec.quality.outFile },
|
|
3990
|
+
role,
|
|
3991
|
+
index,
|
|
3992
|
+
count,
|
|
3993
|
+
MediaType.PNG
|
|
3994
|
+
),
|
|
3995
|
+
`${key} quality ${role}`
|
|
3996
|
+
);
|
|
3997
|
+
}
|
|
3998
|
+
} else {
|
|
3999
|
+
claimOutput(spec.quality.outFile, `${key} quality output`);
|
|
3736
4000
|
}
|
|
3737
|
-
await new Promise((resolve) => setTimeout(resolve, 25 + Math.floor(Math.random() * 25)));
|
|
3738
4001
|
}
|
|
3739
4002
|
}
|
|
4003
|
+
return specs;
|
|
3740
4004
|
}
|
|
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);
|
|
4005
|
+
function pngPath(file) {
|
|
4006
|
+
const extension = path6.extname(file);
|
|
4007
|
+
return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
|
|
3780
4008
|
}
|
|
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 });
|
|
4009
|
+
async function optionalRevisionImage(file, label, requiredFormat) {
|
|
4010
|
+
if (!existsSync4(file)) return null;
|
|
4011
|
+
const bytes = await readFile3(file);
|
|
4012
|
+
const metadata = imageMetadata(bytes);
|
|
4013
|
+
if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
|
|
4014
|
+
if (requiredFormat && metadata.format !== requiredFormat) {
|
|
4015
|
+
throw new Error(`${label} must be ${requiredFormat.toUpperCase()}: ${file}`);
|
|
3797
4016
|
}
|
|
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
|
-
}));
|
|
4017
|
+
return { hash: sha256(bytes), ...metadata };
|
|
3824
4018
|
}
|
|
3825
|
-
function
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
4019
|
+
async function resolveStyleImages(loaded, styleId) {
|
|
4020
|
+
const style = loaded.manifest.styles[styleId];
|
|
4021
|
+
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
4022
|
+
const out = [];
|
|
4023
|
+
for (const img of style.styleImages) {
|
|
4024
|
+
const file = path6.resolve(loaded.root, img.path);
|
|
4025
|
+
const buf = await readFile3(file);
|
|
4026
|
+
const metadata = imageMetadata(buf);
|
|
4027
|
+
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
4028
|
+
if (metadata.width < 1 || metadata.height < 1) {
|
|
4029
|
+
throw new Error(`Style image has invalid dimensions: ${file}`);
|
|
4030
|
+
}
|
|
4031
|
+
out.push({ base64: buf.toString("base64"), ...metadata });
|
|
3829
4032
|
}
|
|
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
|
-
};
|
|
4033
|
+
return out;
|
|
3838
4034
|
}
|
|
3839
|
-
function
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
4035
|
+
function imageMetadata(buf) {
|
|
4036
|
+
if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
|
|
4037
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
|
|
4038
|
+
}
|
|
4039
|
+
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
|
|
4040
|
+
let offset = 2;
|
|
4041
|
+
while (offset + 3 < buf.length) {
|
|
4042
|
+
if (buf[offset] !== 255) return null;
|
|
4043
|
+
while (buf[offset] === 255) offset++;
|
|
4044
|
+
const marker = buf[offset++];
|
|
4045
|
+
if (marker == null || marker === 217 || marker === 218) break;
|
|
4046
|
+
if (marker === 1 || marker >= 208 && marker <= 215) continue;
|
|
4047
|
+
if (offset + 2 > buf.length) return null;
|
|
4048
|
+
const length = buf.readUInt16BE(offset);
|
|
4049
|
+
if (length < 2 || offset + length > buf.length) return null;
|
|
4050
|
+
const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
|
|
4051
|
+
if (isStartOfFrame) {
|
|
4052
|
+
if (length < 7) return null;
|
|
4053
|
+
return {
|
|
4054
|
+
width: buf.readUInt16BE(offset + 5),
|
|
4055
|
+
height: buf.readUInt16BE(offset + 3),
|
|
4056
|
+
format: "jpeg"
|
|
4057
|
+
};
|
|
4058
|
+
}
|
|
4059
|
+
offset += length;
|
|
4060
|
+
}
|
|
4061
|
+
return null;
|
|
3850
4062
|
}
|
|
3851
4063
|
|
|
3852
4064
|
// src/pipeline/pack.ts
|
|
4065
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
4066
|
+
import path7 from "path";
|
|
3853
4067
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
3854
4068
|
if (!Array.isArray(raw) || !raw.length) {
|
|
3855
4069
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
@@ -3928,7 +4142,7 @@ function packSprites(inputs, options = {}) {
|
|
|
3928
4142
|
function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
3929
4143
|
if (options.sourceOverrides) {
|
|
3930
4144
|
if (options.outputRoles?.length) {
|
|
3931
|
-
throw new Error("Quality-profile packing does not accept --output-role; profiles own
|
|
4145
|
+
throw new Error("Quality-profile packing does not accept --output-role; profiles own final output selection.");
|
|
3932
4146
|
}
|
|
3933
4147
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
3934
4148
|
id,
|
|
@@ -4740,6 +4954,8 @@ function hex(c) {
|
|
|
4740
4954
|
|
|
4741
4955
|
// src/pipeline/refine.ts
|
|
4742
4956
|
var execFileAsync = promisify(execFile);
|
|
4957
|
+
var PIXEL_ART_FIXER_MIN_SIDE = 16;
|
|
4958
|
+
var PIXEL_ART_FIXER_MAX_PIXELS = 4e6;
|
|
4743
4959
|
var PIXEL_ART_FIXER_URL = "https://github.com/Retro-Diffusion/pixel-art-fixer";
|
|
4744
4960
|
var PIXEL_ART_FIXER_REVISION = "ef376e57e1c272633ca2dbf5f29ec3fcf6596465";
|
|
4745
4961
|
function message2(error) {
|
|
@@ -4787,25 +5003,33 @@ function firstJsonObject(text) {
|
|
|
4787
5003
|
}
|
|
4788
5004
|
var PYTHON_FIXER_BRIDGE = [
|
|
4789
5005
|
"import json, sys",
|
|
4790
|
-
"from
|
|
4791
|
-
"
|
|
4792
|
-
"
|
|
4793
|
-
"
|
|
5006
|
+
"from PIL import Image",
|
|
5007
|
+
"from pixelfixer.api import _load",
|
|
5008
|
+
"from pixelfixer.core import detect",
|
|
5009
|
+
"from pixelfixer.reconstruct import two_stage_pack",
|
|
5010
|
+
"rgba = _load(open(sys.argv[1], 'rb').read())",
|
|
5011
|
+
"result = detect(rgba, mode='full')",
|
|
5012
|
+
"Image.fromarray(two_stage_pack(rgba, result['cols'], result['rows'])).save(sys.argv[2], 'PNG')",
|
|
5013
|
+
"print(json.dumps({key: result[key] for key in ('cols', 'rows', 'step_x', 'step_y', 'phase_x', 'phase_y', 'consensus')}))"
|
|
4794
5014
|
].join("; ");
|
|
4795
5015
|
function parseDetection(raw) {
|
|
4796
5016
|
if (!raw || typeof raw !== "object") throw new Error("Pixel Art Fixer returned no detection result.");
|
|
4797
5017
|
const value = raw;
|
|
4798
5018
|
const stepX = Number(value.step_x);
|
|
4799
5019
|
const stepY = Number(value.step_y);
|
|
5020
|
+
const phaseX = Number(value.phase_x);
|
|
5021
|
+
const phaseY = Number(value.phase_y);
|
|
4800
5022
|
const columns = Number(value.cols);
|
|
4801
5023
|
const rows = Number(value.rows);
|
|
4802
5024
|
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) {
|
|
5025
|
+
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
5026
|
throw new Error("Pixel Art Fixer returned an invalid grid detection result.");
|
|
4805
5027
|
}
|
|
4806
5028
|
return {
|
|
4807
5029
|
stepX,
|
|
4808
5030
|
stepY,
|
|
5031
|
+
phaseX,
|
|
5032
|
+
phaseY,
|
|
4809
5033
|
columns,
|
|
4810
5034
|
rows,
|
|
4811
5035
|
consensus,
|
|
@@ -4916,6 +5140,23 @@ function auditRefinement(png, detection, palette, minTransparency, minGridConfid
|
|
|
4916
5140
|
function isGridConfidence(value) {
|
|
4917
5141
|
return value === "low" || value === "medium" || value === "high";
|
|
4918
5142
|
}
|
|
5143
|
+
function isDetection(value) {
|
|
5144
|
+
if (!value || typeof value !== "object") return false;
|
|
5145
|
+
const detection = value;
|
|
5146
|
+
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);
|
|
5147
|
+
}
|
|
5148
|
+
function isAudit(value) {
|
|
5149
|
+
if (!value || typeof value !== "object") return false;
|
|
5150
|
+
const audit = value;
|
|
5151
|
+
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");
|
|
5152
|
+
}
|
|
5153
|
+
function validFrameSet(value) {
|
|
5154
|
+
if (!value || typeof value !== "object") return false;
|
|
5155
|
+
const frameSet = value;
|
|
5156
|
+
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(
|
|
5157
|
+
(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)
|
|
5158
|
+
);
|
|
5159
|
+
}
|
|
4919
5160
|
function parseRecordOptions(manifest, record) {
|
|
4920
5161
|
if (manifest.kind !== "refine") throw new Error(`${record} is not a PixelKiln refinement record.`);
|
|
4921
5162
|
const raw = manifest.options;
|
|
@@ -4925,11 +5166,26 @@ function parseRecordOptions(manifest, record) {
|
|
|
4925
5166
|
const palette = options.palette;
|
|
4926
5167
|
const audit = options.audit;
|
|
4927
5168
|
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" || !
|
|
5169
|
+
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
5170
|
throw new Error(`${record} has invalid quality metadata.`);
|
|
4930
5171
|
}
|
|
4931
5172
|
return options;
|
|
4932
5173
|
}
|
|
5174
|
+
function safeFrameRole(role, index) {
|
|
5175
|
+
const safe = role.trim().replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5176
|
+
return safe || `frame-${String(index).padStart(2, "0")}`;
|
|
5177
|
+
}
|
|
5178
|
+
function qualityFrameOutputPath(output, role, index) {
|
|
5179
|
+
if (!/\.png$/i.test(output)) throw new Error("Refined frame-set output must use a .png stem.");
|
|
5180
|
+
return output.replace(/\.png$/i, `-${safeFrameRole(role, index)}.png`);
|
|
5181
|
+
}
|
|
5182
|
+
function sameFrameGrid(first, next) {
|
|
5183
|
+
const phaseMatches = (a, b, step) => {
|
|
5184
|
+
const distance = Math.abs(a - b) % step;
|
|
5185
|
+
return Math.min(distance, step - distance) <= 0.01;
|
|
5186
|
+
};
|
|
5187
|
+
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;
|
|
5188
|
+
}
|
|
4933
5189
|
async function refineAsset(options) {
|
|
4934
5190
|
const source = path10.resolve(options.source);
|
|
4935
5191
|
const output = path10.resolve(options.output);
|
|
@@ -4944,9 +5200,21 @@ async function refineAsset(options) {
|
|
|
4944
5200
|
} catch (error) {
|
|
4945
5201
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
4946
5202
|
}
|
|
5203
|
+
const compatibilityCommand = options.fixerCommand;
|
|
5204
|
+
if (!compatibilityCommand) {
|
|
5205
|
+
if (sourcePng.width < PIXEL_ART_FIXER_MIN_SIDE || sourcePng.height < PIXEL_ART_FIXER_MIN_SIDE) {
|
|
5206
|
+
throw new Error(
|
|
5207
|
+
`Pixel Art Fixer requires each source side to be at least ${PIXEL_ART_FIXER_MIN_SIDE}px; got ${sourcePng.width}x${sourcePng.height}.`
|
|
5208
|
+
);
|
|
5209
|
+
}
|
|
5210
|
+
if (sourcePng.width * sourcePng.height > PIXEL_ART_FIXER_MAX_PIXELS) {
|
|
5211
|
+
throw new Error(
|
|
5212
|
+
`Pixel Art Fixer accepts at most ${PIXEL_ART_FIXER_MAX_PIXELS.toLocaleString("en-US")} source pixels; got ${sourcePng.width}x${sourcePng.height}.`
|
|
5213
|
+
);
|
|
5214
|
+
}
|
|
5215
|
+
}
|
|
4947
5216
|
const temp = await mkdtemp(path10.join(tmpdir(), "pixelkiln-refine-"));
|
|
4948
5217
|
const recovered = path10.join(temp, "native.png");
|
|
4949
|
-
const compatibilityCommand = options.fixerCommand;
|
|
4950
5218
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
4951
5219
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
4952
5220
|
try {
|
|
@@ -4993,11 +5261,13 @@ async function refineAsset(options) {
|
|
|
4993
5261
|
tool: "pixel-art-fixer",
|
|
4994
5262
|
url: PIXEL_ART_FIXER_URL,
|
|
4995
5263
|
revision: options.fixerRevision ?? PIXEL_ART_FIXER_REVISION,
|
|
4996
|
-
protocol: compatibilityCommand ? "python-cli-v1" : "python-
|
|
5264
|
+
protocol: compatibilityCommand ? "python-cli-v1" : "python-core-v1",
|
|
4997
5265
|
confidence: detection.confidence,
|
|
4998
5266
|
consensus: detection.consensus,
|
|
4999
5267
|
stepX: detection.stepX,
|
|
5000
5268
|
stepY: detection.stepY,
|
|
5269
|
+
phaseX: detection.phaseX,
|
|
5270
|
+
phaseY: detection.phaseY,
|
|
5001
5271
|
sourceWidth: sourcePng.width,
|
|
5002
5272
|
sourceHeight: sourcePng.height,
|
|
5003
5273
|
nativeWidth: quantized.width,
|
|
@@ -5018,19 +5288,142 @@ async function refineAsset(options) {
|
|
|
5018
5288
|
await rm3(temp, { recursive: true, force: true });
|
|
5019
5289
|
}
|
|
5020
5290
|
}
|
|
5291
|
+
async function refineFrameSet(options) {
|
|
5292
|
+
if (!Number.isInteger(options.fps) || options.fps < 1 || options.fps > 60) {
|
|
5293
|
+
throw new Error("Frame-set fps must be an integer between 1 and 60.");
|
|
5294
|
+
}
|
|
5295
|
+
if (options.sources.length < 2 || options.sources.length > 64) {
|
|
5296
|
+
throw new Error(`A frame set must contain 2\u201364 ordered sources; got ${options.sources.length}.`);
|
|
5297
|
+
}
|
|
5298
|
+
const roles = options.sources.map((source) => source.role);
|
|
5299
|
+
if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
|
|
5300
|
+
throw new Error("Frame-set roles must be non-empty and unique.");
|
|
5301
|
+
}
|
|
5302
|
+
const output = path10.resolve(options.output);
|
|
5303
|
+
const record = path10.resolve(qualityRecordPath(output));
|
|
5304
|
+
const palette = normalizeRefinementPalette(options.palette);
|
|
5305
|
+
const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
|
|
5306
|
+
if (new Set(finalOutputs).size !== finalOutputs.length) {
|
|
5307
|
+
throw new Error("Frame-set roles resolve to duplicate output filenames.");
|
|
5308
|
+
}
|
|
5309
|
+
const sourceSet = new Set(options.sources.map((source) => path10.resolve(source.path)));
|
|
5310
|
+
if (finalOutputs.some((file) => sourceSet.has(file))) {
|
|
5311
|
+
throw new Error("Frame-set refinement sources and outputs must be different files.");
|
|
5312
|
+
}
|
|
5313
|
+
const temp = await mkdtemp(path10.join(tmpdir(), "pixelkiln-refine-frames-"));
|
|
5314
|
+
try {
|
|
5315
|
+
const frames = [];
|
|
5316
|
+
const outputs = [];
|
|
5317
|
+
const provenanceSources = [];
|
|
5318
|
+
const sourcePaths = [];
|
|
5319
|
+
let firstOptions = null;
|
|
5320
|
+
for (const [index, sourceEntry] of options.sources.entries()) {
|
|
5321
|
+
const source = path10.resolve(sourceEntry.path);
|
|
5322
|
+
const temporaryOutput = path10.join(temp, `${String(index).padStart(2, "0")}.png`);
|
|
5323
|
+
const refined = await refineAsset({
|
|
5324
|
+
source,
|
|
5325
|
+
output: temporaryOutput,
|
|
5326
|
+
palette,
|
|
5327
|
+
fixerPython: options.fixerPython,
|
|
5328
|
+
fixerCommand: options.fixerCommand,
|
|
5329
|
+
fixerArgsPrefix: options.fixerArgsPrefix,
|
|
5330
|
+
fixerRevision: options.fixerRevision,
|
|
5331
|
+
minGridConfidence: options.minGridConfidence,
|
|
5332
|
+
minTransparency: options.minTransparency,
|
|
5333
|
+
force: true
|
|
5334
|
+
});
|
|
5335
|
+
const temporaryRecord = await readArtifactBundleManifest(refined.record);
|
|
5336
|
+
const frameOptions = parseRecordOptions(temporaryRecord, refined.record);
|
|
5337
|
+
firstOptions ??= frameOptions;
|
|
5338
|
+
frames.push({
|
|
5339
|
+
role: sourceEntry.role,
|
|
5340
|
+
sourceWidth: frameOptions.nativeGrid.sourceWidth,
|
|
5341
|
+
sourceHeight: frameOptions.nativeGrid.sourceHeight,
|
|
5342
|
+
detection: refined.detection,
|
|
5343
|
+
audit: refined.audit
|
|
5344
|
+
});
|
|
5345
|
+
sourcePaths.push(source);
|
|
5346
|
+
provenanceSources.push({
|
|
5347
|
+
id: sourceEntry.role,
|
|
5348
|
+
path: source,
|
|
5349
|
+
sha256: await sha256File(source),
|
|
5350
|
+
included: true
|
|
5351
|
+
});
|
|
5352
|
+
outputs.push({
|
|
5353
|
+
path: finalOutputs[index],
|
|
5354
|
+
data: await readFile6(temporaryOutput)
|
|
5355
|
+
});
|
|
5356
|
+
}
|
|
5357
|
+
const first = frames[0];
|
|
5358
|
+
const mismatch = frames.find((frame) => !sameFrameGrid(first, frame));
|
|
5359
|
+
if (mismatch) {
|
|
5360
|
+
throw new Error(
|
|
5361
|
+
`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}.`
|
|
5362
|
+
);
|
|
5363
|
+
}
|
|
5364
|
+
const reasons = frames.flatMap(
|
|
5365
|
+
(frame) => frame.audit.reasons.map((reason) => `${frame.role}: ${reason}`)
|
|
5366
|
+
);
|
|
5367
|
+
const audit = {
|
|
5368
|
+
width: first.audit.width,
|
|
5369
|
+
height: first.audit.height,
|
|
5370
|
+
colorCount: Math.max(...frames.map((frame) => frame.audit.colorCount)),
|
|
5371
|
+
paletteConformant: frames.every((frame) => frame.audit.paletteConformant),
|
|
5372
|
+
transparency: Math.min(...frames.map((frame) => frame.audit.transparency)),
|
|
5373
|
+
thresholds: first.audit.thresholds,
|
|
5374
|
+
safe: frames.every((frame) => frame.audit.safe) && reasons.length === 0,
|
|
5375
|
+
reasons
|
|
5376
|
+
};
|
|
5377
|
+
const baseOptions = firstOptions;
|
|
5378
|
+
const recordOptions = {
|
|
5379
|
+
...baseOptions,
|
|
5380
|
+
audit,
|
|
5381
|
+
review: { status: "pending" },
|
|
5382
|
+
frameSet: {
|
|
5383
|
+
fps: options.fps,
|
|
5384
|
+
count: frames.length,
|
|
5385
|
+
roles,
|
|
5386
|
+
frames
|
|
5387
|
+
}
|
|
5388
|
+
};
|
|
5389
|
+
await writeManagedArtifactBundle(
|
|
5390
|
+
record,
|
|
5391
|
+
outputs,
|
|
5392
|
+
{ kind: "refine", sources: provenanceSources, options: recordOptions },
|
|
5393
|
+
{ force: options.force }
|
|
5394
|
+
);
|
|
5395
|
+
return {
|
|
5396
|
+
sources: sourcePaths,
|
|
5397
|
+
outputs: outputs.map((item) => path10.resolve(item.path)),
|
|
5398
|
+
record,
|
|
5399
|
+
frames,
|
|
5400
|
+
palette,
|
|
5401
|
+
audit
|
|
5402
|
+
};
|
|
5403
|
+
} finally {
|
|
5404
|
+
await rm3(temp, { recursive: true, force: true });
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5021
5407
|
async function checkQualityRecord(recordPath) {
|
|
5022
5408
|
const record = path10.resolve(recordPath);
|
|
5023
5409
|
const manifest = await readArtifactBundleManifest(record);
|
|
5024
5410
|
const options = parseRecordOptions(manifest, record);
|
|
5025
5411
|
const verification = await verifyArtifactBundle(record);
|
|
5026
|
-
const
|
|
5027
|
-
const
|
|
5412
|
+
const sources = manifest.sources.map((source2) => path10.resolve(path10.dirname(record), source2.path));
|
|
5413
|
+
const outputs = manifest.outputs.map((output2) => path10.resolve(path10.dirname(record), output2.path));
|
|
5414
|
+
const source = sources[0] ?? "";
|
|
5415
|
+
const output = outputs[0] ?? "";
|
|
5416
|
+
const expectedCount = options.frameSet?.count ?? 1;
|
|
5028
5417
|
const reasons = [];
|
|
5029
5418
|
if (!verification.fingerprintValid) reasons.push("quality record fingerprint changed");
|
|
5030
5419
|
for (const source2 of verification.changedSources) reasons.push(`source changed: ${source2}`);
|
|
5031
5420
|
for (const changed of verification.changedOutputs) reasons.push(`output changed: ${changed}`);
|
|
5032
|
-
if (manifest.sources.length !==
|
|
5033
|
-
|
|
5421
|
+
if (manifest.sources.length !== expectedCount) {
|
|
5422
|
+
reasons.push(`quality record must contain exactly ${expectedCount} source(s)`);
|
|
5423
|
+
}
|
|
5424
|
+
if (manifest.outputs.length !== expectedCount) {
|
|
5425
|
+
reasons.push(`quality record must contain exactly ${expectedCount} output(s)`);
|
|
5426
|
+
}
|
|
5034
5427
|
if (!options.audit.safe) reasons.push(...options.audit.reasons.map((reason) => `audit: ${reason}`));
|
|
5035
5428
|
if (options.review.status !== "approved") reasons.push("human 1\xD7 review is pending");
|
|
5036
5429
|
return {
|
|
@@ -5041,6 +5434,8 @@ async function checkQualityRecord(recordPath) {
|
|
|
5041
5434
|
record,
|
|
5042
5435
|
source,
|
|
5043
5436
|
output,
|
|
5437
|
+
sources,
|
|
5438
|
+
outputs,
|
|
5044
5439
|
options,
|
|
5045
5440
|
reasons
|
|
5046
5441
|
};
|
|
@@ -5055,7 +5450,7 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5055
5450
|
if (!verification.current) {
|
|
5056
5451
|
const changes = [
|
|
5057
5452
|
...verification.changedSources.map((source) => `source ${source}`),
|
|
5058
|
-
...verification.changedOutputs.map((
|
|
5453
|
+
...verification.changedOutputs.map((output) => `output ${output}`),
|
|
5059
5454
|
...!verification.fingerprintValid ? ["quality metadata"] : []
|
|
5060
5455
|
];
|
|
5061
5456
|
throw new Error(`Cannot approve a stale refinement; changed: ${changes.join(", ")}. Run refine again.`);
|
|
@@ -5063,12 +5458,17 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5063
5458
|
if (!options.audit.safe) {
|
|
5064
5459
|
throw new Error(`Cannot approve a failed quality audit: ${options.audit.reasons.join("; ")}`);
|
|
5065
5460
|
}
|
|
5066
|
-
|
|
5067
|
-
|
|
5461
|
+
const expectedCount = options.frameSet?.count ?? 1;
|
|
5462
|
+
if (manifest.sources.length !== expectedCount || manifest.outputs.length !== expectedCount) {
|
|
5463
|
+
throw new Error(
|
|
5464
|
+
`A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
|
|
5465
|
+
);
|
|
5068
5466
|
}
|
|
5069
5467
|
const root = path10.dirname(record);
|
|
5070
|
-
const
|
|
5071
|
-
|
|
5468
|
+
const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
|
|
5469
|
+
const outputPath = path10.resolve(root, entry.path);
|
|
5470
|
+
return { path: outputPath, data: await readFile6(outputPath) };
|
|
5471
|
+
}));
|
|
5072
5472
|
const sources = manifest.sources.map((source) => ({
|
|
5073
5473
|
...source,
|
|
5074
5474
|
path: path10.resolve(root, source.path)
|
|
@@ -5091,7 +5491,7 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5091
5491
|
};
|
|
5092
5492
|
await writeManagedArtifactBundle(
|
|
5093
5493
|
record,
|
|
5094
|
-
|
|
5494
|
+
outputs,
|
|
5095
5495
|
{ kind: "refine", sources, options: approved }
|
|
5096
5496
|
);
|
|
5097
5497
|
return checkQualityRecord(record);
|
|
@@ -5100,33 +5500,46 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
5100
5500
|
// src/pipeline/quality-profile.ts
|
|
5101
5501
|
async function qualitySource(spec, lock) {
|
|
5102
5502
|
if (spec.source) {
|
|
5103
|
-
|
|
5104
|
-
|
|
5503
|
+
if (spec.generator === "frames") {
|
|
5504
|
+
return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
|
|
5505
|
+
}
|
|
5506
|
+
const source = path11.resolve(spec.root, spec.source);
|
|
5507
|
+
return existsSync6(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
|
|
5105
5508
|
}
|
|
5106
5509
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
5107
|
-
if (!entry) return {
|
|
5510
|
+
if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
|
|
5108
5511
|
if (entry.specHash !== spec.specHash) {
|
|
5109
|
-
return {
|
|
5512
|
+
return { items: null, reason: "raw provider output does not match the current generation spec" };
|
|
5110
5513
|
}
|
|
5111
5514
|
if (entry.status !== "downloaded") {
|
|
5112
|
-
return {
|
|
5515
|
+
return { items: null, reason: `raw provider output is ${entry.status}, not downloaded` };
|
|
5113
5516
|
}
|
|
5114
|
-
|
|
5517
|
+
const frameSet = spec.generator === "frames";
|
|
5518
|
+
if (!frameSet && entry.outputs.length !== 1 || frameSet && entry.outputs.length < 2) {
|
|
5115
5519
|
return {
|
|
5116
|
-
|
|
5117
|
-
reason: `quality profiles require one PNG output; lock entry has ${entry.outputs.length}`
|
|
5520
|
+
items: null,
|
|
5521
|
+
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
5522
|
};
|
|
5119
5523
|
}
|
|
5120
|
-
const
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5524
|
+
const roles = /* @__PURE__ */ new Set();
|
|
5525
|
+
const items = [];
|
|
5526
|
+
for (const [index, output] of entry.outputs.entries()) {
|
|
5527
|
+
if (output.mediaType && output.mediaType !== MediaType.PNG) {
|
|
5528
|
+
return { items: null, reason: `quality profiles require PNG input; lock entry is ${output.mediaType}` };
|
|
5529
|
+
}
|
|
5530
|
+
const role = frameSet ? output.role : "source";
|
|
5531
|
+
if (frameSet && (!role || roles.has(role))) {
|
|
5532
|
+
return { items: null, reason: "frame-set quality requires a unique role for every ordered output" };
|
|
5533
|
+
}
|
|
5534
|
+
if (role) roles.add(role);
|
|
5535
|
+
const source = currentEntryOutputPath(entry, spec, index);
|
|
5536
|
+
if (!existsSync6(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
|
|
5537
|
+
if (await sha256File(source) !== output.sha256) {
|
|
5538
|
+
return { items: null, reason: "raw provider output was modified after download" };
|
|
5539
|
+
}
|
|
5540
|
+
items.push({ role, path: source });
|
|
5128
5541
|
}
|
|
5129
|
-
return {
|
|
5542
|
+
return { items };
|
|
5130
5543
|
}
|
|
5131
5544
|
async function inspectQualityProfile(spec, lock) {
|
|
5132
5545
|
if (!spec.quality) return null;
|
|
@@ -5134,16 +5547,20 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5134
5547
|
const output = spec.quality.outFile;
|
|
5135
5548
|
const record = qualityRecordPath(output);
|
|
5136
5549
|
const source = await qualitySource(spec, lock);
|
|
5137
|
-
if (!source.
|
|
5550
|
+
if (!source.items) {
|
|
5138
5551
|
return { key, state: "blocked", reason: source.reason, source: null, output, record };
|
|
5139
5552
|
}
|
|
5553
|
+
const sourcePaths = source.items.map((item) => item.path);
|
|
5554
|
+
const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
|
|
5140
5555
|
if (!existsSync6(record)) {
|
|
5141
5556
|
return {
|
|
5142
5557
|
key,
|
|
5143
5558
|
state: "needs-refinement",
|
|
5144
5559
|
reason: "no refinement record",
|
|
5145
|
-
source:
|
|
5560
|
+
source: sourcePaths[0],
|
|
5561
|
+
sources: sourcePaths,
|
|
5146
5562
|
output,
|
|
5563
|
+
outputs: expectedOutputs,
|
|
5147
5564
|
record
|
|
5148
5565
|
};
|
|
5149
5566
|
}
|
|
@@ -5152,14 +5569,17 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5152
5569
|
const expectedPalette = normalizeRefinementPalette(spec.quality.palette);
|
|
5153
5570
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
5154
5571
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
5155
|
-
const
|
|
5572
|
+
const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
|
|
5573
|
+
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
5574
|
if (contractChanged) {
|
|
5157
5575
|
return {
|
|
5158
5576
|
key,
|
|
5159
5577
|
state: "needs-refinement",
|
|
5160
5578
|
reason: "quality profile changed",
|
|
5161
|
-
source:
|
|
5579
|
+
source: sourcePaths[0],
|
|
5580
|
+
sources: sourcePaths,
|
|
5162
5581
|
output,
|
|
5582
|
+
outputs: expectedOutputs,
|
|
5163
5583
|
record
|
|
5164
5584
|
};
|
|
5165
5585
|
}
|
|
@@ -5168,8 +5588,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5168
5588
|
key,
|
|
5169
5589
|
state: "needs-refinement",
|
|
5170
5590
|
reason: check.reasons.filter((reason) => reason !== "human 1\xD7 review is pending").join("; ") || "refinement record is stale",
|
|
5171
|
-
source:
|
|
5591
|
+
source: sourcePaths[0],
|
|
5592
|
+
sources: sourcePaths,
|
|
5172
5593
|
output,
|
|
5594
|
+
outputs: expectedOutputs,
|
|
5173
5595
|
record
|
|
5174
5596
|
};
|
|
5175
5597
|
}
|
|
@@ -5178,8 +5600,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5178
5600
|
key,
|
|
5179
5601
|
state: "needs-approval",
|
|
5180
5602
|
reason: "automated checks passed; human 1\xD7 review is pending",
|
|
5181
|
-
source:
|
|
5603
|
+
source: sourcePaths[0],
|
|
5604
|
+
sources: sourcePaths,
|
|
5182
5605
|
output,
|
|
5606
|
+
outputs: expectedOutputs,
|
|
5183
5607
|
record
|
|
5184
5608
|
};
|
|
5185
5609
|
}
|
|
@@ -5187,8 +5611,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5187
5611
|
key,
|
|
5188
5612
|
state: "approved",
|
|
5189
5613
|
reason: "refined output and human approval are current",
|
|
5190
|
-
source:
|
|
5614
|
+
source: sourcePaths[0],
|
|
5615
|
+
sources: sourcePaths,
|
|
5191
5616
|
output,
|
|
5617
|
+
outputs: expectedOutputs,
|
|
5192
5618
|
record
|
|
5193
5619
|
};
|
|
5194
5620
|
} catch (error) {
|
|
@@ -5196,8 +5622,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5196
5622
|
key,
|
|
5197
5623
|
state: "needs-refinement",
|
|
5198
5624
|
reason: `invalid refinement record: ${error instanceof Error ? error.message : String(error)}`,
|
|
5199
|
-
source:
|
|
5625
|
+
source: sourcePaths[0],
|
|
5626
|
+
sources: sourcePaths,
|
|
5200
5627
|
output,
|
|
5628
|
+
outputs: expectedOutputs,
|
|
5201
5629
|
record
|
|
5202
5630
|
};
|
|
5203
5631
|
}
|
|
@@ -5221,8 +5649,7 @@ async function refineQualityProfiles(specs, lock, options = {}) {
|
|
|
5221
5649
|
continue;
|
|
5222
5650
|
}
|
|
5223
5651
|
try {
|
|
5224
|
-
|
|
5225
|
-
source: inspection.source,
|
|
5652
|
+
const common = {
|
|
5226
5653
|
output: inspection.output,
|
|
5227
5654
|
palette: spec.quality.palette,
|
|
5228
5655
|
minGridConfidence: spec.quality.minGridConfidence,
|
|
@@ -5234,7 +5661,18 @@ async function refineQualityProfiles(specs, lock, options = {}) {
|
|
|
5234
5661
|
fixerCommand: options.fixerCommand,
|
|
5235
5662
|
fixerArgsPrefix: options.fixerArgsPrefix,
|
|
5236
5663
|
force: options.force
|
|
5237
|
-
}
|
|
5664
|
+
};
|
|
5665
|
+
if (spec.generator === "frames") {
|
|
5666
|
+
const frameSources = await qualitySource(spec, lock);
|
|
5667
|
+
if (!frameSources.items) throw new Error(frameSources.reason);
|
|
5668
|
+
await refineFrameSet({
|
|
5669
|
+
...common,
|
|
5670
|
+
sources: frameSources.items,
|
|
5671
|
+
fps: spec.quality.fps ?? 12
|
|
5672
|
+
});
|
|
5673
|
+
} else {
|
|
5674
|
+
await refineAsset({ ...common, source: inspection.source });
|
|
5675
|
+
}
|
|
5238
5676
|
result.processed++;
|
|
5239
5677
|
result.items.push(await inspectQualityProfile(spec, lock));
|
|
5240
5678
|
} catch (error) {
|
|
@@ -5248,7 +5686,7 @@ async function refineQualityProfiles(specs, lock, options = {}) {
|
|
|
5248
5686
|
}
|
|
5249
5687
|
return result;
|
|
5250
5688
|
}
|
|
5251
|
-
async function requireApprovedQualitySources(specs, lock, assetIds) {
|
|
5689
|
+
async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles) {
|
|
5252
5690
|
const configured = specs.filter(
|
|
5253
5691
|
(spec) => spec.quality && (!assetIds || assetIds.has(spec.assetId))
|
|
5254
5692
|
);
|
|
@@ -5257,8 +5695,33 @@ async function requireApprovedQualitySources(specs, lock, assetIds) {
|
|
|
5257
5695
|
const rejected = [];
|
|
5258
5696
|
for (const spec of configured) {
|
|
5259
5697
|
const inspection = await inspectQualityProfile(spec, lock);
|
|
5260
|
-
if (inspection.state === "approved")
|
|
5261
|
-
|
|
5698
|
+
if (inspection.state === "approved") {
|
|
5699
|
+
if (spec.generator === "frames") {
|
|
5700
|
+
const check = await checkQualityRecord(inspection.record);
|
|
5701
|
+
const roles = check.options.frameSet.roles;
|
|
5702
|
+
if (outputRoles) {
|
|
5703
|
+
const selectedRole = outputRoles[spec.assetId];
|
|
5704
|
+
if (!selectedRole) {
|
|
5705
|
+
throw new Error(
|
|
5706
|
+
`Mounting quality-approved frame set ${inspection.key} requires asset.outputRole.`
|
|
5707
|
+
);
|
|
5708
|
+
}
|
|
5709
|
+
const index = roles.indexOf(selectedRole);
|
|
5710
|
+
if (index < 0) {
|
|
5711
|
+
throw new Error(
|
|
5712
|
+
`Frame set ${inspection.key} has no approved output role "${selectedRole}".`
|
|
5713
|
+
);
|
|
5714
|
+
}
|
|
5715
|
+
sources[spec.assetId] = check.outputs[index];
|
|
5716
|
+
} else {
|
|
5717
|
+
for (const [index, output] of check.outputs.entries()) {
|
|
5718
|
+
sources[`${spec.assetId}/${roles[index]}`] = output;
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
} else {
|
|
5722
|
+
sources[spec.assetId] = inspection.output;
|
|
5723
|
+
}
|
|
5724
|
+
} else rejected.push(inspection);
|
|
5262
5725
|
}
|
|
5263
5726
|
if (rejected.length) {
|
|
5264
5727
|
const detail = rejected.slice(0, 5).map((item) => `${item.key}: ${item.state} (${item.reason})`).join("; ");
|
|
@@ -5611,7 +6074,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
5611
6074
|
jobId,
|
|
5612
6075
|
// A multi-candidate generator routes through review; record the parent
|
|
5613
6076
|
// so `pick` knows where to look.
|
|
5614
|
-
reviewObjectId: estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
6077
|
+
reviewObjectId: spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
5615
6078
|
status: "processing",
|
|
5616
6079
|
providerMetadata: metadata ? { ...lock.entries[key].providerMetadata, [provider.id]: metadata } : lock.entries[key].providerMetadata
|
|
5617
6080
|
});
|
|
@@ -5659,7 +6122,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
5659
6122
|
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
5660
6123
|
spec: currentSpec
|
|
5661
6124
|
});
|
|
5662
|
-
if (state.status === "review") {
|
|
6125
|
+
if (state.status === "review" || state.status === "review-set") {
|
|
5663
6126
|
upsert(lock, key, {
|
|
5664
6127
|
status: "review",
|
|
5665
6128
|
reviewObjectId: entry.jobId,
|
|
@@ -5672,7 +6135,9 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
5672
6135
|
} : entry.providerMetadata
|
|
5673
6136
|
});
|
|
5674
6137
|
result.review++;
|
|
5675
|
-
log2(
|
|
6138
|
+
log2(
|
|
6139
|
+
state.status === "review-set" ? ` review ${key} (${state.frameUrls.length} ordered frames)` : ` review ${key} (${state.candidateUrls.length} candidates)`
|
|
6140
|
+
);
|
|
5676
6141
|
} else if (state.status === "ready") {
|
|
5677
6142
|
const sourceUrls = state.sources?.length ? state.sources : state.sourceUrl ? [{ url: state.sourceUrl }] : [];
|
|
5678
6143
|
upsert(lock, key, {
|
|
@@ -6431,12 +6896,17 @@ function renderSheet(groups) {
|
|
|
6431
6896
|
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%);
|
|
6432
6897
|
background-size:12px 12px; background-position:0 0,6px 6px; }
|
|
6433
6898
|
.frames { display:flex; flex-wrap:wrap; gap:10px; max-width:100%; overflow-x:auto; }
|
|
6899
|
+
.loop { display:flex; align-items:flex-start; gap:12px; margin-bottom:12px; }
|
|
6900
|
+
.loop img { image-rendering:pixelated; border:2px solid var(--accent); padding:7px;
|
|
6901
|
+
background:var(--panel-deep); max-width:min(var(--preview),calc(100vw - 62px)); height:auto; }
|
|
6902
|
+
.loop-copy { color:var(--dim); font-size:11px; max-width:32ch; }
|
|
6434
6903
|
.cand { border:2px solid var(--line-strong); border-radius:0; padding:7px; background:var(--panel-deep);
|
|
6435
6904
|
cursor:pointer; display:flex; flex:0 0 auto; flex-direction:column; align-items:center; gap:5px;
|
|
6436
6905
|
position:relative; max-width:100%; }
|
|
6437
6906
|
.cand:hover { border-color:var(--dim); }
|
|
6438
6907
|
.cand.active { border-color:var(--dim); box-shadow:0 0 0 2px color-mix(in srgb, var(--dim) 20%, transparent); }
|
|
6439
6908
|
.cand.sel { border-color:var(--ok); box-shadow:0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }
|
|
6909
|
+
.cand.set-frame { cursor:pointer; }
|
|
6440
6910
|
.cand img { image-rendering:pixelated; display:block;
|
|
6441
6911
|
background-image:
|
|
6442
6912
|
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
|
|
@@ -6474,7 +6944,8 @@ function renderSheet(groups) {
|
|
|
6474
6944
|
</header>
|
|
6475
6945
|
<main id="root"></main>
|
|
6476
6946
|
<footer>
|
|
6477
|
-
Click a candidate to choose it
|
|
6947
|
+
Click a candidate to choose it, or any ordered frame to accept its whole set;
|
|
6948
|
+
click again to undo. In the focused row,
|
|
6478
6949
|
<kbd>\u2190</kbd>/<kbd>\u2192</kbd> browses every candidate and <kbd>Enter</kbd> chooses;
|
|
6479
6950
|
<kbd>1</kbd>\u2013<kbd>9</kbd> picks directly, <kbd>0</kbd> skips, and
|
|
6480
6951
|
<kbd>\u2191</kbd>/<kbd>\u2193</kbd> changes rows. Unchosen rows stay in review \u2014 nothing is
|
|
@@ -6522,12 +6993,34 @@ GROUPS.forEach((g, gi) => {
|
|
|
6522
6993
|
context.append(label, source);
|
|
6523
6994
|
review.insertBefore(context, frames);
|
|
6524
6995
|
}
|
|
6996
|
+
if (g.mode === 'frame-set') {
|
|
6997
|
+
const loop = document.createElement('div');
|
|
6998
|
+
loop.className = 'loop';
|
|
6999
|
+
const preview = document.createElement('img');
|
|
7000
|
+
preview.src = g.frameUrls[0];
|
|
7001
|
+
preview.width = displayWidth;
|
|
7002
|
+
preview.height = displayHeight;
|
|
7003
|
+
preview.alt = 'Animated frame-set preview';
|
|
7004
|
+
const copy = document.createElement('div');
|
|
7005
|
+
copy.className = 'loop-copy';
|
|
7006
|
+
copy.textContent = g.frameUrls.length + ' ordered frames \xB7 ' + (g.fps || 12) +
|
|
7007
|
+
' fps. Accepting keeps every frame; one bad frame means leave the set unchosen.';
|
|
7008
|
+
loop.append(preview, copy);
|
|
7009
|
+
review.insertBefore(loop, frames);
|
|
7010
|
+
let loopIndex = 0;
|
|
7011
|
+
setInterval(() => {
|
|
7012
|
+
loopIndex = (loopIndex + 1) % g.frameUrls.length;
|
|
7013
|
+
preview.src = g.frameUrls[loopIndex];
|
|
7014
|
+
}, Math.max(16, Math.round(1000 / (g.fps || 12))));
|
|
7015
|
+
}
|
|
6525
7016
|
g.frameUrls.forEach((url, i) => {
|
|
6526
7017
|
const c = document.createElement('button');
|
|
6527
7018
|
c.type = 'button';
|
|
6528
|
-
c.className = 'cand';
|
|
7019
|
+
c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '');
|
|
6529
7020
|
c.tabIndex = -1;
|
|
6530
|
-
c.setAttribute('aria-label',
|
|
7021
|
+
c.setAttribute('aria-label', g.mode === 'frame-set'
|
|
7022
|
+
? 'Accept ordered frame set from frame ' + (i + 1)
|
|
7023
|
+
: 'Choose candidate ' + (i + 1) + ' of ' + g.frameUrls.length);
|
|
6531
7024
|
|
|
6532
7025
|
const preview = document.createElement('img');
|
|
6533
7026
|
preview.className = 'preview';
|
|
@@ -6537,7 +7030,8 @@ GROUPS.forEach((g, gi) => {
|
|
|
6537
7030
|
preview.loading = 'lazy';
|
|
6538
7031
|
const index = document.createElement('span');
|
|
6539
7032
|
index.className = 'idx';
|
|
6540
|
-
index.textContent = String(i + 1) + ' \xB7 ' +
|
|
7033
|
+
index.textContent = (g.frameLabels?.[i] || String(i + 1)) + ' \xB7 ' +
|
|
7034
|
+
g.width + '\xD7' + g.height + ' \xB7 ' + scaleLabel;
|
|
6541
7035
|
c.append(preview, index);
|
|
6542
7036
|
if (displayScale > 1 && largestSide <= 96) {
|
|
6543
7037
|
const actual = document.createElement('img');
|
|
@@ -6578,6 +7072,14 @@ function activate(i, frames) {
|
|
|
6578
7072
|
}
|
|
6579
7073
|
|
|
6580
7074
|
function choose(gi, i, node, frames, groupEl) {
|
|
7075
|
+
if (GROUPS[gi].mode === 'frame-set') {
|
|
7076
|
+
const selected = picks.has(gi);
|
|
7077
|
+
frames.querySelectorAll('.cand').forEach(n => n.classList.toggle('sel', !selected));
|
|
7078
|
+
if (selected) { picks.delete(gi); groupEl.classList.remove('done'); }
|
|
7079
|
+
else { picks.set(gi, 0); groupEl.classList.add('done'); }
|
|
7080
|
+
refresh();
|
|
7081
|
+
return;
|
|
7082
|
+
}
|
|
6581
7083
|
const current = picks.get(gi);
|
|
6582
7084
|
frames.querySelectorAll('.cand').forEach(n => n.classList.remove('sel'));
|
|
6583
7085
|
if (current === i) { picks.delete(gi); groupEl.classList.remove('done'); }
|
|
@@ -6754,7 +7256,8 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
6754
7256
|
try {
|
|
6755
7257
|
const spec = specByKey.get(key);
|
|
6756
7258
|
const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec });
|
|
6757
|
-
if (state.status !== "review" || !state.candidateUrls.length) continue;
|
|
7259
|
+
if (state.status !== "review" && state.status !== "review-set" || (state.status === "review" ? !state.candidateUrls.length : !state.frameUrls.length)) continue;
|
|
7260
|
+
const frameUrls = state.status === "review" ? state.candidateUrls : state.frameUrls;
|
|
6758
7261
|
const sourceRoute = spec?.revision ? `/revision-source/${encodeURIComponent(String(groups.length))}` : null;
|
|
6759
7262
|
if (sourceRoute && spec?.revision) {
|
|
6760
7263
|
reviewAssets.set(sourceRoute, {
|
|
@@ -6768,9 +7271,14 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
6768
7271
|
styleId: entry.styleId,
|
|
6769
7272
|
prompt: entry.prompt,
|
|
6770
7273
|
reviewObjectId: entry.reviewObjectId,
|
|
6771
|
-
frameUrls
|
|
7274
|
+
frameUrls,
|
|
6772
7275
|
width: entry.width,
|
|
6773
7276
|
height: entry.height,
|
|
7277
|
+
...state.status === "review-set" ? {
|
|
7278
|
+
mode: "frame-set",
|
|
7279
|
+
frameLabels: state.sources.map((source, index) => source.role ?? `frame-${index}`),
|
|
7280
|
+
fps: state.fps
|
|
7281
|
+
} : { mode: "candidates" },
|
|
6774
7282
|
...sourceRoute && spec?.revision && spec.revision.sourceWidth && spec.revision.sourceHeight ? {
|
|
6775
7283
|
revision: {
|
|
6776
7284
|
mode: spec.revision.mode,
|
|
@@ -6813,6 +7321,31 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
6813
7321
|
const entry = lock.entries[key];
|
|
6814
7322
|
if (!group || !entry?.reviewObjectId) continue;
|
|
6815
7323
|
if (!Number.isInteger(index) || index < 0 || index >= group.frameUrls.length) continue;
|
|
7324
|
+
if (group.mode === "frame-set") {
|
|
7325
|
+
const spec = specByKey.get(key);
|
|
7326
|
+
const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec });
|
|
7327
|
+
if (state.status !== "review-set") {
|
|
7328
|
+
throw new Error(`Frame set ${key} is no longer ready for review`);
|
|
7329
|
+
}
|
|
7330
|
+
upsert(lock, key, {
|
|
7331
|
+
status: "selected",
|
|
7332
|
+
objectId: state.objectId,
|
|
7333
|
+
candidateIndex: null,
|
|
7334
|
+
sourceUrl: state.sources[0]?.url ?? null,
|
|
7335
|
+
sourceUrls: state.sources,
|
|
7336
|
+
provider: provider.id,
|
|
7337
|
+
providerMetadata: state.metadata ? {
|
|
7338
|
+
...entry.providerMetadata,
|
|
7339
|
+
[provider.id]: {
|
|
7340
|
+
...entry.providerMetadata[provider.id],
|
|
7341
|
+
...state.metadata
|
|
7342
|
+
}
|
|
7343
|
+
} : entry.providerMetadata
|
|
7344
|
+
});
|
|
7345
|
+
selected++;
|
|
7346
|
+
log2(` accepted ${key} \u2192 ${state.sources.length} ordered frames`);
|
|
7347
|
+
continue;
|
|
7348
|
+
}
|
|
6816
7349
|
const { objectId, sourceUrl, metadata } = await requireSelectCandidate(provider)(
|
|
6817
7350
|
entry.reviewObjectId,
|
|
6818
7351
|
index,
|
|
@@ -9522,9 +10055,15 @@ async function main() {
|
|
|
9522
10055
|
if (!args.reviewer?.trim()) {
|
|
9523
10056
|
throw new Error("refine approve needs --reviewer <name>.");
|
|
9524
10057
|
}
|
|
9525
|
-
|
|
10058
|
+
const pending = await checkQualityRecord(args.from);
|
|
10059
|
+
log(
|
|
10060
|
+
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.`
|
|
10061
|
+
);
|
|
9526
10062
|
log(` Confirm crisp edges, readable forms, palette separation, alpha, and seams.`);
|
|
9527
|
-
if (!await confirm(
|
|
10063
|
+
if (!await confirm(
|
|
10064
|
+
pending.options.frameSet ? " Record this complete frame set as human-approved?" : " Record this asset as human-approved?",
|
|
10065
|
+
args.yes
|
|
10066
|
+
)) {
|
|
9528
10067
|
log(" approval not recorded");
|
|
9529
10068
|
return;
|
|
9530
10069
|
}
|
|
@@ -9533,7 +10072,11 @@ async function main() {
|
|
|
9533
10072
|
note: args.note
|
|
9534
10073
|
});
|
|
9535
10074
|
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
9536
|
-
else
|
|
10075
|
+
else {
|
|
10076
|
+
log(
|
|
10077
|
+
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()}`
|
|
10078
|
+
);
|
|
10079
|
+
}
|
|
9537
10080
|
return;
|
|
9538
10081
|
}
|
|
9539
10082
|
const result = await checkQualityRecord(args.from);
|
|
@@ -10046,7 +10589,10 @@ async function main() {
|
|
|
10046
10589
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
10047
10590
|
];
|
|
10048
10591
|
const qualityRecords = qualitySources ? await Promise.all(
|
|
10049
|
-
|
|
10592
|
+
packagingSpecs.filter((spec) => spec.quality).map((spec) => provenanceFile(
|
|
10593
|
+
`$quality/${spec.assetId}`,
|
|
10594
|
+
qualityRecordPath(spec.quality.outFile)
|
|
10595
|
+
))
|
|
10050
10596
|
) : [];
|
|
10051
10597
|
await writeManagedArtifactBundle(`${base}.pixelkiln.json`, outputs, {
|
|
10052
10598
|
kind: "pack",
|
|
@@ -10098,7 +10644,8 @@ async function main() {
|
|
|
10098
10644
|
const qualitySources = await requireApprovedQualitySources(
|
|
10099
10645
|
packagingSpecs,
|
|
10100
10646
|
lock,
|
|
10101
|
-
new Set(Object.keys(cells))
|
|
10647
|
+
new Set(Object.keys(cells)),
|
|
10648
|
+
outputRoles
|
|
10102
10649
|
);
|
|
10103
10650
|
if (qualitySources) Object.assign(sources, qualitySources);
|
|
10104
10651
|
const { png, atlas, skipped, overBase, sources: artifactSources } = mountStyle(
|
|
@@ -10118,7 +10665,10 @@ async function main() {
|
|
|
10118
10665
|
{ path: metadata, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
10119
10666
|
];
|
|
10120
10667
|
const qualityRecords = qualitySources ? await Promise.all(
|
|
10121
|
-
Object.
|
|
10668
|
+
packagingSpecs.filter((spec) => spec.quality && Object.hasOwn(cells, spec.assetId)).map((spec) => provenanceFile(
|
|
10669
|
+
`$quality/${spec.assetId}`,
|
|
10670
|
+
qualityRecordPath(spec.quality.outFile)
|
|
10671
|
+
))
|
|
10122
10672
|
) : [];
|
|
10123
10673
|
await writeManagedArtifactBundle(companion, outputs, {
|
|
10124
10674
|
kind: "mount",
|