diff --git a/html/locale_en.json b/html/locale_en.json
index 1436a6786..f17742039 100644
--- a/html/locale_en.json
+++ b/html/locale_en.json
@@ -356,7 +356,7 @@
{"id":"","label":"Enable splitting of hires batch processing","localized":"","hint":"Reduces VRAM usage when using hires fix on batches of images"},
{"id":"","label":"Load models using stream loading method","localized":"","hint":"When loading models attempt stream loading optimized for slow or network storage"},
{"id":"","label":"When loading models attempt to reuse previous model dictionary","localized":"","hint":""},
- {"id":"","label":"Disable cross-attention layer optimization","localized":"","hint":"Disable the all cross-attention optimization. May result in higher VRAM usage and longer generation times"},
+ {"id":"","label":"Disabled","localized":"","hint":""},
{"id":"","label":"xFormers","localized":"","hint":"Memory optimization. Non-Deterministic (different results each time)"},
{"id":"","label":"Scaled-Dot-Product","localized":"","hint":"Memory optimization. Non-Deterministic unless SDP memory attention is disabled."},
{"id":"","label":"Doggettx's","localized":"","hint":""},
diff --git a/modules/images.py b/modules/images.py
index 7f29983bb..98b44c4d0 100644
--- a/modules/images.py
+++ b/modules/images.py
@@ -291,7 +291,8 @@ def sanitize_filename_part(text, replace_spaces=True):
class FilenameGenerator:
replacements = {
- 'batch_number': lambda self: NOTHING if self.index <= 1 else self.index,
+ 'batch_number': lambda self: self.batch_number,
+ 'iter_number': lambda self: self.iter_number,
'cfg': lambda self: self.p and self.p.cfg_scale,
'clip_skip': lambda self: self.p and self.p.clip_skip,
'date': lambda self: datetime.datetime.now().strftime('%Y-%m-%d'),
@@ -320,12 +321,17 @@ class FilenameGenerator:
}
default_time_format = '%Y%m%d%H%M%S'
- def __init__(self, p, seed, prompt, image, index = 0):
+ def __init__(self, p, seed, prompt, image, grid=False):
self.p = p
self.seed = seed
self.prompt = prompt
self.image = image
- self.index = index if self.p is None or self.p.batch_size == 1 else self.p.batch_index + 1
+ if not grid:
+ self.batch_number = NOTHING if self.p is None or getattr(self.p, 'batch_size', 1) == 1 else (self.p.batch_index + 1 if hasattr(self.p, 'batch_index') else NOTHING)
+ self.iter_number = NOTHING if self.p is None or getattr(self.p, 'n_iter', 1) == 1 else (self.p.iteration + 1 if hasattr(self.p, 'iteration') else NOTHING)
+ else:
+ self.batch_number = NOTHING
+ self.iter_number = NOTHING
def hasprompt(self, *args):
lower = self.prompt.lower()
@@ -449,7 +455,7 @@ def atomically_save_image():
image_format = 'JPEG'
if shared.opts.image_watermark_enabled:
image = set_watermark(image, shared.opts.image_watermark)
- shared.log.debug(f'Saving: image={fn} type={image_format} size={image.width}x{image.height}')
+ shared.log.debug(f'Saving: image="{fn}" type={image_format} size={image.width}x{image.height}')
# actual save
exifinfo = (exifinfo or "") if shared.opts.image_metadata else ""
if image_format == 'PNG':
@@ -489,8 +495,14 @@ def atomically_save_image():
with open(os.path.join(paths.data_path, "params.txt"), "w", encoding="utf8") as file:
file.write(exifinfo)
if shared.opts.save_log_fn != '' and len(exifinfo) > 0:
- entry = { 'filename': filename, 'time': datetime.datetime.now().isoformat(), 'info': exifinfo }
- shared.writefile(entry, os.path.join(paths.data_path, shared.opts.save_log_fn), mode='a+')
+ fn = os.path.join(paths.data_path, shared.opts.save_log_fn)
+ entries = shared.readfile(fn)
+ idx = len(list(entries))
+ if idx == 0:
+ entries = []
+ entry = { 'id': idx, 'filename': filename, 'time': datetime.datetime.now().isoformat(), 'info': exifinfo }
+ entries.append(entry)
+ shared.writefile(entries, fn, mode='w')
save_queue.task_done()
@@ -499,7 +511,7 @@ save_thread = threading.Thread(target=atomically_save_image, daemon=True)
save_thread.start()
-def save_image(image, path, basename, seed=None, prompt=None, extension='jpg', info=None, short_filename=False, no_prompt=False, grid=False, pnginfo_section_name='parameters', p=None, existing_info=None, forced_filename=None, suffix="", save_to_dirs=None, index=0):
+def save_image(image, path, basename, seed=None, prompt=None, extension='jpg', info=None, short_filename=False, no_prompt=False, grid=False, pnginfo_section_name='parameters', p=None, existing_info=None, forced_filename=None, suffix="", save_to_dirs=None):
"""Save an image.
Args:
image (`PIL.Image`):
@@ -537,7 +549,7 @@ def save_image(image, path, basename, seed=None, prompt=None, extension='jpg', i
return None, None
if path is None or len(path) == 0: # set default path to avoid errors when functions are triggered manually or via api and param is not set
path = shared.opts.outdir_save
- namegen = FilenameGenerator(p, seed, prompt, image, index)
+ namegen = FilenameGenerator(p, seed, prompt, image, grid=grid)
if save_to_dirs is None:
save_to_dirs = (grid and shared.opts.grid_save_to_dirs) or (not grid and shared.opts.save_to_dirs and not no_prompt)
if save_to_dirs:
diff --git a/modules/processing.py b/modules/processing.py
index 772bbafba..90c4038af 100644
--- a/modules/processing.py
+++ b/modules/processing.py
@@ -472,6 +472,7 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts, all_seeds, all_su
"CFG scale": p.cfg_scale,
"Size": f"{p.width}x{p.height}",
"Batch": f'{p.n_iter}x{p.batch_size}' if p.n_iter > 1 or p.batch_size > 1 else None,
+ "Index": f'{p.iteration + 1}x{index + 1}' if (p.n_iter > 1 or p.batch_size > 1) and index >= 0 else None,
"Parser": shared.opts.prompt_attention,
"Model": None if (not shared.opts.add_model_name_to_info) or (not shared.sd_model.sd_checkpoint_info.model_name) else shared.sd_model.sd_checkpoint_info.model_name.replace(',', '').replace(':', ''),
"Model hash": getattr(p, 'sd_model_hash', None if (not shared.opts.add_model_hash_to_info) or (not shared.sd_model.sd_model_hash) else shared.sd_model.sd_model_hash),
@@ -798,7 +799,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
p.scripts.postprocess_batch_list(p, batch_params, batch_number=n)
x_samples_ddim = batch_params.images
- def infotext(index=0): # pylint: disable=function-redefined # noqa: F811
+ def infotext(index): # pylint: disable=function-redefined # noqa: F811
return create_infotext(p, p.prompts, p.seeds, p.subseeds, index=index, all_negative_prompts=p.negative_prompts)
for i, x_sample in enumerate(x_samples_ddim):
@@ -816,7 +817,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
p.restore_faces = False
info = infotext(i)
p.restore_faces = orig
- images.save_image(Image.fromarray(x_sample), path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-face-restoration")
+ images.save_image(Image.fromarray(x_sample), path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-face-restore")
p.ops.append('face')
x_sample = modules.face_restoration.restore_faces(x_sample)
image = Image.fromarray(x_sample)
@@ -831,7 +832,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
info = infotext(i)
p.color_corrections = orig
image_without_cc = apply_overlay(image, p.paste_to, i, p.overlay_images)
- images.save_image(image_without_cc, path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-color-correction")
+ images.save_image(image_without_cc, path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-color-correct")
p.ops.append('color')
image = apply_color_correction(p.color_corrections[i], image)
image = apply_overlay(image, p.paste_to, i, p.overlay_images)
@@ -840,7 +841,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
image.info["parameters"] = text
output_images.append(image)
if shared.opts.samples_save and not p.do_not_save_samples:
- images.save_image(image, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=text, p=p)
+ images.save_image(image, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=text, p=p) # main save image
if hasattr(p, 'mask_for_overlay') and p.mask_for_overlay and any([shared.opts.save_mask, shared.opts.save_mask_composite, shared.opts.return_mask, shared.opts.return_mask_composite]):
image_mask = p.mask_for_overlay.convert('RGB')
image_mask_composite = Image.composite(image.convert('RGBA').convert('RGBa'), Image.new('RGBa', image.size), images.resize_image(3, p.mask_for_overlay, image.width, image.height).convert('L')).convert('RGBA')
@@ -865,13 +866,13 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
if images.check_grid_size(output_images):
grid = images.image_grid(output_images, p.batch_size)
if shared.opts.return_grid:
- text = infotext()
+ text = infotext(-1)
infotexts.insert(0, text)
grid.info["parameters"] = text
output_images.insert(0, grid)
index_of_first_image = 1
if shared.opts.grid_save:
- images.save_image(grid, p.outpath_grids, "grid", p.all_seeds[0], p.all_prompts[0], shared.opts.grid_format, info=infotext(), short_filename=not shared.opts.grid_extended_filename, p=p, grid=True)
+ images.save_image(grid, p.outpath_grids, "", p.all_seeds[0], p.all_prompts[0], shared.opts.grid_format, info=infotext(-1), short_filename=not shared.opts.grid_extended_filename, p=p, grid=True, suffix="-grid") # main save grid
if not p.disable_extra_networks and extra_network_data:
modules.extra_networks.deactivate(p, extra_network_data)
@@ -880,7 +881,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
p,
images_list=output_images,
seed=p.all_seeds[0],
- info=infotext(),
+ info=infotext(0),
comments="\n".join(comments),
subseed=p.all_subseeds[0],
index_of_first_image=index_of_first_image,
@@ -987,7 +988,7 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing):
info = create_infotext(self, self.all_prompts, self.all_seeds, self.all_subseeds, [], iteration=self.iteration, position_in_batch=index)
self.extra_generation_params = orig1
self.restore_faces = orig2
- images.save_image(image, self.outpath_samples, "", seeds[index], prompts[index], shared.opts.samples_format, info=info, suffix="-before-hires", index=index+1)
+ images.save_image(image, self.outpath_samples, "", seeds[index], prompts[index], shared.opts.samples_format, info=info, suffix="-before-hires")
if shared.backend == shared.Backend.DIFFUSERS:
modules.sd_models.set_diffuser_pipe(self.sd_model, modules.sd_models.DiffusersTaskType.TEXT_2_IMAGE)
@@ -1140,7 +1141,7 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing):
self.init_img_width = img.width # pylint: disable=attribute-defined-outside-init
self.init_img_height = img.height # pylint: disable=attribute-defined-outside-init
if shared.opts.save_init_img:
- images.save_image(img, path=shared.opts.outdir_init_images, basename=None, forced_filename=self.init_img_hash, save_to_dirs=False)
+ images.save_image(img, path=shared.opts.outdir_init_images, basename=None, forced_filename=self.init_img_hash, save_to_dirs=False, suffix="-init-image")
image = images.flatten(img, shared.opts.img2img_background_color)
if crop_region is None and self.resize_mode != 4:
image = images.resize_image(self.resize_mode, image, self.width, self.height)
diff --git a/modules/sd_hijack.py b/modules/sd_hijack.py
index 5cd407e19..3f465a600 100644
--- a/modules/sd_hijack.py
+++ b/modules/sd_hijack.py
@@ -37,45 +37,38 @@ def apply_optimizations():
can_use_sdp = hasattr(torch.nn.functional, "scaled_dot_product_attention") and callable(torch.nn.functional.scaled_dot_product_attention)
if devices.device == torch.device("cpu"):
if opts.cross_attention_optimization == "Scaled-Dot-Product":
- shared.log.warning("Scaled dot product cross attention is not available on CPU")
+ shared.log.warning("Cross-attention: Scaled dot product is not available on CPU")
can_use_sdp = False
if opts.cross_attention_optimization == "xFormers":
- shared.log.warning("xFormers cross attention is not available on CPU")
+ shared.log.warning("Cross-attention: xFormers is not available on CPU")
shared.xformers_available = False
- if opts.cross_attention_optimization == "Disable cross-attention layer optimization":
- shared.log.warning("Cross-attention optimization disabled")
+ shared.log.info(f"Cross-attention: optimization={opts.cross_attention_optimization} options={opts.cross_attention_options}")
+ if opts.cross_attention_optimization == "Disabled":
optimization_method = 'none'
if can_use_sdp and opts.cross_attention_optimization == "Scaled-Dot-Product" and 'SDP disable memory attention' in opts.cross_attention_options:
- shared.log.info("Applying scaled dot product cross attention optimization (without memory efficient attention)")
ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.scaled_dot_product_no_mem_attention_forward
ldm.modules.diffusionmodules.model.AttnBlock.forward = sd_hijack_optimizations.sdp_no_mem_attnblock_forward
optimization_method = 'sdp-no-mem'
elif can_use_sdp and opts.cross_attention_optimization == "Scaled-Dot-Product":
- shared.log.info("Applying scaled dot product cross attention optimization")
ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.scaled_dot_product_attention_forward
ldm.modules.diffusionmodules.model.AttnBlock.forward = sd_hijack_optimizations.sdp_attnblock_forward
optimization_method = 'sdp'
if shared.xformers_available and opts.cross_attention_optimization == "xFormers":
- shared.log.info("Applying xformers cross attention optimization")
ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.xformers_attention_forward
ldm.modules.diffusionmodules.model.AttnBlock.forward = sd_hijack_optimizations.xformers_attnblock_forward
optimization_method = 'xformers'
if opts.cross_attention_optimization == "Sub-quadratic":
- shared.log.info("Applying sub-quadratic cross attention optimization")
ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.sub_quad_attention_forward
ldm.modules.diffusionmodules.model.AttnBlock.forward = sd_hijack_optimizations.sub_quad_attnblock_forward
optimization_method = 'sub-quadratic'
if opts.cross_attention_optimization == "Split attention":
- shared.log.info("Applying split attention optimization")
ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.split_cross_attention_forward_v1
optimization_method = 'v1'
if opts.cross_attention_optimization == "InvokeAI's":
- shared.log.info("Applying InvokeAI cross attention optimization")
ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.split_cross_attention_forward_invokeAI
optimization_method = 'invokeai'
if opts.cross_attention_optimization == "Doggettx's":
- shared.log.info("Applying Doggettx cross attention optimization")
ldm.modules.attention.CrossAttention.forward = sd_hijack_optimizations.split_cross_attention_forward
ldm.modules.diffusionmodules.model.AttnBlock.forward = sd_hijack_optimizations.cross_attention_attnblock_forward
optimization_method = 'doggettx'
diff --git a/modules/sd_models.py b/modules/sd_models.py
index 5278015e6..d77134238 100644
--- a/modules/sd_models.py
+++ b/modules/sd_models.py
@@ -261,7 +261,7 @@ def select_checkpoint(op='model'):
return None
checkpoint_info = get_closet_checkpoint_match(model_checkpoint)
if checkpoint_info is not None:
- shared.log.debug(f'Select checkpoint: {op} {checkpoint_info.title if checkpoint_info is not None else None}')
+ shared.log.debug(f'Select checkpoint: {op}="{checkpoint_info.title if checkpoint_info is not None else None}"')
return checkpoint_info
if len(checkpoints_list) == 0 and not shared.cmd_opts.no_download:
shared.log.error("Cannot run without a checkpoint")
@@ -275,7 +275,7 @@ def select_checkpoint(op='model'):
shared.log.info("Selecting first available checkpoint")
# shared.log.warning(f"Loading fallback checkpoint: {checkpoint_info.title}")
shared.opts.data['sd_model_checkpoint'] = checkpoint_info.title
- shared.log.debug(f'Select checkpoint: {checkpoint_info.title if checkpoint_info is not None else None}')
+ shared.log.debug(f'Select checkpoint: {op}="{checkpoint_info.title if checkpoint_info is not None else None}"')
return checkpoint_info
diff --git a/modules/shared.py b/modules/shared.py
index f1da73266..d69f0ff3b 100644
--- a/modules/shared.py
+++ b/modules/shared.py
@@ -340,7 +340,7 @@ def readfile(filename, silent=False):
return data
-def writefile(data, filename, mode='w'):
+def writefile(data, filename, mode='w', silent=False):
def default(obj):
log.error(f"Saving: {filename} not a valid object: {obj}")
@@ -350,7 +350,8 @@ def writefile(data, filename, mode='w'):
with fasteners.InterProcessLock(f"{filename}.lock"):
# skipkeys=True, ensure_ascii=True, check_circular=True, allow_nan=True
output = json.dumps(data, indent=2, default=default)
- log.debug(f'Saving: {filename} len={len(output)}')
+ if not silent:
+ log.debug(f'Saving: {filename} len={len(output)}')
with open(filename, mode, encoding="utf8") as file:
file.write(output)
except Exception as e:
diff --git a/modules/shared_items.py b/modules/shared_items.py
index 8bb05b064..061fe4929 100644
--- a/modules/shared_items.py
+++ b/modules/shared_items.py
@@ -20,7 +20,7 @@ def refresh_vae_list():
def list_crossattention():
return [
- "Disable cross-attention layer optimization",
+ "Disabled",
"xFormers",
"Scaled-Dot-Product",
"Doggettx's",
diff --git a/modules/ui_common.py b/modules/ui_common.py
index fa5569162..8d1bb48fd 100644
--- a/modules/ui_common.py
+++ b/modules/ui_common.py
@@ -75,17 +75,21 @@ def save_files(js_data, images, html_info, index):
class PObject: # pylint: disable=too-few-public-methods
def __init__(self, d=None):
if d is not None:
- for key, value in d.items():
- setattr(self, key, value)
- self.seed = getattr(self, 'seed', None) or getattr(self, 'Seed', None)
+ for k, v in d.items():
+ setattr(self, k, v)
self.prompt = getattr(self, 'prompt', None) or getattr(self, 'Prompt', None)
- self.all_seeds = getattr(self, 'all_seeds', [self.seed])
self.all_prompts = getattr(self, 'all_prompts', [self.prompt])
+ self.negative_prompt = getattr(self, 'negative_prompt', None)
+ self.all_negative_prompt = getattr(self, 'all_negative_prompts', [self.negative_prompt])
+ self.seed = getattr(self, 'seed', None) or getattr(self, 'Seed', None)
+ self.all_seeds = getattr(self, 'all_seeds', [self.seed])
+ self.subseed = getattr(self, 'subseed', None)
+ self.all_subseeds = getattr(self, 'all_subseeds', [self.subseed])
+ self.width = getattr(self, 'width', None)
+ self.height = getattr(self, 'height', None)
+ self.index_of_first_image = getattr(self, 'index_of_first_image', 0)
self.infotexts = getattr(self, 'infotexts', [html_info])
self.infotext = self.infotexts[0] if len(self.infotexts) > 0 else html_info
- self.index_of_first_image = getattr(self, 'index_of_first_image', 0)
- self.batch_size = 1
-
try:
data = json.loads(js_data)
except Exception:
@@ -95,8 +99,6 @@ def save_files(js_data, images, html_info, index):
if index > -1 and shared.opts.save_selected_only and (index >= p.index_of_first_image): # ensures we are looking at a specific non-grid picture, and we have save_selected_only # pylint: disable=no-member
images = [images[index]]
start_index = index
- else:
- p.batch_size = len(images)
filenames = []
fullfns = []
for image_index, filedata in enumerate(images, start_index):
@@ -114,12 +116,12 @@ def save_files(js_data, images, html_info, index):
fullfns.append(fullfn)
destination = shared.opts.outdir_save
if shared.opts.use_save_to_dirs_for_ui:
- namegen = modules.images.FilenameGenerator(p, seed=p.all_seeds[i], prompt=p.all_prompts[i], image=None, index=image_index) # pylint: disable=no-member
+ namegen = modules.images.FilenameGenerator(p, seed=p.all_seeds[i], prompt=p.all_prompts[i], image=None) # pylint: disable=no-member
dirname = namegen.apply(shared.opts.directories_filename_pattern or "[prompt_words]").lstrip(' ').rstrip('\\ /')
destination = os.path.join(destination, dirname)
os.makedirs(destination, exist_ok = True)
shutil.copy(fullfn, destination)
- shared.log.info(f"Copying image: {fullfn} -> {destination}")
+ shared.log.info(f'Copying image: file="{fullfn}" folder="{destination}"')
tgt_filename = os.path.join(destination, os.path.basename(fullfn))
modules.script_callbacks.image_save_btn_callback(tgt_filename)
else:
diff --git a/modules/ui_tempdir.py b/modules/ui_tempdir.py
index 945a004b6..38b4b5923 100644
--- a/modules/ui_tempdir.py
+++ b/modules/ui_tempdir.py
@@ -61,7 +61,7 @@ def pil_to_temp_file(self, img, dir: str, format="png") -> str: # pylint: disabl
file_obj = tempfile.NamedTemporaryFile(delete=False, suffix=".png", dir=dir)
img.save(file_obj, pnginfo=(metadata if use_metadata else None))
name = file_obj.name
- shared.log.debug(f'Saving temp image: {name}')
+ shared.log.debug(f'Saving temp: image="{name}"')
return name