Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a8bf5c479 | ||
|
|
d9e96ce009 | ||
|
|
2883e5af3a | ||
|
|
918c136b2a | ||
|
|
a2fef4995d | ||
|
|
bc26ce54da | ||
|
|
90e8cc84db | ||
|
|
b72560a871 | ||
|
|
3b94fab3ec | ||
|
|
dfdbe7d539 | ||
|
|
d766af1ce1 | ||
|
|
8b4a8d0deb |
@@ -0,0 +1,21 @@
|
||||
# История изменений
|
||||
|
||||
## v2.0.0
|
||||
|
||||
- Добавлена пакетная нода: **BSS WD14 Tagger Batch ⚡**.
|
||||
- Добавлена нода постобработки: **BSS Tags Postprocess 🧹**.
|
||||
- Добавлена нода сохранения caption: **BSS Save Captions 💾**.
|
||||
- Добавлена нода аналитики: **BSS Tag Analytics 📊**.
|
||||
- Добавлены пороги по категориям (`general/character/meta/rating`).
|
||||
- Улучшена совместимость нормализации изображений с форматом ComfyUI `IMAGE`.
|
||||
- Обновлены метаданные релиза до версии `2.0.0`.
|
||||
|
||||
## v1.0.1
|
||||
|
||||
- Улучшена стабильность проверки и загрузки `onnxruntime`.
|
||||
- Повышена надёжность загрузки моделей.
|
||||
|
||||
## v1.0.0
|
||||
|
||||
- Первый публичный релиз ноды.
|
||||
- Базовый WD14-теггер и загрузка изображений из папки.
|
||||
@@ -1,126 +0,0 @@
|
||||
# Руководство по публикации в ComfyUI Registry
|
||||
|
||||
## Подготовленные файлы
|
||||
|
||||
Все необходимые файлы для публикации в [ComfyUI Registry](https://docs.comfy.org/registry/publishing) уже созданы:
|
||||
|
||||
### Основные файлы
|
||||
- ✅ `pyproject.toml` - Конфигурация для ComfyUI Registry
|
||||
- ✅ `LICENSE.txt` - Лицензия MIT
|
||||
- ✅ `requirements.txt` - Зависимости Python
|
||||
- ✅ `README.md` - Обновленная документация
|
||||
- ✅ `install.py` - Скрипт автоматической установки
|
||||
|
||||
### Дополнительные файлы
|
||||
- ✅ `node_list.json` - Справочная информация о нодах
|
||||
- ✅ `package.json` - Метаданные (для совместимости)
|
||||
|
||||
## Секретный ключ
|
||||
|
||||
Ваш API ключ: `66ac3938-0c9c-42fc-b9c4-426d6729164d`
|
||||
|
||||
## Шаги для публикации
|
||||
|
||||
### 1. Настройка аккаунта в ComfyUI Registry
|
||||
|
||||
1. Перейдите на [ComfyUI Registry](https://registry.comfy.org/)
|
||||
2. Создайте аккаунт Publisher с ID: `blacksnowskill`
|
||||
3. Создайте API ключ для публикации
|
||||
4. Сохраните API ключ: `66ac3938-0c9c-42fc-b9c4-426d6729164d`
|
||||
|
||||
### 2. Установка Comfy CLI
|
||||
|
||||
```bash
|
||||
pip install comfy-cli
|
||||
```
|
||||
|
||||
### 3. Подготовка репозитория
|
||||
|
||||
1. Убедитесь, что все файлы закоммичены в Git:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Prepare for ComfyUI Registry publication"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 4. Публикация через Comfy CLI
|
||||
|
||||
```bash
|
||||
comfy node publish
|
||||
```
|
||||
|
||||
Введите ваш API ключ когда будет запрошен.
|
||||
|
||||
### 5. Автоматическая публикация через GitHub Actions
|
||||
|
||||
Создайте файл `.github/workflows/publish_action.yml`:
|
||||
|
||||
```yaml
|
||||
name: Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
with:
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
```
|
||||
|
||||
### 6. Настройка GitHub Secret
|
||||
|
||||
1. Перейдите в Settings → Secrets and Variables → Actions
|
||||
2. Создайте новый секрет: `REGISTRY_ACCESS_TOKEN`
|
||||
3. Вставьте ваш API ключ: `66ac3938-0c9c-42fc-b9c4-426d6729164d`
|
||||
|
||||
### 7. После публикации
|
||||
|
||||
1. Нода будет доступна по адресу: `https://registry.comfy.org/blacksnowskill/wd14-batch-tagger`
|
||||
2. Пользователи смогут установить через ComfyUI Manager
|
||||
3. Обновления будут публиковаться автоматически при изменении `pyproject.toml`
|
||||
|
||||
## Структура файлов
|
||||
|
||||
```
|
||||
wd14_batch_tagger/
|
||||
├── __init__.py # Основной модуль
|
||||
├── nodes.py # Реализация нод
|
||||
├── package.json # Метаданные для ComfyUI Manager
|
||||
├── pyproject.toml # Конфигурация для comfyregistry
|
||||
├── node_list.json # Справочная информация о нодах
|
||||
├── install.py # Скрипт установки
|
||||
├── requirements.txt # Python зависимости
|
||||
├── README.md # Документация
|
||||
├── LICENSE # Лицензия MIT
|
||||
├── models/ # Папка для моделей (создается автоматически)
|
||||
└── custom-node-list-entry.json # Пример записи для PR
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
После публикации протестируйте установку:
|
||||
|
||||
1. Удалите ноду из custom_nodes
|
||||
2. Установите через ComfyUI Manager
|
||||
3. Проверьте работу обеих нод
|
||||
4. Убедитесь, что модели загружаются автоматически
|
||||
|
||||
## Поддержка
|
||||
|
||||
- GitHub Issues: https://github.com/blacksnowskill/wd14_batch_tagger/issues
|
||||
- ComfyUI Community: Упоминайте @blacksnowskill
|
||||
|
||||
---
|
||||
|
||||
**Готово к публикации!** 🚀
|
||||
@@ -1,12 +1,24 @@
|
||||
# BSS WD14 Batch Tagger
|
||||
# BSS WD14 Batch Tagger v2.0
|
||||
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
Custom nodes for **ComfyUI** for automatic WD14 image tagging, batch workflows, tag postprocessing, caption export, and basic tag analytics.
|
||||
Набор нод для ComfyUI для WD14-теггинга, постобработки тегов, сохранения caption и базовой аналитики датасета.
|
||||
|
||||
Automatic image tagging using WD14 models with batch processing for ComfyUI.
|
||||
## Что нового в v2.0
|
||||
|
||||
## Features
|
||||
- ✅ Мультипорог по категориям: `general`, `character`, `meta`, `rating`
|
||||
- ✅ Batch-in/Batch-out нода для массового теггинга
|
||||
- ✅ Нода постобработки тегов (dedupe, сортировка, prepend/append/exclude)
|
||||
- ✅ Нода сохранения caption в `txt/json/csv`
|
||||
- ✅ Нода аналитики тегов (top-k + JSON статистика)
|
||||
|
||||
- WD14 v3 model support (ViT / SwinV2 / EVA02 / ConvNeXT)
|
||||
- Automatic model download from Hugging Face
|
||||
- Single-image and batch tagging nodes
|
||||
- Category-aware thresholds (`general`, `character`, `meta`, `rating`)
|
||||
- Tag postprocessing (dedupe, sorting, include/exclude operations)
|
||||
- Caption saving to `txt`, `json`, `csv`
|
||||
- Tag frequency analytics (`top-k` + JSON stats)
|
||||
## Ноды
|
||||
- **4 WD14 v3 Models**: ViT, SwinV2, EVA02, ConvNeXT
|
||||
- **Auto Download**: Models download automatically from Hugging Face
|
||||
- **GPU Support**: CUDA acceleration for faster processing
|
||||
@@ -15,45 +27,69 @@ Automatic image tagging using WD14 models with batch processing for ComfyUI.
|
||||
- **Custom Tags**: Add/remove tags as needed
|
||||
- **ComfyUI IMAGE Compatibility**: Uses native tensor image format for modern ComfyUI builds
|
||||
|
||||
## Installation
|
||||
### 1) BSS Load Images from Folder 📂
|
||||
Загружает изображения из папки (JPG/JPEG/PNG/WEBP) и отдает `IMAGE` + имена файлов.
|
||||
|
||||
### Via ComfyUI Manager
|
||||
### 2) BSS WD14 Batch Tagger 🌿
|
||||
Теггинг одного изображения с сохранением `.txt`.
|
||||
|
||||
1. Open ComfyUI Manager
|
||||
2. Go to Registry tab
|
||||
3. Search for "BSS WD14 Batch Tagger"
|
||||
4. Click Install
|
||||
5. Restart ComfyUI
|
||||
2. Find **BSS WD14 Batch Tagger**
|
||||
3. Install and restart ComfyUI
|
||||
|
||||
### Manual Installation
|
||||
### Manual
|
||||
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes/
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/BlackSnowSkill/wd14_batch_tagger.git
|
||||
cd wd14_batch_tagger
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
## Included Nodes
|
||||
Параметры порогов:
|
||||
- `general_threshold`
|
||||
- `character_threshold`
|
||||
- `meta_threshold`
|
||||
- `rating_threshold`
|
||||
|
||||
### Nodes
|
||||
### 3) BSS WD14 Tagger Batch ⚡
|
||||
Теггинг списка изображений за один вызов.
|
||||
|
||||
**BSS Load Images from Folder 📂**
|
||||
- Loads images from a folder for batch processing
|
||||
Выходы:
|
||||
- `tags` (список строк)
|
||||
- `tags_json` (json с score/category)
|
||||
|
||||
**BSS WD14 Batch Tagger 🌿**
|
||||
- Tags images using WD14 models
|
||||
- Saves tags to .txt files
|
||||
|
||||
### Basic Workflow
|
||||
### 4) BSS Tags Postprocess 🧹
|
||||
Очистка и нормализация тегов:
|
||||
- remove duplicates
|
||||
- sort alphabetically
|
||||
- prepend/append/exclude
|
||||
- replace underscores
|
||||
|
||||
### 5) BSS Save Captions 💾
|
||||
Сохраняет caption в `txt/json/csv`, есть `overwrite` и `suffix`.
|
||||
1. Use **BSS Load Images from Folder** to load your images
|
||||
2. Connect to **BSS WD14 Batch Tagger** for each image
|
||||
3. Set output folder for tag files
|
||||
4. Run the workflow
|
||||
|
||||
### Settings
|
||||
- **BSS Load Images from Folder 📂** — loads folder images (`jpg`, `jpeg`, `png`, `webp`)
|
||||
- **BSS WD14 Batch Tagger 🌿** — tags a single image and can save `.txt`
|
||||
- **BSS WD14 Tagger Batch ⚡** — tags a list/batch and returns JSON scores
|
||||
- **BSS Tags Postprocess 🧹** — cleanup/sort/dedupe/filter tag strings
|
||||
- **BSS Save Captions 💾** — save captions in `txt/json/csv`
|
||||
- **BSS Tag Analytics 📊** — compute top tag stats
|
||||
|
||||
## Usage (basic pipeline)
|
||||
|
||||
1. Load images with **BSS Load Images from Folder 📂**
|
||||
2. Run tagging via:
|
||||
- **BSS WD14 Batch Tagger 🌿** (single), or
|
||||
- **BSS WD14 Tagger Batch ⚡** (batch)
|
||||
3. (Optional) Clean results using **BSS Tags Postprocess 🧹**
|
||||
4. (Optional) Save captions with **BSS Save Captions 💾**
|
||||
5. (Optional) Inspect distribution using **BSS Tag Analytics 📊**
|
||||
- **Model**: Choose WD14 model (auto-downloads if needed)
|
||||
- **Threshold**: Tag confidence (0.35 default)
|
||||
- **Character Threshold**: Separate threshold for character tags (WD category 4)
|
||||
@@ -68,56 +104,56 @@ pip install -r requirements.txt
|
||||
|
||||
## Models
|
||||
|
||||
- **WD ViT Tagger v3**: Fast, good quality (default)
|
||||
- **WD SwinV2 Tagger v3**: Balanced speed/quality
|
||||
- **WD EVA02 Large Tagger v3**: Best accuracy
|
||||
- **WD ConvNeXT Tagger v3**: Modern architecture
|
||||
Рекомендуемые стартовые параметры WD14:
|
||||
- `general_threshold`: `0.35`
|
||||
- `character_threshold`: `0.85`
|
||||
- `meta_threshold`: `0.50`
|
||||
- `rating_threshold`: `0.50`
|
||||
|
||||
Models download automatically on first use.
|
||||
|
||||
## Requirements
|
||||
## Требования
|
||||
|
||||
- Python 3.8+
|
||||
- ComfyUI
|
||||
- `onnxruntime>=1.18.0,<2.0.0`
|
||||
- CUDA GPU (optional)
|
||||
|
||||
## Troubleshooting
|
||||
## Changelog
|
||||
|
||||
### onnxruntime Not Available Error
|
||||
### v2.0.0
|
||||
|
||||
If you see "onnxruntime is not available", fix it:
|
||||
- Added batch node: **BSS WD14 Tagger Batch ⚡**
|
||||
- Added postprocessing node: **BSS Tags Postprocess 🧹**
|
||||
- Added caption writer node: **BSS Save Captions 💾**
|
||||
- Added analytics node: **BSS Tag Analytics 📊**
|
||||
- Added category-aware WD14 thresholds (`general/character/meta/rating`)
|
||||
- Improved image normalization compatibility with ComfyUI `IMAGE`
|
||||
|
||||
### v1.0.1
|
||||
|
||||
- Stabilized `onnxruntime` handling and compatibility checks
|
||||
- Improved model loading reliability
|
||||
### 6) BSS Tag Analytics 📊
|
||||
Считает top-k частых тегов и возвращает JSON статистику.
|
||||
|
||||
## Установка
|
||||
|
||||
**Step 1: Remove conflicting packages**
|
||||
```bash
|
||||
pip uninstall onnxruntime onnxruntime-gpu -y
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/BlackSnowSkill/wd14_batch_tagger.git
|
||||
cd wd14_batch_tagger
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**Step 2: Install correct version**
|
||||
```bash
|
||||
pip install 'onnxruntime>=1.18.0,<2.0.0'
|
||||
```
|
||||
## Совместимость
|
||||
|
||||
**Step 3: Verify installation**
|
||||
```bash
|
||||
python -c "import onnxruntime; print(onnxruntime.__version__)"
|
||||
```
|
||||
|
||||
**Or reinstall the entire node:**
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes/wd14_batch_tagger
|
||||
pip install -r requirements.txt --upgrade --force-reinstall
|
||||
```
|
||||
|
||||
**Note:** Make sure you're using the correct Python environment (ComfyUI's venv).
|
||||
|
||||
## Support
|
||||
|
||||
- **GitHub**: [Issues](https://github.com/BlackSnowSkill/wd14_batch_tagger/issues)
|
||||
- **Civitai**: [Profile](https://civitai.com/user/llikswonskcalb)
|
||||
- **Boosty**: [Support](https://boosty.to/blacksnowskill)
|
||||
- Python 3.8+
|
||||
- ComfyUI (актуальные сборки)
|
||||
- onnxruntime `>=1.18.0,<2.0.0`
|
||||
|
||||
## License
|
||||
MIT
|
||||
|
||||
MIT License
|
||||
MIT License - see LICENSE file for details.
|
||||
|
||||
---
|
||||
|
||||
+2
-2
@@ -8,6 +8,6 @@ from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
# Version info
|
||||
__version__ = "1.0.1"
|
||||
__version__ = "2.0.0"
|
||||
__author__ = "Blacksnowskill"
|
||||
__description__ = "WD14 Batch Tagger with GPU acceleration and multiple model support"
|
||||
__description__ = "WD14 Batch Tagger v2.0 with batch tools, postprocessing, and analytics"
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
"category": "Image Processing",
|
||||
"tags": ["image", "tagging", "wd14", "batch", "gpu"],
|
||||
"pip": [
|
||||
"onnxruntime>=1.16.0",
|
||||
"onnxruntime>=1.18.0,<2.0.0",
|
||||
"numpy>=1.24.0",
|
||||
"pillow>=9.0.0",
|
||||
"huggingface-hub>=0.16.0"
|
||||
],
|
||||
"custom_installer": "install.py",
|
||||
"python_requirements": [
|
||||
"onnxruntime>=1.16.0",
|
||||
"onnxruntime>=1.18.0,<2.0.0",
|
||||
"numpy>=1.24.0",
|
||||
"pillow>=9.0.0",
|
||||
"huggingface-hub>=0.16.0"
|
||||
|
||||
+24
-90
@@ -1,98 +1,32 @@
|
||||
{
|
||||
"BSS_LoadImagesFolder": {
|
||||
"display_name": "BSS Load Images from Folder 📂",
|
||||
"description": "Load images from a folder and return them as a batch for processing. Supports JPG, JPEG, PNG, and WEBP formats.",
|
||||
"category": "BSS/Image Processing",
|
||||
"inputs": {
|
||||
"folder_path": {
|
||||
"type": "STRING",
|
||||
"description": "Path to folder containing images"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"images": {
|
||||
"type": "IMAGE",
|
||||
"description": "List of loaded images"
|
||||
},
|
||||
"filenames": {
|
||||
"type": "STRING",
|
||||
"description": "List of corresponding filenames"
|
||||
},
|
||||
"folder_path": {
|
||||
"type": "STRING",
|
||||
"description": "Original folder path"
|
||||
}
|
||||
}
|
||||
"description": "Load images from a folder and return IMAGE list + filenames.",
|
||||
"category": "BSS/Image Processing"
|
||||
},
|
||||
"BSS_WD14BatchTagger": {
|
||||
"display_name": "BSS WD14 Batch Tagger 🌿",
|
||||
"description": "Processes images using WD14 models and generates automatic tags with batch processing support.",
|
||||
"category": "BSS/Image Processing",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"type": "IMAGE",
|
||||
"description": "Input image to tag"
|
||||
},
|
||||
"filename": {
|
||||
"type": "STRING",
|
||||
"description": "Original filename"
|
||||
},
|
||||
"folder_path": {
|
||||
"type": "STRING",
|
||||
"description": "Output folder for tag files"
|
||||
},
|
||||
"model": {
|
||||
"type": "SELECT",
|
||||
"description": "WD14 model to use (automatically downloads if not available)",
|
||||
"options": [
|
||||
"wd-vit-tagger-v3|WD ViT Tagger v3",
|
||||
"wd-swinv2-tagger-v3|WD SwinV2 Tagger v3",
|
||||
"wd-eva02-large-tagger-v3|WD EVA02 Large Tagger v3",
|
||||
"wd-convnext-tagger-v3|WD ConvNeXT Tagger v3"
|
||||
]
|
||||
},
|
||||
"threshold": {
|
||||
"type": "FLOAT",
|
||||
"description": "Confidence threshold for tags (0.0-1.0)",
|
||||
"default": 0.35,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
},
|
||||
"character_threshold": {
|
||||
"type": "FLOAT",
|
||||
"description": "Character-specific threshold (0.0-1.0)",
|
||||
"default": 0.85,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
},
|
||||
"replace_underscore": {
|
||||
"type": "BOOLEAN",
|
||||
"description": "Replace underscores with spaces in tags",
|
||||
"default": true
|
||||
},
|
||||
"use_gpu": {
|
||||
"type": "BOOLEAN",
|
||||
"description": "Enable GPU acceleration",
|
||||
"default": false
|
||||
},
|
||||
"prepend_tags": {
|
||||
"type": "STRING",
|
||||
"description": "Custom tags to prepend to results",
|
||||
"multiline": true
|
||||
},
|
||||
"exclude_tags": {
|
||||
"type": "STRING",
|
||||
"description": "Tags to exclude from results (comma-separated)",
|
||||
"multiline": true
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"tags": {
|
||||
"type": "STRING",
|
||||
"description": "Generated tags for the image"
|
||||
}
|
||||
}
|
||||
"description": "Single-image WD14 tagger with category thresholds and txt output.",
|
||||
"category": "BSS/Image Processing"
|
||||
},
|
||||
"BSS_WD14TaggerBatch": {
|
||||
"display_name": "BSS WD14 Tagger Batch ⚡",
|
||||
"description": "Batch WD14 tagging with JSON score output.",
|
||||
"category": "BSS/Image Processing"
|
||||
},
|
||||
"BSS_TagsPostprocess": {
|
||||
"display_name": "BSS Tags Postprocess 🧹",
|
||||
"description": "Normalize, dedupe, sort, and filter tags.",
|
||||
"category": "BSS/Image Processing"
|
||||
},
|
||||
"BSS_SaveCaptions": {
|
||||
"display_name": "BSS Save Captions 💾",
|
||||
"description": "Save captions to txt/json/csv with overwrite and suffix options.",
|
||||
"category": "BSS/Image Processing"
|
||||
},
|
||||
"BSS_TagAnalytics": {
|
||||
"display_name": "BSS Tag Analytics 📊",
|
||||
"description": "Top-k tag statistics and JSON analytics for tag lists.",
|
||||
"category": "BSS/Image Processing"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ import numpy as np
|
||||
from PIL import Image
|
||||
from pathlib import Path
|
||||
import csv
|
||||
from typing import Tuple, List, Dict, Any, Optional
|
||||
from typing import Tuple, List, Dict, Any
|
||||
import logging
|
||||
from huggingface_hub import hf_hub_download
|
||||
import time
|
||||
import sys
|
||||
import json
|
||||
import torch
|
||||
|
||||
# Configure logging FIRST, before any imports that might fail
|
||||
@@ -24,7 +24,7 @@ try:
|
||||
if major < 1 or (major == 1 and minor < 18):
|
||||
logger.warning(f"onnxruntime version {version} may be incompatible. Recommended: >=1.18.0,<2.0.0")
|
||||
except (ValueError, AttributeError):
|
||||
pass # Version check failed, but onnxruntime is imported
|
||||
pass
|
||||
except ImportError as e:
|
||||
error_msg = (
|
||||
f"[BSS WD14] ERROR: onnxruntime is not available in Python {sys.version.split()[0]}\n"
|
||||
@@ -46,54 +46,47 @@ except Exception as e:
|
||||
logger.error(error_msg)
|
||||
raise ImportError(error_msg) from e
|
||||
|
||||
# ComfyUI progress system integration
|
||||
|
||||
def update_progress(progress: float, message: str = ""):
|
||||
"""Update progress for ComfyUI interface."""
|
||||
# This will be handled by ComfyUI's built-in progress system
|
||||
# The progress value should be between 0.0 and 1.0
|
||||
print(f"[BSS WD14] Progress: {int(progress * 100)}% - {message}")
|
||||
|
||||
# Global progress manager for batch processing
|
||||
|
||||
class BatchProgressManager:
|
||||
def __init__(self):
|
||||
self.pbar = None
|
||||
self.current_image = 0
|
||||
self.total_images = 0
|
||||
self.model_loaded = False
|
||||
|
||||
|
||||
def init_progress(self, total_images: int):
|
||||
"""Initialize progress bar for batch processing."""
|
||||
import comfy.utils
|
||||
self.total_images = total_images
|
||||
self.current_image = 0
|
||||
self.model_loaded = False
|
||||
self.pbar = comfy.utils.ProgressBar(total_images)
|
||||
|
||||
if total_images > 0:
|
||||
self.pbar = comfy.utils.ProgressBar(total_images)
|
||||
else:
|
||||
self.pbar = None
|
||||
|
||||
def update_image_progress(self, message: str = ""):
|
||||
"""Update progress for current image."""
|
||||
if self.pbar:
|
||||
self.pbar.update(1)
|
||||
if message:
|
||||
logger.info(f"Image {self.current_image + 1}/{self.total_images} - {message}")
|
||||
|
||||
|
||||
def next_image(self):
|
||||
"""Move to next image."""
|
||||
self.current_image += 1
|
||||
|
||||
# Global progress manager instance
|
||||
|
||||
batch_progress = BatchProgressManager()
|
||||
|
||||
# Global model cache
|
||||
model_cache = {
|
||||
'session': None,
|
||||
'tags': None,
|
||||
'tag_categories': None,
|
||||
'model_name': None,
|
||||
'input_name': None,
|
||||
'input_size': None
|
||||
'input_size': None,
|
||||
}
|
||||
|
||||
# Model configurations with Hugging Face repository info
|
||||
MODEL_CONFIGS = {
|
||||
"wd-vit-tagger-v3": {
|
||||
"repo_id": "SmilingWolf/wd-vit-tagger-v3",
|
||||
@@ -102,150 +95,125 @@ MODEL_CONFIGS = {
|
||||
"display_name": "WD ViT Tagger v3"
|
||||
},
|
||||
"wd-swinv2-tagger-v3": {
|
||||
"repo_id": "SmilingWolf/wd-swinv2-tagger-v3",
|
||||
"repo_id": "SmilingWolf/wd-swinv2-tagger-v3",
|
||||
"filename": "model.onnx",
|
||||
"csv_filename": "selected_tags.csv",
|
||||
"display_name": "WD SwinV2 Tagger v3"
|
||||
},
|
||||
"wd-eva02-large-tagger-v3": {
|
||||
"repo_id": "SmilingWolf/wd-eva02-large-tagger-v3",
|
||||
"filename": "model.onnx",
|
||||
"filename": "model.onnx",
|
||||
"csv_filename": "selected_tags.csv",
|
||||
"display_name": "WD EVA02 Large Tagger v3"
|
||||
},
|
||||
"wd-convnext-tagger-v3": {
|
||||
"repo_id": "SmilingWolf/wd-convnext-tagger-v3",
|
||||
"filename": "model.onnx",
|
||||
"csv_filename": "selected_tags.csv",
|
||||
"csv_filename": "selected_tags.csv",
|
||||
"display_name": "WD ConvNeXT Tagger v3"
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY_DEFAULTS = {
|
||||
-1: "general",
|
||||
0: "general",
|
||||
1: "artist",
|
||||
2: "copyright",
|
||||
3: "character",
|
||||
4: "character",
|
||||
5: "meta",
|
||||
9: "rating",
|
||||
}
|
||||
|
||||
|
||||
def ensure_model_available(model_name: str) -> Tuple[bool, str, str]:
|
||||
"""
|
||||
Ensure model and CSV files are available, download from Hugging Face if needed.
|
||||
|
||||
Args:
|
||||
model_name: Name of the model to check/download
|
||||
|
||||
Returns:
|
||||
Tuple of (success, model_path, csv_path)
|
||||
"""
|
||||
if model_name not in MODEL_CONFIGS:
|
||||
logger.error(f"Unknown model: {model_name}")
|
||||
return False, "", ""
|
||||
|
||||
|
||||
model_dir = Path(__file__).parent / "models"
|
||||
model_dir.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
config = MODEL_CONFIGS[model_name]
|
||||
model_path = model_dir / f"{model_name}.onnx"
|
||||
csv_path = model_dir / f"{model_name}.csv"
|
||||
|
||||
# Check if both files exist
|
||||
|
||||
if model_path.exists() and csv_path.exists():
|
||||
logger.info(f"Model {model_name} already available")
|
||||
return True, str(model_path), str(csv_path)
|
||||
|
||||
# Download from Hugging Face with progress indication
|
||||
|
||||
try:
|
||||
logger.info(f"Downloading {model_name} from Hugging Face...")
|
||||
update_progress(0, f"Starting download of {model_name}...")
|
||||
|
||||
# Download ONNX model
|
||||
|
||||
if not model_path.exists():
|
||||
update_progress(0.1, f"Downloading {model_name} model file...")
|
||||
hf_hub_download(
|
||||
repo_id=config["repo_id"],
|
||||
filename=config["filename"],
|
||||
local_dir=str(model_dir),
|
||||
local_dir_use_symlinks=False
|
||||
local_dir_use_symlinks=False,
|
||||
)
|
||||
# Rename to our naming convention
|
||||
downloaded_model = model_dir / config["filename"]
|
||||
if downloaded_model.exists():
|
||||
if downloaded_model.exists() and downloaded_model != model_path:
|
||||
downloaded_model.rename(model_path)
|
||||
update_progress(0.5, f"Model file downloaded successfully!")
|
||||
|
||||
# Download CSV tags
|
||||
update_progress(0.5, "Model file downloaded successfully!")
|
||||
|
||||
if not csv_path.exists():
|
||||
update_progress(0.5, f"Downloading {model_name} tags file...")
|
||||
hf_hub_download(
|
||||
repo_id=config["repo_id"],
|
||||
filename=config["csv_filename"],
|
||||
local_dir=str(model_dir),
|
||||
local_dir_use_symlinks=False
|
||||
local_dir_use_symlinks=False,
|
||||
)
|
||||
# Rename to our naming convention
|
||||
downloaded_csv = model_dir / config["csv_filename"]
|
||||
if downloaded_csv.exists():
|
||||
if downloaded_csv.exists() and downloaded_csv != csv_path:
|
||||
downloaded_csv.rename(csv_path)
|
||||
update_progress(0.9, f"Tags file downloaded successfully!")
|
||||
|
||||
update_progress(0.9, "Tags file downloaded successfully!")
|
||||
|
||||
update_progress(1.0, f"Download complete! {model_name} ready to use.")
|
||||
logger.info(f"Successfully downloaded {model_name}")
|
||||
return True, str(model_path), str(csv_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download {model_name}: {e}")
|
||||
return False, "", ""
|
||||
|
||||
|
||||
def get_available_models() -> List[str]:
|
||||
"""
|
||||
Get list of all available models (both local and downloadable).
|
||||
|
||||
Returns:
|
||||
List of all available model names
|
||||
"""
|
||||
# Always return all models from config, regardless of local availability
|
||||
return list(MODEL_CONFIGS.keys())
|
||||
|
||||
|
||||
def load_model_once(model_name: str, use_gpu: bool) -> bool:
|
||||
"""
|
||||
Load model and tags once for batch processing.
|
||||
|
||||
Args:
|
||||
model_name: Name of the model to load
|
||||
use_gpu: Whether to use GPU acceleration
|
||||
|
||||
Returns:
|
||||
True if model loaded successfully, False otherwise
|
||||
"""
|
||||
global model_cache
|
||||
|
||||
# Check if model is already loaded
|
||||
|
||||
if model_cache['model_name'] == model_name and model_cache['session'] is not None:
|
||||
return True
|
||||
|
||||
|
||||
try:
|
||||
# Ensure model is available (download if needed)
|
||||
success, model_path, csv_path = ensure_model_available(model_name)
|
||||
if not success:
|
||||
logger.error(f"Failed to ensure model availability: {model_name}")
|
||||
return False
|
||||
|
||||
# Initialize ONNX session with GPU support
|
||||
|
||||
providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] if use_gpu else ["CPUExecutionProvider"]
|
||||
|
||||
try:
|
||||
sess = ort.InferenceSession(model_path, providers=providers)
|
||||
except Exception as e:
|
||||
logger.warning(f"GPU not available, falling back to CPU: {e}")
|
||||
sess = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"])
|
||||
|
||||
input_name = sess.get_inputs()[0].name
|
||||
input_size = sess.get_inputs()[0].shape[1]
|
||||
|
||||
# Load tags from CSV
|
||||
input_name = sess.get_inputs()[0].name
|
||||
input_size = int(sess.get_inputs()[0].shape[1])
|
||||
|
||||
tags: List[str] = []
|
||||
tag_categories: List[int] = []
|
||||
if not Path(csv_path).exists():
|
||||
logger.error(f"CSV file not found: {csv_path}")
|
||||
return False
|
||||
|
||||
|
||||
tags = []
|
||||
tag_categories = []
|
||||
with open(csv_path, newline='', encoding='utf-8') as f:
|
||||
reader = csv.reader(f)
|
||||
next(reader) # Skip header
|
||||
next(reader)
|
||||
for row in reader:
|
||||
if len(row) > 1:
|
||||
tags.append(row[1])
|
||||
@@ -253,6 +221,7 @@ def load_model_once(model_name: str, use_gpu: bool) -> bool:
|
||||
tag_categories.append(int(row[2]))
|
||||
except (ValueError, IndexError):
|
||||
tag_categories.append(-1)
|
||||
|
||||
|
||||
# Cache the model and tags
|
||||
model_cache['session'] = sess
|
||||
@@ -261,40 +230,111 @@ def load_model_once(model_name: str, use_gpu: bool) -> bool:
|
||||
model_cache['model_name'] = model_name
|
||||
model_cache['input_name'] = input_name
|
||||
model_cache['input_size'] = input_size
|
||||
|
||||
logger.info(f"Model {model_name} loaded and cached successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading model {model_name}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def normalize_image_to_uint8(image: Any) -> np.ndarray:
|
||||
if isinstance(image, torch.Tensor):
|
||||
image_np = image.detach().cpu().numpy()
|
||||
else:
|
||||
image_np = np.asarray(image)
|
||||
|
||||
if image_np is None or image_np.size == 0:
|
||||
raise ValueError("Empty image")
|
||||
|
||||
if image_np.ndim == 4:
|
||||
image_np = image_np[0]
|
||||
|
||||
if image_np.ndim != 3:
|
||||
raise ValueError(f"Unsupported image shape: {image_np.shape}")
|
||||
|
||||
if image_np.shape[-1] == 4:
|
||||
image_np = image_np[:, :, :3]
|
||||
|
||||
if image_np.dtype != np.uint8:
|
||||
image_np = np.clip(image_np, 0.0, 1.0)
|
||||
image_np = (image_np * 255.0).astype(np.uint8)
|
||||
|
||||
return image_np
|
||||
|
||||
|
||||
def run_wd14_single(
|
||||
image: Any,
|
||||
model_name: str,
|
||||
threshold_general: float,
|
||||
threshold_character: float,
|
||||
threshold_meta: float,
|
||||
threshold_rating: float,
|
||||
replace_underscore: bool,
|
||||
exclude_tags: str,
|
||||
use_gpu: bool,
|
||||
) -> Tuple[str, List[str], List[Tuple[str, float, str]]]:
|
||||
if not load_model_once(model_name, use_gpu):
|
||||
return "", [], []
|
||||
|
||||
sess = model_cache['session']
|
||||
tags = model_cache['tags']
|
||||
categories = model_cache['tag_categories'] or []
|
||||
input_name = model_cache['input_name']
|
||||
input_size = model_cache['input_size']
|
||||
|
||||
processed_tags = [t.replace("_", " ") if replace_underscore else t for t in tags]
|
||||
exclude_list = [t.strip().lower() for t in exclude_tags.split(",") if t.strip()]
|
||||
|
||||
image_np = normalize_image_to_uint8(image)
|
||||
img = Image.fromarray(image_np)
|
||||
ratio = input_size / max(img.size)
|
||||
new_size = (max(1, int(img.size[0] * ratio)), max(1, int(img.size[1] * ratio)))
|
||||
try:
|
||||
img = img.resize(new_size, Image.Resampling.LANCZOS)
|
||||
except AttributeError:
|
||||
img = img.resize(new_size, Image.LANCZOS)
|
||||
|
||||
square = Image.new("RGB", (input_size, input_size), (255, 255, 255))
|
||||
square.paste(img, ((input_size - new_size[0]) // 2, (input_size - new_size[1]) // 2))
|
||||
|
||||
inp = np.expand_dims(np.array(square).astype(np.float32)[:, :, ::-1], 0)
|
||||
probs = sess.run(None, {input_name: inp})[0][0]
|
||||
|
||||
result_tags: List[str] = []
|
||||
debug_scores: List[Tuple[str, float, str]] = []
|
||||
for idx, (tag, prob) in enumerate(zip(processed_tags, probs)):
|
||||
raw_category = categories[idx] if idx < len(categories) else -1
|
||||
category_name = CATEGORY_DEFAULTS.get(raw_category, "general")
|
||||
if category_name == "character":
|
||||
current_threshold = threshold_character
|
||||
elif category_name == "meta":
|
||||
current_threshold = threshold_meta
|
||||
elif category_name == "rating":
|
||||
current_threshold = threshold_rating
|
||||
else:
|
||||
current_threshold = threshold_general
|
||||
|
||||
if prob > current_threshold and tag.lower() not in exclude_list:
|
||||
result_tags.append(tag)
|
||||
debug_scores.append((tag, float(prob), category_name))
|
||||
|
||||
return ", ".join(result_tags), result_tags, debug_scores
|
||||
|
||||
|
||||
class BSS_LoadImagesFolder:
|
||||
"""
|
||||
Load images from a folder and return them as a batch.
|
||||
Supports JPG, JPEG, PNG, and WEBP formats.
|
||||
Author: Blacksnowskill
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict[str, Any]:
|
||||
return {
|
||||
"required": {
|
||||
"folder_path": ("STRING", {"multiline": False, "default": ""})
|
||||
}
|
||||
}
|
||||
return {"required": {"folder_path": ("STRING", {"multiline": False, "default": ""})}}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING", "STRING")
|
||||
RETURN_NAMES = ("images", "filenames", "folder_path")
|
||||
OUTPUT_IS_LIST = (True, True, False)
|
||||
FUNCTION = "load_images"
|
||||
CATEGORY = "BSS/Image Processing"
|
||||
|
||||
# ComfyUI progress bar support
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
return float("inf")
|
||||
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs):
|
||||
return True
|
||||
@@ -312,55 +352,42 @@ class BSS_LoadImagesFolder:
|
||||
if not folder_path or not os.path.exists(folder_path):
|
||||
logger.error(f"Invalid folder path: {folder_path}")
|
||||
return [], [], folder_path
|
||||
|
||||
# Support JPG, JPEG, PNG, and WEBP formats
|
||||
|
||||
supported_extensions = (".jpg", ".jpeg", ".png", ".webp")
|
||||
folder = Path(folder_path)
|
||||
|
||||
try:
|
||||
image_paths = sorted([p for p in folder.iterdir()
|
||||
if p.suffix.lower() in supported_extensions and p.is_file()])
|
||||
image_paths = sorted([p for p in folder.iterdir() if p.suffix.lower() in supported_extensions and p.is_file()])
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading folder {folder_path}: {e}")
|
||||
return [], [], folder_path
|
||||
|
||||
images, filenames = [], []
|
||||
images: List[torch.Tensor] = []
|
||||
filenames: List[str] = []
|
||||
total_images = len(image_paths)
|
||||
|
||||
# Initialize global progress for batch processing
|
||||
batch_progress.init_progress(total_images)
|
||||
|
||||
# Use ComfyUI's progress system
|
||||
|
||||
import comfy.utils
|
||||
pbar = comfy.utils.ProgressBar(total_images)
|
||||
|
||||
pbar = comfy.utils.ProgressBar(total_images) if total_images > 0 else None
|
||||
|
||||
for i, image_path in enumerate(image_paths):
|
||||
# Update ComfyUI progress bar
|
||||
pbar.update(1)
|
||||
logger.info(f"Loading images... {i+1}/{total_images} - {image_path.name}")
|
||||
|
||||
if pbar:
|
||||
pbar.update(1)
|
||||
logger.info(f"Loading images... {i + 1}/{total_images} - {image_path.name}")
|
||||
|
||||
try:
|
||||
with Image.open(image_path) as img:
|
||||
arr = np.array(img.convert("RGB"), dtype=np.float32) / 255.0
|
||||
if arr.size == 0 or arr.shape[0] == 0 or arr.shape[1] == 0:
|
||||
logger.warning(f"Empty array for {image_path.name}, skipping")
|
||||
continue
|
||||
images.append(torch.from_numpy(arr))
|
||||
filenames.append(image_path.name)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading {image_path.name}: {e}")
|
||||
|
||||
logger.info(f"Loaded {len(images)} images from {folder_path}")
|
||||
return images, filenames, folder_path
|
||||
|
||||
|
||||
class BSS_WD14BatchTagger:
|
||||
"""
|
||||
WD14 Batch Tagger for automatic image tagging using WD14 models.
|
||||
Supports multiple model architectures and GPU acceleration.
|
||||
Author: Blacksnowskill
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict[str, Any]:
|
||||
return {
|
||||
@@ -369,69 +396,51 @@ class BSS_WD14BatchTagger:
|
||||
"filename": ("STRING",),
|
||||
"folder_path": ("STRING", {"default": ""}),
|
||||
"model": (cls._get_model_choices(), {"default": "wd-vit-tagger-v3"}),
|
||||
"threshold": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"general_threshold": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"character_threshold": ("FLOAT", {"default": 0.85, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"meta_threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"rating_threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"replace_underscore": ("BOOLEAN", {"default": True}),
|
||||
"use_gpu": ("BOOLEAN", {"default": False}),
|
||||
"prepend_tags": ("STRING", {"multiline": True, "default": ""}),
|
||||
"exclude_tags": ("STRING", {"multiline": True, "default": ""})
|
||||
"exclude_tags": ("STRING", {"multiline": True, "default": ""}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@classmethod
|
||||
def _get_model_choices(cls) -> List[str]:
|
||||
"""Get model choices with current status indicators."""
|
||||
# Get all available models
|
||||
available_models = get_available_models()
|
||||
|
||||
# Check which models are locally available
|
||||
model_dir = Path(__file__).parent / "models"
|
||||
local_models = set()
|
||||
if model_dir.exists():
|
||||
for model_name in MODEL_CONFIGS.keys():
|
||||
model_path = model_dir / f"{model_name}.onnx"
|
||||
csv_path = model_dir / f"{model_name}.csv"
|
||||
if model_path.exists() and csv_path.exists():
|
||||
if (model_dir / f"{model_name}.onnx").exists() and (model_dir / f"{model_name}.csv").exists():
|
||||
local_models.add(model_name)
|
||||
|
||||
# Create display names for the dropdown with status indicators
|
||||
model_choices = []
|
||||
for model_name in available_models:
|
||||
if model_name in MODEL_CONFIGS:
|
||||
status = "✅" if model_name in local_models else "⬇️"
|
||||
display_name = f"{status} {MODEL_CONFIGS[model_name]['display_name']} ({model_name})"
|
||||
model_choices.append(f"{model_name}|{display_name}")
|
||||
|
||||
return model_choices
|
||||
|
||||
|
||||
choices = []
|
||||
for model_name in get_available_models():
|
||||
status = "✅" if model_name in local_models else "⬇️"
|
||||
display_name = f"{status} {MODEL_CONFIGS[model_name]['display_name']} ({model_name})"
|
||||
choices.append(f"{model_name}|{display_name}")
|
||||
return choices
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs):
|
||||
"""Validate inputs, especially model selection."""
|
||||
model = kwargs.get("model", "")
|
||||
if not model:
|
||||
return True
|
||||
|
||||
# Extract model name from dropdown selection
|
||||
model_name = model.split("|")[0] if "|" in model else model
|
||||
|
||||
# Check if model is in our config
|
||||
if model_name not in MODEL_CONFIGS:
|
||||
return f"Unknown model: {model_name}"
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
"""Force refresh when model status changes."""
|
||||
# This will force ComfyUI to refresh the node when models are downloaded
|
||||
model_dir = Path(__file__).parent / "models"
|
||||
if not model_dir.exists():
|
||||
return float("inf")
|
||||
|
||||
# Check modification time of models directory
|
||||
try:
|
||||
return model_dir.stat().st_mtime
|
||||
except:
|
||||
except Exception:
|
||||
return float("inf")
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
@@ -441,6 +450,49 @@ class BSS_WD14BatchTagger:
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "BSS/Image Processing"
|
||||
|
||||
def tag_batch(
|
||||
self,
|
||||
image: Any,
|
||||
filename: str,
|
||||
folder_path: str,
|
||||
model: str,
|
||||
general_threshold: float,
|
||||
character_threshold: float,
|
||||
meta_threshold: float,
|
||||
rating_threshold: float,
|
||||
replace_underscore: bool,
|
||||
use_gpu: bool,
|
||||
prepend_tags: str,
|
||||
exclude_tags: str,
|
||||
) -> Tuple[str]:
|
||||
try:
|
||||
model_name = model.split("|")[0] if "|" in model else model
|
||||
_, result_tags, _ = run_wd14_single(
|
||||
image,
|
||||
model_name,
|
||||
general_threshold,
|
||||
character_threshold,
|
||||
meta_threshold,
|
||||
rating_threshold,
|
||||
replace_underscore,
|
||||
exclude_tags,
|
||||
use_gpu,
|
||||
)
|
||||
|
||||
try:
|
||||
model_name = model.split("|")[0] if "|" in model else model
|
||||
_, result_tags, _ = run_wd14_single(
|
||||
image,
|
||||
model_name,
|
||||
general_threshold,
|
||||
character_threshold,
|
||||
meta_threshold,
|
||||
rating_threshold,
|
||||
replace_underscore,
|
||||
exclude_tags,
|
||||
use_gpu,
|
||||
)
|
||||
|
||||
def tag_batch(self, image: Any, filename: str, folder_path: str, model: str,
|
||||
threshold: float, character_threshold: float, replace_underscore: bool,
|
||||
use_gpu: bool, prepend_tags: str, exclude_tags: str) -> Tuple[str]:
|
||||
@@ -548,33 +600,237 @@ class BSS_WD14BatchTagger:
|
||||
output_tags += ", "
|
||||
output_tags += ", ".join(result_tags)
|
||||
|
||||
# Save to file
|
||||
if folder_path:
|
||||
base_name = Path(filename).stem
|
||||
txt_path = Path(folder_path) / f"{base_name}.txt"
|
||||
txt_path = Path(folder_path) / f"{Path(filename).stem}.txt"
|
||||
try:
|
||||
with open(txt_path, "w", encoding="utf-8") as f:
|
||||
f.write(output_tags)
|
||||
logger.info(f"Saved tags to {txt_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to write {txt_path}: {e}")
|
||||
|
||||
# Update progress (1 step per image)
|
||||
batch_progress.update_image_progress(f"Generated {len(result_tags)} tags for '{filename}'")
|
||||
batch_progress.next_image()
|
||||
return (output_tags,)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in tag_batch for {filename}: {e}")
|
||||
return ("",)
|
||||
|
||||
|
||||
class BSS_WD14TaggerBatch:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict[str, Any]:
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filenames": ("STRING",),
|
||||
"folder_path": ("STRING", {"default": ""}),
|
||||
"model": (BSS_WD14BatchTagger._get_model_choices(), {"default": "wd-vit-tagger-v3"}),
|
||||
"general_threshold": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"character_threshold": ("FLOAT", {"default": 0.85, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"meta_threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"rating_threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"replace_underscore": ("BOOLEAN", {"default": True}),
|
||||
"use_gpu": ("BOOLEAN", {"default": False}),
|
||||
"prepend_tags": ("STRING", {"multiline": True, "default": ""}),
|
||||
"exclude_tags": ("STRING", {"multiline": True, "default": ""}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("tags", "tags_json")
|
||||
OUTPUT_IS_LIST = (True, False)
|
||||
FUNCTION = "tag_images"
|
||||
CATEGORY = "BSS/Image Processing"
|
||||
|
||||
def tag_images(self, images, filenames, folder_path, model, general_threshold, character_threshold, meta_threshold,
|
||||
rating_threshold, replace_underscore, use_gpu, prepend_tags, exclude_tags):
|
||||
model_name = model.split("|")[0] if "|" in model else model
|
||||
image_list = list(images) if isinstance(images, (list, tuple)) else [images]
|
||||
filename_list = list(filenames) if isinstance(filenames, (list, tuple)) else [filenames]
|
||||
|
||||
if len(filename_list) < len(image_list):
|
||||
filename_list += [f"image_{i:05d}.png" for i in range(len(filename_list), len(image_list))]
|
||||
|
||||
tags_list: List[str] = []
|
||||
payload: Dict[str, Any] = {}
|
||||
|
||||
for image, name in zip(image_list, filename_list):
|
||||
_, result_tags, scored_tags = run_wd14_single(
|
||||
image,
|
||||
model_name,
|
||||
general_threshold,
|
||||
character_threshold,
|
||||
meta_threshold,
|
||||
rating_threshold,
|
||||
replace_underscore,
|
||||
exclude_tags,
|
||||
use_gpu,
|
||||
)
|
||||
output_tags = prepend_tags.strip()
|
||||
if output_tags and result_tags:
|
||||
output_tags += ", "
|
||||
output_tags += ", ".join(result_tags)
|
||||
tags_list.append(output_tags)
|
||||
payload[name] = [{"tag": t, "score": s, "category": c} for t, s, c in scored_tags]
|
||||
|
||||
if folder_path:
|
||||
txt_path = Path(folder_path) / f"{Path(name).stem}.txt"
|
||||
try:
|
||||
with open(txt_path, "w", encoding="utf-8") as f:
|
||||
f.write(output_tags)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to write {txt_path}: {e}")
|
||||
|
||||
return tags_list, json.dumps(payload, ensure_ascii=False)
|
||||
|
||||
|
||||
class BSS_TagsPostprocess:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"tags": ("STRING", {"multiline": True, "default": ""}),
|
||||
"sort_alphabetically": ("BOOLEAN", {"default": False}),
|
||||
"unique_only": ("BOOLEAN", {"default": True}),
|
||||
"replace_underscore": ("BOOLEAN", {"default": True}),
|
||||
"append_tags": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prepend_tags": ("STRING", {"multiline": True, "default": ""}),
|
||||
"exclude_tags": ("STRING", {"multiline": True, "default": ""}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "INT")
|
||||
RETURN_NAMES = ("tags", "tag_count")
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "BSS/Image Processing"
|
||||
|
||||
def process(self, tags, sort_alphabetically, unique_only, replace_underscore, append_tags, prepend_tags, exclude_tags):
|
||||
source = [t.strip() for t in tags.split(",") if t.strip()]
|
||||
if prepend_tags.strip():
|
||||
source = [t.strip() for t in prepend_tags.split(",") if t.strip()] + source
|
||||
if append_tags.strip():
|
||||
source += [t.strip() for t in append_tags.split(",") if t.strip()]
|
||||
|
||||
if replace_underscore:
|
||||
source = [t.replace("_", " ") for t in source]
|
||||
|
||||
excluded = {t.strip().lower() for t in exclude_tags.split(",") if t.strip()}
|
||||
source = [t for t in source if t.lower() not in excluded]
|
||||
|
||||
if unique_only:
|
||||
seen = set()
|
||||
out = []
|
||||
for t in source:
|
||||
key = t.lower()
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
out.append(t)
|
||||
source = out
|
||||
|
||||
if sort_alphabetically:
|
||||
source = sorted(source, key=lambda x: x.lower())
|
||||
|
||||
return ", ".join(source), len(source)
|
||||
|
||||
|
||||
class BSS_SaveCaptions:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"tags": ("STRING",),
|
||||
"filename": ("STRING",),
|
||||
"output_folder": ("STRING", {"default": ""}),
|
||||
"format": (["txt", "json", "csv"], {"default": "txt"}),
|
||||
"overwrite": ("BOOLEAN", {"default": True}),
|
||||
"suffix": ("STRING", {"default": ""}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "BOOLEAN")
|
||||
RETURN_NAMES = ("saved_path", "saved")
|
||||
FUNCTION = "save"
|
||||
CATEGORY = "BSS/Image Processing"
|
||||
|
||||
def save(self, tags, filename, output_folder, format, overwrite, suffix):
|
||||
if not output_folder:
|
||||
return "", False
|
||||
out_dir = Path(output_folder)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
stem = Path(filename).stem
|
||||
base = f"{stem}{suffix}" if suffix else stem
|
||||
out_path = out_dir / f"{base}.{format}"
|
||||
|
||||
if out_path.exists() and not overwrite:
|
||||
return str(out_path), False
|
||||
|
||||
try:
|
||||
if format == "txt":
|
||||
out_path.write_text(tags, encoding="utf-8")
|
||||
elif format == "json":
|
||||
out_path.write_text(json.dumps({"filename": filename, "tags": tags}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
else:
|
||||
with open(out_path, "w", encoding="utf-8", newline="") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(["filename", "tags"])
|
||||
writer.writerow([filename, tags])
|
||||
return str(out_path), True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save captions: {e}")
|
||||
return "", False
|
||||
|
||||
|
||||
class BSS_TagAnalytics:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"tags_list": ("STRING",),
|
||||
"top_k": ("INT", {"default": 20, "min": 1, "max": 200, "step": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("top_tags", "full_stats_json")
|
||||
FUNCTION = "analyze"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "BSS/Image Processing"
|
||||
|
||||
def analyze(self, tags_list, top_k):
|
||||
values = list(tags_list) if isinstance(tags_list, (list, tuple)) else [tags_list]
|
||||
counts: Dict[str, int] = {}
|
||||
total_items = 0
|
||||
for text in values:
|
||||
for tag in [t.strip() for t in str(text).split(",") if t.strip()]:
|
||||
counts[tag] = counts.get(tag, 0) + 1
|
||||
total_items += 1
|
||||
|
||||
sorted_tags = sorted(counts.items(), key=lambda kv: kv[1], reverse=True)
|
||||
top = sorted_tags[:top_k]
|
||||
top_line = ", ".join([f"{tag}:{count}" for tag, count in top])
|
||||
|
||||
payload = {
|
||||
"total_tags": total_items,
|
||||
"unique_tags": len(counts),
|
||||
"top": [{"tag": tag, "count": count} for tag, count in top],
|
||||
}
|
||||
return top_line, json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"BSS_LoadImagesFolder": BSS_LoadImagesFolder,
|
||||
"BSS_WD14BatchTagger": BSS_WD14BatchTagger,
|
||||
"BSS_WD14TaggerBatch": BSS_WD14TaggerBatch,
|
||||
"BSS_TagsPostprocess": BSS_TagsPostprocess,
|
||||
"BSS_SaveCaptions": BSS_SaveCaptions,
|
||||
"BSS_TagAnalytics": BSS_TagAnalytics,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"BSS_LoadImagesFolder": "BSS Load Images from Folder 📂",
|
||||
"BSS_WD14BatchTagger": "BSS WD14 Batch Tagger 🌿",
|
||||
"BSS_WD14TaggerBatch": "BSS WD14 Tagger Batch ⚡",
|
||||
"BSS_TagsPostprocess": "BSS Tags Postprocess 🧹",
|
||||
"BSS_SaveCaptions": "BSS Save Captions 💾",
|
||||
"BSS_TagAnalytics": "BSS Tag Analytics 📊",
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wd14-batch-tagger",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"description": "Automatic image tagging using WD14 models with batch processing capabilities",
|
||||
"author": "Blacksnowskill",
|
||||
"license": "MIT",
|
||||
@@ -28,7 +28,7 @@
|
||||
"install_script": "install.py"
|
||||
},
|
||||
"dependencies": {
|
||||
"onnxruntime": ">=1.16.0",
|
||||
"onnxruntime": ">=1.18.0,<2.0.0",
|
||||
"numpy": ">=1.24.0",
|
||||
"pillow": ">=9.0.0",
|
||||
"huggingface-hub": ">=0.16.0"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "wd14-batch-tagger"
|
||||
description = "Automatic image tagging using WD14 models with batch processing capabilities for ComfyUI"
|
||||
version = "1.0.1"
|
||||
version = "2.0.0"
|
||||
license = { file = "LICENSE.txt" }
|
||||
dependencies = [
|
||||
"onnxruntime>=1.18.0,<2.0.0",
|
||||
|
||||
Reference in New Issue
Block a user