From 41157f7426cb6c8664e0327383a8c3b3572aa9fc Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:20:34 -0700 Subject: [PATCH 001/138] Fix extension requirements install --- installer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/installer.py b/installer.py index ac9c0fe00..d16ec9884 100644 --- a/installer.py +++ b/installer.py @@ -38,7 +38,8 @@ debug = log.debug if os.environ.get('SD_INSTALL_DEBUG', None) is not None else l setuptools, distutils = None, None # defined via ensure_base_requirements current_branch = None pip_log = '--log pip.log ' if os.environ.get('SD_PIP_DEBUG', None) is not None else '' -log_file = os.path.join(os.path.dirname(__file__), 'sdnext.log') +main_directory = os.path.dirname(os.path.abspath(__file__)) +log_file = os.path.join(main_directory, 'sdnext.log') hostname = socket.gethostname() log_rolled = False first_call = True @@ -1279,7 +1280,7 @@ def install_requirements(): # set environment variables controling the behavior of various libraries def set_environment(): log.debug('Setting environment tuning') - os.environ.setdefault('PIP_CONSTRAINT', 'constraints.txt') + os.environ.setdefault('PIP_CONSTRAINT', os.path.join(main_directory, 'constraints.txt')) os.environ.setdefault('ACCELERATE', 'True') os.environ.setdefault('ATTN_PRECISION', 'fp16') os.environ.setdefault('ClDeviceGlobalMemSizeAvailablePercent', '100') From 77d006126928c42dcb3897716eb1c1add7347664 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:51:52 -0700 Subject: [PATCH 002/138] Remove leftover useless code --- installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.py b/installer.py index d16ec9884..db8f423d7 100644 --- a/installer.py +++ b/installer.py @@ -252,7 +252,7 @@ def pip(arg: str, ignore: bool = False, quiet: bool = True, uv = True) -> tuple[ if opts.get('offline_mode', False): log.warning('Offline mode enabled') return None, 'offline' - package = arg.replace("install", "").replace("--upgrade", "").replace("--no-deps", "").replace("--force-reinstall", "").replace(" ", " ").strip() + package = arg.replace("install", "").replace("--upgrade", "").replace("--no-deps", "").replace("--force-reinstall", "").strip() uv = uv and args.uv and not package.startswith('git+') pipCmd = "uv pip" if uv else "pip" if not quiet and '-r ' not in arg: From b37fff123724d6b088bc929708087b8e3f4676e4 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:05:29 -0700 Subject: [PATCH 003/138] Update additional argument handling --- installer.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/installer.py b/installer.py index db8f423d7..caf874837 100644 --- a/installer.py +++ b/installer.py @@ -37,7 +37,7 @@ log = logging.getLogger('sdnext.installer') debug = log.debug if os.environ.get('SD_INSTALL_DEBUG', None) is not None else lambda *args, **kwargs: None setuptools, distutils = None, None # defined via ensure_base_requirements current_branch = None -pip_log = '--log pip.log ' if os.environ.get('SD_PIP_DEBUG', None) is not None else '' +pip_log = '--log pip.log' if os.environ.get('SD_PIP_DEBUG', None) is not None else '' main_directory = os.path.dirname(os.path.abspath(__file__)) log_file = os.path.join(main_directory, 'sdnext.log') hostname = socket.gethostname() @@ -248,7 +248,7 @@ def cleanup_broken_packages(): def pip(arg: str, ignore: bool = False, quiet: bool = True, uv = True) -> tuple[subprocess.CompletedProcess, str]: t_start = time.time() originalArg = arg - arg = arg.replace('>=', '==') + arg = arg.replace('>=', '==').strip() if opts.get('offline_mode', False): log.warning('Offline mode enabled') return None, 'offline' @@ -257,14 +257,21 @@ def pip(arg: str, ignore: bool = False, quiet: bool = True, uv = True) -> tuple[ pipCmd = "uv pip" if uv else "pip" if not quiet and '-r ' not in arg: log.info(f'Install: package="{package}" mode={"uv" if uv else "pip"}') - env_args = os.environ.get("PIP_EXTRA_ARGS", "") - all_args = f'{pip_log}{arg} {env_args}'.strip() + env_args = os.environ.get("PIP_EXTRA_ARGS", "").strip() + all_args: list[str] = [] + if pip_log: + all_args.append(pip_log) + all_args.append(arg) + if env_args: + all_args.append(env_args) if not quiet: - log.debug(f'Running: {pipCmd}="{all_args}"') - result, output = run(sys.executable, "-m", pipCmd, all_args) + log.debug(f'Running: {pipCmd}="{" ".join(all_args)}"') + + result, output = run(sys.executable, "-m", pipCmd, *all_args) + if len(result.stderr) > 0: if uv and result.returncode != 0: - log.warning(f'Install: cmd="{pipCmd}" args="{all_args}" cannot use uv, fallback to pip') + log.warning(f'Install: cmd="{pipCmd}" args="{" ".join(all_args)}" cannot use uv, fallback to pip') debug(f'Install: uv pip error: {result.stderr}') cleanup_broken_packages() return pip(originalArg, ignore, quiet, uv=False) From aff3bf415a98da6c4f6d42f5d04cb987d37c839e Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:22:27 -0700 Subject: [PATCH 004/138] Change pip constraints handling... ...and set "uv" and "constraints" parameters to keyword-only --- installer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/installer.py b/installer.py index caf874837..0099392a5 100644 --- a/installer.py +++ b/installer.py @@ -38,8 +38,7 @@ debug = log.debug if os.environ.get('SD_INSTALL_DEBUG', None) is not None else l setuptools, distutils = None, None # defined via ensure_base_requirements current_branch = None pip_log = '--log pip.log' if os.environ.get('SD_PIP_DEBUG', None) is not None else '' -main_directory = os.path.dirname(os.path.abspath(__file__)) -log_file = os.path.join(main_directory, 'sdnext.log') +log_file = os.path.join(os.path.dirname(__file__), 'sdnext.log') hostname = socket.gethostname() log_rolled = False first_call = True @@ -245,7 +244,7 @@ def cleanup_broken_packages(): pass -def pip(arg: str, ignore: bool = False, quiet: bool = True, uv = True) -> tuple[subprocess.CompletedProcess, str]: +def pip(arg: str, ignore: bool = False, quiet: bool = True, *, uv = True, constraints = True) -> tuple[subprocess.CompletedProcess, str]: t_start = time.time() originalArg = arg arg = arg.replace('>=', '==').strip() @@ -264,6 +263,8 @@ def pip(arg: str, ignore: bool = False, quiet: bool = True, uv = True) -> tuple[ all_args.append(arg) if env_args: all_args.append(env_args) + if constraints and "-c " not in env_args: + all_args.append("-c constraints.txt") if not quiet: log.debug(f'Running: {pipCmd}="{" ".join(all_args)}"') @@ -1287,7 +1288,6 @@ def install_requirements(): # set environment variables controling the behavior of various libraries def set_environment(): log.debug('Setting environment tuning') - os.environ.setdefault('PIP_CONSTRAINT', os.path.join(main_directory, 'constraints.txt')) os.environ.setdefault('ACCELERATE', 'True') os.environ.setdefault('ATTN_PRECISION', 'fp16') os.environ.setdefault('ClDeviceGlobalMemSizeAvailablePercent', '100') From 19bc712ecf687dedb2e40226e00a744438d017e2 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:35:25 -0700 Subject: [PATCH 005/138] Fix typing --- installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer.py b/installer.py index 0099392a5..ea5fd6ed1 100644 --- a/installer.py +++ b/installer.py @@ -244,7 +244,7 @@ def cleanup_broken_packages(): pass -def pip(arg: str, ignore: bool = False, quiet: bool = True, *, uv = True, constraints = True) -> tuple[subprocess.CompletedProcess, str]: +def pip(arg: str, ignore: bool = False, quiet: bool = True, *, uv = True, constraints = True) -> tuple[subprocess.CompletedProcess | None, str]: t_start = time.time() originalArg = arg arg = arg.replace('>=', '==').strip() From 68f1c73e1fced98603e7bcd37bcf2126a7137016 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 29 Apr 2026 07:59:43 +0200 Subject: [PATCH 006/138] remove process preview Co-authored-by: Copilot Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 6 ++++++ modules/control/processors.py | 4 +++- modules/control/test.py | 26 ++++++++++-------------- modules/history.py | 1 - modules/masking.py | 12 +++++------ modules/ui_control.py | 37 +++++++++++++--------------------- modules/ui_control_elements.py | 12 +++++------ modules/ui_control_helpers.py | 18 ++++++++--------- wiki | 2 +- 9 files changed, 56 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29553c783..6f0d96240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log for SD.Next +## Update for 2026-04-29 + +- **Control** + - remove "processed preview" from ui + preprocessor output can still be generated by clicking preview button in in control unit and it will render into normal output area + ## Update for 2026-04-28 ### Highlights for 2026-04-28 diff --git a/modules/control/processors.py b/modules/control/processors.py index bdf5e53c7..1f03f5594 100644 --- a/modules/control/processors.py +++ b/modules/control/processors.py @@ -386,7 +386,9 @@ class Processor: def preview(self): import modules.ui_control_helpers as helpers input_image = helpers.input_source + if input_image is None: + return [] if isinstance(input_image, list): input_image = input_image[0] debug('Control process preview') - return self.__call__(input_image) + return [self.__call__(input_image)] diff --git a/modules/control/test.py b/modules/control/test.py index 29653e41a..6627c7d04 100644 --- a/modules/control/test.py +++ b/modules/control/test.py @@ -39,7 +39,7 @@ def test_processors(image): draw.text((10, 10), processor_id, (0,0,0), font=font) draw.text((8, 8), processor_id, (255,255,255), font=font) res.append(output) - yield output, None, None, res + yield output, None, res rows = round(math.sqrt(len(res))) cols = math.ceil(len(res) / rows) w, h = 256, 256 @@ -52,8 +52,7 @@ def test_processors(image): thumb = image.copy().convert('RGB') thumb.thumbnail((w, h), Image.Resampling.HAMMING) grid.paste(thumb, box=(x, y)) - yield None, grid, None, res - return None, grid, None, res # preview_process, output_image, output_video, output_gallery + yield None, grid, res def test_controlnets(prompt, negative, image): @@ -90,7 +89,7 @@ def test_controlnets(prompt, negative, image): draw.text((10, 10), model_id, (0,0,0), font=font) draw.text((8, 8), model_id, (255,255,255), font=font) res.append(output) - yield output, None, None, res + yield output, None, res rows = round(math.sqrt(len(res))) cols = math.ceil(len(res) / rows) w, h = 256, 256 @@ -103,8 +102,7 @@ def test_controlnets(prompt, negative, image): thumb = image.copy().convert('RGB') thumb.thumbnail((w, h), Image.Resampling.HAMMING) grid.paste(thumb, box=(x, y)) - yield None, grid, None, res - return None, grid, None, res # preview_process, output_image, output_video, output_gallery + yield grid, None, res def test_adapters(prompt, negative, image): @@ -142,7 +140,7 @@ def test_adapters(prompt, negative, image): draw.text((10, 10), model_id, (0,0,0), font=font) draw.text((8, 8), model_id, (255,255,255), font=font) res.append(output) - yield output, None, None, res + yield output, None, res rows = round(math.sqrt(len(res))) cols = math.ceil(len(res) / rows) w, h = 256, 256 @@ -155,8 +153,7 @@ def test_adapters(prompt, negative, image): thumb = image.copy().convert('RGB') thumb.thumbnail((w, h), Image.Resampling.HAMMING) grid.paste(thumb, box=(x, y)) - yield None, grid, None, res - return None, grid, None, res # preview_process, output_image, output_video, output_gallery + yield grid, None, res def test_xs(prompt, negative, image): @@ -193,7 +190,7 @@ def test_xs(prompt, negative, image): draw.text((10, 10), model_id, (0,0,0), font=font) draw.text((8, 8), model_id, (255,255,255), font=font) res.append(output) - yield output, None, None, res + yield output, None, res rows = round(math.sqrt(len(res))) cols = math.ceil(len(res) / rows) w, h = 256, 256 @@ -206,8 +203,7 @@ def test_xs(prompt, negative, image): thumb = image.copy().convert('RGB') thumb.thumbnail((w, h), Image.Resampling.HAMMING) grid.paste(thumb, box=(x, y)) - yield None, grid, None, res - return None, grid, None, res # preview_process, output_image, output_video, output_gallery + yield grid, None, res def test_lite(prompt, negative, image): @@ -245,7 +241,7 @@ def test_lite(prompt, negative, image): draw.text((10, 10), model_id, (0,0,0), font=font) draw.text((8, 8), model_id, (255,255,255), font=font) res.append(output) - yield output, None, None, res + yield output, None, res rows = round(math.sqrt(len(res))) cols = math.ceil(len(res) / rows) w, h = 256, 256 @@ -258,5 +254,5 @@ def test_lite(prompt, negative, image): thumb = image.copy().convert('RGB') thumb.thumbnail((w, h), Image.Resampling.HAMMING) grid.paste(thumb, box=(x, y)) - yield None, grid, None, res - return None, grid, None, res # preview_process, output_image, output_video, output_gallery + yield grid, None, res + return grid, None, res # preview_process, output_image, output_video, output_gallery diff --git a/modules/history.py b/modules/history.py index 62166b895..f98fea1b2 100644 --- a/modules/history.py +++ b/modules/history.py @@ -80,7 +80,6 @@ class History: break current_index -= 1 if item.latent is None: - print('HERE') return None, -1 log.debug(f'History get: index={current_index} time={item.ts} shape={list(item.latent.shape)} dtype={item.latent.dtype} count={self.count}') return item.latent.to(devices.device), current_index diff --git a/modules/masking.py b/modules/masking.py index 9b5b193fa..07dce8a2e 100644 --- a/modules/masking.py +++ b/modules/masking.py @@ -290,9 +290,9 @@ def run_rembg(input_image: Image.Image, input_mask: np.ndarray): 'alpha_matting_foreground_threshold': 240, 'alpha_matting_background_threshold': 10, 'alpha_matting_erode_size': int(opts.mask_erode * 40), - 'session': rembg.new_session(opts.model), + 'session': rembg.new_session(opts.model), # pylint: disable=c-extension-no-member } - mask = rembg.remove(**args) + mask = rembg.remove(**args) # pylint: disable=c-extension-no-member mask = np.array(mask) if input_mask is None: input_mask = np.zeros(mask.shape, dtype='uint8') @@ -574,13 +574,13 @@ def create_segment_ui(): return controls -def bind_controls(image_controls: list[gr.Image], preview_image: gr.Image, output_image: gr.Image): +def bind_controls(image_controls: list[gr.Image], output_image: gr.Image): for image_control in image_controls: - btn_mask.click(run_mask, inputs=[image_control], outputs=[preview_image]) + btn_mask.click(run_mask, inputs=[image_control], outputs=[output_image]) btn_lama.click(run_lama, inputs=[image_control], outputs=[output_image]) - image_control.edit(fn=run_mask_live, inputs=[image_control], outputs=[preview_image]) + image_control.edit(fn=run_mask_live, inputs=[image_control], outputs=[output_image]) for control in controls: - control.change(fn=run_mask_live, inputs=[image_control], outputs=[preview_image]) + control.change(fn=run_mask_live, inputs=[image_control], outputs=[output_image]) def process_kanvas(kanvas_data): diff --git a/modules/ui_control.py b/modules/ui_control.py index da4784403..03e24470a 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -56,11 +56,11 @@ def return_controls(res, t: float | None = None): else: perf = return_stats(t) if res is None: # no response - return [None, None, None, None, '', perf] + return [None, None, None, '', perf] elif isinstance(res, str): # error response - return [None, None, None, None, res, perf] + return [None, None, None, res, perf] elif isinstance(res, tuple): # standard response received as tuple via control_run->yield(output_images, process_image, result_txt) - preview_image = res[1] # may be None + _preview_image = res[1] # may be None output_image = res[0][0] if isinstance(res[0], list) else res[0] # may be image or list of images if isinstance(res[0], list): output_gallery = res[0] if res[0][0] is not None else [] @@ -68,9 +68,9 @@ def return_controls(res, t: float | None = None): output_gallery = [res[0]] if res[0] is not None else [] # must return list, but can receive single image result_txt = res[2] if len(res) > 2 else '' # do we have a message output_video = res[3] if len(res) > 3 else None # do we have a video filename - return [preview_image, output_image, output_video, output_gallery, result_txt, perf] + return [output_image, output_video, output_gallery, result_txt, perf] else: # unexpected - return [None, None, None, None, f'Control: Unexpected response: {type(res)}', perf] + return [None, None, None, f'Control: Unexpected response: {type(res)}', perf] def get_units(*values): @@ -168,7 +168,6 @@ def create_ui(_blocks: gr.Blocks=None): with gr.Accordion(open=False, label="Input", elem_id="control_input", elem_classes=["small-accordion"]): with gr.Row(): show_input = gr.Checkbox(label="Show input", value=True, elem_id="control_show_input") - show_preview = gr.Checkbox(label="Show preview", value=False, elem_id="control_show_preview") with gr.Row(): input_type = gr.Radio(label="Control input type", choices=['Control only', 'Init image same as control', 'Separate init image'], value='Control only', type='index', elem_id='control_input_type') with gr.Row(): @@ -259,15 +258,9 @@ def create_ui(_blocks: gr.Blocks=None): output_image = gr.Image(label="Output", show_label=False, type="pil", interactive=False, tool="editor", height=gr_height, elem_id='control_output_image', elem_classes=['control-image']) with gr.Tab('Video', id='out-video'): output_video = gr.Video(label="Output", show_label=False, height=gr_height, elem_id='control_output_video', elem_classes=['control-image']) - with gr.Column(scale=9, elem_id='control-preview-column', visible=False) as column_preview: - gr.HTML('Preview

