import os, io, json, requests, argparse, traceback, tempfile, zipfile, re, ast, time from datetime import datetime, timezone from collections import defaultdict from PIL import Image, ImageOps import onnxruntime as ort import gradio as gr import pandas as pd import numpy as np # Modules: from modules.classifyTags import categorize_tags_output, generate_tags_json, process_tags_for_misc from modules.pixai import create_pixai_interface from modules.media_handler import handle_single_media_upload, handle_multiple_media_uploads from modules.convert_tags import generate_response, SYSTEM_PROMPTS from modules.multi_comfy import create_multi_comfy TITLE = 'Multi-Tagger v1.5' DESCRIPTION = '\nMulti-Tagger is a versatile application for advanced image analysis and captioning. Supports CUDA and CPU.\n\nSupports CLI mode! Run python app.py -h to learn how to use.' SWINV2_MODEL_DSV3_REPO = 'SmilingWolf/wd-swinv2-tagger-v3' CONV_MODEL_DSV3_REPO = 'SmilingWolf/wd-convnext-tagger-v3' VIT_MODEL_DSV3_REPO = 'SmilingWolf/wd-vit-tagger-v3' VIT_LARGE_MODEL_DSV3_REPO = 'SmilingWolf/wd-vit-large-tagger-v3' EVA02_LARGE_MODEL_DSV3_REPO = 'SmilingWolf/wd-eva02-large-tagger-v3' MOAT_MODEL_DSV2_REPO = 'SmilingWolf/wd-v1-4-moat-tagger-v2' SWIN_MODEL_DSV2_REPO = 'SmilingWolf/wd-v1-4-swinv2-tagger-v2' CONV_MODEL_DSV2_REPO = 'SmilingWolf/wd-v1-4-convnext-tagger-v2' CONV2_MODEL_DSV2_REPO = 'SmilingWolf/wd-v1-4-convnextv2-tagger-v2' VIT_MODEL_DSV2_REPO = 'SmilingWolf/wd-v1-4-vit-tagger-v2' EVA02_LARGE_MODEL_IS_DSV1_REPO = 'deepghs/idolsankaku-eva02-large-tagger-v1' SWINV2_MODEL_IS_DSV1_REPO = 'deepghs/idolsankaku-swinv2-tagger-v1' # Global variables for model components (for memory management) CURRENT_MODEL = None CURRENT_MODEL_NAME = None CURRENT_TAGS_DF = None CURRENT_TAG_NAMES = None CURRENT_RATING_INDEXES = None CURRENT_GENERAL_INDEXES = None CURRENT_CHARACTER_INDEXES = None CURRENT_MODEL_TARGET_SIZE = None # Custom CSS for gallery styling css = """ #custom-gallery {--row-height: 180px;display: grid;grid-auto-rows: min-content;gap: 10px;} #custom-gallery .thumbnail-item {height: var(--row-height);width: 100%;position: relative;overflow: hidden;border-radius: 8px;box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);transition: transform 0.2s ease, box-shadow 0.2s ease;} #custom-gallery .thumbnail-item:hover {transform: translateY(-3px);box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);} #custom-gallery .thumbnail-item img {width: auto;height: 100%;max-width: 100%;max-height: var(--row-height);object-fit: contain;margin: 0 auto;display: block;} #custom-gallery .thumbnail-item img.portrait {max-width: 100%;} #custom-gallery .thumbnail-item img.landscape {max-height: 100%;} .gallery-container {max-height: 500px;overflow-y: auto;padding-right: 0px;--size-80: 500px;} .thumbnails {display: flex;position: absolute;bottom: 0;width: 120px;overflow-x: scroll;padding-top: 320px;padding-bottom: 280px;padding-left: 4px;flex-wrap: wrap;} #custom-gallery .thumbnail-item img {width: auto;height: 100%;max-width: 100%;max-height: var(--row-height);object-fit: initial;width: fit-content;margin: 0px auto;display: block;} code {background: rgba(192, 132, 252, 0.15); padding: 0.15rem 0.5rem; border-radius: 6px; font-size: 0.9rem;color: #f0d9ff;} """ MODEL_FILENAME = 'model.onnx' LABEL_FILENAME = 'selected_tags.csv' class Timer: """Utility class for measuring execution time of different operations""" def __init__(self): self.start_time = time.perf_counter() self.checkpoints = [('Start', self.start_time)] def checkpoint(self, label='Checkpoint'): """Add a checkpoint with a label""" now = time.perf_counter() self.checkpoints.append((label, now)) def report(self, is_clear_checkpoints=True): """Report time elapsed since last checkpoint""" max_label_length = max(len(label) for (label, _) in self.checkpoints) if self.checkpoints else 0 prev_time = self.checkpoints[0][1] if self.checkpoints else self.start_time 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 if is_clear_checkpoints: self.checkpoints.clear() self.checkpoint() def report_all(self): """Report all checkpoint times including total execution time""" 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 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 total_time = self.checkpoints[-1][1] - self.start_time if self.checkpoints else 0 print(f"{'Total Execution Time'.ljust(max_label_length)}: {total_time:.3f} seconds\n") self.checkpoints.clear() def restart(self): """Restart the timer""" self.start_time = time.perf_counter() self.checkpoints = [('Start', self.start_time)] def parse_args() -> argparse.Namespace: """Parse command line arguments""" parser = argparse.ArgumentParser(description='Multi-Tagger - Image analysis and captioning') # Gradio arguments parser.add_argument('--score-slider-step', type=float, default=0.05) parser.add_argument('--score-general-threshold', type=float, default=0.35) parser.add_argument('--score-character-threshold', type=float, default=0.85) parser.add_argument('--share', action='store_true') # CLI analysis arguments (BETA) parser.add_argument('--analyze', '-a', nargs='+', metavar='PATH_OR_URL', help='Analyze images from paths or URLs. Skips Gradio launch.') parser.add_argument('--output', '-o', type=str, default='output', help='Output directory for tag files and downloaded images (default: output)') parser.add_argument('--silent', '-s', action='store_true', help='Run silently without printing results.') parser.add_argument('--ct', action='store_true', help='Print only character tags') parser.add_argument('--cttx', action='store_true', help='Print only categorized list') parser.add_argument('--model', type=str, default=EVA02_LARGE_MODEL_DSV3_REPO, help='WD model repository to use for analysis') parser.add_argument('--general-threshold', type=float, default=0.35, help='General tags threshold (default: 0.35)') parser.add_argument('--character-threshold', type=float, default=0.85, help='Character tags threshold (default: 0.85)') return parser.parse_args() def load_labels(dataframe) -> tuple: """Load tag names and their category indexes from the dataframe""" name_series = dataframe['name'] tag_names = name_series.tolist() # Find indexes for different tag categories 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 def mcut_threshold(probs): """Calculate threshold using Maximum Change in second derivative (MCut) method""" 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 def _download_model_files(model_repo): """Download model files from HuggingFace Hub""" import huggingface_hub csv_path = huggingface_hub.hf_hub_download(model_repo, LABEL_FILENAME) model_path = huggingface_hub.hf_hub_download(model_repo, MODEL_FILENAME) return csv_path, model_path def create_optimized_ort_session(model_path): """Create an optimized ONNX Runtime session with GPU support (PixAI optimized version)""" # Session options for better performance sess_options = ort.SessionOptions() sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess_options.intra_op_num_threads = 0 # Use all available cores sess_options.execution_mode = ort.ExecutionMode.ORT_PARALLEL sess_options.enable_mem_pattern = True sess_options.enable_cpu_mem_arena = True # Check available providers available_providers = ort.get_available_providers() print(f"Available ONNX Runtime providers: {available_providers}") # Use appropriate execution providers (in order of preference) providers = [] # Use CUDA if available with optimized settings if 'CUDAExecutionProvider' in available_providers: cuda_provider = ('CUDAExecutionProvider', { 'device_id': 0, 'arena_extend_strategy': 'kNextPowerOfTwo', 'gpu_mem_limit': 4 * 1024 * 1024 * 1024, # 4GB VRAM 'cudnn_conv_algo_search': 'EXHAUSTIVE', 'do_copy_in_default_stream': True, }) providers.append(cuda_provider) print("Using CUDA provider for ONNX inference") else: print("CUDA provider not available, falling back to CPU") # Always include CPU as fallback providers.append('CPUExecutionProvider') try: session = ort.InferenceSession(model_path, sess_options, providers=providers) print(f"Model loaded with providers: {session.get_providers()}") return session except Exception as e: print(f"Failed to create ONNX session: {e}") raise def _load_model_components_optimized(model_repo): """Load and optimize model components""" global CURRENT_MODEL, CURRENT_MODEL_NAME, CURRENT_TAGS_DF, CURRENT_TAG_NAMES global CURRENT_RATING_INDEXES, CURRENT_GENERAL_INDEXES, CURRENT_CHARACTER_INDEXES, CURRENT_MODEL_TARGET_SIZE # Only reload if model changed if model_repo == CURRENT_MODEL_NAME and CURRENT_MODEL is not None: return # Download files csv_path, model_path = _download_model_files(model_repo) # Load optimized ONNX model CURRENT_MODEL = create_optimized_ort_session(model_path) # Load tags tags_df = pd.read_csv(csv_path) tag_names, rating_indexes, general_indexes, character_indexes = load_labels(tags_df) # Store in global variables CURRENT_TAGS_DF = tags_df CURRENT_TAG_NAMES = tag_names CURRENT_RATING_INDEXES = rating_indexes CURRENT_GENERAL_INDEXES = general_indexes CURRENT_CHARACTER_INDEXES = character_indexes # Get model input size _, height, width, _ = CURRENT_MODEL.get_inputs()[0].shape CURRENT_MODEL_TARGET_SIZE = height CURRENT_MODEL_NAME = model_repo def _raw_predict(image_array, model_session): """Run raw prediction using the model session""" input_name = model_session.get_inputs()[0].name label_name = model_session.get_outputs()[0].name preds = model_session.run([label_name], {input_name: image_array})[0] return preds[0].astype(float) def unload_model(): """Explicitly unload the current model from memory""" global CURRENT_MODEL, CURRENT_MODEL_NAME global CURRENT_RATING_INDEXES, CURRENT_GENERAL_INDEXES, CURRENT_CHARACTER_INDEXES, CURRENT_MODEL_TARGET_SIZE # Delete the model session if CURRENT_MODEL is not None: del CURRENT_MODEL CURRENT_MODEL = None # Clear other large objects CURRENT_TAGS_DF = None CURRENT_TAG_NAMES = None CURRENT_RATING_INDEXES = None CURRENT_GENERAL_INDEXES = None CURRENT_CHARACTER_INDEXES = None CURRENT_MODEL_TARGET_SIZE = None CURRENT_MODEL_NAME = None # Force garbage collection import gc gc.collect() # Clear CUDA cache if using GPU try: import torch if torch.cuda.is_available(): torch.cuda.empty_cache() print('VRAM is cleared!') except ImportError: pass def prepare_image(path): print('Preparing image for model input') image = Image.open(path) image = image.convert('RGBA') target_size = CURRENT_MODEL_TARGET_SIZE # Create white background and composite canvas = Image.new('RGBA', image.size, (255, 255, 255)) canvas.alpha_composite(image) image = canvas.convert('RGB') # Pad to square image_shape = image.size max_dim = max(image_shape) pad_left = (max_dim - image_shape[0]) // 2 pad_top = (max_dim - image_shape[1]) // 2 padded_image = Image.new('RGB', (max_dim, max_dim), (255, 255, 255)) padded_image.paste(image, (pad_left, pad_top)) # Resize if needed if max_dim != target_size: padded_image = padded_image.resize((target_size, target_size), Image.BICUBIC) # Convert to array and preprocess image_array = np.asarray(padded_image, dtype=np.float32) image_array = image_array[:, :, ::-1] # BGR to RGB return np.expand_dims(image_array, axis=0) def create_file(content: str, directory: str, fileName: str) -> str: """Creating a file with the given content""" file_path = os.path.join(directory, fileName) if fileName.endswith('.json'): with open(file_path, 'w', encoding='utf-8') as file: file.write(content) else: with open(file_path, 'w+', encoding='utf-8') as file: file.write(content) return file_path def download_image_from_url(url, output_dir): """Download an image from a URL and return the local path""" try: response = requests.get(url, timeout=30, stream=True) response.raise_for_status() # Get filename from URL or generate one filename = os.path.basename(url.split('?')[0]) if not filename or '.' not in filename: from datetime import datetime filename = f"downloaded_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" filepath = os.path.join(output_dir, filename) with open(filepath, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) print(f"Downloaded: {url} -> {filepath}") return filepath except Exception as e: print(f"Failed to download {url}: {e}") return None def run_cli_analysis(args): """Run CLI-based image analysis""" if not args.analyze: return # Ensure output directory exists output_dir = args.output if args.output else './output' os.makedirs(output_dir, exist_ok=True) # Process each input - could be a URL or local path image_paths = [] for item in args.analyze: # Check if it's a URL if item.startswith(('http://', 'https://')): path = download_image_from_url(item, output_dir) if path: image_paths.append(path) else: # Local file path - handle glob patterns if os.path.isfile(item): image_paths.append(item) elif os.path.isdir(item): # If it's a directory, find all images for root, dirs, files in os.walk(item): for f in files: if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.bmp', '.gif')): image_paths.append(os.path.join(root, f)) else: print(f"Warning: {item} is not a valid file or URL") if not image_paths: print("No valid images found to analyze.") return print(f"Analyzing {len(image_paths)} image(s)...") try: timer = Timer() timer.checkpoint("Start analysis") # Prepare gallery format gallery = [(p, os.path.basename(p)) for p in image_paths] # Load model _load_model_components_optimized(args.model) timer.checkpoint("Model loaded") name_counters = defaultdict(int) results = [] for idx, (image_path, _) in enumerate(gallery): try: image_name = os.path.splitext(os.path.basename(image_path))[0] name_counters[image_name] += 1 if name_counters[image_name] > 1: image_name = f"{image_name}_{name_counters[image_name]:02d}" # Prepare and predict image = prepare_image(image_path) preds = _raw_predict(image, CURRENT_MODEL) labels = list(zip(CURRENT_TAG_NAMES, preds)) # Process ratings ratings_names = [labels[i] for i in CURRENT_RATING_INDEXES] rating = dict(ratings_names) # Process general tags general_names = [labels[i] for i in CURRENT_GENERAL_INDEXES] general_thresh = args.general_threshold general_res = [x for x in general_names if x[1] > general_thresh] general_res = dict(general_res) # Process character tags character_names = [labels[i] for i in CURRENT_CHARACTER_INDEXES] character_thresh = args.character_threshold character_res = [x for x in character_names if x[1] > character_thresh] character_res = dict(character_res) # Sort sorted_general_list = sorted(general_res.items(), key=lambda x: x[1], reverse=True) sorted_general_list = [x[0] for x in sorted_general_list] character_list = list(character_res.keys()) # Format output combined_list = character_list + sorted_general_list sorted_strings = ', '.join(combined_list).replace('(', '\\(').replace(')', '\\)').replace('_', ' ') # Categorized output categorized_strings = categorize_tags_output(sorted_strings, character_res).replace('(', '\\(').replace(')', '\\)') categorized_j = generate_tags_json(sorted_strings, character_res) categorized_text = "\n ".join(f"{k}: {', '.join(v)}" for k, v in categorized_j.items()) result = { 'image': image_name, 'rating': rating, 'character_tags': character_list, 'general_tags': sorted_general_list, 'tags_string': sorted_strings, 'categorized_strings': categorized_strings, 'categorized_text': categorized_text, 'categorized_json': categorized_j } results.append(result) # Save files if output dir specified create_file(sorted_strings, output_dir, f"{image_name}.txt") create_file(categorized_strings, output_dir, f"{image_name}_categorized.txt") # Save JSON json_content = json.dumps(result, indent=2, ensure_ascii=False) create_file(json_content, output_dir, f"{image_name}_result.json") timer.checkpoint(f"Image {idx+1}: {image_name}") except Exception as e: print(f"Error processing {image_path}: {e}") import traceback traceback.print_exc() continue timer.report_all() # Print results unless silent if not args.silent: for r in results: print(f"\n{'='*60}") print(f"Image: {r['image']}") print(f"Rating: {r['rating']}") if args.ct: print(f"Character Tags: {', '.join(r['character_tags'])}") elif args.cttx: print(f"Categorized Output:") for cat, tags in r['categorized_json'].items(): print(f" {cat}: {', '.join(tags)}") else: print(f"Character Tags: {', '.join(r['character_tags'])}") print(f"General Tags: {', '.join(r['general_tags'])}") print(f"Tags String: {r['tags_string']}") print(f"Categorized: {r['categorized_strings']}") print(f"Categorized list:\n{r['categorized_text']}") print(f"{'='*60}\n") print(f"\nResults saved to: {os.path.abspath(output_dir)}") finally: # Clean VRAM unload_model() import gc gc.collect() def predict(gallery, model_repo, model_repo_2, general_thresh, general_mcut_enabled, character_thresh, character_mcut_enabled, characters_merge_enabled, additional_tags_prepend, additional_tags_append, tag_results, progress=gr.Progress()): """Main prediction function for processing images""" tag_results.clear() gallery_len = len(gallery) print(f"Predict load model: {model_repo}, gallery length: {gallery_len}") timer = Timer() progressRatio = 1 progressTotal = gallery_len + 1 current_progress = 0 txt_infos = [] output_dir = tempfile.mkdtemp() if not os.path.exists(output_dir): os.makedirs(output_dir) # Load initial model _load_model_components_optimized(model_repo) current_progress += progressRatio / progressTotal progress(current_progress, desc='Initialize WD model finished') timer.checkpoint("Initialize WD model") timer.report() name_counters = defaultdict(int) for (idx, value) in enumerate(gallery): try: # Handle duplicate filenames image_path = value[0] image_name = os.path.splitext(os.path.basename(image_path))[0] name_counters[image_name] += 1 if name_counters[image_name] > 1: image_name = f"{image_name}_{name_counters[image_name]:02d}" ## Prepare image image = prepare_image(image_path) print(f"Gallery {idx:02d}: Starting run first model ({model_repo})...") ## Load and run first model _load_model_components_optimized(model_repo) preds = _raw_predict(image, CURRENT_MODEL) labels = list(zip(CURRENT_TAG_NAMES, preds)) # Process ratings ratings_names = [labels[i] for i in CURRENT_RATING_INDEXES] rating = dict(ratings_names) # Process general tags general_names = [labels[i] for i in CURRENT_GENERAL_INDEXES] if general_mcut_enabled: general_probs = np.array([x[1] for x in general_names]) general_thresh_temp = mcut_threshold(general_probs) else: general_thresh_temp = general_thresh general_res = [x for x in general_names if x[1] > general_thresh_temp] general_res = dict(general_res) # Process character tags character_names = [labels[i] for i in CURRENT_CHARACTER_INDEXES] if character_mcut_enabled: character_probs = np.array([x[1] for x in character_names]) character_thresh_temp = mcut_threshold(character_probs) character_thresh_temp = max(0.15, character_thresh_temp) else: character_thresh_temp = character_thresh character_res = [x for x in character_names if x[1] > character_thresh_temp] character_res = dict(character_res) character_list_1 = list(character_res.keys()) # Sort general tags by confidence sorted_general_list_1 = sorted(general_res.items(), key=lambda x: x[1], reverse=True) sorted_general_list_1 = [x[0] for x in sorted_general_list_1] # Handle second model if provided if model_repo_2 and model_repo_2 != model_repo: print(f"Gallery {idx:02d}: Starting run second model ({model_repo_2})...") _load_model_components_optimized(model_repo_2) preds_2 = _raw_predict(image, CURRENT_MODEL) labels_2 = list(zip(CURRENT_TAG_NAMES, preds_2)) # Process general tags from second model general_names_2 = [labels_2[i] for i in CURRENT_GENERAL_INDEXES] if general_mcut_enabled: general_probs_2 = np.array([x[1] for x in general_names_2]) general_thresh_temp_2 = mcut_threshold(general_probs_2) else: general_thresh_temp_2 = general_thresh general_res_2 = [x for x in general_names_2 if x[1] > general_thresh_temp_2] general_res_2 = dict(general_res_2) # Process character tags from second model character_names_2 = [labels_2[i] for i in CURRENT_CHARACTER_INDEXES] if character_mcut_enabled: character_probs_2 = np.array([x[1] for x in character_names_2]) character_thresh_temp_2 = mcut_threshold(character_probs_2) character_thresh_temp_2 = max(0.15, character_thresh_temp_2) else: character_thresh_temp_2 = character_thresh character_res_2 = [x for x in character_names_2 if x[1] > character_thresh_temp_2] character_res_2 = dict(character_res_2) character_list_2 = list(character_res_2.keys()) # Sort general tags from second model sorted_general_list_2 = sorted(general_res_2.items(), key=lambda x: x[1], reverse=True) sorted_general_list_2 = [x[0] for x in sorted_general_list_2] # Combine results from both models combined_character_list = list(set(character_list_1 + character_list_2)) combined_general_list = list(set(sorted_general_list_1 + sorted_general_list_2)) else: combined_character_list = character_list_1 combined_general_list = sorted_general_list_1 if not characters_merge_enabled: combined_character_list = [item for item in combined_character_list if item not in combined_general_list] # Handle additional tags prepend_list = [tag.strip() for tag in additional_tags_prepend.split(',') if tag.strip()] append_list = [tag.strip() for tag in additional_tags_append.split(',') if tag.strip()] # Avoid duplicates in prepend/append lists if prepend_list and append_list: append_list = [item for item in append_list if item not in prepend_list] # Remove prepended tags from main list if prepend_list: combined_general_list = [item for item in combined_general_list if item not in prepend_list] # Remove appended tags from main list if append_list: combined_general_list = [item for item in combined_general_list if item not in append_list] # Combine all tags combined_general_list = prepend_list + combined_general_list + append_list # Format output string sorted_general_strings = ', '.join((combined_character_list if characters_merge_enabled else []) + combined_general_list).replace('(', '\\(').replace(')', '\\)').replace('_', ' ') # Generate categorized output categorized_strings = categorize_tags_output(sorted_general_strings, character_res).replace('(', '\\(').replace(')', '\\)') categorized_j = generate_tags_json(sorted_general_strings, character_res) categorized_text = "\n ".join(f"{k}: {', '.join(v)}" for k, v in categorized_j.items()) # Create output files txt_content = f"Output (string): {sorted_general_strings}\n\nCategorized Output: {categorized_strings}" txt_file = create_file(txt_content, output_dir, f"{image_name}_output.txt") txt_infos.append({'path': txt_file, 'name': f"{image_name}_output.txt"}) # Save image copy img = Image.open(image_path) img.save(os.path.join(output_dir, f"{image_name}.png"), format='PNG') txt_infos.append({'path': os.path.join(output_dir, f"{image_name}.png"), 'name': f"{image_name}.png"}) # Create tags text file txt_file = create_file(sorted_general_strings, output_dir, image_name + '.txt') # Create categorized list file categorized_file = create_file(categorized_strings, output_dir, f"{image_name}_categorized.txt") txt_infos.append({'path': categorized_file, 'name': f"{image_name}_categorized.txt"}) txt_infos.append({'path': txt_file, 'name': image_name + '.txt'}) # Store results tag_results[image_path] = {'strings': sorted_general_strings, 'categorized_strings': categorized_strings, 'categorized_text': categorized_text, 'rating': rating, 'character_res': character_res} # Update progress current_progress += progressRatio / progressTotal progress(current_progress, desc=f"Image {idx+1}/{gallery_len} processed") timer.checkpoint(f"Image {idx+1}: {image_name} processed") timer.report() except Exception as e: print(traceback.format_exc()) print('Error predict: ' + str(e)) continue # Create download zip after all images are processed download = None if txt_infos is not None and len(txt_infos) > 0: downloadZipPath = os.path.join(output_dir, 'Multi-Tagger-' + datetime.now().strftime('%Y%m%d-%H%M%S') + '.zip') with zipfile.ZipFile(downloadZipPath, 'w', zipfile.ZIP_DEFLATED) as taggers_zip: for info in txt_infos: taggers_zip.write(info['path'], arcname=info['name']) download = downloadZipPath # Return first image results as default first_image_results = '', {}, '', '', '' if gallery and len(gallery) > 0: first_image_path = gallery[0][0] if first_image_path in tag_results: first_result = tag_results[first_image_path] character_tags_formatted = ", ".join([name.replace("(", "\\(").replace(")", "\\)").replace("_", " ") for name in first_result['character_res'].keys()]) first_image_results = (first_result['strings'], first_result['rating'], character_tags_formatted, first_result.get('categorized_strings', ''), first_result.get('categorized_text', '')) progress(1.0, desc=f"Predict completed for {gallery_len} image(s)") timer.report_all() print('Predict is complete.') # Clean up unload_model() return (download, first_image_results[0], first_image_results[1], first_image_results[2], first_image_results[3], first_image_results[4], tag_results) def get_selection_from_gallery(gallery: list, tag_results: dict, selected_state: gr.SelectData): """Return first image results if no selection""" if not selected_state and gallery and len(gallery) > 0: first_image_path = gallery[0][0] if first_image_path in tag_results: first_result = tag_results[first_image_path] character_tags_formatted = ", ".join([name.replace("(", "\\(").replace(")", "\\)").replace("_", " ") for name in first_result['character_res'].keys()]) return (first_result['strings'], first_result['rating'], character_tags_formatted, first_result.get('categorized_strings', ''), first_result.get('categorized_text', '')) if not selected_state: return '', {}, '', '', '' # Get selected image path selected_value = selected_state.value image_path = None if isinstance(selected_value, dict) and 'image' in selected_value: image_path = selected_value['image']['path'] elif isinstance(selected_value, (list, tuple)) and len(selected_value) > 0: image_path = selected_value[0] else: image_path = str(selected_value) # Return stored results if image_path in tag_results: result = tag_results[image_path] character_tags_formatted = ", ".join([name.replace("(", "\\(").replace(")", "\\)").replace("_", " ") for name in result['character_res'].keys()]) return (result['strings'], result['rating'], character_tags_formatted, result.get('categorized_strings', ''), result.get('categorized_text', '')) return '', {}, '', '', '' def append_gallery(gallery: list, image: str): """Add a single media file (image or video) to the gallery""" return handle_single_media_upload(image, gallery) def extend_gallery(gallery: list, images): """Add multiple media files (images or videos) to the gallery""" return handle_multiple_media_uploads(images, gallery) def auto_convert_tags_to_prompt(categorized_tags_text, system_prompt_key, llama_model_name): """Auto-convert categorized list to prompt if enabled""" if not categorized_tags_text or not str(categorized_tags_text).strip(): return "" # Get the system prompt text from the key prompt_text = SYSTEM_PROMPTS.get(system_prompt_key, SYSTEM_PROMPTS["Creative"]) # Generate response using the selected model and system prompt result = "" for chunk in generate_response(categorized_tags_text, system_prompt=prompt_text, model_name=llama_model_name): result = chunk return result # Parse arguments args = parse_args() # If --analyze is provided, run CLI mode and exit if args.analyze: run_cli_analysis(args) exit(0) dropdown_list = [ EVA02_LARGE_MODEL_DSV3_REPO, VIT_LARGE_MODEL_DSV3_REPO, SWINV2_MODEL_DSV3_REPO, CONV_MODEL_DSV3_REPO, VIT_MODEL_DSV3_REPO, MOAT_MODEL_DSV2_REPO, SWIN_MODEL_DSV2_REPO, CONV_MODEL_DSV2_REPO, CONV2_MODEL_DSV2_REPO, VIT_MODEL_DSV2_REPO, EVA02_LARGE_MODEL_IS_DSV1_REPO, SWINV2_MODEL_IS_DSV1_REPO ] # Llama model choices LLAMA_MODEL_CHOICES = ["gemma-3-1B"] DEFAULT_LLAMA_MODEL = "gemma-3-1B" with gr.Blocks(title=TITLE, css=css, theme="Werli/Purple-Crimson-Gradio-Theme", fill_width=True) as demo: gr.Markdown(value=f"

