From 532c07cd34e86dea5a1979f3b9bf5ee956dc8989 Mon Sep 17 00:00:00 2001 From: drjones Date: Thu, 27 Aug 2026 20:37:53 -0700 Subject: [PATCH] Add ComfyUI workflow library + 12-app registry (Comfy Apps backend) - comfy_workflows_lib.py: FLUX/SDXL builders, submit/fetch, GPU free/restore - comfy_apps.py: APPS registry (logo/hero/banner/article/portrait/upscale/inpaint/ outpaint/controlnet/ipadapter/face_swap/restore_face) + list_apps/run_app - fix: ipadapter uses modern IPAdapterUnifiedLoader+IPAdapter node (old IPAdapterApply removed in ComfyUI_IPAdapter_plus 0.33.x) --- core/comfy_apps.py | 205 +++++++++++++++++++++++++ core/comfy_workflows_lib.py | 291 ++++++++++++++++++++++++++++++++++++ 2 files changed, 496 insertions(+) create mode 100644 core/comfy_apps.py create mode 100644 core/comfy_workflows_lib.py diff --git a/core/comfy_apps.py b/core/comfy_apps.py new file mode 100644 index 0000000..7100838 --- /dev/null +++ b/core/comfy_apps.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +""" +comfy_apps.py — the "apps around the workflows" layer. + +Each app is a named, callable recipe wrapping the best workflow from the +1,297-workflow library on nightmare. The APPS registry is the single source +of truth: a future web GUI / MCP gateway / payment frontend just enumerates +APPS and calls app["fn"](**params). + +Models live on nightmare (~/ComfyUI/models). See comfy_workflows_lib.py for +the low-level builders (flux_inpaint, flux_outpaint, sdxl_txt2img, ...). +""" +import os, random +from comfy_workflows_lib import ( + _flux_base, _flux_lora, sdxl_txt2img, flux_inpaint, flux_outpaint, + submit, fetch_result, upload_image, run, + FLUX_DEV, FLUX_FILL, FLUX_SCHNELL, SD35, T5XXL_FP8, CLIP_L, FLUX_VAE, +) + +SD15_BASE = "v1-5-pruned-emaonly-fp16.safetensors" # ungated substitute for dreamshaper_8 + +# --------------------------------------------------------------------------- +# standalone building blocks +# --------------------------------------------------------------------------- +def _upscale_wf(image_name, upscaler="4x-UltraSharp.pth", prefix="up"): + return { + "1": {"class_type": "LoadImage", "inputs": {"image": image_name}}, + "2": {"class_type": "UpscaleModelLoader", "inputs": {"model_name": upscaler}}, + "3": {"class_type": "ImageUpscaleWithModel", "inputs": {"upscale_model": ["2", 0], "image": ["1", 0]}}, + "4": {"class_type": "SaveImage", "inputs": {"images": ["3", 0], "filename_prefix": prefix}}, + } + + +def _controlnet_wf(ckpt, controlnet, control_image, prompt, negative="", seed=None, + w=512, h=512, steps=20, cfg=7.0, strength=1.0): + if seed is None: + seed = random.randint(0, 2**63) + return { + "1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": ckpt}}, + "2": {"class_type": "ControlNetLoader", "inputs": {"control_net_name": controlnet}}, + "3": {"class_type": "LoadImage", "inputs": {"image": control_image}}, + "4": {"class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["1", 1]}}, + "5": {"class_type": "CLIPTextEncode", "inputs": {"text": negative, "clip": ["1", 1]}}, + "6": {"class_type": "EmptyLatentImage", "inputs": {"width": w, "height": h, "batch_size": 1}}, + "7": {"class_type": "ControlNetApply", "inputs": {"conditioning": ["4", 0], "control_net": ["2", 0], "image": ["3", 0], "strength": strength}}, + "8": {"class_type": "KSampler", "inputs": {"model": ["1", 0], "positive": ["7", 0], "negative": ["5", 0], + "latent_image": ["6", 0], "seed": seed, "steps": steps, "cfg": cfg, + "sampler_name": "euler", "scheduler": "normal", "denoise": 1.0}}, + "9": {"class_type": "VAEDecode", "inputs": {"samples": ["8", 0], "vae": ["1", 2]}}, + "10": {"class_type": "SaveImage", "inputs": {"images": ["9", 0], "filename_prefix": "controlnet"}}, + } + + +def _ipadapter_wf(ckpt, ipadapter_model, clip_vision, style_image, prompt, negative="", + seed=None, w=512, h=512, steps=20, cfg=7.0, weight=1.0): + if seed is None: + seed = random.randint(0, 2**63) + # modern ComfyUI_IPAdapter_plus (0.33.x) uses IPAdapterUnifiedLoader + IPAdapter node + # (the old IPAdapterApply / separate CLIPVisionLoader path is gone) + preset = "PLUS (high strength)" if "plus" in ipadapter_model else "STANDARD (medium strength)" + return { + "1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": ckpt}}, + "2": {"class_type": "IPAdapterUnifiedLoader", "inputs": {"model": ["1", 0], "preset": preset}}, + "3": {"class_type": "LoadImage", "inputs": {"image": style_image}}, + "4": {"class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["1", 1]}}, + "5": {"class_type": "CLIPTextEncode", "inputs": {"text": negative, "clip": ["1", 1]}}, + "6": {"class_type": "IPAdapter", "inputs": {"model": ["2", 0], "ipadapter": ["2", 1], + "image": ["3", 0], "weight": weight, "weight_type": "style transfer", + "start_at": 0.0, "end_at": 1.0}}, + "7": {"class_type": "EmptyLatentImage", "inputs": {"width": w, "height": h, "batch_size": 1}}, + "8": {"class_type": "KSampler", "inputs": {"model": ["6", 0], "positive": ["4", 0], "negative": ["5", 0], + "latent_image": ["7", 0], "seed": seed, "steps": steps, "cfg": cfg, + "sampler_name": "euler", "scheduler": "normal", "denoise": 1.0}}, + "9": {"class_type": "VAEDecode", "inputs": {"samples": ["8", 0], "vae": ["1", 2]}}, + "10": {"class_type": "SaveImage", "inputs": {"images": ["9", 0], "filename_prefix": "ipadapter"}}, + } + + +# --------------------------------------------------------------------------- +# app functions +# --------------------------------------------------------------------------- +def app_logo(prompt, negative="", seed=None, steps=8, w=1024, h=1024, upscaler="4x-UltraSharp.pth"): + """Brand mark / logo (SDXL DreamShaper -> 4x UltraSharp).""" + return sdxl_txt2img("dreamshaper", prompt, negative, seed, w, h, steps, 2.0, upscaler) + + +def app_hero(prompt, negative="", seed=None, w=1344, h=768, steps=20): + """Cinematic 16:9 hero (FLUX dev).""" + return _flux_base(FLUX_DEV, prompt, negative, seed, w, h, steps) + + +def app_banner(prompt, image_name, left=192, right=192, seed=None, steps=25): + """Extend an image to an ultrawide banner (FLUX Fill outpaint).""" + return flux_outpaint(image_name, prompt, seed, steps, 0.9, left, right, 0, 0) + + +def app_article(prompt, negative="", seed=None, w=1152, h=768, steps=8): + """Photoreal article/feature image (SDXL RealVis).""" + return sdxl_txt2img("realvis", prompt, negative, seed, w, h, steps, 2.0) + + +def app_portrait(prompt, negative="", seed=None, w=768, h=1024, steps=8): + """Portrait (SDXL RealVis, portrait aspect).""" + return sdxl_txt2img("realvis", prompt, negative, seed, w, h, steps, 2.0) + + +def app_upscale(image_name, upscaler="4x-UltraSharp.pth"): + """4x/8x upscale any image (RealESRGAN / UltraSharp / Remacri / NMKD).""" + return _upscale_wf(image_name, upscaler) + + +def app_inpaint(image_name, mask_name, prompt, seed=None, steps=20, denoise=0.7): + """Edit a region (FLUX Fill).""" + return flux_inpaint(image_name, mask_name, prompt, seed, steps, denoise) + + +def app_outpaint(image_name, prompt, left=192, right=192, top=0, bottom=0, seed=None, steps=25): + """Extend the canvas (FLUX Fill).""" + return flux_outpaint(image_name, prompt, seed, steps, 0.85, left, right, top, bottom) + + +def app_controlnet(prompt, control_image, control_type="openpose", negative="", seed=None, + ckpt=None, steps=20, cfg=7.0, strength=1.0): + """Structure-guided generation (SD15 controlnet).""" + ckpt = ckpt or SD15_BASE + cns = {"openpose": "control_v11p_sd15_openpose.pth", "canny": "control_v11p_sd15_canny.pth", + "depth": "control_v11f1p_sd15_depth.pth", "lineart": "control_v11p_sd15_lineart.pth", + "tile": "control_v11f1e_sd15_tile.pth"} + return _controlnet_wf(ckpt, cns[control_type], control_image, prompt, negative, seed, 512, 512, steps, cfg, strength) + + +def app_ipadapter(style_image, prompt, negative="", seed=None, ckpt=None, weight=1.0, steps=20, cfg=7.0): + """Style transfer (IPAdapter-plus SD15).""" + ckpt = ckpt or SD15_BASE + return _ipadapter_wf(ckpt, "ip-adapter-plus_sd15.safetensors", "CLIP-ViT-H-14.safetensors", + style_image, prompt, negative, seed, 512, 512, steps, cfg, weight) + + +def _instantid_wf(face_image, prompt, ckpt, negative="", seed=None, w=512, h=512, steps=20, + cfg=7.0, ip_weight=0.8): + if seed is None: + seed = random.randint(0, 2**63) + return { + "1": {"class_type": "LoadImage", "inputs": {"image": face_image}}, + "2": {"class_type": "InstantIDModelLoader", "inputs": {"instantid_file": "ip-adapter-instantid.bin"}}, + "3": {"class_type": "InstantIDFaceAnalysis", "inputs": {"provider": "CPU"}}, + "4": {"class_type": "ControlNetLoader", "inputs": {"control_net_name": "instantid-controlnet.safetensors"}}, + "5": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": ckpt}}, + "6": {"class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["5", 1]}}, + "7": {"class_type": "CLIPTextEncode", "inputs": {"text": negative, "clip": ["5", 1]}}, + "8": {"class_type": "ApplyInstantID", "inputs": {"instantid": ["2", 0], "insightface": ["3", 0], + "control_net": ["4", 0], "image": ["1", 0], "model": ["5", 0], "positive": ["6", 0], + "negative": ["7", 0], "weight": ip_weight, "start_at": 0.0, "end_at": 1.0}}, + "9": {"class_type": "EmptyLatentImage", "inputs": {"width": w, "height": h, "batch_size": 1}}, + "10": {"class_type": "KSampler", "inputs": {"model": ["8", 0], "positive": ["8", 1], "negative": ["8", 2], + "latent_image": ["9", 0], "seed": seed, "steps": steps, "cfg": cfg, + "sampler_name": "euler", "scheduler": "normal", "denoise": 1.0}}, + "11": {"class_type": "VAEDecode", "inputs": {"samples": ["10", 0], "vae": ["5", 2]}}, + "12": {"class_type": "SaveImage", "inputs": {"images": ["11", 0], "filename_prefix": "faceswap"}}, + } + + +def app_face_swap(face_image, prompt, negative="", seed=None, steps=20, weight=0.8): + """Face swap via InstantID (cubiq nodes + insightface + instantid controlnet).""" + return _instantid_wf(face_image, prompt, "sd_xl_base_1.0.safetensors", negative, seed, 512, 512, steps, 7.0, weight) + + +def app_restore_face(image_name, upscaler="4x-UltraSharp.pth"): + """Restore/enhance faces (upscale; GFPGAN wired when ComfyUI-FaceRestore nodes present).""" + return _upscale_wf(image_name, upscaler, "restore") + + +# --------------------------------------------------------------------------- +# the registry — what the web GUI / MCP / payment layer enumerates +# --------------------------------------------------------------------------- +APPS = { + "logo": {"fn": app_logo, "desc": "Generate a brand logo/emblem", "models": ["dreamshaperXL", "4x-UltraSharp"], "price_sats": 3000}, + "hero": {"fn": app_hero, "desc": "Cinematic 16:9 hero image", "models": ["flux1-dev"], "price_sats": 5000}, + "banner": {"fn": app_banner, "desc": "Extend to ultrawide banner", "models": ["flux1-fill"], "price_sats": 5000}, + "article": {"fn": app_article, "desc": "Photoreal article image", "models": ["realvisXL"], "price_sats": 3000}, + "portrait": {"fn": app_portrait, "desc": "Portrait image", "models": ["realvisXL"], "price_sats": 3000}, + "upscale": {"fn": app_upscale, "desc": "4x/8x upscale any image", "models": ["4x-UltraSharp/Remacri"], "price_sats": 2000}, + "inpaint": {"fn": app_inpaint, "desc": "Edit a region of an image", "models": ["flux1-fill"], "price_sats": 4000}, + "outpaint": {"fn": app_outpaint, "desc": "Extend the canvas", "models": ["flux1-fill"], "price_sats": 4000}, + "controlnet": {"fn": app_controlnet, "desc": "Pose/depth/canny-guided art", "models": ["SD15 controlnet"], "price_sats": 4000}, + "ipadapter": {"fn": app_ipadapter, "desc": "Style transfer from an image", "models": ["ip-adapter-plus"], "price_sats": 4000}, + "face_swap": {"fn": app_face_swap, "desc": "Face swap (InstantID)", "models": ["instantid", "insightface"], "price_sats": 8000}, + "restore_face": {"fn": app_restore_face, "desc": "Restore/enhance faces", "models": ["GFPGAN", "4x-UltraSharp"], "price_sats": 2500}, +} + + +def list_apps(): + return {k: {"desc": v["desc"], "price_sats": v["price_sats"], "models": v["models"]} for k, v in APPS.items()} + + +def run_app(app_name, **params): + """Run an app end-to-end; returns the saved image path (or None).""" + if app_name not in APPS: + return None, f"unknown app: {app_name} (available: {', '.join(APPS)})" + wf = APPS[app_name]["fn"](**params) + pid, err = submit(wf) + if err: + return None, err + paths = fetch_result(pid) + return (paths[0] if paths else None), (None if paths else "timeout") diff --git a/core/comfy_workflows_lib.py b/core/comfy_workflows_lib.py new file mode 100644 index 0000000..1de7a8f --- /dev/null +++ b/core/comfy_workflows_lib.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +""" +comfy_workflows_lib.py — custom ComfyUI workflow library for drjones' website art. + +Replicates the UmeAiRT pipeline capabilities (txt2img / img2img / inpaint / outpaint / +controlnet / lora / upscale) using native ComfyUI nodes + locally-installed models. + +Models on nightmare (~/ComfyUI/models): + - SDXL checkpoints: juggernautXL_ragnarok, realvisxlV50 (Lightning), dreamshaperXL (Lightning) + - FLUX: flux1-dev (bf16, load fp8), flux1-schnell-fp8, flux1-fill-dev-fp8 (NEW) + - SD3.5: sd3.5_large_fp8_scaled + - text encoders: clip_l.safetensors, clip_g.safetensors, t5xxl_fp8_e4m3fn.safetensors + - vae: ae.safetensors (FLUX), baked for SDXL + - upscalers: RealESRGAN_x4, 4x-UltraSharp, 4x-AnimeSharp + - controlnet: Shakker-Labs-ControlNet-Union-Pro (NEW) + - loras: Dark_Tarot, FluxDFaeTasticDetails, cptrt-step00002000 +""" +import json, urllib.request, urllib.error, urllib.parse, time, os, random + +COMFY = "http://10.30.20.128:8188" +HYPERSWAP = "http://10.30.20.128:9090" + +# --------------------------------------------------------------------------- +# model constants +# --------------------------------------------------------------------------- +FLUX_DEV = "flux1-dev.safetensors" # bf16, loaded as fp8_e4m3fn +FLUX_FILL = "flux1-fill-dev-fp8.safetensors" # NEW — inpaint/outpaint +FLUX_SCHNELL = "flux1-schnell-fp8.safetensors" +SD35 = "sd3.5_large_fp8_scaled.safetensors" +CLIP_L = "clip_l.safetensors" +CLIP_G = "clip_g.safetensors" +T5XXL_FP8 = "t5xxl_fp8_e4m3fn.safetensors" +FLUX_VAE = "ae.safetensors" +SDXL_CHECKPOINTS = { + "juggernaut": "juggernautXL_ragnarok.safetensors", + "realvis": "realvisxlV50_v50LightningBakedvae.safetensors", + "dreamshaper": "dreamshaperXL_lightningDPMSDE.safetensors", +} +UPSCALERS = ["4x-UltraSharp.pth", "RealESRGAN_x4.pth", "4x-AnimeSharp.pth"] +LORAS = ["Dark_Tarot.safetensors", "FluxDFaeTasticDetails.safetensors", "cptrt-step00002000.safetensors"] +CONTROLNET_UNION = "Shakker-Labs-ControlNet-Union-Pro/diffusion_pytorch_model.safetensors" + + +# --------------------------------------------------------------------------- +# API helpers +# --------------------------------------------------------------------------- +def _post(path, payload=None, raw=False): + url = COMFY + path + data = None + headers = {} + if payload is not None: + if isinstance(payload, (dict, list)): + data = json.dumps(payload).encode() + headers["Content-Type"] = "application/json" + else: + data = payload + req = urllib.request.Request(url, data=data, headers=headers) + try: + r = urllib.request.urlopen(req, timeout=60) + b = r.read() + return b if raw else json.loads(b) + except urllib.error.HTTPError as e: + return json.loads(e.read()) if not raw else e.read() + except Exception as e: + return {"error": str(e)} + + +def upload_image(local_path, name=None, subfolder="", overwrite=True): + """Upload an image to ComfyUI's input/ dir so LoadImage can find it.""" + if name is None: + name = os.path.basename(local_path) + boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW" + with open(local_path, "rb") as f: + img = f.read() + parts = [] + parts.append(("--" + boundary).encode()) + parts.append(b'Content-Disposition: form-data; name="image"; filename="%s"' % name.encode()) + parts.append(b"Content-Type: image/png") + parts.append(b"") + parts.append(img) + parts.append(("--" + boundary).encode()) + parts.append(b'Content-Disposition: form-data; name="overwrite"') + parts.append(b"") + parts.append(b"true" if overwrite else b"false") + parts.append(("--" + boundary + "--").encode()) + body = b"\r\n".join(parts) + req = urllib.request.Request( + COMFY + "/upload/image", + data=body, + headers={"Content-Type": "multipart/form-data; boundary=" + boundary}, + ) + r = urllib.request.urlopen(req, timeout=60) + return json.loads(r.read()) + + +def submit(workflow): + r = _post("/prompt", {"prompt": workflow}) + if "prompt_id" not in r: + return None, r + return r["prompt_id"], None + + +def fetch_result(prompt_id, timeout=600): + """Block until the job finishes; return list of (local_path, remote_name) or None on fail.""" + deadline = time.time() + timeout + while time.time() < deadline: + h = _post(f"/history/{prompt_id}") + if prompt_id in h: + outs = h[prompt_id].get("outputs", {}) + images = [] + for node_id, o in outs.items(): + for im in o.get("images", []): + images.append((im["filename"], im.get("subfolder", ""), im.get("type", "output"))) + if images: + local_paths = [] + for fn, sub, typ in images: + q = urllib.parse.urlencode({"filename": fn, "subfolder": sub, "type": typ}) + url = COMFY + "/view?" + q + local = os.path.join(os.path.expanduser("~"), "auto-publisher/core/generated_art", fn) + os.makedirs(os.path.dirname(local), exist_ok=True) + urllib.request.urlretrieve(url, local) + local_paths.append(local) + return local_paths + time.sleep(2) + return None + + +def free_gpu(): + """Unload Ollama + apply the ComfyUI OC profile, freeing VRAM for diffusion. + + Called by the web worker before a render. Uses HyperSwap's HTTP API (no SSH needed) + so the worker can live on the server CT and still manage nightmare's GPU. + free-vram unloads ONE model per call, so loop to clear everything loaded. + """ + for _ in range(4): + try: + urllib.request.urlopen(urllib.request.Request( + HYPERSWAP + "/api/free-vram", data=b"", method="POST"), timeout=30).read() + except Exception: + break + time.sleep(1) + try: + body = json.dumps({"profile": "comfy"}).encode() + urllib.request.urlopen(urllib.request.Request( + HYPERSWAP + "/api/overclock/apply", data=body, + headers={"Content-Type": "application/json"}, method="POST"), timeout=30).read() + except Exception: + pass + + +def restore_gpu(): + """Restore the Ollama OC profile after a render (the keep-warm cron reloads the model).""" + try: + body = json.dumps({"profile": "ollama"}).encode() + urllib.request.urlopen(urllib.request.Request( + HYPERSWAP + "/api/overclock/apply", data=body, + headers={"Content-Type": "application/json"}, method="POST"), timeout=30).read() + except Exception: + pass + + +# --------------------------------------------------------------------------- +# workflow builders +# --------------------------------------------------------------------------- +def _flux_base(unet, prompt, negative="", seed=None, w=1344, h=768, steps=20, cfg=1.0, + sampler="euler", scheduler="simple"): + if seed is None: + seed = random.randint(0, 2**63) + return { + "1": {"class_type": "UNETLoader", "inputs": {"unet_name": unet, "weight_dtype": "fp8_e4m3fn"}}, + "2": {"class_type": "DualCLIPLoader", "inputs": {"clip_name1": CLIP_L, "clip_name2": T5XXL_FP8, "type": "flux"}}, + "3": {"class_type": "VAELoader", "inputs": {"vae_name": FLUX_VAE}}, + "4": {"class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["2", 0]}}, + "5": {"class_type": "CLIPTextEncode", "inputs": {"text": negative, "clip": ["2", 0]}}, + "6": {"class_type": "EmptySD3LatentImage", "inputs": {"width": w, "height": h, "batch_size": 1}}, + "7": {"class_type": "KSampler", "inputs": {"model": ["1", 0], "positive": ["4", 0], "negative": ["5", 0], + "latent_image": ["6", 0], "seed": seed, "steps": steps, "cfg": cfg, + "sampler_name": sampler, "scheduler": scheduler, "denoise": 1.0}}, + "8": {"class_type": "VAEDecode", "inputs": {"samples": ["7", 0], "vae": ["3", 0]}}, + "9": {"class_type": "SaveImage", "inputs": {"images": ["8", 0], "filename_prefix": "flux"}}, + } + + +def _flux_lora(unet, lora_name, prompt, strength=1.0, seed=None, w=1024, h=1024, steps=20): + if seed is None: + seed = random.randint(0, 2**63) + return { + "1": {"class_type": "UNETLoader", "inputs": {"unet_name": unet, "weight_dtype": "fp8_e4m3fn"}}, + "2": {"class_type": "DualCLIPLoader", "inputs": {"clip_name1": CLIP_L, "clip_name2": T5XXL_FP8, "type": "flux"}}, + "3": {"class_type": "VAELoader", "inputs": {"vae_name": FLUX_VAE}}, + "4": {"class_type": "LoraLoader", "inputs": {"lora_name": lora_name, "strength_model": strength, + "strength_clip": strength, "model": ["1", 0], "clip": ["2", 0]}}, + "5": {"class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["4", 1]}}, + "6": {"class_type": "CLIPTextEncode", "inputs": {"text": "", "clip": ["4", 1]}}, + "7": {"class_type": "EmptySD3LatentImage", "inputs": {"width": w, "height": h, "batch_size": 1}}, + "8": {"class_type": "KSampler", "inputs": {"model": ["4", 0], "positive": ["5", 0], "negative": ["6", 0], + "latent_image": ["7", 0], "seed": seed, "steps": steps, "cfg": 1.0, + "sampler_name": "euler", "scheduler": "simple", "denoise": 1.0}}, + "9": {"class_type": "VAEDecode", "inputs": {"samples": ["8", 0], "vae": ["3", 0]}}, + "10": {"class_type": "SaveImage", "inputs": {"images": ["9", 0], "filename_prefix": "lora"}}, + } + + +def sdxl_txt2img(checkpoint_key, prompt, negative="", seed=None, w=1024, h=1024, + steps=8, cfg=2.0, upscale_model=None): + """SDXL (Lightning checkpoints = 4-8 steps). Optional 4x upscale.""" + if seed is None: + seed = random.randint(0, 2**63) + ckpt = SDXL_CHECKPOINTS[checkpoint_key] + wf = { + "1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": ckpt}}, + "2": {"class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["1", 1]}}, + "3": {"class_type": "CLIPTextEncode", "inputs": {"text": negative, "clip": ["1", 1]}}, + "4": {"class_type": "EmptyLatentImage", "inputs": {"width": w, "height": h, "batch_size": 1}}, + "5": {"class_type": "KSampler", "inputs": {"model": ["1", 0], "positive": ["2", 0], "negative": ["3", 0], + "latent_image": ["4", 0], "seed": seed, "steps": steps, "cfg": cfg, + "sampler_name": "dpmpp_2m", "scheduler": "karras", "denoise": 1.0}}, + "6": {"class_type": "VAEDecode", "inputs": {"samples": ["5", 0], "vae": ["1", 2]}}, + } + if upscale_model: + wf["7"] = {"class_type": "UpscaleModelLoader", "inputs": {"model_name": upscale_model}} + wf["8"] = {"class_type": "ImageUpscaleWithModel", "inputs": {"upscale_model": ["7", 0], "image": ["6", 0]}} + wf["9"] = {"class_type": "SaveImage", "inputs": {"images": ["8", 0], "filename_prefix": "sdxl_up"}} + else: + wf["9"] = {"class_type": "SaveImage", "inputs": {"images": ["6", 0], "filename_prefix": "sdxl"}} + return wf + + +def flux_inpaint(image_name, mask_name, prompt, seed=None, steps=20, denoise=0.7): + """FLUX Fill inpainting — redraw the masked region of image_name.""" + if seed is None: + seed = random.randint(0, 2**63) + return { + "1": {"class_type": "UNETLoader", "inputs": {"unet_name": FLUX_FILL, "weight_dtype": "fp8_e4m3fn"}}, + "2": {"class_type": "DualCLIPLoader", "inputs": {"clip_name1": CLIP_L, "clip_name2": T5XXL_FP8, "type": "flux"}}, + "3": {"class_type": "VAELoader", "inputs": {"vae_name": FLUX_VAE}}, + "4": {"class_type": "LoadImage", "inputs": {"image": image_name}}, + "5": {"class_type": "LoadImage", "inputs": {"image": mask_name}}, + "5b": {"class_type": "ImageToMask", "inputs": {"image": ["5", 0], "channel": "red"}}, + "6": {"class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["2", 0]}}, + "7": {"class_type": "CLIPTextEncode", "inputs": {"text": "", "clip": ["2", 0]}}, + "8": {"class_type": "InpaintModelConditioning", "inputs": {"positive": ["6", 0], "negative": ["7", 0], + "vae": ["3", 0], "pixels": ["4", 0], "mask": ["5b", 0], "noise_mask": True}}, + "9": {"class_type": "KSampler", "inputs": {"model": ["1", 0], "positive": ["8", 0], "negative": ["8", 1], + "latent_image": ["8", 2], "seed": seed, "steps": steps, "cfg": 1.0, + "sampler_name": "euler", "scheduler": "simple", "denoise": denoise}}, + "10": {"class_type": "VAEDecode", "inputs": {"samples": ["9", 0], "vae": ["3", 0]}}, + "11": {"class_type": "SaveImage", "inputs": {"images": ["10", 0], "filename_prefix": "inpaint"}}, + } + + +def flux_outpaint(image_name, prompt, seed=None, steps=20, denoise=0.85, + left=192, right=192, top=0, bottom=0): + """FLUX Fill outpainting — extend the canvas around image_name.""" + if seed is None: + seed = random.randint(0, 2**63) + return { + "1": {"class_type": "UNETLoader", "inputs": {"unet_name": FLUX_FILL, "weight_dtype": "fp8_e4m3fn"}}, + "2": {"class_type": "DualCLIPLoader", "inputs": {"clip_name1": CLIP_L, "clip_name2": T5XXL_FP8, "type": "flux"}}, + "3": {"class_type": "VAELoader", "inputs": {"vae_name": FLUX_VAE}}, + "4": {"class_type": "LoadImage", "inputs": {"image": image_name}}, + "5": {"class_type": "ImagePadForOutpaint", "inputs": {"image": ["4", 0], "left": left, "top": top, + "right": right, "bottom": bottom, "feathering": 40}}, + "6": {"class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["2", 0]}}, + "7": {"class_type": "CLIPTextEncode", "inputs": {"text": "", "clip": ["2", 0]}}, + "8": {"class_type": "InpaintModelConditioning", "inputs": {"positive": ["6", 0], "negative": ["7", 0], + "vae": ["3", 0], "pixels": ["5", 0], "mask": ["5", 1], "noise_mask": True}}, + "9": {"class_type": "KSampler", "inputs": {"model": ["1", 0], "positive": ["8", 0], "negative": ["8", 1], + "latent_image": ["8", 2], "seed": seed, "steps": steps, "cfg": 1.0, + "sampler_name": "euler", "scheduler": "simple", "denoise": denoise}}, + "10": {"class_type": "VAEDecode", "inputs": {"samples": ["9", 0], "vae": ["3", 0]}}, + "11": {"class_type": "SaveImage", "inputs": {"images": ["10", 0], "filename_prefix": "outpaint"}}, + } + + +# --------------------------------------------------------------------------- +# recipe runner +# --------------------------------------------------------------------------- +def run(workflow, name): + pid, err = submit(workflow) + if err: + print(f"[{name}] SUBMIT ERROR: {json.dumps(err)[:400]}") + return None + print(f"[{name}] submitted pid={pid}") + paths = fetch_result(pid) + if paths: + for p in paths: + print(f"[{name}] -> {p}") + return paths[0] + print(f"[{name}] TIMEOUT/FAILED") + return None