diff --git a/CHANGELOG.md b/CHANGELOG.md index a56e32e33..168ca2ae4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log for SD.Next +## Update for 2026-02-05 + +Bugfix refresh +- metadata restore to always-on scripts +- wildcard weights parsing, thanks @Tillerz + ## Update for 2026-02-04 ### Highlights for 2026-02-04 diff --git a/cli/create-previews.py b/cli/create-previews.py deleted file mode 100755 index 25f266441..000000000 --- a/cli/create-previews.py +++ /dev/null @@ -1,301 +0,0 @@ -#!/usr/bin/env python -# pylint: disable=no-member -import os -import re -import json -import time -import logging -import importlib -import asyncio -import argparse -from pathlib import Path -from util import Map, log -from sdapi import get, post, close -from generate import generate # pylint: disable=import-error -grid = importlib.import_module('image-grid').grid - - -options = Map({ - # used by extra networks - 'prompt': 'photo of , photograph, posing, pose, high detailed, intricate, elegant, sharp focus, skin texture, looking forward, facing camera, 135mm, shot on dslr, canon 5d, 4k, modelshoot style, cinematic lighting', - # used by models - 'prompts': [ - ('photo citiscape', 'cityscape during night, photorealistic, high detailed, sharp focus, depth of field, 4k'), - ('photo car', 'photo of a sports car, high detailed, sharp focus, dslr, cinematic lighting, realistic'), - ('photo woman', 'portrait photo of beautiful woman, high detailed, dslr, 35mm'), - ('photo naked', 'full body photo of beautiful sexy naked woman, high detailed, dslr, 35mm'), - - ('photo taylor', 'portrait photo of beautiful woman taylor swift, high detailed, sharp focus, depth of field, dslr, 35mm '), - ('photo ti-mia', 'portrait photo of beautiful woman "ti-mia", naked, high detailed, dslr, 35mm'), - ('photo ti-vlado', 'portrait photo of man "ti-vlado", high detailed, dslr, 35mm'), - ('photo lora-vlado', 'portrait photo of man vlado, high detailed, dslr, 35mm '), - - ('wlop', 'a stunning portrait of sexy teen girl in a wet t-shirt, vivid color palette, digital painting, octane render, highly detailed, particles, light effect, volumetric lighting, art by wlop'), - ('greg rutkowski', 'beautiful woman, high detailed, sharp focus, depth of field, 4k, art by greg rutkowski'), - ('carne griffiths', 'beautiful woman taylor swift, high detailed, sharp focus, depth of field, art by carne griffiths '), - ('carne griffiths', 'man vlado, high detailed, sharp focus, depth of field, art by carne griffiths '), - ], - # save format - 'format': '.jpg', - # used by generate script - 'paths': { - "root": "/mnt/c/Users/mandi/OneDrive/Generative/Generate", - "generate": "image", - "upscale": "upscale", - "grid": "grid", - }, - # generate params - 'generate': { - 'detailer': True, - 'prompt': '', - 'negative_prompt': 'foggy, blurry, blurred, duplicate, ugly, mutilated, mutation, mutated, out of frame, bad anatomy, disfigured, deformed, censored, low res, low resolution, watermark, text, poorly drawn face, poorly drawn hands, signature', - 'steps': 20, - 'batch_size': 2, - 'n_iter': 1, - 'seed': -1, - 'sampler_name': 'UniPC', - 'cfg_scale': 6, - 'width': 512, - 'height': 512, - }, - 'lora': { - 'strength': 1.0, - }, -}) - - -def preview_exists(folder, model): - model = os.path.splitext(model)[0] - for suffix in ['', '.preview']: - for ext in ['.jpg', '.png', '.webp']: - fn = os.path.join(folder, f'{model}{suffix}{ext}') - if os.path.exists(fn): - return True - return False - - -async def preview_models(params): - data = await get('/sdapi/v1/sd-models') - allmodels = [m['title'] for m in data] - models = [] - excluded = [] - for m in allmodels: # loop through all registered models - ok = True - for e in params.exclude: # check if model is excluded - if e in m: - excluded.append(m) - ok = False - break - if ok: - short = m.split(' [')[0] - short = short.replace('.ckpt', '').replace('.safetensors', '') - models.append(short) - if len(params.input) > 0: # check if model is included in cmd line - filtered = [] - for m in params.input: - if m in models: - filtered.append(m) - else: - log.error({ 'model not found': m }) - return - models = filtered - log.info({ 'models preview' }) - log.info({ 'models': len(models), 'excluded': len(excluded) }) - opt = await get('/sdapi/v1/options') - log.info({ 'total jobs': len(models) * options.generate.batch_size, 'per-model': options.generate.batch_size }) - log.info(json.dumps(options, indent=2)) - for model in models: - if preview_exists(opt['ckpt_dir'], model) and len(params.input) == 0: # if model preview exists and not manually included - log.info({ 'model preview exists': model }) - continue - fn = os.path.join(opt['ckpt_dir'], os.path.splitext(model)[0] + options.format) - log.info({ 'model load': model }) - - opt['sd_model_checkpoint'] = model - del opt['sd_lora'] - del opt['sd_lyco'] - await post('/sdapi/v1/options', opt) - opt = await get('/sdapi/v1/options') - images = [] - labels = [] - t0 = time.time() - for label, p in options.prompts: - options.generate.prompt = p - log.info({ 'model generating': model, 'label': label, 'prompt': options.generate.prompt }) - data = await generate(options = options, quiet=True) - if 'image' in data: - for img in data['image']: - images.append(img) - labels.append(label) - else: - log.error({ 'model': model, 'error': data }) - t1 = time.time() - if len(images) == 0: - log.error({ 'model': model, 'error': 'no images generated' }) - continue - image = grid(images = images, labels = labels, border = 8) - log.info({ 'saving preview': fn, 'images': len(images), 'size': [image.width, image.height] }) - image.save(fn) - t = t1 - t0 - its = 1.0 * options.generate.steps * len(images) / t - log.info({ 'model preview created': model, 'image': fn, 'images': len(images), 'grid': [image.width, image.height], 'time': round(t, 2), 'its': round(its, 2) }) - - opt = await get('/sdapi/v1/options') - if opt['sd_model_checkpoint'] != params.model: - log.info({ 'model set default': params.model }) - opt['sd_model_checkpoint'] = params.model - del opt['sd_lora'] - del opt['sd_lyco'] - await post('/sdapi/v1/options', opt) - - -async def lora(params): - opt = await get('/sdapi/v1/options') - folder = opt['lora_dir'] - if not os.path.exists(folder): - log.error({ 'lora directory not found': folder }) - return - models1 = list(Path(folder).glob('**/*.safetensors')) - models2 = list(Path(folder).glob('**/*.ckpt')) - models = [os.path.splitext(f)[0] for f in models1 + models2] - log.info({ 'loras': len(models) }) - for model in models: - if preview_exists('', model) and len(params.input) == 0: # if model preview exists and not manually included - log.info({ 'lora preview exists': model }) - continue - fn = model + options.format - model = os.path.basename(model) - images = [] - labels = [] - t0 = time.time() - keywords = re.sub(r'\d', '', model) - keywords = keywords.replace('-v', ' ').replace('-', ' ').strip().split(' ') - keyword = '\"' + '\" \"'.join(keywords) + '\"' - options.generate.prompt = options.prompt.replace('', keyword) - options.generate.prompt = options.generate.prompt.replace('', '') - options.generate.prompt += f' ' - log.info({ 'lora generating': model, 'keyword': keyword, 'prompt': options.generate.prompt }) - data = await generate(options = options, quiet=True) - if 'image' in data: - for img in data['image']: - images.append(img) - labels.append(keyword) - else: - log.error({ 'lora': model, 'keyword': keyword, 'error': data }) - t1 = time.time() - if len(images) == 0: - log.error({ 'model': model, 'error': 'no images generated' }) - continue - image = grid(images = images, labels = labels, border = 8) - log.info({ 'saving preview': fn, 'images': len(images), 'size': [image.width, image.height] }) - image.save(fn) - t = t1 - t0 - its = 1.0 * options.generate.steps * len(images) / t - log.info({ 'lora preview created': model, 'image': fn, 'images': len(images), 'grid': [image.width, image.height], 'time': round(t, 2), 'its': round(its, 2) }) - - -async def lyco(params): - opt = await get('/sdapi/v1/options') - folder = opt['lyco_dir'] - if not os.path.exists(folder): - log.error({ 'lyco directory not found': folder }) - return - models1 = list(Path(folder).glob('**/*.safetensors')) - models2 = list(Path(folder).glob('**/*.ckpt')) - models = [os.path.splitext(f)[0] for f in models1 + models2] - log.info({ 'lycos': len(models) }) - for model in models: - if preview_exists('', model) and len(params.input) == 0: # if model preview exists and not manually included - log.info({ 'lyco preview exists': model }) - continue - fn = model + options.format - model = os.path.basename(model) - images = [] - labels = [] - t0 = time.time() - keywords = re.sub(r'\d', '', model) - keywords = keywords.replace('-v', ' ').replace('-', ' ').strip().split(' ') - keyword = '\"' + '\" \"'.join(keywords) + '\"' - options.generate.prompt = options.prompt.replace('', keyword) - options.generate.prompt = options.generate.prompt.replace('', '') - options.generate.prompt += f' ' - log.info({ 'lyco generating': model, 'keyword': keyword, 'prompt': options.generate.prompt }) - data = await generate(options = options, quiet=True) - if 'image' in data: - for img in data['image']: - images.append(img) - labels.append(keyword) - else: - log.error({ 'lyco': model, 'keyword': keyword, 'error': data }) - t1 = time.time() - if len(images) == 0: - log.error({ 'model': model, 'error': 'no images generated' }) - continue - image = grid(images = images, labels = labels, border = 8) - log.info({ 'saving preview': fn, 'images': len(images), 'size': [image.width, image.height] }) - image.save(fn) - t = t1 - t0 - its = 1.0 * options.generate.steps * len(images) / t - log.info({ 'lyco preview created': model, 'image': fn, 'images': len(images), 'grid': [image.width, image.height], 'time': round(t, 2), 'its': round(its, 2) }) - - -async def embedding(params): - opt = await get('/sdapi/v1/options') - folder = opt['embeddings_dir'] - if not os.path.exists(folder): - log.error({ 'embeddings directory not found': folder }) - return - models = [os.path.splitext(f)[0] for f in Path(folder).glob('**/*.pt')] - log.info({ 'embeddings': len(models) }) - for model in models: - if preview_exists(folder, model) and len(params.input) == 0: # if model preview exists and not manually included - log.info({ 'embedding preview exists': model }) - continue - fn = os.path.join(folder, model + '.preview' + options.format) - images = [] - labels = [] - t0 = time.time() - keyword = '\"' + re.sub(r'\d', '', model) + '\"' - options.generate.batch_size = 4 - options.generate.prompt = options.prompt.replace('', keyword) - options.generate.prompt = options.generate.prompt.replace('', '') - log.info({ 'embedding generating': model, 'keyword': keyword, 'prompt': options.generate.prompt }) - data = await generate(options = options, quiet=True) - if 'image' in data: - for img in data['image']: - images.append(img) - labels.append(keyword) - else: - log.error({ 'embeding': model, 'keyword': keyword, 'error': data }) - t1 = time.time() - if len(images) == 0: - log.error({ 'model': model, 'error': 'no images generated' }) - continue - image = grid(images = images, labels = labels, border = 8) - log.info({ 'saving preview': fn, 'images': len(images), 'size': [image.width, image.height] }) - image.save(fn) - t = t1 - t0 - its = 1.0 * options.generate.steps * len(images) / t - log.info({ 'embeding preview created': model, 'image': fn, 'images': len(images), 'grid': [image.width, image.height], 'time': round(t, 2), 'its': round(its, 2) }) - - -async def create_previews(params): - await preview_models(params) - await lora(params) - await lyco(params) - await embedding(params) - await close() - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description = 'generate model previews') - parser.add_argument('--model', default='best/icbinp-icantbelieveIts-final.safetensors [73f48afbdc]', help="model used to create extra network previews") - parser.add_argument('--exclude', default=['sd-v20', 'sd-v21', 'inpainting', 'pix2pix'], help="exclude models with keywords") - parser.add_argument('--debug', default = False, action='store_true', help = 'print extra debug information') - parser.add_argument('input', type = str, nargs = '*') - args = parser.parse_args() - if args.debug: - log.setLevel(logging.DEBUG) - log.debug({ 'debug': True }) - log.debug({ 'args': args.__dict__ }) - asyncio.run(create_previews(args)) diff --git a/cli/download.py b/cli/download-file.py old mode 100755 new mode 100644 similarity index 100% rename from cli/download.py rename to cli/download-file.py diff --git a/cli/clone.py b/cli/git-clone.py old mode 100755 new mode 100644 similarity index 100% rename from cli/clone.py rename to cli/git-clone.py diff --git a/cli/install-sf.py b/cli/install-stablefast.py old mode 100755 new mode 100644 similarity index 100% rename from cli/install-sf.py rename to cli/install-stablefast.py diff --git a/cli/docs.py b/cli/search-docs.py old mode 100755 new mode 100644 similarity index 90% rename from cli/docs.py rename to cli/search-docs.py index 11200d206..ef6661ba2 --- a/cli/docs.py +++ b/cli/search-docs.py @@ -89,7 +89,7 @@ class Page(): return '' def __str__(self): - return f'Page(title="{self.title.strip()}" fn="{self.fn}" mtime={self.mtime} h1={[h.strip() for h in self.h1]} h2={len(self.h2)} h3={len(self.h3)} lines={len(self.lines)} size={self.size})' + return f'Page(title="{self.title.strip()}" file="{self.fn}" mtime={self.mtime} h1={[h.strip() for h in self.h1]} h2={len(self.h2)} h3={len(self.h3)} lines={len(self.lines)} size={self.size})' class Pages(): @@ -129,16 +129,14 @@ if __name__ == "__main__": sys.argv.pop(0) if len(sys.argv) < 1: log.error("Usage: python cli/docs.py ") - text = ' '.join(sys.argv) - topk = 10 - full = True - log.info(f'Search: "{text}" topk={topk}, full={full}') + term = ' '.join(sys.argv) + log.info(f'Search: "{term}" topk=10, full=True') t0 = time.time() - results = index.search(text, topk=topk, full=full) + results = index.search(term, topk=10, full=True) t1 = time.time() log.info(f'Results: pages={len(results)} size={index.size} time={t1-t0:.3f}') - for score, page in results: - log.info(f'Score: {score:.2f} {page}') + for _score, _page in results: + log.info(f'Score: {_score:.2f} {_page}') # if len(results) > 0: # log.info('Top result:') # log.info(results[0][1].get()) diff --git a/modules/scripts_manager.py b/modules/scripts_manager.py index 1fbe6e630..3ded208db 100644 --- a/modules/scripts_manager.py +++ b/modules/scripts_manager.py @@ -336,6 +336,7 @@ class ScriptRunner: self.alwayson_scripts = [] self.auto_processing_scripts = [] self.titles = [] + self.alwayson_titles = [] self.infotext_fields = [] self.paste_field_names = [] self.script_load_ctr = 0 @@ -376,6 +377,7 @@ class ScriptRunner: self.selectable_scripts.clear() self.alwayson_scripts.clear() self.titles.clear() + self.alwayson_titles.clear() self.infotext_fields.clear() self.paste_field_names.clear() self.script_load_ctr = 0 @@ -405,6 +407,7 @@ class ScriptRunner: def setup_ui(self, parent='unknown', accordion=True): import modules.api.models as api_models self.titles = [wrap_call(script.title, script.filename, "title") or f"{script.filename} [error]" for script in self.selectable_scripts] + self.alwayson_titles = [wrap_call(script.title, script.filename, "title") or f"{script.filename} [error]" for script in self.alwayson_scripts] inputs = [] inputs_alwayson = [True] @@ -501,7 +504,7 @@ class ScriptRunner: if title == 'None': # called when an initial value is set from ui-config.json to show script's UI components return if title not in self.titles: - errors.log.error(f'Script not found: {title}') + errors.log.error(f'Script: title="{title}" op=init not found') return script_index = self.titles.index(title) self.selectable_scripts[script_index].group.visible = True @@ -511,12 +514,18 @@ class ScriptRunner: def onload_script_visibility(params): title = params.get('Script', None) - if title: + if title and title in self.titles: title_index = self.titles.index(title) visibility = title_index == self.script_load_ctr self.script_load_ctr = (self.script_load_ctr + 1) % len(self.titles) return gr.update(visible=visibility) + elif title and title in self.alwayson_titles: + title_index = self.alwayson_titles.index(title) + visibility = title_index == self.script_load_ctr + self.script_load_ctr = (self.script_load_ctr + 1) % len(self.titles) + return gr.update(visible=visibility) else: + errors.log.warning(f'Script: title="{title}" op=visibility not found') return gr.update(visible=False) self.infotext_fields.append((dropdown, lambda x: gr.update(value=x.get('Script', 'None')))) @@ -526,9 +535,11 @@ class ScriptRunner: def run(self, p, *args): s = ScriptSummary('run') script_index = args[0] if len(args) > 0 else 0 - if script_index == 0: + if (script_index is None) or (script_index == 0): return None - script = self.selectable_scripts[script_index-1] + script = self.selectable_scripts[script_index - 1] + if script is None: + script = self.alwayson_scripts[script_index - 1] if script is None: return None if 'upscale' in script.title(): @@ -549,9 +560,9 @@ class ScriptRunner: def after(self, p, processed, *args): s = ScriptSummary('after') script_index = args[0] if len(args) > 0 else 0 - if script_index == 0: + if (script_index is None) or (script_index == 0): return processed - script = self.selectable_scripts[script_index-1] + script = self.selectable_scripts[script_index - 1] if script is None or not hasattr(script, 'after'): return processed parsed = [] diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py index 1bd51d232..a518657b0 100644 --- a/scripts/xyz_grid.py +++ b/scripts/xyz_grid.py @@ -25,7 +25,7 @@ class Script(scripts_manager.Script): current_axis_options = [] def title(self): - return "XYZ Grid" + return "XYZ Grid Script" def ui(self, is_img2img): self.current_axis_options = [x for x in axis_options if type(x) == AxisOption or x.is_img2img == is_img2img] @@ -135,14 +135,14 @@ class Script(scripts_manager.Script): return gr.update(value = valslist) self.infotext_fields = ( - (x_type, "X Type"), - (x_values, "X Values"), + (x_type, "X Script Type"), + (x_values, "X Script Values"), (x_values_dropdown, lambda params:get_dropdown_update_from_params("X",params)), - (y_type, "Y Type"), - (y_values, "Y Values"), + (y_type, "Y Script Type"), + (y_values, "Y Script Values"), (y_values_dropdown, lambda params:get_dropdown_update_from_params("Y",params)), - (z_type, "Z Type"), - (z_values, "Z Values"), + (z_type, "Z Script Type"), + (z_values, "Z Script Values"), (z_values_dropdown, lambda params:get_dropdown_update_from_params("Z",params)), ) @@ -334,21 +334,21 @@ class Script(scripts_manager.Script): pc.extra_generation_params = copy(pc.extra_generation_params) pc.extra_generation_params['Script'] = self.title() if x_opt.label != 'Nothing': - pc.extra_generation_params["X Type"] = x_opt.label - pc.extra_generation_params["X Values"] = x_values + pc.extra_generation_params["X Script Type"] = x_opt.label + pc.extra_generation_params["X Script Values"] = x_values if x_opt.label in ["[Param] Seed", "[Param] Variation seed"] and not no_fixed_seeds: - pc.extra_generation_params["Fixed X Values"] = ", ".join([str(x) for x in xs]) + pc.extra_generation_params["Fixed X Script Values"] = ", ".join([str(x) for x in xs]) if y_opt.label != 'Nothing': - pc.extra_generation_params["Y Type"] = y_opt.label - pc.extra_generation_params["Y Values"] = y_values + pc.extra_generation_params["Y Script Type"] = y_opt.label + pc.extra_generation_params["Y Script Values"] = y_values if y_opt.label in ["[Param] Seed", "[Param] Variation seed"] and not no_fixed_seeds: - pc.extra_generation_params["Fixed Y Values"] = ", ".join([str(y) for y in ys]) + pc.extra_generation_params["Fixed Y Script Values"] = ", ".join([str(y) for y in ys]) grid_infotext[subgrid_index] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds, grid=f'{len(xs)}x{len(ys)}') if grid_infotext[0] is None and ix == 0 and iy == 0 and iz == 0: # Sets main grid infotext pc.extra_generation_params = copy(pc.extra_generation_params) if z_opt.label != 'Nothing': - pc.extra_generation_params["Z Type"] = z_opt.label - pc.extra_generation_params["Z Values"] = z_values + pc.extra_generation_params["Z Script Type"] = z_opt.label + pc.extra_generation_params["Z Script Values"] = z_values if z_opt.label in ["[Param] Seed", "[Param] Variation seed"] and not no_fixed_seeds: pc.extra_generation_params["Fixed Z Values"] = ", ".join([str(z) for z in zs]) grid_text = f'{len(zs)}x{len(xs)}x{len(ys)}' if len(zs) > 0 else f'{len(xs)}x{len(ys)}' diff --git a/scripts/xyz_grid_on.py b/scripts/xyz_grid_on.py index 99bc89fa3..6e4c245f2 100644 --- a/scripts/xyz_grid_on.py +++ b/scripts/xyz_grid_on.py @@ -141,6 +141,7 @@ class Script(scripts_manager.Script): return gr.update(value = valslist) self.infotext_fields = ( + (enabled, "XYZ Grid Enabled"), (x_type, "X Type"), (x_values, "X Values"), (x_values_dropdown, lambda params:get_dropdown_update_from_params("X",params)), @@ -357,6 +358,7 @@ class Script(scripts_manager.Script): if ix == 0 and iy == 0: # create subgrid info text pc.extra_generation_params = copy(pc.extra_generation_params) pc.extra_generation_params['Script'] = self.title() + pc.extra_generation_params['XYZ Grid Enabled'] = enabled if x_opt.label != 'Nothing': pc.extra_generation_params["X Type"] = x_opt.label pc.extra_generation_params["X Values"] = x_values