File size: 10,305 Bytes
4450790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
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 "typings/litegraph.js";
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) {
      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);