974cc06bc7
Cuatro funciones impuras + pagina madre del grupo comfyui-judge, el gate objetivo de calidad de imagen para tests/DoD y el bucle de mejora de skills: - comfyui_score_aesthetic: estetico LAION-V2 (head MLP sobre CLIP ViT-L/14), subproceso al venv ComfyUI (torch+open_clip). - comfyui_score_clip_alignment: fidelidad prompt-imagen via similitud coseno CLIP. - comfyui_critique_image_llm: critica LLM-vision (compone ask_llm_vision), JSON verdict+score+reasons. - comfyui_judge_image: agregadora, vota mayoria good/bad; degrada si un juez cae. QuickGELU (ViT-L-14-quickgelu/openai) obligatorio: sin el, los embeddings se degradan y el ranking de fidelidad se invierte en silencio. Validado e2e sobre imagenes reales: golden 3 votos coherentes, asserts relativos (nitida>ruido, alineado>desalineado), split 2-1 respeta mayoria en ambos sentidos, degradacion ante 429/model invalido/path invalido sin crash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.6 KiB
Python
139 lines
5.6 KiB
Python
"""comfyui_judge_image — panel multi-juez: vota good/bad la calidad de una imagen.
|
|
|
|
Agregadora del grupo `comfyui-judge`. Es el "modelo adversario" que distingue producto
|
|
bueno de malo combinando tres jueces independientes:
|
|
|
|
- estetico (`comfyui_score_aesthetic`): calidad visual LAION-V2, 0-10
|
|
- fidelidad (`comfyui_score_clip_alignment`): parecido al prompt via CLIP, 0-1
|
|
- critico (`comfyui_critique_image_llm`): veredicto de un LLM-vision, good/bad + 0-10
|
|
|
|
Cada juez emite un voto good/bad; el veredicto final es por MAYORIA. El score agregado es
|
|
la media (ponderable) de los tres normalizados a 0-10. Si un juez falla, se excluye del
|
|
voto y se anota: el panel sigue votando con los restantes (no crashea). Solo devuelve
|
|
``ok=False`` si los tres jueces fallan.
|
|
|
|
Impura: compone tres funciones impuras (subprocesos torch + API Anthropic).
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
|
|
from comfyui_score_aesthetic import comfyui_score_aesthetic # noqa: E402
|
|
from comfyui_score_clip_alignment import comfyui_score_clip_alignment # noqa: E402
|
|
from comfyui_critique_image_llm import comfyui_critique_image_llm # noqa: E402
|
|
|
|
DEFAULT_WEIGHTS = {"aesthetic": 1.0, "clip": 1.0, "llm": 1.0}
|
|
|
|
|
|
def comfyui_judge_image(
|
|
image_path: str,
|
|
prompt: str,
|
|
*,
|
|
weights: dict = None,
|
|
threshold: float = 6.0,
|
|
clip_threshold: float = 0.24,
|
|
server: str = "127.0.0.1:8188",
|
|
model: str = "claude-opus-4-8",
|
|
venv_python: str = "~/ComfyUI/.venv/bin/python3",
|
|
) -> dict:
|
|
"""Juzga una imagen con el panel de tres jueces y devuelve el veredicto por mayoria.
|
|
|
|
Args:
|
|
image_path: ruta a la imagen en disco local.
|
|
prompt: prompt original de la generacion (usado por fidelidad y critico).
|
|
weights: pesos {aesthetic, clip, llm} para el score agregado; None = iguales.
|
|
No afecta al voto por mayoria (1 juez = 1 voto). keyword-only.
|
|
threshold: umbral 0-10 para el voto del juez estetico (score>=threshold => good).
|
|
keyword-only.
|
|
clip_threshold: umbral 0-1 para el voto de fidelidad (score>=clip_threshold => good).
|
|
keyword-only.
|
|
server: host:port del server ComfyUI (pasado a fidelidad). keyword-only.
|
|
model: modelo Anthropic para el juez critico. keyword-only.
|
|
venv_python: python del venv ComfyUI para los jueces estetico/fidelidad. keyword-only.
|
|
|
|
Returns:
|
|
dict ``{ok, verdict, score, votes, reasons, error, details}``.
|
|
``verdict`` 'good'|'bad' por mayoria de votos; ``score`` media ponderada 0-10 de los
|
|
jueces que respondieron; ``votes`` = {clip, aesthetic, llm} cada uno 'good'|'bad'|
|
|
'failed'; ``reasons`` agrega las razones del critico + notas de jueces caidos;
|
|
``details`` lleva el dict crudo de cada juez. ``ok=False`` solo si los tres fallan.
|
|
"""
|
|
w = dict(DEFAULT_WEIGHTS)
|
|
if weights:
|
|
w.update({k: float(v) for k, v in weights.items() if k in w})
|
|
|
|
aes = comfyui_score_aesthetic(image_path, venv_python=venv_python)
|
|
clip = comfyui_score_clip_alignment(image_path, prompt, server=server,
|
|
venv_python=venv_python)
|
|
llm = comfyui_critique_image_llm(image_path, prompt, model=model)
|
|
|
|
votes = {}
|
|
reasons = []
|
|
weighted = [] # (peso, score_0_10)
|
|
|
|
# Juez estetico
|
|
if aes.get("ok"):
|
|
good = aes["score_0_10"] >= threshold
|
|
votes["aesthetic"] = "good" if good else "bad"
|
|
weighted.append((w["aesthetic"], aes["score_0_10"]))
|
|
reasons.append(f"estetico={aes['score_0_10']:.2f}/10 ({votes['aesthetic']})")
|
|
else:
|
|
votes["aesthetic"] = "failed"
|
|
reasons.append(f"estetico FALLO: {aes.get('error', '')}")
|
|
|
|
# Juez de fidelidad
|
|
if clip.get("ok"):
|
|
good = clip["score_0_1"] >= clip_threshold
|
|
votes["clip"] = "good" if good else "bad"
|
|
weighted.append((w["clip"], clip["score_0_1"] * 10.0))
|
|
reasons.append(f"fidelidad={clip['score_0_1']:.3f} ({votes['clip']})")
|
|
else:
|
|
votes["clip"] = "failed"
|
|
reasons.append(f"fidelidad FALLO: {clip.get('error', '')}")
|
|
|
|
# Juez critico LLM
|
|
if llm.get("ok"):
|
|
votes["llm"] = llm["verdict"]
|
|
weighted.append((w["llm"], llm["score_0_10"]))
|
|
reasons.append(f"critico={llm['score_0_10']:.1f}/10 ({llm['verdict']})")
|
|
reasons.extend(f" - {r}" for r in llm.get("reasons", []))
|
|
else:
|
|
votes["llm"] = "failed"
|
|
reasons.append(f"critico FALLO: {llm.get('error', '')}")
|
|
|
|
n_good = sum(1 for v in votes.values() if v == "good")
|
|
n_bad = sum(1 for v in votes.values() if v == "bad")
|
|
n_alive = n_good + n_bad
|
|
|
|
if n_alive == 0:
|
|
return {"ok": False, "verdict": "", "score": 0.0, "votes": votes,
|
|
"reasons": reasons, "error": "los tres jueces fallaron",
|
|
"details": {"aesthetic": aes, "clip": clip, "llm": llm}}
|
|
|
|
# Mayoria; empate => 'bad' (conservador: ante la duda, no es producto).
|
|
verdict = "good" if n_good > n_bad else "bad"
|
|
|
|
tot_w = sum(pw for pw, _ in weighted)
|
|
score = sum(pw * s for pw, s in weighted) / tot_w if tot_w else 0.0
|
|
|
|
return {"ok": True, "verdict": verdict, "score": score, "votes": votes,
|
|
"reasons": reasons, "error": "",
|
|
"details": {"aesthetic": aes, "clip": clip, "llm": llm}}
|
|
|
|
|
|
def _main(argv):
|
|
if len(argv) < 1:
|
|
sys.stderr.write('uso: comfyui_judge_image IMAGEN ["prompt original"]\n')
|
|
return 2
|
|
image_path = argv[0]
|
|
prompt = " ".join(argv[1:])
|
|
res = comfyui_judge_image(image_path, prompt)
|
|
print(json.dumps(res, indent=2, ensure_ascii=False))
|
|
return 0 if res["ok"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(_main(sys.argv[1:]))
|