Compare commits

...

63 Commits
1.4.0 ... 1.9.2

Author SHA1 Message Date
Dominik Reh
524514bd46 Fix parsing for real this time
Fixes #48 (again)
2022-10-29 18:35:06 +02:00
Dominik Reh
106fa13f65 Hotfix for broken parsing
Fixes #48
2022-10-29 17:24:44 +02:00
Dominik Reh
a038664616 Fix new regex for embeddings 2022-10-29 15:55:30 +02:00
Dominik Reh
789f44d52a Support editing tags inside weighting parentheses
Fixes #47
2022-10-29 14:48:44 +02:00
Dominik Reh
59ec54b171 Fix duplicate wildcards
Would occur if the extension folder was also just "wildcards" due to recursive search
2022-10-29 10:08:20 +02:00
Dominik Reh
983da36329 Create tmp folder in root if it doesn't exist
Fixes extension installation on Linux, closes #46
2022-10-29 09:55:30 +02:00
Dominik Reh
48bd3d7b51 Support multiline prompts
Fixes #44
2022-10-28 19:04:41 +02:00
Dominik Reh
c6c9e01410 Formatting 2022-10-28 18:08:02 +02:00
DominikDoom
bf5bb34605 Update README.md 2022-10-28 17:58:59 +02:00
Dominik Reh
860fd34fb4 Support for wildcards from different extensions
Now scans all extensions for a wildcards folder and will combine them
Implements the feature discussed in #37
2022-10-28 17:47:23 +02:00
Dominik Reh
886de4df29 Support installing the script as an extension
Closes #41
2022-10-28 15:46:16 +02:00
Dominik Reh
3e71890489 Support wildcard extension in arbitrary folder
Now no longer only looks in "extensions/wildcards/widlcards".
This enables the user to call the folder whatever they like.
Fixes #37
2022-10-28 15:08:28 +02:00
Dominik Reh
dc77b3f17f Configurable debounce delay
Workaround for the problem in #40
2022-10-26 15:20:06 +02:00
Dominik Reh
40d53d89d1 Hotfix for old scripts/wildcards path loading 2022-10-25 00:45:35 +02:00
Dominik Reh
c733b836e8 Load wildcards on demand
Fixes performance issues with many wildcard files, fixes #38
2022-10-25 00:39:41 +02:00
Dominik Reh
b537ca3938 Add support for PageUp/Down and Home/End scrolling
Closes #34
2022-10-24 17:01:43 +02:00
Dominik Reh
cb08b8467f Support for new wildcards extension folder
Only works for "extensions\wildcards\wildcards\" and its subfolders at the moment.
Closes #35.
2022-10-24 16:07:58 +02:00
DominikDoom
522989da8a Update README.md 2022-10-24 14:06:22 +02:00
Dominik Reh
e8cf50cdaa Support png embeddings
Fixes #33
2022-10-24 14:01:44 +02:00
Dominik Reh
4af8d5285d Make left/right arrow key navigation optional
Fixes #31
2022-10-23 14:50:04 +02:00
Dominik Reh
3759ec055a Allow autocomplete with Tab key
Closes #29
2022-10-22 13:23:16 +02:00
DominikDoom
ced6676aa6 Update README.md 2022-10-18 22:24:01 +02:00
sgmklp
6b3b8ccf45 Some performance optimizations (#21)
Also includes code readability improvements and ability to load results in steps.
Useful for long lists or if showAllResults is true.
2022-10-18 22:21:33 +02:00
DominikDoom
353cb1937e Fix inline code not separated 2022-10-17 15:40:38 +02:00
sgmklp
c8c857f2cc Add Chinese README (#20) 2022-10-17 15:32:57 +02:00
DominikDoom
0112acb820 Move showAllResults next to maxResults in config 2022-10-17 15:03:54 +02:00
DominikDoom
896533c986 Added translation info to README 2022-10-17 14:47:36 +02:00
Dominik Reh
9fc9f1ab7d Don't capture keydown if CTRL or ALT is held
Prevents the script from blocking weight editing and prompt submit shortcuts
Fixes #19
2022-10-17 13:56:15 +02:00
DominikDoom
1d12fdcc6a Merge pull request #16 from sgmklp/dev
Ability to add translations in the main csv or additional files.
2022-10-17 11:07:49 +02:00
sgmklp
1387351d4d add match by class 2022-10-17 11:07:54 +08:00
sgmklp
4593a9a4e1 Update javascript/tagAutocomplete.js
Co-authored-by: DominikDoom <34448969+DominikDoom@users.noreply.github.com>
2022-10-17 10:46:44 +08:00
sgmklp
d5636f9026 also add Tags in extra file without translation 2022-10-17 03:47:26 +08:00
sgmklp
59ccb7ac19 will add new Tag in translation to tag list 2022-10-17 03:30:22 +08:00
sgmklp
d8ec8793fa keep the old match function avaliable 2022-10-17 02:03:45 +08:00
sgmklp
b0bc2d9b0b change the match function of translation 2022-10-17 01:57:21 +08:00
DominikDoom
c67fd336fd Add note about caching issues to README 2022-10-16 14:31:30 +02:00
Dominik Reh
5bf0451432 Support nested wildcards 2022-10-16 13:42:50 +02:00
sgmklp
046e2d99fb add a config option to show all result 2022-10-16 19:02:33 +08:00
sgmklp
cb31b072b4 chang maxResults to default 2022-10-16 13:51:08 +08:00
sgmklp
b858370acf allow use extra translation file 2022-10-16 13:43:03 +08:00
sgmklp
854b1952db . 2022-10-16 01:50:07 +08:00
sgmklp
f77283342e add search by translation 2022-10-16 01:47:53 +08:00
sgmklp
ae1ed19b7d suppost only show the translation to result 2022-10-16 00:00:39 +08:00
sgmklp
5f1b8c8da3 . 2022-10-15 23:06:19 +08:00
sgmklp
db6dcc9568 Make the tranlation in csv adding to result, also support Tag without tranlation. 2022-10-15 22:57:04 +08:00
DominikDoom
53899093c8 Merge pull request #15 from sgmklp/dev 2022-10-15 16:07:18 +02:00
Dominik Reh
f9d98740f4 Fix negative prompt autocomplete after UI change 2022-10-15 16:05:49 +02:00
sgmklp
534f07225e fix write wildcard files with Chinese name with wrong coding to wc.txt 2022-10-15 21:43:57 +08:00
Dominik Reh
b8b0673e2d Formatting 2022-10-15 15:32:23 +02:00
DominikDoom
2f0d18a73f Update README.md 2022-10-15 15:15:24 +02:00
Dominik Reh
e68e7389dd Fix for empty wildcard or embed lines showing in results list 2022-10-15 15:15:13 +02:00
Dominik Reh
b5cecc4e8d Fix for wildcard & embedding file extensions 2022-10-15 14:53:34 +02:00
Dominik Reh
96828c241c Enable wildcards and embeds by default
Since we now search automatically, the script also doesn't try to load anything if none are found
2022-10-15 14:39:26 +02:00
Dominik Reh
07d7eddf66 Move script to js folder for easier install 2022-10-15 14:32:41 +02:00
Dominik Reh
08c10928f8 Automatic wildcard & embed discovery 2022-10-15 14:32:02 +02:00
Dominik Reh
a628d96a41 Added UI checkbox to turn off/on without restart
Also helps with #14
2022-10-15 13:23:03 +02:00
Dominik Reh
3a47a9b010 Insert results as textContent instead of innerHtml
Fixes #7
2022-10-14 09:38:51 +02:00
Dominik Reh
fbfc988fe5 Fix for broken RegEx with tags containing parentheses
Have I mentioned that all my homies hate RegEx?
2022-10-13 23:37:28 +02:00
Dominik Reh
a93a209e7e Update README.md 2022-10-13 22:37:23 +02:00
Dominik Reh
f5c00d8de4 Update README.md 2022-10-13 22:06:31 +02:00
Dominik Reh
0b7bb146a5 Fixed text insertion & curser position
This time for real (hopefully)
All my homies hate RegEx
2022-10-13 21:31:37 +02:00
Dominik Reh
f098b14248 Update README.md 2022-10-13 17:36:56 +02:00
Dominik Reh
9710eef4cc Added wildcard info to README 2022-10-13 17:26:17 +02:00
8 changed files with 1129 additions and 544 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
tags/temp/

View File

@@ -2,6 +2,8 @@
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/DominikDoom/a1111-sd-webui-tagcomplete)](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)
## [中文文档](./README_ZH.md)
This custom script serves as a drop-in extension for the popular [AUTOMATIC1111 web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) for Stable Diffusion.
It displays autocompletion hints for recognized tags from "image booru" boards such as Danbooru, which are primarily used for browsing Anime-style illustrations.
@@ -9,33 +11,54 @@ Since some Stable Diffusion models were trained using this information, for exam
I created this script as a convenience tool since it reduces the need of switching back and forth between the web UI and a booru site to copy-paste tags.
You can either download the files manually as described below, or use a pre-packaged version from [Releases](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases).
You can either clone / download the files manually as described [below](#installation), or use a pre-packaged version from [Releases](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases).
### Disclaimer:
This script is definitely not optimized, and it's not very intelligent. The tags are simply recommended based on their natural order in the CSV, which is their respective image count for the default Danbooru tag list. Also, at least for now there's no way to turn the script off from the ui, but I plan to get around to that eventually.
### Known Issues:
If `replaceUnderscores` is active, the script will currently only partly replace edited tags containing multiple words in brackets.
## Common Problems & Known Issues:
- The browser might cache old versions of the script, config, or embedding/wildcard lists. Try hitting `CTRL+F5` to clear the cache.
- If `replaceUnderscores` is active, the script will currently only partly 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.
### 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 (demo video further down). 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.
It also scans the embeddings folder and displays completion hints for the names of all .pt and .bin files inside if you start typing `<`. Note that some normal tags also use < in Kaomoji (like ">_<" for example), so the results will contain both.
## Screenshots
Demo video (with keyboard navigation):
https://user-images.githubusercontent.com/34448969/195344430-2b5f9945-b98b-4943-9fbc-82cf633321b1.mp4
Wildcard script support:
https://user-images.githubusercontent.com/34448969/195632461-49d226ae-d393-453d-8f04-1e44b073234c.mp4
Dark and Light mode supported, including tag colors:
![tagtypes](https://user-images.githubusercontent.com/34448969/195177127-f63949f8-271d-4767-bccd-f1b5e818a7f8.png)
![tagtypes_light](https://user-images.githubusercontent.com/34448969/195180061-ceebcc25-9e4c-424f-b0c9-ba8e8f4f17f4.png)
## Installation
Simply put `tagAutocomplete.js` into the `javascript` folder of your web UI installation. It will run automatically the next time the web UI is started.
For the script to work, you also need to download the `tags` folder from this repo and paste it and its contents into the web UI root, or create them there manually.
### As an extension (recommended)
Either clone the repo into your extensions folder:
```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).
The tags folder contains `config.json` and the tag data the script uses for autocompletion. By default, Danbooru and e621 tags are included.
Or create a folder there manually and place the `javascript`, `scripts` and `tags` folders in it.
### Config
### In the root folder (old)
Copy the `javascript`, `scripts` and `tags` folder into your web UI installation root. It will run automatically the next time the web UI is started.
---
In both configurations, the tags folder contains `config.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.
### Important:
The script needs **all three folders** to work properly.
## Config
The config contains the following settings and defaults:
```json
{
@@ -46,8 +69,21 @@ The config contains the following settings and defaults:
"negativePrompts": true
},
"maxResults": 5,
"resultStepLength": 500,
"showAllResults": false,
"useLeftRightArrowKeys": false,
"replaceUnderscores": true,
"escapeParentheses": true,
"useWildcards": true,
"useEmbeddings": true,
"translation": {
"searchByTranslation": true,
"onlyShowTranslation": false
},
"extra": {
"extraFile": "",
"onlyTranslationExtraFile": false
},
"colors": {
"danbooru": {
"0": ["lightblue", "dodgerblue"],
@@ -74,12 +110,45 @@ The config contains the following settings and defaults:
|---------|-------------|
| tagFile | Specifies the tag file to use. You can provide a custom tag database of your liking, but since the script was developed with Danbooru tags in mind, it might not work properly with other configurations.|
| activeIn | Allows to selectively (de)activate the script for txt2img, img2img, and the negative prompts for both. |
| maxResults | How many results to show max. For the default tag set, the results are ordered by occurence count. |
| maxResults | How many results to show max. For the default tag set, the results are ordered by occurence count. For embeddings and wildcards it will show all results in a scrollable list. |
| resultStepLength | Allows to load results in smaller batches of the specified size for better performance in long lists or if showAllResults is true. |
| showAllResults | If true, will ignore maxResults and show all results in a scrollable list. **Warning:** can lag your browser for long lists. |
| useLeftRightArrowKeys | If true, left and right arrows will select the first/last result in the popup instead of moving the cursor in the textbox. |
| replaceUnderscores | If true, undescores are replaced with spaces on clicking a tag. Might work better for some models. |
| escapeParentheses | If true, escapes tags containing () so they don't contribute to the web UI's prompt weighting functionality. |
| useWildcards | Used to toggle the wildcard completion functionality. |
| useEmbeddings | Used to toggle the embedding completion functionality. |
| translation | Options for translating tags. More info in the section below. |
| extras | Options for additional tag files / translations. More info in the section below. |
| colors | Contains customizable colors for the tag types, you can 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.|
### CSV tag data
### Translations & Extra tags
With the recent update it is now possible to add translations to the tags. These will be searchable / shown according to the settings in `config.json`:
- `searchByTranslation` - Whether to search for the translated term as well or only the English tag.
- `onlyShowTranslation` - Replaces the English tag with its translation if it has one. Only for displaying, the inserted text at the end is still the English tag.
Example with full and partial chinese tag sets:
![translation](https://user-images.githubusercontent.com/34448969/196175839-8aaacb26-5c90-48e3-be65-647a0b444ead.png)
![translation_mixed](https://user-images.githubusercontent.com/34448969/196176233-76d4cb5f-16cf-4800-a69b-adb64a79ca8b.png)
Translations can be added in multiple ways, which is where the "Extra" file comes into play.
1. Directly in the main tag file. Simply add a third value, separated by comma, containing the translation for the tag in that row.
2. As an extra file containing only the translated tag rows (so still including the english Tag name and tag type). Will be matched to the English tags in the main file based on the name & type, so might be slow for large translation files.
3. As an extra file with `onlyTranslationExtraFile` true. With this configuration, the extra file has to include *only* the translation itself. That means it is purely index based, assigning the translations 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.
So your CSV values would look like this for each method:
| | 1 | 2 | 3 |
|------------|---------------------|--------------------|---------------|
| Main file | `tag,0,translation` | `tag,0` | `tag,0` |
| Extra file | - | `tag,0,translation`| `translation` |
Methods 1 & 2 can also be mixed, in which case translations in the extra file will have priority over those in the main file if they translate the same tag.
The extra files can also be used to just add new / custom tags not included in the main set, provided `onlyTranslationExtraFile` is false.
If an extra tag doesn't match any existing tag, it will be added to the list as a new tag instead.
## CSV tag data
The script expects a CSV file with tags saved in the following way:
```csv
1girl,0
@@ -88,7 +157,7 @@ highres,5
long_hair,0
```
Notably, it does not expect column names in the first row.
The first value needs to be the tag name, while the second value specifies the tag type.
The first value needs to be the tag name, while the second value specifies the tag type. An optional third value will be interpreted as a translation as described in the section above.
The numbering system follows the [tag API docs](https://danbooru.donmai.us/wiki_pages/api%3Atags) of Danbooru:
| Value | Description |
|-------|-------------|

166
README_ZH.md Normal file
View File

@@ -0,0 +1,166 @@
# Booru tag autocompletion for A1111
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/DominikDoom/a1111-sd-webui-tagcomplete)](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)
## [English Document](./README.md)
## 功能概述
本脚本为 [AUTOMATIC1111 web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui)的自定义脚本,能在输入Tag时提供booru风格如Danbooru的TAG自动补全。因为有一些模型是基于这种TAG风格训练的例如[Waifu Diffusion](https://github.com/harubaru/waifu-diffusion)因此使用这些Tag能获得较为精确的效果。
这个脚本的创建是为了减少因复制Tag在Web UI和 booru网站的反复切换。
你可以按照[以下方法](#installation)下载或拷贝文件,也可以使用[Releases](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)中打包好的文件。
## 常见问题 & 已知缺陷:
* 浏览器可能因为缓存无法更新脚本、设置、embedding/wildcard列表尝试使用`CRTL+F5`清空浏览器缓存并重新加载
-`replaceUnderscores`选项开启时, 脚本只会替换Tag的一部分如果Tag包含多个单词,比如将`atago (azur lane)`修改`atago``taihou`并使用自动补全时.会得到 `taihou (azur lane), lane)`的结果, 因为脚本没有把后面的部分认为成同一个Tag。
## [Wildcard](https://github.com/jtkelm2/stable-diffusion-webui-1/blob/master/scripts/wildcards.py) & Embedding 支持
自动补全同样适用于 [Wildcard](https://github.com/jtkelm2/stable-diffusion-webui-1/blob/master/scripts/wildcards.py)中所述的通配符文件(后面有演示视频)。这将使你能够插入Wildcard脚本需要的通配符更进一步的你还可以插入通配符文件内的某个具体Tag。
当输入`__`字符时,`/scripts/wildcards`文件夹下的通配符文件会列出到自动补全,当你选择某个具体通配符文件时,会列出其中的所有的具体Tag,但如果你仅需要选择某个通配符,请按下空格。
当输入`<`字符时,`embeddings`文件夹下的`.pt``.bin`文件会列出到自动完成。需要注意的是,一些颜文字也会包含`<`(比如`>_<`),所以它们也会出现在结果中。
现在这项功能默认是启用的,并会自动扫描`/embeddings``/scripts/wildcards`文件夹,不再需要使用`tags/wildcardNames.txt`文件了,早期版本的用户可以将它删除。
## 演示与截图
演示视频(使用了键盘导航):
https://user-images.githubusercontent.com/34448969/195344430-2b5f9945-b98b-4943-9fbc-82cf633321b1.mp4
Wildcard支持演示:
https://user-images.githubusercontent.com/34448969/195632461-49d226ae-d393-453d-8f04-1e44b073234c.mp4
深浅色主题支持,包括Tag的颜色:
![tagtypes](https://user-images.githubusercontent.com/34448969/195177127-f63949f8-271d-4767-bccd-f1b5e818a7f8.png)
![tagtypes_light](https://user-images.githubusercontent.com/34448969/195180061-ceebcc25-9e4c-424f-b0c9-ba8e8f4f17f4.png)
## 安装
只需要将`javascript``scripts``tags`文件夹复制到你的Web UI安装根目录下.下次启动Web UI时它将自动启动。
`tags`文件夹下包含`config.json`用于设置和Tag数据.csv格式。默认情况下Tag数据包括`Danbooru.csv``e621.csv`
在扫描过`/embeddings``/scripts/wildcards`后,会将列表存放在`tags/temp`文件夹下。删除该文件夹不会有任何影响,下次启动时它会重新创建。
### 注意:
本脚本的允许需要**全部的三个文件夹**。
## 配置文件
配置文件config.json的默认值如下
```json
{
"tagFile": "danbooru.csv",
"activeIn": {
"txt2img": true,
"img2img": true,
"negativePrompts": true
},
"maxResults": 5,
"showAllResults": false,
"replaceUnderscores": true,
"escapeParentheses": true,
"useWildcards": true,
"useEmbeddings": true,
"translation": {
"searchByTranslation": true,
"onlyShowTranslation": false
},
"extra": {
"extraFile": "",
"onlyTranslationExtraFile": false
},
"colors": {
"danbooru": {
"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"]
}
}
}
```
| 设置 | 描述 |
|---------|-------------|
| tagFile | 指定要使用的标记文件。您可以提供您喜欢的自定义标签数据库,但由于该脚本是在考虑 Danbooru 标签的情况下开发的,因此它可能无法与其他配置一起正常工作。|
| activeIn | 允许有选择地(取消)激活 txt2img、img2img 和两者的否定提示的脚本。|
| maxResults | 最多显示多少个结果。对于默认标记集,结果按出现次数排序。对于嵌入和通配符,它​​将在可滚动列表中显示所有结果。 |
| showAllResults | 如果为真,将忽略 maxResults 并在可滚动列表中显示所有结果。 **警告:**对于长列表,您的浏览器可能会滞后。 |
| replaceUnderscores | 如果为 true则在单击标签时将取消划线替换为空格。对于某些型号可能会更好。|
| escapeParentheses | 如果为 true则转义包含 () 的标签,因此它们不会对 Web UI 的提示权重功能做出贡献。 |
| useWildcards | 用于切换通配符完成功能。 |
| useEmbeddings | 用于切换嵌入完成功能。 |
| translation | 用于翻译标签的选项。更多信息在下面的部分。 |
| extras | 附加标签文件/翻译的选项。更多信息在下面的部分。|
| colors | 包含标签类型的可自定义颜色,您可以在此处为自定义标签文件添加新颜色(与文件名相同,不带 .csv。第一个值是暗模式第二个值是亮模式。颜色名称和十六进制代码都应该有效。|
## 翻译&新增Tag
通过最近的更新,现在可以为标签添加翻译。这些将根据 `config.json` 中的设置可搜索/显示:
- `searchByTranslation` - 是同时搜索翻译词还是仅搜索英文标签。
- `onlyShowTranslation` - 如果有英文标签,则用其翻译替换它。仅用于显示,最后插入的文本仍然是英文标签。
完整和部分中文标签集的示例:
![translation](https://user-images.githubusercontent.com/34448969/196175839-8aaacb26-5c90-48e3-be65-647a0b444ead.png)
![translation_mixed](https://user-images.githubusercontent.com/34448969/196176233-76d4cb5f-16cf-4800-a69b-adb64a79ca8b.png)
可以通过多种方式添加翻译,这就是额外文件发挥作用的地方。
1. 直接在主标签文件中。只需添加第三个值,用逗号分隔,包含该行中标签的翻译。
2. 作为仅包含已翻译标签行的额外文件(因此仍包括英文标签名称和标签类型)。将根据名称和类型与主文件中的英文标签匹配,因此对于大型翻译文件可能会很慢。
3. 作为 `onlyTranslationExtraFile` 为 true 的额外文件。使用此配置,额外文件必须包含*仅*翻译本身。这意味着它完全基于索引,将翻译分配给主要标签非常快,但也需要匹配行(包括空行)。如果主文件中的顺序或数量发生变化,则翻译可能不再匹配。
因此,对于每种方法,您的 CSV 值将如下所示:
| | 1 | 2 | 3 |
|------------|---------------------|--------------------|---------------|
| Main file | `tag,0,translation` | `tag,0` | `tag,0` |
| Extra file | - | `tag,0,translation`| `translation` |
方法 1 和 2 也可以混合使用,在这种情况下,如果它们翻译相同的标签,额外文件中的翻译将优先于主文件中的翻译。
如果 `onlyTranslationExtraFile` 为 false额外文件也可用于添加未包含在主集中的新/自定义标签。
如果额外的标签与任何现有标签都不匹配,它将作为新标签添加到列表中。
### CSV tag data
本脚本的Tag文件格式如下你可以安装这个格式制作自己的Tag文件:
```csv
1girl,0
solo,0
highres,5
long_hair,0
```
值得注意的是,它不希望第一行中有列名。
第一个值需要是标签名称,而第二个值指定标签类型。
编号系统遵循 Danbooru 的 [tag API docs](https://danbooru.donmai.us/wiki_pages/api%3Atags):
| Value | Description |
|-------|-------------|
|0 | General |
|1 | Artist |
|3 | Copyright |
|4 | Character |
|5 | Meta |
or of e621:
| Value | Description |
|-------|-------------|
|-1 | Invalid |
|0 | General |
|1 | Artist |
|3 | Copyright |
|4 | Character |
|5 | Species |
|6 | Invalid |
|7 | Meta |
|8 | Lore |
标记类型用于为结果列表中的条目着色.

View File

@@ -0,0 +1,749 @@
var acConfig = null;
var acActive = true;
// Style for new elements. Gets appended to the Gradio root.
let autocompleteCSS_dark = `
.autocompleteResults {
position: absolute;
z-index: 999;
margin: 5px 0 0 0;
background-color: #0b0f19 !important;
border: 1px solid #4b5563 !important;
border-radius: 12px !important;
overflow-y: auto;
}
.autocompleteResultsList > li:nth-child(odd) {
background-color: #111827;
}
.autocompleteResultsList > li {
list-style-type: none;
padding: 10px;
cursor: pointer;
}
.autocompleteResultsList > li:hover {
background-color: #1f2937;
}
.autocompleteResultsList > li.selected {
background-color: #374151;
}
`;
let autocompleteCSS_light = `
.autocompleteResults {
position: absolute;
z-index: 999;
margin: 5px 0 0 0;
background-color: #ffffff !important;
border: 1.5px solid #e5e7eb !important;
border-radius: 12px !important;
overflow-y: auto;
}
.autocompleteResultsList > li:nth-child(odd) {
background-color: #f9fafb;
}
.autocompleteResultsList > li {
list-style-type: none;
padding: 10px;
cursor: pointer;
}
.autocompleteResultsList > li:hover {
background-color: #f5f6f8;
}
.autocompleteResultsList > li.selected {
background-color: #e5e7eb;
}
`;
// Parse the CSV file into a 2D array. Doesn't use regex, so it is very lightweight.
function parseCSV(str) {
var arr = [];
var quote = false; // 'true' means we're inside a quoted field
// Iterate over each character, keep track of current row and column (of the returned array)
for (var row = 0, col = 0, c = 0; c < str.length; c++) {
var cc = str[c], nc = str[c + 1]; // Current character, next character
arr[row] = arr[row] || []; // Create a new row if necessary
arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary
// If the current character is a quotation mark, and we're inside a
// quoted field, and the next character is also a quotation mark,
// add a quotation mark to the current column and skip the next character
if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }
// If it's just one quotation mark, begin/end quoted field
if (cc == '"') { quote = !quote; continue; }
// If it's a comma and we're not in a quoted field, move on to the next column
if (cc == ',' && !quote) { ++col; continue; }
// If it's a newline (CRLF) and we're not in a quoted field, skip the next character
// and move on to the next row and move to column 0 of that new row
if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; }
// If it's a newline (LF or CR) and we're not in a quoted field,
// move on to the next row and move to column 0 of that new row
if (cc == '\n' && !quote) { ++row; col = 0; continue; }
if (cc == '\r' && !quote) { ++row; col = 0; continue; }
// Otherwise, append the current character to the current column
arr[row][col] += cc;
}
return arr;
}
// Load file
function readFile(filePath) {
return new Promise(function (resolve, reject) {
let request = new XMLHttpRequest();
request.open("GET", filePath, true);
request.onload = function () {
var status = request.status;
if (status == 200) {
resolve(request.responseText);
} else {
reject(status);
}
};
request.send(null);
});
}
// Load CSV
async function loadCSV(path) {
let text = await readFile(path);
return parseCSV(text);
}
// Debounce function to prevent spamming the autocomplete function
var dbTimeOut;
const debounce = (func, wait = 300) => {
return function (...args) {
if (dbTimeOut) {
clearTimeout(dbTimeOut);
}
dbTimeOut = setTimeout(() => {
func.apply(this, args);
}, wait);
}
}
// Difference function to fix duplicates not being seen as changes in normal filter
function difference(a, b) {
if (a.length == 0) {
return b;
}
if (b.length == 0) {
return a;
}
return [...b.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) - 1),
a.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map())
)].reduce((acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), []);
}
// Get the identifier for the text area to differentiate between positive and negative
function getTextAreaIdentifier(textArea) {
let txt2img_p = gradioApp().querySelector('#txt2img_prompt > label > textarea');
let txt2img_n = gradioApp().querySelector('#txt2img_neg_prompt > label > textarea');
let img2img_p = gradioApp().querySelector('#img2img_prompt > label > textarea');
let img2img_n = gradioApp().querySelector('#img2img_neg_prompt > label > textarea');
let modifier = "";
switch (textArea) {
case txt2img_p:
modifier = ".txt2img.p";
break;
case txt2img_n:
modifier = ".txt2img.n";
break;
case img2img_p:
modifier = ".img2img.p";
break;
case img2img_n:
modifier = ".img2img.n";
break;
default:
break;
}
return modifier;
}
// Create the result list div and necessary styling
function createResultsDiv(textArea) {
let resultsDiv = document.createElement("div");
let resultsList = document.createElement('ul');
let textAreaId = getTextAreaIdentifier(textArea);
let typeClass = textAreaId.replaceAll(".", " ");
resultsDiv.style.setProperty("max-height", acConfig.maxResults * 50 + "px");
resultsDiv.setAttribute('class', `autocompleteResults ${typeClass}`);
resultsList.setAttribute('class', 'autocompleteResultsList');
resultsDiv.appendChild(resultsList);
return resultsDiv;
}
// Create the checkbox to enable/disable autocomplete
function createCheckbox() {
let label = document.createElement("label");
let input = document.createElement("input");
let span = document.createElement("span");
label.setAttribute('id', 'acActiveCheckbox');
label.setAttribute('class', '"flex items-center text-gray-700 text-sm rounded-lg cursor-pointer dark:bg-transparent');
input.setAttribute('type', 'checkbox');
input.setAttribute('class', 'gr-check-radio gr-checkbox')
span.setAttribute('class', 'ml-2');
span.textContent = "Enable Autocomplete";
label.appendChild(input);
label.appendChild(span);
return label;
}
// The selected tag index. Needs to be up here so hide can access it.
var selectedTag = null;
var previousTags = [];
// Show or hide the results div
function isVisible(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
return resultsDiv.style.display === "block";
}
function showResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
resultsDiv.style.display = "block";
}
function hideResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
resultsDiv.style.display = "none";
selectedTag = null;
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
const TAG_REGEX = /[([]([^,()[\]:| ]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]|((?:\b|<)[^,|\n\r ]*(?:>|\b)?)/g
let hideBlocked = false;
// On click, insert the tag into the prompt textbox with respect to the cursor position
function insertTextAtCursor(textArea, result, tagword) {
let text = result[0];
let tagType = result[1];
let cursorPos = textArea.selectionStart;
var sanitizedText = text
// Replace differently depending on if it's a tag or wildcard
if (tagType === "wildcardFile") {
sanitizedText = "__" + text.replace("Wildcards: ", "") + "__";
} else if (tagType === "wildcardTag") {
sanitizedText = text.replace(/^.*?: /g, "");
} else if (tagType === "embedding") {
sanitizedText = `<${text.replace(/^.*?: /g, "")}>`;
} else {
sanitizedText = acConfig.replaceUnderscores ? text.replaceAll("_", " ") : text;
}
if (acConfig.escapeParentheses) {
sanitizedText = sanitizedText
.replaceAll("(", "\\(")
.replaceAll(")", "\\)")
.replaceAll("[", "\\[")
.replaceAll("]", "\\]");
}
var prompt = textArea.value;
// Edit prompt text
let editStart = Math.max(cursorPos - tagword.length, 0);
let editEnd = Math.min(cursorPos + tagword.length, prompt.length);
let surrounding = prompt.substring(editStart, editEnd);
let match = surrounding.match(new RegExp(escapeRegExp(`${tagword}`), "i"));
let afterInsertCursorPos = editStart + match.index + sanitizedText.length;
var optionalComma = "";
if (tagType !== "wildcardFile") {
optionalComma = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null ? "" : ", ";
}
// Replace partial tag word with new text, add comma if needed
let insert = surrounding.replace(match, sanitizedText + optionalComma);
// Add back start
var newPrompt = prompt.substring(0, editStart) + insert + prompt.substring(editEnd);
textArea.value = newPrompt;
textArea.selectionStart = afterInsertCursorPos + optionalComma.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 }));
// Update previous tags with the edited prompt to prevent re-searching the same term
let tags = [...newPrompt.matchAll(TAG_REGEX)]
.filter(x => x[2] !== undefined ? x[2].length > 0 : x)
.flatMap(x => x[1] || x[2])
previousTags = tags;
// Hide results after inserting
if (tagType === "wildcardFile") {
// If it's a wildcard, we want to keep the results open so the user can select another wildcard
hideBlocked = true;
autocomplete(textArea, prompt, sanitizedText);
setTimeout(() => { hideBlocked = false; }, 100);
} else {
hideResults(textArea);
}
}
function addResultsToList(textArea, results, tagword, resetList) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
let resultsList = resultDiv.querySelector('ul');
// Reset list, selection and scrollTop since the list changed
if (resetList) {
resultsList.innerHTML = "";
selectedTag = null;
resultDiv.scrollTop = 0;
resultCount = 0;
}
// Find right colors from config
let tagFileName = acConfig.tagFile.split(".")[0];
let tagColors = acConfig.colors;
let mode = gradioApp().querySelector('.dark') ? 0 : 1;
let nextLength = Math.min(results.length, resultCount + acConfig.resultStepLength);
for (let i = resultCount; i < nextLength; i++) {
let result = results[i];
let li = document.createElement("li");
//suppost only show the translation to result
if (result[2]) {
li.textContent = result[2];
if (!acConfig.translation.onlyShowTranslation) {
li.textContent += " >> " + result[0];
}
} else {
li.textContent = result[0];
}
// Wildcards & Embeds have no tag type
if (!result[1].startsWith("wildcard") && result[1] !== "embedding") {
// Set the color of the tag
let tagType = result[1];
let colorGroup = tagColors[tagFileName];
// Default to danbooru scheme if no matching one is found
if (colorGroup === undefined) colorGroup = tagColors["danbooru"];
li.style = `color: ${colorGroup[tagType][mode]};`;
}
// Add listener
li.addEventListener("click", function () { insertTextAtCursor(textArea, result, tagword); });
// Add element to list
resultsList.appendChild(li);
}
resultCount = nextLength;
}
function updateSelectionStyle(textArea, newIndex, oldIndex) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
let resultsList = resultDiv.querySelector('ul');
let items = resultsList.getElementsByTagName('li');
if (oldIndex != null) {
items[oldIndex].classList.remove('selected');
}
// make it safer
if (newIndex !== null) {
items[newIndex].classList.add('selected');
}
// Set scrolltop to selected item if we are showing more than max results
if (items.length > acConfig.maxResults) {
let selected = items[newIndex];
resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
}
}
var wildcardFiles = [];
var wildcardExtFiles = [];
var embeddings = [];
var allTags = [];
var results = [];
var tagword = "";
var resultCount = 0;
async function autocomplete(textArea, prompt, fixedTag = null) {
// Return if the function is deactivated in the UI
if (!acActive) return;
// Guard for empty prompt
if (prompt.length === 0) {
hideResults(textArea);
return;
}
if (fixedTag === null) {
// Match tags with RegEx to get the last edited one
// We also match for the weighting format (e.g. "tag:1.0") here, and flatMap it to just the tag part if it exists
let tags = [...prompt.matchAll(TAG_REGEX)]
.filter(x => x[2] !== undefined ? x[2].length > 0 : x)
.flatMap(x => x[1] || x[2])
let diff = difference(tags, previousTags)
previousTags = tags;
// Guard for no difference / only whitespace remaining
if (diff === null || diff.length === 0) {
if (!hideBlocked) hideResults(textArea);
return;
}
tagword = diff[0]
// Guard for empty tagword
if (tagword === null || tagword.length === 0) {
hideResults(textArea);
return;
}
} else {
tagword = fixedTag;
}
tagword = tagword.toLowerCase().replace(/[\n\r]/g, "");
if (acConfig.useWildcards && [...tagword.matchAll(/\b__([^, ]+)__([^, ]*)\b/g)].length > 0) {
// Show wildcards from a file with that name
wcMatch = [...tagword.matchAll(/\b__([^, ]+)__([^, ]*)\b/g)]
let wcFile = wcMatch[0][1];
let wcWord = wcMatch[0][2];
var wcPair;
// Look in normal wildcard files
if (wcFound = wildcardFiles.find(x => x[1].toLowerCase() === wcFile))
wcPair = wcFound;
else // Look in extensions wildcard files
wcPair = wildcardExtFiles.find(x => x[1].toLowerCase() === wcFile);
let wildcards = (await readFile(`file/${wcPair[0]}/${wcPair[1]}.txt`)).split("\n")
.filter(x => x.trim().length > 0); // Remove empty lines
results = wildcards.filter(x => (wcWord !== null && wcWord.length > 0) ? x.toLowerCase().includes(wcWord) : x) // Filter by tagword
.map(x => [wcFile + ": " + x.trim(), "wildcardTag"]); // Mark as wildcard
} else if (acConfig.useWildcards && (tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__")) {
// Show available wildcard files
let tempResults = [];
if (tagword !== "__") {
let lmb = (x) => x[1].toLowerCase().includes(tagword.replace("__", ""))
tempResults = wildcardFiles.filter(lmb).concat(wildcardExtFiles.filter(lmb)) // Filter by tagword
} else {
tempResults = wildcardFiles.concat(wildcardExtFiles);
}
results = tempResults.map(x => ["Wildcards: " + x[1].trim(), "wildcardFile"]); // Mark as wildcard
} else if (acConfig.useEmbeddings && tagword.match(/<[^,> ]*>?/g)) {
// Show embeddings
let tempResults = [];
if (tagword !== "<") {
tempResults = embeddings.filter(x => x.toLowerCase().includes(tagword.replace("<", ""))) // Filter by tagword
} else {
tempResults = embeddings;
}
// Since some tags are kaomoji, we have to still get the normal results first.
genericResults = allTags.filter(x => x[0].toLowerCase().includes(tagword)).slice(0, acConfig.maxResults);
results = genericResults.concat(tempResults.map(x => ["Embeddings: " + x.trim(), "embedding"])); // Mark as embedding
} else {
if (acConfig.translation.searchByTranslation) {
results = allTags.filter(x => x[2] && x[2].toLowerCase().includes(tagword)); // check have translation
// if search by [a~z],first list the translations, and then search English if it is not enough
// if only show translation,it is unnecessary to list English results
if (!acConfig.translation.onlyShowTranslation) {
results = results.concat(allTags.filter(x => x[0].toLowerCase().includes(tagword) && !results.includes(x)));
}
} else {
results = allTags.filter(x => x[0].toLowerCase().includes(tagword));
}
// it's good to show all results
if (!acConfig.showAllResults) {
results = results.slice(0, acConfig.maxResults);
}
}
// Guard for empty results
if (!results.length) {
hideResults(textArea);
return;
}
showResults(textArea);
addResultsToList(textArea, results, tagword, true);
}
var oldSelectedTag = null;
function navigateInList(textArea, event) {
// Return if the function is deactivated in the UI
if (!acActive) return;
validKeys = ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", "Enter", "Tab", "Escape"];
if (acConfig.useLeftRightArrowKeys)
validKeys.push("ArrowLeft", "ArrowRight");
if (!validKeys.includes(event.key)) return;
if (!isVisible(textArea)) return
// Return if ctrl key is pressed to not interfere with weight editing shortcut
if (event.ctrlKey || event.altKey) return;
oldSelectedTag = selectedTag;
switch (event.key) {
case "ArrowUp":
if (selectedTag === null) {
selectedTag = resultCount - 1;
} else {
selectedTag = (selectedTag - 1 + resultCount) % resultCount;
}
break;
case "ArrowDown":
if (selectedTag === null) {
selectedTag = 0;
} else {
selectedTag = (selectedTag + 1) % resultCount;
}
break;
case "PageUp":
if (selectedTag === null || selectedTag === 0) {
selectedTag = resultCount - 1;
} else {
selectedTag = (Math.max(selectedTag - 5, 0) + resultCount) % resultCount;
}
break;
case "PageDown":
if (selectedTag === null || selectedTag === resultCount - 1) {
selectedTag = 0;
} else {
selectedTag = Math.min(selectedTag + 5, resultCount - 1) % resultCount;
}
break;
case "Home":
selectedTag = 0;
break;
case "End":
selectedTag = resultCount - 1;
break;
case "ArrowLeft":
selectedTag = 0;
break;
case "ArrowRight":
selectedTag = resultCount - 1;
break;
case "Enter":
if (selectedTag !== null) {
insertTextAtCursor(textArea, results[selectedTag], tagword);
}
break;
case "Tab":
if (selectedTag === null) {
selectedTag = 0;
}
insertTextAtCursor(textArea, results[selectedTag], tagword);
break;
case "Escape":
hideResults(textArea);
break;
}
if (selectedTag === resultCount - 1
&& (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "ArrowLeft" || event.key === "ArrowRight")) {
addResultsToList(textArea, results, tagword, false);
}
// Update highlighting
if (selectedTag !== null)
updateSelectionStyle(textArea, selectedTag, oldSelectedTag);
// Prevent default behavior
event.preventDefault();
event.stopPropagation();
}
var styleAdded = false;
onUiUpdate(async function () {
// Get our tag base path from the temp file
let tagBasePath = await readFile("file/tmp/tagAutocompletePath.txt");
// Load config
if (acConfig === null) {
try {
acConfig = JSON.parse(await readFile(`file/${tagBasePath}/config.json`));
if (acConfig.translation.onlyShowTranslation) {
acConfig.translation.searchByTranslation = true; // if only show translation, enable search by translation is necessary
}
} catch (e) {
console.error("Error loading config.json: " + e);
return;
}
}
// Load main tags and translations
if (allTags.length === 0) {
try {
allTags = await loadCSV(`file/${tagBasePath}/${acConfig.tagFile}`);
} catch (e) {
console.error("Error loading tags file: " + e);
return;
}
if (acConfig.extra.extraFile) {
try {
extras = await loadCSV(`file/${tagBasePath}/${acConfig.extra.extraFile}`);
if (acConfig.extra.onlyTranslationExtraFile) {
// This works purely on index, so it's not very robust. But a lot faster.
for (let i = 0, n = extras.length; i < n; i++) {
if (extras[i][0]) {
allTags[i][2] = extras[i][0];
}
}
} else {
extras.forEach(e => {
// Check if a tag in allTags has the same name as the extra tag
if (tag = allTags.find(t => t[0] === e[0] && t[1] == e[1])) {
if (e[2]) // If the extra tag has a translation, add it to the tag
tag[2] = e[2];
} else {
// If the tag doesn't exist, add it to allTags
allTags.push(e);
}
});
}
} catch (e) {
console.error("Error loading extra translation file: " + e);
return;
}
}
}
// Load wildcards
if (wildcardFiles.length === 0 && acConfig.useWildcards) {
try {
let wcFileArr = (await readFile(`file/${tagBasePath}/temp/wc.txt`)).split("\n");
let wcBasePath = wcFileArr[0].trim(); // First line should be the base path
wildcardFiles = wcFileArr.slice(1)
.filter(x => x.trim().length > 0) // Remove empty lines
.map(x => [wcBasePath, x.trim().replace(".txt", "")]); // Remove file extension & newlines
// To support multiple sources, we need to separate them using the provided "-----" strings
let wcExtFileArr = (await readFile(`file/${tagBasePath}/temp/wce.txt`)).split("\n");
let splitIndices = [];
for (let index = 0; index < wcExtFileArr.length; index++) {
if (wcExtFileArr[index].trim() === "-----") {
splitIndices.push(index);
}
}
// For each group, add them to the wildcardFiles array with the base path as the first element
for (let i = 0; i < splitIndices.length; i++) {
let start = splitIndices[i - 1] || 0;
if (i > 0) start++; // Skip the "-----" line
let end = splitIndices[i];
let wcExtFile = wcExtFileArr.slice(start, end);
let base = wcExtFile[0].trim() + "/";
wcExtFile = wcExtFile.slice(1)
.filter(x => x.trim().length > 0) // Remove empty lines
.map(x => x.trim().replace(base, "").replace(".txt", "")); // Remove file extension & newlines;
wcExtFile = wcExtFile.map(x => [base, x]);
wildcardExtFiles.push(...wcExtFile);
}
} catch (e) {
console.error("Error loading wildcards: " + e);
}
}
// Load embeddings
if (embeddings.length === 0 && acConfig.useEmbeddings) {
try {
embeddings = (await readFile(`file/${tagBasePath}/temp/emb.txt`)).split("\n")
.filter(x => x.trim().length > 0) // Remove empty lines
.map(x => x.replace(".bin", "").replace(".pt", "").replace(".png", "")); // Remove file extensions
} catch (e) {
console.error("Error loading embeddings.txt: " + e);
}
}
// Find all textareas
let txt2imgTextArea = gradioApp().querySelector('#txt2img_prompt > label > textarea');
let img2imgTextArea = gradioApp().querySelector('#img2img_prompt > label > textarea');
let txt2imgTextArea_n = gradioApp().querySelector('#txt2img_neg_prompt > label > textarea');
let img2imgTextArea_n = gradioApp().querySelector('#img2img_neg_prompt > label > textarea');
let textAreas = [txt2imgTextArea, img2imgTextArea, txt2imgTextArea_n, img2imgTextArea_n];
let quicksettings = gradioApp().querySelector('#quicksettings');
// Not found, we're on a page without prompt textareas
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') || !acConfig.activeIn.negativePrompts) {
return;
}
} else if (!acConfig.activeIn.txt2img && !acConfig.activeIn.img2img) {
return;
}
textAreas.forEach(area => {
// Return if autocomplete is disabled for the current area type in config
let textAreaId = getTextAreaIdentifier(area);
if ((!acConfig.activeIn.img2img && textAreaId.includes("img2img"))
|| (!acConfig.activeIn.txt2img && textAreaId.includes("txt2img"))
|| (!acConfig.activeIn.negativePrompts && textAreaId.includes("n"))) {
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), acConfig.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));
// Add class so we know we've already added the listeners
area.classList.add('autocomplete');
}
});
if (gradioApp().querySelector("#acActiveCheckbox") === null) {
// Add toggle switch
let cb = createCheckbox();
cb.querySelector("input").checked = acActive;
cb.querySelector("input").addEventListener("change", (e) => {
acActive = e.target.checked;
});
quicksettings.parentNode.insertBefore(cb, quicksettings.nextSibling);
}
if (styleAdded) return;
// Add style to dom
let acStyle = document.createElement('style');
let css = gradioApp().querySelector('.dark') ? autocompleteCSS_dark : autocompleteCSS_light;
if (acStyle.styleSheet) {
acStyle.styleSheet.cssText = css;
} else {
acStyle.appendChild(document.createTextNode(css));
}
gradioApp().appendChild(acStyle);
styleAdded = true;
});

View File

@@ -0,0 +1,115 @@
# This helper script scans folders for wildcards and embeddings and writes them
# to a temporary file to expose it to the javascript side
from pathlib import Path
from modules import scripts
# Webui root path
FILE_DIR = Path().absolute()
# The extension base path
EXT_PATH = FILE_DIR.joinpath('extensions')
# Tags base path
def get_tags_base_path():
script_path = Path(scripts.basedir())
if (script_path.is_relative_to(EXT_PATH)):
return script_path.joinpath('tags')
else:
return FILE_DIR.joinpath('tags')
TAGS_PATH = get_tags_base_path()
# The path to the folder containing the wildcards and embeddings
WILDCARD_PATH = FILE_DIR.joinpath('scripts/wildcards')
EMB_PATH = FILE_DIR.joinpath('embeddings')
def find_ext_wildcard_paths():
"""Returns the path to the extension wildcards folder"""
found = list(EXT_PATH.glob('*/wildcards/'))
return found
# The path to the extension wildcards folder
WILDCARD_EXT_PATHS = find_ext_wildcard_paths()
# The path to the temporary files
STATIC_TEMP_PATH = FILE_DIR.joinpath('tmp') # In the webui root, on windows it exists by default, on linux it doesn't
TEMP_PATH = TAGS_PATH.joinpath('temp') # Extension specific temp files
def get_wildcards():
"""Returns a list of all wildcards. Works on nested folders."""
wildcard_files = list(WILDCARD_PATH.rglob("*.txt"))
resolved = [w.relative_to(WILDCARD_PATH).as_posix(
) for w in wildcard_files if w.name != "put wildcards here.txt"]
return resolved
def get_ext_wildcards():
"""Returns a list of all extension wildcards. Works on nested folders."""
wildcard_files = []
for path in WILDCARD_EXT_PATHS:
wildcard_files.append(path.relative_to(FILE_DIR).as_posix())
wildcard_files.extend(p.relative_to(path).as_posix() for p in path.rglob("*.txt") if p.name != "put wildcards here.txt")
wildcard_files.append("-----")
return wildcard_files
def get_embeddings():
"""Returns a list of all embeddings"""
return [str(e.relative_to(EMB_PATH)) for e in EMB_PATH.glob("**/*") if e.suffix in {".bin", ".pt", ".png"}]
def write_tag_base_path():
"""Writes the tag base path to a fixed location temporary file"""
with open(STATIC_TEMP_PATH.joinpath('tagAutocompletePath.txt'), 'w', encoding="utf-8") as f:
f.write(TAGS_PATH.relative_to(FILE_DIR).as_posix())
def write_to_temp_file(name, data):
"""Writes the given data to a temporary file"""
with open(TEMP_PATH.joinpath(name), 'w', encoding="utf-8") as f:
f.write(('\n'.join(data)))
# Write the tag base path to a fixed location temporary file
# to enable the javascript side to find our files regardless of extension folder name
if not STATIC_TEMP_PATH.exists():
STATIC_TEMP_PATH.mkdir(exist_ok=True)
write_tag_base_path()
# Check if the temp path exists and create it if not
if not TEMP_PATH.exists():
TEMP_PATH.mkdir(parents=True, exist_ok=True)
# Set up files to ensure the script doesn't fail to load them
# even if no wildcards or embeddings are found
write_to_temp_file('wc.txt', [])
write_to_temp_file('wce.txt', [])
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 embeddings to emb.txt if found
if EMB_PATH.exists():
embeddings = get_embeddings()
if embeddings:
write_to_temp_file('emb.txt', embeddings)

View File

@@ -1,521 +0,0 @@
var acConfig = null;
// Style for new elements. Gets appended to the Gradio root.
let autocompleteCSS_dark = `
.autocompleteResults {
position: absolute;
z-index: 999;
margin: 5px 0 0 0;
background-color: #0b0f19 !important;
border: 1px solid #4b5563 !important;
border-radius: 12px !important;
overflow-y: auto;
}
.autocompleteResultsList > li:nth-child(odd) {
background-color: #111827;
}
.autocompleteResultsList > li {
list-style-type: none;
padding: 10px;
cursor: pointer;
}
.autocompleteResultsList > li:hover {
background-color: #1f2937;
}
.autocompleteResultsList > li.selected {
background-color: #374151;
}
`;
let autocompleteCSS_light = `
.autocompleteResults {
position: absolute;
z-index: 999;
margin: 5px 0 0 0;
background-color: #ffffff !important;
border: 1.5px solid #e5e7eb !important;
border-radius: 12px !important;
overflow-y: auto;
}
.autocompleteResultsList > li:nth-child(odd) {
background-color: #f9fafb;
}
.autocompleteResultsList > li {
list-style-type: none;
padding: 10px;
cursor: pointer;
}
.autocompleteResultsList > li:hover {
background-color: #f5f6f8;
}
.autocompleteResultsList > li.selected {
background-color: #e5e7eb;
}
`;
// Parse the CSV file into a 2D array. Doesn't use regex, so it is very lightweight.
function parseCSV(str) {
var arr = [];
var quote = false; // 'true' means we're inside a quoted field
// Iterate over each character, keep track of current row and column (of the returned array)
for (var row = 0, col = 0, c = 0; c < str.length; c++) {
var cc = str[c], nc = str[c + 1]; // Current character, next character
arr[row] = arr[row] || []; // Create a new row if necessary
arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary
// If the current character is a quotation mark, and we're inside a
// quoted field, and the next character is also a quotation mark,
// add a quotation mark to the current column and skip the next character
if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }
// If it's just one quotation mark, begin/end quoted field
if (cc == '"') { quote = !quote; continue; }
// If it's a comma and we're not in a quoted field, move on to the next column
if (cc == ',' && !quote) { ++col; continue; }
// If it's a newline (CRLF) and we're not in a quoted field, skip the next character
// and move on to the next row and move to column 0 of that new row
if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; }
// If it's a newline (LF or CR) and we're not in a quoted field,
// move on to the next row and move to column 0 of that new row
if (cc == '\n' && !quote) { ++row; col = 0; continue; }
if (cc == '\r' && !quote) { ++row; col = 0; continue; }
// Otherwise, append the current character to the current column
arr[row][col] += cc;
}
return arr;
}
// Load file
function readFile(filePath) {
let request = new XMLHttpRequest();
request.open("GET", filePath, false);
request.send(null);
return request.responseText;
}
function loadCSV() {
let text = readFile(`file/tags/${acConfig.tagFile}`);
return parseCSV(text);
}
// Debounce function to prevent spamming the autocomplete function
var dbTimeOut;
const debounce = (func, wait = 300) => {
return function (...args) {
if (dbTimeOut) {
clearTimeout(dbTimeOut);
}
dbTimeOut = setTimeout(() => {
func.apply(this, args);
}, wait);
}
}
// Difference function to fix duplicates not being seen as changes in normal filter
function difference(a, b) {
if (a.length == 0) {
return b;
}
if (b.length == 0) {
return a;
}
return [...b.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) - 1),
a.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map())
)].reduce((acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), []);
}
// Get the identifier for the text area to differentiate between positive and negative
function getTextAreaIdentifier(textArea) {
let txt2img_n = gradioApp().querySelector('#negative_prompt > label > textarea');
let img2img = gradioApp().querySelector('#tab_img2img');
let img2img_p = img2img.querySelector('#img2img_prompt > label > textarea');
let img2img_n = img2img.querySelector('#negative_prompt > label > textarea');
let modifier = "";
if (textArea === img2img_p || textArea === img2img_n) {
modifier += ".img2img";
}
if (textArea === txt2img_n || textArea === img2img_n) {
modifier += ".n";
} else {
modifier += ".p";
}
return modifier;
}
// Create the result list div and necessary styling
function createResultsDiv(textArea) {
let resultsDiv = document.createElement("div");
let resultsList = document.createElement('ul');
let textAreaId = getTextAreaIdentifier(textArea);
let typeClass = textAreaId.replaceAll(".", " ");
resultsDiv.style.setProperty("max-height", acConfig.maxResults * 50 + "px");
resultsDiv.setAttribute('class', `autocompleteResults ${typeClass}`);
resultsList.setAttribute('class', 'autocompleteResultsList');
resultsDiv.appendChild(resultsList);
return resultsDiv;
}
// The selected tag index. Needs to be up here so hide can access it.
var selectedTag = null;
// Show or hide the results div
function isVisible(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
return resultsDiv.style.display === "block";
}
function showResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
resultsDiv.style.display = "block";
}
function hideResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
resultsDiv.style.display = "none";
selectedTag = null;
}
let hideBlocked = false;
// On click, insert the tag into the prompt textbox with respect to the cursor position
function insertTextAtCursor(textArea, result, tagword) {
let text = result[0];
let tagType = result[1];
let cursorPos = textArea.selectionStart;
var sanitizedText = text
// Replace differently depending on if it's a tag or wildcard
if (tagType === "wildcardFile") {
sanitizedText = "__" + text.replace("Wildcards: ", "") + "__";
} else if (tagType === "wildcardTag") {
sanitizedText = text.replace(/^.*?: /g, "");
} else {
sanitizedText = acConfig.replaceUnderscores ? text.replaceAll("_", " ") : text;
}
sanitizedText = acConfig.escapeParentheses ? sanitizedText.replaceAll("(", "\\(").replaceAll(")", "\\)") : sanitizedText;
var prompt = textArea.value;
// Edit prompt text
let editStart = Math.max(cursorPos - tagword.length, 0);
let editEnd = Math.min(cursorPos + tagword.length, prompt.length);
let surrounding = prompt.substring(editStart, editEnd);
let insert = surrounding.replace(tagword, sanitizedText);
// Add back start
var newPrompt = prompt.substring(0, editStart) + insert;
// Add comma if needed
var optionalComma = "";
if (tagType !== "wildcardFile") {
optionalComma = surrounding.match(`/${tagword},/g`) !== null ? "" : ", ";
}
// Set selection after insertion
textArea.selectionStart = editStart + newPrompt.length + optionalComma.length;
textArea.selectionEnd = textArea.selectionStart;
// Add back end
newPrompt += optionalComma + prompt.substring(editEnd);
textArea.value = newPrompt;
// 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 }));
// Update previous tags with the edited prompt to prevent re-searching the same term
let tags = newPrompt.match(/[^, ]+/g);
previousTags = tags;
// Hide results after inserting
if (tagType === "wildcardFile") {
// If it's a wildcard, we want to keep the results open so the user can select another wildcard
hideBlocked = true;
autocomplete(textArea, prompt, sanitizedText);
setTimeout(() => { hideBlocked = false; }, 100);
} else {
hideResults(textArea);
}
}
function addResultsToList(textArea, results, tagword) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsList = gradioApp().querySelector('.autocompleteResults' + textAreaId + ' > ul');
resultsList.innerHTML = "";
// Find right colors from config
let tagFileName = acConfig.tagFile.split(".")[0];
let tagColors = acConfig.colors;
let mode = gradioApp().querySelector('.dark') ? 0 : 1;
for (let i = 0; i < results.length; i++) {
let result = results[i];
let li = document.createElement("li");
li.innerHTML = result[0];
// Wildcards have no tag type
if (!result[1].startsWith("wildcard")) {
// Set the color of the tag
let tagType = result[1];
let colorGroup = tagColors[tagFileName];
// Default to danbooru scheme if no matching one is found
if (colorGroup === undefined) colorGroup = tagColors["danbooru"];
li.style = `color: ${colorGroup[tagType][mode]};`;
}
// Add listener
li.addEventListener("click", function () { insertTextAtCursor(textArea, result, tagword); });
// Add element to list
resultsList.appendChild(li);
}
}
function updateSelectionStyle(textArea, num) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
let resultsList = resultDiv.querySelector('ul');
let items = resultsList.getElementsByTagName('li');
for (let i = 0; i < items.length; i++) {
items[i].classList.remove('selected');
}
items[num].classList.add('selected');
// Set scrolltop to selected item if we are showing more than max results
if (items.length > acConfig.maxResults) {
let selected = items[num];
resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
}
}
wildcardFiles = [];
wildcards = {};
allTags = [];
previousTags = [];
results = [];
tagword = "";
resultCount = 0;
function autocomplete(textArea, prompt, fixedTag = null) {
// Guard for empty prompt
if (prompt.length === 0) {
hideResults(textArea);
return;
}
if (fixedTag === null) {
// Match tags with RegEx to get the last edited one
let tags = prompt.match(/[^, ]+/g);
let diff = difference(tags, previousTags)
previousTags = tags;
// Guard for no difference / only whitespace remaining
if (diff === null || diff.length === 0) {
if (!hideBlocked) hideResults(textArea);
return;
}
tagword = diff[0]
// Guard for empty tagword
if (tagword === null || tagword.length === 0) {
hideResults(textArea);
return;
}
} else {
tagword = fixedTag;
}
tagword = tagword.toLowerCase();
if ([...tagword.matchAll(/\b__([^,_ ]+)__([^, ]*)\b/g)].length > 0 && acConfig.useWildcards) {
// Show wildcards from a file with that name
wcMatch = [...tagword.matchAll(/\b__([^,_ ]+)__([^, ]*)\b/g)]
let wcFile = wcMatch[0][1];
let wcWord = wcMatch[0][2];
results = wildcards[wcFile].filter(x => (wcWord !== null) ? x.toLowerCase().includes(wcWord) : x) // Filter by tagword
.map(x => [wcFile + ": " + x.trim(), "wildcardTag"]); // Mark as wildcard
} else if ((tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__") && acConfig.useWildcards) {
// Show available wildcard files
let tempResults = [];
if (tagword !== "__") {
tempResults = wildcardFiles.filter(x => x.toLowerCase().includes(tagword.replace("__", ""))) // Filter by tagword
} else {
tempResults = wildcardFiles;
}
results = tempResults.map(x => ["Wildcards: " + x.trim(), "wildcardFile"]); // Mark as wildcard
} else {
results = allTags.filter(x => x[0].toLowerCase().includes(tagword)).slice(0, acConfig.maxResults);
}
resultCount = results.length;
// Guard for empty results
if (resultCount === 0) {
hideResults(textArea);
return;
}
selectedTag = null; // Reset since the list changed
showResults(textArea);
addResultsToList(textArea, results, tagword);
}
function navigateInList(textArea, event) {
validKeys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Enter", "Escape"];
if (!validKeys.includes(event.key)) return;
if (!isVisible(textArea)) return
switch (event.key) {
case "ArrowUp":
if (selectedTag === null) {
selectedTag = resultCount - 1;
} else {
selectedTag = (selectedTag - 1 + resultCount) % resultCount;
}
break;
case "ArrowDown":
if (selectedTag === null) {
selectedTag = 0;
} else {
selectedTag = (selectedTag + 1) % resultCount;
}
break;
case "ArrowLeft":
selectedTag = 0;
break;
case "ArrowRight":
selectedTag = resultCount - 1;
break;
case "Enter":
if (selectedTag !== null) {
insertTextAtCursor(textArea, results[selectedTag], tagword);
}
break;
case "Escape":
hideResults(textArea);
break;
}
// Update highlighting
if (selectedTag !== null)
updateSelectionStyle(textArea, selectedTag);
// Prevent default behavior
event.preventDefault();
event.stopPropagation();
}
styleAdded = false;
onUiUpdate(function () {
// One-time config, tags & wildcards loading
if (acConfig === null) {
try {
acConfig = JSON.parse(readFile("file/tags/config.json"));
} catch (e) {
console.error("Error loading config.json: " + e);
return;
}
}
if (allTags.length === 0) {
try {
allTags = loadCSV();
} catch (e) {
console.error("Error loading tags file: " + e);
return;
}
}
if (wildcardFiles.length === 0 && acConfig.useWildcards) {
try {
wildcardFiles = readFile("file/tags/wildcardNames.txt").split("\n")
.filter(x => !x.startsWith("//")) // Remove comments
.filter(x => x.toLowerCase().includes(tagword.substring(2))) // Filter by tagword
.filter(x => x.trim().length > 0) // Remove empty lines
wildcardFiles.forEach(fName => {
try {
wildcards[fName.trim()] = readFile(`file/scripts/wildcards/${fName}.txt`).split("\n")
.filter(x => x.trim().length > 0) // Remove empty lines
} catch (e) {
console.log(`Could not load wildcards for ${fName}`);
}
});
} catch (e) {
console.error("Error loading wildcardNames.txt: " + e);
}
}
// Find all textareas
let txt2imgTextArea = gradioApp().querySelector('#txt2img_prompt > label > textarea');
let img2imgTextArea = gradioApp().querySelector('#img2img_prompt > label > textarea');
let negativeTextAreas = Array.from(gradioApp().querySelectorAll('#negative_prompt > label > textarea'));
let textAreas = [txt2imgTextArea, img2imgTextArea, negativeTextAreas[0], negativeTextAreas[1]];
// Not found, we're on a page without prompt textareas
if (textAreas.every(v => v === null || v === undefined)) return;
// Already added?
if (gradioApp().querySelector('.autocompleteResults.p') !== null
&& (gradioApp().querySelector('.autocompleteResults.n') === null
&& !acConfig.activeIn.negativePrompts)) {
return;
}
textAreas.forEach(area => {
// Skip directly if not found on the page
if (area === null || area === undefined) return;
// Return if autocomplete is disabled for the current area type in config
let textAreaId = getTextAreaIdentifier(area);
if (textAreaId.includes("p") || (textAreaId.includes("n") && acConfig.activeIn.negativePrompts)) {
if (textAreaId.includes("img2img")) {
if (!acConfig.activeIn.img2img) return;
} else {
if (!acConfig.activeIn.txt2img) 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), 100));
// 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));
// Add class so we know we've already added the listeners
area.classList.add('autocomplete');
}
});
if (styleAdded) return;
// Add style to dom
let acStyle = document.createElement('style');
let css = gradioApp().querySelector('.dark') ? autocompleteCSS_dark : autocompleteCSS_light;
if (acStyle.styleSheet) {
acStyle.styleSheet.cssText = css;
} else {
acStyle.appendChild(document.createTextNode(css));
}
gradioApp().appendChild(acStyle);
styleAdded = true;
});

View File

@@ -6,9 +6,22 @@
"negativePrompts": true
},
"maxResults": 5,
"resultStepLength": 500,
"delayTime": 100,
"showAllResults": false,
"useLeftRightArrowKeys": false,
"replaceUnderscores": true,
"escapeParentheses": true,
"useWildcards": false,
"useWildcards": true,
"useEmbeddings": true,
"translation": {
"searchByTranslation": true,
"onlyShowTranslation": false
},
"extra": {
"extraFile": "",
"onlyTranslationExtraFile": false
},
"colors": {
"danbooru": {
"0": ["lightblue", "dodgerblue"],
@@ -29,4 +42,4 @@
"8": ["seagreen", "darkseagreen"]
}
}
}
}

View File

@@ -1,7 +0,0 @@
// Put the file names of wildcard files you want to use here. Needed so that the script can access them.
// The default ones are the following, you can uncomment them if you have them
//adjective
//artist
//genre
//site
//style