dikdimon commited on
Commit
25709f1
·
verified ·
1 Parent(s): c468a2e

Upload 42 files

Browse files
Files changed (42) hide show
  1. adetailer-dev/adetailer-dev/.github/ISSUE_TEMPLATE/bug_report.yaml +63 -0
  2. adetailer-dev/adetailer-dev/.github/ISSUE_TEMPLATE/feature_request.yaml +26 -0
  3. adetailer-dev/adetailer-dev/.github/workflows/lgtm.yml +23 -0
  4. adetailer-dev/adetailer-dev/.github/workflows/pypi.yml +49 -0
  5. adetailer-dev/adetailer-dev/.github/workflows/stale.yml +13 -0
  6. adetailer-dev/adetailer-dev/.gitignore +197 -0
  7. adetailer-dev/adetailer-dev/.pre-commit-config.yaml +29 -0
  8. adetailer-dev/adetailer-dev/.vscode/extensions.json +9 -0
  9. adetailer-dev/adetailer-dev/.vscode/settings.json +8 -0
  10. adetailer-dev/adetailer-dev/CHANGELOG.md +462 -0
  11. adetailer-dev/adetailer-dev/LICENSE.md +661 -0
  12. adetailer-dev/adetailer-dev/README.md +118 -0
  13. adetailer-dev/adetailer-dev/Taskfile.yml +32 -0
  14. adetailer-dev/adetailer-dev/aaaaaa/__init__.py +0 -0
  15. adetailer-dev/adetailer-dev/aaaaaa/conditional.py +21 -0
  16. adetailer-dev/adetailer-dev/aaaaaa/helper.py +59 -0
  17. adetailer-dev/adetailer-dev/aaaaaa/p_method.py +34 -0
  18. adetailer-dev/adetailer-dev/aaaaaa/traceback.py +175 -0
  19. adetailer-dev/adetailer-dev/aaaaaa/ui.py +705 -0
  20. adetailer-dev/adetailer-dev/adetailer/__init__.py +18 -0
  21. adetailer-dev/adetailer-dev/adetailer/__version__.py +1 -0
  22. adetailer-dev/adetailer-dev/adetailer/args.py +278 -0
  23. adetailer-dev/adetailer-dev/adetailer/common.py +178 -0
  24. adetailer-dev/adetailer-dev/adetailer/mask.py +269 -0
  25. adetailer-dev/adetailer-dev/adetailer/mediapipe.py +174 -0
  26. adetailer-dev/adetailer-dev/adetailer/ultralytics.py +66 -0
  27. adetailer-dev/adetailer-dev/controlnet_ext/__init__.py +25 -0
  28. adetailer-dev/adetailer-dev/controlnet_ext/common.py +12 -0
  29. adetailer-dev/adetailer-dev/controlnet_ext/controlnet_ext.py +149 -0
  30. adetailer-dev/adetailer-dev/controlnet_ext/controlnet_ext_forge.py +92 -0
  31. adetailer-dev/adetailer-dev/controlnet_ext/restore.py +43 -0
  32. adetailer-dev/adetailer-dev/install.py +77 -0
  33. adetailer-dev/adetailer-dev/preload.py +9 -0
  34. adetailer-dev/adetailer-dev/pyproject.toml +77 -0
  35. adetailer-dev/adetailer-dev/scripts/!adetailer.py +1052 -0
  36. adetailer-dev/adetailer-dev/tests/__init__.py +0 -0
  37. adetailer-dev/adetailer-dev/tests/conftest.py +18 -0
  38. adetailer-dev/adetailer-dev/tests/test_args.py +48 -0
  39. adetailer-dev/adetailer-dev/tests/test_common.py +69 -0
  40. adetailer-dev/adetailer-dev/tests/test_mask.py +236 -0
  41. adetailer-dev/adetailer-dev/tests/test_mediapipe.py +18 -0
  42. adetailer-dev/adetailer-dev/tests/test_ultralytics.py +50 -0
