"""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": , "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:]))