feat(ml): auto-commit con 20 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
"""Helper compartido por los tests de los builders de workflow ComfyUI.
|
||||
|
||||
Valida la invariante del "API format": un dict de nodos numerados, cada uno con
|
||||
`class_type` + `inputs`, donde las conexiones entre nodos son listas
|
||||
`[node_id, output_index]` que referencian un node_id existente.
|
||||
"""
|
||||
|
||||
|
||||
def assert_api_format(wf):
|
||||
"""Comprueba que `wf` es un workflow ComfyUI en API format bien formado.
|
||||
|
||||
- dict no vacio
|
||||
- cada nodo tiene `class_type` (str) e `inputs` (dict)
|
||||
- toda conexion `[node_id, output_index]` apunta a un nodo existente
|
||||
"""
|
||||
assert isinstance(wf, dict) and wf, "workflow debe ser un dict no vacio"
|
||||
ids = set(wf)
|
||||
for nid, node in wf.items():
|
||||
assert isinstance(node, dict), f"nodo {nid} no es dict"
|
||||
assert isinstance(node.get("class_type"), str) and node["class_type"], (
|
||||
f"nodo {nid} sin class_type"
|
||||
)
|
||||
assert isinstance(node.get("inputs"), dict), f"nodo {nid} sin inputs dict"
|
||||
for key, val in node["inputs"].items():
|
||||
if (
|
||||
isinstance(val, list)
|
||||
and len(val) == 2
|
||||
and isinstance(val[0], str)
|
||||
and isinstance(val[1], int)
|
||||
):
|
||||
assert val[0] in ids, (
|
||||
f"nodo {nid}.{key} referencia node_id inexistente {val[0]!r}"
|
||||
)
|
||||
|
||||
|
||||
def class_types(wf):
|
||||
"""Conjunto de los `class_type` presentes en el workflow."""
|
||||
return {n["class_type"] for n in wf.values()}
|
||||
|
||||
|
||||
def node_by_ct(wf, ct):
|
||||
"""Primer nodo cuyo `class_type` es `ct` (lanza StopIteration si no hay)."""
|
||||
return next(n for n in wf.values() if n["class_type"] == ct)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Tests de estructura para comfyui_build_controlnet_workflow (funcion pura)."""
|
||||
|
||||
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_build_controlnet_workflow import comfyui_build_controlnet_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_estructura_controlnet():
|
||||
wf = comfyui_build_controlnet_workflow("ck.safetensors", "ctrl.png", "cn.pth", "POS", "NEG")
|
||||
assert_api_format(wf)
|
||||
cts = class_types(wf)
|
||||
assert "ControlNetLoader" in cts
|
||||
assert "ControlNetApply" in cts
|
||||
|
||||
|
||||
def test_control_image_modelo_y_strength():
|
||||
wf = comfyui_build_controlnet_workflow(
|
||||
"ck.safetensors", "pose.png", "control_openpose.pth", "POS", "NEG", strength=0.65, seed=5
|
||||
)
|
||||
assert node_by_ct(wf, "ControlNetLoader")["inputs"]["control_net_name"] == "control_openpose.pth"
|
||||
# La imagen de control se carga via LoadImage.
|
||||
assert node_by_ct(wf, "LoadImage")["inputs"]["image"] == "pose.png"
|
||||
apply_in = node_by_ct(wf, "ControlNetApply")["inputs"]
|
||||
assert apply_in["strength"] == 0.65
|
||||
assert node_by_ct(wf, "KSampler")["inputs"]["seed"] == 5
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Tests de estructura para comfyui_build_image_to_3d_workflow (funcion pura)."""
|
||||
|
||||
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_build_image_to_3d_workflow import comfyui_build_image_to_3d_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_cadena_hunyuan3d_nativa():
|
||||
wf = comfyui_build_image_to_3d_workflow("obj.png")
|
||||
assert_api_format(wf)
|
||||
# Los 9 nodos nativos de Hunyuan3D-2 (sin custom node).
|
||||
esperado = {
|
||||
"LoadImage",
|
||||
"ImageOnlyCheckpointLoader",
|
||||
"CLIPVisionEncode",
|
||||
"Hunyuan3Dv2Conditioning",
|
||||
"EmptyLatentHunyuan3Dv2",
|
||||
"KSampler",
|
||||
"VAEDecodeHunyuan3D",
|
||||
"VoxelToMeshBasic",
|
||||
"SaveGLB",
|
||||
}
|
||||
assert class_types(wf) == esperado
|
||||
|
||||
|
||||
def test_imagen_checkpoint_y_salida_glb():
|
||||
wf = comfyui_build_image_to_3d_workflow(
|
||||
"robot.png", ckpt_name="hunyuan3d-dit-v2-mini.safetensors", seed=42
|
||||
)
|
||||
assert node_by_ct(wf, "LoadImage")["inputs"]["image"] == "robot.png"
|
||||
assert (
|
||||
node_by_ct(wf, "ImageOnlyCheckpointLoader")["inputs"]["ckpt_name"]
|
||||
== "hunyuan3d-dit-v2-mini.safetensors"
|
||||
)
|
||||
assert node_by_ct(wf, "KSampler")["inputs"]["seed"] == 42
|
||||
# SaveGLB es el nodo de salida: produce la malla .glb.
|
||||
assert "SaveGLB" in class_types(wf)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Tests de estructura para comfyui_build_img2img_workflow (funcion pura)."""
|
||||
|
||||
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_build_img2img_workflow import comfyui_build_img2img_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_estructura_y_class_types():
|
||||
wf = comfyui_build_img2img_workflow("ck.safetensors", "init.png", "POS", "NEG")
|
||||
assert_api_format(wf)
|
||||
# img2img usa VAEEncode (no EmptyLatentImage) para partir de la imagen base.
|
||||
cts = class_types(wf)
|
||||
assert "VAEEncode" in cts
|
||||
assert "LoadImage" in cts
|
||||
assert "EmptyLatentImage" not in cts
|
||||
|
||||
|
||||
def test_denoise_y_init_image():
|
||||
wf = comfyui_build_img2img_workflow(
|
||||
"ck.safetensors", "init.png", "POS", "NEG", denoise=0.45, seed=7
|
||||
)
|
||||
ks = node_by_ct(wf, "KSampler")["inputs"]
|
||||
assert ks["denoise"] == 0.45
|
||||
assert ks["seed"] == 7
|
||||
assert node_by_ct(wf, "LoadImage")["inputs"]["image"] == "init.png"
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Tests de estructura para comfyui_build_inpaint_workflow (funcion pura)."""
|
||||
|
||||
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_build_inpaint_workflow import comfyui_build_inpaint_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_estructura_inpaint():
|
||||
wf = comfyui_build_inpaint_workflow("ck.safetensors", "img.png", "mask.png", "POS", "NEG")
|
||||
assert_api_format(wf)
|
||||
cts = class_types(wf)
|
||||
# Inpaint necesita la mascara y la codificacion para inpaint.
|
||||
assert "LoadImageMask" in cts
|
||||
assert "VAEEncodeForInpaint" in cts
|
||||
|
||||
|
||||
def test_base_y_mascara_se_cargan():
|
||||
wf = comfyui_build_inpaint_workflow(
|
||||
"ck.safetensors", "base.png", "m.png", "POS", "NEG", seed=33, denoise=0.9
|
||||
)
|
||||
img = node_by_ct(wf, "LoadImage")["inputs"]["image"]
|
||||
mask = node_by_ct(wf, "LoadImageMask")["inputs"]["image"]
|
||||
assert img == "base.png"
|
||||
assert mask == "m.png"
|
||||
ks = node_by_ct(wf, "KSampler")["inputs"]
|
||||
assert ks["seed"] == 33 and ks["denoise"] == 0.9
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Tests de estructura para comfyui_build_sdxl_refiner_workflow (funcion pura)."""
|
||||
|
||||
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_build_sdxl_refiner_workflow import comfyui_build_sdxl_refiner_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types
|
||||
|
||||
|
||||
def test_dos_ksampler_advanced():
|
||||
wf = comfyui_build_sdxl_refiner_workflow("base.st", "ref.st", "POS", "NEG")
|
||||
assert_api_format(wf)
|
||||
assert "KSamplerAdvanced" in class_types(wf)
|
||||
ks = [n for n in wf.values() if n["class_type"] == "KSamplerAdvanced"]
|
||||
assert len(ks) == 2
|
||||
|
||||
|
||||
def test_base_emite_ruido_sobrante_y_refiner_lo_recoge():
|
||||
wf = comfyui_build_sdxl_refiner_workflow(
|
||||
"base.st", "ref.st", "POS", "NEG", base_steps=20, refiner_steps=5, seed=9
|
||||
)
|
||||
ks = [n["inputs"] for n in wf.values() if n["class_type"] == "KSamplerAdvanced"]
|
||||
base = next(k for k in ks if k["add_noise"] == "enable")
|
||||
refiner = next(k for k in ks if k["add_noise"] == "disable")
|
||||
# El base arranca el ruido y deja el sobrante; el refiner lo termina sin add_noise.
|
||||
assert base["return_with_leftover_noise"] == "enable"
|
||||
assert refiner["return_with_leftover_noise"] == "disable"
|
||||
# Comparten el corte de pasos: el base termina donde declara base_steps.
|
||||
assert base["end_at_step"] == 20
|
||||
assert refiner["start_at_step"] == 20
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Tests de estructura para comfyui_build_txt2img_workflow (funcion pura)."""
|
||||
|
||||
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_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_estructura_y_class_types():
|
||||
wf = comfyui_build_txt2img_workflow("ck.safetensors", "POS", "NEG")
|
||||
assert_api_format(wf)
|
||||
assert class_types(wf) == {
|
||||
"CheckpointLoaderSimple",
|
||||
"CLIPTextEncode",
|
||||
"EmptyLatentImage",
|
||||
"KSampler",
|
||||
"VAEDecode",
|
||||
"SaveImage",
|
||||
}
|
||||
|
||||
|
||||
def test_params_se_reflejan_en_los_nodos():
|
||||
wf = comfyui_build_txt2img_workflow(
|
||||
"ck.safetensors", "POS", "NEG", steps=8, cfg=6.0, width=640, height=480, seed=123
|
||||
)
|
||||
ks = node_by_ct(wf, "KSampler")["inputs"]
|
||||
assert ks["seed"] == 123
|
||||
assert ks["steps"] == 8
|
||||
assert ks["cfg"] == 6.0
|
||||
lat = node_by_ct(wf, "EmptyLatentImage")["inputs"]
|
||||
assert lat["width"] == 640 and lat["height"] == 480
|
||||
assert node_by_ct(wf, "CheckpointLoaderSimple")["inputs"]["ckpt_name"] == "ck.safetensors"
|
||||
textos = sorted(n["inputs"]["text"] for n in wf.values() if n["class_type"] == "CLIPTextEncode")
|
||||
assert textos == ["NEG", "POS"]
|
||||
|
||||
|
||||
def test_filename_prefix_en_saveimage():
|
||||
wf = comfyui_build_txt2img_workflow("ck.safetensors", "POS", filename_prefix="demo_run")
|
||||
assert node_by_ct(wf, "SaveImage")["inputs"]["filename_prefix"] == "demo_run"
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Tests de estructura para comfyui_build_upscale_workflow (funcion pura)."""
|
||||
|
||||
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_build_upscale_workflow import comfyui_build_upscale_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_method_model_usa_esrgan():
|
||||
wf = comfyui_build_upscale_workflow("img.png", model_name="4x-UltraSharp.pth", method="model")
|
||||
assert_api_format(wf)
|
||||
cts = class_types(wf)
|
||||
assert "UpscaleModelLoader" in cts
|
||||
assert "ImageUpscaleWithModel" in cts
|
||||
assert node_by_ct(wf, "UpscaleModelLoader")["inputs"]["model_name"] == "4x-UltraSharp.pth"
|
||||
|
||||
|
||||
def test_method_latent_no_usa_modelo():
|
||||
wf = comfyui_build_upscale_workflow("img.png", method="latent")
|
||||
assert_api_format(wf)
|
||||
cts = class_types(wf)
|
||||
assert "ImageScaleBy" in cts
|
||||
assert "UpscaleModelLoader" not in cts
|
||||
assert node_by_ct(wf, "LoadImage")["inputs"]["image"] == "img.png"
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Tests de estructura para comfyui_build_view_3d_workflow (funcion pura)."""
|
||||
|
||||
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_build_view_3d_workflow import comfyui_build_view_3d_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_load3d_simple():
|
||||
wf = comfyui_build_view_3d_workflow("mesh.glb", width=800, height=600)
|
||||
assert_api_format(wf)
|
||||
# Visor minimo: un unico nodo Load3D.
|
||||
assert class_types(wf) == {"Load3D"}
|
||||
n = node_by_ct(wf, "Load3D")["inputs"]
|
||||
assert n["model_file"] == "mesh.glb"
|
||||
assert n["width"] == 800 and n["height"] == 600
|
||||
|
||||
|
||||
def test_animation_usa_load3d_advanced():
|
||||
wf = comfyui_build_view_3d_workflow("mesh.glb", animation=True)
|
||||
assert class_types(wf) == {"Load3DAdvanced"}
|
||||
assert node_by_ct(wf, "Load3DAdvanced")["inputs"]["model_file"] == "mesh.glb"
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Tests de estructura y pureza para comfyui_inject_lora (funcion pura)."""
|
||||
|
||||
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_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
from ml.comfyui_inject_lora import comfyui_inject_lora
|
||||
from _comfyui_wf_assert import assert_api_format, class_types
|
||||
|
||||
|
||||
def _lora_node_id(wf):
|
||||
return next(nid for nid, n in wf.items() if n["class_type"] == "LoraLoader")
|
||||
|
||||
|
||||
def test_no_muta_la_entrada():
|
||||
base = comfyui_build_txt2img_workflow("ck.safetensors", "POS", "NEG")
|
||||
base_antes = {k: dict(v) for k, v in base.items()}
|
||||
_ = comfyui_inject_lora(base, "style.safetensors")
|
||||
# La copia profunda garantiza que el dict original queda intacto (pureza).
|
||||
assert "LoraLoader" not in class_types(base)
|
||||
assert set(base) == set(base_antes)
|
||||
|
||||
|
||||
def test_inserta_loraloader_y_respeta_strength():
|
||||
base = comfyui_build_txt2img_workflow("ck.safetensors", "POS", "NEG")
|
||||
inj = comfyui_inject_lora(base, "style.safetensors", strength_model=0.7, strength_clip=0.4)
|
||||
assert_api_format(inj)
|
||||
assert "LoraLoader" in class_types(inj)
|
||||
lid = _lora_node_id(inj)
|
||||
lora_in = inj[lid]["inputs"]
|
||||
assert lora_in["lora_name"] == "style.safetensors"
|
||||
assert lora_in["strength_model"] == 0.7
|
||||
assert lora_in["strength_clip"] == 0.4
|
||||
|
||||
|
||||
def test_reconecta_el_ksampler_al_lora():
|
||||
base = comfyui_build_txt2img_workflow("ck.safetensors", "POS", "NEG")
|
||||
inj = comfyui_inject_lora(base, "style.safetensors")
|
||||
lid = _lora_node_id(inj)
|
||||
ks = next(n for n in inj.values() if n["class_type"] == "KSampler")
|
||||
# El KSampler debe tomar el modelo del LoraLoader, no ya del checkpoint.
|
||||
assert ks["inputs"]["model"][0] == lid
|
||||
Reference in New Issue
Block a user