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]
|
||||
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user