PBandDev commited on
Expose SmartSeed selected seed outputs (#17)
Browse filesAdds selected_seed outputs for CADE Easy and MG_SuperSimple while preserving existing output slot order.
Adds regression coverage for output contracts.
- README.md +3 -2
- docs/EasyNodes.md +3 -2
- mod/easy/mg_cade25_easy.py +11 -11
- mod/easy/mg_supersimple_easy.py +9 -8
- tests/test_smartseed_outputs.py +77 -0
README.md
CHANGED
|
@@ -56,7 +56,7 @@ Photo Dog
|
|
| 56 |
- MGHybrid scheduler: hybrid Karras/Beta sigma stack with smooth tail blending and tiny schedule jitter (ZeSmart-inspired) for more stable, detail-friendly denoising; used by CADE and SuperSimple by default
|
| 57 |
- Seed Latent (MG_SeedLatent): fast, deterministic latent initializer aligned to VAE stride; supports pure-noise starts or image-mixed starts (encode + noise) to gently bias content; batch-ready and resolution-agnostic, pairs well with SuperSimple recommended latent sizes for reproducible pipelines
|
| 58 |
- Muse Blend and Polish: directional post-mix and final low-frequency-preserving clean-up
|
| 59 |
-
- SmartSeed (CADE Easy and SuperSimple): set `seed = 0` to auto-pick a good seed from a tiny low-step probe. Uses a low-discrepancy sweep, avoids speckles/overexposure, and, if available, leverages CLIP-Vision (with `reference_image`) and CLIPSeg focus text to favor semantically aligned candidates. Logs `Smart_seed_random: Start/End`
|
| 60 |
<b>I highly recommend working with SmartSeed.</b>
|
| 61 |
- CADE2.5 pipeline does not just upscale the image, it iterates and adds small details, doing it carefully, at every stage.
|
| 62 |
|
|
@@ -141,7 +141,7 @@ Notes:
|
|
| 141 |
|
| 142 |
12) Very often, the image in `step 3 is of very good quality`, but it usually lacks sharpness. But if you have a `weak system`, you can `limit yourself to 3 steps`.
|
| 143 |
|
| 144 |
-
13) SmartSeed (auto seed pick): set `seed = 0` in Easy or SuperSimple. The node will sample several candidate seeds and do a quick low‑step probe to choose a balanced one. You’ll see logs `Smart_seed_random: Start` and `Smart_seed_random: End. Seed is: <number>`. Use any non‑zero seed for fully deterministic runs.
|
| 145 |
|
| 146 |
14) The 4th step sometimes saves the image for a long time, just wait for the end of the process, it depends on the initial resolution you set.
|
| 147 |
|
|
@@ -222,6 +222,7 @@ MagicNodes/
|
|
| 222 |
|
| 223 |
## CADE 2.5 (mg_cade25.py, mg_cade25_easy.py)
|
| 224 |
- Deterministic preflight: CLIPSeg pinned to CPU; preview mask reset; noise tied to `iter_seed`
|
|
|
|
| 225 |
- Encode/Decode: stride-aligned, with larger overlap for >2K to avoid artifacts
|
| 226 |
- Polish mode (final hi-res refinement):
|
| 227 |
- `polish_enable`, `polish_keep_low` (global form from reference), `polish_edge_lock`, `polish_sigma`
|
|
|
|
| 56 |
- MGHybrid scheduler: hybrid Karras/Beta sigma stack with smooth tail blending and tiny schedule jitter (ZeSmart-inspired) for more stable, detail-friendly denoising; used by CADE and SuperSimple by default
|
| 57 |
- Seed Latent (MG_SeedLatent): fast, deterministic latent initializer aligned to VAE stride; supports pure-noise starts or image-mixed starts (encode + noise) to gently bias content; batch-ready and resolution-agnostic, pairs well with SuperSimple recommended latent sizes for reproducible pipelines
|
| 58 |
- Muse Blend and Polish: directional post-mix and final low-frequency-preserving clean-up
|
| 59 |
+
- SmartSeed (CADE Easy and SuperSimple): set `seed = 0` to auto-pick a good seed from a tiny low-step probe. Uses a low-discrepancy sweep, avoids speckles/overexposure, and, if available, leverages CLIP-Vision (with `reference_image`) and CLIPSeg focus text to favor semantically aligned candidates. Logs `Smart_seed_random: Start/End` and exposes the final choice on the `selected_seed` output.
|
| 60 |
<b>I highly recommend working with SmartSeed.</b>
|
| 61 |
- CADE2.5 pipeline does not just upscale the image, it iterates and adds small details, doing it carefully, at every stage.
|
| 62 |
|
|
|
|
| 141 |
|
| 142 |
12) Very often, the image in `step 3 is of very good quality`, but it usually lacks sharpness. But if you have a `weak system`, you can `limit yourself to 3 steps`.
|
| 143 |
|
| 144 |
+
13) SmartSeed (auto seed pick): set `seed = 0` in Easy or SuperSimple. The node will sample several candidate seeds and do a quick low‑step probe to choose a balanced one. You’ll see logs `Smart_seed_random: Start` and `Smart_seed_random: End. Seed is: <number>`, and the chosen value is available from the `selected_seed` output. Use any non‑zero seed for fully deterministic runs.
|
| 145 |
|
| 146 |
14) The 4th step sometimes saves the image for a long time, just wait for the end of the process, it depends on the initial resolution you set.
|
| 147 |
|
|
|
|
| 222 |
|
| 223 |
## CADE 2.5 (mg_cade25.py, mg_cade25_easy.py)
|
| 224 |
- Deterministic preflight: CLIPSeg pinned to CPU; preview mask reset; noise tied to `iter_seed`
|
| 225 |
+
- CADE Easy appends a `selected_seed` INT output after `mask_preview`; existing LATENT, IMAGE, and mask slots keep their order.
|
| 226 |
- Encode/Decode: stride-aligned, with larger overlap for >2K to avoid artifacts
|
| 227 |
- Polish mode (final hi-res refinement):
|
| 228 |
- `polish_enable`, `polish_keep_low` (global form from reference), `polish_edge_lock`, `polish_sigma`
|
docs/EasyNodes.md
CHANGED
|
@@ -40,13 +40,14 @@ Behavior
|
|
| 40 |
- ControlFusion inside this node always relies on presets (no additional CF UI here) to keep the surface minimal.
|
| 41 |
|
| 42 |
Outputs
|
| 43 |
-
- `(LATENT, IMAGE)` from the final executed step (e.g., step 2 if `step_count=2`). No preview outputs.
|
|
|
|
| 44 |
|
| 45 |
Quickstart
|
| 46 |
1) Drop `MG_SuperSimple` into your graph under `MagicNodes/Easy`.
|
| 47 |
2) Connect `model/positive/negative/vae/latent`, and a `control_net` module; optionally connect `reference_image` and `clip_vision`.
|
| 48 |
3) Choose `step_count` (2/3/4). Leave `custom` Off to use pure presets, or enable it to apply your `seed/steps/cfg/denoise/sampler/scheduler/clipseg_text` across all steps (with Step 1 `denoise=1.0`).
|
| 49 |
-
4) Run. The node returns the final `(LATENT, IMAGE)` for the chosen depth.
|
| 50 |
|
| 51 |
Notes
|
| 52 |
- Presets are read from `pressets/mg_cade25.cfg` and `pressets/mg_controlfusion.cfg`. Keep them in UTF‑8 and prefer `$(ROOT)` over absolute paths.
|
|
|
|
| 40 |
- ControlFusion inside this node always relies on presets (no additional CF UI here) to keep the surface minimal.
|
| 41 |
|
| 42 |
Outputs
|
| 43 |
+
- `(LATENT, IMAGE, selected_seed)` from the final executed step (e.g., step 2 if `step_count=2`). No preview outputs.
|
| 44 |
+
- `selected_seed` is the seed CADE actually used after SmartSeed selection. With a non-zero fixed seed, it matches the requested seed.
|
| 45 |
|
| 46 |
Quickstart
|
| 47 |
1) Drop `MG_SuperSimple` into your graph under `MagicNodes/Easy`.
|
| 48 |
2) Connect `model/positive/negative/vae/latent`, and a `control_net` module; optionally connect `reference_image` and `clip_vision`.
|
| 49 |
3) Choose `step_count` (2/3/4). Leave `custom` Off to use pure presets, or enable it to apply your `seed/steps/cfg/denoise/sampler/scheduler/clipseg_text` across all steps (with Step 1 `denoise=1.0`).
|
| 50 |
+
4) Run. The node returns the final `(LATENT, IMAGE, selected_seed)` for the chosen depth.
|
| 51 |
|
| 52 |
Notes
|
| 53 |
- Presets are read from `pressets/mg_cade25.cfg` and `pressets/mg_controlfusion.cfg`. Keep them in UTF‑8 and prefer `$(ROOT)` over absolute paths.
|
mod/easy/mg_cade25_easy.py
CHANGED
|
@@ -2260,8 +2260,8 @@ class ComfyAdaptiveDetailEnhancer25:
|
|
| 2260 |
},
|
| 2261 |
}
|
| 2262 |
|
| 2263 |
-
RETURN_TYPES = ("LATENT", "IMAGE", "INT", "FLOAT", "FLOAT", "IMAGE")
|
| 2264 |
-
RETURN_NAMES = ("LATENT", "IMAGE", "steps", "cfg", "denoise", "mask_preview")
|
| 2265 |
FUNCTION = "apply_cade2"
|
| 2266 |
CATEGORY = "MagicNodes"
|
| 2267 |
|
|
@@ -3428,7 +3428,7 @@ class ComfyAdaptiveDetailEnhancer25:
|
|
| 3428 |
except Exception:
|
| 3429 |
pass
|
| 3430 |
|
| 3431 |
-
return current_latent, image, int(current_steps), float(current_cfg), float(current_denoise), onnx_mask_img
|
| 3432 |
|
| 3433 |
|
| 3434 |
# === Easy UI wrapper: show only top-level controls ===
|
|
@@ -3461,8 +3461,8 @@ class CADEEasyUI(ComfyAdaptiveDetailEnhancer25):
|
|
| 3461 |
}
|
| 3462 |
|
| 3463 |
# Easy outputs (hide steps/cfg/denoise)
|
| 3464 |
-
RETURN_TYPES = ("LATENT", "IMAGE", "IMAGE")
|
| 3465 |
-
RETURN_NAMES = ("LATENT", "IMAGE", "mask_preview")
|
| 3466 |
FUNCTION = "apply_easy"
|
| 3467 |
|
| 3468 |
def apply_easy(self,
|
|
@@ -3470,7 +3470,7 @@ class CADEEasyUI(ComfyAdaptiveDetailEnhancer25):
|
|
| 3470 |
model, positive, negative, vae, latent,
|
| 3471 |
seed, steps, cfg, denoise, sampler_name, scheduler,
|
| 3472 |
clipseg_text="", reference_image=None, clip_vision=None, custom=False):
|
| 3473 |
-
lat, img, _s, _c, _d, mask = super().apply_cade2(
|
| 3474 |
model, vae, positive, negative, latent,
|
| 3475 |
int(seed), int(steps), float(cfg), float(denoise),
|
| 3476 |
str(sampler_name), str(scheduler), 0.0,
|
|
@@ -3478,11 +3478,11 @@ class CADEEasyUI(ComfyAdaptiveDetailEnhancer25):
|
|
| 3478 |
reference_image=reference_image,
|
| 3479 |
clip_vision=clip_vision,
|
| 3480 |
)
|
| 3481 |
-
return lat, img, mask
|
| 3482 |
|
| 3483 |
# Show simpler outputs in Easy variant
|
| 3484 |
-
RETURN_TYPES = ("LATENT", "IMAGE", "IMAGE")
|
| 3485 |
-
RETURN_NAMES = ("LATENT", "IMAGE", "mask_preview")
|
| 3486 |
FUNCTION = "apply_easy"
|
| 3487 |
|
| 3488 |
def apply_easy(self,
|
|
@@ -3490,13 +3490,13 @@ class CADEEasyUI(ComfyAdaptiveDetailEnhancer25):
|
|
| 3490 |
model, positive, negative, vae, latent,
|
| 3491 |
seed, steps, cfg, denoise, sampler_name, scheduler,
|
| 3492 |
clipseg_text=""):
|
| 3493 |
-
lat, img, _s, _c, _d, mask = super().apply_cade2(
|
| 3494 |
model, vae, positive, negative, latent,
|
| 3495 |
int(seed), int(steps), float(cfg), float(denoise),
|
| 3496 |
str(sampler_name), str(scheduler), 0.0,
|
| 3497 |
preset_step=str(preset_step), custom_override=bool(custom), clipseg_text=str(clipseg_text),
|
| 3498 |
)
|
| 3499 |
-
return lat, img, mask
|
| 3500 |
|
| 3501 |
|
| 3502 |
|
|
|
|
| 2260 |
},
|
| 2261 |
}
|
| 2262 |
|
| 2263 |
+
RETURN_TYPES = ("LATENT", "IMAGE", "INT", "FLOAT", "FLOAT", "IMAGE", "INT")
|
| 2264 |
+
RETURN_NAMES = ("LATENT", "IMAGE", "steps", "cfg", "denoise", "mask_preview", "selected_seed")
|
| 2265 |
FUNCTION = "apply_cade2"
|
| 2266 |
CATEGORY = "MagicNodes"
|
| 2267 |
|
|
|
|
| 3428 |
except Exception:
|
| 3429 |
pass
|
| 3430 |
|
| 3431 |
+
return current_latent, image, int(current_steps), float(current_cfg), float(current_denoise), onnx_mask_img, int(seed)
|
| 3432 |
|
| 3433 |
|
| 3434 |
# === Easy UI wrapper: show only top-level controls ===
|
|
|
|
| 3461 |
}
|
| 3462 |
|
| 3463 |
# Easy outputs (hide steps/cfg/denoise)
|
| 3464 |
+
RETURN_TYPES = ("LATENT", "IMAGE", "IMAGE", "INT")
|
| 3465 |
+
RETURN_NAMES = ("LATENT", "IMAGE", "mask_preview", "selected_seed")
|
| 3466 |
FUNCTION = "apply_easy"
|
| 3467 |
|
| 3468 |
def apply_easy(self,
|
|
|
|
| 3470 |
model, positive, negative, vae, latent,
|
| 3471 |
seed, steps, cfg, denoise, sampler_name, scheduler,
|
| 3472 |
clipseg_text="", reference_image=None, clip_vision=None, custom=False):
|
| 3473 |
+
lat, img, _s, _c, _d, mask, selected_seed = super().apply_cade2(
|
| 3474 |
model, vae, positive, negative, latent,
|
| 3475 |
int(seed), int(steps), float(cfg), float(denoise),
|
| 3476 |
str(sampler_name), str(scheduler), 0.0,
|
|
|
|
| 3478 |
reference_image=reference_image,
|
| 3479 |
clip_vision=clip_vision,
|
| 3480 |
)
|
| 3481 |
+
return lat, img, mask, selected_seed
|
| 3482 |
|
| 3483 |
# Show simpler outputs in Easy variant
|
| 3484 |
+
RETURN_TYPES = ("LATENT", "IMAGE", "IMAGE", "INT")
|
| 3485 |
+
RETURN_NAMES = ("LATENT", "IMAGE", "mask_preview", "selected_seed")
|
| 3486 |
FUNCTION = "apply_easy"
|
| 3487 |
|
| 3488 |
def apply_easy(self,
|
|
|
|
| 3490 |
model, positive, negative, vae, latent,
|
| 3491 |
seed, steps, cfg, denoise, sampler_name, scheduler,
|
| 3492 |
clipseg_text=""):
|
| 3493 |
+
lat, img, _s, _c, _d, mask, selected_seed = super().apply_cade2(
|
| 3494 |
model, vae, positive, negative, latent,
|
| 3495 |
int(seed), int(steps), float(cfg), float(denoise),
|
| 3496 |
str(sampler_name), str(scheduler), 0.0,
|
| 3497 |
preset_step=str(preset_step), custom_override=bool(custom), clipseg_text=str(clipseg_text),
|
| 3498 |
)
|
| 3499 |
+
return lat, img, mask, selected_seed
|
| 3500 |
|
| 3501 |
|
| 3502 |
|
mod/easy/mg_supersimple_easy.py
CHANGED
|
@@ -13,7 +13,7 @@ control_net: ControlNet module for CF (required)
|
|
| 13 |
reference_image/clip_vision: forwarded into CADE (optional)
|
| 14 |
Outputs:
|
| 15 |
|
| 16 |
-
(LATENT, IMAGE) from the final executed step
|
| 17 |
"""
|
| 18 |
|
| 19 |
|
|
@@ -62,8 +62,8 @@ class MG_SuperSimple:
|
|
| 62 |
},
|
| 63 |
}
|
| 64 |
|
| 65 |
-
RETURN_TYPES = ("LATENT", "IMAGE")
|
| 66 |
-
RETURN_NAMES = ("LATENT", "IMAGE")
|
| 67 |
FUNCTION = "run"
|
| 68 |
|
| 69 |
def _cade(self,
|
|
@@ -75,7 +75,7 @@ class MG_SuperSimple:
|
|
| 75 |
clipseg_text: str,
|
| 76 |
reference_image=None, clip_vision=None):
|
| 77 |
# CADE core call mirrors CADEEasyUI -> apply_cade2
|
| 78 |
-
lat, img, _s, _c, _d, _mask = _CADE().apply_cade2(
|
| 79 |
model, vae, positive, negative, latent,
|
| 80 |
int(seed), int(steps), float(cfg), float(denoise),
|
| 81 |
str(sampler_name), str(scheduler), 0.0,
|
|
@@ -83,7 +83,7 @@ class MG_SuperSimple:
|
|
| 83 |
clipseg_text=str(clipseg_text),
|
| 84 |
reference_image=reference_image, clip_vision=clip_vision,
|
| 85 |
)
|
| 86 |
-
return lat, img
|
| 87 |
|
| 88 |
def _cf(self,
|
| 89 |
preset_step: str,
|
|
@@ -112,11 +112,12 @@ class MG_SuperSimple:
|
|
| 112 |
cur_image = None
|
| 113 |
cur_pos = positive
|
| 114 |
cur_neg = negative
|
|
|
|
| 115 |
|
| 116 |
try:
|
| 117 |
# Step 1: CADE with Step 1 preset, denoise forced to 1.0
|
| 118 |
denoise_step1 = 1.0
|
| 119 |
-
lat1, img1 = self._cade(
|
| 120 |
preset_step="Step 1",
|
| 121 |
custom_override=bool(custom),
|
| 122 |
model=model, vae=vae, positive=cur_pos, negative=cur_neg, latent=cur_latent,
|
|
@@ -141,7 +142,7 @@ class MG_SuperSimple:
|
|
| 141 |
# If no external reference_image is provided, use the previous step image
|
| 142 |
# so that reference_clean / CLIP-Vision gating can take effect.
|
| 143 |
ref_img = reference_image if (reference_image is not None) else cur_image
|
| 144 |
-
lat_i, img_i = self._cade(
|
| 145 |
preset_step=f"Step {i}",
|
| 146 |
custom_override=bool(custom),
|
| 147 |
model=model, vae=vae, positive=cur_pos, negative=cur_neg, latent=cur_latent,
|
|
@@ -151,7 +152,7 @@ class MG_SuperSimple:
|
|
| 151 |
reference_image=ref_img, clip_vision=clip_vision,
|
| 152 |
)
|
| 153 |
cur_latent, cur_image = lat_i, img_i
|
| 154 |
-
return (cur_latent, cur_image)
|
| 155 |
finally:
|
| 156 |
# try to free memory on cancel or normal exit
|
| 157 |
try:
|
|
|
|
| 13 |
reference_image/clip_vision: forwarded into CADE (optional)
|
| 14 |
Outputs:
|
| 15 |
|
| 16 |
+
(LATENT, IMAGE, selected_seed) from the final executed step
|
| 17 |
"""
|
| 18 |
|
| 19 |
|
|
|
|
| 62 |
},
|
| 63 |
}
|
| 64 |
|
| 65 |
+
RETURN_TYPES = ("LATENT", "IMAGE", "INT")
|
| 66 |
+
RETURN_NAMES = ("LATENT", "IMAGE", "selected_seed")
|
| 67 |
FUNCTION = "run"
|
| 68 |
|
| 69 |
def _cade(self,
|
|
|
|
| 75 |
clipseg_text: str,
|
| 76 |
reference_image=None, clip_vision=None):
|
| 77 |
# CADE core call mirrors CADEEasyUI -> apply_cade2
|
| 78 |
+
lat, img, _s, _c, _d, _mask, selected_seed = _CADE().apply_cade2(
|
| 79 |
model, vae, positive, negative, latent,
|
| 80 |
int(seed), int(steps), float(cfg), float(denoise),
|
| 81 |
str(sampler_name), str(scheduler), 0.0,
|
|
|
|
| 83 |
clipseg_text=str(clipseg_text),
|
| 84 |
reference_image=reference_image, clip_vision=clip_vision,
|
| 85 |
)
|
| 86 |
+
return lat, img, selected_seed
|
| 87 |
|
| 88 |
def _cf(self,
|
| 89 |
preset_step: str,
|
|
|
|
| 112 |
cur_image = None
|
| 113 |
cur_pos = positive
|
| 114 |
cur_neg = negative
|
| 115 |
+
selected_seed = int(seed)
|
| 116 |
|
| 117 |
try:
|
| 118 |
# Step 1: CADE with Step 1 preset, denoise forced to 1.0
|
| 119 |
denoise_step1 = 1.0
|
| 120 |
+
lat1, img1, selected_seed = self._cade(
|
| 121 |
preset_step="Step 1",
|
| 122 |
custom_override=bool(custom),
|
| 123 |
model=model, vae=vae, positive=cur_pos, negative=cur_neg, latent=cur_latent,
|
|
|
|
| 142 |
# If no external reference_image is provided, use the previous step image
|
| 143 |
# so that reference_clean / CLIP-Vision gating can take effect.
|
| 144 |
ref_img = reference_image if (reference_image is not None) else cur_image
|
| 145 |
+
lat_i, img_i, selected_seed = self._cade(
|
| 146 |
preset_step=f"Step {i}",
|
| 147 |
custom_override=bool(custom),
|
| 148 |
model=model, vae=vae, positive=cur_pos, negative=cur_neg, latent=cur_latent,
|
|
|
|
| 152 |
reference_image=ref_img, clip_vision=clip_vision,
|
| 153 |
)
|
| 154 |
cur_latent, cur_image = lat_i, img_i
|
| 155 |
+
return (cur_latent, cur_image, selected_seed)
|
| 156 |
finally:
|
| 157 |
# try to free memory on cancel or normal exit
|
| 158 |
try:
|
tests/test_smartseed_outputs.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ast
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import unittest
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def _module_tree(relative_path: str) -> ast.Module:
|
| 10 |
+
return ast.parse((ROOT / relative_path).read_text(encoding="utf-8"))
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _class_node(tree: ast.Module, class_name: str) -> ast.ClassDef:
|
| 14 |
+
for node in tree.body:
|
| 15 |
+
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
| 16 |
+
return node
|
| 17 |
+
raise AssertionError(f"class {class_name} not found")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _assigned_tuple(class_node: ast.ClassDef, name: str) -> tuple[str, ...]:
|
| 21 |
+
for node in class_node.body:
|
| 22 |
+
if (
|
| 23 |
+
isinstance(node, ast.Assign)
|
| 24 |
+
and any(isinstance(target, ast.Name) and target.id == name for target in node.targets)
|
| 25 |
+
and isinstance(node.value, ast.Tuple)
|
| 26 |
+
):
|
| 27 |
+
return tuple(
|
| 28 |
+
item.value
|
| 29 |
+
for item in node.value.elts
|
| 30 |
+
if isinstance(item, ast.Constant) and isinstance(item.value, str)
|
| 31 |
+
)
|
| 32 |
+
raise AssertionError(f"{name} tuple not found on {class_node.name}")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class SmartSeedOutputTests(unittest.TestCase):
|
| 36 |
+
def test_easy_cade_exposes_selected_seed_without_reordering_existing_outputs(self):
|
| 37 |
+
tree = _module_tree("mod/easy/mg_cade25_easy.py")
|
| 38 |
+
cade_easy = _class_node(tree, "CADEEasyUI")
|
| 39 |
+
|
| 40 |
+
self.assertEqual(
|
| 41 |
+
_assigned_tuple(cade_easy, "RETURN_NAMES"),
|
| 42 |
+
("LATENT", "IMAGE", "mask_preview", "selected_seed"),
|
| 43 |
+
)
|
| 44 |
+
self.assertEqual(
|
| 45 |
+
_assigned_tuple(cade_easy, "RETURN_TYPES"),
|
| 46 |
+
("LATENT", "IMAGE", "IMAGE", "INT"),
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
def test_base_cade_returns_selected_seed_for_advanced_wiring(self):
|
| 50 |
+
tree = _module_tree("mod/easy/mg_cade25_easy.py")
|
| 51 |
+
base_cade = _class_node(tree, "ComfyAdaptiveDetailEnhancer25")
|
| 52 |
+
|
| 53 |
+
self.assertEqual(
|
| 54 |
+
_assigned_tuple(base_cade, "RETURN_NAMES"),
|
| 55 |
+
("LATENT", "IMAGE", "steps", "cfg", "denoise", "mask_preview", "selected_seed"),
|
| 56 |
+
)
|
| 57 |
+
self.assertEqual(
|
| 58 |
+
_assigned_tuple(base_cade, "RETURN_TYPES"),
|
| 59 |
+
("LATENT", "IMAGE", "INT", "FLOAT", "FLOAT", "IMAGE", "INT"),
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
def test_supersimple_exposes_final_step_selected_seed(self):
|
| 63 |
+
tree = _module_tree("mod/easy/mg_supersimple_easy.py")
|
| 64 |
+
super_simple = _class_node(tree, "MG_SuperSimple")
|
| 65 |
+
|
| 66 |
+
self.assertEqual(
|
| 67 |
+
_assigned_tuple(super_simple, "RETURN_NAMES"),
|
| 68 |
+
("LATENT", "IMAGE", "selected_seed"),
|
| 69 |
+
)
|
| 70 |
+
self.assertEqual(
|
| 71 |
+
_assigned_tuple(super_simple, "RETURN_TYPES"),
|
| 72 |
+
("LATENT", "IMAGE", "INT"),
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
unittest.main()
|