mirror of
https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git
synced 2026-01-27 03:29:55 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1357cddc1 | ||
|
|
11d94e11f9 | ||
|
|
5fbc18ed1d | ||
|
|
223abf5420 | ||
|
|
4331bdccda | ||
|
|
c3f53e1a60 | ||
|
|
951e82f055 | ||
|
|
cd52a0577c | ||
|
|
873a15b5f6 | ||
|
|
811d4622e9 | ||
|
|
296b9456cc | ||
|
|
32c7749a5f | ||
|
|
afe3f23afa | ||
|
|
2571a4f70a | ||
|
|
f8e15307c6 | ||
|
|
95ebde9fce | ||
|
|
c08746a2c0 | ||
|
|
39abf1fe3a | ||
|
|
b047095f80 | ||
|
|
caf65bfda0 | ||
|
|
e7fa5aca18 | ||
|
|
f026e7631c | ||
|
|
15538336c9 | ||
|
|
12340c37cb | ||
|
|
d1fff7bfa7 | ||
|
|
647d3f7ec3 | ||
|
|
90664d47bf | ||
|
|
bc56c3ca72 | ||
|
|
823958507b | ||
|
|
712c4a5862 | ||
|
|
b5817b8d4a | ||
|
|
6269c40580 | ||
|
|
c1fb4619a4 | ||
|
|
a7c5c38c26 | ||
|
|
c14260c1fe |
102
README.md
102
README.md
@@ -15,11 +15,11 @@ You can install it using the inbuilt available extensions list, clone the files
|
||||
|
||||
## Common Problems & Known Issues:
|
||||
- Depending on your browser settings, sometimes an old version of the script can get cached. Try `CTRL+F5` to force-reload the site without cache if e.g. a new feature doesn't appear for you after an update.
|
||||
- If `replaceUnderscores` is active, the script will currently only partially replace edited tags containing multiple words in brackets.
|
||||
For example, editing `atago (azur lane)`, it would be replaced with e.g. `taihou (azur lane), lane)`, since the script currently doesn't see the second part of the bracket as the same tag. So in those cases you should delete the old tag beforehand.
|
||||
|
||||
## Screenshots
|
||||
Demo video (with keyboard navigation):
|
||||
## Screenshots & Demo videos
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
Basic usage (with keyboard navigation):
|
||||
|
||||
https://user-images.githubusercontent.com/34448969/200128020-10d9a8b2-cea6-4e3f-bcd2-8c40c8c73233.mp4
|
||||
|
||||
@@ -31,32 +31,61 @@ Dark and Light mode supported, including tag colors:
|
||||
|
||||

|
||||

