Werli commited on
Commit
9988a3a
·
verified ·
1 Parent(s): 9e31c5a

Upload 6 files

Browse files

Integrated "Gelbooru Image Fetcher" tab which is a big change, it will help you to find characters and other stuff pretty easy and faster, still working on it, so maybe in the future there will be even more features. Also fixed some other minor stuff and changed the description accordingly. Have fun!

Files changed (3) hide show
  1. app.py +640 -592
  2. modules/booru.py +137 -0
  3. modules/classifyTags.py +191 -174
app.py CHANGED
@@ -1,593 +1,641 @@
1
- import os
2
- import io,copy,requests,spaces,gradio as gr,numpy as np
3
- from transformers import AutoProcessor,AutoModelForCausalLM
4
- from PIL import Image,ImageDraw,ImageFont
5
- from unittest.mock import patch
6
- import argparse,huggingface_hub,onnxruntime as rt,pandas as pd,traceback,tempfile,zipfile,re,ast,time
7
- from datetime import datetime,timezone
8
- from collections import defaultdict
9
- from apscheduler.schedulers.background import BackgroundScheduler
10
- import json
11
- from modules.classifyTags import classify_tags,process_tags
12
- from modules.florence2 import process_image,single_task_list,update_task_dropdown
13
- from modules.reorganizer_model import reorganizer_list,reorganizer_class
14
- from modules.tag_enhancer import prompt_enhancer
15
- os.environ['PYTORCH_ENABLE_MPS_FALLBACK']='1'
16
-
17
- TITLE = "Multi-Tagger"
18
- DESCRIPTION = """
19
- Multi-Tagger is a versatile application that combines the Waifu Diffusion and Florence 2 models for advanced image analysis and captioning. Perfect for AI artists and enthusiasts, it offers a range of features:
20
-
21
- - Batch processing for multiple images
22
- - Multi-category tagging with structured tag display.
23
- - CUDA or CPU support.
24
- - Image tagging, various captioning tasks which includes: Caption, Detailed Caption, Object Detection with visual outputs and much more.
25
-
26
- Example image by [me.](https://huggingface.co/Werli)
27
- """
28
-
29
- # Dataset v3 series of models:
30
- SWINV2_MODEL_DSV3_REPO = "SmilingWolf/wd-swinv2-tagger-v3"
31
- CONV_MODEL_DSV3_REPO = "SmilingWolf/wd-convnext-tagger-v3"
32
- VIT_MODEL_DSV3_REPO = "SmilingWolf/wd-vit-tagger-v3"
33
- VIT_LARGE_MODEL_DSV3_REPO = "SmilingWolf/wd-vit-large-tagger-v3"
34
- EVA02_LARGE_MODEL_DSV3_REPO = "SmilingWolf/wd-eva02-large-tagger-v3"
35
- # Dataset v2 series of models:
36
- MOAT_MODEL_DSV2_REPO = "SmilingWolf/wd-v1-4-moat-tagger-v2"
37
- SWIN_MODEL_DSV2_REPO = "SmilingWolf/wd-v1-4-swinv2-tagger-v2"
38
- CONV_MODEL_DSV2_REPO = "SmilingWolf/wd-v1-4-convnext-tagger-v2"
39
- CONV2_MODEL_DSV2_REPO = "SmilingWolf/wd-v1-4-convnextv2-tagger-v2"
40
- VIT_MODEL_DSV2_REPO = "SmilingWolf/wd-v1-4-vit-tagger-v2"
41
- # IdolSankaku series of models:
42
- EVA02_LARGE_MODEL_IS_DSV1_REPO = "deepghs/idolsankaku-eva02-large-tagger-v1"
43
- SWINV2_MODEL_IS_DSV1_REPO = "deepghs/idolsankaku-swinv2-tagger-v1"
44
- # Files to download from the repos
45
- MODEL_FILENAME = "model.onnx"
46
- LABEL_FILENAME = "selected_tags.csv"
47
-
48
- kaomojis=['0_0','(o)_(o)','+_+','+_-','._.','<o>_<o>','<|>_<|>','=_=','>_<','3_3','6_9','>_o','@_@','^_^','o_o','u_u','x_x','|_|','||_||']
49
- def parse_args()->argparse.Namespace:parser=argparse.ArgumentParser();parser.add_argument('--score-slider-step',type=float,default=.05);parser.add_argument('--score-general-threshold',type=float,default=.35);parser.add_argument('--score-character-threshold',type=float,default=.85);parser.add_argument('--share',action='store_true');return parser.parse_args()
50
- def load_labels(dataframe)->list[str]:name_series=dataframe['name'];name_series=name_series.map(lambda x:x.replace('_',' ')if x not in kaomojis else x);tag_names=name_series.tolist();rating_indexes=list(np.where(dataframe['category']==9)[0]);general_indexes=list(np.where(dataframe['category']==0)[0]);character_indexes=list(np.where(dataframe['category']==4)[0]);return tag_names,rating_indexes,general_indexes,character_indexes
51
- def mcut_threshold(probs):sorted_probs=probs[probs.argsort()[::-1]];difs=sorted_probs[:-1]-sorted_probs[1:];t=difs.argmax();thresh=(sorted_probs[t]+sorted_probs[t+1])/2;return thresh
52
-
53
- class Timer:
54
- def __init__(self):self.start_time=time.perf_counter();self.checkpoints=[('Start',self.start_time)]
55
- def checkpoint(self,label='Checkpoint'):now=time.perf_counter();self.checkpoints.append((label,now))
56
- def report(self,is_clear_checkpoints=True):
57
- max_label_length=max(len(label)for(label,_)in self.checkpoints);prev_time=self.checkpoints[0][1]
58
- for(label,curr_time)in self.checkpoints[1:]:elapsed=curr_time-prev_time;print(f"{label.ljust(max_label_length)}: {elapsed:.3f} seconds");prev_time=curr_time
59
- if is_clear_checkpoints:self.checkpoints.clear();self.checkpoint()
60
- def report_all(self):
61
- print('\n> Execution Time Report:');max_label_length=max(len(label)for(label,_)in self.checkpoints)if len(self.checkpoints)>0 else 0;prev_time=self.start_time
62
- for(label,curr_time)in self.checkpoints[1:]:elapsed=curr_time-prev_time;print(f"{label.ljust(max_label_length)}: {elapsed:.3f} seconds");prev_time=curr_time
63
- total_time=self.checkpoints[-1][1]-self.start_time;print(f"{'Total Execution Time'.ljust(max_label_length)}: {total_time:.3f} seconds\n");self.checkpoints.clear()
64
- def restart(self):self.start_time=time.perf_counter();self.checkpoints=[('Start',self.start_time)]
65
- class Predictor:
66
- def __init__(self):
67
- self.model_target_size = None
68
- self.last_loaded_repo = None
69
- def download_model(self, model_repo):
70
- csv_path = huggingface_hub.hf_hub_download(
71
- model_repo,
72
- LABEL_FILENAME,
73
- )
74
- model_path = huggingface_hub.hf_hub_download(
75
- model_repo,
76
- MODEL_FILENAME,
77
- )
78
- return csv_path, model_path
79
- def load_model(self, model_repo):
80
- if model_repo == self.last_loaded_repo:
81
- return
82
-
83
- csv_path, model_path = self.download_model(model_repo)
84
-
85
- tags_df = pd.read_csv(csv_path)
86
- sep_tags = load_labels(tags_df)
87
-
88
- self.tag_names = sep_tags[0]
89
- self.rating_indexes = sep_tags[1]
90
- self.general_indexes = sep_tags[2]
91
- self.character_indexes = sep_tags[3]
92
-
93
- model = rt.InferenceSession(model_path)
94
- _, height, width, _ = model.get_inputs()[0].shape
95
- self.model_target_size = height
96
-
97
- self.last_loaded_repo = model_repo
98
- self.model = model
99
- def prepare_image(self, path):
100
- image = Image.open(path)
101
- image = image.convert("RGBA")
102
- target_size = self.model_target_size
103
-
104
- canvas = Image.new("RGBA", image.size, (255, 255, 255))
105
- canvas.alpha_composite(image)
106
- image = canvas.convert("RGB")
107
-
108
- # Pad image to square
109
- image_shape = image.size
110
- max_dim = max(image_shape)
111
- pad_left = (max_dim - image_shape[0]) // 2
112
- pad_top = (max_dim - image_shape[1]) // 2
113
-
114
- padded_image = Image.new("RGB", (max_dim, max_dim), (255, 255, 255))
115
- padded_image.paste(image, (pad_left, pad_top))
116
-
117
- # Resize
118
- if max_dim != target_size:
119
- padded_image = padded_image.resize(
120
- (target_size, target_size),
121
- Image.BICUBIC,
122
- )
123
- # Convert to numpy array
124
- image_array = np.asarray(padded_image, dtype=np.float32)
125
- # Convert PIL-native RGB to BGR
126
- image_array = image_array[:, :, ::-1]
127
- return np.expand_dims(image_array, axis=0)
128
-
129
- def create_file(self, content: str, directory: str, fileName: str) -> str:
130
- # Write the content to a file
131
- file_path = os.path.join(directory, fileName)
132
- if fileName.endswith('.json'):
133
- with open(file_path, 'w', encoding="utf-8") as file:
134
- file.write(content)
135
- else:
136
- with open(file_path, 'w+', encoding="utf-8") as file:
137
- file.write(content)
138
-
139
- return file_path
140
-
141
- def predict(
142
- self,
143
- gallery,
144
- model_repo,
145
- general_thresh,
146
- general_mcut_enabled,
147
- character_thresh,
148
- character_mcut_enabled,
149
- characters_merge_enabled,
150
- reorganizer_model_repo,
151
- additional_tags_prepend,
152
- additional_tags_append,
153
- tag_results,
154
- progress=gr.Progress()
155
- ):
156
- # Clear tag_results before starting a new prediction
157
- tag_results.clear()
158
-
159
- gallery_len = len(gallery)
160
- print(f"Predict load model: {model_repo}, gallery length: {gallery_len}")
161
-
162
- timer = Timer() # Create a timer
163
- progressRatio = 0.5 if reorganizer_model_repo else 1
164
- progressTotal = gallery_len + 1
165
- current_progress = 0
166
-
167
- self.load_model(model_repo)
168
- current_progress += progressRatio/progressTotal;
169
- progress(current_progress, desc="Initialize wd model finished")
170
- timer.checkpoint(f"Initialize wd model")
171
-
172
- txt_infos = []
173
- output_dir = tempfile.mkdtemp()
174
- if not os.path.exists(output_dir):
175
- os.makedirs(output_dir)
176
-
177
- sorted_general_strings = ""
178
- # Create categorized output string
179
- categorized_output_strings = []
180
- rating = None
181
- character_res = None
182
- general_res = None
183
-
184
- if reorganizer_model_repo:
185
- print(f"Reorganizer load model {reorganizer_model_repo}")
186
- reorganizer = reorganizer_class(reorganizer_model_repo, loadModel=True)
187
- current_progress += progressRatio/progressTotal;
188
- progress(current_progress, desc="Initialize reoganizer model finished")
189
- timer.checkpoint(f"Initialize reoganizer model")
190
-
191
- timer.report()
192
-
193
- prepend_list = [tag.strip() for tag in additional_tags_prepend.split(",") if tag.strip()]
194
- append_list = [tag.strip() for tag in additional_tags_append.split(",") if tag.strip()]
195
- if prepend_list and append_list:
196
- append_list = [item for item in append_list if item not in prepend_list]
197
-
198
- # Dictionary to track counters for each filename
199
- name_counters = defaultdict(int)
200
-
201
- for idx, value in enumerate(gallery):
202
- try:
203
- image_path = value[0]
204
- image_name = os.path.splitext(os.path.basename(image_path))[0]
205
-
206
- # Increment the counter for the current name
207
- name_counters[image_name] += 1
208
-
209
- if name_counters[image_name] > 1:
210
- image_name = f"{image_name}_{name_counters[image_name]:02d}"
211
-
212
- image = self.prepare_image(image_path)
213
-
214
- input_name = self.model.get_inputs()[0].name
215
- label_name = self.model.get_outputs()[0].name
216
- print(f"Gallery {idx:02d}: Starting run wd model...")
217
- preds = self.model.run([label_name], {input_name: image})[0]
218
-
219
- labels = list(zip(self.tag_names, preds[0].astype(float)))
220
-
221
- # First 4 labels are actually ratings: pick one with argmax
222
- ratings_names = [labels[i] for i in self.rating_indexes]
223
- rating = dict(ratings_names)
224
-
225
- # Then we have general tags: pick any where prediction confidence > threshold
226
- general_names = [labels[i] for i in self.general_indexes]
227
-
228
- if general_mcut_enabled:
229
- general_probs = np.array([x[1] for x in general_names])
230
- general_thresh = mcut_threshold(general_probs)
231
-
232
- general_res = [x for x in general_names if x[1] > general_thresh]
233
- general_res = dict(general_res)
234
-
235
- # Everything else is characters: pick any where prediction confidence > threshold
236
- character_names = [labels[i] for i in self.character_indexes]
237
-
238
- if character_mcut_enabled:
239
- character_probs = np.array([x[1] for x in character_names])
240
- character_thresh = mcut_threshold(character_probs)
241
- character_thresh = max(0.15, character_thresh)
242
-
243
- character_res = [x for x in character_names if x[1] > character_thresh]
244
- character_res = dict(character_res)
245
- character_list = list(character_res.keys())
246
-
247
- sorted_general_list = sorted(
248
- general_res.items(),
249
- key=lambda x: x[1],
250
- reverse=True,
251
- )
252
- sorted_general_list = [x[0] for x in sorted_general_list]
253
- # Remove values from character_list that already exist in sorted_general_list
254
- character_list = [item for item in character_list if item not in sorted_general_list]
255
- # Remove values from sorted_general_list that already exist in prepend_list or append_list
256
- if prepend_list:
257
- sorted_general_list = [item for item in sorted_general_list if item not in prepend_list]
258
- if append_list:
259
- sorted_general_list = [item for item in sorted_general_list if item not in append_list]
260
-
261
- sorted_general_list = prepend_list + sorted_general_list + append_list
262
-
263
- sorted_general_strings = ", ".join((character_list if characters_merge_enabled else []) + sorted_general_list).replace("(", "\(").replace(")", "\)")
264
-
265
- classified_tags, unclassified_tags = classify_tags(sorted_general_list)
266
-
267
- # Create a single string of ALL categorized tags for the current image
268
- categorized_output_string = ', '.join([', '.join(tags) for tags in classified_tags.values()])
269
- categorized_output_strings.append(categorized_output_string)
270
- # Collect all categorized output strings into a single string
271
- final_categorized_output = ', '.join(categorized_output_strings)
272
-
273
- # Create a .txt file for "Output (string)" and "Categorized Output (string)"
274
- txt_content = f"Output (string): {sorted_general_strings}\nCategorized Output (string): {final_categorized_output}"
275
- txt_file = self.create_file(txt_content, output_dir, f"{image_name}_output.txt")
276
- txt_infos.append({"path": txt_file, "name": f"{image_name}_output.txt"})
277
-
278
- # Create a .json file for "Categorized (tags)"
279
- json_content = json.dumps(classified_tags, indent=4)
280
- json_file = self.create_file(json_content, output_dir, f"{image_name}_categorized_tags.json")
281
- txt_infos.append({"path": json_file, "name": f"{image_name}_categorized_tags.json"})
282
-
283
- # Save a copy of the uploaded image in PNG format
284
- image_path = value[0]
285
- image = Image.open(image_path)
286
- image.save(os.path.join(output_dir, f"{image_name}.png"), format="PNG")
287
- txt_infos.append({"path": os.path.join(output_dir, f"{image_name}.png"), "name": f"{image_name}.png"})
288
-
289
- current_progress += progressRatio/progressTotal;
290
- progress(current_progress, desc=f"image{idx:02d}, predict finished")
291
- timer.checkpoint(f"image{idx:02d}, predict finished")
292
-
293
- if reorganizer_model_repo:
294
- print(f"Starting reorganizer...")
295
- reorganize_strings = reorganizer.reorganize(sorted_general_strings)
296
- reorganize_strings = re.sub(r" *Title: *", "", reorganize_strings)
297
- reorganize_strings = re.sub(r"\n+", ",", reorganize_strings)
298
- reorganize_strings = re.sub(r",,+", ",", reorganize_strings)
299
- sorted_general_strings += ",\n\n" + reorganize_strings
300
-
301
- current_progress += progressRatio/progressTotal;
302
- progress(current_progress, desc=f"image{idx:02d}, reorganizer finished")
303
- timer.checkpoint(f"image{idx:02d}, reorganizer finished")
304
-
305
- txt_file = self.create_file(sorted_general_strings, output_dir, image_name + ".txt")
306
- txt_infos.append({"path":txt_file, "name": image_name + ".txt"})
307
-
308
- # Store the result in tag_results using image_path as the key
309
- tag_results[image_path] = {
310
- "strings": sorted_general_strings,
311
- "strings2": categorized_output_string, # Store the categorized output string here
312
- "classified_tags": classified_tags,
313
- "rating": rating,
314
- "character_res": character_res,
315
- "general_res": general_res,
316
- "unclassified_tags": unclassified_tags,
317
- "enhanced_tags": "" # Initialize as empty string
318
- }
319
-
320
- timer.report()
321
- except Exception as e:
322
- print(traceback.format_exc())
323
- print("Error predict: " + str(e))
324
- # Zip creation logic:
325
- download = []
326
- if txt_infos is not None and len(txt_infos) > 0:
327
- downloadZipPath = os.path.join(output_dir, "Multi-tagger-" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".zip")
328
- with zipfile.ZipFile(downloadZipPath, 'w', zipfile.ZIP_DEFLATED) as taggers_zip:
329
- for info in txt_infos:
330
- # Get file name from lookup
331
- taggers_zip.write(info["path"], arcname=info["name"])
332
- download.append(downloadZipPath)
333
- # End zip creation logic
334
- if reorganizer_model_repo:
335
- reorganizer.release_vram()
336
- del reorganizer
337
-
338
- progress(1, desc=f"Predict completed")
339
- timer.report_all() # Print all recorded times
340
- print("Predict is complete.")
341
-
342
- return download, sorted_general_strings, final_categorized_output, classified_tags, rating, character_res, general_res, unclassified_tags, tag_results
343
- def get_selection_from_gallery(gallery: list, tag_results: dict, selected_state: gr.SelectData):
344
- if not selected_state:
345
- return selected_state
346
- tag_result = {
347
- "strings": "",
348
- "strings2": "",
349
- "classified_tags": "{}",
350
- "rating": "",
351
- "character_res": "",
352
- "general_res": "",
353
- "unclassified_tags": "{}",
354
- "enhanced_tags": ""
355
- }
356
- if selected_state.value["image"]["path"] in tag_results:
357
- tag_result = tag_results[selected_state.value["image"]["path"]]
358
- return (selected_state.value["image"]["path"], selected_state.value["caption"]), tag_result["strings"], tag_result["strings2"], tag_result["classified_tags"], tag_result["rating"], tag_result["character_res"], tag_result["general_res"], tag_result["unclassified_tags"], tag_result["enhanced_tags"]
359
- def append_gallery(gallery:list,image:str):
360
- if gallery is None:gallery=[]
361
- if not image:return gallery,None
362
- gallery.append(image);return gallery,None
363
- def extend_gallery(gallery:list,images):
364
- if gallery is None:gallery=[]
365
- if not images:return gallery
366
- gallery.extend(images);return gallery
367
- def remove_image_from_gallery(gallery:list,selected_image:str):
368
- if not gallery or not selected_image:return gallery
369
- selected_image=ast.literal_eval(selected_image)
370
- if selected_image in gallery:gallery.remove(selected_image)
371
- return gallery
372
- args = parse_args()
373
- predictor = Predictor()
374
- dropdown_list = [
375
- EVA02_LARGE_MODEL_DSV3_REPO,
376
- SWINV2_MODEL_DSV3_REPO,
377
- CONV_MODEL_DSV3_REPO,
378
- VIT_MODEL_DSV3_REPO,
379
- VIT_LARGE_MODEL_DSV3_REPO,
380
- # ---
381
- MOAT_MODEL_DSV2_REPO,
382
- SWIN_MODEL_DSV2_REPO,
383
- CONV_MODEL_DSV2_REPO,
384
- CONV2_MODEL_DSV2_REPO,
385
- VIT_MODEL_DSV2_REPO,
386
- # ---
387
- SWINV2_MODEL_IS_DSV1_REPO,
388
- EVA02_LARGE_MODEL_IS_DSV1_REPO,
389
- ]
390
-
391
- def _restart_space():
392
- HF_TOKEN=os.getenv('HF_TOKEN')
393
- if not HF_TOKEN:raise ValueError('HF_TOKEN environment variable is not set.')
394
- huggingface_hub.HfApi().restart_space(repo_id='Werli/Multi-Tagger',token=HF_TOKEN,factory_reboot=False)
395
- scheduler=BackgroundScheduler()
396
- # Add a job to restart the space every 2 days (172800 seconds)
397
- restart_space_job = scheduler.add_job(_restart_space, "interval", seconds=172800)
398
- scheduler.start()
399
- next_run_time_utc=restart_space_job.next_run_time.astimezone(timezone.utc)
400
- NEXT_RESTART=f"Next Restart: {next_run_time_utc.strftime('%Y-%m-%d %H:%M:%S')} (UTC) - The space will restart every 2 days to ensure stability and performance. It uses a background scheduler to handle the restart process."
401
-
402
- css = """
403
- #output {height: 500px; overflow: auto; border: 1px solid #ccc;}
404
- label.float.svelte-i3tvor {position: relative !important;}
405
- .reduced-height.svelte-11chud3 {height: calc(80% - var(--size-10));}
406
- """
407
-
408
- with gr.Blocks(title=TITLE, css=css, theme="Werli/Multi-Tagger", fill_width=True) as demo:
409
- gr.Markdown(value=f"<h1 style='text-align: center; margin-bottom: 1rem'>{TITLE}</h1>")
410
- gr.Markdown(value=DESCRIPTION)
411
- gr.Markdown(NEXT_RESTART)
412
- with gr.Tab(label="Waifu Diffusion"):
413
- with gr.Row():
414
- with gr.Column():
415
- submit = gr.Button(value="Submit", variant="primary", size="lg")
416
- with gr.Column(variant="panel"):
417
- # Create an Image component for uploading images
418
- image_input = gr.Image(label="Upload an Image or clicking paste from clipboard button", type="filepath", sources=["upload", "clipboard"], height=150)
419
- with gr.Row():
420
- upload_button = gr.UploadButton("Upload multiple images", file_types=["image"], file_count="multiple", size="sm")
421
- remove_button = gr.Button("Remove Selected Image", size="sm")
422
- gallery = gr.Gallery(columns=5, rows=5, show_share_button=False, interactive=True, height="500px", label="Grid of images")
423
- model_repo = gr.Dropdown(
424
- dropdown_list,
425
- value=EVA02_LARGE_MODEL_DSV3_REPO,
426
- label="Model",
427
- )
428
- with gr.Row():
429
- general_thresh = gr.Slider(
430
- 0,
431
- 1,
432
- step=args.score_slider_step,
433
- value=args.score_general_threshold,
434
- label="General Tags Threshold",
435
- scale=3,
436
- )
437
- general_mcut_enabled = gr.Checkbox(
438
- value=False,
439
- label="Use MCut threshold",
440
- scale=1,
441
- )
442
- with gr.Row():
443
- character_thresh = gr.Slider(
444
- 0,
445
- 1,
446
- step=args.score_slider_step,
447
- value=args.score_character_threshold,
448
- label="Character Tags Threshold",
449
- scale=3,
450
- )
451
- character_mcut_enabled = gr.Checkbox(
452
- value=False,
453
- label="Use MCut threshold",
454
- scale=1,
455
- )
456
- with gr.Row():
457
- characters_merge_enabled = gr.Checkbox(
458
- value=True,
459
- label="Merge characters into the string output",
460
- scale=1,
461
- )
462
- with gr.Row():
463
- reorganizer_model_repo = gr.Dropdown(
464
- [None] + reorganizer_list,
465
- value=None,
466
- label="Reorganizer Model",
467
- info="Use a model to create a description for you",
468
- )
469
- with gr.Row():
470
- additional_tags_prepend = gr.Text(label="Prepend Additional tags (comma split)")
471
- additional_tags_append = gr.Text(label="Append Additional tags (comma split)")
472
- with gr.Row():
473
- clear = gr.ClearButton(
474
- components=[
475
- gallery,
476
- model_repo,
477
- general_thresh,
478
- general_mcut_enabled,
479
- character_thresh,
480
- character_mcut_enabled,
481
- characters_merge_enabled,
482
- reorganizer_model_repo,
483
- additional_tags_prepend,
484
- additional_tags_append,
485
- ],
486
- variant="secondary",
487
- size="lg",
488
- )
489
- with gr.Column(variant="panel"):
490
- download_file = gr.File(label="Download includes: All outputs* and image(s)") # 0
491
- character_res = gr.Label(label="Output (characters)") # 1
492
- sorted_general_strings = gr.Textbox(label="Output (string)*", show_label=True, show_copy_button=True) # 2
493
- final_categorized_output = gr.Textbox(label="Categorized (string)* - If it's too long, select an image to display tags correctly.", show_label=True, show_copy_button=True) # 3
494
- pe_generate_btn = gr.Button(value="ENHANCE TAGS", size="lg", variant="primary") # 4
495
- enhanced_tags = gr.Textbox(label="Enhanced Tags", show_label=True, show_copy_button=True) # 5
496
- prompt_enhancer_model = gr.Radio(["Medium", "Long", "Flux"], label="Model Choice", value="Medium", info="Enhance your prompts with Medium or Long answers") # 6
497
- categorized = gr.JSON(label="Categorized (tags)* - JSON") # 7
498
- rating = gr.Label(label="Rating") # 8
499
- general_res = gr.Label(label="Output (tags)") # 9
500
- unclassified = gr.JSON(label="Unclassified (tags)") # 10
501
- clear.add(
502
- [
503
- download_file,
504
- sorted_general_strings,
505
- final_categorized_output,
506
- categorized,
507
- rating,
508
- character_res,
509
- general_res,
510
- unclassified,
511
- prompt_enhancer_model,
512
- enhanced_tags,
513
- ]
514
- )
515
- tag_results = gr.State({})
516
- # Define the event listener to add the uploaded image to the gallery
517
- image_input.change(append_gallery, inputs=[gallery, image_input], outputs=[gallery, image_input])
518
- # When the upload button is clicked, add the new images to the gallery
519
- upload_button.upload(extend_gallery, inputs=[gallery, upload_button], outputs=gallery)
520
- # Event to update the selected image when an image is clicked in the gallery
521
- selected_image = gr.Textbox(label="Selected Image", visible=False)
522
- gallery.select(get_selection_from_gallery,inputs=[gallery, tag_results],outputs=[selected_image, sorted_general_strings, final_categorized_output, categorized, rating, character_res, general_res, unclassified, enhanced_tags])
523
- # Event to remove a selected image from the gallery
524
- remove_button.click(remove_image_from_gallery, inputs=[gallery, selected_image], outputs=gallery)
525
- # Event to for the Prompt Enhancer Button
526
- pe_generate_btn.click(lambda tags,model:prompt_enhancer('','',tags,model)[0],inputs=[final_categorized_output,prompt_enhancer_model],outputs=[enhanced_tags])
527
- submit.click(
528
- predictor.predict,
529
- inputs=[
530
- gallery,
531
- model_repo,
532
- general_thresh,
533
- general_mcut_enabled,
534
- character_thresh,
535
- character_mcut_enabled,
536
- characters_merge_enabled,
537
- reorganizer_model_repo,
538
- additional_tags_prepend,
539
- additional_tags_append,
540
- tag_results,
541
- ],
542
- outputs=[download_file, sorted_general_strings, final_categorized_output, categorized, rating, character_res, general_res, unclassified, tag_results,],
543
- )
544
- gr.Examples(
545
- [["images/1girl.png", VIT_LARGE_MODEL_DSV3_REPO, 0.35, False, 0.85, False]],
546
- inputs=[
547
- image_input,
548
- model_repo,
549
- general_thresh,
550
- general_mcut_enabled,
551
- character_thresh,
552
- character_mcut_enabled,
553
- ],
554
- )
555
- with gr.Tab(label="Tag Categorizer + Enhancer"):
556
- with gr.Row():
557
- with gr.Column(variant="panel"):
558
- input_tags = gr.Textbox(label="Input Tags (Danbooru comma-separated) - (Experimental)", placeholder="1girl, cat, horns, blue hair, ...")
559
- submit_button = gr.Button(value="Submit", variant="primary", size="lg")
560
- with gr.Column(variant="panel"):
561
- categorized_string = gr.Textbox(label="Categorized (string)", show_label=True, show_copy_button=True, lines=8)
562
- categorized_json = gr.JSON(label="Categorized (tags) - JSON")
563
- submit_button.click(process_tags, inputs=[input_tags], outputs=[categorized_string, categorized_json])
564
- with gr.Column(variant="panel"):
565
- pe_generate_btn = gr.Button(value="ENHANCE TAGS", size="lg", variant="primary")
566
- enhanced_tags = gr.Textbox(label="Enhanced Tags", show_label=True, show_copy_button=True)
567
- prompt_enhancer_model = gr.Radio(["Medium", "Long", "Flux"], label="Model Choice", value="Medium", info="Enhance your prompts with Medium or Long answers")
568
- pe_generate_btn.click(lambda tags,model:prompt_enhancer('','',tags,model)[0],inputs=[categorized_string,prompt_enhancer_model],outputs=[enhanced_tags])
569
- with gr.Tab(label="Florence 2 Image Captioning"):
570
- with gr.Row():
571
- with gr.Column(variant="panel"):
572
- input_img = gr.Image(label="Input Picture")
573
- task_type = gr.Radio(choices=['Single task', 'Cascaded task'], label='Task type selector', value='Single task')
574
- task_prompt = gr.Dropdown(choices=single_task_list, label="Task Prompt", value="Caption")
575
- task_type.change(fn=update_task_dropdown, inputs=task_type, outputs=task_prompt)
576
- text_input = gr.Textbox(label="Text Input (optional)")
577
- submit_btn = gr.Button(value="Submit")
578
- with gr.Column(variant="panel"):
579
- output_text = gr.Textbox(label="Output Text", show_label=True, show_copy_button=True, lines=8)
580
- output_img = gr.Image(label="Output Image")
581
- gr.Examples(
582
- examples=[
583
- ["images/image1.png", 'Object Detection'],
584
- ["images/image2.png", 'OCR with Region']
585
- ],
586
- inputs=[input_img, task_prompt],
587
- outputs=[output_text, output_img],
588
- fn=process_image,
589
- cache_examples=False,
590
- label='Try examples'
591
- )
592
- submit_btn.click(process_image, [input_img, task_prompt, text_input], [output_text, output_img])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  demo.queue(max_size=2).launch()
 
