From 0a1cbe4990c83c3d469fc8f9d652ed341ea43bdf Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 9 Feb 2026 16:21:56 -0800 Subject: [PATCH 1/6] fix(seeds): reduce seed range from 64-bit to 32-bit across all nodes JS Math.random() only has 53 bits of integer precision, making the 64-bit range produce non-uniform values. 32-bit (0xFFFFFFFF) is the standard ComfyUI seed range and works correctly in both Python and JS. Also removes the redundant mode input from SeedHistoryNode, relying on ComfyUI's built-in control_after_generate widget instead. --- kikotools/tools/kiko_film_grain/node.py | 2 +- kikotools/tools/seed_history/logic.py | 14 +++--- kikotools/tools/seed_history/node.py | 66 +++---------------------- tests/unit/tools/test_seed_history.py | 24 ++++----- web/seed_history_ui.js | 2 +- 5 files changed, 27 insertions(+), 81 deletions(-) diff --git a/kikotools/tools/kiko_film_grain/node.py b/kikotools/tools/kiko_film_grain/node.py index 2fbd86e..61f0a8b 100644 --- a/kikotools/tools/kiko_film_grain/node.py +++ b/kikotools/tools/kiko_film_grain/node.py @@ -75,7 +75,7 @@ class KikoFilmGrainNode(ComfyAssetsBaseNode): { "default": 0, "min": 0, - "max": 0xFFFFFFFFFFFFFFFF, + "max": 0xFFFFFFFF, # 2**32 - 1 "description": "Random seed for grain pattern generation", }, ), diff --git a/kikotools/tools/seed_history/logic.py b/kikotools/tools/seed_history/logic.py index c5fde46..28994ac 100644 --- a/kikotools/tools/seed_history/logic.py +++ b/kikotools/tools/seed_history/logic.py @@ -10,14 +10,14 @@ def generate_random_seed() -> int: Generate a cryptographically strong random seed value. Returns: - Random integer in the valid ComfyUI seed range + Random integer in the valid ComfyUI seed range (0 to 2**32 - 1) """ - return random.randint(0, 0xFFFFFFFFFFFFFFFF) + return random.randint(0, 0xFFFFFFFF) # 2**32 - 1 def validate_seed_value(seed: Any) -> bool: """ - Validate that a seed value is within acceptable range. + Validate that a seed value is within acceptable range (0 to 2**32 - 1). Args: seed: Seed value to validate @@ -30,7 +30,7 @@ def validate_seed_value(seed: Any) -> bool: try: seed_int = int(seed) - return 0 <= seed_int <= 0xFFFFFFFFFFFFFFFF + return 0 <= seed_int <= 0xFFFFFFFF # 2**32 - 1 except (ValueError, TypeError): return False @@ -54,11 +54,11 @@ def sanitize_seed_value(seed: Any) -> int: try: seed_int = int(seed) - # Clamp to valid range + # Clamp to valid range (0 to 2**32 - 1) if seed_int < 0: seed_int = 0 - elif seed_int > 0xFFFFFFFFFFFFFFFF: - seed_int = 0xFFFFFFFFFFFFFFFF + elif seed_int > 0xFFFFFFFF: + seed_int = 0xFFFFFFFF return seed_int diff --git a/kikotools/tools/seed_history/node.py b/kikotools/tools/seed_history/node.py index f846f8c..0d20d91 100644 --- a/kikotools/tools/seed_history/node.py +++ b/kikotools/tools/seed_history/node.py @@ -27,26 +27,10 @@ class SeedHistoryNode(ComfyAssetsBaseNode): { "default": 12345, "min": 0, - "max": 0xFFFFFFFFFFFFFFFF, + "max": 0xFFFFFFFF, # 2**32 - 1 "control_after_generate": True, "tooltip": "Seed value for generation processes. " - "Auto-increments/decrements after each run based on mode.", - }, - ), - }, - "optional": { - "mode": ( - [ - "", - "fixed", - "increment", - "decrement", - "randomize", - ], # Added empty string for legacy workflows - { - "default": "fixed", - "tooltip": "Seed behavior after generation: " - "fixed (no change), increment (+1), decrement (-1), or randomize (new random)", + "Use 'control after generate' to set behavior after each run.", }, ), }, @@ -57,51 +41,19 @@ class SeedHistoryNode(ComfyAssetsBaseNode): FUNCTION = "output_seed" CATEGORY = "🫶 ComfyAssets/🌱 Seeds" - @classmethod - def VALIDATE_INPUTS(cls, seed, mode="fixed"): - """Validate inputs and handle legacy workflows.""" - # Handle empty or missing mode from old workflows (legacy support) - if mode is None or mode == "" or mode == "undefined": - return True # Will use default "fixed" in output_seed - - # Validate mode is in allowed list - valid_modes = ["fixed", "increment", "decrement", "randomize"] - if mode not in valid_modes: - return f"Invalid mode: {mode}. Must be one of {valid_modes}" - - return True - - def output_seed(self, seed: int, mode: str = "fixed") -> Tuple[int]: + def output_seed(self, seed: int) -> Tuple[int]: """ Output the seed value for use in other nodes. Args: seed: Input seed value - mode: Seed mode (fixed, increment, decrement, randomize) - not used in output, - but controls the widget behavior via control_after_generate Returns: Tuple containing the seed value """ try: - # Handle empty mode from old workflows - if not mode or mode == "": - mode = "fixed" - - # Validate mode is in allowed list - valid_modes = ["fixed", "increment", "decrement", "randomize"] - if mode not in valid_modes: - import logging - - logger = logging.getLogger(__name__) - logger.warning( - f"{self.__class__.__name__}: Invalid mode '{mode}'. Using 'fixed'." - ) - mode = "fixed" - # Validate and sanitize the seed if not validate_seed_value(seed): - # Log the validation error but don't raise import logging logger = logging.getLogger(__name__) @@ -112,15 +64,9 @@ class SeedHistoryNode(ComfyAssetsBaseNode): return (12345,) clean_seed = sanitize_seed_value(seed) - - # Note: The mode parameter controls the widget's control_after_generate behavior - # The actual increment/decrement/randomize happens automatically in the UI - # based on the control_after_generate setting and the mode dropdown value - return (clean_seed,) except Exception as e: - # Handle any unexpected errors gracefully import logging logger = logging.getLogger(__name__) @@ -194,7 +140,7 @@ class SeedHistoryNode(ComfyAssetsBaseNode): Returns: Range information string """ - max_seed = 0xFFFFFFFFFFFFFFFF + max_seed = 0xFFFFFFFF # 2**32 - 1 return f"Valid range: 0 to {max_seed:,} ({hex(max_seed)})" @classmethod @@ -218,7 +164,7 @@ class SeedHistoryNode(ComfyAssetsBaseNode): Returns: True if seed is in valid range """ - return 0 <= seed <= 0xFFFFFFFFFFFFFFFF + return 0 <= seed <= 0xFFFFFFFF # 2**32 - 1 def __str__(self) -> str: """String representation of the node.""" @@ -230,6 +176,6 @@ class SeedHistoryNode(ComfyAssetsBaseNode): f"SeedHistoryNode(" f"category='{self.CATEGORY}', " f"function='{self.FUNCTION}', " - f"max_seed={hex(0xFFFFFFFFFFFFFFFF)}" + f"max_seed={hex(0xFFFFFFFF)}" # 2**32 - 1 f")" ) diff --git a/tests/unit/tools/test_seed_history.py b/tests/unit/tools/test_seed_history.py index cf30a53..9f54707 100644 --- a/tests/unit/tools/test_seed_history.py +++ b/tests/unit/tools/test_seed_history.py @@ -43,7 +43,7 @@ class TestSeedHistoryNode: assert "min" in seed_config[1] assert "max" in seed_config[1] assert seed_config[1]["min"] == 0 - assert seed_config[1]["max"] == 0xFFFFFFFFFFFFFFFF + assert seed_config[1]["max"] == 0xFFFFFFFF # 2**32 - 1 # Test return types assert SeedHistoryNode.RETURN_TYPES == ("INT",) @@ -56,7 +56,7 @@ class TestSeedHistoryNode: node = SeedHistoryNode() # Test various valid seeds - test_seeds = [0, 12345, 999999, 0xFFFFFFFFFFFFFFFF] + test_seeds = [0, 12345, 999999, 0xFFFFFFFF] # 2**32 - 1 for seed in test_seeds: result = node.output_seed(seed) @@ -73,7 +73,7 @@ class TestSeedHistoryNode: assert result == (12345,) # Fallback # Test seeds too large - result = node.output_seed(0xFFFFFFFFFFFFFFFF + 1) + result = node.output_seed(0xFFFFFFFF + 1) # 2**32 assert result == (12345,) # Fallback def test_generate_new_seed(self): @@ -100,11 +100,11 @@ class TestSeedHistoryNode: # Valid seeds assert node.validate_seed_input(0) assert node.validate_seed_input(12345) - assert node.validate_seed_input(0xFFFFFFFFFFFFFFFF) + assert node.validate_seed_input(0xFFFFFFFF) # 2**32 - 1 # Invalid seeds assert not node.validate_seed_input(-1) - assert not node.validate_seed_input(0xFFFFFFFFFFFFFFFF + 1) + assert not node.validate_seed_input(0xFFFFFFFF + 1) # 2**32 assert not node.validate_seed_input(None) def test_get_seed_info(self): @@ -132,7 +132,7 @@ class TestSeedHistoryNode: range_info = node.get_seed_range_info() assert "Valid range" in range_info # Check for the hex representation which should be in the string - assert "0xffffffffffffffff" in range_info.lower() + assert "0xffffffff" in range_info.lower() # 2**32 - 1 def test_class_methods(self): """Test class methods.""" @@ -143,9 +143,9 @@ class TestSeedHistoryNode: # Test range checking assert SeedHistoryNode.is_seed_in_range(0) assert SeedHistoryNode.is_seed_in_range(12345) - assert SeedHistoryNode.is_seed_in_range(0xFFFFFFFFFFFFFFFF) + assert SeedHistoryNode.is_seed_in_range(0xFFFFFFFF) # 2**32 - 1 assert not SeedHistoryNode.is_seed_in_range(-1) - assert not SeedHistoryNode.is_seed_in_range(0xFFFFFFFFFFFFFFFF + 1) + assert not SeedHistoryNode.is_seed_in_range(0xFFFFFFFF + 1) # 2**32 class TestSeedHistoryLogic: @@ -168,11 +168,11 @@ class TestSeedHistoryLogic: # Valid seeds assert validate_seed_value(0) assert validate_seed_value(12345) - assert validate_seed_value(0xFFFFFFFFFFFFFFFF) + assert validate_seed_value(0xFFFFFFFF) # 2**32 - 1 # Invalid seeds assert not validate_seed_value(-1) - assert not validate_seed_value(0xFFFFFFFFFFFFFFFF + 1) + assert not validate_seed_value(0xFFFFFFFF + 1) # 2**32 assert not validate_seed_value(None) assert not validate_seed_value("invalid") assert not validate_seed_value([]) @@ -182,7 +182,7 @@ class TestSeedHistoryLogic: # Valid seeds should pass through assert sanitize_seed_value(12345) == 12345 assert sanitize_seed_value(0) == 0 - assert sanitize_seed_value(0xFFFFFFFFFFFFFFFF) == 0xFFFFFFFFFFFFFFFF + assert sanitize_seed_value(0xFFFFFFFF) == 0xFFFFFFFF # 2**32 - 1 # String numbers should convert assert sanitize_seed_value("12345") == 12345 @@ -190,7 +190,7 @@ class TestSeedHistoryLogic: # Out of range should clamp assert sanitize_seed_value(-100) == 0 - assert sanitize_seed_value(0xFFFFFFFFFFFFFFFF + 100) == 0xFFFFFFFFFFFFFFFF + assert sanitize_seed_value(0xFFFFFFFF + 100) == 0xFFFFFFFF # clamp to 2**32 - 1 # Invalid should raise try: diff --git a/web/seed_history_ui.js b/web/seed_history_ui.js index 363d7c0..2ebf6f8 100644 --- a/web/seed_history_ui.js +++ b/web/seed_history_ui.js @@ -296,7 +296,7 @@ app.registerExtension({ // Generate new random seed nodeType.prototype.generateRandomSeed = function () { - const newSeed = Math.floor(Math.random() * 0xFFFFFFFFFFFFFFFF); + const newSeed = Math.floor(Math.random() * 0xFFFFFFFF); // 2**32 - 1 const seedWidget = this.widgets?.find(w => w.name === "seed"); if (seedWidget) { From 8773c8f249341510672e198067295191acd0dec6 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 9 Feb 2026 16:22:03 -0800 Subject: [PATCH 2/6] feat(latent): add batch_size as 4th output from EmptyLatentBatchNode Exposes batch_size as an output so downstream nodes can reference it directly without needing a separate input. --- kikotools/tools/empty_latent_batch/node.py | 10 +++++----- tests/unit/tools/test_empty_latent_batch.py | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/kikotools/tools/empty_latent_batch/node.py b/kikotools/tools/empty_latent_batch/node.py index 99a0b6e..82411a9 100644 --- a/kikotools/tools/empty_latent_batch/node.py +++ b/kikotools/tools/empty_latent_batch/node.py @@ -93,14 +93,14 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode): } } - RETURN_TYPES = ("LATENT", "INT", "INT") - RETURN_NAMES = ("latent", "width", "height") + RETURN_TYPES = ("LATENT", "INT", "INT", "INT") + RETURN_NAMES = ("latent", "width", "height", "batch_size") FUNCTION = "create_empty_latent" CATEGORY = "🫶 ComfyAssets/📦 Latents" def create_empty_latent( self, preset: str, width: int, height: int, batch_size: int - ) -> Tuple[Dict[str, torch.Tensor], int, int]: + ) -> Tuple[Dict[str, torch.Tensor], int, int, int]: """ Create empty latent tensor with specified dimensions and batch size. @@ -111,7 +111,7 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode): batch_size: Number of latents in the batch Returns: - Tuple containing (latent dictionary with 'samples' tensor, width, height) + Tuple containing (latent dict, width, height, batch_size) """ try: # Extract original preset name from formatted string if needed @@ -160,7 +160,7 @@ class EmptyLatentBatchNode(ComfyAssetsBaseNode): f"(pixel dims: {final_width}×{final_height})" ) - return (latent_dict, final_width, final_height) + return (latent_dict, final_width, final_height, batch_size) except Exception as e: # Handle any unexpected errors gracefully diff --git a/tests/unit/tools/test_empty_latent_batch.py b/tests/unit/tools/test_empty_latent_batch.py index 1692fc6..5187804 100644 --- a/tests/unit/tools/test_empty_latent_batch.py +++ b/tests/unit/tools/test_empty_latent_batch.py @@ -131,8 +131,8 @@ class TestEmptyLatentBatchNode: def test_node_attributes(self): """Test node class attributes.""" - assert EmptyLatentBatchNode.RETURN_TYPES == ("LATENT", "INT", "INT") - assert EmptyLatentBatchNode.RETURN_NAMES == ("latent", "width", "height") + assert EmptyLatentBatchNode.RETURN_TYPES == ("LATENT", "INT", "INT", "INT") + assert EmptyLatentBatchNode.RETURN_NAMES == ("latent", "width", "height", "batch_size") assert EmptyLatentBatchNode.FUNCTION == "create_empty_latent" assert EmptyLatentBatchNode.CATEGORY == "🫶 ComfyAssets/📦 Latents" @@ -141,13 +141,14 @@ class TestEmptyLatentBatchNode: result = self.node.create_empty_latent("custom", 512, 512, 1) assert isinstance(result, tuple) - assert len(result) == 3 # Now returns (latent, width, height) + assert len(result) == 4 # Returns (latent, width, height, batch_size) - latent_dict, width, height = result + latent_dict, width, height, batch_size = result assert isinstance(latent_dict, dict) assert "samples" in latent_dict assert width == 512 assert height == 512 + assert batch_size == 1 samples = latent_dict["samples"] assert isinstance(samples, torch.Tensor) @@ -155,12 +156,13 @@ class TestEmptyLatentBatchNode: def test_create_empty_latent_with_batch(self): """Test empty latent creation with batch size.""" - batch_size = 3 - result = self.node.create_empty_latent("custom", 1024, 768, batch_size) + input_batch_size = 3 + result = self.node.create_empty_latent("custom", 1024, 768, input_batch_size) - latent_dict, width, height = result + latent_dict, width, height, batch_size = result assert width == 1024 assert height == 768 + assert batch_size == 3 samples = latent_dict["samples"] assert samples.shape == (3, 4, 96, 128) # batch=3, 768/8=96, 1024/8=128 @@ -169,10 +171,11 @@ class TestEmptyLatentBatchNode: # Input dimensions not divisible by 8 result = self.node.create_empty_latent("custom", 513, 515, 1) - latent_dict, width, height = result + latent_dict, width, height, batch_size = result # Dimensions should be rounded UP to nearest multiple of 8 assert width == 520 # 513 -> 520 assert height == 520 # 515 -> 520 + assert batch_size == 1 samples = latent_dict["samples"] # Should be adjusted to 520x520 -> 65x65 latent assert samples.shape == (1, 4, 65, 65) From 2cb22622938661245465f9f7df0e807e1aa186bb Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 9 Feb 2026 16:22:10 -0800 Subject: [PATCH 3/6] chore(local_image_loader): update saved paths and selections --- kikotools/tools/local_image_loader/config.json | 2 +- kikotools/tools/local_image_loader/selections.json | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/kikotools/tools/local_image_loader/config.json b/kikotools/tools/local_image_loader/config.json index d06b7b7..f250a17 100644 --- a/kikotools/tools/local_image_loader/config.json +++ b/kikotools/tools/local_image_loader/config.json @@ -1,5 +1,5 @@ { - "last_path": "/home/vito/ai-apps/ComfyUI/output/vids/images", + "last_path": "/home/vito/ai-apps/ComfyUI/output/vids", "saved_paths": [ "/home/vito/ai-apps/ComfyUI-3.12/output/2025-05-01", "/home/vito/ai-apps/ComfyUI-3.12/output/", diff --git a/kikotools/tools/local_image_loader/selections.json b/kikotools/tools/local_image_loader/selections.json index d27dcf4..c00e6d0 100644 --- a/kikotools/tools/local_image_loader/selections.json +++ b/kikotools/tools/local_image_loader/selections.json @@ -11,7 +11,7 @@ }, "18": { "image": { - "path": "/home/vito/ai-apps/ComfyUI/output/ComfyUI_00002_.png" + "path": "/home/vito/ai-apps/ComfyUI/output/vids/KikoSave_00005.png" } }, "445": { @@ -66,12 +66,22 @@ }, "517": { "image": { - "path": "/home/vito/ai-apps/ComfyUI/output/vids/images/kittybear_00004.png" + "path": "/home/vito/ai-apps/ComfyUI/output/vids/KikoSave_00005.png" } }, "144": { "image": { "path": "/home/vito/ai-apps/ComfyUI/output/2025-04-24/ComfyUI_00002_.png" } + }, + "569": { + "image": { + "path": "/home/vito/ai-apps/ComfyUI/output/vids/KikoSave_00008.png" + } + }, + "136": { + "image": { + "path": "/home/vito/ai-apps/ComfyUI/output/vids/KikoSave_00005.png" + } } } \ No newline at end of file From 36212adf731ac4a4285ee2b45fb8e6054544601f Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 9 Feb 2026 16:25:30 -0800 Subject: [PATCH 4/6] ci: reduce test matrix to Python 3.11, 3.12, 3.13 Drop 3.8, 3.9, and 3.10 which are no longer needed. --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0b2403e..63f7a7a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v6 From d1a5282ca04b194033394ebbc1c6315b6ad26c04 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 9 Feb 2026 16:26:08 -0800 Subject: [PATCH 5/6] style: fix Black formatting issues Remove extra blank line in local_image_loader/node.py and wrap long tuple assertion in test_empty_latent_batch.py. --- kikotools/tools/local_image_loader/node.py | 1 - tests/unit/tools/test_empty_latent_batch.py | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/kikotools/tools/local_image_loader/node.py b/kikotools/tools/local_image_loader/node.py index 90ebedf..c713381 100644 --- a/kikotools/tools/local_image_loader/node.py +++ b/kikotools/tools/local_image_loader/node.py @@ -8,7 +8,6 @@ from typing import Dict, Any, Tuple from ...base.base_node import ComfyAssetsBaseNode from .logic import load_image_from_path, create_empty_tensor - NODE_DIR = os.path.dirname(os.path.abspath(__file__)) SELECTIONS_FILE = os.path.join(NODE_DIR, "selections.json") CONFIG_FILE = os.path.join(NODE_DIR, "config.json") diff --git a/tests/unit/tools/test_empty_latent_batch.py b/tests/unit/tools/test_empty_latent_batch.py index 5187804..5908c99 100644 --- a/tests/unit/tools/test_empty_latent_batch.py +++ b/tests/unit/tools/test_empty_latent_batch.py @@ -132,7 +132,12 @@ class TestEmptyLatentBatchNode: def test_node_attributes(self): """Test node class attributes.""" assert EmptyLatentBatchNode.RETURN_TYPES == ("LATENT", "INT", "INT", "INT") - assert EmptyLatentBatchNode.RETURN_NAMES == ("latent", "width", "height", "batch_size") + assert EmptyLatentBatchNode.RETURN_NAMES == ( + "latent", + "width", + "height", + "batch_size", + ) assert EmptyLatentBatchNode.FUNCTION == "create_empty_latent" assert EmptyLatentBatchNode.CATEGORY == "🫶 ComfyAssets/📦 Latents" From 702d0c889cf182da40e2f3ce56a9f22df720d5ff Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 9 Feb 2026 16:29:11 -0800 Subject: [PATCH 6/6] fix(seed_history): accept legacy kwargs for backward compatibility Old workflows may still pass 'mode' to output_seed. Using **kwargs prevents TypeError when ComfyUI invokes with the extra argument. --- kikotools/tools/seed_history/node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kikotools/tools/seed_history/node.py b/kikotools/tools/seed_history/node.py index 0d20d91..3cd40a9 100644 --- a/kikotools/tools/seed_history/node.py +++ b/kikotools/tools/seed_history/node.py @@ -41,12 +41,13 @@ class SeedHistoryNode(ComfyAssetsBaseNode): FUNCTION = "output_seed" CATEGORY = "🫶 ComfyAssets/🌱 Seeds" - def output_seed(self, seed: int) -> Tuple[int]: + def output_seed(self, seed: int, **kwargs) -> Tuple[int]: """ Output the seed value for use in other nodes. Args: seed: Input seed value + **kwargs: Accepts legacy parameters (e.g. mode) for backward compatibility Returns: Tuple containing the seed value