Skip to content

Commit 6130ef9

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents b7c5e60 + 1c0a0c4 commit 6130ef9

17 files changed

Lines changed: 169 additions & 41 deletions

CHANGELOG.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
1+
## 1.9.3
2+
3+
### Bug Fixes:
4+
* fix get_crop_region_v2 ([#15594](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15594))
5+
6+
## 1.9.2
7+
8+
### Extensions and API:
9+
* restore 1.8.0-style naming of scripts
10+
11+
## 1.9.1
12+
13+
### Minor:
14+
* Add avif support ([#15582](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15582))
15+
* Add filename patterns: `[sampler_scheduler]` and `[scheduler]` ([#15581](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15581))
16+
17+
### Extensions and API:
18+
* undo adding scripts to sys.modules
19+
* Add schedulers API endpoint ([#15577](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15577))
20+
* Remove API upscaling factor limits ([#15560](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15560))
21+
22+
### Bug Fixes:
23+
* Fix images do not match / Coordinate 'right' is less than 'left' ([#15534](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15534))
24+
* fix: remove_callbacks_for_function should also remove from the ordered map ([#15533](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15533))
25+
* fix x1 upscalers ([#15555](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15555))
26+
* Fix cls.__module__ value in extension script ([#15532](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15532))
27+
* fix typo in function call (eror -> error) ([#15531](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15531))
28+
29+
### Other:
30+
* Hide 'No Image data blocks found.' message ([#15567](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15567))
31+
* Allow webui.sh to be runnable from arbitrary directories containing a .git file ([#15561](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15561))
32+
* Compatibility with Debian 11, Fedora 34+ and openSUSE 15.4+ ([#15544](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15544))
33+
* numpy DeprecationWarning product -> prod ([#15547](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15547))
34+
* get_crop_region_v2 ([#15583](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15583), [#15587](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15587))
35+
36+
137
## 1.9.0
238

339
### Features:
@@ -85,7 +121,6 @@
85121
* Fix extra-single-image API not doing upscale failed ([#15465](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15465))
86122
* error handling paste_field callables ([#15470](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15470))
87123

88-
89124
### Hardware:
90125
* Add training support and change lspci for Ascend NPU ([#14981](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14981))
91126
* Update to ROCm5.7 and PyTorch ([#14820](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14820))

javascript/extraNetworks.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ function extraNetworksShowMetadata(text) {
568568
return;
569569
}
570570
} catch (error) {
571-
console.eror(error);
571+
console.error(error);
572572
}
573573

574574
var elem = document.createElement('pre');

modules/api/api.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from secrets import compare_digest
1818

1919
import modules.shared as shared
20-
from modules import sd_samplers, deepbooru, sd_hijack, images, scripts, ui, postprocessing, errors, restart, shared_items, script_callbacks, infotext_utils, sd_models
20+
from modules import sd_samplers, deepbooru, sd_hijack, images, scripts, ui, postprocessing, errors, restart, shared_items, script_callbacks, infotext_utils, sd_models, sd_schedulers
2121
from modules.api import models
2222
from modules.shared import opts
2323
from modules.processing import StableDiffusionProcessingTxt2Img, StableDiffusionProcessingImg2Img, process_images
@@ -221,6 +221,7 @@ def __init__(self, app: FastAPI, queue_lock: Lock):
221221
self.add_api_route("/sdapi/v1/options", self.set_config, methods=["POST"])
222222
self.add_api_route("/sdapi/v1/cmd-flags", self.get_cmd_flags, methods=["GET"], response_model=models.FlagsModel)
223223
self.add_api_route("/sdapi/v1/samplers", self.get_samplers, methods=["GET"], response_model=list[models.SamplerItem])
224+
self.add_api_route("/sdapi/v1/schedulers", self.get_schedulers, methods=["GET"], response_model=list[models.SchedulerItem])
224225
self.add_api_route("/sdapi/v1/upscalers", self.get_upscalers, methods=["GET"], response_model=list[models.UpscalerItem])
225226
self.add_api_route("/sdapi/v1/latent-upscale-modes", self.get_latent_upscale_modes, methods=["GET"], response_model=list[models.LatentUpscalerModeItem])
226227
self.add_api_route("/sdapi/v1/sd-models", self.get_sd_models, methods=["GET"], response_model=list[models.SDModelItem])
@@ -683,6 +684,17 @@ def get_cmd_flags(self):
683684
def get_samplers(self):
684685
return [{"name": sampler[0], "aliases":sampler[2], "options":sampler[3]} for sampler in sd_samplers.all_samplers]
685686

687+
def get_schedulers(self):
688+
return [
689+
{
690+
"name": scheduler.name,
691+
"label": scheduler.label,
692+
"aliases": scheduler.aliases,
693+
"default_rho": scheduler.default_rho,
694+
"need_inner_model": scheduler.need_inner_model,
695+
}
696+
for scheduler in sd_schedulers.schedulers]
697+
686698
def get_upscalers(self):
687699
return [
688700
{

modules/api/models.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ class ExtrasBaseRequest(BaseModel):
147147
gfpgan_visibility: float = Field(default=0, title="GFPGAN Visibility", ge=0, le=1, allow_inf_nan=False, description="Sets the visibility of GFPGAN, values should be between 0 and 1.")
148148
codeformer_visibility: float = Field(default=0, title="CodeFormer Visibility", ge=0, le=1, allow_inf_nan=False, description="Sets the visibility of CodeFormer, values should be between 0 and 1.")
149149
codeformer_weight: float = Field(default=0, title="CodeFormer Weight", ge=0, le=1, allow_inf_nan=False, description="Sets the weight of CodeFormer, values should be between 0 and 1.")
150-
upscaling_resize: float = Field(default=2, title="Upscaling Factor", ge=1, le=8, description="By how much to upscale the image, only used when resize_mode=0.")
150+
upscaling_resize: float = Field(default=2, title="Upscaling Factor", gt=0, description="By how much to upscale the image, only used when resize_mode=0.")
151151
upscaling_resize_w: int = Field(default=512, title="Target Width", ge=1, description="Target width for the upscaler to hit. Only used when resize_mode=1.")
152152
upscaling_resize_h: int = Field(default=512, title="Target Height", ge=1, description="Target height for the upscaler to hit. Only used when resize_mode=1.")
153153
upscaling_crop: bool = Field(default=True, title="Crop to fit", description="Should the upscaler crop the image to fit in the chosen size?")
@@ -235,6 +235,13 @@ class SamplerItem(BaseModel):
235235
aliases: list[str] = Field(title="Aliases")
236236
options: dict[str, str] = Field(title="Options")
237237

238+
class SchedulerItem(BaseModel):
239+
name: str = Field(title="Name")
240+
label: str = Field(title="Label")
241+
aliases: Optional[list[str]] = Field(title="Aliases")
242+
default_rho: Optional[float] = Field(title="Default Rho")
243+
need_inner_model: Optional[bool] = Field(title="Needs Inner Model")
244+
238245
class UpscalerItem(BaseModel):
239246
name: str = Field(title="Name")
240247
model_name: Optional[str] = Field(title="Model Name")

modules/hypernetworks/hypernetwork.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from einops import rearrange, repeat
1212
from ldm.util import default
1313
from modules import devices, sd_models, shared, sd_samplers, hashes, sd_hijack_checkpoint, errors
14-
from modules.textual_inversion import textual_inversion, logging
14+
from modules.textual_inversion import textual_inversion, saving_settings
1515
from modules.textual_inversion.learn_schedule import LearnRateScheduler
1616
from torch import einsum
1717
from torch.nn.init import normal_, xavier_normal_, xavier_uniform_, kaiming_normal_, kaiming_uniform_, zeros_
@@ -533,7 +533,7 @@ def train_hypernetwork(id_task, hypernetwork_name: str, learn_rate: float, batch
533533
model_name=checkpoint.model_name, model_hash=checkpoint.shorthash, num_of_dataset_images=len(ds),
534534
**{field: getattr(hypernetwork, field) for field in ['layer_structure', 'activation_func', 'weight_init', 'add_layer_norm', 'use_dropout', ]}
535535
)
536-
logging.save_settings_to_file(log_directory, {**saved_params, **locals()})
536+
saving_settings.save_settings_to_file(log_directory, {**saved_params, **locals()})
537537

538538
latent_sampling_method = ds.latent_sampling_method
539539

modules/images.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import datetime
4-
4+
import functools
55
import pytz
66
import io
77
import math
@@ -13,6 +13,8 @@
1313
import piexif
1414
import piexif.helper
1515
from PIL import Image, ImageFont, ImageDraw, ImageColor, PngImagePlugin, ImageOps
16+
# pillow_avif needs to be imported somewhere in code for it to work
17+
import pillow_avif # noqa: F401
1618
import string
1719
import json
1820
import hashlib
@@ -347,6 +349,32 @@ def sanitize_filename_part(text, replace_spaces=True):
347349
return text
348350

349351

352+
@functools.cache
353+
def get_scheduler_str(sampler_name, scheduler_name):
354+
"""Returns {Scheduler} if the scheduler is applicable to the sampler"""
355+
if scheduler_name == 'Automatic':
356+
config = sd_samplers.find_sampler_config(sampler_name)
357+
scheduler_name = config.options.get('scheduler', 'Automatic')
358+
return scheduler_name.capitalize()
359+
360+
361+
@functools.cache
362+
def get_sampler_scheduler_str(sampler_name, scheduler_name):
363+
"""Returns the '{Sampler} {Scheduler}' if the scheduler is applicable to the sampler"""
364+
return f'{sampler_name} {get_scheduler_str(sampler_name, scheduler_name)}'
365+
366+
367+
def get_sampler_scheduler(p, sampler):
368+
"""Returns '{Sampler} {Scheduler}' / '{Scheduler}' / 'NOTHING_AND_SKIP_PREVIOUS_TEXT'"""
369+
if hasattr(p, 'scheduler') and hasattr(p, 'sampler_name'):
370+
if sampler:
371+
sampler_scheduler = get_sampler_scheduler_str(p.sampler_name, p.scheduler)
372+
else:
373+
sampler_scheduler = get_scheduler_str(p.sampler_name, p.scheduler)
374+
return sanitize_filename_part(sampler_scheduler, replace_spaces=False)
375+
return NOTHING_AND_SKIP_PREVIOUS_TEXT
376+
377+
350378
class FilenameGenerator:
351379
replacements = {
352380
'seed': lambda self: self.seed if self.seed is not None else '',
@@ -358,6 +386,8 @@ class FilenameGenerator:
358386
'height': lambda self: self.image.height,
359387
'styles': lambda self: self.p and sanitize_filename_part(", ".join([style for style in self.p.styles if not style == "None"]) or "None", replace_spaces=False),
360388
'sampler': lambda self: self.p and sanitize_filename_part(self.p.sampler_name, replace_spaces=False),
389+
'sampler_scheduler': lambda self: self.p and get_sampler_scheduler(self.p, True),
390+
'scheduler': lambda self: self.p and get_sampler_scheduler(self.p, False),
361391
'model_hash': lambda self: getattr(self.p, "sd_model_hash", shared.sd_model.sd_model_hash),
362392
'model_name': lambda self: sanitize_filename_part(shared.sd_model.sd_checkpoint_info.name_for_extra, replace_spaces=False),
363393
'date': lambda self: datetime.datetime.now().strftime('%Y-%m-%d'),
@@ -569,6 +599,16 @@ def save_image_with_geninfo(image, geninfo, filename, extension=None, existing_p
569599
})
570600

571601
piexif.insert(exif_bytes, filename)
602+
elif extension.lower() == '.avif':
603+
if opts.enable_pnginfo and geninfo is not None:
604+
exif_bytes = piexif.dump({
605+
"Exif": {
606+
piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(geninfo or "", encoding="unicode")
607+
},
608+
})
609+
610+
611+
image.save(filename,format=image_format, exif=exif_bytes)
572612
elif extension.lower() == ".gif":
573613
image.save(filename, format=image_format, comment=geninfo)
574614
else:
@@ -747,7 +787,6 @@ def read_info_from_image(image: Image.Image) -> tuple[str | None, dict]:
747787
exif_comment = exif_comment.decode('utf8', errors="ignore")
748788

749789
if exif_comment:
750-
items['exif comment'] = exif_comment
751790
geninfo = exif_comment
752791
elif "comment" in items: # for gif
753792
geninfo = items["comment"].decode('utf8', errors="ignore")

modules/masking.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
11
from PIL import Image, ImageFilter, ImageOps
22

33

4-
def get_crop_region(mask, pad=0):
5-
"""finds a rectangular region that contains all masked ares in an image. Returns (x1, y1, x2, y2) coordinates of the rectangle.
6-
For example, if a user has painted the top-right part of a 512x512 image, the result may be (256, 0, 512, 256)"""
7-
mask_img = mask if isinstance(mask, Image.Image) else Image.fromarray(mask)
8-
box = mask_img.getbbox()
9-
if box:
4+
def get_crop_region_v2(mask, pad=0):
5+
"""
6+
Finds a rectangular region that contains all masked ares in a mask.
7+
Returns None if mask is completely black mask (all 0)
8+
9+
Parameters:
10+
mask: PIL.Image.Image L mode or numpy 1d array
11+
pad: int number of pixels that the region will be extended on all sides
12+
Returns: (x1, y1, x2, y2) | None
13+
14+
Introduced post 1.9.0
15+
"""
16+
mask = mask if isinstance(mask, Image.Image) else Image.fromarray(mask)
17+
if box := mask.getbbox():
1018
x1, y1, x2, y2 = box
11-
else: # when no box is found
12-
x1, y1 = mask_img.size
13-
x2 = y2 = 0
14-
return max(x1 - pad, 0), max(y1 - pad, 0), min(x2 + pad, mask_img.size[0]), min(y2 + pad, mask_img.size[1])
19+
return (max(x1 - pad, 0), max(y1 - pad, 0), min(x2 + pad, mask.size[0]), min(y2 + pad, mask.size[1])) if pad else box
20+
21+
22+
def get_crop_region(mask, pad=0):
23+
"""
24+
Same function as get_crop_region_v2 but handles completely black mask (all 0) differently
25+
when mask all black still return coordinates but the coordinates may be invalid ie x2>x1 or y2>y1
26+
Notes: it is possible for the coordinates to be "valid" again if pad size is sufficiently large
27+
(mask_size.x-pad, mask_size.y-pad, pad, pad)
28+
29+
Extension developer should use get_crop_region_v2 instead unless for compatibility considerations.
30+
"""
31+
mask = mask if isinstance(mask, Image.Image) else Image.fromarray(mask)
32+
if box := get_crop_region_v2(mask, pad):
33+
return box
34+
x1, y1 = mask.size
35+
x2 = y2 = 0
36+
return max(x1 - pad, 0), max(y1 - pad, 0), min(x2 + pad, mask.size[0]), min(y2 + pad, mask.size[1])
1537

1638

1739
def expand_crop_region(crop_region, processing_width, processing_height, image_width, image_height):

modules/processing.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1705,16 +1705,23 @@ def init(self, all_prompts, all_seeds, all_subseeds):
17051705
if self.inpaint_full_res:
17061706
self.mask_for_overlay = image_mask
17071707
mask = image_mask.convert('L')
1708-
crop_region = masking.get_crop_region(mask, self.inpaint_full_res_padding)
1709-
crop_region = masking.expand_crop_region(crop_region, self.width, self.height, mask.width, mask.height)
1710-
x1, y1, x2, y2 = crop_region
1711-
1712-
mask = mask.crop(crop_region)
1713-
image_mask = images.resize_image(2, mask, self.width, self.height)
1714-
self.paste_to = (x1, y1, x2-x1, y2-y1)
1715-
1716-
self.extra_generation_params["Inpaint area"] = "Only masked"
1717-
self.extra_generation_params["Masked area padding"] = self.inpaint_full_res_padding
1708+
crop_region = masking.get_crop_region_v2(mask, self.inpaint_full_res_padding)
1709+
if crop_region:
1710+
crop_region = masking.expand_crop_region(crop_region, self.width, self.height, mask.width, mask.height)
1711+
x1, y1, x2, y2 = crop_region
1712+
mask = mask.crop(crop_region)
1713+
image_mask = images.resize_image(2, mask, self.width, self.height)
1714+
self.paste_to = (x1, y1, x2-x1, y2-y1)
1715+
self.extra_generation_params["Inpaint area"] = "Only masked"
1716+
self.extra_generation_params["Masked area padding"] = self.inpaint_full_res_padding
1717+
else:
1718+
crop_region = None
1719+
image_mask = None
1720+
self.mask_for_overlay = None
1721+
self.inpaint_full_res = False
1722+
massage = 'Unable to perform "Inpaint Only mask" because mask is blank, switch to img2img mode.'
1723+
model_hijack.comments.append(massage)
1724+
logging.info(massage)
17181725
else:
17191726
image_mask = images.resize_image(self.resize_mode, image_mask, self.width, self.height)
17201727
np_mask = np.array(image_mask)
@@ -1742,6 +1749,8 @@ def init(self, all_prompts, all_seeds, all_subseeds):
17421749
image = images.resize_image(self.resize_mode, image, self.width, self.height)
17431750

17441751
if image_mask is not None:
1752+
if self.mask_for_overlay.size != (image.width, image.height):
1753+
self.mask_for_overlay = images.resize_image(self.resize_mode, self.mask_for_overlay, image.width, image.height)
17451754
image_masked = Image.new('RGBa', (image.width, image.height))
17461755
image_masked.paste(image.convert("RGBA").convert("RGBa"), mask=ImageOps.invert(self.mask_for_overlay.convert('L')))
17471756

modules/script_callbacks.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,9 @@ def remove_callbacks_for_function(callback_func):
448448
for callback_list in callback_map.values():
449449
for callback_to_remove in [cb for cb in callback_list if cb.callback == callback_func]:
450450
callback_list.remove(callback_to_remove)
451+
for ordered_callback_list in ordered_callbacks_map.values():
452+
for callback_to_remove in [cb for cb in ordered_callback_list if cb.callback == callback_func]:
453+
ordered_callback_list.remove(callback_to_remove)
451454

452455

453456
def on_app_started(callback, *, name=None):

modules/script_loading.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import importlib.util
33

44
from modules import errors
5-
import sys
65

76

87
loaded_scripts = {}
@@ -14,10 +13,6 @@ def load_module(path):
1413
module_spec.loader.exec_module(module)
1514

1615
loaded_scripts[path] = module
17-
18-
module_name, _ = os.path.splitext(os.path.basename(path))
19-
sys.modules["scripts." + module_name] = module
20-
2116
return module
2217

2318

0 commit comments

Comments
 (0)