| from pathlib import Path
|
|
|
| import numpy as np
|
| import datetime
|
| import random
|
| import math
|
| import os
|
| import cv2
|
| import re
|
| from typing import List, Tuple, AnyStr, NamedTuple
|
|
|
| import json
|
| import hashlib
|
|
|
| from PIL import Image
|
|
|
| import modules.config
|
| import modules.sdxl_styles
|
| from modules.flags import Performance
|
|
|
| LANCZOS = (Image.Resampling.LANCZOS if hasattr(Image, 'Resampling') else Image.LANCZOS)
|
|
|
|
|
|
|
|
|
| LORAS_PROMPT_PATTERN = re.compile(r"(<lora:([^:]+):([+-]?(?:\d+(?:\.\d*)?|\.\d+))>)", re.X)
|
|
|
| HASH_SHA256_LENGTH = 10
|
|
|
|
|
| def erode_or_dilate(x, k):
|
| k = int(k)
|
| if k > 0:
|
| return cv2.dilate(x, kernel=np.ones(shape=(3, 3), dtype=np.uint8), iterations=k)
|
| if k < 0:
|
| return cv2.erode(x, kernel=np.ones(shape=(3, 3), dtype=np.uint8), iterations=-k)
|
| return x
|
|
|
|
|
| def resample_image(im, width, height):
|
| im = Image.fromarray(im)
|
| im = im.resize((int(width), int(height)), resample=LANCZOS)
|
| return np.array(im)
|
|
|
|
|
| def resize_image(im, width, height, resize_mode=1):
|
| """
|
| Resizes an image with the specified resize_mode, width, and height.
|
|
|
| Args:
|
| resize_mode: The mode to use when resizing the image.
|
| 0: Resize the image to the specified width and height.
|
| 1: Resize the image to fill the specified width and height, maintaining the aspect ratio, and then center the image within the dimensions, cropping the excess.
|
| 2: Resize the image to fit within the specified width and height, maintaining the aspect ratio, and then center the image within the dimensions, filling empty with data from image.
|
| im: The image to resize.
|
| width: The width to resize the image to.
|
| height: The height to resize the image to.
|
| """
|
|
|
| im = Image.fromarray(im)
|
|
|
| def resize(im, w, h):
|
| return im.resize((w, h), resample=LANCZOS)
|
|
|
| if resize_mode == 0:
|
| res = resize(im, width, height)
|
|
|
| elif resize_mode == 1:
|
| ratio = width / height
|
| src_ratio = im.width / im.height
|
|
|
| src_w = width if ratio > src_ratio else im.width * height // im.height
|
| src_h = height if ratio <= src_ratio else im.height * width // im.width
|
|
|
| resized = resize(im, src_w, src_h)
|
| res = Image.new("RGB", (width, height))
|
| res.paste(resized, box=(width // 2 - src_w // 2, height // 2 - src_h // 2))
|
|
|
| else:
|
| ratio = width / height
|
| src_ratio = im.width / im.height
|
|
|
| src_w = width if ratio < src_ratio else im.width * height // im.height
|
| src_h = height if ratio >= src_ratio else im.height * width // im.width
|
|
|
| resized = resize(im, src_w, src_h)
|
| res = Image.new("RGB", (width, height))
|
| res.paste(resized, box=(width // 2 - src_w // 2, height // 2 - src_h // 2))
|
|
|
| if ratio < src_ratio:
|
| fill_height = height // 2 - src_h // 2
|
| if fill_height > 0:
|
| res.paste(resized.resize((width, fill_height), box=(0, 0, width, 0)), box=(0, 0))
|
| res.paste(resized.resize((width, fill_height), box=(0, resized.height, width, resized.height)), box=(0, fill_height + src_h))
|
| elif ratio > src_ratio:
|
| fill_width = width // 2 - src_w // 2
|
| if fill_width > 0:
|
| res.paste(resized.resize((fill_width, height), box=(0, 0, 0, height)), box=(0, 0))
|
| res.paste(resized.resize((fill_width, height), box=(resized.width, 0, resized.width, height)), box=(fill_width + src_w, 0))
|
|
|
| return np.array(res)
|
|
|
|
|
| def get_shape_ceil(h, w):
|
| return math.ceil(((h * w) ** 0.5) / 64.0) * 64.0
|
|
|
|
|
| def get_image_shape_ceil(im):
|
| H, W = im.shape[:2]
|
| return get_shape_ceil(H, W)
|
|
|
|
|
| def set_image_shape_ceil(im, shape_ceil):
|
| shape_ceil = float(shape_ceil)
|
|
|
| H_origin, W_origin, _ = im.shape
|
| H, W = H_origin, W_origin
|
|
|
| for _ in range(256):
|
| current_shape_ceil = get_shape_ceil(H, W)
|
| if abs(current_shape_ceil - shape_ceil) < 0.1:
|
| break
|
| k = shape_ceil / current_shape_ceil
|
| H = int(round(float(H) * k / 64.0) * 64)
|
| W = int(round(float(W) * k / 64.0) * 64)
|
|
|
| if H == H_origin and W == W_origin:
|
| return im
|
|
|
| return resample_image(im, width=W, height=H)
|
|
|
|
|
| def HWC3(x):
|
| assert x.dtype == np.uint8
|
| if x.ndim == 2:
|
| x = x[:, :, None]
|
| assert x.ndim == 3
|
| H, W, C = x.shape
|
| assert C == 1 or C == 3 or C == 4
|
| if C == 3:
|
| return x
|
| if C == 1:
|
| return np.concatenate([x, x, x], axis=2)
|
| if C == 4:
|
| color = x[:, :, 0:3].astype(np.float32)
|
| alpha = x[:, :, 3:4].astype(np.float32) / 255.0
|
| y = color * alpha + 255.0 * (1.0 - alpha)
|
| y = y.clip(0, 255).astype(np.uint8)
|
| return y
|
|
|
|
|
| def remove_empty_str(items, default=None):
|
| items = [x for x in items if x != ""]
|
| if len(items) == 0 and default is not None:
|
| return [default]
|
| return items
|
|
|
|
|
| def join_prompts(*args, **kwargs):
|
| prompts = [str(x) for x in args if str(x) != ""]
|
| if len(prompts) == 0:
|
| return ""
|
| if len(prompts) == 1:
|
| return prompts[0]
|
| return ', '.join(prompts)
|
|
|
|
|
| def generate_temp_filename(folder='./outputs/', extension='png'):
|
| current_time = datetime.datetime.now()
|
| date_string = current_time.strftime("%Y-%m-%d")
|
| time_string = current_time.strftime("%Y-%m-%d_%H-%M-%S")
|
| random_number = random.randint(1000, 9999)
|
| filename = f"{time_string}_{random_number}.{extension}"
|
| result = os.path.join(folder, date_string, filename)
|
| return date_string, os.path.abspath(result), filename
|
|
|
|
|
| def sha256(filename, use_addnet_hash=False, length=HASH_SHA256_LENGTH):
|
| if use_addnet_hash:
|
| with open(filename, "rb") as file:
|
| sha256_value = addnet_hash_safetensors(file)
|
| else:
|
| sha256_value = calculate_sha256(filename)
|
|
|
| return sha256_value[:length] if length is not None else sha256_value
|
|
|
|
|
| def addnet_hash_safetensors(b):
|
| """kohya-ss hash for safetensors from https://github.com/kohya-ss/sd-scripts/blob/main/library/train_util.py"""
|
| hash_sha256 = hashlib.sha256()
|
| blksize = 1024 * 1024
|
|
|
| b.seek(0)
|
| header = b.read(8)
|
| n = int.from_bytes(header, "little")
|
|
|
| offset = n + 8
|
| b.seek(offset)
|
| for chunk in iter(lambda: b.read(blksize), b""):
|
| hash_sha256.update(chunk)
|
|
|
| return hash_sha256.hexdigest()
|
|
|
|
|
| def calculate_sha256(filename) -> str:
|
| hash_sha256 = hashlib.sha256()
|
| blksize = 1024 * 1024
|
|
|
| with open(filename, "rb") as f:
|
| for chunk in iter(lambda: f.read(blksize), b""):
|
| hash_sha256.update(chunk)
|
|
|
| return hash_sha256.hexdigest()
|
|
|
|
|
| def quote(text):
|
| if ',' not in str(text) and '\n' not in str(text) and ':' not in str(text):
|
| return text
|
|
|
| return json.dumps(text, ensure_ascii=False)
|
|
|
|
|
| def unquote(text):
|
| if len(text) == 0 or text[0] != '"' or text[-1] != '"':
|
| return text
|
|
|
| try:
|
| return json.loads(text)
|
| except Exception:
|
| return text
|
|
|
|
|
| def unwrap_style_text_from_prompt(style_text, prompt):
|
| """
|
| Checks the prompt to see if the style text is wrapped around it. If so,
|
| returns True plus the prompt text without the style text. Otherwise, returns
|
| False with the original prompt.
|
|
|
| Note that the "cleaned" version of the style text is only used for matching
|
| purposes here. It isn't returned; the original style text is not modified.
|
| """
|
| stripped_prompt = prompt
|
| stripped_style_text = style_text
|
| if "{prompt}" in stripped_style_text:
|
|
|
|
|
| try:
|
| left, right = stripped_style_text.split("{prompt}", 2)
|
| except ValueError as e:
|
|
|
|
|
| print(f"Unable to compare style text to prompt:\n{style_text}")
|
| print(f"Error: {e}")
|
| return False, prompt, ''
|
|
|
| left_pos = stripped_prompt.find(left)
|
| right_pos = stripped_prompt.find(right)
|
| if 0 <= left_pos < right_pos:
|
| real_prompt = stripped_prompt[left_pos + len(left):right_pos]
|
| prompt = stripped_prompt.replace(left + real_prompt + right, '', 1)
|
| if prompt.startswith(", "):
|
| prompt = prompt[2:]
|
| if prompt.endswith(", "):
|
| prompt = prompt[:-2]
|
| return True, prompt, real_prompt
|
| else:
|
|
|
|
|
| if stripped_prompt.endswith(stripped_style_text):
|
| prompt = stripped_prompt[: len(stripped_prompt) - len(stripped_style_text)]
|
| if prompt.endswith(", "):
|
| prompt = prompt[:-2]
|
| return True, prompt, prompt
|
|
|
| return False, prompt, ''
|
|
|
|
|
| def extract_original_prompts(style, prompt, negative_prompt):
|
| """
|
| Takes a style and compares it to the prompt and negative prompt. If the style
|
| matches, returns True plus the prompt and negative prompt with the style text
|
| removed. Otherwise, returns False with the original prompt and negative prompt.
|
| """
|
| if not style.prompt and not style.negative_prompt:
|
| return False, prompt, negative_prompt
|
|
|
| match_positive, extracted_positive, real_prompt = unwrap_style_text_from_prompt(
|
| style.prompt, prompt
|
| )
|
| if not match_positive:
|
| return False, prompt, negative_prompt, ''
|
|
|
| match_negative, extracted_negative, _ = unwrap_style_text_from_prompt(
|
| style.negative_prompt, negative_prompt
|
| )
|
| if not match_negative:
|
| return False, prompt, negative_prompt, ''
|
|
|
| return True, extracted_positive, extracted_negative, real_prompt
|
|
|
|
|
| def extract_styles_from_prompt(prompt, negative_prompt):
|
| extracted = []
|
| applicable_styles = []
|
|
|
| for style_name, (style_prompt, style_negative_prompt) in modules.sdxl_styles.styles.items():
|
| applicable_styles.append(PromptStyle(name=style_name, prompt=style_prompt, negative_prompt=style_negative_prompt))
|
|
|
| real_prompt = ''
|
|
|
| while True:
|
| found_style = None
|
|
|
| for style in applicable_styles:
|
| is_match, new_prompt, new_neg_prompt, new_real_prompt = extract_original_prompts(
|
| style, prompt, negative_prompt
|
| )
|
| if is_match:
|
| found_style = style
|
| prompt = new_prompt
|
| negative_prompt = new_neg_prompt
|
| if real_prompt == '' and new_real_prompt != '' and new_real_prompt != prompt:
|
| real_prompt = new_real_prompt
|
| break
|
|
|
| if not found_style:
|
| break
|
|
|
| applicable_styles.remove(found_style)
|
| extracted.append(found_style.name)
|
|
|
|
|
| if prompt != '':
|
| if real_prompt != '':
|
| extracted.append(modules.sdxl_styles.fooocus_expansion)
|
| else:
|
|
|
| first_word = prompt.split(', ')[0]
|
| first_word_positions = [i for i in range(len(prompt)) if prompt.startswith(first_word, i)]
|
| if len(first_word_positions) > 1:
|
| real_prompt = prompt[:first_word_positions[-1]]
|
| extracted.append(modules.sdxl_styles.fooocus_expansion)
|
| if real_prompt.endswith(', '):
|
| real_prompt = real_prompt[:-2]
|
|
|
| return list(reversed(extracted)), real_prompt, negative_prompt
|
|
|
|
|
| class PromptStyle(NamedTuple):
|
| name: str
|
| prompt: str
|
| negative_prompt: str
|
|
|
|
|
| def is_json(data: str) -> bool:
|
| try:
|
| loaded_json = json.loads(data)
|
| assert isinstance(loaded_json, dict)
|
| except (ValueError, AssertionError):
|
| return False
|
| return True
|
|
|
|
|
| def get_filname_by_stem(lora_name, filenames: List[str]) -> str | None:
|
| for filename in filenames:
|
| path = Path(filename)
|
| if lora_name == path.stem:
|
| return filename
|
| return None
|
|
|
|
|
| def get_file_from_folder_list(name, folders):
|
| if not isinstance(folders, list):
|
| folders = [folders]
|
|
|
| for folder in folders:
|
| filename = os.path.abspath(os.path.realpath(os.path.join(folder, name)))
|
| if os.path.isfile(filename):
|
| return filename
|
|
|
| return os.path.abspath(os.path.realpath(os.path.join(folders[0], name)))
|
|
|
|
|
| def get_enabled_loras(loras: list, remove_none=True) -> list:
|
| return [(lora[1], lora[2]) for lora in loras if lora[0] and (lora[1] != 'None' if remove_none else True)]
|
|
|
|
|
| def parse_lora_references_from_prompt(prompt: str, loras: List[Tuple[AnyStr, float]], loras_limit: int = 5,
|
| skip_file_check=False, prompt_cleanup=True, deduplicate_loras=True,
|
| lora_filenames=None) -> tuple[List[Tuple[AnyStr, float]], str]:
|
|
|
| loras = loras.copy()
|
|
|
| if lora_filenames is None:
|
| lora_filenames = []
|
|
|
| found_loras = []
|
| prompt_without_loras = ''
|
| cleaned_prompt = ''
|
|
|
| for token in prompt.split(','):
|
| matches = LORAS_PROMPT_PATTERN.findall(token)
|
|
|
| if len(matches) == 0:
|
| prompt_without_loras += token + ', '
|
| continue
|
| for match in matches:
|
| lora_name = match[1] + '.safetensors'
|
| if not skip_file_check:
|
| lora_name = get_filname_by_stem(match[1], lora_filenames)
|
| if lora_name is not None:
|
| found_loras.append((lora_name, float(match[2])))
|
| token = token.replace(match[0], '')
|
| prompt_without_loras += token + ', '
|
|
|
| if prompt_without_loras != '':
|
| cleaned_prompt = prompt_without_loras[:-2]
|
|
|
| if prompt_cleanup:
|
| cleaned_prompt = cleanup_prompt(prompt_without_loras)
|
|
|
| new_loras = []
|
| lora_names = [lora[0] for lora in loras]
|
| for found_lora in found_loras:
|
| if deduplicate_loras and (found_lora[0] in lora_names or found_lora in new_loras):
|
| continue
|
| new_loras.append(found_lora)
|
|
|
| if len(new_loras) == 0:
|
| return loras, cleaned_prompt
|
|
|
| updated_loras = []
|
| for lora in loras + new_loras:
|
| if lora[0] != "None":
|
| updated_loras.append(lora)
|
|
|
| return updated_loras[:loras_limit], cleaned_prompt
|
|
|
|
|
| def remove_performance_lora(filenames: list, performance: Performance | None):
|
| loras_without_performance = filenames.copy()
|
|
|
| if performance is None:
|
| return loras_without_performance
|
|
|
| performance_lora = performance.lora_filename()
|
|
|
| for filename in filenames:
|
| path = Path(filename)
|
| if performance_lora == path.name:
|
| loras_without_performance.remove(filename)
|
|
|
| return loras_without_performance
|
|
|
|
|
| def cleanup_prompt(prompt):
|
| prompt = re.sub(' +', ' ', prompt)
|
| prompt = re.sub(',+', ',', prompt)
|
| cleaned_prompt = ''
|
| for token in prompt.split(','):
|
| token = token.strip()
|
| if token == '':
|
| continue
|
| cleaned_prompt += token + ', '
|
| return cleaned_prompt[:-2]
|
|
|
|
|
| def apply_wildcards(wildcard_text, rng, i, read_wildcards_in_order) -> str:
|
| for _ in range(modules.config.wildcards_max_bfs_depth):
|
| placeholders = re.findall(r'__([\w-]+)__', wildcard_text)
|
| if len(placeholders) == 0:
|
| return wildcard_text
|
|
|
| print(f'[Wildcards] processing: {wildcard_text}')
|
| for placeholder in placeholders:
|
| try:
|
| matches = [x for x in modules.config.wildcard_filenames if os.path.splitext(os.path.basename(x))[0] == placeholder]
|
| words = open(os.path.join(modules.config.path_wildcards, matches[0]), encoding='utf-8').read().splitlines()
|
| words = [x for x in words if x != '']
|
| assert len(words) > 0
|
| if read_wildcards_in_order:
|
| wildcard_text = wildcard_text.replace(f'__{placeholder}__', words[i % len(words)], 1)
|
| else:
|
| wildcard_text = wildcard_text.replace(f'__{placeholder}__', rng.choice(words), 1)
|
| except:
|
| print(f'[Wildcards] Warning: {placeholder}.txt missing or empty. '
|
| f'Using "{placeholder}" as a normal word.')
|
| wildcard_text = wildcard_text.replace(f'__{placeholder}__', placeholder)
|
| print(f'[Wildcards] {wildcard_text}')
|
|
|
| print(f'[Wildcards] BFS stack overflow. Current text: {wildcard_text}')
|
| return wildcard_text
|
|
|
|
|
| def get_image_size_info(image: np.ndarray, aspect_ratios: list) -> str:
|
| try:
|
| image = Image.fromarray(np.uint8(image))
|
| width, height = image.size
|
| ratio = round(width / height, 2)
|
| gcd = math.gcd(width, height)
|
| lcm_ratio = f'{width // gcd}:{height // gcd}'
|
| size_info = f'Image Size: {width} x {height}, Ratio: {ratio}, {lcm_ratio}'
|
|
|
| closest_ratio = min(aspect_ratios, key=lambda x: abs(ratio - float(x.split('*')[0]) / float(x.split('*')[1])))
|
| recommended_width, recommended_height = map(int, closest_ratio.split('*'))
|
| recommended_ratio = round(recommended_width / recommended_height, 2)
|
| recommended_gcd = math.gcd(recommended_width, recommended_height)
|
| recommended_lcm_ratio = f'{recommended_width // recommended_gcd}:{recommended_height // recommended_gcd}'
|
|
|
| size_info = f'{width} x {height}, {ratio}, {lcm_ratio}'
|
| size_info += f'\n{recommended_width} x {recommended_height}, {recommended_ratio}, {recommended_lcm_ratio}'
|
|
|
| return size_info
|
| except Exception as e:
|
| return f'Error reading image: {e}'
|
|
|