diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a164f4..d1b304b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,12 +8,12 @@ repos: - id: mixed-line-ending - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.0.292" + rev: "v0.1.3" hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black diff --git a/CHANGELOG.md b/CHANGELOG.md index a2ae99b..e9baf80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 2023-10-30 + +- v23.11.0 +- 이미지의 인덱스 계산방법 변경 + - webui 1.1.0 미만에서 adetailer 실행 불가능하게 함 +- 컨트롤넷 preprocessor 선택지 늘림 +- 추가 yolo 모델 디렉터리를 설정할 수 있는 옵션 추가 +- infotext에 `/`가 있는 항목이 exif에서 복원되지 않는 문제 수정 + - 이전 버전에 생성된 이미지는 여전히 복원안됨 +- 같은 탭에서 항상 같은 시드를 적용하게 하는 옵션 추가 +- 컨트롤넷 1.1.411 (f2aafcf2beb99a03cbdf7db73852228ccd6bd1d6) 버전을 사용중일 경우, + webui 버전 1.6.0 미만에서 사용할 수 없다는 메세지 출력 + ## 2023-10-15 - v23.10.1 diff --git a/adetailer/__version__.py b/adetailer/__version__.py index 5d87e4f..0f1f31a 100644 --- a/adetailer/__version__.py +++ b/adetailer/__version__.py @@ -1 +1 @@ -__version__ = "23.10.1" +__version__ = "23.11.0" diff --git a/adetailer/args.py b/adetailer/args.py index 503eebb..5e3cceb 100644 --- a/adetailer/args.py +++ b/adetailer/args.py @@ -14,11 +14,11 @@ confloat, conint, constr, - root_validator, validator, ) cn_model_regex = r".*(inpaint|tile|scribble|lineart|openpose).*|^None$" +cn_module_regex = r".*(inpaint|tile|pidi|lineart|openpose).*|^None$" class Arg(NamedTuple): @@ -71,20 +71,12 @@ class ADetailerArgs(BaseModel, extra=Extra.forbid): ad_clip_skip: conint(ge=1, le=12) = 1 ad_restore_face: bool = False ad_controlnet_model: constr(regex=cn_model_regex) = "None" - ad_controlnet_module: Optional[constr(regex=r".*inpaint.*|^None$")] = None + ad_controlnet_module: constr(regex=cn_module_regex) = "None" ad_controlnet_weight: confloat(ge=0.0, le=1.0) = 1.0 ad_controlnet_guidance_start: confloat(ge=0.0, le=1.0) = 0.0 ad_controlnet_guidance_end: confloat(ge=0.0, le=1.0) = 1.0 is_api: bool = True - @root_validator(skip_on_failure=True) - def ad_controlnt_module_validator(cls, values): # noqa: N805 - cn_model = values.get("ad_controlnet_model", "None") - cn_module = values.get("ad_controlnet_module", None) - if "inpaint" not in cn_model or cn_module == "None": - values["ad_controlnet_module"] = None - return values - @validator("is_api", pre=True) def is_api_validator(cls, v: Any): # noqa: N805 "tuple is json serializable but cannot be made with json deserialize." @@ -122,12 +114,12 @@ def extra_params(self, suffix: str = "") -> dict[str, Any]: ppop("ADetailer mask max ratio", cond=1.0) ppop("ADetailer x offset", cond=0) ppop("ADetailer y offset", cond=0) - ppop("ADetailer mask merge/invert", cond="None") + ppop("ADetailer mask merge invert", cond="None") ppop("ADetailer inpaint only masked", ["ADetailer inpaint padding"]) ppop( - "ADetailer use inpaint width/height", + "ADetailer use inpaint width height", [ - "ADetailer use inpaint width/height", + "ADetailer use inpaint width height", "ADetailer inpaint width", "ADetailer inpaint height", ], @@ -174,7 +166,7 @@ def extra_params(self, suffix: str = "") -> dict[str, Any]: ], cond="None", ) - ppop("ADetailer ControlNet module") + ppop("ADetailer ControlNet module", cond="None") ppop("ADetailer ControlNet weight", cond=1.0) ppop("ADetailer ControlNet guidance start", cond=0.0) ppop("ADetailer ControlNet guidance end", cond=1.0) @@ -195,13 +187,13 @@ def extra_params(self, suffix: str = "") -> dict[str, Any]: ("ad_mask_max_ratio", "ADetailer mask max ratio"), ("ad_x_offset", "ADetailer x offset"), ("ad_y_offset", "ADetailer y offset"), - ("ad_dilate_erode", "ADetailer dilate/erode"), - ("ad_mask_merge_invert", "ADetailer mask merge/invert"), + ("ad_dilate_erode", "ADetailer dilate erode"), + ("ad_mask_merge_invert", "ADetailer mask merge invert"), ("ad_mask_blur", "ADetailer mask blur"), ("ad_denoising_strength", "ADetailer denoising strength"), ("ad_inpaint_only_masked", "ADetailer inpaint only masked"), ("ad_inpaint_only_masked_padding", "ADetailer inpaint padding"), - ("ad_use_inpaint_width_height", "ADetailer use inpaint width/height"), + ("ad_use_inpaint_width_height", "ADetailer use inpaint width height"), ("ad_inpaint_width", "ADetailer inpaint width"), ("ad_inpaint_height", "ADetailer inpaint height"), ("ad_use_steps", "ADetailer use separate steps"), diff --git a/adetailer/common.py b/adetailer/common.py index 3bf81cb..9b69782 100644 --- a/adetailer/common.py +++ b/adetailer/common.py @@ -36,18 +36,16 @@ def hf_download(file: str): return path +def scan_model_dir(path_: str | Path) -> list[Path]: + if not path_ or not (path := Path(path_)).is_dir(): + return [] + return [p for p in path.rglob("*") if p.is_file() and p.suffix in (".pt", ".pth")] + + def get_models( - model_dir: Union[str, Path], huggingface: bool = True -) -> OrderedDict[str, Optional[str]]: - model_dir = Path(model_dir) - if model_dir.is_dir(): - model_paths = [ - p - for p in model_dir.rglob("*") - if p.is_file() and p.suffix in (".pt", ".pth") - ] - else: - model_paths = [] + model_dir: str | Path, extra_dir: str | Path = "", huggingface: bool = True +) -> OrderedDict[str, str | None]: + model_paths = [*scan_model_dir(model_dir), *scan_model_dir(extra_dir)] models = OrderedDict() if huggingface: diff --git a/adetailer/mask.py b/adetailer/mask.py index 913ea7e..d2f3680 100644 --- a/adetailer/mask.py +++ b/adetailer/mask.py @@ -220,6 +220,7 @@ def filter_k_largest(pred: PredictOutput, k: int = 0) -> PredictOutput: return pred areas = [bbox_area(bbox) for bbox in pred.bboxes] idx = np.argsort(areas)[-k:] + idx = idx[::-1] pred.bboxes = [pred.bboxes[i] for i in idx] pred.masks = [pred.masks[i] for i in idx] return pred diff --git a/adetailer/traceback.py b/adetailer/traceback.py index e3fcd9e..74d1848 100644 --- a/adetailer/traceback.py +++ b/adetailer/traceback.py @@ -92,7 +92,7 @@ def library_version(): for lib in libraries: try: d[lib] = version(lib) - except Exception: + except Exception: # noqa: PERF203 d[lib] = "Unknown" return d @@ -147,7 +147,7 @@ def wrapper(*args, **kwargs): ] if data ] - tables.append(Traceback()) + tables.append(Traceback(extra_lines=1)) console.print(Panel(Group(*tables))) output = "\n" + string.getvalue() diff --git a/adetailer/ui.py b/adetailer/ui.py index d512c61..e49ea5e 100644 --- a/adetailer/ui.py +++ b/adetailer/ui.py @@ -11,11 +11,22 @@ from adetailer.args import ALL_ARGS, MASK_MERGE_INVERT from controlnet_ext import controlnet_exists, get_cn_models -cn_module_choices = [ - "inpaint_global_harmonious", - "inpaint_only", - "inpaint_only+lama", -] +cn_module_choices = { + "inpaint": [ + "inpaint_global_harmonious", + "inpaint_only", + "inpaint_only+lama", + ], + "lineart": [ + "lineart_coarse", + "lineart_realistic", + "lineart_anime", + "lineart_anime_denoise", + ], + "openpose": ["openpose_full", "dw_openpose_full"], + "tile": ["tile_resample", "tile_colorfix", "tile_colorfix+sharp"], + "scribble": ["t2ia_sketch_pidi"], +} class Widgets(SimpleNamespace): @@ -58,11 +69,11 @@ def on_generate_click(state: dict, *values: Any): return state -def on_cn_model_update(cn_model: str): - if "inpaint" in cn_model: - return gr.update( - visible=True, choices=cn_module_choices, value=cn_module_choices[0] - ) +def on_cn_model_update(cn_model_name: str): + for t in cn_module_choices: + if t in cn_model_name: + choices = cn_module_choices[t] + return gr.update(visible=True, choices=choices, value=choices[0]) return gr.update(visible=False, choices=["None"], value="None") @@ -564,8 +575,8 @@ def controlnet(w: Widgets, n: int, is_img2img: bool): w.ad_controlnet_module = gr.Dropdown( label="ControlNet module" + suffix(n), - choices=cn_module_choices, - value="inpaint_global_harmonious", + choices=["None"], + value="None", visible=False, type="value", interactive=controlnet_exists, diff --git a/controlnet_ext/controlnet_ext.py b/controlnet_ext/controlnet_ext.py index ec86d46..524106f 100644 --- a/controlnet_ext/controlnet_ext.py +++ b/controlnet_ext/controlnet_ext.py @@ -4,12 +4,22 @@ import re from functools import lru_cache from pathlib import Path +from textwrap import dedent from modules import extensions, sd_models, shared -from modules.paths import data_path, models_path, script_path -ext_path = Path(data_path, "extensions") -ext_builtin_path = Path(script_path, "extensions-builtin") +try: + from modules.paths import extensions_builtin_dir, extensions_dir, models_path +except ImportError as e: + msg = """ + [-] ADetailer: `stable-diffusion-webui < 1.1.0` is no longer supported. + Please upgrade to stable-diffusion-webui >= 1.1.0. + or you can use ADetailer v23.10.1 (https://github.com/Bing-su/adetailer/archive/refs/tags/v23.10.1.zip) + """ + raise RuntimeError(dedent(msg)) from e + +ext_path = Path(extensions_dir) +ext_builtin_path = Path(extensions_builtin_dir) controlnet_exists = False controlnet_path = None cn_base_path = "" @@ -29,7 +39,7 @@ "scribble": "t2ia_sketch_pidi", "lineart": "lineart_coarse", "openpose": "openpose_full", - "tile": None, + "tile": "tile_resample", } cn_model_regex = re.compile("|".join(cn_model_module.keys())) @@ -60,11 +70,13 @@ def update_scripts_args( if (not self.cn_available) or model == "None": return - if module is None: + if module is None or module == "None": for m, v in cn_model_module.items(): if m in model: module = v break + else: + module = None cn_units = [ self.external_cn.ControlNetUnit( @@ -78,7 +90,13 @@ def update_scripts_args( ) ] - self.external_cn.update_cn_script_in_processing(p, cn_units) + try: + self.external_cn.update_cn_script_in_processing(p, cn_units) + except AttributeError as e: + if "script_args_value" not in str(e): + raise + msg = "[-] Adetailer: ControlNet option not available in WEBUI version lower than 1.6.0 due to updates in ControlNet" + raise RuntimeError(msg) from e def get_cn_model_dirs() -> list[Path]: @@ -91,9 +109,9 @@ def get_cn_model_dirs() -> list[Path]: ext_dir2 = getattr(shared.cmd_opts, "controlnet_dir", "") dirs = [cn_model_dir] - for ext_dir in [cn_model_dir_old, ext_dir1, ext_dir2]: - if ext_dir: - dirs.append(Path(ext_dir)) + dirs += [ + Path(ext_dir) for ext_dir in [cn_model_dir_old, ext_dir1, ext_dir2] if ext_dir + ] return dirs diff --git a/pyproject.toml b/pyproject.toml index b2407ed..31b6342 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,8 +26,10 @@ select = [ "I001", "ISC", "N", + "PERF", "PIE", "PT", + "PTH", "RET", "RUF", "SIM", diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 40ddfa2..f587476 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -53,7 +53,10 @@ no_huggingface = getattr(cmd_opts, "ad_no_huggingface", False) adetailer_dir = Path(models_path, "adetailer") -model_mapping = get_models(adetailer_dir, huggingface=not no_huggingface) +extra_models_dir = shared.opts.data.get("ad_extra_models_dir", "") +model_mapping = get_models( + adetailer_dir, extra_dir=extra_models_dir, huggingface=not no_huggingface +) txt2img_submit_button = img2img_submit_button = None SCRIPT_DEFAULT = "dynamic_prompting,dynamic_thresholding,wildcard_recursive,wildcards,lora_block_weight" @@ -223,6 +226,13 @@ def check_skip_img2img(self, p, *args_) -> None: else: p._ad_skip_img2img = False + @staticmethod + def get_i(p) -> int: + it = p.iteration + bs = p.batch_size + i = p.batch_index + return it * bs + i + def get_args(self, p, *args_) -> list[ADetailerArgs]: """ `args_` is at least 1 in length by `is_ad_enabled` immediately above @@ -308,7 +318,7 @@ def _get_prompt( return prompts def get_prompt(self, p, args: ADetailerArgs) -> tuple[list[str], list[str]]: - i = p._ad_idx + i = self.get_i(p) prompt_sr = p._ad_xyz_prompt_sr if hasattr(p, "_ad_xyz_prompt_sr") else [] prompt = self._get_prompt(args.ad_prompt, p.all_prompts, i, p.prompt, prompt_sr) @@ -323,7 +333,7 @@ def get_prompt(self, p, args: ADetailerArgs) -> tuple[list[str], list[str]]: return prompt, negative_prompt def get_seed(self, p) -> tuple[int, int]: - i = p._ad_idx + i = self.get_i(p) if not p.all_seeds: seed = p.seed @@ -401,7 +411,7 @@ def infotext(p) -> str: ) def write_params_txt(self, p) -> None: - i = p._ad_idx + i = self.get_i(p) lenp = len(p.all_prompts) if i % lenp != lenp - 1: return @@ -503,6 +513,7 @@ def get_i2i_p(self, p, args: ADetailerArgs, image): i2i.cached_uc = [None, None] i2i.scripts, i2i.script_args = self.script_filter(p, args) i2i._ad_disabled = True + i2i._ad_inner = True if args.ad_controlnet_model != "None": self.update_controlnet_args(i2i, args) @@ -512,7 +523,7 @@ def get_i2i_p(self, p, args: ADetailerArgs, image): return i2i def save_image(self, p, image, *, condition: str, suffix: str) -> None: - i = p._ad_idx + i = self.get_i(p) if p.all_prompts: i %= len(p.all_prompts) save_prompt = p.all_prompts[i] @@ -591,17 +602,15 @@ def compare_prompt(p, processed, n: int = 0): def need_call_process(p) -> bool: if p.scripts is None: return False - i = p._ad_idx + i = p.batch_index bs = p.batch_size - return i % bs == bs - 1 + return i == bs - 1 @staticmethod def need_call_postprocess(p) -> bool: if p.scripts is None: return False - i = p._ad_idx - bs = p.batch_size - return i % bs == 0 + return p.batch_index == 0 @staticmethod def get_i2i_init_image(p, pp): @@ -609,6 +618,11 @@ def get_i2i_init_image(p, pp): return p.init_images[0] return pp.image + @staticmethod + def get_each_tap_seed(seed: int, i: int): + use_same_seed = shared.opts.data.get("ad_same_seed_for_each_tap", False) + return seed if use_same_seed else seed + i + @rich_traceback def process(self, p, *args_): if getattr(p, "_ad_disabled", False): @@ -635,7 +649,7 @@ def _postprocess_image_inner( if state.interrupted or state.skipped: return False - i = p._ad_idx + i = self.get_i(p) pp.image = self.get_i2i_init_image(p, pp) i2i = self.get_i2i_p(p, args, pp.image) @@ -688,8 +702,8 @@ def _postprocess_image_inner( if re.match(r"^\s*\[SKIP\]\s*$", p2.prompt): continue - p2.seed = seed + j - p2.subseed = subseed + j + p2.seed = self.get_each_tap_seed(seed, j) + p2.subseed = self.get_each_tap_seed(subseed, j) try: processed = process_images(p2) @@ -715,7 +729,6 @@ def postprocess_image(self, p, pp, *args_): if getattr(p, "_ad_disabled", False) or not self.is_ad_enabled(*args_): return - p._ad_idx = getattr(p, "_ad_idx", -1) + 1 init_image = copy(pp.image) arg_list = self.get_args(p, *args_) @@ -738,7 +751,10 @@ def postprocess_image(self, p, pp, *args_): if self.need_call_process(p): with preseve_prompts(p): - p.scripts.process(copy(p)) + copy_p = copy(p) + if hasattr(p.scripts, "before_process"): + p.scripts.before_process(copy_p) + p.scripts.process(copy_p) self.write_params_txt(p) @@ -766,6 +782,16 @@ def on_ui_settings(): ), ) + shared.opts.add_option( + "ad_extra_models_dir", + shared.OptionInfo( + default="", + label="Extra path to scan adetailer models", + component=gr.Textbox, + section=section, + ), + ) + shared.opts.add_option( "ad_save_previews", shared.OptionInfo(False, "Save mask previews", section=section), @@ -810,6 +836,13 @@ def on_ui_settings(): ), ) + shared.opts.add_option( + "ad_same_seed_for_each_tap", + shared.OptionInfo( + False, "Use same seed for each tab in adetailer", section=section + ), + ) + # xyz_grid