Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9273fb74d | ||
|
|
cf30c94faa | ||
|
|
828c08df0c | ||
|
|
3dbfd29274 | ||
|
|
86527e35b4 | ||
|
|
1bc609893b | ||
|
|
a536d58cf2 | ||
|
|
4c921da6cb | ||
|
|
98bf626ec1 | ||
|
|
57f80f0991 | ||
|
|
31f8fe4517 | ||
|
|
292c99ae17 | ||
|
|
86b63047c2 | ||
|
|
5e4b4fae04 | ||
|
|
7a73a5b1b3 | ||
|
|
47635d5aea | ||
|
|
4415e22744 | ||
|
|
939569d076 | ||
|
|
c8820c89d1 | ||
|
|
d97900de4e | ||
|
|
8c54defc6c | ||
|
|
74565b442f |
@@ -8,11 +8,6 @@
|
||||
|
||||
**ComfyUI-Autocomplete-Plus** is a custom node that provides multiple input assistance features for any text area in [ComfyUI](https://github.com/comfyanonymous/ComfyUI). Currently, it supports Danbooru and e621 tags (e621 does not support some functions).
|
||||
|
||||
## Recent Updates :fire:
|
||||
- Added automatic and manual update check functionality for CSV files
|
||||
- Fixed bug where autocomplete suggestions wouldn't display at certain timings with Microsoft IME
|
||||
- Support for loading and displaying e621 tag CSV
|
||||
|
||||
## Features
|
||||
|
||||
- **:zap:No setup required**: Automatically downloads CSV data optimized for Danbooru tags.
|
||||
@@ -28,11 +23,16 @@
|
||||
|
||||
## Installation
|
||||
|
||||
### ComfyUI-Manager
|
||||
|
||||
1. Search for `Autocomplete-Plus` in [ComfyUI-Manager](https://github.com/Comfy-Org/ComfyUI-Manager), install the custom node that appears, and restart.
|
||||
2. The necessary CSV data will be automatically downloaded from HuggingFace upon startup.
|
||||
|
||||
### Manual
|
||||
|
||||
1. Clone or copy this repository into the `custom_nodes` folder of ComfyUI.
|
||||
`git clone https://github.com/newtextdoc1111/ComfyUI-Autocomplete-Plus.git`
|
||||
2. Launch ComfyUI. The necessary CSV data will be automatically downloaded from HuggingFace on the first launch only.
|
||||
2. Launch ComfyUI. The necessary CSV data will be automatically downloaded from HuggingFace upon startup.
|
||||
|
||||
## Autocomplete
|
||||
|
||||
@@ -47,7 +47,7 @@ When you type in a text input area, tags that partially match the text are displ
|
||||
|
||||

|
||||
|
||||
When you select any tag in a text input area, highly related tags list are displayed. The UI's position and size are automatically adjusted based on the text area being edited.
|
||||
When you select any tag in a text input area, a list of highly related tags is displayed. You can insert a tag by clicking it or by selecting it with the up/down arrow keys and then pressing Enter or Tab. The UI's position and size are automatically adjusted based on the text area being edited.
|
||||
|
||||
- The display position is primarily at the bottom of the text area and automatically adjusts vertically based on available space.
|
||||
- You can switch between vertical and horizontal display positions using the "↕️|↔️" button in the header.
|
||||
@@ -87,7 +87,12 @@ Also, displaying related tags is not supported.
|
||||
|
||||
### User CSV
|
||||
|
||||
Users can also use their own CSV files. For example, frequently used meta tags can be added to the autocomplete suggestions by placing a file named `danbooru_tags_meta.csv` in the `data` folder.
|
||||
Users can also use their own CSV files. CSV files should be placed in the `data` folder according to the following naming convention:
|
||||
|
||||
- **CSV for Autocomplete**: `<danbooru | e621>_tags*.csv`
|
||||
- **CSV for Related Tags**: `<danbooru | e621>_tags_cooccurrence*.csv`
|
||||
|
||||
For example, you can add frequently used meta tags to the autocomplete suggestions by placing a file named `danbooru_tags_meta.csv` in the `data` folder.
|
||||
A header row is not required. A browser reload is necessary to apply the changes.
|
||||
|
||||
**Example of meta tags:**
|
||||
@@ -101,6 +106,15 @@ low_quality,5,9999999,
|
||||
worst_quality,5,9999999,
|
||||
```
|
||||
|
||||
When the browser is reloaded, you can check the list of loaded CSV files in the ComfyUI console log. If a file is not included in the log output, please verify that the file name follows the naming convention.
|
||||
|
||||
**Example of ComfyUI console log output:**
|
||||
```
|
||||
[Autocomplete-Plus] CSV file status:
|
||||
* Danbooru -> base: True, extra: danbooru_tags_meta.csv // If displayed here, meta tags can be autocompleted
|
||||
* E621 -> base: False, extra:
|
||||
```
|
||||
|
||||
>[!NOTE]
|
||||
> If there are multiple user CSV files, they are loaded in alphabetical order. If the same tag exists in multiple files, the one loaded first is retained. The basic CSV is loaded last.
|
||||
|
||||
@@ -131,6 +145,33 @@ worst_quality,5,9999999,
|
||||
|
||||
- **Check CSV updates**: Click the "Check Now" button to check if new CSV files are available in HuggingFace and download them if necessary.
|
||||
|
||||
## Advanced Settings
|
||||
|
||||
### Disabling CSV Update Check on Startup
|
||||
|
||||
By default, ComfyUI performs CSV file update checks and downloads at regular intervals during startup.
|
||||
When starting in an environment without internet access, startup may be delayed until a timeout occurs.
|
||||
|
||||
You can skip the check process during ComfyUI startup by following these steps:
|
||||
|
||||
1. Start ComfyUI once with this custom node installed to generate the `csv_meta.json` file.
|
||||
The `csv_meta.json` file is created directly under this custom node's folder.
|
||||
2. Open `csv_meta.json` in a text editor and change the value of `check_updates_on_startup` from `true` to `false` and save.
|
||||
If `check_updates_on_startup` does not exist, add it under `version`.
|
||||
|
||||
**`csv_meta.json` after modification:**
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"check_updates_on_startup": false,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Additional notes:**
|
||||
- The check process will not be performed until the value of `check_updates_on_startup` is changed back to `true` or the `version` is switched.
|
||||
- Even when `check_updates_on_startup` is `false`, manual checking is still possible by pressing the `Check CSV updates` button in the Autocomplete Plus settings.
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Performance
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
from .modules.api import *
|
||||
from .modules.api import *
|
||||
from .modules import downloader
|
||||
|
||||
# check and download necessary csv files
|
||||
|
||||
+49
-8
@@ -6,11 +6,6 @@
|
||||
|
||||
**ComfyUI-Autocomplete-Plus** は、[ComfyUI](https://github.com/comfyanonymous/ComfyUI) の任意のテキストエリアに複数の入力支援機能を提供するカスタムノードです。現在はDanbooruとe621のタグに対応しています(e621は一部の機能が未対応です)。
|
||||
|
||||
## 最近の更新 :fire:
|
||||
- CSV ファイルの自動・手動更新チェック機能の追加
|
||||
- Microsoft IMEでオートコンプリート候補が表示されないタイミングがある不具合の修正
|
||||
- e621タグ CSV の読み込みと表示のサポート
|
||||
|
||||
## 特徴
|
||||
|
||||
- **:zap:セットアップ不要**: Danbooruタグに最適化された CSV データを自動でダウンロード
|
||||
@@ -26,11 +21,16 @@
|
||||
|
||||
## インストール
|
||||
|
||||
### ComfyUI-Manager
|
||||
|
||||
1. [ComfyUI-Manager](https://github.com/Comfy-Org/ComfyUI-Manager) で `Autocomplete-Plus` と検索して表示されたカスタムノードをインストールし、再起動します
|
||||
2. 起動時に必要な CSV データが HuggingFace から自動的にダウンロードされます
|
||||
|
||||
### マニュアル
|
||||
|
||||
1. このリポジトリを ComfyUI の `custom_nodes` フォルダにクローンまたはコピーします
|
||||
`git clone https://github.com/newtextdoc1111/ComfyUI-Autocomplete-Plus.git`
|
||||
3. ComfyUI を起動します。初回起動時のみ、必要な CSV データが HuggingFace から自動的にダウンロードされます
|
||||
2. ComfyUI を起動します。起動時に必要な CSV データが HuggingFace から自動的にダウンロードされます
|
||||
|
||||
## オートコンプリート
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||

|
||||
|
||||
テキスト入力エリアの任意のタグを選択すると、関連性の高いタグを一覧表示します。UIは編集中のテキストエリアを基準に位置とサイズが自動で調整されます。
|
||||
テキスト入力エリアの任意のタグを選択すると、関連性の高いタグを一覧表示します。タグをクリックするか、キーボードの上下キーで選択後にEnterまたはTabキーでタグを挿入出来ます。UIは編集中のテキストエリアを基準に位置とサイズが自動で調整されます。
|
||||
|
||||
- 表示位置は、テキストエリアの下部を基本とし、空きスペースに応じて上下に自動調整されます
|
||||
- ヘッダーの「↕️|↔️」ボタンで上下と左右の表示位置に切り替えられます
|
||||
@@ -85,7 +85,12 @@
|
||||
|
||||
### ユーザーCSV
|
||||
|
||||
ユーザーが自身で用意した CSV を使用することも可能です。例として、よく使うメタタグを `danbooru_tags_meta.csv` の名前で `data` フォルダーに配置することでオートコンプリート候補に追加できます。
|
||||
ユーザーが自身で用意した CSV を使用することも可能です。 CSV ファイルは以下の命名規則に従って `data` フォルダーに配置してください。
|
||||
|
||||
- **オートコンプリート用 CSV**: <danbooru | e621>_tags*.csv
|
||||
- **関連タグ用 CSV**: <danbooru | e621>_tags_cooccurrence*.csv
|
||||
|
||||
例として、よく使うメタタグを `danbooru_tags_meta.csv` の名前で `data` フォルダーに配置することでオートコンプリート候補に追加できます。
|
||||
ヘッダー行はなくても構いません。反映にはブラウザのリロードが必要です。
|
||||
|
||||
**メタタグの例**
|
||||
@@ -99,6 +104,15 @@ low_quality,5,9999999,
|
||||
worst_quality,5,9999999,
|
||||
```
|
||||
|
||||
ブラウザリロード時、ロードされる CSV ファイル一覧をComfyUIのコンソールのログで確認出来ます。ログ出力に含まれていない場合はファイル名が命名規則に沿っているか確認してください。
|
||||
|
||||
**ComfyUIコンソールログ出力の例:**
|
||||
```
|
||||
[Autocomplete-Plus] CSV file status:
|
||||
* Danbooru -> base: True, extra: danbooru_tags_meta.csv // ここに表示されていればメタタグを入力補完できます
|
||||
* E621 -> base: False, extra:
|
||||
```
|
||||
|
||||
>[!NOTE]
|
||||
> ユーザー CSV が複数ある場合アルファベット順に読み込まれます。同じタグが複数のファイルに存在する場合は先に読み込まれた方が保持されます。基本 CSV は最後にロードされます。
|
||||
|
||||
@@ -129,6 +143,33 @@ worst_quality,5,9999999,
|
||||
|
||||
- **Check CSV updates**: 「Check Now」ボタンを押すと新しい CSV ファイルがHuggingFaceにあるか確認し、必要に応じてダウンロードを行います
|
||||
|
||||
## 上級者向け設定
|
||||
|
||||
### 起動時のCSV更新チェックを無効化する
|
||||
|
||||
デフォルトの動作では、ComfyUI起動時に一定の間隔で CSV ファイルの更新チェックとダウンロード行います。
|
||||
インターネットにアクセス出来ない環境で起動した場合、タイムアウトが発生するまで起動が遅延する事があります。
|
||||
|
||||
以下の手順を行う事により、ComfyUI起動時のチェック処理をスキップする事が出来ます。
|
||||
|
||||
1. このカスタムノードをインストールした状態でComfyUIを一度起動し、 `csv_meta.json` ファイルを生成する
|
||||
`csv_meta.json` はこのカスタムノードのフォルダー直下に作成されます
|
||||
2. `csv_meat.json` をテキストエディターで開き、`check_updates_on_startup` の値を `true` -> `false` に変更し保存する
|
||||
`check_updates_on_startup` が存在しない場合、 `version` の下に追記してください
|
||||
|
||||
**変更後の `csv_meta.json`:**
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"check_updates_on_startup": false,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**補足事項:**
|
||||
- `check_updates_on_startup` の値を再び `true` にするか、 `version` が切り替わるまでチェック処理は行われなくなります
|
||||
- `check_updates_on_startup` が `false` でも、Autocompelte Plusの設定から `Check CSV updates` のボタンを押す事で手動チェックが可能です
|
||||
|
||||
## 既知の問題
|
||||
|
||||
### パフォーマンス
|
||||
|
||||
+89
-81
@@ -10,11 +10,12 @@ from . import downloader as dl
|
||||
# os.path.join(..., '..', 'data') goes up one level and then into 'data'
|
||||
DATA_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "data"))
|
||||
|
||||
DANBOORU_PREFIX = 'danbooru'
|
||||
E621_PREFIX = 'e621'
|
||||
DANBOORU_PREFIX = "danbooru"
|
||||
E621_PREFIX = "e621"
|
||||
|
||||
TAGS_SUFFIX = "tags"
|
||||
COOCCURRENCE_SUFFIX = "tags_cooccurrence"
|
||||
|
||||
TAGS_SUFFIX = 'tags'
|
||||
COOCCURRENCE_SUFFIX = 'tags_cooccurrence'
|
||||
|
||||
def get_csv_file_status():
|
||||
"""
|
||||
@@ -23,17 +24,17 @@ def get_csv_file_status():
|
||||
|
||||
data = {
|
||||
DANBOORU_PREFIX: {
|
||||
'base_tags': False,
|
||||
'extra_tags': [],
|
||||
'base_cooccurrence': False,
|
||||
'extra_cooccurrence': [],
|
||||
"base_tags": False,
|
||||
"extra_tags": [],
|
||||
"base_cooccurrence": False,
|
||||
"extra_cooccurrence": [],
|
||||
},
|
||||
E621_PREFIX: {
|
||||
'base_tags': False,
|
||||
'extra_tags': [],
|
||||
'base_cooccurrence': False,
|
||||
'extra_cooccurrence': [],
|
||||
}
|
||||
"base_tags": False,
|
||||
"extra_tags": [],
|
||||
"base_cooccurrence": False,
|
||||
"extra_cooccurrence": [],
|
||||
},
|
||||
}
|
||||
|
||||
for prefix in [DANBOORU_PREFIX, E621_PREFIX]:
|
||||
@@ -46,27 +47,28 @@ def get_csv_file_status():
|
||||
tags_extra_files = []
|
||||
cooccurrence_extra_files = []
|
||||
|
||||
all_csv_files = [f for f in os.listdir(DATA_DIR) if f.startswith(prefix) and f.endswith('.csv')]
|
||||
all_csv_files = [f for f in os.listdir(DATA_DIR) if f.startswith(prefix) and f.endswith(".csv")]
|
||||
|
||||
# Create extra CSV files list
|
||||
for filename in all_csv_files:
|
||||
if filename in [base_tags_file, base_cooccurrence_file]:
|
||||
continue # Skip base files
|
||||
continue # Skip base files
|
||||
if COOCCURRENCE_SUFFIX in filename.lower():
|
||||
cooccurrence_extra_files.append(filename)
|
||||
elif TAGS_SUFFIX in filename.lower():
|
||||
tags_extra_files.append(filename)
|
||||
|
||||
data[prefix] = {
|
||||
'base_tags': tags_base_exists,
|
||||
'extra_tags': tags_extra_files,
|
||||
'base_cooccurrence': cooccurrence_base_exists,
|
||||
'extra_cooccurrence': cooccurrence_extra_files,
|
||||
"base_tags": tags_base_exists,
|
||||
"extra_tags": tags_extra_files,
|
||||
"base_cooccurrence": cooccurrence_base_exists,
|
||||
"extra_cooccurrence": cooccurrence_extra_files,
|
||||
}
|
||||
|
||||
# Return the lists of extra files
|
||||
return data
|
||||
|
||||
|
||||
def get_last_check_time_from_metadata():
|
||||
"""
|
||||
Helper function to get the last remote check timestamp from csv_meta.json.
|
||||
@@ -75,23 +77,25 @@ def get_last_check_time_from_metadata():
|
||||
try:
|
||||
if not os.path.exists(dl.CSV_META_FILE):
|
||||
return None
|
||||
|
||||
with open(dl.CSV_META_FILE, 'r', encoding='utf-8') as f:
|
||||
|
||||
with open(dl.CSV_META_FILE, "r", encoding="utf-8") as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
|
||||
datasets = metadata.get("hf_datasets", [])
|
||||
if datasets and len(datasets) > 0:
|
||||
return datasets[0].get("last_remote_check_timestamp")
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
except (IOError, json.JSONDecodeError) as e:
|
||||
print(f"[Autocomplete-Plus] Error reading csv_meta.json: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# --- API Endpoints ---
|
||||
|
||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv')
|
||||
|
||||
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv")
|
||||
async def get_csv_list(_request):
|
||||
"""
|
||||
Returns CSV file status.
|
||||
@@ -102,37 +106,48 @@ async def get_csv_list(_request):
|
||||
|
||||
response = {
|
||||
DANBOORU_PREFIX: {
|
||||
'base_tags': csv_file_status[DANBOORU_PREFIX]['base_tags'],
|
||||
'extra_tags': len(csv_file_status[DANBOORU_PREFIX]['extra_tags']),
|
||||
'base_cooccurrence': csv_file_status[DANBOORU_PREFIX]['base_cooccurrence'],
|
||||
'extra_cooccurrence': len(csv_file_status[DANBOORU_PREFIX]['extra_cooccurrence']),
|
||||
"base_tags": csv_file_status[DANBOORU_PREFIX]["base_tags"],
|
||||
"extra_tags": csv_file_status[DANBOORU_PREFIX]["extra_tags"],
|
||||
"base_cooccurrence": csv_file_status[DANBOORU_PREFIX]["base_cooccurrence"],
|
||||
"extra_cooccurrence": csv_file_status[DANBOORU_PREFIX]["extra_cooccurrence"],
|
||||
},
|
||||
E621_PREFIX: {
|
||||
'base_tags': csv_file_status[E621_PREFIX]['base_tags'],
|
||||
'extra_tags': len(csv_file_status[E621_PREFIX]['extra_tags']),
|
||||
'base_cooccurrence': csv_file_status[E621_PREFIX]['base_cooccurrence'],
|
||||
'extra_cooccurrence': len(csv_file_status[E621_PREFIX]['extra_cooccurrence']),
|
||||
}
|
||||
"base_tags": csv_file_status[E621_PREFIX]["base_tags"],
|
||||
"extra_tags": csv_file_status[E621_PREFIX]["extra_tags"],
|
||||
"base_cooccurrence": csv_file_status[E621_PREFIX]["base_cooccurrence"],
|
||||
"extra_cooccurrence": csv_file_status[E621_PREFIX]["extra_cooccurrence"],
|
||||
},
|
||||
}
|
||||
|
||||
# Print csv file status to the console for debugging
|
||||
print(f"""[Autocomplete-Plus] CSV file status:
|
||||
* Danbooru -> base: {response[DANBOORU_PREFIX]["base_tags"]}, extra: [{", ".join(response[DANBOORU_PREFIX]["extra_tags"])}]
|
||||
* E621 -> base: {response[E621_PREFIX]["base_tags"]}, extra: [{", ".join(response[E621_PREFIX]["extra_tags"])}]""")
|
||||
|
||||
return web.json_response(response)
|
||||
|
||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv/{source}/{suffix}/base')
|
||||
|
||||
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv/{source}/{suffix}/base")
|
||||
async def get_base_tags_file(request):
|
||||
"""
|
||||
Returns the base tags CSV file.
|
||||
"""
|
||||
source = str(request.match_info['source'])
|
||||
suffix = str(request.match_info['suffix'])
|
||||
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [TAGS_SUFFIX, COOCCURRENCE_SUFFIX]:
|
||||
source = str(request.match_info["source"])
|
||||
suffix = str(request.match_info["suffix"])
|
||||
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [
|
||||
TAGS_SUFFIX,
|
||||
COOCCURRENCE_SUFFIX,
|
||||
]:
|
||||
return web.json_response({"error": "Invalid tag source or suffix"}, status=400)
|
||||
|
||||
|
||||
file_path = os.path.join(DATA_DIR, f"{source}_{suffix}.csv")
|
||||
if not os.path.exists(file_path):
|
||||
return web.json_response({"error": "Base tags file not found"}, status=404)
|
||||
|
||||
return web.FileResponse(file_path)
|
||||
|
||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv/{source}/{suffix}/extra/{index}')
|
||||
|
||||
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv/{source}/{suffix}/extra/{index}")
|
||||
async def get_extra_tags_file(request):
|
||||
"""
|
||||
Returns the extra tags CSV file at the specified index.
|
||||
@@ -140,16 +155,19 @@ async def get_extra_tags_file(request):
|
||||
try:
|
||||
csv_file_status = get_csv_file_status()
|
||||
|
||||
source = str(request.match_info['source'])
|
||||
suffix = str(request.match_info['suffix'])
|
||||
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [TAGS_SUFFIX, COOCCURRENCE_SUFFIX]:
|
||||
source = str(request.match_info["source"])
|
||||
suffix = str(request.match_info["suffix"])
|
||||
if source not in [DANBOORU_PREFIX, E621_PREFIX] or suffix not in [
|
||||
TAGS_SUFFIX,
|
||||
COOCCURRENCE_SUFFIX,
|
||||
]:
|
||||
return web.json_response({"error": "Invalid tag source or suffix"}, status=400)
|
||||
|
||||
index = int(request.match_info['index'])
|
||||
if index < 0 or index >= len(csv_file_status[source][f'extra_{suffix}']):
|
||||
|
||||
index = int(request.match_info["index"])
|
||||
if index < 0 or index >= len(csv_file_status[source][f"extra_{suffix}"]):
|
||||
return web.json_response({"error": "Invalid index"}, status=404)
|
||||
|
||||
file_path = os.path.join(DATA_DIR, csv_file_status[source][f'extra_{suffix}'][index])
|
||||
file_path = os.path.join(DATA_DIR, csv_file_status[source][f"extra_{suffix}"][index])
|
||||
if not os.path.exists(file_path):
|
||||
return web.json_response({"error": "Extra tags file not found"}, status=404)
|
||||
|
||||
@@ -158,7 +176,8 @@ async def get_extra_tags_file(request):
|
||||
except ValueError:
|
||||
return web.json_response({"error": "Invalid index format"}, status=400)
|
||||
|
||||
@server.PromptServer.instance.routes.post('/autocomplete-plus/csv/force-check-updates')
|
||||
|
||||
@server.PromptServer.instance.routes.post("/autocomplete-plus/csv/force-check-updates")
|
||||
async def force_check_csv_updates(request):
|
||||
"""
|
||||
Forces a check for CSV file updates from HuggingFace, ignoring cooldown.
|
||||
@@ -166,29 +185,29 @@ async def force_check_csv_updates(request):
|
||||
"""
|
||||
try:
|
||||
print("[Autocomplete-Plus] Starting forced check for CSV updates from HuggingFace...")
|
||||
|
||||
|
||||
downloader = dl.Downloader()
|
||||
downloader.run_check_and_download(force_check=True)
|
||||
|
||||
|
||||
print("[Autocomplete-Plus] Forced check completed successfully.")
|
||||
|
||||
|
||||
# Get the updated last check time
|
||||
last_check_time = get_last_check_time_from_metadata()
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"message": "Force check completed successfully",
|
||||
"last_check_time": last_check_time
|
||||
})
|
||||
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Force check completed successfully",
|
||||
"last_check_time": last_check_time,
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Autocomplete-Plus] Error during forced check: {e}")
|
||||
return web.json_response({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}, status=500)
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
|
||||
@server.PromptServer.instance.routes.get('/autocomplete-plus/csv/last-check-time')
|
||||
|
||||
@server.PromptServer.instance.routes.get("/autocomplete-plus/csv/last-check-time")
|
||||
async def get_last_check_time(_request):
|
||||
"""
|
||||
Returns the last remote check timestamp from csv_meta.json.
|
||||
@@ -196,26 +215,15 @@ async def get_last_check_time(_request):
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(dl.CSV_META_FILE):
|
||||
return web.json_response({
|
||||
"last_check_time": None,
|
||||
"message": "csv_meta.json file not found"
|
||||
})
|
||||
|
||||
return web.json_response({"last_check_time": None, "message": "csv_meta.json file not found"})
|
||||
|
||||
last_check_time = get_last_check_time_from_metadata()
|
||||
|
||||
|
||||
if last_check_time is not None:
|
||||
return web.json_response({
|
||||
"last_check_time": last_check_time
|
||||
})
|
||||
return web.json_response({"last_check_time": last_check_time})
|
||||
else:
|
||||
return web.json_response({
|
||||
"last_check_time": None,
|
||||
"message": "No datasets found in metadata"
|
||||
})
|
||||
|
||||
return web.json_response({"last_check_time": None, "message": "No datasets found in metadata"})
|
||||
|
||||
except (IOError, json.JSONDecodeError) as e:
|
||||
print(f"[Autocomplete-Plus] Error reading csv_meta.json: {e}")
|
||||
return web.json_response({
|
||||
"last_check_time": None,
|
||||
"error": str(e)
|
||||
}, status=500)
|
||||
return web.json_response({"last_check_time": None, "error": str(e)}, status=500)
|
||||
|
||||
+54
-26
@@ -20,7 +20,8 @@ CSV_META_FILE = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", C
|
||||
|
||||
DEFAULT_CSV_METADATA = {
|
||||
"version": 1,
|
||||
"hf_datasets" : [
|
||||
"check_updates_on_startup": True,
|
||||
"hf_datasets": [
|
||||
{
|
||||
"hf_dataset_id": "newtextdoc1111/danbooru-tag-csv",
|
||||
"last_remote_check_timestamp": None,
|
||||
@@ -34,12 +35,15 @@ DEFAULT_CSV_METADATA = {
|
||||
"file_name": "danbooru_tags_cooccurrence.csv",
|
||||
"last_download": None,
|
||||
"last_modified_on_hf": None,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
# --- HuggingFace Constants ---
|
||||
HUGGINGFACE_URL = "https://huggingface.co"
|
||||
|
||||
|
||||
# --- Helper Functions ---
|
||||
def get_file_path(file_name: str) -> str:
|
||||
@@ -82,12 +86,14 @@ class Downloader:
|
||||
return default_metadata
|
||||
|
||||
try:
|
||||
with open(CSV_META_FILE, 'r', encoding='utf-8') as f:
|
||||
with open(CSV_META_FILE, "r", encoding="utf-8") as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
if not isinstance(metadata, dict) or metadata.get("version") != DEFAULT_CSV_METADATA["version"]:
|
||||
print(f"[Autocomplete-Plus] Metadata version mismatch. Expected {DEFAULT_CSV_METADATA['version']}, "
|
||||
f"found {metadata.get('version')}. Using default metadata.")
|
||||
print(
|
||||
f"[Autocomplete-Plus] Metadata version mismatch. Expected {DEFAULT_CSV_METADATA['version']}, "
|
||||
f"found {metadata.get('version')}. Using default metadata."
|
||||
)
|
||||
return default_metadata
|
||||
else:
|
||||
self.csv_meta_file_exists_at_start = True
|
||||
@@ -101,7 +107,7 @@ class Downloader:
|
||||
"""Saves metadata to CSV_META_FILE."""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(CSV_META_FILE), exist_ok=True)
|
||||
with open(CSV_META_FILE, 'w', encoding='utf-8') as f:
|
||||
with open(CSV_META_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(self.metadata, f, indent=2)
|
||||
except IOError as e:
|
||||
print(f"[Autocomplete-Plus] Error saving metadata to {CSV_META_FILE}: {e}")
|
||||
@@ -111,12 +117,15 @@ class Downloader:
|
||||
Retrieves the Last-Modified header for a file on HuggingFace and returns it as an ISO 8601 string.
|
||||
hf_filename should be the full filename, e.g., "danbooru_tags.csv".
|
||||
"""
|
||||
url = f"https://huggingface.co/datasets/{dataset_repo_id}/resolve/main/{hf_filename}"
|
||||
url = f"{HUGGINGFACE_URL}/datasets/{dataset_repo_id}/resolve/main/{hf_filename}"
|
||||
try:
|
||||
req = urllib.request.Request(url, method='HEAD',
|
||||
headers={"User-Agent": "ComfyUI-Autocomplete-Plus (Python urllib)"})
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
method="HEAD",
|
||||
headers={"User-Agent": "ComfyUI-Autocomplete-Plus (Python urllib)"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
last_modified_http = response.getheader('Last-Modified')
|
||||
last_modified_http = response.getheader("Last-Modified")
|
||||
if last_modified_http:
|
||||
dt_object = parsedate_to_datetime(last_modified_http)
|
||||
if dt_object.tzinfo is None or dt_object.tzinfo.utcoffset(dt_object) is None:
|
||||
@@ -141,7 +150,7 @@ class Downloader:
|
||||
"""
|
||||
Downloads a file synchronously with progress display to a temporary location.
|
||||
"""
|
||||
download_url = f"https://huggingface.co/datasets/{hf_dataset_id}/resolve/main/{file_name}"
|
||||
download_url = f"{HUGGINGFACE_URL}/datasets/{hf_dataset_id}/resolve/main/{file_name}"
|
||||
final_path = get_file_path(file_name)
|
||||
temp_path = get_temp_download_path(file_name)
|
||||
now_utc = datetime.now(timezone.utc).isoformat()
|
||||
@@ -155,18 +164,28 @@ class Downloader:
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
req = urllib.request.Request(download_url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 11.0; Win64)"})
|
||||
req = urllib.request.Request(
|
||||
download_url,
|
||||
headers={"User-Agent": "Mozilla/5.0 (Windows NT 11.0; Win64)"},
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req) as response:
|
||||
total_size_str = response.getheader("Content-Length")
|
||||
total_size = int(total_size_str) if total_size_str else None
|
||||
chunk_size = 8192
|
||||
|
||||
with open(temp_path, "wb") as f_out, \
|
||||
tqdm(total=total_size, unit='B', unit_scale=True, unit_divisor=1024,
|
||||
desc=f"[Autocomplete-Plus] Downloading {file_name}", leave=False,
|
||||
bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}{postfix}]'
|
||||
) as pbar:
|
||||
with (
|
||||
open(temp_path, "wb") as f_out,
|
||||
tqdm(
|
||||
total=total_size,
|
||||
unit="B",
|
||||
unit_scale=True,
|
||||
unit_divisor=1024,
|
||||
desc=f"[Autocomplete-Plus] Downloading {file_name}",
|
||||
leave=False,
|
||||
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}{postfix}]",
|
||||
) as pbar,
|
||||
):
|
||||
while True:
|
||||
chunk = response.read(chunk_size)
|
||||
if not chunk:
|
||||
@@ -197,12 +216,15 @@ class Downloader:
|
||||
if os.path.exists(final_path):
|
||||
try:
|
||||
file_size_at_final = os.path.getsize(final_path)
|
||||
if file_size_at_final == 0 or \
|
||||
(total_size and total_size > 0 and file_size_at_final < total_size) or \
|
||||
(not total_size and downloaded_size > 0 and file_size_at_final < downloaded_size):
|
||||
if (
|
||||
file_size_at_final == 0
|
||||
or (total_size and total_size > 0 and file_size_at_final < total_size)
|
||||
or (not total_size and downloaded_size > 0 and file_size_at_final < downloaded_size)
|
||||
):
|
||||
os.remove(final_path)
|
||||
print(
|
||||
f"[Autocomplete-Plus] Removed potentially corrupted file at final destination: {final_path}")
|
||||
f"[Autocomplete-Plus] Removed potentially corrupted file at final destination: {final_path}"
|
||||
)
|
||||
except OSError as rm_e:
|
||||
print(f"[Autocomplete-Plus] Error removing potentially corrupted file {final_path}: {rm_e}")
|
||||
|
||||
@@ -221,7 +243,8 @@ class Downloader:
|
||||
perform_hf_check = False
|
||||
except (ValueError, KeyError, TypeError):
|
||||
print(
|
||||
"[Autocomplete-Plus] Invalid or missing timestamp for last_remote_check_timestamp. Will perform remote check.")
|
||||
"[Autocomplete-Plus] Invalid or missing timestamp for last_remote_check_timestamp. Will perform remote check."
|
||||
)
|
||||
|
||||
if perform_hf_check:
|
||||
huggingface_dataset_id = dataset_meta["hf_dataset_id"]
|
||||
@@ -246,7 +269,8 @@ class Downloader:
|
||||
dataset_meta["last_remote_check_timestamp"] = now_utc.isoformat()
|
||||
else:
|
||||
print(
|
||||
"[Autocomplete-Plus] Could not update all remote timestamps from HuggingFace. Will try again later.")
|
||||
"[Autocomplete-Plus] Could not update all remote timestamps from HuggingFace. Will try again later."
|
||||
)
|
||||
|
||||
def _download_csv_files_if_needed(self, dataset_meta: dict):
|
||||
"""Downloads CSV files if they are missing, outdated, or previously failed."""
|
||||
@@ -299,10 +323,14 @@ class Downloader:
|
||||
"""
|
||||
Orchestrates the process of checking for updates and downloading CSV files.
|
||||
This is the main entry point for the downloader logic.
|
||||
|
||||
|
||||
Args:
|
||||
force_check: If True, forces a check of HuggingFace regardless of the last check timestamp.
|
||||
"""
|
||||
# If check_updates_on_startup is False and force_check is not set, skip the check
|
||||
if not (self.metadata.get("check_updates_on_startup", True) or force_check):
|
||||
print('[Autocomplete-Plus] "check_updates_on_startup" is disabled. Skipping CSV update check and download.')
|
||||
return
|
||||
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
|
||||
|
||||
+10
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-autocomplete-plus"
|
||||
description = "Autocomplete and Related Tag display for ComfyUI"
|
||||
version = "1.0.0"
|
||||
version = "1.2.0"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["",]
|
||||
|
||||
@@ -9,7 +9,16 @@ dependencies = ["",]
|
||||
Repository = "https://github.com/newtextdoc1111/ComfyUI-Autocomplete-Plus"
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.12.2",
|
||||
]
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "newtextdoc1111"
|
||||
DisplayName = "ComfyUI-Autocomplete-Plus"
|
||||
Icon = ""
|
||||
|
||||
[tool.ruff]
|
||||
# Allow lines to be as long as 120.
|
||||
line-length = 120
|
||||
|
||||
+144
-147
@@ -37,6 +37,62 @@ body.dark-theme {
|
||||
color: var(--input-text);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-blue);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-yellow);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .danbooru[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-sky);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-orange);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="species"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-white);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="lore"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#autocomplete-plus-root .e621[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
#autocomplete-plus-list {
|
||||
display: grid;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
|
||||
@@ -52,62 +108,6 @@ body.dark-theme {
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-blue);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-yellow);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.danbooru[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-sky);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-orange);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="species"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-white);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="lore"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.e621[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item span {
|
||||
align-content: center;
|
||||
padding: 4px 8px;
|
||||
@@ -124,11 +124,7 @@ body.dark-theme {
|
||||
}
|
||||
|
||||
.autocomplete-plus-item.selected span {
|
||||
background-color: var(
|
||||
--comfy-menu-bg-selected,
|
||||
var(--comfy-button-bg)
|
||||
);
|
||||
font-weight: bold;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.autocomplete-plus-item:hover span {
|
||||
@@ -172,26 +168,91 @@ body.dark-theme {
|
||||
color: var(--input-text);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-blue);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-yellow);
|
||||
}
|
||||
|
||||
#related-tags-root .danbooru[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-sky);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-orange);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="species"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-white);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="lore"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
#related-tags-root .e621[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
#related-tags-header {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
color: var(--descrip-text);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.related-tags-header-text {
|
||||
flex-grow: 1;
|
||||
.related-tags-header-text-container {
|
||||
display:flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.related-tags-header-tag-text {
|
||||
padding: 2px 0;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
.related-tags-header-tag-name {
|
||||
font-weight: bold;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.related-tags-header-tag-name .autocomplete-plus-tag-icon-svg {
|
||||
@@ -200,17 +261,22 @@ body.dark-theme {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.related-tags-header-tag-alias {
|
||||
overflow: auto;
|
||||
max-height: 5em;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--descrip-text);
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
.related-tags-header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.related-tags-layout-toggle {
|
||||
padding: 2px 6px;
|
||||
.related-tags-header-controls button {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: none;
|
||||
color: var(--input-text);
|
||||
cursor: pointer;
|
||||
@@ -218,19 +284,8 @@ body.dark-theme {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.related-tags-layout-toggle:hover {
|
||||
background-color: var(--comfy-button-bg);
|
||||
}
|
||||
|
||||
.related-tags-pin-toggle {
|
||||
padding: 2px 6px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: none;
|
||||
color: var(--input-text);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
.related-tags-header-controls button:hover {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
#related-tags-list {
|
||||
@@ -248,62 +303,6 @@ body.dark-theme {
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-blue);
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-yellow);
|
||||
}
|
||||
|
||||
.related-tag-item.danbooru[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="general"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-sky);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="artist"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-orange);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="copyright"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-purple);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="character"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-green);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="species"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="meta"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-white);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="lore"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-red);
|
||||
}
|
||||
|
||||
.related-tag-item.e621[data-tag-category="unknown"] {
|
||||
color: var(--autocomplete-plus-text-color-cat-gray);
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
.related-tag-item span {
|
||||
align-content: center;
|
||||
@@ -324,11 +323,7 @@ body.dark-theme {
|
||||
}
|
||||
|
||||
.related-tag-item.selected span {
|
||||
background-color: var(
|
||||
--comfy-menu-bg-selected,
|
||||
var(--comfy-button-bg)
|
||||
);
|
||||
font-weight: bold;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.related-tag-item .related-tag-name {
|
||||
@@ -352,9 +347,11 @@ body.dark-theme {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.related-tags-loading-message {
|
||||
.related-tags-message {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-style: italic;
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
+11
-10
@@ -332,10 +332,11 @@ class AutocompleteUI {
|
||||
// Calculate caret position using the helper function (returns viewport-relative coordinates)
|
||||
this.#updatePosition();
|
||||
|
||||
// Highlight the first item
|
||||
this.#highlightItem();
|
||||
|
||||
this.root.style.display = 'block'; // Make it visible
|
||||
|
||||
// Highlight the selected item
|
||||
// This function must be called after the route has been displayed, in order to scroll the highlighted item into view.
|
||||
this.#highlightItem();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -826,7 +827,7 @@ export class AutocompleteEventHandler {
|
||||
this.keyDownWithModifier.set(event.key.toLowerCase(), event.ctrlKey || event.altKey || event.metaKey);
|
||||
|
||||
// Handle autocomplete navigation
|
||||
if (this.autocompleteUI && this.autocompleteUI.isVisible()) {
|
||||
if (this.autocompleteUI.isVisible()) {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
@@ -878,11 +879,11 @@ export class AutocompleteEventHandler {
|
||||
|
||||
if (this.autocompleteUI.isVisible()) {
|
||||
switch (event.key) {
|
||||
case "Escape":
|
||||
case 'ArrowDown':
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
this.autocompleteUI.hide();
|
||||
return; // Return here to prevent updateDisplay after hiding with Escape
|
||||
// Other keys like Enter, Tab, Arrows are handled in keyDown.
|
||||
return; // Prevent redundant display updates
|
||||
|
||||
// For other character keys, Backspace, Delete, we fall through to updateDisplay.
|
||||
}
|
||||
} else {
|
||||
@@ -894,9 +895,9 @@ export class AutocompleteEventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// If the event was not handled by the above (e.g. Escape, or ignored special keys)
|
||||
// If the event was not handled by the above (e.g. Arrow keys, or ignored special keys)
|
||||
// and default action is not prevented, update the display.
|
||||
// This will typically be for character inputs, Delete, or Backspace.
|
||||
// This will typically be for character inputs, Delete, Backspace or IME composition.
|
||||
if (!event.defaultPrevented) {
|
||||
this.autocompleteUI.updateDisplay(event.target);
|
||||
}
|
||||
|
||||
+10
-9
@@ -340,8 +340,8 @@ export async function initializeData(csvListData, source) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extraTagsCount = csvListData[source].extra_tags || 0;
|
||||
const extraCooccurrenceCount = csvListData[source].extra_cooccurrence || 0;
|
||||
const extraTagsFileList = csvListData[source].extra_tags || [];
|
||||
const extraCooccurrenceFileList = csvListData[source].extra_cooccurrence || [];
|
||||
|
||||
const tagsUrl = `/autocomplete-plus/csv/${source}/tags`;
|
||||
const cooccurrenceUrl = `/autocomplete-plus/csv/${source}/tags_cooccurrence`;
|
||||
@@ -349,7 +349,7 @@ export async function initializeData(csvListData, source) {
|
||||
// Factory for loading tags for the current sourte
|
||||
const siteTagsLoaderFactory = async () => {
|
||||
let promiseChain = Promise.resolve();
|
||||
for (let i = 0; i < extraTagsCount; i++) {
|
||||
for (let i = 0; i < extraTagsFileList.length; i++) {
|
||||
promiseChain = promiseChain.then(() => loadTags(`${tagsUrl}/extra/${i}`, source));
|
||||
}
|
||||
if (csvListData[source].base_tags) {
|
||||
@@ -362,7 +362,7 @@ export async function initializeData(csvListData, source) {
|
||||
// Factory for loading cooccurrence data for the current sourte
|
||||
const siteCooccurrenceLoaderFactory = async () => {
|
||||
let promiseChain = Promise.resolve();
|
||||
for (let i = 0; i < extraCooccurrenceCount; i++) {
|
||||
for (let i = 0; i < extraCooccurrenceFileList.length; i++) {
|
||||
promiseChain = promiseChain.then(() => loadCooccurrence(`${cooccurrenceUrl}/extra/${i}`, source));
|
||||
}
|
||||
if (csvListData[source].base_cooccurrence) {
|
||||
@@ -377,14 +377,15 @@ export async function initializeData(csvListData, source) {
|
||||
await Promise.all([
|
||||
Promise.all(tagsLoadPromiseFactories.map(factory => factory())).then(() => {
|
||||
const endTime = performance.now();
|
||||
console.log(`[Autocomplete-Plus] "${source}" Tags loading complete in ${(endTime - startTime).toFixed(2)}ms`);
|
||||
if (csvListData[source].base_tags) {
|
||||
console.log(`[Autocomplete-Plus] "${source}" Tags loading complete in ${(endTime - startTime).toFixed(2)}ms`);
|
||||
}
|
||||
}),
|
||||
Promise.all(cooccurrenceLoadPromiseFactories.map(factory => factory())).then(() => {
|
||||
const endTime = performance.now();
|
||||
// The original log for extraCooccurrenceCount was potentially misleading as it showed
|
||||
// the count for the last processed sourte. Removed for clarity.
|
||||
// If a total count is needed, it should be calculated across all sites.
|
||||
console.log(`[Autocomplete-Plus] "${source}" Co-occurrence loading complete in ${(endTime - startTime).toFixed(2)}ms.`);
|
||||
if (csvListData[source].base_cooccurrence) {
|
||||
console.log(`[Autocomplete-Plus] "${source}" Co-occurrence loading complete in ${(endTime - startTime).toFixed(2)}ms.`);
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
|
||||
+62
-18
@@ -67,7 +67,7 @@ function searchRelatedTags(tag) {
|
||||
|
||||
const tagSource = TagSource.Danbooru; // TODO: Leave the tag source as Danbooru until e621_tags_cooccurrence.csv is ready
|
||||
|
||||
if (!tag || !autoCompleteData[tagSource].cooccurrenceMap.has(tag)) {
|
||||
if (!tag || !autoCompleteData[tagSource].initialized || !autoCompleteData[tagSource].cooccurrenceMap.has(tag)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -199,11 +199,20 @@ class RelatedTagsUI {
|
||||
this.header = document.createElement('div');
|
||||
this.header.id = 'related-tags-header';
|
||||
|
||||
this.headerTextContainer = document.createElement('div');
|
||||
this.headerTextContainer.className = 'related-tags-header-text-container';
|
||||
this.header.appendChild(this.headerTextContainer);
|
||||
|
||||
// Create header text div for the left side
|
||||
this.headerText = document.createElement('div');
|
||||
this.headerText.className = 'related-tags-header-text';
|
||||
this.headerText.className = 'related-tags-header-tag-text';
|
||||
this.headerText.textContent = 'Related Tags';
|
||||
this.header.appendChild(this.headerText);
|
||||
this.headerTextContainer.appendChild(this.headerText);
|
||||
|
||||
// Create header alias div for the 2nd line
|
||||
this.headerAlias = document.createElement('div');
|
||||
this.headerAlias.className = 'related-tags-header-tag-alias';
|
||||
this.headerTextContainer.appendChild(this.headerAlias);
|
||||
|
||||
// Create header controls for the right side
|
||||
this.headerControls = document.createElement('div');
|
||||
@@ -249,7 +258,7 @@ class RelatedTagsUI {
|
||||
this.headerControls.appendChild(this.pinBtn);
|
||||
|
||||
this.header.appendChild(this.headerControls);
|
||||
|
||||
|
||||
this.root.appendChild(this.header);
|
||||
|
||||
// Create a tbody for the tags
|
||||
@@ -318,11 +327,13 @@ class RelatedTagsUI {
|
||||
this.#updateHeader();
|
||||
this.#updateContent();
|
||||
this.#updatePosition();
|
||||
this.#highlightItem();
|
||||
|
||||
// Make visible
|
||||
this.root.style.display = 'block';
|
||||
|
||||
// This function must be called after the content is updated and the root is displayed.
|
||||
this.#highlightItem();
|
||||
|
||||
// Update initialization status if not already done
|
||||
if (!autoCompleteData[TagSource.Danbooru].initialized) {
|
||||
if (this.autoRefreshTimerId) {
|
||||
@@ -389,7 +400,7 @@ class RelatedTagsUI {
|
||||
*/
|
||||
#updateHeader() {
|
||||
// Find the tag data for the current tag
|
||||
const tagData = Object.values(TagSource)
|
||||
let tagData = Object.values(TagSource)
|
||||
.map((source) => {
|
||||
if (source in autoCompleteData && autoCompleteData[source].tagMap.has(this.currentTag)) {
|
||||
return autoCompleteData[source].tagMap.get(this.currentTag);
|
||||
@@ -397,21 +408,49 @@ class RelatedTagsUI {
|
||||
})
|
||||
.find((tagData) => tagData !== undefined);
|
||||
|
||||
if (!tagData) {
|
||||
tagData = new TagData({
|
||||
tag: this.currentTag,
|
||||
source: TagSource.Danbooru,
|
||||
category: 'unknown',
|
||||
count: 0,
|
||||
alias: [],
|
||||
});
|
||||
}
|
||||
|
||||
const tagText = this.currentTag;
|
||||
const categoryText = TagCategory[tagData.source][tagData.category];
|
||||
|
||||
// Update header text with current tag
|
||||
this.headerText.innerHTML = ''; // Clear previous content
|
||||
this.headerText.textContent = 'Tags related to: ';
|
||||
|
||||
const tagName = document.createElement('span');
|
||||
tagName.className = 'related-tags-header-tag-name';
|
||||
tagName.textContent = this.currentTag;
|
||||
if (tagData && ['left', 'right'].includes(settingValues.tagSourceIconPosition)) {
|
||||
tagName.classList.add('related-tags-header-tag-name', tagData.source);
|
||||
tagName.title = `Count: ${tagData.count}\nCategory: ${categoryText}`;
|
||||
tagName.dataset.tagCategory = categoryText;
|
||||
if (tagData.source && ['left', 'right'].includes(settingValues.tagSourceIconPosition)) {
|
||||
const tagSourceIconHtml = `<svg class="autocomplete-plus-tag-icon-svg"><use xlink:href="#autocomplete-plus-icon-${tagData.source}"></use></svg>`;
|
||||
tagName.innerHTML = settingValues.tagSourceIconPosition == 'left'
|
||||
? `${tagSourceIconHtml} ${tagData.tag}`
|
||||
: `${tagData.tag} ${tagSourceIconHtml}`;
|
||||
? `${tagSourceIconHtml} ${tagText}`
|
||||
: `${tagText} ${tagSourceIconHtml}`;
|
||||
} else {
|
||||
tagName.textContent += tagText;
|
||||
}
|
||||
|
||||
|
||||
this.headerText.appendChild(tagName);
|
||||
|
||||
// Clear previous alias
|
||||
this.headerAlias.style.display = 'none';
|
||||
this.headerAlias.innerHTML = '';
|
||||
|
||||
// Add alias if available
|
||||
if (tagData.alias?.length > 0) {
|
||||
this.headerAlias.textContent = tagData.alias.join(', ');
|
||||
this.headerAlias.style.display = 'block';
|
||||
}
|
||||
|
||||
// Update pin button
|
||||
this.pinBtn.textContent = this.isPinned ? '🎯' : '📌';
|
||||
|
||||
@@ -430,7 +469,7 @@ class RelatedTagsUI {
|
||||
if (!autoCompleteData[TagSource.Danbooru].initialized) {
|
||||
// Show loading message
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'related-tags-loading-message';
|
||||
messageDiv.className = 'related-tags-message';
|
||||
messageDiv.textContent = `Initializing cooccurrence data... [${autoCompleteData[TagSource.Danbooru].baseLoadingProgress.cooccurrence}%]`;
|
||||
this.tagsContainer.appendChild(messageDiv);
|
||||
return;
|
||||
@@ -439,6 +478,7 @@ class RelatedTagsUI {
|
||||
if (!this.relatedTags || this.relatedTags.length === 0) {
|
||||
// Show no related tags message
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'related-tags-message';
|
||||
messageDiv.textContent = 'No related tags found';
|
||||
this.tagsContainer.appendChild(messageDiv);
|
||||
return;
|
||||
@@ -528,20 +568,24 @@ class RelatedTagsUI {
|
||||
this.root.style.maxWidth = '';
|
||||
this.tagsContainer.style.maxHeight = '';
|
||||
const rootRect = this.root.getBoundingClientRect();
|
||||
const headerRect = this.header.getBoundingClientRect();
|
||||
// Hide it again after measurement
|
||||
this.root.style.display = 'none';
|
||||
this.root.style.visibility = 'visible';
|
||||
|
||||
// Get the optimal placement area
|
||||
const placementArea = this.#getOptimalPlacementArea(rootRect.width, rootRect.height);
|
||||
|
||||
// Apply Styles
|
||||
// Apply position and size
|
||||
this.root.style.left = `${placementArea.x}px`;
|
||||
this.root.style.top = `${placementArea.y}px`;
|
||||
this.root.style.maxWidth = `${placementArea.width}px`;
|
||||
|
||||
this.tagsContainer.style.maxHeight = `${placementArea.height - headerRect.height}px`;
|
||||
const newHeaderRect = this.header.getBoundingClientRect();
|
||||
|
||||
if(this.relatedTags.length > 0){
|
||||
this.tagsContainer.style.maxHeight = `${placementArea.height - newHeaderRect.height}px`;
|
||||
}
|
||||
|
||||
// Hide it again after measurement
|
||||
this.root.style.display = 'none';
|
||||
this.root.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
/** Highlights the item (row) at the given index */
|
||||
|
||||
Reference in New Issue
Block a user