diff --git a/docs/capabilities/comfyui.md b/docs/capabilities/comfyui.md index 4e1c7d3b..bd915f1c 100644 --- a/docs/capabilities/comfyui.md +++ b/docs/capabilities/comfyui.md @@ -183,6 +183,11 @@ Para tunear nodo a nodo en vez del oneshot: `build_image_to_3d_workflow(image_na `import_workflow_json`/`import_workflow_png`, se resuelven sus dependencias con `resolve_workflow_deps` (instala nodos con `install_custom_node`, descubre modelos con `search_civitai_models`) y se validan con `validate_workflow` antes de encolar. +- **Los 9 builders puros tienen tests de estructura** (`python/functions/ml/tests/test_comfyui_build_*.py` + + `test_comfyui_inject_lora.py`): verifican los `class_type` esperados, que los parámetros se reflejan + en los nodos, la validez de las conexiones `[node_id, output_index]` y la pureza de `inject_lora`. Son + tests offline (no tocan GPU ni server); las funciones impuras del grupo (todo lo que habla con el server, + el navegador o Civitai/HuggingFace) no se cubren con unit tests por diseño — se validan con el server vivo. - **Las funciones `*_ui` requieren la pestaña abierta y el navegador con CDP** (puerto 9222 por defecto). Sin target que matchee `server_url_substr`, devuelven `ok=False`. Para automatización desatendida sin navegador, usa el camino API (`submit_workflow` + `wait_result`). diff --git a/python/functions/ml/comfyui_build_controlnet_workflow.md b/python/functions/ml/comfyui_build_controlnet_workflow.md index a27de1ab..416b3125 100644 --- a/python/functions/ml/comfyui_build_controlnet_workflow.md +++ b/python/functions/ml/comfyui_build_controlnet_workflow.md @@ -38,9 +38,9 @@ params: - name: height desc: "Alto del latente/imagen en px (multiplo de 8). keyword-only." output: "dict en API format con node_ids como claves (CheckpointLoaderSimple '4', EmptyLatentImage '5', LoadImage '10', ControlNetLoader '12', CLIPTextEncode '6'/'7', ControlNetApply '13', KSampler '3', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow." -tested: false -tests: [] -test_file_path: "" +tested: true +tests: ["usa ControlNetLoader+ControlNetApply", "control_image, modelo cn y strength reflejados"] +test_file_path: "python/functions/ml/tests/test_comfyui_build_controlnet_workflow.py" file_path: "python/functions/ml/comfyui_build_controlnet_workflow.py" --- diff --git a/python/functions/ml/comfyui_build_image_to_3d_workflow.md b/python/functions/ml/comfyui_build_image_to_3d_workflow.md index a2f66029..d8c16823 100644 --- a/python/functions/ml/comfyui_build_image_to_3d_workflow.md +++ b/python/functions/ml/comfyui_build_image_to_3d_workflow.md @@ -40,9 +40,9 @@ params: - name: filename_prefix desc: "Prefijo del archivo de malla que SaveGLB escribe en output/ (ej. '3d_mesh' -> '3d_mesh_00001_.glb'). keyword-only." output: "dict en API format con node_ids '1'..'9' como claves; cada valor tiene class_type + inputs. Listo para comfyui_submit_workflow. El nodo '9' (SaveGLB) produce el archivo .glb en el output del servidor." -tested: false -tests: [] -test_file_path: "" +tested: true +tests: ["cadena de 9 nodos Hunyuan3D-2 nativos", "imagen, checkpoint, seed reflejados y SaveGLB presente"] +test_file_path: "python/functions/ml/tests/test_comfyui_build_image_to_3d_workflow.py" file_path: "python/functions/ml/comfyui_build_image_to_3d_workflow.py" --- diff --git a/python/functions/ml/comfyui_build_img2img_workflow.md b/python/functions/ml/comfyui_build_img2img_workflow.md index d54bdd51..78a724f3 100644 --- a/python/functions/ml/comfyui_build_img2img_workflow.md +++ b/python/functions/ml/comfyui_build_img2img_workflow.md @@ -36,9 +36,9 @@ params: - name: scheduler desc: "Scheduler del sampler (ej. 'normal', 'karras'). keyword-only." output: "dict en API format con node_ids como claves (CheckpointLoaderSimple '4', LoadImage '10', VAEEncode '11', CLIPTextEncode '6'/'7', KSampler '3', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow." -tested: false -tests: [] -test_file_path: "" +tested: true +tests: ["usa VAEEncode/LoadImage y no EmptyLatentImage", "denoise e init_image reflejados"] +test_file_path: "python/functions/ml/tests/test_comfyui_build_img2img_workflow.py" file_path: "python/functions/ml/comfyui_build_img2img_workflow.py" --- diff --git a/python/functions/ml/comfyui_build_inpaint_workflow.md b/python/functions/ml/comfyui_build_inpaint_workflow.md index da2c1946..effc3b94 100644 --- a/python/functions/ml/comfyui_build_inpaint_workflow.md +++ b/python/functions/ml/comfyui_build_inpaint_workflow.md @@ -38,9 +38,9 @@ params: - name: scheduler desc: "Scheduler del sampler (ej. 'normal', 'karras'). keyword-only." output: "dict en API format con node_ids como claves (CheckpointLoaderSimple '4', LoadImage '10', LoadImageMask '12', VAEEncodeForInpaint '11', CLIPTextEncode '6'/'7', KSampler '3', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow." -tested: false -tests: [] -test_file_path: "" +tested: true +tests: ["usa LoadImageMask+VAEEncodeForInpaint", "imagen base, mascara, seed y denoise reflejados"] +test_file_path: "python/functions/ml/tests/test_comfyui_build_inpaint_workflow.py" file_path: "python/functions/ml/comfyui_build_inpaint_workflow.py" --- diff --git a/python/functions/ml/comfyui_build_sdxl_refiner_workflow.md b/python/functions/ml/comfyui_build_sdxl_refiner_workflow.md index 401441d1..7886d916 100644 --- a/python/functions/ml/comfyui_build_sdxl_refiner_workflow.md +++ b/python/functions/ml/comfyui_build_sdxl_refiner_workflow.md @@ -36,9 +36,9 @@ params: - name: height desc: "Alto del latente/imagen en px (SDXL nativo 1024). keyword-only." output: "dict en API format con node_ids como claves (CheckpointLoaderSimple base '4' y refiner '14', EmptyLatentImage '5', CLIPTextEncode base '6'/'7' y refiner '16'/'17', KSamplerAdvanced base '3' y refiner '15', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow." -tested: false -tests: [] -test_file_path: "" +tested: true +tests: ["dos KSamplerAdvanced encadenados", "base emite ruido sobrante y refiner lo recoge (start/end_at_step compartidos)"] +test_file_path: "python/functions/ml/tests/test_comfyui_build_sdxl_refiner_workflow.py" file_path: "python/functions/ml/comfyui_build_sdxl_refiner_workflow.py" --- diff --git a/python/functions/ml/comfyui_build_txt2img_workflow.md b/python/functions/ml/comfyui_build_txt2img_workflow.md index 48dc3247..7c2eb281 100644 --- a/python/functions/ml/comfyui_build_txt2img_workflow.md +++ b/python/functions/ml/comfyui_build_txt2img_workflow.md @@ -38,9 +38,9 @@ params: - name: filename_prefix desc: "Prefijo del PNG que SaveImage escribe en output/. keyword-only." output: "dict en API format con node_ids '3'..'9' como claves; cada valor tiene class_type + inputs. Listo para comfyui_submit_workflow." -tested: false -tests: [] -test_file_path: "" +tested: true +tests: ["class_types esperados (6 nodos)", "params seed/steps/cfg/width/height reflejados", "filename_prefix en SaveImage"] +test_file_path: "python/functions/ml/tests/test_comfyui_build_txt2img_workflow.py" file_path: "python/functions/ml/comfyui_build_txt2img_workflow.py" --- diff --git a/python/functions/ml/comfyui_build_upscale_workflow.md b/python/functions/ml/comfyui_build_upscale_workflow.md index 6e4222b0..d9acad07 100644 --- a/python/functions/ml/comfyui_build_upscale_workflow.md +++ b/python/functions/ml/comfyui_build_upscale_workflow.md @@ -22,9 +22,9 @@ params: - name: method desc: "'model' (ESRGAN via UpscaleModelLoader + ImageUpscaleWithModel) o 'latent' (reescalado de pixel x2 con ImageScaleBy, sin modelo). keyword-only." output: "dict en API format. Con method='model': LoadImage '10' + UpscaleModelLoader '12' + ImageUpscaleWithModel '13' + SaveImage '9'. Con method='latent': LoadImage '10' + ImageScaleBy '13' + SaveImage '9'. Listo para comfyui_submit_workflow." -tested: false -tests: [] -test_file_path: "" +tested: true +tests: ["method='model' usa UpscaleModelLoader+ImageUpscaleWithModel", "method='latent' usa ImageScaleBy sin modelo"] +test_file_path: "python/functions/ml/tests/test_comfyui_build_upscale_workflow.py" file_path: "python/functions/ml/comfyui_build_upscale_workflow.py" --- diff --git a/python/functions/ml/comfyui_build_view_3d_workflow.md b/python/functions/ml/comfyui_build_view_3d_workflow.md index cec51293..1e1512b9 100644 --- a/python/functions/ml/comfyui_build_view_3d_workflow.md +++ b/python/functions/ml/comfyui_build_view_3d_workflow.md @@ -24,9 +24,9 @@ params: - name: height desc: "Alto del viewport del nodo en px. keyword-only." output: "dict en API format con un unico nodo '1'. Con animation=False: class_type 'Load3D', inputs {model_file, image, width, height}. Con animation=True: class_type 'Load3DAdvanced', inputs {model_file, viewport_state, width, height}. Cargable con comfyui_load_workflow_ui (inyecta en la UI del navegador) o POSTeable a /prompt." -tested: false -tests: [] -test_file_path: "" +tested: true +tests: ["Load3D simple con model_file/width/height", "animation=True usa Load3DAdvanced"] +test_file_path: "python/functions/ml/tests/test_comfyui_build_view_3d_workflow.py" file_path: "python/functions/ml/comfyui_build_view_3d_workflow.py" --- diff --git a/python/functions/ml/comfyui_inject_lora.md b/python/functions/ml/comfyui_inject_lora.md index 9c635a23..df98ec1c 100644 --- a/python/functions/ml/comfyui_inject_lora.md +++ b/python/functions/ml/comfyui_inject_lora.md @@ -28,9 +28,9 @@ params: - name: clip_node desc: "node_id cuya salida CLIP (slot 1) alimentara el LoRA. Si None, se detecta la fuente que hoy alimenta los CLIPTextEncode.clip. keyword-only." output: "copia del workflow con un nodo LoraLoader insertado (node_id = max id numerico + 1) y reconectado entre la fuente model/clip y sus consumidores." -tested: false -tests: [] -test_file_path: "" +tested: true +tests: ["no muta el dict de entrada (pureza)", "inserta LoraLoader con strength correcto", "reconecta KSampler.model al LoRA"] +test_file_path: "python/functions/ml/tests/test_comfyui_inject_lora.py" file_path: "python/functions/ml/comfyui_inject_lora.py" --- diff --git a/python/functions/ml/tests/_comfyui_wf_assert.py b/python/functions/ml/tests/_comfyui_wf_assert.py new file mode 100644 index 00000000..7d92a848 --- /dev/null +++ b/python/functions/ml/tests/_comfyui_wf_assert.py @@ -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) diff --git a/python/functions/ml/tests/test_comfyui_build_controlnet_workflow.py b/python/functions/ml/tests/test_comfyui_build_controlnet_workflow.py new file mode 100644 index 00000000..e50c06cf --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_build_controlnet_workflow.py @@ -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 diff --git a/python/functions/ml/tests/test_comfyui_build_image_to_3d_workflow.py b/python/functions/ml/tests/test_comfyui_build_image_to_3d_workflow.py new file mode 100644 index 00000000..6cf4867d --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_build_image_to_3d_workflow.py @@ -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) diff --git a/python/functions/ml/tests/test_comfyui_build_img2img_workflow.py b/python/functions/ml/tests/test_comfyui_build_img2img_workflow.py new file mode 100644 index 00000000..cff8f47e --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_build_img2img_workflow.py @@ -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" diff --git a/python/functions/ml/tests/test_comfyui_build_inpaint_workflow.py b/python/functions/ml/tests/test_comfyui_build_inpaint_workflow.py new file mode 100644 index 00000000..562bbf99 --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_build_inpaint_workflow.py @@ -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 diff --git a/python/functions/ml/tests/test_comfyui_build_sdxl_refiner_workflow.py b/python/functions/ml/tests/test_comfyui_build_sdxl_refiner_workflow.py new file mode 100644 index 00000000..58e0fd4a --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_build_sdxl_refiner_workflow.py @@ -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 diff --git a/python/functions/ml/tests/test_comfyui_build_txt2img_workflow.py b/python/functions/ml/tests/test_comfyui_build_txt2img_workflow.py new file mode 100644 index 00000000..6c4657d3 --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_build_txt2img_workflow.py @@ -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" diff --git a/python/functions/ml/tests/test_comfyui_build_upscale_workflow.py b/python/functions/ml/tests/test_comfyui_build_upscale_workflow.py new file mode 100644 index 00000000..22c59ca1 --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_build_upscale_workflow.py @@ -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" diff --git a/python/functions/ml/tests/test_comfyui_build_view_3d_workflow.py b/python/functions/ml/tests/test_comfyui_build_view_3d_workflow.py new file mode 100644 index 00000000..860cba20 --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_build_view_3d_workflow.py @@ -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" diff --git a/python/functions/ml/tests/test_comfyui_inject_lora.py b/python/functions/ml/tests/test_comfyui_inject_lora.py new file mode 100644 index 00000000..497a63bf --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_inject_lora.py @@ -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