new list utility nodes (#13)
* add List-related nodes for Checkpoints / LoRAs / Samplers/ Schedulers * wiki: reorg XY Plot 3rd tutorial and add example * bump version
This commit is contained in:
+143
-51
@@ -1,6 +1,10 @@
|
||||
import hashlib
|
||||
import math
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
import folder_paths # type: ignore
|
||||
from comfy.samplers import KSampler # type: ignore
|
||||
|
||||
from ..collection.register_nodes import register_node
|
||||
|
||||
@@ -165,56 +169,6 @@ class ListFromMultiline:
|
||||
return (output, len(output))
|
||||
|
||||
|
||||
# @register_node('Input Multiline To Batch', 'list')
|
||||
# class BatchFromMultiline:
|
||||
# def __init__(self):
|
||||
# pass
|
||||
|
||||
# @classmethod
|
||||
# def INPUT_TYPES(s):
|
||||
# return {
|
||||
# 'required': {
|
||||
# 'separator': (
|
||||
# 'STRING',
|
||||
# {'default': ',', 'multiline': False},
|
||||
# ),
|
||||
# 'index': (
|
||||
# 'INT',
|
||||
# {
|
||||
# 'min': 0,
|
||||
# 'default': 0,
|
||||
# },
|
||||
# ),
|
||||
# 'multiline': (
|
||||
# 'STRING',
|
||||
# {'default': '', 'multiline': True},
|
||||
# ),
|
||||
# }
|
||||
# }
|
||||
|
||||
# RETURN_TYPES = ('LIST', 'INT', 'INT')
|
||||
# RETURN_NAMES = ('list', 'batch index', 'batch total')
|
||||
|
||||
# FUNCTION = 'run'
|
||||
|
||||
# def run(
|
||||
# self,
|
||||
# separator: str,
|
||||
# index: int,
|
||||
# multiline: str,
|
||||
# ):
|
||||
# lines = multiline.split('\n')
|
||||
# output = []
|
||||
# for line in lines:
|
||||
# tokens = line.split(separator)
|
||||
# trimmed = []
|
||||
# for token in tokens:
|
||||
# trimmed.append(token.strip())
|
||||
# output.append(trimmed)
|
||||
|
||||
# return (output[index], index, len(output))
|
||||
|
||||
|
||||
@register_node('List: from File (backend)', 'list')
|
||||
class ListFromFile:
|
||||
def __init__(self):
|
||||
@@ -410,7 +364,7 @@ class ListRandomSeeds:
|
||||
'list of random seeds',
|
||||
TOOLTIP_OUTPUT_COUNT,
|
||||
)
|
||||
DESCRIPTION = 'Return a list of random seeds.'
|
||||
DESCRIPTION = 'Create a list of random seeds.'
|
||||
|
||||
# ensure the list is regenerated
|
||||
@classmethod
|
||||
@@ -422,3 +376,141 @@ class ListRandomSeeds:
|
||||
for i in range(0, count):
|
||||
values.append(math.floor(random.randrange(0, 0xFFFFFFFFFFFFFFFF)))
|
||||
return (values, len(values))
|
||||
|
||||
|
||||
@register_node('List: Checkpoints', 'list')
|
||||
class ListCheckpoints:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
'required': {
|
||||
'selection': (
|
||||
'SELECTION_LIST',
|
||||
{'all': folder_paths.get_filename_list('checkpoints')},
|
||||
),
|
||||
'with_extension': (
|
||||
'BOOLEAN',
|
||||
{'default': True, 'tooltip': 'keep file extension?'},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
FUNCTION = 'run'
|
||||
RETURN_TYPES = ('LIST', 'INT')
|
||||
RETURN_NAMES = ('list', 'count')
|
||||
OUTPUT_TOOLTIPS = (
|
||||
'list of checkpoint file names',
|
||||
TOOLTIP_OUTPUT_COUNT,
|
||||
)
|
||||
DESCRIPTION = 'Create a list from selected checkpoint file names.'
|
||||
|
||||
def run(self, with_extension, selection, **kwargs):
|
||||
output = []
|
||||
for file in selection['selected']:
|
||||
if not with_extension:
|
||||
output.append(str(Path(file).stem))
|
||||
else:
|
||||
output.append(file)
|
||||
return (output, len(output))
|
||||
|
||||
|
||||
@register_node('List: LoRAs', 'list')
|
||||
class ListLoras:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
'required': {
|
||||
'selection': (
|
||||
'SELECTION_LIST',
|
||||
{'all': folder_paths.get_filename_list('loras')},
|
||||
),
|
||||
'with_extension': (
|
||||
'BOOLEAN',
|
||||
{'default': True, 'tooltip': 'keep file extension?'},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
FUNCTION = 'run'
|
||||
RETURN_TYPES = ('LIST', 'INT')
|
||||
RETURN_NAMES = ('list', 'count')
|
||||
OUTPUT_TOOLTIPS = (
|
||||
'list of LoRA file names',
|
||||
TOOLTIP_OUTPUT_COUNT,
|
||||
)
|
||||
DESCRIPTION = 'Create a list from selected LoRA file names.'
|
||||
|
||||
def run(self, with_extension, selection, **kwargs):
|
||||
output = []
|
||||
for file in selection['selected']:
|
||||
if not with_extension:
|
||||
output.append(str(Path(file).stem))
|
||||
else:
|
||||
output.append(file)
|
||||
return (output, len(output))
|
||||
|
||||
|
||||
@register_node('List: Samplers', 'list')
|
||||
class ListSamplers:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
'required': {
|
||||
'selection': (
|
||||
'SELECTION_LIST',
|
||||
{'all': KSampler.SAMPLERS},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
FUNCTION = 'run'
|
||||
RETURN_TYPES = ('LIST', 'INT')
|
||||
RETURN_NAMES = ('list', 'count')
|
||||
OUTPUT_TOOLTIPS = (
|
||||
'list of sampler names',
|
||||
TOOLTIP_OUTPUT_COUNT,
|
||||
)
|
||||
DESCRIPTION = 'Create a list from selected sampler names.'
|
||||
|
||||
def run(self, selection, **kwargs):
|
||||
output = selection['selected']
|
||||
return (output, len(output))
|
||||
|
||||
|
||||
@register_node('List: Schedulers', 'list')
|
||||
class ListSchedulers:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
'required': {
|
||||
'selection': (
|
||||
'SELECTION_LIST',
|
||||
{'all': KSampler.SCHEDULERS},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
FUNCTION = 'run'
|
||||
RETURN_TYPES = ('LIST', 'INT')
|
||||
RETURN_NAMES = ('list', 'count')
|
||||
OUTPUT_TOOLTIPS = (
|
||||
'list of scheduler names',
|
||||
TOOLTIP_OUTPUT_COUNT,
|
||||
)
|
||||
DESCRIPTION = 'Create a list from selected scheduler names.'
|
||||
|
||||
def run(self, selection, **kwargs):
|
||||
output = selection['selected']
|
||||
return (output, len(output))
|
||||
|
||||
+30
-25
@@ -107,6 +107,16 @@ const customLabels = {
|
||||
nb_elements: 'nb of elements',
|
||||
},
|
||||
},
|
||||
ListCheckpoints: {
|
||||
widget: {
|
||||
with_extension: 'with file extension?',
|
||||
},
|
||||
},
|
||||
ListLoras: {
|
||||
widget: {
|
||||
with_extension: 'with file extension?',
|
||||
},
|
||||
},
|
||||
// CONFIG
|
||||
OutputConfigBackend: {
|
||||
widget: {
|
||||
@@ -121,31 +131,6 @@ const customLabels = {
|
||||
show_config: 'show config',
|
||||
},
|
||||
},
|
||||
OutputConfigAuto: {
|
||||
widget: {
|
||||
auto_connect: 'auto-connect?',
|
||||
multi_nodes: 'if multiple nodes found',
|
||||
error_display: 'display errors',
|
||||
show_config: 'show config',
|
||||
},
|
||||
},
|
||||
NodeConfigApplyLocal: {
|
||||
widget: {
|
||||
error_display: 'display errors',
|
||||
check_level: 'reject on',
|
||||
},
|
||||
},
|
||||
MultiConfigFetch: {
|
||||
widget: {
|
||||
node_by: 'identify nodes by',
|
||||
},
|
||||
},
|
||||
MultiConfigApplyLocal: {
|
||||
widget: {
|
||||
error_display: 'display errors',
|
||||
check_level: 'reject on',
|
||||
},
|
||||
},
|
||||
// QUEUE
|
||||
GenericQueue: {
|
||||
input: {
|
||||
@@ -270,6 +255,26 @@ const customLooks = {
|
||||
list: { shape: 'grid' },
|
||||
},
|
||||
},
|
||||
ListCheckpoints: {
|
||||
output: {
|
||||
list: { shape: 'grid' },
|
||||
},
|
||||
},
|
||||
ListLoras: {
|
||||
output: {
|
||||
list: { shape: 'grid' },
|
||||
},
|
||||
},
|
||||
ListSamplers: {
|
||||
output: {
|
||||
list: { shape: 'grid' },
|
||||
},
|
||||
},
|
||||
ListSchedulers: {
|
||||
output: {
|
||||
list: { shape: 'grid' },
|
||||
},
|
||||
},
|
||||
} as const as Record<string, CustomLook>
|
||||
|
||||
function setNodeLabels(nodeType: typeof LGraphNode, nodeData: ComfyNodeDef) {
|
||||
|
||||
+106
-1
@@ -1,6 +1,7 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { LGraphNode, IWidget } from '@comfyorg/litegraph'
|
||||
|
||||
import { findWidget, loadTextFile } from '~/shared/utils.js'
|
||||
import { makeComboWidget } from '~/widgets/factories.js'
|
||||
|
||||
export function ListFromMultiline(
|
||||
nodeType: LGraphNode,
|
||||
@@ -23,3 +24,107 @@ export function ListFromMultiline(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ListFromSelection(
|
||||
nodeType: LGraphNode,
|
||||
selection_type: string,
|
||||
) {
|
||||
const isCombo = (widget: IWidget) =>
|
||||
widget.name.startsWith(`${selection_type}_`.toLowerCase())
|
||||
|
||||
const findCombos = (node: LGraphNode) =>
|
||||
node.widgets.filter((w: IWidget) => isCombo(w))
|
||||
|
||||
const addCombo = (node: LGraphNode, selectionWidget: IWidget) => {
|
||||
const index = findCombos(node).length
|
||||
const width = node.size[0]
|
||||
const widget = makeComboWidget(
|
||||
node,
|
||||
`${selection_type}_${index}`.toLowerCase(),
|
||||
['none', ...selectionWidget.all],
|
||||
0,
|
||||
)
|
||||
// keep node width
|
||||
node.setSize([width, node.size[1]])
|
||||
|
||||
widget.label = `${selection_type} #${index + 1}`
|
||||
widget.options.serialize = false
|
||||
return widget
|
||||
}
|
||||
|
||||
const refreshSelection = (node: LGraphNode, selectionWidget: IWidget) => {
|
||||
const selected = []
|
||||
let widgetIndex = 0
|
||||
let comboIndex = 0
|
||||
while (widgetIndex < node.widgets.length) {
|
||||
const widget = node.widgets[widgetIndex]
|
||||
if (!isCombo(widget)) {
|
||||
widgetIndex += 1
|
||||
continue
|
||||
}
|
||||
if (widget.selected === 0) {
|
||||
node.widgets.splice(widgetIndex, 1)
|
||||
continue
|
||||
}
|
||||
// rename widget
|
||||
widget.name = `${selection_type}_${comboIndex}`.toLowerCase()
|
||||
widget.label = `${selection_type} #${comboIndex + 1}`
|
||||
// add to collected selected
|
||||
selected.push(widget.value)
|
||||
widgetIndex += 1
|
||||
comboIndex += 1
|
||||
}
|
||||
// add a new combo at the end
|
||||
const last = addCombo(node, selectionWidget)
|
||||
last.onSelected = () => {
|
||||
refreshSelection(node, selectionWidget)
|
||||
}
|
||||
|
||||
selectionWidget.value = { selected } // wrap into dict to avoid issues on validation
|
||||
}
|
||||
|
||||
// recreate widgets from selectionWidget
|
||||
const refreshFromSelection = (node: LGraphNode, selectionWidget: IWidget) => {
|
||||
// delete all combos widgets
|
||||
let widgetIndex = 0
|
||||
while (widgetIndex < node.widgets.length) {
|
||||
const widget = node.widgets[widgetIndex]
|
||||
if (isCombo(widget)) {
|
||||
node.widgets.splice(widgetIndex, 1)
|
||||
continue
|
||||
}
|
||||
widgetIndex += 1
|
||||
}
|
||||
// create combo widgets for selectionWidget
|
||||
const selected = selectionWidget.value.selected
|
||||
for (const value of selected) {
|
||||
const widget = addCombo(node, selectionWidget)
|
||||
widget.value = value
|
||||
// ensure widget.selected is valid
|
||||
widget.callback(widget.value)
|
||||
// only set onSelected now, to avoid having it called by prebious line
|
||||
widget.onSelected = () => {
|
||||
refreshSelection(node, selectionWidget)
|
||||
}
|
||||
}
|
||||
// add a new combo at the end
|
||||
const last = addCombo(node, selectionWidget)
|
||||
last.onSelected = () => {
|
||||
refreshSelection(node, selectionWidget)
|
||||
}
|
||||
}
|
||||
|
||||
const original_onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function (...args: unknown[]) {
|
||||
original_onNodeCreated?.apply(this, ...args)
|
||||
const selectionWidget = findWidget(this, 'selection')
|
||||
refreshFromSelection(this, selectionWidget)
|
||||
}
|
||||
|
||||
const original_onConfigure = nodeType.prototype.onConfigure
|
||||
nodeType.prototype.onConfigure = function (...args: unknown[]) {
|
||||
original_onConfigure?.apply(this, ...args)
|
||||
const selectionWidget = findWidget(this, 'selection')
|
||||
refreshFromSelection(this, selectionWidget)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-5
@@ -7,9 +7,9 @@ import { CATEGORY, type GlobalThisWithCategory } from '~/shared/common.js'
|
||||
import { customizeNode } from '~/custom.js'
|
||||
import { setupDynamicConnections } from '~/shared/dynamic_connections.js'
|
||||
import { QUEUE_STATUS } from '~/widgets/queue.js'
|
||||
import { HIDDEN, BTN, ERROR_DISPLAY } from '~/widgets/misc.js'
|
||||
import { HIDDEN, BTN, ERROR_DISPLAY, SELECTION_LIST } from '~/widgets/misc.js'
|
||||
import { OutputConfigBackend, OutputConfigLocal } from '~/nodes/config.js'
|
||||
import { ListFromMultiline } from '~/nodes/list.js'
|
||||
import { ListFromMultiline, ListFromSelection } from '~/nodes/list.js'
|
||||
import { InputMultiline } from '~/nodes/input.js'
|
||||
import { DebugJSONYAML, DebugWidgetVisibility } from '~/nodes/debug.js'
|
||||
|
||||
@@ -42,6 +42,21 @@ app.registerExtension({
|
||||
start_index: 1,
|
||||
})
|
||||
break
|
||||
case 'ListFromMultiline':
|
||||
ListFromMultiline(nodeType)
|
||||
break
|
||||
case 'ListCheckpoints':
|
||||
ListFromSelection(nodeType, 'checkpoint')
|
||||
break
|
||||
case 'ListLoras':
|
||||
ListFromSelection(nodeType, 'LoRA')
|
||||
break
|
||||
case 'ListSamplers':
|
||||
ListFromSelection(nodeType, 'sampler')
|
||||
break
|
||||
case 'ListSchedulers':
|
||||
ListFromSelection(nodeType, 'scheduler')
|
||||
break
|
||||
// INPUT
|
||||
case 'InputMultiline':
|
||||
InputMultiline(nodeType)
|
||||
@@ -54,9 +69,6 @@ app.registerExtension({
|
||||
start_index: 0,
|
||||
})
|
||||
break
|
||||
case 'ListFromMultiline':
|
||||
ListFromMultiline(nodeType)
|
||||
break
|
||||
// DEBUG
|
||||
case 'DebugJSONYAML':
|
||||
DebugJSONYAML(nodeType)
|
||||
@@ -84,6 +96,7 @@ app.registerExtension({
|
||||
HIDDEN,
|
||||
BTN,
|
||||
ERROR_DISPLAY,
|
||||
SELECTION_LIST,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
+20
-3
@@ -1,5 +1,4 @@
|
||||
import { LGraphNode, IWidget } from '@comfyorg/litegraph'
|
||||
import type { ComfyApp } from '@comfyorg/comfyui-frontend-types'
|
||||
|
||||
import { InputSpec } from '~/.d.ts/comfyui-frontend-types_alt.js'
|
||||
import {
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
makeComboWidget,
|
||||
} from '~/widgets/factories.js'
|
||||
import { makeWidgetAsync } from '~/shared/utils.js'
|
||||
import { log } from '~/shared/common.js'
|
||||
|
||||
/**
|
||||
* Create an hidden widget. To be used in `getCustomWidgets(app)`
|
||||
@@ -21,8 +21,8 @@ export function HIDDEN(
|
||||
node: LGraphNode,
|
||||
inputName: string,
|
||||
inputData: InputSpec,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_app: ComfyApp | undefined,
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// _app: ComfyApp | undefined,
|
||||
) {
|
||||
// the value in inputData is only used when creating a new workdlow
|
||||
// otherwise, the cached value will be used by Comfy
|
||||
@@ -132,3 +132,20 @@ export function ERROR_DISPLAY(
|
||||
)
|
||||
return widget as ErrorDisplayWidget
|
||||
}
|
||||
|
||||
export function SELECTION_LIST(
|
||||
node: LGraphNode,
|
||||
inputName: string,
|
||||
inputData: InputSpec,
|
||||
) {
|
||||
inputData[1].value = { selected: [] } // wrap into dict to avoid issues on validation
|
||||
if (!Array.isArray(inputData[1].all) || !inputData[1].all) {
|
||||
log.error("SELECTION_LIST: 'all is invalid")
|
||||
// overwrite `all`
|
||||
inputData[1].all = []
|
||||
}
|
||||
|
||||
const widget = HIDDEN(node, inputName, inputData)
|
||||
widget.all = inputData[1].all
|
||||
return widget
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user