Convert primitive nodes

This commit is contained in:
space-nuko
2023-05-21 17:35:07 -05:00
parent a3c10d5be9
commit 9c4e5cea94
3 changed files with 325 additions and 36 deletions

View File

@@ -51,7 +51,7 @@
}
async function doRefreshCombos() {
await app.refreshComboInNodes(undefined, true)
await app.refreshComboInNodes(undefined, undefined, true)
}
function refreshView(event?: Event) {

View File

@@ -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 layoutStates, { defaultWorkflowAttributes, type ContainerLayout, type DragItemID, type SerializedDragEntry, type SerializedLayoutState, type WritableLayoutStateStore } from "./stores/layoutStates";
import { ComfyWorkflow, type WorkflowAttributes } from "./stores/workflowState";
import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas";
import ComfyApp from "./components/ComfyApp";
import { iterateNodeDefInputs } from "./ComfyNodeDef";
import { iterateNodeDefInputs, type ComfyNodeDefInputType, type ComfyNodeDefInputOptions } from "./ComfyNodeDef";
import type { ComfyNodeDefInput } from "./ComfyNodeDef";
import type IComfyInputSlot from "./IComfyInputSlot";
import ComfyWidgets from "./widgets"
@@ -28,10 +28,20 @@ type ComfyUIConvertedWidget = {
config: ComfyNodeDefInput
}
/*
* Input slot for widgets converted to inputs
*/
interface IComfyUINodeInputSlot extends INodeInputSlot {
widget?: ComfyUIConvertedWidget
}
/*
* Output slot for PrimitiveNode
*/
interface IComfyUINodeOutputSlot extends INodeOutputSlot {
widget?: ComfyUIConvertedWidget
}
/*
* 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] {
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
const { root, left, right } = layoutState.initDefaultLayout();
@@ -186,9 +358,17 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
// serialized widgets into ComfyWidgetNodes, add new inputs/outputs,
// 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];
if (def == null) {
console.error("Unknown backend node", node.type)
console.error("[convertVanillaWorkflow] Unknown backend node", node.type)
continue;
}
@@ -213,31 +393,14 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
// This input is a widget, it should be converted to an input
// connected to a ComfyWidgetNode.
let widgetNodeType = null;
let widgetInputType = null;
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
let pair = getWidgetTypesFromConfig(inputType);
if (pair == null) {
// Input type is backend-only, we can skip adding a UI node here
continue
}
let [widgetNodeType, widgetInputType] = pair
const newInput: IComfyInputSlot = {
name: inputName,
link: null,
@@ -264,16 +427,7 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
widgetNodeType,
value);
switch (widgetNodeType) {
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;
}
configureWidgetNodeProperties(serWidgetNode, inputOpts)
if (group == null)
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type })

View 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
}