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:
2026-06-28 04:46:47 +02:00
parent a1074d32e7
commit 7d33b39859
7 changed files with 224 additions and 8 deletions
+6 -1
View File
@@ -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
+28
View File
@@ -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"] == []