2 Commits
Author SHA1 Message Date
AngelBottomless f77c699543 Bump version to 1.8.0 in pyproject.toml 2026-01-21 13:57:43 +09:00
AngelBottomless 06658072ff Merge pull request #24 from aria1th/test-fixes
fix issues
2026-01-21 13:57:25 +09:00
6 changed files with 74 additions and 248 deletions
+2 -2
View File
@@ -12,5 +12,5 @@ Run from the repo root:
## Notes ## Notes
- Auto-install runs when loaded in ComfyUI. To disable it, set `COMFYUI_LOGICUTILS_SKIP_INSTALL=1`. - Auto-install is opt-in via `COMFYUI_LOGICUTILS_AUTO_INSTALL=1`.
- To force CPU/GPU tagger dependency selection, set `COMFYUI_LOGICUTILS_IMGUTILS_VARIANT=cpu` or `COMFYUI_LOGICUTILS_IMGUTILS_VARIANT=gpu`. - To force-disable the install hook, set `COMFYUI_LOGICUTILS_SKIP_INSTALL=1`.
+20 -37
View File
@@ -32,62 +32,45 @@ def process_wrap(cmd_str, cwd=None, handler=None):
return process.wait() return process.wait()
if "python_embeded" in sys.executable or "python_embedded" in sys.executable: #standalone python version if "python_embeded" in sys.executable or "python_embedded" in sys.executable: #standalone python version
pip_install = [sys.executable, '-s', '-m', 'pip', 'install', "-U", "--default-timeout=1000"] pip_install = [sys.executable, '-s', '-m', 'pip', 'install', "-U"]
else: else:
pip_install = [sys.executable, '-m', 'pip', 'install', "-U", "--default-timeout=1000"] pip_install = [sys.executable, '-m', 'pip', 'install', "-U"]
def _imgutils_install_candidates() -> list[str]:
"""
Decide which dghs-imgutils package variant to install first.
- `dghs-imgutils[gpu]` includes optional GPU-related dependencies.
- If GPU detection fails, default to CPU variant first.
"""
variant = os.environ.get("COMFYUI_LOGICUTILS_IMGUTILS_VARIANT", "").strip().lower()
if variant in {"gpu", "cuda"}:
return ["dghs-imgutils[gpu]", "dghs-imgutils"]
if variant in {"cpu"}:
return ["dghs-imgutils", "dghs-imgutils[gpu]"]
# auto
try:
import torch
if getattr(torch, "cuda", None) is not None and torch.cuda.is_available():
return ["dghs-imgutils[gpu]", "dghs-imgutils"]
except Exception:
pass
return ["dghs-imgutils", "dghs-imgutils[gpu]"]
def initialization(): def initialization():
auto_install = os.environ.get("COMFYUI_LOGICUTILS_AUTO_INSTALL", "").strip().lower() in {
"1",
"true",
"yes",
}
if not auto_install:
return
try: try:
import piexif import piexif
except Exception: except Exception:
print("piexif not found, installing...")
run_installation("piexif") run_installation("piexif")
try: try:
import chardet import chardet
except Exception: except Exception:
print("chardet not found, installing...")
run_installation("chardet") run_installation("chardet")
try: try:
from imgutils.tagging import get_wd14_tags from imgutils.tagging import get_wd14_tags
except Exception: except Exception:
print("imgutils not found, installing...") # dghs-imgutils currently pins numpy<2, which typically won't have wheels for
for candidate in _imgutils_install_candidates(): # the latest Python releases right away (e.g. Python 3.13 in ComfyUI portable).
print(f"Trying to install {candidate}...") if sys.version_info >= (3, 13):
run_installation(candidate) print(
try: "Skipping auto-install of dghs-imgutils on Python >= 3.13 "
from imgutils.tagging import get_wd14_tags # noqa: F401 "(tagger nodes will be disabled unless installed manually)."
break )
except Exception: else:
continue run_installation("dghs-imgutils[gpu]")
try: try:
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
except Exception: except Exception:
print("pycryptodome not found, installing...")
run_installation("pycryptodome") run_installation("pycryptodome")
def run_installation(pkg_name: str): def run_installation(pkg_name: str):
print(f"Installing {pkg_name}...") print(f"Installing {pkg_name}...")
try: try:
+22 -121
View File
@@ -1,10 +1,8 @@
import base64 import base64
import json import json
import math import math
import random import numpy as np
from pathlib import Path import torch
import numpy as np
import torch
try: try:
import piexif.helper import piexif.helper
@@ -313,7 +311,7 @@ class SaveImageCustomNode:
@fundamental_node @fundamental_node
class SaveTextCustomNode: class SaveTextCustomNode:
def __init__(self): def __init__(self):
self.output_dir = folder_paths.get_output_directory() self.output_dir = folder_paths.get_output_directory()
self.type = "output" self.type = "output"
@@ -355,40 +353,11 @@ class SaveTextCustomNode:
f.write(text) f.write(text)
results.append({"filename": file, "subfolder": subfolder, "type": self.type}) results.append({"filename": file, "subfolder": subfolder, "type": self.type})
return {"ui": {"texts": results}, "outputs": {"images": file.rstrip(".txt")}} return {"ui": {"texts": results}, "outputs": {"images": file.rstrip(".txt")}}
@fundamental_node @fundamental_node
class CommaRejoinNode: class DumpTextJsonlNode:
FUNCTION = "comma_rejoin"
RETURN_TYPES = ("STRING",)
CATEGORY = "text"
custom_name = "Comma Rejoin"
@staticmethod
def comma_rejoin(text, split_separator=",", join_separator=", "):
# Split (by comma or given separator), strip items, then join using the
# join separator verbatim (e.g. ", ").
parts = str(text).split(split_separator) if split_separator else [str(text)]
stripped = [part.strip() for part in parts]
stripped = [part for part in stripped if part != ""]
return (join_separator.join(stripped),)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": ""}),
},
"optional": {
"split_separator": ("STRING", {"default": ","}),
"join_separator": ("STRING", {"default": ", "}),
},
}
@fundamental_node
class DumpTextJsonlNode:
""" """
Appends text to a JSONL file (one JSON object per line). Appends text to a JSONL file (one JSON object per line).
Each line will have the structure: { "<keyname>": "<text_item>" } Each line will have the structure: { "<keyname>": "<text_item>" }
@@ -1306,7 +1275,7 @@ class Base64DecodeNode:
@fundamental_node @fundamental_node
class ImageFromURLNode: class ImageFromURLNode:
FUNCTION = "url_download" FUNCTION = "url_download"
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE",)
CATEGORY = "image" CATEGORY = "image"
@@ -1323,84 +1292,16 @@ class ImageFromURLNode:
return (image,) return (image,)
@classmethod @classmethod
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"url": ("STRING",), "url": ("STRING",),
} }
} }
@fundamental_node @fundamental_node
class RandomImageFromFolderNode: class Base64EncodeNode:
FUNCTION = "random_image_from_folder"
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("image", "path")
CATEGORY = "image"
custom_name = "Random Image From Folder"
@classmethod
def IS_CHANGED(cls, *args, **kwargs):
return float("nan")
@staticmethod
@PILHandlingHodes.output_wrapper
def random_image_from_folder(folder, extension, recursive, seed=0):
folder = str(folder).strip()
if not folder:
raise ValueError("folder must be a non-empty path")
folder_path = Path(folder).expanduser()
if not folder_path.is_dir():
raise ValueError(f"folder is not a directory: {folder_path}")
ext = str(extension).strip().lower().lstrip(".")
if ext == "all":
extensions = {".jpg", ".jpeg", ".png", ".webp"}
elif ext == "jpg":
extensions = {".jpg", ".jpeg"}
else:
extensions = {f".{ext}"}
candidates = []
if recursive:
for suffix in extensions:
candidates.extend(folder_path.rglob(f"*{suffix}"))
else:
for suffix in extensions:
candidates.extend(folder_path.glob(f"*{suffix}"))
candidates = [p for p in candidates if p.is_file()]
if not candidates:
raise FileNotFoundError(
f"No images found in '{folder_path}' with extension '{extension}'"
)
candidates = sorted(set(candidates), key=lambda p: str(p).lower())
rng = random.Random(seed)
chosen = candidates[rng.randrange(len(candidates))]
with Image.open(chosen) as img:
img = img.convert("RGB")
return (img, str(chosen))
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"folder": ("STRING", {"default": ""}),
"extension": (["all", "jpg", "png", "webp"], {"default": "all"}),
"recursive": ("BOOLEAN", {"default": True}),
},
"optional": {
"seed": ("INT", {"default": 0, "min": 0, "max": 2**63 - 1}),
},
}
@fundamental_node
class Base64EncodeNode:
FUNCTION = "base64_encode" FUNCTION = "base64_encode"
RETURN_TYPES = ("STRING",) RETURN_TYPES = ("STRING",)
CATEGORY = "image" CATEGORY = "image"
+28 -30
View File
@@ -20,9 +20,7 @@ _SKIP_INSTALL = os.environ.get("COMFYUI_LOGICUTILS_SKIP_INSTALL", "").strip().lo
if _IN_COMFYUI and not _SKIP_INSTALL: if _IN_COMFYUI and not _SKIP_INSTALL:
initialization() initialization()
else:
print("Skipping ComfyUI-LogicUtils installation.")
from .logic_gates import CLASS_MAPPINGS as LogicMapping, CLASS_NAMES as LogicNames from .logic_gates import CLASS_MAPPINGS as LogicMapping, CLASS_NAMES as LogicNames
from .randomness import CLASS_MAPPINGS as RandomMapping, CLASS_NAMES as RandomNames from .randomness import CLASS_MAPPINGS as RandomMapping, CLASS_NAMES as RandomNames
from .conversion import CLASS_MAPPINGS as ConversionMapping, CLASS_NAMES as ConversionNames from .conversion import CLASS_MAPPINGS as ConversionMapping, CLASS_NAMES as ConversionNames
@@ -34,33 +32,33 @@ else:
IONames = {} IONames = {}
from .auxilary import CLASS_MAPPINGS as AuxilaryMapping, CLASS_NAMES as AuxilaryNames from .auxilary import CLASS_MAPPINGS as AuxilaryMapping, CLASS_NAMES as AuxilaryNames
from .external import CLASS_MAPPINGS as ExternalMapping, CLASS_NAMES as ExternalNames from .external import CLASS_MAPPINGS as ExternalMapping, CLASS_NAMES as ExternalNames
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
} }
NODE_CLASS_MAPPINGS.update(IOMapping) NODE_CLASS_MAPPINGS.update(IOMapping)
NODE_CLASS_MAPPINGS.update(LogicMapping) NODE_CLASS_MAPPINGS.update(LogicMapping)
NODE_CLASS_MAPPINGS.update(RandomMapping) NODE_CLASS_MAPPINGS.update(RandomMapping)
NODE_CLASS_MAPPINGS.update(ConversionMapping) NODE_CLASS_MAPPINGS.update(ConversionMapping)
NODE_CLASS_MAPPINGS.update(MathMapping) NODE_CLASS_MAPPINGS.update(MathMapping)
NODE_CLASS_MAPPINGS.update(ExternalMapping) NODE_CLASS_MAPPINGS.update(ExternalMapping)
NODE_CLASS_MAPPINGS.update(AuxilaryMapping) NODE_CLASS_MAPPINGS.update(AuxilaryMapping)
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
} }
NODE_DISPLAY_NAME_MAPPINGS.update(IONames) NODE_DISPLAY_NAME_MAPPINGS.update(IONames)
NODE_DISPLAY_NAME_MAPPINGS.update(LogicNames) NODE_DISPLAY_NAME_MAPPINGS.update(LogicNames)
NODE_DISPLAY_NAME_MAPPINGS.update(RandomNames) NODE_DISPLAY_NAME_MAPPINGS.update(RandomNames)
NODE_DISPLAY_NAME_MAPPINGS.update(ConversionNames) NODE_DISPLAY_NAME_MAPPINGS.update(ConversionNames)
NODE_DISPLAY_NAME_MAPPINGS.update(MathNames) NODE_DISPLAY_NAME_MAPPINGS.update(MathNames)
NODE_DISPLAY_NAME_MAPPINGS.update(ExternalNames) NODE_DISPLAY_NAME_MAPPINGS.update(ExternalNames)
NODE_DISPLAY_NAME_MAPPINGS.update(AuxilaryNames) NODE_DISPLAY_NAME_MAPPINGS.update(AuxilaryNames)
try: try:
from .pystructure import CLASS_MAPPINGS as PyStructureMapping, CLASS_NAMES as PyStructureNames from .pystructure import CLASS_MAPPINGS as PyStructureMapping, CLASS_NAMES as PyStructureNames
NODE_CLASS_MAPPINGS.update(PyStructureMapping) NODE_CLASS_MAPPINGS.update(PyStructureMapping)
+2 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-logicutils" name = "comfyui-logicutils"
description = "Logical Utils (compare, string, boolean operations) for ComfyUI" description = "Logical Utils (compare, string, boolean operations) for ComfyUI"
version = "1.7.3" version = "1.8.0"
license = "MIT" license = "MIT"
[project.urls] [project.urls]
@@ -13,3 +13,4 @@ PublisherId = "angelbottomless"
DisplayName = "ComfyUI-LogicUtils" DisplayName = "ComfyUI-LogicUtils"
Icon = "" Icon = ""
-57
View File
@@ -1,57 +0,0 @@
import tempfile
import unittest
from pathlib import Path
import torch
from PIL import Image
from import_utils import import_local
class TestIoNodesExtras(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.io_node = import_local("io_node")
def test_comma_rejoin_defaults(self):
Node = self.io_node.CLASS_MAPPINGS["CommaRejoinNode"]
self.assertEqual(Node.comma_rejoin("a,b , c"), ("a, b, c",))
self.assertEqual(Node.comma_rejoin(" a , b ,c "), ("a, b, c",))
def test_comma_rejoin_custom_separators(self):
Node = self.io_node.CLASS_MAPPINGS["CommaRejoinNode"]
self.assertEqual(
Node.comma_rejoin("a| b |c", split_separator="|", join_separator="| "),
("a| b| c",),
)
def test_random_image_from_folder(self):
Node = self.io_node.CLASS_MAPPINGS["RandomImageFromFolderNode"]
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
sub = root / "sub"
sub.mkdir()
img1 = root / "a.png"
img2 = root / "b.jpg"
img3 = sub / "c.png"
Image.new("RGB", (8, 6), (10, 20, 30)).save(img1)
Image.new("RGB", (8, 6), (40, 50, 60)).save(img2)
Image.new("RGB", (8, 6), (70, 80, 90)).save(img3)
# Non-recursive should only pick from root
tensor, path = Node.random_image_from_folder(
str(root), "all", False, seed=0
)
self.assertTrue(torch.is_tensor(tensor))
self.assertEqual(tuple(tensor.shape), (1, 6, 8, 3))
self.assertIn(Path(path), {img1, img2})
# Recursive should include subfolder images
tensor, path = Node.random_image_from_folder(str(root), "png", True, seed=0)
self.assertTrue(torch.is_tensor(tensor))
self.assertEqual(tuple(tensor.shape), (1, 6, 8, 3))
self.assertIn(Path(path), {img1, img3})