From 7d5119a5d7d3862ef395f894a47963cb344000a9 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sun, 24 Sep 2023 17:52:21 -0400 Subject: [PATCH] add model metadata --- CHANGELOG.md | 17 +++- javascript/extraNetworks.js | 31 +++---- javascript/style.css | 2 +- modules/cmd_args.py | 86 +++++++++--------- modules/modelloader.py | 46 +++++++--- modules/sd_models.py | 5 +- modules/shared.py | 23 +++-- modules/textual_inversion/autocrop.py | 4 +- modules/ui_extra_networks.py | 126 +++++++++++++++----------- modules/ui_models.py | 73 ++++++++------- modules/ui_symbols.py | 2 +- 11 files changed, 244 insertions(+), 171 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bae797537..f44e266b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Update for 2023-09-20 +This is a big one, with some major changes and new functionality... + +Note that for this release its recommended to perform a clean install (fresh `git clone`) +Upgrades are still possible and supported, but above is recommended for best experience + - **UI** - added **change log** to UI see *System -> Changelog* @@ -11,7 +16,8 @@ - **Extra networks**: - new details interface to view and save data about extra networks main ui now has a single button on each en to trigger details view - details view includes model/lora metadata parser! + - details view includes model/lora metadata parser! + - details view includes civitai model metadata! - faster search, ability to show/hide/sort networks - refactored subfolder handling *note*: this will trigger model hash recaclulation on first model use @@ -26,8 +32,17 @@ - moved ui options to submenu - default list for new installs is now all samplers, list can be modified in settings - simplified samplers configuration in settings +- **CivitAI**: + - CivitAI integration in *Models -> CivitAI* can now find most + previews AND metadata for most models (checkpoints, loras, embeddings) + metadata is now parsed and saved in *[model].json* + - Description from parsed model metadata is used as model description if there is no manual + description file present in format of *[model].txt* - **Diffusers** - better pipeline auto-detect when loading from safetensors +- **Startup** + - All main CLI parameters can now be set as environment variable as well + for example `--data-dir ` can be specified as `SD_DATADIR=` before starting SD.Next - **Logging** - get browser session info in server log - when running with `--debug` flag, log is force-rotated diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 44e397b84..fc4749c80 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -27,7 +27,7 @@ const setENState = (state) => { if (!state) return; state.tab = getENActiveTab(); state.page = getENActivePage(); - log('setENState', state); + // log('setENState', state); const el = gradioApp().querySelector(`#${state.tab}_extra_state > label > textarea`); el.value = JSON.stringify(state); updateInput(el); @@ -36,9 +36,7 @@ const setENState = (state) => { // methods function showCardDetails(event) { - console.log('HERE1', event); const tabname = getENActiveTab(); - // setENState({ op: 'showCardDetails' }); const btn = gradioApp().getElementById(`${tabname}_extra_details_btn`); btn.click(); event.stopPropagation(); @@ -46,11 +44,9 @@ function showCardDetails(event) { } function getCardDetails(...args) { - console.log('HERE2', event); const el = event?.target?.parentElement?.parentElement; - if (!el?.classList?.contains('card')) return [...args]; - const tabname = getENActiveTab(); - setENState({ op: 'getCardDetails', item: el.dataset.name }); + if (el?.classList?.contains('card')) setENState({ op: 'getCardDetails', item: el.dataset.name }); + else setENState({ op: 'getCardDetails', item: null }); return [...args]; } @@ -65,9 +61,9 @@ function readCardTags(el, tags) { }; if (tags.length === 0) return; const cardTags = tags.split('|'); - if (cardTags.length === 0) return; + if (!cardTags || cardTags.length === 0) return; const tagsEl = el.getElementsByClassName('tags')[0]; - if (tagsEl.children.length > 0) return; + if (!tagsEl?.children || tagsEl.children.length > 0) return; for (const tag of cardTags) { const span = document.createElement('span'); span.classList.add('tag'); @@ -77,12 +73,15 @@ function readCardTags(el, tags) { } } -function readCardDescription(filename, descript) { - const tabname = getENActiveTab(); - const description = gradioApp().querySelector(`#${tabname}_description > label > textarea`); - description.value = descript?.trim() || ''; - description.focus(); - updateInput(description); +function readCardDescription(page, item) { + requestGet('/sd_extra_networks/description', { page, item }, (data) => { + const tabname = getENActiveTab(); + const description = gradioApp().querySelector(`#${tabname}_description > label > textarea`); + description.value = data?.description?.trim() || ''; + // description.focus(); + updateInput(description); + setENState({ op: 'readCardDescription', page, item }); + }); } async function filterExtraNetworksForTab(tabname, searchTerm) { @@ -216,7 +215,7 @@ function setupExtraNetworksForTab(tabname) { if (!el || (el.title === previousCard)) return; if (!hoverTimer) { hoverTimer = setTimeout(() => { - readCardDescription(el.dataset.filename, el.dataset.description); + readCardDescription(el.dataset.page, el.dataset.name); readCardTags(el, el.dataset.tags); previousCard = el.title; }, 300); diff --git a/javascript/style.css b/javascript/style.css index 68b2c9ed1..0e3cd4f62 100644 --- a/javascript/style.css +++ b/javascript/style.css @@ -150,7 +150,7 @@ div#extras_scale_to_tab div.form{ flex-direction: row; } .progressDiv{ position: relative; height: 20px; background: #b4c0cc; margin-bottom: -3px; } .dark .progressDiv{ background: #424c5b; } .progressDiv .progress{ width: 0%; height: 20px; background: #0060df; color: white; font-weight: bold; line-height: 20px; padding: 0 8px 0 0; text-align: right; overflow: visible; white-space: nowrap; padding: 0 0.5em; } -.livePreview { position: absolute; z-index: 300; background-color: transparent; width: -moz-available; width: -webkit-fill-available; } +.livePreview { position: absolute; z-index: 50; background-color: transparent; width: -moz-available; width: -webkit-fill-available; } .livePreview img { position: absolute; object-fit: contain; width: 100%; height: 100%; } .dark .livePreview { background-color: rgb(17 24 39 / var(--tw-bg-opacity)); } .popup-metadata { color: white; background: #0000; display: inline-block; white-space: pre-wrap; font-size: 0.75em; } diff --git a/modules/cmd_args.py b/modules/cmd_args.py index 23e13c08b..4f3a37ac4 100644 --- a/modules/cmd_args.py +++ b/modules/cmd_args.py @@ -1,5 +1,5 @@ -import argparse import os +import argparse from modules.paths_internal import data_path parser = argparse.ArgumentParser(description="SD.Next", conflict_handler='resolve', epilog='For other options see UI Settings page', prog='', add_help=True, formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=55, indent_increment=2, width=200)) @@ -7,47 +7,47 @@ parser._optionals = parser.add_argument_group('Other options') # pylint: disable group = parser.add_argument_group('Server options') # main server args -group.add_argument("--config", type=str, default=os.path.join(data_path, 'config.json'), help="Use specific server configuration file, default: %(default)s") -group.add_argument("--ui-config", type=str, default=os.path.join(data_path, 'ui-config.json'), help="Use specific UI configuration file, default: %(default)s") -group.add_argument("--medvram", action='store_true', help="Split model stages and keep only active part in VRAM, default: %(default)s") -group.add_argument("--lowvram", action='store_true', help="Split model components and keep only active part in VRAM, default: %(default)s") -group.add_argument("--ckpt", type=str, default=None, help="Path to model checkpoint to load immediately, default: %(default)s") -group.add_argument('--vae', type=str, default=None, help='Path to VAE checkpoint to load immediately, default: %(default)s') -group.add_argument("--data-dir", type=str, default="", help="Base path where all user data is stored, default: %(default)s") -group.add_argument("--models-dir", type=str, default="models", help="Base path where all models are stored, default: %(default)s",) -group.add_argument("--allow-code", action='store_true', help="Allow custom script execution, default: %(default)s") -group.add_argument("--share", action='store_true', help="Enable UI accessible through Gradio site, default: %(default)s") -group.add_argument("--insecure", action='store_true', help="Enable extensions tab regardless of other options, default: %(default)s") +group.add_argument("--config", type=str, default=os.environ.get("SD_CONFIG", os.path.join(data_path, 'config.json')), help="Use specific server configuration file, default: %(default)s") +group.add_argument("--ui-config", type=str, default=os.environ.get("SD_UI-CONFIG", os.path.join(data_path, 'ui-config.json')), help="Use specific UI configuration file, default: %(default)s") +group.add_argument("--medvram", default=os.environ.get("SD_MEDVRAM", False), action='store_true', help="Split model stages and keep only active part in VRAM, default: %(default)s") +group.add_argument("--lowvram", default=os.environ.get("SD_LOWVRAM", False), action='store_true', help="Split model components and keep only active part in VRAM, default: %(default)s") +group.add_argument("--ckpt", type=str, default=os.environ.get("SD_MODEL", None), help="Path to model checkpoint to load immediately, default: %(default)s") +group.add_argument('--vae', type=str, default=os.environ.get("SD_VAE", None), help='Path to VAE checkpoint to load immediately, default: %(default)s') +group.add_argument("--data-dir", type=str, default=os.environ.get("SD_DATADIR", ''), help="Base path where all user data is stored, default: %(default)s") +group.add_argument("--models-dir", type=str, default=os.environ.get("SD_MODELDIR", 'models'), help="Base path where all models are stored, default: %(default)s",) +group.add_argument("--allow-code", default=os.environ.get("SD_ALLOW_CODE", False), action='store_true', help="Allow custom script execution, default: %(default)s") +group.add_argument("--share", default=os.environ.get("SD_SHARE", False), action='store_true', help="Enable UI accessible through Gradio site, default: %(default)s") +group.add_argument("--insecure", default=os.environ.get("SD_INSECURE", False), action='store_true', help="Enable extensions tab regardless of other options, default: %(default)s") group.add_argument("--use-cpu", nargs='+', default=[], type=str.lower, help="Force use CPU for specified modules, default: %(default)s") -group.add_argument("--listen", action='store_true', help="Launch web server using public IP address, default: %(default)s") -group.add_argument("--port", type=int, default=7860, help="Launch web server with given server port, default: %(default)s") -group.add_argument("--freeze", action='store_true', help="Disable editing settings", default=False) -group.add_argument("--auth", type=str, help='Set access authentication like "user:pwd,user:pwd""', default=None) -group.add_argument("--auth-file", type=str, help='Set access authentication using file, default: %(default)s', default=None) -group.add_argument("--autolaunch", action='store_true', help="Open the UI URL in the system's default browser upon launch", default=False) -group.add_argument('--docs', default = False, action='store_true', help = "Mount Gradio docs at /docs, default: %(default)s") -group.add_argument('--api-only', default = False, action='store_true', help = "Run in API only mode without starting UI") -group.add_argument("--api-log", default=False, action='store_true', help="Enable logging of all API requests, default: %(default)s") -group.add_argument("--device-id", type=str, help="Select the default CUDA device to use, default: %(default)s", default=None) -group.add_argument("--cors-origins", type=str, help="Allowed CORS origins as comma-separated list, default: %(default)s", default=None) -group.add_argument("--cors-regex", type=str, help="Allowed CORS origins as regular expression, default: %(default)s", default=None) -group.add_argument("--tls-keyfile", type=str, help="Enable TLS and specify key file, default: %(default)s", default=None) -group.add_argument("--tls-certfile", type=str, help="Enable TLS and specify cert file, default: %(default)s", default=None) -group.add_argument("--tls-selfsign", action="store_true", help="Enable TLS with self-signed certificates, default: %(default)s", default=None) -group.add_argument("--server-name", type=str, help="Sets hostname of server, default: %(default)s", default=None) -group.add_argument("--no-hashing", action='store_true', help="Disable hashing of checkpoints, default: %(default)s", default=False) -group.add_argument("--no-metadata", action='store_true', help="Disable reading of metadata from models, default: %(default)s", default=False) -group.add_argument("--no-download", action='store_true', help="Disable download of default model, default: %(default)s", default=False) -group.add_argument("--profile", action='store_true', help="Run profiler, default: %(default)s") -group.add_argument("--disable-queue", action='store_true', help="Disable queues, default: %(default)s") -group.add_argument('--debug', default = False, action='store_true', help = "Run installer with debug logging, default: %(default)s") -group.add_argument('--use-directml', default = False, action='store_true', help = "Use DirectML if no compatible GPU is detected, default: %(default)s") -group.add_argument("--use-openvino", default = False, action='store_true', help="Use Intel OpenVINO backend, default: %(default)s") -group.add_argument("--use-ipex", default = False, action='store_true', help="Force use Intel OneAPI XPU backend, default: %(default)s") -group.add_argument("--use-cuda", default=False, action='store_true', help="Force use nVidia CUDA backend, default: %(default)s") -group.add_argument("--use-rocm", default=False, action='store_true', help="Force use AMD ROCm backend, default: %(default)s") -group.add_argument('--subpath', type=str, help='Customize the URL subpath for usage with reverse proxy') -group.add_argument('--backend', type=str, choices=['original', 'diffusers'], required=False, help='force model pipeline type') +group.add_argument("--listen", default=os.environ.get("SD_LISTEN", False), action='store_true', help="Launch web server using public IP address, default: %(default)s") +group.add_argument("--port", type=int, default=os.environ.get("SD_PORT", 7860), help="Launch web server with given server port, default: %(default)s") +group.add_argument("--freeze", default=os.environ.get("SD_FREEZE", False), action='store_true', help="Disable editing settings") +group.add_argument("--auth", type=str, default=os.environ.get("SD_AUTH", None), help='Set access authentication like "user:pwd,user:pwd""') +group.add_argument("--auth-file", type=str, default=os.environ.get("SD_AUTHFILE", None), help='Set access authentication using file, default: %(default)s') +group.add_argument("--autolaunch", default=os.environ.get("SD_AUTOLAUNCH", False), action='store_true', help="Open the UI URL in the system's default browser upon launch") +group.add_argument('--docs', default=os.environ.get("SD_DOCS", False), action='store_true', help = "Mount Gradio docs at /docs, default: %(default)s") +group.add_argument('--api-only', default=os.environ.get("SD_APIONLY", False), action='store_true', help = "Run in API only mode without starting UI") +group.add_argument("--api-log", default=os.environ.get("SD_APILOG", False), action='store_true', help="Enable logging of all API requests, default: %(default)s") +group.add_argument("--device-id", type=str, default=os.environ.get("SD_DEVICEID", None), help="Select the default CUDA device to use, default: %(default)s") +group.add_argument("--cors-origins", type=str, default=os.environ.get("SD_CORSORIGINS", None), help="Allowed CORS origins as comma-separated list, default: %(default)s") +group.add_argument("--cors-regex", type=str, default=os.environ.get("SD_CORSREGEX", None), help="Allowed CORS origins as regular expression, default: %(default)s") +group.add_argument("--tls-keyfile", type=str, default=os.environ.get("SD_TLSKEYFILE", None), help="Enable TLS and specify key file, default: %(default)s") +group.add_argument("--tls-certfile", type=str, default=os.environ.get("SD_TLSCERTFILE", None), help="Enable TLS and specify cert file, default: %(default)s") +group.add_argument("--tls-selfsign", action="store_true", default=os.environ.get("SD_TLSSELFSIGN", False), help="Enable TLS with self-signed certificates, default: %(default)s") +group.add_argument("--server-name", type=str, default=os.environ.get("SD_SERVERNAME", None), help="Sets hostname of server, default: %(default)s") +group.add_argument("--no-hashing", default=os.environ.get("SD_NOHASHING", False), action='store_true', help="Disable hashing of checkpoints, default: %(default)s") +group.add_argument("--no-metadata", default=os.environ.get("SD_NOMETADATA", False), action='store_true', help="Disable reading of metadata from models, default: %(default)s") +group.add_argument("--no-download", default=os.environ.get("SD_DOWNLOAD", False), action='store_true', help="Disable download of default model, default: %(default)s") +group.add_argument("--profile", default=os.environ.get("SD_PROFILE", False), action='store_true', help="Run profiler, default: %(default)s") +group.add_argument("--disable-queue", default=os.environ.get("SD_DISABLEQUEUE", False), action='store_true', help="Disable queues, default: %(default)s") +group.add_argument('--debug', default=os.environ.get("SD_DEBUG", False), action='store_true', help = "Run installer with debug logging, default: %(default)s") +group.add_argument('--use-directml', default=os.environ.get("SD_USEDIRECTML", False), action='store_true', help = "Use DirectML if no compatible GPU is detected, default: %(default)s") +group.add_argument("--use-openvino", default=os.environ.get("SD_USEOPENVINO", False), action='store_true', help="Use Intel OpenVINO backend, default: %(default)s") +group.add_argument("--use-ipex", default=os.environ.get("SD_USEIPX", False), action='store_true', help="Force use Intel OneAPI XPU backend, default: %(default)s") +group.add_argument("--use-cuda", default=os.environ.get("SD_USECUDA", False), action='store_true', help="Force use nVidia CUDA backend, default: %(default)s") +group.add_argument("--use-rocm", default=os.environ.get("SD_USEROCM", False), action='store_true', help="Force use AMD ROCm backend, default: %(default)s") +group.add_argument('--subpath', type=str, default=os.environ.get("SD_SUBPATH", None), help='Customize the URL subpath for usage with reverse proxy') +group.add_argument('--backend', type=str, default=os.environ.get("SD_BACKEND", None), choices=['original', 'diffusers'], required=False, help='force model pipeline type') # removed args are added here as hidden in fixed format for compatbility reasons @@ -59,7 +59,7 @@ group.add_argument("--theme", type=str, help=argparse.SUPPRESS, default=None) group.add_argument("--disable-console-progressbars", action='store_true', help=argparse.SUPPRESS, default=True) group.add_argument("--disable-safe-unpickle", action='store_true', help=argparse.SUPPRESS, default=True) group.add_argument("--lowram", action='store_true', help=argparse.SUPPRESS) -group.add_argument("--disable-extension-access", default = False, action='store_true', help=argparse.SUPPRESS) +group.add_argument("--disable-extension-access", default=False, action='store_true', help=argparse.SUPPRESS) group.add_argument("--api", help=argparse.SUPPRESS, default=True) group.add_argument("--api-auth", type=str, help=argparse.SUPPRESS, default=None) @@ -81,7 +81,7 @@ def compatibility_args(opts, args): group.add_argument("--ldsr-models-path", help=argparse.SUPPRESS, default=opts.ldsr_models_path) group.add_argument("--clip-models-path", type=str, help=argparse.SUPPRESS, default=opts.clip_models_path) group.add_argument("--opt-channelslast", help=argparse.SUPPRESS, default=opts.opt_channelslast) - group.add_argument("--xformers", default = (opts.cross_attention_optimization == "xFormers"), action='store_true', help=argparse.SUPPRESS) + group.add_argument("--xformers", default=(opts.cross_attention_optimization == "xFormers"), action='store_true', help=argparse.SUPPRESS) group.add_argument("--disable-nan-check", help=argparse.SUPPRESS, default=opts.disable_nan_check) group.add_argument("--rollback-vae", help=argparse.SUPPRESS, default=opts.rollback_vae) group.add_argument("--no-half", help=argparse.SUPPRESS, default=opts.no_half) diff --git a/modules/modelloader.py b/modules/modelloader.py index 85fbfb7b2..3c4b93934 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -5,6 +5,7 @@ import importlib from typing import Dict from urllib.parse import urlparse import PIL.Image as Image +import rich.progress as p from modules import shared from modules.upscaler import Upscaler, UpscalerLanczos, UpscalerNearest, UpscalerNone from modules.paths import script_path, models_path @@ -55,15 +56,32 @@ def walk(top, onerror:callable=None): yield top, nondirs +def download_civit_meta(model_path: str, model_id): + fn = os.path.splitext(model_path)[0] + '.json' + if os.path.exists(fn): + return '' + url = f'https://civitai.com/api/v1/models/{model_id}' + r = shared.req(url) + if r.status_code == 200: + try: + shared.writefile(r.json(), fn, silent=True) + msg = f'CivitAI download: id={model_id} url={url} file={fn}' + shared.log.info(msg) + return msg + except Exception as e: + msg = f'CivitAI download error: id={model_id} url={url} file={fn} {e}' + shared.log.error(msg) + return msg + return '' + def download_civit_preview(model_path: str, preview_url: str): - import requests - import rich.progress as p - _, ext = os.path.splitext(preview_url) - model_name, _ = os.path.splitext(os.path.basename(model_path)) - preview_file = f'{os.path.splitext(model_path)[0]}{ext}' if '.safetensors' in model_path.lower() else f'{model_path}{ext}' - res = f'CivitAI download: name={model_name} url={preview_url}' - req = requests.get(preview_url, stream=True, timeout=30) - total_size = int(req.headers.get('content-length', 0)) + ext = os.path.splitext(preview_url)[1] + preview_file = os.path.splitext(model_path)[0] + ext + if os.path.exists(preview_file): + return '' + res = f'CivitAI download: url={preview_url} file={preview_file}' + r = shared.req(preview_url, stream=True) + total_size = int(r.headers.get('content-length', 0)) block_size = 16384 # 16KB blocks written = 0 img = None @@ -72,7 +90,7 @@ def download_civit_preview(model_path: str, preview_url: str): with open(preview_file, 'wb') as f: with p.Progress(p.TextColumn('[cyan]{task.description}'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), console=shared.console) as progress: task = progress.add_task(description="Download starting", total=total_size) - for data in req.iter_content(block_size): + for data in r.iter_content(block_size): written = written + len(data) f.write(data) progress.update(task, advance=block_size, description="Downloading") @@ -81,7 +99,7 @@ def download_civit_preview(model_path: str, preview_url: str): raise ValueError(f'removed invalid download: bytes={written}') img = Image.open(preview_file) except Exception as e: - shared.log.error(f'CivitAI download error: name={model_name} url={preview_url} {e}') + shared.log.error(f'CivitAI download error: url={preview_url} file={preview_file} {e}') shared.state.end() if img is None: return res @@ -100,11 +118,9 @@ def download_civit_model(model_url: str, model_name: str, model_path: str, model res += ' already exists' shared.log.warning(res) return res - import requests - import rich.progress as p - req = requests.get(model_url, stream=True, timeout=30) - total_size = int(req.headers.get('content-length', 0)) + r = shared.req(model_url, stream=True) + total_size = int(r.headers.get('content-length', 0)) block_size = 16384 # 16KB blocks written = 0 shared.state.begin('civitai-download-model') @@ -113,7 +129,7 @@ def download_civit_model(model_url: str, model_name: str, model_path: str, model with p.Progress(p.TextColumn('[cyan]{task.description}'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), console=shared.console) as progress: task = progress.add_task(description="Download starting", total=total_size) # for data in tqdm(req.iter_content(block_size), total=total_size//1024, unit='KB', unit_scale=False): - for data in req.iter_content(block_size): + for data in r.iter_content(block_size): written = written + len(data) f.write(data) progress.update(task, advance=block_size, description="Downloading") diff --git a/modules/sd_models.py b/modules/sd_models.py index 3f82cbb86..1c1e51e48 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -60,7 +60,7 @@ class CheckpointInfo: if os.path.isfile(filename): # ckpt or safetensor self.name = relname self.filename = filename - self.sha256 = hashes.sha256_from_cache(self.filename, f"checkpoint/{os.path.basename(relname)}.{ext}") + self.sha256 = hashes.sha256_from_cache(self.filename, f"checkpoint/{relname}") self.type = ext # self.model_name = os.path.splitext(name.replace("/", "_").replace("\\", "_"))[0] else: # maybe a diffuser @@ -76,6 +76,9 @@ class CheckpointInfo: self.sha256 = repo[0]['hash'] self.type = 'diffusers' + # info = shared.readfile(self.filename, silent=True) + # if 'tags' in info: + # self.tags = info['tags'] self.shorthash = self.sha256[0:10] if self.sha256 else None self.title = self.name if self.shorthash is None else f'{self.name} [{self.shorthash}]' self.path = self.filename diff --git a/modules/shared.py b/modules/shared.py index 42f5162e3..6ed5b9a95 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -6,8 +6,10 @@ import json import datetime import contextlib import urllib.request +from types import SimpleNamespace from urllib.parse import urlparse from enum import Enum +import requests import gradio as gr import fasteners from rich.console import Console @@ -304,11 +306,10 @@ def list_themes(): def refresh_themes(): - import requests try: - req = requests.get('https://huggingface.co/datasets/freddyaboulton/gradio-theme-subdomains/resolve/main/subdomains.json', timeout=5) - if req.status_code == 200: - res = req.json() + r = req('https://huggingface.co/datasets/freddyaboulton/gradio-theme-subdomains/resolve/main/subdomains.json') + if r.status_code == 200: + res = r.json() fn = os.path.join('html', 'themes.json') writefile(res, fn) list_themes() @@ -853,8 +854,8 @@ def reload_gradio_theme(theme_name=None): default_font_params = {} res = 0 try: - req = urllib.request.Request("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono", method="HEAD") - res = urllib.request.urlopen(req, timeout=3.0).status # pylint: disable=consider-using-with + request = urllib.request.Request("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono", method="HEAD") + res = urllib.request.urlopen(request, timeout=3.0).status # pylint: disable=consider-using-with except Exception: res = 0 if res != 200: @@ -985,6 +986,16 @@ def get_version(): return version +def req(url_addr, **kwargs): + headers = { 'Content-type': 'application/json' } + try: + res = requests.get(url_addr, timeout=30, headers=headers, verify=False, allow_redirects=True, **kwargs) + except Exception as e: + log.error(f'HTTP request error: url={url} {e}') + res = { 'status_code': 500, 'text': f'HTTP request error: url={url} {e}' } + res = SimpleNamespace(**res) + return res + class Shared(sys.modules[__name__].__class__): # this class is here to provide sd_model field as a property, so that it can be created and loaded on demand rather than at program startup. @property def sd_model(self): diff --git a/modules/textual_inversion/autocrop.py b/modules/textual_inversion/autocrop.py index bac4e618e..2ccd4a27d 100644 --- a/modules/textual_inversion/autocrop.py +++ b/modules/textual_inversion/autocrop.py @@ -192,7 +192,7 @@ def image_face_points(im, settings): return [] -def image_corner_points(im, settings): +def image_corner_points(im, settings): # pylint: disable=unused-argument grayscale = im.convert("L") # naive attempt at preventing focal points from collecting at watermarks near the bottom @@ -266,7 +266,7 @@ def centroid(pois): return PointOfInterest(sum(x)/len(pois), sum(y)/len(pois)) -def poi_average(pois, settings): +def poi_average(pois, settings): # pylint: disable=unused-argument weight = 0.0 x = 0.0 y = 0.0 diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 37f4ac1be..b0838a068 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -8,6 +8,7 @@ import threading from datetime import datetime from types import SimpleNamespace from pathlib import Path +from html.parser import HTMLParser from collections import OrderedDict import gradio as gr from PIL import Image @@ -44,6 +45,7 @@ def register_page(page): def init_api(app): + def fetch_file(filename: str = ""): if not os.path.exists(filename): return JSONResponse({ "error": f"file {filename}: not found" }, status_code=404) @@ -62,23 +64,39 @@ def init_api(app): metadata = page.metadata.get(item, 'none') if metadata is None: metadata = '' - shared.log.debug(f"Extra networks metadata: page='{page}' item={item} len={len(metadata)}") + # shared.log.debug(f"Extra networks metadata: page='{page}' item={item} len={len(metadata)}") return JSONResponse({"metadata": metadata}) - def get_info(page: str = "", item: str = ""): - page = next(iter([x for x in shared.extra_networks if x.name == page]), None) + page = next(iter([x for x in get_pages() if x.name == page]), None) if page is None: return JSONResponse({ 'info': 'none' }) - info = page.info.get(item, 'none') + item = next(iter([x for x in page.items if x['name'] == item]), None) + if item is None: + return JSONResponse({ 'info': 'none' }) + info = page.find_info(item['filename']) if info is None: - info = '' - shared.log.debug(f"Extra networks info: page='{page}' item={item} len={len(info)}") + info = {} + # shared.log.debug(f"Extra networks info: page='{page.name}' item={item['name']} len={len(info)}") return JSONResponse({"info": info}) + def get_desc(page: str = "", item: str = ""): + page = next(iter([x for x in get_pages() if x.name == page]), None) + if page is None: + return JSONResponse({ 'description': 'none' }) + item = next(iter([x for x in page.items if x['name'] == item]), None) + if item is None: + return JSONResponse({ 'description': 'none' }) + desc = page.find_description(item['filename']) + if desc is None: + desc = '' + # shared.log.debug(f"Extra networks desc: page='{page.name}' item={item['name']} len={len(desc)}") + return JSONResponse({"description": desc}) + app.add_api_route("/sd_extra_networks/thumb", fetch_file, methods=["GET"]) app.add_api_route("/sd_extra_networks/metadata", get_metadata, methods=["GET"]) app.add_api_route("/sd_extra_networks/info", get_info, methods=["GET"]) + app.add_api_route("/sd_extra_networks/description", get_desc, methods=["GET"]) class ExtraNetworksPage: @@ -94,7 +112,7 @@ class ExtraNetworksPage: self.refresh_time = None # class additional is to keep old extensions happy self.card = ''' -
+
@@ -199,7 +217,6 @@ class ExtraNetworksPage: htmls = [] for item in self.items: self.metadata[item["name"]] = item.get("metadata", {}) - self.info[item["name"]] = item.get('info', None) or self.find_info(item['filename']) htmls.append(self.create_html(item, tabname)) self.html += ''.join(htmls) if len(subdirs_html) > 0 or len(self.html) > 0: @@ -219,9 +236,11 @@ class ExtraNetworksPage: def create_html(self, item, tabname): try: args = { - "tabname": json.dumps(tabname), + "tabname": tabname, + "page": self.name, "name": item["name"], "title": item["name"].replace('_', ' '), + "filename": item["filename"], "tags": '|'.join([item.get("tags")] if isinstance(item.get("tags", {}), str) else list(item.get("tags", {}).keys())), "preview": html.escape(item.get("preview", self.link_preview('html/card-no-preview.png'))), "width": shared.opts.extra_networks_card_size, @@ -241,28 +260,35 @@ class ExtraNetworksPage: return "" def find_preview_file(self, path): - preview_extensions = ["jpg", "jpeg", "png", "webp", "tiff", "jp2"] fn = os.path.splitext(path)[0] - files = listdir(os.path.dirname(path)) + preview_extensions = ["jpg", "jpeg", "png", "webp", "tiff", "jp2"] for file in [f'{fn}{mid}{ext}' for ext in preview_extensions for mid in ['.thumb.', '.preview.', '.']]: - if file in files: + if os.path.exists(file): return file return 'html/card-no-preview.png' def find_preview(self, path): + fn = os.path.splitext(path)[0] preview_extensions = ["jpg", "jpeg", "png", "webp", "tiff", "jp2"] - files = listdir(os.path.dirname(path)) - for file in [f'{path}{mid}{ext}' for ext in preview_extensions for mid in ['.thumb.', '.preview.', '.']]: - if file in files: + for file in [f'{fn}{mid}{ext}' for ext in preview_extensions for mid in ['.thumb.', '.preview.', '.']]: + if os.path.exists(file): if '.thumb.' not in file: self.missing_thumbs.append(file) return self.link_preview(file) return self.link_preview('html/card-no-preview.png') def find_description(self, path): - files = listdir(os.path.dirname(path)) - for file in [f"{path}.txt", f"{path}.description.txt"]: - if file in files: + class HTMLFilter(HTMLParser): + text = "" + def handle_data(self, data): + self.text += data + def handle_endtag(self, tag): + if tag == 'p': + self.text += '\n' + + fn = os.path.splitext(path)[0] + for file in [f"{fn}.txt", f"{fn}.description.txt"]: + if os.path.exists(file): try: with open(file, "r", encoding="utf-8", errors="replace") as f: txt = f.read() @@ -270,21 +296,17 @@ class ExtraNetworksPage: return txt except OSError: pass - return '' + info = self.find_info(path) + desc = info.get('description', '') or '' + f = HTMLFilter() + f.feed(desc) + return f.text def find_info(self, path): - basename, _ext = os.path.splitext(path) - files = listdir(os.path.dirname(path)) - for file in [f"{path}.info", f"{path}.civitai.info", f"{basename}.info", f"{basename}.civitai.info"]: - if file in files: - try: - with open(file, "r", encoding="utf-8", errors="replace") as f: - txt = f.read() - txt = re.sub('[<>]', '', txt) - return txt - except OSError: - pass - return '' + fn = os.path.splitext(path)[0] + '.json' + if os.path.exists(fn): + return shared.readfile(fn, silent=True) + return {} def initialize(): @@ -364,13 +386,10 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): except Exception as e: shared.log.error(f'Extra networks state error: {e}') return - page, item = get_item(state) - shared.log.debug(f'Extra network: op={state.op} page={page.title if page is not None else None} item={item.filename if item is not None else None}') + _page, item = get_item(state) + # shared.log.debug(f'Extra network: op={state.op} page={page.title if page is not None else None} item={item.filename if item is not None else None}') ui.last_item = item - if state.op == 'getCardDetails': - pass - def toggle_visibility(is_visible): is_visible = not is_visible return is_visible, gr.update(visible=is_visible), gr.update(variant=("secondary-down" if is_visible else "secondary")) @@ -395,13 +414,13 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): with gr.Row(): btn_save_desc = gr.Button('Save', elem_classes=['small-button']) btn_delete_desc = gr.Button('Delete', elem_classes=['small-button']) - with gr.Tab('Info'): - info = gr.Textbox('', show_label=False, lines=8, placeholder="Extra network info...") + with gr.Tab('Model metadata'): + info = gr.JSON({}, show_label=False, lines=8) ui.details_components.append(info) with gr.Row(): btn_save_info = gr.Button('Save', elem_classes=['small-button']) btn_delete_info = gr.Button('Delete', elem_classes=['small-button']) - with gr.Tab('Metadata'): + with gr.Tab('Embedded metadata'): meta = gr.JSON({}, show_label=False, lines=8) ui.details_components.append(meta) @@ -420,15 +439,16 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): hmtl = gr.HTML(page.html, elem_id=f'{tabname}{page.name}_extra_page', elem_classes="extra-networks-page") ui.pages.append(hmtl) - def fn_save_img(index): - if index is None or index < 0 or ui.last_item is None or ui.last_item.local_preview is None: + def fn_save_img(): + if ui.last_item is None or ui.last_item.local_preview is None: return 'html/card-no-preview.png' - images = list(ui.gallery.temp_files) - if len(images) < index + 1: + images = list(ui.gallery.temp_files) # gallery cannot be used as input component so looking at most recently registered temp files + if len(images) < 1: shared.log.warning(f'Extra network no image: item={ui.last_item.name}') return 'html/card-no-preview.png' try: - image = Image.open(images[index]) + images.sort(key=lambda f: os.path.getmtime(f), reverse=True) + image = Image.open(images[0]) except Exception as e: shared.log.error(f'Extra network error opening image: item={ui.last_item.name} {e}') return 'html/card-no-preview.png' @@ -467,23 +487,22 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): return desc def fn_save_info(info): - fn = os.path.splitext(ui.last_item.filename)[0] + '.info' - with open(fn, 'w', encoding='utf-8') as f: - f.write(info) + fn = os.path.splitext(ui.last_item.filename)[0] + '.json' + shared.writefile(info, fn, silent=True) shared.log.debug(f'Extra network save desc: item={ui.last_item.name} filename={fn}') return info def fn_delete_info(info): if ui.last_item is None: return info - fn = os.path.splitext(ui.last_item.filename)[0] + '.info' + fn = os.path.splitext(ui.last_item.filename)[0] + '.json' if os.path.exists(fn): shared.log.debug(f'Extra network delete info: item={ui.last_item.name} filename={fn}') os.remove(fn) return '' return info - btn_save_img.click(fn=fn_save_img, _js='(img) => { return selected_gallery_index() }', inputs=[img], outputs=[img]) + btn_save_img.click(fn=fn_save_img, inputs=[], outputs=[img]) btn_delete_img.click(fn=fn_delete_img, inputs=[], outputs=[img]) btn_save_desc.click(fn=fn_save_desc, inputs=[desc], outputs=[desc]) btn_delete_desc.click(fn=fn_delete_desc, inputs=[desc], outputs=[desc]) @@ -495,12 +514,15 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): if item is not None and os.path.exists(item.filename): stat = os.stat(item.filename) desc = item.description - info = page.info.get(item.name, 'N/A') + fullinfo = shared.readfile(os.path.splitext(item.filename)[0] + '.json', silent=True) + if 'modelVersions' in fullinfo: # sanitize massive objects + fullinfo['modelVersions'] = [] + info = fullinfo meta = page.metadata.get(item.name, {}) or {} if type(meta) is str: try: meta = json.loads(meta) - except: + except Exception: meta = {} img = page.find_preview_file(item.filename) lora = '' @@ -539,7 +561,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): {model} ''' - return [text, img, desc, info, meta, gr.update(visible=True)] + return [text, img, desc, info, meta, gr.update(visible=item is not None)] def en_refresh(title): pages = [] diff --git a/modules/ui_models.py b/modules/ui_models.py index bb90ef822..182f45536 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -6,7 +6,7 @@ from modules import sd_models, sd_vae, extras from modules.ui_components import FormRow, ToolButton from modules.ui_common import create_refresh_button from modules.call_queue import wrap_gradio_gpu_call -from modules.shared import opts, log +from modules.shared import opts, log, req import modules.errors import modules.hashes @@ -166,11 +166,11 @@ def create_ui(): m_meta = len(json.dumps(m.metadata)) - 2 m_size = round(stat.st_size / 1024 / 1024 / 1024, 3) m_time = datetime.fromtimestamp(stat.st_mtime) - model_data.append([m_name, m_type, m.filename, m.hash, m_time, m_size, m_meta]) + model_data.append([m_name, m_type, m.filename, m.shorthash, m_time, m_size, m_meta]) total_size += stat.st_size except Exception as e: txt += f"Error: {m.name} {e}
" - txt += f"Model list enumerated {len(sd_models.checkpoints_list.keys())} models in {round(total_size / 1024 / 1024 / 1024, 3)} GB
" + txt += f"Model list enumerated {len(sd_models.checkpoints_list.keys())} models in {round(total_size / 1024 / 1024 / 1024, 3)} GB
" return model_data, txt model_list_btn.click(fn=list_models, inputs=[], outputs=[model_table, models_outcome]) @@ -240,15 +240,13 @@ def create_ui(): data = [] def civit_search(name, tag, model_type): - import requests - headers = { 'Content-type': 'application/json' } types = 'LORA' if model_type == 'LoRA' else 'Checkpoint' url = f'https://civitai.com/api/v1/models?limit=25&types={types}&Sort=Newest' if name is not None and len(name) > 0: url += f'&query={name}' if tag is not None and len(tag) > 0: url += f'&tag={tag}' - r = requests.get(url, timeout=60, headers=headers) + r = req(url) log.debug(f'CivitAI search: name="{name}" tag={tag or "none"} status={r.status_code}') if r.status_code != 200: return [], [], [] @@ -336,38 +334,50 @@ def create_ui(): return res def civit_download_previews(civit_previews_rehash): - import requests - from modules.ui_extra_networks import extra_pages - from modules.modelloader import download_civit_preview - headers = { 'Content-type': 'application/json' } - res = '' - for page in extra_pages: + log.debug('CivitAI download previews') + from modules.ui_extra_networks import get_pages + from modules.modelloader import download_civit_preview, download_civit_meta + res = [] + for page in get_pages(): + if page.name == 'style': + continue for item in page.list_items(): - if item.get('fullname', None) is None: - continue - if 'card-no-preview.png' in item['preview'] and os.path.isfile(item['fullname']): + meta = os.path.splitext(item['filename'])[0] + '.json' + if ('card-no-preview.png' in item['preview'] or not os.path.isfile(meta)) and os.path.isfile(item['filename']): sha = item.get('hash', None) - if item.get('hash', None) is None: - log.debug(f'CivitAI skipping item without hash: name="{item["name"]}"') - continue - r = requests.get(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}', timeout=5, headers=headers) - log.debug(f'CivitAI search: name="{item["name"]}" hash={sha} status={r.status_code}') - if r.status_code == 200: - d = r.json() - if d.get('images') is not None and len(d['images']) > 0 and len(d['images'][0]['url']) > 0: - preview_url = d['images'][0]['url'] - res += download_civit_preview(item['filename'], preview_url) + '
' - elif civit_previews_rehash and os.stat(item['fullname']).st_size < (1024 * 1024 * 1024): - sha = modules.hashes.calculate_sha256(item['fullname'], quiet=True) - r = requests.get(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}', timeout=5, headers=headers) + found = False + if sha is not None and len(sha) > 0: + r = req(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}') log.debug(f'CivitAI search: name="{item["name"]}" hash={sha} status={r.status_code}') if r.status_code == 200: d = r.json() + res.append(download_civit_meta(item['filename'], d['modelId'])) if d.get('images') is not None and len(d['images']) > 0 and len(d['images'][0]['url']) > 0: preview_url = d['images'][0]['url'] - res += download_civit_preview(item['filename'], preview_url) + '
' - return res + res.append(download_civit_preview(item['filename'], preview_url)) + found = True + if not found and civit_previews_rehash and os.stat(item['filename']).st_size < (1024 * 1024 * 1024): + sha = modules.hashes.calculate_sha256(item['filename'], quiet=True)[:10] + r = req(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}') + log.debug(f'CivitAI search: name="{item["name"]}" hash={sha} status={r.status_code}') + if r.status_code == 200: + d = r.json() + res.append(download_civit_meta(item['filename'], d['modelId'])) + if d.get('images') is not None and len(d['images']) > 0 and len(d['images'][0]['url']) > 0: + preview_url = d['images'][0]['url'] + res.append(download_civit_preview(item['filename'], preview_url)) + txt = '
'.join([r for r in res if len(r) > 0]) + return txt + with gr.Row(style={'margin-top': '1em'}): + gr.HTML('

