pixelkiln 0.20.1 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +118 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +118 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -3
- package/dist/index.d.ts +27 -3
- package/dist/index.js +118 -26
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +11 -3
- package/docs/CLI.md +5 -1
- package/docs/COMFYUI.md +8 -4
- package/docs/LIBRARY.md +10 -2
- package/docs/RECOVERY.md +10 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -558,6 +558,8 @@ var LockEntrySchema = import_zod.z.object({
|
|
|
558
558
|
).optional(),
|
|
559
559
|
/** Set at submit time, before the request is awaited, so a crash is recoverable. */
|
|
560
560
|
jobId: import_zod.z.string().nullable().default(null),
|
|
561
|
+
/** False only when a provider checkpoint says more requests remain. */
|
|
562
|
+
submissionComplete: import_zod.z.boolean().optional(),
|
|
561
563
|
/** For `1dir`: the multi-candidate parent object awaiting selection. */
|
|
562
564
|
reviewObjectId: import_zod.z.string().nullable().default(null),
|
|
563
565
|
/** The chosen candidate's own object id, once selected. */
|
|
@@ -2990,7 +2992,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
2990
2992
|
rateLimit() {
|
|
2991
2993
|
return { spacingMs: 0, maxInFlight: 1 };
|
|
2992
2994
|
}
|
|
2993
|
-
async submit(spec, styleImages) {
|
|
2995
|
+
async submit(spec, styleImages, context) {
|
|
2994
2996
|
this.validate(spec, styleImages);
|
|
2995
2997
|
const options = resolvedOptions(spec);
|
|
2996
2998
|
const uploadedImages = /* @__PURE__ */ new Map();
|
|
@@ -3032,22 +3034,35 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
3032
3034
|
if (spec.generator === "frames") {
|
|
3033
3035
|
const frames = options.frames;
|
|
3034
3036
|
const values = spec.providerInputs[frames.vary];
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
+
let promptIds = [];
|
|
3038
|
+
if (context?.previousJobId) {
|
|
3039
|
+
const previous = decodeFrameSetJob(context.previousJobId);
|
|
3040
|
+
if (previous.outputNodeId !== options.outputNodeId || previous.expectedCount !== values.length) {
|
|
3041
|
+
throw new Error("Saved ComfyUI frame checkpoint does not match the current frame set");
|
|
3042
|
+
}
|
|
3043
|
+
promptIds = [...previous.promptIds];
|
|
3044
|
+
}
|
|
3045
|
+
for (let index = promptIds.length; index < values.length; index++) {
|
|
3037
3046
|
promptIds.push(await this.client.submit(await buildWorkflow(index)));
|
|
3047
|
+
const frameJob2 = {
|
|
3048
|
+
expectedCount: values.length,
|
|
3049
|
+
promptIds: [...promptIds],
|
|
3050
|
+
outputNodeId: options.outputNodeId
|
|
3051
|
+
};
|
|
3052
|
+
await context?.checkpoint({
|
|
3053
|
+
jobId: encodeFrameSetJob(frameJob2),
|
|
3054
|
+
metadata: comfyFrameSubmissionMetadata(spec, frameJob2),
|
|
3055
|
+
complete: promptIds.length === values.length
|
|
3056
|
+
});
|
|
3038
3057
|
}
|
|
3058
|
+
const frameJob = {
|
|
3059
|
+
expectedCount: values.length,
|
|
3060
|
+
promptIds,
|
|
3061
|
+
outputNodeId: options.outputNodeId
|
|
3062
|
+
};
|
|
3039
3063
|
return {
|
|
3040
|
-
jobId: encodeFrameSetJob(
|
|
3041
|
-
metadata:
|
|
3042
|
-
inputs,
|
|
3043
|
-
frameSet: {
|
|
3044
|
-
count: values.length,
|
|
3045
|
-
vary: frames.vary,
|
|
3046
|
-
seedStep: frames.seedStep,
|
|
3047
|
-
fps: spec.quality?.fps ?? 12,
|
|
3048
|
-
promptIds
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3064
|
+
jobId: encodeFrameSetJob(frameJob),
|
|
3065
|
+
metadata: comfyFrameSubmissionMetadata(spec, frameJob)
|
|
3051
3066
|
};
|
|
3052
3067
|
}
|
|
3053
3068
|
const workflow = await buildWorkflow();
|
|
@@ -3104,6 +3119,9 @@ var ComfyUIProvider = class _ComfyUIProvider {
|
|
|
3104
3119
|
async poll(jobId, generator, context) {
|
|
3105
3120
|
if (generator === "frames") {
|
|
3106
3121
|
const frameJob = decodeFrameSetJob(jobId);
|
|
3122
|
+
if (frameJob.promptIds.length < frameJob.expectedCount) {
|
|
3123
|
+
return { status: "processing" };
|
|
3124
|
+
}
|
|
3107
3125
|
const images2 = [];
|
|
3108
3126
|
for (let index = 0; index < frameJob.promptIds.length; index++) {
|
|
3109
3127
|
const promptId2 = frameJob.promptIds[index];
|
|
@@ -3376,6 +3394,19 @@ function comfyFrameMetadata(spec, job, images, fps) {
|
|
|
3376
3394
|
}
|
|
3377
3395
|
};
|
|
3378
3396
|
}
|
|
3397
|
+
function comfyFrameSubmissionMetadata(spec, job) {
|
|
3398
|
+
const options = resolvedOptions(spec);
|
|
3399
|
+
return {
|
|
3400
|
+
inputs: providerInputProvenance(spec.providerInputs),
|
|
3401
|
+
frameSet: {
|
|
3402
|
+
count: job.expectedCount,
|
|
3403
|
+
vary: options.frames.vary,
|
|
3404
|
+
seedStep: options.frames.seedStep,
|
|
3405
|
+
fps: spec.quality?.fps ?? 12,
|
|
3406
|
+
promptIds: job.promptIds
|
|
3407
|
+
}
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3379
3410
|
function isImageBinding(workflow, binding) {
|
|
3380
3411
|
const node = workflow[binding.nodeId];
|
|
3381
3412
|
return binding.input === "image" && (node?.class_type === "LoadImage" || node?.class_type === "LoadImageMask");
|
|
@@ -3431,10 +3462,18 @@ function decodeFrameSetJob(jobId) {
|
|
|
3431
3462
|
} catch {
|
|
3432
3463
|
throw new Error(`Invalid ComfyUI frame-set job id "${jobId}"`);
|
|
3433
3464
|
}
|
|
3434
|
-
if (!isObject(value) ||
|
|
3465
|
+
if (!isObject(value) || !Array.isArray(value.promptIds)) {
|
|
3466
|
+
throw new Error(`Invalid ComfyUI frame-set job id "${jobId}"`);
|
|
3467
|
+
}
|
|
3468
|
+
const expectedCount = value.expectedCount === void 0 ? value.promptIds.length : value.expectedCount;
|
|
3469
|
+
if (typeof value.outputNodeId !== "string" || !value.outputNodeId || typeof expectedCount !== "number" || !Number.isSafeInteger(expectedCount) || expectedCount < 2 || expectedCount > 64 || value.promptIds.length < 1 || value.promptIds.length > expectedCount || !value.promptIds.every((id) => typeof id === "string" && id.length > 0)) {
|
|
3435
3470
|
throw new Error(`Invalid ComfyUI frame-set job id "${jobId}"`);
|
|
3436
3471
|
}
|
|
3437
|
-
return {
|
|
3472
|
+
return {
|
|
3473
|
+
expectedCount,
|
|
3474
|
+
outputNodeId: value.outputNodeId,
|
|
3475
|
+
promptIds: value.promptIds
|
|
3476
|
+
};
|
|
3438
3477
|
}
|
|
3439
3478
|
function decodeJob(jobId) {
|
|
3440
3479
|
const split = jobId.lastIndexOf("#");
|
|
@@ -6381,6 +6420,7 @@ function resumeActions(specs, lock) {
|
|
|
6381
6420
|
const key = lockKey(spec.styleId, spec.assetId);
|
|
6382
6421
|
const entry = lock.entries[key];
|
|
6383
6422
|
if (!entry || entry.specHash !== spec.specHash) continue;
|
|
6423
|
+
if (entry.submissionComplete === false) continue;
|
|
6384
6424
|
const command = resumeCommandForStatus(entry.status);
|
|
6385
6425
|
if (!command) continue;
|
|
6386
6426
|
if (command === "poll" && !entry.jobId) continue;
|
|
@@ -6431,10 +6471,13 @@ async function buildPlan(specs, lock, opts = {}) {
|
|
|
6431
6471
|
reason = `${entry.error ?? "download failed"}; run pixelkiln fetch${force} (no generation cost)`;
|
|
6432
6472
|
} else if (entry.status === "failed") {
|
|
6433
6473
|
state = "failed";
|
|
6434
|
-
reason = entry.error ?? "previous attempt failed";
|
|
6474
|
+
reason = entry.jobId && entry.submissionComplete === false ? `${entry.error ?? "submission interrupted"}; saved checkpoint will resume` : entry.error ?? "previous attempt failed";
|
|
6435
6475
|
} else if (entry.status === "selected") {
|
|
6436
6476
|
state = "recoverable";
|
|
6437
6477
|
reason = "provider output is selected; run pixelkiln fetch (no generation cost)";
|
|
6478
|
+
} else if ((entry.status === "pending" || entry.status === "processing") && entry.jobId && entry.submissionComplete === false) {
|
|
6479
|
+
state = "failed";
|
|
6480
|
+
reason = "submission stopped after a saved checkpoint; rerun pixelkiln submit to resume";
|
|
6438
6481
|
} else if (entry.status === "pending" || entry.status === "processing") {
|
|
6439
6482
|
state = "in-flight";
|
|
6440
6483
|
reason = entry.jobId ? "awaiting processing; run pixelkiln poll" : "submission state has no job id; run pixelkiln doctor before retrying";
|
|
@@ -7682,6 +7725,11 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
7682
7725
|
if (since < spacing) await sleep2(spacing - since);
|
|
7683
7726
|
await requireRevisionReady(spec, lock);
|
|
7684
7727
|
const previousEntry = lock.entries[key];
|
|
7728
|
+
const resumesCheckpoint = Boolean(
|
|
7729
|
+
previousEntry?.specHash === spec.specHash && previousEntry.provider === provider.id && previousEntry.generator === spec.generator && previousEntry.submissionComplete === false && previousEntry.jobId
|
|
7730
|
+
);
|
|
7731
|
+
const previousJobId = resumesCheckpoint ? previousEntry.jobId : void 0;
|
|
7732
|
+
const previousMetadata = resumesCheckpoint ? previousEntry.providerMetadata[provider.id] : void 0;
|
|
7685
7733
|
const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
|
|
7686
7734
|
upsert(lock, key, {
|
|
7687
7735
|
styleId: spec.styleId,
|
|
@@ -7700,7 +7748,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
7700
7748
|
...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
|
|
7701
7749
|
} : null,
|
|
7702
7750
|
status: "pending",
|
|
7703
|
-
jobId: null,
|
|
7751
|
+
jobId: previousJobId ?? null,
|
|
7752
|
+
...previousJobId ? { submissionComplete: false } : { submissionComplete: void 0 },
|
|
7704
7753
|
objectId: null,
|
|
7705
7754
|
reviewObjectId: null,
|
|
7706
7755
|
candidateIndex: null,
|
|
@@ -7709,10 +7758,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
7709
7758
|
// Keep the old ownership proof while new bytes are pending. Fetch may
|
|
7710
7759
|
// replace that file only while its hash still matches this record.
|
|
7711
7760
|
supersededOutputs,
|
|
7712
|
-
providerMetadata: {},
|
|
7761
|
+
providerMetadata: resumesCheckpoint ? previousEntry.providerMetadata : {},
|
|
7713
7762
|
sourceUrl: null,
|
|
7714
7763
|
sourceUrls: [],
|
|
7715
|
-
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7764
|
+
submittedAt: resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
7716
7765
|
cost: estimate.amount,
|
|
7717
7766
|
costUnit: estimate.unit,
|
|
7718
7767
|
// Persist routing before the request starts. If the process stops while
|
|
@@ -7725,9 +7774,32 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
7725
7774
|
lastSubmitAt = Date.now();
|
|
7726
7775
|
try {
|
|
7727
7776
|
const refs = styleImages.get(spec.styleId) ?? [];
|
|
7728
|
-
const { jobId, metadata } = await provider.submit(spec, refs
|
|
7777
|
+
const { jobId, metadata } = await provider.submit(spec, refs, {
|
|
7778
|
+
...previousJobId ? { previousJobId } : {},
|
|
7779
|
+
...previousMetadata ? { previousMetadata } : {},
|
|
7780
|
+
checkpoint: async (checkpoint) => {
|
|
7781
|
+
if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
|
|
7782
|
+
const entry = lock.entries[key];
|
|
7783
|
+
upsert(lock, key, {
|
|
7784
|
+
jobId: checkpoint.jobId,
|
|
7785
|
+
submissionComplete: checkpoint.complete,
|
|
7786
|
+
reviewObjectId: checkpoint.complete && (spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature) ? checkpoint.jobId : null,
|
|
7787
|
+
status: checkpoint.complete ? "processing" : "pending",
|
|
7788
|
+
error: null,
|
|
7789
|
+
providerMetadata: checkpoint.metadata ? {
|
|
7790
|
+
...entry.providerMetadata,
|
|
7791
|
+
[provider.id]: {
|
|
7792
|
+
...entry.providerMetadata[provider.id],
|
|
7793
|
+
...checkpoint.metadata
|
|
7794
|
+
}
|
|
7795
|
+
} : entry.providerMetadata
|
|
7796
|
+
});
|
|
7797
|
+
await saveLock(lockPath, lock);
|
|
7798
|
+
}
|
|
7799
|
+
});
|
|
7729
7800
|
upsert(lock, key, {
|
|
7730
7801
|
jobId,
|
|
7802
|
+
submissionComplete: true,
|
|
7731
7803
|
// A multi-candidate generator routes through review; record the parent
|
|
7732
7804
|
// so `pick` knows where to look.
|
|
7733
7805
|
reviewObjectId: spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
|
|
@@ -7741,10 +7813,23 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
|
|
|
7741
7813
|
` ${key} \u2192 ${jobId} (${spec.width}x${spec.height}` + (estimate.candidates > 1 ? `, ${estimate.candidates} ${spec.tileFeature ? "outputs" : "candidates"}` : "") + `, ${estimate.amount})`
|
|
7742
7814
|
);
|
|
7743
7815
|
} catch (err) {
|
|
7744
|
-
failed++;
|
|
7745
7816
|
const message5 = err instanceof Error ? err.message : String(err);
|
|
7746
|
-
|
|
7747
|
-
|
|
7817
|
+
const entry = lock.entries[key];
|
|
7818
|
+
if (entry.jobId && entry.submissionComplete === true) {
|
|
7819
|
+
upsert(lock, key, { status: "processing", error: null });
|
|
7820
|
+
inFlight.set(entry.jobId, spec);
|
|
7821
|
+
submitted++;
|
|
7822
|
+
spent += estimate.amount;
|
|
7823
|
+
log(` ${key} \u2192 ${entry.jobId} (recovered from completed checkpoint)`);
|
|
7824
|
+
} else {
|
|
7825
|
+
failed++;
|
|
7826
|
+
upsert(lock, key, {
|
|
7827
|
+
status: "failed",
|
|
7828
|
+
error: message5,
|
|
7829
|
+
cost: entry.jobId ? estimate.amount : 0
|
|
7830
|
+
});
|
|
7831
|
+
log(` FAILED ${key}: ${message5}`);
|
|
7832
|
+
}
|
|
7748
7833
|
}
|
|
7749
7834
|
await saveLock(lockPath, lock);
|
|
7750
7835
|
}
|
|
@@ -7762,12 +7847,19 @@ async function poll(provider, lock, lockPath, opts = {}) {
|
|
|
7762
7847
|
const specByKey = new Map((opts.specs ?? []).map((s) => [lockKey(s.styleId, s.assetId), s]));
|
|
7763
7848
|
const selectedKeys = opts.specs ? new Set(specByKey.keys()) : null;
|
|
7764
7849
|
const result = { review: 0, completed: 0, failed: 0, stillRunning: 0 };
|
|
7850
|
+
const incompleteSubmissions = Object.entries(lock.entries).filter(
|
|
7851
|
+
([key, entry]) => entry.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && entry.jobId && entry.submissionComplete === false && (entry.status === "pending" || entry.status === "processing")
|
|
7852
|
+
);
|
|
7853
|
+
for (const [key] of incompleteSubmissions) {
|
|
7854
|
+
log(` incomplete submission ${key}; rerun \`pixelkiln submit\` to resume`);
|
|
7855
|
+
}
|
|
7856
|
+
result.stillRunning = incompleteSubmissions.length;
|
|
7765
7857
|
const pending = () => Object.entries(lock.entries).filter(
|
|
7766
|
-
([key, e]) => e.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && e.jobId && (e.status === "pending" || e.status === "processing")
|
|
7858
|
+
([key, e]) => e.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && e.jobId && e.submissionComplete !== false && (e.status === "pending" || e.status === "processing")
|
|
7767
7859
|
);
|
|
7768
7860
|
while (pending().length > 0) {
|
|
7769
7861
|
if (Date.now() - started > timeout) {
|
|
7770
|
-
result.stillRunning
|
|
7862
|
+
result.stillRunning += pending().length;
|
|
7771
7863
|
log(` timed out with ${result.stillRunning} job(s) still running \u2014 re-run \`poll\` to resume`);
|
|
7772
7864
|
break;
|
|
7773
7865
|
}
|