Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec0a5e53ac | |||
| 604d3d4feb | |||
| 643ebfb849 | |||
| 537516e32e | |||
| ca07b25297 | |||
| fbbff7d5e7 | |||
| bdd841d9af | |||
| 7d33b39859 | |||
| a1074d32e7 | |||
| fd16453691 | |||
| 5494507c39 | |||
| dfb3eda087 | |||
| 83738d4035 | |||
| b77d223f01 | |||
| e178ab8d2d |
@@ -15,6 +15,10 @@
|
||||
"godot": {
|
||||
"type": "http",
|
||||
"url": "http://127.0.0.1:8000/mcp"
|
||||
},
|
||||
"ardour": {
|
||||
"command": "/home/enmanuel/audio-tools/ardour-mcp/target/release/ardour_mcp_server",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu
|
||||
|
||||
| Grupo | N | Que cubre |
|
||||
|---|---|---|
|
||||
| [gamedev-2d](gamedev-2d.md) | 36 | Assets 2D para Godot via ComfyUI: 31 builders de workflow (pixelart/seamless/iso/sprite/topdown/card/enemy/prop/structure/foliage/trap/projectile/decal/particle/rune/weather/badge/skill-tree/dialogue/icon/portrait/VFX...) + 5 de apoyo: post-proceso (pixelize, luma->alpha) + puente de assets a Godot 4 (.import + reimport headless). Tag canonico `gamedev-2d` (antes `gamedev`, ya unificado) |
|
||||
| [gamedev-2d](gamedev-2d.md) | 47 | Assets 2D para Godot via ComfyUI: 36 builders de workflow (31 de generación desde texto: pixelart/seamless/iso/sprite/topdown/card/enemy/prop/structure/foliage/trap/projectile/decal/particle/rune/weather/badge/skill-tree/dialogue/icon/portrait/VFX... + 5 de transformación desde imagen: asset_variant/sprite_from_sketch/inpaint_asset/outpaint_asset/directional_sprite) + 11 de apoyo: post-proceso (pixelize, luma->alpha, flatten_alpha), puente de assets a Godot 4 (.import + reimport headless), style presets (get/apply_gamedev_style_preset) y pipelines one-shot (asset_pack/character_set/styled_asset). Tag canonico `gamedev-2d` (antes `gamedev`, ya unificado) |
|
||||
| [gamedev-engine](gamedev-engine.md) | 8 | Runtime de juego C++ multiplataforma (PC + WebAssembly): SDL3 + sokol_gfx + miniaudio. Game loop fixed-timestep, camara 2D, input unificado (teclado/gamepad/touch), sprite batch, setup de render/audio y build a wasm. Grupo hermano de `gamedev-2d` (este ejecuta el juego, aquel genera los assets) |
|
||||
| [registry](registry.md) | 17 | Auditoria y monitorizacion del propio registry: copied-code, uses-functions, unused, proposals, telemetria |
|
||||
| [systemd](systemd.md) | 14 | Generar, instalar, restart y status de unit files systemd via SSH (deploys a VPS) |
|
||||
@@ -72,8 +72,9 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu
|
||||
| [seo](seo.md) | 3 | SEO orientado a datos sobre Google Search Console: autenticar con service account (`gsc_auth`), extraer Search Analytics paginado (`pull_gsc_search_analytics`) y el pipeline de ingesta a DuckDB + espejo Postgres para Metabase (`ingest_gsc_search_analytics`). Cadena de ingesta del proyecto `seo_analytics`; alimenta dashboards de striking distance, CTR opportunities y content decay |
|
||||
| [local-hub](local-hub.md) | 4 | Exponer los procesos locales como subdominios `*.localhost` (via Caddy, sin DNS) y reunirlos en una pantalla principal Glance con estado en vivo, refrescada a diario por dag_engine. Descubre servicios (manifiesto + registry), renderiza Caddyfile + config Glance (puras), y el pipeline `refresh_local_hub` regenera+recarga. Fuente de verdad: `apps/local_hub/local_services.yaml` |
|
||||
| [comfyui-judge](comfyui-judge.md) | 4 | Panel multi-juez de calidad de imagen: estético LAION-V2 (`comfyui_score_aesthetic`, 0-10) + fidelidad CLIP prompt↔imagen (`comfyui_score_clip_alignment`, 0-1) + crítica LLM-vision (`comfyui_critique_image_llm`, good/bad). Agregados por voto mayoría en `comfyui_judge_image`. Gate objetivo para tests/DoD y el bucle de mejora de skills ComfyUI; degrada con gracia si un juez cae. Jueces estético/fidelidad por subproceso al venv ComfyUI (torch+open_clip), crítico via claude-direct |
|
||||
| [comfyui](comfyui.md) | 29 | Controlar ComfyUI (Stable Diffusion por grafos) de dos formas: por API HTTP (build_txt2img_workflow puro → submit → wait → object_info; download_model con validación Civitai/HF) y por la UI web vía CDP sobre la pestaña abierta (load_workflow_ui, set_node_widget_ui para tunear prompt/steps/seed en vivo, queue_prompt_ui = botón Queue Prompt, export_workflow_ui, refresh_nodes_ui). El API format es el puente entre ambos caminos. Las funciones de UI componen `cdp_eval`. Incluye imagen→3D nativo (Hunyuan3D-2, tag `img-to-3d`): build_image_to_3d_workflow + fetch_output_mesh + install_3d_model + pipeline image_to_3d_oneshot |
|
||||
| [comfyui-skill](comfyui-skill.md) | 11 | Tratar una configuración de generación ComfyUI como una skill: receta versionada en disco (checkpoint + LoRAs + params + scaffold de prompt + post-proceso) que se compila a un workflow cambiando solo el subject. Save/load/list de recetas, bucle de mejora genera→juzga→bump con gate objetivo (el score del juez decide qué se promueve), export de la skill a grafo cargable en el navegador, y cosecha de Civitai (extract_recipe_from_png + harvest oneshot) que destila el workflow embebido de una imagen pública en una skill candidata |
|
||||
| [comfyui](comfyui.md) | 126 | Controlar ComfyUI (Stable Diffusion por grafos) de dos formas: por API HTTP (build_txt2img_workflow puro → submit → wait → object_info; download_model con validación Civitai/HF) y por la UI web vía CDP sobre la pestaña abierta (load_workflow_ui, set_node_widget_ui para tunear prompt/steps/seed en vivo, queue_prompt_ui = botón Queue Prompt, export_workflow_ui, refresh_nodes_ui). El API format es el puente entre ambos caminos. Las funciones de UI componen `cdp_eval`. Cubre txt2img/img2img/inpaint/controlnet/sdxl-refiner/flux, upscale + hires-fix + facedetailer, vídeo (LTX/Wan/SVD), audio (ACE-Step), imagen→3D nativo (Hunyuan3D-2) + post-proceso de malla, templates oficiales, civitai harvest y control de cola. N = funciones con tag `comfyui` (incluye los sub-grupos `comfyui-skill`/`comfyui-styles` y 45 de `gamedev-2d`); las páginas madre de cada sub-grupo desglosan su parte |
|
||||
| [comfyui-skill](comfyui-skill.md) | 17 | Tratar una configuración de generación ComfyUI como una skill: receta versionada en disco (checkpoint + LoRAs + params + scaffold de prompt + post-proceso) que se compila a un workflow cambiando solo el subject. Save/load/list de recetas, bucle de mejora genera→juzga→bump con gate objetivo (el score del juez decide qué se promueve), export de la skill a grafo cargable en el navegador, y cosecha de Civitai (extract_recipe_from_png + harvest oneshot) que destila el workflow embebido de una imagen pública en una skill candidata |
|
||||
| [comfyui-styles](comfyui-styles.md) | 5 | Capa de estilo reutilizable sobre los builders ComfyUI. Catálogo WAS (tag `comfyui-styles`): `curated_styles_catalog` (~190 estilos), `generate_styles_llm` (genera estilos por LLM via ask_llm), `append_styles` (merge+dedup+backup sobre el styles.json del selector WAS). Style presets gamedev (tag `gamedev-2d`): `get_gamedev_style_preset` (gameboy/ghibli/pixel-art-retro como datos puros) + `apply_style_preset` (preset+subject → kwargs de un builder gamedev-2d). El estilo se trata como dato curado, no como prompt repetido |
|
||||
| [comfyui-overview](comfyui-overview.md) | — | Mapa cross-grupo de las capacidades de generación ComfyUI (txt2img, img2img/inpaint, controlnet, skills/multiestilo-LoRA, video, upscale/detail, 3D, juez, operación): cada capacidad → builders/pipelines del registry + grafos UI + skills que la cubren. Índice de entrada al stack ComfyUI; las firmas y gotchas viven en `comfyui.md`/`comfyui-skill.md`/`comfyui-judge.md`. Catálogo navegable de los grafos en disco (subcarpetas por capacidad) en `~/ComfyUI/CAPABILITIES.md` |
|
||||
|
||||
## Como anadir grupo
|
||||
|
||||
@@ -8,7 +8,9 @@ Las tres páginas madre detalladas siguen siendo la fuente de verdad por grupo:
|
||||
|
||||
- [comfyui.md](comfyui.md) — grupo `comfyui`: builders de workflow, ejecución HTTP, UI vía CDP, I/O.
|
||||
- [comfyui-skill.md](comfyui-skill.md) — grupo `comfyui-skill`: recetas de estilo versionadas.
|
||||
- [comfyui-styles.md](comfyui-styles.md) — grupo `comfyui-styles`: presets + catálogo de estilo (selector WAS).
|
||||
- [comfyui-judge.md](comfyui-judge.md) — grupo `comfyui-judge`: panel multi-juez de calidad.
|
||||
- [gamedev-2d.md](gamedev-2d.md) — grupo `gamedev-2d`: 47 builders de assets 2D para Godot (45 también `comfyui`).
|
||||
|
||||
El catálogo navegable con los grafos concretos en disco (subcarpetas por capacidad, cómo cargar
|
||||
cada uno) vive **fuera del repo**, junto a la instalación: `~/ComfyUI/CAPABILITIES.md`. Este doc es
|
||||
@@ -25,7 +27,9 @@ Filtros MCP: `mcp__registry__fn_search query="" tag="comfyui"` (y `tag="comfyui-
|
||||
| 02 | **img2img / inpaint** | imagen → imagen, regenerar zona enmascarada | `build_img2img`, `build_inpaint` | ✅ | — |
|
||||
| 03 | **controlnet** | generación guiada por mapa (depth/pose/canny) | `build_controlnet` | ✅ | — |
|
||||
| 04 | **skills (multiestilo/LoRA)** | recetas de estilo reproducibles con `{subject}` | `build_skill_workflow`, `inject_lora`, `generate_with_skill_oneshot`, `harvest_civitai_skill_oneshot` | ✅ ×2 | ✅ ×2 |
|
||||
| 04b | **styles (presets/catálogo)** | estilo reutilizable: catálogo WAS + presets gamedev | `curated_styles_catalog`, `generate_styles_llm`, `append_styles`, `get_gamedev_style_preset`, `apply_style_preset` | — | — |
|
||||
| 05 | **video** | imagen/texto → vídeo (SVD, LTX, Wan) | `build_img2vid`, `build_video` | ✅ | — |
|
||||
| 05b | **audio** | texto → música/SFX/voz (ACE-Step) | `build_audio_workflow`, `fetch_output_audio` | — | — |
|
||||
| 06 | **upscale / detail** | ampliar y recuperar detalle (ESRGAN, hires-fix, FaceDetailer) | `build_upscale`, `build_hires_fix`, `inject_hires_fix`, `build_facedetailer` | — | — |
|
||||
| 07 | **3D** | imagen/texto → malla 3D (Hunyuan3D) + limpieza | `build_image_to_3d`, `build_textured_3d_multiview`, `image_to_3d_oneshot`, `text_to_3d_oneshot`, `mesh_cleanup_oneshot` | — | — |
|
||||
| 08 | **juez / calidad** | puntuar lo generado (gate de DoD y bucle de mejora) | `judge_image`, `score_aesthetic`, `score_clip_alignment`, `critique_image_llm` | — | — |
|
||||
@@ -67,11 +71,25 @@ sus IDs reales cuando se ejecute `fn index`.
|
||||
- `comfyui_extract_recipe_from_png_py_ml` — destila un PNG de Civitai en receta candidata.
|
||||
- CRUD + telemetría: `comfyui_list_skills_py_ml`, `comfyui_load_skill_py_ml`, `comfyui_save_skill_py_ml`, `comfyui_update_skill_score_py_ml`, `comfyui_bump_skill_version_py_ml`.
|
||||
|
||||
### 04b · styles (presets / catálogo)
|
||||
|
||||
Página madre: [comfyui-styles.md](comfyui-styles.md). Estilo reutilizable como dato, no como prompt repetido.
|
||||
|
||||
- `comfyui_curated_styles_catalog_py_ml` (pura) — catálogo curado (~190 estilos) para el selector WAS.
|
||||
- `comfyui_generate_styles_llm_py_ml` (impura) — genera N estilos de una categoría vía `ask_llm`.
|
||||
- `comfyui_append_styles_py_ml` (impura) — fusiona estilos sobre el `styles.json` WAS (merge+dedup+backup).
|
||||
- `comfyui_get_gamedev_style_preset_py_ml` (pura) — receta de *style preset* gamedev (gameboy/ghibli/pixel-art-retro).
|
||||
- `comfyui_apply_style_preset_py_ml` (pura) — traduce un preset + subject a los kwargs de un builder gamedev-2d.
|
||||
|
||||
### 05 · video
|
||||
|
||||
- `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 +120,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
|
||||
|
||||
@@ -12,6 +12,13 @@ submit/wait). Una skill no es un workflow: es la *receta* que compila a uno.
|
||||
|
||||
Filtro MCP: `mcp__registry__fn_search query="" tag="comfyui-skill"`.
|
||||
|
||||
> **Tamaño del grupo (al 28/06/2026):** 17 funciones con tag `comfyui-skill` — CRUD de recetas
|
||||
> (save/load/list), compilación a workflow (`build_skill_workflow`), inyectores encadenables
|
||||
> (`inject_hires_fix`/`inject_multi_lora`, `build_ipadapter_workflow`), bucle de mejora
|
||||
> genera→juzga→bump (`generate_with_skill_oneshot` + `update_skill_score` + `bump_skill_version`),
|
||||
> export a grafo (`export_skill_template`), mixer de capacidades (`compose_capabilities` +
|
||||
> `generate_mixed_oneshot`) y cosecha de Civitai (`extract_recipe_from_png` + `harvest_civitai_skill_oneshot`).
|
||||
|
||||
## Qué es una skill
|
||||
|
||||
Una receta vive en `~/ComfyUI/skills_library/<slug>/` y la manipulan las funciones de este grupo:
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# ComfyUI Styles — presets y catálogo de estilo
|
||||
|
||||
Tag: `comfyui-styles` (+ `gamedev-2d` para los dos presets gamedev). Sub-grupo de
|
||||
[`comfyui`](comfyui.md) que añade una **capa de estilo reutilizable** sobre los builders de
|
||||
workflow: en vez de repetir a mano los mismos modificadores de cámara/iluminación/render en cada
|
||||
prompt, el estilo se trata como un dato curado y reusable.
|
||||
|
||||
Dos vertientes complementarias:
|
||||
|
||||
- **Catálogo WAS** (`comfyui-styles`): ~190 estilos curados en el formato exacto del selector WAS de
|
||||
ComfyUI (*Prompt Styles Selector* / *Prompt Multiple Styles Selector*), generación de estilos
|
||||
nuevos por LLM, y fusión segura sobre el `styles.json` del usuario.
|
||||
- **Style presets gamedev** (`gamedev-2d`): recetas que empaquetan como datos puros el *look* de un
|
||||
juego entero (prefijo/sufijo de prompt, checkpoint, LoRA, negative, tamaño, post-proceso) y se
|
||||
traducen a los kwargs que consume un builder de sujeto del grupo [`gamedev-2d`](gamedev-2d.md).
|
||||
|
||||
Filtro MCP: `mcp__registry__fn_search query="" tag="comfyui-styles"` (catálogo WAS) y
|
||||
`mcp__registry__fn_search query="style preset" tag="gamedev-2d"` (presets gamedev).
|
||||
|
||||
## Funciones del grupo
|
||||
|
||||
### Catálogo WAS — dominio `ml` (tag `comfyui-styles`)
|
||||
|
||||
| ID | Firma corta | Qué hace |
|
||||
|---|---|---|
|
||||
| [comfyui_curated_styles_catalog_py_ml](../../python/functions/ml/comfyui_curated_styles_catalog.md) | `curated_styles_catalog(category=None) -> dict` | Catálogo curado (~190 estilos) en el formato exacto `{nombre: {prompt, negative_prompt}}` que consume el selector WAS. Cada `prompt` son modificadores de estilo potentes (cámara, lente, iluminación, render engine, medio artístico, paleta, mood), no descripciones de escena. Filtra por `category`. **Pura**. |
|
||||
| [comfyui_generate_styles_llm_py_ml](../../python/functions/ml/comfyui_generate_styles_llm.md) | `generate_styles_llm(category, n=8, prefix='', avoid=None, model='claude-haiku-4-5-20251001') -> dict` | Genera N estilos de una categoría temática usando `ask_llm` (grupo claude-direct, API directa, arranque 0), en el mismo formato `{nombre: {prompt, negative_prompt}}`. `avoid` evita duplicar nombres ya existentes. **Impura** (LLM). |
|
||||
| [comfyui_append_styles_py_ml](../../python/functions/ml/comfyui_append_styles.md) | `append_styles(new_styles, styles_path=DEFAULT_STYLES_PATH, overwrite=False, backup=True, dry_run=False) -> dict` | Fusiona (merge + dedup por nombre) un dict de estilos sobre el `styles.json` del selector WAS de forma SEGURA y NO destructiva: preserva todos los existentes (ganan salvo `overwrite=True`), hace backup con timestamp antes de escribir. `dry_run=True` previsualiza sin tocar disco. **Impura** (I/O disco). |
|
||||
|
||||
### Style presets gamedev — dominio `ml` (tag `gamedev-2d`)
|
||||
|
||||
| ID | Firma corta | Qué hace |
|
||||
|---|---|---|
|
||||
| [comfyui_get_gamedev_style_preset_py_ml](../../python/functions/ml/comfyui_get_gamedev_style_preset.md) | `get_gamedev_style_preset(name=None) -> dict` | Devuelve la receta de un *style preset* gamedev curado (`gameboy`, `ghibli`, `pixel-art-retro`) o el catálogo de nombres si `name=None`. Un preset empaqueta como DATOS puros el look de un juego entero: `subject_prefix`/`suffix`, `style`, `negative`, checkpoint recomendado, LoRA + strength, `size`, `transparent`, post-proceso. **Pura**. |
|
||||
| [comfyui_apply_style_preset_py_ml](../../python/functions/ml/comfyui_apply_style_preset.md) | `apply_style_preset(preset, subject, *, style=None, negative=None) -> dict` | Traduce un *style preset* gamedev (de `get_gamedev_style_preset`) + un `subject` del usuario a lo que necesita un builder de sujeto del grupo gamedev-2d: el subject combinado con el prefijo/sufijo del estilo y los kwargs comunes (`style`, `checkpoint`, `lora`, `lora_strength`, `negative`, resolución) listos para `**spread`. `style`/`negative` permiten override puntual. **Pura**. |
|
||||
|
||||
## Ejemplo canónico — generar un estilo, fusionarlo y aplicarlo
|
||||
|
||||
Dos flujos típicos: (1) ampliar el catálogo del selector WAS, y (2) usar un preset gamedev para
|
||||
generar un asset con look consistente.
|
||||
|
||||
### A) Ampliar el catálogo WAS con estilos nuevos por LLM
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from ml.comfyui_generate_styles_llm import comfyui_generate_styles_llm
|
||||
from ml.comfyui_append_styles import comfyui_append_styles
|
||||
|
||||
# 1. Pedir 6 estilos de una categoría. Devuelve el dict {nombre: {prompt, negative_prompt}}
|
||||
# directo (best-effort: {} si el LLM falla).
|
||||
nuevos = comfyui_generate_styles_llm("film noir cinematic", n=6, prefix="noir-")
|
||||
|
||||
# 2. Previsualizar la fusión (no escribe), luego aplicar con backup.
|
||||
if nuevos:
|
||||
print(comfyui_append_styles(nuevos, dry_run=True)["total_after"]) # nº tras fusionar, sin tocar disco
|
||||
res = comfyui_append_styles(nuevos) # backup + merge + dedup + escritura
|
||||
print(res["total_before"], "->", res["total_after"], "añadidos:", len(res["added"]))
|
||||
```
|
||||
|
||||
### B) Aplicar un style preset gamedev a un sujeto
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from ml.comfyui_get_gamedev_style_preset import comfyui_get_gamedev_style_preset
|
||||
from ml.comfyui_apply_style_preset import comfyui_apply_style_preset
|
||||
from ml.comfyui_build_enemy_creature_workflow import comfyui_build_enemy_creature_workflow
|
||||
|
||||
preset = comfyui_get_gamedev_style_preset("gameboy") # receta pura del look Game Boy
|
||||
ap = comfyui_apply_style_preset(preset, "a wizard casting a spell")
|
||||
# ap = {subject, builder_kwargs, size, transparent, post, ...} listo para un builder gamedev-2d:
|
||||
wf = comfyui_build_enemy_creature_workflow(
|
||||
ap["subject"], size=ap["size"], transparent=ap["transparent"], **ap["builder_kwargs"]
|
||||
)
|
||||
```
|
||||
|
||||
El catálogo curado completo se consulta sin red (devuelve el dict plano directo):
|
||||
|
||||
```python
|
||||
from ml.comfyui_curated_styles_catalog import comfyui_curated_styles_catalog
|
||||
print(comfyui_curated_styles_catalog("__categories__")) # {'categories': {...}, 'total': 190}
|
||||
todos = comfyui_curated_styles_catalog() # dict {nombre: {prompt, negative_prompt}}
|
||||
print(len(todos), list(todos)[:5])
|
||||
```
|
||||
|
||||
## Fronteras
|
||||
|
||||
- **No genera imágenes**: este sub-grupo produce y gestiona DATOS de estilo (dicts de prompt /
|
||||
negative, presets). Generar el asset es trabajo de los builders del grupo [`comfyui`](comfyui.md)
|
||||
y [`gamedev-2d`](gamedev-2d.md), o de los pipelines oneshot (p.ej.
|
||||
`comfyui_generate_styled_asset_oneshot_py_pipelines`, que compone un preset + un builder + submit).
|
||||
- **El catálogo WAS asume el custom node WAS instalado**: `append_styles` escribe sobre el
|
||||
`styles.json` que lee el selector WAS en la UI. Sin ese node, el catálogo sigue siendo usable como
|
||||
dict de modificadores, pero el selector no aparecerá en el grafo.
|
||||
- **Los dos presets gamedev (`get`/`apply`) llevan tag `gamedev-2d`**, no `comfyui-styles`: son la
|
||||
vía de estilo para los builders de assets de juego, no para el selector WAS genérico. Se listan
|
||||
aquí por afinidad de capacidad (estilo reutilizable).
|
||||
- **Formato exacto**: el dict de estilos es `{nombre: {prompt, negative_prompt}}`. Los prompts son
|
||||
modificadores (cámara/lente/luz/render/medio/paleta/mood), no descripciones de escena — la escena
|
||||
la pone el `subject` del usuario.
|
||||
@@ -13,6 +13,17 @@ Tag: `comfyui`. Grupo de funciones para controlar [ComfyUI](https://github.com/c
|
||||
|
||||
Filtro MCP: `mcp__registry__fn_search query="" tag="comfyui"`.
|
||||
|
||||
> **Tamaño del grupo (al 28/06/2026):** 126 funciones con tag `comfyui` (63 puras, 50 impuras,
|
||||
> 13 pipelines). El grupo se reparte en sub-grupos con página madre propia:
|
||||
> [`comfyui-skill`](comfyui-skill.md) (recetas de estilo versionadas),
|
||||
> [`comfyui-styles`](comfyui-styles.md) (presets + catálogo de estilo para el selector WAS),
|
||||
> [`comfyui-judge`](comfyui-judge.md) (panel de calidad) y
|
||||
> [`gamedev-2d`](gamedev-2d.md) (assets 2D para Godot: 47 funciones, 45 de ellas también `comfyui`).
|
||||
> Esta página documenta el **núcleo** (lifecycle del server, API HTTP, builders, I/O de workflows,
|
||||
> imagen→3D, UI por CDP, audio, templates); los builders específicos de gamedev-2d viven en su
|
||||
> propia página. El mapa cross-grupo de capacidades está en
|
||||
> [comfyui-overview.md](comfyui-overview.md).
|
||||
|
||||
## Dos caminos, mismo motor
|
||||
|
||||
```
|
||||
@@ -44,7 +55,7 @@ El **API format** (dict de nodos numerados que produce `build_txt2img_workflow`
|
||||
| ID | Firma corta | Qué hace |
|
||||
|---|---|---|
|
||||
| [comfyui_build_txt2img_workflow_py_ml](../../python/functions/ml/comfyui_build_txt2img_workflow.md) | `build_txt2img_workflow(ckpt_name, positive, negative='', *, steps, cfg, width, height, seed, ...) -> dict` | Construye el dict del workflow txt2img básico (Checkpoint → CLIPTextEncode×2 + EmptyLatent → KSampler → VAEDecode → SaveImage) en API format. **Pura**. |
|
||||
| [comfyui_build_flux_workflow_py_ml](../../python/functions/ml/comfyui_build_flux_workflow.md) | `build_flux_workflow(prompt, *, unet='flux1-schnell-fp8-e4m3fn.safetensors', clip_l, t5xxl, vae='ae.safetensors', width=1024, height=1024, steps=4, guidance=3.5, seed, weight_dtype='fp8_e4m3fn', ...) -> dict` | Builder txt2img para **Flux** (schnell/dev): UNETLoader + DualCLIPLoader (clip_l + t5xxl, type flux) + VAELoader → CLIPTextEncode → FluxGuidance + EmptySD3LatentImage → KSampler (cfg fijo 1.0) → VAEDecode → SaveImage. La guía va por FluxGuidance, no por el cfg. fp8 + ~4 pasos para 8 GB. **Pura**. |
|
||||
| [comfyui_build_flux_workflow_py_ml](../../python/functions/ml/comfyui_build_flux_workflow.md) | `build_flux_workflow(prompt, *, variant='schnell', width=1024, height=1024, steps=None, guidance=3.5, seed=0, unet_name=None, clip_l_name='clip_l.safetensors', t5xxl_name='t5xxl_fp8_e4m3fn_scaled.safetensors', vae_name='ae.safetensors', weight_dtype='default', sampler_name='euler', scheduler='simple', ...) -> dict` | Builder txt2img para **Flux** (`variant='schnell'` o `'dev'`): UNETLoader + DualCLIPLoader (clip_l + t5xxl, type flux) + VAELoader → CLIPTextEncode → FluxGuidance + EmptySD3LatentImage → camino custom-advanced (RandomNoise + KSamplerSelect + BasicScheduler → BasicGuider → SamplerCustomAdvanced) → VAEDecode → SaveImage. La guía va por FluxGuidance, no por el cfg. `steps=None` autoselecciona por variante (~4 schnell); `unet_name=None` deduce el checkpoint de la variante; `weight_dtype='default'`. **Pura**. |
|
||||
| [comfyui_object_info_py_ml](../../python/functions/ml/comfyui_object_info.md) | `object_info(server='127.0.0.1:8188', node_class=None, timeout) -> dict` | Catálogo de nodos del server: inputs, tipos y enums (lista de checkpoints/samplers visibles). Para validar antes de enviar. Impura. |
|
||||
| [comfyui_submit_workflow_py_ml](../../python/functions/ml/comfyui_submit_workflow.md) | `submit_workflow(workflow, server, client_id, timeout) -> dict` | Encola un workflow API format vía POST /prompt; devuelve `prompt_id` + posición en cola. HTTP 400 propaga la validación por nodo. Impura. |
|
||||
| [comfyui_wait_result_py_ml](../../python/functions/ml/comfyui_wait_result.md) | `wait_result(prompt_id, server, timeout, poll_interval) -> dict` | Sondea GET /history/{prompt_id} hasta que termina; devuelve los outputs (PNGs con filename/subfolder/type). Impura. |
|
||||
@@ -142,6 +153,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 +203,37 @@ 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. |
|
||||
|
||||
### Estilos — presets y catálogo (sub-grupo `comfyui-styles`)
|
||||
|
||||
Capa de **estilo reutilizable** sobre los builders: un catálogo curado de ~190 modificadores de
|
||||
estilo para el selector WAS (Prompt Styles Selector), generación de estilos por LLM, y *style
|
||||
presets* gamedev (gameboy, ghibli, pixel-art-retro) que empaquetan como datos puros el look de un
|
||||
juego entero (prefijo/sufijo de prompt, checkpoint, LoRA, negative, tamaño). Página madre dedicada:
|
||||
[comfyui-styles.md](comfyui-styles.md). Las 5 funciones:
|
||||
|
||||
| ID | Firma corta | Qué hace |
|
||||
|---|---|---|
|
||||
| [comfyui_curated_styles_catalog_py_ml](../../python/functions/ml/comfyui_curated_styles_catalog.md) | `curated_styles_catalog(category=None) -> dict` | Catálogo curado (~190 estilos) en formato `{nombre: {prompt, negative_prompt}}` para el selector WAS. Cada prompt son modificadores potentes (cámara, lente, iluminación, render, medio, paleta). **Pura**. |
|
||||
| [comfyui_generate_styles_llm_py_ml](../../python/functions/ml/comfyui_generate_styles_llm.md) | `generate_styles_llm(category, n=8, prefix='', avoid=None, model='claude-haiku-4-5-...') -> dict` | Genera N estilos nuevos de una categoría temática vía `ask_llm` (grupo claude-direct), en el mismo formato del selector WAS. **Impura**. |
|
||||
| [comfyui_append_styles_py_ml](../../python/functions/ml/comfyui_append_styles.md) | `append_styles(new_styles, styles_path=..., overwrite=False, backup=True, dry_run=False) -> dict` | Fusiona (merge + dedup) estilos nuevos sobre el `styles.json` del selector WAS de forma NO destructiva: preserva los existentes (salvo `overwrite`), backup con timestamp. **Impura**. |
|
||||
| [comfyui_get_gamedev_style_preset_py_ml](../../python/functions/ml/comfyui_get_gamedev_style_preset.md) | `get_gamedev_style_preset(name=None) -> dict` | Devuelve la receta de un *style preset* gamedev curado (gameboy, ghibli, pixel-art-retro) o el catálogo de nombres si `name=None`. Empaqueta el look como datos puros. **Pura**. |
|
||||
| [comfyui_apply_style_preset_py_ml](../../python/functions/ml/comfyui_apply_style_preset.md) | `apply_style_preset(preset, subject, *, style=None, negative=None) -> dict` | Traduce un *style preset* gamedev + un subject del usuario a los kwargs que consume un builder de sujeto del grupo gamedev-2d (subject combinado + `**kwargs` listos para spread). **Pura**. |
|
||||
|
||||
## 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
|
||||
|
||||
@@ -11,8 +11,9 @@ Cluster de funciones para producir y mover assets 2D de juego entre **ComfyUI**
|
||||
3. **Puente de assets** (CPU): coloca el resultado en un proyecto Godot
|
||||
con sus import settings.
|
||||
|
||||
Tag único del grupo: `gamedev-2d` (los 31 builders de workflow + las 5 funciones de
|
||||
apoyo de post-proceso y puente). El tag plano `gamedev` quedó deprecado y unificado a
|
||||
Tag único del grupo: `gamedev-2d` — **47 funciones**: 36 builders de workflow (31 de
|
||||
generación desde texto + 5 de transformación desde una imagen de entrada) + 11 de apoyo
|
||||
(post-proceso, puente a Godot, style presets y pipelines one-shot). El tag plano `gamedev` quedó deprecado y unificado a
|
||||
`gamedev-2d`. El **runtime de juego C++** (el motor que ejecuta el juego: game loop,
|
||||
cámara, input, render por lotes, audio) vive en el grupo hermano `gamedev-engine`.
|
||||
Filtro: `mcp__registry__fn_search query="" tag="gamedev-2d"`.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,11 +3,11 @@ name: comfyui_build_flux_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_build_flux_workflow(prompt: str, *, unet: str = \"IMG_flux1-schnell-fp8-e4m3fn.safetensors\", clip_l: str = \"clip_l.safetensors\", t5xxl: str = \"t5xxl_fp8_e4m3fn_scaled.safetensors\", vae: str = \"ae.safetensors\", width: int = 1024, height: int = 1024, steps: int = 4, guidance: float = 3.5, seed: int = 0, weight_dtype: str = \"fp8_e4m3fn\", sampler_name: str = \"euler\", scheduler: str = \"simple\", filename_prefix: str = \"comfy_flux\") -> dict"
|
||||
description: "Construye el dict de un workflow ComfyUI txt2img con Flux en API format (nodos numerados con class_type + inputs, conexiones como [node_id, output_index]). A diferencia de SD1.5/SDXL, Flux carga por separado UNETLoader + DualCLIPLoader (clip_l + t5xxl, type flux) + VAELoader; la guia va por FluxGuidance (no por el cfg del KSampler, que se fija a 1.0). Cadena: UNETLoader+DualCLIPLoader+VAELoader -> CLIPTextEncode -> FluxGuidance + EmptySD3LatentImage -> KSampler -> VAEDecode -> SaveImage. Pura, sin red ni I/O. Hermana de comfyui_build_txt2img_workflow."
|
||||
tags: [comfyui, flux, ml, txt2img, workflow]
|
||||
signature: "def comfyui_build_flux_workflow(prompt: str, *, variant: str = \"schnell\", width: int = 1024, height: int = 1024, steps: int | None = None, guidance: float = 3.5, seed: int = 0, unet_name: str | None = None, clip_l_name: str = \"clip_l.safetensors\", t5xxl_name: str = \"t5xxl_fp8_e4m3fn_scaled.safetensors\", vae_name: str = \"ae.safetensors\", weight_dtype: str = \"default\", sampler_name: str = \"euler\", scheduler: str = \"simple\", filename_prefix: str = \"flux\", available: dict | None = None) -> dict"
|
||||
description: "Construye el dict de un workflow ComfyUI para Flux (schnell o dev) en API format (nodos numerados con class_type + inputs, conexiones como [node_id, output_index]). A diferencia de SD1.5/SDXL, Flux carga por separado UNETLoader + DualCLIPLoader (clip_l + t5xxl, type flux) + VAELoader y muestrea con el camino custom-advanced: RandomNoise + KSamplerSelect + BasicScheduler -> BasicGuider -> SamplerCustomAdvanced -> VAEDecode -> SaveImage. variant=schnell (~4 pasos, sin FluxGuidance) o dev (~20 pasos, con FluxGuidance). Validacion opcional de modelos via 'available'. Pura, sin red ni I/O. Hermana de comfyui_build_txt2img_workflow."
|
||||
tags: [comfyui, flux, ml, txt2img, workflow, image-generation]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -16,36 +16,40 @@ error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: prompt
|
||||
desc: "Prompt positivo: lo que se quiere ver en la imagen."
|
||||
- name: unet
|
||||
desc: "Nombre del modelo de difusion en models/diffusion_models/ tal como lo lista comfyui_object_info para UNETLoader (unet_name). Por defecto el Flux schnell fp8. keyword-only."
|
||||
- name: clip_l
|
||||
desc: "Nombre del encoder CLIP-L en models/text_encoders/ (clip_name2 del DualCLIPLoader). Por defecto 'clip_l.safetensors'. keyword-only."
|
||||
- name: t5xxl
|
||||
desc: "Nombre del encoder T5-XXL en models/text_encoders/ (clip_name1 del DualCLIPLoader). Por defecto 't5xxl_fp8_e4m3fn_scaled.safetensors'. keyword-only."
|
||||
- name: vae
|
||||
desc: "Nombre del VAE en models/vae/ (vae_name del VAELoader). Por defecto 'ae.safetensors', el autoencoder de Flux. keyword-only."
|
||||
desc: "Prompt positivo: lo que se quiere ver. Flux ignora el negativo, por eso no se codifica."
|
||||
- name: variant
|
||||
desc: "'schnell' (rapido, ~4 pasos, sin FluxGuidance) o 'dev' (~20 pasos, con FluxGuidance). Determina el unet y los steps por defecto. keyword-only."
|
||||
- name: width
|
||||
desc: "Ancho del latente/imagen en px, multiplo de 16 para SD3/Flux. keyword-only."
|
||||
desc: "Ancho del latente/imagen en px, multiplo de 8. keyword-only."
|
||||
- name: height
|
||||
desc: "Alto del latente/imagen en px, multiplo de 16 para SD3/Flux. keyword-only."
|
||||
desc: "Alto del latente/imagen en px, multiplo de 8. keyword-only."
|
||||
- name: steps
|
||||
desc: "Pasos de sampling del KSampler. Flux schnell rinde con ~4; Flux dev necesita ~20. keyword-only."
|
||||
desc: "Pasos de sampling (BasicScheduler). Si None, default por variante: schnell=4, dev=20. keyword-only."
|
||||
- name: guidance
|
||||
desc: "Valor del nodo FluxGuidance (no es el cfg clasico). Schnell es poco sensible; dev responde a 3.0-4.0. keyword-only."
|
||||
desc: "Valor del nodo FluxGuidance. Solo se aplica en variant=dev; en schnell se ignora (la guia va fija dentro del modelo distilado). dev responde a 3.0-4.0. keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla del KSampler. 0 es determinista; cambiar para variar la imagen. keyword-only."
|
||||
desc: "Semilla de RandomNoise. 0 es determinista; cambiar para variar la imagen. keyword-only."
|
||||
- name: unet_name
|
||||
desc: "Nombre del modelo de difusion en UNETLoader (unet_name de /object_info). Si None, default por variante (IMG_flux1-schnell-fp8-e4m3fn.safetensors / IMG_flux1-dev-fp8-e4m3fn.safetensors). keyword-only."
|
||||
- name: clip_l_name
|
||||
desc: "Nombre del encoder CLIP-L en DualCLIPLoader (clip_name2). Por defecto 'clip_l.safetensors'. keyword-only."
|
||||
- name: t5xxl_name
|
||||
desc: "Nombre del encoder T5-XXL en DualCLIPLoader (clip_name1). Por defecto 't5xxl_fp8_e4m3fn_scaled.safetensors'. keyword-only."
|
||||
- name: vae_name
|
||||
desc: "Nombre del VAE en VAELoader (vae_name). Por defecto 'ae.safetensors', el autoencoder de Flux. keyword-only."
|
||||
- name: weight_dtype
|
||||
desc: "dtype de carga del UNET (uno de 'default', 'fp8_e4m3fn', 'fp8_e4m3fn_fast', 'fp8_e5m2'). fp8 reduce VRAM, clave en GPU de 8GB. keyword-only."
|
||||
desc: "dtype de carga del UNET (uno de 'default', 'fp8_e4m3fn', 'fp8_e4m3fn_fast', 'fp8_e5m2'). Los modelos ya son fp8; 'default' los carga tal cual. keyword-only."
|
||||
- name: sampler_name
|
||||
desc: "Nombre del sampler (Flux usa 'euler'). keyword-only."
|
||||
desc: "Nombre del sampler para KSamplerSelect (Flux usa 'euler'). keyword-only."
|
||||
- name: scheduler
|
||||
desc: "Scheduler del sampler (Flux usa 'simple'). keyword-only."
|
||||
desc: "Scheduler para BasicScheduler (Flux usa 'simple'). keyword-only."
|
||||
- name: filename_prefix
|
||||
desc: "Prefijo del PNG que SaveImage escribe en output/. keyword-only."
|
||||
output: "dict en API format con node_ids como claves (UNETLoader '10', DualCLIPLoader '11', VAELoader '12', CLIPTextEncode positivo '6', FluxGuidance '13', CLIPTextEncode negativo vacio '7', EmptySD3LatentImage '5', KSampler '3', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow."
|
||||
- name: available
|
||||
desc: "Mapa opcional para validar que los modelos existen en el servidor, con claves opcionales 'unet', 'clip', 'vae' (cada una lista de nombres de /object_info). Si se pasa y un modelo elegido falta, lanza FileNotFoundError indicando que falta y donde colocarlo. None = sin validacion. keyword-only."
|
||||
output: "dict en API format con node_ids string como claves (UNETLoader '12', DualCLIPLoader '11', VAELoader '10', EmptyLatentImage '5', CLIPTextEncode '6', FluxGuidance '21' solo en dev, RandomNoise '25', KSamplerSelect '16', BasicScheduler '17', BasicGuider '22', SamplerCustomAdvanced '13', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow."
|
||||
tested: true
|
||||
tests: ["class_types esperados (9 nodos de Flux)", "loaders separados UNET+DualCLIP(flux)+VAE", "guidance via FluxGuidance y cfg del KSampler fijado a 1.0", "params width/height/steps/seed reflejados", "filename_prefix en SaveImage", "determinismo: misma entrada -> mismo dict (builder puro)"]
|
||||
tests: ["class_types esperados del camino custom-advanced", "schnell: sin nodo FluxGuidance, BasicGuider consume CLIPTextEncode directo", "dev: nodo FluxGuidance presente con guidance, BasicGuider lo consume", "steps default por variante (schnell=4, dev=20)", "width/height/seed reflejados en sus nodos", "available: FileNotFoundError si falta un modelo", "variant invalido -> ValueError", "determinismo: misma entrada -> mismo dict (builder puro)"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_build_flux_workflow.py"
|
||||
file_path: "python/functions/ml/comfyui_build_flux_workflow.py"
|
||||
---
|
||||
@@ -56,22 +60,38 @@ file_path: "python/functions/ml/comfyui_build_flux_workflow.py"
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_build_flux_workflow import comfyui_build_flux_workflow
|
||||
from ml.comfyui_submit_workflow import comfyui_submit_workflow
|
||||
from ml.comfyui_wait_result import comfyui_wait_result
|
||||
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
|
||||
|
||||
# Flux schnell: rapido, ~4 pasos, sin FluxGuidance.
|
||||
wf = comfyui_build_flux_workflow(
|
||||
prompt="a red apple on a wooden table, sharp focus, studio lighting",
|
||||
"a red apple on a wooden table, sharp focus, studio light",
|
||||
variant="schnell",
|
||||
width=1024,
|
||||
height=1024,
|
||||
steps=4, # Flux schnell: ~4 pasos basta
|
||||
seed=42,
|
||||
)
|
||||
# wf["10"]["class_type"] == "UNETLoader" # modelo de difusion suelto
|
||||
# wf["11"]["inputs"]["type"] == "flux" # DualCLIPLoader en modo flux
|
||||
# wf["3"]["inputs"]["positive"] == ["13", 0] # KSampler consume FluxGuidance
|
||||
# wf["3"]["inputs"]["cfg"] == 1.0 # la guia va por FluxGuidance
|
||||
# wf["9"]["class_type"] == "SaveImage"
|
||||
# wf["12"]["class_type"] == "UNETLoader" # modelo de difusion suelto
|
||||
# wf["11"]["inputs"]["type"] == "flux" # DualCLIPLoader en modo flux
|
||||
# "21" not in wf # schnell no lleva FluxGuidance
|
||||
# wf["22"]["inputs"]["conditioning"] == ["6", 0] # BasicGuider <- CLIPTextEncode
|
||||
|
||||
sub = comfyui_submit_workflow(wf, server="127.0.0.1:8188")
|
||||
out = comfyui_wait_result(sub["prompt_id"], server="127.0.0.1:8188")
|
||||
img = out["9"]["images"][0]
|
||||
res = comfyui_fetch_output_image(img["filename"], subfolder=img["subfolder"],
|
||||
server="127.0.0.1:8188", dest_dir="/tmp")
|
||||
print(res["path"]) # PNG en disco
|
||||
|
||||
# Flux dev: ~20 pasos, con FluxGuidance.
|
||||
wf_dev = comfyui_build_flux_workflow("a misty forest at dawn", variant="dev",
|
||||
guidance=3.5, width=768, height=1024)
|
||||
# wf_dev["21"]["class_type"] == "FluxGuidance"
|
||||
# wf_dev["22"]["inputs"]["conditioning"] == ["21", 0]
|
||||
```
|
||||
|
||||
O lanzable directo con: `./fn run comfyui_build_flux_workflow` (imprime el JSON del workflow de ejemplo).
|
||||
O lanzable directo con: `./fn run comfyui_build_flux_workflow` (imprime el JSON del workflow schnell de ejemplo).
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
@@ -79,26 +99,34 @@ Cuando vayas a generar txt2img con un modelo Flux (schnell o dev) y necesites el
|
||||
dict del workflow para `comfyui_submit_workflow`. Usala en lugar de
|
||||
`comfyui_build_txt2img_workflow` siempre que el modelo NO sea un checkpoint
|
||||
todo-en-uno SD1.5/SDXL sino Flux con UNET + text encoders + VAE por separado.
|
||||
Flux schnell es ideal en GPU de poca VRAM (8GB) por el fp8 y los ~4 pasos.
|
||||
Flux schnell es ideal en GPU de poca VRAM (8GB) por el fp8 y los ~4 pasos; dev
|
||||
da mejor calidad a cambio de mas tiempo.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Es API format (nodos numerados), NO el formato de la UI de ComfyUI (graph con
|
||||
links). No se puede pegar en la UI tal cual; es el formato que acepta POST
|
||||
/prompt.
|
||||
- Flux NO usa el cfg del KSampler para guiar: este builder lo fija a 1.0 y la
|
||||
guia va por el nodo FluxGuidance. Subir el cfg del KSampler con Flux degrada o
|
||||
rompe la imagen.
|
||||
- El negativo es un CLIPTextEncode vacio cableado al KSampler (igual que el
|
||||
template oficial de Flux). Flux schnell es destilado y practicamente ignora el
|
||||
negativo; no esperes que un prompt negativo tenga el efecto de SD1.5/SDXL.
|
||||
- `unet`, `clip_l`, `t5xxl` y `vae` deben existir en los directorios respectivos
|
||||
visibles para el servidor (models/diffusion_models/, models/text_encoders/,
|
||||
models/vae/). Si no, ComfyUI rechaza el workflow con HTTP 400 al enviarlo (no
|
||||
aqui — esta funcion es pura y no valida contra el servidor). Valida antes con
|
||||
`comfyui_validate_workflow`.
|
||||
- `width`/`height` deben ser multiplos de 16 para EmptySD3LatentImage (Flux), no
|
||||
de 8 como en SD1.5/SDXL.
|
||||
- `weight_dtype` debe ser uno de los que admite UNETLoader ('default',
|
||||
'fp8_e4m3fn', 'fp8_e4m3fn_fast', 'fp8_e5m2'). En 8GB usa fp8 o el modelo no
|
||||
cabe en VRAM.
|
||||
- Camino de muestreo custom-advanced (RandomNoise + KSamplerSelect +
|
||||
BasicScheduler -> BasicGuider -> SamplerCustomAdvanced), el patron oficial de
|
||||
Flux. NO usa KSampler ni cfg; la guia va por FluxGuidance (solo en dev).
|
||||
- schnell es destilado: NO lleva FluxGuidance y practicamente ignora el prompt
|
||||
negativo. dev SI lleva FluxGuidance (nodo '21'); subir `guidance` aumenta la
|
||||
adherencia al prompt.
|
||||
- Los modelos (unet/clip_l/t5xxl/vae) deben existir en el servidor. Esta funcion
|
||||
es pura y no toca disco: por defecto NO valida. Pasa `available` (las listas de
|
||||
/object_info) para que valide y lance FileNotFoundError con la carpeta destino
|
||||
si falta alguno, ANTES de enviar nada a la GPU. Sin `available`, un modelo
|
||||
ausente lo detecta `comfyui_submit_workflow` (HTTP 400 con detalle).
|
||||
- `width`/`height` deben ser multiplos de 8 (EmptyLatentImage). Flux trabaja bien
|
||||
a 1024x1024; tamanos grandes suben mucho la VRAM en 8GB.
|
||||
- Los `clip_name1`/`clip_name2` del DualCLIPLoader van en orden t5xxl, clip_l
|
||||
(igual que el template oficial). El modo flux carga ambos; el orden no afecta
|
||||
al resultado.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (27/06/2026) — refactor al camino custom-advanced (SamplerCustomAdvanced
|
||||
+ BasicGuider), nuevo parametro `variant` (schnell/dev con steps por defecto),
|
||||
FluxGuidance solo en dev, y `available` para validar modelos faltantes con
|
||||
error claro (FileNotFoundError) sin romper la pureza.
|
||||
|
||||
@@ -1,136 +1,241 @@
|
||||
"""Construye un workflow ComfyUI txt2img con Flux en "API format" (dict de nodos numerados).
|
||||
"""Construye un workflow ComfyUI para Flux (schnell o dev) en "API format".
|
||||
|
||||
API format: cada clave es un node_id (string); cada nodo tiene class_type +
|
||||
inputs. Las conexiones entre nodos son listas [node_id, output_index]. Este es
|
||||
el formato que acepta POST /prompt, distinto del formato de la UI (graph con
|
||||
links explicitos).
|
||||
|
||||
A diferencia del builder SD1.5/SDXL (comfyui_build_txt2img_workflow), Flux NO usa
|
||||
un checkpoint todo-en-uno: carga por separado el modelo de difusion (UNETLoader),
|
||||
los dos text encoders (DualCLIPLoader con clip_l + t5xxl, type="flux") y el VAE
|
||||
(VAELoader). La guia no va por el cfg del KSampler (que se fija a 1.0) sino por el
|
||||
nodo FluxGuidance aplicado al condicionamiento positivo. El negativo se deja como
|
||||
un CLIPTextEncode vacio, igual que el template oficial de Flux en ComfyUI.
|
||||
Flux NO se carga como un checkpoint clasico (no CheckpointLoaderSimple). El
|
||||
modelo de difusion se carga con UNETLoader; los dos text encoders (clip_l + t5xxl)
|
||||
con DualCLIPLoader (type="flux"); el VAE con VAELoader. El muestreo usa el camino
|
||||
"custom advanced" (RandomNoise -> KSamplerSelect + BasicScheduler -> BasicGuider
|
||||
-> SamplerCustomAdvanced), que es el patron canonico de los ejemplos oficiales de
|
||||
Flux y el que produce resultados estables con los modelos fp8 distilados.
|
||||
|
||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
|
||||
Diferencias schnell vs dev:
|
||||
- schnell: modelo distilado, ~4 pasos, sin FluxGuidance (la guia va fija dentro
|
||||
del modelo). Rapido. El conditioning del prompt va directo a BasicGuider.
|
||||
- dev: ~20 pasos, el conditioning pasa antes por FluxGuidance (guidance ~3.5),
|
||||
que sube la adherencia al prompt a costa de tiempo. Mejor calidad.
|
||||
|
||||
Flux ignora el prompt negativo, por eso solo se codifica el positivo.
|
||||
|
||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos. La
|
||||
validacion de existencia de modelos en disco se hace pasando `available` (mapa
|
||||
de modelos que el servidor expone via /object_info); recibir ese mapa como
|
||||
argumento no rompe la pureza (el caller hace la unica peticion de red).
|
||||
"""
|
||||
|
||||
# Modelos por defecto para cada variante (nombres tal como los expone el
|
||||
# servidor ComfyUI en /object_info; verificados contra UNETLoader.unet_name,
|
||||
# DualCLIPLoader.clip_name1/2 y VAELoader.vae_name).
|
||||
_DEFAULT_UNET = {
|
||||
"schnell": "IMG_flux1-schnell-fp8-e4m3fn.safetensors",
|
||||
"dev": "IMG_flux1-dev-fp8-e4m3fn.safetensors",
|
||||
}
|
||||
_DEFAULT_STEPS = {"schnell": 4, "dev": 20}
|
||||
|
||||
# Carpeta destino por rol de modelo, para mensajes de error utiles. ComfyUI
|
||||
# acepta tanto la carpeta "diffusion_models" (moderna) como "unet" (legacy) para
|
||||
# el UNET; los text encoders en "text_encoders" o "clip"; el VAE en "vae".
|
||||
_MODEL_DIRS = {
|
||||
"unet": "models/diffusion_models/ (o models/unet/)",
|
||||
"clip": "models/text_encoders/ (o models/clip/)",
|
||||
"vae": "models/vae/",
|
||||
}
|
||||
|
||||
|
||||
def comfyui_build_flux_workflow(
|
||||
prompt: str,
|
||||
*,
|
||||
unet: str = "IMG_flux1-schnell-fp8-e4m3fn.safetensors",
|
||||
clip_l: str = "clip_l.safetensors",
|
||||
t5xxl: str = "t5xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
vae: str = "ae.safetensors",
|
||||
variant: str = "schnell",
|
||||
width: int = 1024,
|
||||
height: int = 1024,
|
||||
steps: int = 4,
|
||||
steps: int | None = None,
|
||||
guidance: float = 3.5,
|
||||
seed: int = 0,
|
||||
weight_dtype: str = "fp8_e4m3fn",
|
||||
unet_name: str | None = None,
|
||||
clip_l_name: str = "clip_l.safetensors",
|
||||
t5xxl_name: str = "t5xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
vae_name: str = "ae.safetensors",
|
||||
weight_dtype: str = "default",
|
||||
sampler_name: str = "euler",
|
||||
scheduler: str = "simple",
|
||||
filename_prefix: str = "comfy_flux",
|
||||
filename_prefix: str = "flux",
|
||||
available: dict | None = None,
|
||||
) -> dict:
|
||||
"""Construye el dict del workflow txt2img de Flux (schnell/dev).
|
||||
"""Construye el dict del workflow Flux (schnell o dev) en API format.
|
||||
|
||||
Cadena de nodos: UNETLoader + DualCLIPLoader + VAELoader -> CLIPTextEncode
|
||||
(positivo) -> FluxGuidance, mas un CLIPTextEncode vacio para el negativo y
|
||||
EmptySD3LatentImage -> KSampler -> VAEDecode -> SaveImage.
|
||||
[-> FluxGuidance solo en dev] -> BasicGuider; RandomNoise + KSamplerSelect +
|
||||
BasicScheduler + EmptyLatentImage -> SamplerCustomAdvanced -> VAEDecode ->
|
||||
SaveImage.
|
||||
|
||||
Args:
|
||||
prompt: prompt positivo (lo que se quiere ver en la imagen).
|
||||
unet: nombre del modelo de difusion en models/diffusion_models/ tal como
|
||||
lo lista comfyui_object_info para UNETLoader (unet_name). Por defecto
|
||||
el Flux schnell fp8 ("IMG_flux1-schnell-fp8-e4m3fn.safetensors").
|
||||
clip_l: nombre del encoder CLIP-L en models/text_encoders/ (clip_name2 del
|
||||
DualCLIPLoader). Por defecto "clip_l.safetensors".
|
||||
t5xxl: nombre del encoder T5-XXL en models/text_encoders/ (clip_name1 del
|
||||
DualCLIPLoader). Por defecto "t5xxl_fp8_e4m3fn_scaled.safetensors".
|
||||
vae: nombre del VAE en models/vae/ (vae_name del VAELoader). Por defecto
|
||||
"ae.safetensors" (el autoencoder de Flux).
|
||||
width: ancho del latente/imagen en px (multiplo de 16 para SD3/Flux). keyword-only.
|
||||
height: alto del latente/imagen en px (multiplo de 16 para SD3/Flux). keyword-only.
|
||||
steps: pasos de sampling del KSampler. Flux schnell rinde bien con ~4;
|
||||
Flux dev necesita ~20. keyword-only.
|
||||
guidance: valor del nodo FluxGuidance (no es el cfg clasico). Schnell es
|
||||
poco sensible a este valor; dev responde a 3.0-4.0. keyword-only.
|
||||
seed: semilla del KSampler (0 = determinista; cambia para variar). keyword-only.
|
||||
weight_dtype: dtype de carga del UNET (uno de "default", "fp8_e4m3fn",
|
||||
"fp8_e4m3fn_fast", "fp8_e5m2"). fp8 reduce VRAM (clave en 8GB). keyword-only.
|
||||
sampler_name: nombre del sampler (Flux usa "euler"). keyword-only.
|
||||
scheduler: scheduler del sampler (Flux usa "simple"). keyword-only.
|
||||
filename_prefix: prefijo del PNG que SaveImage escribe en output/. keyword-only.
|
||||
prompt: prompt positivo (lo que se quiere ver). Flux ignora el negativo.
|
||||
variant: "schnell" (rapido, ~4 pasos, sin FluxGuidance) o "dev"
|
||||
(~20 pasos, con FluxGuidance). keyword-only.
|
||||
width: ancho del latente/imagen en px (multiplo de 8). keyword-only.
|
||||
height: alto del latente/imagen en px (multiplo de 8). keyword-only.
|
||||
steps: pasos de sampling. Si None, default por variante (schnell=4,
|
||||
dev=20). keyword-only.
|
||||
guidance: valor de FluxGuidance. Solo se aplica en variant="dev"; en
|
||||
schnell se ignora (el modelo distilado lleva la guia fija).
|
||||
keyword-only.
|
||||
seed: semilla de RandomNoise (cambia para variar la imagen). keyword-only.
|
||||
unet_name: nombre del modelo de difusion en UNETLoader. Si None, default
|
||||
por variante. keyword-only.
|
||||
clip_l_name: nombre del encoder CLIP-L en DualCLIPLoader. keyword-only.
|
||||
t5xxl_name: nombre del encoder T5-XXL en DualCLIPLoader. keyword-only.
|
||||
vae_name: nombre del VAE en VAELoader. keyword-only.
|
||||
weight_dtype: dtype de los pesos del UNET ("default", "fp8_e4m3fn",
|
||||
"fp8_e4m3fn_fast", "fp8_e5m2"). keyword-only.
|
||||
sampler_name: sampler para KSamplerSelect (ej. "euler"). keyword-only.
|
||||
scheduler: scheduler para BasicScheduler (ej. "simple"). keyword-only.
|
||||
filename_prefix: prefijo del PNG generado por SaveImage en output/.
|
||||
keyword-only.
|
||||
available: mapa opcional para validar que los modelos existen en el
|
||||
servidor, con claves opcionales "unet", "clip", "vae", cada una una
|
||||
lista de nombres disponibles (tal como /object_info los expone). Si
|
||||
se pasa y algun modelo elegido no esta en su lista, se lanza
|
||||
FileNotFoundError indicando que falta y en que carpeta colocarlo.
|
||||
Si es None (default), no se valida disco. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow. Las claves son
|
||||
node_ids (string) y cada valor tiene class_type + inputs.
|
||||
node_ids string y cada valor tiene class_type + inputs.
|
||||
|
||||
Raises:
|
||||
ValueError: si variant no es "schnell" ni "dev".
|
||||
FileNotFoundError: si `available` se pasa y algun modelo (unet/clip/vae)
|
||||
no esta disponible en el servidor; el mensaje lista los que faltan y
|
||||
la carpeta donde colocarlos. La funcion NO crashea de forma opaca:
|
||||
falla con un error claro y accionable antes de enviar nada a la GPU.
|
||||
"""
|
||||
return {
|
||||
"10": {
|
||||
if variant not in ("schnell", "dev"):
|
||||
raise ValueError(
|
||||
f"comfyui_build_flux_workflow: variant '{variant}' invalido; "
|
||||
f"usa 'schnell' o 'dev'"
|
||||
)
|
||||
|
||||
unet = unet_name or _DEFAULT_UNET[variant]
|
||||
n_steps = steps if steps is not None else _DEFAULT_STEPS[variant]
|
||||
|
||||
# Error path: validar contra los modelos que expone el servidor, si el caller
|
||||
# nos pasa el mapa. Pura (no toca disco; recibe las listas ya obtenidas).
|
||||
if available is not None:
|
||||
missing = []
|
||||
checks = (
|
||||
("unet", unet, available.get("unet")),
|
||||
("clip", clip_l_name, available.get("clip")),
|
||||
("clip", t5xxl_name, available.get("clip")),
|
||||
("vae", vae_name, available.get("vae")),
|
||||
)
|
||||
for role, name, names in checks:
|
||||
if names is not None and name not in names:
|
||||
missing.append(
|
||||
f" - '{name}' (rol {role}) no esta en el servidor; "
|
||||
f"colocalo en {_MODEL_DIRS[role]}"
|
||||
)
|
||||
if missing:
|
||||
raise FileNotFoundError(
|
||||
"comfyui_build_flux_workflow: faltan modelos Flux en el "
|
||||
"servidor:\n" + "\n".join(missing)
|
||||
)
|
||||
|
||||
# Loaders (Flux no usa CheckpointLoaderSimple).
|
||||
workflow: dict = {
|
||||
"12": {
|
||||
"class_type": "UNETLoader",
|
||||
"inputs": {"unet_name": unet, "weight_dtype": weight_dtype},
|
||||
},
|
||||
"11": {
|
||||
"class_type": "DualCLIPLoader",
|
||||
"inputs": {
|
||||
"clip_name1": t5xxl,
|
||||
"clip_name2": clip_l,
|
||||
"clip_name1": t5xxl_name,
|
||||
"clip_name2": clip_l_name,
|
||||
"type": "flux",
|
||||
},
|
||||
},
|
||||
"12": {
|
||||
"10": {
|
||||
"class_type": "VAELoader",
|
||||
"inputs": {"vae_name": vae},
|
||||
"inputs": {"vae_name": vae_name},
|
||||
},
|
||||
"5": {
|
||||
"class_type": "EmptyLatentImage",
|
||||
"inputs": {"width": width, "height": height, "batch_size": 1},
|
||||
},
|
||||
"6": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": prompt, "clip": ["11", 0]},
|
||||
},
|
||||
"13": {
|
||||
}
|
||||
|
||||
# Conditioning hacia BasicGuider. En dev pasa por FluxGuidance; en schnell va
|
||||
# directo (el modelo distilado no usa guidance externo).
|
||||
if variant == "dev":
|
||||
workflow["21"] = {
|
||||
"class_type": "FluxGuidance",
|
||||
"inputs": {"conditioning": ["6", 0], "guidance": guidance},
|
||||
},
|
||||
"7": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": "", "clip": ["11", 0]},
|
||||
},
|
||||
"5": {
|
||||
"class_type": "EmptySD3LatentImage",
|
||||
"inputs": {"width": width, "height": height, "batch_size": 1},
|
||||
},
|
||||
"3": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"seed": seed,
|
||||
"steps": steps,
|
||||
"cfg": 1.0,
|
||||
"sampler_name": sampler_name,
|
||||
"scheduler": scheduler,
|
||||
"denoise": 1.0,
|
||||
"model": ["10", 0],
|
||||
"positive": ["13", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["5", 0],
|
||||
}
|
||||
guider_cond = ["21", 0]
|
||||
else:
|
||||
guider_cond = ["6", 0]
|
||||
|
||||
workflow.update(
|
||||
{
|
||||
"25": {
|
||||
"class_type": "RandomNoise",
|
||||
"inputs": {"noise_seed": seed},
|
||||
},
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {"samples": ["3", 0], "vae": ["12", 0]},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": filename_prefix, "images": ["8", 0]},
|
||||
},
|
||||
}
|
||||
"16": {
|
||||
"class_type": "KSamplerSelect",
|
||||
"inputs": {"sampler_name": sampler_name},
|
||||
},
|
||||
"17": {
|
||||
"class_type": "BasicScheduler",
|
||||
"inputs": {
|
||||
"model": ["12", 0],
|
||||
"scheduler": scheduler,
|
||||
"steps": n_steps,
|
||||
"denoise": 1.0,
|
||||
},
|
||||
},
|
||||
"22": {
|
||||
"class_type": "BasicGuider",
|
||||
"inputs": {"model": ["12", 0], "conditioning": guider_cond},
|
||||
},
|
||||
"13": {
|
||||
"class_type": "SamplerCustomAdvanced",
|
||||
"inputs": {
|
||||
"noise": ["25", 0],
|
||||
"guider": ["22", 0],
|
||||
"sampler": ["16", 0],
|
||||
"sigmas": ["17", 0],
|
||||
"latent_image": ["5", 0],
|
||||
},
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {"samples": ["13", 0], "vae": ["10", 0]},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": filename_prefix, "images": ["8", 0]},
|
||||
},
|
||||
}
|
||||
)
|
||||
return workflow
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
wf = comfyui_build_flux_workflow(
|
||||
prompt="a red apple on a wooden table, sharp focus, studio lighting",
|
||||
"a red apple on a wooden table, sharp focus, studio light",
|
||||
variant="schnell",
|
||||
width=1024,
|
||||
height=1024,
|
||||
seed=42,
|
||||
)
|
||||
print(json.dumps(wf, indent=2))
|
||||
|
||||
@@ -78,6 +78,21 @@ CheckpointLoaderSimple -> ... -> KSampler -> VAEDecode --IMAGE--+-> SaveImage (f
|
||||
`-> DepthAnythingV2Preprocessor -> SaveImage (depth)
|
||||
```
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from ml.comfyui_build_parallax_background_workflow import comfyui_build_parallax_background_workflow
|
||||
|
||||
# Fondo apaisado + su mapa de profundidad, 4 bandas de parallax (función pura, sin red).
|
||||
wf = comfyui_build_parallax_background_workflow("forest at dusk, fantasy", layers=4, seed=7)
|
||||
|
||||
# El dict API format trae DOS SaveImage: el fondo y el depth map. Encólalo con submit_workflow.
|
||||
saves = [n for n in wf.values() if n.get("class_type") == "SaveImage"]
|
||||
print(len(saves), "SaveImage (fondo + depth)") # 2
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites el fondo de un nivel 2D con scroll parallax y quieras las capas
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: comfyui_extract_template
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_extract_template(name: str, comfyui_python: str | None = None, to_api: bool = False, server: str = \"127.0.0.1:8188\") -> dict"
|
||||
description: "Extrae el grafo de nodos de un workflow template oficial de ComfyUI por su template_id. Devuelve el grafo completo (formato UI: nodes/links), la lista de class_types que usa (aplanando subgrafos y descartando UUID de instancia), el formato, el bundle y los assets en disco. Opcionalmente (to_api=True) convierte el grafo UI a API format reutilizando comfyui_import_workflow_json (requiere un servidor ComfyUI vivo). Nombre inexistente -> error legible con sugerencias, sin traceback. Localiza el interprete de ComfyUI y usa su API oficial via subprocess. Impura: lee disco (+ red opcional si to_api)."
|
||||
tags: [comfyui, ml, templates, workflow, extract]
|
||||
uses_functions: ["comfyui_import_workflow_json_py_ml"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: name
|
||||
desc: "template_id exacto del template (p.ej. 'sdxl_simple_example', 'image_sdxl'). Usa comfyui_list_templates para ver los nombres disponibles."
|
||||
- name: comfyui_python
|
||||
desc: "Ruta al interprete python de ComfyUI con el paquete comfyui-workflow-templates. None autodetecta (env COMFYUI_PYTHON, ~/ComfyUI/.venv/bin/python)."
|
||||
- name: to_api
|
||||
desc: "True intenta convertir el grafo UI a API format via comfyui_import_workflow_json (requiere servidor ComfyUI vivo en `server`). Si falla, el grafo UI se devuelve igualmente y el motivo va en api_error."
|
||||
- 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: 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"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Lanzable directo (grafo slim + class_types de un template concreto):
|
||||
python/.venv/bin/python3 python/functions/ml/comfyui_extract_template.py sdxl_simple_example
|
||||
|
||||
# Con conversion a API format (necesita ComfyUI corriendo en 127.0.0.1:8188):
|
||||
python/.venv/bin/python3 python/functions/ml/comfyui_extract_template.py sdxl_simple_example --to-api
|
||||
```
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_extract_template import comfyui_extract_template
|
||||
|
||||
res = comfyui_extract_template("sdxl_simple_example")
|
||||
print(res["format"], res["n_nodes"], "nodos") # ui_graph 25 nodos
|
||||
print(res["class_types"]) # ['CheckpointLoaderSimple', 'KSamplerAdvanced', ...]
|
||||
graph = res["graph"] # dict cargable en la UI tal cual
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras reutilizar la estructura de nodos de un template oficial: cargar su
|
||||
grafo en tu UI, usarlo de base para un workflow propio, o saber exactamente que
|
||||
class_types encadena. Segundo paso del flujo listar (`comfyui_list_templates`) ->
|
||||
extraer. Para encolar el resultado en `/prompt` usa `to_api=True` (o pasa el grafo por
|
||||
`comfyui_import_workflow_json`).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El grafo viene en **formato UI** (nodes/links con posiciones), no en API format. La
|
||||
UI de ComfyUI lo entiende tal cual (cargalo o copia el dict); para `/prompt` hay que
|
||||
convertirlo a API format con `to_api=True`.
|
||||
- `to_api=True` reutiliza `comfyui_import_workflow_json`, que necesita un **servidor
|
||||
ComfyUI vivo** para mapear los widgets a sus claves de input. Sin servidor, la
|
||||
extraccion del grafo UI sigue funcionando (ok=True) y el motivo del fallo de
|
||||
conversion va en `api_error` (no rompe). KISS: no se fuerza la conversion.
|
||||
- Templates **subgraphed** (con `definitions.subgraphs`, `has_subgraphs=True`): la
|
||||
conversion a API NO expande el subgraph (limitacion de la normalizacion UI->API
|
||||
estandar), asi que `api_workflow` puede quedar con solo los nodos top-level. Para
|
||||
esos, cargar el grafo UI en la UI es lo fiable. `class_types` sí incluye los nodos
|
||||
reales de dentro del subgraph.
|
||||
- Nombre inexistente -> `ok=False` con `error` legible y sugerencias por substring (o
|
||||
difflib). No lanza traceback.
|
||||
- El paquete vive en el venv de ComfyUI; si no se encuentra el interprete o el paquete,
|
||||
`ok=False` indicando `pip install comfyui-workflow-templates`.
|
||||
@@ -0,0 +1,302 @@
|
||||
"""Extrae el grafo de nodos de un workflow template oficial de ComfyUI por su nombre.
|
||||
|
||||
Funcion impura: lee disco (el .json del template instalado) ejecutando la API oficial
|
||||
del paquete comfyui-workflow-templates dentro del interprete de ComfyUI.
|
||||
|
||||
Dado el nombre de un template (su template_id, p.ej. "image_sdxl" o
|
||||
"api_bfl_flux2_max_sofa_swap"), devuelve:
|
||||
- graph: el dict completo del .json (formato UI: nodes/links con posiciones).
|
||||
- class_types: la lista de tipos de nodo (class_type) que usa, aplanando los
|
||||
subgrafos de `definitions` si los hay.
|
||||
- format: "ui_graph" (lo normal en los templates) o "api".
|
||||
- assets: rutas en disco de los ficheros del template (json + previews .webp).
|
||||
|
||||
Opcionalmente (to_api=True) intenta convertir el grafo UI a API format reutilizando
|
||||
comfyui_import_workflow_json del registry. Esa conversion necesita un servidor ComfyUI
|
||||
vivo para mapear los widgets a sus claves de input; si no lo hay, se devuelve el grafo
|
||||
UI + class_types igualmente y se reporta el motivo en api_error (KISS: no se fuerza la
|
||||
conversion de grafos complejos).
|
||||
|
||||
El paquete vive en el venv de ComfyUI (no en el del registry), por eso esta funcion no
|
||||
lo importa: localiza el interprete de ComfyUI y le pasa un script que usa la API oficial.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _THIS_DIR not in sys.path:
|
||||
sys.path.insert(0, _THIS_DIR)
|
||||
|
||||
|
||||
# Script que corre DENTRO del python de ComfyUI. Resuelve un template por id, vuelca su
|
||||
# grafo + metadata como JSON. Si no existe, devuelve sugerencias cercanas.
|
||||
_EXTRACT_SCRIPT = r"""
|
||||
import json, sys, difflib, re
|
||||
try:
|
||||
import comfyui_workflow_templates_core as core
|
||||
except Exception as exc:
|
||||
print(json.dumps({"__err__": "import", "msg": str(exc)}))
|
||||
sys.exit(0)
|
||||
|
||||
_UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
|
||||
|
||||
TID = json.loads({tid_json!r})
|
||||
m = core.load_manifest()
|
||||
if TID not in m.templates:
|
||||
near = [k for k in m.templates if TID.lower() in k.lower()][:8]
|
||||
if not near:
|
||||
near = difflib.get_close_matches(TID, list(m.templates.keys()), n=8, cutoff=0.6)
|
||||
print(json.dumps({"__err__": "not_found", "suggestions": near}))
|
||||
sys.exit(0)
|
||||
|
||||
entry = m.templates[TID]
|
||||
json_asset = next((a.filename for a in entry.assets if a.filename.endswith(".json")), None)
|
||||
if not json_asset:
|
||||
print(json.dumps({"__err__": "no_json"}))
|
||||
sys.exit(0)
|
||||
|
||||
path = core.get_asset_path(TID, json_asset)
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
graph = json.load(fh)
|
||||
|
||||
# Detecta formato y extrae class_types.
|
||||
fmt = "unknown"
|
||||
class_types = set()
|
||||
has_subgraphs = False
|
||||
if isinstance(graph, dict) and isinstance(graph.get("nodes"), list):
|
||||
fmt = "ui_graph"
|
||||
for n in graph["nodes"]:
|
||||
t = n.get("type") if isinstance(n, dict) else None
|
||||
if t and not _UUID_RE.match(str(t)):
|
||||
class_types.add(t)
|
||||
defs = graph.get("definitions")
|
||||
if isinstance(defs, dict) and isinstance(defs.get("subgraphs"), list):
|
||||
for sg in defs["subgraphs"]:
|
||||
for n in (sg.get("nodes") or []) if isinstance(sg, dict) else []:
|
||||
if isinstance(n, dict) and n.get("type"):
|
||||
has_subgraphs = True
|
||||
if not _UUID_RE.match(str(n["type"])):
|
||||
class_types.add(n["type"])
|
||||
elif isinstance(graph, dict):
|
||||
fmt = "api"
|
||||
for v in graph.values():
|
||||
if isinstance(v, dict) and v.get("class_type"):
|
||||
class_types.add(v["class_type"])
|
||||
|
||||
print(json.dumps({
|
||||
"graph": graph,
|
||||
"class_types": sorted(class_types),
|
||||
"format": fmt,
|
||||
"has_subgraphs": has_subgraphs,
|
||||
"bundle": entry.bundle,
|
||||
"version": entry.version,
|
||||
"assets": core.resolve_all_assets(TID),
|
||||
"json_path": path,
|
||||
}))
|
||||
"""
|
||||
|
||||
|
||||
def _find_comfyui_python(explicit: str | None) -> str | None:
|
||||
"""Localiza un interprete de ComfyUI con el paquete instalado (ver list_templates)."""
|
||||
candidates = []
|
||||
if explicit:
|
||||
candidates.append(os.path.expanduser(explicit))
|
||||
env = os.environ.get("COMFYUI_PYTHON")
|
||||
if env:
|
||||
candidates.append(os.path.expanduser(env))
|
||||
candidates += [
|
||||
os.path.expanduser("~/ComfyUI/.venv/bin/python"),
|
||||
os.path.expanduser("~/ComfyUI/venv/bin/python"),
|
||||
os.path.expanduser("~/comfyui/.venv/bin/python"),
|
||||
sys.executable,
|
||||
]
|
||||
for c in candidates:
|
||||
if c and os.path.isfile(c):
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def comfyui_extract_template(
|
||||
name: str,
|
||||
comfyui_python: str | None = None,
|
||||
to_api: bool = False,
|
||||
server: str = "127.0.0.1:8188",
|
||||
) -> dict:
|
||||
"""Extrae el grafo y los class_types de un template oficial de ComfyUI por nombre.
|
||||
|
||||
Args:
|
||||
name: template_id exacto del template (p.ej. "image_sdxl"). Usa
|
||||
comfyui_list_templates para ver los nombres disponibles.
|
||||
comfyui_python: ruta al interprete python de ComfyUI con el paquete
|
||||
comfyui-workflow-templates. Si None, se autodetecta.
|
||||
to_api: si True, intenta convertir el grafo UI a API format reutilizando
|
||||
comfyui_import_workflow_json (requiere un servidor ComfyUI vivo en
|
||||
`server`). Si la conversion falla, se devuelve el grafo UI igualmente y
|
||||
el motivo va en api_error.
|
||||
server: host:port del servidor ComfyUI para la conversion to_api.
|
||||
|
||||
Returns:
|
||||
dict {ok, name, format, class_types, has_subgraphs, n_nodes, graph,
|
||||
api_workflow, api_error, bundle, version, assets, error}:
|
||||
- graph: el dict del template en formato UI (o API si ya lo estaba).
|
||||
- class_types: lista ordenada de tipos de nodo del grafo (incluye los de
|
||||
subgrafos de `definitions`).
|
||||
- api_workflow: dict en API format si to_api tuvo exito, si no {}.
|
||||
Nunca lanza. Nombre inexistente -> ok=False con error legible + sugerencias.
|
||||
"""
|
||||
py = _find_comfyui_python(comfyui_python)
|
||||
base = {
|
||||
"ok": False,
|
||||
"name": name,
|
||||
"format": "",
|
||||
"class_types": [],
|
||||
"has_subgraphs": False,
|
||||
"n_nodes": 0,
|
||||
"graph": {},
|
||||
"api_workflow": {},
|
||||
"api_error": "",
|
||||
"bundle": "",
|
||||
"version": "",
|
||||
"assets": [],
|
||||
"error": "",
|
||||
}
|
||||
if not py:
|
||||
base["error"] = (
|
||||
"no se encontro un interprete de ComfyUI. Pasa comfyui_python=... o "
|
||||
"define COMFYUI_PYTHON. Instala el paquete con: "
|
||||
"pip install comfyui-workflow-templates"
|
||||
)
|
||||
return base
|
||||
|
||||
script = _EXTRACT_SCRIPT.replace("{tid_json!r}", repr(json.dumps(name)))
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[py, "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
base["error"] = f"fallo al ejecutar el interprete de ComfyUI ({py}): {exc}"
|
||||
return base
|
||||
|
||||
if proc.returncode != 0:
|
||||
base["error"] = f"el interprete de ComfyUI fallo: {proc.stderr.strip()[:500]}"
|
||||
return base
|
||||
|
||||
try:
|
||||
data = json.loads(proc.stdout.strip().splitlines()[-1])
|
||||
except Exception as exc: # noqa: BLE001
|
||||
base["error"] = f"salida no parseable del interprete de ComfyUI: {exc}"
|
||||
return base
|
||||
|
||||
err = data.get("__err__")
|
||||
if err == "import":
|
||||
base["error"] = (
|
||||
f"el paquete comfyui-workflow-templates no esta instalado en {py} "
|
||||
f"({data.get('msg', '')}). Instalalo con: "
|
||||
"pip install comfyui-workflow-templates"
|
||||
)
|
||||
return base
|
||||
if err == "not_found":
|
||||
sug = data.get("suggestions", [])
|
||||
hint = f" ¿Quizas: {', '.join(sug)}?" if sug else ""
|
||||
base["error"] = f"template '{name}' no existe en el paquete.{hint}"
|
||||
return base
|
||||
if err == "no_json":
|
||||
base["error"] = f"el template '{name}' no tiene asset .json."
|
||||
return base
|
||||
|
||||
graph = data.get("graph", {})
|
||||
fmt = data.get("format", "")
|
||||
nodes = graph.get("nodes") if isinstance(graph, dict) else None
|
||||
n_nodes = len(nodes) if isinstance(nodes, list) else (
|
||||
len(graph) if fmt == "api" and isinstance(graph, dict) else 0
|
||||
)
|
||||
|
||||
out = {
|
||||
"ok": True,
|
||||
"name": name,
|
||||
"format": fmt,
|
||||
"class_types": data.get("class_types", []),
|
||||
"has_subgraphs": data.get("has_subgraphs", False),
|
||||
"n_nodes": n_nodes,
|
||||
"graph": graph,
|
||||
"api_workflow": {},
|
||||
"api_error": "",
|
||||
"bundle": data.get("bundle", ""),
|
||||
"version": data.get("version", ""),
|
||||
"assets": data.get("assets", []),
|
||||
"error": "",
|
||||
}
|
||||
|
||||
if to_api:
|
||||
if fmt == "api":
|
||||
out["api_workflow"] = graph
|
||||
else:
|
||||
out["api_workflow"], out["api_error"] = _convert_to_api(graph, server)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _convert_to_api(graph: dict, server: str) -> tuple[dict, str]:
|
||||
"""Convierte un grafo UI a API format via comfyui_import_workflow_json del registry.
|
||||
|
||||
Requiere un servidor ComfyUI vivo para mapear widgets. Devuelve (workflow, "")
|
||||
si tuvo exito o ({}, motivo) si fallo. No lanza.
|
||||
"""
|
||||
try:
|
||||
from comfyui_import_workflow_json import comfyui_import_workflow_json
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {}, f"no se pudo importar comfyui_import_workflow_json: {exc}"
|
||||
|
||||
tmp = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
"w", suffix=".json", delete=False, encoding="utf-8"
|
||||
) as fh:
|
||||
json.dump(graph, fh)
|
||||
tmp = fh.name
|
||||
res = comfyui_import_workflow_json(tmp, server=server)
|
||||
if res.get("ok"):
|
||||
return res.get("workflow", {}), ""
|
||||
return {}, (
|
||||
res.get("error", "conversion fallida")
|
||||
+ f" (requiere un servidor ComfyUI vivo en {server})"
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {}, f"conversion to_api fallida: {exc}"
|
||||
finally:
|
||||
if tmp and os.path.exists(tmp):
|
||||
try:
|
||||
os.unlink(tmp)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
ap = argparse.ArgumentParser(description="Extrae el grafo de un template ComfyUI")
|
||||
ap.add_argument("name", help="template_id (ver comfyui_list_templates)")
|
||||
ap.add_argument("--comfyui-python", default=None)
|
||||
ap.add_argument("--to-api", action="store_true")
|
||||
ap.add_argument("--server", default="127.0.0.1:8188")
|
||||
ap.add_argument("--full", action="store_true", help="incluye el grafo entero")
|
||||
args = ap.parse_args()
|
||||
|
||||
res = comfyui_extract_template(
|
||||
args.name,
|
||||
args.comfyui_python,
|
||||
to_api=args.to_api,
|
||||
server=args.server,
|
||||
)
|
||||
if args.full or not res["ok"]:
|
||||
print(json.dumps(res, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
slim = {k: v for k, v in res.items() if k != "graph"}
|
||||
slim["graph_keys"] = list(res["graph"].keys()) if isinstance(res["graph"], dict) else []
|
||||
print(json.dumps(slim, indent=2, ensure_ascii=False))
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ name: comfyui_interrupt_queue
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_interrupt_queue(server: str = \"127.0.0.1:8188\") -> dict"
|
||||
description: "Corta la generacion en curso de ComfyUI (POST /interrupt) y devuelve el estado de la cola (GET /queue). Devuelve {ok, interrupted, queue_running, queue_pending, error}. NO lanza excepcion en fallo de red: degrada a {ok: False, error}. /interrupt corta solo el prompt en ejecucion, no vacia los pendientes. Impura: HTTP POST + GET, solo stdlib (urllib, json)."
|
||||
signature: "def comfyui_interrupt_queue(*, clear_pending: bool = False, server: str = \"127.0.0.1:8188\", timeout: float = 10.0) -> dict"
|
||||
description: "Corta la generacion en curso de ComfyUI (POST /interrupt) y, si clear_pending=True, vacia ademas la cola de pendientes (POST /queue {\"clear\":true}). Consulta GET /queue al final para reportar queue_remaining. Devuelve {ok, interrupted, cleared, queue_remaining, error}. NO lanza excepcion en fallo de red: degrada a {ok: False, error}. /interrupt corta solo el prompt en ejecucion, no vacia los pendientes salvo clear_pending. Impura: HTTP POST + GET, solo stdlib (urllib, json)."
|
||||
tags: [comfyui, ml, queue, interrupt, control, http]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
@@ -15,12 +15,16 @@ returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: clear_pending
|
||||
desc: "keyword-only. Si True, ademas de cortar el prompt en ejecucion vacia la cola de pendientes con POST /queue {\"clear\":true}. Default False."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema (default '127.0.0.1:8188')."
|
||||
output: "dict con ok (bool, True si interrupt + lectura de cola OK), interrupted (bool, True si POST /interrupt respondio), queue_running (int, prompts ejecutandose), queue_pending (int, prompts encolados), error (str, vacio si todo OK)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
desc: "keyword-only. host:port del servidor ComfyUI sin esquema (default '127.0.0.1:8188')."
|
||||
- name: timeout
|
||||
desc: "keyword-only. Timeout de cada peticion HTTP en segundos (default 10.0)."
|
||||
output: "dict con ok (bool, True si interrupt + clear (si se pidio) + lectura de cola OK), interrupted (bool, True si POST /interrupt respondio), cleared (bool, True si clear_pending y POST /queue {clear:true} respondio; False si no se pidio o fallo), queue_remaining (int, queue_running + queue_pending tras la operacion), error (str, vacio si todo OK)."
|
||||
tested: true
|
||||
tests: ["test_interrumpe_sin_vaciar", "test_clear_pending_vacia_cola", "test_clear_pending_cola_vacia_no_rompe", "test_servidor_caido_no_lanza"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_interrupt_queue.py"
|
||||
file_path: "python/functions/ml/comfyui_interrupt_queue.py"
|
||||
---
|
||||
|
||||
@@ -31,30 +35,47 @@ import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_interrupt_queue import comfyui_interrupt_queue
|
||||
|
||||
# Solo cortar el prompt en ejecucion (los pendientes siguen):
|
||||
res = comfyui_interrupt_queue()
|
||||
# {'ok': True, 'interrupted': True, 'queue_running': 0, 'queue_pending': 0, 'error': ''}
|
||||
if res["ok"] and res["interrupted"]:
|
||||
print(f"cortado; pendientes en cola: {res['queue_pending']}")
|
||||
# {'ok': True, 'interrupted': True, 'cleared': False, 'queue_remaining': 3, 'error': ''}
|
||||
|
||||
# Cortar el actual Y vaciar los pendientes de golpe:
|
||||
res = comfyui_interrupt_queue(clear_pending=True)
|
||||
# {'ok': True, 'interrupted': True, 'cleared': True, 'queue_remaining': 0, 'error': ''}
|
||||
if res["ok"]:
|
||||
print(f"cortado; quedan {res['queue_remaining']} en cola")
|
||||
```
|
||||
|
||||
O lanzable directo con: `./fn run comfyui_interrupt_queue`.
|
||||
O lanzable directo: `./fn run comfyui_interrupt_queue` · `./fn run comfyui_interrupt_queue --clear`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Para abortar una generacion que se esta tomando demasiado, que tira de mas VRAM de
|
||||
la prevista, o tras encolar por error un workflow pesado. Tambien para inspeccionar
|
||||
de un vistazo cuanto queda en cola (`queue_running` / `queue_pending`) sin parsear
|
||||
el JSON de /queue a mano. Es el freno de mano del round-trip build -> submit -> wait.
|
||||
la prevista, o tras encolar por error un workflow pesado. Con `clear_pending=True`
|
||||
es el freno de mano completo: corta el actual y borra todo lo encolado en una sola
|
||||
llamada (sin tener que encadenar `comfyui_queue_manage("clear")` despues). Tras la
|
||||
operacion `queue_remaining` dice de un vistazo cuanto queda en cola.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `/interrupt` corta SOLO el prompt en ejecucion; los pendientes (`queue_pending`)
|
||||
siguen y el siguiente arranca de inmediato. Para vaciar la cola entera hay que
|
||||
llamar `POST /queue` con `{"clear": true}` (no lo hace esta funcion — solo corta
|
||||
+ lee).
|
||||
- `/interrupt` corta SOLO el prompt en ejecucion; sin `clear_pending` los pendientes
|
||||
(`queue_pending`) siguen y el siguiente arranca de inmediato. Pasa
|
||||
`clear_pending=True` para vaciar tambien la cola (POST /queue {"clear": true}).
|
||||
- No es idempotente en el sentido de "sin efecto": si hay algo ejecutandose, lo
|
||||
mata. Si la cola esta vacia, el interrupt es inocuo (interrupted=True igual).
|
||||
mata. Si la cola esta vacia, tanto el interrupt como el clear son inocuos
|
||||
(`interrupted=True`/`cleared=True` igual, `queue_remaining=0`).
|
||||
- `queue_remaining` se lee al FINAL (GET /queue tras interrupt+clear): es
|
||||
`queue_running + queue_pending`. Justo tras un interrupt sin clear puede ser >0
|
||||
porque el siguiente pendiente ya arranco.
|
||||
- En fallo de red NO lanza: devuelve `ok=False` con el mensaje en `error`. Comprueba
|
||||
`ok` antes de fiarte de los conteos.
|
||||
`ok` antes de fiarte de `queue_remaining`.
|
||||
- Tras el interrupt conviene liberar VRAM con `POST /free` si vas a encolar otro
|
||||
trabajo pesado (esta funcion no lo hace).
|
||||
trabajo pesado (esta funcion no lo hace; ver el round-trip build -> submit -> wait).
|
||||
- Para operaciones de cola mas finas (borrar UN prompt por id, contar el historial)
|
||||
usa `comfyui_queue_manage`; esta funcion se centra en el interrupt + clear masivo.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-06-28) — anade flag `clear_pending` (vacia la cola en la misma
|
||||
llamada) + param `timeout`; el output pasa a {ok, interrupted, cleared,
|
||||
queue_remaining, error} y se anaden tests (mock HTTP local).
|
||||
|
||||
@@ -1,38 +1,52 @@
|
||||
"""Interrumpe la generacion en curso de ComfyUI y devuelve el estado de la cola.
|
||||
"""Interrumpe la generacion en curso de ComfyUI y, opcionalmente, vacia la cola.
|
||||
|
||||
Funcion impura: hace red (HTTP POST /interrupt + GET /queue). Solo stdlib.
|
||||
Funcion impura: hace red (HTTP POST /interrupt, POST /queue, GET /queue). Solo
|
||||
stdlib (urllib, json).
|
||||
|
||||
POST /interrupt corta el prompt que ComfyUI esta ejecutando ahora mismo (no vacia
|
||||
la cola: los prompts pendientes siguen). GET /queue devuelve queue_running (lo que
|
||||
se ejecuta) y queue_pending (lo encolado). Esta funcion combina ambos en un dict
|
||||
honesto que NO lanza excepcion en fallo de red: devuelve {ok: False, error}.
|
||||
POST /interrupt corta el prompt que ComfyUI esta ejecutando ahora mismo: NO vacia
|
||||
los pendientes, solo aborta el actual y el siguiente arranca de inmediato. Para
|
||||
vaciar de golpe los pendientes hay que ademas hacer POST /queue con {"clear": true}
|
||||
(lo que activa el flag clear_pending). GET /queue se consulta al final para reportar
|
||||
cuantos trabajos quedan en cola tras la operacion (queue_remaining).
|
||||
|
||||
NO lanza excepcion en fallo de red: devuelve un dict de estado {ok: False, error}.
|
||||
"""
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def comfyui_interrupt_queue(server: str = "127.0.0.1:8188") -> dict:
|
||||
"""Interrumpe la generacion en curso y devuelve el estado de la cola.
|
||||
def comfyui_interrupt_queue(
|
||||
clear_pending: bool = False,
|
||||
server: str = "127.0.0.1:8188",
|
||||
timeout: float = 10.0,
|
||||
) -> dict:
|
||||
"""Corta la generacion en curso de ComfyUI y devuelve el estado de la cola.
|
||||
|
||||
Args:
|
||||
clear_pending: si True, ademas de cortar el prompt en ejecucion vacia la
|
||||
cola de pendientes con POST /queue {"clear": true}. keyword-only.
|
||||
server: host:port del servidor ComfyUI sin esquema (default
|
||||
"127.0.0.1:8188").
|
||||
"127.0.0.1:8188"). keyword-only.
|
||||
timeout: timeout de cada peticion HTTP en segundos (default 10.0).
|
||||
keyword-only.
|
||||
|
||||
Returns:
|
||||
dict con:
|
||||
- ok (bool): True si tanto el interrupt como la lectura de la cola
|
||||
tuvieron exito.
|
||||
- ok (bool): True si el interrupt, la lectura de la cola y (si se pidio)
|
||||
el clear tuvieron exito.
|
||||
- interrupted (bool): True si el POST /interrupt respondio sin error.
|
||||
- queue_running (int): numero de prompts ejecutandose ahora mismo.
|
||||
- queue_pending (int): numero de prompts encolados pendientes.
|
||||
- cleared (bool): True si clear_pending era True y el POST /queue
|
||||
{"clear": true} respondio sin error; False si no se pidio o fallo.
|
||||
- queue_remaining (int): trabajos que quedan en cola tras la operacion
|
||||
(queue_running + queue_pending segun GET /queue al final).
|
||||
- error (str): mensaje de error si algo fallo; cadena vacia si todo OK.
|
||||
"""
|
||||
out = {
|
||||
"ok": False,
|
||||
"interrupted": False,
|
||||
"queue_running": 0,
|
||||
"queue_pending": 0,
|
||||
"cleared": False,
|
||||
"queue_remaining": 0,
|
||||
"error": "",
|
||||
}
|
||||
base = f"http://{server}"
|
||||
@@ -40,19 +54,37 @@ def comfyui_interrupt_queue(server: str = "127.0.0.1:8188") -> dict:
|
||||
# 1. POST /interrupt (cuerpo vacio): corta el prompt en ejecucion.
|
||||
try:
|
||||
req = urllib.request.Request(f"{base}/interrupt", data=b"", method="POST")
|
||||
with urllib.request.urlopen(req, timeout=10.0):
|
||||
with urllib.request.urlopen(req, timeout=timeout):
|
||||
out["interrupted"] = True
|
||||
except urllib.error.URLError as exc:
|
||||
reason = getattr(exc, "reason", exc)
|
||||
out["error"] = f"interrupt fallo: no se pudo conectar a {base}/interrupt: {reason}"
|
||||
return out
|
||||
|
||||
# 2. GET /queue: estado actual de la cola tras el interrupt.
|
||||
# 2. Opcional: POST /queue {"clear": true} para vaciar los pendientes.
|
||||
if clear_pending:
|
||||
try:
|
||||
payload = json.dumps({"clear": True}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{base}/queue",
|
||||
data=payload,
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=timeout):
|
||||
out["cleared"] = True
|
||||
except urllib.error.URLError as exc:
|
||||
reason = getattr(exc, "reason", exc)
|
||||
out["error"] = f"clear fallo: no se pudo conectar a {base}/queue: {reason}"
|
||||
return out
|
||||
|
||||
# 3. GET /queue: cuantos trabajos quedan en cola tras la operacion.
|
||||
try:
|
||||
with urllib.request.urlopen(f"{base}/queue", timeout=10.0) as resp:
|
||||
with urllib.request.urlopen(f"{base}/queue", timeout=timeout) as resp:
|
||||
data = json.loads(resp.read())
|
||||
out["queue_running"] = len(data.get("queue_running", []))
|
||||
out["queue_pending"] = len(data.get("queue_pending", []))
|
||||
running = len(data.get("queue_running", []))
|
||||
pending = len(data.get("queue_pending", []))
|
||||
out["queue_remaining"] = running + pending
|
||||
out["ok"] = True
|
||||
except urllib.error.URLError as exc:
|
||||
reason = getattr(exc, "reason", exc)
|
||||
@@ -63,9 +95,12 @@ def comfyui_interrupt_queue(server: str = "127.0.0.1:8188") -> dict:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
res = comfyui_interrupt_queue()
|
||||
import sys
|
||||
|
||||
clear = "--clear" in sys.argv[1:]
|
||||
res = comfyui_interrupt_queue(clear_pending=clear)
|
||||
print(
|
||||
f"ok={res['ok']} interrupted={res['interrupted']} "
|
||||
f"running={res['queue_running']} pending={res['queue_pending']} "
|
||||
f"cleared={res['cleared']} queue_remaining={res['queue_remaining']} "
|
||||
f"error={res['error']!r}"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: comfyui_list_templates
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_list_templates(comfyui_python: str | None = None, bundle: str | None = None, name_filter: str | None = None, with_nodes: bool = True, workflows_only: bool = True, limit: int = 0) -> dict"
|
||||
description: "Lista los workflow templates oficiales del paquete pip comfyui-workflow-templates (los del menu 'Browse Templates' del frontend de ComfyUI). Devuelve nombre, bundle/categoria, path en disco, n_nodes y node_types (class_types reales, aplanando subgrafos y descartando los UUID de instancia). Localiza el interprete de ComfyUI y usa su API oficial via subprocess (el paquete vive en el venv de ComfyUI, no en el del registry). Impura: lee disco. Filtra entradas no-workflow (index*/localizacion) por defecto."
|
||||
tags: [comfyui, ml, templates, workflow, discovery]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: comfyui_python
|
||||
desc: "Ruta al interprete python de ComfyUI con el paquete comfyui-workflow-templates instalado. None autodetecta (env COMFYUI_PYTHON, ~/ComfyUI/.venv/bin/python, ~/ComfyUI/venv/bin/python)."
|
||||
- name: bundle
|
||||
desc: "Filtra por bundle exacto: 'media-api', 'media-image', 'media-video' o 'media-other'. None = todos."
|
||||
- name: name_filter
|
||||
desc: "Subcadena (case-insensitive) que debe contener el nombre del template. None = sin filtro."
|
||||
- name: with_nodes
|
||||
desc: "True (default) incluye node_types en cada registro; False los omite (registros mas ligeros)."
|
||||
- name: workflows_only
|
||||
desc: "True (default) excluye entradas que no son grafos de workflow (ficheros index*/localizacion del paquete)."
|
||||
- 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: 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"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Lanzable directo (muestra version del paquete + 15 primeros con sus node_types):
|
||||
./fn run comfyui_list_templates
|
||||
|
||||
# Filtrado por bundle de imagen, sin abrir node_types, primeros 20:
|
||||
python/.venv/bin/python3 python/functions/ml/comfyui_list_templates.py \
|
||||
--bundle media-image --no-nodes --limit 20
|
||||
```
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_list_templates import comfyui_list_templates
|
||||
|
||||
res = comfyui_list_templates(name_filter="sdxl")
|
||||
print(res["count"], "templates SDXL") # p.ej. 4
|
||||
for t in res["templates"]:
|
||||
print(t["name"], t["n_nodes"], t["node_types"][:3])
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Para descubrir que workflow templates oficiales trae ComfyUI sin abrir la UI:
|
||||
explorar el catalogo, filtrar por bundle/nombre, o saber que `node_types` usa cada
|
||||
template antes de extraerlo con `comfyui_extract_template`. Primer paso del flujo
|
||||
listar -> extraer -> (cargar en UI / convertir a API).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El paquete `comfyui-workflow-templates` vive en el venv de ComfyUI, NO en el del
|
||||
registry. La funcion no lo importa: localiza el python de ComfyUI y corre su API
|
||||
oficial en un subprocess. Si no encuentra ese interprete (o el paquete no esta
|
||||
instalado) devuelve `ok=False` con un error que dice como instalarlo. No lanza.
|
||||
- Desde la 0.10.x el paquete es multi-bundle y ya NO expone una carpeta `templates/`
|
||||
unica (la API antigua `get_templates_path()` lanza a proposito). Por eso se usa
|
||||
`comfyui_workflow_templates_core` (`load_manifest`/`get_asset_path`).
|
||||
- `node_types` aplana los subgrafos de `definitions` y descarta los `type` que son
|
||||
UUID (instancias de subgraph), para mostrar class_types reales (KSampler, CLIPLoader,
|
||||
…) en vez de identificadores opacos. `n_nodes` cuenta solo los nodos top-level.
|
||||
- `workflows_only=True` (default) excluye ~16 entradas `index*` que son metadata de
|
||||
localizacion del frontend, no grafos. Pasa `workflows_only=False` (o `--all` en CLI)
|
||||
para verlas.
|
||||
- Impura: abre cada `.json` en disco (≈451 ficheros pequeños, ~0.2s). No toca red ni
|
||||
arranca GPU.
|
||||
@@ -0,0 +1,284 @@
|
||||
"""Lista los workflow templates oficiales que trae el paquete comfyui-workflow-templates.
|
||||
|
||||
Funcion impura: lee disco (los .json de los templates instalados) ejecutando la
|
||||
API oficial del paquete dentro del interprete de ComfyUI.
|
||||
|
||||
ComfyUI 0.26+ distribuye los templates oficiales (los del menu "Browse Templates"
|
||||
del frontend) en el paquete pip `comfyui-workflow-templates`, que desde la 0.10.x es
|
||||
un meta-paquete multi-bundle: ya NO expone una carpeta `templates/` unica, sino una
|
||||
API en `comfyui_workflow_templates_core` (`load_manifest`, `iter_templates`,
|
||||
`get_asset_path`). Cada template es un grafo de nodos en formato UI (nodes/links con
|
||||
posiciones), agrupado en uno de cuatro bundles: media-api, media-image, media-video,
|
||||
media-other.
|
||||
|
||||
Como el paquete vive en el venv de ComfyUI (no en el del registry), esta funcion no
|
||||
lo importa directamente: localiza el interprete de ComfyUI y le pasa un script que usa
|
||||
la API oficial y vuelca el catalogo como JSON. Asi es robusta ante cambios de la
|
||||
estructura interna del paquete.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
# Script que corre DENTRO del python de ComfyUI. Usa la API oficial del paquete y
|
||||
# vuelca el catalogo (metadata + node_types por template) como una linea JSON.
|
||||
_DUMP_SCRIPT = r"""
|
||||
import json, sys, re
|
||||
try:
|
||||
import comfyui_workflow_templates_core as core
|
||||
except Exception as exc:
|
||||
print(json.dumps({"__err__": "import", "msg": str(exc)}))
|
||||
sys.exit(0)
|
||||
|
||||
_UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
|
||||
|
||||
def _collect_types(graph):
|
||||
# Recoge class_types reales: aplana los subgrafos de definitions y descarta los
|
||||
# type que son UUID (instancias de subgraph, cuyo contenido real ya se incluye).
|
||||
types = set()
|
||||
if isinstance(graph, dict) and isinstance(graph.get("nodes"), list):
|
||||
for n in graph["nodes"]:
|
||||
if isinstance(n, dict) and n.get("type") and not _UUID_RE.match(str(n["type"])):
|
||||
types.add(n["type"])
|
||||
defs = graph.get("definitions")
|
||||
if isinstance(defs, dict) and isinstance(defs.get("subgraphs"), list):
|
||||
for sg in defs["subgraphs"]:
|
||||
for n in (sg.get("nodes") or []) if isinstance(sg, dict) else []:
|
||||
if isinstance(n, dict) and n.get("type") and not _UUID_RE.match(str(n["type"])):
|
||||
types.add(n["type"])
|
||||
return len(graph["nodes"]), sorted(types)
|
||||
if isinstance(graph, dict): # API format
|
||||
for v in graph.values():
|
||||
if isinstance(v, dict) and v.get("class_type"):
|
||||
types.add(v["class_type"])
|
||||
if types:
|
||||
return len(graph), sorted(types)
|
||||
return 0, []
|
||||
|
||||
WITH_NODES = {with_nodes}
|
||||
m = core.load_manifest()
|
||||
try:
|
||||
import importlib.metadata as _md
|
||||
pkg_version = _md.version("comfyui-workflow-templates")
|
||||
except Exception:
|
||||
pkg_version = ""
|
||||
|
||||
out = []
|
||||
for tid, entry in m.templates.items():
|
||||
json_asset = next(
|
||||
(a.filename for a in entry.assets if a.filename.endswith(".json")), None
|
||||
)
|
||||
path = core.get_asset_path(tid, json_asset) if json_asset else ""
|
||||
rec = {
|
||||
"name": tid,
|
||||
"bundle": entry.bundle,
|
||||
"category": entry.bundle,
|
||||
"version": entry.version,
|
||||
"path": path,
|
||||
"n_nodes": 0,
|
||||
"node_types": [],
|
||||
}
|
||||
rec["is_workflow"] = False
|
||||
if path:
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
graph = json.load(fh)
|
||||
n_nodes, node_types = _collect_types(graph)
|
||||
is_api = isinstance(graph, dict) and any(
|
||||
isinstance(v, dict) and v.get("class_type") for v in graph.values()
|
||||
)
|
||||
rec["is_workflow"] = bool(
|
||||
(isinstance(graph, dict) and isinstance(graph.get("nodes"), list) and graph["nodes"])
|
||||
or is_api
|
||||
)
|
||||
rec["n_nodes"] = n_nodes
|
||||
if WITH_NODES:
|
||||
rec["node_types"] = node_types
|
||||
except Exception:
|
||||
pass
|
||||
out.append(rec)
|
||||
|
||||
print(json.dumps({"package_version": pkg_version, "templates": out}))
|
||||
"""
|
||||
|
||||
|
||||
def _find_comfyui_python(explicit: str | None) -> str | None:
|
||||
"""Devuelve la ruta a un interprete de ComfyUI que tenga el paquete instalado.
|
||||
|
||||
Orden de busqueda: argumento explicito -> env COMFYUI_PYTHON -> candidatos
|
||||
habituales (~/ComfyUI/.venv, ~/ComfyUI/venv) -> el python actual. Devuelve None
|
||||
si ninguno existe en disco.
|
||||
"""
|
||||
candidates = []
|
||||
if explicit:
|
||||
candidates.append(os.path.expanduser(explicit))
|
||||
env = os.environ.get("COMFYUI_PYTHON")
|
||||
if env:
|
||||
candidates.append(os.path.expanduser(env))
|
||||
candidates += [
|
||||
os.path.expanduser("~/ComfyUI/.venv/bin/python"),
|
||||
os.path.expanduser("~/ComfyUI/venv/bin/python"),
|
||||
os.path.expanduser("~/comfyui/.venv/bin/python"),
|
||||
sys.executable,
|
||||
]
|
||||
for c in candidates:
|
||||
if c and os.path.isfile(c):
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def comfyui_list_templates(
|
||||
comfyui_python: str | None = None,
|
||||
bundle: str | None = None,
|
||||
name_filter: str | None = None,
|
||||
with_nodes: bool = True,
|
||||
workflows_only: bool = True,
|
||||
limit: int = 0,
|
||||
) -> dict:
|
||||
"""Lista los templates oficiales de ComfyUI con su grafo de nodos.
|
||||
|
||||
Args:
|
||||
comfyui_python: ruta al interprete python de ComfyUI que tiene instalado
|
||||
el paquete comfyui-workflow-templates. Si None, se autodetecta (env
|
||||
COMFYUI_PYTHON o ~/ComfyUI/.venv/bin/python).
|
||||
bundle: si se da, filtra por bundle exacto ("media-api", "media-image",
|
||||
"media-video", "media-other").
|
||||
name_filter: si se da, filtra a templates cuyo nombre contenga esta
|
||||
subcadena (case-insensitive).
|
||||
with_nodes: si True (default) incluye node_types en cada registro. Si
|
||||
False los omite (registros mas ligeros).
|
||||
workflows_only: si True (default) excluye entradas que no son grafos de
|
||||
workflow (ficheros index*/localizacion del paquete).
|
||||
limit: si > 0, trunca la lista a los primeros N tras filtrar.
|
||||
|
||||
Returns:
|
||||
dict {ok, count, package_version, templates, error}:
|
||||
- templates: lista de {name, category, bundle, version, path, n_nodes,
|
||||
node_types} ordenada por name.
|
||||
- count: numero de templates devueltos (tras filtros y limit).
|
||||
Nunca lanza: cualquier fallo (paquete ausente, interprete no hallado)
|
||||
devuelve ok=False con un error legible.
|
||||
"""
|
||||
py = _find_comfyui_python(comfyui_python)
|
||||
if not py:
|
||||
return {
|
||||
"ok": False,
|
||||
"count": 0,
|
||||
"package_version": "",
|
||||
"templates": [],
|
||||
"error": (
|
||||
"no se encontro un interprete de ComfyUI. Pasa comfyui_python=... "
|
||||
"o define COMFYUI_PYTHON. El paquete se instala con: "
|
||||
"pip install comfyui-workflow-templates"
|
||||
),
|
||||
}
|
||||
|
||||
script = _DUMP_SCRIPT.replace("{with_nodes}", "True" if with_nodes else "False")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[py, "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {
|
||||
"ok": False,
|
||||
"count": 0,
|
||||
"package_version": "",
|
||||
"templates": [],
|
||||
"error": f"fallo al ejecutar el interprete de ComfyUI ({py}): {exc}",
|
||||
}
|
||||
|
||||
if proc.returncode != 0:
|
||||
return {
|
||||
"ok": False,
|
||||
"count": 0,
|
||||
"package_version": "",
|
||||
"templates": [],
|
||||
"error": f"el interprete de ComfyUI fallo: {proc.stderr.strip()[:500]}",
|
||||
}
|
||||
|
||||
try:
|
||||
data = json.loads(proc.stdout.strip().splitlines()[-1])
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {
|
||||
"ok": False,
|
||||
"count": 0,
|
||||
"package_version": "",
|
||||
"templates": [],
|
||||
"error": f"salida no parseable del interprete de ComfyUI: {exc}",
|
||||
}
|
||||
|
||||
if data.get("__err__") == "import":
|
||||
return {
|
||||
"ok": False,
|
||||
"count": 0,
|
||||
"package_version": "",
|
||||
"templates": [],
|
||||
"error": (
|
||||
"el paquete comfyui-workflow-templates no esta instalado en "
|
||||
f"{py} ({data.get('msg', '')}). Instalalo con: "
|
||||
"pip install comfyui-workflow-templates"
|
||||
),
|
||||
}
|
||||
|
||||
templates = data.get("templates", [])
|
||||
if workflows_only:
|
||||
templates = [t for t in templates if t.get("is_workflow")]
|
||||
if bundle:
|
||||
templates = [t for t in templates if t.get("bundle") == bundle]
|
||||
if name_filter:
|
||||
nf = name_filter.lower()
|
||||
templates = [t for t in templates if nf in t.get("name", "").lower()]
|
||||
templates.sort(key=lambda t: t.get("name", ""))
|
||||
if limit and limit > 0:
|
||||
templates = templates[:limit]
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"count": len(templates),
|
||||
"package_version": data.get("package_version", ""),
|
||||
"templates": templates,
|
||||
"error": "",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
ap = argparse.ArgumentParser(description="Lista templates oficiales de ComfyUI")
|
||||
ap.add_argument("--comfyui-python", default=None)
|
||||
ap.add_argument("--bundle", default=None)
|
||||
ap.add_argument("--name-filter", default=None)
|
||||
ap.add_argument("--no-nodes", action="store_true", help="omite node_types")
|
||||
ap.add_argument("--all", action="store_true", help="incluye entradas no-workflow (index*)")
|
||||
ap.add_argument("--limit", type=int, default=0)
|
||||
ap.add_argument("--full", action="store_true", help="dump completo (todos los node_types)")
|
||||
args = ap.parse_args()
|
||||
|
||||
res = comfyui_list_templates(
|
||||
args.comfyui_python,
|
||||
bundle=args.bundle,
|
||||
name_filter=args.name_filter,
|
||||
with_nodes=not args.no_nodes,
|
||||
workflows_only=not args.all,
|
||||
limit=args.limit,
|
||||
)
|
||||
if args.full or not res["ok"]:
|
||||
print(json.dumps(res, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": res["ok"],
|
||||
"count": res["count"],
|
||||
"package_version": res["package_version"],
|
||||
"sample": res["templates"][:15],
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
@@ -3,6 +3,8 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
@@ -10,35 +12,54 @@ from ml.comfyui_build_flux_workflow import comfyui_build_flux_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_estructura_y_class_types():
|
||||
wf = comfyui_build_flux_workflow("POS")
|
||||
_BASE_CTS = {
|
||||
"UNETLoader",
|
||||
"DualCLIPLoader",
|
||||
"VAELoader",
|
||||
"EmptyLatentImage",
|
||||
"CLIPTextEncode",
|
||||
"RandomNoise",
|
||||
"KSamplerSelect",
|
||||
"BasicScheduler",
|
||||
"BasicGuider",
|
||||
"SamplerCustomAdvanced",
|
||||
"VAEDecode",
|
||||
"SaveImage",
|
||||
}
|
||||
|
||||
|
||||
def test_schnell_class_types_sin_fluxguidance():
|
||||
wf = comfyui_build_flux_workflow("POS", variant="schnell")
|
||||
assert_api_format(wf)
|
||||
assert class_types(wf) == {
|
||||
"UNETLoader",
|
||||
"DualCLIPLoader",
|
||||
"VAELoader",
|
||||
"CLIPTextEncode",
|
||||
"FluxGuidance",
|
||||
"EmptySD3LatentImage",
|
||||
"KSampler",
|
||||
"VAEDecode",
|
||||
"SaveImage",
|
||||
}
|
||||
# schnell usa el camino custom-advanced y NO incluye FluxGuidance.
|
||||
assert class_types(wf) == _BASE_CTS
|
||||
# BasicGuider consume el CLIPTextEncode positivo directo.
|
||||
assert node_by_ct(wf, "BasicGuider")["inputs"]["conditioning"] == ["6", 0]
|
||||
|
||||
|
||||
def test_dev_class_types_con_fluxguidance():
|
||||
wf = comfyui_build_flux_workflow("POS", variant="dev", guidance=2.5)
|
||||
assert_api_format(wf)
|
||||
assert class_types(wf) == _BASE_CTS | {"FluxGuidance"}
|
||||
fg = node_by_ct(wf, "FluxGuidance")["inputs"]
|
||||
assert fg["guidance"] == 2.5
|
||||
assert fg["conditioning"] == ["6", 0] # FluxGuidance aplica sobre el positivo
|
||||
# BasicGuider consume la salida de FluxGuidance, no el CLIPTextEncode directo.
|
||||
assert node_by_ct(wf, "BasicGuider")["inputs"]["conditioning"] == ["21", 0]
|
||||
|
||||
|
||||
def test_loaders_separados_de_flux():
|
||||
# Flux carga UNET + dos text encoders + VAE por separado (no checkpoint unico).
|
||||
wf = comfyui_build_flux_workflow(
|
||||
"POS",
|
||||
unet="IMG_flux1-schnell-fp8-e4m3fn.safetensors",
|
||||
clip_l="clip_l.safetensors",
|
||||
t5xxl="t5xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
vae="ae.safetensors",
|
||||
weight_dtype="fp8_e4m3fn",
|
||||
variant="schnell",
|
||||
clip_l_name="clip_l.safetensors",
|
||||
t5xxl_name="t5xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
vae_name="ae.safetensors",
|
||||
)
|
||||
unet = node_by_ct(wf, "UNETLoader")["inputs"]
|
||||
assert unet["unet_name"] == "IMG_flux1-schnell-fp8-e4m3fn.safetensors"
|
||||
assert unet["weight_dtype"] == "fp8_e4m3fn"
|
||||
assert unet["weight_dtype"] == "default"
|
||||
dual = node_by_ct(wf, "DualCLIPLoader")["inputs"]
|
||||
assert dual["type"] == "flux"
|
||||
assert dual["clip_name1"] == "t5xxl_fp8_e4m3fn_scaled.safetensors"
|
||||
@@ -46,25 +67,36 @@ def test_loaders_separados_de_flux():
|
||||
assert node_by_ct(wf, "VAELoader")["inputs"]["vae_name"] == "ae.safetensors"
|
||||
|
||||
|
||||
def test_guidance_y_cfg_de_flux():
|
||||
# La guia va por FluxGuidance; el cfg del KSampler se fija a 1.0 (schnell).
|
||||
wf = comfyui_build_flux_workflow("POS", guidance=2.5)
|
||||
assert node_by_ct(wf, "FluxGuidance")["inputs"]["guidance"] == 2.5
|
||||
ks = node_by_ct(wf, "KSampler")["inputs"]
|
||||
assert ks["cfg"] == 1.0
|
||||
# KSampler positive consume la salida de FluxGuidance, no la del CLIPTextEncode directo.
|
||||
assert ks["positive"] == ["13", 0]
|
||||
def test_unet_default_por_variante():
|
||||
schnell = comfyui_build_flux_workflow("POS", variant="schnell")
|
||||
dev = comfyui_build_flux_workflow("POS", variant="dev")
|
||||
assert (
|
||||
node_by_ct(schnell, "UNETLoader")["inputs"]["unet_name"]
|
||||
== "IMG_flux1-schnell-fp8-e4m3fn.safetensors"
|
||||
)
|
||||
assert (
|
||||
node_by_ct(dev, "UNETLoader")["inputs"]["unet_name"]
|
||||
== "IMG_flux1-dev-fp8-e4m3fn.safetensors"
|
||||
)
|
||||
|
||||
|
||||
def test_steps_default_por_variante():
|
||||
schnell = comfyui_build_flux_workflow("POS", variant="schnell")
|
||||
dev = comfyui_build_flux_workflow("POS", variant="dev")
|
||||
assert node_by_ct(schnell, "BasicScheduler")["inputs"]["steps"] == 4
|
||||
assert node_by_ct(dev, "BasicScheduler")["inputs"]["steps"] == 20
|
||||
# steps explicito gana al default.
|
||||
custom = comfyui_build_flux_workflow("POS", variant="schnell", steps=6)
|
||||
assert node_by_ct(custom, "BasicScheduler")["inputs"]["steps"] == 6
|
||||
|
||||
|
||||
def test_params_se_reflejan_en_los_nodos():
|
||||
wf = comfyui_build_flux_workflow("POS", width=768, height=512, steps=8, seed=123)
|
||||
ks = node_by_ct(wf, "KSampler")["inputs"]
|
||||
assert ks["seed"] == 123
|
||||
assert ks["steps"] == 8
|
||||
lat = node_by_ct(wf, "EmptySD3LatentImage")["inputs"]
|
||||
wf = comfyui_build_flux_workflow(
|
||||
"POS", variant="schnell", width=768, height=512, seed=123
|
||||
)
|
||||
assert node_by_ct(wf, "RandomNoise")["inputs"]["noise_seed"] == 123
|
||||
lat = node_by_ct(wf, "EmptyLatentImage")["inputs"]
|
||||
assert lat["width"] == 768 and lat["height"] == 512
|
||||
pos = node_by_ct(wf, "FluxGuidance")["inputs"]["conditioning"]
|
||||
assert pos == ["6", 0] # FluxGuidance aplica sobre el CLIPTextEncode positivo
|
||||
|
||||
|
||||
def test_filename_prefix_en_saveimage():
|
||||
@@ -72,8 +104,36 @@ def test_filename_prefix_en_saveimage():
|
||||
assert node_by_ct(wf, "SaveImage")["inputs"]["filename_prefix"] == "demo_flux"
|
||||
|
||||
|
||||
def test_variant_invalido_lanza_valueerror():
|
||||
with pytest.raises(ValueError):
|
||||
comfyui_build_flux_workflow("POS", variant="turbo")
|
||||
|
||||
|
||||
def test_available_valida_modelos_faltantes():
|
||||
# Si se pasa 'available' y un modelo elegido no esta, lanza FileNotFoundError
|
||||
# con el nombre que falta (error path: no crashea opaco).
|
||||
available = {
|
||||
"unet": ["otro_modelo.safetensors"], # el schnell por defecto NO esta
|
||||
"clip": ["clip_l.safetensors", "t5xxl_fp8_e4m3fn_scaled.safetensors"],
|
||||
"vae": ["ae.safetensors"],
|
||||
}
|
||||
with pytest.raises(FileNotFoundError) as exc:
|
||||
comfyui_build_flux_workflow("POS", variant="schnell", available=available)
|
||||
assert "IMG_flux1-schnell-fp8-e4m3fn.safetensors" in str(exc.value)
|
||||
|
||||
|
||||
def test_available_ok_no_lanza():
|
||||
available = {
|
||||
"unet": ["IMG_flux1-schnell-fp8-e4m3fn.safetensors"],
|
||||
"clip": ["clip_l.safetensors", "t5xxl_fp8_e4m3fn_scaled.safetensors"],
|
||||
"vae": ["ae.safetensors"],
|
||||
}
|
||||
wf = comfyui_build_flux_workflow("POS", variant="schnell", available=available)
|
||||
assert_api_format(wf)
|
||||
|
||||
|
||||
def test_determinista():
|
||||
# Builder puro: misma entrada -> mismo dict (sin red, seed fijo, sin estado).
|
||||
a = comfyui_build_flux_workflow("POS", seed=123)
|
||||
b = comfyui_build_flux_workflow("POS", seed=123)
|
||||
a = comfyui_build_flux_workflow("POS", variant="dev", seed=123)
|
||||
b = comfyui_build_flux_workflow("POS", variant="dev", seed=123)
|
||||
assert a == b
|
||||
|
||||
@@ -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"]
|
||||
+1
-1
@@ -7,7 +7,7 @@ servidor ComfyUI vivo.
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
from comfyui_fetch_output_audio import _find_audio_output, _is_audio_item
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Tests de comfyui_interrupt_queue contra un servidor ComfyUI simulado.
|
||||
|
||||
La funcion es pura I/O (HTTP), asi que levantamos un http.server local que imita
|
||||
los endpoints relevantes de ComfyUI (/interrupt, /queue) y verificamos:
|
||||
|
||||
- Golden: interrupt sin clear corta el actual pero NO vacia los pendientes.
|
||||
- Edge: clear_pending=True vacia la cola (queue_remaining=0).
|
||||
- Edge: clear_pending=True con la cola ya vacia no rompe.
|
||||
- Error: si el servidor no responde, devuelve {ok:False, error} sin lanzar.
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_interrupt_queue import comfyui_interrupt_queue
|
||||
|
||||
|
||||
class _FakeComfyHandler(http.server.BaseHTTPRequestHandler):
|
||||
"""Imita ComfyUI: estado de cola mutable compartido via la clase del server."""
|
||||
|
||||
def log_message(self, *args): # silenciar el log del servidor en los tests
|
||||
pass
|
||||
|
||||
def _send_json(self, obj, code=200):
|
||||
body = json.dumps(obj).encode()
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_POST(self):
|
||||
st = self.server.state
|
||||
if self.path == "/interrupt":
|
||||
st["running"] = [] # interrupt corta el prompt en ejecucion
|
||||
self._send_json({})
|
||||
return
|
||||
if self.path == "/queue":
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
raw = self.rfile.read(length) if length else b"{}"
|
||||
body = json.loads(raw or b"{}")
|
||||
if body.get("clear"):
|
||||
st["pending"] = [] # clear vacia los pendientes
|
||||
elif "delete" in body:
|
||||
st["pending"] = [
|
||||
p for p in st["pending"] if p not in body["delete"]
|
||||
]
|
||||
self._send_json({})
|
||||
return
|
||||
self._send_json({"error": "not found"}, code=404)
|
||||
|
||||
def do_GET(self):
|
||||
st = self.server.state
|
||||
if self.path == "/queue":
|
||||
self._send_json(
|
||||
{
|
||||
"queue_running": st["running"],
|
||||
"queue_pending": st["pending"],
|
||||
}
|
||||
)
|
||||
return
|
||||
self._send_json({"error": "not found"}, code=404)
|
||||
|
||||
|
||||
def _start_fake_server(running, pending):
|
||||
"""Levanta el servidor fake en un puerto efimero. Devuelve (server, addr, thread)."""
|
||||
server = http.server.HTTPServer(("127.0.0.1", 0), _FakeComfyHandler)
|
||||
server.state = {"running": list(running), "pending": list(pending)}
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
host, port = server.server_address
|
||||
return server, f"{host}:{port}", thread
|
||||
|
||||
|
||||
def _free_port():
|
||||
"""Reserva y libera un puerto para garantizar que NADA escucha ahi (error path)."""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind(("127.0.0.1", 0))
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
return port
|
||||
|
||||
|
||||
def test_interrumpe_sin_vaciar():
|
||||
# Golden: 1 ejecutandose + 2 pendientes; interrupt corta el actual, pendientes siguen.
|
||||
server, addr, _ = _start_fake_server(running=["r1"], pending=["p1", "p2"])
|
||||
try:
|
||||
res = comfyui_interrupt_queue(server=addr)
|
||||
finally:
|
||||
server.shutdown()
|
||||
assert res["ok"] is True
|
||||
assert res["interrupted"] is True
|
||||
assert res["cleared"] is False
|
||||
# running cortado (0) + 2 pendientes que siguen = 2 restantes.
|
||||
assert res["queue_remaining"] == 2
|
||||
assert res["error"] == ""
|
||||
|
||||
|
||||
def test_clear_pending_vacia_cola():
|
||||
# Edge: clear_pending vacia los pendientes -> queue_remaining 0.
|
||||
server, addr, _ = _start_fake_server(running=["r1"], pending=["p1", "p2", "p3"])
|
||||
try:
|
||||
res = comfyui_interrupt_queue(clear_pending=True, server=addr)
|
||||
finally:
|
||||
server.shutdown()
|
||||
assert res["ok"] is True
|
||||
assert res["interrupted"] is True
|
||||
assert res["cleared"] is True
|
||||
assert res["queue_remaining"] == 0
|
||||
assert res["error"] == ""
|
||||
|
||||
|
||||
def test_clear_pending_cola_vacia_no_rompe():
|
||||
# Edge: clear_pending con la cola ya vacia es inocuo, no rompe.
|
||||
server, addr, _ = _start_fake_server(running=[], pending=[])
|
||||
try:
|
||||
res = comfyui_interrupt_queue(clear_pending=True, server=addr)
|
||||
finally:
|
||||
server.shutdown()
|
||||
assert res["ok"] is True
|
||||
assert res["interrupted"] is True
|
||||
assert res["cleared"] is True
|
||||
assert res["queue_remaining"] == 0
|
||||
assert res["error"] == ""
|
||||
|
||||
|
||||
def test_servidor_caido_no_lanza():
|
||||
# Error: nada escucha en el puerto -> {ok:False, error} sin excepcion cruda.
|
||||
dead = f"127.0.0.1:{_free_port()}"
|
||||
res = comfyui_interrupt_queue(server=dead, timeout=1.0)
|
||||
assert res["ok"] is False
|
||||
assert res["interrupted"] is False
|
||||
assert res["error"] != ""
|
||||
assert "interrupt fallo" in res["error"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_interrumpe_sin_vaciar()
|
||||
test_clear_pending_vacia_cola()
|
||||
test_clear_pending_cola_vacia_no_rompe()
|
||||
test_servidor_caido_no_lanza()
|
||||
print("OK: 4 tests passed")
|
||||
@@ -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