feat(ml): panel multi-juez comfyui-judge (estetico + CLIP + LLM-vision)

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>
This commit is contained in:
2026-06-24 14:54:32 +02:00
parent 70d541fca9
commit 974cc06bc7
10 changed files with 965 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
"""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:]))