From 475527f0ee7dc444dd986f6a6aa8c6349390275f Mon Sep 17 00:00:00 2001 From: nabilaba Date: Mon, 9 Jun 2025 13:26:08 +0700 Subject: [PATCH 1/9] Added feature Download, Delete, and No Base Model --- modules/config.py | 2 +- modules/default_pipeline.py | 37 +++++++--- modules/flags.py | 2 +- presets/default.json | 55 ++------------- webui.py | 135 +++++++++++++++++++++++++++++++++++- 5 files changed, 168 insertions(+), 63 deletions(-) diff --git a/modules/config.py b/modules/config.py index 8609b415..1a67747b 100644 --- a/modules/config.py +++ b/modules/config.py @@ -267,7 +267,7 @@ temp_path_cleanup_on_launch = get_config_item_or_set_default( ) default_base_model_name = default_model = get_config_item_or_set_default( key='default_model', - default_value='model.safetensors', + default_value='None', validator=lambda x: isinstance(x, str), expected_type=str ) diff --git a/modules/default_pipeline.py b/modules/default_pipeline.py index 494644d6..524f426a 100644 --- a/modules/default_pipeline.py +++ b/modules/default_pipeline.py @@ -46,13 +46,12 @@ def refresh_controlnets(model_paths): @torch.no_grad() @torch.inference_mode() def assert_model_integrity(): - error_message = None + if model_base.unet_with_lora is None or not hasattr(model_base.unet_with_lora, "model"): + print('[Info] Skipping model integrity check: base model is not loaded.') + return True if not isinstance(model_base.unet_with_lora.model, SDXL): - error_message = 'You have selected base model other than SDXL. This is not supported yet.' - - if error_message is not None: - raise NotImplementedError(error_message) + raise NotImplementedError('You have selected base model other than SDXL. This is not supported yet.') return True @@ -62,6 +61,11 @@ def assert_model_integrity(): def refresh_base_model(name, vae_name=None): global model_base + if name is None or name == 'None': + print('[Info] No base model loaded.') + model_base = core.StableDiffusionModel() + return + filename = get_file_from_folder_list(name, modules.config.paths_checkpoints) vae_filename = None @@ -215,17 +219,34 @@ def set_clip_skip(clip_skip: int): @torch.no_grad() @torch.inference_mode() def clear_all_caches(): - final_clip.fcs_cond_cache = {} + global final_clip + if final_clip is not None and hasattr(final_clip, "fcs_cond_cache"): + final_clip.fcs_cond_cache = {} + else: + print("[Info] Skipping cache clear: final_clip is None.") @torch.no_grad() @torch.inference_mode() def prepare_text_encoder(async_call=True): if async_call: - # TODO: make sure that this is always called in an async way so that users cannot feel it. pass + assert_model_integrity() - ldm_patched.modules.model_management.load_models_gpu([final_clip.patcher, final_expansion.patcher]) + + patchers = [] + + if final_clip is not None and hasattr(final_clip, "patcher"): + patchers.append(final_clip.patcher) + + if final_expansion is not None and hasattr(final_expansion, "patcher"): + patchers.append(final_expansion.patcher) + + if len(patchers) > 0: + ldm_patched.modules.model_management.load_models_gpu(patchers) + else: + print("[Info] No models to load into GPU (no base model).") + return diff --git a/modules/flags.py b/modules/flags.py index 05c29a23..a56104f2 100644 --- a/modules/flags.py +++ b/modules/flags.py @@ -103,7 +103,7 @@ sdxl_aspect_ratios = [ '896*1152', '896*1088', '960*1088', '960*1024', '1024*1024', '1024*960', '1088*960', '1088*896', '1152*896', '1152*832', '1216*832', '1280*768', '1344*768', '1344*704', '1408*704', '1472*704', '1536*640', '1600*640', - '1664*576', '1728*576' + '1664*576', '1728*576', '1080*1920', '1920*1080' ] diff --git a/presets/default.json b/presets/default.json index e9aa2b62..d5caf231 100644 --- a/presets/default.json +++ b/presets/default.json @@ -1,60 +1,13 @@ { - "default_model": "juggernautXL_v8Rundiffusion.safetensors", - "default_refiner": "None", "default_refiner_switch": 0.5, - "default_loras": [ - [ - true, - "sd_xl_offset_example-lora_1.0.safetensors", - 0.1 - ], - [ - true, - "None", - 1.0 - ], - [ - true, - "None", - 1.0 - ], - [ - true, - "None", - 1.0 - ], - [ - true, - "None", - 1.0 - ] - ], "default_cfg_scale": 4.0, "default_sample_sharpness": 2.0, "default_sampler": "dpmpp_2m_sde_gpu", "default_scheduler": "karras", - "default_performance": "Speed", + "default_performance": "Quality", "default_prompt": "", "default_prompt_negative": "", - "default_styles": [ - "Fooocus V2", - "Fooocus Enhance", - "Fooocus Sharp" - ], - "default_aspect_ratio": "1152*896", - "default_overwrite_step": -1, - "checkpoint_downloads": { - "juggernautXL_v8Rundiffusion.safetensors": "https://huggingface.co/lllyasviel/fav_models/resolve/main/fav/juggernautXL_v8Rundiffusion.safetensors" - }, - "embeddings_downloads": {}, - "lora_downloads": { - "sd_xl_offset_example-lora_1.0.safetensors": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_offset_example-lora_1.0.safetensors" - }, - "previous_default_models": [ - "juggernautXL_version8Rundiffusion.safetensors", - "juggernautXL_version7Rundiffusion.safetensors", - "juggernautXL_v7Rundiffusion.safetensors", - "juggernautXL_version6Rundiffusion.safetensors", - "juggernautXL_v6Rundiffusion.safetensors" - ] + "default_styles": [], + "default_aspect_ratio": "1080*1920", + "default_overwrite_step": -1 } \ No newline at end of file diff --git a/webui.py b/webui.py index b8159d85..d617ba6f 100644 --- a/webui.py +++ b/webui.py @@ -24,6 +24,12 @@ from modules.ui_gradio_extensions import reload_javascript from modules.auth import auth_enabled, check_auth from modules.util import is_json +import os +import shutil +import requests +from urllib.parse import urlparse, unquote +from modules.model_loader import load_file_from_url + def get_task(*args): args = list(args) args.pop(0) @@ -654,7 +660,7 @@ with shared.gradio_root: with gr.Tab(label='Models'): with gr.Group(): with gr.Row(): - base_model = gr.Dropdown(label='Base Model (SDXL only)', choices=modules.config.model_filenames, value=modules.config.default_base_model_name, show_label=True) + base_model = gr.Dropdown(label='Base Model (SDXL only)', choices=['None'] + modules.config.model_filenames, value=modules.config.default_base_model_name, show_label=True) refiner_model = gr.Dropdown(label='Refiner (SDXL or SD 1.5)', choices=['None'] + modules.config.model_filenames, value=modules.config.default_refiner_model_name, show_label=True) refiner_switch = gr.Slider(label='Refiner Switch At', minimum=0.1, maximum=1.0, step=0.0001, @@ -886,6 +892,131 @@ with shared.gradio_root: refresh_files.click(refresh_files_clicked, [], refresh_files_output + lora_ctrls, queue=False, show_progress=False) + with gr.Tab(label='Others'): + with gr.Column(): + with gr.Tab(label='Download'): + file_input_path = gr.Textbox( + label='File Path or URL', + placeholder='Enter full path to file or downloadable URL', + lines=1 + ) + + destination_folder = gr.Dropdown( + label='Target Folder', + choices=[ + modules.config.paths_checkpoints[0], + modules.config.paths_loras[0], + modules.config.path_embeddings, + modules.config.path_vae, + modules.config.path_outputs + ], + value=modules.config.paths_checkpoints[0] + ) + + download_result_text = gr.Textbox(label='Download Status', interactive=False) + download_file_button = gr.Button(value='\U00002B07 Download', variant='secondary', elem_classes='refresh_button') + + def perform_download(file_url_or_path, target_directory): + try: + if isinstance(target_directory, tuple): + target_directory = target_directory[1] + + if file_url_or_path.startswith(('http://', 'https://')): + response = requests.get(file_url_or_path, stream=True) + response.raise_for_status() + + # Ambil nama file dari header jika tersedia + content_disposition = response.headers.get('Content-Disposition', '') + if 'filename=' in content_disposition: + filename = content_disposition.split('filename=')[-1].strip('"') + else: + # Fallback: ambil dari path URL + parsed_url = urlparse(file_url_or_path) + filename = unquote(os.path.basename(parsed_url.path)) + downloaded_path = load_file_from_url( + file_url_or_path, + model_dir=target_directory, + progress=True, + file_name=filename + ) + return f"\U00002705 Downloaded to: {downloaded_path}" + + if os.path.isfile(file_url_or_path): + filename = os.path.basename(file_url_or_path) + destination_path = os.path.join(target_directory, filename) + shutil.copy(file_url_or_path, destination_path) + return f"\U00002705 Copied to: {destination_path}" + + return "\U0000274C Error: File not found or invalid input." + + except Exception as e: + return f"\U0000274C Failed: {str(e)}" + + download_file_button.click( + fn=perform_download, + inputs=[file_input_path, destination_folder], + outputs=[download_result_text] + ) + + with gr.Tab(label='Delete'): + delete_folder_dropdown = gr.Dropdown( + label='Select Folder', + choices=[ + modules.config.paths_checkpoints[0], + modules.config.paths_loras[0], + modules.config.path_embeddings, + modules.config.path_vae, + modules.config.path_outputs + ], + value=modules.config.paths_checkpoints[0] + ) + + file_list_dropdown = gr.Dropdown(label="Select File to Delete", choices=[], multiselect=True) + delete_button = gr.Button(value='\U0001F5D1 Delete Selected File(s)', variant='stop') + delete_status = gr.Textbox(visible=True, interactive=False, label="Delete Status") + + def update_file_list(folder): + try: + files = [f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f))] + return gr.update(choices=files, value=[]) + except Exception as e: + return gr.update(choices=[], value=[]) + + def delete_selected_files(folder, selected_files): + deleted = [] + errors = [] + + for fname in selected_files: + try: + file_path = os.path.join(folder, fname) + if os.path.isfile(file_path): + os.remove(file_path) + deleted.append(fname) + else: + errors.append(fname) + except Exception as e: + errors.append(f"{fname} (error: {e})") + + if not deleted and not errors: + status = "⚠️ No files selected." + else: + status = "" + if deleted: + status += f"✅ Deleted: {', '.join(deleted)}. " + if errors: + status += f"❌ Failed: {', '.join(errors)}" + + try: + files = [f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f))] + except Exception: + files = [] + + return status.strip(), gr.update(choices=files, value=[]) + + delete_folder_dropdown.change(update_file_list, inputs=[delete_folder_dropdown], outputs=[file_list_dropdown]) + delete_button.click(delete_selected_files, inputs=[delete_folder_dropdown, file_list_dropdown], outputs=[delete_status, file_list_dropdown]) + + state_is_generating = gr.State(False) load_data_outputs = [advanced_checkbox, image_number, prompt, negative_prompt, style_selections, @@ -1125,4 +1256,4 @@ shared.gradio_root.launch( auth=check_auth if (args_manager.args.share or args_manager.args.listen) and auth_enabled else None, allowed_paths=[modules.config.path_outputs], blocked_paths=[constants.AUTH_FILENAME] -) +) \ No newline at end of file From 31fcbbb76ae75eb5e8f61810ef88139cfae13be8 Mon Sep 17 00:00:00 2001 From: nabilaba Date: Mon, 9 Jun 2025 13:43:44 +0700 Subject: [PATCH 2/9] maybe make like this --- modules/default_pipeline.py | 52 ++++++++++++------------------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/modules/default_pipeline.py b/modules/default_pipeline.py index 524f426a..0ccb0796 100644 --- a/modules/default_pipeline.py +++ b/modules/default_pipeline.py @@ -46,12 +46,13 @@ def refresh_controlnets(model_paths): @torch.no_grad() @torch.inference_mode() def assert_model_integrity(): - if model_base.unet_with_lora is None or not hasattr(model_base.unet_with_lora, "model"): - print('[Info] Skipping model integrity check: base model is not loaded.') - return True + error_message = None if not isinstance(model_base.unet_with_lora.model, SDXL): - raise NotImplementedError('You have selected base model other than SDXL. This is not supported yet.') + error_message = 'You have selected base model other than SDXL. This is not supported yet.' + + if error_message is not None: + raise NotImplementedError(error_message) return True @@ -61,11 +62,6 @@ def assert_model_integrity(): def refresh_base_model(name, vae_name=None): global model_base - if name is None or name == 'None': - print('[Info] No base model loaded.') - model_base = core.StableDiffusionModel() - return - filename = get_file_from_folder_list(name, modules.config.paths_checkpoints) vae_filename = None @@ -219,34 +215,17 @@ def set_clip_skip(clip_skip: int): @torch.no_grad() @torch.inference_mode() def clear_all_caches(): - global final_clip - if final_clip is not None and hasattr(final_clip, "fcs_cond_cache"): - final_clip.fcs_cond_cache = {} - else: - print("[Info] Skipping cache clear: final_clip is None.") + final_clip.fcs_cond_cache = {} @torch.no_grad() @torch.inference_mode() def prepare_text_encoder(async_call=True): if async_call: + # TODO: make sure that this is always called in an async way so that users cannot feel it. pass - assert_model_integrity() - - patchers = [] - - if final_clip is not None and hasattr(final_clip, "patcher"): - patchers.append(final_clip.patcher) - - if final_expansion is not None and hasattr(final_expansion, "patcher"): - patchers.append(final_expansion.patcher) - - if len(patchers) > 0: - ldm_patched.modules.model_management.load_models_gpu(patchers) - else: - print("[Info] No models to load into GPU (no base model).") - + ldm_patched.modules.model_management.load_models_gpu([final_clip.patcher, final_expansion.patcher]) return @@ -288,12 +267,15 @@ def refresh_everything(refiner_model_name, base_model_name, loras, return -refresh_everything( - refiner_model_name=modules.config.default_refiner_model_name, - base_model_name=modules.config.default_base_model_name, - loras=get_enabled_loras(modules.config.default_loras), - vae_name=modules.config.default_vae, -) +if modules.config.default_base_model_name != 'None': + refresh_everything( + refiner_model_name=modules.config.default_refiner_model_name, + base_model_name=modules.config.default_base_model_name, + loras=get_enabled_loras(modules.config.default_loras), + vae_name=modules.config.default_vae, + ) +else: + print('[Startup] Skipping model load (default_base_model_name is "None").') @torch.no_grad() From db3f2c4abc8fce6afa80231371994afa86acabdb Mon Sep 17 00:00:00 2001 From: nabilaba Date: Mon, 9 Jun 2025 15:10:26 +0700 Subject: [PATCH 3/9] my preset --- presets/.gitignore | 3 ++- presets/nabil.json | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 presets/nabil.json diff --git a/presets/.gitignore b/presets/.gitignore index e39511a9..bfd1096a 100644 --- a/presets/.gitignore +++ b/presets/.gitignore @@ -5,4 +5,5 @@ !playground_v2.5.json !pony_v6.json !realistic.json -!sai.json \ No newline at end of file +!sai.json +!nabil.json \ No newline at end of file diff --git a/presets/nabil.json b/presets/nabil.json new file mode 100644 index 00000000..9c2d4a12 --- /dev/null +++ b/presets/nabil.json @@ -0,0 +1,13 @@ +{ + "default_refiner_switch": 0.5, + "default_cfg_scale": 4.0, + "default_sample_sharpness": 2.0, + "default_sampler": "dpmpp_2m_sde_gpu", + "default_scheduler": "karras", + "default_performance": "Quality", + "default_prompt": "ultra-detailed, photorealistic, 8K resolution, high dynamic range, realistic lighting, global illumination, shallow depth of field, cinematic look, DSLR camera, ray tracing, volumetric lighting, bokeh effect, hyper-realistic textures, shot on Canon EOS R5, 50mm lens, HDR imaging, post-processed in Adobe Lightroom and Photoshop", + "default_prompt_negative": "low resolution, extra limbs, cartoon, anime, illustration, drawing, painting, sketch, grainy, noisy, watermark, logo, text, glitch, jpeg artifacts, duplicate, poorly drawn, out of frame, cropped, low quality, bad anatomy, bad hands, extra fingers, distorted face", + "default_styles": [], + "default_aspect_ratio": "1080*1920", + "default_overwrite_step": -1 +} \ No newline at end of file From 9ab148eae6710b0f5611440ff3f27d744660af31 Mon Sep 17 00:00:00 2001 From: nabilaba Date: Tue, 10 Jun 2025 09:32:35 +0700 Subject: [PATCH 4/9] try add auto refresh on load, no manual refresh, after download and delete file too --- webui.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/webui.py b/webui.py index d617ba6f..f9e59759 100644 --- a/webui.py +++ b/webui.py @@ -18,6 +18,8 @@ import copy import launch from extras.inpaint_mask import SAMOptions +modules.config.update_files() # ✅ Force initial scan of models/Loras/etc + from modules.sdxl_styles import legal_style_names from modules.private_logger import get_current_html_path from modules.ui_gradio_extensions import reload_javascript @@ -939,26 +941,58 @@ with shared.gradio_root: progress=True, file_name=filename ) - return f"\U00002705 Downloaded to: {downloaded_path}" + modules.config.update_files() # ✅ REFRESH MODEL LIST + return ( + f"✅ Downloaded to: {downloaded_path}", + gr.update(choices=['None'] + modules.config.model_filenames), + gr.update(choices=['None'] + modules.config.model_filenames), + *[ + item + for _ in range(len(lora_ctrls) // 3) + for item in ( + gr.update(interactive=True), + gr.update(choices=['None'] + modules.config.lora_filenames), + gr.update() + ) + ] + ) if os.path.isfile(file_url_or_path): filename = os.path.basename(file_url_or_path) destination_path = os.path.join(target_directory, filename) shutil.copy(file_url_or_path, destination_path) - return f"\U00002705 Copied to: {destination_path}" - - return "\U0000274C Error: File not found or invalid input." + modules.config.update_files() # ✅ Auto-refresh files + return ( + f"✅ Copied to: {destination_path}", + gr.update(choices=['None'] + modules.config.model_filenames), + gr.update(choices=['None'] + modules.config.model_filenames), + *[ + item + for _ in range(len(lora_ctrls) // 3) + for item in ( + gr.update(interactive=True), + gr.update(choices=['None'] + modules.config.lora_filenames), + gr.update() + ) + ] + ) + + return ("❌ Error: File not found or invalid input.",) + ( + gr.update(), gr.update(), *[gr.update()] * len(lora_ctrls) + ) except Exception as e: - return f"\U0000274C Failed: {str(e)}" + return (f"❌ Failed: {str(e)}",) + ( + gr.update(), gr.update(), *[gr.update()] * len(lora_ctrls) + ) download_file_button.click( fn=perform_download, inputs=[file_input_path, destination_folder], - outputs=[download_result_text] + outputs=[download_result_text, base_model, refiner_model] + lora_ctrls ) - with gr.Tab(label='Delete'): + with gr.Tab(label='Delete') as delete_tab: delete_folder_dropdown = gr.Dropdown( label='Select Folder', choices=[ @@ -1015,7 +1049,13 @@ with shared.gradio_root: delete_folder_dropdown.change(update_file_list, inputs=[delete_folder_dropdown], outputs=[file_list_dropdown]) delete_button.click(delete_selected_files, inputs=[delete_folder_dropdown, file_list_dropdown], outputs=[delete_status, file_list_dropdown]) - + delete_tab.select( + fn=update_file_list, + inputs=[delete_folder_dropdown], + outputs=[file_list_dropdown], + queue=False, + show_progress=False + ) state_is_generating = gr.State(False) From c0996c1f14a8a695a654cda2aeb7332f39a68bf8 Mon Sep 17 00:00:00 2001 From: nabilaba Date: Tue, 10 Jun 2025 09:59:43 +0700 Subject: [PATCH 5/9] added None to clicked refresh all files button --- webui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webui.py b/webui.py index f9e59759..07a61237 100644 --- a/webui.py +++ b/webui.py @@ -878,7 +878,7 @@ with shared.gradio_root: def refresh_files_clicked(): modules.config.update_files() - results = [gr.update(choices=modules.config.model_filenames)] + results = [gr.update(choices=['None'] + modules.config.model_filenames)] results += [gr.update(choices=['None'] + modules.config.model_filenames)] results += [gr.update(choices=[flags.default_vae] + modules.config.vae_filenames)] if not args_manager.args.disable_preset_selection: From cdf57c251d96e0ca9c0ad40214adc16537d707f2 Mon Sep 17 00:00:00 2001 From: nabilaba Date: Tue, 10 Jun 2025 20:21:05 +0700 Subject: [PATCH 6/9] refresh fixed by clicked tab easy way --- webui.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/webui.py b/webui.py index 07a61237..567965bc 100644 --- a/webui.py +++ b/webui.py @@ -18,8 +18,6 @@ import copy import launch from extras.inpaint_mask import SAMOptions -modules.config.update_files() # ✅ Force initial scan of models/Loras/etc - from modules.sdxl_styles import legal_style_names from modules.private_logger import get_current_html_path from modules.ui_gradio_extensions import reload_javascript @@ -659,7 +657,7 @@ with shared.gradio_root: show_progress=False).then( lambda: None, _js='()=>{refresh_style_localization();}') - with gr.Tab(label='Models'): + with gr.Tab(label='Models') as model_tab: with gr.Group(): with gr.Row(): base_model = gr.Dropdown(label='Base Model (SDXL only)', choices=['None'] + modules.config.model_filenames, value=modules.config.default_base_model_name, show_label=True) @@ -893,6 +891,14 @@ with shared.gradio_root: refresh_files_output += [preset_selection] refresh_files.click(refresh_files_clicked, [], refresh_files_output + lora_ctrls, queue=False, show_progress=False) + + model_tab.select( + fn=refresh_files_clicked, + inputs=[], + outputs=refresh_files_output + lora_ctrls, + queue=False, + show_progress=False + ) with gr.Tab(label='Others'): with gr.Column(): From 1cde3d325dd5b58ae8bf7b82f71e9f9617cc698a Mon Sep 17 00:00:00 2001 From: nabilaba Date: Wed, 11 Jun 2025 00:09:21 +0700 Subject: [PATCH 7/9] separate file, and more function --- presets/nabil.json | 13 --- webui.py | 195 +++++-------------------------- webui_others.py | 280 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 311 insertions(+), 177 deletions(-) delete mode 100644 presets/nabil.json create mode 100644 webui_others.py diff --git a/presets/nabil.json b/presets/nabil.json deleted file mode 100644 index 9c2d4a12..00000000 --- a/presets/nabil.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "default_refiner_switch": 0.5, - "default_cfg_scale": 4.0, - "default_sample_sharpness": 2.0, - "default_sampler": "dpmpp_2m_sde_gpu", - "default_scheduler": "karras", - "default_performance": "Quality", - "default_prompt": "ultra-detailed, photorealistic, 8K resolution, high dynamic range, realistic lighting, global illumination, shallow depth of field, cinematic look, DSLR camera, ray tracing, volumetric lighting, bokeh effect, hyper-realistic textures, shot on Canon EOS R5, 50mm lens, HDR imaging, post-processed in Adobe Lightroom and Photoshop", - "default_prompt_negative": "low resolution, extra limbs, cartoon, anime, illustration, drawing, painting, sketch, grainy, noisy, watermark, logo, text, glitch, jpeg artifacts, duplicate, poorly drawn, out of frame, cropped, low quality, bad anatomy, bad hands, extra fingers, distorted face", - "default_styles": [], - "default_aspect_ratio": "1080*1920", - "default_overwrite_step": -1 -} \ No newline at end of file diff --git a/webui.py b/webui.py index 567965bc..0962d634 100644 --- a/webui.py +++ b/webui.py @@ -24,6 +24,7 @@ from modules.ui_gradio_extensions import reload_javascript from modules.auth import auth_enabled, check_auth from modules.util import is_json +import webui_others import os import shutil import requests @@ -562,7 +563,7 @@ with shared.gradio_root: outputs=enhance_input_panel, queue=False, show_progress=False, _js=switch_js) with gr.Column(scale=1, visible=modules.config.default_advanced_checkbox) as advanced_column: - with gr.Tab(label='Settings'): + with gr.Tab(label='Settings') as seetings_tab: if not args_manager.args.disable_preset_selection: preset_selection = gr.Dropdown(label='Preset', choices=modules.config.available_presets, @@ -891,7 +892,23 @@ with shared.gradio_root: refresh_files_output += [preset_selection] refresh_files.click(refresh_files_clicked, [], refresh_files_output + lora_ctrls, queue=False, show_progress=False) - + + advanced_checkbox.select( + fn=refresh_files_clicked, + inputs=[], + outputs=refresh_files_output + lora_ctrls, + queue=False, + show_progress=False + ) + + seetings_tab.select( + fn=refresh_files_clicked, + inputs=[], + outputs=refresh_files_output + lora_ctrls, + queue=False, + show_progress=False + ) + model_tab.select( fn=refresh_files_clicked, inputs=[], @@ -900,168 +917,18 @@ with shared.gradio_root: show_progress=False ) - with gr.Tab(label='Others'): - with gr.Column(): - with gr.Tab(label='Download'): - file_input_path = gr.Textbox( - label='File Path or URL', - placeholder='Enter full path to file or downloadable URL', - lines=1 - ) - - destination_folder = gr.Dropdown( - label='Target Folder', - choices=[ - modules.config.paths_checkpoints[0], - modules.config.paths_loras[0], - modules.config.path_embeddings, - modules.config.path_vae, - modules.config.path_outputs - ], - value=modules.config.paths_checkpoints[0] - ) - - download_result_text = gr.Textbox(label='Download Status', interactive=False) - download_file_button = gr.Button(value='\U00002B07 Download', variant='secondary', elem_classes='refresh_button') - - def perform_download(file_url_or_path, target_directory): - try: - if isinstance(target_directory, tuple): - target_directory = target_directory[1] - - if file_url_or_path.startswith(('http://', 'https://')): - response = requests.get(file_url_or_path, stream=True) - response.raise_for_status() - - # Ambil nama file dari header jika tersedia - content_disposition = response.headers.get('Content-Disposition', '') - if 'filename=' in content_disposition: - filename = content_disposition.split('filename=')[-1].strip('"') - else: - # Fallback: ambil dari path URL - parsed_url = urlparse(file_url_or_path) - filename = unquote(os.path.basename(parsed_url.path)) - downloaded_path = load_file_from_url( - file_url_or_path, - model_dir=target_directory, - progress=True, - file_name=filename - ) - modules.config.update_files() # ✅ REFRESH MODEL LIST - return ( - f"✅ Downloaded to: {downloaded_path}", - gr.update(choices=['None'] + modules.config.model_filenames), - gr.update(choices=['None'] + modules.config.model_filenames), - *[ - item - for _ in range(len(lora_ctrls) // 3) - for item in ( - gr.update(interactive=True), - gr.update(choices=['None'] + modules.config.lora_filenames), - gr.update() - ) - ] - ) - - if os.path.isfile(file_url_or_path): - filename = os.path.basename(file_url_or_path) - destination_path = os.path.join(target_directory, filename) - shutil.copy(file_url_or_path, destination_path) - modules.config.update_files() # ✅ Auto-refresh files - return ( - f"✅ Copied to: {destination_path}", - gr.update(choices=['None'] + modules.config.model_filenames), - gr.update(choices=['None'] + modules.config.model_filenames), - *[ - item - for _ in range(len(lora_ctrls) // 3) - for item in ( - gr.update(interactive=True), - gr.update(choices=['None'] + modules.config.lora_filenames), - gr.update() - ) - ] - ) - - return ("❌ Error: File not found or invalid input.",) + ( - gr.update(), gr.update(), *[gr.update()] * len(lora_ctrls) - ) - - except Exception as e: - return (f"❌ Failed: {str(e)}",) + ( - gr.update(), gr.update(), *[gr.update()] * len(lora_ctrls) - ) - - download_file_button.click( - fn=perform_download, - inputs=[file_input_path, destination_folder], - outputs=[download_result_text, base_model, refiner_model] + lora_ctrls - ) - - with gr.Tab(label='Delete') as delete_tab: - delete_folder_dropdown = gr.Dropdown( - label='Select Folder', - choices=[ - modules.config.paths_checkpoints[0], - modules.config.paths_loras[0], - modules.config.path_embeddings, - modules.config.path_vae, - modules.config.path_outputs - ], - value=modules.config.paths_checkpoints[0] - ) - - file_list_dropdown = gr.Dropdown(label="Select File to Delete", choices=[], multiselect=True) - delete_button = gr.Button(value='\U0001F5D1 Delete Selected File(s)', variant='stop') - delete_status = gr.Textbox(visible=True, interactive=False, label="Delete Status") - - def update_file_list(folder): - try: - files = [f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f))] - return gr.update(choices=files, value=[]) - except Exception as e: - return gr.update(choices=[], value=[]) - - def delete_selected_files(folder, selected_files): - deleted = [] - errors = [] - - for fname in selected_files: - try: - file_path = os.path.join(folder, fname) - if os.path.isfile(file_path): - os.remove(file_path) - deleted.append(fname) - else: - errors.append(fname) - except Exception as e: - errors.append(f"{fname} (error: {e})") - - if not deleted and not errors: - status = "⚠️ No files selected." - else: - status = "" - if deleted: - status += f"✅ Deleted: {', '.join(deleted)}. " - if errors: - status += f"❌ Failed: {', '.join(errors)}" - - try: - files = [f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f))] - except Exception: - files = [] - - return status.strip(), gr.update(choices=files, value=[]) - - delete_folder_dropdown.change(update_file_list, inputs=[delete_folder_dropdown], outputs=[file_list_dropdown]) - delete_button.click(delete_selected_files, inputs=[delete_folder_dropdown, file_list_dropdown], outputs=[delete_status, file_list_dropdown]) - delete_tab.select( - fn=update_file_list, - inputs=[delete_folder_dropdown], - outputs=[file_list_dropdown], - queue=False, - show_progress=False - ) + webui_others.build_others_tab( + prompt=prompt, + negative_prompt=negative_prompt, + aspect_ratio=aspect_ratios_selection, + performance=performance_selection, + styles=style_selections, + base_model=base_model, + refiner_model=refiner_model, + refiner_switch=refiner_switch, + image_number=image_number, + lora_ctrls=lora_ctrls + ) state_is_generating = gr.State(False) diff --git a/webui_others.py b/webui_others.py new file mode 100644 index 00000000..0927541b --- /dev/null +++ b/webui_others.py @@ -0,0 +1,280 @@ +# webui_others.py +import os +import shutil +import requests +from urllib.parse import urlparse, unquote +import gradio as gr +import modules.config +from modules.model_loader import load_file_from_url + +def build_others_tab( + prompt, + negative_prompt, + aspect_ratio, + performance, + styles, + base_model, + refiner_model, + refiner_switch, + image_number, + lora_ctrls +): + with gr.Tab(label="Others") as others_tab: + with gr.Tab(label="Download"): + file_input_path = gr.Textbox( + label="File Path or URL", + placeholder="Enter full path to file or downloadable URL", + lines=1, + ) + + destination_folder = gr.Dropdown( + label="Target Folder", + choices=[ + modules.config.paths_checkpoints[0], + modules.config.paths_loras[0], + modules.config.path_embeddings, + modules.config.path_vae, + ], + value=modules.config.paths_checkpoints[0], + ) + + download_result_text = gr.Textbox( + label="Download Status", interactive=False + ) + download_file_button = gr.Button( + value="\u2b07 Download", + variant="secondary", + elem_classes="refresh_button", + ) + + def perform_download(file_url_or_path, target_directory): + try: + if isinstance(target_directory, tuple): + target_directory = target_directory[1] + + if file_url_or_path.startswith(("http://", "https://")): + response = requests.get(file_url_or_path, stream=True) + response.raise_for_status() + content_disposition = response.headers.get( + "Content-Disposition", "" + ) + if "filename=" in content_disposition: + filename = content_disposition.split("filename=")[ + -1 + ].strip('"') + else: + parsed_url = urlparse(file_url_or_path) + filename = unquote(os.path.basename(parsed_url.path)) + downloaded_path = load_file_from_url( + file_url_or_path, + model_dir=target_directory, + progress=True, + file_name=filename, + ) + return f"\u2705 Downloaded to: {downloaded_path}" + + if os.path.isfile(file_url_or_path): + filename = os.path.basename(file_url_or_path) + destination_path = os.path.join(target_directory, filename) + shutil.copy(file_url_or_path, destination_path) + return f"\u2705 Copied to: {destination_path}" + + return "\u274c Error: File not found or invalid input." + + except Exception as e: + return f"\u274c Failed: {str(e)}" + + download_file_button.click( + fn=perform_download, + inputs=[file_input_path, destination_folder], + outputs=[download_result_text], + ) + + with gr.Tab(label="Delete"): + delete_folder_dropdown = gr.Dropdown( + label="Select Folder", + choices=[ + modules.config.paths_checkpoints[0], + modules.config.paths_loras[0], + modules.config.path_embeddings, + modules.config.path_vae, + modules.config.get_dir_or_set_default("path_presets", "../presets"), + ], + value=modules.config.paths_checkpoints[0], + ) + + file_list_dropdown = gr.Dropdown( + label="Select File to Delete", choices=[], multiselect=True + ) + delete_button = gr.Button( + value="\U0001f5d1 Delete Selected File(s)", variant="stop" + ) + delete_status = gr.Textbox( + visible=True, interactive=False, label="Delete Status" + ) + + def update_file_list(folder): + try: + files = [ + f + for f in os.listdir(folder) + if os.path.isfile(os.path.join(folder, f)) + ] + return gr.update(choices=files, value=[]) + except Exception as e: + return gr.update(choices=[], value=[]) + + def delete_selected_files(folder, selected_files): + deleted = [] + errors = [] + + for fname in selected_files: + try: + file_path = os.path.join(folder, fname) + if os.path.isfile(file_path): + os.remove(file_path) + deleted.append(fname) + else: + errors.append(fname) + except Exception as e: + errors.append(f"{fname} (error: {e})") + + status = "" + if deleted: + status += f"\u2705 Deleted: {', '.join(deleted)}. " + if errors: + status += f"\u274c Failed: {', '.join(errors)}" + if not deleted and not errors: + status = "\u26a0\ufe0f No files selected." + + try: + files = [ + f + for f in os.listdir(folder) + if os.path.isfile(os.path.join(folder, f)) + ] + except Exception: + files = [] + + return status.strip(), gr.update(choices=files, value=[]) + + delete_folder_dropdown.change( + update_file_list, + inputs=[delete_folder_dropdown], + outputs=[file_list_dropdown], + ) + delete_button.click( + delete_selected_files, + inputs=[delete_folder_dropdown, file_list_dropdown], + outputs=[delete_status, file_list_dropdown], + ) + + with gr.Tab(label="Backup/Restore"): + with gr.Tab(label="Preset"): + with gr.Row(): + backup_name = gr.Textbox(label="Backup Filename (no extension)", placeholder="e.g. my_config_backup") + backup_button = gr.Button(value="\u2B06 Backup Settings") + backup_file = gr.File(label="Download .json", interactive=False) + + def clean_aspect_ratio(value): + if not value: + return None + raw = value.split(" ")[0] + return raw.replace("×", "*") # convert Unicode × to ASCII * + + def backup_selected_settings( + filename, + prompt, negative_prompt, aspect_ratio, performance, styles, + base_model, refiner_model, refiner_switch, image_number, + *lora_ctrls + ): + import json + if not filename: + return None + + config = { + "default_prompt": prompt, + "default_prompt_negative": negative_prompt, + "default_aspect_ratio": clean_aspect_ratio(aspect_ratio), + "default_performance": performance, + "default_styles": styles, + "default_model": base_model, + "default_refiner": refiner_model, + "default_refiner_switch": refiner_switch, + "default_image_number": image_number + } + + loras = [] + for i in range(0, len(lora_ctrls), 3): + enabled = lora_ctrls[i] + model = lora_ctrls[i + 1] + weight = lora_ctrls[i + 2] + if model and model != "None": + loras.append([enabled, model, weight]) + + if loras: + config["default_loras"] = loras + + config = {k: v for k, v in config.items() if v not in [None, "", [], "None"]} + + out_path = os.path.join("outputs", f"{filename}.json") + with open(out_path, "w") as f: + json.dump(config, f, indent=2) + + return out_path + + backup_button.click( + fn=backup_selected_settings, + inputs=[ + backup_name, + prompt, + negative_prompt, + aspect_ratio, + performance, + styles, + base_model, + refiner_model, + refiner_switch, + image_number, + ] + lora_ctrls, + outputs=[backup_file], + ) + + with gr.Tab(label="Restore"): + with gr.Row(): + restore_file = gr.File(label="Upload Config File (.json)", file_types=[".json"]) + restore_button = gr.Button(value="\u267B️ Restore to Presets Folder") + restore_status = gr.Textbox(label="Status", interactive=False) + + def restore_config_file(file_obj): + try: + if file_obj is None: + return "\u26A0 No file selected." + filename = os.path.basename(file_obj.name) + presets_dir = os.path.join("presets") + os.makedirs(presets_dir, exist_ok=True) + destination = os.path.join(presets_dir, filename) + shutil.copy(file_obj.name, destination) + return f"\u2705 Saved to presets/{filename}" + except Exception as e: + return f"\u274C Failed to restore: {e}" + + restore_button.click( + fn=restore_config_file, + inputs=[restore_file], + outputs=[restore_status] + ) + + gr.Markdown( + "You can backup your current settings and restore them later. " + "This is useful for saving configurations or sharing with others." + ) + + + others_tab.select( + fn=update_file_list, + inputs=[delete_folder_dropdown], + outputs=[file_list_dropdown], + queue=False, + show_progress=False, + ) From 7a463054192621fae35d80fb5d7dd4461adfd274 Mon Sep 17 00:00:00 2001 From: nabilaba Date: Wed, 11 Jun 2025 00:29:17 +0700 Subject: [PATCH 8/9] remove unused code --- presets/.gitignore | 3 +-- webui.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/presets/.gitignore b/presets/.gitignore index bfd1096a..e39511a9 100644 --- a/presets/.gitignore +++ b/presets/.gitignore @@ -5,5 +5,4 @@ !playground_v2.5.json !pony_v6.json !realistic.json -!sai.json -!nabil.json \ No newline at end of file +!sai.json \ No newline at end of file diff --git a/webui.py b/webui.py index 0962d634..795c8b7a 100644 --- a/webui.py +++ b/webui.py @@ -26,8 +26,6 @@ from modules.util import is_json import webui_others import os -import shutil -import requests from urllib.parse import urlparse, unquote from modules.model_loader import load_file_from_url From dd3546164ac82bb5d4e477ef61313bf573661d44 Mon Sep 17 00:00:00 2001 From: nabilaba Date: Wed, 11 Jun 2025 00:35:30 +0700 Subject: [PATCH 9/9] fix typo --- webui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webui.py b/webui.py index 795c8b7a..df7bc072 100644 --- a/webui.py +++ b/webui.py @@ -561,7 +561,7 @@ with shared.gradio_root: outputs=enhance_input_panel, queue=False, show_progress=False, _js=switch_js) with gr.Column(scale=1, visible=modules.config.default_advanced_checkbox) as advanced_column: - with gr.Tab(label='Settings') as seetings_tab: + with gr.Tab(label='Settings') as settings_tab: if not args_manager.args.disable_preset_selection: preset_selection = gr.Dropdown(label='Preset', choices=modules.config.available_presets, @@ -899,7 +899,7 @@ with shared.gradio_root: show_progress=False ) - seetings_tab.select( + settings_tab.select( fn=refresh_files_clicked, inputs=[], outputs=refresh_files_output + lora_ctrls,