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:
2026-06-24 11:55:09 +02:00
parent 68f0ce0dae
commit 3e75d1bf79
4 changed files with 320 additions and 0 deletions
@@ -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