Compare commits
2
Commits
test-fixes
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f77c699543 | ||
|
|
06658072ff |
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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 = ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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})
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user