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:
+38
-1
@@ -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,
|
||||||
|
|||||||
+109
-26
@@ -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)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,6 +208,8 @@
|
|||||||
"bf16",
|
"bf16",
|
||||||
3072,
|
3072,
|
||||||
0.4,
|
0.4,
|
||||||
|
0,
|
||||||
|
"fixed",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user