From c530167cbe0403506f9ed1184645281feb21d8eb Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Sat, 4 Oct 2025 18:06:22 -0400 Subject: [PATCH] qwen multi-image edits Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 9 ++++++++- javascript/control.js | 8 ++------ modules/control/run.py | 1 + modules/processing_args.py | 16 +++++++++------- modules/processing_class.py | 4 +++- modules/ui_control.py | 18 +++++++++--------- modules/ui_control_helpers.py | 10 +++++----- 7 files changed, 37 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45a1a05e3..9ba0cb443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log for SD.Next -## Update for 2025-10-03 +## Update for 2025-10-04 - **Models** - [WAN 2.2 14B VACE](https://huggingface.co/alibaba-pai/Wan2.2-VACE-Fun-A14B) @@ -18,6 +18,11 @@ note that nunchaku optimized and prequantized unet is replacement for base unet, so its only applicable to base models, not any of finetunes *how to use*: enable nunchaku in settings -> quantization and then load either sdxl-base or sdxl-base-turbo reference models - **Features** + - [Qwen Image-Edit] multi-image editing + requires qwen-image-edit-2509 or its variant as multi-image edits are not available in original qwen-image + in ui control tab: inputs -> separate init image + add image for *input media* and *control media* + can be - [Cache-DiT](https://github.com/vipshop/cache-dit) cache-dit is a unified, flexible and training-free cache acceleration framework compatible with many dit-based models such as FLUX.1, Qwen, HunyuanImage, Wan2.2, Chroma, etc. @@ -87,6 +92,8 @@ - **modular guiders**: automatically used for compatible pipelines when *modular pipelines* is enabled allows for using many different guidance methods: *CFG, CFGZero, PAG, APG, SLG, SEG, TCFG, FDG* +- **Wiki** + - updates to *AMD-ROCm, ZLUDA, LoRA, DirectML, SDNQ* pages - **Fixes** - **Microsoft Florence 2** both base and large variants *note* this will trigger download of the new variant of the model, feel free to delete older variant in `huggingface` folder diff --git a/javascript/control.js b/javascript/control.js index 1c1c8b3cc..c94c61a67 100644 --- a/javascript/control.js +++ b/javascript/control.js @@ -2,13 +2,9 @@ function controlInputMode(inputMode, ...args) { const updateEl = gradioApp().getElementById('control_update'); if (updateEl) updateEl.click(); const tab = gradioApp().querySelector('#control-tab-input button.selected'); - if (!tab) return ['Select', ...args]; + if (!tab) return ['Image', ...args]; inputMode = tab.innerText; - if (inputMode === 'Image') { - if (!gradioApp().getElementById('control_input_select').classList.contains('hidden')) inputMode = 'Select'; - else if (!gradioApp().getElementById('control_input_resize').classList.contains('hidden')) inputMode = 'Outpaint'; - else if (!gradioApp().getElementById('control_input_inpaint').classList.contains('hidden')) inputMode = 'Inpaint'; - } + console.log('HERE0', tab, inputMode); return [inputMode, ...args]; } diff --git a/modules/control/run.py b/modules/control/run.py index 3f6b0f7be..b62cad89f 100644 --- a/modules/control/run.py +++ b/modules/control/run.py @@ -388,6 +388,7 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg ) p.state = state p.is_tile = False + p.init_control = inits or [] p.orig_init_images = inputs # TODO modernui: monkey-patch for missing tabs.select event diff --git a/modules/processing_args.py b/modules/processing_args.py index c1f0b9cdb..3e28e158a 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -123,9 +123,11 @@ def task_specific_kwargs(p, model): } # model specific args - if 'QwenImageEdit' in model_cls and len(getattr(p, 'init_images', [])) == 0: + 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 'LatentConsistencyModelPipeline' in model_cls and hasattr(p, 'init_images') and len(p.init_images) > 0: + 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 '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] init_latent = torch.stack(init_latents, dim=0).to(shared.device) @@ -133,11 +135,11 @@ def task_specific_kwargs(p, model): init_latent = (1 - p.denoising_strength) * init_latent + init_noise task_args = { 'latents': init_latent.to(model.dtype), - 'width': p.width if hasattr(p, 'width') else None, - 'height': p.height if hasattr(p, 'height') else None, + 'width': p.width, + 'height': p.height, } if 'BlipDiffusionPipeline' in model_cls: - if len(getattr(p, 'init_images', [])) == 0: + if len(p.init_images) == 0: shared.log.error('BLiP diffusion requires init image') return task_args task_args = { @@ -146,9 +148,9 @@ def task_specific_kwargs(p, model): 'target_subject_category': getattr(p, 'prompt', '').split()[-1], 'output_type': 'pil', } - if ('WanImageToVideoPipeline' in model_cls) and (getattr(p, 'init_images', None) is not None) and (len(p.init_images) > 0): + if ('WanImageToVideoPipeline' in model_cls) and (p.init_images is not None) and (len(p.init_images) > 0): task_args['image'] = p.init_images[0] - if ('WanVACEPipeline' in model_cls) and (getattr(p, 'init_images', None) is not None) and (len(p.init_images) > 0): + 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 debug_enabled: diff --git a/modules/processing_class.py b/modules/processing_class.py index ddad4686e..8ac7286fc 100644 --- a/modules/processing_class.py +++ b/modules/processing_class.py @@ -78,7 +78,8 @@ class StableDiffusionProcessing: hdr_color_picker: str = None, hdr_tint_ratio: float = 0, # img2img - init_images: list = None, + init_images: list = [], + init_control: list = [], denoising_strength: float = 0.3, image_cfg_scale: float = None, initial_noise_multiplier: float = None, # pylint: disable=unused-argument # a1111 compatibility @@ -214,6 +215,7 @@ class StableDiffusionProcessing: self.detailer_resolution = detailer_resolution self.restore_faces = restore_faces self.init_images = init_images + self.init_control = init_control self.resize_mode = resize_mode self.resize_name = resize_name self.resize_context = resize_context diff --git a/modules/ui_control.py b/modules/ui_control.py index 3f43701af..5ac075d61 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -183,14 +183,14 @@ def create_ui(_blocks: gr.Blocks=None): 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) with gr.Tab('Image', id='in-image') as tab_image: - input_mode = gr.Label(value='select', visible=False) - input_image = gr.Image(label="Input", show_label=False, type="pil", interactive=True, tool="editor", height=gr_height, visible=True, image_mode='RGB', elem_id='control_input_select', elem_classes=['control-image']) - input_resize = gr.Image(label="Input", show_label=False, type="pil", interactive=True, tool="select", height=gr_height, visible=False, image_mode='RGB', elem_id='control_input_resize', elem_classes=['control-image']) - input_inpaint = gr.Image(label="Input", show_label=False, type="pil", interactive=True, tool="sketch", height=gr_height, visible=False, image_mode='RGB', elem_id='control_input_inpaint', brush_radius=32, mask_opacity=0.6, elem_classes=['control-image']) + input_image = gr.Image(label="Input", show_label=False, type="pil", interactive=True, tool="editor", height=gr_height, image_mode='RGB', elem_id='control_input_select', elem_classes=['control-image']) btn_interrogate = ui_sections.create_interrogate_button('control', what='input') - with gr.Row(): - input_buttons = [gr.Button('Select', visible=True, interactive=False), gr.Button('Inpaint', visible=True, interactive=True), gr.Button('Outpaint', visible=True, interactive=True)] + with gr.Tab('Inpaint', id='in-inpaint') as _tab_inpaint: + input_inpaint = gr.Image(label="Input", show_label=False, type="pil", interactive=True, tool="sketch", height=gr_height, image_mode='RGB', elem_id='control_input_inpaint', brush_radius=32, mask_opacity=0.6, elem_classes=['control-image']) + with gr.Tab('Outpaint', id='in-outpaint') as _tab_outpaint: + input_resize = gr.Image(label="Input", show_label=False, type="pil", interactive=True, tool="select", height=gr_height, image_mode='RGB', elem_id='control_input_resize', elem_classes=['control-image']) with gr.Tab('Video', id='in-video') as tab_video: input_video = gr.Video(label="Input", show_label=False, interactive=True, height=gr_height, elem_classes=['control-image']) with gr.Tab('Batch', id='in-batch') as tab_batch: @@ -231,9 +231,9 @@ def create_ui(_blocks: gr.Blocks=None): 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) + # 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: diff --git a/modules/ui_control_helpers.py b/modules/ui_control_helpers.py index c5757c56f..05e31bf1c 100644 --- a/modules/ui_control_helpers.py +++ b/modules/ui_control_helpers.py @@ -77,7 +77,7 @@ def get_video(filepath: str): def select_input(input_mode, input_image, init_image, init_type, input_resize, input_inpaint, input_video, input_batch, input_folder): global busy, input_source, input_init, input_mask # pylint: disable=global-statement busy = True - if input_mode == 'Select': + if input_mode == 'Image': selected_input = input_image elif input_mode == 'Outpaint': selected_input = input_resize @@ -159,16 +159,16 @@ def copy_input(mode_from, mode_to, input_image, input_resize, input_inpaint): if mode_from == mode_to: return [gr.update(), gr.update(), gr.update()] - elif mode_to == 'Select': + elif mode_to == 'Image': return [getimg(input_resize) if mode_from == 'Outpaint' else getimg(input_inpaint), None, None] elif mode_to == 'Inpaint': - return [None, None, getimg(input_image) if mode_from == 'Select' else getimg(input_resize)] + return [None, None, getimg(input_image) if mode_from == 'Image' else getimg(input_resize)] elif mode_to == 'Outpaint': - return [None, getimg(input_image) if mode_from == 'Select' else getimg(input_inpaint), None] + return [None, getimg(input_image) if mode_from == 'Image' else getimg(input_inpaint), None] else: shared.log.error(f'Control transfer unknown input: from={mode_from} to={mode_to}') return [gr.update(), gr.update(), gr.update()] def transfer_input(dst): - return [gr.update(visible=dst=='Select'), gr.update(visible=dst=='Outpaint'), gr.update(visible=dst=='Inpaint'), gr.update(interactive=dst!='Select'), gr.update(interactive=dst!='Inpaint'), gr.update(interactive=dst!='Outpaint')] + return [gr.update(visible=dst=='Image'), gr.update(visible=dst=='Outpaint'), gr.update(visible=dst=='Inpaint'), gr.update(interactive=dst!='Image'), gr.update(interactive=dst!='Inpaint'), gr.update(interactive=dst!='Outpaint')]