Fetch information

Fetches preview and metadata information for all models with missing information
Models with existing previews and information are not updated
') + with gr.Row(): + civit_previews_btn = gr.Button(value="Start", variant='primary') + with gr.Row(): + civit_previews_rehash = gr.Checkbox(value=True, label="Check alternative hash") + + with gr.Row(style={'margin-top': '1em'}): + gr.HTML('

Search for models

') with gr.Row(): with gr.Column(scale=1): civit_model_type = gr.Dropdown(label='Model type', choices=['SD 1.5', 'SD XL', 'LoRA', 'Other'], value='LoRA') @@ -395,9 +405,6 @@ def create_ui(): civit_headers1 = ['ID', 'Name', 'Tags', 'Downloads', 'Rating'] civit_types1 = ['number', 'str', 'str', 'number', 'number'] civit_results1 = gr.DataFrame(value = None, label = 'Search results', show_label = True, interactive = False, wrap = True, overflow_row_behaviour = 'paginate', max_rows = 10, headers = civit_headers1, datatype = civit_types1, type='array') - with gr.Row(): - civit_previews_btn = gr.Button(value="Fetch previews for existing models", variant='primary') - civit_previews_rehash = gr.Checkbox(value=False, label="Check alternative hash") civit_search_text.submit(fn=civit_search, inputs=[civit_search_text, civit_search_tag, civit_model_type], outputs=[civit_results1, civit_results2, civit_results3]) civit_search_tag.submit(fn=civit_search, inputs=[civit_search_text, civit_search_tag, civit_model_type], outputs=[civit_results1, civit_results2, civit_results3]) diff --git a/modules/ui_symbols.py b/modules/ui_symbols.py index db8e071b6..c58aa5211 100644 --- a/modules/ui_symbols.py +++ b/modules/ui_symbols.py @@ -24,4 +24,4 @@ detect = '📐' folder = '📂' random = '🎲️' reuse = '♻️' -info = 'ℹ' +info = 'ℹ' # noqa