From 216558185b1e81285d25e91881a717b9ddd5262f Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 19 Feb 2026 12:21:46 +0100 Subject: [PATCH] update installer usage --- CHANGELOG.md | 9 ++--- installer.py | 45 ++++++++++++++--------- launch.py | 4 +-- modules/control/proc/dwpose/__init__.py | 2 +- modules/control/proc/mediapipe_face.py | 2 +- modules/control/run.py | 4 +-- modules/face/insightface.py | 4 +-- modules/images.py | 7 ++-- modules/intel/openvino/__init__.py | 4 +-- modules/lora/network.py | 4 +-- modules/memmon.py | 2 +- modules/onnx_impl/execution_providers.py | 1 - modules/postprocess/esrgan_model.py | 3 +- modules/processing_vae.py | 6 ++-- modules/sd_models_compile.py | 6 ++-- modules/shared_state.py | 5 +-- modules/theme.py | 39 ++++++++++---------- modules/ui_extra_networks.py | 7 ++-- modules/ui_javascript.py | 8 ++--- scripts/animatediff.py | 2 +- scripts/image2video.py | 4 +-- scripts/infiniteyou_ext.py | 2 +- scripts/mixture_tiling.py | 2 +- scripts/poor_mans_outpainting.py | 7 ++-- scripts/postprocessing_video.py | 5 ++- scripts/pulid_ext.py | 2 +- scripts/sd_upscale.py | 3 +- scripts/stablevideodiffusion.py | 4 +-- scripts/text2video.py | 4 +-- scripts/xyz/xyz_grid_classes.py | 4 +-- scripts/xyz/xyz_grid_draw.py | 7 ++-- scripts/xyz_grid.py | 4 +-- scripts/xyz_grid_on.py | 4 +-- webui.py | 46 ++++++++++++++---------- 34 files changed, 145 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2c6c83e9..1b20e9b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,6 @@ TBD - refactor: reorganize `cli` scripts - refactor: move tests to dedicated `/test/` - refactor: all image handling to `modules/image/` - - refactor: captioning part-2, thanks @CalamitousFelicitousness - refactor: remove face restoration, thanks @CalamitousFelicitousness - refactor: unified command line parsing - refactor: launch use threads to async execute non-critical tasks @@ -61,11 +60,13 @@ TBD - refactor: improve `pydantic==2.x` compatibility - refactor: entire logging into separate `modules/logger` - refactor: replace `timestamp` based startup checks with state caching - - refactor: split monolithic `shared` module and introduce `ui_definitions` - - update `lint` rules, thanks @awsr + - refactor: split monolithic `shared` module and introduce `ui_definitions` + - use `threading` for deferable operatios + - use `threading` for io-independent parallel operations - remove requirements: `clip`, `open-clip` - remove `normalbae` pre-processor - - update `requirements` + - refactor: captioning part-2, thanks @CalamitousFelicitousness + - update `lint` rules, thanks @awsr - **Fixes** - handle `clip` installer doing unwanted `setuptools` update - cleanup for `uv` installer fallback diff --git a/installer.py b/installer.py index 9e237f8c1..91c28b26a 100644 --- a/installer.py +++ b/installer.py @@ -1,4 +1,3 @@ -from typing import overload from functools import lru_cache import os import sys @@ -14,7 +13,7 @@ import cProfile import importlib import importlib.util import importlib.metadata -from modules.logger import setup_logging, get_console, get_log, install_traceback, log, console +from modules.logger import setup_logging, log class Dot(dict): # dot notation access to dictionary attributes @@ -141,7 +140,7 @@ def package_spec(package): # check if package is installed -def installed(package, friendly: str = None, reload = False, quiet = False): # pylint: disable=redefined-outer-name +def installed(package, friendly: str = None, quiet = False): # pylint: disable=redefined-outer-name t_start = time.time() ok = True try: @@ -275,7 +274,6 @@ def install(package, friendly: str = None, ignore: bool = False, reinstall: bool isolation = '' if not no_build_isolation else '--no-build-isolation ' cmd = f"install{' --upgrade' if not args.uv else ''}{' --force-reinstall' if force else ''} {deps}{isolation}{package}" res = pip(cmd, ignore=ignore, uv=package != "uv" and not package.startswith('git+')) - pass ts('install', t_start) return res @@ -1095,10 +1093,11 @@ def install_gradio(): # aiofiles-23.2.1 altair-5.5.0 annotated-types-0.7.0 anyio-4.9.0 attrs-25.3.0 certifi-2025.6.15 charset_normalizer-3.4.2 click-8.2.1 contourpy-1.3.2 cycler-0.12.1 fastapi-0.115.14 ffmpy-0.6.0 filelock-3.18.0 fonttools-4.58.4 fsspec-2025.5.1 gradio-3.43.2 gradio-client-0.5.0 h11-0.16.0 hf-xet-1.1.5 httpcore-1.0.9 httpx-0.28.1 huggingface-hub-0.33.1 idna-3.10 importlib-resources-6.5.2 jinja2-3.1.6 jsonschema-4.24.0 jsonschema-specifications-2025.4.1 kiwisolver-1.4.8 markupsafe-2.1.5 matplotlib-3.10.3 narwhals-1.45.0 numpy-1.26.4 orjson-3.10.18 packaging-25.0 pandas-2.3.0 pillow-10.4.0 pydantic-2.11.7 pydantic-core-2.33.2 pydub-0.25.1 pyparsing-3.2.3 python-dateutil-2.9.0.post0 python-multipart-0.0.20 pytz-2025.2 pyyaml-6.0.2 referencing-0.36.2 requests-2.32.4 rpds-py-0.25.1 semantic-version-2.10.0 six-1.17.0 sniffio-1.3.1 starlette-0.46.2 tqdm-4.67.1 typing-extensions-4.14.0 typing-inspection-0.4.1 tzdata-2025.2 urllib3-2.5.0 uvicorn-0.35.0 websockets-11.0.3 install('gradio==3.43.2', no_deps=True) install('gradio-client==0.5.0', no_deps=True, quiet=True) - pkgs = ['fastapi', 'websockets', 'aiofiles', 'ffmpy', 'pydub', 'uvicorn', 'semantic-version', 'altair', 'python-multipart', 'matplotlib'] - for pkg in pkgs: - if not installed(pkg, quiet=True): - install(pkg, quiet=True) + if not quick_allowed: # on quick path these are guaranteed installed by the state file + pkgs = ['fastapi', 'websockets', 'aiofiles', 'ffmpy', 'pydub', 'uvicorn', 'semantic-version', 'altair', 'python-multipart', 'matplotlib'] + for pkg in pkgs: + if not installed(pkg, quiet=True): + install(pkg, quiet=True) def install_pydantic(): @@ -1178,7 +1177,6 @@ def install_requirements(): if args.optional: quick_allowed = False install_optional() - installed('torch', reload=True) # reload packages cache log.info('Install: verifying requirements') if args.new: log.debug('Install: flag=new') @@ -1480,6 +1478,11 @@ def run_deferred_tasks(): t_start = time.time() log.debug('Starting deferred tasks') time.sleep(1.0) # wait for server to start + try: + from modules.sd_models import write_metadata + write_metadata() + except Exception as e: + log.error(f'Deferred task error: write_metadata {e}') try: check_version() except Exception as e: @@ -1514,20 +1517,28 @@ def get_state(): except Exception: pass try: + from concurrent.futures import ThreadPoolExecutor from modules.paths import extensions_builtin_dir, extensions_dir extension_folders = [extensions_builtin_dir] if args.safe else [extensions_builtin_dir, extensions_dir] + ext_dirs = [] for folder in extension_folders: if not os.path.isdir(folder): continue - extensions = list_extensions_folder(folder, quiet=True) - for ext in extensions: - extension_dir = os.path.join(folder, ext) - try: - res = subprocess.run('git rev-parse HEAD', capture_output=True, shell=True, check=False, cwd=extension_dir) - commit = res.stdout.decode(encoding='utf8', errors='ignore').strip() + for ext in list_extensions_folder(folder, quiet=True): + ext_dirs.append((ext, os.path.join(folder, ext))) + + def _get_commit(item): + ext, ext_dir = item + try: + res = subprocess.run('git rev-parse HEAD', capture_output=True, shell=True, check=False, cwd=ext_dir) + return ext, res.stdout.decode(encoding='utf8', errors='ignore').strip() + except Exception: + return ext, '' + + with ThreadPoolExecutor(max_workers=min(len(ext_dirs), 8), thread_name_prefix='sdnext-git') as pool: + for ext, commit in pool.map(_get_commit, ext_dirs): + if commit: state['extensions'][ext] = commit - except Exception: - pass except Exception: pass return state diff --git a/launch.py b/launch.py index 369aef053..b4076354d 100755 --- a/launch.py +++ b/launch.py @@ -1,13 +1,13 @@ #!/usr/bin/env python -from modules.logger import log +from functools import lru_cache import os import sys import time import shlex import subprocess -from functools import lru_cache import installer +from modules.logger import log debug_install = log.debug if os.environ.get('SD_INSTALL_DEBUG', None) is not None else lambda *args, **kwargs: None diff --git a/modules/control/proc/dwpose/__init__.py b/modules/control/proc/dwpose/__init__.py index 8f1f223af..ba1ea332c 100644 --- a/modules/control/proc/dwpose/__init__.py +++ b/modules/control/proc/dwpose/__init__.py @@ -49,7 +49,7 @@ def check_dependencies(): 'mmpose==1.3.2', 'mmdet==3.3.0', ] - status = [installed(p, reload=False, quiet=True) for p in packages] + status = [installed(p, quiet=True) for p in packages] debug(f'DWPose required={packages} status={status}') if not all(status): log.info(f'Installing dependencies: for=dwpose packages={packages}') diff --git a/modules/control/proc/mediapipe_face.py b/modules/control/proc/mediapipe_face.py index 834d7910c..ceda9f857 100644 --- a/modules/control/proc/mediapipe_face.py +++ b/modules/control/proc/mediapipe_face.py @@ -13,7 +13,7 @@ def check_dependencies(): from modules.logger import log packages = [('mediapipe', 'mediapipe')] for pkg in packages: - if not installed(pkg[1], reload=True, quiet=True): + if not installed(pkg[1], quiet=True): install(pkg[0], pkg[1], ignore=False) try: import mediapipe as mp # pylint: disable=unused-import diff --git a/modules/control/run.py b/modules/control/run.py index 0954abea0..d5e1fc8f3 100644 --- a/modules/control/run.py +++ b/modules/control/run.py @@ -12,7 +12,7 @@ from modules.control.units import lite # Kohya ControlLLLite from modules.control.units import t2iadapter # TencentARC T2I-Adapter from modules.control.units import reference # ControlNet-Reference from modules.control.processor import preprocess_image -from modules import devices, shared, errors, processing, images, sd_models, sd_vae, scripts_manager, masking +from modules import devices, shared, errors, processing, images, video, sd_models, sd_vae, scripts_manager, masking from modules.logger import log from modules.processing_class import StableDiffusionProcessingControl from modules.ui_common import infotext_to_html @@ -679,7 +679,7 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg if video_type != 'None' and isinstance(output_images, list) and 'video' in p.ops: p.do_not_save_grid = True # pylint: disable=attribute-defined-outside-init - output_filename = images.save_video(p, filename=None, images=output_images, video_type=video_type, duration=video_duration, loop=video_loop, pad=video_pad, interpolate=video_interpolate, sync=True) + output_filename = video.save_video(p, filename=None, images=output_images, video_type=video_type, duration=video_duration, loop=video_loop, pad=video_pad, interpolate=video_interpolate, sync=True) if shared.opts.gradio_skip_video: output_filename = '' image_txt = f'| Frames {len(output_images)} | Size {output_images[0].width}x{output_images[0].height}' diff --git a/modules/face/insightface.py b/modules/face/insightface.py index 99be2d32e..b167080e3 100644 --- a/modules/face/insightface.py +++ b/modules/face/insightface.py @@ -11,9 +11,9 @@ def get_app(mp_name, threshold=0.5, resolution=640): global insightface_app, instightface_mp # pylint: disable=global-statement from installer import install, installed, install_insightface - if not installed('insightface', reload=False, quiet=True): + if not installed('insightface', quiet=True): install_insightface() - if not installed('ip_adapter', reload=False, quiet=True): + if not installed('ip_adapter', quiet=True): install('git+https://github.com/tencent-ailab/IP-Adapter.git', 'ip_adapter', ignore=False) if insightface_app is None or mp_name != instightface_mp: diff --git a/modules/images.py b/modules/images.py index e5137c072..32e831e6c 100644 --- a/modules/images.py +++ b/modules/images.py @@ -1,7 +1,8 @@ from modules.image.metadata import image_data, read_info_from_image from modules.image.save import save_image, sanitize_filename_part from modules.image.resize import resize_image -from modules.image.grid import image_grid, check_grid_size, get_grid_size, draw_grid_annotations, draw_prompt_matrix +from modules.image.namegen import FilenameGenerator +from modules.image.grid import image_grid, check_grid_size, get_grid_size, draw_grid_annotations, draw_prompt_matrix, combine_grid __all__ = [ 'check_grid_size', @@ -10,8 +11,10 @@ __all__ = [ 'get_grid_size', 'image_data', 'image_grid', + 'combine_grid', 'read_info_from_image', 'resize_image', 'sanitize_filename_part', - 'save_image' + 'save_image', + 'FilenameGenerator', ] diff --git a/modules/intel/openvino/__init__.py b/modules/intel/openvino/__init__.py index 252c5d730..8eed0eb10 100644 --- a/modules/intel/openvino/__init__.py +++ b/modules/intel/openvino/__init__.py @@ -19,7 +19,7 @@ from types import MappingProxyType from hashlib import sha256 import functools -from modules import shared, devices, sd_models +from modules import shared, devices, sd_models, sd_models_utils from modules.logger import log @@ -527,7 +527,7 @@ def openvino_fx(subgraph, example_inputs, options=None): pass else: # Delete unused subgraphs - subgraph = subgraph.apply(sd_models.convert_to_faketensors) + subgraph = subgraph.apply(sd_models_utils.convert_to_faketensors) devices.torch_gc(force=True, reason='openvino') # Model is fully supported and already cached. Run the cached OV model directly. diff --git a/modules/lora/network.py b/modules/lora/network.py index a959a3338..9172a584d 100644 --- a/modules/lora/network.py +++ b/modules/lora/network.py @@ -1,7 +1,7 @@ import os import enum from collections import namedtuple -from modules import sd_models, hashes, shared +from modules import hashes, shared, sd_models, sd_checkpoint NetworkWeights = namedtuple('NetworkWeights', ['network_key', 'sd_key', 'w', 'sd_module']) @@ -33,7 +33,7 @@ class NetworkOnDisk: self.metadata = {} self.is_safetensors = os.path.splitext(filename)[1].lower() == ".safetensors" if self.is_safetensors: - self.metadata = sd_models.read_metadata_from_safetensors(filename) + self.metadata = sd_checkpoint.read_metadata_from_safetensors(filename) if self.metadata: m = {} for k, v in sorted(self.metadata.items(), key=lambda x: metadata_tags_order.get(x[0], 999)): diff --git a/modules/memmon.py b/modules/memmon.py index eb521f93f..32070ac79 100644 --- a/modules/memmon.py +++ b/modules/memmon.py @@ -55,7 +55,7 @@ class MemUsageMonitor: return self.data def summary(self): - from modules.shared import ram_stats + from modules.memstats import ram_stats gpu = '' cpu = '' gpu = '' diff --git a/modules/onnx_impl/execution_providers.py b/modules/onnx_impl/execution_providers.py index e8978aa06..1b0a47be5 100644 --- a/modules/onnx_impl/execution_providers.py +++ b/modules/onnx_impl/execution_providers.py @@ -99,7 +99,6 @@ def install_execution_provider(ep: ExecutionProvider): from installer import installed, install, uninstall res = "
"
     res += uninstall(["onnxruntime", "onnxruntime-directml", "onnxruntime-gpu", "onnxruntime-training", "onnxruntime-openvino"], quiet=True)
