diff --git a/.eslintrc.json b/.eslintrc.json index 62feb13a5..c86dbb749 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -37,14 +37,19 @@ "object-curly-newline":"off", "prefer-rest-params":"off", "prefer-destructuring":"off", - "radix":"off" + "radix":"off", + "node/shebang": "off" }, "globals": { // asssets "panzoom": "readonly", - // script.js + // logger.js "log": "readonly", "debug": "readonly", + "error": "readonly", + "xhrGet": "readonly", + "xhrPost": "readonly", + // script.js "gradioApp": "readonly", "executeCallbacks": "readonly", "onAfterUiUpdate": "readonly", @@ -87,7 +92,6 @@ // settings.js "registerDragDrop": "readonly", // extraNetworks.js - "requestGet": "readonly", "getENActiveTab": "readonly", "quickApplyStyle": "readonly", "quickSaveStyle": "readonly", diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6cd163b..919041bde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,73 @@ # Change Log for SD.Next -## Update for 2024-11-22 +## Update for 2024-11-28 -- Model loader improvements: +### New models and integrations + +- [Flux Tools](https://blackforestlabs.ai/flux-1-tools/) + **Redux** is actually a tool, **Fill** is inpaint/outpaint optimized version of *Flux-dev* + **Canny** & **Depth** are optimized versions of *Flux-dev* for their respective tasks: they are *not* ControlNets that work on top of a model + to use, go to image or control interface and select *Flux Tools* in scripts + all models are auto-downloaded on first use + *note*: All models are [gated](https://github.com/vladmandic/automatic/wiki/Gated) and require acceptance of terms and conditions via web page + *recommended*: Enable on-the-fly [quantization](https://github.com/vladmandic/automatic/wiki/Quantization) or [compression](https://github.com/vladmandic/automatic/wiki/NNCF-Compression) to reduce resource usage + *todo*: support for Canny/Depth LoRAs + - [Redux](https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev): ~0.1GB + works together with existing model and basically uses input image to analyze it and use that instead of prompt + *recommended*: low denoise strength levels result in more variety + - [Fill](https://huggingface.co/black-forest-labs/FLUX.1-Fill-dev): ~23.8GB, replaces currently loaded model + *note*: can be used in inpaint/outpaint mode only + - [Canny](https://huggingface.co/black-forest-labs/FLUX.1-Canny-dev): ~23.8GB, replaces currently loaded model + *recommended*: guidance scale 30 + - [Depth](https://huggingface.co/black-forest-labs/FLUX.1-Depth-dev): ~23.8GB, replaces currently loaded model + *recommended*: guidance scale 10 +- [StabilityAI SD35 ControlNets]([sd3_medium](https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets)) + - In addition to previously released `InstantX` and `Alimama`, we now have *official* ones from StabilityAI +- [Style Aligned Image Generation](https://style-aligned-gen.github.io/) + enable in scripts, compatible with sd-xl + enter multiple prompts in prompt field separated by new line + style-aligned applies selected attention layers uniformly to all images to achive consistency + can be used with or without input image in which case first prompt is used to establish baseline + *note:* all prompts are processes as a single batch, so vram is limiting factor + +### UI and workflow improvements + +- **Model loader** improvements: - detect model components on model load fail + - allow passing absolute path to model loader - Flux, SD35: force unload model - Flux: apply `bnb` quant when loading *unet/transformer* - Flux: all-in-one safetensors example: - Flux: do not recast quants -- Sampler improvements - - update DPM FlowMatch samplers -- Fixes: - - update `diffusers` - - fix README links - - fix sdxl controlnet single-file loader - - relax settings validator +- **UI**: + - improved stats on generate completion + - improved live preview display and performance + - improved accordion behavior + - auto-size networks height for sidebar + - control: hide preview column by default + - control: optionn to hide input column + - control: add stats + - browser -> server logging framework + - add addtional themes: `black-reimagined` +- **Sampler** improvements + - Euler FlowMatch: add sigma methods (*karras/exponential/betas*) + - DPM FlowMatch: update all and add sigma methods + +### Fixes + +- update `diffusers` +- fix README links +- fix sdxl controlnet single-file loader +- relax settings validator +- improve js progress calls resiliency +- fix text-to-video pipeline +- avoid live-preview if vae-decode is running +- allow xyz-grid with multi-axis s&r +- fix xyz-grid with lora +- fix api script callbacks +- fix gpu memory monitoring +- simplify img2img/inpaint/sketch canvas handling ## Update for 2024-11-21 diff --git a/TODO.md b/TODO.md index 973e062dc..73008039d 100644 --- a/TODO.md +++ b/TODO.md @@ -7,9 +7,9 @@ Main ToDo list can be found at [GitHub projects](https://github.com/users/vladma - SD35 IPAdapter: - SD35 LoRA: - Flux IPAdapter: -- Flux Fill/ControlNet/Redux: - Flux NF4: - SANA: +- LTX-Video: ## Other diff --git a/cli/api-model.js b/cli/api-model.js new file mode 100755 index 000000000..e2ce5344a --- /dev/null +++ b/cli/api-model.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +const sd_url = process.env.SDAPI_URL || 'http://127.0.0.1:7860'; +const sd_username = process.env.SDAPI_USR; +const sd_password = process.env.SDAPI_PWD; +const models = [ + '/mnt/models/stable-diffusion/sd15/lyriel_v16.safetensors', + '/mnt/models/stable-diffusion/flux/flux-finesse_v2-f1h-fp8.safetensors', + '/mnt/models/stable-diffusion/sdxl/TempestV0.1-Artistic.safetensors', +]; + +async function options(data) { + const method = 'POST'; + const headers = new Headers(); + const body = JSON.stringify(data); + headers.set('Content-Type', 'application/json'); + if (sd_username && sd_password) headers.set({ Authorization: `Basic ${btoa('sd_username:sd_password')}` }); + const res = await fetch(`${sd_url}/sdapi/v1/options`, { method, headers, body }); + return res; +} + +async function main() { + for (const model of models) { + console.log('model:', model); + const res = await options({ sd_model_checkpoint: model }); + console.log('result:', res); + } +} + +main(); diff --git a/cli/api-pulid.js b/cli/api-pulid.js index fde0ae43b..033824e9b 100755 --- a/cli/api-pulid.js +++ b/cli/api-pulid.js @@ -10,12 +10,13 @@ const argparse = require('argparse'); const sd_url = process.env.SDAPI_URL || 'http://127.0.0.1:7860'; const sd_username = process.env.SDAPI_USR; const sd_password = process.env.SDAPI_PWD; +let args = {}; function b64(file) { const data = fs.readFileSync(file); - const b64 = Buffer.from(data).toString('base64'); + const b64str = Buffer.from(data).toString('base64'); const ext = path.extname(file).replace('.', ''); - str = `data:image/${ext};base64,${b64}`; + const str = `data:image/${ext};base64,${b64str}`; // console.log('b64:', ext, b64.length); return str; } @@ -39,7 +40,16 @@ function options() { if (args.pulid) { const b64image = b64(args.pulid); opt.script_name = 'pulid'; - opt.script_args = [b64image, 0.9]; + opt.script_args = [ + b64image, // b64 encoded image, required param + 0.9, // strength, optional + 20, // zero, optional + 'dpmpp_sde', // sampler, optional + 'v2', // ortho, optional + true, // restore (disable pulid after run), optional + true, // offload, optional + 'v1.1', // version, optional + ]; } // console.log('options:', opt); return opt; @@ -53,8 +63,8 @@ function init() { parser.add_argument('--height', { type: 'int', help: 'height' }); parser.add_argument('--pulid', { type: 'str', help: 'pulid init image' }); parser.add_argument('--output', { type: 'str', help: 'output path' }); - const args = parser.parse_args(); - return args + const parsed = parser.parse_args(); + return parsed; } async function main() { @@ -73,12 +83,12 @@ async function main() { console.log('result:', json.info); for (const i in json.images) { // eslint-disable-line guard-for-in const file = args.output || `/tmp/test-${i}.jpg`; - const data = atob(json.images[i]) + const data = atob(json.images[i]); fs.writeFileSync(file, data, 'binary'); console.log('image saved:', file); } } } -const args = init(); +args = init(); main(); diff --git a/cli/full-test.sh b/cli/full-test.sh index e410528ad..912dc3a5b 100755 --- a/cli/full-test.sh +++ b/cli/full-test.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +node cli/api-txt2img.js +node cli/api-pulid.js + source venv/bin/activate echo image-exif python cli/api-info.py --input html/logo-bg-0.jpg diff --git a/extensions-builtin/Lora/networks.py b/extensions-builtin/Lora/networks.py index db617ee5b..fd6287c62 100644 --- a/extensions-builtin/Lora/networks.py +++ b/extensions-builtin/Lora/networks.py @@ -88,7 +88,7 @@ def assign_network_names_to_compvis_modules(sd_model): network_name = name.replace(".", "_") network_layer_mapping[network_name] = module module.network_layer_name = network_name - shared.sd_model.network_layer_mapping = network_layer_mapping + sd_model.network_layer_mapping = network_layer_mapping def load_diffusers(name, network_on_disk, lora_scale=shared.opts.extra_networks_default_multiplier) -> network.Network: @@ -141,7 +141,7 @@ def load_network(name, network_on_disk) -> network.Network: sd = sd_models.read_state_dict(network_on_disk.filename, what='network') if shared.sd_model_type == 'f1': # if kohya flux lora, convert state_dict sd = lora_convert._convert_kohya_flux_lora_to_diffusers(sd) or sd # pylint: disable=protected-access - assign_network_names_to_compvis_modules(shared.sd_model) # this should not be needed but is here as an emergency fix for an unknown error people are experiencing in 1.2.0 + assign_network_names_to_compvis_modules(shared.sd_model) keys_failed_to_match = {} matched_networks = {} bundle_embeddings = {} diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 4647bd7f8..3008cee4b 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 4647bd7f86be9d2783a9ba1f38acaa9bcec942d2 +Subproject commit 3008cee4b67bb00f8f1a4fe4510ec27ba92aa418 diff --git a/installer.py b/installer.py index 0b64c3616..37202552d 100644 --- a/installer.py +++ b/installer.py @@ -212,7 +212,7 @@ def installed(package, friendly: str = None, reload = False, quiet = False): if friendly: pkgs = friendly.split() else: - pkgs = [p for p in package.split() if not p.startswith('-') and not p.startswith('=')] + pkgs = [p for p in package.split() if not p.startswith('-') and not p.startswith('=') and not p.startswith('git+')] pkgs = [p.split('/')[-1] for p in pkgs] # get only package name if installing from url for pkg in pkgs: if '!=' in pkg: @@ -295,7 +295,7 @@ def install(package, friendly: str = None, ignore: bool = False, reinstall: bool quick_allowed = False if args.reinstall or reinstall or not installed(package, friendly, quiet=quiet): deps = '' if not no_deps else '--no-deps ' - res = pip(f"install{' --upgrade' if not args.uv else ''} {deps}{package}", ignore=ignore, uv=package != "uv") + res = pip(f"install{' --upgrade' if not args.uv else ''} {deps}{package}", ignore=ignore, uv=package != "uv" and not package.startswith('git+')) try: import importlib # pylint: disable=deprecated-module importlib.reload(pkg_resources) @@ -459,7 +459,7 @@ def check_python(supported_minors=[9, 10, 11, 12], reason=None): def check_diffusers(): if args.skip_all or args.skip_requirements: return - sha = 'b5fd6f13f5434d69d919cc8cedf0b11db664cf06' + sha = '069186fac510d6f6f88a5e435523b235c823a8a0' pkg = pkg_resources.working_set.by_key.get('diffusers', None) minor = int(pkg.version.split('.')[1] if pkg is not None else 0) cur = opts.get('diffusers_version', '') if minor > 0 else '' diff --git a/javascript/base.css b/javascript/base.css index 7daa8b2bd..6c18ad7c5 100644 --- a/javascript/base.css +++ b/javascript/base.css @@ -25,7 +25,6 @@ .progressDiv .progress { width: 0%; height: 20px; background: #0060df; color: white; font-weight: bold; line-height: 20px; padding: 0 8px 0 0; text-align: right; overflow: visible; white-space: nowrap; padding: 0 0.5em; } .livePreview { position: absolute; z-index: 50; background-color: transparent; width: -moz-available; width: -webkit-fill-available; } .livePreview img { position: absolute; object-fit: contain; width: 100%; height: 100%; } -.dark .livePreview { background-color: rgb(17 24 39 / var(--tw-bg-opacity)); } .popup-metadata { color: white; background: #0000; display: inline-block; white-space: pre-wrap; font-size: 0.75em; } /* fullpage image viewer */ diff --git a/javascript/black-teal-reimagined.css b/javascript/black-teal-reimagined.css new file mode 100644 index 000000000..b7567ce75 --- /dev/null +++ b/javascript/black-teal-reimagined.css @@ -0,0 +1,1072 @@ +/* Generic HTML Tags */ +@font-face { + font-family: 'NotoSans'; + font-display: swap; + font-style: normal; + font-weight: 100; + src: local('NotoSans'), url('notosans-nerdfont-regular.ttf'); +} + +html { + scroll-behavior: smooth; +} + +:root, +.light, +.dark { + --font: 'NotoSans'; + --font-mono: 'ui-monospace', 'Consolas', monospace; + --font-size: 16px; + + /* Primary Colors */ + --primary-50: #7dffff; + --primary-100: #72e8e8; + --primary-200: #67d2d2; + --primary-300: #5dbcbc; + --primary-400: #52a7a7; + --primary-500: #489292; + --primary-600: #3e7d7d; + --primary-700: #356969; + --primary-800: #2b5656; + --primary-900: #224444; + --primary-950: #193232; + + /* Neutral Colors */ + --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: #303030; + --neutral-900: #202020; + --neutral-950: #101010; + + /* Highlight and Inactive Colors */ + --highlight-color: var(--primary-200); + --inactive-color: var(--primary-800); + + /* Text Colors */ + --body-text-color: var(--neutral-100); + --body-text-color-subdued: var(--neutral-300); + + /* Background Colors */ + --background-color: var(--neutral-950); + --background-fill-primary: var(--neutral-700); + --input-background-fill: var(--neutral-800); + + /* Padding and Borders */ + --input-padding: 4px; + --input-shadow: none; + --button-primary-text-color: var(--neutral-100); + --button-primary-background-fill: var(--primary-600); + --button-primary-background-fill-hover: var(--primary-800); + --button-secondary-text-color: var(--neutral-100); + --button-secondary-background-fill: var(--neutral-900); + --button-secondary-background-fill-hover: var(--neutral-600); + + /* Border Radius */ + --radius-xs: 2px; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 10px; + --radius-xxl: 15px; + --radius-xxxl: 20px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1); + --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 8px 16px rgba(0, 0, 0, 0.1); + + /* Animation */ + --transition: all 0.3s ease; + + /* Scrollbar */ + --scrollbar-bg: var(--neutral-800); + --scrollbar-thumb: var(--highlight-color); +} + +html { + font-size: var(--font-size); + font-family: var(--font); +} + +body, +button, +input, +select, +textarea { + font-family: var(--font); + color: var(--body-text-color); + transition: var(--transition); +} + +button { + max-width: 400px; + white-space: nowrap; + padding: 8px 12px; + border: none; + border-radius: var(--radius-md); + background-color: var(--button-primary-background-fill); + color: var(--button-primary-text-color); + cursor: pointer; + box-shadow: var(--shadow-sm); + transition: transform 0.2s ease, background-color 0.3s ease; +} + +button:hover { + background-color: var(--button-primary-background-fill-hover); + transform: scale(1.05); +} + +/* Range Input Styles */ +.slider-container { + width: 100%; + /* Ensures the container takes full width */ + max-width: 100%; + /* Prevents overflow */ + padding: 0 10px; + /* Adds padding for aesthetic spacing */ + box-sizing: border-box; + /* Ensures padding doesn't affect width */ +} + +input[type='range'] { + display: block; + margin: 0; + padding: 0; + height: 1em; + background-color: transparent; + overflow: hidden; + cursor: pointer; + box-shadow: none; + -webkit-appearance: none; + opacity: 0.7; + appearance: none; + width: 100%; + /* Makes the slider responsive */ +} + +input[type='range'] { + opacity: 1; +} + +input[type='range']::-webkit-slider-thumb { + -webkit-appearance: none; + height: 1em; + width: 1em; + background-color: var(--highlight-color); + border-radius: var(--radius-xs); + box-shadow: var(--shadow-md); + cursor: pointer; + /* Ensures the thumb is clickable */ +} + +input[type='range']::-webkit-slider-runnable-track { + -webkit-appearance: none; + height: 6px; + background: var(--input-background-fill); + border-radius: var(--radius-md); +} + +input[type='range']::-moz-range-thumb { + height: 1em; + width: 1em; + background-color: var(--highlight-color); + border-radius: var(--radius-xs); + box-shadow: var(--shadow-md); + cursor: pointer; + /* Ensures the thumb is clickable */ +} + +input[type='range']::-moz-range-track { + height: 6px; + background: var(--input-background-fill); + border-radius: var(--radius-md); +} + +@media (max-width: 768px) { + .slider-container { + width: 100%; + /* Adjust width for smaller screens */ + } + + .networks-menu, + .styles-menu { + width: 100%; + /* Ensure menus are full width */ + margin: 0; + /* Reset margins for smaller screens */ + } +} + +/* Scrollbar Styles */ +:root { + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-bg); +} + +::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +::-webkit-scrollbar-track { + background: var(--scrollbar-bg); + border-radius: var(--radius-lg); +} + +::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); +} + +/* Tab Navigation Styles */ +.tab-nav { + display: flex; + /* Use flexbox for layout */ + justify-content: space-evenly; + /* Space out the tabs evenly */ + align-items: center; + /* Center items vertically */ + background: var(--background-color); + /* Background color */ + border-bottom: 1px dashed var(--highlight-color) !important; + /* Bottom border for separation */ + box-shadow: var(--shadow-md); + /* Shadow for depth */ + margin-bottom: 5px; + /* Add some space between the tab nav and the content */ + padding-bottom: 5px; + /* Add space between buttons and border */ +} + +/* Individual Tab Styles */ +.tab-nav>button { + background: var(--neutral-900); + /* No background for default state */ + color: var(--text-color); + /* Text color */ + border: 1px solid var(--highlight-color); + /* No border */ + border-radius: var(--radius-xxl); + /* Rounded corners */ + cursor: pointer; + /* Pointer cursor */ + transition: background 0.3s ease, color 0.3s ease; + /* Smooth transition */ + padding-top: 5px; + padding-bottom: 5px; + padding-right: 10px; + padding-left: 10px; + margin-bottom: 3px; +} + +/* Active Tab Style */ +.tab-nav>button.selected { + background: var(--primary-100); + /* Highlight active tab */ + color: var(--background-color); + /* Change text color for active tab */ +} + +/* Hover State for Tabs */ +.tab-nav>button:hover { + background: var(--highlight-color); + /* Background on hover */ + color: var(--background-color); + /* Change text color on hover */ +} + +/* Responsive Styles */ +@media (max-width: 768px) { + .tab-nav { + flex-direction: column; + /* Stack tabs vertically on smaller screens */ + align-items: stretch; + /* Stretch tabs to full width */ + } + + .tab-nav>button { + width: 100%; + /* Full width for buttons */ + text-align: left; + /* Align text to the left */ + } +} + +/* Quick Settings Panel Styles */ +#quicksettings { + background: var(--background-color); + /* Background color */ + box-shadow: var(--shadow-lg); + /* Shadow for depth */ + border-radius: var(--radius-lg); + /* Rounded corners */ + padding: 1em; + /* Padding for spacing */ + z-index: 200; + /* Ensure it stays on top */ +} + +/* Quick Settings Header */ +#quicksettings .header { + font-size: var(--text-lg); + /* Font size for header */ + font-weight: bold; + /* Bold text */ + margin-bottom: 0.5em; + /* Space below header */ +} + +/* Quick Settings Options */ +#quicksettings .option { + display: flex; + /* Flexbox for layout */ + justify-content: space-between; + /* Space between label and toggle */ + align-items: center; + /* Center items vertically */ + padding: 0.5em 0; + /* Padding for each option */ + border-bottom: 1px solid var(--neutral-600); + /* Separator line */ +} + +/* Option Label Styles */ +#quicksettings .option label { + color: var(--text-color); + /* Text color */ +} + +/* Toggle Switch Styles */ +#quicksettings .option input[type="checkbox"] { + cursor: pointer; + /* Pointer cursor */ +} + +/* Quick Settings Footer */ +#quicksettings .footer { + margin-top: 1em; + /* Space above footer */ + text-align: right; + /* Align text to the right */ +} + +/* Close Button Styles */ +#quicksettings .footer button { + background: var(--button-primary-background-fill); + /* Button background */ + color: var(--button-primary-text-color); + /* Button text color */ + border: none; + /* No border */ + border-radius: var(--radius-md); + /* Rounded corners */ + padding: 0.5em 1em; + /* Padding for button */ + cursor: pointer; + /* Pointer cursor */ + transition: 0.3s ease; + /* Smooth transition */ +} + +/* Close Button Hover State */ +#quicksettings .footer button:hover { + background: var(--highlight-color); + /* Change background on hover */ +} + +/* Responsive Styles */ +@media (max-width: 768px) { + #quicksettings { + right: 10px; + /* Adjust position for smaller screens */ + width: 90%; + /* Full width on smaller screens */ + } +} + +/* Form Styles */ +div.form { + border-width: 0; + box-shadow: var(--shadow-md); + background: var(--background-fill-primary); + border-bottom: 3px solid var(--highlight-color); + padding: 3px; + border-radius: var(--radius-md); + margin: 1px; +} + +/* Gradio Style Classes */ +fieldset .gr-block.gr-box, +label.block span { + padding: 0; + margin-top: -4px; +} + +.border-2 { + border-width: 0; +} + +.border-b-2 { + border-bottom-width: 2px; + border-color: var(--highlight-color) !important; + padding-bottom: 2px; + margin-bottom: 8px; +} + +.bg-white { + color: lightyellow; + background-color: var(--inactive-color); +} + +.gr-box { + border-radius: var(--radius-sm) !important; + background-color: var(--neutral-950) !important; + box-shadow: var(--shadow-md); + border-width: 0; + padding: 4px; + margin: 12px 0; +} + +.gr-button { + font-weight: normal; + box-shadow: var(--shadow-sm); + font-size: 0.8rem; + min-width: 32px; + min-height: 32px; + padding: 3px; + margin: 3px; + transition: var(--transition); +} + +.gr-button:hover { + background-color: var(--highlight-color); +} + +.gr-check-radio { + background-color: var(--inactive-color); + border-width: 0; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); +} + +.gr-check-radio:checked { + background-color: var(--highlight-color); +} + +.gr-compact { + background-color: var(--background-color); +} + +.gr-form { + border-width: 0; +} + +.gr-input { + background-color: var(--neutral-800) !important; + padding: 4px; + margin: 4px; + border-radius: var(--radius-md); + transition: var(--transition); +} + +.gr-input:hover { + background-color: var(--neutral-700); +} + +.gr-input-label { + color: lightyellow; + border-width: 0; + background: transparent; + padding: 2px !important; +} + +.gr-panel { + background-color: var(--background-color); + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); +} + +.eta-bar { + display: none !important; +} + +.gradio-slider { + max-width: 200px; +} + +.gradio-slider input[type="number"] { + background: var(--neutral-950); + margin-top: 2px; +} + +.gradio-image { + height: unset !important; +} + +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.2 rem; + 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); +} + +.p-2 { + padding: 0; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.py-6 { + padding-bottom: 0; +} + +.tabs { + background-color: var(--background-color); +} + +.block.token-counter span { + background-color: var(--input-background-fill) !important; + box-shadow: 2px 2px 2px #111; + border: none !important; + font-size: 0.7rem; +} + +.label-wrap { + margin: 8px 0px 4px 0px; +} + +.gradio-button.tool { + border: none; + background: none; + box-shadow: none; + filter: hue-rotate(340deg) saturate(0.5); +} + +#tab_extensions table td, +#tab_extensions table th, +#tab_config table td, +#tab_config table th { + border: none; +} + +#tab_extensions table tr:hover, +#tab_config table tr:hover { + background-color: var(--neutral-500) !important; +} + +#tab_extensions table, +#tab_config table { + width: 96vw; +} + +#tab_extensions table thead, +#tab_config table thead { + background-color: var(--neutral-700); +} + +#tab_extensions table, +#tab_config table { + background-color: var(--neutral-900); +} + +/* Automatic Style Classes */ +.progressDiv { + border-radius: var(--radius-sm) !important; + position: fixed; + top: 44px; + right: 26px; + max-width: 262px; + height: 48px; + z-index: 99; + box-shadow: var(--button-shadow); +} + +.progressDiv .progress { + border-radius: var(--radius-lg) !important; + background: var(--highlight-color); + line-height: 3rem; + height: 48px; +} + +.gallery-item { + box-shadow: none !important; +} + +.performance { + color: #888; +} + +.image-buttons { + justify-content: center; + gap: 0 !important; +} + +.image-buttons>button { + max-width: 160px; +} + +.tooltip { + background: var(--primary-300); + color: black; + border: none; + border-radius: var(--radius-lg); +} + +#system_row>button, +#settings_row>button, +#config_row>button { + max-width: 10em; +} + +/* Gradio Elements Overrides */ +#div.gradio-container { + overflow-x: hidden; +} + +#img2img_label_copy_to_img2img { + font-weight: normal; +} + +#txt2img_styles, +#img2img_styles, +#control_styles { + padding: 0; + margin-top: 2px; +} + +#txt2img_styles_refresh, +#img2img_styles_refresh, +#control_styles_refresh { + padding: 0; + margin-top: 1em; +} + +#img2img_settings { + min-width: calc(2 * var(--left-column)); + max-width: calc(2 * var(--left-column)); + background-color: var(--neutral-950); + padding-top: 16px; +} + +#interrogate, +#deepbooru { + margin: 0 0px 10px 0px; + max-width: 80px; + max-height: 80px; + font-weight: normal; + font-size: 0.95em; +} + +#quicksettings .gr-button-tool { + font-size: 1.6rem; + box-shadow: none; + margin-left: -20px; + margin-top: -2px; + height: 2.4em; +} + +#footer, +#style_pos_col, +#style_neg_col, +#roll_col, +#extras_upscaler_2, +#extras_upscaler_2_visibility, +#txt2img_seed_resize_from_w, +#txt2img_seed_resize_from_h { + display: none; +} + +#save-animation { + border-radius: var(--radius-sm) !important; + margin-bottom: 16px; + background-color: var(--neutral-950); +} + +#script_list { + padding: 4px; + margin-top: 16px; + margin-bottom: 8px; +} + +#settings>div.flex-wrap { + width: 15em; +} + +#txt2img_cfg_scale { + min-width: 200px; +} + +#txt2img_checkboxes, +#img2img_checkboxes, +#control_checkboxes { + background-color: transparent; + margin-bottom: 0.2em; +} + +#extras_upscale { + margin-top: 10px; +} + +#txt2img_progress_row>div { + min-width: var(--left-column); + max-width: var(--left-column); +} + +#txt2img_settings { + min-width: var(--left-column); + max-width: var(--left-column); + background-color: var(--neutral-950); +} + +#pnginfo_html2_info { + margin-top: -18px; + background-color: var(--input-background-fill); + padding: var(--input-padding); +} + +#txt2img_styles_row, +#img2img_styles_row, +#control_styles_row { + margin-top: -6px; +} + +.block>span { + margin-bottom: 0 !important; + margin-top: var(--spacing-lg); +} + +/* Extra Networks Container */ +#extra_networks_root { + z-index: 100; + background: var(--background-color); + box-shadow: var(--shadow-md); + border-radius: var(--radius-lg); + transform: translateX(100%); + animation: slideIn 0.5s forwards; + overflow: hidden; + /* Prevents overflow of content */ +} + +@keyframes slideIn { + to { + transform: translateX(0); + } +} + +/* Extra Networks Styles */ +.extra-networks { + border-left: 2px solid var(--highlight-color) !important; + padding-left: 4px; +} + +.extra-networks .tab-nav>button:hover { + background: var(--highlight-color); +} + +/* Network tab search and description important fix, dont remove */ +#txt2img_description, +#txt2img_extra_search, +#img2img_description, +#img2img_extra_search, +#control_description, +#control_extra_search { + margin-top: 50px; +} + +.extra-networks .buttons>button:hover { + background: var(--highlight-color); +} + +/* Network Cards Container */ +.extra-network-cards { + display: flex; + flex-wrap: wrap; + overflow-y: auto; + overflow-x: hidden; + align-content: flex-start; + padding-top: 20px; + justify-content: center; + width: 100%; + /* Ensures it takes full width */ +} + +/* Individual Card Styles */ +.extra-network-cards .card { + height: fit-content; + margin: 0 0 0.5em 0.5em; + position: relative; + scroll-snap-align: start; + scroll-margin-top: 0; + background: var(--neutral-800); + /* Background for cards */ + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); + transition: var(--transition); +} + +/* Overlay Styles */ +.extra-network-cards .card .overlay { + z-index: 10; + width: 100%; + background: none; + border-radius: var(--radius-md); +} + +/* Overlay Name Styles */ +.extra-network-cards .card .overlay .name { + font-size: var(--text-lg); + font-weight: bold; + text-shadow: 1px 1px black; + color: white; + overflow-wrap: anywhere; + position: absolute; + bottom: 0; + padding: 0.2em; + z-index: 10; +} + +/* Preview Styles */ +.extra-network-cards .card .preview { + box-shadow: var(--button-shadow); + min-height: 30px; + border-radius: var(--radius-md); + z-index: 9999; +} + +/* Hover Effects */ +.extra-network-cards .card:hover { + transform: scale(1.3); + z-index: 9999; /* Use a high value to ensure it appears on top */ + transition: transform 0.3s ease, z-index 0s; /* Smooth transition */ +} + +.extra-network-cards .card:hover .overlay { + z-index: 10000; /* Ensure overlay is also on top */ +} + +.extra-network-cards .card:hover .preview { + box-shadow: none; + filter: grayscale(0%); +} + +/* Tags Styles */ +.extra-network-cards .card .overlay .tags { + display: none; + overflow-wrap: anywhere; + position: absolute; + top: 100%; + z-index: 20; + background: var(--body-background-fill); + overflow-x: hidden; + overflow-y: auto; + max-height: 333px; +} + +/* Individual Tag Styles */ +.extra-network-cards .card .overlay .tag { + padding: 2px; + margin: 2px; + background: rgba(70, 70, 70, 0.60); + font-size: var(--text-md); + cursor: pointer; + display: inline-block; +} + +/* Actions Styles */ +.extra-network-cards .card .actions>span { + padding: 4px; + font-size: 34px !important; +} + +.extra-network-cards .card .actions { + background: none; +} + +.extra-network-cards .card .actions .details { + bottom: 50px; + background-color: var(--neutral-800); +} + +.extra-network-cards .card .actions>span:hover { + color: var(--highlight-color); +} + +/* Version Styles */ +.extra-network-cards .card .version { + position: absolute; + top: 0; + left: 0; + padding: 2px; + font-weight: bolder; + text-shadow: 1px 1px black; + text-transform: uppercase; + background: gray; + opacity: 75%; + margin: 4px; + line-height: 0.9rem; +} + +/* Hover Actions */ +.extra-network-cards .card:hover .actions { + display: block; +} + +.extra-network-cards .card:hover .overlay .tags { + display: block; +} + +/* No Preview Card Styles */ +.extra-network-cards .card:has(>img[src*="card-no-preview.png"])::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + mix-blend-mode: multiply; + background-color: var(--data-color); +} + +/* Card List Styles */ +.extra-network-cards .card-list { + display: flex; + margin: 0.3em; + padding: 0.3em; + background: var(--input-background-fill); + cursor: pointer; + border-radius: var(--button-large-radius); +} + +.extra-network-cards .card-list .tag { + color: var(--primary-500); + margin-left: 0.8em; +} + +/* Correction color picker styling */ +#txt2img_hdr_color_picker label input { + width: 100%; + height: 100%; +} + +/* Token counters styling */ + +#txt2img_token_counter, #txt2img_negative_token_counter { + display: flex; + flex-direction: column; + justify-content: space-evenly; + padding: 5px; +} + +#txt2img_prompt_container { + margin: 5px; + padding: 0px; +} + +#text2img_prompt label, #text2img_neg_prompt label { + margin: 0px; +} + +/* Based on Gradio Built-in Dark Theme */ +:root, +.light, +.dark { + --body-background-fill: var(--background-color); + --color-accent-soft: var(--neutral-700); + --background-fill-secondary: none; + --border-color-accent: var(--background-color); + --border-color-primary: var(--background-color); + --link-text-color-active: var(--primary-500); + --link-text-color: var(--secondary-500); + --link-text-color-hover: var(--secondary-400); + --link-text-color-visited: var(--secondary-600); + --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: 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); + --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-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: 400; + --slider-color: var(--neutral-900); + --table-radius: var(--radius-lg); + --button-large-padding: 2px 6px; + --button-large-radius: var(--radius-lg); + --button-large-text-size: var(--text-lg); + --button-large-text-weight: 400; + --button-shadow: none; + --button-shadow-active: none; + --button-shadow-hover: none; + --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; + --size-9: 64px; + --size-14: 64px; +} \ No newline at end of file diff --git a/javascript/black-teal.css b/javascript/black-teal.css index c6f266c54..2ebf32e96 100644 --- a/javascript/black-teal.css +++ b/javascript/black-teal.css @@ -134,7 +134,7 @@ svg.feather.feather-image, .feather .feather-image { display: none } .gallery-item { box-shadow: none !important; } .performance { color: #888; } .extra-networks { border-left: 2px solid var(--highlight-color) !important; padding-left: 4px; } -.image-buttons { gap: 10px !important; justify-content: center; } +.image-buttons { justify-content: center; gap: 0 !important; } .image-buttons > button { max-width: 160px; } .tooltip { background: var(--primary-300); color: black; border: none; border-radius: var(--radius-lg) } #system_row > button, #settings_row > button, #config_row > button { max-width: 10em; } diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 77fe125f3..1d1bcfb24 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -3,19 +3,6 @@ let sortVal = -1; // helpers -const requestGet = (url, data, handler) => { - const xhr = new XMLHttpRequest(); - const args = Object.keys(data).map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(data[k])}`).join('&'); - xhr.open('GET', `${url}?${args}`, true); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 200) handler(JSON.parse(xhr.responseText)); - else console.error(`Request: url=${url} status=${xhr.status} err`); - } - }; - xhr.send(JSON.stringify(data)); -}; - const getENActiveTab = () => { let tabName = ''; if (gradioApp().getElementById('tab_txt2img').style.display === 'block') tabName = 'txt2img'; @@ -98,7 +85,7 @@ function readCardTags(el, tags) { } function readCardDescription(page, item) { - requestGet('/sd_extra_networks/description', { page, item }, (data) => { + xhrGet('/sd_extra_networks/description', { page, item }, (data) => { const tabname = getENActiveTab(); const description = gradioApp().querySelector(`#${tabname}_description > label > textarea`); description.value = data?.description?.trim() || ''; @@ -447,6 +434,22 @@ function setupExtraNetworksForTab(tabname) { }; } + // auto-resize networks sidebar + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + for (const el of Array.from(gradioApp().getElementById(`${tabname}_extra_tabs`).querySelectorAll('.extra-networks-page'))) { + const h = Math.trunc(entry.contentRect.height); + if (h <= 0) return; + if (window.opts.extra_networks_card_cover === 'sidebar' && window.opts.theme_type === 'Standard') el.style.height = `max(55vh, ${h - 90}px)`; + // log(`${tabname} height: ${entry.target.id}=${h} ${el.id}=${el.clientHeight}`); + } + } + }); + const settingsEl = gradioApp().getElementById(`${tabname}_settings`); + const interfaceEl = gradioApp().getElementById(`${tabname}_interface`); + if (settingsEl) resizeObserver.observe(settingsEl); + if (interfaceEl) resizeObserver.observe(interfaceEl); + // en style if (!en) return; let lastView; diff --git a/javascript/gallery.js b/javascript/gallery.js index 1f3afd148..05e594e4c 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -94,14 +94,14 @@ async function delayFetchThumb(fn) { outstanding++; const res = await fetch(`/sdapi/v1/browser/thumb?file=${encodeURI(fn)}`, { priority: 'low' }); if (!res.ok) { - console.error(res.statusText); + error(`fetchThumb: ${res.statusText}`); outstanding--; return undefined; } const json = await res.json(); outstanding--; if (!res || !json || json.error || Object.keys(json).length === 0) { - if (json.error) console.error(json.error); + if (json.error) error(`fetchThumb: ${json.error}`); return undefined; } return json; diff --git a/javascript/imageMaskFix.js b/javascript/imageMaskFix.js deleted file mode 100644 index fd37caf90..000000000 --- a/javascript/imageMaskFix.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * temporary fix for https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/668 - * @see https://github.com/gradio-app/gradio/issues/1721 - */ -function imageMaskResize() { - const canvases = gradioApp().querySelectorAll('#img2maskimg .touch-none canvas'); - if (!canvases.length) { - window.removeEventListener('resize', imageMaskResize); - return; - } - const wrapper = canvases[0].closest('.touch-none'); - const previewImage = wrapper.previousElementSibling; - if (!previewImage.complete) { - previewImage.addEventListener('load', imageMaskResize); - return; - } - const w = previewImage.width; - const h = previewImage.height; - const nw = previewImage.naturalWidth; - const nh = previewImage.naturalHeight; - const portrait = nh > nw; - const wW = Math.min(w, portrait ? h / nh * nw : w / nw * nw); - const wH = Math.min(h, portrait ? h / nh * nh : w / nw * nh); - wrapper.style.width = `${wW}px`; - wrapper.style.height = `${wH}px`; - wrapper.style.left = '0px'; - wrapper.style.top = '0px'; - canvases.forEach((c) => { - c.style.width = ''; - c.style.height = ''; - c.style.maxWidth = '100%'; - c.style.maxHeight = '100%'; - c.style.objectFit = 'contain'; - }); -} - -onAfterUiUpdate(imageMaskResize); -window.addEventListener('resize', imageMaskResize); diff --git a/javascript/loader.js b/javascript/loader.js index f3c7fe60f..8cd4811bf 100644 --- a/javascript/loader.js +++ b/javascript/loader.js @@ -20,7 +20,7 @@ async function preloadImages() { try { await Promise.all(imagePromises); } catch (error) { - console.error('Error preloading images:', error); + error(`preloadImages: ${error}`); } } @@ -43,14 +43,16 @@ async function createSplash() { const motdEl = document.getElementById('motd'); if (motdEl) motdEl.innerHTML = text.replace(/["]+/g, ''); }) - .catch((err) => console.error('getMOTD:', err)); + .catch((err) => error(`getMOTD: ${err}`)); } async function removeSplash() { const splash = document.getElementById('splash'); if (splash) splash.remove(); log('removeSplash'); - log('startupTime', Math.round(performance.now() - appStartTime) / 1000); + const t = Math.round(performance.now() - appStartTime) / 1000; + log('startupTime', t); + xhrPost('/sdapi/v1/log', { message: `ready time=${t}` }); } window.onload = createSplash; diff --git a/javascript/logMonitor.js b/javascript/logMonitor.js index e4fe99a7f..9b915e6da 100644 --- a/javascript/logMonitor.js +++ b/javascript/logMonitor.js @@ -2,6 +2,7 @@ let logMonitorEl = null; let logMonitorStatus = true; let logWarnings = 0; let logErrors = 0; +let logConnected = false; function dateToStr(ts) { const dt = new Date(1000 * ts); @@ -29,8 +30,7 @@ async function logMonitor() { row.innerHTML = `${dateToStr(l.created)}${level}${l.facility}${module}${l.msg}`; logMonitorEl.appendChild(row); } catch (e) { - // console.log('logMonitor', e); - console.error('logMonitor line', line); + error(`logMonitor: ${line}`); } }; @@ -46,6 +46,7 @@ async function logMonitor() { if (logMonitorStatus) setTimeout(logMonitor, opts.logmonitor_refresh_period); else setTimeout(logMonitor, 10 * 1000); // on failure try to reconnect every 10sec + if (!opts.logmonitor_show) return; logMonitorStatus = false; if (!logMonitorEl) { @@ -64,14 +65,20 @@ async function logMonitor() { const lines = await res.json(); if (logMonitorEl && lines?.length > 0) logMonitorEl.parentElement.parentElement.style.display = opts.logmonitor_show ? 'block' : 'none'; for (const line of lines) addLogLine(line); + if (!logConnected) { + logConnected = true; + xhrPost('/sdapi/v1/log', { debug: 'connected' }); + } } else { - addLogLine(`{ "created": ${Date.now()}, "level":"ERROR", "module":"logMonitor", "facility":"ui", "msg":"Failed to fetch log: ${res?.status} ${res?.statusText}" }`); + logConnected = false; logErrors++; + addLogLine(`{ "created": ${Date.now()}, "level":"ERROR", "module":"logMonitor", "facility":"ui", "msg":"Failed to fetch log: ${res?.status} ${res?.statusText}" }`); } cleanupLog(atBottom); } catch (err) { - addLogLine(`{ "created": ${Date.now()}, "level":"ERROR", "module":"logMonitor", "facility":"ui", "msg":"Failed to fetch log: server unreachable" }`); + logConnected = false; logErrors++; + addLogLine(`{ "created": ${Date.now()}, "level":"ERROR", "module":"logMonitor", "facility":"ui", "msg":"Failed to fetch log: server unreachable" }`); cleanupLog(atBottom); } } diff --git a/javascript/logger.js b/javascript/logger.js new file mode 100644 index 000000000..1677fa537 --- /dev/null +++ b/javascript/logger.js @@ -0,0 +1,66 @@ +const log = async (...msg) => { + const dt = new Date(); + const ts = `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}.${dt.getMilliseconds().toString().padStart(3, '0')}`; + if (window.logger) window.logger.innerHTML += window.logPrettyPrint(...msg); + console.log(ts, ...msg); // eslint-disable-line no-console +}; + +const debug = async (...msg) => { + const dt = new Date(); + const ts = `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}.${dt.getMilliseconds().toString().padStart(3, '0')}`; + if (window.logger) window.logger.innerHTML += window.logPrettyPrint(...msg); + console.debug(ts, ...msg); // eslint-disable-line no-console +}; + +const error = async (...msg) => { + const dt = new Date(); + const ts = `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}.${dt.getMilliseconds().toString().padStart(3, '0')}`; + if (window.logger) window.logger.innerHTML += window.logPrettyPrint(...msg); + console.error(ts, ...msg); // eslint-disable-line no-console + // const txt = msg.join(' '); + // if (!txt.includes('asctime') && !txt.includes('xhr.')) xhrPost('/sdapi/v1/log', { error: txt }); // eslint-disable-line no-use-before-define +}; + +const xhrInternal = (xhrObj, data, handler = undefined, errorHandler = undefined, ignore = false, serverTimeout = 5000) => { + const err = (msg) => { + if (!ignore) { + error(`${msg}: state=${xhrObj.readyState} status=${xhrObj.status} response=${xhrObj.responseText}`); + if (errorHandler) errorHandler(xhrObj); + } + }; + + xhrObj.setRequestHeader('Content-Type', 'application/json'); + xhrObj.timeout = serverTimeout; + xhrObj.ontimeout = () => err('xhr.ontimeout'); + xhrObj.onerror = () => err('xhr.onerror'); + xhrObj.onabort = () => err('xhr.onabort'); + xhrObj.onreadystatechange = () => { + if (xhrObj.readyState === 4) { + if (xhrObj.status === 200) { + try { + const json = JSON.parse(xhrObj.responseText); + if (handler) handler(json); + } catch (e) { + error(`xhr.onreadystatechange: ${e}`); + } + } else { + err(`xhr.onreadystatechange: state=${xhrObj.readyState} status=${xhrObj.status} response=${xhrObj.responseText}`); + } + } + }; + const req = JSON.stringify(data); + xhrObj.send(req); +}; + +const xhrGet = (url, data, handler = undefined, errorHandler = undefined, ignore = false, serverTimeout = 5000) => { + const xhr = new XMLHttpRequest(); + const args = Object.keys(data).map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(data[k])}`).join('&'); + xhr.open('GET', `${url}?${args}`, true); + xhrInternal(xhr, data, handler, errorHandler, ignore, serverTimeout); +}; + +function xhrPost(url, data, handler = undefined, errorHandler = undefined, ignore = false, serverTimeout = 5000) { + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + xhrInternal(xhr, data, handler, errorHandler, ignore, serverTimeout); +} diff --git a/javascript/notification.js b/javascript/notification.js index 33e8d1c55..c702c90e7 100644 --- a/javascript/notification.js +++ b/javascript/notification.js @@ -4,28 +4,32 @@ let lastHeadImg = null; let notificationButton = null; async function sendNotification() { - if (!notificationButton) { - notificationButton = gradioApp().getElementById('request_notifications'); - if (notificationButton) notificationButton.addEventListener('click', (evt) => Notification.requestPermission(), true); + try { + if (!notificationButton) { + notificationButton = gradioApp().getElementById('request_notifications'); + if (notificationButton) notificationButton.addEventListener('click', (evt) => Notification.requestPermission(), true); + } + if (document.hasFocus()) return; // window is in focus so don't send notifications + let galleryPreviews = gradioApp().querySelectorAll('div[id^="tab_"][style*="display: block"] div[id$="_results"] .thumbnail-item > img'); + if (!galleryPreviews || galleryPreviews.length === 0) galleryPreviews = gradioApp().querySelectorAll('.thumbnail-item > img'); + if (!galleryPreviews || galleryPreviews.length === 0) return; + const headImg = galleryPreviews[0]?.src; + if (!headImg || headImg === lastHeadImg || headImg.includes('logo-bg-')) return; + const audioNotification = gradioApp().querySelector('#audio_notification audio'); + if (audioNotification) audioNotification.play(); + lastHeadImg = headImg; + const imgs = new Set(Array.from(galleryPreviews).map((img) => img.src)); // Multiple copies of the images are in the DOM when one is selected + const notification = new Notification('SD.Next', { + body: `Generated ${imgs.size > 1 ? imgs.size - opts.return_grid : 1} image${imgs.size > 1 ? 's' : ''}`, + icon: headImg, + image: headImg, + }); + notification.onclick = () => { + parent.focus(); + this.close(); + }; + log('sendNotifications'); + } catch (e) { + error(`sendNotification: ${e}`); } - if (document.hasFocus()) return; // window is in focus so don't send notifications - let galleryPreviews = gradioApp().querySelectorAll('div[id^="tab_"][style*="display: block"] div[id$="_results"] .thumbnail-item > img'); - if (!galleryPreviews || galleryPreviews.length === 0) galleryPreviews = gradioApp().querySelectorAll('.thumbnail-item > img'); - if (!galleryPreviews || galleryPreviews.length === 0) return; - const headImg = galleryPreviews[0]?.src; - if (!headImg || headImg === lastHeadImg || headImg.includes('logo-bg-')) return; - const audioNotification = gradioApp().querySelector('#audio_notification audio'); - if (audioNotification) audioNotification.play(); - lastHeadImg = headImg; - const imgs = new Set(Array.from(galleryPreviews).map((img) => img.src)); // Multiple copies of the images are in the DOM when one is selected - const notification = new Notification('SD.Next', { - body: `Generated ${imgs.size > 1 ? imgs.size - opts.return_grid : 1} image${imgs.size > 1 ? 's' : ''}`, - icon: headImg, - image: headImg, - }); - notification.onclick = () => { - parent.focus(); - this.close(); - }; - log('sendNotifications'); } diff --git a/javascript/progressBar.js b/javascript/progressBar.js index a9ecb31e9..c385fe5db 100644 --- a/javascript/progressBar.js +++ b/javascript/progressBar.js @@ -1,28 +1,5 @@ let lastState = {}; -function request(url, data, handler, errorHandler) { - const xhr = new XMLHttpRequest(); - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - try { - const js = JSON.parse(xhr.responseText); - handler(js); - } catch (error) { - console.error(error); - errorHandler(); - } - } else { - errorHandler(); - } - } - }; - const js = JSON.stringify(data); - xhr.send(js); -} - function pad2(x) { return x < 10 ? `0${x}` : x; } @@ -35,8 +12,10 @@ function formatTime(secs) { function checkPaused(state) { lastState.paused = state ? !state : !lastState.paused; - document.getElementById('txt2img_pause').innerText = lastState.paused ? 'Resume' : 'Pause'; - document.getElementById('img2img_pause').innerText = lastState.paused ? 'Resume' : 'Pause'; + const t_el = document.getElementById('txt2img_pause'); + const i_el = document.getElementById('img2img_pause'); + if (t_el) t_el.innerText = lastState.paused ? 'Resume' : 'Pause'; + if (i_el) i_el.innerText = lastState.paused ? 'Resume' : 'Pause'; } function setProgress(res) { @@ -89,28 +68,38 @@ function requestProgress(id_task, progressEl, galleryEl, atEnd = null, onProgres let img; const initLivePreview = () => { + if (!parentGallery) return; + const footers = Array.from(gradioApp().querySelectorAll('.gallery_footer')); + for (const footer of footers) footer.style.display = 'none'; // remove all footers + const galleries = Array.from(gradioApp().querySelectorAll('.gallery_main')); + for (const gallery of galleries) gallery.style.display = 'none'; // remove all footers + + livePreview = document.createElement('div'); + livePreview.className = 'livePreview'; + parentGallery.insertBefore(livePreview, galleryEl); img = new Image(); - if (parentGallery) { - livePreview = document.createElement('div'); - livePreview.className = 'livePreview'; - parentGallery.insertBefore(livePreview, galleryEl); - const rect = galleryEl.getBoundingClientRect(); - if (rect.width) { - livePreview.style.width = `${rect.width}px`; - livePreview.style.height = `${rect.height}px`; - } - img.onload = () => { - livePreview.appendChild(img); - if (livePreview.childElementCount > 2) livePreview.removeChild(livePreview.firstElementChild); - }; - } + img.id = 'livePreviewImage'; + livePreview.appendChild(img); + img.onload = () => { + img.style.width = `min(100%, max(${img.naturalWidth}px, 512px))`; + parentGallery.style.minHeight = `${img.height}px`; + }; }; const done = () => { debug('taskEnd:', id_task); localStorage.removeItem('task'); setProgress(); - if (parentGallery && livePreview) parentGallery.removeChild(livePreview); + const footers = Array.from(gradioApp().querySelectorAll('.gallery_footer')); + for (const footer of footers) footer.style.display = 'flex'; // restore all footers + const galleries = Array.from(gradioApp().querySelectorAll('.gallery_main')); + for (const gallery of galleries) gallery.style.display = 'flex'; // remove all galleries + try { + if (parentGallery && livePreview) { + parentGallery.removeChild(livePreview); + parentGallery.style.minHeight = 'unset'; + } + } catch { /* ignore */ } checkPaused(true); sendNotification(); if (atEnd) atEnd(); @@ -118,20 +107,32 @@ function requestProgress(id_task, progressEl, galleryEl, atEnd = null, onProgres const start = (id_task, id_live_preview) => { // eslint-disable-line no-shadow if (!opts.live_previews_enable || opts.live_preview_refresh_period === 0 || opts.show_progress_every_n_steps === 0) return; - request('./internal/progress', { id_task, id_live_preview }, (res) => { + + const onProgressHandler = (res) => { + // debug('onProgress', res); lastState = res; const elapsedFromStart = (new Date() - dateStart) / 1000; hasStarted |= res.active; if (res.completed || (!res.active && (hasStarted || once)) || (elapsedFromStart > 30 && !res.queued && res.progress === prevProgress)) { + debug('onProgressEnd', res); done(); return; } setProgress(res); if (res.live_preview && !livePreview) initLivePreview(); - if (res.live_preview && galleryEl) img.src = res.live_preview; + if (res.live_preview && galleryEl) { + if (img.src !== res.live_preview) img.src = res.live_preview; + } if (onProgress) onProgress(res); setTimeout(() => start(id_task, id_live_preview), opts.live_preview_refresh_period || 500); - }, done); + }; + + const onProgressErrorHandler = (err) => { + error(`onProgressError: ${err}`); + done(); + }; + + xhrPost('./internal/progress', { id_task, id_live_preview }, onProgressHandler, onProgressErrorHandler, false, 5000); }; start(id_task, 0); } diff --git a/javascript/script.js b/javascript/script.js index 104567dd7..250e90ba2 100644 --- a/javascript/script.js +++ b/javascript/script.js @@ -1,17 +1,3 @@ -const log = (...msg) => { - const dt = new Date(); - const ts = `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}.${dt.getMilliseconds().toString().padStart(3, '0')}`; - if (window.logger) window.logger.innerHTML += window.logPrettyPrint(...msg); - console.log(ts, ...msg); // eslint-disable-line no-console -}; - -const debug = (...msg) => { - const dt = new Date(); - const ts = `${dt.getHours().toString().padStart(2, '0')}:${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}.${dt.getMilliseconds().toString().padStart(3, '0')}`; - if (window.logger) window.logger.innerHTML += window.logPrettyPrint(...msg); - console.debug(ts, ...msg); // eslint-disable-line no-console -}; - async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); // eslint-disable-line no-promise-executor-return } @@ -82,7 +68,7 @@ function executeCallbacks(queue, arg) { try { callback(arg); } catch (e) { - console.error('error running callback', callback, ':', e); + error(`executeCallbacks: ${callback} ${e}`); } } } diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 08fae2eb8..c5145c973 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -16,7 +16,7 @@ tr { border-bottom: none !important; padding: 0 0.5em !important; } td > div > span { overflow-y: auto; max-height: 3em; overflow-x: hidden; } textarea { overflow-y: auto !important; } span { font-size: var(--text-md) !important; } -button { font-size: var(--text-lg) !important; } +button { font-size: var(--text-lg) !important; min-width: unset !important; } input[type='color'] { width: 64px; height: 32px; } input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { margin-left: 4px; } @@ -30,6 +30,19 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { margin-left .hidden { display: none; } .tabitem { padding: 0 !important; } +/* gradio image/canvas elements */ +.image-container { overflow: auto; } +/* +.gradio-image { min-height: fit-content; } +.gradio-image img { object-fit: contain; } +*/ +/* +.gradio-image { min-height: 200px !important; } +.image-container { height: unset !important; } +.control-image { height: unset !important; } +#img2img_sketch, #img2maskimg, #inpaint_sketch { overflow: overlay !important; resize: auto; background: var(--panel-background-fill); z-index: 5; } +*/ + /* color elements */ .gradio-dropdown, .block.gradio-slider, .block.gradio-checkbox, .block.gradio-textbox, .block.gradio-radio, .block.gradio-checkboxgroup, .block.gradio-number, .block.gradio-colorpicker { border-width: 0 !important; box-shadow: none !important;} .gradio-accordion { padding-top: var(--spacing-md) !important; padding-right: 0 !important; padding-bottom: 0 !important; color: var(--body-text-color); } @@ -83,13 +96,12 @@ button.custom-button { border-radius: var(--button-large-radius); padding: var(- .block.token-counter div{ display: inline; } .block.token-counter span{ padding: 0.1em 0.75em; } .performance { font-size: var(--text-xs); color: #444; } -.performance p { display: inline-block; color: var(--body-text-color-subdued) !important } +.performance p { display: inline-block; color: var(--primary-500) !important } .performance .time { margin-right: 0; } .thumbnails { background: var(--body-background-fill); } -.control-image { height: calc(100vw/3) !important; } .prompt textarea { resize: vertical; } +.grid-wrap { overflow-y: auto !important; } #control_results { margin: 0; padding: 0; } -#control_gallery { height: calc(100vw/3 + 60px); } #txt2img_gallery, #img2img_gallery { height: 50vh; } #control-result { background: var(--button-secondary-background-fill); padding: 0.2em; } #control-inputs { margin-top: 1em; } @@ -105,7 +117,6 @@ button.custom-button { border-radius: var(--button-large-radius); padding: var(- #txt2img_prompt, #txt2img_neg_prompt, #img2img_prompt, #img2img_neg_prompt, #control_prompt, #control_neg_prompt { display: contents; } #txt2img_actions_column, #img2img_actions_column, #control_actions { flex-flow: wrap; justify-content: space-between; } - .interrogate-clip { position: absolute; right: 6em; top: 8px; max-width: fit-content; background: none !important; z-index: 50; } .interrogate-blip { position: absolute; right: 4em; top: 8px; max-width: fit-content; background: none !important; z-index: 50; } .interrogate-col { min-width: 0 !important; max-width: fit-content; margin-right: var(--spacing-xxl); } @@ -118,11 +129,9 @@ div#extras_scale_to_tab div.form { flex-direction: row; } #img2img_unused_scale_by_slider { visibility: hidden; width: 0.5em; max-width: 0.5em; min-width: 0.5em; } .inactive{ opacity: 0.5; } div#extras_scale_to_tab div.form { flex-direction: row; } -#mode_img2img .gradio-image>div.fixed-height, #mode_img2img .gradio-image>div.fixed-height img{ height: 480px !important; max-height: 480px !important; min-height: 480px !important; } -#img2img_sketch, #img2maskimg, #inpaint_sketch { overflow: overlay !important; resize: auto; background: var(--panel-background-fill); z-index: 5; } .image-buttons button { min-width: auto; } .infotext { overflow-wrap: break-word; line-height: 1.5em; font-size: 0.95em !important; } -.infotext > p { padding-left: 1em; text-indent: -1em; white-space: pre-wrap; color: var(--block-info-text-color) !important; } +.infotext > p { white-space: pre-wrap; color: var(--block-info-text-color) !important; } .tooltip { display: block; position: fixed; top: 1em; right: 1em; padding: 0.5em; background: var(--input-background-fill); color: var(--body-text-color); border: 1pt solid var(--button-primary-border-color); width: 22em; min-height: 1.3em; font-size: var(--text-xs); transition: opacity 0.2s ease-in; pointer-events: none; opacity: 0; z-index: 999; } .tooltip-show { opacity: 0.9; } @@ -158,11 +167,10 @@ div#extras_scale_to_tab div.form { flex-direction: row; } .progressDiv { position: relative; height: 20px; background: #b4c0cc; margin-bottom: -3px; } .dark .progressDiv { background: #424c5b; } .progressDiv .progress { width: 0%; height: 20px; background: #0060df; color: white; font-weight: bold; line-height: 20px; padding: 0 8px 0 0; text-align: right; overflow: visible; white-space: nowrap; padding: 0 0.5em; } -.livePreview { position: absolute; z-index: 50; background-color: transparent; width: -moz-available; width: -webkit-fill-available; } -.livePreview img { position: absolute; object-fit: contain; width: 100%; height: 100%; } -.dark .livePreview { background-color: rgb(17 24 39 / var(--tw-bg-opacity)); } +.livePreview { position: absolute; z-index: 50; width: -moz-available; width: -webkit-fill-available; height: 100%; background-color: var(--background-color); } +.livePreview img { object-fit: contain; width: 100%; justify-self: center; } .popup-metadata { color: white; background: #0000; display: inline-block; white-space: pre-wrap; font-size: var(--text-xxs); } - +.generating { animation: unset !important; border: unset !important; } /* fullpage image viewer */ #lightboxModal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; overflow: hidden; background-color: rgba(20, 20, 20, 0.75); backdrop-filter: blur(6px); user-select: none; -webkit-user-select: none; flex-direction: row; font-family: 'NotoSans';} @@ -380,8 +388,6 @@ div:has(>#tab-gallery-folders) { flex-grow: 0 !important; background-color: var( #img2img_actions_column { display: flex; min-width: fit-content !important; flex-direction: row;justify-content: space-evenly; align-items: center;} #txt2img_generate_box, #img2img_generate_box, #txt2img_enqueue_wrapper,#img2img_enqueue_wrapper {display: flex;flex-direction: column;height: 4em !important;align-items: stretch;justify-content: space-evenly;} #img2img_interface, #img2img_results, #img2img_footer p { text-wrap: wrap; min-width: 100% !important; max-width: 100% !important;} /* maintain single column for from image operations on larger mobile devices */ - #img2img_sketch, #img2maskimg, #inpaint_sketch {display: flex; overflow: auto !important; resize: none !important; } /* fix inpaint image display being too large for mobile displays */ - #img2maskimg canvas { width: auto !important; max-height: 100% !important; height: auto !important; } #txt2img_sampler, #txt2img_batch, #txt2img_seed_group, #txt2img_advanced, #txt2img_second_pass, #img2img_sampling_group, #img2img_resize_group, #img2img_batch_group, #img2img_seed_group, #img2img_denoise_group, #img2img_advanced_group { width: 100% !important; } /* fix from text/image UI elements to prevent them from moving around within the UI */ #img2img_resize_group .gradio-radio>div { display: flex; flex-direction: column; width: unset !important; } #inpaint_controls div { display:flex;flex-direction: row;} diff --git a/javascript/ui.js b/javascript/ui.js index 8808f1c8b..3e3f14390 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -28,7 +28,7 @@ function clip_gallery_urls(gallery) { const files = gallery.map((v) => v.data); navigator.clipboard.writeText(JSON.stringify(files)).then( () => log('clipboard:', files), - (err) => console.error('clipboard:', files, err), + (err) => error(`clipboard: ${files} ${err}`), ); } @@ -139,7 +139,7 @@ function switch_to_inpaint(...args) { return Array.from(arguments); } -function switch_to_inpaint_sketch(...args) { +function switch_to_composite(...args) { switchToTab('Image'); switch_to_img2img_tab(3); return Array.from(arguments); @@ -493,9 +493,9 @@ function previewTheme() { el.src = `/file=html/${name}.jpg`; } }) - .catch((e) => console.error('previewTheme:', e)); + .catch((e) => error(`previewTheme: ${e}`)); }) - .catch((e) => console.error('previewTheme:', e)); + .catch((e) => error(`previewTheme: ${e}`)); } async function browseFolder() { diff --git a/launch.py b/launch.py index f944a7e54..e00da58c7 100755 --- a/launch.py +++ b/launch.py @@ -55,9 +55,11 @@ def get_custom_args(): if 'PS1' in env: del env['PS1'] installer.log.trace(f'Environment: {installer.print_dict(env)}') - else: - env = [f'{k}={v}' for k, v in os.environ.items() if k.startswith('SD_')] - installer.log.debug(f'Env flags: {env}') + env = [f'{k}={v}' for k, v in os.environ.items() if k.startswith('SD_')] + installer.log.debug(f'Env flags: {env}') + ldd = os.environ.get('LD_PRELOAD', None) + if ldd is not None: + installer.log.debug(f'Linker flags: "{ldd}"') @lru_cache() diff --git a/modules/api/api.py b/modules/api/api.py index f8346995d..d48cbf521 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -35,7 +35,8 @@ class Api: # server api self.add_api_route("/sdapi/v1/motd", server.get_motd, methods=["GET"], response_model=str) - self.add_api_route("/sdapi/v1/log", server.get_log_buffer, methods=["GET"], response_model=List[str]) + self.add_api_route("/sdapi/v1/log", server.get_log, methods=["GET"], response_model=List[str]) + self.add_api_route("/sdapi/v1/log", server.post_log, methods=["POST"]) self.add_api_route("/sdapi/v1/start", self.get_session_start, methods=["GET"]) self.add_api_route("/sdapi/v1/version", server.get_version, methods=["GET"]) self.add_api_route("/sdapi/v1/status", server.get_status, methods=["GET"], response_model=models.ResStatus) diff --git a/modules/api/control.py b/modules/api/control.py index 29c5a77f1..345930341 100644 --- a/modules/api/control.py +++ b/modules/api/control.py @@ -159,6 +159,8 @@ class APIControl(): output_images = [] output_processed = [] output_info = '' + # TODO control script process + # init script args, call scripts.script_control.run, call scripts.script_control.after run.control_set({ 'do_not_save_grid': not req.save_images, 'do_not_save_samples': not req.save_images, **self.prepare_ip_adapter(req) }) run.control_set(getattr(req, "extra", {})) res = run.control_run(**args) diff --git a/modules/api/generate.py b/modules/api/generate.py index b8ee645a4..9b409a14b 100644 --- a/modules/api/generate.py +++ b/modules/api/generate.py @@ -116,6 +116,8 @@ class APIGenerate(): processed = scripts.scripts_txt2img.run(p, *script_args) # Need to pass args as list here else: processed = process_images(p) + processed = scripts.scripts_txt2img.after(p, processed, *script_args) + p.close() shared.state.end(api=False) if processed is None or processed.images is None or len(processed.images) == 0: b64images = [] @@ -166,6 +168,8 @@ class APIGenerate(): processed = scripts.scripts_img2img.run(p, *script_args) # Need to pass args as list here else: processed = process_images(p) + processed = scripts.scripts_img2img.after(p, processed, *script_args) + p.close() shared.state.end(api=False) if processed is None or processed.images is None or len(processed.images) == 0: b64images = [] diff --git a/modules/api/models.py b/modules/api/models.py index e68ebf081..39bcbe383 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -286,10 +286,16 @@ class ResImageInfo(BaseModel): items: dict = Field(title="Items", description="A dictionary containing all the other fields the image had") parameters: dict = Field(title="Parameters", description="A dictionary with parsed generation info fields") -class ReqLog(BaseModel): +class ReqGetLog(BaseModel): lines: int = Field(default=100, title="Lines", description="How many lines to return") clear: bool = Field(default=False, title="Clear", description="Should the log be cleared after returning the lines?") + +class ReqPostLog(BaseModel): + message: Optional[str] = Field(title="Message", description="The info message to log") + debug: Optional[str] = Field(title="Debug message", description="The debug message to log") + error: Optional[str] = Field(title="Error message", description="The error message to log") + class ReqProgress(BaseModel): skip_current_image: bool = Field(default=False, title="Skip current image", description="Skip current image serialization") diff --git a/modules/api/server.py b/modules/api/server.py index 939e19c86..dabbe634c 100644 --- a/modules/api/server.py +++ b/modules/api/server.py @@ -37,12 +37,22 @@ def get_platform(): from modules.loader import get_packages as loader_get_packages return { **installer_get_platform(), **loader_get_packages() } -def get_log_buffer(req: models.ReqLog = Depends()): +def get_log(req: models.ReqGetLog = Depends()): lines = shared.log.buffer[:req.lines] if req.lines > 0 else shared.log.buffer.copy() if req.clear: shared.log.buffer.clear() return lines +def post_log(req: models.ReqPostLog): + if req.message is not None: + shared.log.info(f'UI: {req.message}') + if req.debug is not None: + shared.log.debug(f'UI: {req.debug}') + if req.error is not None: + shared.log.error(f'UI: {req.error}') + return {} + + def get_config(): options = {} for k in shared.opts.data.keys(): diff --git a/modules/call_queue.py b/modules/call_queue.py index 4065d13d9..cdc2fe1f7 100644 --- a/modules/call_queue.py +++ b/modules/call_queue.py @@ -2,7 +2,7 @@ import html import threading import time import cProfile -from modules import shared, progress, errors +from modules import shared, progress, errors, timer queue_lock = threading.Lock() @@ -73,15 +73,16 @@ def wrap_gradio_call(func, extra_outputs=None, add_stats=False, name=None): elapsed_m = int(elapsed // 60) elapsed_s = elapsed % 60 elapsed_text = f"{elapsed_m}m {elapsed_s:.2f}s" if elapsed_m > 0 else f"{elapsed_s:.2f}s" + summary = timer.process.summary(min_time=0.1, total=False).replace('=', ' ') vram_html = '' if not shared.mem_mon.disabled: vram = {k: -(v//-(1024*1024)) for k, v in shared.mem_mon.read().items()} + used = round(100 * vram['used'] / (vram['total'] + 0.001)) if vram.get('active_peak', 0) > 0: - vram_html = " |

" - vram_html += f"GPU active {max(vram['active_peak'], vram['reserved_peak'])} MB reserved {vram['reserved']} | used {vram['used']} MB free {vram['free']} MB total {vram['total']} MB" + vram_html = " | " + vram_html += f"GPU {max(vram['active_peak'], vram['reserved_peak'])} MB {used}%" vram_html += f" | retries {vram['retries']} oom {vram['oom']}" if vram.get('retries', 0) > 0 or vram.get('oom', 0) > 0 else '' - vram_html += "

" if isinstance(res, list): - res[-1] += f"

Time: {elapsed_text}

{vram_html}
" + res[-1] += f"

Time: {elapsed_text} | {summary}{vram_html}

" return tuple(res) return f diff --git a/modules/control/run.py b/modules/control/run.py index 5d6343c98..2fe13dd73 100644 --- a/modules/control/run.py +++ b/modules/control/run.py @@ -87,7 +87,6 @@ def control_run(state: str = '', u.process.override = u.override global instance, pipe, original_pipeline # pylint: disable=global-statement - t_start = time.time() debug(f'Control: type={unit_type} input={inputs} init={inits} type={input_type}') if inputs is None or (type(inputs) is list and len(inputs) == 0): inputs = [None] @@ -717,14 +716,11 @@ def control_run(state: str = '', shared.log.error(f'Control pipeline failed: type={unit_type} units={len(active_model)} error={e}') errors.display(e, 'Control') - t_end = time.time() - if len(output_images) == 0: output_images = None image_txt = '| Images None' else: - image_str = [f'{image.width}x{image.height}' for image in output_images] - image_txt = f'| Time {t_end-t_start:.2f}s | Images {len(output_images)} | Size {" ".join(image_str)}' + image_txt = '' p.init_images = output_images # may be used for hires if video_type != 'None' and isinstance(output_images, list): @@ -738,7 +734,7 @@ def control_run(state: str = '', restore_pipeline() debug(f'Ready: {image_txt}') - html_txt = f'

Ready {image_txt}

' + html_txt = f'

Ready {image_txt}

' if image_txt != '' else '' if len(info_txt) > 0: html_txt = html_txt + infotext_to_html(info_txt[0]) if is_generator: diff --git a/modules/control/units/controlnet.py b/modules/control/units/controlnet.py index 20b99412a..3f68a4896 100644 --- a/modules/control/units/controlnet.py +++ b/modules/control/units/controlnet.py @@ -85,6 +85,9 @@ predefined_f1 = { "XLabs-AI HED": 'XLabs-AI/flux-controlnet-hed-diffusers' } predefined_sd3 = { + "StabilityAI Canny": 'diffusers-internal-dev/sd35-controlnet-canny-8b', + "StabilityAI Depth": 'diffusers-internal-dev/sd35-controlnet-depth-8b', + "StabilityAI Blur": 'diffusers-internal-dev/sd35-controlnet-blur-8b', "InstantX Canny": 'InstantX/SD3-Controlnet-Canny', "InstantX Pose": 'InstantX/SD3-Controlnet-Pose', "InstantX Depth": 'InstantX/SD3-Controlnet-Depth', diff --git a/modules/devices.py b/modules/devices.py index 56ac50091..9ca1863a5 100644 --- a/modules/devices.py +++ b/modules/devices.py @@ -471,7 +471,7 @@ def set_cuda_params(): device_name = get_raw_openvino_device() else: device_name = torch.device(get_optimal_device_name()) - log.info(f'Torch parameters: backend={backend} device={device_name} config={opts.cuda_dtype} dtype={dtype} vae={dtype_vae} unet={dtype_unet} context={inference_context.__name__} nohalf={opts.no_half} nohalfvae={opts.no_half_vae} upscast={opts.upcast_sampling} deterministic={opts.cudnn_deterministic} test-fp16={fp16_ok} test-bf16={bf16_ok} optimization="{opts.cross_attention_optimization}"') + log.info(f'Torch parameters: backend={backend} device={device_name} config={opts.cuda_dtype} dtype={dtype} vae={dtype_vae} unet={dtype_unet} context={inference_context.__name__} nohalf={opts.no_half} nohalfvae={opts.no_half_vae} upcast={opts.upcast_sampling} deterministic={opts.cudnn_deterministic} test-fp16={fp16_ok} test-bf16={bf16_ok} optimization="{opts.cross_attention_optimization}"') def cond_cast_unet(tensor): diff --git a/modules/img2img.py b/modules/img2img.py index 8274386cc..077df1259 100644 --- a/modules/img2img.py +++ b/modules/img2img.py @@ -164,12 +164,7 @@ def img2img(id_task: str, state: str, mode: int, return [], '', '', 'Error: init image not provided' image = init_img.convert("RGB") mask = None - elif mode == 1: # img2img sketch - if sketch is None: - return [], '', '', 'Error: sketch image not provided' - image = sketch.convert("RGB") - mask = None - elif mode == 2: # inpaint + elif mode == 1: # inpaint if init_img_with_mask is None: return [], '', '', 'Error: init image with mask not provided' image = init_img_with_mask["image"] @@ -177,7 +172,12 @@ def img2img(id_task: str, state: str, mode: int, alpha_mask = ImageOps.invert(image.split()[-1]).convert('L').point(lambda x: 255 if x > 0 else 0, mode='1') mask = ImageChops.lighter(alpha_mask, mask.convert('L')).convert('L') image = image.convert("RGB") - elif mode == 3: # inpaint sketch + elif mode == 2: # sketch + if sketch is None: + return [], '', '', 'Error: sketch image not provided' + image = sketch.convert("RGB") + mask = None + elif mode == 3: # composite if inpaint_color_sketch is None: return [], '', '', 'Error: color sketch image not provided' image = inpaint_color_sketch diff --git a/modules/memmon.py b/modules/memmon.py index 6887e1e1c..d9fa3963d 100644 --- a/modules/memmon.py +++ b/modules/memmon.py @@ -42,14 +42,14 @@ class MemUsageMonitor(): if not self.disabled: try: self.data["free"], self.data["total"] = torch.cuda.mem_get_info(self.device.index if self.device.index is not None else torch.cuda.current_device()) + self.data["used"] = self.data["total"] - self.data["free"] torch_stats = torch.cuda.memory_stats(self.device) - self.data["active"] = torch_stats["active.all.current"] + self.data["active"] = torch_stats.get("active.all.current", torch_stats["active_bytes.all.current"]) self.data["active_peak"] = torch_stats["active_bytes.all.peak"] self.data["reserved"] = torch_stats["reserved_bytes.all.current"] self.data["reserved_peak"] = torch_stats["reserved_bytes.all.peak"] - self.data['retries'] = torch_stats["num_alloc_retries"] - self.data['oom'] = torch_stats["num_ooms"] - self.data["used"] = self.data["total"] - self.data["free"] + self.data['retries'] = torch_stats.get("num_alloc_retries", -1) + self.data['oom'] = torch_stats.get("num_ooms", -1) except Exception: self.disabled = True return self.data diff --git a/modules/model_flux.py b/modules/model_flux.py index 17234d9a4..324e50b36 100644 --- a/modules/model_flux.py +++ b/modules/model_flux.py @@ -306,9 +306,17 @@ def load_flux(checkpoint_info, diffusers_load_config): # triggered by opts.sd_ch model_te.loaded_te = shared.opts.sd_text_encoder if vae is not None: kwargs['vae'] = vae - shared.log.debug(f'Load model: type=FLUX preloaded={list(kwargs)}') if repo_id == 'sayakpaul/flux.1-dev-nf4': repo_id = 'black-forest-labs/FLUX.1-dev' # workaround since sayakpaul model is missing model_index.json + if 'Fill' in repo_id: + cls = diffusers.FluxFillPipeline + elif 'Canny' in repo_id: + cls = diffusers.FluxControlPipeline + elif 'Depth' in repo_id: + cls = diffusers.FluxControlPipeline + else: + cls = diffusers.FluxPipeline + shared.log.debug(f'Load model: type=FLUX cls={cls.__name__} preloaded={list(kwargs)} revision={diffusers_load_config.get("revision", None)}') for c in kwargs: if kwargs[c].dtype == torch.float32 and devices.dtype != torch.float32: shared.log.warning(f'Load model: type=FLUX component={c} dtype={kwargs[c].dtype} cast dtype={devices.dtype} recast') @@ -319,7 +327,7 @@ def load_flux(checkpoint_info, diffusers_load_config): # triggered by opts.sd_ch if checkpoint_info.path.endswith('.safetensors') and os.path.isfile(checkpoint_info.path): pipe = diffusers.FluxPipeline.from_single_file(checkpoint_info.path, cache_dir=shared.opts.diffusers_dir, **kwargs, **diffusers_load_config) else: - pipe = diffusers.FluxPipeline.from_pretrained(repo_id, cache_dir=shared.opts.diffusers_dir, **kwargs, **diffusers_load_config) + pipe = cls.from_pretrained(repo_id, cache_dir=shared.opts.diffusers_dir, **kwargs, **diffusers_load_config) # release memory transformer = None diff --git a/modules/model_quant.py b/modules/model_quant.py index 0e7bdd4b3..9482fe898 100644 --- a/modules/model_quant.py +++ b/modules/model_quant.py @@ -5,6 +5,7 @@ from installer import install, log bnb = None quanto = None +ao = None def create_bnb_config(kwargs = None, allow_bnb: bool = True): @@ -12,6 +13,8 @@ def create_bnb_config(kwargs = None, allow_bnb: bool = True): if len(shared.opts.bnb_quantization) > 0 and allow_bnb: if 'Model' in shared.opts.bnb_quantization: load_bnb() + if bnb is None: + return kwargs bnb_config = diffusers.BitsAndBytesConfig( load_in_8bit=shared.opts.bnb_quantization_type in ['fp8'], load_in_4bit=shared.opts.bnb_quantization_type in ['nf4', 'fp4'], @@ -28,6 +31,44 @@ def create_bnb_config(kwargs = None, allow_bnb: bool = True): return kwargs +def create_ao_config(kwargs = None, allow_ao: bool = True): + from modules import shared + if len(shared.opts.torchao_quantization) > 0 and shared.opts.torchao_quantization_mode == 'pre' and allow_ao: + if 'Model' in shared.opts.torchao_quantization: + load_torchao() + if ao is None: + return kwargs + ao_config = {} + # ao_config = diffusers.TorchAoConfig("int8wo") # TODO torchao + shared.log.debug(f'Quantization: module=all type=bnb dtype={shared.opts.torchao_quantization_type}') + if kwargs is None: + return ao_config + else: + kwargs['quantization_config'] = ao_config + return kwargs + return kwargs + + +def load_torchao(msg='', silent=False): + global ao # pylint: disable=global-statement + if ao is not None: + return ao + install('torchao', quiet=True) + try: + import torchao + ao = torchao + fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access + log.debug(f'Quantization: type=quanto version={ao.__version__} fn={fn}') # pylint: disable=protected-access + return ao + except Exception as e: + if len(msg) > 0: + log.error(f"{msg} failed to import optimum.quanto: {e}") + ao = None + if not silent: + raise + return None + + def load_bnb(msg='', silent=False): global bnb # pylint: disable=global-statement if bnb is not None: diff --git a/modules/model_sd3.py b/modules/model_sd3.py index b9d579085..ba036760a 100644 --- a/modules/model_sd3.py +++ b/modules/model_sd3.py @@ -150,6 +150,7 @@ def load_sd3(checkpoint_info, cache_dir=None, config=None): shared.log.debug(f'Load model: type=SD3 kwargs={list(kwargs)} repo="{repo_id}"') kwargs = model_quant.create_bnb_config(kwargs) + kwargs = model_quant.create_ao_config(kwargs) pipe = loader( repo_id, torch_dtype=devices.dtype, diff --git a/modules/modelloader.py b/modules/modelloader.py index ce36a739b..b1b3930d6 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -326,6 +326,9 @@ def find_diffuser(name: str, full=False): return [repo[0]['name']] hf_api = hf.HfApi() models = list(hf_api.list_models(model_name=name, library=['diffusers'], full=True, limit=20, sort="downloads", direction=-1)) + if len(models) == 0: + models = list(hf_api.list_models(model_name=name, full=True, limit=20, sort="downloads", direction=-1)) # widen search + models = [m for m in models if m.id.startswith(name)] # filter exact shared.log.debug(f'Searching diffusers models: {name} {len(models) > 0}') if len(models) > 0: if not full: diff --git a/modules/postprocess/yolo.py b/modules/postprocess/yolo.py index f42b6bb9f..5deab1282 100644 --- a/modules/postprocess/yolo.py +++ b/modules/postprocess/yolo.py @@ -72,7 +72,7 @@ class YoloRestorer(Detailer): imgsz: int = 640, half: bool = True, device = devices.device, - augment: bool = True, + augment: bool = shared.opts.detailer_augment, agnostic: bool = False, retina: bool = False, mask: bool = True, diff --git a/modules/processing.py b/modules/processing.py index 0d557e64e..16e7a9213 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -323,7 +323,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: processed = p.scripts.process_images(p) if processed is not None: samples = processed.images - infotexts = processed.infotexts + infotexts += processed.infotexts if samples is None: if not shared.native: from modules.processing_original import process_original @@ -393,11 +393,8 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if shared.opts.mask_apply_overlay: image = apply_overlay(image, p.paste_to, i, p.overlay_images) - if len(infotexts) > i: - info = infotexts[i] - else: - info = create_infotext(p, p.prompts, p.seeds, p.subseeds, index=i, all_negative_prompts=p.negative_prompts) - infotexts.append(info) + info = create_infotext(p, p.prompts, p.seeds, p.subseeds, index=i, all_negative_prompts=p.negative_prompts) + infotexts.append(info) image.info["parameters"] = info output_images.append(image) if shared.opts.samples_save and not p.do_not_save_samples and p.outpath_samples is not None: diff --git a/modules/processing_args.py b/modules/processing_args.py index ff766ec04..a716b685e 100644 --- a/modules/processing_args.py +++ b/modules/processing_args.py @@ -135,7 +135,7 @@ def set_pipeline_args(p, model, prompts: list, negative_prompts: list, prompts_2 prompts = [p.replace('|image|', '<|image_1|>') for p in prompts] if hasattr(model, 'text_encoder') and hasattr(model, 'tokenizer') and 'prompt_embeds' in possible and prompt_parser_diffusers.embedder is not None: args['prompt_embeds'] = prompt_parser_diffusers.embedder('prompt_embeds') - if 'StableCascade' in model.__class__.__name__ and len(getattr(p, 'negative_pooleds', [])) > 0: + if 'StableCascade' in model.__class__.__name__ and prompt_parser_diffusers.embedder is not None: args['prompt_embeds_pooled'] = prompt_parser_diffusers.embedder('positive_pooleds').unsqueeze(0) elif 'XL' in model.__class__.__name__ and prompt_parser_diffusers.embedder is not None: args['pooled_prompt_embeds'] = prompt_parser_diffusers.embedder('positive_pooleds') diff --git a/modules/processing_callbacks.py b/modules/processing_callbacks.py index f6e3c0672..0bb94abad 100644 --- a/modules/processing_callbacks.py +++ b/modules/processing_callbacks.py @@ -7,7 +7,8 @@ from modules import shared, processing_correction, extra_networks, timer, prompt from modules.lora.networks import network_load p = None -debug_callback = shared.log.trace if os.environ.get('SD_CALLBACK_DEBUG', None) is not None else lambda *args, **kwargs: None +debug = os.environ.get('SD_CALLBACK_DEBUG', None) is not None +debug_callback = shared.log.trace if debug else lambda *args, **kwargs: None def set_callbacks_p(processing): @@ -51,7 +52,8 @@ def diffusers_callback(pipe, step: int = 0, timestep: int = 0, kwargs: dict = {} if p is None: return kwargs latents = kwargs.get('latents', None) - debug_callback(f'Callback: step={step} timestep={timestep} latents={latents.shape if latents is not None else None} kwargs={list(kwargs)}') + if debug: + debug_callback(f'Callback: step={step} timestep={timestep} latents={latents.shape if latents is not None else None} kwargs={list(kwargs)}') order = getattr(pipe.scheduler, "order", 1) if hasattr(pipe, 'scheduler') else 1 shared.state.sampling_step = step // order if shared.state.interrupted or shared.state.skipped: @@ -69,7 +71,7 @@ def diffusers_callback(pipe, step: int = 0, timestep: int = 0, kwargs: dict = {} return kwargs elif shared.opts.nan_skip: assert not torch.isnan(latents[..., 0, 0]).all(), f'NaN detected at step {step}: Skipping...' - if len(getattr(p, 'ip_adapter_names', [])) > 0: + if len(getattr(p, 'ip_adapter_names', [])) > 0 and p.ip_adapter_names[0] != 'None': ip_adapter_scales = list(p.ip_adapter_scales) ip_adapter_starts = list(p.ip_adapter_starts) ip_adapter_ends = list(p.ip_adapter_ends) @@ -80,7 +82,7 @@ def diffusers_callback(pipe, step: int = 0, timestep: int = 0, kwargs: dict = {} debug_callback(f"Callback: IP Adapter scales={ip_adapter_scales}") pipe.set_ip_adapter_scale(ip_adapter_scales) if step != getattr(pipe, 'num_timesteps', 0): - kwargs = processing_correction.correction_callback(p, timestep, kwargs) + kwargs = processing_correction.correction_callback(p, timestep, kwargs, initial=step == 0) kwargs = prompt_callback(step, kwargs) # monkey patch for diffusers callback issues if step == int(getattr(pipe, 'num_timesteps', 100) * p.cfg_end) and 'prompt_embeds' in kwargs and 'negative_prompt_embeds' in kwargs: if "PAG" in shared.sd_model.__class__.__name__: @@ -107,7 +109,5 @@ def diffusers_callback(pipe, step: int = 0, timestep: int = 0, kwargs: dict = {} if shared.cmd_opts.profile and shared.profiler is not None: shared.profiler.step() t1 = time.time() - if 'callback' not in timer.process.records: - timer.process.records['callback'] = 0 - timer.process.records['callback'] += t1 - t0 + timer.process.add('callback', t1 - t0) return kwargs diff --git a/modules/processing_class.py b/modules/processing_class.py index 79f51576f..21e86c1b0 100644 --- a/modules/processing_class.py +++ b/modules/processing_class.py @@ -31,8 +31,8 @@ class StableDiffusionProcessing: n_iter: int = 1, steps: int = 50, clip_skip: int = 1, - width: int = 512, - height: int = 512, + width: int = 1024, + height: int = 1024, # samplers sampler_index: int = None, # pylint: disable=unused-argument # used only to set sampler_name sampler_name: str = None, diff --git a/modules/processing_correction.py b/modules/processing_correction.py index e715d8c49..050fae889 100644 --- a/modules/processing_correction.py +++ b/modules/processing_correction.py @@ -7,9 +7,11 @@ import os import torch from modules import shared, sd_vae_taesd, devices + debug_enabled = os.environ.get('SD_HDR_DEBUG', None) is not None debug = shared.log.trace if debug_enabled else lambda *args, **kwargs: None debug('Trace: HDR') +skip_correction = False def sharpen_tensor(tensor, ratio=0): @@ -116,8 +118,15 @@ def correction(p, timestep, latent): return latent -def correction_callback(p, timestep, kwargs): - if not any([p.hdr_clamp, p.hdr_mode, p.hdr_maximize, p.hdr_sharpen, p.hdr_color, p.hdr_brightness, p.hdr_tint_ratio]): +def correction_callback(p, timestep, kwargs, initial: bool = False): + global skip_correction # pylint: disable=global-statement + if initial: + if not any([p.hdr_clamp, p.hdr_mode, p.hdr_maximize, p.hdr_sharpen, p.hdr_color, p.hdr_brightness, p.hdr_tint_ratio]): + skip_correction = True + return kwargs + else: + skip_correction = False + elif skip_correction: return kwargs latents = kwargs["latents"] if debug_enabled: diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 83d3b1b69..2e8fb357c 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -77,7 +77,6 @@ def process_base(p: processing.StableDiffusionProcessing): clip_skip=p.clip_skip, desc='Base', ) - timer.process.record('args') shared.state.sampling_steps = base_args.get('prior_num_inference_steps', None) or p.steps or base_args.get('num_inference_steps', None) if shared.opts.scheduler_eta is not None and shared.opts.scheduler_eta > 0 and shared.opts.scheduler_eta < 1: p.extra_generation_params["Sampler Eta"] = shared.opts.scheduler_eta @@ -233,7 +232,8 @@ def process_hires(p: processing.StableDiffusionProcessing, output): output = shared.sd_model(**hires_args) # pylint: disable=not-callable if isinstance(output, dict): output = SimpleNamespace(**output) - shared.history.add(output.images, info=processing.create_infotext(p), ops=p.ops) + if hasattr(output, 'images'): + shared.history.add(output.images, info=processing.create_infotext(p), ops=p.ops) sd_models_compile.check_deepcache(enable=False) sd_models_compile.openvino_post_compile(op="base") except AssertionError as e: @@ -315,7 +315,8 @@ def process_refine(p: processing.StableDiffusionProcessing, output): output = shared.sd_refiner(**refiner_args) # pylint: disable=not-callable if isinstance(output, dict): output = SimpleNamespace(**output) - shared.history.add(output.images, info=processing.create_infotext(p), ops=p.ops) + if hasattr(output, 'images'): + shared.history.add(output.images, info=processing.create_infotext(p), ops=p.ops) sd_models_compile.openvino_post_compile(op="refiner") except AssertionError as e: shared.log.info(e) @@ -353,7 +354,7 @@ def process_decode(p: processing.StableDiffusionProcessing, output): if not hasattr(model, 'vae'): if hasattr(model, 'pipe') and hasattr(model.pipe, 'vae'): model = model.pipe - if hasattr(model, "vae") and output.images is not None and len(output.images) > 0: + if (hasattr(model, "vae") or hasattr(model, "vqgan")) and output.images is not None and len(output.images) > 0: if p.hr_resize_mode > 0 and (p.hr_upscaler != 'None' or p.hr_resize_mode == 5): width = max(getattr(p, 'width', 0), getattr(p, 'hr_upscale_to_x', 0)) height = max(getattr(p, 'height', 0), getattr(p, 'hr_upscale_to_y', 0)) diff --git a/modules/processing_helpers.py b/modules/processing_helpers.py index ec7fbf048..22acf296c 100644 --- a/modules/processing_helpers.py +++ b/modules/processing_helpers.py @@ -561,7 +561,9 @@ def save_intermediate(p, latents, suffix): def update_sampler(p, sd_model, second_pass=False): sampler_selection = p.hr_sampler_name if second_pass else p.sampler_name if hasattr(sd_model, 'scheduler'): - if sampler_selection is None or sampler_selection == 'None': + if sampler_selection == 'None': + return + if sampler_selection is None: sampler = sd_samplers.all_samplers_map.get("UniPC") else: sampler = sd_samplers.all_samplers_map.get(sampler_selection, None) diff --git a/modules/processing_vae.py b/modules/processing_vae.py index 3c0357c81..1c4a45f07 100644 --- a/modules/processing_vae.py +++ b/modules/processing_vae.py @@ -33,6 +33,62 @@ def create_latents(image, p, dtype=None, device=None): return latents +def full_vqgan_decode(latents, model): + t0 = time.time() + if model is None or not hasattr(model, 'vqgan'): + shared.log.error('VQGAN not found in model') + return [] + if debug: + devices.torch_gc(force=True) + shared.mem_mon.reset() + + base_device = None + if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False): + base_device = sd_models.move_base(model, devices.cpu) + + if shared.opts.diffusers_offload_mode == "balanced": + shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model) + elif shared.opts.diffusers_offload_mode != "sequential": + sd_models.move_model(model.vqgan, devices.device) + + latents = latents.to(devices.device, dtype=model.vqgan.dtype) + + #normalize latents + scaling_factor = model.vqgan.config.get("scale_factor", None) + if scaling_factor: + latents = latents * scaling_factor + + vae_name = os.path.splitext(os.path.basename(sd_vae.loaded_vae_file))[0] if sd_vae.loaded_vae_file is not None else "default" + vae_stats = f'name="{vae_name}" dtype={model.vqgan.dtype} device={model.vqgan.device}' + latents_stats = f'shape={latents.shape} dtype={latents.dtype} device={latents.device}' + stats = f'vae {vae_stats} latents {latents_stats}' + + log_debug(f'VAE config: {model.vqgan.config}') + try: + decoded = model.vqgan.decode(latents).sample.clamp(0, 1) + except Exception as e: + shared.log.error(f'VAE decode: {stats} {e}') + errors.display(e, 'VAE decode') + decoded = [] + + # delete vae after OpenVINO compile + if 'VAE' in shared.opts.cuda_compile and shared.opts.cuda_compile_backend == "openvino_fx" and shared.compiled_model_state.first_pass_vae: + shared.compiled_model_state.first_pass_vae = False + if not shared.opts.openvino_disable_memory_cleanup and hasattr(shared.sd_model, "vqgan"): + model.vqgan.apply(sd_models.convert_to_faketensors) + devices.torch_gc(force=True) + + if shared.opts.diffusers_offload_mode == "balanced": + shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model) + elif shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False) and base_device is not None: + sd_models.move_base(model, base_device) + t1 = time.time() + if debug: + log_debug(f'VAE memory: {shared.mem_mon.read()}') + shared.log.debug(f'VAE decode: {stats} time={round(t1-t0, 3)}') + return decoded + + def full_vae_decode(latents, model): t0 = time.time() if not hasattr(model, 'vae') and hasattr(model, 'pipe'): @@ -161,7 +217,7 @@ def vae_decode(latents, model, output_type='np', full_quality=True, width=None, return [] if shared.state.interrupted or shared.state.skipped: return [] - if not hasattr(model, 'vae'): + if not hasattr(model, 'vae') and not hasattr(model, 'vqgan'): shared.log.error('VAE not found in model') return [] @@ -176,12 +232,18 @@ def vae_decode(latents, model, output_type='np', full_quality=True, width=None, decoded = latents.float().cpu().numpy() elif full_quality and hasattr(model, "vae"): decoded = full_vae_decode(latents=latents, model=model) + elif hasattr(model, "vqgan"): + decoded = full_vqgan_decode(latents=latents, model=model) else: decoded = taesd_vae_decode(latents=latents) if torch.is_tensor(decoded): if hasattr(model, 'image_processor'): imgs = model.image_processor.postprocess(decoded, output_type=output_type) + elif hasattr(model, "vqgan"): + imgs = decoded.permute(0, 2, 3, 1).cpu().float().numpy() + if output_type == "pil": + imgs = model.numpy_to_pil(imgs) else: import diffusers model.image_processor = diffusers.image_processor.VaeImageProcessor() diff --git a/modules/prompt_parser_diffusers.py b/modules/prompt_parser_diffusers.py index 234272907..2edef4bf5 100644 --- a/modules/prompt_parser_diffusers.py +++ b/modules/prompt_parser_diffusers.py @@ -162,7 +162,7 @@ class PromptEmbedder: def encode(self, pipe, positive_prompt, negative_prompt, batchidx): self.attention = shared.opts.prompt_attention - if self.attention == "xhinker" or 'Flux' in pipe.__class__.__name__: + if self.attention == "xhinker": prompt_embed, positive_pooled, negative_embed, negative_pooled = get_xhinker_text_embeddings(pipe, positive_prompt, negative_prompt, self.clip_skip) else: prompt_embed, positive_pooled, negative_embed, negative_pooled = get_weighted_text_embeddings(pipe, positive_prompt, negative_prompt, self.clip_skip) @@ -583,15 +583,15 @@ def get_xhinker_text_embeddings(pipe, prompt: str = "", neg_prompt: str = "", cl te1_device, te2_device, te3_device = None, None, None if hasattr(pipe, "text_encoder") and pipe.text_encoder.device != devices.device: te1_device = pipe.text_encoder.device - sd_models.move_model(pipe.text_encoder, devices.device) + sd_models.move_model(pipe.text_encoder, devices.device, force=True) if hasattr(pipe, "text_encoder_2") and pipe.text_encoder_2.device != devices.device: te2_device = pipe.text_encoder_2.device - sd_models.move_model(pipe.text_encoder_2, devices.device) + sd_models.move_model(pipe.text_encoder_2, devices.device, force=True) if hasattr(pipe, "text_encoder_3") and pipe.text_encoder_3.device != devices.device: te3_device = pipe.text_encoder_3.device - sd_models.move_model(pipe.text_encoder_3, devices.device) + sd_models.move_model(pipe.text_encoder_3, devices.device, force=True) - if is_sd3: + if 'StableDiffusion3' in pipe.__class__.__name__: prompt_embed, negative_embed, positive_pooled, negative_pooled = get_weighted_text_embeddings_sd3(pipe=pipe, prompt=prompt, neg_prompt=neg_prompt, use_t5_encoder=bool(pipe.text_encoder_3)) elif 'Flux' in pipe.__class__.__name__: prompt_embed, positive_pooled = get_weighted_text_embeddings_flux1(pipe=pipe, prompt=prompt, prompt2=prompt_2, device=devices.device) @@ -601,10 +601,10 @@ def get_xhinker_text_embeddings(pipe, prompt: str = "", neg_prompt: str = "", cl prompt_embed, negative_embed = get_weighted_text_embeddings_sd15(pipe=pipe, prompt=prompt, neg_prompt=neg_prompt, clip_skip=clip_skip) if te1_device is not None: - sd_models.move_model(pipe.text_encoder, te1_device) + sd_models.move_model(pipe.text_encoder, te1_device, force=True) if te2_device is not None: - sd_models.move_model(pipe.text_encoder_2, te1_device) + sd_models.move_model(pipe.text_encoder_2, te1_device, force=True) if te3_device is not None: - sd_models.move_model(pipe.text_encoder_3, te1_device) + sd_models.move_model(pipe.text_encoder_3, te1_device, force=True) return prompt_embed, positive_pooled, negative_embed, negative_pooled diff --git a/modules/sd_checkpoint.py b/modules/sd_checkpoint.py index afc5842e4..e035fc3db 100644 --- a/modules/sd_checkpoint.py +++ b/modules/sd_checkpoint.py @@ -168,7 +168,10 @@ def update_model_hashes(): def get_closet_checkpoint_match(s: str): if s.startswith('https://huggingface.co/'): - s = s.replace('https://huggingface.co/', '') + model_name = s.replace('https://huggingface.co/', '') + checkpoint_info = CheckpointInfo(model_name) # create a virutal model info + checkpoint_info.type = 'huggingface' + return checkpoint_info if s.startswith('huggingface/'): model_name = s.replace('huggingface/', '') checkpoint_info = CheckpointInfo(model_name) # create a virutal model info @@ -185,6 +188,11 @@ def get_closet_checkpoint_match(s: str): if found and len(found) == 1: return found[0] + # absolute path + if s.endswith('.safetensors') and os.path.isfile(s): + checkpoint_info = CheckpointInfo(s) + return checkpoint_info + # reference search """ found = sorted([info for info in shared.reference_models.values() if os.path.basename(info['path']).lower().startswith(s.lower())], key=lambda x: len(x['path'])) @@ -198,8 +206,9 @@ def get_closet_checkpoint_match(s: str): if shared.opts.sd_checkpoint_autodownload and s.count('/') == 1: modelloader.hf_login() found = modelloader.find_diffuser(s, full=True) + found = [f for f in found if f == s] shared.log.info(f'HF search: model="{s}" results={found}') - if found is not None and len(found) == 1 and found[0] == s: + if found is not None and len(found) == 1: checkpoint_info = CheckpointInfo(s) checkpoint_info.type = 'huggingface' return checkpoint_info diff --git a/modules/sd_models.py b/modules/sd_models.py index cf1921a36..68446bdd3 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -16,7 +16,7 @@ import safetensors.torch from omegaconf import OmegaConf from ldm.util import instantiate_from_config from modules import paths, shared, shared_state, modelloader, devices, script_callbacks, sd_vae, sd_unet, errors, sd_models_config, sd_models_compile, sd_hijack_accelerate, sd_detect -from modules.timer import Timer +from modules.timer import Timer, process as process_timer from modules.memstats import memory_stats from modules.modeldata import model_data from modules.sd_checkpoint import CheckpointInfo, select_checkpoint, list_models, checkpoints_list, checkpoint_titles, get_closet_checkpoint_match, model_hash, update_model_hashes, setup_model, write_metadata, read_metadata_from_safetensors # pylint: disable=unused-import @@ -279,7 +279,7 @@ def set_diffuser_options(sd_model, vae = None, op: str = 'model', offload=True): model.eval() return model sd_model = sd_models_compile.apply_compile_to_model(sd_model, eval_model, ["Model", "VAE", "Text Encoder"], op="eval") - if len(shared.opts.torchao_quantization) > 0: + if len(shared.opts.torchao_quantization) > 0 and shared.opts.torchao_quantization_mode != 'post': sd_model = sd_models_compile.torchao_quantization(sd_model) if shared.opts.opt_channelslast and hasattr(sd_model, 'unet'): @@ -319,12 +319,12 @@ def set_diffuser_offload(sd_model, op: str = 'model'): if not (hasattr(sd_model, "has_accelerate") and sd_model.has_accelerate): sd_model.has_accelerate = False if hasattr(sd_model, 'maybe_free_model_hooks') and shared.opts.diffusers_offload_mode == "none": - shared.log.debug(f'Setting {op}: offload={shared.opts.diffusers_offload_mode}') + shared.log.debug(f'Setting {op}: offload={shared.opts.diffusers_offload_mode} limit={shared.opts.cuda_mem_fraction}') sd_model.maybe_free_model_hooks() sd_model.has_accelerate = False if hasattr(sd_model, "enable_model_cpu_offload") and shared.opts.diffusers_offload_mode == "model": try: - shared.log.debug(f'Setting {op}: offload={shared.opts.diffusers_offload_mode}') + shared.log.debug(f'Setting {op}: offload={shared.opts.diffusers_offload_mode} limit={shared.opts.cuda_mem_fraction}') if shared.opts.diffusers_move_base or shared.opts.diffusers_move_unet or shared.opts.diffusers_move_refiner: shared.opts.diffusers_move_base = False shared.opts.diffusers_move_unet = False @@ -339,7 +339,7 @@ def set_diffuser_offload(sd_model, op: str = 'model'): shared.log.error(f'Setting {op}: offload={shared.opts.diffusers_offload_mode} {e}') if hasattr(sd_model, "enable_sequential_cpu_offload") and shared.opts.diffusers_offload_mode == "sequential": try: - shared.log.debug(f'Setting {op}: offload={shared.opts.diffusers_offload_mode}') + shared.log.debug(f'Setting {op}: offload={shared.opts.diffusers_offload_mode} limit={shared.opts.cuda_mem_fraction}') if shared.opts.diffusers_move_base or shared.opts.diffusers_move_unet or shared.opts.diffusers_move_refiner: shared.opts.diffusers_move_base = False shared.opts.diffusers_move_unet = False @@ -359,7 +359,7 @@ def set_diffuser_offload(sd_model, op: str = 'model'): shared.log.error(f'Setting {op}: offload={shared.opts.diffusers_offload_mode} {e}') if shared.opts.diffusers_offload_mode == "balanced": try: - shared.log.debug(f'Setting {op}: offload={shared.opts.diffusers_offload_mode}') + shared.log.debug(f'Setting {op}: offload={shared.opts.diffusers_offload_mode} threshold={shared.opts.diffusers_offload_max_gpu_memory} limit={shared.opts.cuda_mem_fraction}') sd_model = apply_balanced_offload(sd_model) except Exception as e: shared.log.error(f'Setting {op}: offload={shared.opts.diffusers_offload_mode} {e}') @@ -512,7 +512,10 @@ def move_model(model, device=None, force=False): except Exception as e1: t1 = time.time() shared.log.error(f'Model move: device={device} {e1}') - if os.environ.get('SD_MOVE_DEBUG', None) or (t1-t0) > 0.1: + if 'move' not in process_timer.records: + process_timer.records['move'] = 0 + process_timer.records['move'] += t1 - t0 + if os.environ.get('SD_MOVE_DEBUG', None) or (t1-t0) > 1: shared.log.debug(f'Model move: device={device} class={model.__class__.__name__} accelerate={getattr(model, "has_accelerate", False)} fn={fn} time={t1-t0:.2f}') # pylint: disable=protected-access devices.torch_gc() @@ -771,7 +774,7 @@ def load_diffuser_file(model_type, pipeline, checkpoint_info, diffusers_load_con return sd_model -def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=None, op='model'): # pylint: disable=unused-argument +def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=None, op='model', revision=None): # pylint: disable=unused-argument if timer is None: timer = Timer() logging.getLogger("diffusers").setLevel(logging.ERROR) @@ -784,6 +787,8 @@ def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=No "requires_safety_checker": False, # sd15 specific but we cant know ahead of time # "use_safetensors": True, } + if revision is not None: + diffusers_load_config['revision'] = revision if shared.opts.diffusers_model_load_variant != 'default': diffusers_load_config['variant'] = shared.opts.diffusers_model_load_variant if shared.opts.diffusers_pipeline == 'Custom Diffusers Pipeline' and len(shared.opts.custom_diffusers_pipeline) > 0: @@ -1077,6 +1082,8 @@ def set_diffuser_pipe(pipe, new_pipe_type): 'OmniGenPipeline', 'StableDiffusion3ControlNetPipeline', 'InstantIRPipeline', + 'FluxFillPipeline', + 'FluxControlPipeline', ] n = getattr(pipe.__class__, '__name__', '') @@ -1345,7 +1352,7 @@ def reload_text_encoder(initial=False): set_t5(pipe=shared.sd_model, module='text_encoder_3', t5=shared.opts.sd_text_encoder, cache_dir=shared.opts.diffusers_dir) -def reload_model_weights(sd_model=None, info=None, reuse_dict=False, op='model', force=False): +def reload_model_weights(sd_model=None, info=None, reuse_dict=False, op='model', force=False, revision=None): load_dict = shared.opts.sd_model_dict != model_data.sd_dict from modules import lowvram, sd_hijack checkpoint_info = info or select_checkpoint(op=op) # are we selecting model or dictionary @@ -1390,7 +1397,7 @@ def reload_model_weights(sd_model=None, info=None, reuse_dict=False, op='model', load_model(checkpoint_info, already_loaded_state_dict=state_dict, timer=timer, op=op) model_data.sd_dict = shared.opts.sd_model_dict else: - load_diffuser(checkpoint_info, already_loaded_state_dict=state_dict, timer=timer, op=op) + load_diffuser(checkpoint_info, already_loaded_state_dict=state_dict, timer=timer, op=op, revision=revision) if load_dict and next_checkpoint_info is not None: model_data.sd_dict = shared.opts.sd_model_dict shared.opts.data["sd_model_checkpoint"] = next_checkpoint_info.title diff --git a/modules/sd_models_compile.py b/modules/sd_models_compile.py index 91ed84ded..38d3ef57f 100644 --- a/modules/sd_models_compile.py +++ b/modules/sd_models_compile.py @@ -535,7 +535,6 @@ def torchao_quantization(sd_model): if hasattr(sd_model, 'transformer') and 'Model' in shared.opts.torchao_quantization: modules.append('transformer') q.quantize_(sd_model.transformer, fn(), device=devices.device) - # sd_model.transformer = q.autoquant(sd_model.transformer, error_on_unseen=False) if hasattr(sd_model, 'vae') and 'VAE' in shared.opts.torchao_quantization: modules.append('vae') q.quantize_(sd_model.vae, fn(), device=devices.device) diff --git a/modules/sd_samplers.py b/modules/sd_samplers.py index e560744dd..d8416e5d9 100644 --- a/modules/sd_samplers.py +++ b/modules/sd_samplers.py @@ -47,6 +47,8 @@ def visible_sampler_names(): def create_sampler(name, model): + if name is None or name == 'None': + return model.scheduler try: current = model.scheduler.__class__.__name__ except Exception: diff --git a/modules/sd_samplers_common.py b/modules/sd_samplers_common.py index a487fe9b7..f6f6c18d5 100644 --- a/modules/sd_samplers_common.py +++ b/modules/sd_samplers_common.py @@ -1,9 +1,10 @@ +import time import threading from collections import namedtuple import torch import torchvision.transforms as T from PIL import Image -from modules import shared, devices, processing, images, sd_vae_approx, sd_vae_taesd, sd_vae_stablecascade, sd_samplers +from modules import shared, devices, processing, images, sd_vae_approx, sd_vae_taesd, sd_vae_stablecascade, sd_samplers, timer SamplerData = namedtuple('SamplerData', ['name', 'constructor', 'aliases', 'options']) @@ -33,6 +34,7 @@ def setup_img2img_steps(p, steps=None): def single_sample_to_image(sample, approximation=None): with queue_lock: + t0 = time.time() sd_cascade = False if approximation is None: approximation = approximation_indexes.get(shared.opts.show_progress_type, None) @@ -84,6 +86,8 @@ def single_sample_to_image(sample, approximation=None): except Exception as e: warn_once(f'Preview: {e}') image = Image.new(mode="RGB", size=(512, 512)) + t1 = time.time() + timer.process.add('preview', t1 - t0) return image diff --git a/modules/sd_samplers_diffusers.py b/modules/sd_samplers_diffusers.py index 60c75b64e..4672df92e 100644 --- a/modules/sd_samplers_diffusers.py +++ b/modules/sd_samplers_diffusers.py @@ -69,7 +69,7 @@ config = { 'Euler a': { 'steps_offset': 0, 'rescale_betas_zero_snr': False, 'timestep_spacing': 'linspace' }, 'Euler SGM': { 'steps_offset': 0, 'interpolation_type': "linear", 'rescale_betas_zero_snr': False, 'final_sigmas_type': 'zero', 'timestep_spacing': 'trailing', 'use_beta_sigmas': False, 'use_exponential_sigmas': False, 'use_karras_sigmas': False, 'prediction_type': "sample" }, 'Euler EDM': { 'sigma_schedule': "karras" }, - 'Euler FlowMatch': { 'timestep_spacing': "linspace", 'shift': 1, 'use_dynamic_shifting': False }, + 'Euler FlowMatch': { 'timestep_spacing': "linspace", 'shift': 1, 'use_dynamic_shifting': False, 'use_karras_sigmas': False, 'use_exponential_sigmas': False, 'use_beta_sigmas': False }, 'DPM++': { 'solver_order': 2, 'thresholding': False, 'sample_max_value': 1.0, 'algorithm_type': "dpmsolver++", 'solver_type': "midpoint", 'lower_order_final': True, 'use_karras_sigmas': False, 'use_exponential_sigmas': False, 'use_beta_sigmas': False, 'final_sigmas_type': 'sigma_min' }, 'DPM++ 1S': { 'thresholding': False, 'sample_max_value': 1.0, 'algorithm_type': "dpmsolver++", 'solver_type': "midpoint", 'lower_order_final': True, 'use_karras_sigmas': False, 'use_exponential_sigmas': False, 'use_beta_sigmas': False, 'use_lu_lambdas': False, 'final_sigmas_type': 'zero', 'timestep_spacing': 'linspace', 'solver_order': 1 }, @@ -80,13 +80,13 @@ config = { 'DPM++ Cosine': { 'solver_order': 2, 'sigma_schedule': "exponential", 'prediction_type': "v-prediction" }, 'DPM SDE': { 'use_karras_sigmas': False, 'use_exponential_sigmas': False, 'use_beta_sigmas': False, 'noise_sampler_seed': None, 'timestep_spacing': 'linspace', 'steps_offset': 0, }, - 'DPM2 FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver2', 'use_noise_sampler': True }, - 'DPM2a FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver2A', 'use_noise_sampler': True }, - 'DPM2++ 2M FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver++2M', 'use_noise_sampler': True }, - 'DPM2++ 2S FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver++2S', 'use_noise_sampler': True }, - 'DPM2++ SDE FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver++sde', 'use_noise_sampler': True }, - 'DPM2++ 2M SDE FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver++2Msde', 'use_noise_sampler': True }, - 'DPM2++ 3M SDE FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 3, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver++3Msde', 'use_noise_sampler': True }, + 'DPM2 FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver2', 'use_noise_sampler': True, 'beta_start': 0.00085, 'beta_end': 0.012 }, + 'DPM2a FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver2A', 'use_noise_sampler': True, 'beta_start': 0.00085, 'beta_end': 0.012 }, + 'DPM2++ 2M FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver++2M', 'use_noise_sampler': True, 'beta_start': 0.00085, 'beta_end': 0.012 }, + 'DPM2++ 2S FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver++2S', 'use_noise_sampler': True, 'beta_start': 0.00085, 'beta_end': 0.012 }, + 'DPM2++ SDE FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver++sde', 'use_noise_sampler': True, 'beta_start': 0.00085, 'beta_end': 0.012 }, + 'DPM2++ 2M SDE FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 2, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver++2Msde', 'use_noise_sampler': True, 'beta_start': 0.00085, 'beta_end': 0.012 }, + 'DPM2++ 3M SDE FlowMatch': { 'shift': 1, 'use_dynamic_shifting': False, 'solver_order': 3, 'sigma_schedule': None, 'use_beta_sigmas': False, 'algorithm_type': 'dpmsolver++3Msde', 'use_noise_sampler': True, 'beta_start': 0.00085, 'beta_end': 0.012 }, 'Heun': { 'use_beta_sigmas': False, 'use_karras_sigmas': False, 'use_exponential_sigmas': False, 'timestep_spacing': 'linspace' }, 'Heun FlowMatch': { 'timestep_spacing': "linspace", 'shift': 1 }, @@ -200,16 +200,16 @@ class DiffusionSampler: timesteps = re.split(',| ', shared.opts.schedulers_timesteps) timesteps = [int(x) for x in timesteps if x.isdigit()] if len(timesteps) == 0: - if 'use_beta_sigmas' in self.config: - self.config['use_beta_sigmas'] = shared.opts.schedulers_sigma == 'beta' - if 'use_karras_sigmas' in self.config: - self.config['use_karras_sigmas'] = shared.opts.schedulers_sigma == 'karras' - if 'use_exponential_sigmas' in self.config: - self.config['use_exponential_sigmas'] = shared.opts.schedulers_sigma == 'exponential' - if 'use_lu_lambdas' in self.config: - self.config['use_lu_lambdas'] = shared.opts.schedulers_sigma == 'lambdas' if 'sigma_schedule' in self.config: self.config['sigma_schedule'] = shared.opts.schedulers_sigma if shared.opts.schedulers_sigma != 'default' else None + if shared.opts.schedulers_sigma == 'betas' and 'use_beta_sigmas' in self.config: + self.config['use_beta_sigmas'] = True + elif shared.opts.schedulers_sigma == 'karras' and 'use_karras_sigmas' in self.config: + self.config['use_karras_sigmas'] = True + elif shared.opts.schedulers_sigma == 'exponential' and 'use_exponential_sigmas' in self.config: + self.config['use_exponential_sigmas'] = True + elif shared.opts.schedulers_sigma == 'lambdas' and 'use_lu_lambdas' in self.config: + self.config['use_lu_lambdas'] = True else: pass # timesteps are set using set_timesteps in set_pipeline_args @@ -236,7 +236,7 @@ class DiffusionSampler: if 'use_dynamic_shifting' in self.config: if 'Flux' in model.__class__.__name__: self.config['use_dynamic_shifting'] = shared.opts.schedulers_dynamic_shift - if 'use_beta_sigmas' in self.config: + if 'use_beta_sigmas' in self.config and 'sigma_schedule' in self.config: self.config['use_beta_sigmas'] = 'StableDiffusion3' in model.__class__.__name__ if 'rescale_betas_zero_snr' in self.config: self.config['rescale_betas_zero_snr'] = shared.opts.schedulers_rescale_betas diff --git a/modules/shared.py b/modules/shared.py index a5af83f5e..72af37500 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -477,7 +477,7 @@ options_templates.update(options_section(('sd', "Execution & Models"), { "sd_checkpoint_autoload": OptionInfo(True, "Model autoload on start"), "sd_checkpoint_autodownload": OptionInfo(True, "Model auto-download on demand"), "sd_textencoder_cache": OptionInfo(True, "Cache text encoder results", gr.Checkbox, {"visible": False}), - "sd_textencoder_cache_size": OptionInfo(4, "Text encoder results LRU cache size", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1}), + "sd_textencoder_cache_size": OptionInfo(4, "Text encoder cache size", gr.Slider, {"minimum": 0, "maximum": 16, "step": 1}), "stream_load": OptionInfo(False, "Load models using stream loading method", gr.Checkbox, {"visible": not native }), "prompt_mean_norm": OptionInfo(False, "Prompt attention normalization", gr.Checkbox), "comma_padding_backtrack": OptionInfo(20, "Prompt padding", gr.Slider, {"minimum": 0, "maximum": 74, "step": 1, "visible": not native }), @@ -559,8 +559,8 @@ options_templates.update(options_section(('diffusers', "Diffusers Settings"), { "diffusers_extract_ema": OptionInfo(False, "Use model EMA weights when possible"), "diffusers_generator_device": OptionInfo("GPU", "Generator device", gr.Radio, {"choices": ["GPU", "CPU", "Unset"]}), "diffusers_offload_mode": OptionInfo(startup_offload_mode, "Model offload mode", gr.Radio, {"choices": ['none', 'balanced', 'model', 'sequential']}), - "diffusers_offload_max_gpu_memory": OptionInfo(round(gpu_memory * 0.75, 1), "Max GPU memory for balanced offload mode in GB", gr.Slider, {"minimum": 0, "maximum": gpu_memory, "step": 0.01,}), - "diffusers_offload_max_cpu_memory": OptionInfo(round(cpu_memory * 0.75, 1), "Max CPU memory for balanced offload mode in GB", gr.Slider, {"minimum": 0, "maximum": cpu_memory, "step": 0.01,}), + "diffusers_offload_max_gpu_memory": OptionInfo(round(gpu_memory * 0.75, 1), "Max GPU memory before balanced offload", gr.Slider, {"minimum": 0, "maximum": gpu_memory, "step": 0.01, "visible": True }), + "diffusers_offload_max_cpu_memory": OptionInfo(round(cpu_memory * 0.75, 1), "Max CPU memory before balanced offload", gr.Slider, {"minimum": 0, "maximum": cpu_memory, "step": 0.01, "visible": False }), "diffusers_vae_upcast": OptionInfo("default", "VAE upcasting", gr.Radio, {"choices": ['default', 'true', 'false']}), "diffusers_vae_slicing": OptionInfo(True, "VAE slicing"), "diffusers_vae_tiling": OptionInfo(cmd_opts.lowvram or cmd_opts.medvram, "VAE tiling"), @@ -590,6 +590,7 @@ options_templates.update(options_section(('quantization', "Quantization Settings "optimum_quanto_weights_type": OptionInfo("qint8", "Optimum.quanto quantization type", gr.Radio, {"choices": ['qint8', 'qfloat8_e4m3fn', 'qfloat8_e5m2', 'qint4', 'qint2'], "visible": native}), "optimum_quanto_activations_type": OptionInfo("none", "Optimum.quanto quantization activations ", gr.Radio, {"choices": ['none', 'qint8', 'qfloat8_e4m3fn', 'qfloat8_e5m2'], "visible": native}), "torchao_quantization": OptionInfo([], "TorchAO quantization enabled", gr.CheckboxGroup, {"choices": ["Model", "VAE", "Text Encoder"], "visible": native}), + "torchao_quantization_mode": OptionInfo("pre", "TorchAO quantization mode", gr.Radio, {"choices": ['pre', 'post'], "visible": native}), "torchao_quantization_type": OptionInfo("int8", "TorchAO quantization type", gr.Radio, {"choices": ["int8+act", "int8", "int4", "fp8+act", "fp8", "fpx"], "visible": native}), "nncf_compress_weights": OptionInfo([], "NNCF compression enabled", gr.CheckboxGroup, {"choices": ["Model", "VAE", "Text Encoder", "ControlNet"], "visible": native}), "nncf_compress_weights_mode": OptionInfo("INT8", "NNCF compress mode", gr.Radio, {"choices": ['INT8', 'INT8_SYM', 'INT4_ASYM', 'INT4_SYM', 'NF4'] if cmd_opts.use_openvino else ['INT8']}), @@ -823,7 +824,7 @@ options_templates.update(options_section(('postprocessing', "Postprocessing"), { 'postprocessing_enable_in_main_ui': OptionInfo([], "Additional postprocessing operations", gr.Dropdown, lambda: {"multiselect":True, "choices": [x.name for x in shared_items.postprocessing_scripts()]}), 'postprocessing_operation_order': OptionInfo([], "Postprocessing operation order", gr.Dropdown, lambda: {"multiselect":True, "choices": [x.name for x in shared_items.postprocessing_scripts()]}), - "postprocessing_sep_img2img": OptionInfo("

Img2Img & Inpainting

", "", gr.HTML), + "postprocessing_sep_img2img": OptionInfo("

Inpaint

", "", gr.HTML), "img2img_color_correction": OptionInfo(False, "Apply color correction"), "mask_apply_overlay": OptionInfo(True, "Apply mask as overlay"), "img2img_background_color": OptionInfo("#ffffff", "Image transparent color fill", gr.ColorPicker, {}), @@ -831,7 +832,7 @@ options_templates.update(options_section(('postprocessing', "Postprocessing"), { "initial_noise_multiplier": OptionInfo(1.0, "Noise multiplier for image processing", gr.Slider, {"minimum": 0.1, "maximum": 1.5, "step": 0.01, "visible": not native}), "img2img_extra_noise": OptionInfo(0.0, "Extra noise multiplier for img2img", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01, "visible": not native}), - # "postprocessing_sep_detailer": OptionInfo("

Detailer

", "", gr.HTML), + "postprocessing_sep_detailer": OptionInfo("

Detailer

", "", gr.HTML), "detailer_model": OptionInfo("Detailer", "Detailer model", gr.Radio, lambda: {"choices": [x.name() for x in detailers], "visible": False}), "detailer_classes": OptionInfo("", "Detailer classes", gr.Textbox, { "visible": False}), "detailer_conf": OptionInfo(0.6, "Min confidence", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.05, "visible": False}), @@ -843,11 +844,12 @@ options_templates.update(options_section(('postprocessing', "Postprocessing"), { "detailer_blur": OptionInfo(10, "Item edge blur", gr.Slider, {"minimum": 0, "maximum": 100, "step": 1, "visible": False}), "detailer_strength": OptionInfo(0.5, "Detailer strength", gr.Slider, {"minimum": 0, "maximum": 1, "step": 0.01, "visible": False}), "detailer_models": OptionInfo(['face-yolo8n'], "Detailer models", gr.Dropdown, lambda: {"multiselect":True, "choices": list(yolo.list), "visible": False}), - "code_former_weight": OptionInfo(0.2, "CodeFormer weight parameter", gr.Slider, {"minimum": 0, "maximum": 1, "step": 0.01, "visible": False}), "detailer_unload": OptionInfo(False, "Move detailer model to CPU when complete"), + "detailer_augment": OptionInfo(True, "Detailer use model augment"), "postprocessing_sep_face_restore": OptionInfo("

Face restore

", "", gr.HTML), - "face_restoration_model": OptionInfo("Face restorer", "Face restoration", gr.Radio, lambda: {"choices": ['None'] + [x.name() for x in face_restorers]}), + "face_restoration_model": OptionInfo("None", "Face restoration", gr.Radio, lambda: {"choices": ['None'] + [x.name() for x in face_restorers]}), + "code_former_weight": OptionInfo(0.2, "CodeFormer weight parameter", gr.Slider, {"minimum": 0, "maximum": 1, "step": 0.01}), "postprocessing_sep_upscalers": OptionInfo("

Upscaling

", "", gr.HTML), "upscaler_unload": OptionInfo(False, "Unload upscaler after processing"), diff --git a/modules/shared_state.py b/modules/shared_state.py index 7def42b8c..51d33f9ed 100644 --- a/modules/shared_state.py +++ b/modules/shared_state.py @@ -28,6 +28,9 @@ class State: oom = False debug_output = os.environ.get('SD_STATE_DEBUG', None) + def __str__(self) -> str: + return f'State: job={self.job} {self.job_no}/{self.job_count} step={self.sampling_step}/{self.sampling_steps} skipped={self.skipped} interrupted={self.interrupted} paused={self.paused} info={self.textinfo}' + def skip(self): log.debug('Requested skip') self.skipped = True @@ -135,6 +138,8 @@ class State: modules.devices.torch_gc() def set_current_image(self): + if self.job == 'VAE': # avoid generating preview while vae is running + return from modules.shared import opts, cmd_opts """sets self.current_image from self.current_latent if enough sampling steps have been made after the last call to this""" if cmd_opts.lowvram or self.api: diff --git a/modules/style_aligned/inversion.py b/modules/style_aligned/inversion.py new file mode 100644 index 000000000..8c91cc02a --- /dev/null +++ b/modules/style_aligned/inversion.py @@ -0,0 +1,124 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import annotations +from typing import Callable, TYPE_CHECKING +from diffusers import StableDiffusionXLPipeline +import torch +from tqdm import tqdm +if TYPE_CHECKING: + import numpy as np + + +T = torch.Tensor +TN = T +InversionCallback = Callable[[StableDiffusionXLPipeline, int, T, dict[str, T]], dict[str, T]] + + +def _get_text_embeddings(prompt: str, tokenizer, text_encoder, device): + # Tokenize text and get embeddings + text_inputs = tokenizer(prompt, padding='max_length', max_length=tokenizer.model_max_length, truncation=True, return_tensors='pt') + text_input_ids = text_inputs.input_ids + + with torch.no_grad(): + prompt_embeds = text_encoder( + text_input_ids.to(device), + output_hidden_states=True, + ) + + pooled_prompt_embeds = prompt_embeds[0] + prompt_embeds = prompt_embeds.hidden_states[-2] + if prompt == '': + negative_prompt_embeds = torch.zeros_like(prompt_embeds) + negative_pooled_prompt_embeds = torch.zeros_like(pooled_prompt_embeds) + return negative_prompt_embeds, negative_pooled_prompt_embeds + return prompt_embeds, pooled_prompt_embeds + + +def _encode_text_sdxl(model: StableDiffusionXLPipeline, prompt: str) -> tuple[dict[str, T], T]: + device = model._execution_device # pylint: disable=protected-access + prompt_embeds, pooled_prompt_embeds, = _get_text_embeddings(prompt, model.tokenizer, model.text_encoder, device) # pylint: disable=unused-variable + prompt_embeds_2, pooled_prompt_embeds2, = _get_text_embeddings( prompt, model.tokenizer_2, model.text_encoder_2, device) + prompt_embeds = torch.cat((prompt_embeds, prompt_embeds_2), dim=-1) + text_encoder_projection_dim = model.text_encoder_2.config.projection_dim + add_time_ids = model._get_add_time_ids((1024, 1024), (0, 0), (1024, 1024), model.text_encoder.dtype, # pylint: disable=protected-access + text_encoder_projection_dim).to(device) + added_cond_kwargs = {"text_embeds": pooled_prompt_embeds2, "time_ids": add_time_ids} + return added_cond_kwargs, prompt_embeds + + +def _encode_text_sdxl_with_negative(model: StableDiffusionXLPipeline, prompt: str) -> tuple[dict[str, T], T]: + added_cond_kwargs, prompt_embeds = _encode_text_sdxl(model, prompt) + added_cond_kwargs_uncond, prompt_embeds_uncond = _encode_text_sdxl(model, "") + prompt_embeds = torch.cat((prompt_embeds_uncond, prompt_embeds, )) + added_cond_kwargs = {"text_embeds": torch.cat((added_cond_kwargs_uncond["text_embeds"], added_cond_kwargs["text_embeds"])), + "time_ids": torch.cat((added_cond_kwargs_uncond["time_ids"], added_cond_kwargs["time_ids"])),} + return added_cond_kwargs, prompt_embeds + + +def _encode_image(model: StableDiffusionXLPipeline, image: np.ndarray) -> T: + image = torch.from_numpy(image).float() / 255. + image = (image * 2 - 1).permute(2, 0, 1).unsqueeze(0) + latent = model.vae.encode(image.to(model.vae.device, model.vae.dtype))['latent_dist'].mean * model.vae.config.scaling_factor + return latent + + +def _next_step(model: StableDiffusionXLPipeline, model_output: T, timestep: int, sample: T) -> T: + timestep, next_timestep = min(timestep - model.scheduler.config.num_train_timesteps // model.scheduler.num_inference_steps, 999), timestep + alpha_prod_t = model.scheduler.alphas_cumprod[int(timestep)] if timestep >= 0 else model.scheduler.final_alpha_cumprod + alpha_prod_t_next = model.scheduler.alphas_cumprod[int(next_timestep)] + beta_prod_t = 1 - alpha_prod_t + next_original_sample = (sample - beta_prod_t ** 0.5 * model_output) / alpha_prod_t ** 0.5 + next_sample_direction = (1 - alpha_prod_t_next) ** 0.5 * model_output + next_sample = alpha_prod_t_next ** 0.5 * next_original_sample + next_sample_direction + return next_sample + + +def _get_noise_pred(model: StableDiffusionXLPipeline, latent: T, t: T, context: T, guidance_scale: float, added_cond_kwargs: dict[str, T]): + latents_input = torch.cat([latent] * 2) + noise_pred = model.unet(latents_input, t, encoder_hidden_states=context, added_cond_kwargs=added_cond_kwargs)["sample"] + noise_pred_uncond, noise_prediction_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_prediction_text - noise_pred_uncond) + # latents = next_step(model, noise_pred, t, latent) + return noise_pred + + +def _ddim_loop(model: StableDiffusionXLPipeline, z0, prompt, guidance_scale) -> T: + all_latent = [z0] + added_cond_kwargs, text_embedding = _encode_text_sdxl_with_negative(model, prompt) + latent = z0.clone().detach().to(model.text_encoder.dtype) + for i in tqdm(range(model.scheduler.num_inference_steps)): + t = model.scheduler.timesteps[len(model.scheduler.timesteps) - i - 1] + noise_pred = _get_noise_pred(model, latent, t, text_embedding, guidance_scale, added_cond_kwargs) + latent = _next_step(model, noise_pred, t, latent) + all_latent.append(latent) + return torch.cat(all_latent).flip(0) + + +def make_inversion_callback(zts, offset: int = 0): + + def callback_on_step_end(pipeline: StableDiffusionXLPipeline, i: int, t: T, callback_kwargs: dict[str, T]) -> dict[str, T]: # pylint: disable=unused-argument + latents = callback_kwargs['latents'] + latents[0] = zts[max(offset + 1, i + 1)].to(latents.device, latents.dtype) + return {'latents': latents} + return zts[offset], callback_on_step_end + + +@torch.no_grad() +def ddim_inversion(model: StableDiffusionXLPipeline, x0: np.ndarray, prompt: str, num_inference_steps: int, guidance_scale,) -> T: + z0 = _encode_image(model, x0) + model.scheduler.set_timesteps(num_inference_steps, device=z0.device) + zs = _ddim_loop(model, z0, prompt, guidance_scale) + return zs diff --git a/modules/style_aligned/sa_handler.py b/modules/style_aligned/sa_handler.py new file mode 100644 index 000000000..ee4b1ca79 --- /dev/null +++ b/modules/style_aligned/sa_handler.py @@ -0,0 +1,281 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import annotations +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from diffusers import StableDiffusionXLPipeline +from dataclasses import dataclass +import torch +import torch.nn as nn +from torch.nn import functional as nnf +from diffusers.models import attention_processor # pylint: disable=ungrouped-imports +import einops + +T = torch.Tensor + + +@dataclass(frozen=True) +class StyleAlignedArgs: + share_group_norm: bool = True + share_layer_norm: bool = True + share_attention: bool = True + adain_queries: bool = True + adain_keys: bool = True + adain_values: bool = False + full_attention_share: bool = False + shared_score_scale: float = 1. + shared_score_shift: float = 0. + only_self_level: float = 0. + + +def expand_first(feat: T, scale=1.,) -> T: + b = feat.shape[0] + feat_style = torch.stack((feat[0], feat[b // 2])).unsqueeze(1) + if scale == 1: + feat_style = feat_style.expand(2, b // 2, *feat.shape[1:]) + else: + feat_style = feat_style.repeat(1, b // 2, 1, 1, 1) + feat_style = torch.cat([feat_style[:, :1], scale * feat_style[:, 1:]], dim=1) + return feat_style.reshape(*feat.shape) + + +def concat_first(feat: T, dim=2, scale=1.) -> T: + feat_style = expand_first(feat, scale=scale) + return torch.cat((feat, feat_style), dim=dim) + + +def calc_mean_std(feat, eps: float = 1e-5) -> tuple[T, T]: + feat_std = (feat.var(dim=-2, keepdims=True) + eps).sqrt() + feat_mean = feat.mean(dim=-2, keepdims=True) + return feat_mean, feat_std + + +def adain(feat: T) -> T: + feat_mean, feat_std = calc_mean_std(feat) + feat_style_mean = expand_first(feat_mean) + feat_style_std = expand_first(feat_std) + feat = (feat - feat_mean) / feat_std + feat = feat * feat_style_std + feat_style_mean + return feat + + +class DefaultAttentionProcessor(nn.Module): + + def __init__(self): + super().__init__() + self.processor = attention_processor.AttnProcessor2_0() + + def __call__(self, attn: attention_processor.Attention, hidden_states, encoder_hidden_states=None, + attention_mask=None, **kwargs): + return self.processor(attn, hidden_states, encoder_hidden_states, attention_mask) + + +class SharedAttentionProcessor(DefaultAttentionProcessor): + + def shifted_scaled_dot_product_attention(self, attn: attention_processor.Attention, query: T, key: T, value: T) -> T: + logits = torch.einsum('bhqd,bhkd->bhqk', query, key) * attn.scale + logits[:, :, :, query.shape[2]:] += self.shared_score_shift + probs = logits.softmax(-1) + return torch.einsum('bhqk,bhkd->bhqd', probs, value) + + def shared_call( # pylint: disable=unused-argument + self, + attn: attention_processor.Attention, + hidden_states, + encoder_hidden_states=None, + attention_mask=None, + **kwargs + ): + + residual = hidden_states + input_ndim = hidden_states.ndim + if input_ndim == 4: + batch_size, channel, height, width = hidden_states.shape + hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) + batch_size, sequence_length, _ = ( + hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape + ) + + if attention_mask is not None: + attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) + # scaled_dot_product_attention expects attention_mask shape to be + # (batch, heads, source_length, target_length) + attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) + + if attn.group_norm is not None: + hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) + + query = attn.to_q(hidden_states) + key = attn.to_k(hidden_states) + value = attn.to_v(hidden_states) + inner_dim = key.shape[-1] + head_dim = inner_dim // attn.heads + + query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) + # if self.step >= self.start_inject: + if self.adain_queries: + query = adain(query) + if self.adain_keys: + key = adain(key) + if self.adain_values: + value = adain(value) + if self.share_attention: + key = concat_first(key, -2, scale=self.shared_score_scale) + value = concat_first(value, -2) + if self.shared_score_shift != 0: + hidden_states = self.shifted_scaled_dot_product_attention(attn, query, key, value,) + else: + hidden_states = nnf.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + else: + hidden_states = nnf.scaled_dot_product_attention( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + # hidden_states = adain(hidden_states) + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + + # linear proj + hidden_states = attn.to_out[0](hidden_states) + # dropout + hidden_states = attn.to_out[1](hidden_states) + + if input_ndim == 4: + hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) + + if attn.residual_connection: + hidden_states = hidden_states + residual + + hidden_states = hidden_states / attn.rescale_output_factor + return hidden_states + + def __call__(self, attn: attention_processor.Attention, hidden_states, encoder_hidden_states=None, + attention_mask=None, **kwargs): + if self.full_attention_share: + _b, n, _d = hidden_states.shape + hidden_states = einops.rearrange(hidden_states, '(k b) n d -> k (b n) d', k=2) + hidden_states = super().__call__(attn, hidden_states, encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, **kwargs) + hidden_states = einops.rearrange(hidden_states, 'k (b n) d -> (k b) n d', n=n) + else: + hidden_states = self.shared_call(attn, hidden_states, hidden_states, attention_mask, **kwargs) + + return hidden_states + + def __init__(self, style_aligned_args: StyleAlignedArgs): + super().__init__() + self.share_attention = style_aligned_args.share_attention + self.adain_queries = style_aligned_args.adain_queries + self.adain_keys = style_aligned_args.adain_keys + self.adain_values = style_aligned_args.adain_values + self.full_attention_share = style_aligned_args.full_attention_share + self.shared_score_scale = style_aligned_args.shared_score_scale + self.shared_score_shift = style_aligned_args.shared_score_shift + + +def _get_switch_vec(total_num_layers, level): + if level <= 0: + return torch.zeros(total_num_layers, dtype=torch.bool) + if level >= 1: + return torch.ones(total_num_layers, dtype=torch.bool) + to_flip = level > .5 + if to_flip: + level = 1 - level + num_switch = int(level * total_num_layers) + vec = torch.arange(total_num_layers) + vec = vec % (total_num_layers // num_switch) + vec = vec == 0 + if to_flip: + vec = ~vec + return vec + + +def init_attention_processors(pipeline: StableDiffusionXLPipeline, style_aligned_args: StyleAlignedArgs | None = None): + attn_procs = {} + unet = pipeline.unet + number_of_self, number_of_cross = 0, 0 + num_self_layers = len([name for name in unet.attn_processors.keys() if 'attn1' in name]) + if style_aligned_args is None: + only_self_vec = _get_switch_vec(num_self_layers, 1) + else: + only_self_vec = _get_switch_vec(num_self_layers, style_aligned_args.only_self_level) + for i, name in enumerate(unet.attn_processors.keys()): + is_self_attention = 'attn1' in name + if is_self_attention: + number_of_self += 1 + if style_aligned_args is None or only_self_vec[i // 2]: + attn_procs[name] = DefaultAttentionProcessor() + else: + attn_procs[name] = SharedAttentionProcessor(style_aligned_args) + else: + number_of_cross += 1 + attn_procs[name] = DefaultAttentionProcessor() + + unet.set_attn_processor(attn_procs) + + +def register_shared_norm(pipeline: StableDiffusionXLPipeline, + share_group_norm: bool = True, + share_layer_norm: bool = True, + ): + def register_norm_forward(norm_layer: nn.GroupNorm | nn.LayerNorm) -> nn.GroupNorm | nn.LayerNorm: + if not hasattr(norm_layer, 'orig_forward'): + setattr(norm_layer, 'orig_forward', norm_layer.forward) # noqa + orig_forward = norm_layer.orig_forward + + def forward_(hidden_states: T) -> T: + n = hidden_states.shape[-2] + hidden_states = concat_first(hidden_states, dim=-2) + hidden_states = orig_forward(hidden_states) + return hidden_states[..., :n, :] + + norm_layer.forward = forward_ + return norm_layer + + def get_norm_layers(pipeline_, norm_layers_: dict[str, list[nn.GroupNorm | nn.LayerNorm]]): + if isinstance(pipeline_, nn.LayerNorm) and share_layer_norm: + norm_layers_['layer'].append(pipeline_) + if isinstance(pipeline_, nn.GroupNorm) and share_group_norm: + norm_layers_['group'].append(pipeline_) + else: + for layer in pipeline_.children(): + get_norm_layers(layer, norm_layers_) + + norm_layers = {'group': [], 'layer': []} + get_norm_layers(pipeline.unet, norm_layers) + return [register_norm_forward(layer) for layer in norm_layers['group']] + [register_norm_forward(layer) for layer in + norm_layers['layer']] + + +class Handler: + + def register(self, style_aligned_args: StyleAlignedArgs): + self.norm_layers = register_shared_norm(self.pipeline, style_aligned_args.share_group_norm, + style_aligned_args.share_layer_norm) + init_attention_processors(self.pipeline, style_aligned_args) + + def remove(self): + for layer in self.norm_layers: + layer.forward = layer.orig_forward + self.norm_layers = [] + init_attention_processors(self.pipeline, None) + + def __init__(self, pipeline: StableDiffusionXLPipeline): + self.pipeline = pipeline + self.norm_layers = [] diff --git a/modules/timer.py b/modules/timer.py index 8a5db726d..7657ac8e8 100644 --- a/modules/timer.py +++ b/modules/timer.py @@ -15,6 +15,12 @@ class Timer: self.start = end return res + def add(self, name, t): + if name not in self.records: + self.records[name] = t + else: + self.records[name] += t + def record(self, category=None, extra_time=0, reset=True): e = self.elapsed(reset) if category is None: diff --git a/modules/txt2img.py b/modules/txt2img.py index 2f0e2f4b3..e82c744a2 100644 --- a/modules/txt2img.py +++ b/modules/txt2img.py @@ -88,7 +88,7 @@ def txt2img(id_task, state, p.scripts = scripts.scripts_txt2img p.script_args = args p.state = state - processed = scripts.scripts_txt2img.run(p, *args) + processed: processing.Processed = scripts.scripts_txt2img.run(p, *args) if processed is None: processed = processing.process_images(p) processed = scripts.scripts_txt2img.after(p, processed, *args) diff --git a/modules/ui_common.py b/modules/ui_common.py index 9c4bb5cdc..3e7c68bec 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -245,10 +245,18 @@ def create_output_panel(tabname, preview=True, prompt=None, height=None): gr.HTML(value="", elem_id="main_info", visible=False, elem_classes=["main-info"]) # columns are for <576px, <768px, <992px, <1200px, <1400px, >1400px result_gallery = gr.Gallery(value=[], - label='Output', show_label=False, show_download_button=True, allow_preview=True, container=False, preview=preview, - columns=4, object_fit='scale-down', height=height, + label='Output', + show_label=False, + show_download_button=True, + allow_preview=True, + container=False, + preview=preview, + columns=4, + object_fit='scale-down', + height=height, elem_id=f"{tabname}_gallery", - ) + elem_classes=["gallery_main"], + ) if prompt is not None: interrogate_clip_btn, interrogate_booru_btn = ui_sections.create_interrogate_buttons('control') interrogate_clip_btn.click(fn=interrogate_clip, inputs=[result_gallery], outputs=[prompt]) diff --git a/modules/ui_control.py b/modules/ui_control.py index 0bf070036..072d9b9c9 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -9,11 +9,11 @@ from modules.control.units import xs # vislearn ControlNet-XS from modules.control.units import lite # vislearn ControlNet-XS from modules.control.units import t2iadapter # TencentARC T2I-Adapter from modules.control.units import reference # reference pipeline -from modules import errors, shared, progress, ui_components, ui_symbols, ui_common, ui_sections, generation_parameters_copypaste, call_queue, scripts, masking, images, processing_vae # pylint: disable=ungrouped-imports +from modules import errors, shared, progress, ui_components, ui_symbols, ui_common, ui_sections, generation_parameters_copypaste, call_queue, scripts, masking, images, processing_vae, timer # pylint: disable=ungrouped-imports from modules import ui_control_helpers as helpers -gr_height = None +gr_height = 512 max_units = shared.opts.control_max_units units: list[unit.Unit] = [] # main state variable controls: list[gr.component] = [] # list of gr controls @@ -21,13 +21,36 @@ debug = shared.log.trace if os.environ.get('SD_CONTROL_DEBUG', None) is not None debug('Trace: CONTROL') -def return_controls(res): +def return_stats(t: float = None): + if t is None: + elapsed_text = '' + else: + elapsed = time.perf_counter() - t + elapsed_m = int(elapsed // 60) + elapsed_s = elapsed % 60 + elapsed_text = f"Time: {elapsed_m}m {elapsed_s:.2f}s |" if elapsed_m > 0 else f"Time: {elapsed_s:.2f}s |" + summary = timer.process.summary(min_time=0.1, total=False).replace('=', ' ') + vram_html = '' + if not shared.mem_mon.disabled: + vram = {k: -(v//-(1024*1024)) for k, v in shared.mem_mon.read().items()} + used = round(100 * vram['used'] / (vram['total'] + 0.001)) + if vram.get('active_peak', 0) > 0: + vram_html += f"| GPU {max(vram['active_peak'], vram['reserved_peak'])} MB {used}%" + vram_html += f" | retries {vram['retries']} oom {vram['oom']}" if vram.get('retries', 0) > 0 or vram.get('oom', 0) > 0 else '' + return f"

{elapsed_text} {summary} {vram_html}

" + + +def return_controls(res, t: float = None): # return preview, image, video, gallery, text debug(f'Control received: type={type(res)} {res}') + if t is None: + perf = '' + else: + perf = return_stats(t) if res is None: # no response - return [None, None, None, None, ''] + return [None, None, None, None, '', perf] elif isinstance(res, str): # error response - return [None, None, None, None, res] + return [None, None, None, None, res, perf] elif isinstance(res, tuple): # standard response received as tuple via control_run->yield(output_images, process_image, result_txt) preview_image = res[1] # may be None output_image = res[0][0] if isinstance(res[0], list) else res[0] # may be image or list of images @@ -37,9 +60,9 @@ def return_controls(res): output_gallery = [res[0]] if res[0] is not None else [] # must return list, but can receive single image result_txt = res[2] if len(res) > 2 else '' # do we have a message output_video = res[3] if len(res) > 3 else None # do we have a video filename - return [preview_image, output_image, output_video, output_gallery, result_txt] + return [preview_image, output_image, output_video, output_gallery, result_txt, perf] else: # unexpected - return [None, None, None, None, f'Control: Unexpected response: {type(res)}'] + return [None, None, None, None, f'Control: Unexpected response: {type(res)}', perf] def get_units(*values): @@ -67,17 +90,18 @@ def generate_click(job_id: str, state: str, active_tab: str, *args): shared.state.begin('Generate') progress.add_task_to_queue(job_id) with call_queue.queue_lock: - yield [None, None, None, None, 'Control: starting'] + yield [None, None, None, None, 'Control: starting', ''] shared.mem_mon.reset() progress.start_task(job_id) try: + t = time.perf_counter() for results in control_run(state, units, helpers.input_source, helpers.input_init, helpers.input_mask, active_tab, True, *args): progress.record_results(job_id, results) - yield return_controls(results) + yield return_controls(results, t) except Exception as e: shared.log.error(f"Control exception: {e}") errors.display(e, 'Control') - yield [None, None, None, None, f'Control: Exception: {e}'] + yield [None, None, None, None, f'Control: Exception: {e}', ''] progress.finish_task(job_id) shared.state.end() @@ -106,11 +130,12 @@ def create_ui(_blocks: gr.Blocks=None): with gr.Accordion(open=False, label="Input", elem_id="control_input", elem_classes=["small-accordion"]): with gr.Row(): - show_preview = gr.Checkbox(label="Show preview", value=True, elem_id="control_show_preview") + show_input = gr.Checkbox(label="Show input", value=True, elem_id="control_show_input") + show_preview = gr.Checkbox(label="Show preview", value=False, elem_id="control_show_preview") with gr.Row(): input_type = gr.Radio(label="Input type", choices=['Control only', 'Init image same as control', 'Separate init image'], value='Control only', type='index', elem_id='control_input_type') with gr.Row(): - denoising_strength = gr.Slider(minimum=0.01, maximum=1.0, step=0.01, label='Denoising strength', value=0.50, elem_id="control_input_denoising_strength") + denoising_strength = gr.Slider(minimum=0.01, maximum=1.0, step=0.01, label='Denoising strength', value=0.30, elem_id="control_input_denoising_strength") with gr.Accordion(open=False, label="Size", elem_id="control_size", elem_classes=["small-accordion"]): with gr.Tabs(): @@ -153,13 +178,13 @@ def create_ui(_blocks: gr.Blocks=None): override_settings = ui_common.create_override_inputs('control') with gr.Row(variant='compact', elem_id="control_extra_networks", elem_classes=["extra_networks_root"], visible=False) as extra_networks_ui: - from modules import timer, ui_extra_networks + from modules import ui_extra_networks extra_networks_ui = ui_extra_networks.create_ui(extra_networks_ui, btn_extra, 'control', skip_indexing=shared.opts.extra_network_skip_indexing) timer.startup.record('ui-networks') with gr.Row(elem_id='control-inputs'): - with gr.Column(scale=9, elem_id='control-input-column', visible=True) as _column_input: - gr.HTML('Control input

') + with gr.Column(scale=9, elem_id='control-input-column', visible=True) as column_input: + gr.HTML('Input

') with gr.Tabs(elem_classes=['control-tabs'], elem_id='control-tab-input'): with gr.Tab('Image', id='in-image') as tab_image: input_mode = gr.Label(value='select', visible=False) @@ -190,12 +215,12 @@ def create_ui(_blocks: gr.Blocks=None): gr.HTML('Output

') with gr.Tabs(elem_classes=['control-tabs'], elem_id='control-tab-output') as output_tabs: with gr.Tab('Gallery', id='out-gallery'): - output_gallery, _output_gen_info, _output_html_info, _output_html_info_formatted, _output_html_log = ui_common.create_output_panel("control", preview=True, prompt=prompt, height=gr_height) + output_gallery, _output_gen_info, _output_html_info, _output_html_info_formatted, output_html_log = ui_common.create_output_panel("control", preview=True, prompt=prompt, height=gr_height) with gr.Tab('Image', id='out-image'): output_image = gr.Image(label="Output", show_label=False, type="pil", interactive=False, tool="editor", height=gr_height, elem_id='control_output_image', elem_classes=['control-image']) with gr.Tab('Video', id='out-video'): output_video = gr.Video(label="Output", show_label=False, height=gr_height, elem_id='control_output_video', elem_classes=['control-image']) - with gr.Column(scale=9, elem_id='control-preview-column', visible=True) as column_preview: + with gr.Column(scale=9, elem_id='control-preview-column', visible=False) as column_preview: gr.HTML('Preview

') with gr.Tabs(elem_classes=['control-tabs'], elem_id='control-tab-preview'): with gr.Tab('Preview', id='preview-image') as _tab_preview: @@ -498,6 +523,7 @@ def create_ui(_blocks: gr.Blocks=None): btn_update = gr.Button('Update', interactive=True, visible=False, elem_id='control_update') btn_update.click(fn=get_units, inputs=controls, outputs=[], show_progress=True, queue=False) + show_input.change(fn=lambda x: gr.update(visible=x), inputs=[show_input], outputs=[column_input]) show_preview.change(fn=lambda x: gr.update(visible=x), inputs=[show_preview], outputs=[column_preview]) input_type.change(fn=lambda x: gr.update(visible=x == 2), inputs=[input_type], outputs=[column_init]) btn_prompt_counter.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[prompt, steps], outputs=[prompt_counter]) @@ -550,6 +576,7 @@ def create_ui(_blocks: gr.Blocks=None): output_video, output_gallery, result_txt, + output_html_log, ] control_dict = dict( fn=generate_click, diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index f6e6cee97..c326219df 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -16,7 +16,7 @@ from collections import OrderedDict import gradio as gr from PIL import Image from starlette.responses import FileResponse, JSONResponse -from modules import paths, shared, scripts, files_cache, errors, infotext +from modules import paths, shared, files_cache, errors, infotext from modules.ui_components import ToolButton import modules.ui_symbols as symbols @@ -135,6 +135,7 @@ class ExtraNetworksPage: return text.replace('~tabname', tabname) def create_xyz_grid(self): + """ xyz_grid = [x for x in scripts.scripts_data if x.script_class.__module__ == "xyz_grid.py"][0].module def add_prompt(p, opt, x): @@ -150,6 +151,7 @@ class ExtraNetworksPage: opt = xyz_grid.AxisOption(f"[Network] {self.title}", str, add_prompt, choices=lambda: [x["name"] for x in self.items]) if opt not in xyz_grid.axis_options: xyz_grid.axis_options.append(opt) + """ def link_preview(self, filename): quoted_filename = urllib.parse.quote(filename.replace('\\', '/')) diff --git a/modules/ui_img2img.py b/modules/ui_img2img.py index 22c89dac8..3c3d63656 100644 --- a/modules/ui_img2img.py +++ b/modules/ui_img2img.py @@ -1,7 +1,6 @@ import os from PIL import Image import gradio as gr -import numpy as np from modules.call_queue import wrap_gradio_gpu_call, wrap_queued_call from modules import timer, shared, ui_common, ui_sections, generation_parameters_copypaste, processing_vae @@ -56,7 +55,7 @@ def create_ui(): def add_copy_image_controls(tab_name, elem): with gr.Row(variant="compact", elem_id=f"img2img_copy_to_{tab_name}"): - for title, name in zip(['➠ Image', '➠ Sketch', '➠ Inpaint', '➠ Composite'], ['img2img', 'sketch', 'inpaint', 'inpaint_sketch']): + for title, name in zip(['➠ Image', '➠ Inpaint', '➠ Sketch', '➠ Composite'], ['img2img', 'sketch', 'inpaint', 'composite']): if name == tab_name: gr.Button(title, elem_id=f'copy_to_{name}', interactive=False) copy_image_destinations[name] = elem @@ -67,33 +66,36 @@ def create_ui(): with gr.Tabs(elem_id="mode_img2img"): img2img_selected_tab = gr.State(0) # pylint: disable=abstract-class-instantiated state = gr.Textbox(value='', visible=False) - with gr.TabItem('Image', id='img2img', elem_id="img2img_img2img_tab") as tab_img2img: - init_img = gr.Image(label="Image for img2img", elem_id="img2img_image", show_label=False, source="upload", interactive=True, type="pil", tool="editor", image_mode="RGBA", height=512) + with gr.TabItem('Image', id='img2img_image', elem_id="img2img_image_tab") as tab_img2img: + img_init = gr.Image(label="", elem_id="img2img_image", show_label=False, source="upload", interactive=True, type="pil", tool="editor", image_mode="RGBA", height=512) interrogate_clip, interrogate_booru = ui_sections.create_interrogate_buttons('img2img') - add_copy_image_controls('img2img', init_img) + add_copy_image_controls('img2img', img_init) - with gr.TabItem('Sketch', id='img2img_sketch', elem_id="img2img_img2img_sketch_tab") as tab_sketch: - sketch = gr.Image(label="Image for img2img", elem_id="img2img_sketch", show_label=False, source="upload", interactive=True, type="pil", tool="color-sketch", image_mode="RGBA", height=512) - add_copy_image_controls('sketch', sketch) + with gr.TabItem('Inpaint', id='img2img_inpaint', elem_id="img2img_inpaint_tab") as tab_inpaint: + img_inpaint = gr.Image(label="", elem_id="img2img_inpaint", show_label=False, source="upload", interactive=True, type="pil", tool="sketch", image_mode="RGBA", height=512) + add_copy_image_controls('inpaint', img_inpaint) - with gr.TabItem('Inpaint', id='inpaint', elem_id="img2img_inpaint_tab") as tab_inpaint: - init_img_with_mask = gr.Image(label="Image for inpainting with mask", show_label=False, elem_id="img2maskimg", source="upload", interactive=True, type="pil", tool="sketch", image_mode="RGBA", height=512) - add_copy_image_controls('inpaint', init_img_with_mask) + with gr.TabItem('Sketch', id='img2img_sketch', elem_id="img2img_sketch_tab") as tab_sketch: + img_sketch = gr.Image(label="", elem_id="img2img_sketch", show_label=False, source="upload", interactive=True, type="pil", tool="color-sketch", image_mode="RGBA", height=512) + add_copy_image_controls('sketch', img_sketch) - with gr.TabItem('Composite', id='inpaint_sketch', elem_id="img2img_inpaint_sketch_tab") as tab_inpaint_color: - inpaint_color_sketch = gr.Image(label="Color sketch inpainting", show_label=False, elem_id="inpaint_sketch", source="upload", interactive=True, type="pil", tool="color-sketch", image_mode="RGBA", height=512) - inpaint_color_sketch_orig = gr.State(None) # pylint: disable=abstract-class-instantiated - add_copy_image_controls('inpaint_sketch', inpaint_color_sketch) + with gr.TabItem('Composite', id='img2img_composite', elem_id="img2img_composite_tab") as tab_inpaint_color: + img_composite = gr.Image(label="", show_label=False, elem_id="img2img_composite", source="upload", interactive=True, type="pil", tool="color-sketch", image_mode="RGBA", height=512) + img_composite_orig = gr.State(None) # pylint: disable=abstract-class-instantiated + img_composite_orig_update = False - def update_orig(image, state): - if image is not None: - same_size = state is not None and state.size == image.size - has_exact_match = np.any(np.all(np.array(image) == np.array(state), axis=-1)) - edited = same_size and has_exact_match - return image if not edited or state is None else state - return state + def fn_img_composite_upload(): + nonlocal img_composite_orig_update + img_composite_orig_update = True + def fn_img_composite_change(img, img_composite): + nonlocal img_composite_orig_update + res = img if img_composite_orig_update else img_composite + img_composite_orig_update = False + return res - inpaint_color_sketch.change(update_orig, [inpaint_color_sketch, inpaint_color_sketch_orig], inpaint_color_sketch_orig) + img_composite.upload(fn=fn_img_composite_upload, inputs=[], outputs=[]) + img_composite.change(fn=fn_img_composite_change, inputs=[img_composite, img_composite_orig], outputs=[img_composite_orig]) + add_copy_image_controls('composite', img_composite) with gr.TabItem('Upload', id='inpaint_upload', elem_id="img2img_inpaint_upload_tab") as tab_inpaint_upload: init_img_inpaint = gr.Image(label="Image for img2img", show_label=False, source="upload", interactive=True, type="pil", elem_id="img_inpaint_base") @@ -120,13 +122,13 @@ def create_ui(): with gr.Accordion(open=False, label="Sampler", elem_classes=["small-accordion"], elem_id="img2img_sampler_group"): steps, sampler_index = ui_sections.create_sampler_and_steps_selection(None, "img2img") ui_sections.create_sampler_options('img2img') - resize_mode, resize_name, resize_context, width, height, scale_by, selected_scale_tab = ui_sections.create_resize_inputs('img2img', [init_img, sketch], latent=True, non_zero=False) + resize_mode, resize_name, resize_context, width, height, scale_by, selected_scale_tab = ui_sections.create_resize_inputs('img2img', [img_init, img_sketch], latent=True, non_zero=False) batch_count, batch_size = ui_sections.create_batch_inputs('img2img', accordion=True) seed, reuse_seed, subseed, reuse_subseed, subseed_strength, seed_resize_from_h, seed_resize_from_w = ui_sections.create_seed_inputs('img2img') with gr.Accordion(open=False, label="Denoise", elem_classes=["small-accordion"], elem_id="img2img_denoise_group"): with gr.Row(): - denoising_strength = gr.Slider(minimum=0.0, maximum=0.99, step=0.01, label='Denoising strength', value=0.50, elem_id="img2img_denoising_strength") + denoising_strength = gr.Slider(minimum=0.0, maximum=0.99, step=0.01, label='Denoising strength', value=0.30, elem_id="img2img_denoising_strength") refiner_start = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label='Denoise start', value=0.0, elem_id="img2img_refiner_start") full_quality, tiling, hidiffusion, cfg_scale, clip_skip, image_cfg_scale, diffusers_guidance_rescale, pag_scale, pag_adaptive, cfg_end = ui_sections.create_advanced_inputs('img2img') @@ -167,13 +169,8 @@ def create_ui(): img2img_args = [ dummy_component1, state, dummy_component2, img2img_prompt, img2img_negative_prompt, img2img_prompt_styles, - init_img, - sketch, - init_img_with_mask, - inpaint_color_sketch, - inpaint_color_sketch_orig, - init_img_inpaint, - init_mask_inpaint, + img_init, img_sketch, img_inpaint, img_composite, img_composite_orig, + init_img_inpaint, init_mask_inpaint, steps, sampler_index, mask_blur, mask_alpha, @@ -225,10 +222,7 @@ def create_ui(): img2img_batch_files, img2img_batch_input_dir, img2img_batch_output_dir, - init_img, - sketch, - init_img_with_mask, - inpaint_color_sketch, + img_init, img_sketch, img_inpaint, img_composite, init_img_inpaint, ], outputs=[img2img_prompt, dummy_component], @@ -285,7 +279,8 @@ def create_ui(): (seed_resize_from_h, "Seed resize from-2"), *modules.scripts.scripts_img2img.infotext_fields ] - generation_parameters_copypaste.add_paste_fields("img2img", init_img, img2img_paste_fields, override_settings) - generation_parameters_copypaste.add_paste_fields("inpaint", init_img_with_mask, img2img_paste_fields, override_settings) + generation_parameters_copypaste.add_paste_fields("img2img", img_init, img2img_paste_fields, override_settings) + generation_parameters_copypaste.add_paste_fields("sketch", img_sketch, img2img_paste_fields, override_settings) + generation_parameters_copypaste.add_paste_fields("inpaint", img_inpaint, img2img_paste_fields, override_settings) img2img_bindings = generation_parameters_copypaste.ParamBinding(paste_button=img2img_paste, tabname="img2img", source_text_component=img2img_prompt, source_image_component=None) generation_parameters_copypaste.register_paste_params_button(img2img_bindings) diff --git a/scripts/animatediff.py b/scripts/animatediff.py index 4c50f9cf6..91db60915 100644 --- a/scripts/animatediff.py +++ b/scripts/animatediff.py @@ -189,7 +189,7 @@ def set_free_noise(frames): class Script(scripts.Script): def title(self): - return 'Video AnimateDiff' + return 'Video: AnimateDiff' def show(self, is_img2img): # return scripts.AlwaysVisible if shared.native else False diff --git a/scripts/cogvideo.py b/scripts/cogvideo.py index 7f2c7225e..c988c05c4 100644 --- a/scripts/cogvideo.py +++ b/scripts/cogvideo.py @@ -22,7 +22,7 @@ debug = (os.environ.get('SD_LOAD_DEBUG', None) is not None) or (os.environ.get(' class Script(scripts.Script): def title(self): - return 'Video CogVideoX' + return 'Video: CogVideoX' def show(self, is_img2img): return shared.native diff --git a/scripts/flux_tools.py b/scripts/flux_tools.py new file mode 100644 index 000000000..e5fe443b7 --- /dev/null +++ b/scripts/flux_tools.py @@ -0,0 +1,123 @@ +# https://github.com/huggingface/diffusers/pull/9985 + +import time +import gradio as gr +import diffusers +from modules import scripts, processing, shared, devices, sd_models +from installer import install + + +redux_pipe: diffusers.FluxPriorReduxPipeline = None +processor_canny = None +processor_depth = None +title = 'Flux Tools' + + +class Script(scripts.Script): + def title(self): + return f'{title}' + + def show(self, is_img2img): + return is_img2img if shared.native else False + + def ui(self, _is_img2img): # ui elements + with gr.Row(): + gr.HTML('  Flux.1 Redux
') + with gr.Row(): + tool = gr.Dropdown(label='Tool', choices=['None', 'Redux', 'Fill', 'Canny', 'Depth'], value='None') + with gr.Row(): + process = gr.Checkbox(label='Preprocess input images', value=True) + strength = gr.Checkbox(label='Override denoise strength', value=True) + return [tool, strength, process] + + def run(self, p: processing.StableDiffusionProcessing, tool: str = 'None', strength: bool = True, process: bool = True): # pylint: disable=arguments-differ + global redux_pipe, processor_canny, processor_depth # pylint: disable=global-statement + if tool is None or tool == 'None': + return + supported_model_list = ['f1'] + if shared.sd_model_type not in supported_model_list: + shared.log.warning(f'{title}: class={shared.sd_model.__class__.__name__} model={shared.sd_model_type} required={supported_model_list}') + return None + image = getattr(p, 'init_images', None) + if image is None or len(image) == 0: + shared.log.error(f'{title}: tool={tool} no init_images') + return None + else: + image = image[0] if isinstance(image, list) else image + + shared.log.info(f'{title}: tool={tool} init') + + t0 = time.time() + if tool == 'Redux': + # pipe_prior_redux = FluxPriorReduxPipeline.from_pretrained("black-forest-labs/FLUX.1-Redux-dev", revision="refs/pr/8", torch_dtype=torch.bfloat16).to("cuda") + if redux_pipe is None: + redux_pipe = diffusers.FluxPriorReduxPipeline.from_pretrained( + "black-forest-labs/FLUX.1-Redux-dev", + revision="refs/pr/8", + torch_dtype=devices.dtype, + cache_dir=shared.opts.hfcache_dir + ).to(devices.device) + redux_output = redux_pipe(image) + for k, v in redux_output.items(): + p.task_args[k] = v + else: + if redux_pipe is not None: + shared.log.debug(f'{title}: tool=Redux unload') + redux_pipe = None + + if tool == 'Fill': + # pipe = FluxFillPipeline.from_pretrained("black-forest-labs/FLUX.1-Fill-dev", torch_dtype=torch.bfloat16, revision="refs/pr/4").to("cuda") + if p.image_mask is None: + shared.log.error(f'{title}: tool={tool} no image_mask') + return None + if shared.sd_model.__class__.__name__ != 'FluxFillPipeline': + shared.opts.data["sd_model_checkpoint"] = "black-forest-labs/FLUX.1-Fill-dev" + sd_models.reload_model_weights(op='model', revision="refs/pr/4") + p.task_args['image'] = image + p.task_args['mask_image'] = p.image_mask + + if tool == 'Canny': + # pipe = FluxControlPipeline.from_pretrained("black-forest-labs/FLUX.1-Canny-dev", torch_dtype=torch.bfloat16, revision="refs/pr/1").to("cuda") + install('controlnet-aux') + install('timm==0.9.16') + if shared.sd_model.__class__.__name__ != 'FluxControlPipeline' or 'Canny' not in shared.opts.sd_model_checkpoint: + shared.opts.data["sd_model_checkpoint"] = "black-forest-labs/FLUX.1-Canny-dev" + sd_models.reload_model_weights(op='model', revision="refs/pr/1") + if processor_canny is None: + from controlnet_aux import CannyDetector + processor_canny = CannyDetector() + if process: + control_image = processor_canny(image, low_threshold=50, high_threshold=200, detect_resolution=1024, image_resolution=1024) + p.task_args['control_image'] = control_image + else: + p.task_args['control_image'] = image + if strength: + p.task_args['strength'] = None + else: + if processor_canny is not None: + shared.log.debug(f'{title}: tool=Canny unload processor') + processor_canny = None + + if tool == 'Depth': + # pipe = FluxControlPipeline.from_pretrained("black-forest-labs/FLUX.1-Depth-dev", torch_dtype=torch.bfloat16, revision="refs/pr/1").to("cuda") + install('git+https://github.com/asomoza/image_gen_aux.git', 'image_gen_aux') + if shared.sd_model.__class__.__name__ != 'FluxControlPipeline' or 'Depth' not in shared.opts.sd_model_checkpoint: + shared.opts.data["sd_model_checkpoint"] = "black-forest-labs/FLUX.1-Depth-dev" + sd_models.reload_model_weights(op='model', revision="refs/pr/1") + if processor_depth is None: + from image_gen_aux import DepthPreprocessor + processor_depth = DepthPreprocessor.from_pretrained("LiheYoung/depth-anything-large-hf") + if process: + control_image = processor_depth(image)[0].convert("RGB") + p.task_args['control_image'] = control_image + else: + p.task_args['control_image'] = image + if strength: + p.task_args['strength'] = None + else: + if processor_depth is not None: + shared.log.debug(f'{title}: tool=Depth unload processor') + processor_depth = None + + shared.log.debug(f'{title}: tool={tool} ready time={time.time() - t0:.2f}') + devices.torch_gc() diff --git a/scripts/image2video.py b/scripts/image2video.py index 876ed3193..5e08922ee 100644 --- a/scripts/image2video.py +++ b/scripts/image2video.py @@ -13,7 +13,7 @@ MODELS = [ class Script(scripts.Script): def title(self): - return 'Video VGen Image-to-Video' + return 'Video: VGen Image-to-Video' def show(self, is_img2img): return is_img2img if shared.native else False diff --git a/scripts/pulid_ext.py b/scripts/pulid_ext.py index 676fa79f3..ee08e348b 100644 --- a/scripts/pulid_ext.py +++ b/scripts/pulid_ext.py @@ -164,11 +164,13 @@ class Script(scripts.Script): p.batch_size = 1 sdp = shared.opts.cross_attention_optimization == "Scaled-Dot-Product" + sampler_fn = getattr(self.pulid.sampling, f'sample_{sampler}', None) strength = getattr(p, 'pulid_strength', strength) zero = getattr(p, 'pulid_zero', zero) ortho = getattr(p, 'pulid_ortho', ortho) sampler = getattr(p, 'pulid_sampler', sampler) - sampler_fn = getattr(self.pulid.sampling, f'sample_{sampler}', None) + restore = getattr(p, 'pulid_restore', restore) + p.pulid_restore = restore if sampler_fn is None: sampler_fn = self.pulid.sampling.sample_dpmpp_2m_sde @@ -199,7 +201,7 @@ class Script(scripts.Script): return None shared.sd_model.sampler = sampler_fn - shared.log.info(f'PuLID: class={shared.sd_model.__class__.__name__} version="{version}" sdp={sdp} strength={strength} zero={zero} ortho={ortho} sampler={sampler_fn} images={[i.shape for i in images]} offload={offload}') + shared.log.info(f'PuLID: class={shared.sd_model.__class__.__name__} version="{version}" sdp={sdp} strength={strength} zero={zero} ortho={ortho} sampler={sampler_fn} images={[i.shape for i in images]} offload={offload} restore={restore}') self.pulid.attention.NUM_ZERO = zero self.pulid.attention.ORTHO = ortho == 'v1' self.pulid.attention.ORTHO_v2 = ortho == 'v2' diff --git a/scripts/style_aligned.py b/scripts/style_aligned.py new file mode 100644 index 000000000..25feb49bc --- /dev/null +++ b/scripts/style_aligned.py @@ -0,0 +1,117 @@ +import gradio as gr +import torch +import numpy as np +import diffusers +from modules import scripts, processing, shared, devices + + +handler = None +zts = None +supported_model_list = ['sdxl'] +orig_prompt_attention = None + + +class Script(scripts.Script): + def title(self): + return 'Style Aligned Image Generation' + + def show(self, is_img2img): + return shared.native + + def reset(self): + global handler, zts # pylint: disable=global-statement + handler = None + zts = None + shared.log.info('SA: image upload') + + def preset(self, preset): + if preset == 'text': + return [['attention', 'adain_queries', 'adain_keys'], 1.0, 0, 0.0] + if preset == 'image': + return [['group_norm', 'layer_norm', 'attention', 'adain_queries', 'adain_keys'], 1.0, 2, 0.0] + if preset == 'all': + return [['group_norm', 'layer_norm', 'attention', 'adain_queries', 'adain_keys', 'adain_values', 'full_attention_share'], 1.0, 1, 0.5] + + def ui(self, _is_img2img): # ui elements + with gr.Row(): + gr.HTML('  Style Aligned Image Generation

') + with gr.Row(): + preset = gr.Dropdown(label="Preset", choices=['text', 'image', 'all'], value='text') + scheduler = gr.Checkbox(label="Override scheduler", value=False) + with gr.Row(): + shared_opts = gr.Dropdown(label="Shared options", + multiselect=True, + choices=['group_norm', 'layer_norm', 'attention', 'adain_queries', 'adain_keys', 'adain_values', 'full_attention_share'], + value=['attention', 'adain_queries', 'adain_keys'], + ) + with gr.Row(): + shared_score_scale = gr.Slider(label="Scale", minimum=0.0, maximum=2.0, step=0.01, value=1.0) + shared_score_shift = gr.Slider(label="Shift", minimum=0, maximum=10, step=1, value=0) + only_self_level = gr.Slider(label="Level", minimum=0.0, maximum=1.0, step=0.01, value=0.0) + with gr.Row(): + prompt = gr.Textbox(lines=1, label='Optional image description', placeholder='use the style from the image') + with gr.Row(): + image = gr.Image(label='Optional image', source='upload', type='pil') + + image.change(self.reset) + preset.change(self.preset, inputs=[preset], outputs=[shared_opts, shared_score_scale, shared_score_shift, only_self_level]) + + return [image, prompt, scheduler, shared_opts, shared_score_scale, shared_score_shift, only_self_level] + + def run(self, p: processing.StableDiffusionProcessing, image, prompt, scheduler, shared_opts, shared_score_scale, shared_score_shift, only_self_level): # pylint: disable=arguments-differ + global handler, zts, orig_prompt_attention # pylint: disable=global-statement + if shared.sd_model_type not in supported_model_list: + shared.log.warning(f'SA: class={shared.sd_model.__class__.__name__} model={shared.sd_model_type} required={supported_model_list}') + return None + + from modules.style_aligned import sa_handler, inversion + + handler = sa_handler.Handler(shared.sd_model) + sa_args = sa_handler.StyleAlignedArgs( + share_group_norm='group_norm' in shared_opts, + share_layer_norm='layer_norm' in shared_opts, + share_attention='attention' in shared_opts, + adain_queries='adain_queries' in shared_opts, + adain_keys='adain_keys' in shared_opts, + adain_values='adain_values' in shared_opts, + full_attention_share='full_attention_share' in shared_opts, + shared_score_scale=float(shared_score_scale), + shared_score_shift=np.log(shared_score_shift) if shared_score_shift > 0 else 0, + only_self_level=1 if only_self_level else 0, + ) + handler.register(sa_args) + + if scheduler: + shared.sd_model.scheduler = diffusers.DDIMScheduler(beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", clip_sample=False, set_alpha_to_one=False) + p.sampler_name = 'None' + + if image is not None and zts is None: + shared.log.info(f'SA: inversion image={image} prompt="{prompt}"') + image = image.resize((1024, 1024)) + x0 = np.array(image).astype(np.float32) / 255.0 + shared.sd_model.scheduler = diffusers.DDIMScheduler(beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", clip_sample=False, set_alpha_to_one=False) + zts = inversion.ddim_inversion(shared.sd_model, x0, prompt, num_inference_steps=50, guidance_scale=2) + + p.prompt = p.prompt.splitlines() + p.batch_size = len(p.prompt) + orig_prompt_attention = shared.opts.prompt_attention + shared.opts.data['prompt_attention'] = 'fixed' # otherwise need to deal with class_tokens_mask + + if zts is not None: + processing.fix_seed(p) + zT, inversion_callback = inversion.make_inversion_callback(zts, offset=0) + generator = torch.Generator(device='cpu') + generator.manual_seed(p.seed) + latents = torch.randn(p.batch_size, 4, 128, 128, device='cpu', generator=generator, dtype=devices.dtype,).to(devices.device) + latents[0] = zT + p.task_args['latents'] = latents + p.task_args['callback_on_step_end'] = inversion_callback + + shared.log.info(f'SA: batch={p.batch_size} type={"image" if zts is not None else "text"} config={sa_args.__dict__}') + + def after(self, p: processing.StableDiffusionProcessing, *args): # pylint: disable=unused-argument + global handler # pylint: disable=global-statement + if handler is not None: + handler.remove() + handler = None + shared.opts.data['prompt_attention'] = orig_prompt_attention diff --git a/scripts/xyz_grid_classes.py b/scripts/xyz_grid_classes.py index b80b9f13c..cc70d68f8 100644 --- a/scripts/xyz_grid_classes.py +++ b/scripts/xyz_grid_classes.py @@ -1,4 +1,4 @@ -from scripts.xyz_grid_shared import apply_field, apply_task_args, apply_setting, apply_prompt, apply_order, apply_sampler, apply_hr_sampler_name, confirm_samplers, apply_checkpoint, apply_refiner, apply_unet, apply_dict, apply_clip_skip, apply_vae, list_lora, apply_lora, apply_te, apply_styles, apply_upscaler, apply_context, apply_detailer, apply_override, apply_processing, apply_options, apply_seed, format_value_add_label, format_value, format_value_join_list, do_nothing, format_nothing, str_permutations # pylint: disable=no-name-in-module, unused-import +from scripts.xyz_grid_shared import apply_field, apply_task_args, apply_setting, apply_prompt, apply_order, apply_sampler, apply_hr_sampler_name, confirm_samplers, apply_checkpoint, apply_refiner, apply_unet, apply_dict, apply_clip_skip, apply_vae, list_lora, apply_lora, apply_lora_strength, apply_te, apply_styles, apply_upscaler, apply_context, apply_detailer, apply_override, apply_processing, apply_options, apply_seed, format_value_add_label, format_value, format_value_join_list, do_nothing, format_nothing, str_permutations # pylint: disable=no-name-in-module, unused-import from modules import shared, shared_items, sd_samplers, ipadapter, sd_models, sd_vae, sd_unet @@ -97,7 +97,7 @@ axis_options = [ AxisOption("[Prompt] Prompt order", str_permutations, apply_order, fmt=format_value_join_list), AxisOption("[Prompt] Prompt parser", str, apply_setting("prompt_attention"), choices=lambda: ["native", "compel", "xhinker", "a1111", "fixed"]), AxisOption("[Network] LoRA", str, apply_lora, cost=0.5, choices=list_lora), - AxisOption("[Network] LoRA strength", float, apply_setting('extra_networks_default_multiplier')), + AxisOption("[Network] LoRA strength", float, apply_lora_strength, cost=0.6), AxisOption("[Network] Styles", str, apply_styles, choices=lambda: [s.name for s in shared.prompt_styles.styles.values()]), AxisOption("[Param] Width", int, apply_field("width")), AxisOption("[Param] Height", int, apply_field("height")), diff --git a/scripts/xyz_grid_shared.py b/scripts/xyz_grid_shared.py index d3ee0a864..82387fab8 100644 --- a/scripts/xyz_grid_shared.py +++ b/scripts/xyz_grid_shared.py @@ -63,28 +63,15 @@ def apply_seed(p, x, xs): def apply_prompt(p, x, xs): - if not hasattr(p, 'orig_prompt'): - p.orig_prompt = p.prompt - p.orig_negative = p.negative_prompt - if xs[0] not in p.orig_prompt and xs[0] not in p.orig_negative: - shared.log.warning(f'XYZ grid: prompt S/R string="{xs[0]}" not found') - else: - p.prompt = p.orig_prompt.replace(xs[0], x) - p.negative_prompt = p.orig_negative.replace(xs[0], x) - p.all_prompts = None - p.all_negative_prompts = None - """ - if p.all_prompts is not None: - for i in range(len(p.all_prompts)): - for j in range(len(xs)): - p.all_prompts[i] = p.all_prompts[i].replace(xs[j], x) - p.negative_prompt = p.negative_prompt.replace(xs[0], x) - if p.all_negative_prompts is not None: - for i in range(len(p.all_negative_prompts)): - for j in range(len(xs)): - p.all_negative_prompts[i] = p.all_negative_prompts[i].replace(xs[j], x) - """ - shared.log.debug(f'XYZ grid apply prompt: "{xs[0]}"="{x}"') + for s in xs: + if s in p.prompt: + shared.log.debug(f'XYZ grid apply prompt: "{s}"="{x}"') + p.prompt = p.prompt.replace(s, x) + if s in p.negative_prompt: + shared.log.debug(f'XYZ grid apply negative: "{s}"="{x}"') + p.negative_prompt = p.negative_prompt.replace(s, x) + p.all_prompts = None + p.all_negative_prompts = None def apply_order(p, x, xs): @@ -220,6 +207,15 @@ def apply_lora(p, x, xs): shared.log.debug(f'XYZ grid apply LoRA: "{x}"') +def apply_lora_strength(p, x, xs): + shared.log.debug(f'XYZ grid apply LoRA strength: "{x}"') + p.prompt = p.prompt.replace(':1.0>', '>') + p.prompt = p.prompt.replace(f':{shared.opts.extra_networks_default_multiplier}>', '>') + p.all_prompts = None + p.all_negative_prompts = None + shared.opts.data['extra_networks_default_multiplier'] = x + + def apply_te(p, x, xs): shared.opts.data["sd_text_encoder"] = x sd_models.reload_text_encoder() diff --git a/wiki b/wiki index 30f3265bb..f57cdb49d 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 30f3265bb06ac738e4467f58be4df3fc4b49c08b +Subproject commit f57cdb49d8ca928024b43525897d1c1379eab4c4