A whole lotta things

This commit is contained in:
space-nuko
2023-04-05 01:02:47 -05:00
parent 0f703c2123
commit 32f1900568
13 changed files with 1291 additions and 356 deletions

View File

@@ -1,19 +1,31 @@
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode } from "litegraph.js";
import type { LGraphNodeBase } from "litegraph.js";
import type { LGraphNodeBase, LConnectionKind, INodeSlot } from "litegraph.js";
import ComfyAPI from "$lib/api"
import { ComfyWidgets } from "$lib/widgets"
import defaultGraph from "$lib/defaultGraph"
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
import EventEmitter from "events";
import type TypedEmitter from "typed-emitter";
type QueueItem = { num: number, batchCount: number }
type ComfyAppEvents = {
configured: (graph: LGraph) => void
nodeAdded: (node: LGraphNode) => void
nodeRemoved: (node: LGraphNode) => void
nodeConnectionChanged: (kind: LConnectionKind, node: LGraphNode, slot: INodeSlot, targetNode: LGraphNode, targetSlot: INodeSlot) => void
cleared: () => void
}
export default class ComfyApp {
api: ComfyAPI;
canvasEl: HTMLCanvasElement | null = null;
canvasCtx: CanvasRenderingContext2D | null = null;
lGraph: LGraph | null = null;
lCanvas: LGraphCanvas | null = null;
dropZone: HTMLElement | null = null;
nodeOutputs: Record<string, any> = {};
eventBus: TypedEmitter<ComfyAppEvents> = new EventEmitter() as TypedEmitter<ComfyAppEvents>;
private queueItems: QueueItem[] = [];
private processingQueue: boolean = false;
@@ -31,6 +43,8 @@ export default class ComfyApp {
this.lCanvas = new LGraphCanvas(this.canvasEl, this.lGraph);
this.canvasCtx = this.canvasEl.getContext("2d");
this.addGraphLifecycleHooks();
LiteGraph.release_link_on_empty_shows_menu = true;
LiteGraph.alt_drag_do_clone_nodes = true;
@@ -63,7 +77,7 @@ export default class ComfyApp {
// this.#addDrawNodeHandler();
// this.#addDrawGroupsHandler();
// this.#addApiUpdateHandlers();
// this.#addDropHandler();
this.addDropHandler();
// this.#addPasteHandler();
// this.#addKeyboardHandler();
@@ -101,6 +115,52 @@ export default class ComfyApp {
}
private graphOnConfigure() {
console.log("Configured");
this.eventBus.emit("configured", this.lGraph);
}
private graphOnBeforeChange(graph: LGraph, info: any) {
console.log("BeforeChange", info);
this.eventBus.emit("beforeChange", graph, info);
}
private graphOnAfterChange(graph: LGraph, info: any) {
console.log("AfterChange", info);
this.eventBus.emit("afterChange", graph, info);
}
private graphOnNodeAdded(node: LGraphNode) {
console.log("Added", node);
this.eventBus.emit("nodeAdded", node);
}
private graphOnNodeRemoved(node: LGraphNode) {
console.log("Removed", node);
this.eventBus.emit("nodeRemoved", node);
}
private graphOnNodeConnectionChange(kind: LConnectionKind, node: LGraphNode, slot: INodeSlot, targetNode: LGraphNode, targetSlot: INodeSlot) {
console.log("ConnectionChange", node);
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
}
private canvasOnClear() {
console.log("CanvasClear");
this.eventBus.emit("cleared");
}
private addGraphLifecycleHooks() {
this.lGraph.onConfigure = this.graphOnConfigure.bind(this);
this.lGraph.onBeforeChange = this.graphOnBeforeChange.bind(this);
this.lGraph.onAfterChange = this.graphOnAfterChange.bind(this);
this.lGraph.onNodeAdded = this.graphOnNodeAdded.bind(this);
this.lGraph.onNodeRemoved = this.graphOnNodeRemoved.bind(this);
this.lGraph.onNodeConnectionChange = this.graphOnNodeConnectionChange.bind(this);
this.lCanvas.onClear = this.canvasOnClear.bind(this);
}
private async registerNodes() {
const app = this;
@@ -183,235 +243,277 @@ export default class ComfyApp {
// await this.#invokeExtensionsAsync("registerCustomNodes");
}
/**
* Populates the graph with the specified workflow data
* @param {*} graphData A serialized graph object
*/
loadGraphData(graphData: any = null) {
this.clean();
private showDropZone() {
this.dropZone.style.display = "block";
}
if (!graphData) {
graphData = structuredClone(defaultGraph);
}
private hideDropZone() {
this.dropZone.style.display = "none";
}
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
for (let n of graphData.nodes) {
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
}
private allowDrag(event: DragEvent) {
if (event.dataTransfer.items?.length > 0) {
event.dataTransfer.dropEffect = 'copy';
this.showDropZone();
event.preventDefault();
}
}
this.lGraph.configure(graphData);
private async handleDrop(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
this.hideDropZone();
for (const node of this.lGraph._nodes) {
const size = node.computeSize();
size[0] = Math.max(node.size[0], size[0]);
size[1] = Math.max(node.size[1], size[1]);
node.size = size;
if (event.dataTransfer.files.length > 0) {
await this.handleFile(event.dataTransfer.files[0]);
}
}
if (node.widgets) {
// If you break something in the backend and want to patch workflows in the frontend
// This is the place to do this
for (let widget of node.widgets) {
if (node.type == "KSampler" || node.type == "KSamplerAdvanced") {
if (widget.name == "sampler_name") {
if (widget.value.constructor === String && widget.value.startsWith("sample_")) {
widget.value = widget.value.slice(7);
}
}
}
}
}
private addDropHandler() {
this.dropZone = document.getElementById("dropzone");
// this.#invokeExtensions("loadedGraphNode", node);
}
}
window.addEventListener('dragenter', this.allowDrag.bind(this));
this.dropZone.addEventListener('dragover', this.allowDrag.bind(this));
this.dropZone.addEventListener('dragleave', this.hideDropZone.bind(this));
this.dropZone.addEventListener('drop', this.handleDrop.bind(this));
}
/**
* Converts the current graph workflow for sending to the API
* @returns The workflow and node links
*/
async graphToPrompt() {
const workflow = this.lGraph.serialize();
const output = {};
// Process nodes in order of execution
for (const node of this.lGraph.computeExecutionOrder(false, null)) {
const n = workflow.nodes.find((n) => n.id === node.id);
/**
* Populates the graph with the specified workflow data
* @param {*} graphData A serialized graph object
*/
loadGraphData(graphData: any = null) {
this.clean();
if (node.isVirtualNode) {
// Don't serialize frontend only nodes but let them make changes
if (node.applyToGraph) {
node.applyToGraph(workflow);
}
continue;
}
if (!graphData) {
graphData = structuredClone(defaultGraph);
}
if (node.mode === 2) {
// Don't serialize muted nodes
continue;
}
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
for (let n of graphData.nodes) {
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
}
const inputs = {};
const widgets = node.widgets;
this.lGraph.configure(graphData);
// Store all widget values
if (widgets) {
for (const i in widgets) {
const widget = widgets[i];
if (!widget.options || widget.options.serialize !== false) {
inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value;
}
}
}
for (const node of this.lGraph._nodes) {
const size = node.computeSize();
size[0] = Math.max(node.size[0], size[0]);
size[1] = Math.max(node.size[1], size[1]);
node.size = size;
// Store all node links
for (let i in node.inputs) {
let parent = node.getInputNode(i);
if (parent) {
let link = node.getInputLink(i);
while (parent && parent.isVirtualNode) {
link = parent.getInputLink(link.origin_slot);
if (link) {
parent = parent.getInputNode(link.origin_slot);
} else {
parent = null;
}
}
if (node.widgets) {
// If you break something in the backend and want to patch workflows in the frontend
// This is the place to do this
for (let widget of node.widgets) {
if (node.type == "KSampler" || node.type == "KSamplerAdvanced") {
if (widget.name == "sampler_name") {
if (widget.value.constructor === String && widget.value.startsWith("sample_")) {
widget.value = widget.value.slice(7);
}
}
}
}
}
if (link) {
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
}
}
}
// this.#invokeExtensions("loadedGraphNode", node);
}
}
output[String(node.id)] = {
inputs,
class_type: node.comfyClass,
};
}
/**
* Converts the current graph workflow for sending to the API
* @returns The workflow and node links
*/
async graphToPrompt(frontendState: Record<number, any[]> = {}) {
const workflow = this.lGraph.serialize();
// Remove inputs connected to removed nodes
const output = {};
// Process nodes in order of execution
for (const node of this.lGraph.computeExecutionOrder(false, null)) {
const fromFrontend = frontendState[node.id];
if (fromFrontend) {
console.log("Set values!", node, fromFrontend)
node.widgets_values = fromFrontend;
}
for (const o in output) {
for (const i in output[o].inputs) {
if (Array.isArray(output[o].inputs[i])
&& output[o].inputs[i].length === 2
&& !output[output[o].inputs[i][0]]) {
delete output[o].inputs[i];
}
}
}
const n = workflow.nodes.find((n) => n.id === node.id);
return { workflow, output };
}
if (node.isVirtualNode) {
// Don't serialize frontend only nodes but let them make changes
if (node.applyToGraph) {
node.applyToGraph(workflow);
}
continue;
}
async queuePrompt(num: number, batchCount: number = 1) {
this.queueItems.push({ num, batchCount });
if (node.mode === 2) {
// Don't serialize muted nodes
continue;
}
// Only have one action process the items so each one gets a unique seed correctly
if (this.processingQueue) {
return;
}
const inputs = {};
const widgets = node.widgets;
this.processingQueue = true;
try {
while (this.queueItems.length) {
({ num, batchCount } = this.queueItems.pop());
// Store all widget values
if (widgets) {
for (const i in widgets) {
const widget = widgets[i];
if (!widget.options || widget.options.serialize !== false) {
inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value;
}
}
}
// Store all node links
for (let i in node.inputs) {
let parent = node.getInputNode(i);
if (parent) {
let link = node.getInputLink(i);
while (parent && parent.isVirtualNode) {
link = parent.getInputLink(link.origin_slot);
if (link) {
parent = parent.getInputNode(link.origin_slot);
} else {
parent = null;
}
}
if (link) {
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
}
}
}
output[String(node.id)] = {
inputs,
class_type: node.comfyClass,
};
}
// Remove inputs connected to removed nodes
for (const o in output) {
for (const i in output[o].inputs) {
if (Array.isArray(output[o].inputs[i])
&& output[o].inputs[i].length === 2
&& !output[output[o].inputs[i][0]]) {
delete output[o].inputs[i];
}
}
}
return { workflow, output };
}
async queuePrompt(num: number, batchCount: number = 1, frontendState: Record<number, any[]> = {}) {
this.queueItems.push({ num, batchCount });
// Only have one action process the items so each one gets a unique seed correctly
if (this.processingQueue) {
return;
}
this.processingQueue = true;
try {
while (this.queueItems.length) {
({ num, batchCount } = this.queueItems.pop());
console.log(`Queue get! ${num} ${batchCount}`);
for (let i = 0; i < batchCount; i++) {
const p = await this.graphToPrompt();
for (let i = 0; i < batchCount; i++) {
const p = await this.graphToPrompt(frontendState);
try {
await this.api.queuePrompt(num, p);
} catch (error) {
// this.ui.dialog.show(error.response || error.toString());
try {
await this.api.queuePrompt(num, p);
} catch (error) {
// this.ui.dialog.show(error.response || error.toString());
console.error(error.response || error.toString())
break;
}
break;
}
for (const n of p.workflow.nodes) {
const node = this.lGraph.getNodeById(n.id);
if (node.widgets) {
for (const widget of node.widgets) {
// Allow widgets to run callbacks after a prompt has been queued
// e.g. random seed after every gen
// if (widget.afterQueued) {
// widget.afterQueued();
// }
}
}
}
for (const n of p.workflow.nodes) {
const node = this.lGraph.getNodeById(n.id);
if (node.widgets) {
for (const widget of node.widgets) {
// Allow widgets to run callbacks after a prompt has been queued
// e.g. random seed after every gen
// if (widget.afterQueued) {
// widget.afterQueued();
// }
}
}
}
this.lCanvas.draw(true, true);
// await this.ui.queue.update();
}
}
} finally {
this.lCanvas.draw(true, true);
// await this.ui.queue.update();
}
}
} finally {
console.log("Queue finished!");
this.processingQueue = false;
}
}
this.processingQueue = false;
}
}
/**
* Loads workflow data from the specified file
*/
async handleFile(file: File) {
if (file.type === "image/png") {
const pngInfo = await getPngMetadata(file);
if (pngInfo) {
if (pngInfo.workflow) {
this.loadGraphData(JSON.parse(pngInfo.workflow));
} else if (pngInfo.parameters) {
importA1111(this.lGraph, pngInfo.parameters);
}
}
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
const reader = new FileReader();
reader.onload = () => {
this.loadGraphData(JSON.parse(reader.result));
};
reader.readAsText(file);
}
}
/**
* Loads workflow data from the specified file
*/
async handleFile(file: File) {
if (file.type === "image/png") {
const pngInfo = await getPngMetadata(file);
if (pngInfo) {
if (pngInfo.workflow) {
this.loadGraphData(JSON.parse(pngInfo.workflow));
} else if (pngInfo.parameters) {
importA1111(this.lGraph, pngInfo.parameters);
}
}
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
const reader = new FileReader();
reader.onload = () => {
this.loadGraphData(JSON.parse(reader.result as string));
};
reader.readAsText(file);
}
}
// registerExtension(extension) {
// if (!extension.name) {
// throw new Error("Extensions must have a 'name' property.");
// }
// if (this.extensions.find((ext) => ext.name === extension.name)) {
// throw new Error(`Extension named '${extension.name}' already registered.`);
// }
// this.extensions.push(extension);
// }
// registerExtension(extension) {
// if (!extension.name) {
// throw new Error("Extensions must have a 'name' property.");
// }
// if (this.extensions.find((ext) => ext.name === extension.name)) {
// throw new Error(`Extension named '${extension.name}' already registered.`);
// }
// this.extensions.push(extension);
// }
/**
* Refresh combo list on whole nodes
*/
async refreshComboInNodes() {
const defs = await this.api.getNodeDefs();
/**
* Refresh combo list on whole nodes
*/
async refreshComboInNodes() {
const defs = await this.api.getNodeDefs();
for(let nodeNum in this.lGraph._nodes) {
const node = this.lGraph._nodes[nodeNum];
for(let nodeNum in this.lGraph._nodes) {
const node = this.lGraph._nodes[nodeNum];
const def = defs[node.type];
const def = defs[node.type];
for(const widgetNum in node.widgets) {
const widget = node.widgets[widgetNum]
for(const widgetNum in node.widgets) {
const widget = node.widgets[widgetNum]
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
widget.options.values = def["input"]["required"][widget.name][0];
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
widget.options.values = def["input"]["required"][widget.name][0];
if(!widget.options.values.includes(widget.value)) {
widget.value = widget.options.values[0];
}
}
}
}
}
if(!widget.options.values.includes(widget.value)) {
widget.value = widget.options.values[0];
}
}
}
}
}
/**
* Clean current state
*/
clean() {
this.nodeOutputs = {};
}
/**
* Clean current state
*/
clean() {
this.nodeOutputs = {};
}
}