mirror of
https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git
synced 2026-01-26 19:19:57 +00:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c8ed0d265 | ||
|
|
5cbb9cefc2 | ||
|
|
a7468da59b | ||
|
|
f4218a71cd | ||
|
|
eb19cae176 | ||
|
|
7bfe4f3b54 | ||
|
|
9b66d42111 | ||
|
|
7a5a7f31cf | ||
|
|
76cd4bb6f1 | ||
|
|
21b050a355 | ||
|
|
1cb4fc8f25 | ||
|
|
2d7e6181f5 | ||
|
|
c6049fc2fa | ||
|
|
eff53d0ca7 | ||
|
|
a7da670e97 | ||
|
|
da65feea31 | ||
|
|
6997558714 | ||
|
|
a95f422f10 | ||
|
|
5640a438da | ||
|
|
3169c4c653 | ||
|
|
bba5d6b1c0 | ||
|
|
7fbfb7dd43 | ||
|
|
2e93691305 | ||
|
|
eaba97dc3a | ||
|
|
5e453efc2b | ||
|
|
13c7f31bba | ||
|
|
c4c588c1d4 | ||
|
|
1d40449942 | ||
|
|
495feb026c | ||
|
|
b59123f6e6 | ||
|
|
79b670eaea | ||
|
|
6b51dc806b | ||
|
|
5b9af499c3 | ||
|
|
aed449c882 | ||
|
|
a724da362c | ||
|
|
cc67adf82f | ||
|
|
c1d610e390 | ||
|
|
7fabc84a1e | ||
|
|
5d5db7bafe | ||
|
|
40edb89974 | ||
|
|
f9f7732c69 | ||
|
|
e7af9dbfba | ||
|
|
64cf9b2159 | ||
|
|
ba38d1b893 | ||
|
|
4442cb78ec | ||
|
|
6b42efaa40 | ||
|
|
47e0c15835 | ||
|
|
68b4224f37 | ||
|
|
fc6c1ff579 | ||
|
|
50b33b987a | ||
|
|
ea24f7657a | ||
|
|
1e9431faba | ||
|
|
664ae50c1a | ||
|
|
43243a9bf1 | ||
|
|
8912957a26 | ||
|
|
5fe5398b94 | ||
|
|
52f92e4d42 | ||
|
|
0e177d0945 | ||
|
|
94365630c7 | ||
|
|
d4941c7b73 | ||
|
|
91fb1cba38 | ||
|
|
3169420fd3 | ||
|
|
84b6a0394e | ||
|
|
38fd2523e6 | ||
|
|
85db4a61df | ||
|
|
b18823e88f | ||
|
|
83461e2f54 | ||
|
|
a2e7b6bf6c | ||
|
|
f4572469c1 | ||
|
|
672d409e46 | ||
|
|
3b51035c26 | ||
|
|
dcc6602056 |
457
README.md
457
README.md
@@ -1,22 +1,69 @@
|
||||

