ccdd529bdc
Materializa el metodo ganador del report 0215: generar a alta-res con SDXL + LoRA SDXL_pixel-art, downscale contrast-aware con PixelOE (engine=pixeloe para sprites/personajes) o nearest (tiles), y cuantizacion dura con comfyui_pixelize_image (16 colores libres o paleta fija pico-8/nes/game-boy). - pixeloe_downscale_py_ml: downscale contrast-aware via lib pixeloe con bridge de interprete (la lib vive en el venv de ComfyUI, no en el del registry). No-throw, fallback limpio si pixeloe no disponible. - comfyui_pixelart_real_oneshot_py_pipelines: one-shot que compone build_pixelart + submit + wait + fetch + pixeloe_downscale + pixelize_image. Fallback automatico pixeloe->nearest. Sweet-spot 64px personajes, 32px iconos. Verificado por PIL: personaje 64x64=16 colores, icono 32x32=16 colores (vs ~33k de la imagen de difusion cruda). 100% grid duro + outline nitido. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
4.7 KiB
Python
123 lines
4.7 KiB
Python
"""Tests de pixeloe_downscale — tolerantes al entorno.
|
|
|
|
El venv del registry NO trae `pixeloe`, asi que estas pruebas ejercitan el
|
|
"bridge" de interprete (subprocess al python de ComfyUI, que si la tiene). Si
|
|
tampoco hay ningun interprete con pixeloe disponible, la funcion debe degradar
|
|
limpiamente: ok=False con error no vacio y SIN lanzar excepcion.
|
|
|
|
Por eso cada test PASA en los dos escenarios:
|
|
- pixeloe disponible (inproc o via bridge): assert sobre el resultado real.
|
|
- pixeloe ausente en todos lados: assert sobre la degradacion no-throw.
|
|
Asi la suite es verde tanto en este PC (ComfyUI presente) como en uno sin ComfyUI,
|
|
y el contrato "no-throw" queda cubierto en ambos.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
from ml.pixeloe_downscale import pixeloe_downscale # noqa: E402
|
|
|
|
|
|
def _shapes_png(path, w=256, h=256):
|
|
"""PNG 256x256 RGB con un gradiente + formas (contraste con silueta clara)."""
|
|
yy, xx = np.mgrid[0:h, 0:w]
|
|
arr = np.zeros((h, w, 3), dtype=np.uint8)
|
|
arr[..., 0] = (xx * 255 // max(1, w - 1)).astype(np.uint8) # gradiente rojo
|
|
arr[..., 1] = (yy * 255 // max(1, h - 1)).astype(np.uint8) # gradiente verde
|
|
# Bloque azul central: borde duro para que el modo "contrast" tenga silueta.
|
|
arr[h // 4:3 * h // 4, w // 4:3 * w // 4, 2] = 255
|
|
Image.fromarray(arr, "RGB").save(path)
|
|
return path
|
|
|
|
|
|
def test_golden_downscale_64_or_clean_degrade(tmp_path):
|
|
"""Golden: 256x256 -> grid 64x64 (no_upscale). Si pixeloe no esta -> ok=False limpio."""
|
|
src = _shapes_png(str(tmp_path / "raw.png"))
|
|
dst = str(tmp_path / "grid64.png")
|
|
res = pixeloe_downscale(src, dst, target_size=64, no_upscale=True)
|
|
assert isinstance(res, dict)
|
|
if res["ok"]:
|
|
assert os.path.isfile(dst)
|
|
assert res["size"] == [64, 64] # no_upscale=True -> grid real
|
|
assert res["error"] == ""
|
|
assert res["via"] in ("inproc", "bridge")
|
|
assert res["mode"] == "contrast"
|
|
assert res["target_size"] == 64
|
|
else:
|
|
# Degradacion limpia: sin pixeloe en ningun interprete.
|
|
assert res["error"] != ""
|
|
assert res["via"] in ("", "bridge", "inproc")
|
|
|
|
|
|
def test_edge_target_size_32(tmp_path):
|
|
"""Edge: grid de 32 (iconos). size==[32,32] cuando pixeloe esta presente."""
|
|
src = _shapes_png(str(tmp_path / "raw.png"))
|
|
dst = str(tmp_path / "grid32.png")
|
|
res = pixeloe_downscale(src, dst, target_size=32, no_upscale=True)
|
|
if res["ok"]:
|
|
assert res["size"] == [32, 32]
|
|
assert res["target_size"] == 32
|
|
assert os.path.isfile(dst)
|
|
else:
|
|
assert res["error"] != ""
|
|
|
|
|
|
def test_edge_mode_nearest_no_color_matching(tmp_path):
|
|
"""Edge: otro modo + color_matching off; debe seguir produciendo el grid o degradar."""
|
|
src = _shapes_png(str(tmp_path / "raw.png"))
|
|
dst = str(tmp_path / "near.png")
|
|
res = pixeloe_downscale(
|
|
src, dst, mode="nearest", target_size=64,
|
|
color_matching=False, no_upscale=True,
|
|
)
|
|
assert isinstance(res, dict)
|
|
if res["ok"]:
|
|
assert res["mode"] == "nearest"
|
|
assert res["size"] == [64, 64]
|
|
else:
|
|
assert res["error"] != ""
|
|
|
|
|
|
def test_error_missing_src_no_throw(tmp_path):
|
|
"""Error path: src inexistente -> ok=False, error explica, sin excepcion."""
|
|
res = pixeloe_downscale(
|
|
str(tmp_path / "nope.png"), str(tmp_path / "o.png"), target_size=64,
|
|
)
|
|
assert res["ok"] is False
|
|
assert "no existe" in res["error"]
|
|
assert res["size"] == [0, 0]
|
|
|
|
|
|
def test_error_no_interpreter_with_pixeloe(tmp_path):
|
|
"""Error path: forzar comfy_python invalido cuando el actual no tiene pixeloe.
|
|
|
|
Si el interprete que corre el test YA tiene pixeloe (inproc), el comfy_python
|
|
invalido se ignora y la llamada puede salir ok=True; el test sigue siendo
|
|
valido (no-throw). Si NO lo tiene, no hay ningun interprete con pixeloe y debe
|
|
devolver ok=False con error, nunca lanzar.
|
|
"""
|
|
src = _shapes_png(str(tmp_path / "raw.png"))
|
|
dst = str(tmp_path / "o.png")
|
|
res = pixeloe_downscale(
|
|
src, dst, target_size=64, comfy_python="/no/such/python-interpreter",
|
|
)
|
|
assert isinstance(res, dict)
|
|
try:
|
|
import pixeloe # noqa: F401
|
|
has_local = True
|
|
except Exception: # noqa: BLE001
|
|
has_local = False
|
|
if has_local:
|
|
# pixeloe en el interprete del test -> ruta inproc, comfy_python ignorado.
|
|
assert res["ok"] is True
|
|
else:
|
|
# comfy_python invalido + env vacio: si ~/ComfyUI/.venv existe, puede
|
|
# bridgear y salir ok; si no, ok=False con error. Ambos no-throw.
|
|
assert res["ok"] in (True, False)
|
|
if not res["ok"]:
|
|
assert res["error"] != ""
|