') - with gr.Tabs(elem_classes=['control-tabs'], elem_id='control-tab-preview'): - with gr.Tab('Preview', id='preview-image') as _tab_preview: - preview_process = gr.Image(label="Preview", show_label=False, type="pil", interactive=False, height=gr_height, visible=True, elem_id='control_preview', elem_classes=['control-image']) - from modules.ui_control_elements import create_ui_elements - create_ui_elements(units, result_txt, preview_process) + create_ui_elements(units, result_txt, output_gallery) with gr.Row(elem_id="control_script_container"): input_script_args = scripts_manager.scripts_current.setup_ui(parent='control', accordion=True) @@ -284,7 +277,6 @@ def create_ui(_blocks: gr.Blocks=None): 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), @@ -303,7 +295,7 @@ def create_ui(_blocks: gr.Blocks=None): 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], + outputs=[output_tabs, result_txt, width_before, height_before], show_progress='hidden', queue=False, ) @@ -345,7 +337,6 @@ def create_ui(_blocks: gr.Blocks=None): override_script_name, override_script_args, override_settings, ] output_fields = [ - preview_process, output_image, output_video, output_gallery, @@ -470,9 +461,9 @@ def create_ui(_blocks: gr.Blocks=None): generation_parameters_copypaste.register_paste_params_button(bindings) if (installer.version['kanvas'] == 'disabled') or (installer.version['kanvas'] == 'unavailable'): - masking.bind_controls([input_image], preview_process, output_image) + masking.bind_controls([input_image], output_image) else: - masking.bind_kanvas(input_image, preview_process) + masking.bind_kanvas(input_image, output_image) if os.environ.get('SD_CONTROL_DEBUG', None) is not None: # debug only from modules.control.test import test_processors, test_controlnets, test_adapters, test_xs, test_lite @@ -484,11 +475,11 @@ def create_ui(_blocks: gr.Blocks=None): run_test_adapters_btn = gr.Button(value="Test:Adapters", variant='primary', elem_classes=['control-button']) run_test_lite_btn = gr.Button(value="Test:Control-LLLite", variant='primary', elem_classes=['control-button']) - run_test_processors_btn.click(fn=test_processors, inputs=[input_image], outputs=[preview_process, output_image, output_video, output_gallery]) - run_test_controlnets_btn.click(fn=test_controlnets, inputs=[prompt, negative, input_image], outputs=[preview_process, output_image, output_video, output_gallery]) - run_test_xs_btn.click(fn=test_xs, inputs=[prompt, negative, input_image], outputs=[preview_process, output_image, output_video, output_gallery]) - run_test_adapters_btn.click(fn=test_adapters, inputs=[prompt, negative, input_image], outputs=[preview_process, output_image, output_video, output_gallery]) - run_test_lite_btn.click(fn=test_lite, inputs=[prompt, negative, input_image], outputs=[preview_process, output_image, output_video, output_gallery]) + run_test_processors_btn.click(fn=test_processors, inputs=[input_image], outputs=[output_image, output_video, output_gallery]) + run_test_controlnets_btn.click(fn=test_controlnets, inputs=[prompt, negative, input_image], outputs=[output_image, output_video, output_gallery]) + run_test_xs_btn.click(fn=test_xs, inputs=[prompt, negative, input_image], outputs=[output_image, output_video, output_gallery]) + run_test_adapters_btn.click(fn=test_adapters, inputs=[prompt, negative, input_image], outputs=[output_image, output_video, output_gallery]) + run_test_lite_btn.click(fn=test_lite, inputs=[prompt, negative, input_image], outputs=[output_image, output_video, output_gallery]) ui_extra_networks.setup_ui(extra_networks_ui, output_gallery) return [(control_ui, 'Control', 'control')] diff --git a/modules/ui_control_elements.py b/modules/ui_control_elements.py index 6c207087a..cc4579940 100644 --- a/modules/ui_control_elements.py +++ b/modules/ui_control_elements.py @@ -11,7 +11,7 @@ from modules import shared, ui_components, ui_symbols, ui_common, masking # pyli from modules import ui_control_helpers as helpers -def create_ui_elements(units, result_txt, preview_process): +def create_ui_elements(units, result_txt, output_gallery): max_units = shared.opts.control_max_units with gr.Accordion('Control elements', open=False, elem_id="control_elements"): with gr.Tabs(elem_id='control-tabs') as _tabs_control_type: @@ -54,7 +54,7 @@ def create_ui_elements(units, result_txt, preview_process): process_id = process_id, model_id = model_id, model_strength = model_strength, - preview_process = preview_process, + preview_process = output_gallery, preview_btn = preview_btn, image_upload = image_upload, image_reuse = image_reuse, @@ -103,7 +103,7 @@ def create_ui_elements(units, result_txt, preview_process): process_id = process_id, model_id = model_id, model_strength = model_strength, - preview_process = preview_process, + preview_process = output_gallery, preview_btn = btn_preview, image_upload = image_upload, image_reuse = image_reuse, @@ -150,7 +150,7 @@ def create_ui_elements(units, result_txt, preview_process): process_id = process_id, model_id = model_id, model_strength = model_strength, - preview_process = preview_process, + preview_process = output_gallery, preview_btn = btn_preview, image_upload = image_upload, image_reuse = image_reuse, @@ -196,7 +196,7 @@ def create_ui_elements(units, result_txt, preview_process): process_id = process_id, model_id = model_id, model_strength = model_strength, - preview_process = preview_process, + preview_process = output_gallery, preview_btn = btn_preview, image_upload = image_upload, image_reuse = image_reuse, @@ -239,7 +239,7 @@ def create_ui_elements(units, result_txt, preview_process): process_id = process_id, model_id = model_id, model_strength = model_strength, - preview_process = preview_process, + preview_process = output_gallery, preview_btn = btn_preview, image_upload = image_upload, image_reuse = image_reuse, diff --git a/modules/ui_control_helpers.py b/modules/ui_control_helpers.py index 36f63d337..a789ecad1 100644 --- a/modules/ui_control_helpers.py +++ b/modules/ui_control_helpers.py @@ -146,17 +146,17 @@ def select_input(input_mode, input_image, init_image, init_type, input_video, in if selected_input is None: # log.debug(f'Select input: image={selected_input}') input_source = None - return [gr.Tabs.update(), None, ''] + size + return [gr.Tabs.update(), ''] + size elif selected_input == input_prev: # log.debug(f'Select input: image={selected_input} no change') - return [gr.Tabs.update(), None, ''] + size + return [gr.Tabs.update(), ''] + size input_prev = selected_input busy = True input_type = type(selected_input) input_mask = None status = 'Control input | Unknown' - res = [gr.Tabs.update(selected='out-gallery'), input_mask, status] + res = [gr.Tabs.update(selected='out-gallery'), status] # control inputs if isinstance(selected_input, Image.Image): # image via upload -> image if input_mode == 'Outpaint': @@ -166,7 +166,7 @@ def select_input(input_mode, input_image, init_image, init_type, input_video, in input_type = 'PIL.Image' status = f'Control input | Image | Size {selected_input.width if selected_input else 0}x{selected_input.height if selected_input else 0} | Mode {selected_input.mode if selected_input else "Unknown"}' size = [gr.update(value=selected_input.width), gr.update(value=selected_input.height)] - res = [gr.Tabs.update(selected='out-gallery'), input_mask, status] + res = [gr.Tabs.update(selected='out-gallery'), status] elif isinstance(selected_input, dict) and 'kanvas' in selected_input: # kanvas via js -> kanvas dict selected_input, input_mask = process_kanvas(selected_input) input_source = [selected_input] @@ -174,23 +174,23 @@ def select_input(input_mode, input_image, init_image, init_type, input_video, in status = f'Control input | Kanvas | Size {selected_input.width if selected_input else 0}x{selected_input.height if selected_input else 0} | Mode {selected_input.mode if selected_input else "Unknown"}' if selected_input: size = [gr.update(value=selected_input.width), gr.update(value=selected_input.height)] - res = [gr.Tabs.update(selected='out-gallery'), input_mask, status] + res = [gr.Tabs.update(selected='out-gallery'), status] elif isinstance(selected_input, dict) and 'mask' in selected_input: # inpaint -> dict image+mask input_mask = selected_input['mask'] selected_input = selected_input['image'] input_source = [selected_input] input_type = 'PIL.Image' status = f'Control input | Image | Size {selected_input.width if selected_input else 0}x{selected_input.height if selected_input else 0} | Mode {selected_input.mode if selected_input else "Unknown"}' - res = [gr.Tabs.update(selected='out-gallery'), input_mask, status] + res = [gr.Tabs.update(selected='out-gallery'), status] elif isinstance(selected_input, gr.components.image.Image): # not likely input_source = [selected_input.value] input_type = 'gr.Image' - res = [gr.Tabs.update(selected='out-gallery'), input_mask, status] + res = [gr.Tabs.update(selected='out-gallery'), status] elif isinstance(selected_input, str) and os.path.exists(selected_input): # video via upload > tmp filepath to video input_source = selected_input input_type = 'gr.Video' status = get_video(input_source) - res = [gr.Tabs.update(selected='out-video'), input_mask, status] + res = [gr.Tabs.update(selected='out-video'), status] elif isinstance(selected_input, list): # batch or folder via upload -> list of tmp filepaths if hasattr(selected_input[0], 'name'): input_type = 'tempfiles' @@ -199,7 +199,7 @@ def select_input(input_mode, input_image, init_image, init_type, input_video, in input_type = 'files' input_source = selected_input status = f'Control input | Images | Files {len(input_source)}' - res = [gr.Tabs.update(selected='out-gallery'), input_mask, status] + res = [gr.Tabs.update(selected='out-gallery'), status] else: # unknown input_source = None if init_type == 0: # Control only diff --git a/wiki b/wiki index 070218735..890415e99 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 07021873564383be3456b673ad27693db31a8d9a +Subproject commit 890415e996c14738eab02524f11a675f9b5a56b4 From 60143027a06491a70517420d823b833295ef2c91 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 29 Apr 2026 08:18:02 +0200 Subject: [PATCH 007/138] remove media input buttons Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + extensions-builtin/sdnext-kanvas | 2 +- extensions-builtin/sdnext-modernui | 2 +- html/locale_ar.json | 2 +- html/locale_bn.json | 2 +- html/locale_de.json | 2 +- html/locale_en.json | 3 +-- html/locale_es.json | 2 +- html/locale_fr.json | 2 +- html/locale_he.json | 2 +- html/locale_hi.json | 2 +- html/locale_hr.json | 2 +- html/locale_id.json | 2 +- html/locale_it.json | 2 +- html/locale_ja.json | 4 ++-- html/locale_ko.json | 2 +- html/locale_nb.json | 4 ++-- html/locale_po.json | 2 +- html/locale_pt.json | 2 +- html/locale_qq.json | 2 +- html/locale_ru.json | 2 +- html/locale_sr.json | 2 +- html/locale_tb.json | 2 +- html/locale_tlh.json | 2 +- html/locale_tr.json | 2 +- html/locale_ur.json | 2 +- html/locale_vi.json | 2 +- html/locale_xx.json | 2 +- html/locale_zh.json | 2 +- javascript/ui.js | 4 ---- modules/ui_control.py | 5 +---- 31 files changed, 32 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0d96240..ce7e8aa5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Update for 2026-04-29 - **Control** + - remove buttons: input/control/process - remove "processed preview" from ui preprocessor output can still be generated by clicking preview button in in control unit and it will render into normal output area diff --git a/extensions-builtin/sdnext-kanvas b/extensions-builtin/sdnext-kanvas index 9a76f2093..dbea4ea19 160000 --- a/extensions-builtin/sdnext-kanvas +++ b/extensions-builtin/sdnext-kanvas @@ -1 +1 @@ -Subproject commit 9a76f209312dc88b267af126c60fc8b368678131 +Subproject commit dbea4ea19e0605e2158bef607dc423f12261f0a8 diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index f81771717..9c04701e8 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit f81771717424299e99542b2ceb88e3cca9f7f16c +Subproject commit 9c04701e807f9b0102a79d124b9faa87d9b86453 diff --git a/html/locale_ar.json b/html/locale_ar.json index 596040792..b1c1d3cb5 100644 --- a/html/locale_ar.json +++ b/html/locale_ar.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "وسائط الإدخال", "reload": "", "hint": "إضافة صورة إدخال لاستخدامها في معالجة التحويل من صورة إلى صورة، أو التلوين (Inpaint)، أو التحكم" diff --git a/html/locale_bn.json b/html/locale_bn.json index f7fe99ac3..252670494 100644 --- a/html/locale_bn.json +++ b/html/locale_bn.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "ইনপুট মিডিয়া", "reload": "", "hint": "ইমেজ-টু-ইমেজ, ইনপেইন্ট বা কন্ট্রোল প্রসেসিংয়ের জন্য ইনপুট ছবি যোগ করুন" diff --git a/html/locale_de.json b/html/locale_de.json index 1bdd46a1a..9fa94e636 100644 --- a/html/locale_de.json +++ b/html/locale_de.json @@ -4817,7 +4817,7 @@ }, { "id": 14, - "label": "Input Media", + "label": "Input", "localized": "Eingabemedien", "reload": "", "hint": "Eingabebild hinzufügen, das für Image-to-Image-, Inpaint- oder Control-Verarbeitung verwendet werden soll" diff --git a/html/locale_en.json b/html/locale_en.json index db1f36ed2..42ef8550e 100644 --- a/html/locale_en.json +++ b/html/locale_en.json @@ -700,7 +700,6 @@ "i": [ {"id":"control_nav","label":"Images","localized":"","hint":"Create images
Unified interface
Supports T2I and I2I
With optional control guidance"}, {"id":"img2img_nav","label":"I2I","localized":"","hint":"Create image from image
Legacy interface that mimics original image-to-image interface and behavior"}, - {"id":"img2img_results_input_mobile","label":"Input","localized":"","hint":"Show/hide selection of input media used to guide generation","ui":"img2img"}, {"id":"","label":"Image","localized":"","hint":"Create image from image","ui":"img2img"}, {"id":"","label":"Inpaint","localized":"","hint":"","ui":"img2img"}, {"id":"control_params_mask","label":"Inputs","localized":"","hint":"Settings related to Input images","ui":"control"}, @@ -711,7 +710,7 @@ {"id":"","label":"Image Paths","localized":"","hint":"Settings related to image filenames, and output directories"}, {"id":"","label":"Image Metadata","localized":"","hint":"Settings related to handling of metadata that is created with generated images"}, {"id":"","label":"IP Adapters","localized":"","hint":"IP adapters are plugin models that can guide generation towards desired outcome","ui":"txt2img"}, - {"id":"","label":"Input Media","localized":"","hint":"Add input image to be used for image-to-image, inpaint or control processing","ui":"control"}, + {"id":"","label":"Input","localized":"","hint":"Add input image to be used for image-to-image, inpaint or control processing","ui":"control"}, {"id":"","label":"Input Image","localized":"","hint":"","ui":"caption"}, {"id":"","label":"IPEX","localized":"","hint":"","ui":"settings_backends"}, {"id":"","label":"Image Gallery","localized":"","hint":"","ui":"settings_saving-images"}, diff --git a/html/locale_es.json b/html/locale_es.json index 42f8f72bb..94d82fbe0 100644 --- a/html/locale_es.json +++ b/html/locale_es.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "Medios de entrada", "reload": "", "hint": "Añadir imagen de entrada para ser utilizada para el procesamiento de imagen a imagen, inpaint o control" diff --git a/html/locale_fr.json b/html/locale_fr.json index 470792ab9..5b2c37935 100644 --- a/html/locale_fr.json +++ b/html/locale_fr.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "Média d'entrée", "reload": "", "hint": "Ajouter une image d'entrée à utiliser pour le traitement image-à-image, inpaint ou control" diff --git a/html/locale_he.json b/html/locale_he.json index 8d1e7704f..27a37076b 100644 --- a/html/locale_he.json +++ b/html/locale_he.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "מדית קלט", "reload": "", "hint": "הוספת תמונת קלט לשימוש עבור עיבוד תמונה-לתמונה, מילוי או בקרה" diff --git a/html/locale_hi.json b/html/locale_hi.json index 54d213b59..8aa989b29 100644 --- a/html/locale_hi.json +++ b/html/locale_hi.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "इनपुट मीडिया", "reload": "n/a", "hint": "इमेज-टू-इमेज, इनपेंट या कंट्रोल प्रोसेसिंग के लिए उपयोग की जाने वाली इनपुट छवि जोड़ें" diff --git a/html/locale_hr.json b/html/locale_hr.json index d092a3e55..efa35485f 100644 --- a/html/locale_hr.json +++ b/html/locale_hr.json @@ -4824,7 +4824,7 @@ }, { "id": 0, - "label": "Input Media", + "label": "Input", "localized": "Ulazni medij", "reload": "", "hint": "Dodajte ulaznu sliku koja će se koristiti za image-to-image, inpaint ili kontrolnu obradu" diff --git a/html/locale_id.json b/html/locale_id.json index c13c3fe19..7450256f8 100644 --- a/html/locale_id.json +++ b/html/locale_id.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "Media Masukan", "reload": "n/a", "hint": "Tambahkan gambar masukan untuk digunakan dalam pemrosesan image-to-image, inpaint, atau kontrol" diff --git a/html/locale_it.json b/html/locale_it.json index c557311c1..034654b91 100644 --- a/html/locale_it.json +++ b/html/locale_it.json @@ -4817,7 +4817,7 @@ }, { "id": 14, - "label": "Input Media", + "label": "Input", "localized": "Media di input", "reload": "n/a", "hint": "Aggiungi un'immagine di input da utilizzare per elaborazioni image-to-image, inpaint o di controllo" diff --git a/html/locale_ja.json b/html/locale_ja.json index d16520c68..e30dba4f0 100644 --- a/html/locale_ja.json +++ b/html/locale_ja.json @@ -4817,9 +4817,9 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "入力メディア", - "reload": "Input Media", + "reload": "Input", "hint": "画像間変換、インペイント、またはコントロール処理に使用する入力画像を追加します" }, { diff --git a/html/locale_ko.json b/html/locale_ko.json index 00e3a4032..a5918ee95 100644 --- a/html/locale_ko.json +++ b/html/locale_ko.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "입력 미디어", "reload": "n/a", "hint": "이미지 대 이미지, 인페인트 또는 제어 처리에 사용할 입력 이미지 추가" diff --git a/html/locale_nb.json b/html/locale_nb.json index 1542f19f6..89e39b0f7 100644 --- a/html/locale_nb.json +++ b/html/locale_nb.json @@ -4817,8 +4817,8 @@ }, { "id": 13, - "label": "Input Media", - "localized": "Input Media", + "label": "Input", + "localized": "Input", "reload": "", "hint": "Add an image here to use it as a base for editing or guiding the AI." }, diff --git a/html/locale_po.json b/html/locale_po.json index 8d8055819..b391db6af 100644 --- a/html/locale_po.json +++ b/html/locale_po.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "Media wejściowe", "reload": "", "hint": "Dodaj obraz wejściowy do użycia w przetwarzaniu typu image-to-image, inpaint lub control" diff --git a/html/locale_pt.json b/html/locale_pt.json index db261911e..065887bf9 100644 --- a/html/locale_pt.json +++ b/html/locale_pt.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "Mídia de Entrada", "reload": "", "hint": "Adicionar imagem de entrada a ser usada para processamento de imagem-para-imagem, inpaint ou controle" diff --git a/html/locale_qq.json b/html/locale_qq.json index b033d7be9..1d351b192 100644 --- a/html/locale_qq.json +++ b/html/locale_qq.json @@ -4817,7 +4817,7 @@ }, { "id": 0, - "label": "Input Media", + "label": "Input", "localized": "Media Input", "reload": "n/a", "hint": "Addere imaginem input adhibendam pro processu imaginis-ad-imaginem, inpaint, vel moderationis" diff --git a/html/locale_ru.json b/html/locale_ru.json index db706a1a2..318e7754f 100644 --- a/html/locale_ru.json +++ b/html/locale_ru.json @@ -4817,7 +4817,7 @@ }, { "id": 14, - "label": "Input Media", + "label": "Input", "localized": "Входные медиаданные", "reload": "", "hint": "Добавить входное изображение для использования в image-to-image, inpaint или для управления генерацией" diff --git a/html/locale_sr.json b/html/locale_sr.json index d0df82d7b..2cbccaada 100644 --- a/html/locale_sr.json +++ b/html/locale_sr.json @@ -4817,7 +4817,7 @@ }, { "id": 14, - "label": "Input Media", + "label": "Input", "localized": "Ulazni medij", "reload": "", "hint": "Dodajte ulaznu sliku koja će se koristiti za obradu slike-u-sliku, inpaint ili kontrolnu obradu" diff --git a/html/locale_tb.json b/html/locale_tb.json index 53f4ad85a..bb09b9850 100644 --- a/html/locale_tb.json +++ b/html/locale_tb.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "Source Telemetry", "reload": "", "hint": "Upload and synchronize source data for recursive generation, delta-patching, or neural guidance processing" diff --git a/html/locale_tlh.json b/html/locale_tlh.json index 80e758920..a0447e4c8 100644 --- a/html/locale_tlh.json +++ b/html/locale_tlh.json @@ -4817,7 +4817,7 @@ }, { "id": 14, - "label": "Input Media", + "label": "Input", "localized": "nI' Media", "reload": "", "hint": "nI' media" diff --git a/html/locale_tr.json b/html/locale_tr.json index 275409c4c..2a0ddfd06 100644 --- a/html/locale_tr.json +++ b/html/locale_tr.json @@ -4817,7 +4817,7 @@ }, { "id": 14, - "label": "Input Media", + "label": "Input", "localized": "Giriş Medyası", "reload": "", "hint": "Görüntüden görüntüye, inpaint veya kontrol işleme için kullanılacak giriş görüntüsünü ekleyin" diff --git a/html/locale_ur.json b/html/locale_ur.json index 5063f6e7e..4edfe260a 100644 --- a/html/locale_ur.json +++ b/html/locale_ur.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "ان پٹ میڈیا", "reload": "", "hint": "تصویر سے تصویر (image-to-image)، ان پینٹ یا کنٹرول پروسیسنگ کے لیے استعمال ہونے والی ان پٹ تصویر شامل کریں" diff --git a/html/locale_vi.json b/html/locale_vi.json index 4e631bb43..06187acbd 100644 --- a/html/locale_vi.json +++ b/html/locale_vi.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "Phương tiện đầu vào", "reload": "", "hint": "Thêm hình ảnh đầu vào để sử dụng cho xử lý hình ảnh thành hình ảnh, inpaint hoặc điều khiển" diff --git a/html/locale_xx.json b/html/locale_xx.json index 5674c5994..79d813296 100644 --- a/html/locale_xx.json +++ b/html/locale_xx.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "Eniga Amaskomunikilaro", "reload": "", "hint": "Aldoni enigeblan bildon por esti uzata por bild-al-bilda, inpaint aŭ kontrola prilaborado" diff --git a/html/locale_zh.json b/html/locale_zh.json index 7aa02402e..700de6f4e 100644 --- a/html/locale_zh.json +++ b/html/locale_zh.json @@ -4817,7 +4817,7 @@ }, { "id": 13, - "label": "Input Media", + "label": "Input", "localized": "输入媒体", "reload": "", "hint": "添加用于图生图、重绘或控制处理的输入图像" diff --git a/javascript/ui.js b/javascript/ui.js index 0c973cc10..a4bf0a75e 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -108,10 +108,6 @@ function send_to_kanvas(gallery) { const [image] = extract_image_from_gallery(gallery); log('sendToKanvas', image); if (window.loadFromURL && image.data) window.loadFromURL(image.data); - // const inputPanelEl = gradioApp().getElementById('control-template-column-input'); - // if (inputPanelEl) inputPanelEl.classList.remove('hidden'); - const inputPanelCb = gradioApp().getElementById('control_dynamic_input'); - if (inputPanelCb && !inputPanelCb.checked) inputPanelCb.click(); } async function setTheme(val, old) { diff --git a/modules/ui_control.py b/modules/ui_control.py index 03e24470a..56fb39599 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -166,8 +166,6 @@ def create_ui(_blocks: gr.Blocks=None): state = gr.Textbox(value='', visible=False) with gr.Accordion(open=False, label="Input", elem_id="control_input", elem_classes=["small-accordion"]): - with gr.Row(): - show_input = gr.Checkbox(label="Show input", value=True, elem_id="control_show_input") with gr.Row(): input_type = gr.Radio(label="Control input type", choices=['Control only', 'Init image same as control', 'Separate init image'], value='Control only', type='index', elem_id='control_input_type') with gr.Row(): @@ -220,7 +218,7 @@ def create_ui(_blocks: gr.Blocks=None): timer.startup.record('ui-networks') with gr.Row(elem_id='control-inputs'): - with gr.Column(scale=9, elem_id='control-input-column', visible=True) as column_input: + with gr.Column(scale=9, elem_id='control-input-column', visible=True) as _column_input: gr.HTML('Input

