diff --git a/.github/worflows/character_text.yml b/.github/workflows/character_test.yml similarity index 78% rename from .github/worflows/character_text.yml rename to .github/workflows/character_test.yml index 6c98c2c..3539c15 100644 --- a/.github/worflows/character_text.yml +++ b/.github/workflows/character_test.yml @@ -1,6 +1,12 @@ name: Run Character Tag Validator -on: [push, pull_request] +on: + push: + paths: + - 'web/settings/characters.json' + pull_request: + paths: + - 'web/settings/characters.json' jobs: build: @@ -12,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.10 + python-version: 3.10.9 - name: Install dependencies run: | diff --git a/.gitignore b/.gitignore index a1cb9cd..70f0e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ src/custom_schedulers/mixed_decay_growth.py src/prompt_combine_testing.py web/settings/sn0w_settings.json testing/test_scheduler.py +update-dev.bat diff --git a/src/character_select.py b/src/character_select.py index 29ed3d9..6f34c16 100644 --- a/src/character_select.py +++ b/src/character_select.py @@ -69,13 +69,16 @@ class CharacterSelectNode: custom_json_path = os.path.join(dir_path, CUSTOM_CHARACTER_FILE_PATH) if not os.path.exists(custom_json_path): - cls.logger.log(f"Custom character file doesn't exist. Please create the json file at: {custom_json_path}", "WARNING") + cls.logger.log( + f"Custom character file doesn't exist. Please create the json file at: {custom_json_path}", "WARNING" + ) else: with open(custom_json_path, "r", encoding="utf-8") as file: custom_character_data = json.load(file) for custom_character in custom_character_data: for character in character_data: if custom_character["name"] == character["name"]: + character["associated_string"] += ", " + custom_character["associated_string"] character["prompt"] += ", " + custom_character["prompt"] break else: diff --git a/web/js/customLoraLoaders.js b/web/js/customLoraLoaders.js index ce6acc9..10f2476 100644 --- a/web/js/customLoraLoaders.js +++ b/web/js/customLoraLoaders.js @@ -3,18 +3,7 @@ import { app } from '../../../scripts/app.js'; import { api } from '../../../scripts/api.js'; async function fetchExtensionSettings() { - try { - const response = await api.fetchApi( - '/extensions/ComfyUI-Sn0w-Scripts/settings/sn0w_settings.json' - ); - if (!response.ok) { - throw new Error('Network response was not ok ' + response.statusText); - } - const data = await response.json(); - return data; - } catch (error) { - console.error('There was a problem with the fetch operation:', error); - } + return SettingUtils.fetchApi("/extensions/ComfyUI-Sn0w-Scripts/settings/sn0w_settings.json"); } const sn0wSettings = await fetchExtensionSettings(); diff --git a/web/js/loraContextMenu.js b/web/js/loraContextMenu.js index 83c7c5b..5b1369a 100644 --- a/web/js/loraContextMenu.js +++ b/web/js/loraContextMenu.js @@ -62,35 +62,66 @@ app.registerExtension({ // Combine the existing custom loaders with the fetched ones loraLoaders = loraLoaders.concat(...customLoraLoadersArrays); - addLoraLoaders(loraLoaders); const original_getNodeMenuOptions = app.canvas.getNodeMenuOptions; app.canvas.getNodeMenuOptions = function (node) { const options = original_getNodeMenuOptions.apply(this, arguments); + + // Collect all Lora widgets. + const loraWidgets = node.widgets.filter(widget => widget.name.includes("lora") && widget.type === "combo"); + const totalLoras = loraWidgets.length; + if (loraLoaders.includes(node.type)) { const settingUtils = new SettingUtils(); const nullIndex = options.indexOf(null); + let optionsArr = [] - // Check if the filename is in the existingList - const selectedLora = node.widgets[0].value; - const pathArray = selectedLora.split('\\'); - const filename = pathArray[pathArray.length - 1]; - const isFavourite = existingList.includes(filename); + loraWidgets.forEach(widget => { + const selectedLora = widget.value; + const pathArray = selectedLora.split('\\'); + const filename = pathArray[pathArray.length - 1]; + if (filename == "None") { + return; + } + const isFavourite = existingList.includes(filename); - // Create the new menu item - const newMenuItem = { - content: isFavourite ? 'Unfavourite Lora ☆' : 'Favourite Lora ★', - disabled: false, - callback: () => { - SettingUtils.toggleFavourite(existingList, filename, favouriteLoraId); - settingUtils.refreshComboInSingleNode(app, node.title); - }, - }; + const newMenuItem = { + content: (isFavourite ? 'Unfavourite ' : 'Favourite ') + filename.split(".")[0] + (isFavourite ? ' ☆' : ' ★'), + disabled: false, + callback: () => { + SettingUtils.toggleFavourite(existingList, filename, favouriteLoraId); + settingUtils.refreshComboInSingleNode(app, node.title); + }, + }; + + optionsArr.push(newMenuItem); + }); + + let menuItem + if (optionsArr.length > 0) { + if (totalLoras === 1) { + menuItem = optionsArr[0] + } else if (totalLoras > 1) { + menuItem = { + content: "Favourite", + disabled: false, + has_submenu: true, + submenu: { + options: optionsArr + } + } + } + } else { + menuItem = { + content: "No lora selected", + disabled: true, + } + } if (nullIndex !== -1) { - options.splice(nullIndex, 0, newMenuItem); + options.splice(nullIndex, 0, menuItem); } else { - options.push(newMenuItem); + options.push(menuItem); } } return options; diff --git a/web/js/simpleKSampler.js b/web/js/simpleKSampler.js index 522a8ee..f209590 100644 --- a/web/js/simpleKSampler.js +++ b/web/js/simpleKSampler.js @@ -23,13 +23,13 @@ app.registerExtension({ }; nodeType.prototype.onNodeCreated = function () { - this.inputs[3].type = ['STRING', 'CONDITIONING']; - this.inputs[4].type = ['STRING', 'CONDITIONING']; + //this.inputs[3].type = ['STRING', 'CONDITIONING']; + //this.inputs[4].type = ['STRING', 'CONDITIONING']; }; nodeType.prototype.onConfigure = function () { - this.inputs[3].type = ['STRING', 'CONDITIONING']; - this.inputs[4].type = ['STRING', 'CONDITIONING']; + //this.inputs[3].type = ['STRING', 'CONDITIONING']; + //this.inputs[4].type = ['STRING', 'CONDITIONING']; }; } }, diff --git a/web/js/sn0w.js b/web/js/sn0w.js index 4de6a8c..8551d6c 100644 --- a/web/js/sn0w.js +++ b/web/js/sn0w.js @@ -19,6 +19,23 @@ export class SettingUtils { } } + static async fetchApi(route, options) { + if (!options) { + options = {}; + } + if (!options.headers) { + options.headers = {}; + } + const url = `${location.protocol}//${location.host}${route}`; + try { + const response = await fetch(url, options); + const data = await response.json(); + return data; + } catch (error) { + console.error('There was a problem with the fetch operation:', error); + } + } + // Callback function to add or remove the filename from the settings list static async toggleFavourite(existingList, filename, setting) { try { @@ -47,35 +64,33 @@ export class SettingUtils { const textarea = document.createElement('textarea'); // Generate a unique ID for associating the label with the textarea - const uniqueId = `${name.replaceAll(' ', '').replace('[', '').replace(']', '-')}`; + const uniqueId = `${name.replaceAll(' ', '').replaceAll('[', '').replaceAll(']', '-')}`; label.setAttribute('for', uniqueId); label.textContent = name; textarea.id = uniqueId; textarea.value = value; const maxLines = 10; - const lineHeight = 15; // Function to set the height based on the number of lines const adjustHeight = () => { + textarea.style.height = ''; // Allow to shrink const lines = textarea.value.split('\n').length; + const scrollHeight = textarea.scrollHeight + 3; if (lines > maxLines) { + const height = (scrollHeight / lines) * maxLines textarea.setAttribute( 'style', - `width: 100%; height: ${lineHeight * maxLines}px; resize: none;` + `width: 100%; height: ${height}px; resize: none;` ); return; } - const height = lines * lineHeight; - textarea.setAttribute('style', `width: 100%; height: ${height}px; resize: none;`); + textarea.setAttribute('style', `width: 100%; height: ${scrollHeight}px; resize: none;`); }; adjustHeight(); textarea.onchange = () => setter(textarea.value); textarea.addEventListener('input', adjustHeight); - // Apply event handler - textarea.onchange = () => setter(textarea.value); - // Apply additional attributes for (const [key, val] of Object.entries(attrs)) { textarea.setAttribute(key, val); @@ -94,6 +109,17 @@ export class SettingUtils { label.className = 'comfy-tooltip-indicator'; } + // Update height when settings are opened + const dialog = document.getElementById('comfy-settings-dialog'); + const observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + if (mutation.type === 'attributes' && mutation.attributeName === 'open') { + adjustHeight(); + } + }); + }); + observer.observe(dialog, { attributes: true }); + return tr; } @@ -108,7 +134,7 @@ export class SettingUtils { const label = document.createElement('label'); // Generate a unique ID for the setting container - const uniqueId = `${name.replaceAll(' ', '').replace('[', '').replace(']', '-')}`; + const uniqueId = `${name.replaceAll(' ', '').replaceAll('[', '').replaceAll(']', '-')}`; label.setAttribute('for', uniqueId); label.textContent = name; diff --git a/web/js/textbox.js b/web/js/textbox.js index d556174..a79e697 100644 --- a/web/js/textbox.js +++ b/web/js/textbox.js @@ -228,7 +228,7 @@ app.registerExtension({ let trimmedTag = tag.trim(); // Remove HTML tags - trimmedTag = trimmedTag.replace(/<[^>]*>/g, ''); + trimmedTag = trimmedTag.replace(/<|>/g, ""); // Remove the first character if it is a parenthesis if (trimmedTag.startsWith('(')) { @@ -464,15 +464,9 @@ app.registerExtension({ } async function getValidFiles(type) { - const apiRequest = await api.fetchApi(`${SettingUtils.API_PREFIX}/${type}`, { + return await SettingUtils.fetchApi(`${SettingUtils.API_PREFIX}/${type}`, { method: "GET", - }) - if (!apiRequest.ok) { - console.error('API request failed:', apiRequest.statusText); - } else { - const responseBody = await apiRequest.json(); - return responseBody; - } + }); } nodeType.prototype.onNodeCreated = function () { diff --git a/web/settings/characters.json b/web/settings/characters.json index fd48a99..d23ea5c 100644 --- a/web/settings/characters.json +++ b/web/settings/characters.json @@ -159,6 +159,11 @@ "associated_string": "shylily, shylily \\(bikini\\), indie virtual youtuber", "prompt": "ahoge, blue hair, large breasts, long hair, multicolored hair, purple eyes, two-tone hair, virtual youtuber, white hair, animal ears, facial mark, orca girl" }, + { + "name": "Akuma Nihmune (Vtuber)", + "associated_string": "akuma nihmune, indie virtual youtuber", + "prompt": "purple hair, demon horns, mole under eye, heart tattoo, ear piercing, earrings" + }, { "name": "Hasumi (Blue Archive)", "associated_string": "hasumi \\(blue archive\\), blue archive",