Upload caption/wd14_tagger.py with huggingface_hub
Browse files- caption/wd14_tagger.py +151 -0
caption/wd14_tagger.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""WD14 tagger — danbooru-теги для аниме-картинок (SmilingWolf/wd-*-tagger-v3).
|
| 3 |
+
|
| 4 |
+
Пишет теги в <имя_файла>.txt (danbooru-стиль: "1girl, animal ears, ...").
|
| 5 |
+
Уже размеченные файлы пропускает (--force — перезаписать).
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python wd14_tagger.py <папка_или_файл>... [--model vit] [--gen 0.5] [--char 0.85]
|
| 9 |
+
|
| 10 |
+
Зависимости: torch, timm, pandas, pillow, huggingface_hub. Нужен GPU (или CPU).
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import time
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
import pandas as pd
|
| 20 |
+
import timm
|
| 21 |
+
import torch
|
| 22 |
+
from huggingface_hub import hf_hub_download
|
| 23 |
+
from huggingface_hub.utils import HfHubHTTPError
|
| 24 |
+
from PIL import Image
|
| 25 |
+
from timm.data import create_transform, resolve_data_config
|
| 26 |
+
from torch import nn
|
| 27 |
+
from torch.nn import functional as F
|
| 28 |
+
from tqdm import tqdm
|
| 29 |
+
|
| 30 |
+
MODEL_REPO_MAP = {
|
| 31 |
+
"vit": "SmilingWolf/wd-vit-tagger-v3",
|
| 32 |
+
"swinv2": "SmilingWolf/wd-swinv2-tagger-v3",
|
| 33 |
+
"convnext": "SmilingWolf/wd-convnext-tagger-v3",
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def pil_ensure_rgb(image: Image.Image) -> Image.Image:
|
| 40 |
+
if image.mode not in ("RGB", "RGBA"):
|
| 41 |
+
image = image.convert("RGBA" if "transparency" in image.info else "RGB")
|
| 42 |
+
if image.mode == "RGBA":
|
| 43 |
+
canvas = Image.new("RGBA", image.size, (255, 255, 255))
|
| 44 |
+
canvas.alpha_composite(image)
|
| 45 |
+
image = canvas.convert("RGB")
|
| 46 |
+
return image
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def pil_pad_square(image: Image.Image) -> Image.Image:
|
| 50 |
+
w, h = image.size
|
| 51 |
+
px = max(image.size)
|
| 52 |
+
canvas = Image.new("RGB", (px, px), (255, 255, 255))
|
| 53 |
+
canvas.paste(image, ((px - w) // 2, (px - h) // 2))
|
| 54 |
+
return canvas
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@dataclass
|
| 58 |
+
class LabelData:
|
| 59 |
+
names: list
|
| 60 |
+
rating: list
|
| 61 |
+
general: list
|
| 62 |
+
character: list
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def load_labels_hf(repo_id: str) -> LabelData:
|
| 66 |
+
try:
|
| 67 |
+
csv_path = hf_hub_download(repo_id=repo_id, filename="selected_tags.csv")
|
| 68 |
+
except HfHubHTTPError as e:
|
| 69 |
+
raise FileNotFoundError(f"selected_tags.csv failed to download from {repo_id}") from e
|
| 70 |
+
df = pd.read_csv(csv_path, usecols=["name", "category"])
|
| 71 |
+
return LabelData(
|
| 72 |
+
names=df["name"].tolist(),
|
| 73 |
+
rating=list(np.where(df["category"] == 9)[0]),
|
| 74 |
+
general=list(np.where(df["category"] == 0)[0]),
|
| 75 |
+
character=list(np.where(df["category"] == 4)[0]),
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def get_tags(probs, labels, gen_threshold, char_threshold):
|
| 80 |
+
"""Probs -> строка тегов (danbooru-стиль, рейтинг в конце)."""
|
| 81 |
+
probs = list(zip(labels.names, probs.numpy()))
|
| 82 |
+
rating = dict(sorted((probs[i] for i in labels.rating), key=lambda x: x[1], reverse=True))
|
| 83 |
+
gen = {n: p for n, p in (probs[i] for i in labels.general) if p > gen_threshold}
|
| 84 |
+
char = {n: p for n, p in (probs[i] for i in labels.character) if p > char_threshold}
|
| 85 |
+
gen = dict(sorted(gen.items(), key=lambda x: x[1], reverse=True))
|
| 86 |
+
char = dict(sorted(char.items(), key=lambda x: x[1], reverse=True))
|
| 87 |
+
tags = list(gen) + list(char)
|
| 88 |
+
caption = ", ".join(t.replace("_", " ").replace("(", r"\(").replace(")", r"\)") for t in tags)
|
| 89 |
+
caption += ", rating_" + next(iter(rating), "")
|
| 90 |
+
return caption
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def main():
|
| 94 |
+
parser = argparse.ArgumentParser(description="WD14 tagger (danbooru-теги)")
|
| 95 |
+
parser.add_argument("paths", nargs="+", help="папки или файлы (папки обходятся рекурсивно)")
|
| 96 |
+
parser.add_argument("--model", default="vit", choices=sorted(MODEL_REPO_MAP))
|
| 97 |
+
parser.add_argument("--gen", dest="gen_threshold", type=float, default=0.5,
|
| 98 |
+
help="порог уверенности для general-тегов")
|
| 99 |
+
parser.add_argument("--char", dest="char_threshold", type=float, default=0.85,
|
| 100 |
+
help="порог уверенности для character-тегов")
|
| 101 |
+
parser.add_argument("--force", action="store_true", help="перезаписать существующие .txt")
|
| 102 |
+
args = parser.parse_args()
|
| 103 |
+
|
| 104 |
+
repo_id = MODEL_REPO_MAP[args.model]
|
| 105 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 106 |
+
|
| 107 |
+
# собираем файлы
|
| 108 |
+
files = []
|
| 109 |
+
for arg in args.paths:
|
| 110 |
+
p = Path(arg)
|
| 111 |
+
if p.is_dir():
|
| 112 |
+
files.extend(f for f in p.rglob("*") if f.suffix.lower() in IMAGE_EXTS)
|
| 113 |
+
elif p.is_file():
|
| 114 |
+
files.append(p)
|
| 115 |
+
files = sorted(set(files))
|
| 116 |
+
if not files:
|
| 117 |
+
print("Нет файлов для обработки.")
|
| 118 |
+
return
|
| 119 |
+
|
| 120 |
+
print(f"Загрузка модели {args.model} ({repo_id}) на {device}...")
|
| 121 |
+
model = timm.create_model("hf-hub:" + repo_id).eval()
|
| 122 |
+
model.load_state_dict(timm.models.load_state_dict_from_hf(repo_id))
|
| 123 |
+
labels = load_labels_hf(repo_id)
|
| 124 |
+
transform = create_transform(**resolve_data_config(model.pretrained_cfg, model=model))
|
| 125 |
+
if device.type == "cuda":
|
| 126 |
+
model = model.to(device)
|
| 127 |
+
|
| 128 |
+
t0 = time.time()
|
| 129 |
+
done = skipped = 0
|
| 130 |
+
for image_path in tqdm(files, desc="Tagging", unit="img"):
|
| 131 |
+
txt_path = image_path.with_suffix(".txt")
|
| 132 |
+
if txt_path.exists() and not args.force:
|
| 133 |
+
skipped += 1
|
| 134 |
+
continue
|
| 135 |
+
try:
|
| 136 |
+
img = pil_pad_square(pil_ensure_rgb(Image.open(image_path)))
|
| 137 |
+
inputs = transform(img).unsqueeze(0)[:, [2, 1, 0]] # RGB -> BGR
|
| 138 |
+
with torch.inference_mode():
|
| 139 |
+
outputs = F.sigmoid(model(inputs.to(device))).cpu()
|
| 140 |
+
caption = get_tags(outputs.squeeze(0), labels, args.gen_threshold, args.char_threshold)
|
| 141 |
+
txt_path.write_text(caption + "\n")
|
| 142 |
+
done += 1
|
| 143 |
+
except Exception as e:
|
| 144 |
+
tqdm.write(f" SKIP {image_path.name}: {e}")
|
| 145 |
+
|
| 146 |
+
print(f"\nГотово: {done} размечено, {skipped} пропущено (уже есть .txt), "
|
| 147 |
+
f"за {time.strftime('%H:%M:%S', time.gmtime(time.time() - t0))}")
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
if __name__ == "__main__":
|
| 151 |
+
main()
|