├── .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 | ![图新增中文对照翻译](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/6.png) 10 | 11 | 12 | 13 | 基于 https://github.com/DominikDoom/a1111-sd-webui-tagcomplete 插件 14 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/DominikDoom/a1111-sd-webui-tagcomplete)](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases) 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 | ![安装方式](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/0.png) 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 | ![图1](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/3.png) 48 | 49 | ![图2](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/4.png) 50 | 51 | ![图3](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/5.png) 52 | 53 | 54 | ![图4](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/1.png) 55 | 56 | ![图4](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/2.png) 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 | ![图新增中文对照翻译](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/6.png) 10 | 11 | 12 | 13 | 基于 https://github.com/DominikDoom/a1111-sd-webui-tagcomplete 插件 14 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/DominikDoom/a1111-sd-webui-tagcomplete)](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases) 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 | ![安装方式](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/0.png) 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 | ![图1](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/3.png) 48 | 49 | ![图2](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/4.png) 50 | 51 | ![图3](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/5.png) 52 | 53 | 54 | ![图4](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/1.png) 55 | 56 | ![图4](https://raw.githubusercontent.com/inlhx/a1111-sd-webui-tagcomplete-10w/main/img/2.png) 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 | } --------------------------------------------------------------------------------