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