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:
@@ -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:]))
|
||||
Reference in New Issue
Block a user