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