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]