seconohe
SeCoNoHe (SET's ComfyUI Node Helpers)
I have a few ComfyUI custom nodes that I wrote, and I started to repeat the same utils
over and over on each node.
Soon realized it was a waste of resources and a change or fix in one of the utils
was hard to apply to all the nodes.
So I separated them as a Python module available from PyPi.
So this is the Python module. The functionality found here has just one thing in common: was designed to be used by ComfyUI nodes. Other than that you'll find the functionality is really heterogeneous.
📖 Table of Contents
- 🎯 Core Goals
- ⚠️ Important Remarks for Developers
- ✨ Included Functionality
- 🚀 Examples of Nodes Using SeCoNoHe
- 📜 Project History
- ⚖️ License
- 🙏 Attributions
🎯 Core Goals
One of the main goals of these helpers is to be lightweight and non-intrusive.
- Minimal Dependencies: The library aims to pull in as few new dependencies as possible, ideally relying only on what's already available in a standard ComfyUI installation.
- Graceful Fallbacks: For optional features, the helpers check for libraries like
requests
orcolorama
and fall back to built-in Python functionality if they are not found. This prevents SeCoNoHe from being the cause of installation conflicts. - Safe PyTorch Handling: PyTorch (
torch
,torchaudio
) is explicitly not listed as a package dependency to avoidpip
installing a duplicate, multi-gigabyte copy. The helpers assume PyTorch is already present in the ComfyUI environment.
More on the fallbacks:
requests
: I'm quite sure that all ComfyUI installs has it installed, but isn't explicitly listed in the ComfyUI dependencies. If not available we useurllib
, which is part of the Python core. I think, and have no real proof, thatrequests
is more robust thanurllib
. For this reason ifrequests
is installed the download code will use it.colorama
: The logger code tries to use colors for DEBUG, WARNING and ERROR. Ifcolorama
is installed we use the colors from it, otherwise we use the classic ANSI escape sequences. I found a lot of ComfyUI nodes that just uses the ANSI sequences, but I just guess that usingcolorama
is more robust.
In the list of dependencies I included TQDM (progress bar), which is redundant because ComfyUI depends on it.
⚠️ Important Remarks for Developers
Here are some things I learned the hard way that might be useful for other ComfyUI node creators.
Relative Imports
- ✅ ALWAYS use relative imports for code within your node package (e.g.,
from .utils import my_helper
). - ⛔ NEVER use absolute imports for your own node's files.
- 🚫 NEVER modify
sys.path
in any code that ComfyUI will import. This is a common cause of mysterious bugs and conflicts with other custom nodes. - ONLY use absolute imports for dependencies and ComfyUI functionality.
For this use a very isolated version of the Python src
directory structure recommendation.
Recommended Directory Structure
Using what Python calls src
structure is strongly recommended, why? because most modern Python tools assume this is the case,
and most of them misserably fail if you don't use it.
Here is what I recommend:
root
|
\-- __init__.py <-- Your nodes registration, the only Python in your root
\-- pyproject.toml <-- Nodes and project information, the modern way, and needed by ComfyUI registry
|
\-- src/ <-- The `src` magic name
| |
| \-- nodes/ <-- An extra level of indirection, helps with the relative vs absolute imports
| | |
| | \-- __init__.py <-- Internal initialization, __version__, NODES_NAME, main_logger, etc. here
| | \-- nodes.py <-- One or more `nodes_xxx.py` files containing the implementation of your nodes
| | |
| | \-- utils/ <-- One or more submodules with stuff you use in your nodes
| | |
| | \-- __init__.py <-- Always put an init inside them, usually empty
| |
| \-- tests/ <-- Regression tests can be put here
| |
| \-- bootstrap/
| | |
| | \-- __init__.py <-- A `sys.path` nasty trick used for the regression tests
| |
| \-- test_xxx.py <-- Group of regression tests
|
\-- tool/ <-- Command line tools that uses functionality shared with your nodes
|
\-- bootstrap/
| |
| \-- __init__.py <-- A `sys.path` nasty trick used for the tools
|
\-- xxxx.py <-- A tool
Of course tool
and tests
are optional. If you don't have them you can save the extra nodes
level.
But using the extra level allows to add them easily in the future.
The bootstrap is used to allow absolute imports in the command line tools and regression tests.
In the case of the tools you just use things like this:
import bootstrap # noqa: F401
from src.nodes import main_logger
from src.nodes.db.hash import get_hash
from src.nodes.db.models_db import load_known_models, save_known_models, get_db_filename
from src.nodes.utils.misc import cli_add_verbose
Note that here we pretend src
is a module. As we insert the correct path the code in tool
will import your src
.
This is ok because they are standalone tools. NEVER modify the sys.path
in the code that will be imported by ComfyUI,
I saw a lot of nodes doing it.
In the case of the regression tests the imports looks like this:
import bootstrap # noqa: F401
from nodes.nodes_audio import AudioBatch
Here we pretend that nodes
is the package.
Note that this mechanism allows the use of:
- Relative imports in all the code that belongs to your node, this includes what the above example shows as
src/nodes/utils
. In the examplesrc/nodes/nodes.py
will dofrom .utils import xxxx
orfrom .utils.yyyy import xxxx
- Absolute imports from
tests
andtool
, avoiding the classic errorattempted relative import with no known parent package
Using this you won't get:
- Errors like
attempted relative import beyond top-level package
- Mysterious problems because you imported a module named
utils
from the ComfyUI core, or from another node. In particular after changing the order of the imports, or doing a "non-top-level" import.
✨ Included Functionality
Here you'll find an explanation of the functionality.
A full reference can be found here.
🔊 Logger
The ComfyUI console logs are a nightmare, and I don't want to make it worst, so I use a logger that:
- Clear Prefixing: All messages are prefixed with your chosen
NODES_NAME
(e.g.,[MyAwesomeNode] Inference complete.
), so you always know which custom node is talking. - Smart Coloring: Informational messages are kept clean (no color), while
DEBUG
,WARNING
, andERROR
messages are colored to stand out. Text labels are included if colors fail. - Browser Notifications: Warnings and errors are automatically sent to the browser as Toast Notifications, so users don't have to check the console for critical issues.
- Debug Control: Integrates with ComfyUI's
--verbose
flag and allows for per-node debugging via an environment variable ({NODES_NAME}_NODES_DEBUG=1
).
To use the logger, in the /__init__.py
use as the first import:
from .src.nodes import nodes, main_logger
This will pull the /src/nodes/__init__.py
which should include:
from seconohe.logger import initialize_logger
NODES_NAME = "NameForTheNodes"
main_logger = initialize_logger(NODES_NAME)
NODES_NAME will be used in the logs ([{NODES_NAME}] ...
), and will be used for the environment variable name ({NODES_NAME}_NODES_DEBUG
all uppercase)
In your /src/nodes/nodes.py
you get the logger like this (the main one):
from . import main_logger
logger = main_logger
And then use it as any logger from logging
, i.e. logger.error("An error")
In your /src/nodes/utils/yyyyy.py
code you can import the main_logger
or you can create a local one:
from .. import NODES_NAME
logger = logging.getLogger(f"{NODES_NAME}.yyyyy")
As this logger starts with NODES_NAME it will inherit all the main_logger
goodies.
🍞 ComfyUI Toast Notifications
Asking users to look at ComfyUI console logs is ridiculous. IMHO any node trying to really notify the user must do it in the browser. And this is not available from the Python side as a standard mechanism.
I already commented it on Discord and got some attention from ComfyUI people (see this RFC), so I guess this problem will be solved in the future.
Currently the only way to achieve it is using Java Script.
If you register the SeCoNoHe JS code you get a service for it. Note that currently I don't have a simple mechanism to add SeCoNoHe scripts to JS scripts in your own node. You might copy the files to your local JS directory.
If you want to simply register SeCoNoHe extensions do it:
In your /__init__.py
add:
from seconohe import JS_PATH
WEB_DIRECTORY = JS_PATH
If you do it the logger.warning
and logger.error
messages will be logged to the console and also sent to the browser.
If you want to send a custom message import this:
from seconohe.comfy_notification import send_toast_notification
Here is the current protype:
def send_toast_notification(logger: logging.Logger, message: str, summary: str = "Warning", severity: str = "warn",
sid: Optional[str] = None):
"""
Sends a toast notification event to the ComfyUI client.
Args:
logger (logging.Logger): The logger used in case we need to report an error
message (str): The message content of the toast.
severity (str): The type of toast. Can be 'success' | 'info' | 'warn' | 'error' | 'secondary' | 'contrast'
summary (str): Short explanation
sid (str, optional): The session ID of the client to send to.
If None, broadcasts to all clients. Defaults to None.
"""
💾 File Downloader
The objectives are:
- Notify the user that we are downloading a file
- Show progress in the console and the browser
- Notify the user about successful download, or an error
- Clearly log the file origin and destination
Surprisingly I never saw a node implementing all these objectives for its download. To get the start and end notifications you must register the Toast Notifications.
To use the downloader import:
from seconohe.downloader import download_file
And then use this function:
def download_file(logger: logging.Logger, url: str, save_dir: str, file_name: str, force_urllib: bool = False,
kind: str = "model"):
"""
Downloads a file from a URL with progress bars for both console and ComfyUI.
Also GUI notification at start and end.
We also log the URL and destination to the console.
Args:
logger (logging.Logger): The used logger
url (str): The direct download URL for the file.
save_dir (str): The directory where the file will be saved.
file_name (str): The name of the file to be saved on disk.
force_urllib (bool=False): Ignore `requests`
kind (str='model'): Kind of file we are downloading, just for the logs
"""
✍️ Automatic Node Registration
Manually maintaining the NODE_CLASS_MAPPINGS
and NODE_DISPLAY_NAME_MAPPINGS
in __init__.py
is tedious and error-prone.
This helper automates the process.
You just need to add a couple of extra members to your classes:
UNIQUE_NAME
: The unique name that identifies the nodeDISPLAY_NAME
: The name the user will see in the browser
Here is an example:
class ImageDownload:
FUNCTION = "load_or_download_image"
CATEGORY = BASE_CATEGORY + "/" + IO_CATEGORY
DESCRIPTION = ("Downloads an image to ComfyUI's 'input' directory if it doesn't exist, then loads it using the "
"built-in LoadImage logic.")
UNIQUE_NAME = "SET_ImageDownload"
DISPLAY_NAME = "Image Download and Load"
Once all your nodes has these two extra members you use the following code in /__init__.py
:
from .src.nodes import nodes_xxxx, nodes_yyyy
from seconohe.register_nodes import register_nodes
NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS = register_nodes(main_logger, [nodes_xxxx, nodes_yyyy])
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
Of course you can register nodes from just one file:
from .src.nodes import my_nodes
from seconohe.register_nodes import register_nodes
NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS = register_nodes(main_logger, [my_nodes])
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
This is all you need.
If you declare a variable named SUFFIX
in your module all the display names for the nodes in the module will have " {SUFFIX}" added.
⚙️ PyTorch Helpers
get_torch_device_options
: returns a list of devices suitable for a combo so the user can choose the inference device.
It also returns a suitable default value. Example:
from seconohe.torch import get_torch_device_options
...
@classmethod
def INPUT_TYPES(cls):
device_options, default_device = get_torch_device_options()
return {
"required": {
"target_device": (device_options, {
"default": default_device,
"tooltip": "The device (CPU or CUDA) to which the projection layer will be assigned for computation."}),
}
}
get_offload_device
: returns a torch device to move the model after use. Is basically a wrapper for
comfy.model_management.unet_offload_device()
, but can be used from a tool even when no ComfyUI is available.
get_canonical_device
: return a canonical name for a torch device. This is useful to compare torch devices, so we don't think that
cuda
and cuda:0
are different.
model_to_target
context: this context can be used to wrap the inference of a model, it:
- Moves the model to its designated
model.target_device
. - Sets
torch.backends.cudnn.benchmark
based onmodel.cudnn_benchmark_setting
if available. - Sets the model to
eval()
mode. - Wraps the operation in a
torch.no_grad()
context. - Offloads the model to the CPU (
mm.unet_offload_device()
) afterwards.
model.target_device = self.device
logger.debug("Using PyTorch Audio chunking for old model")
with model_to_target(logger, model):
separated_tensors = separate_sources(model, input_tensor_on_device)
Note that this tries to offload the model even if the inference fails. Avoiding the classic VRAM waste after a fail.
🎛️ Changing Widget Values
If during the execution of a node you need to change the value assigned to a widget of the node you can use it.
from seconohe.comfy_node_action import send_node_action
...
send_node_action(logger, "change_widget", WIDGET_NAME, NEW_VALUE)
Note that you must register the JS extensions like with the Toast Notifications.
🚀 Examples of Nodes Using SeCoNoHe
📜 Project History
- 1.0.0 2025-07-24: Initial release.
- 1.0.1 2025-07-25: Better typing hints and docs
- 1.0.2 2025-07-26: Optional version info when registering the nodes
⚖️ License
🙏 Attributions
- Main author: Salvador E. Tropea
- Assisted by Gemini 2.5 Pro