add unet/dir to networks

Signed-off-by: vladmandic <mandic00@live.com>
pull/4699/head
vladmandic 2026-03-22 11:07:09 +01:00
parent 9a5e908ddf
commit afe3786f5f
15 changed files with 119 additions and 24 deletions

View File

@ -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!

View File

@ -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;

View File

@ -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"])

View File

@ -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:

View File

@ -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.)")

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 = """
<table class="simple-table">
@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)]

View File

@ -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]

View File

@ -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"):

View File

@ -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