-    installed("onnxruntime", reload=True)
     packages = ["onnxruntime"] # Failed to load olive: cannot import name '__version__' from 'onnxruntime'
     if ep == ExecutionProvider.DirectML:
         packages.append("onnxruntime-directml")
diff --git a/modules/postprocess/esrgan_model.py b/modules/postprocess/esrgan_model.py
index f79e5de4b..0014725cb 100644
--- a/modules/postprocess/esrgan_model.py
+++ b/modules/postprocess/esrgan_model.py
@@ -4,6 +4,7 @@ from PIL import Image
 from rich.progress import Progress, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn, TimeElapsedColumn
 import modules.postprocess.esrgan_model_arch as arch
 from modules import images, devices, shared
+from modules.images.grid import split_grid
 from modules.logger import log, console
 from modules.upscaler import Upscaler, UpscalerData, compile_upscaler
 
@@ -193,7 +194,7 @@ def esrgan_upscale(model, img):
     if shared.opts.upscaler_tile_size == 0:
         return upscale_without_tiling(model, img)
 
-    grid = images.split_grid(img, shared.opts.upscaler_tile_size, shared.opts.upscaler_tile_size, shared.opts.upscaler_tile_overlap)
+    grid = split_grid(img, shared.opts.upscaler_tile_size, shared.opts.upscaler_tile_size, shared.opts.upscaler_tile_overlap)
     newtiles = []
     scale_factor = 1
 
