[Added] The image downloader node
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
custom: ['https://www.paypal.com/donate/?hosted_button_id=K2T86GDTTMRPL']
|
||||
@@ -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 }}
|
||||
+198
@@ -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
|
||||
.*~
|
||||
@@ -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
|
||||
@@ -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.
|
||||
+28
@@ -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']
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -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}
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user