Compare commits

..

97 Commits
2.0.0 ... 2.5.0

Author SHA1 Message Date
DominikDoom
1cb4fc8f25 Merge branch 'feature-refresh-temp-files' into main
Added a "fake" settings option that refreshes some internal temporary files without needing to restart
2023-05-26 23:45:19 +02:00
DominikDoom
2d7e6181f5 Fix embed refresh & JS selector 2023-05-26 17:18:53 +02:00
DominikDoom
c6049fc2fa Separate refresh and write function to include embeds 2023-05-26 16:55:34 +02:00
DominikDoom
eff53d0ca7 Reset embeds too 2023-05-26 16:47:42 +02:00
DominikDoom
a7da670e97 New "setting" for reloading temp files
This allows refreshing Lora, lycos, hypernets and wildcards without having to restart the UI
2023-05-26 15:40:25 +02:00
DominikDoom
da65feea31 Update README.md
Added openpose-editor bug to known issues since it appears so often
2023-05-24 12:58:15 +02:00
DominikDoom
6997558714 Support for hires fix prompts
Only available on the webui dev branch for now.
Closes #179
2023-05-21 13:54:38 +02:00
DominikDoom
a95f422f10 Merge branch 'feature-rubytext' into main
Live translation feature, pretty WIP so expect some bugs
2023-05-15 19:06:09 +02:00
DominikDoom
5640a438da Merge branch 'main' of https://github.com/DominikDoom/a1111-sd-webui-tagcomplete 2023-05-11 21:18:09 +02:00
DominikDoom
3169c4c653 Support searching chants by name 2023-05-11 21:17:55 +02:00
DominikDoom
bba5d6b1c0 Update README.md 2023-05-11 21:14:40 +02:00
DominikDoom
7fbfb7dd43 Edit README 2023-05-11 19:28:04 +02:00
DominikDoom
2e93691305 Edit README 2023-05-11 19:27:40 +02:00
DominikDoom
eaba97dc3a Format chant fields as list in readme 2023-05-11 19:26:33 +02:00
DominikDoom
5e453efc2b Add chants to readme 2023-05-11 19:25:30 +02:00
DominikDoom
13c7f31bba Merge branch 'feature-chants' into main 2023-05-11 18:42:20 +02:00
DominikDoom
c4c588c1d4 Fix a few typos in demo chants 2023-05-11 18:41:10 +02:00
DominikDoom
1d40449942 Use file selection for chants, fix sorting 2023-05-11 18:37:55 +02:00
DominikDoom
495feb026c Fix formatting 2023-05-11 17:30:47 +02:00
Gin
b59123f6e6 new chants 2023-05-11 18:51:28 +08:00
Gin
79b670eaea fix chants search issue 2023-05-11 13:46:45 +08:00
Gin
6b51dc806b fix chants search issue 2023-05-11 12:47:01 +08:00
Gin
5b9af499c3 吟唱模板功能基本完成 2023-05-11 12:11:18 +08:00
Gin
aed449c882 fix chant issue 2023-05-11 11:21:53 +08:00
Gin
a724da362c try chants 2023-05-11 10:18:49 +08:00
DominikDoom
cc67adf82f Fix quicksettings dropdown changes not being detected
Fixes #176
2023-05-09 14:42:32 +02:00
DominikDoom
c1d610e390 Fix list sometimes not scrolling to top (again) 2023-05-08 19:06:11 +02:00
DominikDoom
7fabc84a1e Prefix CFG and loading vars to prevent collisions 2023-05-08 18:47:05 +02:00
DominikDoom
5d5db7bafe Fix for gradio 3.16
Should resolve part of #175
2023-05-07 20:45:20 +02:00
DominikDoom
40edb89974 Fix model name not being detected after startup
Fixes #174
2023-05-06 20:09:24 +02:00
DominikDoom
f9f7732c69 Add missing safety check
Fixes #173
2023-05-06 11:54:34 +02:00
DominikDoom
e7af9dbfba Remove unnecessary css
Has become obsolete with recent gradio update
2023-05-05 16:00:52 +02:00
DominikDoom
64cf9b2159 Make model dropdown selectors a bit more compact 2023-05-05 15:58:47 +02:00
DominikDoom
ba38d1b893 Fix model change detection in newest gradio
Fixes #172
2023-05-05 11:28:01 +02:00
DominikDoom
4442cb78ec Add theme detection fallback for old versions 2023-05-04 13:06:10 +02:00
DominikDoom
6b42efaa40 Add multidiff upscaler to third party text 2023-05-04 13:04:08 +02:00
DominikDoom
47e0c15835 Merge pull request #171 from re-unknown/main 2023-05-04 12:58:04 +02:00
DominikDoom
68b4224f37 Add support for tiled-diffusion
https://github.com/pkuliyi2015/multidiffusion-upscaler-for-automatic1111
2023-05-04 12:26:55 +02:00
DominikDoom
fc6c1ff579 Add on-demand textarea setup for accordions
Closes #170
2023-05-04 12:25:49 +02:00
ReUnknown
50b33b987a Merge branch 'main' of https://github.com/re-unknown/a1111-sd-webui-tagcomplete into main 2023-05-04 19:25:26 +09:00
ReUnknown
ea24f7657a Text update of settings related to third parties 2023-05-04 19:23:59 +09:00
DominikDoom
1e9431faba Merge pull request #169 from re-unknown/main 2023-05-03 15:57:59 +02:00
ReUnknown
664ae50c1a Support for Tagger 2023-05-03 19:49:42 +09:00
ReUnknown
43243a9bf1 Merge branch 'main' of https://github.com/DominikDoom/a1111-sd-webui-tagcomplete into main 2023-05-03 19:35:29 +09:00
ReUnknown
8912957a26 Support for image browser 2023-05-03 19:35:27 +09:00
DominikDoom
5fe5398b94 Cosmetic variable name fixes 2023-05-03 10:50:47 +02:00
DominikDoom
52f92e4d42 Use glob module instead of pathlib glob
Resolves symlink issue, closes #168
2023-05-03 10:41:22 +02:00
DominikDoom
0e177d0945 Add option for live preview 2023-05-02 17:59:36 +02:00
DominikDoom
94365630c7 Fix end-of-tag detection for brackets before a weight modifier 2023-05-02 17:46:04 +02:00
DominikDoom
d4941c7b73 Fix rt color 2023-05-02 17:46:04 +02:00
DominikDoom
91fb1cba38 Fix replacer matching partial words
Now checks for end of word / string or tag boundary correctly
2023-05-02 17:46:04 +02:00
DominikDoom
3169420fd3 Fix parentheses parsing 2023-05-02 17:46:04 +02:00
DominikDoom
84b6a0394e Fix more bugs related to replacing
Also some involving parentheses
2023-05-02 17:46:04 +02:00
DominikDoom
38fd2523e6 Fix issue with self-containing tags, add auto select on click 2023-05-02 17:46:04 +02:00
DominikDoom
85db4a61df Adjust styling for bigger translation 2023-05-02 17:46:04 +02:00
DominikDoom
b18823e88f Sliding window search, fix double replacement 2023-05-02 17:46:04 +02:00
DominikDoom
83461e2f54 Ruby Text live translation feature (WIP) 2023-05-02 17:46:04 +02:00
DominikDoom
a2e7b6bf6c Also fix dark mode detection for tag colors 2023-05-02 12:30:04 +02:00
DominikDoom
f4572469c1 Merge pull request #167 from derrian-distro/main 2023-05-02 10:09:31 +02:00
derrian-distro
672d409e46 fixed issue in which dark mode was not detected on new auto 2023-05-01 15:06:05 -04:00
DominikDoom
3b51035c26 Fix extra file translations being overwritten if translation file changes 2023-04-21 21:21:21 +02:00
DominikDoom
dcc6602056 Allow translations directly in extra file
QoL feature so you don't need to switch files for the small extra sets
Closes #164
2023-04-21 10:32:52 +02:00
DominikDoom
d1357cddc1 Merge pull request #161 from Kamilowaty122/main
Closes #162
2023-04-15 13:25:07 +02:00
Kamilowaty122
11d94e11f9 Add support for LyCORIS
Adds support for LyCORIS from https://github.com/KohakuBlueleaf/a1111-sd-webui-lycoris
2023-04-15 07:49:11 +02:00
DominikDoom
5fbc18ed1d Use correct separator for extra networks
Now uses the dedicated webui option for it instead of comma
Closes #160
2023-04-13 20:54:48 +02:00
DominikDoom
223abf5420 Remove multiline args from fallback
Fixes #156
2023-04-05 13:05:44 +02:00
DominikDoom
4331bdccda Add mapping fallback for older gradio versions
Fixes #151, fixes #152, fixes #154
2023-04-04 09:06:48 +02:00
DominikDoom
c3f53e1a60 Update README_ZH.md 2023-04-03 14:30:33 +02:00
DominikDoom
951e82f055 Update README.md 2023-04-03 14:25:45 +02:00
DominikDoom
cd52a0577c Update README.md 2023-04-03 14:23:08 +02:00
DominikDoom
873a15b5f6 Move user mappings to options to prevent override
Adresses #127
2023-04-03 14:14:39 +02:00
Dominik Reh
811d4622e9 Model switch fallback for older gradio versions
Fixes #149
2023-03-28 20:49:09 +02:00
Dominik Reh
296b9456cc Don't block enter if no tag is selected
Fixes #147
2023-03-27 01:19:24 +02:00
Dominik Reh
32c7749a5f Fallback for extensions dir
Fixes #145
2023-03-26 16:32:20 +02:00
Dominik Reh
afe3f23afa Use paths provided by webui where applicable
This doesn't solve the gradio security policy issue, just eliminates a bit of guesswork
2023-03-26 10:59:22 +02:00
Dominik Reh
2571a4f70a Fix for last commit 2023-03-26 10:56:11 +02:00
Dominik Reh
f8e15307c6 Set notranslate class & attribute for results
Potential fix for #144
2023-03-26 10:45:03 +02:00
Dominik Reh
95ebde9fce Small check to prevent error during UI restart
Not completely fixed due to gradioApp() stack overflow, but the only thing I can do on my side.
2023-03-25 14:01:37 +01:00
Dominik Reh
c08746a2c0 Fix model name based blacklist
Now also based on the hash mutation observer
2023-03-25 13:40:04 +01:00
DominikDoom
39abf1fe3a Merge pull request #141 from space-nuko/gradio-3.22.1 2023-03-25 13:22:13 +01:00
space-nuko
b047095f80 Fix for gradioApp() 2023-03-21 19:13:05 -04:00
space-nuko
caf65bfda0 Fixes for Gradio 3.22.1 2023-03-20 04:21:05 -04:00
Dominik Reh
e7fa5aca18 Allow longer weighted editing
Also ignores closing parentheses now for writing a prompt in brackets
See #107
2023-03-18 16:03:21 +01:00
Dominik Reh
f026e7631c Fix "Only show alias" option
Fixes #140
2023-03-16 16:30:17 +01:00
DominikDoom
15538336c9 Update README.md
Add Umi AI description
2023-03-07 21:36:40 +01:00
DominikDoom
12340c37cb Update README.md
Add more details for recent embedding, hypernet and lora additions, add more recent install info
2023-03-07 21:01:59 +01:00
Dominik Reh
d1fff7bfa7 Use _ as a stand-in for spaces in < completion
Allows to continue typing embeddings, loras and hypernets if they have spaces in their file name
Closes #130
2023-03-01 11:14:06 +01:00
Dominik Reh
647d3f7ec3 Remove unused imports 2023-02-28 10:43:24 +01:00
Dominik Reh
90664d47bf Fixes for PR #128
Replaced the caret position detection with a more robust version
Added option to turn it off
2023-02-26 17:58:00 +01:00
For-ACGN
bc56c3ca72 add function getTextWidth and improve showResults. 2023-02-26 15:39:17 +08:00
Dominik Reh
823958507b Fix lora and hypernet sorting
Subfolders broke overall alphabetic order, so a final pass was needed
2023-02-25 13:03:05 +01:00
Dominik Reh
712c4a5862 Trigger on unabbreviated lora/hypernet prefix
Makes going back to edit or switch out stuff easier since you don't need to erase the prefix anymore
2023-02-24 14:59:20 +01:00
Dominik Reh
b5817b8d4a Use built-in input update event
This also solves the annoying console error after completion, which was due to missing the target property
See https://github.com/gradio-app/gradio/issues/2981 for details
2023-02-21 18:25:17 +01:00
Dominik Reh
6269c40580 Custom key bindings
Closes #127
2023-02-18 13:25:13 +01:00
DominikDoom
c1fb4619a4 Update README_ZH.md 2023-02-11 16:01:36 +01:00
DominikDoom
a7c5c38c26 Update README.md 2023-02-11 15:52:27 +01:00
DominikDoom
c14260c1fe Rename Extra-quality-tags.csv to extra-quality-tags.csv 2023-02-11 15:38:01 +01:00
20 changed files with 1100 additions and 326 deletions

