Merge pull request #53 from ComfyAssets/feature/seed-range-and-latent-output
fix: 32-bit seed range, latent batch_size output
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,20 @@ 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, **kwargs) -> 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
|
||||
**kwargs: Accepts legacy parameters (e.g. mode) for backward compatibility
|
||||
|
||||
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 +65,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 +141,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 +165,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 +177,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")"
|
||||
)
|
||||
|
||||
@@ -131,8 +131,13 @@ 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 +146,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 +161,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 +176,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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user