') with gr.Tabs(elem_classes=['control-tabs'], elem_id='control-tab-input'): input_mode = gr.Label(value='select', visible=False) @@ -276,7 +274,6 @@ def create_ui(_blocks: gr.Blocks=None): btn_update = gr.Button('Update', interactive=True, visible=False, elem_id='control_update') 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]) 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), From d9d1d92791f04b673807c523f3843954dbbb6f0b Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 29 Apr 2026 08:36:10 +0200 Subject: [PATCH 008/138] remove control init video/batch/folder Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- html/locale_en.json | 6 ++---- javascript/control.js | 4 ++-- modules/ui_control.py | 8 +------- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 9c04701e8..4d92bce7e 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 9c04701e807f9b0102a79d124b9faa87d9b86453 +Subproject commit 4d92bce7e47276f85651478cb9619b5e6dd77d8b diff --git a/html/locale_en.json b/html/locale_en.json index 42ef8550e..d387c860a 100644 --- a/html/locale_en.json +++ b/html/locale_en.json @@ -710,7 +710,7 @@ {"id":"","label":"Image Paths","localized":"","hint":"Settings related to image filenames, and output directories"}, {"id":"","label":"Image Metadata","localized":"","hint":"Settings related to handling of metadata that is created with generated images"}, {"id":"","label":"IP Adapters","localized":"","hint":"IP adapters are plugin models that can guide generation towards desired outcome","ui":"txt2img"}, - {"id":"","label":"Input","localized":"","hint":"Add input image to be used for image-to-image, inpaint or control processing","ui":"control"}, + {"id":"","label":"Input","localized":"","hint":"Add input image to be used for image-to-image, inpaint or control processing
Click to minimize/maximize","ui":"control"}, {"id":"","label":"Input Image","localized":"","hint":"","ui":"caption"}, {"id":"","label":"IPEX","localized":"","hint":"","ui":"settings_backends"}, {"id":"","label":"Image Gallery","localized":"","hint":"","ui":"settings_saving-images"}, @@ -1005,7 +1005,7 @@ {"id":"","label":"Network parameters","localized":"","hint":""} ], "o": [ - {"id":"txt2img_results_mobile","label":"Output","localized":"","hint":"Show/hide selection of output media: generation resuls and live previews during generation process","ui":"txt2img"}, + {"id":"txt2img_results_mobile","label":"Output","localized":"","hint":"Generation resuls and live previews during generation process
Click to minimize/maximize","ui":"txt2img"}, {"id":"","label":"OpenCLiP","localized":"","hint":"Analyze image using CLiP model via OpenCLiP","ui":"caption"}, {"id":"","label":"ONNX","localized":"","hint":""}, {"id":"","label":"Override","localized":"","hint":"Override settings that can change server behavior and are typically applied from imported image metadata","ui":"txt2img"}, @@ -1073,7 +1073,6 @@ {"id":"","label":"Preset Block Merge","localized":"","hint":"","ui":"models_merge_tab"}, {"id":"","label":"Preview metadata","localized":"","hint":""}, {"id":"","label":"Prompt","localized":"","hint":"Describe image you want to generate","ui":"txt2img"}, - {"id":"","label":"Processed Preview","localized":"","hint":"Show/hide section from pre-processing of input images before actual generate","ui":"control"}, {"id":"","label":"PixelArt","localized":"","hint":"","ui":"extras"}, {"id":"","label":"PAG: Perturbed attention guidance","localized":"","hint":"","ui":"settings_advanced"}, {"id":"","label":"PAB: Pyramid attention broadcast","localized":"","hint":"","ui":"settings_advanced"}, @@ -1106,7 +1105,6 @@ {"id":"","label":"Processor","localized":"","hint":"Processor type to use to preprocess image used for ControlNet","ui":"control"}, {"id":"","label":"Pose confidence","localized":"","hint":"","ui":"control"}, {"id":"","label":"Parameter free","localized":"","hint":"","ui":"control"}, - {"id":"","label":"Processed","localized":"","hint":"Show/hide section with processed images","ui":"control"}, {"id":"","label":"Postprocess mask","localized":"","hint":"","ui":"extras"}, {"id":"","label":"PixelArt block size","localized":"","hint":"","ui":"extras"}, {"id":"","label":"PixelArt sharpen","localized":"","hint":"","ui":"extras"}, diff --git a/javascript/control.js b/javascript/control.js index 06da73910..2dacbde06 100644 --- a/javascript/control.js +++ b/javascript/control.js @@ -10,8 +10,8 @@ function controlInputMode(inputMode, ...args) { log('controlInputMode', { mode: inputMode, tab: inputTab, kanvas: typeof Kanvas }); if ((inputTab === 'Image') && (typeof 'Kanvas' !== 'undefined')) { inputTab = 'Kanvas'; - const imageData = window.kanvas.getImage(); - args[0] = imageData; + args[0] = window.kanvas.getImage(); + args[1] = window.kanvas.getImage(); // TODO support separate control } return [inputTab, ...args]; } diff --git a/modules/ui_control.py b/modules/ui_control.py index 56fb39599..3ba7f08bb 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -241,12 +241,6 @@ def create_ui(_blocks: gr.Blocks=None): with gr.Tabs(elem_classes=['control-tabs'], elem_id='control-tab-init'): with gr.Tab('Image', id='init-image') as tab_image_init: init_image = gr.Image(label="Input", show_label=False, type="pil", interactive=True, tool="editor", height=gr_height, elem_classes=['control-image']) - with gr.Tab('Video', id='init-video') as tab_video_init: - init_video = gr.Video(label="Input", show_label=False, interactive=True, height=gr_height, elem_classes=['control-image']) - with gr.Tab('Batch', id='init-batch') as tab_batch_init: - init_batch = gr.File(label="Input", show_label=False, file_count='multiple', file_types=['image'], interactive=True, height=gr_height, elem_classes=['control-image']) - with gr.Tab('Folder', id='init-folder') as tab_folder_init: - init_folder = gr.File(label="Input", show_label=False, file_count='directory', file_types=['image'], interactive=True, height=gr_height, elem_classes=['control-image']) with gr.Column(scale=9, elem_id='control-output-column', visible=True) as _column_output: gr.HTML('Output

') with gr.Tabs(elem_classes=['control-tabs'], elem_id='control-tab-output') as output_tabs: @@ -304,7 +298,7 @@ def create_ui(_blocks: gr.Blocks=None): prompt.submit(**select_dict) negative.submit(**select_dict) btn_generate.click(**select_dict) - for ctrl in [input_image, input_video, input_batch, input_folder, init_image, init_video, init_batch, init_folder, tab_image, tab_video, tab_batch, tab_folder, tab_image_init, tab_video_init, tab_batch_init, tab_folder_init]: + for ctrl in [input_image, input_video, input_batch, input_folder, init_image, tab_image, tab_video, tab_batch, tab_folder, tab_image_init]: if hasattr(ctrl, 'change'): ctrl.change(**select_dict) if hasattr(ctrl, 'clear'): From ebc875d22412582be7325616a3fa1a8b81ed47ca Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 29 Apr 2026 11:20:22 +0200 Subject: [PATCH 009/138] reorg control type Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 10 +++++++++- extensions-builtin/sdnext-kanvas | 2 +- extensions-builtin/sdnext-modernui | 2 +- modules/lora/lora_extract.py | 2 +- .../onnx_stable_diffusion_img2img_pipeline.py | 4 ++-- modules/ui_control.py | 8 +++----- modules/ui_models.py | 6 +++--- modules/ui_models_load.py | 2 +- 8 files changed, 21 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce7e8aa5a..56461699b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,17 @@ # Change Log for SD.Next +### TODO + +- *Separate init image* currently not functional + ## Update for 2026-04-29 +- **UI** + - all ui panels can be minimized/maximized by clicking on their header + state is preserved across sessions and can be used to hide rarely used panels and declutter the workspace - **Control** - - remove buttons: input/control/process + - remove buttons: *input/control/process* + - move params *control input type* to control menu section - remove "processed preview" from ui preprocessor output can still be generated by clicking preview button in in control unit and it will render into normal output area diff --git a/extensions-builtin/sdnext-kanvas b/extensions-builtin/sdnext-kanvas index dbea4ea19..ae5dc5ee3 160000 --- a/extensions-builtin/sdnext-kanvas +++ b/extensions-builtin/sdnext-kanvas @@ -1 +1 @@ -Subproject commit dbea4ea19e0605e2158bef607dc423f12261f0a8 +Subproject commit ae5dc5ee374189b1d2ca1038e93da164b11f8223 diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 4d92bce7e..146bd3643 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 4d92bce7e47276f85651478cb9619b5e6dd77d8b +Subproject commit 146bd36431d3c38e3938054366934be91cce0ba6 diff --git a/modules/lora/lora_extract.py b/modules/lora/lora_extract.py index e1536198a..19a55d209 100644 --- a/modules/lora/lora_extract.py +++ b/modules/lora/lora_extract.py @@ -238,7 +238,7 @@ def create_ui(): with gr.Tab(label="Extract LoRA"): with gr.Row(): - gr.HTML('

 Extract currently loaded LoRA(s)

') + gr.HTML('

 Extract currently loaded LoRA(s)

') with gr.Row(): loaded = gr.Textbox(placeholder="Press refresh to query loaded LoRA", label="Loaded LoRA", interactive=False) create_refresh_button(loaded, lambda: None, lambda: {'value': loaded_lora_str()}, "lora_extract_refresh") diff --git a/modules/onnx_impl/pipelines/onnx_stable_diffusion_img2img_pipeline.py b/modules/onnx_impl/pipelines/onnx_stable_diffusion_img2img_pipeline.py index 82c9740a8..87d6a746a 100644 --- a/modules/onnx_impl/pipelines/onnx_stable_diffusion_img2img_pipeline.py +++ b/modules/onnx_impl/pipelines/onnx_stable_diffusion_img2img_pipeline.py @@ -29,7 +29,7 @@ class OnnxStableDiffusionImg2ImgPipeline(diffusers.OnnxStableDiffusionImg2ImgPip feature_extractor: Any, requires_safety_checker: bool = True ): - super().__init__(vae_encoder, vae_decoder, text_encoder, tokenizer, unet, scheduler, safety_checker, feature_extractor, requires_safety_checker) + super().__init__(vae_encoder, vae_decoder, text_encoder, tokenizer, unet, scheduler, safety_checker, feature_extractor, requires_safety_checker) # pylint: disable=too-many-function-args self.image_processor = VaeImageProcessor(vae_scale_factor=64) def __call__( @@ -72,7 +72,7 @@ class OnnxStableDiffusionImg2ImgPipeline(diffusers.OnnxStableDiffusionImg2ImgPip image = self.image_processor.preprocess(image).cpu().numpy() - # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2) + # `guidance_scale` is defined analog to the guidance weight `w` of equation (2) # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1` # corresponds to doing no classifier free guidance. do_classifier_free_guidance = guidance_scale > 1.0 diff --git a/modules/ui_control.py b/modules/ui_control.py index 3ba7f08bb..e34f3bf07 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -167,7 +167,7 @@ def create_ui(_blocks: gr.Blocks=None): with gr.Accordion(open=False, label="Input", elem_id="control_input", elem_classes=["small-accordion"]): with gr.Row(): - input_type = gr.Radio(label="Control input type", choices=['Control only', 'Init image same as control', 'Separate init image'], value='Control only', type='index', elem_id='control_input_type') + input_type = gr.Radio(label="Use init image", choices=['No: Control only', '1st: Same as control', '2nd: Separate image'], value='No: Control only', type='index', elem_id='control_input_type') with gr.Row(): denoising_strength = gr.Slider(minimum=0.00, maximum=0.99, step=0.01, label='Denoising strength', value=0.30, elem_id="control_input_denoising_strength") @@ -238,9 +238,7 @@ def create_ui(_blocks: gr.Blocks=None): input_folder = gr.File(label="Input", show_label=False, file_count='directory', file_types=['image'], interactive=True, height=gr_height) with gr.Column(scale=9, elem_id='control-init-column', visible=False) as column_init: gr.HTML('Init input

') - with gr.Tabs(elem_classes=['control-tabs'], elem_id='control-tab-init'): - with gr.Tab('Image', id='init-image') as tab_image_init: - init_image = gr.Image(label="Input", show_label=False, type="pil", interactive=True, tool="editor", height=gr_height, elem_classes=['control-image']) + init_image = gr.Image(label="Input", show_label=False, type="pil", interactive=True, tool="editor", height=gr_height, elem_classes=['control-image']) with gr.Column(scale=9, elem_id='control-output-column', visible=True) as _column_output: gr.HTML('Output

') with gr.Tabs(elem_classes=['control-tabs'], elem_id='control-tab-output') as output_tabs: @@ -298,7 +296,7 @@ def create_ui(_blocks: gr.Blocks=None): prompt.submit(**select_dict) negative.submit(**select_dict) btn_generate.click(**select_dict) - for ctrl in [input_image, input_video, input_batch, input_folder, init_image, tab_image, tab_video, tab_batch, tab_folder, tab_image_init]: + for ctrl in [input_image, input_video, input_batch, input_folder, init_image, tab_image, tab_video, tab_batch, tab_folder]: if hasattr(ctrl, 'change'): ctrl.change(**select_dict) if hasattr(ctrl, 'clear'): diff --git a/modules/ui_models.py b/modules/ui_models.py index c8a981261..f02bfb9b3 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -181,7 +181,7 @@ def create_ui(): return ['None'] + sd_models.checkpoint_titles() with gr.Row(): - gr.HTML('

 Merge multiple models

') + gr.HTML('

 Merge multiple models

') with gr.Row(equal_height=False): with gr.Column(variant='compact'): with gr.Row(): @@ -422,7 +422,7 @@ def create_ui(): with gr.Tab(label="Replace", elem_id="models_replace_tab"): with gr.Row(): - gr.HTML('

 Replace model components

') + gr.HTML('

 Replace model components

') with gr.Row(): with gr.Column(scale=3): model_type = gr.Dropdown(label="Base model type", choices=['sd15', 'sdxl', 'sd21', 'sd35', 'flux.1'], value='sdxl', interactive=False) @@ -628,7 +628,7 @@ def create_ui(): from modules.models_hf import hf_search, hf_select, hf_download_model, hf_update_token with gr.Column(scale=6): with gr.Row(): - gr.HTML('

 Download model from huggingface

') + gr.HTML('

 Download model from huggingface

') with gr.Row(): hf_search_text = gr.Textbox('', label='Search models', placeholder='search huggingface models') hf_search_btn = ToolButton(value=ui_symbols.search, interactive=True, elem_id="hf_text_search") diff --git a/modules/ui_models_load.py b/modules/ui_models_load.py index 9e6ca45cf..9246a10d6 100644 --- a/modules/ui_models_load.py +++ b/modules/ui_models_load.py @@ -276,7 +276,7 @@ def create_ui(gr_status, gr_file): return 'Save receipe not implemented yet' with gr.Row(): - gr.HTML('

 Custom model loader

') + gr.HTML('

 Custom model loader

