Compare commits
4
Commits
main
...
test-fixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cb60bc8aa | ||
|
|
1da10d7a31 | ||
|
|
a39013912d | ||
|
|
835e382cd9 |
@@ -12,5 +12,5 @@ Run from the repo root:
|
||||
|
||||
## Notes
|
||||
|
||||
- Auto-install is opt-in via `COMFYUI_LOGICUTILS_AUTO_INSTALL=1`.
|
||||
- To force-disable the install hook, set `COMFYUI_LOGICUTILS_SKIP_INSTALL=1`.
|
||||
- Auto-install runs when loaded in ComfyUI. To disable it, 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()
|
||||
|
||||
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:
|
||||
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():
|
||||
auto_install = os.environ.get("COMFYUI_LOGICUTILS_AUTO_INSTALL", "").strip().lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
}
|
||||
if not auto_install:
|
||||
return
|
||||
|
||||
try:
|
||||
import piexif
|
||||
except Exception:
|
||||
print("piexif not found, installing...")
|
||||
run_installation("piexif")
|
||||
try:
|
||||
import chardet
|
||||
except Exception:
|
||||
print("chardet not found, installing...")
|
||||
run_installation("chardet")
|
||||
try:
|
||||
from imgutils.tagging import get_wd14_tags
|
||||
except Exception:
|
||||
# dghs-imgutils currently pins numpy<2, which typically won't have wheels for
|
||||
# the latest Python releases right away (e.g. Python 3.13 in ComfyUI portable).
|
||||
if sys.version_info >= (3, 13):
|
||||
print(
|
||||
"Skipping auto-install of dghs-imgutils on Python >= 3.13 "
|
||||
"(tagger nodes will be disabled unless installed manually)."
|
||||
)
|
||||
else:
|
||||
run_installation("dghs-imgutils[gpu]")
|
||||
print("imgutils not found, installing...")
|
||||
for candidate in _imgutils_install_candidates():
|
||||
print(f"Trying to install {candidate}...")
|
||||
run_installation(candidate)
|
||||
try:
|
||||
from imgutils.tagging import get_wd14_tags # noqa: F401
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
from Crypto.PublicKey import RSA
|
||||
except Exception:
|
||||
print("pycryptodome not found, installing...")
|
||||
run_installation("pycryptodome")
|
||||
|
||||
|
||||
def run_installation(pkg_name: str):
|
||||
print(f"Installing {pkg_name}...")
|
||||
try:
|
||||
|
||||
+121
-22
@@ -1,8 +1,10 @@
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
import numpy as np
|
||||
import torch
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
try:
|
||||
import piexif.helper
|
||||
@@ -311,7 +313,7 @@ class SaveImageCustomNode:
|
||||
|
||||
|
||||
@fundamental_node
|
||||
class SaveTextCustomNode:
|
||||
class SaveTextCustomNode:
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "output"
|
||||
@@ -353,11 +355,40 @@ class SaveTextCustomNode:
|
||||
f.write(text)
|
||||
results.append({"filename": file, "subfolder": subfolder, "type": self.type})
|
||||
|
||||
return {"ui": {"texts": results}, "outputs": {"images": file.rstrip(".txt")}}
|
||||
|
||||
|
||||
@fundamental_node
|
||||
class DumpTextJsonlNode:
|
||||
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
|
||||
class DumpTextJsonlNode:
|
||||
"""
|
||||
Appends text to a JSONL file (one JSON object per line).
|
||||
Each line will have the structure: { "<keyname>": "<text_item>" }
|
||||
@@ -1275,7 +1306,7 @@ class Base64DecodeNode:
|
||||
|
||||
|
||||
@fundamental_node
|
||||
class ImageFromURLNode:
|
||||
class ImageFromURLNode:
|
||||
FUNCTION = "url_download"
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
CATEGORY = "image"
|
||||
@@ -1292,16 +1323,84 @@ class ImageFromURLNode:
|
||||
return (image,)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"url": ("STRING",),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@fundamental_node
|
||||
class Base64EncodeNode:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"url": ("STRING",),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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
|
||||
class Base64EncodeNode:
|
||||
FUNCTION = "base64_encode"
|
||||
RETURN_TYPES = ("STRING",)
|
||||
CATEGORY = "image"
|
||||
|
||||
@@ -20,7 +20,9 @@ _SKIP_INSTALL = os.environ.get("COMFYUI_LOGICUTILS_SKIP_INSTALL", "").strip().lo
|
||||
|
||||
if _IN_COMFYUI and not _SKIP_INSTALL:
|
||||
initialization()
|
||||
|
||||
else:
|
||||
print("Skipping ComfyUI-LogicUtils installation.")
|
||||
|
||||
from .logic_gates import CLASS_MAPPINGS as LogicMapping, CLASS_NAMES as LogicNames
|
||||
from .randomness import CLASS_MAPPINGS as RandomMapping, CLASS_NAMES as RandomNames
|
||||
from .conversion import CLASS_MAPPINGS as ConversionMapping, CLASS_NAMES as ConversionNames
|
||||
@@ -32,33 +34,33 @@ else:
|
||||
IONames = {}
|
||||
from .auxilary import CLASS_MAPPINGS as AuxilaryMapping, CLASS_NAMES as AuxilaryNames
|
||||
from .external import CLASS_MAPPINGS as ExternalMapping, CLASS_NAMES as ExternalNames
|
||||
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
}
|
||||
NODE_CLASS_MAPPINGS.update(IOMapping)
|
||||
NODE_CLASS_MAPPINGS.update(LogicMapping)
|
||||
NODE_CLASS_MAPPINGS.update(RandomMapping)
|
||||
NODE_CLASS_MAPPINGS.update(ConversionMapping)
|
||||
NODE_CLASS_MAPPINGS.update(MathMapping)
|
||||
NODE_CLASS_MAPPINGS.update(ExternalMapping)
|
||||
NODE_CLASS_MAPPINGS.update(AuxilaryMapping)
|
||||
|
||||
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(IONames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(LogicNames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(RandomNames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(ConversionNames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(MathNames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(ExternalNames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(AuxilaryNames)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
}
|
||||
NODE_CLASS_MAPPINGS.update(IOMapping)
|
||||
NODE_CLASS_MAPPINGS.update(LogicMapping)
|
||||
NODE_CLASS_MAPPINGS.update(RandomMapping)
|
||||
NODE_CLASS_MAPPINGS.update(ConversionMapping)
|
||||
NODE_CLASS_MAPPINGS.update(MathMapping)
|
||||
NODE_CLASS_MAPPINGS.update(ExternalMapping)
|
||||
NODE_CLASS_MAPPINGS.update(AuxilaryMapping)
|
||||
|
||||
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(IONames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(LogicNames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(RandomNames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(ConversionNames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(MathNames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(ExternalNames)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(AuxilaryNames)
|
||||
|
||||
|
||||
try:
|
||||
from .pystructure import CLASS_MAPPINGS as PyStructureMapping, CLASS_NAMES as PyStructureNames
|
||||
NODE_CLASS_MAPPINGS.update(PyStructureMapping)
|
||||
|
||||
@@ -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