pixelkiln 0.16.0 → 0.18.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/README.md +4 -4
- package/dist/cli.d.ts +16 -1
- package/dist/cli.js +329 -62
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +283 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +200 -9
- package/dist/index.d.ts +200 -9
- package/dist/index.js +281 -55
- package/dist/index.js.map +1 -1
- package/docs/AGENTS.md +4 -2
- package/docs/ARCHITECTURE.md +19 -5
- package/docs/CLI.md +26 -3
- package/docs/COMFYUI.md +75 -7
- package/docs/GETTING_STARTED.md +8 -1
- package/docs/LIBRARY.md +24 -2
- package/docs/MANIFEST.md +47 -8
- package/docs/QUALITY.md +13 -8
- package/docs/RECOVERY.md +17 -0
- package/package.json +1 -1
- package/schema/manifest.schema.json +21 -0
- package/schema/recipe.schema.json +4 -0
- package/skills/pixelkiln/SKILL.md +6 -3
- package/skills/pixelkiln/references/comfyui.md +15 -1
- package/skills/pixelkiln/references/quality.md +9 -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;
|
|
@@ -753,6 +764,63 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
753
764
|
}
|
|
754
765
|
};
|
|
755
766
|
}
|
|
767
|
+
async resolveInputs(value, context) {
|
|
768
|
+
const options = parseOptions(context.providerOptions);
|
|
769
|
+
if (!options.workflow || !options.workflowSha256) {
|
|
770
|
+
throw new Error("ComfyUI provider inputs require resolved workflow options");
|
|
771
|
+
}
|
|
772
|
+
const inputs = {};
|
|
773
|
+
const identity = {};
|
|
774
|
+
for (const [name, input] of Object.entries(value)) {
|
|
775
|
+
if (BUILTIN_BINDINGS.has(name)) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
`ComfyUI providerInputs.${name} is reserved; use PixelKiln's built-in ${name} field`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const binding = Object.hasOwn(options.bindings, name) ? options.bindings[name] : void 0;
|
|
781
|
+
if (!binding) {
|
|
782
|
+
throw new Error(
|
|
783
|
+
`ComfyUI providerInputs.${name} has no matching bindings.${name} target`
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
if (isImageBinding(options.workflow, binding)) {
|
|
787
|
+
if (typeof input !== "string") {
|
|
788
|
+
throw new Error(`ComfyUI providerInputs.${name} must be a manifest-relative PNG or JPEG path`);
|
|
789
|
+
}
|
|
790
|
+
const file = path2.resolve(context.root, input);
|
|
791
|
+
let bytes;
|
|
792
|
+
try {
|
|
793
|
+
bytes = await readFile(file);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
throw new Error(
|
|
796
|
+
`ComfyUI providerInputs.${name} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
const format = inputImageFormat(bytes);
|
|
800
|
+
if (!format) {
|
|
801
|
+
throw new Error(`ComfyUI providerInputs.${name} is not a PNG or JPEG: ${file}`);
|
|
802
|
+
}
|
|
803
|
+
const hash = sha256(bytes);
|
|
804
|
+
inputs[name] = { kind: "image", path: file, sha256: hash, format };
|
|
805
|
+
identity[name] = { kind: "image", sha256: hash, format };
|
|
806
|
+
} else {
|
|
807
|
+
const current = options.workflow[binding.nodeId].inputs[binding.input];
|
|
808
|
+
if (typeof current !== "string" && typeof current !== "number" && typeof current !== "boolean") {
|
|
809
|
+
throw new Error(
|
|
810
|
+
`ComfyUI providerInputs.${name} cannot replace non-scalar ${binding.nodeId}.${binding.input}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
if (typeof current !== typeof input) {
|
|
814
|
+
throw new Error(
|
|
815
|
+
`ComfyUI providerInputs.${name} must be ${typeof current} to match ${binding.nodeId}.${binding.input}`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
inputs[name] = input;
|
|
819
|
+
identity[name] = input;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return { inputs, identity };
|
|
823
|
+
}
|
|
756
824
|
supports(generator) {
|
|
757
825
|
return generator === "map";
|
|
758
826
|
}
|
|
@@ -820,6 +888,20 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
820
888
|
if (spec.seed != null && options.bindings.seed) {
|
|
821
889
|
setBinding(workflow, options.bindings.seed, spec.seed);
|
|
822
890
|
}
|
|
891
|
+
for (const [name, input] of Object.entries(spec.providerInputs ?? {})) {
|
|
892
|
+
const binding = Object.hasOwn(options.bindings, name) ? options.bindings[name] : void 0;
|
|
893
|
+
if (!binding) throw new Error(`ComfyUI provider input "${name}" has no binding`);
|
|
894
|
+
if (isComfyImageInput(input)) {
|
|
895
|
+
const bytes = await readFile(input.path);
|
|
896
|
+
if (sha256(bytes) !== input.sha256) {
|
|
897
|
+
throw new Error(`ComfyUI provider input "${name}" changed before upload`);
|
|
898
|
+
}
|
|
899
|
+
const uploaded = await this.client.uploadImage(bytes, input.sha256, input.format);
|
|
900
|
+
setBinding(workflow, binding, uploaded);
|
|
901
|
+
} else {
|
|
902
|
+
setBinding(workflow, binding, input);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
823
905
|
if (spec.revision) {
|
|
824
906
|
if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
|
|
825
907
|
throw new Error("ComfyUI revision source is not ready");
|
|
@@ -854,19 +936,22 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
854
936
|
}
|
|
855
937
|
}
|
|
856
938
|
const promptId = await this.client.submit(workflow);
|
|
857
|
-
|
|
858
|
-
|
|
939
|
+
const inputs = providerInputProvenance(spec.providerInputs);
|
|
940
|
+
const metadata = {
|
|
941
|
+
...Object.keys(inputs).length ? { inputs } : {},
|
|
859
942
|
...spec.revision ? {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {}
|
|
866
|
-
}
|
|
943
|
+
revision: {
|
|
944
|
+
mode: spec.revision.mode,
|
|
945
|
+
sourceAssetId: spec.revision.sourceAssetId,
|
|
946
|
+
sourceSha256: spec.revision.sourceSha256,
|
|
947
|
+
...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {}
|
|
867
948
|
}
|
|
868
949
|
} : {}
|
|
869
950
|
};
|
|
951
|
+
return {
|
|
952
|
+
jobId: encodeJob(promptId, options.outputNodeId),
|
|
953
|
+
...Object.keys(metadata).length ? { metadata } : {}
|
|
954
|
+
};
|
|
870
955
|
}
|
|
871
956
|
async poll(jobId, _generator, context) {
|
|
872
957
|
const { promptId, outputNodeId } = decodeJob(jobId);
|
|
@@ -941,28 +1026,10 @@ function parseOptions(value) {
|
|
|
941
1026
|
throw new Error("ComfyUI numImages must be a whole number from 1 to 16");
|
|
942
1027
|
}
|
|
943
1028
|
if (!isObject(value.bindings)) throw new Error("ComfyUI bindings must be an object");
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
"batchSize",
|
|
949
|
-
"seed",
|
|
950
|
-
"sourceImage",
|
|
951
|
-
"maskImage",
|
|
952
|
-
"strength"
|
|
953
|
-
]);
|
|
954
|
-
const extraBindings = Object.keys(value.bindings).filter((key) => !bindingKeys.has(key));
|
|
955
|
-
if (extraBindings.length) throw new Error(`Unknown ComfyUI binding(s): ${extraBindings.join(", ")}`);
|
|
956
|
-
const bindings = {
|
|
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
|
-
};
|
|
1029
|
+
const bindings = Object.fromEntries(
|
|
1030
|
+
Object.entries(value.bindings).map(([name, binding]) => [name, parseBinding(binding, name)])
|
|
1031
|
+
);
|
|
1032
|
+
if (!bindings.prompt) throw new Error("ComfyUI bindings.prompt must be an object");
|
|
966
1033
|
const workflow = value.workflow == null ? void 0 : parseWorkflow(value.workflow, workflowFile);
|
|
967
1034
|
const workflowSha256 = value.workflowSha256 == null ? void 0 : requiredString(value.workflowSha256, "workflowSha256");
|
|
968
1035
|
return {
|
|
@@ -1002,6 +1069,7 @@ function parseWorkflow(value, label) {
|
|
|
1002
1069
|
return value;
|
|
1003
1070
|
}
|
|
1004
1071
|
function validateWorkflowBindings(workflow, options) {
|
|
1072
|
+
const targets = /* @__PURE__ */ new Map();
|
|
1005
1073
|
for (const [name, binding] of Object.entries(options.bindings)) {
|
|
1006
1074
|
if (!binding) continue;
|
|
1007
1075
|
const node = workflow[binding.nodeId];
|
|
@@ -1011,6 +1079,12 @@ function validateWorkflowBindings(workflow, options) {
|
|
|
1011
1079
|
`ComfyUI ${name} binding refers to missing input "${binding.input}" on node "${binding.nodeId}"`
|
|
1012
1080
|
);
|
|
1013
1081
|
}
|
|
1082
|
+
const target = `${binding.nodeId}.${binding.input}`;
|
|
1083
|
+
const prior = targets.get(target);
|
|
1084
|
+
if (prior) {
|
|
1085
|
+
throw new Error(`ComfyUI bindings.${name} and bindings.${prior} target the same input ${target}`);
|
|
1086
|
+
}
|
|
1087
|
+
targets.set(target, name);
|
|
1014
1088
|
}
|
|
1015
1089
|
if (!workflow[options.outputNodeId]) {
|
|
1016
1090
|
throw new Error(`ComfyUI outputNodeId refers to missing node "${options.outputNodeId}"`);
|
|
@@ -1048,6 +1122,7 @@ function outputImages(entry, outputNodeId) {
|
|
|
1048
1122
|
}
|
|
1049
1123
|
function comfyMetadata(spec, promptId, outputNodeId, images) {
|
|
1050
1124
|
const options = resolvedOptions(spec);
|
|
1125
|
+
const inputs = providerInputProvenance(spec.providerInputs);
|
|
1051
1126
|
return {
|
|
1052
1127
|
promptId,
|
|
1053
1128
|
outputNodeId,
|
|
@@ -1055,6 +1130,7 @@ function comfyMetadata(spec, promptId, outputNodeId, images) {
|
|
|
1055
1130
|
workflowFile: options.workflowFile,
|
|
1056
1131
|
workflowSha256: options.workflowSha256,
|
|
1057
1132
|
files: images.map((image) => ({ ...image })),
|
|
1133
|
+
...Object.keys(inputs).length ? { inputs } : {},
|
|
1058
1134
|
...spec.revision ? {
|
|
1059
1135
|
revision: {
|
|
1060
1136
|
mode: spec.revision.mode,
|
|
@@ -1066,6 +1142,41 @@ function comfyMetadata(spec, promptId, outputNodeId, images) {
|
|
|
1066
1142
|
} : {}
|
|
1067
1143
|
};
|
|
1068
1144
|
}
|
|
1145
|
+
function isImageBinding(workflow, binding) {
|
|
1146
|
+
const node = workflow[binding.nodeId];
|
|
1147
|
+
return binding.input === "image" && (node?.class_type === "LoadImage" || node?.class_type === "LoadImageMask");
|
|
1148
|
+
}
|
|
1149
|
+
function isComfyImageInput(value) {
|
|
1150
|
+
return isObject(value) && value.kind === "image" && typeof value.path === "string" && typeof value.sha256 === "string" && (value.format === "png" || value.format === "jpeg");
|
|
1151
|
+
}
|
|
1152
|
+
function providerInputProvenance(inputs) {
|
|
1153
|
+
return Object.fromEntries(Object.entries(inputs ?? {}).map(([name, input]) => [
|
|
1154
|
+
name,
|
|
1155
|
+
isComfyImageInput(input) ? { kind: "image", sha256: input.sha256, format: input.format } : { kind: "value", value: input }
|
|
1156
|
+
]));
|
|
1157
|
+
}
|
|
1158
|
+
function inputImageFormat(bytes) {
|
|
1159
|
+
if (bytes.length >= 24 && bytes.readUInt32BE(0) === 2303741511 && bytes.readUInt32BE(4) === 218765834 && bytes.toString("ascii", 12, 16) === "IHDR" && bytes.readUInt32BE(16) > 0 && bytes.readUInt32BE(20) > 0) return "png";
|
|
1160
|
+
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) return null;
|
|
1161
|
+
let offset = 2;
|
|
1162
|
+
while (offset + 3 < bytes.length) {
|
|
1163
|
+
if (bytes[offset] !== 255) return null;
|
|
1164
|
+
while (bytes[offset] === 255) offset++;
|
|
1165
|
+
const marker = bytes[offset++];
|
|
1166
|
+
if (marker == null || marker === 217 || marker === 218) break;
|
|
1167
|
+
if (marker === 1 || marker >= 208 && marker <= 215) continue;
|
|
1168
|
+
if (offset + 2 > bytes.length) return null;
|
|
1169
|
+
const length = bytes.readUInt16BE(offset);
|
|
1170
|
+
if (length < 2 || offset + length > bytes.length) return null;
|
|
1171
|
+
const isStartOfFrame = marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
|
|
1172
|
+
if (isStartOfFrame) {
|
|
1173
|
+
if (length < 7) return null;
|
|
1174
|
+
return bytes.readUInt16BE(offset + 3) > 0 && bytes.readUInt16BE(offset + 5) > 0 ? "jpeg" : null;
|
|
1175
|
+
}
|
|
1176
|
+
offset += length;
|
|
1177
|
+
}
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1069
1180
|
function encodeJob(promptId, outputNodeId) {
|
|
1070
1181
|
return `${promptId}#${encodeURIComponent(outputNodeId)}`;
|
|
1071
1182
|
}
|
|
@@ -1550,7 +1661,9 @@ var QualityProfileSchema = z2.object({
|
|
|
1550
1661
|
/** Optional alpha requirement for isolated assets. */
|
|
1551
1662
|
minTransparency: z2.number().min(0).max(1).optional(),
|
|
1552
1663
|
/** Pixel Art Fixer revision written into the quality record. */
|
|
1553
|
-
fixerRevision: z2.string().min(1).optional()
|
|
1664
|
+
fixerRevision: z2.string().min(1).optional(),
|
|
1665
|
+
/** Python executable containing Pixel Art Fixer, relative to the manifest unless absolute. */
|
|
1666
|
+
fixerPython: z2.string().min(1).optional()
|
|
1554
1667
|
}).strict().refine(
|
|
1555
1668
|
(profile) => new Set(profile.palette.map((color) => color.replace(/^#/, "").toLowerCase())).size === profile.palette.length,
|
|
1556
1669
|
{ message: "quality palette colors must be unique", path: ["palette"] }
|
|
@@ -1766,7 +1879,13 @@ var AssetSchema = z2.object({
|
|
|
1766
1879
|
* exactly right for a colour style. Editing the shared prompt to suit one
|
|
1767
1880
|
* style would invalidate every other style's already-generated art.
|
|
1768
1881
|
*/
|
|
1769
|
-
promptByStyle: z2.record(z2.string()).default({})
|
|
1882
|
+
promptByStyle: z2.record(z2.string()).default({}),
|
|
1883
|
+
/**
|
|
1884
|
+
* Named per-asset values consumed by the active provider's declared
|
|
1885
|
+
* bindings. Values are JSON scalars; adapters may interpret a string as a
|
|
1886
|
+
* manifest-relative file when the target node accepts an uploaded input.
|
|
1887
|
+
*/
|
|
1888
|
+
providerInputs: z2.record(z2.union([z2.string(), z2.number().finite(), z2.boolean()])).default({})
|
|
1770
1889
|
}).strict().superRefine((asset, context) => {
|
|
1771
1890
|
if (asset.source && asset.revision) {
|
|
1772
1891
|
context.addIssue({
|
|
@@ -1804,6 +1923,18 @@ var LockEntrySchema = z2.object({
|
|
|
1804
1923
|
maskSha256: z2.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
1805
1924
|
strength: z2.number().min(0).max(1).optional()
|
|
1806
1925
|
}).strict().nullable().default(null),
|
|
1926
|
+
/**
|
|
1927
|
+
* Output hashes owned by the previous generation while its replacement is
|
|
1928
|
+
* pending. They authorize replacing only unchanged PixelKiln-owned files.
|
|
1929
|
+
*/
|
|
1930
|
+
supersededOutputs: z2.array(
|
|
1931
|
+
z2.object({
|
|
1932
|
+
path: z2.string(),
|
|
1933
|
+
sha256: z2.string(),
|
|
1934
|
+
role: z2.string().optional(),
|
|
1935
|
+
mediaType: MediaTypeSchema.optional()
|
|
1936
|
+
})
|
|
1937
|
+
).optional(),
|
|
1807
1938
|
/** Set at submit time, before the request is awaited, so a crash is recoverable. */
|
|
1808
1939
|
jobId: z2.string().nullable().default(null),
|
|
1809
1940
|
/** For `1dir`: the multi-candidate parent object awaiting selection. */
|
|
@@ -3197,6 +3328,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3197
3328
|
return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
|
|
3198
3329
|
});
|
|
3199
3330
|
const styleSpecs = /* @__PURE__ */ new Map();
|
|
3331
|
+
const providerInputIdentities = /* @__PURE__ */ new Map();
|
|
3200
3332
|
for (const [assetId, asset] of Object.entries(manifest.assets)) {
|
|
3201
3333
|
if (!resolutionAssetIds.has(assetId)) continue;
|
|
3202
3334
|
if (asset.styles.length && !asset.styles.includes(styleId)) continue;
|
|
@@ -3204,6 +3336,20 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3204
3336
|
if (!activeProvider.supports(generator)) {
|
|
3205
3337
|
throw new Error(`Provider "${activeProvider.id}" does not support generator "${generator}"`);
|
|
3206
3338
|
}
|
|
3339
|
+
if (Object.keys(asset.providerInputs).length && !activeProvider.resolveInputs) {
|
|
3340
|
+
throw new Error(
|
|
3341
|
+
`Provider "${activeProvider.id}" does not support asset providerInputs (${styleId}/${assetId})`
|
|
3342
|
+
);
|
|
3343
|
+
}
|
|
3344
|
+
const inputResolution = activeProvider.resolveInputs ? await activeProvider.resolveInputs(asset.providerInputs, {
|
|
3345
|
+
root,
|
|
3346
|
+
styleId,
|
|
3347
|
+
assetId,
|
|
3348
|
+
providerOptions
|
|
3349
|
+
}) : { inputs: {} };
|
|
3350
|
+
const providerInputs = inputResolution.inputs;
|
|
3351
|
+
const providerInputIdentity = inputResolution.identity ?? providerInputs;
|
|
3352
|
+
providerInputIdentities.set(assetId, providerInputIdentity);
|
|
3207
3353
|
let width;
|
|
3208
3354
|
let height;
|
|
3209
3355
|
let size;
|
|
@@ -3232,6 +3378,7 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3232
3378
|
assetId,
|
|
3233
3379
|
provider: activeProvider.id,
|
|
3234
3380
|
providerOptions,
|
|
3381
|
+
providerInputs,
|
|
3235
3382
|
generator,
|
|
3236
3383
|
prompt,
|
|
3237
3384
|
width,
|
|
@@ -3273,7 +3420,8 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3273
3420
|
palette: style.quality.palette,
|
|
3274
3421
|
minGridConfidence: style.quality.minGridConfidence,
|
|
3275
3422
|
...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
|
|
3276
|
-
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {}
|
|
3423
|
+
...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
|
|
3424
|
+
...style.quality.fixerPython ? { fixerPython: path4.resolve(root, style.quality.fixerPython) } : {}
|
|
3277
3425
|
}
|
|
3278
3426
|
} : {},
|
|
3279
3427
|
tags,
|
|
@@ -3329,7 +3477,12 @@ async function resolveSpecs(loaded, filter) {
|
|
|
3329
3477
|
...asset.revision.strength == null ? {} : { strength: asset.revision.strength }
|
|
3330
3478
|
};
|
|
3331
3479
|
}
|
|
3332
|
-
resolved.specHash = specHash(
|
|
3480
|
+
resolved.specHash = specHash(
|
|
3481
|
+
resolved,
|
|
3482
|
+
styleImageHashes,
|
|
3483
|
+
providerOptionIdentity,
|
|
3484
|
+
providerInputIdentities.get(assetId)
|
|
3485
|
+
);
|
|
3333
3486
|
activeProvider.validate?.(resolved, resolvedImages);
|
|
3334
3487
|
const estimate = validateCostEstimate(activeProvider.id, activeProvider.estimate(resolved));
|
|
3335
3488
|
resolved.cost = estimate.amount;
|
|
@@ -5075,7 +5228,9 @@ async function refineQualityProfiles(specs, lock, options = {}) {
|
|
|
5075
5228
|
minGridConfidence: spec.quality.minGridConfidence,
|
|
5076
5229
|
...spec.quality.minTransparency == null ? {} : { minTransparency: spec.quality.minTransparency },
|
|
5077
5230
|
fixerRevision: spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION,
|
|
5078
|
-
|
|
5231
|
+
// A one-off CLI/library override wins; otherwise each style can carry
|
|
5232
|
+
// the stable project-local interpreter that provides its pinned fixer.
|
|
5233
|
+
fixerPython: options.fixerPython ?? spec.quality.fixerPython,
|
|
5079
5234
|
fixerCommand: options.fixerCommand,
|
|
5080
5235
|
fixerArgsPrefix: options.fixerArgsPrefix,
|
|
5081
5236
|
force: options.force
|
|
@@ -5199,6 +5354,29 @@ async function anyOutputModified(entry, spec) {
|
|
|
5199
5354
|
}
|
|
5200
5355
|
return false;
|
|
5201
5356
|
}
|
|
5357
|
+
function resumeCommandForStatus(status) {
|
|
5358
|
+
if (status === "pending" || status === "processing") return "poll";
|
|
5359
|
+
if (status === "review") return "pick";
|
|
5360
|
+
if (status === "selected" || status === "download-failed") return "fetch";
|
|
5361
|
+
return null;
|
|
5362
|
+
}
|
|
5363
|
+
function resumeActions(specs, lock) {
|
|
5364
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
5365
|
+
for (const spec of specs) {
|
|
5366
|
+
const key = lockKey(spec.styleId, spec.assetId);
|
|
5367
|
+
const entry = lock.entries[key];
|
|
5368
|
+
if (!entry || entry.specHash !== spec.specHash) continue;
|
|
5369
|
+
const command = resumeCommandForStatus(entry.status);
|
|
5370
|
+
if (!command) continue;
|
|
5371
|
+
if (command === "poll" && !entry.jobId) continue;
|
|
5372
|
+
if (command === "pick" && !entry.reviewObjectId) continue;
|
|
5373
|
+
grouped.set(command, [...grouped.get(command) ?? [], key]);
|
|
5374
|
+
}
|
|
5375
|
+
return ["poll", "pick", "fetch"].flatMap((command) => {
|
|
5376
|
+
const keys = grouped.get(command);
|
|
5377
|
+
return keys?.length ? [{ command, keys }] : [];
|
|
5378
|
+
});
|
|
5379
|
+
}
|
|
5202
5380
|
async function buildPlan(specs, lock, opts = {}) {
|
|
5203
5381
|
const items = [];
|
|
5204
5382
|
for (const spec of specs) {
|
|
@@ -5234,10 +5412,20 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
5234
5412
|
reason = "prompt, size, or style changed";
|
|
5235
5413
|
} else if (entry.status === "download-failed") {
|
|
5236
5414
|
state = "recoverable";
|
|
5237
|
-
|
|
5415
|
+
const force = entry.error?.includes("pass --force") ? " --force" : "";
|
|
5416
|
+
reason = `${entry.error ?? "download failed"}; run pixelkiln fetch${force} (no generation cost)`;
|
|
5238
5417
|
} else if (entry.status === "failed") {
|
|
5239
5418
|
state = "failed";
|
|
5240
5419
|
reason = entry.error ?? "previous attempt failed";
|
|
5420
|
+
} else if (entry.status === "selected") {
|
|
5421
|
+
state = "recoverable";
|
|
5422
|
+
reason = "provider output is selected; run pixelkiln fetch (no generation cost)";
|
|
5423
|
+
} else if (entry.status === "pending" || entry.status === "processing") {
|
|
5424
|
+
state = "in-flight";
|
|
5425
|
+
reason = entry.jobId ? "awaiting processing; run pixelkiln poll" : "submission state has no job id; run pixelkiln doctor before retrying";
|
|
5426
|
+
} else if (entry.status === "review") {
|
|
5427
|
+
state = "in-flight";
|
|
5428
|
+
reason = entry.reviewObjectId ? "awaiting review; run pixelkiln pick" : "review state has no review object id; run pixelkiln doctor";
|
|
5241
5429
|
} else if (entry.status !== "downloaded") {
|
|
5242
5430
|
state = "in-flight";
|
|
5243
5431
|
reason = `awaiting ${entry.status}`;
|
|
@@ -5374,6 +5562,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
5374
5562
|
const since = Date.now() - lastSubmitAt;
|
|
5375
5563
|
if (since < spacing) await sleep2(spacing - since);
|
|
5376
5564
|
await requireRevisionReady(spec, lock);
|
|
5565
|
+
const previousEntry = lock.entries[key];
|
|
5566
|
+
const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
|
|
5377
5567
|
upsert(lock, key, {
|
|
5378
5568
|
styleId: spec.styleId,
|
|
5379
5569
|
assetId: spec.assetId,
|
|
@@ -5397,6 +5587,9 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
5397
5587
|
candidateIndex: null,
|
|
5398
5588
|
error: null,
|
|
5399
5589
|
outputs: [],
|
|
5590
|
+
// Keep the old ownership proof while new bytes are pending. Fetch may
|
|
5591
|
+
// replace that file only while its hash still matches this record.
|
|
5592
|
+
supersededOutputs,
|
|
5400
5593
|
providerMetadata: {},
|
|
5401
5594
|
sourceUrl: null,
|
|
5402
5595
|
sourceUrls: [],
|
|
@@ -5607,22 +5800,37 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
5607
5800
|
);
|
|
5608
5801
|
const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
|
|
5609
5802
|
if (existsSync9(target)) {
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5803
|
+
const currentHash = await sha256File(target);
|
|
5804
|
+
if (recorded && currentHash === recorded.sha256) {
|
|
5805
|
+
if (cacheDir) {
|
|
5806
|
+
await cacheMedia(
|
|
5807
|
+
cacheDir,
|
|
5808
|
+
await readFile7(target),
|
|
5809
|
+
recorded.mediaType ?? MediaType.PNG,
|
|
5810
|
+
recorded.sha256
|
|
5811
|
+
);
|
|
5812
|
+
}
|
|
5813
|
+
outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
|
|
5814
|
+
continue;
|
|
5615
5815
|
}
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5816
|
+
const superseded = (entry.supersededOutputs ?? []).find(
|
|
5817
|
+
(output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
|
|
5818
|
+
);
|
|
5819
|
+
if (!opts.force) {
|
|
5820
|
+
if (!recorded && superseded && currentHash === superseded.sha256) {
|
|
5821
|
+
} else if (recorded || superseded) {
|
|
5822
|
+
throw new Error(
|
|
5823
|
+
`refusing to overwrite modified output ${target}; pass --force to replace it`
|
|
5824
|
+
);
|
|
5825
|
+
} else {
|
|
5826
|
+
throw new Error(
|
|
5827
|
+
`refusing to overwrite untracked output ${target}; pass --force to replace it`
|
|
5828
|
+
);
|
|
5829
|
+
}
|
|
5623
5830
|
}
|
|
5624
|
-
|
|
5625
|
-
|
|
5831
|
+
log2(
|
|
5832
|
+
` replace ${path14.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
|
|
5833
|
+
);
|
|
5626
5834
|
}
|
|
5627
5835
|
const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
|
|
5628
5836
|
let buf = recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null;
|
|
@@ -5670,7 +5878,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
|
|
|
5670
5878
|
sourceUrl: persistentSources[0]?.url ?? null,
|
|
5671
5879
|
sourceUrls: persistentSources,
|
|
5672
5880
|
downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5673
|
-
error: null
|
|
5881
|
+
error: null,
|
|
5882
|
+
supersededOutputs: []
|
|
5674
5883
|
});
|
|
5675
5884
|
result.downloaded++;
|
|
5676
5885
|
} catch (err) {
|
|
@@ -5841,6 +6050,14 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
|
|
|
5841
6050
|
unsettled ? "warning" : "ok",
|
|
5842
6051
|
unsettled ? `${unsettled} spec(s) are not current: ` + Object.entries(counts).filter(([, n]) => n > 0).map(([state, n]) => `${n} ${state}`).join(", ") : "every resolved spec is current"
|
|
5843
6052
|
);
|
|
6053
|
+
const resumable = resumeActions(specs, lock);
|
|
6054
|
+
add(
|
|
6055
|
+
"next-actions",
|
|
6056
|
+
resumable.length ? "warning" : "ok",
|
|
6057
|
+
resumable.length ? "resume paid work without regenerating: " + resumable.map(
|
|
6058
|
+
(action) => `pixelkiln ${action.command} (${action.keys.length} asset${action.keys.length === 1 ? "" : "s"})`
|
|
6059
|
+
).join(", ") : "no generation stage is waiting for poll, pick, or fetch"
|
|
6060
|
+
);
|
|
5844
6061
|
const quality = plan.items.flatMap((item) => item.quality ? [item.quality] : []);
|
|
5845
6062
|
const qualityPending = quality.filter((item) => item.state !== "approved");
|
|
5846
6063
|
if (quality.length) {
|
|
@@ -6578,10 +6795,15 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
|
|
|
6578
6795
|
onProgress: log2,
|
|
6579
6796
|
assets: reviewAssets,
|
|
6580
6797
|
onReady: (url) => {
|
|
6581
|
-
|
|
6798
|
+
const info = { url, keys: groups.map((group) => group.key) };
|
|
6799
|
+
if (opts.onReady) {
|
|
6800
|
+
opts.onReady(info);
|
|
6801
|
+
} else {
|
|
6802
|
+
log2(`
|
|
6582
6803
|
${groups.length} asset(s) awaiting selection: ${url}`);
|
|
6583
|
-
|
|
6804
|
+
log2(` (leave this running; it exits once you apply)
|
|
6584
6805
|
`);
|
|
6806
|
+
}
|
|
6585
6807
|
},
|
|
6586
6808
|
handleApply: async (body) => {
|
|
6587
6809
|
const { selections } = body;
|
|
@@ -6683,6 +6905,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
|
|
|
6683
6905
|
file: asset.file,
|
|
6684
6906
|
tags: [],
|
|
6685
6907
|
styles: [],
|
|
6908
|
+
providerInputs: {},
|
|
6686
6909
|
...generator === "map" ? { width: asset.width, height: asset.height } : asset.width === asset.height && asset.width !== commonSize ? { size: asset.width } : {}
|
|
6687
6910
|
};
|
|
6688
6911
|
}
|
|
@@ -7745,6 +7968,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
|
|
|
7745
7968
|
file: rel,
|
|
7746
7969
|
tags: ["salvaged"],
|
|
7747
7970
|
styles: [ctx.styleId],
|
|
7971
|
+
providerInputs: {},
|
|
7748
7972
|
...orphan.width === orphan.height ? { size: orphan.width } : { width: orphan.width, height: orphan.height }
|
|
7749
7973
|
};
|
|
7750
7974
|
const durableSource = shouldPersistSourceUrl(orphan.previewUrl) ? orphan.previewUrl : null;
|
|
@@ -8543,6 +8767,17 @@ async function checkQualityBaseline(baselinePath) {
|
|
|
8543
8767
|
|
|
8544
8768
|
// src/cli.ts
|
|
8545
8769
|
var log = (msg = "") => console.log(msg);
|
|
8770
|
+
function announceReviewReady(info, stdout = process.stdout, stderr = process.stderr) {
|
|
8771
|
+
const stream = stdout.isTTY ? stdout : stderr;
|
|
8772
|
+
const count = info.keys.length;
|
|
8773
|
+
stream.write(
|
|
8774
|
+
`
|
|
8775
|
+
${count} asset${count === 1 ? "" : "s"} awaiting selection: ${info.url}
|
|
8776
|
+
(leave this running; it exits once you apply)
|
|
8777
|
+
|
|
8778
|
+
`
|
|
8779
|
+
);
|
|
8780
|
+
}
|
|
8546
8781
|
async function provenanceFile(id, file) {
|
|
8547
8782
|
const absolute = path24.resolve(file);
|
|
8548
8783
|
return {
|
|
@@ -8900,7 +9135,7 @@ Options
|
|
|
8900
9135
|
--max-colors <n> audit: maximum distinct opaque colors
|
|
8901
9136
|
--sigma <n> audit: relative outlier cutoff (default: 1.5)
|
|
8902
9137
|
--palette <hexes> refine: final comma-separated #rrggbb colors (repeatable)
|
|
8903
|
-
--fixer-python <path> refine: Python with Pixel Art Fixer installed
|
|
9138
|
+
--fixer-python <path> refine: override Python with Pixel Art Fixer installed
|
|
8904
9139
|
--fixer-revision <sha> refine: Pixel Art Fixer revision to record
|
|
8905
9140
|
--min-grid-confidence <level> refine: high (default), medium, or low
|
|
8906
9141
|
--reviewer <name> refine approve: human reviewer recorded in provenance
|
|
@@ -8914,7 +9149,7 @@ Options
|
|
|
8914
9149
|
--budget <n|provider=n> Refuse to exceed one provider ceiling; repeat keyed budgets
|
|
8915
9150
|
for a mixed-provider run
|
|
8916
9151
|
--provider <id> Choose the account for balance/adopt/salvage/purge in a mixed manifest
|
|
8917
|
-
--force Regenerate
|
|
9152
|
+
--force Regenerate, replace a fetch destination, or rebuild managed output
|
|
8918
9153
|
--dry-run Never spend; doctor also skips provider connectivity
|
|
8919
9154
|
--all salvage --dry-run: list every unclaimed object, not just the first 30
|
|
8920
9155
|
--json Machine-readable output where supported, including quality checks
|
|
@@ -8997,6 +9232,17 @@ function printPlan(plan) {
|
|
|
8997
9232
|
}
|
|
8998
9233
|
}
|
|
8999
9234
|
}
|
|
9235
|
+
function printResumeActions(specs, lock) {
|
|
9236
|
+
const actions = resumeActions(specs, lock);
|
|
9237
|
+
for (const action of actions) {
|
|
9238
|
+
const shown = action.keys.slice(0, 3).join(", ");
|
|
9239
|
+
const more = action.keys.length > 3 ? `, +${action.keys.length - 3} more` : "";
|
|
9240
|
+
log(
|
|
9241
|
+
` next: pixelkiln ${action.command} \u2014 ${action.keys.length} asset${action.keys.length === 1 ? "" : "s"} (${shown}${more})`
|
|
9242
|
+
);
|
|
9243
|
+
}
|
|
9244
|
+
return actions.length;
|
|
9245
|
+
}
|
|
9000
9246
|
function manifestProviderIds(manifest) {
|
|
9001
9247
|
return [...new Set(
|
|
9002
9248
|
Object.values(manifest.styles).map((style) => style.provider ?? manifest.provider)
|
|
@@ -10395,7 +10641,10 @@ async function main() {
|
|
|
10395
10641
|
${total.completed} ready \xB7 ${total.review} awaiting selection \xB7 ${total.failed} failed`
|
|
10396
10642
|
);
|
|
10397
10643
|
if (total.failed || total.stillRunning) process.exitCode = 1;
|
|
10398
|
-
if (args.command === "poll")
|
|
10644
|
+
if (args.command === "poll") {
|
|
10645
|
+
printResumeActions(specs, lock);
|
|
10646
|
+
return;
|
|
10647
|
+
}
|
|
10399
10648
|
}
|
|
10400
10649
|
if (args.command === "pick" || args.command === "gen") {
|
|
10401
10650
|
const total = { selected: 0, skipped: 0 };
|
|
@@ -10404,6 +10653,7 @@ async function main() {
|
|
|
10404
10653
|
port: args.port,
|
|
10405
10654
|
open: !args.noOpen,
|
|
10406
10655
|
onProgress: log,
|
|
10656
|
+
onReady: announceReviewReady,
|
|
10407
10657
|
keys: providerSpecsForRun.map((spec) => lockKey(spec.styleId, spec.assetId)),
|
|
10408
10658
|
specs: providerSpecsForRun
|
|
10409
10659
|
});
|
|
@@ -10417,7 +10667,10 @@ async function main() {
|
|
|
10417
10667
|
selected ${total.selected}, left in review ${total.skipped}`);
|
|
10418
10668
|
}
|
|
10419
10669
|
if (args.command === "gen" && total.skipped) process.exitCode = 1;
|
|
10420
|
-
if (args.command === "pick")
|
|
10670
|
+
if (args.command === "pick") {
|
|
10671
|
+
printResumeActions(specs, lock);
|
|
10672
|
+
return;
|
|
10673
|
+
}
|
|
10421
10674
|
}
|
|
10422
10675
|
if (args.command === "fetch" || args.command === "restore" || args.command === "gen") {
|
|
10423
10676
|
log(`
|
|
@@ -10427,7 +10680,8 @@ async function main() {
|
|
|
10427
10680
|
const groupProvider = providerFor(providerId);
|
|
10428
10681
|
const res = await fetchAssets(groupProvider, providerSpecsForRun, lock, args.lock, {
|
|
10429
10682
|
onProgress: log,
|
|
10430
|
-
repair: args.command === "restore"
|
|
10683
|
+
repair: args.command === "restore",
|
|
10684
|
+
force: args.force
|
|
10431
10685
|
});
|
|
10432
10686
|
total.downloaded += res.downloaded;
|
|
10433
10687
|
total.skipped += res.skipped;
|
|
@@ -10446,10 +10700,22 @@ async function main() {
|
|
|
10446
10700
|
if (args.tag) log(` tagged ${total.tagged} object(s) upstream`);
|
|
10447
10701
|
await saveLock(args.lock, lock);
|
|
10448
10702
|
log(` lockfile written: ${args.lock}`);
|
|
10703
|
+
const resumeActionCount = printResumeActions(specs, lock);
|
|
10449
10704
|
if (args.command === "gen" && specs.some((spec) => spec.quality) && !total.failed) {
|
|
10450
|
-
|
|
10705
|
+
const quality = (await Promise.all(
|
|
10706
|
+
specs.filter((spec) => spec.quality).map((spec) => inspectQualityProfile(spec, lock))
|
|
10707
|
+
)).filter((item) => item !== null);
|
|
10708
|
+
const blocked = quality.filter((item) => item.state === "blocked");
|
|
10709
|
+
if (blocked.length) {
|
|
10710
|
+
log(
|
|
10711
|
+
`
|
|
10712
|
+
${blocked.length} quality source${blocked.length === 1 ? " is" : "s are"} not ready; ` + (resumeActionCount ? "finish the resume steps above before refining." : "run `pixelkiln plan` for the blocking reason before refining.")
|
|
10713
|
+
);
|
|
10714
|
+
} else {
|
|
10715
|
+
log(`
|
|
10451
10716
|
Raw provider output is ready. Run \`pixelkiln refine\` to build the configured quality output.`);
|
|
10452
|
-
|
|
10717
|
+
log(` Packaging stays blocked until each refined PNG has a current human approval.`);
|
|
10718
|
+
}
|
|
10453
10719
|
}
|
|
10454
10720
|
return;
|
|
10455
10721
|
}
|
|
@@ -10463,6 +10729,7 @@ main().catch((err) => {
|
|
|
10463
10729
|
});
|
|
10464
10730
|
export {
|
|
10465
10731
|
COMMANDS,
|
|
10732
|
+
announceReviewReady,
|
|
10466
10733
|
parseArgs
|
|
10467
10734
|
};
|
|
10468
10735
|
//# sourceMappingURL=cli.js.map
|