diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 3279467..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Lint - -on: - pull_request: - paths: - - "**.py" - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Install python packages - run: pip install black ruff pre-commit-hooks - - - name: Run pre-commit-hooks - run: | - check-ast - trailing-whitespace-fixer --markdown-linebreak-ext=md - end-of-file-fixer - mixed-line-ending - - - name: Run black - run: black --check . - - - name: Run ruff - run: ruff check . diff --git a/.github/workflows/notlint.yml b/.github/workflows/notlint.yml new file mode 100644 index 0000000..faa3343 --- /dev/null +++ b/.github/workflows/notlint.yml @@ -0,0 +1,19 @@ +name: Not Lint + +on: + pull_request: + types: + - opened + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: wow-actions/auto-comment@v1 + with: + GITHUB_TOKEN: ${{ github.token }} + pullRequestOpened: | + ![Imgur](https://i.imgur.com/ESow3BL.png) + + LGTM diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07062c0..c3b0f8d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,18 +15,14 @@ repos: - id: end-of-file-fixer - id: mixed-line-ending - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.5 - hooks: - - id: ruff - args: [--fix, --exit-non-zero-on-fix] - - repo: https://github.com/pre-commit/mirrors-prettier rev: "v4.0.0-alpha.8" hooks: - id: prettier - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.3.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.7 hooks: - - id: black + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f6e145..70572b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 2024-04-14 + +- v24.4.1 +- webui 1.9.0에서 발생한 에러 수정 + - extra generation params에 callable이 들어와서 생긴 문제 + - assign_current_image에 None이 들어갈 수 있던 문제 +- webui 1.9.0에서 변경된 scheduler 지원 +- 컨트롤넷 모델을 찾을 때, 대소문자 구분을 하지 않음 (PR #577) +- 몇몇 기능을 스크립트에서 분리하여 별도 파일로 빼냄 + ## 2024-04-10 - v24.4.0 diff --git a/README.md b/README.md index a18a42d..0be6eb5 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,12 @@ ADetailer is an extension for the stable diffusion webui that does automatic mas ## Install +You can install it directly from the Extensions tab. + +![image](https://i.imgur.com/qaXtoI6.png) + +Or + (from Mikubill/sd-webui-controlnet) 1. Open "Extensions" tab. @@ -14,10 +20,6 @@ ADetailer is an extension for the stable diffusion webui that does automatic mas 6. Go to "Installed" tab, click "Check for updates", and then click "Apply and restart UI". (The next time you can also use this method to update extensions.) 7. Completely restart A1111 webui including your terminal. (If you do not know what is a "terminal", you can reboot your computer: turn your computer off and turn it on again.) -You can now install it directly from the Extensions tab. - -![image](https://i.imgur.com/g6GdRBT.png) - ## Options | Model, Prompts | | | @@ -63,8 +65,8 @@ API request example: [wiki/REST-API](https://github.com/Bing-su/adetailer/wiki/R ## Media -- 🎥 [どこよりも詳しいAfter Detailer (adetailer)の使い方① 【Stable Diffusion】](https://youtu.be/sF3POwPUWCE) -- 🎥 [どこよりも詳しいAfter Detailer (adetailer)の使い方② 【Stable Diffusion】](https://youtu.be/urNISRdbIEg) +- 🎥 [どこよりも詳しい After Detailer (adetailer)の使い方 ① 【Stable Diffusion】](https://youtu.be/sF3POwPUWCE) +- 🎥 [どこよりも詳しい After Detailer (adetailer)の使い方 ② 【Stable Diffusion】](https://youtu.be/urNISRdbIEg) - 📜 [ADetailer Installation and 5 Usage Methods](https://kindanai.com/en/manual-adetailer/) diff --git a/aaaaaa/__init__.py b/aaaaaa/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aaaaaa/conditional.py b/aaaaaa/conditional.py new file mode 100644 index 0000000..a16a17a --- /dev/null +++ b/aaaaaa/conditional.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from PIL import Image + +try: + from modules.processing import create_binary_mask +except ImportError: + + def create_binary_mask(image: Image.Image): + return image.convert("L") + + +try: + from modules.sd_schedulers import schedulers +except ImportError: + schedulers = [] diff --git a/aaaaaa/helper.py b/aaaaaa/helper.py new file mode 100644 index 0000000..2bcda32 --- /dev/null +++ b/aaaaaa/helper.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from contextlib import contextmanager +from copy import copy +from typing import TYPE_CHECKING, Any + +import torch + +from modules import safe +from modules.shared import opts + +if TYPE_CHECKING: + # 타입 체커가 빨간 줄을 긋지 않게 하는 편법 + from types import SimpleNamespace + + StableDiffusionProcessingTxt2Img = SimpleNamespace + StableDiffusionProcessingImg2Img = SimpleNamespace +else: + from modules.processing import ( + StableDiffusionProcessingImg2Img, + StableDiffusionProcessingTxt2Img, + ) + +PT = StableDiffusionProcessingTxt2Img | StableDiffusionProcessingImg2Img + + +@contextmanager +def change_torch_load(): + orig = torch.load + try: + torch.load = safe.unsafe_torch_load + yield + finally: + torch.load = orig + + +@contextmanager +def pause_total_tqdm(): + orig = opts.data.get("multiple_tqdm", True) + try: + opts.data["multiple_tqdm"] = False + yield + finally: + opts.data["multiple_tqdm"] = orig + + +@contextmanager +def preseve_prompts(p: PT): + all_pt = copy(p.all_prompts) + all_ng = copy(p.all_negative_prompts) + try: + yield + finally: + p.all_prompts = all_pt + p.all_negative_prompts = all_ng + + +def copy_extra_params(extra_params: dict[str, Any]) -> dict[str, Any]: + return {k: v for k, v in extra_params.items() if not callable(v)} diff --git a/aaaaaa/p_method.py b/aaaaaa/p_method.py new file mode 100644 index 0000000..9a87e7c --- /dev/null +++ b/aaaaaa/p_method.py @@ -0,0 +1,30 @@ +from __future__ import annotations + + +def need_call_process(p) -> bool: + if p.scripts is None: + return False + i = p.batch_index + bs = p.batch_size + return i == bs - 1 + + +def need_call_postprocess(p) -> bool: + if p.scripts is None: + return False + return p.batch_index == 0 + + +def is_img2img_inpaint(p) -> bool: + return hasattr(p, "image_mask") and p.image_mask is not None + + +def is_inpaint_only_masked(p) -> bool: + return hasattr(p, "inpaint_full_res") and p.inpaint_full_res + + +def get_i(p) -> int: + it = p.iteration + bs = p.batch_size + i = p.batch_index + return it * bs + i diff --git a/adetailer/__version__.py b/adetailer/__version__.py index b43269d..de57016 100644 --- a/adetailer/__version__.py +++ b/adetailer/__version__.py @@ -1 +1 @@ -__version__ = "24.4.0" +__version__ = "24.4.1" diff --git a/adetailer/args.py b/adetailer/args.py index 5dd4577..a54ac6c 100644 --- a/adetailer/args.py +++ b/adetailer/args.py @@ -82,6 +82,7 @@ class ADetailerArgs(BaseModel, extra=Extra.forbid): ad_vae: Optional[str] = None ad_use_sampler: bool = False ad_sampler: str = "DPM++ 2M Karras" + ad_scheduler: str = "Use same scheduler" ad_use_noise_multiplier: bool = False ad_noise_multiplier: confloat(ge=0.5, le=1.5) = 1.0 ad_use_clip_skip: bool = False @@ -160,8 +161,13 @@ class ADetailerArgs(BaseModel, extra=Extra.forbid): ) ppop( "ADetailer use separate sampler", - ["ADetailer use separate sampler", "ADetailer sampler"], + [ + "ADetailer use separate sampler", + "ADetailer sampler", + "ADetailer scheduler", + ], ) + ppop("ADetailer scheduler", cond="Use same scheduler") ppop( "ADetailer use separate noise multiplier", ["ADetailer use separate noise multiplier", "ADetailer noise multiplier"], @@ -225,6 +231,7 @@ _all_args = [ ("ad_vae", "ADetailer VAE"), ("ad_use_sampler", "ADetailer use separate sampler"), ("ad_sampler", "ADetailer sampler"), + ("ad_scheduler", "ADetailer scheduler"), ("ad_use_noise_multiplier", "ADetailer use separate noise multiplier"), ("ad_noise_multiplier", "ADetailer noise multiplier"), ("ad_use_clip_skip", "ADetailer use separate CLIP skip"), @@ -247,3 +254,14 @@ BBOX_SORTBY = [ "Area (large to small)", ] MASK_MERGE_INVERT = ["None", "Merge", "Merge and Invert"] + +_script_default = ( + "dynamic_prompting", + "dynamic_thresholding", + "wildcard_recursive", + "wildcards", + "lora_block_weight", + "negpip", + "soft_inpainting", +) +SCRIPT_DEFAULT = ",".join(sorted(_script_default)) diff --git a/adetailer/common.py b/adetailer/common.py index 5fc6b53..dfd7952 100644 --- a/adetailer/common.py +++ b/adetailer/common.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from collections import OrderedDict from dataclasses import dataclass, field from pathlib import Path @@ -37,16 +38,27 @@ def hf_download(file: str, repo_id: str = REPO_ID) -> str | None: return path -def scan_model_dir(path_: str | Path) -> list[Path]: - if not path_ or not (path := Path(path_)).is_dir(): +def safe_mkdir(path: str | os.PathLike[str]) -> None: + path = Path(path) + if not path.exists() and path.parent.exists() and os.access(path.parent, os.W_OK): + path.mkdir() + + +def scan_model_dir(path: Path) -> list[Path]: + if not path.is_dir(): return [] - return [p for p in path.rglob("*") if p.is_file() and p.suffix in (".pt", ".pth")] + return [p for p in path.rglob("*") if p.is_file() and p.suffix == ".pt"] def get_models( - model_dir: str | Path, extra_dir: str | Path = "", huggingface: bool = True + *dirs: str | os.PathLike[str], huggingface: bool = True ) -> OrderedDict[str, str]: - model_paths = [*scan_model_dir(model_dir), *scan_model_dir(extra_dir)] + model_paths = [] + + for dir_ in dirs: + if not dir_: + continue + model_paths.extend(scan_model_dir(Path(dir_))) models = OrderedDict() if huggingface: diff --git a/adetailer/ui.py b/adetailer/ui.py index 04379d5..f17c52c 100644 --- a/adetailer/ui.py +++ b/adetailer/ui.py @@ -51,6 +51,7 @@ class Widgets(SimpleNamespace): class WebuiInfo: ad_model_list: list[str] sampler_names: list[str] + scheduler_names: list[str] t2i_button: gr.Button i2i_button: gr.Button checkpoints_list: list[str] @@ -537,20 +538,33 @@ def inpainting(w: Widgets, n: int, is_img2img: bool, webui_info: WebuiInfo): elem_id=eid("ad_use_sampler"), ) - w.ad_sampler = gr.Dropdown( - label="ADetailer sampler" + suffix(n), - choices=webui_info.sampler_names, - value=webui_info.sampler_names[0], - visible=True, - elem_id=eid("ad_sampler"), - ) + with gr.Row(): + w.ad_sampler = gr.Dropdown( + label="ADetailer sampler" + suffix(n), + choices=webui_info.sampler_names, + value=webui_info.sampler_names[0], + visible=True, + elem_id=eid("ad_sampler"), + ) - w.ad_use_sampler.change( - gr_interactive, - inputs=w.ad_use_sampler, - outputs=w.ad_sampler, - queue=False, - ) + scheduler_names = [ + "Use same scheduler", + *webui_info.scheduler_names, + ] + w.ad_scheduler = gr.Dropdown( + label="ADetailer scheduler" + suffix(n), + choices=scheduler_names, + value=scheduler_names[0], + visible=len(scheduler_names) > 1, + elem_id=eid("ad_scheduler"), + ) + + w.ad_use_sampler.change( + lambda value: (gr_interactive(value), gr_interactive(value)), + inputs=w.ad_use_sampler, + outputs=[w.ad_sampler, w.ad_scheduler], + queue=False, + ) with gr.Row(): with gr.Column(variant="compact"): diff --git a/controlnet_ext/common.py b/controlnet_ext/common.py index beeb60e..b78dbf3 100644 --- a/controlnet_ext/common.py +++ b/controlnet_ext/common.py @@ -8,4 +8,4 @@ cn_model_module = { "tile": "tile_resample", "depth": "depth_midas", } -cn_model_regex = re.compile("|".join(cn_model_module.keys())) +cn_model_regex = re.compile("|".join(cn_model_module.keys()), flags=re.I) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index fddb0bf..0ec861d 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -1,23 +1,35 @@ from __future__ import annotations -import os import platform import re import sys import traceback -from contextlib import contextmanager, suppress +from contextlib import suppress from copy import copy from functools import partial from pathlib import Path from textwrap import dedent -from typing import TYPE_CHECKING, Any, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple, cast import gradio as gr -import torch from PIL import Image, ImageChops from rich import print import modules +from aaaaaa.conditional import create_binary_mask, schedulers +from aaaaaa.helper import ( + change_torch_load, + copy_extra_params, + pause_total_tqdm, + preseve_prompts, +) +from aaaaaa.p_method import ( + get_i, + is_img2img_inpaint, + is_inpaint_only_masked, + need_call_postprocess, + need_call_process, +) from adetailer import ( AFTER_DETAILER, __version__, @@ -25,8 +37,8 @@ from adetailer import ( mediapipe_predict, ultralytics_predict, ) -from adetailer.args import BBOX_SORTBY, ADetailerArgs, SkipImg2ImgOrig -from adetailer.common import PredictOutput, ensure_pil_image +from adetailer.args import BBOX_SORTBY, SCRIPT_DEFAULT, ADetailerArgs, SkipImg2ImgOrig +from adetailer.common import PredictOutput, ensure_pil_image, safe_mkdir from adetailer.mask import ( filter_by_ratio, filter_k_largest, @@ -45,7 +57,7 @@ from controlnet_ext import ( controlnet_type, get_cn_models, ) -from modules import images, paths, safe, script_callbacks, scripts, shared +from modules import images, paths, script_callbacks, scripts, shared from modules.devices import NansException from modules.processing import ( Processed, @@ -56,69 +68,29 @@ from modules.processing import ( from modules.sd_samplers import all_samplers from modules.shared import cmd_opts, opts, state -try: - from modules.processing import create_binary_mask -except ImportError: - - def create_binary_mask(image: Image.Image): - return image.convert("L") - - if TYPE_CHECKING: from fastapi import FastAPI no_huggingface = getattr(cmd_opts, "ad_no_huggingface", False) adetailer_dir = Path(paths.models_path, "adetailer") +safe_mkdir(adetailer_dir) + 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 + adetailer_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,negpip,soft_inpainting" -if ( - not adetailer_dir.exists() - and adetailer_dir.parent.exists() - and os.access(adetailer_dir.parent, os.W_OK) -): - adetailer_dir.mkdir() +txt2img_submit_button = img2img_submit_button = None +txt2img_submit_button = cast(gr.Button, txt2img_submit_button) +img2img_submit_button = cast(gr.Button, img2img_submit_button) print( f"[-] ADetailer initialized. version: {__version__}, num models: {len(model_mapping)}" ) -@contextmanager -def change_torch_load(): - orig = torch.load - try: - torch.load = safe.unsafe_torch_load - yield - finally: - torch.load = orig - - -@contextmanager -def pause_total_tqdm(): - orig = opts.data.get("multiple_tqdm", True) - try: - opts.data["multiple_tqdm"] = False - yield - finally: - opts.data["multiple_tqdm"] = orig - - -@contextmanager -def preseve_prompts(p): - all_pt = copy(p.all_prompts) - all_ng = copy(p.all_negative_prompts) - try: - yield - finally: - p.all_prompts = all_pt - p.all_negative_prompts = all_ng - - class AfterDetailerScript(scripts.Script): def __init__(self): super().__init__() @@ -139,6 +111,7 @@ class AfterDetailerScript(scripts.Script): num_models = opts.data.get("ad_max_models", 2) ad_model_list = list(model_mapping.keys()) sampler_names = [sampler.name for sampler in all_samplers] + scheduler_names = [x.label for x in schedulers] try: checkpoint_list = modules.sd_models.checkpoint_tiles(use_shorts=True) @@ -149,6 +122,7 @@ class AfterDetailerScript(scripts.Script): webui_info = WebuiInfo( ad_model_list=ad_model_list, sampler_names=sampler_names, + scheduler_names=scheduler_names, t2i_button=txt2img_submit_button, i2i_button=img2img_submit_button, checkpoints_list=checkpoint_list, @@ -224,7 +198,7 @@ class AfterDetailerScript(scripts.Script): if not p._ad_skip_img2img: return - if self.is_img2img_inpaint(p): + if is_img2img_inpaint(p): p._ad_disabled = True msg = "[-] ADetailer: img2img inpainting with skip img2img is not supported. (because it's buggy)" print(msg) @@ -241,13 +215,6 @@ class AfterDetailerScript(scripts.Script): p.width = 128 p.height = 128 - @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 @@ -323,14 +290,14 @@ class AfterDetailerScript(scripts.Script): if not prompts[n]: prompts[n] = blank_replacement elif "[PROMPT]" in prompts[n]: - prompts[n] = prompts[n].replace("[PROMPT]", f" {blank_replacement} ") + prompts[n] = prompts[n].replace("[PROMPT]", blank_replacement) for pair in replacements: prompts[n] = prompts[n].replace(pair.s, pair.r) return prompts def get_prompt(self, p, args: ADetailerArgs) -> tuple[list[str], list[str]]: - i = self.get_i(p) + i = get_i(p) prompt_sr = p._ad_xyz_prompt_sr if hasattr(p, "_ad_xyz_prompt_sr") else [] prompt = self._get_prompt( @@ -351,7 +318,7 @@ class AfterDetailerScript(scripts.Script): return prompt, negative_prompt def get_seed(self, p) -> tuple[int, int]: - i = self.get_i(p) + i = get_i(p) if not p.all_seeds: seed = p.seed @@ -401,6 +368,17 @@ class AfterDetailerScript(scripts.Script): return p._ad_orig.sampler_name return p.sampler_name + def get_scheduler(self, p, args: ADetailerArgs) -> dict[str, str]: + "webui >= 1.9.0" + if not args.ad_use_sampler: + return {} + + if args.ad_scheduler == "Use same scheduler": + value = getattr(p, "scheduler", "Automatic") + else: + value = args.ad_scheduler + return {"scheduler": value} + def get_override_settings(self, p, args: ADetailerArgs) -> dict[str, Any]: d = {} @@ -498,6 +476,10 @@ class AfterDetailerScript(scripts.Script): sampler_name = self.get_sampler(p, args) override_settings = self.get_override_settings(p, args) + version_args = {} + if schedulers: + version_args.update(self.get_scheduler(p, args)) + i2i = StableDiffusionProcessingImg2Img( init_images=[image], resize_mode=0, @@ -529,10 +511,11 @@ class AfterDetailerScript(scripts.Script): height=height, restore_faces=args.ad_restore_face, tiling=p.tiling, - extra_generation_params=p.extra_generation_params.copy(), + extra_generation_params=copy_extra_params(p.extra_generation_params), do_not_save_samples=True, do_not_save_grid=True, override_settings=override_settings, + **version_args, ) i2i.cached_c = [None, None] @@ -552,7 +535,7 @@ class AfterDetailerScript(scripts.Script): return i2i def save_image(self, p, image, *, condition: str, suffix: str) -> None: - i = self.get_i(p) + i = get_i(p) if p.all_prompts: i %= len(p.all_prompts) save_prompt = p.all_prompts[i] @@ -598,7 +581,7 @@ class AfterDetailerScript(scripts.Script): merge_invert=args.ad_mask_merge_invert, ) - if self.is_img2img_inpaint(p) and not self.is_inpaint_only_masked(p): + if is_img2img_inpaint(p) and not is_inpaint_only_masked(p): image_mask = self.get_image_mask(p) masks = self.inpaint_mask_filter(image_mask, masks) return masks @@ -633,20 +616,6 @@ class AfterDetailerScript(scripts.Script): ) p._ad_extra_params_result[ng] = processed.all_negative_prompts[0] - @staticmethod - def need_call_process(p) -> bool: - if p.scripts is None: - return False - i = p.batch_index - bs = p.batch_size - return i == bs - 1 - - @staticmethod - def need_call_postprocess(p) -> bool: - if p.scripts is None: - return False - return p.batch_index == 0 - @staticmethod def get_i2i_init_image(p, pp): if getattr(p, "_ad_skip_img2img", False): @@ -658,14 +627,6 @@ class AfterDetailerScript(scripts.Script): use_same_seed = shared.opts.data.get("ad_same_seed_for_each_tap", False) return seed if use_same_seed else seed + i - @staticmethod - def is_img2img_inpaint(p) -> bool: - return hasattr(p, "image_mask") and p.image_mask is not None - - @staticmethod - def is_inpaint_only_masked(p) -> bool: - return hasattr(p, "inpaint_full_res") and p.inpaint_full_res - @staticmethod def inpaint_mask_filter( img2img_mask: Image.Image, ad_mask: list[Image.Image] @@ -696,7 +657,7 @@ class AfterDetailerScript(scripts.Script): if getattr(p, "_ad_disabled", False): return - if self.is_img2img_inpaint(p) and is_all_black(self.get_image_mask(p)): + if is_img2img_inpaint(p) and is_all_black(self.get_image_mask(p)): p._ad_disabled = True msg = ( "[-] ADetailer: img2img inpainting with no mask -- adetailer disabled." @@ -738,7 +699,7 @@ class AfterDetailerScript(scripts.Script): if state.interrupted or state.skipped: return False - i = self.get_i(p) + i = get_i(p) i2i = self.get_i2i_p(p, args, pp.image) seed, subseed = self.get_seed(p) @@ -759,15 +720,15 @@ class AfterDetailerScript(scripts.Script): with change_torch_load(): pred = predictor(ad_model, pp.image, args.ad_confidence, **kwargs) - masks = self.pred_preprocessing(p, pred, args) - shared.state.assign_current_image(pred.preview) - - if not masks: + if pred.preview is None: print( f"[-] ADetailer: nothing detected on image {i + 1} with {ordinal(n + 1)} settings." ) return False + masks = self.pred_preprocessing(p, pred, args) + shared.state.assign_current_image(pred.preview) + self.save_image( p, pred.preview, @@ -824,7 +785,7 @@ class AfterDetailerScript(scripts.Script): arg_list = self.get_args(p, *args_) params_txt_content = Path(paths.data_path, "params.txt").read_text("utf-8") - if self.need_call_postprocess(p): + if need_call_postprocess(p): dummy = Processed(p, [], p.seed, "") with preseve_prompts(p): p.scripts.postprocess(copy(p), dummy) @@ -841,7 +802,7 @@ class AfterDetailerScript(scripts.Script): p, init_image, condition="ad_save_images_before", suffix="-ad-before" ) - if self.need_call_process(p): + if need_call_process(p): with preseve_prompts(p): copy_p = copy(p) if hasattr(p.scripts, "before_process"): diff --git a/tests/test_args.py b/tests/test_args.py new file mode 100644 index 0000000..89b0c2b --- /dev/null +++ b/tests/test_args.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from adetailer.args import ALL_ARGS, ADetailerArgs + + +def test_all_args() -> None: + args = ADetailerArgs() + for attr, _ in ALL_ARGS: + assert hasattr(args, attr), attr + + for attr, _ in args: + if attr == "is_api": + continue + assert attr in ALL_ARGS.attrs, attr