pixelkiln 0.17.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 +7 -7
- package/dist/cli.js +1238 -548
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1225 -544
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +228 -53
- package/dist/index.d.ts +228 -53
- package/dist/index.js +1225 -546
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +1 -1
- package/docs/ARCHITECTURE.md +25 -10
- package/docs/ARTIFACTS.md +9 -4
- package/docs/CLI.md +12 -8
- package/docs/COMFYUI.md +155 -10
- package/docs/GENERATORS.md +16 -0
- package/docs/GETTING_STARTED.md +2 -1
- package/docs/LIBRARY.md +25 -4
- package/docs/MANIFEST.md +62 -12
- package/docs/QUALITY.md +11 -3
- package/docs/README.md +2 -2
- package/package.json +1 -1
- package/schema/manifest.schema.json +42 -1
- package/schema/recipe.schema.json +7 -1
- package/skills/pixelkiln/SKILL.md +2 -0
- package/skills/pixelkiln/references/comfyui.md +26 -4
- package/skills/pixelkiln/references/quality.md +7 -2
package/dist/cli.js
CHANGED
|
@@ -123,13 +123,14 @@ async function sha256File(path25) {
|
|
|
123
123
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
|
-
function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions) {
|
|
126
|
+
function specHash(spec, styleImageHashes, providerOptionIdentity = spec.providerOptions, providerInputIdentity = spec.providerInputs ?? {}) {
|
|
127
127
|
return sha256(
|
|
128
128
|
JSON.stringify({
|
|
129
129
|
// Preserve every existing PixelLab hash while making a provider switch
|
|
130
130
|
// invalidate the spec. Older manifests implicitly mean pixellab.
|
|
131
131
|
provider: spec.provider === "pixellab" ? void 0 : spec.provider,
|
|
132
132
|
providerOptions: providerOptionIdentity && (typeof providerOptionIdentity !== "object" || Object.keys(providerOptionIdentity).length > 0) ? providerOptionIdentity : void 0,
|
|
133
|
+
providerInputs: providerInputIdentity && (typeof providerInputIdentity !== "object" || Object.keys(providerInputIdentity).length > 0) ? providerInputIdentity : void 0,
|
|
133
134
|
generator: spec.generator,
|
|
134
135
|
prompt: spec.prompt,
|
|
135
136
|
width: spec.width,
|
|
@@ -604,6 +605,16 @@ function cacheFileName(hash, mediaType = MediaType.PNG) {
|
|
|
604
605
|
|
|
605
606
|
// src/providers/comfyui.ts
|
|
606
607
|
var DEFAULT_BASE_URL = "http://127.0.0.1:8188";
|
|
608
|
+
var BUILTIN_BINDINGS = /* @__PURE__ */ new Set([
|
|
609
|
+
"prompt",
|
|
610
|
+
"width",
|
|
611
|
+
"height",
|
|
612
|
+
"batchSize",
|
|
613
|
+
"seed",
|
|
614
|
+
"sourceImage",
|
|
615
|
+
"maskImage",
|
|
616
|
+
"strength"
|
|
617
|
+
]);
|
|
607
618
|
var ComfyUIClient = class {
|
|
608
619
|
constructor(baseUrl = process.env.COMFYUI_BASE_URL ?? DEFAULT_BASE_URL, request = fetch) {
|
|
609
620
|
this.request = request;
|
|
@@ -748,23 +759,105 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
748
759
|
identity: {
|
|
749
760
|
outputNodeId: options.outputNodeId,
|
|
750
761
|
numImages: options.numImages,
|
|
762
|
+
...options.frames ? { frames: options.frames } : {},
|
|
751
763
|
bindings: options.bindings,
|
|
752
764
|
workflowSha256
|
|
753
765
|
}
|
|
754
766
|
};
|
|
755
767
|
}
|
|
768
|
+
async resolveInputs(value, context) {
|
|
769
|
+
const options = parseOptions(context.providerOptions);
|
|
770
|
+
if (!options.workflow || !options.workflowSha256) {
|
|
771
|
+
throw new Error("ComfyUI provider inputs require resolved workflow options");
|
|
772
|
+
}
|
|
773
|
+
const inputs = {};
|
|
774
|
+
const identity = {};
|
|
775
|
+
for (const [name, input] of Object.entries(value)) {
|
|
776
|
+
if (BUILTIN_BINDINGS.has(name)) {
|
|
777
|
+
throw new Error(
|
|
778
|
+
`ComfyUI providerInputs.${name} is reserved; use PixelKiln's built-in ${name} field`
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
const binding = Object.hasOwn(options.bindings, name) ? options.bindings[name] : void 0;
|
|
782
|
+
if (!binding) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
`ComfyUI providerInputs.${name} has no matching bindings.${name} target`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
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
|
+
};
|
|
809
|
+
}
|
|
810
|
+
const current = options.workflow[binding.nodeId].inputs[binding.input];
|
|
811
|
+
if (typeof current !== "string" && typeof current !== "number" && typeof current !== "boolean") {
|
|
812
|
+
throw new Error(
|
|
813
|
+
`ComfyUI ${label} cannot replace non-scalar ${binding.nodeId}.${binding.input}`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
if (typeof current !== typeof item) {
|
|
817
|
+
throw new Error(
|
|
818
|
+
`ComfyUI ${label} must be ${typeof current} to match ${binding.nodeId}.${binding.input}`
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
return { runtime: item, stable: item };
|
|
822
|
+
};
|
|
823
|
+
if (Array.isArray(input)) {
|
|
824
|
+
if (!options.frames || name !== options.frames.vary) {
|
|
825
|
+
throw new Error(
|
|
826
|
+
`ComfyUI providerInputs.${name} is an array but frames.vary is ${options.frames ? `"${options.frames.vary}"` : "not configured"}`
|
|
827
|
+
);
|
|
828
|
+
}
|
|
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;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return { inputs, identity };
|
|
839
|
+
}
|
|
756
840
|
supports(generator) {
|
|
757
|
-
return generator === "map";
|
|
841
|
+
return generator === "map" || generator === "frames";
|
|
758
842
|
}
|
|
759
843
|
supportsRevision(_mode) {
|
|
760
844
|
return true;
|
|
761
845
|
}
|
|
762
846
|
estimate(spec) {
|
|
763
|
-
return {
|
|
847
|
+
return {
|
|
848
|
+
unit: "free",
|
|
849
|
+
amount: 0,
|
|
850
|
+
candidates: spec.generator === "frames" ? 1 : resolvedOptions(spec).numImages
|
|
851
|
+
};
|
|
764
852
|
}
|
|
765
853
|
validate(spec, styleImages) {
|
|
766
854
|
const options = resolvedOptions(spec);
|
|
767
|
-
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
|
+
}
|
|
768
861
|
if (spec.width < 16 || spec.height < 16 || spec.width > 4096 || spec.height > 4096) {
|
|
769
862
|
throw new Error("ComfyUI output dimensions must be between 16 and 4096 pixels");
|
|
770
863
|
}
|
|
@@ -786,6 +879,25 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
786
879
|
if (options.numImages > 1 && !options.bindings.batchSize) {
|
|
787
880
|
throw new Error("ComfyUI numImages greater than 1 requires bindings.batchSize");
|
|
788
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
|
+
}
|
|
789
901
|
if (spec.revision) {
|
|
790
902
|
if (!options.bindings.sourceImage) {
|
|
791
903
|
throw new Error("ComfyUI revisions require bindings.sourceImage");
|
|
@@ -810,16 +922,64 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
810
922
|
async submit(spec, styleImages) {
|
|
811
923
|
this.validate(spec, styleImages);
|
|
812
924
|
const options = resolvedOptions(spec);
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
925
|
+
const uploadedImages = /* @__PURE__ */ new Map();
|
|
926
|
+
const bindInput = async (workflow2, name, input) => {
|
|
927
|
+
const binding = Object.hasOwn(options.bindings, name) ? options.bindings[name] : void 0;
|
|
928
|
+
if (!binding) throw new Error(`ComfyUI provider input "${name}" has no binding`);
|
|
929
|
+
if (!isComfyImageInput(input)) {
|
|
930
|
+
setBinding(workflow2, binding, input);
|
|
931
|
+
return;
|
|
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
|
+
};
|
|
822
981
|
}
|
|
982
|
+
const workflow = await buildWorkflow();
|
|
823
983
|
if (spec.revision) {
|
|
824
984
|
if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
|
|
825
985
|
throw new Error("ComfyUI revision source is not ready");
|
|
@@ -854,21 +1014,63 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
854
1014
|
}
|
|
855
1015
|
}
|
|
856
1016
|
const promptId = await this.client.submit(workflow);
|
|
857
|
-
|
|
858
|
-
|
|
1017
|
+
const metadata = {
|
|
1018
|
+
...Object.keys(inputs).length ? { inputs } : {},
|
|
859
1019
|
...spec.revision ? {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {}
|
|
866
|
-
}
|
|
1020
|
+
revision: {
|
|
1021
|
+
mode: spec.revision.mode,
|
|
1022
|
+
sourceAssetId: spec.revision.sourceAssetId,
|
|
1023
|
+
sourceSha256: spec.revision.sourceSha256,
|
|
1024
|
+
...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {}
|
|
867
1025
|
}
|
|
868
1026
|
} : {}
|
|
869
1027
|
};
|
|
1028
|
+
return {
|
|
1029
|
+
jobId: encodeJob(promptId, options.outputNodeId),
|
|
1030
|
+
...Object.keys(metadata).length ? { metadata } : {}
|
|
1031
|
+
};
|
|
870
1032
|
}
|
|
871
|
-
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
|
+
}
|
|
872
1074
|
const { promptId, outputNodeId } = decodeJob(jobId);
|
|
873
1075
|
const history = await this.client.history(promptId);
|
|
874
1076
|
const entry = historyEntry(history, promptId);
|
|
@@ -931,7 +1133,15 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
931
1133
|
}
|
|
932
1134
|
};
|
|
933
1135
|
function parseOptions(value) {
|
|
934
|
-
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
|
+
]);
|
|
935
1145
|
const extra = Object.keys(value).filter((key) => !allowed.has(key));
|
|
936
1146
|
if (extra.length) throw new Error(`Unknown ComfyUI option(s): ${extra.join(", ")}`);
|
|
937
1147
|
const workflowFile = requiredString(value.workflowFile, "workflowFile");
|
|
@@ -941,28 +1151,19 @@ function parseOptions(value) {
|
|
|
941
1151
|
throw new Error("ComfyUI numImages must be a whole number from 1 to 16");
|
|
942
1152
|
}
|
|
943
1153
|
if (!isObject(value.bindings)) throw new Error("ComfyUI bindings must be an object");
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
prompt: parseBinding(value.bindings.prompt, "prompt"),
|
|
958
|
-
...value.bindings.width == null ? {} : { width: parseBinding(value.bindings.width, "width") },
|
|
959
|
-
...value.bindings.height == null ? {} : { height: parseBinding(value.bindings.height, "height") },
|
|
960
|
-
...value.bindings.batchSize == null ? {} : { batchSize: parseBinding(value.bindings.batchSize, "batchSize") },
|
|
961
|
-
...value.bindings.seed == null ? {} : { seed: parseBinding(value.bindings.seed, "seed") },
|
|
962
|
-
...value.bindings.sourceImage == null ? {} : { sourceImage: parseBinding(value.bindings.sourceImage, "sourceImage") },
|
|
963
|
-
...value.bindings.maskImage == null ? {} : { maskImage: parseBinding(value.bindings.maskImage, "maskImage") },
|
|
964
|
-
...value.bindings.strength == null ? {} : { strength: parseBinding(value.bindings.strength, "strength") }
|
|
965
|
-
};
|
|
1154
|
+
const bindings = Object.fromEntries(
|
|
1155
|
+
Object.entries(value.bindings).map(([name, binding]) => [name, parseBinding(binding, name)])
|
|
1156
|
+
);
|
|
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
|
+
}
|
|
966
1167
|
const workflow = value.workflow == null ? void 0 : parseWorkflow(value.workflow, workflowFile);
|
|
967
1168
|
const workflowSha256 = value.workflowSha256 == null ? void 0 : requiredString(value.workflowSha256, "workflowSha256");
|
|
968
1169
|
return {
|
|
@@ -970,10 +1171,22 @@ function parseOptions(value) {
|
|
|
970
1171
|
outputNodeId,
|
|
971
1172
|
numImages: Number(numImages),
|
|
972
1173
|
bindings,
|
|
1174
|
+
...frames ? { frames } : {},
|
|
973
1175
|
...workflow ? { workflow } : {},
|
|
974
1176
|
...workflowSha256 ? { workflowSha256 } : {}
|
|
975
1177
|
};
|
|
976
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
|
+
}
|
|
977
1190
|
function resolvedOptions(spec) {
|
|
978
1191
|
const options = parseOptions(spec.providerOptions);
|
|
979
1192
|
if (!options.workflow || !options.workflowSha256) {
|
|
@@ -1002,6 +1215,7 @@ function parseWorkflow(value, label) {
|
|
|
1002
1215
|
return value;
|
|
1003
1216
|
}
|
|
1004
1217
|
function validateWorkflowBindings(workflow, options) {
|
|
1218
|
+
const targets = /* @__PURE__ */ new Map();
|
|
1005
1219
|
for (const [name, binding] of Object.entries(options.bindings)) {
|
|
1006
1220
|
if (!binding) continue;
|
|
1007
1221
|
const node = workflow[binding.nodeId];
|
|
@@ -1011,6 +1225,12 @@ function validateWorkflowBindings(workflow, options) {
|
|
|
1011
1225
|
`ComfyUI ${name} binding refers to missing input "${binding.input}" on node "${binding.nodeId}"`
|
|
1012
1226
|
);
|
|
1013
1227
|
}
|
|
1228
|
+
const target = `${binding.nodeId}.${binding.input}`;
|
|
1229
|
+
const prior = targets.get(target);
|
|
1230
|
+
if (prior) {
|
|
1231
|
+
throw new Error(`ComfyUI bindings.${name} and bindings.${prior} target the same input ${target}`);
|
|
1232
|
+
}
|
|
1233
|
+
targets.set(target, name);
|
|
1014
1234
|
}
|
|
1015
1235
|
if (!workflow[options.outputNodeId]) {
|
|
1016
1236
|
throw new Error(`ComfyUI outputNodeId refers to missing node "${options.outputNodeId}"`);
|
|
@@ -1048,6 +1268,7 @@ function outputImages(entry, outputNodeId) {
|
|
|
1048
1268
|
}
|
|
1049
1269
|
function comfyMetadata(spec, promptId, outputNodeId, images) {
|
|
1050
1270
|
const options = resolvedOptions(spec);
|
|
1271
|
+
const inputs = providerInputProvenance(spec.providerInputs);
|
|
1051
1272
|
return {
|
|
1052
1273
|
promptId,
|
|
1053
1274
|
outputNodeId,
|
|
@@ -1055,6 +1276,7 @@ function comfyMetadata(spec, promptId, outputNodeId, images) {
|
|
|
1055
1276
|
workflowFile: options.workflowFile,
|
|
1056
1277
|
workflowSha256: options.workflowSha256,
|
|
1057
1278
|
files: images.map((image) => ({ ...image })),
|
|
1279
|
+
...Object.keys(inputs).length ? { inputs } : {},
|
|
1058
1280
|
...spec.revision ? {
|
|
1059
1281
|
revision: {
|
|
1060
1282
|
mode: spec.revision.mode,
|
|
@@ -1066,9 +1288,83 @@ function comfyMetadata(spec, promptId, outputNodeId, images) {
|
|
|
1066
1288
|
} : {}
|
|
1067
1289
|
};
|
|
1068
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
|
+
}
|
|
1308
|
+
function isImageBinding(workflow, binding) {
|
|
1309
|
+
const node = workflow[binding.nodeId];
|
|
1310
|
+
return binding.input === "image" && (node?.class_type === "LoadImage" || node?.class_type === "LoadImageMask");
|
|
1311
|
+
}
|
|
1312
|
+
function isComfyImageInput(value) {
|
|
1313
|
+
return isObject(value) && value.kind === "image" && typeof value.path === "string" && typeof value.sha256 === "string" && (value.format === "png" || value.format === "jpeg");
|
|
1314
|
+
}
|
|
1315
|
+
function providerInputProvenance(inputs) {
|
|
1316
|
+
return Object.fromEntries(Object.entries(inputs ?? {}).map(([name, input]) => [
|
|
1317
|
+
name,
|
|
1318
|
+
Array.isArray(input) ? { kind: "sequence", values: input.map(providerInputValueProvenance) } : providerInputValueProvenance(input)
|
|
1319
|
+
]));
|
|
1320
|
+
}
|
|
1321
|
+
function providerInputValueProvenance(input) {
|
|
1322
|
+
return isComfyImageInput(input) ? { kind: "image", sha256: input.sha256, format: input.format } : { kind: "value", value: input };
|
|
1323
|
+
}
|
|
1324
|
+
function inputImageFormat(bytes) {
|
|
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";
|
|
1326
|
+
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) return null;
|
|
1327
|
+
let offset = 2;
|
|
1328
|
+
while (offset + 3 < bytes.length) {
|
|
1329
|
+
if (bytes[offset] !== 255) return null;
|
|
1330
|
+
while (bytes[offset] === 255) offset++;
|
|
1331
|
+
const marker = bytes[offset++];
|
|
1332
|
+
if (marker == null || marker === 217 || marker === 218) break;
|
|
1333
|
+
if (marker === 1 || marker >= 208 && marker <= 215) continue;
|
|
1334
|
+
if (offset + 2 > bytes.length) return null;
|
|
1335
|
+
const length = bytes.readUInt16BE(offset);
|
|
1336
|
+
if (length < 2 || offset + length > bytes.length) return null;
|
|
1337
|
+
const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
|
|
1338
|
+
if (isStartOfFrame) {
|
|
1339
|
+
if (length < 7) return null;
|
|
1340
|
+
return bytes.readUInt16BE(offset + 3) > 0 && bytes.readUInt16BE(offset + 5) > 0 ? "jpeg" : null;
|
|
1341
|
+
}
|
|
1342
|
+
offset += length;
|
|
1343
|
+
}
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1069
1346
|
function encodeJob(promptId, outputNodeId) {
|
|
1070
1347
|
return `${promptId}#${encodeURIComponent(outputNodeId)}`;
|
|
1071
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
|
+
}
|
|
1072
1368
|
function decodeJob(jobId) {
|
|
1073
1369
|
const split = jobId.lastIndexOf("#");
|
|
1074
1370
|
if (split < 1 || split === jobId.length - 1) throw new Error(`Invalid ComfyUI job id "${jobId}"`);
|
|
@@ -1502,7 +1798,7 @@ function clientFromEnv() {
|
|
|
1502
1798
|
// src/types.ts
|
|
1503
1799
|
import { z as z2 } from "zod";
|
|
1504
1800
|
var MediaTypeSchema = z2.enum(["image/png", "image/gif"]);
|
|
1505
|
-
var GeneratorSchema = z2.enum(["1dir", "map", "pixflux", "tiles", "animation"]);
|
|
1801
|
+
var GeneratorSchema = z2.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames"]);
|
|
1506
1802
|
var GridConfidenceSchema = z2.enum(["low", "medium", "high"]);
|
|
1507
1803
|
var RevisionModeSchema = z2.enum(["image-to-image", "inpaint", "outpaint"]);
|
|
1508
1804
|
function tileVariationCount(descriptions) {
|
|
@@ -1552,7 +1848,9 @@ var QualityProfileSchema = z2.object({
|
|
|
1552
1848
|
/** Pixel Art Fixer revision written into the quality record. */
|
|
1553
1849
|
fixerRevision: z2.string().min(1).optional(),
|
|
1554
1850
|
/** Python executable containing Pixel Art Fixer, relative to the manifest unless absolute. */
|
|
1555
|
-
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()
|
|
1556
1854
|
}).strict().refine(
|
|
1557
1855
|
(profile) => new Set(profile.palette.map((color) => color.replace(/^#/, "").toLowerCase())).size === profile.palette.length,
|
|
1558
1856
|
{ message: "quality palette colors must be unique", path: ["palette"] }
|
|
@@ -1768,7 +2066,18 @@ var AssetSchema = z2.object({
|
|
|
1768
2066
|
* exactly right for a colour style. Editing the shared prompt to suit one
|
|
1769
2067
|
* style would invalidate every other style's already-generated art.
|
|
1770
2068
|
*/
|
|
1771
|
-
promptByStyle: z2.record(z2.string()).default({})
|
|
2069
|
+
promptByStyle: z2.record(z2.string()).default({}),
|
|
2070
|
+
/**
|
|
2071
|
+
* Named per-asset values consumed by the active provider's declared
|
|
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.
|
|
2074
|
+
*/
|
|
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({})
|
|
1772
2081
|
}).strict().superRefine((asset, context) => {
|
|
1773
2082
|
if (asset.source && asset.revision) {
|
|
1774
2083
|
context.addIssue({
|
|
@@ -3078,113 +3387,390 @@ registerProvider({
|
|
|
3078
3387
|
});
|
|
3079
3388
|
|
|
3080
3389
|
// src/manifest.ts
|
|
3081
|
-
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";
|
|
3082
3399
|
import { existsSync as existsSync3 } from "fs";
|
|
3083
3400
|
import path4 from "path";
|
|
3084
|
-
async function
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
${
|
|
3092
|
-
|
|
3093
|
-
const styleIds = new Set(Object.keys(parsed.data.styles));
|
|
3094
|
-
const unknownReferences = [];
|
|
3095
|
-
for (const [assetId, asset] of Object.entries(parsed.data.assets)) {
|
|
3096
|
-
for (const styleId of asset.styles) {
|
|
3097
|
-
if (!styleIds.has(styleId)) unknownReferences.push(`assets.${assetId}.styles: unknown style "${styleId}"`);
|
|
3098
|
-
}
|
|
3099
|
-
for (const styleId of Object.keys(asset.promptByStyle)) {
|
|
3100
|
-
if (!styleIds.has(styleId)) {
|
|
3101
|
-
unknownReferences.push(`assets.${assetId}.promptByStyle: unknown style "${styleId}"`);
|
|
3102
|
-
}
|
|
3103
|
-
}
|
|
3104
|
-
if (asset.revision) {
|
|
3105
|
-
if (!parsed.data.assets[asset.revision.from]) {
|
|
3106
|
-
unknownReferences.push(
|
|
3107
|
-
`assets.${assetId}.revision.from: unknown asset "${asset.revision.from}"`
|
|
3108
|
-
);
|
|
3109
|
-
} else if (asset.revision.from === assetId) {
|
|
3110
|
-
unknownReferences.push(`assets.${assetId}.revision.from: an asset cannot revise itself`);
|
|
3111
|
-
}
|
|
3112
|
-
}
|
|
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
|
+
);
|
|
3113
3410
|
}
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
const parent = parsed.data.assets[assetId]?.revision?.from;
|
|
3127
|
-
if (parent && parent !== assetId && parsed.data.assets[parent]) {
|
|
3128
|
-
visitRevision(parent, [...chain, assetId]);
|
|
3129
|
-
}
|
|
3130
|
-
visiting.delete(assetId);
|
|
3131
|
-
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);
|
|
3132
3423
|
};
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
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();
|
|
3137
3434
|
}
|
|
3138
|
-
return { manifest: parsed.data, root: path4.dirname(abs), path: abs };
|
|
3139
3435
|
}
|
|
3140
|
-
async function
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
let provider = providers.get(id);
|
|
3147
|
-
if (!provider) {
|
|
3148
|
-
provider = createProvider(id, "offline");
|
|
3149
|
-
providers.set(id, provider);
|
|
3150
|
-
}
|
|
3151
|
-
return provider;
|
|
3152
|
-
};
|
|
3153
|
-
const specs = [];
|
|
3154
|
-
const styleIds = Object.keys(manifest.styles).filter(
|
|
3155
|
-
(id) => !filter?.styles?.length || filter.styles.includes(id)
|
|
3156
|
-
);
|
|
3157
|
-
for (const unknownStyle of filter?.styles ?? []) {
|
|
3158
|
-
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) {
|
|
3159
3442
|
throw new Error(
|
|
3160
|
-
`
|
|
3443
|
+
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
3161
3444
|
);
|
|
3162
3445
|
}
|
|
3163
3446
|
}
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
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;
|
|
3168
3455
|
}
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
for (const assetId of [...requestedAssetIds]) {
|
|
3172
|
-
let current = manifest.assets[assetId]?.revision?.from;
|
|
3173
|
-
while (current && !resolutionAssetIds.has(current)) {
|
|
3174
|
-
resolutionAssetIds.add(current);
|
|
3175
|
-
current = manifest.assets[current]?.revision?.from;
|
|
3176
|
-
}
|
|
3456
|
+
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
3457
|
+
if (!merged[key]) merged[key] = entry;
|
|
3177
3458
|
}
|
|
3178
|
-
const
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
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
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
if (Object.keys(current).length === 0) currentDirty.delete(key);
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
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) {
|
|
3188
3774
|
throw new Error(`Style image has invalid dimensions: ${abs}`);
|
|
3189
3775
|
}
|
|
3190
3776
|
hit = { base64: buf.toString("base64"), hash: sha256(buf), ...metadata };
|
|
@@ -3207,10 +3793,11 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3207
3793
|
styleImageDimensions.push(loadedImage);
|
|
3208
3794
|
}
|
|
3209
3795
|
const resolvedImages = style.styleImages.map((image) => {
|
|
3210
|
-
const hit = styleImageCache.get(
|
|
3796
|
+
const hit = styleImageCache.get(path6.resolve(root, image.path));
|
|
3211
3797
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
3212
3798
|
});
|
|
3213
3799
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
3800
|
+
const providerInputIdentities = /* @__PURE__ */ new Map();
|
|
3214
3801
|
for (const [assetId, asset] of Object.entries(manifest.assets)) {
|
|
3215
3802
|
if (!resolutionAssetIds.has(assetId)) continue;
|
|
3216
3803
|
if (asset.styles.length && !asset.styles.includes(styleId)) continue;
|
|
@@ -3218,6 +3805,21 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3218
3805
|
if (!activeProvider.supports(generator)) {
|
|
3219
3806
|
throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
|
|
3220
3807
|
}
|
|
3808
|
+
if (Object.keys(asset.providerInputs).length && !activeProvider.resolveInputs) {
|
|
3809
|
+
throw new Error(
|
|
3810
|
+
`Provider "${activeProvider.id}" does not support asset providerInputs (${styleId}/${assetId})`
|
|
3811
|
+
);
|
|
3812
|
+
}
|
|
3813
|
+
const inputResolution = activeProvider.resolveInputs ? await activeProvider.resolveInputs(asset.providerInputs, {
|
|
3814
|
+
root,
|
|
3815
|
+
styleId,
|
|
3816
|
+
assetId,
|
|
3817
|
+
generator,
|
|
3818
|
+
providerOptions
|
|
3819
|
+
}) : { inputs: {} };
|
|
3820
|
+
const providerInputs = inputResolution.inputs;
|
|
3821
|
+
const providerInputIdentity = inputResolution.identity ?? providerInputs;
|
|
3822
|
+
providerInputIdentities.set(assetId, providerInputIdentity);
|
|
3221
3823
|
let width;
|
|
3222
3824
|
let height;
|
|
3223
3825
|
let size;
|
|
@@ -3236,9 +3838,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3236
3838
|
}
|
|
3237
3839
|
const subject = asset.promptByStyle[styleId] ?? asset.prompt;
|
|
3238
3840
|
const prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
|
|
3239
|
-
const relFile = asset.file ??
|
|
3240
|
-
const outFile =
|
|
3241
|
-
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;
|
|
3242
3844
|
const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
|
|
3243
3845
|
const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
|
|
3244
3846
|
const base = {
|
|
@@ -3246,6 +3848,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3246
3848
|
assetId,
|
|
3247
3849
|
provider: activeProvider.id,
|
|
3248
3850
|
providerOptions,
|
|
3851
|
+
providerInputs,
|
|
3249
3852
|
generator,
|
|
3250
3853
|
prompt,
|
|
3251
3854
|
width,
|
|
@@ -3288,7 +3891,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3288
3891
|
minGridConfidence: style.quality.minGridConfidence,
|
|
3289
3892
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
3290
3893
|
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
3291
|
-
...style.quality.fixerPython ? { fixerPython:
|
|
3894
|
+
...style.quality.fixerPython ? { fixerPython: path6.resolve(root, style.quality.fixerPython) } : {},
|
|
3895
|
+
...style.quality.fps == null ? {} : { fps: style.quality.fps }
|
|
3292
3896
|
}
|
|
3293
3897
|
} : {},
|
|
3294
3898
|
tags,
|
|
@@ -3316,9 +3920,9 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3316
3920
|
);
|
|
3317
3921
|
}
|
|
3318
3922
|
const sourceSpec = await finalize(asset.revision.from);
|
|
3319
|
-
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ?
|
|
3923
|
+
const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path6.resolve(root, sourceSpec.source) : sourceSpec.outFile);
|
|
3320
3924
|
const sourceImage = await optionalRevisionImage(sourceFile, "revision source");
|
|
3321
|
-
const maskFile = asset.revision.mask ?
|
|
3925
|
+
const maskFile = asset.revision.mask ? path6.resolve(root, asset.revision.mask) : void 0;
|
|
3322
3926
|
const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
|
|
3323
3927
|
if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
|
|
3324
3928
|
throw new Error(
|
|
@@ -3344,7 +3948,12 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3344
3948
|
...asset.revision.strength == null ? {} : { strength: asset.revision.strength }
|
|
3345
3949
|
};
|
|
3346
3950
|
}
|
|
3347
|
-
resolved.specHash = specHash(
|
|
3951
|
+
resolved.specHash = specHash(
|
|
3952
|
+
resolved,
|
|
3953
|
+
styleImageHashes,
|
|
3954
|
+
providerOptionIdentity,
|
|
3955
|
+
providerInputIdentities.get(assetId)
|
|
3956
|
+
);
|
|
3348
3957
|
activeProvider.validate?.(resolved, resolvedImages);
|
|
3349
3958
|
const estimate = validateCostEstimate(activeProvider.id, activeProvider.estimate(resolved));
|
|
3350
3959
|
resolved.cost = estimate.amount;
|
|
@@ -3369,349 +3978,92 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3369
3978
|
for (const spec of specs) {
|
|
3370
3979
|
const key = `${spec.styleId}/${spec.assetId}`;
|
|
3371
3980
|
claimOutput(spec.outFile, key);
|
|
3372
|
-
if (spec.quality)
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
}
|
|
3390
|
-
|
|
3391
|
-
const style = loaded.manifest.styles[styleId];
|
|
3392
|
-
if (!style) throw new Error(`Unknown style "${styleId}"`);
|
|
3393
|
-
const out = [];
|
|
3394
|
-
for (const img of style.styleImages) {
|
|
3395
|
-
const file = path4.resolve(loaded.root, img.path);
|
|
3396
|
-
const buf = await readFile2(file);
|
|
3397
|
-
const metadata = imageMetadata(buf);
|
|
3398
|
-
if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
|
|
3399
|
-
if (metadata.width < 1 || metadata.height < 1) {
|
|
3400
|
-
throw new Error(`Style image has invalid dimensions: ${file}`);
|
|
3401
|
-
}
|
|
3402
|
-
out.push({ base64: buf.toString("base64"), ...metadata });
|
|
3403
|
-
}
|
|
3404
|
-
return out;
|
|
3405
|
-
}
|
|
3406
|
-
function imageMetadata(buf) {
|
|
3407
|
-
if (buf.length >= 24 && buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834 && buf.toString("ascii", 12, 16) === "IHDR") {
|
|
3408
|
-
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), format: "png" };
|
|
3409
|
-
}
|
|
3410
|
-
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
|
|
3411
|
-
let offset = 2;
|
|
3412
|
-
while (offset + 3 < buf.length) {
|
|
3413
|
-
if (buf[offset] !== 255) return null;
|
|
3414
|
-
while (buf[offset] === 255) offset++;
|
|
3415
|
-
const marker = buf[offset++];
|
|
3416
|
-
if (marker == null || marker === 217 || marker === 218) break;
|
|
3417
|
-
if (marker === 1 || marker >= 208 && marker <= 215) continue;
|
|
3418
|
-
if (offset + 2 > buf.length) return null;
|
|
3419
|
-
const length = buf.readUInt16BE(offset);
|
|
3420
|
-
if (length < 2 || offset + length > buf.length) return null;
|
|
3421
|
-
const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
|
|
3422
|
-
if (isStartOfFrame) {
|
|
3423
|
-
if (length < 7) return null;
|
|
3424
|
-
return {
|
|
3425
|
-
width: buf.readUInt16BE(offset + 5),
|
|
3426
|
-
height: buf.readUInt16BE(offset + 3),
|
|
3427
|
-
format: "jpeg"
|
|
3428
|
-
};
|
|
3429
|
-
}
|
|
3430
|
-
offset += length;
|
|
3431
|
-
}
|
|
3432
|
-
return null;
|
|
3433
|
-
}
|
|
3434
|
-
|
|
3435
|
-
// src/pipeline/pack.ts
|
|
3436
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
3437
|
-
import path7 from "path";
|
|
3438
|
-
|
|
3439
|
-
// src/outputs.ts
|
|
3440
|
-
import path6 from "path";
|
|
3441
|
-
|
|
3442
|
-
// src/lock.ts
|
|
3443
|
-
import { readFile as readFile3, writeFile, rename, mkdir, rmdir, rm, stat } from "fs/promises";
|
|
3444
|
-
import { existsSync as existsSync4 } from "fs";
|
|
3445
|
-
import path5 from "path";
|
|
3446
|
-
async function loadLock(lockPath) {
|
|
3447
|
-
if (!existsSync4(lockPath)) return { version: 2, entries: {} };
|
|
3448
|
-
try {
|
|
3449
|
-
return parseLock(JSON.parse(await readFile3(lockPath, "utf8")));
|
|
3450
|
-
} catch (err) {
|
|
3451
|
-
throw new Error(
|
|
3452
|
-
`Lockfile at ${lockPath} is malformed:
|
|
3453
|
-
${err instanceof Error ? err.message : String(err)}`
|
|
3454
|
-
);
|
|
3455
|
-
}
|
|
3456
|
-
}
|
|
3457
|
-
var saveQueues = /* @__PURE__ */ new Map();
|
|
3458
|
-
var dirtyPatches = /* @__PURE__ */ new WeakMap();
|
|
3459
|
-
var dirtyDeletes = /* @__PURE__ */ new WeakMap();
|
|
3460
|
-
function saveLock(lockPath, lock) {
|
|
3461
|
-
const queueKey = path5.resolve(lockPath);
|
|
3462
|
-
const previous = saveQueues.get(queueKey) ?? Promise.resolve();
|
|
3463
|
-
const next = previous.catch(() => {
|
|
3464
|
-
}).then(() => writeLockNow(lockPath, lock));
|
|
3465
|
-
saveQueues.set(queueKey, next);
|
|
3466
|
-
const cleanup = () => {
|
|
3467
|
-
if (saveQueues.get(queueKey) === next) saveQueues.delete(queueKey);
|
|
3468
|
-
};
|
|
3469
|
-
void next.then(cleanup, cleanup);
|
|
3470
|
-
return next;
|
|
3471
|
-
}
|
|
3472
|
-
async function writeLockNow(lockPath, lock) {
|
|
3473
|
-
await mkdir(path5.dirname(path5.resolve(lockPath)), { recursive: true });
|
|
3474
|
-
const release = await acquireFileLock(lockPath);
|
|
3475
|
-
try {
|
|
3476
|
-
await writeLockWhileHeld(lockPath, lock);
|
|
3477
|
-
} finally {
|
|
3478
|
-
await release();
|
|
3479
|
-
}
|
|
3480
|
-
}
|
|
3481
|
-
async function writeLockWhileHeld(file, lock) {
|
|
3482
|
-
let disk = { version: 2, entries: {} };
|
|
3483
|
-
if (existsSync4(file)) {
|
|
3484
|
-
try {
|
|
3485
|
-
disk = parseLock(JSON.parse(await readFile3(file, "utf8")));
|
|
3486
|
-
} catch (err) {
|
|
3487
|
-
throw new Error(
|
|
3488
|
-
`Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
3489
|
-
);
|
|
3490
|
-
}
|
|
3491
|
-
}
|
|
3492
|
-
const dirty = dirtyPatches.get(lock) ?? /* @__PURE__ */ new Map();
|
|
3493
|
-
const snapshot = /* @__PURE__ */ new Map();
|
|
3494
|
-
for (const [key, patch] of dirty) snapshot.set(key, { ...patch });
|
|
3495
|
-
const merged = { ...disk.entries };
|
|
3496
|
-
for (const [key, patch] of snapshot) {
|
|
3497
|
-
const local = lock.entries[key];
|
|
3498
|
-
if (!local) continue;
|
|
3499
|
-
merged[key] = merged[key] ? { ...merged[key], ...patch } : local;
|
|
3500
|
-
}
|
|
3501
|
-
for (const [key, entry] of Object.entries(lock.entries)) {
|
|
3502
|
-
if (!merged[key]) merged[key] = entry;
|
|
3503
|
-
}
|
|
3504
|
-
const deletes = dirtyDeletes.get(lock);
|
|
3505
|
-
const deleteSnapshot = deletes ? new Set(deletes) : /* @__PURE__ */ new Set();
|
|
3506
|
-
for (const key of deleteSnapshot) delete merged[key];
|
|
3507
|
-
const sorted = {};
|
|
3508
|
-
for (const key of Object.keys(merged).sort()) {
|
|
3509
|
-
sorted[key] = merged[key];
|
|
3510
|
-
}
|
|
3511
|
-
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3512
|
-
try {
|
|
3513
|
-
await writeFile(tmp, JSON.stringify({ version: 2, entries: sorted }, null, 2) + "\n");
|
|
3514
|
-
await rename(tmp, file);
|
|
3515
|
-
} finally {
|
|
3516
|
-
await rm(tmp, { force: true });
|
|
3517
|
-
}
|
|
3518
|
-
const currentDirty = dirtyPatches.get(lock);
|
|
3519
|
-
if (currentDirty) {
|
|
3520
|
-
for (const [key, patch] of snapshot) {
|
|
3521
|
-
const current = currentDirty.get(key);
|
|
3522
|
-
if (!current) continue;
|
|
3523
|
-
for (const [field, value] of Object.entries(patch)) {
|
|
3524
|
-
if (Object.is(current[field], value)) {
|
|
3525
|
-
delete current[field];
|
|
3526
|
-
}
|
|
3527
|
-
}
|
|
3528
|
-
if (Object.keys(current).length === 0) currentDirty.delete(key);
|
|
3529
|
-
}
|
|
3530
|
-
}
|
|
3531
|
-
const stillDirty = dirtyDeletes.get(lock);
|
|
3532
|
-
if (stillDirty) {
|
|
3533
|
-
for (const key of deleteSnapshot) stillDirty.delete(key);
|
|
3534
|
-
if (stillDirty.size === 0) dirtyDeletes.delete(lock);
|
|
3535
|
-
}
|
|
3536
|
-
const localAfterWrite = lock.entries;
|
|
3537
|
-
lock.entries = { ...merged };
|
|
3538
|
-
for (const [key, patch] of currentDirty ?? []) {
|
|
3539
|
-
const base = lock.entries[key] ?? localAfterWrite[key];
|
|
3540
|
-
if (base) lock.entries[key] = { ...base, ...patch };
|
|
3541
|
-
}
|
|
3542
|
-
}
|
|
3543
|
-
function upsert(lock, key, patch) {
|
|
3544
|
-
dirtyDeletes.get(lock)?.delete(key);
|
|
3545
|
-
const existing = lock.entries[key];
|
|
3546
|
-
const next = { ...existing, ...patch };
|
|
3547
|
-
lock.entries[key] = next;
|
|
3548
|
-
let dirty = dirtyPatches.get(lock);
|
|
3549
|
-
if (!dirty) {
|
|
3550
|
-
dirty = /* @__PURE__ */ new Map();
|
|
3551
|
-
dirtyPatches.set(lock, dirty);
|
|
3552
|
-
}
|
|
3553
|
-
dirty.set(key, { ...dirty.get(key) ?? {}, ...patch });
|
|
3554
|
-
return next;
|
|
3555
|
-
}
|
|
3556
|
-
function remove(lock, key) {
|
|
3557
|
-
if (!lock.entries[key]) return false;
|
|
3558
|
-
delete lock.entries[key];
|
|
3559
|
-
const patches = dirtyPatches.get(lock);
|
|
3560
|
-
if (patches) {
|
|
3561
|
-
patches.delete(key);
|
|
3562
|
-
if (patches.size === 0) dirtyPatches.delete(lock);
|
|
3563
|
-
}
|
|
3564
|
-
let deletes = dirtyDeletes.get(lock);
|
|
3565
|
-
if (!deletes) {
|
|
3566
|
-
deletes = /* @__PURE__ */ new Set();
|
|
3567
|
-
dirtyDeletes.set(lock, deletes);
|
|
3568
|
-
}
|
|
3569
|
-
deletes.add(key);
|
|
3570
|
-
return true;
|
|
3571
|
-
}
|
|
3572
|
-
var FILE_LOCK_TIMEOUT_MS = 1e4;
|
|
3573
|
-
var STALE_FILE_LOCK_MS = 2 * 60 * 1e3;
|
|
3574
|
-
async function acquireFileLock(file) {
|
|
3575
|
-
const lockDir = `${file}.lock`;
|
|
3576
|
-
const deadline = Date.now() + FILE_LOCK_TIMEOUT_MS;
|
|
3577
|
-
for (; ; ) {
|
|
3578
|
-
try {
|
|
3579
|
-
await mkdir(lockDir);
|
|
3580
|
-
return async () => {
|
|
3581
|
-
await rmdir(lockDir).catch(() => {
|
|
3582
|
-
});
|
|
3583
|
-
};
|
|
3584
|
-
} catch (err) {
|
|
3585
|
-
const code = err.code;
|
|
3586
|
-
if (code !== "EEXIST") throw err;
|
|
3587
|
-
try {
|
|
3588
|
-
const age = Date.now() - (await stat(lockDir)).mtimeMs;
|
|
3589
|
-
if (age > STALE_FILE_LOCK_MS) {
|
|
3590
|
-
await rmdir(lockDir);
|
|
3591
|
-
continue;
|
|
3592
|
-
}
|
|
3593
|
-
} catch (lockErr) {
|
|
3594
|
-
if (lockErr.code === "ENOENT") continue;
|
|
3595
|
-
}
|
|
3596
|
-
if (Date.now() >= deadline) {
|
|
3597
|
-
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`);
|
|
3598
4000
|
}
|
|
3599
|
-
await new Promise((resolve) => setTimeout(resolve, 25 + Math.floor(Math.random() * 25)));
|
|
3600
4001
|
}
|
|
3601
4002
|
}
|
|
4003
|
+
return specs;
|
|
3602
4004
|
}
|
|
3603
|
-
function
|
|
3604
|
-
const
|
|
3605
|
-
|
|
3606
|
-
const unit = entry.costUnit ?? "generations";
|
|
3607
|
-
totals[unit] = (totals[unit] ?? 0) + (entry.cost ?? 0);
|
|
3608
|
-
}
|
|
3609
|
-
return totals;
|
|
3610
|
-
}
|
|
3611
|
-
|
|
3612
|
-
// src/outputs.ts
|
|
3613
|
-
function portableOutputPath(file, manifestDir) {
|
|
3614
|
-
const root = path6.resolve(manifestDir);
|
|
3615
|
-
const absolute = path6.isAbsolute(file) ? path6.normalize(file) : path6.resolve(root, file);
|
|
3616
|
-
const relative = path6.relative(root, absolute);
|
|
3617
|
-
const value = path6.isAbsolute(relative) ? absolute : relative;
|
|
3618
|
-
return value.split(path6.sep).join("/");
|
|
3619
|
-
}
|
|
3620
|
-
function resolveOutputPath(recordedPath, manifestDir) {
|
|
3621
|
-
if (path6.isAbsolute(recordedPath)) return path6.normalize(recordedPath);
|
|
3622
|
-
if (path6.win32.isAbsolute(recordedPath)) return recordedPath;
|
|
3623
|
-
return path6.resolve(manifestDir, recordedPath.split(/[\\/]/).join(path6.sep));
|
|
3624
|
-
}
|
|
3625
|
-
function expectedOutputPath(spec, role, index, total, mediaType) {
|
|
3626
|
-
const originalExt = path6.extname(spec.outFile);
|
|
3627
|
-
const ext = mediaType ? mediaExtension(mediaType) : originalExt || ".png";
|
|
3628
|
-
const stem = originalExt ? spec.outFile.slice(0, -originalExt.length) : spec.outFile;
|
|
3629
|
-
if (total === 1) return `${stem}${ext}`;
|
|
3630
|
-
const safeRole = (role ?? fallbackOutputRole(index)).replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
3631
|
-
return `${stem}-${safeRole}${ext}`;
|
|
3632
|
-
}
|
|
3633
|
-
function currentOutputPath(output, spec, index, total) {
|
|
3634
|
-
return expectedOutputPath(spec, output.role, index, total, output.mediaType);
|
|
3635
|
-
}
|
|
3636
|
-
function currentEntryOutputPath(entry, spec, index) {
|
|
3637
|
-
const output = entry.outputs[index];
|
|
3638
|
-
const sourceIndex = output.role ? (entry.sourceUrls ?? []).findIndex((source) => source.role === output.role) : -1;
|
|
3639
|
-
const position = sourceIndex >= 0 ? sourceIndex : index;
|
|
3640
|
-
const total = Math.max(entry.outputs.length, entry.sourceUrls?.length ?? 0);
|
|
3641
|
-
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`;
|
|
3642
4008
|
}
|
|
3643
|
-
function
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
const outputs = entry.outputs.map((output, index) => {
|
|
3651
|
-
const absolute = currentEntryOutputPath(entry, spec, index);
|
|
3652
|
-
const portable = portableOutputPath(absolute, spec.root);
|
|
3653
|
-
if (portable === output.path) return output;
|
|
3654
|
-
changed++;
|
|
3655
|
-
entryChanged = true;
|
|
3656
|
-
return { ...output, path: portable };
|
|
3657
|
-
});
|
|
3658
|
-
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}`);
|
|
3659
4016
|
}
|
|
3660
|
-
return
|
|
3661
|
-
}
|
|
3662
|
-
function fallbackOutputRole(index) {
|
|
3663
|
-
return `output-${String(index).padStart(2, "0")}`;
|
|
3664
|
-
}
|
|
3665
|
-
function outputId(assetId, output, index, total) {
|
|
3666
|
-
if (total === 1) return assetId;
|
|
3667
|
-
return `${assetId}/${output.role ?? fallbackOutputRole(index)}`;
|
|
3668
|
-
}
|
|
3669
|
-
function resolveEntryOutputs(entry, assetId, manifestDir) {
|
|
3670
|
-
return entry.outputs.map((output, index) => ({
|
|
3671
|
-
...output,
|
|
3672
|
-
assetId,
|
|
3673
|
-
id: outputId(assetId, output, index, entry.outputs.length),
|
|
3674
|
-
index,
|
|
3675
|
-
absolutePath: resolveOutputPath(output.path, manifestDir)
|
|
3676
|
-
}));
|
|
3677
|
-
}
|
|
3678
|
-
function resolveSpecEntryOutputs(entry, spec) {
|
|
3679
|
-
return entry.outputs.map((output, index) => ({
|
|
3680
|
-
...output,
|
|
3681
|
-
assetId: spec.assetId,
|
|
3682
|
-
id: outputId(spec.assetId, output, index, entry.outputs.length),
|
|
3683
|
-
index,
|
|
3684
|
-
absolutePath: currentEntryOutputPath(entry, spec, index)
|
|
3685
|
-
}));
|
|
4017
|
+
return { hash: sha256(bytes), ...metadata };
|
|
3686
4018
|
}
|
|
3687
|
-
function
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
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 });
|
|
3691
4032
|
}
|
|
3692
|
-
|
|
3693
|
-
if (entry.outputs.length === 1) return { ok: true, output: entry.outputs[0] };
|
|
3694
|
-
const primary = entry.outputs.filter((output) => !output.role);
|
|
3695
|
-
if (primary.length === 1) return { ok: true, output: primary[0] };
|
|
3696
|
-
return {
|
|
3697
|
-
ok: false,
|
|
3698
|
-
reason: `${entry.outputs.length} outputs are recorded; set \`outputRole\` to choose one`
|
|
3699
|
-
};
|
|
4033
|
+
return out;
|
|
3700
4034
|
}
|
|
3701
|
-
function
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
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;
|
|
3712
4062
|
}
|
|
3713
4063
|
|
|
3714
4064
|
// src/pipeline/pack.ts
|
|
4065
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
4066
|
+
import path7 from "path";
|
|
3715
4067
|
function resolvePackInputs(raw, inputsFilePath) {
|
|
3716
4068
|
if (!Array.isArray(raw) || !raw.length) {
|
|
3717
4069
|
throw new Error("--inputs must be a non-empty JSON array of { id, path }");
|
|
@@ -3790,7 +4142,7 @@ function packSprites(inputs, options = {}) {
|
|
|
3790
4142
|
function packStyle(lock, styleId, manifestDir, options = {}) {
|
|
3791
4143
|
if (options.sourceOverrides) {
|
|
3792
4144
|
if (options.outputRoles?.length) {
|
|
3793
|
-
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.");
|
|
3794
4146
|
}
|
|
3795
4147
|
const inputs2 = Object.entries(options.sourceOverrides).map(([id, source]) => ({
|
|
3796
4148
|
id,
|
|
@@ -4602,6 +4954,8 @@ function hex(c) {
|
|
|
4602
4954
|
|
|
4603
4955
|
// src/pipeline/refine.ts
|
|
4604
4956
|
var execFileAsync = promisify(execFile);
|
|
4957
|
+
var PIXEL_ART_FIXER_MIN_SIDE = 16;
|
|
4958
|
+
var PIXEL_ART_FIXER_MAX_PIXELS = 4e6;
|
|
4605
4959
|
var PIXEL_ART_FIXER_URL = "https://github.com/Retro-Diffusion/pixel-art-fixer";
|
|
4606
4960
|
var PIXEL_ART_FIXER_REVISION = "ef376e57e1c272633ca2dbf5f29ec3fcf6596465";
|
|
4607
4961
|
function message2(error) {
|
|
@@ -4649,25 +5003,33 @@ function firstJsonObject(text) {
|
|
|
4649
5003
|
}
|
|
4650
5004
|
var PYTHON_FIXER_BRIDGE = [
|
|
4651
5005
|
"import json, sys",
|
|
4652
|
-
"from
|
|
4653
|
-
"
|
|
4654
|
-
"
|
|
4655
|
-
"
|
|
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')}))"
|
|
4656
5014
|
].join("; ");
|
|
4657
5015
|
function parseDetection(raw) {
|
|
4658
5016
|
if (!raw || typeof raw !== "object") throw new Error("Pixel Art Fixer returned no detection result.");
|
|
4659
5017
|
const value = raw;
|
|
4660
5018
|
const stepX = Number(value.step_x);
|
|
4661
5019
|
const stepY = Number(value.step_y);
|
|
5020
|
+
const phaseX = Number(value.phase_x);
|
|
5021
|
+
const phaseY = Number(value.phase_y);
|
|
4662
5022
|
const columns = Number(value.cols);
|
|
4663
5023
|
const rows = Number(value.rows);
|
|
4664
5024
|
const consensus = value.consensus;
|
|
4665
|
-
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) {
|
|
4666
5026
|
throw new Error("Pixel Art Fixer returned an invalid grid detection result.");
|
|
4667
5027
|
}
|
|
4668
5028
|
return {
|
|
4669
5029
|
stepX,
|
|
4670
5030
|
stepY,
|
|
5031
|
+
phaseX,
|
|
5032
|
+
phaseY,
|
|
4671
5033
|
columns,
|
|
4672
5034
|
rows,
|
|
4673
5035
|
consensus,
|
|
@@ -4778,6 +5140,23 @@ function auditRefinement(png, detection, palette, minTransparency, minGridConfid
|
|
|
4778
5140
|
function isGridConfidence(value) {
|
|
4779
5141
|
return value === "low" || value === "medium" || value === "high";
|
|
4780
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
|
+
}
|
|
4781
5160
|
function parseRecordOptions(manifest, record) {
|
|
4782
5161
|
if (manifest.kind !== "refine") throw new Error(`${record} is not a PixelKiln refinement record.`);
|
|
4783
5162
|
const raw = manifest.options;
|
|
@@ -4787,11 +5166,26 @@ function parseRecordOptions(manifest, record) {
|
|
|
4787
5166
|
const palette = options.palette;
|
|
4788
5167
|
const audit = options.audit;
|
|
4789
5168
|
const review = options.review;
|
|
4790
|
-
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)) {
|
|
4791
5170
|
throw new Error(`${record} has invalid quality metadata.`);
|
|
4792
5171
|
}
|
|
4793
5172
|
return options;
|
|
4794
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
|
+
}
|
|
4795
5189
|
async function refineAsset(options) {
|
|
4796
5190
|
const source = path10.resolve(options.source);
|
|
4797
5191
|
const output = path10.resolve(options.output);
|
|
@@ -4806,9 +5200,21 @@ async function refineAsset(options) {
|
|
|
4806
5200
|
} catch (error) {
|
|
4807
5201
|
throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
|
|
4808
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
|
+
}
|
|
4809
5216
|
const temp = await mkdtemp(path10.join(tmpdir(), "pixelkiln-refine-"));
|
|
4810
5217
|
const recovered = path10.join(temp, "native.png");
|
|
4811
|
-
const compatibilityCommand = options.fixerCommand;
|
|
4812
5218
|
const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
|
|
4813
5219
|
const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
|
|
4814
5220
|
try {
|
|
@@ -4855,11 +5261,13 @@ async function refineAsset(options) {
|
|
|
4855
5261
|
tool: "pixel-art-fixer",
|
|
4856
5262
|
url: PIXEL_ART_FIXER_URL,
|
|
4857
5263
|
revision: options.fixerRevision ?? PIXEL_ART_FIXER_REVISION,
|
|
4858
|
-
protocol: compatibilityCommand ? "python-cli-v1" : "python-
|
|
5264
|
+
protocol: compatibilityCommand ? "python-cli-v1" : "python-core-v1",
|
|
4859
5265
|
confidence: detection.confidence,
|
|
4860
5266
|
consensus: detection.consensus,
|
|
4861
5267
|
stepX: detection.stepX,
|
|
4862
5268
|
stepY: detection.stepY,
|
|
5269
|
+
phaseX: detection.phaseX,
|
|
5270
|
+
phaseY: detection.phaseY,
|
|
4863
5271
|
sourceWidth: sourcePng.width,
|
|
4864
5272
|
sourceHeight: sourcePng.height,
|
|
4865
5273
|
nativeWidth: quantized.width,
|
|
@@ -4880,19 +5288,142 @@ async function refineAsset(options) {
|
|
|
4880
5288
|
await rm3(temp, { recursive: true, force: true });
|
|
4881
5289
|
}
|
|
4882
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
|
+
}
|
|
4883
5407
|
async function checkQualityRecord(recordPath) {
|
|
4884
5408
|
const record = path10.resolve(recordPath);
|
|
4885
5409
|
const manifest = await readArtifactBundleManifest(record);
|
|
4886
5410
|
const options = parseRecordOptions(manifest, record);
|
|
4887
5411
|
const verification = await verifyArtifactBundle(record);
|
|
4888
|
-
const
|
|
4889
|
-
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;
|
|
4890
5417
|
const reasons = [];
|
|
4891
5418
|
if (!verification.fingerprintValid) reasons.push("quality record fingerprint changed");
|
|
4892
5419
|
for (const source2 of verification.changedSources) reasons.push(`source changed: ${source2}`);
|
|
4893
5420
|
for (const changed of verification.changedOutputs) reasons.push(`output changed: ${changed}`);
|
|
4894
|
-
if (manifest.sources.length !==
|
|
4895
|
-
|
|
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
|
+
}
|
|
4896
5427
|
if (!options.audit.safe) reasons.push(...options.audit.reasons.map((reason) => `audit: ${reason}`));
|
|
4897
5428
|
if (options.review.status !== "approved") reasons.push("human 1\xD7 review is pending");
|
|
4898
5429
|
return {
|
|
@@ -4903,6 +5434,8 @@ async function checkQualityRecord(recordPath) {
|
|
|
4903
5434
|
record,
|
|
4904
5435
|
source,
|
|
4905
5436
|
output,
|
|
5437
|
+
sources,
|
|
5438
|
+
outputs,
|
|
4906
5439
|
options,
|
|
4907
5440
|
reasons
|
|
4908
5441
|
};
|
|
@@ -4917,7 +5450,7 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
4917
5450
|
if (!verification.current) {
|
|
4918
5451
|
const changes = [
|
|
4919
5452
|
...verification.changedSources.map((source) => `source ${source}`),
|
|
4920
|
-
...verification.changedOutputs.map((
|
|
5453
|
+
...verification.changedOutputs.map((output) => `output ${output}`),
|
|
4921
5454
|
...!verification.fingerprintValid ? ["quality metadata"] : []
|
|
4922
5455
|
];
|
|
4923
5456
|
throw new Error(`Cannot approve a stale refinement; changed: ${changes.join(", ")}. Run refine again.`);
|
|
@@ -4925,12 +5458,17 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
4925
5458
|
if (!options.audit.safe) {
|
|
4926
5459
|
throw new Error(`Cannot approve a failed quality audit: ${options.audit.reasons.join("; ")}`);
|
|
4927
5460
|
}
|
|
4928
|
-
|
|
4929
|
-
|
|
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
|
+
);
|
|
4930
5466
|
}
|
|
4931
5467
|
const root = path10.dirname(record);
|
|
4932
|
-
const
|
|
4933
|
-
|
|
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
|
+
}));
|
|
4934
5472
|
const sources = manifest.sources.map((source) => ({
|
|
4935
5473
|
...source,
|
|
4936
5474
|
path: path10.resolve(root, source.path)
|
|
@@ -4953,7 +5491,7 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
4953
5491
|
};
|
|
4954
5492
|
await writeManagedArtifactBundle(
|
|
4955
5493
|
record,
|
|
4956
|
-
|
|
5494
|
+
outputs,
|
|
4957
5495
|
{ kind: "refine", sources, options: approved }
|
|
4958
5496
|
);
|
|
4959
5497
|
return checkQualityRecord(record);
|
|
@@ -4962,33 +5500,46 @@ async function approveQualityRecord(recordPath, approval) {
|
|
|
4962
5500
|
// src/pipeline/quality-profile.ts
|
|
4963
5501
|
async function qualitySource(spec, lock) {
|
|
4964
5502
|
if (spec.source) {
|
|
4965
|
-
|
|
4966
|
-
|
|
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}` };
|
|
4967
5508
|
}
|
|
4968
5509
|
const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
|
|
4969
|
-
if (!entry) return {
|
|
5510
|
+
if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
|
|
4970
5511
|
if (entry.specHash !== spec.specHash) {
|
|
4971
|
-
return {
|
|
5512
|
+
return { items: null, reason: "raw provider output does not match the current generation spec" };
|
|
4972
5513
|
}
|
|
4973
5514
|
if (entry.status !== "downloaded") {
|
|
4974
|
-
return {
|
|
5515
|
+
return { items: null, reason: `raw provider output is ${entry.status}, not downloaded` };
|
|
4975
5516
|
}
|
|
4976
|
-
|
|
5517
|
+
const frameSet = spec.generator === "frames";
|
|
5518
|
+
if (!frameSet && entry.outputs.length !== 1 || frameSet && entry.outputs.length < 2) {
|
|
4977
5519
|
return {
|
|
4978
|
-
|
|
4979
|
-
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}`
|
|
4980
5522
|
};
|
|
4981
5523
|
}
|
|
4982
|
-
const
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
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 });
|
|
4990
5541
|
}
|
|
4991
|
-
return {
|
|
5542
|
+
return { items };
|
|
4992
5543
|
}
|
|
4993
5544
|
async function inspectQualityProfile(spec, lock) {
|
|
4994
5545
|
if (!spec.quality) return null;
|
|
@@ -4996,16 +5547,20 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
4996
5547
|
const output = spec.quality.outFile;
|
|
4997
5548
|
const record = qualityRecordPath(output);
|
|
4998
5549
|
const source = await qualitySource(spec, lock);
|
|
4999
|
-
if (!source.
|
|
5550
|
+
if (!source.items) {
|
|
5000
5551
|
return { key, state: "blocked", reason: source.reason, source: null, output, record };
|
|
5001
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];
|
|
5002
5555
|
if (!existsSync6(record)) {
|
|
5003
5556
|
return {
|
|
5004
5557
|
key,
|
|
5005
5558
|
state: "needs-refinement",
|
|
5006
5559
|
reason: "no refinement record",
|
|
5007
|
-
source:
|
|
5560
|
+
source: sourcePaths[0],
|
|
5561
|
+
sources: sourcePaths,
|
|
5008
5562
|
output,
|
|
5563
|
+
outputs: expectedOutputs,
|
|
5009
5564
|
record
|
|
5010
5565
|
};
|
|
5011
5566
|
}
|
|
@@ -5014,14 +5569,17 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5014
5569
|
const expectedPalette = normalizeRefinementPalette(spec.quality.palette);
|
|
5015
5570
|
const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
|
|
5016
5571
|
const expectedTransparency = spec.quality.minTransparency ?? null;
|
|
5017
|
-
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);
|
|
5018
5574
|
if (contractChanged) {
|
|
5019
5575
|
return {
|
|
5020
5576
|
key,
|
|
5021
5577
|
state: "needs-refinement",
|
|
5022
5578
|
reason: "quality profile changed",
|
|
5023
|
-
source:
|
|
5579
|
+
source: sourcePaths[0],
|
|
5580
|
+
sources: sourcePaths,
|
|
5024
5581
|
output,
|
|
5582
|
+
outputs: expectedOutputs,
|
|
5025
5583
|
record
|
|
5026
5584
|
};
|
|
5027
5585
|
}
|
|
@@ -5030,8 +5588,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5030
5588
|
key,
|
|
5031
5589
|
state: "needs-refinement",
|
|
5032
5590
|
reason: check.reasons.filter((reason) => reason !== "human 1\xD7 review is pending").join("; ") || "refinement record is stale",
|
|
5033
|
-
source:
|
|
5591
|
+
source: sourcePaths[0],
|
|
5592
|
+
sources: sourcePaths,
|
|
5034
5593
|
output,
|
|
5594
|
+
outputs: expectedOutputs,
|
|
5035
5595
|
record
|
|
5036
5596
|
};
|
|
5037
5597
|
}
|
|
@@ -5040,8 +5600,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5040
5600
|
key,
|
|
5041
5601
|
state: "needs-approval",
|
|
5042
5602
|
reason: "automated checks passed; human 1\xD7 review is pending",
|
|
5043
|
-
source:
|
|
5603
|
+
source: sourcePaths[0],
|
|
5604
|
+
sources: sourcePaths,
|
|
5044
5605
|
output,
|
|
5606
|
+
outputs: expectedOutputs,
|
|
5045
5607
|
record
|
|
5046
5608
|
};
|
|
5047
5609
|
}
|
|
@@ -5049,8 +5611,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5049
5611
|
key,
|
|
5050
5612
|
state: "approved",
|
|
5051
5613
|
reason: "refined output and human approval are current",
|
|
5052
|
-
source:
|
|
5614
|
+
source: sourcePaths[0],
|
|
5615
|
+
sources: sourcePaths,
|
|
5053
5616
|
output,
|
|
5617
|
+
outputs: expectedOutputs,
|
|
5054
5618
|
record
|
|
5055
5619
|
};
|
|
5056
5620
|
} catch (error) {
|
|
@@ -5058,8 +5622,10 @@ async function inspectQualityProfile(spec, lock) {
|
|
|
5058
5622
|
key,
|
|
5059
5623
|
state: "needs-refinement",
|
|
5060
5624
|
reason: `invalid refinement record: ${error instanceof Error ? error.message : String(error)}`,
|
|
5061
|
-
source:
|
|
5625
|
+
source: sourcePaths[0],
|
|
5626
|
+
sources: sourcePaths,
|
|
5062
5627
|
output,
|
|
5628
|
+
outputs: expectedOutputs,
|
|
5063
5629
|
record
|
|
5064
5630
|
};
|
|
5065
5631
|
}
|
|
@@ -5083,8 +5649,7 @@ async function refineQualityProfiles(specs, lock, options = {}) {
|
|
|
5083
5649
|
continue;
|
|
5084
5650
|
}
|
|
5085
5651
|
try {
|
|
5086
|
-
|
|
5087
|
-
source: inspection.source,
|
|
5652
|
+
const common = {
|
|
5088
5653
|
output: inspection.output,
|
|
5089
5654
|
palette: spec.quality.palette,
|
|
5090
5655
|
minGridConfidence: spec.quality.minGridConfidence,
|
|
@@ -5096,7 +5661,18 @@ async function refineQualityProfiles(specs, lock, options = {}) {
|
|
|
5096
5661
|
fixerCommand: options.fixerCommand,
|
|
5097
5662
|
fixerArgsPrefix: options.fixerArgsPrefix,
|
|
5098
5663
|
force: options.force
|
|
5099
|
-
}
|
|
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
|
+
}
|
|
5100
5676
|
result.processed++;
|
|
5101
5677
|
result.items.push(await inspectQualityProfile(spec, lock));
|
|
5102
5678
|
} catch (error) {
|
|
@@ -5110,7 +5686,7 @@ async function refineQualityProfiles(specs, lock, options = {}) {
|
|
|
5110
5686
|
}
|
|
5111
5687
|
return result;
|
|
5112
5688
|
}
|
|
5113
|
-
async function requireApprovedQualitySources(specs, lock, assetIds) {
|
|
5689
|
+
async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles) {
|
|
5114
5690
|
const configured = specs.filter(
|
|
5115
5691
|
(spec) => spec.quality && (!assetIds || assetIds.has(spec.assetId))
|
|
5116
5692
|
);
|
|
@@ -5119,8 +5695,33 @@ async function requireApprovedQualitySources(specs, lock, assetIds) {
|
|
|
5119
5695
|
const rejected = [];
|
|
5120
5696
|
for (const spec of configured) {
|
|
5121
5697
|
const inspection = await inspectQualityProfile(spec, lock);
|
|
5122
|
-
if (inspection.state === "approved")
|
|
5123
|
-
|
|
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);
|
|
5124
5725
|
}
|
|
5125
5726
|
if (rejected.length) {
|
|
5126
5727
|
const detail = rejected.slice(0, 5).map((item) => `${item.key}: ${item.state} (${item.reason})`).join("; ");
|
|
@@ -5473,7 +6074,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
5473
6074
|
jobId,
|
|
5474
6075
|
// A multi-candidate generator routes through review; record the parent
|
|
5475
6076
|
// so `pick` knows where to look.
|
|
5476
|
-
reviewObjectId: estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
6077
|
+
reviewObjectId: spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
5477
6078
|
status: "processing",
|
|
5478
6079
|
providerMetadata: metadata ? { ...lock.entries[key].providerMetadata, [provider.id]: metadata } : lock.entries[key].providerMetadata
|
|
5479
6080
|
});
|
|
@@ -5521,7 +6122,7 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
5521
6122
|
tileFeature: entry.tileFeature ?? currentSpec?.tileFeature,
|
|
5522
6123
|
spec: currentSpec
|
|
5523
6124
|
});
|
|
5524
|
-
if (state.status === "review") {
|
|
6125
|
+
if (state.status === "review" || state.status === "review-set") {
|
|
5525
6126
|
upsert(lock, key, {
|
|
5526
6127
|
status: "review",
|
|
5527
6128
|
reviewObjectId: entry.jobId,
|
|
@@ -5534,7 +6135,9 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
5534
6135
|
} : entry.providerMetadata
|
|
5535
6136
|
});
|
|
5536
6137
|
result.review++;
|
|
5537
|
-
log2(
|
|
6138
|
+
log2(
|
|
6139
|
+
state.status === "review-set" ? ` review ${key} (${state.frameUrls.length} ordered frames)` : ` review ${key} (${state.candidateUrls.length} candidates)`
|
|
6140
|
+
);
|
|
5538
6141
|
} else if (state.status === "ready") {
|
|
5539
6142
|
const sourceUrls = state.sources?.length ? state.sources : state.sourceUrl ? [{ url: state.sourceUrl }] : [];
|
|
5540
6143
|
upsert(lock, key, {
|
|
@@ -6293,12 +6896,17 @@ function renderSheet(groups) {
|
|
|
6293
6896
|
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%);
|
|
6294
6897
|
background-size:12px 12px; background-position:0 0,6px 6px; }
|
|
6295
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; }
|
|
6296
6903
|
.cand { border:2px solid var(--line-strong); border-radius:0; padding:7px; background:var(--panel-deep);
|
|
6297
6904
|
cursor:pointer; display:flex; flex:0 0 auto; flex-direction:column; align-items:center; gap:5px;
|
|
6298
6905
|
position:relative; max-width:100%; }
|
|
6299
6906
|
.cand:hover { border-color:var(--dim); }
|
|
6300
6907
|
.cand.active { border-color:var(--dim); box-shadow:0 0 0 2px color-mix(in srgb, var(--dim) 20%, transparent); }
|
|
6301
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; }
|
|
6302
6910
|
.cand img { image-rendering:pixelated; display:block;
|
|
6303
6911
|
background-image:
|
|
6304
6912
|
linear-gradient(45deg,#0000 25%,#7f7f7f22 25%,#7f7f7f22 75%,#0000 75%),
|
|
@@ -6336,7 +6944,8 @@ function renderSheet(groups) {
|
|
|
6336
6944
|
</header>
|
|
6337
6945
|
<main id="root"></main>
|
|
6338
6946
|
<footer>
|
|
6339
|
-
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,
|
|
6340
6949
|
<kbd>\u2190</kbd>/<kbd>\u2192</kbd> browses every candidate and <kbd>Enter</kbd> chooses;
|
|
6341
6950
|
<kbd>1</kbd>\u2013<kbd>9</kbd> picks directly, <kbd>0</kbd> skips, and
|
|
6342
6951
|
<kbd>\u2191</kbd>/<kbd>\u2193</kbd> changes rows. Unchosen rows stay in review \u2014 nothing is
|
|
@@ -6384,12 +6993,34 @@ GROUPS.forEach((g, gi) => {
|
|
|
6384
6993
|
context.append(label, source);
|
|
6385
6994
|
review.insertBefore(context, frames);
|
|
6386
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
|
+
}
|
|
6387
7016
|
g.frameUrls.forEach((url, i) => {
|
|
6388
7017
|
const c = document.createElement('button');
|
|
6389
7018
|
c.type = 'button';
|
|
6390
|
-
c.className = 'cand';
|
|
7019
|
+
c.className = 'cand' + (g.mode === 'frame-set' ? ' set-frame' : '');
|
|
6391
7020
|
c.tabIndex = -1;
|
|
6392
|
-
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);
|
|
6393
7024
|
|
|
6394
7025
|
const preview = document.createElement('img');
|
|
6395
7026
|
preview.className = 'preview';
|
|
@@ -6399,7 +7030,8 @@ GROUPS.forEach((g, gi) => {
|
|
|
6399
7030
|
preview.loading = 'lazy';
|
|
6400
7031
|
const index = document.createElement('span');
|
|
6401
7032
|
index.className = 'idx';
|
|
6402
|
-
index.textContent = String(i + 1) + ' \xB7 ' +
|
|
7033
|
+
index.textContent = (g.frameLabels?.[i] || String(i + 1)) + ' \xB7 ' +
|
|
7034
|
+
g.width + '\xD7' + g.height + ' \xB7 ' + scaleLabel;
|
|
6403
7035
|
c.append(preview, index);
|
|
6404
7036
|
if (displayScale > 1 && largestSide <= 96) {
|
|
6405
7037
|
const actual = document.createElement('img');
|
|
@@ -6440,6 +7072,14 @@ function activate(i, frames) {
|
|
|
6440
7072
|
}
|
|
6441
7073
|
|
|
6442
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
|
+
}
|
|
6443
7083
|
const current = picks.get(gi);
|
|
6444
7084
|
frames.querySelectorAll('.cand').forEach(n => n.classList.remove('sel'));
|
|
6445
7085
|
if (current === i) { picks.delete(gi); groupEl.classList.remove('done'); }
|
|
@@ -6616,7 +7256,8 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
6616
7256
|
try {
|
|
6617
7257
|
const spec = specByKey.get(key);
|
|
6618
7258
|
const state = await provider.poll(entry.reviewObjectId, entry.generator, { spec });
|
|
6619
|
-
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;
|
|
6620
7261
|
const sourceRoute = spec?.revision ? `/revision-source/${encodeURIComponent(String(groups.length))}` : null;
|
|
6621
7262
|
if (sourceRoute && spec?.revision) {
|
|
6622
7263
|
reviewAssets.set(sourceRoute, {
|
|
@@ -6630,9 +7271,14 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
6630
7271
|
styleId: entry.styleId,
|
|
6631
7272
|
prompt: entry.prompt,
|
|
6632
7273
|
reviewObjectId: entry.reviewObjectId,
|
|
6633
|
-
frameUrls
|
|
7274
|
+
frameUrls,
|
|
6634
7275
|
width: entry.width,
|
|
6635
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" },
|
|
6636
7282
|
...sourceRoute && spec?.revision && spec.revision.sourceWidth && spec.revision.sourceHeight ? {
|
|
6637
7283
|
revision: {
|
|
6638
7284
|
mode: spec.revision.mode,
|
|
@@ -6675,6 +7321,31 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
6675
7321
|
const entry = lock.entries[key];
|
|
6676
7322
|
if (!group || !entry?.reviewObjectId) continue;
|
|
6677
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
|
+
}
|
|
6678
7349
|
const { objectId, sourceUrl, metadata } = await requireSelectCandidate(provider)(
|
|
6679
7350
|
entry.reviewObjectId,
|
|
6680
7351
|
index,
|
|
@@ -6767,6 +7438,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
6767
7438
|
file: asset.file,
|
|
6768
7439
|
tags: [],
|
|
6769
7440
|
styles: [],
|
|
7441
|
+
providerInputs: {},
|
|
6770
7442
|
...generator === "map" ? { width: asset.width, height: asset.height } : asset.width === asset.height && asset.width !== commonSize ? { size: asset.width } : {}
|
|
6771
7443
|
};
|
|
6772
7444
|
}
|
|
@@ -7829,6 +8501,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7829
8501
|
file: rel,
|
|
7830
8502
|
tags: ["salvaged"],
|
|
7831
8503
|
styles: [ctx.styleId],
|
|
8504
|
+
providerInputs: {},
|
|
7832
8505
|
...orphan.width === orphan.height ? { size: orphan.width } : { width: orphan.width, height: orphan.height }
|
|
7833
8506
|
};
|
|
7834
8507
|
const durableSource = shouldPersistSourceUrl(orphan.previewUrl) ? orphan.previewUrl : null;
|
|
@@ -9382,9 +10055,15 @@ async function main() {
|
|
|
9382
10055
|
if (!args.reviewer?.trim()) {
|
|
9383
10056
|
throw new Error("refine approve needs --reviewer <name>.");
|
|
9384
10057
|
}
|
|
9385
|
-
|
|
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
|
+
);
|
|
9386
10062
|
log(` Confirm crisp edges, readable forms, palette separation, alpha, and seams.`);
|
|
9387
|
-
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
|
+
)) {
|
|
9388
10067
|
log(" approval not recorded");
|
|
9389
10068
|
return;
|
|
9390
10069
|
}
|
|
@@ -9393,7 +10072,11 @@ async function main() {
|
|
|
9393
10072
|
note: args.note
|
|
9394
10073
|
});
|
|
9395
10074
|
if (args.json) log(JSON.stringify(result2, null, 2));
|
|
9396
|
-
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
|
+
}
|
|
9397
10080
|
return;
|
|
9398
10081
|
}
|
|
9399
10082
|
const result = await checkQualityRecord(args.from);
|
|
@@ -9906,7 +10589,10 @@ async function main() {
|
|
|
9906
10589
|
{ path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
9907
10590
|
];
|
|
9908
10591
|
const qualityRecords = qualitySources ? await Promise.all(
|
|
9909
|
-
|
|
10592
|
+
packagingSpecs.filter((spec) => spec.quality).map((spec) => provenanceFile(
|
|
10593
|
+
`$quality/${spec.assetId}`,
|
|
10594
|
+
qualityRecordPath(spec.quality.outFile)
|
|
10595
|
+
))
|
|
9910
10596
|
) : [];
|
|
9911
10597
|
await writeManagedArtifactBundle(`${base}.pixelkiln.json`, outputs, {
|
|
9912
10598
|
kind: "pack",
|
|
@@ -9958,7 +10644,8 @@ async function main() {
|
|
|
9958
10644
|
const qualitySources = await requireApprovedQualitySources(
|
|
9959
10645
|
packagingSpecs,
|
|
9960
10646
|
lock,
|
|
9961
|
-
new Set(Object.keys(cells))
|
|
10647
|
+
new Set(Object.keys(cells)),
|
|
10648
|
+
outputRoles
|
|
9962
10649
|
);
|
|
9963
10650
|
if (qualitySources) Object.assign(sources, qualitySources);
|
|
9964
10651
|
const { png, atlas, skipped, overBase, sources: artifactSources } = mountStyle(
|
|
@@ -9978,7 +10665,10 @@ async function main() {
|
|
|
9978
10665
|
{ path: metadata, data: JSON.stringify(atlas, null, 2) + "\n" }
|
|
9979
10666
|
];
|
|
9980
10667
|
const qualityRecords = qualitySources ? await Promise.all(
|
|
9981
|
-
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
|
+
))
|
|
9982
10672
|
) : [];
|
|
9983
10673
|
await writeManagedArtifactBundle(companion, outputs, {
|
|
9984
10674
|
kind: "mount",
|