from __future__ import annotations import json import math import random try: from .seed_config import ( build_seed_config_json, build_seed_lock_config_json, configured_seed_from_axes, normalize_reroll_axis, seed_reroll_axis_choices, seed_mode_choices, ) except ImportError: # Allows local smoke tests from the repository root. from seed_config import ( build_seed_config_json, build_seed_lock_config_json, configured_seed_from_axes, normalize_reroll_axis, seed_reroll_axis_choices, seed_mode_choices, ) SXCP_SEED_CONFIG = "SXCP_SEED_CONFIG" SDXL_BUCKET_RESOLUTIONS = [ {"orientation": "portrait", "width": 896, "height": 1792, "aspect": 0.50, "mp": 1.61}, {"orientation": "portrait", "width": 960, "height": 1664, "aspect": 0.58, "mp": 1.60}, {"orientation": "portrait", "width": 1024, "height": 1600, "aspect": 0.64, "mp": 1.64}, {"orientation": "portrait", "width": 1088, "height": 1472, "aspect": 0.74, "mp": 1.60}, {"orientation": "portrait", "width": 1152, "height": 1408, "aspect": 0.82, "mp": 1.62}, {"orientation": "portrait", "width": 1216, "height": 1344, "aspect": 0.90, "mp": 1.63}, {"orientation": "square", "width": 1280, "height": 1280, "aspect": 1.00, "mp": 1.64}, {"orientation": "landscape", "width": 1344, "height": 1216, "aspect": 1.11, "mp": 1.63}, {"orientation": "landscape", "width": 1408, "height": 1152, "aspect": 1.22, "mp": 1.62}, {"orientation": "landscape", "width": 1472, "height": 1088, "aspect": 1.35, "mp": 1.60}, {"orientation": "landscape", "width": 1536, "height": 1024, "aspect": 1.50, "mp": 1.57}, ] KREA2_API_ASPECT_RATIOS = ["1:1", "4:3", "3:2", "16:9", "2.35:1", "4:5", "2:3", "9:16"] KREA2_ASPECT_RATIOS = KREA2_API_ASPECT_RATIOS + ["8:9", "21:9", "9:21", "3:1", "1:3"] KREA2_MEGAPIXEL_PRESETS = [ "1.0MP", "1.25MP", "1.5MP", "1.75MP", "2.0MP", "2.25MP", "2.5MP", "2.75MP", "3.0MP", "3.25MP", "3.5MP", "3.75MP", "4.0MP", "max_for_aspect", ] class SxCPSeedControl: SEED_AXES = ( "category", "subcategory", "content", "clothing", "person", "scene", "pose", "role", "expression", "composition", ) @classmethod def INPUT_TYPES(cls): seed_spec = {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1} required = {} for axis in cls.SEED_AXES: required[f"{axis}_seed_mode"] = (seed_mode_choices(), {"default": "auto"}) required[f"{axis}_seed"] = ("INT", seed_spec) return {"required": required} RETURN_TYPES = (SXCP_SEED_CONFIG, "STRING") RETURN_NAMES = ("seed_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" @classmethod def IS_CHANGED(cls, *args, **kwargs): values = list(args) + list(kwargs.values()) if "random" in values: return random.random() return tuple(args), tuple(sorted(kwargs.items())) @classmethod def _summary(cls, config_json): try: config = json.loads(config_json) except (TypeError, ValueError, json.JSONDecodeError): return "invalid seed config" parts = [] for axis in cls.SEED_AXES: try: value = int(config.get(f"{axis}_seed", -1)) except (TypeError, ValueError): value = -1 parts.append(f"{axis}={'follow_main' if value < 0 else value}") return "resolved seeds: " + "; ".join(parts) def build( self, category_seed_mode, category_seed, subcategory_seed_mode, subcategory_seed, content_seed_mode, content_seed, clothing_seed_mode, clothing_seed, person_seed_mode, person_seed, scene_seed_mode, scene_seed, pose_seed_mode, pose_seed, role_seed_mode, role_seed, expression_seed_mode, expression_seed, composition_seed_mode, composition_seed, ): config = build_seed_config_json( category_seed=category_seed, subcategory_seed=subcategory_seed, content_seed=content_seed, clothing_seed=clothing_seed, person_seed=person_seed, scene_seed=scene_seed, pose_seed=pose_seed, role_seed=role_seed, expression_seed=expression_seed, composition_seed=composition_seed, category_seed_mode=category_seed_mode, subcategory_seed_mode=subcategory_seed_mode, content_seed_mode=content_seed_mode, clothing_seed_mode=clothing_seed_mode, person_seed_mode=person_seed_mode, scene_seed_mode=scene_seed_mode, pose_seed_mode=pose_seed_mode, role_seed_mode=role_seed_mode, expression_seed_mode=expression_seed_mode, composition_seed_mode=composition_seed_mode, ) return ( config, self._summary(config), ) class SxCPGlobalSeed: @classmethod def INPUT_TYPES(cls): seed_spec = {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1} return { "required": { "global_seed": ("INT", seed_spec), } } RETURN_TYPES = ("INT", SXCP_SEED_CONFIG, "STRING") RETURN_NAMES = ("seed", "seed_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, global_seed): seed = max(0, min(0xFFFFFFFF, int(global_seed))) config = build_seed_lock_config_json(base_seed=seed, reroll_axis="none", reroll_seed=-1) return seed, config, f"global seed {seed}; all axes locked" class SxCPSeedLocker: @classmethod def INPUT_TYPES(cls): seed_spec = {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1} reroll_seed_spec = {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1} return { "required": { "base_seed": ("INT", seed_spec), "reroll_axis": ( seed_reroll_axis_choices(), {"default": "none"}, ), "reroll_seed": ("INT", reroll_seed_spec), } } RETURN_TYPES = (SXCP_SEED_CONFIG, "STRING") RETURN_NAMES = ("seed_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, base_seed, reroll_axis, reroll_seed): normalized_axis = normalize_reroll_axis(reroll_axis) config = build_seed_lock_config_json(base_seed=base_seed, reroll_axis=normalized_axis, reroll_seed=reroll_seed) summary = f"base {base_seed}; reroll {normalized_axis} with {'main seed' if int(reroll_seed) < 0 else reroll_seed}" return config, summary class SxCPSDXLBucketSize: @classmethod def INPUT_TYPES(cls): return { "required": { "orientation": (["any", "portrait", "square", "landscape"], {"default": "any"}), "seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}), "row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}), "bucket_index": ("INT", {"default": 0, "min": 0, "max": len(SDXL_BUCKET_RESOLUTIONS), "step": 1}), }, "optional": { "seed_config": (SXCP_SEED_CONFIG,), }, } RETURN_TYPES = ("INT", "INT", "STRING", "STRING", "FLOAT", "FLOAT", "INT", "STRING") RETURN_NAMES = ("width", "height", "resolution", "orientation", "aspect", "megapixels", "bucket_index", "summary") FUNCTION = "build" CATEGORY = "prompt_builder/util" @staticmethod def _configured_bucket_seed(seed_config): return configured_seed_from_axes( seed_config, ("composition", "content"), extra_keys=("seed", "global_seed"), ) @classmethod def IS_CHANGED(cls, *args, **kwargs): seed_value = kwargs.get("seed") if seed_value is None and len(args) > 1: seed_value = args[1] bucket_index = kwargs.get("bucket_index") if bucket_index is None and len(args) > 3: bucket_index = args[3] seed_config = kwargs.get("seed_config", "") if not seed_config and len(args) > 4: seed_config = args[4] try: seed = int(seed_value) except (TypeError, ValueError): seed = -1 try: index = int(bucket_index) except (TypeError, ValueError): index = 0 if index <= 0 and seed < 0 and cls._configured_bucket_seed(seed_config) is None: return random.random() return tuple(args), tuple(sorted(kwargs.items())) def build(self, orientation, seed, row_number, bucket_index, seed_config=""): orientation = str(orientation or "any").strip().lower() pool = [ (index + 1, bucket) for index, bucket in enumerate(SDXL_BUCKET_RESOLUTIONS) if orientation == "any" or bucket["orientation"] == orientation ] if not pool: pool = list(enumerate(SDXL_BUCKET_RESOLUTIONS, start=1)) if int(bucket_index) > 0: pool_position = max(1, min(len(pool), int(bucket_index))) - 1 else: configured_seed = self._configured_bucket_seed(seed_config) if configured_seed is None and int(seed) < 0: rng = random.Random(random.getrandbits(64)) else: bucket_seed = configured_seed if configured_seed is not None else int(seed) rng = random.Random(f"sdxl_bucket:{bucket_seed}:{int(row_number)}:{orientation}") pool_position = rng.randrange(len(pool)) selected_index, selected = pool[pool_position] width = int(selected["width"]) height = int(selected["height"]) selected_orientation = str(selected["orientation"]) aspect = float(selected["aspect"]) mp = float(selected["mp"]) resolution = f"{width}x{height}" summary = ( f"{selected_orientation} bucket {pool_position + 1}/{len(pool)} " f"(table {selected_index}): {resolution}, aspect {aspect:.2f}, {mp:.2f} MP" ) return width, height, resolution, selected_orientation, aspect, mp, selected_index, summary class SxCPKrea2ResolutionSelector: @classmethod def INPUT_TYPES(cls): return { "required": { "megapixels": (KREA2_MEGAPIXEL_PRESETS, {"default": "1.0MP"}), "aspect_ratio": (KREA2_ASPECT_RATIOS, {"default": "1:1"}), }, } RETURN_TYPES = ("INT", "INT", "STRING", "STRING", "STRING", "STRING", "FLOAT", "FLOAT", "STRING", "STRING", "STRING") RETURN_NAMES = ( "width", "height", "resolution", "aspect_ratio", "api_aspect_ratio", "api_resolution", "megapixels", "max_megapixels_for_aspect", "orientation", "summary", "config_json", ) FUNCTION = "select" CATEGORY = "prompt_builder/util" @staticmethod def _aspect_value(aspect_ratio, custom_aspect_width, custom_aspect_height, rng): selected = str(aspect_ratio or "1:1").strip() if selected == "random_api": selected = rng.choice(KREA2_API_ASPECT_RATIOS) if selected == "custom": width = max(0.1, float(custom_aspect_width)) height = max(0.1, float(custom_aspect_height)) return selected, width / height try: left, right = selected.split(":", 1) return selected, max(0.01, float(left) / float(right)) except (TypeError, ValueError): return "1:1", 1.0 @staticmethod def _closest_api_aspect(ratio): def parse(value): left, right = value.split(":", 1) return float(left) / float(right) return min(KREA2_API_ASPECT_RATIOS, key=lambda item: abs(math.log(parse(item) / max(0.01, ratio)))) @staticmethod def _continuous_limit_mp(ratio, max_long_edge, max_megapixels): ratio = max(0.01, float(ratio)) max_long = max(16.0, float(max_long_edge)) if ratio >= 1.0: exact_width = max_long exact_height = max_long / ratio else: exact_width = max_long * ratio exact_height = max_long exact_mp = (exact_width * exact_height) / 1_000_000.0 return max(0.01, min(float(max_megapixels), exact_mp)) @staticmethod def _nearby_multiples(value, multiple): scaled = float(value) / float(multiple) values = { int(math.floor(scaled)) * multiple, int(round(scaled)) * multiple, int(math.ceil(scaled)) * multiple, } return {int(v) for v in values if int(v) > 0} @classmethod def _candidate_sizes(cls, ratio, max_long_edge, max_megapixels, multiple): max_long = max(multiple, int(max_long_edge) // multiple * multiple) max_pixels = float(max_megapixels) * 1_000_000.0 candidates = set() for width in range(multiple, max_long + 1, multiple): for height in cls._nearby_multiples(float(width) / ratio, multiple): candidates.add((width, height)) for height in range(multiple, max_long + 1, multiple): for width in cls._nearby_multiples(float(height) * ratio, multiple): candidates.add((width, height)) valid = [] for width, height in candidates: if width < multiple or height < multiple: continue if max(width, height) > max_long: continue if width * height > max_pixels + 1: continue valid.append((width, height)) return valid @classmethod def _best_size(cls, ratio, target_megapixels, max_long_edge, max_megapixels, multiple): candidates = cls._candidate_sizes(ratio, max_long_edge, max_megapixels, multiple) if not candidates: fallback = max(multiple, int(max_long_edge) // multiple * multiple) return fallback, fallback, (fallback * fallback) / 1_000_000.0, 1.0 target = max((multiple * multiple) / 1_000_000.0, float(target_megapixels)) best = None best_score = None for width, height in candidates: actual_mp = (width * height) / 1_000_000.0 actual_ratio = float(width) / float(height) ratio_error = abs(math.log(actual_ratio / max(0.01, ratio))) mp_error = abs(actual_mp - target) / max(target, 0.01) score = ratio_error * 4.0 + mp_error if best_score is None or score < best_score: best = (width, height, actual_mp, actual_ratio) best_score = score return best @staticmethod def _profile_limits(profile, custom_max_long_edge, custom_max_megapixels): profile = str(profile or "turbo_local_2k").strip() if profile == "raw_local_1k": return 1024, 1.05, "Krea2 RAW local explicit size, up to 1K" if profile == "api_hosted_1k": return 1024, 1.05, "Krea hosted API fields, 1K only" if profile == "custom_limit": return max(256, int(custom_max_long_edge)), max(0.10, float(custom_max_megapixels)), "custom explicit size limit" return 2048, 4.20, "Krea2 Turbo local explicit size, up to 2K" @staticmethod def _preset_megapixels(megapixel_preset): value = str(megapixel_preset or "1.0MP").strip() if value.endswith("MP"): try: return float(value[:-2]) except ValueError: return 1.0 return None def select(self, megapixels, aspect_ratio): multiple = 16 profile = "turbo_local_2k" max_long_edge, max_profile_mp, _profile_label = self._profile_limits(profile, 2048, 4.20) resolved_aspect, ratio = self._aspect_value(aspect_ratio, 1.0, 1.0, random.Random("krea2_resolution")) api_aspect_ratio = resolved_aspect if resolved_aspect in KREA2_API_ASPECT_RATIOS else self._closest_api_aspect(ratio) continuous_max_mp = self._continuous_limit_mp(ratio, max_long_edge, max_profile_mp) max_width, max_height, max_actual_mp, max_actual_ratio = self._best_size( ratio, continuous_max_mp, max_long_edge, max_profile_mp, multiple ) preset = str(megapixels or "1.0MP").strip() target_mp = self._preset_megapixels(preset) if preset == "max_for_aspect": target_mp = max_actual_mp if target_mp is None: target_mp = 1.0 clamped = target_mp > max_actual_mp + 0.001 effective_target_mp = min(float(target_mp), max_actual_mp) width, height, actual_mp, actual_ratio = self._best_size( ratio, effective_target_mp, max_long_edge, max_profile_mp, multiple ) orientation = "square" if width > height: orientation = "landscape" elif height > width: orientation = "portrait" resolution = f"{width}x{height}" api_resolution = "1K" summary_parts = [ f"{resolution}", f"{actual_mp:.2f} MP", f"aspect {resolved_aspect} ({actual_ratio:.3f})", f"max for aspect {max_width}x{max_height} / {max_actual_mp:.2f} MP", "Krea2 Turbo 2K", f"API equivalent {api_aspect_ratio} {api_resolution}", ] if clamped: summary_parts.append(f"target {target_mp:.2f} MP clamped to aspect/profile limit") summary = "; ".join(summary_parts) config = { "profile": profile, "width": width, "height": height, "resolution": resolution, "aspect_ratio": resolved_aspect, "aspect_ratio_value": actual_ratio, "target_megapixels": round(float(target_mp), 4), "megapixels": round(actual_mp, 4), "max_width_for_aspect": max_width, "max_height_for_aspect": max_height, "max_megapixels_for_aspect": round(max_actual_mp, 4), "api_aspect_ratio": api_aspect_ratio, "api_resolution": api_resolution, "orientation": orientation, "round_to": multiple, "clamped": clamped, } return ( width, height, resolution, resolved_aspect, api_aspect_ratio, api_resolution, round(actual_mp, 4), round(max_actual_mp, 4), orientation, summary, json.dumps(config, ensure_ascii=True, sort_keys=True), ) NODE_CLASS_MAPPINGS = { "SxCPGlobalSeed": SxCPGlobalSeed, "SxCPSeedControl": SxCPSeedControl, "SxCPSeedLocker": SxCPSeedLocker, "SxCPSDXLBucketSize": SxCPSDXLBucketSize, "SxCPKrea2ResolutionSelector": SxCPKrea2ResolutionSelector, } NODE_DISPLAY_NAME_MAPPINGS = { "SxCPGlobalSeed": "SxCP Global Seed", "SxCPSeedControl": "SxCP Seed Control", "SxCPSeedLocker": "SxCP Seed Locker", "SxCPSDXLBucketSize": "SxCP SDXL Bucket Size", "SxCPKrea2ResolutionSelector": "SxCP Krea2 Resolution Selector", }