Extract seed resolution nodes
This commit is contained in:
+16
-506
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import math
|
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -30,40 +29,6 @@ SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
|
|||||||
SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT"
|
SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT"
|
||||||
SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE"
|
SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE"
|
||||||
|
|
||||||
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",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
COMMON_INPUT_TOOLTIPS = {
|
COMMON_INPUT_TOOLTIPS = {
|
||||||
"row_number": "Generation row to use. Changing it advances the deterministic selection without changing the main seed.",
|
"row_number": "Generation row to use. Changing it advances the deterministic selection without changing the main seed.",
|
||||||
"start_index": "Metadata/output index offset only. It does not limit category pools or random choices.",
|
"start_index": "Metadata/output index offset only. It does not limit category pools or random choices.",
|
||||||
@@ -436,6 +401,10 @@ try:
|
|||||||
accumulator_move_entry,
|
accumulator_move_entry,
|
||||||
accumulator_save_entries,
|
accumulator_save_entries,
|
||||||
)
|
)
|
||||||
|
from .node_seed_resolution import (
|
||||||
|
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
|
)
|
||||||
from .prompt_builder import (
|
from .prompt_builder import (
|
||||||
build_camera_config_json,
|
build_camera_config_json,
|
||||||
build_camera_orbit_config_json,
|
build_camera_orbit_config_json,
|
||||||
@@ -459,8 +428,6 @@ try:
|
|||||||
build_insta_of_pair,
|
build_insta_of_pair,
|
||||||
build_prompt,
|
build_prompt,
|
||||||
build_prompt_from_configs,
|
build_prompt_from_configs,
|
||||||
build_seed_config_json,
|
|
||||||
build_seed_lock_config_json,
|
|
||||||
camera_angle_choices,
|
camera_angle_choices,
|
||||||
camera_detail_choices,
|
camera_detail_choices,
|
||||||
camera_distance_choices,
|
camera_distance_choices,
|
||||||
@@ -504,7 +471,6 @@ try:
|
|||||||
location_theme_choices,
|
location_theme_choices,
|
||||||
location_pool_preset_choices,
|
location_pool_preset_choices,
|
||||||
save_character_profile_payload,
|
save_character_profile_payload,
|
||||||
seed_mode_choices,
|
|
||||||
subcategory_choices,
|
subcategory_choices,
|
||||||
)
|
)
|
||||||
from .caption_naturalizer import naturalize_caption
|
from .caption_naturalizer import naturalize_caption
|
||||||
@@ -520,6 +486,10 @@ except ImportError:
|
|||||||
accumulator_move_entry,
|
accumulator_move_entry,
|
||||||
accumulator_save_entries,
|
accumulator_save_entries,
|
||||||
)
|
)
|
||||||
|
from node_seed_resolution import (
|
||||||
|
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
|
)
|
||||||
from prompt_builder import (
|
from prompt_builder import (
|
||||||
build_camera_config_json,
|
build_camera_config_json,
|
||||||
build_camera_orbit_config_json,
|
build_camera_orbit_config_json,
|
||||||
@@ -543,8 +513,6 @@ except ImportError:
|
|||||||
build_insta_of_pair,
|
build_insta_of_pair,
|
||||||
build_prompt,
|
build_prompt,
|
||||||
build_prompt_from_configs,
|
build_prompt_from_configs,
|
||||||
build_seed_config_json,
|
|
||||||
build_seed_lock_config_json,
|
|
||||||
camera_angle_choices,
|
camera_angle_choices,
|
||||||
camera_detail_choices,
|
camera_detail_choices,
|
||||||
camera_distance_choices,
|
camera_distance_choices,
|
||||||
@@ -588,7 +556,6 @@ except ImportError:
|
|||||||
location_theme_choices,
|
location_theme_choices,
|
||||||
location_pool_preset_choices,
|
location_pool_preset_choices,
|
||||||
save_character_profile_payload,
|
save_character_profile_payload,
|
||||||
seed_mode_choices,
|
|
||||||
subcategory_choices,
|
subcategory_choices,
|
||||||
)
|
)
|
||||||
from caption_naturalizer import naturalize_caption
|
from caption_naturalizer import naturalize_caption
|
||||||
@@ -787,459 +754,6 @@ class SxCPPromptBuilder:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SxCPSeedControl:
|
|
||||||
SEED_AXES = (
|
|
||||||
"category",
|
|
||||||
"subcategory",
|
|
||||||
"content",
|
|
||||||
"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,)
|
|
||||||
RETURN_NAMES = ("seed_config",)
|
|
||||||
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()))
|
|
||||||
|
|
||||||
def build(
|
|
||||||
self,
|
|
||||||
category_seed_mode,
|
|
||||||
category_seed,
|
|
||||||
subcategory_seed_mode,
|
|
||||||
subcategory_seed,
|
|
||||||
content_seed_mode,
|
|
||||||
content_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,
|
|
||||||
):
|
|
||||||
return (
|
|
||||||
build_seed_config_json(
|
|
||||||
category_seed=category_seed,
|
|
||||||
subcategory_seed=subcategory_seed,
|
|
||||||
content_seed=content_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,
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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": (
|
|
||||||
[
|
|
||||||
"none",
|
|
||||||
"category",
|
|
||||||
"subcategory",
|
|
||||||
"content",
|
|
||||||
"person",
|
|
||||||
"scene",
|
|
||||||
"pose",
|
|
||||||
"role",
|
|
||||||
"expression",
|
|
||||||
"composition",
|
|
||||||
"content_pose",
|
|
||||||
"scene_pose",
|
|
||||||
],
|
|
||||||
{"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):
|
|
||||||
config = build_seed_lock_config_json(base_seed=base_seed, reroll_axis=reroll_axis, reroll_seed=reroll_seed)
|
|
||||||
summary = f"base {base_seed}; reroll {reroll_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):
|
|
||||||
if not seed_config:
|
|
||||||
return None
|
|
||||||
if isinstance(seed_config, dict):
|
|
||||||
raw = seed_config
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
raw = json.loads(str(seed_config))
|
|
||||||
except (TypeError, ValueError, json.JSONDecodeError):
|
|
||||||
return None
|
|
||||||
if not isinstance(raw, dict):
|
|
||||||
return None
|
|
||||||
for key in ("composition_seed", "content_seed", "seed", "global_seed"):
|
|
||||||
try:
|
|
||||||
value = int(raw.get(key))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
continue
|
|
||||||
if value >= 0:
|
|
||||||
return value
|
|
||||||
return None
|
|
||||||
|
|
||||||
@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",
|
|
||||||
f"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),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SxCPCameraControl:
|
class SxCPCameraControl:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -3155,11 +2669,9 @@ class SxCPInstaOFPromptPair:
|
|||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"SxCPPromptBuilder": SxCPPromptBuilder,
|
"SxCPPromptBuilder": SxCPPromptBuilder,
|
||||||
"SxCPGlobalSeed": SxCPGlobalSeed,
|
}
|
||||||
"SxCPSeedControl": SxCPSeedControl,
|
NODE_CLASS_MAPPINGS.update(SEED_RESOLUTION_NODE_CLASS_MAPPINGS)
|
||||||
"SxCPSeedLocker": SxCPSeedLocker,
|
NODE_CLASS_MAPPINGS.update({
|
||||||
"SxCPSDXLBucketSize": SxCPSDXLBucketSize,
|
|
||||||
"SxCPKrea2ResolutionSelector": SxCPKrea2ResolutionSelector,
|
|
||||||
"SxCPCameraControl": SxCPCameraControl,
|
"SxCPCameraControl": SxCPCameraControl,
|
||||||
"SxCPCameraOrbitControl": SxCPCameraOrbitControl,
|
"SxCPCameraOrbitControl": SxCPCameraOrbitControl,
|
||||||
"SxCPQwenCameraTranslator": SxCPQwenCameraTranslator,
|
"SxCPQwenCameraTranslator": SxCPQwenCameraTranslator,
|
||||||
@@ -3195,17 +2707,15 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"SxCPSDXLFormatter": SxCPSDXLFormatter,
|
"SxCPSDXLFormatter": SxCPSDXLFormatter,
|
||||||
"SxCPInstaOFOptions": SxCPInstaOFOptions,
|
"SxCPInstaOFOptions": SxCPInstaOFOptions,
|
||||||
"SxCPInstaOFPromptPair": SxCPInstaOFPromptPair,
|
"SxCPInstaOFPromptPair": SxCPInstaOFPromptPair,
|
||||||
}
|
})
|
||||||
NODE_CLASS_MAPPINGS.update(LOOP_NODE_CLASS_MAPPINGS)
|
NODE_CLASS_MAPPINGS.update(LOOP_NODE_CLASS_MAPPINGS)
|
||||||
_install_input_tooltips(NODE_CLASS_MAPPINGS)
|
_install_input_tooltips(NODE_CLASS_MAPPINGS)
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"SxCPPromptBuilder": "SxCP Prompt Builder",
|
"SxCPPromptBuilder": "SxCP Prompt Builder",
|
||||||
"SxCPGlobalSeed": "SxCP Global Seed",
|
}
|
||||||
"SxCPSeedControl": "SxCP Seed Control",
|
NODE_DISPLAY_NAME_MAPPINGS.update(SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
"SxCPSeedLocker": "SxCP Seed Locker",
|
NODE_DISPLAY_NAME_MAPPINGS.update({
|
||||||
"SxCPSDXLBucketSize": "SxCP SDXL Bucket Size",
|
|
||||||
"SxCPKrea2ResolutionSelector": "SxCP Krea2 Resolution Selector",
|
|
||||||
"SxCPCameraControl": "SxCP Camera Control",
|
"SxCPCameraControl": "SxCP Camera Control",
|
||||||
"SxCPCameraOrbitControl": "SxCP Camera Orbit Control",
|
"SxCPCameraOrbitControl": "SxCP Camera Orbit Control",
|
||||||
"SxCPQwenCameraTranslator": "SxCP Qwen Camera Translator",
|
"SxCPQwenCameraTranslator": "SxCP Qwen Camera Translator",
|
||||||
@@ -3241,7 +2751,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"SxCPSDXLFormatter": "SxCP SDXL Formatter",
|
"SxCPSDXLFormatter": "SxCP SDXL Formatter",
|
||||||
"SxCPInstaOFOptions": "SxCP Insta/OF Options",
|
"SxCPInstaOFOptions": "SxCP Insta/OF Options",
|
||||||
"SxCPInstaOFPromptPair": "SxCP Insta/OF Prompt Pair",
|
"SxCPInstaOFPromptPair": "SxCP Insta/OF Prompt Pair",
|
||||||
}
|
})
|
||||||
NODE_DISPLAY_NAME_MAPPINGS.update(LOOP_NODE_DISPLAY_NAME_MAPPINGS)
|
NODE_DISPLAY_NAME_MAPPINGS.update(LOOP_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
|
|
||||||
WEB_DIRECTORY = "./web"
|
WEB_DIRECTORY = "./web"
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ routing map in `docs/prompt-pool-routing-map.md`.
|
|||||||
|
|
||||||
The current branch adds two major surfaces:
|
The current branch adds two major surfaces:
|
||||||
|
|
||||||
- `SxCP Krea2 Resolution Selector` in `__init__.py`, with README notes.
|
- `SxCP Krea2 Resolution Selector` in `node_seed_resolution.py`, with README
|
||||||
|
notes.
|
||||||
- Expanded hardcore interaction/manual/action pools in
|
- Expanded hardcore interaction/manual/action pools in
|
||||||
`categories/sexual_poses.json`,
|
`categories/sexual_poses.json`,
|
||||||
`categories/expression_composition_pools.json`, `prompt_builder.py`, and
|
`categories/expression_composition_pools.json`, `prompt_builder.py`, and
|
||||||
@@ -271,7 +272,7 @@ Improve later:
|
|||||||
|
|
||||||
### Node / UI Path
|
### Node / UI Path
|
||||||
|
|
||||||
Owner: `__init__.py`, `loop_nodes.py`, `web/*.js`.
|
Owner: `__init__.py`, `node_seed_resolution.py`, `loop_nodes.py`, `web/*.js`.
|
||||||
|
|
||||||
Keep here:
|
Keep here:
|
||||||
|
|
||||||
@@ -279,10 +280,16 @@ Keep here:
|
|||||||
- widget behavior;
|
- widget behavior;
|
||||||
- button actions;
|
- button actions;
|
||||||
- dynamic input slots.
|
- dynamic input slots.
|
||||||
|
- seed and resolution utility node declarations in `node_seed_resolution.py`.
|
||||||
|
|
||||||
|
Already isolated:
|
||||||
|
|
||||||
|
- seed/global-seed/seed-locker and SDXL/Krea2 resolution utility nodes live in
|
||||||
|
`node_seed_resolution.py`, with registration maps imported by `__init__.py`.
|
||||||
|
|
||||||
Improve later:
|
Improve later:
|
||||||
|
|
||||||
- split large node classes into files by family;
|
- split remaining large node classes into files by family;
|
||||||
- keep node display names, return names, and docs in sync through the audit
|
- keep node display names, return names, and docs in sync through the audit
|
||||||
helper;
|
helper;
|
||||||
- add small endpoint tests for profile/accumulator/index-switch routes.
|
- add small endpoint tests for profile/accumulator/index-switch routes.
|
||||||
@@ -400,8 +407,8 @@ Medium-term:
|
|||||||
|
|
||||||
## Recommended Next Passes
|
## Recommended Next Passes
|
||||||
|
|
||||||
1. Split `__init__.py` node classes by family after behavior is covered by smoke
|
1. Continue splitting remaining `__init__.py` node classes by family after
|
||||||
checks.
|
behavior is covered by smoke checks.
|
||||||
2. Continue splitting the internals of `hardcore_role_graphs.py` by action
|
2. Continue splitting the internals of `hardcore_role_graphs.py` by action
|
||||||
family once generated edge cases are covered by smoke fixtures.
|
family once generated edge cases are covered by smoke fixtures.
|
||||||
3. Add more route-level smoke fixtures for generated edge cases that are not
|
3. Add more route-level smoke fixtures for generated edge cases that are not
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ When a result is wrong, first identify which layer owns the bad text:
|
|||||||
- Raw builder prompt acceptable, Krea2 output wrong: edit `krea_formatter.py`.
|
- Raw builder prompt acceptable, Krea2 output wrong: edit `krea_formatter.py`.
|
||||||
- Raw builder prompt acceptable, SDXL tags wrong: edit `sdxl_formatter.py`.
|
- Raw builder prompt acceptable, SDXL tags wrong: edit `sdxl_formatter.py`.
|
||||||
- Natural caption/training caption wrong: edit `caption_naturalizer.py`.
|
- Natural caption/training caption wrong: edit `caption_naturalizer.py`.
|
||||||
- UI/preview/loop behavior wrong: edit `__init__.py`, `loop_nodes.py`, or
|
- UI/preview/loop behavior wrong: edit `__init__.py`, node family modules such
|
||||||
`web/*.js`.
|
as `node_seed_resolution.py`, `loop_nodes.py`, or `web/*.js`.
|
||||||
|
|
||||||
## High-Level Routes
|
## High-Level Routes
|
||||||
|
|
||||||
@@ -691,8 +691,7 @@ These do not own prompt pool wording, but they affect execution and review:
|
|||||||
| Index switch | `loop_nodes.py`, `web/index_switch_slots.js` | Multi-input to selected output, and selected input to multi-output routing. |
|
| Index switch | `loop_nodes.py`, `web/index_switch_slots.js` | Multi-input to selected output, and selected input to multi-output routing. |
|
||||||
| Accumulator | `loop_nodes.py`, `web/accumulator_preview.js` | Stores generated values/images during workflow execution and previews/reorders/deletes them. |
|
| Accumulator | `loop_nodes.py`, `web/accumulator_preview.js` | Stores generated values/images during workflow execution and previews/reorders/deletes them. |
|
||||||
| Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. |
|
| Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. |
|
||||||
| SDXL bucket size | `SxCPSDXLBucketSize` in `__init__.py` | Random/fixed SDXL bucket width and height selection. |
|
| Seed and resolution utility nodes | `node_seed_resolution.py`, imported by `__init__.py` | Global/per-axis seed configs plus SDXL/Krea width/height helpers. |
|
||||||
| Krea2 resolution selector | `SxCPKrea2ResolutionSelector` in `__init__.py` | Krea-compatible width/height and API aspect/resolution helper. |
|
|
||||||
|
|
||||||
## Drift Audit Helper
|
## Drift Audit Helper
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,522 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .prompt_builder import (
|
||||||
|
build_seed_config_json,
|
||||||
|
build_seed_lock_config_json,
|
||||||
|
seed_mode_choices,
|
||||||
|
)
|
||||||
|
except ImportError: # Allows local smoke tests from the repository root.
|
||||||
|
from prompt_builder import (
|
||||||
|
build_seed_config_json,
|
||||||
|
build_seed_lock_config_json,
|
||||||
|
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",
|
||||||
|
"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,)
|
||||||
|
RETURN_NAMES = ("seed_config",)
|
||||||
|
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()))
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self,
|
||||||
|
category_seed_mode,
|
||||||
|
category_seed,
|
||||||
|
subcategory_seed_mode,
|
||||||
|
subcategory_seed,
|
||||||
|
content_seed_mode,
|
||||||
|
content_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,
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
build_seed_config_json(
|
||||||
|
category_seed=category_seed,
|
||||||
|
subcategory_seed=subcategory_seed,
|
||||||
|
content_seed=content_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,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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": (
|
||||||
|
[
|
||||||
|
"none",
|
||||||
|
"category",
|
||||||
|
"subcategory",
|
||||||
|
"content",
|
||||||
|
"person",
|
||||||
|
"scene",
|
||||||
|
"pose",
|
||||||
|
"role",
|
||||||
|
"expression",
|
||||||
|
"composition",
|
||||||
|
"content_pose",
|
||||||
|
"scene_pose",
|
||||||
|
],
|
||||||
|
{"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):
|
||||||
|
config = build_seed_lock_config_json(base_seed=base_seed, reroll_axis=reroll_axis, reroll_seed=reroll_seed)
|
||||||
|
summary = f"base {base_seed}; reroll {reroll_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):
|
||||||
|
if not seed_config:
|
||||||
|
return None
|
||||||
|
if isinstance(seed_config, dict):
|
||||||
|
raw = seed_config
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
raw = json.loads(str(seed_config))
|
||||||
|
except (TypeError, ValueError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
for key in ("composition_seed", "content_seed", "seed", "global_seed"):
|
||||||
|
try:
|
||||||
|
value = int(raw.get(key))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if value >= 0:
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
@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",
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ if str(ROOT) not in sys.path:
|
|||||||
|
|
||||||
import caption_naturalizer # noqa: E402
|
import caption_naturalizer # noqa: E402
|
||||||
import category_library # noqa: E402
|
import category_library # noqa: E402
|
||||||
|
import __init__ as sxcp_nodes # noqa: E402
|
||||||
import krea_formatter # noqa: E402
|
import krea_formatter # noqa: E402
|
||||||
import prompt_builder as pb # noqa: E402
|
import prompt_builder as pb # noqa: E402
|
||||||
import sdxl_formatter # noqa: E402
|
import sdxl_formatter # noqa: E402
|
||||||
@@ -1661,6 +1662,50 @@ def smoke_formatter_metadata_fixtures() -> None:
|
|||||||
_expect(term in caption_text, f"{name}.caption missing {term!r}")
|
_expect(term in caption_text, f"{name}.caption missing {term!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_node_utility_registration() -> None:
|
||||||
|
required_nodes = [
|
||||||
|
"SxCPGlobalSeed",
|
||||||
|
"SxCPSeedControl",
|
||||||
|
"SxCPSeedLocker",
|
||||||
|
"SxCPSDXLBucketSize",
|
||||||
|
"SxCPKrea2ResolutionSelector",
|
||||||
|
]
|
||||||
|
for node_name in required_nodes:
|
||||||
|
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
|
||||||
|
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
|
||||||
|
|
||||||
|
seed_control = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSeedControl"]
|
||||||
|
seed_inputs = seed_control.INPUT_TYPES().get("required") or {}
|
||||||
|
_expect("category_seed_mode" in seed_inputs, "Seed Control lost category seed mode input")
|
||||||
|
_expect("tooltip" in seed_inputs["category_seed_mode"][1], "Seed Control tooltip injection missing")
|
||||||
|
|
||||||
|
seed, seed_config, summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPGlobalSeed"]().build(12345)
|
||||||
|
parsed_seed = json.loads(seed_config)
|
||||||
|
_expect(seed == 12345, "Global Seed did not return the clamped seed")
|
||||||
|
_expect(parsed_seed, "Global Seed config should not be empty")
|
||||||
|
_expect(all(int(value) == 12345 for value in parsed_seed.values()), "Global Seed config did not lock every axis")
|
||||||
|
_expect("all axes locked" in summary, "Global Seed summary changed unexpectedly")
|
||||||
|
|
||||||
|
locker_config, locker_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSeedLocker"]().build(12345, "pose", 999)
|
||||||
|
parsed_locker = json.loads(locker_config)
|
||||||
|
_expect(parsed_locker.get("pose_seed") == 999, "Seed Locker did not apply pose reroll seed")
|
||||||
|
_expect("reroll pose" in locker_summary, "Seed Locker summary lost reroll axis")
|
||||||
|
|
||||||
|
bucket_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSDXLBucketSize"]()
|
||||||
|
bucket_a = bucket_node.build("portrait", 77, 3, 0)
|
||||||
|
bucket_b = bucket_node.build("portrait", 77, 3, 0)
|
||||||
|
_expect(bucket_a == bucket_b, "SDXL bucket should be deterministic for fixed seed and row")
|
||||||
|
_expect(bucket_a[3] == "portrait", "SDXL bucket ignored orientation filter")
|
||||||
|
|
||||||
|
krea_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2ResolutionSelector"]()
|
||||||
|
krea_width, krea_height, _resolution, aspect_ratio, api_aspect, _api_resolution, *_rest = krea_node.select("1.0MP", "9:16")
|
||||||
|
krea_config = json.loads(_rest[-1])
|
||||||
|
_expect(krea_height > krea_width, "Krea2 9:16 selector should return portrait dimensions")
|
||||||
|
_expect(aspect_ratio == "9:16", "Krea2 selector lost requested aspect ratio")
|
||||||
|
_expect(api_aspect == "9:16", "Krea2 selector lost API aspect mapping")
|
||||||
|
_expect(krea_config.get("width") == krea_width and krea_config.get("height") == krea_height, "Krea2 config_json dimensions mismatch")
|
||||||
|
|
||||||
|
|
||||||
SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
||||||
("builtin_single_woman", smoke_builtin_single),
|
("builtin_single_woman", smoke_builtin_single),
|
||||||
("camera_scene_single", smoke_camera_scene_single),
|
("camera_scene_single", smoke_camera_scene_single),
|
||||||
@@ -1684,6 +1729,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
|||||||
("fallback_role_graph_routes", smoke_fallback_role_graph_routes),
|
("fallback_role_graph_routes", smoke_fallback_role_graph_routes),
|
||||||
("expression_disabled", smoke_no_expression_fallback),
|
("expression_disabled", smoke_no_expression_fallback),
|
||||||
("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures),
|
("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures),
|
||||||
|
("node_utility_registration", smoke_node_utility_registration),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user