From c909ed1e31b1efcba14c5a9d4a56f822d2e5d1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 5 Aug 2024 00:34:12 +0200 Subject: [PATCH 1/5] Default to None instead of empty array --- sn0w.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sn0w.py b/sn0w.py index 7663c53..6f36e2a 100644 --- a/sn0w.py +++ b/sn0w.py @@ -235,7 +235,7 @@ class Utility: if isinstance(arr, dict): arr = list(arr.keys()) - favourites = ConfigReader.get_setting(setting, []) + favourites = ConfigReader.get_setting(setting, None) if favourites is None: return arr From a9def243994265664ba74dae1c07e29925402093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 5 Aug 2024 00:43:31 +0200 Subject: [PATCH 2/5] Add better documentation --- sn0w.py | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/sn0w.py b/sn0w.py index 6f36e2a..e4c43f4 100644 --- a/sn0w.py +++ b/sn0w.py @@ -22,6 +22,13 @@ import folder_paths class ConfigReader: """ Handles reading and managing configuration settings for ComfyUI without using the API. + + Methods: + get_setting(setting_id, default) + Retrieve a setting value from the configuration file. + + is_comfy_portable() + Check if comfy is running in portable mode. """ DEFAULT_PATH = os.path.abspath(os.path.join(os.getcwd(), "user/default/comfy.settings.json")) @@ -98,6 +105,13 @@ class Logger: - WARNING - INFORMATIONAL - DEBUG + + Methods: + log(message, level) + Log a message with a specified severity level. + + print_sigmas_differences(name, sigmas) + Prints sigmas to terminal. """ PURPLE_TEXT = "\033[0;35m" @@ -116,7 +130,7 @@ class Logger: cls.enabled_levels = ConfigReader.get_setting("sn0w.LoggingLevel", ["INFORMATIONAL", "WARNING"]) @classmethod - def print_sn0w(cls, message, color): + def _print_sn0w(cls, message, color): """Print a message with a specific color prefix.""" print(f"{color}{cls.PREFIX}{cls.RESET_TEXT}{message}") @@ -132,7 +146,7 @@ class Logger: # Check if the message's level is in the enabled log levels if level.upper() in self.enabled_levels or level.upper() in self.ALWAYS_LOG: - self.print_sn0w(message, color) + self._print_sn0w(message, color) def print_sigmas_differences(self, name, sigmas): """ @@ -143,7 +157,7 @@ class Logger: sigmas (torch.Tensor): A 1D tensor of sigmas with a zero appended at the end. """ if "DEBUG" in self.enabled_levels: - self.print_sn0w(f"Scheduler: {name}", self.PURPLE_TEXT) + self._print_sn0w(f"Scheduler: {name}", self.PURPLE_TEXT) print("Index | Sigma Value | Difference to Next | % Difference") print("-" * 65) @@ -165,6 +179,28 @@ class Logger: class Utility: """ Utility class providing various static methods for common operations. + + Methods: + levenshtein_distance(s1, s2): + Calculate the Levenshtein distance between two strings. + + image_batch(**kwargs): + Concatenate and batch multiple image tensors. + + get_model_type(model_patcher): + Retrieve the type of the model from the model patcher. + + get_model_type_simple(model_patcher): + Retrieve a standardized model type from the model patcher. + + put_favourite_on_top(setting, arr): + Prioritize favorite items in a list based on a setting. + + create_setting_entry(setting_type, setting_value): + Create a setting entry based on the type and value. + + get_node_output(data, node_id, output_id): + Get the output of a node from the workflow data. """ logger = Logger() From 0b2a5fad0378481e81606df441c47a4705563e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 5 Aug 2024 15:52:50 +0200 Subject: [PATCH 3/5] Change textbox highlighting setting to combo --- web/js/textbox.js | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/web/js/textbox.js b/web/js/textbox.js index 40e7e62..d556174 100644 --- a/web/js/textbox.js +++ b/web/js/textbox.js @@ -103,12 +103,21 @@ app.registerExtension({ async function setTextHighlightType(inputEl) { const highlightGradient = await SettingUtils.getSetting('sn0w.TextboxGradientColors'); - if (highlightGradient === null) { - inputEl.highlightGradient = false; - return; + + let shouldHighlightGradient = false; + switch(highlightGradient) { + case "nesting": + shouldHighlightGradient = false; + break; + case "strength": + shouldHighlightGradient = true; + break; + default: + shouldHighlightGradient = false; + break; } - inputEl.highlightGradient = highlightGradient; + inputEl.highlightGradient = shouldHighlightGradient; } async function setTextColors(inputEl, overlayEl) { @@ -492,10 +501,13 @@ const settingsDefinitions = [ }, { id: 'sn0w.TextboxGradientColors', - name: '[Sn0w] Custom Textbox Gradient Highlight', - type: 'boolean', - defaultValue: false, - tooltip: 'Makes the textbox highlighting be a gradient between the first and last color based on the strength of the selection.', + name: '[Sn0w] Textbox Highlight Type', + options: [ + { text: 'Nesting', value: 'nesting' }, + { text: 'Strength', value: 'strength' }, + ], + type: 'combo', + defaultValue: 'nesting', onChange: () => { const nodes = app.graph._nodes.filter(node => node.type === 'Copy/Paste Textbox'); nodes.forEach(node => { @@ -512,6 +524,7 @@ const registerSetting = (settingDefinition) => { const setting = app.ui.settings.addSetting({ id: settingDefinition.id, name: settingDefinition.name, + options: settingDefinition.options, type: settingDefinition.type, defaultValue: settingDefinition.defaultValue, tooltip: settingDefinition.tooltip, From c1eefb8e01e81140535ddf2d2444acba2a823808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 5 Aug 2024 23:34:19 +0200 Subject: [PATCH 4/5] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d9d189c..14bd93f 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ In the ComfyUI settings, you can modify various settings. Some changes will appl | Sort Characters By | Alphabetically
Series | Sorts characters either alphabetically or by their series. | | Sort Loras By | Alphabetically
Last Changed | Sorts loras either alphabetically or by latest changed. | -## Important Note -The custom lora loaders require these paths to be in your `extra_model_paths.yaml` (with your real directories of course). You only need to add the ones you are actively using. +> [!IMPORTANT] +> The custom lora loaders require these paths to be in your `extra_model_paths.yaml` (with your real directories of course). You only need to add the ones you are actively using. ```yaml loras: From 4afe78fa80b13f551baf94c37a4b664c99445f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Tue, 6 Aug 2024 09:38:12 +0200 Subject: [PATCH 5/5] Add character checker action --- .github/worflows/character_text.yml | 28 +++++++ testing/git_check_characters.py | 125 ++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 .github/worflows/character_text.yml create mode 100644 testing/git_check_characters.py diff --git a/.github/worflows/character_text.yml b/.github/worflows/character_text.yml new file mode 100644 index 0000000..6c98c2c --- /dev/null +++ b/.github/worflows/character_text.yml @@ -0,0 +1,28 @@ +name: Run Character Tag Validator + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.10 + + - name: Install dependencies + run: | + pip install requests + + - name: Run script + id: run_script + run: | + python testing/git_check_characters.py || echo "Script failed due to low-use characters. Please run the script locally to address this issue." + + - name: Fail the build if the script failed + if: failure() + run: exit 1 diff --git a/testing/git_check_characters.py b/testing/git_check_characters.py new file mode 100644 index 0000000..5937672 --- /dev/null +++ b/testing/git_check_characters.py @@ -0,0 +1,125 @@ +import xml.etree.ElementTree as ET +from difflib import get_close_matches +import csv +import json +import os +import time +import requests +import sys + +# Constants +CHARACTER_FILE_PATH = os.getenv("CHARACTER_FILE_PATH", "web/settings/characters.json") +TAGS_FILE_PATH = os.getenv("TAGS_FILE_PATH", "testing/danbooru-tags.csv") +BASE_URL = "https://gelbooru.com/index.php?page=dapi&s=tag&q=index&name=" +REQUEST_DELAY = 0.5 # Delay between requests in seconds +MIN_USES = int(os.getenv("MIN_USES", 100)) +SIMILARITY_THRESHOLD = 0.9 + +RED_TEXT = "\033[0;31m" +GREEN_TEXT = "\033[0;32m" +YELLOW_TEXT = "\033[0;33m" +RESET_TEXT = "\033[0m" + + +def validate_character(character): + required_fields = ["name", "associated_string", "prompt"] + return all(field in character and character[field] is not None for field in required_fields) + + +def load_tags(file_path): + tags = {} + with open(file_path, "r", encoding="utf-8") as file: + reader = csv.reader(file) + for row in reader: + tags[row[0]] = int(row[1]) + return tags + + +def normalize_tag(tag): + return tag.replace(" ", "_").replace("\\", "").strip() + + +def check_tags_exist(tags, tag_list): + missing_tags = [normalize_tag(tag) for tag in tag_list if normalize_tag(tag) not in tags] + return missing_tags + + +def suggest_close_tags(tags, missing_tags, n=3, threshold=0.8): + tag_keys = list(tags.keys()) + suggestions = {} + for tag in missing_tags: + normalized_tag = normalize_tag(tag) + close_matches = get_close_matches(normalized_tag, tag_keys, n=n, cutoff=threshold) + if close_matches: + suggestions[tag] = close_matches + return suggestions + + +with open(CHARACTER_FILE_PATH, "r", encoding="utf-8") as file: + characters_data = json.load(file) + +tags_data = load_tags(TAGS_FILE_PATH) + +successful_tags = set() +low_use_characters = [] +total_valid_characters = 0 +good_characters_count = 0 + +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" +) + +for character in characters_data: + if validate_character(character): + total_valid_characters += 1 + associated_tags = [tag.strip() for tag in character["associated_string"].split(",")] + prompt_tags = [tag.strip() for tag in character["prompt"].split(",")] + + missing_tags = check_tags_exist(tags_data, prompt_tags) + if missing_tags: + print( + f"{YELLOW_TEXT}Warning: The following tags for character '{character['name']}' do not exist in TAGS_FILE_PATH: {', '.join(missing_tags)}{RESET_TEXT}" + ) + suggestions = suggest_close_tags(tags_data, missing_tags, threshold=SIMILARITY_THRESHOLD) + for tag, matches in suggestions.items(): + print(f"Suggestions for '{tag}': {', '.join(matches)}") + + normalized_tag = normalize_tag(associated_tags[0]) + + if normalized_tag in successful_tags: + good_characters_count += 1 + continue + + url = f"{BASE_URL}{normalized_tag}" + response = requests.get(url, timeout=10) + time.sleep(REQUEST_DELAY) + + if response.status_code == 200: + root = ET.fromstring(response.content) + if root is not None: + try: + tag_count = int(root.find("tag").find("count").text) + if tag_count < MIN_USES: + print(f"{RED_TEXT}{normalized_tag} DOES NOT HAVE ENOUGH USES: {tag_count}{RESET_TEXT}") + low_use_characters.append(character) + else: + print(f"{GREEN_TEXT}{normalized_tag} is good: {tag_count}{RESET_TEXT}") + good_characters_count += 1 + except AttributeError: + print(f"Could not find count for {normalized_tag}") + else: + print(f"No root element found for {normalized_tag}") + else: + print(f"Failed to retrieve data for {normalized_tag}: {response.status_code}") + else: + print(f"Character not formatted correctly: {character}") + +if total_valid_characters > 0: + good_percentage = (good_characters_count / total_valid_characters) * 100 + print(f"{good_percentage:.2f}% of characters are valid.") +else: + print("No valid characters found.") + +if low_use_characters: + print("Script failed due to low-use characters. Please run the script locally to address this issue.") + sys.exit(1) # Exit with a non-zero status code to indicate failure