diff --git a/.eslintrc.json b/.eslintrc.json index 10d4062ed..0420018ea 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -73,7 +73,7 @@ "register_drag_drop": "readonly", //extraNetworks.js "requestGet": "readonly", - "popup": "readonly", + "getENActiveTab": "readonly", // from python "localization": "readonly", // progressbar.js @@ -82,6 +82,8 @@ // imageviewer.js "modalPrevImage": "readonly", "modalNextImage": "readonly", + // logMonitor.js + "jobStatusEl": "readonly", // loader.js "removeSplash": "readonly" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 380d6a9c7..dc3ea6a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ # Change Log for SD.Next -## Update for 2023-09-24 +## Update for 2023-09-29 **TBD**: Planned before release: - Integrate LoRA/Lyco for *backend:original* - Add FreeU for *backend:diffusers* This is a big one, with some major changes and new functionality... +And probably the biggest release since introduction of **Diffusers** Note that for this release its recommended to perform a clean install (fresh `git clone`) Upgrades are still possible and supported, but above is recommended for best experience @@ -17,13 +18,17 @@ Upgrades are still possible and supported, but above is recommended for best exp - converted submenus from checkboxes to accordion elements any ui state including state of open/closed menus can be saved as default! see *System -> User interface -> Set menu states* + - small visual indicator bottom right of page showing internal server job state - **Extra networks**: - - you can scan [civitai](https://civitai.com/) - for missing metadata and previews directly from extra networks - - new details interface to view and save data about extra networks - main ui now has a single button on each en to trigger details view - - details view includes model/lora metadata parser! - - details view includes civitai model metadata! + - **Details** + - new details interface to view and save data about extra networks + main ui now has a single button on each en to trigger details view + - details view includes model/lora metadata parser! + - details view includes civitai model metadata! + - **Metadata**: + - you can scan [civitai](https://civitai.com/) + for missing metadata and previews directly from extra networks + simply click on button in top-right corner of extra networks page - **Styles** - save/apply icons moved to extra networks - can be edited in details view @@ -31,9 +36,13 @@ Upgrades are still possible and supported, but above is recommended for best exp - support for embedded previews - **VAE** - VAEs are now also listed as part of extra networks - - faster search, ability to show/hide/sort networks - - refactored subfolder handling - *note*: this will trigger model hash recaclulation on first model use + - **Refiner** + - you can load model from extra networks as base model or as refiner + simply select button in top-right of models page + - **General** + - faster search, ability to show/hide/sort networks + - refactored subfolder handling + *note*: this will trigger model hash recaclulation on first model use - **Diffusers**: - better pipeline auto-detect when loading from safetensors - **SDXL Inpaint** @@ -110,6 +119,7 @@ Upgrades are still possible and supported, but above is recommended for best exp - get browser session info in server log - when running with `--debug` flag, log is force-rotated so each `sdnext.log.*` represents exactly one server run + - internal server job state tracking - **API** - add end-to-end example how to use API: `cli/simple-txt2img.js` covers txt2img, upscale, hires, refiner diff --git a/html/locale_en.json b/html/locale_en.json index 3b7a83718..65b762a7f 100644 --- a/html/locale_en.json +++ b/html/locale_en.json @@ -19,6 +19,7 @@ {"id":"","label":"⟲","localized":"","hint":"Refresh"}, {"id":"","label":"βœ•","localized":"","hint":"Close"}, {"id":"","label":"⊜","localized":"","hint":"Fill"}, + {"id":"","label":"⌾","localized":"","hint":"Load model as refiner model when selected, otherwise load as base model"}, {"id":"","label":"πŸ•ΈοΈ","localized":"","hint":"Scan CivitAI for missing metadata and previews"}, {"id":"","label":"πŸ“","localized":"","hint":"Measure"}, {"id":"","label":"πŸ”","localized":"","hint":"Search"} diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index c591065ef..e05cff5cc 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -186,24 +186,31 @@ function setupExtraNetworksForTab(tabname) { gradioApp().querySelector(`#${tabname}_extra_tabs`).classList.add('extra-networks'); const en = gradioApp().getElementById(`${tabname}_extra_networks`); const tabs = gradioApp().querySelector(`#${tabname}_extra_tabs > div`); - const txtSearch = gradioApp().querySelector(`#${tabname}_extra_search textarea`); - const txtDescription = gradioApp().getElementById(`${tabname}_description`); + + // buttons const btnRefresh = gradioApp().getElementById(`${tabname}_extra_refresh`); const btnScan = gradioApp().getElementById(`${tabname}_extra_scan`); const btnSave = gradioApp().getElementById(`${tabname}_extra_save`); const btnClose = gradioApp().getElementById(`${tabname}_extra_close`); - txtSearch.classList.add('search'); - txtDescription.classList.add('description'); + const btnModel = gradioApp().getElementById(`${tabname}_extra_model`); const buttons = document.createElement('span'); buttons.classList.add('buttons'); if (btnRefresh) buttons.appendChild(btnRefresh); + if (btnModel) buttons.appendChild(btnModel); if (btnScan) buttons.appendChild(btnScan); if (btnSave) buttons.appendChild(btnSave); if (btnClose) buttons.appendChild(btnClose); + btnModel.onclick = () => btnModel.classList.toggle('toolbutton-selected'); tabs.appendChild(buttons); + + // search and description const div = document.createElement('div'); div.classList.add('second-line'); tabs.appendChild(div); + const txtSearch = gradioApp().querySelector(`#${tabname}_extra_search textarea`); + const txtDescription = gradioApp().getElementById(`${tabname}_description`); + txtSearch.classList.add('search'); + txtDescription.classList.add('description'); div.appendChild(txtSearch); div.appendChild(txtDescription); let searchTimer = null; @@ -215,6 +222,7 @@ function setupExtraNetworksForTab(tabname) { }, 150); }); + // card hover let hoverTimer = null; let previousCard = null; gradioApp().getElementById(`${tabname}_extra_tabs`).onmouseover = (e) => { @@ -233,6 +241,7 @@ function setupExtraNetworksForTab(tabname) { }; }; + // en style const intersectionObserver = new IntersectionObserver((entries) => { if (!en) return; for (const el of Array.from(gradioApp().querySelectorAll('.extra-networks-page'))) { diff --git a/javascript/logMonitor.js b/javascript/logMonitor.js index bca665672..4bbea22b3 100644 --- a/javascript/logMonitor.js +++ b/javascript/logMonitor.js @@ -1,5 +1,6 @@ let logMonitorEl = null; let logMonitorStatus = true; +let jobStatusEl = null; async function logMonitor() { if (logMonitorStatus) setTimeout(logMonitor, opts.logmonitor_refresh_period); @@ -51,6 +52,9 @@ async function initLogMonitor() { `; el.style.display = 'none'; + jobStatusEl = document.createElement('div'); + jobStatusEl.className = 'jobStatus'; + gradioApp().appendChild(jobStatusEl); fetch(`/sdapi/v1/start?agent=${encodeURI(navigator.userAgent)}`); logMonitor(); log('initLogMonitor'); diff --git a/javascript/progressBar.js b/javascript/progressBar.js index 1305099d6..3da707627 100644 --- a/javascript/progressBar.js +++ b/javascript/progressBar.js @@ -47,7 +47,7 @@ function setProgress(res) { let eta = ''; if (res?.paused) eta = 'Paused'; else if (res?.completed || (progress > 0.99)) eta = 'Finishing'; - else if (sec === 0) eta = 'Starting'; + else if (sec === 0) eta = `Init${res?.job?.length > 0 ? `: ${res.job}` : ''}`; else { const min = Math.floor(sec / 60); sec %= 60; @@ -106,6 +106,7 @@ function requestProgress(id_task, progressEl, galleryEl, atEnd = null, onProgres debug('taskEnd:', id_task); localStorage.removeItem('task'); setProgress(); + if (jobStatusEl) jobStatusEl.style.display = 'none'; if (parentGallery && livePreview) parentGallery.removeChild(livePreview); checkPaused(true); if (atEnd) atEnd(); @@ -113,6 +114,8 @@ function requestProgress(id_task, progressEl, galleryEl, atEnd = null, onProgres const start = (id_task, id_live_preview) => { // eslint-disable-line no-shadow request('./internal/progress', { id_task, id_live_preview }, (res) => { + if (jobStatusEl) jobStatusEl.innerText = (res?.job || '').trim().toUpperCase(); + if (jobStatusEl) jobStatusEl.style.display = jobStatusEl.innerText.length > 0 ? 'block' : 'none'; lastState = res; const elapsedFromStart = (new Date() - dateStart) / 1000; hasStarted |= res.active; @@ -124,7 +127,7 @@ function requestProgress(id_task, progressEl, galleryEl, atEnd = null, onProgres if (res.live_preview && !livePreview) init(); if (res.live_preview && galleryEl) img.src = res.live_preview; if (onProgress) onProgress(res); - setTimeout(() => start(id_task, id_live_preview), opts.live_preview_refresh_period || 250); + setTimeout(() => start(id_task, id_live_preview), opts.live_preview_refresh_period || 500); }, done); }; start(id_task, 0); diff --git a/javascript/style.css b/javascript/style.css index a6ec9ded5..ab7bae02b 100644 --- a/javascript/style.css +++ b/javascript/style.css @@ -120,6 +120,8 @@ div#extras_scale_to_tab div.form{ flex-direction: row; } z-index: var(--layer-2); } .tooltip-show { opacity: 0.9; } +.toolbutton-selected { background: var(--background-fill-primary) !important; } +.jobStatus { position: fixed; bottom: 1em; right: 1em; background: var(--input-background-fill); padding: 0.4em; font-size: 0.8em; color: var(--body-text-color-subdued); } /* settings */ #si-sparkline-memo, #si-sparkline-load { background-color: #111; } diff --git a/javascript/ui.js b/javascript/ui.js index e10c168e3..a3c1ab5c4 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -336,13 +336,19 @@ function updateInput(target) { let desiredCheckpointName = null; function selectCheckpoint(name) { desiredCheckpointName = name; - gradioApp().getElementById('change_checkpoint').click(); + const tabname = getENActiveTab(); + const btnModel = gradioApp().getElementById(`${tabname}_extra_model`); + const isRefiner = btnModel && btnModel.classList.contains('toolbutton-selected'); + if (isRefiner) gradioApp().getElementById('change_refiner').click(); + else gradioApp().getElementById('change_checkpoint').click(); + log(`Change ${isRefiner ? 'refiner' : 'model'}: ${desiredCheckpointName}`); } let desiredVAEName = null; function selectVAE(name) { desiredVAEName = name; gradioApp().getElementById('change_vae').click(); + log(`Change VAE: ${desiredVAEName}`); } function currentImg2imgSourceResolution(_a, _b, scaleBy) { diff --git a/modules/call_queue.py b/modules/call_queue.py index 5bd633c3c..15d5274b8 100644 --- a/modules/call_queue.py +++ b/modules/call_queue.py @@ -40,7 +40,6 @@ def wrap_gradio_gpu_call(func, extra_outputs=None): res[-1] = f"
{html.escape(str(e))}
" finally: progress.finish_task(id_task) - shared.state.end() return res return wrap_gradio_call(f, extra_outputs=extra_outputs, add_stats=True, name=name) diff --git a/modules/hashes.py b/modules/hashes.py index 1470f6685..5dd3a7579 100644 --- a/modules/hashes.py +++ b/modules/hashes.py @@ -1,3 +1,4 @@ +import copy import hashlib import os.path from rich import progress @@ -56,6 +57,7 @@ def sha256(filename, title, use_addnet_hash=False): return None if not os.path.isfile(filename): return None + orig_state = copy.deepcopy(shared.state) shared.state.begin("hashing") if use_addnet_hash: with progress.open(filename, 'rb', description=f'[cyan]Calculating hash: [yellow]{filename}', auto_refresh=True, console=shared.console) as f: @@ -67,6 +69,7 @@ def sha256(filename, title, use_addnet_hash=False): "sha256": sha256_value } shared.state.end() + shared.state = orig_state dump_cache() return sha256_value diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index aa96c5dd8..dc895be6d 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -362,6 +362,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro desc='Base', **task_specific_kwargs ) + # p.steps = base_args['num_inference_steps'] p.extra_generation_params['CFG rescale'] = p.diffusers_guidance_rescale p.extra_generation_params["Eta"] = shared.opts.scheduler_eta if shared.opts.scheduler_eta is not None and shared.opts.scheduler_eta > 0 and shared.opts.scheduler_eta < 1 else None try: @@ -414,6 +415,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro strength=p.denoising_strength, desc='Hires', ) + # p.steps += hires_args['num_inference_steps'] try: output = shared.sd_model(**hires_args) # pylint: disable=not-callable except AssertionError as e: @@ -476,6 +478,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro clip_skip=p.clip_skip, desc='Refiner', ) + # p.steps += refiner_args['num_inference_steps'] try: refiner_output = shared.sd_refiner(**refiner_args) # pylint: disable=not-callable except AssertionError as e: diff --git a/modules/progress.py b/modules/progress.py index 728198047..935033185 100644 --- a/modules/progress.py +++ b/modules/progress.py @@ -43,6 +43,7 @@ class ProgressRequest(BaseModel): class InternalProgressResponse(BaseModel): + job: str = Field(default=None, title="Job name", description="Internal job name") active: bool = Field(title="Whether the task is being worked on right now") queued: bool = Field(title="Whether the task is in queue") paused: bool = Field(title="Whether the task is paused") @@ -64,14 +65,12 @@ def progressapi(req: ProgressRequest): completed = req.id_task in finished_tasks paused = shared.state.paused if not active: - return InternalProgressResponse(active=active, queued=queued, paused=paused, completed=completed, id_live_preview=-1, textinfo="Queued..." if queued else "Waiting...") + return InternalProgressResponse(job=shared.state.job, active=active, queued=queued, paused=paused, completed=completed, id_live_preview=-1, textinfo="Queued..." if queued else "Waiting...") progress = 0 - job_count, job_no = shared.state.job_count, shared.state.job_no - sampling_steps, sampling_step = shared.state.sampling_steps, shared.state.sampling_step - if job_count > 0: - progress += job_no / job_count - if sampling_steps > 0 and job_count > 0: - progress += 1 / job_count * sampling_step / sampling_steps + if shared.state.job_count > 0: + progress += shared.state.job_no / shared.state.job_count + if shared.state.sampling_steps > 0 and shared.state.job_count > 0: + progress += 1 / shared.state.job_count * shared.state.sampling_step / shared.state.sampling_steps progress = min(progress, 1) elapsed_since_start = time.time() - shared.state.time_start predicted_duration = elapsed_since_start / progress if progress > 0 else None @@ -84,4 +83,4 @@ def progressapi(req: ProgressRequest): shared.state.current_image.save(buffered, format='jpeg') live_preview = f'data:image/jpeg;base64,{base64.b64encode(buffered.getvalue()).decode("ascii")}' id_live_preview = shared.state.id_live_preview - return InternalProgressResponse(active=active, queued=queued, paused=paused, completed=completed, progress=progress, eta=eta, live_preview=live_preview, id_live_preview=id_live_preview, textinfo=shared.state.textinfo) + return InternalProgressResponse(job=shared.state.job, active=active, queued=queued, paused=paused, completed=completed, progress=progress, eta=eta, live_preview=live_preview, id_live_preview=id_live_preview, textinfo=shared.state.textinfo) diff --git a/modules/sd_models.py b/modules/sd_models.py index bca8d8a9e..497a67192 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -3,6 +3,7 @@ import io import sys import json import time +import copy import logging import threading import contextlib @@ -545,7 +546,8 @@ class ModelData: if shared.backend == shared.Backend.ORIGINAL: reload_model_weights(op='model') elif shared.backend == shared.Backend.DIFFUSERS: - load_diffuser(op='model') + reload_model_weights(op='model') + # load_diffuser(op='model') else: shared.log.error(f"Unknown Execution backend: {shared.backend}") self.initial = False @@ -1126,6 +1128,9 @@ def reload_model_weights(sd_model=None, info=None, reuse_dict=False, op='model') if checkpoint_info is None: unload_model_weights(op=op) return + orig_state = copy.deepcopy(shared.state) + shared.state = shared.State() + shared.state.begin(f'load-{op}') if load_dict: shared.log.debug(f'Model dict: existing={sd_model is not None} target={checkpoint_info.filename} info={info}') else: @@ -1165,8 +1170,11 @@ def reload_model_weights(sd_model=None, info=None, reuse_dict=False, op='model') model_data.sd_dict = shared.opts.sd_model_dict shared.opts.data["sd_model_checkpoint"] = next_checkpoint_info.title reload_model_weights(reuse_dict=True) # ok we loaded dict now lets redo and load model on top of it + shared.state.end() + shared.state = orig_state return model_data.sd_model if op == 'model' or op == 'dict' else model_data.sd_refiner + # fallback try: load_model_weights(sd_model, checkpoint_info, state_dict, timer) except Exception: @@ -1180,7 +1188,9 @@ def reload_model_weights(sd_model=None, info=None, reuse_dict=False, op='model') if sd_model is not None and not shared.cmd_opts.lowvram and not shared.cmd_opts.medvram and not getattr(sd_model, 'has_accelerate', False): sd_model.to(devices.device) timer.record("device") - shared.log.info(f"Weights loaded in {timer.summary()}") + shared.state.end() + shared.state = orig_state + shared.log.info(f"Loaded: {op} time={timer.summary()}") def disable_offload(sd_model): diff --git a/modules/shared.py b/modules/shared.py index 4ef2e4733..464cacde1 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -147,9 +147,11 @@ class State: self.skipped = False self.textinfo = None self.time_start = time.time() + log.debug(f'State begin: {self.job}') devices.torch_gc() def end(self): + log.debug(f'State end: {self.job} time={time.time() - self.time_start:.2f}s') self.job = "" self.job_count = 0 self.job_no = 0 diff --git a/modules/ui.py b/modules/ui.py index f860e55f4..72078e310 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -1168,7 +1168,14 @@ def create_ui(startup_timer = None): inputs=[component_dict['sd_model_checkpoint'], dummy_component], outputs=[component_dict['sd_model_checkpoint'], text_settings], ) - button_set_vae = gr.Button('Change vae', elem_id='change_vae', visible=False) + button_set_refiner = gr.Button('Change refiner', elem_id='change_refiner', visible=False) + button_set_refiner.click( + fn=lambda value, _: run_settings_single(value, key='sd_model_checkpoint'), + _js="function(v){ var res = desiredCheckpointName; desiredCheckpointName = ''; return [res || v, null]; }", + inputs=[component_dict['sd_model_refiner'], dummy_component], + outputs=[component_dict['sd_model_refiner'], text_settings], + ) + button_set_vae = gr.Button('Change VAE', elem_id='change_vae', visible=False) button_set_vae.click( fn=lambda value, _: run_settings_single(value, key='sd_vae'), _js="function(v){ var res = desiredVAEName; desiredVAEName = ''; return [res || v, null]; }", diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 088893e86..8f9263003 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -366,6 +366,7 @@ class ExtraNetworksUi: self.button_save: gr.Button = None self.button_apply: gr.Button = None self.button_close: gr.Button = None + self.button_model: gr.Checkbox = None self.details_components: list = [] self.last_item: dict = None self.last_page: ExtraNetworksPage = None @@ -449,12 +450,14 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): def ui_tab_change(page): scan_visible = page in ['Model', 'Lora', 'Hypernetwork', 'Embedding'] save_visible = page in ['Style'] - return [gr.update(visible=scan_visible), gr.update(visible=save_visible)] + model_visible = page in ['Model'] + return [gr.update(visible=scan_visible), gr.update(visible=save_visible), gr.update(visible=model_visible)] ui.button_refresh = ToolButton(symbols.refresh, elem_id=tabname+"_extra_refresh") ui.button_scan = ToolButton(symbols.scan, elem_id=tabname+"_extra_scan", visible=True) ui.button_save = ToolButton(symbols.book, elem_id=tabname+"_extra_save", visible=False) ui.button_close = ToolButton(symbols.close, elem_id=tabname+"_extra_close") + ui.button_model = ToolButton(symbols.refine, elem_id=tabname+"_extra_model", visible=True) ui.search = gr.Textbox('', show_label=False, elem_id=tabname+"_extra_search", placeholder="Search...", elem_classes="textbox", lines=2) ui.description = gr.Textbox('', show_label=False, elem_id=tabname+"_description", elem_classes="textbox", lines=2, interactive=False) @@ -466,7 +469,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): with gr.Tab(page.title, id=page.title.lower().replace(" ", "_"), elem_classes="extra-networks-tab") as tab: hmtl = gr.HTML(page.html, elem_id=f'{tabname}{page.name}_extra_page', elem_classes="extra-networks-page") ui.pages.append(hmtl) - tab.select(ui_tab_change, _js="getENActivePage", inputs=[ui.button_details], outputs=[ui.button_scan, ui.button_save]) + tab.select(ui_tab_change, _js="getENActivePage", inputs=[ui.button_details], outputs=[ui.button_scan, ui.button_save, ui.button_model]) # ui.tabs.change(fn=ui_tab_change, inputs=[], outputs=[ui.button_scan, ui.button_save]) diff --git a/modules/ui_symbols.py b/modules/ui_symbols.py index 22422f101..010a70050 100644 --- a/modules/ui_symbols.py +++ b/modules/ui_symbols.py @@ -9,7 +9,13 @@ fill = '⊜' scan = 'πŸ•ΈοΈ' networks = '🌐' paste = '⇦' - +refine = '⌾' +switch = 'β‡…' +detect = 'πŸ“' +folder = 'πŸ“‚' +random = '🎲️' +reuse = '♻️' +info = 'β„Ή' # noqa """ refresh = 'πŸ”„' close = 'πŸ›—' @@ -21,9 +27,3 @@ fill = '⏫' networks = '🌐' paste = 'πŸ“˜' """ -switch = 'β‡…' -detect = 'πŸ“' -folder = 'πŸ“‚' -random = '🎲️' -reuse = '♻️' -info = 'β„Ή' # noqa diff --git a/modules/upscaler.py b/modules/upscaler.py index 6a8c723c5..8ac4113fc 100644 --- a/modules/upscaler.py +++ b/modules/upscaler.py @@ -1,4 +1,5 @@ import os +import copy from abc import abstractmethod import PIL from PIL import Image @@ -79,6 +80,8 @@ class Upscaler: return img def upscale(self, img: PIL.Image, scale, selected_model: str = None): + orig_state = copy.deepcopy(modules.shared.state) + modules.shared.state.begin('upscale') self.scale = scale dest_w = int(img.width * scale) dest_h = int(img.height * scale) @@ -91,6 +94,8 @@ class Upscaler: break if img.width != dest_w or img.height != dest_h: img = img.resize((int(dest_w), int(dest_h)), resample=LANCZOS) + modules.shared.state.end() + modules.shared.state = orig_state return img @abstractmethod