diff --git a/CHANGELOG.md b/CHANGELOG.md index fcec700e5..9f9628ee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log for SD.Next +## Update for 2025-07-30 + +- **Feature** + - Wan select which stage to run: *first/second/both* with configurable *boundary ration* when running both stages + in settings -> model options + - prompt parser allow explict `BOS` and `EOS` tokens in prompt +- **UI** + - modernui checkbox/radio styling +- **Fixes** + - fix Wan 2.2-5B I2V workflow + - fix inpaint image metadata + - fix processing image save loop + - fix progress bar with refine/detailer + - fix api progress reporting endpoint + - add missing interrogate in output panel + ## Update for 2025-07-29 ### Highlights for 2025-07-29 diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 63d6d509b..9741e151b 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 63d6d509b83f57ff333d99744eeb0cd94d7d73f1 +Subproject commit 9741e151b01dda2d2697c8ca8a369e50482e976e diff --git a/installer.py b/installer.py index d3a2eacf0..5bf112e5c 100644 --- a/installer.py +++ b/installer.py @@ -593,7 +593,7 @@ def check_diffusers(): t_start = time.time() if args.skip_all or args.skip_git: return - sha = '56d438727036b0918b30bbe3110c5fe1634ed19d' # diffusers commit hash + sha = 'c052791b5fe29ce8a308bf63dda97aa205b729be' # diffusers commit hash pkg = pkg_resources.working_set.by_key.get('diffusers', None) minor = int(pkg.version.split('.')[1] if pkg is not None else -1) cur = opts.get('diffusers_version', '') if minor > -1 else '' diff --git a/modules/api/server.py b/modules/api/server.py index d495c1e3b..b459ed605 100644 --- a/modules/api/server.py +++ b/modules/api/server.py @@ -85,7 +85,7 @@ def get_history(req: models.ReqHistory = Depends()): return res def get_progress(req: models.ReqProgress = Depends()): - if shared.state.job_count == 0: + if shared.state.job_count == 0: # idle state return models.ResProgress(id=shared.state.id, progress=0, eta_relative=0, state=shared.state.dict(), textinfo=shared.state.textinfo) shared.state.do_set_current_image() current_image = None @@ -94,12 +94,17 @@ def get_progress(req: models.ReqProgress = Depends()): batch_x = max(shared.state.job_no, 0) batch_y = max(shared.state.job_count, 1) step_x = max(shared.state.sampling_step, 0) + prev_steps = max(shared.state.sampling_steps, 1) + while step_x > shared.state.sampling_steps: + shared.state.sampling_steps += prev_steps step_y = max(shared.state.sampling_steps, 1) current = step_y * batch_x + step_x total = step_y * batch_y progress = min((current / total) if current > 0 and total > 0 else 0, 1) time_since_start = time.time() - shared.state.time_start eta_relative = (time_since_start / progress) - time_since_start if progress > 0 else 0 + # shared.log.critical(f'get_progress: batch {batch_x}/{batch_y} step {step_x}/{step_y} current {current}/{total} time={time_since_start} eta={eta_relative}') + # shared.log.critical(shared.state) res = models.ResProgress(id=shared.state.id, progress=round(progress, 2), eta_relative=round(eta_relative, 2), current_image=current_image, textinfo=shared.state.textinfo, state=shared.state.dict(), ) return res diff --git a/modules/generation_parameters_copypaste.py b/modules/generation_parameters_copypaste.py index 75893417e..3d667d8d4 100644 --- a/modules/generation_parameters_copypaste.py +++ b/modules/generation_parameters_copypaste.py @@ -237,8 +237,10 @@ def connect_paste(button, local_paste_fields, input_comp, override_settings_comp if hasattr(output, "step") and type(output.step) == float: valtype = float debug(f'Paste: "{key}"="{v}" type={valtype} var={vars(output)}') - if valtype == bool and v == "False": - val = False + if valtype == bool: + val = False if v.lower() == "false" else True + elif valtype == list: + val = v if isinstance(v, list) else [item.strip() for item in v.split(',')] else: val = valtype(v) res.append(gr.update(value=val)) diff --git a/modules/img2img.py b/modules/img2img.py index 93229e308..d91daaca1 100644 --- a/modules/img2img.py +++ b/modules/img2img.py @@ -311,9 +311,9 @@ def img2img(id_task: str, state: str, mode: int, if mask: p.extra_generation_params["Mask blur"] = mask_blur p.extra_generation_params["Mask alpha"] = mask_alpha - p.extra_generation_params["Mask invert"] = inpainting_mask_invert - p.extra_generation_params["Mask area"] = inpaint_full_res p.extra_generation_params["Mask padding"] = inpaint_full_res_padding + p.extra_generation_params["Mask invert"] = ['masked', 'invert'][inpainting_mask_invert] + p.extra_generation_params["Mask area"] = ["full", "masked"][inpaint_full_res] p.is_batch = mode == 5 if p.is_batch: process_batch(p, img2img_batch_files, img2img_batch_input_dir, img2img_batch_output_dir, img2img_batch_inpaint_mask_dir, args) diff --git a/modules/interrogate/interrogate.py b/modules/interrogate/interrogate.py index ce3f75193..7f7befcf2 100644 --- a/modules/interrogate/interrogate.py +++ b/modules/interrogate/interrogate.py @@ -9,6 +9,7 @@ def interrogate(image): if isinstance(image, dict) and 'name' in image: image = Image.open(image['name']) if image is None: + shared.log.error('Interrogate: no image provided') return '' t0 = time.time() if shared.opts.interrogate_default_type == 'OpenCLiP': diff --git a/modules/model_quant.py b/modules/model_quant.py index c99ca8945..48bacf0a1 100644 --- a/modules/model_quant.py +++ b/modules/model_quant.py @@ -278,16 +278,16 @@ def load_quanto(msg='', silent=False): def upcast_non_layerwise_modules(model, dtype): # pylint: disable=unused-argument - from diffusers.hooks.layerwise_casting import SUPPORTED_PYTORCH_LAYERS + from diffusers.hooks.layerwise_casting import _GO_LC_SUPPORTED_PYTORCH_LAYERS model_children = list(model.children()) if not model_children: - if not isinstance(model, SUPPORTED_PYTORCH_LAYERS): + if not isinstance(model, _GO_LC_SUPPORTED_PYTORCH_LAYERS): model = model.to(dtype) return model for module in model_children: has_children = list(module.children()) if not has_children: - if not isinstance(module, SUPPORTED_PYTORCH_LAYERS): + if not isinstance(module, _GO_LC_SUPPORTED_PYTORCH_LAYERS): module = module.to(dtype) else: module = upcast_non_layerwise_modules(module, dtype) diff --git a/modules/processing.py b/modules/processing.py index 15d22e076..c574c814e 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -373,37 +373,37 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: image.info["parameters"] = info output_images.append(image) + for i, image in enumerate(output_images): is_grid = len(output_images) == p.batch_size * p.n_iter + 1 and i == 0 - for image in output_images: - # resize after - if p.selected_scale_tab_after == 1: - p.width_after, p.height_after = int(image.width * p.scale_by_after), int(image.height * p.scale_by_after) - if p.resize_mode_after != 0 and p.resize_name_after != 'None' and not is_grid: - image = images.resize_image(p.resize_mode_after, image, p.width_after, p.height_after, p.resize_name_after, context=p.resize_context_after) + # resize after + if p.selected_scale_tab_after == 1: + p.width_after, p.height_after = int(image.width * p.scale_by_after), int(image.height * p.scale_by_after) + if p.resize_mode_after != 0 and p.resize_name_after != 'None' and not is_grid: + image = images.resize_image(p.resize_mode_after, image, p.width_after, p.height_after, p.resize_name_after, context=p.resize_context_after) - # save images - if shared.opts.samples_save and not p.do_not_save_samples and p.outpath_samples is not None: - info = create_infotext(p, p.prompts, p.seeds, p.subseeds, index=i) - if isinstance(image, list): - for img in image: - images.save_image(img, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=info, p=p) # main save image - else: - images.save_image(image, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=info, p=p) # main save image + # save images + if shared.opts.samples_save and not p.do_not_save_samples and p.outpath_samples is not None: + info = create_infotext(p, p.prompts, p.seeds, p.subseeds, index=i) + if isinstance(image, list): + for img in image: + images.save_image(img, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=info, p=p) # main save image + else: + images.save_image(image, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=info, p=p) # main save image - if hasattr(p, 'mask_for_overlay') and p.mask_for_overlay and any([shared.opts.save_mask, shared.opts.save_mask_composite, shared.opts.return_mask, shared.opts.return_mask_composite]): - image_mask = p.mask_for_overlay.convert('RGB') - image1 = image.convert('RGBA').convert('RGBa') - image2 = Image.new('RGBa', image.size) - mask = images.resize_image(3, p.mask_for_overlay, image.width, image.height).convert('L') - image_mask_composite = Image.composite(image1, image2, mask).convert('RGBA') - if shared.opts.save_mask: - images.save_image(image_mask, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=info, p=p, suffix="-mask") - if shared.opts.save_mask_composite: - images.save_image(image_mask_composite, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=info, p=p, suffix="-mask-composite") - if shared.opts.return_mask: - output_images.append(image_mask) - if shared.opts.return_mask_composite: - output_images.append(image_mask_composite) + if hasattr(p, 'mask_for_overlay') and p.mask_for_overlay and any([shared.opts.save_mask, shared.opts.save_mask_composite, shared.opts.return_mask, shared.opts.return_mask_composite]): + image_mask = p.mask_for_overlay.convert('RGB') + image1 = image.convert('RGBA').convert('RGBa') + image2 = Image.new('RGBa', image.size) + mask = images.resize_image(3, p.mask_for_overlay, image.width, image.height).convert('L') + image_mask_composite = Image.composite(image1, image2, mask).convert('RGBA') + if shared.opts.save_mask: + images.save_image(image_mask, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=info, p=p, suffix="-mask") + if shared.opts.save_mask_composite: + images.save_image(image_mask_composite, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=info, p=p, suffix="-mask-composite") + if shared.opts.return_mask: + output_images.append(image_mask) + if shared.opts.return_mask_composite: + output_images.append(image_mask_composite) timer.process.record('post') del samples diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 464d349eb..41ed7834c 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -54,7 +54,7 @@ def restore_state(p: processing.StableDiffusionProcessing): def process_pre(p: processing.StableDiffusionProcessing): from modules import ipadapter, sd_hijack_freeu, para_attention, teacache, hidiffusion, ras, pag, cfgzero, transformer_cache, token_merge, linfusion - shared.log.info('Processing apply modifiers') + shared.log.info('Processing modifiers: apply') try: # apply-with-unapply @@ -86,7 +86,7 @@ def process_pre(p: processing.StableDiffusionProcessing): def process_post(p: processing.StableDiffusionProcessing): from modules import ipadapter, hidiffusion, ras, pag, cfgzero, token_merge, linfusion - shared.log.info('Processing unapply modifiers') + shared.log.info('Processing modifiers: unapply') try: sd_models_compile.check_deepcache(enable=False) diff --git a/modules/processing_helpers.py b/modules/processing_helpers.py index 17d1f52c7..4777463ba 100644 --- a/modules/processing_helpers.py +++ b/modules/processing_helpers.py @@ -507,7 +507,7 @@ def update_sampler(p, sd_model, second_pass=False): def get_job_name(p, model): if hasattr(model, 'pipe'): model = model.pipe - if hasattr(p, 'xyz'): + if getattr(p, 'xyz', False): return 'Ignore' # xyz grid handles its own jobs if sd_models.get_diffusers_task(model) == sd_models.DiffusersTaskType.TEXT_2_IMAGE: return 'Text' diff --git a/modules/processing_info.py b/modules/processing_info.py index 529af75a9..eb6e9c3bb 100644 --- a/modules/processing_info.py +++ b/modules/processing_info.py @@ -117,29 +117,28 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts=None, all_seeds=No args["Init image size"] = f"{getattr(p, 'init_img_width', 0)}x{getattr(p, 'init_img_height', 0)}" args["Init image hash"] = getattr(p, 'init_img_hash', None) args['Image CFG scale'] = p.image_cfg_scale - args['Resize scale'] = getattr(p, 'scale_by', None) args["Mask weight"] = getattr(p, "inpainting_mask_weight", shared.opts.inpainting_mask_weight) if p.is_using_inpainting_conditioning else None args["Denoising strength"] = getattr(p, 'denoising_strength', None) + if args["Size"] != args["Init image size"]: + args['Resize scale'] = float(getattr(p, 'scale_by', None)) if getattr(p, 'scale_by', None) != 1 else None + args['Resize mode'] = shared.resize_modes[p.resize_mode] if shared.resize_modes[p.resize_mode] != 'None' else None if args["Size"] is None: args["Size"] = args["Init image size"] - # lookup by index - if getattr(p, 'resize_mode', None) is not None: - args['Resize mode'] = shared.resize_modes[p.resize_mode] if shared.resize_modes[p.resize_mode] != 'None' else None if p.resize_mode_before != 0 and p.resize_name_before != 'None' and hasattr(p, 'init_images') and p.init_images is not None and len(p.init_images) > 0: args['Resize before'] = f"{p.width_before}x{p.height_before}" args['Resize mode before'] = p.resize_mode_before args['Resize name before'] = p.resize_name_before - args['Resize scale before'] = p.scale_by_before if p.scale_by_before != 1.0 else None + args['Resize scale before'] = float(p.scale_by_before) if p.scale_by_before != 1.0 else None if p.resize_mode_after != 0 and p.resize_name_after != 'None': args['Resize after'] = f"{p.width_after}x{p.height_after}" args['Resize mode after'] = p.resize_mode_after args['Resize name after'] = p.resize_name_after - args['Resize scale after'] = p.scale_by_after if p.scale_by_after != 1.0 else None + args['Resize scale after'] = float(p.scale_by_after) if p.scale_by_after != 1.0 else None if p.resize_name_mask != 'None' and p.scale_by_mask != 1.0: args['Resize mask'] = f"{p.width_mask}x{p.height_mask}" args['Resize mode mask'] = p.resize_mode_mask args['Resize name mask'] = p.resize_name_mask - args['Resize scale mask'] = p.scale_by_mask + args['Resize scale mask'] = float(p.scale_by_mask) if 'detailer' in p.ops: args["Detailer"] = ', '.join(shared.opts.detailer_models) if len(shared.opts.detailer_args) == 0 else shared.opts.detailer_args args["Detailer steps"] = p.detailer_steps diff --git a/modules/prompt_parser_diffusers.py b/modules/prompt_parser_diffusers.py index a2c0f4b8b..a4f2fd4b6 100644 --- a/modules/prompt_parser_diffusers.py +++ b/modules/prompt_parser_diffusers.py @@ -360,6 +360,8 @@ def get_prompt_schedule(prompt, steps): def get_tokens(pipe, msg, prompt): global token_dict, token_type # pylint: disable=global-statement if shared.sd_loaded and hasattr(pipe, 'tokenizer') and pipe.tokenizer is not None: + prompt = prompt.replace(' BOS ', ' !!!!!!!! ').replace(' EOS ', ' !!!!!!! ') + debug(f'Prompt tokenizer: type={msg} prompt="{prompt}"') if token_dict is None or token_type != shared.sd_model_type: token_type = shared.sd_model_type fn = pipe.tokenizer.name_or_path @@ -375,6 +377,12 @@ def get_tokens(pipe, msg, prompt): has_eos_token = pipe.tokenizer.eos_token_id is not None ids = pipe.tokenizer(prompt) ids = getattr(ids, 'input_ids', []) + if has_bos_token and has_eos_token: + for i in range(len(ids)): + if ids[i] == 21622: + ids[i] = pipe.tokenizer.bos_token_id + elif ids[i] == 15203: + ids[i] = pipe.tokenizer.eos_token_id tokens = [] for i in ids: try: @@ -383,7 +391,7 @@ def get_tokens(pipe, msg, prompt): except Exception: tokens.append(f'UNK_{i}') token_count = len(ids) - int(has_bos_token) - int(has_eos_token) - debug(f'Prompt tokenizer: type={msg} tokens={token_count} {tokens}') + debug(f'Prompt tokenizer: type={msg} tokens={token_count} tokens={tokens} ids={ids}') return token_count diff --git a/modules/shared.py b/modules/shared.py index 60bc59da1..2769ff361 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -200,7 +200,8 @@ options_templates.update(options_section(('model_options', "Models Options"), { "model_h1_sep": OptionInfo("