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 = """