From 4003dbb6e8b61148f31db33887636a9f68946bf5 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sun, 5 Jul 2026 00:15:05 +0200 Subject: [PATCH] Judge: add seed (reproducible output) + interrupt support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New 'seed' input seeds sampling (torch.manual_seed before generation) so a fixed seed reproduces the same output — no reroll when re-queueing while tweaking downstream nodes; bump it to reroll (named 'seed' so the frontend adds control_after_generate). Generation now honors ComfyUI cancel: a StoppingCriteria halts model.generate() promptly and _check_interrupt() raises between passes. Workflows updated with the seed widget. Co-Authored-By: Claude Opus 4.8 --- nodes/qwen_judge.py | 39 +++++++- workflow/workflow_api.json | 135 +++++++++++++++++++++------ workflow/workflow_ltx_audio.json | 2 + workflow/workflow_ltx_audio_api.json | 3 +- 4 files changed, 151 insertions(+), 28 deletions(-) diff --git a/nodes/qwen_judge.py b/nodes/qwen_judge.py index 9dbee58..67c7aa9 100644 --- a/nodes/qwen_judge.py +++ b/nodes/qwen_judge.py @@ -453,8 +453,33 @@ def _apply_template(processor, messages, think=True): return _format_chatml_qwenvl(messages) +def _check_interrupt(): + """Raise if the user hit ComfyUI's cancel — aborts the node between passes.""" + try: + import comfy.model_management as mm + mm.throw_exception_if_processing_interrupted() + except ImportError: + pass + + +def _interrupt_stopping(): + """StoppingCriteria that halts generate() promptly when ComfyUI is interrupted.""" + try: + import comfy.model_management as mm + from transformers import StoppingCriteria, StoppingCriteriaList + + class _Interrupt(StoppingCriteria): + def __call__(self, input_ids, scores, **kw): + return mm.processing_interrupted() + + return StoppingCriteriaList([_Interrupt()]) + except Exception: + return None + + def _generate_from_messages(model, processor, messages, images, max_new_tokens, temperature, think=True): """Template + forward pass for a chat-message list; returns the decoded string.""" + _check_interrupt() text = _apply_template(processor, messages, think) inputs = processor(text=[text], images=images, return_tensors="pt") inputs = inputs.to(model.device) @@ -464,6 +489,9 @@ def _generate_from_messages(model, processor, messages, images, max_new_tokens, gen_kwargs.update(do_sample=True, temperature=float(temperature)) else: gen_kwargs.update(do_sample=False) + sc = _interrupt_stopping() + if sc is not None: + gen_kwargs["stopping_criteria"] = sc with torch.inference_mode(): out = model.generate(**inputs, **gen_kwargs) @@ -821,6 +849,9 @@ class QwenVLImageJudge: "precision": (["bf16", "fp8", "nf4"], {"default": "bf16"}), "max_new_tokens": ("INT", {"default": 3072, "min": 64, "max": 8192}), "temperature": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.5, "step": 0.05}), + # Seeds sampling so a fixed seed reproduces the same output (no reroll when + # you re-queue while tweaking downstream nodes). Bump it to reroll. + "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}), "swap_eval": ("BOOLEAN", {"default": True}), # Reasoning models (Qwen3.5/3.6) judge verdicts FAR better with thinking on # (off -> they rubber-stamp 'match'). Costs more tokens; raise max_new_tokens. @@ -849,7 +880,7 @@ class QwenVLImageJudge: } def judge(self, reference_image, mode, model_path, precision, - max_new_tokens, temperature, swap_eval, profile="general", + max_new_tokens, temperature, swap_eval, seed=0, profile="general", enable_thinking=True, json_output=False, model_select=MANUAL_CHOICE, generated_image=None, keep_loaded=True, auto_download=True, report_dir="", run_tag="", axes="", reference_description="", @@ -888,6 +919,12 @@ class QwenVLImageJudge: ref_pil = _tensor_to_pil(reference_image) model, processor = _load_model(resolved_path, eff_precision) + # Seed sampling so a fixed seed reproduces the same output; check for cancel. + torch.manual_seed(int(seed)) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(int(seed)) + _check_interrupt() + if mode == "chat": gen_pil = _tensor_to_pil(generated_image) if generated_image is not None else None return self._chat(model, processor, ref_pil, gen_pil, system_prompt, user_prompt, diff --git a/workflow/workflow_api.json b/workflow/workflow_api.json index 5d07205..8b32624 100644 --- a/workflow/workflow_api.json +++ b/workflow/workflow_api.json @@ -1,8 +1,12 @@ { "4": { "class_type": "CheckpointLoaderSimple", - "inputs": { "ckpt_name": "waiIllustriousSDXL_v160.safetensors" }, - "_meta": { "title": "Load Checkpoint (swap for your T2I)" } + "inputs": { + "ckpt_name": "waiIllustriousSDXL_v160.safetensors" + }, + "_meta": { + "title": "Load Checkpoint (swap for your T2I)" + } }, "10": { "class_type": "CalibratorPromptReceptor", @@ -12,59 +16,135 @@ "seed": 12345, "source_file": "" }, - "_meta": { "title": "SxCP External Prompt (Receptor)" } + "_meta": { + "title": "SxCP External Prompt (Receptor)" + } }, "6": { "class_type": "CLIPTextEncode", - "inputs": { "text": ["10", 0], "clip": ["4", 1] }, - "_meta": { "title": "Positive (from receptor)" } + "inputs": { + "text": [ + "10", + 0 + ], + "clip": [ + "4", + 1 + ] + }, + "_meta": { + "title": "Positive (from receptor)" + } }, "7": { "class_type": "CLIPTextEncode", - "inputs": { "text": ["10", 1], "clip": ["4", 1] }, - "_meta": { "title": "Negative (from receptor)" } + "inputs": { + "text": [ + "10", + 1 + ], + "clip": [ + "4", + 1 + ] + }, + "_meta": { + "title": "Negative (from receptor)" + } }, "5": { "class_type": "EmptyLatentImage", - "inputs": { "width": 1024, "height": 1024, "batch_size": 1 }, - "_meta": { "title": "Empty Latent" } + "inputs": { + "width": 1024, + "height": 1024, + "batch_size": 1 + }, + "_meta": { + "title": "Empty Latent" + } }, "3": { "class_type": "KSampler", "inputs": { - "model": ["4", 0], - "positive": ["6", 0], - "negative": ["7", 0], - "latent_image": ["5", 0], - "seed": ["10", 2], + "model": [ + "4", + 0 + ], + "positive": [ + "6", + 0 + ], + "negative": [ + "7", + 0 + ], + "latent_image": [ + "5", + 0 + ], + "seed": [ + "10", + 2 + ], "steps": 28, "cfg": 5.5, "sampler_name": "euler", "scheduler": "normal", "denoise": 1.0 }, - "_meta": { "title": "KSampler (seed from receptor)" } + "_meta": { + "title": "KSampler (seed from receptor)" + } }, "8": { "class_type": "VAEDecode", - "inputs": { "samples": ["3", 0], "vae": ["4", 2] }, - "_meta": { "title": "VAE Decode" } + "inputs": { + "samples": [ + "3", + 0 + ], + "vae": [ + "4", + 2 + ] + }, + "_meta": { + "title": "VAE Decode" + } }, "9": { "class_type": "SaveImage", - "inputs": { "images": ["8", 0], "filename_prefix": "calibrator/gen" }, - "_meta": { "title": "Save Generated" } + "inputs": { + "images": [ + "8", + 0 + ], + "filename_prefix": "calibrator/gen" + }, + "_meta": { + "title": "Save Generated" + } }, "11": { "class_type": "LoadImage", - "inputs": { "image": "reference.png" }, - "_meta": { "title": "Reference Image (put in ComfyUI/input/)" } + "inputs": { + "image": "reference.png" + }, + "_meta": { + "title": "Reference Image (put in ComfyUI/input/)" + } }, "12": { "class_type": "QwenVLImageJudge", "inputs": { - "reference_image": ["11", 0], - "generated_image": ["8", 0], + "reference_image": [ + "11", + 0 + ], + "generated_image": [ + "8", + 0 + ], "model_path": "/media/p5/qwen3vl_4b_abliterated_comfy_convert/hf_bf16", "precision": "bf16", "profile": "general", @@ -74,8 +154,11 @@ "keep_loaded": true, "auto_download": true, "report_dir": "/media/p5/Comfyui/output/calibrator", - "run_tag": "" + "run_tag": "", + "seed": 0 }, - "_meta": { "title": "Qwen3-VL Image Judge (Calibrator)" } + "_meta": { + "title": "Qwen3-VL Image Judge (Calibrator)" + } } -} +} \ No newline at end of file diff --git a/workflow/workflow_ltx_audio.json b/workflow/workflow_ltx_audio.json index 29ca70a..0508d2f 100644 --- a/workflow/workflow_ltx_audio.json +++ b/workflow/workflow_ltx_audio.json @@ -208,6 +208,8 @@ "bf16", 3072, 0.4, + 0, + "fixed", false, false, true, diff --git a/workflow/workflow_ltx_audio_api.json b/workflow/workflow_ltx_audio_api.json index 65b8f9e..818658f 100644 --- a/workflow/workflow_ltx_audio_api.json +++ b/workflow/workflow_ltx_audio_api.json @@ -63,7 +63,8 @@ "user_prompt": [ "2", 1 - ] + ], + "seed": 0 }, "_meta": { "title": "Judge (chat + json_output) -> LTX beats JSON"