From 758fa0c2aef6778de14a01a742bf0bf78c2c0e8b Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Mon, 24 Jun 2024 19:21:36 +0200 Subject: [PATCH 1/7] Fix characters path --- testing/check_characters.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/testing/check_characters.py b/testing/check_characters.py index 92ac3bd..8a2d68c 100644 --- a/testing/check_characters.py +++ b/testing/check_characters.py @@ -7,8 +7,8 @@ import xml.etree.ElementTree as ET from difflib import get_close_matches # Constants -CHARACTER_FILE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'characters.json')) -CUSTOM_CHARACTER_FILE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'custom_characters.json')) +CHARACTER_FILE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../web/settings', 'characters.json')) +CUSTOM_CHARACTER_FILE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../web/settings', 'custom_characters.json')) TAGS_FILE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'danbooru-tags.csv')) LOG_FILE_PATH = os.path.abspath('testing/success_log.txt') BASE_URL = "https://gelbooru.com/index.php?page=dapi&s=tag&q=index&name=" @@ -101,6 +101,8 @@ low_use_characters = [] # List to store characters with too few uses total_valid_characters = 0 # Counter for total valid characters good_characters_count = 0 # Counter for good characters +print(f"{GREEN_TEXT}Note:{RESET_TEXT} Warnings for tags do not mean they should be removed, it is just there to make sure there are no misspelt tags") + # Record successful checks and track low use characters with open(LOG_FILE_PATH, 'a') as log_file: if current_min_uses != MIN_USES: @@ -151,7 +153,7 @@ with open(LOG_FILE_PATH, 'a') as log_file: if total_valid_characters > 0: good_percentage = (good_characters_count / total_valid_characters) * 100 - print(f"{good_percentage:.2f}% of characters are good.") + print(f"{good_percentage:.2f}% of characters are valid.") else: print("No valid characters found.") From 00a3c6d92f8a03500fb5227f132ae7fca498ab6a Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Mon, 24 Jun 2024 19:24:00 +0200 Subject: [PATCH 2/7] Fix create_setting_entry --- src/dynamic_scheduler_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dynamic_scheduler_loader.py b/src/dynamic_scheduler_loader.py index ed17ce9..d2b2caf 100644 --- a/src/dynamic_scheduler_loader.py +++ b/src/dynamic_scheduler_loader.py @@ -4,7 +4,7 @@ def generate_scheduler_node_class(settings, get_sigmas_function): class DynamicSchedulerNode: @classmethod - def create_setting_entry(setting_type, setting_value): + def create_setting_entry(cls, setting_type, setting_value): if setting_type == "INT": return ("INT", {"default": setting_value[1], "min": setting_value[2], "max": setting_value[3]}) elif setting_type == "FLOAT": From 0730437b728ab633f17b58472bc433ba78ac43f2 Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Mon, 24 Jun 2024 23:37:34 +0200 Subject: [PATCH 3/7] added option to disable default characters --- src/character_select.py | 12 +++++++++--- web/js/disableDefaultCharacters.js | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 web/js/disableDefaultCharacters.js diff --git a/src/character_select.py b/src/character_select.py index 0b66eac..de48503 100644 --- a/src/character_select.py +++ b/src/character_select.py @@ -11,6 +11,7 @@ class CharacterSelectNode: final_character_dict = {} final_characters = [] cached_sorting_setting = ConfigReader.get_setting("sn0w.SortBySeries", False) + cached_default_character_setting = ConfigReader.get_setting("sn0w.DisableDefaultCharacters", False) last_character = "" logger = Logger() @@ -25,9 +26,11 @@ class CharacterSelectNode: @classmethod def load_characters(cls, dir_path): - json_path = os.path.join(dir_path, 'web/settings/characters.json') - with open(json_path, 'r') as file: - character_data = json.load(file) + character_data = [] + if (not cls.cached_default_character_setting): + json_path = os.path.join(dir_path, 'web/settings/characters.json') + with open(json_path, 'r') as file: + character_data = json.load(file) custom_json_path = os.path.join(dir_path, 'web/settings/custom_characters.json') if os.path.exists(custom_json_path): @@ -67,6 +70,9 @@ class CharacterSelectNode: def INPUT_TYPES(cls): if not cls.final_characters: # Check if initialization is needed cls.initialize() + elif cls.cached_default_character_setting != ConfigReader.get_setting("sn0w.DisableDefaultCharacters", False): + cls.cached_default_character_setting = not cls.cached_default_character_setting + cls.initialize() else: cls.sort_characters() character_names = ['None'] + list(cls.final_characters) diff --git a/web/js/disableDefaultCharacters.js b/web/js/disableDefaultCharacters.js new file mode 100644 index 0000000..c742264 --- /dev/null +++ b/web/js/disableDefaultCharacters.js @@ -0,0 +1,20 @@ +import { app } from "../../../scripts/app.js"; + +const id = "sn0w.DisableDefaultCharacters"; +const settingDefinition = { + id, + name: "[Sn0w] Disable Default Characters", + defaultValue: false, + type: "boolean" +}; + +let setting; + +const extension = { + name: id, + init() { + setting = app.ui.settings.addSetting(settingDefinition); + }, +}; + +app.registerExtension(extension); From b71f2c7b27a6ecb9506f60673005eee2222b7f45 Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Tue, 25 Jun 2024 02:04:03 +0200 Subject: [PATCH 4/7] Settings now update the character loader automatically --- src/character_select.py | 11 +++++++++++ web/js/disableDefaultCharacters.js | 17 ++++++++++++++++- web/js/sortBySeries.js | 15 +++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/character_select.py b/src/character_select.py index de48503..ec32961 100644 --- a/src/character_select.py +++ b/src/character_select.py @@ -3,8 +3,19 @@ import json import random from ..sn0w import ConfigReader, Logger, AnyType, Utility +from server import PromptServer +from aiohttp import web + any = AnyType("*") +routes = PromptServer.instance.routes +API_PREFIX = '/sn0w' + +@routes.post(f'{API_PREFIX}/update_characters') +async def handle_update_characters(request): + CharacterSelectNode.initialize() + return web.json_response({"status": "ok"}) + class CharacterSelectNode: # Initialize class variables character_dict = {} diff --git a/web/js/disableDefaultCharacters.js b/web/js/disableDefaultCharacters.js index c742264..b05b031 100644 --- a/web/js/disableDefaultCharacters.js +++ b/web/js/disableDefaultCharacters.js @@ -1,11 +1,26 @@ +import { SettingUtils } from './sn0w.js'; import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; + +function updateSorting() { + api.fetchApi(`${SettingUtils.API_PREFIX}/update_characters`, { + method: "POST", + headers: { + "Content-Type": "application/json", + } + }) + setTimeout(() => { + app.refreshComboInNodes(); + }, 50); +} const id = "sn0w.DisableDefaultCharacters"; const settingDefinition = { id, name: "[Sn0w] Disable Default Characters", defaultValue: false, - type: "boolean" + type: "boolean", + onChange: () => updateSorting(), }; let setting; diff --git a/web/js/sortBySeries.js b/web/js/sortBySeries.js index 7d81e98..6ba1ad1 100644 --- a/web/js/sortBySeries.js +++ b/web/js/sortBySeries.js @@ -1,4 +1,18 @@ +import { SettingUtils } from './sn0w.js'; import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; + +function updateSorting() { + api.fetchApi(`${SettingUtils.API_PREFIX}/update_characters`, { + method: "POST", + headers: { + "Content-Type": "application/json", + } + }) + setTimeout(() => { + app.refreshComboInNodes(); + }, 50); +} const id = "sn0w.SortBySeries"; const settingDefinition = { @@ -6,6 +20,7 @@ const settingDefinition = { name: "[Sn0w] Sort Characters By Series", defaultValue: false, type: "boolean", + onChange: () => updateSorting(), }; let setting; From 56e0ae096a32161a4927f303034c1ada00440a52 Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Tue, 25 Jun 2024 02:13:24 +0200 Subject: [PATCH 5/7] Update naming for custom settings --- web/js/sn0w.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/js/sn0w.js b/web/js/sn0w.js index 020d53f..8f6c8d7 100644 --- a/web/js/sn0w.js +++ b/web/js/sn0w.js @@ -47,7 +47,7 @@ export class SettingUtils { const textarea = document.createElement("textarea"); // Generate a unique ID for associating the label with the textarea - const uniqueId = `custom-setting-${Date.now()}`; + const uniqueId = `${name.replaceAll(" ", "").replace("[", "").replace("]", "-")}`; label.setAttribute("for", uniqueId); label.textContent = name; @@ -105,7 +105,7 @@ export class SettingUtils { const label = document.createElement("label"); // Generate a unique ID for the setting container - const uniqueId = `custom-setting-${Date.now()}`; + const uniqueId = `${name.replaceAll(" ", "").replace("[", "").replace("]", "-")}`; label.setAttribute("for", uniqueId); label.textContent = name; From a42ec2113f73879047bb2b0d458c6dfb9af3d26a Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Tue, 25 Jun 2024 02:29:21 +0200 Subject: [PATCH 6/7] Remove unused nodes --- __init__.py | 32 ----------- src/find_resolution.py | 43 --------------- src/get_gelbooru_tags.py | 87 ------------------------------ src/perlin_noise.py | 111 --------------------------------------- 4 files changed, 273 deletions(-) delete mode 100644 src/find_resolution.py delete mode 100644 src/get_gelbooru_tags.py delete mode 100644 src/perlin_noise.py diff --git a/__init__.py b/__init__.py index a4f5dee..794fb71 100644 --- a/__init__.py +++ b/__init__.py @@ -52,38 +52,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { WEB_DIRECTORY = "./web" -settings_path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web/settings/sn0w_settings.json')) -print(settings_path) -try: - with open(settings_path, 'r') as file: - sn0w_settings = json.load(file) - print(sn0w_settings.get("enable_shitty_nodes")) -except FileNotFoundError: - sn0w_settings = {} - -if sn0w_settings.get("enable_shitty_nodes") == True: - from .src.find_resolution import FindResolutionNode - from .src.perlin_noise import FilmGrain - from .src.show_sigmas import ShowSigmasNode - from .src.get_gelbooru_tags import GetGelbooruTagsNode - - NODE_CLASS_MAPPINGS_ADD = { - "Find SDXL Resolution": FindResolutionNode, - "Colored Film Grain": FilmGrain, - "Show Sigmas": ShowSigmasNode, - "Get Gelbooru Tags": GetGelbooruTagsNode, - } - - NODE_DISPLAY_NAME_MAPPINGS_ADD = { - "Find SDXL Resolution": "Find SDXL Resolution", - "Colored Film Grain": "Colored Film Grain", - "Show Sigmas": "Show Sigmas", - "Get Gelbooru Tags": "Get Gelbooru Tags", - } - - NODE_CLASS_MAPPINGS.update(NODE_CLASS_MAPPINGS_ADD) - NODE_DISPLAY_NAME_MAPPINGS.update(NODE_DISPLAY_NAME_MAPPINGS_ADD) - current_unique_id = 0 # Global variable to track the unique ID logger = Logger() diff --git a/src/find_resolution.py b/src/find_resolution.py deleted file mode 100644 index 12dff87..0000000 --- a/src/find_resolution.py +++ /dev/null @@ -1,43 +0,0 @@ -class FindResolutionNode: - resolutions = { - "1024 x 1024 1:1": [ - 1024, - 1024 - ], - "1152 x 896 9:7": [ - 1152, - 896 - ], - "1216 x 832 19:13": [ - 1216, - 832 - ], - "1344 x 768 7:4": [ - 1344, - 768 - ], - "1536 x 640 12:5": [ - 1536, - 640 - ] - } - - @classmethod - def INPUT_TYPES(cls): - resolution_names = sorted(cls.resolutions.keys()) - return { - "required": { - "resolution": (resolution_names, ), - "flip": ("BOOLEAN", {"default": False},), - }, - } - - RETURN_TYPES = ("INT", "INT",) - RETURN_NAMES = ("WIDTH", "HEIGHT") - FUNCTION = "get_resolution" - CATEGORY = "sn0w" - - def get_resolution(self, resolution, flip): - resolutions = self.resolutions - return (resolutions[resolution][1], resolutions[resolution][0]) if flip else (resolutions[resolution][0], resolutions[resolution][1]) - \ No newline at end of file diff --git a/src/get_gelbooru_tags.py b/src/get_gelbooru_tags.py deleted file mode 100644 index 0f0fdae..0000000 --- a/src/get_gelbooru_tags.py +++ /dev/null @@ -1,87 +0,0 @@ -import os -import requests -from urllib.parse import urlparse, parse_qs - -class GetGelbooruTagsNode: - dir_path = os.path.dirname(os.path.realpath(__file__)) - tags_path = '../web/settings/tags/' - files = ['body_tags.txt'] - - for file in files: - with open(os.path.abspath(os.path.join(dir_path, f'{tags_path}{file}')), 'r') as text_file: - valid_tags = {line.strip().lower() for line in text_file} - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "url": ("STRING", {"default": ""}), - "separator": ("STRING", {"default": ", "}), - "replace_underscore": ("BOOLEAN", {"default": True}), - "characteristics_tags": ("BOOLEAN", {"default": True}), - "scene_tags": ("BOOLEAN", {"default": True}), - }, - "optional": { - "api_key (optional)": ("STRING", {"default": ""}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("PROMPT",) - FUNCTION = "get_gelbooru_tags" - CATEGORY = "sn0w" - - def filter_tags(self, input_string, separator, characteristics_tags, scene_tags): - if (characteristics_tags and scene_tags): - return input_string - - # Split the input string by the separator - tags = input_string.split(separator) - return_string = "" - - def add_tags_to_return_string(tag): - nonlocal return_string - if return_string: - return_string += separator - return_string += tag.strip() - - for tag in tags: - processed_tag = tag.replace('\\', '').strip().replace(' ', '_').lower() - - if processed_tag in self.valid_tags: - if (characteristics_tags): - add_tags_to_return_string(tag) - else: - if (scene_tags): - add_tags_to_return_string(tag) - - return return_string - - def get_gelbooru_tags(self, url, separator, replace_underscore, characteristics_tags, scene_tags, **kwargs): - API_URL = "https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1" - - if (kwargs["api_key (optional)"]): - api_key = kwargs["api_key (optional)"] - if (api_key.startswith("&api_key=")): - api_key[9:] - - API_URL = f"{API_URL}&api_key={api_key}" - - if url.startswith("http"): - parsed_url = urlparse(url) - query_params = parse_qs(parsed_url.query) - id_value = query_params.get('id', [None])[0] - else: - id_value = url - - final_api_url = API_URL + f"&id={id_value}" - - r = requests.get(url = final_api_url) - data = r.json() - tags = data['post'][0]['tags'] - tags = tags.replace(" ", separator) - if (replace_underscore): - tags = tags.replace("_", " ") - - tags = self.filter_tags(tags, separator, characteristics_tags, scene_tags) - return (tags,) diff --git a/src/perlin_noise.py b/src/perlin_noise.py deleted file mode 100644 index 96e5272..0000000 --- a/src/perlin_noise.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -import numpy as np -import torch - -class FilmGrain: - def __init__(self): - self.noise_cache = {} - script_dir = os.path.dirname(os.path.abspath(__file__)) - self.noise_img_dir = os.path.join(script_dir, 'img', 'noise') - - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "image": ("IMAGE",), - "intensity": ("FLOAT", { - "default": 0.2, - "min": 0.0, - "max": 1.0, - "step": 0.01 - }), - "scale": ("FLOAT", { - "default": 10, - "min": 1, - "max": 100, - "step": 1 - }), - "cache": ("BOOLEAN", {"default": False},), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "film_grain" - - CATEGORY = "sn0w" - - def film_grain(self, image: torch.Tensor, intensity: float, scale: float, cache: bool): - batch_size, height, width, channels = image.shape - assert channels == 3, "Image must have 3 channels for RGB" - - result = torch.zeros_like(image) - - for b in range(batch_size): - tensor_image = image[b].numpy() - - grain_image = np.zeros_like(tensor_image) - for channel in range(3): - noise = self.get_noise_image(height, width, scale, channel) if cache else self.generate_perlin_noise((height, width), scale) - noise = (noise - noise.min()) / (noise.max() - noise.min()) # Normalize - noise = (noise * 2 - 1) * intensity # Apply intensity - grain_image[:, :, channel] = np.clip(tensor_image[:, :, channel] + noise, 0, 1) - - tensor = torch.from_numpy(grain_image).unsqueeze(0) - result[b] = tensor - - return (result,) - - def generate_perlin_noise(self, shape, scale, octaves=4, persistence=0.5, lacunarity=2): - def smoothstep(t): - return t * t * (3.0 - 2.0 * t) - - def lerp(t, a, b): - return a + t * (b - a) - - def gradient(h, x, y): - vectors = np.array([[1, 1], [-1, 1], [1, -1], [-1, -1]]) - g = vectors[h % 4] - return g[:, :, 0] * x + g[:, :, 1] * y - - height, width = shape - noise = np.zeros(shape) - - for octave in range(octaves): - octave_scale = scale * lacunarity ** octave - x = np.linspace(0, 1, width, endpoint=False) - y = np.linspace(0, 1, height, endpoint=False) - X, Y = np.meshgrid(x, y) - X, Y = X * octave_scale, Y * octave_scale - - xi = X.astype(int) - yi = Y.astype(int) - - xf = X - xi - yf = Y - yi - - u = smoothstep(xf) - v = smoothstep(yf) - - n00 = gradient(np.random.randint(0, 4, (height, width)), xf, yf) - n01 = gradient(np.random.randint(0, 4, (height, width)), xf, yf - 1) - n10 = gradient(np.random.randint(0, 4, (height, width)), xf - 1, yf) - n11 = gradient(np.random.randint(0, 4, (height, width)), xf - 1, yf - 1) - - x1 = lerp(u, n00, n10) - x2 = lerp(u, n01, n11) - y1 = lerp(v, x1, x2) - - noise += y1 * persistence ** octave - - return noise / (1 - persistence ** octaves) - - def get_noise_image(self, height, width, scale, channel): - key = (height, width) - if key in self.noise_cache and self.noise_cache[key][channel] is not None: - return self.noise_cache[key][channel] - else: - noise = self.generate_perlin_noise((height, width), scale) - if key not in self.noise_cache: - self.noise_cache[key] = [None, None, None] - self.noise_cache[key][channel] = noise - return noise \ No newline at end of file From 259788d5ef21d29e396f8e3e913ed80cebe92579 Mon Sep 17 00:00:00 2001 From: sn0w12 Date: Tue, 25 Jun 2024 02:47:05 +0200 Subject: [PATCH 7/7] Update creation of scheduler_settings.js --- src/custom_schedulers/custom_schedulers.py | 50 +++-- src/prompt_combine_testing.py | 245 --------------------- 2 files changed, 29 insertions(+), 266 deletions(-) delete mode 100644 src/prompt_combine_testing.py diff --git a/src/custom_schedulers/custom_schedulers.py b/src/custom_schedulers/custom_schedulers.py index f0dafcf..56e2850 100644 --- a/src/custom_schedulers/custom_schedulers.py +++ b/src/custom_schedulers/custom_schedulers.py @@ -62,6 +62,14 @@ class CustomSchedulers: return (sigmas, ) raise ValueError(f"No scheduler found with the name {name}") + def generate_js_object(self, name, settings): + """Generate JavaScript object string from settings.""" + js_obj = f" \"{name}\": {{\n" + for setting, details in settings.items(): + js_obj += f" \"{setting}\": {json.dumps(details)},\n" + js_obj += " },\n" + return js_obj + def export_scheduler_settings_to_js(self): # Go settings directory base_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) @@ -70,34 +78,34 @@ class CustomSchedulers: filepath = os.path.join(target_dir, 'scheduler_settings.js') # Initialize the JavaScript content with default settings - js_content = "const widgets = {\n" - js_content += """ "polyexponential": { - "sigma_max_poly": ["FLOAT", 14.614642, 0.0, 5000.0, 0.01, false], - "sigma_min_poly": ["FLOAT", 0.0291675, 0.0, 5000.0, 0.01, false], - "rho": ["FLOAT", 1.0, 0.0, 100.0, 0.01, false] - }, - "vp": { - "beta_d": ["FLOAT", 14.0, 0.0, 5000.0, 0.01, false], - "beta_min": ["FLOAT", 0.05, 0.0, 5000.0, 0.01, false], - "eps_s": ["FLOAT", 0.075, 0.0, 1.0, 0.0001, false] - },""" + default_settings = { + "polyexponential": { + "sigma_max_poly": ["FLOAT", 14.614642, 0.0, 5000.0, 0.01, False], + "sigma_min_poly": ["FLOAT", 0.0291675, 0.0, 5000.0, 0.01, False], + "rho": ["FLOAT", 1.0, 0.0, 100.0, 0.01, False] + }, + "vp": { + "beta_d": ["FLOAT", 14.0, 0.0, 5000.0, 0.01, False], + "beta_min": ["FLOAT", 0.05, 0.0, 5000.0, 0.01, False], + "eps_s": ["FLOAT", 0.075, 0.0, 1.0, 0.0001, False] + } + } + js_content = "const widgets = {\n" + for name, settings in default_settings.items(): + js_content += self.generate_js_object(name, settings) + # Convert the Python dictionary to a JavaScript object for name, settings in self.scheduler_defaults.items(): - if name in ["polyexponential", "vp"]: # Merge with defaults + if name not in default_settings: + js_content += self.generate_js_object(name, settings) + else: + # Merge with defaults js_content += f" \"{name}\": {{\n" for setting, details in settings.items(): js_content += f" \"{setting}\": {json.dumps(details)},\n" js_content += " },\n" - else: - # Add new entries if not already covered by defaults - js_content += f"\n \"{name}\": {{\n" - for setting, details in settings.items(): - if isinstance(details, list) and len(details) == 6 and details[0] == "FLOAT": - js_content += f" \"{setting}\": {json.dumps(details)},\n" - else: - print(f"Skipping setting {setting} due to unexpected format: {details}") - js_content += " },\n" + js_content += "};\n\nexport { widgets };\n" # Write the JavaScript content to the file diff --git a/src/prompt_combine_testing.py b/src/prompt_combine_testing.py deleted file mode 100644 index 127f58e..0000000 --- a/src/prompt_combine_testing.py +++ /dev/null @@ -1,245 +0,0 @@ -import re - -def simplify_tags(tags_string): - # dictionary defines categories with specific rules for removing or keeping tags. - # remove contains phrases that, if found in a prompt, suggest the tag should be removed under certain conditions. - # keep contains phrases that, if found in a prompt, indicate the tag should be kept. - # if a remove tag is found and no keep tag is found, it will remove all mentions of the category except the tags found in remove. - # keep always takes priority over remove, if any tag in keep is found nothing will be removed - special_phrases = { - 'eye': { - 'remove': ['covering eyes', 'over eyes', 'covered eyes', 'covering face', 'covering own eyes', 'facing away'], - 'keep': ['looking at viewer'] - }, - 'sclera': { - 'remove': ['covering eyes', 'over eyes', 'covered eyes', 'covering face', 'covering own eyes', 'facing away'], - 'keep': ['looking at viewer'] - }, - 'mouth': { - 'remove': ['facing away'], - 'keep': ['looking at viewer'] - }, - 'teeth': { - 'remove': ['facing away'], - 'keep': ['looking at viewer'] - }, - } - - # Regular expression to match parenthesized parts - parenthesized_pattern = re.compile(r'\([^()]*\)') - parenthesized_parts = [] - - # Extract and temporarily remove parenthesized parts, storing their positions - def extract_parenthesized(match): - parenthesized_parts.append(match.group(0)) - return "\0" # Use a unique placeholder to mark the position - - modified_tags_string = re.sub(parenthesized_pattern, extract_parenthesized, tags_string) - - # Split tags and initialize lists - tags = [tag.strip() for tag in modified_tags_string.split(',')] - final_tags = [] - removed_tags = [] - tag_map = {} - - # Mapping non-superior tags to their superior counterparts - for tag in tags: - for potential_superior in tags: - if tag != potential_superior and tag in potential_superior: - tag_parts = set(tag.split(' ')) - superior_parts = set(potential_superior.split(' ')) - if tag_parts.issubset(superior_parts) and tag_parts != superior_parts: - tag_map[tag] = potential_superior - - # Check for 'keep' conditions globally - global_keep_conditions = {keyword: any(keep_phrase in tags_string for keep_phrase in conditions['keep']) for keyword, conditions in special_phrases.items()} - - # Identify tags that should not be removed because they are contained in 'remove' phrases - non_removable_tags = set() - for tag in tags: - for keyword, conditions in special_phrases.items(): - if any(remove_phrase in tag for remove_phrase in conditions['remove']): - non_removable_tags.add(tag) - - for tag in tags: - if tag in tag_map: - # Append non-superior tags to removed_tags if their superior counterparts are not already included - if tag_map[tag] not in final_tags: - removed_tags.append(tag) - continue - - should_remove_tag = False - for keyword, conditions in special_phrases.items(): - if keyword in tag: - if not global_keep_conditions[keyword] and any(remove_phrase in tags_string for remove_phrase in conditions['remove']) and tag not in non_removable_tags: - should_remove_tag = True - break - - if should_remove_tag: - removed_tags.append(tag) - else: - final_tags.append(tag) - - # Ensure tags explicitly listed in "remove" are not removed - for keyword, conditions in special_phrases.items(): - for remove_tag in conditions['remove']: - if remove_tag in removed_tags: - removed_tags.remove(remove_tag) - final_tags.append(remove_tag) - - # Remove duplicates in the removed_tags list by converting it to a set and back to a list - removed_tags = ', '.join(list(set(removed_tags))) - - # Reinsert parenthesized parts back into their original positions - final_tags_list = modified_tags_string.split(',') - final_tags_with_parentheses = [] - for tag in final_tags_list: - if "\0" in tag: # If placeholder is found, replace it with the original parenthesized part - tag = tag.replace("\0", parenthesized_parts.pop(0), 1) - final_tags_with_parentheses.append(tag.strip()) - - # New step: Re-sort tags to move numeric tags to the front - numeric_tag_pattern = re.compile(r'\b\d+\+?(girls?|boys?)\b') - numeric_tags = [tag for tag in final_tags_with_parentheses if numeric_tag_pattern.match(tag)] - non_numeric_tags = [tag for tag in final_tags_with_parentheses if not numeric_tag_pattern.match(tag)] - - # Prioritize numeric tags in the final list - prioritized_final_tags = numeric_tags + non_numeric_tags - - # Generate the final simplified tags string - simplified_tags_string = ', '.join(prioritized_final_tags) - - return simplified_tags_string, removed_tags - -def simplify_tags_new(tags_string, separator): - # dictionary defines categories with specific rules for removing or keeping tags. - # remove contains phrases that, if found in a prompt, suggest the tag should be removed. - # keep contains phrases that, if found in a prompt, indicate the tag should be kept. - # if a remove tag is found and no keep tag is found, it will remove all mentions of the category except the tags found in remove. - # keep always takes priority over remove, if any tag in keep is found nothing will be removed - special_phrases = { - 'eye': { - 'remove': ['covering eyes', 'over eyes', 'covered eyes', 'covering face', 'covering own eyes', 'facing away'], - 'keep': ['looking at viewer'] - }, - 'sclera': { - 'remove': ['covering eyes', 'over eyes', 'covered eyes', 'covering face', 'covering own eyes', 'facing away'], - 'keep': ['looking at viewer'] - }, - 'mouth': { - 'remove': ['facing away'], - 'keep': ['looking at viewer'] - }, - 'teeth': { - 'remove': ['facing away'], - 'keep': ['looking at viewer'] - }, - } - - # Compile regex patterns outside of loops - parenthesized_pattern = re.compile(r'\([^()]*\)') - numeric_tag_pattern = re.compile(r'\b\d+\+?(girls?|boys?)\b') - - # Extract parenthesized parts - parenthesized_parts = [] - def extract_parenthesized(match): - parenthesized_parts.append(match.group()) - return f"\0{len(parenthesized_parts)-1}\0" - - modified_tags_string = re.sub(parenthesized_pattern, extract_parenthesized, tags_string) - - tags = [tag.strip() for tag in modified_tags_string.split(separator)] - final_tags = [] - removed_tags = set() - tag_map = {} - - for tag in tags: - for potential_superior in tags: - if tag in potential_superior and tag != potential_superior: - tag_map[tag] = potential_superior - - global_keep_conditions = { - keyword: any(keep_phrase in tags_string for keep_phrase in conditions['keep']) - for keyword, conditions in special_phrases.items() - } - - non_removable_tags = { - tag - for tag in tags - for keyword, conditions in special_phrases.items() - if any(remove_phrase in tag for remove_phrase in conditions['remove']) - } - - for tag in tags: - superior_tag = tag_map.get(tag) - if superior_tag and superior_tag not in final_tags: - removed_tags.add(tag) - continue - - should_remove_tag = False - for keyword, conditions in special_phrases.items(): - if keyword in tag and not global_keep_conditions[keyword] and any(remove_phrase in tags_string for remove_phrase in conditions['remove']) and tag not in non_removable_tags: - should_remove_tag = True - break - - if should_remove_tag: - removed_tags.add(tag) - else: - # Avoid adding duplicates - if tag not in final_tags: - final_tags.append(tag) - - # Reinsert parenthesized parts and handle removed tags - final_tags_with_parentheses = [] - for tag in final_tags: - while "\0" in tag: - index = int(tag[tag.find("\0")+1:tag.rfind("\0")]) - tag = tag.replace(f"\0{index}\0", parenthesized_parts[index], 1) - final_tags_with_parentheses.append(tag) - - animagine_formatting = True - - if animagine_formatting: - numeric_tags = [tag for tag in final_tags_with_parentheses if numeric_tag_pattern.match(tag)] - non_numeric_tags = [tag for tag in final_tags_with_parentheses if not numeric_tag_pattern.match(tag)] - - prioritized_final_tags = numeric_tags + non_numeric_tags - else: - prioritized_final_tags = final_tags_with_parentheses - - simplified_tags_string = separator.join(prioritized_final_tags).strip(separator) - removed_tags_string = separator.join(removed_tags).strip(separator) - - return simplified_tags_string, removed_tags_string - -def combine_string(separator, simplify, **kwargs): - # Collect strings that are not None and strip them - strings = [s.strip() for s in (kwargs.get(f"string_{char}") for char in ['a', 'b', 'c', 'd']) if s] - - all_words = set() - combined = [] - - for string in strings: - # Avoid appending the separator at the end; directly construct the final string without it - unique_words = [word for word in string.split(separator) if word not in all_words and not all_words.add(word)] - if unique_words: # Check if there are any unique words to add - combined.append(separator.join(unique_words)) - - # Join all unique words across all strings with the separator, handle simplify directly - final_combined_string = separator.join(combined) - if simplify: - final_tags, removed_tags = simplify_tags(final_combined_string, separator) - - return (final_tags, removed_tags) - -# Test the function with an example string -tags_string1 = "(mirko, boku no hero academia:1.2), " -tags_string2 = "animal ears, breasts, dark-skinned female, large breasts, long hair, parted bangs, rabbit ears, rabbit girl, red eyes, toned, white hair, 1boy, 1girl, after ejaculation, animal ear fluff, animal hands, bell, bikini, black bikini, black thighhighs, blush, breasts apart, cat ears, cat paws, cat tail, clothed female nude male, collar, covered erect nipples, cum, cum in mouth, cum on penis, cum on tongue, facial, fake animal ears, fake tail, fang, fur collar, gloves, gluteal fold, highleg, highleg bikini, holding, holding leash, kneeling, large penis, leash, leash pull, micro bikini, navel, neck bell, nude, open mouth, paw gloves, penis, penis on face, penis over eyes, pet play, smile, stomach, swimsuit, tail, tail ornament, thighhighs, veins, veiny penis, (masterpiece, exceptional, best aesthetic, best quality, masterpiece, extremely detailed:1.1)" -#simplified_tags_string, removed_tags = simplify_tags(tags_string) -#print('Simplified Tags Old:', simplified_tags_string) -#print('Removed Tags Old:', removed_tags) -#print("\n") - -simplified_tags_string_new, removed_tags_new = combine_string(", ", False, [tags_string1, tags_string2]) -print('Simplified Tags New:', simplified_tags_string_new) -print('Removed Tags New:', removed_tags_new) \ No newline at end of file