Compare commits
4
Commits
main
..
test-fixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cb60bc8aa | ||
|
|
1da10d7a31 | ||
|
|
a39013912d | ||
|
|
835e382cd9 |
@@ -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
@@ -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:
|
||||||
|
|||||||
+121
-22
@@ -1,8 +1,10 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import numpy as np
|
import random
|
||||||
import torch
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import piexif.helper
|
import piexif.helper
|
||||||
@@ -311,7 +313,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"
|
||||||
@@ -353,11 +355,40 @@ 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 DumpTextJsonlNode:
|
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
|
||||||
|
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>" }
|
||||||
@@ -1275,7 +1306,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"
|
||||||
@@ -1292,16 +1323,84 @@ 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 Base64EncodeNode:
|
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
|
||||||
|
class Base64EncodeNode:
|
||||||
FUNCTION = "base64_encode"
|
FUNCTION = "base64_encode"
|
||||||
RETURN_TYPES = ("STRING",)
|
RETURN_TYPES = ("STRING",)
|
||||||
CATEGORY = "image"
|
CATEGORY = "image"
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ _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
|
||||||
@@ -32,33 +34,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)
|
||||||
|
|||||||
+1
-2
@@ -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 = ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|
||||||
Reference in New Issue
Block a user