Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Includes 30 custom nodes committed directly, 7 Civitai-exclusive loras stored via Git LFS, and a setup script that installs all dependencies and downloads HuggingFace-hosted models on vast.ai. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
import type {
|
|
ComfyApiEventDetailCached,
|
|
ComfyApiEventDetailError,
|
|
ComfyApiEventDetailExecuted,
|
|
ComfyApiEventDetailExecuting,
|
|
ComfyApiEventDetailExecutionStart,
|
|
ComfyApiEventDetailProgress,
|
|
ComfyApiEventDetailStatus,
|
|
ComfyApiFormat,
|
|
ComfyApiPrompt,
|
|
} from "typings/comfy.js";
|
|
import { api } from "scripts/api.js";
|
|
import type { LGraph as TLGraph, LGraphCanvas as TLGraphCanvas } from "@comfyorg/frontend";
|
|
import { Resolver, getResolver } from "./shared_utils.js";
|
|
|
|
/**
|
|
* Wraps general data of a prompt's execution.
|
|
*/
|
|
export class PromptExecution {
|
|
id: string;
|
|
promptApi: ComfyApiFormat | null = null;
|
|
executedNodeIds: string[] = [];
|
|
totalNodes: number = 0;
|
|
currentlyExecuting: {
|
|
nodeId: string;
|
|
nodeLabel?: string;
|
|
step?: number;
|
|
maxSteps?: number;
|
|
/** The current pass, for nodes with multiple progress passes. */
|
|
pass: number;
|
|
/**
|
|
* The max num of passes. Can be calculated for some nodes, or set to -1 when known there will
|
|
* be multiple passes, but the number cannot be calculated.
|
|
*/
|
|
maxPasses?: number;
|
|
} | null = null;
|
|
errorDetails: any | null = null;
|
|
|
|
apiPrompt: Resolver<null> = getResolver();
|
|
|
|
constructor(id: string) {
|
|
this.id = id;
|
|
}
|
|
|
|
/**
|
|
* Sets the prompt and prompt-related data. This can technically come in lazily, like if the web
|
|
* socket fires the 'execution-start' event before we actually get a response back from the
|
|
* initial prompt call.
|
|
*/
|
|
setPrompt(prompt: ComfyApiPrompt) {
|
|
this.promptApi = prompt.output;
|
|
this.totalNodes = Object.keys(this.promptApi).length;
|
|
this.apiPrompt.resolve(null);
|
|
}
|
|
|
|
getApiNode(nodeId: string | number) {
|
|
return this.promptApi?.[String(nodeId)] || null;
|
|
}
|
|
|
|
private getNodeLabel(nodeId: string | number) {
|
|
const apiNode = this.getApiNode(nodeId);
|
|
let label = apiNode?._meta?.title || apiNode?.class_type || undefined;
|
|
if (!label) {
|
|
const graphNode = this.maybeGetComfyGraph()?.getNodeById(Number(nodeId));
|
|
label = graphNode?.title || graphNode?.type || undefined;
|
|
}
|
|
return label;
|
|
}
|
|
|
|
/**
|
|
* Updates the execution data depending on the passed data, fed from api events.
|
|
*/
|
|
executing(nodeId: string | null, step?: number, maxSteps?: number) {
|
|
if (nodeId == null) {
|
|
// We're done, any left over nodes must be skipped...
|
|
this.currentlyExecuting = null;
|
|
return;
|
|
}
|
|
if (this.currentlyExecuting?.nodeId !== nodeId) {
|
|
if (this.currentlyExecuting != null) {
|
|
this.executedNodeIds.push(nodeId);
|
|
}
|
|
this.currentlyExecuting = { nodeId, nodeLabel: this.getNodeLabel(nodeId), pass: 0 };
|
|
// We'll see if we're known node for multiple passes, that will come in as generic 'progress'
|
|
// updates from the api. If we're known to have multiple passes, then we'll pre-set data to
|
|
// allow the progress bar to handle intial rendering. If we're not, that's OK, the data will
|
|
// be shown with the second pass.
|
|
this.apiPrompt.promise.then(() => {
|
|
// If we execute with a null node id and clear the currently executing, then we can just
|
|
// move on. This seems to only happen with a super-fast execution (like, just seed node
|
|
// and display any for testing).
|
|
if (this.currentlyExecuting == null) {
|
|
return;
|
|
}
|
|
const apiNode = this.getApiNode(nodeId);
|
|
if (!this.currentlyExecuting.nodeLabel) {
|
|
this.currentlyExecuting.nodeLabel = this.getNodeLabel(nodeId);
|
|
}
|
|
if (apiNode?.class_type === "UltimateSDUpscale") {
|
|
// From what I can tell, UltimateSDUpscale, does an initial pass that isn't actually a
|
|
// tile. It seems to always be 4 steps... We'll start our pass at -1, so this prepass is
|
|
// "0" and "1" will start with the first tile. This way, a user knows they have 4 tiles,
|
|
// know this pass counter will go to 4 (and not 5). Also, we cannot calculate maxPasses
|
|
// for 'UltimateSDUpscale' :(
|
|
this.currentlyExecuting.pass--;
|
|
this.currentlyExecuting.maxPasses = -1;
|
|
} else if (apiNode?.class_type === "IterativeImageUpscale") {
|
|
this.currentlyExecuting.maxPasses = (apiNode?.inputs["steps"] as number) ?? -1;
|
|
}
|
|
});
|
|
}
|
|
if (step != null) {
|
|
// If we haven't had any stpes before, or the passes step is lower than the previous, then
|
|
// increase the passes.
|
|
if (!this.currentlyExecuting!.step || step < this.currentlyExecuting!.step) {
|
|
this.currentlyExecuting!.pass!++;
|
|
}
|
|
this.currentlyExecuting!.step = step;
|
|
this.currentlyExecuting!.maxSteps = maxSteps;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If there's an error, we add the details.
|
|
*/
|
|
error(details: any) {
|
|
this.errorDetails = details;
|
|
}
|
|
|
|
private maybeGetComfyGraph(): TLGraph | null {
|
|
return ((window as any)?.app?.graph as TLGraph) || null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A singleton service that wraps the Comfy API and simplifies the event data being fired.
|
|
*/
|
|
class PromptService extends EventTarget {
|
|
promptsMap: Map<string, PromptExecution> = new Map();
|
|
currentExecution: PromptExecution | null = null;
|
|
lastQueueRemaining = 0;
|
|
|
|
constructor(api: any) {
|
|
super();
|
|
const that = this;
|
|
|
|
// Patch the queuePrompt method so we can capture new data going through.
|
|
const queuePrompt = api.queuePrompt;
|
|
api.queuePrompt = async function (num: number, prompt: ComfyApiPrompt, ...args: any[]) {
|
|
let response;
|
|
try {
|
|
response = await queuePrompt.apply(api, [...arguments]);
|
|
} catch (e) {
|
|
const promptExecution = that.getOrMakePrompt("error");
|
|
promptExecution.error({ exception_type: "Unknown." });
|
|
// console.log("ERROR QUEUE PROMPT", response, arguments);
|
|
throw e;
|
|
}
|
|
// console.log("QUEUE PROMPT", response, arguments);
|
|
const promptExecution = that.getOrMakePrompt(response.prompt_id);
|
|
promptExecution.setPrompt(prompt);
|
|
if (!that.currentExecution) {
|
|
that.currentExecution = promptExecution;
|
|
}
|
|
that.promptsMap.set(response.prompt_id, promptExecution);
|
|
that.dispatchEvent(
|
|
new CustomEvent("queue-prompt", {
|
|
detail: {
|
|
prompt: promptExecution,
|
|
},
|
|
}),
|
|
);
|
|
return response;
|
|
};
|
|
|
|
api.addEventListener("status", (e: CustomEvent<ComfyApiEventDetailStatus>) => {
|
|
// console.log("status", JSON.stringify(e.detail));
|
|
// Sometimes a status message is fired when the app loades w/o any details.
|
|
if (!e.detail?.exec_info) return;
|
|
this.lastQueueRemaining = e.detail.exec_info.queue_remaining;
|
|
this.dispatchProgressUpdate();
|
|
});
|
|
|
|
api.addEventListener("execution_start", (e: CustomEvent<ComfyApiEventDetailExecutionStart>) => {
|
|
// console.log("execution_start", JSON.stringify(e.detail));
|
|
if (!this.promptsMap.has(e.detail.prompt_id)) {
|
|
console.warn("'execution_start' fired before prompt was made.");
|
|
}
|
|
const prompt = this.getOrMakePrompt(e.detail.prompt_id);
|
|
this.currentExecution = prompt;
|
|
this.dispatchProgressUpdate();
|
|
});
|
|
|
|
api.addEventListener("executing", (e: CustomEvent<ComfyApiEventDetailExecuting>) => {
|
|
// console.log("executing", JSON.stringify(e.detail));
|
|
if (!this.currentExecution) {
|
|
this.currentExecution = this.getOrMakePrompt("unknown");
|
|
console.warn("'executing' fired before prompt was made.");
|
|
}
|
|
this.currentExecution.executing(e.detail);
|
|
this.dispatchProgressUpdate();
|
|
if (e.detail == null) {
|
|
this.currentExecution = null;
|
|
}
|
|
});
|
|
|
|
api.addEventListener("progress", (e: CustomEvent<ComfyApiEventDetailProgress>) => {
|
|
// console.log("progress", JSON.stringify(e.detail));
|
|
if (!this.currentExecution) {
|
|
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id);
|
|
console.warn("'progress' fired before prompt was made.");
|
|
}
|
|
this.currentExecution.executing(e.detail.node, e.detail.value, e.detail.max);
|
|
this.dispatchProgressUpdate();
|
|
});
|
|
|
|
api.addEventListener("execution_cached", (e: CustomEvent<ComfyApiEventDetailCached>) => {
|
|
// console.log("execution_cached", JSON.stringify(e.detail));
|
|
if (!this.currentExecution) {
|
|
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id);
|
|
console.warn("'execution_cached' fired before prompt was made.");
|
|
}
|
|
for (const cached of e.detail.nodes) {
|
|
this.currentExecution.executing(cached);
|
|
}
|
|
this.dispatchProgressUpdate();
|
|
});
|
|
|
|
api.addEventListener("executed", (e: CustomEvent<ComfyApiEventDetailExecuted>) => {
|
|
// console.log("executed", JSON.stringify(e.detail));
|
|
if (!this.currentExecution) {
|
|
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id);
|
|
console.warn("'executed' fired before prompt was made.");
|
|
}
|
|
});
|
|
|
|
api.addEventListener("execution_error", (e: CustomEvent<ComfyApiEventDetailError>) => {
|
|
// console.log("execution_error", e.detail);
|
|
if (!this.currentExecution) {
|
|
this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id);
|
|
console.warn("'execution_error' fired before prompt was made.");
|
|
}
|
|
this.currentExecution?.error(e.detail);
|
|
this.dispatchProgressUpdate();
|
|
});
|
|
}
|
|
|
|
/** A helper method, since we extend/override api.queuePrompt above anyway. */
|
|
async queuePrompt(prompt: ComfyApiPrompt) {
|
|
return await api.queuePrompt(-1, prompt);
|
|
}
|
|
|
|
dispatchProgressUpdate() {
|
|
this.dispatchEvent(
|
|
new CustomEvent("progress-update", {
|
|
detail: {
|
|
queue: this.lastQueueRemaining,
|
|
prompt: this.currentExecution,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
getOrMakePrompt(id: string) {
|
|
let prompt = this.promptsMap.get(id);
|
|
if (!prompt) {
|
|
prompt = new PromptExecution(id);
|
|
this.promptsMap.set(id, prompt);
|
|
}
|
|
return prompt;
|
|
}
|
|
}
|
|
|
|
export const SERVICE = new PromptService(api);
|