自定义节点的构造
简单示例
以下是图像反转节点的代码,概述了自定义节点开发中的关键概念。
class InvertImageNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": { "image_in": ("IMAGE", {}) },
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image_out",)
CATEGORY = "examples"
FUNCTION = "invert"
def invert(self, image_in):
image_out = 1 - image_in
return (image_out,)
主要属性
每个自定义节点都是一个 Python 类,具有以下关键属性:
INPUT_TYPES
INPUT_TYPES
,顾名思义,定义了节点的输入。该方法返回一个 dict
,其中 必须 包含键 required
,并且 可以 还包含键 optional
和/或 hidden
。required
和 optional
输入之间的唯一区别在于,optional
输入可以不连接。有关 hidden
输入的更多信息,请参见 隐藏输入。
每个键的值是另一个 dict
,其中键值对指定输入的名称和类型。这些类型由一个 tuple
定义,第一个元素定义数据类型,第二个元素是一个包含附加参数的 dict
。
这里我们只有一个必需输入,名为 image_in
,类型为 IMAGE
,没有附加参数。
请注意,与接下来的几个属性不同,INPUT_TYPES
是一个 @classmethod
。这是为了让 Comfy 在运行时计算下拉小部件中的选项(例如要加载的检查点的名称)。我们稍后会详细介绍这一点。
RETURN_TYPES
一个包含 str
的 tuple
,定义节点返回的数据类型。如果节点没有输出,仍然必须提供 RETURN_TYPES = ()
。
如果您只有一个输出,请记得尾随逗号:RETURN_TYPES = ("IMAGE",)
。这是 Python 将其视为 tuple
的必要条件。
RETURN_NAMES
用于标记输出的名称。这是可选的;如果省略,名称将是小写的 RETURN_TYPES
。
CATEGORY
节点将在 ComfyUI 添加节点 菜单中找到的位置。可以将子菜单指定为路径,例如 examples/trivial
。
FUNCTION
在节点执行时应调用的类中 Python 函数的名称。
该函数使用命名参数调用。所有 required
(和 hidden
)输入将包含在内;只有连接的 optional
输入才会包含,因此您应该在函数定义中为它们提供默认值(或使用 **kwargs
捕获它们)。
该函数返回一个与 RETURN_TYPES
对应的元组。这是必要的,即使没有返回任何内容(return ()
)。同样,如果只有一个输出,请记得尾随逗号 return (image_out,)
!
执行控制附加功能
Comfy 的一个很棒的功能是它会缓存输出,仅执行可能与上次运行的结果不同的节点。这可以大大加快许多工作流的速度。
基本上,它通过识别哪些节点生成输出来工作(这些节点,特别是图像预览和保存图像节点,总是会执行),然后向后工作以识别自上次运行以来可能已更改的提供数据的节点。
自定义节点的两个可选特性有助于此过程。
OUTPUT_NODE
默认情况下,节点不被视为输出。设置 OUTPUT_NODE = True
可指定它是。
IS_CHANGED
默认情况下,如果节点的任何输入或小部件已更改,则 Comfy 认为该节点已更改。这通常是正确的,但您可能需要重写此功能,例如,当节点使用随机数时(并且未指定种子——在这种情况下最好有一个种子输入,以便用户可以控制可重复性并避免不必要的执行),或加载可能已外部更改的输入,或有时忽略输入(因此仅因为这些输入已更改而不需要执行)。
尽管名称如此,IS_CHANGED
不应返回 bool
。
IS_CHANGED
传递与 FUNCTION
定义的主函数相同的参数,可以返回任何 Python 对象。该对象将与上一次运行中返回的对象进行比较(如果有),如果 is_changed != is_changed_old
,则该节点将被视为已更改(如果需要深入了解,可以查看 execution.py
中的代码)。
由于 True == True
,返回 True
以表示已更改的节点将被视为未更改!我敢肯定,如果不是因为可能会破坏现有节点,Comfy 代码会更改这一点。
要指定节点始终被视为已更改(最好避免这样做,因为这会阻止 Comfy 优化运行内容),请返回 float("NaN")
。这返回一个 NaN
值,哪个也不等于任何值,甚至是另一个 NaN
。
实际检查更改的一个好示例是内置的 LoadImage 节点的代码,该节点加载图像并返回哈希值。
@classmethod
def IS_CHANGED(s, image):
image_path = folder_paths.get_annotated_filepath(image)
m = hashlib.sha256()
with open(image_path, 'rb') as f:
m.update(f.read())
return m.digest().hex()
其他属性
还有三个其他属性可以用来修改 Comfy 对节点的默认处理。
INPUT_IS_LIST, OUTPUT_IS_LIST
这些用于控制数据的顺序处理,稍后会描述 处理列表。
VALIDATE_INPUTS
如果定义了类方法 VALIDATE_INPUTS
,则在工作流开始执行之前将调用该方法。VALIDATE_INPUTS
应返回 True
(如果输入有效),或描述错误的消息(作为 str
),这将阻止执行。
验证常量
请注意,VALIDATE_INPUTS
只会收到定义为工作流中的常量的输入。任何从其他节点接收的输入在 VALIDATE_INPUTS
中 将 不可用。
VALIDATE_INPUTS
只会使用其签名请求的输入进行调用(即由 inspect.getfullargspec(obj_class.VALIDATE_INPUTS).args
返回的那些)。通过这种方式接收的任何输入将 不会 遵循默认验证规则。例如,在以下代码段中,前端将使用指定的 min
和 max
值来处理 foo
输入,但后端不会强制执行。
class CustomNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": { "foo": ("INT", {"min": 0, "max": 10}) },
}
@classmethod
def VALIDATE_INPUTS(cls, foo):
# YOLO, anything goes!
return True
此外,如果函数接受一个 **kwargs
输入,它将接收 所有 可用输入,并且所有输入都将跳过验证,就像显式指定的一样。
验证类型
如果 VALIDATE_INPUTS
方法接收到一个名为 input_types
的参数,它将接收一个字典,其中键是连接到其他节点输出的每个输入的名称,值是该输出的类型。
当此参数存在时,将跳过所有输入类型的默认验证。以下示例利用了前端允许指定多种类型的事实:
class AddNumbers:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input1": ("INT,FLOAT", {"min": 0, "max": 1000}),
"input2": ("INT,FLOAT", {"min": 0, "max": 1000}),
},
}
@classmethod
def VALIDATE_INPUTS(cls, input_types):
# input1 和 input2 的最小值和最大值仍然会被验证,因为
# 我们没有将 `input1` 或 `input2` 作为参数。
if input_types["input1"] not in ("INT", "FLOAT"):
return "input1 must be an INT or FLOAT type"
if input_types["input2"] not in ("INT", "FLOAT"):
return "input2 must be an INT or FLOAT type"
return True