recoilme commited on
Commit
d0a6bd4
·
verified ·
1 Parent(s): 7464bee

Upload caption/wd14_tagger.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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()