@@ -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
|
||||
@@ -38,8 +38,8 @@ In the ComfyUI settings, you can modify various settings. Some changes will appl
|
||||
| Sort Characters By | Alphabetically<br>Series | Sorts characters either alphabetically or by their series. |
|
||||
| Sort Loras By | Alphabetically<br>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:
|
||||
|
||||
@@ -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()
|
||||
@@ -235,7 +271,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
|
||||
|
||||
|
||||
@@ -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
|
||||
+21
-8
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user