diff --git a/__init__.py b/__init__.py index 0ad69f4..6327d0f 100644 --- a/__init__.py +++ b/__init__.py @@ -85,7 +85,7 @@ NODE_DISPLAY_NAME_MAPPINGS.update(TEXT_SWITCH_NAMES) NODE_DISPLAY_NAME_MAPPINGS.update(TYPE_CONVERTER_NAMES) # Version info -__version__ = "0.46" +__version__ = "1.2.0" print(f"Endless Sea of Stars Custom Nodes v{__version__} loaded successfully!") print("Nodes available under 'Endless 🌊✨' menu") diff --git a/changlelog.md b/changlelog.md index 0142167..0b8570a 100644 --- a/changlelog.md +++ b/changlelog.md @@ -1,65 +1,65 @@ +Jun 23/23, V1.2.0: Added the Endless Pandemonium node, a black box that randomly and invisibly changes parameters on you. Put in ability to use 64 or 16 as the minimum steps for the dimensions in the Randomizer nodes, added CFG Guidance outputs for Flux. Fixed typos and added better credits in README. Some bug squishing. + Jun 22/25, V1.1.1: Minor typos and trying to align version numbers -Jun 21/25, V1.00: Blew it all up and started again - +Jun 21/25, V1.0.0: Blew it all up and started again + Aug 19/24, V0.41: Fixed Image Saver node so it appears - + Oct 20/23, V0.40: Updated ImageSaver to turn off JSON save to image data - + Oct 18/23, V0.39: Added six float output node - + Oct 18/23, V0.38: (UNRELEASED)Putting in hooks for future fixes and improvements - + Oct 18/23, V0.37: Bug fix in Image Saver module that would overwrite files was corrected - + Oct 07/23, V0.36: Killed the scorers until I figure out why CLIP won't load for some people - + Oct 06/23, V0.35: Reverted the Image Saver module as I had inadvertently removed the ability to add date and time to the filenames - + Oct 05/23, V0.33: Renamed nodes to make them shorter and easier to search for, breaks names of previous workflows though - + Oct 07/23, V0.33: Removed Aesthetic Scorer and ImageReward until I can figure out why the CLIP module isn't working for a few people - + Oct 05/23, V0.32: (UNRELEASED)Set rules for image saver so paths + filename length do not exceed 248 (leaves room for extension) - + Oct 04/23, V0.31: Release of V0.28 functionality (int, float, num to X), added String to X, code cleanup, vanity node renaming and recategorization - + Oct 04/23, V0.30: Squished bugs in the various X to X nodes - + Oct 03/23, V0.29: Save Image module added, saves images and JSON to separate folder if requested - + Sep 28/23, V0.28: (UNRELEASED) Added Variable types to X - + Sep 28/23, V0.27: (UNRELEASED) Corrected scoring nodes to actually add the value of the score into the image metadata .... still goobered! - + Sep 24/23, V0.26: (UNRELEASED) starting to correct scoring to get to image metadata - + Sep 24/23, V0.25: Added various X to String Nodes - + Sep 24/23, V0.24: Added In Image Reward scoring model with a single node to load model and output standard deviation and scoring via number or string nodes - + Sep 24/23, V0.23: Rework Aesthetic Score model and integrate it into single node to display score, added a requirements file - + Sep 23/23, V0.22: (UNRELEASED) Convert ImageReward output to base ten score - + Sep 22/23, V0.21: (UNRELEASED) Introduced aestheticscore, recategorized nodes into submenus, added some vanity coding to the node names, changed the ComfyUI manager header text - + Sep 21/23, V0.20: (UNRELEASED) Skeleton for save image - + Sep 21/23, V0.19: (UNRELEASED) Attempt for basic display nodes - + Sep 20/23, V0.16: Added Eight Input Number String - + Sep 18/23, V0.15: Added Combo Parameterizers to reduce number of nodes, allows for common resolution parameters to go to both pos/neg CLIP encode and adds separate pos/neg aesthetic score. Also has a version with pos/neg prompts - + Sep 18/23, V0.13: Fixed typos, added Paramaterizer with Prompt (unreleased to GitHub) - + Sep 18/23, V0.12: Added "Parameterizer", allows for parameters to be added to CLIP Encode - + Sep 15/23, V0.10: Added Six Input Number Widget, first release to GitHub - + Sep 12/23, V0.05: Added Six Input Number String - -Sep 08/23, V0.00: Basic Flow for Six Input Text Switch - -\ + +Sep 08/23, V0.00: Basic Flow for Six Input Text Switch diff --git a/randomizers/__init__.py b/randomizers/__init__.py index b71e43a..73169bf 100644 --- a/randomizers/__init__.py +++ b/randomizers/__init__.py @@ -5,13 +5,13 @@ from .endless_randomizers import ( ) NODE_CLASS_MAPPINGS = { - "Randomzier_Mayhem": EndlessNode_Mayhem, - "Randomzier_Chaos": EndlessNode_Chaos, - # "Randomzier_Pandemonium": EndlessNode_Pandemonium, + "Randomizer_Mayhem": EndlessNode_Mayhem, + "Randomizer_Chaos": EndlessNode_Chaos, + "Randomizer_Pandemonium": EndlessNode_Pandemonium, } NODE_DISPLAY_NAME_MAPPINGS = { - "Randomzier_Mayhem": "Mayhem Randomizer", - "Randomzier_Chaos": "Chaos Randomizer", - # "Randomzier_Pandemonium": "Pandemonium Randomizer", -} + "Randomizer_Mayhem": "Mayhem Randomizer", + "Randomizer_Chaos": "Chaos Randomizer", + "Randomizer_Pandemonium": "Pandemonium Randomizer", +} \ No newline at end of file diff --git a/randomizers/endless_randomizers.py b/randomizers/endless_randomizers.py index f21c5c5..e11824d 100644 --- a/randomizers/endless_randomizers.py +++ b/randomizers/endless_randomizers.py @@ -1,12 +1,7 @@ import random -# Safe samplers and schedulers for Flux (example set from your flux matrix) -SAFE_SAMPLERS = [ - "DDIM", "Euler", "Euler a", "LMS", "Heun", "DPM2", "DPM2 a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE" -] -SAFE_SCHEDULERS = [ - "Default", "Scheduler A", "Scheduler B" # Replace with actual safe schedulers if known -] +def ensure_order(a, b): + return (a, b) if a <= b else (b, a) class EndlessNode_Mayhem: @classmethod @@ -17,10 +12,14 @@ class EndlessNode_Mayhem: "steps_max": ("INT", {"default": 40, "min": 1, "max": 150}), "cfg_min": ("FLOAT", {"default": 6.0, "min": 1.0, "max": 20.0}), "cfg_max": ("FLOAT", {"default": 12.0, "min": 1.0, "max": 20.0}), - "height_min": ("INT", {"default": 512, "min": 64, "max": 4096}), + "guidance_min": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 6.0}), + "guidance_max": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 6.0}), + "height_min": ("INT", {"default": 512, "min": 256, "max": 4096}), "height_max": ("INT", {"default": 768, "min": 256, "max": 4096}), - "width_min": ("INT", {"default": 512, "min": 64, "max": 4096}), + "width_min": ("INT", {"default": 512, "min": 256, "max": 4096}), "width_max": ("INT", {"default": 768, "min": 256, "max": 4096}), + "flip_dimensions": ("BOOLEAN", {"default": True}), + "divisible_by_64": ("BOOLEAN", {"default": False}), "seed_min": ("INT", {"default": 0, "min": 0, "max": 2**32 - 1}), "seed_max": ("INT", {"default": 8675309, "min": 0, "max": 2**32 - 1}), "seed": ("INT", { @@ -31,27 +30,50 @@ class EndlessNode_Mayhem: } } - RETURN_TYPES = ("INT", "FLOAT", "INT", "INT", "INT") - RETURN_NAMES = ("steps", "cfg_scale", "height", "width", "seed") - FUNCTION = "randomize" + RETURN_TYPES = ("INT", "FLOAT", "FLOAT", "INT", "INT", "INT") + RETURN_NAMES = ("steps", "cfg_scale", "cfg_guidance", "height", "width", "seed") + FUNCTION = "randomize_with_flip" CATEGORY = "Endless 🌊✨/Randomizers" - def randomize(self, steps_min, steps_max, cfg_min, cfg_max, height_min, height_max, width_min, width_max, seed_min, seed_max, seed): + def randomize_with_flip(self, steps_min, steps_max, cfg_min, cfg_max, guidance_min, guidance_max, height_min, height_max, width_min, width_max, flip_dimensions, divisible_by_64, seed_min, seed_max, seed): # Use the seed to ensure reproducible randomness random.seed(seed) - # Ensure dimensions are divisible by 16 and at least 256 - height_min = max(256, (height_min // 16) * 16) - height_max = max(256, (height_max // 16) * 16) - width_min = max(256, (width_min // 16) * 16) - width_max = max(256, (width_max // 16) * 16) + # Set divisibility requirement + divisor = 64 if divisible_by_64 else 16 + # Min and max sanity checks + + steps_min, steps_max = ensure_order(steps_min, steps_max) + cfg_min, cfg_max = ensure_order(cfg_min, cfg_max) + guidance_min, guidance_max = ensure_order(guidance_min, guidance_max) + height_min, height_max = ensure_order(height_min, height_max) + width_min, width_max = ensure_order(width_min, width_max) + seed_min, seed_max = ensure_order(seed_min, seed_max) + + + # Ensure dimensions are divisible by divisor and at least 256 + height_min = max(256, (height_min // divisor) * divisor) + height_max = max(256, (height_max // divisor) * divisor) + width_min = max(256, (width_min // divisor) * divisor) + width_max = max(256, (width_max // divisor) * divisor) + + # Random values steps = random.randint(steps_min, steps_max) cfg_scale = round(random.uniform(cfg_min, cfg_max), 2) - height = random.randint(height_min // 16, height_max // 16) * 16 - width = random.randint(width_min // 16, width_max // 16) * 16 + cfg_guidance = round(random.uniform(guidance_min, guidance_max), 2) + + # Pick values based on user-defined intent + height = random.randint(height_min // divisor, height_max // divisor) * divisor + width = random.randint(width_min // divisor, width_max // divisor) * divisor + + # Flip output if requested + if flip_dimensions and random.random() < 0.5: + width, height = height, width + output_seed = random.randint(seed_min, seed_max) - return (steps, cfg_scale, height, width, output_seed) + return (steps, cfg_scale, cfg_guidance, height, width, output_seed) + class EndlessNode_Chaos: @classmethod @@ -62,10 +84,12 @@ class EndlessNode_Chaos: "steps_max": ("INT", {"default": 40, "min": 1, "max": 150}), "cfg_min": ("FLOAT", {"default": 6.0, "min": 1.0, "max": 20.0}), "cfg_max": ("FLOAT", {"default": 12.0, "min": 1.0, "max": 20.0}), - "height_min": ("INT", {"default": 512, "min": 64, "max": 4096}), - "height_max": ("INT", {"default": 768, "min": 64, "max": 4096}), - "width_min": ("INT", {"default": 512, "min": 64, "max": 4096}), - "width_max": ("INT", {"default": 768, "min": 64, "max": 4096}), + "guidance_min": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 6.0}), + "guidance_max": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 6.0}), + "dimension_min": ("INT", {"default": 512, "min": 256, "max": 4096}), + "dimension_max": ("INT", {"default": 1024, "min": 256, "max": 4096}), + "orientation": (["portrait", "landscape", "square", "random"], {"default": "random"}), + "divisible_by_64": ("BOOLEAN", {"default": False}), "seed_min": ("INT", {"default": 0, "min": 0, "max": 2**32 - 1}), "seed_max": ("INT", {"default": 8675309, "min": 0, "max": 2**32 - 1}), "seed": ("INT", { @@ -76,58 +100,112 @@ class EndlessNode_Chaos: } } - RETURN_TYPES = ("INT", "FLOAT", "INT", "INT", "INT") - RETURN_NAMES = ("steps", "cfg_scale", "height", "width", "seed") - FUNCTION = "randomize_with_flip" + RETURN_TYPES = ("INT", "FLOAT", "FLOAT", "INT", "INT", "INT") + RETURN_NAMES = ("steps", "cfg_scale", "cfg_guidance", "height", "width", "seed") + FUNCTION = "randomize_aspect_ratio_chaos" CATEGORY = "Endless 🌊✨/Randomizers" - def randomize_with_flip(self, steps_min, steps_max, cfg_min, cfg_max, height_min, height_max, width_min, width_max, seed_min, seed_max, seed): + def randomize_aspect_ratio_chaos(self, steps_min, steps_max, cfg_min, cfg_max, guidance_min, guidance_max, dimension_min, dimension_max, orientation, divisible_by_64, seed_min, seed_max, seed): # Use the seed to ensure reproducible randomness random.seed(seed) - # Ensure dimensions are divisible by 16 and at least 256 - height_min = max(256, (height_min // 16) * 16) - height_max = max(256, (height_max // 16) * 16) - width_min = max(256, (width_min // 16) * 16) - width_max = max(256, (width_max // 16) * 16) + # Set divisibility requirement + divisor = 64 if divisible_by_64 else 16 + + # Min and max sanity checks + + steps_min, steps_max = ensure_order(steps_min, steps_max) + cfg_min, cfg_max = ensure_order(cfg_min, cfg_max) + guidance_min, guidance_max = ensure_order(guidance_min, guidance_max) + dimension_min, dimension_max = ensure_order(dimension_min, dimension_max) + seed_min, seed_max = ensure_order(seed_min, seed_max) + + + # Ensure min/max dimensions are properly divisible + dimension_min = max(256, (dimension_min // divisor) * divisor) + dimension_max = max(256, (dimension_max // divisor) * divisor) + + # Common aspect ratios (width:height) + aspect_ratios = [ + (1, 1), # 1:1 Square + (4, 3), # 4:3 Classic + (3, 2), # 3:2 Classic photo + (16, 9), # 16:9 Widescreen + (5, 4), # 5:4 + (5, 3), # 5:3 + (7, 5), # 7:5 + (8, 5), # 8:5 + (192, 100), # 1.92:1 Instagram Stories + (21, 9), # 21:9 Ultrawide (2.33:1) + (9, 21), # 9:21 Ultra-tall mobile + (22, 10), # 2.2:1 70mm Cinema + (28, 10), # 2.8:1 Facebook Cover (approx) + (185, 100), # 1.85:1 Cinema + (239, 100), # 2.39:1 Anamorphic + ] + + # Filter aspect ratios based on orientation + if orientation == "square": + filtered_ratios = [(1, 1)] + elif orientation == "landscape": + filtered_ratios = [(w, h) for w, h in aspect_ratios if w > h] + elif orientation == "portrait": + filtered_ratios = [(h, w) for w, h in aspect_ratios if w > h] # Flip to portrait + filtered_ratios.append((1, 1)) # Include square + else: # random + # Include both orientations for non-square ratios + all_ratios = [] + for w, h in aspect_ratios: + all_ratios.append((w, h)) # Landscape + if w != h: # Don't duplicate square + all_ratios.append((h, w)) # Portrait + filtered_ratios = all_ratios + + # Find valid dimensions for each aspect ratio + valid_dimensions = [] + + for aspect_w, aspect_h in filtered_ratios: + # Try different scales to find dimensions within our bounds + for scale in range(1, 100): # Reasonable scale range + width = (aspect_w * scale * divisor) // divisor * divisor + height = (aspect_h * scale * divisor) // divisor * divisor + + # Check if dimensions are within bounds + if (dimension_min <= width <= dimension_max and + dimension_min <= height <= dimension_max): + valid_dimensions.append((width, height)) + + # If we've exceeded max dimension, no point in larger scales + if width > dimension_max or height > dimension_max: + break + + # Remove duplicates and ensure we have at least one option + valid_dimensions = list(set(valid_dimensions)) + + if not valid_dimensions: + # Fallback to square if no valid aspect ratios found + size = (dimension_min // divisor) * divisor + valid_dimensions = [(size, size)] + + # Choose random dimensions + width, height = random.choice(valid_dimensions) + + # Generate other random values steps = random.randint(steps_min, steps_max) cfg_scale = round(random.uniform(cfg_min, cfg_max), 2) - - # Randomly flip height and width with 50% chance - if random.random() < 0.5: - height = random.randint(height_min // 16, height_max // 16) * 16 - width = random.randint(width_min // 16, width_max // 16) * 16 - else: - width = random.randint(height_min // 16, height_max // 16) * 16 - height = random.randint(width_min // 16, width_max // 16) * 16 - output_seed = random.randint(seed_min, seed_max) - return (steps, cfg_scale, height, width, output_seed) + cfg_guidance = round(random.uniform(guidance_min, guidance_max), 2) + + return (steps, cfg_scale, cfg_guidance, height, width, output_seed) + class EndlessNode_Pandemonium: @classmethod def INPUT_TYPES(cls): return { "required": { - "steps_min": ("INT", {"default": 20, "min": 1, "max": 150}), - "steps_max": ("INT", {"default": 40, "min": 1, "max": 150}), - "cfg_min": ("FLOAT", {"default": 6.0, "min": 1.0, "max": 20.0}), - "cfg_max": ("FLOAT", {"default": 12.0, "min": 1.0, "max": 20.0}), - "height_min": ("INT", {"default": 512, "min": 64, "max": 4096}), - "height_max": ("INT", {"default": 768, "min": 64, "max": 4096}), - "width_min": ("INT", {"default": 512, "min": 64, "max": 4096}), - "width_max": ("INT", {"default": 768, "min": 64, "max": 4096}), - "seed_min": ("INT", {"default": 0, "min": 0, "max": 2**32-1}), - "seed_max": ("INT", {"default": 8675309, "min": 0, "max": 2**32 - 1}), - "samplers": ("STRING", { - "multiline": True, - "default": "euler\neuler_ancestral\nheun\nheunpp2\ndpm_2\ndpm_2_ancestral\nlms\ndpm_fast\ndpm_adaptive\ndpmpp_2s_ancestral\ndpmpp_sde\ndpmpp_sde_gpu\ndpmpp_2m\ndpmpp_2m_sde\ndpmpp_2m_sde_gpu\ndpmpp_3m_sde\ndpmpp_3m_sde_gpu\nddpm\nlcm\nddim\nuni_pc\nuni_pc_bh2" - }), - "schedulers": ("STRING", { - "multiline": True, - "default": "normal\nkarras\nexponential\nsgm_uniform\nsimple\nddim_uniform\nbeta" - }), + "divisible_by_64": ("BOOLEAN", {"default": False}), "seed": ("INT", { "default": 0, "min": 0, @@ -136,38 +214,60 @@ class EndlessNode_Pandemonium: } } - RETURN_TYPES = ("INT", "FLOAT", "INT", "INT", "INT", "STRING", "STRING") - RETURN_NAMES = ("steps", "cfg_scale", "height", "width", "seed", "sampler", "scheduler") - FUNCTION = "randomize_all" + RETURN_TYPES = ("INT", "FLOAT", "FLOAT", "INT", "INT", "INT") + RETURN_NAMES = ("steps", "cfg_scale", "cfg_guidance", "height", "width", "seed") + FUNCTION = "randomize_aspect_ratio_mayhem" CATEGORY = "Endless 🌊✨/Randomizers" - def randomize_all(self, steps_min, steps_max, cfg_min, cfg_max, height_min, height_max, width_min, width_max, seed_min, seed_max, samplers, schedulers, seed): - # Use the seed to ensure reproducible randomness + def randomize_aspect_ratio_mayhem(self, divisible_by_64, seed): + import random random.seed(seed) - - # Ensure dimensions are divisible by 16 and at least 256 - height_min = max(256, (height_min // 16) * 16) - height_max = max(256, (height_max // 16) * 16) - width_min = max(256, (width_min // 16) * 16) - width_max = max(256, (width_max // 16) * 16) - - steps = random.randint(steps_min, steps_max) - cfg_scale = round(random.uniform(cfg_min, cfg_max), 2) - height = random.randint(height_min // 16, height_max // 16) * 16 - width = random.randint(width_min // 16, width_max // 16) * 16 - output_seed = random.randint(seed_min, seed_max) - # Parse samplers and schedulers from input strings - sampler_list = [s.strip() for s in samplers.splitlines() if s.strip()] - scheduler_list = [s.strip() for s in schedulers.splitlines() if s.strip()] - - # Fallback to defaults if lists are empty - if not sampler_list: - sampler_list = SAFE_SAMPLERS - if not scheduler_list: - scheduler_list = SAFE_SCHEDULERS + # Fixed internal config + dimension_min = 512 + dimension_max = 1536 + divisor = 64 if divisible_by_64 else 16 - sampler = random.choice(sampler_list) - scheduler = random.choice(scheduler_list) + dimension_min = max(256, (dimension_min // divisor) * divisor) + dimension_max = max(256, (dimension_max // divisor) * divisor) + + # All aspect ratios (landscape + portrait + square) + aspect_ratios = [ + (1, 1), + (4, 3), (3, 2), + (16, 9), (9, 16), + (5, 4), (4, 5), + (5, 3), (3, 5), + (7, 5), (5, 7), + (8, 5), (5, 8), + (192, 100), (100, 192), + (21, 9), (9, 21), + (22, 10), (10, 22), + (28, 10), (10, 28), + (185, 100), (100, 185), + (239, 100), (100, 239) + ] + + valid_dimensions = [] + + for aspect_w, aspect_h in aspect_ratios: + for scale in range(1, 100): + width = (aspect_w * scale * divisor) // divisor * divisor + height = (aspect_h * scale * divisor) // divisor * divisor + if dimension_min <= width <= dimension_max and dimension_min <= height <= dimension_max: + valid_dimensions.append((width, height)) + if width > dimension_max or height > dimension_max: + break + + if not valid_dimensions: + fallback = (dimension_min // divisor) * divisor + valid_dimensions = [(fallback, fallback)] + + width, height = random.choice(valid_dimensions) + steps = random.randint(5, 60) + cfg_scale = round(random.uniform(0, 15), 2) + cfg_guidance = round(random.uniform(1, 4), 2) + output_seed = random.randint(0, seed) + + return (steps, cfg_scale, cfg_guidance, height, width, output_seed) - return (steps, cfg_scale, height, width, output_seed, sampler, scheduler) \ No newline at end of file