Convert primitive nodes
This commit is contained in:
@@ -51,7 +51,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function doRefreshCombos() {
|
async function doRefreshCombos() {
|
||||||
await app.refreshComboInNodes(undefined, true)
|
await app.refreshComboInNodes(undefined, undefined, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshView(event?: Event) {
|
function refreshView(event?: Event) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { LGraph, type INodeInputSlot, type SerializedLGraph, type LinkID, type UUID, type NodeID, LiteGraph, BuiltInSlotType, type SerializedLGraphNode, type Vector2, BuiltInSlotShape, type INodeOutputSlot } from "@litegraph-ts/core";
|
import { LGraph, type INodeInputSlot, type SerializedLGraph, type LinkID, type UUID, type NodeID, LiteGraph, BuiltInSlotType, type SerializedLGraphNode, type Vector2, BuiltInSlotShape, type INodeOutputSlot, type SlotType } from "@litegraph-ts/core";
|
||||||
import type { SerializedAppState } from "./components/ComfyApp";
|
import type { SerializedAppState } from "./components/ComfyApp";
|
||||||
import layoutStates, { defaultWorkflowAttributes, type ContainerLayout, type DragItemID, type SerializedDragEntry, type SerializedLayoutState, type WritableLayoutStateStore } from "./stores/layoutStates";
|
import layoutStates, { defaultWorkflowAttributes, type ContainerLayout, type DragItemID, type SerializedDragEntry, type SerializedLayoutState, type WritableLayoutStateStore } from "./stores/layoutStates";
|
||||||
import { ComfyWorkflow, type WorkflowAttributes } from "./stores/workflowState";
|
import { ComfyWorkflow, type WorkflowAttributes } from "./stores/workflowState";
|
||||||
import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas";
|
import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas";
|
||||||
import ComfyApp from "./components/ComfyApp";
|
import ComfyApp from "./components/ComfyApp";
|
||||||
import { iterateNodeDefInputs } from "./ComfyNodeDef";
|
import { iterateNodeDefInputs, type ComfyNodeDefInputType, type ComfyNodeDefInputOptions } from "./ComfyNodeDef";
|
||||||
import type { ComfyNodeDefInput } from "./ComfyNodeDef";
|
import type { ComfyNodeDefInput } from "./ComfyNodeDef";
|
||||||
import type IComfyInputSlot from "./IComfyInputSlot";
|
import type IComfyInputSlot from "./IComfyInputSlot";
|
||||||
import ComfyWidgets from "./widgets"
|
import ComfyWidgets from "./widgets"
|
||||||
@@ -28,10 +28,20 @@ type ComfyUIConvertedWidget = {
|
|||||||
config: ComfyNodeDefInput
|
config: ComfyNodeDefInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Input slot for widgets converted to inputs
|
||||||
|
*/
|
||||||
interface IComfyUINodeInputSlot extends INodeInputSlot {
|
interface IComfyUINodeInputSlot extends INodeInputSlot {
|
||||||
widget?: ComfyUIConvertedWidget
|
widget?: ComfyUIConvertedWidget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Output slot for PrimitiveNode
|
||||||
|
*/
|
||||||
|
interface IComfyUINodeOutputSlot extends INodeOutputSlot {
|
||||||
|
widget?: ComfyUIConvertedWidget
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ComfyUI frontend nodes that should be converted directly to another type.
|
* ComfyUI frontend nodes that should be converted directly to another type.
|
||||||
*/
|
*/
|
||||||
@@ -158,6 +168,168 @@ function rewriteIDsInGraph(vanillaWorkflow: ComfyVanillaWorkflow) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns [nodeType, inputType] for a config type, like "FLOAT" -> ["ui/number", "number"]
|
||||||
|
*/
|
||||||
|
function getWidgetTypesFromConfig(inputType: ComfyNodeDefInputType): [string, SlotType] | null {
|
||||||
|
let widgetNodeType = null;
|
||||||
|
let widgetInputType = null;
|
||||||
|
|
||||||
|
if (Array.isArray(inputType)) {
|
||||||
|
// Combo options of string[]
|
||||||
|
widgetNodeType = "ui/combo";
|
||||||
|
widgetInputType = "string"
|
||||||
|
}
|
||||||
|
else if (inputType in ComfyWidgets) {
|
||||||
|
// Widget type
|
||||||
|
const widgetFactory = ComfyWidgets[inputType]
|
||||||
|
widgetNodeType = widgetFactory.nodeType;
|
||||||
|
widgetInputType = widgetFactory.inputType
|
||||||
|
}
|
||||||
|
else if ("${inputType}:{inputName}" in ComfyWidgets) {
|
||||||
|
// Widget type override for input of type with given name ("seed", "noise_seed")
|
||||||
|
const widgetFactory = ComfyWidgets["${inputType}:{inputName}"]
|
||||||
|
widgetNodeType = widgetFactory.nodeType;
|
||||||
|
widgetInputType = widgetFactory.inputType
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Backend type, we can safely ignore this
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [widgetNodeType, widgetInputType]
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureWidgetNodeProperties(serWidgetNode: SerializedComfyWidgetNode, inputOpts?: ComfyNodeDefInputOptions) {
|
||||||
|
inputOpts ||= {}
|
||||||
|
switch (serWidgetNode.type) {
|
||||||
|
case "ui/number":
|
||||||
|
serWidgetNode.properties.min = inputOpts.min || 0;
|
||||||
|
serWidgetNode.properties.max = inputOpts.max || 100;
|
||||||
|
serWidgetNode.properties.step = inputOpts.step || 1;
|
||||||
|
break;
|
||||||
|
case "ui/text":
|
||||||
|
serWidgetNode.properties.multiline = inputOpts.multiline || false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Attempts to convert a primitive node
|
||||||
|
* The primitive node should be pruned from the graph afterwards
|
||||||
|
*/
|
||||||
|
function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: SerializedLGraphNode, layoutState: WritableLayoutStateStore, group: ContainerLayout): boolean {
|
||||||
|
// Get the value output
|
||||||
|
// On primitive nodes it's the one in the first slot
|
||||||
|
const mainOutput = (node.outputs || [])[0] as IComfyUINodeOutputSlot;
|
||||||
|
if (!mainOutput || !mainOutput.links) {
|
||||||
|
console.error("PrimitiveNode output had no output with links!", node)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = mainOutput.widget;
|
||||||
|
if (widget === null) {
|
||||||
|
console.error("PrimitiveNode output had no widget config!", node)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [widgetType, widgetOpts] = widget.config
|
||||||
|
|
||||||
|
if (!node.widgets_values) {
|
||||||
|
console.error("PrimitiveNode had no serialized widget values!", node)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pair = getWidgetTypesFromConfig(widgetType);
|
||||||
|
if (pair == null) {
|
||||||
|
// This should never happen! Primitive nodes only deal with frontend types!
|
||||||
|
console.error("PrimitiveNode had a backend type configured!", node)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [widgetNodeType, widgetInputType] = pair
|
||||||
|
|
||||||
|
// PrimitiveNode will have a widget in the first slot with the actual value.
|
||||||
|
// The rest are configuration values for e.g. seed action onprompt queue.
|
||||||
|
const value = node.widgets_values[0];
|
||||||
|
|
||||||
|
const [comfyWidgetNode, serWidgetNode] = createSerializedWidgetNode(
|
||||||
|
vanillaWorkflow,
|
||||||
|
node,
|
||||||
|
0, // first output on the PrimitiveNode
|
||||||
|
false, // this is an output slot index
|
||||||
|
widgetNodeType,
|
||||||
|
value);
|
||||||
|
|
||||||
|
configureWidgetNodeProperties(serWidgetNode, widgetOpts)
|
||||||
|
|
||||||
|
let foundTitle = null;
|
||||||
|
const widgetLayout = layoutState.addWidget(group, comfyWidgetNode)
|
||||||
|
widgetLayout.attrs.title = mainOutput.name;
|
||||||
|
|
||||||
|
// Rewrite links to point to the new widget node
|
||||||
|
const newLinkOutputSlot = serWidgetNode.outputs.findIndex(o => o.name === comfyWidgetNode.outputSlotName)
|
||||||
|
if (newLinkOutputSlot !== -1) {
|
||||||
|
const newLinkOutput = serWidgetNode.outputs[newLinkOutputSlot];
|
||||||
|
// TODO other links need pruning?
|
||||||
|
for (const linkID of mainOutput.links) {
|
||||||
|
const link = vanillaWorkflow.links.find(l => l[0] === linkID)
|
||||||
|
if (link) {
|
||||||
|
link[1] = serWidgetNode.id; // origin node ID
|
||||||
|
link[2] = newLinkOutputSlot; // origin node slot
|
||||||
|
newLinkOutput.links ||= []
|
||||||
|
newLinkOutput.links.push(linkID)
|
||||||
|
|
||||||
|
// Change the title of the widget to the name of the first input connected to
|
||||||
|
if (foundTitle == null) {
|
||||||
|
const targetNode = vanillaWorkflow.nodes.find(n => n.id === link[3]) // target node ID
|
||||||
|
if (targetNode != null) {
|
||||||
|
const foundInput = targetNode.inputs[link[4]] // target node slot
|
||||||
|
if (foundInput != null && foundInput.name) {
|
||||||
|
foundTitle = foundInput.name;
|
||||||
|
widgetLayout.attrs.title = foundTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove links on the old node so they won't be double-removed when it's pruned
|
||||||
|
mainOutput.links = []
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error("Could not find output slot for new widget node!", comfyWidgetNode, serWidgetNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSerializedNode(vanillaWorkflow: SerializedLGraph, node: SerializedLGraphNode) {
|
||||||
|
if (node.outputs) {
|
||||||
|
for (const output of node.outputs) {
|
||||||
|
if (output.links) {
|
||||||
|
vanillaWorkflow.links = vanillaWorkflow.links.filter(l => output.links.indexOf(l[0]) === -1);
|
||||||
|
output.links = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.inputs) {
|
||||||
|
for (const input of node.inputs) {
|
||||||
|
if (input.link) {
|
||||||
|
vanillaWorkflow.links = vanillaWorkflow.links.filter(l => input.link !== l[0]);
|
||||||
|
input.link = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vanillaWorkflow.nodes = vanillaWorkflow.nodes.filter(n => n.id !== node.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Converts a workflow saved with vanilla ComfyUI into a ComfyBox workflow,
|
||||||
|
* adding UI nodes for each widget.
|
||||||
|
*
|
||||||
|
* TODO: test this!
|
||||||
|
*/
|
||||||
export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): [ComfyWorkflow, WritableLayoutStateStore] {
|
export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): [ComfyWorkflow, WritableLayoutStateStore] {
|
||||||
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
|
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
|
||||||
const { root, left, right } = layoutState.initDefaultLayout();
|
const { root, left, right } = layoutState.initDefaultLayout();
|
||||||
@@ -186,9 +358,17 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
|
|||||||
// serialized widgets into ComfyWidgetNodes, add new inputs/outputs,
|
// serialized widgets into ComfyWidgetNodes, add new inputs/outputs,
|
||||||
// then attach the new nodes to the slots
|
// then attach the new nodes to the slots
|
||||||
|
|
||||||
|
// Primitive nodes are special since they can interface with converted
|
||||||
|
// widget inputs
|
||||||
|
if (node.type === "PrimitiveNode") {
|
||||||
|
convertPrimitiveNode(vanillaWorkflow, node, layoutState, left)
|
||||||
|
removeSerializedNode(vanillaWorkflow, node);
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const def = ComfyApp.knownBackendNodes[node.type];
|
const def = ComfyApp.knownBackendNodes[node.type];
|
||||||
if (def == null) {
|
if (def == null) {
|
||||||
console.error("Unknown backend node", node.type)
|
console.error("[convertVanillaWorkflow] Unknown backend node", node.type)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,31 +393,14 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
|
|||||||
// This input is a widget, it should be converted to an input
|
// This input is a widget, it should be converted to an input
|
||||||
// connected to a ComfyWidgetNode.
|
// connected to a ComfyWidgetNode.
|
||||||
|
|
||||||
let widgetNodeType = null;
|
let pair = getWidgetTypesFromConfig(inputType);
|
||||||
let widgetInputType = null;
|
if (pair == null) {
|
||||||
|
// Input type is backend-only, we can skip adding a UI node here
|
||||||
if (Array.isArray(inputType)) {
|
|
||||||
// Combo options of string[]
|
|
||||||
widgetInputType = "string"
|
|
||||||
widgetNodeType = "ui/combo";
|
|
||||||
}
|
|
||||||
else if (inputType in ComfyWidgets) {
|
|
||||||
// Widget type
|
|
||||||
const widgetFactory = ComfyWidgets[inputType]
|
|
||||||
widgetInputType = widgetFactory.inputType
|
|
||||||
widgetNodeType = widgetFactory.nodeType;
|
|
||||||
}
|
|
||||||
else if ("${inputType}:{inputName}" in ComfyWidgets) {
|
|
||||||
// Widget type override for input of type with given name ("seed", "noise_seed")
|
|
||||||
const widgetFactory = ComfyWidgets["${inputType}:{inputName}"]
|
|
||||||
widgetInputType = widgetFactory.inputType
|
|
||||||
widgetNodeType = widgetFactory.nodeType;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Backend type, we can safely ignore this
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let [widgetNodeType, widgetInputType] = pair
|
||||||
|
|
||||||
const newInput: IComfyInputSlot = {
|
const newInput: IComfyInputSlot = {
|
||||||
name: inputName,
|
name: inputName,
|
||||||
link: null,
|
link: null,
|
||||||
@@ -264,16 +427,7 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
|
|||||||
widgetNodeType,
|
widgetNodeType,
|
||||||
value);
|
value);
|
||||||
|
|
||||||
switch (widgetNodeType) {
|
configureWidgetNodeProperties(serWidgetNode, inputOpts)
|
||||||
case "ui/number":
|
|
||||||
serWidgetNode.properties.min = inputOpts?.min || 0;
|
|
||||||
serWidgetNode.properties.max = inputOpts?.max || 100;
|
|
||||||
serWidgetNode.properties.step = inputOpts?.step || 1;
|
|
||||||
break;
|
|
||||||
case "ui/text":
|
|
||||||
serWidgetNode.properties.multiline = inputOpts?.multiline || false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group == null)
|
if (group == null)
|
||||||
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type })
|
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type })
|
||||||
|
|||||||
135
src/tests/data/convertedWidgetAndPrimitiveNode.json
Normal file
135
src/tests/data/convertedWidgetAndPrimitiveNode.json
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"last_node_id": 2,
|
||||||
|
"last_link_id": 1,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "KSampler",
|
||||||
|
"pos": [
|
||||||
|
843,
|
||||||
|
567
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
262
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "MODEL",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "positive",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "negative",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "latent_image",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cfg",
|
||||||
|
"type": "FLOAT",
|
||||||
|
"link": 1,
|
||||||
|
"widget": {
|
||||||
|
"name": "cfg",
|
||||||
|
"config": [
|
||||||
|
"FLOAT",
|
||||||
|
{
|
||||||
|
"default": 8,
|
||||||
|
"min": 0,
|
||||||
|
"max": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"slot_index": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"links": null,
|
||||||
|
"shape": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "KSampler"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
0,
|
||||||
|
"randomize",
|
||||||
|
20,
|
||||||
|
8,
|
||||||
|
"euler",
|
||||||
|
"normal",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "PrimitiveNode",
|
||||||
|
"pos": [
|
||||||
|
506,
|
||||||
|
637
|
||||||
|
],
|
||||||
|
"size": {
|
||||||
|
"0": 210,
|
||||||
|
"1": 82
|
||||||
|
},
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "FLOAT",
|
||||||
|
"type": "FLOAT",
|
||||||
|
"links": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"slot_index": 0,
|
||||||
|
"widget": {
|
||||||
|
"name": "cfg",
|
||||||
|
"config": [
|
||||||
|
"FLOAT",
|
||||||
|
{
|
||||||
|
"default": 8,
|
||||||
|
"min": 0,
|
||||||
|
"max": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {},
|
||||||
|
"widgets_values": [
|
||||||
|
8,
|
||||||
|
"fixed"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
"FLOAT"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user