chore: auto-commit (61 archivos)
- docs/capabilities/INDEX.md - docs/capabilities/comfyui.md - python/functions/browser/comfyui_export_workflow_ui.md - python/functions/browser/comfyui_export_workflow_ui.py - python/functions/browser/comfyui_load_workflow_ui.md - python/functions/browser/comfyui_load_workflow_ui.py - python/functions/browser/comfyui_queue_prompt_ui.md - python/functions/browser/comfyui_queue_prompt_ui.py - python/functions/browser/comfyui_refresh_nodes_ui.md - python/functions/browser/comfyui_refresh_nodes_ui.py - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: comfyui_build_controlnet_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_build_controlnet_workflow(ckpt_name: str, control_image: str, cn_name: str, positive: str, negative: str = \"\", *, strength: float = 1.0, steps: int = 20, cfg: float = 7.0, seed: int = 0, width: int = 512, height: int = 512) -> dict"
|
||||
description: "Construye el dict de un workflow ComfyUI txt2img guiado por ControlNet en API format: CheckpointLoaderSimple + EmptyLatentImage + LoadImage (mapa de control) + ControlNetLoader -> ControlNetApply (inyecta el control sobre el condicionamiento positivo) -> KSampler -> VAEDecode -> SaveImage. Pura, sin red ni I/O. Hermana de comfyui_build_txt2img_workflow."
|
||||
tags: [comfyui, ml, image-generation, controlnet, stable-diffusion, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: ckpt_name
|
||||
desc: "Nombre del checkpoint tal como lo ve el servidor ComfyUI (ej. 'dreamshaper_8.safetensors'). Debe estar en la lista de CheckpointLoaderSimple de comfyui_object_info."
|
||||
- name: control_image
|
||||
desc: "Nombre del archivo de la imagen de control dentro de input/ del servidor (mapa canny/depth/openpose preprocesado); lo carga el nodo LoadImage."
|
||||
- name: cn_name
|
||||
desc: "Nombre del modelo ControlNet en models/controlnet/ tal como lo lista comfyui_object_info para ControlNetLoader (control_net_name)."
|
||||
- name: positive
|
||||
desc: "Prompt positivo: lo que se quiere ver en la imagen."
|
||||
- name: negative
|
||||
desc: "Prompt negativo: lo que se quiere evitar. Por defecto cadena vacia."
|
||||
- name: strength
|
||||
desc: "Fuerza con la que el ControlNet condiciona la generacion (0.0 = nula, 1.0 = plena). keyword-only."
|
||||
- name: steps
|
||||
desc: "Pasos de sampling del KSampler. keyword-only."
|
||||
- name: cfg
|
||||
desc: "Classifier-free guidance scale. keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla del KSampler. 0 es determinista; cambiar para variar. keyword-only."
|
||||
- name: width
|
||||
desc: "Ancho del latente/imagen en px (multiplo de 8). keyword-only."
|
||||
- name: height
|
||||
desc: "Alto del latente/imagen en px (multiplo de 8). keyword-only."
|
||||
output: "dict en API format con node_ids como claves (CheckpointLoaderSimple '4', EmptyLatentImage '5', LoadImage '10', ControlNetLoader '12', CLIPTextEncode '6'/'7', ControlNetApply '13', KSampler '3', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_build_controlnet_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_controlnet_workflow import comfyui_build_controlnet_workflow
|
||||
|
||||
wf = comfyui_build_controlnet_workflow(
|
||||
ckpt_name="dreamshaper_8.safetensors",
|
||||
control_image="pose_canny.png", # mapa de control en input/
|
||||
cn_name="control_v11p_sd15_canny.pth", # modelo en models/controlnet/
|
||||
positive="a knight in shining armor, dramatic lighting",
|
||||
negative="blurry, low quality",
|
||||
strength=0.8,
|
||||
seed=42,
|
||||
)
|
||||
# wf["13"]["class_type"] == "ControlNetApply"
|
||||
# wf["13"]["inputs"]["conditioning"] == ["6", 0] # aplica sobre el positivo
|
||||
# wf["3"]["inputs"]["positive"] == ["13", 0] # KSampler usa el cond condicionado
|
||||
```
|
||||
|
||||
El bloque se lanza con el python del venv. `./fn run` directo no aplica (firma con
|
||||
`*` keyword-only); usa el import o un heredoc.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras controlar la composicion de la imagen con una guia estructural
|
||||
(bordes canny, profundidad depth, pose openpose, scribble) en lugar de dejar la
|
||||
composicion al azar del prompt. Necesitas el mapa de control ya preprocesado en
|
||||
`input/` y el modelo ControlNet adecuado descargado en `models/controlnet/`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Es API format (nodos numerados), NO el formato de la UI de ComfyUI.
|
||||
- `control_image` debe ser el mapa de control YA preprocesado (ej. salida de un
|
||||
preprocesador canny/depth). Este builder NO incluye el nodo preprocesador; si
|
||||
pasas una foto normal, el ControlNet la usara tal cual.
|
||||
- Usa el nodo clasico `ControlNetApply` (un solo `strength`). Para ControlNet
|
||||
avanzado con `start_percent`/`end_percent` necesitas `ControlNetApplyAdvanced`
|
||||
(no cubierto aqui): montalo en la UI y captura con `comfyui_export_workflow_ui`.
|
||||
- `cn_name` debe corresponder a la version del checkpoint (un ControlNet de SD1.5
|
||||
no sirve con un checkpoint SDXL). Valida antes con `comfyui_validate_workflow`.
|
||||
- Es pura: NO valida que los modelos existan en el servidor. Valida antes.
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Construye un workflow ComfyUI con ControlNet en API format (nodos numerados).
|
||||
|
||||
ControlNet condiciona la generacion con una imagen de control (canny, depth,
|
||||
pose, scribble, ...). Cadena de nodos: CheckpointLoaderSimple + EmptyLatentImage
|
||||
+ LoadImage (imagen de control) + ControlNetLoader -> ControlNetApply (inyecta
|
||||
el control sobre el condicionamiento positivo) -> KSampler -> VAEDecode ->
|
||||
SaveImage. Los CLIPTextEncode codifican el prompt positivo y el negativo.
|
||||
|
||||
API format: cada clave es un node_id (string); cada nodo tiene class_type +
|
||||
inputs. Las conexiones entre nodos son listas [node_id, output_index]. Es el
|
||||
formato que acepta POST /prompt, distinto del formato de la UI (graph con links).
|
||||
|
||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
|
||||
"""
|
||||
|
||||
|
||||
def comfyui_build_controlnet_workflow(
|
||||
ckpt_name: str,
|
||||
control_image: str,
|
||||
cn_name: str,
|
||||
positive: str,
|
||||
negative: str = "",
|
||||
*,
|
||||
strength: float = 1.0,
|
||||
steps: int = 20,
|
||||
cfg: float = 7.0,
|
||||
seed: int = 0,
|
||||
width: int = 512,
|
||||
height: int = 512,
|
||||
) -> dict:
|
||||
"""Construye el dict de un workflow txt2img guiado por ControlNet.
|
||||
|
||||
Args:
|
||||
ckpt_name: nombre del checkpoint tal como lo ve el servidor ComfyUI
|
||||
(ej. "dreamshaper_8.safetensors"). Debe estar entre los que devuelve
|
||||
comfyui_object_info para CheckpointLoaderSimple.
|
||||
control_image: nombre del archivo de la imagen de control dentro de la
|
||||
carpeta input/ del servidor ComfyUI (lo carga el nodo LoadImage).
|
||||
Suele ser un mapa preprocesado (canny/depth/openpose).
|
||||
cn_name: nombre del modelo ControlNet en models/controlnet/ tal como lo
|
||||
lista comfyui_object_info para ControlNetLoader (control_net_name).
|
||||
positive: prompt positivo (lo que se quiere ver en la imagen).
|
||||
negative: prompt negativo (lo que se quiere evitar). Por defecto "".
|
||||
strength: fuerza con la que el ControlNet condiciona la generacion
|
||||
(0.0 = nula, 1.0 = plena). keyword-only.
|
||||
steps: pasos de sampling del KSampler. keyword-only.
|
||||
cfg: classifier-free guidance scale. keyword-only.
|
||||
seed: semilla del KSampler. 0 es determinista; cambiar para variar.
|
||||
keyword-only.
|
||||
width: ancho del latente/imagen en px (multiplo de 8). keyword-only.
|
||||
height: alto del latente/imagen en px (multiplo de 8). keyword-only.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow. Las claves son
|
||||
node_ids y cada valor tiene class_type + inputs.
|
||||
"""
|
||||
return {
|
||||
"4": {
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": ckpt_name},
|
||||
},
|
||||
"5": {
|
||||
"class_type": "EmptyLatentImage",
|
||||
"inputs": {"width": width, "height": height, "batch_size": 1},
|
||||
},
|
||||
"10": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": control_image},
|
||||
},
|
||||
"12": {
|
||||
"class_type": "ControlNetLoader",
|
||||
"inputs": {"control_net_name": cn_name},
|
||||
},
|
||||
"6": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": positive, "clip": ["4", 1]},
|
||||
},
|
||||
"7": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": negative, "clip": ["4", 1]},
|
||||
},
|
||||
"13": {
|
||||
"class_type": "ControlNetApply",
|
||||
"inputs": {
|
||||
"conditioning": ["6", 0],
|
||||
"control_net": ["12", 0],
|
||||
"image": ["10", 0],
|
||||
"strength": strength,
|
||||
},
|
||||
},
|
||||
"3": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"seed": seed,
|
||||
"steps": steps,
|
||||
"cfg": cfg,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "normal",
|
||||
"denoise": 1.0,
|
||||
"model": ["4", 0],
|
||||
"positive": ["13", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["5", 0],
|
||||
},
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {"samples": ["3", 0], "vae": ["4", 2]},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": "comfy_controlnet", "images": ["8", 0]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
wf = comfyui_build_controlnet_workflow(
|
||||
ckpt_name="dreamshaper_8.safetensors",
|
||||
control_image="pose_canny.png",
|
||||
cn_name="control_v11p_sd15_canny.pth",
|
||||
positive="a knight in shining armor, dramatic lighting",
|
||||
negative="blurry, low quality",
|
||||
strength=0.8,
|
||||
seed=42,
|
||||
)
|
||||
print(json.dumps(wf, indent=2))
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: comfyui_build_image_to_3d_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.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."
|
||||
tags: [comfyui, ml, img-to-3d, hunyuan3d, mesh, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: image_name
|
||||
desc: "Nombre del archivo de imagen en el input/ del servidor ComfyUI (ej. '3d_src_robot_00001_.png'). Lo carga el nodo LoadImage; debe existir ya en input/ (subelo antes o usa el pipeline oneshot)."
|
||||
- name: ckpt_name
|
||||
desc: "Nombre del checkpoint Hunyuan3D-2 tal como lo ve el servidor (ej. 'hunyuan3d-dit-v2-mini.safetensors'). Debe estar en la lista de comfyui_object_info para ImageOnlyCheckpointLoader."
|
||||
- name: resolution
|
||||
desc: "Resolucion del latente 3D (EmptyLatentHunyuan3Dv2). Mayor = mas detalle de forma y mas VRAM. keyword-only."
|
||||
- name: steps
|
||||
desc: "Pasos de sampling del KSampler de difusion 3D. keyword-only."
|
||||
- name: cfg
|
||||
desc: "Classifier-free guidance scale del KSampler. keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla del KSampler. 0 es determinista; cambiar para variar la malla. keyword-only."
|
||||
- name: octree_resolution
|
||||
desc: "Resolucion del grid de voxels en VAEDecodeHunyuan3D. Mayor = malla mas densa (mas caras) y mas memoria. keyword-only."
|
||||
- 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."
|
||||
- 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."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_build_image_to_3d_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_image_to_3d_workflow import comfyui_build_image_to_3d_workflow
|
||||
|
||||
wf = comfyui_build_image_to_3d_workflow(
|
||||
image_name="3d_src_robot_00001_.png",
|
||||
ckpt_name="hunyuan3d-dit-v2-mini.safetensors",
|
||||
seed=42,
|
||||
)
|
||||
# wf["2"]["class_type"] == "ImageOnlyCheckpointLoader"
|
||||
# wf["3"]["inputs"]["clip_vision"] == ["2", 1] # CLIP_VISION del loader
|
||||
# wf["7"]["class_type"] == "VAEDecodeHunyuan3D"
|
||||
# wf["9"]["class_type"] == "SaveGLB"
|
||||
```
|
||||
|
||||
O lanzable directo con: `./fn run comfyui_build_image_to_3d_workflow` (imprime el JSON del workflow de ejemplo).
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Antes de enviar una reconstruccion imagen->3D a ComfyUI: construye aqui el dict
|
||||
del workflow y pasalo a `comfyui_submit_workflow`. Usala siempre que tengas una
|
||||
imagen ya en el `input/` del servidor y quieras una malla GLB sin escribir el
|
||||
grafo de 9 nodos a mano. Para hacerlo end-to-end desde una imagen en disco (subir
|
||||
+ build + submit + wait + fetch en una llamada), usa el pipeline
|
||||
`comfyui_image_to_3d_oneshot`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Es API format (nodos numerados), NO el formato de la UI de ComfyUI (graph con
|
||||
links). No se pega en la UI tal cual; es el formato que acepta POST /prompt.
|
||||
- Usa nodos NATIVOS de Hunyuan3D-2 de ComfyUI >= 0.26.0. En versiones anteriores
|
||||
(sin `ImageOnlyCheckpointLoader`/`VAEDecodeHunyuan3D`/`SaveGLB`) el server
|
||||
rechaza el workflow al enviarlo. Esta funcion es pura y no valida contra el
|
||||
server: valida con `comfyui_validate_workflow` antes de encolar si dudas.
|
||||
- `image_name` debe existir en el `input/` del servidor ANTES de enviar. Esta
|
||||
funcion solo referencia el nombre; no sube nada (es pura). El pipeline oneshot
|
||||
hace el upload.
|
||||
- `ckpt_name` debe coincidir EXACTAMENTE con un checkpoint visible para el
|
||||
servidor (instalalo con `comfyui_install_3d_model`).
|
||||
- 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.
|
||||
- `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.
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Construye un workflow ComfyUI imagen -> malla 3D en "API format" (Hunyuan3D-2 nativo).
|
||||
|
||||
API format: cada clave es un node_id (string); cada nodo tiene class_type +
|
||||
inputs. Las conexiones entre nodos son listas [node_id, output_index]. Es el
|
||||
formato que acepta POST /prompt, distinto del formato de la UI (graph con links).
|
||||
|
||||
El workflow usa los nodos NATIVOS de Hunyuan3D-2 que trae ComfyUI 0.26.0 (sin
|
||||
custom node de terceros): una imagen de entrada se reconstruye en una malla 3D
|
||||
GLB. Cadena de 9 nodos:
|
||||
|
||||
LoadImage -> ImageOnlyCheckpointLoader -> CLIPVisionEncode ->
|
||||
Hunyuan3Dv2Conditioning -> EmptyLatentHunyuan3Dv2 -> KSampler ->
|
||||
VAEDecodeHunyuan3D -> VoxelToMeshBasic -> SaveGLB
|
||||
|
||||
El checkpoint Hunyuan3D-2 (mini/standard) es self-contained: ImageOnlyCheckpointLoader
|
||||
devuelve MODEL, CLIP_VISION y VAE de un solo .safetensors.
|
||||
|
||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
|
||||
"""
|
||||
|
||||
|
||||
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:
|
||||
"""Construye el dict del workflow imagen->3D nativo (Hunyuan3D-2).
|
||||
|
||||
Args:
|
||||
image_name: nombre del archivo de imagen en el `input/` del servidor
|
||||
ComfyUI (ej. "3d_src_robot_00001_.png"). Lo carga el nodo LoadImage;
|
||||
debe existir ya en input/ (subelo antes, o usa el pipeline oneshot).
|
||||
ckpt_name: nombre del checkpoint Hunyuan3D-2 tal como lo ve el servidor
|
||||
(ej. "hunyuan3d-dit-v2-mini.safetensors"). Debe estar entre los que
|
||||
devuelve comfyui_object_info para ImageOnlyCheckpointLoader.
|
||||
resolution: resolucion del latente 3D (EmptyLatentHunyuan3Dv2). Mayor =
|
||||
mas detalle de forma y mas VRAM. keyword-only.
|
||||
steps: pasos de sampling del KSampler de difusion 3D. keyword-only.
|
||||
cfg: classifier-free guidance scale del KSampler. keyword-only.
|
||||
seed: semilla del KSampler (0 = determinista; cambia para variar la
|
||||
malla). keyword-only.
|
||||
octree_resolution: resolucion del grid de voxels en VAEDecodeHunyuan3D.
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
return {
|
||||
"1": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": image_name},
|
||||
},
|
||||
"2": {
|
||||
"class_type": "ImageOnlyCheckpointLoader",
|
||||
"inputs": {"ckpt_name": ckpt_name},
|
||||
},
|
||||
"3": {
|
||||
"class_type": "CLIPVisionEncode",
|
||||
"inputs": {
|
||||
"clip_vision": ["2", 1],
|
||||
"image": ["1", 0],
|
||||
"crop": "center",
|
||||
},
|
||||
},
|
||||
"4": {
|
||||
"class_type": "Hunyuan3Dv2Conditioning",
|
||||
"inputs": {"clip_vision_output": ["3", 0]},
|
||||
},
|
||||
"5": {
|
||||
"class_type": "EmptyLatentHunyuan3Dv2",
|
||||
"inputs": {"resolution": resolution, "batch_size": 1},
|
||||
},
|
||||
"6": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"seed": seed,
|
||||
"steps": steps,
|
||||
"cfg": cfg,
|
||||
"sampler_name": sampler_name,
|
||||
"scheduler": scheduler,
|
||||
"denoise": 1.0,
|
||||
"model": ["2", 0],
|
||||
"positive": ["4", 0],
|
||||
"negative": ["4", 1],
|
||||
"latent_image": ["5", 0],
|
||||
},
|
||||
},
|
||||
"7": {
|
||||
"class_type": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
"samples": ["6", 0],
|
||||
"vae": ["2", 2],
|
||||
"num_chunks": num_chunks,
|
||||
"octree_resolution": octree_resolution,
|
||||
},
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VoxelToMeshBasic",
|
||||
"inputs": {"voxel": ["7", 0], "threshold": threshold},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveGLB",
|
||||
"inputs": {"mesh": ["8", 0], "filename_prefix": filename_prefix},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
wf = comfyui_build_image_to_3d_workflow(
|
||||
image_name="3d_src_robot_00001_.png",
|
||||
ckpt_name="hunyuan3d-dit-v2-mini.safetensors",
|
||||
seed=42,
|
||||
)
|
||||
print(json.dumps(wf, indent=2))
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: comfyui_build_img2img_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_build_img2img_workflow(ckpt_name: str, init_image: str, positive: str, negative: str = \"\", *, denoise: float = 0.6, steps: int = 20, cfg: float = 7.0, seed: int = 0, sampler_name: str = \"euler\", scheduler: str = \"normal\") -> dict"
|
||||
description: "Construye el dict de un workflow ComfyUI img2img en API format para SD1.5/SDXL: CheckpointLoaderSimple + LoadImage -> VAEEncode -> KSampler (con denoise < 1.0 para conservar la imagen base) -> VAEDecode -> SaveImage. Pura, sin red ni I/O. Hermana de comfyui_build_txt2img_workflow."
|
||||
tags: [comfyui, ml, image-generation, img2img, stable-diffusion, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: ckpt_name
|
||||
desc: "Nombre del checkpoint tal como lo ve el servidor ComfyUI (ej. 'dreamshaper_8.safetensors'). Debe estar en la lista de CheckpointLoaderSimple de comfyui_object_info."
|
||||
- name: init_image
|
||||
desc: "Nombre del archivo de imagen base dentro de la carpeta input/ del servidor ComfyUI; lo carga el nodo LoadImage."
|
||||
- name: positive
|
||||
desc: "Prompt positivo: lo que se quiere ver en la imagen."
|
||||
- name: negative
|
||||
desc: "Prompt negativo: lo que se quiere evitar. Por defecto cadena vacia."
|
||||
- name: denoise
|
||||
desc: "Fuerza de denoising del KSampler (0.0 = identica a la base, 1.0 = ignora la base). Tipico 0.4-0.7 para img2img. keyword-only."
|
||||
- name: steps
|
||||
desc: "Pasos de sampling del KSampler. keyword-only."
|
||||
- name: cfg
|
||||
desc: "Classifier-free guidance scale. keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla del KSampler. 0 es determinista; cambiar para variar. keyword-only."
|
||||
- name: sampler_name
|
||||
desc: "Nombre del sampler (ej. 'euler', 'dpmpp_2m'). keyword-only."
|
||||
- name: scheduler
|
||||
desc: "Scheduler del sampler (ej. 'normal', 'karras'). keyword-only."
|
||||
output: "dict en API format con node_ids como claves (CheckpointLoaderSimple '4', LoadImage '10', VAEEncode '11', CLIPTextEncode '6'/'7', KSampler '3', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_build_img2img_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_img2img_workflow import comfyui_build_img2img_workflow
|
||||
|
||||
wf = comfyui_build_img2img_workflow(
|
||||
ckpt_name="dreamshaper_8.safetensors",
|
||||
init_image="cabin.png", # archivo en el input/ de ComfyUI
|
||||
positive="a cozy cabin in the woods, golden hour",
|
||||
negative="blurry, low quality",
|
||||
denoise=0.55, # conserva ~la mitad de la imagen base
|
||||
seed=42,
|
||||
)
|
||||
# wf["11"]["class_type"] == "VAEEncode"
|
||||
# wf["3"]["inputs"]["latent_image"] == ["11", 0] # KSampler parte del latente de la imagen
|
||||
# wf["3"]["inputs"]["denoise"] == 0.55
|
||||
```
|
||||
|
||||
El bloque de arriba se lanza con el python del venv (`python/.venv/bin/python3`). Nota: `./fn run` directo no aplica a este builder porque su firma usa `*` (keyword-only) y el generador de runner de `fn run` no lo soporta — igual que en `comfyui_build_txt2img_workflow`. Usa el import de arriba o un heredoc.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras transformar una imagen existente con un prompt (variaciones,
|
||||
restyling, refine) en lugar de generar desde ruido. Sube primero la imagen base
|
||||
al `input/` del servidor (o cargala por la UI) y pasa su nombre en `init_image`.
|
||||
Para generar desde cero usa `comfyui_build_txt2img_workflow`; para ampliar una
|
||||
imagen usa `comfyui_build_upscale_workflow`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Es API format (nodos numerados), NO el formato de la UI de ComfyUI. Es lo que
|
||||
acepta POST /prompt.
|
||||
- `init_image` debe existir en la carpeta `input/` del servidor (no es un path
|
||||
local arbitrario). Subela antes con la UI o copiala a `~/ComfyUI/input/`.
|
||||
- `denoise` controla cuanto se conserva de la base: cerca de 1.0 ignora la
|
||||
imagen (casi txt2img); cerca de 0.0 apenas la cambia. 0.4-0.7 es el rango util.
|
||||
- Asume que el checkpoint trae VAE embebido (VAEEncode/VAEDecode usan `["4", 2]`).
|
||||
Para un VAE externo cambia esas conexiones.
|
||||
- Es pura: NO valida que `ckpt_name`/`init_image` existan en el servidor. Si no
|
||||
existen, ComfyUI rechaza el workflow con HTTP 400 al enviarlo. Valida antes con
|
||||
`comfyui_validate_workflow`.
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Construye un workflow ComfyUI img2img en API format (dict de nodos numerados).
|
||||
|
||||
API format: cada clave es un node_id (string); cada nodo tiene class_type +
|
||||
inputs. Las conexiones entre nodos son listas [node_id, output_index]. Es el
|
||||
formato que acepta POST /prompt, distinto del formato de la UI (graph con links).
|
||||
|
||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
|
||||
"""
|
||||
|
||||
|
||||
def comfyui_build_img2img_workflow(
|
||||
ckpt_name: str,
|
||||
init_image: str,
|
||||
positive: str,
|
||||
negative: str = "",
|
||||
*,
|
||||
denoise: float = 0.6,
|
||||
steps: int = 20,
|
||||
cfg: float = 7.0,
|
||||
seed: int = 0,
|
||||
sampler_name: str = "euler",
|
||||
scheduler: str = "normal",
|
||||
) -> dict:
|
||||
"""Construye el dict de un workflow img2img para SD1.5 / SDXL.
|
||||
|
||||
Cadena de nodos: CheckpointLoaderSimple + LoadImage -> VAEEncode ->
|
||||
KSampler (con denoise < 1.0 para conservar la imagen base) -> VAEDecode ->
|
||||
SaveImage. CLIPTextEncode codifica el prompt positivo y el negativo.
|
||||
|
||||
Args:
|
||||
ckpt_name: nombre del checkpoint tal como lo ve el servidor ComfyUI
|
||||
(ej. "dreamshaper_8.safetensors"). Debe estar entre los que devuelve
|
||||
comfyui_object_info para CheckpointLoaderSimple.
|
||||
init_image: nombre del archivo de imagen base dentro de la carpeta
|
||||
input/ del servidor ComfyUI (lo que carga el nodo LoadImage).
|
||||
positive: prompt positivo (lo que se quiere ver en la imagen).
|
||||
negative: prompt negativo (lo que se quiere evitar). Por defecto "".
|
||||
denoise: fuerza de denoising del KSampler (0.0 = identica a la base,
|
||||
1.0 = ignora la base). Tipico 0.4-0.7 para img2img. keyword-only.
|
||||
steps: pasos de sampling del KSampler. keyword-only.
|
||||
cfg: classifier-free guidance scale. keyword-only.
|
||||
seed: semilla del KSampler. keyword-only.
|
||||
sampler_name: nombre del sampler (ej. "euler", "dpmpp_2m"). keyword-only.
|
||||
scheduler: scheduler del sampler (ej. "normal", "karras"). keyword-only.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow. Las claves son
|
||||
node_ids y cada valor tiene class_type + inputs.
|
||||
"""
|
||||
return {
|
||||
"4": {
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": ckpt_name},
|
||||
},
|
||||
"10": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": init_image},
|
||||
},
|
||||
"11": {
|
||||
"class_type": "VAEEncode",
|
||||
"inputs": {"pixels": ["10", 0], "vae": ["4", 2]},
|
||||
},
|
||||
"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": denoise,
|
||||
"model": ["4", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["11", 0],
|
||||
},
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {"samples": ["3", 0], "vae": ["4", 2]},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": "comfy_img2img", "images": ["8", 0]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
wf = comfyui_build_img2img_workflow(
|
||||
ckpt_name="dreamshaper_8.safetensors",
|
||||
init_image="example.png",
|
||||
positive="a cozy cabin in the woods, golden hour, sharp focus",
|
||||
negative="blurry, low quality",
|
||||
denoise=0.6,
|
||||
seed=42,
|
||||
)
|
||||
print(json.dumps(wf, indent=2))
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: comfyui_build_inpaint_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_build_inpaint_workflow(ckpt_name: str, image: str, mask: str, positive: str, negative: str = \"\", *, denoise: float = 1.0, steps: int = 20, cfg: float = 7.0, seed: int = 0, sampler_name: str = \"euler\", scheduler: str = \"normal\") -> dict"
|
||||
description: "Construye el dict de un workflow ComfyUI inpaint en API format para SD1.5/SDXL: CheckpointLoaderSimple + LoadImage (base) + LoadImageMask (mascara) -> VAEEncodeForInpaint -> KSampler -> VAEDecode -> SaveImage. Regenera solo la zona enmascarada conservando el resto. Pura, sin red ni I/O. Hermana de comfyui_build_txt2img_workflow."
|
||||
tags: [comfyui, ml, image-generation, inpaint, stable-diffusion, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: ckpt_name
|
||||
desc: "Nombre del checkpoint tal como lo ve el servidor ComfyUI (ej. 'dreamshaper_8.safetensors'). Debe estar en la lista de CheckpointLoaderSimple de comfyui_object_info."
|
||||
- name: image
|
||||
desc: "Nombre del archivo de la imagen base dentro de la carpeta input/ del servidor ComfyUI; lo carga el nodo LoadImage."
|
||||
- name: mask
|
||||
desc: "Nombre del archivo de la mascara dentro de input/ del servidor; lo carga LoadImageMask. Las zonas blancas se regeneran."
|
||||
- name: positive
|
||||
desc: "Prompt positivo: lo que se quiere ver en la zona enmascarada."
|
||||
- name: negative
|
||||
desc: "Prompt negativo: lo que se quiere evitar. Por defecto cadena vacia."
|
||||
- name: denoise
|
||||
desc: "Fuerza de denoising del KSampler (1.0 regenera por completo la zona enmascarada; <1.0 conserva parte de la base). keyword-only."
|
||||
- name: steps
|
||||
desc: "Pasos de sampling del KSampler. keyword-only."
|
||||
- name: cfg
|
||||
desc: "Classifier-free guidance scale. keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla del KSampler. 0 es determinista; cambiar para variar. keyword-only."
|
||||
- name: sampler_name
|
||||
desc: "Nombre del sampler (ej. 'euler', 'dpmpp_2m'). keyword-only."
|
||||
- name: scheduler
|
||||
desc: "Scheduler del sampler (ej. 'normal', 'karras'). keyword-only."
|
||||
output: "dict en API format con node_ids como claves (CheckpointLoaderSimple '4', LoadImage '10', LoadImageMask '12', VAEEncodeForInpaint '11', CLIPTextEncode '6'/'7', KSampler '3', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_build_inpaint_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_inpaint_workflow import comfyui_build_inpaint_workflow
|
||||
|
||||
wf = comfyui_build_inpaint_workflow(
|
||||
ckpt_name="dreamshaper_8.safetensors",
|
||||
image="room.png", # imagen base en el input/ de ComfyUI
|
||||
mask="room_mask.png", # mascara: blanco = zona a regenerar
|
||||
positive="a vase of red flowers on the table, sharp focus",
|
||||
negative="blurry, low quality",
|
||||
denoise=1.0,
|
||||
seed=42,
|
||||
)
|
||||
# wf["11"]["class_type"] == "VAEEncodeForInpaint"
|
||||
# wf["11"]["inputs"]["mask"] == ["12", 0] # mascara desde LoadImageMask
|
||||
# wf["3"]["inputs"]["latent_image"] == ["11", 0] # KSampler parte del latente inpaint
|
||||
```
|
||||
|
||||
El bloque se lanza con el python del venv (`python/.venv/bin/python3`). `./fn run`
|
||||
directo no aplica a este builder porque su firma usa `*` (keyword-only); usa el
|
||||
import de arriba o un heredoc.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras reemplazar solo una parte de una imagen (quitar un objeto, cambiar
|
||||
un detalle, rellenar una zona) conservando el resto intacto. Sube la imagen base
|
||||
y la mascara al `input/` del servidor y pasa sus nombres. Para transformar la
|
||||
imagen entera usa `comfyui_build_img2img_workflow`; para generar desde cero
|
||||
`comfyui_build_txt2img_workflow`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Es API format (nodos numerados), NO el formato de la UI de ComfyUI. Es lo que
|
||||
acepta POST /prompt.
|
||||
- `image` y `mask` deben existir en la carpeta `input/` del servidor (no son
|
||||
paths locales arbitrarios). Subelos antes con la UI o copialos a `~/ComfyUI/input/`.
|
||||
- `LoadImageMask` lee el canal `red` por defecto: la mascara debe tener la zona a
|
||||
regenerar en blanco. Si tu mascara usa el canal alpha, cambia `channel` en el
|
||||
nodo '12' tras construir.
|
||||
- `VAEEncodeForInpaint` usa `grow_mask_by: 6` (suaviza el borde de la mascara).
|
||||
Ajustalo en el nodo '11' si necesitas un borde mas duro o mas difuso.
|
||||
- Asume que el checkpoint trae VAE embebido (VAEEncodeForInpaint/VAEDecode usan
|
||||
`["4", 2]`). Para un VAE externo cambia esas conexiones.
|
||||
- Es pura: NO valida que `ckpt_name`/`image`/`mask` existan en el servidor.
|
||||
Valida antes con `comfyui_validate_workflow`.
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Construye un workflow ComfyUI inpaint en API format (dict de nodos numerados).
|
||||
|
||||
Inpaint: se reemplaza la zona enmascarada de una imagen conservando el resto.
|
||||
Cadena de nodos: CheckpointLoaderSimple + LoadImage (imagen base) +
|
||||
LoadImageMask (mascara) -> VAEEncodeForInpaint (codifica el latente respetando
|
||||
la mascara) -> KSampler -> VAEDecode -> SaveImage. Los CLIPTextEncode codifican
|
||||
el prompt positivo y el negativo.
|
||||
|
||||
API format: cada clave es un node_id (string); cada nodo tiene class_type +
|
||||
inputs. Las conexiones entre nodos son listas [node_id, output_index]. Es el
|
||||
formato que acepta POST /prompt, distinto del formato de la UI (graph con links).
|
||||
|
||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
|
||||
"""
|
||||
|
||||
|
||||
def comfyui_build_inpaint_workflow(
|
||||
ckpt_name: str,
|
||||
image: str,
|
||||
mask: str,
|
||||
positive: str,
|
||||
negative: str = "",
|
||||
*,
|
||||
denoise: float = 1.0,
|
||||
steps: int = 20,
|
||||
cfg: float = 7.0,
|
||||
seed: int = 0,
|
||||
sampler_name: str = "euler",
|
||||
scheduler: str = "normal",
|
||||
) -> dict:
|
||||
"""Construye el dict de un workflow inpaint para SD1.5 / SDXL.
|
||||
|
||||
Args:
|
||||
ckpt_name: nombre del checkpoint tal como lo ve el servidor ComfyUI
|
||||
(ej. "dreamshaper_8.safetensors"). Debe estar entre los que devuelve
|
||||
comfyui_object_info para CheckpointLoaderSimple.
|
||||
image: nombre del archivo de la imagen base dentro de la carpeta input/
|
||||
del servidor ComfyUI (lo carga el nodo LoadImage).
|
||||
mask: nombre del archivo de la mascara dentro de input/ del servidor
|
||||
(lo carga LoadImageMask; las zonas blancas se regeneran).
|
||||
positive: prompt positivo (lo que se quiere ver en la zona enmascarada).
|
||||
negative: prompt negativo (lo que se quiere evitar). Por defecto "".
|
||||
denoise: fuerza de denoising del KSampler (1.0 regenera por completo la
|
||||
zona enmascarada; <1.0 conserva parte de la base). keyword-only.
|
||||
steps: pasos de sampling del KSampler. keyword-only.
|
||||
cfg: classifier-free guidance scale. keyword-only.
|
||||
seed: semilla del KSampler. 0 es determinista; cambiar para variar.
|
||||
keyword-only.
|
||||
sampler_name: nombre del sampler (ej. "euler", "dpmpp_2m"). keyword-only.
|
||||
scheduler: scheduler del sampler (ej. "normal", "karras"). keyword-only.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow. Las claves son
|
||||
node_ids y cada valor tiene class_type + inputs.
|
||||
"""
|
||||
return {
|
||||
"4": {
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": ckpt_name},
|
||||
},
|
||||
"10": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": image},
|
||||
},
|
||||
"12": {
|
||||
"class_type": "LoadImageMask",
|
||||
"inputs": {"image": mask, "channel": "red"},
|
||||
},
|
||||
"11": {
|
||||
"class_type": "VAEEncodeForInpaint",
|
||||
"inputs": {
|
||||
"pixels": ["10", 0],
|
||||
"vae": ["4", 2],
|
||||
"mask": ["12", 0],
|
||||
"grow_mask_by": 6,
|
||||
},
|
||||
},
|
||||
"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": denoise,
|
||||
"model": ["4", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["11", 0],
|
||||
},
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {"samples": ["3", 0], "vae": ["4", 2]},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": "comfy_inpaint", "images": ["8", 0]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
wf = comfyui_build_inpaint_workflow(
|
||||
ckpt_name="dreamshaper_8.safetensors",
|
||||
image="room.png",
|
||||
mask="room_mask.png",
|
||||
positive="a vase of red flowers on the table, sharp focus",
|
||||
negative="blurry, low quality",
|
||||
seed=42,
|
||||
)
|
||||
print(json.dumps(wf, indent=2))
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: comfyui_build_sdxl_refiner_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_build_sdxl_refiner_workflow(base_ckpt: str, refiner_ckpt: str, positive: str, negative: str = \"\", *, base_steps: int = 20, refiner_steps: int = 5, cfg: float = 7.0, seed: int = 0, width: int = 1024, height: int = 1024) -> dict"
|
||||
description: "Construye el dict de un workflow ComfyUI SDXL base+refiner en API format: dos KSamplerAdvanced encadenados que comparten el total de pasos. El base arranca el ruido y devuelve el latente con ruido sobrante (return_with_leftover_noise=enable), el refiner lo recoge (add_noise=disable) y lo termina. Pura, sin red ni I/O. Hermana de comfyui_build_txt2img_workflow."
|
||||
tags: [comfyui, ml, image-generation, sdxl, refiner, stable-diffusion, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: base_ckpt
|
||||
desc: "Nombre del checkpoint base SDXL tal como lo ve el servidor (ej. 'sd_xl_base_1.0.safetensors'). En CheckpointLoaderSimple."
|
||||
- name: refiner_ckpt
|
||||
desc: "Nombre del checkpoint refiner SDXL (ej. 'sd_xl_refiner_1.0.safetensors')."
|
||||
- name: positive
|
||||
desc: "Prompt positivo: lo que se quiere ver. Se usa para el CLIP del base y el del refiner."
|
||||
- name: negative
|
||||
desc: "Prompt negativo: lo que se quiere evitar. Por defecto cadena vacia."
|
||||
- name: base_steps
|
||||
desc: "Pasos que ejecuta el sampler base (del 0 a base_steps). keyword-only."
|
||||
- name: refiner_steps
|
||||
desc: "Pasos que ejecuta el refiner (de base_steps al total). El total es base_steps + refiner_steps. keyword-only."
|
||||
- name: cfg
|
||||
desc: "Classifier-free guidance scale (compartido por ambos samplers). keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla de ruido (compartida por ambos samplers). keyword-only."
|
||||
- name: width
|
||||
desc: "Ancho del latente/imagen en px (SDXL nativo 1024). keyword-only."
|
||||
- name: height
|
||||
desc: "Alto del latente/imagen en px (SDXL nativo 1024). keyword-only."
|
||||
output: "dict en API format con node_ids como claves (CheckpointLoaderSimple base '4' y refiner '14', EmptyLatentImage '5', CLIPTextEncode base '6'/'7' y refiner '16'/'17', KSamplerAdvanced base '3' y refiner '15', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_build_sdxl_refiner_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_sdxl_refiner_workflow import comfyui_build_sdxl_refiner_workflow
|
||||
|
||||
wf = comfyui_build_sdxl_refiner_workflow(
|
||||
base_ckpt="sd_xl_base_1.0.safetensors",
|
||||
refiner_ckpt="sd_xl_refiner_1.0.safetensors",
|
||||
positive="a majestic lion on a cliff at sunset, ultra detailed",
|
||||
negative="blurry, low quality",
|
||||
base_steps=20, refiner_steps=5,
|
||||
seed=42,
|
||||
)
|
||||
# wf["3"]["inputs"]["steps"] == 25 # total = base + refiner
|
||||
# wf["3"]["inputs"]["end_at_step"] == 20 # base corta en base_steps
|
||||
# wf["15"]["inputs"]["start_at_step"] == 20 # refiner arranca ahi
|
||||
# wf["15"]["inputs"]["latent_image"] == ["3", 0] # encadenado del base
|
||||
```
|
||||
|
||||
El bloque se lanza con el python del venv. `./fn run` directo no aplica (firma con
|
||||
`*` keyword-only); usa el import o un heredoc.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando uses el pipeline oficial SDXL de dos etapas (checkpoint base + checkpoint
|
||||
refiner) para pulir el detalle final. Si solo tienes un checkpoint SDXL completo
|
||||
(sin refiner separado) usa `comfyui_build_txt2img_workflow` con width/height 1024
|
||||
— el refiner separado solo merece la pena con `sd_xl_refiner_*`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Es API format (nodos numerados), NO el formato de la UI de ComfyUI.
|
||||
- Los dos KSamplerAdvanced comparten `steps` = base_steps + refiner_steps. El
|
||||
base va de 0 a base_steps con `return_with_leftover_noise=enable` (no decodifica);
|
||||
el refiner va de base_steps a 10000 (= "hasta el final") con `add_noise=disable`.
|
||||
- El VAE de salida es el del refiner (`["14", 2]`). Ambos checkpoints SDXL traen
|
||||
el mismo VAE, asi que el resultado no cambia; para un VAE externo cambia esa
|
||||
conexion en el nodo '8'.
|
||||
- SDXL es nativo a 1024x1024: bajar mucho la resolucion degrada el resultado.
|
||||
- Es pura: NO valida que los checkpoints existan en el servidor. Valida antes con
|
||||
`comfyui_validate_workflow` (necesitas ambos: base y refiner descargados).
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Construye un workflow ComfyUI SDXL base+refiner en API format.
|
||||
|
||||
SDXL genera en dos etapas: un checkpoint base produce el latente con la mayor
|
||||
parte de los pasos y un checkpoint refiner termina los ultimos pasos para pulir
|
||||
el detalle. Se encadenan dos KSamplerAdvanced compartiendo el numero total de
|
||||
pasos: el base arranca el ruido y devuelve el latente con ruido sobrante
|
||||
(return_with_leftover_noise=enable, no decodifica), y el refiner lo recoge
|
||||
(add_noise=disable) y lo lleva al final.
|
||||
|
||||
Cadena de nodos: CheckpointLoaderSimple base + CheckpointLoaderSimple refiner +
|
||||
EmptyLatentImage + 4 CLIPTextEncode (positivo/negativo por cada CLIP) ->
|
||||
KSamplerAdvanced base -> KSamplerAdvanced refiner -> VAEDecode -> SaveImage.
|
||||
|
||||
API format: cada clave es un node_id (string); cada nodo tiene class_type +
|
||||
inputs. Las conexiones entre nodos son listas [node_id, output_index]. Es el
|
||||
formato que acepta POST /prompt, distinto del formato de la UI (graph con links).
|
||||
|
||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
|
||||
"""
|
||||
|
||||
|
||||
def comfyui_build_sdxl_refiner_workflow(
|
||||
base_ckpt: str,
|
||||
refiner_ckpt: str,
|
||||
positive: str,
|
||||
negative: str = "",
|
||||
*,
|
||||
base_steps: int = 20,
|
||||
refiner_steps: int = 5,
|
||||
cfg: float = 7.0,
|
||||
seed: int = 0,
|
||||
width: int = 1024,
|
||||
height: int = 1024,
|
||||
) -> dict:
|
||||
"""Construye el dict de un workflow SDXL base+refiner (dos KSamplerAdvanced).
|
||||
|
||||
Args:
|
||||
base_ckpt: nombre del checkpoint base SDXL tal como lo ve el servidor
|
||||
ComfyUI (ej. "sd_xl_base_1.0.safetensors"). En CheckpointLoaderSimple.
|
||||
refiner_ckpt: nombre del checkpoint refiner SDXL
|
||||
(ej. "sd_xl_refiner_1.0.safetensors").
|
||||
positive: prompt positivo (lo que se quiere ver en la imagen). Se usa
|
||||
tanto para el CLIP del base como para el del refiner.
|
||||
negative: prompt negativo (lo que se quiere evitar). Por defecto "".
|
||||
base_steps: pasos que ejecuta el sampler base (del 0 a base_steps).
|
||||
keyword-only.
|
||||
refiner_steps: pasos que ejecuta el refiner (de base_steps al total).
|
||||
El total de pasos es base_steps + refiner_steps. keyword-only.
|
||||
cfg: classifier-free guidance scale (compartido). keyword-only.
|
||||
seed: semilla de ruido (compartida por ambos samplers). keyword-only.
|
||||
width: ancho del latente/imagen en px (SDXL nativo 1024). keyword-only.
|
||||
height: alto del latente/imagen en px (SDXL nativo 1024). keyword-only.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow. Las claves son
|
||||
node_ids y cada valor tiene class_type + inputs.
|
||||
"""
|
||||
total_steps = base_steps + refiner_steps
|
||||
return {
|
||||
"4": {
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": base_ckpt},
|
||||
},
|
||||
"14": {
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": refiner_ckpt},
|
||||
},
|
||||
"5": {
|
||||
"class_type": "EmptyLatentImage",
|
||||
"inputs": {"width": width, "height": height, "batch_size": 1},
|
||||
},
|
||||
"6": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": positive, "clip": ["4", 1]},
|
||||
},
|
||||
"7": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": negative, "clip": ["4", 1]},
|
||||
},
|
||||
"16": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": positive, "clip": ["14", 1]},
|
||||
},
|
||||
"17": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": negative, "clip": ["14", 1]},
|
||||
},
|
||||
"3": {
|
||||
"class_type": "KSamplerAdvanced",
|
||||
"inputs": {
|
||||
"add_noise": "enable",
|
||||
"noise_seed": seed,
|
||||
"steps": total_steps,
|
||||
"cfg": cfg,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "normal",
|
||||
"start_at_step": 0,
|
||||
"end_at_step": base_steps,
|
||||
"return_with_leftover_noise": "enable",
|
||||
"model": ["4", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["5", 0],
|
||||
},
|
||||
},
|
||||
"15": {
|
||||
"class_type": "KSamplerAdvanced",
|
||||
"inputs": {
|
||||
"add_noise": "disable",
|
||||
"noise_seed": seed,
|
||||
"steps": total_steps,
|
||||
"cfg": cfg,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "normal",
|
||||
"start_at_step": base_steps,
|
||||
"end_at_step": 10000,
|
||||
"return_with_leftover_noise": "disable",
|
||||
"model": ["14", 0],
|
||||
"positive": ["16", 0],
|
||||
"negative": ["17", 0],
|
||||
"latent_image": ["3", 0],
|
||||
},
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {"samples": ["15", 0], "vae": ["14", 2]},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": "comfy_sdxl", "images": ["8", 0]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
wf = comfyui_build_sdxl_refiner_workflow(
|
||||
base_ckpt="sd_xl_base_1.0.safetensors",
|
||||
refiner_ckpt="sd_xl_refiner_1.0.safetensors",
|
||||
positive="a majestic lion on a cliff at sunset, ultra detailed",
|
||||
negative="blurry, low quality",
|
||||
base_steps=20,
|
||||
refiner_steps=5,
|
||||
seed=42,
|
||||
)
|
||||
print(json.dumps(wf, indent=2))
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: comfyui_build_txt2img_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_build_txt2img_workflow(ckpt_name: str, positive: str, negative: str = \"\", *, steps: int = 20, cfg: float = 7.0, width: int = 512, height: int = 512, seed: int = 0, sampler_name: str = \"euler\", scheduler: str = \"normal\", filename_prefix: str = \"comfy\") -> dict"
|
||||
description: "Construye el dict de un workflow ComfyUI txt2img en API format (nodos numerados con class_type + inputs, conexiones como [node_id, output_index]) para SD1.5/SDXL: CheckpointLoaderSimple -> CLIPTextEncode x2 + EmptyLatentImage -> KSampler -> VAEDecode -> SaveImage. Pura, sin red ni I/O."
|
||||
tags: [comfyui, ml, image-generation, txt2img, stable-diffusion, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: ckpt_name
|
||||
desc: "Nombre del checkpoint tal como lo ve el servidor ComfyUI (ej. 'v1-5-pruned-emaonly-fp16.safetensors'). Debe estar en la lista que devuelve comfyui_object_info para CheckpointLoaderSimple."
|
||||
- name: positive
|
||||
desc: "Prompt positivo: lo que se quiere ver en la imagen."
|
||||
- name: negative
|
||||
desc: "Prompt negativo: lo que se quiere evitar. Por defecto cadena vacia."
|
||||
- name: steps
|
||||
desc: "Pasos de sampling del KSampler. keyword-only."
|
||||
- name: cfg
|
||||
desc: "Classifier-free guidance scale. keyword-only."
|
||||
- name: width
|
||||
desc: "Ancho del latente/imagen en px, multiplo de 8. keyword-only."
|
||||
- name: height
|
||||
desc: "Alto del latente/imagen en px, multiplo de 8. keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla del KSampler. 0 es determinista; cambiar para variar la imagen. keyword-only."
|
||||
- name: sampler_name
|
||||
desc: "Nombre del sampler (ej. 'euler', 'dpmpp_2m'). keyword-only."
|
||||
- name: scheduler
|
||||
desc: "Scheduler del sampler (ej. 'normal', 'karras'). keyword-only."
|
||||
- name: filename_prefix
|
||||
desc: "Prefijo del PNG que SaveImage escribe en output/. keyword-only."
|
||||
output: "dict en API format con node_ids '3'..'9' como claves; cada valor tiene class_type + inputs. Listo para comfyui_submit_workflow."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_build_txt2img_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_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
|
||||
wf = comfyui_build_txt2img_workflow(
|
||||
ckpt_name="v1-5-pruned-emaonly-fp16.safetensors",
|
||||
positive="a red apple on a wooden table, sharp focus",
|
||||
negative="blurry, low quality",
|
||||
steps=20,
|
||||
seed=42,
|
||||
)
|
||||
# wf["3"]["class_type"] == "KSampler"
|
||||
# wf["3"]["inputs"]["model"] == ["4", 0] # conexion al CheckpointLoader
|
||||
# wf["9"]["class_type"] == "SaveImage"
|
||||
```
|
||||
|
||||
O lanzable directo con: `./fn run comfyui_build_txt2img_workflow` (imprime el JSON del workflow de ejemplo).
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Antes de enviar una generacion txt2img a ComfyUI: construye aqui el dict del
|
||||
workflow y pasalo a `comfyui_submit_workflow`. Usala siempre que necesites un
|
||||
txt2img basico sin tener que escribir el grafo de nodos a mano. Para workflows
|
||||
mas complejos (img2img, ControlNet, upscalers) construye el dict tu mismo o
|
||||
extiende esta funcion con un builder hermano.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Es API format (nodos numerados), NO el formato de la UI de ComfyUI (graph con
|
||||
links). No se puede pegar en la UI tal cual; es el formato que acepta POST
|
||||
/prompt.
|
||||
- `ckpt_name` debe coincidir EXACTAMENTE con un checkpoint visible para el
|
||||
servidor. Si no existe, ComfyUI rechaza el workflow con HTTP 400 al enviarlo
|
||||
(no aqui — esta funcion es pura y no valida contra el servidor).
|
||||
- `width`/`height` deben ser multiplos de 8 o el KSampler fallara en el
|
||||
servidor.
|
||||
- Asume que el checkpoint trae VAE embebido (VAEDecode usa `["4", 2]`, la salida
|
||||
VAE del CheckpointLoaderSimple). Para un VAE externo cambia esa conexion.
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Construye un workflow ComfyUI txt2img en "API format" (dict de nodos numerados).
|
||||
|
||||
API format: cada clave es un node_id (string); cada nodo tiene class_type +
|
||||
inputs. Las conexiones entre nodos son listas [node_id, output_index]. Este es
|
||||
el formato que acepta POST /prompt, distinto del formato de la UI (graph con
|
||||
links explicitos).
|
||||
|
||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
|
||||
"""
|
||||
|
||||
|
||||
def comfyui_build_txt2img_workflow(
|
||||
ckpt_name: str,
|
||||
positive: str,
|
||||
negative: str = "",
|
||||
*,
|
||||
steps: int = 20,
|
||||
cfg: float = 7.0,
|
||||
width: int = 512,
|
||||
height: int = 512,
|
||||
seed: int = 0,
|
||||
sampler_name: str = "euler",
|
||||
scheduler: str = "normal",
|
||||
filename_prefix: str = "comfy",
|
||||
) -> dict:
|
||||
"""Construye el dict del workflow txt2img basico para SD1.5 / SDXL.
|
||||
|
||||
Cadena de nodos: CheckpointLoaderSimple -> CLIPTextEncode (positivo y
|
||||
negativo) + EmptyLatentImage -> KSampler -> VAEDecode -> SaveImage.
|
||||
|
||||
Args:
|
||||
ckpt_name: nombre del checkpoint tal como lo ve el servidor ComfyUI
|
||||
(ej. "v1-5-pruned-emaonly-fp16.safetensors"). Debe estar entre los
|
||||
que devuelve comfyui_object_info en CheckpointLoaderSimple.
|
||||
positive: prompt positivo (lo que se quiere ver en la imagen).
|
||||
negative: prompt negativo (lo que se quiere evitar). Por defecto "".
|
||||
steps: pasos de sampling del KSampler.
|
||||
cfg: classifier-free guidance scale.
|
||||
width: ancho del latente/imagen en px (multiplo de 8).
|
||||
height: alto del latente/imagen en px (multiplo de 8).
|
||||
seed: semilla del KSampler (0 = determinista; cambia para variar).
|
||||
sampler_name: nombre del sampler (ej. "euler", "dpmpp_2m").
|
||||
scheduler: scheduler del sampler (ej. "normal", "karras").
|
||||
filename_prefix: prefijo del PNG generado por SaveImage en output/.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow. Las claves son
|
||||
node_ids ("3".."9") y cada valor tiene class_type + inputs.
|
||||
"""
|
||||
return {
|
||||
"4": {
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": ckpt_name},
|
||||
},
|
||||
"5": {
|
||||
"class_type": "EmptyLatentImage",
|
||||
"inputs": {"width": width, "height": height, "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]},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": filename_prefix, "images": ["8", 0]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
wf = comfyui_build_txt2img_workflow(
|
||||
ckpt_name="v1-5-pruned-emaonly-fp16.safetensors",
|
||||
positive="a red apple on a wooden table, sharp focus",
|
||||
negative="blurry, low quality",
|
||||
steps=20,
|
||||
seed=42,
|
||||
)
|
||||
print(json.dumps(wf, indent=2))
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: comfyui_build_upscale_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_build_upscale_workflow(image: str, *, model_name: str = \"4x-UltraSharp.pth\", method: str = \"model\") -> dict"
|
||||
description: "Construye el dict de un workflow ComfyUI de upscale en API format. method='model' usa UpscaleModelLoader + ImageUpscaleWithModel (ESRGAN, alta calidad); method='latent' usa ImageScaleBy (reescalado de pixel x2 sin modelo). Pura, sin red ni I/O."
|
||||
tags: [comfyui, ml, upscale, esrgan, stable-diffusion, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: image
|
||||
desc: "Nombre del archivo de imagen dentro de la carpeta input/ del servidor ComfyUI; lo carga el nodo LoadImage."
|
||||
- name: model_name
|
||||
desc: "Nombre del modelo de upscale en models/upscale_models/ (ej. '4x-UltraSharp.pth'). Solo se usa con method='model'. keyword-only."
|
||||
- name: method
|
||||
desc: "'model' (ESRGAN via UpscaleModelLoader + ImageUpscaleWithModel) o 'latent' (reescalado de pixel x2 con ImageScaleBy, sin modelo). keyword-only."
|
||||
output: "dict en API format. Con method='model': LoadImage '10' + UpscaleModelLoader '12' + ImageUpscaleWithModel '13' + SaveImage '9'. Con method='latent': LoadImage '10' + ImageScaleBy '13' + SaveImage '9'. Listo para comfyui_submit_workflow."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_build_upscale_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_upscale_workflow import comfyui_build_upscale_workflow
|
||||
|
||||
# Upscale con modelo ESRGAN (necesita el .pth en models/upscale_models/)
|
||||
wf = comfyui_build_upscale_workflow("render.png", model_name="4x-UltraSharp.pth")
|
||||
# wf["12"]["class_type"] == "UpscaleModelLoader"
|
||||
# wf["13"]["inputs"]["upscale_model"] == ["12", 0]
|
||||
|
||||
# Upscale rapido sin modelo (reescalado de pixel x2)
|
||||
wf_latent = comfyui_build_upscale_workflow("render.png", method="latent")
|
||||
# wf_latent["13"]["class_type"] == "ImageScaleBy"
|
||||
```
|
||||
|
||||
Lánzalo con el python del venv (import de arriba o heredoc). Nota: `./fn run` directo no aplica porque la firma usa `*` (keyword-only), no soportado por el generador de runner de `fn run`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras ampliar una imagen ya generada. Usa `method="model"` (ESRGAN) para
|
||||
mejor calidad si tienes un upscaler en `models/upscale_models/` (ej. 4x-UltraSharp);
|
||||
usa `method="latent"` para un reescalado rapido sin descargar nada. Pega la salida
|
||||
de un txt2img/img2img como `image` en el input/ del servidor.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `method="latent"` NO es un upscale en espacio latente real (eso requiere un
|
||||
checkpoint+VAE para encode/decode, que esta firma no recibe). Usa `ImageScaleBy`
|
||||
= reescalado de pixel con lanczos x2. Es honesto: barato y sin modelo, pero no
|
||||
recupera detalle como un ESRGAN. Para latent-upscale real construye un workflow
|
||||
con checkpoint + VAEEncode + LatentUpscale + VAEDecode.
|
||||
- Con `method="model"`, `model_name` debe existir en `models/upscale_models/`. Si
|
||||
no, ComfyUI rechaza el workflow al enviarlo (HTTP 400). Valida antes con
|
||||
`comfyui_validate_workflow`.
|
||||
- `image` debe existir en la carpeta `input/` del servidor.
|
||||
- Es pura: no valida contra el servidor.
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Construye un workflow ComfyUI de upscale en API format (dict de nodos numerados).
|
||||
|
||||
Dos modos:
|
||||
- method="model": upscale con modelo ESRGAN (UpscaleModelLoader +
|
||||
ImageUpscaleWithModel). Calidad alta; necesita un modelo en
|
||||
models/upscale_models/ (ej. "4x-UltraSharp.pth").
|
||||
- method="latent": reescalado en espacio de pixel con ImageScaleBy (x2, sin
|
||||
modelo ni checkpoint). Upscale rapido y barato.
|
||||
|
||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
|
||||
"""
|
||||
|
||||
|
||||
def comfyui_build_upscale_workflow(
|
||||
image: str,
|
||||
*,
|
||||
model_name: str = "4x-UltraSharp.pth",
|
||||
method: str = "model",
|
||||
) -> dict:
|
||||
"""Construye el dict de un workflow de upscale para una imagen cargada.
|
||||
|
||||
Args:
|
||||
image: nombre del archivo de imagen dentro de la carpeta input/ del
|
||||
servidor ComfyUI (lo que carga el nodo LoadImage).
|
||||
model_name: nombre del modelo de upscale en models/upscale_models/
|
||||
(ej. "4x-UltraSharp.pth"). Solo se usa con method="model".
|
||||
keyword-only.
|
||||
method: "model" (ESRGAN via UpscaleModelLoader + ImageUpscaleWithModel)
|
||||
o "latent" (reescalado de pixel x2 con ImageScaleBy, sin modelo).
|
||||
keyword-only.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow.
|
||||
|
||||
Raises:
|
||||
ValueError: si method no es "model" ni "latent".
|
||||
"""
|
||||
if method not in ("model", "latent"):
|
||||
raise ValueError(
|
||||
f"comfyui_build_upscale_workflow: method invalido {method!r}; "
|
||||
"usa 'model' o 'latent'."
|
||||
)
|
||||
load = {
|
||||
"10": {"class_type": "LoadImage", "inputs": {"image": image}},
|
||||
}
|
||||
if method == "model":
|
||||
return {
|
||||
**load,
|
||||
"12": {
|
||||
"class_type": "UpscaleModelLoader",
|
||||
"inputs": {"model_name": model_name},
|
||||
},
|
||||
"13": {
|
||||
"class_type": "ImageUpscaleWithModel",
|
||||
"inputs": {"upscale_model": ["12", 0], "image": ["10", 0]},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {
|
||||
"filename_prefix": "comfy_upscale",
|
||||
"images": ["13", 0],
|
||||
},
|
||||
},
|
||||
}
|
||||
# method == "latent": reescalado de pixel x2 sin modelo
|
||||
return {
|
||||
**load,
|
||||
"13": {
|
||||
"class_type": "ImageScaleBy",
|
||||
"inputs": {
|
||||
"upscale_method": "lanczos",
|
||||
"scale_by": 2.0,
|
||||
"image": ["10", 0],
|
||||
},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": "comfy_upscale", "images": ["13", 0]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
print(json.dumps(comfyui_build_upscale_workflow("example.png"), indent=2))
|
||||
print(json.dumps(comfyui_build_upscale_workflow("example.png", method="latent"), indent=2))
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: comfyui_download_model
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_download_model(url: str, dest_subdir: str = 'checkpoints', *, comfyui_dir: str = '~/ComfyUI', filename: str | None = None, token: str | None = None, overwrite: bool = False, timeout_s: float = 1800.0) -> dict"
|
||||
description: "Descarga un checkpoint/LoRA/VAE a <comfyui_dir>/models/<dest_subdir>/<filename> por HTTP siguiendo redirects. Soporta Civitai (token via ?token= y header Authorization Bearer) y HuggingFace (URL directa). Valida que la respuesta NO sea HTML de error y que un .safetensors tenga cabecera valida, asi no deja modelos falsos de 2 KB. Impura: red (HTTP GET) + escritura en disco. Solo stdlib."
|
||||
tags: [comfyui, ml, image-generation, stable-diffusion, http, download, models, civitai, huggingface]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["json", "os", "struct", "urllib.error", "urllib.parse", "urllib.request"]
|
||||
params:
|
||||
- name: url
|
||||
desc: "URL directa de descarga (Civitai api/download/models/<versionId>, HuggingFace resolve, o cualquier HTTP que sirva el binario)."
|
||||
- name: dest_subdir
|
||||
desc: "Subcarpeta dentro de models/ (checkpoints, loras, vae, controlnet, ...). Default 'checkpoints'."
|
||||
- name: comfyui_dir
|
||||
desc: "Raiz de la instalacion de ComfyUI (se expande ~). Default '~/ComfyUI'."
|
||||
- name: filename
|
||||
desc: "Nombre destino. None lo deriva del Content-Disposition de la respuesta o del path de la URL."
|
||||
- name: token
|
||||
desc: "Token de API (Civitai). Se añade como ?token= y como header Authorization Bearer. None lo omite. No hardcodear secretos: pasar desde pass/vault."
|
||||
- name: overwrite
|
||||
desc: "Si False y el destino ya existe, no descarga y devuelve error. Default False."
|
||||
- name: timeout_s
|
||||
desc: "Timeout de la peticion HTTP en segundos. Default 1800 (30 min, modelos grandes)."
|
||||
output: "dict {ok: bool, path: str, size_bytes: int, error: str}. ok False si la respuesta era HTML de error, si un .safetensors no valida su cabecera, si la descarga es < 1 KB, o si fallo red/escritura. En esos casos NO deja basura en disco (limpia el .part)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_download_model.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from ml.comfyui_download_model import comfyui_download_model
|
||||
|
||||
# Civitai (token desde pass, nunca hardcodeado):
|
||||
import subprocess
|
||||
token = subprocess.run(["pass", "civitai/api-token"], capture_output=True, text=True).stdout.strip() or None
|
||||
out = comfyui_download_model(
|
||||
"https://civitai.com/api/download/models/128713",
|
||||
dest_subdir="checkpoints",
|
||||
token=token,
|
||||
)
|
||||
print(out["ok"], out["path"], out["size_bytes"])
|
||||
|
||||
# HuggingFace (URL directa resolve), sin token:
|
||||
out = comfyui_download_model(
|
||||
"https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors",
|
||||
dest_subdir="vae",
|
||||
)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesitas un modelo que ComfyUI no tiene aun: lo bajas a la carpeta
|
||||
correcta y luego llamas `comfyui_refresh_nodes_ui` para que aparezca en los
|
||||
combos de la UI sin recargar. Resuelve el sitio (`models/<dest_subdir>/`) y el
|
||||
nombre por ti, y rechaza descargas que en realidad son paginas de error.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Civitai exige login para muchos modelos**: sin `token` valido, Civitai
|
||||
responde con HTML (login/Cloudflare). La funcion lo detecta (content-type +
|
||||
sniff de los primeros bytes) y devuelve `ok=False` SIN guardar el HTML. Si ves
|
||||
ese error, falta o caduco el token.
|
||||
- La validacion de cabecera safetensors solo aplica a nombres `.safetensors`. Un
|
||||
`.ckpt`/`.pt`/`.bin` se valida solo por content-type, sniff HTML y tamaño minimo
|
||||
(1 KB). Para `.safetensors` ademas se comprueba la cabecera (8 bytes LE de
|
||||
longitud + `{`).
|
||||
- Descarga a `<destino>.part` y solo hace `os.replace` al destino final tras
|
||||
validar: una descarga corrupta o HTML no deja archivo final.
|
||||
- `overwrite=False` (default) NO re-descarga si el archivo ya existe: devuelve
|
||||
`ok=False` con el path existente. Pasa `overwrite=True` para forzar.
|
||||
- Modelos grandes (varios GB) tardan; sube `timeout_s` si hace falta. No abuses
|
||||
del disco: comprueba espacio antes de bajar checkpoints SDXL (~6-7 GB).
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Descarga un checkpoint / LoRA / VAE a la carpeta correcta de ComfyUI.
|
||||
|
||||
Descarga por HTTP a `<comfyui_dir>/models/<dest_subdir>/<filename>` siguiendo
|
||||
redirects. Soporta Civitai (`https://civitai.com/api/download/models/<versionId>`,
|
||||
token opcional via `?token=` y header `Authorization: Bearer`) y HuggingFace (URL
|
||||
directa de resolve). Antes de aceptar el archivo VALIDA que la respuesta no sea
|
||||
una pagina HTML de error (Cloudflare, login wall, 404 estilizado) y que, si el
|
||||
nombre termina en `.safetensors`, tenga una cabecera de safetensors valida. Asi
|
||||
no deja "modelos" que en realidad son HTML de 2 KB.
|
||||
|
||||
Funcion impura: hace red (HTTP GET) y escribe en disco. Solo stdlib.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
_HTML_SNIFF = (b"<!doctype", b"<html", b"<head", b"<?xml")
|
||||
|
||||
|
||||
def _derive_filename(url: str, content_disposition: str) -> str:
|
||||
"""Deriva el nombre de archivo del Content-Disposition o, si no, de la URL."""
|
||||
if content_disposition:
|
||||
# filename="x" | filename=x | filename*=UTF-8''x
|
||||
for part in content_disposition.split(";"):
|
||||
part = part.strip()
|
||||
for key in ("filename*=", "filename="):
|
||||
if part.lower().startswith(key):
|
||||
raw = part[len(key):].strip().strip('"')
|
||||
if "''" in raw: # RFC 5987: UTF-8''<pct-encoded>
|
||||
raw = raw.split("''", 1)[1]
|
||||
name = urllib.parse.unquote(os.path.basename(raw))
|
||||
if name:
|
||||
return name
|
||||
name = os.path.basename(urllib.parse.urlparse(url).path)
|
||||
return name or "model.bin"
|
||||
|
||||
|
||||
def _is_valid_safetensors(path: str) -> bool:
|
||||
"""True si el archivo tiene cabecera de safetensors coherente.
|
||||
|
||||
Formato: 8 bytes little-endian con la longitud N del header JSON, seguidos de
|
||||
N bytes que empiezan por '{'. Rechaza HTML/errores disfrazados de .safetensors.
|
||||
"""
|
||||
try:
|
||||
size = os.path.getsize(path)
|
||||
if size < 9:
|
||||
return False
|
||||
with open(path, "rb") as fh:
|
||||
n = struct.unpack("<Q", fh.read(8))[0]
|
||||
if n <= 0 or n > size - 8 or n > 100_000_000:
|
||||
return False
|
||||
return fh.read(1) == b"{"
|
||||
except Exception: # noqa: BLE001 — archivo ilegible = invalido
|
||||
return False
|
||||
|
||||
|
||||
def comfyui_download_model(
|
||||
url: str,
|
||||
dest_subdir: str = "checkpoints",
|
||||
*,
|
||||
comfyui_dir: str = "~/ComfyUI",
|
||||
filename: str | None = None,
|
||||
token: str | None = None,
|
||||
overwrite: bool = False,
|
||||
timeout_s: float = 1800.0,
|
||||
) -> dict:
|
||||
"""Descarga un modelo a `<comfyui_dir>/models/<dest_subdir>/<filename>`.
|
||||
|
||||
Args:
|
||||
url: URL directa de descarga (Civitai api/download, HuggingFace resolve,
|
||||
o cualquier HTTP que sirva el binario).
|
||||
dest_subdir: subcarpeta dentro de `models/` (checkpoints, loras, vae,
|
||||
controlnet, ...). Default "checkpoints".
|
||||
comfyui_dir: raiz de la instalacion de ComfyUI (se expande ~).
|
||||
filename: nombre destino del archivo. Si None, se deriva del
|
||||
Content-Disposition de la respuesta o del path de la URL.
|
||||
token: token de API (Civitai). Se añade como `?token=` y como header
|
||||
`Authorization: Bearer <token>`. None lo omite.
|
||||
overwrite: si False y el destino ya existe, no descarga y devuelve error.
|
||||
timeout_s: timeout de la peticion HTTP en segundos.
|
||||
|
||||
Returns:
|
||||
dict {ok: bool, path: str, size_bytes: int, error: str}. ok False si la
|
||||
respuesta era HTML de error, si un .safetensors no valida su cabecera, o
|
||||
si fallo la red/escritura. En esos casos no deja basura en disco.
|
||||
"""
|
||||
base = os.path.expanduser(comfyui_dir)
|
||||
dest_dir = os.path.join(base, "models", dest_subdir)
|
||||
|
||||
req_url = url
|
||||
headers = {"User-Agent": "fn-registry/comfyui_download_model"}
|
||||
if token:
|
||||
sep = "&" if "?" in req_url else "?"
|
||||
req_url = f"{req_url}{sep}token={urllib.parse.quote(token)}"
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
req = urllib.request.Request(req_url, headers=headers)
|
||||
tmp_path = None
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout_s) as resp:
|
||||
content_type = resp.headers.get("Content-Type", "")
|
||||
disp = resp.headers.get("Content-Disposition", "")
|
||||
name = filename or _derive_filename(resp.geturl(), disp)
|
||||
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
final_path = os.path.join(dest_dir, name)
|
||||
if os.path.exists(final_path) and not overwrite:
|
||||
return {
|
||||
"ok": False,
|
||||
"path": final_path,
|
||||
"size_bytes": os.path.getsize(final_path),
|
||||
"error": f"ya existe (overwrite=False): {final_path}",
|
||||
}
|
||||
|
||||
# Rechazo temprano por content-type HTML.
|
||||
if "text/html" in content_type.lower():
|
||||
return {
|
||||
"ok": False,
|
||||
"path": "",
|
||||
"size_bytes": 0,
|
||||
"error": (
|
||||
f"la respuesta es HTML (Content-Type: {content_type}), "
|
||||
"no un binario de modelo. Revisa la URL/token."
|
||||
),
|
||||
}
|
||||
|
||||
tmp_path = final_path + ".part"
|
||||
first = resp.read(512)
|
||||
# Sniff de los primeros bytes: HTML aunque el content-type mienta.
|
||||
low = first.lower().lstrip()
|
||||
if any(low.startswith(sig) for sig in _HTML_SNIFF):
|
||||
return {
|
||||
"ok": False,
|
||||
"path": "",
|
||||
"size_bytes": 0,
|
||||
"error": "la respuesta empieza con HTML (pagina de error/login), no un modelo.",
|
||||
}
|
||||
|
||||
size = 0
|
||||
with open(tmp_path, "wb") as fh:
|
||||
fh.write(first)
|
||||
size += len(first)
|
||||
while True:
|
||||
chunk = resp.read(1024 * 256)
|
||||
if not chunk:
|
||||
break
|
||||
fh.write(chunk)
|
||||
size += len(chunk)
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode(errors="replace")[:300]
|
||||
_cleanup(tmp_path)
|
||||
return {"ok": False, "path": "", "size_bytes": 0,
|
||||
"error": f"HTTP {exc.code} en {url}: {body}"}
|
||||
except Exception as exc: # noqa: BLE001 — red/DNS/escritura
|
||||
_cleanup(tmp_path)
|
||||
return {"ok": False, "path": "", "size_bytes": 0,
|
||||
"error": f"fallo descargando {url}: {exc}"}
|
||||
|
||||
# Validacion de tamaño minimo (una pagina de error suele ser < 2 KB).
|
||||
if size < 1024:
|
||||
_cleanup(tmp_path)
|
||||
return {"ok": False, "path": "", "size_bytes": size,
|
||||
"error": f"descarga sospechosamente pequeña ({size} bytes); probable error, no un modelo."}
|
||||
|
||||
# Validacion de cabecera safetensors si aplica.
|
||||
if name.endswith(".safetensors") and not _is_valid_safetensors(tmp_path):
|
||||
_cleanup(tmp_path)
|
||||
return {"ok": False, "path": "", "size_bytes": size,
|
||||
"error": f"{name} no tiene una cabecera safetensors valida; descarga corrupta o HTML disfrazado."}
|
||||
|
||||
os.replace(tmp_path, final_path)
|
||||
return {"ok": True, "path": final_path, "size_bytes": size, "error": ""}
|
||||
|
||||
|
||||
def _cleanup(path: str | None) -> None:
|
||||
if path and os.path.exists(path):
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
out = comfyui_download_model(
|
||||
sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8188/",
|
||||
dest_subdir="checkpoints",
|
||||
filename="smoke_fake.safetensors",
|
||||
)
|
||||
print(json.dumps(out, ensure_ascii=False, indent=2))
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: comfyui_fetch_output_image
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_fetch_output_image(filename: str, *, subfolder: str = \"\", type_: str = \"output\", server: str = \"127.0.0.1:8188\", dest_dir: str = \".\", timeout: float = 60.0) -> dict"
|
||||
description: "Descarga un PNG generado por ComfyUI via GET /view?filename=&subfolder=&type= a disco local. comfyui_wait_result solo devuelve metadata (filename/subfolder/type); esta funcion baja el archivo real. Impura: HTTP GET + escritura en disco, solo stdlib."
|
||||
tags: [comfyui, ml, image-generation, download, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
params:
|
||||
- name: filename
|
||||
desc: "Nombre del archivo en el servidor (ej. 'comfy_00001_.png'), tal como lo reporta comfyui_wait_result en outputs[node].images[].filename."
|
||||
- name: subfolder
|
||||
desc: "Subcarpeta dentro de la carpeta del servidor (vacia por defecto). keyword-only."
|
||||
- name: type_
|
||||
desc: "Tipo de carpeta del servidor: 'output', 'temp' o 'input'. keyword-only."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema. keyword-only."
|
||||
- name: dest_dir
|
||||
desc: "Directorio local donde guardar la imagen; se crea si no existe. keyword-only."
|
||||
- name: timeout
|
||||
desc: "Timeout de la peticion HTTP en segundos. keyword-only."
|
||||
output: "dict {ok, path, size_bytes, error}. path = ruta local del PNG guardado, size_bytes = bytes descargados. Si falla, ok=False y error explica (HTTP/conexion/escritura)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_fetch_output_image.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
|
||||
|
||||
# Tras comfyui_submit_workflow + comfyui_wait_result, baja el PNG al disco
|
||||
res = comfyui_fetch_output_image("comfy_00001_.png", dest_dir="/tmp/comfy_out")
|
||||
# res == {"ok": True, "path": "/tmp/comfy_out/comfy_00001_.png", "size_bytes": 372027, "error": ""}
|
||||
```
|
||||
|
||||
Lánzalo con el python del venv (import de arriba o heredoc). Nota: `./fn run` directo no aplica porque la firma usa `*` (keyword-only), no soportado por el generador de runner de `fn run`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Despues de generar una imagen (submit + wait), cuando necesites el PNG real en
|
||||
disco (no solo su nombre): para abrirlo, mostrarlo, post-procesarlo o moverlo a
|
||||
un vault. Toma `filename`/`subfolder`/`type` directo de la entrada `images[]` que
|
||||
devuelve `comfyui_wait_result` por nodo SaveImage.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: hace HTTP GET al servidor y escribe en disco. Requiere el servidor vivo.
|
||||
- `type_` debe coincidir con la carpeta real: SaveImage escribe en "output",
|
||||
PreviewImage en "temp". Si pasas el type equivocado, el servidor responde 404.
|
||||
- El nombre local es `basename(filename)` dentro de `dest_dir` (no recrea la
|
||||
estructura de subfolder en local).
|
||||
- No reintenta: si el servidor esta reiniciandose, devuelve error de conexion;
|
||||
reintenta tu desde el caller.
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Descarga un PNG generado por ComfyUI via GET /view a disco local.
|
||||
|
||||
comfyui_wait_result devuelve solo metadata (node_id -> {images: [{filename,
|
||||
subfolder, type}]}); esta funcion baja el archivo real al disco local para
|
||||
poder abrirlo, mostrarlo o procesarlo.
|
||||
|
||||
Impura: red (HTTP GET) + escritura en disco. Solo stdlib (urllib, os).
|
||||
"""
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
def comfyui_fetch_output_image(
|
||||
filename: str,
|
||||
*,
|
||||
subfolder: str = "",
|
||||
type_: str = "output",
|
||||
server: str = "127.0.0.1:8188",
|
||||
dest_dir: str = ".",
|
||||
timeout: float = 60.0,
|
||||
) -> dict:
|
||||
"""Baja una imagen del servidor ComfyUI a un directorio local.
|
||||
|
||||
Args:
|
||||
filename: nombre del archivo en el servidor (ej. "comfy_00001_.png"),
|
||||
tal como lo reporta comfyui_wait_result en outputs[node].images.
|
||||
subfolder: subcarpeta dentro de la carpeta del servidor (vacia por
|
||||
defecto). keyword-only.
|
||||
type_: tipo de carpeta del servidor: "output", "temp" o "input".
|
||||
keyword-only.
|
||||
server: host:port del servidor ComfyUI (sin esquema). keyword-only.
|
||||
dest_dir: directorio local donde guardar la imagen; se crea si no existe.
|
||||
keyword-only.
|
||||
timeout: timeout de la peticion HTTP en segundos. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict {ok, path, size_bytes, error}. path = ruta local del PNG guardado;
|
||||
size_bytes = tamano descargado. Si falla, ok=False y error explica.
|
||||
"""
|
||||
qs = urllib.parse.urlencode(
|
||||
{"filename": filename, "subfolder": subfolder, "type": type_}
|
||||
)
|
||||
url = f"http://{server}/view?{qs}"
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
||||
blob = resp.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode(errors="replace")[:200]
|
||||
return {"ok": False, "path": "", "size_bytes": 0,
|
||||
"error": f"HTTP {exc.code} en {url}: {body}"}
|
||||
except urllib.error.URLError as exc:
|
||||
return {"ok": False, "path": "", "size_bytes": 0,
|
||||
"error": f"no se pudo conectar a {url}: {exc.reason}"}
|
||||
try:
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
out_path = os.path.join(dest_dir, os.path.basename(filename))
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(blob)
|
||||
except OSError as exc:
|
||||
return {"ok": False, "path": "", "size_bytes": 0,
|
||||
"error": f"no se pudo escribir en {dest_dir!r}: {exc}"}
|
||||
return {"ok": True, "path": out_path, "size_bytes": len(blob), "error": ""}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
res = comfyui_fetch_output_image("comfy_00001_.png", dest_dir="/tmp")
|
||||
print(json.dumps(res, indent=2))
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: comfyui_fetch_output_mesh
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_fetch_output_mesh(prompt_id: str, *, server: str = \"127.0.0.1:8188\", dest: str | None = None, timeout: float = 120.0) -> dict"
|
||||
description: "Localiza y descarga la malla 3D producida por un workflow ComfyUI a disco local. Hermana de comfyui_fetch_output_image pero para mallas: el nodo SaveGLB expone su salida en GET /history/{prompt_id} bajo la clave '3d' (no 'images'). Localiza el primer .glb/.obj/.ply/.gltf/.fbx/.stl, lo baja via GET /view y opcionalmente lo escribe en dest. Impura: HTTP GET + escritura en disco, solo stdlib."
|
||||
tags: [comfyui, ml, img-to-3d, hunyuan3d, mesh, download, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
params:
|
||||
- name: prompt_id
|
||||
desc: "id devuelto por comfyui_submit_workflow, de un workflow cuyo nodo SaveGLB ya termino (usa comfyui_wait_result antes si dudas)."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema. keyword-only."
|
||||
- name: dest
|
||||
desc: "Ruta destino. Si None, escribe el basename de la malla en el cwd. Si es un directorio (o termina en separador), escribe el basename dentro. Si es una ruta de archivo, escribe ahi. keyword-only."
|
||||
- name: timeout
|
||||
desc: "Timeout de cada peticion HTTP en segundos. keyword-only."
|
||||
output: "dict {ok, path, format, bytes, error}. path = ruta local del archivo de malla guardado, format = extension sin punto (ej. 'glb'), bytes = bytes descargados. Si falla, ok=False y error explica (sin malla en history, HTTP, conexion o escritura)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_fetch_output_mesh.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_fetch_output_mesh import comfyui_fetch_output_mesh
|
||||
|
||||
# Tras comfyui_submit_workflow + comfyui_wait_result de un workflow imagen->3D,
|
||||
# baja el .glb al disco (el SaveGLB lo expone en /history bajo la clave "3d").
|
||||
res = comfyui_fetch_output_mesh("2817f111-e21b-4672-95e7-5bec4314c4a7", dest="/tmp/meshes")
|
||||
# res == {"ok": True, "path": "/tmp/meshes/3d_robot_mesh_00001_.glb",
|
||||
# "format": "glb", "bytes": 60051544, "error": ""}
|
||||
```
|
||||
|
||||
Lánzalo con el python del venv (import de arriba o heredoc). Nota: `./fn run` directo no aplica porque la firma usa `*` (keyword-only), no soportado por el generador de runner de `fn run`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Despues de reconstruir una malla 3D (submit + wait de un workflow Hunyuan3D),
|
||||
cuando necesites el archivo .glb/.obj/.ply real en disco (no solo su nombre): para
|
||||
abrirlo en un visor, post-procesarlo (decimar, recolorear) o moverlo a un vault.
|
||||
Para el flujo completo desde una imagen en disco usa el pipeline
|
||||
`comfyui_image_to_3d_oneshot`, que ya llama a esta funcion al final.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: hace HTTP GET a /history y /view y escribe en disco. Requiere el server
|
||||
vivo y que el prompt YA haya terminado (usa `comfyui_wait_result` antes).
|
||||
- El SaveGLB expone la malla bajo la clave `"3d"` en los outputs, NO bajo
|
||||
`"images"` — por eso `comfyui_fetch_output_image` no sirve para mallas.
|
||||
- El history se purga al reiniciar el server: si el prompt ya no esta, devuelve
|
||||
`ok=False` con "no esta en /history". No reintenta; reintenta tu desde el caller.
|
||||
- Toma el PRIMER archivo de malla que encuentra (prioriza la clave "3d"). Si un
|
||||
workflow exporta varios formatos, baja solo uno; para los demas, llama otra vez
|
||||
o usa GET /view con el filename concreto.
|
||||
- `dest` se interpreta: None -> cwd; directorio -> dentro; archivo -> esa ruta.
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Localiza y descarga la malla 3D producida por un workflow ComfyUI a disco local.
|
||||
|
||||
Hermana de comfyui_fetch_output_image, pero para mallas 3D: el nodo SaveGLB de un
|
||||
workflow Hunyuan3D expone su salida en GET /history/{prompt_id} bajo la clave "3d"
|
||||
(no "images"), con {filename, subfolder, type}. Esta funcion lee ese history,
|
||||
localiza el primer archivo de malla (.glb/.obj/.ply/.gltf/.fbx/.stl/.usdz), lo baja
|
||||
via GET /view a disco local y, opcionalmente, lo escribe en `dest`.
|
||||
|
||||
Impura: red (HTTP GET a /history y /view) + escritura en disco. Solo stdlib.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
_MESH_EXTS = (".glb", ".gltf", ".obj", ".ply", ".fbx", ".stl", ".usdz", ".splat")
|
||||
|
||||
|
||||
def _find_mesh_output(outputs: dict) -> dict | None:
|
||||
"""Busca en los outputs de /history el primer archivo de malla 3D.
|
||||
|
||||
Recorre cada nodo y cada lista de su output; el SaveGLB usa la clave "3d",
|
||||
pero se acepta cualquier lista de dicts con "filename" de extension de malla.
|
||||
Devuelve {filename, subfolder, type} o None si no hay ninguno.
|
||||
"""
|
||||
# Prioriza la clave canonica "3d"; si no, cualquier lista con filename de malla.
|
||||
for prefer in (True, False):
|
||||
for node_out in outputs.values():
|
||||
if not isinstance(node_out, dict):
|
||||
continue
|
||||
for key, items in node_out.items():
|
||||
if prefer and key != "3d":
|
||||
continue
|
||||
if not isinstance(items, list):
|
||||
continue
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
fn = item.get("filename", "")
|
||||
if fn.lower().endswith(_MESH_EXTS):
|
||||
return {
|
||||
"filename": fn,
|
||||
"subfolder": item.get("subfolder", ""),
|
||||
"type": item.get("type", "output"),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_dest(dest: str | None, filename: str) -> str:
|
||||
"""Resuelve la ruta local destino a partir de `dest` y el basename remoto."""
|
||||
base = os.path.basename(filename)
|
||||
if dest is None:
|
||||
return os.path.join(os.getcwd(), base)
|
||||
expanded = os.path.expanduser(dest)
|
||||
if os.path.isdir(expanded) or expanded.endswith(os.sep):
|
||||
return os.path.join(expanded, base)
|
||||
return expanded
|
||||
|
||||
|
||||
def comfyui_fetch_output_mesh(
|
||||
prompt_id: str,
|
||||
*,
|
||||
server: str = "127.0.0.1:8188",
|
||||
dest: str | None = None,
|
||||
timeout: float = 120.0,
|
||||
) -> dict:
|
||||
"""Descarga la malla 3D de un prompt ComfyUI ya ejecutado a disco local.
|
||||
|
||||
Args:
|
||||
prompt_id: id devuelto por comfyui_submit_workflow, de un workflow cuyo
|
||||
nodo SaveGLB ya termino (usa comfyui_wait_result antes si dudas).
|
||||
server: host:port del servidor ComfyUI (sin esquema). keyword-only.
|
||||
dest: ruta destino. Si None, escribe el basename de la malla en el cwd.
|
||||
Si es un directorio (o termina en separador), escribe el basename
|
||||
dentro. Si es una ruta de archivo, escribe ahi. keyword-only.
|
||||
timeout: timeout de cada peticion HTTP en segundos. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict {ok, path, format, bytes, error}. path = ruta local del archivo de
|
||||
malla guardado; format = extension sin punto (ej. "glb"); bytes = tamano
|
||||
descargado. Si falla, ok=False y error explica (sin malla en history,
|
||||
HTTP, conexion o escritura).
|
||||
"""
|
||||
hist_url = f"http://{server}/history/{prompt_id}"
|
||||
try:
|
||||
with urllib.request.urlopen(hist_url, timeout=timeout) as resp:
|
||||
hist = json.loads(resp.read())
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode(errors="replace")[:200]
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"HTTP {exc.code} en {hist_url}: {body}"}
|
||||
except urllib.error.URLError as exc:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"no se pudo conectar a {hist_url}: {exc.reason}"}
|
||||
except json.JSONDecodeError as exc:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"respuesta no es JSON valido desde {hist_url}: {exc}"}
|
||||
|
||||
entry = hist.get(prompt_id)
|
||||
if not entry:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"prompt_id {prompt_id} no esta en /history (¿no termino o se purgo?)"}
|
||||
outputs = entry.get("outputs", {})
|
||||
mesh = _find_mesh_output(outputs)
|
||||
if mesh is None:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"sin archivo de malla 3D en los outputs de {prompt_id}"}
|
||||
|
||||
qs = urllib.parse.urlencode({
|
||||
"filename": mesh["filename"],
|
||||
"subfolder": mesh["subfolder"],
|
||||
"type": mesh["type"],
|
||||
})
|
||||
view_url = f"http://{server}/view?{qs}"
|
||||
try:
|
||||
with urllib.request.urlopen(view_url, timeout=timeout) as resp:
|
||||
blob = resp.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode(errors="replace")[:200]
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"HTTP {exc.code} en {view_url}: {body}"}
|
||||
except urllib.error.URLError as exc:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"no se pudo conectar a {view_url}: {exc.reason}"}
|
||||
|
||||
out_path = _resolve_dest(dest, mesh["filename"])
|
||||
try:
|
||||
parent = os.path.dirname(out_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(blob)
|
||||
except OSError as exc:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"no se pudo escribir en {out_path!r}: {exc}"}
|
||||
|
||||
fmt = os.path.splitext(mesh["filename"])[1].lstrip(".").lower()
|
||||
return {"ok": True, "path": out_path, "format": fmt, "bytes": len(blob), "error": ""}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
pid = sys.argv[1] if len(sys.argv) > 1 else "00000000-0000-0000-0000-000000000000"
|
||||
res = comfyui_fetch_output_mesh(pid, dest="/tmp/comfy_mesh")
|
||||
print(json.dumps(res, indent=2))
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: comfyui_import_workflow_json
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_import_workflow_json(source: str, *, server: str = \"127.0.0.1:8188\", timeout: float = 15.0) -> dict"
|
||||
description: "Lee un workflow ComfyUI desde una URL (http/https) o un path local y lo normaliza a API format. Si viene en formato UI graph ({nodes, links}) lo convierte a API format usando /object_info para mapear los widgets; si ya es API format lo devuelve tal cual. Compone comfyui_object_info. Impura: HTTP GET / lectura de disco."
|
||||
tags: [comfyui, ml, import, workflow, stable-diffusion]
|
||||
uses_functions: [comfyui_object_info_py_ml]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
params:
|
||||
- name: source
|
||||
desc: "URL http(s) de un JSON de workflow (OpenArt, ComfyWorkflows, raw GitHub...) o ruta de un archivo local."
|
||||
- name: server
|
||||
desc: "host:port de ComfyUI usado SOLO para mapear los valores de widget cuando la fuente viene en formato UI graph. keyword-only."
|
||||
- name: timeout
|
||||
desc: "Timeout HTTP en segundos. keyword-only."
|
||||
output: "dict {ok, workflow, format_detected, error}. workflow = dict en API format; format_detected = 'api' (passthrough) o 'ui_graph' (convertido) o ''. Si falla la lectura/parse, ok=False y error explica."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_import_workflow_json.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_import_workflow_json import comfyui_import_workflow_json
|
||||
|
||||
# Desde un archivo local en API format (passthrough)
|
||||
res = comfyui_import_workflow_json("/tmp/mi_workflow.json")
|
||||
# res == {"ok": True, "workflow": {...}, "format_detected": "api", "error": ""}
|
||||
|
||||
# Desde una URL (descarga + normaliza si viene como UI graph)
|
||||
res2 = comfyui_import_workflow_json("https://raw.githubusercontent.com/user/repo/main/wf.json")
|
||||
# res2["format_detected"] in ("api", "ui_graph")
|
||||
```
|
||||
|
||||
Lánzalo con el python del venv (import de arriba o heredoc). Nota: `./fn run` directo no aplica porque la firma usa `*` (keyword-only), no soportado por el generador de runner de `fn run`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras lanzar un workflow ajeno (de OpenArt, ComfyWorkflows, raw GitHub o
|
||||
un .json local) por la API. Devuelve siempre API format, listo para
|
||||
`comfyui_validate_workflow` + `comfyui_submit_workflow`. Para workflows embebidos
|
||||
en un PNG usa `comfyui_import_workflow_png`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: HTTP GET si `source` es URL, lectura de disco si es path. Lectura/JSON
|
||||
invalido devuelven `{ok: False, error: ...}` (no lanza).
|
||||
- La conversion UI graph -> API es best-effort: las CONEXIONES entre nodos se
|
||||
reconstruyen siempre, pero el mapeo de los valores de widget (steps, cfg, texto)
|
||||
necesita `/object_info` del servidor. Si el servidor esta caido, los widgets del
|
||||
UI graph NO se mapean (quedan fuera) — valida el resultado antes de encolar.
|
||||
- El orden de widgets en object_info se asume = orden de widgets_values del UI
|
||||
graph; nodos custom muy raros pueden desalinearse.
|
||||
- API format se detecta porque todos los valores top-level son dicts con
|
||||
`class_type`; UI graph por la clave `nodes`. Otros JSON dan
|
||||
"formato no reconocido".
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Importa un workflow ComfyUI desde una URL (http/https) o un path local.
|
||||
|
||||
Detecta el formato:
|
||||
- API format: dict {node_id: {class_type, inputs}} -> se devuelve tal cual.
|
||||
- UI graph: dict {nodes, links, ...} (lo que exporta "Save" en la UI) -> se
|
||||
normaliza a API format. La normalizacion de los valores de widget necesita el
|
||||
catalogo /object_info del servidor; si el servidor responde, los widgets se
|
||||
mapean por nombre; si no, solo se conservan las conexiones entre nodos.
|
||||
|
||||
Compone comfyui_object_info para el mapeo de widgets del UI graph.
|
||||
|
||||
Impura: red (HTTP GET si source es URL) + lectura de disco. Solo stdlib.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _THIS_DIR not in sys.path:
|
||||
sys.path.insert(0, _THIS_DIR)
|
||||
|
||||
from comfyui_object_info import comfyui_object_info # noqa: E402
|
||||
|
||||
|
||||
def comfyui_import_workflow_json(
|
||||
source: str,
|
||||
*,
|
||||
server: str = "127.0.0.1:8188",
|
||||
timeout: float = 15.0,
|
||||
) -> dict:
|
||||
"""Lee un workflow JSON y lo normaliza a API format.
|
||||
|
||||
Args:
|
||||
source: URL http(s) de un JSON de workflow, o ruta de un archivo local.
|
||||
server: host:port de ComfyUI usado solo para mapear los widgets cuando
|
||||
la fuente viene en formato UI graph. keyword-only.
|
||||
timeout: timeout HTTP en segundos. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict {ok, workflow, format_detected, error}. format_detected es "api",
|
||||
"ui_graph" o "". Si falla, ok=False y error explica el motivo.
|
||||
"""
|
||||
try:
|
||||
if source.startswith(("http://", "https://")):
|
||||
with urllib.request.urlopen(source, timeout=timeout) as resp:
|
||||
raw = resp.read()
|
||||
else:
|
||||
with open(source, "rb") as f:
|
||||
raw = f.read()
|
||||
data = json.loads(raw)
|
||||
except (urllib.error.URLError, OSError) as exc:
|
||||
return {"ok": False, "workflow": {}, "format_detected": "",
|
||||
"error": f"no se pudo leer {source!r}: {exc}"}
|
||||
except json.JSONDecodeError as exc:
|
||||
return {"ok": False, "workflow": {}, "format_detected": "",
|
||||
"error": f"JSON invalido en {source!r}: {exc}"}
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return {"ok": False, "workflow": {}, "format_detected": "",
|
||||
"error": "el JSON no es un objeto de workflow"}
|
||||
|
||||
# API format: todos los valores son dicts con class_type
|
||||
if data and all(isinstance(v, dict) and "class_type" in v for v in data.values()):
|
||||
return {"ok": True, "workflow": data, "format_detected": "api", "error": ""}
|
||||
|
||||
# UI graph: tiene la clave "nodes"
|
||||
if "nodes" in data:
|
||||
obj_info = None
|
||||
try:
|
||||
obj_info = comfyui_object_info(server=server, timeout=min(timeout, 5.0))
|
||||
except Exception:
|
||||
obj_info = None
|
||||
api = _ui_graph_to_api(data, obj_info)
|
||||
return {"ok": True, "workflow": api, "format_detected": "ui_graph", "error": ""}
|
||||
|
||||
return {"ok": False, "workflow": {}, "format_detected": "",
|
||||
"error": "formato de workflow no reconocido (ni API ni UI graph)"}
|
||||
|
||||
|
||||
def _ui_graph_to_api(graph: dict, obj_info) -> dict:
|
||||
"""Convierte un UI graph de ComfyUI a API format (best-effort)."""
|
||||
nodes = graph.get("nodes", []) or []
|
||||
links = graph.get("links", []) or []
|
||||
# link_id -> (src_node_id, src_slot)
|
||||
link_src = {}
|
||||
for lk in links:
|
||||
if isinstance(lk, list) and len(lk) >= 5:
|
||||
link_src[lk[0]] = (str(lk[1]), lk[2])
|
||||
|
||||
api = {}
|
||||
for node in nodes:
|
||||
ctype = node.get("type")
|
||||
if ctype is None:
|
||||
continue
|
||||
nid = str(node.get("id"))
|
||||
inputs = {}
|
||||
connected = set()
|
||||
for inp in node.get("inputs", []) or []:
|
||||
name = inp.get("name")
|
||||
link = inp.get("link")
|
||||
if name is not None and link is not None and link in link_src:
|
||||
src_node, src_slot = link_src[link]
|
||||
inputs[name] = [src_node, src_slot]
|
||||
connected.add(name)
|
||||
widgets = node.get("widgets_values")
|
||||
if isinstance(widgets, dict):
|
||||
inputs.update(widgets)
|
||||
elif isinstance(widgets, list) and widgets:
|
||||
for name, val in zip(_widget_input_names(ctype, obj_info, connected), widgets):
|
||||
inputs[name] = val
|
||||
api[nid] = {"class_type": ctype, "inputs": inputs}
|
||||
return api
|
||||
|
||||
|
||||
def _widget_input_names(ctype, obj_info, connected) -> list:
|
||||
"""Nombres de inputs que son widgets (no conexiones), en orden, via object_info."""
|
||||
if not obj_info or ctype not in obj_info:
|
||||
return []
|
||||
spec = obj_info[ctype].get("input", {})
|
||||
names = []
|
||||
for section in ("required", "optional"):
|
||||
for name, decl in (spec.get(section) or {}).items():
|
||||
if name in connected:
|
||||
continue
|
||||
t = decl[0] if isinstance(decl, list) and decl else decl
|
||||
if isinstance(t, list):
|
||||
names.append(name) # combo/enum => widget
|
||||
elif t in ("INT", "FLOAT", "STRING", "BOOLEAN"):
|
||||
names.append(name)
|
||||
return names
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
res = comfyui_import_workflow_json("/tmp/does_not_exist.json")
|
||||
print(res)
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: comfyui_import_workflow_png
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_import_workflow_png(png_path_or_url: str, *, timeout: float = 15.0) -> dict"
|
||||
description: "Extrae el workflow embebido en los chunks de texto de un PNG de ComfyUI. Lee el chunk 'prompt' (API format) y/o 'workflow' (UI graph) de los chunks tEXt/zTXt/iTXt con stdlib (struct, zlib). Acepta path local o URL. Impura: red opcional + lectura de disco."
|
||||
tags: [comfyui, ml, import, png, workflow, stable-diffusion]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
params:
|
||||
- name: png_path_or_url
|
||||
desc: "Ruta local de un PNG generado por ComfyUI, o URL http(s) de un PNG (ej. de ComfyUI_examples en GitHub)."
|
||||
- name: timeout
|
||||
desc: "Timeout HTTP en segundos (solo si es URL). keyword-only."
|
||||
output: "dict {ok, prompt, workflow, format_detected, error}. prompt = API format (dict) si existe el chunk 'prompt'; workflow = UI graph (dict) si existe el chunk 'workflow'; format_detected = chunks hallados ('prompt', 'workflow' o 'prompt+workflow'). Si no hay metadata, ok=False."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_import_workflow_png.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_import_workflow_png import comfyui_import_workflow_png
|
||||
|
||||
# Desde un PNG generado localmente
|
||||
res = comfyui_import_workflow_png(os.path.expanduser("~/ComfyUI/output/comfy_00001_.png"))
|
||||
# res["ok"] == True
|
||||
# res["format_detected"] # "prompt" (generado por API) o "prompt+workflow" (desde la UI)
|
||||
# res["prompt"]["3"]["class_type"] == "KSampler"
|
||||
|
||||
# Desde una URL (un PNG de ComfyUI_examples trae el workflow embebido)
|
||||
res2 = comfyui_import_workflow_png("https://raw.githubusercontent.com/comfyanonymous/ComfyUI_examples/master/...png")
|
||||
```
|
||||
|
||||
Lánzalo con el python del venv (import de arriba o heredoc). Nota: `./fn run` directo no aplica porque la firma usa `*` (keyword-only), no soportado por el generador de runner de `fn run`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando alguien te pase una imagen PNG de ComfyUI (de los `ComfyUI_examples`, de la
|
||||
comunidad, o tuya) y quieras recuperar el workflow exacto que la genero para
|
||||
relanzarlo o editarlo. El `prompt` (API format) va directo a
|
||||
`comfyui_validate_workflow` + `comfyui_submit_workflow`; el `workflow` (UI graph)
|
||||
puede cargarse en la UI con `comfyui_load_workflow_ui`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: HTTP GET si es URL, lectura de disco si es path. Errores devuelven
|
||||
`{ok: False, error: ...}` (no lanza).
|
||||
- Solo PNG: lee chunks tEXt/zTXt/iTXt. Los JPG/WebP NO llevan estos chunks (usa
|
||||
otra via). Un PNG sin metadata de ComfyUI da `ok=False`.
|
||||
- Los PNG generados por la API REST solo traen el chunk `prompt`; los generados
|
||||
desde la UI traen ademas `workflow`. Por eso `format_detected` puede ser solo
|
||||
"prompt".
|
||||
- El `prompt` recuperado es API format, no el UI graph: para reabrirlo visualmente
|
||||
usa el `workflow` (si existe) o reconstruye el grafo desde el API format en la UI.
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Extrae el workflow embebido en los chunks de texto de un PNG de ComfyUI.
|
||||
|
||||
ComfyUI guarda en los PNG generados dos chunks de texto:
|
||||
- "prompt": el workflow en API format (lo que se envio a POST /prompt).
|
||||
- "workflow": el grafo de la UI (UI graph), presente si se genero desde la UI.
|
||||
|
||||
Lee chunks tEXt, zTXt e iTXt con stdlib (struct, zlib). Impura: red opcional (si
|
||||
source es URL) + lectura de disco.
|
||||
"""
|
||||
import json
|
||||
import struct
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import zlib
|
||||
|
||||
|
||||
def comfyui_import_workflow_png(png_path_or_url: str, *, timeout: float = 15.0) -> dict:
|
||||
"""Devuelve el/los workflow(s) embebido(s) en un PNG de ComfyUI.
|
||||
|
||||
Args:
|
||||
png_path_or_url: ruta local de un PNG, o URL http(s) de un PNG.
|
||||
timeout: timeout HTTP en segundos (solo si es URL). keyword-only.
|
||||
|
||||
Returns:
|
||||
dict {ok, prompt, workflow, format_detected, error}:
|
||||
- prompt: API format (dict) si el chunk "prompt" existe, si no {}.
|
||||
- workflow: UI graph (dict) si el chunk "workflow" existe, si no {}.
|
||||
- format_detected: chunks hallados unidos por "+" ("prompt",
|
||||
"workflow" o "prompt+workflow").
|
||||
Si el PNG no trae metadata de workflow, ok=False.
|
||||
"""
|
||||
try:
|
||||
if png_path_or_url.startswith(("http://", "https://")):
|
||||
with urllib.request.urlopen(png_path_or_url, timeout=timeout) as resp:
|
||||
data = resp.read()
|
||||
else:
|
||||
with open(png_path_or_url, "rb") as f:
|
||||
data = f.read()
|
||||
except (urllib.error.URLError, OSError) as exc:
|
||||
return {"ok": False, "prompt": {}, "workflow": {}, "format_detected": "",
|
||||
"error": f"no se pudo leer {png_path_or_url!r}: {exc}"}
|
||||
|
||||
try:
|
||||
chunks = _png_text_chunks(data)
|
||||
except ValueError as exc:
|
||||
return {"ok": False, "prompt": {}, "workflow": {}, "format_detected": "",
|
||||
"error": str(exc)}
|
||||
|
||||
out = {"ok": False, "prompt": {}, "workflow": {}, "format_detected": "", "error": ""}
|
||||
found = []
|
||||
if "prompt" in chunks:
|
||||
try:
|
||||
out["prompt"] = json.loads(chunks["prompt"])
|
||||
found.append("prompt")
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
if "workflow" in chunks:
|
||||
try:
|
||||
out["workflow"] = json.loads(chunks["workflow"])
|
||||
found.append("workflow")
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
out["format_detected"] = "+".join(found)
|
||||
if found:
|
||||
out["ok"] = True
|
||||
else:
|
||||
out["error"] = "el PNG no contiene metadata de workflow ComfyUI (chunks prompt/workflow)"
|
||||
return out
|
||||
|
||||
|
||||
def _png_text_chunks(data: bytes) -> dict:
|
||||
"""Lee los chunks de texto (tEXt/zTXt/iTXt) de un PNG -> {keyword: texto}."""
|
||||
if data[:8] != b"\x89PNG\r\n\x1a\n":
|
||||
raise ValueError("no es un PNG valido (firma incorrecta)")
|
||||
out = {}
|
||||
off = 8
|
||||
n = len(data)
|
||||
while off + 8 <= n:
|
||||
length = struct.unpack(">I", data[off:off + 4])[0]
|
||||
ctype = data[off + 4:off + 8]
|
||||
body = data[off + 8:off + 8 + length]
|
||||
off += 12 + length # 4 len + 4 type + body + 4 crc
|
||||
if ctype == b"tEXt":
|
||||
kw, _, txt = body.partition(b"\x00")
|
||||
out[kw.decode("latin1")] = txt.decode("latin1")
|
||||
elif ctype == b"zTXt":
|
||||
kw, _, rest = body.partition(b"\x00")
|
||||
if rest:
|
||||
comp_data = rest[1:] # rest[0] = metodo de compresion
|
||||
try:
|
||||
out[kw.decode("latin1")] = zlib.decompress(comp_data).decode("latin1")
|
||||
except zlib.error:
|
||||
pass
|
||||
elif ctype == b"iTXt":
|
||||
kw, _, rest = body.partition(b"\x00")
|
||||
if len(rest) >= 2:
|
||||
comp_flag = rest[0]
|
||||
parts = rest[2:].split(b"\x00", 2) # lang\x00 translated\x00 text
|
||||
if len(parts) == 3:
|
||||
text_bytes = parts[2]
|
||||
if comp_flag == 1:
|
||||
try:
|
||||
text_bytes = zlib.decompress(text_bytes)
|
||||
except zlib.error:
|
||||
text_bytes = b""
|
||||
out[kw.decode("latin1")] = text_bytes.decode("utf-8", "replace")
|
||||
elif ctype == b"IEND":
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json as _json
|
||||
import sys
|
||||
|
||||
path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/missing.png"
|
||||
res = comfyui_import_workflow_png(path)
|
||||
print(_json.dumps({k: v for k, v in res.items() if k != "prompt"}, indent=2))
|
||||
print("nodos en prompt:", len(res["prompt"]))
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: comfyui_inject_lora
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_inject_lora(workflow: dict, lora_name: str, *, strength_model: float = 1.0, strength_clip: float = 1.0, model_node: str | None = None, clip_node: str | None = None) -> dict"
|
||||
description: "Inserta un nodo LoraLoader en un workflow ComfyUI ya construido (API format), reconectando las salidas model/clip de la fuente actual (CheckpointLoaderSimple o LoraLoader previo) hacia el LoRA y repuntando a los consumidores (KSampler, CLIPTextEncode). Llamar varias veces encadena LoRAs. Pura: no muta el dict de entrada (copia profunda)."
|
||||
tags: [comfyui, ml, lora, stable-diffusion, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: workflow
|
||||
desc: "dict en API format (ej. salida de comfyui_build_txt2img_workflow). No se muta; se devuelve una copia."
|
||||
- name: lora_name
|
||||
desc: "Nombre del archivo .safetensors del LoRA en models/loras/."
|
||||
- name: strength_model
|
||||
desc: "Fuerza del LoRA sobre el modelo (UNet). keyword-only."
|
||||
- name: strength_clip
|
||||
desc: "Fuerza del LoRA sobre el CLIP. keyword-only."
|
||||
- name: model_node
|
||||
desc: "node_id cuya salida MODEL (slot 0) alimentara el LoRA. Si None, se detecta la fuente que hoy alimenta KSampler.model (con el CheckpointLoaderSimple como fallback). keyword-only."
|
||||
- name: clip_node
|
||||
desc: "node_id cuya salida CLIP (slot 1) alimentara el LoRA. Si None, se detecta la fuente que hoy alimenta los CLIPTextEncode.clip. keyword-only."
|
||||
output: "copia del workflow con un nodo LoraLoader insertado (node_id = max id numerico + 1) y reconectado entre la fuente model/clip y sus consumidores."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_inject_lora.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
from ml.comfyui_inject_lora import comfyui_inject_lora
|
||||
|
||||
base = comfyui_build_txt2img_workflow("dreamshaper_8.safetensors", "a cat, detailed")
|
||||
wf = comfyui_inject_lora(base, "add_detail.safetensors", strength_model=0.8)
|
||||
# El LoraLoader nuevo recibe model/clip del checkpoint ["4",0]/["4",1]
|
||||
# y ahora KSampler.model == [lora_id, 0], CLIPTextEncode.clip == [lora_id, 1]
|
||||
|
||||
# Encadenar un segundo LoRA: el detector ve que ya pasa por el primero
|
||||
wf = comfyui_inject_lora(wf, "anime_style.safetensors", strength_model=0.6)
|
||||
# Cadena: checkpoint -> lora1 -> lora2 -> KSampler / CLIPTextEncode
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando tengas un workflow txt2img/img2img construido y quieras aplicarle uno o
|
||||
varios LoRAs sin reescribir el grafo. Llama una vez por LoRA: cada llamada inserta
|
||||
el LoraLoader justo antes de los consumidores actuales, asi que encadenar es
|
||||
idempotente respecto al orden de llamada. Para apilar muchos LoRAs, encadena.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Pura: no muta el `workflow` de entrada (trabaja sobre una copia profunda) y NO
|
||||
valida que `lora_name` exista en el servidor. Valida con `comfyui_validate_workflow`.
|
||||
- Asume la convencion de slots de ComfyUI: MODEL=output 0, CLIP=output 1, tanto
|
||||
en CheckpointLoaderSimple como en LoraLoader. Workflows con loaders no estandar
|
||||
pueden necesitar `model_node`/`clip_node` explicitos.
|
||||
- Detecta la fuente actual por el KSampler.model y el primer CLIPTextEncode.clip.
|
||||
Si el workflow no tiene un nodo cuyo class_type acabe en "KSampler", pasa
|
||||
`model_node` explicito o lanza ValueError.
|
||||
- El nuevo node_id es `max(ids numericos) + 1`. Si tu workflow usa ids no
|
||||
numericos, el contador cae a `len(workflow) + 1`.
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Inserta un nodo LoraLoader en un workflow ComfyUI ya construido (API format).
|
||||
|
||||
Reconecta las salidas model/clip de la fuente actual (el CheckpointLoaderSimple
|
||||
o un LoraLoader previo) hacia el nuevo LoraLoader, y repunta a los consumidores
|
||||
(KSampler, CLIPTextEncode) para que pasen por el LoRA. Llamar varias veces sobre
|
||||
el mismo workflow encadena LoRAs.
|
||||
|
||||
Convencion de slots ComfyUI: tanto CheckpointLoaderSimple como LoraLoader
|
||||
exponen MODEL en el output 0 y CLIP en el output 1.
|
||||
|
||||
Funcion pura: no muta el dict de entrada (trabaja sobre una copia profunda).
|
||||
"""
|
||||
import copy
|
||||
|
||||
|
||||
def comfyui_inject_lora(
|
||||
workflow: dict,
|
||||
lora_name: str,
|
||||
*,
|
||||
strength_model: float = 1.0,
|
||||
strength_clip: float = 1.0,
|
||||
model_node: str | None = None,
|
||||
clip_node: str | None = None,
|
||||
) -> dict:
|
||||
"""Devuelve una copia del workflow con un LoraLoader insertado y reconectado.
|
||||
|
||||
Args:
|
||||
workflow: dict en API format (ej. salida de
|
||||
comfyui_build_txt2img_workflow). No se muta.
|
||||
lora_name: nombre del archivo .safetensors del LoRA en models/loras/.
|
||||
strength_model: fuerza del LoRA sobre el modelo (UNet). keyword-only.
|
||||
strength_clip: fuerza del LoRA sobre el CLIP. keyword-only.
|
||||
model_node: node_id cuya salida MODEL (slot 0) alimentara el LoRA. Si
|
||||
None, se detecta la fuente que hoy alimenta el KSampler.model (con el
|
||||
CheckpointLoaderSimple como fallback). keyword-only.
|
||||
clip_node: node_id cuya salida CLIP (slot 1) alimentara el LoRA. Si None,
|
||||
se detecta la fuente que hoy alimenta los CLIPTextEncode.clip.
|
||||
keyword-only.
|
||||
|
||||
Returns:
|
||||
copia del workflow con el LoraLoader insertado. El nuevo node_id es el
|
||||
maximo id numerico existente + 1.
|
||||
|
||||
Raises:
|
||||
ValueError: si no se puede determinar la fuente model/clip y no se pasan
|
||||
model_node/clip_node explicitos.
|
||||
"""
|
||||
wf = copy.deepcopy(workflow)
|
||||
|
||||
def _is_link(v) -> bool:
|
||||
return (
|
||||
isinstance(v, list)
|
||||
and len(v) == 2
|
||||
and isinstance(v[0], str)
|
||||
and isinstance(v[1], int)
|
||||
)
|
||||
|
||||
def _find_class(prefix):
|
||||
for nid, node in wf.items():
|
||||
if str(node.get("class_type", "")).startswith(prefix):
|
||||
return nid
|
||||
return None
|
||||
|
||||
ckpt = _find_class("CheckpointLoader")
|
||||
|
||||
# fuente actual de model/clip: la que alimenta KSampler.model y CLIPTextEncode.clip
|
||||
model_src = None
|
||||
clip_src = None
|
||||
for node in wf.values():
|
||||
ins = node.get("inputs", {})
|
||||
if str(node.get("class_type", "")).endswith("KSampler") and _is_link(ins.get("model")):
|
||||
model_src = list(ins["model"])
|
||||
if node.get("class_type") == "CLIPTextEncode" and clip_src is None and _is_link(ins.get("clip")):
|
||||
clip_src = list(ins["clip"])
|
||||
|
||||
if model_node is not None:
|
||||
model_src = [model_node, 0]
|
||||
elif model_src is None and ckpt is not None:
|
||||
model_src = [ckpt, 0]
|
||||
|
||||
if clip_node is not None:
|
||||
clip_src = [clip_node, 1]
|
||||
elif clip_src is None and ckpt is not None:
|
||||
clip_src = [ckpt, 1]
|
||||
|
||||
if model_src is None or clip_src is None:
|
||||
raise ValueError(
|
||||
"comfyui_inject_lora: no se pudo determinar la fuente model/clip; "
|
||||
"pasa model_node y clip_node explicitos."
|
||||
)
|
||||
|
||||
numeric = [int(k) for k in wf.keys() if str(k).isdigit()]
|
||||
new_id = str((max(numeric) + 1) if numeric else len(wf) + 1)
|
||||
|
||||
wf[new_id] = {
|
||||
"class_type": "LoraLoader",
|
||||
"inputs": {
|
||||
"lora_name": lora_name,
|
||||
"strength_model": strength_model,
|
||||
"strength_clip": strength_clip,
|
||||
"model": list(model_src),
|
||||
"clip": list(clip_src),
|
||||
},
|
||||
}
|
||||
|
||||
# repuntar consumidores de model_src/clip_src hacia el LoraLoader (no el propio LoRA)
|
||||
for nid, node in wf.items():
|
||||
if nid == new_id:
|
||||
continue
|
||||
ins = node.get("inputs", {})
|
||||
for k, v in list(ins.items()):
|
||||
if _is_link(v) and list(v) == list(model_src):
|
||||
ins[k] = [new_id, 0]
|
||||
elif _is_link(v) and list(v) == list(clip_src):
|
||||
ins[k] = [new_id, 1]
|
||||
|
||||
return wf
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
|
||||
base = comfyui_build_txt2img_workflow("dreamshaper_8.safetensors", "a cat")
|
||||
wf = comfyui_inject_lora(base, "add_detail.safetensors", strength_model=0.8)
|
||||
print(json.dumps(wf, indent=2))
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: comfyui_install_3d_model
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_install_3d_model(variant: str = \"mini\", *, hf_token: str | None = None, comfyui_dir: str = \"~/ComfyUI\") -> dict"
|
||||
description: "Instala un checkpoint Hunyuan3D-2 (mini/standard/mv) en la carpeta checkpoints/ de ComfyUI para los nodos nativos imagen->3D (ImageOnlyCheckpointLoader). Cascada: si el destino ya existe reutiliza; si esta en la cache de HuggingFace copia desde ahi (sin red); si no, descarga con huggingface_hub (token de pass si gated). Resuelve la ruta real de checkpoints via extra_model_paths.yaml. Impura: YAML + disco + posible red + subprocess pass."
|
||||
tags: [comfyui, ml, img-to-3d, hunyuan3d, model, install]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
params:
|
||||
- name: variant
|
||||
desc: "'mini' (≈5 GB VRAM, default), 'standard' (dit-v2-0, ≈6 GB) o 'mv' (multiview). Determina el repo de HF y el nombre destino del .safetensors."
|
||||
- name: hf_token
|
||||
desc: "Token de HuggingFace si la variante fuera gated. Si None y hace falta descargar, se intenta leer de 'pass show API_TOKEN_huggingFace'. keyword-only."
|
||||
- name: comfyui_dir
|
||||
desc: "Raiz de la instalacion de ComfyUI (se expande ~). La carpeta real de checkpoints se resuelve via extra_model_paths.yaml. keyword-only."
|
||||
output: "dict {ok, path, bytes, reused_cache, error}. path = ruta del checkpoint en checkpoints/; reused_cache=True si ya estaba instalado o se copio de la cache de HF (sin descarga de red). Si falla, ok=False y error explica."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_install_3d_model.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_install_3d_model import comfyui_install_3d_model
|
||||
|
||||
res = comfyui_install_3d_model("mini")
|
||||
# Si ya esta (cache o instalado): reused_cache=True, sin re-bajar 3.8 GB.
|
||||
# res == {"ok": True, "path": "/mnt/2tb/comfyui_models/checkpoints/hunyuan3d-dit-v2-mini.safetensors",
|
||||
# "bytes": 3819958234, "reused_cache": True, "error": ""}
|
||||
```
|
||||
|
||||
Lánzalo con el python del venv (import de arriba o heredoc). Nota: `./fn run` directo no aplica porque la firma usa `*` (keyword-only), no soportado por el generador de runner de `fn run`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Antes de reconstruir mallas 3D con los nodos nativos de Hunyuan3D-2: asegura que
|
||||
el checkpoint que pide `ImageOnlyCheckpointLoader` esta en `checkpoints/`. Llamala
|
||||
una vez por PC/variante; en sucesivas devuelve `reused_cache=True` al instante. El
|
||||
pipeline `comfyui_image_to_3d_oneshot` NO la llama (asume el modelo ya instalado);
|
||||
ejecutala tu antes la primera vez.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: lee YAML, escribe en disco (copia de GBs cuando toca), y puede hacer red
|
||||
+ subprocess `pass`. La copia desde la cache de HF de un .safetensors de ~3.8 GB
|
||||
tarda unos segundos; el caso `reused_cache` ya-instalado es instantaneo.
|
||||
- Resuelve la carpeta de checkpoints real via `extra_model_paths.yaml` (en este
|
||||
equipo `/mnt/2tb/comfyui_models/checkpoints/`, seccion `is_default`). Si el YAML
|
||||
falta cae a `<comfyui_dir>/models/checkpoints`.
|
||||
- La descarga (rama 3) necesita `huggingface_hub` en el venv. Si no esta instalado
|
||||
y el modelo no esta en la cache, devuelve `ok=False` con instrucciones (instalar
|
||||
huggingface_hub o usar `comfyui_download_model` con la URL de resolve de HF).
|
||||
- Hunyuan3D-2 mini NO es gated (no requiere token). `standard`/`mv` se asumen
|
||||
publicos tambien; si alguno fuera gated, pasa `hf_token` o ten el token en `pass`.
|
||||
- Tras instalar, ComfyUI re-escanea `checkpoints/` dinamicamente (no hace falta
|
||||
reiniciar el server para checkpoints; solo los custom nodes nuevos exigen restart).
|
||||
- No valida el contenido del .safetensors mas alla de un tamano minimo; confia en
|
||||
la integridad de la cache de HF o de la descarga de huggingface_hub.
|
||||
@@ -0,0 +1,189 @@
|
||||
"""Instala un checkpoint Hunyuan3D-2 en la carpeta checkpoints/ de ComfyUI.
|
||||
|
||||
ComfyUI 0.26.0 reconstruye mallas 3D con los nodos nativos de Hunyuan3D-2, que
|
||||
cargan un checkpoint self-contained (DiT de forma + VAE 3D + encoder de imagen en
|
||||
un solo .safetensors) via ImageOnlyCheckpointLoader. Esta funcion resuelve el repo
|
||||
de HuggingFace de la variante pedida, REUTILIZA la cache de HF si ya esta bajado
|
||||
(sin re-descargar), y copia el .safetensors a la carpeta checkpoints/ (la ruta real
|
||||
que declara extra_model_paths.yaml) con el nombre que espera el loader nativo.
|
||||
|
||||
Cascada: (1) si el destino ya existe -> reutiliza; (2) si esta en la cache de HF
|
||||
-> copia desde la cache; (3) si no -> descarga con huggingface_hub (token de
|
||||
`pass` si la variante fuera gated).
|
||||
|
||||
Impura: lectura de YAML, escritura en disco, posible red (HTTP) y subprocess (pass).
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
# variant -> (repo_id HF, ruta del archivo dentro del repo, nombre destino en checkpoints/)
|
||||
_VARIANTS = {
|
||||
"mini": (
|
||||
"tencent/Hunyuan3D-2mini",
|
||||
"hunyuan3d-dit-v2-mini/model.fp16.safetensors",
|
||||
"hunyuan3d-dit-v2-mini.safetensors",
|
||||
),
|
||||
"standard": (
|
||||
"tencent/Hunyuan3D-2",
|
||||
"hunyuan3d-dit-v2-0/model.fp16.safetensors",
|
||||
"hunyuan3d-dit-v2-0.safetensors",
|
||||
),
|
||||
"mv": (
|
||||
"tencent/Hunyuan3D-2mv",
|
||||
"hunyuan3d-dit-v2-mv/model.fp16.safetensors",
|
||||
"hunyuan3d-dit-v2-mv.safetensors",
|
||||
),
|
||||
}
|
||||
|
||||
_MIN_BYTES = 1_000_000 # un .safetensors real pesa GBs; descarta restos/HTML.
|
||||
|
||||
|
||||
def _checkpoints_dir(comfyui_dir: str) -> str:
|
||||
"""Resuelve el directorio real de checkpoints de ComfyUI.
|
||||
|
||||
Lee extra_model_paths.yaml (prefiere la seccion con is_default) para devolver
|
||||
`<base_path>/<checkpoints_subdir>`. Si el YAML no existe o no se puede parsear,
|
||||
cae a la ruta nativa `<comfyui_dir>/models/checkpoints`.
|
||||
"""
|
||||
base = os.path.expanduser(comfyui_dir)
|
||||
native = os.path.join(base, "models", "checkpoints")
|
||||
yml = os.path.join(base, "extra_model_paths.yaml")
|
||||
if not os.path.isfile(yml):
|
||||
return native
|
||||
try:
|
||||
import yaml
|
||||
with open(yml, encoding="utf-8") as fh:
|
||||
data = yaml.safe_load(fh) or {}
|
||||
except Exception: # noqa: BLE001 — YAML/PyYAML no disponible: usar nativa.
|
||||
return native
|
||||
if not isinstance(data, dict):
|
||||
return native
|
||||
fallback = None
|
||||
for section in data.values():
|
||||
if not isinstance(section, dict):
|
||||
continue
|
||||
sub = section.get("checkpoints")
|
||||
if not sub:
|
||||
continue
|
||||
bp = os.path.expanduser(str(section.get("base_path", "")))
|
||||
first_line = str(sub).splitlines()[0].strip()
|
||||
resolved = os.path.join(bp, first_line)
|
||||
if section.get("is_default"):
|
||||
return resolved
|
||||
if fallback is None:
|
||||
fallback = resolved
|
||||
return fallback or native
|
||||
|
||||
|
||||
def _find_in_hf_cache(repo_id: str, repo_filename: str) -> str | None:
|
||||
"""Busca el archivo en la cache local de HuggingFace, sin red.
|
||||
|
||||
Layout: ~/.cache/huggingface/hub/models--<org>--<name>/snapshots/<hash>/...
|
||||
Resuelve el symlink al blob real y verifica un tamano minimo. Devuelve la ruta
|
||||
real o None.
|
||||
"""
|
||||
org_name = repo_id.replace("/", "--")
|
||||
hub = os.path.expanduser("~/.cache/huggingface/hub")
|
||||
cache_root = os.path.join(hub, f"models--{org_name}", "snapshots")
|
||||
if not os.path.isdir(cache_root):
|
||||
return None
|
||||
target = os.path.basename(repo_filename)
|
||||
for snap in os.listdir(cache_root):
|
||||
snap_dir = os.path.join(cache_root, snap)
|
||||
if not os.path.isdir(snap_dir):
|
||||
continue
|
||||
for root, _dirs, files in os.walk(snap_dir):
|
||||
if target in files:
|
||||
real = os.path.realpath(os.path.join(root, target))
|
||||
if os.path.isfile(real) and os.path.getsize(real) >= _MIN_BYTES:
|
||||
return real
|
||||
return None
|
||||
|
||||
|
||||
def _pass_hf_token() -> str | None:
|
||||
"""Lee el token de HuggingFace de `pass API_TOKEN_huggingFace`, o None."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["pass", "show", "API_TOKEN_huggingFace"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if out.returncode == 0:
|
||||
tok = out.stdout.splitlines()[0].strip() if out.stdout.strip() else ""
|
||||
return tok or None
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def comfyui_install_3d_model(
|
||||
variant: str = "mini",
|
||||
*,
|
||||
hf_token: str | None = None,
|
||||
comfyui_dir: str = "~/ComfyUI",
|
||||
) -> dict:
|
||||
"""Instala el checkpoint Hunyuan3D-2 de la variante pedida en checkpoints/.
|
||||
|
||||
Args:
|
||||
variant: "mini" (≈5 GB VRAM, default), "standard" (dit-v2-0, ≈6 GB) o
|
||||
"mv" (multiview). Determina el repo de HF y el nombre destino.
|
||||
hf_token: token de HuggingFace si la variante fuera gated. Si None y hace
|
||||
falta descargar, se intenta leer de `pass show API_TOKEN_huggingFace`.
|
||||
keyword-only.
|
||||
comfyui_dir: raiz de la instalacion de ComfyUI (se expande ~). La carpeta
|
||||
real de checkpoints se resuelve via extra_model_paths.yaml. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict {ok, path, bytes, reused_cache, error}. path = ruta del checkpoint en
|
||||
checkpoints/; reused_cache=True si ya estaba instalado o se copio de la
|
||||
cache de HF (sin descarga de red). Si falla, ok=False y error explica.
|
||||
"""
|
||||
if variant not in _VARIANTS:
|
||||
return {"ok": False, "path": "", "bytes": 0, "reused_cache": False,
|
||||
"error": f"variant {variant!r} no valida; usa {sorted(_VARIANTS)}"}
|
||||
repo_id, repo_filename, dest_name = _VARIANTS[variant]
|
||||
ckpt_dir = _checkpoints_dir(comfyui_dir)
|
||||
dest = os.path.join(ckpt_dir, dest_name)
|
||||
|
||||
# 1. Ya instalado.
|
||||
if os.path.isfile(dest) and os.path.getsize(dest) >= _MIN_BYTES:
|
||||
return {"ok": True, "path": dest, "bytes": os.path.getsize(dest),
|
||||
"reused_cache": True, "error": ""}
|
||||
|
||||
# 2. En la cache de HF -> copiar (sin red).
|
||||
cached = _find_in_hf_cache(repo_id, repo_filename)
|
||||
if cached:
|
||||
try:
|
||||
os.makedirs(ckpt_dir, exist_ok=True)
|
||||
shutil.copy2(cached, dest)
|
||||
except OSError as exc:
|
||||
return {"ok": False, "path": "", "bytes": 0, "reused_cache": False,
|
||||
"error": f"no se pudo copiar de la cache HF a {dest}: {exc}"}
|
||||
return {"ok": True, "path": dest, "bytes": os.path.getsize(dest),
|
||||
"reused_cache": True, "error": ""}
|
||||
|
||||
# 3. Descargar con huggingface_hub (lazy; usa su propia cache).
|
||||
token = hf_token or _pass_hf_token()
|
||||
try:
|
||||
from huggingface_hub import hf_hub_download
|
||||
except ImportError:
|
||||
return {"ok": False, "path": "", "bytes": 0, "reused_cache": False,
|
||||
"error": ("no esta en la cache de HF y huggingface_hub no esta "
|
||||
"instalado en este venv. Instala huggingface_hub o baja "
|
||||
f"el archivo {repo_filename!r} de {repo_id!r} a mano (o con "
|
||||
"comfyui_download_model usando la URL de resolve de HF).")}
|
||||
try:
|
||||
local = hf_hub_download(repo_id=repo_id, filename=repo_filename, token=token)
|
||||
os.makedirs(ckpt_dir, exist_ok=True)
|
||||
shutil.copy2(local, dest)
|
||||
except Exception as exc: # noqa: BLE001 — red/auth/gated/escritura.
|
||||
return {"ok": False, "path": "", "bytes": 0, "reused_cache": False,
|
||||
"error": f"fallo descargando {repo_filename} de {repo_id}: {exc}"}
|
||||
return {"ok": True, "path": dest, "bytes": os.path.getsize(dest),
|
||||
"reused_cache": False, "error": ""}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
print(json.dumps(comfyui_install_3d_model("mini"), ensure_ascii=False, indent=2))
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
name: comfyui_install_custom_node
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_install_custom_node(repo_url: str, *, comfyui_dir: str = \"~/ComfyUI\", pip_install: bool = True, restart: bool = False) -> dict"
|
||||
description: "Instala un custom node de ComfyUI: git clone del repo en custom_nodes/<name> + (si trae requirements.txt) pip install de sus deps en el venv de ComfyUI. El venv suele crearse con uv y no trae pip, asi que el instalador se autodetecta (python -m pip -> uv pip -> pip). NO reinicia el servidor por defecto (restart=False): el nodo se carga al siguiente arranque. Impura: subprocess git/pip/uv + escritura en disco. Solo stdlib."
|
||||
tags: [comfyui, ml, custom-nodes, install, git, pip, stable-diffusion]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["os", "shutil", "subprocess"]
|
||||
params:
|
||||
- name: repo_url
|
||||
desc: "URL del repositorio git del custom node (ej. 'https://github.com/rgthree/rgthree-comfy')."
|
||||
- name: comfyui_dir
|
||||
desc: "Raiz de la instalacion de ComfyUI (se expande ~). Default '~/ComfyUI'. keyword-only."
|
||||
- name: pip_install
|
||||
desc: "Si True y el repo trae requirements.txt, instala sus dependencias en el venv de ComfyUI. keyword-only."
|
||||
- name: restart
|
||||
desc: "NO soportado de forma segura (default False). El nodo se carga al reiniciar el servidor; hazlo tu cuando no haya generaciones en curso. True solo se anota en error, NO reinicia (evita cortar trabajo del servidor). keyword-only."
|
||||
output: "dict {ok, path, pip_done, error}. ok=True si el nodo quedo clonado en disco (o ya estaba). pip_done=True si se instalaron las dependencias. error describe el fallo de git/pip o las advertencias (ya existia, sin requirements, restart ignorado)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_install_custom_node.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_install_custom_node import comfyui_install_custom_node
|
||||
|
||||
out = comfyui_install_custom_node(
|
||||
"https://github.com/rgthree/rgthree-comfy",
|
||||
restart=False, # no reinicia el server; el nodo se carga al proximo arranque
|
||||
)
|
||||
print(out["ok"], out["path"], "pip_done=", out["pip_done"])
|
||||
# {"ok": True, "path": ".../custom_nodes/rgthree-comfy", "pip_done": True, "error": ""}
|
||||
```
|
||||
|
||||
El `./fn run` directo no aplica (firma con `*` keyword-only); usa el import o un
|
||||
heredoc.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando un workflow ajeno usa un nodo custom que no tienes
|
||||
(`comfyui_resolve_workflow_deps` te dice cual falta) o quieras anadir un pack de
|
||||
nodos conocido. Tras instalar, reinicia ComfyUI manualmente (cuando no haya
|
||||
generaciones en curso) para que el nodo aparezca.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: ejecuta `git clone` y, si hay requirements.txt, `pip`/`uv pip` en el
|
||||
venv de ComfyUI; escribe en `~/ComfyUI/custom_nodes/`.
|
||||
- **NO reinicia el servidor**. `restart=True` se ignora (solo se anota en `error`):
|
||||
un restart en caliente corta cualquier generacion en curso. Reinicia tu cuando
|
||||
el server este libre. El nodo NO se carga hasta ese reinicio.
|
||||
- El venv de ComfyUI creado con uv no trae `pip`: la funcion detecta el instalador
|
||||
(`python -m pip` -> `uv pip --python <venv>` -> binario `pip`). Si no hay ninguno,
|
||||
`pip_done=False` y lo anota en `error` (el clone sigue siendo valido).
|
||||
- Idempotente con el clone: si el dir ya existe NO re-clona (lo anota en `error`),
|
||||
pero SI reintenta el pip install si `pip_install=True`.
|
||||
- `ok=True` significa "clonado en disco", no "cargado en el server". Un clone OK
|
||||
con pip fallido devuelve `ok=True, pip_done=False` + el error de pip.
|
||||
- Un repo_url invalido (404) devuelve `ok=False` con el stderr de git.
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Instala un custom node de ComfyUI: git clone + pip install de sus deps.
|
||||
|
||||
Clona el repo en `<comfyui_dir>/custom_nodes/<name>` y, si trae
|
||||
`requirements.txt`, instala sus dependencias en el venv de ComfyUI
|
||||
(`<comfyui_dir>/.venv`). El venv de ComfyUI suele crearse con uv y no trae pip;
|
||||
por eso el instalador se autodetecta en orden: `python -m pip`, luego
|
||||
`uv pip --python <venv>`, luego el binario `pip` del venv. NO reinicia el
|
||||
servidor por defecto (restart=False): el nodo no se carga hasta el siguiente
|
||||
arranque de ComfyUI, asi que reiniciar es una decision explicita del caller (un
|
||||
restart en caliente corta cualquier generacion en curso).
|
||||
|
||||
Impura: ejecuta subprocess (git, pip/uv) y escribe en disco. Solo stdlib.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
_GIT_TIMEOUT = 300.0
|
||||
_PIP_TIMEOUT = 600.0
|
||||
|
||||
|
||||
def _pip_install_cmd(base: str, req: str):
|
||||
"""Comando para instalar requirements en el venv de ComfyUI, o None.
|
||||
|
||||
Prueba en orden: `python -m pip` (si el venv tiene pip), `uv pip` apuntando
|
||||
al python del venv (venvs uv sin pip), y por ultimo el binario `pip` del
|
||||
venv. Devuelve la lista de args lista para subprocess o None si no hay
|
||||
instalador disponible.
|
||||
"""
|
||||
venv_py = os.path.join(base, ".venv", "bin", "python")
|
||||
if os.path.isfile(venv_py):
|
||||
probe = subprocess.run(
|
||||
[venv_py, "-m", "pip", "--version"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if probe.returncode == 0:
|
||||
return [venv_py, "-m", "pip", "install", "-r", req]
|
||||
if shutil.which("uv"):
|
||||
return ["uv", "pip", "install", "-r", req, "--python", venv_py]
|
||||
for cand in ("pip", "pip3"):
|
||||
pip_bin = os.path.join(base, ".venv", "bin", cand)
|
||||
if os.path.isfile(pip_bin):
|
||||
return [pip_bin, "install", "-r", req]
|
||||
return None
|
||||
|
||||
|
||||
def comfyui_install_custom_node(
|
||||
repo_url: str,
|
||||
*,
|
||||
comfyui_dir: str = "~/ComfyUI",
|
||||
pip_install: bool = True,
|
||||
restart: bool = False,
|
||||
) -> dict:
|
||||
"""Clona un custom node y (opcional) instala sus requirements.
|
||||
|
||||
Args:
|
||||
repo_url: URL del repositorio git del custom node
|
||||
(ej. "https://github.com/rgthree/rgthree-comfy").
|
||||
comfyui_dir: raiz de la instalacion de ComfyUI (se expande ~).
|
||||
keyword-only.
|
||||
pip_install: si True y el repo trae requirements.txt, instala sus
|
||||
dependencias en el venv de ComfyUI. keyword-only.
|
||||
restart: NO soportado de forma segura desde aqui (por defecto False).
|
||||
El nodo se carga al reiniciar el servidor; hazlo tu cuando no haya
|
||||
generaciones en curso. Si se pasa True se anota en el error pero NO
|
||||
se reinicia (evita cortar trabajo del servidor). keyword-only.
|
||||
|
||||
Returns:
|
||||
dict {ok, path, pip_done, error}. ok=True si el nodo quedo clonado en
|
||||
disco (o ya estaba). pip_done=True si se instalaron las dependencias.
|
||||
error describe el fallo de git/pip o la advertencia de restart.
|
||||
"""
|
||||
base = os.path.expanduser(comfyui_dir)
|
||||
custom_dir = os.path.join(base, "custom_nodes")
|
||||
name = os.path.basename(repo_url.rstrip("/"))
|
||||
if name.endswith(".git"):
|
||||
name = name[:-4]
|
||||
if not name:
|
||||
return {"ok": False, "path": "", "pip_done": False,
|
||||
"error": f"repo_url invalido: {repo_url!r}"}
|
||||
|
||||
dest = os.path.join(custom_dir, name)
|
||||
already = os.path.isdir(dest)
|
||||
if not already:
|
||||
os.makedirs(custom_dir, exist_ok=True)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["git", "clone", "--depth", "1", repo_url, dest],
|
||||
capture_output=True, text=True, timeout=_GIT_TIMEOUT,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, OSError) as exc:
|
||||
return {"ok": False, "path": "", "pip_done": False,
|
||||
"error": f"git clone fallo: {exc}"}
|
||||
if proc.returncode != 0:
|
||||
return {"ok": False, "path": "", "pip_done": False,
|
||||
"error": f"git clone fallo ({proc.returncode}): {proc.stderr.strip()[:300]}"}
|
||||
|
||||
notes = []
|
||||
if already:
|
||||
notes.append(f"ya existia en {dest} (no se re-clono)")
|
||||
|
||||
pip_done = False
|
||||
if pip_install:
|
||||
req = os.path.join(dest, "requirements.txt")
|
||||
if os.path.isfile(req):
|
||||
cmd = _pip_install_cmd(base, req)
|
||||
if cmd is None:
|
||||
notes.append(f"no se encontro instalador (pip/uv) para {base}/.venv (deps omitidas)")
|
||||
else:
|
||||
try:
|
||||
pproc = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=_PIP_TIMEOUT,
|
||||
)
|
||||
pip_done = pproc.returncode == 0
|
||||
if not pip_done:
|
||||
notes.append(f"pip install fallo: {pproc.stderr.strip()[:300]}")
|
||||
except (subprocess.TimeoutExpired, OSError) as exc:
|
||||
notes.append(f"pip install fallo: {exc}")
|
||||
else:
|
||||
notes.append("sin requirements.txt (nada que instalar)")
|
||||
|
||||
if restart:
|
||||
notes.append(
|
||||
"restart=True ignorado: reinicia ComfyUI manualmente cuando no haya "
|
||||
"generaciones en curso para cargar el nodo"
|
||||
)
|
||||
|
||||
return {"ok": True, "path": dest, "pip_done": pip_done, "error": "; ".join(notes)}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
out = comfyui_install_custom_node(
|
||||
"https://github.com/rgthree/rgthree-comfy", restart=False,
|
||||
)
|
||||
print(json.dumps(out, ensure_ascii=False, indent=2))
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: comfyui_list_installed_models
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_list_installed_models(folder: str | None = None, comfyui_dir: str = \"~/ComfyUI\") -> dict"
|
||||
description: "Lista los modelos instalados de ComfyUI por carpeta de tipo (checkpoints, loras, vae, controlnet, upscale_models), resolviendo las rutas REALES: escanea tanto la nativa <comfyui_dir>/models/<folder>/ como las externas declaradas en extra_model_paths.yaml (en este equipo /mnt/2tb/comfyui_models/). Escaneo de FS (no depende del servidor). Impura: lectura de disco + parse de YAML. Solo stdlib + PyYAML."
|
||||
tags: [comfyui, ml, models, inventory, filesystem, stable-diffusion]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["os", "yaml"]
|
||||
params:
|
||||
- name: folder
|
||||
desc: "Carpeta concreta a listar (ej. 'checkpoints'). Si None, lista todas (checkpoints, loras, vae, controlnet, upscale_models)."
|
||||
- name: comfyui_dir
|
||||
desc: "Raiz de la instalacion de ComfyUI (se expande ~). Default '~/ComfyUI'."
|
||||
output: "dict {ok, models, error}. models = {folder: [nombre, ...]} con los archivos de modelo (dedup por nombre) hallados en la ruta nativa models/<folder>/ y en las externas de extra_model_paths.yaml. ok=True salvo fallo de escaneo."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_list_installed_models.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_list_installed_models import comfyui_list_installed_models
|
||||
|
||||
out = comfyui_list_installed_models()
|
||||
print(out["models"]["checkpoints"])
|
||||
# ['dreamshaper_8.safetensors', 'juggernaut_xl_v11.safetensors', 'v1-5-pruned-emaonly-fp16.safetensors', ...]
|
||||
# -> resueltos desde /mnt/2tb/comfyui_models/checkpoints/ via extra_model_paths.yaml
|
||||
|
||||
# Una sola carpeta:
|
||||
loras = comfyui_list_installed_models(folder="loras")["models"]["loras"]
|
||||
```
|
||||
|
||||
El bloque se lanza con el python del venv (`python/.venv/bin/python3`).
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites saber que checkpoints/LoRAs/VAEs/ControlNets/upscalers tienes ya
|
||||
descargados antes de construir un workflow (los builders necesitan el nombre exacto
|
||||
del modelo) o antes de descargar uno nuevo. Ve los modelos esten en `models/` o en
|
||||
el disco externo de `extra_model_paths.yaml`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: lee disco y parsea `extra_model_paths.yaml`. NO consulta el servidor de
|
||||
ComfyUI, asi que funciona aunque el server este reiniciandose.
|
||||
- **Resuelve la ruta REAL**: en este equipo los modelos viven en
|
||||
`/mnt/2tb/comfyui_models/` (no en `~/ComfyUI/models/`), declarado en
|
||||
`extra_model_paths.yaml`. La funcion lee ese YAML (incluida la sintaxis de
|
||||
carpeta multilinea) y suma esas rutas a la nativa, dedup por nombre.
|
||||
- Si `extra_model_paths.yaml` no existe o PyYAML no lo puede parsear, degrada a
|
||||
solo las rutas nativas `~/ComfyUI/models/<folder>/` (no falla).
|
||||
- Lista por nombre de archivo (no rutas completas) y solo extensiones de modelo
|
||||
(.safetensors, .ckpt, .pt, .pth, .bin, .gguf, .sft, .onnx). Subcarpetas dentro de
|
||||
cada folder NO se recorren (solo el primer nivel).
|
||||
- El catalogo que ve el SERVIDOR (combos de la UI) puede diferir si el server no se
|
||||
ha refrescado tras una descarga; para el combo en vivo usa `comfyui_object_info`
|
||||
o `comfyui_refresh_nodes_ui`.
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Lista los modelos instalados de ComfyUI por carpeta, resolviendo rutas reales.
|
||||
|
||||
ComfyUI puede leer los modelos desde rutas externas declaradas en
|
||||
`<comfyui_dir>/extra_model_paths.yaml` (en este equipo, /mnt/2tb/comfyui_models/),
|
||||
ademas de las nativas `<comfyui_dir>/models/<folder>/`. Esta funcion escanea
|
||||
AMBAS para cada carpeta de tipo (checkpoints, loras, vae, controlnet,
|
||||
upscale_models), de modo que ve los modelos aunque no esten bajo `models/`.
|
||||
|
||||
El escaneo es del sistema de archivos (no depende del servidor ComfyUI), asi que
|
||||
funciona aunque el servidor este reiniciandose.
|
||||
|
||||
Impura: lectura de disco (FS scan + parse de YAML). Solo stdlib + PyYAML.
|
||||
"""
|
||||
import os
|
||||
|
||||
_DEFAULT_FOLDERS = ["checkpoints", "loras", "vae", "controlnet", "upscale_models"]
|
||||
_MODEL_EXTS = (
|
||||
".safetensors", ".ckpt", ".pt", ".pth", ".bin", ".gguf", ".sft", ".onnx",
|
||||
)
|
||||
|
||||
|
||||
def _resolve_external_roots(base: str) -> dict:
|
||||
"""Lee extra_model_paths.yaml y devuelve {folder: [dir_externo, ...]}.
|
||||
|
||||
Maneja valores de carpeta multilinea (varias subrutas por clave). Si el YAML
|
||||
no existe o no se puede parsear, devuelve {} (solo se usaran las rutas
|
||||
nativas).
|
||||
"""
|
||||
roots: dict = {}
|
||||
yml = os.path.join(base, "extra_model_paths.yaml")
|
||||
if not os.path.isfile(yml):
|
||||
return roots
|
||||
try:
|
||||
import yaml
|
||||
with open(yml, encoding="utf-8") as fh:
|
||||
data = yaml.safe_load(fh) or {}
|
||||
except Exception: # noqa: BLE001 — YAML ilegible: degradar a rutas nativas
|
||||
return roots
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return roots
|
||||
for section in data.values():
|
||||
if not isinstance(section, dict):
|
||||
continue
|
||||
bp = os.path.expanduser(str(section.get("base_path", "")))
|
||||
for key in _DEFAULT_FOLDERS:
|
||||
sub = section.get(key)
|
||||
if not sub:
|
||||
continue
|
||||
for line in str(sub).splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
roots.setdefault(key, []).append(os.path.join(bp, line))
|
||||
return roots
|
||||
|
||||
|
||||
def comfyui_list_installed_models(
|
||||
folder: str | None = None,
|
||||
comfyui_dir: str = "~/ComfyUI",
|
||||
) -> dict:
|
||||
"""Lista los modelos en disco por carpeta de tipo.
|
||||
|
||||
Args:
|
||||
folder: carpeta concreta a listar (ej. "checkpoints"). Si None, lista
|
||||
todas las de _DEFAULT_FOLDERS.
|
||||
comfyui_dir: raiz de la instalacion de ComfyUI (se expande ~).
|
||||
|
||||
Returns:
|
||||
dict {ok, models, error}. models es {folder: [nombre, ...]} con los
|
||||
archivos de modelo (dedup por nombre) hallados tanto en la ruta nativa
|
||||
`models/<folder>/` como en las externas de extra_model_paths.yaml. ok es
|
||||
True salvo fallo inesperado.
|
||||
"""
|
||||
base = os.path.expanduser(comfyui_dir)
|
||||
folders = [folder] if folder else list(_DEFAULT_FOLDERS)
|
||||
external = _resolve_external_roots(base)
|
||||
|
||||
models: dict = {}
|
||||
try:
|
||||
for f in folders:
|
||||
dirs = [os.path.join(base, "models", f)] + external.get(f, [])
|
||||
names: list = []
|
||||
seen: set = set()
|
||||
for d in dirs:
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
for entry in sorted(os.listdir(d)):
|
||||
if entry in seen:
|
||||
continue
|
||||
p = os.path.join(d, entry)
|
||||
if os.path.isfile(p) and entry.lower().endswith(_MODEL_EXTS):
|
||||
seen.add(entry)
|
||||
names.append(entry)
|
||||
models[f] = names
|
||||
except OSError as exc:
|
||||
return {"ok": False, "models": models, "error": f"fallo escaneando: {exc}"}
|
||||
|
||||
return {"ok": True, "models": models, "error": ""}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
print(json.dumps(comfyui_list_installed_models(), ensure_ascii=False, indent=2))
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: comfyui_object_info
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_object_info(server: str = \"127.0.0.1:8188\", node_class: str | None = None, timeout: float = 30.0) -> dict"
|
||||
description: "Consulta el catalogo de nodos de un servidor ComfyUI via GET /object_info (o un nodo concreto con /object_info/{node_class}). Devuelve specs de inputs y valores enumerados (ej. lista de checkpoints visibles). Impura: HTTP GET, solo stdlib."
|
||||
tags: [comfyui, ml, image-generation, stable-diffusion, introspection, http]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema (default '127.0.0.1:8188')."
|
||||
- name: node_class
|
||||
desc: "Si se pasa, consulta solo ese class_type via /object_info/{node_class} (ej. 'CheckpointLoaderSimple'). None devuelve el catalogo completo."
|
||||
- name: timeout
|
||||
desc: "Timeout de la peticion HTTP en segundos."
|
||||
output: "dict del catalogo. Con node_class=None es {class_type: spec, ...} (cientos de nodos). Con node_class set, {class_type: spec} de un solo item. Cada spec tiene input.required/optional con tipos y enums; ej. info['CheckpointLoaderSimple']['input']['required']['ckpt_name'][0] es la lista de checkpoints."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_object_info.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_object_info import comfyui_object_info
|
||||
|
||||
info = comfyui_object_info() # catalogo completo
|
||||
print(len(info)) # ~792 nodos
|
||||
ckpts = info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0]
|
||||
print(ckpts) # ['v1-5-pruned-emaonly-fp16.safetensors']
|
||||
|
||||
ks = comfyui_object_info(node_class="KSampler") # solo un nodo
|
||||
print(list(ks.keys())) # ['KSampler']
|
||||
```
|
||||
|
||||
O lanzable directo con: `./fn run comfyui_object_info` (imprime n nodos + checkpoints visibles).
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Antes de construir o enviar un workflow: descubre que checkpoints, samplers,
|
||||
schedulers y nodos existen en el servidor concreto. Usala para validar que el
|
||||
`ckpt_name` que vas a poner en `comfyui_build_txt2img_workflow` existe, o para
|
||||
explorar nodos disponibles (LoRA loaders, upscalers, ControlNet) antes de
|
||||
componer workflows mas ricos.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El catalogo completo es grande (cientos de nodos): preferir `node_class` si
|
||||
solo necesitas uno.
|
||||
- Los valores enumerados (checkpoints, vaes, loras) reflejan lo que el SERVIDOR
|
||||
ve en sus carpetas models/, no lo que hay en tu disco local. Si acabas de
|
||||
copiar un checkpoint, el servidor puede no haberlo escaneado hasta reiniciar o
|
||||
refrescar.
|
||||
- Lanza RuntimeError si ComfyUI no esta arriba (conexion rechazada) o responde
|
||||
con error. El catalogo solo esta disponible con el servidor corriendo.
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Consulta el catalogo de nodos de un servidor ComfyUI via GET /object_info.
|
||||
|
||||
Funcion impura: hace red (HTTP GET). Solo stdlib (urllib, json).
|
||||
|
||||
El catalogo describe cada class_type disponible: sus inputs requeridos y
|
||||
opcionales, sus tipos, y los valores enumerados (ej. la lista de checkpoints
|
||||
visibles para el servidor en CheckpointLoaderSimple). Util para validar un
|
||||
workflow antes de enviarlo y para descubrir que checkpoints/samplers existen.
|
||||
"""
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
def comfyui_object_info(
|
||||
server: str = "127.0.0.1:8188",
|
||||
node_class: str | None = None,
|
||||
timeout: float = 30.0,
|
||||
) -> dict:
|
||||
"""Recupera el catalogo de nodos (o uno concreto) de ComfyUI.
|
||||
|
||||
Args:
|
||||
server: host:port del servidor ComfyUI (sin esquema).
|
||||
node_class: si se pasa, consulta solo ese class_type via
|
||||
GET /object_info/{node_class} (ej. "CheckpointLoaderSimple").
|
||||
Si es None, devuelve el catalogo completo (GET /object_info).
|
||||
timeout: timeout de la peticion HTTP en segundos.
|
||||
|
||||
Returns:
|
||||
dict con el catalogo. Con node_class=None es {class_type: spec, ...}.
|
||||
Con node_class set, ComfyUI devuelve {class_type: spec} (un solo item).
|
||||
|
||||
Raises:
|
||||
RuntimeError: si la peticion HTTP falla (conexion rechazada, timeout,
|
||||
HTTP de error) o si la respuesta no es JSON valido. El mensaje
|
||||
incluye el cuerpo del error cuando ComfyUI lo provee.
|
||||
"""
|
||||
path = "/object_info"
|
||||
if node_class is not None:
|
||||
path = f"/object_info/{urllib.parse.quote(node_class)}"
|
||||
url = f"http://{server}{path}"
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
||||
return json.loads(resp.read())
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode(errors="replace")
|
||||
raise RuntimeError(
|
||||
f"comfyui_object_info: HTTP {exc.code} en {url}: {body}"
|
||||
) from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise RuntimeError(
|
||||
f"comfyui_object_info: no se pudo conectar a {url}: {exc.reason}"
|
||||
) from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(
|
||||
f"comfyui_object_info: respuesta no es JSON valido desde {url}: {exc}"
|
||||
) from exc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
info = comfyui_object_info()
|
||||
print(f"nodos disponibles: {len(info)}")
|
||||
ckpts = info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0]
|
||||
print(f"checkpoints visibles: {ckpts}")
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: comfyui_read_png_metadata
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_read_png_metadata(png_path: str) -> dict"
|
||||
description: "Lee los parametros de generacion de un PNG generado por ComfyUI: extrae el chunk 'prompt' (API format) y resume modelo, seed, steps, cfg, sampler, scheduler, denoise y los prompts positivo/negativo siguiendo las conexiones del KSampler. Comparte el lector de chunks PNG con comfyui_import_workflow_png. Impura: lectura de disco, solo stdlib."
|
||||
tags: [comfyui, ml, png, metadata, workflow, stable-diffusion]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
params:
|
||||
- name: png_path
|
||||
desc: "Ruta local del PNG generado por ComfyUI."
|
||||
output: "dict {ok, prompt, parameters, error}. prompt = workflow API format embebido (dict); parameters = {model, seed, steps, cfg, sampler_name, scheduler, denoise, positive, negative} extraido del KSampler y nodos conectados; error = motivo si ok=False."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_read_png_metadata.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_read_png_metadata import comfyui_read_png_metadata
|
||||
|
||||
res = comfyui_read_png_metadata(os.path.expanduser("~/ComfyUI/output/comfy_00001_.png"))
|
||||
# res["ok"] == True
|
||||
# res["parameters"]["seed"] # ej. 20260623
|
||||
# res["parameters"]["model"] # ej. "dreamshaper_8.safetensors"
|
||||
# res["parameters"]["positive"] # el prompt positivo usado
|
||||
```
|
||||
|
||||
O lanzable directo con: `./fn run comfyui_read_png_metadata <ruta.png>`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras saber con que parametros se genero una imagen (que seed, modelo o
|
||||
prompt) sin abrir el grafo entero: para reproducir una imagen que te gusto, para
|
||||
catalogar outputs, o para comparar generaciones. Si necesitas el workflow completo
|
||||
para relanzarlo usa `comfyui_import_workflow_png` (devuelve el dict entero).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: lee el archivo del disco. Un path inexistente o un PNG sin chunk
|
||||
'prompt' devuelve `{ok: False, error: ...}` (no lanza).
|
||||
- `parameters` se extrae del primer nodo cuyo class_type acaba en "KSampler" y de
|
||||
los CLIPTextEncode conectados a sus inputs positive/negative. Workflows muy
|
||||
custom (varios samplers, sin CheckpointLoaderSimple) pueden dar `parameters`
|
||||
parcial; el `prompt` completo siempre se devuelve para inspeccion manual.
|
||||
- Lee chunks tEXt/zTXt/iTXt; los PNG de la API REST solo traen 'prompt' (no
|
||||
'workflow'), suficiente para los parametros.
|
||||
- Marcada impura (no pura) porque hace I/O de disco, segun la regla de pureza del
|
||||
registry; la logica de parseo en si es determinista.
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Lee los parametros de generacion de un PNG generado por ComfyUI.
|
||||
|
||||
Extrae el chunk "prompt" (API format) de los chunks de texto del PNG y resume
|
||||
los parametros de generacion: modelo, seed, steps, cfg, sampler, scheduler,
|
||||
denoise y los prompts positivo/negativo (siguiendo las conexiones del KSampler).
|
||||
|
||||
Impura: lectura de disco. Solo stdlib (struct, zlib, json).
|
||||
"""
|
||||
import json
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
|
||||
def comfyui_read_png_metadata(png_path: str) -> dict:
|
||||
"""Devuelve {ok, prompt, parameters, error} de un PNG de ComfyUI.
|
||||
|
||||
Args:
|
||||
png_path: ruta del PNG generado por ComfyUI.
|
||||
|
||||
Returns:
|
||||
dict con:
|
||||
- ok: bool.
|
||||
- prompt: el workflow API format embebido (dict), o {}.
|
||||
- parameters: resumen {model, seed, steps, cfg, sampler_name,
|
||||
scheduler, denoise, positive, negative} extraido del KSampler y los
|
||||
nodos conectados, o {}.
|
||||
- error: mensaje si algo fallo.
|
||||
"""
|
||||
try:
|
||||
with open(png_path, "rb") as f:
|
||||
data = f.read()
|
||||
except OSError as exc:
|
||||
return {"ok": False, "prompt": {}, "parameters": {},
|
||||
"error": f"no se pudo leer {png_path!r}: {exc}"}
|
||||
try:
|
||||
chunks = _png_text_chunks(data)
|
||||
except ValueError as exc:
|
||||
return {"ok": False, "prompt": {}, "parameters": {}, "error": str(exc)}
|
||||
|
||||
if "prompt" not in chunks:
|
||||
return {"ok": False, "prompt": {}, "parameters": {},
|
||||
"error": "el PNG no contiene chunk 'prompt' de ComfyUI"}
|
||||
try:
|
||||
prompt = json.loads(chunks["prompt"])
|
||||
except json.JSONDecodeError as exc:
|
||||
return {"ok": False, "prompt": {}, "parameters": {},
|
||||
"error": f"chunk 'prompt' no es JSON valido: {exc}"}
|
||||
|
||||
return {"ok": True, "prompt": prompt, "parameters": _extract_params(prompt), "error": ""}
|
||||
|
||||
|
||||
def _extract_params(prompt: dict) -> dict:
|
||||
params = {}
|
||||
ksampler = None
|
||||
for node in prompt.values():
|
||||
if isinstance(node, dict) and str(node.get("class_type", "")).endswith("KSampler"):
|
||||
ksampler = node
|
||||
break
|
||||
if ksampler:
|
||||
ins = ksampler.get("inputs", {})
|
||||
for k in ("seed", "steps", "cfg", "sampler_name", "scheduler", "denoise"):
|
||||
if k in ins and not isinstance(ins[k], list):
|
||||
params[k] = ins[k]
|
||||
for slot in ("positive", "negative"):
|
||||
link = ins.get(slot)
|
||||
if isinstance(link, list) and len(link) == 2:
|
||||
tnode = prompt.get(str(link[0]), {})
|
||||
txt = tnode.get("inputs", {}).get("text")
|
||||
if isinstance(txt, str):
|
||||
params[slot] = txt
|
||||
for node in prompt.values():
|
||||
if isinstance(node, dict) and str(node.get("class_type", "")).startswith("CheckpointLoader"):
|
||||
ck = node.get("inputs", {}).get("ckpt_name")
|
||||
if ck:
|
||||
params["model"] = ck
|
||||
break
|
||||
return params
|
||||
|
||||
|
||||
def _png_text_chunks(data: bytes) -> dict:
|
||||
"""Lee los chunks de texto (tEXt/zTXt/iTXt) de un PNG -> {keyword: texto}."""
|
||||
if data[:8] != b"\x89PNG\r\n\x1a\n":
|
||||
raise ValueError("no es un PNG valido (firma incorrecta)")
|
||||
out = {}
|
||||
off = 8
|
||||
n = len(data)
|
||||
while off + 8 <= n:
|
||||
length = struct.unpack(">I", data[off:off + 4])[0]
|
||||
ctype = data[off + 4:off + 8]
|
||||
body = data[off + 8:off + 8 + length]
|
||||
off += 12 + length
|
||||
if ctype == b"tEXt":
|
||||
kw, _, txt = body.partition(b"\x00")
|
||||
out[kw.decode("latin1")] = txt.decode("latin1")
|
||||
elif ctype == b"zTXt":
|
||||
kw, _, rest = body.partition(b"\x00")
|
||||
if rest:
|
||||
try:
|
||||
out[kw.decode("latin1")] = zlib.decompress(rest[1:]).decode("latin1")
|
||||
except zlib.error:
|
||||
pass
|
||||
elif ctype == b"iTXt":
|
||||
kw, _, rest = body.partition(b"\x00")
|
||||
if len(rest) >= 2:
|
||||
comp_flag = rest[0]
|
||||
parts = rest[2:].split(b"\x00", 2)
|
||||
if len(parts) == 3:
|
||||
text_bytes = parts[2]
|
||||
if comp_flag == 1:
|
||||
try:
|
||||
text_bytes = zlib.decompress(text_bytes)
|
||||
except zlib.error:
|
||||
text_bytes = b""
|
||||
out[kw.decode("latin1")] = text_bytes.decode("utf-8", "replace")
|
||||
elif ctype == b"IEND":
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/missing.png"
|
||||
res = comfyui_read_png_metadata(path)
|
||||
print(json.dumps({"ok": res["ok"], "parameters": res["parameters"], "error": res["error"]}, indent=2))
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: comfyui_resolve_workflow_deps
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_resolve_workflow_deps(workflow: dict, server: str = \"127.0.0.1:8188\") -> dict"
|
||||
description: "Resuelve las dependencias de un workflow ComfyUI ajeno: lo valida contra /object_info y traduce lo que falta en acciones concretas: nodos custom ausentes -> repos a instalar (comfyui_install_custom_node) y modelos ausentes -> assets a descargar (comfyui_search_civitai_models + comfyui_download_model). Compone comfyui_validate_workflow. Impura: HTTP GET. Solo stdlib."
|
||||
tags: [comfyui, ml, workflow, dependencies, validation, stable-diffusion]
|
||||
uses_functions: [comfyui_validate_workflow_py_ml]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["os", "sys"]
|
||||
params:
|
||||
- name: workflow
|
||||
desc: "dict en API format ({node_id: {class_type, inputs}}), normalmente importado de internet con comfyui_import_workflow_json/_png."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema. Debe estar vivo para consultar /object_info."
|
||||
output: "dict {ok, missing_nodes, missing_models, suggestions, error}. ok = se pudo consultar el servidor; missing_nodes = class_type ausentes (nodos custom); missing_models = lista de {node, input, value}; suggestions = lista de {kind, name, action, hint, ...} (una por nodo/modelo faltante) con la funcion a usar; error = motivo si ok=False."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_resolve_workflow_deps.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_resolve_workflow_deps import comfyui_resolve_workflow_deps
|
||||
|
||||
# Workflow con un nodo custom y un modelo que no estan en este server:
|
||||
wf = {
|
||||
"1": {"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": "no_existe_xyz.safetensors"}},
|
||||
"2": {"class_type": "NodoQueNoExiste_XYZ", "inputs": {}},
|
||||
}
|
||||
res = comfyui_resolve_workflow_deps(wf) # server 127.0.0.1:8188 vivo
|
||||
# res["missing_nodes"] == ["NodoQueNoExiste_XYZ"]
|
||||
# res["missing_models"] == [{"node": "1", "input": "ckpt_name", "value": "no_existe_xyz.safetensors"}]
|
||||
# res["suggestions"] -> 2 acciones: {kind:"node", action:"install_custom_node", ...}
|
||||
# {kind:"model", action:"search_and_download", ...}
|
||||
```
|
||||
|
||||
El bloque se lanza con el python del venv y necesita el server vivo en
|
||||
`127.0.0.1:8188`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Justo despues de importar un workflow de internet
|
||||
(`comfyui_import_workflow_json`/`_png`) y antes de intentar ejecutarlo: te dice
|
||||
exactamente que nodos custom instalar y que modelos descargar para que funcione,
|
||||
con la funcion concreta a usar en cada caso. Evita el ciclo de prueba y error de
|
||||
encolar workflows ajenos.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: compone `comfyui_validate_workflow`, que hace HTTP GET a `/object_info`.
|
||||
Requiere el servidor vivo; si esta caido o reiniciandose devuelve
|
||||
`{ok: False, error: ...}` (no lanza). Reintenta tu con backoff.
|
||||
- Las `suggestions` son guias accionables (hint + funcion), NO resuelven el repo
|
||||
exacto de cada nodo automaticamente: identificar el repo de un nodo custom por
|
||||
su class_type requiere la DB de ComfyUI-Manager o una busqueda manual en GitHub.
|
||||
- `missing_models` solo cubre inputs de modelo conocidos (ckpt_name, lora_name,
|
||||
vae_name, control_net_name, ...) cuyo valor sea un string fuera del combo. No
|
||||
valida rangos numericos ni tipos de conexion (hereda los limites de
|
||||
`comfyui_validate_workflow`).
|
||||
- Un workflow ya completo devuelve `missing_nodes=[]`, `missing_models=[]`,
|
||||
`suggestions=[]` con `ok=True`: listo para encolar.
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Resuelve las dependencias de un workflow ComfyUI ajeno antes de ejecutarlo.
|
||||
|
||||
Dado un workflow (p.ej. importado de internet con comfyui_import_workflow_json /
|
||||
_png), lo valida contra el servidor y traduce lo que falta en acciones
|
||||
concretas: nodos custom ausentes -> repos a instalar (comfyui_install_custom_node)
|
||||
y modelos ausentes -> assets a descargar (comfyui_search_civitai_models +
|
||||
comfyui_download_model). Asi un workflow desconocido se vuelve ejecutable sin
|
||||
prueba y error.
|
||||
|
||||
Compone comfyui_validate_workflow (no reimplementa la consulta a /object_info).
|
||||
|
||||
Impura: red (HTTP GET via validate_workflow). Solo stdlib.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _THIS_DIR not in sys.path:
|
||||
sys.path.insert(0, _THIS_DIR)
|
||||
|
||||
from comfyui_validate_workflow import comfyui_validate_workflow # noqa: E402
|
||||
|
||||
|
||||
def comfyui_resolve_workflow_deps(
|
||||
workflow: dict,
|
||||
server: str = "127.0.0.1:8188",
|
||||
) -> dict:
|
||||
"""Lista nodos y modelos faltantes de un workflow y sugiere como obtenerlos.
|
||||
|
||||
Args:
|
||||
workflow: dict en API format ({node_id: {class_type, inputs}}).
|
||||
server: host:port del servidor ComfyUI (sin esquema). Debe estar vivo
|
||||
para consultar /object_info.
|
||||
|
||||
Returns:
|
||||
dict {ok, missing_nodes, missing_models, suggestions, error}:
|
||||
- ok: la validacion se pudo ejecutar (el servidor respondio).
|
||||
- missing_nodes: class_type ausentes en el servidor (nodos custom).
|
||||
- missing_models: lista de {node, input, value} con modelos no
|
||||
presentes en el combo del nodo.
|
||||
- suggestions: lista de acciones {kind, name, action, hint, ...} — una
|
||||
por nodo o modelo faltante — describiendo que funcion usar.
|
||||
- error: motivo si no se pudo consultar el servidor (ok=False).
|
||||
"""
|
||||
val = comfyui_validate_workflow(workflow, server=server)
|
||||
if not val.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"missing_nodes": [],
|
||||
"missing_models": [],
|
||||
"suggestions": [],
|
||||
"error": val.get("error", "validacion fallida"),
|
||||
}
|
||||
|
||||
missing_nodes = val.get("missing_nodes", [])
|
||||
missing_models = val.get("missing_models", [])
|
||||
suggestions = []
|
||||
for node in missing_nodes:
|
||||
suggestions.append({
|
||||
"kind": "node",
|
||||
"name": node,
|
||||
"action": "install_custom_node",
|
||||
"hint": (
|
||||
f"Nodo custom '{node}' ausente. Localiza su repo (ComfyUI-Manager "
|
||||
f"DB o GitHub por el class_type) e instala con "
|
||||
f"comfyui_install_custom_node('<repo_url>')."
|
||||
),
|
||||
})
|
||||
for m in missing_models:
|
||||
value = m.get("value")
|
||||
suggestions.append({
|
||||
"kind": "model",
|
||||
"name": value,
|
||||
"input": m.get("input"),
|
||||
"node": m.get("node"),
|
||||
"action": "search_and_download",
|
||||
"hint": (
|
||||
f"Modelo '{value}' ausente (input {m.get('input')}). Busca con "
|
||||
f"comfyui_search_civitai_models(query='...') o en HuggingFace, "
|
||||
f"luego comfyui_download_model(url, dest_subdir=...)."
|
||||
),
|
||||
})
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"missing_nodes": missing_nodes,
|
||||
"missing_models": missing_models,
|
||||
"suggestions": suggestions,
|
||||
"error": "",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
# Workflow con un nodo inexistente -> debe aparecer en missing_nodes.
|
||||
wf = {
|
||||
"1": {"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": "dreamshaper_8.safetensors"}},
|
||||
"2": {"class_type": "NodoQueNoExiste_XYZ", "inputs": {}},
|
||||
}
|
||||
print(json.dumps(comfyui_resolve_workflow_deps(wf), indent=2))
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: comfyui_search_civitai_models
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_search_civitai_models(query: str, *, types: str = \"Checkpoint\", base_model: str | None = None, sort: str = \"Highest Rated\", limit: int = 20, token: str | None = None) -> dict"
|
||||
description: "Busca modelos/LoRAs en Civitai via su API publica GET /api/v1/models y normaliza cada resultado a {name, type, base_model, version_id, download_url, nsfw} (primera version del modelo). La busqueda publica funciona SIN token; el token solo sube el rate limit y desbloquea modelos restringidos. Impura: HTTP GET a civitai.com. Solo stdlib."
|
||||
tags: [comfyui, ml, civitai, search, models, stable-diffusion, http]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["json", "urllib.error", "urllib.parse", "urllib.request"]
|
||||
params:
|
||||
- name: query
|
||||
desc: "Texto de busqueda (nombre del modelo, ej. 'dreamshaper')."
|
||||
- name: types
|
||||
desc: "Tipo(s) de modelo Civitai, CSV: Checkpoint, LORA, TextualInversion, Controlnet, VAE, Upscaler, ... keyword-only."
|
||||
- name: base_model
|
||||
desc: "Filtra por modelo base (ej. 'SD 1.5', 'SDXL 1.0'). None no filtra. keyword-only."
|
||||
- name: sort
|
||||
desc: "Orden Civitai: 'Highest Rated', 'Most Downloaded', 'Newest'. keyword-only."
|
||||
- name: limit
|
||||
desc: "Numero maximo de resultados (1-100). keyword-only."
|
||||
- name: token
|
||||
desc: "API token de Civitai (header Authorization Bearer). Opcional: la busqueda publica funciona sin el. No hardcodear: pasar desde pass/vault. keyword-only."
|
||||
output: "dict {ok, items, count, error}. items = lista de {name, type, base_model, version_id, download_url, nsfw} (primera version de cada modelo). ok=False con error si la peticion falla; una busqueda sin resultados devuelve ok=True con items=[] (no es error)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_search_civitai_models.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_search_civitai_models import comfyui_search_civitai_models
|
||||
|
||||
# Busqueda publica, sin token:
|
||||
out = comfyui_search_civitai_models("dreamshaper", types="Checkpoint", limit=5)
|
||||
for it in out["items"]:
|
||||
print(it["name"], it["base_model"], "->", it["download_url"])
|
||||
# out["items"][0] == {"name": "DreamShaper", "type": "Checkpoint",
|
||||
# "base_model": "SD 1.5", "version_id": 128713,
|
||||
# "download_url": "https://civitai.com/api/download/models/128713", "nsfw": False}
|
||||
|
||||
# Filtrar LoRAs SDXL, con token desde pass (si lo hubiera):
|
||||
# import subprocess
|
||||
# tok = subprocess.run(["pass","civitai/api-token"], capture_output=True, text=True).stdout.strip() or None
|
||||
# loras = comfyui_search_civitai_models("detail", types="LORA", base_model="SDXL 1.0", token=tok)
|
||||
```
|
||||
|
||||
El `download_url` que devuelve se pasa directo a `comfyui_download_model(url=...)`
|
||||
para bajar el modelo a la carpeta correcta.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites descubrir un checkpoint o LoRA por nombre/tema antes de
|
||||
descargarlo: te da el `download_url` listo para `comfyui_download_model`. Encadena
|
||||
con `comfyui_resolve_workflow_deps` para resolver los modelos que le faltan a un
|
||||
workflow ajeno.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: HTTP GET a `civitai.com`. Requiere conexion a internet (NO usa el server
|
||||
local de ComfyUI, asi que no choca con reinicios del server).
|
||||
- La busqueda publica funciona sin token. Sin token el rate limit es mas bajo y
|
||||
algunos modelos restringidos no aparecen o su `download_url` exige login.
|
||||
- `download_url` apunta a `api/download/models/<versionId>`: muchos modelos exigen
|
||||
token para descargar aunque salgan en la busqueda (Civitai responde HTML de
|
||||
login). `comfyui_download_model` detecta ese HTML y devuelve ok=False.
|
||||
- Solo se devuelve la PRIMERA version de cada modelo (`modelVersions[0]`, la mas
|
||||
reciente). Para versiones antiguas consulta la API por `version_id`.
|
||||
- Una busqueda sin resultados NO es error: devuelve `ok=True, items=[], count=0`.
|
||||
- No hardcodear el token: pasarlo desde `pass`/vault.
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Busca modelos / LoRAs en Civitai via su API publica GET /api/v1/models.
|
||||
|
||||
Normaliza cada resultado a un dict pequeno y reutilizable
|
||||
({name, type, base_model, version_id, download_url, nsfw}) tomando la primera
|
||||
version del modelo. La busqueda publica funciona sin token; pasar un token solo
|
||||
sube el rate limit y desbloquea modelos restringidos.
|
||||
|
||||
Impura: red (HTTP GET a civitai.com). Solo stdlib (urllib, json).
|
||||
"""
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
_API = "https://civitai.com/api/v1/models"
|
||||
_TIMEOUT = 30.0
|
||||
|
||||
|
||||
def comfyui_search_civitai_models(
|
||||
query: str,
|
||||
*,
|
||||
types: str = "Checkpoint",
|
||||
base_model: str | None = None,
|
||||
sort: str = "Highest Rated",
|
||||
limit: int = 20,
|
||||
token: str | None = None,
|
||||
) -> dict:
|
||||
"""Busca modelos en Civitai y devuelve resultados normalizados.
|
||||
|
||||
Args:
|
||||
query: texto de busqueda (nombre del modelo, ej. "dreamshaper").
|
||||
types: tipo(s) de modelo, CSV. Valores Civitai: Checkpoint, LORA,
|
||||
TextualInversion, Controlnet, VAE, Upscaler, ... keyword-only.
|
||||
base_model: filtra por modelo base (ej. "SD 1.5", "SDXL 1.0"). None no
|
||||
filtra. keyword-only.
|
||||
sort: orden Civitai ("Highest Rated", "Most Downloaded", "Newest").
|
||||
keyword-only.
|
||||
limit: numero maximo de resultados (1-100). keyword-only.
|
||||
token: API token de Civitai (header Authorization Bearer). Opcional: la
|
||||
busqueda publica funciona sin el. No hardcodear: pasar desde
|
||||
pass/vault. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict {ok, items, count, error}. items es una lista de
|
||||
{name, type, base_model, version_id, download_url, nsfw} (la primera
|
||||
version de cada modelo). ok=False con error si la peticion falla; una
|
||||
busqueda sin resultados devuelve ok=True con items=[] (no es error).
|
||||
"""
|
||||
params = [
|
||||
("query", query),
|
||||
("limit", str(max(1, min(int(limit), 100)))),
|
||||
("sort", sort),
|
||||
]
|
||||
for t in str(types).split(","):
|
||||
t = t.strip()
|
||||
if t:
|
||||
params.append(("types", t))
|
||||
if base_model:
|
||||
for bm in (base_model if isinstance(base_model, (list, tuple)) else [base_model]):
|
||||
params.append(("baseModels", str(bm)))
|
||||
|
||||
url = f"{_API}?{urllib.parse.urlencode(params)}"
|
||||
headers = {"User-Agent": "fn-registry/comfyui_search_civitai_models"}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
|
||||
data = json.loads(resp.read())
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode(errors="replace")[:300]
|
||||
return {"ok": False, "items": [], "count": 0,
|
||||
"error": f"HTTP {exc.code} en {url}: {body}"}
|
||||
except urllib.error.URLError as exc:
|
||||
return {"ok": False, "items": [], "count": 0,
|
||||
"error": f"no se pudo conectar a civitai.com: {exc.reason}"}
|
||||
except json.JSONDecodeError as exc:
|
||||
return {"ok": False, "items": [], "count": 0,
|
||||
"error": f"respuesta no es JSON valido: {exc}"}
|
||||
|
||||
items = []
|
||||
for model in data.get("items", []) or []:
|
||||
versions = model.get("modelVersions") or []
|
||||
v0 = versions[0] if versions else {}
|
||||
items.append({
|
||||
"name": model.get("name"),
|
||||
"type": model.get("type"),
|
||||
"base_model": v0.get("baseModel"),
|
||||
"version_id": v0.get("id"),
|
||||
"download_url": v0.get("downloadUrl"),
|
||||
"nsfw": bool(model.get("nsfw", False)),
|
||||
})
|
||||
return {"ok": True, "items": items, "count": len(items), "error": ""}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
out = comfyui_search_civitai_models("dreamshaper", limit=5)
|
||||
print(out["ok"], out["count"])
|
||||
for it in out["items"]:
|
||||
print(f" {it['name']} [{it['base_model']}] v{it['version_id']} -> {it['download_url']}")
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: comfyui_submit_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_submit_workflow(workflow: dict, server: str = \"127.0.0.1:8188\", client_id: str | None = None, timeout: float = 30.0) -> dict"
|
||||
description: "Envia un workflow (API format) a ComfyUI via POST /prompt. Devuelve la respuesta con prompt_id y number (posicion en cola). Si ComfyUI rechaza el workflow (HTTP 400) propaga el cuerpo con los detalles de validacion por nodo. Impura: HTTP POST, solo stdlib (urllib, json, uuid)."
|
||||
tags: [comfyui, ml, image-generation, stable-diffusion, http, queue]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: workflow
|
||||
desc: "dict en API format (resultado de comfyui_build_txt2img_workflow u otro builder). Claves = node_ids, valores con class_type + inputs."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema (default '127.0.0.1:8188')."
|
||||
- name: client_id
|
||||
desc: "Identificador de cliente para correlar eventos. None genera un uuid4; el id usado se devuelve en la respuesta."
|
||||
- name: timeout
|
||||
desc: "Timeout de la peticion HTTP en segundos."
|
||||
output: "dict de respuesta de ComfyUI con prompt_id (str, para comfyui_wait_result), number (int, posicion en cola), node_errors (dict) y la clave anadida client_id (str usado en la peticion)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_submit_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_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
from ml.comfyui_submit_workflow import comfyui_submit_workflow
|
||||
|
||||
wf = comfyui_build_txt2img_workflow(
|
||||
ckpt_name="v1-5-pruned-emaonly-fp16.safetensors",
|
||||
positive="a red apple on a wooden table, sharp focus",
|
||||
negative="blurry, low quality",
|
||||
)
|
||||
resp = comfyui_submit_workflow(wf)
|
||||
prompt_id = resp["prompt_id"] # pasalo a comfyui_wait_result
|
||||
print(prompt_id, resp.get("number"))
|
||||
```
|
||||
|
||||
O lanzable directo (build + submit) con: `./fn run comfyui_submit_workflow`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Tras construir un workflow con `comfyui_build_txt2img_workflow`, para encolarlo
|
||||
en el servidor y obtener el `prompt_id`. Es el segundo paso del round-trip
|
||||
build -> submit -> wait. Reutiliza el `client_id` que devuelve si vas a
|
||||
correlar varios prompts del mismo cliente.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- ComfyUI encola y devuelve de inmediato; NO espera a que termine la
|
||||
generacion. Para recuperar el resultado usa `comfyui_wait_result` con el
|
||||
prompt_id.
|
||||
- Si el workflow es invalido (checkpoint inexistente, conexion mal tipada,
|
||||
input fuera de rango) ComfyUI responde HTTP 400 y esta funcion lanza
|
||||
RuntimeError con el cuerpo de validacion del nodo afectado. Leelo: el detalle
|
||||
dice que nodo y que input fallo.
|
||||
- Encolar tiene efecto secundario: arranca trabajo de GPU en el servidor. No es
|
||||
idempotente — cada llamada encola un prompt nuevo.
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Envia un workflow (API format) a un servidor ComfyUI via POST /prompt.
|
||||
|
||||
Funcion impura: hace red (HTTP POST). Solo stdlib (urllib, json, uuid).
|
||||
|
||||
ComfyUI encola el workflow y devuelve un dict con prompt_id (para seguir el
|
||||
resultado con comfyui_wait_result), number (posicion en la cola) y node_errors.
|
||||
Si el workflow es invalido, ComfyUI responde HTTP 400 con detalles de la
|
||||
validacion por nodo: esta funcion los captura y los propaga en el error.
|
||||
"""
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
|
||||
def comfyui_submit_workflow(
|
||||
workflow: dict,
|
||||
server: str = "127.0.0.1:8188",
|
||||
client_id: str | None = None,
|
||||
timeout: float = 30.0,
|
||||
) -> dict:
|
||||
"""Encola un workflow en ComfyUI y devuelve la respuesta del servidor.
|
||||
|
||||
Args:
|
||||
workflow: dict en API format (resultado de
|
||||
comfyui_build_txt2img_workflow u otro builder). Las claves son
|
||||
node_ids y cada valor tiene class_type + inputs.
|
||||
server: host:port del servidor ComfyUI (sin esquema).
|
||||
client_id: identificador de cliente para correlar eventos (WS/history).
|
||||
Si es None, se genera un uuid4. Se incluye en la respuesta.
|
||||
timeout: timeout de la peticion HTTP en segundos.
|
||||
|
||||
Returns:
|
||||
dict con al menos prompt_id (str), number (int, posicion en cola) y
|
||||
node_errors (dict). Se anade la clave "client_id" usada.
|
||||
|
||||
Raises:
|
||||
RuntimeError: si ComfyUI rechaza el workflow (HTTP 400 con detalles de
|
||||
validacion en el cuerpo), si no se puede conectar, o si la respuesta
|
||||
no es JSON valido. El mensaje incluye el cuerpo del error.
|
||||
"""
|
||||
cid = client_id or str(uuid.uuid4())
|
||||
body = json.dumps({"prompt": workflow, "client_id": cid}).encode()
|
||||
url = f"http://{server}/prompt"
|
||||
req = urllib.request.Request(
|
||||
url, data=body, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
out = json.loads(resp.read())
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode(errors="replace")
|
||||
raise RuntimeError(
|
||||
f"comfyui_submit_workflow: ComfyUI rechazo el workflow "
|
||||
f"(HTTP {exc.code}): {detail}"
|
||||
) from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise RuntimeError(
|
||||
f"comfyui_submit_workflow: no se pudo conectar a {url}: {exc.reason}"
|
||||
) from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(
|
||||
f"comfyui_submit_workflow: respuesta no es JSON valido desde {url}: {exc}"
|
||||
) from exc
|
||||
out["client_id"] = cid
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
|
||||
wf = comfyui_build_txt2img_workflow(
|
||||
ckpt_name="v1-5-pruned-emaonly-fp16.safetensors",
|
||||
positive="a red apple on a wooden table, sharp focus",
|
||||
negative="blurry, low quality",
|
||||
steps=20,
|
||||
seed=42,
|
||||
)
|
||||
resp = comfyui_submit_workflow(wf)
|
||||
print(f"prompt_id={resp['prompt_id']} number={resp.get('number')}")
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: comfyui_validate_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_validate_workflow(workflow: dict, server: str = \"127.0.0.1:8188\", timeout: float = 30.0) -> dict"
|
||||
description: "Valida un workflow ComfyUI (API format) contra el catalogo /object_info de un servidor vivo: cruza los class_type contra los nodos disponibles y los nombres de modelos (ckpt/lora/vae/controlnet/...) contra los combos enumerados de cada nodo. Devuelve nodos y modelos faltantes ANTES de encolar, evitando un HTTP 400. Compone comfyui_object_info. Impura: HTTP GET."
|
||||
tags: [comfyui, ml, validation, workflow, stable-diffusion]
|
||||
uses_functions: [comfyui_object_info_py_ml]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
params:
|
||||
- name: workflow
|
||||
desc: "dict en API format ({node_id: {class_type, inputs}}) a validar."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema. Debe estar vivo para consultar /object_info."
|
||||
- name: timeout
|
||||
desc: "Timeout de la consulta HTTP en segundos."
|
||||
output: "dict {ok, valid, missing_nodes, missing_models, error}. ok = se pudo consultar el servidor; valid = sin nodos ni modelos faltantes; missing_nodes = class_type ausentes; missing_models = lista de {node, input, value} con valores de modelo fuera del combo; error = motivo si ok=False."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_validate_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_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
from ml.comfyui_validate_workflow import comfyui_validate_workflow
|
||||
|
||||
wf = comfyui_build_txt2img_workflow("dreamshaper_8.safetensors", "a cat")
|
||||
res = comfyui_validate_workflow(wf) # server 127.0.0.1:8188 vivo
|
||||
# res == {"ok": True, "valid": True, "missing_nodes": [], "missing_models": [], "error": ""}
|
||||
|
||||
bad = comfyui_build_txt2img_workflow("no_existe.safetensors", "a cat")
|
||||
res2 = comfyui_validate_workflow(bad)
|
||||
# res2["valid"] == False
|
||||
# res2["missing_models"] == [{"node": "4", "input": "ckpt_name", "value": "no_existe.safetensors"}]
|
||||
```
|
||||
|
||||
El bloque de arriba se lanza con el python del venv. El `if __name__ == "__main__"` del archivo valida un txt2img de ejemplo contra el server local (`python/.venv/bin/python3 python/functions/ml/comfyui_validate_workflow.py`). Nota: `./fn run` posicional no aplica porque el primer arg es un dict (workflow), no un escalar de CLI.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Siempre ANTES de `comfyui_submit_workflow`, sobre todo con workflows importados de
|
||||
internet (JSON/PNG ajenos) o que mezclen checkpoints/LoRAs que quiza no tengas
|
||||
descargados. Te dice exactamente que nodos custom faltan (a instalar) y que
|
||||
modelos faltan (a descargar) sin gastar un encolado fallido en el servidor.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: hace HTTP GET a `/object_info`. Requiere el servidor vivo; si esta caido
|
||||
o reiniciandose, devuelve `{ok: False, error: ...}` (no lanza). Reintenta tu.
|
||||
- `missing_models` solo cubre inputs de modelo conocidos (ckpt_name, lora_name,
|
||||
vae_name, control_net_name, model_name, unet_name, clip_name, style_model_name,
|
||||
gligen_name) cuyo valor sea un string fuera del combo enumerado. No valida rangos
|
||||
numericos ni tipos de conexion.
|
||||
- Detecta nodos custom faltantes por class_type ausente en /object_info, pero NO
|
||||
resuelve de que repo instalarlos (eso es trabajo de resolve_workflow_deps, P1).
|
||||
- `sampler_name`/`scheduler` invalidos NO se reportan como missing_models (no son
|
||||
modelos); el servidor los rechazaria al encolar.
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Valida un workflow ComfyUI contra el catalogo /object_info del servidor.
|
||||
|
||||
Cruza los class_type del workflow contra los nodos disponibles y los nombres de
|
||||
modelos (checkpoints, loras, vae, controlnet, upscale) contra los combos
|
||||
enumerados de cada nodo. Asi se detectan nodos o modelos faltantes ANTES de
|
||||
encolar (POST /prompt), evitando un HTTP 400 del servidor.
|
||||
|
||||
Compone comfyui_object_info (no reimplementa la consulta HTTP).
|
||||
|
||||
Impura: red (HTTP GET via comfyui_object_info). Solo stdlib.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _THIS_DIR not in sys.path:
|
||||
sys.path.insert(0, _THIS_DIR)
|
||||
|
||||
from comfyui_object_info import comfyui_object_info # noqa: E402
|
||||
|
||||
# inputs cuyo valor es el nombre (string) de un asset/modelo en disco
|
||||
_MODEL_INPUTS = {
|
||||
"ckpt_name",
|
||||
"lora_name",
|
||||
"vae_name",
|
||||
"control_net_name",
|
||||
"model_name",
|
||||
"unet_name",
|
||||
"clip_name",
|
||||
"style_model_name",
|
||||
"gligen_name",
|
||||
}
|
||||
|
||||
|
||||
def comfyui_validate_workflow(
|
||||
workflow: dict,
|
||||
server: str = "127.0.0.1:8188",
|
||||
timeout: float = 30.0,
|
||||
) -> dict:
|
||||
"""Valida un workflow API format contra un servidor ComfyUI vivo.
|
||||
|
||||
Args:
|
||||
workflow: dict en API format ({node_id: {class_type, inputs}}).
|
||||
server: host:port del servidor ComfyUI (sin esquema).
|
||||
timeout: timeout de la consulta HTTP en segundos.
|
||||
|
||||
Returns:
|
||||
dict {ok, valid, missing_nodes, missing_models, error}:
|
||||
- ok: la validacion se pudo ejecutar (el servidor respondio).
|
||||
- valid: el workflow no tiene nodos ni modelos faltantes.
|
||||
- missing_nodes: lista de class_type ausentes en el servidor.
|
||||
- missing_models: lista de {node, input, value} con valores de modelo
|
||||
no presentes en el combo enumerado correspondiente.
|
||||
- error: mensaje si no se pudo consultar el servidor (ok=False).
|
||||
"""
|
||||
try:
|
||||
obj_info = comfyui_object_info(server=server, timeout=timeout)
|
||||
except RuntimeError as exc:
|
||||
return {
|
||||
"ok": False,
|
||||
"valid": False,
|
||||
"missing_nodes": [],
|
||||
"missing_models": [],
|
||||
"error": str(exc),
|
||||
}
|
||||
|
||||
missing_nodes = []
|
||||
missing_models = []
|
||||
for nid, node in workflow.items():
|
||||
if not isinstance(node, dict):
|
||||
continue
|
||||
ctype = node.get("class_type")
|
||||
if ctype not in obj_info:
|
||||
missing_nodes.append(ctype)
|
||||
continue
|
||||
spec = obj_info[ctype].get("input", {})
|
||||
allowed = {}
|
||||
for section in ("required", "optional"):
|
||||
for name, decl in (spec.get(section) or {}).items():
|
||||
if isinstance(decl, list) and decl and isinstance(decl[0], list):
|
||||
allowed[name] = set(decl[0])
|
||||
for in_name, val in node.get("inputs", {}).items():
|
||||
if in_name in _MODEL_INPUTS and isinstance(val, str):
|
||||
opts = allowed.get(in_name)
|
||||
if opts is not None and val not in opts:
|
||||
missing_models.append(
|
||||
{"node": nid, "input": in_name, "value": val}
|
||||
)
|
||||
|
||||
# dedup missing_nodes preservando orden
|
||||
seen = set()
|
||||
missing_nodes = [c for c in missing_nodes if not (c in seen or seen.add(c))]
|
||||
valid = not missing_nodes and not missing_models
|
||||
return {
|
||||
"ok": True,
|
||||
"valid": valid,
|
||||
"missing_nodes": missing_nodes,
|
||||
"missing_models": missing_models,
|
||||
"error": "",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
sys.path.insert(0, _THIS_DIR)
|
||||
from comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
|
||||
wf = comfyui_build_txt2img_workflow("dreamshaper_8.safetensors", "a cat")
|
||||
print(json.dumps(comfyui_validate_workflow(wf), indent=2))
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: comfyui_wait_result
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_wait_result(prompt_id: str, server: str = \"127.0.0.1:8188\", timeout: float = 180.0, poll_interval: float = 1.0) -> dict"
|
||||
description: "Sondea GET /history/{prompt_id} hasta que un prompt ComfyUI completa (status.completed o status_str success/error) o se agota el timeout. Devuelve los outputs por nodo (node_id -> {images: [...]}). Polling como mecanismo principal (no WebSocket). Impura: HTTP GET en bucle + sleep, solo stdlib."
|
||||
tags: [comfyui, ml, image-generation, stable-diffusion, http, polling]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: prompt_id
|
||||
desc: "id devuelto por comfyui_submit_workflow (clave 'prompt_id' de su respuesta)."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema (default '127.0.0.1:8188')."
|
||||
- name: timeout
|
||||
desc: "Maximo de segundos a esperar antes de lanzar TimeoutError."
|
||||
- name: poll_interval
|
||||
desc: "Segundos entre sondeos de /history."
|
||||
output: "dict de outputs {node_id: {'images': [{'filename', 'subfolder', 'type'}, ...]}} tal como ComfyUI los expone en history[prompt_id]['outputs']. Para un txt2img, el nodo SaveImage ('9') trae el PNG. Puede contener otros tipos (gifs, texto) segun los nodos del workflow."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_wait_result.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
from ml.comfyui_submit_workflow import comfyui_submit_workflow
|
||||
from ml.comfyui_wait_result import comfyui_wait_result
|
||||
|
||||
wf = comfyui_build_txt2img_workflow(
|
||||
ckpt_name="v1-5-pruned-emaonly-fp16.safetensors",
|
||||
positive="a red apple on a wooden table, sharp focus",
|
||||
)
|
||||
pid = comfyui_submit_workflow(wf)["prompt_id"]
|
||||
outputs = comfyui_wait_result(pid, timeout=240)
|
||||
for node_id, out in outputs.items():
|
||||
for img in out.get("images", []):
|
||||
print(img["filename"]) # ej. comfy_00001_.png en ~/ComfyUI/output/
|
||||
```
|
||||
|
||||
O lanzable directo (build + submit + wait) con: `./fn run comfyui_wait_result`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Tercer y ultimo paso del round-trip: tras `comfyui_submit_workflow`, para
|
||||
bloquear hasta que la generacion termine y recuperar las rutas de los PNG
|
||||
generados. Usala cuando quieras el resultado fija (no streaming de progreso paso
|
||||
a paso) — es portable porque solo depende de HTTP, no de websocket-client.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Bloquea el hilo (sondea + duerme). Para varias generaciones en paralelo,
|
||||
encola todas con submit y luego espera cada prompt_id, o usa hilos.
|
||||
- El timeout por defecto (180s) puede quedarse corto en GPUs lentas o workflows
|
||||
pesados (muchos steps, alta resolucion, upscalers). Sube `timeout` segun el
|
||||
caso. Lanza TimeoutError si se agota.
|
||||
- Lanza RuntimeError si la ejecucion termina con status_str "error" (el detalle
|
||||
del fallo va en el mensaje) o si no se puede conectar al servidor.
|
||||
- Devuelve metadatos de los PNG (filename, subfolder, type), NO los bytes de la
|
||||
imagen. Los archivos quedan en la carpeta output/ del servidor; para leerlos
|
||||
desde otra maquina usa GET /view?filename=...&subfolder=...&type=output.
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Sondea GET /history/{prompt_id} hasta que un workflow ComfyUI termina.
|
||||
|
||||
Funcion impura: hace red (HTTP GET en bucle) y duerme entre sondeos. Solo
|
||||
stdlib (urllib, json, time).
|
||||
|
||||
Usa polling de /history como mecanismo principal (no WebSocket): es mas robusto
|
||||
porque no depende de websocket-client, que no esta garantizado en el venv. Para
|
||||
saber si el resultado esta listo (no streaming de progreso paso a paso) el
|
||||
polling de /history es suficiente y portable.
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def comfyui_wait_result(
|
||||
prompt_id: str,
|
||||
server: str = "127.0.0.1:8188",
|
||||
timeout: float = 180.0,
|
||||
poll_interval: float = 1.0,
|
||||
) -> dict:
|
||||
"""Espera a que ComfyUI termine de ejecutar un prompt y devuelve sus outputs.
|
||||
|
||||
Sondea GET /history/{prompt_id} cada poll_interval segundos hasta que
|
||||
status.completed es True o status.status_str es "success"/"error", o hasta
|
||||
agotar el timeout.
|
||||
|
||||
Args:
|
||||
prompt_id: id devuelto por comfyui_submit_workflow.
|
||||
server: host:port del servidor ComfyUI (sin esquema).
|
||||
timeout: maximo de segundos a esperar antes de fallar.
|
||||
poll_interval: segundos entre sondeos.
|
||||
|
||||
Returns:
|
||||
dict de outputs {node_id: {"images": [{filename, subfolder, type}, ...]}}
|
||||
tal como ComfyUI los expone en history[prompt_id]["outputs"]. Puede
|
||||
contener otros tipos de output (gifs, texto) segun los nodos del
|
||||
workflow.
|
||||
|
||||
Raises:
|
||||
TimeoutError: si se agota el timeout sin que el prompt complete.
|
||||
RuntimeError: si la ejecucion termina con status_str "error", si no se
|
||||
puede conectar, o si la respuesta no es JSON valido.
|
||||
"""
|
||||
url = f"http://{server}/history/{prompt_id}"
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
||||
hist = json.loads(resp.read())
|
||||
except urllib.error.URLError as exc:
|
||||
raise RuntimeError(
|
||||
f"comfyui_wait_result: no se pudo conectar a {url}: {exc.reason}"
|
||||
) from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(
|
||||
f"comfyui_wait_result: respuesta no es JSON valido desde {url}: {exc}"
|
||||
) from exc
|
||||
|
||||
entry = hist.get(prompt_id)
|
||||
if entry:
|
||||
status = entry.get("status", {})
|
||||
status_str = status.get("status_str")
|
||||
if status_str == "error":
|
||||
raise RuntimeError(
|
||||
f"comfyui_wait_result: ejecucion fallo para {prompt_id}: "
|
||||
f"{json.dumps(status)[:500]}"
|
||||
)
|
||||
if status.get("completed") or status_str == "success":
|
||||
return entry.get("outputs", {})
|
||||
time.sleep(poll_interval)
|
||||
|
||||
raise TimeoutError(
|
||||
f"comfyui_wait_result: timeout de {timeout}s esperando {prompt_id}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
from comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
from comfyui_submit_workflow import comfyui_submit_workflow
|
||||
|
||||
wf = comfyui_build_txt2img_workflow(
|
||||
ckpt_name="v1-5-pruned-emaonly-fp16.safetensors",
|
||||
positive="a red apple on a wooden table, sharp focus",
|
||||
negative="blurry, low quality",
|
||||
steps=20,
|
||||
seed=42,
|
||||
)
|
||||
resp = comfyui_submit_workflow(wf)
|
||||
pid = resp["prompt_id"]
|
||||
print(f"esperando prompt_id={pid} ...", file=sys.stderr)
|
||||
outputs = comfyui_wait_result(pid)
|
||||
for node_id, out in outputs.items():
|
||||
for img in out.get("images", []):
|
||||
print(f"OUTPUT node={node_id} filename={img['filename']}")
|
||||
Reference in New Issue
Block a user