Move all highlighting to new extension
This commit is contained in:
@@ -7,11 +7,10 @@
|
||||
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/sn0w12/ComfyUI-Sn0w-Scripts">
|
||||
</div>
|
||||
|
||||
|
||||
This extension contains a collection of nodes and improvements created for lora management and general quality of life.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> If you have saved a workflow with the `sn0w ksampler` node, make sure to re-save the workflow before the new comfyui frontend is implemented as the old implementation will corrupt your workflow.
|
||||
> All highlighting/ favourite handling has been moved to [ComfyUI-Syntax-Highlighting](https://github.com/sn0w12/ComfyUI-Syntax-Highlighting)
|
||||
|
||||
# Get Started
|
||||
|
||||
@@ -32,23 +31,20 @@ In the ComfyUI settings, you can modify various settings. Some changes will appl
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Options | Definition |
|
||||
| ----------- | ----------- | ----------- |
|
||||
| Animagine Prompt Style | True / False | Places "1girl/1boy" at the beginning of your prompts in the "Prompt Combine" node. |
|
||||
| Custom Lora Loaders SD... | Textbox | Allows creation of new LoRa loaders by specifying their name and path. |
|
||||
| Custom Textbox Colors | Textbox | Sets the highlight color for text in textboxes. |
|
||||
| Custom Textbox Gradient Highlight | True / False | Makes the textbox highlighting be a gradient between the first and last color based on the strength of the selection. |
|
||||
| Disable Default Characters | True / False | Disables the loading of the default characters in the "Character Selector" node. |
|
||||
| Highlight Favourite Items | True / False | Highlights favorite LoRas and characters in green. |
|
||||
| Logging Level | Warnings<br>Informational<br>Debug | Sets the severity level of logs that will be printed. |
|
||||
| Max Difference in Lora Loading | 0 - 100 | Specifies the maximum allowable difference between a tag and a LoRa for it to be loaded. |
|
||||
| Random Characters from Favourites Only | True / False | Ensures random characters are selected only from your favorited characters. |
|
||||
| Remove Lora Path | True / False | Removes the path information from loras and only keeps the name. |
|
||||
| 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. |
|
||||
| Setting | Options | Definition |
|
||||
| -------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| Animagine Prompt Style | True / False | Places "1girl/1boy" at the beginning of your prompts in the "Prompt Combine" node. |
|
||||
| Custom Lora Loaders SD... | Textbox | Allows creation of new LoRa loaders by specifying their name and path. |
|
||||
| Disable Default Characters | True / False | Disables the loading of the default characters in the "Character Selector" node. |
|
||||
| Logging Level | Warnings<br>Informational<br>Debug | Sets the severity level of logs that will be printed. |
|
||||
| Max Difference in Lora Loading | 0 - 100 | Specifies the maximum allowable difference between a tag and a LoRa for it to be loaded. |
|
||||
| Random Characters from Favourites Only | True / False | Ensures random characters are selected only from your favorited characters. |
|
||||
| Remove Lora Path | True / False | Removes the path information from loras and only keeps the name. |
|
||||
| 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]
|
||||
> 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.
|
||||
> 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:
|
||||
@@ -64,14 +60,17 @@ loras:
|
||||
## Lora Management
|
||||
|
||||
A major focus for me was making loras more manageable by letting users create their own lora loaders with specific folder paths and automatically loading loras based on tags. This makes browsing through your Loras much easier, though it may limit which Loras you can use depending on your configuration. You can configure custom Lora loaders in the ComfyUI settings with the following format:
|
||||
|
||||
```
|
||||
ExampleName1:Value1
|
||||
```
|
||||
|
||||
To create a Lora stack, add a number to the end:
|
||||
|
||||
```
|
||||
Styles XL:style:2
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Favourite Loras & Characters
|
||||
@@ -88,9 +87,9 @@ There are options to sort your loras either alphabetically or by the latest chan
|
||||
|
||||
Text in the copy/paste textbox will be highlighted when the text is in parentheses. You can change the colors of the highlighting by changing the `[Sn0w] Custom Textbox Colors` setting, there should be one either rgb `rgb(0, 0, 0)` or hex `#000000` color per line. You can choose to either do the highlighting by the strength of the text or the nested level it is in.
|
||||
|
||||
Nesting | Strength
|
||||
:-------------------------:|:-------------------------:
|
||||
 | 
