Import ComfyReroute

This commit is contained in:
space-nuko
2023-04-07 04:50:16 -05:00
parent 1ad77f49ef
commit e6bab41563
21 changed files with 1411 additions and 2090 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { onMount } from "svelte";
import { get } from "svelte/store";
import { Pane, Splitpanes } from 'svelte-splitpanes';
import { Button } from "@gradio/button";
import { Backpack, Gear } from 'radix-icons-svelte';
@@ -7,7 +8,7 @@
import ComfyApp from "./ComfyApp";
import widgetState from "$lib/stores/widgetState";
import { LGraphNode } from "litegraph.js";
import { LGraphNode } from "@litegraph-ts/core";
let app: ComfyApp = undefined;
let uiPane: ComfyUIPane = undefined;
@@ -17,7 +18,7 @@
}
function queuePrompt() {
const state = uiPane.getState();
const state = get(widgetState);
console.log("Queuing!", state);
app.queuePrompt(0, 1, state);
}

View File

@@ -1,5 +1,5 @@
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode } from "litegraph.js";
import type { LGraphNodeBase, LConnectionKind, INodeSlot } from "litegraph.js";
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink } from "@litegraph-ts/core";
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
import ComfyAPI from "$lib/api"
import { ComfyWidgets } from "$lib/widgets"
import defaultGraph from "$lib/defaultGraph"
@@ -7,14 +7,31 @@ import { getPngMetadata, importA1111 } from "$lib/pnginfo";
import EventEmitter from "events";
import type TypedEmitter from "typed-emitter";
// Import nodes
import * as basic from "@litegraph-ts/nodes-basic"
import * as nodes from "$lib/nodes/index"
LiteGraph.catch_exceptions = false;
if (typeof window !== "undefined") {
// Load default visibility
nodes.ComfyReroute.setDefaultTextVisibility(!!localStorage["Comfy.ComfyReroute.DefaultVisibility"]);
}
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
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
}
interface ComfyGraphNodeExecutable extends LGraphNodeExecutable {
comfyClass: string
isVirtualNode?: boolean;
applyToGraph(workflow: SerializedLGraph<SerializedLGraphNode<LGraphNode>, SerializedLLink, SerializedLGraphGroup>): void;
}
export default class ComfyApp {
@@ -32,7 +49,6 @@ export default class ComfyApp {
constructor() {
this.api = new ComfyAPI();
this.eventBus.
}
async setup(): Promise<void> {
@@ -44,49 +60,49 @@ export default class ComfyApp {
this.lCanvas = new LGraphCanvas(this.canvasEl, this.lGraph);
this.canvasCtx = this.canvasEl.getContext("2d");
this.addGraphLifecycleHooks();
this.addGraphLifecycleHooks();
LiteGraph.release_link_on_empty_shows_menu = true;
LiteGraph.alt_drag_do_clone_nodes = true;
LiteGraph.release_link_on_empty_shows_menu = true;
LiteGraph.alt_drag_do_clone_nodes = true;
this.lGraph.start();
this.lGraph.start();
// await this.#invokeExtensionsAsync("init");
await this.registerNodes();
// await this.#invokeExtensionsAsync("init");
await this.registerNodes();
// Load previous workflow
let restored = false;
try {
const json = localStorage.getItem("workflow");
if (json) {
const workflow = JSON.parse(json);
this.loadGraphData(workflow);
restored = true;
}
} catch (err) {
console.error("Error loading previous workflow", err);
}
// Load previous workflow
let restored = false;
try {
const json = localStorage.getItem("workflow");
if (json) {
const workflow = JSON.parse(json);
this.loadGraphData(workflow);
restored = true;
}
} catch (err) {
console.error("Error loading previous workflow", err);
}
// We failed to restore a workflow so load the default
if (!restored) {
this.loadGraphData();
}
// We failed to restore a workflow so load the default
if (!restored) {
this.loadGraphData();
}
// Save current workflow automatically
setInterval(() => localStorage.setItem("workflow", JSON.stringify(this.lGraph.serialize())), 1000);
// Save current workflow automatically
setInterval(() => localStorage.setItem("workflow", JSON.stringify(this.lGraph.serialize())), 1000);
// this.#addDrawNodeHandler();
// this.#addDrawGroupsHandler();
// this.#addApiUpdateHandlers();
this.addDropHandler();
// this.#addPasteHandler();
// this.#addKeyboardHandler();
// this.#addDrawNodeHandler();
// this.#addDrawGroupsHandler();
// this.#addApiUpdateHandlers();
this.addDropHandler();
// this.#addPasteHandler();
// this.#addKeyboardHandler();
// await this.#invokeExtensionsAsync("setup");
// await this.#invokeExtensionsAsync("setup");
// Ensure the canvas fills the window
// Ensure the canvas fills the window
this.resizeCanvas();
window.addEventListener("resize", this.resizeCanvas.bind(this));
window.addEventListener("resize", this.resizeCanvas.bind(this));
return Promise.resolve();
}
@@ -165,83 +181,89 @@ export default class ComfyApp {
private async registerNodes() {
const app = this;
// Load node definitions from the backend
// Load node definitions from the backend
const defs = await this.api.getNodeDefs();
// await this.#invokeExtensionsAsync("addCustomNodeDefs", defs);
// await this.#invokeExtensionsAsync("addCustomNodeDefs", defs);
// Generate list of known widgets
const widgets = ComfyWidgets;
// Generate list of known widgets
const widgets = ComfyWidgets;
// const widgets = Object.assign(
// {},
// ComfyWidgets,
// ...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean)
// );
// {},
// ComfyWidgets,
// ...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean)
// );
// Register a node for each definition
for (const nodeId in defs) {
const nodeData = defs[nodeId];
const node: LGraphNodeBase = Object.assign(
function ComfyNode(this: LGraphNode) {
var inputs = nodeData["input"]["required"];
if (nodeData["input"]["optional"] != undefined){
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
}
const config = { minWidth: 1, minHeight: 1 };
for (const inputName in inputs) {
const inputData = inputs[inputName];
const type = inputData[0];
// Register a node for each definition
for (const nodeId in defs) {
const nodeData = defs[nodeId];
if(inputData[1]?.forceInput) {
this.addInput(inputName, type);
} else {
if (Array.isArray(type)) {
// Enums
Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {});
} else if (`${type}:${inputName}` in widgets) {
// Support custom widgets by Type:Name
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
} else if (type in widgets) {
// Standard type widgets
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
} else {
// Node connection inputs
this.addInput(inputName, type);
}
}
}
const ctor = class extends LGraphNode {
constructor(title?: string) {
super(title);
this.type = nodeId; // XXX: workaround dependency in LGraphNode.addInput()
(this as any).comfyClass = nodeId;
var inputs = nodeData["input"]["required"];
if (nodeData["input"]["optional"] != undefined) {
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
}
const config = { minWidth: 1, minHeight: 1 };
for (const inputName in inputs) {
const inputData = inputs[inputName];
const type = inputData[0];
for (const o in nodeData["output"]) {
const output = nodeData["output"][o];
const outputName = nodeData["output_name"][o] || output;
this.addOutput(outputName, output);
}
if (inputData[1]?.forceInput) {
this.addInput(inputName, type);
} else {
if (Array.isArray(type)) {
// Enums
Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {});
} else if (`${type}:${inputName}` in widgets) {
// Support custom widgets by Type:Name
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
} else if (type in widgets) {
// Standard type widgets
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
} else {
// Node connection inputs
this.addInput(inputName, type);
}
}
}
const s = this.computeSize();
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;
for (const o in nodeData["output"]) {
const output = nodeData["output"][o];
const outputName = nodeData["output_name"][o] || output;
this.addOutput(outputName, output);
}
// app.#invokeExtensionsAsync("nodeCreated", this);
const s = this.computeSize();
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;
// app.#invokeExtensionsAsync("nodeCreated", this);
return this;
},
{
title: nodeData.name,
comfyClass: nodeData.name,
}
);
node.prototype.comfyClass = nodeData.name;
}
}
// this.#addNodeContextMenuHandler(node);
// this.#addDrawBackgroundHandler(node, app);
const node: LGraphNodeConstructor = {
class: ctor,
title: nodeData.name,
type: nodeId,
desc: `ComfyNode: ${nodeId}`
}
// await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData);
LiteGraph.registerNodeType(nodeId, node);
node.category = nodeData.category;
}
// this.#addNodeContextMenuHandler(node);
// this.#addDrawBackgroundHandler(node, app);
// await this.#invokeExtensionsAsync("registerCustomNodes");
// await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData);
LiteGraph.registerNodeType(node);
node.category = nodeData.category;
}
// await this.#invokeExtensionsAsync("registerCustomNodes");
}
private showDropZone() {
@@ -330,12 +352,13 @@ export default class ComfyApp {
const output = {};
// Process nodes in order of execution
for (const node of this.lGraph.computeExecutionOrder(false, null)) {
for (const node of this.lGraph.computeExecutionOrder<ComfyGraphNodeExecutable>(false, null)) {
const fromFrontend = frontendState[node.id];
const n = workflow.nodes.find((n) => n.id === node.id);
if (node.isVirtualNode) {
if (node.isVirtualNode || !node.comfyClass) {
console.debug("Not serializing node: ", node.type)
// Don't serialize frontend only nodes but let them make changes
if (node.applyToGraph) {
node.applyToGraph(workflow);
@@ -353,14 +376,12 @@ export default class ComfyApp {
// Store all widget values
if (widgets) {
for (const i in widgets) {
for (let i = 0; i < widgets.length; i++) {
const widget = widgets[i];
if (!widget.options || widget.options.serialize !== false) {
// TODO serializeValue API
let value = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value;
if (fromFrontend) {
console.log("Set values!", value, fromFrontend[i])
value = fromFrontend[i];
value = fromFrontend[i].value;
}
inputs[widget.name] = value
}
@@ -368,21 +389,21 @@ export default class ComfyApp {
}
// Store all node links
for (let i in node.inputs) {
let parent = node.getInputNode(i);
for (let i = 0; i < node.inputs.length; i++) {
let parent: ComfyGraphNodeExecutable = node.getInputNode(i) as ComfyGraphNodeExecutable;
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);
parent = parent.getInputNode(link.origin_slot) as ComfyGraphNodeExecutable;
} else {
parent = null;
}
}
if (link) {
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
inputs[node.inputs[i].name] = [String(link.origin_id), link.origin_slot];
}
}
}
@@ -466,7 +487,7 @@ export default class ComfyApp {
if (pngInfo.workflow) {
this.loadGraphData(JSON.parse(pngInfo.workflow));
} else if (pngInfo.parameters) {
importA1111(this.lGraph, pngInfo.parameters);
importA1111(this.lGraph, pngInfo.parameters, this.api);
}
}
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
@@ -494,18 +515,18 @@ export default class ComfyApp {
async refreshComboInNodes() {
const defs = await this.api.getNodeDefs();
for(let nodeNum in this.lGraph._nodes) {
for (let nodeNum in this.lGraph._nodes) {
const node = this.lGraph._nodes[nodeNum];
const def = defs[node.type];
for(const widgetNum in node.widgets) {
for (const widgetNum in node.widgets) {
const widget = node.widgets[widgetNum]
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
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)) {
if (!widget.options.values.includes(widget.value)) {
widget.value = widget.options.values[0];
}
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { LGraphNode, LGraph } from "litegraph.js";
import type { IWidget } from "litegraph.js";
import { LGraphNode, LGraph } from "@litegraph-ts/core";
import type { IWidget } from "@litegraph-ts/core";
import ComfyApp from "./ComfyApp";
import ComfyPane from "./ComfyPane.svelte";
import widgetState from "$lib/stores/widgetState";

View File

@@ -0,0 +1,5 @@
import { LGraphNode } from "@litegraph-ts/core";
export default class ComfyGraphNode extends LGraphNode {
isVirtualNode: boolean = false;
}

View File

@@ -0,0 +1,247 @@
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode";
export interface ComfyRerouteProperties extends Record<any, any> {
showOutputText: boolean;
horizontal: boolean;
}
export default class ComfyReroute extends ComfyGraphNode {
static defaultVisibility: boolean = true;
static setDefaultTextVisibility(visible: boolean) {
ComfyReroute.defaultVisibility = visible;
if (visible) {
localStorage["Comfy.ComfyReroute.DefaultVisibility"] = "true";
} else {
delete localStorage["Comfy.ComfyReroute.DefaultVisibility"];
}
}
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
override isVirtualNode: boolean = true;
override titleMode: TitleMode = TitleMode.NO_TITLE;
override collapsable: boolean = false;
override properties: ComfyRerouteProperties = {
showOutputText: ComfyReroute.defaultVisibility,
horizontal: false
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "", type: "*" }
],
outputs: [
{ name: "", type: "*" }
],
}
constructor(title?: string) {
super(title);
this.properties ||= {} as any;
this.properties.showOutputText = ComfyReroute.defaultVisibility;
this.properties.horizontal = false;
if (this.properties.showOutputText) {
this.outputs[0].name = "*"
}
}
override onConnectionsChange(type: LConnectionKind, slotIndex: number, isConnected: boolean, _link: LLink) {
this.applyOrientation();
// Prevent multiple connections to different types when we have no input
if (isConnected && type === LConnectionKind.OUTPUT) {
// Ignore wildcard nodes as these will be updated to real types
const types = new Set(this.outputs[0].links.map((l) => this.graph.links[l].type).filter((t) => t !== "*"));
if (types.size > 1) {
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
const linkId = this.outputs[0].links[i];
const link = this.graph.links[linkId];
const node = this.graph.getNodeById(link.target_id);
node.disconnectInput(link.target_slot);
}
}
}
// Find root input
let currentNode: ComfyReroute = this;
let updateNodes: ComfyReroute[] = [];
let inputType: SlotType | null = null;
let inputNode = null;
while (currentNode) {
updateNodes.unshift(currentNode);
const linkId = currentNode.inputs[0].link;
if (linkId !== null) {
const link = this.graph.links[linkId];
const node = this.graph.getNodeById(link.origin_id);
console.warn(node.type)
if (node.class === ComfyReroute) {
console.log("REROUTE2")
if (node === this) {
// We've found a circle
currentNode.disconnectInput(link.target_slot);
currentNode = null;
}
else {
// Move the previous node
currentNode = node as ComfyReroute;
}
} else {
// We've found the end
inputNode = currentNode;
inputType = node.outputs[link.origin_slot]?.type ?? null;
break;
}
} else {
// This path has no input node
currentNode = null;
break;
}
}
// Find all outputs
const nodes: ComfyReroute[] = [this];
let outputType: SlotType | null = null;
while (nodes.length) {
currentNode = nodes.pop();
const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
if (outputs.length) {
for (const linkId of outputs) {
const link = this.graph.links[linkId];
// When disconnecting sometimes the link is still registered
if (!link) continue;
const node = this.graph.getNodeById(link.target_id);
if (node.class === ComfyReroute) {
console.log("REROUTE")
// Follow reroute nodes
nodes.push(node as ComfyReroute);
updateNodes.push(node as ComfyReroute);
} else {
// We've found an output
const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null;
if (inputType && nodeOutType !== inputType) {
// The output doesnt match our input so disconnect it
node.disconnectInput(link.target_slot);
} else {
outputType = nodeOutType;
}
}
}
} else {
// No more outputs for this path
}
}
const displayType = inputType || outputType || "*";
const color = LGraphCanvas.link_type_colors[displayType];
// Update the types of each node
for (const node of updateNodes) {
// If we dont have an input type we are always wildcard but we'll show the output type
// This lets you change the output link to a different type and all nodes will update
node.outputs[0].type = inputType || "*";
(node as any).__outputType = displayType;
node.outputs[0].name = node.properties.showOutputText ? String(displayType) : "";
node.size = node.computeSize();
if ("applyOrientation" in node && typeof node.applyOrientation === "function")
node.applyOrientation();
for (const l of node.outputs[0].links || []) {
const link = this.graph.links[l];
if (link) {
link.color = color;
}
}
}
if (inputNode) {
const link = this.graph.links[inputNode.inputs[0].link];
if (link) {
link.color = color;
}
}
};
override clone(): LGraphNode {
const cloned = super.clone.apply(this) as ComfyReroute;
cloned.removeOutput(0);
cloned.addOutput(this.properties.showOutputText ? "*" : "", "*");
cloned.size = cloned.computeSize();
return cloned;
};
override getExtraMenuOptions(_, options: ContextMenuItem[]): ContextMenuItem[] | null {
options.unshift(
{
content: (this.properties.showOutputText ? "Hide" : "Show") + " Type",
callback: () => {
this.properties.showOutputText = !this.properties.showOutputText;
if (this.properties.showOutputText) {
this.outputs[0].name = (this as any).__outputType || this.outputs[0].type;
} else {
this.outputs[0].name = "";
}
this.size = this.computeSize();
this.applyOrientation();
this.graph.setDirtyCanvas(true, true);
},
},
{
content: (ComfyReroute.defaultVisibility ? "Hide" : "Show") + " Type By Default",
callback: () => {
ComfyReroute.setDefaultTextVisibility(!ComfyReroute.defaultVisibility);
},
},
{
// naming is inverted with respect to LiteGraphNode.horizontal
// LiteGraphNode.horizontal == true means that
// each slot in the inputs and outputs are layed out horizontally,
// which is the opposite of the visual orientation of the inputs and outputs as a node
content: "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"),
callback: () => {
this.properties.horizontal = !this.properties.horizontal;
this.applyOrientation();
},
}
);
return null
}
applyOrientation() {
this.horizontal = this.properties.horizontal;
if (this.horizontal) {
// we correct the input position, because LiteGraphNode.horizontal
// doesn't account for title presence
// which reroute nodes don't have
this.inputs[0].pos = [this.size[0] / 2, 0];
} else {
delete this.inputs[0].pos;
}
this.graph.setDirtyCanvas(true, true);
}
override computeSize(): Vector2 {
return [
this.properties.showOutputText && this.outputs && this.outputs.length
? Math.max(75, LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40)
: 75,
26,
];
}
}
LiteGraph.registerNodeType({
class: ComfyReroute,
title: "Comfy.Reroute",
desc: "Reroutes nodes preserving input/output types",
type: "utils/reroute"
})

1
src/lib/nodes/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as ComfyReroute } from "./ComfyReroute"

View File

@@ -1,4 +1,4 @@
import { LiteGraph, LGraph, LGraphNode } from "litegraph.js"
import { LiteGraph, LGraph, LGraphNode } from "@litegraph-ts/core"
import type ComfyAPI from "$lib/api"
class PNGMetadataPromise extends Promise<Record<string, string>> {
@@ -17,308 +17,308 @@ class PNGMetadataPromise extends Promise<Record<string, string>> {
}
export function getPngMetadata(file: File): PNGMetadataPromise {
return new PNGMetadataPromise((r, _) => {
const reader = new FileReader();
reader.onload = (event: Event) => {
// Get the PNG data as a Uint8Array
const pngData = new Uint8Array((event.target as any).result);
const dataView = new DataView(pngData.buffer);
return new PNGMetadataPromise((r, _) => {
const reader = new FileReader();
reader.onload = (event: Event) => {
// Get the PNG data as a Uint8Array
const pngData = new Uint8Array((event.target as any).result);
const dataView = new DataView(pngData.buffer);
// Check that the PNG signature is present
if (dataView.getUint32(0) !== 0x89504e47) {
console.error("Not a valid PNG file");
r();
return;
}
// Check that the PNG signature is present
if (dataView.getUint32(0) !== 0x89504e47) {
console.error("Not a valid PNG file");
r();
return;
}
// Start searching for chunks after the PNG signature
let offset = 8;
let txt_chunks = {};
// Loop through the chunks in the PNG file
while (offset < pngData.length) {
// Get the length of the chunk
const length = dataView.getUint32(offset);
// Get the chunk type
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8));
if (type === "tEXt") {
// Get the keyword
let keyword_end = offset + 8;
while (pngData[keyword_end] !== 0) {
keyword_end++;
}
const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end));
// Get the text
const text = String.fromCharCode(...pngData.slice(keyword_end + 1, offset + 8 + length));
txt_chunks[keyword] = text;
}
// Start searching for chunks after the PNG signature
let offset = 8;
let txt_chunks = {};
// Loop through the chunks in the PNG file
while (offset < pngData.length) {
// Get the length of the chunk
const length = dataView.getUint32(offset);
// Get the chunk type
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8));
if (type === "tEXt") {
// Get the keyword
let keyword_end = offset + 8;
while (pngData[keyword_end] !== 0) {
keyword_end++;
}
const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end));
// Get the text
const text = String.fromCharCode(...pngData.slice(keyword_end + 1, offset + 8 + length));
txt_chunks[keyword] = text;
}
offset += 12 + length;
}
offset += 12 + length;
}
r(txt_chunks);
};
r(txt_chunks);
};
reader.readAsArrayBuffer(file);
});
reader.readAsArrayBuffer(file);
});
}
type NodeIndex = { node: LGraphNode, index: number }
export async function importA1111(graph: LGraph, parameters: string, api: ComfyAPI) {
const p = parameters.lastIndexOf("\nSteps:");
if (p > -1) {
const embeddings = await api.getEmbeddings();
const opts = parameters
.substr(p)
.split(",")
.reduce((p, n) => {
const s = n.split(":");
p[s[0].trim().toLowerCase()] = s[1].trim();
return p;
}, {});
const p2 = parameters.lastIndexOf("\nNegative prompt:", p);
if (p2 > -1) {
let positive = parameters.substr(0, p2).trim();
let negative = parameters.substring(p2 + 18, p).trim();
const p = parameters.lastIndexOf("\nSteps:");
if (p > -1) {
const embeddings = await api.getEmbeddings();
const opts = parameters
.substr(p)
.split(",")
.reduce((p, n) => {
const s = n.split(":");
p[s[0].trim().toLowerCase()] = s[1].trim();
return p;
}, {});
const p2 = parameters.lastIndexOf("\nNegative prompt:", p);
if (p2 > -1) {
let positive = parameters.substr(0, p2).trim();
let negative = parameters.substring(p2 + 18, p).trim();
const ckptNode = LiteGraph.createNode("CheckpointLoaderSimple");
const clipSkipNode = LiteGraph.createNode("CLIPSetLastLayer");
const positiveNode = LiteGraph.createNode("CLIPTextEncode");
const negativeNode = LiteGraph.createNode("CLIPTextEncode");
const samplerNode = LiteGraph.createNode("KSampler");
const imageNode = LiteGraph.createNode("EmptyLatentImage");
const vaeNode = LiteGraph.createNode("VAEDecode");
const vaeLoaderNode = LiteGraph.createNode("VAELoader");
const saveNode = LiteGraph.createNode("SaveImage");
let hrSamplerNode = null;
const ckptNode = LiteGraph.createNode("CheckpointLoaderSimple");
const clipSkipNode = LiteGraph.createNode("CLIPSetLastLayer");
const positiveNode = LiteGraph.createNode("CLIPTextEncode");
const negativeNode = LiteGraph.createNode("CLIPTextEncode");
const samplerNode = LiteGraph.createNode("KSampler");
const imageNode = LiteGraph.createNode("EmptyLatentImage");
const vaeNode = LiteGraph.createNode("VAEDecode");
const vaeLoaderNode = LiteGraph.createNode("VAELoader");
const saveNode = LiteGraph.createNode("SaveImage");
let hrSamplerNode = null;
const ceil64 = (v) => Math.ceil(v / 64) * 64;
const ceil64 = (v) => Math.ceil(v / 64) * 64;
function getWidget(node: LGraphNode, name: string) {
return node.widgets.find((w) => w.name === name);
}
function getWidget(node: LGraphNode, name: string) {
return node.widgets.find((w) => w.name === name);
}
function setWidgetValue(node: LGraphNode, name: string, value: any, isOptionPrefix: boolean = false) {
const w = getWidget(node, name);
if (isOptionPrefix) {
const o = w.options.values.find((w) => w.startsWith(value));
if (o) {
w.value = o;
} else {
console.warn(`Unknown value '${value}' for widget '${name}'`, node);
w.value = value;
}
} else {
w.value = value;
}
}
function setWidgetValue(node: LGraphNode, name: string, value: any, isOptionPrefix: boolean = false) {
const w = getWidget(node, name);
if (isOptionPrefix) {
const o = w.options.values.find((w) => w.startsWith(value));
if (o) {
w.value = o;
} else {
console.warn(`Unknown value '${value}' for widget '${name}'`, node);
w.value = value;
}
} else {
w.value = value;
}
}
function createLoraNodes(clipNode: LGraphNode, text: string, prevClip: NodeIndex, prevModel: NodeIndex) {
const loras = [];
text = text.replace(/<lora:([^:]+:[^>]+)>/g, function (m, c) {
const s = c.split(":");
const weight = parseFloat(s[1]);
if (isNaN(weight)) {
console.warn("Invalid LORA", m);
} else {
loras.push({ name: s[0], weight });
}
return "";
});
function createLoraNodes(clipNode: LGraphNode, text: string, prevClip: NodeIndex, prevModel: NodeIndex) {
const loras = [];
text = text.replace(/<lora:([^:]+:[^>]+)>/g, function(m, c) {
const s = c.split(":");
const weight = parseFloat(s[1]);
if (isNaN(weight)) {
console.warn("Invalid LORA", m);
} else {
loras.push({ name: s[0], weight });
}
return "";
});
for (const l of loras) {
const loraNode = LiteGraph.createNode("LoraLoader");
graph.add(loraNode);
setWidgetValue(loraNode, "lora_name", l.name, true);
setWidgetValue(loraNode, "strength_model", l.weight);
setWidgetValue(loraNode, "strength_clip", l.weight);
prevModel.node.connect(prevModel.index, loraNode, 0);
prevClip.node.connect(prevClip.index, loraNode, 1);
prevModel = { node: loraNode, index: 0 };
prevClip = { node: loraNode, index: 1 };
}
for (const l of loras) {
const loraNode = LiteGraph.createNode("LoraLoader");
graph.add(loraNode);
setWidgetValue(loraNode, "lora_name", l.name, true);
setWidgetValue(loraNode, "strength_model", l.weight);
setWidgetValue(loraNode, "strength_clip", l.weight);
prevModel.node.connect(prevModel.index, loraNode, 0);
prevClip.node.connect(prevClip.index, loraNode, 1);
prevModel = { node: loraNode, index: 0 };
prevClip = { node: loraNode, index: 1 };
}
prevClip.node.connect(1, clipNode, 0);
prevModel.node.connect(0, samplerNode, 0);
if (hrSamplerNode) {
prevModel.node.connect(0, hrSamplerNode, 0);
}
prevClip.node.connect(1, clipNode, 0);
prevModel.node.connect(0, samplerNode, 0);
if (hrSamplerNode) {
prevModel.node.connect(0, hrSamplerNode, 0);
}
return { text, prevModel, prevClip };
}
return { text, prevModel, prevClip };
}
function replaceEmbeddings(text: string) {
return text.replaceAll(
new RegExp(
"\\b(" + embeddings.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\b|\\b") + ")\\b",
"ig"
),
"embedding:$1"
);
}
function replaceEmbeddings(text: string) {
return text.replaceAll(
new RegExp(
"\\b(" + embeddings.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\b|\\b") + ")\\b",
"ig"
),
"embedding:$1"
);
}
function popOpt(name: string) {
const v = opts[name];
delete opts[name];
return v;
}
function popOpt(name: string) {
const v = opts[name];
delete opts[name];
return v;
}
graph.clear();
graph.add(ckptNode);
graph.add(clipSkipNode);
graph.add(positiveNode);
graph.add(negativeNode);
graph.add(samplerNode);
graph.add(imageNode);
graph.add(vaeNode);
graph.add(vaeLoaderNode);
graph.add(saveNode);
graph.clear();
graph.add(ckptNode);
graph.add(clipSkipNode);
graph.add(positiveNode);
graph.add(negativeNode);
graph.add(samplerNode);
graph.add(imageNode);
graph.add(vaeNode);
graph.add(vaeLoaderNode);
graph.add(saveNode);
ckptNode.connect(1, clipSkipNode, 0);
clipSkipNode.connect(0, positiveNode, 0);
clipSkipNode.connect(0, negativeNode, 0);
ckptNode.connect(0, samplerNode, 0);
positiveNode.connect(0, samplerNode, 1);
negativeNode.connect(0, samplerNode, 2);
imageNode.connect(0, samplerNode, 3);
vaeNode.connect(0, saveNode, 0);
samplerNode.connect(0, vaeNode, 0);
vaeLoaderNode.connect(0, vaeNode, 1);
ckptNode.connect(1, clipSkipNode, 0);
clipSkipNode.connect(0, positiveNode, 0);
clipSkipNode.connect(0, negativeNode, 0);
ckptNode.connect(0, samplerNode, 0);
positiveNode.connect(0, samplerNode, 1);
negativeNode.connect(0, samplerNode, 2);
imageNode.connect(0, samplerNode, 3);
vaeNode.connect(0, saveNode, 0);
samplerNode.connect(0, vaeNode, 0);
vaeLoaderNode.connect(0, vaeNode, 1);
const handlers = {
model(v: string) {
setWidgetValue(ckptNode, "ckpt_name", v, true);
},
"cfg scale"(v: number) {
setWidgetValue(samplerNode, "cfg", +v);
},
"clip skip"(v: number) {
setWidgetValue(clipSkipNode, "stop_at_clip_layer", -v);
},
sampler(v: string) {
let name = v.toLowerCase().replace("++", "pp").replaceAll(" ", "_");
if (name.includes("karras")) {
name = name.replace("karras", "").replace(/_+$/, "");
setWidgetValue(samplerNode, "scheduler", "karras");
} else {
setWidgetValue(samplerNode, "scheduler", "normal");
}
const w = getWidget(samplerNode, "sampler_name");
const o = w.options.values.find((w) => w === name || w === "sample_" + name);
if (o) {
setWidgetValue(samplerNode, "sampler_name", o);
}
},
size(v: string) {
const wxh = v.split("x");
const w = ceil64(+wxh[0]);
const h = ceil64(+wxh[1]);
const hrUp = popOpt("hires upscale");
const hrSz = popOpt("hires resize");
let hrMethod = popOpt("hires upscaler");
const handlers = {
model(v: string) {
setWidgetValue(ckptNode, "ckpt_name", v, true);
},
"cfg scale"(v: number) {
setWidgetValue(samplerNode, "cfg", +v);
},
"clip skip"(v: number) {
setWidgetValue(clipSkipNode, "stop_at_clip_layer", -v);
},
sampler(v: string) {
let name = v.toLowerCase().replace("++", "pp").replaceAll(" ", "_");
if (name.includes("karras")) {
name = name.replace("karras", "").replace(/_+$/, "");
setWidgetValue(samplerNode, "scheduler", "karras");
} else {
setWidgetValue(samplerNode, "scheduler", "normal");
}
const w = getWidget(samplerNode, "sampler_name");
const o = w.options.values.find((w) => w === name || w === "sample_" + name);
if (o) {
setWidgetValue(samplerNode, "sampler_name", o);
}
},
size(v: string) {
const wxh = v.split("x");
const w = ceil64(+wxh[0]);
const h = ceil64(+wxh[1]);
const hrUp = popOpt("hires upscale");
const hrSz = popOpt("hires resize");
let hrMethod = popOpt("hires upscaler");
setWidgetValue(imageNode, "width", w);
setWidgetValue(imageNode, "height", h);
setWidgetValue(imageNode, "width", w);
setWidgetValue(imageNode, "height", h);
if (hrUp || hrSz) {
let uw, uh;
if (hrUp) {
uw = w * hrUp;
uh = h * hrUp;
} else {
const s = hrSz.split("x");
uw = +s[0];
uh = +s[1];
}
if (hrUp || hrSz) {
let uw, uh;
if (hrUp) {
uw = w * hrUp;
uh = h * hrUp;
} else {
const s = hrSz.split("x");
uw = +s[0];
uh = +s[1];
}
let upscaleNode: LGraphNode;
let latentNode: LGraphNode;
let upscaleNode: LGraphNode;
let latentNode: LGraphNode;
if (hrMethod.startsWith("Latent")) {
latentNode = upscaleNode = LiteGraph.createNode("LatentUpscale");
graph.add(upscaleNode);
samplerNode.connect(0, upscaleNode, 0);
if (hrMethod.startsWith("Latent")) {
latentNode = upscaleNode = LiteGraph.createNode("LatentUpscale");
graph.add(upscaleNode);
samplerNode.connect(0, upscaleNode, 0);
switch (hrMethod) {
case "Latent (nearest-exact)":
hrMethod = "nearest-exact";
break;
}
setWidgetValue(upscaleNode, "upscale_method", hrMethod, true);
} else {
const decode = LiteGraph.createNode("VAEDecodeTiled");
graph.add(decode);
samplerNode.connect(0, decode, 0);
vaeLoaderNode.connect(0, decode, 1);
switch (hrMethod) {
case "Latent (nearest-exact)":
hrMethod = "nearest-exact";
break;
}
setWidgetValue(upscaleNode, "upscale_method", hrMethod, true);
} else {
const decode = LiteGraph.createNode("VAEDecodeTiled");
graph.add(decode);
samplerNode.connect(0, decode, 0);
vaeLoaderNode.connect(0, decode, 1);
const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader");
graph.add(upscaleLoaderNode);
setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true);
const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader");
graph.add(upscaleLoaderNode);
setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true);
const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel");
graph.add(modelUpscaleNode);
decode.connect(0, modelUpscaleNode, 1);
upscaleLoaderNode.connect(0, modelUpscaleNode, 0);
const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel");
graph.add(modelUpscaleNode);
decode.connect(0, modelUpscaleNode, 1);
upscaleLoaderNode.connect(0, modelUpscaleNode, 0);
upscaleNode = LiteGraph.createNode("ImageScale");
graph.add(upscaleNode);
modelUpscaleNode.connect(0, upscaleNode, 0);
upscaleNode = LiteGraph.createNode("ImageScale");
graph.add(upscaleNode);
modelUpscaleNode.connect(0, upscaleNode, 0);
const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled"));
graph.add(vaeEncodeNode);
upscaleNode.connect(0, vaeEncodeNode, 0);
vaeLoaderNode.connect(0, vaeEncodeNode, 1);
}
const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled"));
graph.add(vaeEncodeNode);
upscaleNode.connect(0, vaeEncodeNode, 0);
vaeLoaderNode.connect(0, vaeEncodeNode, 1);
}
setWidgetValue(upscaleNode, "width", ceil64(uw));
setWidgetValue(upscaleNode, "height", ceil64(uh));
setWidgetValue(upscaleNode, "width", ceil64(uw));
setWidgetValue(upscaleNode, "height", ceil64(uh));
hrSamplerNode = LiteGraph.createNode("KSampler");
graph.add(hrSamplerNode);
ckptNode.connect(0, hrSamplerNode, 0);
positiveNode.connect(0, hrSamplerNode, 1);
negativeNode.connect(0, hrSamplerNode, 2);
latentNode.connect(0, hrSamplerNode, 3);
hrSamplerNode.connect(0, vaeNode, 0);
}
},
steps(v: number) {
setWidgetValue(samplerNode, "steps", +v);
},
seed(v: number) {
setWidgetValue(samplerNode, "seed", +v);
},
};
hrSamplerNode = LiteGraph.createNode("KSampler");
graph.add(hrSamplerNode);
ckptNode.connect(0, hrSamplerNode, 0);
positiveNode.connect(0, hrSamplerNode, 1);
negativeNode.connect(0, hrSamplerNode, 2);
latentNode.connect(0, hrSamplerNode, 3);
hrSamplerNode.connect(0, vaeNode, 0);
}
},
steps(v: number) {
setWidgetValue(samplerNode, "steps", +v);
},
seed(v: number) {
setWidgetValue(samplerNode, "seed", +v);
},
};
for (const opt in opts) {
if (opt in handlers) {
handlers[opt](popOpt(opt));
}
}
for (const opt in opts) {
if (opt in handlers) {
handlers[opt](popOpt(opt));
}
}
if (hrSamplerNode) {
setWidgetValue(hrSamplerNode, "steps", getWidget(samplerNode, "steps").value);
setWidgetValue(hrSamplerNode, "cfg", getWidget(samplerNode, "cfg").value);
setWidgetValue(hrSamplerNode, "scheduler", getWidget(samplerNode, "scheduler").value);
setWidgetValue(hrSamplerNode, "sampler_name", getWidget(samplerNode, "sampler_name").value);
setWidgetValue(hrSamplerNode, "denoise", +(popOpt("denoising strength") || "1"));
}
if (hrSamplerNode) {
setWidgetValue(hrSamplerNode, "steps", getWidget(samplerNode, "steps").value);
setWidgetValue(hrSamplerNode, "cfg", getWidget(samplerNode, "cfg").value);
setWidgetValue(hrSamplerNode, "scheduler", getWidget(samplerNode, "scheduler").value);
setWidgetValue(hrSamplerNode, "sampler_name", getWidget(samplerNode, "sampler_name").value);
setWidgetValue(hrSamplerNode, "denoise", +(popOpt("denoising strength") || "1"));
}
let n = createLoraNodes(positiveNode, positive, { node: clipSkipNode, index: 0 }, { node: ckptNode, index: 0 });
positive = n.text;
n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel);
negative = n.text;
let n = createLoraNodes(positiveNode, positive, { node: clipSkipNode, index: 0 }, { node: ckptNode, index: 0 });
positive = n.text;
n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel);
negative = n.text;
setWidgetValue(positiveNode, "text", replaceEmbeddings(positive));
setWidgetValue(negativeNode, "text", replaceEmbeddings(negative));
setWidgetValue(positiveNode, "text", replaceEmbeddings(positive));
setWidgetValue(negativeNode, "text", replaceEmbeddings(negative));
graph.arrange();
graph.arrange();
for (const opt of ["model hash", "ensd"]) {
delete opts[opt];
}
for (const opt of ["model hash", "ensd"]) {
delete opts[opt];
}
console.warn("Unhandled parameters:", opts);
}
}
console.warn("Unhandled parameters:", opts);
}
}
}

