feat(ml): auto-commit con 11 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user