Judge: add seed (reproducible output) + interrupt support

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 00:15:05 +02:00
co-authored by Claude Opus 4.8
parent 64e2110835
commit 4003dbb6e8
4 changed files with 151 additions and 28 deletions
+38 -1
View File
@@ -453,8 +453,33 @@ def _apply_template(processor, messages, think=True):
return _format_chatml_qwenvl(messages) 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): 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.""" """Template + forward pass for a chat-message list; returns the decoded string."""
_check_interrupt()
text = _apply_template(processor, messages, think) text = _apply_template(processor, messages, think)
inputs = processor(text=[text], images=images, return_tensors="pt") inputs = processor(text=[text], images=images, return_tensors="pt")
inputs = inputs.to(model.device) 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)) gen_kwargs.update(do_sample=True, temperature=float(temperature))
else: else:
gen_kwargs.update(do_sample=False) gen_kwargs.update(do_sample=False)
sc = _interrupt_stopping()
if sc is not None:
gen_kwargs["stopping_criteria"] = sc
with torch.inference_mode(): with torch.inference_mode():
out = model.generate(**inputs, **gen_kwargs) out = model.generate(**inputs, **gen_kwargs)
@@ -821,6 +849,9 @@ class QwenVLImageJudge:
"precision": (["bf16", "fp8", "nf4"], {"default": "bf16"}), "precision": (["bf16", "fp8", "nf4"], {"default": "bf16"}),
"max_new_tokens": ("INT", {"default": 3072, "min": 64, "max": 8192}), "max_new_tokens": ("INT", {"default": 3072, "min": 64, "max": 8192}),
"temperature": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.5, "step": 0.05}), "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}), "swap_eval": ("BOOLEAN", {"default": True}),
# Reasoning models (Qwen3.5/3.6) judge verdicts FAR better with thinking on # 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. # (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, 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, enable_thinking=True, json_output=False, model_select=MANUAL_CHOICE,
generated_image=None, keep_loaded=True, auto_download=True, generated_image=None, keep_loaded=True, auto_download=True,
report_dir="", run_tag="", axes="", reference_description="", report_dir="", run_tag="", axes="", reference_description="",
@@ -888,6 +919,12 @@ class QwenVLImageJudge:
ref_pil = _tensor_to_pil(reference_image) ref_pil = _tensor_to_pil(reference_image)
model, processor = _load_model(resolved_path, eff_precision) 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": if mode == "chat":
gen_pil = _tensor_to_pil(generated_image) if generated_image is not None else None 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, return self._chat(model, processor, ref_pil, gen_pil, system_prompt, user_prompt,
+108 -25
View File
@@ -1,8 +1,12 @@
{ {
"4": { "4": {
"class_type": "CheckpointLoaderSimple", "class_type": "CheckpointLoaderSimple",
"inputs": { "ckpt_name": "waiIllustriousSDXL_v160.safetensors" }, "inputs": {
"_meta": { "title": "Load Checkpoint (swap for your T2I)" } "ckpt_name": "waiIllustriousSDXL_v160.safetensors"
},
"_meta": {
"title": "Load Checkpoint (swap for your T2I)"
}
}, },
"10": { "10": {
"class_type": "CalibratorPromptReceptor", "class_type": "CalibratorPromptReceptor",
@@ -12,59 +16,135 @@
"seed": 12345, "seed": 12345,
"source_file": "" "source_file": ""
}, },
"_meta": { "title": "SxCP External Prompt (Receptor)" } "_meta": {
"title": "SxCP External Prompt (Receptor)"
}
}, },
"6": { "6": {
"class_type": "CLIPTextEncode", "class_type": "CLIPTextEncode",
"inputs": { "text": ["10", 0], "clip": ["4", 1] }, "inputs": {
"_meta": { "title": "Positive (from receptor)" } "text": [
"10",
0
],
"clip": [
"4",
1
]
},
"_meta": {
"title": "Positive (from receptor)"
}
}, },
"7": { "7": {
"class_type": "CLIPTextEncode", "class_type": "CLIPTextEncode",
"inputs": { "text": ["10", 1], "clip": ["4", 1] }, "inputs": {
"_meta": { "title": "Negative (from receptor)" } "text": [
"10",
1
],
"clip": [
"4",
1
]
},
"_meta": {
"title": "Negative (from receptor)"
}
}, },
"5": { "5": {
"class_type": "EmptyLatentImage", "class_type": "EmptyLatentImage",
"inputs": { "width": 1024, "height": 1024, "batch_size": 1 }, "inputs": {
"_meta": { "title": "Empty Latent" } "width": 1024,
"height": 1024,
"batch_size": 1
},
"_meta": {
"title": "Empty Latent"
}
}, },
"3": { "3": {
"class_type": "KSampler", "class_type": "KSampler",
"inputs": { "inputs": {
"model": ["4", 0], "model": [
"positive": ["6", 0], "4",
"negative": ["7", 0], 0
"latent_image": ["5", 0], ],
"seed": ["10", 2], "positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"5",
0
],
"seed": [
"10",
2
],
"steps": 28, "steps": 28,
"cfg": 5.5, "cfg": 5.5,
"sampler_name": "euler", "sampler_name": "euler",
"scheduler": "normal", "scheduler": "normal",
"denoise": 1.0 "denoise": 1.0
}, },
"_meta": { "title": "KSampler (seed from receptor)" } "_meta": {
"title": "KSampler (seed from receptor)"
}
}, },
"8": { "8": {
"class_type": "VAEDecode", "class_type": "VAEDecode",
"inputs": { "samples": ["3", 0], "vae": ["4", 2] }, "inputs": {
"_meta": { "title": "VAE Decode" } "samples": [
"3",
0
],
"vae": [
"4",
2
]
},
"_meta": {
"title": "VAE Decode"
}
}, },
"9": { "9": {
"class_type": "SaveImage", "class_type": "SaveImage",
"inputs": { "images": ["8", 0], "filename_prefix": "calibrator/gen" }, "inputs": {
"_meta": { "title": "Save Generated" } "images": [
"8",
0
],
"filename_prefix": "calibrator/gen"
},
"_meta": {
"title": "Save Generated"
}
}, },
"11": { "11": {
"class_type": "LoadImage", "class_type": "LoadImage",
"inputs": { "image": "reference.png" }, "inputs": {
"_meta": { "title": "Reference Image (put in ComfyUI/input/)" } "image": "reference.png"
},
"_meta": {
"title": "Reference Image (put in ComfyUI/input/)"
}
}, },
"12": { "12": {
"class_type": "QwenVLImageJudge", "class_type": "QwenVLImageJudge",
"inputs": { "inputs": {
"reference_image": ["11", 0], "reference_image": [
"generated_image": ["8", 0], "11",
0
],
"generated_image": [
"8",
0
],
"model_path": "/media/p5/qwen3vl_4b_abliterated_comfy_convert/hf_bf16", "model_path": "/media/p5/qwen3vl_4b_abliterated_comfy_convert/hf_bf16",
"precision": "bf16", "precision": "bf16",
"profile": "general", "profile": "general",
@@ -74,8 +154,11 @@
"keep_loaded": true, "keep_loaded": true,
"auto_download": true, "auto_download": true,
"report_dir": "/media/p5/Comfyui/output/calibrator", "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)"
}
} }
} }
+2
View File
@@ -208,6 +208,8 @@
"bf16", "bf16",
3072, 3072,
0.4, 0.4,
0,
"fixed",
false, false,
false, false,
true, true,
+2 -1
View File
@@ -63,7 +63,8 @@
"user_prompt": [ "user_prompt": [
"2", "2",
1 1
] ],
"seed": 0
}, },
"_meta": { "_meta": {
"title": "Judge (chat + json_output) -> LTX beats JSON" "title": "Judge (chat + json_output) -> LTX beats JSON"