diff --git a/modules/processing_vae.py b/modules/processing_vae.py
index 28bf33297..d720a57b5 100644
--- a/modules/processing_vae.py
+++ b/modules/processing_vae.py
@@ -2,7 +2,7 @@ import os
 import time
 import numpy as np
 import torch
-from modules import shared, devices, sd_models, sd_vae, errors
+from modules import shared, devices, errors, sd_models, sd_models_utils, sd_vae
 from modules.logger import log
 from modules.vae import sd_vae_taesd
 
@@ -71,7 +71,7 @@ def full_vqgan_decode(latents, model):
     if 'VAE' in shared.opts.cuda_compile and shared.opts.cuda_compile_backend == "openvino_fx" and shared.compiled_model_state.first_pass_vae:
         shared.compiled_model_state.first_pass_vae = False
         if not shared.opts.openvino_disable_memory_cleanup and hasattr(shared.sd_model, "vqgan"):
-            model.vqgan.apply(sd_models.convert_to_faketensors)
+            model.vqgan.apply(sd_models_utils.convert_to_faketensors)
             devices.torch_gc(force=True)
 
     if shared.opts.diffusers_offload_mode == "balanced":
@@ -166,7 +166,7 @@ def full_vae_decode(latents, model):
     if 'VAE' in shared.opts.cuda_compile and shared.opts.cuda_compile_backend == "openvino_fx" and shared.compiled_model_state.first_pass_vae:
         shared.compiled_model_state.first_pass_vae = False
         if not shared.opts.openvino_disable_memory_cleanup and hasattr(shared.sd_model, "vae"):
