mirror of
https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git
synced 2026-01-27 03:29:55 +00:00
Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c6bba2a3d | ||
|
|
9a47c2ec2c | ||
|
|
fe32ad739d | ||
|
|
ade67e30a6 | ||
|
|
e9a21e7a55 | ||
|
|
3ef2a7d206 | ||
|
|
29b5bf0701 | ||
|
|
3eef536b64 | ||
|
|
0d24e697d2 | ||
|
|
a27633da55 | ||
|
|
4cd6174a22 | ||
|
|
9155e4d42c | ||
|
|
700642a400 | ||
|
|
1b592dbf56 | ||
|
|
d1eea880f3 | ||
|
|
119a3ad51f | ||
|
|
c820a22149 | ||
|
|
eb1e1820f9 | ||
|
|
ef59cff651 | ||
|
|
a454383c43 | ||
|
|
bec567fe26 | ||
|
|
d4041096c9 | ||
|
|
0903259ddf | ||
|
|
f3e64b1fa5 | ||
|
|
312cec5d71 | ||
|
|
b71e6339bd | ||
|
|
7ddbc3c0b2 | ||
|
|
4c2ef8f770 | ||
|
|
97c5e4f53c | ||
|
|
1d8d9f64b5 | ||
|
|
7437850600 | ||
|
|
829a4a7b89 | ||
|
|
22472ac8ad | ||
|
|
5f77fa26d3 | ||
|
|
f810b2dd8f | ||
|
|
08d3436f3b | ||
|
|
afa13306ef | ||
|
|
95200e82e1 | ||
|
|
a63ce64f4e | ||
|
|
a966be7546 | ||
|
|
d37e37acfa | ||
|
|
342fbc9041 | ||
|
|
d496569c9a | ||
|
|
7778142520 | ||
|
|
cde90c13c4 | ||
|
|
231b121fe0 | ||
|
|
c659ed2155 | ||
|
|
0a4c17cada | ||
|
|
6e65811d4a | ||
|
|
03673c060e | ||
|
|
1c11c4ad5a | ||
|
|
30c9593d3d | ||
|
|
f840586b6b | ||
|
|
886704e351 | ||
|
|
41626d22c3 | ||
|
|
57076060df | ||
|
|
5ef346cde3 | ||
|
|
edf76d9df2 | ||
|
|
837dc39811 | ||
|
|
f1870b7e87 | ||
|
|
20b6635a2a | ||
|
|
1fe8f26670 | ||
|
|
e82e958c3e | ||
|
|
2dd48eab79 | ||
|
|
4df90f5c95 | ||
|
|
a156214a48 | ||
|
|
15478e73b5 | ||
|
|
fcacf7dd66 | ||
|
|
82f819f336 | ||
|
|
effda54526 | ||
|
|
434301738a | ||
|
|
58804796f0 | ||
|
|
668ca800b8 | ||
|
|
a7233a594f | ||
|
|
4fba7baa69 | ||
|
|
5ebe22ddfc | ||
|
|
44c5450b28 | ||
|
|
5fd48f53de | ||
|
|
7128efc4f4 | ||
|
|
bd0ddfbb24 | ||
|
|
3108daf0e8 | ||
|
|
446ac14e7f | ||
|
|
363895494b | ||
|
|
04551a8132 | ||
|
|
ffc0e378d3 | ||
|
|
440f109f1f | ||
|
|
80fb247dbe | ||
|
|
b3e71e840d | ||
|
|
998514bebb | ||
|
|
d7e98200a8 | ||
|
|
ac790c8ede | ||
|
|
22365ec8d6 | ||
|
|
030a83aa4d | ||
|
|
460d32a4ed | ||
|
|
581bf1e6a4 | ||
|
|
74ea5493e5 | ||
|
|
94ec8884c3 | ||
|
|
6cf9acd6ab | ||
|
|
109a8a155e | ||
|
|
3caa1b51ed | ||
|
|
b44c36425a | ||
|
|
1e81403180 | ||
|
|
0f487a5c5c | ||
|
|
2baa12fea3 | ||
|
|
1a9157fe6e | ||
|
|
67eeb5fbf6 | ||
|
|
5911248ab9 | ||
|
|
1c693c0263 | ||
|
|
11ffed8afc | ||
|
|
cb54b66eda | ||
|
|
92a937ad01 | ||
|
|
ba9dce8d90 | ||
|
|
2622e1b596 | ||
|
|
b03b1a0211 | ||
|
|
3e33169a3a | ||
|
|
d8d991531a | ||
|
|
f626b9453d | ||
|
|
5067afeee9 | ||
|
|
018c6c8198 | ||
|
|
2846d79b7d | ||
|
|
783a847978 | ||
|
|
44effca702 | ||
|
|
475ef59197 | ||
|
|
3953260485 | ||
|
|
0a8e7d7d84 | ||
|
|
46d07d703a | ||
|
|
bd1dbe92c2 | ||
|
|
66fa745d6f | ||
|
|
37b5dca66e | ||
|
|
5db035cc3a | ||
|
|
90cf3147fd | ||
|
|
4d4f23e551 | ||
|
|
80b47c61bb | ||
|
|
57821aae6a | ||
|
|
e23bb6d4ea | ||
|
|
d4cca00575 | ||
|
|
86ea94a565 | ||
|
|
53f46c91a2 | ||
|
|
e5f93188c3 | ||
|
|
3e57842ac6 | ||
|
|
32c4589df3 | ||
|
|
5bbd97588c | ||
|
|
b2a663f7a7 | ||
|
|
6f93d19a2b | ||
|
|
79bab04fd2 | ||
|
|
5b69d1e622 | ||
|
|
651cf5fb46 | ||
|
|
5deb72cddf | ||
|
|
97ebe78205 | ||
|
|
b937e853c9 | ||
|
|
f63bbf947f | ||
|
|
16bc6d8868 | ||
|
|
ebe276ee44 | ||
|
|
995a5ecdba | ||
|
|
90d144a5f4 | ||
|
|
14a4440c33 | ||
|
|
cdf092f3ac | ||
|
|
e1598378dc | ||
|
|
599ad7f95f | ||
|
|
0b2bb138ee | ||
|
|
4a415f1a04 | ||
|
|
21de5fe003 | ||
|
|
a020df91b2 | ||
|
|
0260765b27 | ||
|
|
638c073f37 | ||
|
|
d11b53083b | ||
|
|
571072eea4 | ||
|
|
acfdbf1ed4 | ||
|
|
2e271aea5c | ||
|
|
b28497764f | ||
|
|
0d9d5f1e44 | ||
|
|
de3380818e | ||
|
|
acb85d7bb1 | ||
|
|
39ea33be9f | ||
|
|
1cac893e63 | ||
|
|
94823b871c | ||
|
|
599ff8a6f2 | ||
|
|
6893113e0b | ||
|
|
ed89d0e7e5 | ||
|
|
e47c14ab5e | ||
|
|
2ae512c8be | ||
|
|
4a427e309b | ||
|
|
510ee66b92 | ||
|
|
c41372143d | ||
|
|
f1d911834b | ||
|
|
40d9fc1079 | ||
|
|
88fa4398c8 | ||
|
|
3496fa58d9 | ||
|
|
737b697357 | ||
|
|
8523d7e9b5 | ||
|
|
77c0970500 | ||
|
|
707202ed71 | ||
|
|
922414b4ba | ||
|
|
7f18856321 | ||
|
|
cc1f35fc68 | ||
|
|
ae0f80ab0e | ||
|
|
8a436decf2 | ||
|
|
7357ccb347 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
tags/temp/
|
||||
__pycache__/
|
||||
tags/tag_frequency.db
|
||||
|
||||
87
README.md
87
README.md
@@ -23,11 +23,12 @@ Booru style tag autocompletion for the AUTOMATIC1111 Stable Diffusion WebUI
|
||||
# 📄 Description
|
||||
|
||||
Tag Autocomplete is an extension for the popular [AUTOMATIC1111 web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) for Stable Diffusion.
|
||||
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).
|
||||
|
||||
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) and many of the NAI-descendant models or merges, using exact tags in prompts can often improve composition and consistency.
|
||||
Since most custom Stable Diffusion models were trained using this information or merged with ones that did, using exact tags in prompts can often improve composition and consistency, even if the model itself has a photorealistic style.
|
||||
|
||||
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).
|
||||
Disclaimer: The default tag lists contain NSFW terms, please use them responsibly.
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -42,7 +43,7 @@ You can install it using the inbuilt available extensions list, clone the files
|
||||
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
|
||||
- ➕ [**Extra network**](#extra-networks-embeddings-hypernets-lora-) filenames, including
|
||||
- Textual Inversion embeddings [(jump to readme section)]
|
||||
- Hypernetworks
|
||||
- LoRA
|
||||
@@ -74,6 +75,10 @@ Wildcard script support:
|
||||
|
||||
https://user-images.githubusercontent.com/34448969/200128031-22dd7c33-71d1-464f-ae36-5f6c8fd49df0.mp4
|
||||
|
||||
Extra Network preview support:
|
||||
|
||||
https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/3c0cad84-fb5f-436d-b05a-28db35860d13
|
||||
|
||||
Dark and Light mode supported, including tag colors:
|
||||
|
||||

|
||||
@@ -123,6 +128,49 @@ Completion for these types is triggered by typing `<`. By default it will show t
|
||||
- Or `<lora:` and `<lyco:` respectively for the long form
|
||||
- `<h:` or `<hypernet:` will only show Hypernetworks
|
||||
|
||||
### Live previews
|
||||
Tag Autocomplete will now also show the preview images used for the cards in the Extra Networks menu in a small window next to the regular popup.
|
||||
This enables quick comparisons and additional info for unclear filenames without having to stop typing to look it up in the webui menu.
|
||||
It works for all supported extra network types that use preview images (Loras/Lycos, Embeddings & Hypernetworks). The preview window will stay hidden for normal tags or if no preview was found.
|
||||
|
||||

|
||||
|
||||
### Lora / Lyco trigger word completion
|
||||
This feature will try to add known trigger words on autocompleting a Lora/Lyco.
|
||||
|
||||
It primarily uses the list provided by the [model-keyword](https://github.com/mix1009/model-keyword/) extension, which thus needs to be installed to use this feature. The list is also regularly updated through it.
|
||||
However, once installed, you can deactivate it if you want, since tag autocomplete only needs the local keyword lists it ships with, not the extension itself.
|
||||
The used files are `lora-keyword.txt` and `lora-keyword-user.txt` in the model-keyword installation folder.
|
||||
If the main file isn't found, the feature will simply deactivate itself, everything else should work normally.
|
||||
|
||||
#### Note:
|
||||
As of [v1.5.0](https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/a3ddf464a2ed24c999f67ddfef7969f8291567be), the webui provides a native method to add activation keywords for Lora through the Extra networks config UI.
|
||||
These trigger words will always be preferred over the model-keyword ones and can be used without needing to install the model-keyword extension. This will however, obviously, be limited to those manually added keywords. For automatic discovery of keywords, you will still need the big list provided by model-keyword.
|
||||
|
||||
Custom trigger words can be added through two methods:
|
||||
1. Using the extra networks UI (recommended):
|
||||
- Only works with webui version v1.5.0 upwards, but much easier to use and works without the model-keyword extension
|
||||
- This method requires no manual refresh
|
||||
- <details>
|
||||
<summary>Image example</summary>
|
||||
|
||||

|
||||

|
||||
</details>
|
||||
2. Through the model-keyword UI:
|
||||
- One issue with this method is that it has no official support for the Lycoris extension and doesn't scan its folder for files, so to add them through the UI you will have to temporarily move them into the Lora model folder to be able to select them in model-keywords dropdown. Some are already included in the default list though, so trying it out first is advisable.
|
||||
- After having added your custom keywords, you will need to either restart the UI or use the "Refresh TAC temp files" setting button.
|
||||
- <details>
|
||||
<summary>Image example</summary>
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
Sometimes the inserted keywords can be wrong due to a hash collision, however model-keyword and tag autocomplete take the name of the file into account too if the collision is known.
|
||||
|
||||
If it still inserts something wrong or you simply don't want the keywords added that time, you can undo / redo it directly after as often as you want, until you type something else
|
||||
(It uses the default undo/redo action of the browser, so <kbd>CTRL</kbd> + <kbd>Z</kbd>, context menu and mouse macros should all work).
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -271,6 +319,14 @@ If this option is turned on, it will show a `?` link next to the tag. Clicking t
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Wiki links -->
|
||||
<details>
|
||||
<summary>Extra network live previews</summary>
|
||||
|
||||
This option enables a small preview window alongside the normal completion popup that will show the card preview also usd in the extra networks tab for that file.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Insertion -->
|
||||
<details>
|
||||
<summary>Completion settings</summary>
|
||||
@@ -285,6 +341,26 @@ Depending on the last setting, tag autocomplete will append a comma and space af
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Lora keywords -->
|
||||
<details>
|
||||
<summary>Lora / Lyco trigger word insertion</summary>
|
||||
|
||||
See [the detailed readme section](#lora--lyco-trigger-word-completion) for more info.
|
||||
|
||||
Selects the mode to use for Lora / Lyco trigger word insertion.
|
||||
Needs the [model-keyword](https://github.com/mix1009/model-keyword/) extension to be installed, else it will do nothing.
|
||||
|
||||
- Never
|
||||
- Will not complete trigger words, even if the model-keyword extension is installed
|
||||
- Only user list
|
||||
- Will only load the custom keywords specified in the lora-keyword-user.txt file and ignore the default list
|
||||
- Always
|
||||
- Will load and use both lists
|
||||
|
||||
Switching from "Never" to what you had before or back will not require a restart, but changing between the full and user only list will.
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Wildcard path mode -->
|
||||
<details>
|
||||
<summary>Wildcard path completion</summary>
|
||||
@@ -399,7 +475,9 @@ You can also add this to your quicksettings bar to have the refresh button avail
|
||||
|
||||
# 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 `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.
|
||||
This file needs to be a CSV in the format `<English tag/alias>,<Translation>`. Some older files use a three column format, which requires a compatibility setting to be activated.
|
||||
You can find it under `Settings > Tag autocomplete > Translation filename > Translation file uses old 3-column translation format instead of the new 2-column one`.
|
||||
With it on, the second column will be unused and skipped during parsing.
|
||||
|
||||
Example with Chinese translation:
|
||||
|
||||
@@ -409,6 +487,7 @@ Example with Chinese translation:
|
||||
## 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
|
||||
- [🇯🇵 Japanese tags](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/discussions/265) by @applemango, both machine and human translations available
|
||||
|
||||
> ### 🫵 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.
|
||||
|
||||
429
README_JA.md
429
README_JA.md
@@ -1,21 +1,67 @@
|
||||

|
||||
|
||||
# Booru tag autocompletion for A1111
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)
|
||||
# SD WebUI Tag Autocomplete
|
||||
## [English Document](./README.md), [中文文档](./README_ZH.md), 日本語
|
||||
|
||||
## [English Document](./README.md), [中文文档](./README_ZH.md)
|
||||
Booruスタイルタグを自動補完するためのAUTOMATIC1111 Stable Diffusion WebUI用拡張機能
|
||||
|
||||
このカスタムスクリプトは、Stable Diffusion向けの人気のweb UIである、[AUTOMATIC1111 web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui)の拡張機能として利用できます。
|
||||
[![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]
|
||||
|
||||
[変更内容][release-url] •
|
||||
[確認されている問題](#%EF%B8%8F-よくある問題また現在確認されている問題) •
|
||||
[バグを報告する][issues-url] •
|
||||
[機能追加に関する要望][issues-url]
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
# 📄 説明
|
||||
|
||||
Tag Autocompleteは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モデルもあるため、正確なタグをプロンプトに使用することで、構図を改善し、思い通りの画像が生成できるようになります。
|
||||
例えば[Waifu Diffusion](https://github.com/harubaru/waifu-diffusion)やNAIから派生した多くのモデルやマージなど、Stable Diffusionモデルの中にはこの情報を使って学習されたものもあるため、プロンプトに正確なタグを使用することで、多くのケースで構図を改善した思い通りの画像が生成できるようになります。
|
||||
|
||||
web UIに内蔵されている利用可能な拡張機能リストを使用してインストールするか、[以下の説明](#インストール)に従ってファイルを手動でcloneするか、または[リリース](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)からパッケージ化されたバージョンを使用することができます。
|
||||
組み込みの利用可能な拡張機能リストを使ってインストールしたり、[下記](#-インストール)の説明に従って手動でファイルをcloneしたり、[Releases](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拡張機能をインストールしている場合は、必ずアップデートしてください。古いバージョンでは、他の拡張機能との間で問題が発生することが知られています。
|
||||
<br/>
|
||||
|
||||
# ✨ Features
|
||||
- 🚀 タイピング中に補完のためのヒントを表示 (通常時)
|
||||
- ⌨️ キーボードナビゲーション
|
||||
- 🌒 ダーク&ライトモードのサポート
|
||||
- 🛠️ 多くの[設定](#%EF%B8%8F-設定)とカスタマイズ性を提供
|
||||
- 🌍 [翻訳サポート](#翻訳)タグ、オプションでプロンプトのライブ プレビュー付き
|
||||
- 私が知っている翻訳のリストは[こちら](#翻訳リスト)を参照してください。
|
||||
|
||||
タグの自動補完は組み込まれている補完内容をサポートしています:
|
||||
- 🏷️ **Danbooru & e621 tags** (投稿数上位100k、2022年11月現在)
|
||||
- ✳️ [**ワイルドカード**](#ワイルドカード)
|
||||
- ➕ [**Extra networks**](#extra-networks-embeddings-hypernets-lora-) filenames, including
|
||||
- Textual Inversion embeddings
|
||||
- Hypernetworks
|
||||
- LoRA
|
||||
- LyCORIS / LoHA
|
||||
- 🪄 [**Chants(詠唱)**](#chants詠唱) (長いプロンプトプリセット用のカスタムフォーマット)
|
||||
- 🏷️ "[**Extra file**](#extra-file)", カスタマイズ可能なextra tagsセット
|
||||
|
||||
|
||||
さらに、サードパーティの拡張機能にも対応しています:
|
||||
<details>
|
||||
<summary>クリックして開く</summary>
|
||||
|
||||
- [Image Browser][image-browser-url] - ファイル名とEXIFキーワードによる検索
|
||||
- [Multidiffusion Upscaler][multidiffusion-url] - 地域別のプロンプト
|
||||
- [Dataset Tag Editor][tag-editor-url] - キャプション, 結果の確認, タグの編集 & キャプションの編集
|
||||
- [WD 1.4 Tagger][wd-tagger-url] - 追加と除外タグ
|
||||
- [Umi AI][umi-url] - YAMLワイルドカードの補完
|
||||
</details>
|
||||
<br/>
|
||||
|
||||
## スクリーンショット & デモ動画
|
||||
<details>
|
||||
@@ -34,8 +80,8 @@ https://user-images.githubusercontent.com/34448969/200128031-22dd7c33-71d1-464f-
|
||||

|
||||
</details>
|
||||
|
||||
## インストール
|
||||
### 内蔵されている拡張機能リストを用いた方法
|
||||
# 📦 インストール
|
||||
## 内蔵されている拡張機能リストを用いた方法
|
||||
1. Extensions タブを開く
|
||||
2. Available タブを開く
|
||||
3. "Load from:" をクリック
|
||||
@@ -48,14 +94,14 @@ https://user-images.githubusercontent.com/34448969/200128031-22dd7c33-71d1-464f-
|
||||

|
||||
|
||||
|
||||
### 手動でcloneする方法
|
||||
## 手動で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つを選択して、ワイルドカードをカテゴリ化されたカスタムタグシステムのようなものとして使用することができます。
|
||||
@@ -66,20 +112,21 @@ git clone "https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git" extens
|
||||
|
||||
ワイルドカードはすべての拡張機能フォルダと、古いバージョンをサポートするための `scripts/wildcards` フォルダで検索されます。これは複数の拡張機能からワイルドカードを組み合わせることができることを意味しています。ワイルドカードをグループ化した場合、ネストされたフォルダもサポートされます。
|
||||
|
||||
### Embeddings, Lora & Hypernets
|
||||
## Extra networks (Embeddings, Hypernets, LoRA, ...)
|
||||
これら3つのタイプの補完は、`<`と入力することで行われます。デフォルトでは3つとも混在して表示されますが、以下の方法でさらにフィルタリングを行うことができます:
|
||||
- `<e:` は、embeddingsのみを表示します。
|
||||
- `<l:` 、または `<lora:` はLoraのみを表示します。
|
||||
- `<l:` は、LoRAとLyCORISのみを表示します。
|
||||
- または `<lora:` と `<lyco:` で入力することも可能です
|
||||
- `<h:` 、または `<hypernet:` はHypernetworksのみを表示します
|
||||
|
||||
#### Embedding type filtering
|
||||
### Embedding type filtering
|
||||
Stable Diffusion 1.xまたは2.xモデル用にそれぞれトレーニングされたembeddingsは、他のタイプとの互換性がありません。有効なembeddingsを見つけやすくするため、若干の色の違いも含めて「v1 Embedding」と「v2 Embedding」で分類しています。また、`<v1/2`または`<e:v1/2`に続けて実際の検索のためのキーワードを入力すると、v1またはv2embeddingsのみを含むように検索を絞り込むことができます。
|
||||
|
||||
例:
|
||||
|
||||

|
||||
|
||||
### Chants(詠唱)
|
||||
## Chants(詠唱)
|
||||
Chants(詠唱)は、より長いプロンプトプリセットです。この名前は、中国のユーザーによる初期のプロンプト集からヒントを得たもので、しばしば「呪文書」(原文は「Spellbook」「Codex」)などと呼ばれていました。
|
||||
このような文書から得られるプロンプトのスニペットは、このような理由から呪文や詠唱と呼ばれるにふさわしいものでした。
|
||||
|
||||
@@ -89,8 +136,11 @@ EmbeddingsやLoraと同様に、この機能は `<`, `<c:`, `<chant:` コマン
|
||||
(masterpiece, best quality, high quality, highres, ultra-detailed),
|
||||
```
|
||||
|
||||
|
||||
Chants(詠唱)は、以下のフォーマットに従ってJSONファイルで追加することができます::
|
||||
|
||||
<details>
|
||||
<summary>Chant format (click to expand)</summary>
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
@@ -113,6 +163,10 @@ Chants(詠唱)は、以下のフォーマットに従ってJSONファイル
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
<br/>
|
||||
|
||||
このファイルが拡張機能の `tags` フォルダ内にある場合、settings内の"Chant file"ドロップダウンから選択することができます。
|
||||
|
||||
chantオブジェクトは4つのフィールドを持ちます:
|
||||
@@ -121,7 +175,7 @@ chantオブジェクトは4つのフィールドを持ちます:
|
||||
- `content` - 実際に挿入されるプロンプト
|
||||
- `color` - 表示される色。通常のタグと同じカテゴリーカラーシステムを使用しています。
|
||||
|
||||
### Umi AI tags
|
||||
## Umi AI tags
|
||||
https://github.com/Klokinator/Umi-AI は、Unprompted や Dynamic Wildcards に似た、機能豊富なワイルドカード拡張です。
|
||||
例えば `<[preset][--female][sfw][species]>` はプリセットカテゴリーを選び、女性関連のタグを除外し、さらに次のカテゴリーで絞り込み、実行時にこれらすべての条件に一致するランダムなフィルインを1つ選び出します。補完は `<[`] とそれに続く新しい開く括弧、例えば `<[xyz][`] で始まり、 `>` で閉じるまで続きます。
|
||||
|
||||
@@ -130,78 +184,264 @@ https://github.com/Klokinator/Umi-AI は、Unprompted や Dynamic Wildcards に
|
||||
|
||||
ほとんどの功績は[@ctwrs](https://github.com/ctwrs)によるものです。この方はUmiの開発者の一人として多くの貢献をしています。
|
||||
|
||||
## Settings
|
||||
# 🛠️ 設定
|
||||
|
||||
この拡張機能には、大量の設定&カスタマイズ性が組み込まれています:
|
||||
この拡張機能には多くの設定とカスタマイズ機能が組み込まれています。ほとんどのことははっきりしていますが、詳細な説明は以下のセクションをクリックしてください。
|
||||
|
||||

|
||||
<!-- Filename -->
|
||||
<details>
|
||||
<summary>Tag filename</summary>
|
||||
|
||||
| 設定項目 | 説明 |
|
||||
|---------|-------------|
|
||||
| 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進数、どちらも使えるはずです。
|
||||
スクリプトが使用するメインのタグファイルとなります。デフォルトでは `danbooru.csv` と `e621.csv` が含まれており、ここにカスタムタグを追加することもできますが、大半のモデルはこの2つ以外(主にdanbooru)では学習していないため、あまり意味はありません。
|
||||
|
||||
```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)を参照してください。
|
||||
拡張機能の他の機能(ワイルドカードやLoRA補完など)を使いたいが、通常のタグには興味がない場合は、`None`に設定することも可能です。
|
||||
|
||||
### エイリアス, 翻訳 & Extra tagsについて
|
||||
#### エイリアス
|
||||
Booruのサイトのように、タグは1つまたは複数のエイリアスを持つことができ、完了時に実際の値へリダイレクトされて入力されます。これらは `config.json` の設定をもとに検索/表示されます:
|
||||
- `searchByAlias` - エイリアスも検索対象とするか、実際のタグのみを検索対象とするかを設定します
|
||||
- `onlyShowAlias` - `alias -> actual` の代わりに、エイリアスのみを表示します。表示のみで、最後に挿入されるテキストは実際のタグのままです。
|
||||

|
||||
</details>
|
||||
|
||||
#### 翻訳
|
||||
<!-- Active In -->
|
||||
<details>
|
||||
<summary>"Active in" の設定</summary>
|
||||
|
||||
タグのオートコンプリートがどこにアタッチされ、変更を受け付けるかを指定します。
|
||||
ネガティブプロンプトはtxt2imgとimg2imgの設定に従うので、"親 "がアクティブな場合にのみアクティブとなります。
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<!-- Blacklist -->
|
||||
<details>
|
||||
<summary>Black / Whitelist</summary>
|
||||
|
||||
このオプションは、タグのオートコンプリートをグローバルにオフにすることができますが、特定のモデルに対してのみ有効または無効にしたい場合もあります。
|
||||
例えば、あなたのモデルのほとんどがアニメモデルである場合、boorタグでトレーニングされておらず、その恩恵を受けないフォトリアリスティックモデルをブラックリストに追加し、これらのモデルに切り替えた後に自動的に無効にすることができます。モデル名(拡張子を含む)とwebuiハッシュ(短い形式と長い形式の両方)の両方を使用できます。
|
||||
|
||||
`Blacklist`は指定したすべてのモデルを除外しますが、`Whitelist`はこれらのモデルに対してのみ有効で、デフォルトではオフのままです。例外として、空のホワイトリストは無視されます(空のブラックリストと同じです)。
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Move Popup -->
|
||||
<details>
|
||||
<summary>カーソルで補完ポップアップを移動</summary>
|
||||
|
||||
このオプションは、IDEで行われるような、カーソルの位置に追従するフローティングポップアップを有効または無効にします。スクリプトはポップアップが右側でつぶれないように十分なスペースを確保しようとしますが、長いタグでは必ずしもうまくいきません。無効にした場合、ポップアップは左側に表示されます。
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
</details>
|
||||
<!-- Results Count -->
|
||||
<details>
|
||||
<summary>結果の数</summary>
|
||||
|
||||
一度に表示する結果の量を設定できます。
|
||||
`Show all results`が有効な場合、`Maximum results`で指定された数で切り捨てられるのではなく、スクロール可能なリストが表示されます。パフォーマンス上の理由から、この場合はすべてを一度に読み込むのではなく、ブロック単位で読み込みます。ブロックの大きさは`How many results to load at once`によって決まります。一番下に到達すると、次のブロックがロードされます(しかし、そんなことはめったには起こらないと思います)。
|
||||
|
||||
特筆すべきこととして、`Show all results` が使用される場合でも、`Maximum results` は影響を及ぼします。これは、スクロールが開始される前のポップアップの高さを制限するからです。
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Delay time -->
|
||||
<details>
|
||||
<summary>補完の遅れについて</summary>
|
||||
|
||||
設定によっては、リアルタイムのタグ補完は計算量が多くなることがあります。
|
||||
このオプションは debounce による遅延をミリ秒単位で設定します(1000ミリ秒 = 1秒)。このオプションは、入力が非常に速い場合に特に有効です。
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Search for -->
|
||||
<details>
|
||||
<summary>"Search for" に関する設定</summary>
|
||||
|
||||
特定の補完タイプを有効または無効にします。
|
||||
|
||||
Umi AIワイルドカードは、使用目的が似ているため、異なるフォーマットを使用しますが、ここでは通常のワイルドカードオプションに含まれます。
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Wiki links -->
|
||||
<details>
|
||||
<summary>"?" Wiki links</summary>
|
||||
|
||||
このオプションがオンになっている場合、タグの横に `?` リンクが表示されます。これをクリックすると、danbooruまたはe621のそのタグのWikiページを開こうとします。
|
||||
|
||||
> ⚠️ 警告:
|
||||
>
|
||||
> Danbooruとe621は外部サイトであり、多くのNSFWコンテンツを含んでいます。このため、このオプションはデフォルトで無効になっています。
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Insertion -->
|
||||
<details>
|
||||
<summary>補完設定</summary>
|
||||
|
||||
これらの設定で、テキストの挿入方法を指定できます。
|
||||
|
||||
Booruのサイトでは、タグにスペースの代わりにアンダースコアを使用することがほとんどですが、Stable diffusionで使用されているCLIPエンコーダーは自然言語でトレーニングされているため、前処理中にほとんどのモデルがこのアンダースコアをスペースに置き換えました。したがって、デフォルトではタグのオートコンプリートも同じようになります。
|
||||
|
||||
括弧は、プロンプトの特定の部分をより注目/重視するために、Webuiの制御文字として使用されるため、デフォルトでは括弧を含むタグはエスケープされます (`\( \)`) 。
|
||||
|
||||
最後の設定によりますが、タグのオートコンプリートはタグを挿入した後にカンマとスペースを追加します。
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Wildcard path mode -->
|
||||
<details>
|
||||
<summary>ワイルドカードのパス補完</summary>
|
||||
|
||||
ワイルドカードのいくつかのコレクションは、ネストしたサブフォルダに整理されています。
|
||||
それらは、"hair/colors/light/... " や "clothing/male/casual/... " などのように、ファイルへのフルパスとともにリストアップされています。
|
||||
|
||||
このような場合、手動でフルパスを入力するのは難しいことが多いのですが、それでもリストをさらにスクロールする前に選択範囲を狭めたいものです。
|
||||
この場合、選択する結果がすべてのパスであれば、<kbd>Tab</kbd>(または`ChooseSelectedOrFirst`のカスタムホットキー)でパスの部分補完をトリガーすることが可能です。
|
||||
|
||||
この設定は、補完に使用するモードを決定します:
|
||||
- 次のフォルダレベルまで:
|
||||
- パス内の次の/まで補完し、選択したものを進む方向として優先します
|
||||
- オプションを直接選択したい場合は、<kbd>Enter</kbd> キーまたは `ChooseSelected` ホットキーを使用してスキップし、完全に補完します。
|
||||
- 最初の差分まで:
|
||||
- 結果内の最初の違いまで補完し、追加の情報を待ちます
|
||||
- 例:"/sub/folder_a/..." と "/sub/folder_b/..." がある場合、"su" と入力した後に補完すると、"/sub/folder_" まですべてを挿入し、a または b を入力して明確にするまでそこで停止します。
|
||||
- 矢印キーで何かを選択した場合(EnterキーやTabキーを押すかどうかに関係なく)、それをスキップして完全に補完します。
|
||||
- 常に全て:
|
||||
- 名前が示すように、部分的な補完動作を無効にし、通常のタグのようにすべての状況下で完全なパスを挿入します。
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Alias -->
|
||||
<details>
|
||||
<summary>Alias 設定</summary>
|
||||
|
||||
タグはしばしば複数の別名(Alias)で参照されます。`Search by alias`がオンになっている場合、それらは検索結果に含まれ、正しいタグがわからない場合に役立ちます。この場合でも、挿入時にリンクされている実際のタグに置き換えられます。
|
||||
|
||||
`Only show alias` セットは、エイリアスのみを表示したい場合、またはそのエイリアスがマップするタグも表示したい場合に使用します。
|
||||
(`<alias> ➝ <actual>`として表示されます)
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Translations -->
|
||||
<details>
|
||||
<summary>翻訳設定</summary>
|
||||
|
||||
Tag Autocompleteは、別のファイル(`Translation filename`)で指定されたタグの翻訳をサポートしています。つまり、英語のタグ名が分からなくても、自身の言語の翻訳ファイルがあれば、それを代わりに使うことができます。
|
||||
|
||||
また、コミュニティで使用されている古いファイルのためのレガシーフォーマットオプションや、プロンプト全体のライブ翻訳プレビューなど実験的な機能もあります。
|
||||
|
||||
詳細については、以下の [翻訳に関するセクション](#翻訳) を参照してください。
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Extra file -->
|
||||
<details>
|
||||
<summary>Extra ファイル設定</summary>
|
||||
|
||||
ここで指定したように、通常の結果の前後に追加される追加タグのセットを指定します。一般的に使用される品質タグ (`masterpiece, best quality,` など) のような小さなカスタムタグセットに便利です。
|
||||
|
||||
長いプリセットやプロンプト全体を補完したい場合は、代わりに [Chants(詠唱)](#chants詠唱) を参照してください。
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Chants -->
|
||||
<details>
|
||||
<summary>Chant ファイル名</summary>
|
||||
|
||||
Chantとは、長いプリセット、あるいはプロンプト全体を一度に選択して挿入できるもので、Webuiに内蔵されているスタイルのドロップダウンに似ています。Chantにはいくつかの追加機能があり、より速く使用することができます。
|
||||
|
||||
詳しくは上記の[Chants(詠唱)](#chants詠唱)のセクションを参照してください。
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Hotkeys -->
|
||||
<details>
|
||||
<summary>Hotkeys</summary>
|
||||
|
||||
ほとんどのキーボードナビゲーション機能のホットキーをここで指定できます。
|
||||
https://www.w3.org/TR/uievents-key/#named-key-attribute-value
|
||||
|
||||
機能の説明
|
||||
- Move Up / Down:次のタグを選択
|
||||
- Jump Up / Down:一度に5箇所移動する。
|
||||
- Jump to Start / End: リストの先頭または末尾にジャンプ
|
||||
- ChooseSelected ハイライトされたタグを選択するか、何も選択されていない場合はポップアップを閉じます。
|
||||
- ChooseSelectedOrFirst:上記と同じですが、何も選択されていない場合、デフォルトで最初の結果が選択されます。
|
||||
- Close ポップアップを閉じる
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Colors -->
|
||||
<details>
|
||||
<summary>Colors</summary>
|
||||
|
||||
ここでは、異なるタグカテゴリーに使用されるデフォルトの色を変更することができます。これらは、ソースサイトのカテゴリの色に似ているように選択されています。
|
||||
|
||||
フォーマットは標準的なJSON
|
||||
- オブジェクト名は、タグのファイル名に対応しています。
|
||||
- 数字はタグの種類を表し、タグのソースに依存します。詳細については、[CSV tag data](#csv-tag-data)を参照してください。
|
||||
- 角括弧内の最初の値はダークモード、2番目の値はライトモードです。HTMLの色名と16進数コードのどちらでも使えます。
|
||||
|
||||
これは、カスタムタグ・ファイルに新しいカラーセットを追加するためにも使用できます。
|
||||
|
||||

|
||||
</details>
|
||||
<!-- Temp files refresh -->
|
||||
<details>
|
||||
<summary>TACの一時作成ファイルのリフレッシュ</summary>
|
||||
|
||||
これは "フェイク"設定で、実際には何も設定しません。むしろ、開発者がwebuiのオプションに追加できる更新ボタンを悪用するための小さなハックです。この設定の隣にある更新ボタンをクリックすると、タグオートコンプリートにいくつかの一時的な内部ファイルを再作成・再読み込みさせます。
|
||||
|
||||
タグオートコンプリートは様々な機能、特に余分なネットワークとワイルドカード補完に関連するこれらのファイルに依存しています。この設定は、例えば新しいLoRAをいくつかフォルダに追加し、タグ・オートコンプリートにリストを表示させるためにUIを再起動したくない場合に、リストを再構築するために使用できます。
|
||||
|
||||
また、この設定をクイック設定バーに追加することで、いつでも更新ボタンを利用できるようになります。
|
||||
|
||||

|
||||
</details>
|
||||
<br/>
|
||||
|
||||
# 翻訳
|
||||
タグとエイリアスの両方を翻訳するために使用することができ、また翻訳による検索を可能にするための、追加のファイルを翻訳セクションに追加することができます。
|
||||
このファイルは、`<英語のタグ/エイリアス>,<翻訳>`という形式のCSVである必要がありますが、3列のフォーマットを使用する古いファイルとの後方互換性のために、`oldFormat`をオンにすると代わりにそれを使うことができます。
|
||||
このファイルは、`<English tag/alias>,<Translation>`という形式のCSVである必要がありますが、3列のフォーマットを使用する古いファイルとの後方互換性のために、`oldFormat`をオンにすると、代わりに新しい2列の翻訳形式ではなく、古い3列の翻訳形式を使用するようになります。
|
||||
その場合、2番目のカラムは使用されず、パース時にスキップされます。
|
||||
|
||||
中国語の翻訳例:
|
||||
|
||||

|
||||

|
||||
|
||||
#### Extra file
|
||||
## 翻訳リスト
|
||||
- [🇨🇳 中国語訳](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/discussions/23) by @HalfMAI, 最も一般的なタグを機械翻訳と手作業で修正(レガシーフォーマットを使用)
|
||||
- [🇨🇳 中国語訳](https://github.com/sgmklp/tag-for-autocompletion-with-translation) by @sgmklp, [こちら](https://github.com/zcyzcy88/TagTable)をベースにして、より小さくした手動での翻訳セット。
|
||||
- [🇯🇵 日本語訳](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/discussions/265) by @applemango, 機械翻訳と人力翻訳の両方が利用可能。
|
||||
|
||||
> ### 🫵 あなたの助けが必要です!
|
||||
> 翻訳はコミュニティの努力により支えられています。もしあなたがタグファイルを翻訳したことがある場合、または作成したい場合は、あなたの成果をここに追加できるように、Pull RequestまたはIssueを開いてください。
|
||||
> 機械翻訳は、最も一般的なタグであっても、多くのことを間違えてしまいます。
|
||||
|
||||
## ライブ・プレビュー
|
||||
> ⚠️ 警告:
|
||||
>
|
||||
> この機能はまだ実験的なもので、使用中にバグに遭遇するかもしれません。
|
||||
|
||||
この機能はプロンプト内のすべての検出されたタグのライブプレビューを表示します。検出されたタグは、カンマで正しく区切られたものと長い文章の中にあるものの両方が表示されます。自然な文章では3単語まで検出することができ、1つのタグよりも複数単語の完全な一致を優先します。
|
||||
|
||||
検出されたタグの上には翻訳ファイルからの訳文が表示されるので、英語のタグの意味がよく分からない場合でも、プロンプトにタグが挿入された後でも(完了時のポップアップではなく)簡単に見つけることができます。
|
||||
|
||||
このオプションはデフォルトではオフになっていますが、翻訳ファイルを選択し、「Show live tag translation below prompt」をチェックすることで有効にすることができます。
|
||||
オフでも通常の機能には影響しません。
|
||||
|
||||
中国語翻訳時の例:
|
||||
|
||||

|
||||
|
||||
検出されたタグをクリックすると、そのタグがプロンプトで選択され、素早く編集できます。
|
||||
|
||||

|
||||
|
||||
#### ⚠️ ライブ翻訳に関する確認されている問題:
|
||||
ユーザーがテキストを入力または貼り付けると翻訳が更新されますが、プログラムによる操作(スタイルの適用やPNG Info / Image Browserからの読み込みなど)では更新されません。これは、プログラムによる編集の後に手動で何かを入力することで回避できます。
|
||||
|
||||
# Extra file
|
||||
エクストラファイルは、メインセットに含まれない新しいタグやカスタムタグを追加するために使用されます。
|
||||
[CSV tag data](#csv-tag-data)にある通常のタグのフォーマットと同じですが、ひとつだけ例外があります:
|
||||
カスタムタグにはカウントがないため、3列目(0から数える場合は2列目)はタグの横に表示される灰色のメタテキストに使用されます。
|
||||
@@ -213,7 +453,7 @@ Booruのサイトのように、タグは1つまたは複数のエイリアス
|
||||
|
||||
カスタムタグを通常のタグの前に追加するか、後に追加するかは、設定で選択することができます。
|
||||
|
||||
## CSV tag data
|
||||
# CSV tag data
|
||||
このスクリプトは、以下の方法で保存されたタグ付きCSVファイルを想定しています:
|
||||
```csv
|
||||
<name>,<type>,<postCount>,"<aliases>"
|
||||
@@ -252,3 +492,32 @@ commentary_request,5,2610959,
|
||||
|8 | Lore |
|
||||
|
||||
タグの種類は、結果の一覧のエントリーの色付けに使用されます。
|
||||
|
||||
## ⚠️ よくある問題、また現在確認されている問題:
|
||||
- お使いのブラウザの設定によっては、古いバージョンのスクリプトがキャッシュされることがあります。例えば、アップデート後に新機能が表示されない場合は、キャッシュを使わずにサイトを強制的にリロードするために、
|
||||
<kbd>CTRL</kbd> + <kbd>F5</kbd>
|
||||
を試してください。
|
||||
- プロンプトのポップアップが壊れたスタイルで表示されるか、全く表示されない場合([このような場合](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/7bbfdd54-fc23-4bfc-85af-24704b139b3a))、openpose-editor 拡張機能がインストールされている場合は更新してください。古いバージョンでは他の拡張機能との間で問題が生じることが知られています。
|
||||
|
||||
<!-- 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
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
你可以按照[以下方法](#installation)下载或拷贝文件,也可以使用[Releases](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)中打包好的文件。
|
||||
|
||||
## 常见问题 & 已知缺陷:
|
||||
- 很多中国用户都报告过此扩展名和其他扩展名的 JavaScript 文件被阻止的问题。
|
||||
常见的罪魁祸首是 IDM / Internet Download Manager 浏览器插件,它似乎出于安全目的阻止了本地文件请求。
|
||||
如果您安装了 IDM,请确保在使用 webui 时禁用以下插件:
|
||||
|
||||

|
||||
|
||||
- 当`replaceUnderscores`选项开启时, 脚本只会替换Tag的一部分如果Tag包含多个单词,比如将`atago (azur lane)`修改`atago`为`taihou`并使用自动补全时.会得到 `taihou (azur lane), lane)`的结果, 因为脚本没有把后面的部分认为成同一个Tag。
|
||||
|
||||
## 演示与截图
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Core components
|
||||
var TAC_CFG = null;
|
||||
var tagBasePath = "";
|
||||
var modelKeywordPath = "";
|
||||
var tacSelfTrigger = false;
|
||||
|
||||
// Tag completion data loaded from files
|
||||
var allTags = [];
|
||||
@@ -10,11 +12,14 @@ var extras = [];
|
||||
var wildcardFiles = [];
|
||||
var wildcardExtFiles = [];
|
||||
var yamlWildcards = [];
|
||||
var umiWildcards = [];
|
||||
var embeddings = [];
|
||||
var hypernetworks = [];
|
||||
var loras = [];
|
||||
var lycos = [];
|
||||
var modelKeywordDict = new Map();
|
||||
var chants = [];
|
||||
var styleNames = [];
|
||||
|
||||
// Selected model info for black/whitelisting
|
||||
var currentModelHash = "";
|
||||
@@ -33,6 +38,13 @@ let hideBlocked = false;
|
||||
// Tag selection for keyboard navigation
|
||||
var selectedTag = null;
|
||||
var oldSelectedTag = null;
|
||||
var resultCountBeforeNormalTags = 0;
|
||||
|
||||
// Lora keyword undo/redo history
|
||||
var textBeforeKeywordInsertion = "";
|
||||
var textAfterKeywordInsertion = "";
|
||||
var lastEditWasKeywordInsertion = false;
|
||||
var keywordInsertionUndone = false;
|
||||
|
||||
// UMI
|
||||
var umiPreviousTags = [];
|
||||
|
||||
@@ -8,10 +8,12 @@ const ResultType = Object.freeze({
|
||||
"wildcardTag": 4,
|
||||
"wildcardFile": 5,
|
||||
"yamlWildcard": 6,
|
||||
"hypernetwork": 7,
|
||||
"lora": 8,
|
||||
"lyco": 9,
|
||||
"chant": 10
|
||||
"umiWildcard": 7,
|
||||
"hypernetwork": 8,
|
||||
"lora": 9,
|
||||
"lyco": 10,
|
||||
"chant": 11,
|
||||
"styleName": 12
|
||||
});
|
||||
|
||||
// Class to hold result data and annotations to make it clearer to use
|
||||
@@ -22,9 +24,12 @@ class AutocompleteResult {
|
||||
|
||||
// Additional info, only used in some cases
|
||||
category = null;
|
||||
count = null;
|
||||
count = Number.MAX_SAFE_INTEGER;
|
||||
usageBias = null;
|
||||
aliases = null;
|
||||
meta = null;
|
||||
hash = null;
|
||||
sortKey = null;
|
||||
|
||||
// Constructor
|
||||
constructor(text, type) {
|
||||
|
||||
@@ -9,7 +9,11 @@ const core = [
|
||||
"#img2img_prompt > label > textarea",
|
||||
"#txt2img_neg_prompt > label > textarea",
|
||||
"#img2img_neg_prompt > label > textarea",
|
||||
".prompt > label > textarea"
|
||||
".prompt > label > textarea",
|
||||
"#txt2img_edit_style_prompt > label > textarea",
|
||||
"#txt2img_edit_style_neg_prompt > label > textarea",
|
||||
"#img2img_edit_style_prompt > label > textarea",
|
||||
"#img2img_edit_style_neg_prompt > label > textarea"
|
||||
];
|
||||
|
||||
// Third party text area selectors
|
||||
@@ -57,6 +61,31 @@ const thirdParty = {
|
||||
"[id^=MD-i2i][id$=prompt] textarea",
|
||||
"[id^=MD-i2i][id$=prompt] input[type='text']"
|
||||
]
|
||||
},
|
||||
"adetailer-t2i": {
|
||||
"base": "#txt2img_script_container",
|
||||
"hasIds": true,
|
||||
"onDemand": true,
|
||||
"selectors": [
|
||||
"[id^=script_txt2img_adetailer_ad_prompt] textarea",
|
||||
"[id^=script_txt2img_adetailer_ad_negative_prompt] textarea"
|
||||
]
|
||||
},
|
||||
"adetailer-i2i": {
|
||||
"base": "#img2img_script_container",
|
||||
"hasIds": true,
|
||||
"onDemand": true,
|
||||
"selectors": [
|
||||
"[id^=script_img2img_adetailer_ad_prompt] textarea",
|
||||
"[id^=script_img2img_adetailer_ad_negative_prompt] textarea"
|
||||
]
|
||||
},
|
||||
"deepdanbooru-object-recognition": {
|
||||
"base": "#tab_deepdanboru_object_recg_tab",
|
||||
"hasIds": false,
|
||||
"selectors": [
|
||||
"Found tags",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +119,7 @@ function addOnDemandObservers(setupFunction) {
|
||||
|
||||
let base = gradioApp().querySelector(entry.base);
|
||||
if (!base) continue;
|
||||
|
||||
|
||||
let accordions = [...base?.querySelectorAll(".gradio-accordion")];
|
||||
if (!accordions) continue;
|
||||
|
||||
@@ -115,12 +144,12 @@ function addOnDemandObservers(setupFunction) {
|
||||
[...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));
|
||||
@@ -165,4 +194,4 @@ function getTextAreaIdentifier(textArea) {
|
||||
break;
|
||||
}
|
||||
return modifier;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// Utility functions for tag autocomplete
|
||||
|
||||
// Parse the CSV file into a 2D array. Doesn't use regex, so it is very lightweight.
|
||||
// We are ignoring newlines in quote fields since we expect one-line entries and parsing would break for unclosed quotes otherwise
|
||||
function parseCSV(str) {
|
||||
var arr = [];
|
||||
var quote = false; // 'true' means we're inside a quoted field
|
||||
const arr = [];
|
||||
let quote = false; // 'true' means we're inside a quoted field
|
||||
|
||||
// Iterate over each character, keep track of current row and column (of the returned array)
|
||||
for (var row = 0, col = 0, c = 0; c < str.length; c++) {
|
||||
var cc = str[c], nc = str[c + 1]; // Current character, next character
|
||||
for (let row = 0, col = 0, c = 0; c < str.length; c++) {
|
||||
let cc = str[c], nc = str[c+1]; // Current character, next character
|
||||
arr[row] = arr[row] || []; // Create a new row if necessary
|
||||
arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary
|
||||
|
||||
@@ -22,14 +23,12 @@ function parseCSV(str) {
|
||||
// If it's a comma and we're not in a quoted field, move on to the next column
|
||||
if (cc == ',' && !quote) { ++col; continue; }
|
||||
|
||||
// If it's a newline (CRLF) and we're not in a quoted field, skip the next character
|
||||
// and move on to the next row and move to column 0 of that new row
|
||||
if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; }
|
||||
// If it's a newline (CRLF), skip the next character and move on to the next row and move to column 0 of that new row
|
||||
if (cc == '\r' && nc == '\n') { ++row; col = 0; ++c; quote = false; continue; }
|
||||
|
||||
// If it's a newline (LF or CR) and we're not in a quoted field,
|
||||
// move on to the next row and move to column 0 of that new row
|
||||
if (cc == '\n' && !quote) { ++row; col = 0; continue; }
|
||||
if (cc == '\r' && !quote) { ++row; col = 0; continue; }
|
||||
// If it's a newline (LF or CR) move on to the next row and move to column 0 of that new row
|
||||
if (cc == '\n') { ++row; col = 0; quote = false; continue; }
|
||||
if (cc == '\r') { ++row; col = 0; quote = false; continue; }
|
||||
|
||||
// Otherwise, append the current character to the current column
|
||||
arr[row][col] += cc;
|
||||
@@ -41,7 +40,7 @@ function parseCSV(str) {
|
||||
async function readFile(filePath, json = false, cache = false) {
|
||||
if (!cache)
|
||||
filePath += `?${new Date().getTime()}`;
|
||||
|
||||
|
||||
let response = await fetch(`file=${filePath}`);
|
||||
|
||||
if (response.status != 200) {
|
||||
@@ -61,6 +60,92 @@ async function loadCSV(path) {
|
||||
return parseCSV(text);
|
||||
}
|
||||
|
||||
// Fetch API
|
||||
async function fetchTacAPI(url, json = true, cache = false) {
|
||||
if (!cache) {
|
||||
const appendChar = url.includes("?") ? "&" : "?";
|
||||
url += `${appendChar}${new Date().getTime()}`
|
||||
}
|
||||
|
||||
let response = await fetch(url);
|
||||
|
||||
if (response.status != 200) {
|
||||
console.error(`Error fetching API endpoint "${url}": ` + response.status, response.statusText);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (json)
|
||||
return await response.json();
|
||||
else
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
async function postTacAPI(url, body = null) {
|
||||
let response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: body
|
||||
});
|
||||
|
||||
if (response.status != 200) {
|
||||
console.error(`Error posting to API endpoint "${url}": ` + response.status, response.statusText);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function putTacAPI(url, body = null) {
|
||||
let response = await fetch(url, { method: "PUT", body: body });
|
||||
|
||||
if (response.status != 200) {
|
||||
console.error(`Error putting to API endpoint "${url}": ` + response.status, response.statusText);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Extra network preview thumbnails
|
||||
async function getTacExtraNetworkPreviewURL(filename, type) {
|
||||
const previewJSON = await fetchTacAPI(`tacapi/v1/thumb-preview/${filename}?type=${type}`, true, true);
|
||||
if (previewJSON?.url) {
|
||||
const properURL = `sd_extra_networks/thumb?filename=${previewJSON.url}`;
|
||||
if ((await fetch(properURL)).status == 200) {
|
||||
return properURL;
|
||||
} else {
|
||||
// create blob url
|
||||
const blob = await (await fetch(`tacapi/v1/thumb-preview-blob/${filename}?type=${type}`)).blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
lastStyleRefresh = 0;
|
||||
// Refresh style file if needed
|
||||
async function refreshStyleNamesIfChanged() {
|
||||
// Only refresh once per second
|
||||
currentTimestamp = new Date().getTime();
|
||||
if (currentTimestamp - lastStyleRefresh < 1000) return;
|
||||
lastStyleRefresh = currentTimestamp;
|
||||
|
||||
const response = await fetch(`tacapi/v1/refresh-styles-if-changed?${new Date().getTime()}`)
|
||||
if (response.status === 304) {
|
||||
// Not modified
|
||||
} else if (response.status === 200) {
|
||||
// Reload
|
||||
QUEUE_FILE_LOAD.forEach(async fn => {
|
||||
if (fn.toString().includes("styleNames"))
|
||||
await fn.call(null, true);
|
||||
})
|
||||
} else {
|
||||
// Error
|
||||
console.error(`Error refreshing styles.txt: ` + response.status, response.statusText);
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce function to prevent spamming the autocomplete function
|
||||
var dbTimeOut;
|
||||
const debounce = (func, wait = 300) => {
|
||||
@@ -89,6 +174,136 @@ function difference(a, b) {
|
||||
)].reduce((acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), []);
|
||||
}
|
||||
|
||||
// Object flatten function adapted from https://stackoverflow.com/a/61602592
|
||||
// $roots keeps previous parent properties as they will be added as a prefix for each prop.
|
||||
// $sep is just a preference if you want to seperate nested paths other than dot.
|
||||
function flatten(obj, roots = [], sep = ".") {
|
||||
return Object.keys(obj).reduce(
|
||||
(memo, prop) =>
|
||||
Object.assign(
|
||||
// create a new object
|
||||
{},
|
||||
// include previously returned object
|
||||
memo,
|
||||
Object.prototype.toString.call(obj[prop]) === "[object Object]"
|
||||
? // keep working if value is an object
|
||||
flatten(obj[prop], roots.concat([prop]), sep)
|
||||
: // include current prop and value and prefix prop with the roots
|
||||
{ [roots.concat([prop]).join(sep)]: obj[prop] }
|
||||
),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate biased tag score based on post count and frequent usage
|
||||
function calculateUsageBias(result, count, uses) {
|
||||
// Check setting conditions
|
||||
if (uses < TAC_CFG.frequencyMinCount) {
|
||||
uses = 0;
|
||||
} else if (uses != 0) {
|
||||
result.usageBias = true;
|
||||
}
|
||||
|
||||
switch (TAC_CFG.frequencyFunction) {
|
||||
case "Logarithmic (weak)":
|
||||
return Math.log(1 + count) + Math.log(1 + uses);
|
||||
case "Logarithmic (strong)":
|
||||
return Math.log(1 + count) + 2 * Math.log(1 + uses);
|
||||
case "Usage first":
|
||||
return uses;
|
||||
default:
|
||||
return count;
|
||||
}
|
||||
}
|
||||
// Beautify return type for easier parsing
|
||||
function mapUseCountArray(useCounts, posAndNeg = false) {
|
||||
return useCounts.map(useCount => {
|
||||
if (posAndNeg) {
|
||||
return {
|
||||
"name": useCount[0],
|
||||
"type": useCount[1],
|
||||
"count": useCount[2],
|
||||
"negCount": useCount[3],
|
||||
"lastUseDate": useCount[4]
|
||||
}
|
||||
}
|
||||
return {
|
||||
"name": useCount[0],
|
||||
"type": useCount[1],
|
||||
"count": useCount[2],
|
||||
"lastUseDate": useCount[3]
|
||||
}
|
||||
});
|
||||
}
|
||||
// Call API endpoint to increase bias of tag in the database
|
||||
function increaseUseCount(tagName, type, negative = false) {
|
||||
postTacAPI(`tacapi/v1/increase-use-count?tagname=${tagName}&ttype=${type}&neg=${negative}`);
|
||||
}
|
||||
// Get use count of tag from the database
|
||||
async function getUseCount(tagName, type, negative = false) {
|
||||
const response = await fetchTacAPI(`tacapi/v1/get-use-count?tagname=${tagName}&ttype=${type}&neg=${negative}`, true, false);
|
||||
// Guard for no db
|
||||
if (response == null) return null;
|
||||
// Result
|
||||
return response["result"];
|
||||
}
|
||||
async function getUseCounts(tagNames, types, negative = false) {
|
||||
// While semantically weird, we have to use POST here for the body, as urls are limited in length
|
||||
const body = JSON.stringify({"tagNames": tagNames, "tagTypes": types, "neg": negative});
|
||||
const response = await postTacAPI(`tacapi/v1/get-use-count-list`, body)
|
||||
// Guard for no db
|
||||
if (response == null) return null;
|
||||
// Results
|
||||
return mapUseCountArray(response["result"]);
|
||||
}
|
||||
async function getAllUseCounts() {
|
||||
const response = await fetchTacAPI(`tacapi/v1/get-all-use-counts`);
|
||||
// Guard for no db
|
||||
if (response == null) return null;
|
||||
// Results
|
||||
return mapUseCountArray(response["result"], true);
|
||||
}
|
||||
async function resetUseCount(tagName, type, resetPosCount, resetNegCount) {
|
||||
await putTacAPI(`tacapi/v1/reset-use-count?tagname=${tagName}&ttype=${type}&pos=${resetPosCount}&neg=${resetNegCount}`);
|
||||
}
|
||||
|
||||
function createTagUsageTable(tagCounts) {
|
||||
// Create table
|
||||
let tagTable = document.createElement("table");
|
||||
tagTable.innerHTML =
|
||||
`<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Type</td>
|
||||
<td>Count(+)</td>
|
||||
<td>Count(-)</td>
|
||||
<td>Last used</td>
|
||||
</tr>
|
||||
</thead>`;
|
||||
tagTable.id = "tac_tagUsageTable"
|
||||
|
||||
tagCounts.forEach(t => {
|
||||
let tr = document.createElement("tr");
|
||||
|
||||
// Fill values
|
||||
let values = [t.name, t.type-1, t.count, t.negCount, t.lastUseDate]
|
||||
values.forEach(v => {
|
||||
let td = document.createElement("td");
|
||||
td.innerText = v;
|
||||
tr.append(td);
|
||||
});
|
||||
// Add delete/reset button
|
||||
let delButton = document.createElement("button");
|
||||
delButton.innerText = "🗑️";
|
||||
delButton.title = "Reset count";
|
||||
tr.append(delButton);
|
||||
|
||||
tagTable.append(tr)
|
||||
});
|
||||
|
||||
return tagTable;
|
||||
}
|
||||
|
||||
// Sliding window function to get possible combination groups of an array
|
||||
function toNgrams(inputArray, size) {
|
||||
return Array.from(
|
||||
@@ -97,7 +312,11 @@ function toNgrams(inputArray, size) {
|
||||
);
|
||||
}
|
||||
|
||||
function escapeRegExp(string) {
|
||||
function escapeRegExp(string, wildcardMatching = false) {
|
||||
if (wildcardMatching) {
|
||||
// Escape all characters except asterisks and ?, which should be treated separately as placeholders.
|
||||
return string.replace(/[-[\]{}()+.,\\^$|#\s]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.');
|
||||
}
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
function escapeHTML(unsafeText) {
|
||||
@@ -141,6 +360,49 @@ function observeElement(element, property, callback, delay = 0) {
|
||||
}
|
||||
}
|
||||
|
||||
// Sort functions
|
||||
function getSortFunction() {
|
||||
let criterion = TAC_CFG.modelSortOrder || "Name";
|
||||
|
||||
const textSort = (a, b, reverse = false) => {
|
||||
// Assign keys so next sort is faster
|
||||
if (!a.sortKey) {
|
||||
a.sortKey = a.type === ResultType.chant
|
||||
? a.aliases
|
||||
: a.text;
|
||||
}
|
||||
if (!b.sortKey) {
|
||||
b.sortKey = b.type === ResultType.chant
|
||||
? b.aliases
|
||||
: b.text;
|
||||
}
|
||||
|
||||
return reverse ? b.sortKey.localeCompare(a.sortKey) : a.sortKey.localeCompare(b.sortKey);
|
||||
}
|
||||
const numericSort = (a, b, reverse = false) => {
|
||||
const noKey = reverse ? "-1" : Number.MAX_SAFE_INTEGER;
|
||||
let aParsed = parseFloat(a.sortKey || noKey);
|
||||
let bParsed = parseFloat(b.sortKey || noKey);
|
||||
|
||||
if (aParsed === bParsed) {
|
||||
return textSort(a, b, false);
|
||||
}
|
||||
|
||||
return reverse ? bParsed - aParsed : aParsed - bParsed;
|
||||
}
|
||||
|
||||
return (a, b) => {
|
||||
switch (criterion) {
|
||||
case "Date Modified (newest first)":
|
||||
return numericSort(a, b, true);
|
||||
case "Date Modified (oldest first)":
|
||||
return numericSort(a, b, false);
|
||||
default:
|
||||
return textSort(a, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Queue calling function to process global queues
|
||||
async function processQueue(queue, context, ...args) {
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
|
||||
@@ -7,7 +7,10 @@ class ChantParser extends BaseTagParser {
|
||||
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);
|
||||
let filterCondition = x => {
|
||||
let regex = new RegExp(escapeRegExp(searchTerm, true), 'i');
|
||||
return regex.test(x.terms.toLowerCase()) || regex.test(x.name.toLowerCase());
|
||||
};
|
||||
tempResults = chants.filter(x => filterCondition(x)); // Filter by tagword
|
||||
} else {
|
||||
tempResults = chants;
|
||||
@@ -41,7 +44,7 @@ async function load() {
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
if (tagType === ResultType.chant) {
|
||||
return text.replace(/^.*?: /g, "");
|
||||
return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -51,4 +54,4 @@ 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);
|
||||
QUEUE_AFTER_CONFIG_CHANGE.push(load);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const EMB_REGEX = /<(?!l:|h:|c:)[^,> ]*>?/g;
|
||||
const EMB_TRIGGER = () => TAC_CFG.useEmbeddings && tagword.match(EMB_REGEX);
|
||||
const EMB_TRIGGER = () => TAC_CFG.useEmbeddings && (tagword.match(EMB_REGEX) || TAC_CFG.includeEmbeddingsInNormalResults);
|
||||
|
||||
class EmbeddingParser extends BaseTagParser {
|
||||
parse() {
|
||||
@@ -11,12 +11,18 @@ class EmbeddingParser extends BaseTagParser {
|
||||
if (searchTerm.startsWith("v1") || searchTerm.startsWith("v2")) {
|
||||
versionString = searchTerm.slice(0, 2);
|
||||
searchTerm = searchTerm.slice(2);
|
||||
} else if (searchTerm.startsWith("vxl")) {
|
||||
versionString = searchTerm.slice(0, 3);
|
||||
searchTerm = searchTerm.slice(3);
|
||||
}
|
||||
|
||||
let filterCondition = x => x[0].toLowerCase().includes(searchTerm) || x[0].toLowerCase().replaceAll(" ", "_").includes(searchTerm);
|
||||
let filterCondition = x => {
|
||||
let regex = new RegExp(escapeRegExp(searchTerm, true), 'i');
|
||||
return regex.test(x[0].toLowerCase()) || regex.test(x[0].toLowerCase().replaceAll(" ", "_"));
|
||||
};
|
||||
|
||||
if (versionString)
|
||||
tempResults = embeddings.filter(x => filterCondition(x) && x[1] && x[1] === versionString); // Filter by tagword
|
||||
tempResults = embeddings.filter(x => filterCondition(x) && x[2] && x[2].toLowerCase() === versionString.toLowerCase()); // Filter by tagword
|
||||
else
|
||||
tempResults = embeddings.filter(x => filterCondition(x)); // Filter by tagword
|
||||
} else {
|
||||
@@ -26,8 +32,13 @@ class EmbeddingParser extends BaseTagParser {
|
||||
// Add final results
|
||||
let finalResults = [];
|
||||
tempResults.forEach(t => {
|
||||
let result = new AutocompleteResult(t[0].trim(), ResultType.embedding)
|
||||
result.meta = t[1] + " Embedding";
|
||||
let lastDot = t[0].lastIndexOf(".") > -1 ? t[0].lastIndexOf(".") : t[0].length;
|
||||
let lastSlash = t[0].lastIndexOf("/") > -1 ? t[0].lastIndexOf("/") : -1;
|
||||
let name = t[0].trim().substring(lastSlash + 1, lastDot);
|
||||
|
||||
let result = new AutocompleteResult(name, ResultType.embedding)
|
||||
result.sortKey = t[1];
|
||||
result.meta = t[2] + " Embedding";
|
||||
finalResults.push(result);
|
||||
});
|
||||
|
||||
@@ -38,9 +49,9 @@ class EmbeddingParser extends BaseTagParser {
|
||||
async function load() {
|
||||
if (embeddings.length === 0) {
|
||||
try {
|
||||
embeddings = (await readFile(`${tagBasePath}/temp/emb.txt`)).split("\n")
|
||||
.filter(x => x.trim().length > 0) // Remove empty lines
|
||||
.map(x => x.trim().split(",")); // Split into name, version type pairs
|
||||
embeddings = (await loadCSV(`${tagBasePath}/temp/emb.txt`))
|
||||
.filter(x => x[0]?.trim().length > 0) // Remove empty lines
|
||||
.map(x => [x[0].trim(), x[1], x[2]]); // Return name, sortKey, hash tuples
|
||||
} catch (e) {
|
||||
console.error("Error loading embeddings.txt: " + e);
|
||||
}
|
||||
@@ -49,7 +60,7 @@ async function load() {
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
if (tagType === ResultType.embedding) {
|
||||
return text.replace(/^.*?: /g, "");
|
||||
return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -58,4 +69,4 @@ PARSERS.push(new EmbeddingParser(EMB_TRIGGER));
|
||||
|
||||
// Add our utility functions to their respective queues
|
||||
QUEUE_FILE_LOAD.push(load);
|
||||
QUEUE_SANITIZE.push(sanitize);
|
||||
QUEUE_SANITIZE.push(sanitize);
|
||||
|
||||
@@ -7,8 +7,11 @@ class HypernetParser extends BaseTagParser {
|
||||
let tempResults = [];
|
||||
if (tagword !== "<" && tagword !== "<h:" && tagword !== "<hypernet:") {
|
||||
let searchTerm = tagword.replace("<hypernet:", "").replace("<h:", "").replace("<", "");
|
||||
let filterCondition = x => x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm);
|
||||
tempResults = hypernetworks.filter(x => filterCondition(x)); // Filter by tagword
|
||||
let filterCondition = x => {
|
||||
let regex = new RegExp(escapeRegExp(searchTerm, true), 'i');
|
||||
return regex.test(x.toLowerCase()) || regex.test(x.toLowerCase().replaceAll(" ", "_"));
|
||||
};
|
||||
tempResults = hypernetworks.filter(x => filterCondition(x[0])); // Filter by tagword
|
||||
} else {
|
||||
tempResults = hypernetworks;
|
||||
}
|
||||
@@ -16,8 +19,9 @@ class HypernetParser extends BaseTagParser {
|
||||
// Add final results
|
||||
let finalResults = [];
|
||||
tempResults.forEach(t => {
|
||||
let result = new AutocompleteResult(t.trim(), ResultType.hypernetwork)
|
||||
let result = new AutocompleteResult(t[0].trim(), ResultType.hypernetwork)
|
||||
result.meta = "Hypernetwork";
|
||||
result.sortKey = t[1];
|
||||
finalResults.push(result);
|
||||
});
|
||||
|
||||
@@ -28,9 +32,9 @@ class HypernetParser extends BaseTagParser {
|
||||
async function load() {
|
||||
if (hypernetworks.length === 0) {
|
||||
try {
|
||||
hypernetworks = (await readFile(`${tagBasePath}/temp/hyp.txt`)).split("\n")
|
||||
.filter(x => x.trim().length > 0) //Remove empty lines
|
||||
.map(x => x.trim()); // Remove carriage returns and padding if it exists
|
||||
hypernetworks = (await loadCSV(`${tagBasePath}/temp/hyp.txt`))
|
||||
.filter(x => x[0]?.trim().length > 0) //Remove empty lines
|
||||
.map(x => [x[0]?.trim(), x[1]]); // Remove carriage returns and padding if it exists
|
||||
} catch (e) {
|
||||
console.error("Error loading hypernetworks.txt: " + e);
|
||||
}
|
||||
@@ -48,4 +52,4 @@ PARSERS.push(new HypernetParser(HYP_TRIGGER));
|
||||
|
||||
// Add our utility functions to their respective queues
|
||||
QUEUE_FILE_LOAD.push(load);
|
||||
QUEUE_SANITIZE.push(sanitize);
|
||||
QUEUE_SANITIZE.push(sanitize);
|
||||
|
||||
@@ -7,8 +7,11 @@ class LoraParser extends BaseTagParser {
|
||||
let tempResults = [];
|
||||
if (tagword !== "<" && tagword !== "<l:" && tagword !== "<lora:") {
|
||||
let searchTerm = tagword.replace("<lora:", "").replace("<l:", "").replace("<", "");
|
||||
let filterCondition = x => x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm);
|
||||
tempResults = loras.filter(x => filterCondition(x)); // Filter by tagword
|
||||
let filterCondition = x => {
|
||||
let regex = new RegExp(escapeRegExp(searchTerm, true), 'i');
|
||||
return regex.test(x.toLowerCase()) || regex.test(x.toLowerCase().replaceAll(" ", "_"));
|
||||
};
|
||||
tempResults = loras.filter(x => filterCondition(x[0])); // Filter by tagword
|
||||
} else {
|
||||
tempResults = loras;
|
||||
}
|
||||
@@ -16,8 +19,15 @@ class LoraParser extends BaseTagParser {
|
||||
// Add final results
|
||||
let finalResults = [];
|
||||
tempResults.forEach(t => {
|
||||
let result = new AutocompleteResult(t.trim(), ResultType.lora)
|
||||
const text = t[0].trim();
|
||||
let lastDot = text.lastIndexOf(".") > -1 ? text.lastIndexOf(".") : text.length;
|
||||
let lastSlash = text.lastIndexOf("/") > -1 ? text.lastIndexOf("/") : -1;
|
||||
let name = text.substring(lastSlash + 1, lastDot);
|
||||
|
||||
let result = new AutocompleteResult(name, ResultType.lora)
|
||||
result.meta = "Lora";
|
||||
result.sortKey = t[1];
|
||||
result.hash = t[2];
|
||||
finalResults.push(result);
|
||||
});
|
||||
|
||||
@@ -28,18 +38,24 @@ class LoraParser extends BaseTagParser {
|
||||
async function load() {
|
||||
if (loras.length === 0) {
|
||||
try {
|
||||
loras = (await readFile(`${tagBasePath}/temp/lora.txt`)).split("\n")
|
||||
.filter(x => x.trim().length > 0) // Remove empty lines
|
||||
.map(x => x.trim()); // Remove carriage returns and padding if it exists
|
||||
loras = (await loadCSV(`${tagBasePath}/temp/lora.txt`))
|
||||
.filter(x => x[0]?.trim().length > 0) // Remove empty lines
|
||||
.map(x => [x[0]?.trim(), x[1], x[2]]); // Trim filenames and return the name, sortKey, hash pairs
|
||||
} catch (e) {
|
||||
console.error("Error loading lora.txt: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
async function sanitize(tagType, text) {
|
||||
if (tagType === ResultType.lora) {
|
||||
return `<lora:${text}:${TAC_CFG.extraNetworksDefaultMultiplier}>`;
|
||||
let multiplier = TAC_CFG.extraNetworksDefaultMultiplier;
|
||||
let info = await fetchTacAPI(`tacapi/v1/lora-info/${text}`)
|
||||
if (info && info["preferred weight"]) {
|
||||
multiplier = info["preferred weight"];
|
||||
}
|
||||
|
||||
return `<lora:${text}:${multiplier}>`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -48,4 +64,4 @@ PARSERS.push(new LoraParser(LORA_TRIGGER));
|
||||
|
||||
// Add our utility functions to their respective queues
|
||||
QUEUE_FILE_LOAD.push(load);
|
||||
QUEUE_SANITIZE.push(sanitize);
|
||||
QUEUE_SANITIZE.push(sanitize);
|
||||
|
||||
@@ -5,10 +5,13 @@ class LycoParser extends BaseTagParser {
|
||||
parse() {
|
||||
// Show lyco
|
||||
let tempResults = [];
|
||||
if (tagword !== "<" && tagword !== "<l:" && tagword !== "<lyco:") {
|
||||
let searchTerm = tagword.replace("<lyco:", "").replace("<l:", "").replace("<", "");
|
||||
let filterCondition = x => x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm);
|
||||
tempResults = lycos.filter(x => filterCondition(x)); // Filter by tagword
|
||||
if (tagword !== "<" && tagword !== "<l:" && tagword !== "<lyco:" && tagword !== "<lora:") {
|
||||
let searchTerm = tagword.replace("<lyco:", "").replace("<lora:", "").replace("<l:", "").replace("<", "");
|
||||
let filterCondition = x => {
|
||||
let regex = new RegExp(escapeRegExp(searchTerm, true), 'i');
|
||||
return regex.test(x.toLowerCase()) || regex.test(x.toLowerCase().replaceAll(" ", "_"));
|
||||
};
|
||||
tempResults = lycos.filter(x => filterCondition(x[0])); // Filter by tagword
|
||||
} else {
|
||||
tempResults = lycos;
|
||||
}
|
||||
@@ -16,8 +19,15 @@ class LycoParser extends BaseTagParser {
|
||||
// Add final results
|
||||
let finalResults = [];
|
||||
tempResults.forEach(t => {
|
||||
let result = new AutocompleteResult(t.trim(), ResultType.lyco)
|
||||
const text = t[0].trim();
|
||||
let lastDot = text.lastIndexOf(".") > -1 ? text.lastIndexOf(".") : text.length;
|
||||
let lastSlash = text.lastIndexOf("/") > -1 ? text.lastIndexOf("/") : -1;
|
||||
let name = text.substring(lastSlash + 1, lastDot);
|
||||
|
||||
let result = new AutocompleteResult(name, ResultType.lyco)
|
||||
result.meta = "Lyco";
|
||||
result.sortKey = t[1];
|
||||
result.hash = t[2];
|
||||
finalResults.push(result);
|
||||
});
|
||||
|
||||
@@ -28,18 +38,25 @@ class LycoParser extends BaseTagParser {
|
||||
async function load() {
|
||||
if (lycos.length === 0) {
|
||||
try {
|
||||
lycos = (await readFile(`${tagBasePath}/temp/lyco.txt`)).split("\n")
|
||||
.filter(x => x.trim().length > 0) // Remove empty lines
|
||||
.map(x => x.trim()); // Remove carriage returns and padding if it exists
|
||||
lycos = (await loadCSV(`${tagBasePath}/temp/lyco.txt`))
|
||||
.filter(x => x[0]?.trim().length > 0) // Remove empty lines
|
||||
.map(x => [x[0]?.trim(), x[1], x[2]]); // Trim filenames and return the name, sortKey, hash pairs
|
||||
} catch (e) {
|
||||
console.error("Error loading lyco.txt: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
async function sanitize(tagType, text) {
|
||||
if (tagType === ResultType.lyco) {
|
||||
return `<lyco:${text}:${TAC_CFG.extraNetworksDefaultMultiplier}>`;
|
||||
let multiplier = TAC_CFG.extraNetworksDefaultMultiplier;
|
||||
let info = await fetchTacAPI(`tacapi/v1/lyco-info/${text}`)
|
||||
if (info && info["preferred weight"]) {
|
||||
multiplier = info["preferred weight"];
|
||||
}
|
||||
|
||||
let prefix = TAC_CFG.useLoraPrefixForLycos ? "lora" : "lyco";
|
||||
return `<${prefix}:${text}:${multiplier}>`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -48,4 +65,4 @@ PARSERS.push(new LycoParser(LYCO_TRIGGER));
|
||||
|
||||
// Add our utility functions to their respective queues
|
||||
QUEUE_FILE_LOAD.push(load);
|
||||
QUEUE_SANITIZE.push(sanitize);
|
||||
QUEUE_SANITIZE.push(sanitize);
|
||||
|
||||
42
javascript/ext_modelKeyword.js
Normal file
42
javascript/ext_modelKeyword.js
Normal file
@@ -0,0 +1,42 @@
|
||||
async function load() {
|
||||
let modelKeywordParts = (await readFile(`tmp/modelKeywordPath.txt`)).split(",")
|
||||
modelKeywordPath = modelKeywordParts[0];
|
||||
let customFileExists = modelKeywordParts[1] === "True";
|
||||
|
||||
if (modelKeywordPath.length > 0 && modelKeywordDict.size === 0) {
|
||||
try {
|
||||
let csv_lines = [];
|
||||
// Only add default keywords if wanted by the user
|
||||
if (TAC_CFG.modelKeywordCompletion !== "Only user list")
|
||||
csv_lines = (await loadCSV(`${modelKeywordPath}/lora-keyword.txt`));
|
||||
// Add custom user keywords if the file exists
|
||||
if (customFileExists)
|
||||
csv_lines = csv_lines.concat((await loadCSV(`${modelKeywordPath}/lora-keyword-user.txt`)));
|
||||
|
||||
if (csv_lines.length === 0) return;
|
||||
|
||||
csv_lines = csv_lines.filter(x => x[0].trim().length > 0 && x[0].trim()[0] !== "#") // Remove empty lines and comments
|
||||
|
||||
// Add to the dict
|
||||
csv_lines.forEach(parts => {
|
||||
const hash = parts[0];
|
||||
const keywords = parts[1]?.replaceAll("| ", ", ")?.replaceAll("|", ", ")?.trim();
|
||||
const lastSepIndex = parts[2]?.lastIndexOf("/") + 1 || parts[2]?.lastIndexOf("\\") + 1 || 0;
|
||||
const name = parts[2]?.substring(lastSepIndex).trim() || "none"
|
||||
|
||||
if (modelKeywordDict.has(hash) && name !== "none") {
|
||||
// Add a new name key if the hash already exists
|
||||
modelKeywordDict.get(hash).set(name, keywords);
|
||||
} else {
|
||||
// Create new hash entry
|
||||
let map = new Map().set(name, keywords);
|
||||
modelKeywordDict.set(hash, map);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error loading model-keywords list: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QUEUE_FILE_LOAD.push(load);
|
||||
70
javascript/ext_styles.js
Normal file
70
javascript/ext_styles.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const STYLE_REGEX = /(\$(\d*)\(?)[^$|\[\],\s]*\)?/;
|
||||
const STYLE_TRIGGER = () => TAC_CFG.useStyleVars && tagword.match(STYLE_REGEX);
|
||||
|
||||
var lastStyleVarIndex = "";
|
||||
|
||||
class StyleParser extends BaseTagParser {
|
||||
async parse() {
|
||||
// Refresh if needed
|
||||
await refreshStyleNamesIfChanged();
|
||||
|
||||
// Show styles
|
||||
let tempResults = [];
|
||||
let matchGroups = tagword.match(STYLE_REGEX);
|
||||
|
||||
// Save index to insert again later or clear last one
|
||||
lastStyleVarIndex = matchGroups[2] ? matchGroups[2] : "";
|
||||
|
||||
if (tagword !== matchGroups[1]) {
|
||||
let searchTerm = tagword.replace(matchGroups[1], "");
|
||||
|
||||
let filterCondition = x => {
|
||||
let regex = new RegExp(escapeRegExp(searchTerm, true), 'i');
|
||||
return regex.test(x[0].toLowerCase()) || regex.test(x[0].toLowerCase().replaceAll(" ", "_"));
|
||||
};
|
||||
tempResults = styleNames.filter(x => filterCondition(x)); // Filter by tagword
|
||||
} else {
|
||||
tempResults = styleNames;
|
||||
}
|
||||
|
||||
// Add final results
|
||||
let finalResults = [];
|
||||
tempResults.forEach(t => {
|
||||
let result = new AutocompleteResult(t[0].trim(), ResultType.styleName)
|
||||
result.meta = "Style";
|
||||
finalResults.push(result);
|
||||
});
|
||||
|
||||
return finalResults;
|
||||
}
|
||||
}
|
||||
|
||||
async function load(force = false) {
|
||||
if (styleNames.length === 0 || force) {
|
||||
try {
|
||||
styleNames = (await loadCSV(`${tagBasePath}/temp/styles.txt`))
|
||||
.filter(x => x[0]?.trim().length > 0) // Remove empty lines
|
||||
.filter(x => x[0] !== "None") // Remove "None" style
|
||||
.map(x => [x[0].trim()]); // Trim name
|
||||
} catch (e) {
|
||||
console.error("Error loading styles.txt: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
if (tagType === ResultType.styleName) {
|
||||
if (text.includes(" ")) {
|
||||
return `$${lastStyleVarIndex}(${text})`;
|
||||
} else {
|
||||
return`$${lastStyleVarIndex}${text}`
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
PARSERS.push(new StyleParser(STYLE_TRIGGER));
|
||||
|
||||
// Add our utility functions to their respective queues
|
||||
QUEUE_FILE_LOAD.push(load);
|
||||
QUEUE_SANITIZE.push(sanitize);
|
||||
@@ -7,7 +7,7 @@ class UmiParser extends BaseTagParser {
|
||||
parse(textArea, prompt) {
|
||||
// We are in a UMI yaml tag definition, parse further
|
||||
let umiSubPrompts = [...prompt.matchAll(UMI_PROMPT_REGEX)];
|
||||
|
||||
|
||||
let umiTags = [];
|
||||
let umiTagsWithOperators = []
|
||||
|
||||
@@ -15,7 +15,7 @@ class UmiParser extends BaseTagParser {
|
||||
|
||||
umiSubPrompts.forEach(umiSubPrompt => {
|
||||
umiTags = umiTags.concat([...umiSubPrompt[0].matchAll(UMI_TAG_REGEX)].map(x => x[1].toLowerCase()));
|
||||
|
||||
|
||||
const start = umiSubPrompt.index;
|
||||
const end = umiSubPrompt.index + umiSubPrompt[0].length;
|
||||
if (textArea.selectionStart >= start && textArea.selectionStart <= end) {
|
||||
@@ -74,7 +74,7 @@ class UmiParser extends BaseTagParser {
|
||||
//console.log({ matches })
|
||||
|
||||
const filteredWildcards = (tagword) => {
|
||||
const wildcards = yamlWildcards.filter(x => {
|
||||
const wildcards = umiWildcards.filter(x => {
|
||||
let tags = x[1];
|
||||
const matchesNeg =
|
||||
matches.negative.length === 0
|
||||
@@ -113,7 +113,7 @@ class UmiParser extends BaseTagParser {
|
||||
|| !matches.all.includes(x[0])
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (umiTags.length > 0) {
|
||||
// Get difference for subprompt
|
||||
let tagCountChange = umiTags.length - umiPreviousTags.length;
|
||||
@@ -129,7 +129,7 @@ class UmiParser extends BaseTagParser {
|
||||
return;
|
||||
}
|
||||
|
||||
let umiTagword = diff[0] || '';
|
||||
let umiTagword = tagCountChange < 0 ? '' : diff[0] || '';
|
||||
let tempResults = [];
|
||||
if (umiTagword && umiTagword.length > 0) {
|
||||
umiTagword = umiTagword.toLowerCase().replace(/[\n\r]/g, "");
|
||||
@@ -144,48 +144,53 @@ class UmiParser extends BaseTagParser {
|
||||
// Add final results
|
||||
let finalResults = [];
|
||||
tempResults.forEach(t => {
|
||||
let result = new AutocompleteResult(t[0].trim(), ResultType.yamlWildcard)
|
||||
let result = new AutocompleteResult(t[0].trim(), ResultType.umiWildcard)
|
||||
result.count = t[1];
|
||||
finalResults.push(result);
|
||||
});
|
||||
|
||||
finalResults = finalResults.sort((a, b) => b.count - a.count);
|
||||
return finalResults;
|
||||
} else if (showAll) {
|
||||
let filteredWildcardsSorted = filteredWildcards("");
|
||||
|
||||
|
||||
// Add final results
|
||||
let finalResults = [];
|
||||
filteredWildcardsSorted.forEach(t => {
|
||||
let result = new AutocompleteResult(t[0].trim(), ResultType.yamlWildcard)
|
||||
let result = new AutocompleteResult(t[0].trim(), ResultType.umiWildcard)
|
||||
result.count = t[1];
|
||||
finalResults.push(result);
|
||||
});
|
||||
|
||||
|
||||
originalTagword = tagword;
|
||||
tagword = "";
|
||||
|
||||
finalResults = finalResults.sort((a, b) => b.count - a.count);
|
||||
return finalResults;
|
||||
}
|
||||
} else {
|
||||
let filteredWildcardsSorted = filteredWildcards("");
|
||||
|
||||
|
||||
// Add final results
|
||||
let finalResults = [];
|
||||
filteredWildcardsSorted.forEach(t => {
|
||||
let result = new AutocompleteResult(t[0].trim(), ResultType.yamlWildcard)
|
||||
let result = new AutocompleteResult(t[0].trim(), ResultType.umiWildcard)
|
||||
result.count = t[1];
|
||||
finalResults.push(result);
|
||||
});
|
||||
|
||||
originalTagword = tagword;
|
||||
tagword = "";
|
||||
|
||||
finalResults = finalResults.sort((a, b) => b.count - a.count);
|
||||
return finalResults;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateUmiTags( tagType, sanitizedText, newPrompt, textArea) {
|
||||
// If it was a yaml wildcard, also update the umiPreviousTags
|
||||
if (tagType === ResultType.yamlWildcard && originalTagword.length > 0) {
|
||||
function updateUmiTags(tagType, sanitizedText, newPrompt, textArea) {
|
||||
// If it was a umi wildcard, also update the umiPreviousTags
|
||||
if (tagType === ResultType.umiWildcard && originalTagword.length > 0) {
|
||||
let umiSubPrompts = [...newPrompt.matchAll(UMI_PROMPT_REGEX)];
|
||||
|
||||
let umiTags = [];
|
||||
@@ -203,11 +208,11 @@ function updateUmiTags( tagType, sanitizedText, newPrompt, textArea) {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (yamlWildcards.length === 0) {
|
||||
if (umiWildcards.length === 0) {
|
||||
try {
|
||||
let yamlTags = (await readFile(`${tagBasePath}/temp/wcet.txt`)).split("\n");
|
||||
let umiTags = (await readFile(`${tagBasePath}/temp/umi_tags.txt`)).split("\n");
|
||||
// Split into tag, count pairs
|
||||
yamlWildcards = yamlTags.map(x => x
|
||||
umiWildcards = umiTags.map(x => x
|
||||
.trim()
|
||||
.split(","))
|
||||
.map(([i, ...rest]) => [
|
||||
@@ -218,14 +223,14 @@ async function load() {
|
||||
}, {}),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.error("Error loading yaml wildcards: " + e);
|
||||
console.error("Error loading umi wildcards: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
// Replace underscores only if the yaml tag is not using them
|
||||
if (tagType === ResultType.yamlWildcard && !yamlWildcards.includes(text)) {
|
||||
// Replace underscores only if the umi tag is not using them
|
||||
if (tagType === ResultType.umiWildcard && !umiWildcards.includes(text)) {
|
||||
return text.replaceAll("_", " ");
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -1,24 +1,52 @@
|
||||
// Regex
|
||||
const WC_REGEX = /\b__([^,]+)__([^, ]*)\b/g;
|
||||
const WC_REGEX = new RegExp(/__([^,]+)__([^, ]*)/g);
|
||||
|
||||
// Trigger conditions
|
||||
const WC_TRIGGER = () => TAC_CFG.useWildcards && [...tagword.matchAll(WC_REGEX)].length > 0;
|
||||
const WC_FILE_TRIGGER = () => TAC_CFG.useWildcards && (tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__");
|
||||
const WC_TRIGGER = () => TAC_CFG.useWildcards && [...tagword.matchAll(new RegExp(WC_REGEX.source.replaceAll("__", escapeRegExp(TAC_CFG.wcWrap)), "g"))].length > 0;
|
||||
const WC_FILE_TRIGGER = () => TAC_CFG.useWildcards && (tagword.startsWith(TAC_CFG.wcWrap) && !tagword.endsWith(TAC_CFG.wcWrap) || tagword === TAC_CFG.wcWrap);
|
||||
|
||||
class WildcardParser extends BaseTagParser {
|
||||
async parse() {
|
||||
// Show wildcards from a file with that name
|
||||
let wcMatch = [...tagword.matchAll(WC_REGEX)]
|
||||
let wcMatch = [...tagword.matchAll(new RegExp(WC_REGEX.source.replaceAll("__", escapeRegExp(TAC_CFG.wcWrap)), "g"))];
|
||||
let wcFile = wcMatch[0][1];
|
||||
let wcWord = wcMatch[0][2];
|
||||
|
||||
// Look in normal wildcard files
|
||||
let wcFound = wildcardFiles.find(x => x[1].toLowerCase() === wcFile);
|
||||
let wcFound = wildcardFiles.filter(x => x[1].toLowerCase() === wcFile);
|
||||
if (wcFound.length === 0) wcFound = null;
|
||||
// Use found wildcard file or look in external wildcard files
|
||||
let wcPair = wcFound || wildcardExtFiles.find(x => x[1].toLowerCase() === wcFile);
|
||||
let wcPairs = wcFound || wildcardExtFiles.filter(x => x[1].toLowerCase() === wcFile);
|
||||
|
||||
let wildcards = (await readFile(`${wcPair[0]}/${wcPair[1]}.txt`)).split("\n")
|
||||
.filter(x => x.trim().length > 0 && !x.startsWith('#')); // Remove empty lines and comments
|
||||
if (!wcPairs) return [];
|
||||
|
||||
let wildcards = [];
|
||||
for (let i = 0; i < wcPairs.length; i++) {
|
||||
const basePath = wcPairs[i][0];
|
||||
const fileName = wcPairs[i][1];
|
||||
if (!basePath || !fileName) return;
|
||||
|
||||
// YAML wildcards are already loaded as json, so we can get the values directly.
|
||||
// basePath is the name of the file in this case, and fileName the key
|
||||
if (basePath.endsWith(".yaml")) {
|
||||
const getDescendantProp = (obj, desc) => {
|
||||
const arr = desc.split("/");
|
||||
while (arr.length) {
|
||||
obj = obj[arr.shift()];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
wildcards = wildcards.concat(getDescendantProp(yamlWildcards[basePath], fileName));
|
||||
} else {
|
||||
const fileContent = (await fetchTacAPI(`tacapi/v1/wildcard-contents?basepath=${basePath}&filename=${fileName}.txt`, false))
|
||||
.split("\n")
|
||||
.filter(x => x.trim().length > 0 && !x.startsWith('#')); // Remove empty lines and comments
|
||||
wildcards = wildcards.concat(fileContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (TAC_CFG.sortWildcardResults)
|
||||
wildcards.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
let finalResults = [];
|
||||
let tempResults = wildcards.filter(x => (wcWord !== null && wcWord.length > 0) ? x.toLowerCase().includes(wcWord) : x) // Filter by tagword
|
||||
@@ -36,21 +64,36 @@ class WildcardFileParser extends BaseTagParser {
|
||||
parse() {
|
||||
// Show available wildcard files
|
||||
let tempResults = [];
|
||||
if (tagword !== "__") {
|
||||
let lmb = (x) => x[1].toLowerCase().includes(tagword.replace("__", ""))
|
||||
if (tagword !== TAC_CFG.wcWrap) {
|
||||
let lmb = (x) => x[1].toLowerCase().includes(tagword.replace(TAC_CFG.wcWrap, ""))
|
||||
tempResults = wildcardFiles.filter(lmb).concat(wildcardExtFiles.filter(lmb)) // Filter by tagword
|
||||
} else {
|
||||
tempResults = wildcardFiles.concat(wildcardExtFiles);
|
||||
}
|
||||
|
||||
let finalResults = [];
|
||||
const alreadyAdded = new Map();
|
||||
// Get final results
|
||||
tempResults.forEach(wcFile => {
|
||||
let result = new AutocompleteResult(wcFile[1].trim(), ResultType.wildcardFile);
|
||||
result.meta = "Wildcard file";
|
||||
// Skip duplicate entries incase multiple files have the same name or yaml category
|
||||
if (alreadyAdded.has(wcFile[1])) return;
|
||||
|
||||
let result = null;
|
||||
if (wcFile[0].endsWith(".yaml")) {
|
||||
result = new AutocompleteResult(wcFile[1].trim(), ResultType.yamlWildcard);
|
||||
result.meta = "YAML wildcard collection";
|
||||
} else {
|
||||
result = new AutocompleteResult(wcFile[1].trim(), ResultType.wildcardFile);
|
||||
result.meta = "Wildcard file";
|
||||
result.sortKey = wcFile[2].trim();
|
||||
}
|
||||
|
||||
finalResults.push(result);
|
||||
alreadyAdded.set(wcFile[1], true);
|
||||
});
|
||||
|
||||
finalResults.sort(getSortFunction());
|
||||
|
||||
return finalResults;
|
||||
}
|
||||
}
|
||||
@@ -58,17 +101,19 @@ class WildcardFileParser extends BaseTagParser {
|
||||
async function load() {
|
||||
if (wildcardFiles.length === 0 && wildcardExtFiles.length === 0) {
|
||||
try {
|
||||
let wcFileArr = (await readFile(`${tagBasePath}/temp/wc.txt`)).split("\n");
|
||||
let wcBasePath = wcFileArr[0].trim(); // First line should be the base path
|
||||
wildcardFiles = wcFileArr.slice(1)
|
||||
.filter(x => x.trim().length > 0) // Remove empty lines
|
||||
.map(x => [wcBasePath, x.trim().replace(".txt", "")]); // Remove file extension & newlines
|
||||
let wcFileArr = await loadCSV(`${tagBasePath}/temp/wc.txt`);
|
||||
if (wcFileArr && wcFileArr.length > 0) {
|
||||
let wcBasePath = wcFileArr[0][0].trim(); // First line should be the base path
|
||||
wildcardFiles = wcFileArr.slice(1)
|
||||
.filter(x => x[0]?.trim().length > 0) //Remove empty lines
|
||||
.map(x => [wcBasePath, x[0]?.trim().replace(".txt", ""), x[1]]); // Remove file extension & newlines
|
||||
}
|
||||
|
||||
// To support multiple sources, we need to separate them using the provided "-----" strings
|
||||
let wcExtFileArr = (await readFile(`${tagBasePath}/temp/wce.txt`)).split("\n");
|
||||
let wcExtFileArr = await loadCSV(`${tagBasePath}/temp/wce.txt`);
|
||||
let splitIndices = [];
|
||||
for (let index = 0; index < wcExtFileArr.length; index++) {
|
||||
if (wcExtFileArr[index].trim() === "-----") {
|
||||
if (wcExtFileArr[index][0].trim() === "-----") {
|
||||
splitIndices.push(index);
|
||||
}
|
||||
}
|
||||
@@ -79,14 +124,25 @@ async function load() {
|
||||
let end = splitIndices[i];
|
||||
|
||||
let wcExtFile = wcExtFileArr.slice(start, end);
|
||||
let base = wcExtFile[0].trim() + "/";
|
||||
wcExtFile = wcExtFile.slice(1)
|
||||
.filter(x => x.trim().length > 0) // Remove empty lines
|
||||
.map(x => x.trim().replace(base, "").replace(".txt", "")); // Remove file extension & newlines;
|
||||
|
||||
wcExtFile = wcExtFile.map(x => [base, x]);
|
||||
wildcardExtFiles.push(...wcExtFile);
|
||||
if (wcExtFile && wcExtFile.length > 0) {
|
||||
let base = wcExtFile[0][0].trim() + "/";
|
||||
wcExtFile = wcExtFile.slice(1)
|
||||
.filter(x => x[0]?.trim().length > 0) //Remove empty lines
|
||||
.map(x => [base, x[0]?.trim().replace(base, "").replace(".txt", ""), x[1]]);
|
||||
wildcardExtFiles.push(...wcExtFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Load the yaml wildcard json file and append it as a wildcard file, appending each key as a path component until we reach the end
|
||||
yamlWildcards = await readFile(`${tagBasePath}/temp/wc_yaml.json`, true);
|
||||
|
||||
// Append each key as a path component until we reach a leaf
|
||||
Object.keys(yamlWildcards).forEach(file => {
|
||||
const flattened = flatten(yamlWildcards[file], [], "/");
|
||||
Object.keys(flattened).forEach(key => {
|
||||
wildcardExtFiles.push([file, key]);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error loading wildcards: " + e);
|
||||
}
|
||||
@@ -94,19 +150,18 @@ async function load() {
|
||||
}
|
||||
|
||||
function sanitize(tagType, text) {
|
||||
if (tagType === ResultType.wildcardFile) {
|
||||
return `__${text}__`;
|
||||
if (tagType === ResultType.wildcardFile || tagType === ResultType.yamlWildcard) {
|
||||
return `${TAC_CFG.wcWrap}${text}${TAC_CFG.wcWrap}`;
|
||||
} else if (tagType === ResultType.wildcardTag) {
|
||||
return text.replace(/^.*?: /g, "");
|
||||
return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function keepOpenIfWildcard(tagType, sanitizedText, newPrompt, textArea) {
|
||||
// If it's a wildcard, we want to keep the results open so the user can select another wildcard
|
||||
if (tagType === ResultType.wildcardFile) {
|
||||
if (tagType === ResultType.wildcardFile || tagType === ResultType.yamlWildcard) {
|
||||
hideBlocked = true;
|
||||
autocomplete(textArea, newPrompt, sanitizedText);
|
||||
setTimeout(() => { hideBlocked = false; }, 450);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const styleColors = {
|
||||
"--results-neutral-text": ["#e0e0e0","black"],
|
||||
"--results-bg": ["#0b0f19", "#ffffff"],
|
||||
"--results-border-color": ["#4b5563", "#e5e7eb"],
|
||||
"--results-border-width": ["1px", "1.5px"],
|
||||
@@ -25,18 +26,37 @@ const autocompleteCSS = `
|
||||
background-color: transparent;
|
||||
min-width: fit-content;
|
||||
}
|
||||
.autocompleteResults {
|
||||
.autocompleteParent {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
max-width: calc(100% - 1.5rem);
|
||||
margin: 5px 0 0 0;
|
||||
}
|
||||
.autocompleteResults {
|
||||
background-color: var(--results-bg) !important;
|
||||
border: var(--results-border-width) solid var(--results-border-color) !important;
|
||||
color: var(--results-neutral-text) !important;
|
||||
border-radius: 12px !important;
|
||||
height: fit-content;
|
||||
flex-basis: fit-content;
|
||||
flex-shrink: 0;
|
||||
overflow-y: var(--results-overflow-y);
|
||||
overflow-x: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
.sideInfo {
|
||||
display: none;
|
||||
position: relative;
|
||||
margin-left: 10px;
|
||||
height: 18rem;
|
||||
max-width: 16rem;
|
||||
}
|
||||
.sideInfo > img {
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.autocompleteResultsList > li:nth-child(odd) {
|
||||
background-color: var(--results-bg-odd);
|
||||
}
|
||||
@@ -56,6 +76,7 @@ const autocompleteCSS = `
|
||||
}
|
||||
.acListItem {
|
||||
white-space: break-spaces;
|
||||
min-width: 100px;
|
||||
}
|
||||
.acMetaText {
|
||||
position: relative;
|
||||
@@ -65,6 +86,10 @@ const autocompleteCSS = `
|
||||
white-space: nowrap;
|
||||
color: var(--meta-text-color);
|
||||
}
|
||||
.acMetaText.biased::before {
|
||||
content: "✨";
|
||||
margin-right: 2px;
|
||||
}
|
||||
.acWikiLink {
|
||||
padding: 0.5rem;
|
||||
margin: -0.5rem 0 -0.5rem -0.5rem;
|
||||
@@ -190,16 +215,33 @@ async function syncOptions() {
|
||||
resultStepLength: opts["tac_resultStepLength"],
|
||||
delayTime: opts["tac_delayTime"],
|
||||
useWildcards: opts["tac_useWildcards"],
|
||||
sortWildcardResults: opts["tac_sortWildcardResults"],
|
||||
useEmbeddings: opts["tac_useEmbeddings"],
|
||||
includeEmbeddingsInNormalResults: opts["tac_includeEmbeddingsInNormalResults"],
|
||||
useHypernetworks: opts["tac_useHypernetworks"],
|
||||
useLoras: opts["tac_useLoras"],
|
||||
useLycos: opts["tac_useLycos"],
|
||||
useLycos: opts["tac_useLycos"],
|
||||
useLoraPrefixForLycos: opts["tac_useLoraPrefixForLycos"],
|
||||
showWikiLinks: opts["tac_showWikiLinks"],
|
||||
showExtraNetworkPreviews: opts["tac_showExtraNetworkPreviews"],
|
||||
modelSortOrder: opts["tac_modelSortOrder"],
|
||||
frequencySort: opts["tac_frequencySort"],
|
||||
frequencyFunction: opts["tac_frequencyFunction"],
|
||||
frequencyMinCount: opts["tac_frequencyMinCount"],
|
||||
frequencyMaxAge: opts["tac_frequencyMaxAge"],
|
||||
frequencyRecommendCap: opts["tac_frequencyRecommendCap"],
|
||||
frequencyIncludeAlias: opts["tac_frequencyIncludeAlias"],
|
||||
useStyleVars: opts["tac_useStyleVars"],
|
||||
// Insertion related settings
|
||||
replaceUnderscores: opts["tac_replaceUnderscores"],
|
||||
escapeParentheses: opts["tac_escapeParentheses"],
|
||||
appendComma: opts["tac_appendComma"],
|
||||
appendSpace: opts["tac_appendSpace"],
|
||||
alwaysSpaceAtEnd: opts["tac_alwaysSpaceAtEnd"],
|
||||
wildcardCompletionMode: opts["tac_wildcardCompletionMode"],
|
||||
modelKeywordCompletion: opts["tac_modelKeywordCompletion"],
|
||||
modelKeywordLocation: opts["tac_modelKeywordLocation"],
|
||||
wcWrap: opts["dp_parser_wildcard_wrap"] || "__", // to support custom wrapper chars set by dp_parser
|
||||
// Alias settings
|
||||
alias: {
|
||||
searchByAlias: opts["tac_alias.searchByAlias"],
|
||||
@@ -243,6 +285,17 @@ async function syncOptions() {
|
||||
await loadTags(newCFG);
|
||||
}
|
||||
|
||||
// Refresh temp files if model sort order changed
|
||||
// Contrary to the other loads, this one shouldn't happen on a first time load
|
||||
if (TAC_CFG && newCFG.modelSortOrder !== TAC_CFG.modelSortOrder) {
|
||||
const dropdown = gradioApp().querySelector("#setting_tac_modelSortOrder");
|
||||
dropdown.style.opacity = 0.5;
|
||||
dropdown.style.pointerEvents = "none";
|
||||
await refreshTacTempFiles(true);
|
||||
dropdown.style.opacity = null;
|
||||
dropdown.style.pointerEvents = null;
|
||||
}
|
||||
|
||||
// Update CSS if maxResults changed
|
||||
if (TAC_CFG && newCFG.maxResults !== TAC_CFG.maxResults) {
|
||||
gradioApp().querySelectorAll(".autocompleteResults").forEach(r => {
|
||||
@@ -266,50 +319,65 @@ async function syncOptions() {
|
||||
|
||||
// Create the result list div and necessary styling
|
||||
function createResultsDiv(textArea) {
|
||||
let parentDiv = document.createElement("div");
|
||||
let resultsDiv = document.createElement("div");
|
||||
let resultsList = document.createElement("ul");
|
||||
let sideDiv = document.createElement("div");
|
||||
let sideDivImg = document.createElement("img");
|
||||
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let typeClass = textAreaId.replaceAll(".", " ");
|
||||
|
||||
parentDiv.setAttribute("class", `autocompleteParent${typeClass}`);
|
||||
|
||||
resultsDiv.style.maxHeight = `${TAC_CFG.maxResults * 50}px`;
|
||||
resultsDiv.setAttribute("class", `autocompleteResults ${typeClass} notranslate`);
|
||||
resultsDiv.setAttribute("class", `autocompleteResults${typeClass} notranslate`);
|
||||
resultsDiv.setAttribute("translate", "no");
|
||||
resultsList.setAttribute("class", "autocompleteResultsList");
|
||||
resultsDiv.appendChild(resultsList);
|
||||
|
||||
return resultsDiv;
|
||||
sideDiv.setAttribute("class", `autocompleteResults${typeClass} sideInfo`);
|
||||
sideDiv.appendChild(sideDivImg);
|
||||
|
||||
parentDiv.appendChild(resultsDiv);
|
||||
parentDiv.appendChild(sideDiv);
|
||||
|
||||
return parentDiv;
|
||||
}
|
||||
|
||||
// Show or hide the results div
|
||||
function isVisible(textArea) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
|
||||
return resultsDiv.style.display === "block";
|
||||
let parentDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId);
|
||||
return parentDiv.style.display === "flex";
|
||||
}
|
||||
function showResults(textArea) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
|
||||
resultsDiv.style.display = "block";
|
||||
let parentDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId);
|
||||
parentDiv.style.display = "flex";
|
||||
|
||||
if (TAC_CFG.slidingPopup) {
|
||||
let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd).left;
|
||||
let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - resultsDiv.offsetWidth);
|
||||
|
||||
resultsDiv.style.left = `${offset}px`;
|
||||
let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - parentDiv.offsetWidth);
|
||||
|
||||
parentDiv.style.left = `${offset}px`;
|
||||
} else {
|
||||
if (resultsDiv.style.left)
|
||||
resultsDiv.style.removeProperty("left");
|
||||
if (parentDiv.style.left)
|
||||
parentDiv.style.removeProperty("left");
|
||||
}
|
||||
// Reset here too to make absolutely sure the browser registers it
|
||||
resultsDiv.scrollTop = 0;
|
||||
parentDiv.scrollTop = 0;
|
||||
|
||||
// Ensure preview is hidden
|
||||
let previewDiv = gradioApp().querySelector(`.autocompleteParent${textAreaId} .sideInfo`);
|
||||
previewDiv.style.display = "none";
|
||||
}
|
||||
function hideResults(textArea) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
|
||||
|
||||
let resultsDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId);
|
||||
|
||||
if (!resultsDiv) return;
|
||||
|
||||
|
||||
resultsDiv.style.display = "none";
|
||||
selectedTag = null;
|
||||
}
|
||||
@@ -319,12 +387,12 @@ function isEnabled() {
|
||||
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 = 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 (TAC_CFG.activeIn.modelListMode.toLowerCase() === "blacklist") {
|
||||
@@ -343,9 +411,10 @@ function isEnabled() {
|
||||
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 STYLE_VAR_REGEX = /\$\(?[^$|\[\],\s]*\)?/g;
|
||||
const NORMAL_TAG_REGEX = /[^\s,|<>\[\]:]+_\([^\s,|<>\[\]:]*\)?|[^\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");
|
||||
const TAG_REGEX = () => { return new RegExp(`${POINTY_REGEX.source}|${COMPLETED_WILDCARD_REGEX.source.replaceAll("__", escapeRegExp(TAC_CFG.wcWrap))}|${STYLE_VAR_REGEX.source}|${NORMAL_TAG_REGEX.source}`, "g"); }
|
||||
|
||||
// On click, insert the tag into the prompt textbox with respect to the cursor position
|
||||
async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithoutChoice = false) {
|
||||
@@ -372,7 +441,7 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout
|
||||
}
|
||||
}
|
||||
|
||||
if (tagType === ResultType.wildcardFile
|
||||
if ((tagType === ResultType.wildcardFile || tagType === ResultType.yamlWildcard)
|
||||
&& tabCompletedWithoutChoice
|
||||
&& TAC_CFG.wildcardCompletionMode !== "Always fully"
|
||||
&& sanitizedText.includes("/")) {
|
||||
@@ -399,13 +468,46 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout
|
||||
}
|
||||
}
|
||||
// Don't cut off the __ at the end if it is already the full path
|
||||
if (firstDifference < longestResult) {
|
||||
if (firstDifference > 0 && firstDifference < longestResult) {
|
||||
// +2 because the sanitized text already has the __ at the start but the matched text doesn't
|
||||
sanitizedText = sanitizedText.substring(0, firstDifference + 2);
|
||||
sanitizedText = sanitizedText.substring(0, firstDifference + TAC_CFG.wcWrap.length);
|
||||
} else if (firstDifference === 0) {
|
||||
sanitizedText = tagword;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Frequency db update
|
||||
if (TAC_CFG.frequencySort) {
|
||||
let name = null;
|
||||
|
||||
switch (tagType) {
|
||||
case ResultType.wildcardFile:
|
||||
case ResultType.yamlWildcard:
|
||||
// We only want to update the frequency for a full wildcard, not partial paths
|
||||
if (sanitizedText.endsWith(TAC_CFG.wcWrap))
|
||||
name = text
|
||||
break;
|
||||
case ResultType.chant:
|
||||
// Chants use a slightly different format
|
||||
name = result.aliases;
|
||||
break;
|
||||
default:
|
||||
name = text;
|
||||
break;
|
||||
}
|
||||
|
||||
if (name && name.length > 0) {
|
||||
// Check if it's a negative prompt
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let isNegative = textAreaId.includes("n");
|
||||
// Sanitize name for API call
|
||||
name = encodeURIComponent(name)
|
||||
// Call API & update db
|
||||
increaseUseCount(name, tagType, isNegative)
|
||||
}
|
||||
}
|
||||
|
||||
var prompt = textArea.value;
|
||||
|
||||
// Edit prompt text
|
||||
@@ -417,31 +519,111 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout
|
||||
|
||||
var optionalSeparator = "";
|
||||
let extraNetworkTypes = [ResultType.hypernetwork, ResultType.lora];
|
||||
let noCommaTypes = [ResultType.wildcardFile, ResultType.yamlWildcard].concat(extraNetworkTypes);
|
||||
if (TAC_CFG.appendComma && !noCommaTypes.includes(tagType)) {
|
||||
optionalSeparator = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null ? "" : ", ";
|
||||
let noCommaTypes = [ResultType.wildcardFile, ResultType.yamlWildcard, ResultType.umiWildcard].concat(extraNetworkTypes);
|
||||
if (!noCommaTypes.includes(tagType)) {
|
||||
// Append comma if enabled and not already present
|
||||
let beforeComma = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null;
|
||||
if (TAC_CFG.appendComma)
|
||||
optionalSeparator = beforeComma ? "" : ",";
|
||||
// Add space if enabled
|
||||
if (TAC_CFG.appendSpace && !beforeComma)
|
||||
optionalSeparator += " ";
|
||||
// If at end of prompt and enabled, override the normal setting if not already added
|
||||
if (!TAC_CFG.appendSpace && TAC_CFG.alwaysSpaceAtEnd)
|
||||
optionalSeparator += surrounding.match(new RegExp(`${escapeRegExp(tagword)}$`, "im")) !== null ? " " : "";
|
||||
} else if (extraNetworkTypes.includes(tagType)) {
|
||||
// Use the dedicated separator for extra networks if it's defined, otherwise fall back to space
|
||||
optionalSeparator = TAC_CFG.extraNetworksSeparator || " ";
|
||||
}
|
||||
|
||||
// Escape $ signs since they are special chars for the replace function
|
||||
// We need four since we're also escaping them in replaceAll in the first place
|
||||
sanitizedText = sanitizedText.replaceAll("$", "$$$$");
|
||||
|
||||
// Replace partial tag word with new text, add comma if needed
|
||||
let insert = surrounding.replace(match, sanitizedText + optionalSeparator);
|
||||
|
||||
// Add back start
|
||||
var newPrompt = prompt.substring(0, editStart) + insert + prompt.substring(editEnd);
|
||||
|
||||
// Add lora/lyco keywords if enabled and found
|
||||
let keywordsLength = 0;
|
||||
|
||||
if (TAC_CFG.modelKeywordCompletion !== "Never" && (tagType === ResultType.lora || tagType === ResultType.lyco)) {
|
||||
let keywords = null;
|
||||
// Check built-in activation words first
|
||||
if (tagType === ResultType.lora || tagType === ResultType.lyco) {
|
||||
let info = await fetchTacAPI(`tacapi/v1/lora-info/${result.text}`)
|
||||
if (info && info["activation text"]) {
|
||||
keywords = info["activation text"];
|
||||
}
|
||||
}
|
||||
|
||||
if (!keywords && modelKeywordPath.length > 0 && result.hash && result.hash !== "NOFILE" && result.hash.length > 0) {
|
||||
let nameDict = modelKeywordDict.get(result.hash);
|
||||
let names = [result.text + ".safetensors", result.text + ".pt", result.text + ".ckpt"];
|
||||
|
||||
// No match, try to find a sha256 match from the cache file
|
||||
if (!nameDict) {
|
||||
const sha256 = await fetchTacAPI(`/tacapi/v1/lora-cached-hash/${result.text}`)
|
||||
if (sha256) {
|
||||
nameDict = modelKeywordDict.get(sha256);
|
||||
}
|
||||
}
|
||||
|
||||
if (nameDict) {
|
||||
let found = false;
|
||||
names.forEach(name => {
|
||||
if (!found && nameDict.has(name)) {
|
||||
found = true;
|
||||
keywords = nameDict.get(name);
|
||||
}
|
||||
});
|
||||
|
||||
if (!found)
|
||||
keywords = nameDict.get("none");
|
||||
}
|
||||
}
|
||||
|
||||
if (keywords && keywords.length > 0) {
|
||||
textBeforeKeywordInsertion = newPrompt;
|
||||
|
||||
if (TAC_CFG.modelKeywordLocation === "Start of prompt")
|
||||
newPrompt = `${keywords}, ${newPrompt}`; // Insert keywords
|
||||
else if (TAC_CFG.modelKeywordLocation === "End of prompt")
|
||||
newPrompt = `${newPrompt}, ${keywords}`; // Insert keywords
|
||||
else {
|
||||
let keywordStart = prompt[editStart - 1] === " " ? editStart - 1 : editStart;
|
||||
newPrompt = prompt.substring(0, keywordStart) + `, ${keywords} ${insert}` + prompt.substring(editEnd);
|
||||
}
|
||||
|
||||
|
||||
textAfterKeywordInsertion = newPrompt;
|
||||
keywordInsertionUndone = false;
|
||||
setTimeout(() => lastEditWasKeywordInsertion = true, 200)
|
||||
|
||||
keywordsLength = keywords.length + 2; // +2 for the comma and space
|
||||
}
|
||||
}
|
||||
|
||||
// Insert into prompt textbox and reposition cursor
|
||||
textArea.value = newPrompt;
|
||||
textArea.selectionStart = afterInsertCursorPos + optionalSeparator.length;
|
||||
textArea.selectionStart = afterInsertCursorPos + optionalSeparator.length + keywordsLength;
|
||||
textArea.selectionEnd = textArea.selectionStart
|
||||
|
||||
// Set self trigger flag to show wildcard contents after the filename was inserted
|
||||
if ([ResultType.wildcardFile, ResultType.yamlWildcard, ResultType.umiWildcard].includes(result.type))
|
||||
tacSelfTrigger = true;
|
||||
// Since we've modified a Gradio Textbox component manually, we need to simulate an `input` DOM event to ensure it's propagated back to python.
|
||||
// Uses a built-in method from the webui's ui.js which also already accounts for event target
|
||||
if (tagType === ResultType.wildcardTag || tagType === ResultType.wildcardFile || tagType === ResultType.yamlWildcard)
|
||||
tacSelfTrigger = true;
|
||||
updateInput(textArea);
|
||||
|
||||
// Update previous tags with the edited prompt to prevent re-searching the same term
|
||||
let weightedTags = [...newPrompt.matchAll(WEIGHT_REGEX)]
|
||||
.map(match => match[1]);
|
||||
let tags = newPrompt.match(TAG_REGEX)
|
||||
let tags = newPrompt.match(TAG_REGEX())
|
||||
if (weightedTags !== null) {
|
||||
tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted)))
|
||||
.concat(weightedTags);
|
||||
@@ -534,7 +716,8 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
// Print search term bolded in result
|
||||
itemText.innerHTML = displayText.replace(tagword, `<b>${tagword}</b>`);
|
||||
|
||||
if (result.type === ResultType.wildcardFile && itemText.innerHTML.includes("/")) {
|
||||
const splitTypes = [ResultType.wildcardFile, ResultType.yamlWildcard]
|
||||
if (splitTypes.includes(result.type) && itemText.innerHTML.includes("/")) {
|
||||
let parts = itemText.innerHTML.split("/");
|
||||
let lastPart = parts[parts.length - 1];
|
||||
parts = parts.slice(0, parts.length - 1);
|
||||
@@ -549,12 +732,20 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
let wikiLink = document.createElement("a");
|
||||
wikiLink.classList.add("acWikiLink");
|
||||
wikiLink.innerText = "?";
|
||||
wikiLink.title = "Open external wiki page for this tag"
|
||||
|
||||
let linkPart = displayText;
|
||||
// Only use alias result if it is one
|
||||
if (displayText.includes("➝"))
|
||||
linkPart = displayText.split(" ➝ ")[1];
|
||||
|
||||
|
||||
// Remove any trailing translations
|
||||
if (linkPart.includes("[")) {
|
||||
linkPart = linkPart.split("[")[0]
|
||||
}
|
||||
|
||||
linkPart = encodeURIComponent(linkPart);
|
||||
|
||||
// Set link based on selected file
|
||||
let tagFileNameLower = tagFileName.toLowerCase();
|
||||
if (tagFileNameLower.startsWith("danbooru")) {
|
||||
@@ -562,7 +753,7 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
} else if (tagFileNameLower.startsWith("e621")) {
|
||||
wikiLink.href = `https://e621.net/wiki_pages/${linkPart}`;
|
||||
}
|
||||
|
||||
|
||||
wikiLink.target = "_blank";
|
||||
flexDiv.appendChild(wikiLink);
|
||||
}
|
||||
@@ -587,7 +778,7 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
}
|
||||
|
||||
// Post count
|
||||
if (result.count && !isNaN(result.count)) {
|
||||
if (result.count && !isNaN(result.count) && result.count !== Number.MAX_SAFE_INTEGER) {
|
||||
let postCount = result.count;
|
||||
let formatter;
|
||||
|
||||
@@ -615,12 +806,60 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
else if (result.meta.startsWith("v2"))
|
||||
itemText.classList.add("acEmbeddingV2");
|
||||
}
|
||||
|
||||
|
||||
flexDiv.appendChild(metaDiv);
|
||||
}
|
||||
|
||||
// Add listener
|
||||
li.addEventListener("click", function () { insertTextAtCursor(textArea, result, tagword); });
|
||||
// Add small ✨ marker to indicate usage sorting
|
||||
if (result.usageBias) {
|
||||
flexDiv.querySelector(".acMetaText").classList.add("biased");
|
||||
flexDiv.title = "✨ Frequent tag. Ctrl/Cmd + click to reset usage count."
|
||||
}
|
||||
|
||||
// Check if it's a negative prompt
|
||||
let isNegative = textAreaId.includes("n");
|
||||
|
||||
// Add click listener
|
||||
li.addEventListener("click", (e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
resetUseCount(result.text, result.type, !isNegative, isNegative);
|
||||
flexDiv.querySelector(".acMetaText").classList.remove("biased");
|
||||
} else {
|
||||
insertTextAtCursor(textArea, result, tagword);
|
||||
}
|
||||
});
|
||||
// Add delayed hover listener for extra network previews
|
||||
if (
|
||||
TAC_CFG.showExtraNetworkPreviews &&
|
||||
[
|
||||
ResultType.embedding,
|
||||
ResultType.hypernetwork,
|
||||
ResultType.lora,
|
||||
ResultType.lyco,
|
||||
].includes(result.type)
|
||||
) {
|
||||
li.addEventListener("mouseover", async () => {
|
||||
const me = this;
|
||||
let hoverTimeout;
|
||||
|
||||
hoverTimeout = setTimeout(async () => {
|
||||
// If the tag we hover over is already selected, do nothing
|
||||
if (selectedTag && selectedTag === i) return;
|
||||
|
||||
oldSelectedTag = selectedTag;
|
||||
selectedTag = i;
|
||||
|
||||
// Update selection without scrolling to the item (since we would
|
||||
// immediately trigger the next scroll as the items move under the cursor)
|
||||
updateSelectionStyle(textArea, selectedTag, oldSelectedTag, false);
|
||||
}, 400);
|
||||
// Reset delay timer if we leave the item
|
||||
me.addEventListener("mouseout", () => {
|
||||
clearTimeout(hoverTimeout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add element to list
|
||||
resultsList.appendChild(li);
|
||||
}
|
||||
@@ -633,7 +872,7 @@ function addResultsToList(textArea, results, tagword, resetList) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelectionStyle(textArea, newIndex, oldIndex) {
|
||||
async function updateSelectionStyle(textArea, newIndex, oldIndex, scroll = true) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
|
||||
let resultsList = resultDiv.querySelector('ul');
|
||||
@@ -645,13 +884,37 @@ function updateSelectionStyle(textArea, newIndex, oldIndex) {
|
||||
|
||||
// make it safer
|
||||
if (newIndex !== null) {
|
||||
items[newIndex].classList.add('selected');
|
||||
let selected = items[newIndex];
|
||||
selected.classList.add('selected');
|
||||
|
||||
// Set scrolltop to selected item
|
||||
if (scroll) resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
|
||||
}
|
||||
|
||||
// Set scrolltop to selected item if we are showing more than max results
|
||||
if (items.length > TAC_CFG.maxResults) {
|
||||
let selected = items[newIndex];
|
||||
resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
|
||||
// Show preview if enabled and the selected type supports it
|
||||
if (newIndex !== null) {
|
||||
let selectedResult = results[newIndex];
|
||||
let selectedType = selectedResult.type;
|
||||
// These types support previews (others could technically too, but are not native to the webui gallery)
|
||||
let previewTypes = [ResultType.embedding, ResultType.hypernetwork, ResultType.lora, ResultType.lyco];
|
||||
|
||||
let previewDiv = gradioApp().querySelector(`.autocompleteParent${textAreaId} .sideInfo`);
|
||||
|
||||
if (TAC_CFG.showExtraNetworkPreviews && previewTypes.includes(selectedType)) {
|
||||
let img = previewDiv.querySelector("img");
|
||||
// String representation of our type enum
|
||||
const typeString = Object.keys(ResultType)[selectedType - 1].toLowerCase();
|
||||
// Get image from API
|
||||
let url = await getTacExtraNetworkPreviewURL(selectedResult.text, typeString);
|
||||
if (url) {
|
||||
img.src = url;
|
||||
previewDiv.style.display = "block";
|
||||
} else {
|
||||
previewDiv.style.display = "none";
|
||||
}
|
||||
} else {
|
||||
previewDiv.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,7 +930,7 @@ function updateRuby(textArea, prompt) {
|
||||
ruby.setAttribute("class", `acRuby${typeClass} notranslate`);
|
||||
textArea.parentNode.appendChild(ruby);
|
||||
}
|
||||
|
||||
|
||||
ruby.innerText = prompt;
|
||||
|
||||
let bracketEscapedPrompt = prompt.replaceAll("\\(", "$").replaceAll("\\)", "%");
|
||||
@@ -685,9 +948,9 @@ function updateRuby(textArea, prompt) {
|
||||
.replaceAll(" ", "_")
|
||||
.replaceAll("\\(", "(")
|
||||
.replaceAll("\\)", ")");
|
||||
|
||||
|
||||
const translation = translations?.get(tag) || translations?.get(unsanitizedTag);
|
||||
|
||||
|
||||
let escapedTag = escapeRegExp(tag);
|
||||
return { tag, escapedTag, translation };
|
||||
}
|
||||
@@ -703,14 +966,14 @@ function updateRuby(textArea, 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) => {
|
||||
@@ -725,14 +988,14 @@ function updateRuby(textArea, prompt) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -760,6 +1023,39 @@ function rubyTagClicked(node, textBefore, prompt, textArea) {
|
||||
textArea.setSelectionRange(startPos, endPos);
|
||||
}
|
||||
|
||||
// Check if the last edit was the keyword insertion, and catch undo/redo in that case
|
||||
function checkKeywordInsertionUndo(textArea, event) {
|
||||
if (TAC_CFG.modelKeywordCompletion === "Never") return;
|
||||
|
||||
switch (event.inputType) {
|
||||
case "historyUndo":
|
||||
if (lastEditWasKeywordInsertion && !keywordInsertionUndone) {
|
||||
keywordInsertionUndone = true;
|
||||
textArea.value = textBeforeKeywordInsertion;
|
||||
tacSelfTrigger = true;
|
||||
updateInput(textArea);
|
||||
}
|
||||
break;
|
||||
case "historyRedo":
|
||||
if (lastEditWasKeywordInsertion && keywordInsertionUndone) {
|
||||
keywordInsertionUndone = false;
|
||||
textArea.value = textAfterKeywordInsertion;
|
||||
tacSelfTrigger = true;
|
||||
updateInput(textArea);
|
||||
}
|
||||
case undefined:
|
||||
// undefined is caused by the updateInput event firing, so we just ignore it
|
||||
break;
|
||||
default:
|
||||
// Everything else deactivates the keyword undo and returns to normal undo behavior
|
||||
lastEditWasKeywordInsertion = false;
|
||||
keywordInsertionUndone = false;
|
||||
textBeforeKeywordInsertion = "";
|
||||
textAfterKeywordInsertion = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
// Return if the function is deactivated in the UI
|
||||
if (!isEnabled()) return;
|
||||
@@ -777,9 +1073,9 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
// We also match for the weighting format (e.g. "tag:1.0") here, and combine the two to get the full tag word set
|
||||
let weightedTags = [...prompt.matchAll(WEIGHT_REGEX)]
|
||||
.map(match => match[1]);
|
||||
let tags = prompt.match(TAG_REGEX)
|
||||
let tags = prompt.match(TAG_REGEX())
|
||||
if (weightedTags !== null && tags !== null) {
|
||||
tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted) && !tag.startsWith("<[")))
|
||||
tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted) && !tag.startsWith("<[") && !tag.startsWith("$(")))
|
||||
.concat(weightedTags);
|
||||
}
|
||||
|
||||
@@ -813,46 +1109,29 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
}
|
||||
|
||||
results = [];
|
||||
resultCountBeforeNormalTags = 0;
|
||||
tagword = tagword.toLowerCase().replace(/[\n\r]/g, "");
|
||||
|
||||
// Needed for slicing check later
|
||||
let normalTags = false;
|
||||
|
||||
// Process all parsers
|
||||
let resultCandidates = await processParsers(textArea, prompt);
|
||||
let resultCandidates = (await processParsers(textArea, prompt))?.filter(x => x.length > 0);
|
||||
// If one ore more result candidates match, use their results
|
||||
if (resultCandidates && resultCandidates.length > 0) {
|
||||
// Flatten our candidate(s)
|
||||
results = resultCandidates.flat();
|
||||
// If there was more than one candidate, sort the results by text to mix them
|
||||
// instead of having them added in the order of the parsers
|
||||
let shouldSort = resultCandidates.length > 1;
|
||||
if (shouldSort) {
|
||||
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);
|
||||
});
|
||||
// Sort results, but not if it's umi tags since they are sorted by count
|
||||
if (!(resultCandidates.length === 1 && results[0].type === ResultType.umiWildcard))
|
||||
results = results.sort(getSortFunction());
|
||||
}
|
||||
// Else search the normal tag list
|
||||
if (!resultCandidates || resultCandidates.length === 0
|
||||
|| (TAC_CFG.includeEmbeddingsInNormalResults && !(tagword.startsWith("<") || tagword.startsWith("*<")))
|
||||
) {
|
||||
normalTags = true;
|
||||
resultCountBeforeNormalTags = results.length;
|
||||
|
||||
// Since some tags are kaomoji, we have to add the normal results in some cases
|
||||
if (tagword.startsWith("<") || tagword.startsWith("*<")) {
|
||||
// Create escaped search regex with support for * as a start placeholder
|
||||
let searchRegex;
|
||||
if (tagword.startsWith("*")) {
|
||||
tagword = tagword.slice(1);
|
||||
searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i');
|
||||
} else {
|
||||
searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
|
||||
}
|
||||
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)
|
||||
result.category = g[1];
|
||||
result.count = g[2];
|
||||
result.aliases = g[3];
|
||||
results.push(result);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else { // Else search the normal tag list
|
||||
// Create escaped search regex with support for * as a start placeholder
|
||||
let searchRegex;
|
||||
if (tagword.startsWith("*")) {
|
||||
@@ -867,7 +1146,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
let aliasFilter = (x) => x[3] && x[3].toLowerCase().search(searchRegex) > -1;
|
||||
let translationFilter = (x) => (translations.has(x[0]) && translations.get(x[0]).toLowerCase().search(searchRegex) > -1)
|
||||
|| x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().search(searchRegex) > -1);
|
||||
|
||||
|
||||
let fil;
|
||||
if (TAC_CFG.alias.searchByAlias && TAC_CFG.translation.searchByTranslation)
|
||||
fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x);
|
||||
@@ -905,11 +1184,6 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
results = results.concat(extraResults);
|
||||
}
|
||||
}
|
||||
|
||||
// Slice if the user has set a max result count
|
||||
if (!TAC_CFG.showAllResults) {
|
||||
results = results.slice(0, TAC_CFG.maxResults);
|
||||
}
|
||||
}
|
||||
|
||||
// Guard for empty results
|
||||
@@ -919,6 +1193,57 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort again with frequency / usage count if enabled
|
||||
if (TAC_CFG.frequencySort) {
|
||||
// Split our results into a list of names and types
|
||||
let tagNames = [];
|
||||
let aliasNames = [];
|
||||
let types = [];
|
||||
// Limit to 2k for performance reasons
|
||||
const aliasTypes = [ResultType.tag, ResultType.extra];
|
||||
results.slice(0,2000).forEach(r => {
|
||||
const name = r.type === ResultType.chant ? r.aliases : r.text;
|
||||
// Add to alias list or tag list depending on if the name includes the tagword
|
||||
// (the same criteria is used in the filter in calculateUsageBias)
|
||||
if (aliasTypes.includes(r.type) && !name.includes(tagword)) {
|
||||
aliasNames.push(name);
|
||||
} else {
|
||||
tagNames.push(name);
|
||||
}
|
||||
types.push(r.type);
|
||||
});
|
||||
|
||||
// Check if it's a negative prompt
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let isNegative = textAreaId.includes("n");
|
||||
|
||||
// Request use counts from the DB
|
||||
const names = TAC_CFG.frequencyIncludeAlias ? tagNames.concat(aliasNames) : tagNames;
|
||||
const counts = await getUseCounts(names, types, isNegative) || [];
|
||||
|
||||
// Pre-calculate weights to prevent duplicate work
|
||||
const resultBiasMap = new Map();
|
||||
results.forEach(result => {
|
||||
const name = result.type === ResultType.chant ? result.aliases : result.text;
|
||||
const type = result.type;
|
||||
// Find matching pair from DB results
|
||||
const useStats = counts.find(c => c.name === name && c.type === type);
|
||||
const uses = useStats?.count || 0;
|
||||
// Calculate & set weight
|
||||
const weight = calculateUsageBias(result, result.count, uses)
|
||||
resultBiasMap.set(result, weight);
|
||||
});
|
||||
// Actual sorting with the pre-calculated weights
|
||||
results = results.sort((a, b) => {
|
||||
return resultBiasMap.get(b) - resultBiasMap.get(a);
|
||||
});
|
||||
}
|
||||
|
||||
// Slice if the user has set a max result count and we are not in a extra networks / wildcard list
|
||||
if (!TAC_CFG.showAllResults && normalTags) {
|
||||
results = results.slice(0, TAC_CFG.maxResults + resultCountBeforeNormalTags);
|
||||
}
|
||||
|
||||
addResultsToList(textArea, results, tagword, true);
|
||||
showResults(textArea);
|
||||
}
|
||||
@@ -926,7 +1251,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
function navigateInList(textArea, event) {
|
||||
// Return if the function is deactivated in the UI or the current model is excluded due to white/blacklist settings
|
||||
if (!isEnabled()) return;
|
||||
|
||||
|
||||
let keys = 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
|
||||
@@ -941,12 +1266,17 @@ function navigateInList(textArea, event) {
|
||||
|
||||
if (!validKeys.includes(event.key)) return;
|
||||
if (!isVisible(textArea)) return
|
||||
// Return if ctrl key is pressed to not interfere with weight editing shortcut
|
||||
if (event.ctrlKey || event.altKey) return;
|
||||
// Add modifier keys to base as text+.
|
||||
let modKey = "";
|
||||
if (event.ctrlKey) modKey += "Ctrl+";
|
||||
if (event.altKey) modKey += "Alt+";
|
||||
if (event.shiftKey) modKey += "Shift+";
|
||||
if (event.metaKey) modKey += "Meta+";
|
||||
modKey += event.key;
|
||||
|
||||
oldSelectedTag = selectedTag;
|
||||
|
||||
switch (event.key) {
|
||||
switch (modKey) {
|
||||
case keys["MoveUp"]:
|
||||
if (selectedTag === null) {
|
||||
selectedTag = resultCount - 1;
|
||||
@@ -976,10 +1306,25 @@ function navigateInList(textArea, event) {
|
||||
}
|
||||
break;
|
||||
case keys["JumpToStart"]:
|
||||
selectedTag = 0;
|
||||
if (TAC_CFG.includeEmbeddingsInNormalResults &&
|
||||
selectedTag > resultCountBeforeNormalTags &&
|
||||
resultCountBeforeNormalTags > 0
|
||||
) {
|
||||
selectedTag = resultCountBeforeNormalTags;
|
||||
} else {
|
||||
selectedTag = 0;
|
||||
}
|
||||
break;
|
||||
case keys["JumpToEnd"]:
|
||||
selectedTag = resultCount - 1;
|
||||
// Jump to the end of the list, or the end of embeddings if they are included in the normal results
|
||||
if (TAC_CFG.includeEmbeddingsInNormalResults &&
|
||||
selectedTag < resultCountBeforeNormalTags &&
|
||||
resultCountBeforeNormalTags > 0
|
||||
) {
|
||||
selectedTag = Math.min(resultCountBeforeNormalTags, resultCount - 1);
|
||||
} else {
|
||||
selectedTag = resultCount - 1;
|
||||
}
|
||||
break;
|
||||
case keys["ChooseSelected"]:
|
||||
if (selectedTag !== null) {
|
||||
@@ -1002,6 +1347,8 @@ function navigateInList(textArea, event) {
|
||||
case keys["Close"]:
|
||||
hideResults(textArea);
|
||||
break;
|
||||
default:
|
||||
if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) return;
|
||||
}
|
||||
let moveKeys = [keys["MoveUp"], keys["MoveDown"], keys["JumpUp"], keys["JumpDown"], keys["JumpToStart"], keys["JumpToEnd"]];
|
||||
if (selectedTag === resultCount - 1 && moveKeys.includes(event.key)) {
|
||||
@@ -1016,19 +1363,36 @@ function navigateInList(textArea, event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
async function refreshTacTempFiles() {
|
||||
setTimeout(async () => {
|
||||
async function refreshTacTempFiles(api = false) {
|
||||
const reload = async () => {
|
||||
wildcardFiles = [];
|
||||
wildcardExtFiles = [];
|
||||
yamlWildcards = [];
|
||||
umiWildcards = [];
|
||||
embeddings = [];
|
||||
hypernetworks = [];
|
||||
loras = [];
|
||||
lycos = [];
|
||||
modelKeywordDict.clear();
|
||||
await processQueue(QUEUE_FILE_LOAD, null);
|
||||
|
||||
console.log("TAC: Refreshed temp files");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
if (api) {
|
||||
await postTacAPI("tacapi/v1/refresh-temp-files");
|
||||
await reload();
|
||||
} else {
|
||||
setTimeout(async () => {
|
||||
await reload();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshEmbeddings() {
|
||||
await postTacAPI("tacapi/v1/refresh-embeddings", null);
|
||||
embeddings = [];
|
||||
await processQueue(QUEUE_FILE_LOAD, null);
|
||||
console.log("TAC: Refreshed embeddings");
|
||||
}
|
||||
|
||||
function addAutocompleteToArea(area) {
|
||||
@@ -1050,9 +1414,15 @@ function addAutocompleteToArea(area) {
|
||||
hideResults(area);
|
||||
|
||||
// Add autocomplete event listener
|
||||
area.addEventListener('input', () => {
|
||||
debounce(autocomplete(area, area.value), TAC_CFG.delayTime);
|
||||
area.addEventListener('input', (e) => {
|
||||
updateRuby(area, area.value);
|
||||
|
||||
// Cancel autocomplete itself if the event has no inputType (e.g. because it was triggered by the updateInput() function)
|
||||
if (!e.inputType && !tacSelfTrigger) return;
|
||||
tacSelfTrigger = false;
|
||||
|
||||
debounce(autocomplete(area, area.value), TAC_CFG.delayTime);
|
||||
checkKeywordInsertionUndo(area, e);
|
||||
});
|
||||
// Add focusout event listener
|
||||
area.addEventListener('focusout', debounce(() => {
|
||||
@@ -1112,6 +1482,13 @@ async function setup() {
|
||||
// Listener for internal temp files refresh button
|
||||
gradioApp().querySelector("#refresh_tac_refreshTempFiles")?.addEventListener("click", refreshTacTempFiles);
|
||||
|
||||
// Also add listener for external network refresh button (plus triggering python code)
|
||||
["#img2img_extra_refresh", "#txt2img_extra_refresh"].forEach(e => {
|
||||
gradioApp().querySelector(e)?.addEventListener("click", ()=>{
|
||||
refreshTacTempFiles(true);
|
||||
});
|
||||
})
|
||||
|
||||
// Add mutation observer for the model hash text to also allow hash-based blacklist again
|
||||
let modelHashText = gradioApp().querySelector("#sd_checkpoint_hash");
|
||||
updateModelName();
|
||||
@@ -1122,6 +1499,7 @@ async function setup() {
|
||||
if (mutation.type === "attributes" && mutation.attributeName === "title") {
|
||||
currentModelHash = mutation.target.title;
|
||||
updateModelName();
|
||||
refreshEmbeddings();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1131,8 +1509,8 @@ async function setup() {
|
||||
// Not found, we're on a page without prompt textareas
|
||||
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') || !TAC_CFG.activeIn.negativePrompts) {
|
||||
if (gradioApp().querySelector('.autocompleteParent.p')) {
|
||||
if (gradioApp().querySelector('.autocompleteParent.n') || !TAC_CFG.activeIn.negativePrompts) {
|
||||
return;
|
||||
}
|
||||
} else if (!TAC_CFG.activeIn.txt2img && !TAC_CFG.activeIn.img2img) {
|
||||
@@ -1146,7 +1524,7 @@ async function setup() {
|
||||
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) => {
|
||||
@@ -1155,7 +1533,7 @@ async function setup() {
|
||||
Object.keys(browserVars).forEach((key) => {
|
||||
css = css.replaceAll(`var(${key})`, browserVars[key][browser]);
|
||||
})
|
||||
|
||||
|
||||
if (acStyle.styleSheet) {
|
||||
acStyle.styleSheet.cssText = css;
|
||||
} else {
|
||||
|
||||
85
scripts/model_keyword_support.py
Normal file
85
scripts/model_keyword_support.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# This file provides support for the model-keyword extension to add known lora keywords on completion
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.shared_paths import EXT_PATH, STATIC_TEMP_PATH, TEMP_PATH
|
||||
|
||||
# Set up our hash cache
|
||||
known_hashes_file = TEMP_PATH.joinpath("known_lora_hashes.txt")
|
||||
known_hashes_file.touch()
|
||||
file_needs_update = False
|
||||
|
||||
# Load the hashes from the file
|
||||
hash_dict = {}
|
||||
|
||||
|
||||
def load_hash_cache():
|
||||
if not known_hashes_file.exists():
|
||||
known_hashes_file.touch()
|
||||
with open(known_hashes_file, "r", encoding="utf-8") as file:
|
||||
reader = csv.reader(
|
||||
file.readlines(), delimiter=",", quotechar='"', skipinitialspace=True
|
||||
)
|
||||
for line in reader:
|
||||
name, hash, mtime = line
|
||||
hash_dict[name] = (hash, mtime)
|
||||
|
||||
|
||||
def update_hash_cache():
|
||||
global file_needs_update
|
||||
if file_needs_update:
|
||||
if not known_hashes_file.exists():
|
||||
known_hashes_file.touch()
|
||||
with open(known_hashes_file, "w", encoding="utf-8", newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
for name, (hash, mtime) in hash_dict.items():
|
||||
writer.writerow([name, hash, mtime])
|
||||
|
||||
|
||||
# Copy of the fast inaccurate hash function from the extension
|
||||
# with some modifications to load from and write to the cache
|
||||
def get_lora_simple_hash(path):
|
||||
global file_needs_update
|
||||
mtime = str(Path(path).stat().st_mtime)
|
||||
filename = Path(path).name
|
||||
|
||||
if filename in hash_dict:
|
||||
(hash, old_mtime) = hash_dict[filename]
|
||||
if mtime == old_mtime:
|
||||
return hash
|
||||
try:
|
||||
with open(path, "rb") as file:
|
||||
m = hashlib.sha256()
|
||||
|
||||
file.seek(0x100000)
|
||||
m.update(file.read(0x10000))
|
||||
hash = m.hexdigest()[0:8]
|
||||
|
||||
hash_dict[filename] = (hash, mtime)
|
||||
file_needs_update = True
|
||||
|
||||
return hash
|
||||
except FileNotFoundError:
|
||||
return "NOFILE"
|
||||
|
||||
|
||||
# Find the path of the original model-keyword extension
|
||||
def write_model_keyword_path():
|
||||
# Ensure the file exists even if the extension is not installed
|
||||
mk_path = STATIC_TEMP_PATH.joinpath("modelKeywordPath.txt")
|
||||
mk_path.write_text("")
|
||||
|
||||
base_keywords = list(EXT_PATH.glob("*/lora-keyword.txt"))
|
||||
custom_keywords = list(EXT_PATH.glob("*/lora-keyword-user.txt"))
|
||||
custom_found = custom_keywords is not None and len(custom_keywords) > 0
|
||||
if base_keywords is not None and len(base_keywords) > 0:
|
||||
with open(mk_path, "w", encoding="utf-8") as f:
|
||||
f.write(f"{base_keywords[0].parent.as_posix()},{custom_found}")
|
||||
return True
|
||||
else:
|
||||
print(
|
||||
"Tag Autocomplete: Could not locate model-keyword extension, Lora trigger word completion will be limited to those added through the extra networks menu."
|
||||
)
|
||||
return False
|
||||
74
scripts/shared_paths.py
Normal file
74
scripts/shared_paths.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from pathlib import Path
|
||||
|
||||
from modules import scripts, shared
|
||||
|
||||
try:
|
||||
from modules.paths import extensions_dir, script_path
|
||||
|
||||
# Webui root path
|
||||
FILE_DIR = Path(script_path).absolute()
|
||||
|
||||
# The extension base path
|
||||
EXT_PATH = Path(extensions_dir).absolute()
|
||||
except ImportError:
|
||||
# Webui root path
|
||||
FILE_DIR = Path().absolute()
|
||||
# The extension base path
|
||||
EXT_PATH = FILE_DIR.joinpath("extensions").absolute()
|
||||
|
||||
# Tags base path
|
||||
TAGS_PATH = Path(scripts.basedir()).joinpath("tags").absolute()
|
||||
|
||||
# The path to the folder containing the wildcards and embeddings
|
||||
WILDCARD_PATH = FILE_DIR.joinpath("scripts/wildcards").absolute()
|
||||
EMB_PATH = Path(shared.cmd_opts.embeddings_dir).absolute()
|
||||
HYP_PATH = Path(shared.cmd_opts.hypernetwork_dir).absolute()
|
||||
|
||||
try:
|
||||
LORA_PATH = Path(shared.cmd_opts.lora_dir).absolute()
|
||||
except AttributeError:
|
||||
LORA_PATH = None
|
||||
|
||||
try:
|
||||
try:
|
||||
LYCO_PATH = Path(shared.cmd_opts.lyco_dir_backcompat).absolute()
|
||||
except:
|
||||
LYCO_PATH = Path(shared.cmd_opts.lyco_dir).absolute() # attempt original non-backcompat path
|
||||
except AttributeError:
|
||||
LYCO_PATH = None
|
||||
|
||||
|
||||
def find_ext_wildcard_paths():
|
||||
"""Returns the path to the extension wildcards folder"""
|
||||
found = list(EXT_PATH.glob("*/wildcards/"))
|
||||
# Try to find the wildcard path from the shared opts
|
||||
try:
|
||||
from modules.shared import opts
|
||||
except ImportError: # likely not in an a1111 context
|
||||
opts = None
|
||||
|
||||
# Append custom wildcard paths
|
||||
custom_paths = [
|
||||
getattr(shared.cmd_opts, "wildcards_dir", None), # Cmd arg from the wildcard extension
|
||||
getattr(opts, "wildcard_dir", None), # Custom path from sd-dynamic-prompts
|
||||
]
|
||||
for path in [Path(p).absolute() for p in custom_paths if p is not None]:
|
||||
if path.exists():
|
||||
found.append(path)
|
||||
|
||||
return found
|
||||
|
||||
|
||||
# The path to the extension wildcards folder
|
||||
WILDCARD_EXT_PATHS = find_ext_wildcard_paths()
|
||||
|
||||
# The path to the temporary files
|
||||
# In the webui root, on windows it exists by default, on linux it doesn't
|
||||
STATIC_TEMP_PATH = FILE_DIR.joinpath("tmp").absolute()
|
||||
TEMP_PATH = TAGS_PATH.joinpath("temp").absolute() # Extension specific temp files
|
||||
|
||||
# Make sure these folders exist
|
||||
if not TEMP_PATH.exists():
|
||||
TEMP_PATH.mkdir()
|
||||
if not STATIC_TEMP_PATH.exists():
|
||||
STATIC_TEMP_PATH.mkdir()
|
||||
@@ -1,109 +1,179 @@
|
||||
# This helper script scans folders for wildcards and embeddings and writes them
|
||||
# to a temporary file to expose it to the javascript side
|
||||
|
||||
import sys
|
||||
import glob
|
||||
import importlib
|
||||
import json
|
||||
import sqlite3
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
import gradio as gr
|
||||
import yaml
|
||||
from modules import script_callbacks, scripts, sd_hijack, shared
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import Response, FileResponse, JSONResponse
|
||||
from modules import script_callbacks, sd_hijack, shared, hashes
|
||||
from pydantic import BaseModel
|
||||
|
||||
from scripts.model_keyword_support import (get_lora_simple_hash,
|
||||
load_hash_cache, update_hash_cache,
|
||||
write_model_keyword_path)
|
||||
from scripts.shared_paths import *
|
||||
|
||||
try:
|
||||
from modules.paths import extensions_dir, script_path
|
||||
try:
|
||||
from scripts import tag_frequency_db as tdb
|
||||
except ModuleNotFoundError:
|
||||
from inspect import getframeinfo, currentframe
|
||||
filename = getframeinfo(currentframe()).filename
|
||||
parent = Path(filename).resolve().parent
|
||||
sys.path.append(str(parent))
|
||||
import tag_frequency_db as tdb
|
||||
|
||||
# Webui root path
|
||||
FILE_DIR = Path(script_path)
|
||||
# Ensure the db dependency is reloaded on script reload
|
||||
importlib.reload(tdb)
|
||||
|
||||
# The extension base path
|
||||
EXT_PATH = Path(extensions_dir)
|
||||
except ImportError:
|
||||
# Webui root path
|
||||
FILE_DIR = Path().absolute()
|
||||
# The extension base path
|
||||
EXT_PATH = FILE_DIR.joinpath('extensions')
|
||||
|
||||
# Tags base path
|
||||
TAGS_PATH = Path(scripts.basedir()).joinpath('tags')
|
||||
|
||||
# The path to the folder containing the wildcards and embeddings
|
||||
WILDCARD_PATH = FILE_DIR.joinpath('scripts/wildcards')
|
||||
EMB_PATH = Path(shared.cmd_opts.embeddings_dir)
|
||||
HYP_PATH = Path(shared.cmd_opts.hypernetwork_dir)
|
||||
db = tdb.TagFrequencyDb()
|
||||
if int(db.version) != int(tdb.db_ver):
|
||||
raise ValueError("Database version mismatch")
|
||||
except (ImportError, ValueError, sqlite3.Error) as e:
|
||||
print(f"Tag Autocomplete: Tag frequency database error - \"{e}\"")
|
||||
db = None
|
||||
|
||||
# Attempt to get embedding load function, using the same call as api.
|
||||
try:
|
||||
LORA_PATH = Path(shared.cmd_opts.lora_dir)
|
||||
except AttributeError:
|
||||
LORA_PATH = None
|
||||
load_textual_inversion_embeddings = sd_hijack.model_hijack.embedding_db.load_textual_inversion_embeddings
|
||||
except Exception as e: # Not supported.
|
||||
load_textual_inversion_embeddings = lambda *args, **kwargs: None
|
||||
print("Tag Autocomplete: Cannot reload embeddings instantly:", e)
|
||||
|
||||
# Sorting functions for extra networks / embeddings stuff
|
||||
sort_criteria = {
|
||||
"Name": lambda path, name, subpath: name.lower() if subpath else path.stem.lower(),
|
||||
"Date Modified (newest first)": lambda path, name, subpath: path.stat().st_mtime,
|
||||
"Date Modified (oldest first)": lambda path, name, subpath: path.stat().st_mtime
|
||||
}
|
||||
|
||||
def sort_models(model_list, sort_method = None, name_has_subpath = False):
|
||||
"""Sorts models according to the setting.
|
||||
|
||||
try:
|
||||
LYCO_PATH = Path(shared.cmd_opts.lyco_dir)
|
||||
except AttributeError:
|
||||
LYCO_PATH = None
|
||||
Input: list of (full_path, display_name, {hash}) models.
|
||||
Returns models in the format of name, sort key, meta.
|
||||
Meta is optional and can be a hash, version string or other required info.
|
||||
"""
|
||||
if len(model_list) == 0:
|
||||
return model_list
|
||||
|
||||
def find_ext_wildcard_paths():
|
||||
"""Returns the path to the extension wildcards folder"""
|
||||
found = list(EXT_PATH.glob('*/wildcards/'))
|
||||
return found
|
||||
if sort_method is None:
|
||||
sort_method = getattr(shared.opts, "tac_modelSortOrder", "Name")
|
||||
|
||||
# Get sorting method from dictionary
|
||||
sorter = sort_criteria.get(sort_method, sort_criteria["Name"])
|
||||
|
||||
# The path to the extension wildcards folder
|
||||
WILDCARD_EXT_PATHS = find_ext_wildcard_paths()
|
||||
|
||||
# The path to the temporary files
|
||||
STATIC_TEMP_PATH = FILE_DIR.joinpath('tmp') # In the webui root, on windows it exists by default, on linux it doesn't
|
||||
TEMP_PATH = TAGS_PATH.joinpath('temp') # Extension specific temp files
|
||||
# During merging on the JS side we need to re-sort anyway, so here only the sort criteria are calculated.
|
||||
# The list itself doesn't need to get sorted at this point.
|
||||
if len(model_list[0]) > 2:
|
||||
results = [f'{name},"{sorter(path, name, name_has_subpath)}",{meta}' for path, name, meta in model_list]
|
||||
else:
|
||||
results = [f'{name},"{sorter(path, name, name_has_subpath)}"' for path, name in model_list]
|
||||
return results
|
||||
|
||||
|
||||
def get_wildcards():
|
||||
"""Returns a list of all wildcards. Works on nested folders."""
|
||||
wildcard_files = list(WILDCARD_PATH.rglob("*.txt"))
|
||||
resolved = [w.relative_to(WILDCARD_PATH).as_posix(
|
||||
) for w in wildcard_files if w.name != "put wildcards here.txt"]
|
||||
return resolved
|
||||
resolved = [(w, w.relative_to(WILDCARD_PATH).as_posix())
|
||||
for w in wildcard_files
|
||||
if w.name != "put wildcards here.txt"
|
||||
and w.is_file()]
|
||||
return sort_models(resolved, name_has_subpath=True)
|
||||
|
||||
|
||||
def get_ext_wildcards():
|
||||
"""Returns a list of all extension wildcards. Works on nested folders."""
|
||||
wildcard_files = []
|
||||
|
||||
excluded_folder_names = [s.strip() for s in getattr(shared.opts, "tac_wildcardExclusionList", "").split(",")]
|
||||
for path in WILDCARD_EXT_PATHS:
|
||||
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")
|
||||
resolved = [(w, w.relative_to(path).as_posix())
|
||||
for w in path.rglob("*.txt")
|
||||
if w.name != "put wildcards here.txt"
|
||||
and not any(excluded in w.parts for excluded in excluded_folder_names)
|
||||
and w.is_file()]
|
||||
wildcard_files.extend(sort_models(resolved, name_has_subpath=True))
|
||||
wildcard_files.append("-----")
|
||||
|
||||
return wildcard_files
|
||||
|
||||
def is_umi_format(data):
|
||||
"""Returns True if the YAML file is in UMI format."""
|
||||
issue_found = False
|
||||
for item in data:
|
||||
if not (data[item] and 'Tags' in data[item] and isinstance(data[item]['Tags'], list)):
|
||||
issue_found = True
|
||||
break
|
||||
return not issue_found
|
||||
|
||||
def get_ext_wildcard_tags():
|
||||
count = 0
|
||||
def parse_umi_format(umi_tags, data):
|
||||
global count
|
||||
for item in data:
|
||||
umi_tags[count] = ','.join(data[item]['Tags'])
|
||||
count += 1
|
||||
|
||||
|
||||
def parse_dynamic_prompt_format(yaml_wildcards, data, path):
|
||||
# Recurse subkeys, delete those without string lists as values
|
||||
def recurse_dict(d: dict):
|
||||
for key, value in d.copy().items():
|
||||
if isinstance(value, dict):
|
||||
recurse_dict(value)
|
||||
elif not (isinstance(value, list) and all(isinstance(v, str) for v in value)):
|
||||
del d[key]
|
||||
|
||||
recurse_dict(data)
|
||||
# Add to yaml_wildcards
|
||||
yaml_wildcards[path.name] = data
|
||||
|
||||
|
||||
def get_yaml_wildcards():
|
||||
"""Returns a list of all tags found in extension YAML files found under a Tags: key."""
|
||||
wildcard_tags = {} # { tag: count }
|
||||
yaml_files = []
|
||||
for path in WILDCARD_EXT_PATHS:
|
||||
yaml_files.extend(p for p in path.rglob("*.yml"))
|
||||
yaml_files.extend(p for p in path.rglob("*.yaml"))
|
||||
count = 0
|
||||
yaml_files.extend(p for p in path.rglob("*.yml") if p.is_file())
|
||||
yaml_files.extend(p for p in path.rglob("*.yaml") if p.is_file())
|
||||
|
||||
yaml_wildcards = {}
|
||||
|
||||
umi_tags = {} # { tag: count }
|
||||
|
||||
for path in yaml_files:
|
||||
try:
|
||||
with open(path, encoding="utf8") as file:
|
||||
data = yaml.safe_load(file)
|
||||
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)
|
||||
if (data):
|
||||
if (is_umi_format(data)):
|
||||
parse_umi_format(umi_tags, data)
|
||||
else:
|
||||
parse_dynamic_prompt_format(yaml_wildcards, data, path)
|
||||
else:
|
||||
print('No data found in ' + path.name)
|
||||
except yaml.YAMLError:
|
||||
print('Issue in parsing YAML file ' + path.name )
|
||||
except (yaml.YAMLError, UnicodeDecodeError) as e:
|
||||
print(f'Issue in parsing YAML file {path.name}: {e}')
|
||||
continue
|
||||
|
||||
# Sort by count
|
||||
sorted_tags = sorted(wildcard_tags.items(), key=lambda item: item[1], reverse=True)
|
||||
output = []
|
||||
for tag, count in sorted_tags:
|
||||
output.append(f"{tag},{count}")
|
||||
return output
|
||||
umi_sorted = sorted(umi_tags.items(), key=lambda item: item[1], reverse=True)
|
||||
umi_output = []
|
||||
for tag, count in umi_sorted:
|
||||
umi_output.append(f"{tag},{count}")
|
||||
|
||||
if (len(umi_output) > 0):
|
||||
write_to_temp_file('umi_tags.txt', umi_output)
|
||||
|
||||
with open(TEMP_PATH.joinpath("wc_yaml.json"), "w", encoding="utf-8") as file:
|
||||
json.dump(yaml_wildcards, file, ensure_ascii=False)
|
||||
|
||||
|
||||
def get_embeddings(sd_model):
|
||||
@@ -112,48 +182,49 @@ def get_embeddings(sd_model):
|
||||
# Version constants
|
||||
V1_SHAPE = 768
|
||||
V2_SHAPE = 1024
|
||||
VXL_SHAPE = 2048
|
||||
emb_v1 = []
|
||||
emb_v2 = []
|
||||
emb_vXL = []
|
||||
emb_unknown = []
|
||||
results = []
|
||||
|
||||
try:
|
||||
# The sd_model embedding_db reference only exists in sd.next with diffusers backend
|
||||
try:
|
||||
loaded_sdnext = sd_model.embedding_db.word_embeddings
|
||||
skipped_sdnext = sd_model.embedding_db.skipped_embeddings
|
||||
except (NameError, AttributeError):
|
||||
loaded_sdnext = {}
|
||||
skipped_sdnext = {}
|
||||
|
||||
# Get embedding dict from sd_hijack to separate v1/v2 embeddings
|
||||
emb_type_a = sd_hijack.model_hijack.embedding_db.word_embeddings
|
||||
emb_type_b = sd_hijack.model_hijack.embedding_db.skipped_embeddings
|
||||
# Get the shape of the first item in the dict
|
||||
emb_a_shape = -1
|
||||
emb_b_shape = -1
|
||||
if (len(emb_type_a) > 0):
|
||||
emb_a_shape = next(iter(emb_type_a.items()))[1].shape
|
||||
if (len(emb_type_b) > 0):
|
||||
emb_b_shape = next(iter(emb_type_b.items()))[1].shape
|
||||
loaded = sd_hijack.model_hijack.embedding_db.word_embeddings
|
||||
skipped = sd_hijack.model_hijack.embedding_db.skipped_embeddings
|
||||
loaded = loaded | loaded_sdnext
|
||||
skipped = skipped | skipped_sdnext
|
||||
|
||||
# Add embeddings to the correct list
|
||||
if (emb_a_shape == V1_SHAPE):
|
||||
emb_v1 = list(emb_type_a.keys())
|
||||
elif (emb_a_shape == V2_SHAPE):
|
||||
emb_v2 = list(emb_type_a.keys())
|
||||
for key, emb in (skipped | loaded).items():
|
||||
if emb.filename is None:
|
||||
continue
|
||||
|
||||
if (emb_b_shape == V1_SHAPE):
|
||||
emb_v1 = list(emb_type_b.keys())
|
||||
elif (emb_b_shape == V2_SHAPE):
|
||||
emb_v2 = list(emb_type_b.keys())
|
||||
if emb.shape is None:
|
||||
emb_unknown.append((Path(emb.filename), Path(emb.filename).relative_to(EMB_PATH).as_posix(), ""))
|
||||
elif emb.shape == V1_SHAPE:
|
||||
emb_v1.append((Path(emb.filename), Path(emb.filename).relative_to(EMB_PATH).as_posix(), "v1"))
|
||||
elif emb.shape == V2_SHAPE:
|
||||
emb_v2.append((Path(emb.filename), Path(emb.filename).relative_to(EMB_PATH).as_posix(), "v2"))
|
||||
elif emb.shape == VXL_SHAPE:
|
||||
emb_vXL.append((Path(emb.filename), Path(emb.filename).relative_to(EMB_PATH).as_posix(), "vXL"))
|
||||
else:
|
||||
emb_unknown.append((Path(emb.filename), Path(emb.filename).relative_to(EMB_PATH).as_posix(), ""))
|
||||
|
||||
# Get shape of current model
|
||||
#vec = sd_model.cond_stage_model.encode_embedding_init_text(",", 1)
|
||||
#model_shape = vec.shape[1]
|
||||
# Show relevant entries at the top
|
||||
#if (model_shape == V1_SHAPE):
|
||||
# results = [e + ",v1" for e in emb_v1] + [e + ",v2" for e in emb_v2]
|
||||
#elif (model_shape == V2_SHAPE):
|
||||
# results = [e + ",v2" for e in emb_v2] + [e + ",v1" for e in emb_v1]
|
||||
#else:
|
||||
# raise AttributeError # Fallback to old method
|
||||
results = sorted([e + ",v1" for e in emb_v1] + [e + ",v2" for e in emb_v2], key=lambda x: x.lower())
|
||||
results = sort_models(emb_v1) + sort_models(emb_v2) + sort_models(emb_vXL) + sort_models(emb_unknown)
|
||||
except AttributeError:
|
||||
print("tag_autocomplete_helper: Old webui version or unrecognized model shape, using fallback for embedding completion.")
|
||||
# Get a list of all embeddings in the folder
|
||||
all_embeds = [str(e.relative_to(EMB_PATH)) for e in EMB_PATH.rglob("*") if e.suffix in {".bin", ".pt", ".png",'.webp', '.jxl', '.avif'}]
|
||||
all_embeds = [str(e.relative_to(EMB_PATH)) for e in EMB_PATH.rglob("*") if e.suffix in {".bin", ".pt", ".png",'.webp', '.jxl', '.avif'} and e.is_file()]
|
||||
# Remove files with a size of 0
|
||||
all_embeds = [e for e in all_embeds if EMB_PATH.joinpath(e).stat().st_size > 0]
|
||||
# Remove file extensions
|
||||
@@ -167,27 +238,129 @@ def get_hypernetworks():
|
||||
|
||||
# Get a list of all hypernetworks in the folder
|
||||
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())
|
||||
all_hypernetworks = [(h, h.stem) for h in hyp_paths if h.suffix in {".pt"} and h.is_file()]
|
||||
return sort_models(all_hypernetworks)
|
||||
|
||||
model_keyword_installed = write_model_keyword_path()
|
||||
|
||||
|
||||
def _get_lora():
|
||||
"""
|
||||
Write a list of all lora.
|
||||
Fallback method for when the built-in Lora.networks module is not available.
|
||||
"""
|
||||
# Get a list of all lora in the folder
|
||||
lora_paths = [
|
||||
Path(l)
|
||||
for l in glob.glob(LORA_PATH.joinpath("**/*").as_posix(), recursive=True)
|
||||
]
|
||||
# Get hashes
|
||||
valid_loras = [
|
||||
lf
|
||||
for lf in lora_paths
|
||||
if lf.suffix in {".safetensors", ".ckpt", ".pt"} and lf.is_file()
|
||||
]
|
||||
|
||||
return valid_loras
|
||||
|
||||
|
||||
def _get_lyco():
|
||||
"""
|
||||
Write a list of all LyCORIS/LOHA from https://github.com/KohakuBlueleaf/a1111-sd-webui-lycoris
|
||||
Fallback method for when the built-in Lora.networks module is not available.
|
||||
"""
|
||||
# Get a list of all LyCORIS in the folder
|
||||
lyco_paths = [
|
||||
Path(ly)
|
||||
for ly in glob.glob(LYCO_PATH.joinpath("**/*").as_posix(), recursive=True)
|
||||
]
|
||||
|
||||
# Get hashes
|
||||
valid_lycos = [
|
||||
lyf
|
||||
for lyf in lyco_paths
|
||||
if lyf.suffix in {".safetensors", ".ckpt", ".pt"} and lyf.is_file()
|
||||
]
|
||||
return valid_lycos
|
||||
|
||||
|
||||
# Attempt to use the build-in Lora.networks Lora/LyCORIS models lists.
|
||||
try:
|
||||
import sys
|
||||
from modules import extensions
|
||||
sys.path.append(Path(extensions.extensions_builtin_dir).joinpath("Lora").as_posix())
|
||||
import lora # pyright: ignore [reportMissingImports]
|
||||
|
||||
def _get_lora():
|
||||
return [
|
||||
Path(model.filename).absolute()
|
||||
for model in lora.available_loras.values()
|
||||
if Path(model.filename).absolute().is_relative_to(LORA_PATH)
|
||||
]
|
||||
|
||||
def _get_lyco():
|
||||
return [
|
||||
Path(model.filename).absolute()
|
||||
for model in lora.available_loras.values()
|
||||
if Path(model.filename).absolute().is_relative_to(LYCO_PATH)
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
# no need to report
|
||||
# print(f'Exception setting-up performant fetchers: {e}')
|
||||
|
||||
|
||||
def is_visible(p: Path) -> bool:
|
||||
if getattr(shared.opts, "extra_networks_hidden_models", "When searched") != "Never":
|
||||
return True
|
||||
for part in p.parts:
|
||||
if part.startswith('.'):
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_lora():
|
||||
"""Write a list of all lora"""
|
||||
# Get hashes
|
||||
valid_loras = _get_lora()
|
||||
loras_with_hash = []
|
||||
for l in valid_loras:
|
||||
if not l.exists() or not l.is_file() or not is_visible(l):
|
||||
continue
|
||||
name = l.relative_to(LORA_PATH).as_posix()
|
||||
if model_keyword_installed:
|
||||
hash = get_lora_simple_hash(l)
|
||||
else:
|
||||
hash = ""
|
||||
loras_with_hash.append((l, name, hash))
|
||||
# Sort
|
||||
return sort_models(loras_with_hash)
|
||||
|
||||
# Get a list of all lora in the folder
|
||||
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())
|
||||
|
||||
def get_lyco():
|
||||
"""Write a list of all LyCORIS/LOHA from https://github.com/KohakuBlueleaf/a1111-sd-webui-lycoris"""
|
||||
# Get hashes
|
||||
valid_lycos = _get_lyco()
|
||||
lycos_with_hash = []
|
||||
for ly in valid_lycos:
|
||||
if not ly.exists() or not ly.is_file() or not is_visible(ly):
|
||||
continue
|
||||
name = ly.relative_to(LYCO_PATH).as_posix()
|
||||
if model_keyword_installed:
|
||||
hash = get_lora_simple_hash(ly)
|
||||
else:
|
||||
hash = ""
|
||||
lycos_with_hash.append((ly, name, hash))
|
||||
# Sort
|
||||
return sort_models(lycos_with_hash)
|
||||
|
||||
# Get a list of all LyCORIS in the folder
|
||||
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 get_style_names():
|
||||
try:
|
||||
style_names: list[str] = shared.prompt_styles.styles.keys()
|
||||
style_names = sorted(style_names, key=len, reverse=True)
|
||||
return style_names
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def write_tag_base_path():
|
||||
"""Writes the tag base path to a fixed location temporary file"""
|
||||
@@ -203,19 +376,19 @@ def write_to_temp_file(name, data):
|
||||
|
||||
csv_files = []
|
||||
csv_files_withnone = []
|
||||
def update_tag_files():
|
||||
def update_tag_files(*args, **kwargs):
|
||||
"""Returns a list of all potential tag files"""
|
||||
global csv_files, csv_files_withnone
|
||||
files = [str(t.relative_to(TAGS_PATH)) for t in TAGS_PATH.glob("*.csv")]
|
||||
files = [str(t.relative_to(TAGS_PATH)) for t in TAGS_PATH.glob("*.csv") if t.is_file()]
|
||||
csv_files = files
|
||||
csv_files_withnone = ["None"] + files
|
||||
|
||||
json_files = []
|
||||
json_files_withnone = []
|
||||
def update_json_files():
|
||||
def update_json_files(*args, **kwargs):
|
||||
"""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")]
|
||||
files = [str(j.relative_to(TAGS_PATH)) for j in TAGS_PATH.glob("*.json") if j.is_file()]
|
||||
json_files = files
|
||||
json_files_withnone = ["None"] + files
|
||||
|
||||
@@ -237,10 +410,12 @@ if not TEMP_PATH.exists():
|
||||
# even if no wildcards or embeddings are found
|
||||
write_to_temp_file('wc.txt', [])
|
||||
write_to_temp_file('wce.txt', [])
|
||||
write_to_temp_file('wcet.txt', [])
|
||||
write_to_temp_file('wc_yaml.json', [])
|
||||
write_to_temp_file('umi_tags.txt', [])
|
||||
write_to_temp_file('hyp.txt', [])
|
||||
write_to_temp_file('lora.txt', [])
|
||||
write_to_temp_file('lyco.txt', [])
|
||||
write_to_temp_file('styles.txt', [])
|
||||
# Only reload embeddings if the file doesn't exist, since they are already re-written on model load
|
||||
if not TEMP_PATH.joinpath("emb.txt").exists():
|
||||
write_to_temp_file('emb.txt', [])
|
||||
@@ -250,88 +425,168 @@ if EMB_PATH.exists():
|
||||
# Get embeddings after the model loaded callback
|
||||
script_callbacks.on_model_loaded(get_embeddings)
|
||||
|
||||
def refresh_temp_files():
|
||||
write_temp_files()
|
||||
get_embeddings(shared.sd_model)
|
||||
def refresh_embeddings(force: bool, *args, **kwargs):
|
||||
try:
|
||||
# Fix for SD.Next infinite refresh loop due to gradio not updating after model load on demand.
|
||||
# This will just skip embedding loading if no model is loaded yet (or there really are no embeddings).
|
||||
# Try catch is just for safety incase sd_hijack access fails for some reason.
|
||||
loaded = sd_hijack.model_hijack.embedding_db.word_embeddings
|
||||
skipped = sd_hijack.model_hijack.embedding_db.skipped_embeddings
|
||||
if len((loaded | skipped)) > 0:
|
||||
load_textual_inversion_embeddings(force_reload=force)
|
||||
get_embeddings(None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def write_temp_files():
|
||||
def refresh_temp_files(*args, **kwargs):
|
||||
global WILDCARD_EXT_PATHS
|
||||
skip_wildcard_refresh = getattr(shared.opts, "tac_skipWildcardRefresh", False)
|
||||
if skip_wildcard_refresh:
|
||||
WILDCARD_EXT_PATHS = find_ext_wildcard_paths()
|
||||
write_temp_files(skip_wildcard_refresh)
|
||||
refresh_embeddings(force=True)
|
||||
|
||||
def write_style_names(*args, **kwargs):
|
||||
styles = get_style_names()
|
||||
if styles:
|
||||
write_to_temp_file('styles.txt', styles)
|
||||
|
||||
def write_temp_files(skip_wildcard_refresh = False):
|
||||
# Write wildcards to wc.txt if found
|
||||
if WILDCARD_PATH.exists():
|
||||
if WILDCARD_PATH.exists() and not skip_wildcard_refresh:
|
||||
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:
|
||||
if WILDCARD_EXT_PATHS is not None and not skip_wildcard_refresh:
|
||||
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 yaml extension wildcards to umi_tags.txt and wc_yaml.json if found
|
||||
get_yaml_wildcards()
|
||||
|
||||
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():
|
||||
if model_keyword_installed:
|
||||
load_hash_cache()
|
||||
|
||||
lora_exists = LORA_PATH is not None and LORA_PATH.exists()
|
||||
if lora_exists:
|
||||
lora = get_lora()
|
||||
if lora:
|
||||
write_to_temp_file('lora.txt', lora)
|
||||
|
||||
if LYCO_PATH is not None and LYCO_PATH.exists():
|
||||
lyco_exists = LYCO_PATH is not None and LYCO_PATH.exists()
|
||||
if lyco_exists and not (lora_exists and LYCO_PATH.samefile(LORA_PATH)):
|
||||
lyco = get_lyco()
|
||||
if lyco:
|
||||
write_to_temp_file('lyco.txt', lyco)
|
||||
elif lyco_exists and lora_exists and LYCO_PATH.samefile(LORA_PATH):
|
||||
print("tag_autocomplete_helper: LyCORIS path is the same as LORA path, skipping")
|
||||
|
||||
if model_keyword_installed:
|
||||
update_hash_cache()
|
||||
|
||||
if shared.prompt_styles is not None:
|
||||
write_style_names()
|
||||
|
||||
write_temp_files()
|
||||
|
||||
# Register autocomplete options
|
||||
def on_ui_settings():
|
||||
TAC_SECTION = ("tac", "Tag Autocomplete")
|
||||
# Main tag file
|
||||
shared.opts.add_option("tac_tagFile", shared.OptionInfo("danbooru.csv", "Tag filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
|
||||
# Active in settings
|
||||
shared.opts.add_option("tac_active", shared.OptionInfo(True, "Enable Tag Autocompletion", section=TAC_SECTION))
|
||||
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] [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
|
||||
shared.opts.add_option("tac_slidingPopup", shared.OptionInfo(True, "Move completion popup together with text cursor", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_maxResults", shared.OptionInfo(5, "Maximum results", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_showAllResults", shared.OptionInfo(False, "Show all results", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_resultStepLength", shared.OptionInfo(100, "How many results to load at once", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_delayTime", shared.OptionInfo(100, "Time in ms to wait before triggering completion again (Requires restart)", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_useWildcards", shared.OptionInfo(True, "Search for wildcards", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_useEmbeddings", shared.OptionInfo(True, "Search for embeddings", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_useHypernetworks", shared.OptionInfo(True, "Search for hypernetworks", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_useLoras", shared.OptionInfo(True, "Search for Loras", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_useLycos", shared.OptionInfo(True, "Search for LyCORIS/LoHa", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_showWikiLinks", shared.OptionInfo(False, "Show '?' next to tags, linking to its Danbooru or e621 wiki page (Warning: This is an external site and very likely contains NSFW examples!)", section=TAC_SECTION))
|
||||
# Insertion related settings
|
||||
shared.opts.add_option("tac_replaceUnderscores", shared.OptionInfo(True, "Replace underscores with spaces on insertion", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_escapeParentheses", shared.OptionInfo(True, "Escape parentheses on insertion", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_appendComma", shared.OptionInfo(True, "Append comma on tag autocompletion", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_wildcardCompletionMode", shared.OptionInfo("To next folder level", "How to complete nested wildcard paths (e.g. \"hair/colours/light/...\")", gr.Dropdown, lambda: {"choices": ["To next folder level","To first difference","Always fully"]}, section=TAC_SECTION))
|
||||
# Alias settings
|
||||
shared.opts.add_option("tac_alias.searchByAlias", shared.OptionInfo(True, "Search by alias", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_alias.onlyShowAlias", shared.OptionInfo(False, "Only show alias", section=TAC_SECTION))
|
||||
# Translation 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))
|
||||
|
||||
# Backwards compatibility for pre 1.3.0 webui versions
|
||||
if not (hasattr(shared.OptionInfo, "info") and callable(getattr(shared.OptionInfo, "info"))):
|
||||
def info(self, info):
|
||||
self.label += f" ({info})"
|
||||
return self
|
||||
shared.OptionInfo.info = info
|
||||
if not (hasattr(shared.OptionInfo, "needs_restart") and callable(getattr(shared.OptionInfo, "needs_restart"))):
|
||||
def needs_restart(self):
|
||||
self.label += " (Requires restart)"
|
||||
return self
|
||||
shared.OptionInfo.needs_restart = needs_restart
|
||||
|
||||
# Dictionary of function options and their explanations
|
||||
frequency_sort_functions = {
|
||||
"Logarithmic (weak)": "Will respect the base order and slightly prefer often used tags",
|
||||
"Logarithmic (strong)": "Same as Logarithmic (weak), but with a stronger bias",
|
||||
"Usage first": "Will list used tags by frequency before all others",
|
||||
}
|
||||
|
||||
tac_options = {
|
||||
# Main tag file
|
||||
"tac_tagFile": shared.OptionInfo("danbooru.csv", "Tag filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files),
|
||||
# Active in settings
|
||||
"tac_active": shared.OptionInfo(True, "Enable Tag Autocompletion"),
|
||||
"tac_activeIn.txt2img": shared.OptionInfo(True, "Active in txt2img").needs_restart(),
|
||||
"tac_activeIn.img2img": shared.OptionInfo(True, "Active in img2img").needs_restart(),
|
||||
"tac_activeIn.negativePrompts": shared.OptionInfo(True, "Active in negative prompts").needs_restart(),
|
||||
"tac_activeIn.thirdParty": shared.OptionInfo(True, "Active in third party textboxes").info("See <a href=\"https://github.com/DominikDoom/a1111-sd-webui-tagcomplete#-features\" target=\"_blank\">README</a> for supported extensions").needs_restart(),
|
||||
"tac_activeIn.modelList": shared.OptionInfo("", "Black/Whitelist models").info("Model names [with file extension] or their hashes, separated by commas"),
|
||||
"tac_activeIn.modelListMode": shared.OptionInfo("Blacklist", "Mode to use for model list", gr.Dropdown, lambda: {"choices": ["Blacklist","Whitelist"]}),
|
||||
# Results related settings
|
||||
"tac_slidingPopup": shared.OptionInfo(True, "Move completion popup together with text cursor"),
|
||||
"tac_maxResults": shared.OptionInfo(5, "Maximum results"),
|
||||
"tac_showAllResults": shared.OptionInfo(False, "Show all results"),
|
||||
"tac_resultStepLength": shared.OptionInfo(100, "How many results to load at once"),
|
||||
"tac_delayTime": shared.OptionInfo(100, "Time in ms to wait before triggering completion again").needs_restart(),
|
||||
"tac_useWildcards": shared.OptionInfo(True, "Search for wildcards"),
|
||||
"tac_sortWildcardResults": shared.OptionInfo(True, "Sort wildcard file contents alphabetically").info("If your wildcard files have a specific custom order, disable this to keep it"),
|
||||
"tac_wildcardExclusionList": shared.OptionInfo("", "Wildcard folder exclusion list").info("Add folder names that shouldn't be searched for wildcards, separated by comma.").needs_restart(),
|
||||
"tac_skipWildcardRefresh": shared.OptionInfo(False, "Don't re-scan for wildcard files when pressing the extra networks refresh button").info("Useful to prevent hanging if you use a very large wildcard collection."),
|
||||
"tac_useEmbeddings": shared.OptionInfo(True, "Search for embeddings"),
|
||||
"tac_includeEmbeddingsInNormalResults": shared.OptionInfo(False, "Include embeddings in normal tag results").info("The 'JumpTo...' keybinds (End & Home key by default) will select the first non-embedding result of their direction on the first press for quick navigation in longer lists."),
|
||||
"tac_useHypernetworks": shared.OptionInfo(True, "Search for hypernetworks"),
|
||||
"tac_useLoras": shared.OptionInfo(True, "Search for Loras"),
|
||||
"tac_useLycos": shared.OptionInfo(True, "Search for LyCORIS/LoHa"),
|
||||
"tac_useLoraPrefixForLycos": shared.OptionInfo(True, "Use the '<lora:' prefix instead of '<lyco:' for models in the LyCORIS folder").info("The lyco prefix is included for backwards compatibility and not used anymore by default. Disable this if you are on an old webui version without built-in lyco support."),
|
||||
"tac_showWikiLinks": shared.OptionInfo(False, "Show '?' next to tags, linking to its Danbooru or e621 wiki page").info("Warning: This is an external site and very likely contains NSFW examples!"),
|
||||
"tac_showExtraNetworkPreviews": shared.OptionInfo(True, "Show preview thumbnails for extra networks if available"),
|
||||
"tac_modelSortOrder": shared.OptionInfo("Name", "Model sort order", gr.Dropdown, lambda: {"choices": list(sort_criteria.keys())}).info("Order for extra network models and wildcards in dropdown"),
|
||||
"tac_useStyleVars": shared.OptionInfo(False, "Search for webui style names").info("Suggests style names from the webui dropdown with '$'. Currently requires a secondary extension like <a href=\"https://github.com/SirVeggie/extension-style-vars\" target=\"_blank\">style-vars</a> to actually apply the styles before generating."),
|
||||
# Frequency sorting settings
|
||||
"tac_frequencySort": shared.OptionInfo(True, "Locally record tag usage and sort frequent tags higher").info("Will also work for extra networks, keeping the specified base order"),
|
||||
"tac_frequencyFunction": shared.OptionInfo("Logarithmic (weak)", "Function to use for frequency sorting", gr.Dropdown, lambda: {"choices": list(frequency_sort_functions.keys())}).info("; ".join([f'<b>{key}</b>: {val}' for key, val in frequency_sort_functions.items()])),
|
||||
"tac_frequencyMinCount": shared.OptionInfo(3, "Minimum number of uses for a tag to be considered frequent").info("Tags with less uses than this will not be sorted higher, even if the sorting function would normally result in a higher position."),
|
||||
"tac_frequencyMaxAge": shared.OptionInfo(30, "Maximum days since last use for a tag to be considered frequent").info("Similar to the above, tags that haven't been used in this many days will not be sorted higher. Set to 0 to disable."),
|
||||
"tac_frequencyRecommendCap": shared.OptionInfo(10, "Maximum number of recommended tags").info("Limits the maximum number of recommended tags to not drown out normal results. Set to 0 to disable."),
|
||||
"tac_frequencyIncludeAlias": shared.OptionInfo(False, "Frequency sorting matches aliases for frequent tags").info("Tag frequency will be increased for the main tag even if an alias is used for completion. This option can be used to override the default behavior of alias results being ignored for frequency sorting."),
|
||||
# Insertion related settings
|
||||
"tac_replaceUnderscores": shared.OptionInfo(True, "Replace underscores with spaces on insertion"),
|
||||
"tac_escapeParentheses": shared.OptionInfo(True, "Escape parentheses on insertion"),
|
||||
"tac_appendComma": shared.OptionInfo(True, "Append comma on tag autocompletion"),
|
||||
"tac_appendSpace": shared.OptionInfo(True, "Append space on tag autocompletion").info("will append after comma if the above is enabled"),
|
||||
"tac_alwaysSpaceAtEnd": shared.OptionInfo(True, "Always append space if inserting at the end of the textbox").info("takes precedence over the regular space setting for that position"),
|
||||
"tac_modelKeywordCompletion": shared.OptionInfo("Never", "Try to add known trigger words for LORA/LyCO models", gr.Dropdown, lambda: {"choices": ["Never","Only user list","Always"]}).info("Will use & prefer the native activation keywords settable in the extra networks UI. Other functionality requires the <a href=\"https://github.com/mix1009/model-keyword\" target=\"_blank\">model-keyword</a> extension to be installed, but will work with it disabled.").needs_restart(),
|
||||
"tac_modelKeywordLocation": shared.OptionInfo("Start of prompt", "Where to insert the trigger keyword", gr.Dropdown, lambda: {"choices": ["Start of prompt","End of prompt","Before LORA/LyCO"]}).info("Only relevant if the above option is enabled"),
|
||||
"tac_wildcardCompletionMode": shared.OptionInfo("To next folder level", "How to complete nested wildcard paths", gr.Dropdown, lambda: {"choices": ["To next folder level","To first difference","Always fully"]}).info("e.g. \"hair/colours/light/...\""),
|
||||
# Alias settings
|
||||
"tac_alias.searchByAlias": shared.OptionInfo(True, "Search by alias"),
|
||||
"tac_alias.onlyShowAlias": shared.OptionInfo(False, "Only show alias"),
|
||||
# Translation settings
|
||||
"tac_translation.translationFile": shared.OptionInfo("None", "Translation filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files),
|
||||
"tac_translation.oldFormat": shared.OptionInfo(False, "Translation file uses old 3-column translation format instead of the new 2-column one"),
|
||||
"tac_translation.searchByTranslation": shared.OptionInfo(True, "Search by translation"),
|
||||
"tac_translation.liveTranslation": shared.OptionInfo(False, "Show live tag translation below prompt ").info("WIP, expect some bugs"),
|
||||
# Extra file settings
|
||||
"tac_extra.extraFile": shared.OptionInfo("extra-quality-tags.csv", "Extra filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files).info("for small sets of custom tags"),
|
||||
"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"]}),
|
||||
# Chant settings
|
||||
"tac_chantFile": shared.OptionInfo("demo-chants.json", "Chant filename", gr.Dropdown, lambda: {"choices": json_files_withnone}, refresh=update_json_files).info("Chants are longer prompt presets"),
|
||||
}
|
||||
|
||||
# Add normal settings
|
||||
for key, opt in tac_options.items():
|
||||
opt.section = TAC_SECTION
|
||||
shared.opts.add_option(key, opt)
|
||||
|
||||
# Settings that need special treatment
|
||||
# Custom mappings
|
||||
keymapDefault = """\
|
||||
{
|
||||
@@ -366,11 +621,25 @@ def on_ui_settings():
|
||||
"6": ["red", "maroon"],
|
||||
"7": ["whitesmoke", "black"],
|
||||
"8": ["seagreen", "darkseagreen"]
|
||||
},
|
||||
"derpibooru": {
|
||||
"-1": ["red", "maroon"],
|
||||
"0": ["#60d160", "#3d9d3d"],
|
||||
"1": ["#fff956", "#918e2e"],
|
||||
"3": ["#fd9961", "#a14c2e"],
|
||||
"4": ["#cf5bbe", "#6c1e6c"],
|
||||
"5": ["#3c8ad9", "#1e5e93"],
|
||||
"6": ["#a6a6a6", "#555555"],
|
||||
"7": ["#47abc1", "#1f6c7c"],
|
||||
"8": ["#7871d0", "#392f7d"],
|
||||
"9": ["#df3647", "#8e1c2b"],
|
||||
"10": ["#c98f2b", "#7b470e"],
|
||||
"11": ["#e87ebe", "#a83583"]
|
||||
}
|
||||
}\
|
||||
"""
|
||||
keymapLabel = "Configure Hotkeys. For possible values, see https://www.w3.org/TR/uievents-key, or leave empty / set to 'None' to disable. Must be valid JSON."
|
||||
colorLabel = "Configure colors. See https://github.com/DominikDoom/a1111-sd-webui-tagcomplete#colors for info. Must be valid JSON."
|
||||
colorLabel = "Configure colors. See the Settings section in the README for more info. Must be valid JSON."
|
||||
|
||||
try:
|
||||
shared.opts.add_option("tac_keymap", shared.OptionInfo(keymapDefault, keymapLabel, gr.Code, lambda: {"language": "json", "interactive": True}, section=TAC_SECTION))
|
||||
@@ -380,5 +649,187 @@ def on_ui_settings():
|
||||
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)
|
||||
|
||||
def get_style_mtime():
|
||||
try:
|
||||
style_file = getattr(shared, "styles_filename", "styles.csv")
|
||||
# Check in case a list is returned
|
||||
if isinstance(style_file, list):
|
||||
style_file = style_file[0]
|
||||
|
||||
style_file = Path(FILE_DIR).joinpath(style_file)
|
||||
if Path.exists(style_file):
|
||||
return style_file.stat().st_mtime
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
last_style_mtime = get_style_mtime()
|
||||
|
||||
def api_tac(_: gr.Blocks, app: FastAPI):
|
||||
async def get_json_info(base_path: Path, filename: str = None):
|
||||
if base_path is None or (not base_path.exists()):
|
||||
return Response(status_code=404)
|
||||
|
||||
try:
|
||||
json_candidates = glob.glob(base_path.as_posix() + f"/**/{filename}.json", recursive=True)
|
||||
if json_candidates is not None and len(json_candidates) > 0 and Path(json_candidates[0]).is_file():
|
||||
return FileResponse(json_candidates[0])
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": e}, status_code=500)
|
||||
|
||||
async def get_preview_thumbnail(base_path: Path, filename: str = None, blob: bool = False):
|
||||
if base_path is None or (not base_path.exists()):
|
||||
return Response(status_code=404)
|
||||
|
||||
try:
|
||||
img_glob = glob.glob(base_path.as_posix() + f"/**/{filename}.*", recursive=True)
|
||||
img_candidates = [img for img in img_glob if Path(img).suffix in [".png", ".jpg", ".jpeg", ".webp", ".gif"] and Path(img).is_file()]
|
||||
if img_candidates is not None and len(img_candidates) > 0:
|
||||
if blob:
|
||||
return FileResponse(img_candidates[0])
|
||||
else:
|
||||
return JSONResponse({"url": urllib.parse.quote(img_candidates[0])})
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": e}, status_code=500)
|
||||
|
||||
@app.post("/tacapi/v1/refresh-temp-files")
|
||||
async def api_refresh_temp_files():
|
||||
refresh_temp_files()
|
||||
|
||||
@app.post("/tacapi/v1/refresh-embeddings")
|
||||
async def api_refresh_embeddings():
|
||||
refresh_embeddings(force=False)
|
||||
|
||||
@app.get("/tacapi/v1/lora-info/{lora_name}")
|
||||
async def get_lora_info(lora_name):
|
||||
return await get_json_info(LORA_PATH, lora_name)
|
||||
|
||||
@app.get("/tacapi/v1/lyco-info/{lyco_name}")
|
||||
async def get_lyco_info(lyco_name):
|
||||
return await get_json_info(LYCO_PATH, lyco_name)
|
||||
|
||||
@app.get("/tacapi/v1/lora-cached-hash/{lora_name}")
|
||||
async def get_lora_cached_hash(lora_name: str):
|
||||
path_glob = glob.glob(LORA_PATH.as_posix() + f"/**/{lora_name}.*", recursive=True)
|
||||
paths = [lora for lora in path_glob if Path(lora).suffix in [".safetensors", ".ckpt", ".pt"] and Path(lora).is_file()]
|
||||
if paths is not None and len(paths) > 0:
|
||||
path = paths[0]
|
||||
hash = hashes.sha256_from_cache(path, f"lora/{lora_name}", path.endswith(".safetensors"))
|
||||
if hash is not None:
|
||||
return hash
|
||||
|
||||
return None
|
||||
|
||||
def get_path_for_type(type):
|
||||
if type == "lora":
|
||||
return LORA_PATH
|
||||
elif type == "lyco":
|
||||
return LYCO_PATH
|
||||
elif type == "hypernetwork":
|
||||
return HYP_PATH
|
||||
elif type == "embedding":
|
||||
return EMB_PATH
|
||||
else:
|
||||
return None
|
||||
|
||||
@app.get("/tacapi/v1/thumb-preview/{filename}")
|
||||
async def get_thumb_preview(filename, type):
|
||||
return await get_preview_thumbnail(get_path_for_type(type), filename, False)
|
||||
|
||||
@app.get("/tacapi/v1/thumb-preview-blob/{filename}")
|
||||
async def get_thumb_preview_blob(filename, type):
|
||||
return await get_preview_thumbnail(get_path_for_type(type), filename, True)
|
||||
|
||||
@app.get("/tacapi/v1/wildcard-contents")
|
||||
async def get_wildcard_contents(basepath: str, filename: str):
|
||||
if basepath is None or basepath == "":
|
||||
return Response(status_code=404)
|
||||
|
||||
base = Path(basepath)
|
||||
if base is None or (not base.exists()):
|
||||
return Response(status_code=404)
|
||||
|
||||
try:
|
||||
wildcard_path = base.joinpath(filename)
|
||||
if wildcard_path.exists() and wildcard_path.is_file():
|
||||
return FileResponse(wildcard_path)
|
||||
else:
|
||||
return Response(status_code=404)
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": e}, status_code=500)
|
||||
|
||||
@app.get("/tacapi/v1/refresh-styles-if-changed")
|
||||
async def refresh_styles_if_changed():
|
||||
global last_style_mtime
|
||||
|
||||
mtime = get_style_mtime()
|
||||
if mtime is not None and mtime > last_style_mtime:
|
||||
last_style_mtime = mtime
|
||||
# Update temp file
|
||||
if shared.prompt_styles is not None:
|
||||
write_style_names()
|
||||
|
||||
return Response(status_code=200) # Success
|
||||
else:
|
||||
return Response(status_code=304) # Not modified
|
||||
def db_request(func, get = False):
|
||||
if db is not None:
|
||||
try:
|
||||
if get:
|
||||
ret = func()
|
||||
if ret is list:
|
||||
ret = [{"name": t[0], "type": t[1], "count": t[2], "lastUseDate": t[3]} for t in ret]
|
||||
return JSONResponse({"result": ret})
|
||||
else:
|
||||
func()
|
||||
except sqlite3.Error as e:
|
||||
return JSONResponse({"error": e.__cause__}, status_code=500)
|
||||
else:
|
||||
return JSONResponse({"error": "Database not initialized"}, status_code=500)
|
||||
|
||||
@app.post("/tacapi/v1/increase-use-count")
|
||||
async def increase_use_count(tagname: str, ttype: int, neg: bool):
|
||||
db_request(lambda: db.increase_tag_count(tagname, ttype, neg))
|
||||
|
||||
@app.get("/tacapi/v1/get-use-count")
|
||||
async def get_use_count(tagname: str, ttype: int, neg: bool):
|
||||
return db_request(lambda: db.get_tag_count(tagname, ttype, neg), get=True)
|
||||
|
||||
# Small dataholder class
|
||||
class UseCountListRequest(BaseModel):
|
||||
tagNames: list[str]
|
||||
tagTypes: list[int]
|
||||
neg: bool = False
|
||||
|
||||
# Semantically weird to use post here, but it's required for the body on js side
|
||||
@app.post("/tacapi/v1/get-use-count-list")
|
||||
async def get_use_count_list(body: UseCountListRequest):
|
||||
# If a date limit is set > 0, pass it to the db
|
||||
date_limit = getattr(shared.opts, "tac_frequencyMaxAge", 30)
|
||||
date_limit = date_limit if date_limit > 0 else None
|
||||
|
||||
if db:
|
||||
count_list = list(db.get_tag_counts(body.tagNames, body.tagTypes, body.neg, date_limit))
|
||||
else:
|
||||
count_list = None
|
||||
|
||||
# If a limit is set, return at max the top n results by count
|
||||
if count_list and len(count_list):
|
||||
limit = int(min(getattr(shared.opts, "tac_frequencyRecommendCap", 10), len(count_list)))
|
||||
# Sort by count and return the top n
|
||||
if limit > 0:
|
||||
count_list = sorted(count_list, key=lambda x: x[2], reverse=True)[:limit]
|
||||
|
||||
return db_request(lambda: count_list, get=True)
|
||||
|
||||
@app.put("/tacapi/v1/reset-use-count")
|
||||
async def reset_use_count(tagname: str, ttype: int, pos: bool, neg: bool):
|
||||
db_request(lambda: db.reset_tag_count(tagname, ttype, pos, neg))
|
||||
|
||||
@app.get("/tacapi/v1/get-all-use-counts")
|
||||
async def get_all_tag_counts():
|
||||
return db_request(lambda: db.get_all_tags(), get=True)
|
||||
|
||||
script_callbacks.on_app_started(api_tac)
|
||||
|
||||
190
scripts/tag_frequency_db.py
Normal file
190
scripts/tag_frequency_db.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import sqlite3
|
||||
from contextlib import contextmanager
|
||||
|
||||
from scripts.shared_paths import TAGS_PATH
|
||||
|
||||
db_file = TAGS_PATH.joinpath("tag_frequency.db")
|
||||
timeout = 30
|
||||
db_ver = 1
|
||||
|
||||
|
||||
@contextmanager
|
||||
def transaction(db=db_file):
|
||||
"""Context manager for database transactions.
|
||||
Ensures that the connection is properly closed after the transaction.
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(db, timeout=timeout)
|
||||
|
||||
conn.isolation_level = None
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("BEGIN")
|
||||
yield cursor
|
||||
cursor.execute("COMMIT")
|
||||
except sqlite3.Error as e:
|
||||
print("Tag Autocomplete: Frequency database error:", e)
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
class TagFrequencyDb:
|
||||
"""Class containing creation and interaction methods for the tag frequency database"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.version = self.__check()
|
||||
|
||||
def __check(self):
|
||||
if not db_file.exists():
|
||||
print("Tag Autocomplete: Creating frequency database")
|
||||
with transaction() as cursor:
|
||||
self.__create_db(cursor)
|
||||
self.__update_db_data(cursor, "version", db_ver)
|
||||
print("Tag Autocomplete: Database successfully created")
|
||||
|
||||
return self.__get_version()
|
||||
|
||||
def __create_db(self, cursor: sqlite3.Cursor):
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS db_data (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tag_frequency (
|
||||
name TEXT NOT NULL,
|
||||
type INT NOT NULL,
|
||||
count_pos INT,
|
||||
count_neg INT,
|
||||
last_used TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (name, type)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
def __update_db_data(self, cursor: sqlite3.Cursor, key, value):
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT OR REPLACE
|
||||
INTO db_data (key, value)
|
||||
VALUES (?, ?)
|
||||
""",
|
||||
(key, value),
|
||||
)
|
||||
|
||||
def __get_version(self):
|
||||
db_version = None
|
||||
with transaction() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT value
|
||||
FROM db_data
|
||||
WHERE key = 'version'
|
||||
"""
|
||||
)
|
||||
db_version = cursor.fetchone()
|
||||
|
||||
return db_version[0] if db_version else 0
|
||||
|
||||
def get_all_tags(self):
|
||||
with transaction() as cursor:
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT name, type, count_pos, count_neg, last_used
|
||||
FROM tag_frequency
|
||||
WHERE count_pos > 0 OR count_neg > 0
|
||||
ORDER BY count_pos + count_neg DESC
|
||||
"""
|
||||
)
|
||||
tags = cursor.fetchall()
|
||||
|
||||
return tags
|
||||
|
||||
def get_tag_count(self, tag, ttype, negative=False):
|
||||
count_str = "count_neg" if negative else "count_pos"
|
||||
with transaction() as cursor:
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT {count_str}, last_used
|
||||
FROM tag_frequency
|
||||
WHERE name = ? AND type = ?
|
||||
""",
|
||||
(tag, ttype),
|
||||
)
|
||||
tag_count = cursor.fetchone()
|
||||
|
||||
if tag_count:
|
||||
return tag_count[0], tag_count[1]
|
||||
else:
|
||||
return 0, None
|
||||
|
||||
def get_tag_counts(self, tags: list[str], ttypes: list[str], negative=False, date_limit=None):
|
||||
count_str = "count_neg" if negative else "count_pos"
|
||||
with transaction() as cursor:
|
||||
for tag, ttype in zip(tags, ttypes):
|
||||
if date_limit is not None:
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT {count_str}, last_used
|
||||
FROM tag_frequency
|
||||
WHERE name = ? AND type = ?
|
||||
AND last_used > datetime('now', '-' || ? || ' days')
|
||||
""",
|
||||
(tag, ttype, date_limit),
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT {count_str}, last_used
|
||||
FROM tag_frequency
|
||||
WHERE name = ? AND type = ?
|
||||
""",
|
||||
(tag, ttype),
|
||||
)
|
||||
tag_count = cursor.fetchone()
|
||||
if tag_count:
|
||||
yield (tag, ttype, tag_count[0], tag_count[1])
|
||||
else:
|
||||
yield (tag, ttype, 0, None)
|
||||
|
||||
def increase_tag_count(self, tag, ttype, negative=False):
|
||||
pos_count = self.get_tag_count(tag, ttype, False)[0]
|
||||
neg_count = self.get_tag_count(tag, ttype, True)[0]
|
||||
|
||||
if negative:
|
||||
neg_count += 1
|
||||
else:
|
||||
pos_count += 1
|
||||
|
||||
with transaction() as cursor:
|
||||
cursor.execute(
|
||||
f"""
|
||||
INSERT OR REPLACE
|
||||
INTO tag_frequency (name, type, count_pos, count_neg)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(tag, ttype, pos_count, neg_count),
|
||||
)
|
||||
|
||||
def reset_tag_count(self, tag, ttype, positive=True, negative=False):
|
||||
if positive and negative:
|
||||
set_str = "count_pos = 0, count_neg = 0"
|
||||
elif positive:
|
||||
set_str = "count_pos = 0"
|
||||
elif negative:
|
||||
set_str = "count_neg = 0"
|
||||
|
||||
with transaction() as cursor:
|
||||
cursor.execute(
|
||||
f"""
|
||||
UPDATE tag_frequency
|
||||
SET {set_str}
|
||||
WHERE name = ? AND type = ?
|
||||
""",
|
||||
(tag, ttype),
|
||||
)
|
||||
113301
tags/EnglishDictionary.csv
Normal file
113301
tags/EnglishDictionary.csv
Normal file
File diff suppressed because it is too large
Load Diff
168662
tags/danbooru.csv
168662
tags/danbooru.csv
File diff suppressed because it is too large
Load Diff
@@ -28,5 +28,17 @@
|
||||
"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
|
||||
},
|
||||
{
|
||||
"name": "Pony-Positive",
|
||||
"terms": "Pony,Score,Positive,Quality",
|
||||
"content": "score_9, score_8_up, score_7_up, score_6_up, source_anime, source_furry, source_pony, source_cartoon",
|
||||
"color": 1
|
||||
},
|
||||
{
|
||||
"name": "Pony-Negative",
|
||||
"terms": "Pony,Score,Negative,Quality",
|
||||
"content": "score_1, score_2, score_3, score_4, score_5, source_anime, source_furry, source_pony, source_cartoon",
|
||||
"color": 3
|
||||
}
|
||||
]
|
||||
117315
tags/derpibooru.csv
Normal file
117315
tags/derpibooru.csv
Normal file
File diff suppressed because it is too large
Load Diff
22419
tags/e621_sfw.csv
Normal file
22419
tags/e621_sfw.csv
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user