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:
Bug, Ltd.
2025-01-29 11:18:14 +01:00
committed by GitHub
parent 1c6034bbcb
commit 9dcf6660de
45 changed files with 1613 additions and 400 deletions
+5
View File
@@ -4,6 +4,11 @@
## v0.0
## v0.0.5 (2025-01-29)
- new List utility nodes: Random Seeds / Checkpoints / LoRAs / Samplers/ Schedulers
- reorganize / improve wiki
## v0.0.4 (2025-01-28): better wiki
- replace repo banner and icon
+3 -1
View File
@@ -8,7 +8,9 @@
# News
- 2025-01-28 - v0.0.5: added node `List: Random Seeds`
- 2025-01-29 - v0.0.5:
- new List utility nodes: Random Seeds / Checkpoints / LoRAs / Samplers/ Schedulers
- reorganize / improve wiki
# Node overview
+1 -1
View File
@@ -18,7 +18,7 @@ make_node_list(Path(__file__).parent / 'node_list.json')
load_api()
print(
' 🧬 {}: {} hybrid nodes successfully cloned'.format(
' ⚙️ {}: {} carefully crafted nodes available'.format(
CATEGORY, len(NODE_CLASS_MAPPINGS)
)
)
+30 -25
View File
@@ -82,6 +82,16 @@ const customLabels = {
nb_elements: 'nb of elements',
},
},
ListCheckpoints: {
widget: {
with_extension: 'with file extension?',
},
},
ListLoras: {
widget: {
with_extension: 'with file extension?',
},
},
OutputConfigBackend: {
widget: {
file_path: 'JSON or YAML file (backend)',
@@ -95,31 +105,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',
},
},
GenericQueue: {
input: {
input_list: 'input list',
@@ -238,6 +223,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' },
},
},
};
function setNodeLabels(nodeType, nodeData) {
const nodeName = nodeData.name;
+76
View File
@@ -1,4 +1,5 @@
import { findWidget, loadTextFile } from '../shared/utils.js';
import { makeComboWidget } from '../widgets/factories.js';
export function ListFromMultiline(nodeType) {
const original_onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function (...args) {
@@ -13,3 +14,78 @@ export function ListFromMultiline(nodeType) {
};
};
}
export function ListFromSelection(nodeType, selection_type) {
const isCombo = (widget) => widget.name.startsWith(`${selection_type}_`.toLowerCase());
const findCombos = (node) => node.widgets.filter((w) => isCombo(w));
const addCombo = (node, selectionWidget) => {
const index = findCombos(node).length;
const width = node.size[0];
const widget = makeComboWidget(node, `${selection_type}_${index}`.toLowerCase(), ['none', ...selectionWidget.all], 0);
node.setSize([width, node.size[1]]);
widget.label = `${selection_type} #${index + 1}`;
widget.options.serialize = false;
return widget;
};
const refreshSelection = (node, selectionWidget) => {
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;
}
widget.name = `${selection_type}_${comboIndex}`.toLowerCase();
widget.label = `${selection_type} #${comboIndex + 1}`;
selected.push(widget.value);
widgetIndex += 1;
comboIndex += 1;
}
const last = addCombo(node, selectionWidget);
last.onSelected = () => {
refreshSelection(node, selectionWidget);
};
selectionWidget.value = { selected };
};
const refreshFromSelection = (node, selectionWidget) => {
let widgetIndex = 0;
while (widgetIndex < node.widgets.length) {
const widget = node.widgets[widgetIndex];
if (isCombo(widget)) {
node.widgets.splice(widgetIndex, 1);
continue;
}
widgetIndex += 1;
}
const selected = selectionWidget.value.selected;
for (const value of selected) {
const widget = addCombo(node, selectionWidget);
widget.value = value;
widget.callback(widget.value);
widget.onSelected = () => {
refreshSelection(node, selectionWidget);
};
}
const last = addCombo(node, selectionWidget);
last.onSelected = () => {
refreshSelection(node, selectionWidget);
};
};
const original_onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function (...args) {
original_onNodeCreated?.apply(this, ...args);
const selectionWidget = findWidget(this, 'selection');
refreshFromSelection(this, selectionWidget);
};
const original_onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (...args) {
original_onConfigure?.apply(this, ...args);
const selectionWidget = findWidget(this, 'selection');
refreshFromSelection(this, selectionWidget);
};
}
+18 -5
View File
@@ -3,9 +3,9 @@ import { CATEGORY } 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';
globalThis[CATEGORY] = { log: 'debug' };
@@ -28,6 +28,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;
case 'InputMultiline':
InputMultiline(nodeType);
break;
@@ -38,9 +53,6 @@ app.registerExtension({
start_index: 0,
});
break;
case 'ListFromMultiline':
ListFromMultiline(nodeType);
break;
case 'DebugJSONYAML':
DebugJSONYAML(nodeType);
break;
@@ -63,6 +75,7 @@ app.registerExtension({
HIDDEN,
BTN,
ERROR_DISPLAY,
SELECTION_LIST,
};
},
});
+12 -1
View File
@@ -1,6 +1,7 @@
import { makeButtonWidget, makeComboWidget, } from '../widgets/factories.js';
import { makeWidgetAsync } from '../shared/utils.js';
export function HIDDEN(node, inputName, inputData, _app) {
import { log } from '../shared/common.js';
export function HIDDEN(node, inputName, inputData) {
const initialValue = inputData[1].value;
const widget = {
type: 'HIDDEN',
@@ -69,3 +70,13 @@ export function ERROR_DISPLAY(node, name = 'error_display') {
const widget = makeComboWidget(node, name, ERROR_DISPLAY_CHOICES, 'dialog', () => { }, 'how to display detected errors');
return widget;
}
export function SELECTION_LIST(node, inputName, inputData) {
inputData[1].value = { selected: [] };
if (!Array.isArray(inputData[1].all) || !inputData[1].all) {
log.error("SELECTION_LIST: 'all is invalid");
inputData[1].all = [];
}
const widget = HIDDEN(node, inputName, inputData);
widget.all = inputData[1].all;
return widget;
}
+5 -1
View File
@@ -10,7 +10,11 @@
"List: from Elements (lab)": "Create a list from any number of individual elements",
"List: Merge (lab)": "Merge any number of lists into one",
"List: Limit (lab)": "Limit list to a given size",
"List: Random Seeds (lab)": "Return a list of random seeds",
"List: Random Seeds (lab)": "Create a list of random seeds",
"List: Checkpoints (lab)": "Create a list from selected checkpoint file names",
"List: LoRAs (lab)": "Create a list from selected LoRA file names",
"List: Samplers (lab)": "Create a list from selected sampler names",
"List: Schedulers (lab)": "Create a list from selected scheduler names",
"Output Config: Load (lab)": "Load a local JSON or YAML config file, and dynamically generate outputs",
"Output Config: Retrieve (backend) (lab)": "Retrieve a JSON or YAML config file from backend, and dynamically generate outputs",
"Generic Queue (lab)": "Loop through all values of input list, and send them to output",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "confylab-pack",
"private": true,
"version": "0.0.4",
"version": "0.0.5",
"type": "module",
"scripts": {
"watch": "tsc-watch -p tsconfig.json --onSuccess 'tsc-alias -p tsconfig.json -r build/replacers.js/mock.js'",
+2 -2
View File
@@ -1,7 +1,7 @@
[project]
name = "comfylab-pack"
description = "A collection of carefully experimented nodes, to improve your Comfy UX."
version = "0.0.4"
version = "0.0.5"
authors = [{ name = "bugltd" }]
license = {file = "LICENSE"}
readme = "README.md"
@@ -18,4 +18,4 @@ Changelog = "https://github.com/bugltd/ComfyLab-Pack/blob/main/CHANGELOG.md"
[tool.comfy]
PublisherId = "bugltd"
DisplayName = "ComfyLab Pack"
Icon = "https://github.com/bugltd/ComfyLab-Pack/blob/main/static/repo_icon.png"
Icon = "https://github.com/bugltd/ComfyLab-Pack/blob/main/static/repo_icon_400.png"
+143 -51
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

+2 -1
View File
@@ -41,7 +41,7 @@ That being said, have a good read!
- [XY Plot](./tutorials/XY%20Plot/)
- [1 - the basics](./tutorials/XY%20Plot/1%20-%20the%20basics/)
- [2 - pimp my grid](./tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/)
- [3 - advanced techniques](./tutorials/XY%20Plot/3%20-%20advanced%20techniques/)
- [3 - complex variations](./tutorials/XY%20Plot/3%20-%20complex%20variations/)
- [Format](./tutorials/Format/)
---
@@ -51,3 +51,4 @@ That being said, have a good read!
- [XY Plot](./examples/XY%20Plot/)
- [LoRA testing](./examples/XY%20Plot/LoRA%20testing/)
- [Epochs vs Seeds](./examples/XY%20Plot/LoRA%20testing/Epochs%20vs%20Seeds/)
- [Various resolutions (and aspect ratios)](./examples/XY%20Plot/Various%20resolutions/)
+1
View File
@@ -7,6 +7,7 @@ Here you will find some examples demonstrating how to use the Comfy Lab XY Plot:
- [XY Plot](./XY%20Plot/)
- [LoRA testing](./XY%20Plot/LoRA%20testing/)
- [Epochs vs Seeds](./XY%20Plot/LoRA%20testing/Epochs%20vs%20Seeds/)
- [Various resolutions (and aspect ratios)](./XY%20Plot/Various%20resolutions/)
> [!TIP]
> For more detailed information / explanations, please check the XY Plot [tutorials](../tutorials/XY%20Plot/) and [node references](../node%20reference/xy%20plot/).
@@ -1,14 +1,7 @@
# Examples / XY Plot / LoRA testing / Epochs vs Seeds
> [!NOTE]
> As always, we use the following convention for node color:
>
> - Green: input node you surely want to customize
> - Blue: the specific nodes we use for processing / customization
>
> Of course, adjust to your env / needs: checkpoint, prompt, CFG, ...
These examples are intended for quick re-use, feel free to check [the tutorials](../../../../tutorials/) for more detailed explanations.
> These examples are intended for quick re-use, feel free to check [the tutorials](../../../../tutorials/) for more detailed explanations.
Sections:
@@ -18,7 +11,7 @@ Sections:
- [Epochs vs Random seeds](#epochs-vs-random-seeds)
> [!TIP]
> Here we do not customize the grid look, but it can easily be done by plugging the `OutputConfig: Grid` node to `XY Plot: Render`.
> Here we do not customize the grid look, but it can be done easily by plugging the `Plot Config: Grid` node to `XY Plot: Render`.
## Overview
@@ -51,7 +44,7 @@ We build the LoRA model file name thanks to the `Format String` node. Here speci
> [!NOTE]
> More details in the [node reference](../../../../node%20reference/format.md), you can also visit [this guide](https://pyformat.info/) for different techniques applicable to Python string `format()` method
Finally, we pipe the resulting string to `Load LoRA`, after converting it to type Any (`*`). More explanations in the [XY Plot tutorial: 3 - advanced techniques](../../../../tutorials/XY%20Plot/3%20-%20advanced%20techniques/).
Finally, we pipe the resulting string to `Load LoRA`, after converting it to type Any (`*`). More explanations in the [XY Plot tutorial: 3 - complex variations](../../../../tutorials/XY%20Plot/3%20-%20complex%20variations/).
#### Seed list
+1
View File
@@ -4,6 +4,7 @@ Here you will find some examples demonstrating how to use the Comfy Lab XY Plot:
- [LoRA testing](./LoRA%20testing/)
- [Epochs vs Seeds](./LoRA%20testing/Epochs%20vs%20Seeds/)
- [Various resolutions (and aspect ratios)](./Various%20resolutions/)
> [!TIP]
> For more detailed information / explanations, please check the XY Plot [tutorials](../../tutorials/XY%20Plot/) and [node references](../../node%20reference/xy%20plot/).
@@ -0,0 +1,34 @@
# Examples / XY Plot / Various resolutions (and aspect ratios)
> [!NOTE]
> These examples are intended for quick re-use, feel free to check [the tutorials](../../../tutorials/) for more detailed explanations.
Sections:
- [Overview](#overview)
- [Workflow](#workflow)
> [!TIP]
> Here we do not customize the grid look, but it can be done easily by plugging the `Plot Config: Grid` and/or `Plot Config: Header/Footer` nodes to `XY Plot: Render`.\
> More details in the [node reference](../../../node%20reference/xy%20plot/2%20-%20config%20nodes.md) and the [XY Plot tutorial 2 - pimp my grid](../../../tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/).
## Overview
While probably not the most useful as such, this example demonstrates the ability of the ComfyLab XY Plot to handle various resolutions or aspect ratios within the same grid.
Here we generate images of various resolutions, and apply rotations.
## Workflow
**Load either `various resolutions.json` or `various resolutions.png` into ComfyUI.**\
Adjust the checkpoint to one available in your ComfyUI instance.\
Execute the workflow.
![result](./images/result.jpg)
What we did:
- included the ComfyLab `Resolution to Dimensions` node, to split the first input (`1024 x 1024`, ...) into width and height, piped into `Empty Latent Image`
- we could have done this differently, I chose this method as it is straightforward in this case
- we also used a 2nd input list to set rotations, piped into the `rotation` input of `Rotate Latent`
- remember, we can do that because the `Queue` outputs are of type Any

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Before

Width:  |  Height:  |  Size: 382 KiB

After

Width:  |  Height:  |  Size: 382 KiB

+1 -1
View File
@@ -32,7 +32,7 @@ And last but not least: at stated above, it can take **other types than strings:
### Going further
For more details and advanced techniques, you can check:
For more details and complex variations, you can check:
- [the tutorials of the `Format` nodes](../tutorials/Format/)
- [a guide with different techniques (padding, ...)](https://pyformat.info/)
+14 -10
View File
@@ -4,13 +4,13 @@ All extension nodes, organized by category.
## XY Plot
| Node name | Description | Reference | Tutorial |
| :----------------------------: | :----------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| **XY Plot: Queue** | queue dim1 / dim2 input lists into values | [core concepts](./node%20reference/xy%20plot/0%20-%20core%20concepts.md)<br/>[reference](./node%20reference/xy%20plot/1%20-%20queue%20and%20render.md) | [tutorial #1](./tutorials/XY%20Plot/1%20-%20the%20basics/)<br/>[tutorial #2](./tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/)<br/>[tutorial #3](./tutorials/XY%20Plot/3%20-%20advanced%20techniques/) |
| **XY Plot: Render** | render the XY Plot grid | [core concepts](./node%20reference/xy%20plot/0%20-%20core%20concepts.md)<br/>[reference](./node%20reference/xy%20plot/1%20-%20queue%20and%20render.md) | [tutorial #1](./tutorials/XY%20Plot/1%20-%20the%20basics/)<br/>[tutorial #2](./tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/)<br/>[tutorial #3](./tutorials/XY%20Plot/3%20-%20advanced%20techniques/) |
| **Plot Config: Grid** | configure the grid | [reference](./node%20reference/xy%20plot/2%20-%20config%20nodes.md) | [tutorial #2](./tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/)<br/>[tutorial #3](./tutorials/XY%20Plot/3%20-%20advanced%20techniques/) |
| **Plot Config: Header/Footer** | configure either page header or footer | [reference](./node%20reference/xy%20plot/2%20-%20config%20nodes.md) | [tutorial #2](./tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/)<br/>[tutorial #3](./tutorials/XY%20Plot/3%20-%20advanced%20techniques/) |
| **XY Plot: Split Data** | split XY Plot data into page index / count | [reference](./node%20reference/xy%20plot/2%20-%20config%20nodes.md) | [tutorial #2](./tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/)<br/>[tutorial #3](./tutorials/XY%20Plot/3%20-%20advanced%20techniques/) |
| Node name | Description | Reference | Tutorial |
| :----------------------------: | :----------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| **XY Plot: Queue** | queue dim1 / dim2 input lists into values | [core concepts](./node%20reference/xy%20plot/0%20-%20core%20concepts.md)<br/>[reference](./node%20reference/xy%20plot/1%20-%20queue%20and%20render.md) | [tutorial #1](./tutorials/XY%20Plot/1%20-%20the%20basics/)<br/>[tutorial #2](./tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/)<br/>[tutorial #3](./tutorials/XY%20Plot/3%20-%20complex%20variations/) |
| **XY Plot: Render** | render the XY Plot grid | [core concepts](./node%20reference/xy%20plot/0%20-%20core%20concepts.md)<br/>[reference](./node%20reference/xy%20plot/1%20-%20queue%20and%20render.md) | [tutorial #1](./tutorials/XY%20Plot/1%20-%20the%20basics/)<br/>[tutorial #2](./tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/)<br/>[tutorial #3](./tutorials/XY%20Plot/3%20-%20complex%20variations/) |
| **Plot Config: Grid** | configure the grid | [reference](./node%20reference/xy%20plot/2%20-%20config%20nodes.md) | [tutorial #2](./tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/)<br/>[tutorial #3](./tutorials/XY%20Plot/3%20-%20complex%20variations/) |
| **Plot Config: Header/Footer** | configure either page header or footer | [reference](./node%20reference/xy%20plot/2%20-%20config%20nodes.md) | [tutorial #2](./tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/)<br/>[tutorial #3](./tutorials/XY%20Plot/3%20-%20complex%20variations/) |
| **XY Plot: Split Data** | split XY Plot data into page index / count | [reference](./node%20reference/xy%20plot/2%20-%20config%20nodes.md) | [tutorial #2](./tutorials/XY%20Plot/2%20-%20pimp%20my%20grid/)<br/>[tutorial #3](./tutorials/XY%20Plot/3%20-%20complex%20variations/) |
## Output Config
@@ -35,13 +35,17 @@ All extension nodes, organized by category.
| Node name | Description | Reference | Tutorial |
| :---------------------------: | :-----------------------------------------------------------------------------------------------------------------: | :-------: | :------: |
| **List: from String** | split a single string into elements delimited by a separator<br/>optionally convert values to integers, flotas, ... | | |
| **List: from Multiline** | split a multiline string, each line being a value<br/>optionally convert values to integers, flotas, ... | | |
| **List: from String** | split a single string into elements delimited by a separator<br/>optionally convert values to integers, floats, ... | | |
| **List: from Multiline** | split a multiline string, each line being a value<br/>optionally convert values to integers, floats, ... | | |
| **List: from File (backend)** | read a multiline text file from backend,<br/>and send its content as a list | | |
| **List: from Elements** | create a list from any number of individual elements | | |
| **List: Random Seeds** | create a list of random seeds | | |
| **List: Merge** | merge any number of lists into one | | |
| **List: Limit** | limit list to a given size | | |
| **List: Random Seeds** | create a list of random seeds | | |
| **List: Checkpoints** | create a list of checkpoint file names (multi-selection) | | |
| **List: LoRAs** | create a list of LoRA file names (multi-selection) | | |
| **List: Samplers** | create a list of sampler names (multi-selection) | | |
| **List: Schedulers** | create a list of scheduler names (multi-selection) | | |
## Queue
+1 -1
View File
@@ -9,6 +9,6 @@
- [1 - the basics](./XY%20Plot/1%20-%20the%20basics/)
- [2 - pimp my grid](./XY%20Plot/2%20-%20pimp%20my%20grid/)
- [3 - advanced techniques](./XY%20Plot/3%20-%20advanced%20techniques/)
- [3 - complex variations](./XY%20Plot/3%20-%20complex%20variations/)
**[Format](./Format/)**
@@ -138,11 +138,12 @@ That being said, I hope you have found these tutorials interesting and easy to f
> [!NOTE]
> If you have any suggestion to improve them (content, features, better English, shorter sentences, ...), feel free to open [an issue](https://github.com/bugltd/ComfyLab-Pack/issues), [a discussion](https://github.com/bugltd/ComfyLab-Pack/discussions), or even a Pull Request if you can.
Thank you for reading, and enjoy your beautiful grids my friends!
Thank you for reading, and enjoy your beautiful grids!
## Going further (advanced)
These 2 tutorials are intended for a large audience, and to cover the most standard use cases.\
These 2 tutorials are intended for a large audience, and to cover the most standard use cases.
But please note there are **many advanced techniques you can apply with ComfyLab XY Plot nodes**, for example:
- vary checkpoints, LoRA (any model in fact), ... virtually anything
@@ -153,11 +154,11 @@ But please note there are **many advanced techniques you can apply with ComfyLab
- with the [ComfyLab `Output Config` node](../../Output%20Config/)
- ...
If you want to dive deeper, I propose we move to the **next (and last) tutorial of the XY Plot series: 3 - advanced techniques**.\
But before doing that, please note:
If you want to go deeper, we can now move to the **next tutorial of the XY Plot series: 3 - complex variations**.\
It will show how you can vary many things like checkpoints / LoRAs / ..., and virtually any model or value in a combo widget, by using 3 methods:
- it addresses some edge cases that may not be needed for most
- it is not as detailed as those 2 first tutorials, we'll go faster
- if you haven't done it yet, I strongly advise you read the [XY Plot core concepts](../../../node%20reference/xy%20plot/0%20-%20core%20concepts.md): it's not long, and you may learn some important design choices there.
- simple, with dedicated nodes for checkpoints / LoRAs / samplers / schedulers
- versatile, that should adapt to any kind of model (or combo widget)
- with the [`Output Config` node](../../Output%20Config/), to standardize your workflow and keep parameters separated
Interested? Let's go to the next tutorial: **[3 - advanced techniques](../3%20-%20advanced%20techniques/)**
Interested? Let's go to the next tutorial: **[3 - complex variations](../3%20-%20complex%20variations/)**
@@ -1,139 +0,0 @@
# Tutorials / XY Plot: 3 - advanced techniques
Welcome to this third (and last) tutorial of the XY Plot series!
Here we are going to see a few advanced techniques you may find useful.\
Of course, I cannot anticipate all the use cases, but by following this tutorial, I hope you will see the versatility of the ComfyLab XY Plot nodes, and understand how to adapt for your scenario.\
Here, no frills, we'll go straight to the point, to focus of the functionalities rather than fancy grid customization.
> [!IMPORTANT]
> If you haven't yet, it is recommended to follow (at least read) the first 2 tutorials ([1](../1%20-%20the%20basics/) & [2](../2%20-%20pimp%20my%20grid/)), and read the [XY Plot core concepts](../../../node%20reference/xy%20plot/0%20-%20core%20concepts.md).\
> You will probably need them in this advanced tutorial.
Tutorial sections:
- [Part 1 - Various checkpoints (or LoRAs, ...)](#part-1---various-checkpoints-or-loras-)
- [Part 2 - Various resolutions and aspect ratios](#part-2---various-resolutions-and-aspect-ratios)
- [Part 3 - Using the Output Config node](#part-3---using-the-output-config-node)
- [TL;DR / Conclusion](#tldr--conclusion)
> [!TIP]
> As for all nodes in this extension, you can get useful contextual information, by just **moving the mouse pointer over a node or its inputs / widgets / outputs**. This should help you understand some details, without reading the more detailed wiki pages (yet).
And as always, if you do not want to follow all the steps, you can jump directly to [the conclusion](#tldr--conclusion).
---
## Part 1 - Various checkpoints (or LoRAs, ...)
**Load either `workflow - part 1.json` or `workflow - part 1.png` into ComfyUI.**\
**Important:**
- in the first input list, adjust to checkpoints that are available in your ComfyUI instance
- do no add the `.safetensors` extension, it will be added automatically
Execute the workflow.
![result - part 1](./details/result%20-%20part%201.jpg)
Here we build a grid with 2 different checkpoints, vs a list of predefined seeds.
What should be noticed:
<ins>Checkpoints:</ins>
- we have chosen not to include the `.safetensors` extension in the input
- it would have been displayed in the row headers, making them too long
- instead, we have used a `Format: String` node, to append `.safetensors` to the `dim1 value` output of the `Queue` node
- but by simply doing so, we cannot pipe the text output to the `Load Checkpoint` node, why?
- the `chkpt_name` widget does not accept any string, but a string that exists in the list
- so to bypass this limitation we simply convert the text output to Any, thanks to the `Convert to Any` node
- ![Any output](./details/detail%20-%20part%201%20-%20any.jpg)
> [!NOTE]
> You can read more about the Any output in the [XY Plot core concepts](../../../node%20reference/xy%20plot/0%20-%20core%20concepts.md), and the [2nd tutorial of the `Output Config` node](../../Output%20Config/2%20-%20more%20options)
<ins>Seeds:</ins>
- theoritically, we should have converted the list values to integers, thanks to the `convert` combo widget
- but it also works (for seeds only AFAIK)
### Lessons learned
- combo widgets (as `chkpt_name` in `Load Checkpoint`) do not accept simple strings
- a workaround is to use an output with type Any (`"*"`)
- should we have included the `.safetensors` in the first input, we could have dropped the `Format: String` and `Convert to Any` node, because the type of `Queue` outputs is Any
- but this wouldn't be great in terms of display in the grid
- the checkpoint list is connected to `dim1`, not `dim2`, for a performance reason
- if you want: just switch dim1 / dim2 and check the processing time _(more in the [core concepts page](../../../node%20reference/xy%20plot/0%20-%20core%20concepts.md))_
> [!TIP]
> You can basically use this approach for any combo widget, in particular when loading models (LoRA, ...).\
> But be **very cautious** about the values you set in input: by using the Any type, we bypass the standard ComfyUI type checks, so the workflow may fail if the values are incorrect.
### Variant: Random seeds
Instead of fixed seeds, if you prefer to have a new random set of seeds each time you generate the grid, just replace the `List: ftom Multiline` node with `List: Random Seeds`.
## Part 2 - Various resolutions and aspect ratios
**Load either `workflow - part 2.json` or `workflow - part 2.png` into ComfyUI.**\
Adjust the checkpoint to one available in your ComfyUI instance.\
Execute the workflow.
![result - part 2](./details/result%20-%20part%202.jpg)
What we did:
- include the ComfyLab `Resolution to Dimensions` node, to split the first input (`1024 x 1024`, ...) into width and height, piped into `Empty Latent Image`
- we could have done this differently, I chose this method as it is straightforward in this case
- we also used a 2nd input list to set rotations, piped into the `rotation` input of `Rotate Latent`
- remember, we can do that because the `Queue` outputs are of type Any
While probably not the most useful as such, this example demonstrates **the ability of the ComfyLab XY Plot to handle various resolutions or aspect ratios within the same grid**.
## Part 3 - Using the Output Config node
Here the workflow is similar to [Part1](#part-1---various-checkpoints-or-loras-), except that we use the `Output Config` node, to simplify and standardize our workflow.
**Load either `workflow - part 3.json` or `workflow - part 3.png` into ComfyUI.**\
**Copy one of the `output config - part 3.*` file, adjust the checkpoints to your env, and load it into the `Output Config` node.**
- you should get outputs like this (more than the 4 displayed here):
- ![outputs](./details/detail%20-%20part%203%20-%20output%20config.jpg)
Execute the workflow.
![result - part 3](./details/result%20-%20part%201.jpg)
As expected, we have strictly the same results as in [Part 1](#part-1---various-checkpoints-or-loras-).
What should be noticed:
- the output config file is available in 3 different formats: JSON, JSON5 and YAML
- we replaced the input lists by a single `Output Config` node
- we also added more outputs to the config file: CFG, steps, sampler, ... quite a lot indeed
- of course, we could have used 2 different `Output Config` nodes, one for the XY Plot itself, and another one for the other values: it's up to you to define how you want to standardize your configs
- note how the `Output Config` node auto-detects the type from output values:
- for example, `cfg` is written without quotes, so it's detected as a string
- but for more combo widgets (sampler, scheduler), we have to force type to be Any (`*`)
> [!TIP]
> We won't go into details here about the configuration file itself, but you can find more information in the corresponding [node reference](../../../node%20reference/output%20config.md) and the [dedicated tutorials](../../../tutorials/Output%20Config/).
## TL;DR / Conclusion
In this tutorial, we have:
- seen how to handle various checkpoints (or virtually any sort of model)
- and optimized the grid display at the same time
- demonstrated the ability of the XY Plot to include images or various dimensions / apect ratios within the same grid
- shown how we can use the `Output Config` node, to keep a collection of configs, while standardizing our workflow
This tutorial is still in construction, so I will certainly add most techniques in the future, be sure to check.
Also, you will find [various examples](../../../examples/) in the wiki (with less explanations).
In the meantime, if you have any suggestion, feel free to open [a discussion](https://github.com/bugltd/ComfyLab-Pack/discussions).
Thank you for reading, enjoy!
Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

@@ -0,0 +1,145 @@
# Tutorials / XY Plot: 3 - Complex variations
Welcome to this third tutorial of the XY Plot series!
Here we are going to see a few advanced techniques, to make virtually anything vary, for example models or any value in a combo widget.\
To achieve that, we will use 3 different methods:
- simple: with dedicated nodes, for checkpoints / LoRAs / samplers / schedulers
- versatile: adaptable to any kind of model (or combo widget)
- with the [`Output Config`](../../Output%20Config/) node: to standardize your workflow, and keep parameters separated
Each of these approaches will generate the same results: depending on your specifity, pick the one that matches your specific needs.
> [!TIP]
> In the following, we vary checkpoints vs a fixed list of seeds.
> If you prefer to have a new set of **random seeds** each time you generate the grid, just replace the corresponding `List: ftom Multiline` node with `List: Random Seeds`.
Tutorial sections:
- [Expected results](#expected-results)
- [Part 1 - Simple: with a dedicated node (for checkpoints, LoRAs, samplers, schedulers)](#part-1---simple-with-a-dedicated-node-for-checkpoints-loras-samplers-schedulers)
- [Part 2 - Versatile: any model (or combo widget)](#part-2---versatile-any-model-or-combo-widget)
- [Part 3 - With the `Output Config` node](#part-3---with-the-output-config-node)
- [TL;DR / Conclusion](#tldr--conclusion)
And as always, if you do not want to follow all the steps, you can jump directly to [the conclusion](#tldr--conclusion).
---
## Expected results
If you use the exact same parameters:
- checkpoints: `sd_xl_base_1.0`, `Juggernaut-XI-byRunDiffusion`
- seeds: `156680208700286`, `715168739510668`, `568208062071311`
You should get the same result:\
![result grid](./details/result%20.jpg)
## Part 1 - Simple: with a dedicated node (for checkpoints, LoRAs, samplers, schedulers)
> [!TIP]
> Here we use the `List: Checkpoints` node, that allows to select any number of checkpoints (from your ComfyUI `models/checkpoints` folder), and get a list of file names.\
> Similar nodes exist for: LoRAs, samplers, schedulers.
**Load either `workflow - part 1.json` or `workflow - part 1.png` into ComfyUI.**\
**Important:**
- in the `List: Checkpoints` node, select a few checkpoints from your env
- in the example, I use SDXL 1.0 and Juggernaut XL v11
- ensure the widget `with file extension?` is unchecked, as we will add the extension later
Execute the workflow.
### What should be noticed
> [!NOTE]
> we have chosen to not include the `.safetensors` extension in the input: it would have been displayed in the row headers, making them too long. Of course, you can keep it, and skip the following.
To append the `.safetensors` extension:
- we use a `Format: String` node, to add the extension to the `dim1 value` output of the `Queue` node
- but by simply doing so, we cannot pipe the text output to the `Load Checkpoint` node, why?
- the `chkpt_name` widget does not accept any string, but a string that exists in the list
- so to bypass this limitation we simply convert the text output to Any, thanks to the `Convert to Any` node
- ![Any output](./details/detail%20-%20part%201-%20any.jpg)
> [!NOTE]
> You can read more about the Any output in the [XY Plot core concepts](../../../node%20reference/xy%20plot/0%20-%20core%20concepts.md), and the [2nd tutorial of the `Output Config` node](../../Output%20Config/2%20-%20more%20options/)
### Lessons learned
- combo widgets (as `chkpt_name` in `Load Checkpoint`) do not accept simple strings
- a workaround is to use an output with type Any (`"*"`)
- should we have included the `.safetensors` in the first input, we could have dropped the `Format: String` and `Convert to Any` nodes, because the `XY Plot: Queue` outputs are themselves of type Any
- but this wouldn't be great in terms of display in the grid
- the checkpoint list is connected to `dim1`, not `dim2`, for a performance reason
- for the test: just switch dim1 / dim2 and check the processing time _(more in the [core concepts page](../../../node%20reference/xy%20plot/0%20-%20core%20concepts.md))_
## Part 2 - Versatile: any model (or combo widget)
While it is usually simpler to use the `List` nodes dedicated to checkpoints / LoRAs / samplers / schedulers, there are some cases where we need to use a more versatile approach:
- we want to vary another type of model / value from a combo widget
- we have stored the list as a separate text file (e.g. test case), and prefer to re-use it, rather than manually selecting the values
This short part will cover exactly that, to show how you can adapt the XY Plot to many specific cases.
**Load either `workflow - part 2.json` or `workflow - part 2.png` into ComfyUI.**\
**Important:**
- in the first input list, adjust to checkpoints that are available in your ComfyUI instance
- do no add the `.safetensors` extension, it will be added automatically
Execute the workflow.
### Lessons learned
By using a `List: String` or `List: Multiline`, we can basically send any value, as long as we ensure it matches the destinastion widget.\
You can use this approach for any combo widget, in particular when loading models, or any value in a combo widget.
> [!IMPORTANT]
> Be **very cautious** about the values you set in input: by using the Any type, we bypass the standard ComfyUI type checks, so the workflow may fail if the values are incorrect.
## Part 3 - With the `Output Config` node
Here we use the ComfyLab [`Output Config`](../../../node%20reference/output%20config.md) node, to keep our workflow and values separated.
**Load either `workflow - part 3.json` or `workflow - part 3.png` into ComfyUI.**\
**Copy one of the `output config - part 3.*` files, adjust the checkpoints to your env, and load it into the `Output Config` node.**
- you should get outputs like this (more than the 4 displayed here):
- ![outputs](./details/detail%20-%20part%203%20-%20output%20config.jpg)
Execute the workflow.
As expected, we have strictly the same results as in [Part1](#part-1---simple-with-a-dedicated-node-for-checkpoints-loras-samplers-schedulers) and [Part 2](#part-2---versatile-any-model-or-combo-widget).
### What should be noticed
- the output config file is available in 3 different formats: JSON, JSON5 and YAML
- we have replaced the input lists by a single `Output Config` node
- we've also added more outputs to the config file: CFG, steps, sampler, ... quite a lot indeed
- of course, we could have used 2 different `Output Config` nodes, one for the XY Plot itself, and another one for the other values: it's up to you to define how you want to standardize your configs
- note how the `Output Config` node auto-detects the type from output values:
- for example, `cfg` is written without quotes, so it's detected as a float
- but for combo widgets (sampler, scheduler), we have to force type to be Any (`*`)
> [!TIP]
> We won't go into details here about the configuration file itself, but you can find more information in the corresponding [node reference](../../../node%20reference/output%20config.md) and the [dedicated tutorials](../../../tutorials/Output%20Config/).
## TL;DR / Conclusion
In this tutorial, we have covered 3 different methods to vary checkpoints (or many other things):
- simple one, using a dedicated node: applicable to checkpoints, LoRAs, samplers, schedulers
- a more versatile one, that should adapt to many of your use cases: use a `List: from String` or `List: from Multiline`
- one using the `Output Config` node, to standardize your workflow and keep the values separated
> [!TIP]
> You can find [more examples](../../../examples/) in the wiki.
If you have any suggestion on how to improve this wiki, feel free to open [a discussion](https://github.com/bugltd/ComfyLab-Pack/discussions).
Thank you for reading, enjoy!

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

@@ -1,4 +1,4 @@
# Tutorials / XY Plot / 3 - Advanced techniques / Part 3
# Tutorials / XY Plot / 3 - Complex variations / Part 3
# more details about the config format in the Output Config node reference and tutorials
checkpoints:
value:
@@ -1,5 +1,5 @@
/*
Tutorials / XY Plot / 3 - Advanced techniques / Part 3
Tutorials / XY Plot / 3 - Complex variations / Part 3
more details about the config format in the Output Config node reference and tutorials
*/
{
@@ -1,6 +1,6 @@
{
"last_node_id": 20,
"last_link_id": 22,
"last_node_id": 21,
"last_link_id": 23,
"nodes": [
{
"id": 7,
@@ -305,49 +305,6 @@
1
]
},
{
"id": 15,
"type": "ListFromMultiline",
"pos": [
-240,
180
],
"size": [
290,
200
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "list",
"type": "LIST",
"links": [
18
],
"slot_index": 0,
"shape": 6
},
{
"name": "count",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "ListFromMultiline"
},
"widgets_values": [
0,
true,
"disabled",
"# here we list seed values\n156680208700286\n715168739510668\n568208062071311"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 10,
"type": "PreviewImage",
@@ -460,49 +417,6 @@
"color": "#2a363b",
"bgcolor": "#3f5159"
},
{
"id": 14,
"type": "ListFromMultiline",
"pos": [
-430,
-100
],
"size": [
470,
200
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "list",
"type": "LIST",
"links": [
15
],
"slot_index": 0,
"shape": 6
},
{
"name": "count",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "ListFromMultiline"
},
"widgets_values": [
0,
true,
"disabled",
"# here we list the checkpoints\n# do not add the '.safetensors' extension, it will be appended later\nsd_xl_base_1.0\nJuggernaut-XI-byRunDiffusion"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 13,
"type": "XYPlotQueue",
@@ -521,7 +435,7 @@
{
"name": "dim1",
"type": "LIST",
"link": 15,
"link": 23,
"shape": 6
},
{
@@ -652,6 +566,96 @@
],
"color": "#2a363b",
"bgcolor": "#3f5159"
},
{
"id": 21,
"type": "ListCheckpoints",
"pos": [
-280,
-110
],
"size": [
315,
126
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "list",
"type": "LIST",
"links": [
23
],
"slot_index": 0,
"shape": 6
},
{
"name": "count",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "ListCheckpoints"
},
"widgets_values": [
{
"selected": [
"sd_xl_base_1.0.safetensors"
]
},
false,
"sd_xl_base_1.0.safetensors",
"none"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 15,
"type": "ListFromMultiline",
"pos": [
-240,
180
],
"size": [
290,
200
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "list",
"type": "LIST",
"links": [
18
],
"slot_index": 0,
"shape": 6
},
{
"name": "count",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "ListFromMultiline"
},
"widgets_values": [
0,
true,
"integer",
"# here we list seed values\n156680208700286\n715168739510668\n568208062071311"
],
"color": "#232",
"bgcolor": "#353"
}
],
"links": [
@@ -751,14 +755,6 @@
0,
"XY_PLOT_DATA"
],
[
15,
14,
0,
13,
0,
"LIST"
],
[
18,
15,
@@ -798,21 +794,29 @@
3,
4,
"INT"
],
[
23,
21,
0,
13,
0,
"LIST"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.1000000000000003,
"scale": 1,
"offset": [
478.0620672954512,
266.4479981606637
564,
126
]
},
"node_versions": {
"comfy-core": "0.3.10",
"ComfyLab-Pack": "1985411cc05fb8b6faa361d2fc18c3d867e9bdb5"
"ComfyLab-Pack": "4fe6425c10985b88586d861d8d5e45b62b8cd1fa"
}
},
"version": 0.4
Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

@@ -0,0 +1,819 @@
{
"last_node_id": 20,
"last_link_id": 22,
"nodes": [
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
413,
389
],
"size": [
425.27801513671875,
180.6060791015625
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
26,
474
],
"size": [
315,
98
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "ckpt_name",
"type": "COMBO",
"link": 21,
"widget": {
"name": "ckpt_name"
}
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
1
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"v1-5-pruned-emaonly.ckpt"
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1209,
188
],
"size": [
210,
46
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
11
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 11,
"type": "PreviewImage",
"pos": [
1480,
420
],
"size": [
260,
246
],
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 13
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
415,
186
],
"size": [
422.84503173828125,
164.31304931640625
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
863,
186
],
"size": [
315,
474
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 1
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
},
{
"name": "seed",
"type": "INT",
"link": 22,
"widget": {
"name": "seed"
}
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
440313153020244,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
473,
609
],
"size": [
315,
106
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
1024,
1024,
1
]
},
{
"id": 10,
"type": "PreviewImage",
"pos": [
1820,
180
],
"size": [
440,
320
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 12
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 20,
"type": "FormatString",
"pos": [
470,
-100
],
"size": [
210,
78
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "arg0",
"type": "STRING,INT,FLOAT,BOOLEAN",
"link": 19,
"label": "arg0"
},
{
"name": "arg1",
"type": "STRING,INT,FLOAT,BOOLEAN",
"link": null
}
],
"outputs": [
{
"name": "formatted",
"type": "STRING",
"links": [
20
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "FormatString"
},
"widgets_values": [
"{arg0}.safetensors"
],
"color": "#2a363b",
"bgcolor": "#3f5159"
},
{
"id": 19,
"type": "ConvertToAny",
"pos": [
730,
-100
],
"size": [
168,
26
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "value",
"type": "*",
"link": 20
}
],
"outputs": [
{
"name": "value_any",
"type": "*",
"links": [
21
],
"slot_index": 0,
"label": "value (Any)"
}
],
"properties": {
"Node name for S&R": "ConvertToAny"
},
"widgets_values": [],
"color": "#2a363b",
"bgcolor": "#3f5159"
},
{
"id": 13,
"type": "XYPlotQueue",
"pos": [
110,
-120
],
"size": [
315,
146
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "dim1",
"type": "LIST",
"link": 15,
"shape": 6
},
{
"name": "dim2",
"type": "LIST",
"link": 18,
"shape": 6,
"label": "dim2 (opt)"
}
],
"outputs": [
{
"name": "xy_plot_data",
"type": "XY_PLOT_DATA",
"links": [
14
],
"slot_index": 0,
"label": "XY plot data",
"color_off": "#52395B",
"color_on": "#9521c3"
},
{
"name": "dim1_value",
"type": "*",
"links": [
19
],
"slot_index": 1,
"label": "dim1 value"
},
{
"name": "dim2_value",
"type": "*",
"links": [
22
],
"slot_index": 2,
"label": "dim2 value"
}
],
"properties": {
"Node name for S&R": "XYPlotQueue"
},
"widgets_values": [
0,
0,
0
],
"color": "#2a363b",
"bgcolor": "#3f5159"
},
{
"id": 12,
"type": "XYPlotRender",
"pos": [
1450,
170
],
"size": [
315,
186
],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "xy_plot_data",
"type": "XY_PLOT_DATA",
"link": 14,
"label": "XY plot data",
"color_off": "#52395B",
"color_on": "#9521c3"
},
{
"name": "image",
"type": "IMAGE",
"link": 11
},
{
"name": "plot_config_grid",
"type": "PLOT_CONFIG_GRID",
"link": null,
"shape": 7,
"label": "config: grid (opt)"
},
{
"name": "plot_config_header",
"type": "PLOT_CONFIG_HF",
"link": null,
"shape": 7,
"label": "config: header (opt)"
},
{
"name": "plot_config_footer",
"type": "PLOT_CONFIG_HF",
"link": null,
"shape": 7,
"label": "config: footer (opt)"
}
],
"outputs": [
{
"name": "grid",
"type": "IMAGE",
"links": [
12
],
"slot_index": 0
},
{
"name": "image",
"type": "IMAGE",
"links": [
13
],
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "XYPlotRender"
},
"widgets_values": [
"{dim1}",
"{dim2}",
true
],
"color": "#2a363b",
"bgcolor": "#3f5159"
},
{
"id": 14,
"type": "ListFromMultiline",
"pos": [
-430,
-100
],
"size": [
470,
200
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "list",
"type": "LIST",
"links": [
15
],
"slot_index": 0,
"shape": 6
},
{
"name": "count",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "ListFromMultiline"
},
"widgets_values": [
0,
true,
"disabled",
"# here we list the checkpoints\n# do not add the '.safetensors' extension, it will be appended later\nsd_xl_base_1.0\nJuggernaut-XI-byRunDiffusion"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 15,
"type": "ListFromMultiline",
"pos": [
-240,
180
],
"size": [
290,
200
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "list",
"type": "LIST",
"links": [
18
],
"slot_index": 0,
"shape": 6
},
{
"name": "count",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "ListFromMultiline"
},
"widgets_values": [
0,
true,
"integer",
"# here we list seed values\n156680208700286\n715168739510668\n568208062071311"
],
"color": "#232",
"bgcolor": "#353"
}
],
"links": [
[
1,
4,
0,
3,
0,
"MODEL"
],
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
11,
8,
0,
12,
1,
"IMAGE"
],
[
12,
12,
0,
10,
0,
"IMAGE"
],
[
13,
12,
1,
11,
0,
"IMAGE"
],
[
14,
13,
0,
12,
0,
"XY_PLOT_DATA"
],
[
15,
14,
0,
13,
0,
"LIST"
],
[
18,
15,
0,
13,
1,
"LIST"
],
[
19,
13,
1,
20,
0,
"STRING,INT,FLOAT,BOOLEAN"
],
[
20,
20,
0,
19,
0,
"*"
],
[
21,
19,
0,
4,
0,
"COMBO"
],
[
22,
13,
2,
3,
4,
"INT"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
544,
216
]
},
"node_versions": {
"comfy-core": "0.3.10",
"ComfyLab-Pack": "4fe6425c10985b88586d861d8d5e45b62b8cd1fa"
}
},
"version": 0.4
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Before

Width:  |  Height:  |  Size: 972 KiB

After

Width:  |  Height:  |  Size: 972 KiB

+6 -5
View File
@@ -41,12 +41,13 @@ Contents:
- make the grid RGBA to add a custom background inage
- display 'page X / Y'
## Tutorial 3 - advanced techniques
## Tutorial 3 - complex variations
**[>> Go to the tutorial](./3%20-%20advanced%20techniques/)**
**[>> Go to the tutorial](./3%20-%20complex%20variations/)**
Contents:
- various checkpoints (or virtually any model, LoRA, ...)
- integrate various image resolutions / aspect ratios in the grid
- combine the XY Plot with the `Output Config` node
- various checkpoints (or virtually any model, LoRA, ...), following 3 methods using:
- dedicated nodes to manually select a list of checkpoints / LoRAs / samplers / scheduler
- a more versatile approach for vurtually anything (models, any combo value)
- the `Output Config` node to keep the use case data separated from your workflow