docs(comfyui): consolidar las 5 funciones nuevas del grupo (tests + capability page)
Higiene del grupo comfyui sobre las 5 funciones de la sesión: comfyui_build_audio_workflow, comfyui_fetch_output_audio, comfyui_build_flux_workflow, comfyui_list_templates, comfyui_extract_template. - Tests nuevos para list_templates y extract_template (lógica pura: localización del intérprete, error-path sin el paquete instalado, contrato del dict; golden condicional con skip si no hay ComfyUI con comfyui-workflow-templates). 10 tests, todos verdes. - comfyui_list_templates.md / comfyui_extract_template.md: tested true + tests + test_file_path. - Fix drift de test_file_path en comfyui_fetch_output_audio.md (apuntaba a un *_test.py inexistente; corregido a tests/test_*.py). Elimina el WARN de fn index. - docs/capabilities/comfyui.md: subsecciones Audio (ACE-Step) y Templates oficiales. - docs/capabilities/comfyui-overview.md: sección 05b audio, fetch_output_audio en Outputs, Templates oficiales en Workflows I/O. (flux ya estaba documentada.) fn index limpio (las 5 sin WARN); sin drift nuevo en fn doctor uses-functions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,10 @@ sus IDs reales cuando se ejecute `fn index`.
|
|||||||
- `comfyui_build_img2vid_workflow_py_ml` (pura) — SVD: condicionamiento por CLIP_VISION (sin prompt de texto).
|
- `comfyui_build_img2vid_workflow_py_ml` (pura) — SVD: condicionamiento por CLIP_VISION (sin prompt de texto).
|
||||||
- `comfyui_build_video_workflow_py_ml` (pura) — txt2video LTX-Video 2B o Wan2.1 1.3B.
|
- `comfyui_build_video_workflow_py_ml` (pura) — txt2video LTX-Video 2B o Wan2.1 1.3B.
|
||||||
|
|
||||||
|
### 05b · audio
|
||||||
|
|
||||||
|
- `comfyui_build_audio_workflow_py_ml` (pura) — txt2audio ACE-Step: TextEncodeAceStepAudio (tags + lyrics) → EmptyAceStepLatentAudio → KSampler → VAEDecodeAudio → SaveAudio(.flac).
|
||||||
|
|
||||||
### 06 · upscale / detail
|
### 06 · upscale / detail
|
||||||
|
|
||||||
- `comfyui_build_upscale_workflow_py_ml` (pura) — ESRGAN (`model`) o reescalado pixel (`latent`).
|
- `comfyui_build_upscale_workflow_py_ml` (pura) — ESRGAN (`model`) o reescalado pixel (`latent`).
|
||||||
@@ -102,9 +106,10 @@ sus IDs reales cuando se ejecute `fn index`.
|
|||||||
- Modelos: `comfyui_download_model_py_ml`, `comfyui_list_installed_models_py_ml`, `comfyui_install_custom_node_py_ml`.
|
- Modelos: `comfyui_download_model_py_ml`, `comfyui_list_installed_models_py_ml`, `comfyui_install_custom_node_py_ml`.
|
||||||
- Ejecución: `comfyui_submit_workflow_py_ml`, `comfyui_wait_result_py_ml`, `comfyui_stream_progress_py_ml`, `comfyui_validate_workflow_py_ml`, `comfyui_object_info_py_ml`.
|
- Ejecución: `comfyui_submit_workflow_py_ml`, `comfyui_wait_result_py_ml`, `comfyui_stream_progress_py_ml`, `comfyui_validate_workflow_py_ml`, `comfyui_object_info_py_ml`.
|
||||||
- Cola: `comfyui_queue_manage_py_ml`, `comfyui_interrupt_queue_py_ml`.
|
- Cola: `comfyui_queue_manage_py_ml`, `comfyui_interrupt_queue_py_ml`.
|
||||||
- Outputs: `comfyui_fetch_output_image_py_ml`, `comfyui_fetch_output_video_py_ml`, `comfyui_fetch_output_mesh_py_ml`.
|
- Outputs: `comfyui_fetch_output_image_py_ml`, `comfyui_fetch_output_video_py_ml`, `comfyui_fetch_output_mesh_py_ml`, `comfyui_fetch_output_audio_py_ml`.
|
||||||
- Barridos: `comfyui_batch_generate_py_ml`, `comfyui_build_grid_py_ml`.
|
- Barridos: `comfyui_batch_generate_py_ml`, `comfyui_build_grid_py_ml`.
|
||||||
- Workflows I/O: `comfyui_import_workflow_json_py_ml`, `comfyui_import_workflow_png_py_ml`, `comfyui_read_png_metadata_py_ml`, `comfyui_download_workflow_py_ml`, `comfyui_run_foreign_workflow_oneshot_py_pipelines`.
|
- Workflows I/O: `comfyui_import_workflow_json_py_ml`, `comfyui_import_workflow_png_py_ml`, `comfyui_read_png_metadata_py_ml`, `comfyui_download_workflow_py_ml`, `comfyui_run_foreign_workflow_oneshot_py_pipelines`.
|
||||||
|
- Templates oficiales (paquete `comfyui-workflow-templates`): `comfyui_list_templates_py_ml`, `comfyui_extract_template_py_ml`.
|
||||||
- UI vía CDP: `comfyui_load_workflow_ui_py_browser`, `comfyui_export_workflow_ui_py_browser`, `comfyui_queue_prompt_ui_py_browser`, `comfyui_clear_node_outputs_ui_py_browser`.
|
- UI vía CDP: `comfyui_load_workflow_ui_py_browser`, `comfyui_export_workflow_ui_py_browser`, `comfyui_queue_prompt_ui_py_browser`, `comfyui_clear_node_outputs_ui_py_browser`.
|
||||||
|
|
||||||
## Librería de grafos en disco
|
## Librería de grafos en disco
|
||||||
|
|||||||
@@ -142,6 +142,19 @@ canónica). El resultado es un `.mp4` vía `CreateVideo → SaveVideo`.
|
|||||||
| [comfyui_build_video_workflow_py_ml](../../python/functions/ml/comfyui_build_video_workflow.md) | `build_video_workflow(prompt, *, model='ltx', negative='', width=512, height=320, num_frames=65, steps=20, seed=0, fps=24) -> dict` | Builder txt2video para LTX-Video 2B (`model='ltx'`, 12 nodos LTXV*) o Wan2.1 1.3B (`model='wan'`, UNETLoader+VAELoader+ModelSamplingSD3). Nombres de modelo reales, defaults conservadores 8 GB. **Pura**. |
|
| [comfyui_build_video_workflow_py_ml](../../python/functions/ml/comfyui_build_video_workflow.md) | `build_video_workflow(prompt, *, model='ltx', negative='', width=512, height=320, num_frames=65, steps=20, seed=0, fps=24) -> dict` | Builder txt2video para LTX-Video 2B (`model='ltx'`, 12 nodos LTXV*) o Wan2.1 1.3B (`model='wan'`, UNETLoader+VAELoader+ModelSamplingSD3). Nombres de modelo reales, defaults conservadores 8 GB. **Pura**. |
|
||||||
| [comfyui_build_img2vid_workflow_py_ml](../../python/functions/ml/comfyui_build_img2vid_workflow.md) | `build_img2vid_workflow(image, *, ckpt='svd.safetensors', width=1024, height=576, video_frames=14, motion_bucket_id=127, fps=6, augmentation_level=0.0, steps=20, cfg=2.5, min_cfg=1.0, seed=0, sampler_name='euler', scheduler='karras', filename_prefix='comfy_svd') -> dict` | Builder img2vid (Stable Video Diffusion): anima una imagen estática a clip corto. ImageOnlyCheckpointLoader(`svd.safetensors`, todo-en-uno) + LoadImage → SVD_img2vid_Conditioning → VideoLinearCFGGuidance → KSampler (denoise 1.0) → VAEDecode → SaveAnimatedWEBP. SVD no usa prompt de texto: condiciona por CLIP_VISION de la imagen; movimiento vía `motion_bucket_id`. **Pura**. |
|
| [comfyui_build_img2vid_workflow_py_ml](../../python/functions/ml/comfyui_build_img2vid_workflow.md) | `build_img2vid_workflow(image, *, ckpt='svd.safetensors', width=1024, height=576, video_frames=14, motion_bucket_id=127, fps=6, augmentation_level=0.0, steps=20, cfg=2.5, min_cfg=1.0, seed=0, sampler_name='euler', scheduler='karras', filename_prefix='comfy_svd') -> dict` | Builder img2vid (Stable Video Diffusion): anima una imagen estática a clip corto. ImageOnlyCheckpointLoader(`svd.safetensors`, todo-en-uno) + LoadImage → SVD_img2vid_Conditioning → VideoLinearCFGGuidance → KSampler (denoise 1.0) → VAEDecode → SaveAnimatedWEBP. SVD no usa prompt de texto: condiciona por CLIP_VISION de la imagen; movimiento vía `motion_bucket_id`. **Pura**. |
|
||||||
|
|
||||||
|
### Audio (txt2audio, ACE-Step) — dominio `ml` (tag `audio-generation`)
|
||||||
|
|
||||||
|
ComfyUI ≥ 0.26.0 trae nodos de **audio nativos**. `build_audio_workflow` cubre **ACE-Step v1**
|
||||||
|
(`AUDIO_ace_step_v1_3.5b.safetensors`, Apache 2.0): música y SFX por texto, con `lyrics` opcional
|
||||||
|
para voz cantada. El resultado es un `.flac` vía `VAEDecodeAudio → SaveAudio`, que `fetch_output_audio`
|
||||||
|
localiza y baja a disco (los nodos de audio exponen su salida bajo la clave `"audio"` de `/history`,
|
||||||
|
no `"images"`).
|
||||||
|
|
||||||
|
| ID | Firma corta | Qué hace |
|
||||||
|
|---|---|---|
|
||||||
|
| [comfyui_build_audio_workflow_py_ml](../../python/functions/ml/comfyui_build_audio_workflow.md) | `build_audio_workflow(ckpt_name, prompt, *, lyrics='', seconds=10.0, seed=0, steps=50, cfg=5.0, sampler_name='euler', scheduler='simple', shift=5.0, lyrics_strength=1.0, filename_prefix='audio/comfy_audio') -> dict` | Builder **txt2audio (ACE-Step)** en API format: CheckpointLoaderSimple → TextEncodeAceStepAudio (tags=prompt + lyrics) como positive + ConditioningZeroOut como negative + EmptyAceStepLatentAudio(seconds) → ModelSamplingSD3(shift) → KSampler → VAEDecodeAudio → SaveAudio(.flac). La guía va por `cfg`; `lyrics` opcional para voz cantada. **Pura**. |
|
||||||
|
| [comfyui_fetch_output_audio_py_ml](../../python/functions/ml/comfyui_fetch_output_audio.md) | `fetch_output_audio(prompt_id, *, server='127.0.0.1:8188', dest=None, outputs=None, timeout=120.0) -> dict` | Localiza y descarga el output de **audio** (`.flac`/`.wav`/`.mp3`/`.opus`/`.ogg`/`.m4a`) de `/history` vía GET `/view`. Cubre SaveAudio/SaveAudioMP3/Opus/Advanced (bajo la clave `"audio"`). Hermana de `fetch_output_image`/`video`/`mesh`. Acepta `outputs=` de `wait_result` para no re-consultar `/history`. Impura. |
|
||||||
|
|
||||||
### Imagen → 3D (Hunyuan3D-2 nativo) — dominio `ml` + `pipelines` (tag `img-to-3d`)
|
### Imagen → 3D (Hunyuan3D-2 nativo) — dominio `ml` + `pipelines` (tag `img-to-3d`)
|
||||||
|
|
||||||
ComfyUI ≥ 0.26.0 trae **soporte nativo de Hunyuan3D-2** (sin custom node): una imagen se
|
ComfyUI ≥ 0.26.0 trae **soporte nativo de Hunyuan3D-2** (sin custom node): una imagen se
|
||||||
@@ -179,6 +192,21 @@ report `0079`).
|
|||||||
| [comfyui_export_workflow_ui_py_browser](../../python/functions/browser/comfyui_export_workflow_ui.md) | `export_workflow_ui(*, port, server_url_substr, api_format=True, save_path, timeout_s) -> dict` | Exporta el grafo actual: API format (`graphToPrompt().output`) o UI graph (`graph.serialize()`); opcional a disco. Impura. |
|
| [comfyui_export_workflow_ui_py_browser](../../python/functions/browser/comfyui_export_workflow_ui.md) | `export_workflow_ui(*, port, server_url_substr, api_format=True, save_path, timeout_s) -> dict` | Exporta el grafo actual: API format (`graphToPrompt().output`) o UI graph (`graph.serialize()`); opcional a disco. Impura. |
|
||||||
| [comfyui_refresh_nodes_ui_py_browser](../../python/functions/browser/comfyui_refresh_nodes_ui.md) | `refresh_nodes_ui(*, port, server_url_substr, timeout_s) -> dict` | Refresca los combos (checkpoints/loras/vae) sin recargar la página (`app.refreshComboInNodes`). Impura. |
|
| [comfyui_refresh_nodes_ui_py_browser](../../python/functions/browser/comfyui_refresh_nodes_ui.md) | `refresh_nodes_ui(*, port, server_url_substr, timeout_s) -> dict` | Refresca los combos (checkpoints/loras/vae) sin recargar la página (`app.refreshComboInNodes`). Impura. |
|
||||||
|
|
||||||
|
### Templates oficiales — dominio `ml` (tag `templates`)
|
||||||
|
|
||||||
|
Los workflows del menú **"Browse Templates"** del frontend se distribuyen en el paquete pip
|
||||||
|
`comfyui-workflow-templates` (desde la 0.10.x un meta-paquete multi-bundle con API en
|
||||||
|
`comfyui_workflow_templates_core`). Estas dos funciones leen ese catálogo localizando el intérprete
|
||||||
|
de ComfyUI y usando su API oficial vía subprocess (el paquete vive en el venv de ComfyUI, no en el
|
||||||
|
del registry). Sirven para descubrir grafos oficiales y arrancar un workflow desde una plantilla
|
||||||
|
probada en vez de construirlo a mano. Si no hay un ComfyUI con el paquete, devuelven `ok=False` con
|
||||||
|
un error accionable, sin lanzar.
|
||||||
|
|
||||||
|
| ID | Firma corta | Qué hace |
|
||||||
|
|---|---|---|
|
||||||
|
| [comfyui_list_templates_py_ml](../../python/functions/ml/comfyui_list_templates.md) | `list_templates(comfyui_python=None, bundle=None, name_filter=None, with_nodes=True, workflows_only=True, limit=0) -> dict` | Lista los templates oficiales con su grafo: nombre, bundle/categoría, path en disco, `n_nodes` y `node_types` (class_types reales, aplanando subgrafos y descartando UUID de instancia). Filtra por bundle/nombre; excluye entradas no-workflow por defecto. Impura (lee disco vía el intérprete de ComfyUI). |
|
||||||
|
| [comfyui_extract_template_py_ml](../../python/functions/ml/comfyui_extract_template.md) | `extract_template(name, comfyui_python=None, to_api=False, server='127.0.0.1:8188') -> dict` | Extrae el grafo completo (formato UI) + `class_types` de un template por su `template_id`. `to_api=True` lo convierte a API format vía `comfyui_import_workflow_json` (requiere servidor ComfyUI vivo). Nombre inexistente → `ok=False` con sugerencias cercanas, sin traceback. Impura. |
|
||||||
|
|
||||||
## Ejemplo canónico end-to-end (build → load → tune → queue → resultado)
|
## Ejemplo canónico end-to-end (build → load → tune → queue → resultado)
|
||||||
|
|
||||||
Combina API + UI: construyes el workflow por API, lo cargas en la UI del usuario, ajustas el
|
Combina API + UI: construyes el workflow por API, lo cargas en la UI del usuario, ajustas el
|
||||||
|
|||||||
@@ -24,9 +24,13 @@ params:
|
|||||||
- name: server
|
- name: server
|
||||||
desc: "host:port del servidor ComfyUI usado para la conversion to_api (default '127.0.0.1:8188')."
|
desc: "host:port del servidor ComfyUI usado para la conversion to_api (default '127.0.0.1:8188')."
|
||||||
output: "dict {ok, name, format, class_types, has_subgraphs, n_nodes, graph, api_workflow, api_error, bundle, version, assets, error}. graph = dict del template (formato UI o API). class_types = lista ordenada de tipos de nodo reales. api_workflow = dict API si to_api tuvo exito, si no {}. Nunca lanza: nombre inexistente -> ok=False con error + sugerencias."
|
output: "dict {ok, name, format, class_types, has_subgraphs, n_nodes, graph, api_workflow, api_error, bundle, version, assets, error}. graph = dict del template (formato UI o API). class_types = lista ordenada de tipos de nodo reales. api_workflow = dict API si to_api tuvo exito, si no {}. Nunca lanza: nombre inexistente -> ok=False con error + sugerencias."
|
||||||
tested: false
|
tested: true
|
||||||
tests: []
|
tests:
|
||||||
test_file_path: ""
|
- "sin el paquete instalado -> ok=False con error que menciona comfyui-workflow-templates"
|
||||||
|
- "el nombre pedido se preserva y el dict trae todas sus claves aun en fallo"
|
||||||
|
- "golden (skip si no hay ComfyUI con el paquete): extrae un template real con graph + class_types no vacios"
|
||||||
|
- "golden (skip si no hay ComfyUI con el paquete): nombre inexistente -> ok=False con error legible"
|
||||||
|
test_file_path: "python/functions/ml/tests/test_comfyui_extract_template.py"
|
||||||
file_path: "python/functions/ml/comfyui_extract_template.py"
|
file_path: "python/functions/ml/comfyui_extract_template.py"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ tests:
|
|||||||
- "test_find_saveaudiomp3_bajo_audio"
|
- "test_find_saveaudiomp3_bajo_audio"
|
||||||
- "test_find_prioriza_clave_audio"
|
- "test_find_prioriza_clave_audio"
|
||||||
- "test_find_sin_audio_devuelve_none"
|
- "test_find_sin_audio_devuelve_none"
|
||||||
test_file_path: "python/functions/ml/comfyui_fetch_output_audio_test.py"
|
test_file_path: "python/functions/ml/tests/test_comfyui_fetch_output_audio.py"
|
||||||
file_path: "python/functions/ml/comfyui_fetch_output_audio.py"
|
file_path: "python/functions/ml/comfyui_fetch_output_audio.py"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,15 @@ params:
|
|||||||
- name: limit
|
- name: limit
|
||||||
desc: "Si > 0, trunca a los primeros N templates tras filtrar y ordenar por nombre."
|
desc: "Si > 0, trunca a los primeros N templates tras filtrar y ordenar por nombre."
|
||||||
output: "dict {ok: bool, count: int, package_version: str, templates: list, error: str}. Cada template: {name, category, bundle, version, path, n_nodes, node_types, is_workflow}. Nunca lanza: paquete ausente o interprete no hallado -> ok=False con error legible que indica como instalar (pip install comfyui-workflow-templates)."
|
output: "dict {ok: bool, count: int, package_version: str, templates: list, error: str}. Cada template: {name, category, bundle, version, path, n_nodes, node_types, is_workflow}. Nunca lanza: paquete ausente o interprete no hallado -> ok=False con error legible que indica como instalar (pip install comfyui-workflow-templates)."
|
||||||
tested: false
|
tested: true
|
||||||
tests: []
|
tests:
|
||||||
test_file_path: ""
|
- "_find_comfyui_python: interprete existente se devuelve tal cual"
|
||||||
|
- "_find_comfyui_python: ruta inexistente cae al fallback (sys.executable)"
|
||||||
|
- "sin el paquete instalado -> ok=False con error que menciona comfyui-workflow-templates"
|
||||||
|
- "el dict de retorno conserva todas sus claves aun en fallo"
|
||||||
|
- "golden (skip si no hay ComfyUI con el paquete): catalogo no vacio, cada template con name+bundle"
|
||||||
|
- "golden (skip si no hay ComfyUI con el paquete): bundle inexistente filtra a lista vacia con ok=True"
|
||||||
|
test_file_path: "python/functions/ml/tests/test_comfyui_list_templates.py"
|
||||||
file_path: "python/functions/ml/comfyui_list_templates.py"
|
file_path: "python/functions/ml/comfyui_list_templates.py"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""Tests para comfyui_extract_template.
|
||||||
|
|
||||||
|
Cubre, sin tocar red ni GPU:
|
||||||
|
|
||||||
|
- El camino de error legible cuando el paquete `comfyui-workflow-templates` no
|
||||||
|
esta instalado: subprocess local contra el python del venv del registry (que no
|
||||||
|
lo tiene) -> `ok=False` con mensaje accionable, sin lanzar.
|
||||||
|
- El contrato del dict de retorno (claves presentes, nombre preservado) aun en
|
||||||
|
fallo.
|
||||||
|
|
||||||
|
El golden path (extraer un template real con sus class_types) y el error
|
||||||
|
'template inexistente -> sugerencias' solo se ejecutan si hay un ComfyUI con el
|
||||||
|
paquete instalado; si no, se omiten con `pytest.skip`. Nunca dependen de GPU ni
|
||||||
|
de un servidor ComfyUI vivo (la conversion to_api, que si necesita servidor, no
|
||||||
|
se ejercita aqui).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
from comfyui_extract_template import _find_comfyui_python, comfyui_extract_template
|
||||||
|
|
||||||
|
_PKG = "comfyui_workflow_templates_core"
|
||||||
|
_RET_KEYS = {
|
||||||
|
"ok", "name", "format", "class_types", "has_subgraphs", "n_nodes",
|
||||||
|
"graph", "api_workflow", "api_error", "bundle", "version", "assets", "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _python_con_paquete():
|
||||||
|
"""Devuelve un interprete que importa el paquete, o None (para omitir el golden)."""
|
||||||
|
py = _find_comfyui_python(None)
|
||||||
|
if not py:
|
||||||
|
return None
|
||||||
|
r = subprocess.run([py, "-c", f"import {_PKG}"], capture_output=True)
|
||||||
|
return py if r.returncode == 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_sin_paquete_error_legible():
|
||||||
|
# El venv del registry no tiene el paquete -> ok=False con error que lo menciona.
|
||||||
|
res = comfyui_extract_template("image_sdxl", comfyui_python=sys.executable)
|
||||||
|
assert res["ok"] is False
|
||||||
|
assert res["graph"] == {}
|
||||||
|
assert res["class_types"] == []
|
||||||
|
assert "comfyui-workflow-templates" in res["error"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_preserva_nombre_y_claves():
|
||||||
|
# El nombre pedido se preserva y el dict trae siempre todas sus claves.
|
||||||
|
res = comfyui_extract_template("cualquier_nombre", comfyui_python=sys.executable)
|
||||||
|
assert res["name"] == "cualquier_nombre"
|
||||||
|
assert _RET_KEYS <= set(res)
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_golden_template_real():
|
||||||
|
py = _python_con_paquete()
|
||||||
|
if not py:
|
||||||
|
pytest.skip("no hay ComfyUI con comfyui-workflow-templates instalado")
|
||||||
|
# Toma el primer template real del catalogo y extraelo (to_api=False: sin servidor).
|
||||||
|
from comfyui_list_templates import comfyui_list_templates
|
||||||
|
|
||||||
|
cat = comfyui_list_templates(comfyui_python=py, with_nodes=False, limit=1)
|
||||||
|
assert cat["ok"] and cat["count"] >= 1
|
||||||
|
name = cat["templates"][0]["name"]
|
||||||
|
|
||||||
|
res = comfyui_extract_template(name, comfyui_python=py)
|
||||||
|
assert res["ok"] is True
|
||||||
|
assert res["name"] == name
|
||||||
|
assert isinstance(res["graph"], dict) and res["graph"]
|
||||||
|
assert len(res["class_types"]) > 0
|
||||||
|
assert res["format"] in ("ui_graph", "api")
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_nombre_inexistente_error_con_sugerencias():
|
||||||
|
py = _python_con_paquete()
|
||||||
|
if not py:
|
||||||
|
pytest.skip("no hay ComfyUI con comfyui-workflow-templates instalado")
|
||||||
|
res = comfyui_extract_template(
|
||||||
|
"zzz_template_que_no_existe_jamas", comfyui_python=py
|
||||||
|
)
|
||||||
|
assert res["ok"] is False
|
||||||
|
assert "no existe" in res["error"]
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Tests para comfyui_list_templates.
|
||||||
|
|
||||||
|
Cubre dos cosas sin tocar red ni GPU:
|
||||||
|
|
||||||
|
- La localizacion del interprete (`_find_comfyui_python`), que solo consulta el
|
||||||
|
sistema de ficheros.
|
||||||
|
- El camino de error legible cuando el paquete `comfyui-workflow-templates` no
|
||||||
|
esta instalado: se ejecuta un subprocess local contra el python indicado (el
|
||||||
|
del propio venv del registry, que no tiene el paquete) y se comprueba que la
|
||||||
|
funcion devuelve `ok=False` con un mensaje accionable, sin lanzar.
|
||||||
|
|
||||||
|
El golden path (catalogo de templates no vacio) y un edge de filtrado solo se
|
||||||
|
ejecutan si hay un ComfyUI con el paquete instalado; si no, se omiten con
|
||||||
|
`pytest.skip`. Nunca dependen de GPU ni de un servidor ComfyUI vivo.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
from comfyui_list_templates import _find_comfyui_python, comfyui_list_templates
|
||||||
|
|
||||||
|
_PKG = "comfyui_workflow_templates_core"
|
||||||
|
_RET_KEYS = {"ok", "count", "package_version", "templates", "error"}
|
||||||
|
|
||||||
|
|
||||||
|
def _python_con_paquete():
|
||||||
|
"""Devuelve un interprete que importa el paquete, o None (para omitir el golden)."""
|
||||||
|
py = _find_comfyui_python(None)
|
||||||
|
if not py:
|
||||||
|
return None
|
||||||
|
r = subprocess.run([py, "-c", f"import {_PKG}"], capture_output=True)
|
||||||
|
return py if r.returncode == 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_comfyui_python_explicit_valido():
|
||||||
|
# Un interprete que existe en disco se devuelve tal cual.
|
||||||
|
assert _find_comfyui_python(sys.executable) == sys.executable
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_comfyui_python_inexistente_cae_a_fallback():
|
||||||
|
# Una ruta inexistente no rompe: cae al siguiente candidato (sys.executable existe).
|
||||||
|
got = _find_comfyui_python("/ruta/que/no/existe/python")
|
||||||
|
assert got is not None and os.path.isfile(got)
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_sin_paquete_error_legible():
|
||||||
|
# El venv del registry no tiene el paquete -> ok=False con error que lo menciona.
|
||||||
|
res = comfyui_list_templates(comfyui_python=sys.executable)
|
||||||
|
assert res["ok"] is False
|
||||||
|
assert res["count"] == 0
|
||||||
|
assert res["templates"] == []
|
||||||
|
assert "comfyui-workflow-templates" in res["error"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_retorno_tiene_todas_las_claves():
|
||||||
|
# El contrato del dict de retorno se mantiene aun en fallo.
|
||||||
|
res = comfyui_list_templates(comfyui_python=sys.executable)
|
||||||
|
assert _RET_KEYS <= set(res)
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_golden_catalogo_no_vacio():
|
||||||
|
py = _python_con_paquete()
|
||||||
|
if not py:
|
||||||
|
pytest.skip("no hay ComfyUI con comfyui-workflow-templates instalado")
|
||||||
|
res = comfyui_list_templates(comfyui_python=py, with_nodes=False, limit=5)
|
||||||
|
assert res["ok"] is True
|
||||||
|
assert res["count"] > 0
|
||||||
|
assert len(res["templates"]) == res["count"]
|
||||||
|
# Cada template trae al menos nombre y bundle.
|
||||||
|
for t in res["templates"]:
|
||||||
|
assert t.get("name")
|
||||||
|
assert t.get("bundle")
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_golden_filtro_bundle_inexistente_vacio():
|
||||||
|
py = _python_con_paquete()
|
||||||
|
if not py:
|
||||||
|
pytest.skip("no hay ComfyUI con comfyui-workflow-templates instalado")
|
||||||
|
# Un bundle que no existe filtra a una lista vacia pero la llamada sigue siendo ok.
|
||||||
|
res = comfyui_list_templates(comfyui_python=py, bundle="bundle-inexistente-xyz")
|
||||||
|
assert res["ok"] is True
|
||||||
|
assert res["count"] == 0
|
||||||
|
assert res["templates"] == []
|
||||||
Reference in New Issue
Block a user