From 8b24efe1b56beb6c1162dcf67d1fd26d820a6be5 Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Fri, 14 Jul 2023 15:00:43 -0400 Subject: [PATCH] rework diffusers args as and introduce second pass as replacement for hires --- .eslintrc.json | 25 +++---- .../stable-diffusion-webui-images-browser | 2 +- javascript/imageViewer.js | 1 - modules/img2img.py | 8 ++- modules/processing.py | 62 +++++++++++----- modules/sd_samplers_diffusers.py | 3 + modules/shared.py | 12 ++-- modules/txt2img.py | 11 +-- modules/ui.py | 71 ++++++++++++------- scripts/xyz_grid.py | 10 --- wiki | 2 +- 11 files changed, 128 insertions(+), 79 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 4912704df..770b8b4e2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,21 +16,22 @@ }, "rules": { "max-len": [1, 275, 3], - "no-plusplus":"off", - "no-console":"off", - "no-unused-vars":"off", - "no-return-assign":"off", - "prefer-rest-params":"off", - "no-empty":"off", - "no-restricted-syntax":"off", - "no-param-reassign":"off", - "no-bitwise":"off", - "default-case":"off", - "no-restricted-globals":"off", - "no-mixed-operators":"off", "camelcase":"off", + "default-case":"off", + "no-bitwise":"off", "no-confusing-arrow":"off", + "no-console":"off", + "no-empty":"off", + "no-mixed-operators":"off", + "no-param-reassign":"off", + "no-plusplus":"off", + "no-restricted-globals":"off", + "no-restricted-syntax":"off", + "no-return-assign":"off", + "no-unused-vars":"off", "no-useless-escape":"off", + "object-curly-newline":"off", + "prefer-rest-params":"off", "radix":"off" }, "globals": { diff --git a/extensions-builtin/stable-diffusion-webui-images-browser b/extensions-builtin/stable-diffusion-webui-images-browser index b2f6e4cbe..9229ed5e7 160000 --- a/extensions-builtin/stable-diffusion-webui-images-browser +++ b/extensions-builtin/stable-diffusion-webui-images-browser @@ -1 +1 @@ -Subproject commit b2f6e4cbe507078e3bf2b9d5fce6f83e2e233aa8 +Subproject commit 9229ed5e7e15608ec035d89cc5b2d19c3e629142 diff --git a/javascript/imageViewer.js b/javascript/imageViewer.js index 8d22b66f2..7b244549f 100644 --- a/javascript/imageViewer.js +++ b/javascript/imageViewer.js @@ -38,7 +38,6 @@ function modalSaveImage(event) { } function modalKeyHandler(event) { - console.log('HERE2', event.key); switch (event.key) { case 's': modalSaveImage(); diff --git a/modules/img2img.py b/modules/img2img.py index b744c8281..6b484db54 100644 --- a/modules/img2img.py +++ b/modules/img2img.py @@ -63,7 +63,7 @@ def process_batch(p, input_dir, output_dir, inpaint_mask_dir, args): shared.log.debug(f'Processed: {len(images)} Memory: {memory_stats()} batch') -def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_styles, init_img, sketch, init_img_with_mask, inpaint_color_sketch, inpaint_color_sketch_orig, init_img_inpaint, init_mask_inpaint, steps: int, sampler_index: int, mask_blur: int, mask_alpha: float, inpainting_fill: int, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, image_cfg_scale: float, clip_skip: int, denoising_strength: float, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, seed_enable_extras: bool, selected_scale_tab: int, height: int, width: int, scale_by: float, resize_mode: int, inpaint_full_res: bool, inpaint_full_res_padding: int, inpainting_mask_invert: int, img2img_batch_input_dir: str, img2img_batch_output_dir: str, img2img_batch_inpaint_mask_dir: str, override_settings_texts, *args): # pylint: disable=unused-argument +def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_styles, init_img, sketch, init_img_with_mask, inpaint_color_sketch, inpaint_color_sketch_orig, init_img_inpaint, init_mask_inpaint, steps: int, sampler_index: int, latent_index: int, mask_blur: int, mask_alpha: float, inpainting_fill: int, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, image_cfg_scale: float, diffusers_guidance_rescale: float, refiner_denoise_start: float, refiner_denoise_end: float, clip_skip: int, denoising_strength: float, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, seed_enable_extras: bool, selected_scale_tab: int, height: int, width: int, scale_by: float, resize_mode: int, inpaint_full_res: bool, inpaint_full_res_padding: int, inpainting_mask_invert: int, img2img_batch_input_dir: str, img2img_batch_output_dir: str, img2img_batch_inpaint_mask_dir: str, override_settings_texts, *args): # pylint: disable=unused-argument if shared.sd_model is None: shared.log.warning('Model not loaded') @@ -72,7 +72,7 @@ def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_s if init_img is None: shared.log.debug('Init image not set') - shared.log.debug(f'img2img: id_task={id_task}|mode={mode}|prompt={prompt}|negative_prompt={negative_prompt}|prompt_styles={prompt_styles}|init_img={init_img}|sketch={sketch}|init_img_with_mask={init_img_with_mask}|inpaint_color_sketch={inpaint_color_sketch}|inpaint_color_sketch_orig={inpaint_color_sketch_orig}|init_img_inpaint={init_img_inpaint}|init_mask_inpaint={init_mask_inpaint}|steps={steps}|sampler_index={sampler_index}|mask_blur={mask_blur}|mask_alpha={mask_alpha}|inpainting_fill={inpainting_fill}|restore_faces={restore_faces}|tiling={tiling}|n_iter={n_iter}|batch_size={batch_size}|cfg_scale={cfg_scale}|image_cfg_scale={image_cfg_scale}|clip_skip={clip_skip}|denoising_strength={denoising_strength}|seed={seed}|subseed{subseed}|subseed_strength={subseed_strength}|seed_resize_from_h={seed_resize_from_h}|seed_resize_from_w={seed_resize_from_w}|seed_enable_extras={seed_enable_extras}|selected_scale_tab={selected_scale_tab}|height={height}|width={width}|scale_by={scale_by}|resize_mode={resize_mode}|inpaint_full_res={inpaint_full_res}|inpaint_full_res_padding={inpaint_full_res_padding}|inpainting_mask_invert={inpainting_mask_invert}|img2img_batch_input_dir={img2img_batch_input_dir}|img2img_batch_output_dir={img2img_batch_output_dir}|img2img_batch_inpaint_mask_dir={img2img_batch_inpaint_mask_dir}|override_settings_texts={override_settings_texts}|args={args}') + shared.log.debug(f'img2img: id_task={id_task}|mode={mode}|prompt={prompt}|negative_prompt={negative_prompt}|prompt_styles={prompt_styles}|init_img={init_img}|sketch={sketch}|init_img_with_mask={init_img_with_mask}|inpaint_color_sketch={inpaint_color_sketch}|inpaint_color_sketch_orig={inpaint_color_sketch_orig}|init_img_inpaint={init_img_inpaint}|init_mask_inpaint={init_mask_inpaint}|steps={steps}|sampler_index={sampler_index}|latent_index={latent_index}|mask_blur={mask_blur}|mask_alpha={mask_alpha}|inpainting_fill={inpainting_fill}|restore_faces={restore_faces}|tiling={tiling}|n_iter={n_iter}|batch_size={batch_size}|cfg_scale={cfg_scale}|image_cfg_scale={image_cfg_scale}|clip_skip={clip_skip}|denoising_strength={denoising_strength}|seed={seed}|subseed{subseed}|subseed_strength={subseed_strength}|seed_resize_from_h={seed_resize_from_h}|seed_resize_from_w={seed_resize_from_w}|seed_enable_extras={seed_enable_extras}|selected_scale_tab={selected_scale_tab}|height={height}|width={width}|scale_by={scale_by}|resize_mode={resize_mode}|inpaint_full_res={inpaint_full_res}|inpaint_full_res_padding={inpaint_full_res_padding}|inpainting_mask_invert={inpainting_mask_invert}|img2img_batch_input_dir={img2img_batch_input_dir}|img2img_batch_output_dir={img2img_batch_output_dir}|img2img_batch_inpaint_mask_dir={img2img_batch_inpaint_mask_dir}|override_settings_texts={override_settings_texts}|args={args}') if sampler_index is None: shared.log.warning('Selected sampler is not enabled') @@ -139,6 +139,7 @@ def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_s seed_resize_from_w=seed_resize_from_w, seed_enable_extras=True, sampler_name=sd_samplers.samplers_for_img2img[sampler_index].name, + latent_sampler=sd_samplers.samplers[latent_index].name, batch_size=batch_size, n_iter=n_iter, steps=steps, @@ -155,6 +156,9 @@ def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_s resize_mode=resize_mode, denoising_strength=denoising_strength, image_cfg_scale=image_cfg_scale, + diffusers_guidance_rescale=diffusers_guidance_rescale, + refiner_denoise_start=refiner_denoise_start, + refiner_denoise_end=refiner_denoise_end, inpaint_full_res=inpaint_full_res, inpaint_full_res_padding=inpaint_full_res_padding, inpainting_mask_invert=inpainting_mask_invert, diff --git a/modules/processing.py b/modules/processing.py index 79a3295b0..932748082 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -89,7 +89,7 @@ class StableDiffusionProcessing: """ The first set of paramaters: sd_models -> do_not_reload_embeddings represent the minimum required to create a StableDiffusionProcessing """ - def __init__(self, sd_model=None, outpath_samples=None, outpath_grids=None, prompt: str = "", styles: List[str] = None, seed: int = -1, subseed: int = -1, subseed_strength: float = 0, seed_resize_from_h: int = -1, seed_resize_from_w: int = -1, seed_enable_extras: bool = True, sampler_name: str = None, batch_size: int = 1, n_iter: int = 1, steps: int = 50, cfg_scale: float = 7.0, clip_skip: int = 1, width: int = 512, height: int = 512, restore_faces: bool = False, tiling: bool = False, do_not_save_samples: bool = False, do_not_save_grid: bool = False, extra_generation_params: Dict[Any, Any] = None, overlay_images: Any = None, negative_prompt: str = None, eta: float = None, do_not_reload_embeddings: bool = False, denoising_strength: float = 0, ddim_discretize: str = None, s_min_uncond: float = 0.0, s_churn: float = 0.0, s_tmax: float = None, s_tmin: float = 0.0, s_noise: float = 1.0, override_settings: Dict[str, Any] = None, override_settings_restore_afterwards: bool = True, sampler_index: int = None, script_args: list = None): # pylint: disable=unused-argument + def __init__(self, sd_model=None, outpath_samples=None, outpath_grids=None, prompt: str = "", styles: List[str] = None, seed: int = -1, subseed: int = -1, subseed_strength: float = 0, seed_resize_from_h: int = -1, seed_resize_from_w: int = -1, seed_enable_extras: bool = True, sampler_name: str = None, latent_sampler: str = None, batch_size: int = 1, n_iter: int = 1, steps: int = 50, cfg_scale: float = 7.0, image_cfg_scale: float = None, clip_skip: int = 1, width: int = 512, height: int = 512, restore_faces: bool = False, tiling: bool = False, do_not_save_samples: bool = False, do_not_save_grid: bool = False, extra_generation_params: Dict[Any, Any] = None, overlay_images: Any = None, negative_prompt: str = None, eta: float = None, do_not_reload_embeddings: bool = False, denoising_strength: float = 0, diffusers_guidance_rescale: float = 0.7, ddim_discretize: str = None, s_min_uncond: float = 0.0, s_churn: float = 0.0, s_tmax: float = None, s_tmin: float = 0.0, s_noise: float = 1.0, override_settings: Dict[str, Any] = None, override_settings_restore_afterwards: bool = True, sampler_index: int = None, script_args: list = None): # pylint: disable=unused-argument self.outpath_samples: str = outpath_samples self.outpath_grids: str = outpath_grids @@ -103,10 +103,13 @@ class StableDiffusionProcessing: self.seed_resize_from_h: int = seed_resize_from_h self.seed_resize_from_w: int = seed_resize_from_w self.sampler_name: str = sampler_name + self.latent_sampler: str = latent_sampler self.batch_size: int = batch_size self.n_iter: int = n_iter self.steps: int = steps self.cfg_scale: float = cfg_scale + self.image_cfg_scale = image_cfg_scale + self.diffusers_guidance_rescale = diffusers_guidance_rescale self.width: int = width self.height: int = height self.restore_faces: bool = restore_faces @@ -148,6 +151,7 @@ class StableDiffusionProcessing: self.clip_skip = clip_skip self.iteration = 0 self.is_hr_pass = False + self.refiner_denoise_start = 0 opts.data['clip_skip'] = clip_skip @property @@ -265,7 +269,7 @@ class Processed: self.height = p.height self.sampler_name = p.sampler_name self.cfg_scale = p.cfg_scale - self.image_cfg_scale = getattr(p, 'image_cfg_scale', None) + self.image_cfg_scale = p.image_cfg_scale self.steps = p.steps self.batch_size = p.batch_size self.restore_faces = p.restore_faces @@ -449,13 +453,15 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts, all_seeds, all_su generation_params = { "Steps": p.steps, "Sampler": p.sampler_name, + "Latent sampler": p.latent_sampler, "CFG scale": p.cfg_scale, - "Image CFG scale": getattr(p, 'image_cfg_scale', None), + "Image CFG scale": p.image_cfg_scale, "Seed": all_seeds[index], "Face restoration": opts.face_restoration_model if p.restore_faces else None, "Size": f"{p.width}x{p.height}", "Model hash": getattr(p, 'sd_model_hash', None if not opts.add_model_hash_to_info or not shared.sd_model.sd_model_hash else shared.sd_model.sd_model_hash), "Model": None if not opts.add_model_name_to_info or not shared.sd_model.sd_checkpoint_info.model_name else shared.sd_model.sd_checkpoint_info.model_name.replace(',', '').replace(':', ''), + "Refiner": None if not opts.add_model_name_to_info or not shared.sd_refiner or not shared.sd_refiner.sd_checkpoint_info.model_name else shared.sd_refiner.sd_checkpoint_info.model_name.replace(',', '').replace(':', ''), "VAE": None if not opts.add_model_name_to_info or sd_vae.loaded_vae_file is None else os.path.splitext(os.path.basename(sd_vae.loaded_vae_file))[0], "Variation seed": None if p.subseed_strength == 0 else all_subseeds[index], "Variation seed strength": None if p.subseed_strength == 0 else p.subseed_strength, @@ -497,7 +503,7 @@ def print_profile(profile, msg: str): except Exception: pass profile.disable() - stream = io.StringIO() + stream = io.StringIO() # pylint: disable=abstract-class-instantiated ps = pstats.Stats(profile, stream=stream) ps.sort_stats(pstats.SortKey.CUMULATIVE).print_stats(15) profile = None @@ -618,7 +624,6 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: shared.state.sampling_step = step shared.state.sampling_steps = p.steps shared.state.current_latent = latents - shared.state.set_current_image() def set_pipeline_args(model, prompt, negative_prompt, **kwargs): args = {} @@ -641,14 +646,15 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: args['output_type'] = 'np' if 'callback_steps' in possible: args['callback_steps'] = 1 - if 'callback' in args: + if 'callback' in possible: args['callback'] = diffusers_callback if 'cross_attention_kwargs' in possible: args['cross_attention_kwargs'] = cross_attention_kwargs for arg in kwargs: if arg in possible: args[arg] = kwargs[arg] - log.debug(f'Diffuser pipeline: {pipeline.__class__.__name__} args={args.keys()}') + # log.debug(f'Diffuser pipeline: {pipeline.__class__.__name__} possible={possible}') + log.debug(f'Diffuser pipeline: {pipeline.__class__.__name__} set={args.keys()}') return args @@ -740,29 +746,46 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: # TODO(PVP): change out to latents once possible with `diffusers` task_specific_kwargs = {"image": p.init_images[0], "mask_image": p.image_mask, "strength": p.denoising_strength} - # TODO Diffusers processing is not using p.sample so second pass is ignored shared.sd_model.to(devices.device) pipe_args = set_pipeline_args( model=shared.sd_model, prompt=prompts, negative_prompt=negative_prompts, - output_type='np' if shared.sd_refiner is None else 'latent', + eta=shared.opts.eta_ddim, + guidance_rescale=p.diffusers_guidance_rescale, + # aesthetic_score=shared.opts.diffusers_aesthetics_score, + output_type='np' if (shared.sd_refiner is None or p.enable_hr is False) else 'latent', **task_specific_kwargs ) output = shared.sd_model(**pipe_args) # pylint: disable=not-callable - if shared.sd_refiner is not None: + + # TODO Diffusers processing is not using p.sample so second pass is ignored and we use this instead + if shared.sd_refiner is not None and p.enable_hr: if shared.opts.diffusers_move_base: shared.log.debug('Moving base model to CPU') shared.sd_model.to('cpu') + + if (not hasattr(shared.sd_refiner.scheduler, 'name')) or (shared.sd_refiner.scheduler.name != p.latent_sampler): + sampler = sd_samplers.all_samplers_map.get(p.latent_sampler, None) + if sampler is None: + sampler = sd_samplers.all_samplers_map.get("UniPC") + shared.sd_refiner.scheduler = sd_samplers.create_sampler(sampler.name, shared.sd_refiner) # TODO(Patrick): For wrapped pipelines this is currently a no-op + shared.sd_refiner.to(devices.device) devices.torch_gc() pipe_args = set_pipeline_args( model=shared.sd_refiner, prompt=[p.refiner_prompt] if len(p.refiner_prompt) > 0 else prompts, negative_prompt=[p.refiner_negative] if len(p.refiner_negative) > 0 else negative_prompts, - num_inference_steps=p.refiner_steps, - denoising_start=p.refiner_denoise, + num_inference_steps=p.hr_second_pass_steps, + eta=shared.opts.eta_ddim, + strength=p.denoising_strength, + guidance_scale=p.image_cfg_scale if p.image_cfg_scale is not None else p.cfg_scale, + guidance_rescale=p.diffusers_guidance_rescale, + # aesthetic_score=shared.opts.diffusers_aesthetics_score, + denoising_start=p.refiner_denoise_start, + denoising_end=p.refiner_denoise_end, image=output.images[0], output_type='np' ) @@ -884,7 +907,7 @@ def old_hires_fix_first_pass_dimensions(width, height): class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): sampler = None - def __init__(self, enable_hr: bool = False, denoising_strength: float = 0.75, firstphase_width: int = 0, firstphase_height: int = 0, hr_scale: float = 2.0, hr_upscaler: str = None, hr_second_pass_steps: int = 0, hr_resize_x: int = 0, hr_resize_y: int = 0, refiner_steps: int = 0, refiner_denoise: int = 0, refiner_prompt: str = '', refiner_negative: str = '', **kwargs): + def __init__(self, enable_hr: bool = False, denoising_strength: float = 0.75, firstphase_width: int = 0, firstphase_height: int = 0, hr_scale: float = 2.0, hr_upscaler: str = None, hr_second_pass_steps: int = 0, hr_resize_x: int = 0, hr_resize_y: int = 0, refiner_denoise_start: float = 0, refiner_denoise_end: float = 0, refiner_prompt: str = '', refiner_negative: str = '', **kwargs): super().__init__(**kwargs) self.enable_hr = enable_hr @@ -904,8 +927,8 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): self.truncate_x = 0 self.truncate_y = 0 self.applied_old_hires_behavior_to = None - self.refiner_steps = refiner_steps - self.refiner_denoise = refiner_denoise + self.refiner_denoise_start = refiner_denoise_start + self.refiner_denoise_end = refiner_denoise_end self.refiner_prompt = refiner_prompt self.refiner_negative = refiner_negative @@ -1058,12 +1081,12 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): sampler = None - def __init__(self, init_images: list = None, resize_mode: int = 0, denoising_strength: float = 0.75, image_cfg_scale: float = None, mask: Any = None, mask_blur: int = 4, inpainting_fill: int = 0, inpaint_full_res: bool = True, inpaint_full_res_padding: int = 0, inpainting_mask_invert: int = 0, initial_noise_multiplier: float = None, **kwargs): + def __init__(self, init_images: list = None, resize_mode: int = 0, denoising_strength: float = 0.75, image_cfg_scale: float = None, mask: Any = None, mask_blur: int = 4, inpainting_fill: int = 0, inpaint_full_res: bool = True, inpaint_full_res_padding: int = 0, inpainting_mask_invert: int = 0, initial_noise_multiplier: float = None, refiner_denoise_start: float = 0, refiner_denoise_end: float = 0, refiner_prompt: str = '', refiner_negative: str = '', **kwargs): super().__init__(**kwargs) self.init_images = init_images self.resize_mode: int = resize_mode self.denoising_strength: float = denoising_strength - self.image_cfg_scale: float = image_cfg_scale if (shared.sd_model is not None) and hasattr(shared.sd_model, 'cond_stage_key') and (shared.sd_model.cond_stage_key == "edit") else None + self.image_cfg_scale: float = image_cfg_scale self.init_latent = None self.image_mask = mask self.latent_mask = None @@ -1077,6 +1100,11 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): self.mask = None self.nmask = None self.image_conditioning = None + self.refiner_denoise_start = refiner_denoise_start + self.refiner_denoise_end = refiner_denoise_end + self.refiner_prompt = refiner_prompt + self.refiner_negative = refiner_negative + def init(self, all_prompts, all_seeds, all_subseeds): image_mask = self.image_mask diff --git a/modules/sd_samplers_diffusers.py b/modules/sd_samplers_diffusers.py index 6af0b9b8b..d8190028e 100644 --- a/modules/sd_samplers_diffusers.py +++ b/modules/sd_samplers_diffusers.py @@ -14,6 +14,7 @@ try: # KDPM2DiscreteScheduler, PNDMScheduler, UniPCMultistepScheduler, + LMSDiscreteScheduler, ) except Exception as e: import diffusers @@ -32,6 +33,7 @@ config = { 'PNDM': { 'skip_prk_steps': False, 'set_alpha_to_one': False, 'steps_offset': 0 }, 'DPM 1S': { 'solver_order': 2, 'thresholding': False, 'sample_max_value': 1.0, 'algorithm_type': "dpmsolver++", 'solver_type': "midpoint", 'lower_order_final': True, 'use_karras_sigmas': False }, 'DPM 2M': { 'thresholding': False, 'sample_max_value': 1.0, 'algorithm_type': "dpmsolver++", 'solver_type': "midpoint", 'lower_order_final': True, 'use_karras_sigmas': False }, + 'LMSD': { 'use_karras_sigmas': False, 'timestep_spacing': 'linspace', 'steps_offset': 0 }, } samplers_data_diffusers = [ @@ -45,6 +47,7 @@ samplers_data_diffusers = [ sd_samplers_common.SamplerData('Euler a', lambda model: DiffusionSampler('Euler a', EulerAncestralDiscreteScheduler, model), [], {}), sd_samplers_common.SamplerData('Heun', lambda model: DiffusionSampler('Heun', HeunDiscreteScheduler, model), [], {}), sd_samplers_common.SamplerData('PNDM', lambda model: DiffusionSampler('PNDM', PNDMScheduler, model), [], {}), + sd_samplers_common.SamplerData('LMSD', lambda model: DiffusionSampler('LMSD', LMSDiscreteScheduler, model), [], {}), ] class DiffusionSampler: diff --git a/modules/shared.py b/modules/shared.py index 19f461d43..f29e1434e 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -66,12 +66,12 @@ restricted_opts = { ui_reorder_categories = [ "inpaint", "sampler", - "checkboxes", - "hires_fix", "dimensions", "cfg", "seed", "batch", + "checkboxes", + "second_pass", "override_settings", "scripts", ] @@ -370,6 +370,8 @@ options_templates.update(options_section(('diffusers', "Diffusers Settings"), { "diffusers_vae_slicing": OptionInfo(False, "Enable VAE slicing"), "diffusers_vae_tiling": OptionInfo(False, "Enable VAE tiling"), "diffusers_attention_slicing": OptionInfo(False, "Enable attention slicing"), + # "diffusers_force_zeros": OptionInfo(False, "Force zeros for prompts when empty"), + # "diffusers_aesthetics_score": OptionInfo(6.0, "Require aesthetic score", gr.Slider, {"minimum": 0, "maximum": 10, "step": 0.1}), })) options_templates.update(options_section(('system-paths', "System Paths"), { @@ -488,10 +490,11 @@ options_templates.update(options_section(('live-preview', "Live Previews"), { options_templates.update(options_section(('sampler-params', "Sampler Settings"), { "show_samplers": OptionInfo(["Euler a", "UniPC", "DEIS", "DDIM", "DPM 1S", "DPM 2M", "DPM++ 2M SDE", "DPM++ 2M SDE Karras", "DPM2 Karras", "DPM++ 2M Karras"], "Show samplers in user interface", gr.CheckboxGroup, lambda: {"choices": [x.name for x in list_samplers() if x.name != "PLMS"]}), "fallback_sampler": OptionInfo("Euler a", "Secondary sampler", gr.Dropdown, lambda: {"choices": ["None"] + [x.name for x in list_samplers()]}), - "force_latent_sampler": OptionInfo("None", "Force latent upscaler sampler", gr.Dropdown, lambda: {"choices": ["None"] + [x.name for x in list_samplers()]}), + # "force_latent_sampler": OptionInfo("None", "Force latent upscaler sampler", gr.Dropdown, lambda: {"choices": ["None"] + [x.name for x in list_samplers()]}), 'uni_pc_variant': OptionInfo("bh1", "UniPC variant", gr.Radio, {"choices": ["bh1", "bh2", "vary_coeff"]}), 'uni_pc_skip_type': OptionInfo("time_uniform", "UniPC skip type", gr.Radio, {"choices": ["time_uniform", "time_quadratic", "logSNR"]}), 'eta_noise_seed_delta': OptionInfo(0, "Noise seed delta (eta)", gr.Number, {"precision": 0}), + "eta_ddim": OptionInfo(0.0, "Noise multiplier for DDIM (eta)", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}), "schedulers_sep_diffusers": OptionInfo("

Diffusers specific config

", "", gr.HTML), "schedulers_prediction_type": OptionInfo("default", "Samplers override model prediction type", gr.Radio, lambda: {"choices": ['default', 'epsilon', 'sample', 'v-prediction']}), @@ -514,7 +517,6 @@ options_templates.update(options_section(('sampler-params', "Sampler Settings"), "schedulers_sep_compvis": OptionInfo("

CompVis specific config

", "", gr.HTML), "ddim_discretize": OptionInfo('uniform', "DDIM discretize img2img", gr.Radio, {"choices": ['uniform', 'quad']}), - "eta_ddim": OptionInfo(0.0, "Noise multiplier for DDIM (eta)", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}), })) options_templates.update(options_section(('postprocessing', "Postprocessing"), { @@ -746,7 +748,7 @@ cmd_opts.disable_extension_access = (cmd_opts.share or cmd_opts.listen or (cmd_o devices.device, devices.device_interrogate, devices.device_gfpgan, devices.device_esrgan, devices.device_codeformer = (devices.cpu if any(y in cmd_opts.use_cpu for y in [x, 'all']) else devices.get_optimal_device() for x in ['sd', 'interrogate', 'gfpgan', 'esrgan', 'codeformer']) device = devices.device batch_cond_uncond = opts.always_batch_cond_uncond or not (cmd_opts.lowvram or cmd_opts.medvram) -parallel_processing_allowed = not cmd_opts.lowvram and not cmd_opts.medvram +parallel_processing_allowed = not cmd_opts.lowvram mem_mon = modules.memmon.MemUsageMonitor("MemMon", device, opts) mem_mon.start() if device.type == 'privateuseone': diff --git a/modules/txt2img.py b/modules/txt2img.py index fa8b6d5df..d06d60dae 100644 --- a/modules/txt2img.py +++ b/modules/txt2img.py @@ -5,9 +5,9 @@ from modules.ui import plaintext_to_html from modules.memstats import memory_stats -def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, steps: int, sampler_index: int, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, clip_skip: int, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, seed_enable_extras: bool, height: int, width: int, enable_hr: bool, denoising_strength: float, hr_scale: float, hr_upscaler: str, hr_second_pass_steps: int, hr_resize_x: int, hr_resize_y: int, refiner_steps: int, refiner_denoise: float, refiner_prompt: str, refiner_negative: str, override_settings_texts, *args): # pylint: disable=unused-argument +def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, steps: int, sampler_index: int, latent_index: int, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, image_cfg_scale: float, diffusers_guidance_rescale: float, clip_skip: int, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, seed_enable_extras: bool, height: int, width: int, enable_hr: bool, denoising_strength: float, hr_scale: float, hr_upscaler: str, hr_second_pass_steps: int, hr_resize_x: int, hr_resize_y: int, refiner_denoise_start: int, refiner_denoise_end: float, refiner_prompt: str, refiner_negative: str, override_settings_texts, *args): # pylint: disable=unused-argument - shared.log.debug(f'txt2img: id_task={id_task}|prompt={prompt}|negative_prompt={negative_prompt}|prompt_styles={prompt_styles}|steps={steps}|sampler_index={sampler_index}|restore_faces={restore_faces}|tiling={tiling}|n_iter={n_iter}|batch_size={batch_size}|cfg_scale={cfg_scale}|clip_skip={clip_skip}|seed={seed}|subseed={subseed}|subseed_strength={subseed_strength}|seed_resize_from_h={seed_resize_from_h}|seed_resize_from_w={seed_resize_from_w}|seed_enable_extras={seed_enable_extras}|height={height}|width={width}|enable_hr={enable_hr}|denoising_strength={denoising_strength}|hr_scale={hr_scale}|hr_upscaler={hr_upscaler}|hr_second_pass_steps={hr_second_pass_steps}|hr_resize_x={hr_resize_x}|hr_resize_y={hr_resize_y}|refiner_steps={refiner_steps}|refiner_denoise={refiner_denoise}|refiner_prompt={refiner_prompt}|refiner_negative={refiner_negative}|override_settings_texts={override_settings_texts}args={args}') + shared.log.debug(f'txt2img: id_task={id_task}|prompt={prompt}|negative_prompt={negative_prompt}|prompt_styles={prompt_styles}|steps={steps}|sampler_index={sampler_index}|latent_index={latent_index}|restore_faces={restore_faces}|tiling={tiling}|n_iter={n_iter}|batch_size={batch_size}|cfg_scale={cfg_scale}|clip_skip={clip_skip}|seed={seed}|subseed={subseed}|subseed_strength={subseed_strength}|seed_resize_from_h={seed_resize_from_h}|seed_resize_from_w={seed_resize_from_w}|seed_enable_extras={seed_enable_extras}|height={height}|width={width}|enable_hr={enable_hr}|denoising_strength={denoising_strength}|hr_scale={hr_scale}|hr_upscaler={hr_upscaler}|hr_second_pass_steps={hr_second_pass_steps}|hr_resize_x={hr_resize_x}|hr_resize_y={hr_resize_y}|image_cfg_scale={image_cfg_scale}|diffusers_guidance_rescale={diffusers_guidance_rescale}|refiner_denoise_start={refiner_denoise_start}|refiner_denoise_end={refiner_denoise_end}|refiner_prompt={refiner_prompt}|refiner_negative={refiner_negative}|override_settings_texts={override_settings_texts}args={args}') if sampler_index is None: shared.log.warning('Selected sampler is not enabled') sampler_index = 0 @@ -31,10 +31,13 @@ def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, step seed_resize_from_w=seed_resize_from_w, seed_enable_extras=True, sampler_name=sd_samplers.samplers[sampler_index].name, + latent_sampler=sd_samplers.samplers[latent_index].name, batch_size=batch_size, n_iter=n_iter, steps=steps, cfg_scale=cfg_scale, + image_cfg_scale=image_cfg_scale, + diffusers_guidance_rescale=diffusers_guidance_rescale, clip_skip=clip_skip, width=width, height=height, @@ -47,8 +50,8 @@ def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, step hr_second_pass_steps=hr_second_pass_steps, hr_resize_x=hr_resize_x, hr_resize_y=hr_resize_y, - refiner_steps=refiner_steps, - refiner_denoise=refiner_denoise, + refiner_denoise_start=refiner_denoise_start, + refiner_denoise_end=refiner_denoise_end, refiner_prompt=refiner_prompt, refiner_negative=refiner_negative, override_settings=override_settings, diff --git a/modules/ui.py b/modules/ui.py index 8eb2f2e70..daf9c7dcb 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -89,10 +89,12 @@ def calc_resolution_hires(enable, width, height, hr_scale, hr_resize_x, hr_resiz from modules import processing, devices if not enable: return "" + if modules.shared.backend == modules.shared.Backend.DIFFUSERS: + return "Hires resize: disabled" p = processing.StableDiffusionProcessingTxt2Img(width=width, height=height, enable_hr=True, hr_scale=hr_scale, hr_resize_x=hr_resize_x, hr_resize_y=hr_resize_y) with devices.autocast(): p.init([""], [0], [0]) - return f"resize: from {p.width}x{p.height} to {p.hr_resize_x or p.hr_upscale_to_x}x{p.hr_resize_y or p.hr_upscale_to_y}" + return f"Hires resize: from {p.width}x{p.height} to {p.hr_resize_x or p.hr_upscale_to_x}x{p.hr_resize_y or p.hr_upscale_to_y}" def resize_from_to_html(width, height, scale_by): @@ -100,7 +102,9 @@ def resize_from_to_html(width, height, scale_by): target_height = int(height * scale_by) if not target_width or not target_height: return "no image selected" - return f"resize: from {width}x{height} to {target_width}x{target_height}" + if modules.shared.backend == modules.shared.Backend.DIFFUSERS: + return "Hires resize: disabled" + return f"Hires resize: from {width}x{height} to {target_width}x{target_height}" def apply_styles(prompt, prompt_neg, styles): @@ -367,43 +371,49 @@ def create_ui(startup_timer = None): batch_size = gr.Slider(minimum=1, maximum=32, step=1, label='Batch size', value=1, elem_id="txt2img_batch_size") elif category == "cfg": with FormRow(): - cfg_scale = gr.Slider(minimum=1.0, maximum=30.0, step=0.5, label='CFG Scale', value=6.0, elem_id="txt2img_cfg_scale") + cfg_scale = gr.Slider(minimum=1.0, maximum=30.0, step=0.1, label='CFG Scale', value=6.0, elem_id="txt2img_cfg_scale") clip_skip = gr.Slider(label='CLIP skip', value=1, minimum=1, maximum=14, step=1, elem_id='txt2img_clip_skip', interactive=True) elif category == "seed": seed, reuse_seed, subseed, reuse_subseed, subseed_strength, seed_resize_from_h, seed_resize_from_w, seed_checkbox = create_seed_inputs('txt2img') elif category == "checkboxes": with FormRow(elem_classes="checkboxes-row", variant="compact"): + second_pass = gr.Checkbox(label='Second pass', value=False, elem_id="txt2img_enable_hr") restore_faces = gr.Checkbox(label='Restore faces', value=False, visible=len(modules.shared.face_restorers) > 1, elem_id="txt2img_restore_faces") tiling = gr.Checkbox(label='Tiling', value=False, elem_id="txt2img_tiling") - enable_hr = gr.Checkbox(label='Hires fix', value=False, elem_id="txt2img_enable_hr") - enable_refiner = gr.Checkbox(label='Refiner', value=False, elem_id="txt2img_enable_refiner") - hr_final_resolution = FormHTML(value="", elem_id="txtimg_hr_finalres", label="Upscaled resolution", interactive=False) - elif category == "hires_fix": - with FormGroup(visible=False, elem_id="txt2img_hires_fix") as hr_options: + elif category == "second_pass": + with FormGroup(visible=False, elem_id="txt2img_second_pass") as hr_options: + hr_second_pass_steps, latent_index = create_sampler_and_steps_selection(modules.sd_samplers.samplers, "txt2img") with FormRow(elem_id="txt2img_hires_fix_row1", variant="compact"): denoising_strength = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label='Denoising strength', value=0.7, elem_id="txt2img_denoising_strength") - hr_second_pass_steps = gr.Slider(minimum=0, maximum=99, step=1, label='Hires steps', value=0, elem_id="txt2img_hires_steps") + + with FormRow(): + hr_final_resolution = FormHTML(value="", elem_id="txtimg_hr_finalres", label="Upscaled resolution", interactive=False) with FormRow(elem_id="txt2img_hires_fix_row2", variant="compact"): hr_upscaler = gr.Dropdown(label="Upscaler", elem_id="txt2img_hr_upscaler", choices=[*modules.shared.latent_upscale_modes, *[x.name for x in modules.shared.sd_upscalers]], value=modules.shared.latent_upscale_default_mode) hr_scale = gr.Slider(minimum=1.0, maximum=4.0, step=0.05, label="Upscale by", value=2.0, elem_id="txt2img_hr_scale") with FormRow(elem_id="txt2img_hires_fix_row3", variant="compact"): hr_resize_x = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize width to", value=0, elem_id="txt2img_hr_resize_x") hr_resize_y = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize height to", value=0, elem_id="txt2img_hr_resize_y") - with FormGroup(visible=False, elem_id="txt2img_hires_fix") as refiner_options: + + with FormRow(): + hr_refiner = FormHTML(value="Refiner", elem_id="txtimg_hr_finalres", interactive=False) with FormRow(elem_id="txt2img_refiner_row1", variant="compact"): - refiner_steps = gr.Slider(minimum=1, maximum=99, step=1, label='Steps', value=5, elem_id="txt2img_refiner_steps") - refiner_denoise = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label='Denoise start', value=0.5, elem_id="txt2img_refiner_denoise") + image_cfg_scale = gr.Slider(minimum=1.0, maximum=30.0, step=0.1, label='CFG Scale', value=6.0, elem_id="txt2img_image_cfg_scale") + diffusers_guidance_rescale = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label='Guidance rescale', value=0.7, elem_id="txt2img_image_cfg_rescale") + refiner_denoise_start = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label='Denoise start', value=0.0, elem_id="txt2img_refiner_denoise_start") + refiner_denoise_end = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label='Denoise end', value=1.0, elem_id="txt2img_refiner_denoise_end") with FormRow(elem_id="txt2img_refiner_row2", variant="compact"): refiner_prompt = gr.Textbox(value='', label='Prompt') with FormRow(elem_id="txt2img_refiner_row3", variant="compact"): refiner_negative = gr.Textbox(value='', label='Negative prompt') + elif category == "override_settings": with FormRow(elem_id="txt2img_override_settings_row") as row: override_settings = create_override_settings_dropdown('txt2img', row) elif category == "scripts": with FormGroup(elem_id="txt2img_script_container"): custom_inputs = modules.scripts.scripts_txt2img.setup_ui() - hr_resolution_preview_inputs = [enable_hr, width, height, hr_scale, hr_resize_x, hr_resize_y] + hr_resolution_preview_inputs = [second_pass, width, height, hr_scale, hr_resize_x, hr_resize_y] for preview_input in hr_resolution_preview_inputs: preview_input.change( fn=calc_resolution_hires, @@ -427,26 +437,29 @@ def create_ui(startup_timer = None): txt2img_prompt_styles, steps, sampler_index, + latent_index, restore_faces, tiling, batch_count, batch_size, cfg_scale, + image_cfg_scale, + diffusers_guidance_rescale, clip_skip, seed, subseed, subseed_strength, seed_resize_from_h, seed_resize_from_w, seed_checkbox, height, width, - enable_hr, + second_pass, denoising_strength, hr_scale, hr_upscaler, hr_second_pass_steps, hr_resize_x, hr_resize_y, - refiner_steps, - refiner_denoise, + refiner_denoise_start, + refiner_denoise_end, refiner_prompt, refiner_negative, override_settings, @@ -467,14 +480,17 @@ def create_ui(startup_timer = None): txt_prompt_img.change(fn=modules.images.image_data, inputs=[txt_prompt_img], outputs=[txt2img_prompt, txt_prompt_img]) - enable_hr.change(fn=lambda x: gr_show(x), inputs=[enable_hr], outputs=[hr_options], show_progress = False) - enable_refiner.change(fn=lambda x: gr_show(x), inputs=[enable_refiner], outputs=[refiner_options], show_progress = False) + def enable_hr_change(visible: bool): + return {"visible": visible, "__type__": "update"}, f'Refiner{": disabled" if modules.shared.sd_refiner is None else ""}' + + second_pass.change(enable_hr_change, inputs=[second_pass], outputs=[hr_options, hr_refiner], show_progress = False) txt2img_paste_fields = [ (txt2img_prompt, "Prompt"), (txt2img_negative_prompt, "Negative prompt"), (steps, "Steps"), (sampler_index, "Sampler"), + (latent_index, "Latent sampler"), (restore_faces, "Face restoration"), (cfg_scale, "CFG scale"), (clip_skip, "Clip skip"), @@ -487,7 +503,7 @@ def create_ui(startup_timer = None): (seed_resize_from_w, "Seed resize from-1"), (seed_resize_from_h, "Seed resize from-2"), (denoising_strength, "Denoising strength"), - (enable_hr, lambda d: "Denoising strength" in d), + (second_pass, lambda d: "Denoising strength" in d), (hr_options, lambda d: gr.Row.update(visible="Denoising strength" in d)), (hr_scale, "Hires upscale"), (hr_upscaler, "Hires upscaler"), @@ -658,14 +674,18 @@ def create_ui(startup_timer = None): with FormRow(elem_id="img2img_column_batch"): batch_count = gr.Slider(minimum=1, step=1, label='Batch count', value=1, elem_id="img2img_batch_count") batch_size = gr.Slider(minimum=1, maximum=8, step=1, label='Batch size', value=1, elem_id="img2img_batch_size") + clip_skip = gr.Slider(label='CLIP skip', value=1, minimum=1, maximum=4, step=1, elem_id='img2img_clip_skip', interactive=True) elif category == "cfg": with FormGroup(): with FormRow(): cfg_scale = gr.Slider(minimum=1.0, maximum=30.0, step=0.5, label='CFG Scale', value=6.0, elem_id="img2img_cfg_scale") - image_cfg_scale = gr.Slider(minimum=0, maximum=3.0, step=0.05, label='Image CFG Scale', value=1.5, elem_id="img2img_image_cfg_scale", visible=False) + image_cfg_scale = gr.Slider(minimum=0, maximum=30.0, step=0.05, label='Image CFG Scale', value=1.5, elem_id="img2img_image_cfg_scale") + diffusers_guidance_rescale = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label='CFG Scale', value=0.7, elem_id="txt2img_image_cfg_rescale") + with FormRow(): denoising_strength = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label='Denoising strength', value=0.75, elem_id="img2img_denoising_strength") - clip_skip = gr.Slider(label='CLIP skip', value=1, minimum=1, maximum=4, step=1, elem_id='img2img_clip_skip', interactive=True) + refiner_denoise_start = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label='Denoise start', value=0.0, elem_id="txt2img_refiner_denoise_start") + refiner_denoise_end = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label='Denoise end', value=1.0, elem_id="txt2img_refiner_denoise_end") elif category == "seed": seed, reuse_seed, subseed, reuse_subseed, subseed_strength, seed_resize_from_h, seed_resize_from_w, seed_checkbox = create_seed_inputs('img2img') @@ -746,6 +766,7 @@ def create_ui(startup_timer = None): init_mask_inpaint, steps, sampler_index, + latent_index, mask_blur, mask_alpha, inpainting_fill, @@ -755,6 +776,9 @@ def create_ui(startup_timer = None): batch_size, cfg_scale, image_cfg_scale, + diffusers_guidance_rescale, + refiner_denoise_start, + refiner_denoise_end, clip_skip, denoising_strength, seed, @@ -1146,11 +1170,6 @@ def create_ui(startup_timer = None): show_progress=info.refresh is not None, ) - # TODO breaks pix2pix needs better detect if pix2pix and image_cfg_scale_visibility should be on model change, not on ui create - # image_cfg_scale_visibility = (modules.shared.sd_model is not None) and hasattr(modules.shared.sd_model, 'cond_stage_key') and (modules.shared.sd_model.cond_stage_key == "edit") # pix2pix - # text_settings.change(fn=lambda: gr.update(visible=image_cfg_scale_visibility), inputs=[], outputs=[image_cfg_scale]) - # demo.load(fn=lambda: gr.update(visible=image_cfg_scale_visibility), inputs=[], outputs=[image_cfg_scale]) - button_set_checkpoint = gr.Button('Change checkpoint', elem_id='change_checkpoint', visible=False) button_set_checkpoint.click( fn=lambda value, _: run_settings_single(value, key='sd_model_checkpoint'), diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py index 866eae913..70723cc15 100644 --- a/scripts/xyz_grid.py +++ b/scripts/xyz_grid.py @@ -123,14 +123,6 @@ def apply_styles(p: StableDiffusionProcessingTxt2Img, x: str, _): p.styles.extend(x.split(',')) -def apply_fallback(p, x, xs): - sampler_name = sd_samplers.samplers_map.get(x.lower(), None) - if sampler_name is None: - shared.log.warning(f"XYZ grid: unknown sampler: {x}") - else: - shared.opts.data["force_latent_sampler"] = sampler_name - - def apply_schedulers_solver_order(p, x, xs): shared.opts.data["schedulers_solver_order"] = min(x, p.steps - 1) @@ -351,7 +343,6 @@ class SharedSettingsStackHelper(object): self.sd_model_checkpoint = shared.opts.sd_model_checkpoint self.sd_model_dict = shared.opts.sd_model_dict self.sd_vae_checkpoint = shared.opts.sd_vae - self.force_latent_sampler = shared.opts.force_latent_sampler def __exit__(self, exc_type, exc_value, tb): #Restore overriden settings after plot generation. @@ -359,7 +350,6 @@ class SharedSettingsStackHelper(object): shared.opts.data["schedulers_solver_order"] = self.schedulers_solver_order shared.opts.data["token_merging_ratio_hr"] = self.token_merging_ratio_hr shared.opts.data["token_merging_ratio"] = self.token_merging_ratio - shared.opts.data["force_latent_sampler"] = self.force_latent_sampler if self.sd_model_dict != shared.opts.sd_model_dict: shared.opts.data["sd_model_dict"] = self.sd_model_dict if self.sd_model_checkpoint != shared.opts.sd_model_checkpoint: diff --git a/wiki b/wiki index a0cc6ecfd..fea0bd7d5 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit a0cc6ecfd2de73815daabdfd4b85b5f376e84774 +Subproject commit fea0bd7d596e442f73727b46ea9343021724e032