-            model.vae.apply(sd_models.convert_to_faketensors)
+            model.vae.apply(sd_models_utils.convert_to_faketensors)
             devices.torch_gc(force=True)
 
     elif shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False) and base_device is not None:
diff --git a/modules/sd_models_compile.py b/modules/sd_models_compile.py
index ecaf91989..0b68f6047 100644
--- a/modules/sd_models_compile.py
+++ b/modules/sd_models_compile.py
@@ -1,7 +1,7 @@
 import time
 import logging
 import torch
-from modules import shared, devices, sd_models, errors
+from modules import shared, errors, devices, sd_models, sd_models_utils
 from modules.logger import log
 from installer import setup_logging
 
@@ -319,10 +319,10 @@ def openvino_post_compile(op="base"): # delete unet after OpenVINO compile
         if shared.compiled_model_state.first_pass and op == "base":
             shared.compiled_model_state.first_pass = False
             if not shared.opts.openvino_disable_memory_cleanup and hasattr(shared.sd_model, "unet"):
-                shared.sd_model.unet.apply(sd_models.convert_to_faketensors)
+                shared.sd_model.unet.apply(sd_models_utils.convert_to_faketensors)
                 devices.torch_gc(force=True)
         if shared.compiled_model_state.first_pass_refiner and op == "refiner":
             shared.compiled_model_state.first_pass_refiner = False
             if not shared.opts.openvino_disable_memory_cleanup and hasattr(shared.sd_refiner, "unet"):
-                shared.sd_refiner.unet.apply(sd_models.convert_to_faketensors)
+                shared.sd_refiner.unet.apply(sd_models_utils.convert_to_faketensors)
                 devices.torch_gc(force=True)
diff --git a/modules/shared_state.py b/modules/shared_state.py
index f7a086849..9ada75aa0 100644
--- a/modules/shared_state.py
+++ b/modules/shared_state.py
@@ -271,7 +271,8 @@ class State:
     def do_set_current_image(self):
         if (self.current_latent is None) or self.disable_preview or (self.preview_job == self.job_no):
             return False
-        from modules import shared, sd_samplers
+        from modules import shared, sd_samplers, sd_samplers_common
+        from modules.sd_samplers_common import samples_to_image_grid, sample_to_image
         self.preview_job = self.job_no
         try:
             sample = self.current_latent
@@ -285,7 +286,7 @@ class State:
                         sample = self.current_noise_pred * (-self.current_sigma / (self.current_sigma**2 + 1) ** 0.5) + (original_sample / (self.current_sigma**2 + 1)) # pylint: disable=invalid-unary-operand-type
             except Exception:
                 pass # ignore sigma errors
-            image = sd_samplers.samples_to_image_grid(sample) if shared.opts.show_progress_grid else sd_samplers.sample_to_image(sample)
+            image = samples_to_image_grid(sample) if shared.opts.show_progress_grid else sample_to_image(sample)
             self.assign_current_image(image)
             self.preview_job = -1
             return True
diff --git a/modules/theme.py b/modules/theme.py
index 459fb8df4..8af1b0148 100644
--- a/modules/theme.py
+++ b/modules/theme.py
@@ -3,6 +3,7 @@ import json
 import gradio as gr
 import modules.shared
 import modules.extensions
+from modules.logger import log
 
 
 gradio_theme = gr.themes.Base()
@@ -21,18 +22,18 @@ def refresh_themes(no_update=False):
             with open(themes_file, encoding='utf8') as f:
                 res = json.load(f)
         except Exception:
-            modules.log.error('Exception loading UI themes')
+            log.error('Exception loading UI themes')
     if not no_update:
         try:
-            modules.log.info('Refreshing UI themes')
+            log.info('Refreshing UI themes')
             r = modules.shared.req('https://huggingface.co/datasets/freddyaboulton/gradio-theme-subdomains/resolve/main/subdomains.json')
             if r.status_code == 200:
                 res = r.json()
                 modules.shared.writefile(res, themes_file)
             else:
-                modules.log.error('Error refreshing UI themes')
+                log.error('Error refreshing UI themes')
         except Exception:
-            modules.log.error('Exception refreshing UI themes')
+            log.error('Exception refreshing UI themes')
     return res
 
 
@@ -46,12 +47,12 @@ def list_themes():
         themes = ['lobe']
         modules.shared.opts.data['gradio_theme'] = themes[0]
         modules.shared.opts.data['theme_type'] = 'None'
-        modules.log.info('UI theme: extension="lobe"')
+        log.info('UI theme: extension="lobe"')
     elif 'Cozy-Nest' in extensions and modules.shared.opts.gradio_theme == 'cozy-nest':
         themes = ['cozy-nest']
         modules.shared.opts.data['gradio_theme'] = themes[0]
         modules.shared.opts.data['theme_type'] = 'None'
-        modules.log.info('UI theme: extension="cozy-nest"')
+        log.info('UI theme: extension="cozy-nest"')
     elif modules.shared.opts.theme_type == 'None':
         gradio = ["gradio/default", "gradio/base", "gradio/glass", "gradio/monochrome", "gradio/soft"]
         huggingface = refresh_themes(no_update=True)
@@ -64,7 +65,7 @@ def list_themes():
     elif modules.shared.opts.theme_type == 'Modern':
         ext = next((e for e in modules.extensions.extensions if e.name == 'sdnext-modernui'), None)
         if ext is None:
-            modules.log.error('UI themes: ModernUI not found')
+            log.error('UI themes: ModernUI not found')
             builtin = list_builtin_themes()
             themes = sorted(builtin)
             modules.shared.opts.theme_type = 'Standard'
@@ -79,7 +80,7 @@ def list_themes():
             themes.append('modern/Default')
         themes = sorted(themes)
     else:
-        modules.log.error(f'UI themes: type={modules.shared.opts.theme_type} unknown')
+        log.error(f'UI themes: type={modules.shared.opts.theme_type} unknown')
         themes = []
     return themes
 
@@ -94,7 +95,7 @@ def reload_gradio_theme():
     gradio_theme = gr.themes.Base(**default_font_params)
     available_themes = list_themes()
     if theme_name not in available_themes:
-        # modules.log.error(f'UI theme invalid: type={modules.shared.opts.theme_type} theme="{theme_name}"')
+        # log.error(f'UI theme invalid: type={modules.shared.opts.theme_type} theme="{theme_name}"')
         if modules.shared.opts.theme_type == 'Standard':
             theme_name = 'black-teal'
         elif modules.shared.opts.theme_type == 'Modern':
