├── .gitignore
├── LICENSE
├── README.md
├── README_ZH.md
├── img
├── 0.png
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
└── 6.png
├── javascript
├── __globals.js
├── _baseParser.js
├── _caretPosition.js
├── _result.js
├── _textAreas.js
├── _utils.js
├── ext_embeddings.js
├── ext_hypernets.js
├── ext_loras.js
├── ext_umi.js
├── ext_wildcards.js
└── tagAutocomplete.js
├── scripts
└── tag_autocomplete_helper.py
└── tags
├── colors.json
├── danbooru-10w-zh_cn.csv
├── danbooru-index.csv
├── e621.csv
├── extra-quality-tags.csv
└── keymap.json
/.gitignore:
--------------------------------------------------------------------------------
1 | tags/temp/
2 | __pycache__/
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | AUTOMATIC1111 stable diffusion webui 10万中文tag+自动提示插件 chinese tag plugin
2 |
3 | 10万tag 自动提示简体中文翻译(**安装时需要卸载或关闭已有的a1111-sd-webui-tagcomplete 插件不然会冲突**,删除位置:stable-diffusion-webui\extensions\安装时需要删除或关闭已有的a1111-sd-webui-tagcomplete文件夹 也有可能叫 tagcomplete 这个名字 )
4 |
5 | 推荐stable diffusion学习QQ群:451641031,925505542,680518267
6 |
7 | 新增输入英文同步出中文翻译词功能:
8 |
9 | 
10 |
11 |
12 |
13 | 基于 https://github.com/DominikDoom/a1111-sd-webui-tagcomplete 插件
14 | [](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)
15 |
16 |
17 |
18 |
19 | # stable-diffsion-webui中插件安装方式:
20 |
21 | Extensions -> Install from URL 填写git地址:https://github.com/inlhx/a1111-sd-webui-tagcomplete-10w.git
22 |
23 | 点击Install
24 |
25 | 
26 |
27 | 设置:
28 |
29 | settings->TagComplete
30 |
31 | # 按照下图设置,设置完毕记得重启,如果提示不出来肯定是下图步骤少了或者没重启
32 |
33 | 如果看不见图,可能你被墙了,按照以下文字进行设置:
34 |
35 | settings->TagComplete 选项卡:
36 |
37 | 在页面找到Tag filename 选择:danbooru-index.csv
38 |
39 | 在页面找到Translation filename 选择:danbooru-10w-zh_cn.csv
40 |
41 | 在页面找到:Extra filename (for small sets of custom tags) 设置为空
42 |
43 | # 如果timeout安装不上
44 |
45 | 可以点git仓库下载zip包,然后本地解压目录放到:stable-diffusion-webui\extensions\ 下面重启一下就可以了.
46 |
47 | 
48 |
49 | 
50 |
51 | 
52 |
53 |
54 | 
55 |
56 | 
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/README_ZH.md:
--------------------------------------------------------------------------------
1 | AUTOMATIC1111 stable diffusion webui 10万中文tag+自动提示插件 chinese tag plugin
2 |
3 | 10万tag 自动提示简体中文翻译(**安装时需要卸载或关闭已有的a1111-sd-webui-tagcomplete 插件不然会冲突**,删除位置:stable-diffusion-webui\extensions\安装时需要删除或关闭已有的a1111-sd-webui-tagcomplete文件夹 也有可能叫 tagcomplete 这个名字 )
4 |
5 | 推荐stable diffusion学习QQ群:451641031,925505542,680518267
6 |
7 | 新增输入英文同步出中文翻译词功能:
8 |
9 | 
10 |
11 |
12 |
13 | 基于 https://github.com/DominikDoom/a1111-sd-webui-tagcomplete 插件
14 | [](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)
15 |
16 |
17 |
18 |
19 | # stable-diffsion-webui中插件安装方式:
20 |
21 | Extensions -> Install from URL 填写git地址:https://github.com/inlhx/a1111-sd-webui-tagcomplete-10w.git
22 |
23 | 点击Install
24 |
25 | 
26 |
27 | 设置:
28 |
29 | settings->TagComplete
30 |
31 | # 按照下图设置,设置完毕记得重启,如果提示不出来肯定是下图步骤少了或者没重启
32 |
33 | 如果看不见图,可能你被墙了,按照以下文字进行设置:
34 |
35 | settings->TagComplete 选项卡:
36 |
37 | 在页面找到Tag filename 选择:danbooru-index.csv
38 |
39 | 在页面找到Translation filename 选择:danbooru-10w-zh_cn.csv
40 |
41 | 在页面找到:Extra filename (for small sets of custom tags) 设置为空
42 |
43 | # 如果timeout安装不上
44 |
45 | 可以点git仓库下载zip包,然后本地解压目录放到:stable-diffusion-webui\extensions\ 下面重启一下就可以了.
46 |
47 | 
48 |
49 | 
50 |
51 | 
52 |
53 |
54 | 
55 |
56 | 
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/img/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChinaGPT/a1111-sd-webui-tagcomplete-10w/b963eedf2582ce6404b5a9c368043eb26fa155b0/img/0.png
--------------------------------------------------------------------------------
/img/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChinaGPT/a1111-sd-webui-tagcomplete-10w/b963eedf2582ce6404b5a9c368043eb26fa155b0/img/1.png
--------------------------------------------------------------------------------
/img/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChinaGPT/a1111-sd-webui-tagcomplete-10w/b963eedf2582ce6404b5a9c368043eb26fa155b0/img/2.png
--------------------------------------------------------------------------------
/img/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChinaGPT/a1111-sd-webui-tagcomplete-10w/b963eedf2582ce6404b5a9c368043eb26fa155b0/img/3.png
--------------------------------------------------------------------------------
/img/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChinaGPT/a1111-sd-webui-tagcomplete-10w/b963eedf2582ce6404b5a9c368043eb26fa155b0/img/4.png
--------------------------------------------------------------------------------
/img/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChinaGPT/a1111-sd-webui-tagcomplete-10w/b963eedf2582ce6404b5a9c368043eb26fa155b0/img/5.png
--------------------------------------------------------------------------------
/img/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChinaGPT/a1111-sd-webui-tagcomplete-10w/b963eedf2582ce6404b5a9c368043eb26fa155b0/img/6.png
--------------------------------------------------------------------------------
/javascript/__globals.js:
--------------------------------------------------------------------------------
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 = [];
--------------------------------------------------------------------------------
/javascript/_baseParser.js:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/javascript/_caretPosition.js:
--------------------------------------------------------------------------------
1 | // From https://github.com/component/textarea-caret-position
2 |
3 | // We'll copy the properties below into the mirror div.
4 | // Note that some browsers, such as Firefox, do not concatenate properties
5 | // into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
6 | // so we have to list every single property explicitly.
7 | var properties = [
8 | 'direction', // RTL support
9 | 'boxSizing',
10 | 'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
11 | 'height',
12 | 'overflowX',
13 | 'overflowY', // copy the scrollbar for IE
14 |
15 | 'borderTopWidth',
16 | 'borderRightWidth',
17 | 'borderBottomWidth',
18 | 'borderLeftWidth',
19 | 'borderStyle',
20 |
21 | 'paddingTop',
22 | 'paddingRight',
23 | 'paddingBottom',
24 | 'paddingLeft',
25 |
26 | // https://developer.mozilla.org/en-US/docs/Web/CSS/font
27 | 'fontStyle',
28 | 'fontVariant',
29 | 'fontWeight',
30 | 'fontStretch',
31 | 'fontSize',
32 | 'fontSizeAdjust',
33 | 'lineHeight',
34 | 'fontFamily',
35 |
36 | 'textAlign',
37 | 'textTransform',
38 | 'textIndent',
39 | 'textDecoration', // might not make a difference, but better be safe
40 |
41 | 'letterSpacing',
42 | 'wordSpacing',
43 |
44 | 'tabSize',
45 | 'MozTabSize'
46 |
47 | ];
48 |
49 | var isBrowser = (typeof window !== 'undefined');
50 | var isFirefox = (isBrowser && window.mozInnerScreenX != null);
51 |
52 | function getCaretCoordinates(element, position, options) {
53 | if (!isBrowser) {
54 | throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
55 | }
56 |
57 | var debug = options && options.debug || false;
58 | if (debug) {
59 | var el = document.querySelector('#input-textarea-caret-position-mirror-div');
60 | if (el) el.parentNode.removeChild(el);
61 | }
62 |
63 | // The mirror div will replicate the textarea's style
64 | var div = document.createElement('div');
65 | div.id = 'input-textarea-caret-position-mirror-div';
66 | document.body.appendChild(div);
67 |
68 | var style = div.style;
69 | var computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
70 | var isInput = element.nodeName === 'INPUT';
71 |
72 | // Default textarea styles
73 | style.whiteSpace = 'pre-wrap';
74 | if (!isInput)
75 | style.wordWrap = 'break-word'; // only for textarea-s
76 |
77 | // Position off-screen
78 | style.position = 'absolute'; // required to return coordinates properly
79 | if (!debug)
80 | style.visibility = 'hidden'; // not 'display: none' because we want rendering
81 |
82 | // Transfer the element's properties to the div
83 | properties.forEach(function (prop) {
84 | if (isInput && prop === 'lineHeight') {
85 | // Special case for s because text is rendered centered and line height may be != height
86 | if (computed.boxSizing === "border-box") {
87 | var height = parseInt(computed.height);
88 | var outerHeight =
89 | parseInt(computed.paddingTop) +
90 | parseInt(computed.paddingBottom) +
91 | parseInt(computed.borderTopWidth) +
92 | parseInt(computed.borderBottomWidth);
93 | var targetHeight = outerHeight + parseInt(computed.lineHeight);
94 | if (height > targetHeight) {
95 | style.lineHeight = height - outerHeight + "px";
96 | } else if (height === targetHeight) {
97 | style.lineHeight = computed.lineHeight;
98 | } else {
99 | style.lineHeight = 0;
100 | }
101 | } else {
102 | style.lineHeight = computed.height;
103 | }
104 | } else {
105 | style[prop] = computed[prop];
106 | }
107 | });
108 |
109 | if (isFirefox) {
110 | // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
111 | if (element.scrollHeight > parseInt(computed.height))
112 | style.overflowY = 'scroll';
113 | } else {
114 | style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
115 | }
116 |
117 | div.textContent = element.value.substring(0, position);
118 | // The second special handling for input type="text" vs textarea:
119 | // spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
120 | if (isInput)
121 | div.textContent = div.textContent.replace(/\s/g, '\u00a0');
122 |
123 | var span = document.createElement('span');
124 | // Wrapping must be replicated *exactly*, including when a long word gets
125 | // onto the next line, with whitespace at the end of the line before (#7).
126 | // The *only* reliable way to do that is to copy the *entire* rest of the
127 | // textarea's content into the created at the caret position.
128 | // For inputs, just '.' would be enough, but no need to bother.
129 | span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
130 | div.appendChild(span);
131 |
132 | var coordinates = {
133 | top: span.offsetTop + parseInt(computed['borderTopWidth']),
134 | left: span.offsetLeft + parseInt(computed['borderLeftWidth']),
135 | height: parseInt(computed['lineHeight'])
136 | };
137 |
138 | if (debug) {
139 | span.style.backgroundColor = '#aaa';
140 | } else {
141 | document.body.removeChild(div);
142 | }
143 |
144 | return coordinates;
145 | }
146 |
--------------------------------------------------------------------------------
/javascript/_result.js:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/javascript/_textAreas.js:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/javascript/_utils.js:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/javascript/ext_embeddings.js:
--------------------------------------------------------------------------------
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 !== " x[0].toLowerCase().includes(searchTerm) || x[0].toLowerCase().replaceAll(" ", "_").includes(searchTerm);
17 |
18 | if (versionString)
19 | tempResults = embeddings.filter(x => filterCondition(x) && x[1] && x[1] === versionString); // Filter by tagword
20 | else
21 | tempResults = embeddings.filter(x => filterCondition(x)); // Filter by tagword
22 | } else {
23 | tempResults = embeddings;
24 | }
25 |
26 | // Add final results
27 | let finalResults = [];
28 | tempResults.forEach(t => {
29 | let result = new AutocompleteResult(t[0].trim(), ResultType.embedding)
30 | result.meta = t[1] + " Embedding";
31 | finalResults.push(result);
32 | });
33 |
34 | return finalResults;
35 | }
36 | }
37 |
38 | async function load() {
39 | if (embeddings.length === 0) {
40 | try {
41 | embeddings = (await readFile(`${tagBasePath}/temp/emb.txt`)).split("\n")
42 | .filter(x => x.trim().length > 0) // Remove empty lines
43 | .map(x => x.trim().split(",")); // Split into name, version type pairs
44 | } catch (e) {
45 | console.error("Error loading embeddings.txt: " + e);
46 | }
47 | }
48 | }
49 |
50 | function sanitize(tagType, text) {
51 | if (tagType === ResultType.embedding) {
52 | return text.replace(/^.*?: /g, "");
53 | }
54 | return null;
55 | }
56 |
57 | PARSERS.push(new EmbeddingParser(EMB_TRIGGER));
58 |
59 | // Add our utility functions to their respective queues
60 | QUEUE_FILE_LOAD.push(load);
61 | QUEUE_SANITIZE.push(sanitize);
--------------------------------------------------------------------------------
/javascript/ext_hypernets.js:
--------------------------------------------------------------------------------
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 !== " x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm);
11 | tempResults = hypernetworks.filter(x => filterCondition(x)); // Filter by tagword
12 | } else {
13 | tempResults = hypernetworks;
14 | }
15 |
16 | // Add final results
17 | let finalResults = [];
18 | tempResults.forEach(t => {
19 | let result = new AutocompleteResult(t.trim(), ResultType.hypernetwork)
20 | result.meta = "Hypernetwork";
21 | finalResults.push(result);
22 | });
23 |
24 | return finalResults;
25 | }
26 | }
27 |
28 | async function load() {
29 | if (hypernetworks.length === 0) {
30 | try {
31 | hypernetworks = (await readFile(`${tagBasePath}/temp/hyp.txt`)).split("\n")
32 | .filter(x => x.trim().length > 0) //Remove empty lines
33 | .map(x => x.trim()); // Remove carriage returns and padding if it exists
34 | } catch (e) {
35 | console.error("Error loading hypernetworks.txt: " + e);
36 | }
37 | }
38 | }
39 |
40 | function sanitize(tagType, text) {
41 | if (tagType === ResultType.hypernetwork) {
42 | return ``;
43 | }
44 | return null;
45 | }
46 |
47 | PARSERS.push(new HypernetParser(HYP_TRIGGER));
48 |
49 | // Add our utility functions to their respective queues
50 | QUEUE_FILE_LOAD.push(load);
51 | QUEUE_SANITIZE.push(sanitize);
--------------------------------------------------------------------------------
/javascript/ext_loras.js:
--------------------------------------------------------------------------------
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 !== " x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm);
11 | tempResults = loras.filter(x => filterCondition(x)); // Filter by tagword
12 | } else {
13 | tempResults = loras;
14 | }
15 |
16 | // Add final results
17 | let finalResults = [];
18 | tempResults.forEach(t => {
19 | let result = new AutocompleteResult(t.trim(), ResultType.lora)
20 | result.meta = "Lora";
21 | finalResults.push(result);
22 | });
23 |
24 | return finalResults;
25 | }
26 | }
27 |
28 | async function load() {
29 | if (loras.length === 0) {
30 | try {
31 | loras = (await readFile(`${tagBasePath}/temp/lora.txt`)).split("\n")
32 | .filter(x => x.trim().length > 0) // Remove empty lines
33 | .map(x => x.trim()); // Remove carriage returns and padding if it exists
34 | } catch (e) {
35 | console.error("Error loading lora.txt: " + e);
36 | }
37 | }
38 | }
39 |
40 | function sanitize(tagType, text) {
41 | if (tagType === ResultType.lora) {
42 | return ``;
43 | }
44 | return null;
45 | }
46 |
47 | PARSERS.push(new LoraParser(LORA_TRIGGER));
48 |
49 | // Add our utility functions to their respective queues
50 | QUEUE_FILE_LOAD.push(load);
51 | QUEUE_SANITIZE.push(sanitize);
--------------------------------------------------------------------------------
/javascript/ext_umi.js:
--------------------------------------------------------------------------------
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);
--------------------------------------------------------------------------------
/javascript/ext_wildcards.js:
--------------------------------------------------------------------------------
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);
--------------------------------------------------------------------------------
/javascript/tagAutocomplete.js:
--------------------------------------------------------------------------------
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 | slidingPopup: opts["tac_slidingPopup"],
141 | maxResults: opts["tac_maxResults"],
142 | showAllResults: opts["tac_showAllResults"],
143 | resultStepLength: opts["tac_resultStepLength"],
144 | delayTime: opts["tac_delayTime"],
145 | useWildcards: opts["tac_useWildcards"],
146 | useEmbeddings: opts["tac_useEmbeddings"],
147 | useHypernetworks: opts["tac_useHypernetworks"],
148 | useLoras: opts["tac_useLoras"],
149 | showWikiLinks: opts["tac_showWikiLinks"],
150 | // Insertion related settings
151 | replaceUnderscores: opts["tac_replaceUnderscores"],
152 | escapeParentheses: opts["tac_escapeParentheses"],
153 | appendComma: opts["tac_appendComma"],
154 | // Alias settings
155 | alias: {
156 | searchByAlias: opts["tac_alias.searchByAlias"],
157 | onlyShowAlias: opts["tac_alias.onlyShowAlias"]
158 | },
159 | // Translation settings
160 | translation: {
161 | translationFile: opts["tac_translation.translationFile"],
162 | oldFormat: opts["tac_translation.oldFormat"],
163 | searchByTranslation: opts["tac_translation.searchByTranslation"],
164 | },
165 | // Extra file settings
166 | extra: {
167 | extraFile: opts["tac_extra.extraFile"],
168 | addMode: opts["tac_extra.addMode"]
169 | },
170 | // Settings not from tac but still used by the script
171 | extraNetworksDefaultMultiplier: opts["extra_networks_default_multiplier"]
172 | }
173 |
174 | if (CFG && CFG.colors) {
175 | newCFG["colors"] = CFG.colors;
176 | }
177 | if (newCFG.alias.onlyShowAlias) {
178 | newCFG.alias.searchByAlias = true; // if only show translation, enable search by translation is necessary
179 | }
180 |
181 | // Reload tags if the tag file changed
182 | if (!CFG || newCFG.tagFile !== CFG.tagFile || newCFG.extra.extraFile !== CFG.extra.extraFile) {
183 | allTags = [];
184 | await loadTags(newCFG);
185 | }
186 | // Reload translations if the translation file changed
187 | if (!CFG || newCFG.translation.translationFile !== CFG.translation.translationFile) {
188 | translations.clear();
189 | await loadTranslations(newCFG);
190 | }
191 |
192 | // Update CSS if maxResults changed
193 | if (CFG && newCFG.maxResults !== CFG.maxResults) {
194 | gradioApp().querySelectorAll(".autocompleteResults").forEach(r => {
195 | r.style.maxHeight = `${newCFG.maxResults * 50}px`;
196 | });
197 | }
198 |
199 | // Apply changes
200 | CFG = newCFG;
201 |
202 | // Callback
203 | await processQueue(QUEUE_AFTER_CONFIG_CHANGE, null);
204 | }
205 |
206 | // Create the result list div and necessary styling
207 | function createResultsDiv(textArea) {
208 | let resultsDiv = document.createElement("div");
209 | let resultsList = document.createElement('ul');
210 |
211 | let textAreaId = getTextAreaIdentifier(textArea);
212 | let typeClass = textAreaId.replaceAll(".", " ");
213 |
214 | resultsDiv.style.maxHeight = `${CFG.maxResults * 50}px`;
215 | resultsDiv.setAttribute('class', `autocompleteResults ${typeClass}`);
216 | resultsList.setAttribute('class', 'autocompleteResultsList');
217 | resultsDiv.appendChild(resultsList);
218 |
219 | return resultsDiv;
220 | }
221 |
222 | // Show or hide the results div
223 | function isVisible(textArea) {
224 | let textAreaId = getTextAreaIdentifier(textArea);
225 | let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
226 | return resultsDiv.style.display === "block";
227 | }
228 | function showResults(textArea) {
229 | let textAreaId = getTextAreaIdentifier(textArea);
230 | let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
231 | resultsDiv.style.display = "block";
232 |
233 | if (CFG.slidingPopup) {
234 | let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd).left;
235 | let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - resultsDiv.offsetWidth);
236 |
237 | resultsDiv.style.left = `${offset}px`;
238 | } else {
239 | if (resultsDiv.style.left)
240 | resultsDiv.style.removeProperty("left");
241 | }
242 | }
243 | function hideResults(textArea) {
244 | let textAreaId = getTextAreaIdentifier(textArea);
245 | let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
246 | resultsDiv.style.display = "none";
247 | selectedTag = null;
248 | }
249 |
250 | // Function to check activation criteria
251 | function isEnabled() {
252 | if (CFG.activeIn.global) {
253 | let modelList = CFG.activeIn.modelList
254 | .split(",")
255 | .map(x => x.trim())
256 | .filter(x => x.length > 0);
257 |
258 | let shortHash = currentModelHash.substring(0, 10);
259 | if (CFG.activeIn.modelListMode.toLowerCase() === "blacklist") {
260 | // If the current model is in the blacklist, disable
261 | return modelList.filter(x => x === currentModelName || x === currentModelHash || x === shortHash).length === 0;
262 | } else {
263 | // If the current model is in the whitelist, enable.
264 | // An empty whitelist is ignored.
265 | return modelList.length === 0 || modelList.filter(x => x === currentModelName || x === currentModelHash || x === shortHash).length > 0;
266 | }
267 | } else {
268 | return false;
269 | }
270 | }
271 |
272 | const WEIGHT_REGEX = /[([]([^,()[\]:| ]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]/g;
273 | const POINTY_REGEX = /<[^\s,<](?:[^\t\n\r,<>]*>|[^\t\n\r,> ]*)/g;
274 | const COMPLETED_WILDCARD_REGEX = /__[^\s,_][^\t\n\r,_]*[^\s,_]__[^\s,_]*/g;
275 | const NORMAL_TAG_REGEX = /[^\s,|<>]+| 0) {
290 | sanitizedText = sanitizeResults[0];
291 | } else {
292 | sanitizedText = CFG.replaceUnderscores ? text.replaceAll("_", " ") : text;
293 |
294 | if (CFG.escapeParentheses && tagType === ResultType.tag) {
295 | sanitizedText = sanitizedText
296 | .replaceAll("(", "\\(")
297 | .replaceAll(")", "\\)")
298 | .replaceAll("[", "\\[")
299 | .replaceAll("]", "\\]");
300 | }
301 | }
302 |
303 | var prompt = textArea.value;
304 |
305 | // Edit prompt text
306 | let editStart = Math.max(cursorPos - tagword.length, 0);
307 | let editEnd = Math.min(cursorPos + tagword.length, prompt.length);
308 | let surrounding = prompt.substring(editStart, editEnd);
309 | let match = surrounding.match(new RegExp(escapeRegExp(`${tagword}`), "i"));
310 | let afterInsertCursorPos = editStart + match.index + sanitizedText.length;
311 |
312 | var optionalComma = "";
313 | if (CFG.appendComma && ![ResultType.wildcardFile, ResultType.yamlWildcard].includes(tagType)) {
314 | optionalComma = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null ? "" : ", ";
315 | }
316 |
317 | // Replace partial tag word with new text, add comma if needed
318 | let insert = surrounding.replace(match, sanitizedText + optionalComma);
319 |
320 | // Add back start
321 | var newPrompt = prompt.substring(0, editStart) + insert + prompt.substring(editEnd);
322 | textArea.value = newPrompt;
323 | textArea.selectionStart = afterInsertCursorPos + optionalComma.length;
324 | textArea.selectionEnd = textArea.selectionStart
325 |
326 | // 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.
327 | // Uses a built-in method from the webui's ui.js which also already accounts for event target
328 | updateInput(textArea);
329 |
330 | // Update previous tags with the edited prompt to prevent re-searching the same term
331 | let weightedTags = [...newPrompt.matchAll(WEIGHT_REGEX)]
332 | .map(match => match[1]);
333 | let tags = newPrompt.match(TAG_REGEX)
334 | if (weightedTags !== null) {
335 | tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted)))
336 | .concat(weightedTags);
337 | }
338 | previousTags = tags;
339 |
340 | // Callback
341 | let returns = await processQueueReturn(QUEUE_AFTER_INSERT, null, tagType, sanitizedText, newPrompt, textArea);
342 | // Return if any queue function returned true (has handled hide/show already)
343 | if (returns.some(x => x === true))
344 | return;
345 |
346 | // Hide results after inserting, if it hasn't been hidden already by a queue function
347 | if (!hideBlocked && isVisible(textArea)) {
348 | hideResults(textArea);
349 | }
350 |
351 | ///aliu
352 | // for
353 | // document.getElementById("txt2img_prompt")
354 | // textArea.appendChild(inputElement)
355 |
356 | // updateInput(tb_type)
357 | inputWords(textArea)
358 | // debugger;
359 | }
360 |
361 | //create by aliu
362 | function inputWords(textArea) {
363 | let textAreatxt = textArea.value
364 | let idx = "";
365 | if (textArea.parentNode.parentNode.id=="txt2img_neg_prompt") {
366 | idx = "txt2img_neg_prompt";
367 | } else {
368 | idx = "txt2img_prompt";
369 | }
370 | let alertPromptSpan = gradioApp().querySelector("#" + idx + "Span")
371 | if (textAreatxt == '') {
372 | if (alertPromptSpan != null) {
373 | alertPromptSpan.innerHTML = "";
374 | }
375 | return '';
376 | }
377 | textAreatxt = textAreatxt
378 | .replaceAll("\\(", "(")
379 | .replaceAll("\\)", ")")
380 | .replaceAll("\\[", "[")
381 | .replaceAll("\\]", "]")
382 | .replaceAll(" ,", ",")
383 | .replaceAll(", ", ",")
384 | .replaceAll(",)", ")")
385 | .replaceAll(" )", ")")
386 | .replaceAll("( ", "(");
387 | let txtarr = textAreatxt.split(",");
388 | let alertPrompt = "";
389 | for (i = 0; i < txtarr.length; i++) {
390 | let wordtxt = txtarr[i]
391 | if (wordtxt == '') {
392 | continue;
393 | }
394 |
395 | wordtxt = wordtxt.replaceAll(" ", "_")
396 | let pattern = /\((\w+):\d+\)/;
397 | wordtxt = wordtxt.replace(pattern, "$1");
398 | wordtxt = wordtxt.replaceAll("(", "")
399 | wordtxt = wordtxt.replaceAll(")", "")
400 | wordtxt = wordtxt.replaceAll(",", "")
401 | var tArray = [...translations];
402 | if (tArray) {
403 | let translationKey = [...translations].find(pair => pair[0].includes(wordtxt));
404 | wordtxt = wordtxt.replaceAll("_", " ")
405 | if (translationKey) {
406 | alertPrompt += wordtxt + "=>" + translationKey[1];
407 | } else {
408 | alertPrompt += wordtxt;
409 | }
410 | alertPrompt += ",";
411 | }
412 | // textAreatxt = textAreatxt.replaceAll(" ", "_");//.replaceAll("","").replaceAll("","").replaceAll("","").replaceAll("","")
413 | }
414 |
415 | let alertPromptDiv = gradioApp().querySelector("#" + idx);
416 | if (!alertPromptSpan) {
417 | var spanx = document.createElement("span");
418 | spanx.id = idx + "Span";
419 | spanx.style=" box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.3);background-color:lightyellow;color: #492615; border-bottom:3px solid #A67C52;";
420 | spanx.className="gr-box";
421 | spanx.innerHTML = alertPrompt;
422 | alertPromptDiv.appendChild(spanx);
423 | console.log(alertPrompt)
424 | } else {
425 | alertPromptSpan.innerHTML = alertPrompt;
426 | }
427 |
428 | }
429 |
430 | function addResultsToList(textArea, results, tagword, resetList) {
431 | let textAreaId = getTextAreaIdentifier(textArea);
432 | let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
433 | let resultsList = resultDiv.querySelector('ul');
434 |
435 | // Reset list, selection and scrollTop since the list changed
436 | if (resetList) {
437 | resultsList.innerHTML = "";
438 | selectedTag = null;
439 | resultDiv.scrollTop = 0;
440 | resultCount = 0;
441 | }
442 |
443 | // Find right colors from config
444 | let tagFileName = CFG.tagFile.split(".")[0];
445 | let tagColors = CFG.colors;
446 | let mode = gradioApp().querySelector('.dark') ? 0 : 1;
447 | let nextLength = Math.min(results.length, resultCount + CFG.resultStepLength);
448 |
449 | for (let i = resultCount; i < nextLength; i++) {
450 | let result = results[i];
451 |
452 | // Skip if the result is null or undefined
453 | if (!result)
454 | continue;
455 |
456 | let li = document.createElement("li");
457 |
458 | let flexDiv = document.createElement("div");
459 | flexDiv.classList.add("resultsFlexContainer");
460 | li.appendChild(flexDiv);
461 |
462 | let itemText = document.createElement("div");
463 | itemText.classList.add("acListItem");
464 |
465 | let displayText = "";
466 | // If the tag matches the tagword, we don't need to display the alias
467 | if (result.aliases && !result.text.includes(tagword)) { // Alias
468 | let splitAliases = result.aliases.split(",");
469 | let bestAlias = splitAliases.find(a => a.toLowerCase().includes(tagword));
470 |
471 | // search in translations if no alias matches
472 | if (!bestAlias) {
473 | let tagOrAlias = pair => pair[0] === result.text || splitAliases.includes(pair[0]);
474 | var tArray = [...translations];
475 | if (tArray) {
476 | var translationKey = [...translations].find(pair => tagOrAlias(pair) && pair[1].includes(tagword));
477 | if (translationKey)
478 | bestAlias = translationKey[0];
479 | }
480 | }
481 |
482 | displayText = escapeHTML(bestAlias);
483 |
484 | // Append translation for alias if it exists and is not what the user typed
485 | if (translations.has(bestAlias) && translations.get(bestAlias) !== bestAlias && bestAlias !== result.text)
486 | displayText += `[${translations.get(bestAlias)}]`;
487 |
488 | if (!CFG.alias.onlyShowAlias && result.text !== bestAlias)
489 | displayText += " ➝ " + result.text;
490 | } else { // No alias
491 | displayText = escapeHTML(result.text);
492 | }
493 |
494 | // Append translation for result if it exists
495 | if (translations.has(result.text))
496 | displayText += `[${translations.get(result.text)}]`;
497 |
498 | // Print search term bolded in result
499 | itemText.innerHTML = displayText.replace(tagword, `${tagword}`);
500 |
501 | // Add wiki link if the setting is enabled and a supported tag set loaded
502 | if (CFG.showWikiLinks
503 | && (result.type === ResultType.tag)
504 | && (tagFileName.toLowerCase().startsWith("danbooru") || tagFileName.toLowerCase().startsWith("e621"))) {
505 | let wikiLink = document.createElement("a");
506 | wikiLink.classList.add("acWikiLink");
507 | wikiLink.innerText = "?";
508 |
509 | let linkPart = displayText;
510 | // Only use alias result if it is one
511 | if (displayText.includes("➝"))
512 | linkPart = displayText.split(" ➝ ")[1];
513 |
514 | // Set link based on selected file
515 | let tagFileNameLower = tagFileName.toLowerCase();
516 | if (tagFileNameLower.startsWith("danbooru")) {
517 | wikiLink.href = `https://danbooru.donmai.us/wiki_pages/${linkPart}`;
518 | } else if (tagFileNameLower.startsWith("e621")) {
519 | wikiLink.href = `https://e621.net/wiki_pages/${linkPart}`;
520 | }
521 |
522 | wikiLink.target = "_blank";
523 | flexDiv.appendChild(wikiLink);
524 | }
525 |
526 | flexDiv.appendChild(itemText);
527 |
528 | // Add post count & color if it's a tag
529 | // Wildcards & Embeds have no tag category
530 | if (result.category) {
531 | // Set the color of the tag
532 | let cat = result.category;
533 | let colorGroup = tagColors[tagFileName];
534 | // Default to danbooru scheme if no matching one is found
535 | if (!colorGroup)
536 | colorGroup = tagColors["danbooru"];
537 |
538 | // Set tag type to invalid if not found
539 | if (!colorGroup[cat])
540 | cat = "-1";
541 |
542 | flexDiv.style = `color: ${colorGroup[cat][mode]};`;
543 | }
544 |
545 | // Post count
546 | if (result.count && !isNaN(result.count)) {
547 | let postCount = result.count;
548 | let formatter;
549 |
550 | // Danbooru formats numbers with a padded fraction for 1M or 1k, but not for 10/100k
551 | if (postCount >= 1000000 || (postCount >= 1000 && postCount < 10000))
552 | formatter = Intl.NumberFormat("en", { notation: "compact", minimumFractionDigits: 1, maximumFractionDigits: 1 });
553 | else
554 | formatter = Intl.NumberFormat("en", { notation: "compact" });
555 |
556 | let formattedCount = formatter.format(postCount);
557 |
558 | let countDiv = document.createElement("div");
559 | countDiv.textContent = formattedCount;
560 | countDiv.classList.add("acMetaText");
561 | flexDiv.appendChild(countDiv);
562 | } else if (result.meta) { // Check if there is meta info to display
563 | let metaDiv = document.createElement("div");
564 | metaDiv.textContent = result.meta;
565 | metaDiv.classList.add("acMetaText");
566 |
567 | // Add version info classes if it is an embedding
568 | if (result.type === ResultType.embedding) {
569 | if (result.meta.startsWith("v1"))
570 | itemText.classList.add("acEmbeddingV1");
571 | else if (result.meta.startsWith("v2"))
572 | itemText.classList.add("acEmbeddingV2");
573 | }
574 |
575 | flexDiv.appendChild(metaDiv);
576 | }
577 |
578 | // Add listener
579 | li.addEventListener("click", function () { insertTextAtCursor(textArea, result, tagword); });
580 | // Add element to list
581 | resultsList.appendChild(li);
582 | }
583 | resultCount = nextLength;
584 |
585 | if (resetList)
586 | resultDiv.scrollTop = 0;
587 | }
588 |
589 | function updateSelectionStyle(textArea, newIndex, oldIndex) {
590 | let textAreaId = getTextAreaIdentifier(textArea);
591 | let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
592 | let resultsList = resultDiv.querySelector('ul');
593 | let items = resultsList.getElementsByTagName('li');
594 |
595 | if (oldIndex != null) {
596 | items[oldIndex].classList.remove('selected');
597 | }
598 |
599 | // make it safer
600 | if (newIndex !== null) {
601 | items[newIndex].classList.add('selected');
602 | }
603 |
604 | // Set scrolltop to selected item if we are showing more than max results
605 | if (items.length > CFG.maxResults) {
606 | let selected = items[newIndex];
607 | resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
608 | }
609 | }
610 |
611 | async function autocomplete(textArea, prompt, fixedTag = null) {
612 | // Return if the function is deactivated in the UI
613 | if (!isEnabled()) return;
614 |
615 | // Guard for empty prompt
616 | if (prompt.length === 0) {
617 | hideResults(textArea);
618 | previousTags = [];
619 | tagword = "";
620 | return;
621 | }
622 |
623 | if (fixedTag === null) {
624 | // Match tags with RegEx to get the last edited one
625 | // We also match for the weighting format (e.g. "tag:1.0") here, and combine the two to get the full tag word set
626 | let weightedTags = [...prompt.matchAll(WEIGHT_REGEX)]
627 | .map(match => match[1]);
628 | let tags = prompt.match(TAG_REGEX)
629 | if (weightedTags !== null && tags !== null) {
630 | tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted) && !tag.startsWith("<[")))
631 | .concat(weightedTags);
632 | }
633 |
634 | // Guard for no tags
635 | if (!tags || tags.length === 0) {
636 | previousTags = [];
637 | tagword = "";
638 | hideResults(textArea);
639 | return;
640 | }
641 |
642 | let tagCountChange = tags.length - previousTags.length;
643 | let diff = difference(tags, previousTags);
644 | previousTags = tags;
645 |
646 | // Guard for no difference / only whitespace remaining / last edited tag was fully removed
647 | if (diff === null || diff.length === 0 || (diff.length === 1 && tagCountChange < 0)) {
648 | if (!hideBlocked) hideResults(textArea);
649 | return;
650 | }
651 |
652 | tagword = diff[0]
653 |
654 | // Guard for empty tagword
655 | if (tagword === null || tagword.length === 0) {
656 | hideResults(textArea);
657 | return;
658 | }
659 | } else {
660 | tagword = fixedTag;
661 | }
662 |
663 | results = [];
664 | tagword = tagword.toLowerCase().replace(/[\n\r]/g, "");
665 |
666 | // Process all parsers
667 | let resultCandidates = await processParsers(textArea, prompt);
668 | // If one ore more result candidates match, use their results
669 | if (resultCandidates && resultCandidates.length > 0) {
670 | // Flatten our candidate(s)
671 | results = resultCandidates.flat();
672 | // If there was more than one candidate, sort the results by text to mix them
673 | // instead of having them added in the order of the parsers
674 | let shouldSort = resultCandidates.length > 1;
675 | if (shouldSort) {
676 | results = results.sort((a, b) => a.text.localeCompare(b.text));
677 |
678 | // Since some tags are kaomoji, we have to add the normal results in some cases
679 | if (tagword.startsWith("<") || tagword.startsWith("*<")) {
680 | // Create escaped search regex with support for * as a start placeholder
681 | let searchRegex;
682 | if (tagword.startsWith("*")) {
683 | tagword = tagword.slice(1);
684 | searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i');
685 | } else {
686 | searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
687 | }
688 | let genericResults = allTags.filter(x => x[0].toLowerCase().search(searchRegex) > -1).slice(0, CFG.maxResults);
689 |
690 | genericResults.forEach(g => {
691 | let result = new AutocompleteResult(g[0].trim(), ResultType.tag)
692 | result.category = g[1];
693 | result.count = g[2];
694 | result.aliases = g[3];
695 | results.push(result);
696 | });
697 | }
698 | }
699 | } else { // Else search the normal tag list
700 | // Create escaped search regex with support for * as a start placeholder
701 | let searchRegex;
702 | if (tagword.startsWith("*")) {
703 | tagword = tagword.slice(1);
704 | searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i');
705 | } else {
706 | searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
707 | }
708 | // If onlyShowAlias is enabled, we don't need to include normal results
709 | if (CFG.alias.onlyShowAlias) {
710 | results = allTags.filter(x => x[3] && x[3].toLowerCase().search(searchRegex) > -1);
711 | } else {
712 | // Else both normal tags and aliases/translations are included depending on the config
713 | let baseFilter = (x) => x[0].toLowerCase().search(searchRegex) > -1;
714 | let aliasFilter = (x) => x[3] && x[3].toLowerCase().search(searchRegex) > -1;
715 | let translationFilter = (x) => (translations.has(x[0]) && translations.get(x[0]).toLowerCase().search(searchRegex) > -1)
716 | || x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().search(searchRegex) > -1);
717 |
718 | let fil;
719 | if (CFG.alias.searchByAlias && CFG.translation.searchByTranslation)
720 | fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x);
721 | else if (CFG.alias.searchByAlias && !CFG.translation.searchByTranslation)
722 | fil = (x) => baseFilter(x) || aliasFilter(x);
723 | else if (CFG.translation.searchByTranslation && !CFG.alias.searchByAlias)
724 | fil = (x) => baseFilter(x) || translationFilter(x);
725 | else
726 | fil = (x) => baseFilter(x);
727 |
728 | // Add final results
729 | allTags.filter(fil).forEach(t => {
730 | let result = new AutocompleteResult(t[0].trim(), ResultType.tag)
731 | result.category = t[1];
732 | result.count = t[2];
733 | result.aliases = t[3];
734 | results.push(result);
735 | });
736 |
737 | // Add extras
738 | if (CFG.extra.extraFile) {
739 | let extraResults = [];
740 |
741 | extras.filter(fil).forEach(e => {
742 | let result = new AutocompleteResult(e[0].trim(), ResultType.extra)
743 | result.category = e[1] || 0; // If no category is given, use 0 as the default
744 | result.meta = e[2] || "Custom tag";
745 | result.aliases = e[3] || "";
746 | extraResults.push(result);
747 | });
748 |
749 | if (CFG.extra.addMode === "Insert before") {
750 | results = extraResults.concat(results);
751 | } else {
752 | results = results.concat(extraResults);
753 | }
754 | }
755 | }
756 | // Slice if the user has set a max result count
757 | if (!CFG.showAllResults) {
758 | results = results.slice(0, CFG.maxResults);
759 | }
760 | }
761 |
762 | // Guard for empty results
763 | if (!results || results.length === 0) {
764 | //console.log('No results found for "' + tagword + '"');
765 | hideResults(textArea);
766 | return;
767 | }
768 |
769 | addResultsToList(textArea, results, tagword, true);
770 | showResults(textArea);
771 | }
772 |
773 | function navigateInList(textArea, event) {
774 | // Return if the function is deactivated in the UI or the current model is excluded due to white/blacklist settings
775 | if (!isEnabled()) return;
776 |
777 | // Close window if Home or End is pressed while not a keybinding, since it would break completion on leaving the original tag
778 | if ((event.key === "Home" || event.key === "End") && !Object.values(keymap).includes(event.key)) {
779 | hideResults(textArea);
780 | return;
781 | }
782 |
783 | // All set keys that are not None or empty are valid
784 | // Default keys are: ArrowUp, ArrowDown, PageUp, PageDown, Home, End, Enter, Tab, Escape
785 | validKeys = Object.values(keymap).filter(x => x !== "None" && x !== "");
786 |
787 | if (!validKeys.includes(event.key)) return;
788 | if (!isVisible(textArea)) return
789 | // Return if ctrl key is pressed to not interfere with weight editing shortcut
790 | if (event.ctrlKey || event.altKey) return;
791 |
792 | oldSelectedTag = selectedTag;
793 |
794 | switch (event.key) {
795 | case keymap["MoveUp"]:
796 | if (selectedTag === null) {
797 | selectedTag = resultCount - 1;
798 | } else {
799 | selectedTag = (selectedTag - 1 + resultCount) % resultCount;
800 | }
801 | break;
802 | case keymap["MoveDown"]:
803 | if (selectedTag === null) {
804 | selectedTag = 0;
805 | } else {
806 | selectedTag = (selectedTag + 1) % resultCount;
807 | }
808 | break;
809 | case keymap["JumpUp"]:
810 | if (selectedTag === null || selectedTag === 0) {
811 | selectedTag = resultCount - 1;
812 | } else {
813 | selectedTag = (Math.max(selectedTag - 5, 0) + resultCount) % resultCount;
814 | }
815 | break;
816 | case keymap["JumpDown"]:
817 | if (selectedTag === null || selectedTag === resultCount - 1) {
818 | selectedTag = 0;
819 | } else {
820 | selectedTag = Math.min(selectedTag + 5, resultCount - 1) % resultCount;
821 | }
822 | break;
823 | case keymap["JumpToStart"]:
824 | selectedTag = 0;
825 | break;
826 | case keymap["JumpToEnd"]:
827 | selectedTag = resultCount - 1;
828 | break;
829 | case keymap["ChooseSelected"]:
830 | if (selectedTag !== null) {
831 | insertTextAtCursor(textArea, results[selectedTag], tagword);
832 | }
833 | break;
834 | case keymap["ChooseFirstOrSelected"]:
835 | if (selectedTag === null) {
836 | selectedTag = 0;
837 | }
838 | insertTextAtCursor(textArea, results[selectedTag], tagword);
839 | break;
840 | case keymap["Close"]:
841 | hideResults(textArea);
842 | break;
843 | }
844 | if (selectedTag === resultCount - 1
845 | && (event.key === keymap["MoveUp"] || event.key === keymap["MoveDown"] || event.key === keymap["JumpToStart"] || event.key === keymap["JumpToEnd"])) {
846 | addResultsToList(textArea, results, tagword, false);
847 | }
848 | // Update highlighting
849 | if (selectedTag !== null)
850 | updateSelectionStyle(textArea, selectedTag, oldSelectedTag);
851 |
852 | // Prevent default behavior
853 | event.preventDefault();
854 | event.stopPropagation();
855 | }
856 |
857 | // One-time setup, triggered from onUiUpdate
858 | async function setup() {
859 | // Load key bindings
860 | keymap = (await readFile(`${tagBasePath}/keymap.json`, true));
861 |
862 | // Load colors
863 | CFG["colors"] = (await readFile(`${tagBasePath}/colors.json`, true));
864 |
865 | // Load external files needed by completion extensions
866 | await processQueue(QUEUE_FILE_LOAD, null);
867 |
868 | // Find all textareas
869 | let textAreas = getTextAreas();
870 |
871 | // Add event listener to apply settings button so we can mirror the changes to our internal config
872 | let applySettingsButton = gradioApp().querySelector("#tab_settings #settings_submit") || gradioApp().querySelector("#tab_settings > div > .gr-button-primary");
873 | applySettingsButton?.addEventListener("click", () => {
874 | // Wait 500ms to make sure the settings have been applied to the webui opts object
875 | setTimeout(async () => {
876 | await syncOptions();
877 | }, 500);
878 | });
879 | // Add change listener to our quicksettings to change our internal config without the apply button for them
880 | let quicksettings = gradioApp().querySelector('#quicksettings');
881 | let commonQueryPart = "[id^=setting_tac] > label >";
882 | quicksettings?.querySelectorAll(`${commonQueryPart} input, ${commonQueryPart} textarea, ${commonQueryPart} select`).forEach(e => {
883 | e.addEventListener("change", () => {
884 | setTimeout(async () => {
885 | await syncOptions();
886 | }, 500);
887 | });
888 | });
889 |
890 | // Add change listener to model dropdown to react to model changes
891 | let modelDropdown = gradioApp().querySelector("#setting_sd_model_checkpoint select");
892 | currentModelName = modelDropdown.value;
893 | modelDropdown?.addEventListener("change", () => {
894 | setTimeout(() => {
895 | currentModelName = modelDropdown.value;
896 | }, 100);
897 | });
898 | // Add mutation observer for the model hash text to also allow hash-based blacklist again
899 | let modelHashText = gradioApp().querySelector("#sd_checkpoint_hash");
900 | if (modelHashText) {
901 | currentModelHash = modelHashText.title
902 | let modelHashObserver = new MutationObserver((mutationList, observer) => {
903 | for (const mutation of mutationList) {
904 | if (mutation.type === "attributes" && mutation.attributeName === "title") {
905 | currentModelHash = mutation.target.title;
906 | }
907 | }
908 | });
909 | modelHashObserver.observe(modelHashText, { attributes: true });
910 | }
911 |
912 | // Not found, we're on a page without prompt textareas
913 | if (textAreas.every(v => v === null || v === undefined)) return;
914 | // Already added or unnecessary to add
915 | if (gradioApp().querySelector('.autocompleteResults.p')) {
916 | if (gradioApp().querySelector('.autocompleteResults.n') || !CFG.activeIn.negativePrompts) {
917 | return;
918 | }
919 | } else if (!CFG.activeIn.txt2img && !CFG.activeIn.img2img) {
920 | return;
921 | }
922 |
923 | textAreas.forEach(area => {
924 | // Return if autocomplete is disabled for the current area type in config
925 | let textAreaId = getTextAreaIdentifier(area);
926 | if ((!CFG.activeIn.img2img && textAreaId.includes("img2img"))
927 | || (!CFG.activeIn.txt2img && textAreaId.includes("txt2img"))
928 | || (!CFG.activeIn.negativePrompts && textAreaId.includes("n"))
929 | || (!CFG.activeIn.thirdParty && textAreaId.includes("thirdParty"))) {
930 | return;
931 | }
932 |
933 | // Only add listeners once
934 | if (!area.classList.contains('autocomplete')) {
935 | // Add our new element
936 | var resultsDiv = createResultsDiv(area);
937 | area.parentNode.insertBefore(resultsDiv, area.nextSibling);
938 | // Hide by default so it doesn't show up on page load
939 | hideResults(area);
940 |
941 | // Add autocomplete event listener
942 | area.addEventListener('input', debounce(() => autocomplete(area, area.value), CFG.delayTime));
943 | // Add focusout event listener
944 | area.addEventListener('focusout', debounce(() => hideResults(area), 400));
945 | // Add up and down arrow event listener
946 | area.addEventListener('keydown', (e) => navigateInList(area, e));
947 | area.addEventListener('focusout', (e) => inputWords(area));//aliu
948 | // CompositionEnd fires after the user has finished IME composing
949 | // We need to block hide here to prevent the enter key from insta-closing the results
950 | area.addEventListener('compositionend', () => {
951 | hideBlocked = true;
952 | setTimeout(() => { hideBlocked = false; }, 100);
953 | });
954 |
955 | // Add class so we know we've already added the listeners
956 | area.classList.add('autocomplete');
957 | }
958 | });
959 |
960 | // Add style to dom
961 | let acStyle = document.createElement('style');
962 | //let css = gradioApp().querySelector('.dark') ? autocompleteCSS_dark : autocompleteCSS_light;
963 | let mode = gradioApp().querySelector('.dark') ? 0 : 1;
964 | // Check if we are on webkit
965 | let browser = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 ? "firefox" : "other";
966 |
967 | let css = autocompleteCSS;
968 | // Replace vars with actual values (can't use actual css vars because of the way we inject the css)
969 | Object.keys(styleColors).forEach((key) => {
970 | css = css.replace(`var(${key})`, styleColors[key][mode]);
971 | })
972 | Object.keys(browserVars).forEach((key) => {
973 | css = css.replace(`var(${key})`, browserVars[key][browser]);
974 | })
975 |
976 | if (acStyle.styleSheet) {
977 | acStyle.styleSheet.cssText = css;
978 | } else {
979 | acStyle.appendChild(document.createTextNode(css));
980 | }
981 | gradioApp().appendChild(acStyle);
982 |
983 | // Callback
984 | await processQueue(QUEUE_AFTER_SETUP, null);
985 | }
986 | let loading = false;
987 | onUiUpdate(async () => {
988 | if (loading) return;
989 | if (Object.keys(opts).length === 0) return;
990 | if (CFG) return;
991 | loading = true;
992 | // Get our tag base path from the temp file
993 | tagBasePath = await readFile(`tmp/tagAutocompletePath.txt`);
994 | // Load config from webui opts
995 | await syncOptions();
996 | // Rest of setup
997 | setup();
998 | loading = false;
999 | });
1000 |
--------------------------------------------------------------------------------
/scripts/tag_autocomplete_helper.py:
--------------------------------------------------------------------------------
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 |
9 | # Webui root path
10 | FILE_DIR = Path().absolute()
11 |
12 | # The extension base path
13 | EXT_PATH = FILE_DIR.joinpath('extensions')
14 |
15 | # Tags base path
16 | TAGS_PATH = Path(scripts.basedir()).joinpath('tags')
17 |
18 | # The path to the folder containing the wildcards and embeddings
19 | WILDCARD_PATH = FILE_DIR.joinpath('scripts/wildcards')
20 | EMB_PATH = Path(shared.cmd_opts.embeddings_dir)
21 | HYP_PATH = Path(shared.cmd_opts.hypernetwork_dir)
22 |
23 | try:
24 | LORA_PATH = Path(shared.cmd_opts.lora_dir)
25 | except AttributeError:
26 | LORA_PATH = None
27 |
28 | def find_ext_wildcard_paths():
29 | """Returns the path to the extension wildcards folder"""
30 | found = list(EXT_PATH.glob('*/wildcards/'))
31 | return found
32 |
33 |
34 | # The path to the extension wildcards folder
35 | WILDCARD_EXT_PATHS = find_ext_wildcard_paths()
36 |
37 | # The path to the temporary files
38 | STATIC_TEMP_PATH = FILE_DIR.joinpath('tmp') # In the webui root, on windows it exists by default, on linux it doesn't
39 | TEMP_PATH = TAGS_PATH.joinpath('temp') # Extension specific temp files
40 |
41 |
42 | def get_wildcards():
43 | """Returns a list of all wildcards. Works on nested folders."""
44 | wildcard_files = list(WILDCARD_PATH.rglob("*.txt"))
45 | resolved = [w.relative_to(WILDCARD_PATH).as_posix(
46 | ) for w in wildcard_files if w.name != "put wildcards here.txt"]
47 | return resolved
48 |
49 |
50 | def get_ext_wildcards():
51 | """Returns a list of all extension wildcards. Works on nested folders."""
52 | wildcard_files = []
53 |
54 | for path in WILDCARD_EXT_PATHS:
55 | wildcard_files.append(path.relative_to(FILE_DIR).as_posix())
56 | wildcard_files.extend(p.relative_to(path).as_posix() for p in path.rglob("*.txt") if p.name != "put wildcards here.txt")
57 | wildcard_files.append("-----")
58 |
59 | return wildcard_files
60 |
61 |
62 | def get_ext_wildcard_tags():
63 | """Returns a list of all tags found in extension YAML files found under a Tags: key."""
64 | wildcard_tags = {} # { tag: count }
65 | yaml_files = []
66 | for path in WILDCARD_EXT_PATHS:
67 | yaml_files.extend(p for p in path.rglob("*.yml"))
68 | yaml_files.extend(p for p in path.rglob("*.yaml"))
69 | count = 0
70 | for path in yaml_files:
71 | try:
72 | with open(path, encoding="utf8") as file:
73 | data = yaml.safe_load(file)
74 | for item in data:
75 | if data[item] and 'Tags' in data[item]:
76 | wildcard_tags[count] = ','.join(data[item]['Tags'])
77 | count += 1
78 | else:
79 | print('Issue with tags found in ' + path.name + ' at item ' + item)
80 | except yaml.YAMLError as exc:
81 | print(exc)
82 | # Sort by count
83 | sorted_tags = sorted(wildcard_tags.items(), key=lambda item: item[1], reverse=True)
84 | output = []
85 | for tag, count in sorted_tags:
86 | output.append(f"{tag},{count}")
87 | return output
88 |
89 |
90 | def get_embeddings(sd_model):
91 | """Write a list of all embeddings with their version"""
92 |
93 | # Version constants
94 | V1_SHAPE = 768
95 | V2_SHAPE = 1024
96 | emb_v1 = []
97 | emb_v2 = []
98 | results = []
99 |
100 | try:
101 | # Get embedding dict from sd_hijack to separate v1/v2 embeddings
102 | emb_type_a = sd_hijack.model_hijack.embedding_db.word_embeddings
103 | emb_type_b = sd_hijack.model_hijack.embedding_db.skipped_embeddings
104 | # Get the shape of the first item in the dict
105 | emb_a_shape = -1
106 | emb_b_shape = -1
107 | if (len(emb_type_a) > 0):
108 | emb_a_shape = next(iter(emb_type_a.items()))[1].shape
109 | if (len(emb_type_b) > 0):
110 | emb_b_shape = next(iter(emb_type_b.items()))[1].shape
111 |
112 | # Add embeddings to the correct list
113 | if (emb_a_shape == V1_SHAPE):
114 | emb_v1 = list(emb_type_a.keys())
115 | elif (emb_a_shape == V2_SHAPE):
116 | emb_v2 = list(emb_type_a.keys())
117 |
118 | if (emb_b_shape == V1_SHAPE):
119 | emb_v1 = list(emb_type_b.keys())
120 | elif (emb_b_shape == V2_SHAPE):
121 | emb_v2 = list(emb_type_b.keys())
122 |
123 | # Get shape of current model
124 | #vec = sd_model.cond_stage_model.encode_embedding_init_text(",", 1)
125 | #model_shape = vec.shape[1]
126 | # Show relevant entries at the top
127 | #if (model_shape == V1_SHAPE):
128 | # results = [e + ",v1" for e in emb_v1] + [e + ",v2" for e in emb_v2]
129 | #elif (model_shape == V2_SHAPE):
130 | # results = [e + ",v2" for e in emb_v2] + [e + ",v1" for e in emb_v1]
131 | #else:
132 | # raise AttributeError # Fallback to old method
133 | results = sorted([e + ",v1" for e in emb_v1] + [e + ",v2" for e in emb_v2], key=lambda x: x.lower())
134 | except AttributeError:
135 | print("tag_autocomplete_helper: Old webui version or unrecognized model shape, using fallback for embedding completion.")
136 | # Get a list of all embeddings in the folder
137 | all_embeds = [str(e.relative_to(EMB_PATH)) for e in EMB_PATH.rglob("*") if e.suffix in {".bin", ".pt", ".png",'.webp', '.jxl', '.avif'}]
138 | # Remove files with a size of 0
139 | all_embeds = [e for e in all_embeds if EMB_PATH.joinpath(e).stat().st_size > 0]
140 | # Remove file extensions
141 | all_embeds = [e[:e.rfind('.')] for e in all_embeds]
142 | results = [e + "," for e in all_embeds]
143 |
144 | write_to_temp_file('emb.txt', results)
145 |
146 | def get_hypernetworks():
147 | """Write a list of all hypernetworks"""
148 |
149 | # Get a list of all hypernetworks in the folder
150 | all_hypernetworks = [str(h.name) for h in HYP_PATH.rglob("*") if h.suffix in {".pt"}]
151 | # Remove file extensions
152 | return sorted([h[:h.rfind('.')] for h in all_hypernetworks], key=lambda x: x.lower())
153 |
154 | def get_lora():
155 | """Write a list of all lora"""
156 |
157 | # Get a list of all lora in the folder
158 | all_lora = [str(l.name) for l in LORA_PATH.rglob("*") if l.suffix in {".safetensors", ".ckpt", ".pt"}]
159 | # Remove file extensions
160 | return sorted([l[:l.rfind('.')] for l in all_lora], key=lambda x: x.lower())
161 |
162 |
163 | def write_tag_base_path():
164 | """Writes the tag base path to a fixed location temporary file"""
165 | with open(STATIC_TEMP_PATH.joinpath('tagAutocompletePath.txt'), 'w', encoding="utf-8") as f:
166 | f.write(TAGS_PATH.relative_to(FILE_DIR).as_posix())
167 |
168 |
169 | def write_to_temp_file(name, data):
170 | """Writes the given data to a temporary file"""
171 | with open(TEMP_PATH.joinpath(name), 'w', encoding="utf-8") as f:
172 | f.write(('\n'.join(data)))
173 |
174 |
175 | csv_files = []
176 | csv_files_withnone = []
177 | def update_tag_files():
178 | """Returns a list of all potential tag files"""
179 | global csv_files, csv_files_withnone
180 | files = [str(t.relative_to(TAGS_PATH)) for t in TAGS_PATH.glob("*.csv")]
181 | csv_files = files
182 | csv_files_withnone = ["None"] + files
183 |
184 |
185 |
186 | # Write the tag base path to a fixed location temporary file
187 | # to enable the javascript side to find our files regardless of extension folder name
188 | if not STATIC_TEMP_PATH.exists():
189 | STATIC_TEMP_PATH.mkdir(exist_ok=True)
190 |
191 | write_tag_base_path()
192 | update_tag_files()
193 |
194 | # Check if the temp path exists and create it if not
195 | if not TEMP_PATH.exists():
196 | TEMP_PATH.mkdir(parents=True, exist_ok=True)
197 |
198 | # Set up files to ensure the script doesn't fail to load them
199 | # even if no wildcards or embeddings are found
200 | write_to_temp_file('wc.txt', [])
201 | write_to_temp_file('wce.txt', [])
202 | write_to_temp_file('wcet.txt', [])
203 | write_to_temp_file('hyp.txt', [])
204 | write_to_temp_file('lora.txt', [])
205 | # Only reload embeddings if the file doesn't exist, since they are already re-written on model load
206 | if not TEMP_PATH.joinpath("emb.txt").exists():
207 | write_to_temp_file('emb.txt', [])
208 |
209 | # Write wildcards to wc.txt if found
210 | if WILDCARD_PATH.exists():
211 | wildcards = [WILDCARD_PATH.relative_to(FILE_DIR).as_posix()] + get_wildcards()
212 | if wildcards:
213 | write_to_temp_file('wc.txt', wildcards)
214 |
215 | # Write extension wildcards to wce.txt if found
216 | if WILDCARD_EXT_PATHS is not None:
217 | wildcards_ext = get_ext_wildcards()
218 | if wildcards_ext:
219 | write_to_temp_file('wce.txt', wildcards_ext)
220 | # Write yaml extension wildcards to wcet.txt if found
221 | wildcards_yaml_ext = get_ext_wildcard_tags()
222 | if wildcards_yaml_ext:
223 | write_to_temp_file('wcet.txt', wildcards_yaml_ext)
224 |
225 | # Write embeddings to emb.txt if found
226 | if EMB_PATH.exists():
227 | # Get embeddings after the model loaded callback
228 | script_callbacks.on_model_loaded(get_embeddings)
229 |
230 | if HYP_PATH.exists():
231 | hypernets = get_hypernetworks()
232 | if hypernets:
233 | write_to_temp_file('hyp.txt', hypernets)
234 |
235 | if LORA_PATH is not None and LORA_PATH.exists():
236 | lora = get_lora()
237 | if lora:
238 | write_to_temp_file('lora.txt', lora)
239 |
240 | # Register autocomplete options
241 | def on_ui_settings():
242 | TAC_SECTION = ("tac", "Tag Autocomplete")
243 | # Main tag file
244 | shared.opts.add_option("tac_tagFile", shared.OptionInfo("danbooru-index.csv", "Tag filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
245 | # Active in settings
246 | shared.opts.add_option("tac_active", shared.OptionInfo(True, "Enable Tag Autocompletion", section=TAC_SECTION))
247 | shared.opts.add_option("tac_activeIn.txt2img", shared.OptionInfo(True, "Active in txt2img (Requires restart)", section=TAC_SECTION))
248 | shared.opts.add_option("tac_activeIn.img2img", shared.OptionInfo(True, "Active in img2img (Requires restart)", section=TAC_SECTION))
249 | shared.opts.add_option("tac_activeIn.negativePrompts", shared.OptionInfo(True, "Active in negative prompts (Requires restart)", section=TAC_SECTION))
250 | shared.opts.add_option("tac_activeIn.thirdParty", shared.OptionInfo(True, "Active in third party textboxes [Dataset Tag Editor] (Requires restart)", section=TAC_SECTION))
251 | 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))
252 | 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))
253 | # Results related settings
254 | shared.opts.add_option("tac_slidingPopup", shared.OptionInfo(True, "Move completion popup together with text cursor", section=TAC_SECTION))
255 | shared.opts.add_option("tac_maxResults", shared.OptionInfo(5, "Maximum results", section=TAC_SECTION))
256 | shared.opts.add_option("tac_showAllResults", shared.OptionInfo(False, "Show all results", section=TAC_SECTION))
257 | shared.opts.add_option("tac_resultStepLength", shared.OptionInfo(100, "How many results to load at once", section=TAC_SECTION))
258 | shared.opts.add_option("tac_delayTime", shared.OptionInfo(100, "Time in ms to wait before triggering completion again (Requires restart)", section=TAC_SECTION))
259 | shared.opts.add_option("tac_useWildcards", shared.OptionInfo(True, "Search for wildcards", section=TAC_SECTION))
260 | shared.opts.add_option("tac_useEmbeddings", shared.OptionInfo(True, "Search for embeddings", section=TAC_SECTION))
261 | shared.opts.add_option("tac_useHypernetworks", shared.OptionInfo(True, "Search for hypernetworks", section=TAC_SECTION))
262 | shared.opts.add_option("tac_useLoras", shared.OptionInfo(True, "Search for Loras", section=TAC_SECTION))
263 | 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))
264 | # Insertion related settings
265 | shared.opts.add_option("tac_replaceUnderscores", shared.OptionInfo(True, "Replace underscores with spaces on insertion", section=TAC_SECTION))
266 | shared.opts.add_option("tac_escapeParentheses", shared.OptionInfo(True, "Escape parentheses on insertion", section=TAC_SECTION))
267 | shared.opts.add_option("tac_appendComma", shared.OptionInfo(True, "Append comma on tag autocompletion", section=TAC_SECTION))
268 | # Alias settings
269 | shared.opts.add_option("tac_alias.searchByAlias", shared.OptionInfo(True, "Search by alias", section=TAC_SECTION))
270 | shared.opts.add_option("tac_alias.onlyShowAlias", shared.OptionInfo(False, "Only show alias", section=TAC_SECTION))
271 | # Translation settings
272 | shared.opts.add_option("tac_translation.translationFile", shared.OptionInfo("None", "Translation filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
273 | 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))
274 | shared.opts.add_option("tac_translation.searchByTranslation", shared.OptionInfo(True, "Search by translation", section=TAC_SECTION))
275 | # Extra file settings
276 | shared.opts.add_option("tac_extra.extraFile", shared.OptionInfo("extra-quality-tags.csv", "Extra filename (for small sets of custom tags)", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
277 | 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))
278 |
279 | script_callbacks.on_ui_settings(on_ui_settings)
280 |
--------------------------------------------------------------------------------
/tags/colors.json:
--------------------------------------------------------------------------------
1 | {
2 | "danbooru": {
3 | "-1": ["red", "maroon"],
4 | "0": ["lightblue", "dodgerblue"],
5 | "1": ["indianred", "firebrick"],
6 | "3": ["violet", "darkorchid"],
7 | "4": ["lightgreen", "darkgreen"],
8 | "5": ["orange", "darkorange"]
9 | },
10 | "e621": {
11 | "-1": ["red", "maroon"],
12 | "0": ["lightblue", "dodgerblue"],
13 | "1": ["gold", "goldenrod"],
14 | "3": ["violet", "darkorchid"],
15 | "4": ["lightgreen", "darkgreen"],
16 | "5": ["tomato", "darksalmon"],
17 | "6": ["red", "maroon"],
18 | "7": ["whitesmoke", "black"],
19 | "8": ["seagreen", "darkseagreen"]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tags/danbooru-index.csv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChinaGPT/a1111-sd-webui-tagcomplete-10w/b963eedf2582ce6404b5a9c368043eb26fa155b0/tags/danbooru-index.csv
--------------------------------------------------------------------------------
/tags/extra-quality-tags.csv:
--------------------------------------------------------------------------------
1 | masterpiece,5,Quality tag,
2 | best_quality,5,Quality tag,
3 | high_quality,5,Quality tag,
4 | normal_quality,5,Quality tag,
5 | low_quality,5,Quality tag,
6 | worst_quality,5,Quality tag,
7 |
--------------------------------------------------------------------------------
/tags/keymap.json:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------