[Nodes][Added] ImageDownloader

Not audio, but it is so similar to AudioDownload that I put it here
This commit is contained in:
Salvador E. Tropea
2025-07-20 10:42:31 -03:00
parent c27b0c7e55
commit b905731fbb
4 changed files with 132 additions and 4 deletions
+24
View File
@@ -24,6 +24,8 @@ workflows, especially when dealing with multiple audio inputs or outputs.
- [15. Audio Normalize (Peak)](#15-audio-normalize-peak)
- [16. Audio Apply Batched Gain](#16-audio-apply-batched-gain)
- [17. Audio Download and Load](#17-audio-download-and-load)
- [✨ Extra nodes](#-extra-nodes)
- [1. Image Download and Load](#1-image-download-and-load)
- [🚀 Installation](#-installation)
- [📦 Dependencies](#-dependencies)
- [🖼️ Examples](#️-examples)
@@ -303,6 +305,28 @@ workflows, especially when dealing with multiple audio inputs or outputs.
- **Player**: This node uses an `AUDIO_UI` widget to allow playing the downloaded song.
## ✨ Extra nodes
These nodes are just complements for the other nodes. I.e. they share a lot of code.
### 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 *Audio Batch*, or just do it manually:
+2 -1
View File
@@ -14,7 +14,8 @@ NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
for name, obj in inspect.getmembers(nodes_audio):
if not inspect.isclass(obj) or not hasattr(obj, "INPUT_TYPES"):
# 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
+104 -1
View File
@@ -21,6 +21,12 @@ 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 = "audio"
@@ -1096,7 +1102,7 @@ class AudioDownload:
send_toast_notification(f"Downloading `{filename}`", "Download")
try:
download_model(url=download_url, save_dir=save_dir, file_name=filename)
download_model(url=download_url, save_dir=save_dir, file_name=filename, kind="audio")
except Exception as e:
logger.error(f"Download failed for {download_url}: {e}", exc_info=True)
raise # Re-raise to stop the workflow and show the error
@@ -1143,3 +1149,100 @@ class AudioDownload:
except Exception as e:
logger.error(f"Failed to load or process audio file '{local_filepath}': {e}", exc_info=True)
raise IOError(f"Could not load or process the audio file: {filename}. It may be missing or corrupt.") from e
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 = "image" + "/" + 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
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
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
+2 -2
View File
@@ -191,8 +191,8 @@ 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):
logger.info(f"Downloading model: {file_name}")
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}")