Merge branch 'master' into gradio-theme-support
@@ -1,100 +1,29 @@
|
||||
name: Bug Report
|
||||
description: You think somethings is broken in the UI
|
||||
title: "[Bug]: "
|
||||
labels: ["bug-report"]
|
||||
name: Issue Report
|
||||
description: Something is broken
|
||||
title: "[Issue]: "
|
||||
labels: ["issue"]
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered, and that it hasn't been fixed in a recent build/commit.
|
||||
options:
|
||||
- label: I have searched the existing issues and checked the recent builds/commits
|
||||
required: true
|
||||
label: Issue Description
|
||||
description: Tell us what happened in a very clear and simple way
|
||||
value: Please fill this form with as much information as possible
|
||||
- type: textarea
|
||||
id: platform
|
||||
attributes:
|
||||
label: Version Platform Description
|
||||
description: Describe your platform (program version, OS, browser)
|
||||
value:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
*Please fill this form with as much information as possible, don't forget to fill "What OS..." and "What browsers" and *provide screenshots if possible**
|
||||
- type: textarea
|
||||
id: what-did
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Tell us what happened in a very clear and simple way
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce the problem
|
||||
description: Please provide us with precise step by step information on how to reproduce the bug
|
||||
value: |
|
||||
1. Go to ....
|
||||
2. Press ....
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-should
|
||||
attributes:
|
||||
label: What should have happened?
|
||||
description: Tell what you think the normal behavior should be
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: commit
|
||||
attributes:
|
||||
label: Commit where the problem happens
|
||||
description: Which commit are you running ? (Do not write *Latest version/repo/commit*, as this means nothing and will have changed by the time we read your issue. Rather, copy the **Commit** link at the bottom of the UI, or from the cmd/terminal if you can't launch it.)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: platforms
|
||||
attributes:
|
||||
label: What platforms do you use to access the UI ?
|
||||
multiple: true
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- MacOS
|
||||
- iOS
|
||||
- Android
|
||||
- Other/Cloud
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers do you use to access the UI ?
|
||||
multiple: true
|
||||
options:
|
||||
- Mozilla Firefox
|
||||
- Google Chrome
|
||||
- Brave
|
||||
- Apple Safari
|
||||
- Microsoft Edge
|
||||
- type: textarea
|
||||
id: cmdargs
|
||||
attributes:
|
||||
label: Command Line Arguments
|
||||
description: Are you using any launching parameters/command line arguments (modified webui-user .bat/.sh) ? If yes, please write them below. Write "No" otherwise.
|
||||
render: Shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: extensions
|
||||
attributes:
|
||||
label: List of extensions
|
||||
description: Are you using any extensions other than built-ins? If yes, provide a list, you can copy it at "Extensions" tab. Write "No" otherwise.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Console logs
|
||||
description: Please provide **full** cmd/terminal logs from the moment you started UI to the end of it, after your bug happened. If it's very long, provide a link to pastebin or similar service.
|
||||
render: Shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: misc
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Please provide us with any relevant additional info or context.
|
||||
Any issues without version information will be closed
|
||||
Look at console log and copy the version string from there
|
||||
For example: `Version: f256fb8b Fri Apr 14 17:41:30 2023 -0400`
|
||||
|
||||
Additionally provide any relevant platorm information (OS, browser, versions)
|
||||
|
||||
If possible update to latest version before reporting the issue as older versions cannot be properly supported
|
||||
And search existing **issues** and **discussions** before creating a new one
|
||||
|
||||
@@ -4,37 +4,15 @@ title: "[Feature Request]: "
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the feature you want, and that it's not implemented in a recent build/commit.
|
||||
options:
|
||||
- label: I have searched the existing issues and checked the recent builds/commits
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
*Please fill this form with as much information as possible, provide screenshots and/or illustrations of the feature if possible*
|
||||
- type: textarea
|
||||
id: feature
|
||||
id: description
|
||||
attributes:
|
||||
label: What would your feature do ?
|
||||
description: Tell us about your feature in a very clear and simple way, and what problem it would solve
|
||||
validations:
|
||||
required: true
|
||||
label: Feature description
|
||||
description: Describe the feature in a clear and simple way
|
||||
value:
|
||||
- type: textarea
|
||||
id: workflow
|
||||
id: platform
|
||||
attributes:
|
||||
label: Proposed workflow
|
||||
description: Please provide us with step by step information on how you'd like the feature to be accessed and used
|
||||
value: |
|
||||
1. Go to ....
|
||||
2. Press ....
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: misc
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
label: Version Platform Description
|
||||
description: Describe your platform (program version, OS, browser)
|
||||
value:
|
||||
|
||||
@@ -1,28 +1,11 @@
|
||||
# Please read the [contributing wiki page](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Contributing) before submitting a pull request!
|
||||
## Description
|
||||
|
||||
If you have a large change, pay special attention to this paragraph:
|
||||
A clear and concise description of what you're trying to accomplish with this, so your intent doesn't have to be extracted from your code
|
||||
|
||||
> Before making changes, if you think that your feature will result in more than 100 lines changing, find me and talk to me about the feature you are proposing. It pains me to reject the hard work someone else did, but I won't add everything to the repo, and it's better if the rejection happens before you have to waste time working on the feature.
|
||||
## Notes
|
||||
|
||||
Otherwise, after making sure you're following the rules described in wiki page, remove this section and continue on.
|
||||
More technical discussion about your changes go here, plus anything that a maintainer might have to specifically take a look at, or be wary of
|
||||
|
||||
**Describe what this pull request is trying to achieve.**
|
||||
## Environment and Testing
|
||||
|
||||
A clear and concise description of what you're trying to accomplish with this, so your intent doesn't have to be extracted from your code.
|
||||
|
||||
**Additional notes and description of your changes**
|
||||
|
||||
More technical discussion about your changes go here, plus anything that a maintainer might have to specifically take a look at, or be wary of.
|
||||
|
||||
**Environment this was tested in**
|
||||
|
||||
List the environment you have developed / tested this on. As per the contributing page, changes should be able to work on Windows out of the box.
|
||||
- OS: [e.g. Windows, Linux]
|
||||
- Browser: [e.g. chrome, safari]
|
||||
- Graphics card: [e.g. NVIDIA RTX 2080 8GB, AMD RX 6600 8GB]
|
||||
|
||||
**Screenshots or videos of your changes**
|
||||
|
||||
If applicable, screenshots or a video showing off your changes. If it edits an existing UI, it should ideally contain a comparison of what used to be there, before your changes were made.
|
||||
|
||||
This is **required** for anything that touches the user interface.
|
||||
List the environment you have developed / tested this on
|
||||
|
||||
@@ -18,13 +18,12 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Python 3.10
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.10.6
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
**/requirements*txt
|
||||
cache-dependency-path: requirements*txt
|
||||
- name: Install PyLint
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
@@ -10,20 +10,11 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Python 3.10
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.10.6
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
**/requirements*txt
|
||||
cache-dependency-path: requirements.txt
|
||||
- name: Run tests
|
||||
run: python launch.py --tests test --no-half --disable-opt-split-attention --use-cpu all --skip-torch-cuda-test
|
||||
- name: Upload main app stdout-stderr
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: stdout-stderr
|
||||
path: |
|
||||
test/stdout.txt
|
||||
test/stderr.txt
|
||||
run: python launch.py
|
||||
|
||||
@@ -1,35 +1,41 @@
|
||||
# defaults
|
||||
__pycache__
|
||||
/params.txt
|
||||
/cache.json
|
||||
/config.json
|
||||
/ui-config.json
|
||||
/setup.log
|
||||
venv
|
||||
|
||||
# all models and temp files
|
||||
*.ckpt
|
||||
*.safetensors
|
||||
*.pth
|
||||
/ESRGAN/*
|
||||
/SwinIR/*
|
||||
/repositories
|
||||
/venv
|
||||
/tmp
|
||||
/model.ckpt
|
||||
*.pt
|
||||
*.bin
|
||||
*.optim
|
||||
*.lock
|
||||
*.zip
|
||||
*.rar
|
||||
*.pyc
|
||||
|
||||
# all dynamic stuff
|
||||
/repositories/**/*
|
||||
/extensions/**/*
|
||||
/outputs/**/*
|
||||
/embeddings/**/*
|
||||
/models/**/*
|
||||
/GFPGANv1.3.pth
|
||||
/gfpgan/weights/*.pth
|
||||
/ui-config.json
|
||||
/outputs
|
||||
/config.json
|
||||
/interrogate/**/*
|
||||
/train/log/**/*
|
||||
/tmp
|
||||
/log
|
||||
/webui.settings.bat
|
||||
/embeddings
|
||||
/styles.csv
|
||||
/params.txt
|
||||
/styles.csv.bak
|
||||
/webui-user.bat
|
||||
/webui-user.sh
|
||||
/interrogate
|
||||
/user.css
|
||||
/.idea
|
||||
notification.mp3
|
||||
/SwinIR
|
||||
/textual_inversion
|
||||
.vscode
|
||||
/extensions
|
||||
/test/stdout.txt
|
||||
/test/stderr.txt
|
||||
/cache.json
|
||||
/cert
|
||||
.vscode/
|
||||
|
||||
# unexcluded so folders get created
|
||||
!/repositories/.placeholder
|
||||
!/extensions/.placeholder
|
||||
!/outputs/.placeholder
|
||||
!/models/.placeholder
|
||||
!/models/embeddings/.placeholder
|
||||
!/models/hypernetworks/.placeholder
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
[submodule "wiki"]
|
||||
path = wiki
|
||||
url = https://github.com/vladmandic/automatic.wiki
|
||||
[submodule "extensions-builtin/sd-extension-system-info"]
|
||||
path = extensions-builtin/sd-extension-system-info
|
||||
url = https://github.com/vladmandic/sd-extension-system-info
|
||||
[submodule "extensions-builtin/sd-extension-aesthetic-scorer"]
|
||||
path = extensions-builtin/sd-extension-aesthetic-scorer
|
||||
url = https://github.com/vladmandic/sd-extension-aesthetic-scorer
|
||||
[submodule "extensions-builtin/sd-extension-steps-animation"]
|
||||
path = extensions-builtin/sd-extension-steps-animation
|
||||
url = https://github.com/vladmandic/sd-extension-steps-animation
|
||||
[submodule "extensions-builtin/stable-diffusion-webui-images-browser"]
|
||||
path = extensions-builtin/stable-diffusion-webui-images-browser
|
||||
url = https://github.com/AlUlkesh/stable-diffusion-webui-images-browser
|
||||
ignore = dirty
|
||||
[submodule "extensions-builtin/seed_travel"]
|
||||
path = extensions-builtin/seed_travel
|
||||
url = https://github.com/yownas/seed_travel
|
||||
ignore = dirty
|
||||
[submodule "extensions-builtin/sd-webui-model-converter"]
|
||||
path = extensions-builtin/sd-webui-model-converter
|
||||
url = https://github.com/Akegarasu/sd-webui-model-converter
|
||||
ignore = dirty
|
||||
[submodule "extensions-builtin/sd-dynamic-thresholding"]
|
||||
path = extensions-builtin/sd-dynamic-thresholding
|
||||
url = https://github.com/mcmonkeyprojects/sd-dynamic-thresholding
|
||||
ignore = dirty
|
||||
[submodule "modules/lora"]
|
||||
path = modules/lora
|
||||
url = https://github.com/kohya-ss/sd-scripts
|
||||
ignore = dirty
|
||||
[submodule "extensions-builtin/clip-interrogator-ext"]
|
||||
path = extensions-builtin/clip-interrogator-ext
|
||||
url = https://github.com/pharmapsychotic/clip-interrogator-ext.git
|
||||
[submodule "extensions-builtin/sd-webui-controlnet"]
|
||||
path = extensions-builtin/sd-webui-controlnet
|
||||
url = https://github.com/Mikubill/sd-webui-controlnet
|
||||
[submodule "modules/lycoris"]
|
||||
path = modules/lycoris
|
||||
url = https://github.com/KohakuBlueleaf/LyCORIS
|
||||
[submodule "extensions-builtin/stable-diffusion-webui-rembg"]
|
||||
path = extensions-builtin/stable-diffusion-webui-rembg
|
||||
url = https://github.com/AUTOMATIC1111/stable-diffusion-webui-rembg
|
||||
[submodule "extensions-builtin/a1111-sd-webui-lycoris"]
|
||||
path = extensions-builtin/a1111-sd-webui-lycoris
|
||||
url = https://github.com/KohakuBlueleaf/a1111-sd-webui-lycoris
|
||||
[submodule "extensions-builtin/multidiffusion-upscaler-for-automatic1111"]
|
||||
path = extensions-builtin/multidiffusion-upscaler-for-automatic1111
|
||||
url = https://github.com/pkuliyi2015/multidiffusion-upscaler-for-automatic1111
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"MD012": false,
|
||||
"MD013": false,
|
||||
"MD033": false,
|
||||
"MD036": false,
|
||||
"MD041": false
|
||||
}
|
||||
@@ -1,3 +1,206 @@
|
||||
# See https://pylint.pycqa.org/en/latest/user_guide/messages/message_control.html
|
||||
[MAIN]
|
||||
analyse-fallback-blocks=no
|
||||
clear-cache-post-run=no
|
||||
#enable-all-extensions=
|
||||
#errors-only=
|
||||
#exit-zero=
|
||||
extension-pkg-allow-list=
|
||||
extension-pkg-whitelist=
|
||||
fail-on=
|
||||
fail-under=10
|
||||
ignore=CVS
|
||||
ignore-paths=
|
||||
ignore-patterns=^\.#
|
||||
ignored-modules=
|
||||
jobs=0
|
||||
limit-inference-results=100
|
||||
load-plugins=
|
||||
persistent=yes
|
||||
py-version=3.10
|
||||
recursive=no
|
||||
source-roots=
|
||||
suggestion-mode=yes
|
||||
unsafe-load-any-extension=no
|
||||
#verbose=
|
||||
|
||||
[BASIC]
|
||||
argument-naming-style=snake_case
|
||||
#argument-rgx=
|
||||
attr-naming-style=snake_case
|
||||
#attr-rgx=
|
||||
bad-names=foo, bar, baz, toto, tutu, tata
|
||||
bad-names-rgxs=
|
||||
class-attribute-naming-style=any
|
||||
class-const-naming-style=UPPER_CASE
|
||||
#class-const-rgx=
|
||||
class-naming-style=PascalCase
|
||||
#class-rgx=
|
||||
const-naming-style=snake_case
|
||||
#const-rgx=
|
||||
docstring-min-length=-1
|
||||
function-naming-style=snake_case
|
||||
#function-rgx=
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,j,k,e,ex,ok,p
|
||||
good-names-rgxs=
|
||||
include-naming-hint=no
|
||||
inlinevar-naming-style=any
|
||||
#inlinevar-rgx=
|
||||
method-naming-style=snake_case
|
||||
#method-rgx=
|
||||
module-naming-style=snake_case
|
||||
#module-rgx=
|
||||
name-group=
|
||||
no-docstring-rgx=^_
|
||||
property-classes=abc.abstractproperty
|
||||
#typealias-rgx=
|
||||
#typevar-rgx=
|
||||
variable-naming-style=snake_case
|
||||
#variable-rgx=
|
||||
|
||||
[CLASSES]
|
||||
check-protected-access-in-special-methods=no
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp,
|
||||
asyncSetUp,
|
||||
__post_init__
|
||||
exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
|
||||
valid-classmethod-first-arg=cls
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
[DESIGN]
|
||||
exclude-too-few-public-methods=
|
||||
ignored-parents=
|
||||
max-args=5
|
||||
max-attributes=7
|
||||
max-bool-expr=5
|
||||
max-branches=12
|
||||
max-locals=15
|
||||
max-parents=7
|
||||
max-public-methods=20
|
||||
max-returns=6
|
||||
max-statements=50
|
||||
min-public-methods=2
|
||||
|
||||
[EXCEPTIONS]
|
||||
overgeneral-exceptions=builtins.BaseException,builtins.Exception
|
||||
|
||||
[FORMAT]
|
||||
expected-line-ending-format=
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
indent-after-paren=4
|
||||
indent-string=' '
|
||||
max-line-length=200
|
||||
max-module-lines=9999
|
||||
single-line-class-stmt=no
|
||||
single-line-if-stmt=no
|
||||
|
||||
[IMPORTS]
|
||||
allow-any-import-level=
|
||||
allow-reexport-from-package=no
|
||||
allow-wildcard-with-all=no
|
||||
deprecated-modules=
|
||||
ext-import-graph=
|
||||
import-graph=
|
||||
int-import-graph=
|
||||
known-standard-library=
|
||||
known-third-party=enchant
|
||||
preferred-modules=
|
||||
|
||||
[LOGGING]
|
||||
logging-format-style=new
|
||||
logging-modules=logging
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=C,R,W,E,I
|
||||
confidence=HIGH,
|
||||
CONTROL_FLOW,
|
||||
INFERENCE,
|
||||
INFERENCE_FAILURE,
|
||||
UNDEFINED
|
||||
# disable=C,R,W
|
||||
disable=raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
line-too-long,
|
||||
missing-function-docstring,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
logging-fstring-interpolation,
|
||||
import-outside-toplevel,
|
||||
consider-iterating-dictionary,
|
||||
wrong-import-position,
|
||||
unnecessary-lambda,
|
||||
consider-using-dict-items,
|
||||
dangerous-default-value,
|
||||
enable=c-extension-no-member
|
||||
|
||||
[METHOD_ARGS]
|
||||
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
|
||||
|
||||
[MISCELLANEOUS]
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
notes-rgx=
|
||||
|
||||
[REFACTORING]
|
||||
max-nested-blocks=5
|
||||
never-returning-functions=sys.exit,argparse.parse_error
|
||||
|
||||
[REPORTS]
|
||||
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
|
||||
msg-template=
|
||||
#output-format=
|
||||
reports=no
|
||||
score=no
|
||||
|
||||
[SIMILARITIES]
|
||||
ignore-comments=yes
|
||||
ignore-docstrings=yes
|
||||
ignore-imports=yes
|
||||
ignore-signatures=yes
|
||||
min-similarity-lines=4
|
||||
|
||||
[SPELLING]
|
||||
max-spelling-suggestions=4
|
||||
spelling-dict=
|
||||
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
|
||||
spelling-ignore-words=
|
||||
spelling-private-dict-file=
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
[STRING]
|
||||
check-quote-consistency=no
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
[TYPECHECK]
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
generated-members=numpy.*,torch.*,cv2.*
|
||||
ignore-none=yes
|
||||
ignore-on-opaque-inference=yes
|
||||
ignored-checks-for-mixins=no-member,
|
||||
not-async-context-manager,
|
||||
not-context-manager,
|
||||
attribute-defined-outside-init
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
|
||||
missing-member-hint=yes
|
||||
missing-member-hint-distance=1
|
||||
missing-member-max-choices=1
|
||||
mixin-class-rgx=.*[Mm]ixin
|
||||
signature-mutators=
|
||||
|
||||
[VARIABLES]
|
||||
additional-builtins=
|
||||
allow-global-unused-variables=yes
|
||||
allowed-redefined-builtins=
|
||||
callbacks=cb_,
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
init-import=no
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"python.analysis.extraPaths": [
|
||||
".",
|
||||
"./modules",
|
||||
"./modules/lora",
|
||||
"./repositories/BLIP",
|
||||
"./repositories/CodeFormer",
|
||||
"./repositories/k-diffusion",
|
||||
"./repositories/taming-transformers",
|
||||
"./repositories/stable-diffusion-stability-ai"
|
||||
],
|
||||
"python.analysis.typeCheckingMode": "off"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
* @AUTOMATIC1111
|
||||
|
||||
# if you were managing a localization and were removed from this file, this is because
|
||||
# the intended way to do localizations now is via extensions. See:
|
||||
# https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Developing-extensions
|
||||
# Make a repo with your localization and since you are still listed as a collaborator
|
||||
# you can add it to the wiki page yourself. This change is because some people complained
|
||||
# the git commit log is cluttered with things unrelated to almost everyone and
|
||||
# because I believe this is the best overall for the project to handle localizations almost
|
||||
# entirely without my oversight.
|
||||
|
||||
|
||||
@@ -1,162 +1,149 @@
|
||||
# Stable Diffusion web UI
|
||||
A browser interface based on Gradio library for Stable Diffusion.
|
||||
# Stable Diffusion - Automatic
|
||||
|
||||

|
||||
*Heavily opinionated custom fork of* <https://github.com/AUTOMATIC1111/stable-diffusion-webui>
|
||||
|
||||
## Features
|
||||
[Detailed feature showcase with images](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features):
|
||||
- Original txt2img and img2img modes
|
||||
- One click install and run script (but you still must install python and git)
|
||||
- Outpainting
|
||||
- Inpainting
|
||||
- Color Sketch
|
||||
- Prompt Matrix
|
||||
- Stable Diffusion Upscale
|
||||
- Attention, specify parts of text that the model should pay more attention to
|
||||
- a man in a ((tuxedo)) - will pay more attention to tuxedo
|
||||
- a man in a (tuxedo:1.21) - alternative syntax
|
||||
- select text and press ctrl+up or ctrl+down to automatically adjust attention to selected text (code contributed by anonymous user)
|
||||
- Loopback, run img2img processing multiple times
|
||||
- X/Y/Z plot, a way to draw a 3 dimensional plot of images with different parameters
|
||||
- Textual Inversion
|
||||
- have as many embeddings as you want and use any names you like for them
|
||||
- use multiple embeddings with different numbers of vectors per token
|
||||
- works with half precision floating point numbers
|
||||
- train embeddings on 8GB (also reports of 6GB working)
|
||||
- Extras tab with:
|
||||
- GFPGAN, neural network that fixes faces
|
||||
- CodeFormer, face restoration tool as an alternative to GFPGAN
|
||||
- RealESRGAN, neural network upscaler
|
||||
- ESRGAN, neural network upscaler with a lot of third party models
|
||||
- SwinIR and Swin2SR([see here](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/2092)), neural network upscalers
|
||||
- LDSR, Latent diffusion super resolution upscaling
|
||||
- Resizing aspect ratio options
|
||||
- Sampling method selection
|
||||
- Adjust sampler eta values (noise multiplier)
|
||||
- More advanced noise setting options
|
||||
- Interrupt processing at any time
|
||||
- 4GB video card support (also reports of 2GB working)
|
||||
- Correct seeds for batches
|
||||
- Live prompt token length validation
|
||||
- Generation parameters
|
||||
- parameters you used to generate images are saved with that image
|
||||
- in PNG chunks for PNG, in EXIF for JPEG
|
||||
- can drag the image to PNG info tab to restore generation parameters and automatically copy them into UI
|
||||
- can be disabled in settings
|
||||
- drag and drop an image/text-parameters to promptbox
|
||||
- Read Generation Parameters Button, loads parameters in promptbox to UI
|
||||
- Settings page
|
||||
- Running arbitrary python code from UI (must run with --allow-code to enable)
|
||||
- Mouseover hints for most UI elements
|
||||
- Possible to change defaults/mix/max/step values for UI elements via text config
|
||||
- Tiling support, a checkbox to create images that can be tiled like textures
|
||||
- Progress bar and live image generation preview
|
||||
- Can use a separate neural network to produce previews with almost none VRAM or compute requirement
|
||||
- Negative prompt, an extra text field that allows you to list what you don't want to see in generated image
|
||||
- Styles, a way to save part of prompt and easily apply them via dropdown later
|
||||
- Variations, a way to generate same image but with tiny differences
|
||||
- Seed resizing, a way to generate same image but at slightly different resolution
|
||||
- CLIP interrogator, a button that tries to guess prompt from an image
|
||||
- Prompt Editing, a way to change prompt mid-generation, say to start making a watermelon and switch to anime girl midway
|
||||
- Batch Processing, process a group of files using img2img
|
||||
- Img2img Alternative, reverse Euler method of cross attention control
|
||||
- Highres Fix, a convenience option to produce high resolution pictures in one click without usual distortions
|
||||
- Reloading checkpoints on the fly
|
||||
- Checkpoint Merger, a tab that allows you to merge up to 3 checkpoints into one
|
||||
- [Custom scripts](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Custom-Scripts) with many extensions from community
|
||||
- [Composable-Diffusion](https://energy-based-model.github.io/Compositional-Visual-Generation-with-Composable-Diffusion-Models/), a way to use multiple prompts at once
|
||||
- separate prompts using uppercase `AND`
|
||||
- also supports weights for prompts: `a cat :1.2 AND a dog AND a penguin :2.2`
|
||||
- No token limit for prompts (original stable diffusion lets you use up to 75 tokens)
|
||||
- DeepDanbooru integration, creates danbooru style tags for anime prompts
|
||||
- [xformers](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Xformers), major speed increase for select cards: (add --xformers to commandline args)
|
||||
- via extension: [History tab](https://github.com/yfszzx/stable-diffusion-webui-images-browser): view, direct and delete images conveniently within the UI
|
||||
- Generate forever option
|
||||
- Training tab
|
||||
- hypernetworks and embeddings options
|
||||
- Preprocessing images: cropping, mirroring, autotagging using BLIP or deepdanbooru (for anime)
|
||||
- Clip skip
|
||||
- Hypernetworks
|
||||
- Loras (same as Hypernetworks but more pretty)
|
||||
- A sparate UI where you can choose, with preview, which embeddings, hypernetworks or Loras to add to your prompt.
|
||||
- Can select to load a different VAE from settings screen
|
||||
- Estimated completion time in progress bar
|
||||
- API
|
||||
- Support for dedicated [inpainting model](https://github.com/runwayml/stable-diffusion#inpainting-with-stable-diffusion) by RunwayML.
|
||||
- via extension: [Aesthetic Gradients](https://github.com/AUTOMATIC1111/stable-diffusion-webui-aesthetic-gradients), a way to generate images with a specific aesthetic by using clip images embeds (implementation of [https://github.com/vicgalle/stable-diffusion-aesthetic-gradients](https://github.com/vicgalle/stable-diffusion-aesthetic-gradients))
|
||||
- [Stable Diffusion 2.0](https://github.com/Stability-AI/stablediffusion) support - see [wiki](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#stable-diffusion-20) for instructions
|
||||
- [Alt-Diffusion](https://arxiv.org/abs/2211.06679) support - see [wiki](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#alt-diffusion) for instructions
|
||||
- Now without any bad letters!
|
||||
- Load checkpoints in safetensors format
|
||||
- Eased resolution restriction: generated image's domension must be a multiple of 8 rather than 64
|
||||
- Now with a license!
|
||||
- Reorder elements in the UI from settings screen
|
||||
-
|
||||
Fork is as close as up-to-date with origin as time allows
|
||||
All code changes are merged upstream whenever possible
|
||||
|
||||
## Installation and Running
|
||||
Make sure the required [dependencies](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Dependencies) are met and follow the instructions available for both [NVidia](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Install-and-Run-on-NVidia-GPUs) (recommended) and [AMD](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Install-and-Run-on-AMD-GPUs) GPUs.
|
||||
The idea behind the fork is to enable latest technologies and advances in text-to-image generation
|
||||
*Sometimes this is not the same as "as simple as possible to use"*
|
||||
If you are looking an amazing simple-to-use Stable Diffusion tool, I'd suggest [InvokeAI](https://invoke-ai.github.io/InvokeAI/) specifically due to its automated installer and ease of use
|
||||
|
||||
Alternatively, use online services (like Google Colab):
|
||||
<br>
|
||||
|
||||
- [List of Online Services](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Online-Services)
|
||||

|
||||
|
||||
### Automatic Installation on Windows
|
||||
1. Install [Python 3.10.6](https://www.python.org/downloads/windows/), checking "Add Python to PATH"
|
||||
2. Install [git](https://git-scm.com/download/win).
|
||||
3. Download the stable-diffusion-webui repository, for example by running `git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git`.
|
||||
4. Run `webui-user.bat` from Windows Explorer as normal, non-administrator, user.
|
||||
<br>
|
||||
|
||||
### Automatic Installation on Linux
|
||||
1. Install the dependencies:
|
||||
```bash
|
||||
# Debian-based:
|
||||
sudo apt install wget git python3 python3-venv
|
||||
# Red Hat-based:
|
||||
sudo dnf install wget git python3
|
||||
# Arch-based:
|
||||
sudo pacman -S wget git python3
|
||||
```
|
||||
2. To install in `/home/$(whoami)/stable-diffusion-webui/`, run:
|
||||
```bash
|
||||
bash <(wget -qO- https://raw.githubusercontent.com/AUTOMATIC1111/stable-diffusion-webui/master/webui.sh)
|
||||
```
|
||||
3. Run `webui.sh`.
|
||||
### Installation on Apple Silicon
|
||||
## Notes
|
||||
|
||||
Find the instructions [here](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Installation-on-Apple-Silicon).
|
||||
### Fork does differ in few things
|
||||
|
||||
## Contributing
|
||||
Here's how to add code to this repo: [Contributing](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Contributing)
|
||||
- New installer
|
||||
- New logger
|
||||
- New error and exception handlers
|
||||
- Enhanced environment tuning
|
||||
- Optimized startup and models lazy-loading
|
||||
- Built-in performance profiler
|
||||
- Updated libraries to latest known compatible versions
|
||||
- Includes opinionated **System** and **Options** configuration
|
||||
- Does not rely on `Accelerate` as it only affects distributed systems
|
||||
Gradio web server will be initialized much earlier which model load is done in the background
|
||||
Faster model loading plus ability to fallback on corrupt models
|
||||
- Uses simplified folder structure
|
||||
e.g. `/train`, `/outputs/*`, `/models/*`, etc.
|
||||
- Enhanced training templates
|
||||
- Built-in `LoRA`, `LyCORIS`, `Custom Diffusion`, `Dreambooth` training
|
||||
- Majority of settings configurable via UI without the need for command line flags
|
||||
e.g, cross-optimization methods, system folders, etc.
|
||||
|
||||
## Documentation
|
||||
The documentation was moved from this README over to the project's [wiki](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki).
|
||||
### Optimizations
|
||||
|
||||
## Credits
|
||||
Licenses for borrowed code can be found in `Settings -> Licenses` screen, and also in `html/licenses.html` file.
|
||||
- Optimized for `Torch` 2.0
|
||||
- Runs with `SDP` memory attention enabled by default if supported by system
|
||||
*Note*: `xFormers` and other cross-optimization methods are still available
|
||||
- Auto-adjust parameters when running on **CPU** or **CUDA**
|
||||
*Note:* AMD and M1 platforms are supported, but without out-of-the-box optimizations
|
||||
|
||||
- Stable Diffusion - https://github.com/CompVis/stable-diffusion, https://github.com/CompVis/taming-transformers
|
||||
- k-diffusion - https://github.com/crowsonkb/k-diffusion.git
|
||||
- GFPGAN - https://github.com/TencentARC/GFPGAN.git
|
||||
- CodeFormer - https://github.com/sczhou/CodeFormer
|
||||
- ESRGAN - https://github.com/xinntao/ESRGAN
|
||||
- SwinIR - https://github.com/JingyunLiang/SwinIR
|
||||
- Swin2SR - https://github.com/mv-lab/swin2sr
|
||||
- LDSR - https://github.com/Hafiidz/latent-diffusion
|
||||
- MiDaS - https://github.com/isl-org/MiDaS
|
||||
- Ideas for optimizations - https://github.com/basujindal/stable-diffusion
|
||||
- Cross Attention layer optimization - Doggettx - https://github.com/Doggettx/stable-diffusion, original idea for prompt editing.
|
||||
- Cross Attention layer optimization - InvokeAI, lstein - https://github.com/invoke-ai/InvokeAI (originally http://github.com/lstein/stable-diffusion)
|
||||
- Sub-quadratic Cross Attention layer optimization - Alex Birch (https://github.com/Birch-san/diffusers/pull/1), Amin Rezaei (https://github.com/AminRezaei0x443/memory-efficient-attention)
|
||||
- Textual Inversion - Rinon Gal - https://github.com/rinongal/textual_inversion (we're not using his code, but we are using his ideas).
|
||||
- Idea for SD upscale - https://github.com/jquesnelle/txt2imghd
|
||||
- Noise generation for outpainting mk2 - https://github.com/parlance-zz/g-diffuser-bot
|
||||
- CLIP interrogator idea and borrowing some code - https://github.com/pharmapsychotic/clip-interrogator
|
||||
- Idea for Composable Diffusion - https://github.com/energy-based-model/Compositional-Visual-Generation-with-Composable-Diffusion-Models-PyTorch
|
||||
- xformers - https://github.com/facebookresearch/xformers
|
||||
- DeepDanbooru - interrogator for anime diffusers https://github.com/KichangKim/DeepDanbooru
|
||||
- Sampling in float32 precision from a float16 UNet - marunine for the idea, Birch-san for the example Diffusers implementation (https://github.com/Birch-san/diffusers-play/tree/92feee6)
|
||||
- Instruct pix2pix - Tim Brooks (star), Aleksander Holynski (star), Alexei A. Efros (no star) - https://github.com/timothybrooks/instruct-pix2pix
|
||||
- Security advice - RyotaK
|
||||
- UniPC sampler - Wenliang Zhao - https://github.com/wl-zhao/UniPC
|
||||
- Initial Gradio script - posted on 4chan by an Anonymous user. Thank you Anonymous user.
|
||||
- (You)
|
||||
### Integrated Extensions
|
||||
|
||||
Hand-picked list of extensions that are deeply integrated into core workflows:
|
||||
|
||||
- [System Info](https://github.com/vladmandic/sd-extension-system-info)
|
||||
- [ControlNet](https://github.com/Mikubill/sd-webui-controlnet)
|
||||
- [Image Browser](https://github.com/AlUlkesh/stable-diffusion-webui-images-browser)
|
||||
- [LORA](https://github.com/kohya-ss/sd-scripts) *(both training and inference)*
|
||||
- [LyCORIS](https://github.com/KohakuBlueleaf/LyCORIS) *(both training and inference)*
|
||||
- [Model Converter](https://github.com/Akegarasu/sd-webui-model-converter)
|
||||
- [CLiP Interrogator](https://github.com/pharmapsychotic/clip-interrogator-ext)
|
||||
- [Dynamic Thresholding](https://github.com/mcmonkeyprojects/sd-dynamic-thresholding)
|
||||
- [Steps Animation](https://github.com/vladmandic/sd-extension-steps-animation)
|
||||
- [Seed Travel](https://github.com/yownas/seed_travel)
|
||||
- [Multi-Diffusion Upscaler](https://github.com/pkuliyi2015/multidiffusion-upscaler-for-automatic1111)
|
||||
|
||||
### User Interface
|
||||
|
||||
- Includes updated **UI**: reskinned and reorganized
|
||||
Black and orange dark theme with fixed width options panels and larger previews
|
||||
|
||||
### Removed
|
||||
|
||||
- Drops compatibility with older versions of `python` and requires **3.9** or **3.10**
|
||||
- Drops localizations
|
||||
|
||||
### Integrated CLI/API tools
|
||||
|
||||
Fork adds extra functionality:
|
||||
|
||||
- New skin and UI layout
|
||||
- Ships with set of **CLI** tools that rely on *SD API* for execution:
|
||||
e.g. `generate`, `train`, `bench`, etc.
|
||||
[Full list](<cli/>)
|
||||
|
||||
<br>
|
||||
|
||||
## Install
|
||||
|
||||
1. Install first:
|
||||
**Python** & **Git**
|
||||
2. If you have nVidia GPU, install nVidia CUDA toolkit:
|
||||
<https://developer.nvidia.com/cuda-downloads>
|
||||
3. Clone repository
|
||||
`git clone https://github.com/vladmandic/automatic`
|
||||
|
||||
## Run
|
||||
|
||||
Run desired startup script to install dependencies and extensions and start server:
|
||||
|
||||
- `webui.bat` and `webui.sh`:
|
||||
Platform specific wrapper scripts For Windows, Linux and OSX
|
||||
Starts `launch.py` in a Python virtual environment (venv)
|
||||
*Note*: Server can run without virtual environment, but it is recommended to use it to avoid library version conflicts with other applications
|
||||
**If you're unsure which launcher to use, this is the one you want**
|
||||
- `launch.py`:
|
||||
Main startup script
|
||||
Can be used directly to start server in a manually activated `venv` or to run server without `venv`
|
||||
- `setup.py`:
|
||||
Main installer, used by `launch.py`
|
||||
Can also be used directly to update repository or extensions
|
||||
If running manually, make sure to activate `venv` first (if used)
|
||||
- `webui.py`:
|
||||
Main server script
|
||||
|
||||
Any of the above scripts can be used with `--help` to display detailed usage information and available parameters
|
||||
For example:
|
||||
> webui.bat --help
|
||||
|
||||
Full startup sequence is logged in `setup.log`, so if you encounter any issues, please check it first
|
||||
|
||||
## Update
|
||||
|
||||
The launcher can perform automatic update of main repository, requirements, extensions and submodules:
|
||||
|
||||
- Main repository:
|
||||
Update is *not* performed by default, enable with `--upgrade` flag
|
||||
- Requirements:
|
||||
Check is performed on each startup and missing requirements are auto-installed, can be skipped with `--skip-requirements` flag
|
||||
- Extensions and submodules:
|
||||
Update is performed on each startup and installer for each extension is started, can be skipped with `--skip-extensions` flag
|
||||
- All checks can be skipped using `--quick` flag
|
||||
|
||||
<br>
|
||||
|
||||
## Other
|
||||
|
||||
### Scripts
|
||||
|
||||
This repository comes with a large collection of scripts that can be used to process inputs, train, generate, and benchmark models
|
||||
As well as number of auxiliary scripts that do not rely on **WebUI**, but can be used for end-to-end solutions such as extract frames from videos, etc.
|
||||
For full details see [Docs](cli/README.md)
|
||||
|
||||
<br>
|
||||
|
||||
### Docs
|
||||
|
||||
- Scripts are in [Scripts](cli/README.md)
|
||||
- Everything else is in [Wiki](https://github.com/vladmandic/automatic/wiki)
|
||||
- Except my current [TODO](TODO.md)
|
||||
|
||||
<br>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# TODO
|
||||
|
||||
## Fixes
|
||||
|
||||
Stuff to be fixed...
|
||||
|
||||
- Reconnect UI to ops in progress on browser restart
|
||||
- Redo Extensions tab: see <https://vladmandic.github.io/sd-extension-manager/pages/extensions.html>
|
||||
- Cleanup & integrate CSS into single file
|
||||
- Allow user themes
|
||||
- Replace PngInfo/EXIF metadata handler
|
||||
- Pick & merge PRs from main repo
|
||||
- Create new GitHub hooks/actions for CI/CD
|
||||
- Investigate integration with `Torch-DirectML`
|
||||
- Set defaults for Apple M1
|
||||
- Revisit `torch.compile`
|
||||
- Ask to download default model
|
||||
- Support UI restart on-the-fly
|
||||
- Stream-load as option
|
||||
- Dont spawn extensions installer
|
||||
- Remove `models` from git repo
|
||||
- Merge `Image Info` into `Process`
|
||||
|
||||
## Integration
|
||||
|
||||
Tech that can be integrated as part of the core workflow...
|
||||
|
||||
- [Merge without distortion](https://github.com/ogkalu2/Merge-Stable-Diffusion-models-without-distortion)
|
||||
- [Weighted merges](https://github.com/bbc-mc/sdweb-merge-block-weighted-gui/tree/master)
|
||||
- [Null-text inversion](https://github.com/ouhenio/null-text-inversion-colab)
|
||||
- [Custom diffusion](https://github.com/guaneec/custom-diffusion-webui), [Custom diffusion](https://www.cs.cmu.edu/~custom-diffusion/)
|
||||
- [Dream artist](https://github.com/7eu7d7/DreamArtist-sd-webui-extension)
|
||||
|
||||
## Random
|
||||
|
||||
- Bunch of stuff: <https://pharmapsychotic.com/tools.html>
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#Layer_00000017490036567012494010000005804862750708555662_);}
|
||||
.st1{fill:url(#Layer_00000034050872017694305340000002149777042112552351_);}
|
||||
.st2{fill:url(#Layer_00000138551514007669431480000005277331070455824283_);}
|
||||
.st3{fill:url(#Layer_00000121260822957399194640000002004818051597458359_);}
|
||||
.st4{fill:url(#Layer_00000089551383551611365750000017839558944096048031_);}
|
||||
</style>
|
||||
<g id="Layer_6">
|
||||
|
||||
<linearGradient id="Layer_00000049914890111007527720000017374358503294078090_" gradientUnits="userSpaceOnUse" x1="532.575" y1="791.8751" x2="1084.825" y2="239.625" gradientTransform="matrix(1 0 0 -1 0 1024)">
|
||||
<stop offset="0" style="stop-color:#FF002F;stop-opacity:0.4"/>
|
||||
<stop offset="0.9974" style="stop-color:#7E00B1;stop-opacity:0.4"/>
|
||||
</linearGradient>
|
||||
|
||||
<path id="Layer_00000102536207121589900870000006567919986179283851_" style="fill:url(#Layer_00000049914890111007527720000017374358503294078090_);" d="
|
||||
M669.3,150.4l333.8,715.7L948,866L614.3,150.4H669.3z"/>
|
||||
|
||||
<linearGradient id="Layer_00000160174143896567726850000004375001755377793183_" gradientUnits="userSpaceOnUse" x1="471.225" y1="791.925" x2="1023.525" y2="239.625" gradientTransform="matrix(1 0 0 -1 0 1024)">
|
||||
<stop offset="0" style="stop-color:#FF002F;stop-opacity:0.6"/>
|
||||
<stop offset="0.9974" style="stop-color:#7E00B1;stop-opacity:0.6"/>
|
||||
</linearGradient>
|
||||
|
||||
<path id="Layer_00000060024555235152659250000001459138218033286026_" style="fill:url(#Layer_00000160174143896567726850000004375001755377793183_);" d="
|
||||
M608,150.4l333.8,715.7l-55.1-0.1L552.9,150.4H608z"/>
|
||||
|
||||
<linearGradient id="Layer_00000031207641186489689680000000679959006580279939_" gradientUnits="userSpaceOnUse" x1="409.3" y1="791.5" x2="961.55" y2="239.25" gradientTransform="matrix(1 0 0 -1 0 1024)">
|
||||
<stop offset="0" style="stop-color:#FF002F;stop-opacity:0.8"/>
|
||||
<stop offset="0.9974" style="stop-color:#7E00B1;stop-opacity:0.8"/>
|
||||
</linearGradient>
|
||||
|
||||
<path id="Layer_00000048487563565240844250000000391955575295241088_" style="fill:url(#Layer_00000031207641186489689680000000679959006580279939_);" d="
|
||||
M546.4,150.4l333.8,715.7l-56.6-0.1L491.4,150.4H546.4z"/>
|
||||
|
||||
<linearGradient id="Layer_00000103976646255133082460000010809123275544418985_" gradientUnits="userSpaceOnUse" x1="315.15" y1="822.95" x2="898.7" y2="239.4" gradientTransform="matrix(1 0 0 -1 0 1024)">
|
||||
<stop offset="0" style="stop-color:#FF002F"/>
|
||||
<stop offset="0.9974" style="stop-color:#7E00B1"/>
|
||||
</linearGradient>
|
||||
|
||||
<path id="Layer_00000029027851136719188610000011649511842649520560_" style="fill:url(#Layer_00000103976646255133082460000010809123275544418985_);" d="
|
||||
M365.8,150.4h119.1l332.3,715.7l-57.5-0.1L454.2,212.2h-59.4L365.8,150.4z"/>
|
||||
|
||||
<linearGradient id="Layer_00000164482314129476801000000009884165076028213913_" gradientUnits="userSpaceOnUse" x1="94.75" y1="610.05" x2="618.9" y2="85.9001" gradientTransform="matrix(1 0 0 -1 0 1024)">
|
||||
<stop offset="0" style="stop-color:#FF002F"/>
|
||||
<stop offset="0.9974" style="stop-color:#7E00B1"/>
|
||||
</linearGradient>
|
||||
|
||||
<path id="Layer_00000025403836054873122760000014972920566759998142_" style="fill:url(#Layer_00000164482314129476801000000009884165076028213913_);" d="
|
||||
M19.8,866.1l338.5-715.7l333.9,714.4H456.7l-97.7-206l-95.6,207.3H19.8z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,3 @@
|
||||
# See https://pylint.pycqa.org/en/latest/user_guide/messages/message_control.html
|
||||
[MESSAGES CONTROL]
|
||||
enable=C,R,W,E,I
|
||||
@@ -0,0 +1,163 @@
|
||||
# Stable-Diffusion Productivity Scripts
|
||||
|
||||
*Notes*:
|
||||
- Offline scripts can be used with or without **Automatic WebUI**
|
||||
- Online scripts rely on **Automatic WebUI** API which should be started with `--api` parameter
|
||||
- All scripts have built-in `--help` parameter that can be used to get more information
|
||||
|
||||
<br>
|
||||
|
||||
## Main Scripts
|
||||
|
||||
### Generate
|
||||
|
||||
Text-to-image with all of the possible parameters
|
||||
Supports upsampling, face restoration and grid creation
|
||||
> python generate.py
|
||||
|
||||
By default uses parameters from `generate.json`
|
||||
|
||||
Parameters that are not specified will be randomized:
|
||||
- Prompt will be dynamically created from template of random samples: `random.json`
|
||||
- Sampler/Scheduler will be randomly picked from available ones
|
||||
- CFG Scale set to 5-10
|
||||
|
||||
### Train
|
||||
|
||||
Textual inversion embedding training
|
||||
> python train-ti.py
|
||||
|
||||
Combined pipeline:
|
||||
1. Creates embedding
|
||||
2. Extracts images if input is movie
|
||||
3. Preprocesses images
|
||||
4. Runs training
|
||||
|
||||
LoRA training
|
||||
> python train-lora.py
|
||||
|
||||
Combined pipeline:
|
||||
1. Preprocesses images
|
||||
2. Runs training
|
||||
|
||||
[Detailed documentation](https://github.com/vladmandic/automatic/wiki/Process.md)
|
||||
|
||||
LoRA extract from model
|
||||
> python moidules/lora-extract.py
|
||||
|
||||
<br>
|
||||
|
||||
## Auxiliary Scripts
|
||||
|
||||
### Benchmark
|
||||
|
||||
Benchmark your **Automatic WebUI**
|
||||
Note: Requires SD API
|
||||
|
||||
> python modules/bench.py
|
||||
|
||||
### Embedding Previews
|
||||
|
||||
Create previews of embeddings using preview templates
|
||||
Note: Requires SD API
|
||||
|
||||
> python modules/preview-embeddings.py
|
||||
|
||||
## Grid
|
||||
|
||||
Create flexible image grids from any number of images
|
||||
Note: Offline tool
|
||||
|
||||
> python modiles/grid.py
|
||||
|
||||
### Image Watermark
|
||||
|
||||
Create invisible image watermark and remove existing EXIF tags
|
||||
Note: Offline tool
|
||||
|
||||
> python modules/image-watermark.py
|
||||
|
||||
### Interrogate
|
||||
|
||||
Runs CLiP and Booru image interrogation
|
||||
Note: Requires SD API
|
||||
|
||||
> python modules/interrogate.py
|
||||
|
||||
### Interrogate-Offline
|
||||
|
||||
Standalone implementation of GiT, CLiP and ViT image interrogation
|
||||
Note: Offline tool
|
||||
|
||||
> python modules/interrogate-offline.py
|
||||
|
||||
### Models Previews
|
||||
|
||||
Create previews of models using built-in templates
|
||||
Note: Requires SD API
|
||||
|
||||
> python modules/preview-models.py
|
||||
|
||||
### Palette Extract
|
||||
|
||||
Extract color palette from image(s)
|
||||
Note: Offline tool
|
||||
|
||||
> python modules/palette-extract.py
|
||||
|
||||
### Image Process
|
||||
|
||||
Run image processing to extract face/body segments and run resolution/blur/dynamic-range checks
|
||||
Note: Offline except for interrogate to generate caption files which requires SD API
|
||||
|
||||
> python modules/process.py
|
||||
|
||||
[Detailed documentation](https://github.com/vladmandic/automatic/wiki/Process.md)
|
||||
|
||||
### Prompt Ideas
|
||||
|
||||
Generate complex prompt ideas
|
||||
Note: Offline tool
|
||||
|
||||
> python modules/prompt-ideas.py
|
||||
|
||||
### Prompt Promptist
|
||||
|
||||
Attempts to beautify the provided prompt
|
||||
Note: Offline tool
|
||||
|
||||
> python modules/promptist.py
|
||||
|
||||
### Training Loss-Chart
|
||||
|
||||
Create loss-chart from training log
|
||||
Note: Offline tool, may require adjustment to train paths if used with other repos
|
||||
|
||||
> python modules/train-losschart.py
|
||||
|
||||
### Training Loss-Rate
|
||||
|
||||
Create customizable loss rate to be used in training
|
||||
Note: Offline tool
|
||||
|
||||
> python modules/train-lossrate.py
|
||||
|
||||
### Video Extract
|
||||
|
||||
Extract frames from video files
|
||||
Note: Offline tool
|
||||
|
||||
> python modules/video-extract.py
|
||||
|
||||
<br>
|
||||
|
||||
## Utility Scripts
|
||||
### SDAPI
|
||||
|
||||
Utility module that handles async communication to Automatic API endpoints
|
||||
Note: Requires SD API
|
||||
|
||||
Can be used to manually execute specific commands:
|
||||
> python sdapi.py progress
|
||||
> python sdapi.py interrupt
|
||||
> python sdapi.py shutdown
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"paths":
|
||||
{
|
||||
"root": "/mnt/c/Users/mandi/OneDrive/Generative/Generate",
|
||||
"generate": "image",
|
||||
"upscale": "upscale",
|
||||
"grid": "grid"
|
||||
},
|
||||
"generate":
|
||||
{
|
||||
"restore_faces": true,
|
||||
"prompt": "dynamic",
|
||||
"negative_prompt": "foggy, blurry, blurred, duplicate, ugly, mutilated, mutation, mutated, out of frame, bad anatomy, disfigured, deformed, censored, low res, watermark, text, poorly drawn face, signature",
|
||||
"steps": 30,
|
||||
"batch_size": 2,
|
||||
"n_iter": 1,
|
||||
"seed": -1,
|
||||
"sampler_name": "DPM2 Karras",
|
||||
"cfg_scale": 6,
|
||||
"width": 512,
|
||||
"height": 512
|
||||
},
|
||||
"upscale":
|
||||
{
|
||||
"upscaler_1": "SwinIR_4x",
|
||||
"upscaler_2": "None",
|
||||
"upscale_first": false,
|
||||
"upscaling_resize": 0,
|
||||
"gfpgan_visibility": 0,
|
||||
"codeformer_visibility": 0,
|
||||
"codeformer_weight": 0.5
|
||||
},
|
||||
"options":
|
||||
{
|
||||
"sd_model_checkpoint": "sd-v15-runwayml",
|
||||
"sd_vae": "vae-ft-mse-840000-ema-pruned.ckpt"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
#!/bin/env python
|
||||
# pylint: disable=no-member
|
||||
"""generate batches of images from prompts and upscale them
|
||||
|
||||
params: run with `--help`
|
||||
|
||||
default workflow runs infinite loop and prints stats when interrupted:
|
||||
1. choose random scheduler lookup all available and pick one
|
||||
2. generate dynamic prompt based on styles, embeddings, places, artists, suffixes
|
||||
3. beautify prompt
|
||||
4. generate 3x3 images
|
||||
5. create image grid
|
||||
6. upscale images with face restoration
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import pathlib
|
||||
import secrets
|
||||
import time
|
||||
import sys
|
||||
import importlib
|
||||
|
||||
from random import randrange
|
||||
from PIL import Image
|
||||
from PIL.ExifTags import TAGS
|
||||
from PIL.TiffImagePlugin import ImageFileDirectory_v2
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'modules'))
|
||||
from modules.sdapi import close, get, interrupt, post, session
|
||||
from modules.util import Map, log, safestring
|
||||
|
||||
|
||||
sd = {}
|
||||
random = {}
|
||||
stats = Map({ 'images': 0, 'wall': 0, 'generate': 0, 'upscale': 0 })
|
||||
avg = {}
|
||||
|
||||
|
||||
def grid(data):
|
||||
if len(data.image) > 1:
|
||||
w, h = data.image[0].size
|
||||
rows = round(math.sqrt(len(data.image)))
|
||||
cols = math.ceil(len(data.image) / rows)
|
||||
image = Image.new('RGB', size = (cols * w, rows * h), color = 'black')
|
||||
for i, img in enumerate(data.image):
|
||||
image.paste(img, box=(i % cols * w, i // cols * h))
|
||||
short = data.info.prompt[:min(len(data.info.prompt), 96)] # limit prompt part of filename to 96 chars
|
||||
name = '{seed:0>9} {short}'.format(short = short, seed = data.info.all_seeds[0]) # pylint: disable=consider-using-f-string
|
||||
name = safestring(name) + '.jpg'
|
||||
f = os.path.join(sd.paths.root, sd.paths.grid, name)
|
||||
log.info({ 'grid': { 'name': f, 'size': image.size, 'images': len(data.image) } })
|
||||
image.save(f, 'JPEG', exif = exif(data.info, None, 'grid'), optimize = True, quality = 70)
|
||||
return image
|
||||
|
||||
|
||||
def exif(info, i = None, op = 'generate'):
|
||||
seed = [info.all_seeds[i]] if len(info.all_seeds) > 0 and i is not None else info.all_seeds # always returns list
|
||||
seed = ', '.join([str(x) for x in seed]) # int list to str list to single str
|
||||
template = '{prompt} | negative {negative_prompt} | seed {s} | steps {steps} | cfgscale {cfg_scale} | sampler {sampler_name} | batch {batch_size} | timestamp {job_timestamp} | model {model} | vae {vae}'.format(s = seed, model = sd.options['sd_model_checkpoint'], vae = sd.options['sd_vae'], **info) # pylint: disable=consider-using-f-string
|
||||
if op == 'upscale':
|
||||
template += ' | faces gfpgan' if sd.upscale.gfpgan_visibility > 0 else ''
|
||||
template += ' | faces codeformer' if sd.upscale.codeformer_visibility > 0 else ''
|
||||
template += ' | upscale {resize}x {upscaler}'.format(resize = sd.upscale.upscaling_resize, upscaler = sd.upscale.upscaler_1) if sd.upscale.upscaler_1 != 'None' else '' # pylint: disable=consider-using-f-string
|
||||
template += ' | upscale {resize}x {upscaler}'.format(resize = sd.upscale.upscaling_resize, upscaler = sd.upscale.upscaler_2) if sd.upscale.upscaler_2 != 'None' else '' # pylint: disable=consider-using-f-string
|
||||
if op == 'grid':
|
||||
template += ' | grid {num}'.format(num = sd.generate.batch_size * sd.generate.n_iter) # pylint: disable=consider-using-f-string
|
||||
ifd = ImageFileDirectory_v2()
|
||||
exif_stream = io.BytesIO()
|
||||
_TAGS = dict(((v, k) for k, v in TAGS.items())) # enumerate possible exif tags
|
||||
ifd[_TAGS['ImageDescription']] = template
|
||||
ifd.save(exif_stream)
|
||||
val = b'Exif\x00\x00' + exif_stream.getvalue()
|
||||
return val
|
||||
|
||||
|
||||
def randomize(lst):
|
||||
if len(lst) > 0:
|
||||
return secrets.choice(lst)
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def prompt(params): # generate dynamic prompt or use one if provided
|
||||
sd.generate.prompt = params.prompt if params.prompt != 'dynamic' else randomize(random.prompts)
|
||||
sd.generate.negative_prompt = params.negative if params.negative != 'dynamic' else randomize(random.negative)
|
||||
embedding = params.embedding if params.embedding != 'random' else randomize(random.embeddings)
|
||||
sd.generate.prompt = sd.generate.prompt.replace('<embedding>', embedding)
|
||||
artist = params.artist if params.artist != 'random' else randomize(random.artists)
|
||||
sd.generate.prompt = sd.generate.prompt.replace('<artist>', artist)
|
||||
style = params.style if params.style != 'random' else randomize(random.styles)
|
||||
sd.generate.prompt = sd.generate.prompt.replace('<style>', style)
|
||||
suffix = params.suffix if params.suffix != 'random' else randomize(random.suffixes)
|
||||
sd.generate.prompt = sd.generate.prompt.replace('<suffix>', suffix)
|
||||
place = params.suffix if params.suffix != 'random' else randomize(random.places)
|
||||
sd.generate.prompt = sd.generate.prompt.replace('<place>', place)
|
||||
if params.prompts or params.debug:
|
||||
log.info({ 'random initializers': random })
|
||||
if params.prompt == 'dynamic':
|
||||
log.info({ 'dynamic prompt': sd.generate.prompt })
|
||||
return sd.generate.prompt
|
||||
|
||||
|
||||
def sampler(params, options): # find sampler
|
||||
if params.sampler == 'random':
|
||||
sd.generate.sampler_name = randomize(options.samplers)
|
||||
log.info({ 'random sampler': sd.generate.sampler_name })
|
||||
else:
|
||||
found = [i for i in options.samplers if i.startswith(params.sampler)]
|
||||
if len(found) == 0:
|
||||
log.error({ 'sampler error': sd.generate.sampler_name, 'available': options.samplers})
|
||||
exit()
|
||||
sd.generate.sampler_name = found[0]
|
||||
return sd.generate.sampler_name
|
||||
|
||||
|
||||
async def generate(prompt = None, options = None, quiet = False): # pylint: disable=redefined-outer-name
|
||||
global sd
|
||||
if options:
|
||||
sd = Map(options)
|
||||
if prompt is not None:
|
||||
sd.generate.prompt = prompt
|
||||
if not quiet:
|
||||
log.info({ 'generate': sd.generate })
|
||||
names = []
|
||||
b64s = []
|
||||
images = []
|
||||
info = Map({})
|
||||
data = await post('/sdapi/v1/txt2img', sd.generate)
|
||||
if 'error' in data:
|
||||
log.error({ 'generate': data['error'], 'reason': data['reason'] })
|
||||
return Map({})
|
||||
info = Map(json.loads(data['info']))
|
||||
log.debug({ 'info': info })
|
||||
images = data['images']
|
||||
short = info.prompt[:min(len(info.prompt), 96)] # limit prompt part of filename to 64 chars
|
||||
for i in range(len(images)):
|
||||
b64s.append(images[i])
|
||||
images[i] = Image.open(io.BytesIO(base64.b64decode(images[i].split(',',1)[0])))
|
||||
name = '{seed:0>9} {short}'.format(short = short, seed = info.all_seeds[i]) # pylint: disable=consider-using-f-string
|
||||
name = safestring(name) + '.jpg'
|
||||
f = os.path.join(sd.paths.root, sd.paths.generate, name)
|
||||
names.append(f)
|
||||
if not quiet:
|
||||
log.info({ 'image': { 'name': f, 'size': images[i].size } })
|
||||
images[i].save(f, 'JPEG', exif = exif(info, i), optimize = True, quality = 70)
|
||||
return Map({ 'name': names, 'image': images, 'b64': b64s, 'info': info })
|
||||
|
||||
|
||||
async def upscale(data):
|
||||
data.upscaled = []
|
||||
if sd.upscale.upscaling_resize <=1:
|
||||
return data
|
||||
sd.upscale.image = ''
|
||||
log.info({ 'upscale': sd.upscale })
|
||||
for i in range(len(data.image)):
|
||||
f = data.name[i].replace(sd.paths.generate, sd.paths.upscale)
|
||||
sd.upscale.image = data.b64[i]
|
||||
res = await post('/sdapi/v1/extra-single-image', sd.upscale)
|
||||
image = Image.open(io.BytesIO(base64.b64decode(res['image'].split(',',1)[0])))
|
||||
data.upscaled.append(image)
|
||||
log.info({ 'image': { 'name': f, 'size': image.size } })
|
||||
image.save(f, 'JPEG', exif = exif(data.info, i, 'upscale'), optimize = True, quality = 70)
|
||||
return data
|
||||
|
||||
|
||||
async def init():
|
||||
'''
|
||||
import torch
|
||||
log.info({ 'torch': torch.__version__, 'available': torch.cuda.is_available() })
|
||||
current_device = torch.cuda.current_device()
|
||||
mem_free, mem_total = torch.cuda.mem_get_info()
|
||||
log.info({ 'cuda': torch.version.cuda, 'available': torch.cuda.is_available(), 'arch': torch.cuda.get_arch_list(), 'device': torch.cuda.get_device_name(current_device), 'memory': { 'free': round(mem_free / 1024 / 1024), 'total': (mem_total / 1024 / 1024) } })
|
||||
'''
|
||||
options = Map({})
|
||||
options.flags = await get('/sdapi/v1/cmd-flags')
|
||||
log.debug({ 'flags': options.flags })
|
||||
data = await get('/sdapi/v1/sd-models')
|
||||
options.models = [obj['title'] for obj in data]
|
||||
log.debug({ 'registered models': options.models })
|
||||
found = sd.options.sd_model_checkpoint if sd.options.sd_model_checkpoint in options.models else None
|
||||
if found is None:
|
||||
found = [i for i in options.models if i.startswith(sd.options.sd_model_checkpoint)]
|
||||
if len(found) == 0:
|
||||
log.error({ 'model error': sd.generate.sd_model_checkpoint, 'available': options.models})
|
||||
exit()
|
||||
sd.options.sd_model_checkpoint = found[0]
|
||||
data = await get('/sdapi/v1/samplers')
|
||||
options.samplers = [obj['name'] for obj in data]
|
||||
log.debug({ 'registered samplers': options.samplers })
|
||||
data = await get('/sdapi/v1/upscalers')
|
||||
options.upscalers = [obj['name'] for obj in data]
|
||||
log.debug({ 'registered upscalers': options.upscalers })
|
||||
data = await get('/sdapi/v1/face-restorers')
|
||||
options.restorers = [obj['name'] for obj in data]
|
||||
log.debug({ 'registered face restorers': options.restorers })
|
||||
await interrupt()
|
||||
await post('/sdapi/v1/options', sd.options)
|
||||
options.options = await get('/sdapi/v1/options')
|
||||
log.info({ 'target models': { 'diffuser': options.options['sd_model_checkpoint'], 'vae': options.options['sd_vae'] } })
|
||||
log.info({ 'paths': sd.paths })
|
||||
options.queue = await get('/queue/status')
|
||||
log.info({ 'queue': options.queue })
|
||||
pathlib.Path(sd.paths.root).mkdir(parents = True, exist_ok = True)
|
||||
pathlib.Path(os.path.join(sd.paths.root, sd.paths.generate)).mkdir(parents = True, exist_ok = True)
|
||||
pathlib.Path(os.path.join(sd.paths.root, sd.paths.upscale)).mkdir(parents = True, exist_ok = True)
|
||||
pathlib.Path(os.path.join(sd.paths.root, sd.paths.grid)).mkdir(parents = True, exist_ok = True)
|
||||
return options
|
||||
|
||||
|
||||
def args(): # parse cmd arguments
|
||||
global sd # pylint: disable=global-statement
|
||||
global random # pylint: disable=global-statement
|
||||
parser = argparse.ArgumentParser(description = 'sd pipeline')
|
||||
parser.add_argument('--config', type = str, default = 'generate.json', required = False, help = 'configuration file')
|
||||
parser.add_argument('--random', type = str, default = 'random.json', required = False, help = 'prompt file with randomized sections')
|
||||
parser.add_argument('--max', type = int, default = 1, required = False, help = 'maximum number of generated images')
|
||||
parser.add_argument('--prompt', type = str, default = 'dynamic', required = False, help = 'prompt')
|
||||
parser.add_argument('--negative', type = str, default = 'dynamic', required = False, help = 'negative prompt')
|
||||
parser.add_argument('--artist', type = str, default = 'random', required = False, help = 'artist style, used to guide dynamic prompt when prompt is not provided')
|
||||
parser.add_argument('--embedding', type = str, default = 'random', required = False, help = 'use embedding, used to guide dynamic prompt when prompt is not provided')
|
||||
parser.add_argument('--style', type = str, default = 'random', required = False, help = 'image style, used to guide dynamic prompt when prompt is not provided')
|
||||
parser.add_argument('--suffix', type = str, default = 'random', required = False, help = 'style suffix, used to guide dynamic prompt when prompt is not provided')
|
||||
parser.add_argument('--place', type = str, default = 'random', required = False, help = 'place locator, used to guide dynamic prompt when prompt is not provided')
|
||||
parser.add_argument('--faces', default = False, action='store_true', help = 'restore faces during upscaling')
|
||||
parser.add_argument('--steps', type = int, default = 0, required = False, help = 'number of steps')
|
||||
parser.add_argument('--batch', type = int, default = 0, required = False, help = 'batch size, limited by gpu vram')
|
||||
parser.add_argument('--n', type = int, default = 0, required = False, help = 'number of iterations')
|
||||
parser.add_argument('--cfg', type = int, default = 0, required = False, help = 'classifier free guidance scale')
|
||||
parser.add_argument('--sampler', type = str, default = 'random', required = False, help = 'sampler')
|
||||
parser.add_argument('--seed', type = int, default = 0, required = False, help = 'seed, default is random')
|
||||
parser.add_argument('--upscale', type = int, default = 0, required = False, help = 'upscale factor, disabled if 0')
|
||||
parser.add_argument('--model', type = str, default = '', required = False, help = 'diffusion model')
|
||||
parser.add_argument('--vae', type = str, default = '', required = False, help = 'vae model')
|
||||
parser.add_argument('--path', type = str, default = '', required = False, help = 'output path')
|
||||
parser.add_argument('--width', type = int, default = 0, required = False, help = 'width')
|
||||
parser.add_argument('--height', type = int, default = 0, required = False, help = 'height')
|
||||
parser.add_argument('--beautify', default = False, action='store_true', help = 'beautify prompt')
|
||||
parser.add_argument('--prompts', default = False, action='store_true', help = 'print dynamic prompt templates')
|
||||
parser.add_argument('--debug', default = False, action='store_true', help = 'print extra debug information')
|
||||
params = parser.parse_args()
|
||||
if params.debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.debug({ 'debug': True })
|
||||
log.debug({ 'args': params.__dict__ })
|
||||
home = pathlib.Path(sys.argv[0]).parent
|
||||
if os.path.isfile(params.config):
|
||||
try:
|
||||
with open(params.config, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
sd = Map(data)
|
||||
log.debug({ 'config': sd })
|
||||
except Exception as e:
|
||||
log.error({ 'config error': params.config, 'exception': e })
|
||||
exit()
|
||||
elif os.path.isfile(os.path.join(home, params.config)):
|
||||
try:
|
||||
with open(os.path.join(home, params.config), 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
sd = Map(data)
|
||||
log.debug({ 'config': sd })
|
||||
except Exception as e:
|
||||
log.error({ 'config error': params.config, 'exception': e })
|
||||
exit()
|
||||
else:
|
||||
log.error({ 'config file not found': params.config})
|
||||
exit()
|
||||
if params.prompt == 'dynamic':
|
||||
log.info({ 'prompt template': params.random })
|
||||
if os.path.isfile(params.random):
|
||||
try:
|
||||
with open(params.random, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
random = Map(data)
|
||||
log.debug({ 'random template': sd })
|
||||
except:
|
||||
log.error({ 'random template error': params.random})
|
||||
exit()
|
||||
elif os.path.isfile(os.path.join(home, params.random)):
|
||||
try:
|
||||
with open(os.path.join(home, params.random), 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
random = Map(data)
|
||||
log.debug({ 'random template': sd })
|
||||
except:
|
||||
log.error({ 'random template error': params.random})
|
||||
exit()
|
||||
else:
|
||||
log.error({ 'random template file not found': params.random})
|
||||
exit()
|
||||
_dynamic = prompt(params)
|
||||
|
||||
sd.paths.root = params.path if params.path != '' else sd.paths.root
|
||||
sd.generate.restore_faces = params.faces if params.faces is not None else sd.generate.restore_faces
|
||||
sd.generate.seed = params.seed if params.seed > 0 else sd.generate.seed
|
||||
sd.generate.sampler_name = params.sampler if params.sampler != 'random' else sd.generate.sampler_name
|
||||
sd.generate.batch_size = params.batch if params.batch > 0 else sd.generate.batch_size
|
||||
sd.generate.cfg_scale = params.cfg if params.cfg > 0 else sd.generate.cfg_scale
|
||||
sd.generate.n_iter = params.n if params.n > 0 else sd.generate.n_iter
|
||||
sd.generate.width = params.width if params.width > 0 else sd.generate.width
|
||||
sd.generate.height = params.height if params.height > 0 else sd.generate.height
|
||||
sd.generate.steps = params.steps if params.steps > 0 else sd.generate.steps
|
||||
sd.upscale.upscaling_resize = params.upscale if params.upscale > 0 else sd.upscale.upscaling_resize
|
||||
sd.upscale.codeformer_visibility = 1 if params.faces else sd.upscale.codeformer_visibility
|
||||
sd.options.sd_vae = params.vae if params.vae != '' else sd.options.sd_vae
|
||||
sd.options.sd_model_checkpoint = params.model if params.model != '' else sd.options.sd_model_checkpoint
|
||||
sd.upscale.upscaler_1 = 'SwinIR_4x' if params.upscale > 1 else sd.upscale.upscaler_1
|
||||
if sd.generate.cfg_scale == 0:
|
||||
sd.generate.cfg_scale = randrange(5, 10)
|
||||
return params
|
||||
|
||||
|
||||
async def main():
|
||||
params = args()
|
||||
sess = await session()
|
||||
if sess is None:
|
||||
await close()
|
||||
exit()
|
||||
options = await init()
|
||||
iteration = 0
|
||||
while True:
|
||||
iteration += 1
|
||||
log.info('')
|
||||
log.info({ 'iteration': iteration, 'batch': sd.generate.batch_size, 'n': sd.generate.n_iter, 'total': sd.generate.n_iter * sd.generate.batch_size })
|
||||
dynamic = prompt(params)
|
||||
if params.beautify:
|
||||
try:
|
||||
promptist = importlib.import_module('modules.promptist')
|
||||
sd.generate.prompt = promptist.beautify(dynamic)
|
||||
except Exception as e:
|
||||
log.error({ 'beautify': e })
|
||||
scheduler = sampler(params, options)
|
||||
t0 = time.perf_counter()
|
||||
data = await generate() # generate returns list of images
|
||||
if not 'image' in data:
|
||||
break
|
||||
stats.images += len(data.image)
|
||||
t1 = time.perf_counter()
|
||||
if len(data.image) > 0:
|
||||
avg[scheduler] = (t1 - t0) / len(data.image)
|
||||
stats.generate += t1 - t0
|
||||
_image = grid(data)
|
||||
data = await upscale(data)
|
||||
t2 = time.perf_counter()
|
||||
stats.upscale += t2 - t1
|
||||
stats.wall += t2 - t0
|
||||
its = sd.generate.steps / ((t1 - t0) / len(data.image)) if len(data.image) > 0 else 0
|
||||
avg_time = round((t1 - t0) / len(data.image)) if len(data.image) > 0 else 0
|
||||
log.info({ 'time' : { 'wall': round(t1 - t0), 'average': avg_time, 'upscale': round(t2 - t1), 'its': round(its, 2) } })
|
||||
log.info({ 'generated': stats.images, 'max': params.max, 'progress': round(100 * stats.images / params.max, 1) })
|
||||
if params.max != 0 and stats.images >= params.max:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
asyncio.run(interrupt())
|
||||
asyncio.run(close())
|
||||
log.info({ 'interrupt': True })
|
||||
finally:
|
||||
log.info({ 'sampler performance': avg })
|
||||
log.info({ 'stats' : stats })
|
||||
asyncio.run(close())
|
||||
'''
|
||||
except Exception as e:
|
||||
log.info({ 'sampler performance': avg })
|
||||
log.info({ 'stats': stats })
|
||||
log.critical({ 'exception': e })
|
||||
exit()
|
||||
'''
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
sd api txt2img benchmark
|
||||
"""
|
||||
import asyncio
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
from PIL import Image
|
||||
import sdapi as sdapi
|
||||
from util import Map, log
|
||||
|
||||
|
||||
options = Map({
|
||||
'restore_faces': False,
|
||||
'prompt': 'photo of two dice on a table',
|
||||
'negative_prompt': 'foggy, blurry',
|
||||
'steps': 20,
|
||||
'batch_size': 1,
|
||||
'n_iter': 1,
|
||||
'seed': -1,
|
||||
'sampler_name': 'Euler a',
|
||||
'cfg_scale': 0,
|
||||
'width': 512,
|
||||
'height': 512
|
||||
})
|
||||
|
||||
|
||||
# batch = [1, 1, 2, 4, 8, 12, 16, 24, 32, 48, 64, 96, 128]
|
||||
batch = [1, 1, 2, 4, 8, 12, 16]
|
||||
oom = 0
|
||||
|
||||
|
||||
async def txt2img():
|
||||
t0 = time.perf_counter()
|
||||
data = {}
|
||||
try:
|
||||
data = await sdapi.post('/sdapi/v1/txt2img', options)
|
||||
except:
|
||||
return -1
|
||||
if 'error' in data:
|
||||
return -1
|
||||
if 'info' in data:
|
||||
info = Map(json.loads(data['info']))
|
||||
else:
|
||||
return 0
|
||||
log.debug({ 'info': info })
|
||||
for i in range(len(data['images'])):
|
||||
data['images'][i] = Image.open(io.BytesIO(base64.b64decode(data['images'][i].split(',',1)[0])))
|
||||
log.debug({ 'image': data['images'][i].size })
|
||||
t1 = time.perf_counter()
|
||||
return t1 - t0
|
||||
|
||||
|
||||
def memstats():
|
||||
mem = sdapi.getsync('/sdapi/v1/memory')
|
||||
cpu = mem.get('ram', 'unavailable')
|
||||
gpu = mem.get('cuda', 'unavailable')
|
||||
if 'active' in gpu:
|
||||
gpu['session'] = gpu.pop('active')
|
||||
if 'reserved' in gpu:
|
||||
gpu.pop('allocated')
|
||||
gpu.pop('reserved')
|
||||
gpu.pop('inactive')
|
||||
if 'events' in gpu:
|
||||
global oom # pylint: disable=global-statement
|
||||
oom = gpu['events']['oom']
|
||||
gpu.pop('events')
|
||||
return cpu, gpu
|
||||
|
||||
|
||||
def gb(val: float):
|
||||
return round(val / 1024 / 1024 / 1024, 2)
|
||||
|
||||
|
||||
async def main():
|
||||
log.info({ 'benchmark': { 'batch-sizes': batch } })
|
||||
sdapi.quiet = True
|
||||
await sdapi.session()
|
||||
await sdapi.interrupt()
|
||||
opts = await sdapi.get('/sdapi/v1/options')
|
||||
opts = Map(opts)
|
||||
log.info({ 'options': {
|
||||
'resolution': [options.width, options.height],
|
||||
'model': opts.sd_model_checkpoint,
|
||||
'vae': opts.sd_vae,
|
||||
'hypernetwork': opts.sd_hypernetwork,
|
||||
'sampler': options.sampler_name,
|
||||
'clip-stop': opts.CLIP_stop_at_last_layers,
|
||||
'preview': opts.show_progress_every_n_steps
|
||||
} })
|
||||
cpu, gpu = memstats()
|
||||
log.info({ 'system': { 'cpu': cpu, 'gpu': gpu }})
|
||||
for i in range(len(batch)):
|
||||
if oom > 0:
|
||||
continue
|
||||
options['batch_size'] = batch[i]
|
||||
ts = await txt2img()
|
||||
if ts > 0:
|
||||
await asyncio.sleep(0)
|
||||
cpu, gpu = memstats()
|
||||
if i == 0:
|
||||
log.info({ 'warmup': round(ts, 2) })
|
||||
else:
|
||||
peak = gpu['session']['peak'] if 'session' in gpu else 0
|
||||
log.info({ 'batch': batch[i], 'its': round(options.steps / (ts / batch[i]), 2), 'img': round(ts / batch[i], 2), 'wall': round(ts, 2), 'peak': gb(peak), 'oom': oom > 0 })
|
||||
else:
|
||||
await asyncio.sleep(10)
|
||||
cpu, gpu = memstats()
|
||||
log.info({ 'batch': batch[i], 'result': 'error', 'gpu': gpu, 'oom': oom > 0 })
|
||||
break
|
||||
if oom > 0:
|
||||
log.info({ 'benchmark': 'ended with oom so you should probably restart your automatic server now' })
|
||||
await sdapi.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
log.warning({ 'interrupted': 'keyboard request' })
|
||||
sdapi.interruptsync()
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
Create image grid
|
||||
"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import math
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import filetype
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from util import log
|
||||
|
||||
|
||||
params = None
|
||||
|
||||
|
||||
def wrap(text: str, font: ImageFont.ImageFont, length: int):
|
||||
lines = ['']
|
||||
for word in text.split():
|
||||
line = f'{lines[-1]} {word}'.strip()
|
||||
if font.getlength(line) <= length:
|
||||
lines[-1] = line
|
||||
else:
|
||||
lines.append(word)
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def grid(images, labels = None, width = 0, height = 0, border = 0, square = False, horizontal = False, vertical = False): # pylint: disable=redefined-outer-name
|
||||
if horizontal:
|
||||
rows = 1
|
||||
elif vertical:
|
||||
rows = len(images)
|
||||
elif square:
|
||||
rows = round(math.sqrt(len(images)))
|
||||
else:
|
||||
rows = math.floor(math.sqrt(len(images)))
|
||||
cols = math.ceil(len(images) / rows)
|
||||
size = [0, 0]
|
||||
if width == 0:
|
||||
w = max([i.size[0] for i in images])
|
||||
size[0] = cols * w + cols * border
|
||||
else:
|
||||
size[0] = width
|
||||
w = round(width / cols)
|
||||
if height == 0:
|
||||
h = max([i.size[1] for i in images])
|
||||
size[1] = rows * h + rows * border
|
||||
else:
|
||||
size[1] = height
|
||||
h = round(height / rows)
|
||||
size = tuple(size)
|
||||
image = Image.new('RGB', size = size, color = 'black') # pylint: disable=redefined-outer-name
|
||||
font = ImageFont.truetype('DejaVuSansMono', round(w / 20))
|
||||
for i, img in enumerate(images): # pylint: disable=redefined-outer-name
|
||||
x = (i % cols * w) + (i % cols * border)
|
||||
y = (i // cols * h) + (i // cols * border)
|
||||
img.thumbnail((w, h), Image.HAMMING)
|
||||
image.paste(img, box=(x, y))
|
||||
if labels is not None and len(images) == len(labels):
|
||||
ctx = ImageDraw.Draw(image)
|
||||
label = wrap(labels[i], font, w)
|
||||
ctx.text((x + 1 + round(w / 200), y + 1 + round(w / 200)), label, font = font, fill = (0, 0, 0))
|
||||
ctx.text((x, y), label, font = font, fill = (255, 255, 255))
|
||||
log.info({ 'grid': { 'images': len(images), 'rows': rows, 'cols': cols, 'cell': [w, h] } })
|
||||
return image
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
log.info({ 'create grid' })
|
||||
parser = argparse.ArgumentParser(description='image grid utility')
|
||||
parser.add_argument("--square", default = False, action='store_true', help = "create square grid")
|
||||
parser.add_argument("--horizontal", default = False, action='store_true', help = "create horizontal grid")
|
||||
parser.add_argument("--vertical", default = False, action='store_true', help = "create vertical grid")
|
||||
parser.add_argument("--width", type = int, default = 0, required = False, help = "fixed grid width")
|
||||
parser.add_argument("--height", type = int, default = 0, required = False, help = "fixed grid height")
|
||||
parser.add_argument("--border", type = int, default = 0, required = False, help = "image border")
|
||||
parser.add_argument('--nolabels', default = False, action='store_true', help = "do not print image labels")
|
||||
parser.add_argument('--debug', default = False, action='store_true', help = "print extra debug information")
|
||||
parser.add_argument('output', type = str)
|
||||
parser.add_argument('input', type = str, nargs = '*')
|
||||
params = parser.parse_args()
|
||||
output = params.output if params.output.lower().endswith('.jpg') else params.output + '.jpg'
|
||||
if params.debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.debug({ 'debug': True })
|
||||
log.debug({ 'args': params.__dict__ })
|
||||
images = []
|
||||
labels = []
|
||||
for f in params.input:
|
||||
path = Path(f)
|
||||
if path.is_dir():
|
||||
files = [os.path.join(f, file) for file in os.listdir(f) if os.path.isfile(os.path.join(f, file))]
|
||||
elif path.is_file():
|
||||
files = [f]
|
||||
else:
|
||||
log.warning({ 'grid not a valid file/folder', f})
|
||||
continue
|
||||
files.sort()
|
||||
for file in files:
|
||||
if not filetype.is_image(file):
|
||||
continue
|
||||
log.debug(file)
|
||||
img = Image.open(file)
|
||||
# img.verify()
|
||||
images.append(img)
|
||||
fp = Path(file)
|
||||
if not params.nolabels:
|
||||
labels.append(fp.stem)
|
||||
# log.info({ 'folder': path.parent, 'labels': labels })
|
||||
if len(images) > 0:
|
||||
image = grid(
|
||||
images = images,
|
||||
labels = labels,
|
||||
width = params.width,
|
||||
height = params.height,
|
||||
border = params.border,
|
||||
square = params.square,
|
||||
horizontal = params.horizontal,
|
||||
vertical = params.vertical)
|
||||
image.save(output, 'JPEG', optimize = True, quality = 60)
|
||||
log.info({ 'grid': { 'file': output, 'size': list(image.size) } })
|
||||
else:
|
||||
log.info({ 'grid': 'nothing to do' })
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/bin/env python
|
||||
import os
|
||||
import io
|
||||
import pathlib
|
||||
import argparse
|
||||
import filetype
|
||||
import numpy as np
|
||||
from imwatermark import WatermarkEncoder, WatermarkDecoder
|
||||
from PIL import Image
|
||||
from PIL.ExifTags import TAGS
|
||||
from PIL.TiffImagePlugin import ImageFileDirectory_v2
|
||||
from util import log, Map
|
||||
import piexif
|
||||
import piexif.helper
|
||||
|
||||
|
||||
options = Map({ 'method': 'dwtDctSvd', 'type': 'bytes' })
|
||||
|
||||
|
||||
def get_exif(image):
|
||||
# using piexif
|
||||
res1 = {}
|
||||
try:
|
||||
exif = piexif.load(image.info["exif"])
|
||||
exif = exif.get("Exif", {})
|
||||
for k, v in exif.items():
|
||||
key = list(vars(piexif.ExifIFD).keys())[list(vars(piexif.ExifIFD).values()).index(k)]
|
||||
res1[key] = piexif.helper.UserComment.load(v)
|
||||
except:
|
||||
pass
|
||||
# using pillow
|
||||
res2 = {}
|
||||
try:
|
||||
res2 = { TAGS[k]: v for k, v in image.getexif().items() if k in TAGS }
|
||||
except:
|
||||
pass
|
||||
return {**res1, **res2}
|
||||
|
||||
|
||||
def set_exif(d: dict):
|
||||
ifd = ImageFileDirectory_v2()
|
||||
_TAGS = dict(((v, k) for k, v in TAGS.items())) # enumerate possible exif tags
|
||||
for k, v in d.items():
|
||||
ifd[_TAGS[k]] = v
|
||||
exif_stream = io.BytesIO()
|
||||
ifd.save(exif_stream)
|
||||
bytes = b'Exif\x00\x00' + exif_stream.getvalue()
|
||||
return bytes
|
||||
|
||||
|
||||
def get_watermark(image, args):
|
||||
data = np.asarray(image)
|
||||
decoder = WatermarkDecoder(options.type, args.length)
|
||||
bytes = decoder.decode(data, options.method)
|
||||
try:
|
||||
watermark = str(bytes, 'UTF-8').replace('\x00', '')
|
||||
except:
|
||||
watermark = ''
|
||||
return watermark
|
||||
|
||||
|
||||
def set_watermark(image, args):
|
||||
data = np.asarray(image)
|
||||
encoder = WatermarkEncoder()
|
||||
encoder.set_watermark(options.type, args.wm.encode('utf-8'))
|
||||
encoded = encoder.encode(data, options.method)
|
||||
image = Image.fromarray(encoded)
|
||||
return image
|
||||
|
||||
|
||||
def watermark(args, file):
|
||||
if not os.path.exists(file):
|
||||
log.error({ 'watermark': 'file not found' })
|
||||
return
|
||||
if not filetype.is_image(file):
|
||||
log.error({ 'watermark': 'file is not an image' })
|
||||
return
|
||||
image = Image.open(file)
|
||||
if image.width * image.height < 256 * 256:
|
||||
log.error({ 'watermark': 'image too small' })
|
||||
return
|
||||
|
||||
exif = get_exif(image)
|
||||
|
||||
if args.command == 'read':
|
||||
watermark = get_watermark(image, args)
|
||||
log.info({ 'file': file, 'watermark': watermark, 'exif': exif, 'resolution': f'{image.width}x{image.height}' })
|
||||
|
||||
elif args.command == 'write':
|
||||
metadata = b'' if args.strip else set_exif(exif)
|
||||
if args.output != '':
|
||||
pathlib.Path(args.output).mkdir(parents = True, exist_ok = True)
|
||||
image=set_watermark(image, args)
|
||||
fn = os.path.join(args.output, file)
|
||||
image.save(fn, exif=metadata)
|
||||
|
||||
if args.verify:
|
||||
data = np.asarray(image)
|
||||
decoder = WatermarkDecoder(options.type, args.length)
|
||||
bytes = decoder.decode(data, options.method)
|
||||
if bytes.startswith(b'\xff'):
|
||||
watermark = ''
|
||||
else:
|
||||
watermark = str(bytes, 'UTF-8').replace('\x00', '')
|
||||
else:
|
||||
watermark = args.wm
|
||||
|
||||
log.info({ 'file': fn, 'watermark': watermark, 'exif': None if args.strip else exif, 'resolution': f'{image.width}x{image.height}' })
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description = 'image watermarking')
|
||||
parser.add_argument('command', choices = ['read', 'write'])
|
||||
parser.add_argument('--wm', type=str, required=False, default='mm', help='watermark string')
|
||||
parser.add_argument('--strip', default=False, action='store_true', help = "strip existing exif data")
|
||||
parser.add_argument('--verify', default=False, action='store_true', help = "verify watermark during write")
|
||||
parser.add_argument('--length', type=int, default=16, help="watermark length in bits")
|
||||
parser.add_argument('--output', type=str, required=False, default='', help='folder to store images, default is overwrite in-place')
|
||||
parser.add_argument('input', type=str, nargs='*')
|
||||
args = parser.parse_args()
|
||||
log.info({ 'watermark args': vars(args), 'options': options })
|
||||
for arg in args.input:
|
||||
if os.path.isfile(arg):
|
||||
watermark(args, arg)
|
||||
elif os.path.isdir(arg):
|
||||
for root, _dirs, files in os.walk(arg):
|
||||
for f in files:
|
||||
watermark(args, os.path.join(root, f))
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/bin/env python
|
||||
|
||||
import os
|
||||
import gc
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
import torch
|
||||
import filetype
|
||||
from PIL import Image
|
||||
import transformers
|
||||
from transformers import AutoProcessor, AutoModelForCausalLM
|
||||
from transformers import BlipProcessor, BlipForConditionalGeneration
|
||||
from transformers import VisionEncoderDecoderModel, ViTFeatureExtractor, AutoTokenizer
|
||||
from util import log, Map
|
||||
|
||||
|
||||
model = None
|
||||
processor = None
|
||||
extractor = None
|
||||
dtype = torch.float32
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
|
||||
options = Map({
|
||||
'input': '',
|
||||
'min': 8,
|
||||
'max': 256,
|
||||
'beams': 1,
|
||||
'json': '',
|
||||
'txt': False,
|
||||
'tag': '',
|
||||
'git': True,
|
||||
'blip': True,
|
||||
'precision': 'fp16',
|
||||
'model': 'git',
|
||||
})
|
||||
|
||||
|
||||
def cleanup(s: str):
|
||||
s = s.split('"')[0].split('.')[0].split(' that')[0]
|
||||
s = s.split(' with a letter')[0].split(' with the number')[0].split(' with the word')[0]
|
||||
s = s.replace('arafed image of ', '')
|
||||
return s.replace('a ', '')
|
||||
|
||||
|
||||
def load_model(args):
|
||||
global model
|
||||
global processor
|
||||
global extractor
|
||||
transformers.logging.set_verbosity_error()
|
||||
if args.model == 'git':
|
||||
model_name = "microsoft/git-large-textcaps"
|
||||
if model is None:
|
||||
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=dtype)
|
||||
model.to(device)
|
||||
processor = AutoProcessor.from_pretrained(model_name, torch_dtype=dtype)
|
||||
log.info( { 'interrogate loaded model': model_name })
|
||||
elif args.model == 'blip':
|
||||
model_name = "Salesforce/blip-image-captioning-large"
|
||||
if model is None:
|
||||
model = BlipForConditionalGeneration.from_pretrained(model_name, torch_dtype=dtype)
|
||||
model.to(device)
|
||||
processor = BlipProcessor.from_pretrained(model_name, torch_dtype=dtype)
|
||||
log.info( { 'interrogate loaded model': model_name })
|
||||
elif args.model == 'vit':
|
||||
model_name = "nlpconnect/vit-gpt2-image-captioning"
|
||||
if model is None:
|
||||
model = VisionEncoderDecoderModel.from_pretrained(model_name, torch_dtype=dtype)
|
||||
model.to(device)
|
||||
extractor = ViTFeatureExtractor.from_pretrained(model_name, torch_dtype=dtype)
|
||||
processor = AutoTokenizer.from_pretrained(model_name, torch_dtype=dtype)
|
||||
log.info( { 'interrogate loaded model': model_name })
|
||||
else:
|
||||
log.info( { 'interrogate unknown model': args.model })
|
||||
|
||||
|
||||
def interrogate_files(params, files):
|
||||
args = Map({**options, **params})
|
||||
data = [f for f in files if filetype.is_image(f)]
|
||||
log.info({ 'interrogate files': len(files), 'images': len(data), 'args': args })
|
||||
load_model(args)
|
||||
metadata = {}
|
||||
for image_path in data:
|
||||
image = Image.open(image_path).convert('RGB')
|
||||
caption = ''
|
||||
if args.model == 'git':
|
||||
inputs = processor(images=[image], return_tensors="pt").to(device)
|
||||
ids = model.generate(pixel_values=inputs.pixel_values, num_beams=args.beams, min_length=args.min, max_length=args.max)
|
||||
caption = processor.batch_decode(ids, skip_special_tokens=True)[0]
|
||||
elif args.model == 'blip':
|
||||
inputs = processor(image, return_tensors="pt").to(device, dtype)
|
||||
ids = model.generate(**inputs, num_beams=args.beams, min_length=args.min, max_length=args.max)
|
||||
caption = processor.decode(ids[0], skip_special_tokens=True)
|
||||
elif args.model == 'vit':
|
||||
inputs = extractor(images=[image], return_tensors="pt").pixel_values.to(device)
|
||||
ids = model.generate(inputs, num_beams=args.beams, min_length=args.min, max_length=args.max)
|
||||
caption = processor.batch_decode(ids, skip_special_tokens=True)[0]
|
||||
else:
|
||||
log.error({ 'interrogate unknown model': args.model })
|
||||
|
||||
caption = cleanup(caption)
|
||||
tags = ''
|
||||
if args.tag != '':
|
||||
tags += args.tag + ','
|
||||
tags += caption.split(' ')[0]
|
||||
if args.txt:
|
||||
with open(os.path.splitext(image_path)[0] + '.txt', "wt", encoding='utf-8') as f:
|
||||
f.write(caption + "\n")
|
||||
metadata[image_path] = { 'caption': caption, 'tags': tags }
|
||||
log.info({ 'interrogate image': image_path, 'moodel': args.model, 'caption': caption, 'tags': tags })
|
||||
|
||||
if args.json != '':
|
||||
with open(args.json, "wt", encoding='utf-8') as f:
|
||||
f.write(json.dumps(metadata, indent=2) + "\n")
|
||||
return metadata
|
||||
|
||||
|
||||
def unload_model():
|
||||
global processor
|
||||
global model
|
||||
global extractor
|
||||
if model is not None:
|
||||
del model
|
||||
model = None
|
||||
if processor is not None:
|
||||
del processor
|
||||
processor = None
|
||||
if extractor is not None:
|
||||
del extractor
|
||||
extractor = None
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
with torch.no_grad():
|
||||
torch.cuda.empty_cache()
|
||||
with torch.cuda.device('cuda'):
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description = 'image interrogate')
|
||||
parser.add_argument('input', type=str, nargs='*', help='input file or directory')
|
||||
parser.add_argument('--model', default = 'git', choices = ['git', 'blip', 'vit'], help = "which model to use")
|
||||
parser.add_argument("--min", type=int, default=8, help="min length of caption")
|
||||
parser.add_argument("--max", type=int, default=256, help="max length of caption")
|
||||
parser.add_argument("--beams", type=int, default=1, help="number of beams to use")
|
||||
parser.add_argument("--json", type=str, default='', help="output json file")
|
||||
parser.add_argument("--tag", type=str, default='', help="append tag")
|
||||
parser.add_argument('--txt', default = False, action='store_true', help = "write captions to text files")
|
||||
params = parser.parse_args()
|
||||
log.info({ 'interrogate args': vars(params) })
|
||||
if len(params.input) == 0:
|
||||
parser.print_help()
|
||||
exit(1)
|
||||
files = []
|
||||
for loc in params.input:
|
||||
if os.path.isfile(loc):
|
||||
files.append(loc)
|
||||
elif os.path.isdir(loc):
|
||||
for root, _sub_dirs, dir in os.walk(loc):
|
||||
files = [os.path.join(root, f) for f in dir]
|
||||
t0 = time.time()
|
||||
metadata = interrogate_files(vars(params), files)
|
||||
t1 = time.time()
|
||||
log.info({ 'interrogate files': len(files), 'time': round(t1 - t0, 2) })
|
||||
unload_model()
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
use clip to interrogate image(s)
|
||||
"""
|
||||
|
||||
import io
|
||||
import base64
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import filetype
|
||||
from PIL import Image
|
||||
from util import log, Map
|
||||
import sdapi as sdapi
|
||||
|
||||
|
||||
stats = { 'captions': {}, 'keywords': {} }
|
||||
exclude = ['a', 'in', 'on', 'out', 'at', 'the', 'and', 'with', 'next', 'to', 'it', 'for', 'of', 'into', 'that']
|
||||
|
||||
|
||||
def decode(encoding):
|
||||
if encoding.startswith("data:image/"):
|
||||
encoding = encoding.split(";")[1].split(",")[1]
|
||||
return Image.open(io.BytesIO(base64.b64decode(encoding)))
|
||||
|
||||
|
||||
def encode(f):
|
||||
image = Image.open(f)
|
||||
exif = image.getexif()
|
||||
if image.mode == 'RGBA':
|
||||
image = image.convert('RGB')
|
||||
with io.BytesIO() as stream:
|
||||
image.save(stream, 'JPEG', exif = exif)
|
||||
values = stream.getvalue()
|
||||
encoded = base64.b64encode(values).decode()
|
||||
return encoded
|
||||
|
||||
|
||||
def print_summary():
|
||||
captions = dict(sorted(stats['captions'].items(), key=lambda x:x[1], reverse=True))
|
||||
log.info({ 'caption stats': captions })
|
||||
keywords = dict(sorted(stats['keywords'].items(), key=lambda x:x[1], reverse=True))
|
||||
log.info({ 'keyword stats': keywords })
|
||||
|
||||
|
||||
async def interrogate(f):
|
||||
if not filetype.is_image(f):
|
||||
log.info({ 'interrogate skip': f })
|
||||
return
|
||||
json = Map({ 'image': encode(f) })
|
||||
log.info({ 'interrogate': f })
|
||||
# run clip
|
||||
json.model = 'clip'
|
||||
res = await sdapi.post('/sdapi/v1/interrogate', json)
|
||||
caption = ""
|
||||
style = ""
|
||||
if 'caption' in res:
|
||||
caption = res.caption
|
||||
log.info({ 'interrogate caption': caption })
|
||||
if ', by' in caption:
|
||||
style = caption.split(', by')[1].strip()
|
||||
log.info({ 'interrogate style': style })
|
||||
for word in caption.split(' '):
|
||||
if word not in exclude:
|
||||
stats['captions'][word] = stats['captions'][word] + 1 if word in stats['captions'] else 1
|
||||
else:
|
||||
log.error({ 'interrogate clip error': res })
|
||||
# run booru
|
||||
json.model = 'deepdanbooru'
|
||||
res = await sdapi.post('/sdapi/v1/interrogate', json)
|
||||
keywords = {}
|
||||
if 'caption' in res:
|
||||
for term in res.caption.split(', '):
|
||||
term = term.replace('(', '').replace(')', '').replace('\\', '').split(':')
|
||||
if len(term) < 2:
|
||||
continue
|
||||
keywords[term[0]] = term[1]
|
||||
keywords = dict(sorted(keywords.items(), key=lambda x:x[1], reverse=True))
|
||||
for word in keywords.items():
|
||||
stats['keywords'][word[0]] = stats['keywords'][word[0]] + 1 if word[0] in stats['keywords'] else 1
|
||||
log.info({ 'interrogate keywords': keywords })
|
||||
else:
|
||||
log.error({ 'interrogate booru error': res })
|
||||
return caption, keywords, style
|
||||
|
||||
|
||||
async def main():
|
||||
sys.argv.pop(0)
|
||||
await sdapi.session()
|
||||
if len(sys.argv) == 0:
|
||||
log.error({ 'interrogate': 'no files specified' })
|
||||
for arg in sys.argv:
|
||||
if os.path.exists(arg):
|
||||
if os.path.isfile(arg):
|
||||
await interrogate(arg)
|
||||
elif os.path.isdir(arg):
|
||||
for root, _dirs, files in os.walk(arg):
|
||||
for f in files:
|
||||
caption, keywords, _style = await interrogate(os.path.join(root, f))
|
||||
else:
|
||||
log.error({ 'interrogate unknown file type': arg })
|
||||
else:
|
||||
log.error({ 'interrogate file missing': arg })
|
||||
await sdapi.close()
|
||||
print_summary()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/bin/env python
|
||||
|
||||
"""
|
||||
Extract approximating LoRA by SVD from two SD models
|
||||
Based on: <https://github.com/kohya-ss/sd-scripts/blob/main/networks/extract_lora_from_models.py>
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import torch
|
||||
import transformers
|
||||
from tqdm import tqdm
|
||||
from util import log
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'modules', 'lora'))
|
||||
import library.model_util as model_util
|
||||
import networks.lora as lora
|
||||
|
||||
|
||||
def svd(args): # pylint: disable=redefined-outer-name
|
||||
device = 'cuda' if torch.cuda.is_available() and args.device == 'cuda' else 'cpu'
|
||||
transformers.logging.set_verbosity_error()
|
||||
CLAMP_QUANTILE = 0.99
|
||||
MIN_DIFF = 1e-6
|
||||
if args.precision == 'fp32':
|
||||
save_dtype = torch.float
|
||||
elif args.precision == 'fp16':
|
||||
save_dtype = torch.float16
|
||||
elif args.precision == 'bf16':
|
||||
save_dtype = torch.bfloat16
|
||||
else:
|
||||
save_dtype = None
|
||||
t0 = time.time()
|
||||
log.info({ 'loading model': args.original })
|
||||
text_encoder_o, _, unet_o = model_util.load_models_from_stable_diffusion_checkpoint(args.v2, args.original)
|
||||
log.info({ 'loading model': args.tuned })
|
||||
text_encoder_t, _, unet_t = model_util.load_models_from_stable_diffusion_checkpoint(args.v2, args.tuned)
|
||||
with torch.no_grad():
|
||||
torch.cuda.empty_cache()
|
||||
# create LoRA network to extract weights: Use dim (rank) as alpha
|
||||
lora_network_o = lora.create_network(1.0, args.dim, args.dim, None, text_encoder_o, unet_o)
|
||||
lora_network_t = lora.create_network(1.0, args.dim, args.dim, None, text_encoder_t, unet_t)
|
||||
assert len(lora_network_o.text_encoder_loras) == len(lora_network_t.text_encoder_loras), 'model version is different'
|
||||
# get diffs
|
||||
diffs = {}
|
||||
text_encoder_different = False
|
||||
for i, (lora_o, lora_t) in enumerate(zip(lora_network_o.text_encoder_loras, lora_network_t.text_encoder_loras)):
|
||||
lora_name = lora_o.lora_name
|
||||
module_o = lora_o.org_module
|
||||
module_t = lora_t.org_module
|
||||
diff = module_t.weight - module_o.weight
|
||||
# Text Encoder might be same
|
||||
if torch.max(torch.abs(diff)) > MIN_DIFF:
|
||||
text_encoder_different = True
|
||||
diff = diff.float()
|
||||
diffs[lora_name] = diff
|
||||
|
||||
if not text_encoder_different:
|
||||
log.info({ 'lora': 'text encoder is same, extract U-Net only' })
|
||||
lora_network_o.text_encoder_loras = []
|
||||
diffs = {}
|
||||
|
||||
for i, (lora_o, lora_t) in enumerate(zip(lora_network_o.unet_loras, lora_network_t.unet_loras)):
|
||||
lora_name = lora_o.lora_name
|
||||
module_o = lora_o.org_module
|
||||
module_t = lora_t.org_module
|
||||
diff = module_t.weight - module_o.weight
|
||||
diff = diff.float()
|
||||
diff = diff.to(device)
|
||||
diffs[lora_name] = diff
|
||||
t1 = time.time()
|
||||
log.info({ 'lora models': 'ready', 'time': round(t1 - t0, 2) })
|
||||
|
||||
# make LoRA with svd
|
||||
log.info({ 'lora': 'calculating by svd' })
|
||||
rank = args.dim
|
||||
lora_weights = {}
|
||||
with torch.no_grad():
|
||||
for lora_name, mat in tqdm(list(diffs.items())):
|
||||
conv2d = len(mat.size()) == 4
|
||||
if conv2d:
|
||||
mat = mat.squeeze()
|
||||
U, S, Vh = torch.linalg.svd(mat)
|
||||
U = U[:, :rank]
|
||||
S = S[:rank]
|
||||
U = U @ torch.diag(S)
|
||||
Vh = Vh[:rank, :]
|
||||
dist = torch.cat([U.flatten(), Vh.flatten()])
|
||||
hi_val = torch.quantile(dist, CLAMP_QUANTILE)
|
||||
low_val = -hi_val
|
||||
U = U.clamp(low_val, hi_val)
|
||||
Vh = Vh.clamp(low_val, hi_val)
|
||||
lora_weights[lora_name] = (U, Vh)
|
||||
t2 = time.time()
|
||||
|
||||
# make state dict for LoRA
|
||||
lora_network_o.apply_to(text_encoder_o, unet_o, text_encoder_different, True)
|
||||
lora_sd = lora_network_o.state_dict()
|
||||
log.info({ 'lora extracted weights': len(lora_sd), 'time': round(t2 - t1, 2) })
|
||||
|
||||
for key in list(lora_sd.keys()):
|
||||
if 'alpha' in key:
|
||||
continue
|
||||
lora_name = key.split('.')[0]
|
||||
i = 0 if 'lora_up' in key else 1
|
||||
weights = lora_weights[lora_name][i]
|
||||
# print(key, i, weights.size(), lora_sd[key].size())
|
||||
if len(lora_sd[key].size()) == 4: # pylint: disable=unsubscriptable-object
|
||||
weights = weights.unsqueeze(2).unsqueeze(3)
|
||||
assert weights.size() == lora_sd[key].size(), f'size unmatch: {key}' # pylint: disable=unsubscriptable-object
|
||||
lora_sd[key] = weights # pylint: disable=unsupported-assignment-operation
|
||||
|
||||
# load state dict to LoRA and save it
|
||||
info = lora_network_o.load_state_dict(lora_sd)
|
||||
log.info({ 'lora loading extracted weights': info })
|
||||
|
||||
dir_name = os.path.dirname(args.save)
|
||||
if dir_name and not os.path.exists(dir_name):
|
||||
os.makedirs(dir_name, exist_ok=True)
|
||||
|
||||
# minimum metadata
|
||||
metadata = {'ss_network_dim': str(args.dim), 'ss_network_alpha': str(args.dim)}
|
||||
lora_network_o.save_weights(args.save, save_dtype, metadata)
|
||||
t3 = time.time()
|
||||
log.info({ 'lora saved weights': args.save, 'time': round(t3 - t2, 2) })
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description = 'extract lora weights')
|
||||
parser.add_argument('--v2', action='store_true', help='load Stable Diffusion v2.x model / Stable Diffusion')
|
||||
parser.add_argument('--precision', type=str, default='fp16', choices=[None, 'fp32', 'fp16', 'bf16'], help='precision in saving, same to merging if omitted')
|
||||
parser.add_argument('--device', type=str, default='cuda', choices=['cpu', 'cuda'], help='use cpu or cuda if available')
|
||||
parser.add_argument('--original', type=str, default=None, required=True, help='Stable Diffusion original model: ckpt or safetensors file')
|
||||
parser.add_argument('--tuned', type=str, default=None, required=True, help='Stable Diffusion tuned model, LoRA is difference of `original to tuned`: ckpt or safetensors file')
|
||||
parser.add_argument('--save', type=str, default=None, required=True, help='destination file name: ckpt or safetensors file')
|
||||
parser.add_argument('--dim', type=int, default=4, help='dimension (rank) of LoRA')
|
||||
args = parser.parse_args()
|
||||
log.info({ 'extract lora args': vars(args) })
|
||||
if not os.path.exists(args.original) or not os.path.exists(args.tuned):
|
||||
log.error({ 'models not found': [args.original, args.tuned] })
|
||||
else:
|
||||
svd(args)
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pathlib
|
||||
import argparse
|
||||
import warnings
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
from torchvision import transforms
|
||||
from tqdm import tqdm
|
||||
from util import log, Map
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'modules', 'lora'))
|
||||
import library.model_util as model_util
|
||||
import library.train_util as train_util
|
||||
|
||||
warnings.filterwarnings('ignore')
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
options = Map({
|
||||
'batch': 1,
|
||||
'input': '',
|
||||
'json': '',
|
||||
'max': 1024,
|
||||
'min': 256,
|
||||
'noupscale': False,
|
||||
'precision': 'fp32',
|
||||
'resolution': '512,512',
|
||||
'steps': 64,
|
||||
'vae': 'stabilityai/sd-vae-ft-mse'
|
||||
})
|
||||
vae = None
|
||||
|
||||
|
||||
def get_latents(vae, images, weight_dtype):
|
||||
image_transforms = transforms.Compose([ transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ])
|
||||
img_tensors = [image_transforms(image) for image in images]
|
||||
img_tensors = torch.stack(img_tensors)
|
||||
img_tensors = img_tensors.to(device, weight_dtype)
|
||||
with torch.no_grad():
|
||||
latents = vae.encode(img_tensors).latent_dist.sample().float().to('cpu').numpy()
|
||||
return latents
|
||||
|
||||
|
||||
def get_npz_filename_wo_ext(data_dir, image_key):
|
||||
return os.path.join(data_dir, os.path.splitext(os.path.basename(image_key))[0])
|
||||
|
||||
|
||||
def create_vae_latents(params):
|
||||
args = Map({**options, **params})
|
||||
log.info({ 'latents args': args })
|
||||
if args.steps % 8 > 0:
|
||||
log.warning({ 'latents': 'resolution is not multiple of 8' })
|
||||
image_paths = train_util.glob_images(args.input)
|
||||
if os.path.exists(args.json):
|
||||
log.info({ 'latents metadata': args.json, 'images': len(image_paths) })
|
||||
with open(args.json, 'rt', encoding='utf-8') as f:
|
||||
metadata = json.load(f)
|
||||
else:
|
||||
log.error({ 'latents metadata missing': args.json })
|
||||
return
|
||||
if args.precision == 'fp16':
|
||||
weight_dtype = torch.float16
|
||||
elif args.precision == 'bf16':
|
||||
weight_dtype = torch.bfloat16
|
||||
else:
|
||||
weight_dtype = torch.float32
|
||||
global vae
|
||||
if vae is None:
|
||||
vae = model_util.load_vae(args.vae, weight_dtype)
|
||||
vae.eval()
|
||||
vae.to(device, dtype=weight_dtype)
|
||||
max_reso = tuple([int(t) for t in args.resolution.split(',')])
|
||||
assert len(max_reso) == 2, f'illegal resolution: {args.resolution}'
|
||||
bucket_manager = train_util.BucketManager(args.noupscale, max_reso, args.min, args.max, args.steps)
|
||||
if not args.noupscale:
|
||||
bucket_manager.make_buckets()
|
||||
else:
|
||||
log.warning({ 'latents': 'min and max are ignored if noupscale is set' })
|
||||
img_ar_errors = []
|
||||
def process_batch(is_last):
|
||||
for bucket in bucket_manager.buckets:
|
||||
if (is_last and len(bucket) > 0) or len(bucket) >= args.batch:
|
||||
latents = get_latents(vae, [img for _, img in bucket], weight_dtype)
|
||||
assert latents.shape[2] == bucket[0][1].shape[0] // 8 and latents.shape[3] == bucket[0][1].shape[1] // 8, f'latent shape {latents.shape}, {bucket[0][1].shape}'
|
||||
for (image_key, _), latent in zip(bucket, latents):
|
||||
npz_file_name = get_npz_filename_wo_ext(args.input, image_key)
|
||||
np.savez(npz_file_name, latent)
|
||||
bucket.clear()
|
||||
data = [[(None, ip)] for ip in image_paths]
|
||||
bucket_counts = {}
|
||||
for data_entry in tqdm(data, smoothing=0.0):
|
||||
if data_entry[0] is None:
|
||||
continue
|
||||
img_tensor, image_path = data_entry[0]
|
||||
if img_tensor is not None:
|
||||
image = transforms.functional.to_pil_image(img_tensor)
|
||||
else:
|
||||
image = Image.open(image_path)
|
||||
image_key = os.path.basename(image_path)
|
||||
image_key = os.path.join(os.path.basename(pathlib.Path(image_path).parent), pathlib.Path(image_path).stem)
|
||||
if image_key not in metadata:
|
||||
metadata[image_key] = {}
|
||||
reso, resized_size, ar_error = bucket_manager.select_bucket(image.width, image.height)
|
||||
img_ar_errors.append(abs(ar_error))
|
||||
bucket_counts[reso] = bucket_counts.get(reso, 0) + 1
|
||||
metadata[image_key]['train_resolution'] = (reso[0] - reso[0] % 8, reso[1] - reso[1] % 8)
|
||||
if not args.noupscale:
|
||||
assert resized_size[0] == reso[0] or resized_size[1] == reso[1], f'internal error, resized size not match: {reso}, {resized_size}, {image.width}, {image.height}'
|
||||
assert resized_size[0] >= reso[0] and resized_size[1] >= reso[1], f'internal error, resized size too small: {reso}, {resized_size}, {image.width}, {image.height}'
|
||||
assert resized_size[0] >= reso[0] and resized_size[1] >= reso[1], f'internal error resized size is small: {resized_size}, {reso}'
|
||||
image = np.array(image)
|
||||
if resized_size[0] != image.shape[1] or resized_size[1] != image.shape[0]:
|
||||
image = cv2.resize(image, resized_size, interpolation=cv2.INTER_AREA)
|
||||
if resized_size[0] > reso[0]:
|
||||
trim_size = resized_size[0] - reso[0]
|
||||
image = image[:, trim_size//2:trim_size//2 + reso[0]]
|
||||
if resized_size[1] > reso[1]:
|
||||
trim_size = resized_size[1] - reso[1]
|
||||
image = image[trim_size//2:trim_size//2 + reso[1]]
|
||||
assert image.shape[0] == reso[1] and image.shape[1] == reso[0], f'internal error, illegal trimmed size: {image.shape}, {reso}'
|
||||
bucket_manager.add_image(reso, (image_key, image))
|
||||
process_batch(False)
|
||||
|
||||
process_batch(True)
|
||||
vae.to('cpu')
|
||||
|
||||
bucket_manager.sort()
|
||||
img_ar_errors = np.array(img_ar_errors)
|
||||
for i, reso in enumerate(bucket_manager.resos):
|
||||
count = bucket_counts.get(reso, 0)
|
||||
if count > 0:
|
||||
log.info({ 'latents bucket': i, 'resolution': reso, 'count': count, 'mean ar error': np.mean(img_ar_errors) })
|
||||
with open(args.json, 'wt', encoding='utf-8') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
|
||||
def unload_vae():
|
||||
global vae
|
||||
vae = None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('input', type=str, help='directory for train images')
|
||||
parser.add_argument('--json', type=str, required=True, help='metadata file to input')
|
||||
parser.add_argument('--vae', type=str, required=True, help='model name or path to encode latents')
|
||||
parser.add_argument('--batch', type=int, default=1, help='batch size in inference')
|
||||
parser.add_argument('--resolution', type=str, default='512,512', help='max resolution in fine tuning (width,height)')
|
||||
parser.add_argument('--min', type=int, default=256, help='minimum resolution for buckets')
|
||||
parser.add_argument('--max', type=int, default=1024, help='maximum resolution for buckets')
|
||||
parser.add_argument('--steps', type=int, default=64, help='steps of resolution for buckets, divisible by 8')
|
||||
parser.add_argument('--noupscale', action='store_true', help='make bucket for each image without upscaling')
|
||||
parser.add_argument('--precision', type=str, default='fp32', choices=['fp32', 'fp16', 'bf16'], help='use precision')
|
||||
params = parser.parse_args()
|
||||
create_vae_latents(vars(params))
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/bin/env python
|
||||
# based on <https://huggingface.co/JosephusCheung/ASimilarityCalculatior>
|
||||
|
||||
import safetensors
|
||||
import sys
|
||||
import torch
|
||||
from pathlib import Path
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import warnings
|
||||
from util import log
|
||||
|
||||
warnings.filterwarnings("ignore", category=UserWarning)
|
||||
|
||||
def cal_cross_attn(to_q, to_k, to_v, rand_input):
|
||||
hidden_dim, embed_dim = to_q.shape
|
||||
attn_to_q = nn.Linear(hidden_dim, embed_dim, bias=False)
|
||||
attn_to_k = nn.Linear(hidden_dim, embed_dim, bias=False)
|
||||
attn_to_v = nn.Linear(hidden_dim, embed_dim, bias=False)
|
||||
attn_to_q.load_state_dict({"weight": to_q})
|
||||
attn_to_k.load_state_dict({"weight": to_k})
|
||||
attn_to_v.load_state_dict({"weight": to_v})
|
||||
|
||||
return torch.einsum(
|
||||
"ik, jk -> ik",
|
||||
F.softmax(torch.einsum("ij, kj -> ik", attn_to_q(rand_input), attn_to_k(rand_input)), dim=-1),
|
||||
attn_to_v(rand_input)
|
||||
)
|
||||
|
||||
def load_model(path):
|
||||
if path.suffix == ".safetensors":
|
||||
return safetensors.torch.load_file(path, device="cpu")
|
||||
else:
|
||||
ckpt = torch.load(path, map_location="cpu")
|
||||
return ckpt["state_dict"] if "state_dict" in ckpt else ckpt
|
||||
|
||||
def eval(model, n, input):
|
||||
qk = f"model.diffusion_model.output_blocks.{n}.1.transformer_blocks.0.attn1.to_q.weight"
|
||||
uk = f"model.diffusion_model.output_blocks.{n}.1.transformer_blocks.0.attn1.to_k.weight"
|
||||
vk = f"model.diffusion_model.output_blocks.{n}.1.transformer_blocks.0.attn1.to_v.weight"
|
||||
atoq, atok, atov = model[qk], model[uk], model[vk]
|
||||
attn = cal_cross_attn(atoq, atok, atov, input)
|
||||
return attn
|
||||
|
||||
def main():
|
||||
file1 = Path(sys.argv[1])
|
||||
files = sys.argv[2:]
|
||||
seed = 114514
|
||||
torch.manual_seed(seed)
|
||||
model_a = load_model(file1)
|
||||
log.info(f"base: {file1.name}")
|
||||
|
||||
map_attn_a = {}
|
||||
map_rand_input = {}
|
||||
for n in range(3, 11):
|
||||
hidden_dim, embed_dim = model_a[f"model.diffusion_model.output_blocks.{n}.1.transformer_blocks.0.attn1.to_q.weight"].shape
|
||||
rand_input = torch.randn([embed_dim, hidden_dim])
|
||||
map_attn_a[n] = eval(model_a, n, rand_input)
|
||||
map_rand_input[n] = rand_input
|
||||
del model_a
|
||||
|
||||
for file2 in files:
|
||||
file2 = Path(file2)
|
||||
model_b = load_model(file2)
|
||||
sims = []
|
||||
for n in range(3, 11):
|
||||
attn_a = map_attn_a[n]
|
||||
attn_b = eval(model_b, n, map_rand_input[n])
|
||||
sim = torch.mean(torch.cosine_similarity(attn_a, attn_b))
|
||||
sims.append(sim)
|
||||
log.info(f"{file2}: {torch.mean(torch.stack(sims)) * 1e2:.2f}%")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/bin/env python
|
||||
# based on <https://towardsdatascience.com/image-color-extraction-with-python-in-4-steps-8d9370d9216e>
|
||||
|
||||
import os
|
||||
import io
|
||||
import pathlib
|
||||
import argparse
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import extcolors
|
||||
import filetype
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as patches
|
||||
import matplotlib.image as mpimg
|
||||
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
|
||||
from colormap import rgb2hex
|
||||
from PIL import Image
|
||||
from util import log
|
||||
from grid import grid
|
||||
|
||||
def color_to_df(input):
|
||||
colors_pre_list = str(input).replace('([(','').split(', (')[0:-1]
|
||||
df_rgb = [i.split('), ')[0] + ')' for i in colors_pre_list]
|
||||
df_percent = [i.split('), ')[1].replace(')','') for i in colors_pre_list]
|
||||
#convert RGB to HEX code
|
||||
df_color_up = [rgb2hex(int(i.split(", ")[0].replace("(","")),
|
||||
int(i.split(", ")[1]),
|
||||
int(i.split(", ")[2].replace(")",""))) for i in df_rgb]
|
||||
df = pd.DataFrame(zip(df_color_up, df_percent), columns = ['c_code','occurence'])
|
||||
return df
|
||||
|
||||
|
||||
def palette(img, args, output):
|
||||
size = 1024
|
||||
img.thumbnail((size, size), Image.HAMMING)
|
||||
|
||||
#crate dataframe
|
||||
colors_x = extcolors.extract_from_image(img, tolerance = args.color, limit = 13)
|
||||
df_color = color_to_df(colors_x)
|
||||
|
||||
#annotate text
|
||||
list_color = list(df_color['c_code'])
|
||||
list_precent = [int(i) for i in list(df_color['occurence'])]
|
||||
text_c = [c + ' ' + str(round(p * 100 / sum(list_precent), 1)) +'%' for c, p in zip(list_color, list_precent)]
|
||||
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(120,60), dpi=10)
|
||||
fig.set_facecolor('black')
|
||||
|
||||
#donut plot
|
||||
wedges, _text = ax1.pie(list_precent, labels= text_c, labeldistance= 1.05, colors = list_color, textprops={'fontsize': 100, 'color':'white'})
|
||||
plt.setp(wedges, width=0.3)
|
||||
|
||||
#add image in the center of donut plot
|
||||
data = np.asarray(img)
|
||||
imagebox = OffsetImage(data, zoom=2.5)
|
||||
ab = AnnotationBbox(imagebox, (0, 0))
|
||||
ax1.add_artist(ab)
|
||||
|
||||
#color palette
|
||||
x_posi, y_posi, y_posi2 = 160, -260, -260
|
||||
for c in list_color:
|
||||
if list_color.index(c) <= 5:
|
||||
y_posi += 240
|
||||
rect = patches.Rectangle((x_posi, y_posi), 540, 230, facecolor = c)
|
||||
ax2.add_patch(rect)
|
||||
ax2.text(x = x_posi + 100, y = y_posi + 140, s = c, fontdict={'fontsize': 140}, color = 'white')
|
||||
else:
|
||||
y_posi2 += 240
|
||||
rect = patches.Rectangle((x_posi + 600, y_posi2), 540, 230, facecolor = c)
|
||||
ax2.add_artist(rect)
|
||||
ax2.text(x = x_posi + 700, y = y_posi2 + 140, s = c, fontdict={'fontsize': 140}, color = 'white')
|
||||
|
||||
# add background to force layout
|
||||
fig.set_facecolor('black')
|
||||
ax2.axis('off')
|
||||
tmp = Image.new('RGB', (2000, 1400), (0, 0, 0))
|
||||
plt.imshow(tmp)
|
||||
plt.tight_layout(rect = (-0.08, -0.2, 1.18, 1.05))
|
||||
|
||||
# save image
|
||||
if output is not None:
|
||||
buf = io.BytesIO()
|
||||
plt.savefig(buf, format='png')
|
||||
pltimg = Image.open(buf)
|
||||
pltimg = pltimg.convert('RGB')
|
||||
pltimg.save(output)
|
||||
buf.close()
|
||||
log.info({ 'palette created': output })
|
||||
|
||||
plt.close()
|
||||
return
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description = 'extract image color palette')
|
||||
parser.add_argument('--color', type=int, default=20, help="color tolerance threshdold")
|
||||
parser.add_argument('--output', type=str, required=False, default='', help='folder to store images')
|
||||
parser.add_argument('--suffix', type=str, required=False, default='pallete', help='add suffix to image name')
|
||||
parser.add_argument('--grid', default=False, action='store_true', help = "create grid of images before processing")
|
||||
parser.add_argument('input', type=str, nargs='*')
|
||||
args = parser.parse_args()
|
||||
log.info({ 'palette args': vars(args) })
|
||||
if args.output != '':
|
||||
pathlib.Path(args.output).mkdir(parents = True, exist_ok = True)
|
||||
if not args.grid:
|
||||
for arg in args.input:
|
||||
if os.path.isfile(arg) and filetype.is_image(arg):
|
||||
img = Image.open(arg)
|
||||
output = os.path.join(args.output, pathlib.Path(arg).stem + '-' + args.suffix + '.jpg')
|
||||
palette(img, args, output)
|
||||
elif os.path.isdir(arg):
|
||||
for root, _dirs, files in os.walk(arg):
|
||||
for f in files:
|
||||
if filetype.is_image(os.path.join(root, f)):
|
||||
img = Image.open(os.path.join(root, f))
|
||||
output = os.path.join(args.output, pathlib.Path(f).stem + '-' + args.suffix + '.jpg')
|
||||
palette(img, args, output)
|
||||
else:
|
||||
images = []
|
||||
for arg in args.input:
|
||||
if os.path.isfile(arg) and filetype.is_image(arg):
|
||||
images.append(Image.open(arg))
|
||||
elif os.path.isdir(arg):
|
||||
for root, _dirs, files in os.walk(arg):
|
||||
for f in files:
|
||||
if filetype.is_image(os.path.join(root, f)):
|
||||
images.append(Image.open(os.path.join(root, f)))
|
||||
img = grid(images)
|
||||
output = os.path.join(args.output, args.suffix + '.jpg')
|
||||
palette(img, args, output)
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
create preview images from embeddings
|
||||
"""
|
||||
import os
|
||||
import io
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from inspect import getsourcefile
|
||||
from util import Map, log
|
||||
from sdapi import getsync, postsync
|
||||
from grid import grid
|
||||
|
||||
template = 'photo of "{name}", {suffix}, high detailed, skin texture, looking forward, facing camera, 135mm, shot on dslr, 4k, modelshoot style'
|
||||
img2img_options = Map({
|
||||
'prompt': None,
|
||||
'negative_prompt': 'cartoon, drawing, cgi, sketch, comic, disfigured, deformed',
|
||||
'init_images': [],
|
||||
'sampler_name': 'DPM2 Karras',
|
||||
'batch_size': 4,
|
||||
'n_iter': 1,
|
||||
'steps': 30,
|
||||
'cfg_scale': 6,
|
||||
'width': 512,
|
||||
'height': 512,
|
||||
'restore_faces': False
|
||||
})
|
||||
|
||||
def encode(f):
|
||||
img = Image.open(f)
|
||||
with io.BytesIO() as stream:
|
||||
img.save(stream, 'JPEG')
|
||||
values = stream.getvalue()
|
||||
encoded = base64.b64encode(values).decode()
|
||||
return encoded
|
||||
|
||||
def create_preview(name: str, suffix: str):
|
||||
options = getsync('/sdapi/v1/options')
|
||||
cmdflags = getsync('/sdapi/v1/cmd-flags')
|
||||
img2img_options['prompt'] = template.format(name = name, suffix = suffix)
|
||||
log.debug({ 'preview options': img2img_options })
|
||||
if len(img2img_options['init_images']) == 0:
|
||||
for i in range(img2img_options.batch_size):
|
||||
mask = os.path.join(os.path.dirname(getsourcefile(lambda:0)), 'preview-template'+ str(i+1) +'.jpg')
|
||||
if (not os.path.isfile(mask)):
|
||||
log.error({ 'preview': 'missing preview mask' })
|
||||
return
|
||||
img2img_options['init_images'].append(encode(mask))
|
||||
data = postsync('/sdapi/v1/img2img', img2img_options)
|
||||
if 'error' in data:
|
||||
log.error({ 'preview': data['error'], 'reason': data['reason'] })
|
||||
return
|
||||
info = Map(json.loads(data['info']))
|
||||
if not 'images' in data:
|
||||
log.error({ 'preview': 'no images' })
|
||||
return
|
||||
fn = os.path.join(cmdflags.embeddings_dir, name + '.preview.png')
|
||||
log.info({ 'preview': { 'name': fn, 'model': options.sd_model_checkpoint, 'seed': info.seed } })
|
||||
images = []
|
||||
for b64 in data['images']:
|
||||
images.append(Image.open(io.BytesIO(base64.b64decode(b64.split(",",1)[0]))))
|
||||
image = grid(images, None, square=True)
|
||||
image.save(fn)
|
||||
|
||||
if __name__ == "__main__":
|
||||
log.info({ 'preview': 'start' })
|
||||
cmdflags = getsync('/sdapi/v1/cmd-flags')
|
||||
|
||||
parser = argparse.ArgumentParser(description = 'generate embeddings previews')
|
||||
parser.add_argument('--overwrite', default = False, action='store_true', help = 'overwrite existing previews')
|
||||
parser.add_argument('input', type=str, nargs='*')
|
||||
params = parser.parse_args()
|
||||
|
||||
if len(params.input) == 0:
|
||||
files = list(Path(cmdflags.embeddings_dir).glob('*.pt'))
|
||||
else:
|
||||
files = list(os.path.join(cmdflags.embeddings_dir, a + '.pt') for a in params.input if os.path.isfile(os.path.join(cmdflags.embeddings_dir, a + '.pt')))
|
||||
candidates = [str(f) for f in files]
|
||||
candidates.sort(key=os.path.getctime, reverse=True)
|
||||
|
||||
files = []
|
||||
for f in candidates:
|
||||
fn = f.replace('.pt', '.preview.png')
|
||||
if os.path.isfile(f.replace('.pt', '.preview.png')):
|
||||
if params.overwrite:
|
||||
log.info({ 'preview add': fn })
|
||||
files.append(f)
|
||||
else:
|
||||
log.info({ 'preview skip': fn })
|
||||
else:
|
||||
log.info({ 'preview add': fn })
|
||||
files.append(f)
|
||||
|
||||
log.info({ 'preview embeddings': len(files) })
|
||||
for f in files:
|
||||
name = Path(f).stem
|
||||
create_preview(name, 'person')
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import asyncio
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from util import Map, log
|
||||
from sdapi import get, post, close
|
||||
from grid import grid
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
from generate import sd, generate
|
||||
|
||||
|
||||
default = 'sd-v15-runwayml.ckpt [cc6cb27103]'
|
||||
exclude = ['sd-v20', 'sd-v21', 'inpainting', 'pix2pix']
|
||||
# used by lora
|
||||
prompt = "photo of <keyword> <embedding>, photograph, posing, pose, high detailed, intricate, elegant, sharp focus, skin texture, looking forward, facing camera, 135mm, shot on dslr, canon 5d, 4k, modelshoot style, cinematic lighting"
|
||||
|
||||
# used by models
|
||||
prompts = [
|
||||
('photo citiscape', 'cityscape during night, photorealistic, high detailed, sharp focus, depth of field, 4k'),
|
||||
('photo car', 'photo of a sports car, high detailed, sharp focus, dslr, cinematic lighting, realistic'),
|
||||
('photo woman', 'portrait photo of beautiful woman, high detailed, dslr, 35mm'),
|
||||
('photo naked', 'full body photo of beautiful sexy naked woman, high detailed, dslr, 35mm'),
|
||||
|
||||
('photo taylor', 'portrait photo of beautiful woman taylor swift, high detailed, sharp focus, depth of field, dslr, 35mm <lora:taylor-swift:1>'),
|
||||
('photo ti-mia', 'portrait photo of beautiful woman "ti-mia", naked, high detailed, dslr, 35mm'),
|
||||
('photo ti-vlado', 'portrait photo of man "ti-vlado", high detailed, dslr, 35mm'),
|
||||
('photo lora-vlado', 'portrait photo of man vlado, high detailed, dslr, 35mm <lora:vlado-original:1>'),
|
||||
|
||||
('wlop', 'a stunning portrait of sexy teen girl in a wet t-shirt, vivid color palette, digital painting, octane render, highly detailed, particles, light effect, volumetric lighting, art by wlop'),
|
||||
('greg rutkowski', 'beautiful woman, high detailed, sharp focus, depth of field, 4k, art by greg rutkowski'),
|
||||
('carne griffiths', 'beautiful woman taylor swift, high detailed, sharp focus, depth of field, art by carne griffiths <lora:taylor-swift:1>'),
|
||||
('carne griffiths', 'man vlado, high detailed, sharp focus, depth of field, art by carne griffiths <lora:vlado-full:1>'),
|
||||
]
|
||||
|
||||
options = Map({
|
||||
'generate': {
|
||||
'restore_faces': True,
|
||||
'prompt': '',
|
||||
'negative_prompt': 'digital art, cgi, render, foggy, blurry, blurred, duplicate, ugly, mutilated, mutation, mutated, out of frame, bad anatomy, disfigured, deformed, censored, low res, low resolution, watermark, text, poorly drawn face, poorly drawn hands, signature',
|
||||
'steps': 20,
|
||||
'batch_size': 2,
|
||||
'n_iter': 1,
|
||||
'seed': -1,
|
||||
'sampler_name': 'UniPC',
|
||||
'cfg_scale': 6,
|
||||
'width': 512,
|
||||
'height': 512,
|
||||
},
|
||||
'format': '.jpg',
|
||||
'paths': {
|
||||
"root": "/mnt/c/Users/mandi/OneDrive/Generative/Generate",
|
||||
"generate": "image",
|
||||
"upscale": "upscale",
|
||||
"grid": "grid",
|
||||
},
|
||||
'options': {
|
||||
"sd_model_checkpoint": "sd-v15-runwayml",
|
||||
"sd_vae": "vae-ft-mse-840000-ema-pruned.ckpt",
|
||||
},
|
||||
'lora': {
|
||||
'strength': 0.9,
|
||||
},
|
||||
'hypernetwork': {
|
||||
'keyword': 'beautiful sexy woman',
|
||||
'strength': 1.0,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
async def models(params):
|
||||
global sd
|
||||
data = await get('/sdapi/v1/sd-models')
|
||||
all = [m['title'] for m in data]
|
||||
models = []
|
||||
excluded = []
|
||||
for m in all: # loop through all registered models
|
||||
ok = True
|
||||
for e in exclude: # check if model is excluded
|
||||
if e in m:
|
||||
excluded.append(m)
|
||||
ok = False
|
||||
break
|
||||
if ok:
|
||||
short = m.split(' [')[0]
|
||||
short = short.replace('.ckpt', '').replace('.safetensors', '')
|
||||
models.append(short)
|
||||
if len(params.input) > 0: # check if model is included in cmd line
|
||||
filtered = []
|
||||
for m in params.input:
|
||||
if m in models:
|
||||
filtered.append(m)
|
||||
else:
|
||||
log.error({ 'model not found': m })
|
||||
return
|
||||
models = filtered
|
||||
log.info({ 'models preview' })
|
||||
log.info({ 'models': len(models), 'excluded': len(excluded) })
|
||||
cmdflags = await get('/sdapi/v1/cmd-flags')
|
||||
opt = await get('/sdapi/v1/options')
|
||||
if params.output != '':
|
||||
dir = params.output
|
||||
else:
|
||||
dir = os.path.abspath(os.path.join(cmdflags['hypernetwork_dir'], '..', 'Stable-diffusion'))
|
||||
log.info({ 'output directory': dir })
|
||||
log.info({ 'total jobs': len(models) * options.generate.batch_size, 'per-model': options.generate.batch_size })
|
||||
log.info(json.dumps(options, indent=2))
|
||||
for model in models:
|
||||
fn = os.path.join(dir, os.path.basename(model) + options.format)
|
||||
if os.path.exists(fn) and len(params.input) == 0: # if model preview exists and not manually included
|
||||
log.info({ 'model preview exists': model })
|
||||
continue
|
||||
log.info({ 'model load': model })
|
||||
|
||||
opt['sd_model_checkpoint'] = model
|
||||
await post('/sdapi/v1/options', opt)
|
||||
opt = await get('/sdapi/v1/options')
|
||||
images = []
|
||||
labels = []
|
||||
t0 = time.time()
|
||||
for label, prompt in prompts:
|
||||
options.generate.prompt = prompt
|
||||
log.info({ 'model generating': model, 'label': label, 'prompt': options.generate.prompt })
|
||||
data = await generate(options = options, quiet=True)
|
||||
if 'image' in data:
|
||||
for img in data['image']:
|
||||
images.append(img)
|
||||
labels.append(label)
|
||||
else:
|
||||
log.error({ 'model': model, 'error': data })
|
||||
t1 = time.time()
|
||||
image = grid(images = images, labels = labels, border = 8)
|
||||
log.info({ 'saving preview': fn, 'images': len(images), 'size': [image.width, image.height] })
|
||||
image.save(fn)
|
||||
t = t1 - t0
|
||||
its = 1.0 * options.generate.steps * len(images) / t
|
||||
log.info({ 'model preview created': model, 'image': fn, 'images': len(images), 'grid': [image.width, image.height], 'time': round(t, 2), 'its': round(its, 2) })
|
||||
|
||||
opt = await get('/sdapi/v1/options')
|
||||
if opt['sd_model_checkpoint'] != default and not params.fixed:
|
||||
log.info({ 'model set default': default })
|
||||
opt['sd_model_checkpoint'] = default
|
||||
await post('/sdapi/v1/options', opt)
|
||||
|
||||
|
||||
async def lora(params):
|
||||
cmdflags = await get('/sdapi/v1/cmd-flags')
|
||||
dir = cmdflags['lora_dir']
|
||||
if not os.path.exists(dir):
|
||||
log.error({ 'lora directory not found': dir })
|
||||
return
|
||||
models1 = [f for f in Path(dir).glob('*.safetensors')]
|
||||
models2 = [f for f in Path(dir).glob('*.ckpt')]
|
||||
models = [f.stem for f in models1 + models2]
|
||||
log.info({ 'loras': len(models) })
|
||||
for model in models:
|
||||
fn = os.path.join(dir, model + options.format)
|
||||
if os.path.exists(fn) and len(params.input) == 0: # if model preview exists and not manually included
|
||||
log.info({ 'lora preview exists': model })
|
||||
continue
|
||||
images = []
|
||||
labels = []
|
||||
t0 = time.time()
|
||||
import re
|
||||
keywords = re.sub('\d', '', model)
|
||||
keywords = keywords.replace('-v', ' ').replace('-', ' ').strip().split(' ')
|
||||
keyword = '\"' + '\" \"'.join(keywords) + '\"'
|
||||
options.generate.prompt = prompt.replace('<keyword>', keyword)
|
||||
options.generate.prompt = options.generate.prompt.replace('<embedding>', '')
|
||||
options.generate.prompt += f' <lora:{model}:{options.lora.strength}>'
|
||||
log.info({ 'lora generating': model, 'keyword': keyword, 'prompt': options.generate.prompt })
|
||||
data = await generate(options = options, quiet=True)
|
||||
if 'image' in data:
|
||||
for img in data['image']:
|
||||
images.append(img)
|
||||
labels.append(keyword)
|
||||
else:
|
||||
log.error({ 'lora': model, 'keyword': keyword, 'error': data })
|
||||
t1 = time.time()
|
||||
image = grid(images = images, labels = labels, border = 8)
|
||||
image.save(fn)
|
||||
t = t1 - t0
|
||||
its = 1.0 * options.generate.steps * len(images) / t
|
||||
log.info({ 'lora preview created': model, 'image': fn, 'images': len(images), 'grid': [image.width, image.height], 'time': round(t, 2), 'its': round(its, 2) })
|
||||
|
||||
|
||||
async def hypernetwork(params):
|
||||
cmdflags = await get('/sdapi/v1/cmd-flags')
|
||||
dir = cmdflags['hypernetwork_dir']
|
||||
if not os.path.exists(dir):
|
||||
log.error({ 'hypernetwork directory not found': dir })
|
||||
return
|
||||
models = [f.stem for f in Path(dir).glob('*.pt')]
|
||||
log.info({ 'loras': len(models) })
|
||||
for model in models:
|
||||
fn = os.path.join(dir, model + options.format)
|
||||
if os.path.exists(fn) and len(params.input) == 0: # if model preview exists and not manually included
|
||||
log.info({ 'hypernetwork preview exists': model })
|
||||
continue
|
||||
images = []
|
||||
labels = []
|
||||
t0 = time.time()
|
||||
keyword = options.hypernetwork.keyword
|
||||
options.generate.prompt = prompt.replace('<keyword>', options.hypernetwork.keyword)
|
||||
options.generate.prompt = options.generate.prompt.replace('<embedding>', '')
|
||||
options.generate.prompt = f' <hypernet:{model}:{options.hypernetwork.strength}> ' + options.generate.prompt
|
||||
log.info({ 'hypernetwork generating': model, 'keyword': keyword, 'prompt': options.generate.prompt })
|
||||
data = await generate(options = options, quiet=True)
|
||||
if 'image' in data:
|
||||
for img in data['image']:
|
||||
images.append(img)
|
||||
labels.append(keyword)
|
||||
else:
|
||||
log.error({ 'hypernetwork': model, 'keyword': keyword, 'error': data })
|
||||
t1 = time.time()
|
||||
image = grid(images = images, labels = labels, border = 8)
|
||||
image.save(fn)
|
||||
t = t1 - t0
|
||||
its = 1.0 * options.generate.steps * len(images) / t
|
||||
log.info({ 'hypernetwork preview created': model, 'image': fn, 'images': len(images), 'grid': [image.width, image.height], 'time': round(t, 2), 'its': round(its, 2) })
|
||||
|
||||
|
||||
async def create_previews(params):
|
||||
await models(params)
|
||||
await lora(params)
|
||||
await hypernetwork(params)
|
||||
await close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description = 'generate model previews')
|
||||
parser.add_argument('--output', type = str, default = '', required = False, help = 'output directory')
|
||||
parser.add_argument('--fixed', default = False, action='store_true', help = "do not change model")
|
||||
parser.add_argument('input', type = str, nargs = '*')
|
||||
params = parser.parse_args()
|
||||
asyncio.run(create_previews(params))
|
||||
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
@@ -0,0 +1,500 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
process people images
|
||||
- check image resolution
|
||||
- runs detection of face and body
|
||||
- extracts crop and performs checks:
|
||||
- visible: is face or body detected
|
||||
- in frame: for face based on box, for body based on number of visible keypoints
|
||||
- resolution: is cropped image still of sufficient resolution
|
||||
- optionaly upsample and restore face quality
|
||||
- blur: is image sharp enough
|
||||
- dynamic range: is image bright enough
|
||||
- similarity: compares image to all previously processed images to see if its unique enough
|
||||
- images are resized and optionally squared
|
||||
- face additionally runs through semantic segmentation to remove background
|
||||
- if image passes checks
|
||||
image padded and saved as extracted image
|
||||
- body requires that face is detected and in-frame,
|
||||
but does not have to pass all other checks as body performs its own checks
|
||||
- runs clip interrogation on extracted images to generate filewords
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import math
|
||||
import base64
|
||||
import pathlib
|
||||
import argparse
|
||||
import logging
|
||||
import filetype
|
||||
import numpy as np
|
||||
import mediapipe as mp
|
||||
from PIL import Image, ImageOps
|
||||
from skimage.metrics import structural_similarity as ssim
|
||||
from scipy.stats import beta
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__)))
|
||||
|
||||
from util import log, Map
|
||||
from sdapi import postsync
|
||||
|
||||
|
||||
params = Map({
|
||||
# general settings, do not modify
|
||||
'src': '', # source folder
|
||||
'dst': '', # destination folder
|
||||
'clear_dst': True, # remove all files from destination at the start
|
||||
'format': '.jpg', # image format
|
||||
'target_size': 512, # target resolution
|
||||
'square_images': True, # should output images be squared
|
||||
'segmentation_model': 0, # segmentation model 0/general 1/landscape
|
||||
'segmentation_background': (192, 192, 192), # segmentation background color
|
||||
'blur_samplesize': 60, # sample size to use for blur detection
|
||||
'similarity_size': 64, # base similarity detection on reduced images
|
||||
# original image processing settings
|
||||
'keep_original': False, # keep original image
|
||||
# face processing settings
|
||||
'extract_face': False, # extract face from image
|
||||
'face_score': 0.7, # min face detection score
|
||||
'face_pad': 0.1, # pad face image percentage
|
||||
'face_model': 1, # which face model to use 0/close-up 1/standard
|
||||
'face_blur': False, # check for body blur
|
||||
'face_blur_score': 1.5, # max score for face blur detection
|
||||
'face_range': False, # check for body blur
|
||||
'face_range_score': 0.15, # min score for face dynamic range detection
|
||||
'face_restore': False, # attempt to restore face quality
|
||||
'face_upscale': False, # attempt to scale small faces
|
||||
'face_segmentation': False, # segmentation enabled
|
||||
# body processing settings
|
||||
'extract_body': False, # extract body from image
|
||||
'body_score': 0.9, # min body detection score
|
||||
'body_visibility': 0.5, # min visibility score for each detected body part
|
||||
'body_parts': 15, # min number of detected body parts with sufficient visibility
|
||||
'body_pad': 0.2, # pad body image percentage
|
||||
'body_model': 2, # body model to use 0/low 1/medium 2/high
|
||||
'body_blur': False, # check for body blur
|
||||
'body_blur_score': 1.8, # max score for body blur detection
|
||||
'body_range': False, # check for body blur
|
||||
'body_range_score': 0.15, # min score for body dynamic range detection
|
||||
'body_segmentation': False, # segmentation enabled
|
||||
# similarity detection settings
|
||||
'similarity_score': 0.8, # maximum similarity score before image is discarded
|
||||
# interrogate settings
|
||||
'interrogate_model': ['clip', 'deepdanbooru'], # interrogate models
|
||||
'interrogate_captions': True, # write captions to file
|
||||
'tag_limit': 5, # number of tags to extract
|
||||
})
|
||||
face_model = None
|
||||
body_model = None
|
||||
segmentation_model = None
|
||||
|
||||
|
||||
def detect_blur(image):
|
||||
# based on <https://github.com/karthik9319/Blur-Detection/>
|
||||
bw = ImageOps.grayscale(image)
|
||||
cx, cy = image.size[0] // 2, image.size[1] // 2
|
||||
fft = np.fft.fft2(bw)
|
||||
fftShift = np.fft.fftshift(fft)
|
||||
fftShift[cy - params.blur_samplesize: cy + params.blur_samplesize, cx - params.blur_samplesize: cx + params.blur_samplesize] = 0
|
||||
fftShift = np.fft.ifftshift(fftShift)
|
||||
recon = np.fft.ifft2(fftShift)
|
||||
magnitude = np.log(np.abs(recon))
|
||||
mean = round(np.mean(magnitude), 2)
|
||||
return mean
|
||||
|
||||
|
||||
def detect_dynamicrange(image):
|
||||
# based on <https://towardsdatascience.com/measuring-enhancing-image-quality-attributes-234b0f250e10>
|
||||
data = np.asarray(image)
|
||||
image = np.float32(data)
|
||||
RGB = [0.299, 0.587, 0.114]
|
||||
height, width = image.shape[:2]
|
||||
brightness_image = np.sqrt(image[..., 0] ** 2 * RGB[0] + image[..., 1] ** 2 * RGB[1] + image[..., 2] ** 2 * RGB[2])
|
||||
hist, _ = np.histogram(brightness_image, bins=256, range=(0, 255))
|
||||
img_brightness_pmf = hist / (height * width)
|
||||
dist = beta(2, 2)
|
||||
ys = dist.pdf(np.linspace(0, 1, 256))
|
||||
ref_pmf = ys / np.sum(ys)
|
||||
dot_product = np.dot(ref_pmf, img_brightness_pmf)
|
||||
squared_dist_a = np.sum(ref_pmf ** 2)
|
||||
squared_dist_b = np.sum(img_brightness_pmf ** 2)
|
||||
res = dot_product / math.sqrt(squared_dist_a * squared_dist_b)
|
||||
return round(res, 2)
|
||||
|
||||
|
||||
images = []
|
||||
def detect_simmilar(image):
|
||||
img = image.resize((params.similarity_size, params.similarity_size))
|
||||
img = ImageOps.grayscale(img)
|
||||
data = np.array(img)
|
||||
similarity = 0
|
||||
for i in images:
|
||||
val = ssim(data, i, data_range=255, channel_axis=None, gradient=False, full=False)
|
||||
if val > similarity:
|
||||
similarity = val
|
||||
images.append(data)
|
||||
return similarity
|
||||
|
||||
|
||||
def segmentation(image):
|
||||
global segmentation_model
|
||||
if segmentation_model is None:
|
||||
segmentation_model = mp.solutions.selfie_segmentation.SelfieSegmentation(model_selection=params.segmentation_model)
|
||||
data = np.array(image)
|
||||
results = segmentation_model.process(data)
|
||||
condition = np.stack((results.segmentation_mask,) * 3, axis=-1) > 0.1
|
||||
background = np.zeros(data.shape, dtype=np.uint8)
|
||||
background[:] = params.segmentation_background
|
||||
data = np.where(condition, data, background) # consider using a joint bilateral filter instead of pure combine
|
||||
segmented = Image.fromarray(data)
|
||||
return segmented
|
||||
|
||||
|
||||
def extract_face(img):
|
||||
if not params.extract_face:
|
||||
return None, True
|
||||
if img.mode == 'RGBA':
|
||||
img = img.convert('RGB')
|
||||
scale = max(img.size[0], img.size[1]) / params.target_size
|
||||
resized = img.copy()
|
||||
resized.thumbnail((params.target_size, params.target_size), Image.HAMMING)
|
||||
|
||||
global face_model
|
||||
if face_model is None:
|
||||
face_model = mp.solutions.face_detection.FaceDetection(min_detection_confidence=params.face_score, model_selection=params.face_model)
|
||||
results = face_model.process(np.array(resized))
|
||||
if results.detections is None:
|
||||
return None, False
|
||||
box = results.detections[0].location_data.relative_bounding_box
|
||||
if box.xmin < 0 or box.ymin < 0 or (box.width - box.xmin) > 1 or (box.height - box.ymin) > 1:
|
||||
log.info({ 'process face skip': 'out of frame' })
|
||||
return None, False
|
||||
x = (box.xmin - params.face_pad / 2) * resized.width
|
||||
y = (box.ymin - params.face_pad / 2)* resized.height
|
||||
w = (box.width + params.face_pad) * resized.width
|
||||
h = (box.height + params.face_pad) * resized.height
|
||||
cx = x + w / 2
|
||||
cy = y + h / 2
|
||||
l = max(w, h) / 2
|
||||
square = [scale * (cx - l), scale * (cy - l), scale * (cx + l), scale * (cy + l)]
|
||||
square = [max(square[0], 0), max(square[1], 0), min(square[2], img.width), min(square[3], img.height)]
|
||||
cropped = img.crop(tuple(square))
|
||||
|
||||
upscale = 1
|
||||
if params.face_restore or params.face_upscale:
|
||||
if (cropped.size[0] < params.target_size or cropped.size[1] < params.target_size) and params.face_upscale:
|
||||
upscale = 2
|
||||
kwargs = Map({
|
||||
'image': encode(cropped),
|
||||
'upscaler_1': 'SwinIR_4x' if params.face_upscale else None,
|
||||
'codeformer_visibility': 1.0 if params.face_restore else 0.0,
|
||||
'codeformer_weight': 0.15 if params.face_restore else 0.0,
|
||||
'upscaling_resize': upscale,
|
||||
})
|
||||
original = [cropped.size[0], cropped.size[1]]
|
||||
res = postsync('/sdapi/v1/extra-single-image', kwargs)
|
||||
if 'image' not in res:
|
||||
log.error({ 'process face': 'upscale failed' })
|
||||
raise ValueError('upscale failed')
|
||||
cropped = Image.open(io.BytesIO(base64.b64decode(res['image'])))
|
||||
kwargs.image = [cropped.size[0], cropped.size[1]]
|
||||
upscaled = [cropped.size[0], cropped.size[1]]
|
||||
upscale = False if upscale == 1 else { 'original': original, 'upscaled': upscaled }
|
||||
log.info({ 'process face restore': params.face_restore, 'upscale': upscale })
|
||||
|
||||
if cropped.size[0] < params.target_size and cropped.size[1] < params.target_size:
|
||||
log.info({ 'process face skip': 'low resolution', 'size': [cropped.size[0], cropped.size[1]] })
|
||||
return None, True
|
||||
cropped.thumbnail((params.target_size, params.target_size), Image.HAMMING)
|
||||
|
||||
if params.square_images:
|
||||
squared = Image.new('RGB', (params.target_size, params.target_size))
|
||||
squared.paste(cropped, ((params.target_size - cropped.width) // 2, (params.target_size - cropped.height) // 2))
|
||||
if params.face_segmentation:
|
||||
squared = segmentation(squared)
|
||||
else:
|
||||
squared = cropped
|
||||
|
||||
if params.face_blur:
|
||||
blur = detect_blur(squared)
|
||||
if blur > params.face_blur_score:
|
||||
log.info({ 'process face skip': 'blur check fail', 'blur': blur })
|
||||
return None, True
|
||||
else:
|
||||
log.debug({ 'process face blur': blur })
|
||||
|
||||
if params.face_range:
|
||||
range = detect_dynamicrange(squared)
|
||||
if range < params.face_range_score:
|
||||
log.info({ 'process face skip': 'dynamic range check fail', 'range': range })
|
||||
return None, True
|
||||
else:
|
||||
log.debug({ 'process face dynamic range': range })
|
||||
|
||||
similarity = detect_simmilar(squared)
|
||||
if similarity > params.similarity_score:
|
||||
log.info({ 'process face skip': 'similarity check fail', 'score': round(similarity, 2) })
|
||||
return None, True
|
||||
|
||||
return squared, True
|
||||
|
||||
|
||||
def extract_body(img):
|
||||
if not params.extract_body:
|
||||
return None, True
|
||||
if img.mode == 'RGBA':
|
||||
img = img.convert('RGB')
|
||||
scale = max(img.size[0], img.size[1]) / params.target_size
|
||||
resized = img.copy()
|
||||
resized.thumbnail((params.target_size, params.target_size), Image.HAMMING)
|
||||
|
||||
global body_model
|
||||
if body_model is None:
|
||||
body_model = mp.solutions.pose.Pose(static_image_mode=True, min_detection_confidence=params.body_score, model_complexity=params.body_model)
|
||||
results = body_model.process(np.array(resized))
|
||||
if results.pose_landmarks is None:
|
||||
return None, False
|
||||
x = [resized.width * (i.x - params.body_pad / 2) for i in results.pose_landmarks.landmark if i.visibility > params.body_visibility]
|
||||
y = [resized.height * (i.y - params.body_pad / 2) for i in results.pose_landmarks.landmark if i.visibility > params.body_visibility]
|
||||
if len(x) < params.body_parts:
|
||||
log.info({ 'process body skip': 'insufficient body parts', 'detected': len(x) })
|
||||
return None, True
|
||||
w = max(x) - min(x) + resized.width * params.body_pad
|
||||
h = max(y) - min(y) + resized.height * params.body_pad
|
||||
cx = min(x) + w / 2
|
||||
cy = min(y) + h / 2
|
||||
l = max(w, h) / 2
|
||||
square = [scale * (cx - l), scale * (cy - l), scale * (cx + l), scale * (cy + l)]
|
||||
square = [max(square[0], 0), max(square[1], 0), min(square[2], img.width), min(square[3], img.height)]
|
||||
cropped = img.crop(tuple(square))
|
||||
if cropped.size[0] < params.target_size and cropped.size[1] < params.target_size:
|
||||
log.info({ 'process body skip': 'low resolution', 'size': [cropped.size[0], cropped.size[1]] })
|
||||
return None, True
|
||||
cropped.thumbnail((params.target_size, params.target_size), Image.HAMMING)
|
||||
|
||||
if params.square_images:
|
||||
squared = Image.new('RGB', (params.target_size, params.target_size))
|
||||
squared.paste(cropped, ((params.target_size - cropped.width) // 2, (params.target_size - cropped.height) // 2))
|
||||
if params.body_segmentation:
|
||||
squared = segmentation(squared)
|
||||
else:
|
||||
squared = cropped
|
||||
|
||||
if params.body_blur:
|
||||
blur = detect_blur(squared)
|
||||
if blur > params.body_blur_score:
|
||||
log.info({ 'process body skip': 'blur check fail', 'blur': blur })
|
||||
return None, True
|
||||
else:
|
||||
log.debug({ 'process body blur': blur })
|
||||
|
||||
if params.body_range:
|
||||
range = detect_dynamicrange(squared)
|
||||
if range < params.body_range_score:
|
||||
log.info({ 'process body skip': 'dynamic range check fail', 'range': range })
|
||||
return None, True
|
||||
else:
|
||||
log.debug({ 'process body dynamic range': range })
|
||||
|
||||
similarity = detect_simmilar(squared)
|
||||
if similarity > params.similarity_score:
|
||||
log.info({ 'process body skip': 'similarity check fail', 'score': round(similarity, 2) })
|
||||
return None, True
|
||||
|
||||
return squared, True
|
||||
|
||||
|
||||
def save_original(img):
|
||||
if img.mode == 'RGBA':
|
||||
img = img.convert('RGB')
|
||||
resized = img.copy()
|
||||
resized.thumbnail((params.target_size, params.target_size), Image.HAMMING)
|
||||
if params.square_images:
|
||||
squared = Image.new('RGB', (params.target_size, params.target_size))
|
||||
squared.paste(resized, ((params.target_size - resized.width) // 2, (params.target_size - resized.height) // 2))
|
||||
else:
|
||||
squared = resized
|
||||
return squared
|
||||
|
||||
|
||||
def encode(img):
|
||||
with io.BytesIO() as stream:
|
||||
img.save(stream, 'JPEG')
|
||||
values = stream.getvalue()
|
||||
encoded = base64.b64encode(values).decode()
|
||||
return encoded
|
||||
|
||||
|
||||
def interrogate(img, fn, intag = None):
|
||||
if len(params.interrogate_model) == 0:
|
||||
return
|
||||
caption = ''
|
||||
tags = []
|
||||
for model in params.interrogate_model:
|
||||
json = Map({ 'image': encode(img), 'model': model })
|
||||
res = postsync('/sdapi/v1/interrogate', json)
|
||||
if model == 'clip':
|
||||
caption = res.caption if 'caption' in res else ''
|
||||
caption = caption.split(',')[0].replace('a ', '')
|
||||
if intag is not None:
|
||||
caption = intag + ', ' + caption
|
||||
if model == 'deepdanbooru':
|
||||
tag = res.caption if 'caption' in res else ''
|
||||
tags = tag.split(',')
|
||||
tags = [t.replace('(', '').replace(')', '').replace('\\', '').split(':')[0].strip() for t in tags]
|
||||
if intag is not None:
|
||||
for t in intag.split(',')[::-1]:
|
||||
tags.insert(0, t.strip())
|
||||
if params.interrogate_captions:
|
||||
file = fn.replace(params.format, '.txt')
|
||||
f = open(file, 'w')
|
||||
f.write(caption)
|
||||
f.close()
|
||||
pos = 0 if len(tags) == 0 else 1
|
||||
tags.insert(pos, caption.split(' ')[1])
|
||||
if len(tags) > params.tag_limit:
|
||||
tags = tags[:params.tag_limit]
|
||||
log.info({ 'interrogate': caption, 'tags': tags })
|
||||
return caption, tags
|
||||
|
||||
|
||||
i = {}
|
||||
metadata = Map({})
|
||||
|
||||
# entry point when used as module
|
||||
def process_file(f: str, dst: str = None, preview: bool = False, offline: bool = False, txt = None, tag = None, opts = []):
|
||||
def save(img, f, what):
|
||||
i[what] = i.get(what, 0) + 1
|
||||
if dst is None:
|
||||
dir = os.path.dirname(f)
|
||||
else:
|
||||
dir = dst
|
||||
base = os.path.basename(f).split('.')[0]
|
||||
parent = os.path.basename(pathlib.Path(dir))
|
||||
basename = str(i[what]).rjust(3, '0') + '-' + what + '-' + base
|
||||
fn = basename + params.format
|
||||
# log.debug({ 'save': fn })
|
||||
caption = ''
|
||||
tags = ''
|
||||
if not preview:
|
||||
img.save(os.path.join(dir, fn))
|
||||
if not offline:
|
||||
caption, tags = interrogate(img, os.path.join(dir, fn), tag)
|
||||
metadata[os.path.join(parent, basename)] = { 'caption': caption, 'tags': ','.join(tags) }
|
||||
return fn
|
||||
|
||||
# overrides
|
||||
if len(opts) > 0:
|
||||
params.keep_original = True if 'original' in opts else False
|
||||
params.extract_face = True if 'face' in opts else False
|
||||
params.extract_body = True if 'body' in opts else False
|
||||
params.face_blur = True if 'blur' in opts else False
|
||||
params.body_blur = True if 'blur' in opts else False
|
||||
params.face_range = True if 'range' in opts else False
|
||||
params.body_range = True if 'range' in opts else False
|
||||
params.face_upscale = True if 'upscale' in opts else False
|
||||
params.face_restore = True if 'restore' in opts else False
|
||||
|
||||
log.info({ 'processing': f })
|
||||
try:
|
||||
image = Image.open(f)
|
||||
except Exception as err:
|
||||
log.error({ 'image': f, 'error': err })
|
||||
return 0, {}
|
||||
|
||||
image = ImageOps.exif_transpose(image) # rotate image according to EXIF orientation
|
||||
if txt is not None:
|
||||
params.interrogate_captions = txt
|
||||
|
||||
if image.width < 512 or image.height < 512:
|
||||
log.info({ 'process skip': 'low resolution', 'resolution': [image.width, image.height] })
|
||||
return 0, {}
|
||||
log.debug({ 'resolution': [image.width, image.height], 'mp': round((image.width * image.height) / 1024 / 1024, 1) })
|
||||
|
||||
face, ok = extract_face(image)
|
||||
if face is not None:
|
||||
fn = save(face, f, 'face')
|
||||
log.info({ 'extract face': fn })
|
||||
else:
|
||||
log.debug({ 'no face': f })
|
||||
|
||||
if not ok:
|
||||
return 0, {}
|
||||
|
||||
body, ok = extract_body(image)
|
||||
if body is not None:
|
||||
fn = save(body, f, 'body')
|
||||
log.info({ 'extract body': fn })
|
||||
else:
|
||||
log.debug({ 'no body': f })
|
||||
|
||||
if params.keep_original:
|
||||
resized = save_original(image)
|
||||
fn = save(resized, f, 'original')
|
||||
log.info({ 'original': fn })
|
||||
|
||||
image.close()
|
||||
return i, metadata
|
||||
|
||||
def process_images(src: str, dst: str, args = None):
|
||||
params.src = src
|
||||
params.dst = dst
|
||||
if args is not None:
|
||||
params.update(args)
|
||||
log.info({ 'processing': params })
|
||||
if not os.path.isdir(src):
|
||||
log.error({ 'process': 'not a folder', 'src': src })
|
||||
else:
|
||||
if os.path.isdir(dst) and params.clear_dst:
|
||||
log.info({ 'clear dst': dst })
|
||||
i = [os.path.join(dst, f) for f in os.listdir(dst) if os.path.isfile(os.path.join(dst, f)) and filetype.is_image(os.path.join(dst, f))]
|
||||
for f in i:
|
||||
os.remove(f)
|
||||
pathlib.Path(dst).mkdir(parents=True, exist_ok=True)
|
||||
for root, _sub_dirs, files in os.walk(src):
|
||||
for f in files:
|
||||
i, _metadata = process_file(os.path.join(root, f), dst)
|
||||
return i
|
||||
|
||||
|
||||
def unload_models():
|
||||
global face_model
|
||||
if face_model is not None:
|
||||
face_model = None
|
||||
global body_model
|
||||
if body_model is not None:
|
||||
body_model = None
|
||||
global segmentation_model
|
||||
if segmentation_model is not None:
|
||||
segmentation_model = None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# log.setLevel(logging.DEBUG)
|
||||
parser = argparse.ArgumentParser(description = 'dataset processor')
|
||||
parser.add_argument('--output', type=str, required=True, help='folder to store images')
|
||||
parser.add_argument('--preview', default=False, action='store_true', help = "run processing but do not store results")
|
||||
parser.add_argument('--offline', default=False, action='store_true', help = "run only processing steps that do not require running server")
|
||||
parser.add_argument('--debug', default=False, action='store_true', help = "enable debug logging")
|
||||
parser.add_argument('input', type=str, nargs='*')
|
||||
args = parser.parse_args()
|
||||
params.dst = args.output
|
||||
if args.debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.debug({ 'debug': True })
|
||||
log.info({ 'processing': params })
|
||||
if not os.path.exists(params.dst) and not args.preview:
|
||||
pathlib.Path(params.dst).mkdir(parents=True, exist_ok=True)
|
||||
files = []
|
||||
for loc in args.input:
|
||||
if os.path.isfile(loc):
|
||||
files.append(loc)
|
||||
elif os.path.isdir(loc):
|
||||
for root, _sub_dirs, dir in os.walk(loc):
|
||||
for f in dir:
|
||||
files.append(os.path.join(root, f))
|
||||
for f in files:
|
||||
process_file(f, params.dst, args.preview, args.offline)
|
||||
log.info({ 'processed': i, 'inputs': len(files) })
|
||||
# print(json.dumps(metadata, indent=2))
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
generate prompt ideas
|
||||
model from: <https://huggingface.co/FredZhang7/distilgpt2-stable-diffusion-v2>
|
||||
"""
|
||||
|
||||
import logging
|
||||
import argparse
|
||||
from transformers import GPT2Tokenizer, GPT2LMHeadModel
|
||||
from util import log
|
||||
|
||||
|
||||
tokenizer = None
|
||||
model = None
|
||||
|
||||
|
||||
def prompt(text: str, temp: float = 0.9, top: int = 8, penalty: float = 1.2, alpha: float = 0.6, num: int = 5, length: int = 80):
|
||||
global tokenizer, model # pylint: disable=global-statement
|
||||
if tokenizer is None:
|
||||
tokenizer = GPT2Tokenizer.from_pretrained('distilgpt2')
|
||||
tokenizer.add_special_tokens({'pad_token': '[PAD]'})
|
||||
if model is None:
|
||||
model = GPT2LMHeadModel.from_pretrained('FredZhang7/distilgpt2-stable-diffusion-v2')
|
||||
input_ids = tokenizer(text, return_tensors='pt').input_ids
|
||||
output = model.generate(input_ids,
|
||||
do_sample = True,
|
||||
temperature = temp,
|
||||
top_k = top,
|
||||
max_length = length,
|
||||
num_return_sequences = num,
|
||||
repetition_penalty = penalty,
|
||||
penalty_alpha = alpha,
|
||||
no_repeat_ngram_size = 1,
|
||||
early_stopping = True
|
||||
)
|
||||
outputs = []
|
||||
for i in range(len(output)):
|
||||
outputs.append(tokenizer.decode(output[i], skip_special_tokens=True))
|
||||
return outputs
|
||||
|
||||
|
||||
if __name__ == "__main__": # create & train test embedding when used from cli
|
||||
log.info({ 'idea': 'generate prompts' })
|
||||
parser = argparse.ArgumentParser(description='idea: generate prompts')
|
||||
parser.add_argument("--temp", type = float, default = 0.9, required = False, help = "higher temperature produces more diverse results with a higher risk of less coherent text, default: %(default)s")
|
||||
parser.add_argument("--top", type = int, default = 8, required = False, help = "number of tokens to sample from at each step, default: %(default)s")
|
||||
parser.add_argument("--penalty", type = float, default = 1.2, required = False, help = "penalty value for each repetition of a token, default: %(default)s")
|
||||
parser.add_argument("--alpha", type = float, default = 0.6, required = False, help = "penalty alpha value, default: %(default)s")
|
||||
parser.add_argument("--num", type = int, default = 10, required = False, help = "number of results to generate, default: %(default)s")
|
||||
parser.add_argument("--length", type = int, default = 85, required = False, help = "maximum number of output tokens, default: %(default)s")
|
||||
parser.add_argument('--debug', default = False, action='store_true', help = "print extra debug information, default: %(default)s")
|
||||
parser.add_argument('text', type = str, nargs = '*')
|
||||
params = parser.parse_args()
|
||||
if params.debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.debug({ 'debug': True })
|
||||
log.debug({ 'args': params.__dict__ })
|
||||
sentence = ' '.join(params.text)
|
||||
res = prompt(text = sentence, temp = params.temp, top = params.top, penalty = params.penalty, alpha = params.alpha, num = params.num, length = params.length)
|
||||
log.info({ 'ideas for': sentence })
|
||||
for line in res:
|
||||
log.info(line)
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
use microsoft promptist to beautify prompt
|
||||
- <https://huggingface.co/spaces/microsoft/Promptist>
|
||||
"""
|
||||
|
||||
import sys
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||
from util import log
|
||||
|
||||
|
||||
def load_prompter():
|
||||
model = AutoModelForCausalLM.from_pretrained("microsoft/Promptist") # pylint: disable=redefined-outer-name
|
||||
tokenizer = AutoTokenizer.from_pretrained("gpt2") # pylint: disable=redefined-outer-name
|
||||
tokenizer.pad_token = tokenizer.eos_token
|
||||
tokenizer.padding_side = "left"
|
||||
return model, tokenizer
|
||||
|
||||
|
||||
model, tokenizer = load_prompter()
|
||||
|
||||
|
||||
def beautify(plain_text):
|
||||
input_ids = tokenizer(plain_text.strip() + " Rephrase:", return_tensors = "pt").input_ids
|
||||
eos_id = tokenizer.eos_token_id
|
||||
outputs = model.generate(input_ids, do_sample = False, max_new_tokens = 75, num_beams = 8, num_return_sequences = 8, eos_token_id = eos_id, pad_token_id = eos_id, length_penalty = -1.0)
|
||||
output_texts = tokenizer.batch_decode(outputs, skip_special_tokens = True)
|
||||
texts = []
|
||||
for output_text in output_texts:
|
||||
texts.append(output_text.replace(plain_text+" Rephrase:", "").strip())
|
||||
longest = max(texts, key = len)
|
||||
log.info({ 'beautified': longest })
|
||||
return longest
|
||||
|
||||
if __name__ == "__main__": # create & train test embedding when used from cli
|
||||
sys.argv.pop(0)
|
||||
text = ' '.join(sys.argv)
|
||||
log.info({ 'prompt': text })
|
||||
output = beautify(text)
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
helper methods that creates HTTP session with managed connection pool
|
||||
provides async HTTP get/post methods and several helper methods
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
import requests
|
||||
from util import Map, log
|
||||
|
||||
|
||||
sd_url = "http://127.0.0.1:7860" # automatic1111 api url root
|
||||
use_session = True
|
||||
timeout = aiohttp.ClientTimeout(total = None, sock_connect = 10, sock_read = None) # default value is 5 minutes, we need longer for training
|
||||
sess = None
|
||||
quiet = False
|
||||
|
||||
|
||||
async def result(req):
|
||||
if req.status != 200:
|
||||
if not quiet:
|
||||
log.error({ 'request error': req.status, 'reason': req.reason, 'url': req.url })
|
||||
if not use_session and sess is not None:
|
||||
await sess.close()
|
||||
return Map({ 'error': req.status, 'reason': req.reason, 'url': req.url })
|
||||
else:
|
||||
json = await req.json()
|
||||
if type(json) == list:
|
||||
res = json
|
||||
elif json is None:
|
||||
res = {}
|
||||
else:
|
||||
res = Map(json)
|
||||
log.debug({ 'request': req.status, 'url': req.url, 'reason': req.reason })
|
||||
return res
|
||||
|
||||
|
||||
def resultsync(req: requests.Response):
|
||||
if req.status_code != 200:
|
||||
if not quiet:
|
||||
log.error({ 'request error': req.status_code, 'reason': req.reason, 'url': req.url })
|
||||
return Map({ 'error': req.status_code, 'reason': req.reason, 'url': req.url })
|
||||
else:
|
||||
json = req.json()
|
||||
if type(json) == list:
|
||||
res = json
|
||||
elif json is None:
|
||||
res = {}
|
||||
else:
|
||||
res = Map(json)
|
||||
log.debug({ 'request': req.status_code, 'url': req.url, 'reason': req.reason })
|
||||
return res
|
||||
|
||||
|
||||
async def get(endpoint: str, json: dict = None):
|
||||
global sess # pylint: disable=global-statement
|
||||
sess = sess if sess is not None else await session()
|
||||
try:
|
||||
async with sess.get(url = endpoint, json = json) as req:
|
||||
res = await result(req)
|
||||
return res
|
||||
except Exception as err:
|
||||
log.error({ 'session': err })
|
||||
return {}
|
||||
|
||||
|
||||
def getsync(endpoint: str, json: dict = None):
|
||||
try:
|
||||
req = requests.get(f'{sd_url}{endpoint}', json = json) # pylint: disable=missing-timeout
|
||||
res = resultsync(req)
|
||||
return res
|
||||
except Exception as err:
|
||||
log.error({ 'session': err })
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
async def post(endpoint: str, json: dict = None):
|
||||
global sess # pylint: disable=global-statement
|
||||
# sess = sess if sess is not None else await session()
|
||||
if sess and not sess.closed:
|
||||
await sess.close()
|
||||
sess = await session()
|
||||
try:
|
||||
async with sess.post(url = endpoint, json = json) as req:
|
||||
res = await result(req)
|
||||
return res
|
||||
except Exception as err:
|
||||
log.error({ 'session': err })
|
||||
return {}
|
||||
|
||||
|
||||
def postsync(endpoint: str, json: dict = None):
|
||||
req = requests.post(f'{sd_url}{endpoint}', json = json) # pylint: disable=missing-timeout
|
||||
res = resultsync(req)
|
||||
return res
|
||||
|
||||
|
||||
async def interrupt():
|
||||
res = await get('/sdapi/v1/progress?skip_current_image=true')
|
||||
if 'state' in res and res.state.job_count > 0:
|
||||
log.debug({ 'interrupt': res.state })
|
||||
res = await post('/sdapi/v1/interrupt')
|
||||
await asyncio.sleep(1)
|
||||
return res
|
||||
else:
|
||||
log.debug({ 'interrupt': 'idle' })
|
||||
return { 'interrupt': 'idle' }
|
||||
|
||||
|
||||
def interruptsync():
|
||||
res = getsync('/sdapi/v1/progress?skip_current_image=true')
|
||||
if 'state' in res and res.state.job_count > 0:
|
||||
log.debug({ 'interrupt': res.state })
|
||||
res = postsync('/sdapi/v1/interrupt')
|
||||
return res
|
||||
else:
|
||||
log.debug({ 'interrupt': 'idle' })
|
||||
return { 'interrupt': 'idle' }
|
||||
|
||||
|
||||
async def progress():
|
||||
res = await get('/sdapi/v1/progress?skip_current_image=true')
|
||||
log.debug({ 'progress': res })
|
||||
return res
|
||||
|
||||
|
||||
def progresssync():
|
||||
res = getsync('/sdapi/v1/progress?skip_current_image=true')
|
||||
log.debug({ 'progress': res })
|
||||
return res
|
||||
|
||||
|
||||
def options():
|
||||
options = getsync('/sdapi/v1/options')
|
||||
flags = getsync('/sdapi/v1/cmd-flags')
|
||||
return { 'options': options, 'flags': flags }
|
||||
|
||||
|
||||
def shutdown():
|
||||
try:
|
||||
postsync('/sdapi/v1/shutdown')
|
||||
except Exception as e:
|
||||
log.info({ 'shutdown': e })
|
||||
|
||||
|
||||
async def session():
|
||||
global sess # pylint: disable=global-statement
|
||||
time = aiohttp.ClientTimeout(total = None, sock_connect = 10, sock_read = None) # default value is 5 minutes, we need longer for training
|
||||
sess = aiohttp.ClientSession(timeout = time, base_url = sd_url)
|
||||
log.debug({ 'sdapi': 'session created', 'endpoint': sd_url })
|
||||
"""
|
||||
sess = await aiohttp.ClientSession(timeout = timeout).__aenter__()
|
||||
try:
|
||||
async with sess.get(url = f'{sd_url}/') as req:
|
||||
log.debug({ 'sdapi': 'session created', 'endpoint': sd_url })
|
||||
except Exception as e:
|
||||
log.error({ 'sdapi': e })
|
||||
await asyncio.sleep(0)
|
||||
await sess.__aexit__(None, None, None)
|
||||
sess = None
|
||||
return sess
|
||||
"""
|
||||
return sess
|
||||
|
||||
|
||||
async def close():
|
||||
if sess is not None:
|
||||
await asyncio.sleep(0)
|
||||
await sess.__aexit__(None, None, None)
|
||||
log.debug({ 'sdapi': 'session closed', 'endpoint': sd_url })
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
log.setLevel(logging.DEBUG)
|
||||
if 'interrupt' in sys.argv:
|
||||
asyncio.run(interrupt())
|
||||
if 'progress' in sys.argv:
|
||||
asyncio.run(progress())
|
||||
if 'options' in sys.argv:
|
||||
opt = options()
|
||||
log.debug({ 'options' })
|
||||
print(json.dumps(opt['options'], indent = 2))
|
||||
log.debug({ 'cmd-flags' })
|
||||
print(json.dumps(opt['flags'], indent = 2))
|
||||
if 'shutdown' in sys.argv:
|
||||
shutdown()
|
||||
asyncio.run(close())
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/bin/env python
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pathlib
|
||||
import logging
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
from matplotlib import pyplot as plt
|
||||
from util import log, Map
|
||||
|
||||
|
||||
def settings(logdir: str, name: str):
|
||||
filename = os.path.join(logdir, name, 'settings.json')
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
data = Map(data)
|
||||
log.debug({ 'settings': data })
|
||||
return data
|
||||
|
||||
|
||||
def plot(logdir: str, name: str):
|
||||
f = os.path.join(logdir, name, 'train.csv')
|
||||
if not os.path.isfile(f):
|
||||
log.debug({ 'train log missing': f })
|
||||
return
|
||||
name = pathlib.Path(f).parent.name
|
||||
img = os.path.join(logdir, f"{name}.train.png")
|
||||
|
||||
step, loss, rate = plt.np.loadtxt(f, delimiter = ',', skiprows = 1, usecols = [0, 3, 4], unpack = True)
|
||||
d = settings(logdir, name)
|
||||
# window = d.get('gradient_step', 1) * d.get('batch_size', 1)
|
||||
window = d.get('save_embedding_every', 1)
|
||||
try:
|
||||
log.debug({ 'loss plot': name, 'output': img, 'data': f, 'records': len(step) })
|
||||
except:
|
||||
return # no data
|
||||
if len(step) < 5:
|
||||
return
|
||||
|
||||
plt.rcParams.update({'font.variant':'small-caps'})
|
||||
plt.rc('axes', edgecolor='gray')
|
||||
plt.rc('font', size=10)
|
||||
plt.rc('font', variant='small-caps')
|
||||
plt.grid(color='gray', linewidth=1, axis='both', alpha=0.5)
|
||||
plt.rcParams['figure.figsize'] = [14, 14]
|
||||
plt.rcParams['figure.facecolor'] = 'black'
|
||||
figure, axis = plt.subplots(2, 1)
|
||||
|
||||
# create top graph
|
||||
ax0 = axis[0]
|
||||
ax0.set_facecolor(color = (0.1, 0.1, 0.1, 0.5))
|
||||
ax0.tick_params(axis='x', labelcolor='white')
|
||||
ax0.set_xlabel('step'.upper(), color='white')
|
||||
ax0.set_xlim(0, d.steps)
|
||||
ax0.set_ylim(0, 0.5)
|
||||
ax0.set_axisbelow(True)
|
||||
ax0.xaxis.grid(color='gray', linestyle='dashed')
|
||||
ax0.yaxis.grid(color='gray', linestyle='dashed')
|
||||
|
||||
# loss values
|
||||
ax0.plot(step, loss, color='gray', label='loss value')
|
||||
ax0.set_ylabel('loss value'.upper(), color='#CE6400')
|
||||
ax0.tick_params(axis='y', labelcolor='#CE6400')
|
||||
|
||||
# trendline
|
||||
z = np.polyfit(step, loss, 1)
|
||||
p = np.poly1d(z)
|
||||
ax0.plot(step, p(loss), color='#5020F0', linewidth=3, label='loss trendline')
|
||||
|
||||
# moving average
|
||||
if len(loss) > window:
|
||||
maval = []
|
||||
minval = []
|
||||
maxval = []
|
||||
for ind in range(window - 1):
|
||||
maval.insert(0, np.nan)
|
||||
minval.insert(0, np.nan)
|
||||
maxval.insert(0, np.nan)
|
||||
for ind in range(len(loss) - window + 1):
|
||||
maval.append(np.mean(loss[ind:ind+window]))
|
||||
minval.append(np.min(loss[ind:ind+window]))
|
||||
maxval.append(np.max(loss[ind:ind+window]))
|
||||
ax0.plot(step, maval, color='#CE6400', linewidth=5, label='average loss value')
|
||||
ax0.plot(step, minval, color='#500010', linewidth=5, label='min loss per epoch')
|
||||
ax0.plot(step, maxval, color='#005010', linewidth=5, label='max loss per epoch')
|
||||
|
||||
# learning rate
|
||||
ax0_right = ax0.twinx()
|
||||
ax0_right.set_ylabel('learn rate'.upper(), color='cyan')
|
||||
ax0_right.plot(step, rate, color='cyan', linewidth=3, linestyle='dashed', label='learn rate')
|
||||
ax0_right.tick_params(axis='y', labelcolor='cyan')
|
||||
|
||||
# axis legend
|
||||
handles0, labels0 = ax0.get_legend_handles_labels() # because ax2 is twin, both are included
|
||||
ax0.legend(handles0, labels0, loc="best")
|
||||
|
||||
# embeddings
|
||||
ax1 = axis[1]
|
||||
ax1.set_facecolor(color = (0.1, 0.1, 0.1, 0.5))
|
||||
ax1.set_ylabel('vector average'.upper(), color=(1, 0.2, 0.5))
|
||||
ax1.tick_params(axis='y', labelcolor=(1, 0.2, 0.5))
|
||||
ax1.set_xlim(0, d.steps)
|
||||
ax1_right = ax1.twinx()
|
||||
ax1_right.set_ylabel('vector norm'.upper(), color=(0.2, 1.0, 0.5))
|
||||
ax1_right.tick_params(axis='y', labelcolor=(0.2, 1.0, 0.5))
|
||||
ax1.xaxis.grid(color='gray', linestyle='dashed')
|
||||
ax1.yaxis.grid(color='gray', linestyle='dashed')
|
||||
x = []
|
||||
avg = []
|
||||
norm = []
|
||||
avg_v = [[] for y in range(d.num_vectors_per_token)]
|
||||
norm_v = [[] for y in range(d.num_vectors_per_token)]
|
||||
embedding_files = sorted(pathlib.Path(os.path.join(logdir, name, 'embeddings')).glob('*.pt'), key=os.path.getmtime)
|
||||
for f in embedding_files:
|
||||
embed = torch.load(f, map_location=torch.device("cpu")) # pylint: disable=no-member
|
||||
x.append(embed["step"] + 1)
|
||||
token = list(embed["string_to_token"].keys())[0]
|
||||
tensors = embed["string_to_param"][token]
|
||||
val = tensors.detach().numpy()
|
||||
data = val.flatten()
|
||||
avg.append(np.average(np.abs(data)))
|
||||
norm.append(np.linalg.norm(data))
|
||||
for i in range(val.shape[0]):
|
||||
avg_v[i].append(np.average(np.abs(val[i])))
|
||||
norm_v[i].append(np.linalg.norm(val[i]))
|
||||
ax1.plot(x, avg, color=(1, 0.2, 0.5), linewidth=3, label='all vectors average value')
|
||||
ax1_right.plot(x, norm, color=(0.2, 1, 0.5), linewidth=3, label='all vectors norm value', linestyle='dashed')
|
||||
for i in range(d.num_vectors_per_token):
|
||||
ax1.plot(x, avg_v[i], color= (i / (d.num_vectors_per_token + 1), 0.2, 0.5), linewidth=1, label=f'vector={i} average value')
|
||||
ax1_right.plot(x, norm_v[i], color= (0.2, i / (d.num_vectors_per_token + 1), 0.5), linewidth=1, label=f'vector={i} norm value', linestyle='dashed')
|
||||
|
||||
# axis legend
|
||||
handles1, labels1 = ax1.get_legend_handles_labels() # because ax2 is twin, both are included
|
||||
ax1.legend(handles1, labels1, loc="upper left")
|
||||
ax1_right.legend(loc="upper right")
|
||||
|
||||
# create chart and convert to pil
|
||||
figure.tight_layout()
|
||||
buf = io.BytesIO()
|
||||
plt.savefig(buf, format='png')
|
||||
pltimg = Image.open(buf)
|
||||
size = (pltimg.size[0], pltimg.size[1] + 240)
|
||||
image = Image.new('RGB', size = size, color = (206, 100, 0))
|
||||
font = ImageFont.truetype('DejaVuSansMono', 18)
|
||||
image.paste(pltimg, box=(0, 240))
|
||||
buf.close()
|
||||
plt.close()
|
||||
|
||||
# text
|
||||
textl = f"""NAME: {d.embedding_name.upper()}
|
||||
IMAGES: {d.num_of_dataset_images}
|
||||
VECTORS: {d.num_vectors_per_token}
|
||||
STEPS: {d.steps}
|
||||
BATCH-SIZE: {d.batch_size}
|
||||
GRADIENT-STEP: {d.gradient_step}
|
||||
SAMPLING-METHOD: {d.latent_sampling_method}
|
||||
MODEL: {d.model_name.upper()}
|
||||
LEARN-RATE: {d.learn_rate.replace(' ', '')}
|
||||
"""
|
||||
|
||||
minval = f"{round(np.min(loss), 4)} @ {round(step[np.argmin(loss)])}"
|
||||
maxval = f"{round(np.max(loss), 4)} @ {round(step[np.argmax(loss)])}"
|
||||
textr = f"""{d.datetime}
|
||||
LOSS: {round(loss[-1], 4)}
|
||||
MIN: {minval}
|
||||
MAX: {maxval}
|
||||
TREND: {z[0]:.5f}
|
||||
"""
|
||||
if len(avg) > 0:
|
||||
textr += f"VECTOR AVG: {avg[-1]:.3f}\n"
|
||||
if len(norm) > 0:
|
||||
textr += f"VECTOR NORM: {norm[-1]:.3f}\n"
|
||||
ctx = ImageDraw.Draw(image)
|
||||
ctx.text((8, 8), textl, font = font, fill = (255, 255, 255), spacing = 8)
|
||||
ctx.text((image.size[0] - 220, 8), textr, font = font, fill = (255, 255, 255))
|
||||
|
||||
image.save(img)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
log.setLevel(logging.DEBUG)
|
||||
if len(sys.argv) == 2:
|
||||
arg = sys.argv[1]
|
||||
log.debug({ 'args': arg })
|
||||
plot(os.path.dirname(arg), os.path.basename(arg))
|
||||
else:
|
||||
log.error({ 'loss chart': 'specify embedding name'})
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
auto-generate learn-rate
|
||||
"""
|
||||
import io
|
||||
import math
|
||||
import logging
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
from matplotlib import pyplot as plt
|
||||
from util import log, Map
|
||||
|
||||
|
||||
loss_types = ['linear', 'log', 'linalg', 'power']
|
||||
|
||||
|
||||
def gen_steps(steps, step):
|
||||
return [x for x in range(1, steps + step) if x % step == 0]
|
||||
|
||||
|
||||
def gen_loss_rate(steps: int, step: int, loss_start: float, loss_end: float, loss_type: loss_types, power: int = 3):
|
||||
def norm(val):
|
||||
return ((loss_start - loss_end) * val) / val.max() + loss_end
|
||||
|
||||
steps_val = gen_steps(steps, step)
|
||||
|
||||
if loss_type == 'linear':
|
||||
loss_val = np.interp(steps_val, [steps_val[0], steps_val[-1]], [loss_start, loss_end])
|
||||
|
||||
elif loss_type == 'log':
|
||||
loss_val = np.logspace(loss_start, 0, num=len(steps_val), base=math.e)
|
||||
loss_val = norm(loss_val - loss_val.min())
|
||||
|
||||
elif loss_type == 'linalg':
|
||||
loss_val = np.array(steps_val[::-1], dtype='float')
|
||||
loss_val = norm(loss_val / np.linalg.norm(loss_val))
|
||||
|
||||
elif loss_type == 'power':
|
||||
loss_val = np.array([math.pow(x, power) for x in range(len(steps_val))][::-1])
|
||||
loss_val = norm(loss_val)
|
||||
|
||||
else:
|
||||
return []
|
||||
|
||||
return loss_val
|
||||
|
||||
|
||||
def gen_loss_rate_str(steps: int, step: int, loss_start: float, loss_end: float, loss_type: loss_types, power: int = 3):
|
||||
steps_val = gen_steps(steps, step)
|
||||
loss_val = gen_loss_rate(steps, step, loss_start, loss_end, loss_type, power)
|
||||
loss_rate = [f"{loss_val[i]:.4f}:{steps_val[i]}" for i in range(len(steps_val))]
|
||||
loss_rate = ', '.join(loss_rate)
|
||||
log.debug({ 'loss_rate': loss_rate, 'function': loss_type, 'power': power })
|
||||
return loss_rate
|
||||
|
||||
|
||||
def example_plot(steps: int, step: int, loss_start: float, loss_end: float):
|
||||
plt.rcParams.update({'font.variant':'small-caps'})
|
||||
plt.rc('axes', edgecolor='gray')
|
||||
plt.rc('font', size=10)
|
||||
plt.rc('font', variant='small-caps')
|
||||
plt.grid(color='gray', linewidth=1, axis='both', alpha=0.5)
|
||||
plt.rcParams['figure.figsize'] = [14, 6]
|
||||
plt.figure(facecolor='black')
|
||||
|
||||
loss_rates = []
|
||||
|
||||
ax1 = plt.subplot(1, 2, 1)
|
||||
ax1.set_facecolor('grey')
|
||||
ax1.set_xlabel('step'.upper(), color='white')
|
||||
ax1.set_ylabel('loss value'.upper(), color='white')
|
||||
ax1.xaxis.grid(color='gray', linestyle='dashed')
|
||||
ax1.tick_params(axis='x', labelcolor='white')
|
||||
ax1.tick_params(axis='y', labelcolor='white')
|
||||
ax1.legend(loc="best")
|
||||
for loss_type in [x for x in loss_types if x != 'power']:
|
||||
col = (np.random.random(), np.random.random(), np.random.random())
|
||||
x = gen_steps(steps, step)
|
||||
y = gen_loss_rate(loss_type = loss_type, steps = steps, step = step, loss_start = loss_start, loss_end = loss_end)
|
||||
ax1.plot(x, y, label=loss_type, color = col)
|
||||
loss = gen_loss_rate_str(loss_type = loss_type, steps = steps, step = step, loss_start = loss_start, loss_end = loss_end)
|
||||
loss_rates.append(f"LOSS {loss} TYPE {loss_type}")
|
||||
handles, labels = ax1.get_legend_handles_labels()
|
||||
ax1.legend(handles, labels)
|
||||
|
||||
ax2 = plt.subplot(1, 2, 2)
|
||||
ax2.set_facecolor('grey')
|
||||
ax2.set_xlabel('step'.upper(), color='white')
|
||||
ax2.set_ylabel('loss value'.upper(), color='white')
|
||||
ax2.xaxis.grid(color='gray', linestyle='dashed')
|
||||
ax2.tick_params(axis='x', labelcolor='white')
|
||||
ax2.tick_params(axis='y', labelcolor='white')
|
||||
ax2.legend(loc="best")
|
||||
for power in range(1, 11):
|
||||
col = (np.random.random(), np.random.random(), np.random.random())
|
||||
x = gen_steps(steps, step)
|
||||
y = gen_loss_rate(loss_type = 'power', power = power, steps = steps, step = step, loss_start = loss_start, loss_end = loss_end)
|
||||
ax2.plot(x, y, label=f"power={pow}", color = col)
|
||||
loss = gen_loss_rate_str(loss_type = 'power', power = power, steps = steps, step = step, loss_start = loss_start, loss_end = loss_end)
|
||||
loss_rates.append(f"LOSS {loss} TYPE power={power}")
|
||||
handles, labels = ax2.get_legend_handles_labels()
|
||||
ax2.legend(handles, labels)
|
||||
|
||||
plt.tight_layout()
|
||||
buf = io.BytesIO()
|
||||
plt.savefig(buf, format='png')
|
||||
pltimg = Image.open(buf)
|
||||
size = (pltimg.size[0], pltimg.size[1] + 300)
|
||||
image = Image.new('RGB', size = size, color = (206, 100, 0))
|
||||
font = ImageFont.truetype('DejaVuSansMono', 14)
|
||||
image.paste(pltimg, box=(0, 300))
|
||||
buf.close()
|
||||
|
||||
# text
|
||||
rates = "\n".join(loss_rates)
|
||||
text = f"STEPS {steps} STEP {step} LOSS-START {loss_start} LOSS-END {loss_end}\n" + rates
|
||||
|
||||
ctx = ImageDraw.Draw(image)
|
||||
ctx.text((8, 8), text, font = font, fill = (255, 255, 255), spacing = 8)
|
||||
image.save('lossrate.jpg')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
log.setLevel(logging.DEBUG)
|
||||
arg = Map({
|
||||
"steps": 500,
|
||||
"step": 50,
|
||||
"loss_start": 0.01,
|
||||
"loss_end": 0.001
|
||||
})
|
||||
log.debug({ 'options': arg })
|
||||
example_plot(**arg)
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
generic helper methods
|
||||
"""
|
||||
|
||||
import os
|
||||
import string
|
||||
import logging
|
||||
|
||||
log_format = '%(asctime)s %(levelname)s: %(message)s'
|
||||
logging.basicConfig(level = logging.INFO, format = log_format)
|
||||
log = logging.getLogger("sd")
|
||||
|
||||
|
||||
def set_logfile(logfile):
|
||||
fh = logging.FileHandler(logfile)
|
||||
formatter = logging.Formatter(log_format)
|
||||
fh.setLevel(log.getEffectiveLevel())
|
||||
fh.setFormatter(formatter)
|
||||
log.addHandler(fh)
|
||||
log.info({ 'log file': logfile })
|
||||
|
||||
|
||||
def safestring(text: str):
|
||||
lines = []
|
||||
for line in text.splitlines():
|
||||
lines.append(line.translate(str.maketrans('', '', string.punctuation)).strip())
|
||||
res = ', '.join(lines)
|
||||
return res[:1000]
|
||||
|
||||
|
||||
def get_memory():
|
||||
def gb(val: float):
|
||||
return round(val / 1024 / 1024 / 1024, 2)
|
||||
mem = {}
|
||||
try:
|
||||
import psutil
|
||||
process = psutil.Process(os.getpid())
|
||||
res = process.memory_info()
|
||||
ram_total = 100 * res.rss / process.memory_percent()
|
||||
ram = { 'free': gb(ram_total - res.rss), 'used': gb(res.rss), 'total': gb(ram_total) }
|
||||
mem.update({ 'ram': ram })
|
||||
except Exception as e:
|
||||
mem.update({ 'ram': e })
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
s = torch.cuda.mem_get_info()
|
||||
gpu = { 'free': gb(s[0]), 'used': gb(s[1] - s[0]), 'total': gb(s[1]) }
|
||||
s = dict(torch.cuda.memory_stats('cuda'))
|
||||
allocated = { 'current': gb(s['allocated_bytes.all.current']), 'peak': gb(s['allocated_bytes.all.peak']) }
|
||||
reserved = { 'current': gb(s['reserved_bytes.all.current']), 'peak': gb(s['reserved_bytes.all.peak']) }
|
||||
active = { 'current': gb(s['active_bytes.all.current']), 'peak': gb(s['active_bytes.all.peak']) }
|
||||
inactive = { 'current': gb(s['inactive_split_bytes.all.current']), 'peak': gb(s['inactive_split_bytes.all.peak']) }
|
||||
warnings = { 'retries': s['num_alloc_retries'], 'oom': s['num_ooms'] }
|
||||
mem.update({
|
||||
'gpu': gpu,
|
||||
'gpu-active': active,
|
||||
'gpu-allocated': allocated,
|
||||
'gpu-reserved': reserved,
|
||||
'gpu-inactive': inactive,
|
||||
'events': warnings,
|
||||
})
|
||||
except:
|
||||
pass
|
||||
return Map(mem)
|
||||
|
||||
|
||||
class Map(dict):
|
||||
__slots__ = ('__dict__')
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Map, self).__init__(*args, **kwargs)
|
||||
for arg in args:
|
||||
if isinstance(arg, dict):
|
||||
for k, v in arg.items():
|
||||
if isinstance(v, dict):
|
||||
v = Map(v)
|
||||
if isinstance(v, list):
|
||||
self.__convert(v)
|
||||
self[k] = v
|
||||
if kwargs:
|
||||
for k, v in kwargs.items():
|
||||
if isinstance(v, dict):
|
||||
v = Map(v)
|
||||
elif isinstance(v, list):
|
||||
self.__convert(v)
|
||||
self[k] = v
|
||||
def __convert(self, v):
|
||||
for elem in range(0, len(v)): # pylint: disable=consider-using-enumerate
|
||||
if isinstance(v[elem], dict):
|
||||
v[elem] = Map(v[elem])
|
||||
elif isinstance(v[elem], list):
|
||||
self.__convert(v[elem])
|
||||
def __getattr__(self, attr):
|
||||
return self.get(attr)
|
||||
def __setattr__(self, key, value):
|
||||
self.__setitem__(key, value)
|
||||
def __setitem__(self, key, value):
|
||||
super(Map, self).__setitem__(key, value)
|
||||
self.__dict__.update({key: value})
|
||||
def __delattr__(self, item):
|
||||
self.__delitem__(item)
|
||||
def __delitem__(self, key):
|
||||
super(Map, self).__delitem__(key)
|
||||
del self.__dict__[key]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
use ffmpeg for animation processing
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import pathlib
|
||||
import argparse
|
||||
import filetype
|
||||
from util import log, Map
|
||||
|
||||
|
||||
def probe(src: str):
|
||||
cmd = f"ffprobe -hide_banner -loglevel 0 -print_format json -show_format -show_streams \"{src}\""
|
||||
result = subprocess.run(cmd, shell = True, capture_output = True, text = True, check = True)
|
||||
data = json.loads(result.stdout)
|
||||
stream = [x for x in data['streams'] if x["codec_type"] == "video"][0]
|
||||
format = data['format'] if 'format' in data else {}
|
||||
res = {**stream, **format}
|
||||
video = Map({
|
||||
'codec': res.get('codec_name', 'unknown') + '/' + res.get('codec_tag_string', ''),
|
||||
'resolution': [int(res.get('width', 0)), int(res.get('height', 0))],
|
||||
'duration': float(res.get('duration', 0)),
|
||||
'frames': int(res.get('nb_frames', 0)),
|
||||
'bitrate': round(float(res.get('bit_rate', 0)) / 1024),
|
||||
})
|
||||
return video
|
||||
|
||||
|
||||
def extract(src: str, dst: str, rate: float = 0.015, fps: float = 0, start = 0, end = 0):
|
||||
images = []
|
||||
if not os.path.isfile(src) or not filetype.is_video(src):
|
||||
log.error({ 'extract': 'input is not movie file' })
|
||||
return
|
||||
dst = dst if dst.endswith('/') else dst + '/'
|
||||
|
||||
video = probe(src)
|
||||
log.info({ 'extract': { 'source': src, **video } })
|
||||
|
||||
ssstart = f' -ss {start}' if start > 0 else ''
|
||||
ssend = f' -to {video.duration - end}' if start > 0 else ''
|
||||
filename = pathlib.Path(src).stem
|
||||
if rate > 0:
|
||||
cmd = f"ffmpeg -hide_banner -y -loglevel info {ssstart} {ssend} -i \"{src}\" -filter:v \"select='gt(scene,{rate})',metadata=print\" -vsync vfr -frame_pts 1 \"{dst}{filename}-%05d.jpg\""
|
||||
elif fps > 0:
|
||||
cmd = f"ffmpeg -hide_banner -y -loglevel info {ssstart} {ssend} -i \"{src}\" -r {fps} -vsync vfr -frame_pts 1 \"{dst}{filename}-%05d.jpg\""
|
||||
else:
|
||||
log.error({ 'extract': 'requires either rate or fps' })
|
||||
return 0
|
||||
log.debug({ 'extract': cmd })
|
||||
pathlib.Path(dst).mkdir(parents = True, exist_ok = True)
|
||||
result = subprocess.run(cmd, shell = True, capture_output = True, text = True, check = True)
|
||||
for line in result.stderr.split('\n'):
|
||||
if 'pts_time' in line:
|
||||
log.debug({ 'extract': { 'keyframe': line.strip().split(' ')[-1].split(':')[-1] } })
|
||||
images = next(os.walk(dst))[2]
|
||||
log.info({ 'extract': { 'destination': dst, 'keyframes': len(images), 'rate': rate, 'fps': fps } })
|
||||
return len(images)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="ffmpeg pipeline")
|
||||
parser.add_argument("--input", type = str, required = True, help="input")
|
||||
parser.add_argument("--output", type = str, required = True, help="output")
|
||||
parser.add_argument("--rate", type = float, default = 0, required = False, help="extraction change rate threshold")
|
||||
parser.add_argument("--fps", type = float, default = 0, required = False, help="extraction frames per second")
|
||||
parser.add_argument("--skipstart", type = float, default = 1, required = False, help="skip time from start of video")
|
||||
parser.add_argument("--skipend", type = float, default = 1, required = False, help="skip time to end of video")
|
||||
params = parser.parse_args()
|
||||
extract(src = params.input, dst = params.output, rate = params.rate, fps = params.fps, start = params.skipstart, end = params.skipend)
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"prompts": [
|
||||
"<style> of <embedding> <place>, high detailed, by <artist>, <suffix>"
|
||||
],
|
||||
"negative": [
|
||||
"watermark, fog, clouds, blurry, duplicate, deformed, mutation"
|
||||
],
|
||||
"places": [
|
||||
"standing in the city", "on a spaceship", "in fantasy landscape", "on a shore", "in a forest", "in winter wonderland"
|
||||
],
|
||||
"embeddings": [
|
||||
"man", "man next to a beautiful girl", "man next to a car", "beautiful girl", "sexy naked girl", "cute girl holding a flower", "beautiful robot",
|
||||
"young korean girl with medium-length white hair", "monster", "pin up girl",
|
||||
"man vlado", "beutiful girl ana", "man lee", "beautiful girl abby"
|
||||
],
|
||||
"artists": [
|
||||
"John Salminen", "Greg Rutkowski", "Akihiko Yoshida", "Alejandro Burdisio", "Artgerm", "Patrick Brown", "Walt Disney", "Neal Adams", "Jeremy Chong",
|
||||
"Chris Rallis", "Roy Lichtenstein", "Claude Monet", "Jon Whitcomb", "Pablo Picasso", "Raymond Leech", "Tom Lovell", "Noriyoshi Ohrai", "Shingei",
|
||||
"Helmut Newton", "Maciej Kuciara", "Daniel F. Gerhartz", "Stephan Martinière", "Magali Villeneuve", "Carne Griffiths", "Alberto Seveso",
|
||||
"Vincent Van Gogh", "WLOP", "Frank Xavier Leyendecker", "Peter Lindbergh", "Nick Gentry", "Howard Chandler Christy", "Raphael", "Henri Matisse"
|
||||
],
|
||||
"styles": [
|
||||
"illustration", "painting", "portrait", "photograph", "drawing", "sketch", "pencil sketch", "3d render", "cartoon", "anime", "scribbles", "pop art",
|
||||
"ink painting", "steampunk illustration", "dc comics illustration", "marvel comics", "vray render", "photoillustration", "pixar", "marble sculpture",
|
||||
"bronze sculpture", "christmas theme"
|
||||
],
|
||||
"suffixes": [
|
||||
"cinematic lighting", "artstation", "fineart", "cinematic", "photorealistic", "soft light", "sharp focus", "bokeh", "dreamlike", "semirealism",
|
||||
"colorful", "black and white", "intricate", "elegant"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
Detect model type
|
||||
|
||||
Works for v1 and v2-base (EPS models), both standard inference and inpainting
|
||||
|
||||
But looking at model dumps between EPS and V type models, its only about parametrization, there are no differences in actual model (its just weighted differently without any structural difference)
|
||||
So i don't see easy way to auto-detect if model should be run in `EPS` or `V` mode
|
||||
Only difference are some calculations in `ldm/models/diffusion/ddpm.py` and by then we already need to know which code-path to trigger
|
||||
(maaaybe there could be a way by looking at some cherry-picked base tensors min/max range, but I dont see that as reliable)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import torch
|
||||
|
||||
def signature(model):
|
||||
if model is None:
|
||||
return None
|
||||
try:
|
||||
size = model['state_dict']['model.diffusion_model.input_blocks.1.1.transformer_blocks.0.attn2.to_k.weight'].shape[1]
|
||||
unet = model['state_dict']['model.diffusion_model.input_blocks.0.0.weight'].shape[1]
|
||||
except:
|
||||
return 'unknown'
|
||||
guess = 'v1' if size == 768 else 'v2' # 768 for v1 and 1024 for v2
|
||||
guess += '-inference' if unet == 4 else '-inpainting' # inference models have shorter inputs, 4 for inference 9 for inpainting
|
||||
return guess
|
||||
|
||||
def load(file: str):
|
||||
try:
|
||||
model = torch.load(file, map_location='cpu')
|
||||
return model
|
||||
except Exception as err:
|
||||
print(f"Error loading {f}: {err}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv.pop(0)
|
||||
for f in sys.argv:
|
||||
if os.path.isfile(f):
|
||||
print(f"Model {f} is of type {signature(load(f))}")
|
||||
else:
|
||||
print(f"{f} is not a file")
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
Test Torch Dynamo functionality and backends
|
||||
"""
|
||||
import json
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torchvision.models import resnet18
|
||||
|
||||
|
||||
print('torch:', torch.__version__)
|
||||
try:
|
||||
import torch._dynamo as dynamo # must be imported explicitly or namespace is not found
|
||||
except Exception as err:
|
||||
print('torch without dynamo support', err)
|
||||
|
||||
|
||||
N_ITERS = 20
|
||||
torch._dynamo.config.verbose=True
|
||||
warnings.filterwarnings('ignore', category=UserWarning) # disable those for now as many backends reports tons
|
||||
# torch.set_float32_matmul_precision('high') # enable to test in fp32
|
||||
|
||||
|
||||
def timed(fn): # returns the result of running `fn()` and the time it took for `fn()` to run in ms using CUDA events
|
||||
start = torch.cuda.Event(enable_timing=True)
|
||||
end = torch.cuda.Event(enable_timing=True)
|
||||
start.record()
|
||||
result = fn()
|
||||
end.record()
|
||||
torch.cuda.synchronize()
|
||||
return result, start.elapsed_time(end)
|
||||
|
||||
|
||||
def generate_data(b):
|
||||
return (
|
||||
torch.randn(b, 3, 128, 128).to(torch.float32).cuda(),
|
||||
torch.randint(1000, (b,)).cuda(),
|
||||
)
|
||||
|
||||
|
||||
def init_model():
|
||||
return resnet18().to(torch.float32).cuda()
|
||||
|
||||
|
||||
def eval(mod, inp):
|
||||
return mod(inp)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# first pass, dynamo is going to be slower as it compiles
|
||||
model = init_model()
|
||||
inp = generate_data(16)[0]
|
||||
|
||||
# repeat test
|
||||
results = {}
|
||||
times = []
|
||||
print('eager initial eval:', timed(lambda: eval(model, inp))[1])
|
||||
for i in range(N_ITERS):
|
||||
inp = generate_data(16)[0]
|
||||
_res, time = timed(lambda: eval(model, inp))
|
||||
times.append(time)
|
||||
results['default'] = np.median(times)
|
||||
|
||||
print('dynamo available backends:', dynamo.list_backends())
|
||||
for backend in dynamo.list_backends():
|
||||
try:
|
||||
torch._dynamo.reset() # required before changing backends
|
||||
eval_dyn = dynamo.optimize(backend)(eval)
|
||||
print('dynamo initial eval:', backend, timed(lambda: eval_dyn(model, inp))[1])
|
||||
times = []
|
||||
for i in range(N_ITERS):
|
||||
inp = generate_data(16)[0]
|
||||
_res, time = timed(lambda: eval_dyn(model, inp))
|
||||
times.append(time)
|
||||
results[backend] = np.median(times)
|
||||
except Exception as err:
|
||||
lines = str(err).split('\n')
|
||||
print('dyanmo backend failed:', backend, lines[0]) # print just first error line as backtraces can be quite long
|
||||
results[backend] = 'error'
|
||||
|
||||
# print stats
|
||||
print(json.dumps(results, indent = 4))
|
||||
|
||||
|
||||
"""
|
||||
Reference: <https://github.com/pytorch/pytorch/blob/4f4b62e4a255708e928445b6502139d5962974fa/docs/source/dynamo/get-started.rst>
|
||||
Training & Inference backends:
|
||||
dynamo.optimize("inductor") - Uses TorchInductor backend with AotAutograd and cudagraphs by leveraging codegened Triton kernels
|
||||
dynamo.optimize("aot_nvfuser") - nvFuser with AotAutograd
|
||||
dynamo.optimize("aot_cudagraphs") - cudagraphs with AotAutograd
|
||||
Inference-only backends:
|
||||
dynamo.optimize("ofi") - Uses Torchscript optimize_for_inference
|
||||
dynamo.optimize("fx2trt") - Uses Nvidia TensorRT for inference optimizations
|
||||
dynamo.optimize("onnxrt") - Uses ONNXRT for inference on CPU/GPU
|
||||
"""
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/env python
|
||||
"""
|
||||
print module versions
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import pkg_resources
|
||||
|
||||
modules = [
|
||||
'diffusers', 'xformers', 'tokenizers', 'accelerate', 'safetensors'
|
||||
]
|
||||
|
||||
def get_torch():
|
||||
try:
|
||||
torch = importlib.import_module('torch')
|
||||
print('torch:', { 'version': torch.__version__ })
|
||||
print('cuda:', { 'available': torch.cuda.is_available(), 'version': torch.version.cuda, 'arch': torch.cuda.get_arch_list() })
|
||||
print('device:', { 'name': torch.cuda.get_device_name(torch.cuda.current_device()) })
|
||||
except Exception as err:
|
||||
print('torch:', { 'error': err })
|
||||
|
||||
|
||||
def version(name: str):
|
||||
try:
|
||||
ver = pkg_resources.get_distribution(name).version
|
||||
print(f"{name}: {ver}")
|
||||
except Exception as err:
|
||||
print(f"{name} error: {err}")
|
||||
|
||||
if __name__ == "__main__": # create & train test embedding when used from cli
|
||||
get_torch()
|
||||
for module in modules:
|
||||
version(module)
|
||||
@@ -0,0 +1,3 @@
|
||||
mediapipe
|
||||
colormap
|
||||
invisible-watermark
|
||||
@@ -0,0 +1,274 @@
|
||||
#!/bin/env python
|
||||
|
||||
"""
|
||||
Extract approximating LoRA by SVD from two SD models
|
||||
Based on: <https://github.com/kohya-ss/sd-scripts/blob/main/networks/train_network.py>
|
||||
|
||||
Train LoRA with custom preprocessing, tagging and bucketing
|
||||
|
||||
Disabled/broken:
|
||||
- `accelerate` with *dynamo* enabled
|
||||
- `xformers` due to *faketensors* requirement
|
||||
- `mem_eff_attn` due to *forwardfunc* mismatch
|
||||
- 'use_8bit_adam` due to *bitsandbyttes* CUDA errors
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import gc
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import shutil
|
||||
import argparse
|
||||
import tempfile
|
||||
import torch
|
||||
import logging
|
||||
import importlib
|
||||
import transformers
|
||||
from pathlib import Path
|
||||
from modules.util import log, Map, get_memory
|
||||
import modules.process
|
||||
import modules.sdapi
|
||||
|
||||
latents = importlib.import_module('modules.lora-latents')
|
||||
|
||||
lora_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'modules', 'lora'))
|
||||
sys.path.append(lora_path)
|
||||
lycoris_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'modules', 'lycoris'))
|
||||
sys.path.append(lycoris_path)
|
||||
from train_network import train
|
||||
|
||||
options = Map({
|
||||
"bucket_no_upscale": False,
|
||||
"bucket_reso_steps": 64,
|
||||
"cache_latents": True,
|
||||
"caption_dropout_every_n_epochs": None,
|
||||
"caption_dropout_rate": 0.0,
|
||||
"caption_extension": ".txt",
|
||||
"caption_extention": ".txt",
|
||||
"caption_tag_dropout_rate": 0.0,
|
||||
"clip_skip": None,
|
||||
"color_aug": False,
|
||||
"dataset_repeats": 1,
|
||||
"debug_dataset": False,
|
||||
"enable_bucket": False,
|
||||
"face_crop_aug_range": None,
|
||||
"flip_aug": False,
|
||||
"full_fp16": False,
|
||||
"gradient_accumulation_steps": 1,
|
||||
"gradient_checkpointing": False,
|
||||
"in_json": "",
|
||||
"keep_tokens": None,
|
||||
"learning_rate": 5e-05,
|
||||
"log_prefix": None,
|
||||
"logging_dir": None,
|
||||
"lr_scheduler_num_cycles": 1,
|
||||
"lr_scheduler_power": 1,
|
||||
"lr_scheduler": "cosine",
|
||||
"lr_warmup_steps": 0,
|
||||
"max_bucket_reso": 1024,
|
||||
"max_data_loader_n_workers": 8,
|
||||
"max_grad_norm": 0.0,
|
||||
"max_token_length": None,
|
||||
"max_train_epochs": None,
|
||||
"max_train_steps": 5000,
|
||||
"mem_eff_attn": False,
|
||||
"min_bucket_reso": 256,
|
||||
"mixed_precision": "fp16",
|
||||
"network_alpha": 1.0,
|
||||
"network_args": None,
|
||||
"network_dim": 16,
|
||||
"network_module": "networks.lora",
|
||||
"network_train_text_encoder_only": False,
|
||||
"network_train_unet_only": False,
|
||||
"network_weights": None,
|
||||
"no_metadata": False,
|
||||
"output_dir": "",
|
||||
"output_name": "",
|
||||
"persistent_data_loader_workers": False,
|
||||
"pretrained_model_name_or_path": "",
|
||||
"prior_loss_weight": 1.0,
|
||||
"random_crop": False,
|
||||
"reg_data_dir": None,
|
||||
"resolution": "512,512",
|
||||
"resume": None,
|
||||
"save_every_n_epochs": None,
|
||||
"save_last_n_epochs_state": None,
|
||||
"save_last_n_epochs": None,
|
||||
"save_model_as": "ckpt",
|
||||
"save_n_epoch_ratio": None,
|
||||
"save_precision": "fp16",
|
||||
"save_state": False,
|
||||
"seed": 42,
|
||||
"shuffle_caption": False,
|
||||
"text_encoder_lr": 5e-05,
|
||||
"train_batch_size": 1,
|
||||
"train_data_dir": "",
|
||||
"training_comment": "mood-magic",
|
||||
"unet_lr": 0.001,
|
||||
"use_8bit_adam": False,
|
||||
"v_parameterization": False,
|
||||
"v2": False,
|
||||
"vae": None,
|
||||
"xformers": False,
|
||||
})
|
||||
|
||||
|
||||
def mem_stats():
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
with torch.no_grad():
|
||||
torch.cuda.empty_cache()
|
||||
with torch.cuda.device('cuda'):
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
mem = get_memory()
|
||||
log.info({ 'memory': { 'ram': mem.ram, 'gpu': mem.gpu } })
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description = 'train lora')
|
||||
parser.add_argument('--model', type=str, default=None, required=False, help='original model to use a base for training, default: active model')
|
||||
parser.add_argument('--input', '--dataset', type=str, default=None, required=True, help='input folder with training images')
|
||||
parser.add_argument('--output', '--lora', type=str, default=None, required=True, help='lora name')
|
||||
parser.add_argument('--tag', type=str, default=None, required=False, help='primary tag')
|
||||
parser.add_argument('--dir', type=str, default=None, required=False, help='folder containing lora checkpoints')
|
||||
parser.add_argument('--interim', type=int, default=0, help = 'save interim checkpoints after n epoch')
|
||||
parser.add_argument('--process', type=str, default='original', required=False, help='list of processing steps: original,face,body,blur,range,upscale,restore')
|
||||
parser.add_argument('--noprocess', default = False, action='store_true', help = 'skip processing and use existing input data')
|
||||
parser.add_argument('--notrain', default = False, action='store_true', help = 'just run processing and skip training')
|
||||
parser.add_argument('--nocaptions', default = False, action='store_true', help = 'skip creating captions and tags')
|
||||
parser.add_argument('--nolatents', default = False, action='store_true', help = 'skip generating vae latents')
|
||||
parser.add_argument('--offline', default = False, action='store_true', help = 'do not use webui server for processing')
|
||||
parser.add_argument('--shutdown', default = False, action='store_true', help = 'shutdown webui server')
|
||||
parser.add_argument('--gradient', type=int, default=1, required=False, help='gradient accumulation steps, default: %(default)s')
|
||||
parser.add_argument('--steps', type=int, default=4000, required=False, help='training steps, default: %(default)s')
|
||||
parser.add_argument('--dim', type=int, default=40, required=False, help='network dimension, default: %(default)s')
|
||||
parser.add_argument('--repeats', type=int, default=10, required=False, help='number of repeats per image, default: %(default)s')
|
||||
parser.add_argument('--alpha', type=float, default=0, required=False, help='alpha for weights scaling, default: half of dim')
|
||||
parser.add_argument('--batch', type=int, default=1, required=False, help='batch size, default: %(default)s')
|
||||
parser.add_argument('--lr', type=float, default=1e-04, required=False, help='model learning rate, default: %(default)s')
|
||||
parser.add_argument('--unetlr', type=float, default=1e-04, required=False, help='unet learning rate, default: %(default)s')
|
||||
parser.add_argument('--textlr', type=float, default=5e-05, required=False, help='text encoder learning rate, default: %(default)s')
|
||||
parser.add_argument('--dreambooth', default=False, action='store_true', help = "use dreambooth style training")
|
||||
parser.add_argument('--lycoris', default=False, action='store_true', help = "use lycoris style training")
|
||||
parser.add_argument('--debug', default=False, action='store_true', help = "enable debug logging")
|
||||
args = parser.parse_args()
|
||||
defaults = Map({ 'options': {}, 'flags': {} }) if args.offline else Map(modules.sdapi.options())
|
||||
|
||||
if args.debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.debug({ 'debug': True })
|
||||
if args.model is None:
|
||||
args.model = defaults.options.get('sd_model_checkpoint', None)
|
||||
args.model = args.model.split(' [')[0] if args.model is not None else None
|
||||
if args.dir is None:
|
||||
args.dir = defaults.flags.get('lora_dir', None)
|
||||
if not os.path.isabs(args.model) and args.dir is not None and not os.path.exists(args.model):
|
||||
args.model = os.path.abspath(os.path.join(args.dir, os.pardir, 'Stable-diffusion', args.model))
|
||||
if args.dir is None:
|
||||
args.dir = os.path.join(args.input, 'lora')
|
||||
if not os.path.exists(args.model) or not os.path.isfile(args.model):
|
||||
log.error({ 'lora cannot find model': args.model })
|
||||
exit(1)
|
||||
if not os.path.exists(args.input) or not os.path.isdir(args.input):
|
||||
log.error({ 'lora cannot find training dir': args.input })
|
||||
exit(1)
|
||||
if not os.path.exists(args.dir) or not os.path.isdir(args.dir):
|
||||
log.error({ 'lora cannot find training dir': args.dir })
|
||||
exit(1)
|
||||
options.pretrained_model_name_or_path = args.model
|
||||
options.output_dir = args.dir
|
||||
options.output_name = args.output
|
||||
options.max_train_steps = args.steps
|
||||
options.network_dim = args.dim
|
||||
options.network_alpha = args.dim // 2 if args.alpha == 0 else args.alpha
|
||||
options.gradient_accumulation_steps = args.gradient
|
||||
options.save_every_n_epochs = args.interim if args.interim > 0 else None
|
||||
options.learning_rate = args.lr
|
||||
options.unet_lr = args.unetlr
|
||||
options.text_encoder_lr = args.textlr
|
||||
options.train_batch_size = args.batch
|
||||
log.info({ 'train lora args': vars(options) })
|
||||
transformers.logging.set_verbosity_error()
|
||||
mem_stats()
|
||||
|
||||
json_file = os.path.join(tempfile.gettempdir(), args.output, args.output + '.json')
|
||||
base = os.path.join(tempfile.gettempdir(), args.output)
|
||||
options.train_data_dir = base
|
||||
res = None
|
||||
|
||||
if args.dreambooth:
|
||||
log.info({ 'using dreambooth style training': True })
|
||||
options.in_json = None
|
||||
else:
|
||||
options.in_json = json_file
|
||||
|
||||
for root, _sub_dirs, folder in os.walk(args.input):
|
||||
files = [os.path.join(root, f) for f in folder]
|
||||
|
||||
if not args.noprocess:
|
||||
# preprocess
|
||||
processing_options = args.process.split(',')
|
||||
processing_options = [opt.strip() for opt in re.split(',| ', args.process)]
|
||||
log.info({ 'processing steps': processing_options })
|
||||
|
||||
if os.path.exists(json_file):
|
||||
os.remove(json_file)
|
||||
|
||||
steps = [step for step in processing_options if step in ['face', 'body', 'original']]
|
||||
for step in steps:
|
||||
# processing_options = [step for step in processing_options if step not in ['face', 'body', 'original']].append(step)
|
||||
if step == 'face':
|
||||
opts = [step for step in processing_options if step not in ['body', 'original']]
|
||||
if step == 'body':
|
||||
opts = [step for step in processing_options if step not in ['face', 'original', 'upscale', 'restore']]
|
||||
if step == 'original':
|
||||
opts = [step for step in processing_options if step not in ['face', 'body', 'upscale', 'restore', 'blur', 'range']]
|
||||
log.info({ 'processing step': opts })
|
||||
concept = step
|
||||
if concept == 'original' and args.tag is not None:
|
||||
concept = args.tag.split(',')[0].strip()
|
||||
dir = os.path.join(base, str(args.repeats) + '_' + concept)
|
||||
if os.path.exists(dir):
|
||||
shutil.rmtree(dir, ignore_errors=True)
|
||||
Path(dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for f in files:
|
||||
try:
|
||||
res, metadata = modules.process.process_file(f = f, dst = dir, preview = False, offline = args.offline, txt = args.dreambooth, tag = args.tag, opts = opts)
|
||||
if not args.dreambooth:
|
||||
with open(json_file, "w") as outfile:
|
||||
outfile.write(json.dumps(metadata, indent=2))
|
||||
except ValueError as e:
|
||||
exit(1)
|
||||
log.info({ 'processed step': step, 'outputs': res, 'inputs': len(files), 'metadata': json_file, 'path': dir })
|
||||
|
||||
modules.process.unload_models()
|
||||
mem_stats()
|
||||
|
||||
|
||||
dirs = [os.path.join(base, dir) for dir in os.listdir(base) if os.path.isdir(os.path.join(base, dir))]
|
||||
log.info({ 'input datasets': dirs, 'metadata': json_file })
|
||||
|
||||
if not args.nolatents and not args.dreambooth:
|
||||
# create latents
|
||||
for dir in dirs:
|
||||
latents.create_vae_latents(Map({ 'input': dir, 'json': json_file }))
|
||||
latents.unload_vae()
|
||||
mem_stats()
|
||||
else:
|
||||
log.info({ 'skip processing': len(files), 'metadata': json_file, 'path': dir })
|
||||
|
||||
if args.shutdown:
|
||||
log.info({ 'server shutdown required': True })
|
||||
modules.sdapi.shutdown()
|
||||
time.sleep(1)
|
||||
|
||||
if args.lycoris:
|
||||
log.info({ 'using lycoris network': True })
|
||||
options.network_module = 'lycoris.kohya'
|
||||
if not args.notrain:
|
||||
train(options)
|
||||
mem_stats()
|
||||
@@ -0,0 +1,591 @@
|
||||
#!/bin/env python
|
||||
# pylint: disable=no-member
|
||||
"""
|
||||
simple implementation of training api: `/sdapi/v1/train`
|
||||
- supports: create embedding, image preprocess, train embedding (with all known parameters)
|
||||
- does not (yet) support: create hyper-network, train hyper-network
|
||||
- compatible with progress api: `/sdapi/v1/progress`
|
||||
- if interrupted, auto-continues from last known step
|
||||
- create and preprocess executed as sync jobs
|
||||
- train is executed as async job with progress monitoring
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import importlib
|
||||
from pathlib import Path, PurePath
|
||||
|
||||
import filetype
|
||||
from PIL import Image
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'modules'))
|
||||
from modules.util import Map, log, set_logfile
|
||||
from modules.sdapi import close, get, interrupt, post, progress, session
|
||||
from modules.process import process_images
|
||||
from modules.grid import grid
|
||||
create_preview = importlib.import_module('modules.preview-embeddings').create_preview
|
||||
plot = importlib.import_module('modules.train-losschart').plot
|
||||
extract = importlib.import_module('modules.video-extract').extract
|
||||
gen_loss_rate_str = importlib.import_module('modules.train-lossrate').gen_loss_rate_str
|
||||
|
||||
images = []
|
||||
args = {}
|
||||
options = None
|
||||
cmdflags = None
|
||||
args = Map({
|
||||
"training_model": "sd-v15-runwayml.ckpt",
|
||||
"extract_video": {
|
||||
"rate": 0,
|
||||
"fps": 5,
|
||||
"vstart": 0,
|
||||
"vend": 0
|
||||
},
|
||||
"create_embedding": {
|
||||
"name": "test",
|
||||
"num_vectors_per_token": 1,
|
||||
"overwrite_old": False,
|
||||
"init_text": "*"
|
||||
},
|
||||
"preprocess": {
|
||||
"id_task": 0,
|
||||
"process_src": "",
|
||||
"process_dst": "",
|
||||
"process_width": 512,
|
||||
"process_height": 512,
|
||||
"process_flip": False,
|
||||
"process_split": False,
|
||||
"process_caption": True,
|
||||
"process_caption_deepbooru": False,
|
||||
"preprocess_txt_action": "ignore",
|
||||
"process_focal_crop": True,
|
||||
"process_focal_crop_face_weight": 0.9,
|
||||
"process_focal_crop_entropy_weight": 0.3,
|
||||
"process_focal_crop_edges_weight": 0.5,
|
||||
"process_focal_crop_debug": False,
|
||||
"split_threshold": 0.5,
|
||||
"overlap_ratio": 0.2,
|
||||
"process_multicrop": None,
|
||||
"process_multicrop_mindim": None,
|
||||
"process_multicrop_maxdim": None,
|
||||
"process_multicrop_minarea": None,
|
||||
"process_multicrop_maxarea": None,
|
||||
"process_multicrop_objective": None,
|
||||
"process_multicrop_threshold": None,
|
||||
},
|
||||
"train_embedding": {
|
||||
"id_task": 0,
|
||||
"embedding_name": "",
|
||||
"learn_rate": -1,
|
||||
"batch_size": 1,
|
||||
"steps": 500,
|
||||
"data_root": "",
|
||||
"log_directory": "train/log",
|
||||
"template_filename": "subject_filewords.txt",
|
||||
"gradient_step": 20,
|
||||
"training_width": 512,
|
||||
"training_height": 512,
|
||||
"shuffle_tags": False,
|
||||
"tag_drop_out": 0,
|
||||
"clip_grad_mode": "disabled",
|
||||
"clip_grad_value": "0.1",
|
||||
"latent_sampling_method": "once",
|
||||
"create_image_every": -1,
|
||||
"save_embedding_every": -1,
|
||||
"save_image_with_stored_embedding": False,
|
||||
"preview_from_txt2img": False,
|
||||
"preview_prompt": "",
|
||||
"preview_negative_prompt": "blurry, duplicate, ugly, deformed, low res, watermark, text",
|
||||
"preview_steps": 20,
|
||||
"preview_sampler_index": 0,
|
||||
"preview_cfg_scale": 6,
|
||||
"preview_seed": -1,
|
||||
"preview_width": 512,
|
||||
"preview_height": 512,
|
||||
"varsize": False,
|
||||
"use_weight": False,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
async def plotloss(params):
|
||||
logdir = os.path.abspath(os.path.join(cmdflags['embeddings_dir'], '../train/log'))
|
||||
try:
|
||||
plot(logdir, params.name)
|
||||
except Exception as err:
|
||||
log.warning({ 'loss chart error': err })
|
||||
|
||||
|
||||
async def captions(docs: list):
|
||||
exclude = ['a', 'in', 'on', 'out', 'at', 'the', 'and', 'with', 'next', 'to', 'it', 'for', 'of', 'into', 'that']
|
||||
d = dict()
|
||||
for f in docs:
|
||||
text = open(f, 'r', encoding='utf-8')
|
||||
for line in text:
|
||||
line = line.strip()
|
||||
line = line.lower()
|
||||
words = line.split(" ")
|
||||
for word in words:
|
||||
if word in exclude:
|
||||
continue
|
||||
d[word] = d[word] + 1 if word in d else 1
|
||||
pairs = ((value, key) for (key,value) in d.items())
|
||||
sort = sorted(pairs, reverse = True)
|
||||
if len(sort) > 10:
|
||||
del sort[10:]
|
||||
d = {k: v for v, k in sort}
|
||||
log.info({ 'top captions': d })
|
||||
|
||||
|
||||
async def preprocess_cleanup(params):
|
||||
log.info({ 'preprocess cleanup': params.dst })
|
||||
for f in Path(params.dst).glob('*.png'):
|
||||
f.unlink()
|
||||
for f in Path(params.dst).glob('*.jpg'):
|
||||
f.unlink()
|
||||
for f in Path(params.dst).glob('*.txt'):
|
||||
f.unlink()
|
||||
try:
|
||||
if os.path.isdir(params.dst):
|
||||
Path(params.dst).rmdir()
|
||||
except Exception as err:
|
||||
log.warning({ 'preprocess cleanup': params.dst, 'error': err })
|
||||
|
||||
|
||||
async def preprocess_builtin(params):
|
||||
global images # pylint: disable=global-statement
|
||||
log.debug({ 'preprocess start' })
|
||||
files = [os.path.join(params.src, f) for f in os.listdir(params.src) if os.path.isfile(os.path.join(params.src, f))]
|
||||
candidates = [f for f in files if filetype.is_image(f)]
|
||||
not_images = [f for f in files if (not filetype.is_image(f) and not f.endswith('.txt'))]
|
||||
images = []
|
||||
low_res = []
|
||||
for f in candidates:
|
||||
img = Image.open(f)
|
||||
mp = (img.size[0] * img.size[1]) / 1024 / 1024
|
||||
if mp < 1 or img.size[0] < 512 or img.size[1] < 512:
|
||||
low_res.append(f)
|
||||
os.rename(f, f + '.skip')
|
||||
else:
|
||||
images.append(f)
|
||||
log.debug({ 'preprocess skipping': not_images })
|
||||
log.debug({ 'preprocess low res': low_res })
|
||||
args.preprocess.process_src = params.src
|
||||
args.preprocess.process_dst = params.dst
|
||||
log.debug({ 'preprocess args': args.preprocess })
|
||||
_res = await post('/sdapi/v1/preprocess', json = args.preprocess)
|
||||
processed = [os.path.join(params.dst, f) for f in os.listdir(params.dst) if os.path.isfile(os.path.join(params.dst, f))]
|
||||
processed_imgs = [f for f in processed if f.endswith('.png')]
|
||||
processed_docs = [f for f in processed if f.endswith('.txt')]
|
||||
log.info({ 'preprocess': {
|
||||
'source': params.src,
|
||||
'destination': params.dst,
|
||||
'files': len(files),
|
||||
'images': len(images),
|
||||
'processed': len(processed_imgs),
|
||||
'captions': len(processed_docs),
|
||||
'skipped': len(not_images),
|
||||
'low-res': len(low_res) }
|
||||
})
|
||||
if len(processed_docs) > 0:
|
||||
await captions(processed_docs)
|
||||
return len(processed_imgs)
|
||||
|
||||
|
||||
async def preprocess(params):
|
||||
global images # pylint: disable=global-statement
|
||||
res = 0
|
||||
if os.path.isfile(params.src):
|
||||
if not filetype.is_video(params.src):
|
||||
kind = filetype.guess(params.src)
|
||||
log.error({ 'preprocess error': { 'not a valid movie file': params.src, 'guess': kind } })
|
||||
else:
|
||||
extract_dst = os.path.join(params.dst, 'extract')
|
||||
log.debug({ 'preprocess args': args.extract_video })
|
||||
images = extract(params.src, extract_dst, rate = args.extract_video.rate, fps = args.extract_video.fps, start = args.extract_video.vstart, end = args.extract_video.vend) # extract keyframes from movie
|
||||
if images > 0:
|
||||
params.src = extract_dst
|
||||
res = await preprocess(params) # call again but now with keyframes
|
||||
else:
|
||||
log.error({ 'preprocess video extract': 'no images' })
|
||||
elif os.path.isdir(params.src):
|
||||
if params.overwrite:
|
||||
await preprocess_cleanup(params)
|
||||
elif os.path.isdir(params.dst):
|
||||
log.error({ 'preprocess output folder already exists': params.dst })
|
||||
return 0
|
||||
|
||||
if params.preprocess == 'builtin':
|
||||
res = await preprocess_builtin(params)
|
||||
i = [os.path.join(params.dst, f) for f in os.listdir(params.dst) if os.path.isfile(os.path.join(params.dst, f)) and filetype.is_image(os.path.join(params.dst, f))]
|
||||
images = [Image.open(img) for img in i]
|
||||
res = len(images)
|
||||
|
||||
elif params.preprocess == 'custom':
|
||||
t0 = time.perf_counter()
|
||||
args.preprocess.process_src = params.src
|
||||
args.preprocess.process_dst = params.dst
|
||||
process_images(src = params.src, dst = params.dst)
|
||||
i = [os.path.join(params.dst, f) for f in os.listdir(params.dst) if os.path.isfile(os.path.join(params.dst, f)) and filetype.is_image(os.path.join(params.dst, f))]
|
||||
images = [Image.open(img) for img in i]
|
||||
t1 = time.perf_counter()
|
||||
log.info({ 'preprocess': { 'source': params.src, 'destination': params.dst, 'images': len(images), 'time': round(t1 - t0, 2) } })
|
||||
res = len(images)
|
||||
|
||||
else:
|
||||
args.preprocess.process_dst = params.src
|
||||
i = [os.path.join(params.src, f) for f in os.listdir(params.src) if os.path.isfile(os.path.join(params.src, f)) and filetype.is_image(os.path.join(params.src, f))]
|
||||
images = [Image.open(img) for img in i]
|
||||
res = len(images)
|
||||
|
||||
else:
|
||||
log.error({ 'preprocess error': { 'not a valid input': params.src } })
|
||||
if len(images) > 0:
|
||||
img = grid(images, labels = None, width = 2048, height = 2048, border = 8, square = True)
|
||||
logdir = os.path.abspath(os.path.join(cmdflags['embeddings_dir'], '../train/log'))
|
||||
Path(logdir).mkdir(parents = True, exist_ok = True)
|
||||
fn = os.path.join(logdir, params.name + '.inputs.jpg')
|
||||
img.save(fn)
|
||||
log.info({ 'preprocess input grid': fn })
|
||||
return res
|
||||
|
||||
|
||||
async def check(params):
|
||||
global options # pylint: disable=global-statement
|
||||
options = await get('/sdapi/v1/options')
|
||||
global cmdflags # pylint: disable=global-statement
|
||||
cmdflags = await get('/sdapi/v1/cmd-flags')
|
||||
|
||||
logdir = os.path.abspath(os.path.join(cmdflags['embeddings_dir'], '../train/log', params.name))
|
||||
logfile = os.path.abspath(os.path.join(cmdflags['embeddings_dir'], '../train/log', params.name + '.train.log'))
|
||||
set_logfile(logfile)
|
||||
|
||||
log.info({ 'checking server options' })
|
||||
|
||||
options['training_image_repeats_per_epoch'] = 1
|
||||
|
||||
if params.skipmodel:
|
||||
log.info({ 'using model': options['sd_model_checkpoint'] })
|
||||
else:
|
||||
log.debug({ 'check model': args.training_model })
|
||||
if len(args.training_model) > 0 and not options['sd_model_checkpoint'].startswith(args.training_model):
|
||||
models = await get('/sdapi/v1/sd-models')
|
||||
models = [obj["title"] for obj in models]
|
||||
found = [i for i in models if i.startswith(args.training_model)]
|
||||
if len(found) == 0:
|
||||
log.error({ 'model not found': args.training_model, 'available': models })
|
||||
exit()
|
||||
else:
|
||||
log.warning({ 'switching model': found[0] })
|
||||
options['sd_model_checkpoint'] = found[0]
|
||||
|
||||
log.debug({ 'check embedding': params.name })
|
||||
|
||||
lst = os.path.join(cmdflags['embeddings_dir'])
|
||||
log.debug({ 'embeddings folder': lst })
|
||||
path = Path(cmdflags['embeddings_dir']).glob(f'{params.name}.pt*')
|
||||
matches = [f for f in path]
|
||||
for match in matches:
|
||||
if params.overwrite:
|
||||
log.info({ 'delete embedding': match.name })
|
||||
os.remove(os.path.join(cmdflags['embeddings_dir'], match.name))
|
||||
else:
|
||||
log.error({ 'embedding exists': match.name })
|
||||
await close()
|
||||
exit()
|
||||
f = os.path.join(logdir, 'train.csv')
|
||||
if os.path.isfile(f):
|
||||
if params.overwrite:
|
||||
log.info({ 'delete training log': f })
|
||||
os.remove(os.path.join(logdir, 'train.csv'))
|
||||
else:
|
||||
log.warning({ 'training log exists': f })
|
||||
f = os.path.join(logdir, '..', params.name, '.png')
|
||||
if os.path.isfile(f):
|
||||
if params.overwrite:
|
||||
log.info({ 'delete training graph': f })
|
||||
os.remove(f)
|
||||
|
||||
log.debug({ 'options': 'update' })
|
||||
await post('/sdapi/v1/options', options)
|
||||
|
||||
return
|
||||
|
||||
|
||||
async def create(params):
|
||||
log.debug({ 'create start' })
|
||||
if not os.path.isdir(args.preprocess.process_dst):
|
||||
log.error({ 'train source not found': args.preprocess.process_dst })
|
||||
exit()
|
||||
if params.vectors == -1: # dynamically determine number of vectors depending on number of input images
|
||||
if len(images) <= 20:
|
||||
vectors = 2
|
||||
elif len(images) <= 100:
|
||||
vectors = 4
|
||||
else:
|
||||
vectors = 6
|
||||
else:
|
||||
vectors = params.vectors
|
||||
if os.path.exists(params.name) and os.path.isfile(params.name):
|
||||
log.info({ 'deleting existing embedding': { 'name': params.name } })
|
||||
os.remove(params.name)
|
||||
args.create_embedding.name = params.name
|
||||
words = params.init.split(',')
|
||||
if len(words) > vectors:
|
||||
params.init = ','.join(words[:vectors])
|
||||
log.warning({ 'create embedding init words cut': params.init })
|
||||
args.create_embedding.init_text = params.init
|
||||
args.create_embedding.num_vectors_per_token = vectors
|
||||
log.debug({ 'create args': args.create_embedding })
|
||||
res = await post('/sdapi/v1/create/embedding', args.create_embedding)
|
||||
if 'info' in res:
|
||||
log.info({ 'create embedding': { 'name': params.name, 'init': params.init, 'vectors': vectors, 'message': res.info } })
|
||||
else:
|
||||
log.error({ 'create failed:', res })
|
||||
return None
|
||||
log.debug({ 'create end' })
|
||||
return params.name
|
||||
|
||||
|
||||
async def train(params):
|
||||
log.debug({ 'train start' })
|
||||
args.train_embedding.embedding_name = params.name
|
||||
|
||||
imgs = [f for f in os.listdir(args.preprocess.process_dst) if os.path.isfile(os.path.join(args.preprocess.process_dst, f)) and filetype.is_image(os.path.join(args.preprocess.process_dst, f))]
|
||||
args.train_embedding.data_root = args.preprocess.process_dst
|
||||
if len(imgs) == 0:
|
||||
log.error({ 'train no input images in folder': args.preprocess.process_dst })
|
||||
return
|
||||
|
||||
if params.grad == -1:
|
||||
args.train_embedding.gradient_step = len(imgs) // args.train_embedding.batch_size
|
||||
divisor = args.train_embedding.gradient_step // 60
|
||||
args.train_embedding.gradient_step = args.train_embedding.gradient_step // (1 + divisor)
|
||||
log.info({ 'dynamic gradient step': args.train_embedding.gradient_step })
|
||||
if params.steps == -1:
|
||||
args.train_embedding.steps = params.maxsteps // args.train_embedding.gradient_step
|
||||
log.info({ 'dynamic steps': args.train_embedding.steps, 'estimated total steps': args.train_embedding.steps * args.train_embedding.gradient_step * args.train_embedding.batch_size })
|
||||
|
||||
epoch_size = args.train_embedding.batch_size * args.train_embedding.gradient_step
|
||||
if args.train_embedding.create_image_every == -1:
|
||||
args.train_embedding.create_image_every = args.train_embedding.steps // 10
|
||||
if args.train_embedding.save_embedding_every == -1:
|
||||
args.train_embedding.save_embedding_every = args.train_embedding.steps // 10
|
||||
if args.train_embedding.learn_rate == -1:
|
||||
loss_args = {
|
||||
"steps": args.train_embedding.steps,
|
||||
"step": args.train_embedding.create_image_every,
|
||||
"loss_start": params.rstart,
|
||||
"loss_end": params.rend,
|
||||
"loss_type": 'power',
|
||||
"power": params.rdescend
|
||||
}
|
||||
args.train_embedding.learn_rate = gen_loss_rate_str(**loss_args)
|
||||
log.info({ 'dynamic learn-rate': loss_args })
|
||||
log.debug({ 'learn rate': args.train_embedding.learn_rate, 'params': loss_args })
|
||||
|
||||
log.info({ 'train embedding': {
|
||||
'name': params.name,
|
||||
'source': args.preprocess.process_dst,
|
||||
'images': len(imgs),
|
||||
'steps': args.train_embedding.steps,
|
||||
'batch': args.train_embedding.batch_size,
|
||||
'gradient-step': args.train_embedding.gradient_step,
|
||||
'sampling': args.train_embedding.latent_sampling_method,
|
||||
'epoch-size': epoch_size }
|
||||
})
|
||||
log.info({ 'learn rate': args.train_embedding.learn_rate })
|
||||
log.debug({ 'train args': args.train_embedding })
|
||||
t0 = time.time()
|
||||
res = await post('/sdapi/v1/train/embedding', args.train_embedding)
|
||||
log.info({ 'train result': res })
|
||||
t1 = time.time()
|
||||
log.info({ 'train embedding finished': { 'name': params.name, 'time': round(t1 - t0) } })
|
||||
log.debug({ 'train end': res.info if 'info' in res else res })
|
||||
return
|
||||
|
||||
|
||||
async def pipeline(params):
|
||||
log.debug({ 'pipeline start' })
|
||||
|
||||
# interrupt
|
||||
await interrupt()
|
||||
|
||||
# preprocess
|
||||
num = await preprocess(params)
|
||||
if num == 0:
|
||||
log.warning({ 'preprocess': 'no resulting images'})
|
||||
return
|
||||
|
||||
# create embedding
|
||||
name = await create(params)
|
||||
if not params.name in name:
|
||||
log.error({ 'create embedding failed': name })
|
||||
return
|
||||
|
||||
# train embedding
|
||||
await train(params)
|
||||
|
||||
await plotloss(params)
|
||||
|
||||
# create_preview(params.name, params.init)
|
||||
|
||||
log.debug({ 'pipeline end' })
|
||||
return
|
||||
|
||||
|
||||
async def monitor(params):
|
||||
step = 0
|
||||
t0 = time.perf_counter()
|
||||
t1 = time.perf_counter()
|
||||
log.info({' starting monitor': t0 })
|
||||
finished = 0
|
||||
while True:
|
||||
await asyncio.sleep(params.monitor)
|
||||
res = await progress()
|
||||
if not 'state' in res:
|
||||
log.info({ 'monitor disconnected': res })
|
||||
break
|
||||
if (res.state.job_count == params.steps and res.state.job_no >= res.state.job_count) or (res.eta_relative < 0) or (res.interrupted) or (res.state.job_count == 0): # need exit case if interrupted or failed
|
||||
if res.interrupted:
|
||||
log.info({ 'monitor interrupted': { 'embedding': params.name } })
|
||||
break # exit for monitor job
|
||||
else:
|
||||
finished += 1
|
||||
if finished >= 2: # do it more than once since preprocessing job can finish just in time for monitor to finish
|
||||
log.info({ 'monitor finished': { 'embedding': params.name } })
|
||||
break
|
||||
else:
|
||||
if res.state.job_no == 0:
|
||||
step = 0
|
||||
t0 = time.perf_counter()
|
||||
t1 = time.perf_counter()
|
||||
try:
|
||||
if 'Loss:' in res.textinfo:
|
||||
text = res.textinfo.split('<br/>')[0].split()
|
||||
loss = float(text[-1])
|
||||
else:
|
||||
loss = -1
|
||||
except:
|
||||
loss = -1
|
||||
if math.isnan(loss):
|
||||
log.error({ 'monitor': { 'progress': round(100 * res.progress), 'embedding': params.name, 'eta': round(res.eta_relative), 'step': res.state.job_no, 'steps': res.state.job_count, 'loss': 'nan' } })
|
||||
await interrupt()
|
||||
else:
|
||||
elapsed = t1 - t0
|
||||
log.info({ 'monitor': {
|
||||
'job': res.state.job,
|
||||
'progress': round(100 * res.progress),
|
||||
'embedding': params.name,
|
||||
'epoch': (1 + res.state.job_no // len(images)) if len(images) > 0 else 'n/a',
|
||||
'step': res.state.job_no,
|
||||
'steps': res.state.job_count,
|
||||
'loss': loss if loss > -1 else 'n/a',
|
||||
'total': round(1.0 * elapsed * res.state.job_count / res.state.job_no) if res.state.job_no > 0 and t1 != t0 else 'n/a',
|
||||
'elapsed': round(elapsed),
|
||||
'remaining': round(res.eta_relative),
|
||||
'it/s': round((res.state.job_no - step) / (time.perf_counter() - t1), 2) }
|
||||
})
|
||||
if step % 10 == 0:
|
||||
await plotloss(params)
|
||||
step = res.state.job_no
|
||||
t1 = time.perf_counter()
|
||||
return
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description="sd train ti pipeline")
|
||||
parser.add_argument("--name", type = str, required = True, help = "embedding name, set to auto to use src folder name")
|
||||
parser.add_argument("--src", type = str, required = True, help = "source image folder or movie file")
|
||||
parser.add_argument("--init", type = str, default = "person", required = False, help = "initialization class, default: %(default)s")
|
||||
parser.add_argument("--dst", type = str, default = "/tmp", required = False, help = "destination image folder for processed images, default: %(default)s")
|
||||
parser.add_argument("--steps", type = int, default = -1, required = False, help = "training steps, default: %(default)s")
|
||||
parser.add_argument("--maxsteps", type = int, default = 5000, required = False, help = "max training steps used when dynamic gradient is active, default: %(default)s")
|
||||
parser.add_argument("--vectors", type = int, default = -1, required = False, help = "number of vectors per token, default: dynamic based on number of input images")
|
||||
parser.add_argument("--batch", type = int, default = 1, required = False, help = "batch size, default: %(default)s")
|
||||
parser.add_argument("--rate", type = str, default = "", required = False, help = "learn rate, default: dynamic")
|
||||
parser.add_argument("--rstart", type = float, default = 0.02, required = False, help = "starting learn rate if using dynamic rate, default: %(default)s")
|
||||
parser.add_argument("--rend", type = float, default = 0.0005, required = False, help = "ending learn rate if using dynamic rate, default: %(default)s")
|
||||
parser.add_argument("--rdescend", type = float, default = 2, required = False, help = "learn rate descend power when using dynamic rate, default: %(default)s")
|
||||
parser.add_argument("--grad", type = int, default = -1, required = False, help = "accumulate gradient over n images, default: : %(default)s")
|
||||
parser.add_argument("--type", type = str, default = 'subject', required = False, help = "training type: subject/style/unknown, default: %(default)s")
|
||||
parser.add_argument('--overwrite', default = False, action='store_true', help = "overwrite existing embedding, default: %(default)s")
|
||||
parser.add_argument("--vstart", type = float, default = 0, required = False, help = "if processing video skip first n seconds, default: %(default)s")
|
||||
parser.add_argument("--vend", type = float, default = 0, required = False, help = "if processing video skip last n seconds, default: %(default)s")
|
||||
parser.add_argument('--skipcaption', default = False, action='store_true', help = "do not auto-generate captions, default: %(default)s")
|
||||
parser.add_argument('--skipmodel', default = False, action='store_true', help = "skip model validation and switch, default: %(default)s")
|
||||
parser.add_argument('--preprocess', type = str, choices=['builtin', 'custom', 'none'], default = 'custom', help = "preprocessing type, default: %(default)s")
|
||||
parser.add_argument('--nocleanup', default = False, action='store_true', help = "skip cleanup after completion, default: %(default)s")
|
||||
parser.add_argument("--monitor", type = int, default = 30, required = False, help = "progress monitor frequency, default: : %(default)s")
|
||||
parser.add_argument('--debug', default = False, action='store_true', help = "print extra debug information, default: %(default)s")
|
||||
params = parser.parse_args()
|
||||
if params.debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.debug({ 'debug': True })
|
||||
log.debug({ 'args': params.__dict__ })
|
||||
home = Path(sys.argv[0]).parent
|
||||
global args # pylint: disable=global-statement
|
||||
if params.vstart > 0:
|
||||
args.extract_video.vstart = params.vstart
|
||||
if params.vend > 0:
|
||||
args.extract_video.vend = params.vend
|
||||
if params.steps > -1:
|
||||
args.train_embedding.steps = params.steps
|
||||
if params.batch > -1:
|
||||
args.train_embedding.batch_size = params.batch
|
||||
if params.rate != '':
|
||||
args.train_embedding.learn_rate = params.rate
|
||||
if params.grad > -1:
|
||||
args.train_embedding.gradient_step = params.grad
|
||||
if params.type == 'subject':
|
||||
if params.skipcaption:
|
||||
args.train_embedding.template_filename = 'subject.txt'
|
||||
args.preprocess.process_caption = False
|
||||
else:
|
||||
args.train_embedding.template_filename = 'subject_filewords.txt'
|
||||
elif params.type == 'style':
|
||||
if params.skipcaption:
|
||||
args.train_embedding.template_filename = 'style.txt'
|
||||
args.preprocess.process_caption = False
|
||||
else:
|
||||
args.train_embedding.template_filename = 'style_filewords.txt'
|
||||
else:
|
||||
if params.skipcaption:
|
||||
args.train_embedding.template_filename = 'unknown.txt'
|
||||
args.preprocess.process_caption = False
|
||||
else:
|
||||
args.train_embedding.template_filename = 'unknown_filewords.txt'
|
||||
if params.name == 'auto':
|
||||
params.name = PurePath(params.src).name
|
||||
log.info({ 'training name': params.name })
|
||||
if params.dst == "/tmp":
|
||||
params.dst = os.path.join("/tmp/train", params.name)
|
||||
log.debug({ 'args': params.__dict__ })
|
||||
params.src = os.path.abspath(params.src)
|
||||
params.dst = os.path.abspath(params.dst)
|
||||
|
||||
try:
|
||||
await session()
|
||||
await check(params)
|
||||
a = asyncio.create_task(pipeline(params))
|
||||
b = asyncio.create_task(monitor(params))
|
||||
await asyncio.gather(a, b) # wait for both pipeline and monitor to finish
|
||||
except Exception as e:
|
||||
log.error({ 'exception': e })
|
||||
finally:
|
||||
if not params.nocleanup:
|
||||
await preprocess_cleanup(params)
|
||||
await close()
|
||||
return
|
||||
|
||||
if __name__ == "__main__":
|
||||
log.info({ 'train textual inversion' })
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
log.warning({ 'interrupted': 'keyboard request' })
|
||||
# asyncio.run(interrupt())
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pathlib
|
||||
import argparse
|
||||
import warnings
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
from torchvision import transforms
|
||||
from tqdm import tqdm
|
||||
from util import Map
|
||||
|
||||
from rich.pretty import install as pretty_install
|
||||
from rich.traceback import install as traceback_install
|
||||
from rich.console import Console
|
||||
|
||||
console = Console(log_time=True, log_time_format='%H:%M:%S-%f')
|
||||
pretty_install(console=console)
|
||||
traceback_install(console=console, extra_lines=1, width=console.width, word_wrap=False, indent_guides=False)
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'modules', 'lora'))
|
||||
import library.model_util as model_util
|
||||
import library.train_util as train_util
|
||||
|
||||
warnings.filterwarnings('ignore')
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
options = Map({
|
||||
'batch': 1,
|
||||
'input': '',
|
||||
'json': '',
|
||||
'max': 1024,
|
||||
'min': 256,
|
||||
'noupscale': False,
|
||||
'precision': 'fp32',
|
||||
'resolution': '512,512',
|
||||
'steps': 64,
|
||||
'vae': 'stabilityai/sd-vae-ft-mse'
|
||||
})
|
||||
vae = None
|
||||
|
||||
|
||||
def get_latents(vae, images, weight_dtype):
|
||||
image_transforms = transforms.Compose([ transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ])
|
||||
img_tensors = [image_transforms(image) for image in images]
|
||||
img_tensors = torch.stack(img_tensors)
|
||||
img_tensors = img_tensors.to(device, weight_dtype)
|
||||
with torch.no_grad():
|
||||
latents = vae.encode(img_tensors).latent_dist.sample().float().to('cpu').numpy()
|
||||
return latents
|
||||
|
||||
|
||||
def get_npz_filename_wo_ext(data_dir, image_key):
|
||||
return os.path.join(data_dir, os.path.splitext(os.path.basename(image_key))[0])
|
||||
|
||||
|
||||
def create_vae_latents(params):
|
||||
args = Map({**options, **params})
|
||||
console.log(f'create vae latents args: {args}')
|
||||
image_paths = train_util.glob_images(args.input)
|
||||
if os.path.exists(args.json):
|
||||
with open(args.json, 'rt', encoding='utf-8') as f:
|
||||
metadata = json.load(f)
|
||||
else:
|
||||
return
|
||||
if args.precision == 'fp16':
|
||||
weight_dtype = torch.float16
|
||||
elif args.precision == 'bf16':
|
||||
weight_dtype = torch.bfloat16
|
||||
else:
|
||||
weight_dtype = torch.float32
|
||||
global vae
|
||||
if vae is None:
|
||||
vae = model_util.load_vae(args.vae, weight_dtype)
|
||||
vae.eval()
|
||||
vae.to(device, dtype=weight_dtype)
|
||||
max_reso = tuple([int(t) for t in args.resolution.split(',')])
|
||||
assert len(max_reso) == 2, f'illegal resolution: {args.resolution}'
|
||||
bucket_manager = train_util.BucketManager(args.noupscale, max_reso, args.min, args.max, args.steps)
|
||||
if not args.noupscale:
|
||||
bucket_manager.make_buckets()
|
||||
img_ar_errors = []
|
||||
def process_batch(is_last):
|
||||
for bucket in bucket_manager.buckets:
|
||||
if (is_last and len(bucket) > 0) or len(bucket) >= args.batch:
|
||||
latents = get_latents(vae, [img for _, img in bucket], weight_dtype)
|
||||
assert latents.shape[2] == bucket[0][1].shape[0] // 8 and latents.shape[3] == bucket[0][1].shape[1] // 8, f'latent shape {latents.shape}, {bucket[0][1].shape}'
|
||||
for (image_key, _), latent in zip(bucket, latents):
|
||||
npz_file_name = get_npz_filename_wo_ext(args.input, image_key)
|
||||
np.savez(npz_file_name, latent)
|
||||
bucket.clear()
|
||||
data = [[(None, ip)] for ip in image_paths]
|
||||
bucket_counts = {}
|
||||
for data_entry in tqdm(data, smoothing=0.0):
|
||||
if data_entry[0] is None:
|
||||
continue
|
||||
img_tensor, image_path = data_entry[0]
|
||||
if img_tensor is not None:
|
||||
image = transforms.functional.to_pil_image(img_tensor)
|
||||
else:
|
||||
image = Image.open(image_path)
|
||||
image_key = os.path.basename(image_path)
|
||||
image_key = os.path.join(os.path.basename(pathlib.Path(image_path).parent), pathlib.Path(image_path).stem)
|
||||
if image_key not in metadata:
|
||||
metadata[image_key] = {}
|
||||
reso, resized_size, ar_error = bucket_manager.select_bucket(image.width, image.height)
|
||||
img_ar_errors.append(abs(ar_error))
|
||||
bucket_counts[reso] = bucket_counts.get(reso, 0) + 1
|
||||
metadata[image_key]['train_resolution'] = (reso[0] - reso[0] % 8, reso[1] - reso[1] % 8)
|
||||
if not args.noupscale:
|
||||
assert resized_size[0] == reso[0] or resized_size[1] == reso[1], f'internal error, resized size not match: {reso}, {resized_size}, {image.width}, {image.height}'
|
||||
assert resized_size[0] >= reso[0] and resized_size[1] >= reso[1], f'internal error, resized size too small: {reso}, {resized_size}, {image.width}, {image.height}'
|
||||
assert resized_size[0] >= reso[0] and resized_size[1] >= reso[1], f'internal error resized size is small: {resized_size}, {reso}'
|
||||
image = np.array(image)
|
||||
if resized_size[0] != image.shape[1] or resized_size[1] != image.shape[0]:
|
||||
image = cv2.resize(image, resized_size, interpolation=cv2.INTER_AREA)
|
||||
if resized_size[0] > reso[0]:
|
||||
trim_size = resized_size[0] - reso[0]
|
||||
image = image[:, trim_size//2:trim_size//2 + reso[0]]
|
||||
if resized_size[1] > reso[1]:
|
||||
trim_size = resized_size[1] - reso[1]
|
||||
image = image[trim_size//2:trim_size//2 + reso[1]]
|
||||
assert image.shape[0] == reso[1] and image.shape[1] == reso[0], f'internal error, illegal trimmed size: {image.shape}, {reso}'
|
||||
bucket_manager.add_image(reso, (image_key, image))
|
||||
process_batch(False)
|
||||
|
||||
process_batch(True)
|
||||
vae.to('cpu')
|
||||
|
||||
bucket_manager.sort()
|
||||
img_ar_errors = np.array(img_ar_errors)
|
||||
for i, reso in enumerate(bucket_manager.resos):
|
||||
count = bucket_counts.get(reso, 0)
|
||||
if count > 0:
|
||||
console.log(f'vae latents bucket: {i+1}/{len(bucket_manager.resos)} resolution: {reso} images: {count} mean-ar-error: {np.mean(img_ar_errors)}')
|
||||
with open(args.json, 'wt', encoding='utf-8') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
|
||||
def unload_vae():
|
||||
global vae
|
||||
vae = None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('input', type=str, help='directory for train images')
|
||||
parser.add_argument('--json', type=str, required=True, help='metadata file to input')
|
||||
parser.add_argument('--vae', type=str, required=True, help='model name or path to encode latents')
|
||||
parser.add_argument('--batch', type=int, default=1, help='batch size in inference')
|
||||
parser.add_argument('--resolution', type=str, default='512,512', help='max resolution in fine tuning (width,height)')
|
||||
parser.add_argument('--min', type=int, default=256, help='minimum resolution for buckets')
|
||||
parser.add_argument('--max', type=int, default=1024, help='maximum resolution for buckets')
|
||||
parser.add_argument('--steps', type=int, default=64, help='steps of resolution for buckets, divisible by 8')
|
||||
parser.add_argument('--noupscale', action='store_true', help='make bucket for each image without upscaling')
|
||||
parser.add_argument('--precision', type=str, default='fp32', choices=['fp32', 'fp16', 'bf16'], help='use precision')
|
||||
params = parser.parse_args()
|
||||
create_vae_latents(vars(params))
|
||||
@@ -0,0 +1,141 @@
|
||||
from util import Map
|
||||
|
||||
embedding = Map({
|
||||
"id_task": 0,
|
||||
"embedding_name": "",
|
||||
"learn_rate": -1,
|
||||
"batch_size": 1,
|
||||
"steps": 500,
|
||||
"data_root": "",
|
||||
"log_directory": "train/log",
|
||||
"template_filename": "subject_filewords.txt",
|
||||
"gradient_step": 20,
|
||||
"training_width": 512,
|
||||
"training_height": 512,
|
||||
"shuffle_tags": False,
|
||||
"tag_drop_out": 0,
|
||||
"clip_grad_mode": "disabled",
|
||||
"clip_grad_value": "0.1",
|
||||
"latent_sampling_method": "deterministic",
|
||||
"create_image_every": 0,
|
||||
"save_embedding_every": 0,
|
||||
"save_image_with_stored_embedding": False,
|
||||
"preview_from_txt2img": False,
|
||||
"preview_prompt": "",
|
||||
"preview_negative_prompt": "blurry, duplicate, ugly, deformed, low res, watermark, text",
|
||||
"preview_steps": 20,
|
||||
"preview_sampler_index": 0,
|
||||
"preview_cfg_scale": 6,
|
||||
"preview_seed": -1,
|
||||
"preview_width": 512,
|
||||
"preview_height": 512,
|
||||
"varsize": False,
|
||||
"use_weight": False,
|
||||
})
|
||||
|
||||
lora = Map({
|
||||
"bucket_no_upscale": False,
|
||||
"bucket_reso_steps": 64,
|
||||
"cache_latents": True,
|
||||
"caption_dropout_every_n_epochs": None,
|
||||
"caption_dropout_rate": 0.0,
|
||||
"caption_extension": ".txt",
|
||||
"caption_extention": ".txt",
|
||||
"caption_tag_dropout_rate": 0.0,
|
||||
"clip_skip": None,
|
||||
"color_aug": False,
|
||||
"dataset_repeats": 1,
|
||||
"debug_dataset": False,
|
||||
"enable_bucket": False,
|
||||
"face_crop_aug_range": None,
|
||||
"flip_aug": False,
|
||||
"full_fp16": False,
|
||||
"gradient_accumulation_steps": 1,
|
||||
"gradient_checkpointing": False,
|
||||
"in_json": "",
|
||||
"keep_tokens": None,
|
||||
"learning_rate": 5e-05,
|
||||
"log_prefix": None,
|
||||
"logging_dir": None,
|
||||
"lr_scheduler_num_cycles": 1,
|
||||
"lr_scheduler_power": 1,
|
||||
"lr_scheduler": "cosine",
|
||||
"lr_warmup_steps": 0,
|
||||
"max_bucket_reso": 1024,
|
||||
"max_data_loader_n_workers": 8,
|
||||
"max_grad_norm": 0.0,
|
||||
"max_token_length": None,
|
||||
"max_train_epochs": None,
|
||||
"max_train_steps": 2500,
|
||||
"mem_eff_attn": False,
|
||||
"min_bucket_reso": 256,
|
||||
"mixed_precision": "fp16",
|
||||
"network_alpha": 1.0,
|
||||
"network_args": None,
|
||||
"network_dim": 16,
|
||||
"network_module": "networks.lora",
|
||||
"network_train_text_encoder_only": False,
|
||||
"network_train_unet_only": False,
|
||||
"network_weights": None,
|
||||
"no_metadata": False,
|
||||
"output_dir": "",
|
||||
"output_name": "",
|
||||
"persistent_data_loader_workers": False,
|
||||
"pretrained_model_name_or_path": "",
|
||||
"prior_loss_weight": 1.0,
|
||||
"random_crop": False,
|
||||
"reg_data_dir": None,
|
||||
"resolution": "512,512",
|
||||
"resume": None,
|
||||
"save_every_n_epochs": None,
|
||||
"save_last_n_epochs_state": None,
|
||||
"save_last_n_epochs": None,
|
||||
"save_model_as": "ckpt",
|
||||
"save_n_epoch_ratio": None,
|
||||
"save_precision": "fp16",
|
||||
"save_state": False,
|
||||
"seed": 42,
|
||||
"shuffle_caption": False,
|
||||
"text_encoder_lr": 5e-05,
|
||||
"train_batch_size": 1,
|
||||
"train_data_dir": "",
|
||||
"training_comment": "mood-magic",
|
||||
"unet_lr": 1e-04,
|
||||
"use_8bit_adam": False,
|
||||
"v_parameterization": False,
|
||||
"v2": False,
|
||||
"vae": None,
|
||||
"xformers": False,
|
||||
})
|
||||
|
||||
process = Map({
|
||||
# general settings, do not modify
|
||||
'format': '.jpg', # image format
|
||||
'target_size': 512, # target resolution
|
||||
'segmentation_model': 0, # segmentation model 0/general 1/landscape
|
||||
'segmentation_background': (192, 192, 192), # segmentation background color
|
||||
'blur_score': 1.8, # max score for face blur detection
|
||||
'blur_samplesize': 60, # sample size to use for blur detection
|
||||
'similarity_score': 0.8, # maximum similarity score before image is discarded
|
||||
'similarity_size': 64, # base similarity detection on reduced images
|
||||
'range_score': 0.15, # min score for face color dynamicrange detection
|
||||
# face processing settings
|
||||
'face_score': 0.7, # min face detection score
|
||||
'face_pad': 0.1, # pad face image percentage
|
||||
'face_model': 1, # which face model to use 0/close-up 1/standard
|
||||
# body processing settings
|
||||
'body_score': 0.9, # min body detection score
|
||||
'body_visibility': 0.5, # min visibility score for each detected body part
|
||||
'body_parts': 15, # min number of detected body parts with sufficient visibility
|
||||
'body_pad': 0.2, # pad body image percentage
|
||||
'body_model': 2, # body model to use 0/low 1/medium 2/high
|
||||
# similarity detection settings
|
||||
# interrogate settings
|
||||
'interrogate': False, # interrogate images
|
||||
'interrogate_model': ['clip', 'deepdanbooru'], # interrogate models
|
||||
'tag_limit': 5, # number of tags to extract
|
||||
# validations
|
||||
# tbd
|
||||
'face_segmentation': False, # segmentation enabled
|
||||
'body_segmentation': False, # segmentation enabled
|
||||
})
|
||||
@@ -0,0 +1,326 @@
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import math
|
||||
import base64
|
||||
import pathlib
|
||||
import numpy as np
|
||||
import mediapipe as mp
|
||||
from PIL import Image, ImageOps
|
||||
from skimage.metrics import structural_similarity as ssim
|
||||
from scipy.stats import beta
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__)))
|
||||
|
||||
import util
|
||||
import sdapi
|
||||
import options
|
||||
|
||||
face_model = None
|
||||
body_model = None
|
||||
segmentation_model = None
|
||||
all_images = []
|
||||
all_images_by_type = {}
|
||||
|
||||
|
||||
class Result(object):
|
||||
def __init__(self, type: str, input: str, tag: str = None, requested: list = []):
|
||||
self.type = type
|
||||
self.input = input
|
||||
self.output = ''
|
||||
self.basename = ''
|
||||
self.message = ''
|
||||
self.image = None
|
||||
self.caption = ''
|
||||
self.tag = tag
|
||||
self.tags = []
|
||||
self.ops = []
|
||||
self.steps = requested
|
||||
|
||||
|
||||
def detect_blur(image: Image):
|
||||
# based on <https://github.com/karthik9319/Blur-Detection/>
|
||||
bw = ImageOps.grayscale(image)
|
||||
cx, cy = image.size[0] // 2, image.size[1] // 2
|
||||
fft = np.fft.fft2(bw)
|
||||
fftShift = np.fft.fftshift(fft)
|
||||
fftShift[cy - options.process.blur_samplesize: cy + options.process.blur_samplesize, cx - options.process.blur_samplesize: cx + options.process.blur_samplesize] = 0
|
||||
fftShift = np.fft.ifftshift(fftShift)
|
||||
recon = np.fft.ifft2(fftShift)
|
||||
magnitude = np.log(np.abs(recon))
|
||||
mean = round(np.mean(magnitude), 2)
|
||||
return mean
|
||||
|
||||
|
||||
def detect_dynamicrange(image: Image):
|
||||
# based on <https://towardsdatascience.com/measuring-enhancing-image-quality-attributes-234b0f250e10>
|
||||
data = np.asarray(image)
|
||||
image = np.float32(data)
|
||||
RGB = [0.299, 0.587, 0.114]
|
||||
height, width = image.shape[:2]
|
||||
brightness_image = np.sqrt(image[..., 0] ** 2 * RGB[0] + image[..., 1] ** 2 * RGB[1] + image[..., 2] ** 2 * RGB[2])
|
||||
hist, _ = np.histogram(brightness_image, bins=256, range=(0, 255))
|
||||
img_brightness_pmf = hist / (height * width)
|
||||
dist = beta(2, 2)
|
||||
ys = dist.pdf(np.linspace(0, 1, 256))
|
||||
ref_pmf = ys / np.sum(ys)
|
||||
dot_product = np.dot(ref_pmf, img_brightness_pmf)
|
||||
squared_dist_a = np.sum(ref_pmf ** 2)
|
||||
squared_dist_b = np.sum(img_brightness_pmf ** 2)
|
||||
res = dot_product / math.sqrt(squared_dist_a * squared_dist_b)
|
||||
return round(res, 2)
|
||||
|
||||
|
||||
def detect_simmilar(image: Image):
|
||||
img = image.resize((options.process.similarity_size, options.process.similarity_size))
|
||||
img = ImageOps.grayscale(img)
|
||||
data = np.array(img)
|
||||
similarity = 0
|
||||
for i in all_images:
|
||||
val = ssim(data, i, data_range=255, channel_axis=None, gradient=False, full=False)
|
||||
if val > similarity:
|
||||
similarity = val
|
||||
all_images.append(data)
|
||||
return similarity
|
||||
|
||||
|
||||
def segmentation(res: Result):
|
||||
global segmentation_model
|
||||
if segmentation_model is None:
|
||||
segmentation_model = mp.solutions.selfie_segmentation.SelfieSegmentation(model_selection=options.process.segmentation_model)
|
||||
data = np.array(res.image)
|
||||
results = segmentation_model.process(data)
|
||||
condition = np.stack((results.segmentation_mask,) * 3, axis=-1) > 0.1
|
||||
background = np.zeros(data.shape, dtype=np.uint8)
|
||||
background[:] = options.process.segmentation_background
|
||||
data = np.where(condition, data, background) # consider using a joint bilateral filter instead of pure combine
|
||||
segmented = Image.fromarray(data)
|
||||
res.image = segmented
|
||||
res.ops.append('segmentation')
|
||||
return res
|
||||
|
||||
|
||||
def unload():
|
||||
global face_model
|
||||
if face_model is not None:
|
||||
face_model = None
|
||||
global body_model
|
||||
if body_model is not None:
|
||||
body_model = None
|
||||
global segmentation_model
|
||||
if segmentation_model is not None:
|
||||
segmentation_model = None
|
||||
|
||||
|
||||
def encode(img):
|
||||
with io.BytesIO() as stream:
|
||||
img.save(stream, 'JPEG')
|
||||
values = stream.getvalue()
|
||||
encoded = base64.b64encode(values).decode()
|
||||
return encoded
|
||||
|
||||
|
||||
def reset():
|
||||
unload()
|
||||
global all_images_by_type
|
||||
all_images_by_type = {}
|
||||
global all_images
|
||||
all_images = []
|
||||
|
||||
|
||||
def upscale_restore_image(res: Result, upscale: bool = False, restore: bool = False):
|
||||
kwargs = util.Map({
|
||||
'image': encode(res.image),
|
||||
'codeformer_visibility': 0.0,
|
||||
'codeformer_weight': 0.0,
|
||||
})
|
||||
if res.image.width >= options.process.target_size and res.image.height >= options.process.target_size:
|
||||
upscale = False
|
||||
if upscale:
|
||||
kwargs.upscaler_1 = 'SwinIR_4x'
|
||||
kwargs.upscaling_resize = 2
|
||||
res.ops.append('upscale')
|
||||
if restore:
|
||||
kwargs.codeformer_visibility = 1.0
|
||||
kwargs.codeformer_weight: 0.2
|
||||
res.ops.append('restore')
|
||||
if upscale or restore:
|
||||
result = sdapi.postsync('/sdapi/v1/extra-single-image', kwargs)
|
||||
if 'image' not in result:
|
||||
res.message = 'failed to upscale/restore image'
|
||||
else:
|
||||
res.image = Image.open(io.BytesIO(base64.b64decode(result['image'])))
|
||||
return res
|
||||
|
||||
|
||||
def interrogate_image(res: Result, tag: str = None):
|
||||
caption = ''
|
||||
tags = []
|
||||
for model in options.process.interrogate_model:
|
||||
json = util.Map({ 'image': encode(res.image), 'model': model })
|
||||
result = sdapi.postsync('/sdapi/v1/interrogate', json)
|
||||
if model == 'clip':
|
||||
caption = result.caption if 'caption' in result else ''
|
||||
caption = caption.split(',')[0].replace('a ', '')
|
||||
if tag is not None:
|
||||
caption = res.tag + ', ' + caption
|
||||
if model == 'deepdanbooru':
|
||||
tag = result.caption if 'caption' in result else ''
|
||||
tags = tag.split(',')
|
||||
tags = [t.replace('(', '').replace(')', '').replace('\\', '').split(':')[0].strip() for t in tags]
|
||||
if tag is not None:
|
||||
for t in res.tag.split(',')[::-1]:
|
||||
tags.insert(0, t.strip())
|
||||
pos = 0 if len(tags) == 0 else 1
|
||||
tags.insert(pos, caption.split(' ')[1])
|
||||
if len(tags) > options.process.tag_limit:
|
||||
tags = tags[:options.process.tag_limit]
|
||||
res.caption = caption
|
||||
res.tags = tags
|
||||
res.ops.append('interrogate')
|
||||
return res
|
||||
|
||||
|
||||
def resize_image(res: Result):
|
||||
resized = res.image
|
||||
resized.thumbnail((options.process.target_size, options.process.target_size), Image.HAMMING)
|
||||
res.image = resized
|
||||
res.ops.append('resize')
|
||||
return res
|
||||
|
||||
|
||||
def square_image(res: Result):
|
||||
size = max(res.image.width, res.image.height)
|
||||
squared = Image.new('RGB', (size, size))
|
||||
squared.paste(res.image, ((size - res.image.width) // 2, (size - res.image.height) // 2))
|
||||
res.image = squared
|
||||
res.ops.append('square')
|
||||
return res
|
||||
|
||||
|
||||
def process_face(res: Result):
|
||||
res.ops.append('face')
|
||||
global face_model
|
||||
if face_model is None:
|
||||
face_model = mp.solutions.face_detection.FaceDetection(min_detection_confidence=options.process.face_score, model_selection=options.process.face_model)
|
||||
results = face_model.process(np.array(res.image))
|
||||
if results.detections is None:
|
||||
res.message = 'no face detected'
|
||||
res.image = None
|
||||
return res
|
||||
box = results.detections[0].location_data.relative_bounding_box
|
||||
if box.xmin < 0 or box.ymin < 0 or (box.width - box.xmin) > 1 or (box.height - box.ymin) > 1:
|
||||
res.message = 'face out of frame'
|
||||
res.image = None
|
||||
return res
|
||||
x = max(0, (box.xmin - options.process.face_pad / 2) * res.image.width)
|
||||
y = max(0, (box.ymin - options.process.face_pad / 2)* res.image.height)
|
||||
w = min(res.image.width, (box.width + options.process.face_pad) * res.image.width)
|
||||
h = min(res.image.height, (box.height + options.process.face_pad) * res.image.height)
|
||||
x = max(0, x)
|
||||
res.image = res.image.crop((x, y, x + w, y + h))
|
||||
return res
|
||||
|
||||
|
||||
def process_body(res: Result):
|
||||
res.ops.append('body')
|
||||
global body_model
|
||||
if body_model is None:
|
||||
body_model = mp.solutions.pose.Pose(static_image_mode=True, min_detection_confidence=options.process.body_score, model_complexity=options.process.body_model)
|
||||
results = body_model.process(np.array(res.image))
|
||||
if results.pose_landmarks is None:
|
||||
res.message = 'no body detected'
|
||||
res.image = None
|
||||
return res
|
||||
x0 = [res.image.width * (i.x - options.process.body_pad / 2) for i in results.pose_landmarks.landmark if i.visibility > options.process.body_visibility]
|
||||
y0 = [res.image.height * (i.y - options.process.body_pad / 2) for i in results.pose_landmarks.landmark if i.visibility > options.process.body_visibility]
|
||||
x1 = [res.image.width * (i.x + options.process.body_pad / 2) for i in results.pose_landmarks.landmark if i.visibility > options.process.body_visibility]
|
||||
y1 = [res.image.height * (i.y + options.process.body_pad / 2) for i in results.pose_landmarks.landmark if i.visibility > options.process.body_visibility]
|
||||
if len(x0) < options.process.body_parts:
|
||||
res.message = f'insufficient body parts detected: {len(x0)}'
|
||||
res.image = None
|
||||
return res
|
||||
res.image = res.image.crop((max(0, min(x0)), max(0, min(y0)), min(res.image.width, max(x1)), min(res.image.height, max(y1))))
|
||||
return res
|
||||
|
||||
|
||||
def process_original(res: Result):
|
||||
res.ops.append('original')
|
||||
return res
|
||||
|
||||
|
||||
def save_image(res: Result, folder: str):
|
||||
if res.image is None or folder is None:
|
||||
return res
|
||||
all_images_by_type[res.type] = all_images_by_type.get(res.type, 0) + 1
|
||||
res.basename = os.path.basename(res.input).split('.')[0]
|
||||
res.basename = str(all_images_by_type[res.type]).rjust(3, '0') + '-' + res.type + '-' + res.basename
|
||||
res.basename = os.path.join(folder, res.basename)
|
||||
res.output = res.basename + options.process.format
|
||||
res.image.save(res.output)
|
||||
res.image.close()
|
||||
res.ops.append('save')
|
||||
return res
|
||||
|
||||
|
||||
def file(filename: str, folder: str, tag = None, requested = []):
|
||||
# initialize result dict
|
||||
res = Result(input = filename, type='unknown', tag=tag, requested = requested)
|
||||
# open image
|
||||
try:
|
||||
res.image = Image.open(filename)
|
||||
if res.image.mode == 'RGBA':
|
||||
res.image = res.image.convert('RGB')
|
||||
res.image = ImageOps.exif_transpose(res.image) # rotate image according to EXIF orientation
|
||||
except Exception as e:
|
||||
res.message = f'error opening: {e}'
|
||||
return res
|
||||
# primary steps
|
||||
if 'face' in requested:
|
||||
res.type = 'face'
|
||||
res = process_face(res)
|
||||
elif 'body' in requested:
|
||||
res.type = 'body'
|
||||
res = process_body(res)
|
||||
elif 'original' in requested:
|
||||
res.type = 'original'
|
||||
res = process_original(res)
|
||||
# validation steps
|
||||
if res.image is None:
|
||||
return res
|
||||
if 'blur' in requested:
|
||||
res.ops.append('blur')
|
||||
val = detect_blur(res.image)
|
||||
if val > options.process.blur_score:
|
||||
res.message = f'blur check failed: {val}'
|
||||
res.image = None
|
||||
if 'range' in requested:
|
||||
res.ops.append('range')
|
||||
val = detect_dynamicrange(res.image)
|
||||
if val < options.process.range_score:
|
||||
res.message = f'dynamic range check failed: {val}'
|
||||
res.image = None
|
||||
if 'similarity' in requested:
|
||||
res.ops.append('similarity')
|
||||
val = detect_simmilar(res.image)
|
||||
if val > options.process.similarity_score:
|
||||
res.message = f'dynamic range check failed: {val}'
|
||||
res.image = None
|
||||
if res.image is None:
|
||||
return res
|
||||
# post processing steps
|
||||
res = upscale_restore_image(res, 'upscale' in requested, 'restore' in requested)
|
||||
if res.image.width < options.process.target_size or res.image.height < options.process.target_size:
|
||||
res.message = f'low resolution: [{res.image.width}, {res.image.height}]'
|
||||
res.image = None
|
||||
return res
|
||||
if 'interrogate' in requested:
|
||||
res = interrogate_image(res, tag)
|
||||
if 'resize' in requested:
|
||||
res = resize_image(res)
|
||||
if 'square' in requested:
|
||||
res = square_image(res)
|
||||
if 'segment' in requested:
|
||||
res = segmentation(res)
|
||||
# finally save image
|
||||
res = save_image(res, folder)
|
||||
return res
|
||||
@@ -0,0 +1,111 @@
|
||||
import sys
|
||||
import json
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import requests
|
||||
from util import Map
|
||||
|
||||
|
||||
sd_url = "http://127.0.0.1:7860" # automatic1111 api url root
|
||||
use_session = True
|
||||
timeout = aiohttp.ClientTimeout(total = None, sock_connect = 10, sock_read = None) # default value is 5 minutes, we need longer for training
|
||||
sess = None
|
||||
quiet = False
|
||||
|
||||
|
||||
async def result(req):
|
||||
if req.status != 200:
|
||||
if not use_session and sess is not None:
|
||||
await sess.close()
|
||||
return Map({ 'error': req.status, 'reason': req.reason, 'url': req.url })
|
||||
else:
|
||||
json = await req.json()
|
||||
if type(json) == list:
|
||||
res = json
|
||||
elif json is None:
|
||||
res = {}
|
||||
else:
|
||||
res = Map(json)
|
||||
return res
|
||||
|
||||
|
||||
def resultsync(req: requests.Response):
|
||||
if req.status_code != 200:
|
||||
return Map({ 'error': req.status_code, 'reason': req.reason, 'url': req.url })
|
||||
else:
|
||||
json = req.json()
|
||||
if type(json) == list:
|
||||
res = json
|
||||
elif json is None:
|
||||
res = {}
|
||||
else:
|
||||
res = Map(json)
|
||||
return res
|
||||
|
||||
|
||||
async def get(endpoint: str, json: dict = None):
|
||||
global sess # pylint: disable=global-statement
|
||||
sess = sess if sess is not None else await session()
|
||||
async with sess.get(url = endpoint, json = json) as req:
|
||||
res = await result(req)
|
||||
return res
|
||||
|
||||
|
||||
def getsync(endpoint: str, json: dict = None):
|
||||
req = requests.get(f'{sd_url}{endpoint}', json = json) # pylint: disable=missing-timeout
|
||||
res = resultsync(req)
|
||||
return res
|
||||
|
||||
|
||||
async def post(endpoint: str, json: dict = None):
|
||||
global sess # pylint: disable=global-statement
|
||||
# sess = sess if sess is not None else await session()
|
||||
if sess and not sess.closed:
|
||||
await sess.close()
|
||||
sess = await session()
|
||||
async with sess.post(url = endpoint, json = json) as req:
|
||||
res = await result(req)
|
||||
return res
|
||||
|
||||
|
||||
def postsync(endpoint: str, json: dict = None):
|
||||
req = requests.post(f'{sd_url}{endpoint}', json = json) # pylint: disable=missing-timeout
|
||||
res = resultsync(req)
|
||||
return res
|
||||
|
||||
|
||||
def interrupt():
|
||||
res = getsync('/sdapi/v1/progress?skip_current_image=true')
|
||||
if 'state' in res and res.state.job_count > 0:
|
||||
res = postsync('/sdapi/v1/interrupt')
|
||||
return res
|
||||
else:
|
||||
return { 'interrupt': 'idle' }
|
||||
|
||||
|
||||
def progress():
|
||||
res = getsync('/sdapi/v1/progress?skip_current_image=true')
|
||||
return res
|
||||
|
||||
|
||||
def options():
|
||||
options = getsync('/sdapi/v1/options')
|
||||
flags = getsync('/sdapi/v1/cmd-flags')
|
||||
return { 'options': options, 'flags': flags }
|
||||
|
||||
|
||||
def shutdown():
|
||||
postsync('/sdapi/v1/shutdown')
|
||||
|
||||
|
||||
async def session():
|
||||
global sess # pylint: disable=global-statement
|
||||
time = aiohttp.ClientTimeout(total = None, sock_connect = 10, sock_read = None) # default value is 5 minutes, we need longer for training
|
||||
sess = aiohttp.ClientSession(timeout = time, base_url = sd_url)
|
||||
return sess
|
||||
|
||||
|
||||
async def close():
|
||||
if sess is not None:
|
||||
await asyncio.sleep(0)
|
||||
await sess.__aexit__(None, None, None)
|
||||
@@ -0,0 +1,322 @@
|
||||
#!/bin/env python
|
||||
|
||||
# system imports
|
||||
import os
|
||||
import re
|
||||
import gc
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import pathlib
|
||||
import asyncio
|
||||
import tempfile
|
||||
import argparse
|
||||
|
||||
# 3rd party imports
|
||||
import filetype
|
||||
from tqdm.rich import tqdm
|
||||
|
||||
# local imports
|
||||
import util
|
||||
import sdapi
|
||||
import process
|
||||
import latents
|
||||
import options
|
||||
|
||||
# console handler
|
||||
from rich import print # pylint: disable=redefined-builtin
|
||||
from rich.pretty import install as pretty_install
|
||||
from rich.traceback import install as traceback_install
|
||||
from rich.console import Console
|
||||
console = Console(log_time=True, log_time_format='%H:%M:%S-%f')
|
||||
pretty_install(console=console)
|
||||
import torch, accelerate, diffusers, requests, urllib3, http
|
||||
traceback_install(console=console, extra_lines=1, width=console.width, word_wrap=False, indent_guides=False, suppress=[torch,accelerate,diffusers,asyncio,http,urllib3,requests])
|
||||
|
||||
# lora imports
|
||||
lora_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'modules', 'lora'))
|
||||
sys.path.append(lora_path)
|
||||
lycoris_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'modules', 'lycoris'))
|
||||
sys.path.append(lycoris_path)
|
||||
import train_network
|
||||
|
||||
|
||||
# globals
|
||||
args = None
|
||||
valid_steps = ['original', 'face', 'body', 'blur', 'range', 'upscale', 'restore', 'interrogate', 'resize', 'square', 'segment']
|
||||
|
||||
# methods
|
||||
|
||||
def mem_stats():
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
with torch.no_grad():
|
||||
torch.cuda.empty_cache()
|
||||
with torch.cuda.device('cuda'):
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
mem = util.get_memory()
|
||||
peak = { 'active': mem['gpu-active']['peak'], 'allocated': mem['gpu-allocated']['peak'], 'reserved': mem['gpu-reserved']['peak'] }
|
||||
console.log(f"memory cpu: {mem.ram} gpu current: {mem.gpu} gpu peak: {peak}")
|
||||
|
||||
|
||||
def parse_args():
|
||||
global args
|
||||
parser = argparse.ArgumentParser(description = 'train lora')
|
||||
# basic section
|
||||
parser.add_argument('--output', '--name', type=str, default=None, required=True, help='output filename')
|
||||
parser.add_argument('--type', type=str, choices=['embedding', 'lora', 'lycoris', 'dreambooth'], default=None, required=True, help='training type')
|
||||
parser.add_argument('--tag', type=str, default='person', required=False, help='primary tag, default: %(default)s')
|
||||
parser.add_argument('--process', type=str, default='original,interrogate,resize,square', required=False, help=f'list of possible processing steps: {valid_steps}, default: %(default)s')
|
||||
parser.add_argument('--dir', type=str, default='', required=False, help='where to store processed images, default is system temp/train')
|
||||
parser.add_argument('--input', '--dataset', type=str, default=None, required=True, help='input folder with training images')
|
||||
parser.add_argument('--overwrite', default = False, action='store_true', help = "overwrite existing training, default: %(default)s")
|
||||
|
||||
# global params
|
||||
parser.add_argument('--gradient', type=int, default=1, required=False, help='gradient accumulation steps, default: %(default)s')
|
||||
parser.add_argument('--steps', type=int, default=2500, required=False, help='training steps, default: %(default)s')
|
||||
parser.add_argument('--batch', type=int, default=1, required=False, help='batch size, default: %(default)s')
|
||||
parser.add_argument('--lr', type=float, default=1e-04, required=False, help='model learning rate, default: %(default)s')
|
||||
parser.add_argument('--dim', '--vectors', type=int, default=40, required=False, help='network dimension, default: %(default)s')
|
||||
|
||||
# lora params
|
||||
parser.add_argument('--repeats', type=int, default=10, required=False, help='number of repeats per image, default: %(default)s')
|
||||
parser.add_argument('--alpha', type=float, default=0, required=False, help='alpha for weights scaling, default: half of dim')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def prepare_server():
|
||||
try:
|
||||
server_status = util.Map(sdapi.progress())
|
||||
server_state = server_status['state']
|
||||
except:
|
||||
console.log('server error:', server_status)
|
||||
exit(1)
|
||||
if server_state['job_count'] > 0:
|
||||
console.log('server not idle:', server_state)
|
||||
exit(1)
|
||||
|
||||
server_options = util.Map(sdapi.options())
|
||||
server_options.options.save_training_settings_to_txt = False
|
||||
server_options.options.training_enable_tensorboard = False
|
||||
server_options.options.training_tensorboard_save_images = False
|
||||
server_options.options.pin_memory = True
|
||||
server_options.options.save_optimizer_state = False
|
||||
server_options.options.training_image_repeats_per_epoch = args.repeats
|
||||
server_options.options.training_write_csv_every = 0
|
||||
sdapi.postsync('/sdapi/v1/options', server_options.options)
|
||||
console.log(f'updated server options')
|
||||
|
||||
|
||||
def verify_args():
|
||||
global args
|
||||
server_options = util.Map(sdapi.options())
|
||||
args.model = server_options.options['sd_model_checkpoint'].split(' [')[0]
|
||||
args.lora_dir = server_options.flags['lora_dir']
|
||||
if not os.path.isabs(args.model) and not os.path.exists(args.model):
|
||||
args.model = os.path.abspath(os.path.join(args.lora_dir, os.pardir, 'Stable-diffusion', args.model))
|
||||
|
||||
if not os.path.exists(args.model) or not os.path.isfile(args.model):
|
||||
console.log('cannot find model:', args.model)
|
||||
exit(1)
|
||||
if not os.path.exists(args.input) or not os.path.isdir(args.input):
|
||||
console.log('cannot find training folder:', args.input)
|
||||
exit(1)
|
||||
if not os.path.exists(args.lora_dir) or not os.path.isdir(args.lora_dir):
|
||||
console.log('cannot find lora folder:', args.dir)
|
||||
exit(1)
|
||||
if args.dir != '':
|
||||
args.process_dir = args.dir
|
||||
else:
|
||||
args.process_dir = os.path.join(tempfile.gettempdir(), 'train', args.output)
|
||||
console.log(f'args: {vars(args)}')
|
||||
|
||||
|
||||
async def training_loop():
|
||||
async def async_train():
|
||||
res = await sdapi.post('/sdapi/v1/train/embedding', options.embedding)
|
||||
console.log(f'train embedding result: {res}')
|
||||
|
||||
async def async_monitor():
|
||||
await asyncio.sleep(3)
|
||||
res = util.Map(sdapi.progress())
|
||||
with tqdm(desc='train embedding', total=res.state.job_count) as pbar:
|
||||
while res.state.job_no < res.state.job_count and not res.state.interrupted and not res.state.skipped:
|
||||
await asyncio.sleep(2)
|
||||
prev_job = res.state.job_no
|
||||
res = util.Map(sdapi.progress())
|
||||
loss = re.search(r"Loss: (.*?)(?=\<)", res.textinfo)
|
||||
if loss:
|
||||
pbar.set_postfix({ 'loss': loss.group(0) })
|
||||
pbar.update(res.state.job_no - prev_job)
|
||||
|
||||
a = asyncio.create_task(async_train())
|
||||
b = asyncio.create_task(async_monitor())
|
||||
await asyncio.gather(a, b) # wait for both pipeline and monitor to finish
|
||||
|
||||
|
||||
def train_embedding():
|
||||
console.log(f'{args.type} options: {options.embedding}')
|
||||
create_options = util.Map({
|
||||
"name": args.output,
|
||||
"num_vectors_per_token": args.dim,
|
||||
"overwrite_old": False,
|
||||
"init_text": args.tag,
|
||||
})
|
||||
server_options = util.Map(sdapi.options())
|
||||
fn = os.path.join(server_options.flags.embeddings_dir, args.output) + '.pt'
|
||||
if os.path.exists(fn) and args.overwrite:
|
||||
console.log(f'delete existing embedding {fn}')
|
||||
os.remove(fn)
|
||||
else:
|
||||
console.log(f'embedding exists {fn}')
|
||||
return
|
||||
console.log(f'create embedding {create_options}')
|
||||
res = sdapi.postsync('/sdapi/v1/create/embedding', create_options)
|
||||
if 'info' in res and 'error' in res['info']: # formatted error
|
||||
console.log(res.info)
|
||||
elif 'info' in res: # no error
|
||||
asyncio.run(training_loop())
|
||||
else: # unknown error
|
||||
console.log(f'create embedding error {res}')
|
||||
|
||||
|
||||
def train_lora():
|
||||
fn = os.path.join(args.lora_dir, args.output)
|
||||
for ext in ['.ckpt', '.pt', '.safetensors']:
|
||||
if os.path.exists(fn + ext):
|
||||
if args.overwrite:
|
||||
console.log(f'delete existing lora: {fn + ext}')
|
||||
os.remove(fn + ext)
|
||||
else:
|
||||
console.log(f'lora exists: {fn + ext}')
|
||||
return
|
||||
console.log(f'{args.type} options: {options.lora}')
|
||||
train_network.train(options.lora)
|
||||
|
||||
|
||||
def prepare_options():
|
||||
# lora specific
|
||||
options.lora.pretrained_model_name_or_path = args.model
|
||||
options.lora.output_dir = args.lora_dir
|
||||
options.lora.output_name = args.output
|
||||
options.lora.max_train_steps = args.steps
|
||||
options.lora.network_dim = args.dim
|
||||
options.lora.network_alpha = args.dim // 2 if args.alpha == 0 else args.alpha
|
||||
options.lora.gradient_accumulation_steps = args.gradient
|
||||
options.lora.learning_rate = args.lr
|
||||
options.lora.train_batch_size = args.batch
|
||||
options.lora.network_alpha = args.dim // 2 if args.alpha == 0 else args.alpha
|
||||
options.lora.train_data_dir = args.process_dir
|
||||
if args.type == 'lycoris':
|
||||
console.log('train using lycoris network')
|
||||
options.lora.network_module = 'lycoris.kohya'
|
||||
options.lora.in_json = os.path.join(args.process_dir, args.output + '.json')
|
||||
if args.type == 'dreambooth':
|
||||
console.log('train using dreambooth style training')
|
||||
options.lora.in_json = None
|
||||
if args.type == 'lora':
|
||||
console.log('train using lora style training')
|
||||
options.lora.in_json = os.path.join(args.process_dir, args.output + '.json')
|
||||
if args.type == 'embedding':
|
||||
console.log('train embedding')
|
||||
options.lora.in_json = None
|
||||
pass
|
||||
# embedding specific
|
||||
options.embedding.embedding_name = args.output
|
||||
options.embedding.learn_rate = str(args.lr)
|
||||
options.embedding.batch_size = args.batch
|
||||
options.embedding.steps = args.steps
|
||||
options.embedding.data_root = args.process_dir
|
||||
options.embedding.log_directory = os.path.join(args.process_dir, 'log')
|
||||
options.embedding.gradient_step = args.gradient
|
||||
|
||||
|
||||
def process_inputs():
|
||||
pathlib.Path(args.process_dir).mkdir(parents=True, exist_ok=True)
|
||||
processing_options = args.process.split(',') if isinstance(args.process, str) else args.process
|
||||
processing_options = [opt.strip() for opt in re.split(',| ', args.process)]
|
||||
console.log(f'processing steps: {processing_options}')
|
||||
for step in processing_options:
|
||||
if step not in valid_steps:
|
||||
console.log(f'invalid processing step: {[step]}')
|
||||
exit(1)
|
||||
for root, _sub_dirs, folder in os.walk(args.input):
|
||||
files = [os.path.join(root, f) for f in folder if filetype.is_image(os.path.join(root, f))]
|
||||
console.log(f'processing input images: {len(files)}')
|
||||
if os.path.exists(args.process_dir):
|
||||
console.log('removing existing processed folder:', args.process_dir)
|
||||
shutil.rmtree(args.process_dir, ignore_errors=True)
|
||||
steps = [step for step in processing_options if step in ['face', 'body', 'original']]
|
||||
process.reset()
|
||||
metadata = {}
|
||||
for step in steps:
|
||||
if step == 'face':
|
||||
opts = [step for step in processing_options if step not in ['body', 'original']]
|
||||
if step == 'body':
|
||||
opts = [step for step in processing_options if step not in ['face', 'original', 'upscale', 'restore']] # body does not perform upscale or restore
|
||||
if step == 'original':
|
||||
opts = [step for step in processing_options if step not in ['face', 'body', 'upscale', 'restore', 'blur', 'range', 'segment']] # original does not perform most steps
|
||||
console.log(f'processing current step: {opts}')
|
||||
tag = step
|
||||
if tag == 'original' and args.tag is not None:
|
||||
concept = args.tag.split(',')[0].strip()
|
||||
else:
|
||||
concept = step
|
||||
if args.type in ['lora', 'lycoris', 'dreambooth']:
|
||||
dir = os.path.join(args.process_dir, str(args.repeats) + '_' + concept) # separate concepts per folder
|
||||
if args.type in ['embedding']:
|
||||
dir = os.path.join(args.process_dir) # everything into same folder
|
||||
console.log('processing concept:', concept)
|
||||
console.log('processing output folder:', dir)
|
||||
pathlib.Path(dir).mkdir(parents=True, exist_ok=True)
|
||||
results = {}
|
||||
for f in files:
|
||||
res = process.file(filename = f, folder = dir, tag = args.tag, requested = opts)
|
||||
if res.image: # valid result
|
||||
results[res.type] = results.get(res.type, 0) + 1
|
||||
results['total'] = results.get('total', 0) + 1
|
||||
rel_path = res.basename.replace(os.path.commonpath([res.basename, args.process_dir]), '')
|
||||
if rel_path.startswith(os.path.sep): rel_path = rel_path[1:]
|
||||
metadata[rel_path] = { 'caption': res.caption, 'tags': ','.join(res.tags) }
|
||||
if options.lora.in_json is None:
|
||||
with open(res.output.replace(options.process.format, '.txt'), "w") as outfile:
|
||||
outfile.write(res.caption)
|
||||
console.log(f"processing {'saved' if res.image is not None else 'skipped'}: {f} => {res.output} {res.ops} {res.message}")
|
||||
dirs = [os.path.join(args.process_dir, dir) for dir in os.listdir(args.process_dir) if os.path.isdir(os.path.join(args.process_dir, dir))]
|
||||
console.log(f'input datasets {dirs}')
|
||||
if options.lora.in_json is not None:
|
||||
with open(options.lora.in_json, "w") as outfile: # write json at the end only
|
||||
outfile.write(json.dumps(metadata, indent=2))
|
||||
for dir in dirs: # create latents
|
||||
latents.create_vae_latents(util.Map({ 'input': dir, 'json': options.lora.in_json }))
|
||||
latents.unload_vae()
|
||||
r = { 'inputs': len(files), 'outputs': results, 'metadata': options.lora.in_json }
|
||||
console.log(f'processing steps result: {r}')
|
||||
if args.gradient < 0:
|
||||
console.log(f"setting gradient accumulation to number of images: {results['total']}")
|
||||
options.lora.gradient_accumulation_steps = results['total']
|
||||
options.embedding.gradient_step = results['total']
|
||||
process.unload()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
console.log('train script for stable diffusion')
|
||||
parse_args()
|
||||
prepare_server()
|
||||
verify_args()
|
||||
prepare_options()
|
||||
mem_stats()
|
||||
process_inputs()
|
||||
mem_stats()
|
||||
try:
|
||||
if args.type == 'embedding':
|
||||
train_embedding()
|
||||
if args.type == 'lora' or args.type == 'lycoris' or args.type == 'dreambooth':
|
||||
train_lora()
|
||||
except KeyboardInterrupt as e:
|
||||
console.log('interrupt requested')
|
||||
sdapi.interrupt()
|
||||
mem_stats()
|
||||
console.log('done')
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/bin/env python
|
||||
import os
|
||||
|
||||
import transformers
|
||||
transformers.logging.set_verbosity_error()
|
||||
|
||||
|
||||
def get_memory():
|
||||
def gb(val: float):
|
||||
return round(val / 1024 / 1024 / 1024, 2)
|
||||
mem = {}
|
||||
try:
|
||||
import psutil
|
||||
process = psutil.Process(os.getpid())
|
||||
res = process.memory_info()
|
||||
ram_total = 100 * res.rss / process.memory_percent()
|
||||
ram = { 'free': gb(ram_total - res.rss), 'used': gb(res.rss), 'total': gb(ram_total) }
|
||||
mem.update({ 'ram': ram })
|
||||
except Exception as e:
|
||||
mem.update({ 'ram': e })
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
s = torch.cuda.mem_get_info()
|
||||
gpu = { 'free': gb(s[0]), 'used': gb(s[1] - s[0]), 'total': gb(s[1]) }
|
||||
s = dict(torch.cuda.memory_stats('cuda'))
|
||||
allocated = { 'current': gb(s['allocated_bytes.all.current']), 'peak': gb(s['allocated_bytes.all.peak']) }
|
||||
reserved = { 'current': gb(s['reserved_bytes.all.current']), 'peak': gb(s['reserved_bytes.all.peak']) }
|
||||
active = { 'current': gb(s['active_bytes.all.current']), 'peak': gb(s['active_bytes.all.peak']) }
|
||||
inactive = { 'current': gb(s['inactive_split_bytes.all.current']), 'peak': gb(s['inactive_split_bytes.all.peak']) }
|
||||
warnings = { 'retries': s['num_alloc_retries'], 'oom': s['num_ooms'] }
|
||||
mem.update({
|
||||
'gpu': gpu,
|
||||
'gpu-active': active,
|
||||
'gpu-allocated': allocated,
|
||||
'gpu-reserved': reserved,
|
||||
'gpu-inactive': inactive,
|
||||
'events': warnings,
|
||||
})
|
||||
except:
|
||||
pass
|
||||
return Map(mem)
|
||||
|
||||
|
||||
class Map(dict):
|
||||
__slots__ = ('__dict__')
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Map, self).__init__(*args, **kwargs)
|
||||
for arg in args:
|
||||
if isinstance(arg, dict):
|
||||
for k, v in arg.items():
|
||||
if isinstance(v, dict):
|
||||
v = Map(v)
|
||||
if isinstance(v, list):
|
||||
self.__convert(v)
|
||||
self[k] = v
|
||||
if kwargs:
|
||||
for k, v in kwargs.items():
|
||||
if isinstance(v, dict):
|
||||
v = Map(v)
|
||||
elif isinstance(v, list):
|
||||
self.__convert(v)
|
||||
self[k] = v
|
||||
def __convert(self, v):
|
||||
for elem in range(0, len(v)): # pylint: disable=consider-using-enumerate
|
||||
if isinstance(v[elem], dict):
|
||||
v[elem] = Map(v[elem])
|
||||
elif isinstance(v[elem], list):
|
||||
self.__convert(v[elem])
|
||||
def __getattr__(self, attr):
|
||||
return self.get(attr)
|
||||
def __setattr__(self, key, value):
|
||||
self.__setitem__(key, value)
|
||||
def __setitem__(self, key, value):
|
||||
super(Map, self).__setitem__(key, value)
|
||||
self.__dict__.update({key: value})
|
||||
def __delattr__(self, item):
|
||||
self.__delitem__(item)
|
||||
def __delitem__(self, key):
|
||||
super(Map, self).__delitem__(key)
|
||||
del self.__dict__[key]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -0,0 +1,12 @@
|
||||
#/bin/env bash
|
||||
echo "Installing xformers"
|
||||
|
||||
NVCC_FLAGS="--use_fast_math"
|
||||
FORCE_CUDA="1"
|
||||
TORCH_CUDA_ARCH_LIST="8.6"
|
||||
pip install ninja -q
|
||||
pip uninstall xformers -y 2>/dev/null
|
||||
pip install -v -U git+https://github.com/facebookresearch/xformers.git@main#egg=xformers
|
||||
pip show torch
|
||||
pip show xformers
|
||||
python -m xformers.info
|
||||
@@ -0,0 +1,67 @@
|
||||
model:
|
||||
base_learning_rate: 1.0e-4
|
||||
target: ldm.models.diffusion.ddpm.LatentDiffusion
|
||||
params:
|
||||
linear_start: 0.00085
|
||||
linear_end: 0.0120
|
||||
num_timesteps_cond: 1
|
||||
log_every_t: 200
|
||||
timesteps: 1000
|
||||
first_stage_key: "jpg"
|
||||
cond_stage_key: "txt"
|
||||
image_size: 64
|
||||
channels: 4
|
||||
cond_stage_trainable: false
|
||||
conditioning_key: crossattn
|
||||
monitor: val/loss_simple_ema
|
||||
scale_factor: 0.18215
|
||||
use_ema: False # we set this to false because this is an inference only config
|
||||
|
||||
unet_config:
|
||||
target: ldm.modules.diffusionmodules.openaimodel.UNetModel
|
||||
params:
|
||||
use_checkpoint: True
|
||||
use_fp16: True
|
||||
image_size: 32 # unused
|
||||
in_channels: 4
|
||||
out_channels: 4
|
||||
model_channels: 320
|
||||
attention_resolutions: [ 4, 2, 1 ]
|
||||
num_res_blocks: 2
|
||||
channel_mult: [ 1, 2, 4, 4 ]
|
||||
num_head_channels: 64 # need to fix for flash-attn
|
||||
use_spatial_transformer: True
|
||||
use_linear_in_transformer: True
|
||||
transformer_depth: 1
|
||||
context_dim: 1024
|
||||
legacy: False
|
||||
|
||||
first_stage_config:
|
||||
target: ldm.models.autoencoder.AutoencoderKL
|
||||
params:
|
||||
embed_dim: 4
|
||||
monitor: val/rec_loss
|
||||
ddconfig:
|
||||
#attn_type: "vanilla-xformers"
|
||||
double_z: true
|
||||
z_channels: 4
|
||||
resolution: 256
|
||||
in_channels: 3
|
||||
out_ch: 3
|
||||
ch: 128
|
||||
ch_mult:
|
||||
- 1
|
||||
- 2
|
||||
- 4
|
||||
- 4
|
||||
num_res_blocks: 2
|
||||
attn_resolutions: []
|
||||
dropout: 0.0
|
||||
lossconfig:
|
||||
target: torch.nn.Identity
|
||||
|
||||
cond_stage_config:
|
||||
target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder
|
||||
params:
|
||||
freeze: True
|
||||
layer: "penultimate"
|
||||
@@ -0,0 +1,68 @@
|
||||
model:
|
||||
base_learning_rate: 1.0e-4
|
||||
target: ldm.models.diffusion.ddpm.LatentDiffusion
|
||||
params:
|
||||
parameterization: "v"
|
||||
linear_start: 0.00085
|
||||
linear_end: 0.0120
|
||||
num_timesteps_cond: 1
|
||||
log_every_t: 200
|
||||
timesteps: 1000
|
||||
first_stage_key: "jpg"
|
||||
cond_stage_key: "txt"
|
||||
image_size: 64
|
||||
channels: 4
|
||||
cond_stage_trainable: false
|
||||
conditioning_key: crossattn
|
||||
monitor: val/loss_simple_ema
|
||||
scale_factor: 0.18215
|
||||
use_ema: False # we set this to false because this is an inference only config
|
||||
|
||||
unet_config:
|
||||
target: ldm.modules.diffusionmodules.openaimodel.UNetModel
|
||||
params:
|
||||
use_checkpoint: True
|
||||
use_fp16: True
|
||||
image_size: 32 # unused
|
||||
in_channels: 4
|
||||
out_channels: 4
|
||||
model_channels: 320
|
||||
attention_resolutions: [ 4, 2, 1 ]
|
||||
num_res_blocks: 2
|
||||
channel_mult: [ 1, 2, 4, 4 ]
|
||||
num_head_channels: 64 # need to fix for flash-attn
|
||||
use_spatial_transformer: True
|
||||
use_linear_in_transformer: True
|
||||
transformer_depth: 1
|
||||
context_dim: 1024
|
||||
legacy: False
|
||||
|
||||
first_stage_config:
|
||||
target: ldm.models.autoencoder.AutoencoderKL
|
||||
params:
|
||||
embed_dim: 4
|
||||
monitor: val/rec_loss
|
||||
ddconfig:
|
||||
#attn_type: "vanilla-xformers"
|
||||
double_z: true
|
||||
z_channels: 4
|
||||
resolution: 256
|
||||
in_channels: 3
|
||||
out_ch: 3
|
||||
ch: 128
|
||||
ch_mult:
|
||||
- 1
|
||||
- 2
|
||||
- 4
|
||||
- 4
|
||||
num_res_blocks: 2
|
||||
attn_resolutions: []
|
||||
dropout: 0.0
|
||||
lossconfig:
|
||||
target: torch.nn.Identity
|
||||
|
||||
cond_stage_config:
|
||||
target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder
|
||||
params:
|
||||
freeze: True
|
||||
layer: "penultimate"
|
||||
@@ -0,0 +1,158 @@
|
||||
model:
|
||||
base_learning_rate: 5.0e-05
|
||||
target: ldm.models.diffusion.ddpm.LatentInpaintDiffusion
|
||||
params:
|
||||
linear_start: 0.00085
|
||||
linear_end: 0.0120
|
||||
num_timesteps_cond: 1
|
||||
log_every_t: 200
|
||||
timesteps: 1000
|
||||
first_stage_key: "jpg"
|
||||
cond_stage_key: "txt"
|
||||
image_size: 64
|
||||
channels: 4
|
||||
cond_stage_trainable: false
|
||||
conditioning_key: hybrid
|
||||
scale_factor: 0.18215
|
||||
monitor: val/loss_simple_ema
|
||||
finetune_keys: null
|
||||
use_ema: False
|
||||
|
||||
unet_config:
|
||||
target: ldm.modules.diffusionmodules.openaimodel.UNetModel
|
||||
params:
|
||||
use_checkpoint: True
|
||||
image_size: 32 # unused
|
||||
in_channels: 9
|
||||
out_channels: 4
|
||||
model_channels: 320
|
||||
attention_resolutions: [ 4, 2, 1 ]
|
||||
num_res_blocks: 2
|
||||
channel_mult: [ 1, 2, 4, 4 ]
|
||||
num_head_channels: 64 # need to fix for flash-attn
|
||||
use_spatial_transformer: True
|
||||
use_linear_in_transformer: True
|
||||
transformer_depth: 1
|
||||
context_dim: 1024
|
||||
legacy: False
|
||||
|
||||
first_stage_config:
|
||||
target: ldm.models.autoencoder.AutoencoderKL
|
||||
params:
|
||||
embed_dim: 4
|
||||
monitor: val/rec_loss
|
||||
ddconfig:
|
||||
#attn_type: "vanilla-xformers"
|
||||
double_z: true
|
||||
z_channels: 4
|
||||
resolution: 256
|
||||
in_channels: 3
|
||||
out_ch: 3
|
||||
ch: 128
|
||||
ch_mult:
|
||||
- 1
|
||||
- 2
|
||||
- 4
|
||||
- 4
|
||||
num_res_blocks: 2
|
||||
attn_resolutions: [ ]
|
||||
dropout: 0.0
|
||||
lossconfig:
|
||||
target: torch.nn.Identity
|
||||
|
||||
cond_stage_config:
|
||||
target: ldm.modules.encoders.modules.FrozenOpenCLIPEmbedder
|
||||
params:
|
||||
freeze: True
|
||||
layer: "penultimate"
|
||||
|
||||
|
||||
data:
|
||||
target: ldm.data.laion.WebDataModuleFromConfig
|
||||
params:
|
||||
tar_base: null # for concat as in LAION-A
|
||||
p_unsafe_threshold: 0.1
|
||||
filter_word_list: "data/filters.yaml"
|
||||
max_pwatermark: 0.45
|
||||
batch_size: 8
|
||||
num_workers: 6
|
||||
multinode: True
|
||||
min_size: 512
|
||||
train:
|
||||
shards:
|
||||
- "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-0/{00000..18699}.tar -"
|
||||
- "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-1/{00000..18699}.tar -"
|
||||
- "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-2/{00000..18699}.tar -"
|
||||
- "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-3/{00000..18699}.tar -"
|
||||
- "pipe:aws s3 cp s3://stability-aws/laion-a-native/part-4/{00000..18699}.tar -" #{00000-94333}.tar"
|
||||
shuffle: 10000
|
||||
image_key: jpg
|
||||
image_transforms:
|
||||
- target: torchvision.transforms.Resize
|
||||
params:
|
||||
size: 512
|
||||
interpolation: 3
|
||||
- target: torchvision.transforms.RandomCrop
|
||||
params:
|
||||
size: 512
|
||||
postprocess:
|
||||
target: ldm.data.laion.AddMask
|
||||
params:
|
||||
mode: "512train-large"
|
||||
p_drop: 0.25
|
||||
# NOTE use enough shards to avoid empty validation loops in workers
|
||||
validation:
|
||||
shards:
|
||||
- "pipe:aws s3 cp s3://deep-floyd-s3/datasets/laion_cleaned-part5/{93001..94333}.tar - "
|
||||
shuffle: 0
|
||||
image_key: jpg
|
||||
image_transforms:
|
||||
- target: torchvision.transforms.Resize
|
||||
params:
|
||||
size: 512
|
||||
interpolation: 3
|
||||
- target: torchvision.transforms.CenterCrop
|
||||
params:
|
||||
size: 512
|
||||
postprocess:
|
||||
target: ldm.data.laion.AddMask
|
||||
params:
|
||||
mode: "512train-large"
|
||||
p_drop: 0.25
|
||||
|
||||
lightning:
|
||||
find_unused_parameters: True
|
||||
modelcheckpoint:
|
||||
params:
|
||||
every_n_train_steps: 5000
|
||||
|
||||
callbacks:
|
||||
metrics_over_trainsteps_checkpoint:
|
||||
params:
|
||||
every_n_train_steps: 10000
|
||||
|
||||
image_logger:
|
||||
target: main.ImageLogger
|
||||
params:
|
||||
enable_autocast: False
|
||||
disabled: False
|
||||
batch_frequency: 1000
|
||||
max_images: 4
|
||||
increase_log_steps: False
|
||||
log_first_step: False
|
||||
log_images_kwargs:
|
||||
use_ema_scope: False
|
||||
inpaint: False
|
||||
plot_progressive_rows: False
|
||||
plot_diffusion_rows: False
|
||||
N: 4
|
||||
unconditional_guidance_scale: 5.0
|
||||
unconditional_guidance_label: [""]
|
||||
ddim_steps: 50 # todo check these out for depth2img,
|
||||
ddim_eta: 0.0 # todo check these out for depth2img,
|
||||
|
||||
trainer:
|
||||
benchmark: True
|
||||
val_check_interval: 5000000
|
||||
num_sanity_val_steps: 0
|
||||
accumulate_grad_batches: 1
|
||||
@@ -1,11 +0,0 @@
|
||||
name: automatic
|
||||
channels:
|
||||
- pytorch
|
||||
- defaults
|
||||
dependencies:
|
||||
- python=3.10
|
||||
- pip=22.2.2
|
||||
- cudatoolkit=11.3
|
||||
- pytorch=1.12.1
|
||||
- torchvision=0.13.1
|
||||
- numpy=1.23.1
|
||||
@@ -40,7 +40,7 @@ class LDSR:
|
||||
model = model.to(shared.device)
|
||||
if half_attention:
|
||||
model = model.half()
|
||||
if shared.cmd_opts.opt_channelslast:
|
||||
if shared.opts.opt_channelslast:
|
||||
model = model.to(memory_format=torch.channels_last)
|
||||
|
||||
sd_hijack.model_hijack.hijack(model) # apply optimization
|
||||
|
||||
@@ -2,20 +2,34 @@ import glob
|
||||
import os
|
||||
import re
|
||||
import torch
|
||||
from typing import Union
|
||||
|
||||
from modules import shared, devices, sd_models, errors
|
||||
|
||||
metadata_tags_order = {"ss_sd_model_name": 1, "ss_resolution": 2, "ss_clip_skip": 3, "ss_num_train_images": 10, "ss_tag_frequency": 20}
|
||||
|
||||
re_digits = re.compile(r"\d+")
|
||||
re_unet_down_blocks = re.compile(r"lora_unet_down_blocks_(\d+)_attentions_(\d+)_(.+)")
|
||||
re_unet_mid_blocks = re.compile(r"lora_unet_mid_block_attentions_(\d+)_(.+)")
|
||||
re_unet_up_blocks = re.compile(r"lora_unet_up_blocks_(\d+)_attentions_(\d+)_(.+)")
|
||||
re_text_block = re.compile(r"lora_te_text_model_encoder_layers_(\d+)_(.+)")
|
||||
re_x_proj = re.compile(r"(.*)_([qkv]_proj)$")
|
||||
re_compiled = {}
|
||||
|
||||
suffix_conversion = {
|
||||
"attentions": {},
|
||||
"resnets": {
|
||||
"conv1": "in_layers_2",
|
||||
"conv2": "out_layers_3",
|
||||
"time_emb_proj": "emb_layers_1",
|
||||
"conv_shortcut": "skip_connection",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def convert_diffusers_name_to_compvis(key):
|
||||
def match(match_list, regex):
|
||||
def convert_diffusers_name_to_compvis(key, is_sd2):
|
||||
def match(match_list, regex_text):
|
||||
regex = re_compiled.get(regex_text)
|
||||
if regex is None:
|
||||
regex = re.compile(regex_text)
|
||||
re_compiled[regex_text] = regex
|
||||
|
||||
r = re.match(regex, key)
|
||||
if not r:
|
||||
return False
|
||||
@@ -26,16 +40,33 @@ def convert_diffusers_name_to_compvis(key):
|
||||
|
||||
m = []
|
||||
|
||||
if match(m, re_unet_down_blocks):
|
||||
return f"diffusion_model_input_blocks_{1 + m[0] * 3 + m[1]}_1_{m[2]}"
|
||||
if match(m, r"lora_unet_down_blocks_(\d+)_(attentions|resnets)_(\d+)_(.+)"):
|
||||
suffix = suffix_conversion.get(m[1], {}).get(m[3], m[3])
|
||||
return f"diffusion_model_input_blocks_{1 + m[0] * 3 + m[2]}_{1 if m[1] == 'attentions' else 0}_{suffix}"
|
||||
|
||||
if match(m, re_unet_mid_blocks):
|
||||
return f"diffusion_model_middle_block_1_{m[1]}"
|
||||
if match(m, r"lora_unet_mid_block_(attentions|resnets)_(\d+)_(.+)"):
|
||||
suffix = suffix_conversion.get(m[0], {}).get(m[2], m[2])
|
||||
return f"diffusion_model_middle_block_{1 if m[0] == 'attentions' else m[1] * 2}_{suffix}"
|
||||
|
||||
if match(m, re_unet_up_blocks):
|
||||
return f"diffusion_model_output_blocks_{m[0] * 3 + m[1]}_1_{m[2]}"
|
||||
if match(m, r"lora_unet_up_blocks_(\d+)_(attentions|resnets)_(\d+)_(.+)"):
|
||||
suffix = suffix_conversion.get(m[1], {}).get(m[3], m[3])
|
||||
return f"diffusion_model_output_blocks_{m[0] * 3 + m[2]}_{1 if m[1] == 'attentions' else 0}_{suffix}"
|
||||
|
||||
if match(m, r"lora_unet_down_blocks_(\d+)_downsamplers_0_conv"):
|
||||
return f"diffusion_model_input_blocks_{3 + m[0] * 3}_0_op"
|
||||
|
||||
if match(m, r"lora_unet_up_blocks_(\d+)_upsamplers_0_conv"):
|
||||
return f"diffusion_model_output_blocks_{2 + m[0] * 3}_{2 if m[0]>0 else 1}_conv"
|
||||
|
||||
if match(m, r"lora_te_text_model_encoder_layers_(\d+)_(.+)"):
|
||||
if is_sd2:
|
||||
if 'mlp_fc1' in m[1]:
|
||||
return f"model_transformer_resblocks_{m[0]}_{m[1].replace('mlp_fc1', 'mlp_c_fc')}"
|
||||
elif 'mlp_fc2' in m[1]:
|
||||
return f"model_transformer_resblocks_{m[0]}_{m[1].replace('mlp_fc2', 'mlp_c_proj')}"
|
||||
else:
|
||||
return f"model_transformer_resblocks_{m[0]}_{m[1].replace('self_attn', 'attn')}"
|
||||
|
||||
if match(m, re_text_block):
|
||||
return f"transformer_text_model_encoder_layers_{m[0]}_{m[1]}"
|
||||
|
||||
return key
|
||||
@@ -101,15 +132,22 @@ def load_lora(name, filename):
|
||||
|
||||
sd = sd_models.read_state_dict(filename)
|
||||
|
||||
keys_failed_to_match = []
|
||||
keys_failed_to_match = {}
|
||||
is_sd2 = 'model_transformer_resblocks' in shared.sd_model.lora_layer_mapping
|
||||
|
||||
for key_diffusers, weight in sd.items():
|
||||
fullkey = convert_diffusers_name_to_compvis(key_diffusers)
|
||||
key, lora_key = fullkey.split(".", 1)
|
||||
key_diffusers_without_lora_parts, lora_key = key_diffusers.split(".", 1)
|
||||
key = convert_diffusers_name_to_compvis(key_diffusers_without_lora_parts, is_sd2)
|
||||
|
||||
sd_module = shared.sd_model.lora_layer_mapping.get(key, None)
|
||||
|
||||
if sd_module is None:
|
||||
keys_failed_to_match.append(key_diffusers)
|
||||
m = re_x_proj.match(key)
|
||||
if m:
|
||||
sd_module = shared.sd_model.lora_layer_mapping.get(m.group(1), None)
|
||||
|
||||
if sd_module is None:
|
||||
keys_failed_to_match[key_diffusers] = key
|
||||
continue
|
||||
|
||||
lora_module = lora.modules.get(key, None)
|
||||
@@ -123,15 +161,21 @@ def load_lora(name, filename):
|
||||
|
||||
if type(sd_module) == torch.nn.Linear:
|
||||
module = torch.nn.Linear(weight.shape[1], weight.shape[0], bias=False)
|
||||
elif type(sd_module) == torch.nn.modules.linear.NonDynamicallyQuantizableLinear:
|
||||
module = torch.nn.Linear(weight.shape[1], weight.shape[0], bias=False)
|
||||
elif type(sd_module) == torch.nn.MultiheadAttention:
|
||||
module = torch.nn.Linear(weight.shape[1], weight.shape[0], bias=False)
|
||||
elif type(sd_module) == torch.nn.Conv2d:
|
||||
module = torch.nn.Conv2d(weight.shape[1], weight.shape[0], (1, 1), bias=False)
|
||||
else:
|
||||
print(f'Lora layer {key_diffusers} matched a layer with unsupported type: {type(sd_module).__name__}')
|
||||
continue
|
||||
assert False, f'Lora layer {key_diffusers} matched a layer with unsupported type: {type(sd_module).__name__}'
|
||||
|
||||
with torch.no_grad():
|
||||
module.weight.copy_(weight)
|
||||
|
||||
module.to(device=devices.device, dtype=devices.dtype)
|
||||
module.to(device=devices.cpu, dtype=devices.dtype)
|
||||
|
||||
if lora_key == "lora_up.weight":
|
||||
lora_module.up = module
|
||||
@@ -177,29 +221,120 @@ def load_loras(names, multipliers=None):
|
||||
loaded_loras.append(lora)
|
||||
|
||||
|
||||
def lora_forward(module, input, res):
|
||||
input = devices.cond_cast_unet(input)
|
||||
if len(loaded_loras) == 0:
|
||||
return res
|
||||
def lora_calc_updown(lora, module, target):
|
||||
with torch.no_grad():
|
||||
up = module.up.weight.to(target.device, dtype=target.dtype)
|
||||
down = module.down.weight.to(target.device, dtype=target.dtype)
|
||||
|
||||
lora_layer_name = getattr(module, 'lora_layer_name', None)
|
||||
for lora in loaded_loras:
|
||||
module = lora.modules.get(lora_layer_name, None)
|
||||
if module is not None:
|
||||
if shared.opts.lora_apply_to_outputs and res.shape == input.shape:
|
||||
res = res + module.up(module.down(res)) * lora.multiplier * (module.alpha / module.up.weight.shape[1] if module.alpha else 1.0)
|
||||
if up.shape[2:] == (1, 1) and down.shape[2:] == (1, 1):
|
||||
updown = (up.squeeze(2).squeeze(2) @ down.squeeze(2).squeeze(2)).unsqueeze(2).unsqueeze(3)
|
||||
else:
|
||||
updown = up @ down
|
||||
|
||||
updown = updown * lora.multiplier * (module.alpha / module.up.weight.shape[1] if module.alpha else 1.0)
|
||||
|
||||
return updown
|
||||
|
||||
|
||||
def lora_apply_weights(self: Union[torch.nn.Conv2d, torch.nn.Linear, torch.nn.MultiheadAttention]):
|
||||
"""
|
||||
Applies the currently selected set of Loras to the weights of torch layer self.
|
||||
If weights already have this particular set of loras applied, does nothing.
|
||||
If not, restores orginal weights from backup and alters weights according to loras.
|
||||
"""
|
||||
|
||||
lora_layer_name = getattr(self, 'lora_layer_name', None)
|
||||
if lora_layer_name is None:
|
||||
return
|
||||
|
||||
current_names = getattr(self, "lora_current_names", ())
|
||||
wanted_names = tuple((x.name, x.multiplier) for x in loaded_loras)
|
||||
|
||||
weights_backup = getattr(self, "lora_weights_backup", None)
|
||||
if weights_backup is None:
|
||||
if isinstance(self, torch.nn.MultiheadAttention):
|
||||
weights_backup = (self.in_proj_weight.to(devices.cpu, copy=True), self.out_proj.weight.to(devices.cpu, copy=True))
|
||||
else:
|
||||
weights_backup = self.weight.to(devices.cpu, copy=True)
|
||||
|
||||
self.lora_weights_backup = weights_backup
|
||||
|
||||
if current_names != wanted_names:
|
||||
if weights_backup is not None:
|
||||
if isinstance(self, torch.nn.MultiheadAttention):
|
||||
self.in_proj_weight.copy_(weights_backup[0])
|
||||
self.out_proj.weight.copy_(weights_backup[1])
|
||||
else:
|
||||
res = res + module.up(module.down(input)) * lora.multiplier * (module.alpha / module.up.weight.shape[1] if module.alpha else 1.0)
|
||||
self.weight.copy_(weights_backup)
|
||||
|
||||
return res
|
||||
for lora in loaded_loras:
|
||||
module = lora.modules.get(lora_layer_name, None)
|
||||
if module is not None and hasattr(self, 'weight'):
|
||||
self.weight += lora_calc_updown(lora, module, self.weight)
|
||||
continue
|
||||
|
||||
module_q = lora.modules.get(lora_layer_name + "_q_proj", None)
|
||||
module_k = lora.modules.get(lora_layer_name + "_k_proj", None)
|
||||
module_v = lora.modules.get(lora_layer_name + "_v_proj", None)
|
||||
module_out = lora.modules.get(lora_layer_name + "_out_proj", None)
|
||||
|
||||
if isinstance(self, torch.nn.MultiheadAttention) and module_q and module_k and module_v and module_out:
|
||||
updown_q = lora_calc_updown(lora, module_q, self.in_proj_weight)
|
||||
updown_k = lora_calc_updown(lora, module_k, self.in_proj_weight)
|
||||
updown_v = lora_calc_updown(lora, module_v, self.in_proj_weight)
|
||||
updown_qkv = torch.vstack([updown_q, updown_k, updown_v])
|
||||
|
||||
self.in_proj_weight += updown_qkv
|
||||
self.out_proj.weight += lora_calc_updown(lora, module_out, self.out_proj.weight)
|
||||
continue
|
||||
|
||||
if module is None:
|
||||
continue
|
||||
|
||||
print(f'failed to calculate lora weights for layer {lora_layer_name}')
|
||||
|
||||
setattr(self, "lora_current_names", wanted_names)
|
||||
|
||||
|
||||
def lora_reset_cached_weight(self: Union[torch.nn.Conv2d, torch.nn.Linear]):
|
||||
setattr(self, "lora_current_names", ())
|
||||
setattr(self, "lora_weights_backup", None)
|
||||
|
||||
|
||||
def lora_Linear_forward(self, input):
|
||||
return lora_forward(self, input, torch.nn.Linear_forward_before_lora(self, input))
|
||||
lora_apply_weights(self)
|
||||
|
||||
return torch.nn.Linear_forward_before_lora(self, input)
|
||||
|
||||
|
||||
def lora_Linear_load_state_dict(self, *args, **kwargs):
|
||||
lora_reset_cached_weight(self)
|
||||
|
||||
return torch.nn.Linear_load_state_dict_before_lora(self, *args, **kwargs)
|
||||
|
||||
|
||||
def lora_Conv2d_forward(self, input):
|
||||
return lora_forward(self, input, torch.nn.Conv2d_forward_before_lora(self, input))
|
||||
lora_apply_weights(self)
|
||||
|
||||
return torch.nn.Conv2d_forward_before_lora(self, input)
|
||||
|
||||
|
||||
def lora_Conv2d_load_state_dict(self, *args, **kwargs):
|
||||
lora_reset_cached_weight(self)
|
||||
|
||||
return torch.nn.Conv2d_load_state_dict_before_lora(self, *args, **kwargs)
|
||||
|
||||
|
||||
def lora_MultiheadAttention_forward(self, *args, **kwargs):
|
||||
lora_apply_weights(self)
|
||||
|
||||
return torch.nn.MultiheadAttention_forward_before_lora(self, *args, **kwargs)
|
||||
|
||||
|
||||
def lora_MultiheadAttention_load_state_dict(self, *args, **kwargs):
|
||||
lora_reset_cached_weight(self)
|
||||
|
||||
return torch.nn.MultiheadAttention_load_state_dict_before_lora(self, *args, **kwargs)
|
||||
|
||||
|
||||
def list_available_loras():
|
||||
@@ -212,7 +347,7 @@ def list_available_loras():
|
||||
glob.glob(os.path.join(shared.cmd_opts.lora_dir, '**/*.safetensors'), recursive=True) + \
|
||||
glob.glob(os.path.join(shared.cmd_opts.lora_dir, '**/*.ckpt'), recursive=True)
|
||||
|
||||
for filename in sorted(candidates):
|
||||
for filename in sorted(candidates, key=str.lower):
|
||||
if os.path.isdir(filename):
|
||||
continue
|
||||
|
||||
|
||||
@@ -9,7 +9,11 @@ from modules import script_callbacks, ui_extra_networks, extra_networks, shared
|
||||
|
||||
def unload():
|
||||
torch.nn.Linear.forward = torch.nn.Linear_forward_before_lora
|
||||
torch.nn.Linear._load_from_state_dict = torch.nn.Linear_load_state_dict_before_lora
|
||||
torch.nn.Conv2d.forward = torch.nn.Conv2d_forward_before_lora
|
||||
torch.nn.Conv2d._load_from_state_dict = torch.nn.Conv2d_load_state_dict_before_lora
|
||||
torch.nn.MultiheadAttention.forward = torch.nn.MultiheadAttention_forward_before_lora
|
||||
torch.nn.MultiheadAttention._load_from_state_dict = torch.nn.MultiheadAttention_load_state_dict_before_lora
|
||||
|
||||
|
||||
def before_ui():
|
||||
@@ -20,11 +24,27 @@ def before_ui():
|
||||
if not hasattr(torch.nn, 'Linear_forward_before_lora'):
|
||||
torch.nn.Linear_forward_before_lora = torch.nn.Linear.forward
|
||||
|
||||
if not hasattr(torch.nn, 'Linear_load_state_dict_before_lora'):
|
||||
torch.nn.Linear_load_state_dict_before_lora = torch.nn.Linear._load_from_state_dict
|
||||
|
||||
if not hasattr(torch.nn, 'Conv2d_forward_before_lora'):
|
||||
torch.nn.Conv2d_forward_before_lora = torch.nn.Conv2d.forward
|
||||
|
||||
if not hasattr(torch.nn, 'Conv2d_load_state_dict_before_lora'):
|
||||
torch.nn.Conv2d_load_state_dict_before_lora = torch.nn.Conv2d._load_from_state_dict
|
||||
|
||||
if not hasattr(torch.nn, 'MultiheadAttention_forward_before_lora'):
|
||||
torch.nn.MultiheadAttention_forward_before_lora = torch.nn.MultiheadAttention.forward
|
||||
|
||||
if not hasattr(torch.nn, 'MultiheadAttention_load_state_dict_before_lora'):
|
||||
torch.nn.MultiheadAttention_load_state_dict_before_lora = torch.nn.MultiheadAttention._load_from_state_dict
|
||||
|
||||
torch.nn.Linear.forward = lora.lora_Linear_forward
|
||||
torch.nn.Linear._load_from_state_dict = lora.lora_Linear_load_state_dict
|
||||
torch.nn.Conv2d.forward = lora.lora_Conv2d_forward
|
||||
torch.nn.Conv2d._load_from_state_dict = lora.lora_Conv2d_load_state_dict
|
||||
torch.nn.MultiheadAttention.forward = lora.lora_MultiheadAttention_forward
|
||||
torch.nn.MultiheadAttention._load_from_state_dict = lora.lora_MultiheadAttention_load_state_dict
|
||||
|
||||
script_callbacks.on_model_loaded(lora.assign_lora_names_to_compvis_modules)
|
||||
script_callbacks.on_script_unloaded(unload)
|
||||
@@ -33,6 +53,4 @@ script_callbacks.on_before_ui(before_ui)
|
||||
|
||||
shared.options_templates.update(shared.options_section(('extra_networks', "Extra Networks"), {
|
||||
"sd_lora": shared.OptionInfo("None", "Add Lora to prompt", gr.Dropdown, lambda: {"choices": [""] + [x for x in lora.available_loras]}, refresh=lora.list_available_loras),
|
||||
"lora_apply_to_outputs": shared.OptionInfo(False, "Apply Lora to outputs rather than inputs when possible (experimental)"),
|
||||
|
||||
}))
|
||||
|
||||
@@ -28,4 +28,3 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
|
||||
|
||||
def allowed_directories_for_previews(self):
|
||||
return [shared.cmd_opts.lora_dir]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class='actions'>
|
||||
<div class='additional'>
|
||||
<ul>
|
||||
<a href="#" title="replace preview image with currently selected in gallery" onclick={save_card_preview}>replace preview</a>
|
||||
<a style="font-size:0.5rem" href="#" title="replace preview image with currently selected in gallery" onclick={save_card_preview}>replace preview</a>
|
||||
</ul>
|
||||
<span style="display:none" class='search_term'>{search_term}</span>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ function dimensionChange(e, is_width, is_height){
|
||||
currentHeight = e.target.value*1.0
|
||||
}
|
||||
|
||||
var inImg2img = Boolean(gradioApp().querySelector("button.rounded-t-lg.border-gray-200"))
|
||||
var inImg2img = gradioApp().querySelector("#tab_img2img").style.display == "block";
|
||||
|
||||
if(!inImg2img){
|
||||
return;
|
||||
@@ -22,7 +22,7 @@ function dimensionChange(e, is_width, is_height){
|
||||
|
||||
var tabIndex = get_tab_index('mode_img2img')
|
||||
if(tabIndex == 0){ // img2img
|
||||
targetElement = gradioApp().querySelector('div[data-testid=image] img');
|
||||
targetElement = gradioApp().querySelector('#img2img_image div[data-testid=image] img');
|
||||
} else if(tabIndex == 1){ //Sketch
|
||||
targetElement = gradioApp().querySelector('#img2img_sketch div[data-testid=image] img');
|
||||
} else if(tabIndex == 2){ // Inpaint
|
||||
@@ -30,7 +30,7 @@ function dimensionChange(e, is_width, is_height){
|
||||
} else if(tabIndex == 3){ // Inpaint sketch
|
||||
targetElement = gradioApp().querySelector('#inpaint_sketch div[data-testid=image] img');
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(targetElement){
|
||||
|
||||
@@ -38,7 +38,7 @@ function dimensionChange(e, is_width, is_height){
|
||||
if(!arPreviewRect){
|
||||
arPreviewRect = document.createElement('div')
|
||||
arPreviewRect.id = "imageARPreview";
|
||||
gradioApp().getRootNode().appendChild(arPreviewRect)
|
||||
gradioApp().appendChild(arPreviewRect)
|
||||
}
|
||||
|
||||
|
||||
@@ -91,23 +91,26 @@ onUiUpdate(function(){
|
||||
if(arPreviewRect){
|
||||
arPreviewRect.style.display = 'none';
|
||||
}
|
||||
var inImg2img = Boolean(gradioApp().querySelector("button.rounded-t-lg.border-gray-200"))
|
||||
if(inImg2img){
|
||||
let inputs = gradioApp().querySelectorAll('input');
|
||||
inputs.forEach(function(e){
|
||||
var is_width = e.parentElement.id == "img2img_width"
|
||||
var is_height = e.parentElement.id == "img2img_height"
|
||||
var tabImg2img = gradioApp().querySelector("#tab_img2img");
|
||||
if (tabImg2img) {
|
||||
var inImg2img = tabImg2img.style.display == "block";
|
||||
if(inImg2img){
|
||||
let inputs = gradioApp().querySelectorAll('input');
|
||||
inputs.forEach(function(e){
|
||||
var is_width = e.parentElement.id == "img2img_width"
|
||||
var is_height = e.parentElement.id == "img2img_height"
|
||||
|
||||
if((is_width || is_height) && !e.classList.contains('scrollwatch')){
|
||||
e.addEventListener('input', function(e){dimensionChange(e, is_width, is_height)} )
|
||||
e.classList.add('scrollwatch')
|
||||
}
|
||||
if(is_width){
|
||||
currentWidth = e.value*1.0
|
||||
}
|
||||
if(is_height){
|
||||
currentHeight = e.value*1.0
|
||||
}
|
||||
})
|
||||
}
|
||||
if((is_width || is_height) && !e.classList.contains('scrollwatch')){
|
||||
e.addEventListener('input', function(e){dimensionChange(e, is_width, is_height)} )
|
||||
e.classList.add('scrollwatch')
|
||||
}
|
||||
if(is_width){
|
||||
currentWidth = e.value*1.0
|
||||
}
|
||||
if(is_height){
|
||||
currentHeight = e.value*1.0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -132,13 +132,14 @@ addContextMenuEventListener = initResponse[2];
|
||||
//Start example Context Menu Items
|
||||
let generateOnRepeat = function(genbuttonid,interruptbuttonid){
|
||||
let genbutton = gradioApp().querySelector(genbuttonid);
|
||||
let interruptbutton = gradioApp().querySelector(interruptbuttonid);
|
||||
if(!interruptbutton.offsetParent){
|
||||
const busy = document.getElementById('progressbar')?.style.display == "block"
|
||||
if(!busy){
|
||||
genbutton.click();
|
||||
}
|
||||
clearInterval(window.generateOnRepeatInterval)
|
||||
window.generateOnRepeatInterval = setInterval(function(){
|
||||
if(!interruptbutton.offsetParent){
|
||||
const busy = document.getElementById('progressbar')?.style.display == "block"
|
||||
if(!busy){
|
||||
genbutton.click();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
function extensions_apply(_, _){
|
||||
function extensions_apply(_, _, disable_all){
|
||||
var disable = []
|
||||
var update = []
|
||||
|
||||
@@ -13,10 +13,10 @@ function extensions_apply(_, _){
|
||||
|
||||
restart_reload()
|
||||
|
||||
return [JSON.stringify(disable), JSON.stringify(update)]
|
||||
return [JSON.stringify(disable), JSON.stringify(update), disable_all]
|
||||
}
|
||||
|
||||
function extensions_check(){
|
||||
function extensions_check(_, _){
|
||||
var disable = []
|
||||
|
||||
gradioApp().querySelectorAll('#extensions input[type="checkbox"]').forEach(function(x){
|
||||
|
||||
@@ -21,8 +21,7 @@ titles = {
|
||||
"\u{1f5d1}\ufe0f": "Clear prompt",
|
||||
"\u{1f4cb}": "Apply selected styles to current prompt",
|
||||
"\u{1f4d2}": "Paste available values into the field",
|
||||
"\u{1f3b4}": "Show extra networks",
|
||||
|
||||
"\u{1f3b4}": "Show/hide extra networks",
|
||||
|
||||
"Inpaint a part of image": "Draw a mask over an image, and the script will regenerate the masked area with content according to prompt",
|
||||
"SD upscale": "Upscale image normally, split result into tiles, improve each tile using img2img, merge whole image back",
|
||||
|
||||
@@ -32,13 +32,7 @@ function negmod(n, m) {
|
||||
function updateOnBackgroundChange() {
|
||||
const modalImage = gradioApp().getElementById("modalImage")
|
||||
if (modalImage && modalImage.offsetParent) {
|
||||
let allcurrentButtons = gradioApp().querySelectorAll(".gallery-item.transition-all.\\!ring-2")
|
||||
let currentButton = null
|
||||
allcurrentButtons.forEach(function(elem) {
|
||||
if (elem.parentElement.offsetParent) {
|
||||
currentButton = elem;
|
||||
}
|
||||
})
|
||||
let currentButton = selected_gallery_button();
|
||||
|
||||
if (currentButton?.children?.length > 0 && modalImage.src != currentButton.children[0].src) {
|
||||
modalImage.src = currentButton.children[0].src;
|
||||
@@ -50,22 +44,10 @@ function updateOnBackgroundChange() {
|
||||
}
|
||||
|
||||
function modalImageSwitch(offset) {
|
||||
var allgalleryButtons = gradioApp().querySelectorAll(".gradio-gallery .thumbnail-item")
|
||||
var galleryButtons = []
|
||||
allgalleryButtons.forEach(function(elem) {
|
||||
if (elem.parentElement.offsetParent) {
|
||||
galleryButtons.push(elem);
|
||||
}
|
||||
})
|
||||
var galleryButtons = all_gallery_buttons();
|
||||
|
||||
if (galleryButtons.length > 1) {
|
||||
var allcurrentButtons = gradioApp().querySelectorAll(".gradio-gallery .thumbnail-item.selected")
|
||||
var currentButton = null
|
||||
allcurrentButtons.forEach(function(elem) {
|
||||
if (elem.parentElement.offsetParent) {
|
||||
currentButton = elem;
|
||||
}
|
||||
})
|
||||
var currentButton = selected_gallery_button();
|
||||
|
||||
var result = -1
|
||||
galleryButtons.forEach(function(v, i) {
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
|
||||
// localization = {} -- the dict with translations is created by the backend
|
||||
|
||||
ignore_ids_for_localization={
|
||||
setting_sd_hypernetwork: 'OPTION',
|
||||
setting_sd_model_checkpoint: 'OPTION',
|
||||
setting_realesrgan_enabled_models: 'OPTION',
|
||||
modelmerger_primary_model_name: 'OPTION',
|
||||
modelmerger_secondary_model_name: 'OPTION',
|
||||
modelmerger_tertiary_model_name: 'OPTION',
|
||||
train_embedding: 'OPTION',
|
||||
train_hypernetwork: 'OPTION',
|
||||
txt2img_styles: 'OPTION',
|
||||
img2img_styles: 'OPTION',
|
||||
setting_random_artist_categories: 'SPAN',
|
||||
setting_face_restoration_model: 'SPAN',
|
||||
setting_realesrgan_enabled_models: 'SPAN',
|
||||
extras_upscaler_1: 'SPAN',
|
||||
extras_upscaler_2: 'SPAN',
|
||||
}
|
||||
|
||||
re_num = /^[\.\d]+$/
|
||||
re_emoji = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/u
|
||||
|
||||
original_lines = {}
|
||||
translated_lines = {}
|
||||
|
||||
function textNodesUnder(el){
|
||||
var n, a=[], walk=document.createTreeWalker(el,NodeFilter.SHOW_TEXT,null,false);
|
||||
while(n=walk.nextNode()) a.push(n);
|
||||
return a;
|
||||
}
|
||||
|
||||
function canBeTranslated(node, text){
|
||||
if(! text) return false;
|
||||
if(! node.parentElement) return false;
|
||||
|
||||
parentType = node.parentElement.nodeName
|
||||
if(parentType=='SCRIPT' || parentType=='STYLE' || parentType=='TEXTAREA') return false;
|
||||
|
||||
if (parentType=='OPTION' || parentType=='SPAN'){
|
||||
pnode = node
|
||||
for(var level=0; level<4; level++){
|
||||
pnode = pnode.parentElement
|
||||
if(! pnode) break;
|
||||
|
||||
if(ignore_ids_for_localization[pnode.id] == parentType) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if(re_num.test(text)) return false;
|
||||
if(re_emoji.test(text)) return false;
|
||||
return true
|
||||
}
|
||||
|
||||
function getTranslation(text){
|
||||
if(! text) return undefined
|
||||
|
||||
if(translated_lines[text] === undefined){
|
||||
original_lines[text] = 1
|
||||
}
|
||||
|
||||
tl = localization[text]
|
||||
if(tl !== undefined){
|
||||
translated_lines[tl] = 1
|
||||
}
|
||||
|
||||
return tl
|
||||
}
|
||||
|
||||
function processTextNode(node){
|
||||
text = node.textContent.trim()
|
||||
|
||||
if(! canBeTranslated(node, text)) return
|
||||
|
||||
tl = getTranslation(text)
|
||||
if(tl !== undefined){
|
||||
node.textContent = tl
|
||||
}
|
||||
}
|
||||
|
||||
function processNode(node){
|
||||
if(node.nodeType == 3){
|
||||
processTextNode(node)
|
||||
return
|
||||
}
|
||||
|
||||
if(node.title){
|
||||
tl = getTranslation(node.title)
|
||||
if(tl !== undefined){
|
||||
node.title = tl
|
||||
}
|
||||
}
|
||||
|
||||
if(node.placeholder){
|
||||
tl = getTranslation(node.placeholder)
|
||||
if(tl !== undefined){
|
||||
node.placeholder = tl
|
||||
}
|
||||
}
|
||||
|
||||
textNodesUnder(node).forEach(function(node){
|
||||
processTextNode(node)
|
||||
})
|
||||
}
|
||||
|
||||
function dumpTranslations(){
|
||||
dumped = {}
|
||||
if (localization.rtl) {
|
||||
dumped.rtl = true
|
||||
}
|
||||
|
||||
Object.keys(original_lines).forEach(function(text){
|
||||
if(dumped[text] !== undefined) return
|
||||
|
||||
dumped[text] = localization[text] || text
|
||||
})
|
||||
|
||||
return dumped
|
||||
}
|
||||
|
||||
onUiUpdate(function(m){
|
||||
m.forEach(function(mutation){
|
||||
mutation.addedNodes.forEach(function(node){
|
||||
processNode(node)
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
processNode(gradioApp())
|
||||
|
||||
if (localization.rtl) { // if the language is from right to left,
|
||||
(new MutationObserver((mutations, observer) => { // wait for the style to load
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.tagName === 'STYLE') {
|
||||
observer.disconnect();
|
||||
|
||||
for (const x of node.sheet.rules) { // find all rtl media rules
|
||||
if (Array.from(x.media || []).includes('rtl')) {
|
||||
x.media.appendMedium('all'); // enable them
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
})).observe(gradioApp(), { childList: true });
|
||||
}
|
||||
})
|
||||
|
||||
function download_localization() {
|
||||
text = JSON.stringify(dumpTranslations(), null, 4)
|
||||
|
||||
var element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||
element.setAttribute('download', "localization.json");
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
|
||||
element.click();
|
||||
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ function requestProgress(id_task, progressbarContainer, gallery, atEnd, onProgre
|
||||
|
||||
var divProgress = document.createElement('div')
|
||||
divProgress.className='progressDiv'
|
||||
divProgress.id = 'progressbar'
|
||||
divProgress.style.display = opts.show_progressbar ? "block" : "none"
|
||||
var divInner = document.createElement('div')
|
||||
divInner.className='progress'
|
||||
|
||||
@@ -7,9 +7,31 @@ function set_theme(theme){
|
||||
}
|
||||
}
|
||||
|
||||
function all_gallery_buttons() {
|
||||
var allGalleryButtons = gradioApp().querySelectorAll('[style="display: block;"].tabitem div[id$=_gallery].gradio-gallery .thumbnails > .thumbnail-item.thumbnail-small');
|
||||
var visibleGalleryButtons = [];
|
||||
allGalleryButtons.forEach(function(elem) {
|
||||
if (elem.parentElement.offsetParent) {
|
||||
visibleGalleryButtons.push(elem);
|
||||
}
|
||||
})
|
||||
return visibleGalleryButtons;
|
||||
}
|
||||
|
||||
function selected_gallery_button() {
|
||||
var allCurrentButtons = gradioApp().querySelectorAll('[style="display: block;"].tabitem div[id$=_gallery].gradio-gallery .thumbnail-item.thumbnail-small.selected');
|
||||
var visibleCurrentButton = null;
|
||||
allCurrentButtons.forEach(function(elem) {
|
||||
if (elem.parentElement.offsetParent) {
|
||||
visibleCurrentButton = elem;
|
||||
}
|
||||
})
|
||||
return visibleCurrentButton;
|
||||
}
|
||||
|
||||
function selected_gallery_index(){
|
||||
var buttons = gradioApp().querySelectorAll('[style="display: block;"].tabitem div[id$=_gallery] .gallery-item')
|
||||
var button = gradioApp().querySelector('[style="display: block;"].tabitem div[id$=_gallery] .gallery-item.\\!ring-2')
|
||||
var buttons = all_gallery_buttons();
|
||||
var button = selected_gallery_button();
|
||||
|
||||
var result = -1
|
||||
buttons.forEach(function(v, i){ if(v==button) { result = i } })
|
||||
@@ -18,14 +40,18 @@ function selected_gallery_index(){
|
||||
}
|
||||
|
||||
function extract_image_from_gallery(gallery){
|
||||
if(gallery.length == 1){
|
||||
return [gallery[0]]
|
||||
if (gallery.length == 0){
|
||||
return [null];
|
||||
}
|
||||
if (gallery.length == 1){
|
||||
return [gallery[0]];
|
||||
}
|
||||
|
||||
index = selected_gallery_index()
|
||||
|
||||
if (index < 0 || index >= gallery.length){
|
||||
return [null]
|
||||
// Use the first image in the gallery as the default
|
||||
index = 0;
|
||||
}
|
||||
|
||||
return [gallery[index]];
|
||||
@@ -129,8 +155,10 @@ function create_submit_args(args){
|
||||
}
|
||||
|
||||
function showSubmitButtons(tabname, show){
|
||||
gradioApp().getElementById(tabname+'_interrupt').style.display = show ? "none" : "block"
|
||||
gradioApp().getElementById(tabname+'_skip').style.display = show ? "none" : "block"
|
||||
// gradioApp().getElementById(tabname+'_interrupt').style.display = show ? "none" : "block"
|
||||
// gradioApp().getElementById(tabname+'_skip').style.display = show ? "none" : "block"
|
||||
gradioApp().getElementById(tabname+'_interrupt').style.display = "block"
|
||||
gradioApp().getElementById(tabname+'_skip').style.display = "block"
|
||||
}
|
||||
|
||||
function submit(){
|
||||
|
||||
@@ -1,116 +1,62 @@
|
||||
# this scripts installs necessary requirements and launches main program in webui.py
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
import importlib.util
|
||||
import shlex
|
||||
import platform
|
||||
import json
|
||||
|
||||
import setup
|
||||
from modules import cmd_args
|
||||
from modules.paths_internal import script_path, extensions_dir
|
||||
from modules.paths_internal import script_path
|
||||
|
||||
try:
|
||||
from rich import print # pylint: disable=redefined-builtin
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
commandline_args = os.environ.get('COMMANDLINE_ARGS', "")
|
||||
sys.argv += shlex.split(commandline_args)
|
||||
|
||||
setup.parse_args()
|
||||
args, _ = cmd_args.parser.parse_known_args()
|
||||
|
||||
python = sys.executable
|
||||
git = os.environ.get('GIT', "git")
|
||||
index_url = os.environ.get('INDEX_URL', "")
|
||||
stored_commit_hash = None
|
||||
skip_install = False
|
||||
dir_repos = "repositories"
|
||||
|
||||
if 'GRADIO_ANALYTICS_ENABLED' not in os.environ:
|
||||
os.environ['GRADIO_ANALYTICS_ENABLED'] = 'False'
|
||||
|
||||
|
||||
def check_python_version():
|
||||
is_windows = platform.system() == "Windows"
|
||||
major = sys.version_info.major
|
||||
minor = sys.version_info.minor
|
||||
micro = sys.version_info.micro
|
||||
|
||||
if is_windows:
|
||||
supported_minors = [10]
|
||||
else:
|
||||
supported_minors = [7, 8, 9, 10, 11]
|
||||
|
||||
if not (major == 3 and minor in supported_minors):
|
||||
import modules.errors
|
||||
|
||||
modules.errors.print_error_explanation(f"""
|
||||
INCOMPATIBLE PYTHON VERSION
|
||||
|
||||
This program is tested with 3.10.6 Python, but you have {major}.{minor}.{micro}.
|
||||
If you encounter an error with "RuntimeError: Couldn't install torch." message,
|
||||
or any other error regarding unsuccessful package (library) installation,
|
||||
please downgrade (or upgrade) to the latest version of 3.10 Python
|
||||
and delete current Python and "venv" folder in WebUI's directory.
|
||||
|
||||
You can download 3.10 Python from here: https://www.python.org/downloads/release/python-3109/
|
||||
|
||||
{"Alternatively, use a binary release of WebUI: https://github.com/AUTOMATIC1111/stable-diffusion-webui/releases" if is_windows else ""}
|
||||
|
||||
Use --skip-python-version-check to suppress this warning.
|
||||
""")
|
||||
|
||||
|
||||
def commit_hash():
|
||||
global stored_commit_hash
|
||||
|
||||
global stored_commit_hash # pylint: disable=global-statement
|
||||
if stored_commit_hash is not None:
|
||||
return stored_commit_hash
|
||||
|
||||
try:
|
||||
stored_commit_hash = run(f"{git} rev-parse HEAD").strip()
|
||||
except Exception:
|
||||
stored_commit_hash = "<none>"
|
||||
|
||||
return stored_commit_hash
|
||||
|
||||
|
||||
def run(command, desc=None, errdesc=None, custom_env=None, live=False):
|
||||
if desc is not None:
|
||||
print(desc)
|
||||
|
||||
if live:
|
||||
result = subprocess.run(command, shell=True, env=os.environ if custom_env is None else custom_env)
|
||||
result = subprocess.run(command, check=False, shell=True, env=os.environ if custom_env is None else custom_env)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"""{errdesc or 'Error running command'}.
|
||||
Command: {command}
|
||||
Error code: {result.returncode}""")
|
||||
|
||||
return ""
|
||||
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=os.environ if custom_env is None else custom_env)
|
||||
|
||||
raise RuntimeError(f"""{errdesc or 'Error running command'} Command: {command} Error code: {result.returncode}""")
|
||||
return ''
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE, check=False, stderr=subprocess.PIPE, shell=True, env=os.environ if custom_env is None else custom_env)
|
||||
if result.returncode != 0:
|
||||
|
||||
message = f"""{errdesc or 'Error running command'}.
|
||||
Command: {command}
|
||||
Error code: {result.returncode}
|
||||
stdout: {result.stdout.decode(encoding="utf8", errors="ignore") if len(result.stdout)>0 else '<empty>'}
|
||||
stderr: {result.stderr.decode(encoding="utf8", errors="ignore") if len(result.stderr)>0 else '<empty>'}
|
||||
"""
|
||||
raise RuntimeError(message)
|
||||
|
||||
raise RuntimeError(f"""{errdesc or 'Error running command'}: {command} code: {result.returncode}
|
||||
{result.stdout.decode(encoding="utf8", errors="ignore") if len(result.stdout)>0 else ''}
|
||||
{result.stderr.decode(encoding="utf8", errors="ignore") if len(result.stderr)>0 else ''}
|
||||
""")
|
||||
return result.stdout.decode(encoding="utf8", errors="ignore")
|
||||
|
||||
|
||||
def check_run(command):
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
||||
result = subprocess.run(command, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def is_installed(package):
|
||||
try:
|
||||
spec = importlib.util.find_spec(package)
|
||||
except ModuleNotFoundError:
|
||||
return False
|
||||
|
||||
return spec is not None
|
||||
return setup.installed(package)
|
||||
|
||||
|
||||
def repo_dir(name):
|
||||
@@ -118,235 +64,31 @@ def repo_dir(name):
|
||||
|
||||
|
||||
def run_python(code, desc=None, errdesc=None):
|
||||
return run(f'"{python}" -c "{code}"', desc, errdesc)
|
||||
return run(f'"{sys.executable}" -c "{code}"', desc, errdesc)
|
||||
|
||||
|
||||
def run_pip(args, desc=None):
|
||||
if skip_install:
|
||||
return
|
||||
|
||||
def run_pip(pkg, desc=None):
|
||||
index_url_line = f' --index-url {index_url}' if index_url != '' else ''
|
||||
return run(f'"{python}" -m pip {args} --prefer-binary{index_url_line}', desc=f"Installing {desc}", errdesc=f"Couldn't install {desc}")
|
||||
return run(f'"{sys.executable}" -m pip {pkg} --prefer-binary{index_url_line}', desc=f"Installing {desc}", errdesc=f"Couldn't install {desc}")
|
||||
|
||||
|
||||
def check_run_python(code):
|
||||
return check_run(f'"{python}" -c "{code}"')
|
||||
return check_run(f'"{sys.executable}" -c "{code}"')
|
||||
|
||||
|
||||
def git_clone(url, dir, name, commithash=None):
|
||||
# TODO clone into temporary dir and move if successful
|
||||
|
||||
if os.path.exists(dir):
|
||||
if commithash is None:
|
||||
return
|
||||
|
||||
current_hash = run(f'"{git}" -C "{dir}" rev-parse HEAD', None, f"Couldn't determine {name}'s hash: {commithash}").strip()
|
||||
if current_hash == commithash:
|
||||
return
|
||||
|
||||
run(f'"{git}" -C "{dir}" fetch', f"Fetching updates for {name}...", f"Couldn't fetch {name}")
|
||||
run(f'"{git}" -C "{dir}" checkout {commithash}', f"Checking out commit for {name} with hash: {commithash}...", f"Couldn't checkout commit {commithash} for {name}")
|
||||
return
|
||||
|
||||
run(f'"{git}" clone "{url}" "{dir}"', f"Cloning {name} into {dir}...", f"Couldn't clone {name}")
|
||||
|
||||
if commithash is not None:
|
||||
run(f'"{git}" -C "{dir}" checkout {commithash}', None, "Couldn't checkout {name}'s hash: {commithash}")
|
||||
def git_clone(url, tgt, _name, commithash=None):
|
||||
setup.clone(url, tgt, commithash)
|
||||
|
||||
|
||||
def git_pull_recursive(dir):
|
||||
for subdir, _, _ in os.walk(dir):
|
||||
if os.path.exists(os.path.join(subdir, '.git')):
|
||||
try:
|
||||
output = subprocess.check_output([git, '-C', subdir, 'pull', '--autostash'])
|
||||
print(f"Pulled changes for repository in '{subdir}':\n{output.decode('utf-8').strip()}\n")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Couldn't perform 'git pull' on repository in '{subdir}':\n{e.output.decode('utf-8').strip()}\n")
|
||||
|
||||
|
||||
def version_check(commit):
|
||||
try:
|
||||
import requests
|
||||
commits = requests.get('https://api.github.com/repos/AUTOMATIC1111/stable-diffusion-webui/branches/master').json()
|
||||
if commit != "<none>" and commits['commit']['sha'] != commit:
|
||||
print("--------------------------------------------------------")
|
||||
print("| You are not up to date with the most recent release. |")
|
||||
print("| Consider running `git pull` to update. |")
|
||||
print("--------------------------------------------------------")
|
||||
elif commits['commit']['sha'] == commit:
|
||||
print("You are up to date with the most recent release.")
|
||||
else:
|
||||
print("Not a git clone, can't perform version check.")
|
||||
except Exception as e:
|
||||
print("version check failed", e)
|
||||
|
||||
|
||||
def run_extension_installer(extension_dir):
|
||||
path_installer = os.path.join(extension_dir, "install.py")
|
||||
if not os.path.isfile(path_installer):
|
||||
return
|
||||
|
||||
try:
|
||||
env = os.environ.copy()
|
||||
env['PYTHONPATH'] = os.path.abspath(".")
|
||||
|
||||
print(run(f'"{python}" "{path_installer}"', errdesc=f"Error running install.py for extension {extension_dir}", custom_env=env))
|
||||
except Exception as e:
|
||||
print(e, file=sys.stderr)
|
||||
|
||||
|
||||
def list_extensions(settings_file):
|
||||
settings = {}
|
||||
|
||||
try:
|
||||
if os.path.isfile(settings_file):
|
||||
with open(settings_file, "r", encoding="utf8") as file:
|
||||
settings = json.load(file)
|
||||
except Exception as e:
|
||||
print(e, file=sys.stderr)
|
||||
|
||||
disabled_extensions = set(settings.get('disabled_extensions', []))
|
||||
|
||||
return [x for x in os.listdir(extensions_dir) if x not in disabled_extensions]
|
||||
|
||||
|
||||
def run_extensions_installers(settings_file):
|
||||
if not os.path.isdir(extensions_dir):
|
||||
return
|
||||
|
||||
for dirname_extension in list_extensions(settings_file):
|
||||
run_extension_installer(os.path.join(extensions_dir, dirname_extension))
|
||||
|
||||
|
||||
def prepare_environment():
|
||||
global skip_install
|
||||
|
||||
torch_command = os.environ.get('TORCH_COMMAND', "pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117")
|
||||
requirements_file = os.environ.get('REQS_FILE', "requirements_versions.txt")
|
||||
|
||||
xformers_package = os.environ.get('XFORMERS_PACKAGE', 'xformers==0.0.16rc425')
|
||||
gfpgan_package = os.environ.get('GFPGAN_PACKAGE', "git+https://github.com/TencentARC/GFPGAN.git@8d2447a2d918f8eba5a4a01463fd48e45126a379")
|
||||
clip_package = os.environ.get('CLIP_PACKAGE', "git+https://github.com/openai/CLIP.git@d50d76daa670286dd6cacf3bcd80b5e4823fc8e1")
|
||||
openclip_package = os.environ.get('OPENCLIP_PACKAGE', "git+https://github.com/mlfoundations/open_clip.git@bb6e834e9c70d9c27d0dc3ecedeebeaeb1ffad6b")
|
||||
|
||||
stable_diffusion_repo = os.environ.get('STABLE_DIFFUSION_REPO', "https://github.com/Stability-AI/stablediffusion.git")
|
||||
taming_transformers_repo = os.environ.get('TAMING_TRANSFORMERS_REPO', "https://github.com/CompVis/taming-transformers.git")
|
||||
k_diffusion_repo = os.environ.get('K_DIFFUSION_REPO', 'https://github.com/crowsonkb/k-diffusion.git')
|
||||
codeformer_repo = os.environ.get('CODEFORMER_REPO', 'https://github.com/sczhou/CodeFormer.git')
|
||||
blip_repo = os.environ.get('BLIP_REPO', 'https://github.com/salesforce/BLIP.git')
|
||||
|
||||
stable_diffusion_commit_hash = os.environ.get('STABLE_DIFFUSION_COMMIT_HASH', "47b6b607fdd31875c9279cd2f4f16b92e4ea958e")
|
||||
taming_transformers_commit_hash = os.environ.get('TAMING_TRANSFORMERS_COMMIT_HASH', "24268930bf1dce879235a7fddd0b2355b84d7ea6")
|
||||
k_diffusion_commit_hash = os.environ.get('K_DIFFUSION_COMMIT_HASH', "5b3af030dd83e0297272d861c19477735d0317ec")
|
||||
codeformer_commit_hash = os.environ.get('CODEFORMER_COMMIT_HASH', "c5b4593074ba6214284d6acd5f1719b6c5d739af")
|
||||
blip_commit_hash = os.environ.get('BLIP_COMMIT_HASH', "48211a1594f1321b00f14c9f7a5b4813144b2fb9")
|
||||
|
||||
if not args.skip_python_version_check:
|
||||
check_python_version()
|
||||
|
||||
commit = commit_hash()
|
||||
|
||||
print(f"Python {sys.version}")
|
||||
print(f"Commit hash: {commit}")
|
||||
|
||||
if args.reinstall_torch or not is_installed("torch") or not is_installed("torchvision"):
|
||||
run(f'"{python}" -m {torch_command}', "Installing torch and torchvision", "Couldn't install torch", live=True)
|
||||
|
||||
if not args.skip_torch_cuda_test:
|
||||
run_python("import torch; assert torch.cuda.is_available(), 'Torch is not able to use GPU; add --skip-torch-cuda-test to COMMANDLINE_ARGS variable to disable this check'")
|
||||
|
||||
if not is_installed("gfpgan"):
|
||||
run_pip(f"install {gfpgan_package}", "gfpgan")
|
||||
|
||||
if not is_installed("clip"):
|
||||
run_pip(f"install {clip_package}", "clip")
|
||||
|
||||
if not is_installed("open_clip"):
|
||||
run_pip(f"install {openclip_package}", "open_clip")
|
||||
|
||||
if (not is_installed("xformers") or args.reinstall_xformers) and args.xformers:
|
||||
if platform.system() == "Windows":
|
||||
if platform.python_version().startswith("3.10"):
|
||||
run_pip(f"install -U -I --no-deps {xformers_package}", "xformers")
|
||||
else:
|
||||
print("Installation of xformers is not supported in this version of Python.")
|
||||
print("You can also check this and build manually: https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Xformers#building-xformers-on-windows-by-duckness")
|
||||
if not is_installed("xformers"):
|
||||
exit(0)
|
||||
elif platform.system() == "Linux":
|
||||
run_pip(f"install {xformers_package}", "xformers")
|
||||
|
||||
if not is_installed("pyngrok") and args.ngrok:
|
||||
run_pip("install pyngrok", "ngrok")
|
||||
|
||||
os.makedirs(os.path.join(script_path, dir_repos), exist_ok=True)
|
||||
|
||||
git_clone(stable_diffusion_repo, repo_dir('stable-diffusion-stability-ai'), "Stable Diffusion", stable_diffusion_commit_hash)
|
||||
git_clone(taming_transformers_repo, repo_dir('taming-transformers'), "Taming Transformers", taming_transformers_commit_hash)
|
||||
git_clone(k_diffusion_repo, repo_dir('k-diffusion'), "K-diffusion", k_diffusion_commit_hash)
|
||||
git_clone(codeformer_repo, repo_dir('CodeFormer'), "CodeFormer", codeformer_commit_hash)
|
||||
git_clone(blip_repo, repo_dir('BLIP'), "BLIP", blip_commit_hash)
|
||||
|
||||
if not is_installed("lpips"):
|
||||
run_pip(f"install -r \"{os.path.join(repo_dir('CodeFormer'), 'requirements.txt')}\"", "requirements for CodeFormer")
|
||||
|
||||
if not os.path.isfile(requirements_file):
|
||||
requirements_file = os.path.join(script_path, requirements_file)
|
||||
run_pip(f"install -r \"{requirements_file}\"", "requirements for Web UI")
|
||||
|
||||
run_extensions_installers(settings_file=args.ui_settings_file)
|
||||
|
||||
if args.update_check:
|
||||
version_check(commit)
|
||||
|
||||
if args.update_all_extensions:
|
||||
git_pull_recursive(extensions_dir)
|
||||
|
||||
if "--exit" in sys.argv:
|
||||
print("Exiting because of --exit argument")
|
||||
exit(0)
|
||||
|
||||
if args.tests and not args.no_tests:
|
||||
exitcode = tests(args.tests)
|
||||
exit(exitcode)
|
||||
|
||||
|
||||
def tests(test_dir):
|
||||
if "--api" not in sys.argv:
|
||||
sys.argv.append("--api")
|
||||
if "--ckpt" not in sys.argv:
|
||||
sys.argv.append("--ckpt")
|
||||
sys.argv.append(os.path.join(script_path, "test/test_files/empty.pt"))
|
||||
if "--skip-torch-cuda-test" not in sys.argv:
|
||||
sys.argv.append("--skip-torch-cuda-test")
|
||||
if "--disable-nan-check" not in sys.argv:
|
||||
sys.argv.append("--disable-nan-check")
|
||||
if "--no-tests" not in sys.argv:
|
||||
sys.argv.append("--no-tests")
|
||||
|
||||
print(f"Launching Web UI in another process for testing with arguments: {' '.join(sys.argv[1:])}")
|
||||
|
||||
os.environ['COMMANDLINE_ARGS'] = ""
|
||||
with open(os.path.join(script_path, 'test/stdout.txt'), "w", encoding="utf8") as stdout, open(os.path.join(script_path, 'test/stderr.txt'), "w", encoding="utf8") as stderr:
|
||||
proc = subprocess.Popen([sys.executable, *sys.argv], stdout=stdout, stderr=stderr)
|
||||
|
||||
import test.server_poll
|
||||
exitcode = test.server_poll.run_tests(proc, test_dir)
|
||||
|
||||
print(f"Stopping Web UI process with id {proc.pid}")
|
||||
proc.kill()
|
||||
return exitcode
|
||||
|
||||
|
||||
def start():
|
||||
print(f"Launching {'API server' if '--nowebui' in sys.argv else 'Web UI'} with arguments: {' '.join(sys.argv[1:])}")
|
||||
import webui
|
||||
if '--nowebui' in sys.argv:
|
||||
webui.api_only()
|
||||
else:
|
||||
webui.webui()
|
||||
def run_extension_installer(ext_dir):
|
||||
setup.run_extension_installer(ext_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
prepare_environment()
|
||||
start()
|
||||
setup.run_setup()
|
||||
setup.set_environment()
|
||||
setup.check_torch()
|
||||
setup.log.info(f"Server arguments: {sys.argv[1:]}")
|
||||
setup.log.debug('Starting WebUI')
|
||||
import webui
|
||||
webui.webui()
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
import base64
|
||||
import io
|
||||
import time
|
||||
import base64
|
||||
import datetime
|
||||
import uvicorn
|
||||
from threading import Lock
|
||||
from io import BytesIO
|
||||
from gradio.processing_utils import decode_base64_to_file
|
||||
from typing import List
|
||||
from threading import Lock
|
||||
from secrets import compare_digest
|
||||
import anyio
|
||||
import starlette
|
||||
import fastapi
|
||||
from fastapi import APIRouter, Depends, FastAPI, Request, Response
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from secrets import compare_digest
|
||||
from PIL import PngImagePlugin,Image
|
||||
import piexif
|
||||
import piexif.helper
|
||||
import uvicorn
|
||||
import gradio as gr
|
||||
from gradio.processing_utils import decode_base64_to_file
|
||||
# from gradio_client.utils import decode_base64_to_file
|
||||
|
||||
import modules.shared as shared
|
||||
from modules import sd_samplers, deepbooru, sd_hijack, images, scripts, ui, postprocessing
|
||||
from modules import errors, shared, sd_samplers, deepbooru, sd_hijack, images, scripts, ui, postprocessing
|
||||
from modules.api.models import *
|
||||
from modules.processing import StableDiffusionProcessingTxt2Img, StableDiffusionProcessingImg2Img, process_images
|
||||
from modules.textual_inversion.textual_inversion import create_embedding, train_embedding
|
||||
from modules.textual_inversion.preprocess import preprocess
|
||||
from modules.hypernetworks.hypernetwork import create_hypernetwork, train_hypernetwork
|
||||
from PIL import PngImagePlugin,Image
|
||||
from modules.sd_models import checkpoints_list, unload_model_weights, reload_model_weights
|
||||
from modules.sd_models_config import find_checkpoint_config_near_filename
|
||||
from modules.realesrgan_model import get_realesrgan_models
|
||||
from modules import devices
|
||||
from typing import List
|
||||
import piexif
|
||||
import piexif.helper
|
||||
|
||||
def upscaler_to_index(name: str):
|
||||
try:
|
||||
@@ -60,7 +64,7 @@ def decode_base64_to_image(encoding):
|
||||
try:
|
||||
image = Image.open(BytesIO(base64.b64decode(encoding)))
|
||||
return image
|
||||
except Exception as err:
|
||||
except Exception:
|
||||
raise HTTPException(status_code=500, detail="Invalid encoded image")
|
||||
|
||||
def encode_pil_to_base64(image):
|
||||
@@ -93,15 +97,6 @@ def encode_pil_to_base64(image):
|
||||
return base64.b64encode(bytes_data)
|
||||
|
||||
def api_middleware(app: FastAPI):
|
||||
rich_available = True
|
||||
try:
|
||||
import anyio # importing just so it can be placed on silent list
|
||||
import starlette # importing just so it can be placed on silent list
|
||||
from rich.console import Console
|
||||
console = Console()
|
||||
except:
|
||||
import traceback
|
||||
rich_available = False
|
||||
|
||||
@app.middleware("http")
|
||||
async def log_and_time(req: Request, call_next):
|
||||
@@ -132,10 +127,7 @@ def api_middleware(app: FastAPI):
|
||||
}
|
||||
print(f"API error: {request.method}: {request.url} {err}")
|
||||
if not isinstance(e, HTTPException): # do not print backtrace on known httpexceptions
|
||||
if rich_available:
|
||||
console.print_exception(show_locals=True, max_frames=2, extra_lines=1, suppress=[anyio, starlette], word_wrap=False, width=min([console.width, 200]))
|
||||
else:
|
||||
traceback.print_exc()
|
||||
errors.display(e, 'http api', [anyio, fastapi, uvicorn, starlette])
|
||||
return JSONResponse(status_code=vars(e).get('status_code', 500), content=jsonable_encoder(err))
|
||||
|
||||
@app.middleware("http")
|
||||
@@ -192,11 +184,15 @@ class Api:
|
||||
self.add_api_route("/sdapi/v1/preprocess", self.preprocess, methods=["POST"], response_model=PreprocessResponse)
|
||||
self.add_api_route("/sdapi/v1/train/embedding", self.train_embedding, methods=["POST"], response_model=TrainResponse)
|
||||
self.add_api_route("/sdapi/v1/train/hypernetwork", self.train_hypernetwork, methods=["POST"], response_model=TrainResponse)
|
||||
self.add_api_route("/sdapi/v1/shutdown", self.shutdown, methods=["POST"])
|
||||
self.add_api_route("/sdapi/v1/memory", self.get_memory, methods=["GET"], response_model=MemoryResponse)
|
||||
self.add_api_route("/sdapi/v1/unload-checkpoint", self.unloadapi, methods=["POST"])
|
||||
self.add_api_route("/sdapi/v1/reload-checkpoint", self.reloadapi, methods=["POST"])
|
||||
self.add_api_route("/sdapi/v1/scripts", self.get_scripts_list, methods=["GET"], response_model=ScriptsList)
|
||||
|
||||
self.default_script_arg_txt2img = []
|
||||
self.default_script_arg_img2img = []
|
||||
|
||||
def add_api_route(self, path: str, endpoint, **kwargs):
|
||||
if shared.cmd_opts.api_auth:
|
||||
return self.app.add_api_route(path, endpoint, dependencies=[Depends(self.auth)], **kwargs)
|
||||
@@ -216,21 +212,21 @@ class Api:
|
||||
script_idx = script_name_to_index(script_name, script_runner.selectable_scripts)
|
||||
script = script_runner.selectable_scripts[script_idx]
|
||||
return script, script_idx
|
||||
|
||||
|
||||
def get_scripts_list(self):
|
||||
t2ilist = [str(title.lower()) for title in scripts.scripts_txt2img.titles]
|
||||
i2ilist = [str(title.lower()) for title in scripts.scripts_img2img.titles]
|
||||
|
||||
return ScriptsList(txt2img = t2ilist, img2img = i2ilist)
|
||||
return ScriptsList(txt2img = t2ilist, img2img = i2ilist)
|
||||
|
||||
def get_script(self, script_name, script_runner):
|
||||
if script_name is None or script_name == "":
|
||||
return None, None
|
||||
|
||||
|
||||
script_idx = script_name_to_index(script_name, script_runner.scripts)
|
||||
return script_runner.scripts[script_idx]
|
||||
|
||||
def init_script_args(self, request, selectable_scripts, selectable_idx, script_runner):
|
||||
def init_default_script_args(self, script_runner):
|
||||
#find max idx from the scripts in runner and generate a none array to init script_args
|
||||
last_arg_index = 1
|
||||
for script in script_runner.scripts:
|
||||
@@ -238,22 +234,33 @@ class Api:
|
||||
last_arg_index = script.args_to
|
||||
# None everywhere except position 0 to initialize script args
|
||||
script_args = [None]*last_arg_index
|
||||
script_args[0] = 0
|
||||
|
||||
# get default values
|
||||
with gr.Blocks(): # will throw errors calling ui function without this
|
||||
for script in script_runner.scripts:
|
||||
if script.ui(script.is_img2img):
|
||||
ui_default_values = []
|
||||
for elem in script.ui(script.is_img2img):
|
||||
ui_default_values.append(elem.value)
|
||||
script_args[script.args_from:script.args_to] = ui_default_values
|
||||
return script_args
|
||||
|
||||
def init_script_args(self, request, default_script_args, selectable_scripts, selectable_idx, script_runner):
|
||||
script_args = default_script_args.copy()
|
||||
# position 0 in script_arg is the idx+1 of the selectable script that is going to be run when using scripts.scripts_*2img.run()
|
||||
if selectable_scripts:
|
||||
script_args[selectable_scripts.args_from:selectable_scripts.args_to] = request.script_args
|
||||
script_args[0] = selectable_idx + 1
|
||||
else:
|
||||
# when [0] = 0 no selectable script to run
|
||||
script_args[0] = 0
|
||||
|
||||
# Now check for always on scripts
|
||||
if request.alwayson_scripts and (len(request.alwayson_scripts) > 0):
|
||||
for alwayson_script_name in request.alwayson_scripts.keys():
|
||||
alwayson_script = self.get_script(alwayson_script_name, script_runner)
|
||||
if alwayson_script == None:
|
||||
if alwayson_script is None:
|
||||
raise HTTPException(status_code=422, detail=f"always on script {alwayson_script_name} not found")
|
||||
# Selectable script in always on script param check
|
||||
if alwayson_script.alwayson == False:
|
||||
if not alwayson_script.alwayson:
|
||||
raise HTTPException(status_code=422, detail=f"Cannot have a selectable script in the always on scripts params")
|
||||
# always on script with no arg should always run so you don't really need to add them to the requests
|
||||
if "args" in request.alwayson_scripts[alwayson_script_name]:
|
||||
@@ -265,6 +272,8 @@ class Api:
|
||||
if not script_runner.scripts:
|
||||
script_runner.initialize_scripts(False)
|
||||
ui.create_ui()
|
||||
if not self.default_script_arg_txt2img:
|
||||
self.default_script_arg_txt2img = self.init_default_script_args(script_runner)
|
||||
selectable_scripts, selectable_script_idx = self.get_selectable_script(txt2imgreq.script_name, script_runner)
|
||||
|
||||
populate = txt2imgreq.copy(update={ # Override __init__ params
|
||||
@@ -280,7 +289,7 @@ class Api:
|
||||
args.pop('script_args', None) # will refeed them to the pipeline directly after initializing them
|
||||
args.pop('alwayson_scripts', None)
|
||||
|
||||
script_args = self.init_script_args(txt2imgreq, selectable_scripts, selectable_script_idx, script_runner)
|
||||
script_args = self.init_script_args(txt2imgreq, self.default_script_arg_txt2img, selectable_scripts, selectable_script_idx, script_runner)
|
||||
|
||||
send_images = args.pop('send_images', True)
|
||||
args.pop('save_images', None)
|
||||
@@ -292,7 +301,7 @@ class Api:
|
||||
p.outpath_samples = opts.outdir_txt2img_samples
|
||||
|
||||
shared.state.begin()
|
||||
if selectable_scripts != None:
|
||||
if selectable_scripts is not None:
|
||||
p.script_args = script_args
|
||||
processed = scripts.scripts_txt2img.run(p, *p.script_args) # Need to pass args as list here
|
||||
else:
|
||||
@@ -317,6 +326,8 @@ class Api:
|
||||
if not script_runner.scripts:
|
||||
script_runner.initialize_scripts(True)
|
||||
ui.create_ui()
|
||||
if not self.default_script_arg_img2img:
|
||||
self.default_script_arg_img2img = self.init_default_script_args(script_runner)
|
||||
selectable_scripts, selectable_script_idx = self.get_selectable_script(img2imgreq.script_name, script_runner)
|
||||
|
||||
populate = img2imgreq.copy(update={ # Override __init__ params
|
||||
@@ -334,7 +345,7 @@ class Api:
|
||||
args.pop('script_args', None) # will refeed them to the pipeline directly after initializing them
|
||||
args.pop('alwayson_scripts', None)
|
||||
|
||||
script_args = self.init_script_args(img2imgreq, selectable_scripts, selectable_script_idx, script_runner)
|
||||
script_args = self.init_script_args(img2imgreq, self.default_script_arg_img2img, selectable_scripts, selectable_script_idx, script_runner)
|
||||
|
||||
send_images = args.pop('send_images', True)
|
||||
args.pop('save_images', None)
|
||||
@@ -347,7 +358,7 @@ class Api:
|
||||
p.outpath_samples = opts.outdir_img2img_samples
|
||||
|
||||
shared.state.begin()
|
||||
if selectable_scripts != None:
|
||||
if selectable_scripts is not None:
|
||||
p.script_args = script_args
|
||||
processed = scripts.scripts_img2img.run(p, *p.script_args) # Need to pass args as list here
|
||||
else:
|
||||
@@ -390,7 +401,7 @@ class Api:
|
||||
return ExtrasBatchImagesResponse(images=list(map(encode_pil_to_base64, result[0])), html_info=result[1])
|
||||
|
||||
def pnginfoapi(self, req: PNGInfoRequest):
|
||||
if(not req.image.strip()):
|
||||
if not req.image.strip():
|
||||
return PNGInfoResponse(info="")
|
||||
|
||||
image = decode_base64_to_image(req.image.strip())
|
||||
@@ -420,7 +431,7 @@ class Api:
|
||||
progress += 1 / shared.state.job_count * shared.state.sampling_step / shared.state.sampling_steps
|
||||
|
||||
time_since_start = time.time() - shared.state.time_start
|
||||
eta = (time_since_start/progress)
|
||||
eta = time_since_start / progress
|
||||
eta_relative = eta-time_since_start
|
||||
|
||||
progress = min(progress, 1)
|
||||
@@ -474,7 +485,7 @@ class Api:
|
||||
options = {}
|
||||
for key in shared.opts.data.keys():
|
||||
metadata = shared.opts.data_labels.get(key)
|
||||
if(metadata is not None):
|
||||
if metadata is not None:
|
||||
options.update({key: shared.opts.data.get(key, shared.opts.data_labels.get(key).default)})
|
||||
else:
|
||||
options.update({key: shared.opts.data.get(key, None)})
|
||||
@@ -563,7 +574,7 @@ class Api:
|
||||
def create_hypernetwork(self, args: dict):
|
||||
try:
|
||||
shared.state.begin()
|
||||
filename = create_hypernetwork(**args) # create empty embedding
|
||||
filename = create_hypernetwork(**args) # create empty embedding # pylint: disable=E1111
|
||||
shared.state.end()
|
||||
return CreateResponse(info = "create hypernetwork filename: {filename}".format(filename = filename))
|
||||
except AssertionError as e:
|
||||
@@ -589,7 +600,7 @@ class Api:
|
||||
def train_embedding(self, args: dict):
|
||||
try:
|
||||
shared.state.begin()
|
||||
apply_optimizations = shared.opts.training_xattention_optimizations
|
||||
apply_optimizations = False
|
||||
error = None
|
||||
filename = ''
|
||||
if not apply_optimizations:
|
||||
@@ -611,7 +622,7 @@ class Api:
|
||||
try:
|
||||
shared.state.begin()
|
||||
shared.loaded_hypernetworks = []
|
||||
apply_optimizations = shared.opts.training_xattention_optimizations
|
||||
apply_optimizations = False
|
||||
error = None
|
||||
filename = ''
|
||||
if not apply_optimizations:
|
||||
@@ -631,6 +642,16 @@ class Api:
|
||||
shared.state.end()
|
||||
return TrainResponse(info="train embedding error: {error}".format(error=error))
|
||||
|
||||
def shutdown(self):
|
||||
print('shutdown request received')
|
||||
# from modules.shared import demo
|
||||
# demo.close()
|
||||
# time.sleep(0.5)
|
||||
# import sys
|
||||
# sys.exit(0)
|
||||
import os
|
||||
os._exit(0)
|
||||
|
||||
def get_memory(self):
|
||||
try:
|
||||
import os, psutil
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import html
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
import time
|
||||
import cProfile
|
||||
import pstats
|
||||
import io
|
||||
|
||||
from modules import shared, progress
|
||||
from modules import shared, progress, errors
|
||||
|
||||
queue_lock = threading.Lock()
|
||||
|
||||
@@ -44,7 +45,6 @@ def wrap_gradio_gpu_call(func, extra_outputs=None):
|
||||
|
||||
return wrap_gradio_call(f, extra_outputs=extra_outputs, add_stats=True)
|
||||
|
||||
|
||||
def wrap_gradio_call(func, extra_outputs=None, add_stats=False):
|
||||
def f(*args, extra_outputs_array=extra_outputs, **kwargs):
|
||||
run_memmon = shared.opts.memmon_poll_rate > 0 and not shared.mem_mon.disabled and add_stats
|
||||
@@ -53,25 +53,24 @@ def wrap_gradio_call(func, extra_outputs=None, add_stats=False):
|
||||
t = time.perf_counter()
|
||||
|
||||
try:
|
||||
if shared.cmd_opts.profile:
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
res = list(func(*args, **kwargs))
|
||||
if shared.cmd_opts.profile:
|
||||
pr.disable()
|
||||
s = io.StringIO()
|
||||
ps = pstats.Stats(pr, stream=s)
|
||||
ps.sort_stats(pstats.SortKey.CUMULATIVE)
|
||||
# ps.strip_dirs()
|
||||
ps.print_stats(15)
|
||||
print('Profile:', s.getvalue())
|
||||
except Exception as e:
|
||||
# When printing out our debug argument list, do not print out more than a MB of text
|
||||
max_debug_str_len = 131072 # (1024*1024)/8
|
||||
|
||||
print("Error completing request", file=sys.stderr)
|
||||
argStr = f"Arguments: {str(args)} {str(kwargs)}"
|
||||
print(argStr[:max_debug_str_len], file=sys.stderr)
|
||||
if len(argStr) > max_debug_str_len:
|
||||
print(f"(Argument list truncated at {max_debug_str_len}/{len(argStr)} characters)", file=sys.stderr)
|
||||
|
||||
print(traceback.format_exc(), file=sys.stderr)
|
||||
|
||||
errors.display(e, 'gradio call')
|
||||
shared.state.job = ""
|
||||
shared.state.job_count = 0
|
||||
|
||||
if extra_outputs_array is None:
|
||||
extra_outputs_array = [None, '']
|
||||
|
||||
res = extra_outputs_array + [f"<div class='error'>{html.escape(type(e).__name__+': '+str(e))}</div>"]
|
||||
|
||||
shared.state.skipped = False
|
||||
@@ -106,4 +105,3 @@ def wrap_gradio_call(func, extra_outputs=None, add_stats=False):
|
||||
return tuple(res)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
@@ -1,102 +1,70 @@
|
||||
import argparse
|
||||
import os
|
||||
from modules.paths_internal import models_path, script_path, data_path, extensions_dir, extensions_builtin_dir, sd_default_config, sd_model_file
|
||||
from modules.paths_internal import data_path, sd_default_config, sd_model_file
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser = argparse.ArgumentParser(description="Stable Diffusion", formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=55,indent_increment=2,width=200))
|
||||
|
||||
parser.add_argument("--update-all-extensions", action='store_true', help="launch.py argument: download updates for all extensions when starting the program")
|
||||
parser.add_argument("--skip-python-version-check", action='store_true', help="launch.py argument: do not check python version")
|
||||
parser.add_argument("--skip-torch-cuda-test", action='store_true', help="launch.py argument: do not check if CUDA is able to work properly")
|
||||
parser.add_argument("--reinstall-xformers", action='store_true', help="launch.py argument: install the appropriate version of xformers even if you have some version already installed")
|
||||
parser.add_argument("--reinstall-torch", action='store_true', help="launch.py argument: install the appropriate version of torch even if you have some version already installed")
|
||||
parser.add_argument("--update-check", action='store_true', help="launch.py argument: chck for updates at startup")
|
||||
parser.add_argument("--tests", type=str, default=None, help="launch.py argument: run tests in the specified directory")
|
||||
parser.add_argument("--no-tests", action='store_true', help="launch.py argument: do not run tests even if --tests option is specified")
|
||||
parser.add_argument("--skip-install", action='store_true', help="launch.py argument: skip installation of packages")
|
||||
parser.add_argument("--data-dir", type=str, default=os.path.dirname(os.path.dirname(os.path.realpath(__file__))), help="base path where all user data is stored")
|
||||
parser.add_argument("--config", type=str, default=sd_default_config, help="path to config which constructs model",)
|
||||
parser.add_argument("--ckpt", type=str, default=sd_model_file, help="path to checkpoint of stable diffusion model; if specified, this checkpoint will be added to the list of checkpoints and loaded",)
|
||||
parser.add_argument("--ckpt-dir", type=str, default=None, help="Path to directory with stable diffusion checkpoints")
|
||||
parser.add_argument("--vae-dir", type=str, default=None, help="Path to directory with VAE files")
|
||||
parser.add_argument("--gfpgan-dir", type=str, help="GFPGAN directory", default=('./src/gfpgan' if os.path.exists('./src/gfpgan') else './GFPGAN'))
|
||||
parser.add_argument("--gfpgan-model", type=str, help="GFPGAN model file name", default=None)
|
||||
parser.add_argument("--no-half", action='store_true', help="do not switch the model to 16-bit floats")
|
||||
parser.add_argument("--no-half-vae", action='store_true', help="do not switch the VAE model to 16-bit floats")
|
||||
parser.add_argument("--no-progressbar-hiding", action='store_true', help="do not hide progressbar in gradio UI (we hide it because it slows down ML if you have hardware acceleration in browser)")
|
||||
parser.add_argument("--max-batch-count", type=int, default=16, help="maximum batch count value for the UI")
|
||||
parser.add_argument("--embeddings-dir", type=str, default=os.path.join(data_path, 'embeddings'), help="embeddings directory for textual inversion (default: embeddings)")
|
||||
parser.add_argument("--textual-inversion-templates-dir", type=str, default=os.path.join(script_path, 'textual_inversion_templates'), help="directory with textual inversion templates")
|
||||
parser.add_argument("--hypernetwork-dir", type=str, default=os.path.join(models_path, 'hypernetworks'), help="hypernetwork directory")
|
||||
parser.add_argument("--localizations-dir", type=str, default=os.path.join(script_path, 'localizations'), help="localizations directory")
|
||||
parser.add_argument("--allow-code", action='store_true', help="allow custom script execution from webui")
|
||||
parser.add_argument("--medvram", action='store_true', help="enable stable diffusion model optimizations for sacrificing a little speed for low VRM usage")
|
||||
parser.add_argument("--lowvram", action='store_true', help="enable stable diffusion model optimizations for sacrificing a lot of speed for very low VRM usage")
|
||||
parser.add_argument("--lowram", action='store_true', help="load stable diffusion checkpoint weights to VRAM instead of RAM")
|
||||
parser.add_argument("--always-batch-cond-uncond", action='store_true', help="disables cond/uncond batching that is enabled to save memory with --medvram or --lowvram")
|
||||
parser.add_argument("--unload-gfpgan", action='store_true', help="does not do anything.")
|
||||
parser.add_argument("--precision", type=str, help="evaluate at this precision", choices=["full", "autocast"], default="autocast")
|
||||
parser.add_argument("--upcast-sampling", action='store_true', help="upcast sampling. No effect with --no-half. Usually produces similar results to --no-half with better performance while using less memory.")
|
||||
parser.add_argument("--share", action='store_true', help="use share=True for gradio and make the UI accessible through their site")
|
||||
parser.add_argument("--ngrok", type=str, help="ngrok authtoken, alternative to gradio --share", default=None)
|
||||
parser.add_argument("--ngrok-region", type=str, help="The region in which ngrok should start.", default="us")
|
||||
parser.add_argument("--enable-insecure-extension-access", action='store_true', help="enable extensions tab regardless of other options")
|
||||
parser.add_argument("--codeformer-models-path", type=str, help="Path to directory with codeformer model file(s).", default=os.path.join(models_path, 'Codeformer'))
|
||||
parser.add_argument("--gfpgan-models-path", type=str, help="Path to directory with GFPGAN model file(s).", default=os.path.join(models_path, 'GFPGAN'))
|
||||
parser.add_argument("--esrgan-models-path", type=str, help="Path to directory with ESRGAN model file(s).", default=os.path.join(models_path, 'ESRGAN'))
|
||||
parser.add_argument("--bsrgan-models-path", type=str, help="Path to directory with BSRGAN model file(s).", default=os.path.join(models_path, 'BSRGAN'))
|
||||
parser.add_argument("--realesrgan-models-path", type=str, help="Path to directory with RealESRGAN model file(s).", default=os.path.join(models_path, 'RealESRGAN'))
|
||||
parser.add_argument("--clip-models-path", type=str, help="Path to directory with CLIP model file(s).", default=None)
|
||||
parser.add_argument("--xformers", action='store_true', help="enable xformers for cross attention layers")
|
||||
parser.add_argument("--force-enable-xformers", action='store_true', help="enable xformers for cross attention layers regardless of whether the checking code thinks you can run it; do not make bug reports if this fails to work")
|
||||
parser.add_argument("--xformers-flash-attention", action='store_true', help="enable xformers with Flash Attention to improve reproducibility (supported for SD2.x or variant only)")
|
||||
parser.add_argument("--deepdanbooru", action='store_true', help="does not do anything")
|
||||
parser.add_argument("--opt-split-attention", action='store_true', help="force-enables Doggettx's cross-attention layer optimization. By default, it's on for torch cuda.")
|
||||
parser.add_argument("--opt-sub-quad-attention", action='store_true', help="enable memory efficient sub-quadratic cross-attention layer optimization")
|
||||
parser.add_argument("--sub-quad-q-chunk-size", type=int, help="query chunk size for the sub-quadratic cross-attention layer optimization to use", default=1024)
|
||||
parser.add_argument("--sub-quad-kv-chunk-size", type=int, help="kv chunk size for the sub-quadratic cross-attention layer optimization to use", default=None)
|
||||
parser.add_argument("--sub-quad-chunk-threshold", type=int, help="the percentage of VRAM threshold for the sub-quadratic cross-attention layer optimization to use chunking", default=None)
|
||||
parser.add_argument("--opt-split-attention-invokeai", action='store_true', help="force-enables InvokeAI's cross-attention layer optimization. By default, it's on when cuda is unavailable.")
|
||||
parser.add_argument("--opt-split-attention-v1", action='store_true', help="enable older version of split attention optimization that does not consume all the VRAM it can find")
|
||||
parser.add_argument("--opt-sdp-attention", action='store_true', help="enable scaled dot product cross-attention layer optimization; requires PyTorch 2.*")
|
||||
parser.add_argument("--opt-sdp-no-mem-attention", action='store_true', help="enable scaled dot product cross-attention layer optimization without memory efficient attention, makes image generation deterministic; requires PyTorch 2.*")
|
||||
parser.add_argument("--disable-opt-split-attention", action='store_true', help="force-disables cross-attention layer optimization")
|
||||
parser.add_argument("--disable-nan-check", action='store_true', help="do not check if produced images/latent spaces have nans; useful for running without a checkpoint in CI")
|
||||
parser.add_argument("--use-cpu", nargs='+', help="use CPU as torch device for specified modules", default=[], type=str.lower)
|
||||
parser.add_argument("--listen", action='store_true', help="launch gradio with 0.0.0.0 as server name, allowing to respond to network requests")
|
||||
parser.add_argument("--port", type=int, help="launch gradio with given server port, you need root/admin rights for ports < 1024, defaults to 7860 if available", default=None)
|
||||
parser.add_argument("--show-negative-prompt", action='store_true', help="does not do anything", default=False)
|
||||
parser.add_argument("--ui-config-file", type=str, help="filename to use for ui configuration", default=os.path.join(data_path, 'ui-config.json'))
|
||||
parser.add_argument("--hide-ui-dir-config", action='store_true', help="hide directory configuration from webui", default=False)
|
||||
parser.add_argument("--freeze-settings", action='store_true', help="disable editing settings", default=False)
|
||||
parser.add_argument("--ui-settings-file", type=str, help="filename to use for ui settings", default=os.path.join(data_path, 'config.json'))
|
||||
parser.add_argument("--gradio-debug", action='store_true', help="launch gradio with --debug option")
|
||||
parser.add_argument("--gradio-auth", type=str, help='set gradio authentication like "username:password"; or comma-delimit multiple like "u1:p1,u2:p2,u3:p3"', default=None)
|
||||
parser.add_argument("--gradio-auth-path", type=str, help='set gradio authentication file path ex. "/path/to/auth/file" same auth format as --gradio-auth', default=None)
|
||||
parser.add_argument("--gradio-img2img-tool", type=str, help='does not do anything')
|
||||
parser.add_argument("--gradio-inpaint-tool", type=str, help="does not do anything")
|
||||
parser.add_argument("--opt-channelslast", action='store_true', help="change memory type for stable diffusion to channels last")
|
||||
parser.add_argument("--styles-file", type=str, help="filename to use for styles", default=os.path.join(data_path, 'styles.csv'))
|
||||
parser.add_argument("--autolaunch", action='store_true', help="open the webui URL in the system's default browser upon launch", default=False)
|
||||
parser.add_argument("--theme", type=str, help="launches the UI with light or dark theme", default=None)
|
||||
parser.add_argument("--use-textbox-seed", action='store_true', help="use textbox for seeds in UI (no up/down, but possible to input long seeds)", default=False)
|
||||
parser.add_argument("--disable-console-progressbars", action='store_true', help="do not output progressbars to console", default=False)
|
||||
parser.add_argument("--enable-console-prompts", action='store_true', help="print prompts to console when generating with txt2img and img2img", default=False)
|
||||
parser.add_argument('--vae-path', type=str, help='Checkpoint to use as VAE; setting this argument disables all settings related to VAE', default=None)
|
||||
parser.add_argument("--disable-safe-unpickle", action='store_true', help="disable checking pytorch models for malicious code", default=False)
|
||||
parser.add_argument("--api", action='store_true', help="use api=True to launch the API together with the webui (use --nowebui instead for only the API)")
|
||||
parser.add_argument("--api-auth", type=str, help='Set authentication for API like "username:password"; or comma-delimit multiple like "u1:p1,u2:p2,u3:p3"', default=None)
|
||||
parser.add_argument("--api-log", action='store_true', help="use api-log=True to enable logging of all API requests")
|
||||
parser.add_argument("--nowebui", action='store_true', help="use api=True to launch the API instead of the webui")
|
||||
parser.add_argument("--ui-debug-mode", action='store_true', help="Don't load model to quickly launch UI")
|
||||
parser.add_argument("--device-id", type=str, help="Select the default CUDA device to use (export CUDA_VISIBLE_DEVICES=0,1,etc might be needed before)", default=None)
|
||||
parser.add_argument("--administrator", action='store_true', help="Administrator rights", default=False)
|
||||
parser.add_argument("--cors-allow-origins", type=str, help="Allowed CORS origin(s) in the form of a comma-separated list (no spaces)", default=None)
|
||||
parser.add_argument("--cors-allow-origins-regex", type=str, help="Allowed CORS origin(s) in the form of a single regular expression", default=None)
|
||||
parser.add_argument("-f", action='store_true', help=argparse.SUPPRESS) # allows running as root; implemented outside of webui
|
||||
parser.add_argument("--ui-settings-file", type=str, help=argparse.SUPPRESS, default=os.path.join(data_path, 'config.json'))
|
||||
parser.add_argument("--ui-config-file", type=str, help=argparse.SUPPRESS, default=os.path.join(data_path, 'ui-config.json'))
|
||||
parser.add_argument("--config", type=str, default=sd_default_config, help=argparse.SUPPRESS)
|
||||
parser.add_argument("--theme", type=str, help=argparse.SUPPRESS, default='dark')
|
||||
|
||||
parser.add_argument("--no-half", action='store_true', help="Do not switch the model to 16-bit floats")
|
||||
parser.add_argument("--no-half-vae", action='store_true', help="Do not switch the VAE model to 16-bit floats")
|
||||
parser.add_argument("--precision", type=str, help="Evaluate at this precision", choices=["full", "autocast"], default="autocast")
|
||||
|
||||
parser.add_argument("--medvram", action='store_true', help="Enable model optimizations for sacrificing a little speed for low memory usage")
|
||||
parser.add_argument("--lowvram", action='store_true', help="Enable model optimizations for sacrificing a lot of speed for lowest memory usage")
|
||||
parser.add_argument("--lowram", action='store_true', help="Load checkpoint weights to VRAM instead of RAM")
|
||||
|
||||
parser.add_argument("--ckpt", type=str, default=sd_model_file, help="Path to checkpoint of stable diffusion model to load immediately",)
|
||||
parser.add_argument('--vae', type=str, help='Path to checkpoint of stable diffusion VAE model to load immediately', default=None)
|
||||
parser.add_argument("--data-dir", type=str, default=os.path.dirname(os.path.dirname(os.path.realpath(__file__))), help="Base path where all user data is stored")
|
||||
|
||||
parser.add_argument("--allow-code", action='store_true', help="Allow custom script execution")
|
||||
parser.add_argument("--share", action='store_true', help="Enable to make the UI accessible through Gradio site")
|
||||
parser.add_argument("--enable-insecure", action='store_true', help="Enable extensions tab regardless of other options")
|
||||
parser.add_argument("--use-cpu", nargs='+', help="Force use CPU for specified modules", default=[], type=str.lower)
|
||||
parser.add_argument("--listen", action='store_true', help="Launch web server using public IP address")
|
||||
parser.add_argument("--port", type=int, help="Launch web server with given server port", default=None)
|
||||
parser.add_argument("--hide-ui-dir-config", action='store_true', help="Hide directory configuration from UI", default=False)
|
||||
parser.add_argument("--freeze-settings", action='store_true', help="Disable editing settings", default=False)
|
||||
parser.add_argument("--gradio-auth", type=str, help='Set Gradio authentication like "username:password,username:password""', default=None)
|
||||
parser.add_argument("--gradio-auth-path", type=str, help='Set Gradio authentication using file', default=None)
|
||||
parser.add_argument("--autolaunch", action='store_true', help="Open the UI URL in the system's default browser upon launch", default=False)
|
||||
parser.add_argument("--disable-console-progressbars", action='store_true', help="Do not output progressbars to console", default=True)
|
||||
parser.add_argument("--disable-safe-unpickle", action='store_true', help="Disable checking models for malicious code", default=True)
|
||||
parser.add_argument("--api-auth", type=str, help='Set API authentication', default=None)
|
||||
parser.add_argument("--api-log", action='store_true', help="Enable logging of all API requests")
|
||||
parser.add_argument("--device-id", type=str, help="Select the default CUDA device to use", default=None)
|
||||
parser.add_argument("--cors-origins", type=str, help="Allowed CORS origin(s) in the form of a comma-separated list", default=None)
|
||||
parser.add_argument("--cors-regex", type=str, help="Allowed CORS origin(s) in the form of a single regular expression", default=None)
|
||||
parser.add_argument("--tls-keyfile", type=str, help="Partially enables TLS, requires --tls-certfile to fully function", default=None)
|
||||
parser.add_argument("--tls-certfile", type=str, help="Partially enables TLS, requires --tls-keyfile to fully function", default=None)
|
||||
parser.add_argument("--server-name", type=str, help="Sets hostname of server", default=None)
|
||||
parser.add_argument("--gradio-queue", action='store_true', help="does not do anything", default=True)
|
||||
parser.add_argument("--no-gradio-queue", action='store_true', help="Disables gradio queue; causes the webpage to use http requests instead of websockets; was the defaul in earlier versions")
|
||||
parser.add_argument("--skip-version-check", action='store_true', help="Do not check versions of torch and xformers")
|
||||
parser.add_argument("--no-hashing", action='store_true', help="disable sha256 hashing of checkpoints to help loading performance", default=False)
|
||||
parser.add_argument("--no-download-sd-model", action='store_true', help="don't download SD1.5 model even if no model is found in --ckpt-dir", default=False)
|
||||
parser.add_argument("--no-hashing", action='store_true', help="Disable sha256 hashing of checkpoints", default=False)
|
||||
parser.add_argument("--no-download-sd-model", action='store_true', help="Disable download of default model even if no model is found", default=False)
|
||||
parser.add_argument("--profile", action='store_true', help="Run profiler, default: %(default)s")
|
||||
|
||||
|
||||
def compatibility_args(opts, args):
|
||||
parser.add_argument("--ckpt-dir", type=str, help=argparse.SUPPRESS, default=opts.ckpt_dir)
|
||||
parser.add_argument("--vae-dir", type=str, help=argparse.SUPPRESS, default=opts.vae_dir)
|
||||
parser.add_argument("--embeddings-dir", type=str, help=argparse.SUPPRESS, default=opts.embeddings_dir)
|
||||
parser.add_argument("--embeddings-templates-dir", type=str, help=argparse.SUPPRESS, default=opts.embeddings_templates_dir)
|
||||
parser.add_argument("--hypernetwork-dir", type=str, help=argparse.SUPPRESS, default=opts.hypernetwork_dir)
|
||||
parser.add_argument("--codeformer-models-path", type=str, help=argparse.SUPPRESS, default=opts.codeformer_models_path)
|
||||
parser.add_argument("--gfpgan-models-path", type=str, help=argparse.SUPPRESS, default=opts.gfpgan_models_path)
|
||||
parser.add_argument("--esrgan-models-path", type=str, help=argparse.SUPPRESS, default=opts.esrgan_models_path)
|
||||
parser.add_argument("--bsrgan-models-path", type=str, help=argparse.SUPPRESS, default=opts.bsrgan_models_path)
|
||||
parser.add_argument("--realesrgan-models-path", type=str, help=argparse.SUPPRESS, default=opts.realesrgan_models_path)
|
||||
parser.add_argument("--clip-models-path", type=str, help=argparse.SUPPRESS, default=opts.clip_models_path)
|
||||
parser.add_argument("--disable-nan-check", default = True, action='store_true', help=argparse.SUPPRESS)
|
||||
parser.add_argument("--disable-extension-access", default = False, action='store_true', help=argparse.SUPPRESS)
|
||||
args = parser.parse_args()
|
||||
if vars(parser)['_option_string_actions'].get('--lora-dir', None) is not None:
|
||||
args.lora_dir = opts.lora_dir
|
||||
if vars(parser)['_option_string_actions'].get('--lyco-dir', None) is not None:
|
||||
args.lyco_dir = opts.lyco_dir
|
||||
return args
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import cv2
|
||||
import torch
|
||||
|
||||
import modules.face_restoration
|
||||
import modules.shared
|
||||
from modules import shared, devices, modelloader
|
||||
from modules import shared, devices, modelloader, errors
|
||||
from modules.paths import models_path
|
||||
|
||||
# codeformer people made a choice to include modified basicsr library to their project which makes
|
||||
@@ -33,7 +31,6 @@ def setup_model(dirname):
|
||||
try:
|
||||
from torchvision.transforms.functional import normalize
|
||||
from modules.codeformer.codeformer_arch import CodeFormer
|
||||
from basicsr.utils.download_util import load_file_from_url
|
||||
from basicsr.utils import imwrite, img2tensor, tensor2img
|
||||
from facelib.utils.face_restoration_helper import FaceRestoreHelper
|
||||
from facelib.detection.retinaface import retinaface
|
||||
@@ -136,8 +133,7 @@ def setup_model(dirname):
|
||||
codeformer = FaceRestorerCodeFormer(dirname)
|
||||
shared.face_restorers.append(codeformer)
|
||||
|
||||
except Exception:
|
||||
print("Error setting up CodeFormer:", file=sys.stderr)
|
||||
print(traceback.format_exc(), file=sys.stderr)
|
||||
except Exception as e:
|
||||
errors.display(e, 'codeformer')
|
||||
|
||||
# sys.path = stored_sys_path
|
||||
|
||||
@@ -2,7 +2,6 @@ import os
|
||||
import re
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
from modules import modelloader, paths, deepbooru_model, devices, images, shared
|
||||
|
||||
@@ -675,4 +675,3 @@ class DeepDanbooruModel(nn.Module):
|
||||
self.tags = state_dict.get('tags', [])
|
||||
|
||||
super(DeepDanbooruModel, self).load_state_dict({k: v for k, v in state_dict.items() if k != 'tags'})
|
||||
|
||||
|
||||