{TITLE}

") gr.Markdown(value=f"

{DESCRIPTION}

") with gr.Tab(label='WD'): with gr.Row(): with gr.Column(): with gr.Column(variant='panel'): image_input = gr.Image(label='Upload an Image (or paste from clipboard)', type='filepath', sources=['upload', 'clipboard'], height=150) with gr.Row(): upload_button = gr.UploadButton('UPLOAD VIDEOS', file_types=['image', 'video'], file_count='multiple', size='md') gallery = gr.Gallery(columns=2, show_share_button=False, interactive=True, height='auto', label='Grid of images', preview=False, elem_id='custom-gallery') submit = gr.Button(value='START', variant='primary', size='lg') clear = gr.ClearButton(components=[gallery], value='CLEAR GALLERY', variant='secondary', size='sm') with gr.Column(variant='panel'): model_repo = gr.Dropdown(dropdown_list, value=EVA02_LARGE_MODEL_DSV3_REPO, label='1st Model') PLUS = '+?' gr.Markdown(value=f"

{PLUS}

") model_repo_2 = gr.Dropdown([None] + dropdown_list, value=None, label='2nd Model (Optional)', info='Select another model for diversified results.') with gr.Accordion("OTHER", open=False): general_thresh = gr.Slider(0, 1, step=args.score_slider_step, value=args.score_general_threshold, label='General Tags Threshold', scale=3) character_thresh = gr.Slider(0, 1, step=args.score_slider_step, value=args.score_character_threshold, label='Character Tags Threshold', scale=3) general_mcut_enabled = gr.Checkbox(value=False, label='Use MCut threshold', scale=1) character_mcut_enabled = gr.Checkbox(value=False, label='Use MCut threshold', scale=1) characters_merge_enabled = gr.Checkbox(value=False, label='Merge characters into the string output', scale=1) additional_tags_prepend = gr.Text(label='Prepend Additional tags (comma split)') additional_tags_append = gr.Text(label='Append Additional tags (comma split)') rating = gr.Label(label='RATING') with gr.Row(): clear = gr.ClearButton(components=[gallery, model_repo, general_thresh, general_mcut_enabled, character_thresh, character_mcut_enabled, characters_merge_enabled, additional_tags_prepend, additional_tags_append], value="CLEAR EVERYTHING", variant='secondary', size='lg' ) gr.Markdown('[Based on SmilingWolf/wd-tagger](https://huggingface.co/spaces/SmilingWolf/wd-tagger)') with gr.Column(variant='panel'): download_file = gr.File(label='DOWNLOAD') character_res = gr.Textbox(label='CHARACTER TAGS',show_copy_button=True,lines=2) sorted_general_strings = gr.Textbox(label='OUTPUT',show_label=True,show_copy_button=True,lines=5,max_lines=25) categorized_strings = gr.Textbox(label='CATEGORIZED TAGS',show_label=True,show_copy_button=True,lines=5,max_lines=25) tags_string = gr.Textbox(label='CATEGORIZED LIST (EDITABLE)',show_copy_button=True,lines=5,max_lines=25) convert_tags_output = gr.Textbox(label='PROMPT',show_label=True,lines=5, show_copy_button=True,max_lines=20) with gr.Row(): submit_prompt_to_convert = gr.Button(value='CREATE A PROMPT (RANDOM)', variant='primary', size='md') # System prompt and model selection for conversion with gr.Accordion("Prompt Settings", open=False): system_prompt_dropdown = gr.Dropdown( choices=list(SYSTEM_PROMPTS.keys()), value="Creative", label="System Prompt Style", info="Choose how the tags should be converted into a prompt" ) llama_model_dropdown = gr.Dropdown( choices=LLAMA_MODEL_CHOICES, value=DEFAULT_LLAMA_MODEL, label="LLM Model", info="Select the LLM model for tag-to-prompt conversion" ) auto_convert_checkbox = gr.Checkbox( value=False, label="Auto-convert CATEGORIZED LIST to prompt after analysis", info="When enabled, the prompt will be automatically generated after analysis completes" ) # State to store results tag_results = gr.State({}) # Event handlers image_input.change(append_gallery,inputs=[gallery, image_input], outputs=[gallery, image_input]) upload_button.upload(extend_gallery,inputs=[gallery, upload_button], outputs=gallery) gallery.select(get_selection_from_gallery,inputs=[gallery, tag_results], outputs=[sorted_general_strings, rating, character_res, categorized_strings, tags_string]) # Auto-convert trigger after predict completes def handle_after_predict(download, output_str, rating_val, char_tags, cat_str, cat_tags_str, tag_results_dict, auto_convert, sys_prompt, llm_model): """Handle post-predict: if auto-convert is enabled, run conversion""" prompt_output = "" if auto_convert and cat_tags_str and str(cat_tags_str).strip(): prompt_output = auto_convert_tags_to_prompt(cat_tags_str, sys_prompt, llm_model) return download, output_str, rating_val, char_tags, cat_str, cat_tags_str, prompt_output submit.click( predict, inputs=[gallery, model_repo, model_repo_2, general_thresh, general_mcut_enabled, character_thresh, character_mcut_enabled, characters_merge_enabled, additional_tags_prepend, additional_tags_append, tag_results], outputs=[download_file, sorted_general_strings, rating, character_res, categorized_strings, tags_string, tag_results] ).then( handle_after_predict, inputs=[download_file, sorted_general_strings, rating, character_res, categorized_strings, tags_string, tag_results, auto_convert_checkbox, system_prompt_dropdown, llama_model_dropdown], outputs=[download_file, sorted_general_strings, rating, character_res, categorized_strings, tags_string, convert_tags_output] ) # Convert tags button submit_prompt_to_convert.click( generate_response, inputs=[tags_string, system_prompt_dropdown, llama_model_dropdown], outputs=[convert_tags_output] ) with gr.Tab("PixAI"): pixai_components = create_pixai_interface( system_prompt_options=list(SYSTEM_PROMPTS.keys()), default_system_prompt="Creative", llama_model_choices=LLAMA_MODEL_CHOICES, default_llama_model=DEFAULT_LLAMA_MODEL ) with gr.Tab("ComfyUI Metadata Extractor"): comfy_interface = create_multi_comfy() demo.queue().launch()