4 Commits
Author SHA1 Message Date
AngelBottomless (sleepy) 0cb60bc8aa I think this is something env dependent 2026-01-21 07:40:43 +00:00
AngelBottomless (sleepy) 1da10d7a31 fix 2026-01-21 07:28:53 +00:00
AngelBottomless (sleepy) a39013912d enable auto install 2026-01-21 07:18:56 +00:00
AngelBottomless (sleepy) 835e382cd9 add two nodes 2026-01-21 07:03:24 +00:00
6 changed files with 248 additions and 74 deletions
+2 -2
View File
@@ -12,5 +12,5 @@ Run from the repo root:
## Notes ## Notes
- Auto-install is opt-in via `COMFYUI_LOGICUTILS_AUTO_INSTALL=1`. - Auto-install runs when loaded in ComfyUI. To disable it, set `COMFYUI_LOGICUTILS_SKIP_INSTALL=1`.
- To force-disable the install hook, set `COMFYUI_LOGICUTILS_SKIP_INSTALL=1`. - To force CPU/GPU tagger dependency selection, set `COMFYUI_LOGICUTILS_IMGUTILS_VARIANT=cpu` or `COMFYUI_LOGICUTILS_IMGUTILS_VARIANT=gpu`.
+37 -20
View File
@@ -32,45 +32,62 @@ 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"] pip_install = [sys.executable, '-s', '-m', 'pip', 'install', "-U", "--default-timeout=1000"]
else: else:
pip_install = [sys.executable, '-m', 'pip', 'install', "-U"] pip_install = [sys.executable, '-m', 'pip', 'install', "-U", "--default-timeout=1000"]
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:
# dghs-imgutils currently pins numpy<2, which typically won't have wheels for print("imgutils not found, installing...")
# the latest Python releases right away (e.g. Python 3.13 in ComfyUI portable). for candidate in _imgutils_install_candidates():
if sys.version_info >= (3, 13): print(f"Trying to install {candidate}...")
print( run_installation(candidate)
"Skipping auto-install of dghs-imgutils on Python >= 3.13 " try:
"(tagger nodes will be disabled unless installed manually)." from imgutils.tagging import get_wd14_tags # noqa: F401
) break
else: except Exception:
run_installation("dghs-imgutils[gpu]") continue
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:
+99
View File
@@ -1,6 +1,8 @@
import base64 import base64
import json import json
import math import math
import random
from pathlib import Path
import numpy as np import numpy as np
import torch import torch
@@ -356,6 +358,35 @@ class SaveTextCustomNode:
return {"ui": {"texts": results}, "outputs": {"images": file.rstrip(".txt")}} return {"ui": {"texts": results}, "outputs": {"images": file.rstrip(".txt")}}
@fundamental_node
class CommaRejoinNode:
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 @fundamental_node
class DumpTextJsonlNode: class DumpTextJsonlNode:
""" """
@@ -1300,6 +1331,74 @@ class ImageFromURLNode:
} }
@fundamental_node
class RandomImageFromFolderNode:
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 @fundamental_node
class Base64EncodeNode: class Base64EncodeNode:
FUNCTION = "base64_encode" FUNCTION = "base64_encode"
+2
View File
@@ -20,6 +20,8 @@ _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
+1 -2
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.8.0" version = "1.7.3"
license = "MIT" license = "MIT"
[project.urls] [project.urls]
@@ -13,4 +13,3 @@ PublisherId = "angelbottomless"
DisplayName = "ComfyUI-LogicUtils" DisplayName = "ComfyUI-LogicUtils"
Icon = "" Icon = ""
+57
View File
@@ -0,0 +1,57 @@
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})