diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..73b25eaf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,58 @@ +## Upcoming version: +### Features: + * switch to torch 2.0.0 (except for AMD GPUs) + * visual improvements to custom code scripts + * add filename patterns: [clip_skip], [hasprompt<>], [batch_number], [generation_number] + * add support for saving init images in img2img, and record their hashes in infotext for reproducability + * automatically select current word when adjusting weight with ctrl+up/down + * add dropdowns for X/Y/Z plot + * setting: Stable Diffusion/Random number generator source: makes it possible to make images generated from a given manual seed consistent across different GPUs + * support Gradio's theme API + * use TCMalloc on Linux by default; possible fix for memory leaks + * (optimization) option to remove negative conditioning at low sigma values #9177 + * embed model merge metadata in .safetensors file + * extension settings backup/restore feature #9169 + * add "resize by" and "resize to" tabs to img2img + * add option "keep original size" to textual inversion images preprocess + * image viewer scrolling via analog stick + * button to restore the progress from session lost / tab reload + +### Minor: + * gradio bumped to 3.28.1 + * in extra tab, change extras "scale to" to sliders + * add labels to tool buttons to make it possible to hide them + * add tiled inference support for ScuNET + * add branch support for extension installation + * change linux installation script to insall into current directory rather than /home/username + * sort textual inversion embeddings by name (case insensitive) + * allow styles.csv to be symlinked or mounted in docker + * remove the "do not add watermark to images" option + * make selected tab configurable with UI config + * extra networks UI in now fixed height and scrollable + * add disable_tls_verify arg for use with self-signed certs + +### Extensions: + * Add reload callback + * add is_hr_pass field for processing + +### Bug Fixes: + * fix broken batch image processing on 'Extras/Batch Process' tab + * add "None" option to extra networks dropdowns + * fix FileExistsError for CLIP Interrogator + * fix /sdapi/v1/txt2img endpoint not working on Linux #9319 + * fix disappearing live previews and progressbar during slow tasks + * fix fullscreen image view not working properly in some cases + * prevent alwayson_scripts args param resizing script_arg list when they are inserted in it + * fix prompt schedule for second order samplers + * fix image mask/composite for weird resolutions #9628 + * use correct images for previews when using AND (see #9491) + * one broken image in img2img batch won't stop all processing + * fix image orientation bug in train/preprocess + * fix Ngrok recreating tunnels every reload + * fix --realesrgan-models-path and --ldsr-models-path not working + * fix --skip-install not working + * outpainting Mk2 & Poorman should use the SAMPLE file format to save images, not GRID file format + * do not fail all Loras if some have failed to load when making a picture + +## Before versions: + * everything diff --git a/javascript/hints.js b/javascript/hints.js index bf7123fb..a541f240 100644 --- a/javascript/hints.js +++ b/javascript/hints.js @@ -22,10 +22,12 @@ titles = { "\u{1f4cb}": "Apply selected styles to current prompt", "\u{1f4d2}": "Paste available values into the field", "\u{1f3b4}": "Show/hide extra networks", + "\u{1f5e8}": "Interogate Clip", "\u{1f5ea}": "Interogate Deepbooru", "\u{1F300}": "Restore progress", + "Inpaint a part of image": "Draw a mask over an image, and the script will regenerate the masked area with content according to prompt", "SD upscale": "Upscale image normally, split result into tiles, improve each tile using img2img, merge whole image back", diff --git a/javascript/progressbar.js b/javascript/progressbar.js index 3ee1cfaf..473971f2 100644 --- a/javascript/progressbar.js +++ b/javascript/progressbar.js @@ -66,7 +66,7 @@ function randomId(){ // 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, onProgress){ +function requestProgress(id_task, progressbarContainer, gallery, atEnd, onProgress, inactivityTimeout=40){ var dateStart = new Date() var wasEverActive = false var parentProgressbar = progressbarContainer.parentNode @@ -155,7 +155,7 @@ function requestProgress(id_task, progressbarContainer, gallery, atEnd, onProgre return } - if(elapsedFromStart > 40 && !res.queued && !res.active){ + if(elapsedFromStart > inactivityTimeout && !res.queued && !res.active){ removeProgressBar() return } diff --git a/javascript/ui.js b/javascript/ui.js index 18a338fb..787b77f5 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -159,13 +159,24 @@ function showSubmitButtons(tabname, show){ gradioApp().getElementById(tabname+'_skip').style.display = show ? "none" : "block" } +function showRestoreProgressButton(tabname, show){ + button = gradioApp().getElementById(tabname + "_restore_progress") + if(! button) return + + button.style.display = show ? "flex" : "none" +} + function submit(){ rememberGallerySelection('txt2img_gallery') showSubmitButtons('txt2img', false) var id = randomId() + localStorage.setItem("txt2img_task_id", id); + requestProgress(id, gradioApp().getElementById('txt2img_gallery_container'), gradioApp().getElementById('txt2img_gallery'), function(){ showSubmitButtons('txt2img', true) + localStorage.removeItem("txt2img_task_id") + showRestoreProgressButton('txt2img', false) }) var res = create_submit_args(arguments) @@ -180,8 +191,12 @@ function submit_img2img(){ showSubmitButtons('img2img', false) var id = randomId() + localStorage.setItem("img2img_task_id", id); + requestProgress(id, gradioApp().getElementById('img2img_gallery_container'), gradioApp().getElementById('img2img_gallery'), function(){ showSubmitButtons('img2img', true) + localStorage.removeItem("img2img_task_id") + showRestoreProgressButton('img2img', false) }) var res = create_submit_args(arguments) @@ -192,6 +207,36 @@ function submit_img2img(){ return res } +function restoreProgressTxt2img(x){ + id = localStorage.getItem("txt2img_task_id") + + if(id) { + requestProgress(id, gradioApp().getElementById('txt2img_gallery_container'), gradioApp().getElementById('txt2img_gallery'), function(){ + showSubmitButtons('txt2img', true) + }, null, 0) + } + + return [id] +} +function restoreProgressImg2img(x){ + id = localStorage.getItem("img2img_task_id") + + if(id) { + requestProgress(id, gradioApp().getElementById('img2img_gallery_container'), gradioApp().getElementById('img2img_gallery'), function(){ + showSubmitButtons('img2img', true) + }, null, 0) + } + + return [id] +} + + +onUiLoaded(function () { + showRestoreProgressButton('txt2img', localStorage.getItem("txt2img_task_id")) + showRestoreProgressButton('img2img', localStorage.getItem("img2img_task_id")) +}); + + function modelmerger(){ var id = randomId() requestProgress(id, gradioApp().getElementById('modelmerger_results_panel'), null, function(){}) @@ -1546,32 +1591,6 @@ function selectCheckpoint(name){ gradioApp().getElementById('change_checkpoint').click() } -function restoreProgress (task_tag) { - - if (task_tag) { - let successHandler = ({ current_task }) => { - if (current_task) { - showSubmitButtons(task_tag, false) - requestProgress(current_task, gradioApp().getElementById(`${task_tag}_gallery_container`), gradioApp().getElementById(`${task_tag}_gallery`), function(){ - showSubmitButtons(task_tag, true) - }) - } - } - - let errorHandler = e => window.alert(`invalid internal api respsonse. message: ${e}`) - - fetch("./internal/current_task") - .then(res => res.json()) - .then(successHandler) - .catch(errorHandler) - } - - var res = create_submit_args(arguments) - res[0] = 0 - return res - -} - function currentImg2imgSourceResolution(_, _, scaleBy){ var img = gradioApp().querySelector('#mode_img2img > div[style="display: block;"] img') return img ? [img.naturalWidth, img.naturalHeight, scaleBy] : [0, 0, scaleBy] @@ -1603,3 +1622,5 @@ window.onload = function() { setTimeout(function(){document.body.style.display = "block";},1000) } + + diff --git a/launch.py b/launch.py index e043e156..1dc12dae 100644 --- a/launch.py +++ b/launch.py @@ -222,7 +222,7 @@ def run_extensions_installers(settings_file): def prepare_environment(): - torch_command = os.environ.get('TORCH_COMMAND', "pip install torch==2.0.0 torchvision==0.15.1 --index-url https://download.pytorch.org/whl/cu118") + torch_command = os.environ.get('TORCH_COMMAND', "pip install torch==2.0.0 torchvision==0.15.1 --extra-index-url https://download.pytorch.org/whl/cu118") requirements_file = os.environ.get('REQS_FILE', "requirements_versions.txt") xformers_package = os.environ.get('XFORMERS_PACKAGE', 'xformers==0.0.17') diff --git a/modules/call_queue.py b/modules/call_queue.py index 43f6ebe0..1829f3a6 100644 --- a/modules/call_queue.py +++ b/modules/call_queue.py @@ -4,16 +4,10 @@ import threading import traceback import time -import gradio as gr from modules import shared, progress queue_lock = threading.Lock() -queue_lock_condition = threading.Condition(lock=queue_lock) -def wrap_session_call(func): - def f(request: gr.Request, *args, **kwargs): - return func(request, *args, **kwargs) - return f def wrap_queued_call(func): def f(*args, **kwargs): @@ -26,31 +20,30 @@ def wrap_queued_call(func): def wrap_gradio_gpu_call(func, extra_outputs=None): - def f(request: gr.Request, *args, **kwargs): - user = request.username + def f(*args, **kwargs): # if the first argument is a string that says "task(...)", it is treated as a job id if len(args) > 0 and type(args[0]) == str and args[0][0:5] == "task(" and args[0][-1] == ")": id_task = args[0] - progress.add_task_to_queue(user, id_task) + progress.add_task_to_queue(id_task) else: id_task = None with queue_lock: shared.state.begin() - progress.start_task(user, id_task) + progress.start_task(id_task) try: res = func(*args, **kwargs) + progress.record_results(id_task, res) finally: - progress.finish_task(user, id_task) - progress.set_last_task_result(user, id_task, res) + progress.finish_task(id_task) shared.state.end() return res - return wrap_session_call(wrap_gradio_call(f, extra_outputs=extra_outputs, add_stats=True)) + return wrap_gradio_call(f, extra_outputs=extra_outputs, add_stats=True) def wrap_gradio_call(func, extra_outputs=None, add_stats=False): diff --git a/modules/progress.py b/modules/progress.py index 13568701..5655346b 100644 --- a/modules/progress.py +++ b/modules/progress.py @@ -4,84 +4,46 @@ import time import gradio as gr from pydantic import BaseModel, Field -from typing import Optional -from fastapi import Depends, Security -from fastapi.security import APIKeyCookie -from modules import call_queue from modules.shared import opts import modules.shared as shared -current_task_user = None current_task = None pending_tasks = {} finished_tasks = [] +recorded_results = [] +recorded_results_limit = 2 -def start_task(user, id_task): +def start_task(id_task): global current_task - global current_task_user - current_task_user = user current_task = id_task - pending_tasks.pop((user, id_task), None) + pending_tasks.pop(id_task, None) -def finish_task(user, id_task): +def finish_task(id_task): global current_task - global current_task_user if current_task == id_task: current_task = None - if current_task_user == user: - current_task_user = None - - finished_tasks.append((user, id_task)) + finished_tasks.append(id_task) if len(finished_tasks) > 16: finished_tasks.pop(0) -def add_task_to_queue(user, id_job): - pending_tasks[(user, id_job)] = time.time() - -last_task_id = None -last_task_result = None -last_task_user = None - -def set_last_task_result(user, id_job, result): - - global last_task_id - global last_task_result - global last_task_user - - last_task_id = id_job - last_task_result = result - last_task_user = user +def record_results(id_task, res): + recorded_results.append((id_task, res)) + if len(recorded_results) > recorded_results_limit: + recorded_results.pop(0) -def restore_progress_call(request: gr.Request): - if current_task is None: +def add_task_to_queue(id_job): + pending_tasks[id_job] = time.time() - # image, generation_info, html_info, html_log - return tuple(list([None, None, None, None])) - - else: - user = request.username - - if current_task_user == user: - t_task = current_task - with call_queue.queue_lock_condition: - call_queue.queue_lock_condition.wait_for(lambda: t_task == last_task_id) - - return last_task_result - - return tuple(list([None, None, None, None])) - -class CurrentTaskResponse(BaseModel): - current_task: str = Field(default=None, title="Task ID", description="id of the current progress task") class ProgressRequest(BaseModel): id_task: str = Field(default=None, title="Task ID", description="id of the task to get progress for") @@ -102,21 +64,6 @@ class ProgressResponse(BaseModel): def setup_progress_api(app): return app.add_api_route("/internal/progress", progressapi, methods=["POST"], response_model=ProgressResponse) -def setup_current_task_api(app): - - def get_current_user(token: Optional[str] = Security(APIKeyCookie(name="access-token", auto_error=False))): - return None if token is None else app.tokens.get(token) - - def current_task_api(current_user: str = Depends(get_current_user)): - - if app.auth is None or current_task_user == current_user: - current_user_task = current_task - else: - current_user_task = None - - return CurrentTaskResponse(current_task=current_user_task) - - return app.add_api_route("/internal/current_task", current_task_api, methods=["GET"], response_model=CurrentTaskResponse) def progressapi(req: ProgressRequest): active = req.id_task == current_task @@ -156,4 +103,15 @@ def progressapi(req: ProgressRequest): else: live_preview = None - return ProgressResponse(active=active, queued=queued, completed=completed, progress=progress, eta=eta, live_preview=live_preview, id_live_preview=id_live_preview, textinfo=shared.state.textinfo) \ No newline at end of file + return ProgressResponse(active=active, queued=queued, completed=completed, progress=progress, eta=eta, live_preview=live_preview, id_live_preview=id_live_preview, textinfo=shared.state.textinfo) + + +def restore_progress(id_task): + while id_task == current_task or id_task in pending_tasks: + time.sleep(0.1) + + res = next(iter([x[1] for x in recorded_results if id_task == x[0]]), None) + if res is not None: + return res + + return gr.update(), gr.update(), gr.update(), f"Couldn't restore progress for {id_task}: results either have been discarded or never were obtained" diff --git a/modules/ui.py b/modules/ui.py index c7b01fe5..4072a813 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -19,7 +19,7 @@ import numpy as np from PIL import Image, PngImagePlugin from modules.call_queue import wrap_gradio_gpu_call, wrap_queued_call, wrap_gradio_call -from modules import sd_hijack, sd_models, localization, script_callbacks, ui_extensions, deepbooru, sd_vae, extra_networks, postprocessing, ui_components, ui_common, ui_postprocessing +from modules import sd_hijack, sd_models, localization, script_callbacks, ui_extensions, deepbooru, sd_vae, extra_networks, postprocessing, ui_components, ui_common, ui_postprocessing, progress from modules.ui_components import FormRow, FormColumn, FormGroup, ToolButton, FormHTML from modules.paths import script_path, data_path @@ -41,7 +41,6 @@ from modules.textual_inversion import textual_inversion import modules.hypernetworks.ui from modules.generation_parameters_copypaste import image_from_url_text import modules.extras -from modules.progress import restore_progress_call warnings.filterwarnings("default" if opts.show_warnings else "ignore", category=UserWarning) @@ -86,6 +85,7 @@ extra_networks_symbol = '\U0001F3B4' # 🎴 switch_values_symbol = '\u2B80' # ⮀ restore_progress_symbol = '\U0001F300' # 🌀 + interogate_bubble_symbol = '\U0001F5E8' # 🗨 interogate_2bubble_symbol = '\U0001F5EA' # 🗪 def plaintext_to_html(text): @@ -356,7 +356,8 @@ def create_toprow(is_img2img): if is_img2img: button_interrogate = ToolButton(value=interogate_bubble_symbol, elem_id="interrogate") button_deepbooru = ToolButton(value=interogate_2bubble_symbol, elem_id="deepbooru") - restore_progress_button = ToolButton(value=restore_progress_symbol, elem_id=f"{id_part}_restore_progress") + + restore_progress_button = ToolButton(value=restore_progress_symbol, elem_id=f"{id_part}_restore_progress", visible=False) token_button = gr.Button(visible=False, elem_id=f"{id_part}_token_button") negative_token_button = gr.Button(visible=False, elem_id=f"{id_part}_negative_token_button") @@ -637,15 +638,16 @@ def create_ui(): res_switch_btn.click(lambda w, h: (h, w), inputs=[width, height], outputs=[width, height], show_progress=False) restore_progress_button.click( - fn=restore_progress_call, - _js="() => restoreProgress('txt2img')", - inputs=[], - outputs=[ - txt2img_gallery, - generation_info, - html_info, - html_log, - ] + fn=progress.restore_progress, + _js="restoreProgressTxt2img", + inputs=[dummy_component], + outputs=[ + txt2img_gallery, + generation_info, + html_info, + html_log, + ], + show_progress=False, ) txt_prompt_img.change( @@ -1052,15 +1054,16 @@ def create_ui(): res_switch_btn.click(lambda w, h: (h, w), inputs=[width, height], outputs=[width, height], show_progress=False) restore_progress_button.click( - fn=restore_progress_call, - _js="() => restoreProgress('img2img')", - inputs=[], - outputs=[ - img2img_gallery, - generation_info, - html_info, - html_log, - ] + fn=progress.restore_progress, + _js="restoreProgressImg2img", + inputs=[dummy_component], + outputs=[ + img2img_gallery, + generation_info, + html_info, + html_log, + ], + show_progress=False, ) img2img_interrogate.click( @@ -1700,7 +1703,7 @@ def create_ui(): gr.HTML(shared.html("licenses.html"), elem_id="licenses") gr.Button(value="Show all pages", elem_id="settings_show_all_pages") - + def unload_sd_weights(): modules.sd_models.unload_model_weights() diff --git a/style.css b/style.css index 5b4fa728..e0e16677 100644 --- a/style.css +++ b/style.css @@ -3867,6 +3867,7 @@ div[data-testid="image"] canvas { [id^="spotlight"].menu .spl-prev { transform: translateX(0); } + [id^="spotlight"].menu .spl-next { transform: translateX(0) scaleX(-1); } diff --git a/webui.py b/webui.py index 7f8d4f84..357bf4c1 100644 --- a/webui.py +++ b/webui.py @@ -339,7 +339,6 @@ def webui(): setup_middleware(app) modules.progress.setup_progress_api(app) - modules.progress.setup_current_task_api(app) if launch_api: create_api(app)