@@ -106,22 +107,22 @@ def reload_gradio_theme():
             theme_name = 'black-teal'
 
     modules.shared.opts.data['gradio_theme'] = theme_name
-    modules.log.info(f'UI locale: name="{modules.shared.opts.ui_locale}"')
+    log.info(f'UI locale: name="{modules.shared.opts.ui_locale}"')
 
     if theme_name.lower() in ['lobe', 'cozy-nest']:
-        modules.log.info(f'UI theme extension: name="{theme_name}"')
+        log.info(f'UI theme extension: name="{theme_name}"')
         return None
     elif modules.shared.opts.theme_type == 'Standard':
         gradio_theme = gr.themes.Base(**default_font_params)
-        modules.log.info(f'UI theme: type={modules.shared.opts.theme_type} name="{theme_name}" available={len(available_themes)}')
+        log.info(f'UI theme: type={modules.shared.opts.theme_type} name="{theme_name}" available={len(available_themes)}')
         return 'sdnext.css'
     elif modules.shared.opts.theme_type == 'Modern':
         gradio_theme = gr.themes.Base(**default_font_params)
-        modules.log.info(f'UI theme: type={modules.shared.opts.theme_type} name="{theme_name}" available={len(available_themes)}')
+        log.info(f'UI theme: type={modules.shared.opts.theme_type} name="{theme_name}" available={len(available_themes)}')
         return 'base.css'
     elif modules.shared.opts.theme_type == 'None':
         if theme_name.startswith('gradio/'):
-            modules.log.warning('UI theme: using Gradio default theme which is not optimized for SD.Next')
+            log.warning('UI theme: using Gradio default theme which is not optimized for SD.Next')
             if theme_name == "gradio/default":
                 gradio_theme = gr.themes.Default(**default_font_params)
             elif theme_name == "gradio/base":
@@ -133,18 +134,18 @@ def reload_gradio_theme():
             elif theme_name == "gradio/soft":
                 gradio_theme = gr.themes.Soft(**default_font_params)
             else:
-                modules.log.warning('UI theme: unknown Gradio theme')
+                log.warning('UI theme: unknown Gradio theme')
                 theme_name = "gradio/default"
                 gradio_theme = gr.themes.Default(**default_font_params)
         elif theme_name.startswith('huggingface/'):
-            modules.log.warning('UI theme: using 3rd party theme which is not optimized for SD.Next')
+            log.warning('UI theme: using 3rd party theme which is not optimized for SD.Next')
             try:
                 hf_theme_name = theme_name.replace('huggingface/', '')
                 gradio_theme = gr.themes.ThemeClass.from_hub(hf_theme_name)
             except Exception as e:
-                modules.log.error(f"UI theme: download error accessing HuggingFace {e}")
+                log.error(f"UI theme: download error accessing HuggingFace {e}")
                 gradio_theme = gr.themes.Default(**default_font_params)
-        modules.log.info(f'UI theme: type={modules.shared.opts.theme_type} name="{theme_name}" style={modules.shared.opts.theme_style}')
+        log.info(f'UI theme: type={modules.shared.opts.theme_type} name="{theme_name}" style={modules.shared.opts.theme_style}')
         return 'base.css'
-    modules.log.error(f'UI theme: type={modules.shared.opts.theme_type} unknown')
+    log.error(f'UI theme: type={modules.shared.opts.theme_type} unknown')
     return None
diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py
index 094912e18..b1ade3229 100644
--- a/modules/ui_extra_networks.py
+++ b/modules/ui_extra_networks.py
@@ -18,6 +18,7 @@ from PIL import Image
 from starlette.responses import FileResponse, JSONResponse
 from modules import paths, shared, files_cache, errors, infotext, ui_symbols, ui_components, modelstats
 from modules.logger import log
+from modules.json_helpers import writefile
 
 
 allowed_dirs = []
@@ -838,7 +839,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
 
     def fn_save_info(info):
         fn = os.path.splitext(ui.last_item.filename)[0] + '.json'
-        shared.writefile(info, fn, silent=True)
+        writefile(info, fn, silent=True)
         log.debug(f'Network save info: item="{ui.last_item.name}" filename="{fn}"')
         return info
 
@@ -851,7 +852,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
         fn = os.path.splitext(ui.last_item.filename)[0] + '.json'
         if hasattr(ui.last_item, 'type') and ui.last_item.type == 'Style':
             info.update(**{ 'description': description, 'prompt': prompt, 'negative': negative, 'extra': extra, 'wildcards': wildcards })
-            shared.writefile(info, fn, silent=True)
+            writefile(info, fn, silent=True)
             log.debug(f'Network save style: item="{ui.last_item.name}" filename="{fn}"')
         return info
 
@@ -1077,7 +1078,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False):
             "negative": negative,
             "extra": '',
         }
-        shared.writefile(item, fn, silent=True)
+        writefile(item, fn, silent=True)
         if len(prompt) > 0:
             log.debug(f'Networks type=style quicksave style: item="{name}" filename="{fn}" prompt="{prompt}"')
         else:
diff --git a/modules/ui_javascript.py b/modules/ui_javascript.py
index 0eb88de7c..c789535df 100644
--- a/modules/ui_javascript.py
+++ b/modules/ui_javascript.py
@@ -82,17 +82,17 @@ def html_css(css: list[str]):
         themecss = os.path.join(script_path, "javascript", f"{modules.shared.opts.gradio_theme}.css")
         if os.path.exists(themecss):
             head += stylesheet(themecss)
-            modules.log.debug(f'UI theme: css="{themecss}" base="{css}" user="{usercss}"')
+            log.debug(f'UI theme: css="{themecss}" base="{css}" user="{usercss}"')
         else:
-            modules.log.error(f'UI theme: css="{themecss}" not found')
+            log.error(f'UI theme: css="{themecss}" not found')
     elif modules.shared.opts.theme_type == 'Modern':
         theme_folder = next((e.path for e in modules.extensions.extensions if e.name == 'sdnext-modernui'), None)
         themecss = os.path.join(theme_folder or '', 'themes', f'{modules.shared.opts.gradio_theme}.css')
         if os.path.exists(themecss):
             head += stylesheet(themecss)
-            modules.log.debug(f'UI theme: css="{themecss}" base="{css}" user="{usercss}"')
+            log.debug(f'UI theme: css="{themecss}" base="{css}" user="{usercss}"')
         else:
