From 3d42e6646a0aa25d0796f53bb1edf2f1f719bd9b Mon Sep 17 00:00:00 2001 From: rainlizard <15337628+rainlizard@users.noreply.github.com> Date: Sun, 7 Jun 2026 02:46:56 +1000 Subject: [PATCH] Sort tags --- .tracking | 38 ++++++++++ __init__.py | 8 +- sort_tags_by_popularity.py | 146 +++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 .tracking create mode 100644 sort_tags_by_popularity.py diff --git a/.tracking b/.tracking new file mode 100644 index 0000000..1e6e4ab --- /dev/null +++ b/.tracking @@ -0,0 +1,38 @@ +.gitattributes +.github/workflows/publish.yml +.gitignore +Raffle Example.json +__init__.py +curved_rescale_cfg.py +dev/categorizer/Cursor Composer Prompt.txt +dev/categorizer/extract_categories.bat +dev/categorizer/extract_categories.py +dev/categorizer/manual_categorization (outdated).txt +dev/categorizer/split_n_combine/split_and_combine_script.py +dev/check-missing-tags.py +dev/copy-categories-over.py +dev/find_tags.py +dev/remove-numbers-at-end-of-each-line.py +dev/swap-in-new-categories.py +dev/tag-scraper/tag-scraper.py +dev/tag-scraper/tag_lists/all_tags.txt +dev/tag-scraper/tag_lists/artist.txt +dev/tag-scraper/tag_lists/categorize_files.py +dev/tag-scraper/tag_lists/character.txt +dev/tag-scraper/tag_lists/copyright.txt +dev/tag-scraper/tag_lists/general.txt +dev/tag-scraper/tag_lists/meta.txt +dev/taglist-scraper/danbooru-taglist-scraper.bat +dev/taglist-scraper/danbooru-taglist-scraper.py +dev/taglist-scraper/rearranger.py +lists/!notes.txt +lists/categorized_tags.txt +lists/taglists-explicit.txt +lists/taglists-general.txt +lists/taglists-questionable.txt +lists/taglists-sensitive.txt +preview_history.py +pyproject.toml +raffle.py +readme.md +tag_category_strength.py \ No newline at end of file diff --git a/__init__.py b/__init__.py index 14967ab..7e1f5dc 100644 --- a/__init__.py +++ b/__init__.py @@ -2,22 +2,26 @@ from . import raffle from . import preview_history # Import the renamed module from . import tag_category_strength # Import the new module from . import curved_rescale_cfg # Import the curved rescale cfg module +from . import sort_tags_by_popularity # Import the sort tags by popularity module from .raffle import Raffle from .preview_history import PreviewHistory # Import the renamed class from .tag_category_strength import TagCategoryStrength # Import the new class from .curved_rescale_cfg import CurvedRescaleCFG # Import the curved rescale cfg class +from .sort_tags_by_popularity import SortTagsByPopularity # Import the sort tags by popularity class NODE_CLASS_MAPPINGS = { "Raffle": Raffle, "PreviewHistory": PreviewHistory, # Add the renamed mapping "TagCategoryStrength": TagCategoryStrength, # Add the new mapping - "CurvedRescaleCFG": CurvedRescaleCFG # Add the curved rescale cfg mapping + "CurvedRescaleCFG": CurvedRescaleCFG, # Add the curved rescale cfg mapping + "SortTagsByPopularity": SortTagsByPopularity # Add the sort tags by popularity mapping } NODE_DISPLAY_NAME_MAPPINGS = { "Raffle": "Raffle", "PreviewHistory": "Preview History (Raffle)", # Add the renamed display name "TagCategoryStrength": "Tag Category Strength (Raffle)", # Add the new display name - "CurvedRescaleCFG": "Curved Rescale CFG (Raffle)" # Add the curved rescale cfg display name + "CurvedRescaleCFG": "Curved Rescale CFG (Raffle)", # Add the curved rescale cfg display name + "SortTagsByPopularity": "Sort Tags By Popularity (Raffle)" # Add the sort tags by popularity display name } __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/sort_tags_by_popularity.py b/sort_tags_by_popularity.py new file mode 100644 index 0000000..0c216cb --- /dev/null +++ b/sort_tags_by_popularity.py @@ -0,0 +1,146 @@ +import os +import re + + +class SortTagsByPopularity: + _allowed_tags_cache = None + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input_tags": ("STRING", { + "multiline": True, + "forceInput": True, + "default": "", + "tooltip": "Input tags to sort by Raffle's categorized tag order (comma-separated)" + }), + }, + "optional": { + "remove_duplicates": ("BOOLEAN", { + "default": False, + "tooltip": "Remove duplicate tags while sorting" + }), + } + } + + CATEGORY = "Raffle" + RETURN_TYPES = ("STRING", "STRING") + RETURN_NAMES = ("sorted_tags", "unsorted_tags") + OUTPUT_TOOLTIPS = ( + "Tags found in categorized_tags.txt, sorted by Raffle's categorized tag order", + "Tags not found in categorized_tags.txt, preserved separately" + ) + FUNCTION = "sort_tags" + + def _load_allowed_tags(self): + if SortTagsByPopularity._allowed_tags_cache is not None: + return SortTagsByPopularity._allowed_tags_cache + + extension_path = os.path.normpath(os.path.dirname(__file__)) + categorized_tags_file_path = os.path.join(extension_path, "lists", "categorized_tags.txt") + + if not os.path.exists(categorized_tags_file_path): + raise ValueError(f"Categorized tags file not found at {categorized_tags_file_path}") + + allowed_tags = [] + + try: + with open(categorized_tags_file_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if not line: + continue + + parts = line.split('] ', 1) + if len(parts) != 2: + continue + + allowed_tags.append(parts[1]) + except Exception as e: + raise ValueError(f"Error reading categorized tags file: {str(e)}") + + SortTagsByPopularity._allowed_tags_cache = allowed_tags + return allowed_tags + + def _normalize_tags(self, tag_string): + tag_string = tag_string.replace('\r\n', '\n') + tag_string = tag_string.replace('\n', ',') + + while ' ' in tag_string: + tag_string = tag_string.replace(' ', ' ') + + while ',,' in tag_string: + tag_string = tag_string.replace(',,', ',') + + tags = tag_string.replace(', ', ',').split(',') + return [ + tag.strip() + for tag in tags + if tag.strip() + ] + + def _canonicalize_tag_name(self, tag_name): + return tag_name.strip().replace(' ', '_') + + def _extract_tag_name(self, tag): + weight_pattern = r'^\(([^:]+):([^)]+)\)$' + match = re.match(weight_pattern, tag) + + if match: + return match.group(1).strip() + + return tag + + def _deduplicate_tags(self, tags): + seen = set() + deduplicated_tags = [] + + for tag in tags: + tag_name = self._canonicalize_tag_name(self._extract_tag_name(tag)) + if tag_name in seen: + continue + + seen.add(tag_name) + deduplicated_tags.append(tag) + + return deduplicated_tags + + def sort_tags(self, input_tags, remove_duplicates=False): + input_str = input_tags if isinstance(input_tags, str) else "" + lora_pattern = r']+>' + + lora_tags = re.findall(lora_pattern, input_str) + input_without_lora = re.sub(lora_pattern, '', input_str) + regular_tags = self._normalize_tags(input_without_lora) + + if remove_duplicates: + regular_tags = self._deduplicate_tags(regular_tags) + lora_tags = self._deduplicate_tags(lora_tags) + + allowed_tags = self._load_allowed_tags() + allowed_tag_indexes = { + self._canonicalize_tag_name(tag): index + for index, tag in enumerate(allowed_tags) + } + + sorted_tags = [ + tag for tag in regular_tags + if self._canonicalize_tag_name(self._extract_tag_name(tag)) in allowed_tag_indexes + ] + sorted_tags.sort( + key=lambda tag: allowed_tag_indexes[ + self._canonicalize_tag_name(self._extract_tag_name(tag)) + ] + ) + + unsorted_tags = [ + tag for tag in regular_tags + if self._canonicalize_tag_name(self._extract_tag_name(tag)) not in allowed_tag_indexes + ] + unsorted_tags.extend(lora_tags) + + if not sorted_tags and not unsorted_tags: + return "", "" + + return ", ".join(sorted_tags), ", ".join(unsorted_tags)