') with gr.Row(): choices = list(shared_items.pipelines) choices = ['Current' if x.startswith('Custom') else x for x in choices] From 304918baec48167318f64e6db80277220d44284a Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 29 Apr 2026 19:53:14 +0200 Subject: [PATCH 010/138] monkey-patch multi-image Co-authored-by: Copilot Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 12 ++++----- extensions-builtin/sdnext-kanvas | 2 +- installer.py | 4 +-- javascript/control.js | 8 ++++-- modules/ui_control.py | 13 +++++---- modules/ui_control_helpers.py | 46 ++++++++++++++++++++------------ 6 files changed, 49 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56461699b..c17c8c085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,17 @@ # Change Log for SD.Next -### TODO - -- *Separate init image* currently not functional - ## Update for 2026-04-29 -- **UI** - - all ui panels can be minimized/maximized by clicking on their header - state is preserved across sessions and can be used to hide rarely used panels and declutter the workspace - **Control** - remove buttons: *input/control/process* - move params *control input type* to control menu section - remove "processed preview" from ui preprocessor output can still be generated by clicking preview button in in control unit and it will render into normal output area +- **UI** + - all ui panels can be minimized/maximized by clicking on their header + state is preserved across sessions and can be used to hide rarely used panels and declutter the workspace +- **Kanvas** + - re-order stages by clicking on active stage ## Update for 2026-04-28 diff --git a/extensions-builtin/sdnext-kanvas b/extensions-builtin/sdnext-kanvas index ae5dc5ee3..5c71a0144 160000 --- a/extensions-builtin/sdnext-kanvas +++ b/extensions-builtin/sdnext-kanvas @@ -1 +1 @@ -Subproject commit ae5dc5ee374189b1d2ca1038e93da164b11f8223 +Subproject commit 5c71a01449fe00fcc36525b2a10fe4b90b86731e diff --git a/installer.py b/installer.py index b9cf4ff0c..48998f651 100644 --- a/installer.py +++ b/installer.py @@ -1530,7 +1530,7 @@ def check_version(reset=True): # pylint: disable=unused-argument api_base = f'https://api.github.com/repos/{url_parts}' else: api_base = 'https://api.github.com/repos/vladmandic/sdnext' - branches = requests.get(f'{api_base}/branches', timeout=10).json() + branches = requests.get(f'{api_base}/branches', timeout=5).json() branch_names = [b['name'] for b in branches if 'name' in b] log.trace(f'Repository branches: active={branch_name} available={branch_names}') except Exception as e: @@ -1541,7 +1541,7 @@ def check_version(reset=True): # pylint: disable=unused-argument ts('latest', t_start) return try: - commits = requests.get(f'{api_base}/branches/{branch_name}', timeout=10).json() + commits = requests.get(f'{api_base}/branches/{branch_name}', timeout=5).json() latest = commits['commit']['sha'] if len(latest) != 40: log.error(f'Repository error: commit={latest} invalid') diff --git a/javascript/control.js b/javascript/control.js index 2dacbde06..1505a6b8d 100644 --- a/javascript/control.js +++ b/javascript/control.js @@ -8,11 +8,15 @@ function controlInputMode(inputMode, ...args) { const tabNames = ['Image', 'Video', 'Batch', 'Folder']; let inputTab = tabNames[tabIdx] || 'Image'; log('controlInputMode', { mode: inputMode, tab: inputTab, kanvas: typeof Kanvas }); + + // if kanvas is available overwrite image inputs with kanvas images if ((inputTab === 'Image') && (typeof 'Kanvas' !== 'undefined')) { inputTab = 'Kanvas'; - args[0] = window.kanvas.getImage(); - args[1] = window.kanvas.getImage(); // TODO support separate control + for (let i = 0; i < window.kanvas.stages.maxStages; i++) { + args[4 + i] = window.kanvas.getImage(1 + i, false, false); + } } + return [inputTab, ...args]; } diff --git a/modules/ui_control.py b/modules/ui_control.py index e34f3bf07..abdda0fec 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -238,7 +238,10 @@ def create_ui(_blocks: gr.Blocks=None): input_folder = gr.File(label="Input", show_label=False, file_count='directory', file_types=['image'], interactive=True, height=gr_height) with gr.Column(scale=9, elem_id='control-init-column', visible=False) as column_init: gr.HTML('Init input

') - init_image = gr.Image(label="Input", show_label=False, type="pil", interactive=True, tool="editor", height=gr_height, elem_classes=['control-image']) + if (installer.version['kanvas'] == 'disabled') or (installer.version['kanvas'] == 'unavailable'): + init_image = gr.Image(label="Input", show_label=False, type="pil", interactive=True, tool="editor", height=gr_height, elem_classes=['control-image']) + else: + init_image = gr.HTML(value='

Kanvas not initialized

', elem_id='kanvas-container') with gr.Column(scale=9, elem_id='control-output-column', visible=True) as _column_output: gr.HTML('Output