-            modules.log.error(f'UI theme: css="{themecss}" not found')
+            log.error(f'UI theme: css="{themecss}" not found')
     if usercss is not None:
         head += stylesheet(usercss)
     return head
diff --git a/scripts/animatediff.py b/scripts/animatediff.py
index 93c5615b2..ee23187b6 100644
--- a/scripts/animatediff.py
+++ b/scripts/animatediff.py
@@ -263,7 +263,7 @@ class Script(scripts_manager.Script):
 
 
     def after(self, p: processing.StableDiffusionProcessing, processed: processing.Processed, adapter_index, frames, lora_index, strength, latent_mode, video_type, duration, gif_loop, mp4_pad, mp4_interpolate, override_scheduler, fi_method, fi_iters, fi_order, fi_spatial, fi_temporal): # pylint: disable=arguments-differ, unused-argument
-        from modules.images import save_video
+        from modules.video import save_video
         if video_type != 'None':
             log.debug(f'AnimateDiff video: type={video_type} duration={duration} loop={gif_loop} pad={mp4_pad} interpolate={mp4_interpolate}')
             save_video(p, filename=None, images=processed.images, video_type=video_type, duration=duration, loop=gif_loop, pad=mp4_pad, interpolate=mp4_interpolate)
diff --git a/scripts/image2video.py b/scripts/image2video.py
index ef756381d..be9efed54 100644
--- a/scripts/image2video.py
+++ b/scripts/image2video.py
@@ -1,7 +1,7 @@
 import torch
 import gradio as gr
 import diffusers
-from modules import scripts_manager, processing, shared, images, sd_models, devices
+from modules import scripts_manager, processing, shared, images, video, sd_models, devices
 from modules.logger import log
 
 
@@ -110,5 +110,5 @@ class Script(scripts_manager.Script):
 
         shared.sd_model = orig_pipeline
         if video_type != 'None' and processed is not None:
-            images.save_video(p, filename=None, images=processed.images, video_type=video_type, duration=duration, loop=gif_loop, pad=mp4_pad, interpolate=mp4_interpolate)
+            video.save_video(p, filename=None, images=processed.images, video_type=video_type, duration=duration, loop=gif_loop, pad=mp4_pad, interpolate=mp4_interpolate)
         return processed
diff --git a/scripts/infiniteyou_ext.py b/scripts/infiniteyou_ext.py
index 2411aa267..1b551fa88 100644
--- a/scripts/infiniteyou_ext.py
+++ b/scripts/infiniteyou_ext.py
@@ -15,7 +15,7 @@ orig_pipeline, orig_prompt_attention = None, None
 
 def verify_insightface():
     from installer import installed, install_insightface
-    if not installed('insightface', reload=False, quiet=True):
+    if not installed('insightface', quiet=True):
         install_insightface()
 
 
diff --git a/scripts/mixture_tiling.py b/scripts/mixture_tiling.py
index d411e50ab..5b95e694b 100644
--- a/scripts/mixture_tiling.py
+++ b/scripts/mixture_tiling.py
@@ -14,7 +14,7 @@ def check_dependencies():
         ('ligo-segments', 'ligo-segments'),
     ]
     for pkg in packages:
-        if not installed(pkg[1], reload=True, quiet=True):
+        if not installed(pkg[1], quiet=True):
             install(pkg[0], pkg[1], ignore=False)
     try:
         from ligo.segments import segment # pylint: disable=unused-import
diff --git a/scripts/poor_mans_outpainting.py b/scripts/poor_mans_outpainting.py
index 9d9d4fb22..6569ce290 100644
--- a/scripts/poor_mans_outpainting.py
+++ b/scripts/poor_mans_outpainting.py
@@ -4,6 +4,7 @@ from PIL import Image, ImageDraw
 from modules import images, devices, scripts_manager
 from modules.processing import get_processed, process_images
 from modules.shared import opts, state, log
+from modules.images.grid import split_grid
 
 
 class Script(scripts_manager.Script):
@@ -64,9 +65,9 @@ class Script(scripts_manager.Script):
              mask.height - down - (mask_blur//2 if down > 0 else 0)
         ), fill="black")
         devices.torch_gc()
-        grid = images.split_grid(img, tile_w=p.width, tile_h=p.height, overlap=pixels)
-        grid_mask = images.split_grid(mask, tile_w=p.width, tile_h=p.height, overlap=pixels)
-        grid_latent_mask = images.split_grid(latent_mask, tile_w=p.width, tile_h=p.height, overlap=pixels)
+        grid = split_grid(img, tile_w=p.width, tile_h=p.height, overlap=pixels)
+        grid_mask = split_grid(mask, tile_w=p.width, tile_h=p.height, overlap=pixels)
+        grid_latent_mask = split_grid(latent_mask, tile_w=p.width, tile_h=p.height, overlap=pixels)
         p.n_iter = 1
         p.batch_size = 1
         p.do_not_save_grid = True
diff --git a/scripts/postprocessing_video.py b/scripts/postprocessing_video.py
index 17ff802f8..84836abca 100644
--- a/scripts/postprocessing_video.py
+++ b/scripts/postprocessing_video.py
@@ -1,6 +1,5 @@
 import gradio as gr
-import modules.images
-from modules import scripts_postprocessing
+from modules import video, scripts_postprocessing
 
 
 class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing):
@@ -47,4 +46,4 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing):
         filename = filename.strip() if filename is not None else ''
         if video_type == 'None' or len(filename) == 0 or images is None or len(images) < 2:
             return
-        modules.images.save_video(p=None, filename=filename, images=images, video_type=video_type, duration=duration, loop=loop, pad=pad, interpolate=interpolate, scale=scale, change=change)
+        video.save_video(p=None, filename=filename, images=images, video_type=video_type, duration=duration, loop=loop, pad=pad, interpolate=interpolate, scale=scale, change=change)
diff --git a/scripts/pulid_ext.py b/scripts/pulid_ext.py
index 1eb52556d..62e606f47 100644
--- a/scripts/pulid_ext.py
+++ b/scripts/pulid_ext.py
@@ -30,7 +30,7 @@ class Script(scripts_manager.Script):
 
     def dependencies(self):
         from installer import installed, install, install_insightface
-        if not installed('insightface', reload=False, quiet=True):
+        if not installed('insightface', quiet=True):
             install_insightface()
         if not installed('torchdiffeq'):
             install('torchdiffeq')
