From be13cd817bcbd6cbc7aaa56fbe95f7ced09c5145 Mon Sep 17 00:00:00 2001 From: godmt Date: Tue, 23 Jun 2026 10:55:42 +0900 Subject: [PATCH] update README en/ja and description & tooltips --- README.ja.md | 166 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 177 +++++++++++++++++++++++++++++++++++++++++++++++---- js/web.js | 75 +++++++++++++++++----- nodes.py | 150 ++++++++++++++++++++++++++++++++----------- 4 files changed, 504 insertions(+), 64 deletions(-) create mode 100644 README.ja.md diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 0000000..a2e6df3 --- /dev/null +++ b/README.ja.md @@ -0,0 +1,166 @@ +# ComfyUI-List-Utils + +[English](README.md) + +ComfyUI で「複数の値を順番に処理する」「Python の list 値としてまとめて扱う」「型つきの値セットを受け渡す」ためのプリミティブなカスタムノード集です。 + +この拡張では、似ているけれど役割が違う 3 種類のコンテナを扱います。 + +| 名前 | 意味 | 主な用途 | +| --- | --- | --- | +| `List` | ComfyUI 公式の Data list / List processing | 複数の値を ComfyUI の逐次処理として流す | +| `PyList` | 生の Python `list` 値 | 値の配列を 1 つの値として保持・加工する | +| `Pack` | List-Utils 独自の、型情報つきコンテナ | 複数の異なる型の値をまとめて渡し、後で元の型で取り出す | + +## Installation + +`ComfyUI/custom_nodes` ディレクトリで clone して、ComfyUI を再起動してください。 + +```bash +git clone https://github.com/godmt/ComfyUI-List-Utils.git +``` + +ノードは `list_utils` カテゴリに追加されます。 + +## GUI Help + +各ノードには ComfyUI 上で確認できる説明を追加しています。 + +- ノード自体の説明は、ノードにマウスを重ねたときの説明として表示されます。 +- widget / input には、用途を説明する tooltip を追加しています。 +- output には、何が出力されるかを説明する tooltip を追加しています。 +- `Create List`、`Create PyList`、`Merge List`、`Merge PyList`、`Pack`、`Exec` の動的入力にも tooltip が付きます。 + +## List / PyList / Pack の使い分け + +### List + +`List` は ComfyUI の List processing 用です。たとえば 10 個の prompt、seed、画像、パスなどを順番に処理したいときに使います。 + +List-Utils の `List` 系ノードは、ComfyUI の `OUTPUT_IS_LIST` / `INPUT_IS_LIST` を使います。出力ソケットの型は `STRING`、`INT`、`IMAGE` など元の型として扱われます。 + +よく使う場面: + +- 複数の文字列を 1 件ずつ downstream ノードへ流す +- `Create Range` で連番 seed を作る +- `List Dir` でファイルパスを列挙して順番に処理する +- `List: Get By Index` / `List: Slice` で ComfyUI List を取り出す + +### PyList + +`PyList` は Python の `list` そのものを 1 つの値として受け渡す型です。ComfyUI の逐次処理にはならず、配列をまとめたまま扱いたいときに使います。 + +よく使う場面: + +- 文字列や数値の配列をまとめて保持する +- Python list として index / slice / merge / cast する +- `List To PyList` で ComfyUI List を 1 つの Python list にまとめる +- `PyList To List` で Python list を ComfyUI の逐次処理へ戻す + +### Pack + +`Pack` は複数の値を 1 本の `PACK` ソケットにまとめるためのコンテナです。各入力の値だけでなく、入力名と ComfyUI の型も保持します。 + +`Pack` / `Unpack` は、型の違う値をまとめて受け渡したいときに便利です。たとえば `MODEL`、`CLIP`、`VAE`、`CONDITIONING`、`IMAGE` のように、通常は 1 つの list に入れづらい値をまとめて運べます。 + +## Typical Workflows + +### 文字列を分割して順番に処理する + +1. `Split String` に複数行テキストや区切り文字つきテキストを入力します。 +2. `STRING` 出力を使うと、ComfyUI の `List` として順番に流れます。 +3. `PYLIST` 出力を使うと、Python list 値としてまとめて扱えます。 +4. `length` 出力で要素数を取得できます。 + +### PyList を ComfyUI List に変換する + +1. `Create PyList`、`Split String`、`Create Range` などで `PYLIST` を作ります。 +2. `PyList To List` に接続します。 +3. 出力は ComfyUI の `List` になり、下流で逐次処理できます。 + +逆に、ComfyUI の `List` を Python list としてまとめたい場合は `List To PyList` を使います。 + +### 複数の型をまとめて渡す + +1. `Pack` に値を接続します。 +2. 最後の空ソケットに接続すると、次の入力ソケットが自動で追加されます。 +3. `Unpack` に接続すると、`Pack` 側の入力数と型に合わせて出力が作られます。 + +`Unpack` は一度 `Pack` に接続すると出力型を確定できます。`Switch` ノードや `Anything Everywhere` 系ノードを間に挟んだ構成でも、上流の `Pack` をたどれる場合は型を復元できます。 + +## Nodes + +### Create / Generate + +| Node | Output | Description | +| --- | --- | --- | +| `Split String` | `STRING` List, `PYLIST`, `length` | 文字列を delimiter または行で分割します。空要素は除去されます。 | +| `Create List` | `List` | 複数入力を ComfyUI List として作成します。 | +| `Create PyList` | `PYLIST` | 複数入力を Python list として作成します。 | +| `Create Range` | `INT` List, `PYLIST`, `length` | Python `range(start, stop, step)` で整数列を作ります。 | +| `Create Arange` | `FLOAT` List, `PYLIST`, `length` | `numpy.arange(start, stop, step)` で数値列を作ります。 | +| `Create Linspace` | `FLOAT` List, `PYLIST`, `length` | `numpy.linspace(start, stop, num)` で等間隔の数値列を作ります。 | +| `List Dir` | `STRING` List, `PYLIST`, `length` | 指定フォルダから `glob` pattern に一致する項目を列挙します。 | + +### Merge / Convert + +| Node | Output | Description | +| --- | --- | --- | +| `Merge List` | `List` | 複数の ComfyUI List を連結します。 | +| `Merge PyList` | `PYLIST` | 複数の PyList を連結します。 | +| `List To PyList` | `PYLIST` | ComfyUI List を Python list 値に変換します。 | +| `PyList To List` | `List` | PyList を ComfyUI List processing 用に展開します。 | + +### Get / Slice + +| Node | Output | Description | +| --- | --- | --- | +| `List: Get By Index` | item | ComfyUI List から指定 index の要素を取り出します。 | +| `PyList: Get By Index` | item | PyList から指定 index の要素を取り出します。 | +| `List: Slice` | `List` | ComfyUI List を `start:end` で切り出します。 | +| `PyList: Slice` | `PYLIST` | PyList を `start:end` で切り出します。 | + +### Pack / Unpack + +| Node | Output | Description | +| --- | --- | --- | +| `Pack` | `PACK` | 複数入力を、名前・型・値つきで 1 つの Pack にまとめます。 | +| `Unpack` | original types | Pack から元の値を取り出します。出力型は接続元の Pack に合わせて動的に変わります。 | + +### Inspect / Utility + +| Node | Output | Description | +| --- | --- | --- | +| `Get Length` | `INT` | `len(value)` を返し、ノード上にも表示します。 | +| `Get Shape` | `width`, `height`, `PyList_size`, `channels` | `IMAGE`、`LATENT`、`MASK` の形状を取得します。 | +| `Any To Dict` | `DICT`, `str()` | オブジェクトを dict または文字列に変換して確認します。 | +| `Get Widgets Values` | `LIST` | 接続元ノードの widget values を取得して表示します。 | +| `Any Cast` | selected type | 値を指定型として出力します。基本的な `STRING` / `INT` / `FLOAT` / `BOOLEAN` 変換にも使えます。 | +| `PyList Item Cast` | `PYLIST` | PyList 内の各要素を `STRING` / `INT` / `FLOAT` / `BOOLEAN` などへ変換します。 | +| `Exec` | any | 入力値 `x[0]`, `x[1]`, ... を使って Python コードを実行し、`result` を返します。 | + +## Dynamic Inputs + +一部のノードは入力ソケットを動的に増やします。 + +- `Create List` +- `Create PyList` +- `Merge List` +- `Merge PyList` +- `Pack` +- `Exec` + +最後の空ソケットに接続すると次のソケットが追加されます。接続を外すと未使用ソケットは整理されます。 + +`Create List` / `Merge List` は、最初に接続した入力の型に合わせて出力型が変わります。`Pack` は入力ごとの型を保持します。 + +## Notes + +- `List` と `PyList` は別物です。接続できない、または期待どおりに逐次処理されない場合は `List To PyList` / `PyList To List` で変換してください。 +- `List: Get By Index` と `PyList: Get By Index` は、index が範囲外の場合 `None` を返します。 +- Python の slice と同じく、`start:end` の `end` は含まれません。 +- `Exec` は任意の Python コードを実行します。信頼できる workflow でだけ使ってください。 + +## Example + +![Example workflow](https://github.com/user-attachments/assets/d1543cf3-84db-4b94-b326-88d48f46bd49) diff --git a/README.md b/README.md index c3bdb58..e0f9da7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,167 @@ # ComfyUI-List-Utils -Comfy List and Python list (PyList) utilities which support: create, merge, convert, get or slice -## Installation -goto ComfyUI/custom_nodes dir in terminal(cmd) -git clone https://github.com/godmt/ComfyUI-List-Utils.git -Restart ComfyUI -## Usage -### Pack/Unpack -Unpack node must be connected to Pack node at once to determine the output types. -Once connected, Unpack node maintains its output types even when disconnected. -(You can connect Pack and Unpack through Switch nodes or Anything Everywhere nodes!) -## Examples -![image](https://github.com/user-attachments/assets/d1543cf3-84db-4b94-b326-88d48f46bd49) +[Japanese](README.ja.md) + +Primitive ComfyUI custom nodes for working with ComfyUI List processing, raw Python list values, and typed value bundles. + +This extension uses three similar-looking but very different container concepts: + +| Name | Meaning | Main Use | +| --- | --- | --- | +| `List` | ComfyUI's official Data list / List processing | Run downstream nodes once per item | +| `PyList` | A raw Python `list` value | Keep an array as one value and manipulate it directly | +| `Pack` | A List-Utils typed container | Bundle values of different ComfyUI types and restore them later | + +## Installation + +Clone this repository into `ComfyUI/custom_nodes`, then restart ComfyUI. + +```bash +git clone https://github.com/godmt/ComfyUI-List-Utils.git +``` + +Nodes are added under the `list_utils` category. + +## GUI Help + +The nodes include inline help for the ComfyUI interface: + +- Node-level descriptions explain what each node is for. +- Inputs and widgets include tooltips for expected values. +- Outputs include tooltips for what they return. +- Dynamic inputs created by `Create List`, `Create PyList`, `Merge List`, `Merge PyList`, `Pack`, and `Exec` also receive tooltips. + +## Choosing List, PyList, Or Pack + +### List + +`List` means ComfyUI List processing. Use it when you want ComfyUI to process several prompts, seeds, images, paths, or other values one item at a time. + +List-Utils `List` nodes use ComfyUI's `OUTPUT_IS_LIST` / `INPUT_IS_LIST` behavior. The output socket type remains the original item type, such as `STRING`, `INT`, or `IMAGE`. + +Common uses: + +- Send several strings downstream one by one +- Generate sequential seeds with `Create Range` +- Enumerate files with `List Dir` +- Extract or slice a ComfyUI List with `List: Get By Index` / `List: Slice` + +### PyList + +`PyList` is a raw Python `list` passed as one ComfyUI value. It does not trigger ComfyUI List processing by itself. + +Common uses: + +- Keep strings, numbers, paths, or other values as one array +- Use Python-style index, slice, merge, or cast operations +- Convert a ComfyUI List into one Python list with `List To PyList` +- Convert a Python list back into ComfyUI List processing with `PyList To List` + +### Pack + +`Pack` bundles multiple values into one `PACK` socket while preserving each input's name, type, and value. + +Use `Pack` / `Unpack` when you need to pass different ComfyUI types together. For example, a single Pack can carry `MODEL`, `CLIP`, `VAE`, `CONDITIONING`, and `IMAGE` values, then restore them as separate typed outputs later. + +## Typical Workflows + +### Split Text And Process Each Item + +1. Put multiline or delimiter-separated text into `Split String`. +2. Use the `STRING` output when you want ComfyUI List processing. +3. Use the `PYLIST` output when you want one raw Python list value. +4. Use `length` when you need the item count. + +### Convert PyList To ComfyUI List + +1. Create a `PYLIST` with `Create PyList`, `Split String`, `Create Range`, or a similar node. +2. Connect it to `PyList To List`. +3. The output becomes a ComfyUI List-processing output and downstream nodes run once per item. + +Use `List To PyList` for the opposite direction, when you want to collect a ComfyUI List into one Python list value. + +### Bundle Different Types + +1. Connect values to `Pack`. +2. Connecting the last empty socket adds another input automatically. +3. Connect `Pack` to `Unpack`. +4. `Unpack` creates outputs matching the input count and types from `Pack`. + +`Unpack` can determine its output types after being connected to `Pack` once. It can also trace through some intermediate nodes such as switch-style nodes or Anything Everywhere-style nodes when the upstream `Pack` is discoverable. + +## Nodes + +### Create / Generate + +| Node | Output | Description | +| --- | --- | --- | +| `Split String` | `STRING` List, `PYLIST`, `length` | Splits text by delimiter and/or lines. Empty items are removed. | +| `Create List` | `List` | Creates a ComfyUI List from multiple dynamic inputs. | +| `Create PyList` | `PYLIST` | Creates a raw Python list value from multiple dynamic inputs. | +| `Create Range` | `INT` List, `PYLIST`, `length` | Creates integer values with Python `range(start, stop, step)`. | +| `Create Arange` | `FLOAT` List, `PYLIST`, `length` | Creates values with `numpy.arange(start, stop, step)`. | +| `Create Linspace` | `FLOAT` List, `PYLIST`, `length` | Creates evenly spaced values with `numpy.linspace(start, stop, num)`. | +| `List Dir` | `STRING` List, `PYLIST`, `length` | Lists path matches from a folder using a `glob` pattern. | + +### Merge / Convert + +| Node | Output | Description | +| --- | --- | --- | +| `Merge List` | `List` | Concatenates multiple ComfyUI Lists. | +| `Merge PyList` | `PYLIST` | Concatenates multiple PyLists. | +| `List To PyList` | `PYLIST` | Collects a ComfyUI List into one Python list value. | +| `PyList To List` | `List` | Expands a PyList into ComfyUI List processing. | + +### Get / Slice + +| Node | Output | Description | +| --- | --- | --- | +| `List: Get By Index` | item | Reads one item from a ComfyUI List by index. | +| `PyList: Get By Index` | item | Reads one item from a PyList by index. | +| `List: Slice` | `List` | Slices a ComfyUI List with Python-style `start:end`. | +| `PyList: Slice` | `PYLIST` | Slices a PyList with Python-style `start:end`. | + +### Pack / Unpack + +| Node | Output | Description | +| --- | --- | --- | +| `Pack` | `PACK` | Bundles multiple inputs with their names, types, and values. | +| `Unpack` | original types | Restores values from a Pack. Output types change dynamically to match the connected Pack. | + +### Inspect / Utility + +| Node | Output | Description | +| --- | --- | --- | +| `Get Length` | `INT` | Returns `len(value)` and displays it on the node. | +| `Get Shape` | `width`, `height`, `PyList_size`, `channels` | Reads shape information from `IMAGE`, `LATENT`, or `MASK`. | +| `Any To Dict` | `DICT`, `str()` | Converts an object to a dict when possible and also returns `str(value)`. | +| `Get Widgets Values` | `LIST` | Reads and displays widget values from the connected upstream node. | +| `Any Cast` | selected type | Relabels a value as a selected type and converts basic scalar types when possible. | +| `PyList Item Cast` | `PYLIST` | Converts each item inside a PyList to `STRING`, `INT`, `FLOAT`, `BOOLEAN`, and similar types. | +| `Exec` | any | Runs Python code using inputs as `x[0]`, `x[1]`, ... and returns `result`. | + +## Dynamic Inputs + +These nodes add input sockets dynamically: + +- `Create List` +- `Create PyList` +- `Merge List` +- `Merge PyList` +- `Pack` +- `Exec` + +Connect the last empty socket to add the next socket. Disconnecting sockets cleans up unused inputs. + +`Create List` / `Merge List` adapt their output type from the first connected input. `Pack` preserves the type of each input separately. + +## Notes + +- `List` and `PyList` are different. If a connection does not work, or execution is not happening in the expected mode, use `List To PyList` or `PyList To List`. +- `List: Get By Index` and `PyList: Get By Index` return `None` when the index is out of range. +- Slices use Python behavior: `start:end` does not include the item at `end`. +- `Exec` runs arbitrary Python code. Use it only in workflows you trust. + +## Example + +![Example workflow](https://github.com/user-attachments/assets/d1543cf3-84db-4b94-b326-88d48f46bd49) diff --git a/js/web.js b/js/web.js index cfee246..2d03be8 100644 --- a/js/web.js +++ b/js/web.js @@ -15,7 +15,39 @@ const TypeSlotEvent = { Disconnect: false, } -const dynamic_connection = (node, index, event, prefix = 'in_', type = '*', names = [] +const TOOLTIP_VALUE_INPUT = "Connect a value. Connecting the last empty socket adds another input." +const TOOLTIP_PACK_VALUE_INPUT = "Value to store in the PACK. The input name, type, and value are preserved for Unpack." +const TOOLTIP_PYLIST_INPUT = "Connect a PyList. Connecting the last empty socket adds another input." +const TOOLTIP_EXEC_INPUT = "Input value available inside FUNC as x[index]. Connecting the last empty socket adds another input." +const TOOLTIP_UNPACK_OUTPUT = "Value restored from the connected PACK. Output type is copied from the original Pack input." + +const setTooltip = (slot, tooltip) => { + if (slot && tooltip) { + slot.tooltip = tooltip + } + return slot +} + +const addInputWithTooltip = (node, name, type, tooltip) => { + const index = node.inputs?.length ?? 0 + node.addInput(name, type) + return setTooltip(node.inputs?.[index], tooltip) +} + +const addOutputWithTooltip = (node, name, type, tooltip) => { + const index = node.outputs?.length ?? 0 + node.addOutput(name, type) + return setTooltip(node.outputs?.[index], tooltip) +} + +const dynamic_connection = ( + node, + index, + event, + prefix = 'in_', + type = '*', + names = [], + tooltip = "" ) => { if (!node.inputs[index].name.startsWith(prefix)) { return @@ -37,6 +69,7 @@ const dynamic_connection = (node, index, event, prefix = 'in_', type = '*', name const name = i < names.length ? names[i] : `${prefix}${i + 1}` node.inputs[i].label = name node.inputs[i].name = name + setTooltip(node.inputs[i], tooltip) } } @@ -46,7 +79,7 @@ const dynamic_connection = (node, index, event, prefix = 'in_', type = '*', name const name = nextIndex < names.length ? names[nextIndex] : `${prefix}${nextIndex + 1}` - node.addInput(name, type) + addInputWithTooltip(node, name, type, tooltip) } } @@ -93,10 +126,13 @@ app.registerExtension({ name: "godmt.ListUtils", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "GODMT_Pack" || nodeData.name === "GODMT_CreatePyList") { + const inputTooltip = nodeData.name === "GODMT_Pack" + ? TOOLTIP_PACK_VALUE_INPUT + : TOOLTIP_VALUE_INPUT const onNodeCreated = nodeType.prototype.onNodeCreated nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined - this.addInput(`${_prefix}_1`, '*') + addInputWithTooltip(this, `${_prefix}_1`, '*', inputTooltip) return r } @@ -109,7 +145,7 @@ app.registerExtension({ for (let i = length - 1; i >= 0; i--) { this.removeInput(i) } - this.addInput(`${_prefix}_1`, '*') + addInputWithTooltip(this, `${_prefix}_1`, '*', inputTooltip) } return r } @@ -118,16 +154,18 @@ app.registerExtension({ nodeType.prototype.onConnectionsChange = function (slotType, slot, event, link_info, data) { const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined if (slotType === TypeSlot.Input) { - dynamic_connection(this, slot, event, `${_prefix}_`, '*') + dynamic_connection(this, slot, event, `${_prefix}_`, '*', [], inputTooltip) if (event === TypeSlotEvent.Connect && link_info) { const fromNode = this.graph._nodes.find( (otherNode) => otherNode.id == link_info.origin_id ) const type = fromNode.outputs[link_info.origin_slot].type this.inputs[slot].type = type + setTooltip(this.inputs[slot], inputTooltip) } else if (event === TypeSlotEvent.Disconnect) { this.inputs[slot].type = '*' this.inputs[slot].label = `${_prefix}_${slot + 1}` + setTooltip(this.inputs[slot], inputTooltip) } } return r @@ -136,7 +174,7 @@ app.registerExtension({ const onNodeCreated = nodeType.prototype.onNodeCreated nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined - this.addInput(`x[0]`, '*') + addInputWithTooltip(this, `x[0]`, '*', TOOLTIP_EXEC_INPUT) return r } @@ -149,7 +187,7 @@ app.registerExtension({ for (let i = length - 1; i > 0; i--) { this.removeInput(i) } - this.addInput(`x[0]`, '*') + addInputWithTooltip(this, `x[0]`, '*', TOOLTIP_EXEC_INPUT) } return r } @@ -173,6 +211,7 @@ app.registerExtension({ const name = `x[${i-1}]` this.inputs[i].label = name this.inputs[i].name = name + setTooltip(this.inputs[i], TOOLTIP_EXEC_INPUT) } } @@ -182,7 +221,7 @@ app.registerExtension({ if (this.inputs[this.inputs.length - 1].link != undefined) { const nextIndex = this.inputs.length - 1 // string widget is regarded as input also const name = `x[${nextIndex}]` - this.addInput(name, type) + addInputWithTooltip(this, name, type, TOOLTIP_EXEC_INPUT) } if (event === TypeSlotEvent.Connect && link_info) { @@ -191,6 +230,7 @@ app.registerExtension({ ) const type = fromNode.outputs[link_info.origin_slot].type this.inputs[slot].type = type + setTooltip(this.inputs[slot], TOOLTIP_EXEC_INPUT) } else if (event === TypeSlotEvent.Disconnect) { // this.inputs[slot].type = '*' // this.inputs[slot].label = `x[${slot}]` @@ -207,7 +247,7 @@ app.registerExtension({ const onNodeCreated = nodeType.prototype.onNodeCreated nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined - this.addInput(`${_prefix}_1`, '*') + addInputWithTooltip(this, `${_prefix}_1`, '*', TOOLTIP_VALUE_INPUT) return r } @@ -221,7 +261,7 @@ app.registerExtension({ for (let i = length - 1; i >= 0; i--) { this.removeInput(i) } - this.addInput(`${_prefix}_1`, '*') + addInputWithTooltip(this, `${_prefix}_1`, '*', TOOLTIP_VALUE_INPUT) } return r } @@ -259,13 +299,14 @@ app.registerExtension({ for (let i = 0; i < this.inputs.length; i++) { this.inputs[i].label = `${_prefix}_${i + 1}` this.inputs[i].name = `${_prefix}_${i + 1}` + setTooltip(this.inputs[i], TOOLTIP_VALUE_INPUT) } } // add an extra input if (this.inputs[this.inputs.length - 1].link != undefined) { const nextIndex = this.inputs.length - this.addInput(`${_prefix}_${nextIndex + 1}`, this.inputs[0].type) + addInputWithTooltip(this, `${_prefix}_${nextIndex + 1}`, this.inputs[0].type, TOOLTIP_VALUE_INPUT) } } return r @@ -274,7 +315,7 @@ app.registerExtension({ const onNodeCreated = nodeType.prototype.onNodeCreated nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined - this.addInput(`${_prefix}_1`, 'PYLIST') + addInputWithTooltip(this, `${_prefix}_1`, 'PYLIST', TOOLTIP_PYLIST_INPUT) return r } @@ -287,7 +328,7 @@ app.registerExtension({ for (let i = length - 1; i >= 0; i--) { this.removeInput(i) } - this.addInput(`${_prefix}_1`, 'PYLIST') + addInputWithTooltip(this, `${_prefix}_1`, 'PYLIST', TOOLTIP_PYLIST_INPUT) } return r } @@ -296,16 +337,18 @@ app.registerExtension({ nodeType.prototype.onConnectionsChange = function (slotType, slot, event, link_info, data) { const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined if (slotType === TypeSlot.Input) { - dynamic_connection(this, slot, event, `${_prefix}_`, 'PYLIST') + dynamic_connection(this, slot, event, `${_prefix}_`, 'PYLIST', [], TOOLTIP_PYLIST_INPUT) if (event === TypeSlotEvent.Connect && link_info) { const fromNode = this.graph._nodes.find( (otherNode) => otherNode.id == link_info.origin_id ) const type = fromNode.outputs[link_info.origin_slot].type this.inputs[slot].type = type + setTooltip(this.inputs[slot], TOOLTIP_PYLIST_INPUT) } else if (event === TypeSlotEvent.Disconnect) { this.inputs[slot].type = 'PYLIST' this.inputs[slot].label = `${_prefix}_${slot + 1}` + setTooltip(this.inputs[slot], TOOLTIP_PYLIST_INPUT) } } return r @@ -316,6 +359,7 @@ app.registerExtension({ const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined // shrink outputs to 1 this.outputs[0].type = "*" + setTooltip(this.outputs[0], TOOLTIP_UNPACK_OUTPUT) const output_len = this.outputs.length for (let i = output_len - 1; i > 0; i--) { this.removeOutput(i) @@ -373,10 +417,11 @@ app.registerExtension({ this.removeOutput(i) } for (let i = cur_len; i < output_len; i++) { - this.addOutput(`${_prefix}_${i + 1}`, origin_inputs[i].type) + addOutputWithTooltip(this, `${_prefix}_${i + 1}`, origin_inputs[i].type, TOOLTIP_UNPACK_OUTPUT) } for (let i = 0; i < cur_len && i < output_len; i++) { this.outputs[i].type = origin_inputs[i].type + setTooltip(this.outputs[i], TOOLTIP_UNPACK_OUTPUT) } } } diff --git a/nodes.py b/nodes.py index 7a86811..df1bf1b 100644 --- a/nodes.py +++ b/nodes.py @@ -62,16 +62,22 @@ class SplitString: def INPUT_TYPES(self): return { "required": { - "STRING": ("STRING", {"multiline": True}), - "delimiter": ("STRING", {"default": ","}), - "splitlines": ("BOOLEAN", {"default": False}), - "strip": ("BOOLEAN", {"default": False}) + "STRING": ("STRING", {"multiline": True, "tooltip": "Text to split. Multiline text is supported."}), + "delimiter": ("STRING", {"default": ",", "tooltip": "Delimiter used inside each line. Leave empty to skip delimiter splitting."}), + "splitlines": ("BOOLEAN", {"default": False, "tooltip": "Split the input into lines before applying the delimiter."}), + "strip": ("BOOLEAN", {"default": False, "tooltip": "Trim leading and trailing whitespace from each item."}) } } TITLE = "Split String" + DESCRIPTION = "Split text into both a ComfyUI List-processing output and a PyList. Useful for prompt candidates, filenames, seed lists, and other simple batches." RETURN_TYPES = ("STRING", "PYLIST", "INT") RETURN_NAMES = ("STRING", "PYLIST", "length") + OUTPUT_TOOLTIPS = ( + "Split items as a ComfyUI List-processing output.", + "Split items as one raw Python list value.", + "Number of split items.", + ) OUTPUT_IS_LIST = (True, False, False, ) FUNCTION = "run" CATEGORY = "list_utils" @@ -105,13 +111,15 @@ class ListGetByIndex: def INPUT_TYPES(self): return { "required": { - "ANY": (ANY_TYPE, {"forceInput": True}), - "index": ("INT", {"forceInput": False, "default": 0, "min": -9007199254740992, "max": 9007199254740992}), + "ANY": (ANY_TYPE, {"forceInput": True, "tooltip": "Connect a ComfyUI List-processing value."}), + "index": ("INT", {"forceInput": False, "default": 0, "min": -9007199254740992, "max": 9007199254740992, "tooltip": "Item index to read. 0 is the first item, -1 is the last item."}), } } TITLE = "List: Get By Index" + DESCRIPTION = "Read one item by index from a ComfyUI List-processing input. Use this for List values, not PyList values." RETURN_TYPES = (ANY_TYPE, ) + OUTPUT_TOOLTIPS = ("The item at the selected index. Returns None when the index is out of range.",) INPUT_IS_LIST = True FUNCTION = "run" CATEGORY = "list_utils" @@ -132,13 +140,15 @@ class PyListGetByIndex: def INPUT_TYPES(self): return { "required": { - "PYLIST": ("PYLIST", {"forceInput": True}), - "index": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992}), + "PYLIST": ("PYLIST", {"forceInput": True, "tooltip": "Connect a PyList, which is a raw Python list value."}), + "index": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992, "tooltip": "Item index to read. 0 is the first item, -1 is the last item."}), } } TITLE = "PyList: Get By Index" + DESCRIPTION = "Read one item by index from a PyList. Use this for raw Python list values, not ComfyUI List-processing values." RETURN_TYPES = (ANY_TYPE, ) + OUTPUT_TOOLTIPS = ("The item at the selected index. Returns None when the index is out of range.",) FUNCTION = "run" CATEGORY = "list_utils" @@ -157,14 +167,16 @@ class ListSlice: def INPUT_TYPES(self): return { "required": { - "ANY": (ANY_TYPE, {"forceInput": True}), - "start": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992}), - "end": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992}), + "ANY": (ANY_TYPE, {"forceInput": True, "tooltip": "Connect the ComfyUI List-processing value to slice."}), + "start": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992, "tooltip": "Slice start index, matching Python list[start:end]."}), + "end": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992, "tooltip": "Slice end index. The item at this index is not included."}), } } TITLE = "List: Slice" + DESCRIPTION = "Slice a ComfyUI List-processing input with Python-style start:end semantics and keep it as a List output." RETURN_TYPES = (ANY_TYPE, ) + OUTPUT_TOOLTIPS = ("The sliced ComfyUI List-processing output.",) INPUT_IS_LIST = True OUTPUT_IS_LIST = (True, ) FUNCTION = "run" @@ -184,14 +196,16 @@ class PyListSlice: def INPUT_TYPES(self): return { "required": { - "PYLIST": ("PYLIST", {"forceInput": True}), - "start": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992}), - "end": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992}), + "PYLIST": ("PYLIST", {"forceInput": True, "tooltip": "Connect the PyList to slice."}), + "start": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992, "tooltip": "Slice start index, matching Python list[start:end]."}), + "end": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992, "tooltip": "Slice end index. The item at this index is not included."}), } } TITLE = "PyList: Slice" + DESCRIPTION = "Slice a PyList with Python-style start:end semantics and keep it as a PyList output." RETURN_TYPES = ("PYLIST", ) + OUTPUT_TOOLTIPS = ("The sliced PyList.",) FUNCTION = "run" CATEGORY = "list_utils" @@ -207,12 +221,14 @@ class PyListToList: def INPUT_TYPES(self): return { "required": { - "PYLIST": ("PYLIST", {"forceInput": True}), + "PYLIST": ("PYLIST", {"forceInput": True, "tooltip": "Python list value to expand into ComfyUI List processing."}), } } TITLE = "PyList To List" + DESCRIPTION = "Convert a PyList into ComfyUI List processing. Use this when downstream nodes should run once per item." RETURN_TYPES = (ANY_TYPE, ) + OUTPUT_TOOLTIPS = ("Each PyList item emitted as a ComfyUI List-processing item.",) OUTPUT_IS_LIST = (True,) FUNCTION = "run" CATEGORY = "list_utils" @@ -229,13 +245,15 @@ class ListToPyList: def INPUT_TYPES(self): return { "required": { - "ANY": (ANY_TYPE, {"forceInput": True}), + "ANY": (ANY_TYPE, {"forceInput": True, "tooltip": "ComfyUI List-processing input to collect into one Python list value."}), } } TITLE = "List To PyList" + DESCRIPTION = "Collect a ComfyUI List-processing input into one PyList value. Use this when you want to stop per-item execution and treat the values as an array." RETURN_TYPES = ("PYLIST", ) RETURN_NAMES = ("PYLIST", ) + OUTPUT_TOOLTIPS = ("The whole input List collected as one Python list value.",) INPUT_IS_LIST = True FUNCTION = "run" CATEGORY = "list_utils" @@ -261,7 +279,9 @@ class CreateList: } TITLE = "Create List" + DESCRIPTION = "Create a ComfyUI List-processing output from multiple inputs. Connect the last empty socket to add another input." RETURN_TYPES = (ANY_TYPE, ) + OUTPUT_TOOLTIPS = ("Input values emitted as a ComfyUI List-processing output.",) OUTPUT_IS_LIST = (True, ) FUNCTION = "run" CATEGORY = "list_utils" @@ -293,7 +313,9 @@ class CreatePyList: } TITLE = "Create PyList" + DESCRIPTION = "Create a PyList from multiple inputs. Use this when you want to keep an array as one value. Connect the last empty socket to add another input." RETURN_TYPES = ("PYLIST", ) + OUTPUT_TOOLTIPS = ("A raw Python list containing the input values.",) FUNCTION = "run" CATEGORY = "list_utils" @@ -324,8 +346,10 @@ class MergeList: } TITLE = "Merge List" + DESCRIPTION = "Concatenate multiple ComfyUI List-processing inputs into one List output." INPUT_IS_LIST = True RETURN_TYPES = (ANY_TYPE, ) + OUTPUT_TOOLTIPS = ("The concatenated ComfyUI List-processing output.",) OUTPUT_IS_LIST = (True, ) FUNCTION = "run" CATEGORY = "list_utils" @@ -360,7 +384,9 @@ class MergePyList: } TITLE = "Merge PyList" + DESCRIPTION = "Concatenate multiple PyList inputs as raw Python lists." RETURN_TYPES = ("PYLIST", ) + OUTPUT_TOOLTIPS = ("The concatenated PyList.",) FUNCTION = "run" CATEGORY = "list_utils" @@ -382,15 +408,21 @@ class CreateRange: def INPUT_TYPES(self): return { "required": { - "start": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992}), - "stop": ("INT", {"default": 1, "min": -9007199254740992, "max": 9007199254740992}), - "step": ("INT", {"default": 1, "min": -9007199254740992, "max": 9007199254740992}), + "start": ("INT", {"default": 0, "min": -9007199254740992, "max": 9007199254740992, "tooltip": "First integer to generate."}), + "stop": ("INT", {"default": 1, "min": -9007199254740992, "max": 9007199254740992, "tooltip": "Stop boundary. This value is not included."}), + "step": ("INT", {"default": 1, "min": -9007199254740992, "max": 9007199254740992, "tooltip": "Increment between values, matching Python range()."}), }, } TITLE = "Create Range" + DESCRIPTION = "Generate integers with Python range(start, stop, step), output as both a ComfyUI List and a PyList. Useful for seeds and indexes." RETURN_TYPES = ("INT", "PYLIST", "INT") RETURN_NAMES = ("INT", "PYLIST", "length") + OUTPUT_TOOLTIPS = ( + "Generated integers as a ComfyUI List-processing output.", + "Generated integers as one Python list value.", + "Number of generated items.", + ) OUTPUT_IS_LIST = (True, False, False) FUNCTION = "run" CATEGORY = "list_utils" @@ -408,15 +440,21 @@ class CreateArange: def INPUT_TYPES(self): return { "required": { - "start": ("FLOAT", {"default": 0}), - "stop": ("FLOAT", {"default": 1}), - "step": ("FLOAT", {"default": 1}), + "start": ("FLOAT", {"default": 0, "tooltip": "First value to generate."}), + "stop": ("FLOAT", {"default": 1, "tooltip": "Stop boundary. This value is usually not included."}), + "step": ("FLOAT", {"default": 1, "tooltip": "Increment between values, matching numpy.arange()."}), }, } TITLE = "Create Arange" + DESCRIPTION = "Generate floating-point values with numpy.arange(start, stop, step), output as both a ComfyUI List and a PyList." RETURN_TYPES = ("FLOAT", "PYLIST", "INT") RETURN_NAMES = ("FLOAT", "PYLIST", "length") + OUTPUT_TOOLTIPS = ( + "Generated values as a ComfyUI List-processing output.", + "Generated values as one Python list value.", + "Number of generated items.", + ) OUTPUT_IS_LIST = (True, False, False) FUNCTION = "run" CATEGORY = "list_utils" @@ -434,15 +472,21 @@ class CreateLinspace: def INPUT_TYPES(self): return { "required": { - "start": ("FLOAT", {"default": 0}), - "stop": ("FLOAT", {"default": 1}), - "num": ("INT", {"default": 10, "min": 2}), + "start": ("FLOAT", {"default": 0, "tooltip": "First value."}), + "stop": ("FLOAT", {"default": 1, "tooltip": "Last value. numpy.linspace() includes this value."}), + "num": ("INT", {"default": 10, "min": 2, "tooltip": "Number of values to generate."}), }, } TITLE = "Create Linspace" + DESCRIPTION = "Generate evenly spaced values with numpy.linspace(start, stop, num), output as both a ComfyUI List and a PyList." RETURN_TYPES = ("FLOAT", "PYLIST", "INT") RETURN_NAMES = ("FLOAT", "PYLIST", "length") + OUTPUT_TOOLTIPS = ( + "Generated values as a ComfyUI List-processing output.", + "Generated values as one Python list value.", + "Number of generated items.", + ) OUTPUT_IS_LIST = (True, False, False) FUNCTION = "run" CATEGORY = "list_utils" @@ -469,8 +513,10 @@ class Pack: } TITLE = "Pack" + DESCRIPTION = "Bundle multiple values, including different ComfyUI types, into one PACK with each input name, type, and value preserved. Use Unpack to restore them." RETURN_TYPES = ("PACK", ) RETURN_NAMES = ("PACK", ) + OUTPUT_TOOLTIPS = ("A List-Utils container that preserves input names, types, and values.",) FUNCTION = "run" CATEGORY = "list_utils" @@ -502,7 +548,7 @@ class Unpack: def INPUT_TYPES(self): return { "required": { - "PACK": ("PACK", ), + "PACK": ("PACK", {"tooltip": "Connect a PACK created by the Pack node. Outputs are rebuilt from its saved names and types."}), }, "hidden": { "unique_id": "UNIQUE_ID", @@ -512,8 +558,10 @@ class Unpack: } TITLE = "Unpack" + DESCRIPTION = "Extract the original values from a PACK. When connected to Pack, outputs are created from the Pack input names and types." RETURN_TYPES = ByPassTypeTuple(("*", )) RETURN_NAMES = ByPassTypeTuple(("value_1", )) + OUTPUT_TOOLTIPS = ByPassTypeTuple(("A value stored in the PACK. Outputs grow dynamically to match the connected Pack.",)) FUNCTION = "run" CATEGORY = "list_utils" @@ -538,13 +586,15 @@ class GetLength: def INPUT_TYPES(self): return { "required": { - "ANY" : (ANY_TYPE, {}), + "ANY" : (ANY_TYPE, {"tooltip": "Value to pass to len(). Works with List, PyList, strings, and other values that support len()."}), }, } TITLE = "Get Length" + DESCRIPTION = "Apply Python len() to the input and display the result on the node." RETURN_TYPES = ("INT", ) RETURN_NAMES = ("length", ) + OUTPUT_TOOLTIPS = ("The result of len(value).",) FUNCTION = "run" CATEGORY = "list_utils" OUTPUT_NODE = True @@ -562,7 +612,7 @@ class GetShape: def INPUT_TYPES(self): return { "required": { - "tensor" : ("IMAGE,LATENT,MASK", {}), + "tensor" : ("IMAGE,LATENT,MASK", {"tooltip": "IMAGE, LATENT, or MASK whose shape you want to inspect."}), }, "hidden": { "unique_id": "UNIQUE_ID", @@ -572,8 +622,15 @@ class GetShape: } TITLE = "Get Shape" + DESCRIPTION = "Inspect width, height, batch size, and channels for IMAGE, LATENT, or MASK inputs. LATENT dimensions are converted to image-space size." RETURN_TYPES = ("INT", "INT", "INT", "INT") RETURN_NAMES = ("width", "height", "PyList_size", "channels") + OUTPUT_TOOLTIPS = ( + "Width. LATENT inputs are multiplied by 8 to report image-space width.", + "Height. LATENT inputs are multiplied by 8 to report image-space height.", + "Batch size. The output name is PyList_size for compatibility with existing workflows.", + "Channel count. MASK inputs may be reported as 1 channel.", + ) FUNCTION = "run" CATEGORY = "list_utils" OUTPUT_NODE = True @@ -625,13 +682,18 @@ class AnyToDict: def INPUT_TYPES(self): return { "required": { - "ANY" : (ANY_TYPE, {}), + "ANY" : (ANY_TYPE, {"tooltip": "Any value to inspect. dict values pass through; objects with __dict__ are converted with vars()."}), }, } TITLE = "Any To Dict" + DESCRIPTION = "Convert any value to a dict when possible and also return its string representation. Intended for debugging." RETURN_TYPES = ("DICT", "STRING") RETURN_NAMES = ("DICT", "str()") + OUTPUT_TOOLTIPS = ( + "The value as a dict. Returns an empty dict when conversion is not available.", + "The str() representation of the input value.", + ) FUNCTION = "run" CATEGORY = "list_utils" @@ -653,7 +715,7 @@ class GetWidgetsValues: def INPUT_TYPES(self): return { "required": { - "ANY" : (ANY_TYPE, {}), + "ANY" : (ANY_TYPE, {"tooltip": "Connect any output from the node whose widget values you want to inspect. The value is used only to locate the upstream node."}), }, "hidden": { "unique_id": "UNIQUE_ID", @@ -663,8 +725,10 @@ class GetWidgetsValues: } TITLE = "Get Widgets Values" + DESCRIPTION = "Read and display the upstream node's widgets_values. Intended for debugging node settings." RETURN_TYPES = ("LIST", ) RETURN_NAMES = ("LIST", ) + OUTPUT_TOOLTIPS = ("The upstream node's widgets_values list.",) OUTPUT_NODE = True FUNCTION = "run" CATEGORY = "list_utils" @@ -716,13 +780,15 @@ class AnyCast: def INPUT_TYPES(self): return { "required": { - "ANY" : (ANY_TYPE, {}), - "TYPE": (["*", "STRING", "INT", "FLOAT", "BOOLEAN", "IMAGE", "LATENT", "MASK", "NOISE", "SAMPLER", "SIGMAS", "GUIDER", "MODEL", "CLIP", "VAE", "CONDITIONING", "PYLIST"], {}), + "ANY" : (ANY_TYPE, {"tooltip": "Value to relabel or convert."}), + "TYPE": (["*", "STRING", "INT", "FLOAT", "BOOLEAN", "IMAGE", "LATENT", "MASK", "NOISE", "SAMPLER", "SIGMAS", "GUIDER", "MODEL", "CLIP", "VAE", "CONDITIONING", "PYLIST"], {"tooltip": "ComfyUI type shown on the output socket. STRING, INT, FLOAT, and BOOLEAN also attempt Python conversion."}), }, } TITLE = "Any Cast" + DESCRIPTION = "Output any value as the selected ComfyUI type. STRING, INT, FLOAT, and BOOLEAN also perform Python conversion." RETURN_TYPES = (ANY_TYPE, ) + OUTPUT_TOOLTIPS = ("The value output as the selected TYPE.",) FUNCTION = "run" CATEGORY = "list_utils" @@ -739,14 +805,16 @@ class PyListItemCast: def INPUT_TYPES(self): return { "required": { - "PYLIST": ("PYLIST", {"forceInput": True}), - "TYPE": (["STRING", "INT", "FLOAT", "NUMBER", "BOOLEAN", "PYLIST"], {}), + "PYLIST": ("PYLIST", {"forceInput": True, "tooltip": "PyList whose items should be converted."}), + "TYPE": (["STRING", "INT", "FLOAT", "NUMBER", "BOOLEAN", "PYLIST"], {"tooltip": "Conversion applied to each PyList item. NUMBER is the same as FLOAT."}), }, } TITLE = "PyList Item Cast" + DESCRIPTION = "Convert each item inside a PyList to STRING, INT, FLOAT, BOOLEAN, and similar basic types." RETURN_TYPES = ("PYLIST", ) RETURN_NAMES = ("PYLIST", ) + OUTPUT_TOOLTIPS = ("The PyList after converting each item.",) FUNCTION = "run" CATEGORY = "list_utils" @@ -763,8 +831,8 @@ class ListDir: def INPUT_TYPES(self): return { "required": { - "path": ("STRING", {"default": ""}), - "pattern": ("STRING", {"default": "*"}) + "path": ("STRING", {"default": "", "tooltip": "Folder path to list."}), + "pattern": ("STRING", {"default": "*", "tooltip": "Pattern passed to Path.glob, for example *.png or **/*.json."}) }, "optional": {}, "hidden": { @@ -775,8 +843,14 @@ class ListDir: } TITLE = "List Dir" + DESCRIPTION = "List files and folders under a path using a glob pattern, output as both a ComfyUI List and a PyList." RETURN_TYPES = ("STRING", "PYLIST", "INT") RETURN_NAMES = ("STRING", "PYLIST", "length") + OUTPUT_TOOLTIPS = ( + "Matched items as a ComfyUI List-processing output. Values are Path objects.", + "Matched items as one Python list value.", + "Number of matched items.", + ) OUTPUT_IS_LIST = (True, False, False, ) FUNCTION = "run" CATEGORY = "list_utils" @@ -826,7 +900,7 @@ class Exec: def INPUT_TYPES(self): return { "required": { - "FUNC": ("STRING", {"forceInput": False, "multiline": True, "default": "result = x[0]"}) + "FUNC": ("STRING", {"forceInput": False, "multiline": True, "default": "result = x[0]", "tooltip": "Python code to execute. Input values are x[0], x[1], ... Assign the value to return to result."}) }, "optional": {}, "hidden": { @@ -837,7 +911,9 @@ class Exec: } TITLE = "Exec" + DESCRIPTION = "Execute Python code with input values available as x[0], x[1], ... and return result. Use only in trusted workflows." RETURN_TYPES = (ANY_TYPE, ) + OUTPUT_TOOLTIPS = ("The value assigned to result inside FUNC.",) FUNCTION = "run" CATEGORY = "list_utils"