1
+ import os
2
+ import io,copy,requests,spaces,gradio as gr,numpy as np
3
+ from PIL import Image, ImageOps
4
+ import argparse,huggingface_hub,onnxruntime as rt,pandas as pd,traceback,tempfile,zipfile,re,ast,time
5
+ from datetime import datetime,timezone
6
+ from collections import defaultdict
7
+ from apscheduler.schedulers.background import BackgroundScheduler
8
+ import json
9
+ from modules.classifyTags import classify_tags,process_tags
10
+ from modules.florence2 import process_image,single_task_list,update_task_dropdown
11
+ from modules.reorganizer_model import reorganizer_list,reorganizer_class
12
+ from modules.tag_enhancer import prompt_enhancer
13
+ from modules.booru import gelbooru_gradio,fetch_gelbooru_images,on_select
14
+
15
+ os.environ['PYTORCH_ENABLE_MPS_FALLBACK']='1'
16
+
17
+ TITLE = "Multi-Tagger v1.2"
18
+ DESCRIPTION = """
19
+ Multi-Tagger is a versatile application for advanced image analysis and captioning. Perfect for AI artists or enthusiasts, it offers a range of features:
20
+
21
+ - **Automatic Tag Categorization**: Tags are grouped into categories.
22
+ - **Tag Enhancement**: Boost your prompts with enhanced descriptions using a built-in prompt enhancer.
23
+ - **Reorganizer**: Use a reorganizer model to format tags into a natural-language description.
24
+ - **Batch Support**: Upload and process multiple images simultaneously.
25
+ - **Downloadable Output**: Get almost all results as downloadable `.txt`, `.json`, and `.png` files in a `.zip` archive.
26
+ - **Image Fetcher**: Search for images from **Gelbooru** using flexible tag filters.
27
+ - CUDA or CPU support.
28
+
29
+ Example image by [me](https://huggingface.co/Werli).
30
+ """
31
+
32
+ # Dataset v3 series of models:
33
+ SWINV2_MODEL_DSV3_REPO = "SmilingWolf/wd-swinv2-tagger-v3"
34
+ CONV_MODEL_DSV3_REPO = "SmilingWolf/wd-convnext-tagger-v3"
35
+ VIT_MODEL_DSV3_REPO = "SmilingWolf/wd-vit-tagger-v3"
36
+ VIT_LARGE_MODEL_DSV3_REPO = "SmilingWolf/wd-vit-large-tagger-v3"
37
+ EVA02_LARGE_MODEL_DSV3_REPO = "SmilingWolf/wd-eva02-large-tagger-v3"
38
+ # Dataset v2 series of models:
39
+ MOAT_MODEL_DSV2_REPO = "SmilingWolf/wd-v1-4-moat-tagger-v2"
40
+ SWIN_MODEL_DSV2_REPO = "SmilingWolf/wd-v1-4-swinv2-tagger-v2"
41
+ CONV_MODEL_DSV2_REPO = "SmilingWolf/wd-v1-4-convnext-tagger-v2"
42
+ CONV2_MODEL_DSV2_REPO = "SmilingWolf/wd-v1-4-convnextv2-tagger-v2"
43
+ VIT_MODEL_DSV2_REPO = "SmilingWolf/wd-v1-4-vit-tagger-v2"
44
+ # IdolSankaku series of models:
45
+ EVA02_LARGE_MODEL_IS_DSV1_REPO = "deepghs/idolsankaku-eva02-large-tagger-v1"
46
+ SWINV2_MODEL_IS_DSV1_REPO = "deepghs/idolsankaku-swinv2-tagger-v1"
47
+ # Files to download from the repos
48
+ MODEL_FILENAME = "model.onnx"
49
+ LABEL_FILENAME = "selected_tags.csv"
50
+
51
+ kaomojis=['0_0','(o)_(o)','+_+','+_-','._.','<o>_<o>','<|>_<|>','=_=','>_<','3_3','6_9','>_o','@_@','^_^','o_o','u_u','x_x','|_|','||_||']
52
+ def parse_args()->argparse.Namespace:parser=argparse.ArgumentParser();parser.add_argument('--score-slider-step',type=float,default=.05);parser.add_argument('--score-general-threshold',type=float,default=.35);parser.add_argument('--score-character-threshold',type=float,default=.85);parser.add_argument('--share',action='store_true');return parser.parse_args()
53
+ def load_labels(dataframe)->list[str]:name_series=dataframe['name'];name_series=name_series.map(lambda x:x.replace('_',' ')if x not in kaomojis else x);tag_names=name_series.tolist();rating_indexes=list(np.where(dataframe['category']==9)[0]);general_indexes=list(np.where(dataframe['category']==0)[0]);character_indexes=list(np.where(dataframe['category']==4)[0]);return tag_names,rating_indexes,general_indexes,character_indexes
54
+ def mcut_threshold(probs):sorted_probs=probs[probs.argsort()[::-1]];difs=sorted_probs[:-1]-sorted_probs[1:];t=difs.argmax();thresh=(sorted_probs[t]+sorted_probs[t+1])/2;return thresh
55
+
56
+ class Timer:
57
+ def __init__(self):self.start_time=time.perf_counter();self.checkpoints=[('Start',self.start_time)]
58
+ def checkpoint(self,label='Checkpoint'):now=time.perf_counter();self.checkpoints.append((label,now))
59
+ def report(self,is_clear_checkpoints=True):
60
+ max_label_length=max(len(label)for(label,_)in self.checkpoints);prev_time=self.checkpoints[0][1]
61
+ for(label,curr_time)in self.checkpoints[1:]:elapsed=curr_time-prev_time;print(f"{label.ljust(max_label_length)}: {elapsed:.3f} seconds");prev_time=curr_time
62
+ if is_clear_checkpoints:self.checkpoints.clear();self.checkpoint()
63
+ def report_all(self):
64
+ print('\n> Execution Time Report:');max_label_length=max(len(label)for(label,_)in self.checkpoints)if len(self.checkpoints)>0 else 0;prev_time=self.start_time
65
+ for(label,curr_time)in self.checkpoints[1:]:elapsed=curr_time-prev_time;print(f"{label.ljust(max_label_length)}: {elapsed:.3f} seconds");prev_time=curr_time
66
+ total_time=self.checkpoints[-1][1]-self.start_time;print(f"{'Total Execution Time'.ljust(max_label_length)}: {total_time:.3f} seconds\n");self.checkpoints.clear()
67
+ def restart(self):self.start_time=time.perf_counter();self.checkpoints=[('Start',self.start_time)]
68
+ class Predictor:
69
+ def __init__(self):
70
+ self.model_target_size = None
71
+ self.last_loaded_repo = None
72
+ def download_model(self, model_repo):
73
+ csv_path = huggingface_hub.hf_hub_download(
74
+ model_repo,
75
+ LABEL_FILENAME,
76
+ )
77
+ model_path = huggingface_hub.hf_hub_download(
78
+ model_repo,
79
+ MODEL_FILENAME,
80
+ )
81
+ return csv_path, model_path
82
+ def load_model(self, model_repo):
83
+ if model_repo == self.last_loaded_repo:
84
+ return
85
+
86
+ csv_path, model_path = self.download_model(model_repo)
87
+
88
+ tags_df = pd.read_csv(csv_path)
89
+ sep_tags = load_labels(tags_df)
90
+
91
+ self.tag_names = sep_tags[0]
92
+ self.rating_indexes = sep_tags[1]
93
+ self.general_indexes = sep_tags[2]
94
+ self.character_indexes = sep_tags[3]
95
+
96
+ model = rt.InferenceSession(model_path)
97
+ _, height, width, _ = model.get_inputs()[0].shape
98
+ self.model_target_size = height
99
+
100
+ self.last_loaded_repo = model_repo
101
+ self.model = model
102
+ def prepare_image(self, path):
103
+ image = Image.open(path)
104
+ image = image.convert("RGBA")
105
+ target_size = self.model_target_size
106
+
107
+ canvas = Image.new("RGBA", image.size, (255, 255, 255))
108
+ canvas.alpha_composite(image)
109
+ image = canvas.convert("RGB")
110
+
111
+ # Pad image to square
112
+ image_shape = image.size
113
+ max_dim = max(image_shape)
114
+ pad_left = (max_dim - image_shape[0]) // 2
115
+ pad_top = (max_dim - image_shape[1]) // 2
116
+
117
+ padded_image = Image.new("RGB", (max_dim, max_dim), (255, 255, 255))
118
+ padded_image.paste(image, (pad_left, pad_top))
119
+
120
+ # Resize
121
+ if max_dim != target_size:
122
+ padded_image = padded_image.resize(
123
+ (target_size, target_size),
124
+ Image.BICUBIC,
125
+ )
126
+ # Convert to numpy array
127
+ image_array = np.asarray(padded_image, dtype=np.float32)
128
+ # Convert PIL-native RGB to BGR
129
+ image_array = image_array[:, :, ::-1]
130
+ return np.expand_dims(image_array, axis=0)
131
+
132
+ def create_file(self, content: str, directory: str, fileName: str) -> str:
133
+ # Write the content to a file
134
+ file_path = os.path.join(directory, fileName)
135
+ if fileName.endswith('.json'):
136
+ with open(file_path, 'w', encoding="utf-8") as file:
137
+ file.write(content)
138
+ else:
139
+ with open(file_path, 'w+', encoding="utf-8") as file:
140
+ file.write(content)
141
+
142
+ return file_path
143
+
144
+ def predict(
145
+ self,
146
+ gallery,
147
+ model_repo,
148
+ general_thresh,
149
+ general_mcut_enabled,
150
+ character_thresh,
151
+ character_mcut_enabled,
152
+ characters_merge_enabled,
153
+ reorganizer_model_repo,
154
+ additional_tags_prepend,
155
+ additional_tags_append,
156
+ tag_results,
157
+ progress=gr.Progress()
158
+ ):
159
+ # Clear tag_results before starting a new prediction
160
+ tag_results.clear()
161
+
162
+ gallery_len = len(gallery)
163
+ print(f"Predict load model: {model_repo}, gallery length: {gallery_len}")
164
+
165
+ timer = Timer() # Create a timer
166
+ progressRatio = 0.5 if reorganizer_model_repo else 1
167
+ progressTotal = gallery_len + 1
168
+ current_progress = 0
169
+
170
+ self.load_model(model_repo)
171
+ current_progress += progressRatio/progressTotal;
172
+ progress(current_progress, desc="Initialize wd model finished")
173
+ timer.checkpoint(f"Initialize wd model")
174
+
175
+ txt_infos = []
176
+ output_dir = tempfile.mkdtemp()
177
+ if not os.path.exists(output_dir):
178
+ os.makedirs(output_dir)
179
+
180
+ sorted_general_strings = ""
181
+ # Create categorized output string
182
+ categorized_output_strings = []
183
+ rating = None
184
+ character_res = None
185
+ general_res = None
186
+
187
+ if reorganizer_model_repo:
188
+ print(f"Reorganizer load model {reorganizer_model_repo}")
189
+ reorganizer = reorganizer_class(reorganizer_model_repo, loadModel=True)
190
+ current_progress += progressRatio/progressTotal;
191
+ progress(current_progress, desc="Initialize reoganizer model finished")
192
+ timer.checkpoint(f"Initialize reoganizer model")
193
+
194
+ timer.report()
195
+
196
+ prepend_list = [tag.strip() for tag in additional_tags_prepend.split(",") if tag.strip()]
197
+ append_list = [tag.strip() for tag in additional_tags_append.split(",") if tag.strip()]
198
+ if prepend_list and append_list:
199
+ append_list = [item for item in append_list if item not in prepend_list]
200
+
201
+ # Dictionary to track counters for each filename
202
+ name_counters = defaultdict(int)
203
+
204
+ for idx, value in enumerate(gallery):
205
+ try:
206
+ image_path = value[0]
207
+ image_name = os.path.splitext(os.path.basename(image_path))[0]
208
+
209
+ # Increment the counter for the current name
210
+ name_counters[image_name] += 1
211
+
212
+ if name_counters[image_name] > 1:
213
+ image_name = f"{image_name}_{name_counters[image_name]:02d}"
214
+
215
+ image = self.prepare_image(image_path)
216
+
217
+ input_name = self.model.get_inputs()[0].name
218
+ label_name = self.model.get_outputs()[0].name
219
+ print(f"Gallery {idx:02d}: Starting run wd model...")
220
+ preds = self.model.run([label_name], {input_name: image})[0]
221
+
222
+ labels = list(zip(self.tag_names, preds[0].astype(float)))
223
+
224
+ # First 4 labels are actually ratings: pick one with argmax
225
+ ratings_names = [labels[i] for i in self.rating_indexes]
226
+ rating = dict(ratings_names)
227
+
228
+ # Then we have general tags: pick any where prediction confidence > threshold
229
+ general_names = [labels[i] for i in self.general_indexes]
230
+
231
+ if general_mcut_enabled:
232
+ general_probs = np.array([x[1] for x in general_names])
233
+ general_thresh = mcut_threshold(general_probs)
234
+
235
+ general_res = [x for x in general_names if x[1] > general_thresh]
236
+ general_res = dict(general_res)
237
+
238
+ # Everything else is characters: pick any where prediction confidence > threshold
239
+ character_names = [labels[i] for i in self.character_indexes]
240
+
241
+ if character_mcut_enabled:
242
+ character_probs = np.array([x[1] for x in character_names])
243
+ character_thresh = mcut_threshold(character_probs)
244
+ character_thresh = max(0.15, character_thresh)
245
+
246
+ character_res = [x for x in character_names if x[1] > character_thresh]
247
+ character_res = dict(character_res)
248
+ character_list = list(character_res.keys())
249
+
250
+ sorted_general_list = sorted(
251
+ general_res.items(),
252
+ key=lambda x: x[1],
253
+ reverse=True,
254
+ )
255
+ sorted_general_list = [x[0] for x in sorted_general_list]
256
+ # Remove values from character_list that already exist in sorted_general_list
257
+ character_list = [item for item in character_list if item not in sorted_general_list]
258
+ # Remove values from sorted_general_list that already exist in prepend_list or append_list
259
+ if prepend_list:
260
+ sorted_general_list = [item for item in sorted_general_list if item not in prepend_list]
261
+ if append_list:
262
+ sorted_general_list = [item for item in sorted_general_list if item not in append_list]
263
+
264
+ sorted_general_list = prepend_list + sorted_general_list + append_list
265
+
266
+ sorted_general_strings = ", ".join((character_list if characters_merge_enabled else []) + sorted_general_list).replace("(", "\(").replace(")", "\)")
267
+
268
+ classified_tags, unclassified_tags = classify_tags(sorted_general_list)
269
+
270
+ # Create a single string of ALL categorized tags for the current image
271
+ categorized_output_string = ', '.join([', '.join(tags) for tags in classified_tags.values()])
272
+ categorized_output_strings.append(categorized_output_string)
273
+ # Collect all categorized output strings into a single string
274
+ final_categorized_output = ', '.join(categorized_output_strings)
275
+
276
+ # Create a .txt file for "Output (string)" and "Categorized Output (string)"
277
+ txt_content = f"Output (string): {sorted_general_strings}\nCategorized Output (string): {final_categorized_output}"
278
+ txt_file = self.create_file(txt_content, output_dir, f"{image_name}_output.txt")
279
+ txt_infos.append({"path": txt_file, "name": f"{image_name}_output.txt"})
280
+
281
+ # Create a .json file for "Categorized (tags)"
282
+ json_content = json.dumps(classified_tags, indent=4)
283
+ json_file = self.create_file(json_content, output_dir, f"{image_name}_categorized_tags.json")
284
+ txt_infos.append({"path": json_file, "name": f"{image_name}_categorized_tags.json"})
285
+
286
+ # Save a copy of the uploaded image in PNG format
287
+ image_path = value[0]
288
+ image = Image.open(image_path)
289
+ image.save(os.path.join(output_dir, f"{image_name}.png"), format="PNG")
290
+ txt_infos.append({"path": os.path.join(output_dir, f"{image_name}.png"), "name": f"{image_name}.png"})
291
+
292
+ current_progress += progressRatio/progressTotal;
293
+ progress(current_progress, desc=f"image{idx:02d}, predict finished")
294
+ timer.checkpoint(f"image{idx:02d}, predict finished")
295
+
296
+ if reorganizer_model_repo:
297
+ print(f"Starting reorganizer...")
298
+ reorganize_strings = reorganizer.reorganize(sorted_general_strings)
299
+ reorganize_strings = re.sub(r" *Title: *", "", reorganize_strings)
300
+ reorganize_strings = re.sub(r"\n+", ",", reorganize_strings)
301
+ reorganize_strings = re.sub(r",,+", ",", reorganize_strings)
302
+ sorted_general_strings += ",\n\n" + reorganize_strings
303
+
304
+ current_progress += progressRatio/progressTotal;
305
+ progress(current_progress, desc=f"image{idx:02d}, reorganizer finished")
306
+ timer.checkpoint(f"image{idx:02d}, reorganizer finished")
307
+
308
+ txt_file = self.create_file(sorted_general_strings, output_dir, image_name + ".txt")
309
+ txt_infos.append({"path":txt_file, "name": image_name + ".txt"})
310
+
311
+ # Store the result in tag_results using image_path as the key
312
+ tag_results[image_path] = {
313
+ "strings": sorted_general_strings,
314
+ "strings2": categorized_output_string, # Store the categorized output string here
315
+ "classified_tags": classified_tags,
316
+ "rating": rating,
317
+ "character_res": character_res,
318
+ "general_res": general_res,
319
+ "unclassified_tags": unclassified_tags,
320
+ "enhanced_tags": "" # Initialize as empty string
321
+ }
322
+
323
+ timer.report()
324
+ except Exception as e:
325
+ print(traceback.format_exc())
326
+ print("Error predict: " + str(e))
327
+ # Zip creation logic:
328
+ download = []
329
+ if txt_infos is not None and len(txt_infos) > 0:
330
+ downloadZipPath = os.path.join(output_dir, "Multi-tagger-" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".zip")
331
+ with zipfile.ZipFile(downloadZipPath, 'w', zipfile.ZIP_DEFLATED) as taggers_zip:
332
+ for info in txt_infos:
333
+ # Get file name from lookup
334
+ taggers_zip.write(info["path"], arcname=info["name"])
335
+ download.append(downloadZipPath)
336
+ # End zip creation logic
337
+ if reorganizer_model_repo:
338
+ reorganizer.release_vram()
339
+ del reorganizer
340
+
341
+ progress(1, desc=f"Predict completed")
342
+ timer.report_all() # Print all recorded times
343
+ print("Predict is complete.")
344
+
345
+ return download, sorted_general_strings, final_categorized_output, classified_tags, rating, character_res, general_res, unclassified_tags, tag_results
346
+ def get_selection_from_gallery(gallery: list, tag_results: dict, selected_state: gr.SelectData):
347
+ if not selected_state:
348
+ return selected_state
349
+ tag_result = {
350
+ "strings": "",
351
+ "strings2": "",
352
+ "classified_tags": "{}",
353
+ "rating": "",
354
+ "character_res": "",
355
+ "general_res": "",
356
+ "unclassified_tags": "{}",
357
+ "enhanced_tags": ""
358
+ }
359
+ if selected_state.value["image"]["path"] in tag_results:
360
+ tag_result = tag_results[selected_state.value["image"]["path"]]
361
+ return (selected_state.value["image"]["path"], selected_state.value["caption"]), tag_result["strings"], tag_result["strings2"], tag_result["classified_tags"], tag_result["rating"], tag_result["character_res"], tag_result["general_res"], tag_result["unclassified_tags"], tag_result["enhanced_tags"]
362
+ def append_gallery(gallery:list,image:str):
363
+ if gallery is None:gallery=[]
364
+ if not image:return gallery,None
365
+ gallery.append(image);return gallery,None
366
+ def extend_gallery(gallery:list,images):
367
+ if gallery is None:gallery=[]
368
+ if not images:return gallery
369
+ gallery.extend(images);return gallery
370
+ def remove_image_from_gallery(gallery:list,selected_image:str):
371
+ if not gallery or not selected_image:return gallery
372
+ selected_image=ast.literal_eval(selected_image)
373
+ if selected_image in gallery:gallery.remove(selected_image)
374
+ return gallery
375
+ args = parse_args()
376
+ predictor = Predictor()
377
+ dropdown_list = [
378
+ EVA02_LARGE_MODEL_DSV3_REPO,
379
+ SWINV2_MODEL_DSV3_REPO,
380
+ CONV_MODEL_DSV3_REPO,
381
+ VIT_MODEL_DSV3_REPO,
382
+ VIT_LARGE_MODEL_DSV3_REPO,
383
+ # ---
384
+ MOAT_MODEL_DSV2_REPO,
385
+ SWIN_MODEL_DSV2_REPO,
386
+ CONV_MODEL_DSV2_REPO,
387
+ CONV2_MODEL_DSV2_REPO,
388
+ VIT_MODEL_DSV2_REPO,
389
+ # ---
390
+ SWINV2_MODEL_IS_DSV1_REPO,
391
+ EVA02_LARGE_MODEL_IS_DSV1_REPO,
392
+ ]
393
+
394
+ def _restart_space():
395
+ HF_TOKEN=os.getenv('HF_TOKEN')
396
+ if not HF_TOKEN:raise ValueError('HF_TOKEN environment variable is not set.')
397
+ huggingface_hub.HfApi().restart_space(repo_id='Werli/Multi-Tagger',token=HF_TOKEN,factory_reboot=False)
398
+ scheduler=BackgroundScheduler()
399
+ # Add a job to restart the space every 2 days (172800 seconds)
400
+ restart_space_job = scheduler.add_job(_restart_space, "interval", seconds=172800)
401
+ scheduler.start()
402
+ next_run_time_utc=restart_space_job.next_run_time.astimezone(timezone.utc)
403
+ NEXT_RESTART=f"Next Restart: {next_run_time_utc.strftime('%Y-%m-%d %H:%M:%S')} (UTC) - The space will restart every 2 days to ensure stability and performance. It uses a background scheduler to handle the restart process."
404
+
405
+ css = """
406
+ #output {height: 500px; overflow: auto; border: 1px solid #ccc;}
407
+ label.float.svelte-i3tvor {position: relative !important;}
408
+ .reduced-height.svelte-11chud3 {height: calc(80% - var(--size-10));}
409
+ """
410
+
411
+ with gr.Blocks(title=TITLE, css=css, theme="Werli/Multi-Tagger", fill_width=True) as demo:
412
+ gr.Markdown(value=f"<h1 style='text-align: center; margin-bottom: 1rem'>{TITLE}</h1>")
413
+ gr.Markdown(value=DESCRIPTION)
414
+ gr.Markdown(NEXT_RESTART)
415
+ with gr.Tab(label="Waifu Diffusion"):
416
+ with gr.Row():
417
+ with gr.Column():
418
+ submit = gr.Button(value="Submit", variant="primary", size="lg")
419
+ with gr.Column(variant="panel"):
420
+ # Create an Image component for uploading images
421
+ image_input = gr.Image(label="Upload an Image or clicking paste from clipboard button", type="filepath", sources=["upload", "clipboard"], height=150)
422
+ with gr.Row():
423
+ upload_button = gr.UploadButton("Upload multiple images", file_types=["image"], file_count="multiple", size="sm")
424
+ remove_button = gr.Button("Remove Selected Image", size="sm")
425
+ gallery = gr.Gallery(columns=5, rows=5, show_share_button=False, interactive=True, height="500px", label="Grid of images")
426
+ model_repo = gr.Dropdown(
427
+ dropdown_list,
428
+ value=EVA02_LARGE_MODEL_DSV3_REPO,
429
+ label="Model",
430
+ )
431
+ with gr.Row():
432
+ general_thresh = gr.Slider(
433
+ 0,
434
+ 1,
435
+ step=args.score_slider_step,
436
+ value=args.score_general_threshold,
437
+ label="General Tags Threshold",
438
+ scale=3,
439
+ )
440
+ general_mcut_enabled = gr.Checkbox(
441
+ value=False,
442
+ label="Use MCut threshold",
443
+ scale=1,
444
+ )
445
+ with gr.Row():
446
+ character_thresh = gr.Slider(
447
+ 0,
448
+ 1,
449
+ step=args.score_slider_step,
450
+ value=args.score_character_threshold,
451
+ label="Character Tags Threshold",
452
+ scale=3,
453
+ )
454
+ character_mcut_enabled = gr.Checkbox(
455
+ value=False,
456
+ label="Use MCut threshold",
457
+ scale=1,
458
+ )
459
+ with gr.Row():
460
+ characters_merge_enabled = gr.Checkbox(
461
+ value=True,
462
+ label="Merge characters into the string output",
463
+ scale=1,
464
+ )
465
+ with gr.Row():
466
+ reorganizer_model_repo = gr.Dropdown(
467
+ [None] + reorganizer_list,
468
+ value=None,
469
+ label="Reorganizer Model",
470
+ info="Use a model to create a description for you",
471
+ )
472
+ with gr.Row():
473
+ additional_tags_prepend = gr.Text(label="Prepend Additional tags (comma split)")
474
+ additional_tags_append = gr.Text(label="Append Additional tags (comma split)")
475
+ with gr.Row():
476
+ clear = gr.ClearButton(
477
+ components=[
478
+ gallery,
479
+ model_repo,
480
+ general_thresh,
481
+ general_mcut_enabled,
482
+ character_thresh,
483
+ character_mcut_enabled,
484
+ characters_merge_enabled,
485
+ reorganizer_model_repo,
486
+ additional_tags_prepend,
487
+ additional_tags_append,
488
+ ],
489
+ variant="secondary",
490
+ size="lg",
491
+ )
492
+ with gr.Column(variant="panel"):
493
+ download_file = gr.File(label="Download includes: All outputs* and image(s)") # 0
494
+ character_res = gr.Label(label="Output (characters)") # 1
495
+ sorted_general_strings = gr.Textbox(label="Output (string)*", show_label=True, show_copy_button=True) # 2
496
+ final_categorized_output = gr.Textbox(label="Categorized (string)* - If it's too long, select an image to display tags correctly.", show_label=True, show_copy_button=True) # 3
497
+ pe_generate_btn = gr.Button(value="ENHANCE TAGS", size="lg", variant="primary") # 4
498
+ enhanced_tags = gr.Textbox(label="Enhanced Tags", show_label=True, show_copy_button=True) # 5
499
+ prompt_enhancer_model = gr.Radio(["Medium", "Long", "Flux"], label="Model Choice", value="Medium", info="Enhance your prompts with Medium or Long answers") # 6
500
+ categorized = gr.JSON(label="Categorized (tags)* - JSON") # 7
501
+ rating = gr.Label(label="Rating") # 8
502
+ general_res = gr.Label(label="Output (tags)") # 9
503
+ unclassified = gr.JSON(label="Unclassified (tags)") # 10
504
+ clear.add(
505
+ [
506
+ download_file,
507
+ sorted_general_strings,
508
+ final_categorized_output,
509
+ categorized,
510
+ rating,
511
+ character_res,
512
+ general_res,
513
+ unclassified,
514
+ prompt_enhancer_model,
515
+ enhanced_tags,
516
+ ]
517
+ )
518
+ tag_results = gr.State({})
519
+ # Define the event listener to add the uploaded image to the gallery
520
+ image_input.change(append_gallery, inputs=[gallery, image_input], outputs=[gallery, image_input])
521
+ # When the upload button is clicked, add the new images to the gallery
522
+ upload_button.upload(extend_gallery, inputs=[gallery, upload_button], outputs=gallery)
523
+ # Event to update the selected image when an image is clicked in the gallery
524
+ selected_image = gr.Textbox(label="Selected Image", visible=False)
525
+ gallery.select(get_selection_from_gallery,inputs=[gallery, tag_results],outputs=[selected_image, sorted_general_strings, final_categorized_output, categorized, rating, character_res, general_res, unclassified, enhanced_tags])
526
+ # Event to remove a selected image from the gallery
527
+ remove_button.click(remove_image_from_gallery, inputs=[gallery, selected_image], outputs=gallery)
528
+ # Event to for the Prompt Enhancer Button
529
+ pe_generate_btn.click(lambda tags,model:prompt_enhancer('','',tags,model)[0],inputs=[final_categorized_output,prompt_enhancer_model],outputs=[enhanced_tags])
530
+ submit.click(
531
+ predictor.predict,
532
+ inputs=[
533
+ gallery,
534
+ model_repo,
535
+ general_thresh,
536
+ general_mcut_enabled,
537
+ character_thresh,
538
+ character_mcut_enabled,
539
+ characters_merge_enabled,
540
+ reorganizer_model_repo,
541
+ additional_tags_prepend,
542
+ additional_tags_append,
543
+ tag_results,
544
+ ],
545
+ outputs=[download_file, sorted_general_strings, final_categorized_output, categorized, rating, character_res, general_res, unclassified, tag_results,],
546
+ )
547
+ gr.Examples(
548
+ [["images/1girl.png", VIT_LARGE_MODEL_DSV3_REPO, 0.35, False, 0.85, False]],
549
+ inputs=[
550
+ image_input,
551
+ model_repo,
552
+ general_thresh,
553
+ general_mcut_enabled,
554
+ character_thresh,
555
+ character_mcut_enabled,
556
+ ],
557
+ )
558
+ with gr.Tab(label="Florence 2 Image Captioning"):
559
+ with gr.Row():
560
+ with gr.Column(variant="panel"):
561
+ input_img = gr.Image(label="Input Picture")
562
+ task_type = gr.Radio(choices=['Single task', 'Cascaded task'], label='Task type selector', value='Single task')
563
+ task_prompt = gr.Dropdown(choices=single_task_list, label="Task Prompt", value="Caption")
564
+ task_type.change(fn=update_task_dropdown, inputs=task_type, outputs=task_prompt)
565
+ text_input = gr.Textbox(label="Text Input (optional)")
566
+ submit_btn = gr.Button(value="Submit")
567
+ with gr.Column(variant="panel"):
568
+ output_text = gr.Textbox(label="Output Text", show_label=True, show_copy_button=True, lines=8)
569
+ output_img = gr.Image(label="Output Image")
570
+ gr.Examples(
571
+ examples=[
572
+ ["images/image1.png", 'Object Detection'],
573
+ ["images/image2.png", 'OCR with Region']
574
+ ],
575
+ inputs=[input_img, task_prompt],
576
+ outputs=[output_text, output_img],
577
+ fn=process_image,
578
+ cache_examples=False,
579
+ label='Try examples'
580
+ )
581
+ submit_btn.click(process_image, [input_img, task_prompt, text_input], [output_text, output_img])
582
+ with gr.Tab(label="Gelbooru Image Fetcher"):
583
+ with gr.Row():
584
+ with gr.Column():
585
+ gr.Markdown("### ⚙️ Search Parameters")
586
+ site = gr.Dropdown(label="Select Source", choices=["Gelbooru", "None (will not work)"], value="Gelbooru")
587
+ OR_tags = gr.Textbox(label="OR Tags (comma-separated)", placeholder="e.g. solo, 1girl, 1boy, artist, character, ...")
588
+ AND_tags = gr.Textbox(label="AND Tags (comma-separated)", placeholder="e.g. black hair, cat ears, black hair, granblue fantasy, ...")
589
+ exclude_tags = gr.Textbox(label="Exclude Tags (comma-separated)", placeholder="e.g. animated, watermark, username, ...")
590
+ score = gr.Number(label="Minimum Score", value=0)
591
+ count = gr.Slider(label="Number of Images", minimum=1, maximum=4, step=1, value=1) # Increase if necessary (not recommend)
592
+ Safe = gr.Checkbox(label="Include Safe", value=True)
593
+ Questionable = gr.Checkbox(label="Include Questionable", value=True)
594
+ Explicit = gr.Checkbox(label="Include Explicit", value=False)
595
+ #user_id = gr.Textbox(label="User ID (Optional)", value="")
596
+ #api_key = gr.Textbox(label="API Key (Optional)", value="", type="password")
597
+
598
+ submit_btn = gr.Button("Fetch Images", variant="primary")
599
+
600
+ with gr.Column():
601
+ gr.Markdown("### 📄 Results")
602
+ images_output = gr.Gallery(label="Images", columns=3, rows=2, object_fit="contain", height=500)
603
+ tags_output = gr.Textbox(label="Tags", placeholder="Select an image to show tags", lines=5, show_copy_button=True)
604
+ post_url_output = gr.Textbox(label="Post URL", lines=1, show_copy_button=True)
605
+ image_url_output = gr.Textbox(label="Image URL", lines=1, show_copy_button=True)
606
+
607
+ # State to store tags, URLs
608
+ tags_state = gr.State([])
609
+ post_url_state = gr.State([])
610
+ image_url_state = gr.State([])
611
+
612
+ submit_btn.click(
613
+ fn=gelbooru_gradio,
614
+ inputs=[OR_tags, AND_tags, exclude_tags, score, count, Safe, Questionable, Explicit, site], # add 'api_key' and 'user_id' if necessary
615
+ outputs=[images_output, tags_state, post_url_state, image_url_state],
616
+ )
617
+
618
+ images_output.select(
619
+ fn=on_select,
620
+ inputs=[tags_state, post_url_state, image_url_state],
621
+ outputs=[tags_output, post_url_output, image_url_output],
622
+ )
623
+ gr.Markdown("""
624
+ ---
625
+ ComfyUI version: [Comfyui-Gelbooru](https://github.com/1mckw/Comfyui-Gelbooru)
626
+ """)
627
+ with gr.Tab(label="Categorizer++"):
628
+ with gr.Row():
629
+ with gr.Column(variant="panel"):
630
+ input_tags = gr.Textbox(label="Input Tags", placeholder="1girl, cat, horns, blue hair, ...\nor\n? 1girl 1234567? cat 1234567? horns 1234567? blue hair 1234567? ...", lines=4)
631
+ submit_button = gr.Button(value="Submit", variant="primary", size="lg")
632
+ with gr.Column(variant="panel"):
633
+ categorized_string = gr.Textbox(label="Categorized (string)", show_label=True, show_copy_button=True, lines=8)
634
+ categorized_json = gr.JSON(label="Categorized (tags) - JSON")
635
+ submit_button.click(process_tags, inputs=[input_tags], outputs=[categorized_string, categorized_json])
636
+ with gr.Column(variant="panel"):
637
+ pe_generate_btn = gr.Button(value="ENHANCE TAGS", size="lg", variant="primary")
638
+ enhanced_tags = gr.Textbox(label="Enhanced Tags", show_label=True, show_copy_button=True)
639
+ prompt_enhancer_model = gr.Radio(["Medium", "Long", "Flux"], label="Model Choice", value="Medium", info="Enhance your prompts with Medium or Long answers")
640
+ pe_generate_btn.click(lambda tags,model:prompt_enhancer('','',tags,model)[0],inputs=[categorized_string,prompt_enhancer_model],outputs=[enhanced_tags])
641
  demo.queue(max_size=2).launch()
