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:
@@ -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.4–0.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.3–0.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.5–2.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]
|
||||
|
||||
Reference in New Issue
Block a user