View File

@@ -1,5 +1,5 @@
import { writable, get } from 'svelte/store';
import type { LGraph, LGraphNode, IWidget } from "litegraph.js";
import type { LGraph, LGraphNode, IWidget } from "@litegraph-ts/core";
import type { Readable, Writable } from 'svelte/store';
export type WidgetUIState = {
@@ -64,10 +64,10 @@ function configureFinished(graph: LGraph) {
}
export default
{
...store,
nodeAdded,
nodeRemoved,
configureFinished,
clear
} as WidgetStateStore;
{
...store,
nodeAdded,
nodeRemoved,
configureFinished,
clear
} as WidgetStateStore;

View File

@@ -1,4 +1,4 @@
import type { IWidget, LGraphNode } from "litegraph.js";
import type { IWidget, LGraphNode } from "@litegraph-js/core";
import type ComfyApp from "$lib/components/ComfyApp";
export interface WidgetData {
@@ -28,7 +28,7 @@ function getNumberDefaults(inputData: any, defaultStep: number): NumberDefaults
const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): WidgetData => {
const { val, config } = getNumberDefaults(inputData, 0.5);
return { widget: node.addWidget("number", inputName, val, () => {}, config) };
return { widget: node.addWidget("number", inputName, val, () => { }, config) };
}
@@ -39,7 +39,7 @@ const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any)
"number",
inputName,
val,
function (v) {
function(v) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
},
@@ -55,7 +55,7 @@ const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: a
// if (multiline) {
// return addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
// } else {
return { widget: node.addWidget("text", inputName, defaultVal, () => {}, { multiline }) };
return { widget: node.addWidget("text", inputName, defaultVal, () => { }, { multiline }) };
// }
};
@@ -65,7 +65,7 @@ const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: an
if (inputData[1] && inputData[1].default) {
defaultValue = inputData[1].default;
}
return { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) };
return { widget: node.addWidget("combo", inputName, defaultValue, () => { }, { values: type }) };
}
const IMAGEUPLOAD: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any, app): WidgetData => {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import ComfyApp from "$lib/components/ComfyApp.svelte"
import "litegraph.js/css/litegraph.css";
import "@litegraph-ts/core/css/litegraph.css";
</script>
<ComfyApp/>

View File

@@ -9,8 +9,8 @@ type OutputProps = {}
// contain a `PageLoad` type with a `params` and `data` object that matches our route.
// You need to run the dev server or `svelte-kit sync` to generate them.
export const load: PageLoad<OutputProps> = async ({
params,
data,
params,
data,
}) => {
return {}
return {}
}

File diff suppressed because it is too large Load Diff