Serialize node links instead of widget values

Syncing litegraph widget state is kinda annoying, and unnecessary since
everything will be moved to separate UI component nodes. Instead I
modified the input slot type to store the min/max/step to be copied into
the default UI node later. Now nothing uses litegraph's widgets anymore
This commit is contained in:
space-nuko
2023-04-30 16:37:15 -07:00
parent 1b64c3a502
commit 7880c68d7f
10 changed files with 254 additions and 213 deletions

View File

@@ -110,10 +110,10 @@
app.eventBus.on("configured", nodeState.configureFinished);
app.eventBus.on("cleared", nodeState.clear);
app.eventBus.on("nodeAdded", layoutState.nodeAdded);
app.eventBus.on("nodeRemoved", layoutState.nodeRemoved);
app.eventBus.on("configured", layoutState.configureFinished);
app.eventBus.on("cleared", layoutState.clear);
// app.eventBus.on("nodeAdded", layoutState.nodeAdded);
// app.eventBus.on("nodeRemoved", layoutState.nodeRemoved);
// app.eventBus.on("configured", layoutState.configureFinished);
// app.eventBus.on("cleared", layoutState.clear);
app.eventBus.on("autosave", doAutosave);
app.eventBus.on("restored", doRestore);

View File

@@ -1,4 +1,4 @@
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink } from "@litegraph-ts/core";
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode } from "@litegraph-ts/core";
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
import ComfyAPI from "$lib/api"
import { ComfyWidgets } from "$lib/widgets"
@@ -8,7 +8,7 @@ import EventEmitter from "events";
import type TypedEmitter from "typed-emitter";
// Import nodes
import * as basic from "@litegraph-ts/nodes-basic"
import "@litegraph-ts/nodes-basic"
import * as nodes from "$lib/nodes/index"
import ComfyGraphCanvas from "$lib/ComfyGraphCanvas";
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
@@ -16,6 +16,8 @@ import * as widgets from "$lib/widgets/index"
import type ComfyWidget from "$lib/widgets/ComfyWidget";
import queueState from "$lib/stores/queueState";
import GraphSync from "$lib/GraphSync";
import type { SvelteComponentDev } from "svelte/internal";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
LiteGraph.catch_exceptions = false;
@@ -49,12 +51,6 @@ type ComfyAppEvents = {
restored: (workflow: SerializedAppState) => void
}
interface ComfyGraphNodeExecutable extends LGraphNodeExecutable {
comfyClass: string
isVirtualNode?: boolean;
applyToGraph(workflow: SerializedLGraph<SerializedLGraphNode<LGraphNode>, SerializedLLink, SerializedLGraphGroup>): void;
}
export type Progress = {
value: number,
max: number
@@ -199,7 +195,7 @@ export default class ComfyApp {
}
static node_type_overrides: Record<string, typeof ComfyGraphNode> = {}
static widget_type_overrides: Record<string, Function> = {}
static widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
private registerNodeTypeOverrides() {
ComfyApp.node_type_overrides["SaveImage"] = nodes.ComfySaveImageNode;
@@ -239,6 +235,7 @@ export default class ComfyApp {
super(title);
this.type = nodeId; // XXX: workaround dependency in LGraphNode.addInput()
(this as any).comfyClass = nodeId;
(this as any).isBackendNode = true;
var inputs = nodeData["input"]["required"];
if (nodeData["input"]["optional"] != undefined) {
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
@@ -261,8 +258,8 @@ export default class ComfyApp {
// Standard type widgets
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
} else {
// Node connection inputs
this.addInput(inputName, type);
// Node connection inputs (backend)
this.addInput(inputName, type, { color_off: "orange", color_on: "orange" });
}
}
}
@@ -277,7 +274,7 @@ export default class ComfyApp {
s[0] = Math.max(config.minWidth, s[0] * 1.5);
s[1] = Math.max(config.minHeight, s[1]);
this.size = s;
this.serialize_widgets = true;
this.serialize_widgets = false;
// app.#invokeExtensionsAsync("nodeCreated", this);
@@ -428,7 +425,7 @@ export default class ComfyApp {
this.clean();
if (!graphData) {
graphData = structuredClone(defaultGraph.workflow)
// graphData = structuredClone(defaultGraph.workflow)
}
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
@@ -443,21 +440,6 @@ export default class ComfyApp {
size[0] = Math.max(node.size[0], size[0]);
size[1] = Math.max(node.size[1], size[1]);
node.size = size;
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);
}
}
}
}
}
// this.#invokeExtensions("loadedGraphNode", node);
}
}
@@ -467,60 +449,82 @@ export default class ComfyApp {
* @returns The workflow and node links
*/
async graphToPrompt() {
// Run frontend-only logic
this.lGraph.runStep(1)
const workflow = this.lGraph.serialize();
const output = {};
// Process nodes in order of execution
for (const node of this.lGraph.computeExecutionOrder<ComfyGraphNodeExecutable>(false, null)) {
for (const node of this.lGraph.computeExecutionOrder<ComfyGraphNode>(false, null)) {
const n = workflow.nodes.find((n) => n.id === node.id);
if (node.isVirtualNode || !node.comfyClass) {
if (!node.isBackendNode) {
console.debug("Not serializing node: ", node.type)
// Don't serialize frontend only nodes but let them make changes
if (node.applyToGraph) {
node.applyToGraph(workflow);
}
continue;
}
if (node.mode === 2) {
if (node.mode === NodeMode.NEVER) {
// Don't serialize muted nodes
continue;
}
const inputs = {};
const widgets = node.widgets;
// Store all widget values
if (widgets) {
for (let i = 0; i < widgets.length; i++) {
const widget = widgets[i];
let isVirtual = false;
if ("isVirtual" in widget)
isVirtual = (widget as ComfyWidget<any, any>).isVirtual;
if ((!widget.options || widget.options.serialize !== false) && !isVirtual) {
let value = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value;
inputs[widget.name] = value
// Store all link values
if (node.inputs) {
for (let i = 0; i < node.inputs.length; i++) {
const inp = node.inputs[i];
const inputLink = node.getInputLink(i)
const inputNode = node.getInputNode(i)
if (!inputLink || !inputNode)
continue;
let serialize = true;
if ("config" in inp)
serialize = (inp as IComfyInputSlot).serialize
let isBackendNode = false;
let isInputBackendNode = false;
if ("isBackendNode" in node)
isBackendNode = (node as ComfyGraphNode).isBackendNode;
if ("isBackendNode" in inputNode)
isInputBackendNode = (inputNode as ComfyGraphNode).isBackendNode;
// The reasoning behind this check:
// We only want to serialize inputs to nodes with backend equivalents.
// And in ComfyBox, the nodes in litegraph *never* have widgets, instead they're all inputs.
// All values are passed by separate frontend-only nodes,
// either UI-bound or something like ConstantInteger.
// So we know that any value passed into a backend node *must* come from
// a frontend node.
// The rest (links between backend nodes) will be serialized after this bit runs.
if (serialize && isBackendNode && !isInputBackendNode) {
inputs[inp.name] = inputLink.data
}
}
}
// Store all node links
// Store all links between nodes
for (let i = 0; i < node.inputs.length; i++) {
let parent: ComfyGraphNodeExecutable = node.getInputNode(i) as ComfyGraphNodeExecutable;
let parent: ComfyGraphNode = node.getInputNode(i) as ComfyGraphNode;
if (parent) {
let link = node.getInputLink(i);
while (parent && parent.isVirtualNode) {
while (parent && !parent.isBackendNode) {
link = parent.getInputLink(link.origin_slot);
if (link) {
parent = parent.getInputNode(link.origin_slot) as ComfyGraphNodeExecutable;
parent = parent.getInputNode(link.origin_slot) as ComfyGraphNode;
} else {
parent = null;
}
}
if (link) {
inputs[node.inputs[i].name] = [String(link.origin_id), link.origin_slot];
const input = node.inputs[i]
// TODO can null be a legitimate value in some cases?
// Nodes like CLIPLoader will never have a value in the frontend, hence "null".
if (!(input.name in inputs))
inputs[input.name] = [String(link.origin_id), link.origin_slot];
}
}
}
@@ -573,14 +577,8 @@ export default class ComfyApp {
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 ("afterQueued" in widget) {
(widget as ComfyWidget<any, any>).afterQueued();
}
}
if ("afterQueued" in node) {
(node as ComfyGraphNode).afterQueued();
}
}
@@ -637,17 +635,18 @@ export default class ComfyApp {
const def = defs[node.type];
for (const widgetNum in node.widgets) {
const widget = node.widgets[widgetNum]
throw "refreshComboInNodes unimplemented!"
// 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];
// }
// }
// }
}
}