| 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): |
| print(f"Calculating sha256 for {filename}: ", end='') |
| if use_addnet_hash: |
| with open(filename, "rb") as file: |
| sha256_value = addnet_hash_safetensors(file) |
| else: |
| sha256_value = calculate_sha256(filename) |
| print(f"{sha256_value}") |
|
|
| 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 makedirs_with_log(path): |
| try: |
| os.makedirs(path, exist_ok=True) |
| except OSError as error: |
| print(f'Directory {path} could not be created, reason: {error}') |
|
|
|
|
| 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]: |
| 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}' |
|
|