Add chat mode: use the node as a general VLM, not just a judge
New mode='chat' with system_prompt + user_prompt inputs runs your own prompt over the image(s) and returns raw text in 'analysis' — reusing the same model dropdown, quant, auto-download and backend. Makes it a one-node abliterated VLM for captioning, tagging, Q&A, prompt-from-image, etc. agent_bridge gains --mode chat / --system-prompt / --user-prompt (no receptor needed). Writes a chat report (latest.json) for the agent. Docs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+61
-4
@@ -498,6 +498,17 @@ def _build_describe_prompt(axes: list[str]) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _run_chat(model, processor, images, system_prompt, user_prompt, max_new_tokens, temperature):
|
||||
"""General VLM pass: your own system/user prompt over the image(s) -> raw text."""
|
||||
content = [{"type": "image", "image": img} for img in images]
|
||||
content.append({"type": "text", "text": user_prompt or "Describe this image."})
|
||||
messages = []
|
||||
if system_prompt.strip():
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": content})
|
||||
return _generate_from_messages(model, processor, messages, images, max_new_tokens, temperature)
|
||||
|
||||
|
||||
def _run_describe(model, processor, ref_pil, axes, max_new_tokens, temperature):
|
||||
"""Describe pass: reference only -> raw JSON {caption, axes} string."""
|
||||
messages = [
|
||||
@@ -651,6 +662,27 @@ def _write_report(report_dir, run_tag, overall, merged, diff_analysis, raw_all,
|
||||
return run_path
|
||||
|
||||
|
||||
def _write_chat_report(report_dir, run_tag, system_prompt, user_prompt, response):
|
||||
"""Persist a general-VLM (chat) response so the agent/loop can read it."""
|
||||
base = _report_base_dir(report_dir)
|
||||
try:
|
||||
os.makedirs(base, exist_ok=True)
|
||||
except OSError as e:
|
||||
print(f"[QwenVLImageJudge] could not create report dir {base}: {e}")
|
||||
return ""
|
||||
payload = {"mode": "chat", "run_tag": run_tag, "system_prompt": system_prompt,
|
||||
"user_prompt": user_prompt, "response": response}
|
||||
tag = re.sub(r"[^A-Za-z0-9._-]", "_", run_tag) if run_tag else "chat"
|
||||
run_path = os.path.join(base, f"calib_{tag}.json")
|
||||
for path in (run_path, os.path.join(base, "latest.json")):
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(payload, f, ensure_ascii=False, indent=2)
|
||||
except OSError as e:
|
||||
print(f"[QwenVLImageJudge] failed writing report {path}: {e}")
|
||||
return run_path
|
||||
|
||||
|
||||
def _format_canonical_reference(caption: str, axes_spec: dict) -> str:
|
||||
"""One canonical reference description = the paragraph + the per-axis target
|
||||
values. The compare pass anchors on this so the reference side stays consistent
|
||||
@@ -703,9 +735,10 @@ class QwenVLImageJudge:
|
||||
return {
|
||||
"required": {
|
||||
"reference_image": ("IMAGE",),
|
||||
# describe = reference only -> target description (first pass, seeds the
|
||||
# initial prompt). compare = ref vs generated -> per-axis scoring.
|
||||
"mode": (["compare", "describe"], {"default": "compare"}),
|
||||
# compare = ref vs generated -> per-axis scoring. describe = reference only
|
||||
# -> target description (first pass). chat = general VLM: your own
|
||||
# system_prompt + user_prompt over the image(s) -> raw text.
|
||||
"mode": (["compare", "describe", "chat"], {"default": "compare"}),
|
||||
# Analysis profile: act-specialized axis set (distance-aware where it
|
||||
# matters). `axes` below overrides it when non-empty.
|
||||
"profile": (list(PROFILES.keys()), {"default": "general"}),
|
||||
@@ -730,6 +763,9 @@ class QwenVLImageJudge:
|
||||
# compare: canonical reference text (from describe). When set, compare
|
||||
# anchors on it instead of re-reading the reference image each time.
|
||||
"reference_description": ("STRING", {"default": "", "multiline": True}),
|
||||
# chat mode: use the node as a general VLM with your own prompts.
|
||||
"system_prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"user_prompt": ("STRING", {"default": "Describe this image.", "multiline": True}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -737,7 +773,8 @@ class QwenVLImageJudge:
|
||||
max_new_tokens, temperature, swap_eval, profile="general",
|
||||
model_select=MANUAL_CHOICE, generated_image=None,
|
||||
keep_loaded=True, auto_download=True,
|
||||
report_dir="", run_tag="", prompt_used="", reference_description=""):
|
||||
report_dir="", run_tag="", prompt_used="", reference_description="",
|
||||
system_prompt="", user_prompt="Describe this image."):
|
||||
# `axes` overrides the profile when provided; otherwise use the profile's axis set.
|
||||
axis_list = [a.strip() for a in re.split(r"[,\n]", axes) if a.strip()]
|
||||
if not axis_list:
|
||||
@@ -772,6 +809,12 @@ class QwenVLImageJudge:
|
||||
ref_pil = _tensor_to_pil(reference_image)
|
||||
model, processor = _load_model(resolved_path, eff_precision)
|
||||
|
||||
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,
|
||||
max_new_tokens, temperature, resolved_path, eff_precision,
|
||||
keep_loaded, report_dir, run_tag)
|
||||
|
||||
if mode == "describe":
|
||||
return self._describe(model, processor, ref_pil, axis_list, max_new_tokens,
|
||||
temperature, resolved_path, eff_precision, keep_loaded,
|
||||
@@ -827,6 +870,20 @@ class QwenVLImageJudge:
|
||||
|
||||
return (round(overall, 4), axis_scores, diff_analysis, raw_all, report_path)
|
||||
|
||||
def _chat(self, model, processor, ref_pil, gen_pil, system_prompt, user_prompt,
|
||||
max_new_tokens, temperature, resolved_path, precision, keep_loaded,
|
||||
report_dir, run_tag):
|
||||
"""General-VLM mode: not a judge — just runs your prompt over the image(s)."""
|
||||
images = [ref_pil] + ([gen_pil] if gen_pil is not None else [])
|
||||
text = _run_chat(model, processor, images, system_prompt, user_prompt,
|
||||
max_new_tokens, temperature).strip()
|
||||
if not keep_loaded:
|
||||
_MODEL_CACHE.pop((resolved_path, precision), None)
|
||||
del model
|
||||
torch.cuda.empty_cache()
|
||||
report_path = _write_chat_report(report_dir, run_tag, system_prompt, user_prompt, text)
|
||||
return (1.0, "{}", text, text, report_path)
|
||||
|
||||
def _describe(self, model, processor, ref_pil, axis_list, max_new_tokens,
|
||||
temperature, resolved_path, precision, keep_loaded, report_dir, run_tag):
|
||||
"""First pass: describe the reference image the generator must reproduce.
|
||||
|
||||
Reference in New Issue
Block a user