fix for vlad fork

This commit is contained in:
Miao Xiang
2023-04-23 21:36:59 -07:00
232 changed files with 9741 additions and 3503 deletions
+11
View File
@@ -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']
+35 -89
View File
@@ -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 -1
View File
@@ -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.
+9 -31
View File
@@ -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:
+6 -23
View File
@@ -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
+2 -3
View File
@@ -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
+3 -12
View File
@@ -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
+37 -29
View File
@@ -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
+50
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
{
"MD012": false,
"MD013": false,
"MD033": false,
"MD036": false,
"MD041": false
}
+208 -2
View File
@@ -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
+14
View File
@@ -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"
}
-12
View File
@@ -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.
+153 -149
View File
@@ -1,161 +1,165 @@
# Stable Diffusion web UI
A browser interface based on Gradio library for Stable Diffusion.
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/vladmandic)
![Last Commit](https://img.shields.io/github/last-commit/vladmandic/human?style=flat-square&svg=true)
![License](https://img.shields.io/github/license/vladmandic/human?style=flat-square&svg=true)
![GitHub Status Checks](https://img.shields.io/github/checks-status/vladmandic/human/main?style=flat-square&svg=true)
![](screenshot.png)
# 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).
![screenshot](javascript/black-orange.jpg)
## 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>
+68
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+54
View File
@@ -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

+3
View File
@@ -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
+163
View File
@@ -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
+38
View File
@@ -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"
}
}
+378
View File
@@ -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()
'''
+123
View File
@@ -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()
+125
View File
@@ -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' })
+128
View File
@@ -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))
+166
View File
@@ -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()
+109
View File
@@ -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())
+144
View File
@@ -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)
+160
View File
@@ -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))
+74
View File
@@ -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()
+129
View File
@@ -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)
+101
View File
@@ -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')
+240
View File
@@ -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))
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

+500
View File
@@ -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))
+62
View File
@@ -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)
+39
View File
@@ -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)
+192
View File
@@ -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())
+191
View File
@@ -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'})
+132
View File
@@ -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)
+109
View File
@@ -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
+71
View File
@@ -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)
+31
View File
@@ -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"
]
}
+43
View File
@@ -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")
+97
View 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
"""
+33
View File
@@ -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)
+3
View File
@@ -0,0 +1,3 @@
mediapipe
colormap
invisible-watermark
+274
View File
@@ -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()
+591
View File
@@ -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())
+162
View File
@@ -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))
+141
View File
@@ -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
})
+326
View File
@@ -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
+111
View File
@@ -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)
+322
View File
@@ -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')
+85
View File
@@ -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
+12
View File
@@ -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
+67
View File
@@ -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"
+68
View File
@@ -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"
+158
View File
@@ -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
-11
View File
@@ -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
+1 -1
View File
@@ -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
-6
View File
@@ -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]))
+26 -10
View File
@@ -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 = \
-6
View File
@@ -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]
+1
View File
@@ -0,0 +1 @@
99
-6
View File
@@ -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'))
-6
View File
@@ -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 -1
View File
@@ -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>
 • 
+320
View File
@@ -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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

+4 -3
View File
@@ -132,13 +132,14 @@ addContextMenuEventListener = initResponse[2];
//Start example Context Menu Items
let generateOnRepeat = function(genbuttonid,interruptbuttonid){
let genbutton = gradioApp().querySelector(genbuttonid);
let interruptbutton = gradioApp().querySelector(interruptbuttonid);
if(!interruptbutton.offsetParent){
const busy = document.getElementById('progressbar')?.style.display == "block"
if(!busy){
genbutton.click();
}
clearInterval(window.generateOnRepeatInterval)
window.generateOnRepeatInterval = setInterval(function(){
if(!interruptbutton.offsetParent){
const busy = document.getElementById('progressbar')?.style.display == "block"
if(!busy){
genbutton.click();
}
},
+1
View File
@@ -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;
}
+4 -20
View File
@@ -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()
}
+3
View File
@@ -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()
+7 -10
View File
@@ -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()
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Some files were not shown because too many files have changed in this diff Show More