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
+1
View File
@@ -69,6 +69,7 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu
| [eda](eda.md) | 27 | Exploratory Data Analysis por tabla y base con motor DuckDB + PostgreSQL push-down: perfil base SQL (SUMMARIZE + distinct exacto), estadística numérica/categórica, tipo semántico regex, calidad, correlación/asociación (Pearson/Spearman/Cramér's V/Theil's U/η/MI), relaciones inter-tabla (FK containment + join graph mermaid), modelos baratos (PCA/KMeans/IsolationForest/normalidad/tendencia), capa LLM (dictionary/PII/limpieza/análisis) y generación de notebook. Orquestadores `profile_table` (backend duckdb/postgres, flags run_models/run_llm) y `profile_database` |
| [seo](seo.md) | 3 | SEO orientado a datos sobre Google Search Console: autenticar con service account (`gsc_auth`), extraer Search Analytics paginado (`pull_gsc_search_analytics`) y el pipeline de ingesta a DuckDB + espejo Postgres para Metabase (`ingest_gsc_search_analytics`). Cadena de ingesta del proyecto `seo_analytics`; alimenta dashboards de striking distance, CTR opportunities y content decay |
| [local-hub](local-hub.md) | 4 | Exponer los procesos locales como subdominios `*.localhost` (via Caddy, sin DNS) y reunirlos en una pantalla principal Glance con estado en vivo, refrescada a diario por dag_engine. Descubre servicios (manifiesto + registry), renderiza Caddyfile + config Glance (puras), y el pipeline `refresh_local_hub` regenera+recarga. Fuente de verdad: `apps/local_hub/local_services.yaml` |
| [comfyui-judge](comfyui-judge.md) | 4 | Panel multi-juez de calidad de imagen: estético LAION-V2 (`comfyui_score_aesthetic`, 0-10) + fidelidad CLIP prompt↔imagen (`comfyui_score_clip_alignment`, 0-1) + crítica LLM-vision (`comfyui_critique_image_llm`, good/bad). Agregados por voto mayoría en `comfyui_judge_image`. Gate objetivo para tests/DoD y el bucle de mejora de skills ComfyUI; degrada con gracia si un juez cae. Jueces estético/fidelidad por subproceso al venv ComfyUI (torch+open_clip), crítico via claude-direct |
| [comfyui](comfyui.md) | 29 | Controlar ComfyUI (Stable Diffusion por grafos) de dos formas: por API HTTP (build_txt2img_workflow puro → submit → wait → object_info; download_model con validación Civitai/HF) y por la UI web vía CDP sobre la pestaña abierta (load_workflow_ui, set_node_widget_ui para tunear prompt/steps/seed en vivo, queue_prompt_ui = botón Queue Prompt, export_workflow_ui, refresh_nodes_ui). El API format es el puente entre ambos caminos. Las funciones de UI componen `cdp_eval`. Incluye imagen→3D nativo (Hunyuan3D-2, tag `img-to-3d`): build_image_to_3d_workflow + fetch_output_mesh + install_3d_model + pipeline image_to_3d_oneshot |
## Como anadir grupo
+88
View File
@@ -0,0 +1,88 @@
# comfyui-judge — panel multi-juez de calidad de imagen
El "modelo adversario" del pipeline ComfyUI: el sistema que distingue **producto bueno de
malo** de forma objetiva. Tres jueces independientes puntúan/critican una imagen y un
agregador vota por mayoría. Es el **gate objetivo** que consumen los tests, los contratos
DoD y el bucle de mejora de skills (grupo `comfyui-skill`): un skill no está "hecho" hasta
que la imagen que produce pasa el panel (`verdict == 'good'`).
## Funciones del grupo
| ID | Firma corta | Qué hace |
|---|---|---|
| `comfyui_score_aesthetic_py_ml` | `score_aesthetic(image_path) -> {ok, score_0_10}` | Calidad estética LAION-V2 (head MLP sobre CLIP ViT-L/14). Subproceso al venv ComfyUI. Barato, determinista, sin API. |
| `comfyui_score_clip_alignment_py_ml` | `score_clip_alignment(image_path, prompt) -> {ok, score_0_1}` | Fidelidad prompt↔imagen (similitud coseno CLIP). Subproceso al venv ComfyUI. |
| `comfyui_critique_image_llm_py_ml` | `critique_image_llm(image_path, prompt) -> {ok, verdict, score_0_10, reasons}` | Crítica de un LLM-vision (artefactos, anatomía, watermarks). Compone `ask_llm_vision` (claude-direct). Cuesta API. |
| `comfyui_judge_image_py_ml` | `judge_image(image_path, prompt) -> {ok, verdict, score, votes, reasons}` | **Agregadora.** Llama a los 3, vota good/bad por mayoría, agrega razones. Degrada si un juez cae. |
Los tres jueces son ortogonales a propósito: el estético mide *belleza*, el de fidelidad mide
*que sea lo pedido*, y el crítico LLM ve *defectos finos* que un score global no penaliza. Un
único score se engaña fácil; tres votos independientes, no.
## Ejemplo canónico (end-to-end)
```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from ml.comfyui_judge_image import comfyui_judge_image
img = os.path.expanduser("~/ComfyUI/output/comfy_sdxl_00001_.png")
prompt = "a majestic lion standing on rocks at sunset, photorealistic"
res = comfyui_judge_image(img, prompt)
print(res["verdict"], round(res["score"], 2), res["votes"])
# good 7.1 {'aesthetic': 'good', 'clip': 'good', 'llm': 'good'}
if res["verdict"] != "good":
for r in res["reasons"]:
print(" -", r) # feedback accionable para mejorar el skill
```
Jueces sueltos (cuando solo quieres una dimensión):
```bash
# estético (sin API, rápido)
python/.venv/bin/python3 python/functions/ml/comfyui_score_aesthetic.py ~/ComfyUI/output/comfy_sdxl_00001_.png
# fidelidad
python/.venv/bin/python3 python/functions/ml/comfyui_score_clip_alignment.py ~/ComfyUI/output/comfy_sdxl_00001_.png "a lion at sunset"
```
## Cómo se enchufa a tests / DoD como gate objetivo
- **e2e_check de un skill ComfyUI**: declarar en el `app.md` (o en el contrato del skill) un
check que genere la imagen y exija `comfyui_judge_image(img, prompt)["verdict"] == "good"`.
Es la cláusula golden: producto = imagen que el panel aprueba, no "el workflow no petó".
- **Bucle de mejora** (grupo `comfyui-skill`): tras generar, juzgar; si `verdict == 'bad'`,
las `reasons` del juez crítico indican qué corregir (más steps, otro sampler, fix de prompt)
y se re-genera. Convergencia = el panel aprueba.
- **Selección de la mejor de N**: ejecutar el panel sobre cada candidata y rankear por `score`
(o filtrar por `verdict == 'good'`).
Umbrales por defecto (ajustables): estético `>= 6.0` → good; fidelidad `>= 0.24` → good; el
crítico da su propio verdict. Veredicto final = **mayoría** de los votos vivos; empate → `bad`.
## Fronteras (qué NO cubre)
- **No genera imágenes.** Eso es el grupo `comfyui` (build_*_workflow + submit + wait + fetch).
Este grupo solo *juzga* una imagen ya producida.
- **No es un detector forense de IA** ni un clasificador NSFW/seguridad: juzga *calidad de
producto*, no procedencia ni políticas de contenido.
- **No corre en el venv del registry.** Los jueces estético/fidelidad necesitan torch +
open_clip, que viven en `~/ComfyUI/.venv`; se invocan por subproceso. El crítico necesita la
API Anthropic (claude-direct).
- **No persiste resultados.** Devuelve dicts en memoria; persistir veredictos (operations.db,
e2e_runs) es responsabilidad del consumidor.
## Prerequisitos
- venv de ComfyUI con torch + open_clip 3.x: `~/ComfyUI/.venv/bin/python3`.
- Modelo estético LAION en `/mnt/2tb/comfyui_models/aesthetic/sac+logos+ava1-l14-linearMSE.pth`.
- CLIP ViT-L-14-quickgelu (pretrained openai) cacheado (se descarga la 1ª vez, ~900 MB).
- Token OAuth de Claude (claude-direct) para el juez crítico — lo resuelve `ask_llm_vision`.
## Notas
- **QuickGELU** es obligatorio en CLIP (`ViT-L-14-quickgelu`/`openai`): sin él los embeddings
se degradan en silencio y tanto el score estético como el ranking de fidelidad se desvirtúan.
- El panel **degrada con gracia**: si un juez cae (p.ej. el LLM en HTTP 429), vota con los
demás y lo anota; solo falla si caen los tres.
@@ -0,0 +1,74 @@
---
name: comfyui_critique_image_llm
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: impure
signature: "def comfyui_critique_image_llm(image_path: str, prompt: str, *, model: str = 'claude-opus-4-8', max_tokens: int = 1024, token: str = '') -> dict"
description: "Critica de un LLM-vision sobre una imagen generada: detecta artefactos, anatomia rota, texto ilegible, watermarks, incoherencias de composicion. Tercer juez del panel comfyui-judge. Compone ask_llm_vision (claude-direct, API directa) con un system prompt que obliga a devolver JSON {verdict good|bad, score 0-10, reasons}. Impura: red (API Anthropic) + lectura de imagen."
tags: [comfyui-judge, ml, llm, vision, critique, scoring]
uses_functions: [ask_llm_vision_py_core]
uses_types: []
returns: []
returns_optional: false
error_type: error_go_core
imports: []
params:
- name: image_path
desc: "Ruta a la imagen en disco local."
- name: prompt
desc: "Prompt original de la generacion (contexto para juzgar la fidelidad). Puede ir vacio si solo se evalua calidad intrinseca."
- name: model
desc: "id del modelo Anthropic con vision. Default 'claude-opus-4-8'. keyword-only."
- name: max_tokens
desc: "Maximo de tokens de la respuesta. keyword-only."
- name: token
desc: "Token OAuth; si vacio lo carga ask_llm_vision automaticamente. keyword-only."
output: "dict {ok, verdict, score_0_10, reasons, error}. En exito ok=True, verdict '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. Nunca lanza excepcion."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/ml/comfyui_critique_image_llm.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from ml.comfyui_critique_image_llm import comfyui_critique_image_llm
img = os.path.expanduser("~/ComfyUI/output/comfy_sdxl_00001_.png")
res = comfyui_critique_image_llm(img, "a majestic lion standing on rocks at sunset, photorealistic")
print(res)
# {'ok': True, 'verdict': 'good', 'score_0_10': 8.0,
# 'reasons': ['anatomia del leon correcta', 'iluminacion coherente con el atardecer'], 'error': ''}
```
CLI directa:
```bash
python/.venv/bin/python3 python/functions/ml/comfyui_critique_image_llm.py ~/ComfyUI/output/comfy_sdxl_00001_.png "a majestic lion at sunset"
```
## Cuando usarla
- Cuando los scores numericos (estetico/CLIP) no bastan: el LLM ve defectos finos que el
score global no penaliza (dedos de mas, ojos asimetricos, texto basura, watermarks).
- Como el juez "humano" de `comfyui_judge_image`, para desempatar good/bad.
- Para obtener razones LEGIBLES de por que una imagen es mala (feedback accionable al skill).
## Gotchas
- **Cuesta tokens de API.** Cada llamada va a api.anthropic.com (claude-direct). Usar con
mesura; en validacion, pocas llamadas.
- **Rate limit (HTTP 429).** Si el rate limit compartido esta saturado, devuelve
`ok=False` con el error 429 — no crashea. El panel `comfyui_judge_image` sigue votando con
los otros dos jueces. Reintentar mas tarde o bajar a un modelo con cuota (haiku).
- **El modelo puede no devolver JSON limpio.** Se extrae el primer objeto `{...}` (tolera
fences ```json). Si no hay JSON parseable, `ok=False`.
- **verdict conservador.** Si el modelo responde algo ambiguo (ni 'good' ni 'bad'), se
normaliza a 'bad' (ante la duda, no es producto).
- **NUNCA `claude -p`.** Respeta la regla `llm_invocation`: solo claude-direct via
ask_llm_vision (arranque 0, API directa).
@@ -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:]))
@@ -0,0 +1,80 @@
---
name: comfyui_judge_image
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: impure
signature: "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"
description: "Panel multi-juez que vota good/bad la calidad de una imagen generada. Agregadora del grupo comfyui-judge: combina comfyui_score_aesthetic (estetico 0-10), comfyui_score_clip_alignment (fidelidad 0-1) y comfyui_critique_image_llm (LLM-vision good/bad). Cada juez vota; veredicto por MAYORIA. Si un juez falla, se excluye y se anota; el panel sigue con los restantes. Es el gate objetivo para tests/DoD y el bucle de mejora de skills."
tags: [comfyui-judge, ml, quality, gate, aggregator, scoring]
uses_functions: [comfyui_score_aesthetic_py_ml, comfyui_score_clip_alignment_py_ml, comfyui_critique_image_llm_py_ml]
uses_types: []
returns: []
returns_optional: false
error_type: error_go_core
imports: []
params:
- name: image_path
desc: "Ruta a la imagen en disco local."
- name: prompt
desc: "Prompt original de la generacion (usado por los jueces de fidelidad y critico)."
- name: weights
desc: "Pesos {aesthetic, clip, llm} para el score agregado; None = iguales. No afecta al voto por mayoria (1 juez = 1 voto). keyword-only."
- name: threshold
desc: "Umbral 0-10 para el voto del juez estetico (score>=threshold => good). keyword-only."
- name: clip_threshold
desc: "Umbral 0-1 para el voto de fidelidad (score>=clip_threshold => good). keyword-only."
- name: server
desc: "host:port del server ComfyUI (pasado al juez de fidelidad). keyword-only."
- name: model
desc: "Modelo Anthropic para el juez critico LLM. keyword-only."
- name: venv_python
desc: "Python del venv ComfyUI para los jueces estetico/fidelidad. keyword-only."
output: "dict {ok, verdict, score, votes, reasons, error, details}. verdict 'good'|'bad' por mayoria; score media ponderada 0-10 de los jueces vivos; votes = {clip, aesthetic, llm} cada uno 'good'|'bad'|'failed'; reasons agrega razones del critico + notas de jueces caidos; details lleva el dict crudo de cada juez. ok=False solo si los tres fallan."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/ml/comfyui_judge_image.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from ml.comfyui_judge_image import comfyui_judge_image
img = os.path.expanduser("~/ComfyUI/output/comfy_sdxl_00001_.png")
res = comfyui_judge_image(img, "a majestic lion standing on rocks at sunset, photorealistic")
print(res["verdict"], round(res["score"], 2), res["votes"])
# good 7.1 {'aesthetic': 'good', 'clip': 'good', 'llm': 'good'}
```
CLI directa:
```bash
python/.venv/bin/python3 python/functions/ml/comfyui_judge_image.py ~/ComfyUI/output/comfy_sdxl_00001_.png "a majestic lion at sunset"
```
## Cuando usarla
- Como GATE objetivo de un test o DoD: "la imagen que produce este skill ComfyUI debe pasar
el panel (verdict='good')". Un voto mayoritario es mas robusto que un solo score.
- En el bucle de mejora de skills: si el panel vota 'bad', sus `reasons` dicen que arreglar.
- Para elegir la mejor de N generaciones: ejecutar el panel sobre cada una y rankear por
`score` (o exigir verdict='good').
## Gotchas
- **Es la suma de las gotchas de sus tres jueces.** Necesita el venv de ComfyUI (torch +
open_clip) para estetico/fidelidad y la API Anthropic para el critico.
- **Degradacion, no fallo.** Si un juez cae (p.ej. el LLM en 429), se marca `votes[x]='failed'`,
se anota en `reasons` y el veredicto sale por mayoria de los 2 restantes. Solo `ok=False`
si los TRES fallan.
- **Empate => 'bad'.** Con 2 jueces vivos y voto 1-1 (o cualquier empate), el veredicto es
'bad' por conservadurismo (ante la duda, no es producto).
- **El voto es 1 juez = 1 voto; los `weights` solo ponderan el `score` numerico**, no el
veredicto. Para cambiar el veredicto, ajusta `threshold`/`clip_threshold`.
- **El juez critico cuesta API.** Llamar al panel en bucle sobre muchas imagenes consume
tokens; considerar saltarse el LLM (futuro flag) para barridos grandes.
+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:]))
@@ -0,0 +1,73 @@
---
name: comfyui_score_aesthetic
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: impure
signature: "def comfyui_score_aesthetic(image_path: str, *, model_path: str = '/mnt/2tb/comfyui_models/aesthetic/sac+logos+ava1-l14-linearMSE.pth', venv_python: str = '~/ComfyUI/.venv/bin/python3', device: str = 'auto', timeout: float = 180.0) -> dict"
description: "Puntuacion estetica LAION-V2 (0-10) de una imagen. Juez objetivo del panel comfyui-judge: head MLP lineal sobre embeddings CLIP ViT-L/14 normalizados (modelo sac+logos+ava1-l14-linearMSE). Impura: delega la inferencia a un subproceso contra el python del venv de ComfyUI (torch + open_clip) porque el venv del registry no tiene torch."
tags: [comfyui-judge, ml, aesthetic, clip, quality, scoring]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: error_go_core
imports: []
params:
- name: image_path
desc: "Ruta a la imagen (PNG/JPG/WebP) en disco local."
- name: model_path
desc: "Ruta al .pth del head estetico LAION (sac+logos+ava1-l14-linearMSE). keyword-only."
- name: venv_python
desc: "Python del venv de ComfyUI con torch + open_clip; se expande '~'. La inferencia corre ahi por subproceso porque el venv del registry no tiene torch. keyword-only."
- name: device
desc: "'auto' (cuda si hay, si no cpu), 'cuda' o 'cpu'. keyword-only."
- name: timeout
desc: "Timeout del subproceso en segundos (la primera vez puede descargar CLIP). keyword-only."
output: "dict {ok, score_0_10, error}. En exito ok=True y score_0_10 es el score continuo (~1-10, mayor = mejor). En error ok=False, score_0_10=0.0 y error describe la causa. Nunca lanza excepcion."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/ml/comfyui_score_aesthetic.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from ml.comfyui_score_aesthetic import comfyui_score_aesthetic
res = comfyui_score_aesthetic(os.path.expanduser("~/ComfyUI/output/comfy_sdxl_00001_.png"))
print(res)
# {'ok': True, 'score_0_10': 6.536..., 'error': ''}
```
CLI directa:
```bash
python/.venv/bin/python3 python/functions/ml/comfyui_score_aesthetic.py ~/ComfyUI/output/comfy_sdxl_00001_.png
```
## Cuando usarla
- Cuando necesites una nota objetiva de calidad visual de una imagen generada, sin pasar
por un LLM (barato, determinista, sin coste de API).
- Como uno de los tres jueces de `comfyui_judge_image`, o suelta para comparar dos salidas
de un skill ComfyUI y quedarte con la mejor.
- Antes de promocionar una imagen a "producto": un score bajo (~<5) es señal de descarte.
## Gotchas
- **No corre en el venv del registry.** `python/.venv` no tiene torch; la inferencia se
ejecuta por subproceso contra `~/ComfyUI/.venv/bin/python3`. Si ese python no existe o no
tiene `open_clip`/`torch`, devuelve `ok=False`.
- **QuickGELU obligatorio.** El modelo es `ViT-L-14-quickgelu` pretrained `openai`. Crear
CLIP sin quick_gelu degrada los embeddings y desvirtua el score (mismatch silencioso).
- **Primera llamada lenta.** Si CLIP ViT-L/14 no esta cacheado, la primera ejecucion lo
descarga (~900 MB) y puede acercarse al `timeout`. Cacheado, ~3-6 s en GPU.
- **Score absoluto poco interpretable.** Util sobre todo como medida RELATIVA (A vs B) o con
umbral calibrado (~6.0 separa decente de mediocre en este modelo). Imagenes de ruido caen
a ~3-4; fotos buenas ~6-7.
- **Necesita el .pth** en `/mnt/2tb/comfyui_models/aesthetic/`; si falta, `ok=False`.
@@ -0,0 +1,173 @@
"""comfyui_score_aesthetic — puntuacion estetica LAION-V2 (0-10) de una imagen.
Juez objetivo de calidad del panel `comfyui-judge`. Usa el predictor estetico
LAION "improved aesthetic" (head MLP lineal sobre embeddings CLIP ViT-L/14
normalizados): el .pth `sac+logos+ava1-l14-linearMSE.pth` entrenado sobre AVA +
SAC + LOGOS, que devuelve un score continuo ~1-10 donde mayor = mas atractiva.
Impura: el scoring necesita torch + open_clip + CUDA, que NO estan en el venv del
registry (`python/.venv`). Por eso esta funcion delega la inferencia a un SUBPROCESO
contra el python del venv de ComfyUI (`~/ComfyUI/.venv/bin/python3`), que si los
tiene. El subproceso reconstruye el MLP head, carga CLIP ViT-L-14-quickgelu
(pretrained openai, ya cacheado), embebe la imagen, la normaliza L2 y aplica el head.
La funcion captura un JSON marcado en stdout y nunca lanza excepcion.
"""
import json
import os
import subprocess
import sys
DEFAULT_MODEL_PATH = "/mnt/2tb/comfyui_models/aesthetic/sac+logos+ava1-l14-linearMSE.pth"
DEFAULT_VENV_PYTHON = "~/ComfyUI/.venv/bin/python3"
# Script autonomo ejecutado por el python del venv de ComfyUI (tiene torch + open_clip).
# Reconstruye el MLP head LAION-V2 y puntua la imagen. Emite el resultado en una
# linea marcada con _MARKER para aislarlo de warnings / barras de descarga.
_MARKER = "__AESTHETIC_RESULT__"
_INFER_SCRIPT = r'''
import json, sys
import numpy as np
import torch
import torch.nn as nn
import open_clip
from PIL import Image
MARKER = "__AESTHETIC_RESULT__"
class AestheticMLP(nn.Module):
def __init__(self, input_size=768):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(input_size, 1024),
nn.Dropout(0.2),
nn.Linear(1024, 128),
nn.Dropout(0.2),
nn.Linear(128, 64),
nn.Dropout(0.1),
nn.Linear(64, 16),
nn.Linear(16, 1),
)
def forward(self, x):
return self.layers(x)
def _l2norm(a, axis=-1):
l2 = np.atleast_1d(np.linalg.norm(a, 2, axis))
l2[l2 == 0] = 1
return a / np.expand_dims(l2, axis)
def run(image_path, model_path, device):
if device == "auto":
device = "cuda" if torch.cuda.is_available() else "cpu"
model, _, preprocess = open_clip.create_model_and_transforms(
"ViT-L-14-quickgelu", pretrained="openai")
model = model.to(device).eval()
head = AestheticMLP(768)
head.load_state_dict(torch.load(model_path, map_location="cpu"))
head = head.to(device).eval()
img = preprocess(Image.open(image_path).convert("RGB")).unsqueeze(0).to(device)
with torch.no_grad():
feats = model.encode_image(img).cpu().numpy()
norm = _l2norm(feats)
with torch.no_grad():
pred = head(torch.from_numpy(norm).to(device).float())
return float(pred.cpu().numpy()[0][0])
if __name__ == "__main__":
image_path, model_path, device = sys.argv[1], sys.argv[2], sys.argv[3]
try:
score = run(image_path, model_path, device)
print(MARKER + json.dumps({"ok": True, "score_0_10": score, "error": ""}))
except Exception as exc:
print(MARKER + json.dumps({"ok": False, "score_0_10": 0.0, "error": repr(exc)}))
'''
def comfyui_score_aesthetic(
image_path: str,
*,
model_path: str = DEFAULT_MODEL_PATH,
venv_python: str = DEFAULT_VENV_PYTHON,
device: str = "auto",
timeout: float = 180.0,
) -> dict:
"""Puntua la calidad estetica (0-10) de una imagen con el predictor LAION-V2.
Args:
image_path: ruta a la imagen (PNG/JPG/WebP) en disco local.
model_path: ruta al .pth del head estetico LAION (sac+logos+ava1-l14-linearMSE).
keyword-only.
venv_python: python del venv de ComfyUI con torch + open_clip; se expande `~`.
La inferencia corre ahi por subproceso porque el venv del registry no tiene
torch. keyword-only.
device: "auto" (cuda si hay, si no cpu), "cuda" o "cpu". keyword-only.
timeout: timeout del subproceso en segundos (la primera vez puede descargar CLIP).
keyword-only.
Returns:
dict ``{ok, score_0_10, error}``. En exito ``ok=True`` y ``score_0_10`` es el
score continuo (~1-10, mayor = mejor). En error ``ok=False``, ``score_0_10=0.0``
y ``error`` describe la causa. Nunca lanza excepcion.
"""
img = os.path.expanduser(image_path)
if not os.path.isfile(img):
return {"ok": False, "score_0_10": 0.0, "error": f"imagen no encontrada: {img}"}
py = os.path.expanduser(venv_python)
if not os.path.isfile(py):
return {"ok": False, "score_0_10": 0.0,
"error": f"python del venv ComfyUI no encontrado: {py}"}
mp = os.path.expanduser(model_path)
if not os.path.isfile(mp):
return {"ok": False, "score_0_10": 0.0,
"error": f"modelo estetico no encontrado: {mp}"}
try:
proc = subprocess.run(
[py, "-c", _INFER_SCRIPT, img, mp, device],
capture_output=True, text=True, timeout=timeout,
)
except subprocess.TimeoutExpired:
return {"ok": False, "score_0_10": 0.0,
"error": f"subproceso de inferencia excedio {timeout}s"}
for line in proc.stdout.splitlines():
if line.startswith(_MARKER):
try:
return json.loads(line[len(_MARKER):])
except json.JSONDecodeError as exc:
return {"ok": False, "score_0_10": 0.0,
"error": f"JSON invalido del subproceso: {exc}"}
tail = (proc.stderr or proc.stdout or "")[-400:]
return {"ok": False, "score_0_10": 0.0,
"error": f"sin resultado del subproceso (rc={proc.returncode}): {tail}"}
def _main(argv):
if not argv:
sys.stderr.write("uso: comfyui_score_aesthetic IMAGEN [--device cpu|cuda]\n")
return 2
device = "auto"
image_path = ""
i = 0
while i < len(argv):
if argv[i] == "--device" and i + 1 < len(argv):
device = argv[i + 1]
i += 2
else:
image_path = argv[i]
i += 1
res = comfyui_score_aesthetic(image_path, device=device)
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:]))
@@ -0,0 +1,76 @@
---
name: comfyui_score_clip_alignment
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: impure
signature: "def comfyui_score_clip_alignment(image_path: str, prompt: str, *, server: str = '127.0.0.1:8188', venv_python: str = '~/ComfyUI/.venv/bin/python3', device: str = 'auto', timeout: float = 180.0) -> dict"
description: "Fidelidad prompt<->imagen via CLIP (0-1): similitud coseno entre el embedding de la imagen y el del texto con CLIP ViT-L/14. Juez de fidelidad del panel comfyui-judge. Impura: delega a un subproceso contra el python del venv de ComfyUI (torch + open_clip); calculo local, no via el server (el param server queda reservado)."
tags: [comfyui-judge, ml, clip, alignment, fidelity, scoring]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: error_go_core
imports: []
params:
- name: image_path
desc: "Ruta a la imagen en disco local."
- name: prompt
desc: "Texto contra el que medir la alineacion (idealmente en ingles, como CLIP)."
- name: server
desc: "host:port del server ComfyUI. RESERVADO para un backend futuro basado en nodos CLIPVisionEncode/CLIPTextEncode; hoy el calculo es local via open_clip. keyword-only."
- name: venv_python
desc: "Python del venv de ComfyUI con torch + open_clip; se expande '~'. keyword-only."
- name: device
desc: "'auto' (cuda si hay, si no cpu), 'cuda' o 'cpu'. keyword-only."
- name: timeout
desc: "Timeout del subproceso en segundos. keyword-only."
output: "dict {ok, score_0_1, error}. En exito ok=True y score_0_1 es la similitud coseno clamp a [0,1] (mayor = mas fiel al prompt; tipico 0.28-0.35 buen match, 0.10-0.18 distinto). En error ok=False, score_0_1=0.0 y error describe la causa. Nunca lanza excepcion."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/ml/comfyui_score_clip_alignment.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from ml.comfyui_score_clip_alignment import comfyui_score_clip_alignment
img = os.path.expanduser("~/ComfyUI/output/comfy_sdxl_00001_.png")
print(comfyui_score_clip_alignment(img, "a lion standing on rocks at sunset"))
# {'ok': True, 'score_0_1': 0.2818, 'error': ''} (match)
print(comfyui_score_clip_alignment(img, "a red apple on a table"))
# {'ok': True, 'score_0_1': 0.1187, 'error': ''} (mismatch -> mas bajo)
```
CLI directa:
```bash
python/.venv/bin/python3 python/functions/ml/comfyui_score_clip_alignment.py ~/ComfyUI/output/comfy_sdxl_00001_.png "a lion standing on rocks at sunset"
```
## Cuando usarla
- Cuando necesites comprobar que una imagen REALMENTE muestra lo que el prompt pedia
(fidelidad), no solo que es bonita. Complementa a `comfyui_score_aesthetic`.
- Para elegir entre varias generaciones del mismo prompt la que mejor lo representa.
- Como uno de los tres jueces de `comfyui_judge_image`.
## Gotchas
- **No corre en el venv del registry.** Igual que el scorer estetico, la inferencia va por
subproceso contra `~/ComfyUI/.venv/bin/python3` (torch + open_clip).
- **QuickGELU obligatorio** (`ViT-L-14-quickgelu` pretrained `openai`). Sin quick_gelu los
embeddings se degradan y el ranking de prompts se invierte de forma silenciosa.
- **El score es similitud coseno, no una probabilidad.** Rangos tipicos: ~0.28-0.35 buen
match, ~0.10-0.18 contenido distinto. Usa un umbral (~0.24) o compara RELATIVAMENTE; no
esperes valores cercanos a 1.0 ni para un match perfecto.
- **Prompts en ingles.** CLIP openai esta entrenado en ingles; prompts en otros idiomas dan
scores mas bajos y menos fiables.
- **`server` no se usa hoy.** El calculo es local; el parametro queda como contrato para un
backend server futuro.
@@ -0,0 +1,141 @@
"""comfyui_score_clip_alignment — fidelidad prompt<->imagen via CLIP (0-1).
Juez de fidelidad del panel `comfyui-judge`: mide cuanto se parece el contenido de
una imagen al prompt que se pidio, usando el espacio compartido de CLIP. Embebe la
imagen y el texto con CLIP ViT-L/14, los normaliza y devuelve la similitud coseno
(clamp a 0-1). Valores tipicos: ~0.28-0.35 buen match, ~0.10-0.18 contenido distinto.
Impura: la inferencia necesita torch + open_clip, que NO estan en el venv del registry
(`python/.venv`). Por eso delega a un SUBPROCESO contra el python del venv de ComfyUI
(`~/ComfyUI/.venv/bin/python3`). Se calcula localmente con open_clip (no via el server
ComfyUI): es el camino mas simple y fiable, reusa el mismo CLIP que el scorer estetico
y no depende del estado del grafo del server. El parametro `server` queda reservado para
un backend futuro basado en los nodos CLIPVisionEncode/CLIPTextEncode (ver Gotchas).
"""
import json
import os
import subprocess
import sys
DEFAULT_VENV_PYTHON = "~/ComfyUI/.venv/bin/python3"
_MARKER = "__CLIP_RESULT__"
_INFER_SCRIPT = r'''
import json, sys
import torch
import open_clip
from PIL import Image
MARKER = "__CLIP_RESULT__"
def run(image_path, prompt, device):
if device == "auto":
device = "cuda" if torch.cuda.is_available() else "cpu"
model, _, preprocess = open_clip.create_model_and_transforms(
"ViT-L-14-quickgelu", pretrained="openai")
model = model.to(device).eval()
tokenizer = open_clip.get_tokenizer("ViT-L-14-quickgelu")
img = preprocess(Image.open(image_path).convert("RGB")).unsqueeze(0).to(device)
txt = tokenizer([prompt]).to(device)
with torch.no_grad():
imf = model.encode_image(img)
tf = model.encode_text(txt)
imf = imf / imf.norm(dim=-1, keepdim=True)
tf = tf / tf.norm(dim=-1, keepdim=True)
cos = float((imf @ tf.T).cpu().numpy()[0][0])
return max(0.0, min(1.0, cos))
if __name__ == "__main__":
image_path, prompt, device = sys.argv[1], sys.argv[2], sys.argv[3]
try:
score = run(image_path, prompt, device)
print(MARKER + json.dumps({"ok": True, "score_0_1": score, "error": ""}))
except Exception as exc:
print(MARKER + json.dumps({"ok": False, "score_0_1": 0.0, "error": repr(exc)}))
'''
def comfyui_score_clip_alignment(
image_path: str,
prompt: str,
*,
server: str = "127.0.0.1:8188",
venv_python: str = DEFAULT_VENV_PYTHON,
device: str = "auto",
timeout: float = 180.0,
) -> dict:
"""Mide la fidelidad prompt<->imagen como similitud coseno CLIP (0-1).
Args:
image_path: ruta a la imagen en disco local.
prompt: texto contra el que medir la alineacion (idealmente en ingles, como CLIP).
server: host:port del server ComfyUI. RESERVADO para un backend futuro; hoy el
calculo es local via open_clip (ver Gotchas). keyword-only.
venv_python: python del venv de ComfyUI con torch + open_clip; se expande `~`.
keyword-only.
device: "auto" (cuda si hay, si no cpu), "cuda" o "cpu". keyword-only.
timeout: timeout del subproceso en segundos. keyword-only.
Returns:
dict ``{ok, score_0_1, error}``. En exito ``ok=True`` y ``score_0_1`` es la
similitud coseno clamp a [0,1] (mayor = mas fiel al prompt). En error ``ok=False``,
``score_0_1=0.0`` y ``error`` describe la causa. Nunca lanza excepcion.
"""
img = os.path.expanduser(image_path)
if not os.path.isfile(img):
return {"ok": False, "score_0_1": 0.0, "error": f"imagen no encontrada: {img}"}
if not prompt or not prompt.strip():
return {"ok": False, "score_0_1": 0.0, "error": "prompt vacio"}
py = os.path.expanduser(venv_python)
if not os.path.isfile(py):
return {"ok": False, "score_0_1": 0.0,
"error": f"python del venv ComfyUI no encontrado: {py}"}
try:
proc = subprocess.run(
[py, "-c", _INFER_SCRIPT, img, prompt, device],
capture_output=True, text=True, timeout=timeout,
)
except subprocess.TimeoutExpired:
return {"ok": False, "score_0_1": 0.0,
"error": f"subproceso de inferencia excedio {timeout}s"}
for line in proc.stdout.splitlines():
if line.startswith(_MARKER):
try:
return json.loads(line[len(_MARKER):])
except json.JSONDecodeError as exc:
return {"ok": False, "score_0_1": 0.0,
"error": f"JSON invalido del subproceso: {exc}"}
tail = (proc.stderr or proc.stdout or "")[-400:]
return {"ok": False, "score_0_1": 0.0,
"error": f"sin resultado del subproceso (rc={proc.returncode}): {tail}"}
def _main(argv):
if len(argv) < 2:
sys.stderr.write('uso: comfyui_score_clip_alignment IMAGEN "prompt" [--device cpu|cuda]\n')
return 2
device = "auto"
image_path = argv[0]
prompt_parts = []
i = 1
while i < len(argv):
if argv[i] == "--device" and i + 1 < len(argv):
device = argv[i + 1]
i += 2
else:
prompt_parts.append(argv[i])
i += 1
res = comfyui_score_clip_alignment(image_path, " ".join(prompt_parts), device=device)
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:]))