adetailer-dev/adetailer-dev/.github/ISSUE_TEMPLATE/bug_report.yaml ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Bug report
2
+ description: Create a report
3
+ title: "[Bug]: "
4
+ labels:
5
+ - bug
6
+
7
+ body:
8
+ - type: textarea
9
+ attributes:
10
+ label: Describe the bug
11
+ description: A clear and concise description of what the bug is.
12
+ placeholder: |
13
+ Any language accepted
14
+ 아무 언어 사용가능
15
+ すべての言語に対応
16
+ 接受所有语言
17
+ Se aceptan todos los idiomas
18
+ Alle Sprachen werden akzeptiert
19
+ Toutes les langues sont acceptées
20
+ Принимаются все языки
21
+ validations:
22
+ required: true
23
+
24
+ - type: textarea
25
+ attributes:
26
+ label: Steps to reproduce
27
+ description: |
28
+ Description of how we can reproduce this issue.
29
+ validations:
30
+ required: true
31
+
32
+ - type: textarea
33
+ attributes:
34
+ label: Screenshots
35
+ description: Screenshots related to the issue.
36
+
37
+ - type: textarea
38
+ attributes:
39
+ label: Console logs, from start to end.
40
+ description: |
41
+ The full console log of your terminal.
42
+ placeholder: |
43
+ Python ...
44
+ Version: ...
45
+ Commit hash: ...
46
+ Installing requirements
47
+ ...
48
+
49
+ Launching Web UI with arguments: ...
50
+ [-] ADetailer initialized. version: ...
51
+ ...
52
+ ...
53
+
54
+ Traceback (most recent call last):
55
+ ...
56
+ ...
57
+ render: Shell
58
+ validations:
59
+ required: true
60
+
61
+ - type: textarea
62
+ attributes:
63
+ label: List of installed extensions
adetailer-dev/adetailer-dev/.github/ISSUE_TEMPLATE/feature_request.yaml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Feature request
2
+ description: Suggest an idea for this project
3
+ title: "[Feature Request]: "
4
+ labels:
5
+ - enhancement
6
+
7
+ body:
8
+ - type: textarea
9
+ attributes:
10
+ label: Is your feature request related to a problem? Please describe.
11
+ description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ - type: textarea
14
+ attributes:
15
+ label: Describe the solution you'd like
16
+ description: A clear and concise description of what you want to happen.
17
+
18
+ - type: textarea
19
+ attributes:
20
+ label: Describe alternatives you've considered
21
+ description: A clear and concise description of any alternative solutions or features you've considered.
22
+
23
+ - type: textarea
24
+ attributes:
25
+ label: Additional context
26
+ description: Add any other context or screenshots about the feature request here.
adetailer-dev/adetailer-dev/.github/workflows/lgtm.yml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Empirical Implementation of JDD
2
+
3
+ on:
4
+ pull_request:
5
+ types:
6
+ - opened
7
+
8
+ jobs:
9
+ lint:
10
+ permissions:
11
+ issues: write
12
+ pull-requests: write
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - uses: peter-evans/create-or-update-comment@v4
17
+ with:
18
+ issue-number: ${{ github.event.pull_request.number }}
19
+ body: |
20
+ ![Imgur](https://i.imgur.com/ESow3BL.png)
21
+
22
+ LGTM
23
+ reactions: hooray
adetailer-dev/adetailer-dev/.github/workflows/pypi.yml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Publish to PyPI
2
+ on:
3
+ push:
4
+ tags:
5
+ - "v*"
6
+
7
+ jobs:
8
+ test:
9
+ name: test
10
+ runs-on: macos-14
11
+ strategy:
12
+ matrix:
13
+ python-version:
14
+ - "3.10"
15
+ - "3.11"
16
+ - "3.12"
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - uses: yezz123/setup-uv@v4
27
+
28
+ - name: Install dependencies
29
+ run: |
30
+ uv pip install --system . pytest
31
+
32
+ - name: Run tests
33
+ run: pytest -v
34
+
35
+ build:
36
+ name: build
37
+ runs-on: ubuntu-latest
38
+ permissions:
39
+ id-token: write
40
+ needs: [test]
41
+
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+
45
+ - name: Build wheel
46
+ run: pipx run build
47
+
48
+ - name: Publish to PyPI
49
+ uses: pypa/gh-action-pypi-publish@release/v1
adetailer-dev/adetailer-dev/.github/workflows/stale.yml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Close stale issues and PRs
2
+ on:
3
+ schedule:
4
+ - cron: "30 1 * * *"
5
+
6
+ jobs:
7
+ stale:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/stale@v9
11
+ with:
12
+ days-before-stale: 17
13
+ days-before-close: 3
adetailer-dev/adetailer-dev/.gitignore ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode
3
+
4
+ ### Python ###
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # pdm
109
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110
+ #pdm.lock
111
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112
+ # in version control.
113
+ # https://pdm.fming.dev/#use-with-ide
114
+ .pdm.toml
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .env
128
+ .venv
129
+ env/
130
+ venv/
131
+ ENV/
132
+ env.bak/
133
+ venv.bak/
134
+
135
+ # Spyder project settings
136
+ .spyderproject
137
+ .spyproject
138
+
139
+ # Rope project settings
140
+ .ropeproject
141
+
142
+ # mkdocs documentation
143
+ /site
144
+
145
+ # mypy
146
+ .mypy_cache/
147
+ .dmypy.json
148
+ dmypy.json
149
+
150
+ # Pyre type checker
151
+ .pyre/
152
+
153
+ # pytype static type analyzer
154
+ .pytype/
155
+
156
+ # Cython debug symbols
157
+ cython_debug/
158
+
159
+ # PyCharm
160
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
163
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
164
+ #.idea/
165
+
166
+ ### Python Patch ###
167
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
168
+ poetry.toml
169
+
170
+ # ruff
171
+ .ruff_cache/
172
+
173
+ # LSP config files
174
+ pyrightconfig.json
175
+
176
+ ### VisualStudioCode ###
177
+ .vscode/*
178
+ !.vscode/settings.json
179
+ !.vscode/tasks.json
180
+ !.vscode/launch.json
181
+ !.vscode/extensions.json
182
+ !.vscode/*.code-snippets
183
+
184
+ # Local History for Visual Studio Code
185
+ .history/
186
+
187
+ # Built Visual Studio Code Extensions
188
+ *.vsix
189
+
190
+ ### VisualStudioCode Patch ###
191
+ # Ignore all local history of files
192
+ .history
193
+ .ionide
194
+
195
+ # End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
196
+ *.ipynb
197
+ node_modules
adetailer-dev/adetailer-dev/.pre-commit-config.yaml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ci:
2
+ autoupdate_branch: "dev"
3
+
4
+ repos:
5
+ - repo: https://github.com/pre-commit/pre-commit-hooks
6
+ rev: v4.6.0
7
+ hooks:
8
+ - id: check-added-large-files
9
+ args: [--maxkb=100]
10
+ - id: check-merge-conflict
11
+ - id: check-case-conflict
12
+ - id: check-ast
13
+ - id: check-yaml
14
+ - id: trailing-whitespace
15
+ args: [--markdown-linebreak-ext=md]
16
+ - id: end-of-file-fixer
17
+ - id: mixed-line-ending
18
+
19
+ - repo: https://github.com/rbubley/mirrors-prettier
20
+ rev: v3.3.3
21
+ hooks:
22
+ - id: prettier
23
+
24
+ - repo: https://github.com/astral-sh/ruff-pre-commit
25
+ rev: v0.5.2
26
+ hooks:
27
+ - id: ruff
28
+ args: [--fix, --exit-non-zero-on-fix]
29
+ - id: ruff-format
adetailer-dev/adetailer-dev/.vscode/extensions.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "recommendations": [
3
+ "ms-python.vscode-pylance",
4
+ "ms-python.black-formatter",
5
+ "kevinrose.vsc-python-indent",
6
+ "charliermarsh.ruff",
7
+ "shardulm94.trailing-spaces"
8
+ ]
9
+ }
adetailer-dev/adetailer-dev/.vscode/settings.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "explorer.fileNesting.enabled": true,
3
+ "explorer.fileNesting.patterns": {
4
+ "pyproject.toml": ".env, .gitignore, .pre-commit-config.yaml, Taskfile.yml",
5
+ "README.md": "LICENSE.md, CHANGELOG.md",
6
+ "install.py": "preload.py"
7
+ }
8
+ }
adetailer-dev/adetailer-dev/CHANGELOG.md ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## 2024-06-16
4
+
5
+ - v24.6.0
6
+ - webui 1.6.0 미만 버전을 위한 기능들을 제거하고, 최소 버전을 1.6.0으로 올림
7
+ - 허깅페이스 연결을 체크하는데 1초만 소요되도록 함
8
+ - 허깅페이스 미러 (hf-mirror.com)도 체크함 (합쳐서 2초)
9
+ - InputAccordion을 적용함
10
+
11
+ ## 2024-05-20
12
+
13
+ - v24.5.1
14
+ - uv를 사용하지 않게 함
15
+ - 모든 허깅페이스 모델을 동시에 다운로드 시도함
16
+ - 기본 탭 수를 2에서 4로 변경
17
+
18
+ ## 2024-05-19
19
+
20
+ - v24.5.0
21
+ - 개별 탭 활성화/비활성화 체크박스 추가
22
+ - ad_extra_model_dir 옵션에 |로 구분된 여러 디렉토리를 추가할 수 있게 함 (PR #596)
23
+ - `hypertile` 빌트인 확장이 지원되도록 함
24
+ - 항상 cond 캐시를 비움
25
+ - 설치 스크립트에 uv를 사용함
26
+ - mediapipe 최소 버전을 올려 protobuf 버전 4를 사용하게 함
27
+
28
+ ## 2024-04-17
29
+
30
+ - v24.4.2
31
+ - `params.txt` 파일이 없을 때 에러가 발생하지 않도록 수정
32
+ - 파이썬 3.9 이하에서 유니온 타입 에러 방지
33
+
34
+ ## 2024-04-14
35
+
36
+ - v24.4.1
37
+ - webui 1.9.0에서 발생한 에러 수정
38
+ - extra generation params에 callable이 들어와서 생긴 문제
39
+ - assign_current_image에 None이 들어갈 수 있던 문제
40
+ - webui 1.9.0에서 변경된 scheduler 지원
41
+ - 컨트롤넷 모델을 찾을 때, 대소문자 구분을 하지 않음 (PR #577)
42
+ - 몇몇 기능을 스크립트에서 분리하여 별도 파일로 빼냄
43
+
44
+ ## 2024-04-10
45
+
46
+ - v24.4.0
47
+ - txt2img에서 hires를 설정했을 때, 이미지의 exif에서 Denoising Strength가 adetailer의 denoisiog stregnth로 덮어 쓰이는 문제 수정
48
+ - ad prompt, ad negative prompt에 프롬프트를 변경하는 기능을 적용했을 때(와일드카드 등), 적용된 프롬프트가 이미지의 exif에 제대로 표시됨
49
+
50
+ ## 2024-03-29
51
+
52
+ - v24.3.5
53
+ - 알 수 없는 이유로 인페인팅을 확인하는 과정에서 Txt2Img 인스턴스가 들어오는 문제에 대한 임시 해결
54
+
55
+ ## 2024-03-28
56
+
57
+ - v24.3.4
58
+ - 인페인트에서, 이미지 해상도가 16의 배수가 아닐 때 사이즈 불일치로 인한 opencv 에러 방지
59
+
60
+ ## 2024-03-25
61
+
62
+ - v24.3.3
63
+ - webui 1.6.0 미만 버전에서 create_binary_mask 함수에 대해 ImportError가 발생하는 것 수정
64
+
65
+ ## 2024-03-21
66
+
67
+ - v24.3.2
68
+ - UI를 거치지 않은 입력에 대해, image_mask를 입력했을 때 opencv 에러가 발생하는 것 수정
69
+ - img2img inpaint에서 skip img2img 옵션을 활성화할 경우, adetailer를 비활성화함
70
+ - 마스크 크기에 대해 해결하기 힘든 문제가 있음
71
+
72
+ ## 2024-03-16
73
+
74
+ - v24.3.1
75
+ - YOLO World v2, YOLO9 지원가능한 버전으로 ultralytics 업데이트
76
+ - inpaint full res인 경우 인페인트 모드에서 동작하게 변경
77
+ - inpaint full res가 아닌 경우, 사용자가 입력한 마스크와 교차점이 있는 마스크만 선택하여 사용함
78
+
79
+ ## 2024-03-01
80
+
81
+ - v24.3.0
82
+ - YOLO World 모델 추가: 가장 큰 yolov8x-world.pt 모델만 기본적으로 선택할 수 있게 함.
83
+ - lllyasviel/stable-diffusion-webui-forge에서 컨트롤넷을 사용가능하게 함 (PR #517)
84
+ - 기본 스크립트 목록에 soft_inpainting 추가 (https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14208)
85
+
86
+ - 기존에 설치한 사람에게 소급적용되지는 않음
87
+
88
+ - 감지모델에 대한 간단한 pytest 추가함
89
+ - xyz grid 컨트롤넷 모델 옵션에 `Passthrough` 추가함
90
+
91
+ ## 2024-01-23
92
+
93
+ - v24.1.2
94
+ - controlnet 모델에 `Passthrough` 옵션 추가. 입력으로 들어온 컨트롤넷 옵션을 그대로 사용
95
+ - fastapi 엔드포인트 추가
96
+
97
+ ## 2024-01-10
98
+
99
+ - v24.1.1
100
+ - SDNext 호환 업데이트 (issue #466)
101
+ - 설정 값 state에 초기값 추가
102
+ - 위젯 값을 변경할 때마다 state도 변경되게 함 (기존에는 생성 버튼을 누를 때 적용되었음)
103
+ - `inpaint_depth_hand` 컨트롤넷 모델이 depth 모델로 인식되게 함 (issue #463)
104
+
105
+ ## 2024-01-04
106
+
107
+ - v24.1.0
108
+ - `depth_hand_refiner` ControlNet 추가 (PR #460)
109
+
110
+ ## 2023-12-30
111
+
112
+ - v23.12.0
113
+ - 파일을 인자로 추가하는 몇몇 스크립트에 대해 deepcopy의 에러를 피하기 위해 script_args 복사 방법을 변경함
114
+ - skip img2img 기능을 사용할 때 너비, 높이를 128로 고정하여 스킵 과정이 조금 더 나아짐
115
+ - img2img inpainting 모드에서 adetailer 자동 비활성화
116
+ - 처음 생성된 params.txt 파일을 항상 유지하도록 변경함
117
+
118
+ ## 2023-11-19
119
+
120
+ - v23.11.1
121
+ - 기본 스크립트 목록에 negpip 추가
122
+ - 기존에 설치한 사람에게 소급적용되지는 않음
123
+ - skip img2img 옵션이 2스텝 이상일 때, 제대로 적용되지 않는 문제 수정
124
+ - SD.Next에서 이미지가 np.ndarray로 입력되는 경우 수정
125
+ - 컨트롤넷 경로를 sys.path에 추가하여 --data-dir등을 지정한 경우에도 임포트 에러가 일어나지 않게 함.
126
+
127
+ ## 2023-10-30
128
+
129
+ - v23.11.0
130
+ - 이미지의 인덱스 계산방법 변경
131
+ - webui 1.1.0 미만에서 adetailer 실행 불가능하게 함
132
+ - 컨트롤넷 preprocessor 선택지 늘림
133
+ - 추가 yolo 모델 디렉터리를 설정할 수 있는 옵션 추가
134
+ - infotext에 `/`가 있는 항목이 exif에서 복원되지 않는 문제 수정
135
+ - 이전 버전에 생성된 이미지는 여전히 복원안됨
136
+ - 같은 탭에서 항상 같은 시드를 적용하게 하는 옵션 추가
137
+ - 컨트롤넷 1.1.411 (f2aafcf2beb99a03cbdf7db73852228ccd6bd1d6) 버전을 사용중일 경우,
138
+ webui 버전 1.6.0 미만에서 사용할 수 없다는 메세지 출력
139
+
140
+ ## 2023-10-15
141
+
142
+ - v23.10.1
143
+ - xyz grid에 prompt S/R 추가
144
+ - img2img에서 steps가 1일때 에러가 발생하는 샘플러의 처리를 위해 샘플러 이름도 변경하게 수정
145
+
146
+ ## 2023-10-07
147
+
148
+ - v23.10.0
149
+ - 허깅페이스 모델을 다운로드 실패했을 때, 계속 다운로드를 시도하지 않음
150
+ - img2img에서 img2img단계를 건너뛰는 기능 추가
151
+ - live preview에서 감지 단계를 보여줌 (PR #352)
152
+
153
+ ## 2023-09-20
154
+
155
+ - v23.9.3
156
+ - ultralytics 버전 8.0.181로 업데이트 (https://github.com/ultralytics/ultralytics/pull/4891)
157
+ - mediapipe와 ultralytics의 lazy import
158
+
159
+ ## 2023-09-10
160
+
161
+ - v23.9.2
162
+ - (실험적) VAE 선택 기능
163
+
164
+ ## 2023-09-01
165
+
166
+ - v23.9.1
167
+ - webui 1.6.0에 추가된 인자를 사용해서 생긴 하위 호환 문제 수정
168
+
169
+ ## 2023-08-31
170
+
171
+ - v23.9.0
172
+ - (실험적) 체크포인트 선택기능
173
+ - 버그가 있어 리프레시 버튼은 구현에서 빠짐
174
+ - 1.6.0 업데이트에 따라 img2img에서 사용불가능한 샘플러를 선택했을 때 더이상 Euler로 변경하지 않음
175
+ - 유효하지 않은 인자가 전달되었을 때, 에러를 일으키지 않고 대신 adetailer를 비활성화함
176
+
177
+ ## 2023-08-25
178
+
179
+ - v23.8.1
180
+ - xyz grid에서 model을 `None`으로 설정한 이후에 adetailer가 비활성화 되는 문제 수정
181
+ - skip을 눌렀을 때 진행을 멈춤
182
+ - `--medvram-sdxl`을 설정했을 때에도 cpu를 사용하게 함
183
+
184
+ ## 2023-08-14
185
+
186
+ - v23.8.0
187
+ - `[PROMPT]` 키워드 추가. `ad_prompt` 또는 `ad_negative_prompt`에 사용하면 입력 프롬프트로 대체됨 (PR #243)
188
+ - Only top k largest 옵션 추가 (PR #264)
189
+ - ultralytics 버전 업데이트
190
+
191
+ ## 2023-07-31
192
+
193
+ - v23.7.11
194
+ - separate clip skip 옵션 추가
195
+ - install requirements 정리 (ultralytics 새 버전, mediapipe~=3.20)
196
+
197
+ ## 2023-07-28
198
+
199
+ - v23.7.10
200
+ - ultralytics, mediapipe import문 정리
201
+ - traceback에서 컬러를 없앰 (api 때문), 라이브러리 버전도 보여주게 설정.
202
+ - huggingface_hub, pydantic을 install.py에서 없앰
203
+ - 안쓰는 컨트롤넷 관련 코드 삭제
204
+
205
+ ## 2023-07-23
206
+
207
+ - v23.7.9
208
+ - `ultralytics.utils` ModuleNotFoundError 해결 (https://github.com/ultralytics/ultralytics/issues/3856)
209
+ - `pydantic` 2.0 이상 버전 설치안되도록 함
210
+ - `controlnet_dir` cmd args 문제 수정 (PR #107)
211
+
212
+ ## 2023-07-20
213
+
214
+ - v23.7.8
215
+ - `paste_field_names` 추가했던 것을 되돌림
216
+
217
+ ## 2023-07-19
218
+
219
+ - v23.7.7
220
+ - 인페인팅 단계에서 별도의 샘플러를 선택할 수 있게 옵션을 추가함 (xyz그리드에도 추가)
221
+ - webui 1.0.0-pre 이하 버전에서 batch index 문제 수정
222
+ - 스크립트에 `paste_field_names`을 추가함. 사용되는지는 모르겠음
223
+
224
+ ## 2023-07-16
225
+
226
+ - v23.7.6
227
+ - `ultralytics 8.0.135`에 추가된 cpuinfo 기능을 위해 `py-cpuinfo`를 미리 설치하게 함. (미리 설치 안하면 cpu나 mps사용할 때 재시작해야함)
228
+ - init_image가 RGB 모드가 아닐 때 RGB로 변경.
229
+
230
+ ## 2023-07-07
231
+
232
+ - v23.7.4
233
+ - batch count > 1일때 프롬프트의 인덱스 문제 수정
234
+
235
+ - v23.7.5
236
+ - i2i의 `cached_uc`와 `cached_c`가 p의 `cached_uc`와 `cached_c`가 다른 인스턴스가 되도록 수정
237
+
238
+ ## 2023-07-05
239
+
240
+ - v23.7.3
241
+ - 버그 수정
242
+ - `object()`가 json 직렬화 안되는 문제
243
+ - `process`를 호출함에 따라 배치 카운트가 2이상일 때, all_prompts가 고정되는 문제
244
+ - `ad-before`와 `ad-preview` 이미지 파일명이 실제 파일명과 다른 문제
245
+ - pydantic 2.0 호환성 문제
246
+
247
+ ## 2023-07-04
248
+
249
+ - v23.7.2
250
+ - `mediapipe_face_mesh_eyes_only` 모델 추가: `mediapipe_face_mesh`로 감지한 뒤 눈만 사용함.
251
+ - 매 배치 시작 전에 `scripts.postprocess`를, 후에 `scripts.process`를 호출함.
252
+ - 컨트롤넷을 사용하면 소요 시간이 조금 늘어나지만 몇몇 문제 해결에 도움이 됨.
253
+ - `lora_block_weight`를 스크립트 화이트리스트에 추가함.
254
+ - 한번이라도 ADetailer를 사용한 사람은 수동으로 추가해야함.
255
+
256
+ ## 2023-07-03
257
+
258
+ - v23.7.1
259
+ - `process_images`를 진행한 뒤 `StableDiffusionProcessing` 오브젝트의 close를 호출함
260
+ - api 호출로 사용했는지 확인하는 속성 추가
261
+ - `NansException`이 발생했을 때 중지하지 않고 남은 과정 계속 진행함
262
+
263
+ ## 2023-07-02
264
+
265
+ - v23.7.0
266
+ - `NansException`이 발생하면 로그에 표시하고 원본 이미지를 반환하게 설정
267
+ - `rich`를 사용한 에러 트레이싱
268
+ - install.py에 `rich` 추가
269
+ - 생성 중에 컴포넌트의 값을 변경하면 args의 값도 함께 ��경되는 문제 수정 (issue #180)
270
+ - 터미널 로그로 ad_prompt와 ad_negative_prompt에 적용된 실제 프롬프트 확인할 수 있음 (입력과 다를 경우에만)
271
+
272
+ ## 2023-06-28
273
+
274
+ - v23.6.4
275
+ - 최대 모델 수 5 -> 10개
276
+ - ad_prompt와 ad_negative_prompt에 빈칸으로 놔두면 입력 프롬프트가 사용된다는 문구 추가
277
+ - huggingface 모델 다운로드 실패시 로깅
278
+ - 1st 모델이 `None`일 경우 나머지 입력을 무시하던 문제 수정
279
+ - `--use-cpu` 에 `adetailer` 입력 시 cpu로 yolo모델을 사용함
280
+
281
+ ## 2023-06-20
282
+
283
+ - v23.6.3
284
+ - 컨트롤넷 inpaint 모델에 대해, 3가지 모듈을 사용할 수 있도록 함
285
+ - Noise Multiplier 옵션 추가 (PR #149)
286
+ - pydantic 최소 버전 1.10.8로 설정 (Issue #146)
287
+
288
+ ## 2023-06-05
289
+
290
+ - v23.6.2
291
+ - xyz_grid에서 ADetailer를 사용할 수 있게함.
292
+ - 8가지 옵션만 1st 탭에 적용되도록 함.
293
+
294
+ ## 2023-06-01
295
+
296
+ - v23.6.1
297
+ - `inpaint, scribble, lineart, openpose, tile` 5가지 컨트롤넷 모델 지원 (PR #107)
298
+ - controlnet guidance start, end 인자 추가 (PR #107)
299
+ - `modules.extensions`를 사용하여 컨트롤넷 확장을 불러오고 경로를 알아내로록 변경
300
+ - ui에서 컨트롤넷을 별도 함수로 분리
301
+
302
+ ## 2023-05-30
303
+
304
+ - v23.6.0
305
+ - 스크립트의 이름을 `After Detailer`에서 `ADetailer`로 변경
306
+ - API 사용자는 변경 필요함
307
+ - 몇몇 설정 변경
308
+ - `ad_conf` → `ad_confidence`. 0~100 사이의 int → 0.0~1.0 사이의 float
309
+ - `ad_inpaint_full_res` → `ad_inpaint_only_masked`
310
+ - `ad_inpaint_full_res_padding` → `ad_inpaint_only_masked_padding`
311
+ - mediapipe face mesh 모델 추가
312
+
313
+ - mediapipe 최소 버전 `0.10.0`
314
+
315
+ - rich traceback 제거함
316
+ - huggingface 다운로드 실패할 때 에러가 나지 않게 하고 해당 모델을 제거함
317
+
318
+ ## 2023-05-26
319
+
320
+ - v23.5.19
321
+ - 1번째 탭에도 `None` 옵션을 추가함
322
+ - api로 ad controlnet model에 inpaint가 아닌 다른 컨트롤넷 모델을 사용하지 못하도록 막음
323
+ - adetailer 진행중에 total tqdm 진행바 업데이트를 멈춤
324
+ - state.inturrupted 상태에서 adetailer 과정을 중지함
325
+ - 컨트롤넷 process를 각 batch가 끝난 순간에만 호출하도록 변경
326
+
327
+ ### 2023-05-25
328
+
329
+ - v23.5.18
330
+ - 컨트롤넷 관련 수정
331
+ - unit의 `input_mode`를 `SIMPLE`로 모두 변경
332
+ - 컨트롤넷 유넷 훅과 하이잭 함수들을 adetailer를 실행할 때에만 되돌리는 기능 추가
333
+ - adetailer 처리가 끝난 뒤 컨트롤넷 스크립트의 process를 다시 진행함. (batch count 2 이상일때의 문제 해결)
334
+ - 기본 활성 스크립트 목록에서 컨트롤넷을 뺌
335
+
336
+ ### 2023-05-22
337
+
338
+ - v23.5.17
339
+ - 컨트롤넷 확장이 있으면 컨트롤넷 스크립트를 활성화함. (컨트롤넷 관련 문제 해결)
340
+ - 모든 컴포넌트에 elem_id 설정
341
+ - ui에 버전을 표시함
342
+
343
+ ### 2023-05-19
344
+
345
+ - v23.5.16
346
+ - 추가한 옵션
347
+ - Mask min/max ratio
348
+ - Mask merge mode
349
+ - Restore faces after ADetailer
350
+ - 옵션들을 Accordion으로 묶음
351
+
352
+ ### 2023-05-18
353
+
354
+ - v23.5.15
355
+ - 필요한 것만 임포트하도록 변경 (vae 로딩 오류 없어짐. 로딩 속도 빨라짐)
356
+
357
+ ### 2023-05-17
358
+
359
+ - v23.5.14
360
+ - `[SKIP]`으로 ad prompt 일부를 건너뛰는 기능 추가
361
+ - bbox 정렬 옵션 추가
362
+ - sd_webui 타입힌트를 만들어냄
363
+ - enable checker와 관련된 api 오류 수정?
364
+
365
+ ### 2023-05-15
366
+
367
+ - v23.5.13
368
+ - `[SEP]`으로 ad prompt를 분리하여 적용하는 기능 추가
369
+ - enable checker를 다시 pydantic으로 변경함
370
+ - ui 관련 함수를 adetailer.ui 폴더로 분리함
371
+ - controlnet을 사용할 때 모든 controlnet unit 비활성화
372
+ - adetailer 폴더가 없으면 만들게 함
373
+
374
+ ### 2023-05-13
375
+
376
+ - v23.5.12
377
+ - `ad_enable`을 제외한 입력이 dict타입으로 들어오도록 변경
378
+ - web api로 사용할 때에 특히 사용하기 쉬움
379
+ - web api breaking change
380
+ - `mask_preprocess` 인자를 넣지 않았던 오류 수정 (PR #47)
381
+ - huggingface에서 모델을 다운로드하지 않는 옵션 추가 `--ad-no-huggingface`
382
+
383
+ ### 2023-05-12
384
+
385
+ - v23.5.11
386
+ - `ultralytics` 알람 제거
387
+ - 필요없는 exif 인자 더 제거함
388
+ - `use separate steps` 옵션 추가
389
+ - ui 배치를 조정함
390
+
391
+ ### 2023-05-09
392
+
393
+ - v23.5.10
394
+ - 선택한 스크립트만 ADetailer에 적용하는 옵션 추가, 기본값 `True`. 설정 탭에서 지정가능.
395
+ - 기본값: `dynamic_prompting,dynamic_thresholding,wildcards,wildcard_recursive`
396
+ - `person_yolov8s-seg.pt` 모델 추가
397
+ - `ultralytics`의 최소 버전을 `8.0.97`로 설정 (C:\\ 문제 해결된 버전)
398
+
399
+ ### 2023-05-08
400
+
401
+ - v23.5.9
402
+ - 2가지 이상의 모델을 사용할 수 있음. 기본값: 2, 최대: 5
403
+ - segment 모델을 사용할 수 있게 함. `person_yolov8n-seg.pt` 추가
404
+
405
+ ### 2023-05-07
406
+
407
+ - v23.5.8
408
+ - 프롬프트와 네거티브 프롬프트에 방향키 지원 (PR #24)
409
+ - `mask_preprocess`를 추가함. 이전 버전과 시드값이 달라질 가능성 있음!
410
+ - 이미지 처리가 일어났을 때에만 before이미지를 저장함
411
+ - 설��창의 레이블을 ADetailer 대신 더 적절하게 수정함
412
+
413
+ ### 2023-05-06
414
+
415
+ - v23.5.7
416
+ - `ad_use_cfg_scale` 옵션 추가. cfg 스케일을 따로 사용할지 말지 결정함.
417
+ - `ad_enable` 기본값을 `True`에서 `False`로 변경
418
+ - `ad_model`의 기본값을 `None`에서 첫번째 모델로 변경
419
+ - 최소 2개의 입력(ad_enable, ad_model)만 들어오면 작동하게 변경.
420
+
421
+ - v23.5.7.post0
422
+ - `init_controlnet_ext`을 controlnet_exists == True일때에만 실행
423
+ - webui를 C드라이브 바로 밑에 설치한 사람들에게 `ultralytics` 경고 표시
424
+
425
+ ### 2023-05-05 (어린이날)
426
+
427
+ - v23.5.5
428
+ - `Save images before ADetailer` 옵션 추가
429
+ - 입력으로 들어온 인자와 ALL_ARGS의 길이가 다르면 에러메세지
430
+ - README.md에 설치방법 추가
431
+
432
+ - v23.5.6
433
+ - get_args에서 IndexError가 발생하면 자세한 에러메세지를 볼 수 있음
434
+ - AdetailerArgs에 extra_params 내장
435
+ - scripts_args를 딥카피함
436
+ - postprocess_image를 약간 분리함
437
+
438
+ - v23.5.6.post0
439
+ - `init_controlnet_ext`에서 에러메세지를 자세히 볼 수 있음
440
+
441
+ ### 2023-05-04
442
+
443
+ - v23.5.4
444
+ - use pydantic for arguments validation
445
+ - revert: ad_model to `None` as default
446
+ - revert: `__future__` imports
447
+ - lazily import yolo and mediapipe
448
+
449
+ ### 2023-05-03
450
+
451
+ - v23.5.3.post0
452
+ - remove `__future__` imports
453
+ - change to copy scripts and scripts args
454
+
455
+ - v23.5.3.post1
456
+ - change default ad_model from `None`
457
+
458
+ ### 2023-05-02
459
+
460
+ - v23.5.3
461
+ - Remove `None` from model list and add `Enable ADetailer` checkbox.
462
+ - install.py `skip_install` fix.
adetailer-dev/adetailer-dev/LICENSE.md ADDED
@@ -0,0 +1,661 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ The GNU Affero General Public License is a free, copyleft license for
11
+ software and other kinds of works, specifically designed to ensure
12
+ cooperation with the community in the case of network server software.
13
+
14
+ The licenses for most software and other practical works are designed
15
+ to take away your freedom to share and change the works. By contrast,
16
+ our General Public Licenses are intended to guarantee your freedom to
17
+ share and change all versions of a program--to make sure it remains free
18
+ software for all its users.
19
+
20
+ When we speak of free software, we are referring to freedom, not
21
+ price. Our General Public Licenses are designed to make sure that you
22
+ have the freedom to distribute copies of free software (and charge for
23
+ them if you wish), that you receive source code or can get it if you
24
+ want it, that you can change the software or use pieces of it in new
25
+ free programs, and that you know you can do these things.
26
+
27
+ Developers that use our General Public Licenses protect your rights
28
+ with two steps: (1) assert copyright on the software, and (2) offer
29
+ you this License which gives you legal permission to copy, distribute
30
+ and/or modify the software.
31
+
32
+ A secondary benefit of defending all users' freedom is that
33
+ improvements made in alternate versions of the program, if they
34
+ receive widespread use, become available for other developers to
35
+ incorporate. Many developers of free software are heartened and
36
+ encouraged by the resulting cooperation. However, in the case of
37
+ software used on network servers, this result may fail to come about.
38
+ The GNU General Public License permits making a modified version and
39
+ letting the public access it on a server without ever releasing its
40
+ source code to the public.
41
+
42
+ The GNU Affero General Public License is designed specifically to
43
+ ensure that, in such cases, the modified source code becomes available
44
+ to the community. It requires the operator of a network server to
45
+ provide the source code of the modified version running there to the
46
+ users of that server. Therefore, public use of a modified version, on
47
+ a publicly accessible server, gives the public access to the source
48
+ code of the modified version.
49
+
50
+ An older license, called the Affero General Public License and
51
+ published by Affero, was designed to accomplish similar goals. This is
52
+ a different license, not a version of the Affero GPL, but Affero has
53
+ released a new version of the Affero GPL which permits relicensing under
54
+ this license.
55
+
56
+ The precise terms and conditions for copying, distribution and
57
+ modification follow.
58
+
59
+ TERMS AND CONDITIONS
60
+
61
+ 0. Definitions.
62
+
63
+ "This License" refers to version 3 of the GNU Affero General Public License.
64
+
65
+ "Copyright" also means copyright-like laws that apply to other kinds of
66
+ works, such as semiconductor masks.
67
+
68
+ "The Program" refers to any copyrightable work licensed under this
69
+ License. Each licensee is addressed as "you". "Licensees" and
70
+ "recipients" may be individuals or organizations.
71
+
72
+ To "modify" a work means to copy from or adapt all or part of the work
73
+ in a fashion requiring copyright permission, other than the making of an
74
+ exact copy. The resulting work is called a "modified version" of the
75
+ earlier work or a work "based on" the earlier work.
76
+
77
+ A "covered work" means either the unmodified Program or a work based
78
+ on the Program.
79
+
80
+ To "propagate" a work means to do anything with it that, without
81
+ permission, would make you directly or secondarily liable for
82
+ infringement under applicable copyright law, except executing it on a
83
+ computer or modifying a private copy. Propagation includes copying,
84
+ distribution (with or without modification), making available to the
85
+ public, and in some countries other activities as well.
86
+
87
+ To "convey" a work means any kind of propagation that enables other
88
+ parties to make or receive copies. Mere interaction with a user through
89
+ a computer network, with no transfer of a copy, is not conveying.
90
+
91
+ An interactive user interface displays "Appropriate Legal Notices"
92
+ to the extent that it includes a convenient and prominently visible
93
+ feature that (1) displays an appropriate copyright notice, and (2)
94
+ tells the user that there is no warranty for the work (except to the
95
+ extent that warranties are provided), that licensees may convey the
96
+ work under this License, and how to view a copy of this License. If
97
+ the interface presents a list of user commands or options, such as a
98
+ menu, a prominent item in the list meets this criterion.
99
+
100
+ 1. Source Code.
101
+
102
+ The "source code" for a work means the preferred form of the work
103
+ for making modifications to it. "Object code" means any non-source
104
+ form of a work.
105
+
106
+ A "Standard Interface" means an interface that either is an official
107
+ standard defined by a recognized standards body, or, in the case of
108
+ interfaces specified for a particular programming language, one that
109
+ is widely used among developers working in that language.
110
+
111
+ The "System Libraries" of an executable work include anything, other
112
+ than the work as a whole, that (a) is included in the normal form of
113
+ packaging a Major Component, but which is not part of that Major
114
+ Component, and (b) serves only to enable use of the work with that
115
+ Major Component, or to implement a Standard Interface for which an
116
+ implementation is available to the public in source code form. A
117
+ "Major Component", in this context, means a major essential component
118
+ (kernel, window system, and so on) of the specific operating system
119
+ (if any) on which the executable work runs, or a compiler used to
120
+ produce the work, or an object code interpreter used to run it.
121
+
122
+ The "Corresponding Source" for a work in object code form means all
123
+ the source code needed to generate, install, and (for an executable
124
+ work) run the object code and to modify the work, including scripts to
125
+ control those activities. However, it does not include the work's
126
+ System Libraries, or general-purpose tools or generally available free
127
+ programs which are used unmodified in performing those activities but
128
+ which are not part of the work. For example, Corresponding Source
129
+ includes interface definition files associated with source files for
130
+ the work, and the source code for shared libraries and dynamically
131
+ linked subprograms that the work is specifically designed to require,
132
+ such as by intimate data communication or control flow between those
133
+ subprograms and other parts of the work.
134
+
135
+ The Corresponding Source need not include anything that users
136
+ can regenerate automatically from other parts of the Corresponding
137
+ Source.
138
+
139
+ The Corresponding Source for a work in source code form is that
140
+ same work.
141
+
142
+ 2. Basic Permissions.
143
+
144
+ All rights granted under this License are granted for the term of
145
+ copyright on the Program, and are irrevocable provided the stated
146
+ conditions are met. This License explicitly affirms your unlimited
147
+ permission to run the unmodified Program. The output from running a
148
+ covered work is covered by this License only if the output, given its
149
+ content, constitutes a covered work. This License acknowledges your
150
+ rights of fair use or other equivalent, as provided by copyright law.
151
+
152
+ You may make, run and propagate covered works that you do not
153
+ convey, without conditions so long as your license otherwise remains
154
+ in force. You may convey covered works to others for the sole purpose
155
+ of having them make modifications exclusively for you, or provide you
156
+ with facilities for running those works, provided that you comply with
157
+ the terms of this License in conveying all material for which you do
158
+ not control copyright. Those thus making or running the covered works
159
+ for you must do so exclusively on your behalf, under your direction
160
+ and control, on terms that prohibit them from making any copies of
161
+ your copyrighted material outside their relationship with you.
162
+
163
+ Conveying under any other circumstances is permitted solely under
164
+ the conditions stated below. Sublicensing is not allowed; section 10
165
+ makes it unnecessary.
166
+
167
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168
+
169
+ No covered work shall be deemed part of an effective technological
170
+ measure under any applicable law fulfilling obligations under article
171
+ 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172
+ similar laws prohibiting or restricting circumvention of such
173
+ measures.
174
+
175
+ When you convey a covered work, you waive any legal power to forbid
176
+ circumvention of technological measures to the extent such circumvention
177
+ is effected by exercising rights under this License with respect to
178
+ the covered work, and you disclaim any intention to limit operation or
179
+ modification of the work as a means of enforcing, against the work's
180
+ users, your or third parties' legal rights to forbid circumvention of
181
+ technological measures.
182
+
183
+ 4. Conveying Verbatim Copies.
184
+
185
+ You may convey verbatim copies of the Program's source code as you
186
+ receive it, in any medium, provided that you conspicuously and
187
+ appropriately publish on each copy an appropriate copyright notice;
188
+ keep intact all notices stating that this License and any
189
+ non-permissive terms added in accord with section 7 apply to the code;
190
+ keep intact all notices of the absence of any warranty; and give all
191
+ recipients a copy of this License along with the Program.
192
+
193
+ You may charge any price or no price for each copy that you convey,
194
+ and you may offer support or warranty protection for a fee.
195
+
196
+ 5. Conveying Modified Source Versions.
197
+
198
+ You may convey a work based on the Program, or the modifications to
199
+ produce it from the Program, in the form of source code under the
200
+ terms of section 4, provided that you also meet all of these conditions:
201
+
202
+ a) The work must carry prominent notices stating that you modified
203
+ it, and giving a relevant date.
204
+
205
+ b) The work must carry prominent notices stating that it is
206
+ released under this License and any conditions added under section
207
+ 7. This requirement modifies the requirement in section 4 to
208
+ "keep intact all notices".
209
+
210
+ c) You must license the entire work, as a whole, under this
211
+ License to anyone who comes into possession of a copy. This
212
+ License will therefore apply, along with any applicable section 7
213
+ additional terms, to the whole of the work, and all its parts,
214
+ regardless of how they are packaged. This License gives no
215
+ permission to license the work in any other way, but it does not
216
+ invalidate such permission if you have separately received it.
217
+
218
+ d) If the work has interactive user interfaces, each must display
219
+ Appropriate Legal Notices; however, if the Program has interactive
220
+ interfaces that do not display Appropriate Legal Notices, your
221
+ work need not make them do so.
222
+
223
+ A compilation of a covered work with other separate and independent
224
+ works, which are not by their nature extensions of the covered work,
225
+ and which are not combined with it such as to form a larger program,
226
+ in or on a volume of a storage or distribution medium, is called an
227
+ "aggregate" if the compilation and its resulting copyright are not
228
+ used to limit the access or legal rights of the compilation's users
229
+ beyond what the individual works permit. Inclusion of a covered work
230
+ in an aggregate does not cause this License to apply to the other
231
+ parts of the aggregate.
232
+
233
+ 6. Conveying Non-Source Forms.
234
+
235
+ You may convey a covered work in object code form under the terms
236
+ of sections 4 and 5, provided that you also convey the
237
+ machine-readable Corresponding Source under the terms of this License,
238
+ in one of these ways:
239
+
240
+ a) Convey the object code in, or embodied in, a physical product
241
+ (including a physical distribution medium), accompanied by the
242
+ Corresponding Source fixed on a durable physical medium
243
+ customarily used for software interchange.
244
+
245
+ b) Convey the object code in, or embodied in, a physical product
246
+ (including a physical distribution medium), accompanied by a
247
+ written offer, valid for at least three years and valid for as
248
+ long as you offer spare parts or customer support for that product
249
+ model, to give anyone who possesses the object code either (1) a
250
+ copy of the Corresponding Source for all the software in the
251
+ product that is covered by this License, on a durable physical
252
+ medium customarily used for software interchange, for a price no
253
+ more than your reasonable cost of physically performing this
254
+ conveying of source, or (2) access to copy the
255
+ Corresponding Source from a network server at no charge.
256
+
257
+ c) Convey individual copies of the object code with a copy of the
258
+ written offer to provide the Corresponding Source. This
259
+ alternative is allowed only occasionally and noncommercially, and
260
+ only if you received the object code with such an offer, in accord
261
+ with subsection 6b.
262
+
263
+ d) Convey the object code by offering access from a designated
264
+ place (gratis or for a charge), and offer equivalent access to the
265
+ Corresponding Source in the same way through the same place at no
266
+ further charge. You need not require recipients to copy the
267
+ Corresponding Source along with the object code. If the place to
268
+ copy the object code is a network server, the Corresponding Source
269
+ may be on a different server (operated by you or a third party)
270
+ that supports equivalent copying facilities, provided you maintain
271
+ clear directions next to the object code saying where to find the
272
+ Corresponding Source. Regardless of what server hosts the
273
+ Corresponding Source, you remain obligated to ensure that it is
274
+ available for as long as needed to satisfy these requirements.
275
+
276
+ e) Convey the object code using peer-to-peer transmission, provided
277
+ you inform other peers where the object code and Corresponding
278
+ Source of the work are being offered to the general public at no
279
+ charge under subsection 6d.
280
+
281
+ A separable portion of the object code, whose source code is excluded
282
+ from the Corresponding Source as a System Library, need not be
283
+ included in conveying the object code work.
284
+
285
+ A "User Product" is either (1) a "consumer product", which means any
286
+ tangible personal property which is normally used for personal, family,
287
+ or household purposes, or (2) anything designed or sold for incorporation
288
+ into a dwelling. In determining whether a product is a consumer product,
289
+ doubtful cases shall be resolved in favor of coverage. For a particular
290
+ product received by a particular user, "normally used" refers to a
291
+ typical or common use of that class of product, regardless of the status
292
+ of the particular user or of the way in which the particular user
293
+ actually uses, or expects or is expected to use, the product. A product
294
+ is a consumer product regardless of whether the product has substantial
295
+ commercial, industrial or non-consumer uses, unless such uses represent
296
+ the only significant mode of use of the product.
297
+
298
+ "Installation Information" for a User Product means any methods,
299
+ procedures, authorization keys, or other information required to install
300
+ and execute modified versions of a covered work in that User Product from
301
+ a modified version of its Corresponding Source. The information must
302
+ suffice to ensure that the continued functioning of the modified object
303
+ code is in no case prevented or interfered with solely because
304
+ modification has been made.
305
+
306
+ If you convey an object code work under this section in, or with, or
307
+ specifically for use in, a User Product, and the conveying occurs as
308
+ part of a transaction in which the right of possession and use of the
309
+ User Product is transferred to the recipient in perpetuity or for a
310
+ fixed term (regardless of how the transaction is characterized), the
311
+ Corresponding Source conveyed under this section must be accompanied
312
+ by the Installation Information. But this requirement does not apply
313
+ if neither you nor any third party retains the ability to install
314
+ modified object code on the User Product (for example, the work has
315
+ been installed in ROM).
316
+
317
+ The requirement to provide Installation Information does not include a
318
+ requirement to continue to provide support service, warranty, or updates
319
+ for a work that has been modified or installed by the recipient, or for
320
+ the User Product in which it has been modified or installed. Access to a
321
+ network may be denied when the modification itself materially and
322
+ adversely affects the operation of the network or violates the rules and
323
+ protocols for communication across the network.
324
+
325
+ Corresponding Source conveyed, and Installation Information provided,
326
+ in accord with this section must be in a format that is publicly
327
+ documented (and with an implementation available to the public in
328
+ source code form), and must require no special password or key for
329
+ unpacking, reading or copying.
330
+
331
+ 7. Additional Terms.
332
+
333
+ "Additional permissions" are terms that supplement the terms of this
334
+ License by making exceptions from one or more of its conditions.
335
+ Additional permissions that are applicable to the entire Program shall
336
+ be treated as though they were included in this License, to the extent
337
+ that they are valid under applicable law. If additional permissions
338
+ apply only to part of the Program, that part may be used separately
339
+ under those permissions, but the entire Program remains governed by
340
+ this License without regard to the additional permissions.
341
+
342
+ When you convey a copy of a covered work, you may at your option
343
+ remove any additional permissions from that copy, or from any part of
344
+ it. (Additional permissions may be written to require their own
345
+ removal in certain cases when you modify the work.) You may place
346
+ additional permissions on material, added by you to a covered work,
347
+ for which you have or can give appropriate copyright permission.
348
+
349
+ Notwithstanding any other provision of this License, for material you
350
+ add to a covered work, you may (if authorized by the copyright holders of
351
+ that material) supplement the terms of this License with terms:
352
+
353
+ a) Disclaiming warranty or limiting liability differently from the
354
+ terms of sections 15 and 16 of this License; or
355
+
356
+ b) Requiring preservation of specified reasonable legal notices or
357
+ author attributions in that material or in the Appropriate Legal
358
+ Notices displayed by works containing it; or
359
+
360
+ c) Prohibiting misrepresentation of the origin of that material, or
361
+ requiring that modified versions of such material be marked in
362
+ reasonable ways as different from the original version; or
363
+
364
+ d) Limiting the use for publicity purposes of names of licensors or
365
+ authors of the material; or
366
+
367
+ e) Declining to grant rights under trademark law for use of some
368
+ trade names, trademarks, or service marks; or
369
+
370
+ f) Requiring indemnification of licensors and authors of that
371
+ material by anyone who conveys the material (or modified versions of
372
+ it) with contractual assumptions of liability to the recipient, for
373
+ any liability that these contractual assumptions directly impose on
374
+ those licensors and authors.
375
+
376
+ All other non-permissive additional terms are considered "further
377
+ restrictions" within the meaning of section 10. If the Program as you
378
+ received it, or any part of it, contains a notice stating that it is
379
+ governed by this License along with a term that is a further
380
+ restriction, you may remove that term. If a license document contains
381
+ a further restriction but permits relicensing or conveying under this
382
+ License, you may add to a covered work material governed by the terms
383
+ of that license document, provided that the further restriction does
384
+ not survive such relicensing or conveying.
385
+
386
+ If you add terms to a covered work in accord with this section, you
387
+ must place, in the relevant source files, a statement of the
388
+ additional terms that apply to those files, or a notice indicating
389
+ where to find the applicable terms.
390
+
391
+ Additional terms, permissive or non-permissive, may be stated in the
392
+ form of a separately written license, or stated as exceptions;
393
+ the above requirements apply either way.
394
+
395
+ 8. Termination.
396
+
397
+ You may not propagate or modify a covered work except as expressly
398
+ provided under this License. Any attempt otherwise to propagate or
399
+ modify it is void, and will automatically terminate your rights under
400
+ this License (including any patent licenses granted under the third
401
+ paragraph of section 11).
402
+
403
+ However, if you cease all violation of this License, then your
404
+ license from a particular copyright holder is reinstated (a)
405
+ provisionally, unless and until the copyright holder explicitly and
406
+ finally terminates your license, and (b) permanently, if the copyright
407
+ holder fails to notify you of the violation by some reasonable means
408
+ prior to 60 days after the cessation.
409
+
410
+ Moreover, your license from a particular copyright holder is
411
+ reinstated permanently if the copyright holder notifies you of the
412
+ violation by some reasonable means, this is the first time you have
413
+ received notice of violation of this License (for any work) from that
414
+ copyright holder, and you cure the violation prior to 30 days after
415
+ your receipt of the notice.
416
+
417
+ Termination of your rights under this section does not terminate the
418
+ licenses of parties who have received copies or rights from you under
419
+ this License. If your rights have been terminated and not permanently
420
+ reinstated, you do not qualify to receive new licenses for the same
421
+ material under section 10.
422
+
423
+ 9. Acceptance Not Required for Having Copies.
424
+
425
+ You are not required to accept this License in order to receive or
426
+ run a copy of the Program. Ancillary propagation of a covered work
427
+ occurring solely as a consequence of using peer-to-peer transmission
428
+ to receive a copy likewise does not require acceptance. However,
429
+ nothing other than this License grants you permission to propagate or
430
+ modify any covered work. These actions infringe copyright if you do
431
+ not accept this License. Therefore, by modifying or propagating a
432
+ covered work, you indicate your acceptance of this License to do so.
433
+
434
+ 10. Automatic Licensing of Downstream Recipients.
435
+
436
+ Each time you convey a covered work, the recipient automatically
437
+ receives a license from the original licensors, to run, modify and
438
+ propagate that work, subject to this License. You are not responsible
439
+ for enforcing compliance by third parties with this License.
440
+
441
+ An "entity transaction" is a transaction transferring control of an
442
+ organization, or substantially all assets of one, or subdividing an
443
+ organization, or merging organizations. If propagation of a covered
444
+ work results from an entity transaction, each party to that
445
+ transaction who receives a copy of the work also receives whatever
446
+ licenses to the work the party's predecessor in interest had or could
447
+ give under the previous paragraph, plus a right to possession of the
448
+ Corresponding Source of the work from the predecessor in interest, if
449
+ the predecessor has it or can get it with reasonable efforts.
450
+
451
+ You may not impose any further restrictions on the exercise of the
452
+ rights granted or affirmed under this License. For example, you may
453
+ not impose a license fee, royalty, or other charge for exercise of
454
+ rights granted under this License, and you may not initiate litigation
455
+ (including a cross-claim or counterclaim in a lawsuit) alleging that
456
+ any patent claim is infringed by making, using, selling, offering for
457
+ sale, or importing the Program or any portion of it.
458
+
459
+ 11. Patents.
460
+
461
+ A "contributor" is a copyright holder who authorizes use under this
462
+ License of the Program or a work on which the Program is based. The
463
+ work thus licensed is called the contributor's "contributor version".
464
+
465
+ A contributor's "essential patent claims" are all patent claims
466
+ owned or controlled by the contributor, whether already acquired or
467
+ hereafter acquired, that would be infringed by some manner, permitted
468
+ by this License, of making, using, or selling its contributor version,
469
+ but do not include claims that would be infringed only as a
470
+ consequence of further modification of the contributor version. For
471
+ purposes of this definition, "control" includes the right to grant
472
+ patent sublicenses in a manner consistent with the requirements of
473
+ this License.
474
+
475
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
476
+ patent license under the contributor's essential patent claims, to
477
+ make, use, sell, offer for sale, import and otherwise run, modify and
478
+ propagate the contents of its contributor version.
479
+
480
+ In the following three paragraphs, a "patent license" is any express
481
+ agreement or commitment, however denominated, not to enforce a patent
482
+ (such as an express permission to practice a patent or covenant not to
483
+ sue for patent infringement). To "grant" such a patent license to a
484
+ party means to make such an agreement or commitment not to enforce a
485
+ patent against the party.
486
+
487
+ If you convey a covered work, knowingly relying on a patent license,
488
+ and the Corresponding Source of the work is not available for anyone
489
+ to copy, free of charge and under the terms of this License, through a
490
+ publicly available network server or other readily accessible means,
491
+ then you must either (1) cause the Corresponding Source to be so
492
+ available, or (2) arrange to deprive yourself of the benefit of the
493
+ patent license for this particular work, or (3) arrange, in a manner
494
+ consistent with the requirements of this License, to extend the patent
495
+ license to downstream recipients. "Knowingly relying" means you have
496
+ actual knowledge that, but for the patent license, your conveying the
497
+ covered work in a country, or your recipient's use of the covered work
498
+ in a country, would infringe one or more identifiable patents in that
499
+ country that you have reason to believe are valid.
500
+
501
+ If, pursuant to or in connection with a single transaction or
502
+ arrangement, you convey, or propagate by procuring conveyance of, a
503
+ covered work, and grant a patent license to some of the parties
504
+ receiving the covered work authorizing them to use, propagate, modify
505
+ or convey a specific copy of the covered work, then the patent license
506
+ you grant is automatically extended to all recipients of the covered
507
+ work and works based on it.
508
+
509
+ A patent license is "discriminatory" if it does not include within
510
+ the scope of its coverage, prohibits the exercise of, or is
511
+ conditioned on the non-exercise of one or more of the rights that are
512
+ specifically granted under this License. You may not convey a covered
513
+ work if you are a party to an arrangement with a third party that is
514
+ in the business of distributing software, under which you make payment
515
+ to the third party based on the extent of your activity of conveying
516
+ the work, and under which the third party grants, to any of the
517
+ parties who would receive the covered work from you, a discriminatory
518
+ patent license (a) in connection with copies of the covered work
519
+ conveyed by you (or copies made from those copies), or (b) primarily
520
+ for and in connection with specific products or compilations that
521
+ contain the covered work, unless you entered into that arrangement,
522
+ or that patent license was granted, prior to 28 March 2007.
523
+
524
+ Nothing in this License shall be construed as excluding or limiting
525
+ any implied license or other defenses to infringement that may
526
+ otherwise be available to you under applicable patent law.
527
+
528
+ 12. No Surrender of Others' Freedom.
529
+
530
+ If conditions are imposed on you (whether by court order, agreement or
531
+ otherwise) that contradict the conditions of this License, they do not
532
+ excuse you from the conditions of this License. If you cannot convey a
533
+ covered work so as to satisfy simultaneously your obligations under this
534
+ License and any other pertinent obligations, then as a consequence you may
535
+ not convey it at all. For example, if you agree to terms that obligate you
536
+ to collect a royalty for further conveying from those to whom you convey
537
+ the Program, the only way you could satisfy both those terms and this
538
+ License would be to refrain entirely from conveying the Program.
539
+
540
+ 13. Remote Network Interaction; Use with the GNU General Public License.
541
+
542
+ Notwithstanding any other provision of this License, if you modify the
543
+ Program, your modified version must prominently offer all users
544
+ interacting with it remotely through a computer network (if your version
545
+ supports such interaction) an opportunity to receive the Corresponding
546
+ Source of your version by providing access to the Corresponding Source
547
+ from a network server at no charge, through some standard or customary
548
+ means of facilitating copying of software. This Corresponding Source
549
+ shall include the Corresponding Source for any work covered by version 3
550
+ of the GNU General Public License that is incorporated pursuant to the
551
+ following paragraph.
552
+
553
+ Notwithstanding any other provision of this License, you have
554
+ permission to link or combine any covered work with a work licensed
555
+ under version 3 of the GNU General Public License into a single
556
+ combined work, and to convey the resulting work. The terms of this
557
+ License will continue to apply to the part which is the covered work,
558
+ but the work with which it is combined will remain governed by version
559
+ 3 of the GNU General Public License.
560
+
561
+ 14. Revised Versions of this License.
562
+
563
+ The Free Software Foundation may publish revised and/or new versions of
564
+ the GNU Affero General Public License from time to time. Such new versions
565
+ will be similar in spirit to the present version, but may differ in detail to
566
+ address new problems or concerns.
567
+
568
+ Each version is given a distinguishing version number. If the
569
+ Program specifies that a certain numbered version of the GNU Affero General
570
+ Public License "or any later version" applies to it, you have the
571
+ option of following the terms and conditions either of that numbered
572
+ version or of any later version published by the Free Software
573
+ Foundation. If the Program does not specify a version number of the
574
+ GNU Affero General Public License, you may choose any version ever published
575
+ by the Free Software Foundation.
576
+
577
+ If the Program specifies that a proxy can decide which future
578
+ versions of the GNU Affero General Public License can be used, that proxy's
579
+ public statement of acceptance of a version permanently authorizes you
580
+ to choose that version for the Program.
581
+
582
+ Later license versions may give you additional or different
583
+ permissions. However, no additional obligations are imposed on any
584
+ author or copyright holder as a result of your choosing to follow a
585
+ later version.
586
+
587
+ 15. Disclaimer of Warranty.
588
+
589
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590
+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591
+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592
+ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594
+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595
+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596
+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597
+
598
+ 16. Limitation of Liability.
599
+
600
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602
+ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603
+ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604
+ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605
+ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606
+ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607
+ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608
+ SUCH DAMAGES.
609
+
610
+ 17. Interpretation of Sections 15 and 16.
611
+
612
+ If the disclaimer of warranty and limitation of liability provided
613
+ above cannot be given local legal effect according to their terms,
614
+ reviewing courts shall apply local law that most closely approximates
615
+ an absolute waiver of all civil liability in connection with the
616
+ Program, unless a warranty or assumption of liability accompanies a
617
+ copy of the Program in return for a fee.
618
+
619
+ END OF TERMS AND CONDITIONS
620
+
621
+ How to Apply These Terms to Your New Programs
622
+
623
+ If you develop a new program, and you want it to be of the greatest
624
+ possible use to the public, the best way to achieve this is to make it
625
+ free software which everyone can redistribute and change under these terms.
626
+
627
+ To do so, attach the following notices to the program. It is safest
628
+ to attach them to the start of each source file to most effectively
629
+ state the exclusion of warranty; and each file should have at least
630
+ the "copyright" line and a pointer to where the full notice is found.
631
+
632
+ <one line to give the program's name and a brief idea of what it does.>
633
+ Copyright (C) <year> <name of author>
634
+
635
+ This program is free software: you can redistribute it and/or modify
636
+ it under the terms of the GNU Affero General Public License as published
637
+ by the Free Software Foundation, either version 3 of the License, or
638
+ (at your option) any later version.
639
+
640
+ This program is distributed in the hope that it will be useful,
641
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
642
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643
+ GNU Affero General Public License for more details.
644
+
645
+ You should have received a copy of the GNU Affero General Public License
646
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
647
+
648
+ Also add information on how to contact you by electronic and paper mail.
649
+
650
+ If your software can interact with users remotely through a computer
651
+ network, you should also make sure that it provides a way for users to
652
+ get its source. For example, if your program is a web application, its
653
+ interface could display a "Source" link that leads users to an archive
654
+ of the code. There are many ways you could offer source, and different
655
+ solutions will be better for different programs; see section 13 for the
656
+ specific requirements.
657
+
658
+ You should also get your employer (if you work as a programmer) or school,
659
+ if any, to sign a "copyright disclaimer" for the program, if necessary.
660
+ For more information on this, and how to apply and follow the GNU AGPL, see
661
+ <http://www.gnu.org/licenses/>.
adetailer-dev/adetailer-dev/README.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ADetailer
2
+
3
+ ADetailer is an extension for the stable diffusion webui that does automatic masking and inpainting. It is similar to the Detection Detailer.
4
+
5
+ ## Install
6
+
7
+ You can install it directly from the Extensions tab.
8
+
9
+ ![image](https://i.imgur.com/qaXtoI6.png)
10
+
11
+ Or
12
+
13
+ (from Mikubill/sd-webui-controlnet)
14
+
15
+ 1. Open "Extensions" tab.
16
+ 2. Open "Install from URL" tab in the tab.
17
+ 3. Enter `https://github.com/Bing-su/adetailer.git` to "URL for extension's git repository".
18
+ 4. Press "Install" button.
19
+ 5. Wait 5 seconds, and you will see the message "Installed into stable-diffusion-webui\extensions\adetailer. Use Installed tab to restart".
20
+ 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.)
21
+ 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.)
22
+
23
+ ## Options
24
+
25
+ | Model, Prompts | | |
26
+ | --------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
27
+ | ADetailer model | Determine what to detect. | `None` = disable |
28
+ | ADetailer model classes | Comma separated class names to detect. only available when using YOLO World models | If blank, use default values.<br/>default = [COCO 80 classes](https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/datasets/coco.yaml) |
29
+ | ADetailer prompt, negative prompt | Prompts and negative prompts to apply | If left blank, it will use the same as the input. |
30
+ | Skip img2img | Skip img2img. In practice, this works by changing the step count of img2img to 1. | img2img only |
31
+
32
+ | Detection | | |
33
+ | ------------------------------------ | -------------------------------------------------------------------------------------------- | ------------ |
34
+ | Detection model confidence threshold | Only objects with a detection model confidence above this threshold are used for inpainting. | |
35
+ | Mask min/max ratio | Only use masks whose area is between those ratios for the area of the entire image. | |
36
+ | Mask only the top k largest | Only use the k objects with the largest area of the bbox. | 0 to disable |
37
+
38
+ If you want to exclude objects in the background, try setting the min ratio to around `0.01`.
39
+
40
+ | Mask Preprocessing | | |
41
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
42
+ | Mask x, y offset | Moves the mask horizontally and vertically by | |
43
+ | Mask erosion (-) / dilation (+) | Enlarge or reduce the detected mask. | [opencv example](https://docs.opencv.org/4.7.0/db/df6/tutorial_erosion_dilatation.html) |
44
+ | Mask merge mode | `None`: Inpaint each mask<br/>`Merge`: Merge all masks and inpaint<br/>`Merge and Invert`: Merge all masks and Invert, then inpaint | |
45
+
46
+ Applied in this order: x, y offset → erosion/dilation → merge/invert.
47
+
48
+ #### Inpainting
49
+
50
+ Each option corresponds to a corresponding option on the inpaint tab. Therefore, please refer to the inpaint tab for usage details on how to use each option.
51
+
52
+ ## ControlNet Inpainting
53
+
54
+ You can use the ControlNet extension if you have ControlNet installed and ControlNet models.
55
+
56
+ Support `inpaint, scribble, lineart, openpose, tile, depth` controlnet models. Once you choose a model, the preprocessor is set automatically. It works separately from the model set by the Controlnet extension.
57
+
58
+ If you select `Passthrough`, the controlnet settings you set outside of ADetailer will be used.
59
+
60
+ ## Advanced Options
61
+
62
+ API request example: [wiki/REST-API](https://github.com/Bing-su/adetailer/wiki/REST-API)
63
+
64
+ `[SEP], [SKIP], [PROMPT]` tokens: [wiki/Advanced](https://github.com/Bing-su/adetailer/wiki/Advanced)
65
+
66
+ ## Media
67
+
68
+ - 🎥 [どこよりも詳しい After Detailer (adetailer)の使い方 ① 【Stable Diffusion】](https://youtu.be/sF3POwPUWCE)
69
+ - 🎥 [どこよりも詳しい After Detailer (adetailer)の使い方 ② 【Stable Diffusion】](https://youtu.be/urNISRdbIEg)
70
+
71
+ - 📜 [ADetailer Installation and 5 Usage Methods](https://kindanai.com/en/manual-adetailer/)
72
+
73
+ ## Model
74
+
75
+ | Model | Target | mAP 50 | mAP 50-95 |
76
+ | --------------------- | --------------------- | ----------------------------- | ----------------------------- |
77
+ | face_yolov8n.pt | 2D / realistic face | 0.660 | 0.366 |
78
+ | face_yolov8s.pt | 2D / realistic face | 0.713 | 0.404 |
79
+ | hand_yolov8n.pt | 2D / realistic hand | 0.767 | 0.505 |
80
+ | person_yolov8n-seg.pt | 2D / realistic person | 0.782 (bbox)<br/>0.761 (mask) | 0.555 (bbox)<br/>0.460 (mask) |
81
+ | person_yolov8s-seg.pt | 2D / realistic person | 0.824 (bbox)<br/>0.809 (mask) | 0.605 (bbox)<br/>0.508 (mask) |
82
+ | mediapipe_face_full | realistic face | - | - |
83
+ | mediapipe_face_short | realistic face | - | - |
84
+ | mediapipe_face_mesh | realistic face | - | - |
85
+
86
+ The YOLO models can be found on huggingface [Bingsu/adetailer](https://huggingface.co/Bingsu/adetailer).
87
+
88
+ For a detailed description of the YOLO8 model, see: https://docs.ultralytics.com/models/yolov8/#overview
89
+
90
+ YOLO World model: https://docs.ultralytics.com/models/yolo-world/
91
+
92
+ ### Additional Model
93
+
94
+ Put your [ultralytics](https://github.com/ultralytics/ultralytics) yolo model in `models/adetailer`. The model name should end with `.pt`.
95
+
96
+ It must be a bbox detection or segment model and use all label.
97
+
98
+ ## How it works
99
+
100
+ ADetailer works in three simple steps.
101
+
102
+ 1. Create an image.
103
+ 2. Detect object with a detection model and create a mask image.
104
+ 3. Inpaint using the image from 1 and the mask from 2.
105
+
106
+ ## Development
107
+
108
+ ADetailer is developed and tested using the stable-diffusion 1.5 model, for the latest version of [AUTOMATIC1111/stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) repository only.
109
+
110
+ ## License
111
+
112
+ ADetailer is a derivative work that uses two AGPL-licensed works (stable-diffusion-webui, ultralytics) and is therefore distributed under the AGPL license.
113
+
114
+ ## See Also
115
+
116
+ - https://github.com/ototadana/sd-face-editor
117
+ - https://github.com/continue-revolution/sd-webui-segment-anything
118
+ - https://github.com/portu-sim/sd-webui-bmab
adetailer-dev/adetailer-dev/Taskfile.yml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # https://taskfile.dev
2
+
3
+ version: "3"
4
+
5
+ dotenv:
6
+ - .env
7
+
8
+ tasks:
9
+ default:
10
+ cmds:
11
+ - echo "$PYTHON"
12
+ - echo "$WEBUI"
13
+ - echo "$UV_PYTHON"
14
+ silent: true
15
+
16
+ launch:
17
+ dir: "{{.WEBUI}}"
18
+ cmds:
19
+ - "{{.PYTHON}} launch.py --xformers --api"
20
+ silent: true
21
+
22
+ lint:
23
+ cmds:
24
+ - pre-commit run -a
25
+
26
+ update:
27
+ cmds:
28
+ - "{{.PYTHON}} -m uv pip install -U ultralytics mediapipe ruff pre-commit black devtools pytest"
29
+
30
+ update-torch:
31
+ cmds:
32
+ - "{{.PYTHON}} -m uv pip install -U torch torchvision torchaudio -f https://download.pytorch.org/whl/torch_stable.html"
adetailer-dev/adetailer-dev/aaaaaa/__init__.py ADDED
File without changes
adetailer-dev/adetailer-dev/aaaaaa/conditional.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ msg = "[-] ADetailer: WebUI versions below 1.6.0 are not supported."
4
+
5
+ try:
6
+ from modules.processing import create_binary_mask # noqa: F401
7
+ except ImportError as e:
8
+ raise RuntimeError(msg) from e
9
+
10
+
11
+ try:
12
+ from modules.ui_components import InputAccordion # noqa: F401
13
+ except ImportError as e:
14
+ raise RuntimeError(msg) from e
15
+
16
+
17
+ try:
18
+ from modules.sd_schedulers import schedulers
19
+ except ImportError:
20
+ # webui < 1.9.0
21
+ schedulers = []
adetailer-dev/adetailer-dev/aaaaaa/helper.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from copy import copy
5
+ from typing import TYPE_CHECKING, Any, Union
6
+
7
+ import torch
8
+
9
+ from modules import safe
10
+ from modules.shared import opts
11
+
12
+ if TYPE_CHECKING:
13
+ # 타입 체커가 빨간 줄을 긋지 않게 하는 편법
14
+ from types import SimpleNamespace
15
+
16
+ StableDiffusionProcessingTxt2Img = SimpleNamespace
17
+ StableDiffusionProcessingImg2Img = SimpleNamespace
18
+ else:
19
+ from modules.processing import (
20
+ StableDiffusionProcessingImg2Img,
21
+ StableDiffusionProcessingTxt2Img,
22
+ )
23
+
24
+ PT = Union[StableDiffusionProcessingTxt2Img, StableDiffusionProcessingImg2Img]
25
+
26
+
27
+ @contextmanager
28
+ def change_torch_load():
29
+ orig = torch.load
30
+ try:
31
+ torch.load = safe.unsafe_torch_load
32
+ yield
33
+ finally:
34
+ torch.load = orig
35
+
36
+
37
+ @contextmanager
38
+ def pause_total_tqdm():
39
+ orig = opts.data.get("multiple_tqdm", True)
40
+ try:
41
+ opts.data["multiple_tqdm"] = False
42
+ yield
43
+ finally:
44
+ opts.data["multiple_tqdm"] = orig
45
+
46
+
47
+ @contextmanager
48
+ def preserve_prompts(p: PT):
49
+ all_pt = copy(p.all_prompts)
50
+ all_ng = copy(p.all_negative_prompts)
51
+ try:
52
+ yield
53
+ finally:
54
+ p.all_prompts = all_pt
55
+ p.all_negative_prompts = all_ng
56
+
57
+
58
+ def copy_extra_params(extra_params: dict[str, Any]) -> dict[str, Any]:
59
+ return {k: v for k, v in extra_params.items() if not callable(v)}
adetailer-dev/adetailer-dev/aaaaaa/p_method.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+
4
+ def need_call_process(p) -> bool:
5
+ if p.scripts is None:
6
+ return False
7
+ i = p.batch_index
8
+ bs = p.batch_size
9
+ return i == bs - 1
10
+
11
+
12
+ def need_call_postprocess(p) -> bool:
13
+ if p.scripts is None:
14
+ return False
15
+ return p.batch_index == 0
16
+
17
+
18
+ def is_img2img_inpaint(p) -> bool:
19
+ return hasattr(p, "image_mask") and p.image_mask is not None
20
+
21
+
22
+ def is_inpaint_only_masked(p) -> bool:
23
+ return hasattr(p, "inpaint_full_res") and p.inpaint_full_res
24
+
25
+
26
+ def get_i(p) -> int:
27
+ it = p.iteration
28
+ bs = p.batch_size
29
+ i = p.batch_index
30
+ return it * bs + i
31
+
32
+
33
+ def is_skip_img2img(p) -> bool:
34
+ return getattr(p, "_ad_skip_img2img", False)
adetailer-dev/adetailer-dev/aaaaaa/traceback.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import platform
5
+ import sys
6
+ from collections.abc import Callable
7
+ from importlib.metadata import version
8
+ from typing import Any, TypeVar
9
+
10
+ from rich.console import Console, Group
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+ from rich.traceback import Traceback
14
+ from typing_extensions import ParamSpec
15
+
16
+ from adetailer.__version__ import __version__
17
+ from adetailer.args import ADetailerArgs
18
+
19
+
20
+ def processing(*args: Any) -> dict[str, Any]:
21
+ try:
22
+ from modules.processing import (
23
+ StableDiffusionProcessingImg2Img,
24
+ StableDiffusionProcessingTxt2Img,
25
+ )
26
+ except ImportError:
27
+ return {}
28
+
29
+ p = None
30
+ for arg in args:
31
+ if isinstance(
32
+ arg, (StableDiffusionProcessingTxt2Img, StableDiffusionProcessingImg2Img)
33
+ ):
34
+ p = arg
35
+ break
36
+
37
+ if p is None:
38
+ return {}
39
+
40
+ info = {
41
+ "prompt": p.prompt,
42
+ "negative_prompt": p.negative_prompt,
43
+ "n_iter": p.n_iter,
44
+ "batch_size": p.batch_size,
45
+ "width": p.width,
46
+ "height": p.height,
47
+ "sampler_name": p.sampler_name,
48
+ "enable_hr": getattr(p, "enable_hr", False),
49
+ "hr_upscaler": getattr(p, "hr_upscaler", ""),
50
+ }
51
+
52
+ info.update(sd_models())
53
+ return info
54
+
55
+
56
+ def sd_models() -> dict[str, str]:
57
+ try:
58
+ from modules import shared
59
+
60
+ opts = shared.opts
61
+ except Exception:
62
+ return {}
63
+
64
+ return {
65
+ "checkpoint": getattr(opts, "sd_model_checkpoint", "------"),
66
+ "vae": getattr(opts, "sd_vae", "------"),
67
+ "unet": getattr(opts, "sd_unet", "------"),
68
+ }
69
+
70
+
71
+ def ad_args(*args: Any) -> dict[str, Any]:
72
+ ad_args = []
73
+ for arg in args:
74
+ if not isinstance(arg, dict):
75
+ continue
76
+
77
+ try:
78
+ a = ADetailerArgs(**arg)
79
+ except ValueError:
80
+ continue
81
+
82
+ if not a.need_skip():
83
+ ad_args.append(a)
84
+
85
+ if not ad_args:
86
+ return {}
87
+
88
+ arg0 = ad_args[0]
89
+ return {
90
+ "version": __version__,
91
+ "ad_model": arg0.ad_model,
92
+ "ad_prompt": arg0.ad_prompt,
93
+ "ad_negative_prompt": arg0.ad_negative_prompt,
94
+ "ad_controlnet_model": arg0.ad_controlnet_model,
95
+ "is_api": arg0.is_api,
96
+ }
97
+
98
+
99
+ def library_version():
100
+ libraries = ["torch", "torchvision", "ultralytics", "mediapipe"]
101
+ d = {}
102
+ for lib in libraries:
103
+ try:
104
+ d[lib] = version(lib)
105
+ except Exception: # noqa: PERF203
106
+ d[lib] = "Unknown"
107
+ return d
108
+
109
+
110
+ def sys_info() -> dict[str, Any]:
111
+ try:
112
+ import launch
113
+
114
+ version = launch.git_tag()
115
+ commit = launch.commit_hash()
116
+ except Exception:
117
+ version = "Unknown (too old or vladmandic)"
118
+ commit = "Unknown"
119
+
120
+ return {
121
+ "Platform": platform.platform(),
122
+ "Python": sys.version,
123
+ "Version": version,
124
+ "Commit": commit,
125
+ "Commandline": sys.argv,
126
+ "Libraries": library_version(),
127
+ }
128
+
129
+
130
+ def get_table(title: str, data: dict[str, Any]) -> Table:
131
+ table = Table(title=title, highlight=True)
132
+ table.add_column(" ", justify="right", style="dim")
133
+ table.add_column("Value")
134
+ for key, value in data.items():
135
+ if not isinstance(value, str):
136
+ value = repr(value)
137
+ table.add_row(key, value)
138
+
139
+ return table
140
+
141
+
142
+ P = ParamSpec("P")
143
+ T = TypeVar("T")
144
+
145
+
146
+ def rich_traceback(func: Callable[P, T]) -> Callable[P, T]:
147
+ def wrapper(*args, **kwargs):
148
+ string = io.StringIO()
149
+ width = Console().width
150
+ width = width - 4 if width > 4 else None
151
+ console = Console(file=string, width=width)
152
+ try:
153
+ return func(*args, **kwargs)
154
+ except Exception as e:
155
+ tables = [
156
+ get_table(title, data)
157
+ for title, data in [
158
+ ("System info", sys_info()),
159
+ ("Inputs", processing(*args)),
160
+ ("ADetailer", ad_args(*args)),
161
+ ]
162
+ if data
163
+ ]
164
+ tables.append(Traceback(extra_lines=1))
165
+
166
+ console.print(Panel(Group(*tables)))
167
+ output = "\n" + string.getvalue()
168
+
169
+ try:
170
+ error = e.__class__(output)
171
+ except Exception:
172
+ error = RuntimeError(output)
173
+ raise error from None
174
+
175
+ return wrapper
adetailer-dev/adetailer-dev/aaaaaa/ui.py ADDED
@@ -0,0 +1,705 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from functools import partial
5
+ from itertools import chain
6
+ from types import SimpleNamespace
7
+ from typing import Any
8
+
9
+ import gradio as gr
10
+
11
+ from aaaaaa.conditional import InputAccordion
12
+ from adetailer import ADETAILER, __version__
13
+ from adetailer.args import ALL_ARGS, MASK_MERGE_INVERT
14
+ from controlnet_ext import controlnet_exists, controlnet_type, get_cn_models
15
+
16
+ if controlnet_type == "forge":
17
+ from lib_controlnet import global_state
18
+
19
+ cn_module_choices = {
20
+ "inpaint": list(global_state.get_filtered_preprocessors("Inpaint")),
21
+ "lineart": list(global_state.get_filtered_preprocessors("Lineart")),
22
+ "openpose": list(global_state.get_filtered_preprocessors("OpenPose")),
23
+ "tile": list(global_state.get_filtered_preprocessors("Tile")),
24
+ "scribble": list(global_state.get_filtered_preprocessors("Scribble")),
25
+ "depth": list(global_state.get_filtered_preprocessors("Depth")),
26
+ }
27
+ else:
28
+ cn_module_choices = {
29
+ "inpaint": [
30
+ "inpaint_global_harmonious",
31
+ "inpaint_only",
32
+ "inpaint_only+lama",
33
+ ],
34
+ "lineart": [
35
+ "lineart_coarse",
36
+ "lineart_realistic",
37
+ "lineart_anime",
38
+ "lineart_anime_denoise",
39
+ ],
40
+ "openpose": ["openpose_full", "dw_openpose_full"],
41
+ "tile": ["tile_resample", "tile_colorfix", "tile_colorfix+sharp"],
42
+ "scribble": ["t2ia_sketch_pidi"],
43
+ "depth": ["depth_midas", "depth_hand_refiner"],
44
+ }
45
+
46
+ union = list(chain.from_iterable(cn_module_choices.values()))
47
+ cn_module_choices["union"] = union
48
+
49
+
50
+ class Widgets(SimpleNamespace):
51
+ def tolist(self):
52
+ return [getattr(self, attr) for attr in ALL_ARGS.attrs]
53
+
54
+
55
+ @dataclass
56
+ class WebuiInfo:
57
+ ad_model_list: list[str]
58
+ sampler_names: list[str]
59
+ scheduler_names: list[str]
60
+ t2i_button: gr.Button
61
+ i2i_button: gr.Button
62
+ checkpoints_list: list[str]
63
+ vae_list: list[str]
64
+
65
+
66
+ def gr_interactive(value: bool = True):
67
+ return gr.update(interactive=value)
68
+
69
+
70
+ def ordinal(n: int) -> str:
71
+ d = {1: "st", 2: "nd", 3: "rd"}
72
+ return str(n) + ("th" if 11 <= n % 100 <= 13 else d.get(n % 10, "th"))
73
+
74
+
75
+ def suffix(n: int, c: str = " ") -> str:
76
+ return "" if n == 0 else c + ordinal(n + 1)
77
+
78
+
79
+ def on_widget_change(state: dict, value: Any, *, attr: str):
80
+ if "is_api" in state:
81
+ state = state.copy()
82
+ state.pop("is_api")
83
+ state[attr] = value
84
+ return state
85
+
86
+
87
+ def on_generate_click(state: dict, *values: Any):
88
+ for attr, value in zip(ALL_ARGS.attrs, values):
89
+ state[attr] = value # noqa: PERF403
90
+ state["is_api"] = ()
91
+ return state
92
+
93
+
94
+ def on_ad_model_update(model: str):
95
+ if "-world" in model:
96
+ return gr.update(
97
+ visible=True,
98
+ placeholder="Comma separated class names to detect, ex: 'person,cat'. default: COCO 80 classes",
99
+ )
100
+ return gr.update(visible=False, placeholder="")
101
+
102
+
103
+ def on_cn_model_update(cn_model_name: str):
104
+ cn_model_name = cn_model_name.replace("inpaint_depth", "depth")
105
+ for t in cn_module_choices:
106
+ if t in cn_model_name:
107
+ choices = cn_module_choices[t]
108
+ return gr.update(visible=True, choices=choices, value=choices[0])
109
+ return gr.update(visible=False, choices=["None"], value="None")
110
+
111
+
112
+ def elem_id(item_id: str, n: int, is_img2img: bool) -> str:
113
+ tab = "img2img" if is_img2img else "txt2img"
114
+ suf = suffix(n, "_")
115
+ return f"script_{tab}_adetailer_{item_id}{suf}"
116
+
117
+
118
+ def state_init(w: Widgets) -> dict[str, Any]:
119
+ return {attr: getattr(w, attr).value for attr in ALL_ARGS.attrs}
120
+
121
+
122
+ def adui(
123
+ num_models: int,
124
+ is_img2img: bool,
125
+ webui_info: WebuiInfo,
126
+ ):
127
+ states = []
128
+ infotext_fields = []
129
+ eid = partial(elem_id, n=0, is_img2img=is_img2img)
130
+
131
+ with InputAccordion(
132
+ value=False,
133
+ elem_id=eid("ad_main_accordion"),
134
+ label=ADETAILER,
135
+ visible=True,
136
+ ) as ad_enable:
137
+ with gr.Row():
138
+ with gr.Column(scale=8):
139
+ ad_skip_img2img = gr.Checkbox(
140
+ label="Skip img2img",
141
+ value=False,
142
+ visible=is_img2img,
143
+ elem_id=eid("ad_skip_img2img"),
144
+ )
145
+
146
+ with gr.Column(scale=1, min_width=180):
147
+ gr.Markdown(
148
+ f"v{__version__}",
149
+ elem_id=eid("ad_version"),
150
+ )
151
+
152
+ infotext_fields.append((ad_enable, "ADetailer enable"))
153
+ infotext_fields.append((ad_skip_img2img, "ADetailer skip img2img"))
154
+
155
+ with gr.Group(), gr.Tabs():
156
+ for n in range(num_models):
157
+ with gr.Tab(ordinal(n + 1)):
158
+ state, infofields = one_ui_group(
159
+ n=n,
160
+ is_img2img=is_img2img,
161
+ webui_info=webui_info,
162
+ )
163
+
164
+ states.append(state)
165
+ infotext_fields.extend(infofields)
166
+
167
+ # components: [bool, bool, dict, dict, ...]
168
+ components = [ad_enable, ad_skip_img2img, *states]
169
+ return components, infotext_fields
170
+
171
+
172
+ def one_ui_group(n: int, is_img2img: bool, webui_info: WebuiInfo):
173
+ w = Widgets()
174
+ eid = partial(elem_id, n=n, is_img2img=is_img2img)
175
+
176
+ model_choices = (
177
+ [*webui_info.ad_model_list, "None"]
178
+ if n == 0
179
+ else ["None", *webui_info.ad_model_list]
180
+ )
181
+
182
+ with gr.Group():
183
+ with gr.Row(variant="compact"):
184
+ w.ad_tab_enable = gr.Checkbox(
185
+ label=f"Enable this tab ({ordinal(n + 1)})",
186
+ value=True,
187
+ visible=True,
188
+ elem_id=eid("ad_tab_enable"),
189
+ )
190
+
191
+ with gr.Row():
192
+ w.ad_model = gr.Dropdown(
193
+ label="ADetailer detector" + suffix(n),
194
+ choices=model_choices,
195
+ value=model_choices[0],
196
+ visible=True,
197
+ type="value",
198
+ elem_id=eid("ad_model"),
199
+ info="Select a model to use for detection.",
200
+ )
201
+
202
+ with gr.Row():
203
+ w.ad_model_classes = gr.Textbox(
204
+ label="ADetailer detector classes" + suffix(n),
205
+ value="",
206
+ visible=False,
207
+ elem_id=eid("ad_classes"),
208
+ )
209
+
210
+ w.ad_model.change(
211
+ on_ad_model_update,
212
+ inputs=w.ad_model,
213
+ outputs=w.ad_model_classes,
214
+ queue=False,
215
+ )
216
+
217
+ gr.HTML("<br>")
218
+
219
+ with gr.Group():
220
+ with gr.Row(elem_id=eid("ad_toprow_prompt")):
221
+ w.ad_prompt = gr.Textbox(
222
+ label="ad_prompt" + suffix(n),
223
+ show_label=False,
224
+ lines=3,
225
+ placeholder="ADetailer prompt"
226
+ + suffix(n)
227
+ + "\nIf blank, the main prompt is used.",
228
+ elem_id=eid("ad_prompt"),
229
+ )
230
+
231
+ with gr.Row(elem_id=eid("ad_toprow_negative_prompt")):
232
+ w.ad_negative_prompt = gr.Textbox(
233
+ label="ad_negative_prompt" + suffix(n),
234
+ show_label=False,
235
+ lines=2,
236
+ placeholder="ADetailer negative prompt"
237
+ + suffix(n)
238
+ + "\nIf blank, the main negative prompt is used.",
239
+ elem_id=eid("ad_negative_prompt"),
240
+ )
241
+
242
+ with gr.Group():
243
+ with gr.Accordion(
244
+ "Detection", open=False, elem_id=eid("ad_detection_accordion")
245
+ ):
246
+ detection(w, n, is_img2img)
247
+
248
+ with gr.Accordion(
249
+ "Mask Preprocessing",
250
+ open=False,
251
+ elem_id=eid("ad_mask_preprocessing_accordion"),
252
+ ):
253
+ mask_preprocessing(w, n, is_img2img)
254
+
255
+ with gr.Accordion(
256
+ "Inpainting", open=False, elem_id=eid("ad_inpainting_accordion")
257
+ ):
258
+ inpainting(w, n, is_img2img, webui_info)
259
+
260
+ with gr.Group():
261
+ controlnet(w, n, is_img2img)
262
+
263
+ state = gr.State(lambda: state_init(w))
264
+
265
+ for attr in ALL_ARGS.attrs:
266
+ widget = getattr(w, attr)
267
+ on_change = partial(on_widget_change, attr=attr)
268
+ widget.change(fn=on_change, inputs=[state, widget], outputs=state, queue=False)
269
+
270
+ all_inputs = [state, *w.tolist()]
271
+ target_button = webui_info.i2i_button if is_img2img else webui_info.t2i_button
272
+ target_button.click(
273
+ fn=on_generate_click, inputs=all_inputs, outputs=state, queue=False
274
+ )
275
+
276
+ infotext_fields = [(getattr(w, attr), name + suffix(n)) for attr, name in ALL_ARGS]
277
+
278
+ return state, infotext_fields
279
+
280
+
281
+ def detection(w: Widgets, n: int, is_img2img: bool):
282
+ eid = partial(elem_id, n=n, is_img2img=is_img2img)
283
+
284
+ with gr.Row():
285
+ with gr.Column(variant="compact"):
286
+ w.ad_confidence = gr.Slider(
287
+ label="Detection model confidence threshold" + suffix(n),
288
+ minimum=0.0,
289
+ maximum=1.0,
290
+ step=0.01,
291
+ value=0.3,
292
+ visible=True,
293
+ elem_id=eid("ad_confidence"),
294
+ )
295
+ w.ad_mask_k_largest = gr.Slider(
296
+ label="Mask only the top k largest (0 to disable)" + suffix(n),
297
+ minimum=0,
298
+ maximum=10,
299
+ step=1,
300
+ value=0,
301
+ visible=True,
302
+ elem_id=eid("ad_mask_k_largest"),
303
+ )
304
+
305
+ with gr.Column(variant="compact"):
306
+ w.ad_mask_min_ratio = gr.Slider(
307
+ label="Mask min area ratio" + suffix(n),
308
+ minimum=0.0,
309
+ maximum=1.0,
310
+ step=0.001,
311
+ value=0.0,
312
+ visible=True,
313
+ elem_id=eid("ad_mask_min_ratio"),
314
+ )
315
+ w.ad_mask_max_ratio = gr.Slider(
316
+ label="Mask max area ratio" + suffix(n),
317
+ minimum=0.0,
318
+ maximum=1.0,
319
+ step=0.001,
320
+ value=1.0,
321
+ visible=True,
322
+ elem_id=eid("ad_mask_max_ratio"),
323
+ )
324
+
325
+
326
+ def mask_preprocessing(w: Widgets, n: int, is_img2img: bool):
327
+ eid = partial(elem_id, n=n, is_img2img=is_img2img)
328
+
329
+ with gr.Group():
330
+ with gr.Row():
331
+ with gr.Column(variant="compact"):
332
+ w.ad_x_offset = gr.Slider(
333
+ label="Mask x(→) offset" + suffix(n),
334
+ minimum=-200,
335
+ maximum=200,
336
+ step=1,
337
+ value=0,
338
+ visible=True,
339
+ elem_id=eid("ad_x_offset"),
340
+ )
341
+ w.ad_y_offset = gr.Slider(
342
+ label="Mask y(↑) offset" + suffix(n),
343
+ minimum=-200,
344
+ maximum=200,
345
+ step=1,
346
+ value=0,
347
+ visible=True,
348
+ elem_id=eid("ad_y_offset"),
349
+ )
350
+
351
+ with gr.Column(variant="compact"):
352
+ w.ad_dilate_erode = gr.Slider(
353
+ label="Mask erosion (-) / dilation (+)" + suffix(n),
354
+ minimum=-128,
355
+ maximum=128,
356
+ step=4,
357
+ value=4,
358
+ visible=True,
359
+ elem_id=eid("ad_dilate_erode"),
360
+ )
361
+
362
+ with gr.Row():
363
+ w.ad_mask_merge_invert = gr.Radio(
364
+ label="Mask merge mode" + suffix(n),
365
+ choices=MASK_MERGE_INVERT,
366
+ value="None",
367
+ elem_id=eid("ad_mask_merge_invert"),
368
+ info="None: do nothing, Merge: merge masks, Merge and Invert: merge all masks and invert",
369
+ )
370
+
371
+
372
+ def inpainting(w: Widgets, n: int, is_img2img: bool, webui_info: WebuiInfo):
373
+ eid = partial(elem_id, n=n, is_img2img=is_img2img)
374
+
375
+ with gr.Group():
376
+ with gr.Row():
377
+ w.ad_mask_blur = gr.Slider(
378
+ label="Inpaint mask blur" + suffix(n),
379
+ minimum=0,
380
+ maximum=64,
381
+ step=1,
382
+ value=4,
383
+ visible=True,
384
+ elem_id=eid("ad_mask_blur"),
385
+ )
386
+
387
+ w.ad_denoising_strength = gr.Slider(
388
+ label="Inpaint denoising strength" + suffix(n),
389
+ minimum=0.0,
390
+ maximum=1.0,
391
+ step=0.01,
392
+ value=0.4,
393
+ visible=True,
394
+ elem_id=eid("ad_denoising_strength"),
395
+ )
396
+
397
+ with gr.Row():
398
+ with gr.Column(variant="compact"):
399
+ w.ad_inpaint_only_masked = gr.Checkbox(
400
+ label="Inpaint only masked" + suffix(n),
401
+ value=True,
402
+ visible=True,
403
+ elem_id=eid("ad_inpaint_only_masked"),
404
+ )
405
+ w.ad_inpaint_only_masked_padding = gr.Slider(
406
+ label="Inpaint only masked padding, pixels" + suffix(n),
407
+ minimum=0,
408
+ maximum=256,
409
+ step=4,
410
+ value=32,
411
+ visible=True,
412
+ elem_id=eid("ad_inpaint_only_masked_padding"),
413
+ )
414
+
415
+ w.ad_inpaint_only_masked.change(
416
+ gr_interactive,
417
+ inputs=w.ad_inpaint_only_masked,
418
+ outputs=w.ad_inpaint_only_masked_padding,
419
+ queue=False,
420
+ )
421
+
422
+ with gr.Column(variant="compact"):
423
+ w.ad_use_inpaint_width_height = gr.Checkbox(
424
+ label="Use separate width/height" + suffix(n),
425
+ value=False,
426
+ visible=True,
427
+ elem_id=eid("ad_use_inpaint_width_height"),
428
+ )
429
+
430
+ w.ad_inpaint_width = gr.Slider(
431
+ label="inpaint width" + suffix(n),
432
+ minimum=64,
433
+ maximum=2048,
434
+ step=4,
435
+ value=512,
436
+ visible=True,
437
+ elem_id=eid("ad_inpaint_width"),
438
+ )
439
+
440
+ w.ad_inpaint_height = gr.Slider(
441
+ label="inpaint height" + suffix(n),
442
+ minimum=64,
443
+ maximum=2048,
444
+ step=4,
445
+ value=512,
446
+ visible=True,
447
+ elem_id=eid("ad_inpaint_height"),
448
+ )
449
+
450
+ w.ad_use_inpaint_width_height.change(
451
+ lambda value: (gr_interactive(value), gr_interactive(value)),
452
+ inputs=w.ad_use_inpaint_width_height,
453
+ outputs=[w.ad_inpaint_width, w.ad_inpaint_height],
454
+ queue=False,
455
+ )
456
+
457
+ with gr.Row():
458
+ with gr.Column(variant="compact"):
459
+ w.ad_use_steps = gr.Checkbox(
460
+ label="Use separate steps" + suffix(n),
461
+ value=False,
462
+ visible=True,
463
+ elem_id=eid("ad_use_steps"),
464
+ )
465
+
466
+ w.ad_steps = gr.Slider(
467
+ label="ADetailer steps" + suffix(n),
468
+ minimum=1,
469
+ maximum=150,
470
+ step=1,
471
+ value=28,
472
+ visible=True,
473
+ elem_id=eid("ad_steps"),
474
+ )
475
+
476
+ w.ad_use_steps.change(
477
+ gr_interactive,
478
+ inputs=w.ad_use_steps,
479
+ outputs=w.ad_steps,
480
+ queue=False,
481
+ )
482
+
483
+ with gr.Column(variant="compact"):
484
+ w.ad_use_cfg_scale = gr.Checkbox(
485
+ label="Use separate CFG scale" + suffix(n),
486
+ value=False,
487
+ visible=True,
488
+ elem_id=eid("ad_use_cfg_scale"),
489
+ )
490
+
491
+ w.ad_cfg_scale = gr.Slider(
492
+ label="ADetailer CFG scale" + suffix(n),
493
+ minimum=0.0,
494
+ maximum=30.0,
495
+ step=0.5,
496
+ value=7.0,
497
+ visible=True,
498
+ elem_id=eid("ad_cfg_scale"),
499
+ )
500
+
501
+ w.ad_use_cfg_scale.change(
502
+ gr_interactive,
503
+ inputs=w.ad_use_cfg_scale,
504
+ outputs=w.ad_cfg_scale,
505
+ queue=False,
506
+ )
507
+
508
+ with gr.Row():
509
+ with gr.Column(variant="compact"):
510
+ w.ad_use_checkpoint = gr.Checkbox(
511
+ label="Use separate checkpoint" + suffix(n),
512
+ value=False,
513
+ visible=True,
514
+ elem_id=eid("ad_use_checkpoint"),
515
+ )
516
+
517
+ ckpts = ["Use same checkpoint", *webui_info.checkpoints_list]
518
+
519
+ w.ad_checkpoint = gr.Dropdown(
520
+ label="ADetailer checkpoint" + suffix(n),
521
+ choices=ckpts,
522
+ value=ckpts[0],
523
+ visible=True,
524
+ elem_id=eid("ad_checkpoint"),
525
+ )
526
+
527
+ with gr.Column(variant="compact"):
528
+ w.ad_use_vae = gr.Checkbox(
529
+ label="Use separate VAE" + suffix(n),
530
+ value=False,
531
+ visible=True,
532
+ elem_id=eid("ad_use_vae"),
533
+ )
534
+
535
+ vaes = ["Use same VAE", *webui_info.vae_list]
536
+
537
+ w.ad_vae = gr.Dropdown(
538
+ label="ADetailer VAE" + suffix(n),
539
+ choices=vaes,
540
+ value=vaes[0],
541
+ visible=True,
542
+ elem_id=eid("ad_vae"),
543
+ )
544
+
545
+ with gr.Row(), gr.Column(variant="compact"):
546
+ w.ad_use_sampler = gr.Checkbox(
547
+ label="Use separate sampler" + suffix(n),
548
+ value=False,
549
+ visible=True,
550
+ elem_id=eid("ad_use_sampler"),
551
+ )
552
+
553
+ with gr.Row():
554
+ w.ad_sampler = gr.Dropdown(
555
+ label="ADetailer sampler" + suffix(n),
556
+ choices=webui_info.sampler_names,
557
+ value=webui_info.sampler_names[0],
558
+ visible=True,
559
+ elem_id=eid("ad_sampler"),
560
+ )
561
+
562
+ scheduler_names = [
563
+ "Use same scheduler",
564
+ *webui_info.scheduler_names,
565
+ ]
566
+ w.ad_scheduler = gr.Dropdown(
567
+ label="ADetailer scheduler" + suffix(n),
568
+ choices=scheduler_names,
569
+ value=scheduler_names[0],
570
+ visible=len(scheduler_names) > 1,
571
+ elem_id=eid("ad_scheduler"),
572
+ )
573
+
574
+ w.ad_use_sampler.change(
575
+ lambda value: (gr_interactive(value), gr_interactive(value)),
576
+ inputs=w.ad_use_sampler,
577
+ outputs=[w.ad_sampler, w.ad_scheduler],
578
+ queue=False,
579
+ )
580
+
581
+ with gr.Row():
582
+ with gr.Column(variant="compact"):
583
+ w.ad_use_noise_multiplier = gr.Checkbox(
584
+ label="Use separate noise multiplier" + suffix(n),
585
+ value=False,
586
+ visible=True,
587
+ elem_id=eid("ad_use_noise_multiplier"),
588
+ )
589
+
590
+ w.ad_noise_multiplier = gr.Slider(
591
+ label="Noise multiplier for img2img" + suffix(n),
592
+ minimum=0.5,
593
+ maximum=1.5,
594
+ step=0.01,
595
+ value=1.0,
596
+ visible=True,
597
+ elem_id=eid("ad_noise_multiplier"),
598
+ )
599
+
600
+ w.ad_use_noise_multiplier.change(
601
+ gr_interactive,
602
+ inputs=w.ad_use_noise_multiplier,
603
+ outputs=w.ad_noise_multiplier,
604
+ queue=False,
605
+ )
606
+
607
+ with gr.Column(variant="compact"):
608
+ w.ad_use_clip_skip = gr.Checkbox(
609
+ label="Use separate CLIP skip" + suffix(n),
610
+ value=False,
611
+ visible=True,
612
+ elem_id=eid("ad_use_clip_skip"),
613
+ )
614
+
615
+ w.ad_clip_skip = gr.Slider(
616
+ label="ADetailer CLIP skip" + suffix(n),
617
+ minimum=1,
618
+ maximum=12,
619
+ step=1,
620
+ value=1,
621
+ visible=True,
622
+ elem_id=eid("ad_clip_skip"),
623
+ )
624
+
625
+ w.ad_use_clip_skip.change(
626
+ gr_interactive,
627
+ inputs=w.ad_use_clip_skip,
628
+ outputs=w.ad_clip_skip,
629
+ queue=False,
630
+ )
631
+
632
+ with gr.Row(), gr.Column(variant="compact"):
633
+ w.ad_restore_face = gr.Checkbox(
634
+ label="Restore faces after ADetailer" + suffix(n),
635
+ value=False,
636
+ elem_id=eid("ad_restore_face"),
637
+ )
638
+
639
+
640
+ def controlnet(w: Widgets, n: int, is_img2img: bool):
641
+ eid = partial(elem_id, n=n, is_img2img=is_img2img)
642
+ cn_models = ["None", "Passthrough", *get_cn_models()]
643
+
644
+ with gr.Row(variant="panel"):
645
+ with gr.Column(variant="compact"):
646
+ w.ad_controlnet_model = gr.Dropdown(
647
+ label="ControlNet model" + suffix(n),
648
+ choices=cn_models,
649
+ value="None",
650
+ visible=True,
651
+ type="value",
652
+ interactive=controlnet_exists,
653
+ elem_id=eid("ad_controlnet_model"),
654
+ )
655
+
656
+ w.ad_controlnet_module = gr.Dropdown(
657
+ label="ControlNet module" + suffix(n),
658
+ choices=["None"],
659
+ value="None",
660
+ visible=False,
661
+ type="value",
662
+ interactive=controlnet_exists,
663
+ elem_id=eid("ad_controlnet_module"),
664
+ )
665
+
666
+ w.ad_controlnet_weight = gr.Slider(
667
+ label="ControlNet weight" + suffix(n),
668
+ minimum=0.0,
669
+ maximum=1.0,
670
+ step=0.01,
671
+ value=1.0,
672
+ visible=True,
673
+ interactive=controlnet_exists,
674
+ elem_id=eid("ad_controlnet_weight"),
675
+ )
676
+
677
+ w.ad_controlnet_model.change(
678
+ on_cn_model_update,
679
+ inputs=w.ad_controlnet_model,
680
+ outputs=w.ad_controlnet_module,
681
+ queue=False,
682
+ )
683
+
684
+ with gr.Column(variant="compact"):
685
+ w.ad_controlnet_guidance_start = gr.Slider(
686
+ label="ControlNet guidance start" + suffix(n),
687
+ minimum=0.0,
688
+ maximum=1.0,
689
+ step=0.01,
690
+ value=0.0,
691
+ visible=True,
692
+ interactive=controlnet_exists,
693
+ elem_id=eid("ad_controlnet_guidance_start"),
694
+ )
695
+
696
+ w.ad_controlnet_guidance_end = gr.Slider(
697
+ label="ControlNet guidance end" + suffix(n),
698
+ minimum=0.0,
699
+ maximum=1.0,
700
+ step=0.01,
701
+ value=1.0,
702
+ visible=True,
703
+ interactive=controlnet_exists,
704
+ elem_id=eid("ad_controlnet_guidance_end"),
705
+ )
adetailer-dev/adetailer-dev/adetailer/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .__version__ import __version__
2
+ from .args import ALL_ARGS, ADetailerArgs
3
+ from .common import PredictOutput, get_models
4
+ from .mediapipe import mediapipe_predict
5
+ from .ultralytics import ultralytics_predict
6
+
7
+ ADETAILER = "ADetailer"
8
+
9
+ __all__ = [
10
+ "__version__",
11
+ "ADetailerArgs",
12
+ "ADETAILER",
13
+ "ALL_ARGS",
14
+ "PredictOutput",
15
+ "get_models",
16
+ "mediapipe_predict",
17
+ "ultralytics_predict",
18
+ ]
adetailer-dev/adetailer-dev/adetailer/__version__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __version__ = "24.7.0-dev.0"
adetailer-dev/adetailer-dev/adetailer/args.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections import UserList
4
+ from dataclasses import dataclass
5
+ from functools import cached_property, partial
6
+ from typing import Any, Literal, NamedTuple, Optional
7
+
8
+ try:
9
+ from pydantic.v1 import (
10
+ BaseModel,
11
+ Extra,
12
+ NonNegativeFloat,
13
+ NonNegativeInt,
14
+ PositiveInt,
15
+ confloat,
16
+ conint,
17
+ validator,
18
+ )
19
+ except ImportError:
20
+ from pydantic import (
21
+ BaseModel,
22
+ Extra,
23
+ NonNegativeFloat,
24
+ NonNegativeInt,
25
+ PositiveInt,
26
+ confloat,
27
+ conint,
28
+ validator,
29
+ )
30
+
31
+
32
+ @dataclass
33
+ class SkipImg2ImgOrig:
34
+ steps: int
35
+ sampler_name: str
36
+ width: int
37
+ height: int
38
+
39
+
40
+ class Arg(NamedTuple):
41
+ attr: str
42
+ name: str
43
+
44
+
45
+ class ArgsList(UserList):
46
+ @cached_property
47
+ def attrs(self) -> tuple[str, ...]:
48
+ return tuple(attr for attr, _ in self)
49
+
50
+ @cached_property
51
+ def names(self) -> tuple[str, ...]:
52
+ return tuple(name for _, name in self)
53
+
54
+
55
+ class ADetailerArgs(BaseModel, extra=Extra.forbid):
56
+ ad_model: str = "None"
57
+ ad_model_classes: str = ""
58
+ ad_tab_enable: bool = True
59
+ ad_prompt: str = ""
60
+ ad_negative_prompt: str = ""
61
+ ad_confidence: confloat(ge=0.0, le=1.0) = 0.3
62
+ ad_mask_k_largest: NonNegativeInt = 0
63
+ ad_mask_min_ratio: confloat(ge=0.0, le=1.0) = 0.0
64
+ ad_mask_max_ratio: confloat(ge=0.0, le=1.0) = 1.0
65
+ ad_dilate_erode: int = 4
66
+ ad_x_offset: int = 0
67
+ ad_y_offset: int = 0
68
+ ad_mask_merge_invert: Literal["None", "Merge", "Merge and Invert"] = "None"
69
+ ad_mask_blur: NonNegativeInt = 4
70
+ ad_denoising_strength: confloat(ge=0.0, le=1.0) = 0.4
71
+ ad_inpaint_only_masked: bool = True
72
+ ad_inpaint_only_masked_padding: NonNegativeInt = 32
73
+ ad_use_inpaint_width_height: bool = False
74
+ ad_inpaint_width: PositiveInt = 512
75
+ ad_inpaint_height: PositiveInt = 512
76
+ ad_use_steps: bool = False
77
+ ad_steps: PositiveInt = 28
78
+ ad_use_cfg_scale: bool = False
79
+ ad_cfg_scale: NonNegativeFloat = 7.0
80
+ ad_use_checkpoint: bool = False
81
+ ad_checkpoint: Optional[str] = None
82
+ ad_use_vae: bool = False
83
+ ad_vae: Optional[str] = None
84
+ ad_use_sampler: bool = False
85
+ ad_sampler: str = "DPM++ 2M Karras"
86
+ ad_scheduler: str = "Use same scheduler"
87
+ ad_use_noise_multiplier: bool = False
88
+ ad_noise_multiplier: confloat(ge=0.5, le=1.5) = 1.0
89
+ ad_use_clip_skip: bool = False
90
+ ad_clip_skip: conint(ge=1, le=12) = 1
91
+ ad_restore_face: bool = False
92
+ ad_controlnet_model: str = "None"
93
+ ad_controlnet_module: str = "None"
94
+ ad_controlnet_weight: confloat(ge=0.0, le=1.0) = 1.0
95
+ ad_controlnet_guidance_start: confloat(ge=0.0, le=1.0) = 0.0
96
+ ad_controlnet_guidance_end: confloat(ge=0.0, le=1.0) = 1.0
97
+ is_api: bool = True
98
+
99
+ @validator("is_api", pre=True)
100
+ def is_api_validator(cls, v: Any): # noqa: N805
101
+ "tuple is json serializable but cannot be made with json deserialize."
102
+ return type(v) is not tuple
103
+
104
+ @staticmethod
105
+ def ppop(
106
+ p: dict[str, Any],
107
+ key: str,
108
+ pops: list[str] | None = None,
109
+ cond: Any = None,
110
+ ) -> None:
111
+ if pops is None:
112
+ pops = [key]
113
+ if key not in p:
114
+ return
115
+ value = p[key]
116
+ cond = (not bool(value)) if cond is None else value == cond
117
+
118
+ if cond:
119
+ for k in pops:
120
+ p.pop(k, None)
121
+
122
+ def extra_params(self, suffix: str = "") -> dict[str, Any]:
123
+ if self.need_skip():
124
+ return {}
125
+
126
+ p = {name: getattr(self, attr) for attr, name in ALL_ARGS}
127
+ ppop = partial(self.ppop, p)
128
+
129
+ ppop("ADetailer model classes")
130
+ ppop("ADetailer prompt")
131
+ ppop("ADetailer negative prompt")
132
+ p.pop("ADetailer tab enable", None) # always pop
133
+ ppop("ADetailer mask only top k largest", cond=0)
134
+ ppop("ADetailer mask min ratio", cond=0.0)
135
+ ppop("ADetailer mask max ratio", cond=1.0)
136
+ ppop("ADetailer x offset", cond=0)
137
+ ppop("ADetailer y offset", cond=0)
138
+ ppop("ADetailer mask merge invert", cond="None")
139
+ ppop("ADetailer inpaint only masked", ["ADetailer inpaint padding"])
140
+ ppop(
141
+ "ADetailer use inpaint width height",
142
+ [
143
+ "ADetailer use inpaint width height",
144
+ "ADetailer inpaint width",
145
+ "ADetailer inpaint height",
146
+ ],
147
+ )
148
+ ppop(
149
+ "ADetailer use separate steps",
150
+ ["ADetailer use separate steps", "ADetailer steps"],
151
+ )
152
+ ppop(
153
+ "ADetailer use separate CFG scale",
154
+ ["ADetailer use separate CFG scale", "ADetailer CFG scale"],
155
+ )
156
+ ppop(
157
+ "ADetailer use separate checkpoint",
158
+ ["ADetailer use separate checkpoint", "ADetailer checkpoint"],
159
+ )
160
+ ppop(
161
+ "ADetailer use separate VAE",
162
+ ["ADetailer use separate VAE", "ADetailer VAE"],
163
+ )
164
+ ppop(
165
+ "ADetailer use separate sampler",
166
+ [
167
+ "ADetailer use separate sampler",
168
+ "ADetailer sampler",
169
+ "ADetailer scheduler",
170
+ ],
171
+ )
172
+ ppop("ADetailer scheduler", cond="Use same scheduler")
173
+ ppop(
174
+ "ADetailer use separate noise multiplier",
175
+ ["ADetailer use separate noise multiplier", "ADetailer noise multiplier"],
176
+ )
177
+
178
+ ppop(
179
+ "ADetailer use separate CLIP skip",
180
+ ["ADetailer use separate CLIP skip", "ADetailer CLIP skip"],
181
+ )
182
+
183
+ ppop("ADetailer restore face")
184
+ ppop(
185
+ "ADetailer ControlNet model",
186
+ [
187
+ "ADetailer ControlNet model",
188
+ "ADetailer ControlNet module",
189
+ "ADetailer ControlNet weight",
190
+ "ADetailer ControlNet guidance start",
191
+ "ADetailer ControlNet guidance end",
192
+ ],
193
+ cond="None",
194
+ )
195
+ ppop("ADetailer ControlNet module", cond="None")
196
+ ppop("ADetailer ControlNet weight", cond=1.0)
197
+ ppop("ADetailer ControlNet guidance start", cond=0.0)
198
+ ppop("ADetailer ControlNet guidance end", cond=1.0)
199
+
200
+ if suffix:
201
+ p = {k + suffix: v for k, v in p.items()}
202
+
203
+ return p
204
+
205
+ def is_mediapipe(self) -> bool:
206
+ return self.ad_model.lower().startswith("mediapipe")
207
+
208
+ def need_skip(self) -> bool:
209
+ return self.ad_model == "None" or self.ad_tab_enable is False
210
+
211
+
212
+ _all_args = [
213
+ ("ad_model", "ADetailer model"),
214
+ ("ad_model_classes", "ADetailer model classes"),
215
+ ("ad_tab_enable", "ADetailer tab enable"),
216
+ ("ad_prompt", "ADetailer prompt"),
217
+ ("ad_negative_prompt", "ADetailer negative prompt"),
218
+ ("ad_confidence", "ADetailer confidence"),
219
+ ("ad_mask_k_largest", "ADetailer mask only top k largest"),
220
+ ("ad_mask_min_ratio", "ADetailer mask min ratio"),
221
+ ("ad_mask_max_ratio", "ADetailer mask max ratio"),
222
+ ("ad_x_offset", "ADetailer x offset"),
223
+ ("ad_y_offset", "ADetailer y offset"),
224
+ ("ad_dilate_erode", "ADetailer dilate erode"),
225
+ ("ad_mask_merge_invert", "ADetailer mask merge invert"),
226
+ ("ad_mask_blur", "ADetailer mask blur"),
227
+ ("ad_denoising_strength", "ADetailer denoising strength"),
228
+ ("ad_inpaint_only_masked", "ADetailer inpaint only masked"),
229
+ ("ad_inpaint_only_masked_padding", "ADetailer inpaint padding"),
230
+ ("ad_use_inpaint_width_height", "ADetailer use inpaint width height"),
231
+ ("ad_inpaint_width", "ADetailer inpaint width"),
232
+ ("ad_inpaint_height", "ADetailer inpaint height"),
233
+ ("ad_use_steps", "ADetailer use separate steps"),
234
+ ("ad_steps", "ADetailer steps"),
235
+ ("ad_use_cfg_scale", "ADetailer use separate CFG scale"),
236
+ ("ad_cfg_scale", "ADetailer CFG scale"),
237
+ ("ad_use_checkpoint", "ADetailer use separate checkpoint"),
238
+ ("ad_checkpoint", "ADetailer checkpoint"),
239
+ ("ad_use_vae", "ADetailer use separate VAE"),
240
+ ("ad_vae", "ADetailer VAE"),
241
+ ("ad_use_sampler", "ADetailer use separate sampler"),
242
+ ("ad_sampler", "ADetailer sampler"),
243
+ ("ad_scheduler", "ADetailer scheduler"),
244
+ ("ad_use_noise_multiplier", "ADetailer use separate noise multiplier"),
245
+ ("ad_noise_multiplier", "ADetailer noise multiplier"),
246
+ ("ad_use_clip_skip", "ADetailer use separate CLIP skip"),
247
+ ("ad_clip_skip", "ADetailer CLIP skip"),
248
+ ("ad_restore_face", "ADetailer restore face"),
249
+ ("ad_controlnet_model", "ADetailer ControlNet model"),
250
+ ("ad_controlnet_module", "ADetailer ControlNet module"),
251
+ ("ad_controlnet_weight", "ADetailer ControlNet weight"),
252
+ ("ad_controlnet_guidance_start", "ADetailer ControlNet guidance start"),
253
+ ("ad_controlnet_guidance_end", "ADetailer ControlNet guidance end"),
254
+ ]
255
+
256
+ _args = [Arg(*args) for args in _all_args]
257
+ ALL_ARGS = ArgsList(_args)
258
+
259
+ BBOX_SORTBY = [
260
+ "None",
261
+ "Position (left to right)",
262
+ "Position (center to edge)",
263
+ "Area (large to small)",
264
+ ]
265
+ MASK_MERGE_INVERT = ["None", "Merge", "Merge and Invert"]
266
+
267
+ _script_default = (
268
+ "dynamic_prompting",
269
+ "dynamic_thresholding",
270
+ "wildcard_recursive",
271
+ "wildcards",
272
+ "lora_block_weight",
273
+ "negpip",
274
+ )
275
+ SCRIPT_DEFAULT = ",".join(sorted(_script_default))
276
+
277
+ _builtin_script = ("soft_inpainting", "hypertile_script")
278
+ BUILTIN_SCRIPT = ",".join(sorted(_builtin_script))
adetailer-dev/adetailer-dev/adetailer/common.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections import OrderedDict
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ from contextlib import suppress
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any, Generic, Optional, TypeVar
10
+
11
+ from huggingface_hub import hf_hub_download
12
+ from PIL import Image, ImageDraw
13
+ from rich import print
14
+ from torchvision.transforms.functional import to_pil_image
15
+
16
+ REPO_ID = "Bingsu/adetailer"
17
+
18
+ T = TypeVar("T", int, float)
19
+
20
+
21
+ @dataclass
22
+ class PredictOutput(Generic[T]):
23
+ bboxes: list[list[T]] = field(default_factory=list)
24
+ masks: list[Image.Image] = field(default_factory=list)
25
+ preview: Optional[Image.Image] = None
26
+
27
+
28
+ def hf_download(file: str, repo_id: str = REPO_ID, check_remote: bool = True) -> str:
29
+ if check_remote:
30
+ with suppress(Exception):
31
+ return hf_hub_download(repo_id, file, etag_timeout=1)
32
+
33
+ with suppress(Exception):
34
+ return hf_hub_download(
35
+ repo_id, file, etag_timeout=1, endpoint="https://hf-mirror.com"
36
+ )
37
+
38
+ with suppress(Exception):
39
+ return hf_hub_download(repo_id, file, local_files_only=True)
40
+
41
+ msg = f"[-] ADetailer: Failed to load model {file!r} from huggingface"
42
+ print(msg)
43
+ return "INVALID"
44
+
45
+
46
+ def safe_mkdir(path: str | os.PathLike[str]) -> None:
47
+ path = Path(path)
48
+ if not path.exists() and path.parent.exists() and os.access(path.parent, os.W_OK):
49
+ path.mkdir()
50
+
51
+
52
+ def scan_model_dir(path: Path) -> list[Path]:
53
+ if not path.is_dir():
54
+ return []
55
+ return [p for p in path.rglob("*") if p.is_file() and p.suffix == ".pt"]
56
+
57
+
58
+ def download_models(*names: str, check_remote: bool = True) -> dict[str, str]:
59
+ models = OrderedDict()
60
+ with ThreadPoolExecutor() as executor:
61
+ for name in names:
62
+ if "-world" in name:
63
+ models[name] = executor.submit(
64
+ hf_download,
65
+ name,
66
+ repo_id="Bingsu/yolo-world-mirror",
67
+ check_remote=check_remote,
68
+ )
69
+ else:
70
+ models[name] = executor.submit(
71
+ hf_download,
72
+ name,
73
+ check_remote=check_remote,
74
+ )
75
+ return {name: future.result() for name, future in models.items()}
76
+
77
+
78
+ def get_models(
79
+ *dirs: str | os.PathLike[str], huggingface: bool = True
80
+ ) -> OrderedDict[str, str]:
81
+ model_paths = []
82
+
83
+ for dir_ in dirs:
84
+ if not dir_:
85
+ continue
86
+ model_paths.extend(scan_model_dir(Path(dir_)))
87
+
88
+ models = OrderedDict()
89
+ to_download = [
90
+ "face_yolov8n.pt",
91
+ "face_yolov8s.pt",
92
+ "hand_yolov8n.pt",
93
+ "person_yolov8n-seg.pt",
94
+ "person_yolov8s-seg.pt",
95
+ "yolov8x-worldv2.pt",
96
+ ]
97
+ models.update(download_models(*to_download, check_remote=huggingface))
98
+
99
+ models.update(
100
+ {
101
+ "mediapipe_face_full": "mediapipe_face_full",
102
+ "mediapipe_face_short": "mediapipe_face_short",
103
+ "mediapipe_face_mesh": "mediapipe_face_mesh",
104
+ "mediapipe_face_mesh_eyes_only": "mediapipe_face_mesh_eyes_only",
105
+ }
106
+ )
107
+
108
+ invalid_keys = [k for k, v in models.items() if v == "INVALID"]
109
+ for key in invalid_keys:
110
+ models.pop(key)
111
+
112
+ for path in model_paths:
113
+ if path.name in models:
114
+ continue
115
+ models[path.name] = str(path)
116
+
117
+ return models
118
+
119
+
120
+ def create_mask_from_bbox(
121
+ bboxes: list[list[float]], shape: tuple[int, int]
122
+ ) -> list[Image.Image]:
123
+ """
124
+ Parameters
125
+ ----------
126
+ bboxes: list[list[float]]
127
+ list of [x1, y1, x2, y2]
128
+ bounding boxes
129
+ shape: tuple[int, int]
130
+ shape of the image (width, height)
131
+
132
+ Returns
133
+ -------
134
+ masks: list[Image.Image]
135
+ A list of masks
136
+
137
+ """
138
+ masks = []
139
+ for bbox in bboxes:
140
+ mask = Image.new("L", shape, 0)
141
+ mask_draw = ImageDraw.Draw(mask)
142
+ mask_draw.rectangle(bbox, fill=255)
143
+ masks.append(mask)
144
+ return masks
145
+
146
+
147
+ def create_bbox_from_mask(
148
+ masks: list[Image.Image], shape: tuple[int, int]
149
+ ) -> list[list[int]]:
150
+ """
151
+ Parameters
152
+ ----------
153
+ masks: list[Image.Image]
154
+ A list of masks
155
+ shape: tuple[int, int]
156
+ shape of the image (width, height)
157
+
158
+ Returns
159
+ -------
160
+ bboxes: list[list[float]]
161
+ A list of bounding boxes
162
+
163
+ """
164
+ bboxes = []
165
+ for mask in masks:
166
+ mask = mask.resize(shape)
167
+ bbox = mask.getbbox()
168
+ if bbox is not None:
169
+ bboxes.append(list(bbox))
170
+ return bboxes
171
+
172
+
173
+ def ensure_pil_image(image: Any, mode: str = "RGB") -> Image.Image:
174
+ if not isinstance(image, Image.Image):
175
+ image = to_pil_image(image)
176
+ if image.mode != mode:
177
+ image = image.convert(mode)
178
+ return image
adetailer-dev/adetailer-dev/adetailer/mask.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from enum import IntEnum
4
+ from functools import partial, reduce
5
+ from math import dist
6
+ from typing import Any, TypeVar
7
+
8
+ import cv2
9
+ import numpy as np
10
+ from PIL import Image, ImageChops
11
+
12
+ from adetailer.args import MASK_MERGE_INVERT
13
+ from adetailer.common import PredictOutput, ensure_pil_image
14
+
15
+
16
+ class SortBy(IntEnum):
17
+ NONE = 0
18
+ LEFT_TO_RIGHT = 1
19
+ CENTER_TO_EDGE = 2
20
+ AREA = 3
21
+
22
+
23
+ class MergeInvert(IntEnum):
24
+ NONE = 0
25
+ MERGE = 1
26
+ MERGE_INVERT = 2
27
+
28
+
29
+ T = TypeVar("T", int, float)
30
+
31
+
32
+ def _dilate(arr: np.ndarray, value: int) -> np.ndarray:
33
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (value, value))
34
+ return cv2.dilate(arr, kernel, iterations=1)
35
+
36
+
37
+ def _erode(arr: np.ndarray, value: int) -> np.ndarray:
38
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (value, value))
39
+ return cv2.erode(arr, kernel, iterations=1)
40
+
41
+
42
+ def dilate_erode(img: Image.Image, value: int) -> Image.Image:
43
+ """
44
+ The dilate_erode function takes an image and a value.
45
+ If the value is positive, it dilates the image by that amount.
46
+ If the value is negative, it erodes the image by that amount.
47
+
48
+ Parameters
49
+ ----------
50
+ img: PIL.Image.Image
51
+ the image to be processed
52
+ value: int
53
+ kernel size of dilation or erosion
54
+
55
+ Returns
56
+ -------
57
+ PIL.Image.Image
58
+ The image that has been dilated or eroded
59
+ """
60
+ if value == 0:
61
+ return img
62
+
63
+ arr = np.array(img)
64
+ arr = _dilate(arr, value) if value > 0 else _erode(arr, -value)
65
+
66
+ return Image.fromarray(arr)
67
+
68
+
69
+ def offset(img: Image.Image, x: int = 0, y: int = 0) -> Image.Image:
70
+ """
71
+ The offset function takes an image and offsets it by a given x(→) and y(↑) value.
72
+
73
+ Parameters
74
+ ----------
75
+ mask: Image.Image
76
+ Pass the mask image to the function
77
+ x: int
78
+
79
+ y: int
80
+
81
+
82
+ Returns
83
+ -------
84
+ PIL.Image.Image
85
+ A new image that is offset by x and y
86
+ """
87
+ return ImageChops.offset(img, x, -y)
88
+
89
+
90
+ def is_all_black(img: Image.Image | np.ndarray) -> bool:
91
+ if isinstance(img, Image.Image):
92
+ img = np.array(ensure_pil_image(img, "L"))
93
+ return cv2.countNonZero(img) == 0
94
+
95
+
96
+ def has_intersection(im1: Any, im2: Any) -> bool:
97
+ arr1 = np.array(ensure_pil_image(im1, "L"))
98
+ arr2 = np.array(ensure_pil_image(im2, "L"))
99
+ return not is_all_black(cv2.bitwise_and(arr1, arr2))
100
+
101
+
102
+ def bbox_area(bbox: list[T]) -> T:
103
+ return (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
104
+
105
+
106
+ def mask_preprocess(
107
+ masks: list[Image.Image],
108
+ kernel: int = 0,
109
+ x_offset: int = 0,
110
+ y_offset: int = 0,
111
+ merge_invert: int | MergeInvert | str = MergeInvert.NONE,
112
+ ) -> list[Image.Image]:
113
+ """
114
+ The mask_preprocess function takes a list of masks and preprocesses them.
115
+ It dilates and erodes the masks, and offsets them by x_offset and y_offset.
116
+
117
+ Parameters
118
+ ----------
119
+ masks: list[Image.Image]
120
+ A list of masks
121
+ kernel: int
122
+ kernel size of dilation or erosion
123
+ x_offset: int
124
+
125
+ y_offset: int
126
+
127
+
128
+ Returns
129
+ -------
130
+ list[Image.Image]
131
+ A list of processed masks
132
+ """
133
+ if not masks:
134
+ return []
135
+
136
+ if x_offset != 0 or y_offset != 0:
137
+ masks = [offset(m, x_offset, y_offset) for m in masks]
138
+
139
+ if kernel != 0:
140
+ masks = [dilate_erode(m, kernel) for m in masks]
141
+ masks = [m for m in masks if not is_all_black(m)]
142
+
143
+ return mask_merge_invert(masks, mode=merge_invert)
144
+
145
+
146
+ # Bbox sorting
147
+ def _key_left_to_right(bbox: list[T]) -> T:
148
+ """
149
+ Left to right
150
+
151
+ Parameters
152
+ ----------
153
+ bbox: list[int] | list[float]
154
+ list of [x1, y1, x2, y2]
155
+ """
156
+ return bbox[0]
157
+
158
+
159
+ def _key_center_to_edge(bbox: list[T], *, center: tuple[float, float]) -> float:
160
+ """
161
+ Center to edge
162
+
163
+ Parameters
164
+ ----------
165
+ bbox: list[int] | list[float]
166
+ list of [x1, y1, x2, y2]
167
+ image: Image.Image
168
+ the image
169
+ """
170
+ bbox_center = ((bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2)
171
+ return dist(center, bbox_center)
172
+
173
+
174
+ def _key_area(bbox: list[T]) -> T:
175
+ """
176
+ Large to small
177
+
178
+ Parameters
179
+ ----------
180
+ bbox: list[int] | list[float]
181
+ list of [x1, y1, x2, y2]
182
+ """
183
+ return -bbox_area(bbox)
184
+
185
+
186
+ def sort_bboxes(
187
+ pred: PredictOutput[T], order: int | SortBy = SortBy.NONE
188
+ ) -> PredictOutput[T]:
189
+ if order == SortBy.NONE or len(pred.bboxes) <= 1:
190
+ return pred
191
+
192
+ if order == SortBy.LEFT_TO_RIGHT:
193
+ key = _key_left_to_right
194
+ elif order == SortBy.CENTER_TO_EDGE:
195
+ width, height = pred.preview.size
196
+ center = (width / 2, height / 2)
197
+ key = partial(_key_center_to_edge, center=center)
198
+ elif order == SortBy.AREA:
199
+ key = _key_area
200
+ else:
201
+ raise RuntimeError
202
+
203
+ items = len(pred.bboxes)
204
+ idx = sorted(range(items), key=lambda i: key(pred.bboxes[i]))
205
+ pred.bboxes = [pred.bboxes[i] for i in idx]
206
+ pred.masks = [pred.masks[i] for i in idx]
207
+ return pred
208
+
209
+
210
+ # Filter by ratio
211
+ def is_in_ratio(bbox: list[T], low: float, high: float, orig_area: int) -> bool:
212
+ area = bbox_area(bbox)
213
+ return low <= area / orig_area <= high
214
+
215
+
216
+ def filter_by_ratio(
217
+ pred: PredictOutput[T], low: float, high: float
218
+ ) -> PredictOutput[T]:
219
+ if not pred.bboxes:
220
+ return pred
221
+
222
+ w, h = pred.preview.size
223
+ orig_area = w * h
224
+ items = len(pred.bboxes)
225
+ idx = [i for i in range(items) if is_in_ratio(pred.bboxes[i], low, high, orig_area)]
226
+ pred.bboxes = [pred.bboxes[i] for i in idx]
227
+ pred.masks = [pred.masks[i] for i in idx]
228
+ return pred
229
+
230
+
231
+ def filter_k_largest(pred: PredictOutput[T], k: int = 0) -> PredictOutput[T]:
232
+ if not pred.bboxes or k == 0:
233
+ return pred
234
+ areas = [bbox_area(bbox) for bbox in pred.bboxes]
235
+ idx = np.argsort(areas)[-k:]
236
+ idx = idx[::-1]
237
+ pred.bboxes = [pred.bboxes[i] for i in idx]
238
+ pred.masks = [pred.masks[i] for i in idx]
239
+ return pred
240
+
241
+
242
+ # Merge / Invert
243
+ def mask_merge(masks: list[Image.Image]) -> list[Image.Image]:
244
+ arrs = [np.array(m) for m in masks]
245
+ arr = reduce(cv2.bitwise_or, arrs)
246
+ return [Image.fromarray(arr)]
247
+
248
+
249
+ def mask_invert(masks: list[Image.Image]) -> list[Image.Image]:
250
+ return [ImageChops.invert(m) for m in masks]
251
+
252
+
253
+ def mask_merge_invert(
254
+ masks: list[Image.Image], mode: int | MergeInvert | str
255
+ ) -> list[Image.Image]:
256
+ if isinstance(mode, str):
257
+ mode = MASK_MERGE_INVERT.index(mode)
258
+
259
+ if mode == MergeInvert.NONE or not masks:
260
+ return masks
261
+
262
+ if mode == MergeInvert.MERGE:
263
+ return mask_merge(masks)
264
+
265
+ if mode == MergeInvert.MERGE_INVERT:
266
+ merged = mask_merge(masks)
267
+ return mask_invert(merged)
268
+
269
+ raise RuntimeError
adetailer-dev/adetailer-dev/adetailer/mediapipe.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from functools import partial
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from PIL import Image, ImageDraw
8
+
9
+ from adetailer import PredictOutput
10
+ from adetailer.common import create_bbox_from_mask, create_mask_from_bbox
11
+
12
+
13
+ def mediapipe_predict(
14
+ model_type: str, image: Image.Image, confidence: float = 0.3
15
+ ) -> PredictOutput:
16
+ mapping = {
17
+ "mediapipe_face_short": partial(mediapipe_face_detection, 0),
18
+ "mediapipe_face_full": partial(mediapipe_face_detection, 1),
19
+ "mediapipe_face_mesh": mediapipe_face_mesh,
20
+ "mediapipe_face_mesh_eyes_only": mediapipe_face_mesh_eyes_only,
21
+ }
22
+ if model_type in mapping:
23
+ func = mapping[model_type]
24
+ return func(image, confidence)
25
+ msg = f"[-] ADetailer: Invalid mediapipe model type: {model_type}, Available: {list(mapping.keys())!r}"
26
+ raise RuntimeError(msg)
27
+
28
+
29
+ def mediapipe_face_detection(
30
+ model_type: int, image: Image.Image, confidence: float = 0.3
31
+ ) -> PredictOutput[float]:
32
+ import mediapipe as mp
33
+
34
+ img_width, img_height = image.size
35
+
36
+ mp_face_detection = mp.solutions.face_detection
37
+ draw_util = mp.solutions.drawing_utils
38
+
39
+ img_array = np.array(image)
40
+
41
+ with mp_face_detection.FaceDetection(
42
+ model_selection=model_type, min_detection_confidence=confidence
43
+ ) as face_detector:
44
+ pred = face_detector.process(img_array)
45
+
46
+ if pred.detections is None:
47
+ return PredictOutput()
48
+
49
+ preview_array = img_array.copy()
50
+
51
+ bboxes = []
52
+ for detection in pred.detections:
53
+ draw_util.draw_detection(preview_array, detection)
54
+
55
+ bbox = detection.location_data.relative_bounding_box
56
+ x1 = bbox.xmin * img_width
57
+ y1 = bbox.ymin * img_height
58
+ w = bbox.width * img_width
59
+ h = bbox.height * img_height
60
+ x2 = x1 + w
61
+ y2 = y1 + h
62
+
63
+ bboxes.append([x1, y1, x2, y2])
64
+
65
+ masks = create_mask_from_bbox(bboxes, image.size)
66
+ preview = Image.fromarray(preview_array)
67
+
68
+ return PredictOutput(bboxes=bboxes, masks=masks, preview=preview)
69
+
70
+
71
+ def mediapipe_face_mesh(
72
+ image: Image.Image, confidence: float = 0.3
73
+ ) -> PredictOutput[int]:
74
+ import mediapipe as mp
75
+
76
+ mp_face_mesh = mp.solutions.face_mesh
77
+ draw_util = mp.solutions.drawing_utils
78
+ drawing_styles = mp.solutions.drawing_styles
79
+
80
+ w, h = image.size
81
+
82
+ with mp_face_mesh.FaceMesh(
83
+ static_image_mode=True, max_num_faces=20, min_detection_confidence=confidence
84
+ ) as face_mesh:
85
+ arr = np.array(image)
86
+ pred = face_mesh.process(arr)
87
+
88
+ if pred.multi_face_landmarks is None:
89
+ return PredictOutput()
90
+
91
+ preview = arr.copy()
92
+ masks = []
93
+
94
+ for landmarks in pred.multi_face_landmarks:
95
+ draw_util.draw_landmarks(
96
+ image=preview,
97
+ landmark_list=landmarks,
98
+ connections=mp_face_mesh.FACEMESH_TESSELATION,
99
+ landmark_drawing_spec=None,
100
+ connection_drawing_spec=drawing_styles.get_default_face_mesh_tesselation_style(),
101
+ )
102
+
103
+ points = np.array(
104
+ [[land.x * w, land.y * h] for land in landmarks.landmark], dtype=int
105
+ )
106
+ outline = cv2.convexHull(points).reshape(-1).tolist()
107
+
108
+ mask = Image.new("L", image.size, "black")
109
+ draw = ImageDraw.Draw(mask)
110
+ draw.polygon(outline, fill="white")
111
+ masks.append(mask)
112
+
113
+ bboxes = create_bbox_from_mask(masks, image.size)
114
+ preview = Image.fromarray(preview)
115
+ return PredictOutput(bboxes=bboxes, masks=masks, preview=preview)
116
+
117
+
118
+ def mediapipe_face_mesh_eyes_only(
119
+ image: Image.Image, confidence: float = 0.3
120
+ ) -> PredictOutput[int]:
121
+ import mediapipe as mp
122
+
123
+ mp_face_mesh = mp.solutions.face_mesh
124
+
125
+ left_idx = np.array(list(mp_face_mesh.FACEMESH_LEFT_EYE)).flatten()
126
+ right_idx = np.array(list(mp_face_mesh.FACEMESH_RIGHT_EYE)).flatten()
127
+
128
+ w, h = image.size
129
+
130
+ with mp_face_mesh.FaceMesh(
131
+ static_image_mode=True, max_num_faces=20, min_detection_confidence=confidence
132
+ ) as face_mesh:
133
+ arr = np.array(image)
134
+ pred = face_mesh.process(arr)
135
+
136
+ if pred.multi_face_landmarks is None:
137
+ return PredictOutput()
138
+
139
+ preview = image.copy()
140
+ masks = []
141
+
142
+ for landmarks in pred.multi_face_landmarks:
143
+ points = np.array(
144
+ [[land.x * w, land.y * h] for land in landmarks.landmark], dtype=int
145
+ )
146
+ left_eyes = points[left_idx]
147
+ right_eyes = points[right_idx]
148
+ left_outline = cv2.convexHull(left_eyes).reshape(-1).tolist()
149
+ right_outline = cv2.convexHull(right_eyes).reshape(-1).tolist()
150
+
151
+ mask = Image.new("L", image.size, "black")
152
+ draw = ImageDraw.Draw(mask)
153
+ for outline in (left_outline, right_outline):
154
+ draw.polygon(outline, fill="white")
155
+ masks.append(mask)
156
+
157
+ bboxes = create_bbox_from_mask(masks, image.size)
158
+ preview = draw_preview(preview, bboxes, masks)
159
+ return PredictOutput(bboxes=bboxes, masks=masks, preview=preview)
160
+
161
+
162
+ def draw_preview(
163
+ preview: Image.Image, bboxes: list[list[int]], masks: list[Image.Image]
164
+ ) -> Image.Image:
165
+ red = Image.new("RGB", preview.size, "red")
166
+ for mask in masks:
167
+ masked = Image.composite(red, preview, mask)
168
+ preview = Image.blend(preview, masked, 0.25)
169
+
170
+ draw = ImageDraw.Draw(preview)
171
+ for bbox in bboxes:
172
+ draw.rectangle(bbox, outline="red", width=2)
173
+
174
+ return preview
adetailer-dev/adetailer-dev/adetailer/ultralytics.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ import cv2
7
+ from PIL import Image
8
+ from torchvision.transforms.functional import to_pil_image
9
+
10
+ from adetailer import PredictOutput
11
+ from adetailer.common import create_mask_from_bbox
12
+
13
+ if TYPE_CHECKING:
14
+ import torch
15
+ from ultralytics import YOLO, YOLOWorld
16
+
17
+
18
+ def ultralytics_predict(
19
+ model_path: str | Path,
20
+ image: Image.Image,
21
+ confidence: float = 0.3,
22
+ device: str = "",
23
+ classes: str = "",
24
+ ) -> PredictOutput[float]:
25
+ from ultralytics import YOLO
26
+
27
+ model = YOLO(model_path)
28
+ apply_classes(model, model_path, classes)
29
+ pred = model(image, conf=confidence, device=device)
30
+
31
+ bboxes = pred[0].boxes.xyxy.cpu().numpy()
32
+ if bboxes.size == 0:
33
+ return PredictOutput()
34
+ bboxes = bboxes.tolist()
35
+
36
+ if pred[0].masks is None:
37
+ masks = create_mask_from_bbox(bboxes, image.size)
38
+ else:
39
+ masks = mask_to_pil(pred[0].masks.data, image.size)
40
+ preview = pred[0].plot()
41
+ preview = cv2.cvtColor(preview, cv2.COLOR_BGR2RGB)
42
+ preview = Image.fromarray(preview)
43
+
44
+ return PredictOutput(bboxes=bboxes, masks=masks, preview=preview)
45
+
46
+
47
+ def apply_classes(model: YOLO | YOLOWorld, model_path: str | Path, classes: str):
48
+ if not classes or "-world" not in Path(model_path).stem:
49
+ return
50
+ parsed = [c.strip() for c in classes.split(",") if c.strip()]
51
+ if parsed:
52
+ model.set_classes(parsed)
53
+
54
+
55
+ def mask_to_pil(masks: torch.Tensor, shape: tuple[int, int]) -> list[Image.Image]:
56
+ """
57
+ Parameters
58
+ ----------
59
+ masks: torch.Tensor, dtype=torch.float32, shape=(N, H, W).
60
+ The device can be CUDA, but `to_pil_image` takes care of that.
61
+
62
+ shape: tuple[int, int]
63
+ (W, H) of the original image
64
+ """
65
+ n = masks.shape[0]
66
+ return [to_pil_image(masks[i], mode="L").resize(shape) for i in range(n)]
adetailer-dev/adetailer-dev/controlnet_ext/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ try:
2
+ from .controlnet_ext_forge import (
3
+ ControlNetExt,
4
+ controlnet_exists,
5
+ controlnet_type,
6
+ get_cn_models,
7
+ )
8
+ except ImportError:
9
+ from .controlnet_ext import (
10
+ ControlNetExt,
11
+ controlnet_exists,
12
+ controlnet_type,
13
+ get_cn_models,
14
+ )
15
+
16
+ from .restore import CNHijackRestore, cn_allow_script_control
17
+
18
+ __all__ = [
19
+ "ControlNetExt",
20
+ "CNHijackRestore",
21
+ "cn_allow_script_control",
22
+ "controlnet_exists",
23
+ "controlnet_type",
24
+ "get_cn_models",
25
+ ]
adetailer-dev/adetailer-dev/controlnet_ext/common.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ cn_model_module = {
4
+ "inpaint": "inpaint_global_harmonious",
5
+ "scribble": "t2ia_sketch_pidi",
6
+ "lineart": "lineart_coarse",
7
+ "openpose": "openpose_full",
8
+ "tile": "tile_resample",
9
+ "depth": "depth_midas",
10
+ }
11
+ _names = [*cn_model_module, "union"]
12
+ cn_model_regex = re.compile("|".join(_names), flags=re.IGNORECASE)
adetailer-dev/adetailer-dev/controlnet_ext/controlnet_ext.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import sys
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+
8
+ from modules import extensions, sd_models, shared
9
+ from modules.paths import extensions_builtin_dir, extensions_dir, models_path
10
+
11
+ from .common import cn_model_module, cn_model_regex
12
+
13
+ ext_path = Path(extensions_dir)
14
+ ext_builtin_path = Path(extensions_builtin_dir)
15
+ controlnet_exists = False
16
+ controlnet_type = "standard"
17
+ controlnet_path = None
18
+ cn_base_path = ""
19
+
20
+ for extension in extensions.active():
21
+ if not extension.enabled:
22
+ continue
23
+ # For cases like sd-webui-controlnet-master
24
+ if "sd-webui-controlnet" in extension.name:
25
+ controlnet_exists = True
26
+ controlnet_path = Path(extension.path)
27
+ cn_base_path = ".".join(controlnet_path.parts[-2:])
28
+ break
29
+
30
+ if controlnet_path is not None:
31
+ sd_webui_controlnet_path = controlnet_path.resolve().parent
32
+ if sd_webui_controlnet_path.stem in ("extensions", "extensions-builtin"):
33
+ target_path = str(sd_webui_controlnet_path.parent)
34
+ if target_path not in sys.path:
35
+ sys.path.append(target_path)
36
+
37
+
38
+ class ControlNetExt:
39
+ def __init__(self):
40
+ self.cn_models = ["None"]
41
+ self.cn_available = False
42
+ self.external_cn = None
43
+
44
+ def init_controlnet(self):
45
+ import_path = cn_base_path + ".scripts.external_code"
46
+
47
+ self.external_cn = importlib.import_module(import_path, "external_code")
48
+ self.cn_available = True
49
+ models = self.external_cn.get_models()
50
+ self.cn_models.extend(m for m in models if cn_model_regex.search(m))
51
+
52
+ def update_scripts_args(
53
+ self,
54
+ p,
55
+ model: str,
56
+ module: str | None,
57
+ weight: float,
58
+ guidance_start: float,
59
+ guidance_end: float,
60
+ ):
61
+ if (not self.cn_available) or model == "None":
62
+ return
63
+
64
+ if module == "None":
65
+ module = None
66
+ if module is None:
67
+ for m, v in cn_model_module.items():
68
+ if m in model:
69
+ module = v
70
+ break
71
+
72
+ cn_units = [
73
+ self.external_cn.ControlNetUnit(
74
+ model=model,
75
+ weight=weight,
76
+ control_mode=self.external_cn.ControlMode.BALANCED,
77
+ module=module,
78
+ guidance_start=guidance_start,
79
+ guidance_end=guidance_end,
80
+ pixel_perfect=True,
81
+ )
82
+ ]
83
+
84
+ try:
85
+ self.external_cn.update_cn_script_in_processing(p, cn_units)
86
+ except AttributeError as e:
87
+ if "script_args_value" not in str(e):
88
+ raise
89
+ msg = "[-] Adetailer: ControlNet option not available in WEBUI version lower than 1.6.0 due to updates in ControlNet"
90
+ raise RuntimeError(msg) from e
91
+
92
+
93
+ def get_cn_model_dirs() -> list[Path]:
94
+ cn_model_dir = Path(models_path, "ControlNet")
95
+ if controlnet_path is not None:
96
+ cn_model_dir_old = controlnet_path.joinpath("models")
97
+ else:
98
+ cn_model_dir_old = None
99
+ ext_dir1 = shared.opts.data.get("control_net_models_path", "")
100
+ ext_dir2 = getattr(shared.cmd_opts, "controlnet_dir", "")
101
+
102
+ dirs = [cn_model_dir]
103
+ dirs += [
104
+ Path(ext_dir) for ext_dir in [cn_model_dir_old, ext_dir1, ext_dir2] if ext_dir
105
+ ]
106
+
107
+ return dirs
108
+
109
+
110
+ @lru_cache
111
+ def _get_cn_models() -> list[str]:
112
+ """
113
+ Since we can't import ControlNet, we use a function that does something like
114
+ controlnet's `list(global_state.cn_models_names.values())`.
115
+ """
116
+ cn_model_exts = (".pt", ".pth", ".ckpt", ".safetensors")
117
+ dirs = get_cn_model_dirs()
118
+ name_filter = shared.opts.data.get("control_net_models_name_filter", "")
119
+ name_filter = name_filter.strip(" ").lower()
120
+
121
+ model_paths = []
122
+
123
+ for base in dirs:
124
+ if not base.exists():
125
+ continue
126
+
127
+ for p in base.rglob("*"):
128
+ if (
129
+ p.is_file()
130
+ and p.suffix in cn_model_exts
131
+ and cn_model_regex.search(p.name)
132
+ ):
133
+ if name_filter and name_filter not in p.name.lower():
134
+ continue
135
+ model_paths.append(p)
136
+ model_paths.sort(key=lambda p: p.name)
137
+
138
+ models = []
139
+ for p in model_paths:
140
+ model_hash = sd_models.model_hash(p)
141
+ name = f"{p.stem} [{model_hash}]"
142
+ models.append(name)
143
+ return models
144
+
145
+
146
+ def get_cn_models() -> list[str]:
147
+ if controlnet_exists:
148
+ return _get_cn_models()
149
+ return []
adetailer-dev/adetailer-dev/controlnet_ext/controlnet_ext_forge.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+
5
+ import numpy as np
6
+ from lib_controlnet import external_code, global_state
7
+ from lib_controlnet.external_code import ControlNetUnit
8
+
9
+ from modules import scripts
10
+ from modules.processing import StableDiffusionProcessing
11
+
12
+ from .common import cn_model_regex
13
+
14
+ controlnet_exists = True
15
+ controlnet_type = "forge"
16
+
17
+
18
+ def find_script(p: StableDiffusionProcessing, script_title: str) -> scripts.Script:
19
+ script = next((s for s in p.scripts.scripts if s.title() == script_title), None)
20
+ if not script:
21
+ msg = f"Script not found: {script_title!r}"
22
+ raise RuntimeError(msg)
23
+ return script
24
+
25
+
26
+ def add_forge_script_to_adetailer_run(
27
+ p: StableDiffusionProcessing, script_title: str, script_args: list
28
+ ):
29
+ p.scripts = copy.copy(scripts.scripts_img2img)
30
+ p.scripts.alwayson_scripts = []
31
+ p.script_args_value = []
32
+
33
+ script = copy.copy(find_script(p, script_title))
34
+ script.args_from = len(p.script_args_value)
35
+ script.args_to = len(p.script_args_value) + len(script_args)
36
+ p.scripts.alwayson_scripts.append(script)
37
+ p.script_args_value.extend(script_args)
38
+
39
+
40
+ class ControlNetExt:
41
+ def __init__(self):
42
+ self.cn_available = False
43
+ self.external_cn = external_code
44
+
45
+ def init_controlnet(self):
46
+ self.cn_available = True
47
+
48
+ def update_scripts_args(
49
+ self,
50
+ p,
51
+ model: str,
52
+ module: str | None,
53
+ weight: float,
54
+ guidance_start: float,
55
+ guidance_end: float,
56
+ ):
57
+ if (not self.cn_available) or model == "None":
58
+ return
59
+
60
+ image = np.asarray(p.init_images[0])
61
+ mask = np.full_like(image, fill_value=255)
62
+
63
+ cnet_image = {"image": image, "mask": mask}
64
+
65
+ pres = external_code.pixel_perfect_resolution(
66
+ image,
67
+ target_H=p.height,
68
+ target_W=p.width,
69
+ resize_mode=external_code.resize_mode_from_value(p.resize_mode),
70
+ )
71
+
72
+ add_forge_script_to_adetailer_run(
73
+ p,
74
+ "ControlNet",
75
+ [
76
+ ControlNetUnit(
77
+ enabled=True,
78
+ image=cnet_image,
79
+ model=model,
80
+ module=module,
81
+ weight=weight,
82
+ guidance_start=guidance_start,
83
+ guidance_end=guidance_end,
84
+ processor_res=pres,
85
+ )
86
+ ],
87
+ )
88
+
89
+
90
+ def get_cn_models() -> list[str]:
91
+ models = global_state.get_all_controlnet_names()
92
+ return [m for m in models if cn_model_regex.search(m)]
adetailer-dev/adetailer-dev/controlnet_ext/restore.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+
5
+ from modules import img2img, processing, shared
6
+
7
+
8
+ class CNHijackRestore:
9
+ def __init__(self):
10
+ self.process = hasattr(processing, "__controlnet_original_process_images_inner")
11
+ self.img2img = hasattr(img2img, "__controlnet_original_process_batch")
12
+
13
+ def __enter__(self):
14
+ if self.process:
15
+ self.orig_process = processing.process_images_inner
16
+ processing.process_images_inner = getattr(
17
+ processing, "__controlnet_original_process_images_inner"
18
+ )
19
+ if self.img2img:
20
+ self.orig_img2img = img2img.process_batch
21
+ img2img.process_batch = getattr(
22
+ img2img, "__controlnet_original_process_batch"
23
+ )
24
+
25
+ def __exit__(self, *args, **kwargs):
26
+ if self.process:
27
+ processing.process_images_inner = self.orig_process
28
+ if self.img2img:
29
+ img2img.process_batch = self.orig_img2img
30
+
31
+
32
+ @contextmanager
33
+ def cn_allow_script_control():
34
+ orig = False
35
+ if "control_net_allow_script_control" in shared.opts.data:
36
+ try:
37
+ orig = shared.opts.data["control_net_allow_script_control"]
38
+ shared.opts.data["control_net_allow_script_control"] = True
39
+ yield
40
+ finally:
41
+ shared.opts.data["control_net_allow_script_control"] = orig
42
+ else:
43
+ yield
adetailer-dev/adetailer-dev/install.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import subprocess
5
+ import sys
6
+ from importlib.metadata import version # python >= 3.8
7
+
8
+ from packaging.version import parse
9
+
10
+ import_name = {"py-cpuinfo": "cpuinfo", "protobuf": "google.protobuf"}
11
+
12
+
13
+ def is_installed(
14
+ package: str, min_version: str | None = None, max_version: str | None = None
15
+ ):
16
+ name = import_name.get(package, package)
17
+ try:
18
+ spec = importlib.util.find_spec(name)
19
+ except ModuleNotFoundError:
20
+ return False
21
+
22
+ if spec is None:
23
+ return False
24
+
25
+ if not min_version and not max_version:
26
+ return True
27
+
28
+ if not min_version:
29
+ min_version = "0.0.0"
30
+ if not max_version:
31
+ max_version = "99999999.99999999.99999999"
32
+
33
+ try:
34
+ pkg_version = version(package)
35
+ return parse(min_version) <= parse(pkg_version) <= parse(max_version)
36
+ except Exception:
37
+ return False
38
+
39
+
40
+ def run_pip(*args):
41
+ subprocess.run([sys.executable, "-m", "pip", "install", *args], check=True)
42
+
43
+
44
+ def install():
45
+ deps = [
46
+ # requirements
47
+ ("ultralytics", "8.2.0", None),
48
+ ("mediapipe", "0.10.13", None),
49
+ ("rich", "13.0.0", None),
50
+ ]
51
+
52
+ pkgs = []
53
+ for pkg, low, high in deps:
54
+ if not is_installed(pkg, low, high):
55
+ if low and high:
56
+ cmd = f"{pkg}>={low},<={high}"
57
+ elif low:
58
+ cmd = f"{pkg}>={low}"
59
+ elif high:
60
+ cmd = f"{pkg}<={high}"
61
+ else:
62
+ cmd = pkg
63
+ pkgs.append(cmd)
64
+
65
+ if pkgs:
66
+ run_pip(*pkgs)
67
+
68
+
69
+ try:
70
+ import launch
71
+
72
+ skip_install = launch.args.skip_install
73
+ except Exception:
74
+ skip_install = False
75
+
76
+ if not skip_install:
77
+ install()
adetailer-dev/adetailer-dev/preload.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+
3
+
4
+ def preload(parser: argparse.ArgumentParser):
5
+ parser.add_argument(
6
+ "--ad-no-huggingface",
7
+ action="store_true",
8
+ help="Don't use adetailer models from huggingface",
9
+ )
adetailer-dev/adetailer-dev/pyproject.toml ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "adetailer"
3
+ description = "An object detection and auto-mask extension for stable diffusion webui."
4
+ authors = [{ name = "dowon", email = "ks2515@naver.com" }]
5
+ requires-python = ">=3.9"
6
+ readme = "README.md"
7
+ license = { text = "AGPL-3.0" }
8
+ dependencies = [
9
+ "ultralytics>=8.2",
10
+ "mediapipe>=0.10.13",
11
+ "pydantic<3",
12
+ "rich>=13",
13
+ "huggingface_hub",
14
+ ]
15
+ keywords = [
16
+ "stable-diffusion",
17
+ "stable-diffusion-webui",
18
+ "adetailer",
19
+ "ultralytics",
20
+ ]
21
+ classifiers = [
22
+ "License :: OSI Approved :: GNU Affero General Public License v3",
23
+ "Topic :: Scientific/Engineering :: Image Recognition",
24
+ ]
25
+ dynamic = ["version"]
26
+
27
+ [project.urls]
28
+ repository = "https://github.com/Bing-su/adetailer"
29
+
30
+ [build-system]
31
+ requires = ["hatchling"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.version]
35
+ path = "adetailer/__version__.py"
36
+
37
+ [tool.isort]
38
+ profile = "black"
39
+ known_first_party = ["launch", "modules"]
40
+
41
+ [tool.ruff]
42
+ target-version = "py39"
43
+
44
+ [tool.ruff.lint]
45
+ select = [
46
+ "A",
47
+ "B",
48
+ "C4",
49
+ "C90",
50
+ "E",
51
+ "EM",
52
+ "F",
53
+ "FA",
54
+ "I001",
55
+ "ISC",
56
+ "N",
57
+ "PD",
58
+ "PERF",
59
+ "PIE",
60
+ "PT",
61
+ "PTH",
62
+ "RET",
63
+ "RUF",
64
+ "SIM",
65
+ "T20",
66
+ "TRY",
67
+ "UP",
68
+ "W",
69
+ ]
70
+ ignore = ["B905", "E501"]
71
+ unfixable = ["F401"]
72
+
73
+ [tool.ruff.lint.isort]
74
+ known-first-party = ["launch", "modules"]
75
+
76
+ [tool.ruff.lint.pyupgrade]
77
+ keep-runtime-typing = true
adetailer-dev/adetailer-dev/scripts/!adetailer.py ADDED
@@ -0,0 +1,1052 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ import re
5
+ import sys
6
+ import traceback
7
+ from copy import copy
8
+ from functools import partial
9
+ from pathlib import Path
10
+ from textwrap import dedent
11
+ from typing import TYPE_CHECKING, Any, NamedTuple, cast
12
+
13
+ import gradio as gr
14
+ from PIL import Image, ImageChops
15
+ from rich import print
16
+
17
+ import modules
18
+ from aaaaaa.conditional import create_binary_mask, schedulers
19
+ from aaaaaa.helper import (
20
+ change_torch_load,
21
+ copy_extra_params,
22
+ pause_total_tqdm,
23
+ preserve_prompts,
24
+ )
25
+ from aaaaaa.p_method import (
26
+ get_i,
27
+ is_img2img_inpaint,
28
+ is_inpaint_only_masked,
29
+ is_skip_img2img,
30
+ need_call_postprocess,
31
+ need_call_process,
32
+ )
33
+ from aaaaaa.traceback import rich_traceback
34
+ from aaaaaa.ui import WebuiInfo, adui, ordinal, suffix
35
+ from adetailer import (
36
+ ADETAILER,
37
+ __version__,
38
+ get_models,
39
+ mediapipe_predict,
40
+ ultralytics_predict,
41
+ )
42
+ from adetailer.args import (
43
+ BBOX_SORTBY,
44
+ BUILTIN_SCRIPT,
45
+ SCRIPT_DEFAULT,
46
+ ADetailerArgs,
47
+ SkipImg2ImgOrig,
48
+ )
49
+ from adetailer.common import PredictOutput, ensure_pil_image, safe_mkdir
50
+ from adetailer.mask import (
51
+ filter_by_ratio,
52
+ filter_k_largest,
53
+ has_intersection,
54
+ is_all_black,
55
+ mask_preprocess,
56
+ sort_bboxes,
57
+ )
58
+ from controlnet_ext import (
59
+ CNHijackRestore,
60
+ ControlNetExt,
61
+ cn_allow_script_control,
62
+ controlnet_exists,
63
+ controlnet_type,
64
+ get_cn_models,
65
+ )
66
+ from modules import images, paths, script_callbacks, scripts, shared
67
+ from modules.devices import NansException
68
+ from modules.processing import (
69
+ Processed,
70
+ StableDiffusionProcessingImg2Img,
71
+ create_infotext,
72
+ process_images,
73
+ )
74
+ from modules.sd_samplers import all_samplers
75
+ from modules.shared import cmd_opts, opts, state
76
+
77
+ if TYPE_CHECKING:
78
+ from fastapi import FastAPI
79
+
80
+ PARAMS_TXT = "params.txt"
81
+
82
+ no_huggingface = getattr(cmd_opts, "ad_no_huggingface", False)
83
+ adetailer_dir = Path(paths.models_path, "adetailer")
84
+ safe_mkdir(adetailer_dir)
85
+
86
+ extra_models_dirs = shared.opts.data.get("ad_extra_models_dir", "")
87
+ model_mapping = get_models(
88
+ adetailer_dir,
89
+ *extra_models_dirs.split("|"),
90
+ huggingface=not no_huggingface,
91
+ )
92
+
93
+ txt2img_submit_button = img2img_submit_button = None
94
+ txt2img_submit_button = cast(gr.Button, txt2img_submit_button)
95
+ img2img_submit_button = cast(gr.Button, img2img_submit_button)
96
+
97
+ print(
98
+ f"[-] ADetailer initialized. version: {__version__}, num models: {len(model_mapping)}"
99
+ )
100
+
101
+
102
+ class AfterDetailerScript(scripts.Script):
103
+ def __init__(self):
104
+ super().__init__()
105
+ self.ultralytics_device = self.get_ultralytics_device()
106
+
107
+ self.controlnet_ext = None
108
+
109
+ def __repr__(self):
110
+ return f"{self.__class__.__name__}(version={__version__})"
111
+
112
+ def title(self):
113
+ return ADETAILER
114
+
115
+ def show(self, is_img2img):
116
+ return scripts.AlwaysVisible
117
+
118
+ def ui(self, is_img2img):
119
+ num_models = opts.data.get("ad_max_models", 2)
120
+ ad_model_list = list(model_mapping.keys())
121
+ sampler_names = [sampler.name for sampler in all_samplers]
122
+ scheduler_names = [x.label for x in schedulers]
123
+
124
+ checkpoint_list = modules.sd_models.checkpoint_tiles(use_short=True)
125
+ vae_list = modules.shared_items.sd_vae_items()
126
+
127
+ webui_info = WebuiInfo(
128
+ ad_model_list=ad_model_list,
129
+ sampler_names=sampler_names,
130
+ scheduler_names=scheduler_names,
131
+ t2i_button=txt2img_submit_button,
132
+ i2i_button=img2img_submit_button,
133
+ checkpoints_list=checkpoint_list,
134
+ vae_list=vae_list,
135
+ )
136
+
137
+ components, infotext_fields = adui(num_models, is_img2img, webui_info)
138
+
139
+ self.infotext_fields = infotext_fields
140
+ return components
141
+
142
+ def init_controlnet_ext(self) -> None:
143
+ if self.controlnet_ext is not None:
144
+ return
145
+ self.controlnet_ext = ControlNetExt()
146
+
147
+ if controlnet_exists:
148
+ try:
149
+ self.controlnet_ext.init_controlnet()
150
+ except ImportError:
151
+ error = traceback.format_exc()
152
+ print(
153
+ f"[-] ADetailer: ControlNetExt init failed:\n{error}",
154
+ file=sys.stderr,
155
+ )
156
+
157
+ def update_controlnet_args(self, p, args: ADetailerArgs) -> None:
158
+ if self.controlnet_ext is None:
159
+ self.init_controlnet_ext()
160
+
161
+ if (
162
+ self.controlnet_ext is not None
163
+ and self.controlnet_ext.cn_available
164
+ and args.ad_controlnet_model != "None"
165
+ ):
166
+ self.controlnet_ext.update_scripts_args(
167
+ p,
168
+ model=args.ad_controlnet_model,
169
+ module=args.ad_controlnet_module,
170
+ weight=args.ad_controlnet_weight,
171
+ guidance_start=args.ad_controlnet_guidance_start,
172
+ guidance_end=args.ad_controlnet_guidance_end,
173
+ )
174
+
175
+ def is_ad_enabled(self, *args_) -> bool:
176
+ arg_list = [arg for arg in args_ if isinstance(arg, dict)]
177
+ if not args_ or not arg_list:
178
+ message = f"""
179
+ [-] ADetailer: Invalid arguments passed to ADetailer.
180
+ input: {args_!r}
181
+ ADetailer disabled.
182
+ """
183
+ print(dedent(message), file=sys.stderr)
184
+ return False
185
+
186
+ ad_enabled = args_[0] if isinstance(args_[0], bool) else True
187
+ pydantic_args = []
188
+ for arg in arg_list:
189
+ try:
190
+ pydantic_args.append(ADetailerArgs(**arg))
191
+ except ValueError: # noqa: PERF203
192
+ continue
193
+ not_none = not all(arg.need_skip() for arg in pydantic_args)
194
+ return ad_enabled and not_none
195
+
196
+ def set_skip_img2img(self, p, *args_) -> None:
197
+ if (
198
+ hasattr(p, "_ad_skip_img2img")
199
+ or not hasattr(p, "init_images")
200
+ or not p.init_images
201
+ ):
202
+ return
203
+
204
+ if len(args_) >= 2 and isinstance(args_[1], bool):
205
+ p._ad_skip_img2img = args_[1]
206
+ else:
207
+ p._ad_skip_img2img = False
208
+
209
+ if not p._ad_skip_img2img:
210
+ return
211
+
212
+ if is_img2img_inpaint(p):
213
+ p._ad_disabled = True
214
+ msg = "[-] ADetailer: img2img inpainting with skip img2img is not supported. (because it's buggy)"
215
+ print(msg)
216
+ return
217
+
218
+ p._ad_orig = SkipImg2ImgOrig(
219
+ steps=p.steps,
220
+ sampler_name=p.sampler_name,
221
+ width=p.width,
222
+ height=p.height,
223
+ )
224
+ p.steps = 1
225
+ p.sampler_name = "Euler"
226
+ p.width = 128
227
+ p.height = 128
228
+
229
+ def get_args(self, p, *args_) -> list[ADetailerArgs]:
230
+ """
231
+ `args_` is at least 1 in length by `is_ad_enabled` immediately above
232
+ """
233
+ args = [arg for arg in args_ if isinstance(arg, dict)]
234
+
235
+ if not args:
236
+ message = f"[-] ADetailer: Invalid arguments passed to ADetailer: {args_!r}"
237
+ raise ValueError(message)
238
+
239
+ if hasattr(p, "_ad_xyz"):
240
+ args[0] = {**args[0], **p._ad_xyz}
241
+
242
+ all_inputs = []
243
+
244
+ for n, arg_dict in enumerate(args, 1):
245
+ try:
246
+ inp = ADetailerArgs(**arg_dict)
247
+ except ValueError as e:
248
+ msg = f"[-] ADetailer: ValidationError when validating {ordinal(n)} arguments"
249
+ if hasattr(e, "add_note"):
250
+ e.add_note(msg)
251
+ else:
252
+ print(msg, file=sys.stderr)
253
+ raise
254
+
255
+ all_inputs.append(inp)
256
+
257
+ return all_inputs
258
+
259
+ def extra_params(self, arg_list: list[ADetailerArgs]) -> dict:
260
+ params = {}
261
+ for n, args in enumerate(arg_list):
262
+ params.update(args.extra_params(suffix=suffix(n)))
263
+ params["ADetailer version"] = __version__
264
+ return params
265
+
266
+ @staticmethod
267
+ def get_ultralytics_device() -> str:
268
+ if "adetailer" in shared.cmd_opts.use_cpu:
269
+ return "cpu"
270
+
271
+ if platform.system() == "Darwin":
272
+ return ""
273
+
274
+ vram_args = ["lowvram", "medvram", "medvram_sdxl"]
275
+ if any(getattr(cmd_opts, vram, False) for vram in vram_args):
276
+ return "cpu"
277
+
278
+ return ""
279
+
280
+ def prompt_blank_replacement(
281
+ self, all_prompts: list[str], i: int, default: str
282
+ ) -> str:
283
+ if not all_prompts:
284
+ return default
285
+ if i < len(all_prompts):
286
+ return all_prompts[i]
287
+ j = i % len(all_prompts)
288
+ return all_prompts[j]
289
+
290
+ def _get_prompt(
291
+ self,
292
+ ad_prompt: str,
293
+ all_prompts: list[str],
294
+ i: int,
295
+ default: str,
296
+ replacements: list[PromptSR],
297
+ ) -> list[str]:
298
+ prompts = re.split(r"\s*\[SEP\]\s*", ad_prompt)
299
+ blank_replacement = self.prompt_blank_replacement(all_prompts, i, default)
300
+ for n in range(len(prompts)):
301
+ if not prompts[n]:
302
+ prompts[n] = blank_replacement
303
+ elif "[PROMPT]" in prompts[n]:
304
+ prompts[n] = prompts[n].replace("[PROMPT]", blank_replacement)
305
+
306
+ for pair in replacements:
307
+ prompts[n] = prompts[n].replace(pair.s, pair.r)
308
+ return prompts
309
+
310
+ def get_prompt(self, p, args: ADetailerArgs) -> tuple[list[str], list[str]]:
311
+ i = get_i(p)
312
+ prompt_sr = p._ad_xyz_prompt_sr if hasattr(p, "_ad_xyz_prompt_sr") else []
313
+
314
+ prompt = self._get_prompt(
315
+ ad_prompt=args.ad_prompt,
316
+ all_prompts=p.all_prompts,
317
+ i=i,
318
+ default=p.prompt,
319
+ replacements=prompt_sr,
320
+ )
321
+ negative_prompt = self._get_prompt(
322
+ ad_prompt=args.ad_negative_prompt,
323
+ all_prompts=p.all_negative_prompts,
324
+ i=i,
325
+ default=p.negative_prompt,
326
+ replacements=prompt_sr,
327
+ )
328
+
329
+ return prompt, negative_prompt
330
+
331
+ def get_seed(self, p) -> tuple[int, int]:
332
+ i = get_i(p)
333
+
334
+ if not p.all_seeds:
335
+ seed = p.seed
336
+ elif i < len(p.all_seeds):
337
+ seed = p.all_seeds[i]
338
+ else:
339
+ j = i % len(p.all_seeds)
340
+ seed = p.all_seeds[j]
341
+
342
+ if not p.all_subseeds:
343
+ subseed = p.subseed
344
+ elif i < len(p.all_subseeds):
345
+ subseed = p.all_subseeds[i]
346
+ else:
347
+ j = i % len(p.all_subseeds)
348
+ subseed = p.all_subseeds[j]
349
+
350
+ return seed, subseed
351
+
352
+ def get_width_height(self, p, args: ADetailerArgs) -> tuple[int, int]:
353
+ if args.ad_use_inpaint_width_height:
354
+ width = args.ad_inpaint_width
355
+ height = args.ad_inpaint_height
356
+ elif hasattr(p, "_ad_orig"):
357
+ width = p._ad_orig.width
358
+ height = p._ad_orig.height
359
+ else:
360
+ width = p.width
361
+ height = p.height
362
+
363
+ return width, height
364
+
365
+ def get_steps(self, p, args: ADetailerArgs) -> int:
366
+ if args.ad_use_steps:
367
+ return args.ad_steps
368
+ if hasattr(p, "_ad_orig"):
369
+ return p._ad_orig.steps
370
+ return p.steps
371
+
372
+ def get_cfg_scale(self, p, args: ADetailerArgs) -> float:
373
+ return args.ad_cfg_scale if args.ad_use_cfg_scale else p.cfg_scale
374
+
375
+ def get_sampler(self, p, args: ADetailerArgs) -> str:
376
+ if args.ad_use_sampler:
377
+ return args.ad_sampler
378
+ if hasattr(p, "_ad_orig"):
379
+ return p._ad_orig.sampler_name
380
+ return p.sampler_name
381
+
382
+ def get_scheduler(self, p, args: ADetailerArgs) -> dict[str, str]:
383
+ "webui >= 1.9.0"
384
+ if not args.ad_use_sampler:
385
+ return {"scheduler": getattr(p, "scheduler", "Automatic")}
386
+
387
+ if args.ad_scheduler == "Use same scheduler":
388
+ value = getattr(p, "scheduler", "Automatic")
389
+ else:
390
+ value = args.ad_scheduler
391
+ return {"scheduler": value}
392
+
393
+ def get_override_settings(self, p, args: ADetailerArgs) -> dict[str, Any]:
394
+ d = {}
395
+
396
+ if args.ad_use_clip_skip:
397
+ d["CLIP_stop_at_last_layers"] = args.ad_clip_skip
398
+
399
+ if (
400
+ args.ad_use_checkpoint
401
+ and args.ad_checkpoint
402
+ and args.ad_checkpoint not in ("None", "Use same checkpoint")
403
+ ):
404
+ d["sd_model_checkpoint"] = args.ad_checkpoint
405
+
406
+ if (
407
+ args.ad_use_vae
408
+ and args.ad_vae
409
+ and args.ad_vae not in ("None", "Use same VAE")
410
+ ):
411
+ d["sd_vae"] = args.ad_vae
412
+ return d
413
+
414
+ def get_initial_noise_multiplier(self, p, args: ADetailerArgs) -> float | None:
415
+ return args.ad_noise_multiplier if args.ad_use_noise_multiplier else None
416
+
417
+ @staticmethod
418
+ def infotext(p) -> str:
419
+ return create_infotext(
420
+ p, p.all_prompts, p.all_seeds, p.all_subseeds, None, 0, 0
421
+ )
422
+
423
+ def read_params_txt(self) -> str:
424
+ params_txt = Path(paths.data_path, PARAMS_TXT)
425
+ if params_txt.exists():
426
+ return params_txt.read_text(encoding="utf-8")
427
+ return ""
428
+
429
+ def write_params_txt(self, content: str) -> None:
430
+ params_txt = Path(paths.data_path, PARAMS_TXT)
431
+ if params_txt.exists() and content:
432
+ params_txt.write_text(content, encoding="utf-8")
433
+
434
+ @staticmethod
435
+ def script_args_copy(script_args):
436
+ type_: type[list] | type[tuple] = type(script_args)
437
+ result = []
438
+ for arg in script_args:
439
+ try:
440
+ a = copy(arg)
441
+ except TypeError:
442
+ a = arg
443
+ result.append(a)
444
+ return type_(result)
445
+
446
+ def script_filter(self, p, args: ADetailerArgs):
447
+ script_runner = copy(p.scripts)
448
+ script_args = self.script_args_copy(p.script_args)
449
+
450
+ ad_only_selected_scripts = opts.data.get("ad_only_selected_scripts", True)
451
+ if not ad_only_selected_scripts:
452
+ return script_runner, script_args
453
+
454
+ ad_script_names_string: str = opts.data.get("ad_script_names", SCRIPT_DEFAULT)
455
+ ad_script_names = ad_script_names_string.split(",") + BUILTIN_SCRIPT.split(",")
456
+ script_names_set = {
457
+ name
458
+ for script_name in ad_script_names
459
+ for name in (script_name, script_name.strip())
460
+ }
461
+
462
+ if args.ad_controlnet_model != "None":
463
+ script_names_set.add("controlnet")
464
+
465
+ filtered_alwayson = []
466
+ for script_object in script_runner.alwayson_scripts:
467
+ filepath = script_object.filename
468
+ filename = Path(filepath).stem
469
+ if filename in script_names_set:
470
+ filtered_alwayson.append(script_object)
471
+
472
+ script_runner.alwayson_scripts = filtered_alwayson
473
+ return script_runner, script_args
474
+
475
+ def disable_controlnet_units(
476
+ self, script_args: list[Any] | tuple[Any, ...]
477
+ ) -> None:
478
+ for obj in script_args:
479
+ if "controlnet" in obj.__class__.__name__.lower():
480
+ if hasattr(obj, "enabled"):
481
+ obj.enabled = False
482
+ if hasattr(obj, "input_mode"):
483
+ obj.input_mode = getattr(obj.input_mode, "SIMPLE", "simple")
484
+
485
+ elif isinstance(obj, dict) and "module" in obj:
486
+ obj["enabled"] = False
487
+
488
+ def get_i2i_p(self, p, args: ADetailerArgs, image):
489
+ seed, subseed = self.get_seed(p)
490
+ width, height = self.get_width_height(p, args)
491
+ steps = self.get_steps(p, args)
492
+ cfg_scale = self.get_cfg_scale(p, args)
493
+ initial_noise_multiplier = self.get_initial_noise_multiplier(p, args)
494
+ sampler_name = self.get_sampler(p, args)
495
+ override_settings = self.get_override_settings(p, args)
496
+
497
+ version_args = {}
498
+ if schedulers:
499
+ version_args.update(self.get_scheduler(p, args))
500
+
501
+ i2i = StableDiffusionProcessingImg2Img(
502
+ init_images=[image],
503
+ resize_mode=0,
504
+ denoising_strength=args.ad_denoising_strength,
505
+ mask=None,
506
+ mask_blur=args.ad_mask_blur,
507
+ inpainting_fill=1,
508
+ inpaint_full_res=args.ad_inpaint_only_masked,
509
+ inpaint_full_res_padding=args.ad_inpaint_only_masked_padding,
510
+ inpainting_mask_invert=0,
511
+ initial_noise_multiplier=initial_noise_multiplier,
512
+ sd_model=p.sd_model,
513
+ outpath_samples=p.outpath_samples,
514
+ outpath_grids=p.outpath_grids,
515
+ prompt="", # replace later
516
+ negative_prompt="",
517
+ styles=p.styles,
518
+ seed=seed,
519
+ subseed=subseed,
520
+ subseed_strength=p.subseed_strength,
521
+ seed_resize_from_h=p.seed_resize_from_h,
522
+ seed_resize_from_w=p.seed_resize_from_w,
523
+ sampler_name=sampler_name,
524
+ batch_size=1,
525
+ n_iter=1,
526
+ steps=steps,
527
+ cfg_scale=cfg_scale,
528
+ width=width,
529
+ height=height,
530
+ restore_faces=args.ad_restore_face,
531
+ tiling=p.tiling,
532
+ extra_generation_params=copy_extra_params(p.extra_generation_params),
533
+ do_not_save_samples=True,
534
+ do_not_save_grid=True,
535
+ override_settings=override_settings,
536
+ **version_args,
537
+ )
538
+
539
+ i2i.cached_c = [None, None]
540
+ i2i.cached_uc = [None, None]
541
+ i2i.scripts, i2i.script_args = self.script_filter(p, args)
542
+ i2i._ad_disabled = True
543
+ i2i._ad_inner = True
544
+
545
+ if args.ad_controlnet_model != "Passthrough" and controlnet_type != "forge":
546
+ self.disable_controlnet_units(i2i.script_args)
547
+
548
+ if args.ad_controlnet_model not in ["None", "Passthrough"]:
549
+ self.update_controlnet_args(i2i, args)
550
+ elif args.ad_controlnet_model == "None":
551
+ i2i.control_net_enabled = False
552
+
553
+ return i2i
554
+
555
+ def save_image(self, p, image, *, condition: str, suffix: str) -> None:
556
+ i = get_i(p)
557
+ if p.all_prompts:
558
+ i %= len(p.all_prompts)
559
+ save_prompt = p.all_prompts[i]
560
+ else:
561
+ save_prompt = p.prompt
562
+ seed, _ = self.get_seed(p)
563
+
564
+ if opts.data.get(condition, False):
565
+ images.save_image(
566
+ image=image,
567
+ path=p.outpath_samples,
568
+ basename="",
569
+ seed=seed,
570
+ prompt=save_prompt,
571
+ extension=opts.samples_format,
572
+ info=self.infotext(p),
573
+ p=p,
574
+ suffix=suffix,
575
+ )
576
+
577
+ def get_ad_model(self, name: str):
578
+ if name not in model_mapping:
579
+ msg = f"[-] ADetailer: Model {name!r} not found. Available models: {list(model_mapping.keys())}"
580
+ raise ValueError(msg)
581
+ return model_mapping[name]
582
+
583
+ def sort_bboxes(self, pred: PredictOutput) -> PredictOutput:
584
+ sortby = opts.data.get("ad_bbox_sortby", BBOX_SORTBY[0])
585
+ sortby_idx = BBOX_SORTBY.index(sortby)
586
+ return sort_bboxes(pred, sortby_idx)
587
+
588
+ def pred_preprocessing(self, p, pred: PredictOutput, args: ADetailerArgs):
589
+ pred = filter_by_ratio(
590
+ pred, low=args.ad_mask_min_ratio, high=args.ad_mask_max_ratio
591
+ )
592
+ pred = filter_k_largest(pred, k=args.ad_mask_k_largest)
593
+ pred = self.sort_bboxes(pred)
594
+ masks = mask_preprocess(
595
+ pred.masks,
596
+ kernel=args.ad_dilate_erode,
597
+ x_offset=args.ad_x_offset,
598
+ y_offset=args.ad_y_offset,
599
+ merge_invert=args.ad_mask_merge_invert,
600
+ )
601
+
602
+ if is_img2img_inpaint(p) and not is_inpaint_only_masked(p):
603
+ image_mask = self.get_image_mask(p)
604
+ masks = self.inpaint_mask_filter(image_mask, masks)
605
+ return masks
606
+
607
+ @staticmethod
608
+ def i2i_prompts_replace(
609
+ i2i, prompts: list[str], negative_prompts: list[str], j: int
610
+ ) -> None:
611
+ i1 = min(j, len(prompts) - 1)
612
+ i2 = min(j, len(negative_prompts) - 1)
613
+ prompt = prompts[i1]
614
+ negative_prompt = negative_prompts[i2]
615
+ i2i.prompt = prompt
616
+ i2i.negative_prompt = negative_prompt
617
+
618
+ @staticmethod
619
+ def compare_prompt(extra_params: dict[str, Any], processed, n: int = 0):
620
+ pt = "ADetailer prompt" + suffix(n)
621
+ if pt in extra_params and extra_params[pt] != processed.all_prompts[0]:
622
+ print(
623
+ f"[-] ADetailer: applied {ordinal(n + 1)} ad_prompt: {processed.all_prompts[0]!r}"
624
+ )
625
+
626
+ ng = "ADetailer negative prompt" + suffix(n)
627
+ if ng in extra_params and extra_params[ng] != processed.all_negative_prompts[0]:
628
+ print(
629
+ f"[-] ADetailer: applied {ordinal(n + 1)} ad_negative_prompt: {processed.all_negative_prompts[0]!r}"
630
+ )
631
+
632
+ @staticmethod
633
+ def get_i2i_init_image(p, pp):
634
+ if is_skip_img2img(p):
635
+ return p.init_images[0]
636
+ return pp.image
637
+
638
+ @staticmethod
639
+ def get_each_tab_seed(seed: int, i: int):
640
+ use_same_seed = shared.opts.data.get("ad_same_seed_for_each_tab", False)
641
+ return seed if use_same_seed else seed + i
642
+
643
+ @staticmethod
644
+ def inpaint_mask_filter(
645
+ img2img_mask: Image.Image, ad_mask: list[Image.Image]
646
+ ) -> list[Image.Image]:
647
+ if ad_mask and img2img_mask.size != ad_mask[0].size:
648
+ img2img_mask = img2img_mask.resize(ad_mask[0].size, resample=images.LANCZOS)
649
+ return [mask for mask in ad_mask if has_intersection(img2img_mask, mask)]
650
+
651
+ @staticmethod
652
+ def get_image_mask(p) -> Image.Image:
653
+ mask = p.image_mask
654
+ if getattr(p, "inpainting_mask_invert", False):
655
+ mask = ImageChops.invert(mask)
656
+ mask = create_binary_mask(mask)
657
+
658
+ if is_skip_img2img(p):
659
+ if hasattr(p, "init_images") and p.init_images:
660
+ width, height = p.init_images[0].size
661
+ else:
662
+ msg = "[-] ADetailer: no init_images."
663
+ raise RuntimeError(msg)
664
+ else:
665
+ width, height = p.width, p.height
666
+ return images.resize_image(p.resize_mode, mask, width, height)
667
+
668
+ @rich_traceback
669
+ def process(self, p, *args_):
670
+ if getattr(p, "_ad_disabled", False):
671
+ return
672
+
673
+ if is_img2img_inpaint(p) and is_all_black(self.get_image_mask(p)):
674
+ p._ad_disabled = True
675
+ msg = (
676
+ "[-] ADetailer: img2img inpainting with no mask -- adetailer disabled."
677
+ )
678
+ print(msg)
679
+ return
680
+
681
+ if not self.is_ad_enabled(*args_):
682
+ p._ad_disabled = True
683
+ return
684
+
685
+ self.set_skip_img2img(p, *args_)
686
+ if getattr(p, "_ad_disabled", False):
687
+ # case when img2img inpainting with skip img2img
688
+ return
689
+
690
+ arg_list = self.get_args(p, *args_)
691
+
692
+ if hasattr(p, "_ad_xyz_prompt_sr"):
693
+ replaced_positive_prompt, replaced_negative_prompt = self.get_prompt(
694
+ p, arg_list[0]
695
+ )
696
+ arg_list[0].ad_prompt = replaced_positive_prompt[0]
697
+ arg_list[0].ad_negative_prompt = replaced_negative_prompt[0]
698
+
699
+ extra_params = self.extra_params(arg_list)
700
+ p.extra_generation_params.update(extra_params)
701
+
702
+ def _postprocess_image_inner(
703
+ self, p, pp, args: ADetailerArgs, *, n: int = 0
704
+ ) -> bool:
705
+ """
706
+ Returns
707
+ -------
708
+ bool
709
+
710
+ `True` if image was processed, `False` otherwise.
711
+ """
712
+ if state.interrupted or state.skipped:
713
+ return False
714
+
715
+ i = get_i(p)
716
+
717
+ i2i = self.get_i2i_p(p, args, pp.image)
718
+ seed, subseed = self.get_seed(p)
719
+ ad_prompts, ad_negatives = self.get_prompt(p, args)
720
+
721
+ is_mediapipe = args.is_mediapipe()
722
+
723
+ kwargs = {}
724
+ if is_mediapipe:
725
+ predictor = mediapipe_predict
726
+ ad_model = args.ad_model
727
+ else:
728
+ predictor = ultralytics_predict
729
+ ad_model = self.get_ad_model(args.ad_model)
730
+ kwargs["device"] = self.ultralytics_device
731
+ kwargs["classes"] = args.ad_model_classes
732
+
733
+ with change_torch_load():
734
+ pred = predictor(ad_model, pp.image, args.ad_confidence, **kwargs)
735
+
736
+ if pred.preview is None:
737
+ print(
738
+ f"[-] ADetailer: nothing detected on image {i + 1} with {ordinal(n + 1)} settings."
739
+ )
740
+ return False
741
+
742
+ masks = self.pred_preprocessing(p, pred, args)
743
+ shared.state.assign_current_image(pred.preview)
744
+
745
+ self.save_image(
746
+ p,
747
+ pred.preview,
748
+ condition="ad_save_previews",
749
+ suffix="-ad-preview" + suffix(n, "-"),
750
+ )
751
+
752
+ steps = len(masks)
753
+ processed = None
754
+ state.job_count += steps
755
+
756
+ if is_mediapipe:
757
+ print(f"mediapipe: {steps} detected.")
758
+
759
+ p2 = copy(i2i)
760
+ for j in range(steps):
761
+ p2.image_mask = masks[j]
762
+ p2.init_images[0] = ensure_pil_image(p2.init_images[0], "RGB")
763
+ self.i2i_prompts_replace(p2, ad_prompts, ad_negatives, j)
764
+
765
+ if re.match(r"^\s*\[SKIP\]\s*$", p2.prompt):
766
+ continue
767
+
768
+ p2.seed = self.get_each_tab_seed(seed, j)
769
+ p2.subseed = self.get_each_tab_seed(subseed, j)
770
+
771
+ p2.cached_c = [None, None]
772
+ p2.cached_uc = [None, None]
773
+ try:
774
+ processed = process_images(p2)
775
+ except NansException as e:
776
+ msg = f"[-] ADetailer: 'NansException' occurred with {ordinal(n + 1)} settings.\n{e}"
777
+ print(msg, file=sys.stderr)
778
+ continue
779
+ finally:
780
+ p2.close()
781
+
782
+ self.compare_prompt(p.extra_generation_params, processed, n=n)
783
+ p2 = copy(i2i)
784
+ p2.init_images = [processed.images[0]]
785
+
786
+ if processed is not None:
787
+ pp.image = processed.images[0]
788
+ return True
789
+
790
+ return False
791
+
792
+ @rich_traceback
793
+ def postprocess_image(self, p, pp, *args_):
794
+ if getattr(p, "_ad_disabled", False) or not self.is_ad_enabled(*args_):
795
+ return
796
+
797
+ pp.image = self.get_i2i_init_image(p, pp)
798
+ pp.image = ensure_pil_image(pp.image, "RGB")
799
+ init_image = copy(pp.image)
800
+ arg_list = self.get_args(p, *args_)
801
+ params_txt_content = self.read_params_txt()
802
+
803
+ if need_call_postprocess(p):
804
+ dummy = Processed(p, [], p.seed, "")
805
+ with preserve_prompts(p):
806
+ p.scripts.postprocess(copy(p), dummy)
807
+
808
+ is_processed = False
809
+ with CNHijackRestore(), pause_total_tqdm(), cn_allow_script_control():
810
+ for n, args in enumerate(arg_list):
811
+ if args.need_skip():
812
+ continue
813
+ is_processed |= self._postprocess_image_inner(p, pp, args, n=n)
814
+
815
+ if is_processed and not is_skip_img2img(p):
816
+ self.save_image(
817
+ p, init_image, condition="ad_save_images_before", suffix="-ad-before"
818
+ )
819
+
820
+ if need_call_process(p):
821
+ with preserve_prompts(p):
822
+ copy_p = copy(p)
823
+ p.scripts.before_process(copy_p)
824
+ p.scripts.process(copy_p)
825
+
826
+ self.write_params_txt(params_txt_content)
827
+
828
+
829
+ def on_after_component(component, **_kwargs):
830
+ global txt2img_submit_button, img2img_submit_button
831
+ if getattr(component, "elem_id", None) == "txt2img_generate":
832
+ txt2img_submit_button = component
833
+ return
834
+
835
+ if getattr(component, "elem_id", None) == "img2img_generate":
836
+ img2img_submit_button = component
837
+
838
+
839
+ def on_ui_settings():
840
+ section = ("ADetailer", ADETAILER)
841
+ shared.opts.add_option(
842
+ "ad_max_models",
843
+ shared.OptionInfo(
844
+ default=4,
845
+ label="Max tabs",
846
+ component=gr.Slider,
847
+ component_args={"minimum": 1, "maximum": 15, "step": 1},
848
+ section=section,
849
+ ).needs_reload_ui(),
850
+ )
851
+
852
+ shared.opts.add_option(
853
+ "ad_extra_models_dir",
854
+ shared.OptionInfo(
855
+ default="",
856
+ label="Extra paths to scan adetailer models separated by vertical bars(|)",
857
+ component=gr.Textbox,
858
+ section=section,
859
+ )
860
+ .info("eg. path\\to\\models|C:\\path\\to\\models|another/path/to/models")
861
+ .needs_reload_ui(),
862
+ )
863
+
864
+ shared.opts.add_option(
865
+ "ad_save_previews",
866
+ shared.OptionInfo(False, "Save mask previews", section=section),
867
+ )
868
+
869
+ shared.opts.add_option(
870
+ "ad_save_images_before",
871
+ shared.OptionInfo(False, "Save images before ADetailer", section=section),
872
+ )
873
+
874
+ shared.opts.add_option(
875
+ "ad_only_selected_scripts",
876
+ shared.OptionInfo(
877
+ True, "Apply only selected scripts to ADetailer", section=section
878
+ ),
879
+ )
880
+
881
+ textbox_args = {
882
+ "placeholder": "comma-separated list of script names",
883
+ "interactive": True,
884
+ }
885
+
886
+ shared.opts.add_option(
887
+ "ad_script_names",
888
+ shared.OptionInfo(
889
+ default=SCRIPT_DEFAULT,
890
+ label="Script names to apply to ADetailer (separated by comma)",
891
+ component=gr.Textbox,
892
+ component_args=textbox_args,
893
+ section=section,
894
+ ),
895
+ )
896
+
897
+ shared.opts.add_option(
898
+ "ad_bbox_sortby",
899
+ shared.OptionInfo(
900
+ default="None",
901
+ label="Sort bounding boxes by",
902
+ component=gr.Radio,
903
+ component_args={"choices": BBOX_SORTBY},
904
+ section=section,
905
+ ),
906
+ )
907
+
908
+ shared.opts.add_option(
909
+ "ad_same_seed_for_each_tab",
910
+ shared.OptionInfo(
911
+ False, "Use same seed for each tab in adetailer", section=section
912
+ ),
913
+ )
914
+
915
+
916
+ # xyz_grid
917
+
918
+
919
+ class PromptSR(NamedTuple):
920
+ s: str
921
+ r: str
922
+
923
+
924
+ def set_value(p, x: Any, xs: Any, *, field: str):
925
+ if not hasattr(p, "_ad_xyz"):
926
+ p._ad_xyz = {}
927
+ p._ad_xyz[field] = x
928
+
929
+
930
+ def search_and_replace_prompt(p, x: Any, xs: Any, replace_in_main_prompt: bool):
931
+ if replace_in_main_prompt:
932
+ p.prompt = p.prompt.replace(xs[0], x)
933
+ p.negative_prompt = p.negative_prompt.replace(xs[0], x)
934
+
935
+ if not hasattr(p, "_ad_xyz_prompt_sr"):
936
+ p._ad_xyz_prompt_sr = []
937
+ p._ad_xyz_prompt_sr.append(PromptSR(s=xs[0], r=x))
938
+
939
+
940
+ def make_axis_on_xyz_grid():
941
+ xyz_grid = None
942
+ for script in scripts.scripts_data:
943
+ if script.script_class.__module__ == "xyz_grid.py":
944
+ xyz_grid = script.module
945
+ break
946
+
947
+ if xyz_grid is None:
948
+ return
949
+
950
+ model_list = ["None", *model_mapping.keys()]
951
+ samplers = [sampler.name for sampler in all_samplers]
952
+
953
+ axis = [
954
+ xyz_grid.AxisOption(
955
+ "[ADetailer] ADetailer model 1st",
956
+ str,
957
+ partial(set_value, field="ad_model"),
958
+ choices=lambda: model_list,
959
+ ),
960
+ xyz_grid.AxisOption(
961
+ "[ADetailer] ADetailer prompt 1st",
962
+ str,
963
+ partial(set_value, field="ad_prompt"),
964
+ ),
965
+ xyz_grid.AxisOption(
966
+ "[ADetailer] ADetailer negative prompt 1st",
967
+ str,
968
+ partial(set_value, field="ad_negative_prompt"),
969
+ ),
970
+ xyz_grid.AxisOption(
971
+ "[ADetailer] Prompt S/R (AD 1st)",
972
+ str,
973
+ partial(search_and_replace_prompt, replace_in_main_prompt=False),
974
+ ),
975
+ xyz_grid.AxisOption(
976
+ "[ADetailer] Prompt S/R (AD 1st and main prompt)",
977
+ str,
978
+ partial(search_and_replace_prompt, replace_in_main_prompt=True),
979
+ ),
980
+ xyz_grid.AxisOption(
981
+ "[ADetailer] Mask erosion / dilation 1st",
982
+ int,
983
+ partial(set_value, field="ad_dilate_erode"),
984
+ ),
985
+ xyz_grid.AxisOption(
986
+ "[ADetailer] Inpaint denoising strength 1st",
987
+ float,
988
+ partial(set_value, field="ad_denoising_strength"),
989
+ ),
990
+ xyz_grid.AxisOption(
991
+ "[ADetailer] Inpaint only masked 1st",
992
+ str,
993
+ partial(set_value, field="ad_inpaint_only_masked"),
994
+ choices=lambda: ["True", "False"],
995
+ ),
996
+ xyz_grid.AxisOption(
997
+ "[ADetailer] Inpaint only masked padding 1st",
998
+ int,
999
+ partial(set_value, field="ad_inpaint_only_masked_padding"),
1000
+ ),
1001
+ xyz_grid.AxisOption(
1002
+ "[ADetailer] ADetailer sampler 1st",
1003
+ str,
1004
+ partial(set_value, field="ad_sampler"),
1005
+ choices=lambda: samplers,
1006
+ ),
1007
+ xyz_grid.AxisOption(
1008
+ "[ADetailer] ControlNet model 1st",
1009
+ str,
1010
+ partial(set_value, field="ad_controlnet_model"),
1011
+ choices=lambda: ["None", "Passthrough", *get_cn_models()],
1012
+ ),
1013
+ ]
1014
+
1015
+ if not any(x.label.startswith("[ADetailer]") for x in xyz_grid.axis_options):
1016
+ xyz_grid.axis_options.extend(axis)
1017
+
1018
+
1019
+ def on_before_ui():
1020
+ try:
1021
+ make_axis_on_xyz_grid()
1022
+ except Exception:
1023
+ error = traceback.format_exc()
1024
+ print(
1025
+ f"[-] ADetailer: xyz_grid error:\n{error}",
1026
+ file=sys.stderr,
1027
+ )
1028
+
1029
+
1030
+ # api
1031
+
1032
+
1033
+ def add_api_endpoints(_: gr.Blocks, app: FastAPI):
1034
+ @app.get("/adetailer/v1/version")
1035
+ async def version():
1036
+ return {"version": __version__}
1037
+
1038
+ @app.get("/adetailer/v1/schema")
1039
+ async def schema():
1040
+ if hasattr(ADetailerArgs, "model_json_schema"):
1041
+ return ADetailerArgs.model_json_schema()
1042
+ return ADetailerArgs.schema()
1043
+
1044
+ @app.get("/adetailer/v1/ad_model")
1045
+ async def ad_model():
1046
+ return {"ad_model": list(model_mapping)}
1047
+
1048
+
1049
+ script_callbacks.on_ui_settings(on_ui_settings)
1050
+ script_callbacks.on_after_component(on_after_component)
1051
+ script_callbacks.on_app_started(add_api_endpoints)
1052
+ script_callbacks.on_before_ui(on_before_ui)
adetailer-dev/adetailer-dev/tests/__init__.py ADDED
File without changes
adetailer-dev/adetailer-dev/tests/conftest.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ import requests
3
+ from PIL import Image
4
+
5
+
6
+ def get_image(url: str) -> Image.Image:
7
+ resp = requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"})
8
+ return Image.open(resp.raw)
9
+
10
+
11
+ @pytest.fixture(scope="session")
12
+ def sample_image():
13
+ return get_image("https://i.imgur.com/E5OVXvn.png")
14
+
15
+
16
+ @pytest.fixture(scope="session")
17
+ def sample_image2():
18
+ return get_image("https://i.imgur.com/px5UT7T.png")
adetailer-dev/adetailer-dev/tests/test_args.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from adetailer.args import ALL_ARGS, ADetailerArgs
6
+
7
+
8
+ def test_all_args() -> None:
9
+ args = ADetailerArgs()
10
+ for attr, _ in ALL_ARGS:
11
+ assert hasattr(args, attr), attr
12
+
13
+ for attr, _ in args:
14
+ if attr == "is_api":
15
+ continue
16
+ assert attr in ALL_ARGS.attrs, attr
17
+
18
+
19
+ @pytest.mark.parametrize(
20
+ ("ad_model", "expect"),
21
+ [("mediapipe_face_full", True), ("face_yolov8n.pt", False)],
22
+ )
23
+ def test_is_mediapipe(ad_model: str, expect: bool) -> None:
24
+ args = ADetailerArgs(ad_model=ad_model)
25
+ assert args.is_mediapipe() is expect
26
+
27
+
28
+ @pytest.mark.parametrize(
29
+ ("ad_model", "expect"),
30
+ [("mediapipe_face_full", False), ("face_yolov8n.pt", False), ("None", True)],
31
+ )
32
+ def test_need_skip(ad_model: str, expect: bool) -> None:
33
+ args = ADetailerArgs(ad_model=ad_model)
34
+ assert args.need_skip() is expect
35
+
36
+
37
+ @pytest.mark.parametrize(
38
+ ("ad_model", "ad_tab_enable", "expect"),
39
+ [
40
+ ("face_yolov8n.pt", False, True),
41
+ ("mediapipe_face_full", False, True),
42
+ ("None", True, True),
43
+ ("ace_yolov8s.pt", True, False),
44
+ ],
45
+ )
46
+ def test_need_skip_tab_enable(ad_model: str, ad_tab_enable: bool, expect: bool) -> None:
47
+ args = ADetailerArgs(ad_model=ad_model, ad_tab_enable=ad_tab_enable)
48
+ assert args.need_skip() is expect
adetailer-dev/adetailer-dev/tests/test_common.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from PIL import Image, ImageDraw
3
+
4
+ from adetailer.common import create_bbox_from_mask, create_mask_from_bbox
5
+
6
+
7
+ def test_create_mask_from_bbox():
8
+ img = Image.new("L", (10, 10), color="black")
9
+ bbox = [[1.0, 1.0, 2.0, 2.0], [7.0, 7.0, 8.0, 8.0]]
10
+ masks = create_mask_from_bbox(bbox, img.size)
11
+ expect1 = np.array(
12
+ [
13
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
14
+ [0, 255, 255, 0, 0, 0, 0, 0, 0, 0],
15
+ [0, 255, 255, 0, 0, 0, 0, 0, 0, 0],
16
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
17
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
18
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
19
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
20
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
21
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
22
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
23
+ ],
24
+ dtype=np.uint8,
25
+ )
26
+ expect2 = np.array(
27
+ [
28
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
29
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
30
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
31
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
32
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
33
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
34
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
35
+ [0, 0, 0, 0, 0, 0, 0, 255, 255, 0],
36
+ [0, 0, 0, 0, 0, 0, 0, 255, 255, 0],
37
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
38
+ ],
39
+ dtype=np.uint8,
40
+ )
41
+ assert len(masks) == len(bbox)
42
+ arr1 = np.array(masks[0])
43
+ arr2 = np.array(masks[1])
44
+ assert arr1.shape == expect1.shape
45
+ assert arr2.shape == expect2.shape
46
+ assert arr1.shape == (10, 10)
47
+ assert arr1.dtype == expect1.dtype
48
+ assert arr2.dtype == expect2.dtype
49
+ assert np.array_equal(arr1, expect1)
50
+ assert np.array_equal(arr2, expect2)
51
+
52
+ # The function correctly receives a list of masks and the shape of the image.
53
+
54
+
55
+ def test_create_bbox_from_mask():
56
+ mask = Image.new("L", (10, 10), color="black")
57
+ draw = ImageDraw.Draw(mask)
58
+ draw.rectangle((2, 2, 5, 5), fill="white")
59
+
60
+ result = create_bbox_from_mask([mask], (10, 10))
61
+
62
+ assert isinstance(result, list)
63
+ assert len(result) == 1
64
+ assert all(isinstance(bbox, list) for bbox in result)
65
+ assert all(len(bbox) == 4 for bbox in result)
66
+ assert result[0] == [2, 2, 6, 6]
67
+
68
+ result = create_bbox_from_mask([mask], (256, 256))
69
+ assert result[0] == [38, 38, 166, 166]
adetailer-dev/adetailer-dev/tests/test_mask.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import pytest
4
+ from PIL import Image, ImageDraw
5
+
6
+ from adetailer.mask import (
7
+ bbox_area,
8
+ dilate_erode,
9
+ has_intersection,
10
+ is_all_black,
11
+ mask_invert,
12
+ mask_merge,
13
+ offset,
14
+ )
15
+
16
+
17
+ def test_dilate_positive_value():
18
+ img = Image.new("L", (10, 10), color="black")
19
+ draw = ImageDraw.Draw(img)
20
+ draw.rectangle((3, 3, 5, 5), fill="white")
21
+ value = 3
22
+
23
+ result = dilate_erode(img, value)
24
+
25
+ assert isinstance(result, Image.Image)
26
+ assert result.size == (10, 10)
27
+
28
+ expect = np.array(
29
+ [
30
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
31
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
32
+ [0, 0, 255, 255, 255, 255, 255, 0, 0, 0],
33
+ [0, 0, 255, 255, 255, 255, 255, 0, 0, 0],
34
+ [0, 0, 255, 255, 255, 255, 255, 0, 0, 0],
35
+ [0, 0, 255, 255, 255, 255, 255, 0, 0, 0],
36
+ [0, 0, 255, 255, 255, 255, 255, 0, 0, 0],
37
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
38
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
39
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
40
+ ],
41
+ dtype=np.uint8,
42
+ )
43
+ assert np.array_equal(np.array(result), expect)
44
+
45
+
46
+ def test_offset():
47
+ img = Image.new("L", (10, 10), color="black")
48
+ draw = ImageDraw.Draw(img)
49
+ draw.rectangle((4, 4, 5, 5), fill="white")
50
+
51
+ result = offset(img, x=1, y=2)
52
+
53
+ assert isinstance(result, Image.Image)
54
+ assert result.size == (10, 10)
55
+
56
+ expect = np.array(
57
+ [
58
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
59
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
60
+ [0, 0, 0, 0, 0, 255, 255, 0, 0, 0],
61
+ [0, 0, 0, 0, 0, 255, 255, 0, 0, 0],
62
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
63
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
64
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
65
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
66
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
67
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
68
+ ],
69
+ dtype=np.uint8,
70
+ )
71
+ assert np.array_equal(np.array(result), expect)
72
+
73
+
74
+ class TestIsAllBlack:
75
+ def test_is_all_black_1(self):
76
+ img = Image.new("L", (10, 10), color="black")
77
+ assert is_all_black(img)
78
+
79
+ draw = ImageDraw.Draw(img)
80
+ draw.rectangle((4, 4, 5, 5), fill="white")
81
+ assert not is_all_black(img)
82
+
83
+ def test_is_all_black_2(self):
84
+ img = np.zeros((10, 10), dtype=np.uint8)
85
+ assert is_all_black(img)
86
+
87
+ img[4:6, 4:6] = 255
88
+ assert not is_all_black(img)
89
+
90
+ def test_is_all_black_rgb_image_pil(self):
91
+ img = Image.new("RGB", (10, 10), color="red")
92
+ assert not is_all_black(img)
93
+
94
+ img = Image.new("RGBA", (10, 10), color="red")
95
+ assert not is_all_black(img)
96
+
97
+ def test_is_all_black_rgb_image_numpy(self):
98
+ img = np.full((10, 10, 4), 127, dtype=np.uint8)
99
+ with pytest.raises(cv2.error):
100
+ is_all_black(img)
101
+
102
+ img = np.full((4, 10, 10), 0.5, dtype=np.float32)
103
+ with pytest.raises(cv2.error):
104
+ is_all_black(img)
105
+
106
+
107
+ class TestHasIntersection:
108
+ def test_has_intersection_1(self):
109
+ arr1 = np.array(
110
+ [
111
+ [0, 0, 0, 0],
112
+ [0, 0, 0, 0],
113
+ [0, 0, 0, 0],
114
+ [0, 0, 0, 0],
115
+ ],
116
+ dtype=np.uint8,
117
+ )
118
+ arr2 = arr1.copy()
119
+ assert not has_intersection(arr1, arr2)
120
+
121
+ def test_has_intersection_2(self):
122
+ arr1 = np.array(
123
+ [
124
+ [0, 0, 0, 0],
125
+ [0, 255, 255, 0],
126
+ [0, 255, 255, 0],
127
+ [0, 0, 0, 0],
128
+ ],
129
+ dtype=np.uint8,
130
+ )
131
+ arr2 = np.array(
132
+ [
133
+ [0, 0, 0, 0],
134
+ [0, 0, 0, 0],
135
+ [0, 0, 255, 255],
136
+ [0, 0, 255, 255],
137
+ ],
138
+ dtype=np.uint8,
139
+ )
140
+ assert has_intersection(arr1, arr2)
141
+
142
+ arr3 = np.array(
143
+ [
144
+ [255, 0, 0, 0],
145
+ [0, 0, 0, 0],
146
+ [0, 0, 0, 255],
147
+ [0, 0, 255, 255],
148
+ ],
149
+ dtype=np.uint8,
150
+ )
151
+ assert not has_intersection(arr1, arr3)
152
+
153
+ def test_has_intersection_3(self):
154
+ img1 = Image.new("L", (10, 10), color="black")
155
+ draw1 = ImageDraw.Draw(img1)
156
+ draw1.rectangle((3, 3, 5, 5), fill="white")
157
+ img2 = Image.new("L", (10, 10), color="black")
158
+ draw2 = ImageDraw.Draw(img2)
159
+ draw2.rectangle((6, 6, 8, 8), fill="white")
160
+ assert not has_intersection(img1, img2)
161
+
162
+ img3 = Image.new("L", (10, 10), color="black")
163
+ draw3 = ImageDraw.Draw(img3)
164
+ draw3.rectangle((2, 2, 8, 8), fill="white")
165
+ assert has_intersection(img1, img3)
166
+
167
+ def test_has_intersection_4(self):
168
+ img1 = Image.new("RGB", (10, 10), color="black")
169
+ draw1 = ImageDraw.Draw(img1)
170
+ draw1.rectangle((3, 3, 5, 5), fill="white")
171
+ img2 = Image.new("RGBA", (10, 10), color="black")
172
+ draw2 = ImageDraw.Draw(img2)
173
+ draw2.rectangle((2, 2, 8, 8), fill="white")
174
+ assert has_intersection(img1, img2)
175
+
176
+ def test_has_intersection_5(self):
177
+ img1 = Image.new("RGB", (10, 10), color="black")
178
+ draw1 = ImageDraw.Draw(img1)
179
+ draw1.rectangle((4, 4, 5, 5), fill="white")
180
+ img2 = np.full((10, 10, 4), 255, dtype=np.uint8)
181
+ assert has_intersection(img1, img2)
182
+
183
+
184
+ def test_bbox_area():
185
+ bbox = [0.0, 0.0, 10.0, 10.0]
186
+ assert bbox_area(bbox) == 100
187
+
188
+
189
+ class TestMaskMerge:
190
+ def test_mask_merge(self):
191
+ img1 = Image.new("L", (10, 10), color="black")
192
+ draw1 = ImageDraw.Draw(img1)
193
+ draw1.rectangle((3, 3, 5, 5), fill="white")
194
+
195
+ img2 = Image.new("L", (10, 10), color="black")
196
+ draw2 = ImageDraw.Draw(img2)
197
+ draw2.rectangle((6, 6, 8, 8), fill="white")
198
+
199
+ merged = mask_merge([img1, img2])
200
+ assert len(merged) == 1
201
+
202
+ expect = Image.new("L", (10, 10), color="black")
203
+ draw3 = ImageDraw.Draw(expect)
204
+ draw3.rectangle((3, 3, 5, 5), fill="white")
205
+ draw3.rectangle((6, 6, 8, 8), fill="white")
206
+
207
+ assert np.array_equal(np.array(merged[0]), np.array(expect))
208
+
209
+ def test_merge_mask_different_size(self):
210
+ img1 = Image.new("L", (10, 10), color="black")
211
+ draw1 = ImageDraw.Draw(img1)
212
+ draw1.rectangle((3, 3, 5, 5), fill="white")
213
+
214
+ img2 = Image.new("L", (20, 20), color="black")
215
+ draw2 = ImageDraw.Draw(img2)
216
+ draw2.rectangle((6, 6, 8, 8), fill="white")
217
+
218
+ with pytest.raises(
219
+ cv2.error, match="-209:Sizes of input arguments do not match"
220
+ ):
221
+ mask_merge([img1, img2])
222
+
223
+
224
+ def test_mask_invert():
225
+ img = Image.new("L", (10, 10), color="black")
226
+ draw = ImageDraw.Draw(img)
227
+ draw.rectangle((3, 3, 5, 5), fill="white")
228
+
229
+ inverted = mask_invert([img])
230
+ assert len(inverted) == 1
231
+
232
+ expect = Image.new("L", (10, 10), color="white")
233
+ draw = ImageDraw.Draw(expect)
234
+ draw.rectangle((3, 3, 5, 5), fill="black")
235
+
236
+ assert np.array_equal(np.array(inverted[0]), np.array(expect))
adetailer-dev/adetailer-dev/tests/test_mediapipe.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from PIL import Image
3
+
4
+ from adetailer.mediapipe import mediapipe_predict
5
+
6
+
7
+ @pytest.mark.parametrize(
8
+ "model_name",
9
+ [
10
+ "mediapipe_face_short",
11
+ "mediapipe_face_full",
12
+ "mediapipe_face_mesh",
13
+ "mediapipe_face_mesh_eyes_only",
14
+ ],
15
+ )
16
+ def test_mediapipe(sample_image2: Image.Image, model_name: str):
17
+ result = mediapipe_predict(model_name, sample_image2)
18
+ assert result.preview is not None
adetailer-dev/adetailer-dev/tests/test_ultralytics.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from huggingface_hub import hf_hub_download
3
+ from PIL import Image
4
+
5
+ from adetailer.ultralytics import ultralytics_predict
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ "model_name",
10
+ [
11
+ "face_yolov8n.pt",
12
+ "face_yolov8n_v2.pt",
13
+ "face_yolov8s.pt",
14
+ "face_yolov9c.pt",
15
+ "hand_yolov8n.pt",
16
+ "hand_yolov8s.pt",
17
+ "hand_yolov9c.pt",
18
+ "person_yolov8n-seg.pt",
19
+ "person_yolov8s-seg.pt",
20
+ "person_yolov8m-seg.pt",
21
+ "deepfashion2_yolov8s-seg.pt",
22
+ ],
23
+ )
24
+ def test_ultralytics_hf_models(sample_image: Image.Image, model_name: str):
25
+ model_path = hf_hub_download("Bingsu/adetailer", model_name)
26
+ result = ultralytics_predict(model_path, sample_image)
27
+ assert result.preview is not None
28
+
29
+
30
+ def test_yolo_world_default(sample_image: Image.Image):
31
+ model_path = hf_hub_download("Bingsu/yolo-world-mirror", "yolov8x-worldv2.pt")
32
+ result = ultralytics_predict(model_path, sample_image)
33
+ assert result.preview is not None
34
+
35
+
36
+ @pytest.mark.parametrize(
37
+ "klass",
38
+ [
39
+ "person",
40
+ "bird",
41
+ "yellow bird",
42
+ "person,glasses,headphone",
43
+ "person,bird",
44
+ "glasses,yellow bird",
45
+ ],
46
+ )
47
+ def test_yolo_world(sample_image2: Image.Image, klass: str):
48
+ model_path = hf_hub_download("Bingsu/yolo-world-mirror", "yolov8x-worldv2.pt")
49
+ result = ultralytics_predict(model_path, sample_image2, classes=klass)
50
+ assert result.preview is not None