') with gr.Tabs(elem_classes=['control-tabs'], elem_id='control-tab-output') as output_tabs: @@ -255,11 +258,6 @@ def create_ui(_blocks: gr.Blocks=None): with gr.Row(elem_id="control_script_container"): input_script_args = scripts_manager.scripts_current.setup_ui(parent='control', accordion=True) - # handlers - # for btn in input_buttons: - # btn.click(fn=helpers.copy_input, inputs=[input_mode, btn, input_image, input_resize, input_inpaint], outputs=[input_image, input_resize, input_inpaint], _js='controlInputMode') - # btn.click(fn=helpers.transfer_input, inputs=[btn], outputs=[input_image, input_resize, input_inpaint] + input_buttons) - # hidden button to update gradio control values for u in units: controls.extend(u.controls) @@ -280,10 +278,11 @@ def create_ui(_blocks: gr.Blocks=None): show_progress = 'hidden', ) + image_inputs = 5 * [input_image, init_image] # need to repeat controls for kanvas and non-kanvas modes select_dict = dict( fn=helpers.select_input, _js="controlInputMode", - inputs=[input_mode, input_image, init_image, input_type, input_video, input_batch, input_folder], + inputs=[input_mode, input_type, input_video, input_batch, input_folder] + image_inputs, outputs=[output_tabs, result_txt, width_before, height_before], show_progress='hidden', queue=False, diff --git a/modules/ui_control_helpers.py b/modules/ui_control_helpers.py index a789ecad1..45e933f76 100644 --- a/modules/ui_control_helpers.py +++ b/modules/ui_control_helpers.py @@ -16,7 +16,6 @@ busy = False # used to synchronize select_input and generate_click input_source = None input_init = None input_mask = None -input_prev = None def initialize(): @@ -85,6 +84,8 @@ def get_video(filepath: str): def process_kanvas(x): # only used when kanvas overrides gr.Image object image = None mask = None + if x is None: + return image, mask try: # try base64 decode t0 = time.time() image_data = x.get('image', '') @@ -130,33 +131,35 @@ def process_kanvas(x): # only used when kanvas overrides gr.Image object return image, mask -def select_input(input_mode, input_image, init_image, init_type, input_video, input_batch, input_folder): - global busy, input_source, input_init, input_mask, input_prev # pylint: disable=global-statement +def select_input(input_mode, init_type, input_video, input_batch, input_folder, *args): + global busy, input_source, input_init, input_mask # pylint: disable=global-statement t0 = time.time() busy = False - selected_input = input_image # default: Image or Kanvas + selected_input = args[0] + init_image = args[1] if input_mode == 'Video': selected_input = input_video elif input_mode == 'Batch': selected_input = input_batch elif input_mode == 'Folder': selected_input = input_folder + elif input_mode == 'Kanvas': + pass # temp assignment only until we process kanvas inputs + else: + log.error(f'Input: type={input_mode} unrecognized') + selected_input = None size = [gr.update(), gr.update()] if selected_input is None: - # log.debug(f'Select input: image={selected_input}') input_source = None return [gr.Tabs.update(), ''] + size - elif selected_input == input_prev: - # log.debug(f'Select input: image={selected_input} no change') - return [gr.Tabs.update(), ''] + size - input_prev = selected_input busy = True input_type = type(selected_input) input_mask = None status = 'Control input | Unknown' res = [gr.Tabs.update(selected='out-gallery'), status] + # control inputs if isinstance(selected_input, Image.Image): # image via upload -> image if input_mode == 'Outpaint': @@ -164,23 +167,31 @@ def select_input(input_mode, input_image, init_image, init_type, input_video, in selected_input, input_mask = masking.outpaint(input_image=selected_input) input_source = [selected_input] input_type = 'PIL.Image' - status = f'Control input | Image | Size {selected_input.width if selected_input else 0}x{selected_input.height if selected_input else 0} | Mode {selected_input.mode if selected_input else "Unknown"}' + status = f'Input | Image | Size {selected_input.width if selected_input else 0}x{selected_input.height if selected_input else 0} | Mode {selected_input.mode if selected_input else "Unknown"}' size = [gr.update(value=selected_input.width), gr.update(value=selected_input.height)] res = [gr.Tabs.update(selected='out-gallery'), status] elif isinstance(selected_input, dict) and 'kanvas' in selected_input: # kanvas via js -> kanvas dict - selected_input, input_mask = process_kanvas(selected_input) - input_source = [selected_input] + input_source = [] + for i, selected in enumerate(args): + img, mask = process_kanvas(selected) + if img is not None: # use all images + input_source.append(img) + if i == 0: # use only first mask + selected_input = img + input_mask = mask + if i == 1: + init_image = img # control: separate init image input_type = 'Kanvas' - status = f'Control input | Kanvas | Size {selected_input.width if selected_input else 0}x{selected_input.height if selected_input else 0} | Mode {selected_input.mode if selected_input else "Unknown"}' - if selected_input: - size = [gr.update(value=selected_input.width), gr.update(value=selected_input.height)] + status = f'Input | Kanvas | Images {len(input_source)}' + if len(input_source) > 0: + size = [gr.update(value=input_source[0].width), gr.update(value=input_source[0].height)] res = [gr.Tabs.update(selected='out-gallery'), status] elif isinstance(selected_input, dict) and 'mask' in selected_input: # inpaint -> dict image+mask input_mask = selected_input['mask'] selected_input = selected_input['image'] input_source = [selected_input] input_type = 'PIL.Image' - status = f'Control input | Image | Size {selected_input.width if selected_input else 0}x{selected_input.height if selected_input else 0} | Mode {selected_input.mode if selected_input else "Unknown"}' + status = f'Input | Image | Size {selected_input.width if selected_input else 0}x{selected_input.height if selected_input else 0} | Mode {selected_input.mode if selected_input else "Unknown"}' res = [gr.Tabs.update(selected='out-gallery'), status] elif isinstance(selected_input, gr.components.image.Image): # not likely input_source = [selected_input.value] @@ -198,10 +209,11 @@ def select_input(input_mode, input_image, init_image, init_type, input_video, in else: input_type = 'files' input_source = selected_input - status = f'Control input | Images | Files {len(input_source)}' + status = f'Input | Images | Files {len(input_source)}' res = [gr.Tabs.update(selected='out-gallery'), status] else: # unknown input_source = None + if init_type == 0: # Control only input_init = None elif init_type == 1: # Init image same as control assigned during runtime From 44a13f9b63771de72674fa366eea42d52203c620 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 29 Apr 2026 20:38:23 +0200 Subject: [PATCH 011/138] nano banana prototype multi-image Co-authored-by: Copilot Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- modules/processing_args.py | 5 ++++- pipelines/model_google.py | 21 +++++++++++---------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 146bd3643..e282300b3 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 146bd36431d3c38e3938054366934be91cce0ba6 +Subproject commit e282300b3d109580f0b32da1dcdf18c7c8504186 diff --git a/modules/processing_args.py b/modules/processing_args.py index 6f3305e62..fc7ba7e43 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -156,7 +156,10 @@ def task_specific_kwargs(p, model): if ('WanVACEPipeline' in model_cls) and (p.init_images is not None) and (len(p.init_images) > 0): task_args['reference_images'] = p.init_images if ('GoogleNanoBananaPipeline' in model_cls) and (p.init_images is not None) and (len(p.init_images) > 0): - task_args['image'] = p.init_images[0] + if hasattr(p, 'orig_init_images') and (p.orig_init_images is not None) and len(p.orig_init_images) > 0: + task_args['images'] = p.orig_init_images + else: + task_args['images'] = p.init_images if ('GlmImagePipeline' in model_cls) and (p.init_images is not None) and (len(p.init_images) > 0): task_args['image'] = p.init_images if 'BlipDiffusionPipeline' in model_cls: diff --git a/pipelines/model_google.py b/pipelines/model_google.py index 91045fc53..62007cc6b 100644 --- a/pipelines/model_google.py +++ b/pipelines/model_google.py @@ -28,8 +28,6 @@ aspect_ratios_buckets = { def google_requirements(): from installer import install # , reload install('google-genai==1.52.0') - # install('pydantic==2.11.7', ignore=True, quiet=True) - # reload('pydantic', '2.11.7') def get_size_buckets(width: int, height: int) -> tuple[str, str]: @@ -55,15 +53,18 @@ class GoogleNanoBananaPipeline(): contents=prompt, ) - def img2img(self, prompt, image): + def img2img(self, prompt, images): from google import genai # pylint: disable=no-name-in-module - image_bytes = io.BytesIO() - image.save(image_bytes, format='JPEG') + image_bytes_list = [] + for image in images: + image_bytes = io.BytesIO() + image.save(image_bytes, format='JPEG') + image_bytes_list.append(genai.types.Part.from_bytes(data=image_bytes.getvalue(), mime_type='image/jpeg')) return self.client.models.generate_content( model=self.model, config=self.config, contents=[ - genai.types.Part.from_bytes(data=image_bytes.getvalue(), mime_type='image/jpeg'), + *image_bytes_list, prompt, ], ) @@ -108,7 +109,7 @@ class GoogleNanoBananaPipeline(): log.debug(f'Cloud: model="{self.model}" args={args_log}') return args - def __call__(self, prompt: list[str], width: int, height: int, image: Image.Image = None): + def __call__(self, prompt: list[str], width: int, height: int, images: list[Image.Image] = []): from google import genai # pylint: disable=no-name-in-module if self.client is None: args = self.get_args() @@ -125,13 +126,13 @@ class GoogleNanoBananaPipeline(): response_modalities=["IMAGE"], image_config=image_config ) - log.debug(f'Cloud: model="{self.model}" prompt="{prompt}" size={image_size} ar={aspect_ratio} image={image}') + log.debug(f'Cloud: model="{self.model}" prompt="{prompt}" size={image_size} ar={aspect_ratio} images={len(images) if images is not None else 0}') # log.debug(f'Cloud: config={self.config}') try: t0 = time.time() - if image is not None: - response = self.img2img(prompt, image) + if images is not None and len(images) > 0: + response = self.img2img(prompt, images) else: response = self.txt2img(prompt) t1 = time.time() From ae0cb1600cee5b6c076a00bcf6d16617b111ab4d Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Thu, 30 Apr 2026 03:42:11 +0100 Subject: [PATCH 012/138] fix(save): handle already-decoded images in save_intermediate --- modules/processing_helpers.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/processing_helpers.py b/modules/processing_helpers.py index 7e4b1a4c6..55e2ccb42 100644 --- a/modules/processing_helpers.py +++ b/modules/processing_helpers.py @@ -562,12 +562,17 @@ def apply_circular(enable: bool, model): def save_intermediate(p, latents, suffix): - for i in range(len(latents)): - from modules.processing import create_infotext - info=create_infotext(p, p.all_prompts, p.all_seeds, p.all_subseeds, [], iteration=p.iteration, position_in_batch=i) + from modules.processing import create_infotext + from modules.image import convert + is_latent = torch.is_tensor(latents) and latents.shape[-1] != 3 + if is_latent: decoded = processing_vae.vae_decode(latents=latents, model=shared.sd_model, output_type='pil', vae_type=p.vae_type, width=p.width, height=p.height) - for j in range(len(decoded)): - images.save_image(decoded[j], path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix=suffix) + else: + items = latents if isinstance(latents, list) else ([latents[j] for j in range(latents.shape[0])] if hasattr(latents, 'shape') else [latents]) + decoded = [convert.to_pil(img) if not hasattr(img, 'width') else img for img in items] + for i in range(len(decoded)): + info = create_infotext(p, p.all_prompts, p.all_seeds, p.all_subseeds, [], iteration=p.iteration, position_in_batch=i) + images.save_image(decoded[i], path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix=suffix) def update_sampler(p, sd_model, second_pass=False): From a5bf29edbed2b687f778bb31a4867f6bd5ba174e Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Thu, 30 Apr 2026 03:43:08 +0100 Subject: [PATCH 013/138] feat(anima): add image-to-image and inpainting support --- modules/modeldata.py | 2 +- modules/processing_args.py | 2 +- modules/processing_vae.py | 2 + pipelines/anima/anima_image.py | 168 +++++++++++++++++++++++++++++++++ pipelines/model_anima.py | 6 ++ 5 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 pipelines/anima/anima_image.py diff --git a/modules/modeldata.py b/modules/modeldata.py index 536c2380e..4fe64719c 100644 --- a/modules/modeldata.py +++ b/modules/modeldata.py @@ -72,7 +72,7 @@ def get_model_type(pipe): model_type = 'sana' elif "HiDream" in name: model_type = 'h1' - elif "AnimaTextToImage" in name: + elif name.startswith("Anima") and "AnimateDiff" not in name: model_type = 'anima' elif "Cosmos2TextToImage" in name: model_type = 'cosmos' diff --git a/modules/processing_args.py b/modules/processing_args.py index 6f3305e62..d279162df 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -272,7 +272,7 @@ def set_pipeline_args(p, model, prompts:list, negative_prompts:list, prompts_2:l kwargs['output_type'] = 'np' # only set latent if model has vae # model specific - if 'Kandinsky' in model.__class__.__name__ or 'Cosmos2' in model.__class__.__name__ or 'Anima' in model.__class__.__name__ or 'OmniGen2' in model.__class__.__name__: + if 'Kandinsky' in model.__class__.__name__ or 'Cosmos2' in model.__class__.__name__ or 'OmniGen2' in model.__class__.__name__: kwargs['output_type'] = 'np' # only set latent if model has vae if 'StableCascade' in model.__class__.__name__: kwargs.pop("guidance_scale") # remove diff --git a/modules/processing_vae.py b/modules/processing_vae.py index d35a1d15b..f1f0747b3 100644 --- a/modules/processing_vae.py +++ b/modules/processing_vae.py @@ -247,6 +247,8 @@ def vae_postprocess(tensor, model, output_type='np'): if tensor.ndim == 6 and tensor.shape[1] == 1: tensor = tensor.squeeze(0) images = model.video_processor.postprocess_video(tensor, output_type='pil') + if isinstance(images, list) and len(images) > 0 and isinstance(images[0], list): + images = [frame for batch in images for frame in batch] elif hasattr(model, 'image_processor'): if tensor.ndim == 5 and tensor.shape[1] == 3: # Qwen Image tensor = tensor[:, :, 0] diff --git a/pipelines/anima/anima_image.py b/pipelines/anima/anima_image.py new file mode 100644 index 000000000..5d0334181 --- /dev/null +++ b/pipelines/anima/anima_image.py @@ -0,0 +1,168 @@ +"""Anima img2img and inpainting pipelines (built dynamically from the runtime-imported base class).""" + +from typing import Callable, Dict, List, Optional, Union + +import torch +import torch.nn.functional as F +from PIL import Image +from diffusers.callbacks import MultiPipelineCallbacks, PipelineCallback +from diffusers.image_processor import PipelineImageInput +from diffusers.utils.torch_utils import randn_tensor + +from modules import devices + + +def _encode_image(pipe, image, dtype, device, generator): + """VAE-encode an image and normalize to denoiser latent space.""" + if isinstance(image, list): + image = image[0] + image_tensor = pipe.video_processor.preprocess(image, None, None) + image_tensor = image_tensor.squeeze(0).to(device=device, dtype=pipe.vae.dtype) + image_tensor = image_tensor[None, :, None, :, :] + init_latents = pipe.vae.encode(image_tensor).latent_dist.sample(generator) + latents_mean = torch.tensor(pipe.vae.config.latents_mean, device=device, dtype=torch.float32).view(1, pipe.vae.config.z_dim, 1, 1, 1) + latents_std_inv = (1.0 / torch.tensor(pipe.vae.config.latents_std, device=device, dtype=torch.float32)).view(1, pipe.vae.config.z_dim, 1, 1, 1) + return ((init_latents.float() - latents_mean) * latents_std_inv).to(dtype) + + +def _setup_img2img_schedule(scheduler, strength, num_inference_steps, device): + """Set custom sigma schedule, return first sigma after scheduler shift.""" + custom_sigmas = torch.linspace(max(strength, 0.01), 0.0, num_inference_steps).tolist() + scheduler.set_timesteps(sigmas=custom_sigmas, device=device) + return scheduler.sigmas[0].item() + + +def build_anima_pipeline_classes(base_cls): + """Return (AnimaImageToImagePipeline, AnimaInpaintPipeline) inheriting from base_cls.""" + + class AnimaImageToImagePipeline(base_cls): + """Anima img2img pipeline.""" + + @torch.no_grad() + def __call__( + self, + prompt: Optional[Union[str, List[str]]] = None, + negative_prompt: Optional[Union[str, List[str]]] = None, + image: Optional[PipelineImageInput] = None, + strength: float = 0.8, + height: int = 768, + width: int = 1360, + num_inference_steps: int = 35, + guidance_scale: float = 7.0, + num_images_per_prompt: Optional[int] = 1, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.Tensor] = None, + prompt_embeds: Optional[torch.Tensor] = None, + negative_prompt_embeds: Optional[torch.Tensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback_on_step_end: Optional[Union[Callable[[int, int, Dict], None], PipelineCallback, MultiPipelineCallbacks]] = None, + callback_on_step_end_tensor_inputs: List[str] = ["latents"], + max_sequence_length: int = 512, + ): + actual_sigma = _setup_img2img_schedule(self.scheduler, strength, num_inference_steps, devices.device) + init_latents = _encode_image(self, image, devices.dtype, devices.device, generator) + noise = randn_tensor(init_latents.shape, generator=generator, device=devices.device, dtype=devices.dtype) + noised = (actual_sigma * noise + (1.0 - actual_sigma) * init_latents).to(torch.float32) + + orig_set_timesteps = self.scheduler.set_timesteps + self.scheduler.set_timesteps = lambda *args, **kwargs: None + try: + return super().__call__( + prompt=prompt, negative_prompt=negative_prompt, height=height, width=width, + num_inference_steps=num_inference_steps, guidance_scale=guidance_scale, + num_images_per_prompt=num_images_per_prompt, generator=generator, latents=noised, + prompt_embeds=prompt_embeds, negative_prompt_embeds=negative_prompt_embeds, + output_type=output_type, return_dict=return_dict, + callback_on_step_end=callback_on_step_end, callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs, + max_sequence_length=max_sequence_length, + ) + finally: + self.scheduler.set_timesteps = orig_set_timesteps + + class AnimaInpaintPipeline(AnimaImageToImagePipeline): + """Anima inpainting pipeline.""" + + @torch.no_grad() + def __call__( + self, + prompt: Optional[Union[str, List[str]]] = None, + negative_prompt: Optional[Union[str, List[str]]] = None, + image: Optional[PipelineImageInput] = None, + mask_image: Optional[PipelineImageInput] = None, + strength: float = 0.8, + height: int = 768, + width: int = 1360, + num_inference_steps: int = 35, + guidance_scale: float = 7.0, + num_images_per_prompt: Optional[int] = 1, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + latents: Optional[torch.Tensor] = None, + prompt_embeds: Optional[torch.Tensor] = None, + negative_prompt_embeds: Optional[torch.Tensor] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + callback_on_step_end: Optional[Union[Callable[[int, int, Dict], None], PipelineCallback, MultiPipelineCallbacks]] = None, + callback_on_step_end_tensor_inputs: List[str] = ["latents"], + max_sequence_length: int = 512, + ): + actual_sigma = _setup_img2img_schedule(self.scheduler, strength, num_inference_steps, devices.device) + init_latents = _encode_image(self, image, devices.dtype, devices.device, generator) + noise = randn_tensor(init_latents.shape, generator=generator, device=devices.device, dtype=devices.dtype) + noised = (actual_sigma * noise + (1.0 - actual_sigma) * init_latents).to(torch.float32) + mask_latent = _prepare_mask(self, mask_image, height, width, devices.device) + + orig_set_timesteps = self.scheduler.set_timesteps + self.scheduler.set_timesteps = lambda *args, **kwargs: None + + user_callback = callback_on_step_end + + def blend_callback(pipe, i, t, callback_kwargs): + cur_latents = callback_kwargs.get("latents") + if cur_latents is not None: + sigma_next = pipe.scheduler.sigmas[i + 1].item() if i + 1 < len(pipe.scheduler.sigmas) else 0.0 + init_at_t = sigma_next * noise + (1.0 - sigma_next) * init_latents + blended = mask_latent * cur_latents + (1.0 - mask_latent) * init_at_t.to(cur_latents.dtype) + callback_kwargs["latents"] = blended + if user_callback is not None: + callback_kwargs = user_callback(pipe, i, t, callback_kwargs) + return callback_kwargs + + try: + return base_cls.__call__( + self, + prompt=prompt, negative_prompt=negative_prompt, + height=height, width=width, num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, num_images_per_prompt=num_images_per_prompt, + generator=generator, latents=noised, prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, output_type=output_type, + return_dict=return_dict, callback_on_step_end=blend_callback, + callback_on_step_end_tensor_inputs=["latents"], + max_sequence_length=max_sequence_length, + ) + finally: + self.scheduler.set_timesteps = orig_set_timesteps + + return AnimaImageToImagePipeline, AnimaInpaintPipeline + + +def _prepare_mask(pipe, mask_image, height, width, device): + if isinstance(mask_image, Image.Image): + mask_image = mask_image.convert("L") + if isinstance(mask_image, Image.Image): + import torchvision.transforms.functional as TF + mask_tensor = TF.to_tensor(mask_image).unsqueeze(0).to(device=device, dtype=torch.float32) + elif isinstance(mask_image, torch.Tensor): + mask_tensor = mask_image.to(device=device, dtype=torch.float32) + if mask_tensor.ndim == 2: + mask_tensor = mask_tensor.unsqueeze(0).unsqueeze(0) + elif mask_tensor.ndim == 3: + mask_tensor = mask_tensor.unsqueeze(0) + else: + mask_tensor = torch.ones(1, 1, height, width, device=device, dtype=torch.float32) + latent_h = height // pipe.vae_scale_factor_spatial + latent_w = width // pipe.vae_scale_factor_spatial + mask_latent = F.interpolate(mask_tensor, size=(latent_h, latent_w), mode="nearest") + mask_latent = mask_latent[:, :1, :, :] + mask_latent = mask_latent.unsqueeze(2) + return mask_latent diff --git a/pipelines/model_anima.py b/pipelines/model_anima.py index 2f3149d25..bd0fc1e7e 100644 --- a/pipelines/model_anima.py +++ b/pipelines/model_anima.py @@ -81,6 +81,12 @@ def load_anima(checkpoint_info, diffusers_load_config=None): AnimaTextToImagePipeline = pipeline_mod.AnimaTextToImagePipeline AnimaLLMAdapter = adapter_mod.AnimaLLMAdapter + from pipelines.anima.anima_image import build_anima_pipeline_classes + AnimaImageToImagePipeline, AnimaInpaintPipeline = build_anima_pipeline_classes(AnimaTextToImagePipeline) + diffusers.pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["anima"] = AnimaTextToImagePipeline + diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["anima"] = AnimaImageToImagePipeline + diffusers.pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["anima"] = AnimaInpaintPipeline + # UNET dropdown (shared.opts.sd_unet) may redirect the transformer to a # community file that bundles both the transformer and the llm_adapter. transformer, llm_adapter = load_transformer_components(repo_id, diffusers_load_config, AnimaLLMAdapter) From eaf06fbc74de85e4076555ce9574d1eda12900bb Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 30 Apr 2026 09:30:11 +0200 Subject: [PATCH 014/138] all direct input images Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 20 ++-- extensions-builtin/sdnext-kanvas | 2 +- extensions-builtin/sdnext-modernui | 2 +- modules/control/run.py | 151 ++++++++++++++++++----------- modules/processing_args.py | 5 +- modules/sd_models.py | 2 +- pipelines/model_google.py | 1 + 7 files changed, 113 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c17c8c085..03fe52caa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,25 @@ # Change Log for SD.Next -## Update for 2026-04-29 +## TODO +- Tag multi-image pipes with `use_images_direct` + +## Update for 2026-04-30 + +- **Features** + - **Multi-image** workflows! + for models that support multiple images as inputs, you can now add multiple stages in Kanvas + prompts like "*place character from first image, add background from second image, render in style from third image*" are now possible +- **UI** + - add button to manually reorient input/output panels + - all ui panels can be minimized/maximized by clicking on their header + state is preserved across sessions and can be used to hide rarely used panels and declutter the workspace + - **Kanvas** re-order stages by clicking on active stage - **Control** - remove buttons: *input/control/process* - move params *control input type* to control menu section - remove "processed preview" from ui preprocessor output can still be generated by clicking preview button in in control unit and it will render into normal output area -- **UI** - - all ui panels can be minimized/maximized by clicking on their header - state is preserved across sessions and can be used to hide rarely used panels and declutter the workspace -- **Kanvas** - - re-order stages by clicking on active stage ## Update for 2026-04-28 diff --git a/extensions-builtin/sdnext-kanvas b/extensions-builtin/sdnext-kanvas index 5c71a0144..68a9564f7 160000 --- a/extensions-builtin/sdnext-kanvas +++ b/extensions-builtin/sdnext-kanvas @@ -1 +1 @@ -Subproject commit 5c71a01449fe00fcc36525b2a10fe4b90b86731e +Subproject commit 68a9564f7c9bfd96993341217ff18a1266ff8960 diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index e282300b3..2c1dc64a0 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit e282300b3d109580f0b32da1dcdf18c7c8504186 +Subproject commit 2c1dc64a085cae33cfd67e7406fb07f564dd27d2 diff --git a/modules/control/run.py b/modules/control/run.py index c201367f5..78e23272b 100644 --- a/modules/control/run.py +++ b/modules/control/run.py @@ -274,6 +274,68 @@ def init_units(units: list[unit.Unit]): u.process.override = u.override +def control_process(p: StableDiffusionProcessingControl, + input_script_args: list | None = None, + override_script_name: str | None = None, + override_script_args: list | None = None, + input_image: Image.Image = None, # only used for tiling, otherwise processor.preprocess_image set p params + ): + debug_log(f'Control exec pipeline: task={sd_models.get_diffusers_task(pipe)} class={pipe.__class__}') + if sd_models.get_diffusers_task(pipe) != sd_models.DiffusersTaskType.TEXT_2_IMAGE: # force vae back to gpu if not in txt2img mode + sd_models.move_model(pipe.vae, devices.device) + + # what are we doing? + if 'control' in p.ops: + p.outpath_samples = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_control_samples) + elif 'img2img' in p.ops: + p.outpath_samples = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_img2img_samples) + elif 'txt2img' in p.ops: + p.outpath_samples = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_txt2img_samples) + else: # fallback to txt2img + p.outpath_samples = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_txt2img_samples) + + # init scripts + p.scripts = scripts_manager.scripts_control + p.script_args = input_script_args or [] + if len(p.script_args) == 0: + if not p.scripts: + p.scripts.initialize_scripts(False) + p.script_args = script.init_default_script_args(p.scripts) + + # init override scripts + if override_script_name and override_script_args and len(override_script_name) > 0: + selectable_scripts, selectable_script_idx = script.get_selectable_script(override_script_name, p.scripts) + if selectable_scripts: + for idx in range(len(override_script_args)): + p.script_args[selectable_scripts.args_from + idx] = override_script_args[idx] + p.script_args[0] = selectable_script_idx + 1 + + # actual processing + script_run = False + processed: processing.Processed = None + if p.is_tile: + processed: processing.Processed = tile.run_tiling(p, input_image) + if processed is None and p.scripts is not None: + processed = p.scripts.run(p, *p.script_args) + if processed is None: + processed: processing.Processed = processing.process_images(p) # run actual pipeline + else: + script_run = True + + # postprocessing + if p.scripts is not None: + processed = p.scripts.after(p, processed, *p.script_args) + + output = None + info = None + if processed is not None and processed.images is not None: + output = processed.images + info = [processed.infotext(p, i) for i in range(len(output))] + + # output = pipe(**vars(p)).images # debug: direct pipe exec call + return output, info, script_run + + def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg units: list[unit.Unit] | None = None, inputs: list[Image.Image] | None = None, inits: list[Image.Image] | None = None, mask: Image.Image = None, unit_type: str | None = None, is_generator: bool = True, input_type: int = 0, @@ -633,17 +695,20 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg processed_image = None if frame is not None: inputs = [Image.fromarray(frame)] # cv2 to pil - for i, input_image in enumerate(inputs): - if input_image is not None: - p.ops.append('img2img') + for i, input_image in enumerate(inputs): # loop per-input, but with early-break if pipe is None: # pipe may have been reset externally if video is None: break # non-video: pipeline was consumed, no need to re-process remaining inputs pipe = set_pipe(p, has_models, unit_type, selected_models, active_model, active_strength, active_units, control_conditioning, control_guidance_start, control_guidance_end, inits) debug_log(f'Control pipeline reinit: class={pipe.__class__.__name__}') + pipe.restore_pipeline = restore_pipeline shared.sd_model.restore_pipeline = restore_pipeline debug_log(f'Control Control image: {i + 1} of {len(inputs)}') + + if input_image is not None: + p.ops.append('img2img') + if shared.state.skipped: shared.state.skipped = False continue @@ -652,6 +717,7 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg if is_generator: yield terminate('Interrupted') return terminate('Interrupted') + # get input if isinstance(input_image, str) and os.path.exists(input_image): try: @@ -664,7 +730,7 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg debug_log('Control Init image: same as control') init_image = input_image elif inits is None or len(inits) == 0: - debug_log('Control Init image: none') + debug_log('Control init image: none') init_image = None elif len(inits) > i and isinstance(inits[i], str): debug_log(f'Control: init image: {inits[i]}') @@ -683,7 +749,22 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg continue index += 1 - processed_image, blended_image = preprocess_image(p, pipe, input_image, init_image, mask, input_type, unit_type, active_process, active_model, selected_models, has_models, active_units) + if getattr(pipe, 'use_images_direct', False): + p.init_images = inputs + else: + processed_image, blended_image = preprocess_image(p, + pipe, + input_image, + init_image, + mask, + input_type, + unit_type, + active_process, + active_model, + selected_models, + has_models, + active_units, + ) if is_generator: yield (None, blended_image, '') # result is control_output, proces_output @@ -701,62 +782,18 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg if unit_type == 'lite': instance.apply(selected_models, processed_image, control_conditioning) - # what are we doing? - if 'control' in p.ops: - p.outpath_samples = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_control_samples) - elif 'img2img' in p.ops: - p.outpath_samples = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_img2img_samples) - elif 'txt2img' in p.ops: - p.outpath_samples = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_txt2img_samples) - else: # fallback to txt2img - p.outpath_samples = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_txt2img_samples) - # pipeline output = None script_run = False - if pipe is not None: # run new pipeline - debug_log(f'Control exec pipeline: task={sd_models.get_diffusers_task(pipe)} class={pipe.__class__}') - if sd_models.get_diffusers_task(pipe) != sd_models.DiffusersTaskType.TEXT_2_IMAGE: # force vae back to gpu if not in txt2img mode - sd_models.move_model(pipe.vae, devices.device) - - # init scripts - p.scripts = scripts_manager.scripts_control - p.script_args = input_script_args or [] - if len(p.script_args) == 0: - if not p.scripts: - p.scripts.initialize_scripts(False) - p.script_args = script.init_default_script_args(p.scripts) - - # init override scripts - if override_script_name and override_script_args and len(override_script_name) > 0: - selectable_scripts, selectable_script_idx = script.get_selectable_script(override_script_name, p.scripts) - if selectable_scripts: - for idx in range(len(override_script_args)): - p.script_args[selectable_scripts.args_from + idx] = override_script_args[idx] - p.script_args[0] = selectable_script_idx + 1 - - # actual processing - processed: processing.Processed = None - if p.is_tile: - processed: processing.Processed = tile.run_tiling(p, input_image) - if processed is None and p.scripts is not None: - processed = p.scripts.run(p, *p.script_args) - if processed is None: - processed: processing.Processed = processing.process_images(p) # run actual pipeline - else: - script_run = True - - # postprocessing - if p.scripts is not None: - processed = p.scripts.after(p, processed, *p.script_args) - output = None - if processed is not None and processed.images is not None: - output = processed.images - info_txt = [processed.infotext(p, i) for i in range(len(output))] - - # output = pipe(**vars(p)).images # alternative direct pipe exec call - else: # blend all processed images and return + if pipe is None: # blend all processed images and return output = processed_image + else: # run new pipeline + output, info_txt, script_run = control_process(p, + input_script_args, + override_script_name, + override_script_args, + input_image, + ) # outputs output = output or [] diff --git a/modules/processing_args.py b/modules/processing_args.py index fc7ba7e43..b0d0e0361 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -156,10 +156,7 @@ def task_specific_kwargs(p, model): if ('WanVACEPipeline' in model_cls) and (p.init_images is not None) and (len(p.init_images) > 0): task_args['reference_images'] = p.init_images if ('GoogleNanoBananaPipeline' in model_cls) and (p.init_images is not None) and (len(p.init_images) > 0): - if hasattr(p, 'orig_init_images') and (p.orig_init_images is not None) and len(p.orig_init_images) > 0: - task_args['images'] = p.orig_init_images - else: - task_args['images'] = p.init_images + task_args['images'] = p.init_images if ('GlmImagePipeline' in model_cls) and (p.init_images is not None) and (len(p.init_images) > 0): task_args['image'] = p.init_images if 'BlipDiffusionPipeline' in model_cls: diff --git a/modules/sd_models.py b/modules/sd_models.py index 296f32d5b..5e9e847d2 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -1290,7 +1290,7 @@ def set_diffuser_pipe(pipe, new_pipe_type): add_noise_pred_to_diffusers_callback(new_pipe.pipe) fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access - log.debug(f"Pipeline class change: original={cls} target={new_pipe.__class__.__name__} device={pipe.device} fn={fn}") # pylint: disable=protected-access + log.debug(f"Pipeline class change: source={cls} target={new_pipe.__class__.__name__} device={pipe.device} fn={fn}") # pylint: disable=protected-access if shared.opts.diffusers_offload_mode == 'none': move_model(new_pipe, pipe.device) diff --git a/pipelines/model_google.py b/pipelines/model_google.py index 62007cc6b..9bb062811 100644 --- a/pipelines/model_google.py +++ b/pipelines/model_google.py @@ -40,6 +40,7 @@ def get_size_buckets(width: int, height: int) -> tuple[str, str]: class GoogleNanoBananaPipeline(): def __init__(self, model_name: str): + self.use_images_direct = True self.model = model_name self.client = None self.config = None From 66a270dc16faef9d452a66ff79948b707f57c8c8 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 30 Apr 2026 09:32:10 +0200 Subject: [PATCH 015/138] cleanup Co-authored-by: Copilot Signed-off-by: Vladimir Mandic --- modules/control/run.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/control/run.py b/modules/control/run.py index 78e23272b..3a8aa83a6 100644 --- a/modules/control/run.py +++ b/modules/control/run.py @@ -749,7 +749,7 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg continue index += 1 - if getattr(pipe, 'use_images_direct', False): + if getattr(pipe, 'use_images_direct', False) or getattr(p, 'use_images_direct', False): p.init_images = inputs else: processed_image, blended_image = preprocess_image(p, @@ -765,8 +765,8 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg has_models, active_units, ) - if is_generator: - yield (None, blended_image, '') # result is control_output, proces_output + if is_generator: + yield (None, blended_image, '') # result is control_output, proces_output # final check if has_models: From 2a14d73fab8855e12a3f35a6af33b52a56625c05 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 30 Apr 2026 10:06:34 +0200 Subject: [PATCH 016/138] add skip processing option Co-authored-by: Copilot Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 9 ++++++++- modules/control/run.py | 11 ++++++++--- modules/processing_class.py | 2 ++ modules/ui_control.py | 3 ++- pipelines/model_google.py | 2 +- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fe52caa..ad6888ac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## TODO -- Tag multi-image pipes with `use_images_direct` +- Tag multi-image pipes with `use_images_direct` and allow UI override ## Update for 2026-04-30 @@ -10,16 +10,23 @@ - **Multi-image** workflows! for models that support multiple images as inputs, you can now add multiple stages in Kanvas prompts like "*place character from first image, add background from second image, render in style from third image*" are now possible + - **Anima** support for *img2img* and *inpaint* workflows + - option *inputs -> skip processing* to force images to passed to model as-is without any pre-processing - **UI** - add button to manually reorient input/output panels - all ui panels can be minimized/maximized by clicking on their header state is preserved across sessions and can be used to hide rarely used panels and declutter the workspace - **Kanvas** re-order stages by clicking on active stage + order of stages detemines order of images passed to model - **Control** - remove buttons: *input/control/process* - move params *control input type* to control menu section - remove "processed preview" from ui preprocessor output can still be generated by clicking preview button in in control unit and it will render into normal output area +- **Internal** + - refactor `pip` installer, thanks @awsr +- **Fixes** + - save handle already decoded images ## Update for 2026-04-28 diff --git a/modules/control/run.py b/modules/control/run.py index 3a8aa83a6..d3238675f 100644 --- a/modules/control/run.py +++ b/modules/control/run.py @@ -339,7 +339,8 @@ def control_process(p: StableDiffusionProcessingControl, def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg units: list[unit.Unit] | None = None, inputs: list[Image.Image] | None = None, inits: list[Image.Image] | None = None, mask: Image.Image = None, unit_type: str | None = None, is_generator: bool = True, input_type: int = 0, - prompt: str = '', negative_prompt: str = '', styles: list[str] | None = None, + prompt: str = '', negative_prompt: str = '', + styles: list[str] | None = None, steps: int = 20, sampler_index: int | None = None, seed: int = -1, subseed: int = -1, subseed_strength: float = 0, seed_resize_from_h: int = -1, seed_resize_from_w: int = -1, guidance_name: str = 'Default', guidance_scale: float = 6.0, guidance_rescale: float = 0.0, guidance_start: float = 0.0, guidance_stop: float = 1.0, @@ -358,7 +359,9 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg resize_mode_before: int = 0, resize_name_before: str = 'None', resize_context_before: str = 'None', width_before: int = 512, height_before: int = 512, scale_by_before: float = 1.0, selected_scale_tab_before: int = 0, resize_mode_after: int = 0, resize_name_after: str = 'None', resize_context_after: str = 'None', width_after: int = 0, height_after: int = 0, scale_by_after: float = 1.0, selected_scale_tab_after: int = 0, resize_mode_mask: int = 0, resize_name_mask: str = 'None', resize_context_mask: str = 'None', width_mask: int = 0, height_mask: int = 0, scale_by_mask: float = 1.0, selected_scale_tab_mask: int = 0, - denoising_strength: float = 0.3, batch_count: int = 1, batch_size: int = 1, + denoising_strength: float = 0.3, + skip_processing: bool = False, + batch_count: int = 1, batch_size: int = 1, enable_hr: bool = False, hr_sampler_index: int | None = None, hr_denoising_strength: float = 0.0, hr_resize_mode: int = 0, hr_resize_context: str = 'None', hr_upscaler: str | None = None, hr_force: bool = False, hr_second_pass_steps: int = 20, hr_scale: float = 1.0, hr_resize_x: int = 0, hr_resize_y: int = 0, refiner_steps: int = 5, refiner_start: float = 0.0, refiner_prompt: str = '', refiner_negative: str = '', video_skip_frames: int = 0, video_type: str = 'None', video_duration: float = 2.0, video_loop: bool = False, video_pad: int = 0, video_interpolate: int = 0, @@ -456,6 +459,7 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg seed_resize_from_h = seed_resize_from_h, seed_resize_from_w = seed_resize_from_w, denoising_strength = denoising_strength, + skip_processing = skip_processing, # modular guidance guidance_name = guidance_name, guidance_scale = guidance_scale, @@ -749,8 +753,9 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg continue index += 1 - if getattr(pipe, 'use_images_direct', False) or getattr(p, 'use_images_direct', False): + if getattr(pipe, 'skip_processing', False) or getattr(p, 'skip_processing', False): p.init_images = inputs + p.extra_generation_params['Process'] = False else: processed_image, blended_image = preprocess_image(p, pipe, diff --git a/modules/processing_class.py b/modules/processing_class.py index 9e1ed3e4f..ecbe47a92 100644 --- a/modules/processing_class.py +++ b/modules/processing_class.py @@ -276,6 +276,7 @@ class StableDiffusionProcessing: tome_ratio: float | None = None, todo_ratio: float | None = None, # overrides + skip_processing: bool = False, override_settings_restore_afterwards: bool = True, override_settings: dict[str, Any] | None = None, # metadata @@ -491,6 +492,7 @@ class StableDiffusionProcessing: self.scale_by_before = scale_by_before self.scale_by_after = scale_by_after self.scale_by_mask = scale_by_mask + self.skip_processing = skip_processing # special handled items if firstphase_width != 0 or firstphase_height != 0: diff --git a/modules/ui_control.py b/modules/ui_control.py index abdda0fec..12a4c9eb0 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -170,6 +170,7 @@ def create_ui(_blocks: gr.Blocks=None): input_type = gr.Radio(label="Use init image", choices=['No: Control only', '1st: Same as control', '2nd: Separate image'], value='No: Control only', type='index', elem_id='control_input_type') with gr.Row(): denoising_strength = gr.Slider(minimum=0.00, maximum=0.99, step=0.01, label='Denoising strength', value=0.30, elem_id="control_input_denoising_strength") + skip_processing = gr.Checkbox(label="Skip input processing", value=False, elem_id="control_input_skip_processing") with gr.Accordion(open=False, label="Size", elem_id="control_size", elem_classes=["small-accordion"]): with gr.Tabs(): @@ -318,7 +319,7 @@ def create_ui(_blocks: gr.Blocks=None): resize_mode_before, resize_name_before, resize_context_before, width_before, height_before, scale_by_before, selected_scale_tab_before, resize_mode_after, resize_name_after, resize_context_after, width_after, height_after, scale_by_after, selected_scale_tab_after, resize_mode_mask, resize_name_mask, resize_context_mask, width_mask, height_mask, scale_by_mask, selected_scale_tab_mask, - denoising_strength, batch_count, batch_size, + denoising_strength, skip_processing, batch_count, batch_size, enable_hr, hr_sampler_index, hr_denoising_strength, hr_resize_mode, hr_resize_context, hr_upscaler, hr_force, hr_second_pass_steps, hr_scale, hr_resize_x, hr_resize_y, refiner_steps, refiner_start, refiner_prompt, refiner_negative, video_skip_frames, video_type, video_duration, video_loop, video_pad, video_interpolate, diff --git a/pipelines/model_google.py b/pipelines/model_google.py index 9bb062811..40e52c5bd 100644 --- a/pipelines/model_google.py +++ b/pipelines/model_google.py @@ -40,7 +40,7 @@ def get_size_buckets(width: int, height: int) -> tuple[str, str]: class GoogleNanoBananaPipeline(): def __init__(self, model_name: str): - self.use_images_direct = True + self.skip_processing = True self.model = model_name self.client = None self.config = None From 0be4a2590fadf4275e746253b709513d1328c82b Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 30 Apr 2026 10:06:50 +0200 Subject: [PATCH 017/138] update modernui Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 2c1dc64a0..207579b26 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 2c1dc64a085cae33cfd67e7406fb07f564dd27d2 +Subproject commit 207579b26bcab04ca679e55a029d91fd5ecef08f From 417312ce7748e0e64d1aae07642b117ab96aabb3 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Thu, 30 Apr 2026 11:03:20 +0200 Subject: [PATCH 018/138] masking error handler Co-authored-by: Copilot Signed-off-by: Vladimir Mandic --- modules/masking.py | 10 ++++++++-- modules/processing_prompt.py | 2 +- modules/sd_hijack_hfhub.py | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/modules/masking.py b/modules/masking.py index 07dce8a2e..cc94348c4 100644 --- a/modules/masking.py +++ b/modules/masking.py @@ -385,7 +385,9 @@ def outpaint(input_image: Image.Image, outpaint_type: str = 'Edge'): def run_mask(input_image: Image.Image, input_mask: Image.Image | None = None, return_type: str | None = None, mask_blur: int | None = None, mask_padding: int | None = None, invert=None): - if isinstance(input_image, list) and len(input_image) > 0: + if input_image is None: + return input_mask + elif isinstance(input_image, list) and len(input_image) > 0: input_image = input_image[0] elif isinstance(input_image, dict): input_mask = input_image.get('mask', None) @@ -401,7 +403,11 @@ def run_mask(input_image: Image.Image, input_mask: Image.Image | None = None, re debug(f'Run mask: fn={fn}') # pylint: disable=protected-access debug(f'Run mask: opts={opts}') # pylint: disable=protected-access - size = min(input_image.width, input_image.height) + try: + size = min(input_image.width, input_image.height) + except Exception as e: + log.error(f'Mask input image error: {e}') + return input_mask if invert is not None: opts.invert = invert diff --git a/modules/processing_prompt.py b/modules/processing_prompt.py index 212a6cc58..32ad88ef1 100644 --- a/modules/processing_prompt.py +++ b/modules/processing_prompt.py @@ -152,7 +152,7 @@ def set_prompt(p, negative_attention_masks = prompt_parser_diffusers.embedder('negative_prompt_attention_masks') if negative_embeds is None: - log.warning('Prompt parser encode: empty negative prompt embeds') + # log.warning('Prompt parser encode: empty negative prompt embeds') prompt_parser_diffusers.embedder = None args = set_fallback_prompt(args, possible, prompts=None, negative_prompts=negative_prompts, prompts_2=None, negative_prompts_2=None) prompt_attention = 'fixed' diff --git a/modules/sd_hijack_hfhub.py b/modules/sd_hijack_hfhub.py index 46d0a58d9..17be5a172 100644 --- a/modules/sd_hijack_hfhub.py +++ b/modules/sd_hijack_hfhub.py @@ -16,7 +16,7 @@ def http_get_hijack(*args, **kwargs): fn = kwargs.get("displayed_filename", None) size = kwargs.get("expected_size", None) if fn and not fn.endswith(".json") and size is not None and size > 10240: - log.debug(f'Download start: type=http fn="{fn}" size={size}') + log.debug(f'Download: type=http fn="{fn}" size={size}') debug(f'Download start: type=http args={args} kwargs={kwargs}') t0 = time.time() res = orig_http_get(*args, **kwargs) @@ -34,7 +34,7 @@ def xet_get_hijack(*args, **kwargs): fn = kwargs.get("displayed_filename", None) size = kwargs.get("expected_size", None) if fn and not fn.endswith(".json"): - log.debug(f'Download start: type=xet fn="{fn}" size={size}') + log.debug(f'Download: type=xet fn="{fn}" size={size}') debug(f'Download start: type=xet args={args} kwargs={kwargs}') res = orig_xet_get(*args, **kwargs) debug(f'Download end: type=xet res={res}') From 450bf977e889f121eecc0bca78fec7deda2d9ad5 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Fri, 1 May 2026 01:38:56 +0100 Subject: [PATCH 019/138] fix(lora): use native_active flag instead of applied_layers for restore trigger applied_layers is cleared and re-populated on every network_activate call. With lora_apply_te=True the second activate (TE-only pass) finds all modules already at the target state and skips them all, leaving applied_layers empty and breaking the restore trigger on the next gen. native_active is set from loaded_networks at the end of activate, so it survives idempotent re-runs and only flips false after the restore call clears loaded_networks. --- modules/lora/lora_load.py | 3 +-- modules/lora/networks.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/lora/lora_load.py b/modules/lora/lora_load.py index f9cea425d..51fab0e2c 100644 --- a/modules/lora/lora_load.py +++ b/modules/lora/lora_load.py @@ -342,8 +342,7 @@ def network_load(names, te_multipliers=None, unet_multipliers=None, dyn_dims=Non # Also restore backed-up weights when previously active native modules are removed from modules.lora import networks native_nets = [net for net in l.loaded_networks if len(net.modules) > 0] - had_native = len(networks.applied_layers) > 0 - if native_nets or had_native: + if native_nets or networks.native_active: networks.network_activate() if len(l.loaded_networks) > 0 and l.debug: diff --git a/modules/lora/networks.py b/modules/lora/networks.py index ad95c887e..fe84f3b8a 100644 --- a/modules/lora/networks.py +++ b/modules/lora/networks.py @@ -9,6 +9,7 @@ from modules.logger import log, console applied_layers: list[str] = [] +native_active: bool = False default_components = ['text_encoder', 'text_encoder_2', 'text_encoder_3', 'text_encoder_4', 'unet', 'transformer', 'transformer_2', 'llm_adapter'] @@ -74,6 +75,8 @@ def network_activate(include=None, exclude=None): if task is not None and len(applied_layers) == 0: pbar.remove_task(task) # hide progress bar for no action + global native_active # pylint: disable=global-statement + native_active = len(l.loaded_networks) > 0 l.timer.activate += time.time() - t0 if l.debug and len(l.loaded_networks) > 0: log.debug(f'Network load: type=LoRA networks={[n.name for n in l.loaded_networks]} modules={active_components} layers={total} weights={applied_weight} bias={applied_bias} backup={round(backup_size/1024/1024/1024, 2)} fuse={shared.opts.lora_fuse_native}:{shared.opts.lora_fuse_diffusers} device={device} time={l.timer.summary}') From c855a5146af40ff2db1932c853c8a916508154d0 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 1 May 2026 16:34:38 +0200 Subject: [PATCH 020/138] cleanup Signed-off-by: Vladimir Mandic --- extensions-builtin/sdnext-modernui | 2 +- modules/masking.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 207579b26..5450b2702 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 207579b26bcab04ca679e55a029d91fd5ecef08f +Subproject commit 5450b270227cf03c38a6acd3d8f51a9f687137a4 diff --git a/modules/masking.py b/modules/masking.py index cc94348c4..fba38f731 100644 --- a/modules/masking.py +++ b/modules/masking.py @@ -406,7 +406,6 @@ def run_mask(input_image: Image.Image, input_mask: Image.Image | None = None, re try: size = min(input_image.width, input_image.height) except Exception as e: - log.error(f'Mask input image error: {e}') return input_mask if invert is not None: opts.invert = invert From 63190a50e7fd99d5e8e966ac2bd294dc76d881be Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 May 2026 08:06:57 +0200 Subject: [PATCH 021/138] fix ernie preview Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 4 +++- modules/api/validate.py | 1 + modules/processing_callbacks.py | 4 +++- modules/processing_diffusers.py | 4 ++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6888ac4..370e7bd4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Tag multi-image pipes with `use_images_direct` and allow UI override -## Update for 2026-04-30 +## Update for 2026-05-02 - **Features** - **Multi-image** workflows! @@ -27,6 +27,8 @@ - refactor `pip` installer, thanks @awsr - **Fixes** - save handle already decoded images + - ernie-image preview + - lora false deactivate ## Update for 2026-04-28 diff --git a/modules/api/validate.py b/modules/api/validate.py index 58952332c..1ff77be75 100644 --- a/modules/api/validate.py +++ b/modules/api/validate.py @@ -5,6 +5,7 @@ from modules.logger import log # value is cost: -1=disabled, 0=unlimited, 1=default, >1 expensive request_cost = { "/file": 0, + "/internal/progress": 0, "/run/predict": 0, "/sdapi/v1/browser/thumb": 0, "/sdapi/v1/network/thumb": 0, diff --git a/modules/processing_callbacks.py b/modules/processing_callbacks.py index e1c2c19ba..97bfc0fc4 100644 --- a/modules/processing_callbacks.py +++ b/modules/processing_callbacks.py @@ -139,7 +139,9 @@ def diffusers_callback(pipe, step: int = 0, timestep: int = 0, kwargs: dict | No width = getattr(p, 'width', 1024) height = getattr(p, 'height', 1024) latents = kwargs['latents'] - if len(latents.shape) == 3: # packed format [B, seq_len, patch_channels] + if len(latents.shape) == 4: + latents = pipe._unpatchify_latents(latents) # [B, C*4, h/2, w/2] -> [B, C, h, w] # pylint: disable=protected-access + elif len(latents.shape) == 3: # packed format [B, seq_len, patch_channels] b, seq_len, patch_ch = latents.shape channels = patch_ch // 4 # 4 = 2x2 patch h_patches = height // vae_scale // 2 diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index b7b1030ef..92427c836 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -242,7 +242,7 @@ def process_base(p: processing.StableDiffusionProcessing): def process_hires(p: processing.StableDiffusionProcessing, output): # optional second pass - if (output is None) or (output.images is None): + if (output is None) or not hasattr(output, 'images') or (output.images is None): return output if p.enable_hr: jobid = shared.state.begin('Hires') @@ -368,7 +368,7 @@ def process_hires(p: processing.StableDiffusionProcessing, output): def process_refine(p: processing.StableDiffusionProcessing, output): # optional refiner pass or decode - if (output is None) or (output.images is None): + if (output is None) or not hasattr(output, 'images') or (output.images is None): return output if is_refiner_enabled(p): if shared.opts.samples_save and not p.do_not_save_samples and shared.opts.save_images_before_refiner and hasattr(shared.sd_model, 'vae'): From e74a9a60e56cdc9260f9f40ba9c2ad93f4def1ff Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 May 2026 09:09:06 +0200 Subject: [PATCH 022/138] fix kandinsky detection Co-authored-by: Copilot Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 8 +++----- modules/processing_args.py | 9 +++------ modules/sd_detect.py | 2 +- modules/sd_models.py | 3 ++- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 370e7bd4c..89124bfc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,14 @@ # Change Log for SD.Next -## TODO - -- Tag multi-image pipes with `use_images_direct` and allow UI override - ## Update for 2026-05-02 - **Features** - **Multi-image** workflows! for models that support multiple images as inputs, you can now add multiple stages in Kanvas prompts like "*place character from first image, add background from second image, render in style from third image*" are now possible + - option *inputs -> skip processing* to force images to passed to model as-is without any pre-processing + examples of models that support multi-inputs: *qwen-image-edit, flux.2, google-gemini* - **Anima** support for *img2img* and *inpaint* workflows - - option *inputs -> skip processing* to force images to passed to model as-is without any pre-processing - **UI** - add button to manually reorient input/output panels - all ui panels can be minimized/maximized by clicking on their header @@ -29,6 +26,7 @@ - save handle already decoded images - ernie-image preview - lora false deactivate + - kandinsky-5 t2i/i2i workflows ## Update for 2026-04-28 diff --git a/modules/processing_args.py b/modules/processing_args.py index 635bb4f2a..c1d497d3a 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -129,14 +129,11 @@ def task_specific_kwargs(p, model): } # model specific args - if ('QwenImageEdit' in model_cls) and (p.init_images is None or len(p.init_images) == 0): - task_args['image'] = [Image.new('RGB', (p.width, p.height), (0, 0, 0))] # monkey-patch so qwen-image-edit pipeline does not error-out on t2i - if ('QwenImageEditPlusPipeline' in model_cls) and (p.init_control is not None) and (len(p.init_control) > 0): - task_args['image'] += p.init_control + if (('QwenImageEdit' in model_cls) or ('Kandinsky5I2IPipeline' in model_cls)) and (p.init_images is None or len(p.init_images) == 0): + log.debug(f'Model init: cls={model_cls} image=blank') + task_args['image'] = [Image.new('RGB', (p.width, p.height), (0, 0, 0))] # monkey-patch so i2i pipeline does not error-out on t2i if ('QwenImageLayeredPipeline' in model_cls) and (p.init_images is not None) and (len(p.init_images) > 0): task_args['image'] = p.init_images[0].convert('RGBA') - if ('Flux2' in model_cls) and (p.init_control is not None) and (len(p.init_control) > 0): - task_args['image'] += p.init_control if ('LatentConsistencyModelPipeline' in model_cls) and (len(p.init_images) > 0): p.ops.append('lcm') init_latents = [processing_vae.vae_encode(image, model=shared.sd_model, vae_type=p.vae_type).squeeze(dim=0) for image in p.init_images] diff --git a/modules/sd_detect.py b/modules/sd_detect.py index 6cbbc0ca4..5d8f612c6 100644 --- a/modules/sd_detect.py +++ b/modules/sd_detect.py @@ -126,7 +126,7 @@ def guess_by_name(fn, current_guess): new_guess = 'Kandinsky 2.2' elif 'kandinsky-3' in fn.lower(): new_guess = 'Kandinsky 3.0' - elif 'kandinsky-5.0' in fn.lower() and '2i' not in fn.lower(): + elif 'kandinsky-5.0' in fn.lower(): new_guess = 'Kandinsky 5.0' elif 'hunyuanimage3' in fn.lower() or 'hunyuanimage-3' in fn.lower(): new_guess = 'HunyuanImage3' diff --git a/modules/sd_models.py b/modules/sd_models.py index 5e9e847d2..108fce54c 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -51,6 +51,7 @@ pipe_switch_task_exclude = [ 'NucleusMoEImagePipeline', 'AuraFlowPipeline', 'ChronoEditPipeline', + 'Kandinsky5I2IPipeline', 'GoogleNanoBananaPipeline', ] i2i_pipes = [ @@ -482,7 +483,7 @@ def load_diffuser_force(detected_model_type, checkpoint_info, diffusers_load_con from pipelines.model_kandinsky import load_kandinsky3 sd_model = load_kandinsky3(checkpoint_info, diffusers_load_config) allow_post_quant = False - elif model_type in ['Kandinsky 5.0'] and model_type: + elif model_type in ['Kandinsky 5.0']: from pipelines.model_kandinsky import load_kandinsky5 sd_model = load_kandinsky5(checkpoint_info, diffusers_load_config) allow_post_quant = False From ee5978f72bc81c0246b474f72756fce2e07b974b Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 May 2026 10:22:43 +0200 Subject: [PATCH 023/138] custom vae loader Co-authored-by: Copilot Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 2 ++ modules/sd_vae.py | 39 ++++++++++++++++++++++------------ pipelines/generic.py | 30 ++++++++++++++++++++++++++ pipelines/model_anima.py | 3 +++ pipelines/model_auraflow.py | 2 ++ pipelines/model_chroma.py | 3 +++ pipelines/model_ernie.py | 2 ++ pipelines/model_flux.py | 2 ++ pipelines/model_flux2.py | 2 ++ pipelines/model_flux2_klein.py | 2 ++ pipelines/model_lumina.py | 5 +++++ pipelines/model_nucleus.py | 2 ++ pipelines/model_pixart.py | 2 ++ pipelines/model_qwen.py | 2 ++ pipelines/model_sd3.py | 2 ++ pipelines/model_z_image.py | 2 ++ 16 files changed, 89 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89124bfc8..6dae657b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - option *inputs -> skip processing* to force images to passed to model as-is without any pre-processing examples of models that support multi-inputs: *qwen-image-edit, flux.2, google-gemini* - **Anima** support for *img2img* and *inpaint* workflows + - custom **VAE** loader for all pipelines + *note*: vae still needs to be compatible with the model - **UI** - add button to manually reorient input/output panels - all ui panels can be minimized/maximized by clicking on their header diff --git a/modules/sd_vae.py b/modules/sd_vae.py index 8bad35232..92e7ca12a 100644 --- a/modules/sd_vae.py +++ b/modules/sd_vae.py @@ -169,25 +169,38 @@ def load_vae(model_file, vae_file=None, vae_source="unknown-source"): vae_config = sd_detect.get_load_config(model_file, model_type, config_type='json') if vae_config is not None: diffusers_load_config['config'] = os.path.join(vae_config, 'vae') - log.info(f'Load module: type=VAE model="{vae_file}" source={vae_source} config={diffusers_load_config}') try: import diffusers - if os.path.isfile(vae_file): + vae_class = None + vae_loader = None + if shared.sd_loaded and getattr(shared.sd_model, 'vae', None) is not None: + vae_class = shared.sd_model.vae.__class__ + vae_loader = vae_class.from_single_file if os.path.isfile(vae_file) else vae_class.from_pretrained + elif os.path.isfile(vae_file): if os.path.getsize(vae_file) > 1310944880: # 1.3GB - vae = diffusers.ConsistencyDecoderVAE.from_pretrained('openai/consistency-decoder', **diffusers_load_config) # consistency decoder does not have from single file, so we'll just download it once more + vae_class = diffusers.ConsistencyDecoderVAE + vae_loader = vae_class.from_pretrained + vae_file = 'openai/consistency-decoder' elif os.path.getsize(vae_file) < 10000000: # 10MB - vae = diffusers.AutoencoderTiny.from_single_file(vae_file, **diffusers_load_config) - else: - vae = diffusers.AutoencoderKL.from_single_file(vae_file, **diffusers_load_config) - if getattr(vae.config, 'scaling_factor', 0) == 0.18125 and shared.sd_model_type == 'sdxl': - vae.config.scaling_factor = 0.13025 - log.debug('Setting model: component=VAE fix scaling factor') - vae = vae.to(devices.dtype_vae) + vae_class = diffusers.AutoencoderTiny + vae_loader = vae_class.from_single_file + else: # fallback + vae_class = diffusers.AutoencoderKL + # if getattr(vae.config, 'scaling_factor', 0) == 0.18125 and shared.sd_model_type == 'sdxl': + # vae.config.scaling_factor = 0.13025 + # log.debug('Setting model: component=VAE fix scaling factor') + vae_loader = vae_class.from_single_file else: if 'consistency-decoder' in vae_file: - vae = diffusers.ConsistencyDecoderVAE.from_pretrained(vae_file, **diffusers_load_config) - else: - vae = diffusers.AutoencoderKL.from_pretrained(vae_file, **diffusers_load_config) + vae_class = diffusers.ConsistencyDecoderVAE + else: # fallback + vae_class = diffusers.AutoencoderKL + vae_loader = vae_class.from_pretrained + if vae_loader is not None: + log.info(f'Load module: type=VAE model="{vae_file}" source={vae_source} cls={vae_class.__name__} config={diffusers_load_config}') + vae = vae_loader(vae_file, **diffusers_load_config) + vae = vae.to(devices.dtype_vae) + global loaded_vae_file # pylint: disable=global-statement loaded_vae_file = os.path.basename(vae_file) # log.debug(f'Diffusers VAE config: {vae.config}') diff --git a/pipelines/generic.py b/pipelines/generic.py index 63196fe08..523d24ce3 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -250,3 +250,33 @@ def load_text_encoder(repo_id, cls_name, load_config=None, subfolder="text_encod devices.torch_gc() shared.state.end(jobid) return text_encoder + + +def load_vae_override(pipe, load_config=None, override_cls=None, override_args={}): + if (shared.opts.sd_vae in [None, 'None', 'Default', 'Automatic']): + return + if (pipe is None) or (getattr(pipe, 'vae', None) is None): + return + if load_config is None: + load_config = {} + + cls = override_cls or pipe.vae.__class__ + if not hasattr(cls, 'from_single_file'): + log.error(f'Load model: vae="{shared.opts.sd_vae}" cls={cls.__name__} safetensors=unsupported') + return + load_args, quant_args = model_quant.get_dit_args(load_config, module='VAE') + log.info(f'Load model: vae="{shared.opts.sd_vae}" cls={cls.__name__} args={load_args} quant={quant_args}') + try: + fn = os.path.join(shared.opts.vae_dir, shared.opts.sd_vae) + vae = cls.from_single_file( + fn, + cache_dir=shared.opts.hfcache_dir, + **override_args, + **load_args, + **quant_args, + ) + if vae is not None: + pipe.vae = vae + except Exception as e: + log.error(f'Load model: vae="{shared.opts.sd_vae}" cls={cls.__name__} {e}') + # errors.display(e, 'Load') diff --git a/pipelines/model_anima.py b/pipelines/model_anima.py index bd0fc1e7e..fa7b3b6c2 100644 --- a/pipelines/model_anima.py +++ b/pipelines/model_anima.py @@ -125,6 +125,9 @@ def load_anima(checkpoint_info, diffusers_load_config=None): **load_args, ) + # generic.load_vae_override(pipe, diffusers_load_config, override_cls=diffusers.AutoencoderKLQwenImage, override_args={'low_cpu_mem_usage': False, 'ignore_mismatched_sizes': True}) + generic.load_vae_override(pipe, diffusers_load_config) + del text_encoder del transformer del llm_adapter diff --git a/pipelines/model_auraflow.py b/pipelines/model_auraflow.py index 5d181ebce..597307c52 100644 --- a/pipelines/model_auraflow.py +++ b/pipelines/model_auraflow.py @@ -25,6 +25,8 @@ def load_auraflow(checkpoint_info, diffusers_load_config=None): **load_args, ) + generic.load_vae_override(pipe, diffusers_load_config) + del text_encoder del transformer sd_hijack_te.init_hijack(pipe) diff --git a/pipelines/model_chroma.py b/pipelines/model_chroma.py index 4472a1a85..1e7f9ea8a 100644 --- a/pipelines/model_chroma.py +++ b/pipelines/model_chroma.py @@ -28,6 +28,9 @@ def load_chroma(checkpoint_info, diffusers_load_config=None): diffusers.pipelines.auto_pipeline.AUTO_TEXT2IMAGE_PIPELINES_MAPPING["chroma"] = diffusers.ChromaPipeline diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["chroma"] = diffusers.ChromaImg2ImgPipeline diffusers.pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["chroma"] = diffusers.ChromaInpaintPipeline + + generic.load_vae_override(pipe, diffusers_load_config) + del text_encoder del transformer sd_hijack_te.init_hijack(pipe) diff --git a/pipelines/model_ernie.py b/pipelines/model_ernie.py index c5ca50691..d0eba17aa 100644 --- a/pipelines/model_ernie.py +++ b/pipelines/model_ernie.py @@ -40,6 +40,8 @@ def load_ernie_image(checkpoint_info, diffusers_load_config=None): 'use_pe': shared.opts.model_ernie_enable_pe, } + generic.load_vae_override(pipe, diffusers_load_config) + del transformer del text_encoder sd_hijack_te.init_hijack(pipe) diff --git a/pipelines/model_flux.py b/pipelines/model_flux.py index 23bd40455..17686e95e 100644 --- a/pipelines/model_flux.py +++ b/pipelines/model_flux.py @@ -60,6 +60,8 @@ def load_flux(checkpoint_info, diffusers_load_config=None): **load_args, ) + generic.load_vae_override(pipe, diffusers_load_config) + if os.environ.get('SD_REMOTE_T5', None) is not None: from modules import sd_te_remote log.warning('Remote-TE: applying patch') diff --git a/pipelines/model_flux2.py b/pipelines/model_flux2.py index dd6c364b0..a203730cb 100644 --- a/pipelines/model_flux2.py +++ b/pipelines/model_flux2.py @@ -31,6 +31,8 @@ def load_flux2(checkpoint_info, diffusers_load_config=None): diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["flux2"] = diffusers.Flux2Pipeline diffusers.pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["flux2"] = diffusers.Flux2Pipeline + generic.load_vae_override(pipe, diffusers_load_config) + from pipelines.flux import flux2_lora flux2_lora.apply_patch() diff --git a/pipelines/model_flux2_klein.py b/pipelines/model_flux2_klein.py index 31e539128..3a238805b 100644 --- a/pipelines/model_flux2_klein.py +++ b/pipelines/model_flux2_klein.py @@ -34,6 +34,8 @@ def load_flux2_klein(checkpoint_info, diffusers_load_config=None): diffusers.pipelines.auto_pipeline.AUTO_IMAGE2IMAGE_PIPELINES_MAPPING["flux2klein"] = diffusers.Flux2KleinPipeline diffusers.pipelines.auto_pipeline.AUTO_INPAINT_PIPELINES_MAPPING["flux2klein"] = diffusers.Flux2KleinPipeline + generic.load_vae_override(pipe, diffusers_load_config) + from pipelines.flux import flux2_lora flux2_lora.apply_patch() diff --git a/pipelines/model_lumina.py b/pipelines/model_lumina.py index c55d8281b..c5bc0f460 100644 --- a/pipelines/model_lumina.py +++ b/pipelines/model_lumina.py @@ -18,6 +18,9 @@ def load_lumina(checkpoint_info, diffusers_load_config=None): cache_dir = shared.opts.diffusers_dir, **load_config, ) + + generic.load_vae_override(pipe, diffusers_load_config) + sd_hijack_te.init_hijack(pipe) devices.torch_gc(force=True, reason='load') return pipe @@ -47,6 +50,8 @@ def load_lumina2(checkpoint_info, diffusers_load_config=None): **load_config, ) + generic.load_vae_override(pipe, diffusers_load_config) + del transformer del text_encoder sd_hijack_te.init_hijack(pipe) diff --git a/pipelines/model_nucleus.py b/pipelines/model_nucleus.py index 1dd4ca89b..b7087fa59 100644 --- a/pipelines/model_nucleus.py +++ b/pipelines/model_nucleus.py @@ -42,6 +42,8 @@ def load_nucleus(checkpoint_info, diffusers_load_config=None): 'output_type': 'np', } + generic.load_vae_override(pipe, diffusers_load_config) + del transformer del text_encoder del processor diff --git a/pipelines/model_pixart.py b/pipelines/model_pixart.py index 2f91ac895..1ced8b659 100644 --- a/pipelines/model_pixart.py +++ b/pipelines/model_pixart.py @@ -35,6 +35,8 @@ def load_pixart(checkpoint_info, diffusers_load_config=None): **load_args, ) + generic.load_vae_override(pipe, diffusers_load_config) + del text_encoder del transformer sd_hijack_te.init_hijack(pipe) diff --git a/pipelines/model_qwen.py b/pipelines/model_qwen.py index 36aa99888..52c9fd251 100644 --- a/pipelines/model_qwen.py +++ b/pipelines/model_qwen.py @@ -88,6 +88,8 @@ def load_qwen(checkpoint_info, diffusers_load_config=None): pipe.task_args['layers'] = shared.opts.model_qwen_layers pipe.task_args['resolution'] = 640 + generic.load_vae_override(pipe, diffusers_load_config) + del text_encoder del transformer sd_hijack_te.init_hijack(pipe) diff --git a/pipelines/model_sd3.py b/pipelines/model_sd3.py index c19cc1c11..5717dbc38 100644 --- a/pipelines/model_sd3.py +++ b/pipelines/model_sd3.py @@ -32,6 +32,8 @@ def load_sd3(checkpoint_info, diffusers_load_config=None): **load_args, ) + generic.load_vae_override(pipe, diffusers_load_config) + del text_encoder_3 del transformer sd_hijack_te.init_hijack(pipe) diff --git a/pipelines/model_z_image.py b/pipelines/model_z_image.py index 3081bb595..e619de619 100644 --- a/pipelines/model_z_image.py +++ b/pipelines/model_z_image.py @@ -52,6 +52,8 @@ def load_z_image(checkpoint_info, diffusers_load_config=None): **load_args, ) + generic.load_vae_override(pipe, diffusers_load_config) + del transformer del text_encoder sd_hijack_te.init_hijack(pipe) From 823058b15ac3390797aea551a0eaf44e4ae60dad Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 May 2026 10:41:01 +0200 Subject: [PATCH 024/138] add prompt-enhance info to metadata Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + scripts/prompt_enhance.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dae657b5..24d4dff92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - **Anima** support for *img2img* and *inpaint* workflows - custom **VAE** loader for all pipelines *note*: vae still needs to be compatible with the model + - add prompt enhance info to image metadata - **UI** - add button to manually reorient input/output panels - all ui panels can be minimized/maximized by clicking on their header diff --git a/scripts/prompt_enhance.py b/scripts/prompt_enhance.py index 764b978ac..d3f1cbf64 100644 --- a/scripts/prompt_enhance.py +++ b/scripts/prompt_enhance.py @@ -958,6 +958,8 @@ class PromptEnhanceScript(scripts_manager.Script): shared.prompt_styles.apply_styles_to_extra(p) p.styles = [] jobid = shared.state.begin('LLM') + p.extra_generation_params['LLM'] = get_model_repo_from_display(llm_model) + p.extra_generation_params['Original'] = p.prompt p.prompt = self.enhance( prompt=p.prompt, seed=p.seed, @@ -980,5 +982,4 @@ class PromptEnhanceScript(scripts_manager.Script): keep_thinking=keep_thinking, ) timer.process.record('prompt') - p.extra_generation_params['LLM'] = llm_model shared.state.end(jobid) From 57f5bf5bd5eb9be264073207bf50c4516c755895 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 2 May 2026 10:56:52 +0200 Subject: [PATCH 025/138] add missing js files Co-authored-by: Copilot Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 1 + javascript/jquery.js | 2 ++ javascript/sparkline.js | 5 +++++ javascript/timers.js | 10 ++++++---- scripts/prompt_enhance.py | 6 +++--- 5 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 javascript/jquery.js create mode 100644 javascript/sparkline.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 24d4dff92..eb435c62f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - **Internal** - refactor `pip` installer, thanks @awsr - **Fixes** + - add missing `jquery` and `sparkline` js scripts - save handle already decoded images - ernie-image preview - lora false deactivate diff --git a/javascript/jquery.js b/javascript/jquery.js new file mode 100644 index 000000000..448a06ef8 --- /dev/null +++ b/javascript/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},m=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||m).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),b=new RegExp(ge+"|>"),A=new RegExp(g),D=new RegExp("^"+t+"$"),N={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+d),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},L=/^(?:input|select|textarea|button)$/i,j=/^h\d$/i,O=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,P=/[+~]/,H=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),q=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},R=function(){V()},M=K(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{E.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){E={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(V(e),e=e||T,C)){if(11!==d&&(u=O.exec(t)))if(i=u[1]){if(9===d){if(!(a=e.getElementById(i)))return n;if(a.id===i)return E.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return E.call(n,a),n}else{if(u[2])return E.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return E.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||p&&p.test(t))){if(c=t,f=e,1===d&&(b.test(t)||m.test(t))){(f=P.test(t)&&X(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=k)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+G(l[o]);c=l.join(",")}try{return E.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>x.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function B(e){return e[k]=!0,e}function F(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function $(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&M(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function U(a){return B(function(o){return o=+o,B(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function X(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",R),le.getById=F(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=F(function(e){return i.call(e,"*")}),le.scope=F(function(){return T.querySelectorAll(":scope")}),le.cssHas=F(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(x.filter.ID=function(e){var t=e.replace(H,q);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(x.filter.ID=function(e){var n=e.replace(H,q);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),x.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},x.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},p=[],F(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||p.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+k+"-]").length||p.push("~="),e.querySelectorAll("a#"+k+"+*").length||p.push(".#.+[+~]"),e.querySelectorAll(":checked").length||p.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&p.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||p.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||p.push(":has"),p=p.length&&new RegExp(p.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!p||!p.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(H,q),e[3]=(e[3]||e[4]||e[5]||"").replace(H,q),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return N.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&A.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(H,q).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function C(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||E,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:k.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:m,!0)),T.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=m.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,E=ce(m);var S=/^(?:parents|prev(?:Until|All))/,A={children:!0,contents:!0,next:!0,prev:!0};function D(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Te=/^$|^module$|\/(?:java|ecma)script/i;re=m.createDocumentFragment().appendChild(m.createElement("div")),(be=m.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),re.appendChild(be),le.checkClone=re.cloneNode(!0).cloneNode(!0).lastChild.checked,re.innerHTML="",le.noCloneChecked=!!re.cloneNode(!0).lastChild.defaultValue,re.innerHTML="",le.option=!!re.lastChild;var Ce={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Ee(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function ke(e,t){for(var n=0,r=e.length;n",""]);var Se=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Me(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Ie(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function We(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n
",2===yt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=m.implementation.createHTMLDocument("")).createElement("base")).href=m.location.href,t.head.appendChild(r)):t=m),o=!n&&[],(i=T.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||K})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return R(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Qe(le.pixelPosition,function(e,t){if(t)return t=Ve(e,n),$e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return R(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0
{{prefix}}{{y}}{{suffix}}')},bar:{barColor:"#3366cc",negBarColor:"#f44",stackedBarColor:["#3366cc","#dc3912","#ff9900","#109618","#66aa00","#dd4477","#0099c6","#990099"],zeroColor:c,nullColor:c,zeroAxis:!0,barWidth:4,barSpacing:1,chartRangeMax:c,chartRangeMin:c,chartRangeClip:!1,colorMap:c,tooltipFormat:new h(' {{prefix}}{{value}}{{suffix}}')},tristate:{barWidth:4,barSpacing:1,posBarColor:"#6f6",negBarColor:"#f44",zeroBarColor:"#999",colorMap:{},tooltipFormat:new h(' {{value:map}}'),tooltipValueLookups:{map:{"-1":"Loss",0:"Draw",1:"Win"}}},discrete:{lineHeight:"auto",thresholdColor:c,thresholdValue:0,chartRangeMax:c,chartRangeMin:c,chartRangeClip:!1,tooltipFormat:new h("{{prefix}}{{value}}{{suffix}}")},bullet:{targetColor:"#f33",targetWidth:3,performanceColor:"#33f",rangeColors:["#d3dafe","#a8b6ff","#7f94ff"],base:c,tooltipFormat:new h("{{fieldkey:fields}} - {{value}}"),tooltipValueLookups:{fields:{r:"Range",p:"Performance",t:"Target"}}},pie:{offset:0,sliceColors:["#3366cc","#dc3912","#ff9900","#109618","#66aa00","#dd4477","#0099c6","#990099"],borderWidth:0,borderColor:"#000",tooltipFormat:new h(' {{value}} ({{percent.1}}%)')},box:{raw:!1,boxLineColor:"#000",boxFillColor:"#cdf",whiskerColor:"#000",outlierLineColor:"#333",outlierFillColor:"#fff",medianColor:"#f00",showOutliers:!0,outlierIQR:1.5,spotRadius:1.5,target:c,targetColor:"#4a2",chartRangeMax:c,chartRangeMin:c,tooltipFormat:new h("{{field:fields}}: {{value}}"),tooltipFormatFieldlistKey:"field",tooltipValueLookups:{fields:{lq:"Lower Quartile",med:"Median",uq:"Upper Quartile",lo:"Left Outlier",ro:"Right Outlier",lw:"Left Whisker",rw:"Right Whisker"}}}}},E='.jqstooltip { position: absolute;left: 0px;top: 0px;visibility: hidden;background: rgb(0, 0, 0) transparent;background-color: rgba(0,0,0,0.6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";color: white;font: 10px arial, san serif;text-align: left;white-space: nowrap;padding: 5px;border: 1px solid white;z-index: 10000;}.jqsfield { color: white;font: 10px arial, san serif;text-align: left;}',g=function(){var a,b;return a=function(){this.init.apply(this,arguments)},arguments.length>1?(arguments[0]?(a.prototype=d.extend(new arguments[0],arguments[arguments.length-1]),a._super=arguments[0].prototype):a.prototype=arguments[arguments.length-1],arguments.length>2&&(b=Array.prototype.slice.call(arguments,1,-1),b.unshift(a.prototype),d.extend.apply(d,b))):a.prototype=arguments[0],a.prototype.cls=a,a},d.SPFormatClass=h=g({fre:/\{\{([\w.]+?)(:(.+?))?\}\}/g,precre:/(\w+)\.(\d+)/,init:function(a,b){this.format=a,this.fclass=b},render:function(a,b,d){var e=this,f=a,g,h,i,j,k;return this.format.replace(this.fre,function(){var a;return h=arguments[1],i=arguments[3],g=e.precre.exec(h),g?(k=g[2],h=g[1]):k=!1,j=f[h],j===c?"":i&&b&&b[i]?(a=b[i],a.get?b[i].get(j)||j:b[i][j]||j):(n(j)&&(d.get("numberFormatter")?j=d.get("numberFormatter")(j):j=s(j,k,d.get("numberDigitGroupCount"),d.get("numberDigitGroupSep"),d.get("numberDecimalMark"))),j)})}}),d.spformat=function(a,b){return new h(a,b)},i=function(a,b,c){return ac?c:a},j=function(a,c){var d;return c===2?(d=b.floor(a.length/2),a.length%2?a[d]:(a[d-1]+a[d])/2):a.length%2?(d=(a.length*c+c)/4,d%1?(a[b.floor(d)]+a[b.floor(d)-1])/2:a[d-1]):(d=(a.length*c+2)/4,d%1?(a[b.floor(d)]+a[b.floor(d)-1])/2:a[d-1])},k=function(a){var b;switch(a){case"undefined":a=c;break;case"null":a=null;break;case"true":a=!0;break;case"false":a=!1;break;default:b=parseFloat(a),a==b&&(a=b)}return a},l=function(a){var b,c=[];for(b=a.length;b--;)c[b]=k(a[b]);return c},m=function(a,b){var c,d,e=[];for(c=0,d=a.length;c0;h-=c)a.splice(h,0,e);return a.join("")},o=function(a,b,c){var d;for(d=b.length;d--;){if(c&&b[d]===null)continue;if(b[d]!==a)return!1}return!0},p=function(a){var b=0,c;for(c=a.length;c--;)b+=typeof a[c]=="number"?a[c]:0;return b},r=function(a){return d.isArray(a)?a:[a]},q=function(b){var c;a.createStyleSheet?a.createStyleSheet().cssText=b:(c=a.createElement("style"),c.type="text/css",a.getElementsByTagName("head")[0].appendChild(c),c[typeof a.body.style.WebkitAppearance=="string"?"innerText":"innerHTML"]=b)},d.fn.simpledraw=function(b,e,f,g){var h,i;if(f&&(h=this.data("_jqs_vcanvas")))return h;if(d.fn.sparkline.canvas===!1)return!1;if(d.fn.sparkline.canvas===c){var j=a.createElement("canvas");if(!j.getContext||!j.getContext("2d")){if(!a.namespaces||!!a.namespaces.v)return d.fn.sparkline.canvas=!1,!1;a.namespaces.add("v","urn:schemas-microsoft-com:vml","#default#VML"),d.fn.sparkline.canvas=function(a,b,c,d){return new J(a,b,c)}}else d.fn.sparkline.canvas=function(a,b,c,d){return new I(a,b,c,d)}}return b===c&&(b=d(this).innerWidth()),e===c&&(e=d(this).innerHeight()),h=d.fn.sparkline.canvas(b,e,this,g),i=d(this).data("_jqs_mhandler"),i&&i.registerCanvas(h),h},d.fn.cleardraw=function(){var a=this.data("_jqs_vcanvas");a&&a.reset()},d.RangeMapClass=t=g({init:function(a){var b,c,d=[];for(b in a)a.hasOwnProperty(b)&&typeof b=="string"&&b.indexOf(":")>-1&&(c=b.split(":"),c[0]=c[0].length===0?-Infinity:parseFloat(c[0]),c[1]=c[1].length===0?Infinity:parseFloat(c[1]),c[2]=a[b],d.push(c));this.map=a,this.rangelist=d||!1},get:function(a){var b=this.rangelist,d,e,f;if((f=this.map[a])!==c)return f;if(b)for(d=b.length;d--;){e=b[d];if(e[0]<=a&&e[1]>=a)return e[2]}return c}}),d.range_map=function(a){return new t(a)},u=g({init:function(a,b){var c=d(a);this.$el=c,this.options=b,this.currentPageX=0,this.currentPageY=0,this.el=a,this.splist=[],this.tooltip=null,this.over=!1,this.displayTooltips=!b.get("disableTooltips"),this.highlightEnabled=!b.get("disableHighlight")},registerSparkline:function(a){this.splist.push(a),this.over&&this.updateDisplay()},registerCanvas:function(a){var b=d(a.canvas);this.canvas=a,this.$canvas=b,b.mouseenter(d.proxy(this.mouseenter,this)),b.mouseleave(d.proxy(this.mouseleave,this)),b.click(d.proxy(this.mouseclick,this))},reset:function(a){this.splist=[],this.tooltip&&a&&(this.tooltip.remove(),this.tooltip=c)},mouseclick:function(a){var b=d.Event("sparklineClick");b.originalEvent=a,b.sparklines=this.splist,this.$el.trigger(b)},mouseenter:function(b){d(a.body).unbind("mousemove.jqs"),d(a.body).bind("mousemove.jqs",d.proxy(this.mousemove,this)),this.over=!0,this.currentPageX=b.pageX,this.currentPageY=b.pageY,this.currentEl=b.target,!this.tooltip&&this.displayTooltips&&(this.tooltip=new v(this.options),this.tooltip.updatePosition(b.pageX,b.pageY)),this.updateDisplay()},mouseleave:function(){d(a.body).unbind("mousemove.jqs");var b=this.splist,c=b.length,e=!1,f,g;this.over=!1,this.currentEl=null,this.tooltip&&(this.tooltip.remove(),this.tooltip=null);for(g=0;g
{{prefix}}{{y}}{{suffix}}')},bar:{barColor:"#3366cc",negBarColor:"#f44",stackedBarColor:["#3366cc","#dc3912","#ff9900","#109618","#66aa00","#dd4477","#0099c6","#990099"],zeroColor:c,nullColor:c,zeroAxis:!0,barWidth:4,barSpacing:1,chartRangeMax:c,chartRangeMin:c,chartRangeClip:!1,colorMap:c,tooltipFormat:new h(' {{prefix}}{{value}}{{suffix}}')},tristate:{barWidth:4,barSpacing:1,posBarColor:"#6f6",negBarColor:"#f44",zeroBarColor:"#999",colorMap:{},tooltipFormat:new h(' {{value:map}}'),tooltipValueLookups:{map:{"-1":"Loss",0:"Draw",1:"Win"}}},discrete:{lineHeight:"auto",thresholdColor:c,thresholdValue:0,chartRangeMax:c,chartRangeMin:c,chartRangeClip:!1,tooltipFormat:new h("{{prefix}}{{value}}{{suffix}}")},bullet:{targetColor:"#f33",targetWidth:3,performanceColor:"#33f",rangeColors:["#d3dafe","#a8b6ff","#7f94ff"],base:c,tooltipFormat:new h("{{fieldkey:fields}} - {{value}}"),tooltipValueLookups:{fields:{r:"Range",p:"Performance",t:"Target"}}},pie:{offset:0,sliceColors:["#3366cc","#dc3912","#ff9900","#109618","#66aa00","#dd4477","#0099c6","#990099"],borderWidth:0,borderColor:"#000",tooltipFormat:new h(' {{value}} ({{percent.1}}%)')},box:{raw:!1,boxLineColor:"#000",boxFillColor:"#cdf",whiskerColor:"#000",outlierLineColor:"#333",outlierFillColor:"#fff",medianColor:"#f00",showOutliers:!0,outlierIQR:1.5,spotRadius:1.5,target:c,targetColor:"#4a2",chartRangeMax:c,chartRangeMin:c,tooltipFormat:new h("{{field:fields}}: {{value}}"),tooltipFormatFieldlistKey:"field",tooltipValueLookups:{fields:{lq:"Lower Quartile",med:"Median",uq:"Upper Quartile",lo:"Left Outlier",ro:"Right Outlier",lw:"Left Whisker",rw:"Right Whisker"}}}}},E='.jqstooltip { position: absolute;left: 0px;top: 0px;visibility: hidden;background: rgb(0, 0, 0) transparent;background-color: rgba(0,0,0,0.6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";color: white;font: 10px arial, san serif;text-align: left;white-space: nowrap;padding: 5px;border: 1px solid white;z-index: 10000;}.jqsfield { color: white;font: 10px arial, san serif;text-align: left;}',g=function(){var a,b;return a=function(){this.init.apply(this,arguments)},arguments.length>1?(arguments[0]?(a.prototype=d.extend(new arguments[0],arguments[arguments.length-1]),a._super=arguments[0].prototype):a.prototype=arguments[arguments.length-1],arguments.length>2&&(b=Array.prototype.slice.call(arguments,1,-1),b.unshift(a.prototype),d.extend.apply(d,b))):a.prototype=arguments[0],a.prototype.cls=a,a},d.SPFormatClass=h=g({fre:/\{\{([\w.]+?)(:(.+?))?\}\}/g,precre:/(\w+)\.(\d+)/,init:function(a,b){this.format=a,this.fclass=b},render:function(a,b,d){var e=this,f=a,g,h,i,j,k;return this.format.replace(this.fre,function(){var a;return h=arguments[1],i=arguments[3],g=e.precre.exec(h),g?(k=g[2],h=g[1]):k=!1,j=f[h],j===c?"":i&&b&&b[i]?(a=b[i],a.get?b[i].get(j)||j:b[i][j]||j):(n(j)&&(d.get("numberFormatter")?j=d.get("numberFormatter")(j):j=s(j,k,d.get("numberDigitGroupCount"),d.get("numberDigitGroupSep"),d.get("numberDecimalMark"))),j)})}}),d.spformat=function(a,b){return new h(a,b)},i=function(a,b,c){return ac?c:a},j=function(a,c){var d;return c===2?(d=b.floor(a.length/2),a.length%2?a[d]:(a[d-1]+a[d])/2):a.length%2?(d=(a.length*c+c)/4,d%1?(a[b.floor(d)]+a[b.floor(d)-1])/2:a[d-1]):(d=(a.length*c+2)/4,d%1?(a[b.floor(d)]+a[b.floor(d)-1])/2:a[d-1])},k=function(a){var b;switch(a){case"undefined":a=c;break;case"null":a=null;break;case"true":a=!0;break;case"false":a=!1;break;default:b=parseFloat(a),a==b&&(a=b)}return a},l=function(a){var b,c=[];for(b=a.length;b--;)c[b]=k(a[b]);return c},m=function(a,b){var c,d,e=[];for(c=0,d=a.length;c0;h-=c)a.splice(h,0,e);return a.join("")},o=function(a,b,c){var d;for(d=b.length;d--;){if(c&&b[d]===null)continue;if(b[d]!==a)return!1}return!0},p=function(a){var b=0,c;for(c=a.length;c--;)b+=typeof a[c]=="number"?a[c]:0;return b},r=function(a){return d.isArray(a)?a:[a]},q=function(b){var c;a.createStyleSheet?a.createStyleSheet().cssText=b:(c=a.createElement("style"),c.type="text/css",a.getElementsByTagName("head")[0].appendChild(c),c[typeof a.body.style.WebkitAppearance=="string"?"innerText":"innerHTML"]=b)},d.fn.simpledraw=function(b,e,f,g){var h,i;if(f&&(h=this.data("_jqs_vcanvas")))return h;if(d.fn.sparkline.canvas===!1)return!1;if(d.fn.sparkline.canvas===c){var j=a.createElement("canvas");if(!j.getContext||!j.getContext("2d")){if(!a.namespaces||!!a.namespaces.v)return d.fn.sparkline.canvas=!1,!1;a.namespaces.add("v","urn:schemas-microsoft-com:vml","#default#VML"),d.fn.sparkline.canvas=function(a,b,c,d){return new J(a,b,c)}}else d.fn.sparkline.canvas=function(a,b,c,d){return new I(a,b,c,d)}}return b===c&&(b=d(this).innerWidth()),e===c&&(e=d(this).innerHeight()),h=d.fn.sparkline.canvas(b,e,this,g),i=d(this).data("_jqs_mhandler"),i&&i.registerCanvas(h),h},d.fn.cleardraw=function(){var a=this.data("_jqs_vcanvas");a&&a.reset()},d.RangeMapClass=t=g({init:function(a){var b,c,d=[];for(b in a)a.hasOwnProperty(b)&&typeof b=="string"&&b.indexOf(":")>-1&&(c=b.split(":"),c[0]=c[0].length===0?-Infinity:parseFloat(c[0]),c[1]=c[1].length===0?Infinity:parseFloat(c[1]),c[2]=a[b],d.push(c));this.map=a,this.rangelist=d||!1},get:function(a){var b=this.rangelist,d,e,f;if((f=this.map[a])!==c)return f;if(b)for(d=b.length;d--;){e=b[d];if(e[0]<=a&&e[1]>=a)return e[2]}return c}}),d.range_map=function(a){return new t(a)},u=g({init:function(a,b){var c=d(a);this.$el=c,this.options=b,this.currentPageX=0,this.currentPageY=0,this.el=a,this.splist=[],this.tooltip=null,this.over=!1,this.displayTooltips=!b.get("disableTooltips"),this.highlightEnabled=!b.get("disableHighlight")},registerSparkline:function(a){this.splist.push(a),this.over&&this.updateDisplay()},registerCanvas:function(a){var b=d(a.canvas);this.canvas=a,this.$canvas=b,b.mouseenter(d.proxy(this.mouseenter,this)),b.mouseleave(d.proxy(this.mouseleave,this)),b.click(d.proxy(this.mouseclick,this))},reset:function(a){this.splist=[],this.tooltip&&a&&(this.tooltip.remove(),this.tooltip=c)},mouseclick:function(a){var b=d.Event("sparklineClick");b.originalEvent=a,b.sparklines=this.splist,this.$el.trigger(b)},mouseenter:function(b){d(a.body).unbind("mousemove.jqs"),d(a.body).bind("mousemove.jqs",d.proxy(this.mousemove,this)),this.over=!0,this.currentPageX=b.pageX,this.currentPageY=b.pageY,this.currentEl=b.target,!this.tooltip&&this.displayTooltips&&(this.tooltip=new v(this.options),this.tooltip.updatePosition(b.pageX,b.pageY)),this.updateDisplay()},mouseleave:function(){d(a.body).unbind("mousemove.jqs");var b=this.splist,c=b.length,e=!1,f,g;this.over=!1,this.currentEl=null,this.tooltip&&(this.tooltip.remove(),this.tooltip=null);for(g=0;g