diff --git a/TODO.md b/TODO.md index f9da5b122..239cdc080 100644 --- a/TODO.md +++ b/TODO.md @@ -25,8 +25,6 @@ Stuff to be added... Stuff to be investigated... -- TXT2IMG: -- `TensorRT` ## Merge PRs @@ -51,6 +49,7 @@ Tech that can be integrated as part of the core workflow... - [Custom diffusion](https://github.com/guaneec/custom-diffusion-webui), [Custom diffusion](https://www.cs.cmu.edu/~custom-diffusion/) - [Dream artist](https://github.com/7eu7d7/DreamArtist-sd-webui-extension) - [QuickEmbedding](https://github.com/ethansmith2000/QuickEmbedding) +- `TensorRT` ## Random @@ -58,7 +57,19 @@ Tech that can be integrated as part of the core workflow... ### Pending Code Updates -- add `--safe` mode which skips loading user extensions +This is a massive one due to huge number of changes, but hopefully it will fo ok... + +- new **prompt parsers** + select in UI -> Settings -> Stable Diffusion + - **Full**: my new implementation + - **A1111**: for backward compatibility + - **Compel**: as used in ComfyUI and InvokeAI (a.k.a *Temporal Weighting*) + - **Fixed**: for really old backward compatibility +- added `--safe` command line flag mode which skips loading user extensions please try to use it before opening new issue -- add option in settings: **Prompt attention parser** - to allow for backward compatibility with a1111 (broken) parser +- reintroduce `--api-only` mode to start server without ui +- monitor **extensions** install/startup and + log if they modify any packages/requirements + this is a *deep-experimental* python hack, but i think its worth it as extensions modifying requirements is one of most common causes of issues +- port *all* upstream code from [A1111](https://github.com/AUTOMATIC1111/stable-diffusion-webui) + up to today - commit hash `89f9faa` diff --git a/extensions-builtin/Lora/scripts/lora_script.py b/extensions-builtin/Lora/scripts/lora_script.py index 060bda059..7b485d97d 100644 --- a/extensions-builtin/Lora/scripts/lora_script.py +++ b/extensions-builtin/Lora/scripts/lora_script.py @@ -20,7 +20,6 @@ def before_ui(): ui_extra_networks.register_page(ui_extra_networks_lora.ExtraNetworksPageLora()) extra_networks.register_extra_network(extra_networks_lora.ExtraNetworkLora()) - if not hasattr(torch.nn, 'Linear_forward_before_lora'): torch.nn.Linear_forward_before_lora = torch.nn.Linear.forward diff --git a/extensions-builtin/Lora/ui_extra_networks_lora.py b/extensions-builtin/Lora/ui_extra_networks_lora.py index 2050e3faa..6553a7ebf 100644 --- a/extensions-builtin/Lora/ui_extra_networks_lora.py +++ b/extensions-builtin/Lora/ui_extra_networks_lora.py @@ -14,7 +14,7 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): def list_items(self): for name, lora_on_disk in lora.available_loras.items(): - path, ext = os.path.splitext(lora_on_disk.filename) + path, _ext = os.path.splitext(lora_on_disk.filename) if shared.opts.lora_preferred_name == "Filename" or lora_on_disk.alias.lower() in lora.forbidden_lora_aliases: alias = name @@ -34,4 +34,3 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): def allowed_directories_for_previews(self): return [shared.cmd_opts.lora_dir] - diff --git a/installer.py b/installer.py index 6f6ea7035..4c5f0ed65 100644 --- a/installer.py +++ b/installer.py @@ -43,7 +43,7 @@ args = Dot({ 'version': False, 'ignore': False, }) - +git_commit = "unknown" # setup console and file logging def setup_logging(clean=False): @@ -369,6 +369,10 @@ def list_extensions(folder): # run installer for each installed and enabled extension and optionally update them def install_extensions(): + import pkg_resources + pkg_resources._initialize_master_working_set() # pylint: disable=protected-access + pkgs = [f'{p.project_name}=={p._version}' for p in pkg_resources.working_set] # pylint: disable=protected-access,not-an-iterable + log.debug(f'Installed packages: {len(pkgs)}') from modules.paths_internal import extensions_builtin_dir, extensions_dir extensions_duplicates = [] extensions_enabled = [] @@ -390,6 +394,12 @@ def install_extensions(): log.error(f'Error updating extension: {os.path.join(folder, ext)}') if not args.skip_extensions: run_extension_installer(os.path.join(folder, ext)) + pkg_resources._initialize_master_working_set() # pylint: disable=protected-access + updated = [f'{p.project_name}=={p._version}' for p in pkg_resources.working_set] # pylint: disable=protected-access,not-an-iterable + diff = [x for x in updated if x not in pkgs] + pkgs = updated + if len(diff) > 0: + log.info(f'Extension installed packages: {ext} {diff}') log.info(f'Extensions enabled: {extensions_enabled}') if len(extensions_duplicates) > 0: log.warning(f'Extensions duplicates: {extensions_duplicates}') @@ -500,6 +510,8 @@ def check_version(offline=False, reset=True): # pylint: disable=unused-argument if args.version: return commit = git('rev-parse HEAD') + global git_commit # pylint: disable=global-statement + git_commit = commit[:7] try: import requests except ImportError: @@ -583,6 +595,7 @@ def add_args(): group.add_argument('--debug', default = False, action='store_true', help = "Run installer with debug logging, default: %(default)s") group.add_argument('--reset', default = False, action='store_true', help = "Reset main repository to latest version, default: %(default)s") group.add_argument('--upgrade', default = False, action='store_true', help = "Upgrade main repository to latest version, 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("--use-ipex", default = False, action='store_true', help="Use Intel OneAPI XPU backend, 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-cuda", default=False, action='store_true', help="Force use nVidia CUDA backend, default: %(default)s") diff --git a/javascript/.eslintrc.json b/javascript/.eslintrc.json index e0ce80061..5fcb88998 100644 --- a/javascript/.eslintrc.json +++ b/javascript/.eslintrc.json @@ -16,6 +16,7 @@ "no-unused-vars":"off", "no-plusplus":"off", "no-param-reassign":"off", - "no-restricted-syntax":"off" + "no-restricted-syntax":"off", + "no-mixed-operators":"off" } } diff --git a/javascript/aspectRatioOverlay.js b/javascript/aspectRatioOverlay.js index 20984f6a4..1159cb697 100644 --- a/javascript/aspectRatioOverlay.js +++ b/javascript/aspectRatioOverlay.js @@ -1,3 +1,5 @@ +/* global gradioApp, onUiUpdate, get_tab_index */ + let currentWidth = null; let currentHeight = null; let arFrameTimeout = setTimeout(() => {}, 0); @@ -9,7 +11,7 @@ function dimensionChange(e, is_width, is_height) { if (!inImg2img) return; let targetElement = null; const tabIndex = get_tab_index('mode_img2img'); - if (tabIndex === 0) targetElement = gradioApp().querySelector('#img2img_image div[data-testid=image] img'); // img2img + if (tabIndex === 0) targetElement = gradioApp().querySelector('#img2img_image div[data-testid=image] img'); // img2img else if (tabIndex === 1) targetElement = gradioApp().querySelector('#img2img_sketch div[data-testid=image] img'); // Sketch else if (tabIndex === 2) targetElement = gradioApp().querySelector('#img2maskimg div[data-testid=image] img'); // Inpaint else if (tabIndex === 3) targetElement = gradioApp().querySelector('#inpaint_sketch div[data-testid=image] img'); // Inpaint sketch @@ -23,26 +25,20 @@ function dimensionChange(e, is_width, is_height) { } const viewportOffset = targetElement.getBoundingClientRect(); - const viewportscale = Math.min(targetElement.clientWidth / targetElement.naturalWidth, targetElement.clientHeight / targetElement.naturalHeight); - const scaledx = targetElement.naturalWidth * viewportscale; const scaledy = targetElement.naturalHeight * viewportscale; - const cleintRectTop = (viewportOffset.top + window.scrollY); const cleintRectLeft = (viewportOffset.left + window.scrollX); const cleintRectCentreY = cleintRectTop + (targetElement.clientHeight / 2); const cleintRectCentreX = cleintRectLeft + (targetElement.clientWidth / 2); - const arscale = Math.min(scaledx / currentWidth, scaledy / currentHeight); const arscaledx = currentWidth * arscale; const arscaledy = currentHeight * arscale; - const arRectTop = cleintRectCentreY - (arscaledy / 2); const arRectLeft = cleintRectCentreX - (arscaledx / 2); const arRectWidth = arscaledx; const arRectHeight = arscaledy; - arPreviewRect.style.top = `${arRectTop}px`; arPreviewRect.style.left = `${arRectLeft}px`; arPreviewRect.style.width = `${arRectWidth}px`; @@ -58,9 +54,7 @@ function dimensionChange(e, is_width, is_height) { onUiUpdate(() => { const arPreviewRect = gradioApp().querySelector('#imageARPreview'); - if (arPreviewRect) { - arPreviewRect.style.display = 'none'; - } + if (arPreviewRect) arPreviewRect.style.display = 'none'; const tabImg2img = gradioApp().querySelector('#tab_img2img'); if (tabImg2img) { const inImg2img = tabImg2img.style.display === 'block'; @@ -69,17 +63,12 @@ onUiUpdate(() => { inputs.forEach((e) => { const is_width = e.parentElement.id === 'img2img_width'; const is_height = e.parentElement.id === 'img2img_height'; - if ((is_width || is_height) && !e.classList.contains('scrollwatch')) { - e.addEventListener('input', (e) => { dimensionChange(e, is_width, is_height); }); + e.addEventListener('input', (evt) => { dimensionChange(evt, is_width, is_height); }); e.classList.add('scrollwatch'); } - if (is_width) { - currentWidth = e.value * 1.0; - } - if (is_height) { - currentHeight = e.value * 1.0; - } + if (is_width) currentWidth = e.value * 1.0; + if (is_height) currentHeight = e.value * 1.0; }); } } diff --git a/javascript/contextMenus.js b/javascript/contextMenus.js index 1c8d840f1..880ff3172 100644 --- a/javascript/contextMenus.js +++ b/javascript/contextMenus.js @@ -1,23 +1,18 @@ -contextMenuInit = function () { +/* global gradioApp, uiCurrentTab, onUiUpdate, get_uiCurrentTabContent */ + +const contextMenuInit = () => { let eventListenerApplied = false; const menuSpecs = new Map(); - const uid = function () { - return Date.now().toString(36) + Math.random().toString(36).substring(2); - }; + const uid = () => Date.now().toString(36) + Math.random().toString(36).substring(2); function showContextMenu(event, element, menuEntries) { const posx = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; const posy = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; - const oldMenu = gradioApp().querySelector('#context-menu'); - if (oldMenu) { - oldMenu.remove(); - } - + if (oldMenu) oldMenu.remove(); const tabButton = uiCurrentTab; const baseStyle = window.getComputedStyle(tabButton); - const contextMenu = document.createElement('nav'); contextMenu.id = 'context-menu'; contextMenu.style.background = baseStyle.background; @@ -25,40 +20,26 @@ contextMenuInit = function () { contextMenu.style.fontFamily = baseStyle.fontFamily; contextMenu.style.top = `${posy}px`; contextMenu.style.left = `${posx}px`; - const contextMenuList = document.createElement('ul'); contextMenuList.className = 'context-menu-items'; contextMenu.append(contextMenuList); - menuEntries.forEach((entry) => { const contextMenuEntry = document.createElement('a'); contextMenuEntry.innerHTML = entry.name; - contextMenuEntry.addEventListener('click', (e) => { - entry.func(); - }); + contextMenuEntry.addEventListener('click', (e) => entry.func()); contextMenuList.append(contextMenuEntry); }); - gradioApp().appendChild(contextMenu); - const menuWidth = contextMenu.offsetWidth + 4; const menuHeight = contextMenu.offsetHeight + 4; - const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; - - if ((windowWidth - posx) < menuWidth) { - contextMenu.style.left = `${windowWidth - menuWidth}px`; - } - - if ((windowHeight - posy) < menuHeight) { - contextMenu.style.top = `${windowHeight - menuHeight}px`; - } + if ((windowWidth - posx) < menuWidth) contextMenu.style.left = `${windowWidth - menuWidth}px`; + if ((windowHeight - posy) < menuHeight) contextMenu.style.top = `${windowHeight - menuHeight}px`; } function appendContextMenuOption(targetElementSelector, entryName, entryFunction) { - currentItems = menuSpecs.get(targetElementSelector); - + let currentItems = menuSpecs.get(targetElementSelector); if (!currentItems) { currentItems = []; menuSpecs.set(targetElementSelector, currentItems); @@ -69,7 +50,6 @@ contextMenuInit = function () { func: entryFunction, isNew: true, }; - currentItems.push(newItem); return newItem.id; } @@ -89,15 +69,11 @@ contextMenuInit = function () { gradioApp().addEventListener('click', (e) => { if (!e.isTrusted) return; const oldMenu = gradioApp().querySelector('#context-menu'); - if (oldMenu) { - oldMenu.remove(); - } + if (oldMenu) oldMenu.remove(); }); gradioApp().addEventListener('contextmenu', (e) => { const oldMenu = gradioApp().querySelector('#context-menu'); - if (oldMenu) { - oldMenu.remove(); - } + if (oldMenu) oldMenu.remove(); menuSpecs.forEach((v, k) => { if (e.composedPath()[0].matches(k)) { showContextMenu(e, e.composedPath()[0], v); @@ -107,14 +83,13 @@ contextMenuInit = function () { }); eventListenerApplied = true; } - return [appendContextMenuOption, removeContextMenuOption, addContextMenuEventListener]; }; -initResponse = contextMenuInit(); -appendContextMenuOption = initResponse[0]; -removeContextMenuOption = initResponse[1]; -addContextMenuEventListener = initResponse[2]; +const initResponse = contextMenuInit(); +const appendContextMenuOption = initResponse[0]; +const removeContextMenuOption = initResponse[1]; +const addContextMenuEventListener = initResponse[2]; (function () { // Start example Context Menu Items @@ -128,9 +103,7 @@ addContextMenuEventListener = initResponse[2]; window.generateOnRepeatInterval = setInterval( () => { const busy = document.getElementById('progressbar')?.style.display === 'block'; - if (!busy) { - genbutton.click(); - } + if (!busy) genbutton.click(); }, 500, ); @@ -151,7 +124,6 @@ addContextMenuEventListener = initResponse[2]; appendContextMenuOption('#txt2img_generate', 'Cancel generate forever', cancelGenerateForever); appendContextMenuOption('#img2img_interrupt', 'Cancel generate forever', cancelGenerateForever); appendContextMenuOption('#img2img_generate', 'Cancel generate forever', cancelGenerateForever); - appendContextMenuOption( '#roll', 'Roll three', @@ -165,6 +137,4 @@ addContextMenuEventListener = initResponse[2]; }()); // End example Context Menu Items -onUiUpdate(() => { - addContextMenuEventListener(); -}); +onUiUpdate(() => addContextMenuEventListener()); diff --git a/javascript/hires_fix.js b/javascript/hires_fix.js index 1ed690c4f..b71f4ddbb 100644 --- a/javascript/hires_fix.js +++ b/javascript/hires_fix.js @@ -1,16 +1,14 @@ -function setInactive(elem, inactive) { - if (inactive) elem.classList.add('inactive'); - else elem.classList.remove('inactive'); -} - +/* global gradioApp, opts */ function onCalcResolutionHires(enable, width, height, hr_scale, hr_resize_x, hr_resize_y) { + function setInactive(elem, inactive) { + elem.classList.toggle('inactive', !!inactive); + } const hrUpscaleBy = gradioApp().getElementById('txt2img_hr_scale'); const hrResizeX = gradioApp().getElementById('txt2img_hr_resize_x'); const hrResizeY = gradioApp().getElementById('txt2img_hr_resize_y'); gradioApp().getElementById('txt2img_hires_fix_row2').style.display = opts.use_old_hires_fix_width_height ? 'none' : ''; setInactive(hrUpscaleBy, opts.use_old_hires_fix_width_height || hr_resize_x > 0 || hr_resize_y > 0); - setInactive(hrResizeX, opts.use_old_hires_fix_width_height || hr_resize_x === 0); - setInactive(hrResizeY, opts.use_old_hires_fix_width_height || hr_resize_y === 0); - // return [enable, width, height, hr_scale, hr_resize_x, hr_resize_y]; - setTimeout(() => [enable, width, height, hr_scale, hr_resize_x, hr_resize_y], 100); + setInactive(hrResizeX, opts.use_old_hires_fix_width_height || hr_resize_x == 0); + setInactive(hrResizeY, opts.use_old_hires_fix_width_height || hr_resize_y == 0); + return [enable, width, height, hr_scale, hr_resize_x, hr_resize_y]; } diff --git a/javascript/imageMaskFix.js b/javascript/imageMaskFix.js index ec64feaa6..0cbb08822 100644 --- a/javascript/imageMaskFix.js +++ b/javascript/imageMaskFix.js @@ -1,3 +1,4 @@ +/* global gradioApp, onUiUpdate */ /** * temporary fix for https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/668 * @see https://github.com/gradio-app/gradio/issues/1721 @@ -5,7 +6,6 @@ function imageMaskResize() { const canvases = gradioApp().querySelectorAll('#img2maskimg .touch-none canvas'); if (!canvases.length) { - canvases_fixed = false; window.removeEventListener('resize', imageMaskResize); return; } @@ -14,7 +14,7 @@ function imageMaskResize() { const previewImage = wrapper.previousElementSibling; if (!previewImage.complete) { - previewImage.addEventListener('load', () => imageMaskResize()); + previewImage.addEventListener('load', imageMaskResize); return; } @@ -23,7 +23,6 @@ function imageMaskResize() { const nw = previewImage.naturalWidth; const nh = previewImage.naturalHeight; const portrait = nh > nw; - const factor = portrait; const wW = Math.min(w, portrait ? h / nh * nw : w / nw * nw); const wH = Math.min(h, portrait ? h / nh * nh : w / nw * nh); @@ -34,12 +33,13 @@ function imageMaskResize() { wrapper.style.top = '0px'; canvases.forEach((c) => { - c.style.width = c.style.height = ''; + c.style.width = ''; + c.style.height = ''; c.style.maxWidth = '100%'; c.style.maxHeight = '100%'; c.style.objectFit = 'contain'; }); } +onUiUpdate(imageMaskResize); window.addEventListener('resize', imageMaskResize); -onUiUpdate(() => imageMaskResize()); diff --git a/javascript/imageParams.js b/javascript/imageParams.js index 2530d585a..89bfe58a1 100644 --- a/javascript/imageParams.js +++ b/javascript/imageParams.js @@ -1,12 +1,10 @@ +/* global gradioApp, get_tab_index */ window.onload = (function () { window.addEventListener('drop', (e) => { const target = e.composedPath()[0]; if (!target.placeholder) return; - const idx = selected_gallery_index(); if (target.placeholder.indexOf('Prompt') == -1) return; - const prompt_target = get_tab_index('tabs') == 1 ? 'img2img_prompt_image' : 'txt2img_prompt_image'; - e.stopPropagation(); e.preventDefault(); const imgParent = gradioApp().getElementById(prompt_target); diff --git a/javascript/imageviewer.js b/javascript/imageviewer.js index 45c786ee5..cfad41ff5 100644 --- a/javascript/imageviewer.js +++ b/javascript/imageviewer.js @@ -1,3 +1,4 @@ +/* global gradioApp, onUiUpdate */ // A full size 'lightbox' preview modal shown when left clicking on gallery previews function closeModal() { gradioApp().getElementById('lightboxModal').style.display = 'none'; @@ -96,6 +97,11 @@ function modalKeyHandler(event) { } } +function modalZoomSet(modalImage, enable) { + localStorage.setItem('modalZoom', enable ? 'yes' : 'no'); + if (modalImage) modalImage.classList.toggle('modalImageFullscreen', !!enable); +} + function setupImageForLightbox(e) { if (e.dataset.modded) return; e.dataset.modded = true; @@ -106,21 +112,15 @@ function setupImageForLightbox(e) { const event = isFirefox ? 'mousedown' : 'click'; e.addEventListener(event, (evt) => { if (evt.button != 0) return; - initialZoom = (localStorage.getItem('modalZoom') || true) == 'yes'; + const initialZoom = (localStorage.getItem('modalZoom') || true) == 'yes'; modalZoomSet(gradioApp().getElementById('modalImage'), initialZoom); evt.preventDefault(); showModal(evt); }, true); } -function modalZoomSet(modalImage, enable) { - if (enable) modalImage.classList.add('modalImageFullscreen'); - else modalImage.classList.remove('modalImageFullscreen'); - localStorage.setItem('modalZoom', enable ? 'yes' : 'no'); -} - function modalZoomToggle(event) { - modalImage = gradioApp().getElementById('modalImage'); + const modalImage = gradioApp().getElementById('modalImage'); modalZoomSet(modalImage, !modalImage.classList.contains('modalImageFullscreen')); event.stopPropagation(); } diff --git a/javascript/progressbar.js b/javascript/progressbar.js index 09c7539cf..c1a2b5b36 100644 --- a/javascript/progressbar.js +++ b/javascript/progressbar.js @@ -1,133 +1,133 @@ +/* global opts */ function rememberGallerySelection(id_gallery) {} function getGallerySelectedIndex(id_gallery) {} function request(url, data, handler, errorHandler) { - var xhr = new XMLHttpRequest(); - var url = url; - xhr.open("POST", url, true); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - try { - var js = JSON.parse(xhr.responseText); - handler(js) - } catch (error) { - console.error(error); - errorHandler() - } - } else{ - errorHandler() - } + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + try { + const js = JSON.parse(xhr.responseText); + handler(js); + } catch (error) { + console.error(error); + errorHandler(); } - }; - var js = JSON.stringify(data); - xhr.send(js); + } else { + errorHandler(); + } + } + }; + const js = JSON.stringify(data); + xhr.send(js); } function pad2(x) { - return x<10 ? '0'+x : x + return x < 10 ? `0${x}` : x; } function formatTime(secs) { - if(secs > 3600) return pad2(Math.floor(secs/60/60)) + ":" + pad2(Math.floor(secs/60)%60) + ":" + pad2(Math.floor(secs)%60) - else if(secs > 60) return pad2(Math.floor(secs/60)) + ":" + pad2(Math.floor(secs)%60) - else return Math.floor(secs) + "s" + if (secs > 3600) return `${pad2(Math.floor(secs / 60 / 60))}:${pad2(Math.floor(secs / 60) % 60)}:${pad2(Math.floor(secs) % 60)}`; + if (secs > 60) return `${pad2(Math.floor(secs / 60))}:${pad2(Math.floor(secs) % 60)}`; + return `${Math.floor(secs)}s`; } function setTitle(progress) { - var title = 'SD.Next' - if (progress) title += ' ' + progress.split(' ')[0].trim(); - if (document.title != title) document.title = title; + let title = 'SD.Next'; + if (progress) title += ` ${progress.split(' ')[0].trim()}`; + if (document.title != title) document.title = title; } function randomId() { - return "task(" + Math.random().toString(36).slice(2, 7) + Math.random().toString(36).slice(2, 7) + Math.random().toString(36).slice(2, 7)+")" + return `task(${Math.random().toString(36).slice(2, 7)}${Math.random().toString(36).slice(2, 7)}${Math.random().toString(36).slice(2, 7)})`; } // starts sending progress requests to "/internal/progress" uri, creating progressbar above progressbarContainer element and // preview inside gallery element. Cleans up all created stuff when the task is over and calls atEnd. // calls onProgress every time there is a progress update function requestProgress(id_task, progressbarContainer, gallery, atEnd = null, onProgress = null, once = false) { - var hasStarted = false - var dateStart = new Date() - var prevProgress = null - var parentProgressbar = progressbarContainer.parentNode - var parentGallery = gallery ? gallery.parentNode : null - var divProgress = document.createElement('div') - divProgress.className='progressDiv' - divProgress.id = 'progressbar' - divProgress.style.display = opts.show_progressbar ? "block" : "none" - var divInner = document.createElement('div') - divInner.className='progress' - divProgress.appendChild(divInner) - parentProgressbar.insertBefore(divProgress, progressbarContainer) - localStorage.setItem('task', id_task); - console.debug('task active:', id_task) - if (parentGallery) { - var livePreview = document.createElement('div') - livePreview.className='livePreview' - parentGallery.insertBefore(livePreview, gallery) - } + let hasStarted = false; + const dateStart = new Date(); + const prevProgress = null; + const parentProgressbar = progressbarContainer.parentNode; + const parentGallery = gallery ? gallery.parentNode : null; + const divProgress = document.createElement('div'); + divProgress.className = 'progressDiv'; + divProgress.id = 'progressbar'; + divProgress.style.display = opts.show_progressbar ? 'block' : 'none'; + const divInner = document.createElement('div'); + divInner.className = 'progress'; + divProgress.appendChild(divInner); + parentProgressbar.insertBefore(divProgress, progressbarContainer); + localStorage.setItem('task', id_task); + console.debug('task active:', id_task); + if (parentGallery) { + const livePreview = document.createElement('div'); + livePreview.className = 'livePreview'; + parentGallery.insertBefore(livePreview, gallery); + } - var removeProgressBar = function() { - console.debug('task end: ', id_task) - localStorage.removeItem('task'); - setTitle("") - if (divProgress) parentProgressbar.removeChild(divProgress) - if (parentGallery) parentGallery.removeChild(livePreview) - if (atEnd) atEnd() - } + const removeProgressBar = function () { + console.debug('task end: ', id_task); + localStorage.removeItem('task'); + setTitle(''); + if (divProgress) parentProgressbar.removeChild(divProgress); + if (parentGallery) parentGallery.removeChild(livePreview); + if (atEnd) atEnd(); + }; - var fun = function(id_task, id_live_preview){ - request("./internal/progress", {"id_task": id_task, "id_live_preview": id_live_preview}, function(res){ - var elapsedFromStart = (new Date() - dateStart) / 1000 - if (res.completed) { - removeProgressBar() - return - } - var rect = progressbarContainer.getBoundingClientRect() - if (rect.width) divProgress.style.width = rect.width + "px"; - progressText = "" - divInner.style.width = ((res.progress || 0) * 100.0) + '%' - divInner.style.background = res.progress ? "" : "transparent" - if (res.progress > 0) progressText = ((res.progress || 0) * 100.0).toFixed(0) + '%' - if (res.eta) progressText += " ETA: " + formatTime(res.eta) - setTitle(progressText) - if (res.textinfo && res.textinfo.indexOf("\n") == -1) progressText = res.textinfo + " " + progressText - divInner.textContent = progressText - hasStarted |= res.active - if (!res.active && (hasStarted || once)) { - removeProgressBar() - return - } - if (res.completed) { - removeProgressBar() - return - } - if (elapsedFromStart > 30 && !res.queued && res.progress == prevProgress) { - removeProgressBar() - return - } - if (res.live_preview && gallery) { - var rect = gallery.getBoundingClientRect() - if(rect.width){ - livePreview.style.width = rect.width + "px" - livePreview.style.height = rect.height + "px" - } - var img = new Image(); - img.onload = function() { - livePreview.appendChild(img) - if (livePreview.childElementCount > 2) livePreview.removeChild(livePreview.firstElementChild) - } - img.src = res.live_preview; - } - if (onProgress) onProgress(res) - setTimeout(() => fun(id_task, res.id_live_preview), opts.live_preview_refresh_period || 250) - }, function() { - removeProgressBar() - }) - } - fun(id_task, 0) + const fun = function (id_task, id_live_preview) { + request('./internal/progress', { id_task, id_live_preview }, (res) => { + const elapsedFromStart = (new Date() - dateStart) / 1000; + if (res.completed) { + removeProgressBar(); + return; + } + var rect = progressbarContainer.getBoundingClientRect(); + if (rect.width) divProgress.style.width = `${rect.width}px`; + progressText = ''; + divInner.style.width = `${(res.progress || 0) * 100.0}%`; + divInner.style.background = res.progress ? '' : 'transparent'; + if (res.progress > 0) progressText = `${((res.progress || 0) * 100.0).toFixed(0)}%`; + if (res.eta) progressText += ` ETA: ${formatTime(res.eta)}`; + setTitle(progressText); + if (res.textinfo && res.textinfo.indexOf('\n') == -1) progressText = `${res.textinfo} ${progressText}`; + divInner.textContent = progressText; + hasStarted |= res.active; + if (!res.active && (hasStarted || once)) { + removeProgressBar(); + return; + } + if (res.completed) { + removeProgressBar(); + return; + } + if (elapsedFromStart > 30 && !res.queued && res.progress == prevProgress) { + removeProgressBar(); + return; + } + if (res.live_preview && gallery) { + var rect = gallery.getBoundingClientRect(); + if (rect.width) { + livePreview.style.width = `${rect.width}px`; + livePreview.style.height = `${rect.height}px`; + } + const img = new Image(); + img.onload = function () { + livePreview.appendChild(img); + if (livePreview.childElementCount > 2) livePreview.removeChild(livePreview.firstElementChild); + }; + img.src = res.live_preview; + } + if (onProgress) onProgress(res); + setTimeout(() => fun(id_task, res.id_live_preview), opts.live_preview_refresh_period || 250); + }, () => { + removeProgressBar(); + }); + }; + fun(id_task, 0); } diff --git a/javascript/style.css b/javascript/style.css index 6254de8cb..76d8fc467 100644 --- a/javascript/style.css +++ b/javascript/style.css @@ -108,7 +108,12 @@ button.custom-button{ } } -#txt2img_gallery img, #img2img_gallery img{ +a{ + font-weight: bold; + cursor: pointer; +} + +#txt2img_gallery img, #img2img_gallery img, #extras_gallery img{ object-fit: scale-down; } #txt2img_actions_column, #img2img_actions_column { @@ -406,6 +411,21 @@ div#extras_scale_to_tab div.form{ #lightboxModal > img.modalImageFullscreen{ object-fit: contain; height: 100%; + width: 100%; + min-height: 0; +} + +table.settings-value-table{ + background: white; + border-collapse: collapse; + margin: 1em; + border: 4px solid white; +} + +table.settings-value-table td{ + padding: 0.4em; + border: 1px solid #ccc; + max-width: 36em; } .modalPrev, diff --git a/launch.py b/launch.py index 4a38f9eeb..404fba472 100644 --- a/launch.py +++ b/launch.py @@ -118,7 +118,10 @@ def start_server(immediate=True, server=None): installer.log.info("Test only") server.wants_restart = False else: - server = server.webui() + if args.api_only: + server = server.api_only() + else: + server = server.webui() installer.log.info(f'Memory {get_memory_stats()}') return server diff --git a/modules/cmd_args.py b/modules/cmd_args.py index e4a209dce..1bcd089e4 100644 --- a/modules/cmd_args.py +++ b/modules/cmd_args.py @@ -43,6 +43,7 @@ group.add_argument("--use-ipex", default = False, action='store_true', help="Use 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-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') # removed args are added here as hidden in fixed format for compatbility reasons group.add_argument("-f", action='store_true', help=argparse.SUPPRESS) # allows running as root; implemented outside of webui diff --git a/modules/devices.py b/modules/devices.py index de0bd6fd5..0acd61871 100644 --- a/modules/devices.py +++ b/modules/devices.py @@ -60,8 +60,8 @@ def get_device_for(task): return get_optimal_device() -def torch_gc(): - if shared.opts.disable_gc: +def torch_gc(force=False): + if shared.opts.disable_gc and not force: return gc.collect() if shared.cmd_opts.use_ipex: diff --git a/modules/esrgan_model.py b/modules/esrgan_model.py index bb4c6619b..f2565ca47 100644 --- a/modules/esrgan_model.py +++ b/modules/esrgan_model.py @@ -156,13 +156,16 @@ class UpscalerESRGAN(Upscaler): def load_model(self, path: str): if "http" in path: - filename = load_file_from_url(url=self.model_url, model_dir=self.model_path, - file_name="%s.pth" % self.model_name, - progress=True) + filename = load_file_from_url( + url=self.model_url, + model_dir=self.model_path, + file_name=f"{self.model_name}.pth", + progress=True, + ) else: filename = path if not os.path.exists(filename) or filename is None: - print("Unable to load %s from %s" % (self.model_path, filename)) + print(f"Unable to load {self.model_path} from {filename}") return None state_dict = torch.load(filename, map_location='cpu' if devices.device_esrgan.type == 'mps' else None) diff --git a/modules/esrgan_model_arch.py b/modules/esrgan_model_arch.py index 411d98d38..af7660ec9 100644 --- a/modules/esrgan_model_arch.py +++ b/modules/esrgan_model_arch.py @@ -36,7 +36,7 @@ class RRDBNet(nn.Module): elif upsample_mode == 'pixelshuffle': upsample_block = pixelshuffle_block else: - raise NotImplementedError('upsample mode [{:s}] is not found'.format(upsample_mode)) + raise NotImplementedError(f'upsample mode [{upsample_mode}] is not found') if upscale == 3: upsampler = upsample_block(nf, nf, 3, act_type=act_type, convtype=convtype) else: @@ -169,7 +169,7 @@ class GaussianNoise(nn.Module): scale = self.sigma * x.detach() if self.is_relative_detach else self.sigma * x sampled_noise = self.noise.repeat(*x.size()).normal_() * scale x = x + sampled_noise - return x + return x def conv1x1(in_planes, out_planes, stride=1): return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False) @@ -259,10 +259,10 @@ class Upsample(nn.Module): def extra_repr(self): if self.scale_factor is not None: - info = 'scale_factor=' + str(self.scale_factor) + info = f'scale_factor={self.scale_factor}' else: - info = 'size=' + str(self.size) - info += ', mode=' + self.mode + info = f'size={self.size}' + info += f', mode={self.mode}' return info @@ -348,7 +348,7 @@ def act(act_type, inplace=True, neg_slope=0.2, n_prelu=1, beta=1.0): elif act_type == 'sigmoid': # [0, 1] range output layer = nn.Sigmoid() else: - raise NotImplementedError('activation layer [{:s}] is not found'.format(act_type)) + raise NotImplementedError(f'activation layer [{act_type}] is not found') return layer @@ -370,7 +370,7 @@ def norm(norm_type, nc): elif norm_type == 'none': def norm_layer(x): return Identity() else: - raise NotImplementedError('normalization layer [{:s}] is not found'.format(norm_type)) + raise NotImplementedError(f'normalization layer [{norm_type}] is not found') return layer @@ -386,7 +386,7 @@ def pad(pad_type, padding): elif pad_type == 'zero': layer = nn.ZeroPad2d(padding) else: - raise NotImplementedError('padding layer [{:s}] is not implemented'.format(pad_type)) + raise NotImplementedError(f'padding layer [{pad_type}] is not implemented') return layer @@ -431,7 +431,7 @@ def conv_block(in_nc, out_nc, kernel_size, stride=1, dilation=1, groups=1, bias= pad_type='zero', norm_type=None, act_type='relu', mode='CNA', convtype='Conv2D', spectral_norm=False): """ Conv layer with padding, normalization, activation """ - assert mode in ['CNA', 'NAC', 'CNAC'], 'Wrong conv mode [{:s}]'.format(mode) + assert mode in ['CNA', 'NAC', 'CNAC'], f'Wrong conv mode [{mode}]' padding = get_valid_padding(kernel_size, dilation) p = pad(pad_type, padding) if pad_type and pad_type != 'zero' else None padding = padding if pad_type == 'zero' else 0 diff --git a/modules/extra_networks_hypernet.py b/modules/extra_networks_hypernet.py index c5c150455..aa2a14efd 100644 --- a/modules/extra_networks_hypernet.py +++ b/modules/extra_networks_hypernet.py @@ -10,7 +10,8 @@ class ExtraNetworkHypernet(extra_networks.ExtraNetwork): additional = shared.opts.sd_hypernetwork if additional != "None" and additional in shared.hypernetworks and len([x for x in params_list if x.items[0] == additional]) == 0: - p.all_prompts = [x + f"" for x in p.all_prompts] + hypernet_prompt_text = f"" + p.all_prompts = [f"{prompt}{hypernet_prompt_text}" for prompt in p.all_prompts] params_list.append(extra_networks.ExtraNetworkParams(items=[additional, shared.opts.extra_networks_default_multiplier])) names = [] diff --git a/modules/generation_parameters_copypaste.py b/modules/generation_parameters_copypaste.py index 8e6eedcaa..2d6c64951 100644 --- a/modules/generation_parameters_copypaste.py +++ b/modules/generation_parameters_copypaste.py @@ -51,6 +51,7 @@ def image_from_url_text(filedata): filename = filedata["name"] is_in_right_dir = ui_tempdir.check_tmp_file(shared.demo, filename) if is_in_right_dir: + filename = filename.rsplit('?', 1)[0] image = Image.open(filename) geninfo, _items = images.read_info_from_image(image) image.info['parameters'] = geninfo @@ -134,6 +135,7 @@ def connect_paste_params_buttons(): _js=jsfunc, inputs=[binding.source_image_component], outputs=[destination_image_component, destination_width_component, destination_height_component] if destination_width_component else [destination_image_component], + show_progress=False, ) if binding.source_text_component is not None and fields is not None: connect_paste(binding.paste_button, fields, binding.source_text_component, override_settings_component, binding.tabname) @@ -149,6 +151,7 @@ def connect_paste_params_buttons(): _js=f"switch_to_{binding.tabname}", inputs=[], outputs=[], + show_progress=False, ) @@ -257,8 +260,8 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model v = v[1:-1] if v[0] == '"' and v[-1] == '"' else v m = re_imagesize.match(v) if m is not None: - res[k+"-1"] = m.group(1) - res[k+"-2"] = m.group(2) + res[f"{k}-1"] = m.group(1) + res[f"{k}-2"] = m.group(2) else: res[k] = v # Missing CLIP skip means it was set to 1 (the default) @@ -404,10 +407,12 @@ def connect_paste(button, local_paste_fields, input_comp, override_settings_comp fn=paste_func, inputs=[input_comp], outputs=[x[0] for x in local_paste_fields], + show_progress=False, ) button.click( fn=None, _js=f"recalculate_prompts_{tabname}", inputs=[], outputs=[], + show_progress=False, ) diff --git a/modules/hashes.py b/modules/hashes.py index b8f00f74f..2a7c7aed3 100644 --- a/modules/hashes.py +++ b/modules/hashes.py @@ -11,7 +11,7 @@ cache_data = None def dump_cache(): - with filelock.FileLock(cache_filename+".lock"): + with filelock.FileLock(f"{cache_filename}.lock"): with open(cache_filename, "w", encoding="utf8") as file: json.dump(cache_data, file, indent=4) @@ -19,7 +19,7 @@ def dump_cache(): def cache(subsection): global cache_data # pylint: disable=global-statement if cache_data is None: - with filelock.FileLock(cache_filename+".lock"): + with filelock.FileLock(f"{cache_filename}.lock"): if not os.path.isfile(cache_filename): cache_data = {} else: diff --git a/modules/hypernetworks/hypernetwork.py b/modules/hypernetworks/hypernetwork.py index d13b811d5..b05ee2db9 100644 --- a/modules/hypernetworks/hypernetwork.py +++ b/modules/hypernetworks/hypernetwork.py @@ -235,7 +235,7 @@ class Hypernetwork: if shared.opts.save_optimizer_state and self.optimizer_state_dict: optimizer_saved_dict['hash'] = self.shorthash() optimizer_saved_dict['optimizer_state_dict'] = self.optimizer_state_dict - torch.save(optimizer_saved_dict, filename + '.optim') + torch.save(optimizer_saved_dict, f"{filename}.optim") def load(self, filename): self.filename = filename diff --git a/modules/images.py b/modules/images.py index b407ef9cf..8f782bcdd 100644 --- a/modules/images.py +++ b/modules/images.py @@ -308,6 +308,7 @@ class FilenameGenerator: 'generation_number': lambda self: NOTHING_AND_SKIP_PREVIOUS_TEXT if self.p.n_iter == 1 and self.p.batch_size == 1 else self.p.iteration * self.p.batch_size + self.p.batch_index + 1, 'hasprompt': lambda self, *args: self.hasprompt(*args), # accepts formats:[hasprompt..] 'clip_skip': lambda self: shared.opts.data["CLIP_stop_at_last_layers"], + 'denoising': lambda self: self.p.denoising_strength if self.p and self.p.denoising_strength else NOTHING_AND_SKIP_PREVIOUS_TEXT, } default_time_format = '%Y%m%d%H%M%S' @@ -401,7 +402,7 @@ def get_next_sequence_number(path, basename): """ result = -1 if basename != '': - basename = basename + "-" + basename = f"{basename}-" prefix_length = len(basename) for p in os.listdir(path): if p.startswith(basename): @@ -448,7 +449,7 @@ def atomically_save_image(): # additional metadata saved in files if shared.opts.save_txt and len(exifinfo_data) > 0: with open(txt_fullfn, "w", encoding="utf8") as file: - file.write(exifinfo_data + "\n") + file.write(f"{exifinfo_data}\n") with open(os.path.join(paths.data_path, "params.txt"), "w", encoding="utf8") as file: file.write(exifinfo_data) if shared.opts.save_log_fn != '' and len(exifinfo_data) > 0: @@ -524,7 +525,7 @@ def save_image(image, path, basename, seed=None, prompt=None, extension='jpg', i file_decoration = shared.opts.samples_filename_pattern or "[seed]-[prompt_spaces]" add_number = shared.opts.save_images_add_number or file_decoration == '' if file_decoration != "" and add_number: - file_decoration = "-" + file_decoration + file_decoration = f"-{file_decoration}" file_decoration = namegen.apply(file_decoration) + suffix if add_number: basecount = get_next_sequence_number(path, basename) diff --git a/modules/img2img.py b/modules/img2img.py index 34c0dd168..6fbdaa332 100644 --- a/modules/img2img.py +++ b/modules/img2img.py @@ -33,7 +33,8 @@ def process_batch(p, input_dir, output_dir, inpaint_mask_dir, args): break try: img = Image.open(image) - except UnidentifiedImageError: + except UnidentifiedImageError as e: + shared.log.error(f"Image error: {e}") continue # Use the EXIF orientation of photos taken by smartphones. img = ImageOps.exif_transpose(img) diff --git a/modules/interrogate.py b/modules/interrogate.py index 91c00e129..0a9613b4b 100644 --- a/modules/interrogate.py +++ b/modules/interrogate.py @@ -26,7 +26,7 @@ def category_types(): def download_default_clip_interrogate_categories(content_dir): shared.log.info("Downloading CLIP categories...") - tmpdir = content_dir + "_tmp" + tmpdir = f"{content_dir}_tmp" cat_types = ["artists", "flavors", "mediums", "movements"] try: @@ -211,7 +211,7 @@ class InterrogateModels: if shared.opts.interrogate_return_ranks: res += f", ({match}:{score/100:.3f})" else: - res += ", " + match + res += f", {match}" except Exception as e: errors.display(e, 'interrogate') diff --git a/modules/mac_specific.py b/modules/mac_specific.py index 9e3d13243..c4e26784b 100644 --- a/modules/mac_specific.py +++ b/modules/mac_specific.py @@ -53,6 +53,11 @@ if has_mps: CondFunc('torch.cumsum', cumsum_fix_func, None) CondFunc('torch.Tensor.cumsum', cumsum_fix_func, None) CondFunc('torch.narrow', lambda orig_func, *args, **kwargs: orig_func(*args, **kwargs).clone(), None) - if version.parse(torch.__version__) == version.parse("2.0"): + # MPS workaround for https://github.com/pytorch/pytorch/issues/96113 - CondFunc('torch.nn.functional.layer_norm', lambda orig_func, x, normalized_shape, weight, bias, eps, **kwargs: orig_func(x.float(), normalized_shape, weight.float() if weight is not None else None, bias.float() if bias is not None else bias, eps).to(x.dtype), lambda *args, **kwargs: len(args) == 6) + CondFunc('torch.nn.functional.layer_norm', lambda orig_func, x, normalized_shape, weight, bias, eps, **kwargs: orig_func(x.float(), normalized_shape, weight.float() if weight is not None else None, bias.float() if bias is not None else bias, eps).to(x.dtype), lambda _, input, *args, **kwargs: len(args) == 4 and input.device.type == 'mps') + + # MPS workaround for https://github.com/pytorch/pytorch/issues/92311 + if platform.processor() == 'i386': + for funcName in ['torch.argmax', 'torch.Tensor.argmax']: + CondFunc(funcName, lambda _, input, *args, **kwargs: torch.max(input.float() if input.dtype == torch.int64 else input, *args, **kwargs)[1], lambda _, input, *args, **kwargs: input.device.type == 'mps') diff --git a/modules/middleware.py b/modules/middleware.py index 7e29a2c91..1ea34340e 100644 --- a/modules/middleware.py +++ b/modules/middleware.py @@ -1,3 +1,4 @@ +import ssl import time import datetime import logging @@ -17,6 +18,7 @@ errors.install() def setup_middleware(app: FastAPI, cmd_opts): log.info('Initializing middleware') + ssl._create_default_https_context = ssl._create_unverified_context # pylint: disable=protected-access uvicorn_logger=logging.getLogger("uvicorn.error") uvicorn_logger.disabled = True from fastapi.middleware.cors import CORSMiddleware diff --git a/modules/modelloader.py b/modules/modelloader.py index dce51549c..831de6631 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -22,9 +22,6 @@ def load_models(model_path: str, model_url: str = None, command_path: str = None """ output = [] - if ext_filter is None: - ext_filter = [] - try: places = [] @@ -39,22 +36,14 @@ def load_models(model_path: str, model_url: str = None, command_path: str = None places.append(model_path) for place in places: - if os.path.exists(place): - for file in glob.iglob(os.path.join(place, '**/**'), recursive=True): - full_path = file - if os.path.isdir(full_path): - continue - if os.path.islink(full_path) and not os.path.exists(full_path): - print(f"Skipping broken symlink: {full_path}") - continue - if ext_blacklist is not None and any([full_path.endswith(x) for x in ext_blacklist]): - continue - if len(ext_filter) != 0: - _model_name, extension = os.path.splitext(file) - if extension not in ext_filter: - continue - if file not in output: - output.append(full_path) + for full_path in shared.walk_files(place, allowed_extensions=ext_filter): + if os.path.islink(full_path) and not os.path.exists(full_path): + print(f"Skipping broken symlink: {full_path}") + continue + if ext_blacklist is not None and any([full_path.endswith(x) for x in ext_blacklist]): + continue + if full_path not in output: + output.append(full_path) if model_url is not None and len(output) == 0: if download_name is not None: @@ -131,20 +120,6 @@ def move_files(src_path: str, dest_path: str, ext_filter: str = None): pass -builtin_upscaler_classes = [] -forbidden_upscaler_classes = set() - - -def list_builtin_upscalers(): - load_upscalers() - builtin_upscaler_classes.clear() - builtin_upscaler_classes.extend(Upscaler.__subclasses__()) - -def forbid_loaded_nonbuiltin_upscalers(): - for cls in Upscaler.__subclasses__(): - if cls not in builtin_upscaler_classes: - forbidden_upscaler_classes.add(cls) - def load_upscalers(): # We can only do this 'magic' method to dynamically load upscalers if they are referenced, @@ -161,10 +136,16 @@ def load_upscalers(): datas = [] commandline_options = vars(shared.cmd_opts) - for cls in Upscaler.__subclasses__(): - if cls in forbidden_upscaler_classes: - continue + # some of upscaler classes will not go away after reloading their modules, and we'll end + # up with two copies of those classes. The newest copy will always be the last in the list, + # so we go from end to beginning and ignore duplicates + used_classes = {} + for cls in reversed(Upscaler.__subclasses__()): + classname = str(cls) + if classname not in used_classes: + used_classes[classname] = cls + for cls in reversed(used_classes.values()): name = cls.__name__ cmd_name = f"{name.lower().replace('upscaler', '')}_models_path" scaler = cls(commandline_options.get(cmd_name, None)) diff --git a/modules/models/diffusion/ddpm_edit.py b/modules/models/diffusion/ddpm_edit.py index f3d49c44c..f880bc3c7 100644 --- a/modules/models/diffusion/ddpm_edit.py +++ b/modules/models/diffusion/ddpm_edit.py @@ -223,7 +223,7 @@ class DDPM(pl.LightningModule): for k in keys: for ik in ignore_keys: if k.startswith(ik): - print("Deleting key {} from state_dict.".format(k)) + print(f"Deleting key {k} from state_dict.") del sd[k] missing, unexpected = self.load_state_dict(sd, strict=False) if not only_model else self.model.load_state_dict( sd, strict=False) @@ -386,7 +386,7 @@ class DDPM(pl.LightningModule): _, loss_dict_no_ema = self.shared_step(batch) with self.ema_scope(): _, loss_dict_ema = self.shared_step(batch) - loss_dict_ema = {key + '_ema': loss_dict_ema[key] for key in loss_dict_ema} + loss_dict_ema = {f"{key}_ema": loss_dict_ema[key] for key in loss_dict_ema} self.log_dict(loss_dict_no_ema, prog_bar=False, logger=True, on_step=False, on_epoch=True) self.log_dict(loss_dict_ema, prog_bar=False, logger=True, on_step=False, on_epoch=True) diff --git a/modules/models/diffusion/uni_pc/uni_pc.py b/modules/models/diffusion/uni_pc/uni_pc.py index fc78bd42d..4df51e587 100644 --- a/modules/models/diffusion/uni_pc/uni_pc.py +++ b/modules/models/diffusion/uni_pc/uni_pc.py @@ -95,7 +95,7 @@ class NoiseScheduleVP: """ if schedule not in ['discrete', 'linear', 'cosine']: - raise ValueError("Unsupported noise schedule {}. The schedule needs to be 'discrete' or 'linear' or 'cosine'".format(schedule)) + raise ValueError(f"Unsupported noise schedule {schedule}. The schedule needs to be 'discrete' or 'linear' or 'cosine'") self.schedule = schedule if schedule == 'discrete': @@ -382,7 +382,7 @@ def get_time_steps(noise_schedule, skip_type, t_T, t_0, N, device): t = torch.linspace(t_T**(1. / t_order), t_0**(1. / t_order), N + 1).pow(t_order).to(device) return t else: - raise ValueError("Unsupported skip_type {}, need to be 'logSNR' or 'time_uniform' or 'time_quadratic'".format(skip_type)) + raise ValueError(f"Unsupported skip_type {skip_type}, need to be 'logSNR' or 'time_uniform' or 'time_quadratic'") class UniPC: def __init__( diff --git a/modules/paths.py b/modules/paths.py index 0bda3a780..ca84d6c93 100644 --- a/modules/paths.py +++ b/modules/paths.py @@ -23,7 +23,7 @@ for possible_sd_path in possible_sd_paths: sd_path = os.path.abspath(possible_sd_path) break -assert sd_path is not None, "Couldn't find Stable Diffusion in any of: " + str(possible_sd_paths) +assert sd_path is not None, f"Couldn't find Stable Diffusion in any of: {possible_sd_paths}" path_dirs = [ (sd_path, 'ldm', 'Stable Diffusion', []), diff --git a/modules/processing.py b/modules/processing.py index 9931f6d78..fdeb5cd75 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -14,6 +14,7 @@ from ldm.data.util import AddMiDaS from ldm.models.diffusion.ddpm import LatentDepth2ImageDiffusion from einops import repeat, rearrange from blendmodes.blend import blendLayers, BlendType +from installer import git_commit import modules.sd_hijack from modules import devices, prompt_parser, masking, sd_samplers, lowvram, generation_parameters_copypaste, script_callbacks, extra_networks, sd_vae_approx, scripts # pylint: disable=unused-import from modules.sd_hijack import model_hijack @@ -40,56 +41,41 @@ def setup_color_correction(image): def apply_color_correction(correction, original_image): logging.info("Applying color correction.") image = Image.fromarray(cv2.cvtColor(exposure.match_histograms( - cv2.cvtColor( - np.asarray(original_image), - cv2.COLOR_RGB2LAB - ), + cv2.cvtColor(np.asarray(original_image), cv2.COLOR_RGB2LAB), correction, channel_axis=2 ), cv2.COLOR_LAB2RGB).astype("uint8")) - image = blendLayers(image, original_image, BlendType.LUMINOSITY) - return image def apply_overlay(image, paste_loc, index, overlays): if overlays is None or index >= len(overlays): return image - overlay = overlays[index] - if paste_loc is not None: x, y, w, h = paste_loc base_image = Image.new('RGBA', (overlay.width, overlay.height)) image = images.resize_image(1, image, w, h) base_image.paste(image, (x, y)) image = base_image - image = image.convert('RGBA') image.alpha_composite(overlay) image = image.convert('RGB') - return image def txt2img_image_conditioning(sd_model, x, width, height): if sd_model.model.conditioning_key in {'hybrid', 'concat'}: # Inpainting models - # The "masked-image" in this case will just be all zeros since the entire image is masked. image_conditioning = torch.zeros(x.shape[0], 3, height, width, device=x.device) image_conditioning = sd_model.get_first_stage_encoding(sd_model.encode_first_stage(image_conditioning)) - # Add the fake full 1s mask to the first dimension. image_conditioning = torch.nn.functional.pad(image_conditioning, (0, 0, 0, 0, 1, 0), value=1.0) image_conditioning = image_conditioning.to(x.dtype) - return image_conditioning - elif sd_model.model.conditioning_key == "crossattn-adm": # UnCLIP models - return x.new_zeros(x.shape[0], 2*sd_model.noise_augmentor.time_embed.dim, dtype=x.dtype, device=x.device) - else: # Dummy zero conditioning if we're not using inpainting or unclip models. # Still takes up a bit of memory, but no encoder call. @@ -165,7 +151,6 @@ class StableDiffusionProcessing: def txt2img_image_conditioning(self, x, width=None, height=None): self.is_using_inpainting_conditioning = self.sd_model.model.conditioning_key in {'hybrid', 'concat'} - return txt2img_image_conditioning(self.sd_model, x, width or self.width, height or self.height) def depth2img_image_conditioning(self, source_image): @@ -174,7 +159,6 @@ class StableDiffusionProcessing: transformed = transformer({"jpg": rearrange(source_image[0], "c h w -> h w c")}) midas_in = torch.from_numpy(transformed["midas_in"][None, ...]).to(device=shared.device) midas_in = repeat(midas_in, "1 ... -> n ...", n=self.batch_size) - conditioning_image = self.sd_model.get_first_stage_encoding(self.sd_model.encode_first_stage(source_image)) conditioning = torch.nn.functional.interpolate( self.sd_model.depth_model(midas_in), @@ -182,14 +166,12 @@ class StableDiffusionProcessing: mode="bicubic", align_corners=False, ) - (depth_min, depth_max) = torch.aminmax(conditioning) conditioning = 2. * (conditioning - depth_min) / (depth_max - depth_min) - 1. return conditioning def edit_image_conditioning(self, source_image): conditioning_image = self.sd_model.encode_first_stage(source_image).mode() - return conditioning_image def unclip_image_conditioning(self, source_image): @@ -202,7 +184,6 @@ class StableDiffusionProcessing: def inpainting_image_conditioning(self, source_image, latent_image, image_mask=None): self.is_using_inpainting_conditioning = True - # Handle the different mask inputs if image_mask is not None: if torch.is_tensor(image_mask): @@ -216,7 +197,6 @@ class StableDiffusionProcessing: conditioning_mask = torch.round(conditioning_mask) else: conditioning_mask = source_image.new_ones(1, 1, *source_image.shape[-2:]) - # Create another latent image, this time with a masked version of the original input. # Smoothly interpolate between the masked and unmasked latent conditioning image using a parameter. conditioning_mask = conditioning_mask.to(device=source_image.device, dtype=source_image.dtype) @@ -225,35 +205,27 @@ class StableDiffusionProcessing: source_image * (1.0 - conditioning_mask), getattr(self, "inpainting_mask_weight", shared.opts.inpainting_mask_weight) ) - # Encode the new masked image using first stage of network. conditioning_image = self.sd_model.get_first_stage_encoding(self.sd_model.encode_first_stage(conditioning_image)) - # Create the concatenated conditioning tensor to be fed to `c_concat` conditioning_mask = torch.nn.functional.interpolate(conditioning_mask, size=latent_image.shape[-2:]) conditioning_mask = conditioning_mask.expand(conditioning_image.shape[0], -1, -1, -1) image_conditioning = torch.cat([conditioning_mask, conditioning_image], dim=1) image_conditioning = image_conditioning.to(shared.device).type(self.sd_model.dtype) - return image_conditioning def img2img_image_conditioning(self, source_image, latent_image, image_mask=None): source_image = devices.cond_cast_float(source_image) - # HACK: Using introspection as the Depth2Image model doesn't appear to uniquely # identify itself with a field common to all models. The conditioning_key is also hybrid. if isinstance(self.sd_model, LatentDepth2ImageDiffusion): return self.depth2img_image_conditioning(source_image) - if self.sd_model.cond_stage_key == "edit": return self.edit_image_conditioning(source_image) - if self.sampler.conditioning_key in {'hybrid', 'concat'}: return self.inpainting_image_conditioning(source_image, latent_image, image_mask=image_mask) - if self.sampler.conditioning_key == "crossattn-adm": return self.unclip_image_conditioning(source_image) - # Dummy zero conditioning if we're not using inpainting or depth model. return latent_image.new_zeros(latent_image.shape[0], 5, 1, 1) @@ -344,7 +316,6 @@ class Processed: "clip_skip": self.clip_skip, "is_using_inpainting_conditioning": self.is_using_inpainting_conditioning, } - return json.dumps(obj) def infotext(self, p: StableDiffusionProcessing, index): @@ -468,6 +439,7 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts, all_seeds, all_su "Clip skip": p.clip_skip, "ENSD": None if opts.eta_noise_seed_delta == 0 else opts.eta_noise_seed_delta, "Init image hash": getattr(p, 'init_img_hash', None), + "Version": git_commit, "Token merging ratio": None if not (opts.token_merging or cmd_opts.token_merging) or opts.token_merging_hr_only else opts.token_merging_ratio, "Token merging ratio hr": None if not (opts.token_merging or cmd_opts.token_merging) else opts.token_merging_ratio_hr, "Token merging random": None if opts.token_merging_random is False else opts.token_merging_random, @@ -479,7 +451,7 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts, all_seeds, all_su } generation_params.update(p.extra_generation_params) generation_params_text = ", ".join([k if k == v else f'{k}: {generation_parameters_copypaste.quote(v)}' for k, v in generation_params.items() if v is not None]) - negative_prompt_text = "\nNegative prompt: " + p.all_negative_prompts[index] if p.all_negative_prompts[index] else "" + negative_prompt_text = f"\nNegative prompt: {p.all_negative_prompts[index]}" if p.all_negative_prompts[index] else "" return f"{all_prompts[index]}{negative_prompt_text}\n{generation_params_text}".strip() @@ -726,7 +698,16 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if not p.disable_extra_networks and extra_network_data: extra_networks.deactivate(p, extra_network_data) devices.torch_gc() - res = Processed(p, output_images, p.all_seeds[0], infotext(), comments="".join(["\n\n" + x for x in comments]), subseed=p.all_subseeds[0], index_of_first_image=index_of_first_image, infotexts=infotexts) + res = Processed( + p, + images_list=output_images, + seed=p.all_seeds[0], + info=infotext(), + comments="".join(f"\n\n{comment}" for comment in comments), + subseed=p.all_subseeds[0], + index_of_first_image=index_of_first_image, + infotexts=infotexts, + ) if p.scripts is not None: p.scripts.postprocess(p, res) return res diff --git a/modules/realesrgan_model.py b/modules/realesrgan_model.py index 75dccf1ac..db05e8b1a 100644 --- a/modules/realesrgan_model.py +++ b/modules/realesrgan_model.py @@ -1,12 +1,10 @@ import os import sys - import numpy as np from PIL import Image from basicsr.utils.download_util import load_file_from_url - from modules.upscaler import Upscaler, UpscalerData -from modules.shared import cmd_opts, opts, device +from modules.shared import opts, device from modules import modelloader import modules.errors as errors @@ -27,9 +25,9 @@ class UpscalerRealESRGAN(Upscaler): for scaler in scalers: if scaler.local_data_path.startswith("http"): filename = modelloader.friendly_name(scaler.local_data_path) - local = next(iter([local_model for local_model in local_model_paths if local_model.endswith(filename + '.pth')]), None) - if local: - scaler.local_data_path = local + local_model_candidates = [local_model for local_model in local_model_paths if local_model.endswith(f"{filename}.pth")] + if local_model_candidates: + scaler.local_data_path = local_model_candidates[0] if scaler.name in opts.realesrgan_enabled_models: self.scalers.append(scaler) diff --git a/modules/safe.py b/modules/safe.py index 483b85a90..4cc5a10f5 100644 --- a/modules/safe.py +++ b/modules/safe.py @@ -38,7 +38,7 @@ class RestrictedUnpickler(pickle.Unpickler): return getattr(collections, name) if module == 'torch._utils' and name in ['_rebuild_tensor_v2', '_rebuild_parameter', '_rebuild_device_tensor_from_numpy']: return getattr(torch._utils, name) # pylint: disable=protected-access - if module == 'torch' and name in ['FloatStorage', 'HalfStorage', 'IntStorage', 'LongStorage', 'DoubleStorage', 'ByteStorage', 'float32']: + if module == 'torch' and name in ['FloatStorage', 'HalfStorage', 'IntStorage', 'LongStorage', 'DoubleStorage', 'ByteStorage', 'float32', 'BFloat16Storage']: return getattr(torch, name) if module == 'torch.nn.modules.container' and name in ['ParameterDict']: return getattr(torch.nn.modules.container, name) diff --git a/modules/scripts.py b/modules/scripts.py index 3c168d2d5..ef173ed5c 100644 --- a/modules/scripts.py +++ b/modules/scripts.py @@ -142,7 +142,8 @@ class Script: def elem_id(self, item_id): """helper function to generate id for a HTML element, constructs final id out of script name, tab and user-supplied item_id""" need_tabname = self.show(True) == self.show(False) - tabname = ('img2img' if self.is_img2img else 'txt2txt') + "_" if need_tabname else "" + tabkind = 'img2img' if self.is_img2img else 'txt2txt' + tabname = f"{tabkind}_" if need_tabname else "" title = re.sub(r'[^a-z_0-9]', '', re.sub(r'\s', '_', self.title().lower())) return f'script_{tabname}{title}_{item_id}' @@ -481,7 +482,7 @@ def add_classes_to_gradio_component(comp): elem_classes = comp.elem_classes if elem_classes is None: elem_classes = [] - comp.elem_classes = ["gradio-" + comp.get_block_name(), *(elem_classes)] + comp.elem_classes = [f"gradio-{comp.get_block_name()}", *(comp.elem_classes or [])] if getattr(comp, 'multiselect', False): comp.elem_classes.append('multiselect') diff --git a/modules/sd_hijack.py b/modules/sd_hijack.py index 2a5f26662..a783ea51a 100644 --- a/modules/sd_hijack.py +++ b/modules/sd_hijack.py @@ -237,8 +237,9 @@ class StableDiffusionModelHijack: self.comments = [] def get_prompt_lengths(self, text): + if self.clip is None: + return 0, 0 _, token_count = self.clip.process_texts([text]) - return token_count, self.clip.get_target_prompt_token_count(token_count) diff --git a/modules/sd_hijack_clip_old.py b/modules/sd_hijack_clip_old.py index 6d9fbbe6c..a3476e956 100644 --- a/modules/sd_hijack_clip_old.py +++ b/modules/sd_hijack_clip_old.py @@ -75,7 +75,8 @@ def forward_old(self: sd_hijack_clip.FrozenCLIPEmbedderWithCustomWordsBase, text self.hijack.comments += hijack_comments if len(used_custom_terms) > 0: - self.hijack.comments.append("Used embeddings: " + ", ".join([f'{word} [{checksum}]' for word, checksum in used_custom_terms])) + embedding_names = ", ".join(f"{word} [{checksum}]" for word, checksum in used_custom_terms) + self.hijack.comments.append(f"Used embeddings: {embedding_names}") self.hijack.fixes = hijack_fixes return self.process_tokens(remade_batch_tokens, batch_multipliers) diff --git a/modules/sd_hijack_optimizations.py b/modules/sd_hijack_optimizations.py index 596162782..e8c8ce763 100644 --- a/modules/sd_hijack_optimizations.py +++ b/modules/sd_hijack_optimizations.py @@ -275,6 +275,9 @@ def sub_quad_attention_forward(self, x, context=None, mask=None): k = k.unflatten(-1, (h, -1)).transpose(1,2).flatten(end_dim=1) v = v.unflatten(-1, (h, -1)).transpose(1,2).flatten(end_dim=1) + if q.device.type == 'mps': + q, k, v = q.contiguous(), k.contiguous(), v.contiguous() + dtype = q.dtype if shared.opts.upcast_attn: q, k = q.float(), k.float() diff --git a/modules/sd_hijack_unet.py b/modules/sd_hijack_unet.py index 7ff553ae3..252e8e5fc 100644 --- a/modules/sd_hijack_unet.py +++ b/modules/sd_hijack_unet.py @@ -19,7 +19,7 @@ class TorchHijackForUnet: if hasattr(torch, item): return getattr(torch, item) - raise AttributeError("'{}' object has no attribute '{}'".format(type(self).__name__, item)) + raise AttributeError(f"'{type(self).__name__}' object has no attribute '{item}'") def cat(self, tensors, *args, **kwargs): if len(tensors) == 2: diff --git a/modules/sd_models.py b/modules/sd_models.py index 107e771e7..2db6ee587 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -2,6 +2,7 @@ import collections import os.path import re import io +import threading from os import mkdir from urllib import request from rich import progress # pylint: disable=redefined-builtin @@ -41,7 +42,7 @@ class CheckpointInfo: self.name_for_extra = os.path.splitext(os.path.basename(filename))[0] self.model_name = os.path.splitext(name.replace("/", "_").replace("\\", "_"))[0] self.hash = model_hash(filename) - self.sha256 = hashes.sha256_from_cache(self.filename, "checkpoint/" + name) + self.sha256 = hashes.sha256_from_cache(self.filename, f"checkpoint/{name}") self.shorthash = self.sha256[0:10] if self.sha256 else None self.title = name if self.shorthash is None else f'{name} [{self.shorthash}]' self.ids = [self.hash, self.model_name, self.title, name, f'{name} [{self.hash}]'] + ([self.shorthash, self.sha256, f'{self.name} [{self.shorthash}]'] if self.shorthash else []) @@ -59,7 +60,7 @@ class CheckpointInfo: checkpoint_aliases[i] = self def calculate_shorthash(self): - self.sha256 = hashes.sha256(self.filename, "checkpoint/" + self.name) + self.sha256 = hashes.sha256(self.filename, f"checkpoint/{self.name}") if self.sha256 is None: return self.shorthash = self.sha256[0:10] @@ -349,6 +350,29 @@ sd1_clip_weight = 'cond_stage_model.transformer.text_model.embeddings.token_embe sd2_clip_weight = 'cond_stage_model.model.transformer.resblocks.0.attn.in_proj_weight' +class SdModelData: + def __init__(self): + self.sd_model = None + self.lock = threading.Lock() + + def get_sd_model(self): + if self.sd_model is None: + with self.lock: + try: + load_model() + except Exception as e: + shared.log.error("Failed to load stable diffusion model") + errors.display(e, "loading stable diffusion model") + self.sd_model = None + return self.sd_model + + def set_sd_model(self, v): + self.sd_model = v + + +model_data = SdModelData() + + def load_model(checkpoint_info=None, already_loaded_state_dict=None, timer=None): shared.log.debug(f'Load model: info={checkpoint_info is not None} dict={already_loaded_state_dict is not None}') from modules import lowvram, sd_hijack @@ -358,9 +382,11 @@ def load_model(checkpoint_info=None, already_loaded_state_dict=None, timer=None) if timer is None: timer = Timer() current_checkpoint_info = None - if shared.sd_model: - current_checkpoint_info = shared.sd_model.sd_checkpoint_info + if model_data.sd_model is not None: + sd_hijack.model_hijack.undo_hijack(model_data.sd_model) + current_checkpoint_info = model_data.sd_model.sd_checkpoint_info unload_model_weights() + model_data.sd_model = None do_inpainting_hijack() devices.set_cuda_params() if already_loaded_state_dict is not None: @@ -405,14 +431,14 @@ def load_model(checkpoint_info=None, already_loaded_state_dict=None, timer=None) if shared.cmd_opts.use_ipex: sd_model = torch.xpu.optimize(sd_model, dtype=devices.dtype) shared.log.info("Applied IPEX Optimize") - shared.sd_model = sd_model + model_data.sd_model = sd_model sd_hijack.model_hijack.embedding_db.load_textual_inversion_embeddings(force_reload=True) # Reload embeddings after model load as they may or may not fit the model timer.record("embeddings") script_callbacks.model_loaded_callback(sd_model) timer.record("callbacks") shared.log.info(f"Model loaded in {timer.summary()}") current_checkpoint_info = None - devices.torch_gc() + devices.torch_gc(force=True) shared.log.info(f'Model load finished: {memory_stats()}') @@ -426,7 +452,7 @@ def reload_model_weights(sd_model=None, info=None): from modules import lowvram, sd_hijack checkpoint_info = info or select_checkpoint() if not sd_model: - sd_model = shared.sd_model + sd_model = model_data.sd_model if sd_model is None: # previous model load failed current_checkpoint_info = None else: @@ -443,7 +469,6 @@ def reload_model_weights(sd_model=None, info=None): else: unload_model_weights() sd_model = None - shared.sd_model = None timer = Timer() state_dict = get_checkpoint_state_dict(checkpoint_info, timer) checkpoint_config = sd_models_config.find_checkpoint_config(state_dict, checkpoint_info) @@ -452,7 +477,7 @@ def reload_model_weights(sd_model=None, info=None): del sd_model checkpoints_loaded.clear() load_model(checkpoint_info, already_loaded_state_dict=state_dict, timer=timer) - return shared.sd_model + return model_data.sd_model try: load_model_weights(sd_model, checkpoint_info, state_dict, timer) except Exception: @@ -471,14 +496,12 @@ def reload_model_weights(sd_model=None, info=None): def unload_model_weights(sd_model=None, _info=None): from modules import sd_hijack - if shared.sd_model: - # shared.sd_model.cond_stage_model.to(devices.cpu) - # shared.sd_model.first_stage_model.to(devices.cpu) - shared.sd_model.to(devices.cpu) - sd_hijack.model_hijack.undo_hijack(shared.sd_model) - shared.sd_model = None + if model_data.sd_model: + model_data.sd_model.to(devices.cpu) + sd_hijack.model_hijack.undo_hijack(model_data.sd_model) + model_data.sd_model = None sd_model = None - devices.torch_gc() + devices.torch_gc(force=True) shared.log.debug(f'Model weights unloaded: {memory_stats()}') return sd_model diff --git a/modules/sd_models_config.py b/modules/sd_models_config.py index a9c515b14..819bebd34 100644 --- a/modules/sd_models_config.py +++ b/modules/sd_models_config.py @@ -109,7 +109,7 @@ def find_checkpoint_config_near_filename(info): if info is None: return None - config = os.path.splitext(info.filename)[0] + ".yaml" + config = f"{os.path.splitext(info.filename)[0]}.yaml" if os.path.exists(config): return config diff --git a/modules/sd_samplers_kdiffusion.py b/modules/sd_samplers_kdiffusion.py index 8b4bf0652..e83850da2 100644 --- a/modules/sd_samplers_kdiffusion.py +++ b/modules/sd_samplers_kdiffusion.py @@ -206,7 +206,7 @@ class TorchHijack: if hasattr(torch, item): return getattr(torch, item) - raise AttributeError("'{}' object has no attribute '{}'".format(type(self).__name__, item)) + raise AttributeError(f"'{type(self).__name__}' object has no attribute '{item}'") def randn_like(self, x): if self.sampler_noises: diff --git a/modules/sd_vae.py b/modules/sd_vae.py index 27bfb070d..6b8a9c6f8 100644 --- a/modules/sd_vae.py +++ b/modules/sd_vae.py @@ -82,7 +82,7 @@ def refresh_vae_list(): def find_vae_near_checkpoint(checkpoint_file): checkpoint_path = os.path.splitext(checkpoint_file)[0] - for vae_location in [checkpoint_path + ".vae.pt", checkpoint_path + ".vae.ckpt", checkpoint_path + ".vae.safetensors"]: + for vae_location in [f"{checkpoint_path}.vae.pt", f"{checkpoint_path}.vae.ckpt", f"{checkpoint_path}.vae.safetensors"]: if os.path.isfile(vae_location): return vae_location diff --git a/modules/shared.py b/modules/shared.py index 832f9b43c..62e12cbf1 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -7,6 +7,7 @@ import urllib.request import gradio as gr import tqdm import requests +from ldm.models.diffusion.ddpm import LatentDiffusion from modules import errors, ui_components, shared_items, cmd_args from modules.paths_internal import models_path, script_path, data_path, sd_configs_path, sd_default_config, sd_model_file, default_sd_model_file, extensions_dir, extensions_builtin_dir # pylint: disable=W0611 import modules.interrogate @@ -27,7 +28,6 @@ cmd_opts, _ = parser.parse_known_args() hide_dirs = {"visible": not cmd_opts.hide_ui_dir_config} is_device_dml = False xformers_available = False -sd_model = None clip_model = None interrogator = modules.interrogate.InterrogateModels("interrogate") sd_upscalers = [] @@ -411,7 +411,7 @@ options_templates.update(options_section(('ui', "User interface"), { "keyedit_precision_attention": OptionInfo(0.1, "Ctrl+up/down precision when editing (attention:1.1)", gr.Slider, {"minimum": 0.01, "maximum": 0.2, "step": 0.001}), "keyedit_precision_extra": OptionInfo(0.05, "Ctrl+up/down precision when editing ", gr.Slider, {"minimum": 0.01, "maximum": 0.2, "step": 0.001}), "keyedit_delimiters": OptionInfo(".,\/!?%^*;:{}=`~()", "Ctrl+up/down word delimiters"), # pylint: disable=anomalous-backslash-in-string - "quicksettings": OptionInfo("sd_model_checkpoint", "Quicksettings list"), + "quicksettings_list": OptionInfo(["sd_model_checkpoint"], "Quicksettings list", ui_components.DropdownMulti, lambda: {"choices": list(opts.data_labels.keys())}), "hidden_tabs": OptionInfo([], "Hidden UI tabs", ui_components.DropdownMulti, lambda: {"choices": [x for x in tab_names]}), "ui_tab_reorder": OptionInfo("From Text, From Image, Process Image", "UI tabs order"), "ui_scripts_reorder": OptionInfo("Enable Dynamic Thresholding, ControlNet", "UI scripts order"), @@ -550,6 +550,8 @@ class Options: def load(self, filename): with open(filename, "r", encoding="utf8") as file: self.data = json.load(file) + if self.data.get('quicksettings') is not None and self.data.get('quicksettings_list') is None: + self.data['quicksettings_list'] = [i.strip() for i in self.data.get('quicksettings').split(',')] bad_settings = 0 for k, v in self.data.items(): info = self.data_labels.get(k, None) @@ -750,3 +752,21 @@ def html(filename): with open(path, encoding="utf8") as file: return file.read() return "" + +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. + sd_model_val = None + + @property + def sd_model(self): + import modules.sd_models # pylint: disable=W0621 + # return modules.sd_models.model_data.sd_model + return modules.sd_models.model_data.get_sd_model() + + @sd_model.setter + def sd_model(self, value): + import modules.sd_models # pylint: disable=W0621 + modules.sd_models.model_data.set_sd_model(value) + +sd_model: LatentDiffusion = None # this var is here just for IDE's type checking; it cannot be accessed because the class field above will be accessed instead +sys.modules[__name__].__class__ = Shared diff --git a/modules/textual_inversion/autocrop.py b/modules/textual_inversion/autocrop.py index 68e1103c5..7097ecd36 100644 --- a/modules/textual_inversion/autocrop.py +++ b/modules/textual_inversion/autocrop.py @@ -1,10 +1,8 @@ +import os import cv2 import requests -import os -from collections import defaultdict -from math import log, sqrt import numpy as np -from PIL import Image, ImageDraw +from PIL import ImageDraw GREEN = "#0F0" BLUE = "#00F" @@ -12,63 +10,63 @@ RED = "#F00" def crop_image(im, settings): - """ Intelligently crop an image to the subject matter """ + """ Intelligently crop an image to the subject matter """ - scale_by = 1 - if is_landscape(im.width, im.height): - scale_by = settings.crop_height / im.height - elif is_portrait(im.width, im.height): - scale_by = settings.crop_width / im.width - elif is_square(im.width, im.height): - if is_square(settings.crop_width, settings.crop_height): - scale_by = settings.crop_width / im.width - elif is_landscape(settings.crop_width, settings.crop_height): - scale_by = settings.crop_width / im.width - elif is_portrait(settings.crop_width, settings.crop_height): - scale_by = settings.crop_height / im.height + scale_by = 1 + if is_landscape(im.width, im.height): + scale_by = settings.crop_height / im.height + elif is_portrait(im.width, im.height): + scale_by = settings.crop_width / im.width + elif is_square(im.width, im.height): + if is_square(settings.crop_width, settings.crop_height): + scale_by = settings.crop_width / im.width + elif is_landscape(settings.crop_width, settings.crop_height): + scale_by = settings.crop_width / im.width + elif is_portrait(settings.crop_width, settings.crop_height): + scale_by = settings.crop_height / im.height - im = im.resize((int(im.width * scale_by), int(im.height * scale_by))) - im_debug = im.copy() + im = im.resize((int(im.width * scale_by), int(im.height * scale_by))) + im_debug = im.copy() - focus = focal_point(im_debug, settings) + focus = focal_point(im_debug, settings) - # take the focal point and turn it into crop coordinates that try to center over the focal - # point but then get adjusted back into the frame - y_half = int(settings.crop_height / 2) - x_half = int(settings.crop_width / 2) + # take the focal point and turn it into crop coordinates that try to center over the focal + # point but then get adjusted back into the frame + y_half = int(settings.crop_height / 2) + x_half = int(settings.crop_width / 2) - x1 = focus.x - x_half - if x1 < 0: - x1 = 0 - elif x1 + settings.crop_width > im.width: - x1 = im.width - settings.crop_width + x1 = focus.x - x_half + if x1 < 0: + x1 = 0 + elif x1 + settings.crop_width > im.width: + x1 = im.width - settings.crop_width - y1 = focus.y - y_half - if y1 < 0: - y1 = 0 - elif y1 + settings.crop_height > im.height: - y1 = im.height - settings.crop_height + y1 = focus.y - y_half + if y1 < 0: + y1 = 0 + elif y1 + settings.crop_height > im.height: + y1 = im.height - settings.crop_height - x2 = x1 + settings.crop_width - y2 = y1 + settings.crop_height + x2 = x1 + settings.crop_width + y2 = y1 + settings.crop_height - crop = [x1, y1, x2, y2] + crop = [x1, y1, x2, y2] - results = [] + results = [] - results.append(im.crop(tuple(crop))) + results.append(im.crop(tuple(crop))) - if settings.annotate_image: - d = ImageDraw.Draw(im_debug) - rect = list(crop) - rect[2] -= 1 - rect[3] -= 1 - d.rectangle(rect, outline=GREEN) - results.append(im_debug) - if settings.destop_view_image: - im_debug.show() + if settings.annotate_image: + d = ImageDraw.Draw(im_debug) + rect = list(crop) + rect[2] -= 1 + rect[3] -= 1 + d.rectangle(rect, outline=GREEN) + results.append(im_debug) + if settings.destop_view_image: + im_debug.show() - return results + return results def focal_point(im, settings): corner_points = image_corner_points(im, settings) if settings.corner_points_weight > 0 else [] @@ -79,118 +77,118 @@ def focal_point(im, settings): weight_pref_total = 0 if len(corner_points) > 0: - weight_pref_total += settings.corner_points_weight + weight_pref_total += settings.corner_points_weight if len(entropy_points) > 0: - weight_pref_total += settings.entropy_points_weight + weight_pref_total += settings.entropy_points_weight if len(face_points) > 0: - weight_pref_total += settings.face_points_weight + weight_pref_total += settings.face_points_weight corner_centroid = None if len(corner_points) > 0: - corner_centroid = centroid(corner_points) - corner_centroid.weight = settings.corner_points_weight / weight_pref_total - pois.append(corner_centroid) + corner_centroid = centroid(corner_points) + corner_centroid.weight = settings.corner_points_weight / weight_pref_total + pois.append(corner_centroid) entropy_centroid = None if len(entropy_points) > 0: - entropy_centroid = centroid(entropy_points) - entropy_centroid.weight = settings.entropy_points_weight / weight_pref_total - pois.append(entropy_centroid) + entropy_centroid = centroid(entropy_points) + entropy_centroid.weight = settings.entropy_points_weight / weight_pref_total + pois.append(entropy_centroid) face_centroid = None if len(face_points) > 0: - face_centroid = centroid(face_points) - face_centroid.weight = settings.face_points_weight / weight_pref_total - pois.append(face_centroid) + face_centroid = centroid(face_points) + face_centroid.weight = settings.face_points_weight / weight_pref_total + pois.append(face_centroid) average_point = poi_average(pois, settings) if settings.annotate_image: - d = ImageDraw.Draw(im) - max_size = min(im.width, im.height) * 0.07 - if corner_centroid is not None: - color = BLUE - box = corner_centroid.bounding(max_size * corner_centroid.weight) - d.text((box[0], box[1]-15), "Edge: %.02f" % corner_centroid.weight, fill=color) - d.ellipse(box, outline=color) - if len(corner_points) > 1: - for f in corner_points: - d.rectangle(f.bounding(4), outline=color) - if entropy_centroid is not None: - color = "#ff0" - box = entropy_centroid.bounding(max_size * entropy_centroid.weight) - d.text((box[0], box[1]-15), "Entropy: %.02f" % entropy_centroid.weight, fill=color) - d.ellipse(box, outline=color) - if len(entropy_points) > 1: - for f in entropy_points: - d.rectangle(f.bounding(4), outline=color) - if face_centroid is not None: - color = RED - box = face_centroid.bounding(max_size * face_centroid.weight) - d.text((box[0], box[1]-15), "Face: %.02f" % face_centroid.weight, fill=color) - d.ellipse(box, outline=color) - if len(face_points) > 1: - for f in face_points: - d.rectangle(f.bounding(4), outline=color) + d = ImageDraw.Draw(im) + max_size = min(im.width, im.height) * 0.07 + if corner_centroid is not None: + color = BLUE + box = corner_centroid.bounding(max_size * corner_centroid.weight) + d.text((box[0], box[1]-15), f"Edge: {corner_centroid.weight:.02f}", fill=color) + d.ellipse(box, outline=color) + if len(corner_points) > 1: + for f in corner_points: + d.rectangle(f.bounding(4), outline=color) + if entropy_centroid is not None: + color = "#ff0" + box = entropy_centroid.bounding(max_size * entropy_centroid.weight) + d.text((box[0], box[1]-15), f"Entropy: {entropy_centroid.weight:.02f}", fill=color) + d.ellipse(box, outline=color) + if len(entropy_points) > 1: + for f in entropy_points: + d.rectangle(f.bounding(4), outline=color) + if face_centroid is not None: + color = RED + box = face_centroid.bounding(max_size * face_centroid.weight) + d.text((box[0], box[1]-15), f"Face: {face_centroid.weight:.02f}", fill=color) + d.ellipse(box, outline=color) + if len(face_points) > 1: + for f in face_points: + d.rectangle(f.bounding(4), outline=color) + + d.ellipse(average_point.bounding(max_size), outline=GREEN) - d.ellipse(average_point.bounding(max_size), outline=GREEN) - return average_point def image_face_points(im, settings): if settings.dnn_model_path is not None: - detector = cv2.FaceDetectorYN.create( - settings.dnn_model_path, - "", - (im.width, im.height), - 0.9, # score threshold - 0.3, # nms threshold - 5000 # keep top k before nms - ) - faces = detector.detect(np.array(im)) - results = [] - if faces[1] is not None: - for face in faces[1]: - x = face[0] - y = face[1] - w = face[2] - h = face[3] - results.append( - PointOfInterest( - int(x + (w * 0.5)), # face focus left/right is center - int(y + (h * 0.33)), # face focus up/down is close to the top of the head - size = w, - weight = 1/len(faces[1]) - ) - ) - return results + detector = cv2.FaceDetectorYN.create( + settings.dnn_model_path, + "", + (im.width, im.height), + 0.9, # score threshold + 0.3, # nms threshold + 5000 # keep top k before nms + ) + faces = detector.detect(np.array(im)) + results = [] + if faces[1] is not None: + for face in faces[1]: + x = face[0] + y = face[1] + w = face[2] + h = face[3] + results.append( + PointOfInterest( + int(x + (w * 0.5)), # face focus left/right is center + int(y + (h * 0.33)), # face focus up/down is close to the top of the head + size = w, + weight = 1/len(faces[1]) + ) + ) + return results else: - np_im = np.array(im) - gray = cv2.cvtColor(np_im, cv2.COLOR_BGR2GRAY) + np_im = np.array(im) + gray = cv2.cvtColor(np_im, cv2.COLOR_BGR2GRAY) - tries = [ - [ f'{cv2.data.haarcascades}haarcascade_eye.xml', 0.01 ], - [ f'{cv2.data.haarcascades}haarcascade_frontalface_default.xml', 0.05 ], - [ f'{cv2.data.haarcascades}haarcascade_profileface.xml', 0.05 ], - [ f'{cv2.data.haarcascades}haarcascade_frontalface_alt.xml', 0.05 ], - [ f'{cv2.data.haarcascades}haarcascade_frontalface_alt2.xml', 0.05 ], - [ f'{cv2.data.haarcascades}haarcascade_frontalface_alt_tree.xml', 0.05 ], - [ f'{cv2.data.haarcascades}haarcascade_eye_tree_eyeglasses.xml', 0.05 ], - [ f'{cv2.data.haarcascades}haarcascade_upperbody.xml', 0.05 ] - ] - for t in tries: - classifier = cv2.CascadeClassifier(t[0]) - minsize = int(min(im.width, im.height) * t[1]) # at least N percent of the smallest side - try: - faces = classifier.detectMultiScale(gray, scaleFactor=1.1, - minNeighbors=7, minSize=(minsize, minsize), flags=cv2.CASCADE_SCALE_IMAGE) - except: - continue + tries = [ + [ f'{cv2.data.haarcascades}haarcascade_eye.xml', 0.01 ], + [ f'{cv2.data.haarcascades}haarcascade_frontalface_default.xml', 0.05 ], + [ f'{cv2.data.haarcascades}haarcascade_profileface.xml', 0.05 ], + [ f'{cv2.data.haarcascades}haarcascade_frontalface_alt.xml', 0.05 ], + [ f'{cv2.data.haarcascades}haarcascade_frontalface_alt2.xml', 0.05 ], + [ f'{cv2.data.haarcascades}haarcascade_frontalface_alt_tree.xml', 0.05 ], + [ f'{cv2.data.haarcascades}haarcascade_eye_tree_eyeglasses.xml', 0.05 ], + [ f'{cv2.data.haarcascades}haarcascade_upperbody.xml', 0.05 ] + ] + for t in tries: + classifier = cv2.CascadeClassifier(t[0]) + minsize = int(min(im.width, im.height) * t[1]) # at least N percent of the smallest side + try: + faces = classifier.detectMultiScale(gray, scaleFactor=1.1, + minNeighbors=7, minSize=(minsize, minsize), flags=cv2.CASCADE_SCALE_IMAGE) + except: + continue - if len(faces) > 0: - rects = [[f[0], f[1], f[0] + f[2], f[1] + f[3]] for f in faces] - return [PointOfInterest((r[0] +r[2]) // 2, (r[1] + r[3]) // 2, size=abs(r[0]-r[2]), weight=1/len(rects)) for r in rects] + if len(faces) > 0: + rects = [[f[0], f[1], f[0] + f[2], f[1] + f[3]] for f in faces] + return [PointOfInterest((r[0] +r[2]) // 2, (r[1] + r[3]) // 2, size=abs(r[0]-r[2]), weight=1/len(rects)) for r in rects] return [] @@ -204,11 +202,11 @@ def image_corner_points(im, settings): np_im = np.array(grayscale) points = cv2.goodFeaturesToTrack( - np_im, - maxCorners=100, - qualityLevel=0.04, - minDistance=min(grayscale.width, grayscale.height)*0.06, - useHarrisDetector=False, + np_im, + maxCorners=100, + qualityLevel=0.04, + minDistance=min(grayscale.width, grayscale.height)*0.06, + useHarrisDetector=False, ) if points is None: @@ -216,8 +214,8 @@ def image_corner_points(im, settings): focal_points = [] for point in points: - x, y = point.ravel() - focal_points.append(PointOfInterest(x, y, size=4, weight=1/len(points))) + x, y = point.ravel() + focal_points.append(PointOfInterest(x, y, size=4, weight=1/len(points))) return focal_points @@ -226,13 +224,13 @@ def image_entropy_points(im, settings): landscape = im.height < im.width portrait = im.height > im.width if landscape: - move_idx = [0, 2] - move_max = im.size[0] + move_idx = [0, 2] + move_max = im.size[0] elif portrait: - move_idx = [1, 3] - move_max = im.size[1] + move_idx = [1, 3] + move_max = im.size[1] else: - return [] + return [] e_max = 0 crop_current = [0, 0, settings.crop_width, settings.crop_height] @@ -241,9 +239,9 @@ def image_entropy_points(im, settings): crop = im.crop(tuple(crop_current)) e = image_entropy(crop) - if (e > e_max): - e_max = e - crop_best = list(crop_current) + if e > e_max: + e_max = e + crop_best = list(crop_current) crop_current[move_idx[0]] += 4 crop_current[move_idx[1]] += 4 @@ -263,9 +261,9 @@ def image_entropy(im): return -np.log2(hist / hist.sum()).sum() def centroid(pois): - x = [poi.x for poi in pois] - y = [poi.y for poi in pois] - return PointOfInterest(sum(x)/len(pois), sum(y)/len(pois)) + x = [poi.x for poi in pois] + y = [poi.y for poi in pois] + return PointOfInterest(sum(x)/len(pois), sum(y)/len(pois)) def poi_average(pois, settings): @@ -283,59 +281,59 @@ def poi_average(pois, settings): def is_landscape(w, h): - return w > h + return w > h def is_portrait(w, h): - return h > w + return h > w def is_square(w, h): - return w == h + return w == h def download_and_cache_models(dirname): - download_url = 'https://github.com/opencv/opencv_zoo/blob/91fb0290f50896f38a0ab1e558b74b16bc009428/models/face_detection_yunet/face_detection_yunet_2022mar.onnx?raw=true' - model_file_name = 'face_detection_yunet.onnx' + download_url = 'https://github.com/opencv/opencv_zoo/blob/91fb0290f50896f38a0ab1e558b74b16bc009428/models/face_detection_yunet/face_detection_yunet_2022mar.onnx?raw=true' + model_file_name = 'face_detection_yunet.onnx' - if not os.path.exists(dirname): - os.makedirs(dirname) + if not os.path.exists(dirname): + os.makedirs(dirname) - cache_file = os.path.join(dirname, model_file_name) - if not os.path.exists(cache_file): - print(f"downloading face detection model from '{download_url}' to '{cache_file}'") - response = requests.get(download_url) - with open(cache_file, "wb") as f: - f.write(response.content) + cache_file = os.path.join(dirname, model_file_name) + if not os.path.exists(cache_file): + print(f"downloading face detection model from '{download_url}' to '{cache_file}'") + response = requests.get(download_url, timeout=60*60*2) + with open(cache_file, "wb") as f: + f.write(response.content) - if os.path.exists(cache_file): - return cache_file - return None + if os.path.exists(cache_file): + return cache_file + return None class PointOfInterest: - def __init__(self, x, y, weight=1.0, size=10): - self.x = x - self.y = y - self.weight = weight - self.size = size + def __init__(self, x, y, weight=1.0, size=10): + self.x = x + self.y = y + self.weight = weight + self.size = size - def bounding(self, size): - return [ - self.x - size//2, - self.y - size//2, - self.x + size//2, - self.y + size//2 - ] + def bounding(self, size): + return [ + self.x - size//2, + self.y - size//2, + self.x + size//2, + self.y + size//2 + ] class Settings: - def __init__(self, crop_width=512, crop_height=512, corner_points_weight=0.5, entropy_points_weight=0.5, face_points_weight=0.5, annotate_image=False, dnn_model_path=None): - self.crop_width = crop_width - self.crop_height = crop_height - self.corner_points_weight = corner_points_weight - self.entropy_points_weight = entropy_points_weight - self.face_points_weight = face_points_weight - self.annotate_image = annotate_image - self.destop_view_image = False - self.dnn_model_path = dnn_model_path + def __init__(self, crop_width=512, crop_height=512, corner_points_weight=0.5, entropy_points_weight=0.5, face_points_weight=0.5, annotate_image=False, dnn_model_path=None): + self.crop_width = crop_width + self.crop_height = crop_height + self.corner_points_weight = corner_points_weight + self.entropy_points_weight = entropy_points_weight + self.face_points_weight = face_points_weight + self.annotate_image = annotate_image + self.destop_view_image = False + self.dnn_model_path = dnn_model_path diff --git a/modules/textual_inversion/dataset.py b/modules/textual_inversion/dataset.py index af9fbcf28..f53a73b89 100644 --- a/modules/textual_inversion/dataset.py +++ b/modules/textual_inversion/dataset.py @@ -1,19 +1,16 @@ import os +import re +import random +from collections import defaultdict import numpy as np import PIL import torch from PIL import Image from torch.utils.data import Dataset, DataLoader, Sampler from torchvision import transforms -from collections import defaultdict -from random import shuffle, choices - -import random import tqdm -from modules import devices, shared -import re - from ldm.modules.distributions.distributions import DiagonalGaussianDistribution +from modules import devices, shared re_numbers_at_start = re.compile(r"^[-\d]+\s*") @@ -72,7 +69,7 @@ class PersonalizedBase(Dataset): except Exception: continue - text_filename = os.path.splitext(path)[0] + ".txt" + text_filename = f"{os.path.splitext(path)[0]}.txt" filename = os.path.basename(path) if os.path.exists(text_filename): @@ -118,7 +115,7 @@ class PersonalizedBase(Dataset): weight = torch.ones(latent_sample.shape) else: weight = None - + if latent_sampling_method == "random": entry = DatasetEntry(filename=path, filename_text=filename_text, latent_dist=latent_dist, weight=weight) else: @@ -193,16 +190,16 @@ class GroupedBatchSampler(Sampler): b = self.batch_size for g in self.groups: - shuffle(g) + random.shuffle(g) batches = [] for g in self.groups: batches.extend(g[i*b:(i+1)*b] for i in range(len(g) // b)) for _ in range(self.n_rand_batches): - rand_group = choices(self.groups, self.probs)[0] - batches.append(choices(rand_group, k=b)) + rand_group = random.choices(self.groups, self.probs)[0] + batches.append(random.choices(rand_group, k=b)) - shuffle(batches) + random.shuffle(batches) yield from batches @@ -243,4 +240,4 @@ class BatchLoaderRandom(BatchLoader): return self def collate_wrapper_random(batch): - return BatchLoaderRandom(batch) \ No newline at end of file + return BatchLoaderRandom(batch) diff --git a/modules/textual_inversion/logging.py b/modules/textual_inversion/logging.py index b8440f656..a79696e3f 100644 --- a/modules/textual_inversion/logging.py +++ b/modules/textual_inversion/logging.py @@ -16,7 +16,7 @@ def save_settings_to_file(log_directory, all_params): if all_params.get('preview_from_txt2img'): keys = keys | saved_params_previews params.update({k: v for k, v in all_params.items() if k in keys}) - filename = f"{params['embedding_name']}-{now.strftime('%Y-%m-%d_%H-%M-%S')}.json" + filename = f"settings-{now.strftime('%Y-%m-%d_%H-%M-%S')}.json" with open(os.path.join(log_directory, filename), "w", encoding='utf-8') as file: print(f'Training settings file: {os.path.join(log_directory, filename)}') json.dump(params, file, indent=2) diff --git a/modules/textual_inversion/preprocess.py b/modules/textual_inversion/preprocess.py index ed93bf979..5d6f885b9 100644 --- a/modules/textual_inversion/preprocess.py +++ b/modules/textual_inversion/preprocess.py @@ -58,9 +58,9 @@ def save_pic_with_caption(image, index, params: PreprocessParams, existing_capti image.save(os.path.join(params.dstdir, f"{basename}.png")) if params.preprocess_txt_action == 'prepend' and existing_caption: - caption = existing_caption + ' ' + caption + caption = f"{existing_caption} {caption}" elif params.preprocess_txt_action == 'append' and existing_caption: - caption = caption + ' ' + existing_caption + caption = f"{caption} {existing_caption}" elif params.preprocess_txt_action == 'copy' and existing_caption: caption = existing_caption caption = caption.strip() @@ -173,7 +173,7 @@ def preprocess_work(process_src, process_dst, process_width, process_height, pre params.src = filename existing_caption = None - existing_caption_filename = os.path.splitext(filename)[0] + '.txt' + existing_caption_filename = f"{os.path.splitext(filename)[0]}.txt" if os.path.exists(existing_caption_filename): with open(existing_caption_filename, 'r', encoding="utf8") as file: existing_caption = file.read() diff --git a/modules/textual_inversion/textual_inversion.py b/modules/textual_inversion/textual_inversion.py index bee86ceac..9693730ab 100644 --- a/modules/textual_inversion/textual_inversion.py +++ b/modules/textual_inversion/textual_inversion.py @@ -60,7 +60,7 @@ class Embedding: 'hash': self.checksum(), 'optimizer_state_dict': self.optimizer_state_dict, } - torch.save(optimizer_saved_dict, filename + '.optim') + torch.save(optimizer_saved_dict, f"{filename}.optim") def checksum(self): if self.cached_checksum is not None: @@ -419,8 +419,8 @@ def train_embedding(id_task, embedding_name, learn_rate, batch_size, gradient_st optimizer = torch.optim.AdamW([embedding.vec], lr=scheduler.learn_rate, weight_decay=0.0) if shared.opts.save_optimizer_state: optimizer_state_dict = None - if os.path.exists(filename + '.optim'): - optimizer_saved_dict = torch.load(filename + '.optim', map_location='cpu') + if os.path.exists(f"{filename}.optim"): + optimizer_saved_dict = torch.load(f"{filename}.optim", map_location='cpu') if embedding.checksum() == optimizer_saved_dict.get('hash', None): optimizer_state_dict = optimizer_saved_dict.get('optimizer_state_dict', None) if optimizer_state_dict is not None: diff --git a/modules/ui.py b/modules/ui.py index fd0ea31b3..cc0de9e6e 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -76,7 +76,7 @@ def visit(x, func, path=""): for c in x.children: visit(c, func, path) elif x.label is not None: - func(path + "/" + str(x.label), x) + func(f"{path}/{x.label}", x) def add_style(name: str, prompt: str, negative_prompt: str): @@ -127,7 +127,7 @@ def process_interrogate(interrogation_function, mode, ii_input_dir, ii_output_di img = Image.open(image) filename = os.path.basename(image) left, _ = os.path.splitext(filename) - print(interrogation_function(img), file=open(os.path.join(ii_output_dir, left + ".txt"), 'a', encoding='utf-8')) + print(interrogation_function(img), file=open(os.path.join(ii_output_dir, f"{left}.txt"), 'a', encoding='utf-8')) return [gr.update(), None] @@ -147,21 +147,21 @@ def change_clip_skip(val): def create_seed_inputs(target_interface): - with FormRow(elem_id=target_interface + '_seed_row', variant="compact"): - seed = gr.Number(label='Seed', value=-1, elem_id=target_interface + '_seed') + with FormRow(elem_id=f"{target_interface}_seed_row", variant="compact"): + seed = gr.Number(label='Seed', value=-1, elem_id=f"{target_interface}_seed") seed.style(container=False) - random_seed = ToolButton(random_symbol, elem_id=target_interface + '_random_seed', label='Random seed') - reuse_seed = ToolButton(reuse_symbol, elem_id=target_interface + '_reuse_seed', label='Reuse seed') - seed_checkbox = gr.Checkbox(label='Extra', elem_id=target_interface + '_subseed_show', value=False, visible=False) # Ghost checkbox, so it still gets sent. For compatibility with extensions that call txt2img or img2img manually - with FormRow(visible=True, elem_id=target_interface + '_subseed_row'): - subseed = gr.Number(label='Variation seed', value=-1, elem_id=target_interface + '_subseed') + random_seed = ToolButton(random_symbol, elem_id=f"{target_interface}_random_seed", label='Random seed') + reuse_seed = ToolButton(reuse_symbol, elem_id=f"{target_interface}_reuse_seed", label='Reuse seed') + seed_checkbox = gr.Checkbox(label='Extra', elem_id=f"{target_interface}_subseed_show", value=False) # Ghost checkbox for compatibility + with FormRow(visible=True, elem_id=f"{target_interface}_subseed_row"): + subseed = gr.Number(label='Variation seed', value=-1, elem_id=f"{target_interface}_subseed") subseed.style(container=False) - random_subseed = ToolButton(random_symbol, elem_id=target_interface + '_random_subseed') - reuse_subseed = ToolButton(reuse_symbol, elem_id=target_interface + '_reuse_subseed') - subseed_strength = gr.Slider(label='Strength', value=0.0, minimum=0, maximum=1, step=0.01, elem_id=target_interface + '_subseed_strength') + random_subseed = ToolButton(random_symbol, elem_id=f"{target_interface}_random_subseed") + reuse_subseed = ToolButton(reuse_symbol, elem_id=f"{target_interface}_reuse_subseed") + subseed_strength = gr.Slider(label='Variation strength', value=0.0, minimum=0, maximum=1, step=0.01, elem_id=f"{target_interface}_subseed_strength") with FormRow(visible=False): - seed_resize_from_w = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize seed from width", value=0, elem_id=target_interface + '_seed_resize_from_w') - seed_resize_from_h = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize seed from height", value=0, elem_id=target_interface + '_seed_resize_from_h') + seed_resize_from_w = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize seed from width", value=0, elem_id=f"{target_interface}_seed_resize_from_w") + seed_resize_from_h = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize seed from height", value=0, elem_id=f"{target_interface}_seed_resize_from_h") random_seed.click(fn=lambda: [-1, -1], show_progress=False, inputs=[], outputs=[seed, subseed]) random_subseed.click(fn=lambda: -1, show_progress=False, inputs=[], outputs=[subseed]) return seed, reuse_seed, subseed, reuse_subseed, subseed_strength, seed_resize_from_h, seed_resize_from_w, seed_checkbox @@ -615,7 +615,7 @@ def create_ui(): ) button.click( fn=lambda: None, - _js="switch_to_"+name.replace(" ", "_"), + _js=f"switch_to_{name.replace(' ', '_')}", inputs=[], outputs=[], ) @@ -679,7 +679,7 @@ def create_ui(): with FormGroup(): with FormRow(): cfg_scale = gr.Slider(minimum=1.0, maximum=30.0, step=0.5, label='CFG Scale', value=6.0, elem_id="img2img_cfg_scale") - image_cfg_scale = gr.Slider(minimum=0, maximum=3.0, step=0.05, label='Image CFG Scale', value=1.5, elem_id="img2img_image_cfg_scale", visible=modules.shared.sd_model and modules.shared.sd_model.cond_stage_key == "edit") + image_cfg_scale = gr.Slider(minimum=0, maximum=3.0, step=0.05, label='Image CFG Scale', value=1.5, elem_id="img2img_image_cfg_scale", visible=False) denoising_strength = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label='Denoising strength', value=0.75, elem_id="img2img_denoising_strength") clip_skip = gr.Slider(label='CLIP Skip', value=modules.shared.opts.CLIP_stop_at_last_layers, minimum=1, maximum=4, step=1, elem_id='img2img_clip_skip', interactive=True) clip_skip.change(fn=change_clip_skip, show_progress=False, inputs=clip_skip) @@ -1271,16 +1271,16 @@ def create_ui(): elif t == bool: comp = gr.Checkbox else: - raise ValueError(f'bad options item type: {str(t)} for key {key}') - elem_id = "setting_"+key + raise ValueError(f'bad options item type: {t} for key {key}') + elem_id = f"setting_{key}" if info.refresh is not None: if is_quicksettings: res = comp(label=info.label, value=fun(), elem_id=elem_id, **(args or {})) - create_refresh_button(res, info.refresh, info.component_args, "refresh_" + key) + create_refresh_button(res, info.refresh, info.component_args, f"refresh_{key}") else: with FormRow(): res = comp(label=info.label, value=fun(), elem_id=elem_id, **(args or {})) - create_refresh_button(res, info.refresh, info.component_args, "refresh_" + key) + create_refresh_button(res, info.refresh, info.component_args, f"refresh_{key}") else: res = comp(label=info.label, value=fun(), elem_id=elem_id, **(args or {})) return res @@ -1331,7 +1331,7 @@ def create_ui(): result = gr.HTML(elem_id="settings_result") - quicksettings_names = [x.strip() for x in opts.quicksettings.split(",")] + quicksettings_names = opts.quicksettings_list quicksettings_names = {x: i for i, x in enumerate(quicksettings_names) if x != 'quicksettings'} quicksettings_list = [] previous_section = None @@ -1366,7 +1366,7 @@ def create_ui(): request_notifications = gr.Button(value='Request browser notifications', elem_id="request_notifications", visible=False) _show_all_pages = gr.Button(value="Show all pages", variant='primary', elem_id="settings_show_all_pages") - with gr.TabItem("Licenses", id="licenses"): + with gr.TabItem("Licenses", id="licenses", elem_id="settings_tab_licenses"): gr.HTML(modules.shared.html("licenses.html"), elem_id="licenses") def unload_sd_weights(): @@ -1443,7 +1443,7 @@ def create_ui(): for interface, label, ifid in interfaces: if label in modules.shared.opts.hidden_tabs: continue - with gr.TabItem(label, id=ifid, elem_id='tab_' + ifid): + with gr.TabItem(label, id=ifid, elem_id=f"tab_{ifid}"): interface.render() if opts.notification_audio_enable and os.path.exists(os.path.join(script_path, opts.notification_audio_path)): @@ -1471,11 +1471,9 @@ def create_ui(): show_progress=info.refresh is not None, ) - text_settings.change( - fn=lambda: gr.update(visible=modules.shared.sd_model and modules.shared.sd_model.cond_stage_key == "edit"), - inputs=[], - outputs=[image_cfg_scale], - ) + update_image_cfg_scale_visibility = lambda: gr.update(visible=modules.shared.sd_model and modules.shared.sd_model.cond_stage_key == "edit") # pylint: disable=unnecessary-lambda-assignment + text_settings.change(fn=update_image_cfg_scale_visibility, inputs=[], outputs=[image_cfg_scale]) + demo.load(fn=update_image_cfg_scale_visibility, inputs=[], outputs=[image_cfg_scale]) button_set_checkpoint = gr.Button('Change checkpoint', elem_id='change_checkpoint', visible=False) button_set_checkpoint.click( @@ -1549,10 +1547,10 @@ def create_ui(): def loadsave(path, x): def apply_field(obj, field, condition=None, init_field=None): - key = path + "/" + field + key = f"{path}/{field}" if getattr(obj, 'custom_script_source', None) is not None: - key = 'customscript/' + obj.custom_script_source + '/' + key + key = f"customscript/{obj.custom_script_source}/{key}" if getattr(obj, 'do_not_save_to_config', False): return @@ -1699,5 +1697,20 @@ def reload_javascript(): gradio.routes.templates.TemplateResponse = template_response +def setup_ui_api(app): + from pydantic import BaseModel, Field # pylint: disable=no-name-in-module + from typing import List + + class QuicksettingsHint(BaseModel): + name: str = Field(title="Name of the quicksettings field") + label: str = Field(title="Label of the quicksettings field") + + def quicksettings_hint(): + return [QuicksettingsHint(name=k, label=v.label) for k, v in opts.data_labels.items()] + + app.add_api_route("/internal/quicksettings-hint", quicksettings_hint, methods=["GET"], response_model=List[QuicksettingsHint]) + app.add_api_route("/internal/ping", lambda: {}, methods=["GET"]) + + if not hasattr(modules.shared, 'GradioTemplateResponseOriginal'): modules.shared.GradioTemplateResponseOriginal = gradio.routes.templates.TemplateResponse diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 3aeea5783..c044b23d0 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -1,6 +1,5 @@ import json import html -import glob import os.path import urllib.parse from pathlib import Path @@ -63,7 +62,9 @@ class ExtraNetworksPage: pass def link_preview(self, filename): - return "./sd_extra_networks/thumb?filename=" + urllib.parse.quote(filename.replace('\\', '/')) + "&mtime=" + str(os.path.getmtime(filename)) + quoted_filename = urllib.parse.quote(filename.replace('\\', '/')) + mtime = os.path.getmtime(filename) + return f"./sd_extra_networks/thumb?filename={quoted_filename}&mtime={mtime}" def search_terms_from_path(self, filename, possible_directories=None): abspath = os.path.abspath(filename) @@ -78,17 +79,20 @@ class ExtraNetworksPage: items_html = '' self.metadata = {} subdirs = {} - for parentdir in [os.path.abspath(x) for x in self.allowed_directories_for_previews()]: - for x in glob.glob(os.path.join(parentdir, '**/*'), recursive=True): - if not os.path.isdir(x): - continue - subdir = os.path.abspath(x)[len(parentdir):].replace("\\", "/") - while subdir.startswith("/"): - subdir = subdir[1:] - is_empty = len(os.listdir(x)) == 0 - if not is_empty and not subdir.endswith("/"): - subdir = subdir + "/" - subdirs[subdir] = 1 + allowed_folders = [os.path.abspath(x) for x in self.allowed_directories_for_previews()] + for parentdir in [*set(allowed_folders)]: + for root, dirs, _files in os.walk(parentdir): + for dirname in dirs: + x = os.path.join(root, dirname) + if not os.path.isdir(x): + continue + subdir = os.path.abspath(x)[len(parentdir):].replace("\\", "/") + while subdir.startswith("/"): + subdir = subdir[1:] + is_empty = len(os.listdir(x)) == 0 + if not is_empty and not subdir.endswith("/"): + subdir = subdir + "/" + subdirs[subdir] = 1 if subdirs: subdirs = {"": 1, **subdirs} subdirs_html = "".join([f""" @@ -181,7 +185,7 @@ def intialize(): class ExtraNetworksUi: def __init__(self): self.pages = None - self.stored_extra_pages = None + self.stored_extra_pages = [] self.button_save_preview = None self.preview_target_filename = None self.button_save_description = None diff --git a/modules/ui_tempdir.py b/modules/ui_tempdir.py index 7e5849ba5..db6e20e79 100644 --- a/modules/ui_tempdir.py +++ b/modules/ui_tempdir.py @@ -39,7 +39,7 @@ def save_pil_to_file(pil_image, dir=None): # pylint: disable=redefined-builtin already_saved_as = getattr(pil_image, 'already_saved_as', None) if already_saved_as and os.path.isfile(already_saved_as): register_tmp_file(shared.demo, already_saved_as) - file_obj = Savedfile(already_saved_as) + file_obj = Savedfile(f'{already_saved_as}?{os.path.getmtime(already_saved_as)}') return file_obj if shared.opts.temp_dir != "": dir = shared.opts.temp_dir diff --git a/scripts/custom_code.py b/scripts/custom_code.py index 2dd036f2d..56b0db222 100644 --- a/scripts/custom_code.py +++ b/scripts/custom_code.py @@ -77,7 +77,7 @@ return process_images(p) module.display = display indent = " " * indent_level - indented = code.replace('\n', '\n' + indent) + indented = code.replace('\n', f"\n{indent}") body = f"""def __webuitemp__(): {indent}{indented} __webuitemp__()""" diff --git a/scripts/loopback.py b/scripts/loopback.py index 5ce5e8271..96ec03a03 100644 --- a/scripts/loopback.py +++ b/scripts/loopback.py @@ -84,7 +84,7 @@ class Script(scripts.Script): p.color_corrections = initial_color_corrections if append_interrogation != "None": - p.prompt = original_prompt + ", " if original_prompt != "" else "" + p.prompt = f"{original_prompt}, " if original_prompt else "" if append_interrogation == "CLIP": p.prompt += shared.interrogator.interrogate(p.init_images[0]) elif append_interrogation == "DeepBooru": diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py index 64ccab852..b456b3cb2 100644 --- a/scripts/xyz_grid.py +++ b/scripts/xyz_grid.py @@ -227,7 +227,7 @@ axis_options = [ AxisOptionTxt2Img("Hires upscaler", str, apply_field("hr_upscaler"), choices=lambda: [*shared.latent_upscale_modes, *[x.name for x in shared.sd_upscalers]]), AxisOptionTxt2Img("Fallback latent upscaler sampler", str, apply_fallback, fmt=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers]), AxisOptionImg2Img("Cond. Image Mask Weight", float, apply_field("inpainting_mask_weight")), - AxisOption("VAE", str, apply_vae, cost=0.7, choices=lambda: list(sd_vae.vae_dict)), + AxisOption("VAE", str, apply_vae, cost=0.7, choices=lambda: ['None'] + list(sd_vae.vae_dict)), AxisOption("Styles", str, apply_styles, choices=lambda: list(shared.prompt_styles.styles)), AxisOption("UniPC Order", int, apply_uni_pc_order, cost=0.5), AxisOption("Face restore", str, apply_face_restore, fmt=format_value), @@ -446,7 +446,7 @@ class Script(scripts.Script): z_type.change(fn=select_axis, inputs=[z_type,z_values_dropdown], outputs=[fill_z_button,z_values,z_values_dropdown]) def get_dropdown_update_from_params(axis,params): - val_key = axis + " Values" + val_key = f"{axis} Values" vals = params.get(val_key,"") valslist = [x.strip() for x in chain.from_iterable(csv.reader(StringIO(vals))) if x] return gr.update(value = valslist) diff --git a/webui.py b/webui.py index bbd6cfff5..f941375d8 100644 --- a/webui.py +++ b/webui.py @@ -5,6 +5,7 @@ import signal import asyncio import logging import warnings +from threading import Thread from modules import timer, errors startup_timer = timer.Timer() @@ -27,6 +28,7 @@ warnings.filterwarnings(action="ignore", category=UserWarning, module="torchvisi startup_timer.record("torch") from modules import import_hook # pylint: disable=W0611,C0411,C0412 +from fastapi import FastAPI # pylint: disable=W0611,C0411 import gradio # pylint: disable=W0611,C0411 startup_timer.record("gradio") errors.install([gradio]) @@ -148,6 +150,8 @@ def initialize(): def load_model(): shared.state.begin() shared.state.job = 'load model' + + """ try: modules.sd_models.load_model() modules.sd_models.skip_next_load = True @@ -155,17 +159,22 @@ def load_model(): errors.display(e, "loading stable diffusion model") log.error("Stable diffusion model failed to load") exit(1) + """ + Thread(target=lambda: shared.sd_model).start() + if shared.sd_model is None: log.warning("No stable diffusion model loaded") # exit(1) else: shared.opts.data["sd_model_checkpoint"] = shared.sd_model.sd_checkpoint_info.title - shared.opts.onchange("sd_model_checkpoint", wrap_queued_call(lambda: modules.sd_models.reload_model_weights())) + shared.opts.onchange("sd_model_checkpoint", wrap_queued_call(lambda: modules.sd_models.reload_model_weights()), call=False) + shared.state.end() startup_timer.record("checkpoint") def create_api(app): + log.debug('Creating API') from modules.api.api import Api api = Api(app, queue_lock) return api @@ -177,7 +186,6 @@ def monkey_patch_docs(): self.redoc_url = "/redoc" self.setup_original() - from fastapi import FastAPI setup_original = getattr(FastAPI, "setup_original", None) if setup_original is None: FastAPI.setup_original = FastAPI.setup @@ -199,8 +207,8 @@ def async_policy(): asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy()) -def start_ui(): - log.debug('Entering StartUI') +def start_common(): + log.debug('Entering start sequence') logging.disable(logging.NOTSET if cmd_opts.debug else logging.DEBUG) create_paths(opts) async_policy() @@ -208,6 +216,10 @@ def start_ui(): if shared.opts.clean_temp_dir_at_start: ui_tempdir.cleanup_tmpdr() startup_timer.record("cleanup") + + +def start_ui(): + log.debug('Creating UI') modules.script_callbacks.before_ui_callback() startup_timer.record("scripts before_ui_callback") shared.demo = modules.ui.create_ui() @@ -250,6 +262,12 @@ def start_ui(): shared.demo.server.wants_restart = False setup_middleware(app, cmd_opts) + if cmd_opts.subpath: + redirector = FastAPI() + redirector.get("/") + _mounted_app = gradio.mount_gradio_app(redirector, shared.demo, path=f"/{cmd_opts.subpath}") + shared.log.info('Redirector mounted: /{cmd_opts.subpath}') + cmd_opts.autolaunch = False startup_timer.record("start") @@ -262,12 +280,27 @@ def start_ui(): def webui(): - log.debug('Entering WebUI') + start_common() start_ui() load_model() log.info(f"Startup time: {startup_timer.summary()}") return shared.demo.server +def api_only(): + start_common() + app = FastAPI() + setup_middleware(app, cmd_opts) + api = create_api(app) + api.wants_restart = False + modules.script_callbacks.app_started_callback(None, app) + log.info(f"Startup time: {startup_timer.summary()}") + api.launch(server_name="0.0.0.0" if cmd_opts.listen else "127.0.0.1", port=cmd_opts.port if cmd_opts.port else 7861) + return api + + if __name__ == "__main__": - webui() + if cmd_opts.api_only: + api_only() + else: + webui()