modules/booru.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import re
3
+ import base64
4
+ import io
5
+ import numpy as np
6
+ from PIL import Image, ImageOps
7
+ import torch
8
+ import gradio as gr
9
+
10
+ # Helper to load image from URL
11
+ def loadImageFromUrl(url):
12
+ if url.startswith("data:image/"):
13
+ i = Image.open(io.BytesIO(base64.b64decode(url.split(",")[1])))
14
+ elif url.startswith("s3://"):
15
+ raise Exception("S3 URLs not supported in this interface")
16
+ else:
17
+ response = requests.get(url, timeout=5)
18
+ if response.status_code != 200:
19
+ raise Exception(response.text)
20
+ i = Image.open(io.BytesIO(response.content))
21
+
22
+ i = ImageOps.exif_transpose(i)
23
+ if i.mode != "RGBA":
24
+ i = i.convert("RGBA")
25
+
26
+ alpha = i.split()[-1]
27
+ image = Image.new("RGB", i.size, (0, 0, 0))
28
+ image.paste(i, mask=alpha)
29
+
30
+ image = np.array(image).astype(np.float32) / 255.0
31
+ image = torch.from_numpy(image)[None,]
32
+ return image
33
+
34
+ # Fetch data from Gelbooru or None
35
+ def fetch_gelbooru_images(site, OR_tags, AND_tags, exclude_tag, score, count, Safe, Questionable, Explicit): # add 'api_key' and 'user_id' if necessary
36
+ # AND_tags
37
+ AND_tags = AND_tags.rstrip(',').rstrip(' ')
38
+ AND_tags = AND_tags.split(',')
39
+ AND_tags = [item.strip().replace(' ', '_').replace('\\', '') for item in AND_tags]
40
+ AND_tags = [item for item in AND_tags if item]
41
+ if len(AND_tags) > 1:
42
+ AND_tags = '+'.join(AND_tags)
43
+ else:
44
+ AND_tags = AND_tags[0] if AND_tags else ''
45
+
46
+ # OR_tags
47
+ OR_tags = OR_tags.rstrip(',').rstrip(' ')
48
+ OR_tags = OR_tags.split(',')
49
+ OR_tags = [item.strip().replace(' ', '_').replace('\\', '') for item in OR_tags]
50
+ OR_tags = [item for item in OR_tags if item]
51
+ if len(OR_tags) > 1:
52
+ OR_tags = '{' + ' ~ '.join(OR_tags) + '}'
53
+ else:
54
+ OR_tags = OR_tags[0] if OR_tags else ''
55
+
56
+ # Exclude tags
57
+ exclude_tag = '+'.join('-' + item.strip().replace(' ', '_') for item in exclude_tag.split(','))
58
+
59
+ rate_exclusion = ""
60
+ if not Safe:
61
+ if site == "None":
62
+ rate_exclusion += "+-rating%3asafe"
63
+ else:
64
+ rate_exclusion += "+-rating%3ageneral"
65
+ if not Questionable:
66
+ if site == "None":
67
+ rate_exclusion += "+-rating%3aquestionable"
68
+ else:
69
+ rate_exclusion += "+-rating%3aquestionable+-rating%3aSensitive"
70
+ if not Explicit:
71
+ if site == "None":
72
+ rate_exclusion += "+-rating%3aexplicit"
73
+ else:
74
+ rate_exclusion += "+-rating%3aexplicit"
75
+
76
+ if site == "None":
77
+ base_url = "https://api.example.com/index.php"
78
+ else:
79
+ base_url = "https://gelbooru.com/index.php"
80
+
81
+ query_params = (
82
+ f"page=dapi&s=post&q=index&tags=sort%3arandom+"
83
+ f"{exclude_tag}+{OR_tags}+{AND_tags}+{rate_exclusion}"
84
+ f"+score%3a>{score}&limit={count}&json=1"
85
+ #f"+score%3a>{score}&api_key={api_key}&user_id={user_id}&limit={count}&json=1"
86
+ )
87
+ url = f"{base_url}?{query_params}".replace("-+", "")
88
+ url = re.sub(r"\++", "+", url)
89
+
90
+ response = requests.get(url, verify=True)
91
+ if site == "None":
92
+ posts = response.json()
93
+ else:
94
+ posts = response.json().get('post', [])
95
+
96
+ image_urls = [post.get("file_url", "") for post in posts]
97
+ tags_list = [post.get("tags", "").replace(" ", ", ").replace("_", " ").replace("(", "\\(").replace(")", "\\)").strip() for post in posts]
98
+ #tags_list = [post.get("tags", "").replace("_", " ").replace(" ", ", ").strip() for post in posts]
99
+ ids_list = [str(post.get("id", "")) for post in posts]
100
+
101
+ if site == "Gelbooru":
102
+ post_urls = [f"https://gelbooru.com/index.php?page=post&s=view&id={id}" for id in ids_list]
103
+ #else:
104
+ # post_urls = [f"https://api.none.com/index.php?page=post&s=view&id={id}" for id in ids_list]
105
+
106
+ return image_urls, tags_list, post_urls
107
+
108
+ # Main function to fetch and return processed images
109
+ def gelbooru_gradio(
110
+ OR_tags, AND_tags, exclude_tags, score, count, Safe, Questionable, Explicit, site # add 'api_key' and 'user_id' if necessary
111
+ ):
112
+ image_urls, tags_list, post_urls = fetch_gelbooru_images(
113
+ site, OR_tags, AND_tags, exclude_tags, score, count, Safe, Questionable, Explicit # 'api_key' and 'user_id' if necessary
114
+ )
115
+
116
+ if not image_urls:
117
+ return [], [], [], []
118
+
119
+ image_data = []
120
+ for url in image_urls:
121
+ try:
122
+ image = loadImageFromUrl(url)
123
+ image = (image * 255).clamp(0, 255).cpu().numpy().astype(np.uint8)[0]
124
+ image = Image.fromarray(image)
125
+ image_data.append(image)
126
+ except Exception as e:
127
+ print(f"Error loading image from {url}: {e}")
128
+ continue
129
+
130
+ return image_data, tags_list, post_urls, image_urls
131
+
132
+ # Update UI on image click
133
+ def on_select(evt: gr.SelectData, tags_list, post_url_list, image_url_list):
134
+ idx = evt.index
135
+ if idx < len(tags_list):
136
+ return tags_list[idx], post_url_list[idx], image_url_list[idx]
137
+ return "No tags", "", ""
modules/classifyTags.py CHANGED
@@ -1,174 +1,191 @@
1
- from collections import defaultdict
2
-
3
- # Define grouping rules (categories and keywords)
4
- # Provided categories and reversed_categories
5
- categories = {
6
- "Explicit" : ["sex", "69", "paizuri", "cum", "precum", "areola_slip", "hetero", "erection", "oral", "fellatio", "yaoi", "ejaculation", "ejaculating", "masturbation", "handjob", "bulge", "rape", "_rape", "doggystyle", "threesome", "missionary", "object_insertion", "nipple", "nipples", "pussy", "anus", "penis", "groin", "testicles", "testicle", "anal", "cameltoe", "areolae", "dildo", "clitoris", "top-down_bottom-up", "gag", "groping", "gagged", "gangbang", "orgasm", "femdom", "incest", "bukkake", "breast_out", "vaginal", "vagina", "public_indecency", "breast_sucking", "folded", "cunnilingus", "_cunnilingus", "foreskin", "bestiality", "footjob", "uterus", "womb", "flaccid", "defloration", "butt_plug", "cowgirl_position", "reverse_cowgirl_position", "squatting_cowgirl_position", "reverse_upright_straddle", "irrumatio", "deepthroat", "pokephilia", "gaping", "orgy", "cleft_of_venus", "futanari", "futasub", "futa", "cumdrip", "fingering", "vibrator", "partially_visible_vulva", "penetration", "penetrated", "cumshot", "exhibitionism", "breast_milk", "grinding", "clitoral", "urethra", "phimosis", "cervix", "impregnation", "tribadism", "molestation", "pubic_hair", "clothed_female_nude_male", "clothed_male_nude_female", "clothed_female_nude_female", "clothed_male_nude_male", "sex_machine", "milking_machine", "ovum", "chikan", "pussy_juice_drip_through_clothes", "ejaculating_while_penetrated", "suspended_congress", "reverse_suspended_congress", "spread_pussy_under_clothes", "anilingus", "reach-around", "humping", "consensual_tentacles", "tentacle_pit", "cum_in_", ],
7
- #外観状態/外觀狀態
8
- "Appearance Status" : ["backless", "bandaged_neck", "bleeding", "blood", "_blood", "blush", "body_writing", "bodypaint", "bottomless", "breath", "bruise", "butt_crack", "cold", "covered_mouth", "crack", "cross-section", "crotchless", "crying", "curvy", "cuts", "dirty", "dripping", "drunk", "from_mouth", "glowing", "hairy", "halterneck", "hot", "injury", "latex", "leather", "levitation", "lipstick_mark", "_markings", "makeup", "mole", "moles", "no_bra", "nosebleed", "nude", "outfit", "pantylines", "peeing", "piercing", "_piercing", "piercings", "pregnant", "public_nudity", "reverse", "_skin", "_submerged", "saliva", "scar", "scratches", "see-through", "shadow", "shibari", "sideless", "skindentation", "sleeping","tan", "soap_bubbles", "steam", "steaming_body", "stitches", "sweat", "sweatdrop", "sweaty", "tanlines", "tattoo", "tattoo", "tears", "topless", "transparent", "trefoil", "trembling", "veins", "visible_air", "wardrobe_malfunction", "wet", "x-ray", "unconscious", "handprint", ],
9
- #動作姿勢/動作姿勢
10
- "Action Pose" : ["afloat", "afterimage", "against_fourth_wall", "against_wall", "aiming", "all_fours", "another's_mouth", "arm_", "arm_support", "arms_", "arms_behind_back", "asphyxiation", "attack", "back", "ballet", "bara", "bathing", "battle", "bdsm", "beckoning", "bent_over", "bite_mark", "biting", "bondage", "breast_suppress", "breathing", "burning", "bust_cup", "carry", "carrying", "caught", "chained", "cheek_squash", "chewing", "cigarette", "clapping", "closed_eye", "come_hither", "cooking", "covering", "cuddling", "dancing", "_docking", "destruction", "dorsiflexion", "dreaming", "dressing", "drinking", "driving", "dropping", "eating", "exercise", "expansion", "exposure", "facing", "failure", "fallen_down", "falling", "feeding", "fetal_position", "fighting", "finger_on_trigger", "finger_to_cheek", "finger_to_mouth", "firing", "fishing", "flashing", "fleeing", "flexible", "flexing", "floating", "flying", "fourth_wall", "freediving", "frogtie", "_grab", "girl_on_top", "giving", "grabbing", "grabbing_", "gymnastics", "_hold", "hadanugi_dousa", "hairdressing", "hand_", "hand_on", "hand_on_wall", "hands_", "headpat", "hiding", "holding", "hug", "hugging", "imagining", "in_container", "in_mouth", "in_palm", "jealous", "jumping", "kabedon", "kicking", "kiss", "kissing", "kneeling", "_lift", "lactation", "laundry", "licking", "lifted_by_self", "looking", "lowleg", "lying", "melting", "midair", "moaning", "_open", "on_back", "on_bed", "on_ground", "on_lap", "on_one_knee", "one_eye_closed", "open_", "over_mouth", "own_mouth", "_peek", "_pose", "_press", "_pull", "padding", "paint", "painting_(action)", "palms_together", "pee", "peeking", "pervert", "petting", "pigeon-toed", "piggyback", "pinching", "pinky_out", "pinned", "plantar_flexion", "planted", "playing", "pocky", "pointing", "poke", "poking", "pouring", "pov", "praying", "presenting", "profanity", "pulled_by_self", "pulling", "pump_action", "punching", "_rest", "raised", "reaching", "reading", "reclining", "reverse_grip", "riding", "running", "_slip", "salute", "screaming", "seiza", "selfie", "sewing", "shaking", "shoe_dangle", "shopping", "shouting", "showering", "shushing", "singing", "sitting", "slapping", "smell", "smelling", "smoking", "smother", "solo", "spanked", "spill", "spilling", "spinning", "splashing", "split", "squatting", "squeezed", "breasts_squeezed_together", "standing", "standing_on_", "staring", "straddling", "strangling", "stretching", "surfing", "suspension", "swimming", "talking", "teardrop", "tearing_clothes", "throwing", "tied_up", "tiptoes", "toe_scrunch", "toothbrush", "trigger_discipline", "tripping", "tsundere", "turning_head", "twitching", "two-handed", "tying", "_up", "unbuttoned", "undressed", "undressing", "unsheathed", "unsheathing", "unzipped", "unzipping", "upright_straddle", "v", "V", "vore", "_wielding","wading", "walk-in", "walking", "wariza", "waving", "wedgie", "wrestling", "writing", "yawning", "yokozuwari", "_conscious", "massage", "struggling", "shrugging", "drugged", "tentacles_under_clothes", "restrained_by_tentacles", "tentacles_around_arms", "tentacles_around_legs", "restrained_legs", "restrained_tail", "restrained_arms", "tentacles_on_female", "archery", "cleaning", "tempura", "facepalm", "sadism", ],
11
- #頭部装飾/頭部服飾
12
- "Headwear" : ["antennae", "antlers", "aura", "bandaged_head", "bandana", "bandeau", "beanie", "beanie", "beret", "bespectacled", "blindfold", "bonnet", "_cap", "circlet", "crown", "_drill", "_drills", "diadem", "_eyewear", "ear_covers", "ear_ornament", "ear_tag", "earbuds", "earclip", "earmuffs", "earphones", "earpiece", "earring", "earrings", "eyeliner", "eyepatch", "eyewear_on_head", "facial", "fedora", "glasses", "goggles", "_headwear", "hachimaki", "hair_bobbles", "hair_ornament", "hair_rings", "hair_tie", "hairband", "hairclip", "hairpin", "hairpods", "halo", "hat", "head-mounted_display", "head_wreath", "headband", "headdress", "headgear", "headphones", "headpiece", "headset", "helm", "helmet", "hood", "kabuto_(helmet)", "kanzashi", "_mask", "maid_headdress", "mask", "mask", "mechanical_ears", "mechanical_eye", "mechanical_horns", "mob_cap", "monocle", "neck_ruff", "nightcap", "on_head", "pince-nez", "qingdai_guanmao", "scarf_over_mouth", "scrunchie", "sunglasses", "tam_o'_shanter", "tate_eboshi", "tiara", "topknot", "turban", "veil", "visor", "wig", "mitre", "tricorne", "bicorne", ],
13
- #手部装飾/手部服飾
14
- "Handwear" : ["arm_warmers", "armband", "armlet", "bandaged_arm", "bandaged_fingers", "bandaged_hand", "bandaged_wrist", "bangle", "bracelet", "bracelets", "bracer", "cuffs", "elbow_pads", "_gauntlets", "_glove", "_gloves", "gauntlets", "gloves", "kote", "kurokote", "mechanical_arm", "mechanical_arms", "mechanical_hands", "mittens", "mitts", "nail_polish", "prosthetic_arm", "wrist_cuffs", "wrist_guards", "wristband", "yugake", ],
15
- #ワンピース衣装/一件式服裝
16
- "One-Piece Outfit" : ["bodystocking", "bodysuit", "dress", "furisode", "gown", "hanfu", "jumpsuit", "kimono", "leotard", "microdress", "one-piece", "overalls", "robe", "spacesuit", "sundress", "yukata", ],
17
- #上半身衣装/上半身服裝
18
- "Upper Body Clothing" : ["aiguillette", "apron", "_apron", "armor", "_armor", "ascot", "babydoll", "bikini", "_bikini", "blazer", "_blazer", "blouse", "_blouse", "bowtie", "_bowtie", "bra", "_bra", "breast_curtain", "breast_curtains", "breast_pocket", "breastplate", "bustier", "camisole", "cape", "capelet", "cardigan", "center_opening", "chemise", "chest_jewel", "choker", "cloak", "coat", "coattails", "collar", "_collar", "corset", "criss-cross_halter", "crop_top", "dougi", "feather_boa", "gakuran", "hagoromo", "hanten_(clothes)", "haori", "harem_pants", "harness", "hoodie", "jacket", "_jacket", "japanese_clothes", "kappougi", "kariginu", "lapels", "lingerie", "_lingerie", "maid", "mechanical_wings", "mizu_happi", "muneate", "neckerchief", "necktie", "negligee", "nightgown", "pajamas", "_pajamas", "pauldron", "pauldrons", "plunging_neckline", "raincoat", "rei_no_himo", "sailor_collar", "sarashi", "scarf", "serafuku", "shawl", "shirt", "shoulder_", "sleepwear", "sleeve", "sleeveless", "sleeves", "_sleeves", "sode", "spaghetti_strap", "sportswear", "strapless", "suit", "sundress", "suspenders", "sweater", "swimsuit", "_top", "_torso", "t-shirt", "tabard", "tailcoat", "tank_top", "tasuki", "tie_clip", "tunic", "turtleneck", "tuxedo", "_uniform", "undershirt", "uniform", "v-neck", "vambraces", "vest", "waistcoat", ],
19
- #下半身衣装/下半身服裝
20
- "Lower Body Clothing" : ["bare_hips", "bloomers", "briefs", "buruma", "crotch_seam", "cutoffs", "denim", "faulds", "fundoshi", "g-string", "garter_straps", "hakama", "hip_vent", "jeans", "knee_pads", "loincloth", "mechanical_tail", "microskirt", "miniskirt", "overskirt", "panties", "pants", "pantsu", "panty_straps", "pelvic_curtain", "petticoat", "sarong", "shorts", "side_slit", "skirt", "sweatpants", "swim_trunks", "thong", "underwear", "waist_cape", ],
21
- #足元・レッグウェア/腳與腿部服飾
22
- "Foot & Legwear" : ["anklet", "bandaged_leg", "boot", "boots", "_footwear", "flats", "flip-flops", "geta", "greaves", "_heels", "kneehigh", "kneehighs", "_legwear", "leg_warmers", "leggings", "loafers", "mary_janes", "mechanical_legs", "okobo", "over-kneehighs", "pantyhose", "prosthetic_leg", "pumps", "_shoe", "_sock", "sandals", "shoes", "skates", "slippers", "sneakers", "socks", "spikes", "tabi", "tengu-geta", "thigh_strap", "thighhighs", "uwabaki", "zouri", "legband", "ankleband", ],
23
- #その他の装飾/其他服飾
24
- "Other Accessories" : ["alternate_", "anklet", "badge", "beads", "belt", "belts", "bow", "brooch", "buckle", "button", "buttons", "_clothes", "_costume", "_cutout", "casual", "charm", "clothes_writing", "clothing_aside", "costume", "cow_print", "cross", "d-pad", "double-breasted", "drawstring", "epaulettes", "fabric", "fishnets", "floral_print", "formal", "frills", "_garter", "gem", "holster", "jewelry", "_knot", "lace", "lanyard", "leash", "magatama", "mechanical_parts", "medal", "medallion", "naked_bandage", "necklace", "_ornament", "(ornament)", "o-ring", "obi", "obiage", "obijime", "_pin", "_print", "padlock", "patterned_clothing", "pendant", "piercing", "plaid", "pocket", "polka_dot", "pom_pom_(clothes)", "pom_pom_(clothes)", "pouch", "ribbon", "_ribbon", "_stripe", "_stripes", "sash", "shackles", "shimenawa", "shrug_(clothing)", "skin_tight", "spandex", "strap", "sweatband", "_trim", "tassel", "zettai_ryouiki", "zipper", ],
25
- #表情/表情
26
- "Facial Expression" : ["ahegao", "anger_vein", "angry", "annoyed", "confused", "drooling", "embarrassed", "expressionless", "eye_contact", "_face", "frown", "fucked_silly", "furrowed_brow", "glaring", "gloom_(expression)", "grimace", "grin", "happy", "jitome", "laughing", "_mouth", "nervous", "notice_lines", "o_o", "parted_lips", "pout", "puff_of_air", "restrained", "sad", "sanpaku", "scared", "scowl", "serious", "shaded_face", "shy", "sigh", "sleepy", "smile", "smirk", "smug", "snot", "spoken_ellipsis", "spoken_exclamation_mark", "spoken_interrobang", "spoken_question_mark", "squiggle", "surprised", "tareme", "tearing_up", "thinking", "tongue", "tongue_out", "torogao", "tsurime", "turn_pale", "wide-eyed", "wince", "worried", "heartbeat", ],
27
- #絵文字/表情符號
28
- "Facial Emoji" : ["!!", "!", "!?", "+++", "+_+", "...", "...?", "._.", "03:00", "0_0", ":/", ":3", ":<", ":>", ":>=", ":d", ":i", ":o", ":p", ":q", ":t", ":x", ":|", ";(", ";)", ";3", ";d", ";o", ";p", ";q", "=_=", ">:(", ">:)", ">_<", ">_o", ">o<", "?", "??", "@_@", "\m/", "\n/", "\o/", "\||/", "^^^", "^_^", "c:", "d:", "o_o", "o3o", "u_u", "w", "x", "x_x", "xd", "zzz", "|_|", ],
29
- #頭部/頭部
30
- "Head" : ["afro", "ahoge", "animal_ear_fluff", "_bangs", "_bun", "bald", "beard", "blunt_bangs", "blunt_ends", "bob_cut", "bowl_cut", "braid", "braids", "buzz_cut", "circle_cut", "colored_tips", "cowlick", "dot_nose", "dreadlocks", "_ear", "_ears", "_eye", "_eyes", "enpera", "eyeball", "eyebrow", "eyebrow_cut", "eyebrows", "eyelashes", "eyeshadow", "faceless", "facepaint", "facial_mark", "fang", "forehead", "freckles", "goatee", "_hair", "_horn", "_horns", "hair_", "hair_bun", "hair_flaps", "hair_intakes", "hair_tubes", "half_updo", "head_tilt", "heterochromia", "hime_cut", "hime_cut", "horns", "in_eye", "inverted_bob", "kemonomimi_mode", "lips", "mascara", "mohawk", "mouth_", "mustache", "nose", "one-eyed", "one_eye", "one_side_up", "_pupils", "parted_bangs", "pompadour", "ponytail", "ringlets", "_sclera", "sideburns", "sidecut", "sidelock", "sidelocks", "skull", "snout", "stubble", "swept_bangs", "tails", "teeth", "third_eye", "twintails", "two_side_up", "undercut", "updo", "v-shaped_eyebrows", "whiskers", "tentacle_hair", ],
31
- #手部/手部
32
- "Hands" : ["_arm", "_arms", "claws", "_finger", "_fingers", "fingernails", "_hand", "_nail", "_nails", "palms", "rings", "thumbs_up", ],
33
- #上半身/上半身
34
- "Upper Body" : ["abs", "armpit", "armpits", "backboob", "belly", "biceps", "breast_rest", "breasts", "button_gap", "cleavage", "collarbone", "dimples_of_venus", "downblouse", "flat_chest", "linea_alba", "median_furrow", "midriff", "nape", "navel", "pectorals", "ribs", "_shoulder", "_shoulders", "shoulder_blades", "sideboob", "sidetail", "spine", "stomach", "strap_gap", "toned", "underboob", "underbust", ],
35
- #下半身/下半身
36
- "Lower Body" : ["ankles", "ass", "barefoot", "crotch", "feet", "highleg", "hip_bones", "hooves", "kneepits", "knees", "legs", "soles", "tail", "thigh_gap", "thighlet", "thighs", "toenail", "toenails", "toes", "wide_hips", ],
37
- #生物/生物
38
- "Creature" : ["(animal)", "anglerfish", "animal", "bear", "bee", "bird", "bug", "butterfly", "cat", "chick", "chicken", "chinese_zodiac", "clownfish", "coral", "crab", "creature", "crow", "dog", "dove", "dragon", "duck", "eagle", "fish", "fish", "fox", "fox", "frog", "frog", "goldfish", "hamster", "horse", "jellyfish", "ladybug", "lion", "mouse", "octopus", "owl", "panda", "penguin", "pig", "pigeon", "rabbit", "rooster", "seagull", "shark", "sheep", "shrimp", "snail", "snake", "squid", "starfish", "tanuki", "tentacles", "goo_tentacles", "plant_tentacles", "crotch_tentacles", "mechanical_tentacles", "squidward_tentacles", "suction_tentacles", "penis_tentacles", "translucent_tentacles", "back_tentacles", "red_tentacles", "green_tentacles", "blue_tentacles", "black_tentacles", "pink_tentacles", "purple_tentacles", "face_tentacles", "tentacles_everywhere", "milking_tentacles", "tiger", "turtle", "weasel", "whale", "wolf", "parrot", "sparrow", "unicorn", ],
39
- #植物/植物
40
- "Plant" : ["bamboo", "bouquet", "branch", "bush", "cherry_blossoms", "clover", "daisy", "(flower)", "flower", "flower", "gourd", "hibiscus", "holly", "hydrangea", "leaf", "lily_pad", "lotus", "moss", "palm_leaf", "palm_tree", "petals", "plant", "plum_blossoms", "rose", "spider_lily", "sunflower", "thorns", "tree", "tulip", "vines", "wisteria", "acorn", ],
41
- #食べ物/食物
42
- "Food" : ["apple", "baguette", "banana", "baozi", "beans", "bento", "berry", "blueberry", "bread", "broccoli", "burger", "cabbage", "cake", "candy", "carrot", "cheese", "cherry", "chili_pepper", "chocolate", "coconut", "cookie", "corn", "cream", "crepe", "cucumber", "cucumber", "cupcake", "curry", "dango", "dessert", "doughnut", "egg", "eggplant", "_(food)", "_(fruit)", "food", "french_fries", "fruit", "grapes", "ice_cream", "icing", "lemon", "lettuce", "lollipop", "macaron", "mandarin_orange", "meat", "melon", "mochi", "mushroom", "noodles", "omelet", "omurice", "onigiri", "onion", "pancake", "parfait", "pasties", "pastry", "peach", "pineapple", "pizza", "popsicle", "potato", "pudding", "pumpkin", "radish", "ramen", "raspberry", "rice", "roasted_sweet_potato", "sandwich", "sausage", "seaweed", "skewer", "spitroast", "spring_onion", "strawberry", "sushi", "sweet_potato", "sweets", "taiyaki", "takoyaki", "tamagoyaki", "tempurakanbea", "toast", "tomato", "vegetable", "wagashi", "wagashi", "watermelon", "jam", "popcorn", ],
43
- #飲み物/飲品
44
- "Beverage" : ["alcohol", "beer", "coffee", "cola", "drink", "juice", "juice_box", "milk", "sake", "soda", "tea", "_tea", "whiskey", "wine", "cocktail", ],
45
- #音楽/音樂
46
- "Music" : ["band", "baton_(conducting)", "beamed", "cello", "concert", "drum", "drumsticks", "eighth_note", "flute", "guitar", "harp", "horn", "(instrument)", "idol", "instrument", "k-pop", "lyre", "(music)", "megaphone", "microphone", "music", "musical_note", "phonograph", "piano", "plectrum", "quarter_note", "recorder", "sixteenth_note", "sound_effects", "trumpet", "utaite", "violin", "whistle", ],
47
- #武器・装備/武器・裝備
48
- "Weapons & Equipment" : ["ammunition", "arrow_(projectile)", "axe", "bandolier", "baseball_bat", "beretta_92", "bolt_action", "bomb", "bullet", "bullpup", "cannon", "chainsaw", "crossbow", "dagger", "energy_sword", "explosive", "fighter_jet", "gohei", "grenade", "gun", "hammer", "handgun", "holstered", "jet", "katana", "knife", "kunai", "lance", "mallet", "nata_(tool)", "polearm", "quiver", "rapier", "revolver", "rifle", "rocket_launcher", "scabbard", "scope", "scythe", "sheath", "sheathed", "shield", "shotgun", "shuriken", "spear", "staff", "suppressor", "sword", "tank", "tantou", "torpedo", "trident", "(weapon)", "wand", "weapon", "whip", "yumi_(bow)", "h&k_hk416", "rocket_launcher", "heckler_&_koch", "_weapon", ],
49
- #乗り物/交通器具
50
- "Vehicles" : ["aircraft", "airplane", "bicycle", "boat", "car", "caterpillar_tracks", "flight_deck", "helicopter", "motor_vehicle", "motorcycle", "ship", "spacecraft", "spoiler_(automobile)", "train", "truck", "watercraft", "wheel", "wheelbarrow", "wheelchair", "inflatable_raft", ],
51
- #建物/建物
52
- "Buildings" : ["apartment", "aquarium", "architecture", "balcony", "building", "cafe", "castle", "church", "gym", "hallway", "hospital", "house", "library", "(place)", "porch", "restaurant", "restroom", "rooftop", "shop", "skyscraper", "stadium", "stage", "temple", "toilet", "tower", "train_station", "veranda", ],
53
- #室内/室內
54
- "Indoor" : ["bath", "bathroom", "bathtub", "bed", "bed_sheet", "bedroom", "blanket", "bookshelf", "carpet", "ceiling", "chair", "chalkboard", "classroom", "counter", "cupboard", "curtains", "cushion", "dakimakura", "desk", "door", "doorway", "drawer", "_floor", "floor", "futon", "indoors", "interior", "kitchen", "kotatsu", "locker", "mirror", "pillow", "room", "rug", "school_desk", "shelf", "shouji", "sink", "sliding_doors", "stairs", "stool", "storeroom", "table", "tatami", "throne", "window", "windowsill", "bathhouse", "chest_of_drawers", ],
55
- #屋外/室外
56
- "Outdoor" : ["alley", "arch", "beach", "bridge", "bus_stop", "bush", "cave", "(city)", "city", "cliff", "crescent", "crosswalk", "day", "desert", "fence", "ferris_wheel", "field", "forest", "grass", "graveyard", "hill", "lake", "lamppost", "moon", "mountain", "night", "ocean", "onsen", "outdoors", "path", "pool", "poolside", "railing", "railroad", "river", "road", "rock", "sand", "shore", "sky", "smokestack", "snow", "snowball", "snowman", "street", "sun", "sunlight", "sunset", "tent", "torii", "town", "tree", "turret", "utility_pole", "valley", "village", "waterfall", ],
57
- #物品/物品
58
- "Objects" : ["anchor", "android", "armchair", "(bottle)", "backpack", "bag", "ball", "balloon", "bandages", "bandaid", "bandaids", "banknote", "banner", "barcode", "barrel", "baseball", "basket", "basketball", "beachball", "bell", "bench", "binoculars", "board_game", "bone", "book", "bottle", "bowl", "box", "box_art", "briefcase", "broom", "bucket", "(chess)", "(computer)", "(computing)", "(container)", "cage", "calligraphy_brush", "camera", "can", "candle", "candlestand", "cane", "card", "cartridge", "cellphone", "chain", "chandelier", "chess", "chess_piece", "choko_(cup)", "chopsticks", "cigar", "clipboard", "clock", "clothesline", "coin", "comb", "computer", "condom", "controller", "cosmetics", "couch", "cowbell", "crazy_straw", "cup", "cutting_board", "dice", "digital_media_player", "doll", "drawing_tablet", "drinking_straw", "easel", "electric_fan", "emblem", "envelope", "eraser", "feathers", "figure", "fire", "fishing_rod", "flag", "flask", "folding_fan", "fork", "frying_pan", "(gemstone)", "game_console", "gears", "gemstone", "gift", "glass", "glowstick", "gold", "handbag", "handcuffs", "handheld_game_console", "hose", "id_card", "innertube", "iphone", "jack-o'-lantern", "jar", "joystick", "key", "keychain", "kiseru", "ladder", "ladle", "lamp", "lantern", "laptop", "letter", "letterboxed", "lifebuoy", "lipstick", "liquid", "lock", "lotion", "_machine", "map", "marker", "model_kit", "money", "monitor", "mop", "mug", "needle", "newspaper", "nintendo", "nintendo_switch", "notebook", "(object)", "ofuda", "orb", "origami", "(playing_card)", "pack", "paddle", "paintbrush", "pan", "paper", "parasol", "patch", "pc", "pen", "pencil", "pencil", "pendant_watch", "phone", "pill", "pinwheel", "plate", "playstation", "pocket_watch", "pointer", "poke_ball", "pole", "quill", "racket", "randoseru", "remote_control", "ring", "rope", "sack", "saddle", "sakazuki", "satchel", "saucer", "scissors", "scroll", "seashell", "seatbelt", "shell", "shide", "shopping_cart", "shovel", "shower_head", "silk", "sketchbook", "smartphone", "soap", "sparkler", "spatula", "speaker", "spoon", "statue", "stethoscope", "stick", "sticker", "stopwatch", "string", "stuffed_", "stylus", "suction_cups", "suitcase", "surfboard", "syringe", "talisman", "tanzaku", "tape", "teacup", "teapot", "teddy_bear", "television", "test_tube", "tiles", "tokkuri", "tombstone", "torch", "towel", "toy", "traffic_cone", "tray", "treasure_chest", "uchiwa", "umbrella", "vase", "vial", "video_game", "viewfinder", "volleyball", "wallet", "watch", "watch", "whisk", "whiteboard", "wreath", "wrench", "wristwatch", "yunomi", "ace_of_hearts", "inkwell", "compass", "ipod", "sunscreen", "rocket", "cobblestone", ],
59
- #キャラクター設定/角色設定
60
- "Character Design" : ["+boys", "+girls", "1other", "39", "_boys", "_challenge", "_connection", "_female", "_fur", "_girls", "_interface", "_male", "_man", "_person", "abyssal_ship", "age_difference", "aged_down", "aged_up", "albino", "alien", "alternate_muscle_size", "ambiguous_gender", "amputee", "androgynous", "angel", "animalization", "ass-to-ass", "assault_visor", "au_ra", "baby", "bartender", "beak", "bishounen", "borrowed_character", "boxers", "boy", "breast_envy", "breathing_fire", "bride", "broken", "brother_and_sister", "brothers", "camouflage", "cheating_(relationship)", "cheerleader", "chibi", "child", "clone", "command_spell", "comparison", "contemporary", "corpse", "corruption", "cosplay", "couple", "creature_and_personification", "crossdressing", "crossover", "cyberpunk", "cyborg", "cyclops", "damaged", "dancer", "danmaku", "darkness", "death", "defeat", "demon", "disembodied_", "draph", "drone", "duel", "dwarf", "egyptian", "electricity", "elezen", "elf", "enmaided", "erune", "everyone", "evolutionary_line", "expressions", "fairy", "family", "fangs", "fantasy", "fashion", "fat", "father_and_daughter", "father_and_son", "fewer_digits", "fins", "flashback", "fluffy", "fumo_(doll)", "furry", "fusion", "fuuin_no_tsue", "gameplay_mechanics", "genderswap", "ghost", "giant", "giantess", "gibson_les_paul", "girl", "goblin", "groom", "guro", "gyaru", "habit", "harem", "harpy", "harvin", "heads_together", "health_bar", "height_difference", "hitodama", "horror_(theme)", "humanization", "husband_and_wife", "hydrokinesis", "hypnosis", "hyur", "idol", "insignia", "instant_loss", "interracial", "interspecies", "japari_bun", "jeweled_branch_of_hourai", "jiangshi", "jirai_kei", "joints", "karakasa_obake", "keyhole", "kitsune", "knight", "kodona", "kogal", "kyuubi", "lamia", "left-handed", "loli", "lolita", "look-alike", "machinery", "magic", "male_focus", "manly", "matching_outfits", "mature_female", "mecha", "mermaid", "meta", "miko", "milestone_celebration", "military", "mind_control", "miniboy", "minigirl", "miqo'te", "monster", "monsterification", "mother_and_daughter", "mother_and_son", "multiple_others", "muscular", "nanodesu_(phrase)", "narrow_waist", "nekomata", "netorare", "ninja", "no_humans", "nontraditional", "nun", "nurse", "object_namesake", "obliques", "office_lady", "old", "on_body", "onee-shota", "oni", "orc", "others", "otoko_no_ko", "oversized_object", "paint_splatter", "pantyshot", "pawpads", "persona", "personality", "personification", "pet_play", "petite", "pirate", "playboy_bunny", "player_2", "plugsuit", "plump", "poi", "pokemon", "police", "policewoman", "pom_pom_(cheerleading)", "princess", "prosthesis", "pun", "puppet", "race_queen", "radio_antenna", "real_life_insert", "redesign", "reverse_trap", "rigging", "robot", "rod_of_remorse", "sailor", "salaryman", "samurai", "sangvis_ferri", "scales", "scene_reference", "school", "sheikah", "shota", "shrine", "siblings", "side-by-side", "sidesaddle", "sisters", "size_difference", "skeleton", "skinny", "slave", "slime_(substance)", "soldier", "spiked_shell", "spokencharacter", "steampunk", "streetwear", "striker_unit", "strongman", "submerged", "suggestive", "super_saiyan", "superhero", "surreal", "take_your_pick", "tall", "talons", "taur", "teacher", "team_rocket", "three-dimensional_maneuver_gear", "time_paradox", "tomboy", "traditional_youkai", "transformation", "trick_or_treat", "tusks", "twins", "ufo", "under_covers", "v-fin", "v-fin", "vampire", "virtual_youtuber", "waitress", "watching_television", "wedding", "what", "when_you_see_it", "wife_and_wife", "wing", "wings", "witch", "world_war_ii", "yandere", "year_of", "yes", "yin_yang", "yordle", "you're_doing_it_wrong", "you_gonna_get_raped", "yukkuri_shiteitte_ne", "yuri", "zombie", "(alice_in_wonderland)", "(arknights)", "(blue_archive)", "(cosplay)", "(creature)", "(emblem)", "(evangelion)", "(fate)", "(fate/stay_night)", "(ff11)", "(fire_emblem)", "(genshin_impact)", "(grimm)", "(houseki_no_kuni)", "(hyouka)", "(idolmaster)", "(jojo)", "(kancolle)", "(kantai_collection)", "(kill_la_kill)", "(league_of_legends)", "(legends)", "(lyomsnpmp)", "(machimazo)", "(madoka_magica)", "(mecha)", "(meme)", "(nier:automata)", "(organ)", "(overwatch)", "(pokemon)", "(project_moon)", "(project_sekai)", "(sao)", "(senran_kagura)", "(splatoon)", "(touhou)", "(tsukumo_sana)", "(youkai_watch)", "(yu-gi-oh!_gx)", "(zelda)", "sextuplets", "imperial_japanese_army", "extra_faces", "_miku", ],
61
- #構図/構圖
62
- "Composition" : ["abstract", "anime_coloring", "animification", "back-to-back", "bad_anatomy", "blurry", "border", "bound", "cameo", "cheek-to-cheek", "chromatic_aberration", "close-up", "collage", "color_guide", "colorful", "comic", "contrapposto", "cover", "cowboy_shot", "crosshatching", "depth_of_field", "dominatrix", "dutch_angle", "_focus", "face-to-face", "fake_screenshot", "film_grain", "fisheye", "flat_color", "foreshortening", "from_above", "from_behind", "from_below", "from_side", "full_body", "glitch", "greyscale", "halftone", "head_only", "heads-up_display", "high_contrast", "horizon", "_inset", "inset", "jaggy_lines", "1koma", "2koma", "3koma", "4koma", "5koma", "leaning", "leaning_forward", "leaning_to_the_side", "left-to-right_manga", "lens_flare", "limited_palette", "lineart", "lineup", "lower_body", "(medium)", "marker_(medium)", "meme", "mixed_media", "monochrome", "multiple_views", "muted_color", "oekaki", "on_side", "out_of_frame", "outline", "painting", "parody", "partially_colored", "partially_underwater_shot", "perspective", "photorealistic", "picture_frame", "pillarboxed", "portrait", "poster_(object)", "product_placement", "profile", "realistic", "recording", "retro_artstyle", "(style)", "_style", "sandwiched", "science_fiction", "sepia", "shikishi", "side-by-side", "sideways", "sideways_glance", "silhouette", "sketch", "spot_color", "still_life", "straight-on", "symmetry", "(texture)", "tachi-e", "taking_picture", "tegaki", "too_many", "traditional_media", "turnaround", "underwater", "upper_body", "upside-down", "upskirt", "variations", "wide_shot", "_design", "symbolism", "rounded_corners", "surrounded", ],
63
- #季節/季節
64
- "Season" : ["akeome", "anniversary", "autumn", "birthday", "christmas", "_day", "festival", "halloween", "kotoyoro", "nengajou", "new_year", "spring_(season)", "summer", "tanabata", "valentine", "winter", ],
65
- #背景/背景
66
- "Background" : ["_background", "backlighting", "bloom", "bokeh", "brick_wall", "bubble", "cable", "caustics", "cityscape", "cloud", "confetti", "constellation", "contrail", "crowd", "crystal", "dark", "debris", "dusk", "dust", "egasumi", "embers", "emphasis_lines", "energy", "evening", "explosion", "fireworks", "fog", "footprints", "glint", "graffiti", "ice", "industrial_pipe", "landscape", "light", "light_particles", "light_rays", "lightning", "lights", "moonlight", "motion_blur", "motion_lines", "mountainous_horizon", "nature", "(planet)", "pagoda", "people", "pillar", "planet", "power_lines", "puddle", "rain", "rainbow", "reflection", "ripples", "rubble", "ruins", "scenery", "shade", "shooting_star", "sidelighting", "smoke", "snowflakes", "snowing", "space", "sparkle", "sparks", "speed_lines", "spider_web", "spotlight", "star_(sky)", "stone_wall", "sunbeam", "sunburst", "sunrise", "_theme", "tile_wall", "twilight", "wall_clock", "wall_of_text", "water", "waves", "wind", "wire", "wooden_wall", "lighthouse", ],
67
- # パターン/圖案
68
- "Patterns" : ["arrow", "bass_clef", "blank_censor", "circle", "cube", "heart", "hexagon", "hexagram", "light_censor", "(pattern)", "pattern", "pentagram", "roman_numeral", "(shape)", "(symbol)", "shape", "sign", "symbol", "tally", "treble_clef", "triangle", "tube", "yagasuri", ],
69
- #検閲/審查
70
- "Censorship" : ["blur_censor", "_censor", "_censoring", "censored", "character_censor", "convenient", "hair_censor", "heart_censor", "identity_censor", "maebari", "novelty_censor", "soap_censor", "steam_censor", "tail_censor", "uncensored", ],
71
- #その他/其他
72
- "Others" : ["2007", "2008", "2009", "2010", "2011", "2012", "2013", "2014", "2015", "2016", "2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024", "artist", "artist_name", "artistic_error", "asian", "(company)", "character_name", "content_rating", "copyright", "cover_page", "dated", "english_text", "japan", "layer", "logo", "name", "numbered", "page_number", "pixiv_id", "ranguage", "reference_sheet", "signature", "speech_bubble", "subtitled", "text", "thank_you", "typo", "username", "wallpaper", "watermark", "web_address", "screwdriver", "translated", ],
73
- "Quality Tags" : ["masterpiece", "_quality", "highres", "absurdres", "ultra-detailed", "lowres", ],
74
- }
75
-
76
- reversed_categories = {value: key for key, values in categories.items() for value in values}
77
-
78
- # Precompute keyword lengths
79
- keyword_lengths = {keyword: len(keyword) for keyword in reversed_categories}
80
-
81
- # Trie for efficient keyword matching
82
- class TrieNode:
83
- def __init__(self):
84
- self.children = {}
85
- self.category = None
86
-
87
- def build_trie(keywords):
88
- root = TrieNode()
89
- for keyword, category in reversed_categories.items():
90
- node = root
91
- for char in keyword:
92
- if char not in node.children:
93
- node.children[char] = TrieNode()
94
- node = node.children[char]
95
- node.category = category
96
- return root
97
-
98
- trie_root = build_trie(reversed_categories)
99
-
100
- def find_category(trie_root, tag):
101
- node = trie_root
102
- for char in tag:
103
- if char in node.children:
104
- node = node.children[char]
105
- if node.category:
106
- return node.category
107
- else:
108
- break
109
- return None
110
-
111
- def classify_tags(tags: list[str], local_test: bool = False):
112
- # Dictionary for automatic classification
113
- classified_tags: defaultdict[str, list] = defaultdict(list)
114
- fuzzy_match_tags: defaultdict[str, list] = defaultdict(list)
115
- unclassified_tags: list[str] = []
116
-
117
- # Logic for automatic grouping
118
- for tag in tags:
119
- classified = False
120
- tag_new = tag.replace(" ", "_").replace("-", "_").replace("\\(", "(").replace("\\)", ")") # Replace spaces in source tags with underscores
121
-
122
- # Exact match using the trie
123
- category = find_category(trie_root, tag_new)
124
- if category:
125
- classified = True
126
- else:
127
- # Fuzzy match
128
- tag_parts = tag_new.split("_")
129
- for keyword, keyword_length in keyword_lengths.items():
130
- if keyword in tag_new and keyword_length > 3: # Adjust the threshold if needed
131
- classified = True
132
- category = reversed_categories[keyword]
133
- break
134
-
135
- if classified and tag not in classified_tags[category]: # Avoid duplicates
136
- classified_tags[category].append(tag)
137
- elif not classified and tag not in unclassified_tags:
138
- unclassified_tags.append(tag) # Unclassified tags
139
-
140
- if local_test:
141
- # Output the grouping result
142
- for category, tags in classified_tags.items():
143
- print(f"{category}:")
144
- print(", ".join(tags))
145
- print()
146
-
147
- print()
148
- print("Fuzzy match:")
149
- for category, tags in fuzzy_match_tags.items():
150
- print(f"{category}:")
151
- print(", ".join(tags))
152
- print()
153
- print()
154
-
155
- if len(unclassified_tags) > 0:
156
- print(f"\nUnclassified tags: {len(unclassified_tags)}")
157
- print(f"{unclassified_tags[:200]}") # Display some unclassified tags
158
-
159
- return classified_tags, unclassified_tags
160
-
161
- # Code for "Tag Categorizer" tab
162
- def process_tags(input_tags: str):
163
- # Clean and split the input tags <- Fix later
164
- # tags = [tag.strip().split()[0] for tag in input_tags.split('?') if tag.strip()]
165
- # tags = [tag.replace('_', ' ') for tag in tags]
166
- tags = [tag.strip() for tag in input_tags.split(',') if tag.strip()]
167
- classified_tags, unclassified_tags = classify_tags(tags)
168
-
169
- categorized_string = ', '.join([tag for category in classified_tags.values() for tag in category])
170
- categorized_json = {category: tags for category, tags in classified_tags.items()}
171
-
172
- return categorized_string, categorized_json, "" # Initialize enhanced_prompt as empty
173
-
174
- tags = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import defaultdict
2
+ import re
3
+ # Define grouping rules (categories and keywords)
4
+ # Provided categories and reversed_categories
5
+ categories = {
6
+ "Explicit" : ["sex", "69", "paizuri", "cum", "precum", "areola_slip", "hetero", "erection", "oral", "fellatio", "yaoi", "ejaculation", "ejaculating", "masturbation", "handjob", "bulge", "rape", "_rape", "doggystyle", "threesome", "missionary", "object_insertion", "nipple", "nipples", "pussy", "anus", "penis", "groin", "testicles", "testicle", "anal", "cameltoe", "areolae", "dildo", "clitoris", "top-down_bottom-up", "gag", "groping", "gagged", "gangbang", "orgasm", "femdom", "incest", "bukkake", "breast_out", "vaginal", "vagina", "public_indecency", "breast_sucking", "folded", "cunnilingus", "_cunnilingus", "foreskin", "bestiality", "footjob", "uterus", "womb", "flaccid", "defloration", "butt_plug", "cowgirl_position", "reverse_cowgirl_position", "squatting_cowgirl_position", "reverse_upright_straddle", "irrumatio", "deepthroat", "pokephilia", "gaping", "orgy", "cleft_of_venus", "futanari", "futasub", "futa", "cumdrip", "fingering", "vibrator", "partially_visible_vulva", "penetration", "penetrated", "cumshot", "exhibitionism", "breast_milk", "grinding", "clitoral", "urethra", "phimosis", "cervix", "impregnation", "tribadism", "molestation", "pubic_hair", "clothed_female_nude_male", "clothed_male_nude_female", "clothed_female_nude_female", "clothed_male_nude_male", "sex_machine", "milking_machine", "ovum", "chikan", "pussy_juice_drip_through_clothes", "ejaculating_while_penetrated", "suspended_congress", "reverse_suspended_congress", "spread_pussy_under_clothes", "anilingus", "reach-around", "humping", "consensual_tentacles", "tentacle_pit", "cum_in_", ],
7
+ #外観状態/外觀狀態
8
+ "Appearance Status" : ["backless", "bandaged_neck", "bleeding", "blood", "_blood", "blush", "body_writing", "bodypaint", "bottomless", "breath", "bruise", "butt_crack", "cold", "covered_mouth", "crack", "cross-section", "crotchless", "crying", "curvy", "cuts", "dirty", "dripping", "drunk", "from_mouth", "glowing", "hairy", "halterneck", "hot", "injury", "latex", "leather", "levitation", "lipstick_mark", "_markings", "makeup", "mole", "moles", "no_bra", "nosebleed", "nude", "outfit", "pantylines", "peeing", "piercing", "_piercing", "piercings", "pregnant", "public_nudity", "reverse", "_skin", "_submerged", "saliva", "scar", "scratches", "see-through", "shadow", "shibari", "sideless", "skindentation", "sleeping","tan", "soap_bubbles", "steam", "steaming_body", "stitches", "sweat", "sweatdrop", "sweaty", "tanlines", "tattoo", "tattoo", "tears", "topless", "transparent", "trefoil", "trembling", "veins", "visible_air", "wardrobe_malfunction", "wet", "x-ray", "unconscious", "handprint", ],
9
+ #動作姿勢/動作姿勢
10
+ "Action Pose" : ["afloat", "afterimage", "against_fourth_wall", "against_wall", "aiming", "all_fours", "another's_mouth", "arm_", "arm_support", "arms_", "arms_behind_back", "asphyxiation", "attack", "back", "ballet", "bara", "bathing", "battle", "bdsm", "beckoning", "bent_over", "bite_mark", "biting", "bondage", "breast_suppress", "breathing", "burning", "bust_cup", "carry", "carrying", "caught", "chained", "cheek_squash", "chewing", "cigarette", "clapping", "closed_eye", "come_hither", "cooking", "covering", "cuddling", "dancing", "_docking", "destruction", "dorsiflexion", "dreaming", "dressing", "drinking", "driving", "dropping", "eating", "exercise", "expansion", "exposure", "facing", "failure", "fallen_down", "falling", "feeding", "fetal_position", "fighting", "finger_on_trigger", "finger_to_cheek", "finger_to_mouth", "firing", "fishing", "flashing", "fleeing", "flexible", "flexing", "floating", "flying", "fourth_wall", "freediving", "frogtie", "_grab", "girl_on_top", "giving", "grabbing", "grabbing_", "gymnastics", "_hold", "hadanugi_dousa", "hairdressing", "hand_", "hand_on", "hand_on_wall", "hands_", "headpat", "hiding", "holding", "hug", "hugging", "imagining", "in_container", "in_mouth", "in_palm", "jealous", "jumping", "kabedon", "kicking", "kiss", "kissing", "kneeling", "_lift", "lactation", "laundry", "licking", "lifted_by_self", "looking", "lowleg", "lying", "melting", "midair", "moaning", "_open", "on_back", "on_bed", "on_ground", "on_lap", "on_one_knee", "one_eye_closed", "open_", "over_mouth", "own_mouth", "_peek", "_pose", "_press", "_pull", "padding", "paint", "painting_(action)", "palms_together", "pee", "peeking", "pervert", "petting", "pigeon-toed", "piggyback", "pinching", "pinky_out", "pinned", "plantar_flexion", "planted", "playing", "pocky", "pointing", "poke", "poking", "pouring", "pov", "praying", "presenting", "profanity", "pulled_by_self", "pulling", "pump_action", "punching", "_rest", "raised", "reaching", "reading", "reclining", "reverse_grip", "riding", "running", "_slip", "salute", "screaming", "seiza", "selfie", "sewing", "shaking", "shoe_dangle", "shopping", "shouting", "showering", "shushing", "singing", "sitting", "slapping", "smell", "smelling", "smoking", "smother", "solo", "spanked", "spill", "spilling", "spinning", "splashing", "split", "squatting", "squeezed", "breasts_squeezed_together", "standing", "standing_on_", "staring", "straddling", "strangling", "stretching", "surfing", "suspension", "swimming", "talking", "teardrop", "tearing_clothes", "throwing", "tied_up", "tiptoes", "toe_scrunch", "toothbrush", "trigger_discipline", "tripping", "tsundere", "turning_head", "twitching", "two-handed", "tying", "_up", "unbuttoned", "undressed", "undressing", "unsheathed", "unsheathing", "unzipped", "unzipping", "upright_straddle", "v", "V", "vore", "_wielding","wading", "walk-in", "walking", "wariza", "waving", "wedgie", "wrestling", "writing", "yawning", "yokozuwari", "_conscious", "massage", "struggling", "shrugging", "drugged", "tentacles_under_clothes", "restrained_by_tentacles", "tentacles_around_arms", "tentacles_around_legs", "restrained_legs", "restrained_tail", "restrained_arms", "tentacles_on_female", "archery", "cleaning", "tempura", "facepalm", "sadism", ],
11
+ #頭部装飾/頭部服飾
12
+ "Headwear" : ["antennae", "antlers", "aura", "bandaged_head", "bandana", "bandeau", "beanie", "beanie", "beret", "bespectacled", "blindfold", "bonnet", "_cap", "circlet", "crown", "_drill", "_drills", "diadem", "_eyewear", "ear_covers", "ear_ornament", "ear_tag", "earbuds", "earclip", "earmuffs", "earphones", "earpiece", "earring", "earrings", "eyeliner", "eyepatch", "eyewear_on_head", "facial", "fedora", "glasses", "goggles", "_headwear", "hachimaki", "hair_bobbles", "hair_ornament", "hair_rings", "hair_tie", "hairband", "hairclip", "hairpin", "hairpods", "halo", "hat", "head-mounted_display", "head_wreath", "headband", "headdress", "headgear", "headphones", "headpiece", "headset", "helm", "helmet", "hood", "kabuto_(helmet)", "kanzashi", "_mask", "maid_headdress", "mask", "mask", "mechanical_ears", "mechanical_eye", "mechanical_horns", "mob_cap", "monocle", "neck_ruff", "nightcap", "on_head", "pince-nez", "qingdai_guanmao", "scarf_over_mouth", "scrunchie", "sunglasses", "tam_o'_shanter", "tate_eboshi", "tiara", "topknot", "turban", "veil", "visor", "wig", "mitre", "tricorne", "bicorne", ],
13
+ #手部装飾/手部服飾
14
+ "Handwear" : ["arm_warmers", "armband", "armlet", "bandaged_arm", "bandaged_fingers", "bandaged_hand", "bandaged_wrist", "bangle", "bracelet", "bracelets", "bracer", "cuffs", "elbow_pads", "_gauntlets", "_glove", "_gloves", "gauntlets", "gloves", "kote", "kurokote", "mechanical_arm", "mechanical_arms", "mechanical_hands", "mittens", "mitts", "nail_polish", "prosthetic_arm", "wrist_cuffs", "wrist_guards", "wristband", "yugake", ],
15
+ #ワンピース衣装/一件式服裝
16
+ "One-Piece Outfit" : ["bodystocking", "bodysuit", "dress", "furisode", "gown", "hanfu", "jumpsuit", "kimono", "leotard", "microdress", "one-piece", "overalls", "robe", "spacesuit", "sundress", "yukata", ],
17
+ #上半身衣装/上半身服裝
18
+ "Upper Body Clothing" : ["aiguillette", "apron", "_apron", "armor", "_armor", "ascot", "babydoll", "bikini", "_bikini", "blazer", "_blazer", "blouse", "_blouse", "bowtie", "_bowtie", "bra", "_bra", "breast_curtain", "breast_curtains", "breast_pocket", "breastplate", "bustier", "camisole", "cape", "capelet", "cardigan", "center_opening", "chemise", "chest_jewel", "choker", "cloak", "coat", "coattails", "collar", "_collar", "corset", "criss-cross_halter", "crop_top", "dougi", "feather_boa", "gakuran", "hagoromo", "hanten_(clothes)", "haori", "harem_pants", "harness", "hoodie", "jacket", "_jacket", "japanese_clothes", "kappougi", "kariginu", "lapels", "lingerie", "_lingerie", "maid", "mechanical_wings", "mizu_happi", "muneate", "neckerchief", "necktie", "negligee", "nightgown", "pajamas", "_pajamas", "pauldron", "pauldrons", "plunging_neckline", "raincoat", "rei_no_himo", "sailor_collar", "sarashi", "scarf", "serafuku", "shawl", "shirt", "shoulder_", "sleepwear", "sleeve", "sleeveless", "sleeves", "_sleeves", "sode", "spaghetti_strap", "sportswear", "strapless", "suit", "sundress", "suspenders", "sweater", "swimsuit", "_top", "_torso", "t-shirt", "tabard", "tailcoat", "tank_top", "tasuki", "tie_clip", "tunic", "turtleneck", "tuxedo", "_uniform", "undershirt", "uniform", "v-neck", "vambraces", "vest", "waistcoat", ],
19
+ #下半身衣装/下半身服裝
20
+ "Lower Body Clothing" : ["bare_hips", "bloomers", "briefs", "buruma", "crotch_seam", "cutoffs", "denim", "faulds", "fundoshi", "g-string", "garter_straps", "hakama", "hip_vent", "jeans", "knee_pads", "loincloth", "mechanical_tail", "microskirt", "miniskirt", "overskirt", "panties", "pants", "pantsu", "panty_straps", "pelvic_curtain", "petticoat", "sarong", "shorts", "side_slit", "skirt", "sweatpants", "swim_trunks", "thong", "underwear", "waist_cape", ],
21
+ #足元・レッグウェア/腳與腿部服飾
22
+ "Foot & Legwear" : ["anklet", "bandaged_leg", "boot", "boots", "_footwear", "flats", "flip-flops", "geta", "greaves", "_heels", "kneehigh", "kneehighs", "_legwear", "leg_warmers", "leggings", "loafers", "mary_janes", "mechanical_legs", "okobo", "over-kneehighs", "pantyhose", "prosthetic_leg", "pumps", "_shoe", "_sock", "sandals", "shoes", "skates", "slippers", "sneakers", "socks", "spikes", "tabi", "tengu-geta", "thigh_strap", "thighhighs", "uwabaki", "zouri", "legband", "ankleband", ],
23
+ #その他の装飾/其他服飾
24
+ "Other Accessories" : ["alternate_", "anklet", "badge", "beads", "belt", "belts", "bow", "brooch", "buckle", "button", "buttons", "_clothes", "_costume", "_cutout", "casual", "charm", "clothes_writing", "clothing_aside", "costume", "cow_print", "cross", "d-pad", "double-breasted", "drawstring", "epaulettes", "fabric", "fishnets", "floral_print", "formal", "frills", "_garter", "gem", "holster", "jewelry", "_knot", "lace", "lanyard", "leash", "magatama", "mechanical_parts", "medal", "medallion", "naked_bandage", "necklace", "_ornament", "(ornament)", "o-ring", "obi", "obiage", "obijime", "_pin", "_print", "padlock", "patterned_clothing", "pendant", "piercing", "plaid", "pocket", "polka_dot", "pom_pom_(clothes)", "pom_pom_(clothes)", "pouch", "ribbon", "_ribbon", "_stripe", "_stripes", "sash", "shackles", "shimenawa", "shrug_(clothing)", "skin_tight", "spandex", "strap", "sweatband", "_trim", "tassel", "zettai_ryouiki", "zipper", ],
25
+ #表情/表情
26
+ "Facial Expression" : ["ahegao", "anger_vein", "angry", "annoyed", "confused", "drooling", "embarrassed", "expressionless", "eye_contact", "_face", "frown", "fucked_silly", "furrowed_brow", "glaring", "gloom_(expression)", "grimace", "grin", "happy", "jitome", "laughing", "_mouth", "nervous", "notice_lines", "o_o", "parted_lips", "pout", "puff_of_air", "restrained", "sad", "sanpaku", "scared", "scowl", "serious", "shaded_face", "shy", "sigh", "sleepy", "smile", "smirk", "smug", "snot", "spoken_ellipsis", "spoken_exclamation_mark", "spoken_interrobang", "spoken_question_mark", "squiggle", "surprised", "tareme", "tearing_up", "thinking", "tongue", "tongue_out", "torogao", "tsurime", "turn_pale", "wide-eyed", "wince", "worried", "heartbeat", ],
27
+ #絵文字/表情符號
28
+ "Facial Emoji" : ["!!", "!", "!?", "+++", "+_+", "...", "...?", "._.", "03:00", "0_0", ":/", ":3", ":<", ":>", ":>=", ":d", ":i", ":o", ":p", ":q", ":t", ":x", ":|", ";(", ";)", ";3", ";d", ";o", ";p", ";q", "=_=", ">:(", ">:)", ">_<", ">_o", ">o<", "?", "??", "@_@", "\m/", "\n/", "\o/", "\||/", "^^^", "^_^", "c:", "d:", "o_o", "o3o", "u_u", "w", "x", "x_x", "xd", "zzz", "|_|", ],
29
+ #頭部/頭部
30
+ "Head" : ["afro", "ahoge", "animal_ear_fluff", "_bangs", "_bun", "bald", "beard", "blunt_bangs", "blunt_ends", "bob_cut", "bowl_cut", "braid", "braids", "buzz_cut", "circle_cut", "colored_tips", "cowlick", "dot_nose", "dreadlocks", "_ear", "_ears", "_eye", "_eyes", "enpera", "eyeball", "eyebrow", "eyebrow_cut", "eyebrows", "eyelashes", "eyeshadow", "faceless", "facepaint", "facial_mark", "fang", "forehead", "freckles", "goatee", "_hair", "_horn", "_horns", "hair_", "hair_bun", "hair_flaps", "hair_intakes", "hair_tubes", "half_updo", "head_tilt", "heterochromia", "hime_cut", "hime_cut", "horns", "in_eye", "inverted_bob", "kemonomimi_mode", "lips", "mascara", "mohawk", "mouth_", "mustache", "nose", "one-eyed", "one_eye", "one_side_up", "_pupils", "parted_bangs", "pompadour", "ponytail", "ringlets", "_sclera", "sideburns", "sidecut", "sidelock", "sidelocks", "skull", "snout", "stubble", "swept_bangs", "tails", "teeth", "third_eye", "twintails", "two_side_up", "undercut", "updo", "v-shaped_eyebrows", "whiskers", "tentacle_hair", ],
31
+ #手部/手部
32
+ "Hands" : ["_arm", "_arms", "claws", "_finger", "_fingers", "fingernails", "_hand", "_nail", "_nails", "palms", "rings", "thumbs_up", ],
33
+ #上半身/上半身
34
+ "Upper Body" : ["abs", "armpit", "armpits", "backboob", "belly", "biceps", "breast_rest", "breasts", "button_gap", "cleavage", "collarbone", "dimples_of_venus", "downblouse", "flat_chest", "linea_alba", "median_furrow", "midriff", "nape", "navel", "pectorals", "ribs", "_shoulder", "_shoulders", "shoulder_blades", "sideboob", "sidetail", "spine", "stomach", "strap_gap", "toned", "underboob", "underbust", ],
35
+ #下半身/下半身
36
+ "Lower Body" : ["ankles", "ass", "barefoot", "crotch", "feet", "highleg", "hip_bones", "hooves", "kneepits", "knees", "legs", "soles", "tail", "thigh_gap", "thighlet", "thighs", "toenail", "toenails", "toes", "wide_hips", ],
37
+ #生物/生物
38
+ "Creature" : ["(animal)", "anglerfish", "animal", "bear", "bee", "bird", "bug", "butterfly", "cat", "chick", "chicken", "chinese_zodiac", "clownfish", "coral", "crab", "creature", "crow", "dog", "dove", "dragon", "duck", "eagle", "fish", "fish", "fox", "fox", "frog", "frog", "goldfish", "hamster", "horse", "jellyfish", "ladybug", "lion", "mouse", "octopus", "owl", "panda", "penguin", "pig", "pigeon", "rabbit", "rooster", "seagull", "shark", "sheep", "shrimp", "snail", "snake", "squid", "starfish", "tanuki", "tentacles", "goo_tentacles", "plant_tentacles", "crotch_tentacles", "mechanical_tentacles", "squidward_tentacles", "suction_tentacles", "penis_tentacles", "translucent_tentacles", "back_tentacles", "red_tentacles", "green_tentacles", "blue_tentacles", "black_tentacles", "pink_tentacles", "purple_tentacles", "face_tentacles", "tentacles_everywhere", "milking_tentacles", "tiger", "turtle", "weasel", "whale", "wolf", "parrot", "sparrow", "unicorn", ],
39
+ #植物/植物
40
+ "Plant" : ["bamboo", "bouquet", "branch", "bush", "cherry_blossoms", "clover", "daisy", "(flower)", "flower", "flower", "gourd", "hibiscus", "holly", "hydrangea", "leaf", "lily_pad", "lotus", "moss", "palm_leaf", "palm_tree", "petals", "plant", "plum_blossoms", "rose", "spider_lily", "sunflower", "thorns", "tree", "tulip", "vines", "wisteria", "acorn", ],
41
+ #食べ物/食物
42
+ "Food" : ["apple", "baguette", "banana", "baozi", "beans", "bento", "berry", "blueberry", "bread", "broccoli", "burger", "cabbage", "cake", "candy", "carrot", "cheese", "cherry", "chili_pepper", "chocolate", "coconut", "cookie", "corn", "cream", "crepe", "cucumber", "cucumber", "cupcake", "curry", "dango", "dessert", "doughnut", "egg", "eggplant", "_(food)", "_(fruit)", "food", "french_fries", "fruit", "grapes", "ice_cream", "icing", "lemon", "lettuce", "lollipop", "macaron", "mandarin_orange", "meat", "melon", "mochi", "mushroom", "noodles", "omelet", "omurice", "onigiri", "onion", "pancake", "parfait", "pasties", "pastry", "peach", "pineapple", "pizza", "popsicle", "potato", "pudding", "pumpkin", "radish", "ramen", "raspberry", "rice", "roasted_sweet_potato", "sandwich", "sausage", "seaweed", "skewer", "spitroast", "spring_onion", "strawberry", "sushi", "sweet_potato", "sweets", "taiyaki", "takoyaki", "tamagoyaki", "tempurakanbea", "toast", "tomato", "vegetable", "wagashi", "wagashi", "watermelon", "jam", "popcorn", ],
43
+ #飲み物/飲品
44
+ "Beverage" : ["alcohol", "beer", "coffee", "cola", "drink", "juice", "juice_box", "milk", "sake", "soda", "tea", "_tea", "whiskey", "wine", "cocktail", ],
45
+ #音楽/音樂
46
+ "Music" : ["band", "baton_(conducting)", "beamed", "cello", "concert", "drum", "drumsticks", "eighth_note", "flute", "guitar", "harp", "horn", "(instrument)", "idol", "instrument", "k-pop", "lyre", "(music)", "megaphone", "microphone", "music", "musical_note", "phonograph", "piano", "plectrum", "quarter_note", "recorder", "sixteenth_note", "sound_effects", "trumpet", "utaite", "violin", "whistle", ],
47
+ #武器・装備/武器・裝備
48
+ "Weapons & Equipment" : ["ammunition", "arrow_(projectile)", "axe", "bandolier", "baseball_bat", "beretta_92", "bolt_action", "bomb", "bullet", "bullpup", "cannon", "chainsaw", "crossbow", "dagger", "energy_sword", "explosive", "fighter_jet", "gohei", "grenade", "gun", "hammer", "handgun", "holstered", "jet", "katana", "knife", "kunai", "lance", "mallet", "nata_(tool)", "polearm", "quiver", "rapier", "revolver", "rifle", "rocket_launcher", "scabbard", "scope", "scythe", "sheath", "sheathed", "shield", "shotgun", "shuriken", "spear", "staff", "suppressor", "sword", "tank", "tantou", "torpedo", "trident", "(weapon)", "wand", "weapon", "whip", "yumi_(bow)", "h&k_hk416", "rocket_launcher", "heckler_&_koch", "_weapon", ],
49
+ #乗り物/交通器具
50
+ "Vehicles" : ["aircraft", "airplane", "bicycle", "boat", "car", "caterpillar_tracks", "flight_deck", "helicopter", "motor_vehicle", "motorcycle", "ship", "spacecraft", "spoiler_(automobile)", "train", "truck", "watercraft", "wheel", "wheelbarrow", "wheelchair", "inflatable_raft", ],
51
+ #建物/建物
52
+ "Buildings" : ["apartment", "aquarium", "architecture", "balcony", "building", "cafe", "castle", "church", "gym", "hallway", "hospital", "house", "library", "(place)", "porch", "restaurant", "restroom", "rooftop", "shop", "skyscraper", "stadium", "stage", "temple", "toilet", "tower", "train_station", "veranda", ],
53
+ #室内/室內
54
+ "Indoor" : ["bath", "bathroom", "bathtub", "bed", "bed_sheet", "bedroom", "blanket", "bookshelf", "carpet", "ceiling", "chair", "chalkboard", "classroom", "counter", "cupboard", "curtains", "cushion", "dakimakura", "desk", "door", "doorway", "drawer", "_floor", "floor", "futon", "indoors", "interior", "kitchen", "kotatsu", "locker", "mirror", "pillow", "room", "rug", "school_desk", "shelf", "shouji", "sink", "sliding_doors", "stairs", "stool", "storeroom", "table", "tatami", "throne", "window", "windowsill", "bathhouse", "chest_of_drawers", ],
55
+ #屋外/室外
56
+ "Outdoor" : ["alley", "arch", "beach", "bridge", "bus_stop", "bush", "cave", "(city)", "city", "cliff", "crescent", "crosswalk", "day", "desert", "fence", "ferris_wheel", "field", "forest", "grass", "graveyard", "hill", "lake", "lamppost", "moon", "mountain", "night", "ocean", "onsen", "outdoors", "path", "pool", "poolside", "railing", "railroad", "river", "road", "rock", "sand", "shore", "sky", "smokestack", "snow", "snowball", "snowman", "street", "sun", "sunlight", "sunset", "tent", "torii", "town", "tree", "turret", "utility_pole", "valley", "village", "waterfall", ],
57
+ #物品/物品
58
+ "Objects" : ["anchor", "android", "armchair", "(bottle)", "backpack", "bag", "ball", "balloon", "bandages", "bandaid", "bandaids", "banknote", "banner", "barcode", "barrel", "baseball", "basket", "basketball", "beachball", "bell", "bench", "binoculars", "board_game", "bone", "book", "bottle", "bowl", "box", "box_art", "briefcase", "broom", "bucket", "(chess)", "(computer)", "(computing)", "(container)", "cage", "calligraphy_brush", "camera", "can", "candle", "candlestand", "cane", "card", "cartridge", "cellphone", "chain", "chandelier", "chess", "chess_piece", "choko_(cup)", "chopsticks", "cigar", "clipboard", "clock", "clothesline", "coin", "comb", "computer", "condom", "controller", "cosmetics", "couch", "cowbell", "crazy_straw", "cup", "cutting_board", "dice", "digital_media_player", "doll", "drawing_tablet", "drinking_straw", "easel", "electric_fan", "emblem", "envelope", "eraser", "feathers", "figure", "fire", "fishing_rod", "flag", "flask", "folding_fan", "fork", "frying_pan", "(gemstone)", "game_console", "gears", "gemstone", "gift", "glass", "glowstick", "gold", "handbag", "handcuffs", "handheld_game_console", "hose", "id_card", "innertube", "iphone", "jack-o'-lantern", "jar", "joystick", "key", "keychain", "kiseru", "ladder", "ladle", "lamp", "lantern", "laptop", "letter", "letterboxed", "lifebuoy", "lipstick", "liquid", "lock", "lotion", "_machine", "map", "marker", "model_kit", "money", "monitor", "mop", "mug", "needle", "newspaper", "nintendo", "nintendo_switch", "notebook", "(object)", "ofuda", "orb", "origami", "(playing_card)", "pack", "paddle", "paintbrush", "pan", "paper", "parasol", "patch", "pc", "pen", "pencil", "pencil", "pendant_watch", "phone", "pill", "pinwheel", "plate", "playstation", "pocket_watch", "pointer", "poke_ball", "pole", "quill", "racket", "randoseru", "remote_control", "ring", "rope", "sack", "saddle", "sakazuki", "satchel", "saucer", "scissors", "scroll", "seashell", "seatbelt", "shell", "shide", "shopping_cart", "shovel", "shower_head", "silk", "sketchbook", "smartphone", "soap", "sparkler", "spatula", "speaker", "spoon", "statue", "stethoscope", "stick", "sticker", "stopwatch", "string", "stuffed_", "stylus", "suction_cups", "suitcase", "surfboard", "syringe", "talisman", "tanzaku", "tape", "teacup", "teapot", "teddy_bear", "television", "test_tube", "tiles", "tokkuri", "tombstone", "torch", "towel", "toy", "traffic_cone", "tray", "treasure_chest", "uchiwa", "umbrella", "vase", "vial", "video_game", "viewfinder", "volleyball", "wallet", "watch", "watch", "whisk", "whiteboard", "wreath", "wrench", "wristwatch", "yunomi", "ace_of_hearts", "inkwell", "compass", "ipod", "sunscreen", "rocket", "cobblestone", ],
59
+ #キャラクター設定/角色設定
60
+ "Character Design" : ["+boys", "+girls", "1other", "39", "_boys", "_challenge", "_connection", "_female", "_fur", "_girls", "_interface", "_male", "_man", "_person", "abyssal_ship", "age_difference", "aged_down", "aged_up", "albino", "alien", "alternate_muscle_size", "ambiguous_gender", "amputee", "androgynous", "angel", "animalization", "ass-to-ass", "assault_visor", "au_ra", "baby", "bartender", "beak", "bishounen", "borrowed_character", "boxers", "boy", "breast_envy", "breathing_fire", "bride", "broken", "brother_and_sister", "brothers", "camouflage", "cheating_(relationship)", "cheerleader", "chibi", "child", "clone", "command_spell", "comparison", "contemporary", "corpse", "corruption", "cosplay", "couple", "creature_and_personification", "crossdressing", "crossover", "cyberpunk", "cyborg", "cyclops", "damaged", "dancer", "danmaku", "darkness", "death", "defeat", "demon", "disembodied_", "draph", "drone", "duel", "dwarf", "egyptian", "electricity", "elezen", "elf", "enmaided", "erune", "everyone", "evolutionary_line", "expressions", "fairy", "family", "fangs", "fantasy", "fashion", "fat", "father_and_daughter", "father_and_son", "fewer_digits", "fins", "flashback", "fluffy", "fumo_(doll)", "furry", "fusion", "fuuin_no_tsue", "gameplay_mechanics", "genderswap", "ghost", "giant", "giantess", "gibson_les_paul", "girl", "goblin", "groom", "guro", "gyaru", "habit", "harem", "harpy", "harvin", "heads_together", "health_bar", "height_difference", "hitodama", "horror_(theme)", "humanization", "husband_and_wife", "hydrokinesis", "hypnosis", "hyur", "idol", "insignia", "instant_loss", "interracial", "interspecies", "japari_bun", "jeweled_branch_of_hourai", "jiangshi", "jirai_kei", "joints", "karakasa_obake", "keyhole", "kitsune", "knight", "kodona", "kogal", "kyuubi", "lamia", "left-handed", "loli", "lolita", "look-alike", "machinery", "magic", "male_focus", "manly", "matching_outfits", "mature_female", "mecha", "mermaid", "meta", "miko", "milestone_celebration", "military", "mind_control", "miniboy", "minigirl", "miqo'te", "monster", "monsterification", "mother_and_daughter", "mother_and_son", "multiple_others", "muscular", "nanodesu_(phrase)", "narrow_waist", "nekomata", "netorare", "ninja", "no_humans", "nontraditional", "nun", "nurse", "object_namesake", "obliques", "office_lady", "old", "on_body", "onee-shota", "oni", "orc", "others", "otoko_no_ko", "oversized_object", "paint_splatter", "pantyshot", "pawpads", "persona", "personality", "personification", "pet_play", "petite", "pirate", "playboy_bunny", "player_2", "plugsuit", "plump", "poi", "pokemon", "police", "policewoman", "pom_pom_(cheerleading)", "princess", "prosthesis", "pun", "puppet", "race_queen", "radio_antenna", "real_life_insert", "redesign", "reverse_trap", "rigging", "robot", "rod_of_remorse", "sailor", "salaryman", "samurai", "sangvis_ferri", "scales", "scene_reference", "school", "sheikah", "shota", "shrine", "siblings", "side-by-side", "sidesaddle", "sisters", "size_difference", "skeleton", "skinny", "slave", "slime_(substance)", "soldier", "spiked_shell", "spokencharacter", "steampunk", "streetwear", "striker_unit", "strongman", "submerged", "suggestive", "super_saiyan", "superhero", "surreal", "take_your_pick", "tall", "talons", "taur", "teacher", "team_rocket", "three-dimensional_maneuver_gear", "time_paradox", "tomboy", "traditional_youkai", "transformation", "trick_or_treat", "tusks", "twins", "ufo", "under_covers", "v-fin", "v-fin", "vampire", "virtual_youtuber", "waitress", "watching_television", "wedding", "what", "when_you_see_it", "wife_and_wife", "wing", "wings", "witch", "world_war_ii", "yandere", "year_of", "yes", "yin_yang", "yordle", "you're_doing_it_wrong", "you_gonna_get_raped", "yukkuri_shiteitte_ne", "yuri", "zombie", "(alice_in_wonderland)", "(arknights)", "(blue_archive)", "(cosplay)", "(creature)", "(emblem)", "(evangelion)", "(fate)", "(fate/stay_night)", "(ff11)", "(fire_emblem)", "(genshin_impact)", "(grimm)", "(houseki_no_kuni)", "(hyouka)", "(idolmaster)", "(jojo)", "(kancolle)", "(kantai_collection)", "(kill_la_kill)", "(league_of_legends)", "(legends)", "(lyomsnpmp)", "(machimazo)", "(madoka_magica)", "(mecha)", "(meme)", "(nier:automata)", "(organ)", "(overwatch)", "(pokemon)", "(project_moon)", "(project_sekai)", "(sao)", "(senran_kagura)", "(splatoon)", "(touhou)", "(tsukumo_sana)", "(youkai_watch)", "(yu-gi-oh!_gx)", "(zelda)", "sextuplets", "imperial_japanese_army", "extra_faces", "_miku", ],
61
+ #構図/構圖
62
+ "Composition" : ["abstract", "anime_coloring", "animification", "back-to-back", "bad_anatomy", "blurry", "border", "bound", "cameo", "cheek-to-cheek", "chromatic_aberration", "close-up", "collage", "color_guide", "colorful", "comic", "contrapposto", "cover", "cowboy_shot", "crosshatching", "depth_of_field", "dominatrix", "dutch_angle", "_focus", "face-to-face", "fake_screenshot", "film_grain", "fisheye", "flat_color", "foreshortening", "from_above", "from_behind", "from_below", "from_side", "full_body", "glitch", "greyscale", "halftone", "head_only", "heads-up_display", "high_contrast", "horizon", "_inset", "inset", "jaggy_lines", "1koma", "2koma", "3koma", "4koma", "5koma", "leaning", "leaning_forward", "leaning_to_the_side", "left-to-right_manga", "lens_flare", "limited_palette", "lineart", "lineup", "lower_body", "(medium)", "marker_(medium)", "meme", "mixed_media", "monochrome", "multiple_views", "muted_color", "oekaki", "on_side", "out_of_frame", "outline", "painting", "parody", "partially_colored", "partially_underwater_shot", "perspective", "photorealistic", "picture_frame", "pillarboxed", "portrait", "poster_(object)", "product_placement", "profile", "realistic", "recording", "retro_artstyle", "(style)", "_style", "sandwiched", "science_fiction", "sepia", "shikishi", "side-by-side", "sideways", "sideways_glance", "silhouette", "sketch", "spot_color", "still_life", "straight-on", "symmetry", "(texture)", "tachi-e", "taking_picture", "tegaki", "too_many", "traditional_media", "turnaround", "underwater", "upper_body", "upside-down", "upskirt", "variations", "wide_shot", "_design", "symbolism", "rounded_corners", "surrounded", ],
63
+ #季節/季節
64
+ "Season" : ["akeome", "anniversary", "autumn", "birthday", "christmas", "_day", "festival", "halloween", "kotoyoro", "nengajou", "new_year", "spring_(season)", "summer", "tanabata", "valentine", "winter", ],
65
+ #背景/背景
66
+ "Background" : ["_background", "backlighting", "bloom", "bokeh", "brick_wall", "bubble", "cable", "caustics", "cityscape", "cloud", "confetti", "constellation", "contrail", "crowd", "crystal", "dark", "debris", "dusk", "dust", "egasumi", "embers", "emphasis_lines", "energy", "evening", "explosion", "fireworks", "fog", "footprints", "glint", "graffiti", "ice", "industrial_pipe", "landscape", "light", "light_particles", "light_rays", "lightning", "lights", "moonlight", "motion_blur", "motion_lines", "mountainous_horizon", "nature", "(planet)", "pagoda", "people", "pillar", "planet", "power_lines", "puddle", "rain", "rainbow", "reflection", "ripples", "rubble", "ruins", "scenery", "shade", "shooting_star", "sidelighting", "smoke", "snowflakes", "snowing", "space", "sparkle", "sparks", "speed_lines", "spider_web", "spotlight", "star_(sky)", "stone_wall", "sunbeam", "sunburst", "sunrise", "_theme", "tile_wall", "twilight", "wall_clock", "wall_of_text", "water", "waves", "wind", "wire", "wooden_wall", "lighthouse", ],
67
+ # パターン/圖案
68
+ "Patterns" : ["arrow", "bass_clef", "blank_censor", "circle", "cube", "heart", "hexagon", "hexagram", "light_censor", "(pattern)", "pattern", "pentagram", "roman_numeral", "(shape)", "(symbol)", "shape", "sign", "symbol", "tally", "treble_clef", "triangle", "tube", "yagasuri", ],
69
+ #検閲/審查
70
+ "Censorship" : ["blur_censor", "_censor", "_censoring", "censored", "character_censor", "convenient", "hair_censor", "heart_censor", "identity_censor", "maebari", "novelty_censor", "soap_censor", "steam_censor", "tail_censor", "uncensored", ],
71
+ #その他/其他
72
+ "Others" : ["2007", "2008", "2009", "2010", "2011", "2012", "2013", "2014", "2015", "2016", "2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024", "artist", "artist_name", "artistic_error", "asian", "(company)", "character_name", "content_rating", "copyright", "cover_page", "dated", "english_text", "japan", "layer", "logo", "name", "numbered", "page_number", "pixiv_id", "ranguage", "reference_sheet", "signature", "speech_bubble", "subtitled", "text", "thank_you", "typo", "username", "wallpaper", "watermark", "web_address", "screwdriver", "translated", ],
73
+ "Quality Tags" : ["masterpiece", "_quality", "highres", "absurdres", "ultra-detailed", "lowres", ],
74
+ }
75
+
76
+ reversed_categories = {value: key for key, values in categories.items() for value in values}
77
+
78
+ # Precompute keyword lengths
79
+ keyword_lengths = {keyword: len(keyword) for keyword in reversed_categories}
80
+
81
+ # Trie for efficient keyword matching
82
+ class TrieNode:
83
+ def __init__(self):
84
+ self.children = {}
85
+ self.category = None
86
+
87
+ def build_trie(keywords):
88
+ root = TrieNode()
89
+ for keyword, category in reversed_categories.items():
90
+ node = root
91
+ for char in keyword:
92
+ if char not in node.children:
93
+ node.children[char] = TrieNode()
94
+ node = node.children[char]
95
+ node.category = category
96
+ return root
97
+
98
+ trie_root = build_trie(reversed_categories)
99
+
100
+ def find_category(trie_root, tag):
101
+ node = trie_root
102
+ for char in tag:
103
+ if char in node.children:
104
+ node = node.children[char]
105
+ if node.category:
106
+ return node.category
107
+ else:
108
+ break
109
+ return None
110
+
111
+ def classify_tags(tags: list[str], local_test: bool = False):
112
+ # Dictionary for automatic classification
113
+ classified_tags: defaultdict[str, list] = defaultdict(list)
114
+ fuzzy_match_tags: defaultdict[str, list] = defaultdict(list)
115
+ unclassified_tags: list[str] = []
116
+
117
+ # Logic for automatic grouping
118
+ for tag in tags:
119
+ classified = False
120
+ tag_new = tag.replace(" ", "_").replace("-", "_").replace("\\(", "(").replace("\\)", ")") # Replace spaces in source tags with underscores
121
+
122
+ # Exact match using the trie
123
+ category = find_category(trie_root, tag_new)
124
+ if category:
125
+ classified = True
126
+ else:
127
+ # Fuzzy match
128
+ tag_parts = tag_new.split("_")
129
+ for keyword, keyword_length in keyword_lengths.items():
130
+ if keyword in tag_new and keyword_length > 3: # Adjust the threshold if needed
131
+ classified = True
132
+ category = reversed_categories[keyword]
133
+ break
134
+
135
+ if classified and tag not in classified_tags[category]: # Avoid duplicates
136
+ classified_tags[category].append(tag)
137
+ elif not classified and tag not in unclassified_tags:
138
+ unclassified_tags.append(tag) # Unclassified tags
139
+
140
+ if local_test:
141
+ # Output the grouping result
142
+ for category, tags in classified_tags.items():
143
+ print(f"{category}:")
144
+ print(", ".join(tags))
145
+ print()
146
+
147
+ print()
148
+ print("Fuzzy match:")
149
+ for category, tags in fuzzy_match_tags.items():
150
+ print(f"{category}:")
151
+ print(", ".join(tags))
152
+ print()
153
+ print()
154
+
155
+ if len(unclassified_tags) > 0:
156
+ print(f"\nUnclassified tags: {len(unclassified_tags)}")
157
+ print(f"{unclassified_tags[:200]}") # Display some unclassified tags
158
+
159
+ return classified_tags, unclassified_tags
160
+
161
+ # Code for "Tag Categorizer" tab
162
+ def process_tags(input_tags: str):
163
+ # Split tags using regex to handle both commas and question marks
164
+ tags = []
165
+ for tag in re.split(r'\?|,|\n', input_tags):
166
+ tag = tag.strip()
167
+ if tag:
168
+ # Remove numbers at the end of tags
169
+ tag = re.sub(r'\b\d+\b', '', tag).strip()
170
+
171
+ # Replace underscores with spaces
172
+ tag = tag.replace('_', ' ')
173
+
174
+ # Escape parentheses (handle both escaped and unescaped)
175
+ if '(' in tag or ')' in tag:
176
+ # First, replace existing backslashes to handle properly
177
+ tag = tag.replace('\\', '')
178
+ # Replace parentheses with escaped versions
179
+ tag = tag.replace('(', r'\(').replace(')', r'\)')
180
+
181
+ if tag: # Only add if tag is not empty after processing
182
+ tags.append(tag)
183
+
184
+ # Classify the cleaned tags
185
+ classified_tags, unclassified_tags = classify_tags(tags)
186
+
187
+ # Create the outputs
188
+ categorized_string = ', '.join([tag for category in classified_tags.values() for tag in category])
189
+ categorized_json = {category: tags for category, tags in classified_tags.items()}
190
+
191
+ return categorized_string, categorized_json, "" # Initialize enhanced_prompt as empty