chore: auto-commit (14 archivos)

- docs/capabilities/comfyui.md
- python/functions/ml/comfyui_build_image_to_3d_workflow.md
- python/functions/ml/comfyui_build_image_to_3d_workflow.py
- python/functions/ml/tests/test_comfyui_build_image_to_3d_workflow.py
- python/functions/ml/comfyui_build_facedetailer_workflow.md
- python/functions/ml/comfyui_build_facedetailer_workflow.py
- python/functions/ml/comfyui_build_hires_fix_workflow.md
- python/functions/ml/comfyui_build_hires_fix_workflow.py
- python/functions/ml/tests/test_comfyui_build_facedetailer_workflow.py
- python/functions/ml/tests/test_comfyui_build_hires_fix_workflow.py
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 02:34:10 +02:00
parent 3823a28d1c
commit f686b338d6
14 changed files with 1265 additions and 28 deletions
@@ -0,0 +1,115 @@
---
name: comfyui_build_facedetailer_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_facedetailer_workflow(base_workflow_or_image, ckpt_name: str, positive: str, negative: str = \"\", *, bbox_model: str = \"face_yolov8m.pt\", denoise: float = 0.5, steps: int = 20, cfg: float = 8.0, seed: int = 0, guide_size: float = 512.0, bbox_threshold: float = 0.5, feather: int = 5, sampler_name: str = \"euler\", scheduler: str = \"normal\", filename_prefix: str = \"facedetail\") -> dict"
description: "Construye un workflow ComfyUI con FaceDetailer (Impact-Pack) en API format: detecta caras con UltralyticsDetectorProvider (YOLO bbox) y las regenera con un sampler de difusion para recuperar detalle (el pain #1 de retratos). Acepta el nombre de una imagen ya en input/ (modo str) o un workflow base como dict (modo workflow, p.ej. el de comfyui_build_txt2img_workflow): en este caso toma la imagen del VAEDecode y reutiliza el CheckpointLoaderSimple. Class_types reales verificados en /object_info. Pura, sin red ni I/O."
tags: [comfyui, ml, facedetailer, impact-pack, portrait, workflow]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: base_workflow_or_image
desc: "Nombre (str) de una imagen ya en el input/ del servidor, o un workflow base (dict en API format). Con str monta LoadImage + CheckpointLoaderSimple nuevos; con dict toma la imagen del primer VAEDecode y reutiliza su CheckpointLoaderSimple."
- name: ckpt_name
desc: "Checkpoint para el sampler del detailer (y para el loader nuevo en modo imagen). Debe existir en el servidor (CheckpointLoaderSimple)."
- name: positive
desc: "Prompt positivo para regenerar las caras (ej. 'detailed face, sharp eyes, skin texture'). Se codifica con el CLIP del checkpoint."
- name: negative
desc: "Prompt negativo. Por defecto ''."
- name: bbox_model
desc: "Modelo de deteccion Ultralytics. Acepta nombre corto ('face_yolov8m.pt') o prefijado ('bbox/face_yolov8m.pt'); si no trae prefijo se asume 'bbox/'. keyword-only."
- name: denoise
desc: "Fuerza de re-difusion de cada cara (0.5 por defecto; mas alto = mas cambio, mas riesgo de perder identidad). keyword-only."
- name: steps
desc: "Pasos de sampling del detailer. keyword-only."
- name: cfg
desc: "Classifier-free guidance del detailer. keyword-only."
- name: seed
desc: "Semilla del sampler del detailer. keyword-only."
- name: guide_size
desc: "Tamano (px) al que se reescala cada cara recortada antes de re-difundirla (FaceDetailer.guide_size). keyword-only."
- name: bbox_threshold
desc: "Umbral de confianza del detector de caras (0..1). Mas alto = menos falsos positivos, riesgo de no detectar caras pequenas. keyword-only."
- name: feather
desc: "Pixeles de difuminado del borde de la mascara al recomponer la cara sobre la imagen. keyword-only."
- name: sampler_name
desc: "Sampler del detailer (ej. 'euler'). keyword-only."
- name: scheduler
desc: "Scheduler del detailer (ej. 'normal'). keyword-only."
- name: filename_prefix
desc: "Prefijo del PNG final que escribe SaveImage. keyword-only."
output: "dict en API format listo para comfyui_submit_workflow. En modo dict contiene los nodos del workflow base mas los del detailer (node_ids prefijados 'fd_' para no colisionar); el SaveImage 'fd_save' produce la imagen con las caras regeneradas."
tested: true
tests: ["modo imagen monta UltralyticsDetectorProvider + FaceDetailer + SaveImage", "modo workflow reutiliza VAEDecode y CheckpointLoaderSimple del base y conserva sus nodos", "normaliza bbox_model corto a prefijo bbox/", "dict sin VAEDecode lanza ValueError"]
test_file_path: "python/functions/ml/tests/test_comfyui_build_facedetailer_workflow.py"
file_path: "python/functions/ml/comfyui_build_facedetailer_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_facedetailer_workflow import comfyui_build_facedetailer_workflow
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
# Modo workflow: genera un retrato y le aplica FaceDetailer en el mismo grafo.
base = comfyui_build_txt2img_workflow(
ckpt_name="dreamshaper_8.safetensors",
positive="portrait of a woman, soft light",
width=512, height=768, seed=7,
)
wf = comfyui_build_facedetailer_workflow(
base,
ckpt_name="dreamshaper_8.safetensors",
positive="detailed face, sharp eyes, skin texture",
negative="blurry, deformed",
denoise=0.45,
)
# wf["fd_det"]["class_type"] == "UltralyticsDetectorProvider"
# wf["fd_det"]["inputs"]["model_name"] == "bbox/face_yolov8m.pt"
# wf["fd_face"]["class_type"] == "FaceDetailer"
# wf["fd_face"]["inputs"]["image"] == ["8", 0] # VAEDecode del base
# wf["4"]["class_type"] == "CheckpointLoaderSimple" # nodos del base conservados
```
O lanzable directo con: `./fn run comfyui_build_facedetailer_workflow` (imprime el JSON del workflow de ejemplo en modo imagen).
## Cuando usarla
Cuando una imagen generada tiene caras mediocres (ojos borrosos, piel plana,
rasgos deformados) y quieres regenerarlas con detalle sin rehacer toda la imagen.
Es el ADetailer/FaceDetailer "pro" del flujo de retratos. Encadénala tras
`comfyui_build_txt2img_workflow` (pásale el dict) para detail en una sola cola, o
pásale el nombre de una imagen ya en `input/` para mejorar una imagen existente.
Después: `comfyui_submit_workflow``comfyui_wait_result``comfyui_fetch_output_image`.
## Gotchas
- Es API format (nodos numerados / con prefijo `fd_`), NO el formato de la UI.
- Requiere **ComfyUI-Impact-Pack** instalado (provee `FaceDetailer` y
`UltralyticsDetectorProvider`). Si el server responde HTTP 400 "node type not
found: FaceDetailer", el custom node no está cargado: revísalo en el Manager.
- El modelo de detección debe estar en `models/ultralytics/bbox/` (aquí
`face_yolov8m.pt`). El nodo lo referencia con prefijo de subcarpeta
(`bbox/face_yolov8m.pt`); la función normaliza el nombre corto automáticamente.
- **No usa SAM** (segment-anything): `sam_model_opt` es opcional y aquí no hay
modelo SAM instalado (`SAMLoader` reporta lista vacía). FaceDetailer funciona
solo con el detector de bounding box, que basta para caras. Si instalas un SAM
y quieres máscaras más finas, habría que añadir el `SAMLoader` aparte.
- En **modo workflow** (dict) se reutiliza el primer `CheckpointLoaderSimple` y el
primer `VAEDecode` del base. Si el base usa otro loader (p.ej. un flujo SDXL con
loaders distintos), se monta un `CheckpointLoaderSimple` propio con `ckpt_name`
asegúrate de que el checkpoint case con el espacio latente del base.
- El SaveImage del workflow base (si lo tenía) se conserva: el grafo produce tanto
la imagen base como la "detailed" (`fd_save`). Si solo quieres la final, ignora
la otra salida.
- `denoise` alto (>0.6) puede cambiar la identidad de la cara; 0.40.5 conserva
rasgos y añade detalle.
@@ -0,0 +1,230 @@
"""Construye un workflow ComfyUI con FaceDetailer (Impact-Pack) en API format.
FaceDetailer es el nodo estrella de ComfyUI-Impact-Pack para el "pain #1" de los
retratos: detecta las caras de una imagen (con un detector YOLO via
UltralyticsDetectorProvider) y regenera cada una por separado con un sampler de
difusion, recuperando detalle (ojos, piel, dientes) que el primer render pierde.
Esta funcion monta el sub-grafo del detailer y lo conecta a una fuente de imagen,
que puede ser:
- una imagen ya subida al `input/` del servidor (pasa su nombre como str), o
- un workflow base ya construido (pasa el dict, p.ej. el de
`comfyui_build_txt2img_workflow`): el detailer toma la imagen del `VAEDecode`
del workflow y reutiliza su `CheckpointLoaderSimple` (model/clip/vae).
Cadena del sub-grafo (sobre los class_types REALES de Impact-Pack, verificados en
`/object_info`):
UltralyticsDetectorProvider -> BBOX_DETECTOR
CheckpointLoaderSimple (nuevo o reutilizado) -> MODEL, CLIP, VAE
CLIPTextEncode (positive, negative) -> CONDITIONING
FaceDetailer(image, model, clip, vae, positive, negative, bbox_detector, ...) -> IMAGE
SaveImage
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
"""
from __future__ import annotations
def _normalize_bbox_model(name: str) -> str:
"""Normaliza el nombre del modelo de deteccion al formato del nodo.
UltralyticsDetectorProvider expone los modelos con prefijo de subcarpeta
(`bbox/face_yolov8m.pt`, `segm/person_yolov8m-seg.pt`). Acepta tanto el
nombre corto (`face_yolov8m.pt`) como el ya prefijado; si no trae prefijo
`bbox/` ni `segm/`, asume `bbox/` (caras/manos son detectores de bounding box).
"""
if name.startswith(("bbox/", "segm/")):
return name
return f"bbox/{name}"
def _find_first(workflow: dict, class_type: str) -> str | None:
"""Devuelve el node_id del primer nodo con ese class_type, o None."""
for node_id, node in workflow.items():
if isinstance(node, dict) and node.get("class_type") == class_type:
return node_id
return None
def comfyui_build_facedetailer_workflow(
base_workflow_or_image,
ckpt_name: str,
positive: str,
negative: str = "",
*,
bbox_model: str = "face_yolov8m.pt",
denoise: float = 0.5,
steps: int = 20,
cfg: float = 8.0,
seed: int = 0,
guide_size: float = 512.0,
bbox_threshold: float = 0.5,
feather: int = 5,
sampler_name: str = "euler",
scheduler: str = "normal",
filename_prefix: str = "facedetail",
) -> dict:
"""Construye un workflow ComfyUI que aplica FaceDetailer a una imagen.
Args:
base_workflow_or_image: o bien el nombre (str) de una imagen ya presente
en el `input/` del servidor, o bien un workflow base (dict en API
format, p.ej. el de `comfyui_build_txt2img_workflow`). Con str se monta
un `LoadImage` y un `CheckpointLoaderSimple` nuevos; con dict se toma la
imagen del primer `VAEDecode` y se reutiliza su `CheckpointLoaderSimple`.
ckpt_name: checkpoint para el sampler del detailer (y para el loader nuevo
en el modo imagen). Debe existir en el servidor (CheckpointLoaderSimple).
positive: prompt positivo para regenerar las caras (p.ej. "detailed face,
sharp eyes, skin texture"). Se codifica con el CLIP del checkpoint.
negative: prompt negativo. Por defecto "".
bbox_model: modelo de deteccion de Ultralytics. Acepta nombre corto
("face_yolov8m.pt") o prefijado ("bbox/face_yolov8m.pt"). keyword-only.
denoise: fuerza de re-difusion de cada cara (0.5 por defecto; mas alto =
mas cambio, mas riesgo de perder identidad). keyword-only.
steps: pasos de sampling del detailer. keyword-only.
cfg: classifier-free guidance del detailer. keyword-only.
seed: semilla del sampler del detailer. keyword-only.
guide_size: tamano (px) al que se reescala cada cara recortada antes de
re-difundirla (FaceDetailer.guide_size). keyword-only.
bbox_threshold: umbral de confianza del detector de caras (0..1). Mas alto
= menos falsos positivos, riesgo de no detectar caras pequenas.
keyword-only.
feather: pixeles de difuminado del borde de la mascara al recomponer la
cara sobre la imagen. keyword-only.
sampler_name: sampler del detailer (ej. "euler"). keyword-only.
scheduler: scheduler del detailer (ej. "normal"). keyword-only.
filename_prefix: prefijo del PNG final que escribe SaveImage. keyword-only.
Returns:
dict en API format listo para `comfyui_submit_workflow`. En el modo dict
contiene los nodos del workflow base mas los del detailer (con node_ids
prefijados `fd_` para no colisionar); el SaveImage `fd_save` produce la
imagen con las caras regeneradas.
Raises:
ValueError: si se pasa un dict sin `VAEDecode` (no hay fuente de imagen)
o un tipo que no es str ni dict.
"""
bbox_norm = _normalize_bbox_model(bbox_model)
if isinstance(base_workflow_or_image, str):
# Modo imagen: cargar una imagen del input/ y montar un checkpoint nuevo.
base: dict = {}
nodes = {
"fd_load": {
"class_type": "LoadImage",
"inputs": {"image": base_workflow_or_image},
},
"fd_ckpt": {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": ckpt_name},
},
}
image_in = ["fd_load", 0]
ckpt_id = "fd_ckpt"
elif isinstance(base_workflow_or_image, dict):
# Modo workflow: tomar la imagen del VAEDecode y reutilizar el checkpoint.
base = dict(base_workflow_or_image)
vae_decode_id = _find_first(base, "VAEDecode")
if vae_decode_id is None:
raise ValueError(
"comfyui_build_facedetailer_workflow: el workflow base no tiene "
"VAEDecode; no hay fuente de imagen para el detailer. Pasa el nombre "
"de una imagen (str) o un workflow que decodifique a imagen."
)
image_in = [vae_decode_id, 0]
ckpt_id = _find_first(base, "CheckpointLoaderSimple")
nodes = {}
if ckpt_id is None:
# El base no usa CheckpointLoaderSimple (p.ej. SDXL con otro loader):
# montamos uno propio para el sampler del detailer.
nodes["fd_ckpt"] = {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": ckpt_name},
}
ckpt_id = "fd_ckpt"
else:
raise ValueError(
"comfyui_build_facedetailer_workflow: base_workflow_or_image debe ser "
f"str (nombre de imagen) o dict (workflow), no {type(base_workflow_or_image).__name__}."
)
model = [ckpt_id, 0]
clip = [ckpt_id, 1]
vae = [ckpt_id, 2]
nodes.update(
{
"fd_pos": {
"class_type": "CLIPTextEncode",
"inputs": {"text": positive, "clip": clip},
},
"fd_neg": {
"class_type": "CLIPTextEncode",
"inputs": {"text": negative, "clip": clip},
},
"fd_det": {
"class_type": "UltralyticsDetectorProvider",
"inputs": {"model_name": bbox_norm},
},
"fd_face": {
"class_type": "FaceDetailer",
"inputs": {
"image": image_in,
"model": model,
"clip": clip,
"vae": vae,
"positive": ["fd_pos", 0],
"negative": ["fd_neg", 0],
"bbox_detector": ["fd_det", 0],
"guide_size": guide_size,
"guide_size_for": True,
"max_size": 1024.0,
"seed": seed,
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": denoise,
"feather": feather,
"noise_mask": True,
"force_inpaint": True,
"bbox_threshold": bbox_threshold,
"bbox_dilation": 10,
"bbox_crop_factor": 3.0,
"sam_detection_hint": "center-1",
"sam_dilation": 0,
"sam_threshold": 0.93,
"sam_bbox_expansion": 0,
"sam_mask_hint_threshold": 0.7,
"sam_mask_hint_use_negative": "False",
"drop_size": 10,
"wildcard": "",
"cycle": 1,
},
},
"fd_save": {
"class_type": "SaveImage",
"inputs": {"filename_prefix": filename_prefix, "images": ["fd_face", 0]},
},
}
)
return {**base, **nodes}
if __name__ == "__main__":
import json
# Modo imagen: regenerar caras de una imagen ya en el input/ del servidor.
wf = comfyui_build_facedetailer_workflow(
"portrait_00001_.png",
ckpt_name="dreamshaper_8.safetensors",
positive="detailed face, sharp eyes, skin texture",
negative="blurry, deformed",
seed=42,
)
print(json.dumps(wf, indent=2))
@@ -0,0 +1,104 @@
---
name: comfyui_build_hires_fix_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_hires_fix_workflow(ckpt_name: str, positive: str, negative: str = \"\", *, first_pass: tuple[int, int] = (768, 768), upscale_by: float = 1.5, denoise: float = 0.4, steps: int = 20, cfg: float = 7.0, seed: int = 0, upscale_model: str = \"4x_foolhardy_Remacri.pth\", sampler_name: str = \"euler\", scheduler: str = \"normal\", tile_width: int = 512, tile_height: int = 512, filename_prefix: str = \"hires\") -> dict"
description: "Construye un workflow ComfyUI de hires-fix de 2 pasadas en API format: genera una imagen base pequena (KSampler) y la amplia re-difundiendola por tiles con UltimateSDUpscale + un modelo de upscale (Remacri), anadiendo detalle real a alta resolucion. UltimateSDUpscale es la segunda pasada de muestreo (recibe model/positive/negative/vae). Distinto de comfyui_build_upscale_workflow, que es ESRGAN puro sin re-difusion. Class_types verificados en /object_info. Pura, sin red ni I/O."
tags: [comfyui, ml, hires-fix, ultimatesdupscale, upscale, workflow]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: ckpt_name
desc: "Checkpoint tal como lo ve el servidor (CheckpointLoaderSimple)."
- name: positive
desc: "Prompt positivo (se usa en la base y en la re-difusion tiled)."
- name: negative
desc: "Prompt negativo. Por defecto ''."
- name: first_pass
desc: "(ancho, alto) en px de la pasada base (latente pequeno y rapido). Por defecto (768, 768). keyword-only."
- name: upscale_by
desc: "Factor de ampliacion de UltimateSDUpscale sobre la imagen base (1.5 -> 768 pasa a 1152). keyword-only."
- name: denoise
desc: "Fuerza de re-difusion de la segunda pasada (0.4 por defecto). <1 conserva la composicion base y solo anade detalle; 1.0 la re-generaria entera. keyword-only."
- name: steps
desc: "Pasos de sampling (ambas pasadas). keyword-only."
- name: cfg
desc: "Classifier-free guidance (ambas pasadas). keyword-only."
- name: seed
desc: "Semilla de la pasada base (UltimateSDUpscale usa la misma). keyword-only."
- name: upscale_model
desc: "Modelo de upscale en models/upscale_models/ que usa UltimateSDUpscale para escalar antes de re-difundir (ej. '4x_foolhardy_Remacri.pth'). keyword-only."
- name: sampler_name
desc: "Sampler (ambas pasadas). keyword-only."
- name: scheduler
desc: "Scheduler (ambas pasadas). keyword-only."
- name: tile_width
desc: "Ancho de tile de UltimateSDUpscale (px). Tiles mas pequenos = menos VRAM, mas costuras. keyword-only."
- name: tile_height
desc: "Alto de tile de UltimateSDUpscale (px). keyword-only."
- name: filename_prefix
desc: "Prefijo del PNG final que escribe SaveImage. keyword-only."
output: "dict en API format listo para comfyui_submit_workflow. node_ids: '4' CheckpointLoaderSimple, '5' EmptyLatentImage, '6'/'7' CLIPTextEncode, '3' KSampler (base), '8' VAEDecode, '11' UpscaleModelLoader, '12' UltimateSDUpscale, '9' SaveImage."
tested: true
tests: ["cadena base (KSampler) + UltimateSDUpscale + SaveImage", "denoise de la 2a pasada <1 (re-difusion parcial)", "first_pass refleja width/height en EmptyLatentImage", "upscale_model llega a UpscaleModelLoader"]
test_file_path: "python/functions/ml/tests/test_comfyui_build_hires_fix_workflow.py"
file_path: "python/functions/ml/comfyui_build_hires_fix_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_hires_fix_workflow import comfyui_build_hires_fix_workflow
wf = comfyui_build_hires_fix_workflow(
ckpt_name="dreamshaper_8.safetensors",
positive="a fox in a forest, intricate detail, sharp focus",
negative="blurry, low quality",
first_pass=(768, 768),
upscale_by=1.5,
denoise=0.4,
seed=42,
)
# wf["3"]["class_type"] == "KSampler" # pasada base
# wf["12"]["class_type"] == "UltimateSDUpscale" # pasada de detalle (re-difusion)
# wf["12"]["inputs"]["denoise"] == 0.4 # <1 = solo anade detalle
# wf["11"]["inputs"]["model_name"] == "4x_foolhardy_Remacri.pth"
```
O lanzable directo con: `./fn run comfyui_build_hires_fix_workflow` (imprime el JSON del workflow de ejemplo).
## Cuando usarla
Cuando una imagen a baja resolución se ve plana o sin detalle y quieres una
versión grande y nítida que el modelo "redibuja" en alta (no un simple escalado).
Es el "hires fix" idiomático: genera la base pequeña y rápida, luego añade detalle
real al ampliar. Úsala cuando `comfyui_build_upscale_workflow` (ESRGAN puro) se
queda corto porque no inventa detalle nuevo. Después: `comfyui_submit_workflow`
`comfyui_wait_result``comfyui_fetch_output_image`.
## Gotchas
- Es API format (nodos numerados), NO el formato de la UI.
- Requiere el custom node **UltimateSDUpscale** (`comfyui_ultimatesdupscale`). Si
el server responde HTTP 400 "node type not found: UltimateSDUpscale", el custom
node no está cargado.
- El `upscale_model` debe existir en `models/upscale_models/` (aquí
`4x_foolhardy_Remacri.pth`). Sin él, el server rechaza el workflow al encolar.
- **2 etapas de muestreo, 1 KSampler explícito**: UltimateSDUpscale re-samplea
cada tile internamente (por eso recibe `model`/`positive`/`negative`/`vae`), así
que el grafo tiene el KSampler base + el UltimateSDUpscale, no dos KSampler.
- `denoise` de la 2ª pasada controla cuánto cambia: 0.30.45 añade detalle sin
alterar la composición; >0.6 puede deformar caras o introducir artefactos.
- `upscale_by` alto + `tile_width/height` grandes = más VRAM. En 8 GB conviene
tiles de 512 y `upscale_by` 1.52.0.
- Coste real: la 2ª pasada re-difunde N tiles, es bastante más lenta que un upscale
ESRGAN puro. Para solo agrandar sin re-difusión usa `comfyui_build_upscale_workflow`.
@@ -0,0 +1,167 @@
"""Construye un workflow ComfyUI de "hires fix" de 2 pasadas en API format.
El hires fix clasico genera una imagen pequena nitida y luego la amplia
*re-difundiendola* (no solo escalando pixeles), de modo que el modelo anade
detalle coherente a la resolucion alta. Este builder lo implementa con
UltimateSDUpscale (custom node), que hace la segunda pasada por TILES con un
modelo de upscale (ESRGAN/Remacri) + un sampler con `denoise` parcial:
Pasada 1 (base): CheckpointLoaderSimple -> CLIPTextEncode(+/-) +
EmptyLatentImage(first_pass) -> KSampler -> VAEDecode
Pasada 2 (detalle): UpscaleModelLoader(Remacri) +
UltimateSDUpscale(image, model, +/-, vae, upscale_model,
upscale_by, denoise<1, tiled) -> SaveImage
UltimateSDUpscale ES la segunda pasada de muestreo: re-samplea cada tile con el
checkpoint (de ahi que reciba `model`, `positive`, `negative`, `vae`), por eso el
grafo tiene UN KSampler explicito (la base) + el UltimateSDUpscale (el detalle).
Distinto de `comfyui_build_upscale_workflow`, que es ESRGAN puro SIN re-difusion.
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
"""
from __future__ import annotations
def comfyui_build_hires_fix_workflow(
ckpt_name: str,
positive: str,
negative: str = "",
*,
first_pass: tuple[int, int] = (768, 768),
upscale_by: float = 1.5,
denoise: float = 0.4,
steps: int = 20,
cfg: float = 7.0,
seed: int = 0,
upscale_model: str = "4x_foolhardy_Remacri.pth",
sampler_name: str = "euler",
scheduler: str = "normal",
tile_width: int = 512,
tile_height: int = 512,
filename_prefix: str = "hires",
) -> dict:
"""Construye el dict de un workflow hires-fix (base + UltimateSDUpscale).
Args:
ckpt_name: checkpoint tal como lo ve el servidor (CheckpointLoaderSimple).
positive: prompt positivo (se usa en la base y en la re-difusion tiled).
negative: prompt negativo. Por defecto "".
first_pass: (ancho, alto) en px de la pasada base (latente pequeno y
rapido). Por defecto (768, 768). keyword-only.
upscale_by: factor de ampliacion de UltimateSDUpscale sobre la imagen base
(1.5 -> 768 pasa a 1152). keyword-only.
denoise: fuerza de re-difusion de la segunda pasada (0.4 por defecto).
<1 para conservar la composicion base y solo anadir detalle; 1.0 la
re-generaria entera. keyword-only.
steps: pasos de sampling (ambas pasadas). keyword-only.
cfg: classifier-free guidance (ambas pasadas). keyword-only.
seed: semilla de la pasada base (UltimateSDUpscale usa la misma).
keyword-only.
upscale_model: modelo de upscale en models/upscale_models/ que usa
UltimateSDUpscale para escalar antes de re-difundir (ej.
"4x_foolhardy_Remacri.pth"). keyword-only.
sampler_name: sampler (ambas pasadas). keyword-only.
scheduler: scheduler (ambas pasadas). keyword-only.
tile_width: ancho de tile de UltimateSDUpscale (px). Tiles mas pequenos =
menos VRAM, mas costuras. keyword-only.
tile_height: alto de tile de UltimateSDUpscale (px). keyword-only.
filename_prefix: prefijo del PNG final que escribe SaveImage. keyword-only.
Returns:
dict en API format listo para `comfyui_submit_workflow`. node_ids:
"4" CheckpointLoaderSimple, "5" EmptyLatentImage, "6"/"7" CLIPTextEncode,
"3" KSampler (base), "8" VAEDecode, "11" UpscaleModelLoader,
"12" UltimateSDUpscale, "9" SaveImage.
"""
w, h = first_pass
return {
"4": {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": ckpt_name},
},
"5": {
"class_type": "EmptyLatentImage",
"inputs": {"width": w, "height": h, "batch_size": 1},
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {"text": positive, "clip": ["4", 1]},
},
"7": {
"class_type": "CLIPTextEncode",
"inputs": {"text": negative, "clip": ["4", 1]},
},
"3": {
"class_type": "KSampler",
"inputs": {
"seed": seed,
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": 1.0,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["5", 0],
},
},
"8": {
"class_type": "VAEDecode",
"inputs": {"samples": ["3", 0], "vae": ["4", 2]},
},
"11": {
"class_type": "UpscaleModelLoader",
"inputs": {"model_name": upscale_model},
},
"12": {
"class_type": "UltimateSDUpscale",
"inputs": {
"image": ["8", 0],
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"vae": ["4", 2],
"upscale_model": ["11", 0],
"upscale_by": upscale_by,
"seed": seed,
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": denoise,
"mode_type": "Linear",
"tile_width": tile_width,
"tile_height": tile_height,
"mask_blur": 8,
"tile_padding": 32,
"seam_fix_mode": "None",
"seam_fix_denoise": 1.0,
"seam_fix_width": 64,
"seam_fix_mask_blur": 8,
"seam_fix_padding": 16,
"force_uniform_tiles": True,
"tiled_decode": False,
},
},
"9": {
"class_type": "SaveImage",
"inputs": {"filename_prefix": filename_prefix, "images": ["12", 0]},
},
}
if __name__ == "__main__":
import json
wf = comfyui_build_hires_fix_workflow(
ckpt_name="dreamshaper_8.safetensors",
positive="a fox in a forest, intricate detail, sharp focus",
negative="blurry, low quality",
first_pass=(768, 768),
upscale_by=1.5,
denoise=0.4,
seed=42,
)
print(json.dumps(wf, indent=2))
@@ -3,10 +3,10 @@ name: comfyui_build_image_to_3d_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
version: "1.1.0"
purity: pure
signature: "def comfyui_build_image_to_3d_workflow(image_name: str, ckpt_name: str = \"hunyuan3d-dit-v2-mini.safetensors\", *, resolution: int = 3072, steps: int = 30, cfg: float = 5.5, seed: int = 0, octree_resolution: int = 256, num_chunks: int = 8000, threshold: float = 0.6, sampler_name: str = \"euler\", scheduler: str = \"normal\", filename_prefix: str = \"3d_mesh\") -> dict"
description: "Construye el dict de un workflow ComfyUI imagen->malla 3D en API format usando los nodos NATIVOS de Hunyuan3D-2 de ComfyUI 0.26.0 (sin custom node). Cadena de 9 nodos: LoadImage -> ImageOnlyCheckpointLoader -> CLIPVisionEncode -> Hunyuan3Dv2Conditioning -> EmptyLatentHunyuan3Dv2 -> KSampler -> VAEDecodeHunyuan3D -> VoxelToMeshBasic -> SaveGLB. El SaveGLB produce un .glb. Pura, sin red ni I/O."
signature: "def comfyui_build_image_to_3d_workflow(image_name: str, ckpt_name: str = \"hunyuan3d-dit-v2-mini.safetensors\", *, resolution: int = 3072, steps: int = 30, cfg: float = 5.5, seed: int = 0, octree_resolution: int = 256, num_chunks: int = 8000, threshold: float = 0.6, sampler_name: str = \"euler\", scheduler: str = \"normal\", filename_prefix: str = \"3d_mesh\", watertight: bool = False) -> dict"
description: "Construye el dict de un workflow ComfyUI imagen->malla 3D en API format usando los nodos NATIVOS de Hunyuan3D-2 de ComfyUI 0.26.0 (sin custom node). Cadena de 9 nodos: LoadImage -> ImageOnlyCheckpointLoader -> CLIPVisionEncode -> Hunyuan3Dv2Conditioning -> EmptyLatentHunyuan3Dv2 -> KSampler -> VAEDecodeHunyuan3D -> (VoxelToMeshBasic | VoxelToMesh surface-net si watertight=True) -> SaveGLB. El SaveGLB produce un .glb. Pura, sin red ni I/O."
tags: [comfyui, ml, img-to-3d, hunyuan3d, mesh, workflow]
uses_functions: []
uses_types: []
@@ -32,16 +32,18 @@ params:
- name: num_chunks
desc: "Numero de chunks de decode del VAE 3D; controla el troceado del grid para caber en memoria. keyword-only."
- name: threshold
desc: "Umbral de iso-superficie de VoxelToMeshBasic (marching cubes simple sobre el grid de voxels). keyword-only."
desc: "Umbral de iso-superficie del nodo voxel->malla (marching cubes / surface net sobre el grid de voxels). keyword-only."
- name: sampler_name
desc: "Nombre del sampler del KSampler (ej. 'euler'). keyword-only."
- name: scheduler
desc: "Scheduler del sampler (ej. 'normal'). keyword-only."
- name: filename_prefix
desc: "Prefijo del archivo de malla que SaveGLB escribe en output/ (ej. '3d_mesh' -> '3d_mesh_00001_.glb'). keyword-only."
output: "dict en API format con node_ids '1'..'9' como claves; cada valor tiene class_type + inputs. Listo para comfyui_submit_workflow. El nodo '9' (SaveGLB) produce el archivo .glb en el output del servidor."
- name: watertight
desc: "Si False (default, retro-compatible) el nodo '8' es VoxelToMeshBasic (malla NO estanca). Si True usa VoxelToMesh con algorithm='surface net', que produce una malla estanca/manifold de raiz sin post-proceso. keyword-only."
output: "dict en API format con node_ids '1'..'9' como claves; cada valor tiene class_type + inputs. Listo para comfyui_submit_workflow. El nodo '9' (SaveGLB) produce el archivo .glb en el output del servidor. El nodo '8' es VoxelToMeshBasic (watertight=False) o VoxelToMesh surface-net (watertight=True)."
tested: true
tests: ["cadena de 9 nodos Hunyuan3D-2 nativos", "imagen, checkpoint, seed reflejados y SaveGLB presente"]
tests: ["cadena de 9 nodos Hunyuan3D-2 nativos", "imagen, checkpoint, seed reflejados y SaveGLB presente", "watertight=True usa VoxelToMesh surface-net; default conserva VoxelToMeshBasic"]
test_file_path: "python/functions/ml/tests/test_comfyui_build_image_to_3d_workflow.py"
file_path: "python/functions/ml/comfyui_build_image_to_3d_workflow.py"
---
@@ -91,7 +93,20 @@ grafo de 9 nodos a mano. Para hacerlo end-to-end desde una imagen en disco (subi
- El camino nativo es **shape-only**: la malla sale SIN color/textura. Para color
por vertice o textura horneada haria falta el wrapper de kijai (compila
custom_rasterizer) — fuera de alcance.
- `VoxelToMeshBasic` no garantiza malla estanca (`watertight=False` es esperable).
Para watertight probar el nodo `VoxelToMesh` con su algoritmo alternativo.
- Con el default (`watertight=False`) el nodo `VoxelToMeshBasic` produce malla NO
estanca ("cube-soup"), lo esperable; se arregla a posteriori con
`comfyui_make_watertight` o el pipeline `comfyui_mesh_cleanup_oneshot`. Para
malla estanca DE RAÍZ pasa `watertight=True`: usa `VoxelToMesh` con
`algorithm="surface net"` (manifold cerrado sin reparar, ver report 0088). El
nodo `VoxelToMesh` es nativo de ComfyUI >= 0.26.0 (`nodes_hunyuan3d.py`); en
versiones sin él, usar el default + post-proceso.
- `octree_resolution` alto (256) produce mallas muy densas (decenas de MB de GLB,
>1M caras) sin decimacion. Para web conviene un paso de simplificacion posterior.
>1M caras) sin decimacion. Para web conviene un paso de simplificacion posterior
(`comfyui_simplify_mesh` / `comfyui_mesh_cleanup_oneshot`).
## Capability growth log
- v1.1.0 (2026-06-24) — añade `watertight=False` (keyword-only, retro-compatible):
con `True` el nodo voxel→malla usa `VoxelToMesh` (`algorithm="surface net"`) en
vez de `VoxelToMeshBasic`, para mallas estancas de raíz sin post-proceso. El
default conserva el comportamiento histórico exacto.
@@ -10,7 +10,15 @@ GLB. Cadena de 9 nodos:
LoadImage -> ImageOnlyCheckpointLoader -> CLIPVisionEncode ->
Hunyuan3Dv2Conditioning -> EmptyLatentHunyuan3Dv2 -> KSampler ->
VAEDecodeHunyuan3D -> VoxelToMeshBasic -> SaveGLB
VAEDecodeHunyuan3D -> (VoxelToMeshBasic | VoxelToMesh) -> SaveGLB
El paso voxel->malla depende del parametro `watertight`:
- watertight=False (default): VoxelToMeshBasic, el comportamiento historico
(marching cubes simple; malla NO estanca, "cube-soup", que luego se arregla con
comfyui_make_watertight).
- watertight=True: VoxelToMesh con algorithm="surface net" (verificado en
/object_info, nodes_hunyuan3d.py), que produce una malla manifold/estanca de
raiz, sin post-proceso (ver report 0088).
El checkpoint Hunyuan3D-2 (mini/standard) es self-contained: ImageOnlyCheckpointLoader
devuelve MODEL, CLIP_VISION y VAE de un solo .safetensors.
@@ -33,6 +41,7 @@ def comfyui_build_image_to_3d_workflow(
sampler_name: str = "euler",
scheduler: str = "normal",
filename_prefix: str = "3d_mesh",
watertight: bool = False,
) -> dict:
"""Construye el dict del workflow imagen->3D nativo (Hunyuan3D-2).
@@ -53,18 +62,39 @@ def comfyui_build_image_to_3d_workflow(
Mayor = malla mas densa (mas caras) y mas memoria. keyword-only.
num_chunks: numero de chunks de decode del VAE 3D; controla el troceado
del grid para caber en memoria. keyword-only.
threshold: umbral de iso-superficie de VoxelToMeshBasic (marching cubes
simple sobre el grid de voxels). keyword-only.
threshold: umbral de iso-superficie del nodo voxel->malla (marching cubes
/ surface net sobre el grid de voxels). keyword-only.
sampler_name: nombre del sampler del KSampler (ej. "euler"). keyword-only.
scheduler: scheduler del sampler (ej. "normal"). keyword-only.
filename_prefix: prefijo del archivo de malla que SaveGLB escribe en
output/ (ej. "3d_mesh" -> "3d_mesh_00001_.glb"). keyword-only.
watertight: si False (default, retro-compatible) el nodo "8" es
VoxelToMeshBasic (malla NO estanca). Si True usa VoxelToMesh con
algorithm="surface net", que produce una malla estanca/manifold de
raiz sin post-proceso. keyword-only.
Returns:
dict en API format con node_ids "1".."9" como claves; cada valor tiene
class_type + inputs. Listo para comfyui_submit_workflow. El nodo "9"
(SaveGLB) produce el archivo .glb en el output del servidor.
(SaveGLB) produce el archivo .glb en el output del servidor. El nodo "8"
es VoxelToMeshBasic (watertight=False) o VoxelToMesh surface-net
(watertight=True).
"""
voxel_node = (
{
"class_type": "VoxelToMesh",
"inputs": {
"voxel": ["7", 0],
"algorithm": "surface net",
"threshold": threshold,
},
}
if watertight
else {
"class_type": "VoxelToMeshBasic",
"inputs": {"voxel": ["7", 0], "threshold": threshold},
}
)
return {
"1": {
"class_type": "LoadImage",
@@ -114,10 +144,7 @@ def comfyui_build_image_to_3d_workflow(
"octree_resolution": octree_resolution,
},
},
"8": {
"class_type": "VoxelToMeshBasic",
"inputs": {"voxel": ["7", 0], "threshold": threshold},
},
"8": voxel_node,
"9": {
"class_type": "SaveGLB",
"inputs": {"mesh": ["8", 0], "filename_prefix": filename_prefix},
@@ -0,0 +1,84 @@
"""Tests de comfyui_build_facedetailer_workflow (builder puro, sin red)."""
import os
import sys
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from ml.comfyui_build_facedetailer_workflow import comfyui_build_facedetailer_workflow # noqa: E402
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow # noqa: E402
def test_image_mode_builds_detailer_chain():
wf = comfyui_build_facedetailer_workflow(
"portrait_00001_.png",
ckpt_name="dreamshaper_8.safetensors",
positive="detailed face",
seed=42,
)
# Detector + FaceDetailer + SaveImage presentes.
assert wf["fd_det"]["class_type"] == "UltralyticsDetectorProvider"
assert wf["fd_face"]["class_type"] == "FaceDetailer"
assert wf["fd_save"]["class_type"] == "SaveImage"
# La imagen viene del LoadImage propio del modo str.
assert wf["fd_load"]["class_type"] == "LoadImage"
assert wf["fd_face"]["inputs"]["image"] == ["fd_load", 0]
# FaceDetailer conecta el bbox_detector y la conditioning.
assert wf["fd_face"]["inputs"]["bbox_detector"] == ["fd_det", 0]
assert wf["fd_face"]["inputs"]["positive"] == ["fd_pos", 0]
assert wf["fd_face"]["inputs"]["seed"] == 42
def test_workflow_mode_reuses_base_nodes():
base = comfyui_build_txt2img_workflow(
ckpt_name="dreamshaper_8.safetensors",
positive="portrait of a woman",
seed=7,
)
wf = comfyui_build_facedetailer_workflow(
base,
ckpt_name="dreamshaper_8.safetensors",
positive="detailed face",
denoise=0.45,
)
# Los nodos del base se conservan.
assert wf["4"]["class_type"] == "CheckpointLoaderSimple"
assert wf["8"]["class_type"] == "VAEDecode"
# La imagen del detailer viene del VAEDecode del base.
assert wf["fd_face"]["inputs"]["image"] == ["8", 0]
# Reutiliza el checkpoint del base (model/clip/vae del nodo "4").
assert wf["fd_face"]["inputs"]["model"] == ["4", 0]
assert wf["fd_face"]["inputs"]["vae"] == ["4", 2]
assert wf["fd_face"]["inputs"]["denoise"] == 0.45
def test_normalizes_short_bbox_model():
wf = comfyui_build_facedetailer_workflow(
"x.png", ckpt_name="dreamshaper_8.safetensors", positive="face",
bbox_model="face_yolov8m.pt",
)
assert wf["fd_det"]["inputs"]["model_name"] == "bbox/face_yolov8m.pt"
# Un nombre ya prefijado se respeta.
wf2 = comfyui_build_facedetailer_workflow(
"x.png", ckpt_name="dreamshaper_8.safetensors", positive="face",
bbox_model="bbox/face_yolov8m.pt",
)
assert wf2["fd_det"]["inputs"]["model_name"] == "bbox/face_yolov8m.pt"
def test_dict_without_vaedecode_raises():
with pytest.raises(ValueError):
comfyui_build_facedetailer_workflow(
{"1": {"class_type": "LoadImage", "inputs": {"image": "x.png"}}},
ckpt_name="dreamshaper_8.safetensors",
positive="face",
)
def test_invalid_base_type_raises():
with pytest.raises(ValueError):
comfyui_build_facedetailer_workflow(
123, ckpt_name="dreamshaper_8.safetensors", positive="face",
)
@@ -0,0 +1,50 @@
"""Tests de comfyui_build_hires_fix_workflow (builder puro, sin red)."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from ml.comfyui_build_hires_fix_workflow import comfyui_build_hires_fix_workflow # noqa: E402
def test_base_ksampler_and_ultimate_upscale_present():
wf = comfyui_build_hires_fix_workflow(
ckpt_name="dreamshaper_8.safetensors",
positive="a fox",
seed=42,
)
# Pasada base = KSampler; pasada de detalle = UltimateSDUpscale.
assert wf["3"]["class_type"] == "KSampler"
assert wf["12"]["class_type"] == "UltimateSDUpscale"
assert wf["9"]["class_type"] == "SaveImage"
# El SaveImage cuelga del UltimateSDUpscale (la imagen final).
assert wf["9"]["inputs"]["images"] == ["12", 0]
def test_second_pass_denoise_is_partial():
wf = comfyui_build_hires_fix_workflow(
ckpt_name="dreamshaper_8.safetensors", positive="x", denoise=0.4,
)
# La base re-genera entera (denoise=1.0); la 2a pasada solo anade detalle (<1).
assert wf["3"]["inputs"]["denoise"] == 1.0
assert wf["12"]["inputs"]["denoise"] == 0.4
assert wf["12"]["inputs"]["denoise"] < 1.0
def test_first_pass_dims_reflected():
wf = comfyui_build_hires_fix_workflow(
ckpt_name="dreamshaper_8.safetensors", positive="x", first_pass=(640, 960),
)
assert wf["5"]["inputs"]["width"] == 640
assert wf["5"]["inputs"]["height"] == 960
def test_upscale_model_wired():
wf = comfyui_build_hires_fix_workflow(
ckpt_name="dreamshaper_8.safetensors", positive="x",
upscale_model="4x_foolhardy_Remacri.pth",
)
assert wf["11"]["class_type"] == "UpscaleModelLoader"
assert wf["11"]["inputs"]["model_name"] == "4x_foolhardy_Remacri.pth"
assert wf["12"]["inputs"]["upscale_model"] == ["11", 0]
@@ -40,3 +40,19 @@ def test_imagen_checkpoint_y_salida_glb():
assert node_by_ct(wf, "KSampler")["inputs"]["seed"] == 42
# SaveGLB es el nodo de salida: produce la malla .glb.
assert "SaveGLB" in class_types(wf)
def test_default_conserva_voxeltomeshbasic():
# Retro-compatibilidad: sin watertight el nodo "8" es VoxelToMeshBasic.
wf = comfyui_build_image_to_3d_workflow("obj.png")
assert wf["8"]["class_type"] == "VoxelToMeshBasic"
assert "algorithm" not in wf["8"]["inputs"]
def test_watertight_usa_voxeltomesh_surface_net():
wf = comfyui_build_image_to_3d_workflow("obj.png", watertight=True)
assert wf["8"]["class_type"] == "VoxelToMesh"
assert wf["8"]["inputs"]["algorithm"] == "surface net"
assert wf["8"]["inputs"]["voxel"] == ["7", 0]
# El resto de la cadena no cambia: SaveGLB sigue colgando del nodo "8".
assert wf["9"]["inputs"]["mesh"] == ["8", 0]
@@ -0,0 +1,86 @@
---
name: comfyui_mesh_cleanup_oneshot
kind: pipeline
lang: py
domain: pipelines
version: "1.0.0"
purity: impure
signature: "def comfyui_mesh_cleanup_oneshot(in_path: str, *, target_faces: int = 80000, watertight: bool = True, method: str = \"repair\", out_path: str | None = None) -> dict"
description: "Pipeline de limpieza de mallas 3D en una sola llamada: decima a un presupuesto de caras (comfyui_simplify_mesh) y, opcionalmente, la hace estanca (comfyui_make_watertight). Capitaliza el 'Golden 3' del report 0088 (80k caras + estanca a la vez) para las mallas densas no estancas que produce el pipeline Hunyuan3D de ComfyUI. Promocion de secuencia (issue 0087). Impuro: lee y escribe archivos en disco; requiere trimesh + pymeshlab + scipy."
tags: [comfyui, pipelines, mesh, cleanup, watertight, launcher]
uses_functions: [comfyui_simplify_mesh_py_ml, comfyui_make_watertight_py_ml]
uses_types: []
returns: []
returns_optional: false
error_type: error_py_core
imports: [comfyui_simplify_mesh_py_ml, comfyui_make_watertight_py_ml]
params:
- name: in_path
desc: "Ruta de la malla de entrada (.glb/.obj/.ply/.gltf/.stl/.off)."
- name: target_faces
desc: "Caras objetivo del paso de decimacion (comfyui_simplify_mesh). keyword-only."
- name: watertight
desc: "Si True (default) aplica comfyui_make_watertight tras decimar; si False solo decima. keyword-only."
- name: method
desc: "Metodo de make_watertight cuando watertight=True. 'repair' (default) conserva las caras decimadas pero no garantiza estanqueidad en mallas muy rotas; 'voxel' garantiza is_watertight=True via voxeliza+fill+marching cubes, a costa de re-mallar (cambia el conteo de caras y descarta apariencia). keyword-only."
- name: out_path
desc: "Ruta de salida final. Si None, se deriva de in_path ('<in>_cleaned.glb'). keyword-only."
output: "dict {ok, in_path, out_path, in_faces, simplified_faces, final_faces, watertight_requested, is_watertight, method, steps, error}. steps = resultados crudos de cada funcion compuesta. Si algun paso falla, ok=False y error explica en cual."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/pipelines/comfyui_mesh_cleanup_oneshot.py"
---
## Ejemplo
```bash
# Limpia la malla densa de ComfyUI: decima a 80k caras y la hace estanca.
./fn run comfyui_mesh_cleanup_oneshot ~/ComfyUI/output/3d_mesh_00002_.glb
```
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from pipelines.comfyui_mesh_cleanup_oneshot import comfyui_mesh_cleanup_oneshot
res = comfyui_mesh_cleanup_oneshot(
os.path.expanduser("~/ComfyUI/output/3d_mesh_00002_.glb"),
target_faces=80000,
watertight=True,
)
# res["ok"] == True
# res["in_faces"] >> res["final_faces"] # decimada
# res["is_watertight"] # True/False segun method y la malla
print(res["in_faces"], "->", res["final_faces"], "watertight:", res["is_watertight"])
```
## Cuando usarla
Justo después de generar una malla 3D con ComfyUI/Hunyuan3D
(`comfyui_image_to_3d_oneshot`, `comfyui_text_to_3d_oneshot`), cuando el GLB sale
denso (decenas de MB, >1M caras) y/o no estanco. En una llamada la dejas lista
para web (ligera) o impresión 3D (cerrada). Es la promoción del patrón
"simplify → make_watertight" que se repetía a mano (reports 0088/0091).
## Gotchas
- Impuro: lee y **escribe** archivos en disco. Con `watertight=True` deja también
un intermedio `<out>_simplified.glb` junto al final.
- Necesita `trimesh` + `pymeshlab` + `scipy` (decimación). Con `method="voxel"`
además `scikit-image` (marching cubes). Si falta una, el paso correspondiente
devuelve `ok=False` con el comando `uv add` a ejecutar.
- **`method="repair"` (default) NO garantiza estanqueidad** en mallas muy rotas:
cierra huecos pequeños y conserva las caras decimadas. El output reporta
`is_watertight` real — compruébalo. Si necesitas estanqueidad GARANTIZADA usa
`method="voxel"` (re-malla: cambia el conteo de caras y descarta UV/colores).
- El `target_faces` aplica al paso de decimación. Con `method="voxel"` el conteo
final lo fija el voxel-remesh (mira `final_faces`), no `target_faces`.
- El pipeline NO falla si la malla no queda estanca con `repair`: devuelve `ok=True`
con `is_watertight=False`. Decide en el caller si reintentar con `method="voxel"`.
- No toca el servidor ComfyUI: opera sobre archivos GLB ya en disco.
## Capability growth log
- v1.0.0 (2026-06-24) — pipeline inicial. Compone `comfyui_simplify_mesh` +
`comfyui_make_watertight` (issue 0087, capitaliza report 0088).
@@ -0,0 +1,119 @@
"""comfyui_mesh_cleanup_oneshot — limpia una malla 3D en una sola llamada.
Promocion de la secuencia repetida (issue 0087) del post-proceso de mallas
Hunyuan3D: decimar a un presupuesto de caras razonable y, opcionalmente, hacerla
estanca. Capitaliza el "Golden 3" del report 0088 (80k caras + estanca a la vez).
Compone funciones del registry del grupo `comfyui`:
comfyui_simplify_mesh_py_ml (decima conservando apariencia)
comfyui_make_watertight_py_ml (cierra la malla; solo si watertight=True)
Las mallas nativas de ComfyUI/Hunyuan3D salen densas (decenas de MB, >1M caras) y
NO estancas (VoxelToMeshBasic produce "cube-soup"). Este pipeline las deja listas
para web/impresion: ligeras y, si se pide, cerradas.
Pipeline impuro: lee y escribe archivos en disco. Requiere trimesh + pymeshlab +
scipy (simplify) y, para method="voxel", scikit-image (make_watertight).
"""
from __future__ import annotations
import os
import sys
# Importa las funciones del registry (mismo arbol python/functions).
_FUNCTIONS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _FUNCTIONS_ROOT not in sys.path:
sys.path.insert(0, _FUNCTIONS_ROOT)
from ml.comfyui_make_watertight import comfyui_make_watertight
from ml.comfyui_simplify_mesh import comfyui_simplify_mesh
def comfyui_mesh_cleanup_oneshot(
in_path: str,
*,
target_faces: int = 80000,
watertight: bool = True,
method: str = "repair",
out_path: str | None = None,
) -> dict:
"""Decima una malla y, opcionalmente, la hace estanca, en una sola llamada.
Args:
in_path: ruta de la malla de entrada (.glb/.obj/.ply/.gltf/.stl/.off).
target_faces: caras objetivo del paso de decimacion (comfyui_simplify_mesh).
keyword-only.
watertight: si True (default) aplica comfyui_make_watertight tras decimar;
si False solo decima. keyword-only.
method: metodo de make_watertight cuando watertight=True. "repair" (default)
conserva las caras decimadas (fill_holes + fix_normals) pero NO garantiza
estanqueidad en mallas muy rotas; "voxel" GARANTIZA is_watertight=True via
voxeliza+fill+marching cubes, a costa de re-mallar (cambia el conteo de
caras y descarta apariencia). keyword-only.
out_path: ruta de salida final. Si None, se deriva de in_path
("<in>_cleaned.glb"). keyword-only.
Returns:
dict {ok, in_path, out_path, in_faces, simplified_faces, final_faces,
watertight_requested, is_watertight, method, steps, error}. `steps` es la
lista de resultados crudos de cada funcion compuesta (para auditar). Si algun
paso falla, ok=False y error explica en cual.
"""
base = {
"ok": False, "in_path": in_path, "out_path": "",
"in_faces": 0, "simplified_faces": 0, "final_faces": 0,
"watertight_requested": watertight, "is_watertight": None,
"method": method, "steps": [], "error": "",
}
if watertight and method not in ("repair", "voxel"):
return {**base, "error": f"method '{method}' invalido (usa 'repair' o 'voxel')"}
if out_path is None:
out_path = os.path.splitext(in_path)[0] + "_cleaned.glb"
# Paso 1: decimar. Si no se pide watertight, este es el output final directo.
simplify_out = out_path if not watertight else (
os.path.splitext(out_path)[0] + "_simplified.glb"
)
s = comfyui_simplify_mesh(in_path, target_faces=target_faces, out_path=simplify_out)
steps = [{"step": "simplify_mesh", **s}]
if not s.get("ok"):
return {**base, "steps": steps, "error": f"simplify_mesh fallo: {s.get('error')}"}
in_faces = s["in_faces"]
simplified_faces = s["out_faces"]
if not watertight:
return {
**base, "ok": True, "out_path": s["out_path"],
"in_faces": in_faces, "simplified_faces": simplified_faces,
"final_faces": simplified_faces, "is_watertight": None,
"steps": steps,
}
# Paso 2: hacer estanca la malla decimada.
w = comfyui_make_watertight(s["out_path"], method=method, out_path=out_path)
steps.append({"step": "make_watertight", **w})
if not w.get("ok"):
return {
**base, "out_path": s["out_path"],
"in_faces": in_faces, "simplified_faces": simplified_faces,
"final_faces": simplified_faces, "steps": steps,
"error": f"make_watertight fallo: {w.get('error')}",
}
return {
**base, "ok": True, "out_path": w["out_path"],
"in_faces": in_faces, "simplified_faces": simplified_faces,
"final_faces": w["out_faces"], "is_watertight": w["is_watertight"],
"steps": steps,
}
if __name__ == "__main__":
import json
src = sys.argv[1] if len(sys.argv) > 1 else (
os.path.expanduser("~/ComfyUI/output/3d_mesh_00002_.glb"))
res = comfyui_mesh_cleanup_oneshot(src)
print(json.dumps({k: v for k, v in res.items() if k != "steps"}, indent=2))
@@ -0,0 +1,89 @@
---
name: comfyui_txt2img_oneshot
kind: pipeline
lang: py
domain: pipelines
version: "1.0.0"
purity: impure
signature: "def comfyui_txt2img_oneshot(prompt: str, *, ckpt: str = \"dreamshaper_8.safetensors\", negative: str = \"\", server: str = \"127.0.0.1:8188\", dest: str | None = None, wait_timeout: float = 300.0, **gen) -> dict"
description: "Pipeline prompt de texto -> PNG en disco en una sola llamada. Construye el workflow txt2img de Stable Diffusion, lo encola en ComfyUI, espera y descarga la imagen. Compone comfyui_build_txt2img_workflow + comfyui_submit_workflow + comfyui_wait_result + comfyui_fetch_output_image. Promocion de secuencia (issue 0087). Impuro: HTTP + disco."
tags: [comfyui, pipelines, txt2img, stable-diffusion, launcher]
uses_functions: [comfyui_build_txt2img_workflow_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml]
uses_types: []
returns: []
returns_optional: false
error_type: error_py_core
imports: [comfyui_build_txt2img_workflow_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml]
params:
- name: prompt
desc: "Prompt positivo (lo que se quiere ver en la imagen)."
- name: ckpt
desc: "Checkpoint Stable Diffusion tal como lo ve el servidor (CheckpointLoaderSimple). Por defecto 'dreamshaper_8.safetensors'. keyword-only."
- name: negative
desc: "Prompt negativo. Por defecto ''. keyword-only."
- name: server
desc: "host:port del servidor ComfyUI (sin esquema). keyword-only."
- name: dest
desc: "Directorio local donde guardar el PNG (None = cwd). keyword-only."
- name: wait_timeout
desc: "Segundos maximos esperando a que el server termine. keyword-only."
- name: gen
desc: "Parametros de generacion pasados a comfyui_build_txt2img_workflow (steps, cfg, width, height, seed, sampler_name, scheduler, filename_prefix). keyword-only (**gen)."
output: "dict {ok, image_path, prompt_id, error}. image_path = ruta local del PNG descargado; prompt_id = id del trabajo en ComfyUI. Si falla, ok=False y error explica en que paso."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/pipelines/comfyui_txt2img_oneshot.py"
---
## Ejemplo
```bash
# Genera una imagen desde texto y la baja a /tmp.
./fn run comfyui_txt2img_oneshot "a red apple on a wooden table, sharp focus"
```
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from pipelines.comfyui_txt2img_oneshot import comfyui_txt2img_oneshot
res = comfyui_txt2img_oneshot(
"a red apple on a wooden table, sharp focus",
negative="blurry, low quality",
dest="/tmp/comfy_txt2img",
steps=20,
seed=42,
)
# res["ok"] == True
# res["image_path"] # ruta local del PNG
print(res["image_path"], res["prompt_id"])
```
## Cuando usarla
Cuando solo quieres "texto → imagen" sin montar el grafo a mano ni encadenar
submit/wait/fetch tú mismo. Es la forma de una sola llamada del flujo txt2img más
común. Para añadir detalle de caras encadénala con `comfyui_build_facedetailer_workflow`,
o para nitidez en alta con `comfyui_build_hires_fix_workflow` (esos son builders;
este pipeline ejecuta el camino básico end-to-end).
## Gotchas
- Impuro: requiere el **servidor ComfyUI vivo** en `server` (default
`127.0.0.1:8188`). Si está caído, falla en el paso submit con el error de conexión.
- `ckpt` debe existir en el servidor (CheckpointLoaderSimple). Default
`dreamshaper_8.safetensors`; cámbialo si usas SDXL u otro.
- `dest` es un **directorio** (se crea si no existe), no un nombre de archivo: el
PNG conserva el nombre que le da ComfyUI (`<filename_prefix>_NNNNN_.png`).
- `wait_timeout` por defecto 300 s. Subir resolución/steps puede requerir más; si
el server está cargando el modelo por primera vez, la primera generación tarda más.
- Devuelve el **primer** PNG de los outputs. Para batches de varias imágenes usa
`comfyui_batch_generate` o itera con distintos `seed`.
- No reintenta: si el server está ocupado con otra cola, encola igualmente y espera
su turno hasta `wait_timeout`.
## Capability growth log
- v1.0.0 (2026-06-24) — pipeline inicial. Compone build_txt2img + submit + wait +
fetch_output_image (issue 0087, roadmap del report 0092).
@@ -0,0 +1,117 @@
"""comfyui_txt2img_oneshot — prompt de texto -> PNG en disco en una sola llamada.
Promocion de la secuencia repetida (issue 0087): construir el workflow txt2img ->
encolar -> esperar -> descargar la imagen. Compone funciones del registry del
grupo `comfyui`:
comfyui_build_txt2img_workflow_py_ml (workflow de nodos en API format)
comfyui_submit_workflow_py_ml (POST /prompt)
comfyui_wait_result_py_ml (poll /history)
comfyui_fetch_output_image_py_ml (GET /view -> disco)
Pipeline impuro: red (HTTP) + escritura en disco.
"""
from __future__ import annotations
import os
import sys
# Importa las funciones del registry (mismo arbol python/functions).
_FUNCTIONS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _FUNCTIONS_ROOT not in sys.path:
sys.path.insert(0, _FUNCTIONS_ROOT)
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
from ml.comfyui_submit_workflow import comfyui_submit_workflow
from ml.comfyui_wait_result import comfyui_wait_result
def comfyui_txt2img_oneshot(
prompt: str,
*,
ckpt: str = "dreamshaper_8.safetensors",
negative: str = "",
server: str = "127.0.0.1:8188",
dest: str | None = None,
wait_timeout: float = 300.0,
**gen,
) -> dict:
"""Genera una imagen desde un prompt de texto, end-to-end.
Args:
prompt: prompt positivo (lo que se quiere ver en la imagen).
ckpt: checkpoint Stable Diffusion tal como lo ve el servidor
(CheckpointLoaderSimple). Por defecto "dreamshaper_8.safetensors".
keyword-only.
negative: prompt negativo. Por defecto "". keyword-only.
server: host:port del servidor ComfyUI (sin esquema). keyword-only.
dest: directorio local donde guardar el PNG (None = cwd). keyword-only.
wait_timeout: segundos maximos esperando a que el server termine.
keyword-only.
**gen: parametros de generacion pasados a comfyui_build_txt2img_workflow
(steps, cfg, width, height, seed, sampler_name, scheduler,
filename_prefix).
Returns:
dict {ok, image_path, prompt_id, error}. image_path = ruta local del PNG
descargado; prompt_id = id del trabajo en ComfyUI. Si falla, ok=False y
error explica en que paso.
"""
# 1. Construir el workflow (funcion pura del registry).
workflow = comfyui_build_txt2img_workflow(ckpt, prompt, negative, **gen)
# 2. Encolar.
try:
sub = comfyui_submit_workflow(workflow, server=server)
prompt_id = sub["prompt_id"]
except (RuntimeError, KeyError) as exc:
return {"ok": False, "image_path": "", "prompt_id": "",
"error": f"submit fallo: {exc}"}
# 3. Esperar a que termine.
try:
outputs = comfyui_wait_result(prompt_id, server=server, timeout=wait_timeout)
except (TimeoutError, RuntimeError) as exc:
return {"ok": False, "image_path": "", "prompt_id": prompt_id,
"error": f"wait fallo: {exc}"}
# 4. Localizar el primer PNG en los outputs (nodo SaveImage -> images).
img = None
for node_out in outputs.values():
images = node_out.get("images") if isinstance(node_out, dict) else None
if images:
img = images[0]
break
if img is None:
return {"ok": False, "image_path": "", "prompt_id": prompt_id,
"error": f"el workflow no produjo imagenes (outputs={list(outputs)})"}
# 5. Descargar la imagen a disco.
fetched = comfyui_fetch_output_image(
img["filename"],
subfolder=img.get("subfolder", ""),
type_=img.get("type", "output"),
server=server,
dest_dir=dest or ".",
)
if not fetched.get("ok"):
return {"ok": False, "image_path": "", "prompt_id": prompt_id,
"error": f"fetch de imagen fallo: {fetched.get('error')}"}
return {"ok": True, "image_path": fetched["path"], "prompt_id": prompt_id,
"error": ""}
if __name__ == "__main__":
import json
res = comfyui_txt2img_oneshot(
"a red apple on a wooden table, sharp focus",
negative="blurry, low quality",
dest="/tmp/comfy_txt2img",
steps=20,
seed=42,
)
print(json.dumps(res, indent=2))