Merge branch 'dev'

This commit is contained in:
Dowon
2024-04-14 18:05:48 +09:00
16 changed files with 286 additions and 169 deletions

View File

@@ -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 .

19
.github/workflows/notlint.yml vendored Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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/)

0
aaaaaa/__init__.py Normal file
View File

16
aaaaaa/conditional.py Normal file
View File

@@ -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 = []

59
aaaaaa/helper.py Normal file
View File

@@ -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)}

30
aaaaaa/p_method.py Normal file
View File

@@ -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

View File

@@ -1 +1 @@
__version__ = "24.4.0"
__version__ = "24.4.1"

View File

@@ -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))

View File

@@ -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:

View File

@@ -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"):

View File

@@ -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)

View File

@@ -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"):

14
tests/test_args.py Normal file
View File

@@ -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