diff --git a/scripts/sd_upscale.py b/scripts/sd_upscale.py
index ee944f54a..206efd7ea 100644
--- a/scripts/sd_upscale.py
+++ b/scripts/sd_upscale.py
@@ -5,6 +5,7 @@ from modules import processing, shared, images, devices, scripts_manager
 from modules.processing import get_processed
 from modules.shared import opts, state, log
 from modules.image.util import flatten
+from modules.images.grid import split_grid
 
 
 class Script(scripts_manager.Script):
@@ -48,7 +49,7 @@ class Script(scripts_manager.Script):
         else:
             img = init_img
         devices.torch_gc()
-        grid = images.split_grid(img, tile_w=init_img.width, tile_h=init_img.height, overlap=overlap)
+        grid = split_grid(img, tile_w=init_img.width, tile_h=init_img.height, overlap=overlap)
         batch_size = p.batch_size
         upscale_count = p.n_iter
         p.n_iter = 1
diff --git a/scripts/stablevideodiffusion.py b/scripts/stablevideodiffusion.py
index 2ded2a9c2..6ea267256 100644
--- a/scripts/stablevideodiffusion.py
+++ b/scripts/stablevideodiffusion.py
@@ -5,7 +5,7 @@ Additional params for StableVideoDiffusion
 import os
 import torch
 import gradio as gr
-from modules import scripts_manager, processing, shared, sd_models, images, modelloader
+from modules import scripts_manager, processing, shared, sd_models, images, modelloader, video
 from modules.logger import log
 
 
@@ -121,5 +121,5 @@ class Script(scripts_manager.Script):
         # run processing
         processed = processing.process_images(p)
         if video_type != 'None':
-            images.save_video(p, filename=None, images=processed.images, video_type=video_type, duration=duration, loop=gif_loop, pad=mp4_pad, interpolate=mp4_interpolate)
+            video.save_video(p, filename=None, images=processed.images, video_type=video_type, duration=duration, loop=gif_loop, pad=mp4_pad, interpolate=mp4_interpolate)
         return processed
