From afe3786f5f2d0e279a612ef1f1e7d029edc5889d Mon Sep 17 00:00:00 2001 From: vladmandic Date: Sun, 22 Mar 2026 11:07:09 +0100 Subject: [PATCH] add unet/dir to networks Signed-off-by: vladmandic --- CHANGELOG.md | 11 +++++--- javascript/ui.js | 8 ++++++ modules/api/api.py | 2 ++ modules/api/endpoints.py | 11 +++++++- modules/api/models.py | 4 +++ modules/civitai/metadata_civitai.py | 4 +-- modules/lora/lora_apply.py | 2 +- modules/options_handler.py | 5 +++- modules/sd_checkpoint.py | 26 +++++++++-------- modules/sd_unet.py | 1 + modules/shared.py | 1 + modules/ui_extra_networks.py | 6 ++-- modules/ui_extra_networks_unet.py | 43 +++++++++++++++++++++++++++++ modules/ui_models.py | 12 +++++++- modules/ui_settings.py | 7 +++++ 15 files changed, 119 insertions(+), 24 deletions(-) create mode 100644 modules/ui_extra_networks_unet.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 503b46a0e..7951844f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,22 @@ # Change Log for SD.Next -## Update for 2026-03-20 +## Update for 2026-03-22 -### Highlights for 2026-03-20 +### Highlights for 2026-03-22 This release brings massive code refactoring to modernize codebase and removal of some obsolete features. Leaner & Faster! And since its a bit quieter period when it comes to new models, notable additions would be : *FireRed-Image-Edit* *SkyWorks-UniPic-3* and *Anima-Preview-2* +If you're on Windows platform, we have a brand new [All-in-one Installer & Launcher](https://github.com/vladmandic/sdnext-launcher): simply download `exe` or `zip` and done! + *What else*? Really a lot! -New color grading module, updated localization with new languages and improved translations, new civitai integration module, several new upscalers, improvements to LLM/VLM in captioning and prompt enhance, a lot of new control preprocessors, new realtime server info panel, some new UI themes +New color grading module, updated localization with new languages and improved translations, new civitai integration module, new finetunes loader, several new upscalers, improvements to LLM/VLM in captioning and prompt enhance, a lot of new control preprocessors, new realtime server info panel, some new UI themes And major work on API hardening: security, rate limits, secrets handling, new endpoints, etc. But also many smaller quality-of-life improvements - for full details, see [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) [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) | [Sponsor](https://github.com/sponsors/vladmandic) -### Details for 2026-03-20 +### Details for 2026-03-22 - **Models** - [Google Flash 3.1 Image](https://ai.google.dev/gemini-api/docs/models/gemini-3-flash-preview) a.k.a. *Nano Banana 2* @@ -58,6 +60,7 @@ But also many smaller quality-of-life improvements - for full details, see [Chan > `set TORCH_COMMAND='torch==2.9.1 torchvision==0.24.1 torchaudio==2.9.1 --index-url https://download.pytorch.org/whl/cu126'` - **UI** - new panel: **server info** with detailed runtime informaton + - **networks** add **UNet/DiT** - **localization** improved translation quality and new translations locales: *en, en1, en2, en3, en4, hr, es, it, fr, de, pt, ru, zh, ja, ko, hi, ar, bn, ur, id, vi, tr, sr, po, he, xx, yy, qq, tlh* yes, this now includes stuff like *latin, esperanto, arabic, hebrew, klingon* and a lot more! diff --git a/javascript/ui.js b/javascript/ui.js index e76d55611..913cb0fc6 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -524,6 +524,14 @@ function selectVAE(name) { markSelectedCards([desiredVAEName], 'vae'); } +let desiredUNetName = null; +function selectUNet(name) { + desiredUNetName = name; + gradioApp().getElementById('change_unet').click(); + log(`selectUNet: ${desiredUNetName}`); + markSelectedCards([desiredUNetName], 'unet'); +} + function selectReference(name) { log(`selectReference: ${name}`); desiredCheckpointName = name; diff --git a/modules/api/api.py b/modules/api/api.py index 15cf83884..f460fea51 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -88,6 +88,7 @@ class Api: self.add_api_route("/sdapi/v1/sd-vae", endpoints.get_sd_vaes, methods=["GET"], response_model=list[models.ItemVae]) self.add_api_route("/sdapi/v1/extensions", endpoints.get_extensions_list, methods=["GET"], response_model=list[models.ItemExtension]) self.add_api_route("/sdapi/v1/extra-networks", endpoints.get_extra_networks, methods=["GET"], response_model=list[models.ItemExtraNetwork]) + self.add_api_route("/sdapi/v1/unets", endpoints.get_unets, methods=["GET"], response_model=list[models.ItemUNet]) # functional api self.add_api_route("/sdapi/v1/png-info", endpoints.post_pnginfo, methods=["POST"], response_model=models.ResImageInfo, tags=["Functional"]) @@ -98,6 +99,7 @@ class Api: self.add_api_route("/sdapi/v1/reload-checkpoint", endpoints.post_reload_checkpoint, methods=["POST"], tags=["Functional"]) self.add_api_route("/sdapi/v1/lock-checkpoint", endpoints.post_lock_checkpoint, methods=["POST"], tags=["Functional"]) self.add_api_route("/sdapi/v1/refresh-vae", endpoints.post_refresh_vae, methods=["POST"], tags=["Functional"]) + self.add_api_route("/sdapi/v1/refresh-unets", endpoints.post_refresh_unets, methods=["POST"], tags=["Functional"]) self.add_api_route("/sdapi/v1/latents", endpoints.get_latent_history, methods=["GET"], response_model=list[str], tags=["Functional"]) self.add_api_route("/sdapi/v1/latents", endpoints.post_latent_history, methods=["POST"], response_model=int, tags=["Functional"]) self.add_api_route("/sdapi/v1/modules", endpoints.get_modules, methods=["GET"], tags=["Functional"]) diff --git a/modules/api/endpoints.py b/modules/api/endpoints.py index 775fa1bc9..6f02b2986 100644 --- a/modules/api/endpoints.py +++ b/modules/api/endpoints.py @@ -65,7 +65,6 @@ get_restorers = get_detailers # legacy alias for /sdapi/v1/face-restorers def get_ip_adapters(): """ List available IP-Adapter models. - Returns adapter names that can be used for image-prompt conditioning during generation. """ from modules import ipadapter @@ -75,6 +74,11 @@ def get_prompt_styles(): """List all saved prompt styles with their prompt, negative prompt, and preview.""" return [{ 'name': v.name, 'prompt': v.prompt, 'negative_prompt': v.negative_prompt, 'extra': v.extra, 'filename': v.filename, 'preview': v.preview} for v in shared.prompt_styles.styles.values()] +def get_unets(): + """List available UNet models with their names and filenames.""" + from modules.sd_unet import unet_dict + return [{"name": k, "filename": v} for k, v in unet_dict.items()] + def get_embeddings(): """List loaded and skipped textual-inversion embeddings for the current model.""" db = getattr(shared.sd_model, 'embedding_db', None) if shared.sd_loaded else None @@ -221,6 +225,11 @@ def post_lock_checkpoint(lock:bool=False): modeldata.model_data.locked = lock return {} +def post_refresh_unets(): + """Rescan UNet directories and update the available UNet list.""" + import modules.sd_unet + return modules.sd_unet.refresh_unet_list() + def get_checkpoint(): """Return information about the currently loaded checkpoint including type, class, title, and hash.""" if not shared.sd_loaded or shared.sd_model is None: diff --git a/modules/api/models.py b/modules/api/models.py index f51bd3ad9..f4bf64899 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -146,6 +146,10 @@ class ItemStyle(BaseModel): filename: str | None = Field(title="Filename", description="Path to the styles file") preview: str | None = Field(title="Preview", description="URL to the style preview image") +class ItemUNet(BaseModel): + name: str = Field(title="Name", description="UNet/DiT name") + filename: str | None = Field(title="Filename", description="Path to the UNet/DiT file") + class ItemExtraNetwork(BaseModel): name: str = Field(title="Name", description="Network short name") type: str = Field(title="Type", description="Network type (lora, checkpoint, embedding, etc.)") diff --git a/modules/civitai/metadata_civitai.py b/modules/civitai/metadata_civitai.py index 57c4e082e..e337749dd 100644 --- a/modules/civitai/metadata_civitai.py +++ b/modules/civitai/metadata_civitai.py @@ -219,8 +219,8 @@ def civit_search_metadata(title: str = None, raw: bool = False): import concurrent with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: future_items = {} - for fn in candidates: - future_items[executor.submit(atomic_civit_search_metadata, fn, results)] = fn + for candidate in candidates: + future_items[executor.submit(atomic_civit_search_metadata, candidate, results)] = candidate for future in concurrent.futures.as_completed(future_items): future.result() yield results if raw else create_search_metadata_table(results) diff --git a/modules/lora/lora_apply.py b/modules/lora/lora_apply.py index 62bc4d15d..0cd33e851 100644 --- a/modules/lora/lora_apply.py +++ b/modules/lora/lora_apply.py @@ -4,13 +4,13 @@ import re import time from typing import TYPE_CHECKING import torch -import diffusers.models.lora from modules.lora import lora_common as l from modules import shared, devices, errors, model_quant from modules.logger import log if TYPE_CHECKING: from collections.abc import Callable + import diffusers.models.lora bnb = None diff --git a/modules/options_handler.py b/modules/options_handler.py index 5041eb86c..9fb5c8faf 100644 --- a/modules/options_handler.py +++ b/modules/options_handler.py @@ -93,7 +93,10 @@ class Options: def set(self, key, value): """sets an option and calls its onchange callback, returning True if the option changed and False otherwise""" - oldval = self.data.get(key, None) + if key in self.secrets: + oldval = self.secrets.get(key, None) + else: + oldval = self.data.get(key, None) if oldval is None: if key in self.data_labels: oldval = self.data_labels[key].default diff --git a/modules/sd_checkpoint.py b/modules/sd_checkpoint.py index f6cd40fa3..c861fcf12 100644 --- a/modules/sd_checkpoint.py +++ b/modules/sd_checkpoint.py @@ -24,8 +24,8 @@ warn_once = False class CheckpointInfo: - def __init__(self, filename, sha=None, subfolder=None): - self.name = None + def __init__(self, filename, name=None, sha=None, subfolder=None, model_type: str = 'checkpoint'): + self.name = name self.hash = sha self.filename = filename self.type = '' @@ -62,9 +62,9 @@ class CheckpointInfo: self.sha256 = None self.type = 'unknown' elif os.path.isfile(filename): # ckpt or safetensor - self.name = relname + self.name = self.name or relname self.filename = filename - self.sha256 = hashes.sha256_from_cache(self.filename, f"checkpoint/{relname}") + self.sha256 = hashes.sha256_from_cache(self.filename, f"{model_type}/{relname}") or hashes.sha256_from_cache(self.filename, f"{model_type}/{name}") self.type = ext if 'nf4' in filename: self.type = 'transformer' @@ -74,12 +74,12 @@ class CheckpointInfo: else: repo = [r for r in modelloader.diffuser_repos if self.hash == r['hash']] if len(repo) == 0: - self.name = filename + self.name = self.name or filename self.filename = filename self.sha256 = None self.type = 'unknown' else: - self.name = os.path.join(os.path.basename(shared.opts.diffusers_dir), repo[0]['name']) + self.name = self.name or os.path.join(os.path.basename(shared.opts.diffusers_dir), repo[0]["name"]) self.filename = repo[0]['path'] self.sha256 = repo[0]['hash'] self.type = 'diffusers' @@ -109,7 +109,7 @@ class CheckpointInfo: return self.shorthash def __str__(self): - return f'CheckpointInfo(name="{self.name}" filename="{self.filename}" hash={self.shorthash} type={self.type} title="{self.title}" path="{self.path}" subfolder="{self.subfolder}")' + return f'CheckpointInfo(name="{self.name}" filename="{self.filename}" sha256={self.sha256} sha={self.shorthash} type={self.type} title="{self.title}" path="{self.path}" subfolder="{self.subfolder}")' def setup_model(): @@ -160,7 +160,7 @@ def list_models(): checkpoints_list = dict(sorted(checkpoints_list.items(), key=lambda cp: cp[1].filename)) -def update_model_hashes(): +def update_model_hashes(model_list: dict = None, model_type: str = 'checkpoint'): def update_model_hashes_table(rows): html = """ @@ -186,14 +186,16 @@ def update_model_hashes(): log.error(f'Model list: row={row} {e}') return html.format(tbody=tbody) - lst = [ckpt for ckpt in checkpoints_list.values() if ckpt.hash is None] + if model_list is None: + model_list = checkpoints_list + lst = [ckpt for ckpt in model_list.values() if ckpt.hash is None] for ckpt in lst: ckpt.hash = model_hash(ckpt.filename) - lst = [ckpt for ckpt in checkpoints_list.values() if ckpt.sha256 is None or ckpt.shorthash is None] - log.info(f'Models list: hash missing={len(lst)} total={len(checkpoints_list)}') + lst = [ckpt for ckpt in model_list.values() if ckpt.sha256 is None or ckpt.shorthash is None] + log.info(f'Models list: hash missing={len(lst)} total={len(model_list)}') updated = [] for ckpt in lst: - ckpt.sha256 = hashes.sha256(ckpt.filename, f"checkpoint/{ckpt.name}") + ckpt.sha256 = hashes.sha256(ckpt.filename, f"{model_type}/{ckpt.name}") ckpt.shorthash = ckpt.sha256[0:10] if ckpt.sha256 is not None else None updated.append(ckpt) yield update_model_hashes_table(updated) diff --git a/modules/sd_unet.py b/modules/sd_unet.py index a3ac1c633..4d6ab58b2 100644 --- a/modules/sd_unet.py +++ b/modules/sd_unet.py @@ -102,3 +102,4 @@ def refresh_unet_list(): name = os.path.splitext(basename)[0] if ".safetensors" in basename else basename unet_dict[name] = file log.info(f'Available UNets: path="{shared.opts.unet_dir}" items={len(unet_dict)}') + return unet_dict diff --git a/modules/shared.py b/modules/shared.py index 2bd4cdc53..0a9a20a9a 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -151,6 +151,7 @@ def list_samplers(): modules.sd_samplers.set_samplers() return modules.sd_samplers.all_samplers + log.debug('Initializing: default modes') startup_offload_mode, startup_offload_min_gpu, startup_offload_max_gpu, startup_cross_attention, startup_sdp_options, startup_sdp_choices, startup_sdp_override_options, startup_sdp_override_choices, startup_offload_always, startup_offload_never = get_default_modes(cmd_opts=cmd_opts, mem_stat=mem_stat) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index d80ff9573..17a1c5b7e 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -584,6 +584,8 @@ def register_pages(): register_page(ExtraNetworksPageLora()) from modules.ui_extra_networks_wildcards import ExtraNetworksPageWildcards register_page(ExtraNetworksPageWildcards()) + from modules.ui_extra_networks_unet import ExtraNetworksPageUNets + register_page(ExtraNetworksPageUNets()) if shared.opts.latent_history > 0: from modules.ui_extra_networks_history import ExtraNetworksPageHistory register_page(ExtraNetworksPageHistory()) @@ -596,7 +598,7 @@ def get_pages(title=None): visible = shared.opts.extra_networks pages: list[ExtraNetworksPage] = [] if 'All' in visible or visible == []: # default en sort order - visible = ['Model', 'Lora', 'Style', 'Wildcards', 'Embedding', 'VAE', 'History', 'Hypernetwork'] + visible = ['Model', 'Lora', 'UNet/DiT', 'Style', 'Wildcards', 'Embedding', 'VAE', 'History', 'Hypernetwork'] titles = [page.title for page in shared.extra_networks] if title is None: @@ -743,7 +745,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): with ui.tabs: def ui_tab_change(page): - scan_visible = page in ['Model', 'Lora', 'VAE', 'Hypernetwork', 'Embedding'] + scan_visible = page in ['Model', 'Lora', 'VAE', 'UNet/DiT', 'Hypernetwork', 'Embedding'] save_visible = page in ['Style'] model_visible = page in ['Model'] return [gr.update(visible=scan_visible), gr.update(visible=save_visible), gr.update(visible=model_visible)] diff --git a/modules/ui_extra_networks_unet.py b/modules/ui_extra_networks_unet.py new file mode 100644 index 000000000..7d6e016d6 --- /dev/null +++ b/modules/ui_extra_networks_unet.py @@ -0,0 +1,43 @@ +import html +import json +import os +from modules import shared, ui_extra_networks, sd_unet, hashes, modelstats +from modules.logger import log + + +class ExtraNetworksPageUNets(ui_extra_networks.ExtraNetworksPage): + def __init__(self): + super().__init__('UNet/DiT') + + def refresh(self): + return sd_unet.refresh_unet_list() + + def list_items(self): + for name, filename in sd_unet.unet_dict.items(): + try: + size, mtime = modelstats.stat(filename) + info = self.find_info(filename) + version = self.find_version(None, info) + record = { + "type": 'UNet/DiT', + "name": name, + "alias": os.path.splitext(os.path.basename(filename))[0], + "title": name, + "filename": filename, + "hash": hashes.sha256_from_cache(filename, f"unet/{name}"), + "preview": self.find_preview(filename), + "local_preview": f"{os.path.splitext(filename)[0]}.{shared.opts.samples_format}", + "metadata": {}, + "onclick": '"' + html.escape(f"""return selectUNet({json.dumps(name)})""") + '"', + "mtime": mtime, + "size": size, + "info": info, + "description": self.find_description(filename, info), + "version": version.get("baseModel", "N/A") if info else "N/A", + } + yield record + except Exception as e: + log.debug(f'Networks error: type=vae file="{filename}" {e}') + + def allowed_directories_for_previews(self): + return [v for v in [shared.opts.unet_dir] if v is not None] diff --git a/modules/ui_models.py b/modules/ui_models.py index 2856448e6..2edfd0ade 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -12,6 +12,16 @@ from modules.shared import opts, log extra_ui = [] +def update_model_hashes(): + from modules import sd_unet, sd_checkpoint + unets = {} + for k, v in sd_unet.unet_dict.items(): + unets[k] = sd_checkpoint.CheckpointInfo(name=k, filename=v, model_type='unet') + print('HERE3', unets[k]) + yield from sd_models.update_model_hashes(unets, model_type='unet') + yield from sd_models.update_model_hashes(model_type='checkpoint') + + def create_ui(): log.debug('UI initialize: tab=models') dummy_component = gr.Label(visible=False) @@ -143,7 +153,7 @@ def create_ui(): with gr.Row(): model_table = gr.HTML(value='', elem_id="model_list_table") - model_checkhash_btn.click(fn=sd_models.update_model_hashes, inputs=[], outputs=[model_table]) + model_checkhash_btn.click(fn=update_model_hashes, inputs=[], outputs=[model_table]) model_list_btn.click(fn=lambda: create_models_table(list(sd_models.checkpoints_list.values())), inputs=[], outputs=[model_table]) with gr.Tab(label="Metadata", elem_id="models_metadata_tab"): diff --git a/modules/ui_settings.py b/modules/ui_settings.py index 317186343..80c310e5b 100644 --- a/modules/ui_settings.py +++ b/modules/ui_settings.py @@ -396,6 +396,13 @@ def create_quicksettings(interfaces): inputs=[shared.settings_components['sd_vae'], dummy_component], outputs=[shared.settings_components['sd_vae'], text_settings], ) + button_set_unet = gr.Button("Change UNet", elem_id="change_unet", visible=False) + button_set_unet.click( + fn=lambda value, _: run_settings_single(value, key="sd_unet"), + _js="function(v){ var res = desiredUNetName; desiredUNetName = ''; return [res || v, null]; }", + inputs=[shared.settings_components["sd_unet"], dummy_component], + outputs=[shared.settings_components["sd_unet"], text_settings], + ) def reference_submit(model): if '@' not in model: # diffusers