feat(ml): comfyui_build_flux_workflow — builder Flux schnell+dev (camino custom-advanced)
Builder puro que arma el workflow ComfyUI de Flux en API format con el camino canonico custom-advanced (UNETLoader + DualCLIPLoader[flux] + VAELoader -> RandomNoise + KSamplerSelect + BasicScheduler -> BasicGuider -> SamplerCustomAdvanced -> VAEDecode -> SaveImage). - variant 'schnell' (~4 pasos, sin FluxGuidance) o 'dev' (~20 pasos, con FluxGuidance), con unet y steps por defecto por variante. - Parametro 'available' opcional valida los modelos contra /object_info y lanza FileNotFoundError claro (que falta + carpeta) sin romper la pureza. - width/height/seed/guidance/prefijo parametrizables. - 11 tests unitarios (class_types schnell vs dev, defaults por variante, error path, determinismo). Verificado con generaciones reales (schnell 1024 y 768, dev 768x1024) que producen PNG en disco.
This commit is contained in:
@@ -3,11 +3,11 @@ name: comfyui_build_flux_workflow
|
|||||||
kind: function
|
kind: function
|
||||||
lang: py
|
lang: py
|
||||||
domain: ml
|
domain: ml
|
||||||
version: "1.0.0"
|
version: "1.1.0"
|
||||||
purity: pure
|
purity: pure
|
||||||
signature: "def comfyui_build_flux_workflow(prompt: str, *, unet: str = \"IMG_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"
|
signature: "def comfyui_build_flux_workflow(prompt: str, *, variant: str = \"schnell\", width: int = 1024, height: int = 1024, steps: int | None = None, guidance: float = 3.5, seed: int = 0, unet_name: str | None = None, clip_l_name: str = \"clip_l.safetensors\", t5xxl_name: str = \"t5xxl_fp8_e4m3fn_scaled.safetensors\", vae_name: str = \"ae.safetensors\", weight_dtype: str = \"default\", sampler_name: str = \"euler\", scheduler: str = \"simple\", filename_prefix: str = \"flux\", available: dict | None = None) -> 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."
|
description: "Construye el dict de un workflow ComfyUI para Flux (schnell o dev) 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 y muestrea con el camino custom-advanced: RandomNoise + KSamplerSelect + BasicScheduler -> BasicGuider -> SamplerCustomAdvanced -> VAEDecode -> SaveImage. variant=schnell (~4 pasos, sin FluxGuidance) o dev (~20 pasos, con FluxGuidance). Validacion opcional de modelos via 'available'. Pura, sin red ni I/O. Hermana de comfyui_build_txt2img_workflow."
|
||||||
tags: [comfyui, flux, ml, txt2img, workflow]
|
tags: [comfyui, flux, ml, txt2img, workflow, image-generation]
|
||||||
uses_functions: []
|
uses_functions: []
|
||||||
uses_types: []
|
uses_types: []
|
||||||
returns: []
|
returns: []
|
||||||
@@ -16,36 +16,40 @@ error_type: ""
|
|||||||
imports: []
|
imports: []
|
||||||
params:
|
params:
|
||||||
- name: prompt
|
- name: prompt
|
||||||
desc: "Prompt positivo: lo que se quiere ver en la imagen."
|
desc: "Prompt positivo: lo que se quiere ver. Flux ignora el negativo, por eso no se codifica."
|
||||||
- name: unet
|
- name: variant
|
||||||
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."
|
desc: "'schnell' (rapido, ~4 pasos, sin FluxGuidance) o 'dev' (~20 pasos, con FluxGuidance). Determina el unet y los steps por defecto. 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
|
- name: width
|
||||||
desc: "Ancho del latente/imagen en px, multiplo de 16 para SD3/Flux. keyword-only."
|
desc: "Ancho del latente/imagen en px, multiplo de 8. keyword-only."
|
||||||
- name: height
|
- name: height
|
||||||
desc: "Alto del latente/imagen en px, multiplo de 16 para SD3/Flux. keyword-only."
|
desc: "Alto del latente/imagen en px, multiplo de 8. keyword-only."
|
||||||
- name: steps
|
- name: steps
|
||||||
desc: "Pasos de sampling del KSampler. Flux schnell rinde con ~4; Flux dev necesita ~20. keyword-only."
|
desc: "Pasos de sampling (BasicScheduler). Si None, default por variante: schnell=4, dev=20. keyword-only."
|
||||||
- name: guidance
|
- 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."
|
desc: "Valor del nodo FluxGuidance. Solo se aplica en variant=dev; en schnell se ignora (la guia va fija dentro del modelo distilado). dev responde a 3.0-4.0. keyword-only."
|
||||||
- name: seed
|
- name: seed
|
||||||
desc: "Semilla del KSampler. 0 es determinista; cambiar para variar la imagen. keyword-only."
|
desc: "Semilla de RandomNoise. 0 es determinista; cambiar para variar la imagen. keyword-only."
|
||||||
|
- name: unet_name
|
||||||
|
desc: "Nombre del modelo de difusion en UNETLoader (unet_name de /object_info). Si None, default por variante (IMG_flux1-schnell-fp8-e4m3fn.safetensors / IMG_flux1-dev-fp8-e4m3fn.safetensors). keyword-only."
|
||||||
|
- name: clip_l_name
|
||||||
|
desc: "Nombre del encoder CLIP-L en DualCLIPLoader (clip_name2). Por defecto 'clip_l.safetensors'. keyword-only."
|
||||||
|
- name: t5xxl_name
|
||||||
|
desc: "Nombre del encoder T5-XXL en DualCLIPLoader (clip_name1). Por defecto 't5xxl_fp8_e4m3fn_scaled.safetensors'. keyword-only."
|
||||||
|
- name: vae_name
|
||||||
|
desc: "Nombre del VAE en VAELoader (vae_name). Por defecto 'ae.safetensors', el autoencoder de Flux. keyword-only."
|
||||||
- name: weight_dtype
|
- 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."
|
desc: "dtype de carga del UNET (uno de 'default', 'fp8_e4m3fn', 'fp8_e4m3fn_fast', 'fp8_e5m2'). Los modelos ya son fp8; 'default' los carga tal cual. keyword-only."
|
||||||
- name: sampler_name
|
- name: sampler_name
|
||||||
desc: "Nombre del sampler (Flux usa 'euler'). keyword-only."
|
desc: "Nombre del sampler para KSamplerSelect (Flux usa 'euler'). keyword-only."
|
||||||
- name: scheduler
|
- name: scheduler
|
||||||
desc: "Scheduler del sampler (Flux usa 'simple'). keyword-only."
|
desc: "Scheduler para BasicScheduler (Flux usa 'simple'). keyword-only."
|
||||||
- name: filename_prefix
|
- name: filename_prefix
|
||||||
desc: "Prefijo del PNG que SaveImage escribe en output/. keyword-only."
|
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."
|
- name: available
|
||||||
|
desc: "Mapa opcional para validar que los modelos existen en el servidor, con claves opcionales 'unet', 'clip', 'vae' (cada una lista de nombres de /object_info). Si se pasa y un modelo elegido falta, lanza FileNotFoundError indicando que falta y donde colocarlo. None = sin validacion. keyword-only."
|
||||||
|
output: "dict en API format con node_ids string como claves (UNETLoader '12', DualCLIPLoader '11', VAELoader '10', EmptyLatentImage '5', CLIPTextEncode '6', FluxGuidance '21' solo en dev, RandomNoise '25', KSamplerSelect '16', BasicScheduler '17', BasicGuider '22', SamplerCustomAdvanced '13', VAEDecode '8', SaveImage '9'). Listo para comfyui_submit_workflow."
|
||||||
tested: true
|
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)"]
|
tests: ["class_types esperados del camino custom-advanced", "schnell: sin nodo FluxGuidance, BasicGuider consume CLIPTextEncode directo", "dev: nodo FluxGuidance presente con guidance, BasicGuider lo consume", "steps default por variante (schnell=4, dev=20)", "width/height/seed reflejados en sus nodos", "available: FileNotFoundError si falta un modelo", "variant invalido -> ValueError", "determinismo: misma entrada -> mismo dict (builder puro)"]
|
||||||
test_file_path: "python/functions/ml/tests/test_comfyui_build_flux_workflow.py"
|
test_file_path: "python/functions/ml/tests/test_comfyui_build_flux_workflow.py"
|
||||||
file_path: "python/functions/ml/comfyui_build_flux_workflow.py"
|
file_path: "python/functions/ml/comfyui_build_flux_workflow.py"
|
||||||
---
|
---
|
||||||
@@ -56,22 +60,38 @@ file_path: "python/functions/ml/comfyui_build_flux_workflow.py"
|
|||||||
import sys, os
|
import sys, os
|
||||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
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
|
from ml.comfyui_build_flux_workflow import comfyui_build_flux_workflow
|
||||||
|
from ml.comfyui_submit_workflow import comfyui_submit_workflow
|
||||||
|
from ml.comfyui_wait_result import comfyui_wait_result
|
||||||
|
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
|
||||||
|
|
||||||
|
# Flux schnell: rapido, ~4 pasos, sin FluxGuidance.
|
||||||
wf = comfyui_build_flux_workflow(
|
wf = comfyui_build_flux_workflow(
|
||||||
prompt="a red apple on a wooden table, sharp focus, studio lighting",
|
"a red apple on a wooden table, sharp focus, studio light",
|
||||||
|
variant="schnell",
|
||||||
width=1024,
|
width=1024,
|
||||||
height=1024,
|
height=1024,
|
||||||
steps=4, # Flux schnell: ~4 pasos basta
|
|
||||||
seed=42,
|
seed=42,
|
||||||
)
|
)
|
||||||
# wf["10"]["class_type"] == "UNETLoader" # modelo de difusion suelto
|
# wf["12"]["class_type"] == "UNETLoader" # modelo de difusion suelto
|
||||||
# wf["11"]["inputs"]["type"] == "flux" # DualCLIPLoader en modo flux
|
# wf["11"]["inputs"]["type"] == "flux" # DualCLIPLoader en modo flux
|
||||||
# wf["3"]["inputs"]["positive"] == ["13", 0] # KSampler consume FluxGuidance
|
# "21" not in wf # schnell no lleva FluxGuidance
|
||||||
# wf["3"]["inputs"]["cfg"] == 1.0 # la guia va por FluxGuidance
|
# wf["22"]["inputs"]["conditioning"] == ["6", 0] # BasicGuider <- CLIPTextEncode
|
||||||
# wf["9"]["class_type"] == "SaveImage"
|
|
||||||
|
sub = comfyui_submit_workflow(wf, server="127.0.0.1:8188")
|
||||||
|
out = comfyui_wait_result(sub["prompt_id"], server="127.0.0.1:8188")
|
||||||
|
img = out["9"]["images"][0]
|
||||||
|
res = comfyui_fetch_output_image(img["filename"], subfolder=img["subfolder"],
|
||||||
|
server="127.0.0.1:8188", dest_dir="/tmp")
|
||||||
|
print(res["path"]) # PNG en disco
|
||||||
|
|
||||||
|
# Flux dev: ~20 pasos, con FluxGuidance.
|
||||||
|
wf_dev = comfyui_build_flux_workflow("a misty forest at dawn", variant="dev",
|
||||||
|
guidance=3.5, width=768, height=1024)
|
||||||
|
# wf_dev["21"]["class_type"] == "FluxGuidance"
|
||||||
|
# wf_dev["22"]["inputs"]["conditioning"] == ["21", 0]
|
||||||
```
|
```
|
||||||
|
|
||||||
O lanzable directo con: `./fn run comfyui_build_flux_workflow` (imprime el JSON del workflow de ejemplo).
|
O lanzable directo con: `./fn run comfyui_build_flux_workflow` (imprime el JSON del workflow schnell de ejemplo).
|
||||||
|
|
||||||
## Cuando usarla
|
## Cuando usarla
|
||||||
|
|
||||||
@@ -79,26 +99,34 @@ 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
|
dict del workflow para `comfyui_submit_workflow`. Usala en lugar de
|
||||||
`comfyui_build_txt2img_workflow` siempre que el modelo NO sea un checkpoint
|
`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.
|
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.
|
Flux schnell es ideal en GPU de poca VRAM (8GB) por el fp8 y los ~4 pasos; dev
|
||||||
|
da mejor calidad a cambio de mas tiempo.
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- Es API format (nodos numerados), NO el formato de la UI de ComfyUI (graph con
|
- 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
|
links). No se puede pegar en la UI tal cual; es el formato que acepta POST
|
||||||
/prompt.
|
/prompt.
|
||||||
- Flux NO usa el cfg del KSampler para guiar: este builder lo fija a 1.0 y la
|
- Camino de muestreo custom-advanced (RandomNoise + KSamplerSelect +
|
||||||
guia va por el nodo FluxGuidance. Subir el cfg del KSampler con Flux degrada o
|
BasicScheduler -> BasicGuider -> SamplerCustomAdvanced), el patron oficial de
|
||||||
rompe la imagen.
|
Flux. NO usa KSampler ni cfg; la guia va por FluxGuidance (solo en dev).
|
||||||
- El negativo es un CLIPTextEncode vacio cableado al KSampler (igual que el
|
- schnell es destilado: NO lleva FluxGuidance y practicamente ignora el prompt
|
||||||
template oficial de Flux). Flux schnell es destilado y practicamente ignora el
|
negativo. dev SI lleva FluxGuidance (nodo '21'); subir `guidance` aumenta la
|
||||||
negativo; no esperes que un prompt negativo tenga el efecto de SD1.5/SDXL.
|
adherencia al prompt.
|
||||||
- `unet`, `clip_l`, `t5xxl` y `vae` deben existir en los directorios respectivos
|
- Los modelos (unet/clip_l/t5xxl/vae) deben existir en el servidor. Esta funcion
|
||||||
visibles para el servidor (models/diffusion_models/, models/text_encoders/,
|
es pura y no toca disco: por defecto NO valida. Pasa `available` (las listas de
|
||||||
models/vae/). Si no, ComfyUI rechaza el workflow con HTTP 400 al enviarlo (no
|
/object_info) para que valide y lance FileNotFoundError con la carpeta destino
|
||||||
aqui — esta funcion es pura y no valida contra el servidor). Valida antes con
|
si falta alguno, ANTES de enviar nada a la GPU. Sin `available`, un modelo
|
||||||
`comfyui_validate_workflow`.
|
ausente lo detecta `comfyui_submit_workflow` (HTTP 400 con detalle).
|
||||||
- `width`/`height` deben ser multiplos de 16 para EmptySD3LatentImage (Flux), no
|
- `width`/`height` deben ser multiplos de 8 (EmptyLatentImage). Flux trabaja bien
|
||||||
de 8 como en SD1.5/SDXL.
|
a 1024x1024; tamanos grandes suben mucho la VRAM en 8GB.
|
||||||
- `weight_dtype` debe ser uno de los que admite UNETLoader ('default',
|
- Los `clip_name1`/`clip_name2` del DualCLIPLoader van en orden t5xxl, clip_l
|
||||||
'fp8_e4m3fn', 'fp8_e4m3fn_fast', 'fp8_e5m2'). En 8GB usa fp8 o el modelo no
|
(igual que el template oficial). El modo flux carga ambos; el orden no afecta
|
||||||
cabe en VRAM.
|
al resultado.
|
||||||
|
|
||||||
|
## Capability growth log
|
||||||
|
|
||||||
|
- v1.1.0 (27/06/2026) — refactor al camino custom-advanced (SamplerCustomAdvanced
|
||||||
|
+ BasicGuider), nuevo parametro `variant` (schnell/dev con steps por defecto),
|
||||||
|
FluxGuidance solo en dev, y `available` para validar modelos faltantes con
|
||||||
|
error claro (FileNotFoundError) sin romper la pureza.
|
||||||
|
|||||||
@@ -1,136 +1,241 @@
|
|||||||
"""Construye un workflow ComfyUI txt2img con Flux en "API format" (dict de nodos numerados).
|
"""Construye un workflow ComfyUI para Flux (schnell o dev) en "API format".
|
||||||
|
|
||||||
API format: cada clave es un node_id (string); cada nodo tiene class_type +
|
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
|
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
|
el formato que acepta POST /prompt, distinto del formato de la UI (graph con
|
||||||
links explicitos).
|
links explicitos).
|
||||||
|
|
||||||
A diferencia del builder SD1.5/SDXL (comfyui_build_txt2img_workflow), Flux NO usa
|
Flux NO se carga como un checkpoint clasico (no CheckpointLoaderSimple). El
|
||||||
un checkpoint todo-en-uno: carga por separado el modelo de difusion (UNETLoader),
|
modelo de difusion se carga con UNETLoader; los dos text encoders (clip_l + t5xxl)
|
||||||
los dos text encoders (DualCLIPLoader con clip_l + t5xxl, type="flux") y el VAE
|
con DualCLIPLoader (type="flux"); el VAE con VAELoader. El muestreo usa el camino
|
||||||
(VAELoader). La guia no va por el cfg del KSampler (que se fija a 1.0) sino por el
|
"custom advanced" (RandomNoise -> KSamplerSelect + BasicScheduler -> BasicGuider
|
||||||
nodo FluxGuidance aplicado al condicionamiento positivo. El negativo se deja como
|
-> SamplerCustomAdvanced), que es el patron canonico de los ejemplos oficiales de
|
||||||
un CLIPTextEncode vacio, igual que el template oficial de Flux en ComfyUI.
|
Flux y el que produce resultados estables con los modelos fp8 distilados.
|
||||||
|
|
||||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
|
Diferencias schnell vs dev:
|
||||||
|
- schnell: modelo distilado, ~4 pasos, sin FluxGuidance (la guia va fija dentro
|
||||||
|
del modelo). Rapido. El conditioning del prompt va directo a BasicGuider.
|
||||||
|
- dev: ~20 pasos, el conditioning pasa antes por FluxGuidance (guidance ~3.5),
|
||||||
|
que sube la adherencia al prompt a costa de tiempo. Mejor calidad.
|
||||||
|
|
||||||
|
Flux ignora el prompt negativo, por eso solo se codifica el positivo.
|
||||||
|
|
||||||
|
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos. La
|
||||||
|
validacion de existencia de modelos en disco se hace pasando `available` (mapa
|
||||||
|
de modelos que el servidor expone via /object_info); recibir ese mapa como
|
||||||
|
argumento no rompe la pureza (el caller hace la unica peticion de red).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Modelos por defecto para cada variante (nombres tal como los expone el
|
||||||
|
# servidor ComfyUI en /object_info; verificados contra UNETLoader.unet_name,
|
||||||
|
# DualCLIPLoader.clip_name1/2 y VAELoader.vae_name).
|
||||||
|
_DEFAULT_UNET = {
|
||||||
|
"schnell": "IMG_flux1-schnell-fp8-e4m3fn.safetensors",
|
||||||
|
"dev": "IMG_flux1-dev-fp8-e4m3fn.safetensors",
|
||||||
|
}
|
||||||
|
_DEFAULT_STEPS = {"schnell": 4, "dev": 20}
|
||||||
|
|
||||||
|
# Carpeta destino por rol de modelo, para mensajes de error utiles. ComfyUI
|
||||||
|
# acepta tanto la carpeta "diffusion_models" (moderna) como "unet" (legacy) para
|
||||||
|
# el UNET; los text encoders en "text_encoders" o "clip"; el VAE en "vae".
|
||||||
|
_MODEL_DIRS = {
|
||||||
|
"unet": "models/diffusion_models/ (o models/unet/)",
|
||||||
|
"clip": "models/text_encoders/ (o models/clip/)",
|
||||||
|
"vae": "models/vae/",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def comfyui_build_flux_workflow(
|
def comfyui_build_flux_workflow(
|
||||||
prompt: str,
|
prompt: str,
|
||||||
*,
|
*,
|
||||||
unet: str = "IMG_flux1-schnell-fp8-e4m3fn.safetensors",
|
variant: str = "schnell",
|
||||||
clip_l: str = "clip_l.safetensors",
|
|
||||||
t5xxl: str = "t5xxl_fp8_e4m3fn_scaled.safetensors",
|
|
||||||
vae: str = "ae.safetensors",
|
|
||||||
width: int = 1024,
|
width: int = 1024,
|
||||||
height: int = 1024,
|
height: int = 1024,
|
||||||
steps: int = 4,
|
steps: int | None = None,
|
||||||
guidance: float = 3.5,
|
guidance: float = 3.5,
|
||||||
seed: int = 0,
|
seed: int = 0,
|
||||||
weight_dtype: str = "fp8_e4m3fn",
|
unet_name: str | None = None,
|
||||||
|
clip_l_name: str = "clip_l.safetensors",
|
||||||
|
t5xxl_name: str = "t5xxl_fp8_e4m3fn_scaled.safetensors",
|
||||||
|
vae_name: str = "ae.safetensors",
|
||||||
|
weight_dtype: str = "default",
|
||||||
sampler_name: str = "euler",
|
sampler_name: str = "euler",
|
||||||
scheduler: str = "simple",
|
scheduler: str = "simple",
|
||||||
filename_prefix: str = "comfy_flux",
|
filename_prefix: str = "flux",
|
||||||
|
available: dict | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Construye el dict del workflow txt2img de Flux (schnell/dev).
|
"""Construye el dict del workflow Flux (schnell o dev) en API format.
|
||||||
|
|
||||||
Cadena de nodos: UNETLoader + DualCLIPLoader + VAELoader -> CLIPTextEncode
|
Cadena de nodos: UNETLoader + DualCLIPLoader + VAELoader -> CLIPTextEncode
|
||||||
(positivo) -> FluxGuidance, mas un CLIPTextEncode vacio para el negativo y
|
[-> FluxGuidance solo en dev] -> BasicGuider; RandomNoise + KSamplerSelect +
|
||||||
EmptySD3LatentImage -> KSampler -> VAEDecode -> SaveImage.
|
BasicScheduler + EmptyLatentImage -> SamplerCustomAdvanced -> VAEDecode ->
|
||||||
|
SaveImage.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prompt: prompt positivo (lo que se quiere ver en la imagen).
|
prompt: prompt positivo (lo que se quiere ver). Flux ignora el negativo.
|
||||||
unet: nombre del modelo de difusion en models/diffusion_models/ tal como
|
variant: "schnell" (rapido, ~4 pasos, sin FluxGuidance) o "dev"
|
||||||
lo lista comfyui_object_info para UNETLoader (unet_name). Por defecto
|
(~20 pasos, con FluxGuidance). keyword-only.
|
||||||
el Flux schnell fp8 ("IMG_flux1-schnell-fp8-e4m3fn.safetensors").
|
width: ancho del latente/imagen en px (multiplo de 8). keyword-only.
|
||||||
clip_l: nombre del encoder CLIP-L en models/text_encoders/ (clip_name2 del
|
height: alto del latente/imagen en px (multiplo de 8). keyword-only.
|
||||||
DualCLIPLoader). Por defecto "clip_l.safetensors".
|
steps: pasos de sampling. Si None, default por variante (schnell=4,
|
||||||
t5xxl: nombre del encoder T5-XXL en models/text_encoders/ (clip_name1 del
|
dev=20). keyword-only.
|
||||||
DualCLIPLoader). Por defecto "t5xxl_fp8_e4m3fn_scaled.safetensors".
|
guidance: valor de FluxGuidance. Solo se aplica en variant="dev"; en
|
||||||
vae: nombre del VAE en models/vae/ (vae_name del VAELoader). Por defecto
|
schnell se ignora (el modelo distilado lleva la guia fija).
|
||||||
"ae.safetensors" (el autoencoder de Flux).
|
keyword-only.
|
||||||
width: ancho del latente/imagen en px (multiplo de 16 para SD3/Flux). keyword-only.
|
seed: semilla de RandomNoise (cambia para variar la imagen). keyword-only.
|
||||||
height: alto del latente/imagen en px (multiplo de 16 para SD3/Flux). keyword-only.
|
unet_name: nombre del modelo de difusion en UNETLoader. Si None, default
|
||||||
steps: pasos de sampling del KSampler. Flux schnell rinde bien con ~4;
|
por variante. keyword-only.
|
||||||
Flux dev necesita ~20. keyword-only.
|
clip_l_name: nombre del encoder CLIP-L en DualCLIPLoader. keyword-only.
|
||||||
guidance: valor del nodo FluxGuidance (no es el cfg clasico). Schnell es
|
t5xxl_name: nombre del encoder T5-XXL en DualCLIPLoader. keyword-only.
|
||||||
poco sensible a este valor; dev responde a 3.0-4.0. keyword-only.
|
vae_name: nombre del VAE en VAELoader. keyword-only.
|
||||||
seed: semilla del KSampler (0 = determinista; cambia para variar). keyword-only.
|
weight_dtype: dtype de los pesos del UNET ("default", "fp8_e4m3fn",
|
||||||
weight_dtype: dtype de carga del UNET (uno de "default", "fp8_e4m3fn",
|
"fp8_e4m3fn_fast", "fp8_e5m2"). keyword-only.
|
||||||
"fp8_e4m3fn_fast", "fp8_e5m2"). fp8 reduce VRAM (clave en 8GB). keyword-only.
|
sampler_name: sampler para KSamplerSelect (ej. "euler"). keyword-only.
|
||||||
sampler_name: nombre del sampler (Flux usa "euler"). keyword-only.
|
scheduler: scheduler para BasicScheduler (ej. "simple"). keyword-only.
|
||||||
scheduler: scheduler del sampler (Flux usa "simple"). keyword-only.
|
filename_prefix: prefijo del PNG generado por SaveImage en output/.
|
||||||
filename_prefix: prefijo del PNG que SaveImage escribe en output/. keyword-only.
|
keyword-only.
|
||||||
|
available: mapa opcional para validar que los modelos existen en el
|
||||||
|
servidor, con claves opcionales "unet", "clip", "vae", cada una una
|
||||||
|
lista de nombres disponibles (tal como /object_info los expone). Si
|
||||||
|
se pasa y algun modelo elegido no esta en su lista, se lanza
|
||||||
|
FileNotFoundError indicando que falta y en que carpeta colocarlo.
|
||||||
|
Si es None (default), no se valida disco. keyword-only.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict en API format listo para comfyui_submit_workflow. Las claves son
|
dict en API format listo para comfyui_submit_workflow. Las claves son
|
||||||
node_ids (string) y cada valor tiene class_type + inputs.
|
node_ids string y cada valor tiene class_type + inputs.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: si variant no es "schnell" ni "dev".
|
||||||
|
FileNotFoundError: si `available` se pasa y algun modelo (unet/clip/vae)
|
||||||
|
no esta disponible en el servidor; el mensaje lista los que faltan y
|
||||||
|
la carpeta donde colocarlos. La funcion NO crashea de forma opaca:
|
||||||
|
falla con un error claro y accionable antes de enviar nada a la GPU.
|
||||||
"""
|
"""
|
||||||
return {
|
if variant not in ("schnell", "dev"):
|
||||||
"10": {
|
raise ValueError(
|
||||||
|
f"comfyui_build_flux_workflow: variant '{variant}' invalido; "
|
||||||
|
f"usa 'schnell' o 'dev'"
|
||||||
|
)
|
||||||
|
|
||||||
|
unet = unet_name or _DEFAULT_UNET[variant]
|
||||||
|
n_steps = steps if steps is not None else _DEFAULT_STEPS[variant]
|
||||||
|
|
||||||
|
# Error path: validar contra los modelos que expone el servidor, si el caller
|
||||||
|
# nos pasa el mapa. Pura (no toca disco; recibe las listas ya obtenidas).
|
||||||
|
if available is not None:
|
||||||
|
missing = []
|
||||||
|
checks = (
|
||||||
|
("unet", unet, available.get("unet")),
|
||||||
|
("clip", clip_l_name, available.get("clip")),
|
||||||
|
("clip", t5xxl_name, available.get("clip")),
|
||||||
|
("vae", vae_name, available.get("vae")),
|
||||||
|
)
|
||||||
|
for role, name, names in checks:
|
||||||
|
if names is not None and name not in names:
|
||||||
|
missing.append(
|
||||||
|
f" - '{name}' (rol {role}) no esta en el servidor; "
|
||||||
|
f"colocalo en {_MODEL_DIRS[role]}"
|
||||||
|
)
|
||||||
|
if missing:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"comfyui_build_flux_workflow: faltan modelos Flux en el "
|
||||||
|
"servidor:\n" + "\n".join(missing)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Loaders (Flux no usa CheckpointLoaderSimple).
|
||||||
|
workflow: dict = {
|
||||||
|
"12": {
|
||||||
"class_type": "UNETLoader",
|
"class_type": "UNETLoader",
|
||||||
"inputs": {"unet_name": unet, "weight_dtype": weight_dtype},
|
"inputs": {"unet_name": unet, "weight_dtype": weight_dtype},
|
||||||
},
|
},
|
||||||
"11": {
|
"11": {
|
||||||
"class_type": "DualCLIPLoader",
|
"class_type": "DualCLIPLoader",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"clip_name1": t5xxl,
|
"clip_name1": t5xxl_name,
|
||||||
"clip_name2": clip_l,
|
"clip_name2": clip_l_name,
|
||||||
"type": "flux",
|
"type": "flux",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"12": {
|
"10": {
|
||||||
"class_type": "VAELoader",
|
"class_type": "VAELoader",
|
||||||
"inputs": {"vae_name": vae},
|
"inputs": {"vae_name": vae_name},
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"class_type": "EmptyLatentImage",
|
||||||
|
"inputs": {"width": width, "height": height, "batch_size": 1},
|
||||||
},
|
},
|
||||||
"6": {
|
"6": {
|
||||||
"class_type": "CLIPTextEncode",
|
"class_type": "CLIPTextEncode",
|
||||||
"inputs": {"text": prompt, "clip": ["11", 0]},
|
"inputs": {"text": prompt, "clip": ["11", 0]},
|
||||||
},
|
},
|
||||||
"13": {
|
}
|
||||||
|
|
||||||
|
# Conditioning hacia BasicGuider. En dev pasa por FluxGuidance; en schnell va
|
||||||
|
# directo (el modelo distilado no usa guidance externo).
|
||||||
|
if variant == "dev":
|
||||||
|
workflow["21"] = {
|
||||||
"class_type": "FluxGuidance",
|
"class_type": "FluxGuidance",
|
||||||
"inputs": {"conditioning": ["6", 0], "guidance": guidance},
|
"inputs": {"conditioning": ["6", 0], "guidance": guidance},
|
||||||
|
}
|
||||||
|
guider_cond = ["21", 0]
|
||||||
|
else:
|
||||||
|
guider_cond = ["6", 0]
|
||||||
|
|
||||||
|
workflow.update(
|
||||||
|
{
|
||||||
|
"25": {
|
||||||
|
"class_type": "RandomNoise",
|
||||||
|
"inputs": {"noise_seed": seed},
|
||||||
},
|
},
|
||||||
"7": {
|
"16": {
|
||||||
"class_type": "CLIPTextEncode",
|
"class_type": "KSamplerSelect",
|
||||||
"inputs": {"text": "", "clip": ["11", 0]},
|
"inputs": {"sampler_name": sampler_name},
|
||||||
},
|
},
|
||||||
"5": {
|
"17": {
|
||||||
"class_type": "EmptySD3LatentImage",
|
"class_type": "BasicScheduler",
|
||||||
"inputs": {"width": width, "height": height, "batch_size": 1},
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"class_type": "KSampler",
|
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"seed": seed,
|
"model": ["12", 0],
|
||||||
"steps": steps,
|
|
||||||
"cfg": 1.0,
|
|
||||||
"sampler_name": sampler_name,
|
|
||||||
"scheduler": scheduler,
|
"scheduler": scheduler,
|
||||||
|
"steps": n_steps,
|
||||||
"denoise": 1.0,
|
"denoise": 1.0,
|
||||||
"model": ["10", 0],
|
},
|
||||||
"positive": ["13", 0],
|
},
|
||||||
"negative": ["7", 0],
|
"22": {
|
||||||
|
"class_type": "BasicGuider",
|
||||||
|
"inputs": {"model": ["12", 0], "conditioning": guider_cond},
|
||||||
|
},
|
||||||
|
"13": {
|
||||||
|
"class_type": "SamplerCustomAdvanced",
|
||||||
|
"inputs": {
|
||||||
|
"noise": ["25", 0],
|
||||||
|
"guider": ["22", 0],
|
||||||
|
"sampler": ["16", 0],
|
||||||
|
"sigmas": ["17", 0],
|
||||||
"latent_image": ["5", 0],
|
"latent_image": ["5", 0],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"8": {
|
"8": {
|
||||||
"class_type": "VAEDecode",
|
"class_type": "VAEDecode",
|
||||||
"inputs": {"samples": ["3", 0], "vae": ["12", 0]},
|
"inputs": {"samples": ["13", 0], "vae": ["10", 0]},
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
"class_type": "SaveImage",
|
"class_type": "SaveImage",
|
||||||
"inputs": {"filename_prefix": filename_prefix, "images": ["8", 0]},
|
"inputs": {"filename_prefix": filename_prefix, "images": ["8", 0]},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
return workflow
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import json
|
import json
|
||||||
|
|
||||||
wf = comfyui_build_flux_workflow(
|
wf = comfyui_build_flux_workflow(
|
||||||
prompt="a red apple on a wooden table, sharp focus, studio lighting",
|
"a red apple on a wooden table, sharp focus, studio light",
|
||||||
|
variant="schnell",
|
||||||
|
width=1024,
|
||||||
|
height=1024,
|
||||||
seed=42,
|
seed=42,
|
||||||
)
|
)
|
||||||
print(json.dumps(wf, indent=2))
|
print(json.dumps(wf, indent=2))
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(__file__))
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
|
|
||||||
@@ -10,35 +12,54 @@ from ml.comfyui_build_flux_workflow import comfyui_build_flux_workflow
|
|||||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||||
|
|
||||||
|
|
||||||
def test_estructura_y_class_types():
|
_BASE_CTS = {
|
||||||
wf = comfyui_build_flux_workflow("POS")
|
|
||||||
assert_api_format(wf)
|
|
||||||
assert class_types(wf) == {
|
|
||||||
"UNETLoader",
|
"UNETLoader",
|
||||||
"DualCLIPLoader",
|
"DualCLIPLoader",
|
||||||
"VAELoader",
|
"VAELoader",
|
||||||
|
"EmptyLatentImage",
|
||||||
"CLIPTextEncode",
|
"CLIPTextEncode",
|
||||||
"FluxGuidance",
|
"RandomNoise",
|
||||||
"EmptySD3LatentImage",
|
"KSamplerSelect",
|
||||||
"KSampler",
|
"BasicScheduler",
|
||||||
|
"BasicGuider",
|
||||||
|
"SamplerCustomAdvanced",
|
||||||
"VAEDecode",
|
"VAEDecode",
|
||||||
"SaveImage",
|
"SaveImage",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_schnell_class_types_sin_fluxguidance():
|
||||||
|
wf = comfyui_build_flux_workflow("POS", variant="schnell")
|
||||||
|
assert_api_format(wf)
|
||||||
|
# schnell usa el camino custom-advanced y NO incluye FluxGuidance.
|
||||||
|
assert class_types(wf) == _BASE_CTS
|
||||||
|
# BasicGuider consume el CLIPTextEncode positivo directo.
|
||||||
|
assert node_by_ct(wf, "BasicGuider")["inputs"]["conditioning"] == ["6", 0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_dev_class_types_con_fluxguidance():
|
||||||
|
wf = comfyui_build_flux_workflow("POS", variant="dev", guidance=2.5)
|
||||||
|
assert_api_format(wf)
|
||||||
|
assert class_types(wf) == _BASE_CTS | {"FluxGuidance"}
|
||||||
|
fg = node_by_ct(wf, "FluxGuidance")["inputs"]
|
||||||
|
assert fg["guidance"] == 2.5
|
||||||
|
assert fg["conditioning"] == ["6", 0] # FluxGuidance aplica sobre el positivo
|
||||||
|
# BasicGuider consume la salida de FluxGuidance, no el CLIPTextEncode directo.
|
||||||
|
assert node_by_ct(wf, "BasicGuider")["inputs"]["conditioning"] == ["21", 0]
|
||||||
|
|
||||||
|
|
||||||
def test_loaders_separados_de_flux():
|
def test_loaders_separados_de_flux():
|
||||||
# Flux carga UNET + dos text encoders + VAE por separado (no checkpoint unico).
|
# Flux carga UNET + dos text encoders + VAE por separado (no checkpoint unico).
|
||||||
wf = comfyui_build_flux_workflow(
|
wf = comfyui_build_flux_workflow(
|
||||||
"POS",
|
"POS",
|
||||||
unet="IMG_flux1-schnell-fp8-e4m3fn.safetensors",
|
variant="schnell",
|
||||||
clip_l="clip_l.safetensors",
|
clip_l_name="clip_l.safetensors",
|
||||||
t5xxl="t5xxl_fp8_e4m3fn_scaled.safetensors",
|
t5xxl_name="t5xxl_fp8_e4m3fn_scaled.safetensors",
|
||||||
vae="ae.safetensors",
|
vae_name="ae.safetensors",
|
||||||
weight_dtype="fp8_e4m3fn",
|
|
||||||
)
|
)
|
||||||
unet = node_by_ct(wf, "UNETLoader")["inputs"]
|
unet = node_by_ct(wf, "UNETLoader")["inputs"]
|
||||||
assert unet["unet_name"] == "IMG_flux1-schnell-fp8-e4m3fn.safetensors"
|
assert unet["unet_name"] == "IMG_flux1-schnell-fp8-e4m3fn.safetensors"
|
||||||
assert unet["weight_dtype"] == "fp8_e4m3fn"
|
assert unet["weight_dtype"] == "default"
|
||||||
dual = node_by_ct(wf, "DualCLIPLoader")["inputs"]
|
dual = node_by_ct(wf, "DualCLIPLoader")["inputs"]
|
||||||
assert dual["type"] == "flux"
|
assert dual["type"] == "flux"
|
||||||
assert dual["clip_name1"] == "t5xxl_fp8_e4m3fn_scaled.safetensors"
|
assert dual["clip_name1"] == "t5xxl_fp8_e4m3fn_scaled.safetensors"
|
||||||
@@ -46,25 +67,36 @@ def test_loaders_separados_de_flux():
|
|||||||
assert node_by_ct(wf, "VAELoader")["inputs"]["vae_name"] == "ae.safetensors"
|
assert node_by_ct(wf, "VAELoader")["inputs"]["vae_name"] == "ae.safetensors"
|
||||||
|
|
||||||
|
|
||||||
def test_guidance_y_cfg_de_flux():
|
def test_unet_default_por_variante():
|
||||||
# La guia va por FluxGuidance; el cfg del KSampler se fija a 1.0 (schnell).
|
schnell = comfyui_build_flux_workflow("POS", variant="schnell")
|
||||||
wf = comfyui_build_flux_workflow("POS", guidance=2.5)
|
dev = comfyui_build_flux_workflow("POS", variant="dev")
|
||||||
assert node_by_ct(wf, "FluxGuidance")["inputs"]["guidance"] == 2.5
|
assert (
|
||||||
ks = node_by_ct(wf, "KSampler")["inputs"]
|
node_by_ct(schnell, "UNETLoader")["inputs"]["unet_name"]
|
||||||
assert ks["cfg"] == 1.0
|
== "IMG_flux1-schnell-fp8-e4m3fn.safetensors"
|
||||||
# KSampler positive consume la salida de FluxGuidance, no la del CLIPTextEncode directo.
|
)
|
||||||
assert ks["positive"] == ["13", 0]
|
assert (
|
||||||
|
node_by_ct(dev, "UNETLoader")["inputs"]["unet_name"]
|
||||||
|
== "IMG_flux1-dev-fp8-e4m3fn.safetensors"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_steps_default_por_variante():
|
||||||
|
schnell = comfyui_build_flux_workflow("POS", variant="schnell")
|
||||||
|
dev = comfyui_build_flux_workflow("POS", variant="dev")
|
||||||
|
assert node_by_ct(schnell, "BasicScheduler")["inputs"]["steps"] == 4
|
||||||
|
assert node_by_ct(dev, "BasicScheduler")["inputs"]["steps"] == 20
|
||||||
|
# steps explicito gana al default.
|
||||||
|
custom = comfyui_build_flux_workflow("POS", variant="schnell", steps=6)
|
||||||
|
assert node_by_ct(custom, "BasicScheduler")["inputs"]["steps"] == 6
|
||||||
|
|
||||||
|
|
||||||
def test_params_se_reflejan_en_los_nodos():
|
def test_params_se_reflejan_en_los_nodos():
|
||||||
wf = comfyui_build_flux_workflow("POS", width=768, height=512, steps=8, seed=123)
|
wf = comfyui_build_flux_workflow(
|
||||||
ks = node_by_ct(wf, "KSampler")["inputs"]
|
"POS", variant="schnell", width=768, height=512, seed=123
|
||||||
assert ks["seed"] == 123
|
)
|
||||||
assert ks["steps"] == 8
|
assert node_by_ct(wf, "RandomNoise")["inputs"]["noise_seed"] == 123
|
||||||
lat = node_by_ct(wf, "EmptySD3LatentImage")["inputs"]
|
lat = node_by_ct(wf, "EmptyLatentImage")["inputs"]
|
||||||
assert lat["width"] == 768 and lat["height"] == 512
|
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():
|
def test_filename_prefix_en_saveimage():
|
||||||
@@ -72,8 +104,36 @@ def test_filename_prefix_en_saveimage():
|
|||||||
assert node_by_ct(wf, "SaveImage")["inputs"]["filename_prefix"] == "demo_flux"
|
assert node_by_ct(wf, "SaveImage")["inputs"]["filename_prefix"] == "demo_flux"
|
||||||
|
|
||||||
|
|
||||||
|
def test_variant_invalido_lanza_valueerror():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
comfyui_build_flux_workflow("POS", variant="turbo")
|
||||||
|
|
||||||
|
|
||||||
|
def test_available_valida_modelos_faltantes():
|
||||||
|
# Si se pasa 'available' y un modelo elegido no esta, lanza FileNotFoundError
|
||||||
|
# con el nombre que falta (error path: no crashea opaco).
|
||||||
|
available = {
|
||||||
|
"unet": ["otro_modelo.safetensors"], # el schnell por defecto NO esta
|
||||||
|
"clip": ["clip_l.safetensors", "t5xxl_fp8_e4m3fn_scaled.safetensors"],
|
||||||
|
"vae": ["ae.safetensors"],
|
||||||
|
}
|
||||||
|
with pytest.raises(FileNotFoundError) as exc:
|
||||||
|
comfyui_build_flux_workflow("POS", variant="schnell", available=available)
|
||||||
|
assert "IMG_flux1-schnell-fp8-e4m3fn.safetensors" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_available_ok_no_lanza():
|
||||||
|
available = {
|
||||||
|
"unet": ["IMG_flux1-schnell-fp8-e4m3fn.safetensors"],
|
||||||
|
"clip": ["clip_l.safetensors", "t5xxl_fp8_e4m3fn_scaled.safetensors"],
|
||||||
|
"vae": ["ae.safetensors"],
|
||||||
|
}
|
||||||
|
wf = comfyui_build_flux_workflow("POS", variant="schnell", available=available)
|
||||||
|
assert_api_format(wf)
|
||||||
|
|
||||||
|
|
||||||
def test_determinista():
|
def test_determinista():
|
||||||
# Builder puro: misma entrada -> mismo dict (sin red, seed fijo, sin estado).
|
# Builder puro: misma entrada -> mismo dict (sin red, seed fijo, sin estado).
|
||||||
a = comfyui_build_flux_workflow("POS", seed=123)
|
a = comfyui_build_flux_workflow("POS", variant="dev", seed=123)
|
||||||
b = comfyui_build_flux_workflow("POS", seed=123)
|
b = comfyui_build_flux_workflow("POS", variant="dev", seed=123)
|
||||||
assert a == b
|
assert a == b
|
||||||
|
|||||||
Reference in New Issue
Block a user