|
||||
|
||||
# Booru tag autocompletion for A1111
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)
|
||||
# SD WebUI Tag Autocomplete
|
||||
## English • [简体中文](./README_ZH.md) • [日本語](./README_JA.md)
|
||||
|
||||
## [中文文档](./README_ZH.md)
|
||||
Booru style tag autocompletion for the AUTOMATIC1111 Stable Diffusion WebUI
|
||||
|
||||
This custom script serves as a drop-in extension for the popular [AUTOMATIC1111 web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) for Stable Diffusion.
|
||||
[![Github Release][release-shield]][release-url]
|
||||
[![stargazers][stargazers-shield]][stargazers-url]
|
||||
[![contributors][contributors-shield]][contributors-url]
|
||||
[![forks][forks-shield]][forks-url]
|
||||
[![issues][issues-shield]][issues-url]
|
||||
|
||||
[Changelog][release-url] •
|
||||
[Known Issues](#%EF%B8%8F-common-problems--known-issues) •
|
||||
[Report Bug][issues-url] •
|
||||
[Request Feature][issues-url]
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
# 📄 Description
|
||||
|
||||
Tag Autocomplete is an extension for the popular [AUTOMATIC1111 web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) for Stable Diffusion.
|
||||
|
||||
It displays autocompletion hints for recognized tags from "image booru" boards such as Danbooru, which are primarily used for browsing Anime-style illustrations.
|
||||
Since some Stable Diffusion models were trained using this information, for example [Waifu Diffusion](https://github.com/harubaru/waifu-diffusion), using exact tags in prompts can often improve composition and help to achieve a wanted look.
|
||||
Since some Stable Diffusion models were trained using this information, for example [Waifu Diffusion](https://github.com/harubaru/waifu-diffusion) and many of the NAI-descendant models or merges, using exact tags in prompts can often improve composition and consistency.
|
||||
|
||||
You can install it using the inbuilt available extensions list, clone the files manually as described [below](#installation), or use a pre-packaged version from [Releases](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases).
|
||||
You can install it using the inbuilt available extensions list, clone the files manually as described [below](#-installation), or use a pre-packaged version from [Releases](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases).
|
||||
|
||||
## 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.
|
||||
<br/>
|
||||
|
||||
## Screenshots & Demo videos
|
||||
# ✨ Features
|
||||
- 🚀 Instant completion hints while typing (under normal circumstances)
|
||||
- ⌨️ Keyboard navigation
|
||||
- 🌒 Dark & Light mode support
|
||||
- 🛠️ Many [settings](#%EF%B8%8F-settings) and customizability
|
||||
- 🌍 [Translation support](#translations) for tags, with optional live preview for the full prompt
|
||||
- **Note:** Translation files are provided by the community, see [here](#list-of-translations) for a list of translations I know of.
|
||||
|
||||
Tag autocomplete supports built-in completion for:
|
||||
- 🏷️ **Danbooru & e621 tags** (Top 100k by post count, as of November 2022)
|
||||
- ✳️ [**Wildcards**](#wildcards)
|
||||
- ➕ [**Extra network**](#extra-networks-embeddings-hypernets-lora) filenames, including
|
||||
- Textual Inversion embeddings [(jump to readme section)]
|
||||
- Hypernetworks
|
||||
- LoRA
|
||||
- LyCORIS / LoHA
|
||||
- 🪄 [**Chants**](#chants) (custom format for longer prompt presets)
|
||||
- 🏷️ "[**Extra file**](#extra-file)", one set of customizable extra tags
|
||||
|
||||
|
||||
Additionally, some support for other third party extensions exists:
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
- [Image Browser][image-browser-url] - Filename & EXIF keyword search
|
||||
- [Multidiffusion Upscaler][multidiffusion-url] - Regional Prompts
|
||||
- [Dataset Tag Editor][tag-editor-url] - Caption, Interrogate Result, Edit Tags & Edit Caption
|
||||
- [WD 1.4 Tagger][wd-tagger-url] - Additional & Excluded tags
|
||||
- [Umi AI][umi-url] - Completion for YAML wildcards
|
||||
</details>
|
||||
<br/>
|
||||
|
||||
# 🖼️ Screenshots & Demo videos
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
Basic usage (with keyboard navigation):
|
||||
@@ -32,29 +79,33 @@ Dark and Light mode supported, including tag colors:
|
||||

|
||||

|
||||
</details>
|
||||
<br/>
|
||||
|
||||
## Installation
|
||||
### 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
|
||||
# 📦 Installation
|
||||
## 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
|
||||
- Alternatively, use <kbd>CRTL</kbd> + <kbd>F</kbd> to search for the text on the page
|
||||
5. Click **Install** on the right side
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
### Manual clone
|
||||
## 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).
|
||||
|
||||
## Additional completion support
|
||||
### Wildcards
|
||||
<br/>
|
||||
|
||||
# ❇️ 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.
|
||||
@@ -65,20 +116,67 @@ This enables you to either insert categories to be replaced by the script, or di
|
||||
|
||||
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.
|
||||
|
||||
### 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:
|
||||
## Extra networks (Embeddings, Hypernets, LoRA, ...)
|
||||
Completion for these types is triggered by typing `<`. By default it will show them all mixed together, but further filtering can be done in the following way:
|
||||
- `<e:` will only show embeddings
|
||||
- `<l:` or `<lora:` will only show Lora
|
||||
- `<l:` will only show LoRA and LyCORIS
|
||||
- Or `<lora:` and `<lyco:` respectively for the long form
|
||||
- `<h:` or `<hypernet:` will only show Hypernetworks
|
||||
|
||||
#### Embedding type filtering
|
||||
### 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.
|
||||
|
||||
For example:
|
||||
|
||||

|
||||
|
||||
### Umi AI tags
|
||||
## Chants
|
||||
Chants are longer prompt presets. The name is inspired by some early prompt collections from Chinese users, which often were called along the lines of "Spellbook", "Codex", etc. The prompt snippets from such documents were fittingly called spells or chants for this reason.
|
||||
|
||||
Similar to embeddings and loras, this feature is triggered by typing the `<`, `<c:` or `<chant:` commands. For instance, when you enter `<c:HighQuality` in the prompt box and select it, the following prompt text will be inserted:
|
||||
```
|
||||
(masterpiece, best quality, high quality, highres, ultra-detailed),
|
||||
```
|
||||
|
||||
|
||||
Chants can be added in JSON files following this format:
|
||||
<details>
|
||||
<summary>Chant format (click to expand)</summary>
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Basic-NegativePrompt",
|
||||
"terms": "Negative,Low,Quality",
|
||||
"content": "(worst quality, low quality, normal quality)",
|
||||
"color": 3
|
||||
},
|
||||
{
|
||||
"name": "Basic-HighQuality",
|
||||
"terms": "Best,High,Quality",
|
||||
"content": "(masterpiece, best quality, high quality, highres, ultra-detailed)",
|
||||
"color": 1
|
||||
},
|
||||
{
|
||||
"name": "Basic-Start",
|
||||
"terms": "Basic, Start, Simple, Demo",
|
||||
"content": "(masterpiece, best quality, high quality, highres), 1girl, extremely beautiful detailed face, ...",
|
||||
"color": 5
|
||||
}
|
||||
]
|
||||
```
|
||||
</details>
|
||||
<br/>
|
||||
|
||||
The file can then be selected using the "Chant file" settings dropdown if it is located inside the extension's `tags` folder.
|
||||
|
||||
A chant object has four fields:
|
||||
- `name` - Display name
|
||||
- `terms` - Search terms
|
||||
- `content` - The actual prompt content
|
||||
- `color` - Color, using the same category color system as normal tags
|
||||
|
||||
## 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 `>`.
|
||||
|
||||
@@ -87,76 +185,236 @@ It also shows how many fill-in tags are available to choose from for that combo
|
||||
|
||||
Most of the credit goes to [@ctwrs](https://github.com/ctwrs) here, they contributed a lot as one of the Umi developers.
|
||||
|
||||
## Settings
|
||||
# 🛠️ Settings
|
||||
|
||||
The extension has a large amount of configuration & customizability built in:
|
||||
The extension has a large amount of configuration & customizability built in. Most should be self-explanatory, but for a detailed description click on a section below.
|
||||
|
||||

|
||||
<!-- Filename -->
|
||||
<details>
|
||||
<summary>Tag filename</summary>
|
||||
|
||||
The main tag file the script uses. Included by default are `danbooru.csv` and `e621.csv`. While you can add custom tags here, the vast majority of models are not trained on anything other than these two (mostly danbooru), so it will not have much benefit.
|
||||
|
||||
You can also set it to `None` if you want to use other functionality of the extension (e.g. Wildcard or LoRA completion), but aren't interested in the normal tags.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Active In -->
|
||||
<details>
|
||||
<summary>"Active in" settings</summary>
|
||||
|
||||
Specifies where tag autocomplete should attach itself to and listen for changes.
|
||||
Negative prompts follow the settings for txt2img & img2img, so they will only be active if their "parent" is active.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Blacklist -->
|
||||
<details>
|
||||
<summary>Black / Whitelist</summary>
|
||||
|
||||
While the above options can turn off tag autocomplete globally, sometimes you might want to enable or disable it only for specific models. For example, if most of your models are Anime ones, you could add your photorealistic models, that weren't trained on booru tags and don't benefit from it, to the blacklist, which will automatically disable it after you switch to these models. You can use both the model name (including file extension) and their webui hashes (both short and long form).
|
||||
|
||||
`Blacklist` will exclude all specified models, while `Whitelist` will only activate it for these and stay off by default. One exception is an empty whitelist, which will be ignored (making it the same as an empty blacklist).
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Move Popup -->
|
||||
<details>
|
||||
<summary>Move completion popup with cursor</summary>
|
||||
|
||||
This option enables or disables the floating popup to follow the position of your cursor, like it would do in an IDE. The script tries to reserve enough room for the popup to prevent squishing on the right side, but that doesn't always work for longer tags. If disabled, the popup will stay on the left.
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
</details>
|
||||
<!-- Results Count -->
|
||||
<details>
|
||||
<summary>Result count</summary>
|
||||
|
||||
Settings for the amount of results to show at once.
|
||||
If `Show all results` is active, it will show a scrollable list instead of cutting it off after the number specified in `Maximum results`. For performance reasons, in that case not all are loaded at once, but instead in blocks. The block size is dictated by `How many results to load at once`. Once you reach the bottom, the next block will load (but that should rarely happen).
|
||||
|
||||
Notably, `Maximum results` will still have an influence if `Show all results` is used, since it dictates the height of the popup before scrolling begins.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Delay time -->
|
||||
<details>
|
||||
<summary>Completion delay</summary>
|
||||
|
||||
Depending on the configuration, real time tag completion can get computationally expensive.
|
||||
This option sets a "debounce" delay in milliseconds (1000ms = 1s), during which no second completion will get queried. This might especially be useful if you type very fast.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Search for -->
|
||||
<details>
|
||||
<summary>"Search for" settings</summary>
|
||||
|
||||
Pretty self explanatory, enables or disables certain completion types.
|
||||
|
||||
Umi AI wildcards are included in the normal wildcard option here, although they use a different format, since their usage intention is similar.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Wiki links -->
|
||||
<details>
|
||||
<summary>"?" Wiki links</summary>
|
||||
|
||||
If this option is turned on, it will show a `?` link next to the tag. Clicking this will try to open the wiki page for that tag on danbooru or e621, depending on which tag file you use.
|
||||
|
||||
> ⚠️ Warning:
|
||||
>
|
||||
> Danbooru and e621 are external sites and include a lot of NSFW content, which might show in the list of examples for a tag on its wiki page. Because of this, the option is disabled by default.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Insertion -->
|
||||
<details>
|
||||
<summary>Completion settings</summary>
|
||||
|
||||
These settings specify how the text will be inserted.
|
||||
|
||||
Booru sites mostly use underscores in tags instead of spaces, but during preprocessing most models replaced this back with spaces since the CLIP encoder used in Stable diffusion was trained on natural language. Thus, by default tag autocomplete will as well.
|
||||
|
||||
Parentheses are used as control characters in the webui to give more attention / weight to a specific part of the prompt, so tags including parentheses are escaped (`\( \)`) by default to not influence that.
|
||||
|
||||
Depending on the last setting, tag autocomplete will append a comma and space after inserting a tag, which may help for rapid completion of multiple tags in a row.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Alias -->
|
||||
<details>
|
||||
<summary>Alias settings</summary>
|
||||
|
||||
Tags often are referred to with multiple aliases. If `Search by alias` is turned on, those will be included in the search results, which might help if you are unsure of the correct tag. They will still get replaced by the actual tag they are linked to on insertion, since that is what the models were trained on.
|
||||
|
||||
`Only show alias` sets if you want to see only the alias or also the tag it maps to
|
||||
(shown as `<alias> ➝ <actual>`)
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Translations -->
|
||||
<details>
|
||||
<summary>Translation settings</summary>
|
||||
|
||||
Tag Autocomplete has support for tag translations specified in a separate file (`Translation filename`). You can search for tags using those translations, meaning that if you don't know the English tagword and have a translation file in your native language, you can use that instead.
|
||||
|
||||
It also has a legacy format option for some old files used in the community, as well as an experimental live translation preview for the whole prompt so you can easily find and edit tags afterwards.
|
||||
|
||||
For more details, see the [section on translations](#translations) below.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Extra file -->
|
||||
<details>
|
||||
<summary>Extra file settings</summary>
|
||||
|
||||
Specifies a set of extra tags that get appended either before or after the regular results, as specified here. Mostly useful for small custom tag sets such as the commonly used quality tags (masterpiece, best quality, etc.)
|
||||
|
||||
If you want completion for longer presets or even whole prompts, have a look at [Chants](#chants) instead.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Chants -->
|
||||
<details>
|
||||
<summary>Chant filename</summary>
|
||||
|
||||
Chants are longer presets or even whole prompts that can be selected & inserted at once, similar to the built in styles dropdown of the webui. They do offer some additional features though, and are faster to use.
|
||||
|
||||
For more info, see the section on [Chants](#chants) above.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Hotkeys -->
|
||||
<details>
|
||||
<summary>Hotkeys</summary>
|
||||
|
||||
You can specify the hotkeys for most keyboard navigation features here.
|
||||
Should be one of the key names specified in https://www.w3.org/TR/uievents-key/#named-key-attribute-value.
|
||||
|
||||
Function explanation:
|
||||
- Move Up / Down: Select the next tag
|
||||
- Jump Up / Down: Move by five places at once
|
||||
- Jump to Start / End: Jump to the top or bottom of the list
|
||||
- ChooseSelected: Select the highlighted tag, or close popup if nothing was selected
|
||||
- ChooseSelectedOrFirst: Same as above, but default to the first result if nothing was selected
|
||||
- Close: Closes the popup
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Colors -->
|
||||
<details>
|
||||
<summary>Colors</summary>
|
||||
|
||||
Here, you can change the default colors used for different tag categories. They were chosen to be similar to the category colors of their source site.
|
||||
|
||||
The format is standard JSON
|
||||
- The object names correspond to the tag filename they should be used for.
|
||||
- The numbers are specifying the tag type, which is dependent on the tag source. For more info, see [CSV tag data](#csv-tag-data).
|
||||
- The first value in the square brackets is for dark, the second for light mode. HTML color names and hex codes should both work.
|
||||
|
||||
| Setting | Description |
|
||||
|---------|-------------|
|
||||
| tagFile | Specifies the tag file to use. You can provide a custom tag database of your liking, but since the script was developed with Danbooru tags in mind, it might not work properly with other configurations.|
|
||||
| activeIn | Allows to selectively (de)activate the script for txt2img, img2img, and the negative prompts for both. |
|
||||
| maxResults | How many results to show max. For the default tag set, the results are ordered by occurence count. For embeddings and wildcards it will show all results in a scrollable list. |
|
||||
| resultStepLength | Allows to load results in smaller batches of the specified size for better performance in long lists or if showAllResults is true. |
|
||||
| delayTime | Specifies how much to wait in milliseconds before triggering autocomplete. Helps prevent too frequent updates while typing. |
|
||||
| showAllResults | If true, will ignore maxResults and show all results in a scrollable list. **Warning:** can lag your browser for long lists. |
|
||||
| replaceUnderscores | If true, undescores are replaced with spaces on clicking a tag. Might work better for some models. |
|
||||
| escapeParentheses | If true, escapes tags containing () so they don't contribute to the web UI's prompt weighting functionality. |
|
||||
| appendComma | Specifies the starting value of the "Append commas" UI switch. If UI options are disabled, this will always be used. |
|
||||
| useWildcards | Used to toggle the wildcard completion functionality. |
|
||||
| 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 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": {
|
||||
"-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"]
|
||||
}
|
||||
}
|
||||
```
|
||||
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).
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Temp files refresh -->
|
||||
<details>
|
||||
<summary>Refresh TAC temp files</summary>
|
||||
|
||||
### Aliases, Translations & Extra tags
|
||||
#### Aliases
|
||||
Like on Booru sites, tags can have one or multiple aliases which redirect to the actual value on completion. These will be searchable / shown according to the settings in `config.json`:
|
||||
- `searchByAlias` - Whether to also search for the alias or only the actual tag.
|
||||
- `onlyShowAlias` - Shows only the alias instead of `alias -> actual`. Only for displaying, the inserted text at the end is still the actual tag.
|
||||
This is a "fake" setting, meaning it doesn't actually configure anything. Rather, it is a small hack to abuse the refresh button developers can add to webui options. Clicking on the refresh button next to this setting will force tag autocomplete to recreate and reload some temporary internal files, which normally only happens on restarting the UI.
|
||||
|
||||
#### Translations
|
||||
Tag autocomplete depends on these files for various functionality, especially related to extra networks and wildcard completion. This setting can be used to rebuild the lists if you have, for example, added a few new LoRAs into the folder and don't want to restart the UI to get tag autocomplete to list them.
|
||||
|
||||
You can also add this to your quicksettings bar to have the refresh button available at all times.
|
||||
|
||||

|
||||
</details>
|
||||
<br/>
|
||||
|
||||
# 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 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 `Translation file uses old 3-column translation format instead of the new 2-column one` to support them. In that case, the second column will be unused and skipped during parsing.
|
||||
|
||||
Example with chinese translation:
|
||||
Example with Chinese translation:
|
||||
|
||||

|
||||

|
||||
|
||||
#### Extra file
|
||||
## List of translations
|
||||
- [🇨🇳 Chinese tags](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/discussions/23) by @HalfMAI, using machine translation and manual correction for the most common tags (uses legacy format)
|
||||
- [🇨🇳 Chinese tags](https://github.com/sgmklp/tag-for-autocompletion-with-translation) by @sgmklp, smaller set of manual translations based on https://github.com/zcyzcy88/TagTable
|
||||
|
||||
> ### 🫵 I need your help!
|
||||
> Translations are a community effort. If you have translated a tag file or want to create one, please open a Pull Request or Issue so your link can be added here.
|
||||
> Please make sure the quality is alright though, machine translation gets a lot of stuff wrong even for the most common tags.
|
||||
|
||||
## Live preview
|
||||
> ⚠️ Warning:
|
||||
>
|
||||
> This feature is still experimental, you might encounter some bugs when using it.
|
||||
|
||||
This will show a live preview of all detected tags in the prompt, both correctly separated by commas as well as in a longer sentence. It can detect up to three-word pairs in natural sentences, preferring perfect multi-word matches over single tags.
|
||||
|
||||
Above the detected tags will be their translation from the translation file, so if you aren't sure what the English tag means, you can easily find it there even after they have been inserted into the prompt (instead of just in the popup during completion).
|
||||
|
||||
The option defaults to off, but you can activate it by choosing a translation file and checking "Show live tag translation below prompt".
|
||||
It will not affect the normal functionality if it is off.
|
||||
|
||||
Example with Chinese translation:
|
||||
|
||||

|
||||
|
||||
Clicking on a detected tag will also select it in the prompt for quick editing.
|
||||
|
||||

|
||||
|
||||
#### ⚠️ Known issues with live translation:
|
||||
The translation updates when the user types or pastes text, but not if the action happens programmatically (e.g. applying a style or loading from PNG Info / Image Browser). This can be worked around by typing something manually after the programmatic edit.
|
||||
|
||||
# Extra file
|
||||
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.
|
||||
@@ -168,7 +426,7 @@ An example with the included (very basic) extra-quality-tags.csv file:
|
||||
|
||||
Whether the custom tags should be added before or after the normal tags can be chosen in the settings.
|
||||
|
||||
## CSV tag data
|
||||
# CSV tag data
|
||||
The script expects a CSV file with tags saved in the following way:
|
||||
```csv
|
||||
<name>,<type>,<postCount>,"<aliases>"
|
||||
@@ -207,3 +465,34 @@ or similarly for e621:
|
||||
|8 | Lore |
|
||||
|
||||
The tag type is used for coloring entries in the result list.
|
||||
|
||||
|
||||
## ⚠️ Common Problems & Known Issues:
|
||||
- Depending on your browser settings, sometimes an old version of the script can get cached. Try
|
||||
<kbd>CTRL</kbd> + <kbd>F5</kbd>
|
||||
to force-reload the site without cache if e.g. a new feature doesn't appear for you after an update.
|
||||
- If the prompt popup has broken styling for you or doesn't appear at all (like [this](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/7bbfdd54-fc23-4bfc-85af-24704b139b3a)), make sure to update your **openpose-editor** extension if you have it installed. It is known to cause issues with other extensions in older versions.
|
||||
|
||||
|
||||
<!-- Variable declarations for shorter main text -->
|
||||
[release-shield]: https://img.shields.io/github/v/release/DominikDoom/a1111-sd-webui-tagcomplete?logo=github&style=
|
||||
[release-url]: https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/DominikDoom/a1111-sd-webui-tagcomplete
|
||||
[contributors-url]: https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/graphs/contributors
|
||||
|
||||
[forks-shield]: https://img.shields.io/github/forks/DominikDoom/a1111-sd-webui-tagcomplete
|
||||
[forks-url]: https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/network/members
|
||||
|
||||
[stargazers-shield]: https://img.shields.io/github/stars/DominikDoom/a1111-sd-webui-tagcomplete
|
||||
[stargazers-url]: https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/stargazers
|
||||
|
||||
[issues-shield]: https://img.shields.io/github/issues/DominikDoom/a1111-sd-webui-tagcomplete
|
||||
[issues-url]: https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/issues/new/choose
|
||||
|
||||
<!-- Links for feature section -->
|
||||
[image-browser-url]: https://github.com/AlUlkesh/stable-diffusion-webui-images-browser
|
||||
[multidiffusion-url]: https://github.com/pkuliyi2015/multidiffusion-upscaler-for-automatic1111
|
||||
[tag-editor-url]: https://github.com/toshiaki1729/stable-diffusion-webui-dataset-tag-editor
|
||||
[wd-tagger-url]: https://github.com/toriato/stable-diffusion-webui-wd14-tagger
|
||||
[umi-url]: https://github.com/Klokinator/Umi-AI
|
||||
254
README_JA.md
Normal file
254
README_JA.md
Normal file
@@ -0,0 +1,254 @@
|
||||

|
||||
|
||||
# Booru tag autocompletion for A1111
|
||||
|
||||
[](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)
|
||||
|
||||
## [English Document](./README.md), [中文文档](./README_ZH.md)
|
||||
|
||||
このカスタムスクリプトは、Stable Diffusion向けの人気のweb UIである、[AUTOMATIC1111 web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui)の拡張機能として利用できます。
|
||||
|
||||
主にアニメ系イラストを閲覧するための掲示板「Danbooru」などで利用されているタグの自動補完ヒントを表示するための拡張機能となります。
|
||||
[Waifu Diffusion](https://github.com/harubaru/waifu-diffusion)など、この情報を使って学習させたStable Diffusionモデルもあるため、正確なタグをプロンプトに使用することで、構図を改善し、思い通りの画像が生成できるようになります。
|
||||
|
||||
web UIに内蔵されている利用可能な拡張機能リストを使用してインストールするか、[以下の説明](#インストール)に従ってファイルを手動でcloneするか、または[リリース](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)からパッケージ化されたバージョンを使用することができます。
|
||||
|
||||
## よく発生する問題 & 発見されている課題:
|
||||
- ブラウザの設定によっては、古いバージョンのスクリプトがキャッシュされていることがあります。アップデート後に新機能が表示されない場合などには、`CTRL+F5`でキャッシュを利用せずにサイトを強制的にリロードしてみてください。
|
||||
- プロンプトのポップアップのスタイルが崩れていたり、全く表示されない場合([このような場合](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/7bbfdd54-fc23-4bfc-85af-24704b139b3a))、openpose-editor拡張機能をインストールしている場合は、必ずアップデートしてください。古いバージョンでは、他の拡張機能との間で問題が発生することが知られています。
|
||||
|
||||
## スクリーンショット & デモ動画
|
||||
<details>
|
||||
<summary>クリックすると開きます</summary>
|
||||
基本的な使い方 (キーボード操作を用いたもの):
|
||||
|
||||
https://user-images.githubusercontent.com/34448969/200128020-10d9a8b2-cea6-4e3f-bcd2-8c40c8c73233.mp4
|
||||
|
||||
ワイルドカードをサポート:
|
||||
|
||||
https://user-images.githubusercontent.com/34448969/200128031-22dd7c33-71d1-464f-ae36-5f6c8fd49df0.mp4
|
||||
|
||||
タグカラーを含むDarkモードとLightモードに対応:
|
||||
|
||||

|
||||

|
||||
</details>
|
||||
|
||||
## インストール
|
||||
### 内蔵されている拡張機能リストを用いた方法
|
||||
1. Extensions タブを開く
|
||||
2. Available タブを開く
|
||||
3. "Load from:" をクリック
|
||||
4. リストの中から "Booru tag autocompletion" を探す
|
||||
- この拡張機能は最初から利用可能だったものなので、 "oldest first" を選択すると、リストの上位に表示されます。
|
||||
5. 右側にある "Install" をクリック
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
### 手動でcloneする方法
|
||||
```bash
|
||||
git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extensions/tag-autocomplete
|
||||
```
|
||||
(第2引数でフォルダ名を指定可能なので、好きな名前を指定しても良いでしょう)
|
||||
|
||||
## 追加で有効化できる補完機能
|
||||
### ワイルドカード
|
||||
|
||||
自動補完は、https://github.com/AUTOMATIC1111/stable-diffusion-webui-wildcards 、または他の類似のスクリプト/拡張機能で使用されるワイルドカードファイルでも利用可能です。補完は `__` (ダブルアンダースコア) と入力することで開始されます。最初にワイルドカードファイルのリストが表示され、1つを選択すると、そのファイル内の置換オプションが表示されます。
|
||||
これにより、スクリプトによって置換されるカテゴリを挿入するか、または直接1つを選択して、ワイルドカードをカテゴリ化されたカスタムタグシステムのようなものとして使用することができます。
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
ワイルドカードはすべての拡張機能フォルダと、古いバージョンをサポートするための `scripts/wildcards` フォルダで検索されます。これは複数の拡張機能からワイルドカードを組み合わせることができることを意味しています。ワイルドカードをグループ化した場合、ネストされたフォルダもサポートされます。
|
||||
|
||||
### Embeddings, Lora & Hypernets
|
||||
これら3つのタイプの補完は、`<`と入力することで行われます。デフォルトでは3つとも混在して表示されますが、以下の方法でさらにフィルタリングを行うことができます:
|
||||
- `<e:` は、embeddingsのみを表示します。
|
||||
- `<l:` 、または `<lora:` はLoraのみを表示します。
|
||||
- `<h:` 、または `<hypernet:` はHypernetworksのみを表示します
|
||||
|
||||
#### Embedding type filtering
|
||||
Stable Diffusion 1.xまたは2.xモデル用にそれぞれトレーニングされたembeddingsは、他のタイプとの互換性がありません。有効なembeddingsを見つけやすくするため、若干の色の違いも含めて「v1 Embedding」と「v2 Embedding」で分類しています。また、`<v1/2`または`<e:v1/2`に続けて実際の検索のためのキーワードを入力すると、v1またはv2embeddingsのみを含むように検索を絞り込むことができます。
|
||||
|
||||
例:
|
||||
|
||||

|
||||
|
||||
### Chants(詠唱)
|
||||
Chants(詠唱)は、より長いプロンプトプリセットです。この名前は、中国のユーザーによる初期のプロンプト集からヒントを得たもので、しばしば「呪文書」(原文は「Spellbook」「Codex」)などと呼ばれていました。
|
||||
このような文書から得られるプロンプトのスニペットは、このような理由から呪文や詠唱と呼ばれるにふさわしいものでした。
|
||||
|
||||
EmbeddingsやLoraと同様に、この機能は `<`, `<c:`, `<chant:` コマンドを入力することで発動します。例えば、プロンプトボックスに `<c:HighQuality` と入力して選択すると、次のようなプロンプトテキストが挿入されます:
|
||||
|
||||
```
|
||||
(masterpiece, best quality, high quality, highres, ultra-detailed),
|
||||
```
|
||||
|
||||
|
||||
Chants(詠唱)は、以下のフォーマットに従ってJSONファイルで追加することができます::
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Basic-NegativePrompt",
|
||||
"terms": "Negative,Low,Quality",
|
||||
"content": "(worst quality, low quality, normal quality)",
|
||||
"color": 3
|
||||
},
|
||||
{
|
||||
"name": "Basic-HighQuality",
|
||||
"terms": "Best,High,Quality",
|
||||
"content": "(masterpiece, best quality, high quality, highres, ultra-detailed)",
|
||||
"color": 1
|
||||
},
|
||||
{
|
||||
"name": "Basic-Start",
|
||||
"terms": "Basic, Start, Simple, Demo",
|
||||
"content": "(masterpiece, best quality, high quality, highres), 1girl, extremely beautiful detailed face, ...",
|
||||
"color": 5
|
||||
}
|
||||
]
|
||||
```
|
||||
このファイルが拡張機能の `tags` フォルダ内にある場合、settings内の"Chant file"ドロップダウンから選択することができます。
|
||||
|
||||
chantオブジェクトは4つのフィールドを持ちます:
|
||||
- `name` - 表示される名称
|
||||
- `terms` - 検索キーワード
|
||||
- `content` - 実際に挿入されるプロンプト
|
||||
- `color` - 表示される色。通常のタグと同じカテゴリーカラーシステムを使用しています。
|
||||
|
||||
### Umi AI tags
|
||||
https://github.com/Klokinator/Umi-AI は、Unprompted や Dynamic Wildcards に似た、機能豊富なワイルドカード拡張です。
|
||||
例えば `<[preset][--female][sfw][species]>` はプリセットカテゴリーを選び、女性関連のタグを除外し、さらに次のカテゴリーで絞り込み、実行時にこれらすべての条件に一致するランダムなフィルインを1つ選び出します。補完は `<[`] とそれに続く新しい開く括弧、例えば `<[xyz][`] で始まり、 `>` で閉じるまで続きます。
|
||||
|
||||
タグの自動補完は、これらのオプションをスマートに提案していきます。つまり、カテゴリータグの追加を続けても、その前に来たものと一致する結果だけが表示されるのです。
|
||||
また、タグの投稿数の代わりに、そのコンボから選択可能なフィルインタグの数を表示し、大規模になる初期内容に対して迅速な概要とフィルタリングを可能にします。
|
||||
|
||||
ほとんどの功績は[@ctwrs](https://github.com/ctwrs)によるものです。この方はUmiの開発者の一人として多くの貢献をしています。
|
||||
|
||||
## Settings
|
||||
|
||||
この拡張機能には、大量の設定&カスタマイズ性が組み込まれています:
|
||||
|
||||

|
||||
|
||||
| 設定項目 | 説明 |
|
||||
|---------|-------------|
|
||||
| tagFile | 使用するタグファイルを指定します。お好みのタグデータベースを用意することができますが、このスクリプトはDanbooruタグを想定して開発されているため、他の構成では正常に動作しない場合があります。|
|
||||
| activeIn | txt2img、img2img、またはその両方のネガティブプロンプトのスクリプトを有効、または無効にすることができます。 |
|
||||
| maxResults | 最大何件の結果を表示するか。デフォルトのタグセットでは、結果は出現回数順に表示されます。embeddingsとワイルドカードの場合は、スクロール可能なリストですべての結果を表示します。 |
|
||||
| resultStepLength | 長いリストやshowAllResultsがtrueの場合に、指定したサイズの小さなバッチで結果を読み込むことができるようにします。 |
|
||||
| delayTime | オートコンプリートを起動するまでの待ち時間をミリ秒単位で指定できます。これは入力中に頻繁に補完内容が更新されるのを防ぐのに役立ちます。 |
|
||||
| showAllResults | trueの場合、maxResultsを無視し、すべての結果をスクロール可能なリストで表示します。**警告:** 長いリストの場合、ブラウザが遅くなることがあります。 |
|
||||
| replaceUnderscores | trueにした場合、タグをクリックしたときに `_`(アンダースコア)が ` `(スペース)に置き換えられます。モデルによっては便利になるかもしれません。 |
|
||||
| escapeParentheses | trueの場合、()を含むタグをエスケープして、Web UIのプロンプトの重み付け機能に影響を与えないようにします。 |
|
||||
| appendComma | UIスイッチ "Append commas"の開始される値を指定することができます。UIのオプションが無効の場合、常にこの値が使用されます。 |
|
||||
| useWildcards | ワイルドカード補完機能の切り替えに使用します。 |
|
||||
| useEmbeddings | embedding補完機能の切り替えに使用します。 |
|
||||
| alias | エイリアスに関するオプションです。詳しくは下のセクションをご覧ください。 |
|
||||
| translation | 翻訳用のオプションです。詳しくは下のセクションをご覧ください。 |
|
||||
| extras | タグファイル/エイリアス/翻訳を追加するためのオプションです。詳しくは下記をご覧ください。 |
|
||||
| chantFile | chants(長いプロンプト・プリセット/ショートカット)に使用するためファイルです。 |
|
||||
| keymap | カスタマイズ可能なhotkeyを設定するために利用します。 |
|
||||
| colors | タグの色をカスタマイズできます。詳しくは下記をご覧ください。 |
|
||||
### Colors
|
||||
タグタイプに関する色は、タグ自動補完設定のためのJSONコードを変更することで指定することができます。
|
||||
フォーマットは標準的なJSONで、オブジェクト名は、それらが使用されるタグのファイル名(.csvを除く)に対応しています。
|
||||
角括弧の中の最初の値はダークモード、2番目の値はライトモードです。色の名称と16進数、どちらも使えるはずです。
|
||||
|
||||
```json
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
```
|
||||
また、カスタムタグファイルの新しいカラーセットを追加する際にも使用できます。
|
||||
数字はタグの種類を指定するもので、タグのソースに依存します。例として、[CSV tag data](#csv-tag-data)を参照してください。
|
||||
|
||||
### エイリアス, 翻訳 & Extra tagsについて
|
||||
#### エイリアス
|
||||
Booruのサイトのように、タグは1つまたは複数のエイリアスを持つことができ、完了時に実際の値へリダイレクトされて入力されます。これらは `config.json` の設定をもとに検索/表示されます:
|
||||
- `searchByAlias` - エイリアスも検索対象とするか、実際のタグのみを検索対象とするかを設定します
|
||||
- `onlyShowAlias` - `alias -> actual` の代わりに、エイリアスのみを表示します。表示のみで、最後に挿入されるテキストは実際のタグのままです。
|
||||
|
||||
#### 翻訳
|
||||
タグとエイリアスの両方を翻訳するために使用することができ、また翻訳による検索を可能にするための、追加のファイルを翻訳セクションに追加することができます。
|
||||
このファイルは、`<英語のタグ/エイリアス>,<翻訳>`という形式のCSVである必要がありますが、3列のフォーマットを使用する古いファイルとの後方互換性のために、`oldFormat`をオンにすると代わりにそれを使うことができます。
|
||||
|
||||
中国語の翻訳例:
|
||||
|
||||

|
||||

|
||||
|
||||
#### Extra file
|
||||
エクストラファイルは、メインセットに含まれない新しいタグやカスタムタグを追加するために使用されます。
|
||||
[CSV tag data](#csv-tag-data)にある通常のタグのフォーマットと同じですが、ひとつだけ例外があります:
|
||||
カスタムタグにはカウントがないため、3列目(0から数える場合は2列目)はタグの横に表示される灰色のメタテキストに使用されます。
|
||||
空欄のままだと、「カスタムタグ」と表示されます。
|
||||
|
||||
これは同梱されるextra-quality-tags.csvファイルを使用した例で、非常に基本的な内容となります:
|
||||
|
||||

|
||||
|
||||
カスタムタグを通常のタグの前に追加するか、後に追加するかは、設定で選択することができます。
|
||||
|
||||
## CSV tag data
|
||||
このスクリプトは、以下の方法で保存されたタグ付きCSVファイルを想定しています:
|
||||
```csv
|
||||
<name>,<type>,<postCount>,"<aliases>"
|
||||
```
|
||||
Example:
|
||||
```csv
|
||||
1girl,0,4114588,"1girls,sole_female"
|
||||
solo,0,3426446,"female_solo,solo_female"
|
||||
highres,5,3008413,"high_res,high_resolution,hires"
|
||||
long_hair,0,2898315,longhair
|
||||
commentary_request,5,2610959,
|
||||
```
|
||||
注目すべきは、最初の行にカラム名を記載していないことと、count と aliases の両方が技術的にはオプションであることです、
|
||||
ただし、countは常にデフォルトデータに含まれています。複数のエイリアスは同様にカンマで区切る必要がありますが、CSVの解析に支障がないようにダブルクオーテーションで囲みます。
|
||||
|
||||
番号の付け方についてはDanbooruの[tag API docs](https://danbooru.donmai.us/wiki_pages/api%3Atags)を参照してください:
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
|0 | General |
|
||||
|1 | Artist |
|
||||
|3 | Copyright |
|
||||
|4 | Character |
|
||||
|5 | Meta |
|
||||
|
||||
また、e621についても同様です:
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
|-1 | Invalid |
|
||||
|0 | General |
|
||||
|1 | Artist |
|
||||
|3 | Copyright |
|
||||
|4 | Character |
|
||||
|5 | Species |
|
||||
|6 | Invalid |
|
||||
|7 | Meta |
|
||||
|8 | Lore |
|
||||
|
||||
タグの種類は、結果の一覧のエントリーの色付けに使用されます。
|
||||
@@ -3,7 +3,7 @@
|
||||
# Booru tag autocompletion for A1111
|
||||
|
||||
[](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)
|
||||
## [English Document](./README.md)
|
||||
## [English Document](./README.md), [日本語ドキュメント](./README_JA.md)
|
||||
|
||||
## 功能概述
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Core components
|
||||
var CFG = null;
|
||||
var TAC_CFG = null;
|
||||
var tagBasePath = "";
|
||||
|
||||
// Tag completion data loaded from files
|
||||
@@ -14,6 +14,7 @@ var embeddings = [];
|
||||
var hypernetworks = [];
|
||||
var loras = [];
|
||||
var lycos = [];
|
||||
var chants = [];
|
||||
|
||||
// Selected model info for black/whitelisting
|
||||
var currentModelHash = "";
|
||||
|
||||
@@ -10,7 +10,8 @@ const ResultType = Object.freeze({
|
||||
"yamlWildcard": 6,
|
||||
"hypernetwork": 7,
|
||||
"lora": 8,
|
||||
"lyco": 9
|
||||
"lyco": 9,
|
||||
"chant": 10
|
||||
});
|
||||
|
||||
// Class to hold result data and annotations to make it clearer to use
|
||||
|
||||
@@ -8,7 +8,8 @@ const core = [
|
||||
"#txt2img_prompt > label > textarea",
|
||||
"#img2img_prompt > label > textarea",
|
||||
"#txt2img_neg_prompt > label > textarea",
|
||||
"#img2img_neg_prompt > label > textarea"
|
||||
"#img2img_neg_prompt > label > textarea",
|
||||
".prompt > label > textarea"
|
||||
];
|
||||
|
||||
// Third party text area selectors
|
||||
@@ -22,24 +23,58 @@ const thirdParty = {
|
||||
"Edit Caption",
|
||||
"Edit Tags"
|
||||
]
|
||||
},
|
||||
"image browser": {
|
||||
"base": "#tab_image_browser",
|
||||
"hasIds": false,
|
||||
"selectors": [
|
||||
"Filename keyword search",
|
||||
"EXIF keyword search"
|
||||
]
|
||||
},
|
||||
"tab_tagger": {
|
||||
"base": "#tab_tagger",
|
||||
"hasIds": false,
|
||||
"selectors": [
|
||||
"Additional tags (split by comma)",
|
||||
"Exclude tags (split by comma)"
|
||||
]
|
||||
},
|
||||
"tiled-diffusion-t2i": {
|
||||
"base": "#txt2img_script_container",
|
||||
"hasIds": true,
|
||||
"onDemand": true,
|
||||
"selectors": [
|
||||
"[id^=MD-t2i][id$=prompt] textarea",
|
||||
"[id^=MD-t2i][id$=prompt] input[type='text']"
|
||||
]
|
||||
},
|
||||
"tiled-diffusion-i2i": {
|
||||
"base": "#img2img_script_container",
|
||||
"hasIds": true,
|
||||
"onDemand": true,
|
||||
"selectors": [
|
||||
"[id^=MD-i2i][id$=prompt] textarea",
|
||||
"[id^=MD-i2i][id$=prompt] input[type='text']"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function getTextAreas() {
|
||||
// First get all core text areas
|
||||
let textAreas = [...gradioApp().querySelectorAll(core.join(", "))];
|
||||
|
||||
|
||||
for (const [key, entry] of Object.entries(thirdParty)) {
|
||||
if (entry.hasIds) { // If the entry has proper ids, we can just select them
|
||||
textAreas = textAreas.concat([...gradioApp().querySelectorAll(entry.selectors.join(", "))]);
|
||||
} else { // Otherwise, we have to find the text areas by their adjacent labels
|
||||
let base = gradioApp().querySelector(entry.base);
|
||||
|
||||
|
||||
// Safety check
|
||||
if (!base) continue;
|
||||
|
||||
let allTextAreas = [...base.querySelectorAll("textarea")];
|
||||
|
||||
|
||||
let allTextAreas = [...base.querySelectorAll("textarea, input[type='text']")];
|
||||
|
||||
// Filter the text areas where the adjacent label matches one of the selectors
|
||||
let matchingTextAreas = allTextAreas.filter(ta => [...ta.parentElement.childNodes].some(x => entry.selectors.includes(x.innerText)));
|
||||
textAreas = textAreas.concat(matchingTextAreas);
|
||||
@@ -49,6 +84,55 @@ function getTextAreas() {
|
||||
return textAreas;
|
||||
}
|
||||
|
||||
function addOnDemandObservers(setupFunction) {
|
||||
for (const [key, entry] of Object.entries(thirdParty)) {
|
||||
if (!entry.onDemand) continue;
|
||||
|
||||
let base = gradioApp().querySelector(entry.base);
|
||||
if (!base) continue;
|
||||
|
||||
let accordions = [...base?.querySelectorAll(".gradio-accordion")];
|
||||
if (!accordions) continue;
|
||||
|
||||
accordions.forEach(acc => {
|
||||
let accObserver = new MutationObserver((mutationList, observer) => {
|
||||
for (const mutation of mutationList) {
|
||||
if (mutation.type === "childList") {
|
||||
let newChildren = mutation.addedNodes;
|
||||
if (!newChildren) {
|
||||
accObserver.disconnect();
|
||||
continue;
|
||||
}
|
||||
|
||||
newChildren.forEach(child => {
|
||||
if (child.classList.contains("gradio-accordion") || child.querySelector(".gradio-accordion")) {
|
||||
let newAccordions = [...child.querySelectorAll(".gradio-accordion")];
|
||||
newAccordions.forEach(nAcc => accObserver.observe(nAcc, { childList: true }));
|
||||
}
|
||||
});
|
||||
|
||||
if (entry.hasIds) { // If the entry has proper ids, we can just select them
|
||||
[...gradioApp().querySelectorAll(entry.selectors.join(", "))].forEach(x => setupFunction(x));
|
||||
} else { // Otherwise, we have to find the text areas by their adjacent labels
|
||||
let base = gradioApp().querySelector(entry.base);
|
||||
|
||||
// Safety check
|
||||
if (!base) continue;
|
||||
|
||||
let allTextAreas = [...base.querySelectorAll("textarea, input[type='text']")];
|
||||
|
||||
// Filter the text areas where the adjacent label matches one of the selectors
|
||||
let matchingTextAreas = allTextAreas.filter(ta => [...ta.parentElement.childNodes].some(x => entry.selectors.includes(x.innerText)));
|
||||
matchingTextAreas.forEach(x => setupFunction(x));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
accObserver.observe(acc, { childList: true });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const thirdPartyIdSet = new Set();
|
||||
// Get the identifier for the text area to differentiate between positive and negative
|
||||
function getTextAreaIdentifier(textArea) {
|
||||
|
||||
@@ -89,6 +89,14 @@ function difference(a, b) {
|
||||
)].reduce((acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), []);
|
||||
}
|
||||
|
||||
// Sliding window function to get possible combination groups of an array
|
||||
function toNgrams(inputArray, size) {
|
||||
return Array.from(
|
||||
{ length: inputArray.length - (size - 1) }, //get the appropriate length
|
||||
(_, index) => inputArray.slice(index, index + size) //create the windows
|
||||
);
|
||||
}
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
@@ -98,6 +106,41 @@ function escapeHTML(unsafeText) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// For black/whitelisting
|
||||
function updateModelName() {
|
||||
let sdm = gradioApp().querySelector("#setting_sd_model_checkpoint");
|
||||
let modelDropdown = sdm?.querySelector("input") || sdm?.querySelector("select");
|
||||
if (modelDropdown) {
|
||||
currentModelName = modelDropdown.value;
|
||||
} else {
|
||||
// Fallback for intermediate versions
|
||||
modelDropdown = sdm?.querySelector("span.single-select");
|
||||
currentModelName = modelDropdown?.textContent || "";
|
||||
}
|
||||
}
|
||||
|
||||
// From https://stackoverflow.com/a/61975440, how to detect JS value changes
|
||||
function observeElement(element, property, callback, delay = 0) {
|
||||
let elementPrototype = Object.getPrototypeOf(element);
|
||||
if (elementPrototype.hasOwnProperty(property)) {
|
||||
let descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
|
||||
Object.defineProperty(element, property, {
|
||||
get: function() {
|
||||
return descriptor.get.apply(this, arguments);
|
||||
},
|
||||
set: function () {
|
||||
let oldValue = this[property];
|
||||
descriptor.set.apply(this, arguments);
|
||||
let newValue = this[property];
|
||||
if (typeof callback == "function") {
|
||||
setTimeout(callback.bind(this, oldValue, newValue), delay);
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Queue calling function to process global queues
|
||||
async function processQueue(queue, context, ...args) {
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
|
||||
54
javascript/ext_chants.js
Normal file
54
javascript/ext_chants.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const CHANT_REGEX = /<(?!e:|h:|l:)[^,> ]*>?/g;
|
||||
const CHANT_TRIGGER = () => TAC_CFG.chantFile && TAC_CFG.chantFile !== "None" && tagword.match(CHANT_REGEX);
|
||||
|
||||
class ChantParser extends BaseTagParser {
|
||||
parse() {
|
||||
// Show Chant
|
||||
let tempResults = [];
|
||||
if (tagword !== "<" && tagword !== "<c:") {
|
||||
let searchTerm = tagword.replace("<chant:", "").replace("<c:", "").replace("<", "");
|
||||
let filterCondition = x => x.terms.toLowerCase().includes(searchTerm) || x.name.toLowerCase().includes(searchTerm);
|
||||
tempResults = chants.filter(x => filterCondition(x)); // Filter by tagword
|
||||
} else {
|
||||
tempResults = chants;
|
||||
}
|
||||
|
||||
// Add final results
|
||||
let finalResults = [];
|
||||
tempResults.forEach(t => {
|
||||
let result = new AutocompleteResult(t.content.trim(), ResultType.chant)
|
||||
result.meta = "Chant";
|
||||
result.aliases = t.name;
|
||||
result.category = t.color;
|
||||
finalResults.push(result);
|
||||
});
|
||||
|
||||
return finalResults;
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (TAC_CFG.chantFile && TAC_CFG.chantFile !== "None") {
|
||||
try {
|
||||
chants = await readFile(`${tagBasePath}/${TAC_CFG.chantFile}?`, true);
|
||||
} catch (e) {
|
||||
console.error("Error loading chants.json: " + e);
|
||||
}
|
||||
} else {
|
||||
chants = [];
|
||||
}
|
||||
}
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
if (tagType === ResultType.chant) {
|
||||
return text.replace(/^.*?: /g, "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
PARSERS.push(new ChantParser(CHANT_TRIGGER));
|
||||
|
||||
// Add our utility functions to their respective queues
|
||||
QUEUE_FILE_LOAD.push(load);
|
||||
QUEUE_SANITIZE.push(sanitize);
|
||||
QUEUE_AFTER_CONFIG_CHANGE.push(load);
|
||||
@@ -1,5 +1,5 @@
|
||||
const EMB_REGEX = /<(?!l:|h:)[^,> ]*>?/g;
|
||||
const EMB_TRIGGER = () => CFG.useEmbeddings && tagword.match(EMB_REGEX);
|
||||
const EMB_REGEX = /<(?!l:|h:|c:)[^,> ]*>?/g;
|
||||
const EMB_TRIGGER = () => TAC_CFG.useEmbeddings && tagword.match(EMB_REGEX);
|
||||
|
||||
class EmbeddingParser extends BaseTagParser {
|
||||
parse() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const HYP_REGEX = /<(?!e:|l:)[^,> ]*>?/g;
|
||||
const HYP_TRIGGER = () => CFG.useHypernetworks && tagword.match(HYP_REGEX);
|
||||
const HYP_REGEX = /<(?!e:|l:|c:)[^,> ]*>?/g;
|
||||
const HYP_TRIGGER = () => TAC_CFG.useHypernetworks && tagword.match(HYP_REGEX);
|
||||
|
||||
class HypernetParser extends BaseTagParser {
|
||||
parse() {
|
||||
@@ -39,7 +39,7 @@ async function load() {
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
if (tagType === ResultType.hypernetwork) {
|
||||
return `<hypernet:${text}:${CFG.extraNetworksDefaultMultiplier}>`;
|
||||
return `<hypernet:${text}:${TAC_CFG.extraNetworksDefaultMultiplier}>`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const LORA_REGEX = /<(?!e:|h:)[^,> ]*>?/g;
|
||||
const LORA_TRIGGER = () => CFG.useLoras && tagword.match(LORA_REGEX);
|
||||
const LORA_REGEX = /<(?!e:|h:|c:)[^,> ]*>?/g;
|
||||
const LORA_TRIGGER = () => TAC_CFG.useLoras && tagword.match(LORA_REGEX);
|
||||
|
||||
class LoraParser extends BaseTagParser {
|
||||
parse() {
|
||||
@@ -39,7 +39,7 @@ async function load() {
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
if (tagType === ResultType.lora) {
|
||||
return `<lora:${text}:${CFG.extraNetworksDefaultMultiplier}>`;
|
||||
return `<lora:${text}:${TAC_CFG.extraNetworksDefaultMultiplier}>`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const LYCO_REGEX = /<(?!e:|h:)[^,> ]*>?/g;
|
||||
const LYCO_TRIGGER = () => CFG.useLycos && tagword.match(LYCO_REGEX);
|
||||
const LYCO_REGEX = /<(?!e:|h:|c:)[^,> ]*>?/g;
|
||||
const LYCO_TRIGGER = () => TAC_CFG.useLycos && tagword.match(LYCO_REGEX);
|
||||
|
||||
class LycoParser extends BaseTagParser {
|
||||
parse() {
|
||||
@@ -39,7 +39,7 @@ async function load() {
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
if (tagType === ResultType.lyco) {
|
||||
return `<lyco:${text}:${CFG.extraNetworksDefaultMultiplier}>`;
|
||||
return `<lyco:${text}:${TAC_CFG.extraNetworksDefaultMultiplier}>`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const UMI_PROMPT_REGEX = /<[^\s]*?\[[^,<>]*[\]|]?>?/gi;
|
||||
const UMI_TAG_REGEX = /(?:\[|\||--)([^<>\[\]\-|]+)/gi;
|
||||
|
||||
const UMI_TRIGGER = () => CFG.useWildcards && [...tagword.matchAll(UMI_PROMPT_REGEX)].length > 0;
|
||||
const UMI_TRIGGER = () => TAC_CFG.useWildcards && [...tagword.matchAll(UMI_PROMPT_REGEX)].length > 0;
|
||||
|
||||
class UmiParser extends BaseTagParser {
|
||||
parse(textArea, prompt) {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
const WC_REGEX = /\b__([^,]+)__([^, ]*)\b/g;
|
||||
|
||||
// Trigger conditions
|
||||
const WC_TRIGGER = () => CFG.useWildcards && [...tagword.matchAll(WC_REGEX)].length > 0;
|
||||
const WC_FILE_TRIGGER = () => CFG.useWildcards && (tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__");
|
||||
const WC_TRIGGER = () => TAC_CFG.useWildcards && [...tagword.matchAll(WC_REGEX)].length > 0;
|
||||
const WC_FILE_TRIGGER = () => TAC_CFG.useWildcards && (tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__");
|
||||
|
||||
class WildcardParser extends BaseTagParser {
|
||||
async parse() {
|
||||
@@ -107,7 +107,7 @@ function keepOpenIfWildcard(tagType, sanitizedText, newPrompt, textArea) {
|
||||
if (tagType === ResultType.wildcardFile) {
|
||||
hideBlocked = true;
|
||||
autocomplete(textArea, newPrompt, sanitizedText);
|
||||
setTimeout(() => { hideBlocked = false; }, 100);
|
||||
setTimeout(() => { hideBlocked = false; }, 450);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
"--meta-text-color": ["#6b6f7b", "#a2a9b4"],
|
||||
"--embedding-v1-color": ["lightsteelblue", "#2b5797"],
|
||||
"--embedding-v2-color": ["skyblue", "#2d89ef"],
|
||||
"--live-translation-rt": ["whitesmoke", "#222"],
|
||||
"--live-translation-color-1": ["lightskyblue", "#2d89ef"],
|
||||
"--live-translation-color-2": ["palegoldenrod", "#eb5700"],
|
||||
"--live-translation-color-3": ["darkseagreen", "darkgreen"],
|
||||
}
|
||||
const browserVars = {
|
||||
"--results-overflow-y": {
|
||||
@@ -20,10 +24,6 @@ const autocompleteCSS = `
|
||||
#quicksettings [id^=setting_tac] {
|
||||
background-color: transparent;
|
||||
min-width: fit-content;
|
||||
align-self: center;
|
||||
}
|
||||
#quicksettings [id^=setting_tac] > label > span {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.autocompleteResults {
|
||||
position: absolute;
|
||||
@@ -78,6 +78,39 @@ const autocompleteCSS = `
|
||||
.acListItem.acEmbeddingV2 {
|
||||
color: var(--embedding-v2-color);
|
||||
}
|
||||
.acRuby {
|
||||
padding: var(--input-padding);
|
||||
color: #888;
|
||||
font-size: 0.8rem;
|
||||
user-select: none;
|
||||
}
|
||||
.acRuby > ruby {
|
||||
display: inline-flex;
|
||||
flex-direction: column-reverse;
|
||||
margin-top: 0.5rem;
|
||||
vertical-align: bottom;
|
||||
cursor: pointer;
|
||||
}
|
||||
.acRuby > ruby::hover {
|
||||
text-decoration: underline;
|
||||
text-shadow: 0 0 10px var(--live-translation-color-1);
|
||||
}
|
||||
.acRuby > :nth-child(3n+1) {
|
||||
color: var(--live-translation-color-1);
|
||||
}
|
||||
.acRuby > :nth-child(3n+2) {
|
||||
color: var(--live-translation-color-2);
|
||||
}
|
||||
.acRuby > :nth-child(3n+3) {
|
||||
color: var(--live-translation-color-3);
|
||||
}
|
||||
.acRuby > ruby > rt {
|
||||
line-height: 1rem;
|
||||
padding: 0px 5px 0px 0px;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
color: var(--live-translation-rt);
|
||||
}
|
||||
`;
|
||||
|
||||
async function loadTags(c) {
|
||||
@@ -90,9 +123,17 @@ async function loadTags(c) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await loadExtraTags(c);
|
||||
}
|
||||
|
||||
async function loadExtraTags(c) {
|
||||
if (c.extra.extraFile && c.extra.extraFile !== "None") {
|
||||
try {
|
||||
extras = await loadCSV(`${tagBasePath}/${c.extra.extraFile}`);
|
||||
// Add translations to the main translation map for extra tags that have them
|
||||
extras.forEach(e => {
|
||||
if (e[4]) translations.set(e[0], e[4]);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error loading extra file: " + e);
|
||||
return;
|
||||
@@ -141,7 +182,7 @@ async function syncOptions() {
|
||||
useEmbeddings: opts["tac_useEmbeddings"],
|
||||
useHypernetworks: opts["tac_useHypernetworks"],
|
||||
useLoras: opts["tac_useLoras"],
|
||||
useLycos: opts["tac_useLycos"],
|
||||
useLycos: opts["tac_useLycos"],
|
||||
showWikiLinks: opts["tac_showWikiLinks"],
|
||||
// Insertion related settings
|
||||
replaceUnderscores: opts["tac_replaceUnderscores"],
|
||||
@@ -157,12 +198,15 @@ async function syncOptions() {
|
||||
translationFile: opts["tac_translation.translationFile"],
|
||||
oldFormat: opts["tac_translation.oldFormat"],
|
||||
searchByTranslation: opts["tac_translation.searchByTranslation"],
|
||||
liveTranslation: opts["tac_translation.liveTranslation"],
|
||||
},
|
||||
// Extra file settings
|
||||
extra: {
|
||||
extraFile: opts["tac_extra.extraFile"],
|
||||
addMode: opts["tac_extra.addMode"]
|
||||
},
|
||||
// Chant file settings
|
||||
chantFile: opts["tac_chantFile"],
|
||||
// Settings not from tac but still used by the script
|
||||
extraNetworksDefaultMultiplier: opts["extra_networks_default_multiplier"],
|
||||
extraNetworksSeparator: opts["extra_networks_add_text_separator"],
|
||||
@@ -170,30 +214,39 @@ async function syncOptions() {
|
||||
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
|
||||
}
|
||||
|
||||
// Reload tags if the tag file changed
|
||||
if (!CFG || newCFG.tagFile !== CFG.tagFile || newCFG.extra.extraFile !== CFG.extra.extraFile) {
|
||||
// Reload translations if the translation file changed
|
||||
if (!TAC_CFG || newCFG.translation.translationFile !== TAC_CFG.translation.translationFile) {
|
||||
translations.clear();
|
||||
await loadTranslations(newCFG);
|
||||
await loadExtraTags(newCFG);
|
||||
}
|
||||
// Reload tags if the tag file changed (after translations so extra tag translations get re-added)
|
||||
if (!TAC_CFG || newCFG.tagFile !== TAC_CFG.tagFile || newCFG.extra.extraFile !== TAC_CFG.extra.extraFile) {
|
||||
allTags = [];
|
||||
await loadTags(newCFG);
|
||||
}
|
||||
// Reload translations if the translation file changed
|
||||
if (!CFG || newCFG.translation.translationFile !== CFG.translation.translationFile) {
|
||||
translations.clear();
|
||||
await loadTranslations(newCFG);
|
||||
}
|
||||
|
||||
// Update CSS if maxResults changed
|
||||
if (CFG && newCFG.maxResults !== CFG.maxResults) {
|
||||
if (TAC_CFG && newCFG.maxResults !== TAC_CFG.maxResults) {
|
||||
gradioApp().querySelectorAll(".autocompleteResults").forEach(r => {
|
||||
r.style.maxHeight = `${newCFG.maxResults * 50}px`;
|
||||
});
|
||||
}
|
||||
|
||||
// Remove ruby div if live preview was disabled
|
||||
if (newCFG.translation.liveTranslation === false) {
|
||||
[...gradioApp().querySelectorAll('.acRuby')].forEach(r => {
|
||||
r.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Apply changes
|
||||
CFG = newCFG;
|
||||
TAC_CFG = newCFG;
|
||||
|
||||
// Callback
|
||||
await processQueue(QUEUE_AFTER_CONFIG_CHANGE, null);
|
||||
@@ -207,7 +260,7 @@ function createResultsDiv(textArea) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let typeClass = textAreaId.replaceAll(".", " ");
|
||||
|
||||
resultsDiv.style.maxHeight = `${CFG.maxResults * 50}px`;
|
||||
resultsDiv.style.maxHeight = `${TAC_CFG.maxResults * 50}px`;
|
||||
resultsDiv.setAttribute("class", `autocompleteResults ${typeClass} notranslate`);
|
||||
resultsDiv.setAttribute("translate", "no");
|
||||
resultsList.setAttribute("class", "autocompleteResultsList");
|
||||
@@ -227,7 +280,7 @@ function showResults(textArea) {
|
||||
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
|
||||
resultsDiv.style.display = "block";
|
||||
|
||||
if (CFG.slidingPopup) {
|
||||
if (TAC_CFG.slidingPopup) {
|
||||
let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd).left;
|
||||
let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - resultsDiv.offsetWidth);
|
||||
|
||||
@@ -236,6 +289,8 @@ function showResults(textArea) {
|
||||
if (resultsDiv.style.left)
|
||||
resultsDiv.style.removeProperty("left");
|
||||
}
|
||||
// Reset here too to make absolutely sure the browser registers it
|
||||
resultsDiv.scrollTop = 0;
|
||||
}
|
||||
function hideResults(textArea) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
@@ -249,18 +304,18 @@ function hideResults(textArea) {
|
||||
|
||||
// Function to check activation criteria
|
||||
function isEnabled() {
|
||||
if (CFG.activeIn.global) {
|
||||
if (TAC_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
|
||||
let modelList = TAC_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 (TAC_CFG.activeIn.modelListMode.toLowerCase() === "blacklist") {
|
||||
// If the current model is in the blacklist, disable
|
||||
return modelList.filter(x => x === currentModelName || x === modelNameWithoutHash || x === currentModelHash || x === shortHash).length === 0;
|
||||
} else {
|
||||
@@ -277,6 +332,7 @@ 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 RUBY_TAG_REGEX = /[\w\d<][\w\d' \-?!/$%]{2,}>?/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
|
||||
@@ -293,9 +349,9 @@ async function insertTextAtCursor(textArea, result, tagword) {
|
||||
if (sanitizeResults && sanitizeResults.length > 0) {
|
||||
sanitizedText = sanitizeResults[0];
|
||||
} else {
|
||||
sanitizedText = CFG.replaceUnderscores ? text.replaceAll("_", " ") : text;
|
||||
sanitizedText = TAC_CFG.replaceUnderscores ? text.replaceAll("_", " ") : text;
|
||||
|
||||
if (CFG.escapeParentheses && tagType === ResultType.tag) {
|
||||
if (TAC_CFG.escapeParentheses && tagType === ResultType.tag) {
|
||||
sanitizedText = sanitizedText
|
||||
.replaceAll("(", "\\(")
|
||||
.replaceAll(")", "\\)")
|
||||
@@ -316,11 +372,11 @@ async function insertTextAtCursor(textArea, result, tagword) {
|
||||
var optionalSeparator = "";
|
||||
let extraNetworkTypes = [ResultType.hypernetwork, ResultType.lora];
|
||||
let noCommaTypes = [ResultType.wildcardFile, ResultType.yamlWildcard].concat(extraNetworkTypes);
|
||||
if (CFG.appendComma && !noCommaTypes.includes(tagType)) {
|
||||
if (TAC_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 || " ";
|
||||
optionalSeparator = TAC_CFG.extraNetworksSeparator || " ";
|
||||
}
|
||||
|
||||
// Replace partial tag word with new text, add comma if needed
|
||||
@@ -367,15 +423,16 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
if (resetList) {
|
||||
resultsList.innerHTML = "";
|
||||
selectedTag = null;
|
||||
oldSelectedTag = null;
|
||||
resultDiv.scrollTop = 0;
|
||||
resultCount = 0;
|
||||
}
|
||||
|
||||
// Find right colors from config
|
||||
let tagFileName = CFG.tagFile.split(".")[0];
|
||||
let tagColors = CFG.colorMap;
|
||||
let mode = gradioApp().querySelector('.dark') ? 0 : 1;
|
||||
let nextLength = Math.min(results.length, resultCount + CFG.resultStepLength);
|
||||
let tagFileName = TAC_CFG.tagFile.split(".")[0];
|
||||
let tagColors = TAC_CFG.colorMap;
|
||||
let mode = (document.querySelector(".dark") || gradioApp().querySelector(".dark")) ? 0 : 1;
|
||||
let nextLength = Math.min(results.length, resultCount + TAC_CFG.resultStepLength);
|
||||
|
||||
for (let i = resultCount; i < nextLength; i++) {
|
||||
let result = results[i];
|
||||
@@ -395,7 +452,9 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
|
||||
let displayText = "";
|
||||
// If the tag matches the tagword, we don't need to display the alias
|
||||
if (result.aliases && !result.text.includes(tagword)) { // Alias
|
||||
if(result.type === ResultType.chant) {
|
||||
displayText = escapeHTML(result.aliases);
|
||||
} else if (result.aliases && !result.text.includes(tagword)) { // Alias
|
||||
let splitAliases = result.aliases.split(",");
|
||||
let bestAlias = splitAliases.find(a => a.toLowerCase().includes(tagword));
|
||||
|
||||
@@ -416,7 +475,7 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
if (translations.has(bestAlias) && translations.get(bestAlias) !== bestAlias && bestAlias !== result.text)
|
||||
displayText += `[${translations.get(bestAlias)}]`;
|
||||
|
||||
if (!CFG.alias.onlyShowAlias && result.text !== bestAlias)
|
||||
if (!TAC_CFG.alias.onlyShowAlias && result.text !== bestAlias)
|
||||
displayText += " ➝ " + result.text;
|
||||
} else { // No alias
|
||||
displayText = escapeHTML(result.text);
|
||||
@@ -430,7 +489,7 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
itemText.innerHTML = displayText.replace(tagword, `<b>${tagword}</b>`);
|
||||
|
||||
// Add wiki link if the setting is enabled and a supported tag set loaded
|
||||
if (CFG.showWikiLinks
|
||||
if (TAC_CFG.showWikiLinks
|
||||
&& (result.type === ResultType.tag)
|
||||
&& (tagFileName.toLowerCase().startsWith("danbooru") || tagFileName.toLowerCase().startsWith("e621"))) {
|
||||
let wikiLink = document.createElement("a");
|
||||
@@ -513,8 +572,11 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
}
|
||||
resultCount = nextLength;
|
||||
|
||||
if (resetList)
|
||||
if (resetList) {
|
||||
selectedTag = null;
|
||||
oldSelectedTag = null;
|
||||
resultDiv.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelectionStyle(textArea, newIndex, oldIndex) {
|
||||
@@ -533,12 +595,117 @@ function updateSelectionStyle(textArea, newIndex, oldIndex) {
|
||||
}
|
||||
|
||||
// Set scrolltop to selected item if we are showing more than max results
|
||||
if (items.length > CFG.maxResults) {
|
||||
if (items.length > TAC_CFG.maxResults) {
|
||||
let selected = items[newIndex];
|
||||
resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRuby(textArea, prompt) {
|
||||
if (!TAC_CFG.translation.liveTranslation) return;
|
||||
if (!TAC_CFG.translation.translationFile || TAC_CFG.translation.translationFile === "None") return;
|
||||
|
||||
let ruby = gradioApp().querySelector('.acRuby' + getTextAreaIdentifier(textArea));
|
||||
if (!ruby) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let typeClass = textAreaId.replaceAll(".", " ");
|
||||
ruby = document.createElement("div");
|
||||
ruby.setAttribute("class", `acRuby${typeClass} notranslate`);
|
||||
textArea.parentNode.appendChild(ruby);
|
||||
}
|
||||
|
||||
ruby.innerText = prompt;
|
||||
|
||||
let bracketEscapedPrompt = prompt.replaceAll("\\(", "$").replaceAll("\\)", "%");
|
||||
|
||||
let rubyTags = bracketEscapedPrompt.match(RUBY_TAG_REGEX);
|
||||
if (!rubyTags) return;
|
||||
|
||||
rubyTags.sort((a, b) => b.length - a.length);
|
||||
rubyTags = new Set(rubyTags);
|
||||
|
||||
const prepareTag = (tag) => {
|
||||
tag = tag.replaceAll("$", "\\(").replaceAll("%", "\\)");
|
||||
|
||||
let unsanitizedTag = tag
|
||||
.replaceAll(" ", "_")
|
||||
.replaceAll("\\(", "(")
|
||||
.replaceAll("\\)", ")");
|
||||
|
||||
const translation = translations?.get(tag) || translations?.get(unsanitizedTag);
|
||||
|
||||
let escapedTag = escapeRegExp(tag);
|
||||
return { tag, escapedTag, translation };
|
||||
}
|
||||
|
||||
const replaceOccurences = (text, tuple) => {
|
||||
let { tag, escapedTag, translation } = tuple;
|
||||
let searchRegex = new RegExp(`(?<!<ruby>)(?:\\b)${escapedTag}(?:\\b|$|(?=[,|: \\t\\n\\r]))(?!<rt>)`, "g");
|
||||
return text.replaceAll(searchRegex, `<ruby>${escapeHTML(tag)}<rt>${translation}</rt></ruby>`);
|
||||
}
|
||||
|
||||
let html = escapeHTML(prompt);
|
||||
|
||||
// First try to find direct matches
|
||||
[...rubyTags].forEach(tag => {
|
||||
let tuple = prepareTag(tag);
|
||||
|
||||
if (tuple.translation) {
|
||||
html = replaceOccurences(html, tuple);
|
||||
} else {
|
||||
let subTags = tuple.tag.split(" ").filter(x => x.trim().length > 0);
|
||||
// Return if there is only one word
|
||||
if (subTags.length === 1) return;
|
||||
|
||||
let subHtml = tag.replaceAll("$", "\\(").replaceAll("%", "\\)");
|
||||
|
||||
let translateNgram = (windows) => {
|
||||
windows.forEach(window => {
|
||||
let combinedTag = window.join(" ");
|
||||
let subTuple = prepareTag(combinedTag);
|
||||
|
||||
if (subTuple.tag.length <= 2) return;
|
||||
|
||||
if (subTuple.translation) {
|
||||
subHtml = replaceOccurences(subHtml, subTuple);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Perform n-gram sliding window search
|
||||
translateNgram(toNgrams(subTags, 3));
|
||||
translateNgram(toNgrams(subTags, 2));
|
||||
translateNgram(toNgrams(subTags, 1));
|
||||
|
||||
let escapedTag = escapeRegExp(tuple.tag);
|
||||
|
||||
let searchRegex = new RegExp(`(?<!<ruby>)(?:\\b)${escapedTag}(?:\\b|$|(?=[,|: \\t\\n\\r]))(?!<rt>)`, "g");
|
||||
html = html.replaceAll(searchRegex, subHtml);
|
||||
}
|
||||
});
|
||||
|
||||
ruby.innerHTML = html;
|
||||
|
||||
// Add listeners for auto selection
|
||||
const childNodes = [...ruby.childNodes];
|
||||
[...ruby.children].forEach(child => {
|
||||
const textBefore = childNodes.slice(0, childNodes.indexOf(child)).map(x => x.childNodes[0]?.textContent || x.textContent).join("")
|
||||
child.onclick = () => rubyTagClicked(child, textBefore, prompt, textArea);
|
||||
});
|
||||
}
|
||||
|
||||
function rubyTagClicked(node, textBefore, prompt, textArea) {
|
||||
let selectionText = node.childNodes[0].textContent;
|
||||
|
||||
// Find start and end position of the tag in the prompt
|
||||
let startPos = prompt.indexOf(textBefore) + textBefore.length;
|
||||
let endPos = startPos + selectionText.length;
|
||||
|
||||
// Select in text area
|
||||
textArea.focus();
|
||||
textArea.setSelectionRange(startPos, endPos);
|
||||
}
|
||||
|
||||
async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
// Return if the function is deactivated in the UI
|
||||
if (!isEnabled()) return;
|
||||
@@ -604,7 +771,11 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
// instead of having them added in the order of the parsers
|
||||
let shouldSort = resultCandidates.length > 1;
|
||||
if (shouldSort) {
|
||||
results = results.sort((a, b) => a.text.localeCompare(b.text));
|
||||
results = results.sort((a, b) => {
|
||||
let sortByA = a.type === ResultType.chant ? a.aliases : a.text;
|
||||
let sortByB = b.type === ResultType.chant ? b.aliases : b.text;
|
||||
return sortByA.localeCompare(sortByB);
|
||||
});
|
||||
|
||||
// Since some tags are kaomoji, we have to add the normal results in some cases
|
||||
if (tagword.startsWith("<") || tagword.startsWith("*<")) {
|
||||
@@ -616,7 +787,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
} else {
|
||||
searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
|
||||
}
|
||||
let genericResults = allTags.filter(x => x[0].toLowerCase().search(searchRegex) > -1).slice(0, CFG.maxResults);
|
||||
let genericResults = allTags.filter(x => x[0].toLowerCase().search(searchRegex) > -1).slice(0, TAC_CFG.maxResults);
|
||||
|
||||
genericResults.forEach(g => {
|
||||
let result = new AutocompleteResult(g[0].trim(), ResultType.tag)
|
||||
@@ -644,11 +815,11 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
|| 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)
|
||||
if (TAC_CFG.alias.searchByAlias && TAC_CFG.translation.searchByTranslation)
|
||||
fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x);
|
||||
else if (CFG.alias.searchByAlias && !CFG.translation.searchByTranslation)
|
||||
else if (TAC_CFG.alias.searchByAlias && !TAC_CFG.translation.searchByTranslation)
|
||||
fil = (x) => baseFilter(x) || aliasFilter(x);
|
||||
else if (CFG.translation.searchByTranslation && !CFG.alias.searchByAlias)
|
||||
else if (TAC_CFG.translation.searchByTranslation && !TAC_CFG.alias.searchByAlias)
|
||||
fil = (x) => baseFilter(x) || translationFilter(x);
|
||||
else
|
||||
fil = (x) => baseFilter(x);
|
||||
@@ -663,7 +834,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
});
|
||||
|
||||
// Add extras
|
||||
if (CFG.extra.extraFile) {
|
||||
if (TAC_CFG.extra.extraFile) {
|
||||
let extraResults = [];
|
||||
|
||||
extras.filter(fil).forEach(e => {
|
||||
@@ -674,7 +845,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
extraResults.push(result);
|
||||
});
|
||||
|
||||
if (CFG.extra.addMode === "Insert before") {
|
||||
if (TAC_CFG.extra.addMode === "Insert before") {
|
||||
results = extraResults.concat(results);
|
||||
} else {
|
||||
results = results.concat(extraResults);
|
||||
@@ -682,8 +853,8 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
}
|
||||
|
||||
// Slice if the user has set a max result count
|
||||
if (!CFG.showAllResults) {
|
||||
results = results.slice(0, CFG.maxResults);
|
||||
if (!TAC_CFG.showAllResults) {
|
||||
results = results.slice(0, TAC_CFG.maxResults);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,7 +873,7 @@ 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;
|
||||
let keys = TAC_CFG.keymap;
|
||||
|
||||
// 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)) {
|
||||
@@ -774,8 +945,8 @@ function navigateInList(textArea, event) {
|
||||
hideResults(textArea);
|
||||
break;
|
||||
}
|
||||
if (selectedTag === resultCount - 1
|
||||
&& (event.key === keys["MoveUp"] || event.key === keys["MoveDown"] || event.key === keys["JumpToStart"] || event.key === keys["JumpToEnd"])) {
|
||||
let moveKeys = [keys["MoveUp"], keys["MoveDown"], keys["JumpUp"], keys["JumpDown"], keys["JumpToStart"], keys["JumpToEnd"]];
|
||||
if (selectedTag === resultCount - 1 && moveKeys.includes(event.key)) {
|
||||
addResultsToList(textArea, results, tagword, false);
|
||||
}
|
||||
// Update highlighting
|
||||
@@ -787,6 +958,63 @@ function navigateInList(textArea, event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
async function refreshTacTempFiles() {
|
||||
setTimeout(async () => {
|
||||
wildcardFiles = [];
|
||||
wildcardExtFiles = [];
|
||||
yamlWildcards = [];
|
||||
embeddings = [];
|
||||
hypernetworks = [];
|
||||
loras = [];
|
||||
lycos = [];
|
||||
await processQueue(QUEUE_FILE_LOAD, null);
|
||||
|
||||
console.log("TAC: Refreshed temp files");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function addAutocompleteToArea(area) {
|
||||
// Return if autocomplete is disabled for the current area type in config
|
||||
let textAreaId = getTextAreaIdentifier(area);
|
||||
if ((!TAC_CFG.activeIn.img2img && textAreaId.includes("img2img"))
|
||||
|| (!TAC_CFG.activeIn.txt2img && textAreaId.includes("txt2img"))
|
||||
|| (!TAC_CFG.activeIn.negativePrompts && textAreaId.includes("n"))
|
||||
|| (!TAC_CFG.activeIn.thirdParty && textAreaId.includes("thirdParty"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add listeners once
|
||||
if (!area.classList.contains('autocomplete')) {
|
||||
// Add our new element
|
||||
var resultsDiv = createResultsDiv(area);
|
||||
area.parentNode.insertBefore(resultsDiv, area.nextSibling);
|
||||
// Hide by default so it doesn't show up on page load
|
||||
hideResults(area);
|
||||
|
||||
// Add autocomplete event listener
|
||||
area.addEventListener('input', () => {
|
||||
debounce(autocomplete(area, area.value), TAC_CFG.delayTime);
|
||||
updateRuby(area, area.value);
|
||||
});
|
||||
// Add focusout event listener
|
||||
area.addEventListener('focusout', debounce(() => {
|
||||
if (!hideBlocked)
|
||||
hideResults(area);
|
||||
}, 400));
|
||||
// Add up and down arrow event listener
|
||||
area.addEventListener('keydown', (e) => navigateInList(area, e));
|
||||
// CompositionEnd fires after the user has finished IME composing
|
||||
// We need to block hide here to prevent the enter key from insta-closing the results
|
||||
area.addEventListener('compositionend', () => {
|
||||
hideBlocked = true;
|
||||
setTimeout(() => { hideBlocked = false; }, 100);
|
||||
});
|
||||
|
||||
// Add class so we know we've already added the listeners
|
||||
area.classList.add('autocomplete');
|
||||
}
|
||||
}
|
||||
|
||||
// One-time setup, triggered from onUiUpdate
|
||||
async function setup() {
|
||||
// Load external files needed by completion extensions
|
||||
@@ -795,6 +1023,9 @@ async function setup() {
|
||||
// Find all textareas
|
||||
let textAreas = getTextAreas();
|
||||
|
||||
// Add mutation observer to accordions inside a base that has onDemand set to true
|
||||
addOnDemandObservers(addAutocompleteToArea);
|
||||
|
||||
// Add event listener to apply settings button so we can mirror the changes to our internal config
|
||||
let applySettingsButton = gradioApp().querySelector("#tab_settings #settings_submit") || gradioApp().querySelector("#tab_settings > div > .gr-button-primary");
|
||||
applySettingsButton?.addEventListener("click", () => {
|
||||
@@ -805,7 +1036,7 @@ async function setup() {
|
||||
});
|
||||
// Add change listener to our quicksettings to change our internal config without the apply button for them
|
||||
let quicksettings = gradioApp().querySelector('#quicksettings');
|
||||
let commonQueryPart = "[id^=setting_tac] > label >";
|
||||
let commonQueryPart = "[id^=setting_tac] > label";
|
||||
quicksettings?.querySelectorAll(`${commonQueryPart} input, ${commonQueryPart} textarea, ${commonQueryPart} select`).forEach(e => {
|
||||
e.addEventListener("change", () => {
|
||||
setTimeout(async () => {
|
||||
@@ -813,23 +1044,26 @@ async function setup() {
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
quicksettings?.querySelectorAll(`[id^=setting_tac].gradio-dropdown input`).forEach(e => {
|
||||
observeElement(e, "value", () => {
|
||||
setTimeout(async () => {
|
||||
await syncOptions();
|
||||
}, 500);
|
||||
})
|
||||
});
|
||||
// Listener for internal temp files refresh button
|
||||
gradioApp().querySelector("#refresh_tac_refreshTempFiles")?.addEventListener("click", refreshTacTempFiles);
|
||||
|
||||
// Add mutation observer for the model hash text to also allow hash-based blacklist again
|
||||
let modelHashText = gradioApp().querySelector("#sd_checkpoint_hash");
|
||||
updateModelName();
|
||||
if (modelHashText) {
|
||||
currentModelHash = modelHashText.title
|
||||
let modelHashObserver = new MutationObserver((mutationList, observer) => {
|
||||
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;
|
||||
}
|
||||
updateModelName();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -840,63 +1074,28 @@ async function setup() {
|
||||
if (textAreas.every(v => v === null || v === undefined)) return;
|
||||
// Already added or unnecessary to add
|
||||
if (gradioApp().querySelector('.autocompleteResults.p')) {
|
||||
if (gradioApp().querySelector('.autocompleteResults.n') || !CFG.activeIn.negativePrompts) {
|
||||
if (gradioApp().querySelector('.autocompleteResults.n') || !TAC_CFG.activeIn.negativePrompts) {
|
||||
return;
|
||||
}
|
||||
} else if (!CFG.activeIn.txt2img && !CFG.activeIn.img2img) {
|
||||
} else if (!TAC_CFG.activeIn.txt2img && !TAC_CFG.activeIn.img2img) {
|
||||
return;
|
||||
}
|
||||
|
||||
textAreas.forEach(area => {
|
||||
// Return if autocomplete is disabled for the current area type in config
|
||||
let textAreaId = getTextAreaIdentifier(area);
|
||||
if ((!CFG.activeIn.img2img && textAreaId.includes("img2img"))
|
||||
|| (!CFG.activeIn.txt2img && textAreaId.includes("txt2img"))
|
||||
|| (!CFG.activeIn.negativePrompts && textAreaId.includes("n"))
|
||||
|| (!CFG.activeIn.thirdParty && textAreaId.includes("thirdParty"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add listeners once
|
||||
if (!area.classList.contains('autocomplete')) {
|
||||
// Add our new element
|
||||
var resultsDiv = createResultsDiv(area);
|
||||
area.parentNode.insertBefore(resultsDiv, area.nextSibling);
|
||||
// Hide by default so it doesn't show up on page load
|
||||
hideResults(area);
|
||||
|
||||
// Add autocomplete event listener
|
||||
area.addEventListener('input', debounce(() => autocomplete(area, area.value), CFG.delayTime));
|
||||
// Add focusout event listener
|
||||
area.addEventListener('focusout', debounce(() => hideResults(area), 400));
|
||||
// Add up and down arrow event listener
|
||||
area.addEventListener('keydown', (e) => navigateInList(area, e));
|
||||
// CompositionEnd fires after the user has finished IME composing
|
||||
// We need to block hide here to prevent the enter key from insta-closing the results
|
||||
area.addEventListener('compositionend', () => {
|
||||
hideBlocked = true;
|
||||
setTimeout(() => { hideBlocked = false; }, 100);
|
||||
});
|
||||
|
||||
// Add class so we know we've already added the listeners
|
||||
area.classList.add('autocomplete');
|
||||
}
|
||||
});
|
||||
textAreas.forEach(area => addAutocompleteToArea(area));
|
||||
|
||||
// Add style to dom
|
||||
let acStyle = document.createElement('style');
|
||||
//let css = gradioApp().querySelector('.dark') ? autocompleteCSS_dark : autocompleteCSS_light;
|
||||
let mode = gradioApp().querySelector('.dark') ? 0 : 1;
|
||||
let mode = (document.querySelector(".dark") || gradioApp().querySelector(".dark")) ? 0 : 1;
|
||||
// Check if we are on webkit
|
||||
let browser = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 ? "firefox" : "other";
|
||||
|
||||
let css = autocompleteCSS;
|
||||
// Replace vars with actual values (can't use actual css vars because of the way we inject the css)
|
||||
Object.keys(styleColors).forEach((key) => {
|
||||
css = css.replace(`var(${key})`, styleColors[key][mode]);
|
||||
css = css.replaceAll(`var(${key})`, styleColors[key][mode]);
|
||||
})
|
||||
Object.keys(browserVars).forEach((key) => {
|
||||
css = css.replace(`var(${key})`, browserVars[key][browser]);
|
||||
css = css.replaceAll(`var(${key})`, browserVars[key][browser]);
|
||||
})
|
||||
|
||||
if (acStyle.styleSheet) {
|
||||
@@ -909,17 +1108,17 @@ async function setup() {
|
||||
// Callback
|
||||
await processQueue(QUEUE_AFTER_SETUP, null);
|
||||
}
|
||||
let loading = false;
|
||||
var tacLoading = false;
|
||||
onUiUpdate(async () => {
|
||||
if (loading) return;
|
||||
if (tacLoading) return;
|
||||
if (Object.keys(opts).length === 0) return;
|
||||
if (CFG) return;
|
||||
loading = true;
|
||||
if (TAC_CFG) return;
|
||||
tacLoading = true;
|
||||
// Get our tag base path from the temp file
|
||||
tagBasePath = await readFile(`tmp/tagAutocompletePath.txt`);
|
||||
// Load config from webui opts
|
||||
await syncOptions();
|
||||
// Rest of setup
|
||||
setup();
|
||||
loading = false;
|
||||
tacLoading = false;
|
||||
});
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
# This helper script scans folders for wildcards and embeddings and writes them
|
||||
# to a temporary file to expose it to the javascript side
|
||||
|
||||
import gradio as gr
|
||||
import glob
|
||||
from pathlib import Path
|
||||
from modules import scripts, script_callbacks, shared, sd_hijack
|
||||
|
||||
import gradio as gr
|
||||
import yaml
|
||||
from modules import script_callbacks, scripts, sd_hijack, shared
|
||||
|
||||
try:
|
||||
from modules.paths import script_path, extensions_dir
|
||||
from modules.paths import extensions_dir, script_path
|
||||
|
||||
# Webui root path
|
||||
FILE_DIR = Path(script_path)
|
||||
|
||||
@@ -64,7 +67,7 @@ def get_ext_wildcards():
|
||||
wildcard_files = []
|
||||
|
||||
for path in WILDCARD_EXT_PATHS:
|
||||
wildcard_files.append(path.relative_to(FILE_DIR).as_posix())
|
||||
wildcard_files.append(path.as_posix())
|
||||
wildcard_files.extend(p.relative_to(path).as_posix() for p in path.rglob("*.txt") if p.name != "put wildcards here.txt")
|
||||
wildcard_files.append("-----")
|
||||
|
||||
@@ -83,14 +86,18 @@ def get_ext_wildcard_tags():
|
||||
try:
|
||||
with open(path, encoding="utf8") as file:
|
||||
data = yaml.safe_load(file)
|
||||
for item in data:
|
||||
if data[item] and 'Tags' in data[item]:
|
||||
wildcard_tags[count] = ','.join(data[item]['Tags'])
|
||||
count += 1
|
||||
else:
|
||||
print('Issue with tags found in ' + path.name + ' at item ' + item)
|
||||
except yaml.YAMLError as exc:
|
||||
print(exc)
|
||||
if data:
|
||||
for item in data:
|
||||
if data[item] and 'Tags' in data[item] and isinstance(data[item]['Tags'], list):
|
||||
wildcard_tags[count] = ','.join(data[item]['Tags'])
|
||||
count += 1
|
||||
else:
|
||||
print('Issue with tags found in ' + path.name + ' at item ' + item)
|
||||
else:
|
||||
print('No data found in ' + path.name)
|
||||
except yaml.YAMLError:
|
||||
print('Issue in parsing YAML file ' + path.name )
|
||||
continue
|
||||
# Sort by count
|
||||
sorted_tags = sorted(wildcard_tags.items(), key=lambda item: item[1], reverse=True)
|
||||
output = []
|
||||
@@ -159,7 +166,8 @@ def get_hypernetworks():
|
||||
"""Write a list of all 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"}]
|
||||
hyp_paths = [Path(h) for h in glob.glob(HYP_PATH.joinpath("**/*").as_posix(), recursive=True)]
|
||||
all_hypernetworks = [str(h.name) for h in hyp_paths if h.suffix in {".pt"}]
|
||||
# Remove file extensions
|
||||
return sorted([h[:h.rfind('.')] for h in all_hypernetworks], key=lambda x: x.lower())
|
||||
|
||||
@@ -167,7 +175,8 @@ def get_lora():
|
||||
"""Write a list of all 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"}]
|
||||
lora_paths = [Path(l) for l in glob.glob(LORA_PATH.joinpath("**/*").as_posix(), recursive=True)]
|
||||
all_lora = [str(l.name) for l in lora_paths if l.suffix in {".safetensors", ".ckpt", ".pt"}]
|
||||
# Remove file extensions
|
||||
return sorted([l[:l.rfind('.')] for l in all_lora], key=lambda x: x.lower())
|
||||
|
||||
@@ -175,14 +184,15 @@ 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"}]
|
||||
lyco_paths = [Path(ly) for ly in glob.glob(LYCO_PATH.joinpath("**/*").as_posix(), recursive=True)]
|
||||
all_lyco = [str(ly.name) for ly in lyco_paths 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"""
|
||||
with open(STATIC_TEMP_PATH.joinpath('tagAutocompletePath.txt'), 'w', encoding="utf-8") as f:
|
||||
f.write(TAGS_PATH.relative_to(FILE_DIR).as_posix())
|
||||
f.write(TAGS_PATH.as_posix())
|
||||
|
||||
|
||||
def write_to_temp_file(name, data):
|
||||
@@ -200,6 +210,14 @@ def update_tag_files():
|
||||
csv_files = files
|
||||
csv_files_withnone = ["None"] + files
|
||||
|
||||
json_files = []
|
||||
json_files_withnone = []
|
||||
def update_json_files():
|
||||
"""Returns a list of all potential json files"""
|
||||
global json_files, json_files_withnone
|
||||
files = [str(j.relative_to(TAGS_PATH)) for j in TAGS_PATH.glob("*.json")]
|
||||
json_files = files
|
||||
json_files_withnone = ["None"] + files
|
||||
|
||||
|
||||
# Write the tag base path to a fixed location temporary file
|
||||
@@ -209,6 +227,7 @@ if not STATIC_TEMP_PATH.exists():
|
||||
|
||||
write_tag_base_path()
|
||||
update_tag_files()
|
||||
update_json_files()
|
||||
|
||||
# Check if the temp path exists and create it if not
|
||||
if not TEMP_PATH.exists():
|
||||
@@ -226,41 +245,49 @@ write_to_temp_file('lyco.txt', [])
|
||||
if not TEMP_PATH.joinpath("emb.txt").exists():
|
||||
write_to_temp_file('emb.txt', [])
|
||||
|
||||
# Write wildcards to wc.txt if found
|
||||
if WILDCARD_PATH.exists():
|
||||
wildcards = [WILDCARD_PATH.relative_to(FILE_DIR).as_posix()] + get_wildcards()
|
||||
if wildcards:
|
||||
write_to_temp_file('wc.txt', wildcards)
|
||||
|
||||
# Write extension wildcards to wce.txt if found
|
||||
if WILDCARD_EXT_PATHS is not None:
|
||||
wildcards_ext = get_ext_wildcards()
|
||||
if wildcards_ext:
|
||||
write_to_temp_file('wce.txt', wildcards_ext)
|
||||
# Write yaml extension wildcards to wcet.txt if found
|
||||
wildcards_yaml_ext = get_ext_wildcard_tags()
|
||||
if wildcards_yaml_ext:
|
||||
write_to_temp_file('wcet.txt', wildcards_yaml_ext)
|
||||
|
||||
# Write embeddings to emb.txt if found
|
||||
if EMB_PATH.exists():
|
||||
# Get embeddings after the model loaded callback
|
||||
script_callbacks.on_model_loaded(get_embeddings)
|
||||
|
||||
if HYP_PATH.exists():
|
||||
hypernets = get_hypernetworks()
|
||||
if hypernets:
|
||||
write_to_temp_file('hyp.txt', hypernets)
|
||||
def refresh_temp_files():
|
||||
write_temp_files()
|
||||
get_embeddings(shared.sd_model)
|
||||
|
||||
if LORA_PATH is not None and LORA_PATH.exists():
|
||||
lora = get_lora()
|
||||
if lora:
|
||||
write_to_temp_file('lora.txt', lora)
|
||||
def write_temp_files():
|
||||
# Write wildcards to wc.txt if found
|
||||
if WILDCARD_PATH.exists():
|
||||
wildcards = [WILDCARD_PATH.relative_to(FILE_DIR).as_posix()] + get_wildcards()
|
||||
if wildcards:
|
||||
write_to_temp_file('wc.txt', wildcards)
|
||||
|
||||
if LYCO_PATH is not None and LYCO_PATH.exists():
|
||||
lyco = get_lyco()
|
||||
if lyco:
|
||||
write_to_temp_file('lyco.txt', lyco)
|
||||
# Write extension wildcards to wce.txt if found
|
||||
if WILDCARD_EXT_PATHS is not None:
|
||||
wildcards_ext = get_ext_wildcards()
|
||||
if wildcards_ext:
|
||||
write_to_temp_file('wce.txt', wildcards_ext)
|
||||
# Write yaml extension wildcards to wcet.txt if found
|
||||
wildcards_yaml_ext = get_ext_wildcard_tags()
|
||||
if wildcards_yaml_ext:
|
||||
write_to_temp_file('wcet.txt', wildcards_yaml_ext)
|
||||
|
||||
if HYP_PATH.exists():
|
||||
hypernets = get_hypernetworks()
|
||||
if hypernets:
|
||||
write_to_temp_file('hyp.txt', hypernets)
|
||||
|
||||
if LORA_PATH is not None and LORA_PATH.exists():
|
||||
lora = get_lora()
|
||||
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)
|
||||
|
||||
|
||||
write_temp_files()
|
||||
|
||||
# Register autocomplete options
|
||||
def on_ui_settings():
|
||||
@@ -272,7 +299,7 @@ def on_ui_settings():
|
||||
shared.opts.add_option("tac_activeIn.txt2img", shared.OptionInfo(True, "Active in txt2img (Requires restart)", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_activeIn.img2img", shared.OptionInfo(True, "Active in img2img (Requires restart)", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_activeIn.negativePrompts", shared.OptionInfo(True, "Active in negative prompts (Requires restart)", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_activeIn.thirdParty", shared.OptionInfo(True, "Active in third party textboxes [Dataset Tag Editor] (Requires restart)", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_activeIn.thirdParty", shared.OptionInfo(True, "Active in third party textboxes [Dataset Tag Editor] [Image Browser] [Tagger] [Multidiffusion Upscaler] (Requires restart)", section=TAC_SECTION))
|
||||
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
|
||||
@@ -298,9 +325,12 @@ def on_ui_settings():
|
||||
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))
|
||||
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))
|
||||
shared.opts.add_option("tac_translation.searchByTranslation", shared.OptionInfo(True, "Search by translation", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_translation.liveTranslation", shared.OptionInfo(False, "Show live tag translation below prompt (WIP, expect some bugs)", section=TAC_SECTION))
|
||||
# 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))
|
||||
# Chant settings
|
||||
shared.opts.add_option("tac_chantFile", shared.OptionInfo("demo-chants.json", "Chant filename (Chants are longer prompt presets)", gr.Dropdown, lambda: {"choices": json_files_withnone}, refresh=update_json_files, section=TAC_SECTION))
|
||||
# Custom mappings
|
||||
keymapDefault = """\
|
||||
{
|
||||
@@ -348,5 +378,6 @@ def on_ui_settings():
|
||||
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))
|
||||
|
||||
|
||||
shared.opts.add_option("tac_refreshTempFiles", shared.OptionInfo("Refresh TAC temp files", "Refresh internal temp files", gr.HTML, {}, refresh=refresh_temp_files, section=TAC_SECTION))
|
||||
|
||||
script_callbacks.on_ui_settings(on_ui_settings)
|
||||
|
||||
32
tags/demo-chants.json
Normal file
32
tags/demo-chants.json
Normal file
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"name": "Basic-NegativePrompt",
|
||||
"terms": "Basic,Negative,Low,Quality",
|
||||
"content": "(worst quality, low quality, normal quality)",
|
||||
"color": 3
|
||||
},
|
||||
{
|
||||
"name": "Basic-HighQuality",
|
||||
"terms": "Basic,Best,High,Quality",
|
||||
"content": "(masterpiece, best quality, high quality, highres, ultra-detailed)",
|
||||
"color": 1
|
||||
},
|
||||
{
|
||||
"name": "Basic-Start",
|
||||
"terms": "Basic, Start, Simple, Demo",
|
||||
"content": "(masterpiece, best quality, high quality, highres), 1girl, extremely beautiful detailed face, short curly hair, light smile, flower dress, outdoors, leaf, tree, best shadow",
|
||||
"color": 5
|
||||
},
|
||||
{
|
||||
"name": "Fancy-FireMagic",
|
||||
"terms": "Fire, Magic, Fancy",
|
||||
"content": "(extremely detailed CG unity 8k wallpaper), (masterpiece), (best quality), (ultra-detailed), (best illustration),(best shadow), (an extremely delicate and beautiful), dynamic angle, floating, fine detail, (bloom), (shine), glinting stars, classic, (painting), (sketch),\n\na girl, solo, bare shoulders, flat_chest, diamond and glaring eyes, beautiful detailed cold face, very long blue and sliver hair, floating black feathers, wavy hair, extremely delicate and beautiful girls, beautiful detailed eyes, glowing eyes,\n\npalace, the best building, ((Fire butterflies, Flying sparks, Flames))",
|
||||
"color": 5
|
||||
},
|
||||
{
|
||||
"name": "Fancy-WaterMagic",
|
||||
"terms": "Water, Magic, Fancy",
|
||||
"content": "(extremely detailed CG unity 8k wallpaper), (masterpiece), (best quality), (ultra-detailed), (best illustration),(best shadow), (an extremely delicate and beautiful), classic, dynamic angle, floating, fine detail, Depth of field, classic, (painting), (sketch), (bloom), (shine), glinting stars,\n\na girl, solo, bare shoulders, flat chest, diamond and glaring eyes, beautiful detailed cold face, very long blue and sliver hair, floating black feathers, wavy hair, extremely delicate and beautiful girls, beautiful detailed eyes, glowing eyes,\n\nriver, (forest),palace, (fairyland,feather,flowers, nature),(sunlight),Hazy fog, mist",
|
||||
"color": 5
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
masterpiece,5,Quality tag,
|
||||
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,
|
||||
masterpiece,5,Quality tag,,
|
||||
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,,
|
||||
|
||||
|
Reference in New Issue
Block a user