|
||||
| Nesting | Strength |
|
||||
| :----------------------------------------: | :-------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
If you have unclosed parentheses it will be highlighted in red.
|
||||
|
||||
@@ -110,22 +109,30 @@ A custom scheduler that generally produces similar quality results as most other
|
||||
> <summary><i>Comparisons</i></summary>
|
||||
> Images generated at 20 steps, 8 cfg, 1152 x 896, no upscaling.<br> Sigmoid settings at 20 sigma max, 0 sigma min, 3.5 steepness and 0.8 midpoint ratio.
|
||||
>
|
||||
>```
|
||||
>(score_9, score_8_up, score_7_up, score_6_up, source_anime, BREAK:1.1), 1girl, lips, long hair, grey eyes, black coat, medium breasts, cowboy shot, black robe, gold jewelry, looking at viewer, simple background, solo, white background, closed mouth, arms behind back, (red hair, dress, long dress, robe, white dress, gold trim:1.1)
|
||||
>```
|
||||
> 
|
||||
> ```
|
||||
>(power \(chainsaw man\), chainsaw man:1.1), blonde hair, cross-shaped pupils, demon girl, demon horns, double-parted bangs, hair between eyes, long hair, red horns, symbol-shaped pupils, yellow eyes, 1girl, black necktie, black pants, collared shirt, double v, looking at viewer, necktie, orange eyes, pants, parted lips, red background, red jacket, sharp teeth, shirt partially tucked in, shirt tucked in, simple background, sleeves past wrists, smile, solo, teeth, v, white shirt, (score_9, score_8_up, score_7_up, score_6_up, source_anime, BREAK:1.1)
|
||||
>```
|
||||
> 
|
||||
> ```
|
||||
>(score_9, score_8_up, score_7_up, score_6_up, source_anime, BREAK:1.1), (makima \(chainsaw man\), chainsaw man:1.1), hair between eyes, long hair, medium breasts, red hair, ringed eyes, yellow eyes, 1836547, 1girl, @ \(symbol\), black necktie, braided ponytail, business suit, collared shirt, formal, light smile, looking at viewer, necktie, office lady, sidelocks, simple background, smile, solo, suit, white shirt
|
||||
>```
|
||||
> 
|
||||
> ```
|
||||
>(score_9, score_8_up, score_7_up, score_6_up, source_anime, BREAK:1.1), (makima \(chainsaw man\), chainsaw man:1.1), hair between eyes, long hair, medium breasts, red hair, ringed eyes, yellow eyes, 1girl, black jacket, black necktie, black pants, black vest, closed eyes, collared shirt, formal, necktie, pants, shirt tucked in, simple background, sitting, solo, suit jacket, vest, white background, white shirt
|
||||
>```
|
||||
> 
|
||||
> ```
|
||||
> (score_9, score_8_up, score_7_up, score_6_up, source_anime, BREAK:1.1), 1girl, lips, long hair, grey eyes, black coat, medium breasts, cowboy shot, black robe, gold jewelry, looking at viewer, simple background, solo, white background, closed mouth, arms behind back, (red hair, dress, long dress, robe, white dress, gold trim:1.1)
|
||||
> ```
|
||||
>
|
||||
> 
|
||||
>
|
||||
> ```
|
||||
> (power \(chainsaw man\), chainsaw man:1.1), blonde hair, cross-shaped pupils, demon girl, demon horns, double-parted bangs, hair between eyes, long hair, red horns, symbol-shaped pupils, yellow eyes, 1girl, black necktie, black pants, collared shirt, double v, looking at viewer, necktie, orange eyes, pants, parted lips, red background, red jacket, sharp teeth, shirt partially tucked in, shirt tucked in, simple background, sleeves past wrists, smile, solo, teeth, v, white shirt, (score_9, score_8_up, score_7_up, score_6_up, source_anime, BREAK:1.1)
|
||||
> ```
|
||||
>
|
||||
> 
|
||||
>
|
||||
> ```
|
||||
> (score_9, score_8_up, score_7_up, score_6_up, source_anime, BREAK:1.1), (makima \(chainsaw man\), chainsaw man:1.1), hair between eyes, long hair, medium breasts, red hair, ringed eyes, yellow eyes, 1836547, 1girl, @ \(symbol\), black necktie, braided ponytail, business suit, collared shirt, formal, light smile, looking at viewer, necktie, office lady, sidelocks, simple background, smile, solo, suit, white shirt
|
||||
> ```
|
||||
>
|
||||
> 
|
||||
>
|
||||
> ```
|
||||
> (score_9, score_8_up, score_7_up, score_6_up, source_anime, BREAK:1.1), (makima \(chainsaw man\), chainsaw man:1.1), hair between eyes, long hair, medium breasts, red hair, ringed eyes, yellow eyes, 1girl, black jacket, black necktie, black pants, black vest, closed eyes, collared shirt, formal, necktie, pants, shirt tucked in, simple background, sitting, solo, suit jacket, vest, white background, white shirt
|
||||
> ```
|
||||
>
|
||||
> 
|
||||
>
|
||||
> </details>
|
||||
|
||||
#### Sigmoid graph
|
||||
@@ -139,28 +146,34 @@ A custom scheduler that generally produces similar quality results as most other
|
||||
Nodes that are specifically for generating and processing images.
|
||||
|
||||
### Sn0w KSampler
|
||||
|
||||
> Ksampler with more settings and optional inputs. Support for custom schedulers
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> - Can take both text and conditioning as positive and negative input.
|
||||
> - Can take a latent image or can generate one if none is provided.
|
||||
> - Supports schedulers such as align your steps, you can also create your own scheduler in `src/custom_schedulers/`, make sure to follow the example provided in `get_sigmas_sigmoid.py`.
|
||||
> - You can also just get the sigmas from any of the scheduler nodes and drag them into `sigmas (optional)`, if you do the scheduler widget will not be used.
|
||||
> - Can take both text and conditioning as positive and negative input.
|
||||
> - Can take a latent image or can generate one if none is provided.
|
||||
> - Supports schedulers such as align your steps, you can also create your own scheduler in `src/custom_schedulers/`, make sure to follow the example provided in `get_sigmas_sigmoid.py`.
|
||||
> - You can also just get the sigmas from any of the scheduler nodes and drag them into `sigmas (optional)`, if you do the scheduler widget will not be used.
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
### Lora Tester
|
||||
|
||||
> Automatically generates X amount of images with provided loras.
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> - I recommend making lora_info and add_default_generation inputs and using the outputs from the Lora Selector.
|
||||
> - Takes normal KSampler input but takes positive and negative inputs as text.
|
||||
> - Outputs a batch of images.
|
||||
> - I recommend making lora_info and add_default_generation inputs and using the outputs from the Lora Selector.
|
||||
> - Takes normal KSampler input but takes positive and negative inputs as text.
|
||||
> - Outputs a batch of images.
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
## Lora Management
|
||||
@@ -168,131 +181,166 @@ Nodes that are specifically for generating and processing images.
|
||||
Nodes that handle lora selection, stacking, and loading.
|
||||
|
||||
### Load Lora Sn0w
|
||||
|
||||
> Normal load lora but can put favourite loras at the top of the list.
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> 
|
||||
> 
|
||||
>
|
||||
> </details>
|
||||
|
||||
### Load Lora Folder
|
||||
|
||||
> Dynamically applies Lora models based on similarity to a provided prompt.
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> - This node processes a given prompt to identify and apply the most similar Lora models of the tags found in the prompt.
|
||||
> - For each part of the prompt, the node calculates a distance between tags and available Lora model filenames. The max distance can be chosen in the settings.
|
||||
> - **Example folder input:** `*master_folder, subfolder1:3, -excludefolder, subfolder2`
|
||||
> - `*master_folder` specifies all paths must include `master_folder`.
|
||||
> - `subfolder1:3` indicates up to 3 models from `subfolder1` can be loaded.
|
||||
> - `-excludefolder` ensures any path containing `excludefolder` is ignored.
|
||||
> - `subfolder2` loads models from this subfolder without a numeric limit.
|
||||
> - Note: You need the loras separated into XL and 1.5 as stated in the `Important Note`.
|
||||
> - Note: Loras need to be named very similarly to the tag, with at most 5 characters different. Words in the lora filename can be separated by spaces or underscores.
|
||||
> - This node processes a given prompt to identify and apply the most similar Lora models of the tags found in the prompt.
|
||||
> - For each part of the prompt, the node calculates a distance between tags and available Lora model filenames. The max distance can be chosen in the settings.
|
||||
> - **Example folder input:** `*master_folder, subfolder1:3, -excludefolder, subfolder2`
|
||||
> - `*master_folder` specifies all paths must include `master_folder`.
|
||||
> - `subfolder1:3` indicates up to 3 models from `subfolder1` can be loaded.
|
||||
> - `-excludefolder` ensures any path containing `excludefolder` is ignored.
|
||||
> - `subfolder2` loads models from this subfolder without a numeric limit.
|
||||
> - Note: You need the loras separated into XL and 1.5 as stated in the `Important Note`.
|
||||
> - Note: Loras need to be named very similarly to the tag, with at most 5 characters different. Words in the lora filename can be separated by spaces or underscores.
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
### Lora Selector
|
||||
|
||||
> Automatically selects X amount of loras between two numbers.
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> - Loras have to be formatted like the default kohya_ss outputs. (lora_name-000001)
|
||||
> - Select the first lora.
|
||||
> - Select the number of the highest lora you want to test.
|
||||
> - Select the amount of loras you want to test.
|
||||
> - Outputs list of loras like this: \<lora:name:strength>
|
||||
> - Add default generation adds an extra "nothing" at the end of the list, used in Lora Tester to generate an image without the lora.
|
||||
> - Loras have to be formatted like the default kohya_ss outputs. (lora_name-000001)
|
||||
> - Select the first lora.
|
||||
> - Select the number of the highest lora you want to test.
|
||||
> - Select the amount of loras you want to test.
|
||||
> - Outputs list of loras like this: \<lora:name:strength>
|
||||
> - Add default generation adds an extra "nothing" at the end of the list, used in Lora Tester to generate an image without the lora.
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
### Lora Stacker
|
||||
|
||||
> Outputs a list of loras for Lora Tester. Does not load any loras by itself.
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> - Basically Lora Selector but manual.
|
||||
> - Specifically for the lora testers.
|
||||
> - Basically Lora Selector but manual.
|
||||
> - Specifically for the lora testers.
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
## Character and Text Processing
|
||||
|
||||
Nodes for selecting and combining character and textual elements.
|
||||
|
||||
### Character Selector
|
||||
|
||||
> Outputs a character name and prompt.
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> - Loads `characters.json` and outputs prompt based on it.
|
||||
> - You can create a file named `custom_characters.json` and add characters there if you want, they will be loaded with all the other characters if you format it like the `characters.json` file.
|
||||
> - If `custom_characters.json` has a character with the same name as `characters.json` it will add the custom prompt at the end of the normal one, this can be useful if you have loras that need activation tags.
|
||||
> - Loads `characters.json` and outputs prompt based on it.
|
||||
> - You can create a file named `custom_characters.json` and add characters there if you want, they will be loaded with all the other characters if you format it like the `characters.json` file.
|
||||
> - If `custom_characters.json` has a character with the same name as `characters.json` it will add the custom prompt at the end of the normal one, this can be useful if you have loras that need activation tags.
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
### Prompt Combine
|
||||
|
||||
> Combines multiple strings with a specified separator and optionally simplifies the result by removing redundant or incompatible tags.
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> - If simplify is enabled, the simplification process identifies and removes redundant tags (e.g., when a tag is fully encompassed by another, more descriptive tag) and tags incompatible with factors such as facing away, covered eyes, etc. Any tags that are in parentheses will not be removed.
|
||||
> - If simplify is enabled, the simplification process identifies and removes redundant tags (e.g., when a tag is fully encompassed by another, more descriptive tag) and tags incompatible with factors such as facing away, covered eyes, etc. Any tags that are in parentheses will not be removed.
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
## Utility Nodes
|
||||
|
||||
Nodes that provide utility functions across the system.
|
||||
|
||||
### Get Font Size Node
|
||||
|
||||
> Estimates the optimal font size for text to fit within an image based on Lora information.
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> - This node takes an image and a string, estimating the best font size to ensure that the longest piece of the string fits within the image. The approach considers the image's width and the length of the text to find a size that balances visibility and fit.
|
||||
> - This node takes an image and a string, estimating the best font size to ensure that the longest piece of the string fits within the image. The approach considers the image's width and the length of the text to find a size that balances visibility and fit.
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 
|
||||
> </details>
|
||||
|
||||
### Copy/Paste Textbox
|
||||
|
||||
> A multiline textbox with copy, paste and highlighting functionality.
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> 
|
||||
> 
|
||||
>
|
||||
> </details>
|
||||
|
||||
### Upscale Image With Model By
|
||||
|
||||
> Upscales an image by a certain amount with an upscale model.
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> 
|
||||
> 
|
||||
>
|
||||
> </details>
|
||||
|
||||
### Filter Tags
|
||||
|
||||
> Filters a string to only returns tags that have to do with a chosen category. // WIP
|
||||
>
|
||||
> <details>
|
||||
> <summary>ℹ️ <i>See More Information</i></summary>
|
||||
>
|
||||
> 
|
||||
> 
|
||||
>
|
||||
> </details>
|
||||
<br>
|
||||
> <br>
|
||||
|
||||
# Example Workflows
|
||||
|
||||
## Lora Tester
|
||||
|
||||
Note: Uses [ImagesGrid](https://github.com/LEv145/images-grid-comfy-plugin) for making the actual grid from the image output.
|
||||
|
||||

|
||||
|
||||
## T2I
|
||||
|
||||
My general text to image workflow, note that it uses several nodes from different custom nodes.
|
||||
|
||||

|
||||
|
||||
# Known Issues
|
||||
|
||||
* Embedding highlighting does not work on linux.
|
||||
- Embedding highlighting does not work on linux.
|
||||
|
||||
@@ -151,12 +151,24 @@ class ConfigReader:
|
||||
"sn0w.DisableDefaultCharacters": "sn0w.CharacterSettings.DisableDefaultCharacters",
|
||||
"sn0w.ExcludedRandomCharacters": "sn0w.CharacterSettings.ExcludedRandomCharacters",
|
||||
"sn0w.SortCharactersBy": "sn0w.CharacterSettings.SortCharactersBy",
|
||||
"sn0w.TextboxSettings.GradientColors": "SyntaxHighlighting.textbox-colors",
|
||||
}
|
||||
|
||||
path = ConfigReader._get_path()
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
settings = json.load(file)
|
||||
|
||||
# Handle favorites migration
|
||||
favorite_loras = cls.get_setting("sn0w.FavouriteLoras", [])
|
||||
favorite_chars = cls.get_setting("sn0w.FavouriteCharacters", [])
|
||||
|
||||
if favorite_loras or favorite_chars:
|
||||
combined_favorites = list(set(favorite_loras + favorite_chars)) # Merge and remove duplicates
|
||||
cls.set_setting("SyntaxHighlighting.favorites", combined_favorites)
|
||||
cls.set_setting("sn0w.FavouriteLoras", None) # Remove old setting
|
||||
cls.set_setting("sn0w.FavouriteCharacters", None) # Remove old setting
|
||||
|
||||
# Handle other setting migrations
|
||||
for setting in settings:
|
||||
if setting in conversion_map:
|
||||
setting_value = cls.get_setting(setting)
|
||||
@@ -390,15 +402,22 @@ class Utility:
|
||||
@classmethod
|
||||
def get_node_output(cls, data, node_id, output_id):
|
||||
"""Get the output of a node from the workflow data."""
|
||||
workflow = data.get("workflow", {})
|
||||
nodes = workflow.get("nodes", [])
|
||||
if not data:
|
||||
return None
|
||||
|
||||
for node in nodes:
|
||||
if int(node.get("id")) == int(node_id):
|
||||
for output in node.get("outputs", []):
|
||||
if int(output["slot_index"]) == int(output_id):
|
||||
return output
|
||||
return None
|
||||
try:
|
||||
workflow = data.get("workflow", {})
|
||||
nodes = workflow.get("nodes", [])
|
||||
|
||||
for node in nodes:
|
||||
if int(node.get("id")) == int(node_id):
|
||||
for output in node.get("outputs", []):
|
||||
if int(output["slot_index"]) == int(output_id):
|
||||
return output
|
||||
return None
|
||||
except Exception as e:
|
||||
cls.logger.log(f"Error getting node output: {e}", "ERROR")
|
||||
return None
|
||||
|
||||
|
||||
class AnyType(str):
|
||||
|
||||
@@ -113,10 +113,14 @@ class SimpleSamplerCustom:
|
||||
samples = custom_sampler.sample(
|
||||
model, add_noise, noise_seed, cfg, positive_prompt, negative_prompt, sampler, sigmas, latent_image
|
||||
)
|
||||
if image_output["links"] != []:
|
||||
try:
|
||||
if image_output is not None and image_output["links"] != []:
|
||||
image = vae_decode.decode(vae, samples[1])
|
||||
else:
|
||||
image = (None,)
|
||||
except (TypeError, KeyError):
|
||||
# If we get a TypeError (NoneType) or KeyError, decode the image
|
||||
image = vae_decode.decode(vae, samples[1])
|
||||
else:
|
||||
image = (None,)
|
||||
|
||||
return (image[0], samples[1], positive_prompt, negative_prompt)
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { SettingUtils } from "./sn0w.js";
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
const favouriteCharactersId = "sn0w.FavouriteCharacters";
|
||||
let existingList = [];
|
||||
|
||||
app.registerExtension({
|
||||
name: "sn0w.CharacterContextMenu",
|
||||
async setup() {
|
||||
const hasSyntaxHighlight = await SettingUtils.fetchApi("/SyntaxHighlighter/enabled");
|
||||
if (hasSyntaxHighlight.enabled) {
|
||||
SettingUtils.logSn0w("Syntax Highlighter is enabled.", "informational", "character");
|
||||
return;
|
||||
}
|
||||
|
||||
existingList = await SettingUtils.getSetting("sn0w.FavouriteCharacters");
|
||||
if (!Array.isArray(existingList)) {
|
||||
existingList = [];
|
||||
}
|
||||
|
||||
const original_getNodeMenuOptions = app.canvas.getNodeMenuOptions;
|
||||
app.canvas.getNodeMenuOptions = function (node) {
|
||||
const options = original_getNodeMenuOptions.apply(this, arguments);
|
||||
if (node.type === "Character Selector") {
|
||||
const settingUtils = new SettingUtils();
|
||||
const nullIndex = options.indexOf(null);
|
||||
|
||||
// Check if the filename is in the existingList
|
||||
const selectedCharacter = node.widgets[0].value;
|
||||
const isFavourite = existingList.includes(selectedCharacter);
|
||||
|
||||
// Create the new menu item
|
||||
const newMenuItem = {
|
||||
content: isFavourite ? "Unfavourite Character ☆" : "Favourite Character ★",
|
||||
disabled: false,
|
||||
callback: () => {
|
||||
SettingUtils.toggleFavourite(existingList, selectedCharacter, favouriteCharactersId);
|
||||
settingUtils.refreshComboInSingleNode(app, "Character Selector");
|
||||
},
|
||||
};
|
||||
|
||||
if (nullIndex !== -1) {
|
||||
options.splice(nullIndex, 0, newMenuItem);
|
||||
} else {
|
||||
options.push(newMenuItem);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
SettingUtils.observeContextMenu(existingList);
|
||||
},
|
||||
});
|
||||
@@ -1,154 +0,0 @@
|
||||
import { SettingUtils } from "./sn0w.js";
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
|
||||
async function addLoraLoaders(loraLoaders) {
|
||||
try {
|
||||
const response = await api.fetchApi(`${SettingUtils.API_PREFIX}/add_lora_loaders`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(loraLoaders),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
SettingUtils.logSn0w("Lora Loaders successfully added.", "informational", "lora", data.message);
|
||||
} catch (error) {
|
||||
SettingUtils.logSn0w(error.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
const settingDefinitions = [
|
||||
{
|
||||
id: "sn0w.HighlightFavourite",
|
||||
category: ["sn0w", "ContextMenu", "Highlight Favourite Items"],
|
||||
name: "Highlight Favourite Items",
|
||||
defaultValue: false,
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
id: "sn0w.previewImageDelay",
|
||||
category: ["sn0w", "ContextMenu", "Preview Image Delay"],
|
||||
name: "Preview Image Delay (ms)",
|
||||
defaultValue: 300,
|
||||
type: "slider",
|
||||
attrs: { min: 0, max: 500, step: 10 },
|
||||
},
|
||||
];
|
||||
const favouriteLoraId = "sn0w.FavouriteLoras";
|
||||
|
||||
let loraLoaders = ["Load Lora Sn0w", "LoraLoader"];
|
||||
let existingList = [];
|
||||
|
||||
app.registerExtension({
|
||||
name: "sn0w.LoraContextMenu",
|
||||
init() {
|
||||
settingDefinitions.forEach((settingDefinition) => {
|
||||
app.ui.settings.addSetting(settingDefinition);
|
||||
});
|
||||
},
|
||||
async setup() {
|
||||
const hasSyntaxHighlight = await SettingUtils.fetchApi("/SyntaxHighlighter/enabled");
|
||||
if (hasSyntaxHighlight.enabled) {
|
||||
SettingUtils.logSn0w("Syntax Highlighter is enabled.", "informational", "lora");
|
||||
return;
|
||||
}
|
||||
|
||||
const customLoaderKeys = ["sn0w.CustomLoraLoaders", "sn0w.CustomLoraLoaders.XL", "sn0w.CustomLoraLoaders.3"];
|
||||
let customLoraLoadersArrays = [];
|
||||
|
||||
// Fetch and process each custom loader
|
||||
for (const key of customLoaderKeys) {
|
||||
const customLoaders = await SettingUtils.getSetting(key);
|
||||
let customLoaderArray = [];
|
||||
if (customLoaders) {
|
||||
customLoaderArray = customLoaders.split("\n").map((item) => item.split(":")[0]);
|
||||
}
|
||||
customLoraLoadersArrays.push(customLoaderArray);
|
||||
}
|
||||
|
||||
existingList = await SettingUtils.getSetting(favouriteLoraId);
|
||||
|
||||
if (!Array.isArray(existingList)) {
|
||||
existingList = [];
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (!node.widgets) {
|
||||
return options;
|
||||
}
|
||||
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 = [];
|
||||
|
||||
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);
|
||||
|
||||
const newMenuItem = {
|
||||
content: (isFavourite ? "Unfavourite " : "Favourite ") + filename.split(".")[0] + (isFavourite ? " ☆" : " ★"),
|
||||
disabled: false,
|
||||
callback: () => {
|
||||
SettingUtils.toggleFavourite(existingList, filename, favouriteLoraId);
|
||||
settingUtils.refreshComboInSingleNode(app, node.title);
|
||||
},
|
||||
};
|
||||
|
||||
const itemExists = optionsArr.some((item) => item.content === newMenuItem.content);
|
||||
|
||||
if (!itemExists) {
|
||||
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, menuItem);
|
||||
} else {
|
||||
options.push(menuItem);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
SettingUtils.observeContextMenu(existingList);
|
||||
},
|
||||
});
|
||||
+1
-213
@@ -64,6 +64,7 @@ export class SettingUtils {
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("There was a problem with the fetch operation:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,219 +417,6 @@ export class SettingUtils {
|
||||
await this.#invokeExtensionsAsync("refreshComboInSingleNodeByName", graphCanvas, defs);
|
||||
}
|
||||
|
||||
static addPreviewImage(entry, path, maxHeight = 300, maxWidth = 300) {
|
||||
const preview = document.createElement("img");
|
||||
preview.className = "preview-image";
|
||||
preview.src = path;
|
||||
preview.style.maxWidth = `${maxWidth}px`;
|
||||
preview.style.maxHeight = `${maxHeight}px`;
|
||||
preview.style.position = "fixed";
|
||||
|
||||
const rect = entry.getBoundingClientRect();
|
||||
const padding = 20;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
if (rect.left >= maxWidth + padding) {
|
||||
preview.style.right = `${viewportWidth - rect.left + padding}px`;
|
||||
} else {
|
||||
preview.style.left = `${rect.right + padding}px`;
|
||||
}
|
||||
|
||||
const estimatedHeight = Math.min(maxHeight, preview.naturalHeight || maxHeight);
|
||||
let topPosition = rect.top;
|
||||
|
||||
if (rect.top + estimatedHeight > viewportHeight - padding) {
|
||||
topPosition = viewportHeight - estimatedHeight - padding;
|
||||
}
|
||||
|
||||
preview.style.top = `${topPosition}px`;
|
||||
document.body.appendChild(preview);
|
||||
|
||||
setTimeout(() => {
|
||||
preview.classList.add("fade-in");
|
||||
}, 100);
|
||||
}
|
||||
|
||||
static removeAllPreviewImages() {
|
||||
const previewImages = document.querySelectorAll(".preview-image");
|
||||
previewImages.forEach((img) => {
|
||||
img.classList.add("fade-out");
|
||||
img.classList.remove("fade-in");
|
||||
img.addEventListener(
|
||||
"transitionend",
|
||||
() => {
|
||||
img.remove();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static async addStarsToFavourited(menuEntries, existingList, previewImages) {
|
||||
const [highlightLora, hoverDelay] = await Promise.all([SettingUtils.getSetting("sn0w.HighlightFavourite"), SettingUtils.getSetting("sn0w.previewImageDelay", 300)]);
|
||||
|
||||
const root = document.documentElement;
|
||||
const comfyMenuBgColor = SettingUtils.hexToRgb(getComputedStyle(root).getPropertyValue("--comfy-menu-bg").trim());
|
||||
|
||||
// Function to check and update the background color
|
||||
const checkAndUpdateBackgroundColor = (entry, currentBgColor) => {
|
||||
if (highlightLora && currentBgColor == comfyMenuBgColor) {
|
||||
entry.setAttribute("style", "background-color: green !important");
|
||||
// Ensure the entry uses flexbox for alignment
|
||||
entry.style.display = "flex";
|
||||
entry.style.alignItems = "center";
|
||||
}
|
||||
};
|
||||
|
||||
const observerConfig = { attributes: true, attributeFilter: ["style"] };
|
||||
|
||||
// Create the observer
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === "attributes" && mutation.attributeName === "style") {
|
||||
const entry = mutation.target;
|
||||
const currentBgColor = window.getComputedStyle(entry).backgroundColor;
|
||||
checkAndUpdateBackgroundColor(entry, currentBgColor);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let hoverTimer;
|
||||
let starred = [0, []];
|
||||
menuEntries.forEach((entry) => {
|
||||
const value = entry.getAttribute("data-value");
|
||||
let filename = value;
|
||||
if (value !== null && value.includes("\\")) {
|
||||
const pathArray = value.split("\\");
|
||||
filename = pathArray[pathArray.length - 1];
|
||||
}
|
||||
|
||||
if (existingList.includes(filename)) {
|
||||
// Create star element
|
||||
const star = document.createElement("span");
|
||||
star.innerHTML = "★";
|
||||
star.style.marginLeft = "auto"; // Push the star to the right
|
||||
star.style.alignSelf = "center"; // Center the star vertically
|
||||
|
||||
// Ensure the entry uses flexbox for alignment
|
||||
entry.style.display = "flex";
|
||||
entry.style.alignItems = "center";
|
||||
|
||||
// Check if star is already added to avoid duplication
|
||||
if (!entry.querySelector("span")) {
|
||||
entry.appendChild(star);
|
||||
starred[0]++;
|
||||
starred[1].push(filename);
|
||||
}
|
||||
|
||||
// Initial background color check
|
||||
const currentBgColor = window.getComputedStyle(entry).backgroundColor;
|
||||
observer.observe(entry, observerConfig);
|
||||
checkAndUpdateBackgroundColor(entry, currentBgColor);
|
||||
}
|
||||
|
||||
entry.addEventListener("mouseover", () => {
|
||||
SettingUtils.removeAllPreviewImages();
|
||||
|
||||
hoverTimer = setTimeout(() => {
|
||||
const imageFileName = filename
|
||||
.split(".")[0]
|
||||
.replaceAll(/\(.*?\)/g, "")
|
||||
.trim()
|
||||
.replaceAll(" ", "_")
|
||||
.toLowerCase();
|
||||
|
||||
const imageData = previewImages.find((img) => img.filename.toLowerCase().trim().replaceAll(" ", "_") === imageFileName);
|
||||
|
||||
if (imageData) {
|
||||
SettingUtils.addPreviewImage(entry, imageData.path);
|
||||
}
|
||||
}, hoverDelay);
|
||||
});
|
||||
|
||||
entry.addEventListener("click", () => {
|
||||
clearTimeout(hoverTimer);
|
||||
SettingUtils.removeAllPreviewImages();
|
||||
});
|
||||
|
||||
entry.addEventListener("mouseout", () => {
|
||||
clearTimeout(hoverTimer);
|
||||
SettingUtils.removeAllPreviewImages();
|
||||
});
|
||||
});
|
||||
SettingUtils.logSn0w(`Total Favourited Items: ${starred[0]}`, "informational", "node", starred[1]);
|
||||
}
|
||||
|
||||
static async observeContextMenu(existingList) {
|
||||
SettingUtils.contextMenuList = Array.from(new Set([...SettingUtils.contextMenuList, ...existingList]));
|
||||
const hasSyntaxHighlight = await SettingUtils.fetchApi("/SyntaxHighlighter/enabled");
|
||||
if (SettingUtils.observingContextMenu || hasSyntaxHighlight.enabled === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
.preview-image {
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
.preview-image.fade-in {
|
||||
opacity: 1;
|
||||
}
|
||||
.preview-image.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
const images = await fetch("/extensions/ComfyUI-Sn0w-Scripts/images/images.json").then((response) => response.json());
|
||||
|
||||
const handleMutations = SettingUtils.leadingEdgeDebounce(function (mutations) {
|
||||
let isPreviewImage = false;
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.classList && node.classList.contains("preview-image")) {
|
||||
isPreviewImage = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
mutation.removedNodes.forEach((node) => {
|
||||
if (node.classList && node.classList.contains("preview-image")) {
|
||||
isPreviewImage = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (isPreviewImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const litecontextmenu = document.getElementsByClassName("litecontextmenu")[0];
|
||||
if (litecontextmenu) {
|
||||
const menuEntries = litecontextmenu.querySelectorAll(".litemenu-entry");
|
||||
SettingUtils.addStarsToFavourited(menuEntries, SettingUtils.contextMenuList, images.images);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const observer = new MutationObserver(handleMutations);
|
||||
|
||||
observer.observe(document, {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
characterData: false,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
SettingUtils.observingContextMenu = true;
|
||||
}
|
||||
|
||||
// MISC
|
||||
static debounce(func, delay) {
|
||||
let debounceTimer;
|
||||
|
||||
+1
-472
@@ -12,13 +12,6 @@ app.registerExtension({
|
||||
return textbox;
|
||||
};
|
||||
|
||||
nodeType.prototype.updateHighlightType = function () {
|
||||
const inputEl = this.inputEl.inputEl;
|
||||
const overlayEl = this.overlayEl;
|
||||
setTextColors(inputEl, overlayEl);
|
||||
setTimeout(() => syncText(this.inputEl.inputEl, this.overlayEl), 50);
|
||||
};
|
||||
|
||||
nodeType.prototype.populate = async function () {
|
||||
this.inputEl = this.widgets[0];
|
||||
|
||||
@@ -31,481 +24,17 @@ app.registerExtension({
|
||||
this.addWidget("button", "Paste", "Paste", () => {
|
||||
navigator.clipboard.readText().then(text => {
|
||||
this.inputEl.value = text;
|
||||
syncText(this.inputEl.inputEl, this.overlayEl);
|
||||
api.dispatchCustomEvent("update_text_highlight");
|
||||
}).catch(error => {
|
||||
console.error('Failed to read clipboard contents:', error);
|
||||
});
|
||||
}, { serialize: false });
|
||||
|
||||
// Ensure the input element's parent exists
|
||||
if (this.inputEl && this.inputEl.inputEl && this.inputEl.inputEl.parentNode) {
|
||||
const hasSyntaxHighlight = await SettingUtils.fetchApi("/SyntaxHighlighter/enabled");
|
||||
if (hasSyntaxHighlight.enabled) {
|
||||
SettingUtils.logSn0w("Syntax Highlighter is enabled.", "informational", "textbox");
|
||||
return;
|
||||
}
|
||||
// Create overlay div for highlighting
|
||||
this.overlayEl = document.createElement("div");
|
||||
this.overlayEl.className = "input-overlay";
|
||||
this.inputEl.inputEl.parentNode.insertBefore(this.overlayEl, this.inputEl.inputEl);
|
||||
|
||||
this.inputEl.inputEl.style.background = "transparent";
|
||||
|
||||
addEventListeners(this);
|
||||
addObversers(this);
|
||||
|
||||
setOverlayPosition(this.inputEl, this.overlayEl);
|
||||
setOverlayStyle(this.inputEl, this.overlayEl);
|
||||
} else {
|
||||
console.error("Parent node of input element is not available.");
|
||||
}
|
||||
};
|
||||
|
||||
function addEventListeners(node) {
|
||||
node.inputEl.inputEl.addEventListener("input", () => {
|
||||
syncText(node.inputEl.inputEl, node.overlayEl);
|
||||
setOverlayStyle(node.inputEl, node.overlayEl);
|
||||
});
|
||||
|
||||
node.inputEl.inputEl.addEventListener("keydown", (event) => {
|
||||
if (event.ctrlKey && (event.key === "ArrowUp" || event.key === "ArrowDown")) {
|
||||
setTimeout(() => {
|
||||
syncText(node.inputEl.inputEl, node.overlayEl);
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addObversers(node) {
|
||||
const observer = new MutationObserver(() => {
|
||||
setOverlayPosition(node.inputEl, node.overlayEl);
|
||||
});
|
||||
|
||||
observer.observe(node.inputEl.inputEl, {
|
||||
attributes: true,
|
||||
attributeFilter: ["style"],
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
|
||||
const parentObserver = new MutationObserver(() => {
|
||||
if (!document.contains(node.inputEl.inputEl)) {
|
||||
node.overlayEl.remove();
|
||||
}
|
||||
});
|
||||
|
||||
parentObserver.observe(node.inputEl.inputEl.parentNode, {
|
||||
childList: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function setTextHighlightType(inputEl) {
|
||||
const highlightGradient = await SettingUtils.getSetting("sn0w.TextboxSettings.GradientColors");
|
||||
|
||||
let shouldHighlightGradient = false;
|
||||
switch (highlightGradient) {
|
||||
case "nesting":
|
||||
shouldHighlightGradient = false;
|
||||
break;
|
||||
case "strength":
|
||||
shouldHighlightGradient = true;
|
||||
break;
|
||||
default:
|
||||
shouldHighlightGradient = false;
|
||||
break;
|
||||
}
|
||||
|
||||
inputEl.highlightGradient = shouldHighlightGradient;
|
||||
}
|
||||
|
||||
async function setTextColors(inputEl, overlayEl) {
|
||||
const customTextboxColors = await SettingUtils.getSetting("sn0w.TextboxSettings");
|
||||
setTextHighlightType(inputEl);
|
||||
|
||||
const defaultColors = ["#559c22", "#229c57", "#229c8b", "#226f9c", "#22479c"];
|
||||
|
||||
const colors = !customTextboxColors || customTextboxColors.trim() === "" ? defaultColors : customTextboxColors.split("\n");
|
||||
|
||||
inputEl.colors = colors.map((color) => (color.charAt(0) === "#" ? SettingUtils.hexToRgb(color) : color));
|
||||
inputEl.errorColor = "var(--error-text)";
|
||||
|
||||
syncText(inputEl, overlayEl);
|
||||
return colors;
|
||||
}
|
||||
|
||||
function escapeHtml(char) {
|
||||
switch (char) {
|
||||
case "<":
|
||||
return "<";
|
||||
case ">":
|
||||
return ">";
|
||||
default:
|
||||
return char;
|
||||
}
|
||||
}
|
||||
|
||||
function interpolateColor(color1, color2, factor) {
|
||||
const rgb1 = color1.match(/\d+/g).map(Number);
|
||||
const rgb2 = color2.match(/\d+/g).map(Number);
|
||||
|
||||
const r = Math.round(rgb1[0] + factor * (rgb2[0] - rgb1[0]));
|
||||
const g = Math.round(rgb1[1] + factor * (rgb2[1] - rgb1[1]));
|
||||
const b = Math.round(rgb1[2] + factor * (rgb2[2] - rgb1[2]));
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
function easeInOutCubic(t) {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
|
||||
function charToType(char) {
|
||||
switch (char) {
|
||||
case "<":
|
||||
return "-lora";
|
||||
case "e":
|
||||
return "-embedding";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function extractLoraName(text, currentIndex) {
|
||||
const loraPrefix = "lora:";
|
||||
const startIndex = text.lastIndexOf(loraPrefix, currentIndex) + loraPrefix.length;
|
||||
|
||||
// Find the end of the LoRA name (next space, comma, or ".safetensors")
|
||||
let endIndex = text.indexOf(" ", startIndex);
|
||||
const commaIndex = text.indexOf(",", startIndex);
|
||||
const safetensorsIndex = text.indexOf(".safetensors", startIndex);
|
||||
|
||||
if (endIndex === -1 || (commaIndex !== -1 && commaIndex < endIndex)) {
|
||||
endIndex = commaIndex;
|
||||
}
|
||||
if (endIndex === -1 || (safetensorsIndex !== -1 && safetensorsIndex < endIndex)) {
|
||||
endIndex = safetensorsIndex;
|
||||
}
|
||||
|
||||
// If there's no space, comma, or ".safetensors" after the LoRA name, consider the end of the text
|
||||
const loraName = endIndex === -1 ? text.slice(startIndex) : text.slice(startIndex, endIndex);
|
||||
return loraName;
|
||||
}
|
||||
|
||||
function validateName(validFiles, name) {
|
||||
if (!validFiles) {
|
||||
console.error("Valid names not defined or not an array.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validFiles.includes(name)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function processTags(tags) {
|
||||
let returnArray = [];
|
||||
tags.forEach((tag) => {
|
||||
if (tag.trim()) {
|
||||
returnArray.push(processTag(tag));
|
||||
}
|
||||
});
|
||||
return returnArray;
|
||||
}
|
||||
|
||||
function processTag(tag) {
|
||||
let trimmedTag = tag.trim();
|
||||
|
||||
// Remove HTML tags
|
||||
trimmedTag = trimmedTag.replace(/<|>/g, "");
|
||||
|
||||
// Remove the first character if it is a parenthesis
|
||||
if (trimmedTag.startsWith("(")) {
|
||||
trimmedTag = trimmedTag.substring(1).trim();
|
||||
}
|
||||
|
||||
// Remove everything after a colon
|
||||
const colonIndex = trimmedTag.indexOf(":");
|
||||
if (colonIndex !== -1) {
|
||||
trimmedTag = trimmedTag.substring(0, colonIndex).trim();
|
||||
}
|
||||
|
||||
return trimmedTag;
|
||||
}
|
||||
|
||||
const charPairs = {
|
||||
"(": ")",
|
||||
"<": ">",
|
||||
};
|
||||
|
||||
async function syncText(inputEl, overlayEl, tries = 1) {
|
||||
const text = inputEl.value;
|
||||
overlayEl.textContent = text;
|
||||
|
||||
const colors = inputEl.colors;
|
||||
const errorColor = inputEl.errorColor;
|
||||
const shouldHighlightGradient = inputEl.highlightGradient;
|
||||
const loraColor = colors ? colors[0] : undefined;
|
||||
|
||||
if (!colors || !errorColor || !loraColor || shouldHighlightGradient == undefined) {
|
||||
if (tries < 5) {
|
||||
setTimeout(() => syncText(inputEl, overlayEl, tries++), tries * 5);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let uniqueIdCounter = 0;
|
||||
const generateUniqueId = (type = "") => `span-${uniqueIdCounter++}${type}`;
|
||||
|
||||
let nestingLevel = 0;
|
||||
let highlightedText = "";
|
||||
let lastIndex = 0;
|
||||
let spanStack = [];
|
||||
const uniqueIdMap = new Map();
|
||||
|
||||
const extractTags = (text) => {
|
||||
let tags = text.split(",").map((tag) => tag.trim());
|
||||
tags = processTags(tags);
|
||||
const tagCounts = new Map();
|
||||
|
||||
tags.forEach((tag) => {
|
||||
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
||||
});
|
||||
|
||||
return { tags, tagCounts };
|
||||
};
|
||||
|
||||
const { tags, tagCounts } = extractTags(text);
|
||||
|
||||
/*
|
||||
* This loop iterates over each character in the `text` string to apply syntax highlighting and handle special cases.
|
||||
* - lastIndex: index of the last processed character, used to slice text segments.
|
||||
* - spanStack: stack to manage opened spans, storing their ids, start positions, original colors, and characters.
|
||||
*/
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i].toLowerCase();
|
||||
|
||||
// Handle escape characters
|
||||
if (char === "\\" && i + 1 < text.length && (text[i + 1] === "(" || text[i + 1] === ")")) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle wrong escape characters
|
||||
if (char === "/" && i + 1 < text.length && (text[i + 1] === "(" || text[i + 1] === ")")) {
|
||||
highlightedText += text.slice(lastIndex, i) + `<span style="background-color: ${errorColor};">/</span>`;
|
||||
console.error(`Replace "${char}" at char ${i} with "\\"`);
|
||||
lastIndex = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let color = colors[nestingLevel % colors.length];
|
||||
let uniqueId = generateUniqueId(charToType(char));
|
||||
if (inputEl.highlightGradient === true) {
|
||||
color = `id-${uniqueId}`;
|
||||
}
|
||||
switch (char) {
|
||||
case "(":
|
||||
case "<":
|
||||
highlightedText += text.slice(lastIndex, i) + `<span id="${uniqueId}" style="background-color: ${color};">${escapeHtml(char)}`;
|
||||
spanStack.push({ id: uniqueId, start: highlightedText.length, originalSpan: `<span id="${uniqueId}" style="background-color: ${color};">`, nestingLevel, originalColor: color, originalChar: char });
|
||||
nestingLevel++;
|
||||
lastIndex = i + 1;
|
||||
break;
|
||||
case "e":
|
||||
let embeddingColor = colors[0];
|
||||
const embeddingPrefix = "embedding:";
|
||||
|
||||
// Check if the text starts with "embedding:" at position i
|
||||
if (text.toLowerCase().startsWith(embeddingPrefix, i)) {
|
||||
// Find the end of the embedding (next space or comma)
|
||||
let endIndex = i + embeddingPrefix.length;
|
||||
while (endIndex < text.length && text[endIndex] !== " " && text[endIndex] !== ",") {
|
||||
endIndex++;
|
||||
}
|
||||
|
||||
// Get the full embedding text
|
||||
const embeddingText = text.slice(i, endIndex);
|
||||
if (validateName(inputEl.validEmbeddings, embeddingText.split(":")[1]) === false) {
|
||||
embeddingColor = errorColor;
|
||||
}
|
||||
const wrappedEmbedding = `<span id="${uniqueId}" style="background-color: ${embeddingColor};">${escapeHtml(embeddingText)}</span>`;
|
||||
highlightedText += text.slice(lastIndex, i) + wrappedEmbedding;
|
||||
lastIndex = i + embeddingText.length;
|
||||
}
|
||||
break;
|
||||
case ")":
|
||||
case ">":
|
||||
if (nestingLevel > 0) {
|
||||
const { id, originalColor, originalChar } = spanStack[spanStack.length - 1];
|
||||
if (charPairs[originalChar] === char) {
|
||||
spanStack.pop();
|
||||
highlightedText += text.slice(lastIndex, i) + `${escapeHtml(char)}</span>`;
|
||||
nestingLevel--;
|
||||
|
||||
// Extract and validate the LoRA name
|
||||
if (id.endsWith("lora")) {
|
||||
const loraName = extractLoraName(text, i);
|
||||
if (validateName(inputEl.validLoras, loraName) === false) {
|
||||
uniqueIdMap.set(id, [errorColor, originalColor]);
|
||||
lastIndex = i + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (inputEl.highlightGradient === true || id.endsWith("lora")) {
|
||||
// Check for the strength
|
||||
const strengthText = text.slice(Math.max(0, i - 10), i);
|
||||
const match = strengthText.match(/(\d+(\.\d+)?)\s*$/);
|
||||
if (match) {
|
||||
const strength = parseFloat(match[1]);
|
||||
const clampedStrength = Math.max(0, Math.min(2, strength));
|
||||
const normalizedStrength = clampedStrength / 2;
|
||||
const newColor = interpolateColor(colors[0], colors[colors.length - 1], easeInOutCubic(normalizedStrength));
|
||||
uniqueIdMap.set(id, [newColor, originalColor]);
|
||||
} else {
|
||||
uniqueIdMap.set(id, [interpolateColor(colors[0], colors[colors.length - 1], 0.5), originalColor]);
|
||||
}
|
||||
}
|
||||
lastIndex = i + 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
highlightedText += text.slice(lastIndex);
|
||||
|
||||
if (nestingLevel > 0) {
|
||||
// Apply red highlight to the unclosed spans
|
||||
while (spanStack.length > 0) {
|
||||
const spanData = spanStack.pop();
|
||||
if (spanData) {
|
||||
const { id, start, originalSpan } = spanData;
|
||||
const errorSpanTag = `<span id="${id}" style="background-color: ${errorColor};">`;
|
||||
|
||||
if (originalSpan) {
|
||||
highlightedText = highlightedText.replace(originalSpan, errorSpanTag);
|
||||
highlightedText += `</span>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the updated colors to the highlighted text
|
||||
uniqueIdMap.forEach((colors, id) => {
|
||||
const [newColor, originalColor] = [colors[0], colors[1]];
|
||||
highlightedText = highlightedText.replace(`id="${id}" style="background-color: ${originalColor};"`, `id="${id}" style="background-color: ${newColor};"`);
|
||||
});
|
||||
|
||||
// Highlight duplicate tags
|
||||
highlightedText
|
||||
.split(/(,)/)
|
||||
.map((segment) => {
|
||||
if (segment === ",") {
|
||||
return segment;
|
||||
}
|
||||
|
||||
const trimmedSegment = processTag(segment);
|
||||
|
||||
// Check for duplicates
|
||||
if (tagCounts.get(trimmedSegment) > 1) {
|
||||
highlightedText = highlightedText.replaceAll(trimmedSegment, `<span style="background-color: ${errorColor};">${trimmedSegment}</span>`);
|
||||
}
|
||||
})
|
||||
.join("");
|
||||
|
||||
overlayEl.innerHTML = highlightedText;
|
||||
}
|
||||
|
||||
function setOverlayPosition(inputEl, overlayEl) {
|
||||
const textareaStyle = window.getComputedStyle(inputEl.inputEl);
|
||||
overlayEl.style.left = textareaStyle.left;
|
||||
overlayEl.style.top = textareaStyle.top;
|
||||
overlayEl.style.width = textareaStyle.width;
|
||||
overlayEl.style.height = textareaStyle.height;
|
||||
overlayEl.style.display = textareaStyle.display;
|
||||
overlayEl.style.transform = textareaStyle.transform;
|
||||
overlayEl.style.transformOrigin = textareaStyle.transformOrigin;
|
||||
}
|
||||
|
||||
function setOverlayStyle(inputEl, overlayEl) {
|
||||
const textareaStyle = window.getComputedStyle(inputEl.inputEl);
|
||||
overlayEl.style.backgroundColor = "var(--comfy-input-bg)";
|
||||
overlayEl.style.position = "absolute";
|
||||
overlayEl.style.fontFamily = textareaStyle.fontFamily;
|
||||
overlayEl.style.fontSize = textareaStyle.fontSize;
|
||||
overlayEl.style.fontWeight = textareaStyle.fontWeight;
|
||||
overlayEl.style.lineHeight = textareaStyle.lineHeight;
|
||||
overlayEl.style.letterSpacing = textareaStyle.letterSpacing;
|
||||
overlayEl.style.whiteSpace = textareaStyle.whiteSpace;
|
||||
overlayEl.style.color = "rgba(0,0,0,0)";
|
||||
overlayEl.style.padding = textareaStyle.padding;
|
||||
overlayEl.style.boxSizing = textareaStyle.boxSizing;
|
||||
overlayEl.style.zIndex = "1";
|
||||
overlayEl.style.pointerEvents = "none";
|
||||
overlayEl.style.color = "transparent";
|
||||
overlayEl.style.overflow = "hidden";
|
||||
overlayEl.style.whiteSpace = "pre-wrap";
|
||||
overlayEl.style.wordWrap = "break-word";
|
||||
}
|
||||
|
||||
async function setValidFiles(inputEl) {
|
||||
inputEl.validLoras = await getValidFiles("loras");
|
||||
inputEl.validEmbeddings = await getValidFiles("embeddings");
|
||||
}
|
||||
|
||||
async function getValidFiles(type) {
|
||||
return await SettingUtils.fetchApi(`${SettingUtils.API_PREFIX}/${type}`, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
this.populate();
|
||||
setTextColors(this.inputEl.inputEl, this.overlayEl);
|
||||
setValidFiles(this.inputEl.inputEl);
|
||||
this.getTextboxText = this.getTextboxText.bind(this);
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const settingsDefinitions = [
|
||||
{
|
||||
id: "sn0w.TextboxSettings",
|
||||
name: "Custom Textbox Colors",
|
||||
type: SettingUtils.createMultilineSetting,
|
||||
defaultValue: "#559c22\n#229c57\n#229c8b\n#226f9c\n#22479c",
|
||||
tooltip: "A list of either rgb or hex colors, one color per line.",
|
||||
onChange: () => {
|
||||
const nodes = app.graph._nodes.filter((node) => node.type === "Copy/Paste Textbox");
|
||||
nodes.forEach((node) => {
|
||||
node.updateHighlightType();
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "sn0w.TextboxSettings.GradientColors",
|
||||
name: "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) => {
|
||||
node.updateHighlightType();
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Register settings
|
||||
settingsDefinitions.forEach((setting) => {
|
||||
SettingUtils.registerSetting(setting);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user