Author SHA1 Message Date
BlackSnowSkill 6a8bf5c479 Update README.md 2026-02-11 02:23:48 +05:00
BlackSnowSkill d9e96ce009 Merge pull request #4
README: русификация + пример workflow; удалить PUBLICATION_GUID; ноды рефактор для v2.0
2026-02-11 02:09:35 +05:00
BlackSnowSkill 2883e5af3a Merge remote-tracking branch 'origin/main' into codex/improve-comfyui-node-compatibility-ckeiw0 2026-02-10 21:07:47 +00:00
BlackSnowSkill 918c136b2a Polish Russian docs and move full history to CHANGELOG 2026-02-10 23:43:57 +05:00
BlackSnowSkill a2fef4995d Localize README to Russian and remove publication guide 2026-02-10 20:07:09 +05:00
BlackSnowSkill bc26ce54da Merge pull request #3
Refocus README on user-facing docs and changelog
2026-02-10 19:57:43 +05:00
BlackSnowSkill 90e8cc84db Merge branch 'main' into codex/improve-comfyui-node-compatibility-1xlc9z 2026-02-10 19:56:50 +05:00
BlackSnowSkill b72560a871 Refocus README on user docs and changelog standards 2026-02-10 19:55:51 +05:00
BlackSnowSkill 3b94fab3ec Merge pull request #2
v2.0.0: batch tagging, postprocess, save & analytics nodes + release metadata
2026-02-10 19:49:43 +05:00
BlackSnowSkill dfdbe7d539 Merge branch 'main' into codex/improve-comfyui-node-compatibility-dqcqbc 2026-02-10 19:49:14 +05:00
BlackSnowSkill d766af1ce1 Release v2.0.0 with batch tagging, postprocess, save, and analytics nodes 2026-02-10 19:46:48 +05:00
BlackSnowSkill 8b4a8d0deb Merge pull request #1 from BlackSnowSkill/codex/improve-comfyui-node-compatibility
Codex-generated pull request
2026-02-10 19:28:03 +05:00
9 changed files with 568 additions and 447 deletions
+21
View File
@@ -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-теггер и загрузка изображений из папки.
-126
View File
@@ -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
---
**Готово к публикации!** 🚀
+93 -57
View File
@@ -1,12 +1,24 @@
# BSS WD14 Batch Tagger
# BSS WD14 Batch Tagger v2.0
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
View File
@@ -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"
+2 -2
View File
@@ -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
View File
@@ -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"
}
}
+423 -167
View File
@@ -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
View File
@@ -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
View File
@@ -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",