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 CHANGED
@@ -919,7 +919,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
919
919
  rateLimit() {
920
920
  return { spacingMs: 0, maxInFlight: 1 };
921
921
  }
922
- async submit(spec, styleImages) {
922
+ async submit(spec, styleImages, context) {
923
923
  this.validate(spec, styleImages);
924
924
  const options = resolvedOptions(spec);
925
925
  const uploadedImages = /* @__PURE__ */ new Map();
@@ -961,22 +961,35 @@ var ComfyUIProvider = class _ComfyUIProvider {
961
961
  if (spec.generator === "frames") {
962
962
  const frames = options.frames;
963
963
  const values = spec.providerInputs[frames.vary];
964
- const promptIds = [];
965
- for (let index = 0; index < values.length; index++) {
964
+ let promptIds = [];
965
+ if (context?.previousJobId) {
966
+ const previous = decodeFrameSetJob(context.previousJobId);
967
+ if (previous.outputNodeId !== options.outputNodeId || previous.expectedCount !== values.length) {
968
+ throw new Error("Saved ComfyUI frame checkpoint does not match the current frame set");
969
+ }
970
+ promptIds = [...previous.promptIds];
971
+ }
972
+ for (let index = promptIds.length; index < values.length; index++) {
966
973
  promptIds.push(await this.client.submit(await buildWorkflow(index)));
974
+ const frameJob2 = {
975
+ expectedCount: values.length,
976
+ promptIds: [...promptIds],
977
+ outputNodeId: options.outputNodeId
978
+ };
979
+ await context?.checkpoint({
980
+ jobId: encodeFrameSetJob(frameJob2),
981
+ metadata: comfyFrameSubmissionMetadata(spec, frameJob2),
982
+ complete: promptIds.length === values.length
983
+ });
967
984
  }
985
+ const frameJob = {
986
+ expectedCount: values.length,
987
+ promptIds,
988
+ outputNodeId: options.outputNodeId
989
+ };
968
990
  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
- }
991
+ jobId: encodeFrameSetJob(frameJob),
992
+ metadata: comfyFrameSubmissionMetadata(spec, frameJob)
980
993
  };
981
994
  }
982
995
  const workflow = await buildWorkflow();
@@ -1033,6 +1046,9 @@ var ComfyUIProvider = class _ComfyUIProvider {
1033
1046
  async poll(jobId, generator, context) {
1034
1047
  if (generator === "frames") {
1035
1048
  const frameJob = decodeFrameSetJob(jobId);
1049
+ if (frameJob.promptIds.length < frameJob.expectedCount) {
1050
+ return { status: "processing" };
1051
+ }
1036
1052
  const images2 = [];
1037
1053
  for (let index = 0; index < frameJob.promptIds.length; index++) {
1038
1054
  const promptId2 = frameJob.promptIds[index];
@@ -1305,6 +1321,19 @@ function comfyFrameMetadata(spec, job, images, fps) {
1305
1321
  }
1306
1322
  };
1307
1323
  }
1324
+ function comfyFrameSubmissionMetadata(spec, job) {
1325
+ const options = resolvedOptions(spec);
1326
+ return {
1327
+ inputs: providerInputProvenance(spec.providerInputs),
1328
+ frameSet: {
1329
+ count: job.expectedCount,
1330
+ vary: options.frames.vary,
1331
+ seedStep: options.frames.seedStep,
1332
+ fps: spec.quality?.fps ?? 12,
1333
+ promptIds: job.promptIds
1334
+ }
1335
+ };
1336
+ }
1308
1337
  function isImageBinding(workflow, binding) {
1309
1338
  const node = workflow[binding.nodeId];
1310
1339
  return binding.input === "image" && (node?.class_type === "LoadImage" || node?.class_type === "LoadImageMask");
@@ -1360,10 +1389,18 @@ function decodeFrameSetJob(jobId) {
1360
1389
  } catch {
1361
1390
  throw new Error(`Invalid ComfyUI frame-set job id "${jobId}"`);
1362
1391
  }
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)) {
1392
+ if (!isObject(value) || !Array.isArray(value.promptIds)) {
1364
1393
  throw new Error(`Invalid ComfyUI frame-set job id "${jobId}"`);
1365
1394
  }
1366
- return { outputNodeId: value.outputNodeId, promptIds: value.promptIds };
1395
+ const expectedCount = value.expectedCount === void 0 ? value.promptIds.length : value.expectedCount;
1396
+ 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)) {
1397
+ throw new Error(`Invalid ComfyUI frame-set job id "${jobId}"`);
1398
+ }
1399
+ return {
1400
+ expectedCount,
1401
+ outputNodeId: value.outputNodeId,
1402
+ promptIds: value.promptIds
1403
+ };
1367
1404
  }
1368
1405
  function decodeJob(jobId) {
1369
1406
  const split = jobId.lastIndexOf("#");
@@ -2146,6 +2183,8 @@ var LockEntrySchema = z2.object({
2146
2183
  ).optional(),
2147
2184
  /** Set at submit time, before the request is awaited, so a crash is recoverable. */
2148
2185
  jobId: z2.string().nullable().default(null),
2186
+ /** False only when a provider checkpoint says more requests remain. */
2187
+ submissionComplete: z2.boolean().optional(),
2149
2188
  /** For `1dir`: the multi-candidate parent object awaiting selection. */
2150
2189
  reviewObjectId: z2.string().nullable().default(null),
2151
2190
  /** The chosen candidate's own object id, once selected. */
@@ -5916,6 +5955,7 @@ function resumeActions(specs, lock) {
5916
5955
  const key = lockKey(spec.styleId, spec.assetId);
5917
5956
  const entry = lock.entries[key];
5918
5957
  if (!entry || entry.specHash !== spec.specHash) continue;
5958
+ if (entry.submissionComplete === false) continue;
5919
5959
  const command = resumeCommandForStatus(entry.status);
5920
5960
  if (!command) continue;
5921
5961
  if (command === "poll" && !entry.jobId) continue;
@@ -5966,10 +6006,13 @@ async function buildPlan(specs, lock, opts = {}) {
5966
6006
  reason = `${entry.error ?? "download failed"}; run pixelkiln fetch${force} (no generation cost)`;
5967
6007
  } else if (entry.status === "failed") {
5968
6008
  state = "failed";
5969
- reason = entry.error ?? "previous attempt failed";
6009
+ reason = entry.jobId && entry.submissionComplete === false ? `${entry.error ?? "submission interrupted"}; saved checkpoint will resume` : entry.error ?? "previous attempt failed";
5970
6010
  } else if (entry.status === "selected") {
5971
6011
  state = "recoverable";
5972
6012
  reason = "provider output is selected; run pixelkiln fetch (no generation cost)";
6013
+ } else if ((entry.status === "pending" || entry.status === "processing") && entry.jobId && entry.submissionComplete === false) {
6014
+ state = "failed";
6015
+ reason = "submission stopped after a saved checkpoint; rerun pixelkiln submit to resume";
5973
6016
  } else if (entry.status === "pending" || entry.status === "processing") {
5974
6017
  state = "in-flight";
5975
6018
  reason = entry.jobId ? "awaiting processing; run pixelkiln poll" : "submission state has no job id; run pixelkiln doctor before retrying";
@@ -6113,6 +6156,11 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
6113
6156
  if (since < spacing) await sleep2(spacing - since);
6114
6157
  await requireRevisionReady(spec, lock);
6115
6158
  const previousEntry = lock.entries[key];
6159
+ const resumesCheckpoint = Boolean(
6160
+ previousEntry?.specHash === spec.specHash && previousEntry.provider === provider.id && previousEntry.generator === spec.generator && previousEntry.submissionComplete === false && previousEntry.jobId
6161
+ );
6162
+ const previousJobId = resumesCheckpoint ? previousEntry.jobId : void 0;
6163
+ const previousMetadata = resumesCheckpoint ? previousEntry.providerMetadata[provider.id] : void 0;
6116
6164
  const supersededOutputs = previousEntry?.outputs.length ? previousEntry.outputs : previousEntry?.supersededOutputs ?? [];
6117
6165
  upsert(lock, key, {
6118
6166
  styleId: spec.styleId,
@@ -6131,7 +6179,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
6131
6179
  ...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
6132
6180
  } : null,
6133
6181
  status: "pending",
6134
- jobId: null,
6182
+ jobId: previousJobId ?? null,
6183
+ ...previousJobId ? { submissionComplete: false } : { submissionComplete: void 0 },
6135
6184
  objectId: null,
6136
6185
  reviewObjectId: null,
6137
6186
  candidateIndex: null,
@@ -6140,10 +6189,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
6140
6189
  // Keep the old ownership proof while new bytes are pending. Fetch may
6141
6190
  // replace that file only while its hash still matches this record.
6142
6191
  supersededOutputs,
6143
- providerMetadata: {},
6192
+ providerMetadata: resumesCheckpoint ? previousEntry.providerMetadata : {},
6144
6193
  sourceUrl: null,
6145
6194
  sourceUrls: [],
6146
- submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
6195
+ submittedAt: resumesCheckpoint ? previousEntry.submittedAt : (/* @__PURE__ */ new Date()).toISOString(),
6147
6196
  cost: estimate.amount,
6148
6197
  costUnit: estimate.unit,
6149
6198
  // Persist routing before the request starts. If the process stops while
@@ -6156,9 +6205,32 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
6156
6205
  lastSubmitAt = Date.now();
6157
6206
  try {
6158
6207
  const refs = styleImages.get(spec.styleId) ?? [];
6159
- const { jobId, metadata } = await provider.submit(spec, refs);
6208
+ const { jobId, metadata } = await provider.submit(spec, refs, {
6209
+ ...previousJobId ? { previousJobId } : {},
6210
+ ...previousMetadata ? { previousMetadata } : {},
6211
+ checkpoint: async (checkpoint) => {
6212
+ if (!checkpoint.jobId) throw new Error("Provider checkpoint returned an empty job id");
6213
+ const entry = lock.entries[key];
6214
+ upsert(lock, key, {
6215
+ jobId: checkpoint.jobId,
6216
+ submissionComplete: checkpoint.complete,
6217
+ reviewObjectId: checkpoint.complete && (spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature) ? checkpoint.jobId : null,
6218
+ status: checkpoint.complete ? "processing" : "pending",
6219
+ error: null,
6220
+ providerMetadata: checkpoint.metadata ? {
6221
+ ...entry.providerMetadata,
6222
+ [provider.id]: {
6223
+ ...entry.providerMetadata[provider.id],
6224
+ ...checkpoint.metadata
6225
+ }
6226
+ } : entry.providerMetadata
6227
+ });
6228
+ await saveLock(lockPath, lock);
6229
+ }
6230
+ });
6160
6231
  upsert(lock, key, {
6161
6232
  jobId,
6233
+ submissionComplete: true,
6162
6234
  // A multi-candidate generator routes through review; record the parent
6163
6235
  // so `pick` knows where to look.
6164
6236
  reviewObjectId: spec.generator === "frames" || estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
@@ -6172,10 +6244,23 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
6172
6244
  ` ${key} \u2192 ${jobId} (${spec.width}x${spec.height}` + (estimate.candidates > 1 ? `, ${estimate.candidates} ${spec.tileFeature ? "outputs" : "candidates"}` : "") + `, ${estimate.amount})`
6173
6245
  );
6174
6246
  } catch (err) {
6175
- failed++;
6176
6247
  const message5 = err instanceof Error ? err.message : String(err);
6177
- upsert(lock, key, { status: "failed", error: message5, cost: 0 });
6178
- log2(` FAILED ${key}: ${message5}`);
6248
+ const entry = lock.entries[key];
6249
+ if (entry.jobId && entry.submissionComplete === true) {
6250
+ upsert(lock, key, { status: "processing", error: null });
6251
+ inFlight.set(entry.jobId, spec);
6252
+ submitted++;
6253
+ spent += estimate.amount;
6254
+ log2(` ${key} \u2192 ${entry.jobId} (recovered from completed checkpoint)`);
6255
+ } else {
6256
+ failed++;
6257
+ upsert(lock, key, {
6258
+ status: "failed",
6259
+ error: message5,
6260
+ cost: entry.jobId ? estimate.amount : 0
6261
+ });
6262
+ log2(` FAILED ${key}: ${message5}`);
6263
+ }
6179
6264
  }
6180
6265
  await saveLock(lockPath, lock);
6181
6266
  }
@@ -6193,12 +6278,19 @@ async function poll(provider, lock, lockPath, opts = {}) {
6193
6278
  const specByKey = new Map((opts.specs ?? []).map((s) => [lockKey(s.styleId, s.assetId), s]));
6194
6279
  const selectedKeys = opts.specs ? new Set(specByKey.keys()) : null;
6195
6280
  const result = { review: 0, completed: 0, failed: 0, stillRunning: 0 };
6281
+ const incompleteSubmissions = Object.entries(lock.entries).filter(
6282
+ ([key, entry]) => entry.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && entry.jobId && entry.submissionComplete === false && (entry.status === "pending" || entry.status === "processing")
6283
+ );
6284
+ for (const [key] of incompleteSubmissions) {
6285
+ log2(` incomplete submission ${key}; rerun \`pixelkiln submit\` to resume`);
6286
+ }
6287
+ result.stillRunning = incompleteSubmissions.length;
6196
6288
  const pending = () => Object.entries(lock.entries).filter(
6197
- ([key, e]) => e.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && e.jobId && (e.status === "pending" || e.status === "processing")
6289
+ ([key, e]) => e.provider === provider.id && (!selectedKeys || selectedKeys.has(key)) && e.jobId && e.submissionComplete !== false && (e.status === "pending" || e.status === "processing")
6198
6290
  );
6199
6291
  while (pending().length > 0) {
6200
6292
  if (Date.now() - started > timeout) {
6201
- result.stillRunning = pending().length;
6293
+ result.stillRunning += pending().length;
6202
6294
  log2(` timed out with ${result.stillRunning} job(s) still running \u2014 re-run \`poll\` to resume`);
6203
6295
  break;
6204
6296
  }