Delete tests
Browse files- tests/README.md +0 -29
- tests/__init__.py +0 -0
- tests/compare/conftest.py +0 -41
- tests/compare/test_quality.py +0 -195
- tests/conftest.py +0 -36
- tests/inference/__init__.py +0 -0
- tests/inference/graphs/default_graph_sdxl1_0.json +0 -144
- tests/inference/test_async_nodes.py +0 -410
- tests/inference/test_execution.py +0 -587
- tests/inference/test_inference.py +0 -237
- tests/inference/testing_nodes/testing-pack/__init__.py +0 -26
- tests/inference/testing_nodes/testing-pack/async_test_nodes.py +0 -343
- tests/inference/testing_nodes/testing-pack/conditions.py +0 -194
- tests/inference/testing_nodes/testing-pack/flow_control.py +0 -173
- tests/inference/testing_nodes/testing-pack/specific_tests.py +0 -498
- tests/inference/testing_nodes/testing-pack/stubs.py +0 -129
- tests/inference/testing_nodes/testing-pack/tools.py +0 -53
- tests/inference/testing_nodes/testing-pack/util.py +0 -364
tests/README.md
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
# Automated Testing
|
| 2 |
-
|
| 3 |
-
## Running tests locally
|
| 4 |
-
|
| 5 |
-
Additional requirements for running tests:
|
| 6 |
-
```
|
| 7 |
-
pip install pytest
|
| 8 |
-
pip install websocket-client==1.6.1
|
| 9 |
-
opencv-python==4.6.0.66
|
| 10 |
-
scikit-image==0.21.0
|
| 11 |
-
```
|
| 12 |
-
Run inference tests:
|
| 13 |
-
```
|
| 14 |
-
pytest tests/inference
|
| 15 |
-
```
|
| 16 |
-
|
| 17 |
-
## Quality regression test
|
| 18 |
-
Compares images in 2 directories to ensure they are the same
|
| 19 |
-
|
| 20 |
-
1) Run an inference test to save a directory of "ground truth" images
|
| 21 |
-
```
|
| 22 |
-
pytest tests/inference --output_dir tests/inference/baseline
|
| 23 |
-
```
|
| 24 |
-
2) Make code edits
|
| 25 |
-
|
| 26 |
-
3) Run inference and quality comparison tests
|
| 27 |
-
```
|
| 28 |
-
pytest
|
| 29 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/__init__.py
DELETED
|
File without changes
|
tests/compare/conftest.py
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import pytest
|
| 3 |
-
|
| 4 |
-
# Command line arguments for pytest
|
| 5 |
-
def pytest_addoption(parser):
|
| 6 |
-
parser.addoption('--baseline_dir', action="store", default='tests/inference/baseline', help='Directory for ground-truth images')
|
| 7 |
-
parser.addoption('--test_dir', action="store", default='tests/inference/samples', help='Directory for images to test')
|
| 8 |
-
parser.addoption('--metrics_file', action="store", default='tests/metrics.md', help='Output file for metrics')
|
| 9 |
-
parser.addoption('--img_output_dir', action="store", default='tests/compare/samples', help='Output directory for diff metric images')
|
| 10 |
-
|
| 11 |
-
# This initializes args at the beginning of the test session
|
| 12 |
-
@pytest.fixture(scope="session", autouse=True)
|
| 13 |
-
def args_pytest(pytestconfig):
|
| 14 |
-
args = {}
|
| 15 |
-
args['baseline_dir'] = pytestconfig.getoption('baseline_dir')
|
| 16 |
-
args['test_dir'] = pytestconfig.getoption('test_dir')
|
| 17 |
-
args['metrics_file'] = pytestconfig.getoption('metrics_file')
|
| 18 |
-
args['img_output_dir'] = pytestconfig.getoption('img_output_dir')
|
| 19 |
-
|
| 20 |
-
# Initialize metrics file
|
| 21 |
-
with open(args['metrics_file'], 'a') as f:
|
| 22 |
-
# if file is empty, write header
|
| 23 |
-
if os.stat(args['metrics_file']).st_size == 0:
|
| 24 |
-
f.write("| date | run | file | status | value | \n")
|
| 25 |
-
f.write("| --- | --- | --- | --- | --- | \n")
|
| 26 |
-
|
| 27 |
-
return args
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def gather_file_basenames(directory: str):
|
| 31 |
-
files = []
|
| 32 |
-
for file in os.listdir(directory):
|
| 33 |
-
if file.endswith(".png"):
|
| 34 |
-
files.append(file)
|
| 35 |
-
return files
|
| 36 |
-
|
| 37 |
-
# Creates the list of baseline file names to use as a fixture
|
| 38 |
-
def pytest_generate_tests(metafunc):
|
| 39 |
-
if "baseline_fname" in metafunc.fixturenames:
|
| 40 |
-
baseline_fnames = gather_file_basenames(metafunc.config.getoption("baseline_dir"))
|
| 41 |
-
metafunc.parametrize("baseline_fname", baseline_fnames)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/compare/test_quality.py
DELETED
|
@@ -1,195 +0,0 @@
|
|
| 1 |
-
import datetime
|
| 2 |
-
import numpy as np
|
| 3 |
-
import os
|
| 4 |
-
from PIL import Image
|
| 5 |
-
import pytest
|
| 6 |
-
from pytest import fixture
|
| 7 |
-
from typing import Tuple, List
|
| 8 |
-
|
| 9 |
-
from cv2 import imread, cvtColor, COLOR_BGR2RGB
|
| 10 |
-
from skimage.metrics import structural_similarity as ssim
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
"""
|
| 14 |
-
This test suite compares images in 2 directories by file name
|
| 15 |
-
The directories are specified by the command line arguments --baseline_dir and --test_dir
|
| 16 |
-
|
| 17 |
-
"""
|
| 18 |
-
# ssim: Structural Similarity Index
|
| 19 |
-
# Returns a tuple of (ssim, diff_image)
|
| 20 |
-
def ssim_score(img0: np.ndarray, img1: np.ndarray) -> Tuple[float, np.ndarray]:
|
| 21 |
-
score, diff = ssim(img0, img1, channel_axis=-1, full=True)
|
| 22 |
-
# rescale the difference image to 0-255 range
|
| 23 |
-
diff = (diff * 255).astype("uint8")
|
| 24 |
-
return score, diff
|
| 25 |
-
|
| 26 |
-
# Metrics must return a tuple of (score, diff_image)
|
| 27 |
-
METRICS = {"ssim": ssim_score}
|
| 28 |
-
METRICS_PASS_THRESHOLD = {"ssim": 0.95}
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
class TestCompareImageMetrics:
|
| 32 |
-
@fixture(scope="class")
|
| 33 |
-
def test_file_names(self, args_pytest):
|
| 34 |
-
test_dir = args_pytest['test_dir']
|
| 35 |
-
fnames = self.gather_file_basenames(test_dir)
|
| 36 |
-
yield fnames
|
| 37 |
-
del fnames
|
| 38 |
-
|
| 39 |
-
@fixture(scope="class", autouse=True)
|
| 40 |
-
def teardown(self, args_pytest):
|
| 41 |
-
yield
|
| 42 |
-
# Runs after all tests are complete
|
| 43 |
-
# Aggregate output files into a grid of images
|
| 44 |
-
baseline_dir = args_pytest['baseline_dir']
|
| 45 |
-
test_dir = args_pytest['test_dir']
|
| 46 |
-
img_output_dir = args_pytest['img_output_dir']
|
| 47 |
-
metrics_file = args_pytest['metrics_file']
|
| 48 |
-
|
| 49 |
-
grid_dir = os.path.join(img_output_dir, "grid")
|
| 50 |
-
os.makedirs(grid_dir, exist_ok=True)
|
| 51 |
-
|
| 52 |
-
for metric_dir in METRICS.keys():
|
| 53 |
-
metric_path = os.path.join(img_output_dir, metric_dir)
|
| 54 |
-
for file in os.listdir(metric_path):
|
| 55 |
-
if file.endswith(".png"):
|
| 56 |
-
score = self.lookup_score_from_fname(file, metrics_file)
|
| 57 |
-
image_file_list = []
|
| 58 |
-
image_file_list.append([
|
| 59 |
-
os.path.join(baseline_dir, file),
|
| 60 |
-
os.path.join(test_dir, file),
|
| 61 |
-
os.path.join(metric_path, file)
|
| 62 |
-
])
|
| 63 |
-
# Create grid
|
| 64 |
-
image_list = [[Image.open(file) for file in files] for files in image_file_list]
|
| 65 |
-
grid = self.image_grid(image_list)
|
| 66 |
-
grid.save(os.path.join(grid_dir, f"{metric_dir}_{score:.3f}_{file}"))
|
| 67 |
-
|
| 68 |
-
# Tests run for each baseline file name
|
| 69 |
-
@fixture()
|
| 70 |
-
def fname(self, baseline_fname):
|
| 71 |
-
yield baseline_fname
|
| 72 |
-
del baseline_fname
|
| 73 |
-
|
| 74 |
-
def test_directories_not_empty(self, args_pytest):
|
| 75 |
-
baseline_dir = args_pytest['baseline_dir']
|
| 76 |
-
test_dir = args_pytest['test_dir']
|
| 77 |
-
assert len(os.listdir(baseline_dir)) != 0, f"Baseline directory {baseline_dir} is empty"
|
| 78 |
-
assert len(os.listdir(test_dir)) != 0, f"Test directory {test_dir} is empty"
|
| 79 |
-
|
| 80 |
-
def test_dir_has_all_matching_metadata(self, fname, test_file_names, args_pytest):
|
| 81 |
-
# Check that all files in baseline_dir have a file in test_dir with matching metadata
|
| 82 |
-
baseline_file_path = os.path.join(args_pytest['baseline_dir'], fname)
|
| 83 |
-
file_paths = [os.path.join(args_pytest['test_dir'], f) for f in test_file_names]
|
| 84 |
-
file_match = self.find_file_match(baseline_file_path, file_paths)
|
| 85 |
-
assert file_match is not None, f"Could not find a file in {args_pytest['test_dir']} with matching metadata to {baseline_file_path}"
|
| 86 |
-
|
| 87 |
-
# For a baseline image file, finds the corresponding file name in test_dir and
|
| 88 |
-
# compares the images using the metrics in METRICS
|
| 89 |
-
@pytest.mark.parametrize("metric", METRICS.keys())
|
| 90 |
-
def test_pipeline_compare(
|
| 91 |
-
self,
|
| 92 |
-
args_pytest,
|
| 93 |
-
fname,
|
| 94 |
-
test_file_names,
|
| 95 |
-
metric,
|
| 96 |
-
):
|
| 97 |
-
baseline_dir = args_pytest['baseline_dir']
|
| 98 |
-
test_dir = args_pytest['test_dir']
|
| 99 |
-
metrics_output_file = args_pytest['metrics_file']
|
| 100 |
-
img_output_dir = args_pytest['img_output_dir']
|
| 101 |
-
|
| 102 |
-
baseline_file_path = os.path.join(baseline_dir, fname)
|
| 103 |
-
|
| 104 |
-
# Find file match
|
| 105 |
-
file_paths = [os.path.join(test_dir, f) for f in test_file_names]
|
| 106 |
-
test_file = self.find_file_match(baseline_file_path, file_paths)
|
| 107 |
-
|
| 108 |
-
# Run metrics
|
| 109 |
-
sample_baseline = self.read_img(baseline_file_path)
|
| 110 |
-
sample_secondary = self.read_img(test_file)
|
| 111 |
-
|
| 112 |
-
score, metric_img = METRICS[metric](sample_baseline, sample_secondary)
|
| 113 |
-
metric_status = score > METRICS_PASS_THRESHOLD[metric]
|
| 114 |
-
|
| 115 |
-
# Save metric values
|
| 116 |
-
with open(metrics_output_file, 'a') as f:
|
| 117 |
-
run_info = os.path.splitext(fname)[0]
|
| 118 |
-
metric_status_str = "PASS ✅" if metric_status else "FAIL ❌"
|
| 119 |
-
date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 120 |
-
f.write(f"| {date_str} | {run_info} | {metric} | {metric_status_str} | {score} | \n")
|
| 121 |
-
|
| 122 |
-
# Save metric image
|
| 123 |
-
metric_img_dir = os.path.join(img_output_dir, metric)
|
| 124 |
-
os.makedirs(metric_img_dir, exist_ok=True)
|
| 125 |
-
output_filename = f'{fname}'
|
| 126 |
-
Image.fromarray(metric_img).save(os.path.join(metric_img_dir, output_filename))
|
| 127 |
-
|
| 128 |
-
assert score > METRICS_PASS_THRESHOLD[metric]
|
| 129 |
-
|
| 130 |
-
def read_img(self, filename: str) -> np.ndarray:
|
| 131 |
-
cvImg = imread(filename)
|
| 132 |
-
cvImg = cvtColor(cvImg, COLOR_BGR2RGB)
|
| 133 |
-
return cvImg
|
| 134 |
-
|
| 135 |
-
def image_grid(self, img_list: list[list[Image.Image]]):
|
| 136 |
-
# imgs is a 2D list of images
|
| 137 |
-
# Assumes the input images are a rectangular grid of equal sized images
|
| 138 |
-
rows = len(img_list)
|
| 139 |
-
cols = len(img_list[0])
|
| 140 |
-
|
| 141 |
-
w, h = img_list[0][0].size
|
| 142 |
-
grid = Image.new('RGB', size=(cols*w, rows*h))
|
| 143 |
-
|
| 144 |
-
for i, row in enumerate(img_list):
|
| 145 |
-
for j, img in enumerate(row):
|
| 146 |
-
grid.paste(img, box=(j*w, i*h))
|
| 147 |
-
return grid
|
| 148 |
-
|
| 149 |
-
def lookup_score_from_fname(self,
|
| 150 |
-
fname: str,
|
| 151 |
-
metrics_output_file: str
|
| 152 |
-
) -> float:
|
| 153 |
-
fname_basestr = os.path.splitext(fname)[0]
|
| 154 |
-
with open(metrics_output_file, 'r') as f:
|
| 155 |
-
for line in f:
|
| 156 |
-
if fname_basestr in line:
|
| 157 |
-
score = float(line.split('|')[5])
|
| 158 |
-
return score
|
| 159 |
-
raise ValueError(f"Could not find score for {fname} in {metrics_output_file}")
|
| 160 |
-
|
| 161 |
-
def gather_file_basenames(self, directory: str):
|
| 162 |
-
files = []
|
| 163 |
-
for file in os.listdir(directory):
|
| 164 |
-
if file.endswith(".png"):
|
| 165 |
-
files.append(file)
|
| 166 |
-
return files
|
| 167 |
-
|
| 168 |
-
def read_file_prompt(self, fname:str) -> str:
|
| 169 |
-
# Read prompt from image file metadata
|
| 170 |
-
img = Image.open(fname)
|
| 171 |
-
img.load()
|
| 172 |
-
return img.info['prompt']
|
| 173 |
-
|
| 174 |
-
def find_file_match(self, baseline_file: str, file_paths: List[str]):
|
| 175 |
-
# Find a file in file_paths with matching metadata to baseline_file
|
| 176 |
-
baseline_prompt = self.read_file_prompt(baseline_file)
|
| 177 |
-
|
| 178 |
-
# Do not match empty prompts
|
| 179 |
-
if baseline_prompt is None or baseline_prompt == "":
|
| 180 |
-
return None
|
| 181 |
-
|
| 182 |
-
# Find file match
|
| 183 |
-
# Reorder test_file_names so that the file with matching name is first
|
| 184 |
-
# This is an optimization because matching file names are more likely
|
| 185 |
-
# to have matching metadata if they were generated with the same script
|
| 186 |
-
basename = os.path.basename(baseline_file)
|
| 187 |
-
file_path_basenames = [os.path.basename(f) for f in file_paths]
|
| 188 |
-
if basename in file_path_basenames:
|
| 189 |
-
match_index = file_path_basenames.index(basename)
|
| 190 |
-
file_paths.insert(0, file_paths.pop(match_index))
|
| 191 |
-
|
| 192 |
-
for f in file_paths:
|
| 193 |
-
test_file_prompt = self.read_file_prompt(f)
|
| 194 |
-
if baseline_prompt == test_file_prompt:
|
| 195 |
-
return f
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/conftest.py
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import pytest
|
| 3 |
-
|
| 4 |
-
# Command line arguments for pytest
|
| 5 |
-
def pytest_addoption(parser):
|
| 6 |
-
parser.addoption('--output_dir', action="store", default='tests/inference/samples', help='Output directory for generated images')
|
| 7 |
-
parser.addoption("--listen", type=str, default="127.0.0.1", metavar="IP", nargs="?", const="0.0.0.0", help="Specify the IP address to listen on (default: 127.0.0.1). If --listen is provided without an argument, it defaults to 0.0.0.0. (listens on all)")
|
| 8 |
-
parser.addoption("--port", type=int, default=8188, help="Set the listen port.")
|
| 9 |
-
|
| 10 |
-
# This initializes args at the beginning of the test session
|
| 11 |
-
@pytest.fixture(scope="session", autouse=True)
|
| 12 |
-
def args_pytest(pytestconfig):
|
| 13 |
-
args = {}
|
| 14 |
-
args['output_dir'] = pytestconfig.getoption('output_dir')
|
| 15 |
-
args['listen'] = pytestconfig.getoption('listen')
|
| 16 |
-
args['port'] = pytestconfig.getoption('port')
|
| 17 |
-
|
| 18 |
-
os.makedirs(args['output_dir'], exist_ok=True)
|
| 19 |
-
|
| 20 |
-
return args
|
| 21 |
-
|
| 22 |
-
def pytest_collection_modifyitems(items):
|
| 23 |
-
# Modifies items so tests run in the correct order
|
| 24 |
-
|
| 25 |
-
LAST_TESTS = ['test_quality']
|
| 26 |
-
|
| 27 |
-
# Move the last items to the end
|
| 28 |
-
last_items = []
|
| 29 |
-
for test_name in LAST_TESTS:
|
| 30 |
-
for item in items.copy():
|
| 31 |
-
print(item.module.__name__, item) # noqa: T201
|
| 32 |
-
if item.module.__name__ == test_name:
|
| 33 |
-
last_items.append(item)
|
| 34 |
-
items.remove(item)
|
| 35 |
-
|
| 36 |
-
items.extend(last_items)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/__init__.py
DELETED
|
File without changes
|
tests/inference/graphs/default_graph_sdxl1_0.json
DELETED
|
@@ -1,144 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"4": {
|
| 3 |
-
"inputs": {
|
| 4 |
-
"ckpt_name": "sd_xl_base_1.0.safetensors"
|
| 5 |
-
},
|
| 6 |
-
"class_type": "CheckpointLoaderSimple"
|
| 7 |
-
},
|
| 8 |
-
"5": {
|
| 9 |
-
"inputs": {
|
| 10 |
-
"width": 1024,
|
| 11 |
-
"height": 1024,
|
| 12 |
-
"batch_size": 1
|
| 13 |
-
},
|
| 14 |
-
"class_type": "EmptyLatentImage"
|
| 15 |
-
},
|
| 16 |
-
"6": {
|
| 17 |
-
"inputs": {
|
| 18 |
-
"text": "a photo of a cat",
|
| 19 |
-
"clip": [
|
| 20 |
-
"4",
|
| 21 |
-
1
|
| 22 |
-
]
|
| 23 |
-
},
|
| 24 |
-
"class_type": "CLIPTextEncode"
|
| 25 |
-
},
|
| 26 |
-
"10": {
|
| 27 |
-
"inputs": {
|
| 28 |
-
"add_noise": "enable",
|
| 29 |
-
"noise_seed": 42,
|
| 30 |
-
"steps": 20,
|
| 31 |
-
"cfg": 7.5,
|
| 32 |
-
"sampler_name": "euler",
|
| 33 |
-
"scheduler": "normal",
|
| 34 |
-
"start_at_step": 0,
|
| 35 |
-
"end_at_step": 32,
|
| 36 |
-
"return_with_leftover_noise": "enable",
|
| 37 |
-
"model": [
|
| 38 |
-
"4",
|
| 39 |
-
0
|
| 40 |
-
],
|
| 41 |
-
"positive": [
|
| 42 |
-
"6",
|
| 43 |
-
0
|
| 44 |
-
],
|
| 45 |
-
"negative": [
|
| 46 |
-
"15",
|
| 47 |
-
0
|
| 48 |
-
],
|
| 49 |
-
"latent_image": [
|
| 50 |
-
"5",
|
| 51 |
-
0
|
| 52 |
-
]
|
| 53 |
-
},
|
| 54 |
-
"class_type": "KSamplerAdvanced"
|
| 55 |
-
},
|
| 56 |
-
"12": {
|
| 57 |
-
"inputs": {
|
| 58 |
-
"samples": [
|
| 59 |
-
"14",
|
| 60 |
-
0
|
| 61 |
-
],
|
| 62 |
-
"vae": [
|
| 63 |
-
"4",
|
| 64 |
-
2
|
| 65 |
-
]
|
| 66 |
-
},
|
| 67 |
-
"class_type": "VAEDecode"
|
| 68 |
-
},
|
| 69 |
-
"13": {
|
| 70 |
-
"inputs": {
|
| 71 |
-
"filename_prefix": "test_inference",
|
| 72 |
-
"images": [
|
| 73 |
-
"12",
|
| 74 |
-
0
|
| 75 |
-
]
|
| 76 |
-
},
|
| 77 |
-
"class_type": "SaveImage"
|
| 78 |
-
},
|
| 79 |
-
"14": {
|
| 80 |
-
"inputs": {
|
| 81 |
-
"add_noise": "disable",
|
| 82 |
-
"noise_seed": 42,
|
| 83 |
-
"steps": 20,
|
| 84 |
-
"cfg": 7.5,
|
| 85 |
-
"sampler_name": "euler",
|
| 86 |
-
"scheduler": "normal",
|
| 87 |
-
"start_at_step": 32,
|
| 88 |
-
"end_at_step": 10000,
|
| 89 |
-
"return_with_leftover_noise": "disable",
|
| 90 |
-
"model": [
|
| 91 |
-
"16",
|
| 92 |
-
0
|
| 93 |
-
],
|
| 94 |
-
"positive": [
|
| 95 |
-
"17",
|
| 96 |
-
0
|
| 97 |
-
],
|
| 98 |
-
"negative": [
|
| 99 |
-
"20",
|
| 100 |
-
0
|
| 101 |
-
],
|
| 102 |
-
"latent_image": [
|
| 103 |
-
"10",
|
| 104 |
-
0
|
| 105 |
-
]
|
| 106 |
-
},
|
| 107 |
-
"class_type": "KSamplerAdvanced"
|
| 108 |
-
},
|
| 109 |
-
"15": {
|
| 110 |
-
"inputs": {
|
| 111 |
-
"conditioning": [
|
| 112 |
-
"6",
|
| 113 |
-
0
|
| 114 |
-
]
|
| 115 |
-
},
|
| 116 |
-
"class_type": "ConditioningZeroOut"
|
| 117 |
-
},
|
| 118 |
-
"16": {
|
| 119 |
-
"inputs": {
|
| 120 |
-
"ckpt_name": "sd_xl_refiner_1.0.safetensors"
|
| 121 |
-
},
|
| 122 |
-
"class_type": "CheckpointLoaderSimple"
|
| 123 |
-
},
|
| 124 |
-
"17": {
|
| 125 |
-
"inputs": {
|
| 126 |
-
"text": "a photo of a cat",
|
| 127 |
-
"clip": [
|
| 128 |
-
"16",
|
| 129 |
-
1
|
| 130 |
-
]
|
| 131 |
-
},
|
| 132 |
-
"class_type": "CLIPTextEncode"
|
| 133 |
-
},
|
| 134 |
-
"20": {
|
| 135 |
-
"inputs": {
|
| 136 |
-
"text": "",
|
| 137 |
-
"clip": [
|
| 138 |
-
"16",
|
| 139 |
-
1
|
| 140 |
-
]
|
| 141 |
-
},
|
| 142 |
-
"class_type": "CLIPTextEncode"
|
| 143 |
-
}
|
| 144 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/test_async_nodes.py
DELETED
|
@@ -1,410 +0,0 @@
|
|
| 1 |
-
import pytest
|
| 2 |
-
import time
|
| 3 |
-
import torch
|
| 4 |
-
import urllib.error
|
| 5 |
-
import numpy as np
|
| 6 |
-
import subprocess
|
| 7 |
-
|
| 8 |
-
from pytest import fixture
|
| 9 |
-
from comfy_execution.graph_utils import GraphBuilder
|
| 10 |
-
from tests.inference.test_execution import ComfyClient
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
@pytest.mark.execution
|
| 14 |
-
class TestAsyncNodes:
|
| 15 |
-
@fixture(scope="class", autouse=True, params=[
|
| 16 |
-
(False, 0),
|
| 17 |
-
(True, 0),
|
| 18 |
-
(True, 100),
|
| 19 |
-
])
|
| 20 |
-
def _server(self, args_pytest, request):
|
| 21 |
-
pargs = [
|
| 22 |
-
'python','main.py',
|
| 23 |
-
'--output-directory', args_pytest["output_dir"],
|
| 24 |
-
'--listen', args_pytest["listen"],
|
| 25 |
-
'--port', str(args_pytest["port"]),
|
| 26 |
-
'--extra-model-paths-config', 'tests/inference/extra_model_paths.yaml',
|
| 27 |
-
]
|
| 28 |
-
use_lru, lru_size = request.param
|
| 29 |
-
if use_lru:
|
| 30 |
-
pargs += ['--cache-lru', str(lru_size)]
|
| 31 |
-
# Running server with args: pargs
|
| 32 |
-
p = subprocess.Popen(pargs)
|
| 33 |
-
yield
|
| 34 |
-
p.kill()
|
| 35 |
-
torch.cuda.empty_cache()
|
| 36 |
-
|
| 37 |
-
@fixture(scope="class", autouse=True)
|
| 38 |
-
def shared_client(self, args_pytest, _server):
|
| 39 |
-
client = ComfyClient()
|
| 40 |
-
n_tries = 5
|
| 41 |
-
for i in range(n_tries):
|
| 42 |
-
time.sleep(4)
|
| 43 |
-
try:
|
| 44 |
-
client.connect(listen=args_pytest["listen"], port=args_pytest["port"])
|
| 45 |
-
except ConnectionRefusedError:
|
| 46 |
-
# Retrying...
|
| 47 |
-
pass
|
| 48 |
-
else:
|
| 49 |
-
break
|
| 50 |
-
yield client
|
| 51 |
-
del client
|
| 52 |
-
torch.cuda.empty_cache()
|
| 53 |
-
|
| 54 |
-
@fixture
|
| 55 |
-
def client(self, shared_client, request):
|
| 56 |
-
shared_client.set_test_name(f"async_nodes[{request.node.name}]")
|
| 57 |
-
yield shared_client
|
| 58 |
-
|
| 59 |
-
@fixture
|
| 60 |
-
def builder(self, request):
|
| 61 |
-
yield GraphBuilder(prefix=request.node.name)
|
| 62 |
-
|
| 63 |
-
# Happy Path Tests
|
| 64 |
-
|
| 65 |
-
def test_basic_async_execution(self, client: ComfyClient, builder: GraphBuilder):
|
| 66 |
-
"""Test that a basic async node executes correctly."""
|
| 67 |
-
g = builder
|
| 68 |
-
image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 69 |
-
sleep_node = g.node("TestSleep", value=image.out(0), seconds=0.1)
|
| 70 |
-
output = g.node("SaveImage", images=sleep_node.out(0))
|
| 71 |
-
|
| 72 |
-
result = client.run(g)
|
| 73 |
-
|
| 74 |
-
# Verify execution completed
|
| 75 |
-
assert result.did_run(sleep_node), "Async sleep node should have executed"
|
| 76 |
-
assert result.did_run(output), "Output node should have executed"
|
| 77 |
-
|
| 78 |
-
# Verify the image passed through correctly
|
| 79 |
-
result_images = result.get_images(output)
|
| 80 |
-
assert len(result_images) == 1, "Should have 1 image"
|
| 81 |
-
assert np.array(result_images[0]).min() == 0 and np.array(result_images[0]).max() == 0, "Image should be black"
|
| 82 |
-
|
| 83 |
-
def test_multiple_async_parallel_execution(self, client: ComfyClient, builder: GraphBuilder):
|
| 84 |
-
"""Test that multiple async nodes execute in parallel."""
|
| 85 |
-
g = builder
|
| 86 |
-
image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 87 |
-
|
| 88 |
-
# Create multiple async sleep nodes with different durations
|
| 89 |
-
sleep1 = g.node("TestSleep", value=image.out(0), seconds=0.3)
|
| 90 |
-
sleep2 = g.node("TestSleep", value=image.out(0), seconds=0.4)
|
| 91 |
-
sleep3 = g.node("TestSleep", value=image.out(0), seconds=0.5)
|
| 92 |
-
|
| 93 |
-
# Add outputs for each
|
| 94 |
-
_output1 = g.node("PreviewImage", images=sleep1.out(0))
|
| 95 |
-
_output2 = g.node("PreviewImage", images=sleep2.out(0))
|
| 96 |
-
_output3 = g.node("PreviewImage", images=sleep3.out(0))
|
| 97 |
-
|
| 98 |
-
start_time = time.time()
|
| 99 |
-
result = client.run(g)
|
| 100 |
-
elapsed_time = time.time() - start_time
|
| 101 |
-
|
| 102 |
-
# Should take ~0.5s (max duration) not 1.2s (sum of durations)
|
| 103 |
-
assert elapsed_time < 0.8, f"Parallel execution took {elapsed_time}s, expected < 0.8s"
|
| 104 |
-
|
| 105 |
-
# Verify all nodes executed
|
| 106 |
-
assert result.did_run(sleep1) and result.did_run(sleep2) and result.did_run(sleep3)
|
| 107 |
-
|
| 108 |
-
def test_async_with_dependencies(self, client: ComfyClient, builder: GraphBuilder):
|
| 109 |
-
"""Test async nodes with proper dependency handling."""
|
| 110 |
-
g = builder
|
| 111 |
-
image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 112 |
-
image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 113 |
-
|
| 114 |
-
# Chain of async operations
|
| 115 |
-
sleep1 = g.node("TestSleep", value=image1.out(0), seconds=0.2)
|
| 116 |
-
sleep2 = g.node("TestSleep", value=image2.out(0), seconds=0.2)
|
| 117 |
-
|
| 118 |
-
# Average depends on both async results
|
| 119 |
-
average = g.node("TestVariadicAverage", input1=sleep1.out(0), input2=sleep2.out(0))
|
| 120 |
-
output = g.node("SaveImage", images=average.out(0))
|
| 121 |
-
|
| 122 |
-
result = client.run(g)
|
| 123 |
-
|
| 124 |
-
# Verify execution order
|
| 125 |
-
assert result.did_run(sleep1) and result.did_run(sleep2)
|
| 126 |
-
assert result.did_run(average) and result.did_run(output)
|
| 127 |
-
|
| 128 |
-
# Verify averaged result
|
| 129 |
-
result_images = result.get_images(output)
|
| 130 |
-
avg_value = np.array(result_images[0]).mean()
|
| 131 |
-
assert abs(avg_value - 127.5) < 1, f"Average value {avg_value} should be ~127.5"
|
| 132 |
-
|
| 133 |
-
def test_async_validate_inputs(self, client: ComfyClient, builder: GraphBuilder):
|
| 134 |
-
"""Test async VALIDATE_INPUTS function."""
|
| 135 |
-
g = builder
|
| 136 |
-
# Create a test node with async validation
|
| 137 |
-
validation_node = g.node("TestAsyncValidation", value=5.0, threshold=10.0)
|
| 138 |
-
g.node("SaveImage", images=validation_node.out(0))
|
| 139 |
-
|
| 140 |
-
# Should pass validation
|
| 141 |
-
result = client.run(g)
|
| 142 |
-
assert result.did_run(validation_node)
|
| 143 |
-
|
| 144 |
-
# Test validation failure
|
| 145 |
-
validation_node.inputs['threshold'] = 3.0 # Will fail since value > threshold
|
| 146 |
-
with pytest.raises(urllib.error.HTTPError):
|
| 147 |
-
client.run(g)
|
| 148 |
-
|
| 149 |
-
def test_async_lazy_evaluation(self, client: ComfyClient, builder: GraphBuilder):
|
| 150 |
-
"""Test async nodes with lazy evaluation."""
|
| 151 |
-
g = builder
|
| 152 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 153 |
-
input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 154 |
-
mask = g.node("StubMask", value=0.0, height=512, width=512, batch_size=1)
|
| 155 |
-
|
| 156 |
-
# Create async nodes that will be evaluated lazily
|
| 157 |
-
sleep1 = g.node("TestSleep", value=input1.out(0), seconds=0.3)
|
| 158 |
-
sleep2 = g.node("TestSleep", value=input2.out(0), seconds=0.3)
|
| 159 |
-
|
| 160 |
-
# Use lazy mix that only needs sleep1 (mask=0.0)
|
| 161 |
-
lazy_mix = g.node("TestLazyMixImages", image1=sleep1.out(0), image2=sleep2.out(0), mask=mask.out(0))
|
| 162 |
-
g.node("SaveImage", images=lazy_mix.out(0))
|
| 163 |
-
|
| 164 |
-
start_time = time.time()
|
| 165 |
-
result = client.run(g)
|
| 166 |
-
elapsed_time = time.time() - start_time
|
| 167 |
-
|
| 168 |
-
# Should only execute sleep1, not sleep2
|
| 169 |
-
assert elapsed_time < 0.5, f"Should skip sleep2, took {elapsed_time}s"
|
| 170 |
-
assert result.did_run(sleep1), "Sleep1 should have executed"
|
| 171 |
-
assert not result.did_run(sleep2), "Sleep2 should have been skipped"
|
| 172 |
-
|
| 173 |
-
def test_async_check_lazy_status(self, client: ComfyClient, builder: GraphBuilder):
|
| 174 |
-
"""Test async check_lazy_status function."""
|
| 175 |
-
g = builder
|
| 176 |
-
# Create a node with async check_lazy_status
|
| 177 |
-
lazy_node = g.node("TestAsyncLazyCheck",
|
| 178 |
-
input1="value1",
|
| 179 |
-
input2="value2",
|
| 180 |
-
condition=True)
|
| 181 |
-
g.node("SaveImage", images=lazy_node.out(0))
|
| 182 |
-
|
| 183 |
-
result = client.run(g)
|
| 184 |
-
assert result.did_run(lazy_node)
|
| 185 |
-
|
| 186 |
-
# Error Handling Tests
|
| 187 |
-
|
| 188 |
-
def test_async_execution_error(self, client: ComfyClient, builder: GraphBuilder):
|
| 189 |
-
"""Test that async execution errors are properly handled."""
|
| 190 |
-
g = builder
|
| 191 |
-
image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 192 |
-
# Create an async node that will error
|
| 193 |
-
error_node = g.node("TestAsyncError", value=image.out(0), error_after=0.1)
|
| 194 |
-
g.node("SaveImage", images=error_node.out(0))
|
| 195 |
-
|
| 196 |
-
try:
|
| 197 |
-
client.run(g)
|
| 198 |
-
assert False, "Should have raised an error"
|
| 199 |
-
except Exception as e:
|
| 200 |
-
assert 'prompt_id' in e.args[0], f"Did not get proper error message: {e}"
|
| 201 |
-
assert e.args[0]['node_id'] == error_node.id, "Error should be from async error node"
|
| 202 |
-
|
| 203 |
-
def test_async_validation_error(self, client: ComfyClient, builder: GraphBuilder):
|
| 204 |
-
"""Test async validation error handling."""
|
| 205 |
-
g = builder
|
| 206 |
-
# Node with async validation that will fail
|
| 207 |
-
validation_node = g.node("TestAsyncValidationError", value=15.0, max_value=10.0)
|
| 208 |
-
g.node("SaveImage", images=validation_node.out(0))
|
| 209 |
-
|
| 210 |
-
with pytest.raises(urllib.error.HTTPError) as exc_info:
|
| 211 |
-
client.run(g)
|
| 212 |
-
# Verify it's a validation error
|
| 213 |
-
assert exc_info.value.code == 400
|
| 214 |
-
|
| 215 |
-
def test_async_timeout_handling(self, client: ComfyClient, builder: GraphBuilder):
|
| 216 |
-
"""Test handling of async operations that timeout."""
|
| 217 |
-
g = builder
|
| 218 |
-
image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 219 |
-
# Very long sleep that would timeout
|
| 220 |
-
timeout_node = g.node("TestAsyncTimeout", value=image.out(0), timeout=0.5, operation_time=2.0)
|
| 221 |
-
g.node("SaveImage", images=timeout_node.out(0))
|
| 222 |
-
|
| 223 |
-
try:
|
| 224 |
-
client.run(g)
|
| 225 |
-
assert False, "Should have raised a timeout error"
|
| 226 |
-
except Exception as e:
|
| 227 |
-
assert 'timeout' in str(e).lower(), f"Expected timeout error, got: {e}"
|
| 228 |
-
|
| 229 |
-
def test_concurrent_async_error_recovery(self, client: ComfyClient, builder: GraphBuilder):
|
| 230 |
-
"""Test that workflow can recover after async errors."""
|
| 231 |
-
g = builder
|
| 232 |
-
image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 233 |
-
|
| 234 |
-
# First run with error
|
| 235 |
-
error_node = g.node("TestAsyncError", value=image.out(0), error_after=0.1)
|
| 236 |
-
g.node("SaveImage", images=error_node.out(0))
|
| 237 |
-
|
| 238 |
-
try:
|
| 239 |
-
client.run(g)
|
| 240 |
-
except Exception:
|
| 241 |
-
pass # Expected
|
| 242 |
-
|
| 243 |
-
# Second run should succeed
|
| 244 |
-
g2 = GraphBuilder(prefix="recovery_test")
|
| 245 |
-
image2 = g2.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 246 |
-
sleep_node = g2.node("TestSleep", value=image2.out(0), seconds=0.1)
|
| 247 |
-
g2.node("SaveImage", images=sleep_node.out(0))
|
| 248 |
-
|
| 249 |
-
result = client.run(g2)
|
| 250 |
-
assert result.did_run(sleep_node), "Should be able to run after error"
|
| 251 |
-
|
| 252 |
-
def test_sync_error_during_async_execution(self, client: ComfyClient, builder: GraphBuilder):
|
| 253 |
-
"""Test handling when sync node errors while async node is executing."""
|
| 254 |
-
g = builder
|
| 255 |
-
image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 256 |
-
|
| 257 |
-
# Async node that takes time
|
| 258 |
-
sleep_node = g.node("TestSleep", value=image.out(0), seconds=0.5)
|
| 259 |
-
|
| 260 |
-
# Sync node that will error immediately
|
| 261 |
-
error_node = g.node("TestSyncError", value=image.out(0))
|
| 262 |
-
|
| 263 |
-
# Both feed into output
|
| 264 |
-
g.node("PreviewImage", images=sleep_node.out(0))
|
| 265 |
-
g.node("PreviewImage", images=error_node.out(0))
|
| 266 |
-
|
| 267 |
-
try:
|
| 268 |
-
client.run(g)
|
| 269 |
-
assert False, "Should have raised an error"
|
| 270 |
-
except Exception as e:
|
| 271 |
-
# Verify the sync error was caught even though async was running
|
| 272 |
-
assert 'prompt_id' in e.args[0]
|
| 273 |
-
|
| 274 |
-
# Edge Cases
|
| 275 |
-
|
| 276 |
-
def test_async_with_execution_blocker(self, client: ComfyClient, builder: GraphBuilder):
|
| 277 |
-
"""Test async nodes with execution blockers."""
|
| 278 |
-
g = builder
|
| 279 |
-
image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 280 |
-
image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 281 |
-
|
| 282 |
-
# Async sleep nodes
|
| 283 |
-
sleep1 = g.node("TestSleep", value=image1.out(0), seconds=0.2)
|
| 284 |
-
sleep2 = g.node("TestSleep", value=image2.out(0), seconds=0.2)
|
| 285 |
-
|
| 286 |
-
# Create list of images
|
| 287 |
-
image_list = g.node("TestMakeListNode", value1=sleep1.out(0), value2=sleep2.out(0))
|
| 288 |
-
|
| 289 |
-
# Create list of blocking conditions - [False, True] to block only the second item
|
| 290 |
-
int1 = g.node("StubInt", value=1)
|
| 291 |
-
int2 = g.node("StubInt", value=2)
|
| 292 |
-
block_list = g.node("TestMakeListNode", value1=int1.out(0), value2=int2.out(0))
|
| 293 |
-
|
| 294 |
-
# Compare each value against 2, so first is False (1 != 2) and second is True (2 == 2)
|
| 295 |
-
compare = g.node("TestIntConditions", a=block_list.out(0), b=2, operation="==")
|
| 296 |
-
|
| 297 |
-
# Block based on the comparison results
|
| 298 |
-
blocker = g.node("TestExecutionBlocker", input=image_list.out(0), block=compare.out(0), verbose=False)
|
| 299 |
-
|
| 300 |
-
output = g.node("PreviewImage", images=blocker.out(0))
|
| 301 |
-
|
| 302 |
-
result = client.run(g)
|
| 303 |
-
images = result.get_images(output)
|
| 304 |
-
assert len(images) == 1, "Should have blocked second image"
|
| 305 |
-
|
| 306 |
-
def test_async_caching_behavior(self, client: ComfyClient, builder: GraphBuilder):
|
| 307 |
-
"""Test that async nodes are properly cached."""
|
| 308 |
-
g = builder
|
| 309 |
-
image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 310 |
-
sleep_node = g.node("TestSleep", value=image.out(0), seconds=0.2)
|
| 311 |
-
g.node("SaveImage", images=sleep_node.out(0))
|
| 312 |
-
|
| 313 |
-
# First run
|
| 314 |
-
result1 = client.run(g)
|
| 315 |
-
assert result1.did_run(sleep_node), "Should run first time"
|
| 316 |
-
|
| 317 |
-
# Second run - should be cached
|
| 318 |
-
start_time = time.time()
|
| 319 |
-
result2 = client.run(g)
|
| 320 |
-
elapsed_time = time.time() - start_time
|
| 321 |
-
|
| 322 |
-
assert not result2.did_run(sleep_node), "Should be cached"
|
| 323 |
-
assert elapsed_time < 0.1, f"Cached run took {elapsed_time}s, should be instant"
|
| 324 |
-
|
| 325 |
-
def test_async_with_dynamic_prompts(self, client: ComfyClient, builder: GraphBuilder):
|
| 326 |
-
"""Test async nodes within dynamically generated prompts."""
|
| 327 |
-
g = builder
|
| 328 |
-
image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 329 |
-
image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 330 |
-
|
| 331 |
-
# Node that generates async nodes dynamically
|
| 332 |
-
dynamic_async = g.node("TestDynamicAsyncGeneration",
|
| 333 |
-
image1=image1.out(0),
|
| 334 |
-
image2=image2.out(0),
|
| 335 |
-
num_async_nodes=3,
|
| 336 |
-
sleep_duration=0.2)
|
| 337 |
-
g.node("SaveImage", images=dynamic_async.out(0))
|
| 338 |
-
|
| 339 |
-
start_time = time.time()
|
| 340 |
-
result = client.run(g)
|
| 341 |
-
elapsed_time = time.time() - start_time
|
| 342 |
-
|
| 343 |
-
# Should execute async nodes in parallel within dynamic prompt
|
| 344 |
-
assert elapsed_time < 0.5, f"Dynamic async execution took {elapsed_time}s"
|
| 345 |
-
assert result.did_run(dynamic_async)
|
| 346 |
-
|
| 347 |
-
def test_async_resource_cleanup(self, client: ComfyClient, builder: GraphBuilder):
|
| 348 |
-
"""Test that async resources are properly cleaned up."""
|
| 349 |
-
g = builder
|
| 350 |
-
image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 351 |
-
|
| 352 |
-
# Create multiple async nodes that use resources
|
| 353 |
-
resource_nodes = []
|
| 354 |
-
for i in range(5):
|
| 355 |
-
node = g.node("TestAsyncResourceUser",
|
| 356 |
-
value=image.out(0),
|
| 357 |
-
resource_id=f"resource_{i}",
|
| 358 |
-
duration=0.1)
|
| 359 |
-
resource_nodes.append(node)
|
| 360 |
-
g.node("PreviewImage", images=node.out(0))
|
| 361 |
-
|
| 362 |
-
result = client.run(g)
|
| 363 |
-
|
| 364 |
-
# Verify all nodes executed
|
| 365 |
-
for node in resource_nodes:
|
| 366 |
-
assert result.did_run(node)
|
| 367 |
-
|
| 368 |
-
# Run again to ensure resources were cleaned up
|
| 369 |
-
result2 = client.run(g)
|
| 370 |
-
# Should be cached but not error due to resource conflicts
|
| 371 |
-
for node in resource_nodes:
|
| 372 |
-
assert not result2.did_run(node), "Should be cached"
|
| 373 |
-
|
| 374 |
-
def test_async_cancellation(self, client: ComfyClient, builder: GraphBuilder):
|
| 375 |
-
"""Test cancellation of async operations."""
|
| 376 |
-
# This would require implementing cancellation in the client
|
| 377 |
-
# For now, we'll test that long-running async operations can be interrupted
|
| 378 |
-
pass # TODO: Implement when cancellation API is available
|
| 379 |
-
|
| 380 |
-
def test_mixed_sync_async_execution(self, client: ComfyClient, builder: GraphBuilder):
|
| 381 |
-
"""Test workflows with both sync and async nodes."""
|
| 382 |
-
g = builder
|
| 383 |
-
image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 384 |
-
image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 385 |
-
mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
|
| 386 |
-
|
| 387 |
-
# Mix of sync and async operations
|
| 388 |
-
# Sync: lazy mix images
|
| 389 |
-
sync_op1 = g.node("TestLazyMixImages", image1=image1.out(0), image2=image2.out(0), mask=mask.out(0))
|
| 390 |
-
# Async: sleep
|
| 391 |
-
async_op1 = g.node("TestSleep", value=sync_op1.out(0), seconds=0.2)
|
| 392 |
-
# Sync: custom validation
|
| 393 |
-
sync_op2 = g.node("TestCustomValidation1", input1=async_op1.out(0), input2=0.5)
|
| 394 |
-
# Async: sleep again
|
| 395 |
-
async_op2 = g.node("TestSleep", value=sync_op2.out(0), seconds=0.2)
|
| 396 |
-
|
| 397 |
-
output = g.node("SaveImage", images=async_op2.out(0))
|
| 398 |
-
|
| 399 |
-
result = client.run(g)
|
| 400 |
-
|
| 401 |
-
# Verify all nodes executed in correct order
|
| 402 |
-
assert result.did_run(sync_op1)
|
| 403 |
-
assert result.did_run(async_op1)
|
| 404 |
-
assert result.did_run(sync_op2)
|
| 405 |
-
assert result.did_run(async_op2)
|
| 406 |
-
|
| 407 |
-
# Image should be a mix of black and white (gray)
|
| 408 |
-
result_images = result.get_images(output)
|
| 409 |
-
avg_value = np.array(result_images[0]).mean()
|
| 410 |
-
assert abs(avg_value - 63.75) < 5, f"Average value {avg_value} should be ~63.75"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/test_execution.py
DELETED
|
@@ -1,587 +0,0 @@
|
|
| 1 |
-
from io import BytesIO
|
| 2 |
-
import numpy
|
| 3 |
-
from PIL import Image
|
| 4 |
-
import pytest
|
| 5 |
-
from pytest import fixture
|
| 6 |
-
import time
|
| 7 |
-
import torch
|
| 8 |
-
from typing import Union, Dict
|
| 9 |
-
import json
|
| 10 |
-
import subprocess
|
| 11 |
-
import websocket #NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
|
| 12 |
-
import uuid
|
| 13 |
-
import urllib.request
|
| 14 |
-
import urllib.parse
|
| 15 |
-
import urllib.error
|
| 16 |
-
from comfy_execution.graph_utils import GraphBuilder, Node
|
| 17 |
-
|
| 18 |
-
class RunResult:
|
| 19 |
-
def __init__(self, prompt_id: str):
|
| 20 |
-
self.outputs: Dict[str,Dict] = {}
|
| 21 |
-
self.runs: Dict[str,bool] = {}
|
| 22 |
-
self.prompt_id: str = prompt_id
|
| 23 |
-
|
| 24 |
-
def get_output(self, node: Node):
|
| 25 |
-
return self.outputs.get(node.id, None)
|
| 26 |
-
|
| 27 |
-
def did_run(self, node: Node):
|
| 28 |
-
return self.runs.get(node.id, False)
|
| 29 |
-
|
| 30 |
-
def get_images(self, node: Node):
|
| 31 |
-
output = self.get_output(node)
|
| 32 |
-
if output is None:
|
| 33 |
-
return []
|
| 34 |
-
return output.get('image_objects', [])
|
| 35 |
-
|
| 36 |
-
def get_prompt_id(self):
|
| 37 |
-
return self.prompt_id
|
| 38 |
-
|
| 39 |
-
class ComfyClient:
|
| 40 |
-
def __init__(self):
|
| 41 |
-
self.test_name = ""
|
| 42 |
-
|
| 43 |
-
def connect(self,
|
| 44 |
-
listen:str = '127.0.0.1',
|
| 45 |
-
port:Union[str,int] = 8188,
|
| 46 |
-
client_id: str = str(uuid.uuid4())
|
| 47 |
-
):
|
| 48 |
-
self.client_id = client_id
|
| 49 |
-
self.server_address = f"{listen}:{port}"
|
| 50 |
-
ws = websocket.WebSocket()
|
| 51 |
-
ws.connect("ws://{}/ws?clientId={}".format(self.server_address, self.client_id))
|
| 52 |
-
self.ws = ws
|
| 53 |
-
|
| 54 |
-
def queue_prompt(self, prompt):
|
| 55 |
-
p = {"prompt": prompt, "client_id": self.client_id}
|
| 56 |
-
data = json.dumps(p).encode('utf-8')
|
| 57 |
-
req = urllib.request.Request("http://{}/prompt".format(self.server_address), data=data)
|
| 58 |
-
return json.loads(urllib.request.urlopen(req).read())
|
| 59 |
-
|
| 60 |
-
def get_image(self, filename, subfolder, folder_type):
|
| 61 |
-
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
| 62 |
-
url_values = urllib.parse.urlencode(data)
|
| 63 |
-
with urllib.request.urlopen("http://{}/view?{}".format(self.server_address, url_values)) as response:
|
| 64 |
-
return response.read()
|
| 65 |
-
|
| 66 |
-
def get_history(self, prompt_id):
|
| 67 |
-
with urllib.request.urlopen("http://{}/history/{}".format(self.server_address, prompt_id)) as response:
|
| 68 |
-
return json.loads(response.read())
|
| 69 |
-
|
| 70 |
-
def set_test_name(self, name):
|
| 71 |
-
self.test_name = name
|
| 72 |
-
|
| 73 |
-
def run(self, graph):
|
| 74 |
-
prompt = graph.finalize()
|
| 75 |
-
for node in graph.nodes.values():
|
| 76 |
-
if node.class_type == 'SaveImage':
|
| 77 |
-
node.inputs['filename_prefix'] = self.test_name
|
| 78 |
-
|
| 79 |
-
prompt_id = self.queue_prompt(prompt)['prompt_id']
|
| 80 |
-
result = RunResult(prompt_id)
|
| 81 |
-
while True:
|
| 82 |
-
out = self.ws.recv()
|
| 83 |
-
if isinstance(out, str):
|
| 84 |
-
message = json.loads(out)
|
| 85 |
-
if message['type'] == 'executing':
|
| 86 |
-
data = message['data']
|
| 87 |
-
if data['prompt_id'] != prompt_id:
|
| 88 |
-
continue
|
| 89 |
-
if data['node'] is None:
|
| 90 |
-
break
|
| 91 |
-
result.runs[data['node']] = True
|
| 92 |
-
elif message['type'] == 'execution_error':
|
| 93 |
-
raise Exception(message['data'])
|
| 94 |
-
elif message['type'] == 'execution_cached':
|
| 95 |
-
pass # Probably want to store this off for testing
|
| 96 |
-
|
| 97 |
-
history = self.get_history(prompt_id)[prompt_id]
|
| 98 |
-
for node_id in history['outputs']:
|
| 99 |
-
node_output = history['outputs'][node_id]
|
| 100 |
-
result.outputs[node_id] = node_output
|
| 101 |
-
images_output = []
|
| 102 |
-
if 'images' in node_output:
|
| 103 |
-
for image in node_output['images']:
|
| 104 |
-
image_data = self.get_image(image['filename'], image['subfolder'], image['type'])
|
| 105 |
-
image_obj = Image.open(BytesIO(image_data))
|
| 106 |
-
images_output.append(image_obj)
|
| 107 |
-
node_output['image_objects'] = images_output
|
| 108 |
-
|
| 109 |
-
return result
|
| 110 |
-
|
| 111 |
-
#
|
| 112 |
-
# Loop through these variables
|
| 113 |
-
#
|
| 114 |
-
@pytest.mark.execution
|
| 115 |
-
class TestExecution:
|
| 116 |
-
#
|
| 117 |
-
# Initialize server and client
|
| 118 |
-
#
|
| 119 |
-
@fixture(scope="class", autouse=True, params=[
|
| 120 |
-
# (use_lru, lru_size)
|
| 121 |
-
(False, 0),
|
| 122 |
-
(True, 0),
|
| 123 |
-
(True, 100),
|
| 124 |
-
])
|
| 125 |
-
def _server(self, args_pytest, request):
|
| 126 |
-
# Start server
|
| 127 |
-
pargs = [
|
| 128 |
-
'python','main.py',
|
| 129 |
-
'--output-directory', args_pytest["output_dir"],
|
| 130 |
-
'--listen', args_pytest["listen"],
|
| 131 |
-
'--port', str(args_pytest["port"]),
|
| 132 |
-
'--extra-model-paths-config', 'tests/inference/extra_model_paths.yaml',
|
| 133 |
-
]
|
| 134 |
-
use_lru, lru_size = request.param
|
| 135 |
-
if use_lru:
|
| 136 |
-
pargs += ['--cache-lru', str(lru_size)]
|
| 137 |
-
print("Running server with args:", pargs) # noqa: T201
|
| 138 |
-
p = subprocess.Popen(pargs)
|
| 139 |
-
yield
|
| 140 |
-
p.kill()
|
| 141 |
-
torch.cuda.empty_cache()
|
| 142 |
-
|
| 143 |
-
def start_client(self, listen:str, port:int):
|
| 144 |
-
# Start client
|
| 145 |
-
comfy_client = ComfyClient()
|
| 146 |
-
# Connect to server (with retries)
|
| 147 |
-
n_tries = 5
|
| 148 |
-
for i in range(n_tries):
|
| 149 |
-
time.sleep(4)
|
| 150 |
-
try:
|
| 151 |
-
comfy_client.connect(listen=listen, port=port)
|
| 152 |
-
except ConnectionRefusedError as e:
|
| 153 |
-
print(e) # noqa: T201
|
| 154 |
-
print(f"({i+1}/{n_tries}) Retrying...") # noqa: T201
|
| 155 |
-
else:
|
| 156 |
-
break
|
| 157 |
-
return comfy_client
|
| 158 |
-
|
| 159 |
-
@fixture(scope="class", autouse=True)
|
| 160 |
-
def shared_client(self, args_pytest, _server):
|
| 161 |
-
client = self.start_client(args_pytest["listen"], args_pytest["port"])
|
| 162 |
-
yield client
|
| 163 |
-
del client
|
| 164 |
-
torch.cuda.empty_cache()
|
| 165 |
-
|
| 166 |
-
@fixture
|
| 167 |
-
def client(self, shared_client, request):
|
| 168 |
-
shared_client.set_test_name(f"execution[{request.node.name}]")
|
| 169 |
-
yield shared_client
|
| 170 |
-
|
| 171 |
-
@fixture
|
| 172 |
-
def builder(self, request):
|
| 173 |
-
yield GraphBuilder(prefix=request.node.name)
|
| 174 |
-
|
| 175 |
-
def test_lazy_input(self, client: ComfyClient, builder: GraphBuilder):
|
| 176 |
-
g = builder
|
| 177 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 178 |
-
input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 179 |
-
mask = g.node("StubMask", value=0.0, height=512, width=512, batch_size=1)
|
| 180 |
-
|
| 181 |
-
lazy_mix = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
|
| 182 |
-
output = g.node("SaveImage", images=lazy_mix.out(0))
|
| 183 |
-
result = client.run(g)
|
| 184 |
-
|
| 185 |
-
result_image = result.get_images(output)[0]
|
| 186 |
-
assert numpy.array(result_image).any() == 0, "Image should be black"
|
| 187 |
-
assert result.did_run(input1)
|
| 188 |
-
assert not result.did_run(input2)
|
| 189 |
-
assert result.did_run(mask)
|
| 190 |
-
assert result.did_run(lazy_mix)
|
| 191 |
-
|
| 192 |
-
def test_full_cache(self, client: ComfyClient, builder: GraphBuilder):
|
| 193 |
-
g = builder
|
| 194 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 195 |
-
input2 = g.node("StubImage", content="NOISE", height=512, width=512, batch_size=1)
|
| 196 |
-
mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
|
| 197 |
-
|
| 198 |
-
lazy_mix = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
|
| 199 |
-
g.node("SaveImage", images=lazy_mix.out(0))
|
| 200 |
-
|
| 201 |
-
client.run(g)
|
| 202 |
-
result2 = client.run(g)
|
| 203 |
-
for node_id, node in g.nodes.items():
|
| 204 |
-
assert not result2.did_run(node), f"Node {node_id} ran, but should have been cached"
|
| 205 |
-
|
| 206 |
-
def test_partial_cache(self, client: ComfyClient, builder: GraphBuilder):
|
| 207 |
-
g = builder
|
| 208 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 209 |
-
input2 = g.node("StubImage", content="NOISE", height=512, width=512, batch_size=1)
|
| 210 |
-
mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
|
| 211 |
-
|
| 212 |
-
lazy_mix = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
|
| 213 |
-
g.node("SaveImage", images=lazy_mix.out(0))
|
| 214 |
-
|
| 215 |
-
client.run(g)
|
| 216 |
-
mask.inputs['value'] = 0.4
|
| 217 |
-
result2 = client.run(g)
|
| 218 |
-
assert not result2.did_run(input1), "Input1 should have been cached"
|
| 219 |
-
assert not result2.did_run(input2), "Input2 should have been cached"
|
| 220 |
-
|
| 221 |
-
def test_error(self, client: ComfyClient, builder: GraphBuilder):
|
| 222 |
-
g = builder
|
| 223 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 224 |
-
# Different size of the two images
|
| 225 |
-
input2 = g.node("StubImage", content="NOISE", height=256, width=256, batch_size=1)
|
| 226 |
-
mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
|
| 227 |
-
|
| 228 |
-
lazy_mix = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
|
| 229 |
-
g.node("SaveImage", images=lazy_mix.out(0))
|
| 230 |
-
|
| 231 |
-
try:
|
| 232 |
-
client.run(g)
|
| 233 |
-
assert False, "Should have raised an error"
|
| 234 |
-
except Exception as e:
|
| 235 |
-
assert 'prompt_id' in e.args[0], f"Did not get back a proper error message: {e}"
|
| 236 |
-
|
| 237 |
-
@pytest.mark.parametrize("test_value, expect_error", [
|
| 238 |
-
(5, True),
|
| 239 |
-
("foo", True),
|
| 240 |
-
(5.0, False),
|
| 241 |
-
])
|
| 242 |
-
def test_validation_error_literal(self, test_value, expect_error, client: ComfyClient, builder: GraphBuilder):
|
| 243 |
-
g = builder
|
| 244 |
-
validation1 = g.node("TestCustomValidation1", input1=test_value, input2=3.0)
|
| 245 |
-
g.node("SaveImage", images=validation1.out(0))
|
| 246 |
-
|
| 247 |
-
if expect_error:
|
| 248 |
-
with pytest.raises(urllib.error.HTTPError):
|
| 249 |
-
client.run(g)
|
| 250 |
-
else:
|
| 251 |
-
client.run(g)
|
| 252 |
-
|
| 253 |
-
@pytest.mark.parametrize("test_type, test_value", [
|
| 254 |
-
("StubInt", 5),
|
| 255 |
-
("StubMask", 5.0)
|
| 256 |
-
])
|
| 257 |
-
def test_validation_error_edge1(self, test_type, test_value, client: ComfyClient, builder: GraphBuilder):
|
| 258 |
-
g = builder
|
| 259 |
-
stub = g.node(test_type, value=test_value)
|
| 260 |
-
validation1 = g.node("TestCustomValidation1", input1=stub.out(0), input2=3.0)
|
| 261 |
-
g.node("SaveImage", images=validation1.out(0))
|
| 262 |
-
|
| 263 |
-
with pytest.raises(urllib.error.HTTPError):
|
| 264 |
-
client.run(g)
|
| 265 |
-
|
| 266 |
-
@pytest.mark.parametrize("test_type, test_value, expect_error", [
|
| 267 |
-
("StubInt", 5, True),
|
| 268 |
-
("StubFloat", 5.0, False)
|
| 269 |
-
])
|
| 270 |
-
def test_validation_error_edge2(self, test_type, test_value, expect_error, client: ComfyClient, builder: GraphBuilder):
|
| 271 |
-
g = builder
|
| 272 |
-
stub = g.node(test_type, value=test_value)
|
| 273 |
-
validation2 = g.node("TestCustomValidation2", input1=stub.out(0), input2=3.0)
|
| 274 |
-
g.node("SaveImage", images=validation2.out(0))
|
| 275 |
-
|
| 276 |
-
if expect_error:
|
| 277 |
-
with pytest.raises(urllib.error.HTTPError):
|
| 278 |
-
client.run(g)
|
| 279 |
-
else:
|
| 280 |
-
client.run(g)
|
| 281 |
-
|
| 282 |
-
@pytest.mark.parametrize("test_type, test_value, expect_error", [
|
| 283 |
-
("StubInt", 5, True),
|
| 284 |
-
("StubFloat", 5.0, False)
|
| 285 |
-
])
|
| 286 |
-
def test_validation_error_edge3(self, test_type, test_value, expect_error, client: ComfyClient, builder: GraphBuilder):
|
| 287 |
-
g = builder
|
| 288 |
-
stub = g.node(test_type, value=test_value)
|
| 289 |
-
validation3 = g.node("TestCustomValidation3", input1=stub.out(0), input2=3.0)
|
| 290 |
-
g.node("SaveImage", images=validation3.out(0))
|
| 291 |
-
|
| 292 |
-
if expect_error:
|
| 293 |
-
with pytest.raises(urllib.error.HTTPError):
|
| 294 |
-
client.run(g)
|
| 295 |
-
else:
|
| 296 |
-
client.run(g)
|
| 297 |
-
|
| 298 |
-
@pytest.mark.parametrize("test_type, test_value, expect_error", [
|
| 299 |
-
("StubInt", 5, True),
|
| 300 |
-
("StubFloat", 5.0, False)
|
| 301 |
-
])
|
| 302 |
-
def test_validation_error_edge4(self, test_type, test_value, expect_error, client: ComfyClient, builder: GraphBuilder):
|
| 303 |
-
g = builder
|
| 304 |
-
stub = g.node(test_type, value=test_value)
|
| 305 |
-
validation4 = g.node("TestCustomValidation4", input1=stub.out(0), input2=3.0)
|
| 306 |
-
g.node("SaveImage", images=validation4.out(0))
|
| 307 |
-
|
| 308 |
-
if expect_error:
|
| 309 |
-
with pytest.raises(urllib.error.HTTPError):
|
| 310 |
-
client.run(g)
|
| 311 |
-
else:
|
| 312 |
-
client.run(g)
|
| 313 |
-
|
| 314 |
-
@pytest.mark.parametrize("test_value1, test_value2, expect_error", [
|
| 315 |
-
(0.0, 0.5, False),
|
| 316 |
-
(0.0, 5.0, False),
|
| 317 |
-
(0.0, 7.0, True)
|
| 318 |
-
])
|
| 319 |
-
def test_validation_error_kwargs(self, test_value1, test_value2, expect_error, client: ComfyClient, builder: GraphBuilder):
|
| 320 |
-
g = builder
|
| 321 |
-
validation5 = g.node("TestCustomValidation5", input1=test_value1, input2=test_value2)
|
| 322 |
-
g.node("SaveImage", images=validation5.out(0))
|
| 323 |
-
|
| 324 |
-
if expect_error:
|
| 325 |
-
with pytest.raises(urllib.error.HTTPError):
|
| 326 |
-
client.run(g)
|
| 327 |
-
else:
|
| 328 |
-
client.run(g)
|
| 329 |
-
|
| 330 |
-
def test_cycle_error(self, client: ComfyClient, builder: GraphBuilder):
|
| 331 |
-
g = builder
|
| 332 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 333 |
-
input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 334 |
-
mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
|
| 335 |
-
|
| 336 |
-
lazy_mix1 = g.node("TestLazyMixImages", image1=input1.out(0), mask=mask.out(0))
|
| 337 |
-
lazy_mix2 = g.node("TestLazyMixImages", image1=lazy_mix1.out(0), image2=input2.out(0), mask=mask.out(0))
|
| 338 |
-
g.node("SaveImage", images=lazy_mix2.out(0))
|
| 339 |
-
|
| 340 |
-
# When the cycle exists on initial submission, it should raise a validation error
|
| 341 |
-
with pytest.raises(urllib.error.HTTPError):
|
| 342 |
-
client.run(g)
|
| 343 |
-
|
| 344 |
-
def test_dynamic_cycle_error(self, client: ComfyClient, builder: GraphBuilder):
|
| 345 |
-
g = builder
|
| 346 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 347 |
-
input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 348 |
-
generator = g.node("TestDynamicDependencyCycle", input1=input1.out(0), input2=input2.out(0))
|
| 349 |
-
g.node("SaveImage", images=generator.out(0))
|
| 350 |
-
|
| 351 |
-
# When the cycle is in a graph that is generated dynamically, it should raise a runtime error
|
| 352 |
-
try:
|
| 353 |
-
client.run(g)
|
| 354 |
-
assert False, "Should have raised an error"
|
| 355 |
-
except Exception as e:
|
| 356 |
-
assert 'prompt_id' in e.args[0], f"Did not get back a proper error message: {e}"
|
| 357 |
-
assert e.args[0]['node_id'] == generator.id, "Error should have been on the generator node"
|
| 358 |
-
|
| 359 |
-
def test_missing_node_error(self, client: ComfyClient, builder: GraphBuilder):
|
| 360 |
-
g = builder
|
| 361 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 362 |
-
input2 = g.node("StubImage", id="removeme", content="WHITE", height=512, width=512, batch_size=1)
|
| 363 |
-
input3 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 364 |
-
mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
|
| 365 |
-
mix1 = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
|
| 366 |
-
mix2 = g.node("TestLazyMixImages", image1=input1.out(0), image2=input3.out(0), mask=mask.out(0))
|
| 367 |
-
# We have multiple outputs. The first is invalid, but the second is valid
|
| 368 |
-
g.node("SaveImage", images=mix1.out(0))
|
| 369 |
-
g.node("SaveImage", images=mix2.out(0))
|
| 370 |
-
g.remove_node("removeme")
|
| 371 |
-
|
| 372 |
-
client.run(g)
|
| 373 |
-
|
| 374 |
-
# Add back in the missing node to make sure the error doesn't break the server
|
| 375 |
-
input2 = g.node("StubImage", id="removeme", content="WHITE", height=512, width=512, batch_size=1)
|
| 376 |
-
client.run(g)
|
| 377 |
-
|
| 378 |
-
def test_custom_is_changed(self, client: ComfyClient, builder: GraphBuilder):
|
| 379 |
-
g = builder
|
| 380 |
-
# Creating the nodes in this specific order previously caused a bug
|
| 381 |
-
save = g.node("SaveImage")
|
| 382 |
-
is_changed = g.node("TestCustomIsChanged", should_change=False)
|
| 383 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 384 |
-
|
| 385 |
-
save.set_input('images', is_changed.out(0))
|
| 386 |
-
is_changed.set_input('image', input1.out(0))
|
| 387 |
-
|
| 388 |
-
result1 = client.run(g)
|
| 389 |
-
result2 = client.run(g)
|
| 390 |
-
is_changed.set_input('should_change', True)
|
| 391 |
-
result3 = client.run(g)
|
| 392 |
-
result4 = client.run(g)
|
| 393 |
-
assert result1.did_run(is_changed), "is_changed should have been run"
|
| 394 |
-
assert not result2.did_run(is_changed), "is_changed should have been cached"
|
| 395 |
-
assert result3.did_run(is_changed), "is_changed should have been re-run"
|
| 396 |
-
assert result4.did_run(is_changed), "is_changed should not have been cached"
|
| 397 |
-
|
| 398 |
-
def test_undeclared_inputs(self, client: ComfyClient, builder: GraphBuilder):
|
| 399 |
-
g = builder
|
| 400 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 401 |
-
input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 402 |
-
input3 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 403 |
-
input4 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 404 |
-
average = g.node("TestVariadicAverage", input1=input1.out(0), input2=input2.out(0), input3=input3.out(0), input4=input4.out(0))
|
| 405 |
-
output = g.node("SaveImage", images=average.out(0))
|
| 406 |
-
|
| 407 |
-
result = client.run(g)
|
| 408 |
-
result_image = result.get_images(output)[0]
|
| 409 |
-
expected = 255 // 4
|
| 410 |
-
assert numpy.array(result_image).min() == expected and numpy.array(result_image).max() == expected, "Image should be grey"
|
| 411 |
-
|
| 412 |
-
def test_for_loop(self, client: ComfyClient, builder: GraphBuilder):
|
| 413 |
-
g = builder
|
| 414 |
-
iterations = 4
|
| 415 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 416 |
-
input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 417 |
-
is_changed = g.node("TestCustomIsChanged", should_change=True, image=input2.out(0))
|
| 418 |
-
for_open = g.node("TestForLoopOpen", remaining=iterations, initial_value1=is_changed.out(0))
|
| 419 |
-
average = g.node("TestVariadicAverage", input1=input1.out(0), input2=for_open.out(2))
|
| 420 |
-
for_close = g.node("TestForLoopClose", flow_control=for_open.out(0), initial_value1=average.out(0))
|
| 421 |
-
output = g.node("SaveImage", images=for_close.out(0))
|
| 422 |
-
|
| 423 |
-
for iterations in range(1, 5):
|
| 424 |
-
for_open.set_input('remaining', iterations)
|
| 425 |
-
result = client.run(g)
|
| 426 |
-
result_image = result.get_images(output)[0]
|
| 427 |
-
expected = 255 // (2 ** iterations)
|
| 428 |
-
assert numpy.array(result_image).min() == expected and numpy.array(result_image).max() == expected, "Image should be grey"
|
| 429 |
-
assert result.did_run(is_changed)
|
| 430 |
-
|
| 431 |
-
def test_mixed_expansion_returns(self, client: ComfyClient, builder: GraphBuilder):
|
| 432 |
-
g = builder
|
| 433 |
-
val_list = g.node("TestMakeListNode", value1=0.1, value2=0.2, value3=0.3)
|
| 434 |
-
mixed = g.node("TestMixedExpansionReturns", input1=val_list.out(0))
|
| 435 |
-
output_dynamic = g.node("SaveImage", images=mixed.out(0))
|
| 436 |
-
output_literal = g.node("SaveImage", images=mixed.out(1))
|
| 437 |
-
|
| 438 |
-
result = client.run(g)
|
| 439 |
-
images_dynamic = result.get_images(output_dynamic)
|
| 440 |
-
assert len(images_dynamic) == 3, "Should have 2 images"
|
| 441 |
-
assert numpy.array(images_dynamic[0]).min() == 25 and numpy.array(images_dynamic[0]).max() == 25, "First image should be 0.1"
|
| 442 |
-
assert numpy.array(images_dynamic[1]).min() == 51 and numpy.array(images_dynamic[1]).max() == 51, "Second image should be 0.2"
|
| 443 |
-
assert numpy.array(images_dynamic[2]).min() == 76 and numpy.array(images_dynamic[2]).max() == 76, "Third image should be 0.3"
|
| 444 |
-
|
| 445 |
-
images_literal = result.get_images(output_literal)
|
| 446 |
-
assert len(images_literal) == 3, "Should have 2 images"
|
| 447 |
-
for i in range(3):
|
| 448 |
-
assert numpy.array(images_literal[i]).min() == 255 and numpy.array(images_literal[i]).max() == 255, "All images should be white"
|
| 449 |
-
|
| 450 |
-
def test_mixed_lazy_results(self, client: ComfyClient, builder: GraphBuilder):
|
| 451 |
-
g = builder
|
| 452 |
-
val_list = g.node("TestMakeListNode", value1=0.0, value2=0.5, value3=1.0)
|
| 453 |
-
mask = g.node("StubMask", value=val_list.out(0), height=512, width=512, batch_size=1)
|
| 454 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 455 |
-
input2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 456 |
-
mix = g.node("TestLazyMixImages", image1=input1.out(0), image2=input2.out(0), mask=mask.out(0))
|
| 457 |
-
rebatch = g.node("RebatchImages", images=mix.out(0), batch_size=3)
|
| 458 |
-
output = g.node("SaveImage", images=rebatch.out(0))
|
| 459 |
-
|
| 460 |
-
result = client.run(g)
|
| 461 |
-
images = result.get_images(output)
|
| 462 |
-
assert len(images) == 3, "Should have 3 image"
|
| 463 |
-
assert numpy.array(images[0]).min() == 0 and numpy.array(images[0]).max() == 0, "First image should be 0.0"
|
| 464 |
-
assert numpy.array(images[1]).min() == 127 and numpy.array(images[1]).max() == 127, "Second image should be 0.5"
|
| 465 |
-
assert numpy.array(images[2]).min() == 255 and numpy.array(images[2]).max() == 255, "Third image should be 1.0"
|
| 466 |
-
|
| 467 |
-
def test_output_reuse(self, client: ComfyClient, builder: GraphBuilder):
|
| 468 |
-
g = builder
|
| 469 |
-
input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 470 |
-
|
| 471 |
-
output1 = g.node("SaveImage", images=input1.out(0))
|
| 472 |
-
output2 = g.node("SaveImage", images=input1.out(0))
|
| 473 |
-
|
| 474 |
-
result = client.run(g)
|
| 475 |
-
images1 = result.get_images(output1)
|
| 476 |
-
images2 = result.get_images(output2)
|
| 477 |
-
assert len(images1) == 1, "Should have 1 image"
|
| 478 |
-
assert len(images2) == 1, "Should have 1 image"
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
# This tests that only constant outputs are used in the call to `IS_CHANGED`
|
| 482 |
-
def test_is_changed_with_outputs(self, client: ComfyClient, builder: GraphBuilder):
|
| 483 |
-
g = builder
|
| 484 |
-
input1 = g.node("StubConstantImage", value=0.5, height=512, width=512, batch_size=1)
|
| 485 |
-
test_node = g.node("TestIsChangedWithConstants", image=input1.out(0), value=0.5)
|
| 486 |
-
|
| 487 |
-
output = g.node("PreviewImage", images=test_node.out(0))
|
| 488 |
-
|
| 489 |
-
result = client.run(g)
|
| 490 |
-
images = result.get_images(output)
|
| 491 |
-
assert len(images) == 1, "Should have 1 image"
|
| 492 |
-
assert numpy.array(images[0]).min() == 63 and numpy.array(images[0]).max() == 63, "Image should have value 0.25"
|
| 493 |
-
|
| 494 |
-
result = client.run(g)
|
| 495 |
-
images = result.get_images(output)
|
| 496 |
-
assert len(images) == 1, "Should have 1 image"
|
| 497 |
-
assert numpy.array(images[0]).min() == 63 and numpy.array(images[0]).max() == 63, "Image should have value 0.25"
|
| 498 |
-
assert not result.did_run(test_node), "The execution should have been cached"
|
| 499 |
-
|
| 500 |
-
def test_parallel_sleep_nodes(self, client: ComfyClient, builder: GraphBuilder):
|
| 501 |
-
g = builder
|
| 502 |
-
image = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 503 |
-
|
| 504 |
-
# Create sleep nodes for each duration
|
| 505 |
-
sleep_node1 = g.node("TestSleep", value=image.out(0), seconds=2.8)
|
| 506 |
-
sleep_node2 = g.node("TestSleep", value=image.out(0), seconds=2.9)
|
| 507 |
-
sleep_node3 = g.node("TestSleep", value=image.out(0), seconds=3.0)
|
| 508 |
-
|
| 509 |
-
# Add outputs to verify the execution
|
| 510 |
-
_output1 = g.node("PreviewImage", images=sleep_node1.out(0))
|
| 511 |
-
_output2 = g.node("PreviewImage", images=sleep_node2.out(0))
|
| 512 |
-
_output3 = g.node("PreviewImage", images=sleep_node3.out(0))
|
| 513 |
-
|
| 514 |
-
start_time = time.time()
|
| 515 |
-
result = client.run(g)
|
| 516 |
-
elapsed_time = time.time() - start_time
|
| 517 |
-
|
| 518 |
-
# The test should take around 0.4 seconds (the longest sleep duration)
|
| 519 |
-
# plus some overhead, but definitely less than the sum of all sleeps (0.9s)
|
| 520 |
-
# We'll allow for up to 0.8s total to account for overhead
|
| 521 |
-
assert elapsed_time < 4.0, f"Parallel execution took {elapsed_time}s, expected less than 0.8s"
|
| 522 |
-
|
| 523 |
-
# Verify that all nodes executed
|
| 524 |
-
assert result.did_run(sleep_node1), "Sleep node 1 should have run"
|
| 525 |
-
assert result.did_run(sleep_node2), "Sleep node 2 should have run"
|
| 526 |
-
assert result.did_run(sleep_node3), "Sleep node 3 should have run"
|
| 527 |
-
|
| 528 |
-
def test_parallel_sleep_expansion(self, client: ComfyClient, builder: GraphBuilder):
|
| 529 |
-
g = builder
|
| 530 |
-
# Create input images with different values
|
| 531 |
-
image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 532 |
-
image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 533 |
-
image3 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 534 |
-
|
| 535 |
-
# Create a TestParallelSleep node that expands into multiple TestSleep nodes
|
| 536 |
-
parallel_sleep = g.node("TestParallelSleep",
|
| 537 |
-
image1=image1.out(0),
|
| 538 |
-
image2=image2.out(0),
|
| 539 |
-
image3=image3.out(0),
|
| 540 |
-
sleep1=0.4,
|
| 541 |
-
sleep2=0.5,
|
| 542 |
-
sleep3=0.6)
|
| 543 |
-
output = g.node("SaveImage", images=parallel_sleep.out(0))
|
| 544 |
-
|
| 545 |
-
start_time = time.time()
|
| 546 |
-
result = client.run(g)
|
| 547 |
-
elapsed_time = time.time() - start_time
|
| 548 |
-
|
| 549 |
-
# Similar to the previous test, expect parallel execution of the sleep nodes
|
| 550 |
-
# which should complete in less than the sum of all sleeps
|
| 551 |
-
assert elapsed_time < 0.8, f"Expansion execution took {elapsed_time}s, expected less than 0.8s"
|
| 552 |
-
|
| 553 |
-
# Verify the parallel sleep node executed
|
| 554 |
-
assert result.did_run(parallel_sleep), "ParallelSleep node should have run"
|
| 555 |
-
|
| 556 |
-
# Verify we get an image as output (blend of the three input images)
|
| 557 |
-
result_images = result.get_images(output)
|
| 558 |
-
assert len(result_images) == 1, "Should have 1 image"
|
| 559 |
-
# Average pixel value should be around 170 (255 * 2 // 3)
|
| 560 |
-
avg_value = numpy.array(result_images[0]).mean()
|
| 561 |
-
assert avg_value == 170, f"Image average value {avg_value} should be 170"
|
| 562 |
-
|
| 563 |
-
# This tests that nodes with OUTPUT_IS_LIST function correctly when they receive an ExecutionBlocker
|
| 564 |
-
# as input. We also test that when that list (containing an ExecutionBlocker) is passed to a node,
|
| 565 |
-
# only that one entry in the list is blocked.
|
| 566 |
-
def test_execution_block_list_output(self, client: ComfyClient, builder: GraphBuilder):
|
| 567 |
-
g = builder
|
| 568 |
-
image1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 569 |
-
image2 = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 570 |
-
image3 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 571 |
-
image_list = g.node("TestMakeListNode", value1=image1.out(0), value2=image2.out(0), value3=image3.out(0))
|
| 572 |
-
int1 = g.node("StubInt", value=1)
|
| 573 |
-
int2 = g.node("StubInt", value=2)
|
| 574 |
-
int3 = g.node("StubInt", value=3)
|
| 575 |
-
int_list = g.node("TestMakeListNode", value1=int1.out(0), value2=int2.out(0), value3=int3.out(0))
|
| 576 |
-
compare = g.node("TestIntConditions", a=int_list.out(0), b=2, operation="==")
|
| 577 |
-
blocker = g.node("TestExecutionBlocker", input=image_list.out(0), block=compare.out(0), verbose=False)
|
| 578 |
-
|
| 579 |
-
list_output = g.node("TestMakeListNode", value1=blocker.out(0))
|
| 580 |
-
output = g.node("PreviewImage", images=list_output.out(0))
|
| 581 |
-
|
| 582 |
-
result = client.run(g)
|
| 583 |
-
assert result.did_run(output), "The execution should have run"
|
| 584 |
-
images = result.get_images(output)
|
| 585 |
-
assert len(images) == 2, "Should have 2 images"
|
| 586 |
-
assert numpy.array(images[0]).min() == 0 and numpy.array(images[0]).max() == 0, "First image should be black"
|
| 587 |
-
assert numpy.array(images[1]).min() == 0 and numpy.array(images[1]).max() == 0, "Second image should also be black"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/test_inference.py
DELETED
|
@@ -1,237 +0,0 @@
|
|
| 1 |
-
from copy import deepcopy
|
| 2 |
-
from io import BytesIO
|
| 3 |
-
import numpy
|
| 4 |
-
import os
|
| 5 |
-
from PIL import Image
|
| 6 |
-
import pytest
|
| 7 |
-
from pytest import fixture
|
| 8 |
-
import time
|
| 9 |
-
import torch
|
| 10 |
-
from typing import Union
|
| 11 |
-
import json
|
| 12 |
-
import subprocess
|
| 13 |
-
import websocket #NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
|
| 14 |
-
import uuid
|
| 15 |
-
import urllib.request
|
| 16 |
-
import urllib.parse
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
from comfy.samplers import KSampler
|
| 20 |
-
|
| 21 |
-
"""
|
| 22 |
-
These tests generate and save images through a range of parameters
|
| 23 |
-
"""
|
| 24 |
-
|
| 25 |
-
class ComfyGraph:
|
| 26 |
-
def __init__(self,
|
| 27 |
-
graph: dict,
|
| 28 |
-
sampler_nodes: list[str],
|
| 29 |
-
):
|
| 30 |
-
self.graph = graph
|
| 31 |
-
self.sampler_nodes = sampler_nodes
|
| 32 |
-
|
| 33 |
-
def set_prompt(self, prompt, negative_prompt=None):
|
| 34 |
-
# Sets the prompt for the sampler nodes (eg. base and refiner)
|
| 35 |
-
for node in self.sampler_nodes:
|
| 36 |
-
prompt_node = self.graph[node]['inputs']['positive'][0]
|
| 37 |
-
self.graph[prompt_node]['inputs']['text'] = prompt
|
| 38 |
-
if negative_prompt:
|
| 39 |
-
negative_prompt_node = self.graph[node]['inputs']['negative'][0]
|
| 40 |
-
self.graph[negative_prompt_node]['inputs']['text'] = negative_prompt
|
| 41 |
-
|
| 42 |
-
def set_sampler_name(self, sampler_name:str, ):
|
| 43 |
-
# sets the sampler name for the sampler nodes (eg. base and refiner)
|
| 44 |
-
for node in self.sampler_nodes:
|
| 45 |
-
self.graph[node]['inputs']['sampler_name'] = sampler_name
|
| 46 |
-
|
| 47 |
-
def set_scheduler(self, scheduler:str):
|
| 48 |
-
# sets the sampler name for the sampler nodes (eg. base and refiner)
|
| 49 |
-
for node in self.sampler_nodes:
|
| 50 |
-
self.graph[node]['inputs']['scheduler'] = scheduler
|
| 51 |
-
|
| 52 |
-
def set_filename_prefix(self, prefix:str):
|
| 53 |
-
# sets the filename prefix for the save nodes
|
| 54 |
-
for node in self.graph:
|
| 55 |
-
if self.graph[node]['class_type'] == 'SaveImage':
|
| 56 |
-
self.graph[node]['inputs']['filename_prefix'] = prefix
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
class ComfyClient:
|
| 60 |
-
# From examples/websockets_api_example.py
|
| 61 |
-
|
| 62 |
-
def connect(self,
|
| 63 |
-
listen:str = '127.0.0.1',
|
| 64 |
-
port:Union[str,int] = 8188,
|
| 65 |
-
client_id: str = str(uuid.uuid4())
|
| 66 |
-
):
|
| 67 |
-
self.client_id = client_id
|
| 68 |
-
self.server_address = f"{listen}:{port}"
|
| 69 |
-
ws = websocket.WebSocket()
|
| 70 |
-
ws.connect("ws://{}/ws?clientId={}".format(self.server_address, self.client_id))
|
| 71 |
-
self.ws = ws
|
| 72 |
-
|
| 73 |
-
def queue_prompt(self, prompt):
|
| 74 |
-
p = {"prompt": prompt, "client_id": self.client_id}
|
| 75 |
-
data = json.dumps(p).encode('utf-8')
|
| 76 |
-
req = urllib.request.Request("http://{}/prompt".format(self.server_address), data=data)
|
| 77 |
-
return json.loads(urllib.request.urlopen(req).read())
|
| 78 |
-
|
| 79 |
-
def get_image(self, filename, subfolder, folder_type):
|
| 80 |
-
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
| 81 |
-
url_values = urllib.parse.urlencode(data)
|
| 82 |
-
with urllib.request.urlopen("http://{}/view?{}".format(self.server_address, url_values)) as response:
|
| 83 |
-
return response.read()
|
| 84 |
-
|
| 85 |
-
def get_history(self, prompt_id):
|
| 86 |
-
with urllib.request.urlopen("http://{}/history/{}".format(self.server_address, prompt_id)) as response:
|
| 87 |
-
return json.loads(response.read())
|
| 88 |
-
|
| 89 |
-
def get_images(self, graph, save=True):
|
| 90 |
-
prompt = graph
|
| 91 |
-
if not save:
|
| 92 |
-
# Replace save nodes with preview nodes
|
| 93 |
-
prompt_str = json.dumps(prompt)
|
| 94 |
-
prompt_str = prompt_str.replace('SaveImage', 'PreviewImage')
|
| 95 |
-
prompt = json.loads(prompt_str)
|
| 96 |
-
|
| 97 |
-
prompt_id = self.queue_prompt(prompt)['prompt_id']
|
| 98 |
-
output_images = {}
|
| 99 |
-
while True:
|
| 100 |
-
out = self.ws.recv()
|
| 101 |
-
if isinstance(out, str):
|
| 102 |
-
message = json.loads(out)
|
| 103 |
-
if message['type'] == 'executing':
|
| 104 |
-
data = message['data']
|
| 105 |
-
if data['node'] is None and data['prompt_id'] == prompt_id:
|
| 106 |
-
break #Execution is done
|
| 107 |
-
else:
|
| 108 |
-
continue #previews are binary data
|
| 109 |
-
|
| 110 |
-
history = self.get_history(prompt_id)[prompt_id]
|
| 111 |
-
for node_id in history['outputs']:
|
| 112 |
-
node_output = history['outputs'][node_id]
|
| 113 |
-
images_output = []
|
| 114 |
-
if 'images' in node_output:
|
| 115 |
-
for image in node_output['images']:
|
| 116 |
-
image_data = self.get_image(image['filename'], image['subfolder'], image['type'])
|
| 117 |
-
images_output.append(image_data)
|
| 118 |
-
output_images[node_id] = images_output
|
| 119 |
-
|
| 120 |
-
return output_images
|
| 121 |
-
|
| 122 |
-
#
|
| 123 |
-
# Initialize graphs
|
| 124 |
-
#
|
| 125 |
-
default_graph_file = 'tests/inference/graphs/default_graph_sdxl1_0.json'
|
| 126 |
-
with open(default_graph_file, 'r') as file:
|
| 127 |
-
default_graph = json.loads(file.read())
|
| 128 |
-
DEFAULT_COMFY_GRAPH = ComfyGraph(graph=default_graph, sampler_nodes=['10','14'])
|
| 129 |
-
DEFAULT_COMFY_GRAPH_ID = os.path.splitext(os.path.basename(default_graph_file))[0]
|
| 130 |
-
|
| 131 |
-
#
|
| 132 |
-
# Loop through these variables
|
| 133 |
-
#
|
| 134 |
-
comfy_graph_list = [DEFAULT_COMFY_GRAPH]
|
| 135 |
-
comfy_graph_ids = [DEFAULT_COMFY_GRAPH_ID]
|
| 136 |
-
prompt_list = [
|
| 137 |
-
'a painting of a cat',
|
| 138 |
-
]
|
| 139 |
-
|
| 140 |
-
sampler_list = KSampler.SAMPLERS
|
| 141 |
-
scheduler_list = KSampler.SCHEDULERS
|
| 142 |
-
|
| 143 |
-
@pytest.mark.inference
|
| 144 |
-
@pytest.mark.parametrize("sampler", sampler_list)
|
| 145 |
-
@pytest.mark.parametrize("scheduler", scheduler_list)
|
| 146 |
-
@pytest.mark.parametrize("prompt", prompt_list)
|
| 147 |
-
class TestInference:
|
| 148 |
-
#
|
| 149 |
-
# Initialize server and client
|
| 150 |
-
#
|
| 151 |
-
@fixture(scope="class", autouse=True)
|
| 152 |
-
def _server(self, args_pytest):
|
| 153 |
-
# Start server
|
| 154 |
-
p = subprocess.Popen([
|
| 155 |
-
'python','main.py',
|
| 156 |
-
'--output-directory', args_pytest["output_dir"],
|
| 157 |
-
'--listen', args_pytest["listen"],
|
| 158 |
-
'--port', str(args_pytest["port"]),
|
| 159 |
-
])
|
| 160 |
-
yield
|
| 161 |
-
p.kill()
|
| 162 |
-
torch.cuda.empty_cache()
|
| 163 |
-
|
| 164 |
-
def start_client(self, listen:str, port:int):
|
| 165 |
-
# Start client
|
| 166 |
-
comfy_client = ComfyClient()
|
| 167 |
-
# Connect to server (with retries)
|
| 168 |
-
n_tries = 5
|
| 169 |
-
for i in range(n_tries):
|
| 170 |
-
time.sleep(4)
|
| 171 |
-
try:
|
| 172 |
-
comfy_client.connect(listen=listen, port=port)
|
| 173 |
-
except ConnectionRefusedError as e:
|
| 174 |
-
print(e) # noqa: T201
|
| 175 |
-
print(f"({i+1}/{n_tries}) Retrying...") # noqa: T201
|
| 176 |
-
else:
|
| 177 |
-
break
|
| 178 |
-
return comfy_client
|
| 179 |
-
|
| 180 |
-
#
|
| 181 |
-
# Client and graph fixtures with server warmup
|
| 182 |
-
#
|
| 183 |
-
# Returns a "_client_graph", which is client-graph pair corresponding to an initialized server
|
| 184 |
-
# The "graph" is the default graph
|
| 185 |
-
@fixture(scope="class", params=comfy_graph_list, ids=comfy_graph_ids, autouse=True)
|
| 186 |
-
def _client_graph(self, request, args_pytest, _server) -> (ComfyClient, ComfyGraph):
|
| 187 |
-
comfy_graph = request.param
|
| 188 |
-
|
| 189 |
-
# Start client
|
| 190 |
-
comfy_client = self.start_client(args_pytest["listen"], args_pytest["port"])
|
| 191 |
-
|
| 192 |
-
# Warm up pipeline
|
| 193 |
-
comfy_client.get_images(graph=comfy_graph.graph, save=False)
|
| 194 |
-
|
| 195 |
-
yield comfy_client, comfy_graph
|
| 196 |
-
del comfy_client
|
| 197 |
-
del comfy_graph
|
| 198 |
-
torch.cuda.empty_cache()
|
| 199 |
-
|
| 200 |
-
@fixture
|
| 201 |
-
def client(self, _client_graph):
|
| 202 |
-
client = _client_graph[0]
|
| 203 |
-
yield client
|
| 204 |
-
|
| 205 |
-
@fixture
|
| 206 |
-
def comfy_graph(self, _client_graph):
|
| 207 |
-
# avoid mutating the graph
|
| 208 |
-
graph = deepcopy(_client_graph[1])
|
| 209 |
-
yield graph
|
| 210 |
-
|
| 211 |
-
def test_comfy(
|
| 212 |
-
self,
|
| 213 |
-
client,
|
| 214 |
-
comfy_graph,
|
| 215 |
-
sampler,
|
| 216 |
-
scheduler,
|
| 217 |
-
prompt,
|
| 218 |
-
request
|
| 219 |
-
):
|
| 220 |
-
test_info = request.node.name
|
| 221 |
-
comfy_graph.set_filename_prefix(test_info)
|
| 222 |
-
# Settings for comfy graph
|
| 223 |
-
comfy_graph.set_sampler_name(sampler)
|
| 224 |
-
comfy_graph.set_scheduler(scheduler)
|
| 225 |
-
comfy_graph.set_prompt(prompt)
|
| 226 |
-
|
| 227 |
-
# Generate
|
| 228 |
-
images = client.get_images(comfy_graph.graph)
|
| 229 |
-
|
| 230 |
-
assert len(images) != 0, "No images generated"
|
| 231 |
-
# assert all images are not blank
|
| 232 |
-
for images_output in images.values():
|
| 233 |
-
for image_data in images_output:
|
| 234 |
-
pil_image = Image.open(BytesIO(image_data))
|
| 235 |
-
assert numpy.array(pil_image).any() != 0, "Image is blank"
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/testing_nodes/testing-pack/__init__.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
from .specific_tests import TEST_NODE_CLASS_MAPPINGS, TEST_NODE_DISPLAY_NAME_MAPPINGS
|
| 2 |
-
from .flow_control import FLOW_CONTROL_NODE_CLASS_MAPPINGS, FLOW_CONTROL_NODE_DISPLAY_NAME_MAPPINGS
|
| 3 |
-
from .util import UTILITY_NODE_CLASS_MAPPINGS, UTILITY_NODE_DISPLAY_NAME_MAPPINGS
|
| 4 |
-
from .conditions import CONDITION_NODE_CLASS_MAPPINGS, CONDITION_NODE_DISPLAY_NAME_MAPPINGS
|
| 5 |
-
from .stubs import TEST_STUB_NODE_CLASS_MAPPINGS, TEST_STUB_NODE_DISPLAY_NAME_MAPPINGS
|
| 6 |
-
from .async_test_nodes import ASYNC_TEST_NODE_CLASS_MAPPINGS, ASYNC_TEST_NODE_DISPLAY_NAME_MAPPINGS
|
| 7 |
-
|
| 8 |
-
# NODE_CLASS_MAPPINGS = GENERAL_NODE_CLASS_MAPPINGS.update(COMPONENT_NODE_CLASS_MAPPINGS)
|
| 9 |
-
# NODE_DISPLAY_NAME_MAPPINGS = GENERAL_NODE_DISPLAY_NAME_MAPPINGS.update(COMPONENT_NODE_DISPLAY_NAME_MAPPINGS)
|
| 10 |
-
|
| 11 |
-
NODE_CLASS_MAPPINGS = {}
|
| 12 |
-
NODE_CLASS_MAPPINGS.update(TEST_NODE_CLASS_MAPPINGS)
|
| 13 |
-
NODE_CLASS_MAPPINGS.update(FLOW_CONTROL_NODE_CLASS_MAPPINGS)
|
| 14 |
-
NODE_CLASS_MAPPINGS.update(UTILITY_NODE_CLASS_MAPPINGS)
|
| 15 |
-
NODE_CLASS_MAPPINGS.update(CONDITION_NODE_CLASS_MAPPINGS)
|
| 16 |
-
NODE_CLASS_MAPPINGS.update(TEST_STUB_NODE_CLASS_MAPPINGS)
|
| 17 |
-
NODE_CLASS_MAPPINGS.update(ASYNC_TEST_NODE_CLASS_MAPPINGS)
|
| 18 |
-
|
| 19 |
-
NODE_DISPLAY_NAME_MAPPINGS = {}
|
| 20 |
-
NODE_DISPLAY_NAME_MAPPINGS.update(TEST_NODE_DISPLAY_NAME_MAPPINGS)
|
| 21 |
-
NODE_DISPLAY_NAME_MAPPINGS.update(FLOW_CONTROL_NODE_DISPLAY_NAME_MAPPINGS)
|
| 22 |
-
NODE_DISPLAY_NAME_MAPPINGS.update(UTILITY_NODE_DISPLAY_NAME_MAPPINGS)
|
| 23 |
-
NODE_DISPLAY_NAME_MAPPINGS.update(CONDITION_NODE_DISPLAY_NAME_MAPPINGS)
|
| 24 |
-
NODE_DISPLAY_NAME_MAPPINGS.update(TEST_STUB_NODE_DISPLAY_NAME_MAPPINGS)
|
| 25 |
-
NODE_DISPLAY_NAME_MAPPINGS.update(ASYNC_TEST_NODE_DISPLAY_NAME_MAPPINGS)
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/testing_nodes/testing-pack/async_test_nodes.py
DELETED
|
@@ -1,343 +0,0 @@
|
|
| 1 |
-
import torch
|
| 2 |
-
import asyncio
|
| 3 |
-
from typing import Dict
|
| 4 |
-
from comfy.utils import ProgressBar
|
| 5 |
-
from comfy_execution.graph_utils import GraphBuilder
|
| 6 |
-
from comfy.comfy_types.node_typing import ComfyNodeABC
|
| 7 |
-
from comfy.comfy_types import IO
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
class TestAsyncValidation(ComfyNodeABC):
|
| 11 |
-
"""Test node with async VALIDATE_INPUTS."""
|
| 12 |
-
|
| 13 |
-
@classmethod
|
| 14 |
-
def INPUT_TYPES(cls):
|
| 15 |
-
return {
|
| 16 |
-
"required": {
|
| 17 |
-
"value": ("FLOAT", {"default": 5.0}),
|
| 18 |
-
"threshold": ("FLOAT", {"default": 10.0}),
|
| 19 |
-
},
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
RETURN_TYPES = ("IMAGE",)
|
| 23 |
-
FUNCTION = "process"
|
| 24 |
-
CATEGORY = "_for_testing/async"
|
| 25 |
-
|
| 26 |
-
@classmethod
|
| 27 |
-
async def VALIDATE_INPUTS(cls, value, threshold):
|
| 28 |
-
# Simulate async validation (e.g., checking remote service)
|
| 29 |
-
await asyncio.sleep(0.05)
|
| 30 |
-
|
| 31 |
-
if value > threshold:
|
| 32 |
-
return f"Value {value} exceeds threshold {threshold}"
|
| 33 |
-
return True
|
| 34 |
-
|
| 35 |
-
def process(self, value, threshold):
|
| 36 |
-
# Create image based on value
|
| 37 |
-
intensity = value / 10.0
|
| 38 |
-
image = torch.ones([1, 512, 512, 3]) * intensity
|
| 39 |
-
return (image,)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
class TestAsyncError(ComfyNodeABC):
|
| 43 |
-
"""Test node that errors during async execution."""
|
| 44 |
-
|
| 45 |
-
@classmethod
|
| 46 |
-
def INPUT_TYPES(cls):
|
| 47 |
-
return {
|
| 48 |
-
"required": {
|
| 49 |
-
"value": (IO.ANY, {}),
|
| 50 |
-
"error_after": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 10.0}),
|
| 51 |
-
},
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
RETURN_TYPES = (IO.ANY,)
|
| 55 |
-
FUNCTION = "error_execution"
|
| 56 |
-
CATEGORY = "_for_testing/async"
|
| 57 |
-
|
| 58 |
-
async def error_execution(self, value, error_after):
|
| 59 |
-
await asyncio.sleep(error_after)
|
| 60 |
-
raise RuntimeError("Intentional async execution error for testing")
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
class TestAsyncValidationError(ComfyNodeABC):
|
| 64 |
-
"""Test node with async validation that always fails."""
|
| 65 |
-
|
| 66 |
-
@classmethod
|
| 67 |
-
def INPUT_TYPES(cls):
|
| 68 |
-
return {
|
| 69 |
-
"required": {
|
| 70 |
-
"value": ("FLOAT", {"default": 5.0}),
|
| 71 |
-
"max_value": ("FLOAT", {"default": 10.0}),
|
| 72 |
-
},
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
RETURN_TYPES = ("IMAGE",)
|
| 76 |
-
FUNCTION = "process"
|
| 77 |
-
CATEGORY = "_for_testing/async"
|
| 78 |
-
|
| 79 |
-
@classmethod
|
| 80 |
-
async def VALIDATE_INPUTS(cls, value, max_value):
|
| 81 |
-
await asyncio.sleep(0.05)
|
| 82 |
-
# Always fail validation for values > max_value
|
| 83 |
-
if value > max_value:
|
| 84 |
-
return f"Async validation failed: {value} > {max_value}"
|
| 85 |
-
return True
|
| 86 |
-
|
| 87 |
-
def process(self, value, max_value):
|
| 88 |
-
# This won't be reached if validation fails
|
| 89 |
-
image = torch.ones([1, 512, 512, 3]) * (value / max_value)
|
| 90 |
-
return (image,)
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
class TestAsyncTimeout(ComfyNodeABC):
|
| 94 |
-
"""Test node that simulates timeout scenarios."""
|
| 95 |
-
|
| 96 |
-
@classmethod
|
| 97 |
-
def INPUT_TYPES(cls):
|
| 98 |
-
return {
|
| 99 |
-
"required": {
|
| 100 |
-
"value": (IO.ANY, {}),
|
| 101 |
-
"timeout": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0}),
|
| 102 |
-
"operation_time": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 10.0}),
|
| 103 |
-
},
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
RETURN_TYPES = (IO.ANY,)
|
| 107 |
-
FUNCTION = "timeout_execution"
|
| 108 |
-
CATEGORY = "_for_testing/async"
|
| 109 |
-
|
| 110 |
-
async def timeout_execution(self, value, timeout, operation_time):
|
| 111 |
-
try:
|
| 112 |
-
# This will timeout if operation_time > timeout
|
| 113 |
-
await asyncio.wait_for(asyncio.sleep(operation_time), timeout=timeout)
|
| 114 |
-
return (value,)
|
| 115 |
-
except asyncio.TimeoutError:
|
| 116 |
-
raise RuntimeError(f"Operation timed out after {timeout} seconds")
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
class TestSyncError(ComfyNodeABC):
|
| 120 |
-
"""Test node that errors synchronously (for mixed sync/async testing)."""
|
| 121 |
-
|
| 122 |
-
@classmethod
|
| 123 |
-
def INPUT_TYPES(cls):
|
| 124 |
-
return {
|
| 125 |
-
"required": {
|
| 126 |
-
"value": (IO.ANY, {}),
|
| 127 |
-
},
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
RETURN_TYPES = (IO.ANY,)
|
| 131 |
-
FUNCTION = "sync_error"
|
| 132 |
-
CATEGORY = "_for_testing/async"
|
| 133 |
-
|
| 134 |
-
def sync_error(self, value):
|
| 135 |
-
raise RuntimeError("Intentional sync execution error for testing")
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
class TestAsyncLazyCheck(ComfyNodeABC):
|
| 139 |
-
"""Test node with async check_lazy_status."""
|
| 140 |
-
|
| 141 |
-
@classmethod
|
| 142 |
-
def INPUT_TYPES(cls):
|
| 143 |
-
return {
|
| 144 |
-
"required": {
|
| 145 |
-
"input1": (IO.ANY, {"lazy": True}),
|
| 146 |
-
"input2": (IO.ANY, {"lazy": True}),
|
| 147 |
-
"condition": ("BOOLEAN", {"default": True}),
|
| 148 |
-
},
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
RETURN_TYPES = ("IMAGE",)
|
| 152 |
-
FUNCTION = "process"
|
| 153 |
-
CATEGORY = "_for_testing/async"
|
| 154 |
-
|
| 155 |
-
async def check_lazy_status(self, condition, input1, input2):
|
| 156 |
-
# Simulate async checking (e.g., querying remote service)
|
| 157 |
-
await asyncio.sleep(0.05)
|
| 158 |
-
|
| 159 |
-
needed = []
|
| 160 |
-
if condition and input1 is None:
|
| 161 |
-
needed.append("input1")
|
| 162 |
-
if not condition and input2 is None:
|
| 163 |
-
needed.append("input2")
|
| 164 |
-
return needed
|
| 165 |
-
|
| 166 |
-
def process(self, input1, input2, condition):
|
| 167 |
-
# Return a simple image
|
| 168 |
-
return (torch.ones([1, 512, 512, 3]),)
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
class TestDynamicAsyncGeneration(ComfyNodeABC):
|
| 172 |
-
"""Test node that dynamically generates async nodes."""
|
| 173 |
-
|
| 174 |
-
@classmethod
|
| 175 |
-
def INPUT_TYPES(cls):
|
| 176 |
-
return {
|
| 177 |
-
"required": {
|
| 178 |
-
"image1": ("IMAGE",),
|
| 179 |
-
"image2": ("IMAGE",),
|
| 180 |
-
"num_async_nodes": ("INT", {"default": 3, "min": 1, "max": 10}),
|
| 181 |
-
"sleep_duration": ("FLOAT", {"default": 0.2, "min": 0.1, "max": 1.0}),
|
| 182 |
-
},
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
RETURN_TYPES = ("IMAGE",)
|
| 186 |
-
FUNCTION = "generate_async_workflow"
|
| 187 |
-
CATEGORY = "_for_testing/async"
|
| 188 |
-
|
| 189 |
-
def generate_async_workflow(self, image1, image2, num_async_nodes, sleep_duration):
|
| 190 |
-
g = GraphBuilder()
|
| 191 |
-
|
| 192 |
-
# Create multiple async sleep nodes
|
| 193 |
-
sleep_nodes = []
|
| 194 |
-
for i in range(num_async_nodes):
|
| 195 |
-
image = image1 if i % 2 == 0 else image2
|
| 196 |
-
sleep_node = g.node("TestSleep", value=image, seconds=sleep_duration)
|
| 197 |
-
sleep_nodes.append(sleep_node)
|
| 198 |
-
|
| 199 |
-
# Average all results
|
| 200 |
-
if len(sleep_nodes) == 1:
|
| 201 |
-
final_node = sleep_nodes[0]
|
| 202 |
-
else:
|
| 203 |
-
avg_inputs = {"input1": sleep_nodes[0].out(0)}
|
| 204 |
-
for i, node in enumerate(sleep_nodes[1:], 2):
|
| 205 |
-
avg_inputs[f"input{i}"] = node.out(0)
|
| 206 |
-
final_node = g.node("TestVariadicAverage", **avg_inputs)
|
| 207 |
-
|
| 208 |
-
return {
|
| 209 |
-
"result": (final_node.out(0),),
|
| 210 |
-
"expand": g.finalize(),
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
class TestAsyncResourceUser(ComfyNodeABC):
|
| 215 |
-
"""Test node that uses resources during async execution."""
|
| 216 |
-
|
| 217 |
-
# Class-level resource tracking for testing
|
| 218 |
-
_active_resources: Dict[str, bool] = {}
|
| 219 |
-
|
| 220 |
-
@classmethod
|
| 221 |
-
def INPUT_TYPES(cls):
|
| 222 |
-
return {
|
| 223 |
-
"required": {
|
| 224 |
-
"value": (IO.ANY, {}),
|
| 225 |
-
"resource_id": ("STRING", {"default": "resource_0"}),
|
| 226 |
-
"duration": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0}),
|
| 227 |
-
},
|
| 228 |
-
}
|
| 229 |
-
|
| 230 |
-
RETURN_TYPES = (IO.ANY,)
|
| 231 |
-
FUNCTION = "use_resource"
|
| 232 |
-
CATEGORY = "_for_testing/async"
|
| 233 |
-
|
| 234 |
-
async def use_resource(self, value, resource_id, duration):
|
| 235 |
-
# Check if resource is already in use
|
| 236 |
-
if self._active_resources.get(resource_id, False):
|
| 237 |
-
raise RuntimeError(f"Resource {resource_id} is already in use!")
|
| 238 |
-
|
| 239 |
-
# Mark resource as in use
|
| 240 |
-
self._active_resources[resource_id] = True
|
| 241 |
-
|
| 242 |
-
try:
|
| 243 |
-
# Simulate resource usage
|
| 244 |
-
await asyncio.sleep(duration)
|
| 245 |
-
return (value,)
|
| 246 |
-
finally:
|
| 247 |
-
# Always clean up resource
|
| 248 |
-
self._active_resources[resource_id] = False
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
class TestAsyncBatchProcessing(ComfyNodeABC):
|
| 252 |
-
"""Test async processing of batched inputs."""
|
| 253 |
-
|
| 254 |
-
@classmethod
|
| 255 |
-
def INPUT_TYPES(cls):
|
| 256 |
-
return {
|
| 257 |
-
"required": {
|
| 258 |
-
"images": ("IMAGE",),
|
| 259 |
-
"process_time_per_item": ("FLOAT", {"default": 0.1, "min": 0.01, "max": 1.0}),
|
| 260 |
-
},
|
| 261 |
-
"hidden": {
|
| 262 |
-
"unique_id": "UNIQUE_ID",
|
| 263 |
-
},
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
RETURN_TYPES = ("IMAGE",)
|
| 267 |
-
FUNCTION = "process_batch"
|
| 268 |
-
CATEGORY = "_for_testing/async"
|
| 269 |
-
|
| 270 |
-
async def process_batch(self, images, process_time_per_item, unique_id):
|
| 271 |
-
batch_size = images.shape[0]
|
| 272 |
-
pbar = ProgressBar(batch_size, node_id=unique_id)
|
| 273 |
-
|
| 274 |
-
# Process each image in the batch
|
| 275 |
-
processed = []
|
| 276 |
-
for i in range(batch_size):
|
| 277 |
-
# Simulate async processing
|
| 278 |
-
await asyncio.sleep(process_time_per_item)
|
| 279 |
-
|
| 280 |
-
# Simple processing: invert the image
|
| 281 |
-
processed_image = 1.0 - images[i:i+1]
|
| 282 |
-
processed.append(processed_image)
|
| 283 |
-
|
| 284 |
-
pbar.update(1)
|
| 285 |
-
|
| 286 |
-
# Stack processed images
|
| 287 |
-
result = torch.cat(processed, dim=0)
|
| 288 |
-
return (result,)
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
class TestAsyncConcurrentLimit(ComfyNodeABC):
|
| 292 |
-
"""Test concurrent execution limits for async nodes."""
|
| 293 |
-
|
| 294 |
-
_semaphore = asyncio.Semaphore(2) # Only allow 2 concurrent executions
|
| 295 |
-
|
| 296 |
-
@classmethod
|
| 297 |
-
def INPUT_TYPES(cls):
|
| 298 |
-
return {
|
| 299 |
-
"required": {
|
| 300 |
-
"value": (IO.ANY, {}),
|
| 301 |
-
"duration": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 2.0}),
|
| 302 |
-
"node_id": ("INT", {"default": 0}),
|
| 303 |
-
},
|
| 304 |
-
}
|
| 305 |
-
|
| 306 |
-
RETURN_TYPES = (IO.ANY,)
|
| 307 |
-
FUNCTION = "limited_execution"
|
| 308 |
-
CATEGORY = "_for_testing/async"
|
| 309 |
-
|
| 310 |
-
async def limited_execution(self, value, duration, node_id):
|
| 311 |
-
async with self._semaphore:
|
| 312 |
-
# Node {node_id} acquired semaphore
|
| 313 |
-
await asyncio.sleep(duration)
|
| 314 |
-
# Node {node_id} releasing semaphore
|
| 315 |
-
return (value,)
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
# Add node mappings
|
| 319 |
-
ASYNC_TEST_NODE_CLASS_MAPPINGS = {
|
| 320 |
-
"TestAsyncValidation": TestAsyncValidation,
|
| 321 |
-
"TestAsyncError": TestAsyncError,
|
| 322 |
-
"TestAsyncValidationError": TestAsyncValidationError,
|
| 323 |
-
"TestAsyncTimeout": TestAsyncTimeout,
|
| 324 |
-
"TestSyncError": TestSyncError,
|
| 325 |
-
"TestAsyncLazyCheck": TestAsyncLazyCheck,
|
| 326 |
-
"TestDynamicAsyncGeneration": TestDynamicAsyncGeneration,
|
| 327 |
-
"TestAsyncResourceUser": TestAsyncResourceUser,
|
| 328 |
-
"TestAsyncBatchProcessing": TestAsyncBatchProcessing,
|
| 329 |
-
"TestAsyncConcurrentLimit": TestAsyncConcurrentLimit,
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
ASYNC_TEST_NODE_DISPLAY_NAME_MAPPINGS = {
|
| 333 |
-
"TestAsyncValidation": "Test Async Validation",
|
| 334 |
-
"TestAsyncError": "Test Async Error",
|
| 335 |
-
"TestAsyncValidationError": "Test Async Validation Error",
|
| 336 |
-
"TestAsyncTimeout": "Test Async Timeout",
|
| 337 |
-
"TestSyncError": "Test Sync Error",
|
| 338 |
-
"TestAsyncLazyCheck": "Test Async Lazy Check",
|
| 339 |
-
"TestDynamicAsyncGeneration": "Test Dynamic Async Generation",
|
| 340 |
-
"TestAsyncResourceUser": "Test Async Resource User",
|
| 341 |
-
"TestAsyncBatchProcessing": "Test Async Batch Processing",
|
| 342 |
-
"TestAsyncConcurrentLimit": "Test Async Concurrent Limit",
|
| 343 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/testing_nodes/testing-pack/conditions.py
DELETED
|
@@ -1,194 +0,0 @@
|
|
| 1 |
-
import re
|
| 2 |
-
import torch
|
| 3 |
-
|
| 4 |
-
class TestIntConditions:
|
| 5 |
-
def __init__(self):
|
| 6 |
-
pass
|
| 7 |
-
|
| 8 |
-
@classmethod
|
| 9 |
-
def INPUT_TYPES(cls):
|
| 10 |
-
return {
|
| 11 |
-
"required": {
|
| 12 |
-
"a": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
|
| 13 |
-
"b": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
|
| 14 |
-
"operation": (["==", "!=", "<", ">", "<=", ">="],),
|
| 15 |
-
},
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
RETURN_TYPES = ("BOOLEAN",)
|
| 19 |
-
FUNCTION = "int_condition"
|
| 20 |
-
|
| 21 |
-
CATEGORY = "Testing/Logic"
|
| 22 |
-
|
| 23 |
-
def int_condition(self, a, b, operation):
|
| 24 |
-
if operation == "==":
|
| 25 |
-
return (a == b,)
|
| 26 |
-
elif operation == "!=":
|
| 27 |
-
return (a != b,)
|
| 28 |
-
elif operation == "<":
|
| 29 |
-
return (a < b,)
|
| 30 |
-
elif operation == ">":
|
| 31 |
-
return (a > b,)
|
| 32 |
-
elif operation == "<=":
|
| 33 |
-
return (a <= b,)
|
| 34 |
-
elif operation == ">=":
|
| 35 |
-
return (a >= b,)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
class TestFloatConditions:
|
| 39 |
-
def __init__(self):
|
| 40 |
-
pass
|
| 41 |
-
|
| 42 |
-
@classmethod
|
| 43 |
-
def INPUT_TYPES(cls):
|
| 44 |
-
return {
|
| 45 |
-
"required": {
|
| 46 |
-
"a": ("FLOAT", {"default": 0, "min": -999999999999.0, "max": 999999999999.0, "step": 1}),
|
| 47 |
-
"b": ("FLOAT", {"default": 0, "min": -999999999999.0, "max": 999999999999.0, "step": 1}),
|
| 48 |
-
"operation": (["==", "!=", "<", ">", "<=", ">="],),
|
| 49 |
-
},
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
RETURN_TYPES = ("BOOLEAN",)
|
| 53 |
-
FUNCTION = "float_condition"
|
| 54 |
-
|
| 55 |
-
CATEGORY = "Testing/Logic"
|
| 56 |
-
|
| 57 |
-
def float_condition(self, a, b, operation):
|
| 58 |
-
if operation == "==":
|
| 59 |
-
return (a == b,)
|
| 60 |
-
elif operation == "!=":
|
| 61 |
-
return (a != b,)
|
| 62 |
-
elif operation == "<":
|
| 63 |
-
return (a < b,)
|
| 64 |
-
elif operation == ">":
|
| 65 |
-
return (a > b,)
|
| 66 |
-
elif operation == "<=":
|
| 67 |
-
return (a <= b,)
|
| 68 |
-
elif operation == ">=":
|
| 69 |
-
return (a >= b,)
|
| 70 |
-
|
| 71 |
-
class TestStringConditions:
|
| 72 |
-
def __init__(self):
|
| 73 |
-
pass
|
| 74 |
-
|
| 75 |
-
@classmethod
|
| 76 |
-
def INPUT_TYPES(cls):
|
| 77 |
-
return {
|
| 78 |
-
"required": {
|
| 79 |
-
"a": ("STRING", {"multiline": False}),
|
| 80 |
-
"b": ("STRING", {"multiline": False}),
|
| 81 |
-
"operation": (["a == b", "a != b", "a IN b", "a MATCH REGEX(b)", "a BEGINSWITH b", "a ENDSWITH b"],),
|
| 82 |
-
"case_sensitive": ("BOOLEAN", {"default": True}),
|
| 83 |
-
},
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
RETURN_TYPES = ("BOOLEAN",)
|
| 87 |
-
FUNCTION = "string_condition"
|
| 88 |
-
|
| 89 |
-
CATEGORY = "Testing/Logic"
|
| 90 |
-
|
| 91 |
-
def string_condition(self, a, b, operation, case_sensitive):
|
| 92 |
-
if not case_sensitive:
|
| 93 |
-
a = a.lower()
|
| 94 |
-
b = b.lower()
|
| 95 |
-
|
| 96 |
-
if operation == "a == b":
|
| 97 |
-
return (a == b,)
|
| 98 |
-
elif operation == "a != b":
|
| 99 |
-
return (a != b,)
|
| 100 |
-
elif operation == "a IN b":
|
| 101 |
-
return (a in b,)
|
| 102 |
-
elif operation == "a MATCH REGEX(b)":
|
| 103 |
-
try:
|
| 104 |
-
return (re.match(b, a) is not None,)
|
| 105 |
-
except:
|
| 106 |
-
return (False,)
|
| 107 |
-
elif operation == "a BEGINSWITH b":
|
| 108 |
-
return (a.startswith(b),)
|
| 109 |
-
elif operation == "a ENDSWITH b":
|
| 110 |
-
return (a.endswith(b),)
|
| 111 |
-
|
| 112 |
-
class TestToBoolNode:
|
| 113 |
-
def __init__(self):
|
| 114 |
-
pass
|
| 115 |
-
|
| 116 |
-
@classmethod
|
| 117 |
-
def INPUT_TYPES(cls):
|
| 118 |
-
return {
|
| 119 |
-
"required": {
|
| 120 |
-
"value": ("*",),
|
| 121 |
-
},
|
| 122 |
-
"optional": {
|
| 123 |
-
"invert": ("BOOLEAN", {"default": False}),
|
| 124 |
-
},
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
RETURN_TYPES = ("BOOLEAN",)
|
| 128 |
-
FUNCTION = "to_bool"
|
| 129 |
-
|
| 130 |
-
CATEGORY = "Testing/Logic"
|
| 131 |
-
|
| 132 |
-
def to_bool(self, value, invert = False):
|
| 133 |
-
if isinstance(value, torch.Tensor):
|
| 134 |
-
if value.max().item() == 0 and value.min().item() == 0:
|
| 135 |
-
result = False
|
| 136 |
-
else:
|
| 137 |
-
result = True
|
| 138 |
-
else:
|
| 139 |
-
try:
|
| 140 |
-
result = bool(value)
|
| 141 |
-
except:
|
| 142 |
-
# Can't convert it? Well then it's something or other. I dunno, I'm not a Python programmer.
|
| 143 |
-
result = True
|
| 144 |
-
|
| 145 |
-
if invert:
|
| 146 |
-
result = not result
|
| 147 |
-
|
| 148 |
-
return (result,)
|
| 149 |
-
|
| 150 |
-
class TestBoolOperationNode:
|
| 151 |
-
def __init__(self):
|
| 152 |
-
pass
|
| 153 |
-
|
| 154 |
-
@classmethod
|
| 155 |
-
def INPUT_TYPES(cls):
|
| 156 |
-
return {
|
| 157 |
-
"required": {
|
| 158 |
-
"a": ("BOOLEAN",),
|
| 159 |
-
"b": ("BOOLEAN",),
|
| 160 |
-
"op": (["a AND b", "a OR b", "a XOR b", "NOT a"],),
|
| 161 |
-
},
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
RETURN_TYPES = ("BOOLEAN",)
|
| 165 |
-
FUNCTION = "bool_operation"
|
| 166 |
-
|
| 167 |
-
CATEGORY = "Testing/Logic"
|
| 168 |
-
|
| 169 |
-
def bool_operation(self, a, b, op):
|
| 170 |
-
if op == "a AND b":
|
| 171 |
-
return (a and b,)
|
| 172 |
-
elif op == "a OR b":
|
| 173 |
-
return (a or b,)
|
| 174 |
-
elif op == "a XOR b":
|
| 175 |
-
return (a ^ b,)
|
| 176 |
-
elif op == "NOT a":
|
| 177 |
-
return (not a,)
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
CONDITION_NODE_CLASS_MAPPINGS = {
|
| 181 |
-
"TestIntConditions": TestIntConditions,
|
| 182 |
-
"TestFloatConditions": TestFloatConditions,
|
| 183 |
-
"TestStringConditions": TestStringConditions,
|
| 184 |
-
"TestToBoolNode": TestToBoolNode,
|
| 185 |
-
"TestBoolOperationNode": TestBoolOperationNode,
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
CONDITION_NODE_DISPLAY_NAME_MAPPINGS = {
|
| 189 |
-
"TestIntConditions": "Int Condition",
|
| 190 |
-
"TestFloatConditions": "Float Condition",
|
| 191 |
-
"TestStringConditions": "String Condition",
|
| 192 |
-
"TestToBoolNode": "To Bool",
|
| 193 |
-
"TestBoolOperationNode": "Bool Operation",
|
| 194 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/testing_nodes/testing-pack/flow_control.py
DELETED
|
@@ -1,173 +0,0 @@
|
|
| 1 |
-
from comfy_execution.graph_utils import GraphBuilder, is_link
|
| 2 |
-
from comfy_execution.graph import ExecutionBlocker
|
| 3 |
-
from .tools import VariantSupport
|
| 4 |
-
|
| 5 |
-
NUM_FLOW_SOCKETS = 5
|
| 6 |
-
@VariantSupport()
|
| 7 |
-
class TestWhileLoopOpen:
|
| 8 |
-
def __init__(self):
|
| 9 |
-
pass
|
| 10 |
-
|
| 11 |
-
@classmethod
|
| 12 |
-
def INPUT_TYPES(cls):
|
| 13 |
-
inputs = {
|
| 14 |
-
"required": {
|
| 15 |
-
"condition": ("BOOLEAN", {"default": True}),
|
| 16 |
-
},
|
| 17 |
-
"optional": {
|
| 18 |
-
},
|
| 19 |
-
}
|
| 20 |
-
for i in range(NUM_FLOW_SOCKETS):
|
| 21 |
-
inputs["optional"][f"initial_value{i}"] = ("*",)
|
| 22 |
-
return inputs
|
| 23 |
-
|
| 24 |
-
RETURN_TYPES = tuple(["FLOW_CONTROL"] + ["*"] * NUM_FLOW_SOCKETS)
|
| 25 |
-
RETURN_NAMES = tuple(["FLOW_CONTROL"] + [f"value{i}" for i in range(NUM_FLOW_SOCKETS)])
|
| 26 |
-
FUNCTION = "while_loop_open"
|
| 27 |
-
|
| 28 |
-
CATEGORY = "Testing/Flow"
|
| 29 |
-
|
| 30 |
-
def while_loop_open(self, condition, **kwargs):
|
| 31 |
-
values = []
|
| 32 |
-
for i in range(NUM_FLOW_SOCKETS):
|
| 33 |
-
values.append(kwargs.get(f"initial_value{i}", None))
|
| 34 |
-
return tuple(["stub"] + values)
|
| 35 |
-
|
| 36 |
-
@VariantSupport()
|
| 37 |
-
class TestWhileLoopClose:
|
| 38 |
-
def __init__(self):
|
| 39 |
-
pass
|
| 40 |
-
|
| 41 |
-
@classmethod
|
| 42 |
-
def INPUT_TYPES(cls):
|
| 43 |
-
inputs = {
|
| 44 |
-
"required": {
|
| 45 |
-
"flow_control": ("FLOW_CONTROL", {"rawLink": True}),
|
| 46 |
-
"condition": ("BOOLEAN", {"forceInput": True}),
|
| 47 |
-
},
|
| 48 |
-
"optional": {
|
| 49 |
-
},
|
| 50 |
-
"hidden": {
|
| 51 |
-
"dynprompt": "DYNPROMPT",
|
| 52 |
-
"unique_id": "UNIQUE_ID",
|
| 53 |
-
}
|
| 54 |
-
}
|
| 55 |
-
for i in range(NUM_FLOW_SOCKETS):
|
| 56 |
-
inputs["optional"][f"initial_value{i}"] = ("*",)
|
| 57 |
-
return inputs
|
| 58 |
-
|
| 59 |
-
RETURN_TYPES = tuple(["*"] * NUM_FLOW_SOCKETS)
|
| 60 |
-
RETURN_NAMES = tuple([f"value{i}" for i in range(NUM_FLOW_SOCKETS)])
|
| 61 |
-
FUNCTION = "while_loop_close"
|
| 62 |
-
|
| 63 |
-
CATEGORY = "Testing/Flow"
|
| 64 |
-
|
| 65 |
-
def explore_dependencies(self, node_id, dynprompt, upstream):
|
| 66 |
-
node_info = dynprompt.get_node(node_id)
|
| 67 |
-
if "inputs" not in node_info:
|
| 68 |
-
return
|
| 69 |
-
for k, v in node_info["inputs"].items():
|
| 70 |
-
if is_link(v):
|
| 71 |
-
parent_id = v[0]
|
| 72 |
-
if parent_id not in upstream:
|
| 73 |
-
upstream[parent_id] = []
|
| 74 |
-
self.explore_dependencies(parent_id, dynprompt, upstream)
|
| 75 |
-
upstream[parent_id].append(node_id)
|
| 76 |
-
|
| 77 |
-
def collect_contained(self, node_id, upstream, contained):
|
| 78 |
-
if node_id not in upstream:
|
| 79 |
-
return
|
| 80 |
-
for child_id in upstream[node_id]:
|
| 81 |
-
if child_id not in contained:
|
| 82 |
-
contained[child_id] = True
|
| 83 |
-
self.collect_contained(child_id, upstream, contained)
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def while_loop_close(self, flow_control, condition, dynprompt=None, unique_id=None, **kwargs):
|
| 87 |
-
assert dynprompt is not None
|
| 88 |
-
if not condition:
|
| 89 |
-
# We're done with the loop
|
| 90 |
-
values = []
|
| 91 |
-
for i in range(NUM_FLOW_SOCKETS):
|
| 92 |
-
values.append(kwargs.get(f"initial_value{i}", None))
|
| 93 |
-
return tuple(values)
|
| 94 |
-
|
| 95 |
-
# We want to loop
|
| 96 |
-
upstream = {}
|
| 97 |
-
# Get the list of all nodes between the open and close nodes
|
| 98 |
-
self.explore_dependencies(unique_id, dynprompt, upstream)
|
| 99 |
-
|
| 100 |
-
contained = {}
|
| 101 |
-
open_node = flow_control[0]
|
| 102 |
-
self.collect_contained(open_node, upstream, contained)
|
| 103 |
-
contained[unique_id] = True
|
| 104 |
-
contained[open_node] = True
|
| 105 |
-
|
| 106 |
-
# We'll use the default prefix, but to avoid having node names grow exponentially in size,
|
| 107 |
-
# we'll use "Recurse" for the name of the recursively-generated copy of this node.
|
| 108 |
-
graph = GraphBuilder()
|
| 109 |
-
for node_id in contained:
|
| 110 |
-
original_node = dynprompt.get_node(node_id)
|
| 111 |
-
node = graph.node(original_node["class_type"], "Recurse" if node_id == unique_id else node_id)
|
| 112 |
-
node.set_override_display_id(node_id)
|
| 113 |
-
for node_id in contained:
|
| 114 |
-
original_node = dynprompt.get_node(node_id)
|
| 115 |
-
node = graph.lookup_node("Recurse" if node_id == unique_id else node_id)
|
| 116 |
-
assert node is not None
|
| 117 |
-
for k, v in original_node["inputs"].items():
|
| 118 |
-
if is_link(v) and v[0] in contained:
|
| 119 |
-
parent = graph.lookup_node(v[0])
|
| 120 |
-
assert parent is not None
|
| 121 |
-
node.set_input(k, parent.out(v[1]))
|
| 122 |
-
else:
|
| 123 |
-
node.set_input(k, v)
|
| 124 |
-
new_open = graph.lookup_node(open_node)
|
| 125 |
-
assert new_open is not None
|
| 126 |
-
for i in range(NUM_FLOW_SOCKETS):
|
| 127 |
-
key = f"initial_value{i}"
|
| 128 |
-
new_open.set_input(key, kwargs.get(key, None))
|
| 129 |
-
my_clone = graph.lookup_node("Recurse")
|
| 130 |
-
assert my_clone is not None
|
| 131 |
-
result = map(lambda x: my_clone.out(x), range(NUM_FLOW_SOCKETS))
|
| 132 |
-
return {
|
| 133 |
-
"result": tuple(result),
|
| 134 |
-
"expand": graph.finalize(),
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
@VariantSupport()
|
| 138 |
-
class TestExecutionBlockerNode:
|
| 139 |
-
def __init__(self):
|
| 140 |
-
pass
|
| 141 |
-
|
| 142 |
-
@classmethod
|
| 143 |
-
def INPUT_TYPES(cls):
|
| 144 |
-
inputs = {
|
| 145 |
-
"required": {
|
| 146 |
-
"input": ("*",),
|
| 147 |
-
"block": ("BOOLEAN",),
|
| 148 |
-
"verbose": ("BOOLEAN", {"default": False}),
|
| 149 |
-
},
|
| 150 |
-
}
|
| 151 |
-
return inputs
|
| 152 |
-
|
| 153 |
-
RETURN_TYPES = ("*",)
|
| 154 |
-
RETURN_NAMES = ("output",)
|
| 155 |
-
FUNCTION = "execution_blocker"
|
| 156 |
-
|
| 157 |
-
CATEGORY = "Testing/Flow"
|
| 158 |
-
|
| 159 |
-
def execution_blocker(self, input, block, verbose):
|
| 160 |
-
if block:
|
| 161 |
-
return (ExecutionBlocker("Blocked Execution" if verbose else None),)
|
| 162 |
-
return (input,)
|
| 163 |
-
|
| 164 |
-
FLOW_CONTROL_NODE_CLASS_MAPPINGS = {
|
| 165 |
-
"TestWhileLoopOpen": TestWhileLoopOpen,
|
| 166 |
-
"TestWhileLoopClose": TestWhileLoopClose,
|
| 167 |
-
"TestExecutionBlocker": TestExecutionBlockerNode,
|
| 168 |
-
}
|
| 169 |
-
FLOW_CONTROL_NODE_DISPLAY_NAME_MAPPINGS = {
|
| 170 |
-
"TestWhileLoopOpen": "While Loop Open",
|
| 171 |
-
"TestWhileLoopClose": "While Loop Close",
|
| 172 |
-
"TestExecutionBlocker": "Execution Blocker",
|
| 173 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/testing_nodes/testing-pack/specific_tests.py
DELETED
|
@@ -1,498 +0,0 @@
|
|
| 1 |
-
import torch
|
| 2 |
-
import time
|
| 3 |
-
import asyncio
|
| 4 |
-
from comfy.utils import ProgressBar
|
| 5 |
-
from .tools import VariantSupport
|
| 6 |
-
from comfy_execution.graph_utils import GraphBuilder
|
| 7 |
-
from comfy.comfy_types.node_typing import ComfyNodeABC
|
| 8 |
-
from comfy.comfy_types import IO
|
| 9 |
-
|
| 10 |
-
class TestLazyMixImages:
|
| 11 |
-
@classmethod
|
| 12 |
-
def INPUT_TYPES(cls):
|
| 13 |
-
return {
|
| 14 |
-
"required": {
|
| 15 |
-
"image1": ("IMAGE",{"lazy": True}),
|
| 16 |
-
"image2": ("IMAGE",{"lazy": True}),
|
| 17 |
-
"mask": ("MASK",),
|
| 18 |
-
},
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
RETURN_TYPES = ("IMAGE",)
|
| 22 |
-
FUNCTION = "mix"
|
| 23 |
-
|
| 24 |
-
CATEGORY = "Testing/Nodes"
|
| 25 |
-
|
| 26 |
-
def check_lazy_status(self, mask, image1, image2):
|
| 27 |
-
mask_min = mask.min()
|
| 28 |
-
mask_max = mask.max()
|
| 29 |
-
needed = []
|
| 30 |
-
if image1 is None and (mask_min != 1.0 or mask_max != 1.0):
|
| 31 |
-
needed.append("image1")
|
| 32 |
-
if image2 is None and (mask_min != 0.0 or mask_max != 0.0):
|
| 33 |
-
needed.append("image2")
|
| 34 |
-
return needed
|
| 35 |
-
|
| 36 |
-
# Not trying to handle different batch sizes here just to keep the demo simple
|
| 37 |
-
def mix(self, mask, image1, image2):
|
| 38 |
-
mask_min = mask.min()
|
| 39 |
-
mask_max = mask.max()
|
| 40 |
-
if mask_min == 0.0 and mask_max == 0.0:
|
| 41 |
-
return (image1,)
|
| 42 |
-
elif mask_min == 1.0 and mask_max == 1.0:
|
| 43 |
-
return (image2,)
|
| 44 |
-
|
| 45 |
-
if len(mask.shape) == 2:
|
| 46 |
-
mask = mask.unsqueeze(0)
|
| 47 |
-
if len(mask.shape) == 3:
|
| 48 |
-
mask = mask.unsqueeze(3)
|
| 49 |
-
if mask.shape[3] < image1.shape[3]:
|
| 50 |
-
mask = mask.repeat(1, 1, 1, image1.shape[3])
|
| 51 |
-
|
| 52 |
-
result = image1 * (1. - mask) + image2 * mask,
|
| 53 |
-
return (result[0],)
|
| 54 |
-
|
| 55 |
-
class TestVariadicAverage:
|
| 56 |
-
@classmethod
|
| 57 |
-
def INPUT_TYPES(cls):
|
| 58 |
-
return {
|
| 59 |
-
"required": {
|
| 60 |
-
"input1": ("IMAGE",),
|
| 61 |
-
},
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
RETURN_TYPES = ("IMAGE",)
|
| 65 |
-
FUNCTION = "variadic_average"
|
| 66 |
-
|
| 67 |
-
CATEGORY = "Testing/Nodes"
|
| 68 |
-
|
| 69 |
-
def variadic_average(self, input1, **kwargs):
|
| 70 |
-
inputs = [input1]
|
| 71 |
-
while 'input' + str(len(inputs) + 1) in kwargs:
|
| 72 |
-
inputs.append(kwargs['input' + str(len(inputs) + 1)])
|
| 73 |
-
return (torch.stack(inputs).mean(dim=0),)
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
class TestCustomIsChanged:
|
| 77 |
-
@classmethod
|
| 78 |
-
def INPUT_TYPES(cls):
|
| 79 |
-
return {
|
| 80 |
-
"required": {
|
| 81 |
-
"image": ("IMAGE",),
|
| 82 |
-
},
|
| 83 |
-
"optional": {
|
| 84 |
-
"should_change": ("BOOL", {"default": False}),
|
| 85 |
-
},
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
RETURN_TYPES = ("IMAGE",)
|
| 89 |
-
FUNCTION = "custom_is_changed"
|
| 90 |
-
|
| 91 |
-
CATEGORY = "Testing/Nodes"
|
| 92 |
-
|
| 93 |
-
def custom_is_changed(self, image, should_change=False):
|
| 94 |
-
return (image,)
|
| 95 |
-
|
| 96 |
-
@classmethod
|
| 97 |
-
def IS_CHANGED(cls, should_change=False, *args, **kwargs):
|
| 98 |
-
if should_change:
|
| 99 |
-
return float("NaN")
|
| 100 |
-
else:
|
| 101 |
-
return False
|
| 102 |
-
|
| 103 |
-
class TestIsChangedWithConstants:
|
| 104 |
-
@classmethod
|
| 105 |
-
def INPUT_TYPES(cls):
|
| 106 |
-
return {
|
| 107 |
-
"required": {
|
| 108 |
-
"image": ("IMAGE",),
|
| 109 |
-
"value": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0}),
|
| 110 |
-
},
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
RETURN_TYPES = ("IMAGE",)
|
| 114 |
-
FUNCTION = "custom_is_changed"
|
| 115 |
-
|
| 116 |
-
CATEGORY = "Testing/Nodes"
|
| 117 |
-
|
| 118 |
-
def custom_is_changed(self, image, value):
|
| 119 |
-
return (image * value,)
|
| 120 |
-
|
| 121 |
-
@classmethod
|
| 122 |
-
def IS_CHANGED(cls, image, value):
|
| 123 |
-
if image is None:
|
| 124 |
-
return value
|
| 125 |
-
else:
|
| 126 |
-
return image.mean().item() * value
|
| 127 |
-
|
| 128 |
-
class TestCustomValidation1:
|
| 129 |
-
@classmethod
|
| 130 |
-
def INPUT_TYPES(cls):
|
| 131 |
-
return {
|
| 132 |
-
"required": {
|
| 133 |
-
"input1": ("IMAGE,FLOAT",),
|
| 134 |
-
"input2": ("IMAGE,FLOAT",),
|
| 135 |
-
},
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
RETURN_TYPES = ("IMAGE",)
|
| 139 |
-
FUNCTION = "custom_validation1"
|
| 140 |
-
|
| 141 |
-
CATEGORY = "Testing/Nodes"
|
| 142 |
-
|
| 143 |
-
def custom_validation1(self, input1, input2):
|
| 144 |
-
if isinstance(input1, float) and isinstance(input2, float):
|
| 145 |
-
result = torch.ones([1, 512, 512, 3]) * input1 * input2
|
| 146 |
-
else:
|
| 147 |
-
result = input1 * input2
|
| 148 |
-
return (result,)
|
| 149 |
-
|
| 150 |
-
@classmethod
|
| 151 |
-
def VALIDATE_INPUTS(cls, input1=None, input2=None):
|
| 152 |
-
if input1 is not None:
|
| 153 |
-
if not isinstance(input1, (torch.Tensor, float)):
|
| 154 |
-
return f"Invalid type of input1: {type(input1)}"
|
| 155 |
-
if input2 is not None:
|
| 156 |
-
if not isinstance(input2, (torch.Tensor, float)):
|
| 157 |
-
return f"Invalid type of input2: {type(input2)}"
|
| 158 |
-
|
| 159 |
-
return True
|
| 160 |
-
|
| 161 |
-
class TestCustomValidation2:
|
| 162 |
-
@classmethod
|
| 163 |
-
def INPUT_TYPES(cls):
|
| 164 |
-
return {
|
| 165 |
-
"required": {
|
| 166 |
-
"input1": ("IMAGE,FLOAT",),
|
| 167 |
-
"input2": ("IMAGE,FLOAT",),
|
| 168 |
-
},
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
RETURN_TYPES = ("IMAGE",)
|
| 172 |
-
FUNCTION = "custom_validation2"
|
| 173 |
-
|
| 174 |
-
CATEGORY = "Testing/Nodes"
|
| 175 |
-
|
| 176 |
-
def custom_validation2(self, input1, input2):
|
| 177 |
-
if isinstance(input1, float) and isinstance(input2, float):
|
| 178 |
-
result = torch.ones([1, 512, 512, 3]) * input1 * input2
|
| 179 |
-
else:
|
| 180 |
-
result = input1 * input2
|
| 181 |
-
return (result,)
|
| 182 |
-
|
| 183 |
-
@classmethod
|
| 184 |
-
def VALIDATE_INPUTS(cls, input_types, input1=None, input2=None):
|
| 185 |
-
if input1 is not None:
|
| 186 |
-
if not isinstance(input1, (torch.Tensor, float)):
|
| 187 |
-
return f"Invalid type of input1: {type(input1)}"
|
| 188 |
-
if input2 is not None:
|
| 189 |
-
if not isinstance(input2, (torch.Tensor, float)):
|
| 190 |
-
return f"Invalid type of input2: {type(input2)}"
|
| 191 |
-
|
| 192 |
-
if 'input1' in input_types:
|
| 193 |
-
if input_types['input1'] not in ["IMAGE", "FLOAT"]:
|
| 194 |
-
return f"Invalid type of input1: {input_types['input1']}"
|
| 195 |
-
if 'input2' in input_types:
|
| 196 |
-
if input_types['input2'] not in ["IMAGE", "FLOAT"]:
|
| 197 |
-
return f"Invalid type of input2: {input_types['input2']}"
|
| 198 |
-
|
| 199 |
-
return True
|
| 200 |
-
|
| 201 |
-
@VariantSupport()
|
| 202 |
-
class TestCustomValidation3:
|
| 203 |
-
@classmethod
|
| 204 |
-
def INPUT_TYPES(cls):
|
| 205 |
-
return {
|
| 206 |
-
"required": {
|
| 207 |
-
"input1": ("IMAGE,FLOAT",),
|
| 208 |
-
"input2": ("IMAGE,FLOAT",),
|
| 209 |
-
},
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
RETURN_TYPES = ("IMAGE",)
|
| 213 |
-
FUNCTION = "custom_validation3"
|
| 214 |
-
|
| 215 |
-
CATEGORY = "Testing/Nodes"
|
| 216 |
-
|
| 217 |
-
def custom_validation3(self, input1, input2):
|
| 218 |
-
if isinstance(input1, float) and isinstance(input2, float):
|
| 219 |
-
result = torch.ones([1, 512, 512, 3]) * input1 * input2
|
| 220 |
-
else:
|
| 221 |
-
result = input1 * input2
|
| 222 |
-
return (result,)
|
| 223 |
-
|
| 224 |
-
class TestCustomValidation4:
|
| 225 |
-
@classmethod
|
| 226 |
-
def INPUT_TYPES(cls):
|
| 227 |
-
return {
|
| 228 |
-
"required": {
|
| 229 |
-
"input1": ("FLOAT",),
|
| 230 |
-
"input2": ("FLOAT",),
|
| 231 |
-
},
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
RETURN_TYPES = ("IMAGE",)
|
| 235 |
-
FUNCTION = "custom_validation4"
|
| 236 |
-
|
| 237 |
-
CATEGORY = "Testing/Nodes"
|
| 238 |
-
|
| 239 |
-
def custom_validation4(self, input1, input2):
|
| 240 |
-
result = torch.ones([1, 512, 512, 3]) * input1 * input2
|
| 241 |
-
return (result,)
|
| 242 |
-
|
| 243 |
-
@classmethod
|
| 244 |
-
def VALIDATE_INPUTS(cls, input1, input2):
|
| 245 |
-
if input1 is not None:
|
| 246 |
-
if not isinstance(input1, float):
|
| 247 |
-
return f"Invalid type of input1: {type(input1)}"
|
| 248 |
-
if input2 is not None:
|
| 249 |
-
if not isinstance(input2, float):
|
| 250 |
-
return f"Invalid type of input2: {type(input2)}"
|
| 251 |
-
|
| 252 |
-
return True
|
| 253 |
-
|
| 254 |
-
class TestCustomValidation5:
|
| 255 |
-
@classmethod
|
| 256 |
-
def INPUT_TYPES(cls):
|
| 257 |
-
return {
|
| 258 |
-
"required": {
|
| 259 |
-
"input1": ("FLOAT", {"min": 0.0, "max": 1.0}),
|
| 260 |
-
"input2": ("FLOAT", {"min": 0.0, "max": 1.0}),
|
| 261 |
-
},
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
RETURN_TYPES = ("IMAGE",)
|
| 265 |
-
FUNCTION = "custom_validation5"
|
| 266 |
-
|
| 267 |
-
CATEGORY = "Testing/Nodes"
|
| 268 |
-
|
| 269 |
-
def custom_validation5(self, input1, input2):
|
| 270 |
-
value = input1 * input2
|
| 271 |
-
return (torch.ones([1, 512, 512, 3]) * value,)
|
| 272 |
-
|
| 273 |
-
@classmethod
|
| 274 |
-
def VALIDATE_INPUTS(cls, **kwargs):
|
| 275 |
-
if kwargs['input2'] == 7.0:
|
| 276 |
-
return "7s are not allowed. I've never liked 7s."
|
| 277 |
-
return True
|
| 278 |
-
|
| 279 |
-
class TestDynamicDependencyCycle:
|
| 280 |
-
@classmethod
|
| 281 |
-
def INPUT_TYPES(cls):
|
| 282 |
-
return {
|
| 283 |
-
"required": {
|
| 284 |
-
"input1": ("IMAGE",),
|
| 285 |
-
"input2": ("IMAGE",),
|
| 286 |
-
},
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
RETURN_TYPES = ("IMAGE",)
|
| 290 |
-
FUNCTION = "dynamic_dependency_cycle"
|
| 291 |
-
|
| 292 |
-
CATEGORY = "Testing/Nodes"
|
| 293 |
-
|
| 294 |
-
def dynamic_dependency_cycle(self, input1, input2):
|
| 295 |
-
g = GraphBuilder()
|
| 296 |
-
mask = g.node("StubMask", value=0.5, height=512, width=512, batch_size=1)
|
| 297 |
-
mix1 = g.node("TestLazyMixImages", image1=input1, mask=mask.out(0))
|
| 298 |
-
mix2 = g.node("TestLazyMixImages", image1=mix1.out(0), image2=input2, mask=mask.out(0))
|
| 299 |
-
|
| 300 |
-
# Create the cyle
|
| 301 |
-
mix1.set_input("image2", mix2.out(0))
|
| 302 |
-
|
| 303 |
-
return {
|
| 304 |
-
"result": (mix2.out(0),),
|
| 305 |
-
"expand": g.finalize(),
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
class TestMixedExpansionReturns:
|
| 309 |
-
@classmethod
|
| 310 |
-
def INPUT_TYPES(cls):
|
| 311 |
-
return {
|
| 312 |
-
"required": {
|
| 313 |
-
"input1": ("FLOAT",),
|
| 314 |
-
},
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
RETURN_TYPES = ("IMAGE","IMAGE")
|
| 318 |
-
FUNCTION = "mixed_expansion_returns"
|
| 319 |
-
|
| 320 |
-
CATEGORY = "Testing/Nodes"
|
| 321 |
-
|
| 322 |
-
def mixed_expansion_returns(self, input1):
|
| 323 |
-
white_image = torch.ones([1, 512, 512, 3])
|
| 324 |
-
if input1 <= 0.1:
|
| 325 |
-
return (torch.ones([1, 512, 512, 3]) * 0.1, white_image)
|
| 326 |
-
elif input1 <= 0.2:
|
| 327 |
-
return {
|
| 328 |
-
"result": (torch.ones([1, 512, 512, 3]) * 0.2, white_image),
|
| 329 |
-
}
|
| 330 |
-
else:
|
| 331 |
-
g = GraphBuilder()
|
| 332 |
-
mask = g.node("StubMask", value=0.3, height=512, width=512, batch_size=1)
|
| 333 |
-
black = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1)
|
| 334 |
-
white = g.node("StubImage", content="WHITE", height=512, width=512, batch_size=1)
|
| 335 |
-
mix = g.node("TestLazyMixImages", image1=black.out(0), image2=white.out(0), mask=mask.out(0))
|
| 336 |
-
return {
|
| 337 |
-
"result": (mix.out(0), white_image),
|
| 338 |
-
"expand": g.finalize(),
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
class TestSamplingInExpansion:
|
| 342 |
-
@classmethod
|
| 343 |
-
def INPUT_TYPES(cls):
|
| 344 |
-
return {
|
| 345 |
-
"required": {
|
| 346 |
-
"model": ("MODEL",),
|
| 347 |
-
"clip": ("CLIP",),
|
| 348 |
-
"vae": ("VAE",),
|
| 349 |
-
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
| 350 |
-
"steps": ("INT", {"default": 20, "min": 1, "max": 100}),
|
| 351 |
-
"cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 30.0}),
|
| 352 |
-
"prompt": ("STRING", {"multiline": True, "default": "a beautiful landscape with mountains and trees"}),
|
| 353 |
-
"negative_prompt": ("STRING", {"multiline": True, "default": "blurry, bad quality, worst quality"}),
|
| 354 |
-
},
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
RETURN_TYPES = ("IMAGE",)
|
| 358 |
-
FUNCTION = "sampling_in_expansion"
|
| 359 |
-
|
| 360 |
-
CATEGORY = "Testing/Nodes"
|
| 361 |
-
|
| 362 |
-
def sampling_in_expansion(self, model, clip, vae, seed, steps, cfg, prompt, negative_prompt):
|
| 363 |
-
g = GraphBuilder()
|
| 364 |
-
|
| 365 |
-
# Create a basic image generation workflow using the input model, clip and vae
|
| 366 |
-
# 1. Setup text prompts using the provided CLIP model
|
| 367 |
-
positive_prompt = g.node("CLIPTextEncode",
|
| 368 |
-
text=prompt,
|
| 369 |
-
clip=clip)
|
| 370 |
-
negative_prompt = g.node("CLIPTextEncode",
|
| 371 |
-
text=negative_prompt,
|
| 372 |
-
clip=clip)
|
| 373 |
-
|
| 374 |
-
# 2. Create empty latent with specified size
|
| 375 |
-
empty_latent = g.node("EmptyLatentImage", width=512, height=512, batch_size=1)
|
| 376 |
-
|
| 377 |
-
# 3. Setup sampler and generate image latent
|
| 378 |
-
sampler = g.node("KSampler",
|
| 379 |
-
model=model,
|
| 380 |
-
positive=positive_prompt.out(0),
|
| 381 |
-
negative=negative_prompt.out(0),
|
| 382 |
-
latent_image=empty_latent.out(0),
|
| 383 |
-
seed=seed,
|
| 384 |
-
steps=steps,
|
| 385 |
-
cfg=cfg,
|
| 386 |
-
sampler_name="euler_ancestral",
|
| 387 |
-
scheduler="normal")
|
| 388 |
-
|
| 389 |
-
# 4. Decode latent to image using VAE
|
| 390 |
-
output = g.node("VAEDecode", samples=sampler.out(0), vae=vae)
|
| 391 |
-
|
| 392 |
-
return {
|
| 393 |
-
"result": (output.out(0),),
|
| 394 |
-
"expand": g.finalize(),
|
| 395 |
-
}
|
| 396 |
-
|
| 397 |
-
class TestSleep(ComfyNodeABC):
|
| 398 |
-
@classmethod
|
| 399 |
-
def INPUT_TYPES(cls):
|
| 400 |
-
return {
|
| 401 |
-
"required": {
|
| 402 |
-
"value": (IO.ANY, {}),
|
| 403 |
-
"seconds": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 9999.0, "step": 0.01, "tooltip": "The amount of seconds to sleep."}),
|
| 404 |
-
},
|
| 405 |
-
"hidden": {
|
| 406 |
-
"unique_id": "UNIQUE_ID",
|
| 407 |
-
},
|
| 408 |
-
}
|
| 409 |
-
RETURN_TYPES = (IO.ANY,)
|
| 410 |
-
FUNCTION = "sleep"
|
| 411 |
-
|
| 412 |
-
CATEGORY = "_for_testing"
|
| 413 |
-
|
| 414 |
-
async def sleep(self, value, seconds, unique_id):
|
| 415 |
-
pbar = ProgressBar(seconds, node_id=unique_id)
|
| 416 |
-
start = time.time()
|
| 417 |
-
expiration = start + seconds
|
| 418 |
-
now = start
|
| 419 |
-
while now < expiration:
|
| 420 |
-
now = time.time()
|
| 421 |
-
pbar.update_absolute(now - start)
|
| 422 |
-
await asyncio.sleep(0.01)
|
| 423 |
-
return (value,)
|
| 424 |
-
|
| 425 |
-
class TestParallelSleep(ComfyNodeABC):
|
| 426 |
-
@classmethod
|
| 427 |
-
def INPUT_TYPES(cls):
|
| 428 |
-
return {
|
| 429 |
-
"required": {
|
| 430 |
-
"image1": ("IMAGE", ),
|
| 431 |
-
"image2": ("IMAGE", ),
|
| 432 |
-
"image3": ("IMAGE", ),
|
| 433 |
-
"sleep1": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.01}),
|
| 434 |
-
"sleep2": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.01}),
|
| 435 |
-
"sleep3": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.01}),
|
| 436 |
-
},
|
| 437 |
-
"hidden": {
|
| 438 |
-
"unique_id": "UNIQUE_ID",
|
| 439 |
-
},
|
| 440 |
-
}
|
| 441 |
-
RETURN_TYPES = ("IMAGE",)
|
| 442 |
-
FUNCTION = "parallel_sleep"
|
| 443 |
-
CATEGORY = "_for_testing"
|
| 444 |
-
OUTPUT_NODE = True
|
| 445 |
-
|
| 446 |
-
def parallel_sleep(self, image1, image2, image3, sleep1, sleep2, sleep3, unique_id):
|
| 447 |
-
# Create a graph dynamically with three TestSleep nodes
|
| 448 |
-
g = GraphBuilder()
|
| 449 |
-
|
| 450 |
-
# Create sleep nodes for each duration and image
|
| 451 |
-
sleep_node1 = g.node("TestSleep", value=image1, seconds=sleep1)
|
| 452 |
-
sleep_node2 = g.node("TestSleep", value=image2, seconds=sleep2)
|
| 453 |
-
sleep_node3 = g.node("TestSleep", value=image3, seconds=sleep3)
|
| 454 |
-
|
| 455 |
-
# Blend the results using TestVariadicAverage
|
| 456 |
-
blend = g.node("TestVariadicAverage",
|
| 457 |
-
input1=sleep_node1.out(0),
|
| 458 |
-
input2=sleep_node2.out(0),
|
| 459 |
-
input3=sleep_node3.out(0))
|
| 460 |
-
|
| 461 |
-
return {
|
| 462 |
-
"result": (blend.out(0),),
|
| 463 |
-
"expand": g.finalize(),
|
| 464 |
-
}
|
| 465 |
-
|
| 466 |
-
TEST_NODE_CLASS_MAPPINGS = {
|
| 467 |
-
"TestLazyMixImages": TestLazyMixImages,
|
| 468 |
-
"TestVariadicAverage": TestVariadicAverage,
|
| 469 |
-
"TestCustomIsChanged": TestCustomIsChanged,
|
| 470 |
-
"TestIsChangedWithConstants": TestIsChangedWithConstants,
|
| 471 |
-
"TestCustomValidation1": TestCustomValidation1,
|
| 472 |
-
"TestCustomValidation2": TestCustomValidation2,
|
| 473 |
-
"TestCustomValidation3": TestCustomValidation3,
|
| 474 |
-
"TestCustomValidation4": TestCustomValidation4,
|
| 475 |
-
"TestCustomValidation5": TestCustomValidation5,
|
| 476 |
-
"TestDynamicDependencyCycle": TestDynamicDependencyCycle,
|
| 477 |
-
"TestMixedExpansionReturns": TestMixedExpansionReturns,
|
| 478 |
-
"TestSamplingInExpansion": TestSamplingInExpansion,
|
| 479 |
-
"TestSleep": TestSleep,
|
| 480 |
-
"TestParallelSleep": TestParallelSleep,
|
| 481 |
-
}
|
| 482 |
-
|
| 483 |
-
TEST_NODE_DISPLAY_NAME_MAPPINGS = {
|
| 484 |
-
"TestLazyMixImages": "Lazy Mix Images",
|
| 485 |
-
"TestVariadicAverage": "Variadic Average",
|
| 486 |
-
"TestCustomIsChanged": "Custom IsChanged",
|
| 487 |
-
"TestIsChangedWithConstants": "IsChanged With Constants",
|
| 488 |
-
"TestCustomValidation1": "Custom Validation 1",
|
| 489 |
-
"TestCustomValidation2": "Custom Validation 2",
|
| 490 |
-
"TestCustomValidation3": "Custom Validation 3",
|
| 491 |
-
"TestCustomValidation4": "Custom Validation 4",
|
| 492 |
-
"TestCustomValidation5": "Custom Validation 5",
|
| 493 |
-
"TestDynamicDependencyCycle": "Dynamic Dependency Cycle",
|
| 494 |
-
"TestMixedExpansionReturns": "Mixed Expansion Returns",
|
| 495 |
-
"TestSamplingInExpansion": "Sampling In Expansion",
|
| 496 |
-
"TestSleep": "Test Sleep",
|
| 497 |
-
"TestParallelSleep": "Test Parallel Sleep",
|
| 498 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/testing_nodes/testing-pack/stubs.py
DELETED
|
@@ -1,129 +0,0 @@
|
|
| 1 |
-
import torch
|
| 2 |
-
|
| 3 |
-
class StubImage:
|
| 4 |
-
def __init__(self):
|
| 5 |
-
pass
|
| 6 |
-
|
| 7 |
-
@classmethod
|
| 8 |
-
def INPUT_TYPES(cls):
|
| 9 |
-
return {
|
| 10 |
-
"required": {
|
| 11 |
-
"content": (['WHITE', 'BLACK', 'NOISE'],),
|
| 12 |
-
"height": ("INT", {"default": 512, "min": 1, "max": 1024 ** 3, "step": 1}),
|
| 13 |
-
"width": ("INT", {"default": 512, "min": 1, "max": 4096 ** 3, "step": 1}),
|
| 14 |
-
"batch_size": ("INT", {"default": 1, "min": 1, "max": 1024 ** 3, "step": 1}),
|
| 15 |
-
},
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
RETURN_TYPES = ("IMAGE",)
|
| 19 |
-
FUNCTION = "stub_image"
|
| 20 |
-
|
| 21 |
-
CATEGORY = "Testing/Stub Nodes"
|
| 22 |
-
|
| 23 |
-
def stub_image(self, content, height, width, batch_size):
|
| 24 |
-
if content == "WHITE":
|
| 25 |
-
return (torch.ones(batch_size, height, width, 3),)
|
| 26 |
-
elif content == "BLACK":
|
| 27 |
-
return (torch.zeros(batch_size, height, width, 3),)
|
| 28 |
-
elif content == "NOISE":
|
| 29 |
-
return (torch.rand(batch_size, height, width, 3),)
|
| 30 |
-
|
| 31 |
-
class StubConstantImage:
|
| 32 |
-
def __init__(self):
|
| 33 |
-
pass
|
| 34 |
-
@classmethod
|
| 35 |
-
def INPUT_TYPES(cls):
|
| 36 |
-
return {
|
| 37 |
-
"required": {
|
| 38 |
-
"value": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
| 39 |
-
"height": ("INT", {"default": 512, "min": 1, "max": 1024 ** 3, "step": 1}),
|
| 40 |
-
"width": ("INT", {"default": 512, "min": 1, "max": 4096 ** 3, "step": 1}),
|
| 41 |
-
"batch_size": ("INT", {"default": 1, "min": 1, "max": 1024 ** 3, "step": 1}),
|
| 42 |
-
},
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
RETURN_TYPES = ("IMAGE",)
|
| 46 |
-
FUNCTION = "stub_constant_image"
|
| 47 |
-
|
| 48 |
-
CATEGORY = "Testing/Stub Nodes"
|
| 49 |
-
|
| 50 |
-
def stub_constant_image(self, value, height, width, batch_size):
|
| 51 |
-
return (torch.ones(batch_size, height, width, 3) * value,)
|
| 52 |
-
|
| 53 |
-
class StubMask:
|
| 54 |
-
def __init__(self):
|
| 55 |
-
pass
|
| 56 |
-
|
| 57 |
-
@classmethod
|
| 58 |
-
def INPUT_TYPES(cls):
|
| 59 |
-
return {
|
| 60 |
-
"required": {
|
| 61 |
-
"value": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
| 62 |
-
"height": ("INT", {"default": 512, "min": 1, "max": 1024 ** 3, "step": 1}),
|
| 63 |
-
"width": ("INT", {"default": 512, "min": 1, "max": 4096 ** 3, "step": 1}),
|
| 64 |
-
"batch_size": ("INT", {"default": 1, "min": 1, "max": 1024 ** 3, "step": 1}),
|
| 65 |
-
},
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
RETURN_TYPES = ("MASK",)
|
| 69 |
-
FUNCTION = "stub_mask"
|
| 70 |
-
|
| 71 |
-
CATEGORY = "Testing/Stub Nodes"
|
| 72 |
-
|
| 73 |
-
def stub_mask(self, value, height, width, batch_size):
|
| 74 |
-
return (torch.ones(batch_size, height, width) * value,)
|
| 75 |
-
|
| 76 |
-
class StubInt:
|
| 77 |
-
def __init__(self):
|
| 78 |
-
pass
|
| 79 |
-
|
| 80 |
-
@classmethod
|
| 81 |
-
def INPUT_TYPES(cls):
|
| 82 |
-
return {
|
| 83 |
-
"required": {
|
| 84 |
-
"value": ("INT", {"default": 0, "min": -0xffffffff, "max": 0xffffffff, "step": 1}),
|
| 85 |
-
},
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
RETURN_TYPES = ("INT",)
|
| 89 |
-
FUNCTION = "stub_int"
|
| 90 |
-
|
| 91 |
-
CATEGORY = "Testing/Stub Nodes"
|
| 92 |
-
|
| 93 |
-
def stub_int(self, value):
|
| 94 |
-
return (value,)
|
| 95 |
-
|
| 96 |
-
class StubFloat:
|
| 97 |
-
def __init__(self):
|
| 98 |
-
pass
|
| 99 |
-
|
| 100 |
-
@classmethod
|
| 101 |
-
def INPUT_TYPES(cls):
|
| 102 |
-
return {
|
| 103 |
-
"required": {
|
| 104 |
-
"value": ("FLOAT", {"default": 0.0, "min": -1.0e38, "max": 1.0e38, "step": 0.01}),
|
| 105 |
-
},
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
RETURN_TYPES = ("FLOAT",)
|
| 109 |
-
FUNCTION = "stub_float"
|
| 110 |
-
|
| 111 |
-
CATEGORY = "Testing/Stub Nodes"
|
| 112 |
-
|
| 113 |
-
def stub_float(self, value):
|
| 114 |
-
return (value,)
|
| 115 |
-
|
| 116 |
-
TEST_STUB_NODE_CLASS_MAPPINGS = {
|
| 117 |
-
"StubImage": StubImage,
|
| 118 |
-
"StubConstantImage": StubConstantImage,
|
| 119 |
-
"StubMask": StubMask,
|
| 120 |
-
"StubInt": StubInt,
|
| 121 |
-
"StubFloat": StubFloat,
|
| 122 |
-
}
|
| 123 |
-
TEST_STUB_NODE_DISPLAY_NAME_MAPPINGS = {
|
| 124 |
-
"StubImage": "Stub Image",
|
| 125 |
-
"StubConstantImage": "Stub Constant Image",
|
| 126 |
-
"StubMask": "Stub Mask",
|
| 127 |
-
"StubInt": "Stub Int",
|
| 128 |
-
"StubFloat": "Stub Float",
|
| 129 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/testing_nodes/testing-pack/tools.py
DELETED
|
@@ -1,53 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
def MakeSmartType(t):
|
| 3 |
-
if isinstance(t, str):
|
| 4 |
-
return SmartType(t)
|
| 5 |
-
return t
|
| 6 |
-
|
| 7 |
-
class SmartType(str):
|
| 8 |
-
def __ne__(self, other):
|
| 9 |
-
if self == "*" or other == "*":
|
| 10 |
-
return False
|
| 11 |
-
selfset = set(self.split(','))
|
| 12 |
-
otherset = set(other.split(','))
|
| 13 |
-
return not selfset.issubset(otherset)
|
| 14 |
-
|
| 15 |
-
def VariantSupport():
|
| 16 |
-
def decorator(cls):
|
| 17 |
-
if hasattr(cls, "INPUT_TYPES"):
|
| 18 |
-
old_input_types = getattr(cls, "INPUT_TYPES")
|
| 19 |
-
def new_input_types(*args, **kwargs):
|
| 20 |
-
types = old_input_types(*args, **kwargs)
|
| 21 |
-
for category in ["required", "optional"]:
|
| 22 |
-
if category not in types:
|
| 23 |
-
continue
|
| 24 |
-
for key, value in types[category].items():
|
| 25 |
-
if isinstance(value, tuple):
|
| 26 |
-
types[category][key] = (MakeSmartType(value[0]),) + value[1:]
|
| 27 |
-
return types
|
| 28 |
-
setattr(cls, "INPUT_TYPES", new_input_types)
|
| 29 |
-
if hasattr(cls, "RETURN_TYPES"):
|
| 30 |
-
old_return_types = cls.RETURN_TYPES
|
| 31 |
-
setattr(cls, "RETURN_TYPES", tuple(MakeSmartType(x) for x in old_return_types))
|
| 32 |
-
if hasattr(cls, "VALIDATE_INPUTS"):
|
| 33 |
-
# Reflection is used to determine what the function signature is, so we can't just change the function signature
|
| 34 |
-
raise NotImplementedError("VariantSupport does not support VALIDATE_INPUTS yet")
|
| 35 |
-
else:
|
| 36 |
-
def validate_inputs(input_types):
|
| 37 |
-
inputs = cls.INPUT_TYPES()
|
| 38 |
-
for key, value in input_types.items():
|
| 39 |
-
if isinstance(value, SmartType):
|
| 40 |
-
continue
|
| 41 |
-
if "required" in inputs and key in inputs["required"]:
|
| 42 |
-
expected_type = inputs["required"][key][0]
|
| 43 |
-
elif "optional" in inputs and key in inputs["optional"]:
|
| 44 |
-
expected_type = inputs["optional"][key][0]
|
| 45 |
-
else:
|
| 46 |
-
expected_type = None
|
| 47 |
-
if expected_type is not None and MakeSmartType(value) != expected_type:
|
| 48 |
-
return f"Invalid type of {key}: {value} (expected {expected_type})"
|
| 49 |
-
return True
|
| 50 |
-
setattr(cls, "VALIDATE_INPUTS", validate_inputs)
|
| 51 |
-
return cls
|
| 52 |
-
return decorator
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/inference/testing_nodes/testing-pack/util.py
DELETED
|
@@ -1,364 +0,0 @@
|
|
| 1 |
-
from comfy_execution.graph_utils import GraphBuilder
|
| 2 |
-
from .tools import VariantSupport
|
| 3 |
-
|
| 4 |
-
@VariantSupport()
|
| 5 |
-
class TestAccumulateNode:
|
| 6 |
-
def __init__(self):
|
| 7 |
-
pass
|
| 8 |
-
|
| 9 |
-
@classmethod
|
| 10 |
-
def INPUT_TYPES(cls):
|
| 11 |
-
return {
|
| 12 |
-
"required": {
|
| 13 |
-
"to_add": ("*",),
|
| 14 |
-
},
|
| 15 |
-
"optional": {
|
| 16 |
-
"accumulation": ("ACCUMULATION",),
|
| 17 |
-
},
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
RETURN_TYPES = ("ACCUMULATION",)
|
| 21 |
-
FUNCTION = "accumulate"
|
| 22 |
-
|
| 23 |
-
CATEGORY = "Testing/Lists"
|
| 24 |
-
|
| 25 |
-
def accumulate(self, to_add, accumulation = None):
|
| 26 |
-
if accumulation is None:
|
| 27 |
-
value = [to_add]
|
| 28 |
-
else:
|
| 29 |
-
value = accumulation["accum"] + [to_add]
|
| 30 |
-
return ({"accum": value},)
|
| 31 |
-
|
| 32 |
-
@VariantSupport()
|
| 33 |
-
class TestAccumulationHeadNode:
|
| 34 |
-
def __init__(self):
|
| 35 |
-
pass
|
| 36 |
-
|
| 37 |
-
@classmethod
|
| 38 |
-
def INPUT_TYPES(cls):
|
| 39 |
-
return {
|
| 40 |
-
"required": {
|
| 41 |
-
"accumulation": ("ACCUMULATION",),
|
| 42 |
-
},
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
RETURN_TYPES = ("ACCUMULATION", "*",)
|
| 46 |
-
FUNCTION = "accumulation_head"
|
| 47 |
-
|
| 48 |
-
CATEGORY = "Testing/Lists"
|
| 49 |
-
|
| 50 |
-
def accumulation_head(self, accumulation):
|
| 51 |
-
accum = accumulation["accum"]
|
| 52 |
-
if len(accum) == 0:
|
| 53 |
-
return (accumulation, None)
|
| 54 |
-
else:
|
| 55 |
-
return ({"accum": accum[1:]}, accum[0])
|
| 56 |
-
|
| 57 |
-
class TestAccumulationTailNode:
|
| 58 |
-
def __init__(self):
|
| 59 |
-
pass
|
| 60 |
-
|
| 61 |
-
@classmethod
|
| 62 |
-
def INPUT_TYPES(cls):
|
| 63 |
-
return {
|
| 64 |
-
"required": {
|
| 65 |
-
"accumulation": ("ACCUMULATION",),
|
| 66 |
-
},
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
RETURN_TYPES = ("ACCUMULATION", "*",)
|
| 70 |
-
FUNCTION = "accumulation_tail"
|
| 71 |
-
|
| 72 |
-
CATEGORY = "Testing/Lists"
|
| 73 |
-
|
| 74 |
-
def accumulation_tail(self, accumulation):
|
| 75 |
-
accum = accumulation["accum"]
|
| 76 |
-
if len(accum) == 0:
|
| 77 |
-
return (None, accumulation)
|
| 78 |
-
else:
|
| 79 |
-
return ({"accum": accum[:-1]}, accum[-1])
|
| 80 |
-
|
| 81 |
-
@VariantSupport()
|
| 82 |
-
class TestAccumulationToListNode:
|
| 83 |
-
def __init__(self):
|
| 84 |
-
pass
|
| 85 |
-
|
| 86 |
-
@classmethod
|
| 87 |
-
def INPUT_TYPES(cls):
|
| 88 |
-
return {
|
| 89 |
-
"required": {
|
| 90 |
-
"accumulation": ("ACCUMULATION",),
|
| 91 |
-
},
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
RETURN_TYPES = ("*",)
|
| 95 |
-
OUTPUT_IS_LIST = (True,)
|
| 96 |
-
|
| 97 |
-
FUNCTION = "accumulation_to_list"
|
| 98 |
-
|
| 99 |
-
CATEGORY = "Testing/Lists"
|
| 100 |
-
|
| 101 |
-
def accumulation_to_list(self, accumulation):
|
| 102 |
-
return (accumulation["accum"],)
|
| 103 |
-
|
| 104 |
-
@VariantSupport()
|
| 105 |
-
class TestListToAccumulationNode:
|
| 106 |
-
def __init__(self):
|
| 107 |
-
pass
|
| 108 |
-
|
| 109 |
-
@classmethod
|
| 110 |
-
def INPUT_TYPES(cls):
|
| 111 |
-
return {
|
| 112 |
-
"required": {
|
| 113 |
-
"list": ("*",),
|
| 114 |
-
},
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
RETURN_TYPES = ("ACCUMULATION",)
|
| 118 |
-
INPUT_IS_LIST = (True,)
|
| 119 |
-
|
| 120 |
-
FUNCTION = "list_to_accumulation"
|
| 121 |
-
|
| 122 |
-
CATEGORY = "Testing/Lists"
|
| 123 |
-
|
| 124 |
-
def list_to_accumulation(self, list):
|
| 125 |
-
return ({"accum": list},)
|
| 126 |
-
|
| 127 |
-
@VariantSupport()
|
| 128 |
-
class TestAccumulationGetLengthNode:
|
| 129 |
-
def __init__(self):
|
| 130 |
-
pass
|
| 131 |
-
|
| 132 |
-
@classmethod
|
| 133 |
-
def INPUT_TYPES(cls):
|
| 134 |
-
return {
|
| 135 |
-
"required": {
|
| 136 |
-
"accumulation": ("ACCUMULATION",),
|
| 137 |
-
},
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
RETURN_TYPES = ("INT",)
|
| 141 |
-
|
| 142 |
-
FUNCTION = "accumlength"
|
| 143 |
-
|
| 144 |
-
CATEGORY = "Testing/Lists"
|
| 145 |
-
|
| 146 |
-
def accumlength(self, accumulation):
|
| 147 |
-
return (len(accumulation['accum']),)
|
| 148 |
-
|
| 149 |
-
@VariantSupport()
|
| 150 |
-
class TestAccumulationGetItemNode:
|
| 151 |
-
def __init__(self):
|
| 152 |
-
pass
|
| 153 |
-
|
| 154 |
-
@classmethod
|
| 155 |
-
def INPUT_TYPES(cls):
|
| 156 |
-
return {
|
| 157 |
-
"required": {
|
| 158 |
-
"accumulation": ("ACCUMULATION",),
|
| 159 |
-
"index": ("INT", {"default":0, "step":1})
|
| 160 |
-
},
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
RETURN_TYPES = ("*",)
|
| 164 |
-
|
| 165 |
-
FUNCTION = "get_item"
|
| 166 |
-
|
| 167 |
-
CATEGORY = "Testing/Lists"
|
| 168 |
-
|
| 169 |
-
def get_item(self, accumulation, index):
|
| 170 |
-
return (accumulation['accum'][index],)
|
| 171 |
-
|
| 172 |
-
@VariantSupport()
|
| 173 |
-
class TestAccumulationSetItemNode:
|
| 174 |
-
def __init__(self):
|
| 175 |
-
pass
|
| 176 |
-
|
| 177 |
-
@classmethod
|
| 178 |
-
def INPUT_TYPES(cls):
|
| 179 |
-
return {
|
| 180 |
-
"required": {
|
| 181 |
-
"accumulation": ("ACCUMULATION",),
|
| 182 |
-
"index": ("INT", {"default":0, "step":1}),
|
| 183 |
-
"value": ("*",),
|
| 184 |
-
},
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
RETURN_TYPES = ("ACCUMULATION",)
|
| 188 |
-
|
| 189 |
-
FUNCTION = "set_item"
|
| 190 |
-
|
| 191 |
-
CATEGORY = "Testing/Lists"
|
| 192 |
-
|
| 193 |
-
def set_item(self, accumulation, index, value):
|
| 194 |
-
new_accum = accumulation['accum'][:]
|
| 195 |
-
new_accum[index] = value
|
| 196 |
-
return ({"accum": new_accum},)
|
| 197 |
-
|
| 198 |
-
class TestIntMathOperation:
|
| 199 |
-
def __init__(self):
|
| 200 |
-
pass
|
| 201 |
-
|
| 202 |
-
@classmethod
|
| 203 |
-
def INPUT_TYPES(cls):
|
| 204 |
-
return {
|
| 205 |
-
"required": {
|
| 206 |
-
"a": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
|
| 207 |
-
"b": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
|
| 208 |
-
"operation": (["add", "subtract", "multiply", "divide", "modulo", "power"],),
|
| 209 |
-
},
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
RETURN_TYPES = ("INT",)
|
| 213 |
-
FUNCTION = "int_math_operation"
|
| 214 |
-
|
| 215 |
-
CATEGORY = "Testing/Logic"
|
| 216 |
-
|
| 217 |
-
def int_math_operation(self, a, b, operation):
|
| 218 |
-
if operation == "add":
|
| 219 |
-
return (a + b,)
|
| 220 |
-
elif operation == "subtract":
|
| 221 |
-
return (a - b,)
|
| 222 |
-
elif operation == "multiply":
|
| 223 |
-
return (a * b,)
|
| 224 |
-
elif operation == "divide":
|
| 225 |
-
return (a // b,)
|
| 226 |
-
elif operation == "modulo":
|
| 227 |
-
return (a % b,)
|
| 228 |
-
elif operation == "power":
|
| 229 |
-
return (a ** b,)
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
from .flow_control import NUM_FLOW_SOCKETS
|
| 233 |
-
@VariantSupport()
|
| 234 |
-
class TestForLoopOpen:
|
| 235 |
-
def __init__(self):
|
| 236 |
-
pass
|
| 237 |
-
|
| 238 |
-
@classmethod
|
| 239 |
-
def INPUT_TYPES(cls):
|
| 240 |
-
return {
|
| 241 |
-
"required": {
|
| 242 |
-
"remaining": ("INT", {"default": 1, "min": 0, "max": 100000, "step": 1}),
|
| 243 |
-
},
|
| 244 |
-
"optional": {
|
| 245 |
-
f"initial_value{i}": ("*",) for i in range(1, NUM_FLOW_SOCKETS)
|
| 246 |
-
},
|
| 247 |
-
"hidden": {
|
| 248 |
-
"initial_value0": ("*",)
|
| 249 |
-
}
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
RETURN_TYPES = tuple(["FLOW_CONTROL", "INT",] + ["*"] * (NUM_FLOW_SOCKETS-1))
|
| 253 |
-
RETURN_NAMES = tuple(["flow_control", "remaining"] + [f"value{i}" for i in range(1, NUM_FLOW_SOCKETS)])
|
| 254 |
-
FUNCTION = "for_loop_open"
|
| 255 |
-
|
| 256 |
-
CATEGORY = "Testing/Flow"
|
| 257 |
-
|
| 258 |
-
def for_loop_open(self, remaining, **kwargs):
|
| 259 |
-
graph = GraphBuilder()
|
| 260 |
-
if "initial_value0" in kwargs:
|
| 261 |
-
remaining = kwargs["initial_value0"]
|
| 262 |
-
graph.node("TestWhileLoopOpen", condition=remaining, initial_value0=remaining, **{(f"initial_value{i}"): kwargs.get(f"initial_value{i}", None) for i in range(1, NUM_FLOW_SOCKETS)})
|
| 263 |
-
outputs = [kwargs.get(f"initial_value{i}", None) for i in range(1, NUM_FLOW_SOCKETS)]
|
| 264 |
-
return {
|
| 265 |
-
"result": tuple(["stub", remaining] + outputs),
|
| 266 |
-
"expand": graph.finalize(),
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
@VariantSupport()
|
| 270 |
-
class TestForLoopClose:
|
| 271 |
-
def __init__(self):
|
| 272 |
-
pass
|
| 273 |
-
|
| 274 |
-
@classmethod
|
| 275 |
-
def INPUT_TYPES(cls):
|
| 276 |
-
return {
|
| 277 |
-
"required": {
|
| 278 |
-
"flow_control": ("FLOW_CONTROL", {"rawLink": True}),
|
| 279 |
-
},
|
| 280 |
-
"optional": {
|
| 281 |
-
f"initial_value{i}": ("*",{"rawLink": True}) for i in range(1, NUM_FLOW_SOCKETS)
|
| 282 |
-
},
|
| 283 |
-
}
|
| 284 |
-
|
| 285 |
-
RETURN_TYPES = tuple(["*"] * (NUM_FLOW_SOCKETS-1))
|
| 286 |
-
RETURN_NAMES = tuple([f"value{i}" for i in range(1, NUM_FLOW_SOCKETS)])
|
| 287 |
-
FUNCTION = "for_loop_close"
|
| 288 |
-
|
| 289 |
-
CATEGORY = "Testing/Flow"
|
| 290 |
-
|
| 291 |
-
def for_loop_close(self, flow_control, **kwargs):
|
| 292 |
-
graph = GraphBuilder()
|
| 293 |
-
while_open = flow_control[0]
|
| 294 |
-
sub = graph.node("TestIntMathOperation", operation="subtract", a=[while_open,1], b=1)
|
| 295 |
-
cond = graph.node("TestToBoolNode", value=sub.out(0))
|
| 296 |
-
input_values = {f"initial_value{i}": kwargs.get(f"initial_value{i}", None) for i in range(1, NUM_FLOW_SOCKETS)}
|
| 297 |
-
while_close = graph.node("TestWhileLoopClose",
|
| 298 |
-
flow_control=flow_control,
|
| 299 |
-
condition=cond.out(0),
|
| 300 |
-
initial_value0=sub.out(0),
|
| 301 |
-
**input_values)
|
| 302 |
-
return {
|
| 303 |
-
"result": tuple([while_close.out(i) for i in range(1, NUM_FLOW_SOCKETS)]),
|
| 304 |
-
"expand": graph.finalize(),
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
NUM_LIST_SOCKETS = 10
|
| 308 |
-
@VariantSupport()
|
| 309 |
-
class TestMakeListNode:
|
| 310 |
-
def __init__(self):
|
| 311 |
-
pass
|
| 312 |
-
|
| 313 |
-
@classmethod
|
| 314 |
-
def INPUT_TYPES(cls):
|
| 315 |
-
return {
|
| 316 |
-
"required": {
|
| 317 |
-
"value1": ("*",),
|
| 318 |
-
},
|
| 319 |
-
"optional": {
|
| 320 |
-
f"value{i}": ("*",) for i in range(1, NUM_LIST_SOCKETS)
|
| 321 |
-
},
|
| 322 |
-
}
|
| 323 |
-
|
| 324 |
-
RETURN_TYPES = ("*",)
|
| 325 |
-
FUNCTION = "make_list"
|
| 326 |
-
OUTPUT_IS_LIST = (True,)
|
| 327 |
-
|
| 328 |
-
CATEGORY = "Testing/Lists"
|
| 329 |
-
|
| 330 |
-
def make_list(self, **kwargs):
|
| 331 |
-
result = []
|
| 332 |
-
for i in range(NUM_LIST_SOCKETS):
|
| 333 |
-
if f"value{i}" in kwargs:
|
| 334 |
-
result.append(kwargs[f"value{i}"])
|
| 335 |
-
return (result,)
|
| 336 |
-
|
| 337 |
-
UTILITY_NODE_CLASS_MAPPINGS = {
|
| 338 |
-
"TestAccumulateNode": TestAccumulateNode,
|
| 339 |
-
"TestAccumulationHeadNode": TestAccumulationHeadNode,
|
| 340 |
-
"TestAccumulationTailNode": TestAccumulationTailNode,
|
| 341 |
-
"TestAccumulationToListNode": TestAccumulationToListNode,
|
| 342 |
-
"TestListToAccumulationNode": TestListToAccumulationNode,
|
| 343 |
-
"TestAccumulationGetLengthNode": TestAccumulationGetLengthNode,
|
| 344 |
-
"TestAccumulationGetItemNode": TestAccumulationGetItemNode,
|
| 345 |
-
"TestAccumulationSetItemNode": TestAccumulationSetItemNode,
|
| 346 |
-
"TestForLoopOpen": TestForLoopOpen,
|
| 347 |
-
"TestForLoopClose": TestForLoopClose,
|
| 348 |
-
"TestIntMathOperation": TestIntMathOperation,
|
| 349 |
-
"TestMakeListNode": TestMakeListNode,
|
| 350 |
-
}
|
| 351 |
-
UTILITY_NODE_DISPLAY_NAME_MAPPINGS = {
|
| 352 |
-
"TestAccumulateNode": "Accumulate",
|
| 353 |
-
"TestAccumulationHeadNode": "Accumulation Head",
|
| 354 |
-
"TestAccumulationTailNode": "Accumulation Tail",
|
| 355 |
-
"TestAccumulationToListNode": "Accumulation to List",
|
| 356 |
-
"TestListToAccumulationNode": "List to Accumulation",
|
| 357 |
-
"TestAccumulationGetLengthNode": "Accumulation Get Length",
|
| 358 |
-
"TestAccumulationGetItemNode": "Accumulation Get Item",
|
| 359 |
-
"TestAccumulationSetItemNode": "Accumulation Set Item",
|
| 360 |
-
"TestForLoopOpen": "For Loop Open",
|
| 361 |
-
"TestForLoopClose": "For Loop Close",
|
| 362 |
-
"TestIntMathOperation": "Int Math Operation",
|
| 363 |
-
"TestMakeListNode": "Make List",
|
| 364 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|