mirror of https://github.com/vladmandic/automatic
parent
9a5e908ddf
commit
afe3786f5f
11
CHANGELOG.md
11
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!
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.)")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue