diff --git a/CHANGELOG.md b/CHANGELOG.md index df917041b..d2d70f3e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Highlights for 2024-12-19 -### SD.Next X-mass edition: *What's new?* +### SD.Next Xmass edition: *What's new?* While we have several new supported models, workflows and tools, this release is primarily about *quality-of-life improvements*: - New memory management engine: list of changes that went into this one is long: changes to GPU offloading, LoRA loader, system memory management, etc. @@ -15,18 +15,18 @@ While we have several new supported models, workflows and tools, this release is - New settings panel with simplified and streamlined configuration We've also added support for several new models (see [supported models](https://vladmandic.github.io/sdnext-docs/Model-Support/) for full list) such as highly anticipated [NVLabs Sana](https://huggingface.co/Efficient-Large-Model/Sana_1600M_1024px) -And several new video models: [Lightricks LTX-Video](https://huggingface.co/Lightricks/LTX-Video), [Hunyuan Video](https://huggingface.co/tencent/HunyuanVideo) and [Genmo Mochi.1 Preview](https://huggingface.co/genmo/mochi-1-preview) +And several new SOTA video models: [Lightricks LTX-Video](https://huggingface.co/Lightricks/LTX-Video), [Hunyuan Video](https://huggingface.co/tencent/HunyuanVideo) and [Genmo Mochi.1 Preview](https://huggingface.co/genmo/mochi-1-preview) And a lot of Control and IPAdapter goodies -- for SDXL there is new [ProMax](https://huggingface.co/xinsir/controlnet-union-sdxl-1.0), improved *Union* and *Tiling* -- for FLUX.1 there are [Flux Tools](https://blackforestlabs.ai/flux-1-tools/) as well as official *Canny* and *Depth* models and a cool [Redux](https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev) model -- for SD 3.5 there are official *Canny*, *Blur* and *Depth* in addition to existing 3rd party models +- for **SDXL** there is new [ProMax](https://huggingface.co/xinsir/controlnet-union-sdxl-1.0), improved *Union* and *Tiling* models +- for **FLUX.1** there are [Flux Tools](https://blackforestlabs.ai/flux-1-tools/) as well as official *Canny* and *Depth* models and a cool [Redux](https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev) model +- for **SD3.5** there are official *Canny*, *Blur* and *Depth* models in addition to existing 3rd party models as well as [InstantX](https://huggingface.co/InstantX/SD3.5-Large-IP-Adapter) IP-adapter Plus couple of new integrated workflows such as [FreeScale](https://github.com/ali-vilab/FreeScale) and [Style Aligned Image Generation](https://style-aligned-gen.github.io/) -And it wouldn't be a X-mass edition custom themes: *Snowflake* and *Elf-Green* +And it wouldn't be a Xmass edition without couple of custom themes: *Snowflake* and *Elf-Green*! -[README](https://github.com/vladmandic/automatic/blob/master/README.md) | [CHANGELOG](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) +[ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) ## Details for 2024-12-19 @@ -136,6 +136,7 @@ And it wouldn't be a X-mass edition custom themes: *Snowflake* and *Elf-Green* - balanced offload: add both high and low watermark, defaults as below `0.25` for low-watermark: skip offload if memory usage is below 25% `0.70` high-watermark: must offload if memory usage is above 70% + - balanced offload will attempt to run offload as non-blocking and force gc at the end - change-in-behavior: low-end systems, triggered by either `lowvrwam` or by detection of <=4GB will use *sequential offload* all other systems use *balanced offload* by default (can be changed in settings) @@ -198,6 +199,7 @@ And it wouldn't be a X-mass edition custom themes: *Snowflake* and *Elf-Green* - taesd limit render to 2024px - taesd downscale preview to 1024px max: configurable in settings -> live preview - uninstall conflicting `wandb` package +- dont skip diffusers version check if quick is specified ## Update for 2024-11-21 diff --git a/installer.py b/installer.py index 76bd5e9e8..f1360541f 100644 --- a/installer.py +++ b/installer.py @@ -457,9 +457,9 @@ def check_python(supported_minors=[9, 10, 11, 12], reason=None): # check diffusers version def check_diffusers(): - if args.skip_all or args.skip_requirements: + if args.skip_all or args.skip_git: return - sha = '862a7d5038c1c53641ffcab146a7eeb5ab683656' # diffusers commit hash + sha = 'b64ca6c11cbc1644c22f1dae441c8124d588bb14' # diffusers commit hash pkg = pkg_resources.working_set.by_key.get('diffusers', None) minor = int(pkg.version.split('.')[1] if pkg is not None else 0) cur = opts.get('diffusers_version', '') if minor > 0 else '' diff --git a/modules/devices.py b/modules/devices.py index 3f1439fb7..949fab4aa 100644 --- a/modules/devices.py +++ b/modules/devices.py @@ -186,7 +186,7 @@ def get_device_for(task): # pylint: disable=unused-argument return get_optimal_device() -def torch_gc(force=False, fast=False): +def torch_gc(force:bool=False, fast:bool=False, reason:str=None): def get_stats(): mem_dict = memstats.memory_stats() gpu_dict = mem_dict.get('gpu', {}) @@ -207,15 +207,21 @@ def torch_gc(force=False, fast=False): from modules.shared import cmd_opts t0 = time.time() - gpu, used_gpu, ram, used_ram, oom = get_stats() + gpu, used_gpu, ram, _used_ram, oom = get_stats() threshold = 0 if (cmd_opts.lowvram and not cmd_opts.use_zluda) else opts.torch_gc_threshold collected = 0 - if force or threshold == 0 or used_gpu >= threshold or used_ram >= threshold: + if reason is None and force: + reason='force' + if threshold == 0 or used_gpu >= threshold: force = True + if reason is None: + reason = 'threshold' if oom > previous_oom: previous_oom = oom log.warning(f'Torch GPU out-of-memory error: {memstats.memory_stats()}') force = True + if reason is None: + reason = 'oom' if force: # actual gc collected = gc.collect() if not fast else 0 # python gc @@ -237,10 +243,10 @@ def torch_gc(force=False, fast=False): new_gpu, new_used_gpu, new_ram, new_used_ram, oom = get_stats() before = { 'gpu': gpu, 'ram': ram } after = { 'gpu': new_gpu, 'ram': new_ram, 'oom': oom } - utilization = { 'gpu': new_used_gpu, 'ram': new_used_ram, 'threshold': threshold } - results = { 'saved': round(gpu - new_gpu, 2), 'collected': collected } + utilization = { 'gpu': new_used_gpu, 'ram': new_used_ram } + results = { 'gpu': round(gpu - new_gpu, 2), 'py': collected } fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access - log.debug(f'GC: utilization={utilization} gc={results} before={before} after={after} device={torch.device(get_optimal_device_name())} fn={fn} time={round(t1 - t0, 2)}') + log.debug(f'GC: current={after} prev={before} load={utilization} gc={results} fn={fn} why={reason} time={t1-t0:.2f}') return new_gpu, new_ram diff --git a/modules/ipadapter.py b/modules/ipadapter.py index 4e93a6eee..c1b6ed52f 100644 --- a/modules/ipadapter.py +++ b/modules/ipadapter.py @@ -9,12 +9,15 @@ import os import time import json from PIL import Image +import diffusers +import transformers from modules import processing, shared, devices, sd_models -clip_repo = "h94/IP-Adapter" clip_loaded = None adapters_loaded = [] +CLIP_ID = "h94/IP-Adapter" +SIGLIP_ID = 'google/siglip-so400m-patch14-384' ADAPTERS_NONE = { 'None': { 'name': 'none', 'repo': 'none', 'subfolder': 'none' }, } @@ -37,11 +40,13 @@ ADAPTERS_SDXL = { 'Ostris Composition ViT-H SDXL': { 'name': 'ip_plus_composition_sdxl.safetensors', 'repo': 'ostris/ip-composition-adapter', 'subfolder': '' }, } ADAPTERS_SD3 = { - 'InstantX Large': { 'name': 'ip-adapter.bin', 'repo': 'InstantX/SD3.5-Large-IP-Adapter' }, + 'None': { 'name': 'none', 'repo': 'none', 'subfolder': 'none' }, + 'InstantX Large': { 'name': 'none', 'repo': 'InstantX/SD3.5-Large-IP-Adapter', 'subfolder': 'none', 'revision': 'refs/pr/10' }, } ADAPTERS_F1 = { - 'XLabs AI v1': { 'name': 'ip_adapter.safetensors', 'repo': 'XLabs-AI/flux-ip-adapter' }, - 'XLabs AI v2': { 'name': 'ip_adapter.safetensors', 'repo': 'XLabs-AI/flux-ip-adapter-v2' }, + 'None': { 'name': 'none', 'repo': 'none', 'subfolder': 'none' }, + 'XLabs AI v1': { 'name': 'ip_adapter.safetensors', 'repo': 'XLabs-AI/flux-ip-adapter', 'subfolder': 'none' }, + 'XLabs AI v2': { 'name': 'ip_adapter.safetensors', 'repo': 'XLabs-AI/flux-ip-adapter-v2', 'subfolder': 'none' }, } ADAPTERS = { **ADAPTERS_SD15, **ADAPTERS_SDXL, **ADAPTERS_SD3, **ADAPTERS_F1 } ADAPTERS_ALL = { **ADAPTERS_SD15, **ADAPTERS_SDXL, **ADAPTERS_SD3, **ADAPTERS_F1 } @@ -126,6 +131,8 @@ def crop_images(images, crops): shared.log.error(f'IP adapter: failed to crop image: source={len(images[i])} faces={len(cropped)}') except Exception as e: shared.log.error(f'IP adapter: failed to crop image: {e}') + if shared.sd_model_type == 'sd3' and len(images) == 1: + return images[0] return images @@ -144,27 +151,64 @@ def unapply(pipe): # pylint: disable=arguments-differ pass -def apply(pipe, p: processing.StableDiffusionProcessing, adapter_names=[], adapter_scales=[1.0], adapter_crops=[False], adapter_starts=[0.0], adapter_ends=[1.0], adapter_images=[]): - global clip_loaded, adapters_loaded # pylint: disable=global-statement - # overrides - if hasattr(p, 'ip_adapter_names'): - if isinstance(p.ip_adapter_names, str): - p.ip_adapter_names = [p.ip_adapter_names] - adapters = [ADAPTERS_ALL.get(adapter_name, None) for adapter_name in p.ip_adapter_names if adapter_name is not None and adapter_name.lower() != 'none'] - adapter_names = p.ip_adapter_names - else: - if isinstance(adapter_names, str): - adapter_names = [adapter_names] - adapters = [ADAPTERS.get(adapter, None) for adapter in adapter_names] - adapters = [adapter for adapter in adapters if adapter is not None and adapter['name'].lower() != 'none'] - if len(adapters) == 0: - unapply(pipe) - if hasattr(p, 'ip_adapter_images'): - del p.ip_adapter_images - return False - if shared.sd_model_type not in ['sd', 'sdxl', 'sd3', 'f1']: - shared.log.error(f'IP adapter: model={shared.sd_model_type} class={pipe.__class__.__name__} not supported') - return False +def load_image_encoder(pipe: diffusers.DiffusionPipeline, adapter_names: list[str]): + global clip_loaded # pylint: disable=global-statement + for adapter_name in adapter_names: + # which clip to use + clip_repo = CLIP_ID + if 'ViT' not in adapter_name: # defaults per model + clip_subfolder = 'models/image_encoder' if shared.sd_model_type == 'sd' else 'sdxl_models/image_encoder' + if 'ViT-H' in adapter_name: + clip_subfolder = 'models/image_encoder' # this is vit-h + elif 'ViT-G' in adapter_name: + clip_subfolder = 'sdxl_models/image_encoder' # this is vit-g + else: + if shared.sd_model_type == 'sd': + clip_subfolder = 'models/image_encoder' + elif shared.sd_model_type == 'sdxl': + clip_subfolder = 'sdxl_models/image_encoder' + elif shared.sd_model_type == 'sd3': + clip_repo = SIGLIP_ID + clip_subfolder = None + elif shared.sd_model_type == 'f1': + shared.log.error(f'IP adapter: adapter={adapter_name} type={shared.sd_model_type} cls={shared.sd_model.__class__.__name__}: unsupported base model') + return False + else: + shared.log.error(f'IP adapter: unknown model type: {adapter_name}') + return False + + # load image encoder used by ip adapter + if pipe.image_encoder is None or clip_loaded != f'{clip_repo}/{clip_subfolder}': + try: + if shared.sd_model_type == 'sd3': + pipe.image_encoder = transformers.SiglipVisionModel.from_pretrained(clip_repo, torch_dtype=devices.dtype, cache_dir=shared.opts.hfcache_dir) + else: + pipe.image_encoder = transformers.CLIPVisionModelWithProjection.from_pretrained(clip_repo, subfolder=clip_subfolder, torch_dtype=devices.dtype, cache_dir=shared.opts.hfcache_dir, use_safetensors=True) + shared.log.debug(f'IP adapter load: encoder="{clip_repo}/{clip_subfolder}" cls={pipe.image_encoder.__class__.__name__}') + clip_loaded = f'{clip_repo}/{clip_subfolder}' + except Exception as e: + shared.log.error(f'IP adapter load: encoder="{clip_repo}/{clip_subfolder}" {e}') + return False + sd_models.move_model(pipe.image_encoder, devices.device) + return True + + +def load_feature_extractor(pipe): + # load feature extractor used by ip adapter + if pipe.feature_extractor is None: + try: + if shared.sd_model_type == 'sd3': + pipe.feature_extractor = transformers.SiglipImageProcessor.from_pretrained(SIGLIP_ID, torch_dtype=devices.dtype, cache_dir=shared.opts.hfcache_dir) + else: + pipe.feature_extractor = transformers.CLIPImageProcessor() + shared.log.debug(f'IP adapter load: extractor={pipe.feature_extractor.__class__.__name__}') + except Exception as e: + shared.log.error(f'IP adapter load: extractor {e}') + return False + return True + + +def parse_params(p: processing.StableDiffusionProcessing, adapters: list, adapter_scales: list[float], adapter_crops: list[bool], adapter_starts: list[float], adapter_ends: list[float], adapter_images: list): if hasattr(p, 'ip_adapter_scales'): adapter_scales = p.ip_adapter_scales if hasattr(p, 'ip_adapter_crops'): @@ -205,6 +249,33 @@ def apply(pipe, p: processing.StableDiffusionProcessing, adapter_names=[], adapt p.ip_adapter_starts = adapter_starts.copy() adapter_ends = get_scales(adapter_ends, adapter_images) p.ip_adapter_ends = adapter_ends.copy() + return adapter_images, adapter_masks, adapter_scales, adapter_crops, adapter_starts, adapter_ends + + +def apply(pipe, p: processing.StableDiffusionProcessing, adapter_names=[], adapter_scales=[1.0], adapter_crops=[False], adapter_starts=[0.0], adapter_ends=[1.0], adapter_images=[]): + global adapters_loaded # pylint: disable=global-statement + # overrides + if hasattr(p, 'ip_adapter_names'): + if isinstance(p.ip_adapter_names, str): + p.ip_adapter_names = [p.ip_adapter_names] + adapters = [ADAPTERS_ALL.get(adapter_name, None) for adapter_name in p.ip_adapter_names if adapter_name is not None and adapter_name.lower() != 'none'] + adapter_names = p.ip_adapter_names + else: + if isinstance(adapter_names, str): + adapter_names = [adapter_names] + adapters = [ADAPTERS.get(adapter_name, None) for adapter_name in adapter_names if adapter_name.lower() != 'none'] + + if len(adapters) == 0: + unapply(pipe) + if hasattr(p, 'ip_adapter_images'): + del p.ip_adapter_images + return False + if shared.sd_model_type not in ['sd', 'sdxl', 'sd3', 'f1']: + shared.log.error(f'IP adapter: model={shared.sd_model_type} class={pipe.__class__.__name__} not supported') + return False + + adapter_images, adapter_masks, adapter_scales, adapter_crops, adapter_starts, adapter_ends = parse_params(p, adapters, adapter_scales, adapter_crops, adapter_starts, adapter_ends, adapter_images) + # init code if pipe is None: return False @@ -223,61 +294,29 @@ def apply(pipe, p: processing.StableDiffusionProcessing, adapter_names=[], adapt shared.log.error(f'IP adapter: pipeline not supported: {pipe.__class__.__name__}') return False - for adapter_name in adapter_names: - # which clip to use - if 'ViT' not in adapter_name: # defaults per model - if shared.sd_model_type == 'sd': - clip_subfolder = 'models/image_encoder' - else: - clip_subfolder = 'sdxl_models/image_encoder' - if 'ViT-H' in adapter_name: - clip_subfolder = 'models/image_encoder' # this is vit-h - elif 'ViT-G' in adapter_name: - clip_subfolder = 'sdxl_models/image_encoder' # this is vit-g - else: - if shared.sd_model_type == 'sd': - clip_subfolder = 'models/image_encoder' - elif shared.sd_model_type == 'sdxl': - clip_subfolder = 'sdxl_models/image_encoder' - elif shared.sd_model_type == 'sd3': - shared.log.error(f'IP adapter: adapter={adapter_name} type={shared.sd_model_type} cls={shared.sd_model.__class__.__name__}: unsupported base model') - return False - elif shared.sd_model_type == 'f1': - shared.log.error(f'IP adapter: adapter={adapter_name} type={shared.sd_model_type} cls={shared.sd_model.__class__.__name__}: unsupported base model') - return False - else: - shared.log.error(f'IP adapter: unknown model type: {adapter_name}') - return False + if not load_image_encoder(pipe, adapter_names): + return False - # load feature extractor used by ip adapter - if pipe.feature_extractor is None: - try: - from transformers import CLIPImageProcessor - shared.log.debug('IP adapter load: feature extractor') - pipe.feature_extractor = CLIPImageProcessor() - except Exception as e: - shared.log.error(f'IP adapter load: feature extractor {e}') - return False - - # load image encoder used by ip adapter - if pipe.image_encoder is None or clip_loaded != f'{clip_repo}/{clip_subfolder}': - try: - from transformers import CLIPVisionModelWithProjection - shared.log.debug(f'IP adapter load: image encoder="{clip_repo}/{clip_subfolder}"') - pipe.image_encoder = CLIPVisionModelWithProjection.from_pretrained(clip_repo, subfolder=clip_subfolder, torch_dtype=devices.dtype, cache_dir=shared.opts.diffusers_dir, use_safetensors=True) - clip_loaded = f'{clip_repo}/{clip_subfolder}' - except Exception as e: - shared.log.error(f'IP adapter load: image encoder="{clip_repo}/{clip_subfolder}" {e}') - return False - sd_models.move_model(pipe.image_encoder, devices.device) + if not load_feature_extractor(pipe): + return False # main code try: t0 = time.time() - repos = [adapter['repo'] for adapter in adapters] - subfolders = [adapter['subfolder'] for adapter in adapters] - names = [adapter['name'] for adapter in adapters] - pipe.load_ip_adapter(repos, subfolder=subfolders, weight_name=names) + repos = [adapter.get('repo', None) for adapter in adapters if adapter.get('repo', 'none') != 'none'] + subfolders = [adapter.get('subfolder', None) for adapter in adapters if adapter.get('subfolder', 'none') != 'none'] + names = [adapter.get('name', None) for adapter in adapters if adapter.get('name', 'none') != 'none'] + revisions = [adapter.get('revision', None) for adapter in adapters if adapter.get('revision', 'none') != 'none'] + kwargs = {} + if len(repos) == 1: + repos = repos[0] + if len(subfolders) > 0: + kwargs['subfolder'] = subfolders if len(subfolders) > 1 else subfolders[0] + if len(names) > 0: + kwargs['weight_name'] = names if len(names) > 1 else names[0] + if len(revisions) > 0: + kwargs['revision'] = revisions[0] + pipe.load_ip_adapter(repos, **kwargs) adapters_loaded = names if hasattr(p, 'ip_adapter_layers'): pipe.set_ip_adapter_scale(p.ip_adapter_layers) @@ -286,8 +325,8 @@ def apply(pipe, p: processing.StableDiffusionProcessing, adapter_names=[], adapt for i in range(len(adapter_scales)): if adapter_starts[i] > 0: adapter_scales[i] = 0.00 - pipe.set_ip_adapter_scale(adapter_scales) - ip_str = [f'{os.path.splitext(adapter)[0]}:{scale}:{start}:{end}' for adapter, scale, start, end in zip(adapter_names, adapter_scales, adapter_starts, adapter_ends)] + pipe.set_ip_adapter_scale(adapter_scales if len(adapter_scales) > 1 else adapter_scales[0]) + ip_str = [f'{os.path.splitext(adapter)[0]}:{scale}:{start}:{end}:{crop}' for adapter, scale, start, end, crop in zip(adapter_names, adapter_scales, adapter_starts, adapter_ends, adapter_crops)] p.task_args['ip_adapter_image'] = crop_images(adapter_images, adapter_crops) if len(adapter_masks) > 0: p.cross_attention_kwargs = { 'ip_adapter_masks': adapter_masks } diff --git a/modules/processing.py b/modules/processing.py index 4bc5e3c81..f39393f95 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -480,5 +480,5 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if not p.disable_extra_networks: shared.log.info(f'Processed: images={len(output_images)} its={(p.steps * len(output_images)) / (t1 - t0):.2f} time={t1-t0:.2f} timers={timer.process.dct()} memory={memstats.memory_stats()}') - devices.torch_gc(force=True) + devices.torch_gc(force=True, reason='final') return processed diff --git a/modules/sd_models.py b/modules/sd_models.py index 52d0a1deb..a3698c5c7 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -496,7 +496,11 @@ def apply_balanced_offload(sd_model, exclude=[]): used_gpu -= module_size debug_move(f'Offload: type=balanced op={"move" if do_offload else "skip"} gpu={prev_gpu:.3f}:{used_gpu:.3f} perc={perc_gpu:.2f} ram={used_ram:.3f} current={module.device} dtype={module.dtype} component={module.__class__.__name__} size={module_size:.3f}') except Exception as e: - if 'bitsandbytes' not in str(e): + if 'out of memory' in str(e): + devices.torch_gc(fast=True, force=True, reason='oom') + elif 'bitsandbytes' in str(e): + pass + else: shared.log.error(f'Offload: type=balanced op=apply module={module_name} {e}') if os.environ.get('SD_MOVE_DEBUG', None): errors.display(e, f'Offload: type=balanced op=apply module={module_name}') @@ -508,7 +512,7 @@ def apply_balanced_offload(sd_model, exclude=[]): if device_map and max_memory: module.balanced_offload_device_map = device_map module.balanced_offload_max_memory = max_memory - devices.torch_gc(fast=True, force=True) + devices.torch_gc(fast=True, force=True, reason='offload') apply_balanced_offload_to_module(sd_model) if hasattr(sd_model, "pipe"): @@ -518,7 +522,6 @@ def apply_balanced_offload(sd_model, exclude=[]): if hasattr(sd_model, "decoder_pipe"): apply_balanced_offload_to_module(sd_model.decoder_pipe) set_accelerate(sd_model) - devices.torch_gc(fast=True) t = time.time() - t0 process_timer.add('offload', t) fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access diff --git a/scripts/flux_tools.py b/scripts/flux_tools.py index 909257a37..50904eedb 100644 --- a/scripts/flux_tools.py +++ b/scripts/flux_tools.py @@ -7,7 +7,8 @@ from modules import scripts, processing, shared, devices, sd_models from installer import install -redux_pipe: diffusers.FluxPriorReduxPipeline = None +# redux_pipe: diffusers.FluxPriorReduxPipeline = None +redux_pipe = None processor_canny = None processor_depth = None title = 'Flux Tools'