From 502589a9a112802fe1c295e4417cfe93178eec3e Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Mon, 21 Jul 2025 08:48:35 -0300 Subject: [PATCH] [Added] The image downloader node --- .flake8 | 8 + .github/FUNDING.yml | 1 + .../publish_comfy_registry_action.yml | 20 ++ .gitignore | 198 +++++++++++++++++ .pre-commit-config.yaml | 66 ++++++ README.md | 95 ++++++++ __init__.py | 28 +++ example_workflows/image_download.jpg | Bin 0 -> 18113 bytes example_workflows/image_download.json | 1 + js/toast_handler.js | 32 +++ mypy.ini | 22 ++ source/__init__.py | 0 source/nodes/__init__.py | 0 source/nodes/nodes_img.py | 129 +++++++++++ source/nodes/utils/__init__.py | 0 source/nodes/utils/ansi.py | 113 ++++++++++ source/nodes/utils/comfy_notification.py | 47 ++++ source/nodes/utils/downloader.py | 206 ++++++++++++++++++ source/nodes/utils/logger.py | 136 ++++++++++++ source/nodes/utils/misc.py | 6 + 20 files changed, 1108 insertions(+) create mode 100644 .flake8 create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/publish_comfy_registry_action.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 README.md create mode 100644 __init__.py create mode 100644 example_workflows/image_download.jpg create mode 100644 example_workflows/image_download.json create mode 100644 js/toast_handler.js create mode 100644 mypy.ini create mode 100644 source/__init__.py create mode 100644 source/nodes/__init__.py create mode 100644 source/nodes/nodes_img.py create mode 100644 source/nodes/utils/__init__.py create mode 100644 source/nodes/utils/ansi.py create mode 100644 source/nodes/utils/comfy_notification.py create mode 100644 source/nodes/utils/downloader.py create mode 100644 source/nodes/utils/logger.py create mode 100644 source/nodes/utils/misc.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..10dbcd5 --- /dev/null +++ b/.flake8 @@ -0,0 +1,8 @@ +# .flake8 +[flake8] +max-line-length = 127 +# extend-ignore = E203, W503 +# Add plugins if you installed them via additional_dependencies: +# select = C,E,F,W,B,B950 # Example: using flake8-bugbear (B, B950) +# docstring-convention = google +# ... other flake8 or plugin settings diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..fe37c8b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ['https://www.paypal.com/donate/?hosted_button_id=K2T86GDTTMRPL'] diff --git a/.github/workflows/publish_comfy_registry_action.yml b/.github/workflows/publish_comfy_registry_action.yml new file mode 100644 index 0000000..be0b02f --- /dev/null +++ b/.github/workflows/publish_comfy_registry_action.yml @@ -0,0 +1,20 @@ +name: Publish to Comfy registry +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "pyproject.toml" + +jobs: + publish-node: + name: Publish Custom Node to registry + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Publish Custom Node + uses: Comfy-Org/publish-node-action@main + with: + personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d95843e --- /dev/null +++ b/.gitignore @@ -0,0 +1,198 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the enitre vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +*.dst +*.epr +.*~ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..621d23a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,66 @@ +# .pre-commit-config.yaml + +# Optional: Set a minimum pre-commit version +# min_pre_commit_version: '2.9.0' + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 # Use the latest stable version + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + # - id: check-added-large-files # Optional: good for catching accidental large file commits + +- repo: https://github.com/pycqa/flake8 + rev: 7.0.0 # Use the latest stable version of flake8 + hooks: + - id: flake8 + # Optional: specify arguments for flake8 + # args: ['--max-line-length=88', '--extend-ignore=E203'] + # It's often better to configure flake8 via .flake8, setup.cfg, or pyproject.toml + # so it's consistent whether run via pre-commit or manually. + additional_dependencies: [ + # Add any flake8 plugins you use here, e.g.: + # 'flake8-bugbear', + # 'flake8-comprehensions', + # 'flake8-docstrings', + # 'pep8-naming' + ] + +- repo: https://github.com/codespell-project/codespell + rev: v2.2.6 # Use the latest stable version + hooks: + - id: codespell + args: [ + # "--ignore-words-list=your,custom,words,here", + # Or point to a file: + # "--ignore-words=.codespellignore", + "--skip=*.json,*.lock,*.svg,*.css,*.html", # Skip file types less likely to need it + # "--check-filenames", + # "--check-hidden" + ] + # You can create a .codespellignore file with one word per line for words to ignore. + + +# --- Use a "local" hook for mypy --- +- repo: local + hooks: + - id: mypy + name: mypy + # The command to execute. It's expected to be found in the PATH + # of the environment where you run `git commit`. + entry: mypy + args: ["--explicit-package-bases", "source/"] + # 'system' tells pre-commit to find the command in the current environment + # instead of building an isolated one. + language: system + # Specify which files to run on. + types: [python] + pass_filenames: false + # You might still need args if not configured elsewhere, but a mypy.ini is better. + # args: [--config-file=mypy.ini] + # `pass_filenames: false` can be useful here if you want mypy to analyze the + # whole project as configured in mypy.ini, rather than just the changed files. + # Try without it first. + # pass_filenames: false diff --git a/README.md b/README.md new file mode 100644 index 0000000..490f51e --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# ComfyUI Image Miscellaneous Nodes 🎨 + +This repository provides a set of custom nodes for ComfyUI focused on image manipulation. +Currently we just have an image downloader node, but I didn't wanted to put it with +my audio nodes. + + +## ⚙️ Main features + +✅ No extra dependencies, we use the same modules as ComfyUI + +✅ Warnings and errors visible in the browser, configurable debug information in the console + + +## 📜 Table of Contents + +- 🚀 [Installation](#-installation) +- 📦 [Dependencies](#-dependencies) +- 🖼️ [Examples](#️-examples) +- ✨ [Nodes](#-extra-nodes) + - [1. Image Download and Load](#1-image-download-and-load) +- 📝 [Usage Notes](#-usage-notes) +- 📜 [Project History](#-project-history) +- ⚖️ [License](#️-license) +- 🙏 [Attributions](#-attributions) + +## ✨ Nodes + +### 1. Image Download and Load + - **Display Name:** `Image Download and Load` + - **Internal Name:** `SET_ImageDownload` + - **Category:** `image/io` + - **Description:** Downloads an image file from a URL into the `ComfyUI/input/` directory if it's not already there, and then loads it as an image and mask. This is perfect for creating self-contained, shareable workflows with example image. + - **Inputs:** + - `image_bypass` (IMAGE, Optional): If an image is provided here it will be used for the output. You can connect a `Load Image` node here, if the connected node is muted (bypassed) we download the file, otherwise we use the image from the `Load Image` node. + - `mask_bypass` (MASK, Optional): This input complements `image_bypass`. + - `base_url` (STRING): The URL of the directory containing the image file. + - `filename` (STRING): The name of the image file to download (e.g., photo.jpg, art.png). + - **Output:** + - `image` (IMAGE): The loaded image. + - `alpha_mask` (MASK): The alpha mask for the loaded image. + - **Behavior Details:** + - **Caching:** The node checks the `ComfyUI/input/` folder first. If the file with the specified `filename` already exists, the download is skipped. + - **Bypass:** If only one of `image_bypass` and `mask_bypass` is connected the other will be assumed to be empty. You should connect both or avoid using the output corresponding to the unconnected input. + + +## 🚀 Installation + +You can install the nodes from the ComfyUI nodes manager, the name is *Image Misc*, or just do it manually: + +1. Clone this repository into your `ComfyUI/custom_nodes/` directory: + ```bash + cd ComfyUI/custom_nodes/ + git clone https://github.com/set-soft/ComfyUI-ImageMisc ComfyUI-ImageMisc + ``` +2. Restart ComfyUI. + +The nodes should then appear under the "image/io" category in the "Add Node" menu. + + +## 📦 Dependencies + +- PyTorch: Installed by ComfyUI +- Requests (optional): Usually an indirect ComfyUI dependency. If installed it will be used for downloads, it should be more robust than then built-in `urllib`, used as fallback. +- Colorama (optional): Might help to get colored log messages on some terminals. We use ANSI escape sequences when it isn't installed. + + +## 🖼️ Examples + +Once installed the examples are available in the ComfyUI workflow templates, in the *Image Misc* section (or ComfyUI-ImageMisc). + +- [image_download.json](example_workflows/image_download.json): Shows how to use the image downloader node. + + +## 📝 Usage Notes + +- **Logging:** 🔊 The nodes use Python's `logging` module. Debug messages can be helpful for understanding the transformations being applied. + You can control log verbosity through ComfyUI's startup arguments (e.g., `--preview-method auto --verbose DEBUG` for more detailed ComfyUI logs + which might also affect custom node loggers if they are configured to inherit levels). The logger name used is "ImageMisc". + You can force debugging level for these nodes defining the `IMAGEMISC_NODES_DEBUG` environment variable to `1`. + + +## 📜 Project History + +- 1.0.0 2025-07-21: Initial release + - Just the download image. + + +## ⚖️ License + +[GPL-3.0](LICENSE) + +## 🙏 Attributions + +- Good part of the initial code and this README was generated using Gemini 2.5 Pro. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..b4bd949 --- /dev/null +++ b/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2025 Salvador E. Tropea +# Copyright (c) 2025 Instituto Nacional de Tecnologïa Industrial +# License: GPL-3.0 +# Project: ComfyUI-ImageMisc +from .source.nodes import nodes_img +import inspect +import logging +from .source.nodes.utils.misc import NODES_NAME + +init_logger = logging.getLogger(NODES_NAME + ".__init__") + +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} + +for name, obj in inspect.getmembers(nodes_img): + # We skip nodes imported from the ComfyUI main nodes + if not inspect.isclass(obj) or not hasattr(obj, "INPUT_TYPES") or obj.__module__ == "nodes": + continue + assert hasattr(obj, "UNIQUE_NAME"), f"No name for {obj.__name__}" + NODE_CLASS_MAPPINGS[obj.UNIQUE_NAME] = obj + NODE_DISPLAY_NAME_MAPPINGS[obj.UNIQUE_NAME] = obj.DISPLAY_NAME + +init_logger.info(f"Registering {len(NODE_CLASS_MAPPINGS)} node(s).") +init_logger.debug(f"{list(NODE_DISPLAY_NAME_MAPPINGS.values())}") + +WEB_DIRECTORY = "./js" +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/example_workflows/image_download.jpg b/example_workflows/image_download.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0ed93aef860d0d283478e72ad9daf8df4c903970 GIT binary patch literal 18113 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 KMM(6#^8Wy19G|EF literal 0 HcmV?d00001 diff --git a/example_workflows/image_download.json b/example_workflows/image_download.json new file mode 100644 index 0000000..b2a317b --- /dev/null +++ b/example_workflows/image_download.json @@ -0,0 +1 @@ +{"id":"63a02d67-42e7-4217-9d52-28b9c456c35b","revision":0,"last_node_id":5,"last_link_id":4,"nodes":[{"id":4,"type":"MaskPreview","pos":[1457.7564697265625,1385.4490966796875],"size":[273.6515197753906,258],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"mask","name":"mask","type":"MASK","link":4}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"MaskPreview"},"widgets_values":[],"color":"#222","bgcolor":"#000"},{"id":3,"type":"PreviewImage","pos":[1461.320068359375,1076.2701416015625],"size":[273.6515197753906,258],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":3}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"PreviewImage"},"widgets_values":[],"color":"#222","bgcolor":"#000"},{"id":1,"type":"SET_ImageDownload","pos":[1077.0606689453125,1075.701416015625],"size":[270,314],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"image_bypass","name":"image_bypass","shape":7,"type":"IMAGE","link":1},{"localized_name":"mask_bypass","name":"mask_bypass","shape":7,"type":"MASK","link":2},{"localized_name":"base_url","name":"base_url","type":"STRING","widget":{"name":"base_url"},"link":null},{"localized_name":"filename","name":"filename","type":"STRING","widget":{"name":"filename"},"link":null}],"outputs":[{"localized_name":"image","name":"image","type":"IMAGE","links":[3]},{"localized_name":"alpha_mask","name":"alpha_mask","type":"MASK","links":[4]}],"properties":{"aux_id":"set-soft/ComfyUI-ImageMisc","ver":"3ab5664ee7057737c3215db19b078afbcead7a08","Node name for S&R":"SET_ImageDownload"},"widgets_values":["https://raw.githubusercontent.com/set-soft/AudioSeparation/refs/heads/main/example_workflows/","audioseparation_logo.jpg"],"color":"#2a363b","bgcolor":"#3f5159"},{"id":2,"type":"LoadImage","pos":[727.1275024414062,1075.3787841796875],"size":[274.080078125,314],"flags":{},"order":0,"mode":4,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[1]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":[2]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"LoadImage"},"widgets_values":["example.png","image"],"color":"#222","bgcolor":"#000"},{"id":5,"type":"MarkdownNote","pos":[733.544921875,821.4869384765625],"size":[989.1290283203125,186.43255615234375],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# If you \"unbypass\" the *Load Image* node you'll get the image and mask from it\n# When the inputs are not connected or come from a bypassed node you get the downloaded image\n# Images are downloaded to the ComfyUI's *input* directory, so they become available to be selected in the *Load Image* node. Just press R to refresh the nodes.\n# To \"bypass\" a node just select it and choose the icon with an arrow around a circle.\n"],"color":"#432","bgcolor":"#653"}],"links":[[1,2,0,1,0,"IMAGE"],[2,2,1,1,1,"MASK"],[3,1,0,3,0,"IMAGE"],[4,1,1,4,0,"MASK"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8432169278238246,"offset":[-188.01505304476098,-782.3508645860205]}},"version":0.4} \ No newline at end of file diff --git a/js/toast_handler.js b/js/toast_handler.js new file mode 100644 index 0000000..652f204 --- /dev/null +++ b/js/toast_handler.js @@ -0,0 +1,32 @@ +// Copyright (c) 2025 Salvador E. Tropea +// Copyright (c) 2025 Instituto Nacional de Tecnología Industrial +// License: GPLv3 +// Project: ComfyUI-ImageMisc + +// This script adds an event named "set-imagemisc-toast" +// Used to notify the user in the GUI using the Toast API + +import { app } from "/scripts/app.js"; + +// Register a new extension +app.registerExtension({ + name: "SET.ImageMisc.ToastHandler", // Unique name + + // The setup function is executed when the extension is loaded + setup() { + // Add a listener for our custom event + app.api.addEventListener("set-imagemisc-toast", (event) => { + // The data from Python is in event.detail + const { message, summary, severity } = event.detail; + + // Use the ComfyUI toast API to show the message + // app.ui.toast.addMessage is the modern way to do this + app.extensionManager.toast.add({ + severity: severity, + summary: summary, + detail: message, + life: 6000 + }); + }); + }, +}); diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..d888af5 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,22 @@ +[mypy] +# Basic configuration +python_version = 3.11 +warn_return_any = True +warn_unused_ignores = True +mypy_path = source/ + +# This is often needed when starting out, especially with libraries that lack stubs +ignore_missing_imports = True + +# You can get stricter over time by removing ignore_missing_imports +# and setting flags like these: +# disallow_untyped_defs = True +# disallow_any_unimported = True + +# Tell mypy to ignore errors from third-party libraries if they are not typed +[mypy-torch.*] +ignore_missing_imports = True +[mypy-torchaudio.*] +ignore_missing_imports = True +[mypy-pytest.*] +ignore_missing_imports = True diff --git a/source/__init__.py b/source/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/nodes/__init__.py b/source/nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/nodes/nodes_img.py b/source/nodes/nodes_img.py new file mode 100644 index 0000000..55271f0 --- /dev/null +++ b/source/nodes/nodes_img.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2025 Salvador E. Tropea +# Copyright (c) 2025 Instituto Nacional de Tecnologïa Industrial +# License: GPL-3.0 +# Project: ComfyUI-ImageMisc +# From code generated by Gemini 2.5 Pro +import os +import torch +from typing import Optional +from .utils.logger import main_logger +from .utils.downloader import download_model +from .utils.comfy_notification import send_toast_notification +try: + from folder_paths import get_input_directory # To get the ComfyUI input directory +except ModuleNotFoundError: + # No ComfyUI, this is a test environment + def get_input_directory(): + return "" +try: + # We need to import the built-in LoadImage class for ImageDownload + from nodes import LoadImage # type: ignore[attr-defined] + has_load_image = True +except Exception: + has_load_image = False + +logger = main_logger +BASE_CATEGORY = "image" +IO_CATEGORY = "io" + + +if has_load_image: + class ImageDownload: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "base_url": ("STRING", { + "default": + "https://raw.githubusercontent.com/set-soft/AudioSeparation/refs/heads/main/example_workflows/", + "tooltip": "The base URL where the image file is located." + }), + "filename": ("STRING", { + "default": "audioseparation_logo.jpg", + "tooltip": "The name of the image file to download (e.g., photo.jpg, art.png)." + }), + }, + "optional": { + "image_bypass": ("IMAGE", { + "tooltip": "If this image is present will be used instead of the downloaded one" + }), + "mask_bypass": ("MASK", {"tooltip": "If this mask is present will be used instead of the downloaded one"}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK") + RETURN_NAMES = ("image", "alpha_mask") + 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" + OUTPUT_NODE = True + + def load_or_download_image(self, base_url: str, filename: str, image_bypass: Optional[torch.Tensor] = None, + mask_bypass: Optional[torch.Tensor] = None): + # If we have something at the bypass inputs use it + if image_bypass is not None or mask_bypass is not None: + if image_bypass is None: + # Just a mask + assert mask_bypass is not None, "This should not be possible if image_bypass is None" # For mypy + image_bypass = torch.zeros(mask_bypass.shape + (3,), dtype=torch.float32, device="cpu") + logger.warning("ImageDownload: Returning an empty image") + elif mask_bypass is None: + # This is ComfyUI behavior when we don't have transparency + mask_bypass = torch.zeros((64, 64), dtype=torch.float32, device="cpu").unsqueeze(0) + logger.warning("ImageDownload: Returning an empty mask") + return (image_bypass, mask_bypass) + + save_dir = get_input_directory() + local_filepath = os.path.join(save_dir, filename) + + if not os.path.exists(local_filepath): + logger.info(f"File '{filename}' not found locally. Attempting to download.") + + if not base_url.endswith('/'): + base_url += '/' + download_url = base_url + filename + send_toast_notification(f"Downloading `{filename}`", "Download") + + try: + download_model(url=download_url, save_dir=save_dir, file_name=filename, kind="image") + except Exception as e: + logger.error(f"Download failed for {download_url}: {e}", exc_info=True) + raise + send_toast_notification("Finished downloading", "Download", 'success') + else: + logger.info(f"Found existing file, skipping download: '{local_filepath}'") + + # --- REUSE ComfyUI's LoadImage LOGIC --- + try: + # Instantiate the built-in LoadImage node + loader_instance = LoadImage() + + # The LoadImage node's `load_image` method expects the filename as passed + # by the ComfyUI widget, which is just the filename. It internally + # resolves the path using folder_paths. + + logger.debug(f"Calling built-in LoadImage.load_image() with filename: '{filename}'") + + # Call the method and return its result directly + result = loader_instance.load_image(filename) + downloaded_file = { + "images": [{ + "filename": filename, + "subfolder": "", + "type": "input" # We stored the file in the "input" folder + }] + } + return {"ui": downloaded_file, "result": result} + + except Exception as e: + logger.error(f"Failed to load image '{filename}' using built-in LoadImage node: {e}", exc_info=True) + # Re-raise to make the error visible in ComfyUI + raise IOError(f"Could not load the image file '{filename}' using the standard loader. " + "It may be corrupt or in an unsupported format.") from e +else: + logger.error("Failed to import ComfyUI `LoadImage`, please fill an issue here: " + "https://github.com/set-soft/ComfyUI-ImageMisc/issues") diff --git a/source/nodes/utils/__init__.py b/source/nodes/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/nodes/utils/ansi.py b/source/nodes/utils/ansi.py new file mode 100644 index 0000000..5e7d0cc --- /dev/null +++ b/source/nodes/utils/ansi.py @@ -0,0 +1,113 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +''' +This module generates ANSI character codes to printing colors to terminals. +See: http://en.wikipedia.org/wiki/ANSI_escape_code +''' +import sys +import os + +CSI = '\033[' +OSC = '\033]' +BEL = '\a' +is_a_tty = sys.stderr.isatty() and os.name == 'posix' + + +def code_to_chars(code): + return CSI + str(code) + 'm' if is_a_tty else '' + + +def set_title(title): + return OSC + '2;' + title + BEL + + +def clear_screen(mode=2): + return CSI + str(mode) + 'J' + + +def clear_line(mode=2): + return CSI + str(mode) + 'K' + + +class AnsiCodes(object): + def __init__(self): + # the subclasses declare class attributes which are numbers. + # Upon instantiation we define instance attributes, which are the same + # as the class attributes but wrapped with the ANSI escape sequence + for name in dir(self): + if not name.startswith('_'): + value = getattr(self, name) + setattr(self, name, code_to_chars(value)) + + +class AnsiCursor(object): + def UP(self, n=1): + return CSI + str(n) + 'A' + + def DOWN(self, n=1): + return CSI + str(n) + 'B' + + def FORWARD(self, n=1): + return CSI + str(n) + 'C' + + def BACK(self, n=1): + return CSI + str(n) + 'D' + + def POS(self, x=1, y=1): + return CSI + str(y) + ';' + str(x) + 'H' + + +class AnsiFore(AnsiCodes): + BLACK = 30 + RED = 31 + GREEN = 32 + YELLOW = 33 + BLUE = 34 + MAGENTA = 35 + CYAN = 36 + WHITE = 37 + RESET = 39 + + # These are fairly well supported, but not part of the standard. + LIGHTBLACK_EX = 90 + LIGHTRED_EX = 91 + LIGHTGREEN_EX = 92 + LIGHTYELLOW_EX = 93 + LIGHTBLUE_EX = 94 + LIGHTMAGENTA_EX = 95 + LIGHTCYAN_EX = 96 + LIGHTWHITE_EX = 97 + + +class AnsiBack(AnsiCodes): + BLACK = 40 + RED = 41 + GREEN = 42 + YELLOW = 43 + BLUE = 44 + MAGENTA = 45 + CYAN = 46 + WHITE = 47 + RESET = 49 + + # These are fairly well supported, but not part of the standard. + LIGHTBLACK_EX = 100 + LIGHTRED_EX = 101 + LIGHTGREEN_EX = 102 + LIGHTYELLOW_EX = 103 + LIGHTBLUE_EX = 104 + LIGHTMAGENTA_EX = 105 + LIGHTCYAN_EX = 106 + LIGHTWHITE_EX = 107 + + +class AnsiStyle(AnsiCodes): + BRIGHT = 1 + DIM = 2 + NORMAL = 22 + RESET_ALL = 0 + + +Fore = AnsiFore() +Back = AnsiBack() +Style = AnsiStyle() +Cursor = AnsiCursor() diff --git a/source/nodes/utils/comfy_notification.py b/source/nodes/utils/comfy_notification.py new file mode 100644 index 0000000..e279e0b --- /dev/null +++ b/source/nodes/utils/comfy_notification.py @@ -0,0 +1,47 @@ +# Copyright (c) 2025 Salvador E. Tropea +# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial +# License: GPLv3 +# Project: ComfyUI-ImageMisc +# +# ComfyUI Toast API messages +# Original code from Gemini 2.5 Pro, which was really outdated +# Took ideas from Easy Use nodes and looking at ComfyUI code +import logging +from typing import Optional +# ComfyUI imports +try: + from server import PromptServer + with_comfy = True +except Exception: + with_comfy = False +# Local imports +from .misc import NODES_NAME + +logger = logging.getLogger(f"{NODES_NAME}.comfy_notification") + + +def send_toast_notification(message: str, summary: str = "Warning", severity: str = "warn", sid: Optional[str] = None): + """ + Sends a toast notification event to the ComfyUI client. + + Args: + 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. + """ + if not with_comfy: + return + try: + PromptServer.instance.send_sync( + "set-imagemisc-toast", # This is our custom event name + { + 'message': message, + 'summary': summary, + 'severity': severity + }, + sid + ) + except Exception as e: + logger.error(f"when trying to use ComfyUI PromptServer: {e}") diff --git a/source/nodes/utils/downloader.py b/source/nodes/utils/downloader.py new file mode 100644 index 0000000..67e413c --- /dev/null +++ b/source/nodes/utils/downloader.py @@ -0,0 +1,206 @@ +# Copyright (c) 2025 Salvador E. Tropea +# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial +# License: GPLv3 +# Project: ComfyUI-AudioSeparation, ComfyUI-AudioBatch and ComfyUI-ImageMisc +# +# Model downloader w/TQDM and ComfyUI progress +# Original code from Gemini 2.5 Pro +import logging +import os +# Requests is better than the core Python urllib, and is a really common package +# But we don't really need it. Lets make it optional: +try: + import requests + with_requests = True +except Exception: + with_requests = False +import urllib +from tqdm import tqdm +# ComfyUI imports +try: + import comfy.utils + with_comfy = True +except Exception: + with_comfy = False +# Local imports +from .misc import NODES_NAME + +logger = logging.getLogger(f"{NODES_NAME}.downloader") + + +def download_model_requests(url: str, save_dir: str, file_name: str): + """ + Downloads a file from a URL with progress bars for both console and ComfyUI. + + Args: + 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. + """ + full_path = os.path.join(save_dir, file_name) + + # Ensure the save directory exists + os.makedirs(save_dir, exist_ok=True) + try: + # Use a streaming request to handle large files and get content length + with requests.get(url, stream=True, timeout=10) as r: + r.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx) + + # Get total file size from headers + total_size_in_bytes = int(r.headers.get('content-length', 0)) + block_size = 1024 # 1 Kibibyte + + # --- Setup Progress Bars --- + # Console progress bar using tqdm + progress_bar_console = tqdm( + total=total_size_in_bytes, + unit='iB', + unit_scale=True, + desc=f"Downloading {file_name}" + ) + + # ComfyUI progress bar + progress_bar_ui = comfy.utils.ProgressBar(total_size_in_bytes) if with_comfy else None + + # --- Download Loop --- + downloaded_size = 0 + with open(full_path, 'wb') as f: + for chunk in r.iter_content(chunk_size=block_size): + if chunk: # filter out keep-alive new chunks + chunk_size = len(chunk) + + # Update console progress bar + progress_bar_console.update(chunk_size) + + # Update ComfyUI progress bar + downloaded_size += chunk_size + if progress_bar_ui: + progress_bar_ui.update(chunk_size) # ProgressBar takes absolute value, but update is incremental + + # Write chunk to file + f.write(chunk) + + # --- Cleanup --- + progress_bar_console.close() + + # Final check to see if download was complete + if total_size_in_bytes != 0 and progress_bar_console.n != total_size_in_bytes: + logger.error("Download failed: Size mismatch.") + # Optional: remove partial file + # os.remove(full_path) + raise IOError(f"Download failed for {file_name}. Expected {total_size_in_bytes} but got " + f"{progress_bar_console.n}") + + return full_path + + except requests.exceptions.RequestException as e: + logger.error(f"Network error while downloading {file_name}: {e}") + # Clean up partial file if it exists + if os.path.exists(full_path): + try: + os.remove(full_path) + except OSError: + pass + raise + except Exception as e: + logger.error(f"An error occurred during download: {e}") + if os.path.exists(full_path): + try: + os.remove(full_path) + except OSError: + pass + raise + + +# A simple version implemented using the Python urllib +class Downloader: + def __init__(self, model_path, model_name): + self.model_path = model_path + self.model_name = model_name + self.model_full_name = os.path.join(self.model_path, self.model_name) + # Ensure the directory for the model_path exists before __init__ if used elsewhere + # or create it at the start of download_model + + # A TQDM helper class for urlretrieve reporthook + # This is a common pattern for this use case. + class TqdmUpTo(tqdm): + """ + Provides `update_to(block_num, block_size, total_size)` + and updates the TQDM bar. + """ + def __init__(self, unit, unit_scale, unit_divisor, miniters, desc): + super().__init__(unit=unit, unit_scale=unit_scale, unit_divisor=unit_divisor, miniters=miniters, desc=desc) + self.ui_bar = None + self.total = None + + def update_to(self, block_num=1, block_size=1, total_size=None): + """ + block_num : int, optional + Number of blocks transferred so far [default: 1]. + block_size : int, optional + Size of each block (in tqdm units) [default: 1]. + total_size : int, optional + Total size (in tqdm units). If [default: None] remains unchanged. + """ + if total_size is not None and self.total is None: + self.total = total_size + # ComfyUI progress bar + if self.ui_bar is None and with_comfy: + self.ui_bar = comfy.utils.ProgressBar(total_size) + # self.update() will take the *difference* from the last call. + # So we pass the number of new blocks * block_size. + # Since block_num is cumulative, we calculate the new amount. + chunk_size = block_num * block_size - self.n + self.update(chunk_size) # self.n is current progress + if self.ui_bar: + self.ui_bar.update(chunk_size) # ProgressBar takes absolute value, but update is incremental + + def download_model(self, url: str): + try: + # Ensure the directory exists + # Use or '.' for current dir if dirname is empty + os.makedirs(self.model_path or '.', exist_ok=True) + + # Get filename for tqdm description + filename = self.model_name + + # Use TqdmUpTo as a context manager + with self.TqdmUpTo(unit='iB', unit_scale=True, unit_divisor=1024, miniters=1, + desc=f"Downloading {filename}") as t: + # urlretrieve(url, filename=None, reporthook=None, data=None) + # reporthook is called with (block_num, block_size, total_size) + urllib.request.urlretrieve(url, self.model_full_name, reporthook=t.update_to) + # The 'with' statement ensures t.close() is called. + + return filename + + except urllib.error.URLError as e: # More specific exception for network issues + # Clean up partially downloaded file if an error occurs + if os.path.exists(self.model_full_name): + os.remove(self.model_full_name) + raise Exception(f"An error occurred while downloading the model (URL Error): {e.reason} from {url}") + + except Exception as e: + # Clean up partially downloaded file if an error occurs + if os.path.exists(self.model_full_name): + os.remove(self.model_full_name) + raise Exception(f"An unexpected error occurred while downloading the model: {e}") + + +def download_model_urllib(url: str, save_dir: str, file_name: str): + return Downloader(save_dir, file_name).download_model(url) + + +def download_model(url: str, save_dir: str, file_name: str, force_urllib: bool = False, kind: str = "model"): + logger.info(f"Downloading {kind}: {file_name}") + logger.info(f"Source URL: {url}") + full_name = os.path.join(save_dir, file_name) + logger.info(f"Destination: {full_name}") + + if with_requests and not force_urllib: + download_model_requests(url, save_dir, file_name) + else: + download_model_urllib(url, save_dir, file_name) + + logger.info(f"Successfully downloaded {full_name}") + return full_name diff --git a/source/nodes/utils/logger.py b/source/nodes/utils/logger.py new file mode 100644 index 0000000..63d659d --- /dev/null +++ b/source/nodes/utils/logger.py @@ -0,0 +1,136 @@ +# Copyright (c) 2025 Salvador E. Tropea +# Copyright (c) 2025 Instituto Nacional de Tecnologïa Industrial +# License: GPL-3.0 +# Project: ComfyUI-ImageMisc +from __future__ import annotations # Good practice +import logging +import os +import sys +from typing import Any, Callable +from .misc import NODES_NAME, NODES_DEBUG_VAR +from .comfy_notification import send_toast_notification + + +# 1. Initialize variables with the `Any` type. +# This tells mypy not to make assumptions about their specific class. +Fore: Any +Back: Any +Style: Any + +# 2. Perform the runtime import logic as before. +try: + from colorama import init as colorama_init, Fore, Back, Style + colorama_init() +except ImportError: + # If colorama is not available, import our fallback. + # mypy will now allow this assignment because the variables were declared as Any. + from .ansi import Fore, Back, Style + + +class CustomFormatter(logging.Formatter): + """Logging Formatter to add colors""" + + def __init__(self): + super(logging.Formatter, self).__init__() + white = Fore.WHITE + Style.BRIGHT + yellow = Fore.YELLOW + Style.BRIGHT + red = Fore.RED + Style.BRIGHT + red_alarm = Fore.RED + Back.WHITE + Style.BRIGHT + cyan = Fore.CYAN + Style.BRIGHT + reset = Style.RESET_ALL + # format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s " + # "(%(filename)s:%(lineno)d)" + format = f"[{NODES_NAME} %(levelname)s] %(message)s (%(name)s - %(filename)s:%(lineno)d)" + format_simple = f"[{NODES_NAME}] %(message)s" + + self.FORMATS = { + logging.DEBUG: cyan + format + reset, + logging.INFO: white + format_simple + reset, + logging.WARNING: yellow + format + reset, + logging.ERROR: red + format + reset, + logging.CRITICAL: red_alarm + format + reset + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + +def on_log_error_or_warning(record: logging.LogRecord) -> None: + """ + This function is called whenever a log with level WARNING or higher is emitted. + The 'record' object contains all information about the log event. + """ + if record.levelno == logging.WARNING: + summary = "Warning" + severity = "warn" + else: + summary = "Error" + severity = "error" + send_toast_notification(record.getMessage(), summary=summary, severity=severity) + + +class WarningAndErrorFilter(logging.Filter): + """ + A custom log filter that intercepts logs of a certain level. + """ + def __init__(self, callback: Callable, level: int = logging.WARNING): + """ + Initializes the filter. + + Args: + callback: The function to call when a log record meets the level criteria. + level: The minimum level to trigger the callback. + """ + super().__init__() + self._callback = callback + self._level = level + + def filter(self, record: logging.LogRecord) -> bool: + """ + This method is called for every log record. + """ + # Check if the log level is WARNING or higher + if record.levelno >= self._level: + self._callback(record) + + # Always return True to ensure the log is always processed + # by the handlers after this filter. + return True + + +# Create a new logger +logger = logging.getLogger(NODES_NAME) +logger.propagate = False + +# Add the custom filter to the logger. +logger.addFilter(WarningAndErrorFilter(callback=on_log_error_or_warning)) + +# Add handler if we don't have one. +if not logger.handlers: + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(CustomFormatter()) + logger.addHandler(handler) + +# ###################### +# Logger setup +# ###################### +# 1. Determine the ComfyUI global log level (influenced by --verbose) +main_logger = logger +comfy_root_logger = logging.getLogger('comfy') +effective_comfy_level = logging.getLogger().getEffectiveLevel() +# 2. Check our custom environment variable for more verbosity +try: + nodes_debug_env = int(os.environ.get(NODES_DEBUG_VAR, "0")) +except ValueError: + nodes_debug_env = 0 +# 3. Set node's logger level +if nodes_debug_env: + main_logger.setLevel(logging.DEBUG - (nodes_debug_env - 1)) + final_level_str = f"DEBUG (due to {NODES_DEBUG_VAR}={nodes_debug_env})" +else: + main_logger.setLevel(effective_comfy_level) + final_level_str = logging.getLevelName(effective_comfy_level) + " (matching ComfyUI global)" +_initial_setup_logger = logging.getLogger(NODES_NAME + ".setup") # A temporary logger for this message +_initial_setup_logger.debug(f"{NODES_NAME} logger level set to: {final_level_str}") diff --git a/source/nodes/utils/misc.py b/source/nodes/utils/misc.py new file mode 100644 index 0000000..9fcc976 --- /dev/null +++ b/source/nodes/utils/misc.py @@ -0,0 +1,6 @@ +# Copyright (c) 2025 Salvador E. Tropea +# Copyright (c) 2025 Instituto Nacional de Tecnologïa Industrial +# License: GPL-3.0 +# Project: ComfyUI-ImageMisc +NODES_NAME = "ImageMisc" +NODES_DEBUG_VAR = NODES_NAME.upper() + "_NODES_DEBUG"