自定义节点的构造

简单示例

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

  1. class InvertImageNode:
  2. @classmethod
  3. def INPUT_TYPES(cls):
  4. return {
  5. "required": { "image_in": ("IMAGE", {}) },
  6. }
  7. RETURN_TYPES = ("IMAGE",)
  8. RETURN_NAMES = ("image_out",)
  9. CATEGORY = "examples"
  10. FUNCTION = "invert"
  11. def invert(self, image_in):
  12. image_out = 1 - image_in
  13. return (image_out,)

主要属性

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

INPUT_TYPES

INPUT_TYPES,顾名思义,定义了节点的输入。该方法返回一个 dict,其中 必须 包含键 required,并且 可以 还包含键 optional 和/或 hiddenrequiredoptional 输入之间的唯一区别在于,optional 输入可以不连接。有关 hidden 输入的更多信息,请参见 隐藏输入

每个键的值是另一个 dict,其中键值对指定输入的名称和类型。这些类型由一个 tuple 定义,第一个元素定义数据类型,第二个元素是一个包含附加参数的 dict

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

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

RETURN_TYPES

一个包含 strtuple,定义节点返回的数据类型。如果节点没有输出,仍然必须提供 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 节点的代码,该节点加载图像并返回哈希值。

  1. @classmethod
  2. def IS_CHANGED(s, image):
  3. image_path = folder_paths.get_annotated_filepath(image)
  4. m = hashlib.sha256()
  5. with open(image_path, 'rb') as f:
  6. m.update(f.read())
  7. 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 返回的那些)。通过这种方式接收的任何输入将 不会 遵循默认验证规则。例如,在以下代码段中,前端将使用指定的 minmax 值来处理 foo 输入,但后端不会强制执行。

  1. class CustomNode:
  2. @classmethod
  3. def INPUT_TYPES(cls):
  4. return {
  5. "required": { "foo": ("INT", {"min": 0, "max": 10}) },
  6. }
  7. @classmethod
  8. def VALIDATE_INPUTS(cls, foo):
  9. # YOLO, anything goes!
  10. return True

此外,如果函数接受一个 **kwargs 输入,它将接收 所有 可用输入,并且所有输入都将跳过验证,就像显式指定的一样。

验证类型

如果 VALIDATE_INPUTS 方法接收到一个名为 input_types 的参数,它将接收一个字典,其中键是连接到其他节点输出的每个输入的名称,值是该输出的类型。

当此参数存在时,将跳过所有输入类型的默认验证。以下示例利用了前端允许指定多种类型的事实:

  1. class AddNumbers:
  2. @classmethod
  3. def INPUT_TYPES(cls):
  4. return {
  5. "required": {
  6. "input1": ("INT,FLOAT", {"min": 0, "max": 1000}),
  7. "input2": ("INT,FLOAT", {"min": 0, "max": 1000}),
  8. },
  9. }
  10. @classmethod
  11. def VALIDATE_INPUTS(cls, input_types):
  12. # input1 和 input2 的最小值和最大值仍然会被验证,因为
  13. # 我们没有将 `input1` 或 `input2` 作为参数。
  14. if input_types["input1"] not in ("INT", "FLOAT"):
  15. return "input1 must be an INT or FLOAT type"
  16. if input_types["input2"] not in ("INT", "FLOAT"):
  17. return "input2 must be an INT or FLOAT type"
  18. return True