diff --git a/docs/capabilities/comfyui.md b/docs/capabilities/comfyui.md index f7d9625e..8eac6a9a 100644 --- a/docs/capabilities/comfyui.md +++ b/docs/capabilities/comfyui.md @@ -71,6 +71,18 @@ El **API format** (dict de nodos numerados que produce `build_txt2img_workflow` | [comfyui_resolve_workflow_deps_py_ml](../../python/functions/ml/comfyui_resolve_workflow_deps.md) | `resolve_workflow_deps(workflow, server='127.0.0.1:8188') -> dict` | Para un workflow ajeno: valida y traduce lo que falta en acciones (`{missing_nodes, missing_models, suggestions}`). Compone `validate_workflow`. Impura. | | [comfyui_list_installed_models_py_ml](../../python/functions/ml/comfyui_list_installed_models.md) | `list_installed_models(folder=None, comfyui_dir='~/ComfyUI') -> dict` | Lista modelos por carpeta resolviendo la ruta real de `extra_model_paths.yaml` (`/mnt/2tb/comfyui_models/`) + la nativa. Escaneo de FS, no depende del server. Impura. | +### Retoque pro y oneshot — dominio `ml` + `pipelines` (P0, lote report 0093) + +Builders que envuelven custom-nodes "pro" ya instalados (Impact-Pack, UltimateSDUpscale) y la +promoción del flujo txt2img a una sola llamada. Los class_types se verificaron contra el +`/object_info` del server vivo (FaceDetailer, UltralyticsDetectorProvider, UltimateSDUpscale). + +| ID | Firma corta | Qué hace | +|---|---|---| +| [comfyui_build_facedetailer_workflow_py_ml](../../python/functions/ml/comfyui_build_facedetailer_workflow.md) | `build_facedetailer_workflow(base_workflow_or_image, ckpt_name, positive, negative='', *, bbox_model='face_yolov8m.pt', denoise=0.5, ...) -> dict` | Builder **FaceDetailer** (Impact-Pack): detecta caras con `UltralyticsDetectorProvider` (YOLO bbox) y las regenera para recuperar detalle (el pain #1 de retratos). Acepta el nombre de una imagen en `input/` (str) o un workflow base (dict): toma la imagen del `VAEDecode` y reutiliza el `CheckpointLoaderSimple`. No usa SAM (no instalado). **Pura**. | +| [comfyui_build_hires_fix_workflow_py_ml](../../python/functions/ml/comfyui_build_hires_fix_workflow.md) | `build_hires_fix_workflow(ckpt_name, positive, negative='', *, first_pass=(768,768), upscale_by=1.5, denoise=0.4, steps=20, ...) -> dict` | Builder **hires fix** de 2 pasadas: genera base (KSampler) y la amplía re-difundiéndola por tiles con `UltimateSDUpscale` + Remacri (`denoise<1` = añade detalle real). Distinto de `build_upscale_workflow` (ESRGAN puro, sin re-difusión). **Pura**. | +| [comfyui_txt2img_oneshot_py_pipelines](../../python/functions/pipelines/comfyui_txt2img_oneshot.md) | `txt2img_oneshot(prompt, *, ckpt='dreamshaper_8.safetensors', negative='', server, dest=None, wait_timeout, **gen) -> dict` | **Pipeline** texto → PNG en disco en una llamada: build_txt2img + submit + wait + fetch_output_image → `{ok, image_path, prompt_id, error}`. Promoción de la secuencia (issue 0087). Impuro. | + ### Vídeo (txt2video) — dominio `ml` (tag `video-generation`) ComfyUI ≥ 0.26.0 trae soporte nativo para **vídeo por difusión**. `build_video_workflow` cubre @@ -97,7 +109,7 @@ report `0079`). | ID | Firma corta | Qué hace | |---|---|---| -| [comfyui_build_image_to_3d_workflow_py_ml](../../python/functions/ml/comfyui_build_image_to_3d_workflow.md) | `build_image_to_3d_workflow(image_name, ckpt_name='hunyuan3d-dit-v2-mini.safetensors', *, resolution, steps, cfg, seed, octree_resolution, num_chunks, threshold, ...) -> dict` | Builder del workflow imagen→3D de 9 nodos (Hunyuan3D-2 nativo) en API format. El SaveGLB produce un `.glb`. **Pura**. | +| [comfyui_build_image_to_3d_workflow_py_ml](../../python/functions/ml/comfyui_build_image_to_3d_workflow.md) | `build_image_to_3d_workflow(image_name, ckpt_name='hunyuan3d-dit-v2-mini.safetensors', *, resolution, steps, cfg, seed, octree_resolution, num_chunks, threshold, ..., watertight=False) -> dict` | Builder del workflow imagen→3D de 9 nodos (Hunyuan3D-2 nativo) en API format. El SaveGLB produce un `.glb`. `watertight=True` usa `VoxelToMesh` (`algorithm='surface net'`) en vez de `VoxelToMeshBasic` → malla estanca de raíz (default conserva el comportamiento histórico). **Pura**. | | [comfyui_generate_views_from_image_py_ml](../../python/functions/ml/comfyui_generate_views_from_image.md) | `generate_views_from_image(image_name, *, method='auto', server, azimuths=(90,180,270), elevation, dest_dir, validate_only=False, ...) -> dict` | Sintetiza vistas novel-view (back/left/right) desde 1 imagen con StableZero123/SV3D nativos, para alimentar el 3D multi-vista. **Honesta**: si el nodo+checkpoint no están, devuelve `ok=False` con la acción y NO encola. `validate_only=True` valida sin tocar GPU. Impura. | | [comfyui_build_view_3d_workflow_py_ml](../../python/functions/ml/comfyui_build_view_3d_workflow.md) | `build_view_3d_workflow(model_file, *, animation=False, width, height) -> dict` | Monta el visor 3D nativo `Load3D` (o `Load3DAdvanced` con `animation=True`) para VER un GLB/OBJ existente, orbitando con el ratón, sin ejecutar el grafo. `model_file` relativo a `input/3d/`. Cárgalo con `load_workflow_ui`. **Pura**. | | [comfyui_fetch_output_mesh_py_ml](../../python/functions/ml/comfyui_fetch_output_mesh.md) | `fetch_output_mesh(prompt_id, *, server, dest=None, timeout) -> dict` | Localiza la malla en `/history/{prompt_id}` (el SaveGLB la expone bajo la clave `"3d"`, no `"images"`) y la baja via GET `/view` a disco. Hermana de `fetch_output_image`. Impura. | @@ -107,6 +119,7 @@ report `0079`). | [comfyui_build_textured_3d_multiview_workflow_py_ml](../../python/functions/ml/comfyui_build_textured_3d_multiview_workflow.md) | `build_textured_3d_multiview_workflow(image_name, *, ckpt='hunyuan3d-dit-v2-mv.safetensors', views=6, octree=384, max_faces=50000, upscale_model='4x_foolhardy_Remacri.pth') -> dict` | Builder imagen→malla 3D **con textura PBR** vía el wrapper Hunyuan3DWrapper (kijai): 4/6 vistas + delight + sample multi-vista + upscale Remacri + bake sobre UV (19 nodos). Cobertura de atlas 32.93% (report 0082). **Pura**. En 8 GB ejecutar en 2 fases (shape→`/free`→paint). | | [comfyui_simplify_mesh_py_ml](../../python/functions/ml/comfyui_simplify_mesh.md) | `simplify_mesh(in_path, *, target_faces=80000, weld=True, out_path=None) -> dict` | **Post-proceso**: decima un GLB/OBJ/PLY denso (suelda cube-soup + quadric edge collapse de pymeshlab), conservando vertex colors o textura+UV. 964k→80k caras, 34.7→1.43 MB medido (report 0090). `weld=True` es clave: sin él la cube-soup de `VoxelToMeshBasic` no decima. Impura (trimesh+pymeshlab+scipy). | | [comfyui_make_watertight_py_ml](../../python/functions/ml/comfyui_make_watertight.md) | `make_watertight(in_path, *, method='voxel', pitch=None, out_path=None) -> dict` | **Post-proceso**: hace estanca una malla. `method='voxel'` (voxeliza+fill+marching cubes) garantiza `is_watertight=True` a costa de más caras y de descartar la apariencia; `method='repair'` (fill_holes+fix_normals) conserva detalle pero no garantiza estanqueidad. La vía de raíz es `VoxelToMesh surface net` (report 0088). Impura. | +| [comfyui_mesh_cleanup_oneshot_py_pipelines](../../python/functions/pipelines/comfyui_mesh_cleanup_oneshot.md) | `mesh_cleanup_oneshot(in_path, *, target_faces=80000, watertight=True, method='repair', out_path=None) -> dict` | **Pipeline** de limpieza en una llamada: `simplify_mesh` → (si `watertight`) `make_watertight`. Capitaliza el "80k caras + estanco" del report 0088. `method='voxel'` garantiza estanqueidad; `method='repair'` conserva caras. Reporta `{in_faces, simplified_faces, final_faces, is_watertight}`. Impuro. | ### Por la UI web (CDP) — dominio `browser` @@ -193,16 +206,17 @@ Para tunear nodo a nodo en vez del oneshot: `build_image_to_3d_workflow(image_na - **No es un grupo de generación genérica de imágenes**: cubre ComfyUI concretamente (su API y su frontend litegraph). Para otros backends (Automatic1111, diffusers) harían falta otras funciones. -- **Los builders cubren txt2img, img2img, upscale, LoRA stacks, inpaint, ControlNet, SDXL - refiner, vídeo (LTX/Wan) y 3D texturizado multi-vista** (`build_txt2img_workflow`, - `build_img2img_workflow`, `build_upscale_workflow`, `inject_lora`, `build_inpaint_workflow`, - `build_controlnet_workflow`, `build_sdxl_refiner_workflow`, `build_video_workflow`, - `build_textured_3d_multiview_workflow`). Workflows aún más complejos (multi-ControlNet avanzado, - IPAdapter) se montan en la UI a mano y se capturan con `export_workflow_ui`, o se importan de - internet con `import_workflow_json`/`import_workflow_png`, se resuelven sus dependencias con - `resolve_workflow_deps` (instala nodos con `install_custom_node`, descubre modelos con - `search_civitai_models`) y se validan con `validate_workflow` antes de encolar. -- **Los 11 builders puros tienen tests de estructura** (`python/functions/ml/tests/test_comfyui_build_*.py` +- **Los builders cubren txt2img, img2img, upscale (ESRGAN y hires-fix con re-difusión), LoRA + stacks, inpaint, ControlNet, SDXL refiner, FaceDetailer, vídeo (LTX/Wan) y 3D texturizado + multi-vista** (`build_txt2img_workflow`, `build_img2img_workflow`, `build_upscale_workflow`, + `build_hires_fix_workflow`, `inject_lora`, `build_inpaint_workflow`, `build_controlnet_workflow`, + `build_sdxl_refiner_workflow`, `build_facedetailer_workflow`, `build_video_workflow`, + `build_textured_3d_multiview_workflow`). Lo que aún NO tiene builder propio (IPAdapter, + multi-ControlNet avanzado) se monta en la UI a mano y se captura con `export_workflow_ui`, o se + importa de internet con `import_workflow_json`/`import_workflow_png`, se resuelven sus dependencias + con `resolve_workflow_deps` (instala nodos con `install_custom_node`, descubre modelos con + `search_civitai_models`) y se valida con `validate_workflow` antes de encolar. +- **Los 13 builders puros tienen tests de estructura** (`python/functions/ml/tests/test_comfyui_build_*.py` + `test_comfyui_inject_lora.py`): verifican los `class_type` esperados, que los parámetros se reflejan en los nodos, la validez de las conexiones `[node_id, output_index]` y la pureza de `inject_lora`. Son tests offline (no tocan GPU ni server); las funciones impuras del grupo (todo lo que habla con el server, @@ -221,6 +235,10 @@ Para tunear nodo a nodo en vez del oneshot: `build_image_to_3d_workflow(image_na CUDA + `ComfyUI_essentials` + el upscaler Remacri) y debe ejecutarse en 2 fases en 8 GB (shape→`/free`→paint). Detalle y cobertura medida en `reports/0082`; shape-only y comparación vs la app local en `reports/0069-2026-06-23-comfyui-img-to-3d.md`. +- **Estanqueidad de la malla**: el default de `build_image_to_3d_workflow` (`VoxelToMeshBasic`) da + malla NO estanca; con `watertight=True` (`VoxelToMesh surface-net`) sale estanca de raíz. Si ya + tienes el GLB en disco, `mesh_cleanup_oneshot` decima + cierra en una llamada (`method='voxel'` + garantiza `is_watertight=True`; `method='repair'` conserva caras sin garantía). Ver `reports/0088`. - La primitiva de transport CDP es [`cdp_eval`](../../python/functions/browser/cdp_eval.md) (grupo navegador): si necesitas leer/escribir algo del grafo que estas funciones no cubren, compón `cdp_eval` directamente antes de inventar nada. diff --git a/python/functions/ml/comfyui_build_facedetailer_workflow.md b/python/functions/ml/comfyui_build_facedetailer_workflow.md new file mode 100644 index 00000000..f9f1b3bf --- /dev/null +++ b/python/functions/ml/comfyui_build_facedetailer_workflow.md @@ -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. diff --git a/python/functions/ml/comfyui_build_facedetailer_workflow.py b/python/functions/ml/comfyui_build_facedetailer_workflow.py new file mode 100644 index 00000000..dd757b28 --- /dev/null +++ b/python/functions/ml/comfyui_build_facedetailer_workflow.py @@ -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)) diff --git a/python/functions/ml/comfyui_build_hires_fix_workflow.md b/python/functions/ml/comfyui_build_hires_fix_workflow.md new file mode 100644 index 00000000..9dc4853b --- /dev/null +++ b/python/functions/ml/comfyui_build_hires_fix_workflow.md @@ -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`. diff --git a/python/functions/ml/comfyui_build_hires_fix_workflow.py b/python/functions/ml/comfyui_build_hires_fix_workflow.py new file mode 100644 index 00000000..26b63eae --- /dev/null +++ b/python/functions/ml/comfyui_build_hires_fix_workflow.py @@ -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)) diff --git a/python/functions/ml/comfyui_build_image_to_3d_workflow.md b/python/functions/ml/comfyui_build_image_to_3d_workflow.md index d8c16823..1340347d 100644 --- a/python/functions/ml/comfyui_build_image_to_3d_workflow.md +++ b/python/functions/ml/comfyui_build_image_to_3d_workflow.md @@ -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. diff --git a/python/functions/ml/comfyui_build_image_to_3d_workflow.py b/python/functions/ml/comfyui_build_image_to_3d_workflow.py index db05c204..a0964029 100644 --- a/python/functions/ml/comfyui_build_image_to_3d_workflow.py +++ b/python/functions/ml/comfyui_build_image_to_3d_workflow.py @@ -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}, diff --git a/python/functions/ml/tests/test_comfyui_build_facedetailer_workflow.py b/python/functions/ml/tests/test_comfyui_build_facedetailer_workflow.py new file mode 100644 index 00000000..25b20fbe --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_build_facedetailer_workflow.py @@ -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", + ) diff --git a/python/functions/ml/tests/test_comfyui_build_hires_fix_workflow.py b/python/functions/ml/tests/test_comfyui_build_hires_fix_workflow.py new file mode 100644 index 00000000..3cb61835 --- /dev/null +++ b/python/functions/ml/tests/test_comfyui_build_hires_fix_workflow.py @@ -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] diff --git a/python/functions/ml/tests/test_comfyui_build_image_to_3d_workflow.py b/python/functions/ml/tests/test_comfyui_build_image_to_3d_workflow.py index 6cf4867d..541074f7 100644 --- a/python/functions/ml/tests/test_comfyui_build_image_to_3d_workflow.py +++ b/python/functions/ml/tests/test_comfyui_build_image_to_3d_workflow.py @@ -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] diff --git a/python/functions/pipelines/comfyui_mesh_cleanup_oneshot.md b/python/functions/pipelines/comfyui_mesh_cleanup_oneshot.md new file mode 100644 index 00000000..0fc2a942 --- /dev/null +++ b/python/functions/pipelines/comfyui_mesh_cleanup_oneshot.md @@ -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 ('_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 `_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). diff --git a/python/functions/pipelines/comfyui_mesh_cleanup_oneshot.py b/python/functions/pipelines/comfyui_mesh_cleanup_oneshot.py new file mode 100644 index 00000000..5020f8db --- /dev/null +++ b/python/functions/pipelines/comfyui_mesh_cleanup_oneshot.py @@ -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 + ("_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)) diff --git a/python/functions/pipelines/comfyui_txt2img_oneshot.md b/python/functions/pipelines/comfyui_txt2img_oneshot.md new file mode 100644 index 00000000..45c7da4a --- /dev/null +++ b/python/functions/pipelines/comfyui_txt2img_oneshot.md @@ -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 (`_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). diff --git a/python/functions/pipelines/comfyui_txt2img_oneshot.py b/python/functions/pipelines/comfyui_txt2img_oneshot.py new file mode 100644 index 00000000..ffb0bc7e --- /dev/null +++ b/python/functions/pipelines/comfyui_txt2img_oneshot.py @@ -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))