fix for vlad fork
@@ -0,0 +1,11 @@
|
||||
github: [vladmandic]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -1,100 +1,46 @@
|
||||
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
|
||||
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)
|
||||
- type: markdown
|
||||
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
|
||||
If issue is setup, installation or startup related, please check `setup.log` before reporting
|
||||
And when posting console logs, please use code blocks ( \`\`\` ) to format them insead of uploading screenshots
|
||||
- type: markdown
|
||||
attributes:
|
||||
label: What should have happened?
|
||||
description: Tell what you think the normal behavior should be
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: commit
|
||||
value: |
|
||||
Verify that issue persists after restarting server from command line
|
||||
Restarting from UI or reloading browser is insufficient to create reproducible environment
|
||||
- type: markdown
|
||||
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
|
||||
value: |
|
||||
If you have additional extensions installed, try to reproduce the issue with user extensions disabled
|
||||
And if the issue is with compatibility with specific extension, mark it as such when creating the issue
|
||||
- type: markdown
|
||||
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.
|
||||
value: |
|
||||
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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: WebUI Community Support
|
||||
url: https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions
|
||||
url: https://github.com/vladmandic/automatic/discussions
|
||||
about: Please ask and answer questions here.
|
||||
|
||||
@@ -1,40 +1,18 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature Request]: "
|
||||
title: "[Feature]: "
|
||||
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,43 @@
|
||||
# defaults
|
||||
__pycache__
|
||||
/params.txt
|
||||
/cache.json
|
||||
/config.json
|
||||
/ui-config.json
|
||||
/setup.log
|
||||
/styles.csv
|
||||
/user.css
|
||||
/webui-user.bat
|
||||
/javascript/themes.json
|
||||
venv
|
||||
|
||||
# all models and temp files
|
||||
*.bak
|
||||
*.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
|
||||
!/models/VAE-approx
|
||||
!/models/VAE-approx/model.pt
|
||||
|
||||
@@ -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,209 @@
|
||||
# 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=^repositories/.*$,
|
||||
^extensions/.*$,
|
||||
/usr/lib/.*$,
|
||||
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,
|
||||
unnecessary-dunder-call,
|
||||
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,14 @@
|
||||
{
|
||||
"python.analysis.extraPaths": [
|
||||
".",
|
||||
"./modules",
|
||||
"./modules/lora",
|
||||
"./repositories/BLIP",
|
||||
"./repositories/CodeFormer",
|
||||
"./repositories/k-diffusion",
|
||||
"./repositories/taming-transformers",
|
||||
"./repositories/stable-diffusion-stability-ai",
|
||||
"./repositories/stable-diffusion-stability-ai/ldm"
|
||||
],
|
||||
"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,161 +1,165 @@
|
||||
# Stable Diffusion web UI
|
||||
A browser interface based on Gradio library for Stable Diffusion.
|
||||
[](https://github.com/sponsors/vladmandic)
|
||||

|
||||

|
||||

|
||||
|
||||

|
||||
# Stable Diffusion - Automatic
|
||||
|
||||
## 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
|
||||
*Heavily opinionated custom fork of* <https://github.com/AUTOMATIC1111/stable-diffusion-webui>
|
||||
|
||||
## 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.
|
||||
Fork is as close as up-to-date with origin as time allows
|
||||
All code changes are merged upstream whenever possible
|
||||
|
||||
Alternatively, use online services (like Google Colab):
|
||||
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
|
||||
|
||||
- [List of Online Services](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Online-Services)
|
||||
<br>
|
||||
|
||||
### 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.
|
||||
### Follow [Development updates](https://github.com/vladmandic/automatic/discussions/99) for daily updates on new features/fixes
|
||||
|
||||
### 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
|
||||
<br>
|
||||
|
||||
Find the instructions [here](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Installation-on-Apple-Silicon).
|
||||

|
||||
|
||||
## Contributing
|
||||
Here's how to add code to this repo: [Contributing](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Contributing)
|
||||
<br>
|
||||
|
||||
## Documentation
|
||||
The documentation was moved from this README over to the project's [wiki](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki).
|
||||
## Notes
|
||||
|
||||
## Credits
|
||||
Licenses for borrowed code can be found in `Settings -> Licenses` screen, and also in `html/licenses.html` file.
|
||||
### Fork does differ in few things
|
||||
|
||||
- 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)
|
||||
- New installer
|
||||
- Advanced CUDA tuning
|
||||
Available in UI Settings
|
||||
- Advanced 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.
|
||||
- New logger
|
||||
- New error and exception handlers
|
||||
|
||||
### Optimizations
|
||||
|
||||
- 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
|
||||
|
||||
### 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
|
||||
- Includes support for **Gradio themes**
|
||||
*Settings* -> *User interface* -> *UI theme*
|
||||
Link to themes list & previews: <https://huggingface.co/spaces/gradio/theme-gallery>
|
||||
|
||||
### 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
|
||||
- **Quick mode**: Automatically enabled if timestamp of last sucessful setup is newer than actual repository version or version of newest extension
|
||||
|
||||
<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,68 @@
|
||||
# TODO
|
||||
|
||||
## Issues
|
||||
|
||||
Stuff to be fixed...
|
||||
|
||||
- ClipSkip not updated on read gen info
|
||||
- Usage of `sd_vae` in quick settings
|
||||
- Run VAE with hires at 1280
|
||||
- Make TensorFlow optional
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
Stuff to be added...
|
||||
|
||||
- Add Gradio theme maker
|
||||
- Create new GitHub hooks/actions for CI/CD
|
||||
- Move Restart Server from WebUI to Launch and reload modules
|
||||
- Redo Extensions tab: see <https://vladmandic.github.io/sd-extension-manager/pages/extensions.html>
|
||||
- Stream-load models as option for slow storage
|
||||
- Autodetect nVidia and AMD: `nvidia-smi` vs `rocm-smi`
|
||||
|
||||
## Investigate
|
||||
|
||||
Stuff to be investigated...
|
||||
|
||||
- Torch Compile
|
||||
- `Torch-DirectML`
|
||||
- `TensorRT`
|
||||
|
||||
## Merge PRs
|
||||
|
||||
Pick & merge PRs from main repo...
|
||||
|
||||
- Merge backlog: <https://github.com/vladmandic/automatic/pulls>
|
||||
|
||||
## Models
|
||||
|
||||
StabilityAI is working on new stuff...
|
||||
|
||||
- SD XL
|
||||
- SD ReImagined
|
||||
|
||||
## 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>
|
||||
|
||||
### Pending Code Updates
|
||||
|
||||
- fix VAE dtype
|
||||
should fix most issues with NaN or black images
|
||||
- add built-in Gradio themes
|
||||
- fix setup race conditions
|
||||
- reduce requirements
|
||||
- more AMD specific work
|
||||
- initial work on Apple platform support
|
||||
- additional PR merges
|
||||
|
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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import os
|
||||
from modules import paths
|
||||
|
||||
|
||||
def preload(parser):
|
||||
parser.add_argument("--ldsr-models-path", type=str, help="Path to directory with LDSR model file(s).", default=os.path.join(paths.models_path, 'LDSR'))
|
||||
@@ -8,7 +8,7 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork):
|
||||
def activate(self, p, params_list):
|
||||
additional = shared.opts.sd_lora
|
||||
|
||||
if additional != "" and additional in lora.available_loras and len([x for x in params_list if x.items[0] == additional]) == 0:
|
||||
if additional != "None" and additional in lora.available_loras and len([x for x in params_list if x.items[0] == additional]) == 0:
|
||||
p.all_prompts = [x + f"<lora:{additional}:{shared.opts.extra_networks_default_multiplier}>" for x in p.all_prompts]
|
||||
params_list.append(extra_networks.ExtraNetworkParams(items=[additional, shared.opts.extra_networks_default_multiplier]))
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import torch
|
||||
from typing import Union
|
||||
import torch
|
||||
|
||||
from modules import shared, devices, sd_models, errors
|
||||
|
||||
@@ -136,9 +136,10 @@ def load_lora(name, filename):
|
||||
is_sd2 = 'model_transformer_resblocks' in shared.sd_model.lora_layer_mapping
|
||||
|
||||
for key_diffusers, weight in sd.items():
|
||||
key_diffusers_without_lora_parts, lora_key = key_diffusers.split(".", 1)
|
||||
lora_key_parts = key_diffusers.split(".", 1)
|
||||
key_diffusers_without_lora_parts = lora_key_parts[0]
|
||||
lora_key = lora_key_parts[1] if len(lora_key_parts) > 1 else ""
|
||||
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:
|
||||
@@ -166,11 +167,10 @@ def load_lora(name, filename):
|
||||
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)
|
||||
module = torch.nn.Conv2d(weight.shape[1], weight.shape[0], (weight.shape[2], weight.shape[3]), 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)
|
||||
@@ -229,7 +229,24 @@ def lora_calc_updown(lora, module, target):
|
||||
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:
|
||||
permute, h, w = False, 1, 1
|
||||
if len(up.shape) == 4 and len(down.shape) == 4:
|
||||
if up.shape[2:] == (1, 1):
|
||||
up = up.squeeze(2).squeeze(2)
|
||||
else:
|
||||
n, c, h, w = up.shape
|
||||
up = up.view(n, c, -1).permute(2, 0, 1)
|
||||
permute = True
|
||||
if down.shape[2:] == (1, 1):
|
||||
down = down.squeeze(2).squeeze(2)
|
||||
else:
|
||||
n, c, h, w = down.shape
|
||||
down = down.view(n, c, -1).permute(2, 0, 1)
|
||||
permute = True
|
||||
updown = up @ down
|
||||
if permute:
|
||||
nh, nw = updown.shape[1:]
|
||||
updown = updown.permute(1, 2, 0).view(nh, nw, h, w)
|
||||
|
||||
updown = updown * lora.multiplier * (module.alpha / module.up.weight.shape[1] if module.alpha else 1.0)
|
||||
|
||||
@@ -301,10 +318,10 @@ def lora_reset_cached_weight(self: Union[torch.nn.Conv2d, torch.nn.Linear]):
|
||||
setattr(self, "lora_weights_backup", None)
|
||||
|
||||
|
||||
def lora_Linear_forward(self, input):
|
||||
def lora_Linear_forward(self, lora_input):
|
||||
lora_apply_weights(self)
|
||||
|
||||
return torch.nn.Linear_forward_before_lora(self, input)
|
||||
return torch.nn.Linear_forward_before_lora(self, lora_input)
|
||||
|
||||
|
||||
def lora_Linear_load_state_dict(self, *args, **kwargs):
|
||||
@@ -313,10 +330,10 @@ def lora_Linear_load_state_dict(self, *args, **kwargs):
|
||||
return torch.nn.Linear_load_state_dict_before_lora(self, *args, **kwargs)
|
||||
|
||||
|
||||
def lora_Conv2d_forward(self, input):
|
||||
def lora_Conv2d_forward(self, lora_input):
|
||||
lora_apply_weights(self)
|
||||
|
||||
return torch.nn.Conv2d_forward_before_lora(self, input)
|
||||
return torch.nn.Conv2d_forward_before_lora(self, lora_input)
|
||||
|
||||
|
||||
def lora_Conv2d_load_state_dict(self, *args, **kwargs):
|
||||
@@ -339,7 +356,6 @@ def lora_MultiheadAttention_load_state_dict(self, *args, **kwargs):
|
||||
|
||||
def list_available_loras():
|
||||
available_loras.clear()
|
||||
|
||||
os.makedirs(shared.cmd_opts.lora_dir, exist_ok=True)
|
||||
|
||||
candidates = \
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import os
|
||||
from modules import paths
|
||||
|
||||
|
||||
def preload(parser):
|
||||
parser.add_argument("--lora-dir", type=str, help="Path to directory with Lora networks.", default=os.path.join(paths.models_path, 'Lora'))
|
||||
@@ -52,5 +52,5 @@ 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),
|
||||
"sd_lora": shared.OptionInfo("None", "Add Lora to prompt", gr.Dropdown, lambda: {"choices": ["None"] + [x for x in lora.available_loras]}, refresh=lora.list_available_loras),
|
||||
}))
|
||||
|
||||
@@ -27,5 +27,4 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
|
||||
}
|
||||
|
||||
def allowed_directories_for_previews(self):
|
||||
return [shared.cmd_opts.lora_dir]
|
||||
|
||||
return [shared.opts.lora_dir]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
99
|
||||
@@ -1,6 +0,0 @@
|
||||
import os
|
||||
from modules import paths
|
||||
|
||||
|
||||
def preload(parser):
|
||||
parser.add_argument("--scunet-models-path", type=str, help="Path to directory with ScuNET model file(s).", default=os.path.join(paths.models_path, 'ScuNET'))
|
||||
@@ -1,6 +0,0 @@
|
||||
import os
|
||||
from modules import paths
|
||||
|
||||
|
||||
def preload(parser):
|
||||
parser.add_argument("--swinir-models-path", type=str, help="Path to directory with SwinIR model file(s).", default=os.path.join(paths.models_path, 'SwinIR'))
|
||||
@@ -1,11 +1,10 @@
|
||||
import contextlib
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
from basicsr.utils.download_util import load_file_from_url
|
||||
from tqdm import tqdm
|
||||
from rich import print, progress # pylint: disable=redefined-builtin
|
||||
|
||||
from modules import modelloader, devices, script_callbacks, shared
|
||||
from modules.shared import cmd_opts, opts, state
|
||||
@@ -59,17 +58,17 @@ class UpscalerSwinIR(Upscaler):
|
||||
return None
|
||||
if filename.endswith(".v2.pth"):
|
||||
model = net2(
|
||||
upscale=scale,
|
||||
in_chans=3,
|
||||
img_size=64,
|
||||
window_size=8,
|
||||
img_range=1.0,
|
||||
depths=[6, 6, 6, 6, 6, 6],
|
||||
embed_dim=180,
|
||||
num_heads=[6, 6, 6, 6, 6, 6],
|
||||
mlp_ratio=2,
|
||||
upsampler="nearest+conv",
|
||||
resi_connection="1conv",
|
||||
upscale=scale,
|
||||
in_chans=3,
|
||||
img_size=64,
|
||||
window_size=8,
|
||||
img_range=1.0,
|
||||
depths=[6, 6, 6, 6, 6, 6],
|
||||
embed_dim=180,
|
||||
num_heads=[6, 6, 6, 6, 6, 6],
|
||||
mlp_ratio=2,
|
||||
upsampler="nearest+conv",
|
||||
resi_connection="1conv",
|
||||
)
|
||||
params = None
|
||||
else:
|
||||
@@ -88,7 +87,8 @@ class UpscalerSwinIR(Upscaler):
|
||||
)
|
||||
params = "params_ema"
|
||||
|
||||
pretrained_model = torch.load(filename)
|
||||
with progress.open(filename, 'rb', description=f'Loading weights: [cyan]{filename}', auto_refresh=True) as f:
|
||||
pretrained_model = torch.load(filename)
|
||||
if params is not None:
|
||||
model.load_state_dict(pretrained_model[params], strict=True)
|
||||
else:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div>
|
||||
<a href="/docs">API</a>
|
||||
•
|
||||
<a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui">Github</a>
|
||||
<a href="https://github.com/vladmandic/automatic">Github</a>
|
||||
•
|
||||
<a href="https://gradio.app">Gradio</a>
|
||||
•
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
/* generic html tags */
|
||||
:root { --font: system-ui, "Segoe UI", "Roboto", "sans-serif"; }
|
||||
html { font-size: 16px; }
|
||||
body, button, input, select, textarea { font-family: var(--font); overflow-x: hidden; }
|
||||
button { font-size: 1.2rem; }
|
||||
img { background-color: black; }
|
||||
input[type=range] { height: 18px; appearance: none; margin-top: 0; min-width: 160px; background-color: black; width: 100%; background: transparent; }
|
||||
input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 18px; cursor: pointer; box-shadow: 2px 2px 3px #111111; background: #50555C; border-radius: 2px; border: 0px solid #222222; }
|
||||
input[type=range]::-moz-range-track { width: 100%; height: 18px; cursor: pointer; box-shadow: 2px 2px 3px #111111; background: #50555C; border-radius: 2px; border: 0px solid #222222; }
|
||||
input[type=range]::-webkit-slider-thumb { box-shadow: 2px 2px 3px #111111; border: 0px solid #000000; height: 18px; width: 40px; border-radius: 2px; background: var(--highlight-color); cursor: pointer; -webkit-appearance: none; margin-top: 0px; }
|
||||
input[type=range]::-moz-range-thumb { box-shadow: 2px 2px 3px #111111; border: 0px solid #000000; height: 18px; width: 40px; border-radius: 2px; background: var(--highlight-color); cursor: pointer; -webkit-appearance: none; margin-top: 0px; }
|
||||
::-webkit-scrollbar { width: 12px; }
|
||||
::-webkit-scrollbar-track { background: #333333; }
|
||||
::-webkit-scrollbar-thumb { background-color: var(--highlight-color); border-radius: 2px; border-width: 0; box-shadow: 2px 2px 3px #111111; }
|
||||
div.form { border-width: 0; box-shadow: none; background: transparent; overflow: visible; gap: 0.5em; margin-bottom: 6px; }
|
||||
|
||||
/* main gradio components by selector */
|
||||
div.gradio-container.dark > div.w-full.flex.flex-col.min-h-screen > div { background-color: black; }
|
||||
|
||||
/* gradio shadowroot */
|
||||
.gradio-container { font-family: var(--font); --left-column: 490px; --highlight-color: #CE6400; --inactive-color: #4E1400; }
|
||||
|
||||
/* gradio style classes */
|
||||
.border-2 { border-width: 0; }
|
||||
.border-b-2 { border-bottom-width: 2px; border-color: var(--highlight-color) !important; padding-bottom: 2px; margin-bottom: 8px; }
|
||||
.dark .bg-white { color: lightyellow; border-radius: 0; background-color: var(--inactive-color); }
|
||||
.dark .bg-gray-200, .dark .\!bg-gray-200 { background-color: transparent; }
|
||||
.dark .dark\:bg-gray-900 { background-color: black; }
|
||||
.dark .gr-box { border-radius: 0 !important; background-color: #111111 !important; box-shadow: 2px 2px 3px #111111; border-width: 0; padding: 4px; margin: 12px 0px 12px 0px }
|
||||
.dark .gr-button { border-radius: 0; font-weight: normal; box-shadow: 2px 2px 3px #111111; font-size: 0.8rem; min-width: 32px; min-height: 32px; padding: 3px; margin: 3px; }
|
||||
.dark .gr-check-radio { background-color: var(--inactive-color); border-width: 0; border-radius: 2px; box-shadow: 2px 2px 3px #111111; }
|
||||
.dark .gr-check-radio:checked { background-color: var(--highlight-color); }
|
||||
.dark .gr-compact { border-radius: 0; background-color: black; }
|
||||
.dark .gr-form { border-radius: 0; border-width: 0; }
|
||||
.dark .gr-input { background-color: #333333 !important; padding: 4px; margin: 4px; }
|
||||
.dark .gr-input-label { color: lightyellow; border-width: 0; background: transparent; padding: 2px !important; }
|
||||
.dark .gr-panel { border-radius: 0; background-color: black; }
|
||||
.dark { background-color: black; }
|
||||
.dark fieldset span.text-gray-500, .dark .gr-block.gr-box span.text-gray-500, .dark label.block span { padding: 0; margin-top: -4px; }
|
||||
.dark fieldset span.text-gray-500, .dark .gr-block.gr-box span.text-gray-500, .dark label.block span { border-radius: 0;}
|
||||
.eta-bar { display: none !important }
|
||||
.extra-networks .tab-nav .search { max-height: 2em; }
|
||||
.extra-network-cards .card .actions .name { font-weight: 400; font-size: 1.2rem; }
|
||||
.extra-network-cards .card { width: 18em; height: 12em; box-shadow: none; }
|
||||
.extra-network-cards .card:hover { transform: scale(1.5); transition: all 0.3s ease; z-index: 99; box-shadow: none; }
|
||||
svg.feather.feather-image, .feather .feather-image { display: none }
|
||||
.gap-2 { padding-top: 8px; }
|
||||
.gr-box > div > div > input.gr-text-input { right: 0; width: 4em; padding: 0; top: -12px; border: none; max-height: 20px; }
|
||||
.output-html { line-height: 1.2rem; overflow-x: hidden; }
|
||||
.output-html > div { margin-bottom: 8px; }
|
||||
.overflow-hidden .flex .flex-col .relative col .gap-4 { min-width: var(--left-column); max-width: var(--left-column); } /* this is a problematic one */
|
||||
.p-2 { padding: 0; }
|
||||
.px-4 { padding-lefT: 1rem; padding-right: 1rem; }
|
||||
.py-6 { padding-bottom: 0; }
|
||||
.rounded-lg { border-radius: 0; }
|
||||
.tabs { background-color: black; }
|
||||
.gradio-button.tool { border-radius: 0; height: 2em; }
|
||||
.block.token-counter span { background-color: #222 !important; box-shadow: 2px 2px 2px #111; border: none !important; border-radius: 0; font-size: 0.8rem; }
|
||||
.tab-nav { zoom: 130%; margin-bottom: 16px; border-bottom: 2px solid #CE6400 !important; padding-bottom: 2px; }
|
||||
.label-wrap { margin: 16px 0px 8px 0px; }
|
||||
.gradio-slider input[type="number"] { width: 4.5em; font-size: 0.8rem; }
|
||||
#tab_extensions table td, #tab_extensions table th { border: none; padding: 0.5em; }
|
||||
#tab_extensions table { width: 96vw }
|
||||
#tab_extensions table thead { background-color: var(--neutral-700); }
|
||||
|
||||
/* automatic style classes */
|
||||
.progressDiv { border-radius: 0 !important; position: fixed; top: 318px; right: 26px; max-width: 262px; height: 48px; z-index: 99; }
|
||||
.progressDiv .progress { border-radius: 0 !important; background: var(--highlight-color); line-height: 3rem; height: 48px; }
|
||||
.gallery-item { box-shadow: none !important; }
|
||||
.performance { color: #888; }
|
||||
.modalControls { background-color: #4E1400; }
|
||||
|
||||
/* gradio elements overrides */
|
||||
#div.gradio-container.dark { overflow-x: hidden; }
|
||||
#img2img_label_copy_to_img2img { font-weight: normal; }
|
||||
#txt2img_prompt, #txt2img_neg_prompt, #img2img_prompt, #img2img_neg_prompt { background-color: black; box-shadow: 4px 4px 4px 0px #333333 !important; }
|
||||
#txt2img_prompt > label > textarea, #txt2img_neg_prompt > label > textarea, #img2img_prompt > label > textarea, #img2img_neg_prompt > label > textarea { font-size: 1.2rem; }
|
||||
#img2img_settings { min-width: calc(2 * var(--left-column)); max-width: calc(2 * var(--left-column)); background-color: #111111; padding-top: 16px; }
|
||||
#interrogate, #deepbooru { margin: 16px 0px 16px 0px; max-width: 100px; max-height: 74px; font-weight: normal; font-size: 14px; }
|
||||
#lightboxModal { background-color: rgba(20, 20, 20, 0.8) }
|
||||
#quicksettings .gr-button-tool { font-size: 1.6rem; box-shadow: none; margin-left: -20px; margin-top: -2px; height: 2.4em; }
|
||||
#quicksettings > div, #quicksettings > fieldset { min-width: 26em; max-width: 26em; line-height: 2em; }
|
||||
#refresh_sd_model_checkpoint { height: 40px; margin-left: -14px; background: #333333; box-shadow: none; }
|
||||
#refresh_txt2img_styles, #refresh_img2img_styles, #open_folder_txt2img, #open_folder_img2img, #open_folder_extras, #footer, #style_pos_col, #style_neg_col, #roll_col, #save_zip_txt2img, #save_zip_img2img, #extras_upscaler_2, #extras_upscaler_2_visibility, #txt2img_res_switch_btn, #img2img_res_switch_btn, #txt2img_seed_resize_from_w, #txt2img_seed_resize_from_h, #txt2img_tiling { display: none; }
|
||||
#save-animation { border-radius: 0 !important; margin-bottom: 16px; background-color: #111111; }
|
||||
#script_list { padding: 4px; margin-top: 20px; margin-bottom: 20px; }
|
||||
#settings > div.flex-wrap { width: 15em; }
|
||||
#tab_extensions table { background-color: #222222; }
|
||||
#txt2img_actions_column, #img2img_actions_column { min-width: 260px !important; max-width: 260px !important; }
|
||||
#txt2img_cfg_scale { min-width: 200px; }
|
||||
#txt2img_checkboxes, #img2img_checkboxes { background-color: transparent; }
|
||||
#txt2img_checkboxes, #img2img_checkboxes { margin-bottom: 0.2em; }
|
||||
#txt2img_gallery, #img2img_gallery, #extras_gallery { background: black !important; padding: 0; margin: 0; object-fit: contain; box-shadow: none; min-height: 0; }
|
||||
#txt2img_generate, #img2img_generate { height: 36px; border: none; border-radius: 0; min-height: 36px; padding: 0; }
|
||||
#txt2img_interrupt, #img2img_interrupt, #txt2img_skip, #img2img_skip { height: 36px; min-width: 116px; max-width: 116px; border: none; border-radius: 0; background-color: var(--inactive-color); margin-top: 46px; display: block !important; padding: 0; }
|
||||
#txt2img_progress_row > div { min-width: var(--left-column); max-width: var(--left-column); }
|
||||
#txt2img_results, #img2img_results, #extras_results { background-color: black; padding: 0; }
|
||||
#txt2img_seed_row { padding: 0; margin-top: 8px; }
|
||||
#txt2img_settings { min-width: var(--left-column); max-width: var(--left-column); background-color: #111111; padding-top: 16px; }
|
||||
#txt2img_subseed_row { padding: 0; margin-top: 16px; }
|
||||
#txt2img_subseed_show { min-width: 74px; padding: 8px 0 0 0 }
|
||||
#txt2img_subseed_strength { margin-top: 0; }
|
||||
#txt2img_tools, #img2img_tools { margin-top: 54px; scale: 120%; margin-left: 26px; }
|
||||
#txtimg_hr_finalres { max-width: 200px; }
|
||||
#pnginfo_html2_info { margin-top: -18px; background-color: var(--input-background-fill); padding: var(--input-padding) }
|
||||
|
||||
/* custom elements overrides */
|
||||
#steps-animation, #controlnet { border-width: 0; }
|
||||
|
||||
/* based on gradio built-in dark theme */
|
||||
.dark {
|
||||
--body-background-fill: black;
|
||||
--body-text-color: var(--neutral-100);
|
||||
--color-accent-soft: var(--neutral-700);
|
||||
--background-fill-primary: #222222;
|
||||
--background-fill-secondary: none;
|
||||
--border-color-accent: black;
|
||||
--border-color-primary: black;
|
||||
--link-text-color-active: var(--secondary-500);
|
||||
--link-text-color: var(--secondary-500);
|
||||
--link-text-color-hover: var(--secondary-400);
|
||||
--link-text-color-visited: var(--secondary-600);
|
||||
--body-text-color-subdued: var(--neutral-400);
|
||||
--shadow-spread: 1px;
|
||||
--block-background-fill: None;
|
||||
--block-border-color: var(--border-color-primary);
|
||||
--block_border_width: None;
|
||||
--block-info-text-color: var(--body-text-color-subdued);
|
||||
--block-label-background-fill: var(--background-fill-secondary);
|
||||
--block-label-border-color: var(--border-color-primary);
|
||||
--block_label_border_width: None;
|
||||
--block-label-text-color: var(--neutral-200);
|
||||
--block_shadow: None;
|
||||
--block_title_background_fill: None;
|
||||
--block_title_border_color: None;
|
||||
--block_title_border_width: None;
|
||||
--block-title-text-color: white;
|
||||
--panel-background-fill: var(--background-fill-secondary);
|
||||
--panel-border-color: var(--border-color-primary);
|
||||
--panel_border_width: None;
|
||||
--checkbox-background-color: var(--neutral-800);
|
||||
--checkbox-background-color-focus: var(--checkbox-background-color);
|
||||
--checkbox-background-color-hover: var(--checkbox-background-color);
|
||||
--checkbox-background-color-selected: var(--secondary-600);
|
||||
--checkbox-border-color: var(--neutral-700);
|
||||
--checkbox-border-color-focus: var(--secondary-500);
|
||||
--checkbox-border-color-hover: var(--neutral-600);
|
||||
--checkbox-border-color-selected: var(--secondary-600);
|
||||
--checkbox-border-width: var(--input-border-width);
|
||||
--checkbox-label-background-fill: None;
|
||||
--checkbox-label-background-fill-hover: None;
|
||||
--checkbox-label-background-fill-selected: var(--checkbox-label-background-fill);
|
||||
--checkbox-label-border-color: var(--border-color-primary);
|
||||
--checkbox-label-border-color-hover: var(--checkbox-label-border-color);
|
||||
--checkbox-label-border-width: var(--input-border-width);
|
||||
--checkbox-label-text-color: var(--body-text-color);
|
||||
--checkbox-label-text-color-selected: var(--checkbox-label-text-color);
|
||||
--error-background-fill: var(--background-fill-primary);
|
||||
--error-border-color: var(--border-color-primary);
|
||||
--error_border_width: None;
|
||||
--error-text-color: #ef4444;
|
||||
--input-background-fill: var(--neutral-800);
|
||||
--input-background-fill-focus: var(--secondary-600);
|
||||
--input-background-fill-hover: var(--input-background-fill);
|
||||
--input-border-color: var(--border-color-primary);
|
||||
--input-border-color-focus: var(--neutral-700);
|
||||
--input-border-color-hover: var(--input-border-color);
|
||||
--input_border_width: None;
|
||||
--input-placeholder-color: var(--neutral-500);
|
||||
--input-shadow: 2px 2px 2px 2px #111111;
|
||||
--input-shadow-focus: 2px 2px 2px 2px #111111;
|
||||
--loader_color: None;
|
||||
--slider_color: None;
|
||||
--stat-background-fill: linear-gradient(to right, var(--primary-400), var(--primary-600));
|
||||
--table-border-color: var(--neutral-700);
|
||||
--table-even-background-fill: #222222;
|
||||
--table-odd-background-fill: #333333;
|
||||
--table-row-focus: var(--color-accent-soft);
|
||||
--button-border-width: var(--input-border-width);
|
||||
--button-cancel-background-fill: linear-gradient(to bottom right, #dc2626, #b91c1c);
|
||||
--button-cancel-background-fill-hover: linear-gradient(to bottom right, #dc2626, #dc2626);
|
||||
--button-cancel-border-color: #dc2626;
|
||||
--button-cancel-border-color-hover: var(--button-cancel-border-color);
|
||||
--button-cancel-text-color: white;
|
||||
--button-cancel-text-color-hover: var(--button-cancel-text-color);
|
||||
--button-primary-background-fill: linear-gradient(to bottom right, var(--primary-500), var(--primary-800));
|
||||
--button-primary-background-fill-hover: linear-gradient(to bottom right, var(--primary-500), var(--primary-300));
|
||||
--button-primary-border-color: var(--primary-500);
|
||||
--button-primary-border-color-hover: var(--button-primary-border-color);
|
||||
--button-primary-text-color: white;
|
||||
--button-primary-text-color-hover: var(--button-primary-text-color);
|
||||
--button-secondary-background-fill: linear-gradient(to bottom right, var(--neutral-600), var(--neutral-800));
|
||||
--button-secondary-background-fill-hover: linear-gradient(to bottom right, var(--neutral-600), var(--neutral-400));
|
||||
--button-secondary-border-color: var(--neutral-600);
|
||||
--button-secondary-border-color-hover: var(--button-secondary-border-color);
|
||||
--button-secondary-text-color: white;
|
||||
--button-secondary-text-color-hover: var(--button-secondary-text-color);
|
||||
--primary-50: #fff7ed;
|
||||
--primary-100: #ffedd5;
|
||||
--primary-200: #fed7aa;
|
||||
--primary-300: #fdba74;
|
||||
--primary-400: #fb923c;
|
||||
--primary-500: #f97316;
|
||||
--primary-600: #ea580c;
|
||||
--primary-700: #c2410c;
|
||||
--primary-800: #9a3412;
|
||||
--primary-900: #7c2d12;
|
||||
--primary-950: #6c2e12;
|
||||
--secondary-50: #eff6ff;
|
||||
--secondary-100: #dbeafe;
|
||||
--secondary-200: #bfdbfe;
|
||||
--secondary-300: #93c5fd;
|
||||
--secondary-400: #60a5fa;
|
||||
--secondary-500: #3b82f6;
|
||||
--secondary-600: #2563eb;
|
||||
--secondary-700: #1d4ed8;
|
||||
--secondary-800: #1e40af;
|
||||
--secondary-900: #1e3a8a;
|
||||
--secondary-950: #1d3660;
|
||||
--neutral-50: #f0f0f0;
|
||||
--neutral-100: #e0e0e0;
|
||||
--neutral-200: #d0d0d0;
|
||||
--neutral-300: #b0b0b0;
|
||||
--neutral-400: #909090;
|
||||
--neutral-500: #707070;
|
||||
--neutral-600: #606060;
|
||||
--neutral-700: #404040;
|
||||
--neutral-800: #333333;
|
||||
--neutral-900: #111827;
|
||||
--neutral-950: #0b0f19;
|
||||
--spacing-xxs: 1px;
|
||||
--spacing-xs: 2px;
|
||||
--spacing-sm: 4px;
|
||||
--spacing-md: 6px;
|
||||
--spacing-lg: 8px;
|
||||
--spacing-xl: 10px;
|
||||
--spacing-xxl: 18px;
|
||||
--radius-xxs: 0;
|
||||
--radius-xs: 0;
|
||||
--radius-sm: 0;
|
||||
--radius-md: 0;
|
||||
--radius-lg: 0;
|
||||
--radius-xl: 0;
|
||||
--radius-xxl: 0;
|
||||
--text-xxs: 9px;
|
||||
--text-xs: 10px;
|
||||
--text-sm: 12px;
|
||||
--text-md: 14px;
|
||||
--text-lg: 16px;
|
||||
--text-xl: 22px;
|
||||
--text-xxl: 26px;
|
||||
--font: 'Source Sans Pro', 'ui-sans-serif', 'system-ui', sans-serif;
|
||||
--font-mono: 'IBM Plex Mono', 'ui-monospace', 'Consolas', monospace;
|
||||
--body-text-size: var(--text-md);
|
||||
--body-text-weight: 400;
|
||||
--embed-radius: var(--radius-lg);
|
||||
--color-accent: var(--primary-500);
|
||||
--shadow-drop: 0;
|
||||
--shadow-drop-lg: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-inset: rgba(0,0,0,0.05) 0px 2px 4px 0px inset;
|
||||
--block-border-width: 1px;
|
||||
--block-info-text-size: var(--text-sm);
|
||||
--block-info-text-weight: 400;
|
||||
--block-label-border-width: 1px;
|
||||
--block-label-margin: 0;
|
||||
--block-label-padding: var(--spacing-sm) var(--spacing-lg);
|
||||
--block-label-radius: calc(var(--radius-lg) - 1px) 0 calc(var(--radius-lg) - 1px) 0;
|
||||
--block-label-right-radius: 0 calc(var(--radius-lg) - 1px) 0 calc(var(--radius-lg) - 1px);
|
||||
--block-label-text-size: var(--text-sm);
|
||||
--block-label-text-weight: 400;
|
||||
--block-padding: var(--spacing-xl) calc(var(--spacing-xl) + 2px);
|
||||
--block-radius: var(--radius-lg);
|
||||
--block-shadow: var(--shadow-drop);
|
||||
--block-title-background-fill: none;
|
||||
--block-title-border-color: none;
|
||||
--block-title-border-width: 0px;
|
||||
--block-title-padding: 0;
|
||||
--block-title-radius: none;
|
||||
--block-title-text-size: var(--text-md);
|
||||
--block-title-text-weight: 400;
|
||||
--container-radius: var(--radius-lg);
|
||||
--form-gap-width: 1px;
|
||||
--layout-gap: var(--spacing-xxl);
|
||||
--panel-border-width: 0;
|
||||
--section-header-text-size: var(--text-md);
|
||||
--section-header-text-weight: 400;
|
||||
--checkbox-border-radius: var(--radius-sm);
|
||||
--checkbox-label-gap: 2px;
|
||||
--checkbox-label-padding: var(--spacing-md) calc(2 * var(--spacing-md));
|
||||
--checkbox-label-shadow: var(--shadow-drop);
|
||||
--checkbox-label-text-size: var(--text-md);
|
||||
--checkbox-label-text-weight: 400;
|
||||
--checkbox-check: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
||||
--radio-circle: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
|
||||
--checkbox-shadow: var(--input-shadow);
|
||||
--error-border-width: 1px;
|
||||
--input-border-width: 0;
|
||||
--input-padding: 6px;
|
||||
--input-radius: var(--radius-lg);
|
||||
--input-text-size: var(--text-md);
|
||||
--input-text-weight: 400;
|
||||
--loader-color: var(--color-accent);
|
||||
--prose-text-size: var(--text-md);
|
||||
--prose-text-weight: 400;
|
||||
--prose-header-text-weight: 600;
|
||||
--slider-color: ;
|
||||
--table-radius: var(--radius-lg);
|
||||
--button-large-padding: 2px 10px;
|
||||
--button-large-radius: var(--radius-lg);
|
||||
--button-large-text-size: var(--text-lg);
|
||||
--button-large-text-weight: 400;
|
||||
--button-shadow: 4px 4px 4px 0px #333333;
|
||||
--button-shadow-active: 1px 1px 4px 0px #555555;
|
||||
--button-shadow-hover: 1px 1px 4px 0px #555555;
|
||||
--button-small-padding: var(--spacing-sm) calc(2 * var(--spacing-sm));
|
||||
--button-small-radius: var(--radius-lg);
|
||||
--button-small-text-size: var(--text-md);
|
||||
--button-small-text-weight: 400;
|
||||
--button-transition: none;
|
||||
}
|
||||
|
After Width: | Height: | Size: 151 KiB |
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -61,6 +61,7 @@ window.document.addEventListener('dragover', e => {
|
||||
|
||||
window.document.addEventListener('drop', e => {
|
||||
const target = e.composedPath()[0];
|
||||
if (!target.placeholder) return;
|
||||
if (target.placeholder.indexOf("Prompt") == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,48 +2,32 @@
|
||||
function extensions_apply(_, _, disable_all){
|
||||
var disable = []
|
||||
var update = []
|
||||
|
||||
gradioApp().querySelectorAll('#extensions input[type="checkbox"]').forEach(function(x){
|
||||
if(x.name.startsWith("enable_") && ! x.checked)
|
||||
disable.push(x.name.substr(7))
|
||||
|
||||
if(x.name.startsWith("update_") && x.checked)
|
||||
update.push(x.name.substr(7))
|
||||
if(x.name.startsWith("enable_") && ! x.checked) disable.push(x.name.substr(7))
|
||||
if(x.name.startsWith("update_") && x.checked) update.push(x.name.substr(7))
|
||||
})
|
||||
|
||||
restart_reload()
|
||||
|
||||
return [JSON.stringify(disable), JSON.stringify(update), disable_all]
|
||||
}
|
||||
|
||||
function extensions_check(_, _){
|
||||
var disable = []
|
||||
|
||||
gradioApp().querySelectorAll('#extensions input[type="checkbox"]').forEach(function(x){
|
||||
if(x.name.startsWith("enable_") && ! x.checked)
|
||||
disable.push(x.name.substr(7))
|
||||
if(x.name.startsWith("enable_") && ! x.checked) disable.push(x.name.substr(7))
|
||||
})
|
||||
|
||||
gradioApp().querySelectorAll('#extensions .extension_status').forEach(function(x){
|
||||
x.innerHTML = "Loading..."
|
||||
})
|
||||
|
||||
|
||||
var id = randomId()
|
||||
requestProgress(id, gradioApp().getElementById('extensions_installed_top'), null, function(){
|
||||
|
||||
})
|
||||
|
||||
requestProgress(id, gradioApp().getElementById('extensions_installed_top'), null, null, null, false)
|
||||
return [id, JSON.stringify(disable)]
|
||||
}
|
||||
|
||||
function install_extension_from_index(button, url){
|
||||
button.disabled = "disabled"
|
||||
button.value = "Installing..."
|
||||
|
||||
textarea = gradioApp().querySelector('#extension_to_install textarea')
|
||||
textarea.value = url
|
||||
updateInput(textarea)
|
||||
|
||||
gradioApp().querySelector('#install_extension_button').click()
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@ function setupExtraNetworksForTab(tabname){
|
||||
var search = gradioApp().querySelector('#'+tabname+'_extra_search textarea')
|
||||
var refresh = gradioApp().getElementById(tabname+'_extra_refresh')
|
||||
var descriptInput = gradioApp().getElementById(tabname+ '_description_input')
|
||||
var close = gradioApp().getElementById(tabname+'_extra_close')
|
||||
|
||||
search.classList.add('search')
|
||||
tabs.appendChild(search)
|
||||
tabs.appendChild(refresh)
|
||||
tabs.appendChild(descriptInput)
|
||||
|
||||
tabs.appendChild(close)
|
||||
|
||||
search.addEventListener("input", function(evt){
|
||||
searchTerm = search.value.toLowerCase()
|
||||
|
||||
|
||||
@@ -2,12 +2,8 @@
|
||||
|
||||
let txt2img_gallery, img2img_gallery, modal = undefined;
|
||||
onUiUpdate(function(){
|
||||
if (!txt2img_gallery) {
|
||||
txt2img_gallery = attachGalleryListeners("txt2img")
|
||||
}
|
||||
if (!img2img_gallery) {
|
||||
img2img_gallery = attachGalleryListeners("img2img")
|
||||
}
|
||||
if (!txt2img_gallery) txt2img_gallery = attachGalleryListeners("txt2img")
|
||||
if (!img2img_gallery) img2img_gallery = attachGalleryListeners("img2img")
|
||||
if (!modal) {
|
||||
modal = gradioApp().getElementById('lightboxModal')
|
||||
modalObserver.observe(modal, { attributes : true, attributeFilter : ['style'] });
|
||||
@@ -15,10 +11,11 @@ onUiUpdate(function(){
|
||||
});
|
||||
|
||||
let modalObserver = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutationRecord) {
|
||||
let selectedTab = gradioApp().querySelector('#tabs div button.bg-white')?.innerText
|
||||
if (mutationRecord.target.style.display === 'none' && selectedTab === 'txt2img' || selectedTab === 'img2img')
|
||||
gradioApp().getElementById(selectedTab+"_generation_info_button").click()
|
||||
mutations.forEach((mutationRecord) => {
|
||||
let selectedTab = gradioApp().querySelector('#tabs div button.selected')?.innerText
|
||||
if (!selectedTab) selectedTab = gradioApp().querySelector('#tabs div button')?.innerText
|
||||
if (mutationRecord.target.style.display === 'none' && (selectedTab === 'txt2img' || selectedTab === 'img2img'))
|
||||
gradioApp().getElementById(selectedTab+"_generation_info_button")?.click()
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 89 KiB |