feat(ml): comfyui_build_flux_workflow builder txt2img Flux (API format)
Builder puro hermano de comfyui_build_txt2img_workflow para modelos Flux (schnell/dev): UNETLoader + DualCLIPLoader (clip_l + t5xxl, type flux) + VAELoader -> CLIPTextEncode -> FluxGuidance + EmptySD3LatentImage -> KSampler (cfg fijo 1.0) -> VAEDecode -> SaveImage. La guia va por FluxGuidance, no por el cfg del sampler. fp8 + ~4 pasos para GPU de 8GB. class_type/inputs verificados contra /object_info del server vivo. Validado end-to-end: genera imagen real (prompt_id 909b8876, flux_builder_test_00001_.png, status success). 6 tests unitarios verde. Pagina madre docs/capabilities/comfyui.md actualizada con la fila del builder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
---
|
||||
name: comfyui_build_flux_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_build_flux_workflow(prompt: str, *, unet: str = \"flux1-schnell-fp8-e4m3fn.safetensors\", clip_l: str = \"clip_l.safetensors\", t5xxl: str = \"t5xxl_fp8_e4m3fn_scaled.safetensors\", vae: str = \"ae.safetensors\", width: int = 1024, height: int = 1024, steps: int = 4, guidance: float = 3.5, seed: int = 0, weight_dtype: str = \"fp8_e4m3fn\", sampler_name: str = \"euler\", scheduler: str = \"simple\", filename_prefix: str = \"comfy_flux\") -> dict"
|
||||
description: "Construye el dict de un workflow ComfyUI txt2img con Flux en API format (nodos numerados con class_type + inputs, conexiones como [node_id, output_index]). A diferencia de SD1.5/SDXL, Flux carga por separado UNETLoader + DualCLIPLoader (clip_l + t5xxl, type flux) + VAELoader; la guia va por FluxGuidance (no por el cfg del KSampler, que se fija a 1.0). Cadena: UNETLoader+DualCLIPLoader+VAELoader -> CLIPTextEncode -> FluxGuidance + EmptySD3LatentImage -> KSampler -> VAEDecode -> SaveImage. Pura, sin red ni I/O. Hermana de comfyui_build_txt2img_workflow."
|
||||
tags: [comfyui, flux, ml, txt2img, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: prompt
|
||||
desc: "Prompt positivo: lo que se quiere ver en la imagen."
|
||||
- name: unet
|
||||
desc: "Nombre del modelo de difusion en models/diffusion_models/ tal como lo lista comfyui_object_info para UNETLoader (unet_name). Por defecto el Flux schnell fp8. keyword-only."
|
||||
- name: clip_l
|
||||
desc: "Nombre del encoder CLIP-L en models/text_encoders/ (clip_name2 del DualCLIPLoader). Por defecto 'clip_l.safetensors'. keyword-only."
|
||||
- name: t5xxl
|
||||
desc: "Nombre del encoder T5-XXL en models/text_encoders/ (clip_name1 del DualCLIPLoader). Por defecto 't5xxl_fp8_e4m3fn_scaled.safetensors'. keyword-only."
|
||||
- name: vae
|
||||
desc: "Nombre del VAE en models/vae/ (vae_name del VAELoader). Por defecto 'ae.safetensors', el autoencoder de Flux. keyword-only."
|
||||
- name: width
|
||||
desc: "Ancho del latente/imagen en px, multiplo de 16 para SD3/Flux. keyword-only."
|
||||
- name: height
|
||||
desc: "Alto del latente/imagen en px, multiplo de 16 para SD3/Flux. keyword-only."
|
||||
- name: steps
|
||||
desc: "Pasos de sampling del KSampler. Flux schnell rinde con ~4; Flux dev necesita ~20. keyword-only."
|
||||
- name: guidance
|
||||
desc: "Valor del nodo FluxGuidance (no es el cfg clasico). Schnell es poco sensible; dev responde a 3.0-4.0. keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla del KSampler. 0 es determinista; cambiar para variar la imagen. keyword-only."
|
||||
- name: weight_dtype
|
||||
desc: "dtype de carga del UNET (uno de 'default', 'fp8_e4m3fn', 'fp8_e4m3fn_fast', 'fp8_e5m2'). fp8 reduce VRAM, clave en GPU de 8GB. keyword-only."
|
||||
- name: sampler_name
|
||||
desc: "Nombre del sampler (Flux usa 'euler'). keyword-only."
|
||||
- name: scheduler
|
||||
desc: "Scheduler del sampler (Flux usa 'simple'). keyword-only."
|
||||
- name: filename_prefix
|
||||
desc: "Prefijo del PNG que SaveImage escribe en output/. keyword-only."
|
||||
output: "dict en API format con node_ids como claves (UNETLoader '10', DualCLIPLoader '11', VAELoader '12', CLIPTextEncode positivo '6', FluxGuidance '13', CLIPTextEncode negativo vacio '7', EmptySD3LatentImage '5', KSampler '3', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow."
|
||||
tested: true
|
||||
tests: ["class_types esperados (9 nodos de Flux)", "loaders separados UNET+DualCLIP(flux)+VAE", "guidance via FluxGuidance y cfg del KSampler fijado a 1.0", "params width/height/steps/seed reflejados", "filename_prefix en SaveImage", "determinismo: misma entrada -> mismo dict (builder puro)"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_build_flux_workflow.py"
|
||||
file_path: "python/functions/ml/comfyui_build_flux_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_flux_workflow import comfyui_build_flux_workflow
|
||||
|
||||
wf = comfyui_build_flux_workflow(
|
||||
prompt="a red apple on a wooden table, sharp focus, studio lighting",
|
||||
width=1024,
|
||||
height=1024,
|
||||
steps=4, # Flux schnell: ~4 pasos basta
|
||||
seed=42,
|
||||
)
|
||||
# wf["10"]["class_type"] == "UNETLoader" # modelo de difusion suelto
|
||||
# wf["11"]["inputs"]["type"] == "flux" # DualCLIPLoader en modo flux
|
||||
# wf["3"]["inputs"]["positive"] == ["13", 0] # KSampler consume FluxGuidance
|
||||
# wf["3"]["inputs"]["cfg"] == 1.0 # la guia va por FluxGuidance
|
||||
# wf["9"]["class_type"] == "SaveImage"
|
||||
```
|
||||
|
||||
O lanzable directo con: `./fn run comfyui_build_flux_workflow` (imprime el JSON del workflow de ejemplo).
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando vayas a generar txt2img con un modelo Flux (schnell o dev) y necesites el
|
||||
dict del workflow para `comfyui_submit_workflow`. Usala en lugar de
|
||||
`comfyui_build_txt2img_workflow` siempre que el modelo NO sea un checkpoint
|
||||
todo-en-uno SD1.5/SDXL sino Flux con UNET + text encoders + VAE por separado.
|
||||
Flux schnell es ideal en GPU de poca VRAM (8GB) por el fp8 y los ~4 pasos.
|
||||
|
||||
## 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.
|
||||
- Flux NO usa el cfg del KSampler para guiar: este builder lo fija a 1.0 y la
|
||||
guia va por el nodo FluxGuidance. Subir el cfg del KSampler con Flux degrada o
|
||||
rompe la imagen.
|
||||
- El negativo es un CLIPTextEncode vacio cableado al KSampler (igual que el
|
||||
template oficial de Flux). Flux schnell es destilado y practicamente ignora el
|
||||
negativo; no esperes que un prompt negativo tenga el efecto de SD1.5/SDXL.
|
||||
- `unet`, `clip_l`, `t5xxl` y `vae` deben existir en los directorios respectivos
|
||||
visibles para el servidor (models/diffusion_models/, models/text_encoders/,
|
||||
models/vae/). Si no, ComfyUI rechaza el workflow con HTTP 400 al enviarlo (no
|
||||
aqui — esta funcion es pura y no valida contra el servidor). Valida antes con
|
||||
`comfyui_validate_workflow`.
|
||||
- `width`/`height` deben ser multiplos de 16 para EmptySD3LatentImage (Flux), no
|
||||
de 8 como en SD1.5/SDXL.
|
||||
- `weight_dtype` debe ser uno de los que admite UNETLoader ('default',
|
||||
'fp8_e4m3fn', 'fp8_e4m3fn_fast', 'fp8_e5m2'). En 8GB usa fp8 o el modelo no
|
||||
cabe en VRAM.
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Construye un workflow ComfyUI txt2img con Flux 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).
|
||||
|
||||
A diferencia del builder SD1.5/SDXL (comfyui_build_txt2img_workflow), Flux NO usa
|
||||
un checkpoint todo-en-uno: carga por separado el modelo de difusion (UNETLoader),
|
||||
los dos text encoders (DualCLIPLoader con clip_l + t5xxl, type="flux") y el VAE
|
||||
(VAELoader). La guia no va por el cfg del KSampler (que se fija a 1.0) sino por el
|
||||
nodo FluxGuidance aplicado al condicionamiento positivo. El negativo se deja como
|
||||
un CLIPTextEncode vacio, igual que el template oficial de Flux en ComfyUI.
|
||||
|
||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
|
||||
"""
|
||||
|
||||
|
||||
def comfyui_build_flux_workflow(
|
||||
prompt: str,
|
||||
*,
|
||||
unet: str = "flux1-schnell-fp8-e4m3fn.safetensors",
|
||||
clip_l: str = "clip_l.safetensors",
|
||||
t5xxl: str = "t5xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
vae: str = "ae.safetensors",
|
||||
width: int = 1024,
|
||||
height: int = 1024,
|
||||
steps: int = 4,
|
||||
guidance: float = 3.5,
|
||||
seed: int = 0,
|
||||
weight_dtype: str = "fp8_e4m3fn",
|
||||
sampler_name: str = "euler",
|
||||
scheduler: str = "simple",
|
||||
filename_prefix: str = "comfy_flux",
|
||||
) -> dict:
|
||||
"""Construye el dict del workflow txt2img de Flux (schnell/dev).
|
||||
|
||||
Cadena de nodos: UNETLoader + DualCLIPLoader + VAELoader -> CLIPTextEncode
|
||||
(positivo) -> FluxGuidance, mas un CLIPTextEncode vacio para el negativo y
|
||||
EmptySD3LatentImage -> KSampler -> VAEDecode -> SaveImage.
|
||||
|
||||
Args:
|
||||
prompt: prompt positivo (lo que se quiere ver en la imagen).
|
||||
unet: nombre del modelo de difusion en models/diffusion_models/ tal como
|
||||
lo lista comfyui_object_info para UNETLoader (unet_name). Por defecto
|
||||
el Flux schnell fp8 ("flux1-schnell-fp8-e4m3fn.safetensors").
|
||||
clip_l: nombre del encoder CLIP-L en models/text_encoders/ (clip_name2 del
|
||||
DualCLIPLoader). Por defecto "clip_l.safetensors".
|
||||
t5xxl: nombre del encoder T5-XXL en models/text_encoders/ (clip_name1 del
|
||||
DualCLIPLoader). Por defecto "t5xxl_fp8_e4m3fn_scaled.safetensors".
|
||||
vae: nombre del VAE en models/vae/ (vae_name del VAELoader). Por defecto
|
||||
"ae.safetensors" (el autoencoder de Flux).
|
||||
width: ancho del latente/imagen en px (multiplo de 16 para SD3/Flux). keyword-only.
|
||||
height: alto del latente/imagen en px (multiplo de 16 para SD3/Flux). keyword-only.
|
||||
steps: pasos de sampling del KSampler. Flux schnell rinde bien con ~4;
|
||||
Flux dev necesita ~20. keyword-only.
|
||||
guidance: valor del nodo FluxGuidance (no es el cfg clasico). Schnell es
|
||||
poco sensible a este valor; dev responde a 3.0-4.0. keyword-only.
|
||||
seed: semilla del KSampler (0 = determinista; cambia para variar). keyword-only.
|
||||
weight_dtype: dtype de carga del UNET (uno de "default", "fp8_e4m3fn",
|
||||
"fp8_e4m3fn_fast", "fp8_e5m2"). fp8 reduce VRAM (clave en 8GB). keyword-only.
|
||||
sampler_name: nombre del sampler (Flux usa "euler"). keyword-only.
|
||||
scheduler: scheduler del sampler (Flux usa "simple"). keyword-only.
|
||||
filename_prefix: prefijo del PNG que SaveImage escribe en output/. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow. Las claves son
|
||||
node_ids (string) y cada valor tiene class_type + inputs.
|
||||
"""
|
||||
return {
|
||||
"10": {
|
||||
"class_type": "UNETLoader",
|
||||
"inputs": {"unet_name": unet, "weight_dtype": weight_dtype},
|
||||
},
|
||||
"11": {
|
||||
"class_type": "DualCLIPLoader",
|
||||
"inputs": {
|
||||
"clip_name1": t5xxl,
|
||||
"clip_name2": clip_l,
|
||||
"type": "flux",
|
||||
},
|
||||
},
|
||||
"12": {
|
||||
"class_type": "VAELoader",
|
||||
"inputs": {"vae_name": vae},
|
||||
},
|
||||
"6": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": prompt, "clip": ["11", 0]},
|
||||
},
|
||||
"13": {
|
||||
"class_type": "FluxGuidance",
|
||||
"inputs": {"conditioning": ["6", 0], "guidance": guidance},
|
||||
},
|
||||
"7": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": "", "clip": ["11", 0]},
|
||||
},
|
||||
"5": {
|
||||
"class_type": "EmptySD3LatentImage",
|
||||
"inputs": {"width": width, "height": height, "batch_size": 1},
|
||||
},
|
||||
"3": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"seed": seed,
|
||||
"steps": steps,
|
||||
"cfg": 1.0,
|
||||
"sampler_name": sampler_name,
|
||||
"scheduler": scheduler,
|
||||
"denoise": 1.0,
|
||||
"model": ["10", 0],
|
||||
"positive": ["13", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["5", 0],
|
||||
},
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {"samples": ["3", 0], "vae": ["12", 0]},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": filename_prefix, "images": ["8", 0]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
wf = comfyui_build_flux_workflow(
|
||||
prompt="a red apple on a wooden table, sharp focus, studio lighting",
|
||||
seed=42,
|
||||
)
|
||||
print(json.dumps(wf, indent=2))
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Tests de estructura para comfyui_build_flux_workflow (funcion pura)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_build_flux_workflow import comfyui_build_flux_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_estructura_y_class_types():
|
||||
wf = comfyui_build_flux_workflow("POS")
|
||||
assert_api_format(wf)
|
||||
assert class_types(wf) == {
|
||||
"UNETLoader",
|
||||
"DualCLIPLoader",
|
||||
"VAELoader",
|
||||
"CLIPTextEncode",
|
||||
"FluxGuidance",
|
||||
"EmptySD3LatentImage",
|
||||
"KSampler",
|
||||
"VAEDecode",
|
||||
"SaveImage",
|
||||
}
|
||||
|
||||
|
||||
def test_loaders_separados_de_flux():
|
||||
# Flux carga UNET + dos text encoders + VAE por separado (no checkpoint unico).
|
||||
wf = comfyui_build_flux_workflow(
|
||||
"POS",
|
||||
unet="flux1-schnell-fp8-e4m3fn.safetensors",
|
||||
clip_l="clip_l.safetensors",
|
||||
t5xxl="t5xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
vae="ae.safetensors",
|
||||
weight_dtype="fp8_e4m3fn",
|
||||
)
|
||||
unet = node_by_ct(wf, "UNETLoader")["inputs"]
|
||||
assert unet["unet_name"] == "flux1-schnell-fp8-e4m3fn.safetensors"
|
||||
assert unet["weight_dtype"] == "fp8_e4m3fn"
|
||||
dual = node_by_ct(wf, "DualCLIPLoader")["inputs"]
|
||||
assert dual["type"] == "flux"
|
||||
assert dual["clip_name1"] == "t5xxl_fp8_e4m3fn_scaled.safetensors"
|
||||
assert dual["clip_name2"] == "clip_l.safetensors"
|
||||
assert node_by_ct(wf, "VAELoader")["inputs"]["vae_name"] == "ae.safetensors"
|
||||
|
||||
|
||||
def test_guidance_y_cfg_de_flux():
|
||||
# La guia va por FluxGuidance; el cfg del KSampler se fija a 1.0 (schnell).
|
||||
wf = comfyui_build_flux_workflow("POS", guidance=2.5)
|
||||
assert node_by_ct(wf, "FluxGuidance")["inputs"]["guidance"] == 2.5
|
||||
ks = node_by_ct(wf, "KSampler")["inputs"]
|
||||
assert ks["cfg"] == 1.0
|
||||
# KSampler positive consume la salida de FluxGuidance, no la del CLIPTextEncode directo.
|
||||
assert ks["positive"] == ["13", 0]
|
||||
|
||||
|
||||
def test_params_se_reflejan_en_los_nodos():
|
||||
wf = comfyui_build_flux_workflow("POS", width=768, height=512, steps=8, seed=123)
|
||||
ks = node_by_ct(wf, "KSampler")["inputs"]
|
||||
assert ks["seed"] == 123
|
||||
assert ks["steps"] == 8
|
||||
lat = node_by_ct(wf, "EmptySD3LatentImage")["inputs"]
|
||||
assert lat["width"] == 768 and lat["height"] == 512
|
||||
pos = node_by_ct(wf, "FluxGuidance")["inputs"]["conditioning"]
|
||||
assert pos == ["6", 0] # FluxGuidance aplica sobre el CLIPTextEncode positivo
|
||||
|
||||
|
||||
def test_filename_prefix_en_saveimage():
|
||||
wf = comfyui_build_flux_workflow("POS", filename_prefix="demo_flux")
|
||||
assert node_by_ct(wf, "SaveImage")["inputs"]["filename_prefix"] == "demo_flux"
|
||||
|
||||
|
||||
def test_determinista():
|
||||
# Builder puro: misma entrada -> mismo dict (sin red, seed fijo, sin estado).
|
||||
a = comfyui_build_flux_workflow("POS", seed=123)
|
||||
b = comfyui_build_flux_workflow("POS", seed=123)
|
||||
assert a == b
|
||||
Reference in New Issue
Block a user