feat(ml): auto-commit con 6 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 17:47:28 +02:00
parent d5660aa13f
commit 3887e59092
6 changed files with 708 additions and 0 deletions
@@ -0,0 +1,110 @@
"""Tests de estructura, conexiones y validacion para comfyui_build_ipadapter_workflow (pura)."""
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
import pytest
from ml.comfyui_build_ipadapter_workflow import comfyui_build_ipadapter_workflow
from _comfyui_wf_assert import assert_api_format, class_types
def _node(wf, class_type):
return next((n for n in wf.values() if n["class_type"] == class_type), None)
def _node_id(wf, class_type):
return next((nid for nid, n in wf.items() if n["class_type"] == class_type), None)
def test_style_mode_nodos_y_conexiones():
wf = comfyui_build_ipadapter_workflow(
"a castle, oil painting", "ref.png",
base_checkpoint="dreamshaper_8.safetensors", mode="style", weight=0.75,
)
assert_api_format(wf)
cts = class_types(wf)
assert "LoadImage" in cts
assert "IPAdapterUnifiedLoader" in cts
assert "IPAdapter" in cts
ckpt = _node_id(wf, "CheckpointLoaderSimple")
load_id = _node_id(wf, "LoadImage")
loader_id = _node_id(wf, "IPAdapterUnifiedLoader")
apply_node = _node(wf, "IPAdapter")
apply_id = _node_id(wf, "IPAdapter")
# loader toma el MODEL del checkpoint
assert _node(wf, "IPAdapterUnifiedLoader")["inputs"]["model"] == [ckpt, 0]
# el nodo IPAdapter cablea model/ipadapter del loader y la imagen del LoadImage
assert apply_node["inputs"]["model"] == [loader_id, 0]
assert apply_node["inputs"]["ipadapter"] == [loader_id, 1]
assert apply_node["inputs"]["image"] == [load_id, 0]
assert apply_node["inputs"]["weight"] == 0.75
# KSampler repuntado a la salida MODEL del IPAdapter
assert _node(wf, "KSampler")["inputs"]["model"] == [apply_id, 0]
# defaults de modo style
assert _node(wf, "IPAdapterUnifiedLoader")["inputs"]["preset"] == "STANDARD (medium strength)"
assert apply_node["inputs"]["weight_type"] == "standard"
def test_faceid_mode_nodos_y_conexiones():
wf = comfyui_build_ipadapter_workflow(
"a knight portrait", "face.png",
base_checkpoint="dreamshaper_8.safetensors", mode="faceid",
weight=0.9, lora_strength=0.7,
)
assert_api_format(wf)
cts = class_types(wf)
assert "IPAdapterUnifiedLoaderFaceID" in cts
assert "IPAdapterFaceID" in cts
# no debe haber rama style
assert "IPAdapterUnifiedLoader" not in cts
assert "IPAdapter" not in cts
loader = _node(wf, "IPAdapterUnifiedLoaderFaceID")
apply_node = _node(wf, "IPAdapterFaceID")
loader_id = _node_id(wf, "IPAdapterUnifiedLoaderFaceID")
load_id = _node_id(wf, "LoadImage")
apply_id = _node_id(wf, "IPAdapterFaceID")
assert loader["inputs"]["preset"] == "FACEID PLUS V2"
assert loader["inputs"]["lora_strength"] == 0.7
assert loader["inputs"]["provider"] == "CPU"
assert apply_node["inputs"]["model"] == [loader_id, 0]
assert apply_node["inputs"]["ipadapter"] == [loader_id, 1]
assert apply_node["inputs"]["image"] == [load_id, 0]
assert apply_node["inputs"]["weight"] == 0.9
assert apply_node["inputs"]["weight_type"] == "linear"
assert _node(wf, "KSampler")["inputs"]["model"] == [apply_id, 0]
def test_mode_invalido_lanza_valueerror():
with pytest.raises(ValueError):
comfyui_build_ipadapter_workflow(
"x", "ref.png", base_checkpoint="ck.safetensors", mode="bogus")
def test_ref_image_vacia_lanza_valueerror():
with pytest.raises(ValueError):
comfyui_build_ipadapter_workflow(
"x", "", base_checkpoint="ck.safetensors", mode="style")
def test_preset_y_weight_type_override():
wf = comfyui_build_ipadapter_workflow(
"x", "ref.png", base_checkpoint="ck.safetensors", mode="style",
preset="PLUS (high strength)", weight_type="style transfer",
)
assert _node(wf, "IPAdapterUnifiedLoader")["inputs"]["preset"] == "PLUS (high strength)"
assert _node(wf, "IPAdapter")["inputs"]["weight_type"] == "style transfer"
def test_determinista():
kw = dict(base_checkpoint="ck.safetensors", mode="faceid", seed=42)
a = comfyui_build_ipadapter_workflow("x", "ref.png", **kw)
b = comfyui_build_ipadapter_workflow("x", "ref.png", **kw)
assert a == b
@@ -0,0 +1,107 @@
"""Tests de estructura, orden y pureza para comfyui_inject_multi_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__), "..", ".."))
import pytest
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
from ml.comfyui_inject_multi_lora import comfyui_inject_multi_lora
from _comfyui_wf_assert import assert_api_format, class_types
def _lora_nodes(wf):
"""Mapa lora_name -> (node_id, inputs) de los LoraLoader del workflow."""
return {
n["inputs"]["lora_name"]: (nid, n["inputs"])
for nid, n in wf.items()
if n["class_type"] == "LoraLoader"
}
def test_encadena_n_loras():
base = comfyui_build_txt2img_workflow("ck.safetensors", "POS", "NEG")
inj = comfyui_inject_multi_lora(
base,
[
{"name": "a.safetensors", "strength_model": 0.9},
{"name": "b.safetensors", "strength_model": 0.5},
{"name": "c.safetensors", "strength_model": 0.3},
],
)
assert_api_format(inj)
loras = _lora_nodes(inj)
assert set(loras) == {"a.safetensors", "b.safetensors", "c.safetensors"}
def test_orden_y_cadena_validos():
# Cadena esperada: checkpoint -> a -> b -> KSampler/CLIPTextEncode.
base = comfyui_build_txt2img_workflow("ck.safetensors", "POS", "NEG")
inj = comfyui_inject_multi_lora(
base,
[{"name": "a.safetensors"}, {"name": "b.safetensors"}],
)
loras = _lora_nodes(inj)
a_id, a_in = loras["a.safetensors"]
b_id, b_in = loras["b.safetensors"]
ckpt = next(nid for nid, n in inj.items() if n["class_type"] == "CheckpointLoaderSimple")
# 'a' (primer elemento) toma el MODEL del checkpoint.
assert a_in["model"] == [ckpt, 0]
# 'b' (segundo elemento) toma el MODEL de 'a' (salida slot 0).
assert b_in["model"] == [a_id, 0]
# El KSampler queda al final de la cadena: toma el MODEL de 'b'.
ks = next(n for n in inj.values() if n["class_type"] == "KSampler")
assert ks["inputs"]["model"] == [b_id, 0]
# El CLIP tambien se encadena hasta 'b' (salida slot 1).
cte = next(n for n in inj.values() if n["class_type"] == "CLIPTextEncode")
assert cte["inputs"]["clip"] == [b_id, 1]
def test_respeta_pesos_por_posicion():
base = comfyui_build_txt2img_workflow("ck.safetensors", "POS", "NEG")
inj = comfyui_inject_multi_lora(
base,
[
{"name": "a.safetensors", "strength_model": 0.9, "strength_clip": 0.8},
{"name": "b.safetensors", "strength_model": 0.4},
],
)
loras = _lora_nodes(inj)
assert loras["a.safetensors"][1]["strength_model"] == 0.9
assert loras["a.safetensors"][1]["strength_clip"] == 0.8
assert loras["b.safetensors"][1]["strength_model"] == 0.4
# default strength_clip = 1.0 cuando no se especifica
assert loras["b.safetensors"][1]["strength_clip"] == 1.0
def test_no_muta_la_entrada():
base = comfyui_build_txt2img_workflow("ck.safetensors", "POS", "NEG")
claves_antes = set(base)
_ = comfyui_inject_multi_lora(base, [{"name": "a.safetensors"}, {"name": "b.safetensors"}])
assert "LoraLoader" not in class_types(base)
assert set(base) == claves_antes
def test_lista_vacia_devuelve_copia_sin_loras():
base = comfyui_build_txt2img_workflow("ck.safetensors", "POS", "NEG")
inj = comfyui_inject_multi_lora(base, [])
assert "LoraLoader" not in class_types(inj)
assert class_types(inj) == class_types(base)
def test_lora_sin_name_lanza_valueerror():
base = comfyui_build_txt2img_workflow("ck.safetensors", "POS", "NEG")
with pytest.raises(ValueError):
comfyui_inject_multi_lora(base, [{"strength_model": 0.5}])
with pytest.raises(ValueError):
comfyui_inject_multi_lora(base, ["a.safetensors"]) # no es dict
def test_determinista():
base = comfyui_build_txt2img_workflow("ck.safetensors", "POS", "NEG")
spec = [{"name": "a.safetensors"}, {"name": "b.safetensors"}]
assert comfyui_inject_multi_lora(base, spec) == comfyui_inject_multi_lora(base, spec)