ShawnGGG commited on
Commit
94e2f05
1 Parent(s): 1ae8021

Upload 21 files

Browse files
a1111-sd-webui-tagcomplete/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ tags/temp/
2
+ __pycache__/
a1111-sd-webui-tagcomplete/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Dominik Reh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
a1111-sd-webui-tagcomplete/README.md ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ![tag_autocomplete_light](https://user-images.githubusercontent.com/34448969/208306863-90bbd663-2cb4-47f1-a7fe-7b662a7b95e2.png)
2
+
3
+ # Booru tag autocompletion for A1111
4
+
5
+ [![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)
6
+
7
+ ## [中文文档](./README_ZH.md)
8
+
9
+ 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.
10
+
11
+ It displays autocompletion hints for recognized tags from "image booru" boards such as Danbooru, which are primarily used for browsing Anime-style illustrations.
12
+ Since some Stable Diffusion models were trained using this information, for example [Waifu Diffusion](https://github.com/harubaru/waifu-diffusion), using exact tags in prompts can often improve composition and help to achieve a wanted look.
13
+
14
+ You can install it using the inbuilt available extensions list, clone the files manually as described [below](#installation), or use a pre-packaged version from [Releases](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases).
15
+
16
+ ## Common Problems & Known Issues:
17
+ - 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.
18
+ - If `replaceUnderscores` is active, the script will currently only partially replace edited tags containing multiple words in brackets.
19
+ 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.
20
+
21
+ ## Screenshots
22
+ Demo video (with keyboard navigation):
23
+
24
+ https://user-images.githubusercontent.com/34448969/200128020-10d9a8b2-cea6-4e3f-bcd2-8c40c8c73233.mp4
25
+
26
+ Wildcard script support:
27
+
28
+ https://user-images.githubusercontent.com/34448969/200128031-22dd7c33-71d1-464f-ae36-5f6c8fd49df0.mp4
29
+
30
+ Dark and Light mode supported, including tag colors:
31
+
32
+ ![results_dark](https://user-images.githubusercontent.com/34448969/200128214-3b6f21b4-9dda-4acf-820e-5df0285c30d6.png)
33
+ ![results_light](https://user-images.githubusercontent.com/34448969/200128217-bfac8b60-6673-447b-90fd-dc6326f1618c.png)
34
+
35
+ ## Installation
36
+ ### As an extension (recommended)
37
+ Either clone the repo into your extensions folder:
38
+ ```bash
39
+ git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extensions/tag-autocomplete
40
+ ```
41
+ (The second argument specifies the name of the folder, you can choose whatever you like).
42
+
43
+ Or create a folder there manually and place the `javascript`, `scripts` and `tags` folders in it.
44
+
45
+ ### In the root folder (legacy)
46
+ This installation method is for old webui versions pre-extension system, it will not work on current versions!
47
+
48
+ ---
49
+
50
+ 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.
51
+ 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.
52
+
53
+ ### Important:
54
+ The script needs **all three folders** to work properly.
55
+
56
+ ## Wildcard & Embedding support
57
+ 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.
58
+
59
+ 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.
60
+
61
+ ## Settings
62
+
63
+ The extension has a large amount of configuration & customizability built in:
64
+
65
+ ![image](https://user-images.githubusercontent.com/34448969/204093162-99c6a0e7-8183-4f47-963b-1f172774f527.png)
66
+
67
+ | Setting | Description |
68
+ |---------|-------------|
69
+ | 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.|
70
+ | activeIn | Allows to selectively (de)activate the script for txt2img, img2img, and the negative prompts for both. |
71
+ | 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. |
72
+ | resultStepLength | Allows to load results in smaller batches of the specified size for better performance in long lists or if showAllResults is true. |
73
+ | delayTime | Specifies how much to wait in milliseconds before triggering autocomplete. Helps prevent too frequent updates while typing. |
74
+ | showAllResults | If true, will ignore maxResults and show all results in a scrollable list. **Warning:** can lag your browser for long lists. |
75
+ | replaceUnderscores | If true, undescores are replaced with spaces on clicking a tag. Might work better for some models. |
76
+ | escapeParentheses | If true, escapes tags containing () so they don't contribute to the web UI's prompt weighting functionality. |
77
+ | appendComma | Specifies the starting value of the "Append commas" UI switch. If UI options are disabled, this will always be used. |
78
+ | useWildcards | Used to toggle the wildcard completion functionality. |
79
+ | useEmbeddings | Used to toggle the embedding completion functionality. |
80
+ | alias | Options for aliases. More info in the section below. |
81
+ | translation | Options for translations. More info in the section below. |
82
+ | extras | Options for additional tag files / aliases / translations. More info in the section below. |
83
+
84
+ ### colors.json
85
+ Additionally, tag type colors can be specified using the separate `colors.json` file in the extension's `tags` folder.
86
+ 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.
87
+ ```json
88
+ {
89
+ "danbooru": {
90
+ "-1": ["red", "maroon"],
91
+ "0": ["lightblue", "dodgerblue"],
92
+ "1": ["indianred", "firebrick"],
93
+ "3": ["violet", "darkorchid"],
94
+ "4": ["lightgreen", "darkgreen"],
95
+ "5": ["orange", "darkorange"]
96
+ },
97
+ "e621": {
98
+ "-1": ["red", "maroon"],
99
+ "0": ["lightblue", "dodgerblue"],
100
+ "1": ["gold", "goldenrod"],
101
+ "3": ["violet", "darkorchid"],
102
+ "4": ["lightgreen", "darkgreen"],
103
+ "5": ["tomato", "darksalmon"],
104
+ "6": ["red", "maroon"],
105
+ "7": ["whitesmoke", "black"],
106
+ "8": ["seagreen", "darkseagreen"]
107
+ }
108
+ }
109
+ ```
110
+ The numbers are specifying the tag type, which is dependent on the tag source. For an example, see [CSV tag data](#csv-tag-data).
111
+
112
+ ### Aliases, Translations & Extra tags
113
+ #### Aliases
114
+ Like on Booru sites, tags can have one or multiple aliases which redirect to the actual value on completion. These will be searchable / shown according to the settings in `config.json`:
115
+ - `searchByAlias` - Whether to also search for the alias or only the actual tag.
116
+ - `onlyShowAlias` - Shows only the alias instead of `alias -> actual`. Only for displaying, the inserted text at the end is still the actual tag.
117
+
118
+ #### Translations
119
+ 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.
120
+ 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.
121
+
122
+ Example with chinese translation:
123
+
124
+ ![IME-input](https://user-images.githubusercontent.com/34448969/200126551-2264e9cc-abb2-4450-9afa-43f362a77ab0.png)
125
+ ![english-input](https://user-images.githubusercontent.com/34448969/200126513-bf6b3940-6e22-41b0-a369-f2b4640f87d6.png)
126
+
127
+ #### Extra file
128
+ An extra file can be used to add new / custom tags not included in the main set.
129
+ The format is identical to the normal tag format shown in [CSV tag data](#csv-tag-data) below, with one exception:
130
+ 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.
131
+ If left empty, it will instead show "Custom tag".
132
+
133
+ An example with the included (very basic) extra-quality-tags.csv file:
134
+
135
+ ![image](https://user-images.githubusercontent.com/34448969/218264276-cd77ba8e-62d8-41a2-b03c-6c04887ee18b.png)
136
+
137
+ Whether the custom tags should be added before or after the normal tags can be chosen in the settings.
138
+
139
+ ## CSV tag data
140
+ The script expects a CSV file with tags saved in the following way:
141
+ ```csv
142
+ <name>,<type>,<postCount>,"<aliases>"
143
+ ```
144
+ Example:
145
+ ```csv
146
+ 1girl,0,4114588,"1girls,sole_female"
147
+ solo,0,3426446,"female_solo,solo_female"
148
+ highres,5,3008413,"high_res,high_resolution,hires"
149
+ long_hair,0,2898315,longhair
150
+ commentary_request,5,2610959,
151
+ ```
152
+ Notably, it does not expect column names in the first row and both count and aliases are technically optional,
153
+ although count is always included in the default data. Multiple aliases need to be comma separated as well, but encased in string quotes to not break the CSV parsing.
154
+
155
+ The numbering system follows the [tag API docs](https://danbooru.donmai.us/wiki_pages/api%3Atags) of Danbooru:
156
+ | Value | Description |
157
+ |-------|-------------|
158
+ |0 | General |
159
+ |1 | Artist |
160
+ |3 | Copyright |
161
+ |4 | Character |
162
+ |5 | Meta |
163
+
164
+ or similarly for e621:
165
+ | Value | Description |
166
+ |-------|-------------|
167
+ |-1 | Invalid |
168
+ |0 | General |
169
+ |1 | Artist |
170
+ |3 | Copyright |
171
+ |4 | Character |
172
+ |5 | Species |
173
+ |6 | Invalid |
174
+ |7 | Meta |
175
+ |8 | Lore |
176
+
177
+ The tag type is used for coloring entries in the result list.
a1111-sd-webui-tagcomplete/README_ZH.md ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ![tag_autocomplete_light_zh](https://user-images.githubusercontent.com/34448969/208307331-430696b4-e854-4458-b9e9-f6a6594f19e1.png)
2
+
3
+ # Booru tag autocompletion for A1111
4
+
5
+ [![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)
6
+ ## [English Document](./README.md)
7
+
8
+ ## 功能概述
9
+
10
+ 本脚本为 [AUTOMATIC1111 web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui)的自定义脚本,能在输入Tag时提供booru风格(如Danbooru)的TAG自动补全。因为有一些模型是基于这种TAG风格训练的(例如[Waifu Diffusion](https://github.com/harubaru/waifu-diffusion)),因此使用这些Tag能获得较为精确的效果。
11
+
12
+ 这个脚本的创建是为了减少因复制Tag在Web UI和 booru网站的反复切换。
13
+ 你可以按照[以下方法](#installation)下载或拷贝文件,也可以使用[Releases](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)中打包好的文件。
14
+
15
+ ## 常见问题 & 已知缺陷:
16
+ - 当`replaceUnderscores`选项开启时, 脚本只会替换Tag的一部分如果Tag包含多个单词,比如将`atago (azur lane)`修改`atago`为`taihou`并使用自动补全时.会得到 `taihou (azur lane), lane)`的结果, 因为脚本没有把后面的部分认为成同一个Tag。
17
+
18
+ ## 演示与截图
19
+ 演示视频(使用了键盘导航):
20
+
21
+ https://user-images.githubusercontent.com/34448969/200128020-10d9a8b2-cea6-4e3f-bcd2-8c40c8c73233.mp4
22
+
23
+ Wildcard支持演示:
24
+
25
+ https://user-images.githubusercontent.com/34448969/200128031-22dd7c33-71d1-464f-ae36-5f6c8fd49df0.mp4
26
+
27
+ 深浅色主题支持,包括Tag的颜色:
28
+
29
+ ![results_dark](https://user-images.githubusercontent.com/34448969/200128214-3b6f21b4-9dda-4acf-820e-5df0285c30d6.png)
30
+ ![results_light](https://user-images.githubusercontent.com/34448969/200128217-bfac8b60-6673-447b-90fd-dc6326f1618c.png)
31
+
32
+ ## 安装
33
+ ### 作为一种扩展(推荐)
34
+ 要么把它克隆到你的扩展文件夹里
35
+ ```bash
36
+ git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extensions/tag-autocomplete
37
+ ```
38
+ (第二个参数指定文件夹的名称,你可以选择任何你喜欢的东西)。
39
+
40
+ 或者手动创建一个文件夹,将 `javascript`、`scripts`和`tags`文件夹放在其中。
41
+
42
+ ### 在根目录下(过时的方法)
43
+ 这种安装方法适用于添加扩展系统之前的旧版webui,在目前的版本上是行不通的。
44
+
45
+ ---
46
+ 在这两种配置中,标签文件夹包含`colors.json`和脚本用于自动完成的标签数据。
47
+ 默认情况下,Tag数据包括`Danbooru.csv`和`e621.csv`。
48
+
49
+ 在扫描过`/embeddings`和wildcards后,会将列表存放在`tags/temp`文件夹下。删除该文件夹不会有任何影响,下次启动时它会重新创建。
50
+
51
+ ### 注意:
52
+ 本脚本的允许需要**全部的三个文件夹**。
53
+
54
+ ## [Wildcard](https://github.com/jtkelm2/stable-diffusion-webui-1/blob/master/scripts/wildcards.py) & Embedding 支持
55
+ 自动补全同样适用于 [Wildcard](https://github.com/jtkelm2/stable-diffusion-webui-1/blob/master/scripts/wildcards.py)中所述的通配符文件(后面有演示视频)。这将使你能够插入Wildcard脚本需要的通配符,更进一步的,你还可以插入通配符文件内的某个具体Tag。
56
+
57
+ 当输入`__`字符时,`/scripts/wildcards`文件夹下的通配符文件会列出到自动补全,当你选择某个具体通配符文件时,会列出其中的所有的具体Tag,但如果你仅需要选择某个通配符,请按下空格。
58
+
59
+ 当输入`<`字符时,`embeddings`文件夹下的`.pt`和`.bin`文件会列出到自动完成。需要注意的是,一些颜文字也会包含`<`(比如`>_<`),所以它们也会出现在结果中。
60
+
61
+ 现在这项功能默认是启用的,并会自动扫描`/embeddings`和`/scripts/wildcards`文件夹,不再需要使用`tags/wildcardNames.txt`文件了,早期版本的用户可以将它删除。
62
+
63
+ ## 配置文件
64
+ 该扩展有大量的配置和可定制性的内建:
65
+
66
+ ![image](https://user-images.githubusercontent.com/34448969/204093162-99c6a0e7-8183-4f47-963b-1f172774f527.png)
67
+
68
+ | 设置 | 描述 |
69
+ |---------|-------------|
70
+ | tagFile | 指定要使用的标记文件。您可以提供您喜欢的自定义标签数据库,但由于该脚本是在考虑 Danbooru 标签的情况下开发的,因此它可能无法与其他配置一起正常工作。|
71
+ | activeIn | 允许有选择地(取消)激活 txt2img、img2img 和两者的否定提示的脚本。|
72
+ | maxResults | 最多显示多少个结果。对于默认标记集,结果按出现次数排序。对于嵌入和通配符,它​​将在可滚动列表中显示所有结果。 |
73
+ | showAllResults | 如果为真,将忽略 maxResults 并在可滚动列表中显示所有结果。 **警告:** 对于长列表,您的浏览器可能会滞后。 |
74
+ | resultStepLength | 允许以指定大小的小批次加载结果,以便在长列表中获得更好的性能,或者在showAllResults为真时。 |
75
+ | delayTime | 指定在触发自动完成之前要等待多少毫秒。有助于防止打字时过于频繁的更新。 |
76
+ | replaceUnderscores | 如果为 true,则在单击标签时将取消划线替换为空格。对于某些型号可能会更好。|
77
+ | escapeParentheses | 如果为 true,则转义包含 () 的标签,因此它们不会对 Web UI 的提示权重功能做出贡献。 |
78
+ | useWildcards | 用于切换通配符完成功能。 |
79
+ | useEmbeddings | 用于切换嵌入完成功能。 |
80
+ | alias | 标签别名的选项。更多信息在下面的部分。 |
81
+ | translation | 用于翻译标签的选项。更多信息在下面的部分。 |
82
+ | extras | 附加标签文件/翻译的选项。更多信息在下面的部分。|
83
+
84
+ ### colors.json (标签颜色)
85
+ 此外,标签类型的颜色可以使用扩展的`tags`文件夹中单独的`colors.json`文件来指定。
86
+ 你也可以在这里为自定义标签文件添加新的(与文件名相同,不带 .csv)。第一个值是暗模式,第二个值是亮模式。颜色名称和十六进制代码都被支持。
87
+ ```json
88
+ {
89
+ "danbooru": {
90
+ "-1": ["red", "maroon"],
91
+ "0": ["lightblue", "dodgerblue"],
92
+ "1": ["indianred", "firebrick"],
93
+ "3": ["violet", "darkorchid"],
94
+ "4": ["lightgreen", "darkgreen"],
95
+ "5": ["orange", "darkorange"]
96
+ },
97
+ "e621": {
98
+ "-1": ["red", "maroon"],
99
+ "0": ["lightblue", "dodgerblue"],
100
+ "1": ["gold", "goldenrod"],
101
+ "3": ["violet", "darkorchid"],
102
+ "4": ["lightgreen", "darkgreen"],
103
+ "5": ["tomato", "darksalmon"],
104
+ "6": ["red", "maroon"],
105
+ "7": ["whitesmoke", "black"],
106
+ "8": ["seagreen", "darkseagreen"]
107
+ }
108
+ }
109
+ ```
110
+ 数字是指定标签的类型,这取决于标签的来源。例如,见[CSV tag data](#csv-tag-data)。
111
+
112
+ ### 别名,翻译&新增Tag
113
+ #### 别名
114
+ 像Booru网站一样,标签可以有一个或多个别名,完成后重定向到实际值。这些将根据`config.json`中的设置进行搜索/显示。
115
+ - `searchByAlias` - 是否也要搜索别名,或只搜索实际的标签。
116
+ - `onlyShowAlias` - 只显示别名,不显示 `别名->实际`。仅用于显示,最后的文本仍然是实际的标签。
117
+
118
+ #### 翻译
119
+ 可以在翻译部分添加一个额外的文件,它将被用来翻译标签和别名,同时也可以通过翻译进行搜索。
120
+ 这个文件需要是CSV格式的`<英语标签/别名>,<翻译>`,但为了向后兼容使用三栏格式的旧的额外文件,你可以打开`oldFormat`来代替它。
121
+
122
+ 完整和部分中文标签集的示例:
123
+
124
+ ![IME-input](https://user-images.githubusercontent.com/34448969/200126551-2264e9cc-abb2-4450-9afa-43f362a77ab0.png)
125
+ ![english-input](https://user-images.githubusercontent.com/34448969/200126513-bf6b3940-6e22-41b0-a369-f2b4640f87d6.png)
126
+
127
+ #### Extra文件
128
+ 额外文件可以用来添加未包含在主集中的新的/自定义标签。
129
+ 其格式与下面 [CSV tag data](#csv-tag-data) 中的正常标签格式相同,但有一个例外。
130
+ 由于自定义标签没有帖子计数,第三列(如果从零开始计算,则为第二列)用于显示标签旁边的灰色元文本。
131
+ 如果留空,它将显示 "Custom tag"。
132
+
133
+ 以默认的(非常基本的)extra-quality-tags.csv为例:
134
+
135
+ ![image](https://user-images.githubusercontent.com/34448969/218264276-cd77ba8e-62d8-41a2-b03c-6c04887ee18b.png)
136
+
137
+ 你可以在设置中选择自定义标签是否应该加在常规标签之前或之后。
138
+
139
+ ### CSV tag data
140
+ 本脚本的Tag文件格式如下,你可以安装这个格式制作自己的Tag文件:
141
+ ```csv
142
+ 1girl,0,4114588,"1girls,sole_female"
143
+ solo,0,3426446,"female_solo,solo_female"
144
+ highres,5,3008413,"high_res,high_resolution,hires"
145
+ long_hair,0,2898315,longhair
146
+ commentary_request,5,2610959,
147
+ ```
148
+ 值得注意的是,不希望在第一行有列名,而且count和aliases在技术上都是可选的。
149
+ 尽管count总是包含在默认数据中。多个别名也需要用逗号分隔,但要用字符串引号包裹,以免破坏CSV解析。
150
+ 编号系统遵循 Danbooru 的 [tag API docs](https://danbooru.donmai.us/wiki_pages/api%3Atags):
151
+ | Value | Description |
152
+ |-------|-------------|
153
+ |0 | General |
154
+ |1 | Artist |
155
+ |3 | Copyright |
156
+ |4 | Character |
157
+ |5 | Meta |
158
+
159
+ 类似的还有e621:
160
+ | Value | Description |
161
+ |-------|-------------|
162
+ |-1 | Invalid |
163
+ |0 | General |
164
+ |1 | Artist |
165
+ |3 | Copyright |
166
+ |4 | Character |
167
+ |5 | Species |
168
+ |6 | Invalid |
169
+ |7 | Meta |
170
+ |8 | Lore |
171
+
172
+ 标记类型用于为结果列表中的条目着色.
a1111-sd-webui-tagcomplete/javascript/__globals.js ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Core components
2
+ var CFG = null;
3
+ var tagBasePath = "";
4
+ var keymap = null;
5
+
6
+ // Tag completion data loaded from files
7
+ var allTags = [];
8
+ var translations = new Map();
9
+ var extras = [];
10
+ // Same for tag-likes
11
+ var wildcardFiles = [];
12
+ var wildcardExtFiles = [];
13
+ var yamlWildcards = [];
14
+ var embeddings = [];
15
+ var hypernetworks = [];
16
+ var loras = [];
17
+
18
+ // Selected model info for black/whitelisting
19
+ var currentModelHash = "";
20
+ var currentModelName = "";
21
+
22
+ // Current results
23
+ var results = [];
24
+ var resultCount = 0;
25
+
26
+ // Relevant for parsing
27
+ var previousTags = [];
28
+ var tagword = "";
29
+ var originalTagword = "";
30
+ let hideBlocked = false;
31
+
32
+ // Tag selection for keyboard navigation
33
+ var selectedTag = null;
34
+ var oldSelectedTag = null;
35
+
36
+ // UMI
37
+ var umiPreviousTags = [];
38
+
39
+ /// Extendability system:
40
+ /// Provides "queues" for other files of the script (or really any js)
41
+ /// to add functions to be called at certain points in the script.
42
+ /// Similar to a callback system, but primitive.
43
+
44
+ // Queues
45
+ const QUEUE_AFTER_INSERT = [];
46
+ const QUEUE_AFTER_SETUP = [];
47
+ const QUEUE_FILE_LOAD = [];
48
+ const QUEUE_AFTER_CONFIG_CHANGE = [];
49
+ const QUEUE_SANITIZE = [];
50
+
51
+ // List of parsers to try
52
+ const PARSERS = [];
a1111-sd-webui-tagcomplete/javascript/_baseParser.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class FunctionNotOverriddenError extends Error {
2
+ constructor(message = "", ...args) {
3
+ super(message, ...args);
4
+ this.message = message + " is an abstract base function and must be overwritten.";
5
+ }
6
+ }
7
+
8
+ class BaseTagParser {
9
+ triggerCondition = null;
10
+
11
+ constructor (triggerCondition) {
12
+ if (new.target === BaseTagParser) {
13
+ throw new TypeError("Cannot construct abstract BaseCompletionParser directly");
14
+ }
15
+ this.triggerCondition = triggerCondition;
16
+ }
17
+
18
+ parse() {
19
+ throw new FunctionNotOverriddenError("parse()");
20
+ }
21
+ }
a1111-sd-webui-tagcomplete/javascript/_result.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Result data type for cleaner use of optional completion result properties
2
+
3
+ // Type enum
4
+ const ResultType = Object.freeze({
5
+ "tag": 1,
6
+ "extra": 2,
7
+ "embedding": 3,
8
+ "wildcardTag": 4,
9
+ "wildcardFile": 5,
10
+ "yamlWildcard": 6,
11
+ "hypernetwork": 7,
12
+ "lora": 8
13
+ });
14
+
15
+ // Class to hold result data and annotations to make it clearer to use
16
+ class AutocompleteResult {
17
+ // Main properties
18
+ text = "";
19
+ type = ResultType.tag;
20
+
21
+ // Additional info, only used in some cases
22
+ category = null;
23
+ count = null;
24
+ aliases = null;
25
+ meta = null;
26
+
27
+ // Constructor
28
+ constructor(text, type) {
29
+ this.text = text;
30
+ this.type = type;
31
+ }
32
+ }
a1111-sd-webui-tagcomplete/javascript/_textAreas.js ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Utility functions to select text areas the script should work on,
2
+ // including third party options.
3
+ // Supported third party options so far:
4
+ // - Dataset Tag Editor
5
+
6
+ // Core text area selectors
7
+ const core = [
8
+ "#txt2img_prompt > label > textarea",
9
+ "#img2img_prompt > label > textarea",
10
+ "#txt2img_neg_prompt > label > textarea",
11
+ "#img2img_neg_prompt > label > textarea"
12
+ ];
13
+
14
+ // Third party text area selectors
15
+ const thirdParty = {
16
+ "dataset-tag-editor": {
17
+ "base": "#tab_dataset_tag_editor_interface",
18
+ "hasIds": false,
19
+ "selectors": [
20
+ "Caption of Selected Image",
21
+ "Interrogate Result",
22
+ "Edit Caption",
23
+ "Edit Tags"
24
+ ]
25
+ }
26
+ }
27
+
28
+ function getTextAreas() {
29
+ // First get all core text areas
30
+ let textAreas = [...gradioApp().querySelectorAll(core.join(", "))];
31
+
32
+ for (const [key, entry] of Object.entries(thirdParty)) {
33
+ if (entry.hasIds) { // If the entry has proper ids, we can just select them
34
+ textAreas = textAreas.concat([...gradioApp().querySelectorAll(entry.selectors.join(", "))]);
35
+ } else { // Otherwise, we have to find the text areas by their adjacent labels
36
+ let base = gradioApp().querySelector(entry.base);
37
+
38
+ // Safety check
39
+ if (!base) continue;
40
+
41
+ let allTextAreas = [...base.querySelectorAll("textarea")];
42
+
43
+ // Filter the text areas where the adjacent label matches one of the selectors
44
+ let matchingTextAreas = allTextAreas.filter(ta => [...ta.parentElement.childNodes].some(x => entry.selectors.includes(x.innerText)));
45
+ textAreas = textAreas.concat(matchingTextAreas);
46
+ }
47
+ };
48
+
49
+ return textAreas;
50
+ }
51
+
52
+ const thirdPartyIdSet = new Set();
53
+ // Get the identifier for the text area to differentiate between positive and negative
54
+ function getTextAreaIdentifier(textArea) {
55
+ let txt2img_p = gradioApp().querySelector('#txt2img_prompt > label > textarea');
56
+ let txt2img_n = gradioApp().querySelector('#txt2img_neg_prompt > label > textarea');
57
+ let img2img_p = gradioApp().querySelector('#img2img_prompt > label > textarea');
58
+ let img2img_n = gradioApp().querySelector('#img2img_neg_prompt > label > textarea');
59
+
60
+ let modifier = "";
61
+ switch (textArea) {
62
+ case txt2img_p:
63
+ modifier = ".txt2img.p";
64
+ break;
65
+ case txt2img_n:
66
+ modifier = ".txt2img.n";
67
+ break;
68
+ case img2img_p:
69
+ modifier = ".img2img.p";
70
+ break;
71
+ case img2img_n:
72
+ modifier = ".img2img.n";
73
+ break;
74
+ default:
75
+ // If the text area is not a core text area, it must be a third party text area
76
+ // Add it to the set of third party text areas and get its index as a unique identifier
77
+ if (!thirdPartyIdSet.has(textArea))
78
+ thirdPartyIdSet.add(textArea);
79
+
80
+ modifier = `.thirdParty.ta${[...thirdPartyIdSet].indexOf(textArea)}`;
81
+ break;
82
+ }
83
+ return modifier;
84
+ }
a1111-sd-webui-tagcomplete/javascript/_utils.js ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Utility functions for tag autocomplete
2
+
3
+ // Parse the CSV file into a 2D array. Doesn't use regex, so it is very lightweight.
4
+ function parseCSV(str) {
5
+ var arr = [];
6
+ var quote = false; // 'true' means we're inside a quoted field
7
+
8
+ // Iterate over each character, keep track of current row and column (of the returned array)
9
+ for (var row = 0, col = 0, c = 0; c < str.length; c++) {
10
+ var cc = str[c], nc = str[c + 1]; // Current character, next character
11
+ arr[row] = arr[row] || []; // Create a new row if necessary
12
+ arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary
13
+
14
+ // If the current character is a quotation mark, and we're inside a
15
+ // quoted field, and the next character is also a quotation mark,
16
+ // add a quotation mark to the current column and skip the next character
17
+ if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }
18
+
19
+ // If it's just one quotation mark, begin/end quoted field
20
+ if (cc == '"') { quote = !quote; continue; }
21
+
22
+ // If it's a comma and we're not in a quoted field, move on to the next column
23
+ if (cc == ',' && !quote) { ++col; continue; }
24
+
25
+ // If it's a newline (CRLF) and we're not in a quoted field, skip the next character
26
+ // and move on to the next row and move to column 0 of that new row
27
+ if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; }
28
+
29
+ // If it's a newline (LF or CR) and we're not in a quoted field,
30
+ // move on to the next row and move to column 0 of that new row
31
+ if (cc == '\n' && !quote) { ++row; col = 0; continue; }
32
+ if (cc == '\r' && !quote) { ++row; col = 0; continue; }
33
+
34
+ // Otherwise, append the current character to the current column
35
+ arr[row][col] += cc;
36
+ }
37
+ return arr;
38
+ }
39
+
40
+ // Load file
41
+ async function readFile(filePath, json = false, cache = false) {
42
+ if (!cache)
43
+ filePath += `?${new Date().getTime()}`;
44
+
45
+ let response = await fetch(`file=${filePath}`);
46
+
47
+ if (response.status != 200) {
48
+ console.error(`Error loading file "${filePath}": ` + response.status, response.statusText);
49
+ return null;
50
+ }
51
+
52
+ if (json)
53
+ return await response.json();
54
+ else
55
+ return await response.text();
56
+ }
57
+
58
+ // Load CSV
59
+ async function loadCSV(path) {
60
+ let text = await readFile(path);
61
+ return parseCSV(text);
62
+ }
63
+
64
+ // Debounce function to prevent spamming the autocomplete function
65
+ var dbTimeOut;
66
+ const debounce = (func, wait = 300) => {
67
+ return function (...args) {
68
+ if (dbTimeOut) {
69
+ clearTimeout(dbTimeOut);
70
+ }
71
+
72
+ dbTimeOut = setTimeout(() => {
73
+ func.apply(this, args);
74
+ }, wait);
75
+ }
76
+ }
77
+
78
+ // Difference function to fix duplicates not being seen as changes in normal filter
79
+ function difference(a, b) {
80
+ if (a.length == 0) {
81
+ return b;
82
+ }
83
+ if (b.length == 0) {
84
+ return a;
85
+ }
86
+
87
+ return [...b.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) - 1),
88
+ a.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map())
89
+ )].reduce((acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), []);
90
+ }
91
+
92
+ function escapeRegExp(string) {
93
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
94
+ }
95
+ function escapeHTML(unsafeText) {
96
+ let div = document.createElement('div');
97
+ div.textContent = unsafeText;
98
+ return div.innerHTML;
99
+ }
100
+
101
+ // Queue calling function to process global queues
102
+ async function processQueue(queue, context, ...args) {
103
+ for (let i = 0; i < queue.length; i++) {
104
+ await queue[i].call(context, ...args);
105
+ }
106
+ }
107
+ // The same but with return values
108
+ async function processQueueReturn(queue, context, ...args)
109
+ {
110
+ let qeueueReturns = [];
111
+ for (let i = 0; i < queue.length; i++) {
112
+ let returnValue = await queue[i].call(context, ...args);
113
+ if (returnValue)
114
+ qeueueReturns.push(returnValue);
115
+ }
116
+ return qeueueReturns;
117
+ }
118
+ // Specific to tag completion parsers
119
+ async function processParsers(textArea, prompt) {
120
+ // Get all parsers that have a successful trigger condition
121
+ let matchingParsers = PARSERS.filter(parser => parser.triggerCondition());
122
+ // Guard condition
123
+ if (matchingParsers.length === 0) {
124
+ return null;
125
+ }
126
+
127
+ let parseFunctions = matchingParsers.map(parser => parser.parse);
128
+ // Process them and return the results
129
+ return await processQueueReturn(parseFunctions, null, textArea, prompt);
130
+ }
a1111-sd-webui-tagcomplete/javascript/ext_embeddings.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const EMB_REGEX = /<(?!l:|h:)[^,> ]*>?/g;
2
+ const EMB_TRIGGER = () => CFG.useEmbeddings && tagword.match(EMB_REGEX);
3
+
4
+ class EmbeddingParser extends BaseTagParser {
5
+ parse() {
6
+ // Show embeddings
7
+ let tempResults = [];
8
+ if (tagword !== "<" && tagword !== "<e:") {
9
+ let searchTerm = tagword.replace("<e:", "").replace("<", "");
10
+ let versionString;
11
+ if (searchTerm.startsWith("v1") || searchTerm.startsWith("v2")) {
12
+ versionString = searchTerm.slice(0, 2);
13
+ searchTerm = searchTerm.slice(2);
14
+ }
15
+ if (versionString)
16
+ tempResults = embeddings.filter(x => x[0].toLowerCase().includes(searchTerm) && x[1] && x[1] === versionString); // Filter by tagword
17
+ else
18
+ tempResults = embeddings.filter(x => x[0].toLowerCase().includes(searchTerm)); // Filter by tagword
19
+ } else {
20
+ tempResults = embeddings;
21
+ }
22
+
23
+ // Add final results
24
+ let finalResults = [];
25
+ tempResults.forEach(t => {
26
+ let result = new AutocompleteResult(t[0].trim(), ResultType.embedding)
27
+ result.meta = t[1] + " Embedding";
28
+ finalResults.push(result);
29
+ });
30
+
31
+ return finalResults;
32
+ }
33
+ }
34
+
35
+ async function load() {
36
+ if (embeddings.length === 0) {
37
+ try {
38
+ embeddings = (await readFile(`${tagBasePath}/temp/emb.txt`)).split("\n")
39
+ .filter(x => x.trim().length > 0) // Remove empty lines
40
+ .map(x => x.trim().split(",")); // Split into name, version type pairs
41
+ } catch (e) {
42
+ console.error("Error loading embeddings.txt: " + e);
43
+ }
44
+ }
45
+ }
46
+
47
+ function sanitize(tagType, text) {
48
+ if (tagType === ResultType.embedding) {
49
+ return text.replace(/^.*?: /g, "");
50
+ }
51
+ return null;
52
+ }
53
+
54
+ PARSERS.push(new EmbeddingParser(EMB_TRIGGER));
55
+
56
+ // Add our utility functions to their respective queues
57
+ QUEUE_FILE_LOAD.push(load);
58
+ QUEUE_SANITIZE.push(sanitize);
a1111-sd-webui-tagcomplete/javascript/ext_hypernets.js ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const HYP_REGEX = /<(?!e:|l:)[^,> ]*>?/g;
2
+ const HYP_TRIGGER = () => CFG.useHypernetworks && tagword.match(HYP_REGEX);
3
+
4
+ class HypernetParser extends BaseTagParser {
5
+ parse() {
6
+ // Show hypernetworks
7
+ let tempResults = [];
8
+ if (tagword !== "<" && tagword !== "<h:" && tagword !== "<hypernet:") {
9
+ let searchTerm = tagword.replace("<hypernet:", "").replace("<h:", "").replace("<", "");
10
+ tempResults = hypernetworks.filter(x => x.toLowerCase().includes(searchTerm)); // Filter by tagword
11
+ } else {
12
+ tempResults = hypernetworks;
13
+ }
14
+
15
+ // Add final results
16
+ let finalResults = [];
17
+ tempResults.forEach(t => {
18
+ let result = new AutocompleteResult(t.trim(), ResultType.hypernetwork)
19
+ result.meta = "Hypernetwork";
20
+ finalResults.push(result);
21
+ });
22
+
23
+ return finalResults;
24
+ }
25
+ }
26
+
27
+ async function load() {
28
+ if (hypernetworks.length === 0) {
29
+ try {
30
+ hypernetworks = (await readFile(`${tagBasePath}/temp/hyp.txt`)).split("\n")
31
+ .filter(x => x.trim().length > 0) //Remove empty lines
32
+ .map(x => x.trim()); // Remove carriage returns and padding if it exists
33
+ } catch (e) {
34
+ console.error("Error loading hypernetworks.txt: " + e);
35
+ }
36
+ }
37
+ }
38
+
39
+ function sanitize(tagType, text) {
40
+ if (tagType === ResultType.hypernetwork) {
41
+ return `<hypernet:${text}:${CFG.extraNetworksDefaultMultiplier}>`;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ PARSERS.push(new HypernetParser(HYP_TRIGGER));
47
+
48
+ // Add our utility functions to their respective queues
49
+ QUEUE_FILE_LOAD.push(load);
50
+ QUEUE_SANITIZE.push(sanitize);
a1111-sd-webui-tagcomplete/javascript/ext_loras.js ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const LORA_REGEX = /<(?!e:|h:)[^,> ]*>?/g;
2
+ const LORA_TRIGGER = () => CFG.useLoras && tagword.match(LORA_REGEX);
3
+
4
+ class LoraParser extends BaseTagParser {
5
+ parse() {
6
+ // Show lora
7
+ let tempResults = [];
8
+ if (tagword !== "<" && tagword !== "<l:" && tagword !== "<lora:") {
9
+ let searchTerm = tagword.replace("<lora:", "").replace("<l:", "").replace("<", "");
10
+ tempResults = loras.filter(x => x.toLowerCase().includes(searchTerm)); // Filter by tagword
11
+ } else {
12
+ tempResults = loras;
13
+ }
14
+
15
+ // Add final results
16
+ let finalResults = [];
17
+ tempResults.forEach(t => {
18
+ let result = new AutocompleteResult(t.trim(), ResultType.lora)
19
+ result.meta = "Lora";
20
+ finalResults.push(result);
21
+ });
22
+
23
+ return finalResults;
24
+ }
25
+ }
26
+
27
+ async function load() {
28
+ if (loras.length === 0) {
29
+ try {
30
+ loras = (await readFile(`${tagBasePath}/temp/lora.txt`)).split("\n")
31
+ .filter(x => x.trim().length > 0) // Remove empty lines
32
+ .map(x => x.trim()); // Remove carriage returns and padding if it exists
33
+ } catch (e) {
34
+ console.error("Error loading lora.txt: " + e);
35
+ }
36
+ }
37
+ }
38
+
39
+ function sanitize(tagType, text) {
40
+ if (tagType === ResultType.lora) {
41
+ return `<lora:${text}:${CFG.extraNetworksDefaultMultiplier}>`;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ PARSERS.push(new LoraParser(LORA_TRIGGER));
47
+
48
+ // Add our utility functions to their respective queues
49
+ QUEUE_FILE_LOAD.push(load);
50
+ QUEUE_SANITIZE.push(sanitize);
a1111-sd-webui-tagcomplete/javascript/ext_umi.js ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const UMI_PROMPT_REGEX = /<[^\s]*?\[[^,<>]*[\]|]?>?/gi;
2
+ const UMI_TAG_REGEX = /(?:\[|\||--)([^<>\[\]\-|]+)/gi;
3
+
4
+ const UMI_TRIGGER = () => CFG.useWildcards && [...tagword.matchAll(UMI_PROMPT_REGEX)].length > 0;
5
+
6
+ class UmiParser extends BaseTagParser {
7
+ parse(textArea, prompt) {
8
+ // We are in a UMI yaml tag definition, parse further
9
+ let umiSubPrompts = [...prompt.matchAll(UMI_PROMPT_REGEX)];
10
+
11
+ let umiTags = [];
12
+ let umiTagsWithOperators = []
13
+
14
+ const insertAt = (str,char,pos) => str.slice(0,pos) + char + str.slice(pos);
15
+
16
+ umiSubPrompts.forEach(umiSubPrompt => {
17
+ umiTags = umiTags.concat([...umiSubPrompt[0].matchAll(UMI_TAG_REGEX)].map(x => x[1].toLowerCase()));
18
+
19
+ const start = umiSubPrompt.index;
20
+ const end = umiSubPrompt.index + umiSubPrompt[0].length;
21
+ if (textArea.selectionStart >= start && textArea.selectionStart <= end) {
22
+ umiTagsWithOperators = insertAt(umiSubPrompt[0], '###', textArea.selectionStart - start);
23
+ }
24
+ });
25
+
26
+ // Safety check since UMI parsing sometimes seems to trigger outside of an UMI subprompt and thus fails
27
+ if (umiTagsWithOperators.length === 0) {
28
+ return null;
29
+ }
30
+
31
+ const promptSplitToTags = umiTagsWithOperators.replace(']###[', '][').split("][");
32
+
33
+ const clean = (str) => str
34
+ .replaceAll('>', '')
35
+ .replaceAll('<', '')
36
+ .replaceAll('[', '')
37
+ .replaceAll(']', '')
38
+ .trim();
39
+
40
+ const matches = promptSplitToTags.reduce((acc, curr) => {
41
+ let isOptional = curr.includes("|");
42
+ let isNegative = curr.startsWith("--");
43
+ let out;
44
+ if (isOptional) {
45
+ out = {
46
+ hasCursor: curr.includes("###"),
47
+ tags: clean(curr).split('|').map(x => ({
48
+ hasCursor: x.includes("###"),
49
+ isNegative: x.startsWith("--"),
50
+ tag: clean(x).replaceAll("###", '').replaceAll("--", '')
51
+ }))
52
+ };
53
+ acc.optional.push(out);
54
+ acc.all.push(...out.tags.map(x => x.tag));
55
+ } else if (isNegative) {
56
+ out = {
57
+ hasCursor: curr.includes("###"),
58
+ tags: clean(curr).replaceAll("###", '').split('|'),
59
+ };
60
+ out.tags = out.tags.map(x => x.startsWith("--") ? x.substring(2) : x);
61
+ acc.negative.push(out);
62
+ acc.all.push(...out.tags);
63
+ } else {
64
+ out = {
65
+ hasCursor: curr.includes("###"),
66
+ tags: clean(curr).replaceAll("###", '').split('|'),
67
+ };
68
+ acc.positive.push(out);
69
+ acc.all.push(...out.tags);
70
+ }
71
+ return acc;
72
+ }, { positive: [], negative: [], optional: [], all: [] });
73
+
74
+ //console.log({ matches })
75
+
76
+ const filteredWildcards = (tagword) => {
77
+ const wildcards = yamlWildcards.filter(x => {
78
+ let tags = x[1];
79
+ const matchesNeg =
80
+ matches.negative.length === 0
81
+ || matches.negative.every(x =>
82
+ x.hasCursor
83
+ || x.tags.every(t => !tags[t])
84
+ );
85
+ if (!matchesNeg) return false;
86
+ const matchesPos =
87
+ matches.positive.length === 0
88
+ || matches.positive.every(x =>
89
+ x.hasCursor
90
+ || x.tags.every(t => tags[t])
91
+ );
92
+ if (!matchesPos) return false;
93
+ const matchesOpt =
94
+ matches.optional.length === 0
95
+ || matches.optional.some(x =>
96
+ x.tags.some(t =>
97
+ t.hasCursor
98
+ || t.isNegative
99
+ ? !tags[t.tag]
100
+ : tags[t.tag]
101
+ ));
102
+ if (!matchesOpt) return false;
103
+ return true;
104
+ }).reduce((acc, val) => {
105
+ Object.keys(val[1]).forEach(tag => acc[tag] = acc[tag] + 1 || 1);
106
+ return acc;
107
+ }, {});
108
+
109
+ return Object.entries(wildcards)
110
+ .sort((a, b) => b[1] - a[1])
111
+ .filter(x =>
112
+ x[0] === tagword
113
+ || !matches.all.includes(x[0])
114
+ );
115
+ }
116
+
117
+ if (umiTags.length > 0) {
118
+ // Get difference for subprompt
119
+ let tagCountChange = umiTags.length - umiPreviousTags.length;
120
+ let diff = difference(umiTags, umiPreviousTags);
121
+ umiPreviousTags = umiTags;
122
+
123
+ // Show all condition
124
+ let showAll = tagword.endsWith("[") || tagword.endsWith("[--") || tagword.endsWith("|");
125
+
126
+ // Exit early if the user closed the bracket manually
127
+ if ((!diff || diff.length === 0 || (diff.length === 1 && tagCountChange < 0)) && !showAll) {
128
+ if (!hideBlocked) hideResults(textArea);
129
+ return;
130
+ }
131
+
132
+ let umiTagword = diff[0] || '';
133
+ let tempResults = [];
134
+ if (umiTagword && umiTagword.length > 0) {
135
+ umiTagword = umiTagword.toLowerCase().replace(/[\n\r]/g, "");
136
+ originalTagword = tagword;
137
+ tagword = umiTagword;
138
+ let filteredWildcardsSorted = filteredWildcards(umiTagword);
139
+ let searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(umiTagword)}`, 'i')
140
+ let baseFilter = x => x[0].toLowerCase().search(searchRegex) > -1;
141
+ let spaceIncludeFilter = x => x[0].toLowerCase().replaceAll(" ", "_").search(searchRegex) > -1;
142
+ tempResults = filteredWildcardsSorted.filter(x => baseFilter(x) || spaceIncludeFilter(x)) // Filter by tagword
143
+
144
+ // Add final results
145
+ let finalResults = [];
146
+ tempResults.forEach(t => {
147
+ let result = new AutocompleteResult(t[0].trim(), ResultType.yamlWildcard)
148
+ result.count = t[1];
149
+ finalResults.push(result);
150
+ });
151
+
152
+ return finalResults;
153
+ } else if (showAll) {
154
+ let filteredWildcardsSorted = filteredWildcards("");
155
+
156
+ // Add final results
157
+ let finalResults = [];
158
+ filteredWildcardsSorted.forEach(t => {
159
+ let result = new AutocompleteResult(t[0].trim(), ResultType.yamlWildcard)
160
+ result.count = t[1];
161
+ finalResults.push(result);
162
+ });
163
+
164
+ originalTagword = tagword;
165
+ tagword = "";
166
+ return finalResults;
167
+ }
168
+ } else {
169
+ let filteredWildcardsSorted = filteredWildcards("");
170
+
171
+ // Add final results
172
+ let finalResults = [];
173
+ filteredWildcardsSorted.forEach(t => {
174
+ let result = new AutocompleteResult(t[0].trim(), ResultType.yamlWildcard)
175
+ result.count = t[1];
176
+ finalResults.push(result);
177
+ });
178
+
179
+ originalTagword = tagword;
180
+ tagword = "";
181
+ return finalResults;
182
+ }
183
+ }
184
+ }
185
+
186
+ function updateUmiTags( tagType, sanitizedText, newPrompt, textArea) {
187
+ // If it was a yaml wildcard, also update the umiPreviousTags
188
+ if (tagType === ResultType.yamlWildcard && originalTagword.length > 0) {
189
+ let umiSubPrompts = [...newPrompt.matchAll(UMI_PROMPT_REGEX)];
190
+
191
+ let umiTags = [];
192
+ umiSubPrompts.forEach(umiSubPrompt => {
193
+ umiTags = umiTags.concat([...umiSubPrompt[0].matchAll(UMI_TAG_REGEX)].map(x => x[1].toLowerCase()));
194
+ });
195
+
196
+ umiPreviousTags = umiTags;
197
+
198
+ hideResults(textArea);
199
+
200
+ return true;
201
+ }
202
+ return false;
203
+ }
204
+
205
+ async function load() {
206
+ if (yamlWildcards.length === 0) {
207
+ try {
208
+ let yamlTags = (await readFile(`${tagBasePath}/temp/wcet.txt`)).split("\n");
209
+ // Split into tag, count pairs
210
+ yamlWildcards = yamlTags.map(x => x
211
+ .trim()
212
+ .split(","))
213
+ .map(([i, ...rest]) => [
214
+ i,
215
+ rest.reduce((a, b) => {
216
+ a[b.toLowerCase()] = true;
217
+ return a;
218
+ }, {}),
219
+ ]);
220
+ } catch (e) {
221
+ console.error("Error loading yaml wildcards: " + e);
222
+ }
223
+ }
224
+ }
225
+
226
+ function sanitize(tagType, text) {
227
+ // Replace underscores only if the yaml tag is not using them
228
+ if (tagType === ResultType.yamlWildcard && !yamlWildcards.includes(text)) {
229
+ return text.replaceAll("_", " ");
230
+ }
231
+ return null;
232
+ }
233
+
234
+ // Add UMI parser
235
+ PARSERS.push(new UmiParser(UMI_TRIGGER));
236
+
237
+ // Add our utility functions to their respective queues
238
+ QUEUE_FILE_LOAD.push(load);
239
+ QUEUE_SANITIZE.push(sanitize);
240
+ QUEUE_AFTER_INSERT.push(updateUmiTags);
a1111-sd-webui-tagcomplete/javascript/ext_wildcards.js ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Regex
2
+ const WC_REGEX = /\b__([^,]+)__([^, ]*)\b/g;
3
+
4
+ // Trigger conditions
5
+ const WC_TRIGGER = () => CFG.useWildcards && [...tagword.matchAll(WC_REGEX)].length > 0;
6
+ const WC_FILE_TRIGGER = () => CFG.useWildcards && (tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__");
7
+
8
+ class WildcardParser extends BaseTagParser {
9
+ async parse() {
10
+ // Show wildcards from a file with that name
11
+ let wcMatch = [...tagword.matchAll(WC_REGEX)]
12
+ let wcFile = wcMatch[0][1];
13
+ let wcWord = wcMatch[0][2];
14
+
15
+ // Look in normal wildcard files
16
+ let wcFound = wildcardFiles.find(x => x[1].toLowerCase() === wcFile);
17
+ // Use found wildcard file or look in external wildcard files
18
+ let wcPair = wcFound || wildcardExtFiles.find(x => x[1].toLowerCase() === wcFile);
19
+
20
+ let wildcards = (await readFile(`${wcPair[0]}/${wcPair[1]}.txt`)).split("\n")
21
+ .filter(x => x.trim().length > 0 && !x.startsWith('#')); // Remove empty lines and comments
22
+
23
+ let finalResults = [];
24
+ let tempResults = wildcards.filter(x => (wcWord !== null && wcWord.length > 0) ? x.toLowerCase().includes(wcWord) : x) // Filter by tagword
25
+ tempResults.forEach(t => {
26
+ let result = new AutocompleteResult(t.trim(), ResultType.wildcardTag);
27
+ result.meta = wcFile;
28
+ finalResults.push(result);
29
+ });
30
+
31
+ return finalResults;
32
+ }
33
+ }
34
+
35
+ class WildcardFileParser extends BaseTagParser {
36
+ parse() {
37
+ // Show available wildcard files
38
+ let tempResults = [];
39
+ if (tagword !== "__") {
40
+ let lmb = (x) => x[1].toLowerCase().includes(tagword.replace("__", ""))
41
+ tempResults = wildcardFiles.filter(lmb).concat(wildcardExtFiles.filter(lmb)) // Filter by tagword
42
+ } else {
43
+ tempResults = wildcardFiles.concat(wildcardExtFiles);
44
+ }
45
+
46
+ let finalResults = [];
47
+ // Get final results
48
+ tempResults.forEach(wcFile => {
49
+ let result = new AutocompleteResult(wcFile[1].trim(), ResultType.wildcardFile);
50
+ result.meta = "Wildcard file";
51
+ finalResults.push(result);
52
+ });
53
+
54
+ return finalResults;
55
+ }
56
+ }
57
+
58
+ async function load() {
59
+ if (wildcardFiles.length === 0 && wildcardExtFiles.length === 0) {
60
+ try {
61
+ let wcFileArr = (await readFile(`${tagBasePath}/temp/wc.txt`)).split("\n");
62
+ let wcBasePath = wcFileArr[0].trim(); // First line should be the base path
63
+ wildcardFiles = wcFileArr.slice(1)
64
+ .filter(x => x.trim().length > 0) // Remove empty lines
65
+ .map(x => [wcBasePath, x.trim().replace(".txt", "")]); // Remove file extension & newlines
66
+
67
+ // To support multiple sources, we need to separate them using the provided "-----" strings
68
+ let wcExtFileArr = (await readFile(`${tagBasePath}/temp/wce.txt`)).split("\n");
69
+ let splitIndices = [];
70
+ for (let index = 0; index < wcExtFileArr.length; index++) {
71
+ if (wcExtFileArr[index].trim() === "-----") {
72
+ splitIndices.push(index);
73
+ }
74
+ }
75
+ // For each group, add them to the wildcardFiles array with the base path as the first element
76
+ for (let i = 0; i < splitIndices.length; i++) {
77
+ let start = splitIndices[i - 1] || 0;
78
+ if (i > 0) start++; // Skip the "-----" line
79
+ let end = splitIndices[i];
80
+
81
+ let wcExtFile = wcExtFileArr.slice(start, end);
82
+ let base = wcExtFile[0].trim() + "/";
83
+ wcExtFile = wcExtFile.slice(1)
84
+ .filter(x => x.trim().length > 0) // Remove empty lines
85
+ .map(x => x.trim().replace(base, "").replace(".txt", "")); // Remove file extension & newlines;
86
+
87
+ wcExtFile = wcExtFile.map(x => [base, x]);
88
+ wildcardExtFiles.push(...wcExtFile);
89
+ }
90
+ } catch (e) {
91
+ console.error("Error loading wildcards: " + e);
92
+ }
93
+ }
94
+ }
95
+
96
+ function sanitize(tagType, text) {
97
+ if (tagType === ResultType.wildcardFile) {
98
+ return `__${text}__`;
99
+ } else if (tagType === ResultType.wildcardTag) {
100
+ return text.replace(/^.*?: /g, "");
101
+ }
102
+ return null;
103
+ }
104
+
105
+ function keepOpenIfWildcard(tagType, sanitizedText, newPrompt, textArea) {
106
+ // If it's a wildcard, we want to keep the results open so the user can select another wildcard
107
+ if (tagType === ResultType.wildcardFile) {
108
+ hideBlocked = true;
109
+ autocomplete(textArea, newPrompt, sanitizedText);
110
+ setTimeout(() => { hideBlocked = false; }, 100);
111
+ return true;
112
+ }
113
+ return false;
114
+ }
115
+
116
+ // Register the parsers
117
+ PARSERS.push(new WildcardParser(WC_TRIGGER));
118
+ PARSERS.push(new WildcardFileParser(WC_FILE_TRIGGER));
119
+
120
+ // Add our utility functions to their respective queues
121
+ QUEUE_FILE_LOAD.push(load);
122
+ QUEUE_SANITIZE.push(sanitize);
123
+ QUEUE_AFTER_INSERT.push(keepOpenIfWildcard);
a1111-sd-webui-tagcomplete/javascript/tagAutocomplete.js ADDED
@@ -0,0 +1,909 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const styleColors = {
2
+ "--results-bg": ["#0b0f19", "#ffffff"],
3
+ "--results-border-color": ["#4b5563", "#e5e7eb"],
4
+ "--results-border-width": ["1px", "1.5px"],
5
+ "--results-bg-odd": ["#111827", "#f9fafb"],
6
+ "--results-hover": ["#1f2937", "#f5f6f8"],
7
+ "--results-selected": ["#374151", "#e5e7eb"],
8
+ "--meta-text-color": ["#6b6f7b", "#a2a9b4"],
9
+ "--embedding-v1-color": ["lightsteelblue", "#2b5797"],
10
+ "--embedding-v2-color": ["skyblue", "#2d89ef"],
11
+ }
12
+ const browserVars = {
13
+ "--results-overflow-y": {
14
+ "firefox": "scroll",
15
+ "other": "auto"
16
+ }
17
+ }
18
+ // Style for new elements. Gets appended to the Gradio root.
19
+ const autocompleteCSS = `
20
+ #quicksettings [id^=setting_tac] {
21
+ background-color: transparent;
22
+ min-width: fit-content;
23
+ align-self: center;
24
+ }
25
+ #quicksettings [id^=setting_tac] > label > span {
26
+ margin-bottom: 0px;
27
+ }
28
+ [id^=refresh_tac] {
29
+ max-width: 2.5em;
30
+ min-width: 2.5em;
31
+ height: 2.4em;
32
+ }
33
+ .autocompleteResults {
34
+ position: absolute;
35
+ z-index: 999;
36
+ max-width: calc(100% - 1.5rem);
37
+ margin: 5px 0 0 0;
38
+ background-color: var(--results-bg) !important;
39
+ border: var(--results-border-width) solid var(--results-border-color) !important;
40
+ border-radius: 12px !important;
41
+ overflow-y: var(--results-overflow-y);
42
+ overflow-x: hidden;
43
+ word-break: break-word;
44
+ }
45
+ .autocompleteResultsList > li:nth-child(odd) {
46
+ background-color: var(--results-bg-odd);
47
+ }
48
+ .autocompleteResultsList > li {
49
+ list-style-type: none;
50
+ padding: 10px;
51
+ cursor: pointer;
52
+ }
53
+ .autocompleteResultsList > li:hover {
54
+ background-color: var(--results-hover);
55
+ }
56
+ .autocompleteResultsList > li.selected {
57
+ background-color: var(--results-selected);
58
+ }
59
+ .resultsFlexContainer {
60
+ display: flex;
61
+ }
62
+ .acListItem {
63
+ white-space: break-spaces;
64
+ }
65
+ .acMetaText {
66
+ position: relative;
67
+ flex-grow: 1;
68
+ text-align: end;
69
+ padding: 0 0 0 15px;
70
+ white-space: nowrap;
71
+ color: var(--meta-text-color);
72
+ }
73
+ .acWikiLink {
74
+ padding: 0.5rem;
75
+ margin: -0.5rem 0 -0.5rem -0.5rem;
76
+ }
77
+ .acWikiLink:hover {
78
+ text-decoration: underline;
79
+ }
80
+ .acListItem.acEmbeddingV1 {
81
+ color: var(--embedding-v1-color);
82
+ }
83
+ .acListItem.acEmbeddingV2 {
84
+ color: var(--embedding-v2-color);
85
+ }
86
+ `;
87
+
88
+ async function loadTags(c) {
89
+ // Load main tags and aliases
90
+ if (allTags.length === 0 && c.tagFile && c.tagFile !== "None") {
91
+ try {
92
+ allTags = await loadCSV(`${tagBasePath}/${c.tagFile}`);
93
+ } catch (e) {
94
+ console.error("Error loading tags file: " + e);
95
+ return;
96
+ }
97
+ }
98
+ if (c.extra.extraFile && c.extra.extraFile !== "None") {
99
+ try {
100
+ extras = await loadCSV(`${tagBasePath}/${c.extra.extraFile}`);
101
+ } catch (e) {
102
+ console.error("Error loading extra file: " + e);
103
+ return;
104
+ }
105
+ }
106
+ }
107
+
108
+ async function loadTranslations(c) {
109
+ if (c.translation.translationFile && c.translation.translationFile !== "None") {
110
+ try {
111
+ let tArray = await loadCSV(`${tagBasePath}/${c.translation.translationFile}`);
112
+ tArray.forEach(t => {
113
+ if (c.translation.oldFormat)
114
+ translations.set(t[0], t[2]);
115
+ else
116
+ translations.set(t[0], t[1]);
117
+ });
118
+ } catch (e) {
119
+ console.error("Error loading translations file: " + e);
120
+ return;
121
+ }
122
+ }
123
+ }
124
+
125
+ async function syncOptions() {
126
+ let newCFG = {
127
+ // Main tag file
128
+ tagFile: opts["tac_tagFile"],
129
+ // Active in settings
130
+ activeIn: {
131
+ global: opts["tac_active"],
132
+ txt2img: opts["tac_activeIn.txt2img"],
133
+ img2img: opts["tac_activeIn.img2img"],
134
+ negativePrompts: opts["tac_activeIn.negativePrompts"],
135
+ thirdParty: opts["tac_activeIn.thirdParty"],
136
+ modelList: opts["tac_activeIn.modelList"],
137
+ modelListMode: opts["tac_activeIn.modelListMode"]
138
+ },
139
+ // Results related settings
140
+ maxResults: opts["tac_maxResults"],
141
+ showAllResults: opts["tac_showAllResults"],
142
+ resultStepLength: opts["tac_resultStepLength"],
143
+ delayTime: opts["tac_delayTime"],
144
+ useWildcards: opts["tac_useWildcards"],
145
+ useEmbeddings: opts["tac_useEmbeddings"],
146
+ useHypernetworks: opts["tac_useHypernetworks"],
147
+ useLoras: opts["tac_useLoras"],
148
+ showWikiLinks: opts["tac_showWikiLinks"],
149
+ // Insertion related settings
150
+ replaceUnderscores: opts["tac_replaceUnderscores"],
151
+ escapeParentheses: opts["tac_escapeParentheses"],
152
+ appendComma: opts["tac_appendComma"],
153
+ // Alias settings
154
+ alias: {
155
+ searchByAlias: opts["tac_alias.searchByAlias"],
156
+ onlyShowAlias: opts["tac_alias.onlyShowAlias"]
157
+ },
158
+ // Translation settings
159
+ translation: {
160
+ translationFile: opts["tac_translation.translationFile"],
161
+ oldFormat: opts["tac_translation.oldFormat"],
162
+ searchByTranslation: opts["tac_translation.searchByTranslation"],
163
+ },
164
+ // Extra file settings
165
+ extra: {
166
+ extraFile: opts["tac_extra.extraFile"],
167
+ addMode: opts["tac_extra.addMode"]
168
+ },
169
+ // Settings not from tac but still used by the script
170
+ extraNetworksDefaultMultiplier: opts["extra_networks_default_multiplier"]
171
+ }
172
+
173
+ if (CFG && CFG.colors) {
174
+ newCFG["colors"] = CFG.colors;
175
+ }
176
+ if (newCFG.alias.onlyShowAlias) {
177
+ newCFG.alias.searchByAlias = true; // if only show translation, enable search by translation is necessary
178
+ }
179
+
180
+ // Reload tags if the tag file changed
181
+ if (!CFG || newCFG.tagFile !== CFG.tagFile || newCFG.extra.extraFile !== CFG.extra.extraFile) {
182
+ allTags = [];
183
+ await loadTags(newCFG);
184
+ }
185
+ // Reload translations if the translation file changed
186
+ if (!CFG || newCFG.translation.translationFile !== CFG.translation.translationFile) {
187
+ translations.clear();
188
+ await loadTranslations(newCFG);
189
+ }
190
+
191
+ // Update CSS if maxResults changed
192
+ if (CFG && newCFG.maxResults !== CFG.maxResults) {
193
+ gradioApp().querySelectorAll(".autocompleteResults").forEach(r => {
194
+ r.style.maxHeight = `${newCFG.maxResults * 50}px`;
195
+ });
196
+ }
197
+
198
+ // Apply changes
199
+ CFG = newCFG;
200
+
201
+ // Callback
202
+ await processQueue(QUEUE_AFTER_CONFIG_CHANGE, null);
203
+ }
204
+
205
+ // Create the result list div and necessary styling
206
+ function createResultsDiv(textArea) {
207
+ let resultsDiv = document.createElement("div");
208
+ let resultsList = document.createElement('ul');
209
+
210
+ let textAreaId = getTextAreaIdentifier(textArea);
211
+ let typeClass = textAreaId.replaceAll(".", " ");
212
+
213
+ resultsDiv.style.maxHeight = `${CFG.maxResults * 50}px`;
214
+ resultsDiv.setAttribute('class', `autocompleteResults ${typeClass}`);
215
+ resultsList.setAttribute('class', 'autocompleteResultsList');
216
+ resultsDiv.appendChild(resultsList);
217
+
218
+ return resultsDiv;
219
+ }
220
+
221
+ // Show or hide the results div
222
+ function isVisible(textArea) {
223
+ let textAreaId = getTextAreaIdentifier(textArea);
224
+ let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
225
+ return resultsDiv.style.display === "block";
226
+ }
227
+ function showResults(textArea) {
228
+ let textAreaId = getTextAreaIdentifier(textArea);
229
+ let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
230
+ resultsDiv.style.display = "block";
231
+ }
232
+ function hideResults(textArea) {
233
+ let textAreaId = getTextAreaIdentifier(textArea);
234
+ let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
235
+ resultsDiv.style.display = "none";
236
+ selectedTag = null;
237
+ }
238
+
239
+ // Function to check activation criteria
240
+ function isEnabled() {
241
+ if (CFG.activeIn.global) {
242
+ let modelList = CFG.activeIn.modelList
243
+ .split(",")
244
+ .map(x => x.trim())
245
+ .filter(x => x.length > 0);
246
+
247
+ let shortHash = currentModelHash.substring(0, 10);
248
+ if (CFG.activeIn.modelListMode.toLowerCase() === "blacklist") {
249
+ // If the current model is in the blacklist, disable
250
+ return modelList.filter(x => x === currentModelName || x === currentModelHash || x === shortHash).length === 0;
251
+ } else {
252
+ // If the current model is in the whitelist, enable.
253
+ // An empty whitelist is ignored.
254
+ return modelList.length === 0 || modelList.filter(x => x === currentModelName || x === currentModelHash || x === shortHash).length > 0;
255
+ }
256
+ } else {
257
+ return false;
258
+ }
259
+ }
260
+
261
+ const WEIGHT_REGEX = /[([]([^,()[\]:| ]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]/g;
262
+ const POINTY_REGEX = /<[^\s,<](?:[^\t\n\r,<>]*>|[^\t\n\r,> ]*)/g;
263
+ const COMPLETED_WILDCARD_REGEX = /__[^\s,_][^\t\n\r,_]*[^\s,_]__[^\s,_]*/g;
264
+ const NORMAL_TAG_REGEX = /[^\s,|<>]+|</g;
265
+ const TAG_REGEX = new RegExp(`${POINTY_REGEX.source}|${COMPLETED_WILDCARD_REGEX.source}|${NORMAL_TAG_REGEX.source}`, "g");
266
+
267
+ // On click, insert the tag into the prompt textbox with respect to the cursor position
268
+ async function insertTextAtCursor(textArea, result, tagword) {
269
+ let text = result.text;
270
+ let tagType = result.type;
271
+
272
+ let cursorPos = textArea.selectionStart;
273
+ var sanitizedText = text
274
+
275
+ // Run sanitize queue and use first result as sanitized text
276
+ sanitizeResults = await processQueueReturn(QUEUE_SANITIZE, null, tagType, text);
277
+
278
+ if (sanitizeResults && sanitizeResults.length > 0) {
279
+ sanitizedText = sanitizeResults[0];
280
+ } else {
281
+ sanitizedText = CFG.replaceUnderscores ? text.replaceAll("_", " ") : text;
282
+
283
+ if (CFG.escapeParentheses && tagType === ResultType.tag) {
284
+ sanitizedText = sanitizedText
285
+ .replaceAll("(", "\\(")
286
+ .replaceAll(")", "\\)")
287
+ .replaceAll("[", "\\[")
288
+ .replaceAll("]", "\\]");
289
+ }
290
+ }
291
+
292
+ var prompt = textArea.value;
293
+
294
+ // Edit prompt text
295
+ let editStart = Math.max(cursorPos - tagword.length, 0);
296
+ let editEnd = Math.min(cursorPos + tagword.length, prompt.length);
297
+ let surrounding = prompt.substring(editStart, editEnd);
298
+ let match = surrounding.match(new RegExp(escapeRegExp(`${tagword}`), "i"));
299
+ let afterInsertCursorPos = editStart + match.index + sanitizedText.length;
300
+
301
+ var optionalComma = "";
302
+ if (CFG.appendComma && ![ResultType.wildcardFile, ResultType.yamlWildcard].includes(tagType)) {
303
+ optionalComma = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null ? "" : ", ";
304
+ }
305
+
306
+ // Replace partial tag word with new text, add comma if needed
307
+ let insert = surrounding.replace(match, sanitizedText + optionalComma);
308
+
309
+ // Add back start
310
+ var newPrompt = prompt.substring(0, editStart) + insert + prompt.substring(editEnd);
311
+ textArea.value = newPrompt;
312
+ textArea.selectionStart = afterInsertCursorPos + optionalComma.length;
313
+ textArea.selectionEnd = textArea.selectionStart
314
+
315
+ // 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.
316
+ // Uses a built-in method from the webui's ui.js which also already accounts for event target
317
+ updateInput(textArea);
318
+
319
+ // Update previous tags with the edited prompt to prevent re-searching the same term
320
+ let weightedTags = [...newPrompt.matchAll(WEIGHT_REGEX)]
321
+ .map(match => match[1]);
322
+ let tags = newPrompt.match(TAG_REGEX)
323
+ if (weightedTags !== null) {
324
+ tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted)))
325
+ .concat(weightedTags);
326
+ }
327
+ previousTags = tags;
328
+
329
+ // Callback
330
+ let returns = await processQueueReturn(QUEUE_AFTER_INSERT, null, tagType, sanitizedText, newPrompt, textArea);
331
+ // Return if any queue function returned true (has handled hide/show already)
332
+ if (returns.some(x => x === true))
333
+ return;
334
+
335
+ // Hide results after inserting, if it hasn't been hidden already by a queue function
336
+ if (!hideBlocked && isVisible(textArea)) {
337
+ hideResults(textArea);
338
+ }
339
+ }
340
+
341
+ function addResultsToList(textArea, results, tagword, resetList) {
342
+ let textAreaId = getTextAreaIdentifier(textArea);
343
+ let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
344
+ let resultsList = resultDiv.querySelector('ul');
345
+
346
+ // Reset list, selection and scrollTop since the list changed
347
+ if (resetList) {
348
+ resultsList.innerHTML = "";
349
+ selectedTag = null;
350
+ resultDiv.scrollTop = 0;
351
+ resultCount = 0;
352
+ }
353
+
354
+ // Find right colors from config
355
+ let tagFileName = CFG.tagFile.split(".")[0];
356
+ let tagColors = CFG.colors;
357
+ let mode = gradioApp().querySelector('.dark') ? 0 : 1;
358
+ let nextLength = Math.min(results.length, resultCount + CFG.resultStepLength);
359
+
360
+ for (let i = resultCount; i < nextLength; i++) {
361
+ let result = results[i];
362
+
363
+ // Skip if the result is null or undefined
364
+ if (!result)
365
+ continue;
366
+
367
+ let li = document.createElement("li");
368
+
369
+ let flexDiv = document.createElement("div");
370
+ flexDiv.classList.add("resultsFlexContainer");
371
+ li.appendChild(flexDiv);
372
+
373
+ let itemText = document.createElement("div");
374
+ itemText.classList.add("acListItem");
375
+
376
+ let displayText = "";
377
+ // If the tag matches the tagword, we don't need to display the alias
378
+ if (result.aliases && !result.text.includes(tagword)) { // Alias
379
+ let splitAliases = result.aliases.split(",");
380
+ let bestAlias = splitAliases.find(a => a.toLowerCase().includes(tagword));
381
+
382
+ // search in translations if no alias matches
383
+ if (!bestAlias) {
384
+ let tagOrAlias = pair => pair[0] === result.text || splitAliases.includes(pair[0]);
385
+ var tArray = [...translations];
386
+ if (tArray) {
387
+ var translationKey = [...translations].find(pair => tagOrAlias(pair) && pair[1].includes(tagword));
388
+ if (translationKey)
389
+ bestAlias = translationKey[0];
390
+ }
391
+ }
392
+
393
+ displayText = escapeHTML(bestAlias);
394
+
395
+ // Append translation for alias if it exists and is not what the user typed
396
+ if (translations.has(bestAlias) && translations.get(bestAlias) !== bestAlias && bestAlias !== result.text)
397
+ displayText += `[${translations.get(bestAlias)}]`;
398
+
399
+ if (!CFG.alias.onlyShowAlias && result.text !== bestAlias)
400
+ displayText += " ➝ " + result.text;
401
+ } else { // No alias
402
+ displayText = escapeHTML(result.text);
403
+ }
404
+
405
+ // Append translation for result if it exists
406
+ if (translations.has(result.text))
407
+ displayText += ` → ${translations.get(result.text)}`;
408
+
409
+ // Print search term bolded in result
410
+ itemText.innerHTML = displayText.replace(tagword, `<b>${tagword}</b>`);
411
+
412
+ // Add wiki link if the setting is enabled and a supported tag set loaded
413
+ if (CFG.showWikiLinks
414
+ && (result.type === ResultType.tag)
415
+ && (tagFileName.toLowerCase().startsWith("danbooru") || tagFileName.toLowerCase().startsWith("e621"))) {
416
+ let wikiLink = document.createElement("a");
417
+ wikiLink.classList.add("acWikiLink");
418
+ wikiLink.innerText = "?";
419
+
420
+ let linkPart = displayText;
421
+ // Only use alias result if it is one
422
+ if (displayText.includes("➝"))
423
+ linkPart = displayText.split(" ➝ ")[1];
424
+
425
+ // Set link based on selected file
426
+ let tagFileNameLower = tagFileName.toLowerCase();
427
+ if (tagFileNameLower.startsWith("danbooru")) {
428
+ wikiLink.href = `https://danbooru.donmai.us/wiki_pages/${linkPart}`;
429
+ } else if (tagFileNameLower.startsWith("e621")) {
430
+ wikiLink.href = `https://e621.net/wiki_pages/${linkPart}`;
431
+ }
432
+
433
+ wikiLink.target = "_blank";
434
+ flexDiv.appendChild(wikiLink);
435
+ }
436
+
437
+ flexDiv.appendChild(itemText);
438
+
439
+ // Add post count & color if it's a tag
440
+ // Wildcards & Embeds have no tag category
441
+ if (result.category) {
442
+ // Set the color of the tag
443
+ let cat = result.category;
444
+ let colorGroup = tagColors[tagFileName];
445
+ // Default to danbooru scheme if no matching one is found
446
+ if (!colorGroup)
447
+ colorGroup = tagColors["danbooru"];
448
+
449
+ // Set tag type to invalid if not found
450
+ if (!colorGroup[cat])
451
+ cat = "-1";
452
+
453
+ flexDiv.style = `color: ${colorGroup[cat][mode]};`;
454
+ }
455
+
456
+ // Post count
457
+ if (result.count && !isNaN(result.count)) {
458
+ let postCount = result.count;
459
+ let formatter;
460
+
461
+ // Danbooru formats numbers with a padded fraction for 1M or 1k, but not for 10/100k
462
+ if (postCount >= 1000000 || (postCount >= 1000 && postCount < 10000))
463
+ formatter = Intl.NumberFormat("en", { notation: "compact", minimumFractionDigits: 1, maximumFractionDigits: 1 });
464
+ else
465
+ formatter = Intl.NumberFormat("en", {notation: "compact"});
466
+
467
+ let formattedCount = formatter.format(postCount);
468
+
469
+ let countDiv = document.createElement("div");
470
+ countDiv.textContent = formattedCount;
471
+ countDiv.classList.add("acMetaText");
472
+ flexDiv.appendChild(countDiv);
473
+ } else if (result.meta) { // Check if there is meta info to display
474
+ let metaDiv = document.createElement("div");
475
+ metaDiv.textContent = result.meta;
476
+ metaDiv.classList.add("acMetaText");
477
+
478
+ // Add version info classes if it is an embedding
479
+ if (result.type === ResultType.embedding) {
480
+ if (result.meta.startsWith("v1"))
481
+ itemText.classList.add("acEmbeddingV1");
482
+ else if (result.meta.startsWith("v2"))
483
+ itemText.classList.add("acEmbeddingV2");
484
+ }
485
+
486
+ flexDiv.appendChild(metaDiv);
487
+ }
488
+
489
+ // Add listener
490
+ li.addEventListener("click", function () { insertTextAtCursor(textArea, result, tagword); });
491
+ // Add element to list
492
+ resultsList.appendChild(li);
493
+ }
494
+ resultCount = nextLength;
495
+
496
+ if (resetList)
497
+ resultDiv.scrollTop = 0;
498
+ }
499
+
500
+ function updateSelectionStyle(textArea, newIndex, oldIndex) {
501
+ let textAreaId = getTextAreaIdentifier(textArea);
502
+ let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
503
+ let resultsList = resultDiv.querySelector('ul');
504
+ let items = resultsList.getElementsByTagName('li');
505
+
506
+ if (oldIndex != null) {
507
+ items[oldIndex].classList.remove('selected');
508
+ }
509
+
510
+ // make it safer
511
+ if (newIndex !== null) {
512
+ items[newIndex].classList.add('selected');
513
+ }
514
+
515
+ // Set scrolltop to selected item if we are showing more than max results
516
+ if (items.length > CFG.maxResults) {
517
+ let selected = items[newIndex];
518
+ resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
519
+ }
520
+ }
521
+
522
+ async function autocomplete(textArea, prompt, fixedTag = null) {
523
+ // Return if the function is deactivated in the UI
524
+ if (!isEnabled()) return;
525
+
526
+ // Guard for empty prompt
527
+ if (prompt.length === 0) {
528
+ hideResults(textArea);
529
+ previousTags = [];
530
+ tagword = "";
531
+ return;
532
+ }
533
+
534
+ if (fixedTag === null) {
535
+ // Match tags with RegEx to get the last edited one
536
+ // We also match for the weighting format (e.g. "tag:1.0") here, and combine the two to get the full tag word set
537
+ let weightedTags = [...prompt.matchAll(WEIGHT_REGEX)]
538
+ .map(match => match[1]);
539
+ let tags = prompt.match(TAG_REGEX)
540
+ if (weightedTags !== null && tags !== null) {
541
+ tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted) && !tag.startsWith("<[")))
542
+ .concat(weightedTags);
543
+ }
544
+
545
+ // Guard for no tags
546
+ if (!tags || tags.length === 0) {
547
+ previousTags = [];
548
+ tagword = "";
549
+ hideResults(textArea);
550
+ return;
551
+ }
552
+
553
+ let tagCountChange = tags.length - previousTags.length;
554
+ let diff = difference(tags, previousTags);
555
+ previousTags = tags;
556
+
557
+ // Guard for no difference / only whitespace remaining / last edited tag was fully removed
558
+ if (diff === null || diff.length === 0 || (diff.length === 1 && tagCountChange < 0)) {
559
+ if (!hideBlocked) hideResults(textArea);
560
+ return;
561
+ }
562
+
563
+ tagword = diff[0]
564
+
565
+ // Guard for empty tagword
566
+ if (tagword === null || tagword.length === 0) {
567
+ hideResults(textArea);
568
+ return;
569
+ }
570
+ } else {
571
+ tagword = fixedTag;
572
+ }
573
+
574
+ results = [];
575
+ tagword = tagword.toLowerCase().replace(/[\n\r]/g, "");
576
+
577
+ // Process all parsers
578
+ let resultCandidates = await processParsers(textArea, prompt);
579
+ // If one ore more result candidates match, use their results
580
+ if (resultCandidates && resultCandidates.length > 0) {
581
+ // Flatten our candidate(s)
582
+ results = resultCandidates.flat();
583
+ // If there was more than one candidate, sort the results by text to mix them
584
+ // instead of having them added in the order of the parsers
585
+ let shouldSort = resultCandidates.length > 1;
586
+ if (shouldSort) {
587
+ results = results.sort((a, b) => a.text.localeCompare(b.text));
588
+
589
+ // Since some tags are kaomoji, we have to add the normal results in some cases
590
+ if (tagword.startsWith("<") || tagword.startsWith("*<")) {
591
+ // Create escaped search regex with support for * as a start placeholder
592
+ let searchRegex;
593
+ if (tagword.startsWith("*")) {
594
+ tagword = tagword.slice(1);
595
+ searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i');
596
+ } else {
597
+ searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
598
+ }
599
+ let genericResults = allTags.filter(x => x[0].toLowerCase().search(searchRegex) > -1).slice(0, CFG.maxResults);
600
+
601
+ genericResults.forEach(g => {
602
+ let result = new AutocompleteResult(g[0].trim(), ResultType.tag)
603
+ result.category = g[1];
604
+ result.count = g[2];
605
+ result.aliases = g[3];
606
+ results.push(result);
607
+ });
608
+ }
609
+ }
610
+ } else { // Else search the normal tag list
611
+ // Create escaped search regex with support for * as a start placeholder
612
+ let searchRegex;
613
+ if (tagword.startsWith("*")) {
614
+ tagword = tagword.slice(1);
615
+ searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i');
616
+ } else {
617
+ searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
618
+ }
619
+ // If onlyShowAlias is enabled, we don't need to include normal results
620
+ if (CFG.alias.onlyShowAlias) {
621
+ results = allTags.filter(x => x[3] && x[3].toLowerCase().search(searchRegex) > -1);
622
+ } else {
623
+ // Else both normal tags and aliases/translations are included depending on the config
624
+ let baseFilter = (x) => x[0].toLowerCase().search(searchRegex) > -1;
625
+ let aliasFilter = (x) => x[3] && x[3].toLowerCase().search(searchRegex) > -1;
626
+ let translationFilter = (x) => (translations.has(x[0]) && translations.get(x[0]).toLowerCase().search(searchRegex) > -1)
627
+ || x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().search(searchRegex) > -1);
628
+
629
+ let fil;
630
+ if (CFG.alias.searchByAlias && CFG.translation.searchByTranslation)
631
+ fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x);
632
+ else if (CFG.alias.searchByAlias && !CFG.translation.searchByTranslation)
633
+ fil = (x) => baseFilter(x) || aliasFilter(x);
634
+ else if (CFG.translation.searchByTranslation && !CFG.alias.searchByAlias)
635
+ fil = (x) => baseFilter(x) || translationFilter(x);
636
+ else
637
+ fil = (x) => baseFilter(x);
638
+
639
+ // Add final results
640
+ allTags.filter(fil).forEach(t => {
641
+ let result = new AutocompleteResult(t[0].trim(), ResultType.tag)
642
+ result.category = t[1];
643
+ result.count = t[2];
644
+ result.aliases = t[3];
645
+ results.push(result);
646
+ });
647
+
648
+ // Add extras
649
+ if (CFG.extra.extraFile) {
650
+ let extraResults = [];
651
+
652
+ extras.filter(fil).forEach(e => {
653
+ let result = new AutocompleteResult(e[0].trim(), ResultType.extra)
654
+ result.category = e[1] || 0; // If no category is given, use 0 as the default
655
+ result.meta = e[2] || "Custom tag";
656
+ result.aliases = e[3] || "";
657
+ extraResults.push(result);
658
+ });
659
+
660
+ if (CFG.extra.addMode === "Insert before") {
661
+ results = extraResults.concat(results);
662
+ } else {
663
+ results = results.concat(extraResults);
664
+ }
665
+ }
666
+ }
667
+ // Slice if the user has set a max result count
668
+ if (!CFG.showAllResults) {
669
+ results = results.slice(0, CFG.maxResults);
670
+ }
671
+ }
672
+
673
+ // Guard for empty results
674
+ if (!results || results.length === 0) {
675
+ //console.log('No results found for "' + tagword + '"');
676
+ hideResults(textArea);
677
+ return;
678
+ }
679
+
680
+ addResultsToList(textArea, results, tagword, true);
681
+ showResults(textArea);
682
+ }
683
+
684
+ function navigateInList(textArea, event) {
685
+ // Return if the function is deactivated in the UI or the current model is excluded due to white/blacklist settings
686
+ if (!isEnabled()) return;
687
+
688
+ // Close window if Home or End is pressed while not a keybinding, since it would break completion on leaving the original tag
689
+ if ((event.key === "Home" || event.key === "End") && !Object.values(keymap).includes(event.key)) {
690
+ hideResults(textArea);
691
+ return;
692
+ }
693
+
694
+ // All set keys that are not None or empty are valid
695
+ // Default keys are: ArrowUp, ArrowDown, PageUp, PageDown, Home, End, Enter, Tab, Escape
696
+ validKeys = Object.values(keymap).filter(x => x !== "None" && x !== "");
697
+
698
+ if (!validKeys.includes(event.key)) return;
699
+ if (!isVisible(textArea)) return
700
+ // Return if ctrl key is pressed to not interfere with weight editing shortcut
701
+ if (event.ctrlKey || event.altKey) return;
702
+
703
+ oldSelectedTag = selectedTag;
704
+
705
+ switch (event.key) {
706
+ case keymap["MoveUp"]:
707
+ if (selectedTag === null) {
708
+ selectedTag = resultCount - 1;
709
+ } else {
710
+ selectedTag = (selectedTag - 1 + resultCount) % resultCount;
711
+ }
712
+ break;
713
+ case keymap["MoveDown"]:
714
+ if (selectedTag === null) {
715
+ selectedTag = 0;
716
+ } else {
717
+ selectedTag = (selectedTag + 1) % resultCount;
718
+ }
719
+ break;
720
+ case keymap["JumpUp"]:
721
+ if (selectedTag === null || selectedTag === 0) {
722
+ selectedTag = resultCount - 1;
723
+ } else {
724
+ selectedTag = (Math.max(selectedTag - 5, 0) + resultCount) % resultCount;
725
+ }
726
+ break;
727
+ case keymap["JumpDown"]:
728
+ if (selectedTag === null || selectedTag === resultCount - 1) {
729
+ selectedTag = 0;
730
+ } else {
731
+ selectedTag = Math.min(selectedTag + 5, resultCount - 1) % resultCount;
732
+ }
733
+ break;
734
+ case keymap["JumpToStart"]:
735
+ selectedTag = 0;
736
+ break;
737
+ case keymap["JumpToEnd"]:
738
+ selectedTag = resultCount - 1;
739
+ break;
740
+ case keymap["ChooseSelected"]:
741
+ if (selectedTag !== null) {
742
+ insertTextAtCursor(textArea, results[selectedTag], tagword);
743
+ }
744
+ break;
745
+ case keymap["ChooseFirstOrSelected"]:
746
+ if (selectedTag === null) {
747
+ selectedTag = 0;
748
+ }
749
+ insertTextAtCursor(textArea, results[selectedTag], tagword);
750
+ break;
751
+ case keymap["Close"]:
752
+ hideResults(textArea);
753
+ break;
754
+ }
755
+ if (selectedTag === resultCount - 1
756
+ && (event.key === keymap["MoveUp"] || event.key === keymap["MoveDown"] || event.key === keymap["JumpToStart"] || event.key === keymap["JumpToEnd"])) {
757
+ addResultsToList(textArea, results, tagword, false);
758
+ }
759
+ // Update highlighting
760
+ if (selectedTag !== null)
761
+ updateSelectionStyle(textArea, selectedTag, oldSelectedTag);
762
+
763
+ // Prevent default behavior
764
+ event.preventDefault();
765
+ event.stopPropagation();
766
+ }
767
+
768
+ // One-time setup, triggered from onUiUpdate
769
+ async function setup() {
770
+ // Load key bindings
771
+ keymap = (await readFile(`${tagBasePath}/keymap.json`, true));
772
+
773
+ // Load colors
774
+ CFG["colors"] = (await readFile(`${tagBasePath}/colors.json`, true));
775
+
776
+ // Load external files needed by completion extensions
777
+ await processQueue(QUEUE_FILE_LOAD, null);
778
+
779
+ // Find all textareas
780
+ let textAreas = getTextAreas();
781
+
782
+ // Add event listener to apply settings button so we can mirror the changes to our internal config
783
+ let applySettingsButton = gradioApp().querySelector("#tab_settings #settings_submit") || gradioApp().querySelector("#tab_settings > div > .gr-button-primary");
784
+ applySettingsButton?.addEventListener("click", () => {
785
+ // Wait 500ms to make sure the settings have been applied to the webui opts object
786
+ setTimeout(async () => {
787
+ await syncOptions();
788
+ }, 500);
789
+ });
790
+ // Add change listener to our quicksettings to change our internal config without the apply button for them
791
+ let quicksettings = gradioApp().querySelector('#quicksettings');
792
+ let commonQueryPart = "[id^=setting_tac] > label >";
793
+ quicksettings?.querySelectorAll(`${commonQueryPart} input, ${commonQueryPart} textarea, ${commonQueryPart} select`).forEach(e => {
794
+ e.addEventListener("change", () => {
795
+ setTimeout(async () => {
796
+ await syncOptions();
797
+ }, 500);
798
+ });
799
+ });
800
+
801
+ // Add change listener to model dropdown to react to model changes
802
+ let modelDropdown = gradioApp().querySelector("#setting_sd_model_checkpoint select");
803
+ currentModelName = modelDropdown.value;
804
+ modelDropdown?.addEventListener("change", () => {
805
+ setTimeout(() => {
806
+ currentModelName = modelDropdown.value;
807
+ }, 100);
808
+ });
809
+ // Add mutation observer for the model hash text to also allow hash-based blacklist again
810
+ let modelHashText = gradioApp().querySelector("#sd_checkpoint_hash");
811
+ if (modelHashText) {
812
+ currentModelHash = modelHashText.title
813
+ let modelHashObserver = new MutationObserver((mutationList, observer) => {
814
+ for (const mutation of mutationList) {
815
+ if (mutation.type === "attributes" && mutation.attributeName === "title") {
816
+ currentModelHash = mutation.target.title;
817
+ }
818
+ }
819
+ });
820
+ modelHashObserver.observe(modelHashText, { attributes: true });
821
+ }
822
+
823
+ // Not found, we're on a page without prompt textareas
824
+ if (textAreas.every(v => v === null || v === undefined)) return;
825
+ // Already added or unnecessary to add
826
+ if (gradioApp().querySelector('.autocompleteResults.p')) {
827
+ if (gradioApp().querySelector('.autocompleteResults.n') || !CFG.activeIn.negativePrompts) {
828
+ return;
829
+ }
830
+ } else if (!CFG.activeIn.txt2img && !CFG.activeIn.img2img) {
831
+ return;
832
+ }
833
+
834
+ textAreas.forEach(area => {
835
+ // Return if autocomplete is disabled for the current area type in config
836
+ let textAreaId = getTextAreaIdentifier(area);
837
+ if ((!CFG.activeIn.img2img && textAreaId.includes("img2img"))
838
+ || (!CFG.activeIn.txt2img && textAreaId.includes("txt2img"))
839
+ || (!CFG.activeIn.negativePrompts && textAreaId.includes("n"))
840
+ || (!CFG.activeIn.thirdParty && textAreaId.includes("thirdParty"))) {
841
+ return;
842
+ }
843
+
844
+ // Only add listeners once
845
+ if (!area.classList.contains('autocomplete')) {
846
+ // Add our new element
847
+ var resultsDiv = createResultsDiv(area);
848
+ area.parentNode.insertBefore(resultsDiv, area.nextSibling);
849
+ // Hide by default so it doesn't show up on page load
850
+ hideResults(area);
851
+
852
+ // Add autocomplete event listener
853
+ area.addEventListener('input', debounce(() => autocomplete(area, area.value), CFG.delayTime));
854
+ // Add focusout event listener
855
+ area.addEventListener('focusout', debounce(() => hideResults(area), 400));
856
+ // Add up and down arrow event listener
857
+ area.addEventListener('keydown', (e) => navigateInList(area, e));
858
+ // CompositionEnd fires after the user has finished IME composing
859
+ // We need to block hide here to prevent the enter key from insta-closing the results
860
+ area.addEventListener('compositionend', () => {
861
+ hideBlocked = true;
862
+ setTimeout(() => { hideBlocked = false; }, 100);
863
+ });
864
+
865
+ // Add class so we know we've already added the listeners
866
+ area.classList.add('autocomplete');
867
+ }
868
+ });
869
+
870
+ // Add style to dom
871
+ let acStyle = document.createElement('style');
872
+ //let css = gradioApp().querySelector('.dark') ? autocompleteCSS_dark : autocompleteCSS_light;
873
+ let mode = gradioApp().querySelector('.dark') ? 0 : 1;
874
+ // Check if we are on webkit
875
+ let browser = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 ? "firefox" : "other";
876
+
877
+ let css = autocompleteCSS;
878
+ // Replace vars with actual values (can't use actual css vars because of the way we inject the css)
879
+ Object.keys(styleColors).forEach((key) => {
880
+ css = css.replace(`var(${key})`, styleColors[key][mode]);
881
+ })
882
+ Object.keys(browserVars).forEach((key) => {
883
+ css = css.replace(`var(${key})`, browserVars[key][browser]);
884
+ })
885
+
886
+ if (acStyle.styleSheet) {
887
+ acStyle.styleSheet.cssText = css;
888
+ } else {
889
+ acStyle.appendChild(document.createTextNode(css));
890
+ }
891
+ gradioApp().appendChild(acStyle);
892
+
893
+ // Callback
894
+ await processQueue(QUEUE_AFTER_SETUP, null);
895
+ }
896
+ let loading = false;
897
+ onUiUpdate(async () => {
898
+ if (loading) return;
899
+ if (Object.keys(opts).length === 0) return;
900
+ if (CFG) return;
901
+ loading = true;
902
+ // Get our tag base path from the temp file
903
+ tagBasePath = await readFile(`tmp/tagAutocompletePath.txt`);
904
+ // Load config from webui opts
905
+ await syncOptions();
906
+ // Rest of setup
907
+ setup();
908
+ loading = false;
909
+ });
a1111-sd-webui-tagcomplete/scripts/__pycache__/tag_autocomplete_helper.cpython-310.pyc ADDED
Binary file (11 kB). View file
 
a1111-sd-webui-tagcomplete/scripts/tag_autocomplete_helper.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This helper script scans folders for wildcards and embeddings and writes them
2
+ # to a temporary file to expose it to the javascript side
3
+
4
+ import gradio as gr
5
+ from pathlib import Path
6
+ from modules import scripts, script_callbacks, shared, sd_hijack
7
+ import yaml
8
+ import time
9
+ import threading
10
+
11
+ # Webui root path
12
+ FILE_DIR = Path().absolute()
13
+
14
+ # The extension base path
15
+ EXT_PATH = FILE_DIR.joinpath('extensions')
16
+
17
+ # Tags base path
18
+ TAGS_PATH = Path(scripts.basedir()).joinpath('tags')
19
+
20
+ # The path to the folder containing the wildcards and embeddings
21
+ WILDCARD_PATH = FILE_DIR.joinpath('scripts/wildcards')
22
+ EMB_PATH = Path(shared.cmd_opts.embeddings_dir)
23
+ HYP_PATH = Path(shared.cmd_opts.hypernetwork_dir)
24
+
25
+ try:
26
+ LORA_PATH = Path(shared.cmd_opts.lora_dir)
27
+ except AttributeError:
28
+ LORA_PATH = None
29
+
30
+ def find_ext_wildcard_paths():
31
+ """Returns the path to the extension wildcards folder"""
32
+ found = list(EXT_PATH.glob('*/wildcards/'))
33
+ return found
34
+
35
+
36
+ # The path to the extension wildcards folder
37
+ WILDCARD_EXT_PATHS = find_ext_wildcard_paths()
38
+
39
+ # The path to the temporary files
40
+ STATIC_TEMP_PATH = FILE_DIR.joinpath('tmp') # In the webui root, on windows it exists by default, on linux it doesn't
41
+ TEMP_PATH = TAGS_PATH.joinpath('temp') # Extension specific temp files
42
+
43
+
44
+ def get_wildcards():
45
+ """Returns a list of all wildcards. Works on nested folders."""
46
+ wildcard_files = list(WILDCARD_PATH.rglob("*.txt"))
47
+ resolved = [w.relative_to(WILDCARD_PATH).as_posix(
48
+ ) for w in wildcard_files if w.name != "put wildcards here.txt"]
49
+ return resolved
50
+
51
+
52
+ def get_ext_wildcards():
53
+ """Returns a list of all extension wildcards. Works on nested folders."""
54
+ wildcard_files = []
55
+
56
+ for path in WILDCARD_EXT_PATHS:
57
+ wildcard_files.append(path.relative_to(FILE_DIR).as_posix())
58
+ wildcard_files.extend(p.relative_to(path).as_posix() for p in path.rglob("*.txt") if p.name != "put wildcards here.txt")
59
+ wildcard_files.append("-----")
60
+
61
+ return wildcard_files
62
+
63
+
64
+ def get_ext_wildcard_tags():
65
+ """Returns a list of all tags found in extension YAML files found under a Tags: key."""
66
+ wildcard_tags = {} # { tag: count }
67
+ yaml_files = []
68
+ for path in WILDCARD_EXT_PATHS:
69
+ yaml_files.extend(p for p in path.rglob("*.yml"))
70
+ yaml_files.extend(p for p in path.rglob("*.yaml"))
71
+ count = 0
72
+ for path in yaml_files:
73
+ try:
74
+ with open(path, encoding="utf8") as file:
75
+ data = yaml.safe_load(file)
76
+ for item in data:
77
+ if data[item] and 'Tags' in data[item]:
78
+ wildcard_tags[count] = ','.join(data[item]['Tags'])
79
+ count += 1
80
+ else:
81
+ print('Issue with tags found in ' + path.name + ' at item ' + item)
82
+ except yaml.YAMLError as exc:
83
+ print(exc)
84
+ # Sort by count
85
+ sorted_tags = sorted(wildcard_tags.items(), key=lambda item: item[1], reverse=True)
86
+ output = []
87
+ for tag, count in sorted_tags:
88
+ output.append(f"{tag},{count}")
89
+ return output
90
+
91
+
92
+ def get_embeddings(sd_model):
93
+ """Write a list of all embeddings with their version"""
94
+
95
+ # Version constants
96
+ V1_SHAPE = 768
97
+ V2_SHAPE = 1024
98
+ emb_v1 = []
99
+ emb_v2 = []
100
+ results = []
101
+
102
+ try:
103
+ # Get embedding dict from sd_hijack to separate v1/v2 embeddings
104
+ emb_type_a = sd_hijack.model_hijack.embedding_db.word_embeddings
105
+ emb_type_b = sd_hijack.model_hijack.embedding_db.skipped_embeddings
106
+ # Get the shape of the first item in the dict
107
+ emb_a_shape = -1
108
+ emb_b_shape = -1
109
+ if (len(emb_type_a) > 0):
110
+ emb_a_shape = next(iter(emb_type_a.items()))[1].shape
111
+ if (len(emb_type_b) > 0):
112
+ emb_b_shape = next(iter(emb_type_b.items()))[1].shape
113
+
114
+ # Add embeddings to the correct list
115
+ if (emb_a_shape == V1_SHAPE):
116
+ emb_v1 = list(emb_type_a.keys())
117
+ elif (emb_a_shape == V2_SHAPE):
118
+ emb_v2 = list(emb_type_a.keys())
119
+
120
+ if (emb_b_shape == V1_SHAPE):
121
+ emb_v1 = list(emb_type_b.keys())
122
+ elif (emb_b_shape == V2_SHAPE):
123
+ emb_v2 = list(emb_type_b.keys())
124
+
125
+ # Get shape of current model
126
+ #vec = sd_model.cond_stage_model.encode_embedding_init_text(",", 1)
127
+ #model_shape = vec.shape[1]
128
+ # Show relevant entries at the top
129
+ #if (model_shape == V1_SHAPE):
130
+ # results = [e + ",v1" for e in emb_v1] + [e + ",v2" for e in emb_v2]
131
+ #elif (model_shape == V2_SHAPE):
132
+ # results = [e + ",v2" for e in emb_v2] + [e + ",v1" for e in emb_v1]
133
+ #else:
134
+ # raise AttributeError # Fallback to old method
135
+ results = sorted([e + ",v1" for e in emb_v1] + [e + ",v2" for e in emb_v2], key=lambda x: x.lower())
136
+ except AttributeError:
137
+ print("tag_autocomplete_helper: Old webui version or unrecognized model shape, using fallback for embedding completion.")
138
+ # Get a list of all embeddings in the folder
139
+ all_embeds = [str(e.relative_to(EMB_PATH)) for e in EMB_PATH.rglob("*") if e.suffix in {".bin", ".pt", ".png",'.webp', '.jxl', '.avif'}]
140
+ # Remove files with a size of 0
141
+ all_embeds = [e for e in all_embeds if EMB_PATH.joinpath(e).stat().st_size > 0]
142
+ # Remove file extensions
143
+ all_embeds = [e[:e.rfind('.')] for e in all_embeds]
144
+ results = [e + "," for e in all_embeds]
145
+
146
+ write_to_temp_file('emb.txt', results)
147
+
148
+ def get_hypernetworks():
149
+ """Write a list of all hypernetworks"""
150
+
151
+ # Get a list of all hypernetworks in the folder
152
+ all_hypernetworks = [str(h.name) for h in HYP_PATH.rglob("*") if h.suffix in {".pt"}]
153
+ # Remove file extensions
154
+ return sorted([h[:h.rfind('.')] for h in all_hypernetworks], key=lambda x: x.lower())
155
+
156
+ def get_lora():
157
+ """Write a list of all lora"""
158
+
159
+ # Get a list of all lora in the folder
160
+ all_lora = [str(l.name) for l in LORA_PATH.rglob("*") if l.suffix in {".safetensors", ".ckpt", ".pt"}]
161
+ # Remove file extensions
162
+ return sorted([l[:l.rfind('.')] for l in all_lora], key=lambda x: x.lower())
163
+
164
+
165
+ def write_tag_base_path():
166
+ """Writes the tag base path to a fixed location temporary file"""
167
+ with open(STATIC_TEMP_PATH.joinpath('tagAutocompletePath.txt'), 'w', encoding="utf-8") as f:
168
+ f.write(TAGS_PATH.relative_to(FILE_DIR).as_posix())
169
+
170
+
171
+ def write_to_temp_file(name, data):
172
+ """Writes the given data to a temporary file"""
173
+ with open(TEMP_PATH.joinpath(name), 'w', encoding="utf-8") as f:
174
+ f.write(('\n'.join(data)))
175
+
176
+
177
+ csv_files = []
178
+ csv_files_withnone = []
179
+ def update_tag_files():
180
+ """Returns a list of all potential tag files"""
181
+ global csv_files, csv_files_withnone
182
+ files = [str(t.relative_to(TAGS_PATH)) for t in TAGS_PATH.glob("*.csv")]
183
+ csv_files = files
184
+ csv_files_withnone = ["None"] + files
185
+
186
+
187
+
188
+ # Write the tag base path to a fixed location temporary file
189
+ # to enable the javascript side to find our files regardless of extension folder name
190
+ if not STATIC_TEMP_PATH.exists():
191
+ STATIC_TEMP_PATH.mkdir(exist_ok=True)
192
+
193
+ write_tag_base_path()
194
+ update_tag_files()
195
+
196
+ # Check if the temp path exists and create it if not
197
+ if not TEMP_PATH.exists():
198
+ TEMP_PATH.mkdir(parents=True, exist_ok=True)
199
+
200
+ # Set up files to ensure the script doesn't fail to load them
201
+ # even if no wildcards or embeddings are found
202
+ write_to_temp_file('wc.txt', [])
203
+ write_to_temp_file('wce.txt', [])
204
+ write_to_temp_file('wcet.txt', [])
205
+ write_to_temp_file('hyp.txt', [])
206
+ write_to_temp_file('lora.txt', [])
207
+ # Only reload embeddings if the file doesn't exist, since they are already re-written on model load
208
+ if not TEMP_PATH.joinpath("emb.txt").exists():
209
+ write_to_temp_file('emb.txt', [])
210
+
211
+ # Write wildcards to wc.txt if found
212
+ if WILDCARD_PATH.exists():
213
+ wildcards = [WILDCARD_PATH.relative_to(FILE_DIR).as_posix()] + get_wildcards()
214
+ if wildcards:
215
+ write_to_temp_file('wc.txt', wildcards)
216
+
217
+ # Write extension wildcards to wce.txt if found
218
+ if WILDCARD_EXT_PATHS is not None:
219
+ wildcards_ext = get_ext_wildcards()
220
+ if wildcards_ext:
221
+ write_to_temp_file('wce.txt', wildcards_ext)
222
+ # Write yaml extension wildcards to wcet.txt if found
223
+ wildcards_yaml_ext = get_ext_wildcard_tags()
224
+ if wildcards_yaml_ext:
225
+ write_to_temp_file('wcet.txt', wildcards_yaml_ext)
226
+
227
+ # Write embeddings to emb.txt if found
228
+ if EMB_PATH.exists():
229
+ # Get embeddings after the model loaded callback
230
+ script_callbacks.on_model_loaded(get_embeddings)
231
+
232
+ if HYP_PATH.exists():
233
+ hypernets = get_hypernetworks()
234
+ if hypernets:
235
+ write_to_temp_file('hyp.txt', hypernets)
236
+
237
+ if LORA_PATH is not None and LORA_PATH.exists():
238
+ lora = get_lora()
239
+ if lora:
240
+ write_to_temp_file('lora.txt', lora)
241
+
242
+ # Register autocomplete options
243
+ def on_ui_settings():
244
+ TAC_SECTION = ("tac", "Tag Autocomplete")
245
+ # Main tag file
246
+ shared.opts.add_option("tac_tagFile", shared.OptionInfo("zh_cn.csv", "Tag filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
247
+ # Active in settings
248
+ shared.opts.add_option("tac_active", shared.OptionInfo(True, "Enable Tag Autocompletion", section=TAC_SECTION))
249
+ shared.opts.add_option("tac_activeIn.txt2img", shared.OptionInfo(True, "Active in txt2img (Requires restart)", section=TAC_SECTION))
250
+ shared.opts.add_option("tac_activeIn.img2img", shared.OptionInfo(True, "Active in img2img (Requires restart)", section=TAC_SECTION))
251
+ shared.opts.add_option("tac_activeIn.negativePrompts", shared.OptionInfo(True, "Active in negative prompts (Requires restart)", section=TAC_SECTION))
252
+ shared.opts.add_option("tac_activeIn.thirdParty", shared.OptionInfo(True, "Active in third party textboxes [Dataset Tag Editor] (Requires restart)", section=TAC_SECTION))
253
+ 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))
254
+ 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))
255
+ # Results related settings
256
+ shared.opts.add_option("tac_maxResults", shared.OptionInfo(5, "Maximum results", section=TAC_SECTION))
257
+ shared.opts.add_option("tac_showAllResults", shared.OptionInfo(True, "Show all results", section=TAC_SECTION))
258
+ shared.opts.add_option("tac_resultStepLength", shared.OptionInfo(100, "How many results to load at once", section=TAC_SECTION))
259
+ shared.opts.add_option("tac_delayTime", shared.OptionInfo(100, "Time in ms to wait before triggering completion again (Requires restart)", section=TAC_SECTION))
260
+ shared.opts.add_option("tac_useWildcards", shared.OptionInfo(True, "Search for wildcards", section=TAC_SECTION))
261
+ shared.opts.add_option("tac_useEmbeddings", shared.OptionInfo(True, "Search for embeddings", section=TAC_SECTION))
262
+ shared.opts.add_option("tac_useHypernetworks", shared.OptionInfo(True, "Search for hypernetworks", section=TAC_SECTION))
263
+ shared.opts.add_option("tac_useLoras", shared.OptionInfo(True, "Search for Loras", section=TAC_SECTION))
264
+ 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))
265
+ # Insertion related settings
266
+ shared.opts.add_option("tac_replaceUnderscores", shared.OptionInfo(True, "Replace underscores with spaces on insertion", section=TAC_SECTION))
267
+ shared.opts.add_option("tac_escapeParentheses", shared.OptionInfo(True, "Escape parentheses on insertion", section=TAC_SECTION))
268
+ shared.opts.add_option("tac_appendComma", shared.OptionInfo(True, "Append comma on tag autocompletion", section=TAC_SECTION))
269
+ # Alias settings
270
+ shared.opts.add_option("tac_alias.searchByAlias", shared.OptionInfo(True, "Search by alias", section=TAC_SECTION))
271
+ shared.opts.add_option("tac_alias.onlyShowAlias", shared.OptionInfo(False, "Only show alias", section=TAC_SECTION))
272
+ # Translation settings
273
+ shared.opts.add_option("tac_translation.translationFile", shared.OptionInfo("zh_cn_tr.csv", "Translation filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
274
+ 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))
275
+ shared.opts.add_option("tac_translation.searchByTranslation", shared.OptionInfo(True, "Search by translation", section=TAC_SECTION))
276
+ # Extra file settings
277
+ shared.opts.add_option("tac_extra.extraFile", shared.OptionInfo("None", "Extra filename (for small sets of custom tags)", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
278
+ 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))
279
+
280
+ script_callbacks.on_ui_settings(on_ui_settings)
a1111-sd-webui-tagcomplete/tags/colors.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "zh_cn": {
3
+ "1": ["lightblue", "dodgerblue"],
4
+ "2": ["violet", "darkorchid"],
5
+ "3": ["lightgreen", "darkgreen"]
6
+ }
7
+ }
a1111-sd-webui-tagcomplete/tags/keymap.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "Usage": "For possible values, see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values. To disable a keybinding, leave it empty or set it to 'None'.",
3
+
4
+ "MoveUp": "ArrowUp",
5
+ "MoveDown": "ArrowDown",
6
+ "JumpUp": "PageUp",
7
+ "JumpDown": "PageDown",
8
+ "JumpToStart": "Home",
9
+ "JumpToEnd": "End",
10
+ "ChooseSelected": "Enter",
11
+ "ChooseFirstOrSelected": "Tab",
12
+ "Close": "Escape"
13
+ }
a1111-sd-webui-tagcomplete/tags/zh_cn.csv ADDED
The diff for this file is too large to render. See raw diff
 
a1111-sd-webui-tagcomplete/tags/zh_cn_tr.csv ADDED
The diff for this file is too large to render. See raw diff