144
README.md
View File

@@ -15,11 +15,12 @@ You can install it using the inbuilt available extensions list, clone the files
## Common Problems & Known Issues:
- Depending on your browser settings, sometimes an old version of the script can get cached. Try `CTRL+F5` to force-reload the site without cache if e.g. a new feature doesn't appear for you after an update.
- If `replaceUnderscores` is active, the script will currently only partially replace edited tags containing multiple words in brackets.
For example, editing `atago (azur lane)`, it would be replaced with e.g. `taihou (azur lane), lane)`, since the script currently doesn't see the second part of the bracket as the same tag. So in those cases you should delete the old tag beforehand.
- If the prompt popup has broken styling for you or doesn't appear at all (like [this](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/7bbfdd54-fc23-4bfc-85af-24704b139b3a)), make sure to update your openpose-editor extension if you have it installed. It is known to cause issues with other extensions in older versions.
## Screenshots
Demo video (with keyboard navigation):
## Screenshots & Demo videos
<details>
<summary>Click to expand</summary>
Basic usage (with keyboard navigation):
https://user-images.githubusercontent.com/34448969/200128020-10d9a8b2-cea6-4e3f-bcd2-8c40c8c73233.mp4
@@ -31,32 +32,101 @@ Dark and Light mode supported, including tag colors:
![results_dark](https://user-images.githubusercontent.com/34448969/200128214-3b6f21b4-9dda-4acf-820e-5df0285c30d6.png)
![results_light](https://user-images.githubusercontent.com/34448969/200128217-bfac8b60-6673-447b-90fd-dc6326f1618c.png)
</details>
## Installation
### As an extension (recommended)
Either clone the repo into your extensions folder:
### Using the built-in extension list
1. Open the Extensions tab
2. Open the Available sub-tab
3. Click "Load from:"
4. Find "Booru tag autocompletion" in the list
- The extension was one of the first available, so selecting "oldest first" will show it high up in the list.
5. Click "Install" on the right side
![Load from index](https://user-images.githubusercontent.com/34448969/223537209-24c7623e-7410-427e-857f-9da936aadb21.png)
![Order by oldest](https://user-images.githubusercontent.com/34448969/223537231-48e982b8-0920-48c5-87e5-8c81ebbb5fe3.png)
![Install](https://user-images.githubusercontent.com/34448969/223537336-5c02ccb1-233d-4e0d-9e73-d1b889252c49.png)
### Manual clone
```bash
git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extensions/tag-autocomplete
```
(The second argument specifies the name of the folder, you can choose whatever you like).
Or create a folder there manually and place the `javascript`, `scripts` and `tags` folders in it.
## Additional completion support
### Wildcards
Autocompletion also works with wildcard files used by https://github.com/AUTOMATIC1111/stable-diffusion-webui-wildcards or other similar scripts/extensions.
Completion is triggered by typing `__` (double underscore). It will first show a list of your wildcard files, and upon choosing one, the replacement options inside that file.
This enables you to either insert categories to be replaced by the script, or directly choose one and use wildcards as a sort of categorized custom tag system.
### In the root folder (legacy)
This installation method is for old webui versions pre-extension system, it will not work on current versions!
![Wildcard files](https://user-images.githubusercontent.com/34448969/223534518-8488c2e1-d9e5-4870-844f-adbf3bfb1eee.png)
![Wildcard replacements](https://user-images.githubusercontent.com/34448969/223534534-69597907-59de-4ba8-ae83-b01386570124.png)
---
In both configurations, the `tags` folder contains `colors.json` and the tag data the script uses for autocompletion. By default, Danbooru and e621 tags are included.
After scanning for embeddings and wildcards, the script will also create a `temp` directory here which lists the found files so they can be accessed in the browser side of the script. You can delete the temp folder without consequences as it will be recreated on the next startup.
Wildcards are searched for in every extension folder, as well as the `scripts/wildcards` folder to support legacy versions. This means that you can combine wildcards from multiple extensions. Nested folders are also supported if you have grouped your wildcards in that way.
### Important:
The script needs **all three folders** to work properly.
### Embeddings, Lora & Hypernets
Completion for these three types is triggered by typing `<`. By default it will show all three mixed together, but further filtering can be done in the following way:
- `<e:` will only show embeddings
- `<l:` or `<lora:` will only show Lora
- `<h:` or `<hypernet:` will only show Hypernetworks
## Wildcard & Embedding support
Autocompletion also works with wildcard files used by [this script](https://github.com/jtkelm2/stable-diffusion-webui-1/blob/master/scripts/wildcards.py) of the same name or other similar scripts/extensions. This enables you to either insert categories to be replaced by the script, or even replace them with the actual wildcard file content in the same step. Wildcards are searched for in every extension folder as well as the `scripts/wildcards` folder to support legacy versions. This means that you can combine wildcards from multiple extensions. Nested folders are also supported if you have grouped your wildcards in that way.
#### Embedding type filtering
Embeddings trained for Stable Diffusion 1.x or 2.x models respectively are incompatible with the other type. To make it easier to find valid embeds, they are categorized by "v1 Embedding" and "v2 Embedding", including a slight color difference. You can also filter your search to include only v1 or v2 embeddings by typing `<v1/2` or `<e:v1/2` followed by the actual search term.
It also scans the embeddings folder and displays completion hints for the names of all .pt, .bin and .png files inside if you start typing `<`. Note that some normal tags also use < in Kaomoji (like ">_<" for example), so the results will contain both.
For example:
![Embedding version filter](https://user-images.githubusercontent.com/34448969/223533883-d99c04b7-a199-4f56-a4e5-242eee9726a2.png)
### Chants
Chants are longer prompt presets. The name is inspired by some early prompt collections from Chinese users, which often were called along the lines of "Spellbook", "Codex", etc. The prompt snippets from such documents were fittingly called spells or chants for this reason.
Similar to embeddings and loras, this feature is triggered by typing the `<`, `<c:` or `<chant:` commands. For instance, when you enter `<c:HighQuality` in the prompt box and select it, the following prompt text will be inserted:
```
(masterpiece, best quality, high quality, highres, ultra-detailed),
```
Chants can be added in JSON files following this format:
```json
[
{
"name": "Basic-NegativePrompt",
"terms": "Negative,Low,Quality",
"content": "(worst quality, low quality, normal quality)",
"color": 3
},
{
"name": "Basic-HighQuality",
"terms": "Best,High,Quality",
"content": "(masterpiece, best quality, high quality, highres, ultra-detailed)",
"color": 1
},
{
"name": "Basic-Start",
"terms": "Basic, Start, Simple, Demo",
"content": "(masterpiece, best quality, high quality, highres), 1girl, extremely beautiful detailed face, ...",
"color": 5
}
]
```
The file can then be selected using the "Chant file" settings dropdown if it is located inside the extension's `tags` folder.
A chant object has four fields:
- `name` - Display name
- `terms` - Search terms
- `content` - The actual prompt content
- `color` - Color, using the same category color system as normal tags
### Umi AI tags
https://github.com/Klokinator/Umi-AI is a feature-rich wildcard extension similar to Unprompted or Dynamic Wildcards.
In recent releases, it uses YAML-based wildcard tags to enable a complex chaining system,for example `<[preset][--female][sfw][species]>` will choose the preset category, exclude female related tags, further narrow it down with the following categories, and then choose one random fill-in matching all these criteria at runtime. Completion is triggered by `<[` and then each following new unclosed bracket, e.g. `<[xyz][`, until closed by `>`.
Tag Autocomplete can recommend these options in a smart way, meaning while you continue to add category tags, it will only show results still matching what comes before.
It also shows how many fill-in tags are available to choose from for that combo in place of the tag post count, enabling a quick overview and filtering of the large initial set.
Most of the credit goes to [@ctwrs](https://github.com/ctwrs) here, they contributed a lot as one of the Umi developers.
## Settings
@@ -79,11 +149,14 @@ The extension has a large amount of configuration & customizability built in:
| useEmbeddings | Used to toggle the embedding completion functionality. |
| alias | Options for aliases. More info in the section below. |
| translation | Options for translations. More info in the section below. |
| extras | Options for additional tag files / aliases / translations. More info in the section below. |
### colors.json
Additionally, tag type colors can be specified using the separate `colors.json` file in the extension's `tags` folder.
You can also add new ones here for custom tag files (same name as filename, without the .csv). The first value is for dark, the second for light mode. Color names and hex codes should both work.
| extras | Options for additional tag files / aliases / translations. More info below. |
| chantFile | The file to use for chants (longer prompt presets / shortcuts). |
| keymap | Customizable hotkeys. |
| colors | Customizable tag colors. More info below. |
### Colors
Tag type colors can be specified by changing the JSON code in the tag autocomplete settings.
The format is standard JSON, with the object names corresponding to the tag filenames (without the .csv) they should be used for.
The first value in the square brackets is for dark, the second for light mode. Color names and hex codes should both work.
```json
{
"danbooru": {
@@ -107,6 +180,7 @@ You can also add new ones here for custom tag files (same name as filename, with
}
}
```
This can also be used to add new color sets for custom tag files.
The numbers are specifying the tag type, which is dependent on the tag source. For an example, see [CSV tag data](#csv-tag-data).
### Aliases, Translations & Extra tags
@@ -117,34 +191,24 @@ Like on Booru sites, tags can have one or multiple aliases which redirect to the
#### Translations
An additional file can be added in the translation section, which will be used to translate both tags and aliases and also enables searching by translation.
This file needs to be a CSV in the format `<English tag/alias>,<Translation>`, but for backwards compatibility with older extra files that used a three column format, you can turn on `oldFormat` to use that instead.
This file needs to be a CSV in the format `<English tag/alias>,<Translation>`, but for backwards compatibility with older files that used a three column format, you can turn on `oldFormat` to use that instead.
Example with chinese translation:
![IME-input](https://user-images.githubusercontent.com/34448969/200126551-2264e9cc-abb2-4450-9afa-43f362a77ab0.png)
![english-input](https://user-images.githubusercontent.com/34448969/200126513-bf6b3940-6e22-41b0-a369-f2b4640f87d6.png)
**Important**
As of a recent update, translations added in the old Extra file way will only work as an alias and not be visible anymore if typing the English tag for that translation.
#### Extra file
Aliases can be added in multiple ways, which is where the "Extra" file comes into play.
1. As an extra file containing tag, category, optional count and the new alias. Will be matched to the English tags in the main file based on the name & type, so might be slow for large files.
2. As an extra file with `onlyAliasExtraFile` true. With this configuration, the extra file has to include *only* the alias itself. That means it is purely index based, assigning the aliases to the main tags is really fast but also needs the lines to match (including empty lines). If the order or amount in the main file changes, the translations will potentially not match anymore. Not recommended.
An extra file can be used to add new / custom tags not included in the main set.
The format is identical to the normal tag format shown in [CSV tag data](#csv-tag-data) below, with one exception:
Since custom tags likely have no count, column three (or two if counting from zero) is instead used for the gray meta text displayed next to the tag.
If left empty, it will instead show "Custom tag".
So your CSV values would look like this for each method:
| | 1 | 2 |
|------------|--------------------------|--------------------------|
| Main file | `tag,type,count,(alias)` | `tag,type,count,(alias)` |
| Extra file | `tag,type,(count),alias` | `alias` |
An example with the included (very basic) extra-quality-tags.csv file:
Count in the extra file is optional, since there isn't always a post count for custom tag sets.
![image](https://user-images.githubusercontent.com/34448969/218264276-cd77ba8e-62d8-41a2-b03c-6c04887ee18b.png)
The extra files can also be used to just add new / custom tags not included in the main set, provided `onlyAliasExtraFile` is false.
If an extra tag doesn't match any existing tag, it will be added to the list as a new tag instead. For this, it will need to include the post count and alias columns even if they don't contain anything, so it could be in the form of `tag,type,,`.
##### WARNING
Do not use e621.csv or danbooru.csv as an extra file. Alias comparison has exponential runtime, so for the combination of danbooru+e621, it will need to do 10,000,000,000 (yes, ten billion) lookups and usually take multiple minutes to load.
Whether the custom tags should be added before or after the normal tags can be chosen in the settings.
## CSV tag data
The script expects a CSV file with tags saved in the following way:

View File

@@ -81,9 +81,10 @@ git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extens
| translation | 用于翻译标签的选项。更多信息在下面的部分。 |
| extras | 附加标签文件/翻译的选项。更多信息在下面的部分。|
### colors.json (标签颜色)
此外,标签类型的颜色可以使用扩展的`tags`文件夹中单独的`colors.json`文件来指定。
你也可以在这里为自定义标签文件添加新的(与文件名相同,不带 .csv。第一个值是暗模式第二个值是亮模式。颜色名称和十六进制代码都被支持。
### 标签颜色
标签类型的颜色可以通过改变标签自动完成设置中的JSON代码来指定。格式是标准的JSON对象名称对应于它们应该使用的标签文件名没有.csv
方括号中的第一个值是指深色,第二个是指浅色模式。颜色名称和十六进制代码都应该有效。
```json
{
"danbooru": {
@@ -107,7 +108,7 @@ git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extens
}
}
```
数字是指定标签的类型,这取决于标签来源。例如,见[CSV tag data](#csv-tag-data)。
这也可以用来为自定义标签文件添加新的颜色集。数字是指定标签的类型,这取决于标签来源。关于例子,见[CSV tag data](#csv-tag-data)。
### 别名,翻译&新增Tag
#### 别名
@@ -124,24 +125,20 @@ git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extens
![IME-input](https://user-images.githubusercontent.com/34448969/200126551-2264e9cc-abb2-4450-9afa-43f362a77ab0.png)
![english-input](https://user-images.githubusercontent.com/34448969/200126513-bf6b3940-6e22-41b0-a369-f2b4640f87d6.png)
**重要的是**
从最近的更新来看用旧的Extra文件方式添加的翻译只能作为一个别名使用如果输入该翻译的英文标签将不再可见
#### Extra文件
额外文件可以用来添加未包含在主集中的新的/自定义标签。
其格式与下面 [CSV tag data](#csv-tag-data) 中的正常标签格式相同,但有一个例外。
由于自定义标签没有帖子计数,第三列(如果从零开始计算,则为第二列)用于显示标签旁边的灰色元文本。
如果留空,它将显示 "Custom tag"。
可以通过多种方式添加别名,这就是额外文件发挥作用的地方。
1. 作为仅包含已翻译标签行的额外文件(因此仍包括英文标签名称和标签类型)。将根据名称和类型与主文件中的英文标签匹配,因此对于大型翻译文件可能会很慢。
2. 作为 `onlyAliasExtraFile` 为 true 的额外文件。使用此配置,额外文件必须包含*仅*翻译本身。这意味着它完全基于索引,将翻译分配给主要标签非常快,但也需要匹配行(包括空行)。如果主文件中的顺序或数量发生变化,则翻译可能不再匹配。
以默认的非常基本的extra-quality-tags.csv为例
因此,对于每种方法,您的 CSV 值将如下所示:
| | 1 | 2 |
|------------|--------------------------|--------------------------|
| Main file | `tag,type,count,(alias)` | `tag,type,count,(alias)` |
| Extra file | `tag,type,(count),alias` | `alias` |
![image](https://user-images.githubusercontent.com/34448969/218264276-cd77ba8e-62d8-41a2-b03c-6c04887ee18b.png)
额外文件中的计数是可选的,因为自定义标签集并不总是有帖子计数
如果额外的标签与任何现有标签都不匹配,它将作为新标签添加到列表中。
你可以在设置中选择自定义标签是否应该加在常规标签之前或之后
### CSV tag data
本脚本的Tag文件格式如下你可以安装这个格式制作自己的Tag文件:
本脚本的Tag文件格式如下你可以安装这个格式制作自己的Tag文件
```csv
1girl,0,4114588,"1girls,sole_female"
solo,0,3426446,"female_solo,solo_female"
@@ -151,7 +148,7 @@ commentary_request,5,2610959,
```
值得注意的是不希望在第一行有列名而且count和aliases在技术上都是可选的。
尽管count总是包含在默认数据中。多个别名也需要用逗号分隔但要用字符串引号包裹以免破坏CSV解析。
编号系统遵循 Danbooru 的 [tag API docs](https://danbooru.donmai.us/wiki_pages/api%3Atags):
编号系统遵循 Danbooru 的 [tag API docs](https://danbooru.donmai.us/wiki_pages/api%3Atags)
| Value | Description |
|-------|-------------|
|0 | General |

View File

@@ -1,5 +1,5 @@
// Core components
var CFG = null;
var TAC_CFG = null;
var tagBasePath = "";
// Tag completion data loaded from files
@@ -13,6 +13,8 @@ var yamlWildcards = [];
var embeddings = [];
var hypernetworks = [];
var loras = [];
var lycos = [];
var chants = [];
// Selected model info for black/whitelisting
var currentModelHash = "";

View File

@@ -0,0 +1,145 @@
// From https://github.com/component/textarea-caret-position
// We'll copy the properties below into the mirror div.
// Note that some browsers, such as Firefox, do not concatenate properties
// into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
// so we have to list every single property explicitly.
var properties = [
'direction', // RTL support
'boxSizing',
'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
'height',
'overflowX',
'overflowY', // copy the scrollbar for IE
'borderTopWidth',
'borderRightWidth',
'borderBottomWidth',
'borderLeftWidth',
'borderStyle',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
'fontStyle',
'fontVariant',
'fontWeight',
'fontStretch',
'fontSize',
'fontSizeAdjust',
'lineHeight',
'fontFamily',
'textAlign',
'textTransform',
'textIndent',
'textDecoration', // might not make a difference, but better be safe
'letterSpacing',
'wordSpacing',
'tabSize',
'MozTabSize'
];
var isBrowser = (typeof window !== 'undefined');
var isFirefox = (isBrowser && window.mozInnerScreenX != null);
function getCaretCoordinates(element, position, options) {
if (!isBrowser) {
throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
}
var debug = options && options.debug || false;
if (debug) {
var el = document.querySelector('#input-textarea-caret-position-mirror-div');
if (el) el.parentNode.removeChild(el);
}
// The mirror div will replicate the textarea's style
var div = document.createElement('div');
div.id = 'input-textarea-caret-position-mirror-div';
document.body.appendChild(div);
var style = div.style;
var computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
var isInput = element.nodeName === 'INPUT';
// Default textarea styles
style.whiteSpace = 'pre-wrap';
if (!isInput)
style.wordWrap = 'break-word'; // only for textarea-s
// Position off-screen
style.position = 'absolute'; // required to return coordinates properly
if (!debug)
style.visibility = 'hidden'; // not 'display: none' because we want rendering
// Transfer the element's properties to the div
properties.forEach(function (prop) {
if (isInput && prop === 'lineHeight') {
// Special case for <input>s because text is rendered centered and line height may be != height
if (computed.boxSizing === "border-box") {
var height = parseInt(computed.height);
var outerHeight =
parseInt(computed.paddingTop) +
parseInt(computed.paddingBottom) +
parseInt(computed.borderTopWidth) +
parseInt(computed.borderBottomWidth);
var targetHeight = outerHeight + parseInt(computed.lineHeight);
if (height > targetHeight) {
style.lineHeight = height - outerHeight + "px";
} else if (height === targetHeight) {
style.lineHeight = computed.lineHeight;
} else {
style.lineHeight = 0;
}
} else {
style.lineHeight = computed.height;
}
} else {
style[prop] = computed[prop];
}
});
if (isFirefox) {
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
if (element.scrollHeight > parseInt(computed.height))
style.overflowY = 'scroll';
} else {
style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
}
div.textContent = element.value.substring(0, position);
// The second special handling for input type="text" vs textarea:
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
if (isInput)
div.textContent = div.textContent.replace(/\s/g, '\u00a0');
var span = document.createElement('span');
// Wrapping must be replicated *exactly*, including when a long word gets
// onto the next line, with whitespace at the end of the line before (#7).
// The *only* reliable way to do that is to copy the *entire* rest of the
// textarea's content into the <span> created at the caret position.
// For inputs, just '.' would be enough, but no need to bother.
span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
div.appendChild(span);
var coordinates = {
top: span.offsetTop + parseInt(computed['borderTopWidth']),
left: span.offsetLeft + parseInt(computed['borderLeftWidth']),
height: parseInt(computed['lineHeight'])
};
if (debug) {
span.style.backgroundColor = '#aaa';
} else {
document.body.removeChild(div);
}
return coordinates;
}

View File

@@ -9,7 +9,9 @@ const ResultType = Object.freeze({
"wildcardFile": 5,
"yamlWildcard": 6,
"hypernetwork": 7,
"lora": 8
"lora": 8,
"lyco": 9,
"chant": 10
});
// Class to hold result data and annotations to make it clearer to use

View File

@@ -8,7 +8,8 @@ const core = [
"#txt2img_prompt > label > textarea",
"#img2img_prompt > label > textarea",
"#txt2img_neg_prompt > label > textarea",
"#img2img_neg_prompt > label > textarea"
"#img2img_neg_prompt > label > textarea",
".prompt > label > textarea"
];
// Third party text area selectors
@@ -22,24 +23,58 @@ const thirdParty = {
"Edit Caption",
"Edit Tags"
]
},
"image browser": {
"base": "#tab_image_browser",
"hasIds": false,
"selectors": [
"Filename keyword search",
"EXIF keyword search"
]
},
"tab_tagger": {
"base": "#tab_tagger",
"hasIds": false,
"selectors": [
"Additional tags (split by comma)",
"Exclude tags (split by comma)"
]
},
"tiled-diffusion-t2i": {
"base": "#txt2img_script_container",
"hasIds": true,
"onDemand": true,
"selectors": [
"[id^=MD-t2i][id$=prompt] textarea",
"[id^=MD-t2i][id$=prompt] input[type='text']"
]
},
"tiled-diffusion-i2i": {
"base": "#img2img_script_container",
"hasIds": true,
"onDemand": true,
"selectors": [
"[id^=MD-i2i][id$=prompt] textarea",
"[id^=MD-i2i][id$=prompt] input[type='text']"
]
}
}
function getTextAreas() {
// First get all core text areas
let textAreas = [...gradioApp().querySelectorAll(core.join(", "))];
for (const [key, entry] of Object.entries(thirdParty)) {
if (entry.hasIds) { // If the entry has proper ids, we can just select them
textAreas = textAreas.concat([...gradioApp().querySelectorAll(entry.selectors.join(", "))]);
} else { // Otherwise, we have to find the text areas by their adjacent labels
let base = gradioApp().querySelector(entry.base);
// Safety check
if (!base) continue;
let allTextAreas = [...base.querySelectorAll("textarea")];
let allTextAreas = [...base.querySelectorAll("textarea, input[type='text']")];
// Filter the text areas where the adjacent label matches one of the selectors
let matchingTextAreas = allTextAreas.filter(ta => [...ta.parentElement.childNodes].some(x => entry.selectors.includes(x.innerText)));
textAreas = textAreas.concat(matchingTextAreas);
@@ -49,6 +84,55 @@ function getTextAreas() {
return textAreas;
}
function addOnDemandObservers(setupFunction) {
for (const [key, entry] of Object.entries(thirdParty)) {
if (!entry.onDemand) continue;
let base = gradioApp().querySelector(entry.base);
if (!base) continue;
let accordions = [...base?.querySelectorAll(".gradio-accordion")];
if (!accordions) continue;
accordions.forEach(acc => {
let accObserver = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === "childList") {
let newChildren = mutation.addedNodes;
if (!newChildren) {
accObserver.disconnect();
continue;
}
newChildren.forEach(child => {
if (child.classList.contains("gradio-accordion") || child.querySelector(".gradio-accordion")) {
let newAccordions = [...child.querySelectorAll(".gradio-accordion")];
newAccordions.forEach(nAcc => accObserver.observe(nAcc, { childList: true }));
}
});
if (entry.hasIds) { // If the entry has proper ids, we can just select them
[...gradioApp().querySelectorAll(entry.selectors.join(", "))].forEach(x => setupFunction(x));
} else { // Otherwise, we have to find the text areas by their adjacent labels
let base = gradioApp().querySelector(entry.base);
// Safety check
if (!base) continue;
let allTextAreas = [...base.querySelectorAll("textarea, input[type='text']")];
// Filter the text areas where the adjacent label matches one of the selectors
let matchingTextAreas = allTextAreas.filter(ta => [...ta.parentElement.childNodes].some(x => entry.selectors.includes(x.innerText)));
matchingTextAreas.forEach(x => setupFunction(x));
}
}
}
});
accObserver.observe(acc, { childList: true });
});
};
}
const thirdPartyIdSet = new Set();
// Get the identifier for the text area to differentiate between positive and negative
function getTextAreaIdentifier(textArea) {

View File

@@ -89,6 +89,14 @@ function difference(a, b) {
)].reduce((acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), []);
}
// Sliding window function to get possible combination groups of an array
function toNgrams(inputArray, size) {
return Array.from(
{ length: inputArray.length - (size - 1) }, //get the appropriate length
(_, index) => inputArray.slice(index, index + size) //create the windows
);
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
@@ -98,6 +106,41 @@ function escapeHTML(unsafeText) {
return div.innerHTML;
}
// For black/whitelisting
function updateModelName() {
let sdm = gradioApp().querySelector("#setting_sd_model_checkpoint");
let modelDropdown = sdm?.querySelector("input") || sdm?.querySelector("select");
if (modelDropdown) {
currentModelName = modelDropdown.value;
} else {
// Fallback for intermediate versions
modelDropdown = sdm?.querySelector("span.single-select");
currentModelName = modelDropdown?.textContent || "";
}
}
// From https://stackoverflow.com/a/61975440, how to detect JS value changes
function observeElement(element, property, callback, delay = 0) {
let elementPrototype = Object.getPrototypeOf(element);
if (elementPrototype.hasOwnProperty(property)) {
let descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
Object.defineProperty(element, property, {
get: function() {
return descriptor.get.apply(this, arguments);
},
set: function () {
let oldValue = this[property];
descriptor.set.apply(this, arguments);
let newValue = this[property];
if (typeof callback == "function") {
setTimeout(callback.bind(this, oldValue, newValue), delay);
}
return newValue;
}
});
}
}
// Queue calling function to process global queues
async function processQueue(queue, context, ...args) {
for (let i = 0; i < queue.length; i++) {

54
javascript/ext_chants.js Normal file
View File

@@ -0,0 +1,54 @@
const CHANT_REGEX = /<(?!e:|h:|l:)[^,> ]*>?/g;
const CHANT_TRIGGER = () => TAC_CFG.chantFile && TAC_CFG.chantFile !== "None" && tagword.match(CHANT_REGEX);
class ChantParser extends BaseTagParser {
parse() {
// Show Chant
let tempResults = [];
if (tagword !== "<" && tagword !== "<c:") {
let searchTerm = tagword.replace("<chant:", "").replace("<c:", "").replace("<", "");
let filterCondition = x => x.terms.toLowerCase().includes(searchTerm) || x.name.toLowerCase().includes(searchTerm);
tempResults = chants.filter(x => filterCondition(x)); // Filter by tagword
} else {
tempResults = chants;
}
// Add final results
let finalResults = [];
tempResults.forEach(t => {
let result = new AutocompleteResult(t.content.trim(), ResultType.chant)
result.meta = "Chant";
result.aliases = t.name;
result.category = t.color;
finalResults.push(result);
});
return finalResults;
}
}
async function load() {
if (TAC_CFG.chantFile && TAC_CFG.chantFile !== "None") {
try {
chants = await readFile(`${tagBasePath}/${TAC_CFG.chantFile}?`, true);
} catch (e) {
console.error("Error loading chants.json: " + e);
}
} else {
chants = [];
}
}
function sanitize(tagType, text) {
if (tagType === ResultType.chant) {
return text.replace(/^.*?: /g, "");
}
return null;
}
PARSERS.push(new ChantParser(CHANT_TRIGGER));
// Add our utility functions to their respective queues
QUEUE_FILE_LOAD.push(load);
QUEUE_SANITIZE.push(sanitize);
QUEUE_AFTER_CONFIG_CHANGE.push(load);

View File

@@ -1,5 +1,5 @@
const EMB_REGEX = /<(?!l:|h:)[^,> ]*>?/g;
const EMB_TRIGGER = () => CFG.useEmbeddings && tagword.match(EMB_REGEX);
const EMB_REGEX = /<(?!l:|h:|c:)[^,> ]*>?/g;
const EMB_TRIGGER = () => TAC_CFG.useEmbeddings && tagword.match(EMB_REGEX);
class EmbeddingParser extends BaseTagParser {
parse() {
@@ -12,10 +12,13 @@ class EmbeddingParser extends BaseTagParser {
versionString = searchTerm.slice(0, 2);
searchTerm = searchTerm.slice(2);
}
let filterCondition = x => x[0].toLowerCase().includes(searchTerm) || x[0].toLowerCase().replaceAll(" ", "_").includes(searchTerm);
if (versionString)
tempResults = embeddings.filter(x => x[0].toLowerCase().includes(searchTerm) && x[1] && x[1] === versionString); // Filter by tagword
tempResults = embeddings.filter(x => filterCondition(x) && x[1] && x[1] === versionString); // Filter by tagword
else
tempResults = embeddings.filter(x => x[0].toLowerCase().includes(searchTerm)); // Filter by tagword
tempResults = embeddings.filter(x => filterCondition(x)); // Filter by tagword
} else {
tempResults = embeddings;
}

View File

@@ -1,13 +1,14 @@
const HYP_REGEX = /<(?!e:|l:)[^,> ]*>?/g;
const HYP_TRIGGER = () => CFG.useHypernetworks && tagword.match(HYP_REGEX);
const HYP_REGEX = /<(?!e:|l:|c:)[^,> ]*>?/g;
const HYP_TRIGGER = () => TAC_CFG.useHypernetworks && tagword.match(HYP_REGEX);
class HypernetParser extends BaseTagParser {
parse() {
// Show hypernetworks
let tempResults = [];
if (tagword !== "<" && tagword !== "<h:") {
let searchTerm = tagword.replace("<h:", "").replace("<", "");
tempResults = hypernetworks.filter(x => x.toLowerCase().includes(searchTerm)); // Filter by tagword
if (tagword !== "<" && tagword !== "<h:" && tagword !== "<hypernet:") {
let searchTerm = tagword.replace("<hypernet:", "").replace("<h:", "").replace("<", "");
let filterCondition = x => x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm);
tempResults = hypernetworks.filter(x => filterCondition(x)); // Filter by tagword
} else {
tempResults = hypernetworks;
}
@@ -38,7 +39,7 @@ async function load() {
function sanitize(tagType, text) {
if (tagType === ResultType.hypernetwork) {
return `<hypernet:${text}:${CFG.extraNetworksDefaultMultiplier}>`;
return `<hypernet:${text}:${TAC_CFG.extraNetworksDefaultMultiplier}>`;
}
return null;
}

View File

@@ -1,13 +1,14 @@
const LORA_REGEX = /<(?!e:|h:)[^,> ]*>?/g;
const LORA_TRIGGER = () => CFG.useLoras && tagword.match(LORA_REGEX);
const LORA_REGEX = /<(?!e:|h:|c:)[^,> ]*>?/g;
const LORA_TRIGGER = () => TAC_CFG.useLoras && tagword.match(LORA_REGEX);
class LoraParser extends BaseTagParser {
parse() {
// Show lora
let tempResults = [];
if (tagword !== "<" && tagword !== "<l:") {
let searchTerm = tagword.replace("<l:", "").replace("<", "");
tempResults = loras.filter(x => x.toLowerCase().includes(searchTerm)); // Filter by tagword
if (tagword !== "<" && tagword !== "<l:" && tagword !== "<lora:") {
let searchTerm = tagword.replace("<lora:", "").replace("<l:", "").replace("<", "");
let filterCondition = x => x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm);
tempResults = loras.filter(x => filterCondition(x)); // Filter by tagword
} else {
tempResults = loras;
}
@@ -38,7 +39,7 @@ async function load() {
function sanitize(tagType, text) {
if (tagType === ResultType.lora) {
return `<lora:${text}:${CFG.extraNetworksDefaultMultiplier}>`;
return `<lora:${text}:${TAC_CFG.extraNetworksDefaultMultiplier}>`;
}
return null;
}

51
javascript/ext_lycos.js Normal file
View File

@@ -0,0 +1,51 @@
const LYCO_REGEX = /<(?!e:|h:|c:)[^,> ]*>?/g;
const LYCO_TRIGGER = () => TAC_CFG.useLycos && tagword.match(LYCO_REGEX);
class LycoParser extends BaseTagParser {
parse() {
// Show lyco
let tempResults = [];
if (tagword !== "<" && tagword !== "<l:" && tagword !== "<lyco:") {
let searchTerm = tagword.replace("<lyco:", "").replace("<l:", "").replace("<", "");
let filterCondition = x => x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm);
tempResults = lycos.filter(x => filterCondition(x)); // Filter by tagword
} else {
tempResults = lycos;
}
// Add final results
let finalResults = [];
tempResults.forEach(t => {
let result = new AutocompleteResult(t.trim(), ResultType.lyco)
result.meta = "Lyco";
finalResults.push(result);
});
return finalResults;
}
}
async function load() {
if (lycos.length === 0) {
try {
lycos = (await readFile(`${tagBasePath}/temp/lyco.txt`)).split("\n")
.filter(x => x.trim().length > 0) // Remove empty lines
.map(x => x.trim()); // Remove carriage returns and padding if it exists
} catch (e) {
console.error("Error loading lyco.txt: " + e);
}
}
}
function sanitize(tagType, text) {
if (tagType === ResultType.lyco) {
return `<lyco:${text}:${TAC_CFG.extraNetworksDefaultMultiplier}>`;
}
return null;
}
PARSERS.push(new LycoParser(LYCO_TRIGGER));
// Add our utility functions to their respective queues
QUEUE_FILE_LOAD.push(load);
QUEUE_SANITIZE.push(sanitize);

View File

@@ -1,7 +1,7 @@
const UMI_PROMPT_REGEX = /<[^\s]*?\[[^,<>]*[\]|]?>?/gi;
const UMI_TAG_REGEX = /(?:\[|\||--)([^<>\[\]\-|]+)/gi;
const UMI_TRIGGER = () => CFG.useWildcards && [...tagword.matchAll(UMI_PROMPT_REGEX)].length > 0;
const UMI_TRIGGER = () => TAC_CFG.useWildcards && [...tagword.matchAll(UMI_PROMPT_REGEX)].length > 0;
class UmiParser extends BaseTagParser {
parse(textArea, prompt) {

View File

@@ -2,8 +2,8 @@
const WC_REGEX = /\b__([^,]+)__([^, ]*)\b/g;
// Trigger conditions
const WC_TRIGGER = () => CFG.useWildcards && [...tagword.matchAll(WC_REGEX)].length > 0;
const WC_FILE_TRIGGER = () => CFG.useWildcards && (tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__");
const WC_TRIGGER = () => TAC_CFG.useWildcards && [...tagword.matchAll(WC_REGEX)].length > 0;
const WC_FILE_TRIGGER = () => TAC_CFG.useWildcards && (tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__");
class WildcardParser extends BaseTagParser {
async parse() {

View File

@@ -1,4 +1,4 @@
const styleColors = {
const styleColors = {
"--results-bg": ["#0b0f19", "#ffffff"],
"--results-border-color": ["#4b5563", "#e5e7eb"],
"--results-border-width": ["1px", "1.5px"],
@@ -8,6 +8,10 @@ const styleColors = {
"--meta-text-color": ["#6b6f7b", "#a2a9b4"],
"--embedding-v1-color": ["lightsteelblue", "#2b5797"],
"--embedding-v2-color": ["skyblue", "#2d89ef"],
"--live-translation-rt": ["whitesmoke", "#222"],
"--live-translation-color-1": ["lightskyblue", "#2d89ef"],
"--live-translation-color-2": ["palegoldenrod", "#eb5700"],
"--live-translation-color-3": ["darkseagreen", "darkgreen"],
}
const browserVars = {
"--results-overflow-y": {
@@ -20,15 +24,6 @@ const autocompleteCSS = `
#quicksettings [id^=setting_tac] {
background-color: transparent;
min-width: fit-content;
align-self: center;
}
#quicksettings [id^=setting_tac] > label > span {
margin-bottom: 0px;
}
[id^=refresh_tac] {
max-width: 2.5em;
min-width: 2.5em;
height: 2.4em;
}
.autocompleteResults {
position: absolute;
@@ -83,6 +78,39 @@ const autocompleteCSS = `
.acListItem.acEmbeddingV2 {
color: var(--embedding-v2-color);
}
.acRuby {
padding: var(--input-padding);
color: #888;
font-size: 0.8rem;
user-select: none;
}
.acRuby > ruby {
display: inline-flex;
flex-direction: column-reverse;
margin-top: 0.5rem;
vertical-align: bottom;
cursor: pointer;
}
.acRuby > ruby::hover {
text-decoration: underline;
text-shadow: 0 0 10px var(--live-translation-color-1);
}
.acRuby > :nth-child(3n+1) {
color: var(--live-translation-color-1);
}
.acRuby > :nth-child(3n+2) {
color: var(--live-translation-color-2);
}
.acRuby > :nth-child(3n+3) {
color: var(--live-translation-color-3);
}
.acRuby > ruby > rt {
line-height: 1rem;
padding: 0px 5px 0px 0px;
text-align: left;
font-size: 1rem;
color: var(--live-translation-rt);
}
`;
async function loadTags(c) {
@@ -95,9 +123,17 @@ async function loadTags(c) {
return;
}
}
await loadExtraTags(c);
}
async function loadExtraTags(c) {
if (c.extra.extraFile && c.extra.extraFile !== "None") {
try {
extras = await loadCSV(`${tagBasePath}/${c.extra.extraFile}`);
// Add translations to the main translation map for extra tags that have them
extras.forEach(e => {
if (e[4]) translations.set(e[0], e[4]);
});
} catch (e) {
console.error("Error loading extra file: " + e);
return;
@@ -137,6 +173,7 @@ async function syncOptions() {
modelListMode: opts["tac_activeIn.modelListMode"]
},
// Results related settings
slidingPopup: opts["tac_slidingPopup"],
maxResults: opts["tac_maxResults"],
showAllResults: opts["tac_showAllResults"],
resultStepLength: opts["tac_resultStepLength"],
@@ -145,6 +182,7 @@ async function syncOptions() {
useEmbeddings: opts["tac_useEmbeddings"],
useHypernetworks: opts["tac_useHypernetworks"],
useLoras: opts["tac_useLoras"],
useLycos: opts["tac_useLycos"],
showWikiLinks: opts["tac_showWikiLinks"],
// Insertion related settings
replaceUnderscores: opts["tac_replaceUnderscores"],
@@ -160,43 +198,55 @@ async function syncOptions() {
translationFile: opts["tac_translation.translationFile"],
oldFormat: opts["tac_translation.oldFormat"],
searchByTranslation: opts["tac_translation.searchByTranslation"],
liveTranslation: opts["tac_translation.liveTranslation"],
},
// Extra file settings
extra: {
extraFile: opts["tac_extra.extraFile"],
addMode: opts["tac_extra.addMode"]
},
// Chant file settings
chantFile: opts["tac_chantFile"],
// Settings not from tac but still used by the script
extraNetworksDefaultMultiplier: opts["extra_networks_default_multiplier"]
extraNetworksDefaultMultiplier: opts["extra_networks_default_multiplier"],
extraNetworksSeparator: opts["extra_networks_add_text_separator"],
// Custom mapping settings
keymap: JSON.parse(opts["tac_keymap"]),
colorMap: JSON.parse(opts["tac_colormap"])
}
if (CFG && CFG.colors) {
newCFG["colors"] = CFG.colors;
}
if (newCFG.alias.onlyShowAlias) {
newCFG.alias.searchByAlias = true; // if only show translation, enable search by translation is necessary
}
// Reload tags if the tag file changed
if (!CFG || newCFG.tagFile !== CFG.tagFile || newCFG.extra.extraFile !== CFG.extra.extraFile) {
// Reload translations if the translation file changed
if (!TAC_CFG || newCFG.translation.translationFile !== TAC_CFG.translation.translationFile) {
translations.clear();
await loadTranslations(newCFG);
await loadExtraTags(newCFG);
}
// Reload tags if the tag file changed (after translations so extra tag translations get re-added)
if (!TAC_CFG || newCFG.tagFile !== TAC_CFG.tagFile || newCFG.extra.extraFile !== TAC_CFG.extra.extraFile) {
allTags = [];
await loadTags(newCFG);
}
// Reload translations if the translation file changed
if (!CFG || newCFG.translation.translationFile !== CFG.translation.translationFile) {
translations.clear();
await loadTranslations(newCFG);
}
// Update CSS if maxResults changed
if (CFG && newCFG.maxResults !== CFG.maxResults) {
if (TAC_CFG && newCFG.maxResults !== TAC_CFG.maxResults) {
gradioApp().querySelectorAll(".autocompleteResults").forEach(r => {
r.style.maxHeight = `${newCFG.maxResults * 50}px`;
});
}
// Remove ruby div if live preview was disabled
if (newCFG.translation.liveTranslation === false) {
[...gradioApp().querySelectorAll('.acRuby')].forEach(r => {
r.remove();
});
}
// Apply changes
CFG = newCFG;
TAC_CFG = newCFG;
// Callback
await processQueue(QUEUE_AFTER_CONFIG_CHANGE, null);
@@ -205,14 +255,15 @@ async function syncOptions() {
// Create the result list div and necessary styling
function createResultsDiv(textArea) {
let resultsDiv = document.createElement("div");
let resultsList = document.createElement('ul');
let resultsList = document.createElement("ul");
let textAreaId = getTextAreaIdentifier(textArea);
let typeClass = textAreaId.replaceAll(".", " ");
resultsDiv.style.maxHeight = `${CFG.maxResults * 50}px`;
resultsDiv.setAttribute('class', `autocompleteResults ${typeClass}`);
resultsList.setAttribute('class', 'autocompleteResultsList');
resultsDiv.style.maxHeight = `${TAC_CFG.maxResults * 50}px`;
resultsDiv.setAttribute("class", `autocompleteResults ${typeClass} notranslate`);
resultsDiv.setAttribute("translate", "no");
resultsList.setAttribute("class", "autocompleteResultsList");
resultsDiv.appendChild(resultsList);
return resultsDiv;
@@ -228,40 +279,60 @@ function showResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
resultsDiv.style.display = "block";
if (TAC_CFG.slidingPopup) {
let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd).left;
let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - resultsDiv.offsetWidth);
resultsDiv.style.left = `${offset}px`;
} else {
if (resultsDiv.style.left)
resultsDiv.style.removeProperty("left");
}
// Reset here too to make absolutely sure the browser registers it
resultsDiv.scrollTop = 0;
}
function hideResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
if (!resultsDiv) return;
resultsDiv.style.display = "none";
selectedTag = null;
}
// Function to check activation criteria
function isEnabled() {
if (CFG.activeIn.global) {
let modelList = CFG.activeIn.modelList
if (TAC_CFG.activeIn.global) {
// Skip check if the current model was not correctly detected, since it could wrongly disable the script otherwise
if (!currentModelName || !currentModelHash) return true;
let modelList = TAC_CFG.activeIn.modelList
.split(",")
.map(x => x.trim())
.filter(x => x.length > 0);
let shortHash = currentModelHash.substring(0, 10);
if (CFG.activeIn.modelListMode.toLowerCase() === "blacklist") {
let modelNameWithoutHash = currentModelName.replace(/\[.*\]$/g, "").trim();
if (TAC_CFG.activeIn.modelListMode.toLowerCase() === "blacklist") {
// If the current model is in the blacklist, disable
return modelList.filter(x => x === currentModelName || x === currentModelHash || x === shortHash).length === 0;
return modelList.filter(x => x === currentModelName || x === modelNameWithoutHash || x === currentModelHash || x === shortHash).length === 0;
} else {
// If the current model is in the whitelist, enable.
// An empty whitelist is ignored.
return modelList.length === 0 || modelList.filter(x => x === currentModelName || x === currentModelHash || x === shortHash).length > 0;
return modelList.length === 0 || modelList.filter(x => x === currentModelName || x === modelNameWithoutHash || x === currentModelHash || x === shortHash).length > 0;
}
} else {
return false;
}
}
const WEIGHT_REGEX = /[([]([^,()[\]:| ]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]/g;
const WEIGHT_REGEX = /[([]([^()[\]:|]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]/g;
const POINTY_REGEX = /<[^\s,<](?:[^\t\n\r,<>]*>|[^\t\n\r,> ]*)/g;
const COMPLETED_WILDCARD_REGEX = /__[^\s,_][^\t\n\r,_]*[^\s,_]__[^\s,_]*/g;
const NORMAL_TAG_REGEX = /[^\s,|<>]+|</g;
const NORMAL_TAG_REGEX = /[^\s,|<>)\]]+|</g;
const RUBY_TAG_REGEX = /[\w\d<][\w\d' \-?!/$%]{2,}>?/g;
const TAG_REGEX = new RegExp(`${POINTY_REGEX.source}|${COMPLETED_WILDCARD_REGEX.source}|${NORMAL_TAG_REGEX.source}`, "g");
// On click, insert the tag into the prompt textbox with respect to the cursor position
@@ -278,9 +349,9 @@ async function insertTextAtCursor(textArea, result, tagword) {
if (sanitizeResults && sanitizeResults.length > 0) {
sanitizedText = sanitizeResults[0];
} else {
sanitizedText = CFG.replaceUnderscores ? text.replaceAll("_", " ") : text;
sanitizedText = TAC_CFG.replaceUnderscores ? text.replaceAll("_", " ") : text;
if (CFG.escapeParentheses && tagType === ResultType.tag) {
if (TAC_CFG.escapeParentheses && tagType === ResultType.tag) {
sanitizedText = sanitizedText
.replaceAll("(", "\\(")
.replaceAll(")", "\\)")
@@ -298,23 +369,28 @@ async function insertTextAtCursor(textArea, result, tagword) {
let match = surrounding.match(new RegExp(escapeRegExp(`${tagword}`), "i"));
let afterInsertCursorPos = editStart + match.index + sanitizedText.length;
var optionalComma = "";
if (CFG.appendComma && ![ResultType.wildcardFile, ResultType.yamlWildcard].includes(tagType)) {
optionalComma = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null ? "" : ", ";
var optionalSeparator = "";
let extraNetworkTypes = [ResultType.hypernetwork, ResultType.lora];
let noCommaTypes = [ResultType.wildcardFile, ResultType.yamlWildcard].concat(extraNetworkTypes);
if (TAC_CFG.appendComma && !noCommaTypes.includes(tagType)) {
optionalSeparator = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null ? "" : ", ";
} else if (extraNetworkTypes.includes(tagType)) {
// Use the dedicated separator for extra networks if it's defined, otherwise fall back to space
optionalSeparator = TAC_CFG.extraNetworksSeparator || " ";
}
// Replace partial tag word with new text, add comma if needed
let insert = surrounding.replace(match, sanitizedText + optionalComma);
let insert = surrounding.replace(match, sanitizedText + optionalSeparator);
// Add back start
var newPrompt = prompt.substring(0, editStart) + insert + prompt.substring(editEnd);
textArea.value = newPrompt;
textArea.selectionStart = afterInsertCursorPos + optionalComma.length;
textArea.selectionStart = afterInsertCursorPos + optionalSeparator.length;
textArea.selectionEnd = textArea.selectionStart
// Since we've modified a Gradio Textbox component manually, we need to simulate an `input` DOM event to ensure its
// internal Svelte data binding remains in sync.
textArea.dispatchEvent(new Event("input", { bubbles: true }));
// Since we've modified a Gradio Textbox component manually, we need to simulate an `input` DOM event to ensure it's propagated back to python.
// Uses a built-in method from the webui's ui.js which also already accounts for event target
updateInput(textArea);
// Update previous tags with the edited prompt to prevent re-searching the same term
let weightedTags = [...newPrompt.matchAll(WEIGHT_REGEX)]
@@ -347,15 +423,16 @@ function addResultsToList(textArea, results, tagword, resetList) {
if (resetList) {
resultsList.innerHTML = "";
selectedTag = null;
oldSelectedTag = null;
resultDiv.scrollTop = 0;
resultCount = 0;
}
// Find right colors from config
let tagFileName = CFG.tagFile.split(".")[0];
let tagColors = CFG.colors;
let mode = gradioApp().querySelector('.dark') ? 0 : 1;
let nextLength = Math.min(results.length, resultCount + CFG.resultStepLength);
let tagFileName = TAC_CFG.tagFile.split(".")[0];
let tagColors = TAC_CFG.colorMap;
let mode = (document.querySelector(".dark") || gradioApp().querySelector(".dark")) ? 0 : 1;
let nextLength = Math.min(results.length, resultCount + TAC_CFG.resultStepLength);
for (let i = resultCount; i < nextLength; i++) {
let result = results[i];
@@ -375,7 +452,9 @@ function addResultsToList(textArea, results, tagword, resetList) {
let displayText = "";
// If the tag matches the tagword, we don't need to display the alias
if (result.aliases && !result.text.includes(tagword)) { // Alias
if(result.type === ResultType.chant) {
displayText = escapeHTML(result.aliases);
} else if (result.aliases && !result.text.includes(tagword)) { // Alias
let splitAliases = result.aliases.split(",");
let bestAlias = splitAliases.find(a => a.toLowerCase().includes(tagword));
@@ -396,7 +475,7 @@ function addResultsToList(textArea, results, tagword, resetList) {
if (translations.has(bestAlias) && translations.get(bestAlias) !== bestAlias && bestAlias !== result.text)
displayText += `[${translations.get(bestAlias)}]`;
if (!CFG.alias.onlyShowAlias && result.text !== bestAlias)
if (!TAC_CFG.alias.onlyShowAlias && result.text !== bestAlias)
displayText += " ➝ " + result.text;
} else { // No alias
displayText = escapeHTML(result.text);
@@ -410,7 +489,7 @@ function addResultsToList(textArea, results, tagword, resetList) {
itemText.innerHTML = displayText.replace(tagword, `<b>${tagword}</b>`);
// Add wiki link if the setting is enabled and a supported tag set loaded
if (CFG.showWikiLinks
if (TAC_CFG.showWikiLinks
&& (result.type === ResultType.tag)
&& (tagFileName.toLowerCase().startsWith("danbooru") || tagFileName.toLowerCase().startsWith("e621"))) {
let wikiLink = document.createElement("a");
@@ -493,8 +572,11 @@ function addResultsToList(textArea, results, tagword, resetList) {
}
resultCount = nextLength;
if (resetList)
if (resetList) {
selectedTag = null;
oldSelectedTag = null;
resultDiv.scrollTop = 0;
}
}
function updateSelectionStyle(textArea, newIndex, oldIndex) {
@@ -513,12 +595,117 @@ function updateSelectionStyle(textArea, newIndex, oldIndex) {
}
// Set scrolltop to selected item if we are showing more than max results
if (items.length > CFG.maxResults) {
if (items.length > TAC_CFG.maxResults) {
let selected = items[newIndex];
resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
}
}
function updateRuby(textArea, prompt) {
if (!TAC_CFG.translation.liveTranslation) return;
if (!TAC_CFG.translation.translationFile || TAC_CFG.translation.translationFile === "None") return;
let ruby = gradioApp().querySelector('.acRuby' + getTextAreaIdentifier(textArea));
if (!ruby) {
let textAreaId = getTextAreaIdentifier(textArea);
let typeClass = textAreaId.replaceAll(".", " ");
ruby = document.createElement("div");
ruby.setAttribute("class", `acRuby${typeClass} notranslate`);
textArea.parentNode.appendChild(ruby);
}
ruby.innerText = prompt;
let bracketEscapedPrompt = prompt.replaceAll("\\(", "$").replaceAll("\\)", "%");
let rubyTags = bracketEscapedPrompt.match(RUBY_TAG_REGEX);
if (!rubyTags) return;
rubyTags.sort((a, b) => b.length - a.length);
rubyTags = new Set(rubyTags);
const prepareTag = (tag) => {
tag = tag.replaceAll("$", "\\(").replaceAll("%", "\\)");
let unsanitizedTag = tag
.replaceAll(" ", "_")
.replaceAll("\\(", "(")
.replaceAll("\\)", ")");
const translation = translations?.get(tag) || translations?.get(unsanitizedTag);
let escapedTag = escapeRegExp(tag);
return { tag, escapedTag, translation };
}
const replaceOccurences = (text, tuple) => {
let { tag, escapedTag, translation } = tuple;
let searchRegex = new RegExp(`(?<!<ruby>)(?:\\b)${escapedTag}(?:\\b|$|(?=[,|: \\t\\n\\r]))(?!<rt>)`, "g");
return text.replaceAll(searchRegex, `<ruby>${escapeHTML(tag)}<rt>${translation}</rt></ruby>`);
}
let html = escapeHTML(prompt);
// First try to find direct matches
[...rubyTags].forEach(tag => {
let tuple = prepareTag(tag);
if (tuple.translation) {
html = replaceOccurences(html, tuple);
} else {
let subTags = tuple.tag.split(" ").filter(x => x.trim().length > 0);
// Return if there is only one word
if (subTags.length === 1) return;
let subHtml = tag.replaceAll("$", "\\(").replaceAll("%", "\\)");
let translateNgram = (windows) => {
windows.forEach(window => {
let combinedTag = window.join(" ");
let subTuple = prepareTag(combinedTag);
if (subTuple.tag.length <= 2) return;
if (subTuple.translation) {
subHtml = replaceOccurences(subHtml, subTuple);
}
});
}
// Perform n-gram sliding window search
translateNgram(toNgrams(subTags, 3));
translateNgram(toNgrams(subTags, 2));
translateNgram(toNgrams(subTags, 1));
let escapedTag = escapeRegExp(tuple.tag);
let searchRegex = new RegExp(`(?<!<ruby>)(?:\\b)${escapedTag}(?:\\b|$|(?=[,|: \\t\\n\\r]))(?!<rt>)`, "g");
html = html.replaceAll(searchRegex, subHtml);
}
});
ruby.innerHTML = html;
// Add listeners for auto selection
const childNodes = [...ruby.childNodes];
[...ruby.children].forEach(child => {
const textBefore = childNodes.slice(0, childNodes.indexOf(child)).map(x => x.childNodes[0]?.textContent || x.textContent).join("")
child.onclick = () => rubyTagClicked(child, textBefore, prompt, textArea);
});
}
function rubyTagClicked(node, textBefore, prompt, textArea) {
let selectionText = node.childNodes[0].textContent;
// Find start and end position of the tag in the prompt
let startPos = prompt.indexOf(textBefore) + textBefore.length;
let endPos = startPos + selectionText.length;
// Select in text area
textArea.focus();
textArea.setSelectionRange(startPos, endPos);
}
async function autocomplete(textArea, prompt, fixedTag = null) {
// Return if the function is deactivated in the UI
if (!isEnabled()) return;
@@ -584,7 +771,11 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
// instead of having them added in the order of the parsers
let shouldSort = resultCandidates.length > 1;
if (shouldSort) {
results = results.sort((a, b) => a.text.localeCompare(b.text));
results = results.sort((a, b) => {
let sortByA = a.type === ResultType.chant ? a.aliases : a.text;
let sortByB = b.type === ResultType.chant ? b.aliases : b.text;
return sortByA.localeCompare(sortByB);
});
// Since some tags are kaomoji, we have to add the normal results in some cases
if (tagword.startsWith("<") || tagword.startsWith("*<")) {
@@ -596,7 +787,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
} else {
searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
}
let genericResults = allTags.filter(x => x[0].toLowerCase().search(searchRegex) > -1).slice(0, CFG.maxResults);
let genericResults = allTags.filter(x => x[0].toLowerCase().search(searchRegex) > -1).slice(0, TAC_CFG.maxResults);
genericResults.forEach(g => {
let result = new AutocompleteResult(g[0].trim(), ResultType.tag)
@@ -615,58 +806,55 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i');
} else {
searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
}
// If onlyShowAlias is enabled, we don't need to include normal results
if (CFG.alias.onlyShowAlias) {
results = allTags.filter(x => x[3] && x[3].toLowerCase().search(searchRegex) > -1);
} else {
// Else both normal tags and aliases/translations are included depending on the config
let baseFilter = (x) => x[0].toLowerCase().search(searchRegex) > -1;
let aliasFilter = (x) => x[3] && x[3].toLowerCase().search(searchRegex) > -1;
let translationFilter = (x) => (translations.has(x[0]) && translations.get(x[0]).toLowerCase().search(searchRegex) > -1)
|| x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().search(searchRegex) > -1);
let fil;
if (CFG.alias.searchByAlias && CFG.translation.searchByTranslation)
fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x);
else if (CFG.alias.searchByAlias && !CFG.translation.searchByTranslation)
fil = (x) => baseFilter(x) || aliasFilter(x);
else if (CFG.translation.searchByTranslation && !CFG.alias.searchByAlias)
fil = (x) => baseFilter(x) || translationFilter(x);
else
fil = (x) => baseFilter(x);
}
// Add final results
allTags.filter(fil).forEach(t => {
let result = new AutocompleteResult(t[0].trim(), ResultType.tag)
result.category = t[1];
result.count = t[2];
result.aliases = t[3];
results.push(result);
// Both normal tags and aliases/translations are included depending on the config
let baseFilter = (x) => x[0].toLowerCase().search(searchRegex) > -1;
let aliasFilter = (x) => x[3] && x[3].toLowerCase().search(searchRegex) > -1;
let translationFilter = (x) => (translations.has(x[0]) && translations.get(x[0]).toLowerCase().search(searchRegex) > -1)
|| x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().search(searchRegex) > -1);
let fil;
if (TAC_CFG.alias.searchByAlias && TAC_CFG.translation.searchByTranslation)
fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x);
else if (TAC_CFG.alias.searchByAlias && !TAC_CFG.translation.searchByTranslation)
fil = (x) => baseFilter(x) || aliasFilter(x);
else if (TAC_CFG.translation.searchByTranslation && !TAC_CFG.alias.searchByAlias)
fil = (x) => baseFilter(x) || translationFilter(x);
else
fil = (x) => baseFilter(x);
// Add final results
allTags.filter(fil).forEach(t => {
let result = new AutocompleteResult(t[0].trim(), ResultType.tag)
result.category = t[1];
result.count = t[2];
result.aliases = t[3];
results.push(result);
});
// Add extras
if (TAC_CFG.extra.extraFile) {
let extraResults = [];
extras.filter(fil).forEach(e => {
let result = new AutocompleteResult(e[0].trim(), ResultType.extra)
result.category = e[1] || 0; // If no category is given, use 0 as the default
result.meta = e[2] || "Custom tag";
result.aliases = e[3] || "";
extraResults.push(result);
});
// Add extras
if (CFG.extra.extraFile) {
let extraResults = [];
extras.filter(fil).forEach(e => {
let result = new AutocompleteResult(e[0].trim(), ResultType.extra)
result.category = e[1] || 0; // If no category is given, use 0 as the default
result.meta = e[2] || "Custom tag";
result.aliases = e[3] || "";
extraResults.push(result);
});
if (CFG.extra.addMode === "Insert before") {
results = extraResults.concat(results);
} else {
results = results.concat(extraResults);
}
if (TAC_CFG.extra.addMode === "Insert before") {
results = extraResults.concat(results);
} else {
results = results.concat(extraResults);
}
}
// Slice if the user has set a max result count
if (!CFG.showAllResults) {
results = results.slice(0, CFG.maxResults);
if (!TAC_CFG.showAllResults) {
results = results.slice(0, TAC_CFG.maxResults);
}
}
@@ -684,8 +872,18 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
function navigateInList(textArea, event) {
// Return if the function is deactivated in the UI or the current model is excluded due to white/blacklist settings
if (!isEnabled()) return;
let keys = TAC_CFG.keymap;
validKeys = ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", "Enter", "Tab", "Escape"];
// Close window if Home or End is pressed while not a keybinding, since it would break completion on leaving the original tag
if ((event.key === "Home" || event.key === "End") && !Object.values(keys).includes(event.key)) {
hideResults(textArea);
return;
}
// All set keys that are not None or empty are valid
// Default keys are: ArrowUp, ArrowDown, PageUp, PageDown, Home, End, Enter, Tab, Escape
validKeys = Object.values(keys).filter(x => x !== "None" && x !== "");
if (!validKeys.includes(event.key)) return;
if (!isVisible(textArea)) return
@@ -695,63 +893,60 @@ function navigateInList(textArea, event) {
oldSelectedTag = selectedTag;
switch (event.key) {
case "ArrowUp":
case keys["MoveUp"]:
if (selectedTag === null) {
selectedTag = resultCount - 1;
} else {
selectedTag = (selectedTag - 1 + resultCount) % resultCount;
}
break;
case "ArrowDown":
case keys["MoveDown"]:
if (selectedTag === null) {
selectedTag = 0;
} else {
selectedTag = (selectedTag + 1) % resultCount;
}
break;
case "PageUp":
case keys["JumpUp"]:
if (selectedTag === null || selectedTag === 0) {
selectedTag = resultCount - 1;
} else {
selectedTag = (Math.max(selectedTag - 5, 0) + resultCount) % resultCount;
}
break;
case "PageDown":
case keys["JumpDown"]:
if (selectedTag === null || selectedTag === resultCount - 1) {
selectedTag = 0;
} else {
selectedTag = Math.min(selectedTag + 5, resultCount - 1) % resultCount;
}
break;
case "Home":
case keys["JumpToStart"]:
selectedTag = 0;
break;
case "End":
case keys["JumpToEnd"]:
selectedTag = resultCount - 1;
break;
case "ArrowLeft":
selectedTag = 0;
break;
case "ArrowRight":
selectedTag = resultCount - 1;
break;
case "Enter":
case keys["ChooseSelected"]:
if (selectedTag !== null) {
insertTextAtCursor(textArea, results[selectedTag], tagword);
} else {
hideResults(textArea);
return;
}
break;
case "Tab":
case keys["ChooseFirstOrSelected"]:
if (selectedTag === null) {
selectedTag = 0;
}
insertTextAtCursor(textArea, results[selectedTag], tagword);
break;
case "Escape":
case keys["Close"]:
hideResults(textArea);
break;
}
if (selectedTag === resultCount - 1
&& (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "ArrowLeft" || event.key === "ArrowRight")) {
let moveKeys = [keys["MoveUp"], keys["MoveDown"], keys["JumpUp"], keys["JumpDown"], keys["JumpToStart"], keys["JumpToEnd"]];
if (selectedTag === resultCount - 1 && moveKeys.includes(event.key)) {
addResultsToList(textArea, results, tagword, false);
}
// Update highlighting
@@ -763,17 +958,71 @@ function navigateInList(textArea, event) {
event.stopPropagation();
}
async function refreshTacTempFiles() {
setTimeout(async () => {
wildcardFiles = [];
wildcardExtFiles = [];
yamlWildcards = [];
embeddings = [];
hypernetworks = [];
loras = [];
lycos = [];
await processQueue(QUEUE_FILE_LOAD, null);
console.log("TAC: Refreshed temp files");
}, 2000);
}
function addAutocompleteToArea(area) {
// Return if autocomplete is disabled for the current area type in config
let textAreaId = getTextAreaIdentifier(area);
if ((!TAC_CFG.activeIn.img2img && textAreaId.includes("img2img"))
|| (!TAC_CFG.activeIn.txt2img && textAreaId.includes("txt2img"))
|| (!TAC_CFG.activeIn.negativePrompts && textAreaId.includes("n"))
|| (!TAC_CFG.activeIn.thirdParty && textAreaId.includes("thirdParty"))) {
return;
}
// Only add listeners once
if (!area.classList.contains('autocomplete')) {
// Add our new element
var resultsDiv = createResultsDiv(area);
area.parentNode.insertBefore(resultsDiv, area.nextSibling);
// Hide by default so it doesn't show up on page load
hideResults(area);
// Add autocomplete event listener
area.addEventListener('input', () => {
debounce(autocomplete(area, area.value), TAC_CFG.delayTime);
updateRuby(area, area.value);
});
// Add focusout event listener
area.addEventListener('focusout', debounce(() => hideResults(area), 400));
// Add up and down arrow event listener
area.addEventListener('keydown', (e) => navigateInList(area, e));
// CompositionEnd fires after the user has finished IME composing
// We need to block hide here to prevent the enter key from insta-closing the results
area.addEventListener('compositionend', () => {
hideBlocked = true;
setTimeout(() => { hideBlocked = false; }, 100);
});
// Add class so we know we've already added the listeners
area.classList.add('autocomplete');
}
}
// One-time setup, triggered from onUiUpdate
async function setup() {
// Load colors
CFG["colors"] = (await readFile(`${tagBasePath}/colors.json`, true));
// Load external files needed by completion extensions
await processQueue(QUEUE_FILE_LOAD, null);
// Find all textareas
let textAreas = getTextAreas();
// Add mutation observer to accordions inside a base that has onDemand set to true
addOnDemandObservers(addAutocompleteToArea);
// Add event listener to apply settings button so we can mirror the changes to our internal config
let applySettingsButton = gradioApp().querySelector("#tab_settings #settings_submit") || gradioApp().querySelector("#tab_settings > div > .gr-button-primary");
applySettingsButton?.addEventListener("click", () => {
@@ -784,7 +1033,7 @@ async function setup() {
});
// Add change listener to our quicksettings to change our internal config without the apply button for them
let quicksettings = gradioApp().querySelector('#quicksettings');
let commonQueryPart = "[id^=setting_tac] > label >";
let commonQueryPart = "[id^=setting_tac] > label";
quicksettings?.querySelectorAll(`${commonQueryPart} input, ${commonQueryPart} textarea, ${commonQueryPart} select`).forEach(e => {
e.addEventListener("change", () => {
setTimeout(async () => {
@@ -792,23 +1041,26 @@ async function setup() {
}, 500);
});
});
// Add change listener to model dropdown to react to model changes
let modelDropdown = gradioApp().querySelector("#setting_sd_model_checkpoint select");
currentModelName = modelDropdown.value;
modelDropdown?.addEventListener("change", () => {
setTimeout(() => {
currentModelName = modelDropdown.value;
}, 100);
quicksettings?.querySelectorAll(`[id^=setting_tac].gradio-dropdown input`).forEach(e => {
observeElement(e, "value", () => {
setTimeout(async () => {
await syncOptions();
}, 500);
})
});
// Listener for internal temp files refresh button
gradioApp().querySelector("#refresh_tac_refreshTempFiles")?.addEventListener("click", refreshTacTempFiles);
// Add mutation observer for the model hash text to also allow hash-based blacklist again
let modelHashText = gradioApp().querySelector("#sd_checkpoint_hash");
updateModelName();
if (modelHashText) {
currentModelHash = modelHashText.title
let modelHashObserver = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === "attributes" && mutation.attributeName === "title") {
currentModelHash = mutation.target.title;
updateModelName();
}
}
});
@@ -819,63 +1071,28 @@ async function setup() {
if (textAreas.every(v => v === null || v === undefined)) return;
// Already added or unnecessary to add
if (gradioApp().querySelector('.autocompleteResults.p')) {
if (gradioApp().querySelector('.autocompleteResults.n') || !CFG.activeIn.negativePrompts) {
if (gradioApp().querySelector('.autocompleteResults.n') || !TAC_CFG.activeIn.negativePrompts) {
return;
}
} else if (!CFG.activeIn.txt2img && !CFG.activeIn.img2img) {
} else if (!TAC_CFG.activeIn.txt2img && !TAC_CFG.activeIn.img2img) {
return;
}
textAreas.forEach(area => {
// Return if autocomplete is disabled for the current area type in config
let textAreaId = getTextAreaIdentifier(area);
if ((!CFG.activeIn.img2img && textAreaId.includes("img2img"))
|| (!CFG.activeIn.txt2img && textAreaId.includes("txt2img"))
|| (!CFG.activeIn.negativePrompts && textAreaId.includes("n"))
|| (!CFG.activeIn.thirdParty && textAreaId.includes("thirdParty"))) {
return;
}
// Only add listeners once
if (!area.classList.contains('autocomplete')) {
// Add our new element
var resultsDiv = createResultsDiv(area);
area.parentNode.insertBefore(resultsDiv, area.nextSibling);
// Hide by default so it doesn't show up on page load
hideResults(area);
// Add autocomplete event listener
area.addEventListener('input', debounce(() => autocomplete(area, area.value), CFG.delayTime));
// Add focusout event listener
area.addEventListener('focusout', debounce(() => hideResults(area), 400));
// Add up and down arrow event listener
area.addEventListener('keydown', (e) => navigateInList(area, e));
// CompositionEnd fires after the user has finished IME composing
// We need to block hide here to prevent the enter key from insta-closing the results
area.addEventListener('compositionend', () => {
hideBlocked = true;
setTimeout(() => { hideBlocked = false; }, 100);
});
// Add class so we know we've already added the listeners
area.classList.add('autocomplete');
}
});
textAreas.forEach(area => addAutocompleteToArea(area));
// Add style to dom
let acStyle = document.createElement('style');
//let css = gradioApp().querySelector('.dark') ? autocompleteCSS_dark : autocompleteCSS_light;
let mode = gradioApp().querySelector('.dark') ? 0 : 1;
let mode = (document.querySelector(".dark") || gradioApp().querySelector(".dark")) ? 0 : 1;
// Check if we are on webkit
let browser = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 ? "firefox" : "other";
let css = autocompleteCSS;
// Replace vars with actual values (can't use actual css vars because of the way we inject the css)
Object.keys(styleColors).forEach((key) => {
css = css.replace(`var(${key})`, styleColors[key][mode]);
css = css.replaceAll(`var(${key})`, styleColors[key][mode]);
})
Object.keys(browserVars).forEach((key) => {
css = css.replace(`var(${key})`, browserVars[key][browser]);
css = css.replaceAll(`var(${key})`, browserVars[key][browser]);
})
if (acStyle.styleSheet) {
@@ -888,17 +1105,17 @@ async function setup() {
// Callback
await processQueue(QUEUE_AFTER_SETUP, null);
}
let loading = false;
var tacLoading = false;
onUiUpdate(async () => {
if (loading) return;
if (tacLoading) return;
if (Object.keys(opts).length === 0) return;
if (CFG) return;
loading = true;
if (TAC_CFG) return;
tacLoading = true;
// Get our tag base path from the temp file
tagBasePath = await readFile(`tmp/tagAutocompletePath.txt`);
// Load config from webui opts
await syncOptions();
// Rest of setup
setup();
loading = false;
tacLoading = false;
});

View File

@@ -1,18 +1,26 @@
# This helper script scans folders for wildcards and embeddings and writes them
# to a temporary file to expose it to the javascript side
import gradio as gr
import glob
from pathlib import Path
from modules import scripts, script_callbacks, shared, sd_hijack
import gradio as gr
import yaml
import time
import threading
from modules import script_callbacks, scripts, sd_hijack, shared
# Webui root path
FILE_DIR = Path().absolute()
try:
from modules.paths import extensions_dir, script_path
# The extension base path
EXT_PATH = FILE_DIR.joinpath('extensions')
# Webui root path
FILE_DIR = Path(script_path)
# The extension base path
EXT_PATH = Path(extensions_dir)
except ImportError:
# Webui root path
FILE_DIR = Path().absolute()
# The extension base path
EXT_PATH = FILE_DIR.joinpath('extensions')
# Tags base path
TAGS_PATH = Path(scripts.basedir()).joinpath('tags')
@@ -26,6 +34,11 @@ try:
LORA_PATH = Path(shared.cmd_opts.lora_dir)
except AttributeError:
LORA_PATH = None
try:
LYCO_PATH = Path(shared.cmd_opts.lyco_dir)
except AttributeError:
LYCO_PATH = None
def find_ext_wildcard_paths():
"""Returns the path to the extension wildcards folder"""
@@ -149,18 +162,28 @@ def get_hypernetworks():
"""Write a list of all hypernetworks"""
# Get a list of all hypernetworks in the folder
all_hypernetworks = [str(h.name) for h in HYP_PATH.rglob("*") if h.suffix in {".pt"}]
hyp_paths = [Path(h) for h in glob.glob(HYP_PATH.joinpath("**/*").as_posix(), recursive=True)]
all_hypernetworks = [str(h.name) for h in hyp_paths if h.suffix in {".pt"}]
# Remove file extensions
return [h[:h.rfind('.')] for h in all_hypernetworks]
return sorted([h[:h.rfind('.')] for h in all_hypernetworks], key=lambda x: x.lower())
def get_lora():
"""Write a list of all lora"""
# Get a list of all lora in the folder
all_lora = [str(l.name) for l in LORA_PATH.rglob("*") if l.suffix in {".safetensors", ".ckpt", ".pt"}]
lora_paths = [Path(l) for l in glob.glob(LORA_PATH.joinpath("**/*").as_posix(), recursive=True)]
all_lora = [str(l.name) for l in lora_paths if l.suffix in {".safetensors", ".ckpt", ".pt"}]
# Remove file extensions
return [l[:l.rfind('.')] for l in all_lora]
return sorted([l[:l.rfind('.')] for l in all_lora], key=lambda x: x.lower())
def get_lyco():
"""Write a list of all LyCORIS/LOHA from https://github.com/KohakuBlueleaf/a1111-sd-webui-lycoris"""
# Get a list of all LyCORIS in the folder
lyco_paths = [Path(ly) for ly in glob.glob(LYCO_PATH.joinpath("**/*").as_posix(), recursive=True)]
all_lyco = [str(ly.name) for ly in lyco_paths if ly.suffix in {".safetensors", ".ckpt", ".pt"}]
# Remove file extensions
return sorted([ly[:ly.rfind('.')] for ly in all_lyco], key=lambda x: x.lower())
def write_tag_base_path():
"""Writes the tag base path to a fixed location temporary file"""
@@ -183,6 +206,14 @@ def update_tag_files():
csv_files = files
csv_files_withnone = ["None"] + files
json_files = []
json_files_withnone = []
def update_json_files():
"""Returns a list of all potential json files"""
global json_files, json_files_withnone
files = [str(j.relative_to(TAGS_PATH)) for j in TAGS_PATH.glob("*.json")]
json_files = files
json_files_withnone = ["None"] + files
# Write the tag base path to a fixed location temporary file
@@ -192,6 +223,7 @@ if not STATIC_TEMP_PATH.exists():
write_tag_base_path()
update_tag_files()
update_json_files()
# Check if the temp path exists and create it if not
if not TEMP_PATH.exists():
@@ -204,40 +236,54 @@ write_to_temp_file('wce.txt', [])
write_to_temp_file('wcet.txt', [])
write_to_temp_file('hyp.txt', [])
write_to_temp_file('lora.txt', [])
write_to_temp_file('lyco.txt', [])
# Only reload embeddings if the file doesn't exist, since they are already re-written on model load
if not TEMP_PATH.joinpath("emb.txt").exists():
write_to_temp_file('emb.txt', [])
# Write wildcards to wc.txt if found
if WILDCARD_PATH.exists():
wildcards = [WILDCARD_PATH.relative_to(FILE_DIR).as_posix()] + get_wildcards()
if wildcards:
write_to_temp_file('wc.txt', wildcards)
# Write extension wildcards to wce.txt if found
if WILDCARD_EXT_PATHS is not None:
wildcards_ext = get_ext_wildcards()
if wildcards_ext:
write_to_temp_file('wce.txt', wildcards_ext)
# Write yaml extension wildcards to wcet.txt if found
wildcards_yaml_ext = get_ext_wildcard_tags()
if wildcards_yaml_ext:
write_to_temp_file('wcet.txt', wildcards_yaml_ext)
# Write embeddings to emb.txt if found
if EMB_PATH.exists():
# Get embeddings after the model loaded callback
script_callbacks.on_model_loaded(get_embeddings)
if HYP_PATH.exists():
hypernets = get_hypernetworks()
if hypernets:
write_to_temp_file('hyp.txt', hypernets)
def refresh_temp_files():
write_temp_files()
get_embeddings(shared.sd_model)
if LORA_PATH is not None and LORA_PATH.exists():
lora = get_lora()
if lora:
write_to_temp_file('lora.txt', lora)
def write_temp_files():
# Write wildcards to wc.txt if found
if WILDCARD_PATH.exists():
wildcards = [WILDCARD_PATH.relative_to(FILE_DIR).as_posix()] + get_wildcards()
if wildcards:
write_to_temp_file('wc.txt', wildcards)
# Write extension wildcards to wce.txt if found
if WILDCARD_EXT_PATHS is not None:
wildcards_ext = get_ext_wildcards()
if wildcards_ext:
write_to_temp_file('wce.txt', wildcards_ext)
# Write yaml extension wildcards to wcet.txt if found
wildcards_yaml_ext = get_ext_wildcard_tags()
if wildcards_yaml_ext:
write_to_temp_file('wcet.txt', wildcards_yaml_ext)
if HYP_PATH.exists():
hypernets = get_hypernetworks()
if hypernets:
write_to_temp_file('hyp.txt', hypernets)
if LORA_PATH is not None and LORA_PATH.exists():
lora = get_lora()
if lora:
write_to_temp_file('lora.txt', lora)
if LYCO_PATH is not None and LYCO_PATH.exists():
lyco = get_lyco()
if lyco:
write_to_temp_file('lyco.txt', lyco)
write_temp_files()
# Register autocomplete options
def on_ui_settings():
@@ -249,10 +295,11 @@ def on_ui_settings():
shared.opts.add_option("tac_activeIn.txt2img", shared.OptionInfo(True, "Active in txt2img (Requires restart)", section=TAC_SECTION))
shared.opts.add_option("tac_activeIn.img2img", shared.OptionInfo(True, "Active in img2img (Requires restart)", section=TAC_SECTION))
shared.opts.add_option("tac_activeIn.negativePrompts", shared.OptionInfo(True, "Active in negative prompts (Requires restart)", section=TAC_SECTION))
shared.opts.add_option("tac_activeIn.thirdParty", shared.OptionInfo(True, "Active in third party textboxes [Dataset Tag Editor] (Requires restart)", section=TAC_SECTION))
shared.opts.add_option("tac_activeIn.thirdParty", shared.OptionInfo(True, "Active in third party textboxes [Dataset Tag Editor] [Image Browser] [Tagger] [Multidiffusion Upscaler] (Requires restart)", section=TAC_SECTION))
shared.opts.add_option("tac_activeIn.modelList", shared.OptionInfo("", "List of model names (with file extension) or their hashes to use as black/whitelist, separated by commas.", section=TAC_SECTION))
shared.opts.add_option("tac_activeIn.modelListMode", shared.OptionInfo("Blacklist", "Mode to use for model list", gr.Dropdown, lambda: {"choices": ["Blacklist","Whitelist"]}, section=TAC_SECTION))
# Results related settings
shared.opts.add_option("tac_slidingPopup", shared.OptionInfo(True, "Move completion popup together with text cursor", section=TAC_SECTION))
shared.opts.add_option("tac_maxResults", shared.OptionInfo(5, "Maximum results", section=TAC_SECTION))
shared.opts.add_option("tac_showAllResults", shared.OptionInfo(False, "Show all results", section=TAC_SECTION))
shared.opts.add_option("tac_resultStepLength", shared.OptionInfo(100, "How many results to load at once", section=TAC_SECTION))
@@ -261,6 +308,7 @@ def on_ui_settings():
shared.opts.add_option("tac_useEmbeddings", shared.OptionInfo(True, "Search for embeddings", section=TAC_SECTION))
shared.opts.add_option("tac_useHypernetworks", shared.OptionInfo(True, "Search for hypernetworks", section=TAC_SECTION))
shared.opts.add_option("tac_useLoras", shared.OptionInfo(True, "Search for Loras", section=TAC_SECTION))
shared.opts.add_option("tac_useLycos", shared.OptionInfo(True, "Search for LyCORIS/LoHa", section=TAC_SECTION))
shared.opts.add_option("tac_showWikiLinks", shared.OptionInfo(False, "Show '?' next to tags, linking to its Danbooru or e621 wiki page (Warning: This is an external site and very likely contains NSFW examples!)", section=TAC_SECTION))
# Insertion related settings
shared.opts.add_option("tac_replaceUnderscores", shared.OptionInfo(True, "Replace underscores with spaces on insertion", section=TAC_SECTION))
@@ -273,8 +321,59 @@ def on_ui_settings():
shared.opts.add_option("tac_translation.translationFile", shared.OptionInfo("None", "Translation filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
shared.opts.add_option("tac_translation.oldFormat", shared.OptionInfo(False, "Translation file uses old 3-column translation format instead of the new 2-column one", section=TAC_SECTION))
shared.opts.add_option("tac_translation.searchByTranslation", shared.OptionInfo(True, "Search by translation", section=TAC_SECTION))
shared.opts.add_option("tac_translation.liveTranslation", shared.OptionInfo(False, "Show live tag translation below prompt (WIP, expect some bugs)", section=TAC_SECTION))
# Extra file settings
shared.opts.add_option("tac_extra.extraFile", shared.OptionInfo("extra-quality-tags.csv", "Extra filename (for small sets of custom tags)", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
shared.opts.add_option("tac_extra.addMode", shared.OptionInfo("Insert before", "Mode to add the extra tags to the main tag list", gr.Dropdown, lambda: {"choices": ["Insert before","Insert after"]}, section=TAC_SECTION))
# Chant settings
shared.opts.add_option("tac_chantFile", shared.OptionInfo("demo-chants.json", "Chant filename (Chants are longer prompt presets)", gr.Dropdown, lambda: {"choices": json_files_withnone}, refresh=update_json_files, section=TAC_SECTION))
# Custom mappings
keymapDefault = """\
{
"MoveUp": "ArrowUp",
"MoveDown": "ArrowDown",
"JumpUp": "PageUp",
"JumpDown": "PageDown",
"JumpToStart": "Home",
"JumpToEnd": "End",
"ChooseSelected": "Enter",
"ChooseFirstOrSelected": "Tab",
"Close": "Escape"
}\
"""
colorDefault = """\
{
"danbooru": {
"-1": ["red", "maroon"],
"0": ["lightblue", "dodgerblue"],
"1": ["indianred", "firebrick"],
"3": ["violet", "darkorchid"],
"4": ["lightgreen", "darkgreen"],
"5": ["orange", "darkorange"]
},
"e621": {
"-1": ["red", "maroon"],
"0": ["lightblue", "dodgerblue"],
"1": ["gold", "goldenrod"],
"3": ["violet", "darkorchid"],
"4": ["lightgreen", "darkgreen"],
"5": ["tomato", "darksalmon"],
"6": ["red", "maroon"],
"7": ["whitesmoke", "black"],
"8": ["seagreen", "darkseagreen"]
}
}\
"""
keymapLabel = "Configure Hotkeys. For possible values, see https://www.w3.org/TR/uievents-key, or leave empty / set to 'None' to disable. Must be valid JSON."
colorLabel = "Configure colors. See https://github.com/DominikDoom/a1111-sd-webui-tagcomplete#colors for info. Must be valid JSON."
try:
shared.opts.add_option("tac_keymap", shared.OptionInfo(keymapDefault, keymapLabel, gr.Code, lambda: {"language": "json", "interactive": True}, section=TAC_SECTION))
shared.opts.add_option("tac_colormap", shared.OptionInfo(colorDefault, colorLabel, gr.Code, lambda: {"language": "json", "interactive": True}, section=TAC_SECTION))
except AttributeError:
shared.opts.add_option("tac_keymap", shared.OptionInfo(keymapDefault, keymapLabel, gr.Textbox, section=TAC_SECTION))
shared.opts.add_option("tac_colormap", shared.OptionInfo(colorDefault, colorLabel, gr.Textbox, section=TAC_SECTION))
shared.opts.add_option("tac_refreshTempFiles", shared.OptionInfo("Refresh TAC temp files", "Refresh internal temp files", gr.HTML, {}, refresh=refresh_temp_files, section=TAC_SECTION))
script_callbacks.on_ui_settings(on_ui_settings)

View File

@@ -1,6 +0,0 @@
masterpiece,5,Quality tag,
best_quality,5,Quality tag,
high_quality,5,Quality tag,
normal_quality,5,Quality tag,
low_quality,5,Quality tag,
worst_quality,5,Quality tag,
1 masterpiece 5 Quality tag
2 best_quality 5 Quality tag
3 high_quality 5 Quality tag
4 normal_quality 5 Quality tag
5 low_quality 5 Quality tag
6 worst_quality 5 Quality tag

View File

@@ -1,21 +0,0 @@
{
"danbooru": {
"-1": ["red", "maroon"],
"0": ["lightblue", "dodgerblue"],
"1": ["indianred", "firebrick"],
"3": ["violet", "darkorchid"],
"4": ["lightgreen", "darkgreen"],
"5": ["orange", "darkorange"]
},
"e621": {
"-1": ["red", "maroon"],
"0": ["lightblue", "dodgerblue"],
"1": ["gold", "goldenrod"],
"3": ["violet", "darkorchid"],
"4": ["lightgreen", "darkgreen"],
"5": ["tomato", "darksalmon"],
"6": ["red", "maroon"],
"7": ["whitesmoke", "black"],
"8": ["seagreen", "darkseagreen"]
}
}

32
tags/demo-chants.json Normal file
View File

@@ -0,0 +1,32 @@
[
{
"name": "Basic-NegativePrompt",
"terms": "Basic,Negative,Low,Quality",
"content": "(worst quality, low quality, normal quality)",
"color": 3
},
{
"name": "Basic-HighQuality",
"terms": "Basic,Best,High,Quality",
"content": "(masterpiece, best quality, high quality, highres, ultra-detailed)",
"color": 1
},
{
"name": "Basic-Start",
"terms": "Basic, Start, Simple, Demo",
"content": "(masterpiece, best quality, high quality, highres), 1girl, extremely beautiful detailed face, short curly hair, light smile, flower dress, outdoors, leaf, tree, best shadow",
"color": 5
},
{
"name": "Fancy-FireMagic",
"terms": "Fire, Magic, Fancy",
"content": "(extremely detailed CG unity 8k wallpaper), (masterpiece), (best quality), (ultra-detailed), (best illustration),(best shadow), (an extremely delicate and beautiful), dynamic angle, floating, fine detail, (bloom), (shine), glinting stars, classic, (painting), (sketch),\n\na girl, solo, bare shoulders, flat_chest, diamond and glaring eyes, beautiful detailed cold face, very long blue and sliver hair, floating black feathers, wavy hair, extremely delicate and beautiful girls, beautiful detailed eyes, glowing eyes,\n\npalace, the best building, ((Fire butterflies, Flying sparks, Flames))",
"color": 5
},
{
"name": "Fancy-WaterMagic",
"terms": "Water, Magic, Fancy",
"content": "(extremely detailed CG unity 8k wallpaper), (masterpiece), (best quality), (ultra-detailed), (best illustration),(best shadow), (an extremely delicate and beautiful), classic, dynamic angle, floating, fine detail, Depth of field, classic, (painting), (sketch), (bloom), (shine), glinting stars,\n\na girl, solo, bare shoulders, flat chest, diamond and glaring eyes, beautiful detailed cold face, very long blue and sliver hair, floating black feathers, wavy hair, extremely delicate and beautiful girls, beautiful detailed eyes, glowing eyes,\n\nriver, (forest),palace, (fairyland,feather,flowers, nature),(sunlight),Hazy fog, mist",
"color": 5
}
]

View File

@@ -0,0 +1,6 @@
masterpiece,5,Quality tag,,
best_quality,5,Quality tag,,
high_quality,5,Quality tag,,
normal_quality,5,Quality tag,,
low_quality,5,Quality tag,,
worst_quality,5,Quality tag,,
1 masterpiece 5 Quality tag
2 best_quality 5 Quality tag
3 high_quality 5 Quality tag
4 normal_quality 5 Quality tag
5 low_quality 5 Quality tag
6 worst_quality 5 Quality tag