From c5dadf1c407752be84a9a0bdf1d9f9e381bbe08d Mon Sep 17 00:00:00 2001 From: Gin3601 Date: Wed, 20 May 2026 11:17:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=9B=BE=E7=94=9F=E5=9B=BE?= =?UTF-8?q?=E8=BF=87=E7=A8=8B=E4=B8=AD=E7=9A=84BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sampling/nodes.py | 47 ++++++++++++++++++++-- tests/test_loader_prompt_refactor.py | 60 ++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/sampling/nodes.py b/sampling/nodes.py index ed9ad7d..815231c 100644 --- a/sampling/nodes.py +++ b/sampling/nodes.py @@ -71,6 +71,7 @@ _DEFAULT_SAMPLERS = ["euler", "euler_ancestral", "heun", "dpm_2", "dpm_2_ancestr "lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_ancestral", "dpmpp_sde", "dpmpp_2m", "dpmpp_2m_sde", "ddim", "uni_pc"] _DEFAULT_SCHEDULERS = ["normal", "karras", "exponential", "sgm_uniform", "simple", "ddim_uniform"] +_PRIMITIVE_NUMBER_INPUT = "STRING,FLOAT,INT" def _get_samplers() -> list[str]: @@ -91,6 +92,25 @@ def _get_schedulers() -> list[str]: return _DEFAULT_SCHEDULERS +def _normalize_flux_guidance(value, fallback: float | None) -> float | None: + if value is None: + return fallback + if isinstance(value, bool): + raise RuntimeError("[LLS] flux_guidance must be a number, not BOOLEAN.") + if isinstance(value, str): + stripped = value.strip() + if not stripped: + return fallback + value = stripped + try: + normalized = float(value) + except (TypeError, ValueError) as exc: + raise RuntimeError(f"[LLS] Invalid flux_guidance value: {value!r}") from exc + if normalized < 0.0 or normalized > 100.0: + raise RuntimeError(f"[LLS] flux_guidance out of supported range [0.0, 100.0]: {normalized}") + return normalized + + def _common_ksampler( model, seed: int, @@ -216,13 +236,29 @@ class LLSSimpleKSampler: "sampler_name": (_get_samplers(), {"default": "euler_ancestral"}), "scheduler": (_get_schedulers(), {"default": "karras"}), "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), - "flux_guidance": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 100.0, "step": 0.1}), + "flux_guidance": ( + _PRIMITIVE_NUMBER_INPUT, + {"default": 3.5, "widgetType": "FLOAT", "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.1}, + ), }, "optional": { "task_context": (LLS_TASK_CONTEXT_TYPE,), }, } + @classmethod + def VALIDATE_INPUTS(cls, flux_guidance=None, input_types=None): + received_type = None + if isinstance(input_types, dict): + received_type = input_types.get("flux_guidance") + if received_type is not None and received_type not in {"STRING", "FLOAT", "INT"}: + return f"flux_guidance only accepts STRING/FLOAT/INT inputs, got {received_type}." + try: + _normalize_flux_guidance(flux_guidance, fallback=3.5) + except RuntimeError as exc: + return str(exc) + return True + def sample( self, model, @@ -236,7 +272,7 @@ class LLSSimpleKSampler: sampler_name: str, scheduler: str, denoise: float, - flux_guidance: float, + flux_guidance, task_context=None, ): if comfy_sample is None: @@ -254,6 +290,9 @@ class LLSSimpleKSampler: info = parse_model_info(context) family = context.get("resolved_model_family") or info["family"] latent_source = latent_image.get("source") or context.get("latent_source") + default_flux_guidance = context.get("recommended_guidance") + if default_flux_guidance is None: + default_flux_guidance = info.get("guidance", info.get("default_guidance")) effective_task_mode = context.get("task_mode") or "txt2img" if latent_source == "empty_latent": effective_task_mode = "txt2img" @@ -266,7 +305,7 @@ class LLSSimpleKSampler: sampler_name = str(context.get("recommended_sampler", sampler_name)) scheduler = str(context.get("recommended_scheduler", scheduler)) denoise = float(context.get("recommended_denoise", denoise)) - context_guidance = context.get("recommended_guidance") + context_guidance = default_flux_guidance if context_guidance is not None: flux_guidance = float(context_guidance) else: @@ -280,6 +319,8 @@ class LLSSimpleKSampler: if preset.get("guidance") is not None: flux_guidance = float(preset["guidance"]) + flux_guidance = _normalize_flux_guidance(flux_guidance, fallback=default_flux_guidance) + if is_flux_family(family) and context.get("supports_flux_guidance", True): guidance_value = flux_guidance else: diff --git a/tests/test_loader_prompt_refactor.py b/tests/test_loader_prompt_refactor.py index d3f2309..844a849 100644 --- a/tests/test_loader_prompt_refactor.py +++ b/tests/test_loader_prompt_refactor.py @@ -572,6 +572,66 @@ class TestLoaderPromptRefactor(unittest.TestCase): self.assertEqual(task_context["task_mode"], "img2img") self.assertEqual(task_context["denoise"], 0.5) + def test_ksampler_accepts_blank_flux_guidance_for_legacy_workflows(self): + load_plugin_package() + from lls_node_test_refactor.sampling import nodes as sampling_nodes + + required = sampling_nodes.LLSSimpleKSampler.INPUT_TYPES()["required"] + flux_input_type, flux_input_opts = required["flux_guidance"] + self.assertEqual(flux_input_type, "STRING,FLOAT,INT") + self.assertEqual(flux_input_opts["widgetType"], "FLOAT") + self.assertTrue(sampling_nodes.LLSSimpleKSampler.VALIDATE_INPUTS(flux_guidance="")) + + recorded = {} + + def fake_common_ksampler(**kwargs): + recorded.update(kwargs) + return kwargs["latent"] + + fake_node_helpers = types.SimpleNamespace( + conditioning_set_values=lambda conditioning, values: [{"conditioning": conditioning, **values}] + ) + with mock.patch.object(sampling_nodes, "comfy_sample", object()), mock.patch.object( + sampling_nodes, + "comfy_samplers", + object(), + ), mock.patch.object( + sampling_nodes, + "_common_ksampler", + side_effect=fake_common_ksampler, + ), mock.patch.object( + sampling_nodes, + "node_helpers", + fake_node_helpers, + ): + node = sampling_nodes.LLSSimpleKSampler() + latent, sample_info, task_context = node.sample( + model="model", + positive=[["pos", {}]], + negative=[["neg", {}]], + latent_image={"samples": FakeTensor((1, 128, 64, 64)), "source": "empty_latent"}, + quality_preset="Manual", + seed=123, + steps=20, + cfg=1.0, + sampler_name="euler", + scheduler="simple", + denoise=1.0, + flux_guidance="", + task_context={ + "resolved_model_family": "FLUX_DEV", + "supports_flux_guidance": True, + "recommended_guidance": 3.5, + }, + ) + + sample_data = json.loads(sample_info) + self.assertEqual(recorded["positive"][0]["guidance"], 3.5) + self.assertEqual(recorded["negative"][0]["guidance"], 3.5) + self.assertEqual(sample_data["guidance"], 3.5) + self.assertEqual(task_context["guidance"], 3.5) + self.assertEqual(latent["samples"].shape, (1, 128, 64, 64)) + def test_vae_decode_updates_task_context_for_flux(self): plugin = load_plugin_package() node_cls = plugin.NODE_CLASS_MAPPINGS["LLSSimpleVAEDecode"]