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,121 @@
|
||||
"""comfyui_critique_image_llm — critica de un LLM-vision sobre una imagen generada.
|
||||
|
||||
Tercer juez del panel `comfyui-judge`: el criterio "humano" que detecta lo que los
|
||||
scores numericos no ven — artefactos, anatomia rota, manos/dedos mal, texto ilegible,
|
||||
composicion incoherente, watermarks. Compone `ask_llm_vision` (grupo claude-direct,
|
||||
API directa de Anthropic) con un system prompt que obliga al modelo a devolver un
|
||||
veredicto estructurado JSON: good/bad + score 0-10 + lista de razones.
|
||||
|
||||
Impura: red (API Anthropic) + lectura de la imagen. Respeta la regla `llm_invocation`:
|
||||
SIEMPRE via claude-direct (`ask_llm_vision`), NUNCA `claude -p`. Cada llamada cuesta
|
||||
tokens de la API; usar con mesura.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "core"))
|
||||
|
||||
from ask_llm_vision import ask_llm_vision # noqa: E402
|
||||
|
||||
DEFAULT_MODEL = "claude-opus-4-8"
|
||||
|
||||
_SYSTEM = (
|
||||
"Eres un juez experto de calidad de imagenes generadas por IA. Evaluas con dureza "
|
||||
"y objetividad: artefactos visuales, anatomia (manos, dedos, ojos, simetria), "
|
||||
"coherencia de la composicion, fidelidad al prompt, texto ilegible, watermarks. "
|
||||
"Respondes SIEMPRE y SOLO con un objeto JSON valido, sin texto adicional ni "
|
||||
"markdown, con esta forma exacta: "
|
||||
'{"verdict": "good"|"bad", "score": <numero 0-10>, "reasons": ["razon corta", ...]}. '
|
||||
"verdict='good' si la imagen es un producto usable y sin defectos graves; 'bad' si "
|
||||
"tiene artefactos, anatomia rota o no cumple el prompt. score: 0=basura, 10=perfecta. "
|
||||
"reasons: 1-4 frases cortas que justifican el veredicto."
|
||||
)
|
||||
|
||||
|
||||
def _extract_json(text: str) -> dict:
|
||||
"""Extrae el primer objeto JSON del texto del modelo (tolera fences ```json)."""
|
||||
fenced = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
|
||||
candidate = fenced.group(1) if fenced else None
|
||||
if candidate is None:
|
||||
brace = re.search(r"\{.*\}", text, re.DOTALL)
|
||||
candidate = brace.group(0) if brace else None
|
||||
if candidate is None:
|
||||
raise ValueError("no se encontro objeto JSON en la respuesta")
|
||||
return json.loads(candidate)
|
||||
|
||||
|
||||
def comfyui_critique_image_llm(
|
||||
image_path: str,
|
||||
prompt: str,
|
||||
*,
|
||||
model: str = DEFAULT_MODEL,
|
||||
max_tokens: int = 1024,
|
||||
token: str = "",
|
||||
) -> dict:
|
||||
"""Pide a un LLM-vision que critique una imagen y devuelve un veredicto estructurado.
|
||||
|
||||
Args:
|
||||
image_path: ruta a la imagen en disco local.
|
||||
prompt: el prompt original con el que se genero la imagen (contexto para juzgar
|
||||
la fidelidad). Puede ir vacio si solo se evalua calidad intrinseca.
|
||||
model: id del modelo Anthropic con vision. Default "claude-opus-4-8". keyword-only.
|
||||
max_tokens: maximo de tokens de la respuesta. keyword-only.
|
||||
token: token OAuth; si vacio lo carga ask_llm_vision automaticamente. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict ``{ok, verdict, score_0_10, reasons, error}``. En exito ``ok=True``,
|
||||
``verdict`` es 'good'|'bad', ``score_0_10`` el score del modelo y ``reasons`` la
|
||||
lista de razones. En error (imagen invalida, API caida, 429, JSON no parseable)
|
||||
``ok=False`` con ``error`` describiendo la causa. Nunca lanza excepcion.
|
||||
"""
|
||||
user_prompt = (
|
||||
f"Prompt original de la generacion: {prompt!r}.\n"
|
||||
if prompt and prompt.strip()
|
||||
else "No se aporto el prompt original; evalua la calidad intrinseca.\n"
|
||||
) + "Evalua esta imagen y responde solo con el JSON pedido."
|
||||
|
||||
res = ask_llm_vision(
|
||||
user_prompt, image_path,
|
||||
model=model, system=_SYSTEM, max_tokens=max_tokens, token=token,
|
||||
)
|
||||
if not res.get("ok"):
|
||||
return {"ok": False, "verdict": "", "score_0_10": 0.0, "reasons": [],
|
||||
"error": res.get("error", "ask_llm_vision fallo")}
|
||||
|
||||
try:
|
||||
data = _extract_json(res["text"])
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
return {"ok": False, "verdict": "", "score_0_10": 0.0, "reasons": [],
|
||||
"error": f"respuesta no parseable: {exc}; texto={res['text'][:200]!r}"}
|
||||
|
||||
verdict = str(data.get("verdict", "")).strip().lower()
|
||||
if verdict not in ("good", "bad"):
|
||||
verdict = "bad" # conservador ante respuesta ambigua
|
||||
try:
|
||||
score = float(data.get("score", 0.0))
|
||||
except (TypeError, ValueError):
|
||||
score = 0.0
|
||||
reasons = data.get("reasons", [])
|
||||
if not isinstance(reasons, list):
|
||||
reasons = [str(reasons)]
|
||||
reasons = [str(r) for r in reasons]
|
||||
|
||||
return {"ok": True, "verdict": verdict, "score_0_10": score,
|
||||
"reasons": reasons, "error": ""}
|
||||
|
||||
|
||||
def _main(argv):
|
||||
if len(argv) < 1:
|
||||
sys.stderr.write('uso: comfyui_critique_image_llm IMAGEN ["prompt original"]\n')
|
||||
return 2
|
||||
image_path = argv[0]
|
||||
prompt = " ".join(argv[1:])
|
||||
res = comfyui_critique_image_llm(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