diff --git a/docs/capabilities/comfyui-overview.md b/docs/capabilities/comfyui-overview.md index bcf1785e..b53c78d6 100644 --- a/docs/capabilities/comfyui-overview.md +++ b/docs/capabilities/comfyui-overview.md @@ -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_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 - `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`. - 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`. -- 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`. - 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`. ## Librería de grafos en disco diff --git a/docs/capabilities/comfyui.md b/docs/capabilities/comfyui.md index e4dcca83..87ae0499 100644 --- a/docs/capabilities/comfyui.md +++ b/docs/capabilities/comfyui.md @@ -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_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`) 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_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) Combina API + UI: construyes el workflow por API, lo cargas en la UI del usuario, ajustas el diff --git a/python/functions/ml/comfyui_extract_template.md b/python/functions/ml/comfyui_extract_template.md index e88351eb..9f9cfd85 100644 --- a/python/functions/ml/comfyui_extract_template.md +++ b/python/functions/ml/comfyui_extract_template.md @@ -24,9 +24,13 @@ params: - name: server 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." -tested: false -tests: [] -test_file_path: "" +tested: true +tests: + - "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" --- diff --git a/python/functions/ml/comfyui_fetch_output_audio.md b/python/functions/ml/comfyui_fetch_output_audio.md index 21ace1bb..617b839c 100644 --- a/python/functions/ml/comfyui_fetch_output_audio.md +++ b/python/functions/ml/comfyui_fetch_output_audio.md @@ -33,7 +33,7 @@ tests: - "test_find_saveaudiomp3_bajo_audio" - "test_find_prioriza_clave_audio" - "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" --- diff --git a/python/functions/ml/comfyui_list_templates.md b/python/functions/ml/comfyui_list_templates.md index 1155eb05..657bc300 100644 --- a/python/functions/ml/comfyui_list_templates.md +++ b/python/functions/ml/comfyui_list_templates.md @@ -28,9 +28,15 @@ params: - name: limit 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)." -tested: false -tests: [] -test_file_path: "" +tested: true +tests: + - "_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" --- diff --git a/python/functions/ml/tests/test_comfyui_extract_template.py b/python/functions/ml/tests/test_comfyui_extract_template.py new file mode 100644 index 00000000..4fc7321a --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_extract_template.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"] diff --git a/python/functions/ml/tests/test_comfyui_list_templates.py b/python/functions/ml/tests/test_comfyui_list_templates.py new file mode 100644 index 00000000..0be4028d --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_list_templates.py @@ -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"] == []