diff --git a/scripts/text2video.py b/scripts/text2video.py
index 81c28799c..3605ff4a1 100644
--- a/scripts/text2video.py
+++ b/scripts/text2video.py
@@ -7,7 +7,7 @@ TODO text2video items:
 """
 
 import gradio as gr
-from modules import scripts_manager, processing, shared, images, sd_models, modelloader
+from modules import scripts_manager, processing, shared, images, video, sd_models, modelloader
 from modules.logger import log
 
 
@@ -92,5 +92,5 @@ class Script(scripts_manager.Script):
         processed = processing.process_images(p)
 
         if video_type != 'None':
-            images.save_video(p, filename=None, images=processed.images, video_type=video_type, duration=duration, loop=gif_loop, pad=mp4_pad, interpolate=mp4_interpolate)
+            video.save_video(p, filename=None, images=processed.images, video_type=video_type, duration=duration, loop=gif_loop, pad=mp4_pad, interpolate=mp4_interpolate)
         return processed
diff --git a/scripts/xyz/xyz_grid_classes.py b/scripts/xyz/xyz_grid_classes.py
index a6c6678b7..ca78569ad 100644
--- a/scripts/xyz/xyz_grid_classes.py
+++ b/scripts/xyz/xyz_grid_classes.py
@@ -248,8 +248,8 @@ axis_options = [
     AxisOption("[Postprocess] Context", str, apply_context, choices=lambda: ["Add with forward", "Remove with forward", "Add with backward", "Remove with backward"]),
     AxisOption("[Postprocess] Detailer", bool, apply_detailer, fmt=format_bool, choices=lambda: [False, True]),
     AxisOption("[Postprocess] Detailer strength", str, apply_field("detailer_strength")),
-    AxisOption("[Quant] SDNQ quant mode", str, apply_sdnq_quant, cost=0.9, fmt=format_value_add_label, choices=lambda: ['none'] + sorted(shared.sdnq_quant_modes)),
-    AxisOption("[Quant] SDNQ quant mode TE", str, apply_sdnq_quant_te, cost=0.9, fmt=format_value_add_label, choices=lambda: ['none'] + sorted(shared.sdnq_quant_modes)),
+    AxisOption("[Quant] SDNQ quant mode", str, apply_sdnq_quant, cost=0.9, fmt=format_value_add_label, choices=lambda: ['none'] + sorted(shared_items.sdnq_quant_modes)),
+    AxisOption("[Quant] SDNQ quant mode TE", str, apply_sdnq_quant_te, cost=0.9, fmt=format_value_add_label, choices=lambda: ['none'] + sorted(shared_items.sdnq_quant_modes)),
     AxisOption("[HDR] Mode", int, apply_field("hdr_mode")),
     AxisOption("[HDR] Brightness", float, apply_field("hdr_brightness")),
     AxisOption("[HDR] Color", float, apply_field("hdr_color")),
diff --git a/scripts/xyz/xyz_grid_draw.py b/scripts/xyz/xyz_grid_draw.py
index 0c4efa705..ed88b656f 100644
--- a/scripts/xyz/xyz_grid_draw.py
+++ b/scripts/xyz/xyz_grid_draw.py
@@ -1,15 +1,16 @@
 import time
 from copy import copy
 from PIL import Image
+from modues.images.grid import GridAnnotation
 from modules import shared, images, processing
 from modules.logger import log
 from modules.image.util import draw_text
 
 
 def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend, include_lone_images, include_sub_grids, first_axes_processed, second_axes_processed, margin_size, no_grid: False, include_time: False, include_text: False): # pylint: disable=unused-argument
-    x_texts = [[images.GridAnnotation(x)] for x in x_labels]
-    y_texts = [[images.GridAnnotation(y)] for y in y_labels]
-    z_texts = [[images.GridAnnotation(z)] for z in z_labels]
+    x_texts = [[GridAnnotation(x)] for x in x_labels]
+    y_texts = [[GridAnnotation(y)] for y in y_labels]
+    z_texts = [[GridAnnotation(z)] for z in z_labels]
     list_size = (len(xs) * len(ys) * len(zs))
     processed_result = None
 
diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py
index 50dd4be6e..c8681229a 100644
--- a/scripts/xyz_grid.py
+++ b/scripts/xyz_grid.py
@@ -12,7 +12,7 @@ from scripts.xyz.xyz_grid_shared import str_permutations, list_to_csv_string, re
 from scripts.xyz.xyz_grid_classes import axis_options, AxisOption, SharedSettingsStackHelper # pylint: disable=no-name-in-module
 from scripts.xyz.xyz_grid_draw import draw_xyz_grid # pylint: disable=no-name-in-module
 from scripts.xyz.xyz_grid_shared import apply_field, apply_task_args, apply_setting, apply_prompt, apply_order, apply_sampler, apply_hr_sampler_name, confirm_samplers, apply_checkpoint, apply_refiner, apply_unet, apply_clip_skip, apply_vae, list_lora, apply_lora, apply_lora_strength, apply_te, apply_styles, apply_upscaler, apply_context, apply_detailer, apply_override, apply_processing, apply_options, apply_seed, format_value_add_label, format_value, format_value_join_list, do_nothing, format_nothing # pylint: disable=no-name-in-module, unused-import
-from modules import shared, errors, scripts_manager, images, processing
+from modules import shared, errors, scripts_manager, images, video, processing
 from modules.ui_components import ToolButton
 from modules.ui_sections import create_video_inputs
 import modules.ui_symbols as symbols
@@ -412,7 +412,7 @@ class Script(scripts_manager.Script):
             debug(f'XYZ grid remove subgrids: total={processed.images}')
 
         if create_video and video_type != 'None' and not shared.state.interrupted:
-            images.save_video(p, filename=None, images=have_images, video_type=video_type, duration=video_duration, loop=video_loop, pad=video_pad, interpolate=video_interpolate)
+            video.save_video(p, filename=None, images=have_images, video_type=video_type, duration=video_duration, loop=video_loop, pad=video_pad, interpolate=video_interpolate)
 
         shared.state.end(jobid)
         return processed
diff --git a/scripts/xyz_grid_on.py b/scripts/xyz_grid_on.py
index 24851a7bb..6a9af5b00 100644
--- a/scripts/xyz_grid_on.py
+++ b/scripts/xyz_grid_on.py
@@ -11,7 +11,7 @@ import gradio as gr
 from scripts.xyz.xyz_grid_shared import str_permutations, list_to_csv_string, restore_comma, re_range, re_plain_comma # pylint: disable=no-name-in-module
 from scripts.xyz.xyz_grid_classes import axis_options, AxisOption, SharedSettingsStackHelper # pylint: disable=no-name-in-module
 from scripts.xyz.xyz_grid_draw import draw_xyz_grid # pylint: disable=no-name-in-module
-from modules import shared, errors, scripts_manager, images, processing
+from modules import shared, errors, scripts_manager, images, video, processing
 from modules.ui_components import ToolButton
 from modules.ui_sections import create_video_inputs
 import modules.ui_symbols as symbols
@@ -440,7 +440,7 @@ class Script(scripts_manager.Script):
             debug(f'XYZ grid remove subgrids: total={processed.images}')
 
         if create_video and video_type != 'None' and not shared.state.interrupted:
-            images.save_video(p, filename=None, images=have_images, video_type=video_type, duration=video_duration, loop=video_loop, pad=video_pad, interpolate=video_interpolate)
+            video.save_video(p, filename=None, images=have_images, video_type=video_type, duration=video_duration, loop=video_loop, pad=video_pad, interpolate=video_interpolate)
 
         p.do_not_save_grid = True
         p.do_not_save_samples = True
diff --git a/webui.py b/webui.py
index 4097f62bf..3d25bc047 100644
--- a/webui.py
+++ b/webui.py
@@ -74,6 +74,7 @@ fastapi_args = {
 
 def initialize():
     log.debug('Initializing: modules')
+    from concurrent.futures import ThreadPoolExecutor, as_completed
 
     modules.sd_checkpoint.init_metadata()
     modules.hashes.init_cache()
@@ -81,22 +82,32 @@ def initialize():
     modules.sd_samplers.list_samplers()
     timer.startup.record("samplers")
 
-    modules.sd_vae.refresh_vae_list()
-    timer.startup.record("vae")
+    # run independent filesystem scans in parallel
+    def _scan_vae():
+        modules.sd_vae.refresh_vae_list()
+    def _scan_unet():
+        modules.sd_unet.refresh_unet_list()
+    def _scan_te():
+        modules.model_te.refresh_te_list()
+    def _scan_models():
+        modules.modelloader.cleanup_models()
+        modules.sd_checkpoint.setup_model()
+    def _scan_lora():
+        from modules.lora import lora_load
+        lora_load.list_available_networks()
+    def _scan_upscalers():
+        modules.modelloader.load_upscalers()
 
-    modules.sd_unet.refresh_unet_list()
-    timer.startup.record("unet")
-
-    modules.model_te.refresh_te_list()
-    timer.startup.record("te")
-
-    modules.modelloader.cleanup_models()
-    modules.sd_checkpoint.setup_model()
-    timer.startup.record("models")
-
-    from modules.lora import lora_load
-    lora_load.list_available_networks()
-    timer.startup.record("lora")
+    scans = [_scan_vae, _scan_unet, _scan_te, _scan_models, _scan_lora, _scan_upscalers]
+    with ThreadPoolExecutor(max_workers=len(scans), thread_name_prefix='sdnext-scan') as pool:
+        futures = {pool.submit(fn): fn.__name__ for fn in scans}
+        for future in as_completed(futures):
+            name = futures[future]
+            try:
+                future.result()
+            except Exception as e:
+                log.error(f'Scan error: {name} {e}')
+    timer.startup.record("scans")
 
     shared.prompt_styles.reload()
     timer.startup.record("styles")
@@ -115,9 +126,6 @@ def initialize():
     timer.startup.records["extensions"] = t_total # scripts can reset the time
     log.debug(f'Extensions init time: {t_timer.summary()}')
 
-    modules.modelloader.load_upscalers()
-    timer.startup.record("upscalers")
-
     modules.ui_extra_networks.initialize()
     modules.ui_extra_networks.register_pages()
     modules.extra_networks.initialize()
@@ -128,6 +136,7 @@ def initialize():
     hf_init()
     hf_check_cache()
 
+
     if shared.cmd_opts.tls_keyfile is not None and shared.cmd_opts.tls_certfile is not None:
         try:
             if not os.path.exists(shared.cmd_opts.tls_keyfile):
@@ -374,7 +383,6 @@ def webui(restart=False):
     start_common()
     app = start_ui()
     modules.script_callbacks.after_ui_callback()
-    modules.sd_models.write_metadata()
 
     load_model()
     mount_subpath(app)