|
||||
</details>
|
||||
|
||||
## Installation
|
||||
### As an extension (recommended)
|
||||
Either clone the repo into your extensions folder:
|
||||
### Using the built-in extension list
|
||||
1. Open the Extensions tab
|
||||
2. Open the Available sub-tab
|
||||
3. Click "Load from:"
|
||||
4. Find "Booru tag autocompletion" in the list
|
||||
- The extension was one of the first available, so selecting "oldest first" will show it high up in the list.
|
||||
5. Click "Install" on the right side
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
### Manual clone
|
||||
```bash
|
||||
git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extensions/tag-autocomplete
|
||||
```
|
||||
(The second argument specifies the name of the folder, you can choose whatever you like).
|
||||
|
||||
Or create a folder there manually and place the `javascript`, `scripts` and `tags` folders in it.
|
||||
## Additional completion support
|
||||
### Wildcards
|
||||
Autocompletion also works with wildcard files used by https://github.com/AUTOMATIC1111/stable-diffusion-webui-wildcards or other similar scripts/extensions.
|
||||
Completion is triggered by typing `__` (double underscore). It will first show a list of your wildcard files, and upon choosing one, the replacement options inside that file.
|
||||
This enables you to either insert categories to be replaced by the script, or directly choose one and use wildcards as a sort of categorized custom tag system.
|
||||
|
||||
### In the root folder (legacy)
|
||||
This installation method is for old webui versions pre-extension system, it will not work on current versions!
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
In both configurations, the `tags` folder contains `colors.json` and the tag data the script uses for autocompletion. By default, Danbooru and e621 tags are included.
|
||||
After scanning for embeddings and wildcards, the script will also create a `temp` directory here which lists the found files so they can be accessed in the browser side of the script. You can delete the temp folder without consequences as it will be recreated on the next startup.
|
||||
Wildcards are searched for in every extension folder, as well as the `scripts/wildcards` folder to support legacy versions. This means that you can combine wildcards from multiple extensions. Nested folders are also supported if you have grouped your wildcards in that way.
|
||||
|
||||
### Important:
|
||||
The script needs **all three folders** to work properly.
|
||||
### Embeddings, Lora & Hypernets
|
||||
Completion for these three types is triggered by typing `<`. By default it will show all three mixed together, but further filtering can be done in the following way:
|
||||
- `<e:` will only show embeddings
|
||||
- `<l:` or `<lora:` will only show Lora
|
||||
- `<h:` or `<hypernet:` will only show Hypernetworks
|
||||
|
||||
## Wildcard & Embedding support
|
||||
Autocompletion also works with wildcard files used by [this script](https://github.com/jtkelm2/stable-diffusion-webui-1/blob/master/scripts/wildcards.py) of the same name or other similar scripts/extensions. This enables you to either insert categories to be replaced by the script, or even replace them with the actual wildcard file content in the same step. Wildcards are searched for in every extension folder as well as the `scripts/wildcards` folder to support legacy versions. This means that you can combine wildcards from multiple extensions. Nested folders are also supported if you have grouped your wildcards in that way.
|
||||
#### Embedding type filtering
|
||||
Embeddings trained for Stable Diffusion 1.x or 2.x models respectively are incompatible with the other type. To make it easier to find valid embeds, they are categorized by "v1 Embedding" and "v2 Embedding", including a slight color difference. You can also filter your search to include only v1 or v2 embeddings by typing `<v1/2` or `<e:v1/2` followed by the actual search term.
|
||||
|
||||
It also scans the embeddings folder and displays completion hints for the names of all .pt, .bin and .png files inside if you start typing `<`. Note that some normal tags also use < in Kaomoji (like ">_<" for example), so the results will contain both.
|
||||
For example:
|
||||
|
||||

|
||||
|
||||
### Umi AI tags
|
||||
https://github.com/Klokinator/Umi-AI is a feature-rich wildcard extension similar to Unprompted or Dynamic Wildcards.
|
||||
In recent releases, it uses YAML-based wildcard tags to enable a complex chaining system,for example `<[preset][--female][sfw][species]>` will choose the preset category, exclude female related tags, further narrow it down with the following categories, and then choose one random fill-in matching all these criteria at runtime. Completion is triggered by `<[` and then each following new unclosed bracket, e.g. `<[xyz][`, until closed by `>`.
|
||||
|
||||
Tag Autocomplete can recommend these options in a smart way, meaning while you continue to add category tags, it will only show results still matching what comes before.
|
||||
It also shows how many fill-in tags are available to choose from for that combo in place of the tag post count, enabling a quick overview and filtering of the large initial set.
|
||||
|
||||
Most of the credit goes to [@ctwrs](https://github.com/ctwrs) here, they contributed a lot as one of the Umi developers.
|
||||
|
||||
## Settings
|
||||
|
||||
@@ -79,11 +108,13 @@ The extension has a large amount of configuration & customizability built in:
|
||||
| useEmbeddings | Used to toggle the embedding completion functionality. |
|
||||
| alias | Options for aliases. More info in the section below. |
|
||||
| translation | Options for translations. More info in the section below. |
|
||||
| extras | Options for additional tag files / aliases / translations. More info in the section below. |
|
||||
|
||||
### colors.json
|
||||
Additionally, tag type colors can be specified using the separate `colors.json` file in the extension's `tags` folder.
|
||||
You can also add new ones here for custom tag files (same name as filename, without the .csv). The first value is for dark, the second for light mode. Color names and hex codes should both work.
|
||||
| extras | Options for additional tag files / aliases / translations. More info below. |
|
||||
| keymap | Customizable hotkeys. |
|
||||
| colors | Customizable tag colors. More info below. |
|
||||
### Colors
|
||||
Tag type colors can be specified by changing the JSON code in the tag autocomplete settings.
|
||||
The format is standard JSON, with the object names corresponding to the tag filenames (without the .csv) they should be used for.
|
||||
The first value in the square brackets is for dark, the second for light mode. Color names and hex codes should both work.
|
||||
```json
|
||||
{
|
||||
"danbooru": {
|
||||
@@ -107,6 +138,7 @@ You can also add new ones here for custom tag files (same name as filename, with
|
||||
}
|
||||
}
|
||||
```
|
||||
This can also be used to add new color sets for custom tag files.
|
||||
The numbers are specifying the tag type, which is dependent on the tag source. For an example, see [CSV tag data](#csv-tag-data).
|
||||
|
||||
### Aliases, Translations & Extra tags
|
||||
@@ -117,34 +149,24 @@ Like on Booru sites, tags can have one or multiple aliases which redirect to the
|
||||
|
||||
#### Translations
|
||||
An additional file can be added in the translation section, which will be used to translate both tags and aliases and also enables searching by translation.
|
||||
This file needs to be a CSV in the format `<English tag/alias>,<Translation>`, but for backwards compatibility with older extra files that used a three column format, you can turn on `oldFormat` to use that instead.
|
||||
This file needs to be a CSV in the format `<English tag/alias>,<Translation>`, but for backwards compatibility with older files that used a three column format, you can turn on `oldFormat` to use that instead.
|
||||
|
||||
Example with chinese translation:
|
||||
|
||||

|
||||

|
||||
|
||||
**Important**
|
||||
As of a recent update, translations added in the old Extra file way will only work as an alias and not be visible anymore if typing the English tag for that translation.
|
||||
|
||||
#### Extra file
|
||||
Aliases can be added in multiple ways, which is where the "Extra" file comes into play.
|
||||
1. As an extra file containing tag, category, optional count and the new alias. Will be matched to the English tags in the main file based on the name & type, so might be slow for large files.
|
||||
2. As an extra file with `onlyAliasExtraFile` true. With this configuration, the extra file has to include *only* the alias itself. That means it is purely index based, assigning the aliases to the main tags is really fast but also needs the lines to match (including empty lines). If the order or amount in the main file changes, the translations will potentially not match anymore. Not recommended.
|
||||
An extra file can be used to add new / custom tags not included in the main set.
|
||||
The format is identical to the normal tag format shown in [CSV tag data](#csv-tag-data) below, with one exception:
|
||||
Since custom tags likely have no count, column three (or two if counting from zero) is instead used for the gray meta text displayed next to the tag.
|
||||
If left empty, it will instead show "Custom tag".
|
||||
|
||||
So your CSV values would look like this for each method:
|
||||
| | 1 | 2 |
|
||||
|------------|--------------------------|--------------------------|
|
||||
| Main file | `tag,type,count,(alias)` | `tag,type,count,(alias)` |
|
||||
| Extra file | `tag,type,(count),alias` | `alias` |
|
||||
An example with the included (very basic) extra-quality-tags.csv file:
|
||||
|
||||
Count in the extra file is optional, since there isn't always a post count for custom tag sets.
|
||||

|
||||
|
||||
The extra files can also be used to just add new / custom tags not included in the main set, provided `onlyAliasExtraFile` is false.
|
||||
If an extra tag doesn't match any existing tag, it will be added to the list as a new tag instead. For this, it will need to include the post count and alias columns even if they don't contain anything, so it could be in the form of `tag,type,,`.
|
||||
|
||||
##### WARNING
|
||||
Do not use e621.csv or danbooru.csv as an extra file. Alias comparison has exponential runtime, so for the combination of danbooru+e621, it will need to do 10,000,000,000 (yes, ten billion) lookups and usually take multiple minutes to load.
|
||||
Whether the custom tags should be added before or after the normal tags can be chosen in the settings.
|
||||
|
||||
## CSV tag data
|
||||
The script expects a CSV file with tags saved in the following way:
|
||||
|
||||
33
README_ZH.md
33
README_ZH.md
@@ -81,9 +81,10 @@ git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extens
|
||||
| translation | 用于翻译标签的选项。更多信息在下面的部分。 |
|
||||
| extras | 附加标签文件/翻译的选项。更多信息在下面的部分。|
|
||||
|
||||
### colors.json (标签颜色)
|
||||
此外,标签类型的颜色可以使用扩展的`tags`文件夹中单独的`colors.json`文件来指定。
|
||||
你也可以在这里为自定义标签文件添加新的(与文件名相同,不带 .csv)。第一个值是暗模式,第二个值是亮模式。颜色名称和十六进制代码都被支持。
|
||||
### 标签颜色
|
||||
标签类型的颜色可以通过改变标签自动完成设置中的JSON代码来指定。格式是标准的JSON,对象名称对应于它们应该使用的标签文件名(没有.csv)
|
||||
|
||||
方括号中的第一个值是指深色,第二个是指浅色模式。颜色名称和十六进制代码都应该有效。
|
||||
```json
|
||||
{
|
||||
"danbooru": {
|
||||
@@ -107,7 +108,7 @@ git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extens
|
||||
}
|
||||
}
|
||||
```
|
||||
数字是指定标签的类型,这取决于标签的来源。例如,见[CSV tag data](#csv-tag-data)。
|
||||
这也可以用来为自定义标签文件添加新的颜色集。数字是指定标签的类型,这取决于标签来源。关于例子,见[CSV tag data](#csv-tag-data)。
|
||||
|
||||
### 别名,翻译&新增Tag
|
||||
#### 别名
|
||||
@@ -124,24 +125,20 @@ git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extens
|
||||

|
||||

|
||||
|
||||
**重要的是**
|
||||
从最近的更新来看,用旧的Extra文件方式添加的翻译只能作为一个别名使用,如果输入该翻译的英文标签,将不再可见。
|
||||
#### Extra文件
|
||||
额外文件可以用来添加未包含在主集中的新的/自定义标签。
|
||||
其格式与下面 [CSV tag data](#csv-tag-data) 中的正常标签格式相同,但有一个例外。
|
||||
由于自定义标签没有帖子计数,第三列(如果从零开始计算,则为第二列)用于显示标签旁边的灰色元文本。
|
||||
如果留空,它将显示 "Custom tag"。
|
||||
|
||||
可以通过多种方式添加别名,这就是额外文件发挥作用的地方。
|
||||
1. 作为仅包含已翻译标签行的额外文件(因此仍包括英文标签名称和标签类型)。将根据名称和类型与主文件中的英文标签匹配,因此对于大型翻译文件可能会很慢。
|
||||
2. 作为 `onlyAliasExtraFile` 为 true 的额外文件。使用此配置,额外文件必须包含*仅*翻译本身。这意味着它完全基于索引,将翻译分配给主要标签非常快,但也需要匹配行(包括空行)。如果主文件中的顺序或数量发生变化,则翻译可能不再匹配。
|
||||
以默认的(非常基本的)extra-quality-tags.csv为例:
|
||||
|
||||
因此,对于每种方法,您的 CSV 值将如下所示:
|
||||
| | 1 | 2 |
|
||||
|------------|--------------------------|--------------------------|
|
||||
| Main file | `tag,type,count,(alias)` | `tag,type,count,(alias)` |
|
||||
| Extra file | `tag,type,(count),alias` | `alias` |
|
||||

|
||||
|
||||
额外文件中的计数是可选的,因为自定义标签集并不总是有帖子计数。
|
||||
如果额外的标签与任何现有标签都不匹配,它将作为新标签添加到列表中。
|
||||
你可以在设置中选择自定义标签是否应该加在常规标签之前或之后。
|
||||
|
||||
### CSV tag data
|
||||
本脚本的Tag文件格式如下,你可以安装这个格式制作自己的Tag文件:
|
||||
本脚本的Tag文件格式如下,你可以安装这个格式制作自己的Tag文件:
|
||||
```csv
|
||||
1girl,0,4114588,"1girls,sole_female"
|
||||
solo,0,3426446,"female_solo,solo_female"
|
||||
@@ -151,7 +148,7 @@ commentary_request,5,2610959,
|
||||
```
|
||||
值得注意的是,不希望在第一行有列名,而且count和aliases在技术上都是可选的。
|
||||
尽管count总是包含在默认数据中。多个别名也需要用逗号分隔,但要用字符串引号包裹,以免破坏CSV解析。
|
||||
编号系统遵循 Danbooru 的 [tag API docs](https://danbooru.donmai.us/wiki_pages/api%3Atags):
|
||||
编号系统遵循 Danbooru 的 [tag API docs](https://danbooru.donmai.us/wiki_pages/api%3Atags):
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
|0 | General |
|
||||
|
||||
@@ -13,6 +13,7 @@ var yamlWildcards = [];
|
||||
var embeddings = [];
|
||||
var hypernetworks = [];
|
||||
var loras = [];
|
||||
var lycos = [];
|
||||
|
||||
// Selected model info for black/whitelisting
|
||||
var currentModelHash = "";
|
||||
|
||||
145
javascript/_caretPosition.js
Normal file
145
javascript/_caretPosition.js
Normal file
@@ -0,0 +1,145 @@
|
||||
// From https://github.com/component/textarea-caret-position
|
||||
|
||||
// We'll copy the properties below into the mirror div.
|
||||
// Note that some browsers, such as Firefox, do not concatenate properties
|
||||
// into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
|
||||
// so we have to list every single property explicitly.
|
||||
var properties = [
|
||||
'direction', // RTL support
|
||||
'boxSizing',
|
||||
'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
|
||||
'height',
|
||||
'overflowX',
|
||||
'overflowY', // copy the scrollbar for IE
|
||||
|
||||
'borderTopWidth',
|
||||
'borderRightWidth',
|
||||
'borderBottomWidth',
|
||||
'borderLeftWidth',
|
||||
'borderStyle',
|
||||
|
||||
'paddingTop',
|
||||
'paddingRight',
|
||||
'paddingBottom',
|
||||
'paddingLeft',
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
|
||||
'fontStyle',
|
||||
'fontVariant',
|
||||
'fontWeight',
|
||||
'fontStretch',
|
||||
'fontSize',
|
||||
'fontSizeAdjust',
|
||||
'lineHeight',
|
||||
'fontFamily',
|
||||
|
||||
'textAlign',
|
||||
'textTransform',
|
||||
'textIndent',
|
||||
'textDecoration', // might not make a difference, but better be safe
|
||||
|
||||
'letterSpacing',
|
||||
'wordSpacing',
|
||||
|
||||
'tabSize',
|
||||
'MozTabSize'
|
||||
|
||||
];
|
||||
|
||||
var isBrowser = (typeof window !== 'undefined');
|
||||
var isFirefox = (isBrowser && window.mozInnerScreenX != null);
|
||||
|
||||
function getCaretCoordinates(element, position, options) {
|
||||
if (!isBrowser) {
|
||||
throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
|
||||
}
|
||||
|
||||
var debug = options && options.debug || false;
|
||||
if (debug) {
|
||||
var el = document.querySelector('#input-textarea-caret-position-mirror-div');
|
||||
if (el) el.parentNode.removeChild(el);
|
||||
}
|
||||
|
||||
// The mirror div will replicate the textarea's style
|
||||
var div = document.createElement('div');
|
||||
div.id = 'input-textarea-caret-position-mirror-div';
|
||||
document.body.appendChild(div);
|
||||
|
||||
var style = div.style;
|
||||
var computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
|
||||
var isInput = element.nodeName === 'INPUT';
|
||||
|
||||
// Default textarea styles
|
||||
style.whiteSpace = 'pre-wrap';
|
||||
if (!isInput)
|
||||
style.wordWrap = 'break-word'; // only for textarea-s
|
||||
|
||||
// Position off-screen
|
||||
style.position = 'absolute'; // required to return coordinates properly
|
||||
if (!debug)
|
||||
style.visibility = 'hidden'; // not 'display: none' because we want rendering
|
||||
|
||||
// Transfer the element's properties to the div
|
||||
properties.forEach(function (prop) {
|
||||
if (isInput && prop === 'lineHeight') {
|
||||
// Special case for <input>s because text is rendered centered and line height may be != height
|
||||
if (computed.boxSizing === "border-box") {
|
||||
var height = parseInt(computed.height);
|
||||
var outerHeight =
|
||||
parseInt(computed.paddingTop) +
|
||||
parseInt(computed.paddingBottom) +
|
||||
parseInt(computed.borderTopWidth) +
|
||||
parseInt(computed.borderBottomWidth);
|
||||
var targetHeight = outerHeight + parseInt(computed.lineHeight);
|
||||
if (height > targetHeight) {
|
||||
style.lineHeight = height - outerHeight + "px";
|
||||
} else if (height === targetHeight) {
|
||||
style.lineHeight = computed.lineHeight;
|
||||
} else {
|
||||
style.lineHeight = 0;
|
||||
}
|
||||
} else {
|
||||
style.lineHeight = computed.height;
|
||||
}
|
||||
} else {
|
||||
style[prop] = computed[prop];
|
||||
}
|
||||
});
|
||||
|
||||
if (isFirefox) {
|
||||
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
|
||||
if (element.scrollHeight > parseInt(computed.height))
|
||||
style.overflowY = 'scroll';
|
||||
} else {
|
||||
style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
|
||||
}
|
||||
|
||||
div.textContent = element.value.substring(0, position);
|
||||
// The second special handling for input type="text" vs textarea:
|
||||
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
|
||||
if (isInput)
|
||||
div.textContent = div.textContent.replace(/\s/g, '\u00a0');
|
||||
|
||||
var span = document.createElement('span');
|
||||
// Wrapping must be replicated *exactly*, including when a long word gets
|
||||
// onto the next line, with whitespace at the end of the line before (#7).
|
||||
// The *only* reliable way to do that is to copy the *entire* rest of the
|
||||
// textarea's content into the <span> created at the caret position.
|
||||
// For inputs, just '.' would be enough, but no need to bother.
|
||||
span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
|
||||
div.appendChild(span);
|
||||
|
||||
var coordinates = {
|
||||
top: span.offsetTop + parseInt(computed['borderTopWidth']),
|
||||
left: span.offsetLeft + parseInt(computed['borderLeftWidth']),
|
||||
height: parseInt(computed['lineHeight'])
|
||||
};
|
||||
|
||||
if (debug) {
|
||||
span.style.backgroundColor = '#aaa';
|
||||
} else {
|
||||
document.body.removeChild(div);
|
||||
}
|
||||
|
||||
return coordinates;
|
||||
}
|
||||
@@ -9,7 +9,8 @@ const ResultType = Object.freeze({
|
||||
"wildcardFile": 5,
|
||||
"yamlWildcard": 6,
|
||||
"hypernetwork": 7,
|
||||
"lora": 8
|
||||
"lora": 8,
|
||||
"lyco": 9
|
||||
});
|
||||
|
||||
// Class to hold result data and annotations to make it clearer to use
|
||||
|
||||
@@ -12,10 +12,13 @@ class EmbeddingParser extends BaseTagParser {
|
||||
versionString = searchTerm.slice(0, 2);
|
||||
searchTerm = searchTerm.slice(2);
|
||||
}
|
||||
|
||||
let filterCondition = x => x[0].toLowerCase().includes(searchTerm) || x[0].toLowerCase().replaceAll(" ", "_").includes(searchTerm);
|
||||
|
||||
if (versionString)
|
||||
tempResults = embeddings.filter(x => x[0].toLowerCase().includes(searchTerm) && x[1] && x[1] === versionString); // Filter by tagword
|
||||
tempResults = embeddings.filter(x => filterCondition(x) && x[1] && x[1] === versionString); // Filter by tagword
|
||||
else
|
||||
tempResults = embeddings.filter(x => x[0].toLowerCase().includes(searchTerm)); // Filter by tagword
|
||||
tempResults = embeddings.filter(x => filterCondition(x)); // Filter by tagword
|
||||
} else {
|
||||
tempResults = embeddings;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ class HypernetParser extends BaseTagParser {
|
||||
parse() {
|
||||
// Show hypernetworks
|
||||
let tempResults = [];
|
||||
if (tagword !== "<" && tagword !== "<h:") {
|
||||
let searchTerm = tagword.replace("<h:", "").replace("<", "");
|
||||
tempResults = hypernetworks.filter(x => x.toLowerCase().includes(searchTerm)); // Filter by tagword
|
||||
if (tagword !== "<" && tagword !== "<h:" && tagword !== "<hypernet:") {
|
||||
let searchTerm = tagword.replace("<hypernet:", "").replace("<h:", "").replace("<", "");
|
||||
let filterCondition = x => x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm);
|
||||
tempResults = hypernetworks.filter(x => filterCondition(x)); // Filter by tagword
|
||||
} else {
|
||||
tempResults = hypernetworks;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ class LoraParser extends BaseTagParser {
|
||||
parse() {
|
||||
// Show lora
|
||||
let tempResults = [];
|
||||
if (tagword !== "<" && tagword !== "<l:") {
|
||||
let searchTerm = tagword.replace("<l:", "").replace("<", "");
|
||||
tempResults = loras.filter(x => x.toLowerCase().includes(searchTerm)); // Filter by tagword
|
||||
if (tagword !== "<" && tagword !== "<l:" && tagword !== "<lora:") {
|
||||
let searchTerm = tagword.replace("<lora:", "").replace("<l:", "").replace("<", "");
|
||||
let filterCondition = x => x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm);
|
||||
tempResults = loras.filter(x => filterCondition(x)); // Filter by tagword
|
||||
} else {
|
||||
tempResults = loras;
|
||||
}
|
||||
|
||||
51
javascript/ext_lycos.js
Normal file
51
javascript/ext_lycos.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const LYCO_REGEX = /<(?!e:|h:)[^,> ]*>?/g;
|
||||
const LYCO_TRIGGER = () => CFG.useLycos && tagword.match(LYCO_REGEX);
|
||||
|
||||
class LycoParser extends BaseTagParser {
|
||||
parse() {
|
||||
// Show lyco
|
||||
let tempResults = [];
|
||||
if (tagword !== "<" && tagword !== "<l:" && tagword !== "<lyco:") {
|
||||
let searchTerm = tagword.replace("<lyco:", "").replace("<l:", "").replace("<", "");
|
||||
let filterCondition = x => x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm);
|
||||
tempResults = lycos.filter(x => filterCondition(x)); // Filter by tagword
|
||||
} else {
|
||||
tempResults = lycos;
|
||||
}
|
||||
|
||||
// Add final results
|
||||
let finalResults = [];
|
||||
tempResults.forEach(t => {
|
||||
let result = new AutocompleteResult(t.trim(), ResultType.lyco)
|
||||
result.meta = "Lyco";
|
||||
finalResults.push(result);
|
||||
});
|
||||
|
||||
return finalResults;
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (lycos.length === 0) {
|
||||
try {
|
||||
lycos = (await readFile(`${tagBasePath}/temp/lyco.txt`)).split("\n")
|
||||
.filter(x => x.trim().length > 0) // Remove empty lines
|
||||
.map(x => x.trim()); // Remove carriage returns and padding if it exists
|
||||
} catch (e) {
|
||||
console.error("Error loading lyco.txt: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
if (tagType === ResultType.lyco) {
|
||||
return `<lyco:${text}:${CFG.extraNetworksDefaultMultiplier}>`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
PARSERS.push(new LycoParser(LYCO_TRIGGER));
|
||||
|
||||
// Add our utility functions to their respective queues
|
||||
QUEUE_FILE_LOAD.push(load);
|
||||
QUEUE_SANITIZE.push(sanitize);
|
||||
@@ -1,4 +1,4 @@
|
||||
const styleColors = {
|
||||
const styleColors = {
|
||||
"--results-bg": ["#0b0f19", "#ffffff"],
|
||||
"--results-border-color": ["#4b5563", "#e5e7eb"],
|
||||
"--results-border-width": ["1px", "1.5px"],
|
||||
@@ -25,11 +25,6 @@ const autocompleteCSS = `
|
||||
#quicksettings [id^=setting_tac] > label > span {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
[id^=refresh_tac] {
|
||||
max-width: 2.5em;
|
||||
min-width: 2.5em;
|
||||
height: 2.4em;
|
||||
}
|
||||
.autocompleteResults {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
@@ -137,6 +132,7 @@ async function syncOptions() {
|
||||
modelListMode: opts["tac_activeIn.modelListMode"]
|
||||
},
|
||||
// Results related settings
|
||||
slidingPopup: opts["tac_slidingPopup"],
|
||||
maxResults: opts["tac_maxResults"],
|
||||
showAllResults: opts["tac_showAllResults"],
|
||||
resultStepLength: opts["tac_resultStepLength"],
|
||||
@@ -145,6 +141,7 @@ async function syncOptions() {
|
||||
useEmbeddings: opts["tac_useEmbeddings"],
|
||||
useHypernetworks: opts["tac_useHypernetworks"],
|
||||
useLoras: opts["tac_useLoras"],
|
||||
useLycos: opts["tac_useLycos"],
|
||||
showWikiLinks: opts["tac_showWikiLinks"],
|
||||
// Insertion related settings
|
||||
replaceUnderscores: opts["tac_replaceUnderscores"],
|
||||
@@ -167,11 +164,11 @@ async function syncOptions() {
|
||||
addMode: opts["tac_extra.addMode"]
|
||||
},
|
||||
// Settings not from tac but still used by the script
|
||||
extraNetworksDefaultMultiplier: opts["extra_networks_default_multiplier"]
|
||||
}
|
||||
|
||||
if (CFG && CFG.colors) {
|
||||
newCFG["colors"] = CFG.colors;
|
||||
extraNetworksDefaultMultiplier: opts["extra_networks_default_multiplier"],
|
||||
extraNetworksSeparator: opts["extra_networks_add_text_separator"],
|
||||
// Custom mapping settings
|
||||
keymap: JSON.parse(opts["tac_keymap"]),
|
||||
colorMap: JSON.parse(opts["tac_colormap"])
|
||||
}
|
||||
if (newCFG.alias.onlyShowAlias) {
|
||||
newCFG.alias.searchByAlias = true; // if only show translation, enable search by translation is necessary
|
||||
@@ -205,14 +202,15 @@ async function syncOptions() {
|
||||
// Create the result list div and necessary styling
|
||||
function createResultsDiv(textArea) {
|
||||
let resultsDiv = document.createElement("div");
|
||||
let resultsList = document.createElement('ul');
|
||||
let resultsList = document.createElement("ul");
|
||||
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let typeClass = textAreaId.replaceAll(".", " ");
|
||||
|
||||
resultsDiv.style.maxHeight = `${CFG.maxResults * 50}px`;
|
||||
resultsDiv.setAttribute('class', `autocompleteResults ${typeClass}`);
|
||||
resultsList.setAttribute('class', 'autocompleteResultsList');
|
||||
resultsDiv.setAttribute("class", `autocompleteResults ${typeClass} notranslate`);
|
||||
resultsDiv.setAttribute("translate", "no");
|
||||
resultsList.setAttribute("class", "autocompleteResultsList");
|
||||
resultsDiv.appendChild(resultsList);
|
||||
|
||||
return resultsDiv;
|
||||
@@ -228,10 +226,23 @@ function showResults(textArea) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
|
||||
resultsDiv.style.display = "block";
|
||||
|
||||
if (CFG.slidingPopup) {
|
||||
let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd).left;
|
||||
let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - resultsDiv.offsetWidth);
|
||||
|
||||
resultsDiv.style.left = `${offset}px`;
|
||||
} else {
|
||||
if (resultsDiv.style.left)
|
||||
resultsDiv.style.removeProperty("left");
|
||||
}
|
||||
}
|
||||
function hideResults(textArea) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
|
||||
|
||||
if (!resultsDiv) return;
|
||||
|
||||
resultsDiv.style.display = "none";
|
||||
selectedTag = null;
|
||||
}
|
||||
@@ -239,29 +250,33 @@ function hideResults(textArea) {
|
||||
// Function to check activation criteria
|
||||
function isEnabled() {
|
||||
if (CFG.activeIn.global) {
|
||||
// Skip check if the current model was not correctly detected, since it could wrongly disable the script otherwise
|
||||
if (!currentModelName || !currentModelHash) return true;
|
||||
|
||||
let modelList = CFG.activeIn.modelList
|
||||
.split(",")
|
||||
.map(x => x.trim())
|
||||
.filter(x => x.length > 0);
|
||||
|
||||
let shortHash = currentModelHash.substring(0, 10);
|
||||
let modelNameWithoutHash = currentModelName.replace(/\[.*\]$/g, "").trim();
|
||||
if (CFG.activeIn.modelListMode.toLowerCase() === "blacklist") {
|
||||
// If the current model is in the blacklist, disable
|
||||
return modelList.filter(x => x === currentModelName || x === currentModelHash || x === shortHash).length === 0;
|
||||
return modelList.filter(x => x === currentModelName || x === modelNameWithoutHash || x === currentModelHash || x === shortHash).length === 0;
|
||||
} else {
|
||||
// If the current model is in the whitelist, enable.
|
||||
// An empty whitelist is ignored.
|
||||
return modelList.length === 0 || modelList.filter(x => x === currentModelName || x === currentModelHash || x === shortHash).length > 0;
|
||||
return modelList.length === 0 || modelList.filter(x => x === currentModelName || x === modelNameWithoutHash || x === currentModelHash || x === shortHash).length > 0;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const WEIGHT_REGEX = /[([]([^,()[\]:| ]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]/g;
|
||||
const WEIGHT_REGEX = /[([]([^()[\]:|]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]/g;
|
||||
const POINTY_REGEX = /<[^\s,<](?:[^\t\n\r,<>]*>|[^\t\n\r,> ]*)/g;
|
||||
const COMPLETED_WILDCARD_REGEX = /__[^\s,_][^\t\n\r,_]*[^\s,_]__[^\s,_]*/g;
|
||||
const NORMAL_TAG_REGEX = /[^\s,|<>]+|</g;
|
||||
const NORMAL_TAG_REGEX = /[^\s,|<>)\]]+|</g;
|
||||
const TAG_REGEX = new RegExp(`${POINTY_REGEX.source}|${COMPLETED_WILDCARD_REGEX.source}|${NORMAL_TAG_REGEX.source}`, "g");
|
||||
|
||||
// On click, insert the tag into the prompt textbox with respect to the cursor position
|
||||
@@ -298,23 +313,28 @@ async function insertTextAtCursor(textArea, result, tagword) {
|
||||
let match = surrounding.match(new RegExp(escapeRegExp(`${tagword}`), "i"));
|
||||
let afterInsertCursorPos = editStart + match.index + sanitizedText.length;
|
||||
|
||||
var optionalComma = "";
|
||||
if (CFG.appendComma && ![ResultType.wildcardFile, ResultType.yamlWildcard].includes(tagType)) {
|
||||
optionalComma = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null ? "" : ", ";
|
||||
var optionalSeparator = "";
|
||||
let extraNetworkTypes = [ResultType.hypernetwork, ResultType.lora];
|
||||
let noCommaTypes = [ResultType.wildcardFile, ResultType.yamlWildcard].concat(extraNetworkTypes);
|
||||
if (CFG.appendComma && !noCommaTypes.includes(tagType)) {
|
||||
optionalSeparator = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null ? "" : ", ";
|
||||
} else if (extraNetworkTypes.includes(tagType)) {
|
||||
// Use the dedicated separator for extra networks if it's defined, otherwise fall back to space
|
||||
optionalSeparator = CFG.extraNetworksSeparator || " ";
|
||||
}
|
||||
|
||||
// Replace partial tag word with new text, add comma if needed
|
||||
let insert = surrounding.replace(match, sanitizedText + optionalComma);
|
||||
let insert = surrounding.replace(match, sanitizedText + optionalSeparator);
|
||||
|
||||
// Add back start
|
||||
var newPrompt = prompt.substring(0, editStart) + insert + prompt.substring(editEnd);
|
||||
textArea.value = newPrompt;
|
||||
textArea.selectionStart = afterInsertCursorPos + optionalComma.length;
|
||||
textArea.selectionStart = afterInsertCursorPos + optionalSeparator.length;
|
||||
textArea.selectionEnd = textArea.selectionStart
|
||||
|
||||
// Since we've modified a Gradio Textbox component manually, we need to simulate an `input` DOM event to ensure its
|
||||
// internal Svelte data binding remains in sync.
|
||||
textArea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
// Since we've modified a Gradio Textbox component manually, we need to simulate an `input` DOM event to ensure it's propagated back to python.
|
||||
// Uses a built-in method from the webui's ui.js which also already accounts for event target
|
||||
updateInput(textArea);
|
||||
|
||||
// Update previous tags with the edited prompt to prevent re-searching the same term
|
||||
let weightedTags = [...newPrompt.matchAll(WEIGHT_REGEX)]
|
||||
@@ -353,7 +373,7 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
|
||||
// Find right colors from config
|
||||
let tagFileName = CFG.tagFile.split(".")[0];
|
||||
let tagColors = CFG.colors;
|
||||
let tagColors = CFG.colorMap;
|
||||
let mode = gradioApp().querySelector('.dark') ? 0 : 1;
|
||||
let nextLength = Math.min(results.length, resultCount + CFG.resultStepLength);
|
||||
|
||||
@@ -615,55 +635,52 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i');
|
||||
} else {
|
||||
searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
|
||||
}
|
||||
// If onlyShowAlias is enabled, we don't need to include normal results
|
||||
if (CFG.alias.onlyShowAlias) {
|
||||
results = allTags.filter(x => x[3] && x[3].toLowerCase().search(searchRegex) > -1);
|
||||
} else {
|
||||
// Else both normal tags and aliases/translations are included depending on the config
|
||||
let baseFilter = (x) => x[0].toLowerCase().search(searchRegex) > -1;
|
||||
let aliasFilter = (x) => x[3] && x[3].toLowerCase().search(searchRegex) > -1;
|
||||
let translationFilter = (x) => (translations.has(x[0]) && translations.get(x[0]).toLowerCase().search(searchRegex) > -1)
|
||||
|| x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().search(searchRegex) > -1);
|
||||
|
||||
let fil;
|
||||
if (CFG.alias.searchByAlias && CFG.translation.searchByTranslation)
|
||||
fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x);
|
||||
else if (CFG.alias.searchByAlias && !CFG.translation.searchByTranslation)
|
||||
fil = (x) => baseFilter(x) || aliasFilter(x);
|
||||
else if (CFG.translation.searchByTranslation && !CFG.alias.searchByAlias)
|
||||
fil = (x) => baseFilter(x) || translationFilter(x);
|
||||
else
|
||||
fil = (x) => baseFilter(x);
|
||||
}
|
||||
|
||||
// Add final results
|
||||
allTags.filter(fil).forEach(t => {
|
||||
let result = new AutocompleteResult(t[0].trim(), ResultType.tag)
|
||||
result.category = t[1];
|
||||
result.count = t[2];
|
||||
result.aliases = t[3];
|
||||
results.push(result);
|
||||
// Both normal tags and aliases/translations are included depending on the config
|
||||
let baseFilter = (x) => x[0].toLowerCase().search(searchRegex) > -1;
|
||||
let aliasFilter = (x) => x[3] && x[3].toLowerCase().search(searchRegex) > -1;
|
||||
let translationFilter = (x) => (translations.has(x[0]) && translations.get(x[0]).toLowerCase().search(searchRegex) > -1)
|
||||
|| x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().search(searchRegex) > -1);
|
||||
|
||||
let fil;
|
||||
if (CFG.alias.searchByAlias && CFG.translation.searchByTranslation)
|
||||
fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x);
|
||||
else if (CFG.alias.searchByAlias && !CFG.translation.searchByTranslation)
|
||||
fil = (x) => baseFilter(x) || aliasFilter(x);
|
||||
else if (CFG.translation.searchByTranslation && !CFG.alias.searchByAlias)
|
||||
fil = (x) => baseFilter(x) || translationFilter(x);
|
||||
else
|
||||
fil = (x) => baseFilter(x);
|
||||
|
||||
// Add final results
|
||||
allTags.filter(fil).forEach(t => {
|
||||
let result = new AutocompleteResult(t[0].trim(), ResultType.tag)
|
||||
result.category = t[1];
|
||||
result.count = t[2];
|
||||
result.aliases = t[3];
|
||||
results.push(result);
|
||||
});
|
||||
|
||||
// Add extras
|
||||
if (CFG.extra.extraFile) {
|
||||
let extraResults = [];
|
||||
|
||||
extras.filter(fil).forEach(e => {
|
||||
let result = new AutocompleteResult(e[0].trim(), ResultType.extra)
|
||||
result.category = e[1] || 0; // If no category is given, use 0 as the default
|
||||
result.meta = e[2] || "Custom tag";
|
||||
result.aliases = e[3] || "";
|
||||
extraResults.push(result);
|
||||
});
|
||||
|
||||
// Add extras
|
||||
if (CFG.extra.extraFile) {
|
||||
let extraResults = [];
|
||||
|
||||
extras.filter(fil).forEach(e => {
|
||||
let result = new AutocompleteResult(e[0].trim(), ResultType.extra)
|
||||
result.category = e[1] || 0; // If no category is given, use 0 as the default
|
||||
result.meta = e[2] || "Custom tag";
|
||||
result.aliases = e[3] || "";
|
||||
extraResults.push(result);
|
||||
});
|
||||
|
||||
if (CFG.extra.addMode === "Insert before") {
|
||||
results = extraResults.concat(results);
|
||||
} else {
|
||||
results = results.concat(extraResults);
|
||||
}
|
||||
if (CFG.extra.addMode === "Insert before") {
|
||||
results = extraResults.concat(results);
|
||||
} else {
|
||||
results = results.concat(extraResults);
|
||||
}
|
||||
}
|
||||
|
||||
// Slice if the user has set a max result count
|
||||
if (!CFG.showAllResults) {
|
||||
results = results.slice(0, CFG.maxResults);
|
||||
@@ -684,8 +701,18 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
function navigateInList(textArea, event) {
|
||||
// Return if the function is deactivated in the UI or the current model is excluded due to white/blacklist settings
|
||||
if (!isEnabled()) return;
|
||||
|
||||
let keys = CFG.keymap;
|
||||
|
||||
validKeys = ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", "Enter", "Tab", "Escape"];
|
||||
// Close window if Home or End is pressed while not a keybinding, since it would break completion on leaving the original tag
|
||||
if ((event.key === "Home" || event.key === "End") && !Object.values(keys).includes(event.key)) {
|
||||
hideResults(textArea);
|
||||
return;
|
||||
}
|
||||
|
||||
// All set keys that are not None or empty are valid
|
||||
// Default keys are: ArrowUp, ArrowDown, PageUp, PageDown, Home, End, Enter, Tab, Escape
|
||||
validKeys = Object.values(keys).filter(x => x !== "None" && x !== "");
|
||||
|
||||
if (!validKeys.includes(event.key)) return;
|
||||
if (!isVisible(textArea)) return
|
||||
@@ -695,63 +722,60 @@ function navigateInList(textArea, event) {
|
||||
oldSelectedTag = selectedTag;
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowUp":
|
||||
case keys["MoveUp"]:
|
||||
if (selectedTag === null) {
|
||||
selectedTag = resultCount - 1;
|
||||
} else {
|
||||
selectedTag = (selectedTag - 1 + resultCount) % resultCount;
|
||||
}
|
||||
break;
|
||||
case "ArrowDown":
|
||||
case keys["MoveDown"]:
|
||||
if (selectedTag === null) {
|
||||
selectedTag = 0;
|
||||
} else {
|
||||
selectedTag = (selectedTag + 1) % resultCount;
|
||||
}
|
||||
break;
|
||||
case "PageUp":
|
||||
case keys["JumpUp"]:
|
||||
if (selectedTag === null || selectedTag === 0) {
|
||||
selectedTag = resultCount - 1;
|
||||
} else {
|
||||
selectedTag = (Math.max(selectedTag - 5, 0) + resultCount) % resultCount;
|
||||
}
|
||||
break;
|
||||
case "PageDown":
|
||||
case keys["JumpDown"]:
|
||||
if (selectedTag === null || selectedTag === resultCount - 1) {
|
||||
selectedTag = 0;
|
||||
} else {
|
||||
selectedTag = Math.min(selectedTag + 5, resultCount - 1) % resultCount;
|
||||
}
|
||||
break;
|
||||
case "Home":
|
||||
case keys["JumpToStart"]:
|
||||
selectedTag = 0;
|
||||
break;
|
||||
case "End":
|
||||
case keys["JumpToEnd"]:
|
||||
selectedTag = resultCount - 1;
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
selectedTag = 0;
|
||||
break;
|
||||
case "ArrowRight":
|
||||
selectedTag = resultCount - 1;
|
||||
break;
|
||||
case "Enter":
|
||||
case keys["ChooseSelected"]:
|
||||
if (selectedTag !== null) {
|
||||
insertTextAtCursor(textArea, results[selectedTag], tagword);
|
||||
} else {
|
||||
hideResults(textArea);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "Tab":
|
||||
case keys["ChooseFirstOrSelected"]:
|
||||
if (selectedTag === null) {
|
||||
selectedTag = 0;
|
||||
}
|
||||
insertTextAtCursor(textArea, results[selectedTag], tagword);
|
||||
break;
|
||||
case "Escape":
|
||||
case keys["Close"]:
|
||||
hideResults(textArea);
|
||||
break;
|
||||
}
|
||||
if (selectedTag === resultCount - 1
|
||||
&& (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "ArrowLeft" || event.key === "ArrowRight")) {
|
||||
&& (event.key === keys["MoveUp"] || event.key === keys["MoveDown"] || event.key === keys["JumpToStart"] || event.key === keys["JumpToEnd"])) {
|
||||
addResultsToList(textArea, results, tagword, false);
|
||||
}
|
||||
// Update highlighting
|
||||
@@ -765,9 +789,6 @@ function navigateInList(textArea, event) {
|
||||
|
||||
// One-time setup, triggered from onUiUpdate
|
||||
async function setup() {
|
||||
// Load colors
|
||||
CFG["colors"] = (await readFile(`${tagBasePath}/colors.json`, true));
|
||||
|
||||
// Load external files needed by completion extensions
|
||||
await processQueue(QUEUE_FILE_LOAD, null);
|
||||
|
||||
@@ -793,14 +814,6 @@ async function setup() {
|
||||
});
|
||||
});
|
||||
|
||||
// Add change listener to model dropdown to react to model changes
|
||||
let modelDropdown = gradioApp().querySelector("#setting_sd_model_checkpoint select");
|
||||
currentModelName = modelDropdown.value;
|
||||
modelDropdown?.addEventListener("change", () => {
|
||||
setTimeout(() => {
|
||||
currentModelName = modelDropdown.value;
|
||||
}, 100);
|
||||
});
|
||||
// Add mutation observer for the model hash text to also allow hash-based blacklist again
|
||||
let modelHashText = gradioApp().querySelector("#sd_checkpoint_hash");
|
||||
if (modelHashText) {
|
||||
@@ -809,6 +822,14 @@ async function setup() {
|
||||
for (const mutation of mutationList) {
|
||||
if (mutation.type === "attributes" && mutation.attributeName === "title") {
|
||||
currentModelHash = mutation.target.title;
|
||||
let modelDropdown = gradioApp().querySelector("#setting_sd_model_checkpoint span.single-select")
|
||||
if (modelDropdown) {
|
||||
currentModelName = modelDropdown.textContent;
|
||||
} else {
|
||||
// Fallback for older versions
|
||||
modelDropdown = gradioApp().querySelector("#setting_sd_model_checkpoint select");
|
||||
currentModelName = modelDropdown.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,14 +5,19 @@ import gradio as gr
|
||||
from pathlib import Path
|
||||
from modules import scripts, script_callbacks, shared, sd_hijack
|
||||
import yaml
|
||||
import time
|
||||
import threading
|
||||
|
||||
# Webui root path
|
||||
FILE_DIR = Path().absolute()
|
||||
try:
|
||||
from modules.paths import script_path, extensions_dir
|
||||
# Webui root path
|
||||
FILE_DIR = Path(script_path)
|
||||
|
||||
# The extension base path
|
||||
EXT_PATH = FILE_DIR.joinpath('extensions')
|
||||
# The extension base path
|
||||
EXT_PATH = Path(extensions_dir)
|
||||
except ImportError:
|
||||
# Webui root path
|
||||
FILE_DIR = Path().absolute()
|
||||
# The extension base path
|
||||
EXT_PATH = FILE_DIR.joinpath('extensions')
|
||||
|
||||
# Tags base path
|
||||
TAGS_PATH = Path(scripts.basedir()).joinpath('tags')
|
||||
@@ -26,6 +31,11 @@ try:
|
||||
LORA_PATH = Path(shared.cmd_opts.lora_dir)
|
||||
except AttributeError:
|
||||
LORA_PATH = None
|
||||
|
||||
try:
|
||||
LYCO_PATH = Path(shared.cmd_opts.lyco_dir)
|
||||
except AttributeError:
|
||||
LYCO_PATH = None
|
||||
|
||||
def find_ext_wildcard_paths():
|
||||
"""Returns the path to the extension wildcards folder"""
|
||||
@@ -151,7 +161,7 @@ def get_hypernetworks():
|
||||
# Get a list of all hypernetworks in the folder
|
||||
all_hypernetworks = [str(h.name) for h in HYP_PATH.rglob("*") if h.suffix in {".pt"}]
|
||||
# Remove file extensions
|
||||
return [h[:h.rfind('.')] for h in all_hypernetworks]
|
||||
return sorted([h[:h.rfind('.')] for h in all_hypernetworks], key=lambda x: x.lower())
|
||||
|
||||
def get_lora():
|
||||
"""Write a list of all lora"""
|
||||
@@ -159,8 +169,15 @@ def get_lora():
|
||||
# Get a list of all lora in the folder
|
||||
all_lora = [str(l.name) for l in LORA_PATH.rglob("*") if l.suffix in {".safetensors", ".ckpt", ".pt"}]
|
||||
# Remove file extensions
|
||||
return [l[:l.rfind('.')] for l in all_lora]
|
||||
return sorted([l[:l.rfind('.')] for l in all_lora], key=lambda x: x.lower())
|
||||
|
||||
def get_lyco():
|
||||
"""Write a list of all LyCORIS/LOHA from https://github.com/KohakuBlueleaf/a1111-sd-webui-lycoris"""
|
||||
|
||||
# Get a list of all LyCORIS in the folder
|
||||
all_lyco = [str(ly.name) for ly in LYCO_PATH.rglob("*") if ly.suffix in {".safetensors", ".ckpt", ".pt"}]
|
||||
# Remove file extensions
|
||||
return sorted([ly[:ly.rfind('.')] for ly in all_lyco], key=lambda x: x.lower())
|
||||
|
||||
def write_tag_base_path():
|
||||
"""Writes the tag base path to a fixed location temporary file"""
|
||||
@@ -204,6 +221,7 @@ write_to_temp_file('wce.txt', [])
|
||||
write_to_temp_file('wcet.txt', [])
|
||||
write_to_temp_file('hyp.txt', [])
|
||||
write_to_temp_file('lora.txt', [])
|
||||
write_to_temp_file('lyco.txt', [])
|
||||
# Only reload embeddings if the file doesn't exist, since they are already re-written on model load
|
||||
if not TEMP_PATH.joinpath("emb.txt").exists():
|
||||
write_to_temp_file('emb.txt', [])
|
||||
@@ -239,6 +257,11 @@ if LORA_PATH is not None and LORA_PATH.exists():
|
||||
if lora:
|
||||
write_to_temp_file('lora.txt', lora)
|
||||
|
||||
if LYCO_PATH is not None and LYCO_PATH.exists():
|
||||
lyco = get_lyco()
|
||||
if lyco:
|
||||
write_to_temp_file('lyco.txt', lyco)
|
||||
|
||||
# Register autocomplete options
|
||||
def on_ui_settings():
|
||||
TAC_SECTION = ("tac", "Tag Autocomplete")
|
||||
@@ -253,6 +276,7 @@ def on_ui_settings():
|
||||
shared.opts.add_option("tac_activeIn.modelList", shared.OptionInfo("", "List of model names (with file extension) or their hashes to use as black/whitelist, separated by commas.", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_activeIn.modelListMode", shared.OptionInfo("Blacklist", "Mode to use for model list", gr.Dropdown, lambda: {"choices": ["Blacklist","Whitelist"]}, section=TAC_SECTION))
|
||||
# Results related settings
|
||||
shared.opts.add_option("tac_slidingPopup", shared.OptionInfo(True, "Move completion popup together with text cursor", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_maxResults", shared.OptionInfo(5, "Maximum results", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_showAllResults", shared.OptionInfo(False, "Show all results", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_resultStepLength", shared.OptionInfo(100, "How many results to load at once", section=TAC_SECTION))
|
||||
@@ -261,6 +285,7 @@ def on_ui_settings():
|
||||
shared.opts.add_option("tac_useEmbeddings", shared.OptionInfo(True, "Search for embeddings", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_useHypernetworks", shared.OptionInfo(True, "Search for hypernetworks", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_useLoras", shared.OptionInfo(True, "Search for Loras", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_useLycos", shared.OptionInfo(True, "Search for LyCORIS/LoHa", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_showWikiLinks", shared.OptionInfo(False, "Show '?' next to tags, linking to its Danbooru or e621 wiki page (Warning: This is an external site and very likely contains NSFW examples!)", section=TAC_SECTION))
|
||||
# Insertion related settings
|
||||
shared.opts.add_option("tac_replaceUnderscores", shared.OptionInfo(True, "Replace underscores with spaces on insertion", section=TAC_SECTION))
|
||||
@@ -276,5 +301,52 @@ def on_ui_settings():
|
||||
# Extra file settings
|
||||
shared.opts.add_option("tac_extra.extraFile", shared.OptionInfo("extra-quality-tags.csv", "Extra filename (for small sets of custom tags)", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_extra.addMode", shared.OptionInfo("Insert before", "Mode to add the extra tags to the main tag list", gr.Dropdown, lambda: {"choices": ["Insert before","Insert after"]}, section=TAC_SECTION))
|
||||
# Custom mappings
|
||||
keymapDefault = """\
|
||||
{
|
||||
"MoveUp": "ArrowUp",
|
||||
"MoveDown": "ArrowDown",
|
||||
"JumpUp": "PageUp",
|
||||
"JumpDown": "PageDown",
|
||||
"JumpToStart": "Home",
|
||||
"JumpToEnd": "End",
|
||||
"ChooseSelected": "Enter",
|
||||
"ChooseFirstOrSelected": "Tab",
|
||||
"Close": "Escape"
|
||||
}\
|
||||
"""
|
||||
colorDefault = """\
|
||||
{
|
||||
"danbooru": {
|
||||
"-1": ["red", "maroon"],
|
||||
"0": ["lightblue", "dodgerblue"],
|
||||
"1": ["indianred", "firebrick"],
|
||||
"3": ["violet", "darkorchid"],
|
||||
"4": ["lightgreen", "darkgreen"],
|
||||
"5": ["orange", "darkorange"]
|
||||
},
|
||||
"e621": {
|
||||
"-1": ["red", "maroon"],
|
||||
"0": ["lightblue", "dodgerblue"],
|
||||
"1": ["gold", "goldenrod"],
|
||||
"3": ["violet", "darkorchid"],
|
||||
"4": ["lightgreen", "darkgreen"],
|
||||
"5": ["tomato", "darksalmon"],
|
||||
"6": ["red", "maroon"],
|
||||
"7": ["whitesmoke", "black"],
|
||||
"8": ["seagreen", "darkseagreen"]
|
||||
}
|
||||
}\
|
||||
"""
|
||||
keymapLabel = "Configure Hotkeys. For possible values, see https://www.w3.org/TR/uievents-key, or leave empty / set to 'None' to disable. Must be valid JSON."
|
||||
colorLabel = "Configure colors. See https://github.com/DominikDoom/a1111-sd-webui-tagcomplete#colors for info. Must be valid JSON."
|
||||
|
||||
try:
|
||||
shared.opts.add_option("tac_keymap", shared.OptionInfo(keymapDefault, keymapLabel, gr.Code, lambda: {"language": "json", "interactive": True}, section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_colormap", shared.OptionInfo(colorDefault, colorLabel, gr.Code, lambda: {"language": "json", "interactive": True}, section=TAC_SECTION))
|
||||
except AttributeError:
|
||||
shared.opts.add_option("tac_keymap", shared.OptionInfo(keymapDefault, keymapLabel, gr.Textbox, section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_colormap", shared.OptionInfo(colorDefault, colorLabel, gr.Textbox, section=TAC_SECTION))
|
||||
|
||||
|
||||
script_callbacks.on_ui_settings(on_ui_settings)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"danbooru": {
|
||||
"-1": ["red", "maroon"],
|
||||
"0": ["lightblue", "dodgerblue"],
|
||||
"1": ["indianred", "firebrick"],
|
||||
"3": ["violet", "darkorchid"],
|
||||
"4": ["lightgreen", "darkgreen"],
|
||||
"5": ["orange", "darkorange"]
|
||||
},
|
||||
"e621": {
|
||||
"-1": ["red", "maroon"],
|
||||
"0": ["lightblue", "dodgerblue"],
|
||||
"1": ["gold", "goldenrod"],
|
||||
"3": ["violet", "darkorchid"],
|
||||
"4": ["lightgreen", "darkgreen"],
|
||||
"5": ["tomato", "darksalmon"],
|
||||
"6": ["red", "maroon"],
|
||||
"7": ["whitesmoke", "black"],
|
||||
"8": ["seagreen", "darkseagreen"]
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,4 @@ best_quality,5,Quality tag,
|
||||
high_quality,5,Quality tag,
|
||||
normal_quality,5,Quality tag,
|
||||
low_quality,5,Quality tag,
|
||||
worst_quality,5,Quality tag,
|
||||
worst_quality,5,Quality tag,
|
||||
|
Reference in New Issue
Block a user