ComfyUI自定义节点剖析

以下是“反转图像节点”的代码,概述了自定义节点开发中的关键概念。

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,)

1、主要属性

每个自定义节点都是一个 Python 类,具有以下关键属性:

1.1 INPUT_TYPES

INPUT_TYPES,顾名思义,定义节点的输入。该方法返回一个字典,该字典必须包含必需的键,也可能包含可选和/或隐藏的键。必需输入和可选输入之间的唯一区别是可选输入可以不连接。有关隐藏输入的更多信息,请参阅隐藏输入

每个键都有另一个字典作为其值,其中键值对指定名称和类型或输入。类型由元组定义,元组的第一个元素定义数据类型,第二个元素是附加参数的字典。

这里我们只有一个名为 image_in的必需输入,类型为 IMAGE,没有附加参数。

请注意,与接下来的几个属性不同,这个 INPUT_TYPES 是一个 @classmethod。这样,下拉小部件中的选项(例如要加载的检查点的名称)就可以由 Comfy 在运行时计算。我们稍后会详细介绍。

​1.2 RETURN_TYPES

定义节点返回的数据类型的 str 元组。如果节点没有输出,则仍必须提供 RETURN_TYPES = ()

如果你只有一个输出,请记住尾随逗号: RETURN_TYPES = ("IMAGE",) 。这是 Python 使其成为元组所必需的。

​1.3 RETURN_NAMES

用于标记输出的名称。这是可选的;如果省略,名称只是小写的 RETURN_TYPES

1.4 CATEGORY

在 ComfyUI Add Node 菜单中将找到节点的位置。子菜单可以指定为路径,例如 examples/trivial

1.5 FUNCTION

执行节点时应调用的类中的 Python 函数的名称。

使用命名参数调用该函数。将包括所有必需(和隐藏)输入;仅当它们已连接时才会包括可选输入,因此你应该在函数定义中为它们提供默认值,或使用 **kwargs 捕获它们。

该函数返回一个与 RETURN_TYPES 对应的元组。即使没有返回任何内容( return ()),这也是必需的。同样,如果你只有一个输出,请记住尾随逗号 return (image_out,)

​2、执行控制附加功能

Comfy 的一大特色是它缓存输出,并且只执行可能产生与上次运行不同结果的节点。这可以大大加快许多工作流程。

本质上,这是通过识别哪些节点产生输出(这些节点,尤其是图像预览和保存图像节点,始终被执行),然后向后工作以识别哪些节点提供自上次运行以来可能已更改的数据来实现的。

自定义节点的两个可选功能可协助此过程。

2.1 OUTPUT_NODE

默认情况下,节点不被视为输出。设置 OUTPUT_NODE = True 以指定它是输出。

​2.2 IS_CHANGED

默认情况下,如果节点的任何输入或小部件已更改,Comfy 会认为该节点已更改。这通常是正确的,但是如果节点使用随机数(并且未指定种子 - 在这种情况下,最佳做法是输入种子,以便用户可以控制可重复性并避免不必要的执行),或者加载可能已在外部更改的输入,或者有时忽略输入(因此不需要仅仅因为这些输入已更改而执行),则可能需要覆盖此设置。

尽管名称如此, IS_CHANGED 不应返回布尔值

IS_CHANGED 传递的参数与 FUNCTION 定义的主函数相同,并且可以返回任何 Python 对象。将此对象与上次运行中返回的对象(如果有)进行比较,如果 is_changed != is_changed_old(如果需要挖掘,此代码位于执行.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()

3、其他属性

还有三个其他属性可用于修改节点的默认 Comfy 处理。

​3.1 INPUT_IS_LIST、OUTPUT_IS_LIST​

这些用于控制数据的顺序处理,稍后将进行描述。

​3.2 VALIDATE_INPUTS

如果定义了类方法 VALIDATE_INPUTS,则将在工作流开始执行之前调用该方法。如果输入有效, VALIDATE_INPUTS 应返回 True,否则返回描述错误的消息(以 str 形式,这将阻止执行。

3.3 验证常量

请注意, VALIDATE_INPUTS 将仅接收在工作流中定义为常量的输入。从其他节点接收的任何输入在 VALIDATE_INPUTS 中都不可用。

VALIDATE_INPUTS 仅使用其签名请求的输入( inspect.getfullargspec(obj_class.VALIDATE_INPUTS).args 返回的输入)进行调用。以这种方式接收的任何输入都不会通过默认验证规则。例如,在以下代码片段中,前端将使用 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 输入,它将接收所有可用输入,并且所有输入都将跳过验证,就像明确指定一样。

​3.4 验证类型

如果 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):
        # The min and max of input1 and input2 are still validated because
        # we didn't take `input1` or `input2` as arguments
        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

原文链接:Anatomy of a custom node

汇智网翻译整理,转载请标明出处