Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 346f859b86 | |||
| 643ebfb849 | |||
| 537516e32e | |||
| ca07b25297 | |||
| fbbff7d5e7 | |||
| bdd841d9af |
File diff suppressed because one or more lines are too long
@@ -49,7 +49,9 @@ params:
|
||||
- name: filename_prefix
|
||||
desc: "Prefijo del archivo de salida en SaveImage. keyword-only."
|
||||
output: "dict en API format listo para comfyui_submit_workflow: img2img base (parte de input_image) con prompt de variante + ImageScale opcional (normaliza a size) + LoRA opcional. Nodos: CheckpointLoaderSimple '4', LoadImage '10', VAEEncode '11', CLIPTextEncode '6'/'7', KSampler '3' (denoise medio), VAEDecode '8', SaveImage '9', + ImageScale y LoraLoader si aplican."
|
||||
tested: false
|
||||
tested: true
|
||||
tests: ["estructura img2img (LoadImage+VAEEncode, sin EmptyLatentImage)", "input_image/prompt reflejados en LoadImage y CLIPTextEncode positivo", "size por defecto inserta ImageScale a 512; size=None lo omite", "denoise se clampa a [0,1]", "filename_prefix/seed/lora opcional reflejados", "input_image o variant vacios -> ValueError", "determinismo: misma entrada -> mismo dict"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_build_asset_variant_workflow.py"
|
||||
file_path: python/functions/ml/comfyui_build_asset_variant_workflow.py
|
||||
---
|
||||
|
||||
|
||||
@@ -44,7 +44,9 @@ params:
|
||||
- name: filename_prefix
|
||||
desc: "Prefijo del archivo de salida del SaveImage. keyword-only."
|
||||
output: "dict en API format listo para comfyui_submit_workflow (claves = node_ids string, valores = class_type + inputs). SV3D: ImageOnlyCheckpointLoader + LoadImage + SV3D_Conditioning + VideoLinearCFGGuidance + KSampler + VAEDecode + SaveImage (los N frames del orbit). Zero123: ImageOnlyCheckpointLoader + LoadImage + StableZero123_Conditioning_Batched + KSampler + VAEDecode + SaveImage (un batch de directions vistas). El frame i (i-esima imagen del SaveImage, azimuth creciente desde la frontal) = direccion i de directional_sprite_view_order(directions). El modulo expone ademas directional_sprite_view_order(directions) -> lista de nombres de direccion alineada por indice con los frames."
|
||||
tested: false
|
||||
tested: true
|
||||
tests: ["sv3d: estructura + orbit (video_frames=directions, size nativa 576)", "orbit_frames override", "zero123: StableZero123_Conditioning_Batched, azimuth equiespaciado, size 256", "cfg/ckpt por defecto segun modelo", "elevation/seed reflejados", "directional_sprite_view_order para 4/8/N", "errores: input vacio, model invalido, directions<1", "determinismo"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_build_directional_sprite_workflow.py"
|
||||
file_path: "python/functions/ml/comfyui_build_directional_sprite_workflow.py"
|
||||
---
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ params:
|
||||
- name: labels
|
||||
desc: "rotulos opcionales, uno por imagen (mismo orden); reservan una franja bajo cada celda."
|
||||
output: "dict con ok (bool), out_path (str, ruta del PNG generado), rows (int, filas), cols (int, columnas), error (str, vacio si OK)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
tested: true
|
||||
tests: ["grid basico: ok + out_path + cols/rows (ceil(sqrt(N)))", "cols explicito define filas", "cell define dimension del canvas", "labels reservan franja bajo cada celda", "error: lista vacia", "error: ruta inexistente", "determinismo del dict de salida"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_build_grid.py"
|
||||
file_path: "python/functions/ml/comfyui_build_grid.py"
|
||||
---
|
||||
|
||||
|
||||
@@ -57,7 +57,9 @@ params:
|
||||
- name: filename_prefix
|
||||
desc: "Prefijo del archivo de salida en SaveImage. keyword-only."
|
||||
output: "dict en API format listo para comfyui_submit_workflow: inpaint que repinta SOLO la region marcada en blanco por la mascara, conservando el resto del asset, con grow_mask para difuminar la costura, escalado consistente opcional (img+mask) y LoRA de estilo opcional. Nodos modo vae_encode: CheckpointLoaderSimple '4', LoadImage '10', LoadImageMask '12', VAEEncodeForInpaint '11', CLIPTextEncode '6'/'7', KSampler '3', VAEDecode '8', SaveImage '9' (+ ImageScale/ImageToMask si size, + LoraLoader si lora). Modo noise_mask sustituye VAEEncodeForInpaint por VAEEncode + SetLatentNoiseMask (+ GrowMask)."
|
||||
tested: false
|
||||
tested: true
|
||||
tests: ["estructura vae_encode (LoadImage+LoadImageMask+VAEEncodeForInpaint)", "prompt de region + grow_mask reflejados", "grow_mask se clampa a [0,64]", "mode noise_mask degrada a VAEEncode+SetLatentNoiseMask+GrowMask", "size inserta ImageScale a imagen y mascara + ImageToMask", "lora opcional + filename_prefix", "errores: input/mask/prompt vacios, mode invalido", "determinismo"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_build_inpaint_asset_workflow.py"
|
||||
file_path: python/functions/ml/comfyui_build_inpaint_asset_workflow.py
|
||||
---
|
||||
|
||||
|
||||
@@ -55,7 +55,9 @@ params:
|
||||
- name: filename_prefix
|
||||
desc: "Prefijo del archivo de salida en SaveImage. keyword-only."
|
||||
output: "dict en API format listo para comfyui_submit_workflow: outpaint que extiende el lienzo por los lados pedidos y genera lo nuevo con '{prompt}, {style}, seamless extension...', conservando el asset original. Nodos: CheckpointLoaderSimple '4', LoadImage '10', ImagePadForOutpaint (id nuevo, reusa el '12' que libera el LoadImageMask eliminado), VAEEncodeForInpaint '11' (pixels <- pad IMAGE, mask <- pad MASK), CLIPTextEncode '6'/'7', KSampler '3', VAEDecode '8', SaveImage '9' (+ LoraLoader si lora). El LoadImageMask de la base inpaint se elimina: la mascara la GENERA el pad."
|
||||
tested: false
|
||||
tested: true
|
||||
tests: ["estructura outpaint (ImagePadForOutpaint, sin LoadImageMask)", "pad cableado a VAEEncodeForInpaint (pixels<-IMAGE, mask<-MASK)", "extensiones redondeadas a multiplo de 8", "sin extension (todo 0 tras redondear) -> ValueError", "feather y prompt reflejados", "lora opcional + filename_prefix", "errores: input/prompt vacios", "determinismo"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_build_outpaint_asset_workflow.py"
|
||||
file_path: python/functions/ml/comfyui_build_outpaint_asset_workflow.py
|
||||
---
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@ params:
|
||||
- name: filename_prefix
|
||||
desc: "Prefijo del archivo de salida en SaveImage. keyword-only."
|
||||
output: "dict en API format listo para comfyui_submit_workflow: txt2img base (CheckpointLoaderSimple '4', EmptyLatentImage '5', CLIPTextEncode '6'/'7', KSampler '3' denoise 1.0, VAEDecode '8', SaveImage '9') + rama ControlNet (LoadImage del boceto -> [Preprocessor del control_type si preprocess] -> ControlNetApply -> KSampler.positive, con ControlNetLoader del modelo CN) + LoraLoader si lora. Es UN sprite; varios objetos del mismo set -> llamar por subject/sketch_image con el mismo style/checkpoint/(lora)."
|
||||
tested: false
|
||||
tested: true
|
||||
tests: ["estructura txt2img + ControlNet (EmptyLatentImage, ControlNetLoader/Apply)", "lineart: preprocesador + modelo por defecto, ControlNetApply consume el mapa de lineas", "canny: preprocesador + modelo", "preprocess=False pasa el boceto directo al ControlNet", "controlnet_name override + strength reflejado", "strength se clampa a [0,2]", "lora opcional", "errores: sketch/subject vacios, control_type invalido", "determinismo"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_build_sprite_from_sketch_workflow.py"
|
||||
file_path: python/functions/ml/comfyui_build_sprite_from_sketch_workflow.py
|
||||
---
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ params:
|
||||
- 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: ""
|
||||
tested: true
|
||||
tests: ["_extract_json: fence json", "_extract_json: brace plano", "_extract_json: sin objeto -> ValueError", "flujo: veredicto estructurado good", "verdict ambiguo -> bad conservador", "API caida -> ok=False", "respuesta no parseable -> ok=False"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_critique_image_llm.py"
|
||||
file_path: "python/functions/ml/comfyui_critique_image_llm.py"
|
||||
---
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ params:
|
||||
- name: nsfw
|
||||
desc: "Marca provenance.nsfw. keyword-only."
|
||||
output: "dict {ok, recipe, slug, has_workflow, error}. recipe sigue el schema minimo de comfyui_save_skill con provenance.source='civitai' y score_n=0. ok=False solo si no hay ni workflow embebido ni civitai_meta utilizable."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
tested: true
|
||||
tests: ["_slugify (normaliza y acota a 6 tokens)", "_loras_from_prompt", "_dims_from_prompt + _checkpoint_from_prompt", "_detect_base_workflow (flux/txt2img)", "_from_civitai_meta (mapea steps/cfg/size/modelo/prompts)", "flujo fallback a civitai_meta sin workflow embebido", "slug derivado del prompt", "error: sin workflow ni meta"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_extract_recipe_from_png.py"
|
||||
file_path: "python/functions/ml/comfyui_extract_recipe_from_png.py"
|
||||
---
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ params:
|
||||
- name: resample
|
||||
desc: "filtro de reescalado: 'lanczos' (por defecto), 'nearest', 'bilinear', 'bicubic', 'area'. String desconocido -> LANCZOS. keyword-only."
|
||||
output: "dict con ok (bool), out_path (str, ruta del PNG RGB; vacio si error), size ([w,h] final), error (str, vacio si OK)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
tested: true
|
||||
tests: ["aplana transparente sobre blanco -> RGB sin alpha", "color de fondo personalizado", "size redimensiona a cuadrado", "out_path por defecto con sufijo _flat", "error: imagen inexistente", "determinismo (mismos bytes de salida)"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_flatten_alpha_on_color.py"
|
||||
file_path: "python/functions/ml/comfyui_flatten_alpha_on_color.py"
|
||||
---
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ params:
|
||||
- name: timeout
|
||||
desc: "Timeout HTTP en segundos. keyword-only."
|
||||
output: "dict {ok, workflow, format_detected, error}. workflow = dict en API format; format_detected = 'api' (passthrough) o 'ui_graph' (convertido) o ''. Si falla la lectura/parse, ok=False y error explica."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
tested: true
|
||||
tests: ["API format se devuelve tal cual (format=api)", "UI graph se normaliza a API (descarta Note, resuelve conexiones)", "JSON invalido -> error", "formato no reconocido -> error", "JSON no es objeto -> error", "archivo inexistente -> error", "determinismo"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_import_workflow_json.py"
|
||||
file_path: "python/functions/ml/comfyui_import_workflow_json.py"
|
||||
---
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ name: comfyui_interrupt_queue
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_interrupt_queue(server: str = \"127.0.0.1:8188\") -> dict"
|
||||
description: "Corta la generacion en curso de ComfyUI (POST /interrupt) y devuelve el estado de la cola (GET /queue). Devuelve {ok, interrupted, queue_running, queue_pending, error}. NO lanza excepcion en fallo de red: degrada a {ok: False, error}. /interrupt corta solo el prompt en ejecucion, no vacia los pendientes. Impura: HTTP POST + GET, solo stdlib (urllib, json)."
|
||||
signature: "def comfyui_interrupt_queue(*, clear_pending: bool = False, server: str = \"127.0.0.1:8188\", timeout: float = 10.0) -> dict"
|
||||
description: "Corta la generacion en curso de ComfyUI (POST /interrupt) y, si clear_pending=True, vacia ademas la cola de pendientes (POST /queue {\"clear\":true}). Consulta GET /queue al final para reportar queue_remaining. Devuelve {ok, interrupted, cleared, queue_remaining, error}. NO lanza excepcion en fallo de red: degrada a {ok: False, error}. /interrupt corta solo el prompt en ejecucion, no vacia los pendientes salvo clear_pending. Impura: HTTP POST + GET, solo stdlib (urllib, json)."
|
||||
tags: [comfyui, ml, queue, interrupt, control, http]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
@@ -15,12 +15,16 @@ returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: clear_pending
|
||||
desc: "keyword-only. Si True, ademas de cortar el prompt en ejecucion vacia la cola de pendientes con POST /queue {\"clear\":true}. Default False."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema (default '127.0.0.1:8188')."
|
||||
output: "dict con ok (bool, True si interrupt + lectura de cola OK), interrupted (bool, True si POST /interrupt respondio), queue_running (int, prompts ejecutandose), queue_pending (int, prompts encolados), error (str, vacio si todo OK)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
desc: "keyword-only. host:port del servidor ComfyUI sin esquema (default '127.0.0.1:8188')."
|
||||
- name: timeout
|
||||
desc: "keyword-only. Timeout de cada peticion HTTP en segundos (default 10.0)."
|
||||
output: "dict con ok (bool, True si interrupt + clear (si se pidio) + lectura de cola OK), interrupted (bool, True si POST /interrupt respondio), cleared (bool, True si clear_pending y POST /queue {clear:true} respondio; False si no se pidio o fallo), queue_remaining (int, queue_running + queue_pending tras la operacion), error (str, vacio si todo OK)."
|
||||
tested: true
|
||||
tests: ["test_interrumpe_sin_vaciar", "test_clear_pending_vacia_cola", "test_clear_pending_cola_vacia_no_rompe", "test_servidor_caido_no_lanza"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_interrupt_queue.py"
|
||||
file_path: "python/functions/ml/comfyui_interrupt_queue.py"
|
||||
---
|
||||
|
||||
@@ -31,30 +35,47 @@ import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_interrupt_queue import comfyui_interrupt_queue
|
||||
|
||||
# Solo cortar el prompt en ejecucion (los pendientes siguen):
|
||||
res = comfyui_interrupt_queue()
|
||||
# {'ok': True, 'interrupted': True, 'queue_running': 0, 'queue_pending': 0, 'error': ''}
|
||||
if res["ok"] and res["interrupted"]:
|
||||
print(f"cortado; pendientes en cola: {res['queue_pending']}")
|
||||
# {'ok': True, 'interrupted': True, 'cleared': False, 'queue_remaining': 3, 'error': ''}
|
||||
|
||||
# Cortar el actual Y vaciar los pendientes de golpe:
|
||||
res = comfyui_interrupt_queue(clear_pending=True)
|
||||
# {'ok': True, 'interrupted': True, 'cleared': True, 'queue_remaining': 0, 'error': ''}
|
||||
if res["ok"]:
|
||||
print(f"cortado; quedan {res['queue_remaining']} en cola")
|
||||
```
|
||||
|
||||
O lanzable directo con: `./fn run comfyui_interrupt_queue`.
|
||||
O lanzable directo: `./fn run comfyui_interrupt_queue` · `./fn run comfyui_interrupt_queue --clear`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Para abortar una generacion que se esta tomando demasiado, que tira de mas VRAM de
|
||||
la prevista, o tras encolar por error un workflow pesado. Tambien para inspeccionar
|
||||
de un vistazo cuanto queda en cola (`queue_running` / `queue_pending`) sin parsear
|
||||
el JSON de /queue a mano. Es el freno de mano del round-trip build -> submit -> wait.
|
||||
la prevista, o tras encolar por error un workflow pesado. Con `clear_pending=True`
|
||||
es el freno de mano completo: corta el actual y borra todo lo encolado en una sola
|
||||
llamada (sin tener que encadenar `comfyui_queue_manage("clear")` despues). Tras la
|
||||
operacion `queue_remaining` dice de un vistazo cuanto queda en cola.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `/interrupt` corta SOLO el prompt en ejecucion; los pendientes (`queue_pending`)
|
||||
siguen y el siguiente arranca de inmediato. Para vaciar la cola entera hay que
|
||||
llamar `POST /queue` con `{"clear": true}` (no lo hace esta funcion — solo corta
|
||||
+ lee).
|
||||
- `/interrupt` corta SOLO el prompt en ejecucion; sin `clear_pending` los pendientes
|
||||
(`queue_pending`) siguen y el siguiente arranca de inmediato. Pasa
|
||||
`clear_pending=True` para vaciar tambien la cola (POST /queue {"clear": true}).
|
||||
- No es idempotente en el sentido de "sin efecto": si hay algo ejecutandose, lo
|
||||
mata. Si la cola esta vacia, el interrupt es inocuo (interrupted=True igual).
|
||||
mata. Si la cola esta vacia, tanto el interrupt como el clear son inocuos
|
||||
(`interrupted=True`/`cleared=True` igual, `queue_remaining=0`).
|
||||
- `queue_remaining` se lee al FINAL (GET /queue tras interrupt+clear): es
|
||||
`queue_running + queue_pending`. Justo tras un interrupt sin clear puede ser >0
|
||||
porque el siguiente pendiente ya arranco.
|
||||
- En fallo de red NO lanza: devuelve `ok=False` con el mensaje en `error`. Comprueba
|
||||
`ok` antes de fiarte de los conteos.
|
||||
`ok` antes de fiarte de `queue_remaining`.
|
||||
- Tras el interrupt conviene liberar VRAM con `POST /free` si vas a encolar otro
|
||||
trabajo pesado (esta funcion no lo hace).
|
||||
trabajo pesado (esta funcion no lo hace; ver el round-trip build -> submit -> wait).
|
||||
- Para operaciones de cola mas finas (borrar UN prompt por id, contar el historial)
|
||||
usa `comfyui_queue_manage`; esta funcion se centra en el interrupt + clear masivo.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-06-28) — anade flag `clear_pending` (vacia la cola en la misma
|
||||
llamada) + param `timeout`; el output pasa a {ok, interrupted, cleared,
|
||||
queue_remaining, error} y se anaden tests (mock HTTP local).
|
||||
|
||||
@@ -1,38 +1,52 @@
|
||||
"""Interrumpe la generacion en curso de ComfyUI y devuelve el estado de la cola.
|
||||
"""Interrumpe la generacion en curso de ComfyUI y, opcionalmente, vacia la cola.
|
||||
|
||||
Funcion impura: hace red (HTTP POST /interrupt + GET /queue). Solo stdlib.
|
||||
Funcion impura: hace red (HTTP POST /interrupt, POST /queue, GET /queue). Solo
|
||||
stdlib (urllib, json).
|
||||
|
||||
POST /interrupt corta el prompt que ComfyUI esta ejecutando ahora mismo (no vacia
|
||||
la cola: los prompts pendientes siguen). GET /queue devuelve queue_running (lo que
|
||||
se ejecuta) y queue_pending (lo encolado). Esta funcion combina ambos en un dict
|
||||
honesto que NO lanza excepcion en fallo de red: devuelve {ok: False, error}.
|
||||
POST /interrupt corta el prompt que ComfyUI esta ejecutando ahora mismo: NO vacia
|
||||
los pendientes, solo aborta el actual y el siguiente arranca de inmediato. Para
|
||||
vaciar de golpe los pendientes hay que ademas hacer POST /queue con {"clear": true}
|
||||
(lo que activa el flag clear_pending). GET /queue se consulta al final para reportar
|
||||
cuantos trabajos quedan en cola tras la operacion (queue_remaining).
|
||||
|
||||
NO lanza excepcion en fallo de red: devuelve un dict de estado {ok: False, error}.
|
||||
"""
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def comfyui_interrupt_queue(server: str = "127.0.0.1:8188") -> dict:
|
||||
"""Interrumpe la generacion en curso y devuelve el estado de la cola.
|
||||
def comfyui_interrupt_queue(
|
||||
clear_pending: bool = False,
|
||||
server: str = "127.0.0.1:8188",
|
||||
timeout: float = 10.0,
|
||||
) -> dict:
|
||||
"""Corta la generacion en curso de ComfyUI y devuelve el estado de la cola.
|
||||
|
||||
Args:
|
||||
clear_pending: si True, ademas de cortar el prompt en ejecucion vacia la
|
||||
cola de pendientes con POST /queue {"clear": true}. keyword-only.
|
||||
server: host:port del servidor ComfyUI sin esquema (default
|
||||
"127.0.0.1:8188").
|
||||
"127.0.0.1:8188"). keyword-only.
|
||||
timeout: timeout de cada peticion HTTP en segundos (default 10.0).
|
||||
keyword-only.
|
||||
|
||||
Returns:
|
||||
dict con:
|
||||
- ok (bool): True si tanto el interrupt como la lectura de la cola
|
||||
tuvieron exito.
|
||||
- ok (bool): True si el interrupt, la lectura de la cola y (si se pidio)
|
||||
el clear tuvieron exito.
|
||||
- interrupted (bool): True si el POST /interrupt respondio sin error.
|
||||
- queue_running (int): numero de prompts ejecutandose ahora mismo.
|
||||
- queue_pending (int): numero de prompts encolados pendientes.
|
||||
- cleared (bool): True si clear_pending era True y el POST /queue
|
||||
{"clear": true} respondio sin error; False si no se pidio o fallo.
|
||||
- queue_remaining (int): trabajos que quedan en cola tras la operacion
|
||||
(queue_running + queue_pending segun GET /queue al final).
|
||||
- error (str): mensaje de error si algo fallo; cadena vacia si todo OK.
|
||||
"""
|
||||
out = {
|
||||
"ok": False,
|
||||
"interrupted": False,
|
||||
"queue_running": 0,
|
||||
"queue_pending": 0,
|
||||
"cleared": False,
|
||||
"queue_remaining": 0,
|
||||
"error": "",
|
||||
}
|
||||
base = f"http://{server}"
|
||||
@@ -40,19 +54,37 @@ def comfyui_interrupt_queue(server: str = "127.0.0.1:8188") -> dict:
|
||||
# 1. POST /interrupt (cuerpo vacio): corta el prompt en ejecucion.
|
||||
try:
|
||||
req = urllib.request.Request(f"{base}/interrupt", data=b"", method="POST")
|
||||
with urllib.request.urlopen(req, timeout=10.0):
|
||||
with urllib.request.urlopen(req, timeout=timeout):
|
||||
out["interrupted"] = True
|
||||
except urllib.error.URLError as exc:
|
||||
reason = getattr(exc, "reason", exc)
|
||||
out["error"] = f"interrupt fallo: no se pudo conectar a {base}/interrupt: {reason}"
|
||||
return out
|
||||
|
||||
# 2. GET /queue: estado actual de la cola tras el interrupt.
|
||||
# 2. Opcional: POST /queue {"clear": true} para vaciar los pendientes.
|
||||
if clear_pending:
|
||||
try:
|
||||
payload = json.dumps({"clear": True}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{base}/queue",
|
||||
data=payload,
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=timeout):
|
||||
out["cleared"] = True
|
||||
except urllib.error.URLError as exc:
|
||||
reason = getattr(exc, "reason", exc)
|
||||
out["error"] = f"clear fallo: no se pudo conectar a {base}/queue: {reason}"
|
||||
return out
|
||||
|
||||
# 3. GET /queue: cuantos trabajos quedan en cola tras la operacion.
|
||||
try:
|
||||
with urllib.request.urlopen(f"{base}/queue", timeout=10.0) as resp:
|
||||
with urllib.request.urlopen(f"{base}/queue", timeout=timeout) as resp:
|
||||
data = json.loads(resp.read())
|
||||
out["queue_running"] = len(data.get("queue_running", []))
|
||||
out["queue_pending"] = len(data.get("queue_pending", []))
|
||||
running = len(data.get("queue_running", []))
|
||||
pending = len(data.get("queue_pending", []))
|
||||
out["queue_remaining"] = running + pending
|
||||
out["ok"] = True
|
||||
except urllib.error.URLError as exc:
|
||||
reason = getattr(exc, "reason", exc)
|
||||
@@ -63,9 +95,12 @@ def comfyui_interrupt_queue(server: str = "127.0.0.1:8188") -> dict:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
res = comfyui_interrupt_queue()
|
||||
import sys
|
||||
|
||||
clear = "--clear" in sys.argv[1:]
|
||||
res = comfyui_interrupt_queue(clear_pending=clear)
|
||||
print(
|
||||
f"ok={res['ok']} interrupted={res['interrupted']} "
|
||||
f"running={res['queue_running']} pending={res['queue_pending']} "
|
||||
f"cleared={res['cleared']} queue_remaining={res['queue_remaining']} "
|
||||
f"error={res['error']!r}"
|
||||
)
|
||||
|
||||
@@ -32,9 +32,9 @@ params:
|
||||
- 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: ""
|
||||
tested: true
|
||||
tests: ["tres votos good -> verdict good + score medio", "mayoria bad", "empate -> bad conservador", "juez caido se excluye sin crashear", "los tres jueces fallan -> ok=False", "weights afectan score pero no el voto"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_judge_image.py"
|
||||
file_path: "python/functions/ml/comfyui_judge_image.py"
|
||||
---
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ params:
|
||||
- name: png_path
|
||||
desc: "Ruta local del PNG generado por ComfyUI."
|
||||
output: "dict {ok, prompt, parameters, error}. prompt = workflow API format embebido (dict); parameters = {model, seed, steps, cfg, sampler_name, scheduler, denoise, positive, negative} extraido del KSampler y nodos conectados; error = motivo si ok=False."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
tested: true
|
||||
tests: ["extrae prompt embebido + parametros del KSampler (seed/steps/cfg/sampler/scheduler/denoise/positive/negative/model)", "error: archivo inexistente", "error: PNG sin chunk prompt", "error: chunk prompt no es JSON", "error: no es un PNG valido", "determinismo"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_read_png_metadata.py"
|
||||
file_path: "python/functions/ml/comfyui_read_png_metadata.py"
|
||||
---
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ params:
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema. Debe estar vivo para consultar /object_info."
|
||||
output: "dict {ok, missing_nodes, missing_models, suggestions, error}. ok = se pudo consultar el servidor; missing_nodes = class_type ausentes (nodos custom); missing_models = lista de {node, input, value}; suggestions = lista de {kind, name, action, hint, ...} (una por nodo/modelo faltante) con la funcion a usar; error = motivo si ok=False."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
tested: true
|
||||
tests: ["traduce nodos y modelos faltantes en suggestions (install_custom_node / search_and_download)", "sin faltantes -> suggestions vacio", "servidor caido -> ok=False con error propagado"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_resolve_workflow_deps.py"
|
||||
file_path: "python/functions/ml/comfyui_resolve_workflow_deps.py"
|
||||
---
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ params:
|
||||
- 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: ""
|
||||
tested: true
|
||||
tests: ["error: imagen inexistente (guard previo al subproceso)", "error: python del venv ComfyUI ausente", "error: .pth del modelo ausente", "nunca lanza excepcion + determinismo del error"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_score_aesthetic.py"
|
||||
file_path: "python/functions/ml/comfyui_score_aesthetic.py"
|
||||
---
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ params:
|
||||
- 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: ""
|
||||
tested: true
|
||||
tests: ["error: imagen inexistente", "error: prompt vacio", "error: python del venv ComfyUI ausente", "nunca lanza excepcion + determinismo del error"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_score_clip_alignment.py"
|
||||
file_path: "python/functions/ml/comfyui_score_clip_alignment.py"
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Tests de estructura/determinismo para comfyui_build_asset_variant_workflow (func pura, img2img)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_build_asset_variant_workflow import comfyui_build_asset_variant_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def _texts(wf):
|
||||
return [n["inputs"].get("text", "") for n in wf.values() if n["class_type"] == "CLIPTextEncode"]
|
||||
|
||||
|
||||
def test_estructura_img2img():
|
||||
# img2img: parte de una imagen (LoadImage + VAEEncode), NO de EmptyLatentImage.
|
||||
wf = comfyui_build_asset_variant_workflow("enemy.png", "ice element")
|
||||
assert_api_format(wf)
|
||||
cts = class_types(wf)
|
||||
for ct in ("CheckpointLoaderSimple", "LoadImage", "VAEEncode", "CLIPTextEncode",
|
||||
"KSampler", "VAEDecode", "SaveImage"):
|
||||
assert ct in cts, f"falta nodo {ct}"
|
||||
assert "EmptyLatentImage" not in cts # img2img no genera desde ruido
|
||||
|
||||
|
||||
def test_load_image_y_prompt_reflejados():
|
||||
wf = comfyui_build_asset_variant_workflow(" enemy_creature_00001_.png ", "fire element")
|
||||
# input_image se strippea y llega al LoadImage.
|
||||
assert node_by_ct(wf, "LoadImage")["inputs"]["image"] == "enemy_creature_00001_.png"
|
||||
# el positivo contiene la variante + el refuerzo de composicion.
|
||||
pos = [t for t in _texts(wf) if "same composition" in t]
|
||||
assert pos and "fire element" in pos[0]
|
||||
|
||||
|
||||
def test_size_default_inserta_imagescale():
|
||||
# size=512 por defecto -> normaliza la base con un ImageScale a 512x512.
|
||||
wf = comfyui_build_asset_variant_workflow("enemy.png", "golden tier 2")
|
||||
scale = node_by_ct(wf, "ImageScale")["inputs"]
|
||||
assert scale["width"] == 512 and scale["height"] == 512
|
||||
|
||||
|
||||
def test_size_none_sin_imagescale():
|
||||
wf = comfyui_build_asset_variant_workflow("enemy.png", "frozen", size=None)
|
||||
assert "ImageScale" not in class_types(wf)
|
||||
|
||||
|
||||
def test_denoise_se_clampa():
|
||||
assert node_by_ct(comfyui_build_asset_variant_workflow("e.png", "v", denoise=2.0),
|
||||
"KSampler")["inputs"]["denoise"] == 1.0
|
||||
assert node_by_ct(comfyui_build_asset_variant_workflow("e.png", "v", denoise=-1.0),
|
||||
"KSampler")["inputs"]["denoise"] == 0.0
|
||||
assert node_by_ct(comfyui_build_asset_variant_workflow("e.png", "v", denoise=0.5),
|
||||
"KSampler")["inputs"]["denoise"] == 0.5
|
||||
|
||||
|
||||
def test_filename_prefix_y_seed():
|
||||
wf = comfyui_build_asset_variant_workflow("e.png", "v", seed=123, filename_prefix="mio")
|
||||
assert node_by_ct(wf, "SaveImage")["inputs"]["filename_prefix"] == "mio"
|
||||
assert node_by_ct(wf, "KSampler")["inputs"]["seed"] == 123
|
||||
|
||||
|
||||
def test_lora_inyecta_loraloader():
|
||||
sin = comfyui_build_asset_variant_workflow("e.png", "v")
|
||||
con = comfyui_build_asset_variant_workflow("e.png", "v", lora="SD15_dark.safetensors")
|
||||
assert "LoraLoader" not in class_types(sin)
|
||||
assert "LoraLoader" in class_types(con)
|
||||
|
||||
|
||||
def test_input_image_vacio_lanza():
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_asset_variant_workflow(" ", "v")
|
||||
|
||||
|
||||
def test_variant_vacio_lanza():
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_asset_variant_workflow("e.png", "")
|
||||
|
||||
|
||||
def test_determinista():
|
||||
a = comfyui_build_asset_variant_workflow("e.png", "ice", seed=7, denoise=0.5)
|
||||
b = comfyui_build_asset_variant_workflow("e.png", "ice", seed=7, denoise=0.5)
|
||||
assert a == b
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Tests de estructura/determinismo para comfyui_build_directional_sprite_workflow (func pura, 2.5D)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_build_directional_sprite_workflow import (
|
||||
comfyui_build_directional_sprite_workflow,
|
||||
directional_sprite_view_order,
|
||||
)
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_sv3d_estructura_y_orbit_default():
|
||||
wf = comfyui_build_directional_sprite_workflow("goblin.png", directions=8, model="sv3d")
|
||||
assert_api_format(wf)
|
||||
cts = class_types(wf)
|
||||
for ct in ("LoadImage", "ImageOnlyCheckpointLoader", "SV3D_Conditioning",
|
||||
"VideoLinearCFGGuidance", "KSampler", "VAEDecode", "SaveImage"):
|
||||
assert ct in cts, f"falta nodo {ct}"
|
||||
cond = node_by_ct(wf, "SV3D_Conditioning")["inputs"]
|
||||
# video_frames default = directions; size nativa sv3d = 576.
|
||||
assert cond["video_frames"] == 8
|
||||
assert cond["width"] == 576 and cond["height"] == 576
|
||||
|
||||
|
||||
def test_sv3d_orbit_frames_override():
|
||||
wf = comfyui_build_directional_sprite_workflow("g.png", directions=8, orbit_frames=21)
|
||||
assert node_by_ct(wf, "SV3D_Conditioning")["inputs"]["video_frames"] == 21
|
||||
|
||||
|
||||
def test_zero123_estructura_y_azimuth():
|
||||
wf = comfyui_build_directional_sprite_workflow("g.png", directions=4, model="zero123")
|
||||
assert_api_format(wf)
|
||||
cts = class_types(wf)
|
||||
assert "StableZero123_Conditioning_Batched" in cts
|
||||
assert "SV3D_Conditioning" not in cts # camino distinto al sv3d
|
||||
cond = node_by_ct(wf, "StableZero123_Conditioning_Batched")["inputs"]
|
||||
# batch = directions; size nativa zero123 = 256; azimuth equiespaciado 360/N.
|
||||
assert cond["batch_size"] == 4
|
||||
assert cond["width"] == 256 and cond["height"] == 256
|
||||
assert cond["azimuth_batch_increment"] == 90.0
|
||||
|
||||
|
||||
def test_cfg_y_ckpt_default_por_modelo():
|
||||
sv3d = comfyui_build_directional_sprite_workflow("g.png", model="sv3d")
|
||||
z123 = comfyui_build_directional_sprite_workflow("g.png", model="zero123")
|
||||
assert node_by_ct(sv3d, "KSampler")["inputs"]["cfg"] == 2.5
|
||||
assert node_by_ct(z123, "KSampler")["inputs"]["cfg"] == 4.0
|
||||
assert node_by_ct(sv3d, "ImageOnlyCheckpointLoader")["inputs"]["ckpt_name"] == "3D_sv3d_p.safetensors"
|
||||
assert node_by_ct(z123, "ImageOnlyCheckpointLoader")["inputs"]["ckpt_name"] == "3D_stable_zero123.ckpt"
|
||||
|
||||
|
||||
def test_elevation_y_seed_reflejados():
|
||||
wf = comfyui_build_directional_sprite_workflow("g.png", model="sv3d", elevation=15.0, seed=42)
|
||||
assert node_by_ct(wf, "SV3D_Conditioning")["inputs"]["elevation"] == 15.0
|
||||
assert node_by_ct(wf, "KSampler")["inputs"]["seed"] == 42
|
||||
|
||||
|
||||
def test_view_order_helper():
|
||||
assert directional_sprite_view_order(8) == ["S", "SE", "E", "NE", "N", "NW", "W", "SW"]
|
||||
assert directional_sprite_view_order(4) == ["S", "E", "N", "W"]
|
||||
# N no canonico -> etiquetas por azimuth.
|
||||
assert directional_sprite_view_order(6) == ["az0", "az60", "az120", "az180", "az240", "az300"]
|
||||
|
||||
|
||||
def test_errores():
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_directional_sprite_workflow("")
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_directional_sprite_workflow("g.png", model="turbo")
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_directional_sprite_workflow("g.png", directions=0)
|
||||
|
||||
|
||||
def test_determinista():
|
||||
a = comfyui_build_directional_sprite_workflow("g.png", directions=8, seed=7, elevation=15.0)
|
||||
b = comfyui_build_directional_sprite_workflow("g.png", directions=8, seed=7, elevation=15.0)
|
||||
assert a == b
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Tests offline para comfyui_build_grid (impura PIL: lee N imagenes -> PNG grid).
|
||||
|
||||
Sin red, sin GPU, sin servidor: crea PNGs reales en un tmp_path y monta el grid.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_build_grid import comfyui_build_grid
|
||||
|
||||
PIL = pytest.importorskip("PIL")
|
||||
from PIL import Image # noqa: E402
|
||||
|
||||
|
||||
def _png(path, size=(64, 64), color=(120, 30, 30)):
|
||||
Image.new("RGB", size, color).save(path)
|
||||
return str(path)
|
||||
|
||||
|
||||
def test_grid_basico(tmp_path):
|
||||
paths = [_png(tmp_path / f"i{i}.png") for i in range(4)]
|
||||
out = tmp_path / "grid.png"
|
||||
res = comfyui_build_grid(paths, out_path=str(out))
|
||||
assert res["ok"] is True
|
||||
assert res["error"] == ""
|
||||
assert os.path.isfile(res["out_path"]) and res["out_path"] == str(out)
|
||||
# 4 imagenes -> ceil(sqrt(4)) = 2 columnas, 2 filas.
|
||||
assert res["cols"] == 2 and res["rows"] == 2
|
||||
|
||||
|
||||
def test_cols_explicito_y_filas(tmp_path):
|
||||
paths = [_png(tmp_path / f"i{i}.png") for i in range(5)]
|
||||
res = comfyui_build_grid(paths, cols=5, out_path=str(tmp_path / "g.png"))
|
||||
assert res["cols"] == 5 and res["rows"] == 1
|
||||
|
||||
|
||||
def test_cell_define_dimension_del_canvas(tmp_path):
|
||||
paths = [_png(tmp_path / f"i{i}.png") for i in range(2)]
|
||||
res = comfyui_build_grid(paths, cols=2, cell=128, out_path=str(tmp_path / "g.png"))
|
||||
with Image.open(res["out_path"]) as im:
|
||||
# 2 columnas x 128 cell = 256 ancho; 1 fila x 128 = 128 alto.
|
||||
assert im.size == (256, 128)
|
||||
|
||||
|
||||
def test_labels_reservan_franja(tmp_path):
|
||||
paths = [_png(tmp_path / f"i{i}.png") for i in range(2)]
|
||||
res = comfyui_build_grid(paths, cols=2, cell=64, labels=["a", "b"],
|
||||
out_path=str(tmp_path / "g.png"))
|
||||
with Image.open(res["out_path"]) as im:
|
||||
# con labels se reservan 22px bajo cada celda: alto = 64 + 22.
|
||||
assert im.size == (128, 86)
|
||||
|
||||
|
||||
def test_error_lista_vacia():
|
||||
res = comfyui_build_grid([])
|
||||
assert res["ok"] is False and "vacio" in res["error"]
|
||||
|
||||
|
||||
def test_error_ruta_inexistente(tmp_path):
|
||||
res = comfyui_build_grid([str(tmp_path / "no_existe.png")])
|
||||
assert res["ok"] is False and "no existen" in res["error"]
|
||||
|
||||
|
||||
def test_determinista_mismo_dict(tmp_path):
|
||||
paths = [_png(tmp_path / f"i{i}.png") for i in range(3)]
|
||||
a = comfyui_build_grid(paths, out_path=str(tmp_path / "a.png"))
|
||||
b = comfyui_build_grid(paths, out_path=str(tmp_path / "b.png"))
|
||||
# rows/cols/ok son determableinistas para las mismas entradas.
|
||||
assert (a["ok"], a["rows"], a["cols"]) == (b["ok"], b["rows"], b["cols"])
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Tests de estructura/determinismo para comfyui_build_inpaint_asset_workflow (func pura, inpaint)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_build_inpaint_asset_workflow import comfyui_build_inpaint_asset_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def _texts(wf):
|
||||
return [n["inputs"].get("text", "") for n in wf.values() if n["class_type"] == "CLIPTextEncode"]
|
||||
|
||||
|
||||
def test_estructura_vae_encode():
|
||||
wf = comfyui_build_inpaint_asset_workflow("asset.png", "mask.png", "a golden sword")
|
||||
assert_api_format(wf)
|
||||
cts = class_types(wf)
|
||||
for ct in ("CheckpointLoaderSimple", "LoadImage", "LoadImageMask",
|
||||
"VAEEncodeForInpaint", "CLIPTextEncode", "KSampler", "VAEDecode", "SaveImage"):
|
||||
assert ct in cts, f"falta nodo {ct}"
|
||||
|
||||
|
||||
def test_prompt_region_y_grow_mask():
|
||||
wf = comfyui_build_inpaint_asset_workflow("a.png", "m.png", "blue shield", grow_mask=8)
|
||||
pos = [t for t in _texts(wf) if "seamless blend" in t]
|
||||
assert pos and "blue shield" in pos[0]
|
||||
assert node_by_ct(wf, "VAEEncodeForInpaint")["inputs"]["grow_mask_by"] == 8
|
||||
|
||||
|
||||
def test_grow_mask_se_clampa():
|
||||
wf = comfyui_build_inpaint_asset_workflow("a.png", "m.png", "p", grow_mask=999)
|
||||
assert node_by_ct(wf, "VAEEncodeForInpaint")["inputs"]["grow_mask_by"] == 64
|
||||
|
||||
|
||||
def test_modo_noise_mask_degrada():
|
||||
# noise_mask reemplaza VAEEncodeForInpaint por VAEEncode + SetLatentNoiseMask (+ GrowMask).
|
||||
wf = comfyui_build_inpaint_asset_workflow("a.png", "m.png", "p", mode="noise_mask", grow_mask=6)
|
||||
cts = class_types(wf)
|
||||
assert "VAEEncodeForInpaint" not in cts
|
||||
assert "VAEEncode" in cts and "SetLatentNoiseMask" in cts and "GrowMask" in cts
|
||||
|
||||
|
||||
def test_size_inserta_imagescale_a_imagen_y_mascara():
|
||||
# size en modo vae_encode escala imagen Y mascara de forma consistente.
|
||||
wf = comfyui_build_inpaint_asset_workflow("a.png", "m.png", "p", size=768)
|
||||
scales = [n for n in wf.values() if n["class_type"] == "ImageScale"]
|
||||
assert len(scales) == 2 # una para la imagen, otra para la mascara
|
||||
assert all(s["inputs"]["width"] == 768 and s["inputs"]["height"] == 768 for s in scales)
|
||||
assert "ImageToMask" in class_types(wf)
|
||||
|
||||
|
||||
def test_lora_y_filename():
|
||||
wf = comfyui_build_inpaint_asset_workflow("a.png", "m.png", "p", lora="x.safetensors",
|
||||
filename_prefix="mio")
|
||||
assert "LoraLoader" in class_types(wf)
|
||||
assert node_by_ct(wf, "SaveImage")["inputs"]["filename_prefix"] == "mio"
|
||||
|
||||
|
||||
def test_errores():
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_inpaint_asset_workflow("", "m.png", "p")
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_inpaint_asset_workflow("a.png", "", "p")
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_inpaint_asset_workflow("a.png", "m.png", "")
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_inpaint_asset_workflow("a.png", "m.png", "p", mode="otro")
|
||||
|
||||
|
||||
def test_determinista():
|
||||
a = comfyui_build_inpaint_asset_workflow("a.png", "m.png", "orb", seed=7, grow_mask=6)
|
||||
b = comfyui_build_inpaint_asset_workflow("a.png", "m.png", "orb", seed=7, grow_mask=6)
|
||||
assert a == b
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Tests de estructura/determinismo para comfyui_build_outpaint_asset_workflow (func pura, outpaint)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_build_outpaint_asset_workflow import comfyui_build_outpaint_asset_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_estructura_outpaint():
|
||||
wf = comfyui_build_outpaint_asset_workflow("bg.png", "more forest", right=256)
|
||||
assert_api_format(wf)
|
||||
cts = class_types(wf)
|
||||
for ct in ("CheckpointLoaderSimple", "LoadImage", "ImagePadForOutpaint",
|
||||
"VAEEncodeForInpaint", "CLIPTextEncode", "KSampler", "VAEDecode", "SaveImage"):
|
||||
assert ct in cts, f"falta nodo {ct}"
|
||||
# outpaint genera su mascara con el pad: NO usa LoadImageMask.
|
||||
assert "LoadImageMask" not in cts
|
||||
|
||||
|
||||
def test_pad_cableado_a_vaeencode():
|
||||
# VAEEncodeForInpaint toma pixels de la IMAGE del pad y mask de la MASK del pad.
|
||||
wf = comfyui_build_outpaint_asset_workflow("bg.png", "sky", top=128)
|
||||
pad_id = next(nid for nid, n in wf.items() if n["class_type"] == "ImagePadForOutpaint")
|
||||
enc = node_by_ct(wf, "VAEEncodeForInpaint")["inputs"]
|
||||
assert enc["pixels"] == [pad_id, 0]
|
||||
assert enc["mask"] == [pad_id, 1]
|
||||
|
||||
|
||||
def test_extensiones_redondeadas_a_8():
|
||||
# _round8 normaliza al multiplo de 8 mas cercano.
|
||||
wf = comfyui_build_outpaint_asset_workflow("bg.png", "p", right=10)
|
||||
pad = node_by_ct(wf, "ImagePadForOutpaint")["inputs"]
|
||||
assert pad["right"] == 8 and pad["left"] == 0 and pad["top"] == 0 and pad["bottom"] == 0
|
||||
|
||||
|
||||
def test_sin_extension_lanza():
|
||||
# las cuatro extensiones a 0 (tras redondear) -> no hay nada que extender.
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_outpaint_asset_workflow("bg.png", "p", left=3, right=2)
|
||||
|
||||
|
||||
def test_feather_y_prompt():
|
||||
wf = comfyui_build_outpaint_asset_workflow("bg.png", "open sky", top=64, feather=30)
|
||||
assert node_by_ct(wf, "ImagePadForOutpaint")["inputs"]["feathering"] == 30
|
||||
pos = [n["inputs"]["text"] for n in wf.values()
|
||||
if n["class_type"] == "CLIPTextEncode" and "seamless extension" in n["inputs"].get("text", "")]
|
||||
assert pos and "open sky" in pos[0]
|
||||
|
||||
|
||||
def test_lora_y_filename():
|
||||
wf = comfyui_build_outpaint_asset_workflow("bg.png", "p", right=64, lora="x.safetensors",
|
||||
filename_prefix="mio")
|
||||
assert "LoraLoader" in class_types(wf)
|
||||
assert node_by_ct(wf, "SaveImage")["inputs"]["filename_prefix"] == "mio"
|
||||
|
||||
|
||||
def test_errores_vacios():
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_outpaint_asset_workflow("", "p", right=64)
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_outpaint_asset_workflow("bg.png", "", right=64)
|
||||
|
||||
|
||||
def test_determinista():
|
||||
a = comfyui_build_outpaint_asset_workflow("bg.png", "forest", right=256, seed=7)
|
||||
b = comfyui_build_outpaint_asset_workflow("bg.png", "forest", right=256, seed=7)
|
||||
assert a == b
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Tests de estructura/determinismo para comfyui_build_sprite_from_sketch_workflow (func pura, ControlNet)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_build_sprite_from_sketch_workflow import comfyui_build_sprite_from_sketch_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_estructura_txt2img_mas_controlnet():
|
||||
# txt2img (EmptyLatentImage, denoise alto) guiado por ControlNet atado al boceto.
|
||||
wf = comfyui_build_sprite_from_sketch_workflow("sketch.png", "armored knight")
|
||||
assert_api_format(wf)
|
||||
cts = class_types(wf)
|
||||
for ct in ("CheckpointLoaderSimple", "EmptyLatentImage", "CLIPTextEncode", "KSampler",
|
||||
"VAEDecode", "SaveImage", "LoadImage", "ControlNetLoader", "ControlNetApply"):
|
||||
assert ct in cts, f"falta nodo {ct}"
|
||||
|
||||
|
||||
def test_lineart_default_preprocesador_y_modelo():
|
||||
wf = comfyui_build_sprite_from_sketch_workflow("s.png", "knight", control_type="lineart")
|
||||
assert "LineArtPreprocessor" in class_types(wf)
|
||||
assert node_by_ct(wf, "ControlNetLoader")["inputs"]["control_net_name"] == \
|
||||
"control_v11p_sd15_lineart_fp16.safetensors"
|
||||
# el ControlNetApply consume el mapa de lineas del preprocesador, no el LoadImage directo.
|
||||
pre_id = next(nid for nid, n in wf.items() if n["class_type"].endswith("Preprocessor"))
|
||||
assert node_by_ct(wf, "ControlNetApply")["inputs"]["image"] == [pre_id, 0]
|
||||
|
||||
|
||||
def test_canny_preprocesador_y_modelo():
|
||||
wf = comfyui_build_sprite_from_sketch_workflow("s.png", "chest", control_type="canny")
|
||||
assert "CannyEdgePreprocessor" in class_types(wf)
|
||||
assert node_by_ct(wf, "ControlNetLoader")["inputs"]["control_net_name"] == \
|
||||
"control_v11p_sd15_canny_fp16.safetensors"
|
||||
|
||||
|
||||
def test_preprocess_false_pasa_boceto_directo():
|
||||
wf = comfyui_build_sprite_from_sketch_workflow("s.png", "k", preprocess=False)
|
||||
assert not any(n["class_type"].endswith("Preprocessor") for n in wf.values())
|
||||
load_id = next(nid for nid, n in wf.items() if n["class_type"] == "LoadImage")
|
||||
assert node_by_ct(wf, "ControlNetApply")["inputs"]["image"] == [load_id, 0]
|
||||
|
||||
|
||||
def test_controlnet_name_override_y_strength():
|
||||
wf = comfyui_build_sprite_from_sketch_workflow(
|
||||
"s.png", "k", control_type="lineart",
|
||||
controlnet_name="control_v11p_sd15_canny_fp16.safetensors", strength=0.65)
|
||||
assert node_by_ct(wf, "ControlNetLoader")["inputs"]["control_net_name"] == \
|
||||
"control_v11p_sd15_canny_fp16.safetensors"
|
||||
assert node_by_ct(wf, "ControlNetApply")["inputs"]["strength"] == 0.65
|
||||
|
||||
|
||||
def test_strength_se_clampa():
|
||||
wf = comfyui_build_sprite_from_sketch_workflow("s.png", "k", strength=5.0)
|
||||
assert node_by_ct(wf, "ControlNetApply")["inputs"]["strength"] == 2.0
|
||||
|
||||
|
||||
def test_lora_inyecta():
|
||||
assert "LoraLoader" in class_types(
|
||||
comfyui_build_sprite_from_sketch_workflow("s.png", "k", lora="x.safetensors"))
|
||||
|
||||
|
||||
def test_errores():
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_sprite_from_sketch_workflow("", "k")
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_sprite_from_sketch_workflow("s.png", "")
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_sprite_from_sketch_workflow("s.png", "k", control_type="depth")
|
||||
|
||||
|
||||
def test_determinista():
|
||||
a = comfyui_build_sprite_from_sketch_workflow("s.png", "knight", seed=7, strength=0.8)
|
||||
b = comfyui_build_sprite_from_sketch_workflow("s.png", "knight", seed=7, strength=0.8)
|
||||
assert a == b
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Tests offline para comfyui_critique_image_llm (impura: critica LLM-vision via ask_llm_vision).
|
||||
|
||||
Sin red, sin API: prueba el parser de JSON puro (_extract_json) y el flujo con ask_llm_vision
|
||||
monkeypatcheado (veredicto estructurado, ambiguo->bad conservador, API caida, texto no parseable).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
import ml.comfyui_critique_image_llm as mod
|
||||
from ml.comfyui_critique_image_llm import comfyui_critique_image_llm, _extract_json
|
||||
|
||||
|
||||
def test_extract_json_fenced():
|
||||
txt = 'blah\n```json\n{"verdict": "good", "score": 8}\n```\nfin'
|
||||
assert _extract_json(txt) == {"verdict": "good", "score": 8}
|
||||
|
||||
|
||||
def test_extract_json_brace_plano():
|
||||
assert _extract_json(' {"verdict": "bad", "score": 2} ') == {"verdict": "bad", "score": 2}
|
||||
|
||||
|
||||
def test_extract_json_sin_objeto_lanza():
|
||||
with pytest.raises(ValueError):
|
||||
_extract_json("no hay json aqui")
|
||||
|
||||
|
||||
def _fake_vision(text, ok=True):
|
||||
return lambda user_prompt, image_path, **kw: {"ok": ok, "text": text, "error": "" if ok else "429"}
|
||||
|
||||
|
||||
def test_flujo_veredicto_estructurado(monkeypatch):
|
||||
monkeypatch.setattr(mod, "ask_llm_vision",
|
||||
_fake_vision('{"verdict": "good", "score": 8.5, "reasons": ["nitida"]}'))
|
||||
res = comfyui_critique_image_llm("i.png", "a cat")
|
||||
assert res["ok"] is True
|
||||
assert res["verdict"] == "good" and res["score_0_10"] == 8.5
|
||||
assert res["reasons"] == ["nitida"]
|
||||
|
||||
|
||||
def test_verdict_ambiguo_cae_a_bad(monkeypatch):
|
||||
monkeypatch.setattr(mod, "ask_llm_vision",
|
||||
_fake_vision('{"verdict": "maybe", "score": 5}'))
|
||||
res = comfyui_critique_image_llm("i.png", "p")
|
||||
assert res["ok"] is True and res["verdict"] == "bad" # conservador ante ambiguo
|
||||
|
||||
|
||||
def test_api_caida_ok_false(monkeypatch):
|
||||
monkeypatch.setattr(mod, "ask_llm_vision", _fake_vision("", ok=False))
|
||||
res = comfyui_critique_image_llm("i.png", "p")
|
||||
assert res["ok"] is False and res["error"]
|
||||
|
||||
|
||||
def test_respuesta_no_parseable_ok_false(monkeypatch):
|
||||
monkeypatch.setattr(mod, "ask_llm_vision", _fake_vision("lo siento, no puedo"))
|
||||
res = comfyui_critique_image_llm("i.png", "p")
|
||||
assert res["ok"] is False and "no parseable" in res["error"]
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Tests offline para comfyui_extract_recipe_from_png (impura: destila PNG -> receta de skill).
|
||||
|
||||
Sin red, sin servidor: prueba los helpers puros de extraccion y el flujo de degradacion a la
|
||||
`meta` de Civitai cuando el PNG no trae workflow embebido (PNG inexistente -> sin workflow).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_extract_recipe_from_png import (
|
||||
comfyui_extract_recipe_from_png,
|
||||
_slugify,
|
||||
_loras_from_prompt,
|
||||
_dims_from_prompt,
|
||||
_checkpoint_from_prompt,
|
||||
_detect_base_workflow,
|
||||
_from_civitai_meta,
|
||||
)
|
||||
|
||||
|
||||
def test_slugify():
|
||||
assert _slugify("A Red Apple!", "fb") == "a_red_apple"
|
||||
assert _slugify("", "fallback") == "fallback"
|
||||
# acota a 6 tokens.
|
||||
assert _slugify("one two three four five six seven eight", "fb").count("_") == 5
|
||||
|
||||
|
||||
def test_loras_from_prompt():
|
||||
prompt = {"7": {"class_type": "LoraLoader",
|
||||
"inputs": {"lora_name": "style.safetensors",
|
||||
"strength_model": 0.8, "strength_clip": 0.7}}}
|
||||
loras = _loras_from_prompt(prompt)
|
||||
assert loras == [{"name": "style.safetensors", "strength_model": 0.8, "strength_clip": 0.7}]
|
||||
assert _loras_from_prompt({}) == []
|
||||
|
||||
|
||||
def test_dims_y_checkpoint_from_prompt():
|
||||
prompt = {
|
||||
"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "dream.safetensors"}},
|
||||
"5": {"class_type": "EmptyLatentImage", "inputs": {"width": 832, "height": 1216}},
|
||||
}
|
||||
assert _dims_from_prompt(prompt) == {"width": 832, "height": 1216}
|
||||
assert _checkpoint_from_prompt(prompt) == "dream.safetensors"
|
||||
|
||||
|
||||
def test_detect_base_workflow():
|
||||
assert _detect_base_workflow({"1": {"class_type": "UNETLoader", "inputs": {}}}) == "flux"
|
||||
assert _detect_base_workflow({"1": {"class_type": "CheckpointLoaderSimple", "inputs": {}}}) == "txt2img"
|
||||
|
||||
|
||||
def test_from_civitai_meta():
|
||||
meta = {"steps": 25, "sampler": "Euler a", "Size": "832x1216", "seed": 7,
|
||||
"cfgScale": 6.5, "Model": "mymodel", "prompt": "a cat", "negativePrompt": "blurry"}
|
||||
out = _from_civitai_meta(meta)
|
||||
assert out["checkpoint"] == "mymodel"
|
||||
assert out["positive"] == "a cat" and out["negative"] == "blurry"
|
||||
assert out["params"]["steps"] == 25 and out["params"]["cfg"] == 6.5
|
||||
assert out["params"]["width"] == 832 and out["params"]["height"] == 1216
|
||||
|
||||
|
||||
def test_flujo_fallback_civitai_meta(tmp_path):
|
||||
# PNG inexistente -> sin workflow embebido; cae a la meta de Civitai (utilizable).
|
||||
res = comfyui_extract_recipe_from_png(
|
||||
str(tmp_path / "no.png"),
|
||||
civitai_meta={"prompt": "a knight", "Model": "dream.safetensors", "steps": 20})
|
||||
assert res["ok"] is True
|
||||
assert res["has_workflow"] is False
|
||||
recipe = res["recipe"]
|
||||
assert recipe["checkpoint"] == "dream.safetensors"
|
||||
assert recipe["prompt_scaffold"]["positive"] == "a knight"
|
||||
assert recipe["provenance"]["source"] == "civitai" and recipe["score_n"] == 0
|
||||
|
||||
|
||||
def test_slug_derivado_del_prompt(tmp_path):
|
||||
res = comfyui_extract_recipe_from_png(
|
||||
str(tmp_path / "no.png"), civitai_meta={"prompt": "Fire Goblin Warrior"})
|
||||
assert res["ok"] is True and res["slug"] == "fire_goblin_warrior"
|
||||
|
||||
|
||||
def test_error_sin_workflow_ni_meta(tmp_path):
|
||||
res = comfyui_extract_recipe_from_png(str(tmp_path / "no.png"))
|
||||
assert res["ok"] is False and res["recipe"] == {}
|
||||
assert "no trae workflow" in res["error"]
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Tests offline para comfyui_flatten_alpha_on_color (impura PIL: aplana RGBA sobre fondo solido).
|
||||
|
||||
Sin red, sin GPU, sin servidor: crea un PNG RGBA real y verifica el RGB resultante.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_flatten_alpha_on_color import comfyui_flatten_alpha_on_color
|
||||
|
||||
PIL = pytest.importorskip("PIL")
|
||||
from PIL import Image # noqa: E402
|
||||
|
||||
|
||||
def _rgba(path, size=(32, 32), color=(0, 0, 0, 0)):
|
||||
Image.new("RGBA", size, color).save(path)
|
||||
return str(path)
|
||||
|
||||
|
||||
def test_aplana_transparente_sobre_blanco(tmp_path):
|
||||
src = _rgba(tmp_path / "sprite.png", color=(0, 0, 0, 0)) # totalmente transparente
|
||||
out = tmp_path / "flat.png"
|
||||
res = comfyui_flatten_alpha_on_color(src, out_path=str(out), color=(255, 255, 255))
|
||||
assert res["ok"] is True and res["error"] == ""
|
||||
with Image.open(res["out_path"]) as im:
|
||||
assert im.mode == "RGB" # sin alpha
|
||||
# sobre alpha 0 queda el fondo solido: blanco.
|
||||
assert im.getpixel((0, 0)) == (255, 255, 255)
|
||||
|
||||
|
||||
def test_color_de_fondo_personalizado(tmp_path):
|
||||
src = _rgba(tmp_path / "s.png", color=(0, 0, 0, 0))
|
||||
res = comfyui_flatten_alpha_on_color(src, out_path=str(tmp_path / "o.png"), color=(10, 20, 30))
|
||||
with Image.open(res["out_path"]) as im:
|
||||
assert im.getpixel((0, 0)) == (10, 20, 30)
|
||||
|
||||
|
||||
def test_size_redimensiona_cuadrado(tmp_path):
|
||||
src = _rgba(tmp_path / "s.png", size=(32, 16))
|
||||
res = comfyui_flatten_alpha_on_color(src, out_path=str(tmp_path / "o.png"), size=64)
|
||||
assert res["size"] == [64, 64]
|
||||
with Image.open(res["out_path"]) as im:
|
||||
assert im.size == (64, 64)
|
||||
|
||||
|
||||
def test_out_path_default_sufijo_flat(tmp_path):
|
||||
src = _rgba(tmp_path / "sprite.png")
|
||||
res = comfyui_flatten_alpha_on_color(src) # out_path None -> <base>_flat.png
|
||||
assert res["ok"] is True
|
||||
assert res["out_path"].endswith("sprite_flat.png")
|
||||
|
||||
|
||||
def test_error_imagen_inexistente(tmp_path):
|
||||
res = comfyui_flatten_alpha_on_color(str(tmp_path / "no.png"))
|
||||
assert res["ok"] is False and "no existe" in res["error"]
|
||||
|
||||
|
||||
def test_determinista(tmp_path):
|
||||
src = _rgba(tmp_path / "s.png", color=(5, 5, 5, 128))
|
||||
a = comfyui_flatten_alpha_on_color(src, out_path=str(tmp_path / "a.png"), color=(200, 0, 0))
|
||||
b = comfyui_flatten_alpha_on_color(src, out_path=str(tmp_path / "b.png"), color=(200, 0, 0))
|
||||
with Image.open(a["out_path"]) as ia, Image.open(b["out_path"]) as ib:
|
||||
assert ia.tobytes() == ib.tobytes()
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Tests offline para comfyui_import_workflow_json (impura: lee disco/URL + normaliza a API format).
|
||||
|
||||
Sin red, sin servidor: lee workflows desde archivos locales. Para el caso UI graph monkeypatchea
|
||||
comfyui_object_info (devuelve None) para no consultar el servidor; se valida la resolucion de
|
||||
conexiones y el descarte de nodos virtuales (Note).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
import ml.comfyui_import_workflow_json as mod
|
||||
from ml.comfyui_import_workflow_json import comfyui_import_workflow_json
|
||||
from _comfyui_wf_assert import assert_api_format, class_types
|
||||
|
||||
|
||||
_API = {
|
||||
"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "m.safetensors"}},
|
||||
"2": {"class_type": "VAEDecode", "inputs": {"samples": ["1", 0], "vae": ["1", 2]}},
|
||||
}
|
||||
|
||||
_UI_GRAPH = {
|
||||
"nodes": [
|
||||
{"id": 1, "type": "CheckpointLoaderSimple", "inputs": [], "widgets_values": ["m.safetensors"]},
|
||||
{"id": 2, "type": "Note", "inputs": []},
|
||||
{"id": 3, "type": "VAEDecode",
|
||||
"inputs": [{"name": "samples", "link": 10}, {"name": "vae", "link": 11}]},
|
||||
],
|
||||
"links": [
|
||||
[10, 1, 0, 3, 0, "LATENT"],
|
||||
[11, 1, 2, 3, 1, "VAE"],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _write(tmp_path, name, obj):
|
||||
p = tmp_path / name
|
||||
p.write_text(json.dumps(obj))
|
||||
return str(p)
|
||||
|
||||
|
||||
def test_api_format_se_devuelve_tal_cual(tmp_path):
|
||||
res = comfyui_import_workflow_json(_write(tmp_path, "api.json", _API))
|
||||
assert res["ok"] is True and res["format_detected"] == "api"
|
||||
assert res["workflow"] == _API
|
||||
|
||||
|
||||
def test_ui_graph_se_normaliza(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(mod, "comfyui_object_info", lambda server="", timeout=5.0: None)
|
||||
res = comfyui_import_workflow_json(_write(tmp_path, "ui.json", _UI_GRAPH))
|
||||
assert res["ok"] is True and res["format_detected"] == "ui_graph"
|
||||
api = res["workflow"]
|
||||
assert_api_format(api)
|
||||
# el nodo virtual Note se descarta; las conexiones del VAEDecode se resuelven al CheckpointLoader.
|
||||
assert "Note" not in class_types(api)
|
||||
assert "2" not in api
|
||||
assert api["3"]["inputs"]["samples"] == ["1", 0]
|
||||
assert api["3"]["inputs"]["vae"] == ["1", 2]
|
||||
|
||||
|
||||
def test_json_invalido_error(tmp_path):
|
||||
p = tmp_path / "bad.json"
|
||||
p.write_text("no soy json {")
|
||||
res = comfyui_import_workflow_json(str(p))
|
||||
assert res["ok"] is False and "JSON invalido" in res["error"]
|
||||
|
||||
|
||||
def test_formato_no_reconocido(tmp_path):
|
||||
res = comfyui_import_workflow_json(_write(tmp_path, "x.json", {"foo": "bar"}))
|
||||
assert res["ok"] is False and "no reconocido" in res["error"]
|
||||
|
||||
|
||||
def test_json_no_es_objeto(tmp_path):
|
||||
res = comfyui_import_workflow_json(_write(tmp_path, "lst.json", [1, 2, 3]))
|
||||
assert res["ok"] is False and "no es un objeto de workflow" in res["error"]
|
||||
|
||||
|
||||
def test_archivo_inexistente(tmp_path):
|
||||
res = comfyui_import_workflow_json(str(tmp_path / "no.json"))
|
||||
assert res["ok"] is False and "no se pudo leer" in res["error"]
|
||||
|
||||
|
||||
def test_determinista(tmp_path):
|
||||
path = _write(tmp_path, "api.json", _API)
|
||||
assert comfyui_import_workflow_json(path) == comfyui_import_workflow_json(path)
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Tests de comfyui_interrupt_queue contra un servidor ComfyUI simulado.
|
||||
|
||||
La funcion es pura I/O (HTTP), asi que levantamos un http.server local que imita
|
||||
los endpoints relevantes de ComfyUI (/interrupt, /queue) y verificamos:
|
||||
|
||||
- Golden: interrupt sin clear corta el actual pero NO vacia los pendientes.
|
||||
- Edge: clear_pending=True vacia la cola (queue_remaining=0).
|
||||
- Edge: clear_pending=True con la cola ya vacia no rompe.
|
||||
- Error: si el servidor no responde, devuelve {ok:False, error} sin lanzar.
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_interrupt_queue import comfyui_interrupt_queue
|
||||
|
||||
|
||||
class _FakeComfyHandler(http.server.BaseHTTPRequestHandler):
|
||||
"""Imita ComfyUI: estado de cola mutable compartido via la clase del server."""
|
||||
|
||||
def log_message(self, *args): # silenciar el log del servidor en los tests
|
||||
pass
|
||||
|
||||
def _send_json(self, obj, code=200):
|
||||
body = json.dumps(obj).encode()
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_POST(self):
|
||||
st = self.server.state
|
||||
if self.path == "/interrupt":
|
||||
st["running"] = [] # interrupt corta el prompt en ejecucion
|
||||
self._send_json({})
|
||||
return
|
||||
if self.path == "/queue":
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
raw = self.rfile.read(length) if length else b"{}"
|
||||
body = json.loads(raw or b"{}")
|
||||
if body.get("clear"):
|
||||
st["pending"] = [] # clear vacia los pendientes
|
||||
elif "delete" in body:
|
||||
st["pending"] = [
|
||||
p for p in st["pending"] if p not in body["delete"]
|
||||
]
|
||||
self._send_json({})
|
||||
return
|
||||
self._send_json({"error": "not found"}, code=404)
|
||||
|
||||
def do_GET(self):
|
||||
st = self.server.state
|
||||
if self.path == "/queue":
|
||||
self._send_json(
|
||||
{
|
||||
"queue_running": st["running"],
|
||||
"queue_pending": st["pending"],
|
||||
}
|
||||
)
|
||||
return
|
||||
self._send_json({"error": "not found"}, code=404)
|
||||
|
||||
|
||||
def _start_fake_server(running, pending):
|
||||
"""Levanta el servidor fake en un puerto efimero. Devuelve (server, addr, thread)."""
|
||||
server = http.server.HTTPServer(("127.0.0.1", 0), _FakeComfyHandler)
|
||||
server.state = {"running": list(running), "pending": list(pending)}
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
host, port = server.server_address
|
||||
return server, f"{host}:{port}", thread
|
||||
|
||||
|
||||
def _free_port():
|
||||
"""Reserva y libera un puerto para garantizar que NADA escucha ahi (error path)."""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind(("127.0.0.1", 0))
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
return port
|
||||
|
||||
|
||||
def test_interrumpe_sin_vaciar():
|
||||
# Golden: 1 ejecutandose + 2 pendientes; interrupt corta el actual, pendientes siguen.
|
||||
server, addr, _ = _start_fake_server(running=["r1"], pending=["p1", "p2"])
|
||||
try:
|
||||
res = comfyui_interrupt_queue(server=addr)
|
||||
finally:
|
||||
server.shutdown()
|
||||
assert res["ok"] is True
|
||||
assert res["interrupted"] is True
|
||||
assert res["cleared"] is False
|
||||
# running cortado (0) + 2 pendientes que siguen = 2 restantes.
|
||||
assert res["queue_remaining"] == 2
|
||||
assert res["error"] == ""
|
||||
|
||||
|
||||
def test_clear_pending_vacia_cola():
|
||||
# Edge: clear_pending vacia los pendientes -> queue_remaining 0.
|
||||
server, addr, _ = _start_fake_server(running=["r1"], pending=["p1", "p2", "p3"])
|
||||
try:
|
||||
res = comfyui_interrupt_queue(clear_pending=True, server=addr)
|
||||
finally:
|
||||
server.shutdown()
|
||||
assert res["ok"] is True
|
||||
assert res["interrupted"] is True
|
||||
assert res["cleared"] is True
|
||||
assert res["queue_remaining"] == 0
|
||||
assert res["error"] == ""
|
||||
|
||||
|
||||
def test_clear_pending_cola_vacia_no_rompe():
|
||||
# Edge: clear_pending con la cola ya vacia es inocuo, no rompe.
|
||||
server, addr, _ = _start_fake_server(running=[], pending=[])
|
||||
try:
|
||||
res = comfyui_interrupt_queue(clear_pending=True, server=addr)
|
||||
finally:
|
||||
server.shutdown()
|
||||
assert res["ok"] is True
|
||||
assert res["interrupted"] is True
|
||||
assert res["cleared"] is True
|
||||
assert res["queue_remaining"] == 0
|
||||
assert res["error"] == ""
|
||||
|
||||
|
||||
def test_servidor_caido_no_lanza():
|
||||
# Error: nada escucha en el puerto -> {ok:False, error} sin excepcion cruda.
|
||||
dead = f"127.0.0.1:{_free_port()}"
|
||||
res = comfyui_interrupt_queue(server=dead, timeout=1.0)
|
||||
assert res["ok"] is False
|
||||
assert res["interrupted"] is False
|
||||
assert res["error"] != ""
|
||||
assert "interrupt fallo" in res["error"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_interrumpe_sin_vaciar()
|
||||
test_clear_pending_vacia_cola()
|
||||
test_clear_pending_cola_vacia_no_rompe()
|
||||
test_servidor_caido_no_lanza()
|
||||
print("OK: 4 tests passed")
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Tests offline para comfyui_judge_image (impura: panel multi-juez por mayoria).
|
||||
|
||||
Sin GPU, sin red, sin servidor: monkeypatchea los tres jueces (estetico, fidelidad CLIP,
|
||||
critico LLM) con stubs para probar la LOGICA de voto, agregacion y exclusion de jueces caidos.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
import ml.comfyui_judge_image as mod
|
||||
from ml.comfyui_judge_image import comfyui_judge_image
|
||||
|
||||
|
||||
def _aes(score, ok=True):
|
||||
return lambda image_path, **kw: {"ok": ok, "score_0_10": score, "error": "" if ok else "boom"}
|
||||
|
||||
|
||||
def _clip(score, ok=True):
|
||||
return lambda image_path, prompt, **kw: {"ok": ok, "score_0_1": score, "error": "" if ok else "boom"}
|
||||
|
||||
|
||||
def _llm(verdict, score=7.0, ok=True):
|
||||
return lambda image_path, prompt, **kw: {
|
||||
"ok": ok, "verdict": verdict, "score_0_10": score,
|
||||
"reasons": ["motivo"], "error": "" if ok else "boom"}
|
||||
|
||||
|
||||
def _patch(monkeypatch, aes, clip, llm):
|
||||
monkeypatch.setattr(mod, "comfyui_score_aesthetic", aes)
|
||||
monkeypatch.setattr(mod, "comfyui_score_clip_alignment", clip)
|
||||
monkeypatch.setattr(mod, "comfyui_critique_image_llm", llm)
|
||||
|
||||
|
||||
def test_tres_good_verdict_good(monkeypatch):
|
||||
_patch(monkeypatch, _aes(8.0), _clip(0.30), _llm("good"))
|
||||
res = comfyui_judge_image("i.png", "a cat")
|
||||
assert res["ok"] is True and res["verdict"] == "good"
|
||||
assert res["votes"] == {"aesthetic": "good", "clip": "good", "llm": "good"}
|
||||
# score = media de 8, 3.0(=0.30*10), 7 = 6.0
|
||||
assert abs(res["score"] - 6.0) < 1e-9
|
||||
|
||||
|
||||
def test_mayoria_bad(monkeypatch):
|
||||
# estetico bajo (bad) + clip bajo (bad) + llm good -> 2 bad, 1 good -> bad.
|
||||
_patch(monkeypatch, _aes(2.0), _clip(0.05), _llm("good"))
|
||||
res = comfyui_judge_image("i.png", "p")
|
||||
assert res["verdict"] == "bad"
|
||||
|
||||
|
||||
def test_empate_es_bad_conservador(monkeypatch):
|
||||
# 1 good (estetico) + 1 bad (clip) + 1 failed (llm) -> empate -> bad.
|
||||
_patch(monkeypatch, _aes(8.0), _clip(0.05), _llm("good", ok=False))
|
||||
res = comfyui_judge_image("i.png", "p")
|
||||
assert res["votes"]["llm"] == "failed"
|
||||
assert res["verdict"] == "bad"
|
||||
|
||||
|
||||
def test_juez_caido_se_excluye_no_crashea(monkeypatch):
|
||||
# estetico falla pero el panel sigue votando con los otros dos.
|
||||
_patch(monkeypatch, _aes(0.0, ok=False), _clip(0.30), _llm("good"))
|
||||
res = comfyui_judge_image("i.png", "p")
|
||||
assert res["ok"] is True
|
||||
assert res["votes"]["aesthetic"] == "failed"
|
||||
assert res["verdict"] == "good"
|
||||
|
||||
|
||||
def test_tres_fallan_ok_false(monkeypatch):
|
||||
_patch(monkeypatch, _aes(0.0, ok=False), _clip(0.0, ok=False), _llm("", ok=False))
|
||||
res = comfyui_judge_image("i.png", "p")
|
||||
assert res["ok"] is False and "los tres jueces fallaron" in res["error"]
|
||||
|
||||
|
||||
def test_weights_afectan_score_no_voto(monkeypatch):
|
||||
_patch(monkeypatch, _aes(10.0), _clip(0.30), _llm("good", score=0.0))
|
||||
base = comfyui_judge_image("i.png", "p")
|
||||
# subir el peso del estetico (10) y anular el del llm (0) sube el score agregado.
|
||||
weighted = comfyui_judge_image("i.png", "p", weights={"aesthetic": 5.0, "llm": 0.0})
|
||||
assert weighted["score"] > base["score"]
|
||||
assert weighted["verdict"] == base["verdict"] == "good"
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Tests offline para comfyui_read_png_metadata (impura stdlib: parsea metadata de un PNG ComfyUI).
|
||||
|
||||
Sin red, sin GPU, sin servidor: fabrica PNGs con chunk de texto 'prompt' y verifica el parsing.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_read_png_metadata import comfyui_read_png_metadata
|
||||
|
||||
PIL = pytest.importorskip("PIL")
|
||||
from PIL import Image # noqa: E402
|
||||
from PIL.PngImagePlugin import PngInfo # noqa: E402
|
||||
|
||||
|
||||
_PROMPT = {
|
||||
"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "model.safetensors"}},
|
||||
"2": {"class_type": "CLIPTextEncode", "inputs": {"text": "a cat on a table"}},
|
||||
"3": {"class_type": "CLIPTextEncode", "inputs": {"text": "blurry, lowres"}},
|
||||
"4": {"class_type": "KSampler", "inputs": {
|
||||
"seed": 42, "steps": 20, "cfg": 7.0, "sampler_name": "euler",
|
||||
"scheduler": "normal", "denoise": 1.0,
|
||||
"positive": ["2", 0], "negative": ["3", 0], "model": ["1", 0], "latent_image": ["5", 0]}},
|
||||
}
|
||||
|
||||
|
||||
def _png_with_prompt(path, prompt_obj=_PROMPT, text=None):
|
||||
info = PngInfo()
|
||||
info.add_text("prompt", text if text is not None else json.dumps(prompt_obj))
|
||||
Image.new("RGB", (8, 8), (0, 0, 0)).save(path, pnginfo=info)
|
||||
return str(path)
|
||||
|
||||
|
||||
def _png_plain(path):
|
||||
Image.new("RGB", (8, 8), (0, 0, 0)).save(path)
|
||||
return str(path)
|
||||
|
||||
|
||||
def test_extrae_prompt_y_parametros(tmp_path):
|
||||
res = comfyui_read_png_metadata(_png_with_prompt(tmp_path / "g.png"))
|
||||
assert res["ok"] is True and res["error"] == ""
|
||||
assert res["prompt"] == _PROMPT
|
||||
p = res["parameters"]
|
||||
assert p["seed"] == 42 and p["steps"] == 20 and p["cfg"] == 7.0
|
||||
assert p["sampler_name"] == "euler" and p["scheduler"] == "normal" and p["denoise"] == 1.0
|
||||
assert p["positive"] == "a cat on a table" and p["negative"] == "blurry, lowres"
|
||||
assert p["model"] == "model.safetensors"
|
||||
|
||||
|
||||
def test_error_archivo_inexistente(tmp_path):
|
||||
res = comfyui_read_png_metadata(str(tmp_path / "no.png"))
|
||||
assert res["ok"] is False and "no se pudo leer" in res["error"]
|
||||
|
||||
|
||||
def test_error_png_sin_chunk_prompt(tmp_path):
|
||||
res = comfyui_read_png_metadata(_png_plain(tmp_path / "plain.png"))
|
||||
assert res["ok"] is False and "no contiene chunk 'prompt'" in res["error"]
|
||||
|
||||
|
||||
def test_error_prompt_no_json(tmp_path):
|
||||
res = comfyui_read_png_metadata(_png_with_prompt(tmp_path / "bad.png", text="no soy json {"))
|
||||
assert res["ok"] is False and "no es JSON valido" in res["error"]
|
||||
|
||||
|
||||
def test_error_no_es_png(tmp_path):
|
||||
bad = tmp_path / "fake.png"
|
||||
bad.write_bytes(b"esto no es un PNG")
|
||||
res = comfyui_read_png_metadata(str(bad))
|
||||
assert res["ok"] is False and res["error"]
|
||||
|
||||
|
||||
def test_determinista(tmp_path):
|
||||
path = _png_with_prompt(tmp_path / "g.png")
|
||||
assert comfyui_read_png_metadata(path) == comfyui_read_png_metadata(path)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Tests offline para comfyui_resolve_workflow_deps (impura: compone comfyui_validate_workflow).
|
||||
|
||||
Sin red, sin servidor: monkeypatchea comfyui_validate_workflow para probar la traduccion de
|
||||
nodos/modelos faltantes en sugerencias accionables y el error path cuando el servidor no responde.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
import ml.comfyui_resolve_workflow_deps as mod
|
||||
from ml.comfyui_resolve_workflow_deps import comfyui_resolve_workflow_deps
|
||||
|
||||
_WF = {"1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "x.safetensors"}}}
|
||||
|
||||
|
||||
def test_traduce_nodos_y_modelos_faltantes(monkeypatch):
|
||||
monkeypatch.setattr(mod, "comfyui_validate_workflow", lambda wf, server="": {
|
||||
"ok": True,
|
||||
"missing_nodes": ["FooNode"],
|
||||
"missing_models": [{"node": "1", "input": "ckpt_name", "value": "x.safetensors"}],
|
||||
})
|
||||
res = comfyui_resolve_workflow_deps(_WF)
|
||||
assert res["ok"] is True and res["error"] == ""
|
||||
assert res["missing_nodes"] == ["FooNode"]
|
||||
kinds = {s["kind"] for s in res["suggestions"]}
|
||||
assert kinds == {"node", "model"}
|
||||
node_sug = next(s for s in res["suggestions"] if s["kind"] == "node")
|
||||
assert node_sug["action"] == "install_custom_node" and node_sug["name"] == "FooNode"
|
||||
model_sug = next(s for s in res["suggestions"] if s["kind"] == "model")
|
||||
assert model_sug["action"] == "search_and_download" and model_sug["name"] == "x.safetensors"
|
||||
|
||||
|
||||
def test_sin_faltantes_suggestions_vacio(monkeypatch):
|
||||
monkeypatch.setattr(mod, "comfyui_validate_workflow", lambda wf, server="": {
|
||||
"ok": True, "missing_nodes": [], "missing_models": []})
|
||||
res = comfyui_resolve_workflow_deps(_WF)
|
||||
assert res["ok"] is True and res["suggestions"] == []
|
||||
|
||||
|
||||
def test_servidor_caido_propaga_error(monkeypatch):
|
||||
monkeypatch.setattr(mod, "comfyui_validate_workflow", lambda wf, server="": {
|
||||
"ok": False, "error": "no se pudo conectar al servidor"})
|
||||
res = comfyui_resolve_workflow_deps(_WF)
|
||||
assert res["ok"] is False
|
||||
assert "no se pudo conectar" in res["error"]
|
||||
assert res["suggestions"] == []
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Tests offline para comfyui_score_aesthetic (impura: scoring LAION-V2 via subproceso torch).
|
||||
|
||||
Sin GPU, sin torch, sin servidor: ejercita SOLO los guards previos al subproceso (imagen,
|
||||
python del venv ComfyUI y .pth del modelo ausentes), que cortan antes de tocar la GPU.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_score_aesthetic import comfyui_score_aesthetic
|
||||
|
||||
PIL = pytest.importorskip("PIL")
|
||||
from PIL import Image # noqa: E402
|
||||
|
||||
|
||||
def _png(path):
|
||||
Image.new("RGB", (8, 8), (0, 0, 0)).save(path)
|
||||
return str(path)
|
||||
|
||||
|
||||
def test_error_imagen_inexistente(tmp_path):
|
||||
res = comfyui_score_aesthetic(str(tmp_path / "no.png"))
|
||||
assert res["ok"] is False and res["score_0_10"] == 0.0
|
||||
assert "imagen no encontrada" in res["error"]
|
||||
|
||||
|
||||
def test_error_venv_python_inexistente(tmp_path):
|
||||
# imagen valida pero venv_python ausente -> corta antes del subproceso.
|
||||
res = comfyui_score_aesthetic(_png(tmp_path / "i.png"),
|
||||
venv_python=str(tmp_path / "no_python"))
|
||||
assert res["ok"] is False and "python del venv ComfyUI no encontrado" in res["error"]
|
||||
|
||||
|
||||
def test_error_modelo_inexistente(tmp_path):
|
||||
# imagen + python validos, .pth ausente -> error de modelo, sin lanzar el subproceso.
|
||||
res = comfyui_score_aesthetic(_png(tmp_path / "i.png"),
|
||||
venv_python=sys.executable,
|
||||
model_path=str(tmp_path / "no.pth"))
|
||||
assert res["ok"] is False and "modelo estetico no encontrado" in res["error"]
|
||||
|
||||
|
||||
def test_nunca_lanza_y_es_determinista(tmp_path):
|
||||
img = _png(tmp_path / "i.png")
|
||||
a = comfyui_score_aesthetic(img, venv_python=str(tmp_path / "x"))
|
||||
b = comfyui_score_aesthetic(img, venv_python=str(tmp_path / "x"))
|
||||
assert a == b and a["ok"] is False
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Tests offline para comfyui_score_clip_alignment (impura: similitud CLIP via subproceso torch).
|
||||
|
||||
Sin GPU, sin torch, sin servidor: ejercita SOLO los guards previos al subproceso (imagen
|
||||
ausente, prompt vacio, python del venv ComfyUI ausente).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_score_clip_alignment import comfyui_score_clip_alignment
|
||||
|
||||
PIL = pytest.importorskip("PIL")
|
||||
from PIL import Image # noqa: E402
|
||||
|
||||
|
||||
def _png(path):
|
||||
Image.new("RGB", (8, 8), (0, 0, 0)).save(path)
|
||||
return str(path)
|
||||
|
||||
|
||||
def test_error_imagen_inexistente(tmp_path):
|
||||
res = comfyui_score_clip_alignment(str(tmp_path / "no.png"), "a cat")
|
||||
assert res["ok"] is False and res["score_0_1"] == 0.0
|
||||
assert "imagen no encontrada" in res["error"]
|
||||
|
||||
|
||||
def test_error_prompt_vacio(tmp_path):
|
||||
res = comfyui_score_clip_alignment(_png(tmp_path / "i.png"), " ")
|
||||
assert res["ok"] is False and "prompt vacio" in res["error"]
|
||||
|
||||
|
||||
def test_error_venv_python_inexistente(tmp_path):
|
||||
res = comfyui_score_clip_alignment(_png(tmp_path / "i.png"), "a cat",
|
||||
venv_python=str(tmp_path / "no_python"))
|
||||
assert res["ok"] is False and "python del venv ComfyUI no encontrado" in res["error"]
|
||||
|
||||
|
||||
def test_nunca_lanza_y_es_determinista(tmp_path):
|
||||
img = _png(tmp_path / "i.png")
|
||||
a = comfyui_score_clip_alignment(img, "a cat", venv_python=str(tmp_path / "x"))
|
||||
b = comfyui_score_clip_alignment(img, "a cat", venv_python=str(tmp_path / "x"))
|
||||
assert a == b and a["ok"] is False
|
||||
Reference in New Issue
Block a user