feat(ml): auto-commit con 11 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 01:39:30 +02:00
parent d7245efa59
commit d3f05a19a5
11 changed files with 1148 additions and 12 deletions
+31 -12
View File
@@ -42,6 +42,8 @@ El **API format** (dict de nodos numerados que produce `build_txt2img_workflow`
| [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_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. | | [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. |
| [comfyui_download_model_py_ml](../../python/functions/ml/comfyui_download_model.md) | `download_model(url, dest_subdir='checkpoints', *, comfyui_dir, filename, token, overwrite, timeout_s) -> dict` | Descarga un checkpoint/LoRA/VAE a `models/<dest_subdir>/`. Soporta Civitai (token) y HuggingFace. Valida que no sea HTML de error ni `.safetensors` corrupto. Impura. | | [comfyui_download_model_py_ml](../../python/functions/ml/comfyui_download_model.md) | `download_model(url, dest_subdir='checkpoints', *, comfyui_dir, filename, token, overwrite, timeout_s) -> dict` | Descarga un checkpoint/LoRA/VAE a `models/<dest_subdir>/`. Soporta Civitai (token) y HuggingFace. Valida que no sea HTML de error ni `.safetensors` corrupto. Impura. |
| [comfyui_interrupt_queue_py_ml](../../python/functions/ml/comfyui_interrupt_queue.md) | `interrupt_queue(server='127.0.0.1:8188') -> dict` | Corta la generación en curso (POST `/interrupt`) y lee la cola (GET `/queue`) → `{ok, interrupted, queue_running, queue_pending, error}`. Freno de mano; degrada limpio en fallo de red. Impura. |
| [comfyui_batch_generate_py_ml](../../python/functions/ml/comfyui_batch_generate.md) | `batch_generate(workflow, *, seeds=None, server='127.0.0.1:8188') -> dict` | Encola N variantes (una por seed), parcheando el campo de semilla de los nodos sampler sin mutar el original → `{ok, prompt_ids, count, error}`. Re-roll en una llamada. Compone `submit_workflow`. Impura. |
### Builders, validación e import — dominio `ml` (P0, issue 0064) ### Builders, validación e import — dominio `ml` (P0, issue 0064)
@@ -69,6 +71,18 @@ El **API format** (dict de nodos numerados que produce `build_txt2img_workflow`
| [comfyui_resolve_workflow_deps_py_ml](../../python/functions/ml/comfyui_resolve_workflow_deps.md) | `resolve_workflow_deps(workflow, server='127.0.0.1:8188') -> dict` | Para un workflow ajeno: valida y traduce lo que falta en acciones (`{missing_nodes, missing_models, suggestions}`). Compone `validate_workflow`. Impura. | | [comfyui_resolve_workflow_deps_py_ml](../../python/functions/ml/comfyui_resolve_workflow_deps.md) | `resolve_workflow_deps(workflow, server='127.0.0.1:8188') -> dict` | Para un workflow ajeno: valida y traduce lo que falta en acciones (`{missing_nodes, missing_models, suggestions}`). Compone `validate_workflow`. Impura. |
| [comfyui_list_installed_models_py_ml](../../python/functions/ml/comfyui_list_installed_models.md) | `list_installed_models(folder=None, comfyui_dir='~/ComfyUI') -> dict` | Lista modelos por carpeta resolviendo la ruta real de `extra_model_paths.yaml` (`/mnt/2tb/comfyui_models/`) + la nativa. Escaneo de FS, no depende del server. Impura. | | [comfyui_list_installed_models_py_ml](../../python/functions/ml/comfyui_list_installed_models.md) | `list_installed_models(folder=None, comfyui_dir='~/ComfyUI') -> dict` | Lista modelos por carpeta resolviendo la ruta real de `extra_model_paths.yaml` (`/mnt/2tb/comfyui_models/`) + la nativa. Escaneo de FS, no depende del server. Impura. |
### Vídeo (txt2video) — dominio `ml` (tag `video-generation`)
ComfyUI ≥ 0.26.0 trae soporte nativo para **vídeo por difusión**. `build_video_workflow` cubre
los dos modelos que caben en 8 GB: **LTX-Video 2B v0.9.5** (`model='ltx'`, checkpoint todo-en-uno +
VAE temporal + scheduler propio — validado end-to-end en `reports/0084`, clip real de 65 frames,
pico ~7.7 GB) y **Wan2.1 T2V 1.3B** (`model='wan'`, diffusion + umt5 + vae aparte — plantilla nativa
canónica). El resultado es un `.mp4` vía `CreateVideo → SaveVideo`.
| ID | Firma corta | Qué hace |
|---|---|---|
| [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**. |
### Imagen → 3D (Hunyuan3D-2 nativo) — dominio `ml` + `pipelines` (tag `img-to-3d`) ### Imagen → 3D (Hunyuan3D-2 nativo) — dominio `ml` + `pipelines` (tag `img-to-3d`)
ComfyUI ≥ 0.26.0 trae **soporte nativo de Hunyuan3D-2** (sin custom node): una imagen se ComfyUI ≥ 0.26.0 trae **soporte nativo de Hunyuan3D-2** (sin custom node): una imagen se
@@ -89,6 +103,7 @@ report `0079`).
| [comfyui_fetch_output_mesh_py_ml](../../python/functions/ml/comfyui_fetch_output_mesh.md) | `fetch_output_mesh(prompt_id, *, server, dest=None, timeout) -> dict` | Localiza la malla en `/history/{prompt_id}` (el SaveGLB la expone bajo la clave `"3d"`, no `"images"`) y la baja via GET `/view` a disco. Hermana de `fetch_output_image`. Impura. | | [comfyui_fetch_output_mesh_py_ml](../../python/functions/ml/comfyui_fetch_output_mesh.md) | `fetch_output_mesh(prompt_id, *, server, dest=None, timeout) -> dict` | Localiza la malla en `/history/{prompt_id}` (el SaveGLB la expone bajo la clave `"3d"`, no `"images"`) y la baja via GET `/view` a disco. Hermana de `fetch_output_image`. Impura. |
| [comfyui_install_3d_model_py_ml](../../python/functions/ml/comfyui_install_3d_model.md) | `install_3d_model(variant='mini', *, hf_token=None, comfyui_dir) -> dict` | Instala el checkpoint Hunyuan3D-2 (mini/standard/mv) en `checkpoints/`. Cascada: ya-instalado → cache de HF → descarga. Resuelve la ruta real via `extra_model_paths.yaml`. Impura. | | [comfyui_install_3d_model_py_ml](../../python/functions/ml/comfyui_install_3d_model.md) | `install_3d_model(variant='mini', *, hf_token=None, comfyui_dir) -> dict` | Instala el checkpoint Hunyuan3D-2 (mini/standard/mv) en `checkpoints/`. Cascada: ya-instalado → cache de HF → descarga. Resuelve la ruta real via `extra_model_paths.yaml`. Impura. |
| [comfyui_image_to_3d_oneshot_py_pipelines](../../python/functions/pipelines/comfyui_image_to_3d_oneshot.md) | `image_to_3d_oneshot(image_path, *, server, variant='mini', dest=None, wait_timeout, **gen) -> dict` | **Pipeline** imagen en disco → malla GLB en una llamada: upload + build + submit + wait + fetch. Promoción de la secuencia (issue 0087). Impuro. | | [comfyui_image_to_3d_oneshot_py_pipelines](../../python/functions/pipelines/comfyui_image_to_3d_oneshot.md) | `image_to_3d_oneshot(image_path, *, server, variant='mini', dest=None, wait_timeout, **gen) -> dict` | **Pipeline** imagen en disco → malla GLB en una llamada: upload + build + submit + wait + fetch. Promoción de la secuencia (issue 0087). Impuro. |
| [comfyui_build_textured_3d_multiview_workflow_py_ml](../../python/functions/ml/comfyui_build_textured_3d_multiview_workflow.md) | `build_textured_3d_multiview_workflow(image_name, *, ckpt='hunyuan3d-dit-v2-mv.safetensors', views=6, octree=384, max_faces=50000, upscale_model='4x_foolhardy_Remacri.pth') -> dict` | Builder imagen→malla 3D **con textura PBR** vía el wrapper Hunyuan3DWrapper (kijai): 4/6 vistas + delight + sample multi-vista + upscale Remacri + bake sobre UV (19 nodos). Cobertura de atlas 32.93% (report 0082). **Pura**. En 8 GB ejecutar en 2 fases (shape→`/free`→paint). |
### Por la UI web (CDP) — dominio `browser` ### Por la UI web (CDP) — dominio `browser`
@@ -175,30 +190,34 @@ Para tunear nodo a nodo en vez del oneshot: `build_image_to_3d_workflow(image_na
- **No es un grupo de generación genérica de imágenes**: cubre ComfyUI concretamente (su API - **No es un grupo de generación genérica de imágenes**: cubre ComfyUI concretamente (su API
y su frontend litegraph). Para otros backends (Automatic1111, diffusers) harían falta otras y su frontend litegraph). Para otros backends (Automatic1111, diffusers) harían falta otras
funciones. funciones.
- **Los builders cubren txt2img, img2img, upscale, LoRA stacks, inpaint, ControlNet y SDXL - **Los builders cubren txt2img, img2img, upscale, LoRA stacks, inpaint, ControlNet, SDXL
refiner** (`build_txt2img_workflow`, `build_img2img_workflow`, `build_upscale_workflow`, refiner, vídeo (LTX/Wan) y 3D texturizado multi-vista** (`build_txt2img_workflow`,
`inject_lora`, `build_inpaint_workflow`, `build_controlnet_workflow`, `build_sdxl_refiner_workflow`). `build_img2img_workflow`, `build_upscale_workflow`, `inject_lora`, `build_inpaint_workflow`,
Workflows aún más complejos (multi-ControlNet avanzado, IPAdapter, vídeo) se montan en la UI a mano `build_controlnet_workflow`, `build_sdxl_refiner_workflow`, `build_video_workflow`,
y se capturan con `export_workflow_ui`, o se importan de internet con `build_textured_3d_multiview_workflow`). Workflows aún más complejos (multi-ControlNet avanzado,
`import_workflow_json`/`import_workflow_png`, se resuelven sus dependencias con IPAdapter) se montan en la UI a mano y se capturan con `export_workflow_ui`, o se importan de
internet con `import_workflow_json`/`import_workflow_png`, se resuelven sus dependencias con
`resolve_workflow_deps` (instala nodos con `install_custom_node`, descubre modelos con `resolve_workflow_deps` (instala nodos con `install_custom_node`, descubre modelos con
`search_civitai_models`) y se validan con `validate_workflow` antes de encolar. `search_civitai_models`) y se validan con `validate_workflow` antes de encolar.
- **Los 9 builders puros tienen tests de estructura** (`python/functions/ml/tests/test_comfyui_build_*.py` - **Los 11 builders puros tienen tests de estructura** (`python/functions/ml/tests/test_comfyui_build_*.py`
+ `test_comfyui_inject_lora.py`): verifican los `class_type` esperados, que los parámetros se reflejan + `test_comfyui_inject_lora.py`): verifican los `class_type` esperados, que los parámetros se reflejan
en los nodos, la validez de las conexiones `[node_id, output_index]` y la pureza de `inject_lora`. Son en los nodos, la validez de las conexiones `[node_id, output_index]` y la pureza de `inject_lora`. Son
tests offline (no tocan GPU ni server); las funciones impuras del grupo (todo lo que habla con el server, tests offline (no tocan GPU ni server); las funciones impuras del grupo (todo lo que habla con el server,
el navegador o Civitai/HuggingFace) no se cubren con unit tests por diseño — se validan con el server vivo. el navegador o Civitai/HuggingFace) no se cubren con unit tests por diseño — se validan con el server vivo.
- **Control de cola**: `interrupt_queue` corta la generación en curso + lee `/queue`; `batch_generate`
encola N variantes por seed (re-roll). No vacían la cola entera (eso es `POST /queue {"clear": true}`).
- **Las funciones `*_ui` requieren la pestaña abierta y el navegador con CDP** (puerto 9222 por - **Las funciones `*_ui` requieren la pestaña abierta y el navegador con CDP** (puerto 9222 por
defecto). Sin target que matchee `server_url_substr`, devuelven `ok=False`. Para automatización defecto). Sin target que matchee `server_url_substr`, devuelven `ok=False`. Para automatización
desatendida sin navegador, usa el camino API (`submit_workflow` + `wait_result`). desatendida sin navegador, usa el camino API (`submit_workflow` + `wait_result`).
- **`download_model` no gestiona el catálogo del server**: tras bajar un modelo, llama - **`download_model` no gestiona el catálogo del server**: tras bajar un modelo, llama
`refresh_nodes_ui` (o recarga la página) para que ComfyUI lo vea en los combos. `refresh_nodes_ui` (o recarga la página) para que ComfyUI lo vea en los combos.
- **El camino imagen→3D es shape-only**: los nodos nativos de Hunyuan3D-2 - **El camino imagen→3D nativo es shape-only**: los nodos nativos de Hunyuan3D-2
(`build_image_to_3d_workflow`, `fetch_output_mesh`, `install_3d_model`, `image_to_3d_oneshot`) (`build_image_to_3d_workflow`, `fetch_output_mesh`, `install_3d_model`, `image_to_3d_oneshot`)
reconstruyen la FORMA, sin color ni textura horneada. Para color/textura haría falta el wrapper reconstruyen la FORMA, sin color ni textura horneada. Para **textura PBR** está
de kijai (compila `custom_rasterizer`) — fuera del grupo. Tampoco hay decimación: las mallas son `build_textured_3d_multiview_workflow`, que usa el wrapper de kijai (requiere `custom_rasterizer`
densas (decenas de MB de GLB). Decisión y comparación vs la app local en CUDA + `ComfyUI_essentials` + el upscaler Remacri) y debe ejecutarse en 2 fases en 8 GB
`reports/0069-2026-06-23-comfyui-img-to-3d.md`. (shape→`/free`→paint). Detalle y cobertura medida en `reports/0082`; shape-only y comparación vs la
app local en `reports/0069-2026-06-23-comfyui-img-to-3d.md`.
- La primitiva de transport CDP es [`cdp_eval`](../../python/functions/browser/cdp_eval.md) (grupo - La primitiva de transport CDP es [`cdp_eval`](../../python/functions/browser/cdp_eval.md) (grupo
navegador): si necesitas leer/escribir algo del grafo que estas funciones no cubren, compón navegador): si necesitas leer/escribir algo del grafo que estas funciones no cubren, compón
`cdp_eval` directamente antes de inventar nada. `cdp_eval` directamente antes de inventar nada.
@@ -0,0 +1,75 @@
---
name: comfyui_batch_generate
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: impure
signature: "def comfyui_batch_generate(workflow: dict, *, seeds: list | None = None, server: str = \"127.0.0.1:8188\") -> dict"
description: "Encola N variantes de un workflow ComfyUI, una por seed de la lista, parcheando el campo de semilla de los nodos sampler (KSampler.seed, KSamplerAdvanced/SamplerCustom.noise_seed) sin mutar el original (deepcopy), y recoge cada prompt_id. Compone comfyui_submit_workflow. Util para barridos de re-roll: misma escena, varias semillas, una sola llamada. Devuelve {ok, prompt_ids, count, error}. Impura: HTTP POST por variante, solo stdlib."
tags: [comfyui, ml, batch, seeds, queue, http]
uses_functions: ["comfyui_submit_workflow_py_ml"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: workflow
desc: "dict en API format (resultado de un builder). No se muta: cada variante es una copia profunda con la semilla parcheada."
- name: seeds
desc: "Lista de semillas (int); cada una produce una variante encolada. None o vacia encola el workflow tal cual una sola vez. keyword-only."
- name: server
desc: "host:port del servidor ComfyUI sin esquema (default '127.0.0.1:8188'). keyword-only."
output: "dict con ok (bool, True si TODAS las variantes se encolaron), prompt_ids (list[str] en orden de seeds, para comfyui_wait_result), count (int, variantes encoladas con exito), error (str, primer error; vacio si OK). Si una variante falla, detiene el barrido y devuelve los prompt_ids ya encolados."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/ml/comfyui_batch_generate.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
from ml.comfyui_batch_generate import comfyui_batch_generate
wf = comfyui_build_txt2img_workflow(
ckpt_name="v1-5-pruned-emaonly-fp16.safetensors",
positive="a red apple on a wooden table, sharp focus",
negative="blurry, low quality",
)
res = comfyui_batch_generate(wf, seeds=[1, 2, 3])
# {'ok': True, 'prompt_ids': ['<id1>', '<id2>', '<id3>'], 'count': 3, 'error': ''}
for pid in res["prompt_ids"]:
pass # comfyui_wait_result(pid) para recoger cada resultado
```
O lanzable directo (build txt2img + encolar 2 seeds) con: `./fn run comfyui_batch_generate`.
## Cuando usarla
Para generar varias variantes de la misma escena cambiando solo la semilla
(re-roll de calidad) en una sola llamada, en vez de editar el seed y reenviar a
mano N veces. Aplica a cualquier workflow con nodo sampler: txt2img, img2img,
video (parchea `noise_seed` del SamplerCustom de LTX), etc. Tras encolar, sigue
cada `prompt_id` con `comfyui_wait_result`.
## Gotchas
- Parchea TODO input llamado `seed` o `noise_seed` en cualquier nodo. Si un
workflow tiene varios samplers, todos reciben la misma semilla de la variante
(normalmente lo deseado). Si necesitas semillas independientes por sampler,
parchea a mano.
- Encolar tiene efecto secundario: arranca trabajo de GPU. N seeds = N prompts en
cola = N corridas de GPU en serie. En 8GB, no encoles 20 videos a la vez sin
vigilar VRAM/tiempo.
- `seeds=None` encola el workflow tal cual UNA vez (sin tocar la semilla): util
como "submit con la firma de batch".
- Fail-fast: si una variante es rechazada (HTTP 400), detiene el barrido,
devuelve `ok=False` + `error` y los `prompt_ids` ya encolados (no hace rollback
de los anteriores — ya estan en la cola del servidor).
- Si necesitas cortar un barrido a medias, usa `comfyui_interrupt_queue` (corta el
que se ejecuta) o `POST /queue {"clear": true}` para vaciar los pendientes.
@@ -0,0 +1,91 @@
"""Encola N variantes de un workflow ComfyUI, una por seed, y recoge los prompt_ids.
Funcion impura: hace red (POST /prompt por variante, via comfyui_submit_workflow).
Compone comfyui_submit_workflow.
Para cada seed de la lista, copia el workflow (deepcopy, no muta el original),
parchea el campo de semilla de los nodos sampler (KSampler.seed, KSamplerAdvanced.
noise_seed, SamplerCustom.noise_seed — en general cualquier input "seed"/"noise_seed")
y lo encola. Util para barridos de re-roll: misma escena, varias semillas, una sola
llamada. Devuelve los prompt_ids en el mismo orden que la lista de seeds; cada uno
se sigue con comfyui_wait_result.
"""
import copy
import os
import sys
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
if _THIS_DIR not in sys.path:
sys.path.insert(0, _THIS_DIR)
from comfyui_submit_workflow import comfyui_submit_workflow # noqa: E402
# Campos de semilla conocidos en los nodos sampler de ComfyUI.
_SEED_KEYS = ("seed", "noise_seed")
def _patch_seed(workflow: dict, seed: int) -> dict:
"""Copia el workflow y fija `seed` en todos los inputs de semilla (no muta el original)."""
wf = copy.deepcopy(workflow)
for node in wf.values():
inputs = node.get("inputs")
if not isinstance(inputs, dict):
continue
for key in _SEED_KEYS:
if key in inputs:
inputs[key] = seed
return wf
def comfyui_batch_generate(
workflow: dict,
*,
seeds: list | None = None,
server: str = "127.0.0.1:8188",
) -> dict:
"""Encola una variante del workflow por cada seed y devuelve los prompt_ids.
Args:
workflow: dict en API format (resultado de un builder). No se muta: cada
variante es una copia profunda con la semilla parcheada.
seeds: lista de semillas (int). Cada una produce una variante encolada. Si
es None o vacia, se encola el workflow tal cual una sola vez (sin
parchear semilla). keyword-only.
server: host:port del servidor ComfyUI sin esquema. keyword-only.
Returns:
dict con:
- ok (bool): True si TODAS las variantes se encolaron sin error.
- prompt_ids (list[str]): prompt_id de cada variante encolada, en orden.
- count (int): numero de variantes encoladas con exito.
- error (str): primer error encontrado; cadena vacia si todo OK. Si una
variante falla, se detiene el barrido y se devuelven los prompt_ids ya
encolados.
"""
out = {"ok": False, "prompt_ids": [], "count": 0, "error": ""}
variants = [(s, _patch_seed(workflow, s)) for s in seeds] if seeds else [(None, workflow)]
for seed, wf in variants:
try:
resp = comfyui_submit_workflow(wf, server=server)
except RuntimeError as exc:
label = "tal cual" if seed is None else f"seed={seed}"
out["error"] = f"variante {label} fallo al encolar: {exc}"
return out
out["prompt_ids"].append(resp["prompt_id"])
out["count"] = len(out["prompt_ids"])
out["ok"] = True
return out
if __name__ == "__main__":
from comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
wf = comfyui_build_txt2img_workflow(
ckpt_name="v1-5-pruned-emaonly-fp16.safetensors",
positive="a red apple on a wooden table, sharp focus",
negative="blurry, low quality",
)
res = comfyui_batch_generate(wf, seeds=[1, 2])
print(f"ok={res['ok']} count={res['count']} ids={res['prompt_ids']} error={res['error']!r}")
@@ -0,0 +1,86 @@
---
name: comfyui_build_textured_3d_multiview_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_textured_3d_multiview_workflow(image_name: str, *, ckpt: str = \"hunyuan3d-dit-v2-mv.safetensors\", views: int = 6, octree: int = 384, max_faces: int = 50000, upscale_model: str = \"4x_foolhardy_Remacri.pth\") -> dict"
description: "Construye el dict (API format) del pipeline imagen->malla 3D texturizada PBR multi-vista de ComfyUI via el wrapper Hunyuan3DWrapper (kijai). Cadena: LoadImage -> Hy3DModelLoader -> Hy3DGenerateMesh -> Hy3DVAEDecode(octree) -> Hy3DPostprocessMesh(max_faces) -> Hy3DMeshUVWrap -> Hy3DCameraConfig(4 o 6 vistas) + Hy3DRenderMultiView + Hy3DDelightImage -> Hy3DSampleMultiView -> [UpscaleModelLoader+ImageUpscaleWithModel(Remacri)+ImageResize+] -> Hy3DBakeFromMultiview -> Hy3DMeshVerticeInpaintTexture -> Hy3DApplyTexture -> Hy3DExportMesh(glb). Portado del report 0082 (cobertura de atlas 32.93% con 6 vistas + Remacri + octree 384). Pura, sin red ni I/O."
tags: [comfyui, ml, img-to-3d, texture, multiview, hunyuan3d, workflow]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: image_name
desc: "Nombre del archivo de imagen de referencia tal como lo ve el servidor ComfyUI en su carpeta input/ (subido con POST /upload/image)."
- name: ckpt
desc: "Checkpoint del modelo de forma Hunyuan3D para Hy3DModelLoader. Por defecto el variante multi-vista hunyuan3d-dit-v2-mv. keyword-only."
- name: views
desc: "Numero de vistas de camara: 4 (front/left/back/right) o 6 (anade top/bottom, rellena concavidades). Otro valor lanza ValueError. keyword-only."
- name: octree
desc: "octree_resolution del Hy3DVAEDecode (mas alto = malla mas fina, mas VRAM; 384 en el report 0082). keyword-only."
- name: max_faces
desc: "max_facenum del Hy3DPostprocessMesh (decimacion; 50000 en el report 0082). keyword-only."
- name: upscale_model
desc: "Modelo de upscale ESRGAN en upscale_models/ para mejorar las vistas antes del bake (factor dominante de cobertura). Cadena vacia desactiva el upscale. keyword-only."
output: "dict en API format listo para comfyui_submit_workflow. node_ids '1'..'19'; los nodos de upscale ('13'..'15') solo presentes si upscale_model esta activo. El SaveGLB-equivalente Hy3DExportMesh produce un .glb texturizado en output/3D/."
tested: true
tests: ["estructura completa shape+paint+upscale (18 class_types)", "params imagen/ckpt/octree/max_faces reflejados", "6 vistas configuran 6 azimuths/elevations", "4 vistas configuran 4 azimuths", "sin upscale omite nodos Remacri y el bake toma del sample", "views invalido lanza ValueError"]
test_file_path: "python/functions/ml/tests/test_comfyui_build_textured_3d_multiview_workflow.py"
file_path: "python/functions/ml/comfyui_build_textured_3d_multiview_workflow.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_build_textured_3d_multiview_workflow import (
comfyui_build_textured_3d_multiview_workflow,
)
wf = comfyui_build_textured_3d_multiview_workflow(
"tex_src_character.png", views=6, octree=384, max_faces=50000,
upscale_model="4x_foolhardy_Remacri.pth",
)
# wf["9"]["class_type"] == "Hy3DCameraConfig" (6 vistas)
# wf["19"]["class_type"] == "Hy3DExportMesh" (.glb texturizado)
# OJO: en 8GB ejecutar en 2 fases (ver Gotchas), no de una pasada
```
O lanzable directo con: `./fn run comfyui_build_textured_3d_multiview_workflow` (imprime el JSON del workflow de ejemplo).
## Cuando usarla
Cuando quieras una malla 3D **con textura** desde una sola imagen, con mejor
cobertura de atlas que el image-to-3D nativo (que da geometria sin pintar). Es el
builder del pipeline de texturizado multi-vista del report 0082: 6 vistas de
camara + delight + sample multi-vista + upscale Remacri de las vistas + bake sobre
el UV. Para geometria sin textura usa `comfyui_build_image_to_3d_workflow`
(nodos nativos, mas ligero).
## Gotchas
- **Ejecutar en 2 fases en 8GB**: el grafo es monolitico (shape + paint en un
dict) por claridad, pero el grafo entero da OOM en 8GB (confirmado reports
0075/0081/0082). El camino valido es: ejecutar la fase shape (nodos 1-5 ->
Hy3DExportMesh del shape), liberar VRAM con `POST /free`, y luego la fase paint
arrancando desde `Hy3DLoadMesh` del .glb del shape. La separacion + el /free los
orquesta el pipeline impuro que consuma este builder; este dict es la referencia
de cableado completo.
- Requiere el custom node **ComfyUI-Hunyuan3DWrapper** (kijai) + `custom_rasterizer`
CUDA compilado, **ComfyUI_essentials** (para `ImageResize+`) y el modelo
`4x_foolhardy_Remacri.pth` en `upscale_models/`. Si falta algo, ComfyUI rechaza
el workflow con HTTP 400 (esta funcion es pura y no valida contra el servidor).
- `ckpt` por defecto es el variante multi-vista (`-mv`). El report 0082 uso
`hy3dgen/hunyuan3d-dit-v2-0-fp16.safetensors`; ajusta `ckpt` al nombre real que
el servidor enumere en Hy3DModelLoader.
- `upscale_model=""` desactiva el upscale: el bake toma las vistas directas del
Hy3DSampleMultiView. Pierde la mejora dominante de cobertura (el report midio
20.81% -> 32.93% al cablear Remacri en serie).
- Render bonito del GLB no disponible headless; verificar con `Load3D`/`Preview3D`
en la UI de ComfyUI o el visor de `apps/img_to_3d_webapp`.
@@ -0,0 +1,241 @@
"""Construye un workflow ComfyUI imagen->malla 3D texturizada multi-vista (API format).
Usa el wrapper ComfyUI-Hunyuan3DWrapper (kijai): genera la geometria con
Hy3DGenerateMesh/Hy3DVAEDecode, la limpia y le hace UV unwrap, renderiza N vistas
de camara, sintetiza la textura multi-vista (Hy3DSampleMultiView) opcionalmente
mejorada con un upscaler ESRGAN (Remacri), la hornea sobre el atlas UV
(Hy3DBakeFromMultiview), rellena los huecos por vertices y exporta el GLB con
material PBR. Portado del pipeline validado en el report 0082 (cobertura de atlas
32.93 % con 6 vistas + Remacri + octree 384).
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
IMPORTANTE: el grafo es monolitico (shape + paint en un solo dict) por claridad,
pero en 8 GB de VRAM debe ejecutarse en 2 fases (shape -> /free -> paint), no de
una pasada. La separacion en fases y el /free los orquesta el pipeline impuro que
consuma este builder. Ver la seccion Gotchas del .md.
"""
# Vistas de camara soportadas: tabla (azimuths, elevations, weights) por numero de vistas.
# 4 = front/left/back/right; 6 anade top/bottom (rellena concavidades que 4 camaras no ven).
_CAMERA_PRESETS = {
4: {
"camera_azimuths": "0, 90, 180, 270",
"camera_elevations": "0, 0, 0, 0",
"view_weights": "1, 0.1, 0.5, 0.1",
},
6: {
"camera_azimuths": "0, 90, 180, 270, 0, 180",
"camera_elevations": "0, 0, 0, 0, 90, -90",
"view_weights": "1, 0.1, 0.5, 0.1, 0.05, 0.05",
},
}
def comfyui_build_textured_3d_multiview_workflow(
image_name: str,
*,
ckpt: str = "hunyuan3d-dit-v2-mv.safetensors",
views: int = 6,
octree: int = 384,
max_faces: int = 50000,
upscale_model: str = "4x_foolhardy_Remacri.pth",
) -> dict:
"""Construye el dict del workflow imagen->3D texturizado multi-vista.
Args:
image_name: nombre del archivo de imagen de referencia tal como lo ve el
servidor ComfyUI en su carpeta input/ (subido con POST /upload/image).
ckpt: checkpoint del modelo de forma Hunyuan3D para Hy3DModelLoader (por
defecto el variante multi-vista hunyuan3d-dit-v2-mv). keyword-only.
views: numero de vistas de camara: 4 (front/left/back/right) o 6 (anade
top/bottom). Cualquier otro valor lanza ValueError. keyword-only.
octree: octree_resolution del Hy3DVAEDecode (mas alto = malla mas fina,
mas VRAM). keyword-only.
max_faces: max_facenum del Hy3DPostprocessMesh (decimacion de la malla).
keyword-only.
upscale_model: nombre del modelo de upscale ESRGAN en upscale_models/ para
mejorar las vistas antes del bake. Cadena vacia o None desactiva el
upscale (el bake toma las vistas directas del sample). keyword-only.
Returns:
dict en API format listo para comfyui_submit_workflow. node_ids "1".."19"
(los de upscale "13".."15" solo presentes si upscale_model esta activo).
Raises:
ValueError: si views no es 4 ni 6.
"""
if views not in _CAMERA_PRESETS:
raise ValueError(
f"comfyui_build_textured_3d_multiview_workflow: views debe ser 4 o 6, "
f"no {views!r}"
)
cam = _CAMERA_PRESETS[views]
wf = {
# --- Fase shape: imagen -> malla limpia con UV ---
"1": {
"class_type": "LoadImage",
"inputs": {"image": image_name},
},
"2": {
"class_type": "Hy3DModelLoader",
"inputs": {"model": ckpt, "attention_mode": "sdpa", "cublas_ops": False},
},
"3": {
"class_type": "Hy3DGenerateMesh",
"inputs": {
"pipeline": ["2", 0],
"image": ["1", 0],
"guidance_scale": 5.5,
"steps": 30,
"seed": 42,
"force_offload": True,
},
},
"4": {
"class_type": "Hy3DVAEDecode",
"inputs": {
"vae": ["2", 1],
"latents": ["3", 0],
"box_v": 1.01,
"octree_resolution": octree,
"num_chunks": 8000,
"mc_level": 0,
"mc_algo": "mc",
"enable_flash_vdm": True,
"force_offload": True,
},
},
"5": {
"class_type": "Hy3DPostprocessMesh",
"inputs": {
"trimesh": ["4", 0],
"remove_floaters": True,
"remove_degenerate_faces": True,
"reduce_faces": True,
"max_facenum": max_faces,
"smooth_normals": False,
},
},
"6": {
"class_type": "Hy3DMeshUVWrap",
"inputs": {"trimesh": ["5", 0]},
},
# --- Fase paint: render multi-vista + delight + sample + bake + textura ---
"7": {
"class_type": "DownloadAndLoadHy3DPaintModel",
"inputs": {"model": "hunyuan3d-paint-v2-0"},
},
"8": {
"class_type": "DownloadAndLoadHy3DDelightModel",
"inputs": {"model": "hunyuan3d-delight-v2-0"},
},
"9": {
"class_type": "Hy3DCameraConfig",
"inputs": {
"camera_azimuths": cam["camera_azimuths"],
"camera_elevations": cam["camera_elevations"],
"view_weights": cam["view_weights"],
"camera_distance": 1.45,
"ortho_scale": 1.2,
},
},
"10": {
"class_type": "Hy3DRenderMultiView",
"inputs": {
"trimesh": ["6", 0],
"render_size": 1024,
"texture_size": 1024,
"camera_config": ["9", 0],
"normal_space": "world",
},
},
"11": {
"class_type": "Hy3DDelightImage",
"inputs": {
"delight_pipe": ["8", 0],
"image": ["1", 0],
"steps": 50,
"width": 512,
"height": 512,
"cfg_image": 1.0,
"seed": 42,
},
},
"12": {
"class_type": "Hy3DSampleMultiView",
"inputs": {
"pipeline": ["7", 0],
"ref_image": ["11", 0],
"normal_maps": ["10", 0],
"position_maps": ["10", 1],
"view_size": 512,
"steps": 25,
"seed": 0,
"camera_config": ["9", 0],
},
},
}
# Upscale opcional de los multiviews antes del bake (factor dominante de cobertura).
if upscale_model:
wf["13"] = {
"class_type": "UpscaleModelLoader",
"inputs": {"model_name": upscale_model},
}
wf["14"] = {
"class_type": "ImageUpscaleWithModel",
"inputs": {"upscale_model": ["13", 0], "image": ["12", 0]},
}
wf["15"] = {
"class_type": "ImageResize+",
"inputs": {
"image": ["14", 0],
"width": 1024,
"height": 1024,
"interpolation": "lanczos",
"method": "stretch",
"condition": "always",
"multiple_of": 0,
},
}
bake_images = ["15", 0]
else:
bake_images = ["12", 0]
wf["16"] = {
"class_type": "Hy3DBakeFromMultiview",
"inputs": {
"images": bake_images,
"renderer": ["10", 2],
"camera_config": ["9", 0],
},
}
wf["17"] = {
"class_type": "Hy3DMeshVerticeInpaintTexture",
"inputs": {"texture": ["16", 0], "mask": ["16", 1], "renderer": ["16", 2]},
}
wf["18"] = {
"class_type": "Hy3DApplyTexture",
"inputs": {"texture": ["17", 0], "renderer": ["17", 2]},
}
wf["19"] = {
"class_type": "Hy3DExportMesh",
"inputs": {
"trimesh": ["18", 0],
"filename_prefix": "3D/textured_multiview",
"file_format": "glb",
"save_file": True,
},
}
return wf
if __name__ == "__main__":
import json
wf = comfyui_build_textured_3d_multiview_workflow(
"tex_src_character.png", views=6, octree=384, max_faces=50000
)
print(json.dumps(wf, indent=2))
@@ -0,0 +1,90 @@
---
name: comfyui_build_video_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_video_workflow(prompt: str, *, model: str = \"ltx\", negative: str = \"\", width: int = 512, height: int = 320, num_frames: int = 65, steps: int = 20, seed: int = 0, fps: int = 24) -> dict"
description: "Construye el dict de un workflow ComfyUI txt2video en API format para LTX-Video 2B v0.9.5 (model='ltx') o Wan2.1 T2V 1.3B (model='wan'), con los nombres de modelo reales. LTX: CLIPLoader(ltxv)+CheckpointLoaderSimple -> CLIPTextEncode x2 -> LTXVConditioning+EmptyLTXVLatentVideo+LTXVScheduler+KSamplerSelect -> SamplerCustom -> VAEDecode -> CreateVideo -> SaveVideo. Wan: UNETLoader+CLIPLoader(wan)+VAELoader+ModelSamplingSD3 -> CLIPTextEncode x2+EmptyHunyuanLatentVideo -> KSampler(uni_pc/simple) -> VAEDecode -> CreateVideo -> SaveVideo. Defaults conservadores para 8GB. Pura, sin red ni I/O. Hermana de comfyui_build_txt2img_workflow."
tags: [comfyui, ml, video-generation, txt2video, ltx-video, wan, workflow]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: prompt
desc: "Prompt positivo: lo que se quiere ver en el clip de video."
- name: model
desc: "'ltx' (LTX-Video 2B v0.9.5, todo-en-uno) o 'wan' (Wan2.1 T2V 1.3B, diffusion+vae aparte). Cualquier otro valor lanza ValueError. keyword-only."
- name: negative
desc: "Prompt negativo: lo que se quiere evitar. Por defecto cadena vacia. keyword-only."
- name: width
desc: "Ancho del video en px (multiplo de 32 recomendado). keyword-only."
- name: height
desc: "Alto del video en px (multiplo de 32 recomendado). keyword-only."
- name: num_frames
desc: "Numero de frames del clip (longitud temporal del latente de video). keyword-only."
- name: steps
desc: "Pasos de sampling: LTXVScheduler para ltx, KSampler para wan. keyword-only."
- name: seed
desc: "Semilla del sampler. 0 es determinista; cambiar para variar el clip. keyword-only."
- name: fps
desc: "Frames por segundo del video (CreateVideo). En LTX se usa tambien como frame_rate del LTXVConditioning. keyword-only."
output: "dict en API format listo para comfyui_submit_workflow. node_ids string; cada valor con class_type + inputs. LTX devuelve 12 nodos; Wan 11. La cfg/sampler/scheduler se fijan internamente segun el modelo (LTX: cfg 3.0, euler; Wan: cfg 6.0, uni_pc/simple, shift 8.0)."
tested: true
tests: ["LTX: nodos LTXV* presentes + t5xxl fp8 + ckpt real", "Wan: UNETLoader/VAELoader/ModelSamplingSD3 + umt5 + wan_2.1_vae", "params reflejados (width/height/num_frames/steps/seed/fps)", "model invalido lanza ValueError"]
test_file_path: "python/functions/ml/tests/test_comfyui_build_video_workflow.py"
file_path: "python/functions/ml/comfyui_build_video_workflow.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_build_video_workflow import comfyui_build_video_workflow
wf = comfyui_build_video_workflow(
"A red fox runs through a sunlit autumn forest, cinematic, shallow depth of field",
model="ltx",
negative="low quality, worst quality, deformed, motion smear",
width=512, height=320, num_frames=65, steps=25, seed=42, fps=24,
)
# wf["72"]["class_type"] == "SamplerCustom" (camino LTX)
# wf["79"]["class_type"] == "SaveVideo"
# -> comfyui_submit_workflow(wf) para encolar el clip
```
O lanzable directo con: `./fn run comfyui_build_video_workflow` (imprime el JSON del workflow LTX de ejemplo).
## Cuando usarla
Antes de enviar una generacion de video txt2video a ComfyUI: construye aqui el
dict del workflow y pasalo a `comfyui_submit_workflow`. Usa `model="ltx"` por
defecto (cupo en 8GB confirmado, scheduler y VAE temporales propios); `model="wan"`
si quieres el camino Wan2.1 1.3B (umt5 + vae aparte). Hermana de
`comfyui_build_txt2img_workflow` para imagen estatica.
## Gotchas
- Es API format (nodos numerados), NO el formato de la UI de ComfyUI. Es lo que
acepta POST /prompt.
- Los nombres de modelo estan fijados a los reales del equipo
(`ltx-video-2b-v0.9.5.safetensors` + `t5xxl_fp8_e4m3fn_scaled.safetensors`;
`wan2.1_t2v_1.3B_fp16.safetensors` + `umt5_xxl_fp8_e4m3fn_scaled.safetensors` +
`wan_2.1_vae.safetensors`). Deben existir y ser visibles para el servidor o
ComfyUI rechaza el workflow con HTTP 400 al enviarlo (esta funcion es pura y no
valida contra el servidor).
- Cupo 8GB: con los defaults (512x320, 65 frames) LTX pico ~7.7 GB en el report
0084 sin OOM. Subir resolucion o num_frames acerca el techo. Si da OOM, bajar a
512x288 / 49 frames.
- El camino LTX esta validado de extremo a extremo (report 0084: clip real de 65
frames). El camino Wan modela la plantilla nativa canonica de ComfyUI pero NO se
ejecuto en esa sesion; verificar nombres de modelo antes de tirar de el.
- LTX usa cfg baja (3.0). Subirla degrada el video. Por eso la cfg no es parametro:
se fija segun el modelo.
- `SaveVideo` necesita `format`/`codec` (aqui "auto"/"auto"); sin ellos ComfyUI
responde HTTP 400 (gotcha del importador, report 0084). Este builder ya los pone.
@@ -0,0 +1,232 @@
"""Construye un workflow ComfyUI txt2video en "API format" (dict de nodos numerados).
Soporta dos modelos de difusion de video nativos de ComfyUI 0.26, ambos pensados
para caber en 8 GB de VRAM con parametros conservadores:
- model="ltx": LTX-Video 2B v0.9.5. Checkpoint todo-en-uno (UNet + VAE temporal) +
text encoder t5xxl en fp8. Cadena CLIPLoader(ltxv) + CheckpointLoaderSimple ->
CLIPTextEncode x2 -> LTXVConditioning + EmptyLTXVLatentVideo + LTXVScheduler +
KSamplerSelect -> SamplerCustom -> VAEDecode -> CreateVideo -> SaveVideo.
Validado de extremo a extremo en el report 0084 (clip real de 65 frames).
- model="wan": Wan2.1 T2V 1.3B. Diffusion model (UNETLoader) + text encoder umt5
fp8 (CLIPLoader type=wan) + wan_2.1_vae aparte (VAELoader) + ModelSamplingSD3 ->
CLIPTextEncode x2 + EmptyHunyuanLatentVideo -> KSampler(uni_pc/simple) ->
VAEDecode -> CreateVideo -> SaveVideo. Plantilla nativa canonica de ComfyUI.
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
"""
# Nombres reales de los modelos tal como los ve el servidor ComfyUI.
_LTX_CKPT = "ltx-video-2b-v0.9.5.safetensors"
_LTX_CLIP = "t5xxl_fp8_e4m3fn_scaled.safetensors"
_WAN_UNET = "wan2.1_t2v_1.3B_fp16.safetensors"
_WAN_CLIP = "umt5_xxl_fp8_e4m3fn_scaled.safetensors"
_WAN_VAE = "wan_2.1_vae.safetensors"
def comfyui_build_video_workflow(
prompt: str,
*,
model: str = "ltx",
negative: str = "",
width: int = 512,
height: int = 320,
num_frames: int = 65,
steps: int = 20,
seed: int = 0,
fps: int = 24,
) -> dict:
"""Construye el dict del workflow txt2video para LTX-Video 2B o Wan2.1 1.3B.
Args:
prompt: prompt positivo (lo que se quiere ver en el clip).
model: "ltx" (LTX-Video 2B v0.9.5) o "wan" (Wan2.1 T2V 1.3B). keyword-only.
negative: prompt negativo. keyword-only.
width: ancho del video en px (multiplo de 32 recomendado). keyword-only.
height: alto del video en px (multiplo de 32 recomendado). keyword-only.
num_frames: numero de frames del clip (longitud temporal del latente).
keyword-only.
steps: pasos de sampling (LTXVScheduler para ltx, KSampler para wan).
keyword-only.
seed: semilla del sampler (0 = determinista; cambiar para variar).
keyword-only.
fps: frames por segundo del video resultante (CreateVideo). En LTX se usa
ademas como frame_rate del condicionamiento LTXVConditioning.
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. La cfg, el
sampler y el scheduler se fijan internamente segun el modelo (LTX: cfg 3.0,
euler; Wan: cfg 6.0, uni_pc/simple, shift 8.0).
Raises:
ValueError: si model no es "ltx" ni "wan".
"""
m = model.lower()
if m == "ltx":
return {
"38": {
"class_type": "CLIPLoader",
"inputs": {"clip_name": _LTX_CLIP, "type": "ltxv", "device": "default"},
},
"44": {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": _LTX_CKPT},
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {"text": prompt, "clip": ["38", 0]},
},
"7": {
"class_type": "CLIPTextEncode",
"inputs": {"text": negative, "clip": ["38", 0]},
},
"70": {
"class_type": "EmptyLTXVLatentVideo",
"inputs": {
"width": width,
"height": height,
"length": num_frames,
"batch_size": 1,
},
},
"71": {
"class_type": "LTXVScheduler",
"inputs": {
"steps": steps,
"max_shift": 2.05,
"base_shift": 0.95,
"stretch": True,
"terminal": 0.1,
"latent": ["70", 0],
},
},
"73": {
"class_type": "KSamplerSelect",
"inputs": {"sampler_name": "euler"},
},
"69": {
"class_type": "LTXVConditioning",
"inputs": {
"positive": ["6", 0],
"negative": ["7", 0],
"frame_rate": fps,
},
},
"72": {
"class_type": "SamplerCustom",
"inputs": {
"model": ["44", 0],
"positive": ["69", 0],
"negative": ["69", 1],
"sampler": ["73", 0],
"sigmas": ["71", 0],
"latent_image": ["70", 0],
"add_noise": True,
"noise_seed": seed,
"cfg": 3.0,
},
},
"8": {
"class_type": "VAEDecode",
"inputs": {"samples": ["72", 0], "vae": ["44", 2]},
},
"78": {
"class_type": "CreateVideo",
"inputs": {"images": ["8", 0], "fps": fps},
},
"79": {
"class_type": "SaveVideo",
"inputs": {
"video": ["78", 0],
"filename_prefix": "video",
"format": "auto",
"codec": "auto",
},
},
}
if m == "wan":
return {
"37": {
"class_type": "UNETLoader",
"inputs": {"unet_name": _WAN_UNET, "weight_dtype": "default"},
},
"38": {
"class_type": "CLIPLoader",
"inputs": {"clip_name": _WAN_CLIP, "type": "wan", "device": "default"},
},
"39": {
"class_type": "VAELoader",
"inputs": {"vae_name": _WAN_VAE},
},
"48": {
"class_type": "ModelSamplingSD3",
"inputs": {"shift": 8.0, "model": ["37", 0]},
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {"text": prompt, "clip": ["38", 0]},
},
"7": {
"class_type": "CLIPTextEncode",
"inputs": {"text": negative, "clip": ["38", 0]},
},
"40": {
"class_type": "EmptyHunyuanLatentVideo",
"inputs": {
"width": width,
"height": height,
"length": num_frames,
"batch_size": 1,
},
},
"3": {
"class_type": "KSampler",
"inputs": {
"seed": seed,
"steps": steps,
"cfg": 6.0,
"sampler_name": "uni_pc",
"scheduler": "simple",
"denoise": 1.0,
"model": ["48", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["40", 0],
},
},
"8": {
"class_type": "VAEDecode",
"inputs": {"samples": ["3", 0], "vae": ["39", 0]},
},
"78": {
"class_type": "CreateVideo",
"inputs": {"images": ["8", 0], "fps": fps},
},
"79": {
"class_type": "SaveVideo",
"inputs": {
"video": ["78", 0],
"filename_prefix": "video",
"format": "auto",
"codec": "auto",
},
},
}
raise ValueError(
f"comfyui_build_video_workflow: model debe ser 'ltx' o 'wan', no {model!r}"
)
if __name__ == "__main__":
import json
wf = comfyui_build_video_workflow(
"A red fox runs through a sunlit autumn forest, cinematic, shallow depth of field",
model="ltx",
negative="low quality, worst quality, deformed, motion smear",
seed=42,
)
print(json.dumps(wf, indent=2))
@@ -0,0 +1,60 @@
---
name: comfyui_interrupt_queue
kind: function
lang: py
domain: ml
version: "1.0.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)."
tags: [comfyui, ml, queue, interrupt, control, http]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- 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: ""
file_path: "python/functions/ml/comfyui_interrupt_queue.py"
---
## Ejemplo
```python
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
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']}")
```
O lanzable directo con: `./fn run comfyui_interrupt_queue`.
## 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.
## 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).
- 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).
- En fallo de red NO lanza: devuelve `ok=False` con el mensaje en `error`. Comprueba
`ok` antes de fiarte de los conteos.
- Tras el interrupt conviene liberar VRAM con `POST /free` si vas a encolar otro
trabajo pesado (esta funcion no lo hace).
@@ -0,0 +1,71 @@
"""Interrumpe la generacion en curso de ComfyUI y devuelve el estado de la cola.
Funcion impura: hace red (HTTP POST /interrupt + GET /queue). Solo stdlib.
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}.
"""
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.
Args:
server: host:port del servidor ComfyUI sin esquema (default
"127.0.0.1:8188").
Returns:
dict con:
- ok (bool): True si tanto el interrupt como la lectura de la cola
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.
- error (str): mensaje de error si algo fallo; cadena vacia si todo OK.
"""
out = {
"ok": False,
"interrupted": False,
"queue_running": 0,
"queue_pending": 0,
"error": "",
}
base = f"http://{server}"
# 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):
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.
try:
with urllib.request.urlopen(f"{base}/queue", timeout=10.0) as resp:
data = json.loads(resp.read())
out["queue_running"] = len(data.get("queue_running", []))
out["queue_pending"] = len(data.get("queue_pending", []))
out["ok"] = True
except urllib.error.URLError as exc:
reason = getattr(exc, "reason", exc)
out["error"] = f"queue fallo: no se pudo conectar a {base}/queue: {reason}"
except json.JSONDecodeError as exc:
out["error"] = f"queue fallo: respuesta no es JSON valido: {exc}"
return out
if __name__ == "__main__":
res = comfyui_interrupt_queue()
print(
f"ok={res['ok']} interrupted={res['interrupted']} "
f"running={res['queue_running']} pending={res['queue_pending']} "
f"error={res['error']!r}"
)
@@ -0,0 +1,80 @@
"""Tests de estructura para comfyui_build_textured_3d_multiview_workflow (pura)."""
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__), "..", ".."))
from ml.comfyui_build_textured_3d_multiview_workflow import (
comfyui_build_textured_3d_multiview_workflow,
)
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
def test_estructura_shape_paint_y_upscale():
wf = comfyui_build_textured_3d_multiview_workflow("ref.png", views=6)
assert_api_format(wf)
cts = class_types(wf)
# Fase shape (geometria) + fase paint (textura multi-vista) + upscale Remacri.
for ct in (
"LoadImage",
"Hy3DModelLoader",
"Hy3DGenerateMesh",
"Hy3DVAEDecode",
"Hy3DPostprocessMesh",
"Hy3DMeshUVWrap",
"DownloadAndLoadHy3DPaintModel",
"DownloadAndLoadHy3DDelightModel",
"Hy3DCameraConfig",
"Hy3DRenderMultiView",
"Hy3DDelightImage",
"Hy3DSampleMultiView",
"UpscaleModelLoader",
"ImageUpscaleWithModel",
"ImageResize+",
"Hy3DBakeFromMultiview",
"Hy3DApplyTexture",
"Hy3DExportMesh",
):
assert ct in cts, f"falta {ct}"
def test_params_imagen_ckpt_octree_faces():
wf = comfyui_build_textured_3d_multiview_workflow(
"robot.png", ckpt="custom-mv.safetensors", octree=256, max_faces=40000
)
assert node_by_ct(wf, "LoadImage")["inputs"]["image"] == "robot.png"
assert node_by_ct(wf, "Hy3DModelLoader")["inputs"]["model"] == "custom-mv.safetensors"
assert node_by_ct(wf, "Hy3DVAEDecode")["inputs"]["octree_resolution"] == 256
assert node_by_ct(wf, "Hy3DPostprocessMesh")["inputs"]["max_facenum"] == 40000
def test_6_vistas_configura_camara():
wf = comfyui_build_textured_3d_multiview_workflow("ref.png", views=6)
cam = node_by_ct(wf, "Hy3DCameraConfig")["inputs"]
# 6 azimuths/elevations (front/left/back/right + top/bottom).
assert len(cam["camera_azimuths"].split(",")) == 6
assert len(cam["camera_elevations"].split(",")) == 6
def test_4_vistas_configura_camara():
wf = comfyui_build_textured_3d_multiview_workflow("ref.png", views=4)
cam = node_by_ct(wf, "Hy3DCameraConfig")["inputs"]
assert len(cam["camera_azimuths"].split(",")) == 4
def test_sin_upscale_omite_nodos_remacri():
wf = comfyui_build_textured_3d_multiview_workflow("ref.png", upscale_model="")
cts = class_types(wf)
assert "UpscaleModelLoader" not in cts
assert "ImageUpscaleWithModel" not in cts
# El bake toma las vistas directas del sample multi-vista (nodo 12).
assert node_by_ct(wf, "Hy3DBakeFromMultiview")["inputs"]["images"] == ["12", 0]
def test_views_invalido_lanza_valueerror():
with pytest.raises(ValueError):
comfyui_build_textured_3d_multiview_workflow("ref.png", views=3)
@@ -0,0 +1,91 @@
"""Tests de estructura para comfyui_build_video_workflow (funcion pura)."""
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__), "..", ".."))
from ml.comfyui_build_video_workflow import comfyui_build_video_workflow
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
def test_ltx_estructura_y_nodos():
wf = comfyui_build_video_workflow("a fox running", model="ltx")
assert_api_format(wf)
cts = class_types(wf)
# Nodos clave del camino LTX presentes.
for ct in (
"CLIPLoader",
"CheckpointLoaderSimple",
"EmptyLTXVLatentVideo",
"LTXVScheduler",
"KSamplerSelect",
"LTXVConditioning",
"SamplerCustom",
"CreateVideo",
"SaveVideo",
):
assert ct in cts, f"falta {ct} en LTX"
# El CLIPLoader de LTX usa el text encoder t5xxl fp8 con type=ltxv.
clip = node_by_ct(wf, "CLIPLoader")["inputs"]
assert clip["type"] == "ltxv"
assert clip["clip_name"] == "t5xxl_fp8_e4m3fn_scaled.safetensors"
assert node_by_ct(wf, "CheckpointLoaderSimple")["inputs"]["ckpt_name"] == (
"ltx-video-2b-v0.9.5.safetensors"
)
def test_wan_estructura_y_nodos():
wf = comfyui_build_video_workflow("a fox running", model="wan")
assert_api_format(wf)
cts = class_types(wf)
# Wan usa UNETLoader + VAELoader aparte + ModelSamplingSD3 + KSampler nativo.
for ct in (
"UNETLoader",
"CLIPLoader",
"VAELoader",
"ModelSamplingSD3",
"EmptyHunyuanLatentVideo",
"KSampler",
"CreateVideo",
"SaveVideo",
):
assert ct in cts, f"falta {ct} en Wan"
assert node_by_ct(wf, "UNETLoader")["inputs"]["unet_name"] == (
"wan2.1_t2v_1.3B_fp16.safetensors"
)
assert node_by_ct(wf, "CLIPLoader")["inputs"]["type"] == "wan"
assert node_by_ct(wf, "VAELoader")["inputs"]["vae_name"] == "wan_2.1_vae.safetensors"
def test_params_se_reflejan_ltx():
wf = comfyui_build_video_workflow(
"POS", model="ltx", negative="NEG", width=640, height=384,
num_frames=49, steps=18, seed=7, fps=30,
)
lat = node_by_ct(wf, "EmptyLTXVLatentVideo")["inputs"]
assert lat["width"] == 640 and lat["height"] == 384 and lat["length"] == 49
assert node_by_ct(wf, "LTXVScheduler")["inputs"]["steps"] == 18
assert node_by_ct(wf, "SamplerCustom")["inputs"]["noise_seed"] == 7
assert node_by_ct(wf, "CreateVideo")["inputs"]["fps"] == 30
textos = sorted(
n["inputs"]["text"] for n in wf.values() if n["class_type"] == "CLIPTextEncode"
)
assert textos == ["NEG", "POS"]
def test_params_se_reflejan_wan():
wf = comfyui_build_video_workflow(
"POS", model="wan", num_frames=33, steps=15, seed=99,
)
assert node_by_ct(wf, "EmptyHunyuanLatentVideo")["inputs"]["length"] == 33
ks = node_by_ct(wf, "KSampler")["inputs"]
assert ks["steps"] == 15 and ks["seed"] == 99
def test_model_invalido_lanza_valueerror():
with pytest.raises(ValueError):
comfyui_build_video_workflow("x", model="sora")