Extract seed resolution nodes
This commit is contained in:
+16
-506
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import re
|
||||
|
||||
@@ -30,40 +29,6 @@ SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
|
||||
SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT"
|
||||
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 = {
|
||||
"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.",
|
||||
@@ -436,6 +401,10 @@ try:
|
||||
accumulator_move_entry,
|
||||
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 (
|
||||
build_camera_config_json,
|
||||
build_camera_orbit_config_json,
|
||||
@@ -459,8 +428,6 @@ try:
|
||||
build_insta_of_pair,
|
||||
build_prompt,
|
||||
build_prompt_from_configs,
|
||||
build_seed_config_json,
|
||||
build_seed_lock_config_json,
|
||||
camera_angle_choices,
|
||||
camera_detail_choices,
|
||||
camera_distance_choices,
|
||||
@@ -504,7 +471,6 @@ try:
|
||||
location_theme_choices,
|
||||
location_pool_preset_choices,
|
||||
save_character_profile_payload,
|
||||
seed_mode_choices,
|
||||
subcategory_choices,
|
||||
)
|
||||
from .caption_naturalizer import naturalize_caption
|
||||
@@ -520,6 +486,10 @@ except ImportError:
|
||||
accumulator_move_entry,
|
||||
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 (
|
||||
build_camera_config_json,
|
||||
build_camera_orbit_config_json,
|
||||
@@ -543,8 +513,6 @@ except ImportError:
|
||||
build_insta_of_pair,
|
||||
build_prompt,
|
||||
build_prompt_from_configs,
|
||||
build_seed_config_json,
|
||||
build_seed_lock_config_json,
|
||||
camera_angle_choices,
|
||||
camera_detail_choices,
|
||||
camera_distance_choices,
|
||||
@@ -588,7 +556,6 @@ except ImportError:
|
||||
location_theme_choices,
|
||||
location_pool_preset_choices,
|
||||
save_character_profile_payload,
|
||||
seed_mode_choices,
|
||||
subcategory_choices,
|
||||
)
|
||||
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:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -3155,11 +2669,9 @@ class SxCPInstaOFPromptPair:
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SxCPPromptBuilder": SxCPPromptBuilder,
|
||||
"SxCPGlobalSeed": SxCPGlobalSeed,
|
||||
"SxCPSeedControl": SxCPSeedControl,
|
||||
"SxCPSeedLocker": SxCPSeedLocker,
|
||||
"SxCPSDXLBucketSize": SxCPSDXLBucketSize,
|
||||
"SxCPKrea2ResolutionSelector": SxCPKrea2ResolutionSelector,
|
||||
}
|
||||
NODE_CLASS_MAPPINGS.update(SEED_RESOLUTION_NODE_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update({
|
||||
"SxCPCameraControl": SxCPCameraControl,
|
||||
"SxCPCameraOrbitControl": SxCPCameraOrbitControl,
|
||||
"SxCPQwenCameraTranslator": SxCPQwenCameraTranslator,
|
||||
@@ -3195,17 +2707,15 @@ NODE_CLASS_MAPPINGS = {
|
||||
"SxCPSDXLFormatter": SxCPSDXLFormatter,
|
||||
"SxCPInstaOFOptions": SxCPInstaOFOptions,
|
||||
"SxCPInstaOFPromptPair": SxCPInstaOFPromptPair,
|
||||
}
|
||||
})
|
||||
NODE_CLASS_MAPPINGS.update(LOOP_NODE_CLASS_MAPPINGS)
|
||||
_install_input_tooltips(NODE_CLASS_MAPPINGS)
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SxCPPromptBuilder": "SxCP Prompt Builder",
|
||||
"SxCPGlobalSeed": "SxCP Global Seed",
|
||||
"SxCPSeedControl": "SxCP Seed Control",
|
||||
"SxCPSeedLocker": "SxCP Seed Locker",
|
||||
"SxCPSDXLBucketSize": "SxCP SDXL Bucket Size",
|
||||
"SxCPKrea2ResolutionSelector": "SxCP Krea2 Resolution Selector",
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update({
|
||||
"SxCPCameraControl": "SxCP Camera Control",
|
||||
"SxCPCameraOrbitControl": "SxCP Camera Orbit Control",
|
||||
"SxCPQwenCameraTranslator": "SxCP Qwen Camera Translator",
|
||||
@@ -3241,7 +2751,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SxCPSDXLFormatter": "SxCP SDXL Formatter",
|
||||
"SxCPInstaOFOptions": "SxCP Insta/OF Options",
|
||||
"SxCPInstaOFPromptPair": "SxCP Insta/OF Prompt Pair",
|
||||
}
|
||||
})
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(LOOP_NODE_DISPLAY_NAME_MAPPINGS)
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
@@ -7,7 +7,8 @@ routing map in `docs/prompt-pool-routing-map.md`.
|
||||
|
||||
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
|
||||
`categories/sexual_poses.json`,
|
||||
`categories/expression_composition_pools.json`, `prompt_builder.py`, and
|
||||
@@ -271,7 +272,7 @@ Improve later:
|
||||
|
||||
### 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:
|
||||
|
||||
@@ -279,10 +280,16 @@ Keep here:
|
||||
- widget behavior;
|
||||
- button actions;
|
||||
- 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:
|
||||
|
||||
- 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
|
||||
helper;
|
||||
- add small endpoint tests for profile/accumulator/index-switch routes.
|
||||
@@ -400,8 +407,8 @@ Medium-term:
|
||||
|
||||
## Recommended Next Passes
|
||||
|
||||
1. Split `__init__.py` node classes by family after behavior is covered by smoke
|
||||
checks.
|
||||
1. Continue splitting remaining `__init__.py` node classes by family after
|
||||
behavior is covered by smoke checks.
|
||||
2. Continue splitting the internals of `hardcore_role_graphs.py` by action
|
||||
family once generated edge cases are covered by smoke fixtures.
|
||||
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, SDXL tags wrong: edit `sdxl_formatter.py`.
|
||||
- Natural caption/training caption wrong: edit `caption_naturalizer.py`.
|
||||
- UI/preview/loop behavior wrong: edit `__init__.py`, `loop_nodes.py`, or
|
||||
`web/*.js`.
|
||||
- UI/preview/loop behavior wrong: edit `__init__.py`, node family modules such
|
||||
as `node_seed_resolution.py`, `loop_nodes.py`, or `web/*.js`.
|
||||
|
||||
## 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. |
|
||||
| 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. |
|
||||
| SDXL bucket size | `SxCPSDXLBucketSize` in `__init__.py` | Random/fixed SDXL bucket width and height selection. |
|
||||
| Krea2 resolution selector | `SxCPKrea2ResolutionSelector` in `__init__.py` | Krea-compatible width/height and API aspect/resolution helper. |
|
||||
| Seed and resolution utility nodes | `node_seed_resolution.py`, imported by `__init__.py` | Global/per-axis seed configs plus SDXL/Krea width/height helpers. |
|
||||
|
||||
## 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 category_library # noqa: E402
|
||||
import __init__ as sxcp_nodes # noqa: E402
|
||||
import krea_formatter # noqa: E402
|
||||
import prompt_builder as pb # 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}")
|
||||
|
||||
|
||||
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]]] = [
|
||||
("builtin_single_woman", smoke_builtin_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),
|
||||
("expression_disabled", smoke_no_expression_fallback),
|
||||
("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures),
|
||||
("node_utility_registration", smoke_node_utility_registration),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user