diff --git a/.eslintrc.json b/.eslintrc.json index 95946364f..77b6fb242 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -118,7 +118,7 @@ "idbDel": "readonly", "idbAdd": "readonly", "idbCount": "readonly", - "idbClean": "readonly", + "idbFolderCleanup": "readonly", "initChangelog": "readonly", "sendNotification": "readonly", "monitorConnection": "readonly" diff --git a/TODO.md b/TODO.md index 6d8fdf98d..ed7c72258 100644 --- a/TODO.md +++ b/TODO.md @@ -93,6 +93,24 @@ TODO: *Prioritize*! - [asyncio.run](https://docs.python.org/3.14/library/asyncio-runner.html#asyncio.run) - [asyncio.Runner](https://docs.python.org/3.14/library/asyncio-runner.html#asyncio.Runner) +### Shutil + +#### rmtree + +- `onerror` deprecated and replaced with `onexc` in **Python 3.12** +``` python + def excRemoveReadonly(func, path, exc: BaseException): + import stat + shared.log.debug(f'Exception during cleanup: {func} {path} {type(exc).__name__}') + if func in (os.rmdir, os.remove, os.unlink) and isinstance(exc, PermissionError): + shared.log.debug(f'Retrying cleanup: {path}') + os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + func(path) + # ... + try: + shutil.rmtree(found.path, ignore_errors=False, onexc=excRemoveReadonly) +``` + ## Code TODO > npm run todo diff --git a/installer.py b/installer.py index b7067eb2d..5c7814021 100644 --- a/installer.py +++ b/installer.py @@ -1392,6 +1392,7 @@ def set_environment(): os.environ.setdefault('UVICORN_TIMEOUT_KEEP_ALIVE', '60') os.environ.setdefault('RUNAI_STREAMER_CHUNK_BYTESIZE', '2097152') os.environ.setdefault('RUNAI_STREAMER_MEMORY_LIMIT', '-1') + os.environ.setdefault('RUNAI_STREAMER_LOG_LEVEL', 'DEBUG' if os.environ.get('SD_LOAD_DEBUG') else 'WARNING') allocator = f'garbage_collection_threshold:{opts.get("torch_gc_threshold", 80)/100:0.2f},max_split_size_mb:512' if opts.get("torch_malloc", "native") == 'cudaMallocAsync': allocator += ',backend:cudaMallocAsync' diff --git a/javascript/gallery.js b/javascript/gallery.js index d3d8825dc..dd1adfd57 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -1,4 +1,5 @@ /* eslint-disable max-classes-per-file */ +/* eslint lines-between-class-members: ["error", "always", { "exceptAfterSingleLine": true }] */ let ws; let url; let currentImage; @@ -6,9 +7,10 @@ let pruneImagesTimer; let outstanding = 0; let lastSort = 0; let lastSortName = 'None'; -let idbIsCleaning = false; -let activeGalleryFolder = ''; const galleryHashes = new Set(); +let maintenanceController = new AbortController(); +const folderStylesheet = new CSSStyleSheet(); +const fileStylesheet = new CSSStyleSheet(); // Store separator states for the session const separatorStates = new Map(); const el = { @@ -21,8 +23,103 @@ const el = { const SUPPORTED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'tiff', 'jp2', 'jxl', 'gif', 'mp4', 'mkv', 'avi', 'mjpeg', 'mpg', 'avr']; -async function awaitForIDB(num = 0) { - while (outstanding > num || idbIsCleaning) await new Promise((resolve) => setTimeout(resolve, 50)); // eslint-disable-line no-promise-executor-return +async function awaitForIDB(num = 0, signal = null) { + const timeout = AbortSignal.timeout(180000); // Failsafe to ensure no memory leaks + const combinedSignals = signal ? AbortSignal.any([timeout, signal]) : timeout; + while (outstanding > num && !combinedSignals.aborted) await new Promise((resolve) => { setTimeout(resolve, 50); }); +} + +/** + * Wait for gallery to finish populating + * @param {number} expectedSize - Expected gallery size + * @param {AbortSignal} signal - AbortController signal + */ +async function awaitForGallery(expectedSize, signal) { + const timeout = AbortSignal.timeout(180000); // Failsafe to ensure no memory leaks + const combinedSignals = AbortSignal.any([timeout, signal]); + while (galleryHashes.size < expectedSize && !combinedSignals.aborted) await new Promise((resolve) => { setTimeout(resolve, 500); }); // longer interval because it's a low priority check +} + +function updateGalleryStyles() { + if (opts.theme_type?.toLowerCase() === 'modern') { + folderStylesheet.replaceSync(` + .gallery-folder { cursor: pointer; padding: 8px 6px 8px 6px; background-color: var(--sd-button-normal-color); border-radius: var(--sd-border-radius); text-align: left; min-width: 12em;} + .gallery-folder:hover { background-color: var(--button-primary-background-fill-hover); } + .gallery-folder-selected { background-color: var(--sd-button-selected-color); color: var(--sd-button-selected-text-color); } + .gallery-folder-icon { font-size: 1.2em; color: var(--sd-button-icon-color); margin-right: 1em; filter: drop-shadow(1px 1px 2px black); float: left; } + `); + } else { + folderStylesheet.replaceSync(` + .gallery-folder { cursor: pointer; padding: 8px 6px 8px 6px; } + .gallery-folder:hover { background-color: var(--button-primary-background-fill-hover); } + .gallery-folder-selected { background-color: var(--button-primary-background-fill); } + `); + } + fileStylesheet.replaceSync(` + .gallery-file { + object-fit: contain; + cursor: pointer; + height: ${opts.extra_networks_card_size}px; + width: ${opts.browser_fixed_width ? `${opts.extra_networks_card_size}px` : 'unset'}; + } + .gallery-file:hover { + filter: grayscale(100%); + } + `); +} + +// Classes + +/* This isn't as robust as the Web Locks API, but it will at least work if accessing a remote machine without HTTPS */ +class SimpleFunctionQueue { + #id; + #running; + #queue; + + constructor(id) { + this.#id = id; + this.#running = false; + this.#queue = []; + } + + /** + * @param {{ + * signal: AbortSignal, + * callback: Function + * }} config + */ + enqueue(config) { + if (!(config.signal instanceof AbortSignal) || typeof config.callback !== 'function') { + throw new Error('Invalid configuration. Object must contain an AbortSignal and a function'); + } + if (config.signal.aborted) { + debug(`${this.#id} Queue: Skipping addition to queue due to "${config.signal.reason}"`); + return; + } + this.#queue.push(config); + this.#tryRunNext(); + } + + async #tryRunNext() { + if (this.#running || !this.#queue.length) return; + try { + const { signal, callback } = this.#queue.shift(); + if (signal.aborted) { + return; + } + this.#running = true; + if (callback.constructor.name.toLowerCase() === 'asyncfunction') { + await callback(); + } else { + callback(); + } + } catch (err) { + error(`${this.#id} Queue:`, err); + } finally { + this.#running = false; + this.#tryRunNext(); + } + } } // HTML Elements @@ -32,32 +129,17 @@ class GalleryFolder extends HTMLElement { super(); this.name = decodeURI(name); this.shadow = this.attachShadow({ mode: 'open' }); + this.shadow.adoptedStyleSheets = [folderStylesheet]; } connectedCallback() { - const style = document.createElement('style'); // silly but necessasry since we're inside shadowdom - if (window.opts.theme_type === 'Modern') { - style.textContent = ` - .gallery-folder { cursor: pointer; padding: 8px 6px 8px 6px; background-color: var(--sd-button-normal-color); border-radius: var(--sd-border-radius); text-align: left; min-width: 12em;} - .gallery-folder:hover { background-color: var(--button-primary-background-fill-hover); } - .gallery-folder-selected { background-color: var(--sd-button-selected-color); color: var(--sd-button-selected-text-color); } - .gallery-folder-icon { font-size: 1.2em; color: var(--sd-button-icon-color); margin-right: 1em; filter: drop-shadow(1px 1px 2px black); float: left; } - `; - } else { - style.textContent = ` - .gallery-folder { cursor: pointer; padding: 8px 6px 8px 6px; } - .gallery-folder:hover { background-color: var(--button-primary-background-fill-hover); } - .gallery-folder-selected { background-color: var(--button-primary-background-fill); } - `; - } - this.shadow.appendChild(style); const div = document.createElement('div'); div.className = 'gallery-folder'; div.innerHTML = `\uf03e ${this.name}`; div.addEventListener('click', () => { for (const folder of el.folders.children) { - if (folder.name === this.name) folder.shadow.children[1].classList.add('gallery-folder-selected'); - else folder.shadow.children[1].classList.remove('gallery-folder-selected'); + if (folder.name === this.name) folder.shadow.firstElementChild.classList.add('gallery-folder-selected'); + else folder.shadow.firstElementChild.classList.remove('gallery-folder-selected'); } }); div.addEventListener('click', fetchFilesWS); // eslint-disable-line no-use-before-define @@ -208,10 +290,13 @@ async function delayFetchThumb(fn) { } class GalleryFile extends HTMLElement { - constructor(folder, file) { + #signal; + + constructor(folder, file, signal = undefined) { super(); this.folder = folder; this.name = file; + this.#signal = signal; this.size = 0; this.mtime = 0; this.hash = undefined; @@ -220,6 +305,7 @@ class GalleryFile extends HTMLElement { this.height = 0; this.src = `${this.folder}/${this.name}`; this.shadow = this.attachShadow({ mode: 'open' }); + this.shadow.adoptedStyleSheets = [fileStylesheet]; } async connectedCallback() { @@ -238,21 +324,6 @@ class GalleryFile extends HTMLElement { } this.hash = await getHash(`${this.folder}/${this.name}/${this.size}/${this.mtime}`); // eslint-disable-line no-use-before-define - galleryHashes.add(this.hash); - const style = document.createElement('style'); - const width = opts.browser_fixed_width ? `${opts.extra_networks_card_size}px` : 'unset'; - style.textContent = ` - .gallery-file { - object-fit: contain; - cursor: pointer; - height: ${opts.extra_networks_card_size}px; - width: ${width}; - } - .gallery-file:hover { - filter: grayscale(100%); - } - `; - const cache = (this.hash && opts.browser_cache) ? await idbGet(this.hash) : undefined; const img = document.createElement('img'); img.className = 'gallery-file'; @@ -306,6 +377,11 @@ class GalleryFile extends HTMLElement { img.src = `file=${this.src}`; } } + if (this.#signal && !this.#signal.aborted) { + // Guard against accessing external context from a stale initialization + galleryHashes.add(this.hash); // Add to hashes Set *after* any database operations + this.#signal = null; // Clean up reference to AbortSignal + } if (!ok) { return; } @@ -325,7 +401,6 @@ class GalleryFile extends HTMLElement { this.style.display = shouldDisplayBasedOnSearch ? 'unset' : 'none'; } - this.shadow.appendChild(style); this.shadow.appendChild(img); } } @@ -554,9 +629,15 @@ async function gallerySort(btn) { updateStatusWithSort(`${arr.length.toLocaleString()} images | ${Math.floor(t1 - t0).toLocaleString()}ms`); } +/** + * Function for removing the cleaning overlay + * @callback ClearMsgCallback + * @returns {void} + */ + /** * Generate and display the overlay to announce cleanup is in progress. - * @returns {() => void} Function for clearing the overlay + * @returns {ClearMsgCallback} */ function showCleaningMsg() { const parent = el.folders.parentElement; @@ -567,47 +648,75 @@ function showCleaningMsg() { parent.style.position = 'relative'; cleaningOverlay.style.cssText = 'position: absolute; height: 100%; width: 100%; background-color: hsl(210 50 20 / 0.8); display: flex; align-items: center; justify-content: center;'; msg.style.cssText = 'display: block; background-color: hsl(0 0 10); color: white; padding: 12px; border-radius: 8px; margin-right: 16px;'; - msg.innerText = 'Running thumbnail cleanup'; + msg.innerText = 'Thumbnail cleanup...'; anim.classList.add('idbBusyAnim'); cleaningOverlay.append(msg, anim); parent.append(cleaningOverlay); - return () => { - parent.style.position = ''; - cleaningOverlay.remove(); - }; + return () => { cleaningOverlay.remove(); }; } -async function thumbCacheCleanup() { - if (idbIsCleaning) return; - await awaitForIDB(); - idbIsCleaning = true; +const maintenanceQueue = new SimpleFunctionQueue('Maintenance'); - const t0 = performance.now(); - const cachedHashesCount = await idbCount() - .catch(() => 0); - if (cachedHashesCount < galleryHashes.size + 500) { - // Don't run when there aren't many excess entries - idbIsCleaning = false; +/** + * Handles calling the cleanup function for the thumbnail cache + * @param {string} folder - Folder to clean + * @param {number} imgCount - Expected number of images in gallery + * @param {AbortController} controller - AbortController that's handling this task + */ +async function thumbCacheCleanup(folder, imgCount, controller) { + try { + if (typeof folder !== 'string' || typeof imgCount !== 'number') { + throw new Error('Function called with invalid arguments'); + } + debug('Thumbnail DB cleanup: Waiting for gallery data to settle'); + await awaitForGallery(imgCount, controller.signal); + } catch (err) { + debug(`Thumbnail DB cleanup: Skipping cleanup for "${folder}" due to "${controller.signal.aborted ? controller.signal.reason : 'timeout'}"`); return; } - const removeOverlayFunc = showCleaningMsg(); - idbClean(galleryHashes, activeGalleryFolder) - .then((delcount, folder) => { - const t1 = performance.now(); - log(`Thumbnail DB cleanup: folder=${folder} kept=${galleryHashes.size} deleted=${delcount} time=${Math.floor(t1 - t0)}ms`); - }) - .catch((err) => { - error('Thumbnail DB cleanup: Cleanup failed.', err.message); - }) - .finally(() => { - removeOverlayFunc(); - idbIsCleaning = false; - }); + maintenanceQueue.enqueue({ + signal: controller.signal, + callback: async () => { + log(`Thumbnail DB cleanup: Checking if "${folder}" needs cleaning`); + const t0 = performance.now(); + const staticGalleryHashes = new Set(galleryHashes); // External context should be safe since this function run is guarded by AbortController/AbortSignal in the SimpleFunctionQueue + const cachedHashesCount = await idbCount(folder) + .catch((e) => { + error(`Thumbnail DB cleanup: Error when getting entry count for "${folder}".`, e); + return Infinity; // Forces next check to fail if something went wrong + }); + if (cachedHashesCount < staticGalleryHashes.size + 500) { + // Don't run when there aren't many excess entries + return; + } + + if (controller.signal.aborted) { + debug(`Thumbnail DB cleanup: Cancelling "${folder}" cleanup due to "${controller.signal.reason}"`); + return; + } + const cb_clearMsg = showCleaningMsg(); + await idbFolderCleanup(staticGalleryHashes, folder, controller.signal) + .then((delcount) => { + const t1 = performance.now(); + log(`Thumbnail DB cleanup: folder=${folder} kept=${staticGalleryHashes.size} deleted=${delcount} time=${Math.floor(t1 - t0)}ms`); + }) + .catch((reason) => { + if (typeof reason === 'string' || (reason instanceof DOMException && reason.name === 'AbortError')) { + log('Thumbnail DB cleanup:', reason?.message || reason); + } else { + error('Thumbnail DB cleanup:', reason.message); + } + }) + .finally(() => { + cb_clearMsg(); + }); + }, + }); } -async function fetchFilesHT(evt) { +async function fetchFilesHT(evt, controller) { const t0 = performance.now(); const fragment = document.createDocumentFragment(); updateStatusWithSort(`Folder: ${evt.target.name} | in-progress`); @@ -625,7 +734,7 @@ async function fetchFilesHT(evt) { const ext = fileName.split('.').pop().toLowerCase(); if (SUPPORTED_EXTENSIONS.includes(ext)) { numFiles++; - const f = new GalleryFile(data[0], fileName); + const f = new GalleryFile(data[0], fileName, controller.signal); fragment.appendChild(f); } } @@ -633,30 +742,33 @@ async function fetchFilesHT(evt) { el.files.appendChild(fragment); const t1 = performance.now(); - activeGalleryFolder = evt.target.name; log(`gallery: folder=${evt.target.name} num=${numFiles} time=${Math.floor(t1 - t0)}ms`); updateStatusWithSort(`Folder: ${evt.target.name} | ${numFiles.toLocaleString()} images | ${Math.floor(t1 - t0).toLocaleString()}ms`); addSeparators(); - thumbCacheCleanup(); + thumbCacheCleanup(evt.target.name, numFiles, controller); } async function fetchFilesWS(evt) { // fetch file-by-file list over websockets - if (idbIsCleaning) return; - galleryHashes.clear(); // Only called here because fetchFilesHT isn't called directly - el.files.innerHTML = ''; if (!url) return; + const controller = new AbortController(); // Only called here because fetchFilesHT isn't called directly + maintenanceController.abort('Gallery update'); // Abort previous controller + maintenanceController = controller; // Point to new controller for next time + galleryHashes.clear(); // Must happen AFTER the AbortController steps + + el.files.innerHTML = ''; + updateGalleryStyles(); if (ws && ws.readyState === WebSocket.OPEN) ws.close(); // abort previous request let wsConnected = false; try { ws = new WebSocket(`${url}/sdapi/v1/browser/files`); - wsConnected = await wsConnect(ws); + wsConnected = await wsConnect(ws); // Warning. This changes "evt". } catch (err) { log('gallery: ws connect error', err); return; } log(`gallery: connected=${wsConnected} state=${ws?.readyState} url=${ws?.url}`); if (!wsConnected) { - await fetchFilesHT(evt); // fallback to http + await fetchFilesHT(evt, controller); // fallback to http return; } updateStatusWithSort(`Folder: ${evt.target.name}`); @@ -674,7 +786,7 @@ async function fetchFilesWS(evt) { // fetch file-by-file list over websockets const fileName = data[1]; const ext = fileName.split('.').pop().toLowerCase(); if (SUPPORTED_EXTENSIONS.includes(ext)) { - const file = new GalleryFile(data[0], fileName); + const file = new GalleryFile(data[0], fileName, controller.signal); numFiles++; fragment.appendChild(file); if (numFiles % 100 === 0) { @@ -688,11 +800,10 @@ async function fetchFilesWS(evt) { // fetch file-by-file list over websockets ws.onclose = (event) => { el.files.appendChild(fragment); // gallerySort(); - activeGalleryFolder = evt.target.name; log(`gallery: folder=${evt.target.name} num=${numFiles} time=${Math.floor(t1 - t0)}ms`); updateStatusWithSort(`Folder: ${evt.target.name} | ${numFiles.toLocaleString()} images | ${Math.floor(t1 - t0).toLocaleString()}ms`); addSeparators(); - thumbCacheCleanup(); + thumbCacheCleanup(evt.target.name, numFiles, controller); }; ws.onerror = (event) => { log('gallery ws error', event); @@ -759,6 +870,7 @@ async function initGallery() { // triggered on gradio change to monitor when ui error('initGallery', 'Missing gallery elements'); return; } + updateGalleryStyles(); setOverlayAnimation(); el.search.addEventListener('input', gallerySearch); el.btnSend = gradioApp().getElementById('tab-gallery-send-image'); diff --git a/javascript/indexdb.js b/javascript/indexdb.js index ee84cde12..24386eaa5 100644 --- a/javascript/indexdb.js +++ b/javascript/indexdb.js @@ -1,9 +1,12 @@ -let db; +/** + * @type {?IDBDatabase} + */ +let db = null; async function initIndexDB() { async function createDB() { return new Promise((resolve, reject) => { - const request = indexedDB.open('SDNext'); + const request = indexedDB.open('SDNext', 2); request.onerror = (evt) => reject(evt); request.onsuccess = (evt) => { db = evt.target.result; @@ -16,9 +19,15 @@ async function initIndexDB() { }; request.onupgradeneeded = (evt) => { db = evt.target.result; - const store = db.createObjectStore('thumbs', { keyPath: 'hash' }); - store.createIndex('hash', 'hash', { unique: true }); - const index = store.index('hash'); + const oldver = evt.oldVersion; + if (oldver < 1) { + const store = db.createObjectStore('thumbs', { keyPath: 'hash' }); + store.createIndex('hash', 'hash', { unique: true }); + } + if (oldver < 2) { + const existingStore = request.transaction.objectStore('thumbs'); + existingStore.createIndex('folder', 'folder', { unique: false }); + } resolve(); }; }); @@ -75,57 +84,104 @@ async function put(record) { }); } -async function idbGetAllKeys() { +async function idbGetAllKeys(index = null, query = null) { if (!db) return null; return new Promise((resolve, reject) => { - const request = db - .transaction('thumbs', 'readonly') - .objectStore('thumbs') - .getAllKeys(); - request.onsuccess = () => resolve(request.result); - request.onerror = (evt) => reject(evt); + try { + let request; + const transaction = db.transaction('thumbs', 'readonly'); + const store = transaction.objectStore('thumbs'); + if (index) { + request = store.index(index).getAllKeys(query); + } else { + request = store.getAllKeys(query); + } + request.onsuccess = () => resolve(request.result); + request.onerror = (e) => reject(e); + transaction.onabort = (e) => reject(e); + } catch (err) { + reject(err); + } }); } -async function idbCount() { +/** + * Get the number of entries in the IndexedDB thumbnail cache. + * @global + * @param {?string} folder - If specified, get the count for this gallery folder. Otherwise get the total count. + * @returns {Promise} + */ +async function idbCount(folder = null) { if (!db) return null; return new Promise((resolve, reject) => { - const request = db - .transaction('thumbs', 'readonly') - .objectStore('thumbs') - .count(); - request.onsuccess = () => resolve(request.result); - request.onerror = (evt) => reject(evt); + try { + let request; + const transaction = db.transaction('thumbs', 'readonly'); + const store = transaction.objectStore('thumbs'); + if (folder) { + request = store.index('folder').count(folder); + } else { + request = store.count(); + } + request.onsuccess = () => resolve(request.result); + request.onerror = (e) => reject(e); + transaction.onabort = (e) => reject(e); + } catch (err) { + reject(err); + } }); } -async function idbClean(keepSet, folder = null) { +/** + * Cleanup function for IndexedDB thumbnail cache. + * @global + * @param {Set} keepSet - Set containing the hashes of the current files in the folder + * @param {string} folder - Folder name/path + * @param {AbortSignal} signal - Signal from the AbortController for thumbCacheCleanup() + */ +async function idbFolderCleanup(keepSet, folder, signal) { if (!db) return null; if (!(keepSet instanceof Set)) { - throw new TypeError('IndexedDB cleaning function must be given a Set() of hashes to keep'); + throw new TypeError('IndexedDB cleaning function must be given a Set() of the current gallery hashes'); } - if (folder === null) { + if (typeof folder !== 'string') { throw new Error('IndexedDB cleaning function must be told the current active folder'); } + + let removals = new Set(await idbGetAllKeys('folder', folder)); + removals = removals.difference(keepSet); // Don't need to keep full set in memory + const totalRemovals = removals.size; + if (signal.aborted) { + throw `Aborting. ${signal.reason}`; // eslint-disable-line no-throw-literal + } return new Promise((resolve, reject) => { - let counter = 0; - const request = db - .transaction('thumbs', 'readwrite') - .objectStore('thumbs') - .openCursor(); - request.onsuccess = (evt) => { - const cursor = evt.target.result; - if (cursor) { - if (folder === cursor.value.folder && !keepSet.has(cursor.key)) { - cursor.delete(); - counter++; - } - cursor.continue(); - } else { - resolve(counter, folder); - } + const transaction = db.transaction('thumbs', 'readwrite'); + function abortTransaction() { + signal.removeEventListener('abort', abortTransaction); + transaction.abort(); + } + signal.addEventListener('abort', abortTransaction); + + try { + const store = transaction.objectStore('thumbs'); + removals.forEach((entry) => { store.delete(entry); }); + } catch (err) { + error(err); + abortTransaction(); + } + + transaction.onabort = () => { + signal.removeEventListener('abort', abortTransaction); + reject(`Aborting. ${signal.reason}`); // eslint-disable-line prefer-promise-reject-errors + }; + transaction.onerror = () => { + signal.removeEventListener('abort', abortTransaction); + reject(new Error('Database transaction error')); + }; + transaction.oncomplete = async () => { + signal.removeEventListener('abort', abortTransaction); + resolve(totalRemovals); }; - request.onerror = (evt) => reject(evt); }); } diff --git a/javascript/startup.js b/javascript/startup.js index 82194d942..5a1af8f5e 100644 --- a/javascript/startup.js +++ b/javascript/startup.js @@ -38,7 +38,6 @@ async function initStartup() { await initAccordions(); await initSettings(); await initImageViewer(); - await initGallery(); await initiGenerationParams(); await initChangelog(); await setupControlUI(); @@ -47,6 +46,8 @@ async function initStartup() { await reconnectUI(); await waitForOpts(); + await initGallery(); + log('mountURL', window.opts.subpath); if (window.opts.subpath?.length > 0) { window.subpath = window.opts.subpath; diff --git a/modules/control/unit.py b/modules/control/unit.py index f47c1caac..5591749de 100644 --- a/modules/control/unit.py +++ b/modules/control/unit.py @@ -180,7 +180,7 @@ class Unit(): # mashup of gradio controls and mapping to actual implementation c self.adapter.load(model_id) else: self.controls.append(model_id) - model_id.change(fn=self.adapter.load, inputs=[model_id], outputs=[result_txt], show_progress=True) + model_id.change(fn=self.adapter.load, inputs=[model_id], outputs=[result_txt], show_progress='full') if extra_controls is not None and len(extra_controls) > 0: extra_controls[0].change(fn=adapter_extra, inputs=extra_controls) elif self.type == 'controlnet': @@ -189,8 +189,8 @@ class Unit(): # mashup of gradio controls and mapping to actual implementation c self.controlnet.load(model_id) else: self.controls.append(model_id) - model_id.change(fn=self.controlnet.load, inputs=[model_id], outputs=[result_txt], show_progress=True) - model_id.change(fn=control_choices, inputs=[model_id], outputs=[control_mode, control_tile], show_progress=False) + model_id.change(fn=self.controlnet.load, inputs=[model_id], outputs=[result_txt], show_progress='full') + model_id.change(fn=control_choices, inputs=[model_id], outputs=[control_mode, control_tile], show_progress='hidden') if extra_controls is not None and len(extra_controls) > 0: extra_controls[0].change(fn=controlnet_extra, inputs=extra_controls) elif self.type == 'xs': @@ -199,7 +199,7 @@ class Unit(): # mashup of gradio controls and mapping to actual implementation c self.controlnet.load(model_id) else: self.controls.append(model_id) - model_id.change(fn=self.controlnet.load, inputs=[model_id, extra_controls[0]], outputs=[result_txt], show_progress=True) + model_id.change(fn=self.controlnet.load, inputs=[model_id, extra_controls[0]], outputs=[result_txt], show_progress='full') if extra_controls is not None and len(extra_controls) > 0: extra_controls[0].change(fn=controlnetxs_extra, inputs=extra_controls) elif self.type == 'lite': @@ -208,7 +208,7 @@ class Unit(): # mashup of gradio controls and mapping to actual implementation c self.controlnet.load(model_id) else: self.controls.append(model_id) - model_id.change(fn=self.controlnet.load, inputs=[model_id], outputs=[result_txt], show_progress=True) + model_id.change(fn=self.controlnet.load, inputs=[model_id], outputs=[result_txt], show_progress='full') if extra_controls is not None and len(extra_controls) > 0: extra_controls[0].change(fn=controlnetxs_extra, inputs=extra_controls) elif self.type == 'reference': @@ -229,7 +229,7 @@ class Unit(): # mashup of gradio controls and mapping to actual implementation c self.process.load(process_id) else: self.controls.append(process_id) - process_id.change(fn=self.process.load, inputs=[process_id], outputs=[result_txt], show_progress=True) + process_id.change(fn=self.process.load, inputs=[process_id], outputs=[result_txt], show_progress='full') if reset_btn is not None: reset_btn.click(fn=self.reset, inputs=[], outputs=[enabled_cb, model_id, process_id, model_strength]) if preview_btn is not None: diff --git a/modules/framepack/framepack_ui.py b/modules/framepack/framepack_ui.py index de1ab5347..394387df7 100644 --- a/modules/framepack/framepack_ui.py +++ b/modules/framepack/framepack_ui.py @@ -115,6 +115,6 @@ def create_ui(prompt, negative, styles, _overrides, init_image, last_image, mp4_ _js="submit_framepack", inputs=state_inputs + framepack_inputs, outputs=framepack_outputs, - show_progress=False, + show_progress='hidden', ) generate.click(**framepack_dict) diff --git a/modules/generation_parameters_copypaste.py b/modules/generation_parameters_copypaste.py index efeb6f2ba..9ccf44ee7 100644 --- a/modules/generation_parameters_copypaste.py +++ b/modules/generation_parameters_copypaste.py @@ -158,7 +158,7 @@ def connect_paste_params_buttons(): fn=send_image, inputs=[binding.source_image_component], outputs=[destination_image_component], - show_progress=False, + show_progress='hidden', ) override_settings_component = binding.override_settings_component or paste_fields[binding.tabname]["override_settings_component"] @@ -177,7 +177,7 @@ def connect_paste_params_buttons(): _js=f"switch_to_{binding.tabname}", inputs=[], outputs=[], - show_progress=False, + show_progress='hidden', ) @@ -294,12 +294,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, + show_progress='hidden', ) button.click( fn=None, _js=f"recalculate_prompts_{tabname}", inputs=[], outputs=[], - show_progress=False, + show_progress='hidden', ) diff --git a/modules/ltx/ltx_ui.py b/modules/ltx/ltx_ui.py index ade98132a..7d99bef65 100644 --- a/modules/ltx/ltx_ui.py +++ b/modules/ltx/ltx_ui.py @@ -72,6 +72,6 @@ def create_ui(prompt, negative, styles, overrides, init_image, init_strength, la _js="submit_ltx", inputs=state_inputs + video_inputs, outputs=video_outputs, - show_progress=False, + show_progress='hidden', ) generate.click(**video_dict) diff --git a/modules/sd_hijack_safetensors.py b/modules/sd_hijack_safetensors.py index 20e85f40d..a9ca5ca78 100644 --- a/modules/sd_hijack_safetensors.py +++ b/modules/sd_hijack_safetensors.py @@ -1,6 +1,3 @@ -import io -import os -import contextlib import safetensors.torch import transformers from installer import install, log @@ -15,21 +12,17 @@ def hijacked_load_file(checkpoint_file, device="cpu"): if not checkpoint_file.endswith('.safetensors'): return orig_load_file(checkpoint_file, device=device) - install('runai_model_streamer') - log.debug(f'Loader: method=runai type=file chunk={os.environ["RUNAI_STREAMER_CHUNK_BYTESIZE"]} limit={os.environ["RUNAI_STREAMER_MEMORY_LIMIT"]} device={device}') + install('runai_model_streamer>=0.15.1') state_dict = {} - stdout = io.StringIO() from runai_model_streamer import SafetensorsStreamer - with contextlib.redirect_stdout(stdout): - try: - with SafetensorsStreamer() as streamer: - streamer.stream_file(checkpoint_file) - for key, tensor in streamer.get_tensors(): - state_dict[key] = tensor.to(device) - except Exception as e: - log.error(f'Loader: {e}') - log.error(stdout.getvalue()) - errors.display(e, 'runai') + try: + with SafetensorsStreamer() as streamer: + streamer.stream_file(checkpoint_file) + for key, tensor in streamer.get_tensors(): + state_dict[key] = tensor.to(device) + except Exception as e: + log.error(f'Loader: {e}') + errors.display(e, 'runai') return state_dict @@ -37,21 +30,17 @@ def hijacked_load_state_dict(checkpoint_file, is_quantized: bool = False, map_lo if not checkpoint_file.endswith(".safetensors"): return orig_load_state_dict(checkpoint_file=checkpoint_file, is_quantized=is_quantized, map_location=map_location, weights_only=weights_only) - install('runai_model_streamer') - log.trace(f'Loader: method=runai type=dict chunk={os.environ["RUNAI_STREAMER_CHUNK_BYTESIZE"]} limit={os.environ["RUNAI_STREAMER_MEMORY_LIMIT"]} device={map_location} quantized={is_quantized}') + install('runai_model_streamer>=0.15.1') state_dict = {} - stdout = io.StringIO() from runai_model_streamer import SafetensorsStreamer - with contextlib.redirect_stdout(stdout): - try: - with SafetensorsStreamer() as streamer: - streamer.stream_file(checkpoint_file) - for key, tensor in streamer.get_tensors(): - state_dict[key] = tensor.to(map_location) if map_location != "meta" else tensor - except Exception as e: - log.error(f'Loader: {e}') - log.error(stdout.getvalue()) - errors.display(e, 'runai') + try: + with SafetensorsStreamer() as streamer: + streamer.stream_file(checkpoint_file) + for key, tensor in streamer.get_tensors(): + state_dict[key] = tensor.to(map_location) if map_location != "meta" else tensor + except Exception as e: + log.error(f'Loader: {e}') + errors.display(e, 'runai') return state_dict diff --git a/modules/sd_models.py b/modules/sd_models.py index a8d501105..66048d58e 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -65,6 +65,8 @@ def set_huggingface_options(): else: sd_hijack_accelerate.restore_accelerate() if (shared.opts.runai_streamer_diffusers or shared.opts.runai_streamer_transformers) and (sys.platform == 'linux'): + import os + log.debug(f'Loader: runai enabled chunk={os.environ["RUNAI_STREAMER_CHUNK_BYTESIZE"]} limit={os.environ["RUNAI_STREAMER_MEMORY_LIMIT"]}') sd_hijack_safetensors.hijack_safetensors(shared.opts.runai_streamer_diffusers, shared.opts.runai_streamer_transformers) else: sd_hijack_safetensors.restore_safetensors() @@ -629,8 +631,7 @@ def load_sdnq_model(checkpoint_info, pipeline, diffusers_load_config, op): if shared.opts.runai_streamer_diffusers and (sys.platform == 'linux'): load_method = 'streamer' from installer import install - install('runai_model_streamer') - shared.log.trace(f'Loader: method={load_method} chunk={os.environ["RUNAI_STREAMER_CHUNK_BYTESIZE"]} limit={os.environ["RUNAI_STREAMER_MEMORY_LIMIT"]}') + install('runai_model_streamer>=0.15.1') elif shared.opts.sd_parallel_load: load_method = 'threaded' else: diff --git a/modules/ui_common.py b/modules/ui_common.py index 71a062bfa..3a3bac650 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -285,23 +285,23 @@ def create_output_panel(tabname, preview=True, prompt=None, height=None, transfe with gr.Group(): html_info = gr.HTML(elem_id=f'html_info_{tabname}', elem_classes="infotext", visible=False) # contains raw infotext as returned by wrapped call html_info_formatted = gr.HTML(elem_id=f'html_info_formatted_{tabname}', elem_classes="infotext", visible=True) # contains html formatted infotext - html_info.change(fn=infotext_to_html, inputs=[html_info], outputs=[html_info_formatted], show_progress=False) + html_info.change(fn=infotext_to_html, inputs=[html_info], outputs=[html_info_formatted], show_progress='hidden') html_log = gr.HTML(elem_id=f'html_log_{tabname}') generation_info = gr.Textbox(visible=False, elem_id=f'generation_info_{tabname}') generation_info_button = gr.Button(visible=False, elem_id=f"{tabname}_generation_info_button") result_field = result_info or html_info_formatted - generation_info_button.click(fn=update_generation_info, show_progress=False, + generation_info_button.click(fn=update_generation_info, show_progress='hidden', _js="(x, y, z) => [x, y, selected_gallery_index()]", # triggered on gallery change from js inputs=[generation_info, html_info, html_info], outputs=[html_info, result_field], ) - save.click(fn=call_queue.wrap_gradio_call(save_files), show_progress=False, + save.click(fn=call_queue.wrap_gradio_call(save_files), show_progress='hidden', _js="(x, y, z, i) => [x, y, z, selected_gallery_index()]", inputs=[generation_info, result_gallery, html_info, html_info], outputs=[download_files, html_log], ) - delete.click(fn=call_queue.wrap_gradio_call(delete_files), show_progress=False, + delete.click(fn=call_queue.wrap_gradio_call(delete_files), show_progress='hidden', _js="(x, y, i, j) => [x, y, ...selected_gallery_files()]", inputs=[generation_info, result_gallery, html_info, html_info], outputs=[result_gallery, html_log], @@ -345,7 +345,7 @@ def create_refresh_button(refresh_component, refresh_method, refreshed_args = No return gr.update(**args) refresh_button = ui_components.ToolButton(value=ui_symbols.refresh, elem_id=elem_id, visible=visible) - refresh_button.click(fn=refresh, inputs=[], outputs=[refresh_component], show_progress=False) + refresh_button.click(fn=refresh, inputs=[], outputs=[refresh_component], show_progress='hidden') return refresh_button @@ -374,7 +374,7 @@ def reuse_seed(seed_component: gr.Number, reuse_button: gr.Button, subseed:bool= shared.log.debug(f'Reuse seed: index={selected_gallery_index} seed={seed} subseed={subseed}') return seed - reuse_button.click(fn=reuse_click, _js="selected_gallery_index", inputs=[seed_component], outputs=[seed_component], show_progress=False) + reuse_button.click(fn=reuse_click, _js="selected_gallery_index", inputs=[seed_component], outputs=[seed_component], show_progress='hidden') def connect_reuse_seed(seed: gr.Number, reuse_seed_btn: gr.Button, generation_info: gr.Textbox, is_subseed, subseed_strength=None): @@ -405,9 +405,9 @@ def connect_reuse_seed(seed: gr.Number, reuse_seed_btn: gr.Button, generation_in return [restore_seed, gr_show(False)] dummy_component = gr.Number(visible=False, value=0) if subseed_strength is None: - reuse_seed_btn.click(fn=copy_seed, _js="(x, y) => [x, selected_gallery_index()]", show_progress=False, inputs=[generation_info, dummy_component], outputs=[seed, dummy_component]) + reuse_seed_btn.click(fn=copy_seed, _js="(x, y) => [x, selected_gallery_index()]", show_progress='hidden', inputs=[generation_info, dummy_component], outputs=[seed, dummy_component]) else: - reuse_seed_btn.click(fn=copy_seed, _js="(x, y) => [x, selected_gallery_index()]", show_progress=False, inputs=[generation_info, dummy_component], outputs=[seed, dummy_component, subseed_strength]) + reuse_seed_btn.click(fn=copy_seed, _js="(x, y) => [x, selected_gallery_index()]", show_progress='hidden', inputs=[generation_info, dummy_component], outputs=[seed, dummy_component, subseed_strength]) def update_token_counter(text): diff --git a/modules/ui_control.py b/modules/ui_control.py index 4bc95917c..5480c6dce 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -243,20 +243,20 @@ def create_ui(_blocks: gr.Blocks=None): for u in units: controls.extend(u.controls) btn_update = gr.Button('Update', interactive=True, visible=False, elem_id='control_update') - btn_update.click(fn=get_units, inputs=controls, outputs=[], show_progress=False, queue=False) + btn_update.click(fn=get_units, inputs=controls, outputs=[], show_progress='hidden', queue=False) show_input.change(fn=lambda x: gr.update(visible=x), inputs=[show_input], outputs=[column_input]) show_preview.change(fn=lambda x: gr.update(visible=x), inputs=[show_preview], outputs=[column_preview]) input_type.change(fn=lambda x: gr.update(visible=x == 2), inputs=[input_type], outputs=[column_init]) - btn_prompt_counter.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[prompt], outputs=[prompt_counter], show_progress = False) - btn_negative_counter.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[negative], outputs=[negative_counter], show_progress = False) + btn_prompt_counter.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[prompt], outputs=[prompt_counter], show_progress = 'hidden') + btn_negative_counter.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[negative], outputs=[negative_counter], show_progress = 'hidden') select_dict = dict( fn=helpers.select_input, _js="controlInputMode", inputs=[input_mode, input_image, init_image, input_type, input_video, input_batch, input_folder], outputs=[output_tabs, preview_process, result_txt, width_before, height_before], - show_progress=False, + show_progress='hidden', queue=False, ) @@ -305,7 +305,7 @@ def create_ui(_blocks: gr.Blocks=None): _js="submit_control", inputs=[tabs_state, state, tabs_state] + input_fields + input_script_args, outputs=output_fields, - show_progress=True, + show_progress='full', ) prompt.submit(**control_dict) negative.submit(**control_dict) diff --git a/modules/ui_docs.py b/modules/ui_docs.py index 8b1381ce8..08306e5ac 100644 --- a/modules/ui_docs.py +++ b/modules/ui_docs.py @@ -242,7 +242,7 @@ def create_ui_logs(): _changelog_result = gr.HTML(elem_id="changelog_result") changelog_markdown = gr.Markdown('', elem_id="changelog_markdown") - get_changelog_btn.click(fn=get_changelog, outputs=[changelog_markdown], show_progress=True) + get_changelog_btn.click(fn=get_changelog, outputs=[changelog_markdown], show_progress='full') def create_ui_github(): @@ -254,9 +254,9 @@ def create_ui_github(): with gr.Row(): github_md_btn = gr.Button(value='html2md', elem_id="github_md_btn", visible=False) github_md = gr.Markdown(elem_id="github_md", value='', elem_classes="github-md") - github_search.submit(fn=search_github, inputs=[github_search], outputs=[github_result], show_progress=True) - github_search_btn.click(fn=search_github, inputs=[github_search], outputs=[github_result], show_progress=True) - github_md_btn.click(fn=get_github_page, _js='getGitHubWikiPage', inputs=[github_search], outputs=[github_md], show_progress=True) + github_search.submit(fn=search_github, inputs=[github_search], outputs=[github_result], show_progress='full') + github_search_btn.click(fn=search_github, inputs=[github_search], outputs=[github_result], show_progress='full') + github_md_btn.click(fn=get_github_page, _js='getGitHubWikiPage', inputs=[github_search], outputs=[github_md], show_progress='full') def create_ui_docs(): @@ -268,10 +268,10 @@ def create_ui_docs(): with gr.Row(): docs_md_btn = gr.Button(value='html2md', elem_id="docs_md_btn", visible=False) docs_md = gr.Markdown(elem_id="docs_md", value='', elem_classes="docs-md") - docs_search.submit(fn=search_docs, inputs=[docs_search], outputs=[docs_result], show_progress=False) - docs_search.change(fn=search_docs, inputs=[docs_search], outputs=[docs_result], show_progress=False) - docs_search_btn.click(fn=search_docs, inputs=[docs_search], outputs=[docs_result], show_progress=False) - docs_md_btn.click(fn=get_docs_page, _js='getDocsPage', inputs=[docs_search], outputs=[docs_md], show_progress=False) + docs_search.submit(fn=search_docs, inputs=[docs_search], outputs=[docs_result], show_progress='hidden') + docs_search.change(fn=search_docs, inputs=[docs_search], outputs=[docs_result], show_progress='hidden') + docs_search_btn.click(fn=search_docs, inputs=[docs_search], outputs=[docs_result], show_progress='hidden') + docs_md_btn.click(fn=get_docs_page, _js='getDocsPage', inputs=[docs_search], outputs=[docs_md], show_progress='hidden') def create_ui(): diff --git a/modules/ui_extensions.py b/modules/ui_extensions.py index f1240a64c..640c0d622 100644 --- a/modules/ui_extensions.py +++ b/modules/ui_extensions.py @@ -191,10 +191,11 @@ def install_extension(extension_to_install, search_text, sort_column): def uninstall_extension(extension_path, search_text, sort_column): - def excRemoveReadonly(func, path, exc: Exception): + def errorRemoveReadonly(func, path, exc): import stat - shared.log.debug(f'Exception during cleanup: {func} {path} {type(exc).__name__}') - if func in (os.rmdir, os.remove, os.unlink) and isinstance(exc, PermissionError): + excvalue = exc[1] + shared.log.debug(f'Exception during cleanup: {func} {path} {excvalue.strerror}') + if func in (os.rmdir, os.remove, os.unlink) and excvalue.errno == errno.EACCES: shared.log.debug(f'Retrying cleanup: {path}') os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) func(path) @@ -203,7 +204,7 @@ def uninstall_extension(extension_path, search_text, sort_column): if len(found) > 0 and os.path.isdir(extension_path): found = found[0] try: - shutil.rmtree(found.path, ignore_errors=False, onexc=excRemoveReadonly) + shutil.rmtree(found.path, ignore_errors=False, onerror=errorRemoveReadonly) # pylint: disable=deprecated-argument # extensions.extensions = [extension for extension in extensions.extensions if os.path.abspath(found.path) != os.path.abspath(extension_path)] except Exception as e: shared.log.warning(f'Extension uninstall failed: {found.path} {e}') diff --git a/modules/ui_history.py b/modules/ui_history.py index 8cfc43bdb..3787278f3 100644 --- a/modules/ui_history.py +++ b/modules/ui_history.py @@ -8,4 +8,4 @@ def create_ui(): _history_table = gr.HTML('', elem_id='history_table') with gr.Row(): _history_timeline = gr.HTML('', elem_id='history_timeline') - btn_refresh.click(_js='refreshHistory', fn=None, inputs=[], outputs=[], show_progress=False) + btn_refresh.click(_js='refreshHistory', fn=None, inputs=[], outputs=[], show_progress='hidden') diff --git a/modules/ui_img2img.py b/modules/ui_img2img.py index a89046df0..d6e1591a2 100644 --- a/modules/ui_img2img.py +++ b/modules/ui_img2img.py @@ -203,7 +203,7 @@ def create_ui(): img2img_html_info, img2img_html_log, ], - show_progress=False, + show_progress='hidden', ) img2img_prompt.submit(**img2img_dict) img2img_negative_prompt.submit(**img2img_dict) @@ -229,8 +229,8 @@ def create_ui(): ) interrogate_btn.click(fn=lambda *args: process_interrogate(*args), **interrogate_args) - img2img_token_button.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[img2img_prompt], outputs=[img2img_token_counter], show_progress = False) - img2img_negative_token_button.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[img2img_negative_prompt], outputs=[img2img_negative_token_counter], show_progress = False) + img2img_token_button.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[img2img_prompt], outputs=[img2img_token_counter], show_progress = 'hidden') + img2img_negative_token_button.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[img2img_negative_prompt], outputs=[img2img_negative_token_counter], show_progress = 'hidden') ui_extra_networks.setup_ui(extra_networks_ui_img2img, img2img_gallery) img2img_paste_fields = [ diff --git a/modules/ui_models.py b/modules/ui_models.py index 8ea7459a2..909613c14 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -532,7 +532,7 @@ def create_ui(): _js="downloadCivitModel", inputs=[_dummy, _dummy, _dummy, civit_folder, civit_token, civitai_models_output], outputs=[civitai_models_output], - show_progress=True, + show_progress='full', ) with gr.Tab(label="Huggingface", elem_id="models_huggingface_tab"): diff --git a/modules/ui_sections.py b/modules/ui_sections.py index ffa0fbcfb..0d57eccb2 100644 --- a/modules/ui_sections.py +++ b/modules/ui_sections.py @@ -45,7 +45,7 @@ def create_toprow(is_img2img: bool = False, id_part: str = None, generate_visibl button_paste = gr.Button(value='Restore', variant='secondary', elem_id=f"{id_part}_paste") # symbols.paste button_clear = gr.Button(value='Clear', variant='secondary', elem_id=f"{id_part}_clear_prompt_btn") # symbols.clear button_extra = gr.Button(value='Networks', variant='secondary', elem_id=f"{id_part}_extra_networks_btn") # symbols.networks - button_clear.click(fn=lambda *x: ['', ''], inputs=[prompt, negative_prompt], outputs=[prompt, negative_prompt], show_progress=False) + button_clear.click(fn=lambda *x: ['', ''], inputs=[prompt, negative_prompt], outputs=[prompt, negative_prompt], show_progress='hidden') with gr.Row(elem_id=f"{id_part}_counters"): token_counter = gr.HTML(value="0/75", elem_id=f"{id_part}_token_counter", elem_classes=["token-counter"], visible=False) token_button = gr.Button(visible=False, elem_id=f"{id_part}_token_button") @@ -57,9 +57,9 @@ def create_toprow(is_img2img: bool = False, id_part: str = None, generate_visibl styles_btn_select = ToolButton('Select', elem_id=f"{id_part}_styles_select", visible=False) styles_btn_apply = ToolButton(ui_symbols.style_apply, elem_id=f"{id_part}_styles_apply", visible=True) styles_btn_save = ToolButton(ui_symbols.style_save, elem_id=f"{id_part}_styles_save", visible=True) - styles_btn_select.click(_js="applyStyles", fn=parse_style, inputs=[styles], outputs=[styles], show_progress=False) - styles_btn_apply.click(fn=apply_styles, inputs=[prompt, negative_prompt, styles], outputs=[prompt, negative_prompt, styles], show_progress=False) - styles_btn_save.click(fn=lambda: None, _js='() => quickSaveStyle()', inputs=[], outputs=[], show_progress=False) + styles_btn_select.click(_js="applyStyles", fn=parse_style, inputs=[styles], outputs=[styles], show_progress='hidden') + styles_btn_apply.click(fn=apply_styles, inputs=[prompt, negative_prompt, styles], outputs=[prompt, negative_prompt, styles], show_progress='hidden') + styles_btn_save.click(fn=lambda: None, _js='() => quickSaveStyle()', inputs=[], outputs=[], show_progress='hidden') return prompt, styles, negative_prompt, submit, reprocess, button_paste, button_extra, token_counter, token_button, negative_token_counter, negative_token_button @@ -85,9 +85,9 @@ def create_resolution_inputs(tab, default_width=1024, default_height=1024): ar_list = ['AR'] + [x.strip() for x in shared.opts.aspect_ratios.split(',') if x.strip() != ''] ar_dropdown = gr.Dropdown(show_label=False, interactive=True, choices=ar_list, value=ar_list[0], elem_id=f"{tab}_ar", elem_classes=["ar-dropdown"]) for c in [ar_dropdown, width, height]: - c.change(fn=ar_change, inputs=[ar_dropdown, width, height], outputs=[width, height], show_progress=False) + c.change(fn=ar_change, inputs=[ar_dropdown, width, height], outputs=[width, height], show_progress='hidden') res_switch_btn = ToolButton(value=ui_symbols.switch, elem_id=f"{tab}_res_btn_swap") - res_switch_btn.click(lambda w, h: (h, w), inputs=[width, height], outputs=[width, height], show_progress=False) + res_switch_btn.click(lambda w, h: (h, w), inputs=[width, height], outputs=[width, height], show_progress='hidden') return width, height @@ -120,8 +120,8 @@ def create_seed_inputs(tab, reuse_visible=True, accordion=True, subseed_visible= with gr.Row(visible=seed_resize_visible): seed_resize_from_w = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize seed from width", value=0, elem_id=f"{tab}_seed_resize_from_w") seed_resize_from_h = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize seed from height", value=0, elem_id=f"{tab}_seed_resize_from_h") - random_seed.click(fn=lambda: -1, show_progress=False, inputs=[], outputs=[seed]) - random_subseed.click(fn=lambda: -1, show_progress=False, inputs=[], outputs=[subseed]) + random_seed.click(fn=lambda: -1, show_progress='hidden', inputs=[], outputs=[seed]) + random_subseed.click(fn=lambda: -1, show_progress='hidden', inputs=[], outputs=[subseed]) return seed, reuse_seed, subseed, reuse_subseed, subseed_strength, seed_resize_from_h, seed_resize_from_w @@ -351,17 +351,17 @@ def create_resize_inputs(tab, images, accordion=True, latent=False, non_zero=Tru ar_list = ['AR'] + [x.strip() for x in shared.opts.aspect_ratios.split(',') if x.strip() != ''] ar_dropdown = gr.Dropdown(show_label=False, interactive=True, choices=ar_list, value=ar_list[0], elem_id=f"{tab}_resize_ar", elem_classes=["ar-dropdown"]) for c in [ar_dropdown, width, height]: - c.change(fn=ar_change, inputs=[ar_dropdown, width, height], outputs=[width, height], show_progress=False) + c.change(fn=ar_change, inputs=[ar_dropdown, width, height], outputs=[width, height], show_progress='hidden') res_switch_btn = ToolButton(value=ui_symbols.switch, elem_id=f"{tab}_resize_size_swap") - res_switch_btn.click(lambda w, h: (h, w), inputs=[width, height], outputs=[width, height], show_progress=False) + res_switch_btn.click(lambda w, h: (h, w), inputs=[width, height], outputs=[width, height], show_progress='hidden') detect_image_size_btn = ToolButton(value=ui_symbols.detect, elem_id=f"{tab}_resize_detect_size") el = tab.split('_')[0] - detect_image_size_btn.click(fn=lambda w, h, _: (w or gr.update(), h or gr.update()), _js=f'currentImageResolution{el}', inputs=[dummy_component, dummy_component, dummy_component], outputs=[width, height], show_progress=False) + detect_image_size_btn.click(fn=lambda w, h, _: (w or gr.update(), h or gr.update()), _js=f'currentImageResolution{el}', inputs=[dummy_component, dummy_component, dummy_component], outputs=[width, height], show_progress='hidden') with gr.Tab(label="Scale", id=1, elem_id=f"{tab}_scale_tab_scale") as tab_scale_by: scale_by = gr.Slider(minimum=0.05, maximum=8.0, step=0.05, label=f"Scale{prefix}" if non_zero else "Resize scale", value=1.0, elem_id=f"{tab}_scale") if images is not None: for component in images: - component.change(fn=lambda: None, _js="updateImg2imgResizeToTextAfterChangingImage", inputs=[], outputs=[], show_progress=False) + component.change(fn=lambda: None, _js="updateImg2imgResizeToTextAfterChangingImage", inputs=[], outputs=[], show_progress='hidden') tab_scale_to.select(fn=lambda: 0, inputs=[], outputs=[selected_scale_tab]) tab_scale_by.select(fn=lambda: 1, inputs=[], outputs=[selected_scale_tab]) # resize_mode.change(fn=lambda x: gr.update(visible=x != 0), inputs=[resize_mode], outputs=[_resize_group]) diff --git a/modules/ui_settings.py b/modules/ui_settings.py index 330bba6b8..2f4bcaa2d 100644 --- a/modules/ui_settings.py +++ b/modules/ui_settings.py @@ -100,7 +100,7 @@ def create_setting_component(key, is_quicksettings=False): except Exception as e: shared.log.error(f'Quicksetting: component={res} {e}') if dirty_indicator is not None: - dirty_indicator.click(fn=lambda: shared.opts.get_default(key), outputs=[res], show_progress=False) + dirty_indicator.click(fn=lambda: shared.opts.get_default(key), outputs=[res], show_progress='hidden') dirtyable_setting.__exit__() return res @@ -113,7 +113,7 @@ def create_dirty_indicator(key, keys_to_reset, **kwargs): elements_to_reset = [shared.settings_components[_key] for _key in keys_to_reset if shared.settings_components[_key] is not None] indicator = gr.Button('', elem_classes="modification-indicator", elem_id=f"modification_indicator_{key}", **kwargs) - indicator.click(fn=get_default_values, outputs=elements_to_reset, show_progress=True) + indicator.click(fn=get_default_values, outputs=elements_to_reset, show_progress='full') return indicator @@ -369,7 +369,7 @@ def create_quicksettings(interfaces): fn=lambda value, k=k, progress=info.refresh is not None: run_settings_single(value, key=k, progress=progress), inputs=[component], outputs=[component, text_settings], - show_progress=info.refresh is not None, + show_progress='full' if info.refresh is not None else 'hidden', ) button_set_checkpoint = gr.Button('Change model', elem_id='change_checkpoint', visible=False) diff --git a/modules/ui_txt2img.py b/modules/ui_txt2img.py index 8497491c9..aea99fa1e 100644 --- a/modules/ui_txt2img.py +++ b/modules/ui_txt2img.py @@ -77,7 +77,7 @@ def create_ui(): txt2img_html_info, txt2img_html_log, ], - show_progress=False, + show_progress='hidden', ) txt2img_prompt.submit(**txt2img_dict) @@ -160,7 +160,7 @@ def create_ui(): txt2img_bindings = generation_parameters_copypaste.ParamBinding(paste_button=txt2img_paste, tabname="txt2img", source_text_component=txt2img_prompt, source_image_component=None) generation_parameters_copypaste.register_paste_params_button(txt2img_bindings) - txt2img_token_button.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[txt2img_prompt], outputs=[txt2img_token_counter], show_progress = False) - txt2img_negative_token_button.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[txt2img_negative_prompt], outputs=[txt2img_negative_token_counter], show_progress = False) + txt2img_token_button.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[txt2img_prompt], outputs=[txt2img_token_counter], show_progress = 'hidden') + txt2img_negative_token_button.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[txt2img_negative_prompt], outputs=[txt2img_negative_token_counter], show_progress = 'hidden') ui_extra_networks.setup_ui(extra_networks_ui, txt2img_gallery) diff --git a/modules/ui_video_vlm.py b/modules/ui_video_vlm.py index 6689c0c68..96cf8933b 100644 --- a/modules/ui_video_vlm.py +++ b/modules/ui_video_vlm.py @@ -65,6 +65,6 @@ def create_ui(prompt_element:gr.Textbox, image_element:gr.Image): fn=enhance_prompt, inputs=[enable, model, image_element, prompt_element, system_prompt, nsfw], outputs=prompt_element, - show_progress=True, + show_progress='full', ) return enable, model, system_prompt diff --git a/modules/video_models/video_load.py b/modules/video_models/video_load.py index 4ad4a3925..375861840 100644 --- a/modules/video_models/video_load.py +++ b/modules/video_models/video_load.py @@ -1,4 +1,5 @@ import os +import sys import copy import time import transformers # pylint: disable=unused-import @@ -7,6 +8,15 @@ from modules import shared, errors, sd_models, sd_checkpoint, model_quant, devic from modules.video_models import models_def, video_utils, video_overrides, video_cache +def _loader(component): + """Return loader type for log messages.""" + if sys.platform != 'linux': + return 'default' + if component == 'diffusers': + return 'runai' if shared.opts.runai_streamer_diffusers else 'default' + return 'runai' if shared.opts.runai_streamer_transformers else 'default' + + loaded_model = None @@ -60,7 +70,7 @@ def load_model(selected: models_def.Model): selected.te_folder = 'text_encoder' selected.te_revision = None - shared.log.debug(f'Video load: module=te repo="{selected.te or selected.repo}" folder="{selected.te_folder}" cls={selected.te_cls.__name__} quant={model_quant.get_quant_type(quant_args)}') + shared.log.debug(f'Video load: module=te repo="{selected.te or selected.repo}" folder="{selected.te_folder}" cls={selected.te_cls.__name__} quant={model_quant.get_quant_type(quant_args)} loader={_loader("transformers")}') kwargs["text_encoder"] = selected.te_cls.from_pretrained( pretrained_model_name_or_path=selected.te or selected.repo, subfolder=selected.te_folder, @@ -80,7 +90,7 @@ def load_model(selected: models_def.Model): if dit_folder is not None and dit_folder not in kwargs: # get a new quant arg on every loop to prevent the quant config classes getting entangled load_args, quant_args = model_quant.get_dit_args({}, module='Model', device_map=True) - shared.log.debug(f'Video load: module=transformer repo="{selected.dit or selected.repo}" module="{dit_folder}" folder="{dit_folder}" cls={selected.dit_cls.__name__} quant={model_quant.get_quant_type(quant_args)}') + shared.log.debug(f'Video load: module=transformer repo="{selected.dit or selected.repo}" module="{dit_folder}" folder="{dit_folder}" cls={selected.dit_cls.__name__} quant={model_quant.get_quant_type(quant_args)} loader={_loader("diffusers")}') kwargs[dit_folder] = selected.dit_cls.from_pretrained( pretrained_model_name_or_path=selected.dit or selected.repo, subfolder=dit_folder, @@ -91,7 +101,7 @@ def load_model(selected: models_def.Model): **offline_args, ) else: - shared.log.debug(f'Video load: module=transformer repo="{selected.dit or selected.repo}" module="{dit_folder}" folder="{dit_folder}" cls={selected.dit_cls.__name__} skip') + shared.log.debug(f'Video load: module=transformer repo="{selected.dit or selected.repo}" module="{dit_folder}" folder="{dit_folder}" cls={selected.dit_cls.__name__} loader={_loader("diffusers")} skip') if selected.dit_folder is None: selected.dit_folder = ['transformer'] diff --git a/modules/video_models/video_ui.py b/modules/video_models/video_ui.py index b099784b1..de0511d64 100644 --- a/modules/video_models/video_ui.py +++ b/modules/video_models/video_ui.py @@ -116,7 +116,7 @@ def create_ui_size(): seed = gr.Number(label='Initial seed', value=-1, elem_id="video_seed", container=True) random_seed = ToolButton(ui_symbols.random, elem_id="video_seed_random") reuse_seed = ToolButton(ui_symbols.reuse, elem_id="video_seed_reuse") - random_seed.click(fn=lambda: -1, show_progress=False, inputs=[], outputs=[seed]) + random_seed.click(fn=lambda: -1, show_progress='hidden', inputs=[], outputs=[seed]) return width, height, frames, seed, reuse_seed @@ -194,7 +194,7 @@ def create_ui(prompt, negative, styles, overrides, init_image, init_strength, la _js="submit_video", inputs=state_inputs + video_inputs, outputs=video_outputs, - show_progress=False, + show_progress='hidden', ) generate.click(**video_dict) return [engine, model, steps, sampler_index] diff --git a/pipelines/generic.py b/pipelines/generic.py index be59bd75e..911cdc3c6 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -1,4 +1,5 @@ import os +import sys import json import diffusers import transformers @@ -8,6 +9,15 @@ from modules import shared, devices, errors, sd_models, model_quant debug = os.environ.get('SD_LOAD_DEBUG', None) is not None +def _loader(component): + """Return loader type for log messages.""" + if sys.platform != 'linux': + return 'default' + if component == 'diffusers': + return 'runai' if shared.opts.runai_streamer_diffusers else 'default' + return 'runai' if shared.opts.runai_streamer_transformers else 'default' + + def load_transformer(repo_id, cls_name, load_config=None, subfolder="transformer", allow_quant=True, variant=None, dtype=None, modules_to_not_convert=None, modules_dtype_dict=None): transformer = None if load_config is None: @@ -31,7 +41,7 @@ def load_transformer(repo_id, cls_name, load_config=None, subfolder="transformer local_file = sd_unet.unet_dict[shared.opts.sd_unet] if local_file is not None and local_file.lower().endswith('.gguf'): - shared.log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') + shared.log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" loader={_loader("diffusers")} args={load_args}') from modules import ggml ggml.install_gguf() loader = cls_name.from_single_file if hasattr(cls_name, 'from_single_file') else cls_name.from_pretrained @@ -43,7 +53,7 @@ def load_transformer(repo_id, cls_name, load_config=None, subfolder="transformer ) transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None) elif local_file is not None and local_file.lower().endswith('.safetensors'): - shared.log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" args={load_args}') + shared.log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" loader={_loader("diffusers")} args={load_args}') if dtype is not None: load_args['torch_dtype'] = dtype loader = cls_name.from_single_file if hasattr(cls_name, 'from_single_file') else cls_name.from_pretrained @@ -54,7 +64,7 @@ def load_transformer(repo_id, cls_name, load_config=None, subfolder="transformer **quant_args, ) else: - shared.log.debug(f'Load model: transformer="{repo_id}" cls={cls_name.__name__} subfolder={subfolder} quant="{quant_type}" args={load_args}') + shared.log.debug(f'Load model: transformer="{repo_id}" cls={cls_name.__name__} subfolder={subfolder} quant="{quant_type}" loader={_loader("diffusers")} args={load_args}') if 'sdnq-' in repo_id.lower(): quant_args = {} if dtype is not None: @@ -115,7 +125,7 @@ def load_text_encoder(repo_id, cls_name, load_config=None, subfolder="text_encod # load from local file gguf if local_file is not None and local_file.lower().endswith('.gguf'): - shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}"') + shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}" loader={_loader("transformers")}') """ from modules import ggml ggml.install_gguf() @@ -132,7 +142,7 @@ def load_text_encoder(repo_id, cls_name, load_config=None, subfolder="text_encod # load from local file safetensors elif local_file is not None and local_file.lower().endswith('.safetensors'): - shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}"') + shared.log.debug(f'Load model: text_encoder="{local_file}" cls={cls_name.__name__} quant="{quant_type}" loader={_loader("transformers")}') from modules import model_te text_encoder = model_te.load_t5(local_file) text_encoder = model_quant.do_post_load_quant(text_encoder, allow=quant_type is not None) @@ -143,7 +153,7 @@ def load_text_encoder(repo_id, cls_name, load_config=None, subfolder="text_encod import nunchaku repo_id = 'nunchaku-tech/nunchaku-t5/awq-int4-flux.1-t5xxl.safetensors' cls_name = nunchaku.NunchakuT5EncoderModel - shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="SVDQuant"') + shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="SVDQuant" loader={_loader("transformers")}') text_encoder = nunchaku.NunchakuT5EncoderModel.from_pretrained( repo_id, torch_dtype=dtype, @@ -157,7 +167,7 @@ def load_text_encoder(repo_id, cls_name, load_config=None, subfolder="text_encod repo_id = 'Disty0/t5-xxl' with open(os.path.join('configs', 'flux', 'text_encoder_2', 'config.json'), encoding='utf8') as f: load_args['config'] = transformers.T5Config(**json.load(f)) - shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') + shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" loader={_loader("transformers")} shared={shared.opts.te_shared_t5}') text_encoder = cls_name.from_pretrained( repo_id, cache_dir=shared.opts.hfcache_dir, @@ -170,7 +180,7 @@ def load_text_encoder(repo_id, cls_name, load_config=None, subfolder="text_encod else: repo_id = 'Wan-AI/Wan2.1-T2V-1.3B-Diffusers' subfolder = 'text_encoder' - shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') + shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" loader={_loader("transformers")} shared={shared.opts.te_shared_t5}') text_encoder = cls_name.from_pretrained( repo_id, cache_dir=shared.opts.hfcache_dir, @@ -181,7 +191,7 @@ def load_text_encoder(repo_id, cls_name, load_config=None, subfolder="text_encod elif cls_name == transformers.Qwen2_5_VLForConditionalGeneration and allow_shared and shared.opts.te_shared_t5: repo_id = 'hunyuanvideo-community/HunyuanImage-2.1-Diffusers' subfolder = 'text_encoder' - shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') + shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" loader={_loader("transformers")} shared={shared.opts.te_shared_t5}') text_encoder = cls_name.from_pretrained( repo_id, cache_dir=shared.opts.hfcache_dir, @@ -192,7 +202,7 @@ def load_text_encoder(repo_id, cls_name, load_config=None, subfolder="text_encod # load from repo if text_encoder is None: - shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" shared={shared.opts.te_shared_t5}') + shared.log.debug(f'Load model: text_encoder="{repo_id}" cls={cls_name.__name__} quant="{quant_type}" loader={_loader("transformers")} shared={shared.opts.te_shared_t5}') if subfolder is not None: load_args['subfolder'] = subfolder if variant is not None: diff --git a/scripts/postprocessing_upscale.py b/scripts/postprocessing_upscale.py index b1e06f042..3633c08a5 100644 --- a/scripts/postprocessing_upscale.py +++ b/scripts/postprocessing_upscale.py @@ -33,7 +33,7 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): extras_upscaler_2 = gr.Dropdown(label='Refine upscaler', elem_id="extras_upscaler_2", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) extras_upscaler_2_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Blend strength", value=0.0, elem_id="extras_upscaler_2_visibility") - upscaling_res_switch_btn.click(lambda w, h: (h, w), inputs=[upscaling_resize_w, upscaling_resize_h], outputs=[upscaling_resize_w, upscaling_resize_h], show_progress=False) + upscaling_res_switch_btn.click(lambda w, h: (h, w), inputs=[upscaling_resize_w, upscaling_resize_h], outputs=[upscaling_resize_w, upscaling_resize_h], show_progress='hidden') tab_scale_by.select(fn=lambda: 0, inputs=[], outputs=[selected_tab]) tab_scale_to.select(fn=lambda: 1, inputs=[], outputs=[selected_tab]) diff --git a/scripts/prompts_from_file.py b/scripts/prompts_from_file.py index ae2e8038f..fae363131 100644 --- a/scripts/prompts_from_file.py +++ b/scripts/prompts_from_file.py @@ -105,8 +105,8 @@ class Script(scripts_manager.Script): checkbox_iterate_batch = gr.Checkbox(label="Use same seed", value=False, elem_id=self.elem_id("checkbox_iterate_batch")) prompt_txt = gr.Textbox(label="Prompts", lines=2, elem_id=self.elem_id("prompt_txt"), value='') file = gr.File(label="Upload prompts", type='binary', elem_id=self.elem_id("file")) - file.change(fn=load_prompt_file, inputs=[file], outputs=[file, prompt_txt, prompt_txt], show_progress=False) - prompt_txt.change(lambda tb: gr.update(lines=7) if ("\n" in tb) else gr.update(lines=2), inputs=[prompt_txt], outputs=[prompt_txt], show_progress=False) + file.change(fn=load_prompt_file, inputs=[file], outputs=[file, prompt_txt, prompt_txt], show_progress='hidden') + prompt_txt.change(lambda tb: gr.update(lines=7) if ("\n" in tb) else gr.update(lines=2), inputs=[prompt_txt], outputs=[prompt_txt], show_progress='hidden') return [checkbox_iterate, checkbox_iterate_batch, prompt_txt] def run(self, p, checkbox_iterate, checkbox_iterate_batch, prompt_txt: str): # pylint: disable=arguments-differ