Hydragee commited on
Commit
772b344
·
verified ·
1 Parent(s): d066e07

Upload folder using huggingface_hub

Browse files
modules/art_tools.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/art_tools.py
2
+
3
+ import os
4
+ import math
5
+ import numpy as np
6
+ from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps
7
+ import gradio as gr
8
+ import io
9
+
10
+ # --- ADOPTABLE GRID MAKER ---
11
+
12
+ def hex_to_rgb(hex_color):
13
+ """Hex color kodunu (ör: #FF0000) (255, 0, 0) tuple'ına çevirir."""
14
+ hex_color = hex_color.lstrip('#')
15
+ return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + (255,) # Alpha ekle
16
+
17
+ def create_grid_collage(files, columns, add_labels, label_type, start_number, bg_color_hex, title_text, banner_color_hex, title_color_hex):
18
+ """
19
+ Adoptable Style Grid Collage Maker
20
+ """
21
+ if not files:
22
+ return None, "⚠️ Lütfen resim dosyalarını seçin."
23
+
24
+ try:
25
+ images = []
26
+ for file_obj in files:
27
+ try:
28
+ img = Image.open(file_obj.name).convert("RGBA")
29
+ images.append(img)
30
+ except:
31
+ continue
32
+
33
+ if not images:
34
+ return None, "⚠️ Geçerli resim bulunamadı."
35
+
36
+
37
+ count = len(images)
38
+ cols = int(columns)
39
+ rows = math.ceil(count / cols)
40
+
41
+ # 1. Boyut Standardizasyonu (İlk resme göre)
42
+ base_w, base_h = images[0].size
43
+ resized_images = []
44
+ for img in images:
45
+ if img.size != (base_w, base_h):
46
+ resized_images.append(img.resize((base_w, base_h), Image.Resampling.LANCZOS))
47
+ else:
48
+ resized_images.append(img)
49
+
50
+ # Tasarım Parametreleri
51
+ padding = 40 # Resimler arası boşluk biraz arttı
52
+ frame_width = 15 # Çerçeve kalınlığı arttı
53
+
54
+ # Renkler
55
+ bg_color = hex_to_rgb(bg_color_hex)
56
+ banner_color = hex_to_rgb(banner_color_hex)
57
+ title_color = hex_to_rgb(title_color_hex)
58
+ frame_color = title_color # Çerçeve rengi başlık rengiyle aynı olsun
59
+
60
+ # Canvas Boyutu (Önce hesapla)
61
+ grid_w = (cols * base_w) + ((cols + 1) * padding)
62
+
63
+ # Header Yüksekliği: Grid genişliğine orantılı olsun (Genişlik arttıkça yükseklik de artsın ki text büyüyebilsin)
64
+ # Min 250px, yoksa genişliğin %15'i
65
+ header_height = max(250, int(grid_w * 0.15))
66
+
67
+ grid_h = header_height + (rows * base_h) + ((rows + 1) * padding)
68
+
69
+ grid_img = Image.new("RGBA", (grid_w, grid_h), bg_color)
70
+ draw = ImageDraw.Draw(grid_img)
71
+
72
+ # --- HEADER (BANNER) ÇİZİMİ ---
73
+ draw.rectangle([(0, 0), (grid_w, header_height)], fill=banner_color)
74
+ draw.rectangle([(0, header_height-15), (grid_w, header_height)], fill=frame_color)
75
+
76
+ # Başlangıç Font Boyutu (Çok daha büyük başlıyoruz - Grid genişliğine orantılı)
77
+ font_title_size = int(grid_w * 0.1) # Genişliğin %10'u ile başla
78
+
79
+ # --- BAŞLIK FONTU BÜYÜKLÜĞÜNÜ AYARLA (Agresif Scaling) ---
80
+ target_width = grid_w * 0.95 # Banner genişliğinin %95'ini doldurmalı
81
+ max_height_text = header_height * 0.8 # Yüksekliğin %80'ini geçmesin (Hava kalsın)
82
+
83
+ try:
84
+ font_path = "timesbd.ttf"
85
+ font_title = ImageFont.truetype(font_path, font_title_size)
86
+ except:
87
+ font_path = None
88
+ font_title = ImageFont.load_default()
89
+
90
+ if font_path:
91
+ # İterasyonla en uygun boyutu bul
92
+ for _ in range(50): # Sonsuz döngüden kaçınmak için limitli
93
+ bbox_title = draw.textbbox((0, 0), title_text, font=font_title)
94
+
95
+ w_total = (bbox_title[2] - bbox_title[0])
96
+ h_max = bbox_title[3] - bbox_title[1]
97
+
98
+ # Çok büyükse küçült
99
+ if w_total > target_width or h_max > max_height_text:
100
+ font_title_size -= 5
101
+ if font_title_size < 20: break
102
+ font_title = ImageFont.truetype(font_path, font_title_size)
103
+ # Çok küçükse büyüt (Eğer dikeyde ve yatayda yer varsa)
104
+ elif w_total < target_width * 0.9 and h_max < max_height_text * 0.9:
105
+ font_title_size += 5
106
+ font_title = ImageFont.truetype(font_path, font_title_size)
107
+ else:
108
+ break # Tamamdır
109
+
110
+ # Son Ölçümler & Çizim
111
+ bbox_title = draw.textbbox((0, 0), title_text, font=font_title)
112
+ w_title = bbox_title[2] - bbox_title[0]
113
+ h_title = bbox_title[3] - bbox_title[1]
114
+
115
+ start_x_text = (grid_w - w_title) // 2
116
+ bg_center_y = header_height // 2
117
+
118
+ # Başlık çiz
119
+ draw.text((start_x_text, bg_center_y - h_title//2), title_text, font=font_title, fill=title_color, stroke_width=int(font_title_size*0.02), stroke_fill="black")
120
+
121
+ # --- RESİMLERİ YERLEŞTİR ---
122
+ # Numara Fontunu Resim Boyutuna Göre Ayarla (Çok Büyük)
123
+ try:
124
+ num_font_size = int(base_w * 0.25) # Resim genişliğinin %25'i kadar (Kocaman)
125
+ font_num = ImageFont.truetype("impact.ttf", num_font_size)
126
+ except:
127
+ font_num = ImageFont.load_default()
128
+
129
+ current_num = int(start_number)
130
+
131
+ for i, img in enumerate(resized_images):
132
+ r = i // cols
133
+ c = i % cols
134
+
135
+ x = padding + (c * (base_w + padding))
136
+ y = header_height + padding + (r * (base_h + padding))
137
+
138
+ # Kalın Çerçeve
139
+ draw.rectangle(
140
+ [(x - frame_width, y - frame_width),
141
+ (x + base_w + frame_width, y + base_h + frame_width)],
142
+ fill=None, outline=frame_color, width=frame_width
143
+ )
144
+
145
+ # Resim
146
+ grid_img.paste(img, (x, y), mask=img)
147
+
148
+ # --- OVERLAY NUMARA (DEVASA) ---
149
+ if add_labels:
150
+ if str(label_type).startswith("Sadece"):
151
+ num_str = f"{current_num}"
152
+ else:
153
+ num_str = f"#{current_num}"
154
+ current_num += 1
155
+
156
+ bbox_num = draw.textbbox((0, 0), num_str, font=font_num)
157
+ w_num = bbox_num[2] - bbox_num[0]
158
+ h_num = bbox_num[3] - bbox_num[1]
159
+
160
+ # Ortala ve Biraz daha yukarı al (Resmin altından %10 yukarıda)
161
+ num_x = x + (base_w - w_num) // 2
162
+ num_y = y + base_h - h_num - int(base_h * 0.05) - 20
163
+
164
+ # Çok Kalın Outline
165
+ outline_width = int(num_font_size * 0.08) # Font boyutuna göre outline kalınlığı
166
+ draw.text((num_x, num_y), num_str, font=font_num, fill="white", stroke_width=outline_width, stroke_fill="black")
167
+
168
+ return grid_img, "✅ Adoptable Grid oluşturuldu!"
169
+
170
+ except Exception as e:
171
+ import traceback
172
+ traceback.print_exc()
173
+ return None, f"❌ Hata: {str(e)}"
modules/cl_tagger_module.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/cl_tagger_module.py
2
+
3
+ import numpy as np
4
+ from PIL import Image
5
+ import json
6
+ import os
7
+ import onnxruntime as ort
8
+ from huggingface_hub import hf_hub_download
9
+ from dataclasses import dataclass
10
+ from typing import Dict
11
+
12
+ # --- Data Classes ---
13
+ @dataclass
14
+ class LabelData:
15
+ names: list[str]
16
+ rating: list[np.int64]
17
+ general: list[np.int64]
18
+ artist: list[np.int64]
19
+ character: list[np.int64]
20
+ copyright: list[np.int64]
21
+ meta: list[np.int64]
22
+ quality: list[np.int64]
23
+ model: list[np.int64]
24
+
25
+ # --- Helper Functions ---
26
+ def pil_ensure_rgb(image: Image.Image) -> Image.Image:
27
+ if image.mode not in ["RGB", "RGBA"]:
28
+ image = image.convert("RGBA") if "transparency" in image.info else image.convert("RGB")
29
+ if image.mode == "RGBA":
30
+ background = Image.new("RGB", image.size, (255, 255, 255))
31
+ background.paste(image, mask=image.split()[3])
32
+ image = background
33
+ return image
34
+
35
+ def pil_pad_square(image: Image.Image) -> Image.Image:
36
+ width, height = image.size
37
+ if width == height: return image
38
+ new_size = max(width, height)
39
+ new_image = Image.new(image.mode, (new_size, new_size), (255, 255, 255))
40
+ paste_position = ((new_size - width) // 2, (new_size - height) // 2)
41
+ new_image.paste(image, paste_position)
42
+ return new_image
43
+
44
+ def load_tag_mapping(mapping_path):
45
+ with open(mapping_path, 'r', encoding='utf-8') as f: tag_mapping_data = json.load(f)
46
+ if isinstance(tag_mapping_data, dict) and "idx_to_tag" in tag_mapping_data:
47
+ idx_to_tag = {int(k): v for k, v in tag_mapping_data["idx_to_tag"].items()}
48
+ tag_to_category = tag_mapping_data["tag_to_category"]
49
+ elif isinstance(tag_mapping_data, dict):
50
+ try:
51
+ tag_mapping_data_int_keys = {int(k): v for k, v in tag_mapping_data.items()}
52
+ idx_to_tag = {idx: data['tag'] for idx, data in tag_mapping_data_int_keys.items()}
53
+ tag_to_category = {data['tag']: data['category'] for data in tag_mapping_data_int_keys.values()}
54
+ except (KeyError, ValueError) as e:
55
+ raise ValueError(f"Unsupported tag mapping format (dict): {e}. Expected int keys with 'tag' and 'category'.")
56
+ else:
57
+ raise ValueError("Unsupported tag mapping format: Expected a dictionary.")
58
+
59
+ names = [None] * (max(idx_to_tag.keys()) + 1)
60
+ rating, general, artist, character, copyright, meta, quality, model_name = [], [], [], [], [], [], [], []
61
+ for idx, tag in idx_to_tag.items():
62
+ if idx >= len(names): names.extend([None] * (idx - len(names) + 1))
63
+ names[idx] = tag
64
+ category = tag_to_category.get(tag, 'Unknown')
65
+ idx_int = int(idx)
66
+ if category == 'Rating': rating.append(idx_int)
67
+ elif category == 'General': general.append(idx_int)
68
+ elif category == 'Artist': artist.append(idx_int)
69
+ elif category == 'Character': character.append(idx_int)
70
+ elif category == 'Copyright': copyright.append(idx_int)
71
+ elif category == 'Meta': meta.append(idx_int)
72
+ elif category == 'Quality': quality.append(idx_int)
73
+ elif category == 'Model': model_name.append(idx_int)
74
+
75
+ return LabelData(names=names, rating=np.array(rating, dtype=np.int64), general=np.array(general, dtype=np.int64), artist=np.array(artist, dtype=np.int64),
76
+ character=np.array(character, dtype=np.int64), copyright=np.array(copyright, dtype=np.int64), meta=np.array(meta, dtype=np.int64), quality=np.array(quality, dtype=np.int64), model=np.array(model_name, dtype=np.int64)), idx_to_tag, tag_to_category
77
+
78
+ def preprocess_image(image: Image.Image, target_size=(448, 448)):
79
+ image = pil_ensure_rgb(image)
80
+ image = pil_pad_square(image)
81
+ image_resized = image.resize(target_size, Image.BICUBIC)
82
+ img_array = np.array(image_resized, dtype=np.float32) / 255.0
83
+ img_array = img_array.transpose(2, 0, 1) # HWC -> CHW
84
+ img_array = img_array[::-1, :, :] # BGR conversion
85
+ mean = np.array([0.5, 0.5, 0.5], dtype=np.float32).reshape(3, 1, 1)
86
+ std = np.array([0.5, 0.5, 0.5], dtype=np.float32).reshape(3, 1, 1)
87
+ img_array = (img_array - mean) / std
88
+ img_array = np.expand_dims(img_array, axis=0) # Add batch dimension
89
+ return image, img_array
90
+
91
+ def get_tags(probs, labels: LabelData, gen_threshold, char_threshold):
92
+ result = {
93
+ "rating": [], "general": [], "character": [], "copyright": [],
94
+ "artist": [], "meta": [], "quality": [], "model": []
95
+ }
96
+
97
+ # Rating (select max)
98
+ if len(labels.rating) > 0:
99
+ valid_indices = labels.rating[labels.rating < len(probs)]
100
+ if len(valid_indices) > 0:
101
+ rating_probs = probs[valid_indices]
102
+ if len(rating_probs) > 0:
103
+ rating_idx_local = np.argmax(rating_probs)
104
+ rating_idx_global = valid_indices[rating_idx_local]
105
+ if rating_idx_global < len(labels.names) and labels.names[rating_idx_global] is not None:
106
+ rating_name = labels.names[rating_idx_global]
107
+ rating_conf = float(rating_probs[rating_idx_local])
108
+ result["rating"].append((rating_name, rating_conf))
109
+
110
+ # Quality (select max)
111
+ if len(labels.quality) > 0:
112
+ valid_indices = labels.quality[labels.quality < len(probs)]
113
+ if len(valid_indices) > 0:
114
+ quality_probs = probs[valid_indices]
115
+ if len(quality_probs) > 0:
116
+ quality_idx_local = np.argmax(quality_probs)
117
+ quality_idx_global = valid_indices[quality_idx_local]
118
+ if quality_idx_global < len(labels.names) and labels.names[quality_idx_global] is not None:
119
+ quality_name = labels.names[quality_idx_global]
120
+ quality_conf = float(quality_probs[quality_idx_local])
121
+ result["quality"].append((quality_name, quality_conf))
122
+
123
+ # Threshold-based categories
124
+ category_map = {
125
+ "general": (labels.general, gen_threshold),
126
+ "character": (labels.character, char_threshold),
127
+ "copyright": (labels.copyright, char_threshold),
128
+ "artist": (labels.artist, char_threshold),
129
+ "meta": (labels.meta, gen_threshold),
130
+ "model": (labels.model, gen_threshold)
131
+ }
132
+ for category, (indices, threshold) in category_map.items():
133
+ if len(indices) > 0:
134
+ valid_indices = indices[(indices < len(probs))]
135
+ if len(valid_indices) > 0:
136
+ category_probs = probs[valid_indices]
137
+ mask = category_probs >= threshold
138
+ selected_indices_local = np.where(mask)[0]
139
+ if len(selected_indices_local) > 0:
140
+ selected_indices_global = valid_indices[selected_indices_local]
141
+ selected_probs = category_probs[selected_indices_local]
142
+ for idx_global, prob_val in zip(selected_indices_global, selected_probs):
143
+ if idx_global < len(labels.names) and labels.names[idx_global] is not None:
144
+ result[category].append((labels.names[idx_global], float(prob_val)))
145
+
146
+ for k in result:
147
+ result[k] = sorted(result[k], key=lambda x: x[1], reverse=True)
148
+ return result
149
+
150
+ # --- Constants ---
151
+ REPO_ID = "cella110n/cl_tagger"
152
+ MODEL_OPTIONS = {
153
+ "cl_tagger_1_00": "cl_tagger_1_00/model_optimized.onnx"
154
+ }
155
+ DEFAULT_MODEL = "cl_tagger_1_00"
156
+ CACHE_DIR = "./models/model_cache"
157
+
158
+ class CLTagger:
159
+ def __init__(self):
160
+ self.onnx_model_path = None
161
+ self.tag_mapping_path = None
162
+ self.labels_data = None
163
+ self.idx_to_tag = None
164
+ self.tag_to_category = None
165
+ self.current_model = None
166
+ self.session = None
167
+ self.initialize_onnx_paths(DEFAULT_MODEL)
168
+
169
+ def initialize_onnx_paths(self, model_choice=DEFAULT_MODEL):
170
+ if not model_choice in MODEL_OPTIONS:
171
+ print(f"Invalid model choice: {model_choice}, falling back to default: {DEFAULT_MODEL}")
172
+ model_choice = DEFAULT_MODEL
173
+
174
+ self.current_model = model_choice
175
+ model_dir = model_choice
176
+ onnx_filename = MODEL_OPTIONS[model_choice]
177
+ tag_mapping_filename = f"{model_dir}/tag_mapping.json"
178
+
179
+ print(f"Initializing ONNX paths and labels for model: {model_choice}...")
180
+ hf_token = os.environ.get("HF_TOKEN")
181
+
182
+ try:
183
+ print(f"Attempting to download ONNX model: {onnx_filename}")
184
+ self.onnx_model_path = hf_hub_download(
185
+ repo_id=REPO_ID,
186
+ filename=onnx_filename,
187
+ cache_dir=CACHE_DIR,
188
+ token=hf_token,
189
+ force_download=False
190
+ )
191
+ print(f"ONNX model path: {self.onnx_model_path}")
192
+
193
+ print(f"Attempting to download Tag mapping: {tag_mapping_filename}")
194
+ self.tag_mapping_path = hf_hub_download(
195
+ repo_id=REPO_ID,
196
+ filename=tag_mapping_filename,
197
+ cache_dir=CACHE_DIR,
198
+ token=hf_token,
199
+ force_download=False
200
+ )
201
+ print(f"Tag mapping path: {self.tag_mapping_path}")
202
+
203
+ print("Loading labels from mapping...")
204
+ self.labels_data, self.idx_to_tag, self.tag_to_category = load_tag_mapping(self.tag_mapping_path)
205
+ print(f"Labels loaded. Count: {len(self.labels_data.names)}")
206
+
207
+ print("Creating ONNX Runtime session (CPUExecutionProvider)...")
208
+ self.session = ort.InferenceSession(
209
+ self.onnx_model_path,
210
+ providers=["CPUExecutionProvider"]
211
+ )
212
+ print("ONNX Runtime session ready.")
213
+ return True
214
+
215
+ except Exception as e:
216
+ print(f"Error during initialization of CLTagger: {e}")
217
+ import traceback; traceback.print_exc()
218
+ self.onnx_model_path = None
219
+ self.tag_mapping_path = None
220
+ self.labels_data = None
221
+ self.idx_to_tag = None
222
+ self.tag_to_category = None
223
+ self.current_model = None
224
+ self.session = None
225
+ raise
226
+
227
+ def predict(self, image_input: Image.Image, gen_threshold: float, char_threshold: float) -> tuple[str, Image.Image | None, Dict]:
228
+ """
229
+ Tahmin işlemini gerçekleştirir.
230
+ :param image_input: Giriş PIL Image nesnesi.
231
+ :param gen_threshold: Genel/Meta/Model etiketleri için eşik değeri.
232
+ :param char_threshold: Karakter/Telif Hakkı/Sanatçı etiketleri için eşik değeri.
233
+ :return: (etiketler_string, None, ham_tahminler_sözlüğü)
234
+ """
235
+
236
+ if self.onnx_model_path is None or self.labels_data is None or self.session is None:
237
+ message = "Error: CLTagger not initialized. Check startup logs."
238
+ print(message)
239
+ return message, None, {}
240
+
241
+ try:
242
+ original_pil_image, input_tensor = preprocess_image(image_input)
243
+ input_tensor = input_tensor.astype(np.float32)
244
+
245
+ except Exception as e:
246
+ message = f"Error processing input image in CLTagger: {e}"
247
+ print(message)
248
+ return message, None, {}
249
+
250
+ try:
251
+ input_name = self.session.get_inputs()[0].name
252
+ output_name = self.session.get_outputs()[0].name
253
+ outputs = self.session.run([output_name], {input_name: input_tensor})[0]
254
+
255
+ if np.isnan(outputs).any() or np.isinf(outputs).any():
256
+ outputs = np.nan_to_num(outputs, nan=0.0, posinf=1.0, neginf=0.0)
257
+
258
+ def stable_sigmoid(x):
259
+ return 1 / (1 + np.exp(-np.clip(x, -30, 30)))
260
+ probs = stable_sigmoid(outputs[0])
261
+
262
+ except Exception as e:
263
+ message = f"Error during ONNX inference in CLTagger: {e}"
264
+ print(message)
265
+ import traceback; traceback.print_exc()
266
+ return message, None, {}
267
+
268
+ try:
269
+ predictions = get_tags(probs, self.labels_data, gen_threshold, char_threshold)
270
+
271
+ output_tags = []
272
+ if predictions.get("rating"): output_tags.append(predictions["rating"][0][0].replace("_", " "))
273
+ if predictions.get("quality"): output_tags.append(predictions["quality"][0][0].replace("_", " "))
274
+ for category in ["artist", "character", "copyright", "general", "meta", "model"]:
275
+ tags_in_category = predictions.get(category, [])
276
+ for tag, prob in tags_in_category:
277
+ if category == "meta" and any(p in tag.lower() for p in ['id', 'commentary', 'request', 'mismatch']):
278
+ continue
279
+ output_tags.append(tag.replace("_", " "))
280
+ output_text = ", ".join(output_tags)
281
+
282
+ # Visualization removed. Returning None for the image placeholder to maintain signature compatibility.
283
+ return output_text, None, predictions
284
+
285
+ except Exception as e:
286
+ message = f"Error during post-processing in CLTagger: {e}"
287
+ print(message)
288
+ import traceback; traceback.print_exc()
289
+ return message, None, {}
290
+
291
+ # Module initialization
292
+ if __name__ == "__main__":
293
+ try:
294
+ tagger_instance = CLTagger()
295
+ print("CLTagger module loaded successfully for testing.")
296
+ except Exception as e:
297
+ print(f"Failed to initialize CLTagger module: {e}")
modules/config_manager.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/config_manager.py
2
+ import os
3
+ import json
4
+ import gradio as gr
5
+
6
+ # --- Yapılandırma ve Dosya Yolları ---
7
+ CONFIG_FILE = "data/configs/config.json"
8
+ TEMPLATES_FILE = "data/configs/renaming_templates.json" # Yeni Şablon Dosyası
9
+
10
+ # --- ŞABLON YÖNETİMİ FONKSİYONLARI (YENİ) ---
11
+ def load_renaming_templates():
12
+ default_templates = ["Adoptable {Number} (Female)", "{Number}", "Character_{Number}", "Design #{Number}"]
13
+ if os.path.exists(TEMPLATES_FILE):
14
+ try:
15
+ with open(TEMPLATES_FILE, "r", encoding="utf-8") as f:
16
+ loaded = json.load(f)
17
+ if isinstance(loaded, list) and loaded: return loaded
18
+ except: pass
19
+ return default_templates
20
+
21
+ def save_renaming_templates_to_file(templates):
22
+ try:
23
+ os.makedirs(os.path.dirname(TEMPLATES_FILE), exist_ok=True)
24
+ with open(TEMPLATES_FILE, "w", encoding="utf-8") as f:
25
+ json.dump(templates, f, indent=4, ensure_ascii=False)
26
+ except Exception as e: print(f"Şablon kaydetme hatası: {e}")
27
+
28
+ def add_renaming_template(new_template, current_list):
29
+ if not new_template: return gr.update(), current_list
30
+ if "{Number}" not in new_template:
31
+ return gr.update(value="❌ Şablon '{Number}' içermelidir!"), current_list
32
+
33
+ if new_template not in current_list:
34
+ current_list.append(new_template)
35
+ save_renaming_templates_to_file(current_list)
36
+ # Dropdown'ı güncelle ve yeni ekleneni seçili yap
37
+ return gr.update(choices=current_list, value=new_template), current_list
38
+ return gr.update(value=new_template), current_list
39
+
40
+ def delete_renaming_template(selected_template, current_list):
41
+ if selected_template in current_list:
42
+ current_list.remove(selected_template)
43
+ save_renaming_templates_to_file(current_list)
44
+ new_val = current_list[0] if current_list else ""
45
+ return gr.update(choices=current_list, value=new_val), current_list
46
+ return gr.update(), current_list
47
+
48
+ # --- Yapılandırma Yönetimi Fonksiyonları ---
49
+
50
+ def save_config(config_data):
51
+ """
52
+ Klasör yollarını ve tüm uygulama ayarlarını bir JSON dosyasına kaydeder.
53
+ """
54
+ try:
55
+ os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
56
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
57
+ json.dump(config_data, f, indent=4)
58
+ except IOError as e:
59
+ print(f"Yapılandırma dosyası yazma hatası: {e}")
60
+
61
+ def load_config():
62
+ """
63
+ Tüm uygulama ayarlarını bir JSON dosyasından yükler veya varsayılan değerleri döndürür.
64
+ """
65
+ default_config = {
66
+ "folder_paths": [],
67
+ "general_settings": {
68
+ "device": "Auto",
69
+ "language": "tr",
70
+ "theme": "Default"
71
+ },
72
+ "global_tagger_settings": {
73
+ "use_joint": True,
74
+ "joint_thresh": 0.25,
75
+ "use_cl_tagger": True,
76
+ "cl_gen_thresh": 0.55,
77
+ "cl_char_thresh": 0.60,
78
+ "use_pixai_tagger": False,
79
+ "pixai_general_thresh": 0.30,
80
+ "pixai_char_thresh": 0.85,
81
+ "use_animetagger": False,
82
+ "animetagger_model": "MobileNet V4 (Hızlı)",
83
+ "animetagger_thresh": 0.35,
84
+
85
+ # Gemini Ayarları
86
+ "use_gemini": False,
87
+ "gemini_api_key": "",
88
+ "gemini_mode": "Vision",
89
+ "gemini_model": "gemini-2.5-flash",
90
+ "gemini_prompt_vision": "Describe this image in a detailed, natural language caption for an AI image generator dataset. Focus on the main subject, clothing, pose, and background. Do not use bullet points.",
91
+ "gemini_prompt_tags": "Create a fluent, natural language caption based on the provided tags. Focus on describing the scene vividly.",
92
+ "gemini_prompt_hybrid": "Describe this image using the visual details and refine the description with the provided tags for accuracy. Focus on a cohesive narrative.",
93
+ "gemini_system_instruction": "You are a helpful assistant that generates detailed and accurate image captions for AI image generation datasets.",
94
+
95
+ # Kural Dosyaları
96
+ "replacement_file": "",
97
+ "synonym_file": "",
98
+ "addition_file": "",
99
+ "sort_order": "Alfabetik",
100
+ "context_weight": 0.0,
101
+
102
+ # Ayrı kategorizasyon ayarları
103
+ "tekil_enable_categorization": False,
104
+ "tekil_selected_categories": [],
105
+ "toplu_enable_categorization": False,
106
+ "toplu_selected_categories": [],
107
+ "dual1_enable_categorization": False,
108
+ "dual1_selected_categories": [],
109
+ "dual2_enable_categorization": False,
110
+ "dual2_selected_categories": []
111
+ },
112
+ "image_tools_settings": {
113
+ "folder_paths": [],
114
+ "scale_factor": 1.0,
115
+ "selected_renaming_file": "",
116
+ "renaming_type": "Rastgele",
117
+ "watermark_text": "Örnek Filigran",
118
+ "watermark_opacity": 0.35,
119
+ "watermark_font_ratio": 0.05,
120
+ "watermark_angle": -45,
121
+ "renaming_templates": [],
122
+ "brightness_level": 0,
123
+ "contrast_level": 0,
124
+ "denoise_level": 0,
125
+ "sharpen_amount": 0.0,
126
+ "sequential_pattern": "Adoptable #Number",
127
+ "sequential_start_number": 1,
128
+ "sequential_digit_count": 4
129
+ },
130
+ "art_tools_settings": {
131
+ "grid_cols": 3,
132
+ "grid_bg_color": "#000000",
133
+ "grid_title_text": "ADOPTABLE SALE!",
134
+ "grid_banner_color": "#000000",
135
+ "grid_title_color": "#FFD700",
136
+ "grid_add_labels": True,
137
+ "grid_label_type": "Numara (#1, #2...)",
138
+ "grid_start_num": 1
139
+ }
140
+ }
141
+
142
+ try:
143
+ if os.path.exists(CONFIG_FILE):
144
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
145
+ loaded_config = json.load(f)
146
+ def update_dict(d, u):
147
+ for k, v in u.items():
148
+ if isinstance(v, dict):
149
+ # Eğer u[k] dict değilse (eski config uyumsuzluğu), d[k]'yı koru veya override et?
150
+ # Basitçe d[k] var ve dict ise recursive, değilse direk al.
151
+ d[k] = update_dict(d.get(k, {}), v)
152
+ else:
153
+ d[k] = v
154
+ return d
155
+ return update_dict(default_config.copy(), loaded_config)
156
+ except (IOError, json.JSONDecodeError) as e:
157
+ print(f"Yapılandırma dosyası okuma hatası veya dosya yok: {e}. Varsayılanlar yükleniyor.")
158
+ return default_config
159
+
160
+ # --- Ayarları Kaydetme Fonksiyonları ---
161
+
162
+ def save_global_tagger_settings(
163
+ device, language, theme,
164
+ use_joint, joint_thresh,
165
+ use_cl_tagger, cl_gen_thresh, cl_char_thresh,
166
+ use_pixai_tagger, pixai_general_thresh, pixai_char_thresh,
167
+ use_animetagger, animetagger_model, animetagger_thresh,
168
+ # Gemini Parametreleri
169
+ use_gemini, gemini_api_key, gemini_mode, gemini_model,
170
+ gemini_prompt_vision, gemini_prompt_tags, gemini_prompt_hybrid,
171
+ gemini_system_instruction,
172
+ # Kural Dosyaları
173
+ replacement_file, synonym_file, addition_file,
174
+ sort_order,
175
+ context_weight,
176
+ # Kategorizasyon
177
+ tekil_enable, tekil_cats,
178
+ toplu_enable, toplu_cats,
179
+ dual1_enable, dual1_cats,
180
+ dual2_enable, dual2_cats
181
+ ):
182
+ config_data = load_config()
183
+ config_data["general_settings"] = {"device": device, "language": language, "theme": theme}
184
+ config_data["global_tagger_settings"] = {
185
+ "use_joint": use_joint, "joint_thresh": joint_thresh,
186
+ "use_cl_tagger": use_cl_tagger, "cl_gen_thresh": cl_gen_thresh, "cl_char_thresh": cl_char_thresh,
187
+ "use_pixai_tagger": use_pixai_tagger, "pixai_general_thresh": pixai_general_thresh, "pixai_char_thresh": pixai_char_thresh,
188
+ "use_animetagger": use_animetagger, "animetagger_model": animetagger_model, "animetagger_thresh": animetagger_thresh,
189
+
190
+ # Gemini
191
+ "use_gemini": use_gemini, "gemini_api_key": gemini_api_key, "gemini_mode": gemini_mode,
192
+ "gemini_model": gemini_model,
193
+ "gemini_prompt_vision": gemini_prompt_vision,
194
+ "gemini_prompt_tags": gemini_prompt_tags,
195
+ "gemini_prompt_hybrid": gemini_prompt_hybrid,
196
+ "gemini_system_instruction": gemini_system_instruction,
197
+
198
+ # Dosyalar
199
+ "replacement_file": replacement_file, "synonym_file": synonym_file, "addition_file": addition_file,
200
+ "sort_order": sort_order,
201
+ "context_weight": context_weight,
202
+
203
+ "tekil_enable_categorization": tekil_enable, "tekil_selected_categories": tekil_cats,
204
+ "toplu_enable_categorization": toplu_enable, "toplu_selected_categories": toplu_cats,
205
+ "dual1_enable_categorization": dual1_enable, "dual1_selected_categories": dual1_cats,
206
+ "dual2_enable_categorization": dual2_enable, "dual2_selected_categories": dual2_cats
207
+ }
208
+ save_config(config_data)
209
+ return gr.update(value="✅ Tüm genel ve özelleştirilmiş kategorizasyon ayarları kaydedildi.", visible=True)
210
+
211
+ def save_image_tools_settings(folder_paths_str, scale_factor, watermark_text, watermark_opacity, watermark_font_ratio, watermark_angle, brightness_level, contrast_level, denoise_level, sharpen_amount):
212
+ config_data = load_config()
213
+ config_data["image_tools_settings"]["folder_paths"] = [path.strip() for path in folder_paths_str.split('\n') if path.strip()]
214
+ config_data["image_tools_settings"]["scale_factor"] = scale_factor
215
+ config_data["image_tools_settings"]["watermark_text"] = watermark_text
216
+ config_data["image_tools_settings"]["watermark_opacity"] = watermark_opacity
217
+ config_data["image_tools_settings"]["watermark_font_ratio"] = watermark_font_ratio
218
+ config_data["image_tools_settings"]["watermark_angle"] = watermark_angle
219
+ config_data["image_tools_settings"]["brightness_level"] = brightness_level
220
+ config_data["image_tools_settings"]["contrast_level"] = contrast_level
221
+ config_data["image_tools_settings"]["denoise_level"] = denoise_level
222
+ config_data["image_tools_settings"]["sharpen_amount"] = sharpen_amount
223
+
224
+ save_config(config_data)
225
+ return gr.update(value="✅ Resim araçları ayarları kaydedildi.", visible=True)
226
+
227
+ def save_art_tools_settings(grid_cols, grid_bg_color, grid_title_text, grid_banner_color, grid_title_color, grid_add_labels, grid_label_type, grid_start_num):
228
+ config_data = load_config()
229
+ # Eğer art_tools_settings keyi yoksa oluştur (eski configler için)
230
+ if "art_tools_settings" not in config_data:
231
+ config_data["art_tools_settings"] = {}
232
+
233
+ config_data["art_tools_settings"]["grid_cols"] = grid_cols
234
+ config_data["art_tools_settings"]["grid_bg_color"] = grid_bg_color
235
+ config_data["art_tools_settings"]["grid_title_text"] = grid_title_text
236
+ config_data["art_tools_settings"]["grid_banner_color"] = grid_banner_color
237
+ config_data["art_tools_settings"]["grid_title_color"] = grid_title_color
238
+ config_data["art_tools_settings"]["grid_add_labels"] = grid_add_labels
239
+ config_data["art_tools_settings"]["grid_label_type"] = grid_label_type
240
+ config_data["art_tools_settings"]["grid_start_num"] = grid_start_num
241
+
242
+ save_config(config_data)
243
+ return gr.update(value="✅ Art Studio ayarları kaydedildi.", visible=True)
modules/hydra_layers.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/hydra_layers.py
2
+
3
+ import math
4
+ import re
5
+ from typing import Any
6
+ import torch
7
+ from torch import Tensor
8
+ from torch.nn import (
9
+ Module, ModuleList, Parameter, Buffer,
10
+ Linear, LayerNorm, RMSNorm, Dropout, Flatten, Identity,
11
+ init
12
+ )
13
+ from torch.nn.functional import pad, scaled_dot_product_attention, silu, gelu
14
+ from einops import rearrange
15
+
16
+ # --- GLU.PY ---
17
+ class GatedUnit(Module):
18
+ def __init__(self, dim: int = -1) -> None:
19
+ super().__init__()
20
+ self.dim = dim
21
+ def _activation(self, x: Tensor) -> Tensor:
22
+ raise NotImplementedError
23
+ def forward(self, x: Tensor) -> Tensor:
24
+ f, g = x.chunk(2, dim=self.dim)
25
+ return self._activation(f) * g
26
+
27
+ class SwiGLU(GatedUnit):
28
+ def __init__(self, dim: int = -1) -> None:
29
+ super().__init__(dim)
30
+ def _activation(self, x: Tensor) -> Tensor:
31
+ return silu(x)
32
+
33
+ # --- HYDRA_POOL.PY UTILS ---
34
+ class IndexedAdd(Module):
35
+ def __init__(self, n_indices: int, dim: int, weight_shape: tuple[int, ...] | None = None, *, inplace: bool = False, device=None, dtype=None) -> None:
36
+ super().__init__()
37
+ self.dim = dim
38
+ self.inplace = inplace
39
+ self.index = Buffer(torch.empty(2, n_indices, device=device, dtype=torch.int32))
40
+ self.weight = Parameter(torch.ones(*(sz if sz != -1 else n_indices for sz in weight_shape), device=device, dtype=dtype)) if weight_shape is not None else None
41
+
42
+ def forward(self, dst: Tensor, src: Tensor) -> Tensor:
43
+ src = src.index_select(self.dim, self.index[0])
44
+ if self.weight is not None: src.mul_(self.weight)
45
+ return dst.index_add_(self.dim, self.index[1], src) if self.inplace else dst.index_add(self.dim, self.index[1], src)
46
+
47
+ class BatchLinear(Module):
48
+ def __init__(self, batch_shape: tuple[int, ...] | int, in_features: int, out_features: int, *, bias: bool = False, flatten: bool = False, bias_inplace: bool = True, device=None, dtype=None) -> None:
49
+ super().__init__()
50
+ if isinstance(batch_shape, int): batch_shape = (batch_shape,)
51
+ self.flatten = -(len(batch_shape) + 1) if flatten else 0
52
+ self.weight = Parameter(torch.empty(*batch_shape, in_features, out_features, device=device, dtype=dtype))
53
+ bt = self.weight.flatten(end_dim=-3).mT
54
+ for idx in range(bt.size(0)): init.kaiming_uniform_(bt[idx], a=math.sqrt(5))
55
+ self.bias = Parameter(torch.zeros(*batch_shape, out_features, device=device, dtype=dtype)) if bias else None
56
+ self.bias_inplace = bias_inplace
57
+
58
+ def forward(self, x: Tensor) -> Tensor:
59
+ x = torch.matmul(x.unsqueeze(-2), self.weight).squeeze(-2)
60
+ if self.bias is not None:
61
+ if self.bias_inplace: x.add_(self.bias)
62
+ else: x = x + self.bias
63
+ if self.flatten: x = x.flatten(self.flatten)
64
+ return x
65
+
66
+ class Mean(Module):
67
+ def __init__(self, dim: tuple[int, ...] | int = -1, *, keepdim: bool = False) -> None:
68
+ super().__init__()
69
+ self.dim = dim
70
+ self.keepdim = keepdim
71
+ def forward(self, x: Tensor) -> Tensor:
72
+ return x.mean(self.dim, self.keepdim)
73
+
74
+ class _MidBlock(Module):
75
+ def __init__(self, attn_dim: int, head_dim: int, n_classes: int, *, ff_ratio: float, ff_dropout: float, q_cls_inplace: bool = True, device=None, dtype=None) -> None:
76
+ super().__init__()
77
+ self.head_dim = head_dim
78
+ self.q_cls_inplace = q_cls_inplace
79
+ hidden_dim = int(attn_dim * ff_ratio)
80
+ self.q_proj = Linear(attn_dim, attn_dim, bias=False, device=device, dtype=dtype)
81
+ self.q_cls = Parameter(torch.zeros(n_classes, attn_dim, device=device, dtype=dtype))
82
+ self.q_norm = RMSNorm(head_dim, eps=1e-5, elementwise_affine=False)
83
+ self.attn_out = Linear(attn_dim, attn_dim, bias=False, device=device, dtype=dtype)
84
+ self.ff_norm = LayerNorm(attn_dim, device=device, dtype=dtype)
85
+ self.ff_in = Linear(attn_dim, hidden_dim * 2, bias=False, device=device, dtype=dtype)
86
+ self.ff_act = SwiGLU()
87
+ self.ff_drop = Dropout(ff_dropout)
88
+ self.ff_out = Linear(hidden_dim, attn_dim, bias=False, device=device, dtype=dtype)
89
+
90
+ def _forward_q(self, x: Tensor) -> Tensor:
91
+ x = self.q_proj(x)
92
+ if self.q_cls_inplace: x.add_(self.q_cls)
93
+ else: x = x + self.q_cls
94
+ x = self.q_norm(x)
95
+ return rearrange(x, "... s (h e) -> ... h s e", e=self.head_dim)
96
+
97
+ def _forward_attn(self, x: Tensor, k: Tensor, v: Tensor, attn_mask: Tensor | None) -> Tensor:
98
+ a = scaled_dot_product_attention(self._forward_q(x), k, v, attn_mask=attn_mask)
99
+ a = rearrange(a, "... h s e -> ... s (h e)")
100
+ return x + self.attn_out(a)
101
+
102
+ def _forward_ff(self, x: Tensor) -> Tensor:
103
+ f = self.ff_out(self.ff_drop(self.ff_act(self.ff_in(self.ff_norm(x)))))
104
+ return x + f
105
+
106
+ def forward(self, x: Tensor, k: Tensor, v: Tensor, attn_mask: Tensor | None = None) -> Tensor:
107
+ return self._forward_ff(self._forward_attn(x, k, v, attn_mask))
108
+
109
+ class HydraPool(Module):
110
+ def __init__(self, attn_dim: int, head_dim: int, n_classes: int, *, mid_blocks: int = 0, roots: tuple[int, int, int] = (0, 0, 0), ff_ratio: float = 3.0, ff_dropout: float = 0.0, input_dim: int = -1, output_dim: int = 1, device=None, dtype=None) -> None:
111
+ super().__init__()
112
+ if input_dim < 0: input_dim = attn_dim
113
+ self.n_classes = n_classes
114
+ self.head_dim = head_dim
115
+ self.output_dim = output_dim
116
+ self._has_roots = False
117
+ self._has_ff = False
118
+ self._q_normed = None
119
+
120
+ if roots != (0, 0, 0):
121
+ self._has_roots = True
122
+ n_roots, n_classroots, n_subclasses = roots
123
+ self.cls = Parameter(torch.randn(attn_dim // head_dim, n_classes, head_dim, device=device, dtype=dtype))
124
+ self.roots = Parameter(torch.randn(attn_dim // head_dim, n_roots, head_dim, device=device, dtype=dtype)) if n_roots > 0 else None
125
+ self.clsroots = IndexedAdd(n_classroots, dim=-2, weight_shape=(attn_dim // head_dim, -1, 1), device=device, dtype=dtype) if n_classroots > 0 else None
126
+ self.clscls = IndexedAdd(n_subclasses, dim=-2, weight_shape=(attn_dim // head_dim, -1, 1), inplace=True, device=device, dtype=dtype) if n_subclasses > 0 else None
127
+ self.q = Buffer(torch.empty(attn_dim // head_dim, n_classes, head_dim, device=device, dtype=dtype))
128
+ else:
129
+ self.q = Parameter(torch.randn(attn_dim // head_dim, n_classes, head_dim, device=device, dtype=dtype))
130
+ self._q_normed = False
131
+
132
+ self.kv = Linear(input_dim, attn_dim * 2, bias=False, device=device, dtype=dtype)
133
+ self.qk_norm = RMSNorm(head_dim, eps=1e-5, elementwise_affine=False)
134
+
135
+ if ff_ratio > 0.0:
136
+ self._has_ff = True
137
+ hidden_dim = int(attn_dim * ff_ratio)
138
+ self.ff_norm = LayerNorm(attn_dim, device=device, dtype=dtype)
139
+ self.ff_in = Linear(attn_dim, hidden_dim * 2, bias=False, device=device, dtype=dtype)
140
+ self.ff_act = SwiGLU()
141
+ self.ff_drop = Dropout(ff_dropout)
142
+ self.ff_out = Linear(hidden_dim, attn_dim, bias=False, device=device, dtype=dtype)
143
+
144
+ self.mid_blocks = ModuleList(_MidBlock(attn_dim, head_dim, n_classes, ff_ratio=ff_ratio, ff_dropout=ff_dropout, device=device, dtype=dtype) for _ in range(mid_blocks))
145
+ self.out_proj = BatchLinear(n_classes, attn_dim, output_dim * 2, device=device, dtype=dtype)
146
+ self.out_act = SwiGLU()
147
+
148
+ def create_head(self) -> Module:
149
+ return Flatten(-2) if self.output_dim == 1 else Mean(-1)
150
+
151
+ def _forward_q(self) -> Tensor:
152
+ if self._q_normed is None:
153
+ q = self.qk_norm(self.roots) if self.roots is not None else self.cls
154
+ if self.clsroots is not None: q = self.clsroots(self.cls, q)
155
+ if self.clscls is not None: q = self.clscls(q, q.detach())
156
+ return self.qk_norm(q)
157
+ elif self._q_normed is False: return self.qk_norm(self.q)
158
+ else: return self.q
159
+
160
+ def _forward_attn(self, x: Tensor, attn_mask: Tensor | None) -> tuple[Tensor, Tensor, Tensor]:
161
+ q = self._forward_q().expand(*x.shape[:-2], -1, -1, -1)
162
+ x = self.kv(x)
163
+ k, v = rearrange(x, "... s (n h e) -> n ... h s e", n=2, e=self.head_dim).unbind(0)
164
+ k = self.qk_norm(k)
165
+ x = scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
166
+ return rearrange(x, "... h s e -> ... s (h e)"), k, v
167
+
168
+ def _forward_ff(self, x: Tensor) -> Tensor:
169
+ if not self._has_ff: return x
170
+ return x + self.ff_out(self.ff_drop(self.ff_act(self.ff_in(self.ff_norm(x)))))
171
+
172
+ def forward(self, x: Tensor, attn_mask: Tensor | None = None) -> Tensor:
173
+ x, k, v = self._forward_attn(x, attn_mask)
174
+ x = self._forward_ff(x)
175
+ for block in self.mid_blocks: x = block(x, k, v, attn_mask)
176
+ return self.out_act(self.out_proj(x))
177
+
178
+ @staticmethod
179
+ def for_state(state_dict: dict[str, Any], prefix: str = "", *, ff_dropout: float = 0.0, device=None, dtype=None) -> "HydraPool":
180
+ n_heads, n_classes, head_dim = state_dict[f"{prefix}q"].shape
181
+ attn_dim = n_heads * head_dim
182
+ roots_t, clsroots_t, clscls_t = state_dict.get(f"{prefix}roots"), state_dict.get(f"{prefix}clsroots.index"), state_dict.get(f"{prefix}clscls.index")
183
+ roots = (roots_t.size(1) if roots_t is not None else 0, clsroots_t.size(1) if clsroots_t is not None else 0, clscls_t.size(1) if clscls_t is not None else 0)
184
+ input_dim = state_dict[f"{prefix}kv.weight"].size(1)
185
+ output_dim = state_dict[f"{prefix}out_proj.weight"].size(2) // 2
186
+ ffout_t = state_dict.get(f"{prefix}ff_out.weight")
187
+ hidden_dim = ffout_t.size(1) + 0.5 if ffout_t is not None else 0
188
+ ff_ratio = hidden_dim / attn_dim
189
+ pattern = re.compile(rf"^{re.escape(prefix)}mid_blocks\.([0-9]+)\.")
190
+ mid_blocks = max([-1, *(int(match[1]) for key in state_dict if (match := pattern.match(key)) is not None)]) + 1
191
+ return HydraPool(attn_dim, head_dim, n_classes, mid_blocks=mid_blocks, roots=roots, ff_ratio=ff_ratio, ff_dropout=ff_dropout, input_dim=input_dim, output_dim=output_dim, device=device, dtype=dtype)
modules/localization/languages.py ADDED
@@ -0,0 +1,611 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Dil seçeneğini ConfigManager'dan (circular import olmaması için) veya direkt dosyadan okuyabiliriz.
3
+ # En sağlıklısı: Bir "current_language" global değişkeni tutmak ve bunu main.py veya config_manager update ettiğinde değiştirmek.
4
+ # Ancak basit olması açısından burada dosyayı okuyalım (cache ile).
5
+
6
+ import json
7
+ import os
8
+
9
+ _CACHED_LANG = None
10
+
11
+ def get_db_lang():
12
+ global _CACHED_LANG
13
+ # Her çağrıda dosya okumak performansı düşürebilir, bu yüzden basit bir cache mekanizması ekleyelim.
14
+ # Ancak dil değiştiğinde uygulamanın yeniden başlatılması gerektiği için bu güvenlidir.
15
+ if _CACHED_LANG: return _CACHED_LANG
16
+
17
+ config_path = "data/configs/config.json"
18
+ if os.path.exists(config_path):
19
+ try:
20
+ with open(config_path, "r", encoding="utf-8") as f:
21
+ data = json.load(f)
22
+ _CACHED_LANG = data.get("general_settings", {}).get("language", "tr")
23
+ return _CACHED_LANG
24
+ except: pass
25
+ return "tr"
26
+
27
+ def get_str(key):
28
+ lang = get_db_lang()
29
+
30
+ # 1. Hedef dil sözlüğünü al
31
+ target_dict = STRINGS.get(lang, STRINGS["tr"])
32
+
33
+ # 2. Key varsa döndür
34
+ if key in target_dict:
35
+ return target_dict[key]
36
+
37
+ # 3. Yedeğe (İngilizce'ye) bak (Eğer hedef dilde yoksa)
38
+ if lang != "en" and "en" in STRINGS and key in STRINGS["en"]:
39
+ return STRINGS["en"][key]
40
+
41
+ # 4. Hiçbir yerde yoksa key'i olduğu gibi döndür
42
+ return key
43
+
44
+ STRINGS = {
45
+ "tr": {
46
+ # Settings Tab
47
+ "tab_settings_title": "⚙️ Ayarlar",
48
+ "settings_header": "<h2 style='color:#2c3e50;'>Genel Yapılandırma ve Model Ayarları</h2>",
49
+ "save_settings_btn": "💾 Tüm Ayarları Kaydet",
50
+ "status_label": "Durum",
51
+ "device_section": "### 🖥️ Cihaz, Dil & Kural Dosyaları",
52
+ "device_label": "Çalıştırma Cihazı",
53
+ "device_info": "Modellerin hangi işlemcide çalışacağını belirler.",
54
+ "language_label": "Dil / Language",
55
+ "language_info": "Uygulama dilini değiştirir (Yeniden başlatma gerekir).",
56
+ "theme_label": "Arayüz Teması / UI Theme",
57
+ "theme_info": "Uygulama temasını değiştirir (Yeniden başlatma gerekir).",
58
+ "replacement_file_label": "Değiştirme Dosyası (Regex)",
59
+ "synonym_file_label": "Birleştirme Dosyası (Eşanlamlı)",
60
+ "addition_file_label": "Ekleme Dosyası (Sona Eklenecekler)",
61
+ "sort_order_label": "Etiket Sıralama Yöntemi",
62
+ "sort_alpha": "Alfabetik",
63
+ "sort_len": "Uzunluğa Göre",
64
+ "sort_random": "Rastgele",
65
+ "sort_orig": "Orijinal",
66
+
67
+ "model_mobilenet": "MobileNet V4 (Hızlı)",
68
+ "model_convnext": "ConvNeXt V2 Huge (Pro)",
69
+ "model_caformer": "Caformer B36 (Yeni)",
70
+ "categorization_section": "### 🗂️ Mod Bazlı Kategorizasyon Ayarları",
71
+ "categorization_desc": "Aşağıdaki sekmelerden her bir mod için ayrı kategorizasyon ayarı yapabilirsiniz.",
72
+ "tab_single": "Tekil Mod",
73
+ "tab_batch": "Toplu Mod",
74
+ "tab_dual_left": "Dual Mod (Sol)",
75
+ "tab_dual_right": "Dual Mod (Sağ)",
76
+ "enable_cat_single": "Tekil Modda Kategorizasyonu Etkinleştir",
77
+ "cat_single_label": "Tekil Mod Kategorileri",
78
+ "enable_cat_batch": "Toplu Modda Kategorizasyonu Etkinleştir",
79
+ "cat_batch_label": "Toplu Mod Kategorileri",
80
+ "enable_cat_dual1": "Dual Mod Resim 1 (Sol) Kategorizasyon",
81
+ "cat_dual1_label": "Dual Resim 1 Kategorileri",
82
+ "enable_cat_dual2": "Dual Mod Resim 2 (Sağ) Kategorizasyon",
83
+ "cat_dual2_label": "Dual Resim 2 Kategorileri",
84
+ "model_sensitivity_section": "### 🤖 Model Hassasiyet Ayarları",
85
+ "context_weight_label": "Bağlam Ağırlığı (Context Weight)",
86
+ "context_weight_info": "Etiketleri rafine ederken veritabanında olmayan 'Unknown' etiketleri temizleme şiddeti.",
87
+ "joint_settings": "Joint Tagger Ayarları",
88
+ "use_joint": "Joint Etiketleyiciyi Kullan",
89
+ "joint_thresh": "Joint Eşiği",
90
+ "cl_settings": "CL Tagger Ayarları",
91
+ "use_cl": "CL Tagger'ı Kullan",
92
+ "cl_gen_thresh": "CL General/Meta/Model Eşiği",
93
+ "cl_char_thresh": "CL Character/Copyright/Artist Eşiği",
94
+ "pixai_settings": "PixAI Tagger Ayarları",
95
+ "use_pixai": "PixAI Tagger'ı Kullan",
96
+ "pixai_gen_thresh": "PixAI General Eşiği",
97
+ "pixai_char_thresh": "PixAI Character Eşiği",
98
+ "anime_settings": "Anime Tagger (MobileNet/ConvNeXt/Caformer)",
99
+ "use_anime": "Anime Tagger'ı Kullan",
100
+ "anime_model_label": "Model Seçimi",
101
+ "anime_thresh": "Etiket Eşiği",
102
+ "gemini_settings": "✨ Gemini Caption (Doğal Dil)",
103
+ "gemini_desc": "Görselleri etiket listesi yerine **doğal cümlelerle** betimler.",
104
+ "use_gemini": "Gemini Caption Kullan",
105
+ "gemini_api_key": "Google Gemini API Key",
106
+ "gemini_model": "Gemini Modeli",
107
+ "gemini_model_info": "Kullanılacak Gemini model versiyonunu seçin.",
108
+ "gemini_mode_label": "Gemini Modu",
109
+ "gemini_mode_info": "Vision: Sadece resme bakar. Tags: Sadece etiketleri kullanır. Vision + Tags: İkisini birleştirir.",
110
+ "gemini_system_instr": "Sistem Talimatı (System Instruction)",
111
+ "gemini_system_info": "Modelin genel davranışını ve kimliğini belirleyen ana talimat.",
112
+ "gemini_prompt_settings": "#### 📝 Mod Bazlı Prompt Ayarları",
113
+ "prompt_vision": "Vision Modu Prompt",
114
+ "prompt_tags": "Tags Modu Prompt",
115
+ "prompt_hybrid": "Vision + Tags Modu Prompt",
116
+ "save_success": "✅ Tüm genel ve özelleştirilmiş kategorizasyon ayarları kaydedildi. Dil değişikliğinin etkili olması için uygulamayı yeniden başlatın.",
117
+
118
+ # App Title
119
+ "app_title": "Ultra Pro Tagger V1",
120
+ "app_subtitle": "Yapay Zeka Destekli Otomatik Etiketleme ve Düzenleme Suiti",
121
+
122
+ # Main UI
123
+ "btn_close_app": "Uygulamayı Kapat",
124
+ "btn_restart_app": "Uygulamayı Yeniden Başlat",
125
+
126
+ # Single Tab
127
+ "tab_single_title": "✨ Tekil Etiketleme",
128
+ "btn_process_single": "Görseli Etiketle (Genel Ayarlar ile)",
129
+ "label_image_upload": "Görsel Yükle",
130
+ "tab_raw_tags": "Has Etiketler",
131
+ "label_unique_tags": "Birleşik Benzersiz Etiketler",
132
+ "btn_send_to_cat": "Bu Etiketleri Kategorizasyona Gönder ➡️",
133
+ "tab_refined_tags": "Rafine Edilmiş",
134
+ "label_refined_tags": "Rafine Edilmiş (Temizlenmiş) Etiketler",
135
+ "tab_categorized_tags": "Kategorize Edilmiş",
136
+ "label_categorized_tags": "Kategorize Edilmiş Etiketler",
137
+ "header_wildcard": "### 📂 Wildcard Paketine Ekle",
138
+ "tab_gemini_caption": "Cümlelendirilmiş (GeminiCaption)",
139
+ "label_gemini_caption": "Gemini Betimlemesi",
140
+ "tab_combined_caption": "Kategorize + Cümlelendirilmiş (GeminiCaption)",
141
+ "label_combined_caption": "Kategorize ve Gemini Birleşimi",
142
+
143
+ # Batch Tab
144
+ "tab_batch_title": "📚 Toplu Etiketleme",
145
+ "btn_process_batch": "Resimleri İşle (Genel Ayarlar ile)",
146
+ "label_batch_input": "Resimleri Yükleyin (Çoklu Seçim)",
147
+ "tab_batch_original": "Orijinal Etiketler (Birleşik)",
148
+ "label_batch_original": "Tüm Resimlerden Orijinal Etiketler",
149
+ "tab_batch_refined": "Rafine Edilmiş (Birleşik)",
150
+ "label_batch_refined": "Tüm Resimlerden Rafine Edilmiş Etiketler",
151
+ "tab_batch_cat": "Kategorize Edilmiş (Satır Satır)",
152
+ "tab_batch_gemini": "Gemini Caption (Tümü)",
153
+ "label_batch_gemini": "Tüm Görsellerin Betimlemeleri",
154
+ "tab_batch_html": "Detaylı HTML Raporu",
155
+ "label_batch_html": "Detaylı Sonuçlar",
156
+ "btn_batch_download": "Tüm Sonuç Etiketleri İndir (.txt)",
157
+
158
+ # Dual Tab
159
+ "tab_dual_title": "↔️ Dual Etiketleme",
160
+ "dual_note": "<p style='color:#666; font-size:0.9em;'>Not: Dual modda her iki resim için 'Ayarlar' sekmesindeki ayrı kategorizasyon ayarları kullanılır.</p>",
161
+ "btn_process_dual": "Görselleri Karşılaştır ve Etiketle",
162
+ "label_img1": "Görsel 1",
163
+ "label_img2": "Görsel 2",
164
+ "tab_dual_combined": "Birleştirilmiş",
165
+ "label_dual_combined": "İki Resmin Birleşimi (Benzersiz)",
166
+ "label_dual_refined": "İki Resmin Birleşimi (Rafine)",
167
+ "label_dual_cat": "Birleşik Kategorize Edilmiş",
168
+ "label_gemini_img1": "Görsel 1 Gemini",
169
+ "label_gemini_img2": "Görsel 2 Gemini",
170
+
171
+ # Tools Tab
172
+ "tab_prompt_gen": "📝 Prompt Oluşturucu",
173
+ "tab_category_tools": "🏷️ Kategori Araçları",
174
+ "tab_rule_management": "🔁 Kural Yönetimi",
175
+ "tab_text_tools": "📝 Metin Araçları",
176
+ "tab_image_tools": "🖼️ Resim Araçları",
177
+ "tab_video_creator": "🎬 Video Oluşturucu",
178
+ "tab_art_studio": "🎨 Sanat Stüdyosu",
179
+ "label_tags_input": "Etiketleri girin (virgülle ayırın)",
180
+ "placeholder_tags_input": "örnek: 1kız, mavi saç, uzun saç, kedi kulakları...",
181
+
182
+ # Category Tools
183
+ "cat_live_tab_title": "Canlı Kategorizasyon",
184
+ "cat_live_header": "<h2 style='color:#2563eb'>Etiketleri Kategorilere Ayırın</h2>",
185
+ "cat_live_main_tab": "Ana Kategorizasyon",
186
+ "cat_live_main_select": "Gösterilecek Ana Kategorileri Seç",
187
+ "cat_live_main_btn": "Ana Kategorilere Ayır",
188
+ "cat_live_unmatched_btn": "⚠️ Eşleşmeyenleri Düzenleyiciye Aktar ve Düzenle ➡️",
189
+ "cat_live_main_output": "Ana Kategorize Edilmiş Etiketler",
190
+
191
+ "cat_manage_tab_title": "Kategori/Etiket Düzenleme",
192
+ "cat_manage_header": "<h3 style='color:#007bff;'>Kategori ve Etiket Dosyalarını Yönetin</h3>",
193
+ "cat_manage_main_accordion": "Ana Kategori Yönetimi",
194
+ "cat_manage_main_grid": "Tüm Kategoriler",
195
+ "cat_manage_main_refresh": "↻ Ana Kategorileri Yenile",
196
+ "cat_manage_main_select": "Kategori Seç",
197
+ "cat_manage_tags_input": "Etiketler (virgülle ayırın)",
198
+ "btn_add": "Ekle",
199
+ "btn_delete": "Sil",
200
+
201
+ # Category Redesign
202
+ "cat_redesign_live_tab": "Kategorilere Ayır",
203
+ "cat_redesign_main_tab": "📂 Kategori Yönetimi",
204
+
205
+
206
+
207
+ # Image Tools
208
+ "img_tools_header": "<h3 style='color:#0077b6;'>Toplu Resim İşleme</h3>",
209
+ "img_tools_paths_input": "Klasör Yolları (Her satıra bir yol)",
210
+ "img_tools_select_folder_btn": "📁 Klasör Seç",
211
+ "img_tools_clear_btn": "🗑️ Temizle",
212
+ "img_tools_save_settings_btn": "Yolu ve Araç Ayarlarını Kaydet",
213
+ "img_tools_res_accordion": "1. 📐 Çözünürlük",
214
+ "img_tools_scale_label": "Oran",
215
+ "btn_apply": "Uygula",
216
+ "img_tools_rename_accordion": "2. 📝 İsimlendirme (Şablonlu)",
217
+ "img_tools_rename_desc": "Dosyaları sırasıyla yeniden adlandırır. Şablonun içinde mutlaka **`{Number}`** olmalıdır.",
218
+ "img_tools_template_select": "Hazır Şablonlar",
219
+ "img_tools_delete_template_btn": "🗑️ Şablonu Sil",
220
+ "img_tools_new_template_input": "Yeni Şablon Ekle",
221
+ "img_tools_new_template_placeholder": "Örn: Adoptable {Number} (Elf)",
222
+ "img_tools_add_template_btn": "➕ Listeye Ekle",
223
+ "img_tools_start_num": "Başlangıç Numarası (Start)",
224
+ "img_tools_digit_count": "Basamak Sayısı (001 için 3)",
225
+ "img_tools_rename_btn": "Dosyaları Yeniden Adlandır",
226
+ "img_tools_watermark_accordion": "3. 🖋️ Filigran",
227
+ "img_tools_watermark_default": "Örnek Filigran",
228
+ "img_tools_watermark_text": "Metin",
229
+ "img_tools_watermark_opacity": "Opaklık",
230
+ "img_tools_watermark_size": "Font Boyutu",
231
+ "img_tools_watermark_angle": "Açı",
232
+ "img_tools_format_accordion": "4. 🖼️ Format",
233
+ "img_tools_format_label": "Format",
234
+ "btn_convert": "Dönüştür",
235
+ "img_tools_bc_accordion": "5. 🔅 Parlaklık/Kontrast",
236
+ "img_tools_brightness": "Parlaklık",
237
+ "img_tools_contrast": "Kontrast",
238
+ "img_tools_ds_accordion": "6. 🧼 Gürültü/Netlik",
239
+ "img_tools_denoise": "Gürültü Azaltma",
240
+ "img_tools_sharpen": "Netleştirme",
241
+
242
+ # Text Tools
243
+ "txt_tools_header": "<h3 style='color:#28a745;'>Manuel Metin Araçları</h3>",
244
+ "txt_tools_input": "İşlenecek Metin",
245
+ "txt_tools_tab_clean": "Temizleme",
246
+ "txt_tools_clean_btn": "Temizle ve Tekilleştir",
247
+ "label_result": "Sonuç",
248
+ "txt_tools_tab_format": "Format Değiştirme",
249
+ "txt_tools_lines_to_comma": "Satır -> Virgül",
250
+ "txt_tools_comma_to_lines": "Virgül -> Satır",
251
+ "txt_tools_tab_random": "Rastgele Seç",
252
+ "txt_tools_line_count": "Satır Sayısı",
253
+ "btn_select": "Seç",
254
+
255
+ "btn_filter": "Filtrele",
256
+
257
+ # Rule Tools
258
+ "rule_tools_intro": "Bu alanda etiketlerin nasıl değiştirileceğini, birleştirileceğini ve sonuçlara hangi etiketlerin ekleneceğini yönetebilirsiniz.",
259
+ "rule_rep_desc": "Etiketleri otomatik olarak başka etiketlerle değiştirir.",
260
+ "rule_rep_accordion": "Değiştirme Kuralları (Regex Destekli)",
261
+ "rule_active_file": "Aktif Dosya",
262
+ "rule_new_file_name": "Yeni Dosya Adı",
263
+ "rule_create_file_btn": "Yeni Dosya Oluştur",
264
+ "rule_tab_quick_add": "Hızlı Kural Ekle",
265
+ "rule_rep_old": "Eski Etiket (veya Regex)",
266
+ "rule_rep_new": "Yeni Etiket",
267
+ "rule_add_to_file_btn": "Kuralı Dosyaya Ekle",
268
+ "rule_tab_edit_file": "Dosyayı Düzenle",
269
+ "rule_file_content_manual": "Dosya İçeriği (Manuel Düzenleme)",
270
+ "rule_save_all_btn": "Tüm İçeriği Kaydet",
271
+ "rule_syn_accordion": "Birleştirme Kuralları (Eşanlamlı)",
272
+ "rule_syn_intro": "Birden fazla etiketi tek bir ana etikette birleştirir (Diğerlerini siler).",
273
+ "rule_syn_main": "Ana Etiket (Kalan)",
274
+ "rule_syn_remove": "Silinecek Eşanlamlılar (Virgülle ayırın)",
275
+ "rule_add_accordion": "Ekleme Kuralları (Sonuçlara Otomatik Eklenecekler)",
276
+ "rule_add_intro": "Bu bölümdeki etiketler, işlem bittikten sonra sonuç listesinin sonuna otomatik olarak eklenir.",
277
+ "rule_add_content_input": "Eklenecek Etiketler (Her satıra veya virgülle)",
278
+ "rule_add_save_btn": "Ekleme Kuralını Kaydet",
279
+
280
+ # Art Tools
281
+ "art_header": "### 🎨 Furry & Adoptable Araçları",
282
+ "art_desc": "Satış için grid oluşturma araçları.",
283
+ "art_tab_grid": "田 Adoptable Grid Oluşturucu",
284
+ "art_file_input": "Görselleri Seç (Çoklu)",
285
+ "art_grid_settings": "Grid Ayarları",
286
+ "art_cols": "Sütun Sayısı (Yan Yana Kaç Resim?)",
287
+ "art_bg_color": "Arkaplan Rengi",
288
+ "art_label_settings": "Etiket Ayarları",
289
+ "art_add_labels_check": "Resim Altına Yazı Ekle",
290
+ "art_label_type": "Yazı Tipi",
291
+ "art_start_num": "Başlangıç Numarası / Fiyatı",
292
+ "art_create_btn": "Grid Oluştur",
293
+ "art_output": "Grid Sonucu",
294
+
295
+ # Prompt Generator
296
+ "prompt_template_mgmt_header": "### 📝 Şablon Yönetimi",
297
+ "prompt_select_template": "Şablon Seçiniz",
298
+ "prompt_new_template_name": "Yeni Şablon Adı",
299
+ "prompt_template_content": "Şablon İçeriği",
300
+ "btn_update": "💾 Güncelle",
301
+ "btn_add_new": "➕ Yeni Ekle",
302
+ "prompt_generation_header": "### 🚀 Üretim",
303
+ "prompt_gen_btn": "SEÇİLİ ŞABLONU OLUŞTUR",
304
+ "prompt_gen_all_btn": "TÜM ŞABLONLARI OLUŞTUR",
305
+ "prompt_var_settings_accordion": "🛠️ Değişken Ayarları (Seçenek Ekle/Düzenle)",
306
+ "prompt_select_var": "Düzenlenecek Değişkeni Seç",
307
+ "prompt_var_label": "Etiket Adı",
308
+ "prompt_var_default": "Varsayılan Değer",
309
+ "prompt_var_options": "Seçenekler (Her satıra bir seçenek yazın)",
310
+ "prompt_update_var_btn": "Değişkeni Güncelle",
311
+
312
+ # Video Creator
313
+ "vid_header": "### 🎬 Pro Video Stüdyosu",
314
+ "vid_desc": "Görselleri ve müzikleri birleştirin, format seçin ve efekt ekleyin.",
315
+ "vid_img_input": "1. Görselleri Seçin (Slayt için çoklu)",
316
+ "vid_audio_input": "2. Müzikleri Seçin (Çoklu)",
317
+ "vid_resolution": "Video Formatı (Çözünürlük)",
318
+ "vid_speed": "İşleme Hızı",
319
+ "vid_text_overlay": "Video Üzerine Yazı Ekle (Opsiyonel)",
320
+ "vid_spectrum_check": "Ses Dalgası (Spectrum) Ekle (DİKKAT: Render süresini çok uzatır)",
321
+ "vid_create_btn": "VİDEOYU OLUŞTUR",
322
+ "vid_preview": "Önizleme",
323
+ "vid_status": "İşlem Durumu",
324
+ },
325
+ "en": {
326
+ # Settings Tab
327
+ "tab_settings_title": "⚙️ Tagger & Model Settings",
328
+ "settings_header": "<h2 style='color:#2c3e50;'>General Configuration and Model Settings</h2><p style='color:#666;'>Manage tagging modes, sensitivity, and language options here.</p>",
329
+ "save_settings_btn": "💾 Save All Settings",
330
+ "status_label": "Status",
331
+ "device_section": "### 🖥️ Device, Language & Rule Files",
332
+ "device_label": "Execution Device",
333
+ "device_info": "Determines which processor the models will run on.",
334
+ "language_label": "Dil / Language",
335
+ "language_info": "Changes application language (Requires restart).",
336
+ "theme_label": "UI Theme",
337
+ "theme_info": "Changes the application theme (Requires restart).",
338
+ "replacement_file_label": "Replacement File (Regex)",
339
+ "synonym_file_label": "Synonym File",
340
+ "addition_file_label": "Addition File (Append to End)",
341
+ "sort_order_label": "Tag Sorting Method",
342
+ "sort_alpha": "Alphabetical",
343
+ "sort_len": "By Length",
344
+ "sort_random": "Random",
345
+ "sort_orig": "Original",
346
+
347
+ "model_mobilenet": "MobileNet V4 (Fast)",
348
+ "model_convnext": "ConvNeXt V2 Huge (Pro)",
349
+ "model_caformer": "Caformer B36 (New)",
350
+ "categorization_section": "### 🗂️ Mode-Based Categorization Settings",
351
+ "categorization_desc": "Configure separate categorization settings for each mode below.",
352
+ "tab_single": "Single Mode",
353
+ "tab_batch": "Batch Mode",
354
+ "tab_dual_left": "Dual Mode (Left)",
355
+ "tab_dual_right": "Dual Mode (Right)",
356
+ "enable_cat_single": "Enable Categorization in Single Mode",
357
+ "cat_single_label": "Single Mode Categories",
358
+ "enable_cat_batch": "Enable Categorization in Batch Mode",
359
+ "cat_batch_label": "Batch Mode Categories",
360
+ "enable_cat_dual1": "Dual Mode Image 1 (Left) Categorization",
361
+ "cat_dual1_label": "Dual Image 1 Categories",
362
+ "enable_cat_dual2": "Dual Mode Image 2 (Right) Categorization",
363
+ "cat_dual2_label": "Dual Image 2 Categories",
364
+ "model_sensitivity_section": "### 🤖 Model Sensitivity Settings",
365
+ "context_weight_label": "Context Weight",
366
+ "context_weight_info": "Intensity of removing 'Unknown' tags not in database during refinement.",
367
+ "joint_settings": "Joint Tagger Settings",
368
+ "use_joint": "Use Joint Tagger",
369
+ "joint_thresh": "Joint Threshold",
370
+ "cl_settings": "CL Tagger Settings",
371
+ "use_cl": "Use CL Tagger",
372
+ "cl_gen_thresh": "CL General/Meta/Model Threshold",
373
+ "cl_char_thresh": "CL Character/Copyright/Artist Threshold",
374
+ "pixai_settings": "PixAI Tagger Settings",
375
+ "use_pixai": "Use PixAI Tagger",
376
+ "pixai_gen_thresh": "PixAI General Threshold",
377
+ "pixai_char_thresh": "PixAI Character Threshold",
378
+ "anime_settings": "Anime Tagger (MobileNet/ConvNeXt/Caformer)",
379
+ "use_anime": "Use Anime Tagger",
380
+ "anime_model_label": "Model Selection",
381
+ "anime_thresh": "Tag Threshold",
382
+ "gemini_settings": "✨ Gemini Caption (Natural Language)",
383
+ "gemini_desc": "Describes images with **natural sentences** instead of just tag lists.",
384
+ "use_gemini": "Use Gemini Caption",
385
+ "gemini_api_key": "Google Gemini API Key",
386
+ "gemini_model": "Gemini Model",
387
+ "gemini_model_info": "Select the Gemini model version to use.",
388
+ "gemini_mode_label": "Gemini Mode",
389
+ "gemini_mode_info": "Vision: Only looks at image. Tags: Only uses tags. Vision + Tags: Combines both.",
390
+ "gemini_system_instr": "System Instruction",
391
+ "gemini_system_info": "Main instruction defining the model's behavior and identity.",
392
+ "gemini_prompt_settings": "#### 📝 Mode-Based Prompt Settings",
393
+ "prompt_vision": "Vision Mode Prompt",
394
+ "prompt_tags": "Tags Mode Prompt",
395
+ "prompt_hybrid": "Vision + Tags Mode Prompt",
396
+ "save_success": "✅ All general and categorization settings saved. Please restart to apply language changes.",
397
+
398
+ # App Title
399
+ "app_title": "Ultra Pro Tagger V1",
400
+ "app_subtitle": "AI-Powered Automatic Tagging and Editing Suite",
401
+
402
+ # Main UI
403
+ "btn_close_app": "Close Application",
404
+ "btn_restart_app": "Restart Application",
405
+
406
+ # Single Tab
407
+ "tab_single_title": "✨ Single Tagging",
408
+ "btn_process_single": "Tag Image (With General Settings)",
409
+ "label_image_upload": "Upload Image",
410
+ "tab_raw_tags": "Raw Tags",
411
+ "label_unique_tags": "Merged Unique Tags",
412
+ "btn_send_to_cat": "Send These Tags to Categorization ➡️",
413
+ "tab_refined_tags": "Refined",
414
+ "label_refined_tags": "Refined (Cleaned) Tags",
415
+ "tab_categorized_tags": "Categorized",
416
+ "label_categorized_tags": "Categorized Tags",
417
+ "header_wildcard": "### 📂 Add to Wildcard Pack",
418
+ "tab_gemini_caption": "Captioned (GeminiCaption)",
419
+ "label_gemini_caption": "Gemini Caption",
420
+ "tab_combined_caption": "Categorized + Captioned (GeminiCaption)",
421
+ "label_combined_caption": "Categorized and Gemini Combination",
422
+
423
+ # Batch Tab
424
+ "tab_batch_title": "📚 Batch Tagging",
425
+ "btn_process_batch": "Process Images (With General Settings)",
426
+ "label_batch_input": "Upload Images (Multiple Selection)",
427
+ "tab_batch_original": "Original Tags (Combined)",
428
+ "label_batch_original": "Original Tags from All Images",
429
+ "tab_batch_refined": "Refined (Combined)",
430
+ "label_batch_refined": "Refined Tags from All Images",
431
+ "tab_batch_cat": "Categorized (Line by Line)",
432
+ "tab_batch_gemini": "Gemini Caption (All)",
433
+ "label_batch_gemini": "Captions for All Images",
434
+ "tab_batch_html": "Detailed HTML Report",
435
+ "label_batch_html": "Detailed Results",
436
+ "btn_batch_download": "Download All Result Tags (.txt)",
437
+
438
+ # Dual Tab
439
+ "tab_dual_title": "↔️ Dual Tagging",
440
+ "dual_note": "<p style='color:#666; font-size:0.9em;'>Note: Dual mode uses separate categorization settings for each image from the 'Settings' tab.</p>",
441
+ "btn_process_dual": "Compare and Tag Images",
442
+ "label_img1": "Image 1",
443
+ "label_img2": "Image 2",
444
+ "tab_dual_combined": "Combined",
445
+ "label_dual_combined": "Combined Unique Tags",
446
+ "label_dual_refined": "Combined Refined Tags",
447
+ "label_dual_cat": "Combined Categorized",
448
+ "label_gemini_img1": "Image 1 Gemini",
449
+ "label_gemini_img2": "Image 2 Gemini",
450
+
451
+ # Tools Tab
452
+ "tab_prompt_gen": "📝 Prompt Generator",
453
+ "tab_category_tools": "🏷️ Category Tools",
454
+ "tab_rule_management": "🔁 Rule Management",
455
+ "tab_text_tools": "📝 Text Tools",
456
+ "tab_image_tools": "🖼️ Image Tools",
457
+ "tab_video_creator": "🎬 Video Creator",
458
+ "tab_art_studio": "🎨 Art Studio",
459
+ "label_tags_input": "Enter Tags (comma separated)",
460
+ "placeholder_tags_input": "example: 1girl, blue hair, long hair, cat ears...",
461
+
462
+ # Category Tools
463
+ "cat_live_tab_title": "Live Categorization",
464
+ "cat_live_header": "<h2 style='color:#2563eb'>Categorize Tags</h2>",
465
+ "cat_live_main_tab": "Main Categorization",
466
+ "cat_live_main_select": "Select Main Categories to Show",
467
+ "cat_live_main_btn": "Categorize by Main",
468
+ "cat_live_unmatched_btn": "⚠️ Transfer Unmatched to Editor and Edit ➡️",
469
+ "cat_live_main_output": "Main Categorized Tags",
470
+ "cat_live_sub_tab": "Sub Categorization",
471
+ "cat_live_sub_select": "Select Sub Categories to Show",
472
+ "cat_live_sub_btn": "Categorize by Sub",
473
+ "cat_live_sub_output": "Sub Categorized Tags",
474
+ "cat_manage_tab_title": "Category/Tag Editing",
475
+ "cat_manage_header": "<h3 style='color:#007bff;'>Manage Category and Tag Files</h3>",
476
+ "cat_manage_main_accordion": "Main Category Management",
477
+ "cat_manage_main_grid": "All Categories",
478
+ "cat_manage_main_refresh": "↻ Refresh Main Categories",
479
+ "cat_manage_main_select": "Select Category",
480
+ "cat_manage_tags_input": "Tags (comma separated)",
481
+ "btn_add": "Add",
482
+ "btn_delete": "Delete",
483
+ "cat_manage_sub_accordion": "Sub Category Management",
484
+ "cat_manage_sub_grid": "All Sub Categories",
485
+ "cat_manage_sub_refresh": "↻ Refresh Sub Categories",
486
+ # Category Redesign
487
+ "cat_redesign_live_tab": "⚡ Live Test",
488
+ "cat_redesign_main_tab": "📂 Main Category Management",
489
+ "cat_redesign_sub_tab": "📁 Sub Category Management",
490
+ "cat_manage_sub_select": "Select Sub Category",
491
+
492
+ # Image Tools
493
+ "img_tools_header": "<h3 style='color:#0077b6;'>Batch Image Processing</h3>",
494
+ "img_tools_paths_input": "Folder Paths (One per line)",
495
+ "img_tools_select_folder_btn": "📁 Select Folder",
496
+ "img_tools_clear_btn": "🗑️ Clear",
497
+ "img_tools_save_settings_btn": "Save Path and Tool Settings",
498
+ "img_tools_res_accordion": "1. 📐 Resolution",
499
+ "img_tools_scale_label": "Scale",
500
+ "btn_apply": "Apply",
501
+ "img_tools_rename_accordion": "2. 📝 Renaming (Templated)",
502
+ "img_tools_rename_desc": "Renames files sequentially. Template must include **`{Number}`**.",
503
+ "img_tools_template_select": "Ready Templates",
504
+ "img_tools_delete_template_btn": "🗑️ Delete Template",
505
+ "img_tools_new_template_input": "Add New Template",
506
+ "img_tools_new_template_placeholder": "Ex: Adoptable {Number} (Elf)",
507
+ "img_tools_add_template_btn": "➕ Add to List",
508
+ "img_tools_start_num": "Start Number",
509
+ "img_tools_digit_count": "Digits (e.g. 3 for 001)",
510
+ "img_tools_rename_btn": "Rename Files",
511
+ "img_tools_watermark_accordion": "3. 🖋️ Watermark",
512
+ "img_tools_watermark_default": "Sample Watermark",
513
+ "img_tools_watermark_text": "Text",
514
+ "img_tools_watermark_opacity": "Opacity",
515
+ "img_tools_watermark_size": "Font Size",
516
+ "img_tools_watermark_angle": "Angle",
517
+ "img_tools_format_accordion": "4. 🖼️ Format",
518
+ "img_tools_format_label": "Format",
519
+ "btn_convert": "Convert",
520
+ "img_tools_bc_accordion": "5. 🔅 Brightness/Contrast",
521
+ "img_tools_brightness": "Brightness",
522
+ "img_tools_contrast": "Contrast",
523
+ "img_tools_ds_accordion": "6. 🧼 Denoise/Sharpen",
524
+ "img_tools_denoise": "Denoise",
525
+ "img_tools_sharpen": "Sharpen",
526
+
527
+ # Text Tools
528
+ "txt_tools_header": "<h3 style='color:#28a745;'>Manual Text Tools</h3>",
529
+ "txt_tools_input": "Text to Process",
530
+ "txt_tools_tab_clean": "Cleaning",
531
+ "txt_tools_clean_btn": "Clean and Uniquify",
532
+ "label_result": "Result",
533
+ "txt_tools_tab_format": "Change Format",
534
+ "txt_tools_lines_to_comma": "Lines -> Comma",
535
+ "txt_tools_comma_to_lines": "Comma -> Lines",
536
+ "txt_tools_tab_random": "Select Random",
537
+ "txt_tools_line_count": "Line Count",
538
+ "btn_select": "Select",
539
+ "txt_tools_tab_refine": "Refine",
540
+ "txt_tools_refine_desc": "Words to remove are read from **`{filename}`**.",
541
+ "btn_filter": "Filter",
542
+
543
+ # Rule Tools
544
+ "rule_tools_intro": "Manage how tags are replaced, merged, and appended to results in this area.",
545
+ "rule_rep_desc": "Automatically replaces tags with other tags.",
546
+ "rule_rep_accordion": "Replacement Rules (Regex Supported)",
547
+ "rule_active_file": "Active File",
548
+ "rule_new_file_name": "New File Name",
549
+ "rule_create_file_btn": "Create New File",
550
+ "rule_tab_quick_add": "Quick Add Rule",
551
+ "rule_rep_old": "Old Tag (or Regex)",
552
+ "rule_rep_new": "New Tag",
553
+ "rule_add_to_file_btn": "Add Rule to File",
554
+ "rule_tab_edit_file": "Edit File",
555
+ "rule_file_content_manual": "File Content (Manual Edit)",
556
+ "rule_save_all_btn": "Save All Content",
557
+ "rule_syn_accordion": "Merging Rules (Synonyms)",
558
+ "rule_syn_intro": "Merges multiple tags into one main tag (Deletes others).",
559
+ "rule_syn_main": "Main Tag (Kept)",
560
+ "rule_syn_remove": "Synonyms to Delete (Comma separated)",
561
+ "rule_add_accordion": "Addition Rules (Auto Append)",
562
+ "rule_add_intro": "Tags in this section are automatically appended to the end of the result list.",
563
+ "rule_add_content_input": "Tags to Add (Line by line or comma)",
564
+ "rule_add_save_btn": "Save Addition Rules",
565
+
566
+ # Art Tools
567
+ "art_header": "### 🎨 Furry & Adoptable Tools",
568
+ "art_desc": "Grid creation tools for sales.",
569
+ "art_tab_grid": "田 Adoptable Grid Creator",
570
+ "art_file_input": "Select Images (Multiple)",
571
+ "art_grid_settings": "Grid Settings",
572
+ "art_cols": "Columns (How many images side-by-side?)",
573
+ "art_bg_color": "Background Color",
574
+ "art_label_settings": "Label Settings",
575
+ "art_add_labels_check": "Add Text Below Image",
576
+ "art_label_type": "Label Type",
577
+ "art_start_num": "Start Number / Price",
578
+ "art_create_btn": "Create Grid",
579
+ "art_output": "Grid Result",
580
+
581
+ # Prompt Generator
582
+ "prompt_template_mgmt_header": "### 📝 Template Management",
583
+ "prompt_select_template": "Select Template",
584
+ "prompt_new_template_name": "New Template Name",
585
+ "prompt_template_content": "Template Content",
586
+ "btn_update": "💾 Update",
587
+ "btn_add_new": "➕ Add New",
588
+ "prompt_generation_header": "### 🚀 Generation",
589
+ "prompt_gen_btn": "GENERATE SELECTED TEMPLATE",
590
+ "prompt_gen_all_btn": "GENERATE ALL TEMPLATES",
591
+ "prompt_var_settings_accordion": "🛠️ Variable Settings (Add/Edit Options)",
592
+ "prompt_select_var": "Select Variable to Edit",
593
+ "prompt_var_label": "Label Name",
594
+ "prompt_var_default": "Default Value",
595
+ "prompt_var_options": "Options (One per line)",
596
+ "prompt_update_var_btn": "Update Variable",
597
+
598
+ # Video Creator
599
+ "vid_header": "### 🎬 Pro Video Studio",
600
+ "vid_desc": "Combine images and music, select format and add effects.",
601
+ "vid_img_input": "1. Select Images (Multiple for Slideshow)",
602
+ "vid_audio_input": "2. Select Music (Multiple)",
603
+ "vid_resolution": "Video Format (Resolution)",
604
+ "vid_speed": "Processing Speed",
605
+ "vid_text_overlay": "Add Text Over Video (Optional)",
606
+ "vid_spectrum_check": "Add Audio Spectrum (WARNING: Increases render time significantly)",
607
+ "vid_create_btn": "CREATE VIDEO",
608
+ "vid_preview": "Preview",
609
+ "vid_status": "Process Status",
610
+ },
611
+ }
modules/managers/category_manager.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import functools
3
+ import re
4
+ import gradio as gr
5
+ import random
6
+
7
+ # --- Klasör Sabitleri ---
8
+ KATEGORILER_KLASORU = "data/rules/kategoriler"
9
+ ALT_KATEGORILER_KLASORU = "data/rules/alt_kategoriler"
10
+
11
+ # --- KATEGORİZASYON ---
12
+
13
+ @functools.lru_cache(maxsize=32)
14
+ def kategori_sozlugu_yukle():
15
+ kategori_sozluk = {}
16
+ if not os.path.exists(KATEGORILER_KLASORU): os.makedirs(KATEGORILER_KLASORU, exist_ok=True); return {}
17
+ try:
18
+ for dosya in os.listdir(KATEGORILER_KLASORU):
19
+ if dosya.endswith(".txt"):
20
+ kat = dosya.replace(".txt", "").replace("_", " ")
21
+ with open(os.path.join(KATEGORILER_KLASORU, dosya), "r", encoding="utf-8") as f:
22
+ kategori_sozluk[kat] = {s.strip().lower() for s in f if s.strip()}
23
+ except: pass
24
+ return kategori_sozluk
25
+
26
+
27
+
28
+ def kategorizelendir_ileri(girdi_etiketleri_str, secilen_kategoriler_listesi, kategori_sozluk_ref):
29
+ if not girdi_etiketleri_str or not secilen_kategoriler_listesi: return ""
30
+ girdi = [t.strip().lower() for t in girdi_etiketleri_str.split(",") if t.strip()]
31
+ secilen = set(secilen_kategoriler_listesi)
32
+ sonuc = []
33
+ for t in girdi:
34
+ for k, v in kategori_sozluk_ref.items():
35
+ if k in secilen and t in v:
36
+ sonuc.append(t); break
37
+ return ", ".join(sorted(list(set(sonuc))))
38
+
39
+ def kategori_html_ve_kopyala(kategori_adi, etiketler):
40
+ renk_map = {
41
+ 0: ("#fef9c3", "#fde047", "🔶"), 1: ("#dbeafe", "#60a5fa", "🔷"),
42
+ 2: ("#fee2e2", "#f87171", "🟥"), 3: ("#dcfce7", "#4ade80", "🟩"),
43
+ 4: ("#f3e8ff", "#c084fc", "🟪"), "DİĞER / EŞLEŞMEYENLER": ("#f1f5f9", "#94a3b8", "🚫")
44
+ }
45
+ idx = 0
46
+ try: idx = int(kategori_adi.split()[0])
47
+ except: pass
48
+
49
+ if kategori_adi.upper().startswith("DİĞER"): renk, border, ikon = renk_map["DİĞER / EŞLEŞMEYENLER"]
50
+ else: renk, border, ikon = renk_map.get(idx % 5, ("#f3f4f6", "#d1d5db", "🗂️"))
51
+
52
+ text_id = f"text_{random.randint(0,999999)}"
53
+ metin = ", ".join(etiketler)
54
+ chipler = "".join([f"<span style='background:rgba(255,255,255,0.6); padding:4px 8px; border-radius:6px; border:1px solid rgba(0,0,0,0.1); font-size:0.9em; color:#374151; display:inline-block;'>{e}</span>" for e in sorted(etiketler)])
55
+
56
+ html = f"""
57
+ <div style="background:{renk}; border:2px solid {border}; border-radius:12px; padding:12px; margin-bottom:15px; display:flex; flex-direction:column; gap:10px; box-shadow: 0 2px 5px rgba(0,0,0,0.05);">
58
+ <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid rgba(0,0,0,0.05); padding-bottom:8px;">
59
+ <div style="font-weight:bold; font-size:1.05em; color:#1f2937; display:flex; align-items:center; gap:6px;">
60
+ <span>{ikon}</span> <span>{kategori_adi.upper()}</span>
61
+ <span style="background:rgba(0,0,0,0.1); color:#4b5563; font-size:0.8em; padding:1px 6px; border-radius:10px;">{len(etiketler)}</span>
62
+ </div>
63
+ <button onclick="navigator.clipboard.writeText(document.getElementById('{text_id}').value); this.innerText='Kopyalandı!'" style="background:#2563eb; color:white; border:none; padding:4px 12px; border-radius:6px; cursor:pointer; font-size:0.85em; font-weight:500; transition: background 0.2s;">Kopyala</button>
64
+ </div>
65
+ <div style="display:flex; flex-wrap:wrap; gap:6px; max-height:150px; overflow-y:auto; padding-right:5px;">
66
+ {chipler if etiketler else "<span style='color:#6b7280; font-style:italic; font-size:0.9em;'>Bu kategoride etiket bulunamadı.</span>"}
67
+ </div>
68
+ <textarea id='{text_id}' style='display:none;'>{metin}</textarea>
69
+ </div>
70
+ """
71
+ return html
72
+
73
+ def full_kategori_html_ve_kopyala(kategoriler, etiketler_list):
74
+ html = "<div style='margin-bottom:20px;'>"
75
+ for k, e in zip(kategoriler, etiketler_list): html += kategori_html_ve_kopyala(k, e)
76
+ return html + "</div>"
77
+
78
+ def etiket_ekle(kategori, etiketler_str):
79
+ if not kategori: return gr.update(value="Kategori seçin")
80
+ yeni_etiketler = {e.strip().lower() for e in etiketler_str.split(",") if e.strip()}
81
+ if not yeni_etiketler: return gr.update(value="Eklenecek geçerli etiket yok.")
82
+ try:
83
+ if os.path.exists(KATEGORILER_KLASORU):
84
+ for dosya in os.listdir(KATEGORILER_KLASORU):
85
+ if dosya.endswith(".txt"):
86
+ dosya_kategori_adi = dosya.replace(".txt", "").replace("_", " ")
87
+ if dosya_kategori_adi == kategori: continue
88
+ dosya_yolu = os.path.join(KATEGORILER_KLASORU, dosya)
89
+ mevcut_diger = set()
90
+ with open(dosya_yolu, "r", encoding="utf-8") as f: mevcut_diger = {l.strip().lower() for l in f if l.strip()}
91
+ if not mevcut_diger.isdisjoint(yeni_etiketler):
92
+ guncel_etiketler = mevcut_diger - yeni_etiketler
93
+ with open(dosya_yolu, "w", encoding="utf-8") as f:
94
+ for e in sorted(guncel_etiketler): f.write(e + "\n")
95
+ safe = re.sub(r'[^\w\.-]', '_', kategori)
96
+ path = os.path.join(KATEGORILER_KLASORU, f"{safe}.txt")
97
+ mevcut_hedef = set()
98
+ if os.path.exists(path):
99
+ with open(path, "r", encoding="utf-8") as f: mevcut_hedef = {l.strip().lower() for l in f if l.strip()}
100
+ with open(path, "w", encoding="utf-8") as f:
101
+ for e in sorted(mevcut_hedef | yeni_etiketler): f.write(e + "\n")
102
+ kategori_sozlugu_yukle.cache_clear()
103
+ return gr.update(value=f"✅ {len(yeni_etiketler)} etiket '{kategori}' kategorisine taşındı.", visible=True)
104
+ except Exception as e: return gr.update(value=f"❌ Hata: {e}")
105
+
106
+ def etiket_sil(kategori, etiketler_str):
107
+ safe = re.sub(r'[^\w\.-]', '_', kategori)
108
+ path = os.path.join(KATEGORILER_KLASORU, f"{safe}.txt")
109
+ if not os.path.exists(path): return gr.update(value="Dosya yok")
110
+ sil = {e.strip().lower() for e in etiketler_str.split(",") if e.strip()}
111
+ try:
112
+ mevcut = {l.strip().lower() for l in open(path, encoding="utf-8") if l.strip()}
113
+ with open(path, "w", encoding="utf-8") as f:
114
+ for e in sorted(mevcut - sil): f.write(e + "\n")
115
+ kategori_sozlugu_yukle.cache_clear()
116
+ return gr.update(value=f"{len(sil & mevcut)} etiket silindi.", visible=True)
117
+ except Exception as e: return gr.update(value=f"Hata: {e}")
118
+
119
+ def kategorileri_grid_html():
120
+ d = kategori_sozlugu_yukle()
121
+ html = "<div class='kategori-grid-app2'>"
122
+ for i, k in enumerate(sorted(d.keys())):
123
+ count = len(d[k])
124
+ # Kart yapısı
125
+ html += f"""
126
+ <div class='kategori-card-app2'>
127
+ <div class='kategori-title-app2'>{k.upper()}</div>
128
+ <div style='margin-top:auto; display:flex; justify-content:space-between; align-items:center;'>
129
+ <span style='font-size:0.85em; color:#64748b;'>Toplam Etiket</span>
130
+ <span style='background:#f1f5f9; color:#334151; padding:4px 12px; border-radius:20px; font-weight:bold; font-size:0.9em; border:1px solid #e2e8f0;'>{count}</span>
131
+ </div>
132
+ </div>
133
+ """
134
+ return html + "</div>"
135
+
136
+ def grid_guncelle():
137
+ kategori_sozlugu_yukle.cache_clear()
138
+ return kategorileri_grid_html()
139
+
140
+
141
+
142
+
143
+
144
+ def kategori_listesi(): return list(kategori_sozlugu_yukle().keys())
145
+
146
+
147
+ def filter_and_display_main_categorized_tags(girdi_etiketleri_str, selected_main_categories):
148
+ ref = kategori_sozlugu_yukle()
149
+ girdi_etiketleri = {etiket.strip().lower() for etiket in girdi_etiketleri_str.split(",") if etiket.strip()}
150
+ sonuc = {k: [] for k in ref if k in (selected_main_categories if selected_main_categories else ref.keys())}
151
+ diger = set()
152
+ for etiket in girdi_etiketleri:
153
+ found = False
154
+ for kategori, kume in ref.items():
155
+ if etiket in kume and kategori in sonuc:
156
+ sonuc[kategori].append(etiket); found = True; break
157
+ if not found: diger.add(etiket)
158
+ cats, tags = [], []
159
+ if diger: cats.append("DİĞER / EŞLEŞMEYENLER"); tags.append(sorted(list(diger)))
160
+ for k in sorted(sonuc.keys()): cats.append(k); tags.append(sorted(sonuc[k]))
161
+
162
+ html_output = full_kategori_html_ve_kopyala(cats, tags)
163
+ eslesmeyen_str = ", ".join(sorted(list(diger)))
164
+ return html_output, eslesmeyen_str
165
+
166
+
167
+
modules/managers/localization_manager.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from modules.config_manager import load_config
3
+ from modules.localization.languages import STRINGS
4
+
5
+ class LocalizationManager:
6
+ _instance = None
7
+
8
+ def __new__(cls):
9
+ if cls._instance is None:
10
+ cls._instance = super(LocalizationManager, cls).__new__(cls)
11
+ cls._instance.config = load_config()
12
+ cls._instance.language = cls._instance.config.get("general_settings", {}).get("language", "tr")
13
+ cls._instance.strings = STRINGS.get(cls._instance.language, STRINGS["tr"])
14
+ return cls._instance
15
+
16
+ def get(self, key):
17
+ """Returns the localized string for the given key."""
18
+ return self.strings.get(key, key)
19
+
20
+ def set_language(self, lang_code):
21
+ if lang_code in STRINGS:
22
+ self.language = lang_code
23
+ self.strings = STRINGS[lang_code]
24
+ return True
25
+ return False
26
+
27
+ # Global instance
28
+ loc = LocalizationManager()
29
+
30
+ def get_str(key):
31
+ return loc.get(key)
modules/managers/rule_manager.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import functools
3
+ import re
4
+ import gradio as gr
5
+
6
+ # --- Klasör Sabitleri ---
7
+ REPLACEMENT_RULES_KLASORU = "data/rules/replacement_rules"
8
+ SYNONYM_RULES_KLASORU = "data/rules/synonym_rules"
9
+ ADDITION_RULES_KLASORU = "data/rules/addition_rules"
10
+
11
+ # --- DEĞİŞTİRME KURALLARI (REPLACEMENT) ---
12
+
13
+ @functools.lru_cache(maxsize=32)
14
+ def get_replacement_files():
15
+ if not os.path.exists(REPLACEMENT_RULES_KLASORU): os.makedirs(REPLACEMENT_RULES_KLASORU); return []
16
+ return [os.path.join(REPLACEMENT_RULES_KLASORU, f) for f in os.listdir(REPLACEMENT_RULES_KLASORU) if f.endswith(".txt")]
17
+
18
+ @functools.lru_cache(maxsize=32)
19
+ def load_replacement_rules(file_path):
20
+ rules = {}
21
+ if not file_path or not os.path.exists(file_path): return rules
22
+ try:
23
+ with open(file_path, "r", encoding="utf-8") as f:
24
+ for line in f:
25
+ line = line.strip()
26
+ if not line or line.startswith('#'): continue
27
+ parts = line.split('->')
28
+ if len(parts) == 2:
29
+ old_tag = parts[0].strip().lower().replace(r'\b', '')
30
+ new_tag = parts[1].strip().lower()
31
+ rules[old_tag] = new_tag
32
+ return rules
33
+ except: return {}
34
+
35
+ def apply_replacements(tags_string, replacement_file_path):
36
+ if not tags_string or not replacement_file_path: return tags_string
37
+ rules = load_replacement_rules(replacement_file_path)
38
+ if not rules: return tags_string
39
+ tags = [t.strip().lower() for t in tags_string.split(",") if t.strip()]
40
+ processed_tags = []
41
+ for tag in tags:
42
+ if tag in rules: processed_tags.append(rules[tag])
43
+ else: processed_tags.append(tag)
44
+ return ", ".join(processed_tags)
45
+
46
+ def create_replacement_file(file_name):
47
+ if not file_name.strip(): return "Hata"
48
+ path = os.path.join(REPLACEMENT_RULES_KLASORU, f"{re.sub(r'[^\w\.-]', '_', file_name)}.txt")
49
+ if not os.path.exists(REPLACEMENT_RULES_KLASORU): os.makedirs(REPLACEMENT_RULES_KLASORU)
50
+ with open(path, "w", encoding="utf-8") as f:
51
+ f.write("# Eski Etiket -> Yeni Etiket\n")
52
+ f.write("# blue_hair -> cyan hair\n")
53
+ f.write("# Artık regex kullanmanıza gerek yoktur, tam eşleşme yapılır.\n")
54
+ get_replacement_files.cache_clear()
55
+ return f"Oluşturuldu: {path}"
56
+
57
+ def read_replacement_file_content(file_path):
58
+ try: return open(file_path, "r", encoding="utf-8").read()
59
+ except: return ""
60
+
61
+ def save_replacement_file_content(file_path, content):
62
+ try:
63
+ with open(file_path, "w", encoding="utf-8") as f: f.write(content)
64
+ load_replacement_rules.cache_clear()
65
+ return "Kaydedildi"
66
+ except: return "Hata"
67
+
68
+ def append_replacement_rule_to_file(file_path, old_tag, new_tag):
69
+ if not file_path or not os.path.exists(file_path): return gr.update(value="❌ Geçerli bir dosya seçili değil.")
70
+ if not old_tag.strip(): return gr.update(value="❌ Eski etiket boş olamaz.")
71
+ clean_old = old_tag.strip()
72
+ clean_new = new_tag.strip()
73
+ rule_line = f"\n{clean_old} -> {clean_new}"
74
+ try:
75
+ with open(file_path, "a", encoding="utf-8") as f: f.write(rule_line)
76
+ load_replacement_rules.cache_clear()
77
+ return gr.update(value=f"✅ Kural eklendi: '{clean_old}' -> '{clean_new}'", visible=True)
78
+ except Exception as e: return gr.update(value=f"❌ Hata: {e}", visible=True)
79
+
80
+ # --- BİRLEŞTİRME KURALLARI (SYNONYM) ---
81
+
82
+ @functools.lru_cache(maxsize=32)
83
+ def get_synonym_files():
84
+ if not os.path.exists(SYNONYM_RULES_KLASORU): os.makedirs(SYNONYM_RULES_KLASORU); return []
85
+ return [os.path.join(SYNONYM_RULES_KLASORU, f) for f in os.listdir(SYNONYM_RULES_KLASORU) if f.endswith(".txt")]
86
+
87
+ @functools.lru_cache(maxsize=32)
88
+ def load_synonym_rules(file_path):
89
+ rules = {}
90
+ if not file_path or not os.path.exists(file_path): return rules
91
+ try:
92
+ with open(file_path, "r", encoding="utf-8") as f:
93
+ for line in f:
94
+ if ':' in line and not line.strip().startswith('#'):
95
+ p = line.split(':', 1)
96
+ rules.setdefault(p[0].strip().lower(), set()).update({s.strip().lower() for s in p[1].split(',') if s.strip()})
97
+ return rules
98
+ except: return {}
99
+
100
+ def apply_synonym_consolidation(tags_string, synonym_file_path):
101
+ if not tags_string or not synonym_file_path: return tags_string
102
+ rules = load_synonym_rules(synonym_file_path)
103
+ tag_set = {t.strip() for t in tags_string.split(',') if t.strip()}
104
+ remove = set()
105
+ for main, syns in rules.items():
106
+ if main in tag_set:
107
+ for s in syns:
108
+ if s in tag_set: remove.add(s)
109
+ return ", ".join(list(tag_set - remove))
110
+
111
+ def create_synonym_file(file_name):
112
+ path = os.path.join(SYNONYM_RULES_KLASORU, f"{re.sub(r'[^\w\.-]', '_', file_name)}.txt")
113
+ if not os.path.exists(SYNONYM_RULES_KLASORU): os.makedirs(SYNONYM_RULES_KLASORU)
114
+ with open(path, "w", encoding="utf-8") as f: f.write("# main: s1, s2\n")
115
+ get_synonym_files.cache_clear()
116
+ return f"Oluşturuldu: {path}"
117
+
118
+ def read_synonym_file_content(file_path):
119
+ try: return open(file_path, "r", encoding="utf-8").read()
120
+ except: return ""
121
+
122
+ def save_synonym_file_content(file_path, content):
123
+ try:
124
+ with open(file_path, "w", encoding="utf-8") as f: f.write(content)
125
+ load_synonym_rules.cache_clear()
126
+ return "Kaydedildi"
127
+ except: return "Hata"
128
+
129
+ def append_synonym_rule_to_file(file_path, main_tag, remove_tags_str):
130
+ if not file_path or not os.path.exists(file_path): return gr.update(value="❌ Geçerli bir dosya seçili değil.")
131
+ if not main_tag.strip() or not remove_tags_str.strip(): return gr.update(value="❌ Ana etiket veya silinecekler boş olamaz.")
132
+ rule_line = f"\n{main_tag.strip()}: {remove_tags_str.strip()}"
133
+ try:
134
+ with open(file_path, "a", encoding="utf-8") as f: f.write(rule_line)
135
+ load_synonym_rules.cache_clear()
136
+ return gr.update(value=f"✅ Kural eklendi: '{main_tag}' şunları silecek: {remove_tags_str}", visible=True)
137
+ except Exception as e: return gr.update(value=f"❌ Hata: {e}", visible=True)
138
+
139
+ # --- EKLEME KURALLARI (ADDITION) ---
140
+
141
+ @functools.lru_cache(maxsize=32)
142
+ def get_addition_files():
143
+ if not os.path.exists(ADDITION_RULES_KLASORU): os.makedirs(ADDITION_RULES_KLASORU); return []
144
+ return [os.path.join(ADDITION_RULES_KLASORU, f) for f in os.listdir(ADDITION_RULES_KLASORU) if f.endswith(".txt")]
145
+
146
+ @functools.lru_cache(maxsize=32)
147
+ def load_addition_rules(file_path):
148
+ tags = []
149
+ if not file_path or not os.path.exists(file_path): return ""
150
+ try:
151
+ with open(file_path, "r", encoding="utf-8") as f:
152
+ for line in f:
153
+ line = line.strip()
154
+ if not line or line.startswith('#'): continue
155
+ parts = [t.strip() for t in line.split(',') if t.strip()]
156
+ tags.extend(parts)
157
+ return ", ".join(tags)
158
+ except: return ""
159
+
160
+ def apply_additions(tags_string, addition_file_path):
161
+ if not addition_file_path: return tags_string
162
+ additional_tags = load_addition_rules(addition_file_path)
163
+ if not additional_tags: return tags_string
164
+ if not tags_string.strip(): return additional_tags
165
+ return f"{tags_string}, {additional_tags}"
166
+
167
+ def create_addition_file(file_name):
168
+ if not file_name.strip(): return "Hata"
169
+ path = os.path.join(ADDITION_RULES_KLASORU, f"{re.sub(r'[^\w\.-]', '_', file_name)}.txt")
170
+ if not os.path.exists(ADDITION_RULES_KLASORU): os.makedirs(ADDITION_RULES_KLASORU)
171
+ with open(path, "w", encoding="utf-8") as f:
172
+ f.write("# İşlem sonunda eklenecek etiketler (virgülle veya yeni satırla)\n")
173
+ f.write("# best quality, masterpiece\n")
174
+ get_addition_files.cache_clear()
175
+ return f"Oluşturuldu: {path}"
176
+
177
+ def read_addition_file_content(file_path):
178
+ try: return open(file_path, "r", encoding="utf-8").read()
179
+ except: return ""
180
+
181
+ def save_addition_file_content(file_path, content):
182
+ try:
183
+ with open(file_path, "w", encoding="utf-8") as f: f.write(content)
184
+ load_addition_rules.cache_clear()
185
+ return "Kaydedildi"
186
+ except: return "Hata"
modules/prompt_generator.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/prompt_generator.py
2
+
3
+ import gradio as gr
4
+ import json
5
+ import os
6
+ import re
7
+ import random
8
+ from modules.localization.languages import get_str
9
+
10
+ # --- Veriler ve Sabitler ---
11
+
12
+ SABLON_DOSYA_YOLU = "data/configs/prompt_sablonlar.json"
13
+ DEGISKENLER_DOSYA_YOLU = "data/configs/prompt_variables.json"
14
+
15
+ # Varsayılan Veriler (İlk kurulum için)
16
+ varsayilan_etiketler_ham = [
17
+ "Güzellik ve Makyaj", "Karakterin Yaşı", "Göz Rengi", "Saç Rengi ve Stili",
18
+ "Göğüs ve Göğüs Büyüklüğü", "Dönüşüm Öncesi Yüz İfadesi", "Dönüşüm Sırasındaki Yüz İfadesi",
19
+ "Dönüşüm Sonrası Yüz İfadesi", "Dönüşülen Hayvan", "Dönüşülen Hayvanın Fiziksel Özellikleri",
20
+ "Arkaplan/Mekan", "Dönüşüm Öncesi Poz", "Dönüşüm Esnası Poz 1", "Dönüşüm Esnası Poz 2",
21
+ "Dönüşüm Sonu Poz", "Dönüşüm Sonrası Poz 1 (Nude 1)", "Dönüşüm Sonrası Poz 2 (Nude 2)",
22
+ "Dönüşüm Sonrası Poz 3 (Blowjob)", "Dönüşüm Sonrası Poz 4 (Blowjob 2)", "Dönüşüm Sonrası Poz 5 (Missionary Sex)",
23
+ "Dönüşüm Sonrası Poz 6 (Cowgirl Position Sex)", "Dönüşüm Sonrası Poz 7 (Doggystyle Sex)",
24
+ "Dönüşüm Sonrası Poz 8 (Doggystyle Sex From Side)", "Dönüşüm Sonrası Poz 9 (Carrying Sex)",
25
+ "Dönüşüm Sonrası Poz 10 (On Side Sex)", "Dönüşüm Sonrası Poz 11 (Reverse Cowgirl Sex)",
26
+ "Dönüşüm Sonrası Poz 12 (After Sex Ejaculation)"
27
+ ]
28
+
29
+ varsayilan_degerler_ham = [
30
+ "sexy", "25", "green eyes", "hair, red hair, long hair",
31
+ "breasts, fake tits, huge breasts", "seductive smile, closed mouth",
32
+ "(open mouth:1.1), (shocked, surprised expression:1.2), (wide-eyed:1.3), (screaming in pain:1.2), crying, gritting teeth, painful expression",
33
+ "(moaning, embarrassed:1.2)", "deer",
34
+ "animal ears, anthro, biped, black nose, brown body, brown fur, deer, deer ears, deer girl, deer tail, fur, furry, furry female, inner ear fluff, mammal, markings, multicolored body, multicolored fur, short tail, spikes, spots, spotted body, spotted fur, tail, tuft, white body, white fur, antlers, white antlers",
35
+ "blurry, blurry background, forest, nature, outdoors, outside, plant, tree",
36
+ "accessory, bare shoulders, blue ribbon, blush, bodily fluids, breasts, cleavage, cleavage cutout, closed mouth, clothed, clothing, clothing cutout, cowboy shot, dress, gloves, handwear, looking at viewer, ribbon, ribbons, sensitive, short dress, sleeveless, sleeveless dress, solo, sweat, white clothing, white dress, white gloves",
37
+ "all fours, blush, collarbone, genitals, kneeling, leaning, leaning backward, looking at viewer, nipples, nude, solo",
38
+ "all fours, blush, collarbone, genitals, kneeling, leaning, leaning backward, looking at viewer, nipples, nude, solo",
39
+ "all fours, blush, collarbone, kneeling, leaning, leaning backward, looking at viewer, nipples, nude, solo",
40
+ "anus, breasts with fur, clitoral hood, clitoris, completely nude, explicit, genitals, leg up, legs up, looking at viewer, looking up, lying, nipples, nude, on back, open mouth, paw pose, presenting, presenting pussy, pussy, solo, spread legs, spreading, teeth, tongue, tongue out, from above",
41
+ "all fours, anus, barefoot, bodily fluids, breasts with fur, completely nude, explicit, from behind, genitals, looking at viewer, looking back, nipples, nude, open mouth, presenting, pussy, solo, teeth, tongue, tongue out",
42
+ "all fours, anthro on human, anthro penetrated, ass up, balls, blush, bodily fluids, butt from the front, completely nude, deepthroat, duo, erection, explicit, faceless character, faceless male, fellatio, first person view, genitals, human male penetrating, human male penetrating anthro, human on anthro, human penetrating anthro, human pov, human/anthro, looking at viewer, lying, male, male on anthro, male pov, male/anthro, nude, oral, oral penetration, penetrating pov, penetration, penile, penile penetration, penis, penis in mouth, pov, saliva, saliva on penis, seductive, sex, solo focus, spread legs, spreading, sweat, sweaty butt, testicles, vein, wet",
43
+ "anthro penetrated, balls, breasts with fur, completely nude, duo, erection, explicit, faceless character, faceless human, faceless male, fellatio, from side, genitals, hetero, human male penetrating, human male penetrating anthro, human on anthro, human penetrating, human penetrating anthro, human/anthro, humanoid genitalia, humanoid penis, looking at another, male, male/anthro, nipples, nude, open mouth, oral, oral penetration, penetration, penile, penile penetration, penis, penis in mouth, pink nipples, sex, side view, solo focus, testicles",
44
+ "anthro penetrated, breasts with fur, cum, cum from pussy, cum in pussy, cum inside, cum on fur, duo, erection, explicit, faceless character, faceless male, first person view, from front position, genitals, human male penetrating, human male penetrating anthro, human penetrating, human penetrating anthro, human pov, human/anthro, humanoid penis, leaking cum, looking pleasured, lying anthro, male, male pov, male/anthro, missionary, missionary position, nipples, open mouth, orgasm, penetrating pov, penetration, penile, penile penetration, penis, penis in pussy, pov, pov crotch, pussy, saliva, sex, solo focus, spread legs, spreading, tongue, tongue out, vaginal, vaginal penetration, from above, male kneeling",
45
+ "anthro penetrated, arms behind back, breasts with fur, collarbone, completely nude, cowgirl position, duo, erection, explicit, first person view, from front position, genitals, anthro on top, hetero, human male penetrating, human male penetrating anthro, human on anthro, human penetrating, human penetrating anthro, human/anthro, looking at viewer, male, male on anthro, male on bottom, male pov, male/anthro, nipples, nude, on bottom, on top, open mouth, penetration, penile, penile penetration, penis, penis in pussy, pov, pussy, sex, solo focus, spread legs, straddling, sweat, vaginal, vaginal penetration, vein, veiny penis, male lying, anthro kneeling",
46
+ "ahegao, all fours, anthro penetrated, anus, back, back boob, balls, big penis, blush, bodily fluids, breasts with fur, deep penetration, doggystyle, duo, erection, explicit, first person view, from behind, from behind position, fucked silly, genital fluids, genitals, glistening, glistening butt, hetero, huge penis, human male penetrating, human male penetrating anthro, human on anthro, human penetrating, human penetrating anthro, human/anthro, humanoid genitalia, humanoid penis, large penis, looking at viewer, looking back, looking back at viewer, looking pleasured, male, male pov, male/anthro, open mouth, penetrating pov, penetration, penile, penile penetration, penis, penis in pussy, pov, pussy, pussy juice, rear view, sex, sex from behind, solo focus, sweat, sweaty butt, tongue, tongue out, trembling, vaginal, vaginal fluids, vaginal penetration, vein, veiny penis",
47
+ "all fours, ambiguous penetration, anthro penetrated, bodily fluids, breasts with fur, breath, butt grab, completely nude, cum, cum in pussy, cum inside, cum overflow, doggystyle, duo, explicit, faceless character, faceless male, from behind position, genital fluids, hand on butt, hetero, human male penetrating, human male penetrating anthro, human penetrated, human penetrating, human/anthro, looking pleasured, male, male/anthro, motion lines, nipples, nude, open mouth, penetration, penile, saliva, sex, sex from behind, solo focus, sweat, tongue, tongue out",
48
+ "anthro penetrated, balls, breasts with fur, clitoris, cum, cum in pussy, cum inside, duo, erection, explicit, folded, fucked silly, full nelson, genital fluids, human male penetrating, human male penetrating anthro, human on anthro, human penetrating, human penetrating anthro, human/anthro, humanoid penis, large penis, legs up, looking pleasured, male, male/anthro, nipples, nude, open mouth, penetration, penile, penile penetration, penis, penis in pussy, pussy, reverse suspended congress, saliva, sex, sex from behind, solo focus, spread legs, teeth, testicles, tongue, tongue out, butt fur, feet out of frame, male standing, anthro on lap, fur legs",
49
+ "anthro penetrated, breasts with fur, clitoral hood, clitoris, duo, erection, explicit, faceless character, faceless male, human male penetrating, human male penetrating anthro, human on anthro, human penetrating, human penetrating anthro, human/anthro, leg lift, looking at viewer, lying, male, male on anthro, male/anthro, nipples, nude, on side, open mouth, penetration, penile, penile penetration, penis, penis in mouth, pussy, pussy juice, raised leg, sex, anthro focus, spread legs, spreading, vein, veiny penis, fur legs, male kneeling, anthro lying, male grabbing",
50
+ "anthro penetrated, anus, back, balls, big penis, breasts with fur, duo, erection, explicit, eye roll, faceless character, faceless human, faceless male, anthro on top, first person view, from behind, fucked silly, genital fluids, genitals, happy, hetero, human male penetrating, human male penetrating anthro, human on anthro, human penetrating, human penetrating anthro, human/anthro, humanoid penis, looking back, looking pleasured, lying, male, male on anthro, male on bottom, male pov, male/anthro, nipples, nude, on bottom, on top, open mouth, penetration, penile, penile penetration, penis, penis in pussy, pov, puckered anus, pussy, reverse cowgirl position, sex, sex from behind, solo focus, straddling, toned, tongue, tongue out, vaginal, vaginal penetration, vein, veiny penis, male lying, from below",
51
+ "after sex, anus, balls, breasts with fur, completely nude, cum, cum from pussy, cum in pussy, cum inside, cum on fur, cum on penis, cumdrip, duo, erection, explicit, faceless character, faceless male, first person view, hetero, human on anthro, human pov, human/anthro, humanoid penis, leaking cum, looking at viewer, lying, male, male pov, male/anthro, nipples, nude, on back, on back, open mouth, penis, pov, pussy, solo focus, spread legs, spreading, sweat, teeth, from above, anthro lying, male kneeling"
52
+ ]
53
+
54
+ VARSAYILAN_SABLONLAR = {
55
+ "Kalıp 1: Dönüşüm Öncesi": "{0}, {1}yo, {2}, {3}, {4}, {5}, {11}, {10}",
56
+ "Kalıp 2: Dönüşüm Esnası 1": "(mammal humanoid, {8} humanoid:1.1), transformation, species transformation, {9}, The scene features a {8} hybrid, {8} fur, {8} tail, {8} snout and ears, photomorph, {6}, {0}, {1}yo, {2}, {3}, {4}, {12}, {10}",
57
+ "Kalıp 3: Dönüşüm Esnası 2": "(mammal humanoid, {8} anthro:1.1), transformation, species transformation, {9}, The scene features a {8} hybrid, {8} fur, {8} tail, {8} snout and ears, photomorph, {6}, {0}, {1}yo, {2}, {3}, {4}, {12}, {10}",
58
+ "Kalıp 5: Dönüşüm Sonu": "(mammal anthro, {8} anthro:1.1), transformation, species transformation, {9}, The scene features a {8} anthro, {8} fur, {8} tail, {8} snout and ears, photomorph, {6}, {0}, {1}yo, {2}, {3}, {4}, {12}, {10}",
59
+ }
60
+
61
+ sablonlar = {}
62
+ degiskenler = [] # Liste: [{'id': 0, 'label': '...', 'default': '...', 'options': [...]}]
63
+
64
+ # --- Yardımcı Fonksiyonlar ---
65
+
66
+ def sort_keys_naturally(keys):
67
+ def extract_number(key):
68
+ match = re.search(r'Kalıp (\d+):', key)
69
+ if match: return int(match.group(1))
70
+ return 9999
71
+ return sorted(keys, key=extract_number)
72
+
73
+ def sablonlari_yukle():
74
+ global sablonlar
75
+ os.makedirs(os.path.dirname(SABLON_DOSYA_YOLU), exist_ok=True)
76
+ loaded_data = {}
77
+ if os.path.exists(SABLON_DOSYA_YOLU):
78
+ try:
79
+ with open(SABLON_DOSYA_YOLU, "r", encoding="utf-8") as f:
80
+ loaded_data = json.load(f)
81
+ except: loaded_data = {}
82
+
83
+ for k, v in VARSAYILAN_SABLONLAR.items():
84
+ if k not in loaded_data: loaded_data[k] = v
85
+
86
+ sorted_keys = sort_keys_naturally(list(loaded_data.keys()))
87
+ sablonlar = {k: loaded_data[k] for k in sorted_keys}
88
+ sablonlari_kaydet()
89
+
90
+ def sablonlari_kaydet():
91
+ try:
92
+ with open(SABLON_DOSYA_YOLU, "w", encoding="utf-8") as f:
93
+ json.dump(sablonlar, f, ensure_ascii=False, indent=4)
94
+ return "Şablonlar başarıyla kaydedildi."
95
+ except Exception as e: return f"Kaydetme hatası: {e}"
96
+
97
+ def degiskenleri_yukle():
98
+ """Değişkenleri JSON'dan yükler, yoksa varsayılan listelerden oluşturur."""
99
+ global degiskenler
100
+ os.makedirs(os.path.dirname(DEGISKENLER_DOSYA_YOLU), exist_ok=True)
101
+
102
+ if os.path.exists(DEGISKENLER_DOSYA_YOLU):
103
+ try:
104
+ with open(DEGISKENLER_DOSYA_YOLU, "r", encoding="utf-8") as f:
105
+ degiskenler = json.load(f)
106
+ except:
107
+ degiskenler = []
108
+
109
+ # Dosya yoksa veya boşsa varsayılanları yükle
110
+ if not degiskenler and varsayilan_etiketler_ham:
111
+ for i, (etiket, deger) in enumerate(zip(varsayilan_etiketler_ham, varsayilan_degerler_ham)):
112
+ degiskenler.append({
113
+ "id": i,
114
+ "label": etiket,
115
+ "default": deger,
116
+ "options": [deger]
117
+ })
118
+ degiskenleri_kaydet()
119
+
120
+ def degiskenleri_kaydet():
121
+ try:
122
+ with open(DEGISKENLER_DOSYA_YOLU, "w", encoding="utf-8") as f:
123
+ json.dump(degiskenler, f, ensure_ascii=False, indent=4)
124
+ return "Değişkenler kaydedildi."
125
+ except Exception as e: return f"Hata: {e}"
126
+
127
+ # Başlangıç Yüklemeleri
128
+ sablonlari_yukle()
129
+ degiskenleri_yukle()
130
+
131
+ def metin_olusturucu(sablon_metni, *girdiler):
132
+ try:
133
+ clean_inputs = [str(g).strip() for g in girdiler]
134
+ sonuc_metni = sablon_metni.format(*clean_inputs)
135
+ return sonuc_metni.strip()
136
+ except IndexError: return "Hata: Şablondaki değişken sayısı, girilen değer sayısından fazla."
137
+ except Exception as e: return f"Bilinmeyen hata: {e}"
138
+
139
+ def sablon_secim_degisti(secim):
140
+ return sablonlar.get(secim, "")
141
+
142
+ def mevcut_sablonu_guncelle(secim_adi, yeni_icerik):
143
+ if not secim_adi: return "Lütfen bir şablon seçin."
144
+ sablonlar[secim_adi] = yeni_icerik
145
+ sonuc = sablonlari_kaydet()
146
+ return f"'{secim_adi}' güncellendi. {sonuc}"
147
+
148
+ def yeni_sablon_ekle(yeni_ad, yeni_icerik):
149
+ if not yeni_ad: return "Hata: İsim boş.", gr.update(), gr.update(), gr.update()
150
+ if yeni_ad in sablonlar: return "Hata: İsim zaten var.", gr.update(), gr.update(), gr.update()
151
+ sablonlar[yeni_ad] = yeni_icerik
152
+ sorted_keys = sort_keys_naturally(list(sablonlar.keys()))
153
+ sablonlar = {k: sablonlar[k] for k in sorted_keys}
154
+ sablonlari_kaydet()
155
+ # Hem generatör hem manager dropdown'ını güncelle
156
+ update = gr.update(choices=list(sablonlar.keys()), value=yeni_ad)
157
+ return f"'{yeni_ad}' eklendi.", update, update, yeni_icerik
158
+
159
+ def sablon_sil(secim_adi):
160
+ if not secim_adi: return "Hata: Seçim yok.", gr.update(), gr.update(), gr.update()
161
+ if secim_adi in sablonlar:
162
+ del sablonlar[secim_adi]
163
+ sablonlari_kaydet()
164
+ keys = list(sablonlar.keys())
165
+ val = keys[0] if keys else None
166
+ cont = sablonlar[val] if val else ""
167
+ update = gr.update(choices=keys, value=val)
168
+ return f"'{secim_adi}' silindi.", update, update, cont
169
+ return "Hata: Bulunamadı.", gr.update(), gr.update(), gr.update()
170
+
171
+ def tum_metinleri_olustur(*girdiler):
172
+ tum_sonuclar = []
173
+ clean_inputs = [str(g).strip() for g in girdiler]
174
+ for ad, sablon in sablonlar.items():
175
+ try:
176
+ text = sablon.format(*clean_inputs)
177
+ # DÜZELTME: Artık başlıklar eklenmiyor, sadece metin ekleniyor.
178
+ tum_sonuclar.append(f"{text}")
179
+ except: pass
180
+ # Sonuçlar arasında 2 satır boşluk bırakılır.
181
+ return "\n\n".join(tum_sonuclar)
182
+
183
+ # --- Değişken Seçenekleri Yönetim Fonksiyonları ---
184
+
185
+ def get_degisken_bilgisi(label_with_id):
186
+ """Dropdown'dan seçilen etikete göre bilgileri getirir. Seçenekleri satır satır döndürür."""
187
+ try:
188
+ # "{0} - Göz Rengi" formatından ID'yi ayıkla
189
+ match = re.match(r"\{(\d+)\}", label_with_id)
190
+ if not match: return "", "", ""
191
+
192
+ index = int(match.group(1))
193
+
194
+ if 0 <= index < len(degiskenler):
195
+ d = degiskenler[index]
196
+ # Seçenekleri listesinden alıp satır satır string'e çevir
197
+ opts = "\n".join(d.get("options", []))
198
+ return d.get("label", ""), d.get("default", ""), opts
199
+ except:
200
+ pass
201
+ return "", "", ""
202
+
203
+ def degisken_guncelle(label_with_id, label_new, default_val, options_text):
204
+ """Mevcut değişkenin etiketini ve seçeneklerini günceller. (Satır bazlı)"""
205
+ global degiskenler
206
+
207
+ try:
208
+ match = re.match(r"\{(\d+)\}", label_with_id)
209
+ if not match: return "Hata: Geçersiz değişken seçimi.", gr.update(), gr.update(), gr.update()
210
+
211
+ index = int(match.group(1))
212
+
213
+ # Satır satır gelen metni listeye çevir
214
+ options_list = [line.strip() for line in options_text.split('\n') if line.strip()]
215
+
216
+ # Seçenekler boşsa default değeri ekle
217
+ if not options_list and default_val:
218
+ options_list.append(default_val)
219
+
220
+ # Default değer seçeneklerde yoksa en başa ekle (Kullanım kolaylığı için)
221
+ if default_val and default_val not in options_list:
222
+ options_list.insert(0, default_val)
223
+
224
+ if 0 <= index < len(degiskenler):
225
+ degiskenler[index]["label"] = label_new
226
+ degiskenler[index]["default"] = default_val
227
+ degiskenler[index]["options"] = options_list
228
+
229
+ degiskenleri_kaydet()
230
+
231
+ msg = f"Değişken {{{index}}} güncellendi."
232
+ # Seçenekler metin kutusuna geri satır satır yazılır
233
+ updated_options_text = "\n".join(options_list)
234
+
235
+ return (
236
+ msg,
237
+ gr.update(choices=options_list, value=default_val, label=f"{{{index}}} - {label_new}"), # Ana UI kutusu
238
+ gr.update(choices=[f"{{{d['id']}}} - {d['label']}" for d in degiskenler], value=f"{{{index}}} - {label_new}"), # Yönetim listesi
239
+ gr.update(value=updated_options_text)
240
+ )
241
+ else:
242
+ return "Hata: Değişken bulunamadı.", gr.update(), gr.update(), gr.update()
243
+
244
+ except Exception as e:
245
+ return f"Hata: {e}", gr.update(), gr.update(), gr.update()
246
+
247
+ # --- UI ---
248
+
249
+ def create_prompt_generator_ui():
250
+ degiskenleri_yukle() # UI oluşturulurken en güncel veriyi çek
251
+
252
+ # Değişken listesini yönetim paneli için hazırla
253
+ degisken_secim_listesi = [f"{{{d['id']}}} - {d['label']}" for d in degiskenler]
254
+
255
+ # Helper function for dropdown
256
+ def get_sablon_keys():
257
+ return list(sablonlar.keys())
258
+
259
+ sablon_keys = get_sablon_keys()
260
+
261
+ unsorted_inputs = [] # Generator inputs container
262
+ giris_kutulari = [] # Final sorted inputs
263
+
264
+ with gr.Tabs():
265
+
266
+ # --- 1. SEKME: ÜRETİCİ (GENERATOR) ---
267
+ with gr.TabItem("🚀 Üretici"):
268
+ gr.Markdown("### 📝 Hızlı Prompt Oluştur")
269
+
270
+ with gr.Row():
271
+ sablon_secici_generator = gr.Dropdown(choices=sablon_keys, label=get_str("prompt_select_template"), interactive=True, scale=3)
272
+ btn_olustur = gr.Button(get_str("prompt_gen_btn"), variant="primary", scale=1)
273
+ btn_tumu_olustur = gr.Button(get_str("prompt_gen_all_btn"), variant="secondary", scale=1)
274
+
275
+ cikti_alani = gr.Textbox(label=get_str("label_result"), lines=5, interactive=False, show_copy_button=True)
276
+
277
+ with gr.Accordion("Gelişmiş Değişken Girdileri", open=True):
278
+ # Dinamik Girdi Kutuları (Dropdown) - 3 Kolonlu düzen
279
+ with gr.Row():
280
+ # Kolon 1
281
+ with gr.Column():
282
+ for i in range(0, len(degiskenler), 3):
283
+ d = degiskenler[i]
284
+ comp = gr.Dropdown(choices=d["options"], label=d["label"], value=d["default"], interactive=True)
285
+ unsorted_inputs.append((i, comp))
286
+ # Kolon 2
287
+ with gr.Column():
288
+ for i in range(1, len(degiskenler), 3):
289
+ d = degiskenler[i]
290
+ comp = gr.Dropdown(choices=d["options"], label=d["label"], value=d["default"], interactive=True)
291
+ unsorted_inputs.append((i, comp))
292
+ # Kolon 3
293
+ with gr.Column():
294
+ for i in range(2, len(degiskenler), 3):
295
+ d = degiskenler[i]
296
+ comp = gr.Dropdown(choices=d["options"], label=d["label"], value=d["default"], interactive=True)
297
+ unsorted_inputs.append((i, comp))
298
+
299
+ # Listeyi indekse göre sırala ve sadece componentleri al
300
+ unsorted_inputs.sort(key=lambda x: x[0])
301
+ giris_kutulari = [x[1] for x in unsorted_inputs]
302
+
303
+ # --- 2. SEKME: ŞABLON YÖNETİCİSİ ---
304
+ with gr.TabItem("🛠️ Şablon Yöneticisi"):
305
+ gr.Markdown(get_str("prompt_template_mgmt_header"))
306
+
307
+ with gr.Row():
308
+ sablon_secici_manager = gr.Dropdown(choices=sablon_keys, label="Düzenlenecek Şabloyu Seç", interactive=True, scale=2)
309
+ btn_sil = gr.Button("🗑️ Seçili Şablonu Sil", variant="stop", scale=1)
310
+
311
+ gr.Markdown("---")
312
+ gr.Markdown("#### Şablon Ekle / Düzenle")
313
+
314
+ yeni_sablon_adi = gr.Textbox(label=get_str("prompt_new_template_name"), placeholder="Yeni şablon ise adını girin (Mevcut birini seçtiyseniz burası otomatik dolar)")
315
+ sablon_icerik = gr.Textbox(label=get_str("prompt_template_content"), lines=12, interactive=True, placeholder="Şablon içeriğini buraya yazın. Değişkenler için {0}, {1}... kullanın.")
316
+
317
+ with gr.Row():
318
+ btn_guncelle = gr.Button(get_str("btn_update"), variant="primary")
319
+ btn_yeni_ekle = gr.Button(get_str("btn_add_new"), variant="secondary")
320
+
321
+ status_msg = gr.Textbox(label=get_str("status_label"), interactive=False, visible=True)
322
+
323
+ # --- 3. SEKME: DEĞİŞKEN YÖNETİCİSİ ---
324
+ with gr.TabItem("🧩 Değişken Yöneticisi"):
325
+ gr.Markdown("### Değişkenleri Düzenle")
326
+
327
+ yonetim_secim_dd = gr.Dropdown(
328
+ label=get_str("prompt_select_var"),
329
+ choices=degisken_secim_listesi,
330
+ value=degisken_secim_listesi[0] if degisken_secim_listesi else None,
331
+ interactive=True
332
+ )
333
+
334
+ with gr.Row():
335
+ yonetim_label = gr.Textbox(label=get_str("prompt_var_label"), interactive=True)
336
+ yonetim_default = gr.Textbox(label=get_str("prompt_var_default"), interactive=True)
337
+
338
+ yonetim_options = gr.Textbox(
339
+ label=get_str("prompt_var_options"),
340
+ placeholder="Mavi Göz\nYeşil Göz\nEla Göz",
341
+ lines=8,
342
+ interactive=True
343
+ )
344
+
345
+ yonetim_kaydet_btn = gr.Button(get_str("prompt_update_var_btn"), variant="primary")
346
+ yonetim_durum = gr.Textbox(label=get_str("status_label"), visible=False)
347
+
348
+ # --- Event Bağlantıları ---
349
+
350
+ # 1. Üretici (Generator) Eventleri
351
+ # Şablon seçildiğinde içeriği almamıza gerek yok, içerik backend'de tutuluyor.
352
+ # Ama generate fonksiyonuna içeriği text olarak göndermek daha esnek.
353
+ # Bu yüzden generator'da gizli bir textbox tutabiliriz veya direk seçimi gönderebiliriz.
354
+ # Mevcut yapıda 'sablon_icerik' kullanılıyordu. Generator sekmesinde bunu gizli bir state veya textbox olarak tutalım.
355
+ gen_gizli_icerik = gr.State("")
356
+
357
+ sablon_secici_generator.change(fn=sablon_secim_degisti, inputs=sablon_secici_generator, outputs=gen_gizli_icerik)
358
+
359
+ # Oluşturma
360
+ btn_olustur.click(fn=metin_olusturucu, inputs=[gen_gizli_icerik] + giris_kutulari, outputs=cikti_alani)
361
+ btn_tumu_olustur.click(fn=tum_metinleri_olustur, inputs=giris_kutulari, outputs=cikti_alani)
362
+
363
+ # 2. Şablon Yöneticisi Eventleri
364
+
365
+ # Seçim değişince ismi ve içeriği doldur
366
+ def manager_select_change(secim):
367
+ content = sablon_secim_degisti(secim)
368
+ return secim, content # İsim kutusuna ve içerik kutusuna yaz
369
+
370
+ sablon_secici_manager.change(fn=manager_select_change, inputs=sablon_secici_manager, outputs=[yeni_sablon_adi, sablon_icerik])
371
+
372
+ # Güncelle (Sadece içeriği güncelle, isim zaten seçili)
373
+ # create_prompt_generator_ui içinde helper
374
+ def manager_update_wrapper(name, content):
375
+ if not name: return "Lütfen bir şablon se��in veya adını yazın."
376
+ return mevcut_sablonu_guncelle(name, content)
377
+
378
+ btn_guncelle.click(fn=manager_update_wrapper, inputs=[yeni_sablon_adi, sablon_icerik], outputs=status_msg)
379
+
380
+ # Yeni Ekle
381
+ # Çıktılar: status, gen_dropdown, man_dropdown, content_box (temizleme veya aynı kalma?)
382
+ btn_yeni_ekle.click(
383
+ fn=yeni_sablon_ekle,
384
+ inputs=[yeni_sablon_adi, sablon_icerik],
385
+ outputs=[status_msg, sablon_secici_generator, sablon_secici_manager, sablon_icerik]
386
+ )
387
+
388
+ # Sil
389
+ btn_sil.click(
390
+ fn=sablon_sil,
391
+ inputs=[sablon_secici_manager],
392
+ outputs=[status_msg, sablon_secici_generator, sablon_secici_manager, sablon_icerik]
393
+ )
394
+
395
+ # 3. Değişken Yöneticisi Eventleri
396
+ yonetim_secim_dd.change(
397
+ fn=get_degisken_bilgisi,
398
+ inputs=[yonetim_secim_dd],
399
+ outputs=[yonetim_label, yonetim_default, yonetim_options]
400
+ )
401
+
402
+ def yonetim_wrapper(dd_val, lbl, dflt, opts):
403
+ res = degisken_guncelle(dd_val, lbl, dflt, opts)
404
+ return res[0], res[2], res[3]
405
+
406
+ yonetim_kaydet_btn.click(
407
+ fn=yonetim_wrapper,
408
+ inputs=[yonetim_secim_dd, yonetim_label, yonetim_default, yonetim_options],
409
+ outputs=[yonetim_durum, yonetim_secim_dd, yonetim_options]
410
+ ).then(
411
+ fn=None,
412
+ _js="() => { alert('Değişken güncellendi! Seçeneklerin ana ekrana yansıması için lütfen sayfayı yenileyin veya uygulamayı yeniden başlatın.'); }"
413
+ )
modules/shared_state.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # modules/shared_state.py
3
+
4
+ import time
5
+
6
+ class SharedState:
7
+ def __init__(self):
8
+ self.web_image_data = None
9
+ self.last_update_time = 0
10
+ self.force_tab_switch = False
11
+ self.new_image_event = False
12
+
13
+ shared_state = SharedState()
14
+
15
+ def set_web_image(data):
16
+ """Web'den gelen resim verisini ayarlar (Base64 string)."""
17
+ shared_state.web_image_data = data
18
+ shared_state.last_update_time = time.time()
19
+ shared_state.force_tab_switch = True
20
+ shared_state.new_image_event = True
21
+
22
+ def reset_image_event():
23
+ """Resim işlendiğinde event'i sıfırlar."""
24
+ shared_state.new_image_event = False
25
+
26
+ def check_and_reset_tab_switch():
27
+ """Sekme değişikliği gerekiyorsa True döner ve flag'i sıfırlar."""
28
+ if shared_state.force_tab_switch:
29
+ shared_state.force_tab_switch = False
30
+ return True
31
+ return False
32
+
33
+ def get_web_image():
34
+ """Web'den gelen son resim verisini ve zaman damgasını döndürür."""
35
+ return shared_state.web_image_data, shared_state.last_update_time
modules/tagger.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import tempfile
4
+ import gradio as gr
5
+ from PIL import Image
6
+
7
+ # Import Tagger Processors
8
+ from modules.taggers.joint import JointTaggerProcessor, run_joint_classifier
9
+ from modules.taggers.cl import CLTaggerProcessor
10
+ from modules.taggers.pixai import PixaiTaggerProcessor
11
+ from modules.taggers.anime import AnimeTaggerProcessor
12
+ from modules.taggers.gemini import GeminiTaggerProcessor
13
+
14
+ # Import Managers and Utils
15
+ from modules.managers.category_manager import kategori_sozlugu_yukle, kategorizelendir_ileri
16
+ from modules.tagger_refinement import rafine_etiketler
17
+ from modules.utils.tag_utils import unique_and_sort_tags
18
+ from modules.managers.rule_manager import apply_additions
19
+
20
+ # --- CL Tagger Modülünü İçe Aktarma ---
21
+ try:
22
+ from modules.cl_tagger_module import CLTagger
23
+ except ImportError:
24
+ try: from .cl_tagger_module import CLTagger # Fallback for relative import
25
+ except ImportError:
26
+ print("cl_tagger_module.py bulunamadı.")
27
+ CLTagger = None
28
+
29
+ # Global Instance Variables
30
+ cl_tagger_instance = None
31
+ try:
32
+ if CLTagger:
33
+ cl_tagger_instance = CLTagger()
34
+ print("CL Tagger (ONNX) başarıyla yüklendi.")
35
+ except Exception as e:
36
+ print(f"CL Tagger (ONNX) yüklenirken hata: {e}")
37
+ cl_tagger_instance = None
38
+
39
+ # --- Ortak İşleme Fonksiyonu ---
40
+ def _process_single_image(
41
+ image,
42
+ joint_thresh, use_joint,
43
+ cl_gen_thresh, cl_char_thresh, use_cl_tagger,
44
+ pixai_general_thresh, pixai_char_thresh, use_pixai_tagger,
45
+ animetagger_model, animetagger_thresh, use_animetagger,
46
+ use_gemini, gemini_api_key, gemini_mode, gemini_model_id,
47
+ gemini_prompt_vision, gemini_prompt_tags, gemini_prompt_hybrid,
48
+ gemini_system_instruction,
49
+ replacement_file_path, synonym_file_path, addition_file_path,
50
+ sort_order="Alfabetik",
51
+ device: str = "Auto",
52
+ context_weight: float = 0.0,
53
+ enable_cat_for_gemini=False, selected_cats_for_gemini=None
54
+ ):
55
+ if image is None: return "", "", "", "⚠️ Resim yüklenmedi.", []
56
+
57
+ alert_messages = []
58
+ all_tags = set()
59
+ all_raw_tags_for_original_sort = []
60
+ gemini_result_text = ""
61
+
62
+ current_joint_processor = JointTaggerProcessor(replacement_file_path, synonym_file_path, addition_file_path)
63
+ current_cl_tagger_processor = CLTaggerProcessor(cl_tagger_instance, replacement_file_path, synonym_file_path, addition_file_path)
64
+ current_pixai_processor = PixaiTaggerProcessor(replacement_file_path, synonym_file_path, addition_file_path)
65
+ current_animetagger_processor = AnimeTaggerProcessor(replacement_file_path, synonym_file_path, addition_file_path)
66
+ current_gemini_processor = GeminiTaggerProcessor()
67
+
68
+ if use_joint:
69
+ j_raw, j_alert, j_order = current_joint_processor.predict(image, joint_thresh, replacement_file_path, synonym_file_path, addition_file_path, sort_order, device)
70
+ if "Hata" not in j_raw and "❌" not in j_alert:
71
+ all_tags.update(t.strip() for t in j_raw.split(',') if t.strip())
72
+ all_raw_tags_for_original_sort.extend(j_order)
73
+ else: alert_messages.append(f"Joint: {j_alert}")
74
+
75
+ if use_cl_tagger:
76
+ cl_raw, cl_alert, cl_order = current_cl_tagger_processor.predict(image, cl_gen_thresh, cl_char_thresh, replacement_file_path, synonym_file_path, addition_file_path, sort_order)
77
+ if "Hata" not in cl_raw and "❌" not in cl_alert:
78
+ all_tags.update(t.strip() for t in cl_raw.split(',') if t.strip())
79
+ all_raw_tags_for_original_sort.extend(cl_order)
80
+ else: alert_messages.append(f"CL: {cl_alert}")
81
+
82
+ if use_pixai_tagger:
83
+ p_raw, p_alert, p_order = current_pixai_processor.predict(image, pixai_general_thresh, pixai_char_thresh, replacement_file_path, synonym_file_path, addition_file_path, "Orijinal", device)
84
+ if "Hata" not in p_raw and "❌" not in p_alert:
85
+ all_tags.update(t.strip() for t in p_raw.split(',') if t.strip())
86
+ all_raw_tags_for_original_sort.extend(p_order)
87
+ else: alert_messages.append(f"PixAI: {p_alert}")
88
+
89
+ if use_animetagger:
90
+ a_raw, a_alert, a_order = current_animetagger_processor.predict(image, animetagger_model, animetagger_thresh, replacement_file_path, synonym_file_path, addition_file_path, "Orijinal", device)
91
+ if "Hata" not in a_raw and "❌" not in a_alert:
92
+ all_tags.update(t.strip() for t in a_raw.split(',') if t.strip())
93
+ all_raw_tags_for_original_sort.extend(a_order)
94
+ else: alert_messages.append(f"AnimeTagger: {a_alert}")
95
+
96
+ combined_final_tags_string = unique_and_sort_tags(", ".join(list(all_tags)), sort_order, original_order_ref=all_raw_tags_for_original_sort)
97
+
98
+ refined_final_tags_string = rafine_etiketler(combined_final_tags_string, context_weight)
99
+
100
+ refined_final_tags_string = apply_additions(refined_final_tags_string, addition_file_path)
101
+
102
+ if use_gemini:
103
+ active_prompt = gemini_prompt_vision
104
+ if gemini_mode == "Tags": active_prompt = gemini_prompt_tags
105
+ elif gemini_mode == "Vision + Tags": active_prompt = gemini_prompt_hybrid
106
+
107
+ tags_content_for_gemini = ""
108
+ if gemini_mode in ["Tags", "Vision + Tags"]:
109
+ source_tags = refined_final_tags_string if refined_final_tags_string else combined_final_tags_string
110
+ if enable_cat_for_gemini and selected_cats_for_gemini:
111
+ tags_content_for_gemini = kategorizelendir_ileri(source_tags, selected_cats_for_gemini, kategori_sozlugu_yukle())
112
+ else:
113
+ tags_content_for_gemini = source_tags
114
+
115
+ g_text, g_alert = current_gemini_processor.predict(
116
+ image, gemini_api_key, active_prompt, gemini_mode,
117
+ tags_content_for_gemini, gemini_model_id,
118
+ gemini_system_instruction
119
+ )
120
+ if "❌" in g_alert or "⚠️" in g_alert: alert_messages.append(f"Gemini: {g_alert}")
121
+ else: gemini_result_text = g_text
122
+
123
+ final_alert = "✅ İşlem tamamlandı!" if not alert_messages else "❌ Hatalar: " + "; ".join(alert_messages)
124
+
125
+ return combined_final_tags_string, refined_final_tags_string, gemini_result_text, final_alert, all_raw_tags_for_original_sort
126
+
127
+ # --- Ana Etiketleme Fonksiyonları ---
128
+
129
+ def toplu_islem(
130
+ image,
131
+ joint_thresh, use_joint,
132
+ cl_gen_thresh, cl_char_thresh, use_cl_tagger,
133
+ pixai_general_thresh, pixai_char_thresh, use_pixai_tagger,
134
+ animetagger_model, animetagger_thresh, use_animetagger,
135
+ use_gemini, gemini_api_key, gemini_mode, gemini_model_id,
136
+ gemini_prompt_vision, gemini_prompt_tags, gemini_prompt_hybrid,
137
+ gemini_system_instruction,
138
+ replacement_file_path, synonym_file_path, addition_file_path,
139
+ enable_categorization, selected_categories, sort_order="Alfabetik", device: str = "Auto", context_weight: float = 0.0
140
+ ):
141
+ final_tags_string, refined_tags_string, gemini_out, alert_message, _ = _process_single_image(
142
+ image,
143
+ joint_thresh, use_joint,
144
+ cl_gen_thresh, cl_char_thresh, use_cl_tagger,
145
+ pixai_general_thresh, pixai_char_thresh, use_pixai_tagger,
146
+ animetagger_model, animetagger_thresh, use_animetagger,
147
+ use_gemini, gemini_api_key, gemini_mode, gemini_model_id,
148
+ gemini_prompt_vision, gemini_prompt_tags, gemini_prompt_hybrid,
149
+ gemini_system_instruction,
150
+ replacement_file_path, synonym_file_path, addition_file_path,
151
+ sort_order, device, context_weight,
152
+ enable_cat_for_gemini=enable_categorization, selected_cats_for_gemini=selected_categories
153
+ )
154
+
155
+ source_for_cat = refined_tags_string if refined_tags_string else final_tags_string
156
+ categorized_tags_output = kategorizelendir_ileri(source_for_cat, selected_categories, kategori_sozlugu_yukle()) if (enable_categorization and selected_categories) else ""
157
+
158
+ parts = []
159
+ if categorized_tags_output: parts.append(categorized_tags_output)
160
+ if gemini_out: parts.append(gemini_out)
161
+ combined_cat_gem = ", ".join(parts)
162
+
163
+ return alert_message, final_tags_string, refined_tags_string, categorized_tags_output, gemini_out, combined_cat_gem
164
+
165
+ def toplu_islem_batch(
166
+ progress=gr.Progress(), images=None,
167
+ joint_thresh=0.25, use_joint=True,
168
+ cl_gen_thresh=0.55, cl_char_thresh=0.60, use_cl_tagger=True,
169
+ pixai_general_thresh=0.30, pixai_char_thresh=0.85, use_pixai_tagger=False,
170
+ animetagger_model="MobileNet V4 (Hızlı)", animetagger_thresh=0.35, use_animetagger=False,
171
+ use_gemini=False, gemini_api_key="", gemini_mode="Vision", gemini_model_id="gemini-2.5-flash",
172
+ gemini_prompt_vision="", gemini_prompt_tags="", gemini_prompt_hybrid="",
173
+ gemini_system_instruction="",
174
+ replacement_file_path="", synonym_file_path="", addition_file_path="",
175
+ enable_categorization=False, selected_categories=None, sort_order="Alfabetik", device: str = "Auto", context_weight: float = 0.0
176
+ ):
177
+ if not images: return "⚠️ Resim yüklenmedi.", "", None, "", "", "", ""
178
+ all_final, all_refined, all_cat, all_orig, all_gemini, results_html, alerts = [], [], [], [], [], [], []
179
+ total = len(images)
180
+
181
+ for i, img_obj in enumerate(images):
182
+ progress(i / total, desc=f"İşleniyor: {os.path.basename(img_obj.name)} ({i+1}/{total})")
183
+ try: image = Image.open(img_obj.name)
184
+ except Exception as e: alerts.append(f"{img_obj.name}: Okuma hatası: {e}"); continue
185
+
186
+ comb, ref, gem_out, alert, orig = _process_single_image(
187
+ image,
188
+ joint_thresh, use_joint,
189
+ cl_gen_thresh, cl_char_thresh, use_cl_tagger,
190
+ pixai_general_thresh, pixai_char_thresh, use_pixai_tagger,
191
+ animetagger_model, animetagger_thresh, use_animetagger,
192
+ use_gemini, gemini_api_key, gemini_mode, gemini_model_id,
193
+ gemini_prompt_vision, gemini_prompt_tags, gemini_prompt_hybrid,
194
+ gemini_system_instruction,
195
+ replacement_file_path, synonym_file_path, addition_file_path,
196
+ sort_order, device, context_weight,
197
+ enable_cat_for_gemini=enable_categorization, selected_cats_for_gemini=selected_categories
198
+ )
199
+
200
+ if "❌" in alert or "⚠️" in alert: alerts.append(f"{img_obj.name}: {alert}")
201
+ else:
202
+ all_final.append(comb); all_refined.append(ref); all_orig.extend(orig)
203
+ if gem_out: all_gemini.append(f"--- {os.path.basename(img_obj.name)} ---\n{gem_out}")
204
+
205
+ source = ref if ref else comb
206
+ cat_out = kategorizelendir_ileri(source, selected_categories, kategori_sozlugu_yukle()) if (enable_categorization and selected_categories) else ""
207
+ if cat_out: all_cat.append(cat_out)
208
+
209
+ html_part = f"<div style='border:1px solid #ddd; padding:10px; margin:5px;'><b>{os.path.basename(img_obj.name)}</b><br>Tags: {ref}"
210
+ if gem_out: html_part += f"<br><i>Gemini: {gem_out}</i>"
211
+ html_part += "</div>"
212
+ results_html.append(html_part)
213
+
214
+ progress(1.0, desc="✅ Bitti.")
215
+ final_alert = "✅ Başarılı!" if not alerts else "⚠️ Hatalar:\n" + "\n".join(alerts)
216
+ out_file = None
217
+ if all_final:
218
+ with tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8', delete=False, suffix=".txt") as tf:
219
+ tf.write("\n".join(all_final)); out_file = tf.name
220
+
221
+ gemini_combined_str = "\n\n".join(all_gemini)
222
+ return final_alert, "<br>".join(results_html), out_file, "\n".join(all_cat), unique_and_sort_tags(", ".join(list(set(all_orig))), "Alfabetik"), unique_and_sort_tags(", ".join(all_refined), "Alfabetik"), gemini_combined_str
223
+
224
+ def process_dual_images(image1, image2, joint_thresh1, use_joint1, joint_thresh2, use_joint2, cl_gen_thresh1, cl_char_thresh1, use_cl_tagger1, cl_gen_thresh2, cl_char_thresh2, use_cl_tagger2, pixai_general_thresh1, pixai_char_thresh1, use_pixai_tagger1, pixai_general_thresh2, pixai_char_thresh2, use_pixai_tagger2, animetagger_model1, animetagger_thresh1, use_animetagger1, animetagger_model2, animetagger_thresh2, use_animetagger2,
225
+ use_gemini, gemini_api_key, gemini_mode, gemini_model_id,
226
+ gemini_prompt_vision, gemini_prompt_tags, gemini_prompt_hybrid,
227
+ gemini_system_instruction,
228
+ replacement_file_path, synonym_file_path, addition_file_path,
229
+ enable_categorization_img1, selected_categories_img1, enable_categorization_img2, selected_categories_img2, sort_order="Alfabetik", device: str = "Auto", context_weight: float = 0.0):
230
+ images_settings = [
231
+ (image1, use_joint1, joint_thresh1, cl_gen_thresh1, cl_char_thresh1, use_cl_tagger1, pixai_general_thresh1, pixai_char_thresh1, use_pixai_tagger1, animetagger_model1, animetagger_thresh1, use_animetagger1, enable_categorization_img1, selected_categories_img1),
232
+ (image2, use_joint2, joint_thresh2, cl_gen_thresh2, cl_char_thresh2, use_cl_tagger2, pixai_general_thresh2, pixai_char_thresh2, use_pixai_tagger2, animetagger_model2, animetagger_thresh2, use_animetagger2, enable_categorization_img2, selected_categories_img2)
233
+ ]
234
+ alerts, cats, all_tags_sets, refined_sets, orig_tags, gemini_results = [], [], [], [], [], []
235
+
236
+ for img, uj, jt, cg, cc, uc, pg, pc, up, am, at, ua, enc, selc in images_settings:
237
+ comb, ref, gem_out, alert, orig = _process_single_image(
238
+ img, jt, uj, cg, cc, uc, pg, pc, up, am, at, ua,
239
+ use_gemini, gemini_api_key, gemini_mode, gemini_model_id,
240
+ gemini_prompt_vision, gemini_prompt_tags, gemini_prompt_hybrid,
241
+ gemini_system_instruction,
242
+ replacement_file_path, synonym_file_path, addition_file_path,
243
+ sort_order, device, context_weight,
244
+ enable_cat_for_gemini=enc, selected_cats_for_gemini=selc
245
+ )
246
+ if "❌" in alert: alerts.append(alert)
247
+ all_tags_sets.append(set(comb.split(', '))); refined_sets.append(set(ref.split(', '))); orig_tags.extend(orig)
248
+ gemini_results.append(gem_out)
249
+ src = ref if ref else comb
250
+ cats.append(kategorizelendir_ileri(src, selc, kategori_sozlugu_yukle()) if (enc and selc) else "")
251
+
252
+ combined_all = unique_and_sort_tags(", ".join(list(set().union(*all_tags_sets))), sort_order, orig_tags)
253
+ combined_ref = unique_and_sort_tags(", ".join(list(set().union(*refined_sets))), sort_order, orig_tags)
254
+
255
+ combined_all = apply_additions(combined_all, addition_file_path)
256
+ combined_ref = apply_additions(combined_ref, addition_file_path)
257
+
258
+ combined_cat = unique_and_sort_tags(", ".join([c for c in cats if c]), sort_order, orig_tags)
259
+
260
+ valid_captions = [g.strip() for g in gemini_results if g and g.strip()]
261
+ gemini_combined_text = " ".join(valid_captions)
262
+
263
+ parts = []
264
+ if combined_cat: parts.append(combined_cat)
265
+ if gemini_combined_text: parts.append(gemini_combined_text)
266
+
267
+ combined_cat_plus_gem = ", ".join(parts)
268
+
269
+ return alerts, combined_all, combined_ref, combined_cat, gemini_combined_text, combined_cat_plus_gem
modules/tagger_refinement.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # modules/tagger_refinement.py
3
+
4
+ import os
5
+ import re
6
+ import csv
7
+ import joblib
8
+ import numpy as np
9
+ import pathlib
10
+ import logging
11
+ from collections import Counter, OrderedDict
12
+ from lark import Lark, Token
13
+ from lark.exceptions import ParseError
14
+ import hnswlib
15
+ import compress_fasttext
16
+ from scipy.sparse import csr_matrix
17
+
18
+ # --- LOGGING SETUP (Sessizleştirme) ---
19
+ logging.basicConfig(level=logging.WARNING)
20
+ for _name in ("gensim", "hnswlib"):
21
+ logging.getLogger(_name).setLevel(logging.ERROR)
22
+
23
+ # --- Dosya Yolları Sabitleri (Veri ve Modeller) ---
24
+ PATH_TF_IDF = "models/binaries/tf_idf_files_420.joblib"
25
+ PATH_HNSW_TAGS = "models/binaries/tfidf_hnsw_tags.bin"
26
+ PATH_FASTTEXT = "models/binaries/e621FastTextModel010Replacement_small.bin"
27
+ PATH_DB_RATING = "data/databases/word_rating_probabilities.csv"
28
+ PATH_DB_FLUFFYROCK = "data/databases/fluffyrock_3m.csv"
29
+
30
+ # --- GLOBAL MODEL DEĞİŞKENLERİ ---
31
+ _HNSW_TAG = None
32
+ _HNSW_DIM = None
33
+ _HNSW_N_TAG = None
34
+ _TF_IDF_COMPONENTS = None
35
+ _FASTTEXT_MODEL = None
36
+ _TAG_DATA_CACHE = None
37
+ _NSFW_TAGS = None
38
+ _ARTIST_SET = None
39
+
40
+ # --- PARSER (Lark Grammar) ---
41
+ grammar = r"""
42
+ !start: (prompt | /[][():]/+)*
43
+ prompt: (emphasized | plain | comma | WHITESPACE)*
44
+ !emphasized: "(" prompt ")"
45
+ | "(" prompt ":" [WHITESPACE] NUMBER [WHITESPACE] ")"
46
+ comma: ","
47
+ WHITESPACE: /\s+/
48
+ plain: /([^,\\\[\]():|]|\\.)+/
49
+ %import common.SIGNED_NUMBER -> NUMBER
50
+ """
51
+ parser = Lark(grammar, start='start')
52
+
53
+ def load_tf_idf_components():
54
+ global _TF_IDF_COMPONENTS, _NSFW_TAGS, _ARTIST_SET
55
+
56
+ if _TF_IDF_COMPONENTS is None:
57
+ fname = PATH_TF_IDF
58
+ if os.path.exists(fname):
59
+ try:
60
+ _TF_IDF_COMPONENTS = joblib.load(fname)
61
+ if 'tag_to_row_index' in _TF_IDF_COMPONENTS:
62
+ _TF_IDF_COMPONENTS['row_to_tag'] = {idx: tag for tag, idx in _TF_IDF_COMPONENTS['tag_to_row_index'].items()}
63
+ idf = _TF_IDF_COMPONENTS['idf']
64
+ if isinstance(idf, dict):
65
+ t2c = _TF_IDF_COMPONENTS['tag_to_column_index']
66
+ n_cols = max(t2c.values()) + 1
67
+ idf_by_col = np.ones(n_cols, dtype=np.float32)
68
+ for term, col in t2c.items(): idf_by_col[col] = float(idf.get(term, 1.0))
69
+ _TF_IDF_COMPONENTS['idf'] = idf_by_col
70
+ except Exception:
71
+ _TF_IDF_COMPONENTS = {}
72
+ else:
73
+ _TF_IDF_COMPONENTS = {}
74
+
75
+ if _NSFW_TAGS is None:
76
+ _NSFW_TAGS = set()
77
+ if os.path.exists(PATH_DB_RATING):
78
+ with open(PATH_DB_RATING, 'r', newline='', encoding='utf-8') as csvfile:
79
+ reader = csv.reader(csvfile); next(reader, None)
80
+ for row in reader:
81
+ if float(row[1]) >= 0.95: _NSFW_TAGS.add(row[0])
82
+
83
+ if _ARTIST_SET is None:
84
+ _ARTIST_SET = set()
85
+ if os.path.exists(PATH_DB_FLUFFYROCK):
86
+ with open(PATH_DB_FLUFFYROCK, 'r', newline='', encoding='utf-8') as csvfile:
87
+ reader = csv.reader(csvfile)
88
+ for row in reader:
89
+ if row[0].startswith('by_'): _ARTIST_SET.add(row[0][3:])
90
+
91
+ def get_tag_data_cache():
92
+ global _TAG_DATA_CACHE
93
+ if _TAG_DATA_CACHE is None:
94
+ if not os.path.exists(PATH_DB_FLUFFYROCK): return None
95
+ def build_aliases(rev=False):
96
+ d = {}
97
+ with open(PATH_DB_FLUFFYROCK, 'r', newline='', encoding='utf-8') as f:
98
+ r = csv.reader(f)
99
+ for row in r:
100
+ tag = ''.join(c for c in row[0] if ord(c)<128)
101
+ alist = [] if row[3] == "null" else [''.join(c for c in alias if ord(c)<128) for alias in row[3].split(',')]
102
+ if rev:
103
+ for a in alist: d.setdefault(a, []).append(tag)
104
+ else: d[tag] = alist
105
+ return d
106
+ def build_counts():
107
+ d = {}
108
+ with open(PATH_DB_FLUFFYROCK, 'r', newline='', encoding='utf-8') as f:
109
+ r = csv.reader(f)
110
+ for row in r:
111
+ if row[2].isdigit(): d[row[0]] = int(row[2])
112
+ return d
113
+ _TAG_DATA_CACHE = {'tag2aliases': build_aliases(), 'alias2tags': build_aliases(rev=True), 'tag2count': build_counts()}
114
+ return _TAG_DATA_CACHE
115
+
116
+ def is_artist(name):
117
+ load_tf_idf_components()
118
+ return name in _ARTIST_SET
119
+
120
+ def _l2_normalize_rows(mat):
121
+ mat = np.asarray(mat, dtype=np.float32)
122
+ norms = np.linalg.norm(mat, axis=1, keepdims=True)
123
+ norms[norms == 0.0] = 1.0
124
+ return mat / norms
125
+
126
+ def _ensure_hnsw_indexes():
127
+ global _HNSW_TAG, _HNSW_DIM, _HNSW_N_TAG
128
+ if _HNSW_TAG is not None: return
129
+ load_tf_idf_components()
130
+ if not _TF_IDF_COMPONENTS: return
131
+ reduced_matrix = _TF_IDF_COMPONENTS.get('reduced_matrix')
132
+ row_to_tag = _TF_IDF_COMPONENTS.get('row_to_tag')
133
+ if reduced_matrix is None: return
134
+ rm = _l2_normalize_rows(reduced_matrix).astype(np.float32)
135
+ n_items, dim = rm.shape
136
+ _HNSW_DIM = dim
137
+ tag_rows = []
138
+ for i in range(n_items):
139
+ tag = row_to_tag.get(i, "")
140
+ base = tag[3:] if tag.startswith("by_") else tag
141
+ if tag in {"by_unknown_artist", "by_conditional_dnp"}: tag_rows.append(i); continue
142
+ if not is_artist(base): tag_rows.append(i)
143
+
144
+ tag_path = pathlib.Path(PATH_HNSW_TAGS)
145
+ idx = hnswlib.Index(space='cosine', dim=dim)
146
+ if tag_path.exists():
147
+ try:
148
+ idx.load_index(str(tag_path), max_elements=max(1, len(tag_rows)))
149
+ idx.set_ef(200)
150
+ _HNSW_TAG = idx
151
+ _HNSW_N_TAG = len(tag_rows)
152
+ except Exception: pass
153
+
154
+ def _hnsw_query(vec, k=2000):
155
+ _ensure_hnsw_indexes()
156
+ if _HNSW_TAG is None: return [], []
157
+ q = np.asarray(vec, dtype=np.float32).reshape(-1)
158
+ q_norm = np.linalg.norm(q)
159
+ if q_norm > 0: q = q / q_norm
160
+ labels, dists = _HNSW_TAG.knn_query(q, k=min(k, _HNSW_N_TAG))
161
+ return labels[0], 1.0 - dists[0]
162
+
163
+ special_tags = ["score:0", "score:1", "score:2", "score:3", "score:4", "score:5", "score:6", "score:7", "score:8", "score:9", "rating:s", "rating:q", "rating:e"]
164
+ MODEL_SPECIFIC_TAGS = {"masterpiece", "best quality", "good quality", "normal quality", "low quality", "worst quality", "highres", "lowres", "absurdres", "source_pony", "source_furry", "rating_safe", "rating_explicit", "rating_questionable"}
165
+
166
+ def remove_special_tags(original_string):
167
+ tags = [tag.strip() for tag in original_string.split(",")]
168
+ remaining = [t for t in tags if t not in special_tags]
169
+ return ", ".join(remaining)
170
+
171
+ def extract_tags(tree):
172
+ tags_with_positions = []
173
+ def _traverse(node):
174
+ if isinstance(node, Token) and node.type == '__ANON_1': tags_with_positions.append((node.value, node.start_pos))
175
+ elif not isinstance(node, Token):
176
+ for child in node.children: _traverse(child)
177
+ _traverse(tree)
178
+ return tags_with_positions
179
+
180
+ def construct_pseudo_vector(pseudo_doc_terms):
181
+ load_tf_idf_components()
182
+ if not _TF_IDF_COMPONENTS: return None
183
+ idf = _TF_IDF_COMPONENTS['idf']
184
+ t2c = _TF_IDF_COMPONENTS['tag_to_column_index']
185
+ cols, data = [], []
186
+ for term, w in pseudo_doc_terms.items():
187
+ j = t2c.get(term)
188
+ if j is None: continue
189
+ cols.append(j); data.append(w * idf[j])
190
+ return csr_matrix((data, cols, [0, len(cols)]), shape=(1, len(idf)), dtype=np.float32)
191
+
192
+ def get_similar_tags_tfidf(pseudo_doc_terms):
193
+ load_tf_idf_components()
194
+ if not _TF_IDF_COMPONENTS: return {}
195
+ pseudo = construct_pseudo_vector(pseudo_doc_terms)
196
+ if pseudo is None: return {}
197
+ svd = _TF_IDF_COMPONENTS['svd_model']
198
+ reduced = svd.transform(pseudo)
199
+ top_inds, top_sims = _hnsw_query(reduced)
200
+ row_to_tag = _TF_IDF_COMPONENTS['row_to_tag']
201
+ sim_dict = {}
202
+ for i, sim in zip(top_inds, top_sims):
203
+ tag = row_to_tag.get(int(i))
204
+ if tag and tag not in _NSFW_TAGS: sim_dict[tag] = float(sim)
205
+ sorted_sim = OrderedDict(sorted(sim_dict.items(), key=lambda x: x[1], reverse=True))
206
+ return OrderedDict((k.replace('_', ' ').replace('(', '\\(').replace(')', '\\)'), v) for k, v in sorted_sim.items())
207
+
208
+ def rafine_etiketler(tags_string, context_weight=0.5):
209
+ global _FASTTEXT_MODEL
210
+ if not tags_string or not tags_string.strip(): return ""
211
+ load_tf_idf_components()
212
+ data_cache = get_tag_data_cache()
213
+ if _FASTTEXT_MODEL is None and os.path.exists(PATH_FASTTEXT):
214
+ try: _FASTTEXT_MODEL = compress_fasttext.models.CompressedFastTextKeyedVectors.load(PATH_FASTTEXT)
215
+ except: pass
216
+ if (not _FASTTEXT_MODEL or not data_cache or not _TF_IDF_COMPONENTS) and context_weight < 0.01: return tags_string
217
+
218
+ try:
219
+ input_str = remove_special_tags(tags_string.lower())
220
+ try: parsed = parser.parse(input_str)
221
+ except ParseError: return tags_string
222
+ raw_tags = extract_tags(parsed)
223
+ tag_data = []
224
+ for tag_text, start_pos in raw_tags:
225
+ mod_tag = tag_text.replace('_', ' ').replace('\\(', '(').replace('\\)', ')').strip()
226
+ tf_idf_tag = re.sub(r'\\([()])', r'\1', re.sub(r' ', '_', tag_text.strip().removeprefix('by ').removeprefix('by_')))
227
+ tag_data.append({"original_tag": tag_text, "modified_tag": mod_tag, "tf_idf_matrix_tag": tf_idf_tag})
228
+
229
+ terms = [t["tf_idf_matrix_tag"] for t in tag_data]
230
+ suggested_context = {}
231
+ if _TF_IDF_COMPONENTS and _HNSW_TAG: suggested_context = get_similar_tags_tfidf(dict(Counter(terms)))
232
+
233
+ valid_tags = []
234
+ for item in tag_data:
235
+ orig, mod = item["original_tag"], item["modified_tag"]
236
+ search = mod.replace(' ', '_')
237
+ if mod in special_tags or orig in MODEL_SPECIFIC_TAGS: continue
238
+ if not data_cache:
239
+ valid_tags.append(orig); continue
240
+ if is_artist(search.lower().removeprefix('by_')):
241
+ valid_tags.append(orig); continue
242
+ if search in data_cache['tag2count'] or search in data_cache['tag2aliases']:
243
+ valid_tags.append(orig); continue
244
+ if not _FASTTEXT_MODEL:
245
+ if context_weight < 0.5: valid_tags.append(orig)
246
+ continue
247
+
248
+ similar_words = _FASTTEXT_MODEL.most_similar(search, topn=20)
249
+ candidates = []
250
+ seen_cand = set()
251
+ for sim_word, sim in similar_words:
252
+ if sim_word in seen_cand: continue
253
+ if sim_word in data_cache['tag2aliases']:
254
+ candidates.append((sim_word.replace('_', ' '), sim)); seen_cand.add(sim_word)
255
+ else:
256
+ targets = data_cache['alias2tags'].get(sim_word, [])
257
+ for t in targets:
258
+ if t not in seen_cand: candidates.append((t.replace('_', ' '), sim)); seen_cand.add(t)
259
+
260
+ scored_candidates = []
261
+ for word, sim in candidates:
262
+ ctx_score = float(suggested_context.get(word) or suggested_context.get(word.replace('(', '\\(').replace(')', '\\)')) or 0.0)
263
+ final_score = (1.0 - context_weight) * sim + (context_weight * ctx_score)
264
+ scored_candidates.append((word, final_score))
265
+ scored_candidates.sort(key=lambda x: x[1], reverse=True)
266
+ if scored_candidates:
267
+ best_candidate, best_score = scored_candidates[0]
268
+ if best_score > 0.6: valid_tags.append(best_candidate)
269
+ return ", ".join(valid_tags)
270
+ except Exception: return tags_string
modules/taggers/anime.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import gc
4
+ import pandas as pd
5
+ import timm
6
+ import torch
7
+ from safetensors.torch import load_file
8
+ from modules.taggers.base import TaggerProcessor
9
+ from modules.taggers.joint import get_torch_device
10
+
11
+ # --- ANIME TAGGER Global Durum ---
12
+ ANIMETAGGER_MODEL_MAP = {
13
+ "mobilenetv4": {"folder": "models/anime_taggers/mobilenetv4_conv_aa_large.dbv4-full", "arch": "mobilenetv4_conv_aa_large"},
14
+ "convnextv2": {"folder": "models/anime_taggers/convnextv2_huge.dbv4-full", "arch": "convnextv2_huge"},
15
+ "caformer": {"folder": "models/anime_taggers/caformer_s36.dbv4-full", "arch": "caformer_s36"},
16
+ # Legacy Support
17
+ "MobileNet V4 (Hızlı)": {"folder": "models/anime_taggers/mobilenetv4_conv_aa_large.dbv4-full", "arch": "mobilenetv4_conv_aa_large"},
18
+ "ConvNeXt V2 Huge (Pro)": {"folder": "models/anime_taggers/convnextv2_huge.dbv4-full", "arch": "convnextv2_huge"},
19
+ "Caformer B36 (Yeni)": {"folder": "models/anime_taggers/caformer_s36.dbv4-full", "arch": "caformer_s36"},
20
+ }
21
+ CURRENT_ANIMETAGGER_MODEL = None
22
+ CURRENT_ANIMETAGGER_TRANSFORM = None
23
+ CURRENT_ANIMETAGGER_TAGS = []
24
+ LOADED_ANIMETAGGER_KEY = None
25
+
26
+ def load_animetagger_model(selection_key, device_pref: str):
27
+ global CURRENT_ANIMETAGGER_MODEL, CURRENT_ANIMETAGGER_TRANSFORM, CURRENT_ANIMETAGGER_TAGS, LOADED_ANIMETAGGER_KEY
28
+ target_device = get_torch_device(device_pref)
29
+ if LOADED_ANIMETAGGER_KEY == selection_key and CURRENT_ANIMETAGGER_MODEL is not None:
30
+ if next(CURRENT_ANIMETAGGER_MODEL.parameters()).device.type != target_device:
31
+ CURRENT_ANIMETAGGER_MODEL.to(target_device)
32
+ return True
33
+
34
+ print(f"\n--- ANIME TAGGER YÜKLENİYOR: {selection_key} ---")
35
+ if CURRENT_ANIMETAGGER_MODEL is not None:
36
+ del CURRENT_ANIMETAGGER_MODEL
37
+ CURRENT_ANIMETAGGER_MODEL = None
38
+ gc.collect()
39
+ if torch.cuda.is_available(): torch.cuda.empty_cache()
40
+
41
+ config = ANIMETAGGER_MODEL_MAP.get(selection_key)
42
+ if not config: return False
43
+ folder_path = config["folder"]
44
+ model_path = os.path.join(folder_path, "model.safetensors")
45
+ tags_path = os.path.join(folder_path, "selected_tags.csv")
46
+ if not os.path.exists(model_path) or not os.path.exists(tags_path): return False
47
+
48
+ try:
49
+ df = pd.read_csv(tags_path)
50
+ CURRENT_ANIMETAGGER_TAGS = df['name'].tolist() if 'name' in df.columns else df.iloc[:, 0].tolist()
51
+ model = timm.create_model(config['arch'], pretrained=False, num_classes=len(CURRENT_ANIMETAGGER_TAGS))
52
+ model.load_state_dict(load_file(model_path))
53
+ model.to(target_device).eval()
54
+ CURRENT_ANIMETAGGER_MODEL = model
55
+ except Exception as e:
56
+ print(f"Model yükleme hatası ({selection_key}): {e}")
57
+ return False
58
+
59
+ from timm.data import resolve_data_config
60
+ from timm.data.transforms_factory import create_transform
61
+ data_config = resolve_data_config({}, model=CURRENT_ANIMETAGGER_MODEL)
62
+ CURRENT_ANIMETAGGER_TRANSFORM = create_transform(**data_config)
63
+ LOADED_ANIMETAGGER_KEY = selection_key
64
+ return True
65
+
66
+
67
+ class AnimeTaggerProcessor(TaggerProcessor):
68
+ def predict(self, image, model_key, threshold, replacement_file_path, synonym_file_path, addition_file_path, sort_order="Alfabetik", device_pref: str = "Auto"):
69
+ self.replacement_file = replacement_file_path
70
+ self.synonym_file = synonym_file_path
71
+ self.addition_file = addition_file_path
72
+
73
+ if image is None: return "", "⚠️ Resim yüklenmedi.", []
74
+ try:
75
+ if not load_animetagger_model(model_key, device_pref):
76
+ return "", f"❌ Anime Tagger modeli yüklenemedi: {model_key}.", []
77
+ target_device = get_torch_device(device_pref)
78
+ img = image.convert("RGB") if image.mode != "RGB" else image
79
+ tensor = CURRENT_ANIMETAGGER_TRANSFORM(img).unsqueeze(0).to(target_device)
80
+ with torch.no_grad():
81
+ output = CURRENT_ANIMETAGGER_MODEL(tensor)
82
+ probs = torch.sigmoid(output)[0].cpu().numpy()
83
+ results = dict(zip(CURRENT_ANIMETAGGER_TAGS, probs))
84
+ filtered_results = {tag: float(score) for tag, score in results.items() if score > threshold}
85
+ sorted_items = sorted(filtered_results.items(), key=lambda x: x[1], reverse=True)
86
+ original_order_for_anime = [tag.replace("_", " ") for tag, score in sorted_items]
87
+ raw_tags_string = ", ".join(original_order_for_anime)
88
+ final_tags = self.process_tags(raw_tags_string, sort_order, original_order_for_anime)
89
+ return final_tags, "✅ Anime Tagger işlemi tamamlandı!", original_order_for_anime
90
+ except Exception as e:
91
+ return f"Hata: {e}", f"❌ Anime Tagger hata: {e}", []
modules/taggers/base.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from modules.utils.tag_utils import clean_tags_underscore, unique_and_sort_tags
3
+ from modules.managers.rule_manager import apply_replacements, apply_synonym_consolidation, apply_additions
4
+
5
+ class TaggerProcessor:
6
+ def __init__(self, replacement_file_path, synonym_file_path, addition_file_path):
7
+ self.replacement_file = replacement_file_path
8
+ self.synonym_file = synonym_file_path
9
+ self.addition_file = addition_file_path
10
+
11
+ def process_tags(self, raw_tags_string, sort_order="Alfabetik", original_order_ref=None):
12
+ cleaned = clean_tags_underscore(raw_tags_string)
13
+ replaced = apply_replacements(cleaned, self.replacement_file)
14
+ consolidated = apply_synonym_consolidation(replaced, self.synonym_file)
15
+ final_tags = unique_and_sort_tags(consolidated, sort_order, original_order_ref)
16
+ # Ekleme Kurallarını en son uygula (Listenin sonuna ekler)
17
+ final_tags_with_additions = apply_additions(final_tags, self.addition_file)
18
+ return final_tags_with_additions
modules/taggers/cl.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from modules.taggers.base import TaggerProcessor
3
+
4
+ class CLTaggerProcessor(TaggerProcessor):
5
+ def __init__(self, cl_tagger_instance_ref, replacement_file_path, synonym_file_path, addition_file_path):
6
+ super().__init__(replacement_file_path, synonym_file_path, addition_file_path)
7
+ self.cl_tagger_ref = cl_tagger_instance_ref
8
+
9
+ def predict(self, image, gen_threshold, char_threshold, replacement_file_path, synonym_file_path, addition_file_path, sort_order="Alfabetik", device_pref: str = "Auto"):
10
+ self.replacement_file = replacement_file_path
11
+ self.synonym_file = synonym_file_path
12
+ self.addition_file = addition_file_path
13
+
14
+ if self.cl_tagger_ref is None or self.cl_tagger_ref.session is None:
15
+ return "", "❌ CL Tagger modülü yüklenemedi.", []
16
+ if image is None: return "", "⚠️ Resim yüklenmedi.", []
17
+ try:
18
+ ai_tags_string_raw, _, raw_predictions_dict = self.cl_tagger_ref.predict(image, gen_threshold, char_threshold)
19
+ all_raw_tags_with_probs = []
20
+ for category_key in ["rating", "quality", "artist", "character", "copyright", "general", "meta", "model"]:
21
+ all_raw_tags_with_probs.extend(raw_predictions_dict.get(category_key, []))
22
+ all_raw_tags_with_probs_sorted = sorted(all_raw_tags_with_probs, key=lambda x: x[1], reverse=True)
23
+ original_order_for_cl = [tag_name.replace("_", " ") for tag_name, _ in all_raw_tags_with_probs_sorted]
24
+ final_tags = self.process_tags(ai_tags_string_raw, sort_order, original_order_for_cl)
25
+ return final_tags, "✅ CL Tagger işlemi tamamlandı!", original_order_for_cl
26
+ except Exception as e:
27
+ return f"Hata: {e}", f"❌ CL Tagger hata: {e}", []
modules/taggers/gemini.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ try:
3
+ import google.generativeai as genai
4
+ GEMINI_AVAILABLE = True
5
+ except ImportError:
6
+ GEMINI_AVAILABLE = False
7
+
8
+ class GeminiTaggerProcessor:
9
+ def predict(self, image, api_key, prompt, mode="Vision", tags_text="", model_id="gemini-2.5-flash", system_instruction=None):
10
+ if not GEMINI_AVAILABLE: return "Kütüphane eksik: pip install google-generativeai", "⚠️ Kütüphane eksik."
11
+ if not api_key: return "API Key Eksik", "⚠️ Gemini API Key girilmedi."
12
+ try:
13
+ genai.configure(api_key=api_key)
14
+ model = genai.GenerativeModel(
15
+ model_id,
16
+ system_instruction=system_instruction if system_instruction and system_instruction.strip() else None
17
+ )
18
+
19
+ if mode == "Tags":
20
+ full_prompt = f"{prompt}\n\nEtiketler:\n{tags_text}"
21
+ response = model.generate_content(full_prompt)
22
+ elif mode == "Vision + Tags":
23
+ full_prompt = f"{prompt}\n\nTespit Edilen Etiketler:\n{tags_text}"
24
+ response = model.generate_content([full_prompt, image])
25
+ else:
26
+ response = model.generate_content([prompt, image])
27
+
28
+ return response.text.strip(), "✅ Gemini işlemi tamamlandı!"
29
+ except Exception as e:
30
+ return f"Hata: {str(e)}", f"❌ Gemini Hatası: {str(e)}"
modules/taggers/image_utils.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import math
3
+ import numpy as np
4
+ import torch
5
+ from torch import Tensor
6
+ from einops import rearrange
7
+ from PIL import Image, ImageCms
8
+ from PIL.Image import Resampling
9
+ from PIL.ImageCms import (
10
+ Direction, Intent, ImageCmsProfile, createProfile, getDefaultIntent,
11
+ isIntentSupported, profileToProfile
12
+ )
13
+ from PIL.ImageOps import exif_transpose
14
+ import timm
15
+
16
+ _SRGB = createProfile(colorSpace='sRGB')
17
+ _INTENT_FLAGS = {
18
+ Intent.PERCEPTUAL: ImageCms.FLAGS["HIGHRESPRECALC"],
19
+ Intent.RELATIVE_COLORIMETRIC: (ImageCms.FLAGS["HIGHRESPRECALC"] | ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]),
20
+ Intent.ABSOLUTE_COLORIMETRIC: ImageCms.FLAGS["HIGHRESPRECALC"]
21
+ }
22
+
23
+ def _coalesce_intent(intent: Intent | int) -> Intent:
24
+ if isinstance(intent, Intent): return intent
25
+ mapping = {0: Intent.PERCEPTUAL, 1: Intent.RELATIVE_COLORIMETRIC, 2: Intent.SATURATION, 3: Intent.ABSOLUTE_COLORIMETRIC}
26
+ if intent in mapping: return mapping[intent]
27
+ raise ValueError("invalid intent")
28
+
29
+ def process_srgb(img: Image, *, resize=None, crop=None, expect: tuple = None) -> Image:
30
+ img.load()
31
+ try: exif_transpose(img, in_place=True)
32
+ except Exception: pass
33
+ size = (img.width, img.height)
34
+ if expect is not None and size != expect: raise RuntimeError(f"Image size mismatch.")
35
+
36
+ if (icc_raw := img.info.get("icc_profile")) is not None:
37
+ try:
38
+ profile = ImageCmsProfile(torch.io.BytesIO(icc_raw))
39
+ working_mode = img.mode
40
+ if img.mode.startswith(("RGB", "BGR", "P")): working_mode = "RGBA" if img.has_transparency_data else "RGB"
41
+ elif img.mode.startswith(("L", "I", "F")) or img.mode == "1": working_mode = "LA" if img.has_transparency_data else "L"
42
+ if img.mode != working_mode: img = img.convert(working_mode)
43
+ mode = "RGBA" if img.has_transparency_data else "RGB"
44
+ intent = Intent.RELATIVE_COLORIMETRIC
45
+ if isIntentSupported(profile, intent, Direction.INPUT) != 1: intent = _coalesce_intent(getDefaultIntent(profile))
46
+ if (flags := _INTENT_FLAGS.get(intent)) is None: raise RuntimeError("Unsupported intent")
47
+
48
+ if img.mode == mode: profileToProfile(img, profile, _SRGB, renderingIntent=intent, inPlace=True, flags=flags)
49
+ else: img = cast(Image, profileToProfile(img, profile, _SRGB, renderingIntent=intent, outputMode=mode, flags=flags))
50
+ except Exception: pass
51
+
52
+ if img.has_transparency_data:
53
+ if img.mode != "RGBa":
54
+ try: img = img.convert("RGBa")
55
+ except ValueError: img = img.convert("RGBA").convert("RGBa")
56
+ elif img.mode != "RGB": img = img.convert("RGB")
57
+
58
+ if crop is not None:
59
+ if not isinstance(crop, tuple): crop = crop(size)
60
+ img = img.crop(crop)
61
+ size = (img.width, img.height)
62
+
63
+ if resize is not None:
64
+ if not isinstance(resize, tuple): resize = resize(size)
65
+ if size != resize:
66
+ img = img.resize(resize, Resampling.LANCZOS, box=None, reducing_gap=3.0)
67
+
68
+ return img
69
+
70
+ def put_srgb_patch(img: Image, patch_data: Tensor, patch_coord: Tensor, patch_valid: Tensor, patch_size: int) -> None:
71
+ if img.mode not in ("RGB", "RGBA", "RGBa"): raise ValueError(f"Image has non-RGB mode {img.mode}.")
72
+ patches = rearrange(np.asarray(img)[:, :, :3], "(h p1) (w p2) c -> h w (p1 p2 c)", p1=patch_size, p2=patch_size)
73
+ coords = np.stack(np.meshgrid(np.arange(patches.shape[0], dtype=np.int16), np.arange(patches.shape[1], dtype=np.int16), indexing="ij"), axis=-1)
74
+ coords = rearrange(coords, "h w c -> (h w) c")
75
+ patches = rearrange(patches, "h w p -> (h w) p")
76
+ n = patches.shape[0]
77
+ np.copyto(patch_data[:n].numpy(), patches, casting="no")
78
+ np.copyto(patch_coord[:n].numpy(), coords, casting="no")
79
+ patch_valid[:n] = True
80
+
81
+ def sdpa_attn_mask(patch_valid: Tensor, num_prefix_tokens: int = 0, symmetric: bool = True, q_len: int | None = None, dtype: torch.dtype | None = None) -> Tensor:
82
+ mask = patch_valid.unflatten(-1, (1, 1, -1))
83
+ if num_prefix_tokens:
84
+ mask = torch.cat((torch.ones(*mask.shape[:-1], num_prefix_tokens, device=patch_valid.device, dtype=torch.bool), mask), dim=-1)
85
+ return mask
86
+
87
+ # Monkey Patching Timm for NaFlexVit
88
+ try:
89
+ timm.models.naflexvit.create_attention_mask = sdpa_attn_mask
90
+ except AttributeError:
91
+ pass
92
+
93
+ def get_image_size_for_seq(image_hw, patch_size: int = 16, max_seq_len: int = 1024, max_ratio: float = 1.0, eps: float = 1e-5) -> tuple[int, int]:
94
+ h, w = image_hw
95
+ max_py = int(max((h * max_ratio) // patch_size, 1))
96
+ max_px = int(max((w * max_ratio) // patch_size, 1))
97
+ if (max_py * max_px) <= max_seq_len: return max_py * patch_size, max_px * patch_size
98
+
99
+ def patchify(ratio: float) -> tuple[int, int]:
100
+ return (min(int(math.ceil((h * ratio) / patch_size)), max_py), min(int(math.ceil((w * ratio) / patch_size)), max_px))
101
+
102
+ py, px = patchify(eps)
103
+ if (py * px) > max_seq_len: raise ValueError(f"Image too large.")
104
+ ratio = eps
105
+ while (max_ratio - ratio) >= eps:
106
+ mid = (ratio + max_ratio) / 2.0
107
+ mpy, mpx = patchify(mid)
108
+ if (mpy * mpx) > max_seq_len: max_ratio = mid
109
+ else:
110
+ ratio = mid
111
+ py, px = mpy, mpx
112
+ if (py * px) == max_seq_len: break
113
+ return py * patch_size, px * patch_size
114
+
115
+ def process_image_jtp(img: Image, patch_size: int, max_seq_len: int) -> Image:
116
+ def compute_resize(wh):
117
+ h, w = get_image_size_for_seq((wh[1], wh[0]), patch_size, max_seq_len)
118
+ return w, h
119
+ return process_srgb(img, resize=compute_resize)
120
+
121
+ def patchify_image(img: Image, patch_size: int, max_seq_len: int) -> tuple[Tensor, Tensor, Tensor]:
122
+ patches = torch.zeros(max_seq_len, patch_size * patch_size * 3, device="cpu", dtype=torch.uint8)
123
+ patch_coords = torch.zeros(max_seq_len, 2, device="cpu", dtype=torch.int16)
124
+ patch_valid = torch.zeros(max_seq_len, device="cpu", dtype=torch.bool)
125
+ put_srgb_patch(img, patches, patch_coords, patch_valid, patch_size)
126
+ return patches, patch_coords, patch_valid
modules/taggers/joint.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import timm
4
+ import torch
5
+ import huggingface_hub
6
+ from safetensors import safe_open
7
+ from PIL import Image
8
+
9
+ from modules.hydra_layers import HydraPool
10
+ from modules.taggers.image_utils import process_image_jtp, patchify_image
11
+ from modules.taggers.base import TaggerProcessor
12
+
13
+ # Global State
14
+ INITIAL_TORCH_DEVICE = ["cpu", "cuda"][torch.cuda.is_available()]
15
+ JOINT_MODEL = None
16
+ JOINT_TAGS = []
17
+ PATCH_SIZE = 16
18
+ MAX_SEQ_LEN = 1024
19
+
20
+ def get_torch_device(device_pref: str) -> str:
21
+ if device_pref == "CUDA" and torch.cuda.is_available(): return "cuda"
22
+ elif device_pref == "Auto" and torch.cuda.is_available(): return "cuda"
23
+ return "cpu"
24
+
25
+ run_joint_classifier = None
26
+
27
+ # Initialize Model Loading on Import (or lazily)
28
+ # To preserve behavior, we'll try to load it immediately but wrap in try/except
29
+ try:
30
+ print("Joint Tagger (JTP-3 Hydra) Yükleniyor...")
31
+ jtp3_path = huggingface_hub.hf_hub_download(repo_id="RedRocket/JTP-3", filename="models/jtp-3-hydra.safetensors")
32
+
33
+ with safe_open(jtp3_path, framework="pt", device="cpu") as f:
34
+ metadata = f.metadata()
35
+ state_dict = {key: f.get_tensor(key) for key in f.keys()}
36
+
37
+ tags = metadata["classifier.labels"].split("\n")
38
+ JOINT_TAGS = [t.replace("_", " ").replace("vulva", "pussy") for t in tags]
39
+
40
+ joint_model = timm.create_model(
41
+ 'naflexvit_so400m_patch16_siglip',
42
+ pretrained=False, num_classes=0,
43
+ pos_embed_interp_mode="bilinear",
44
+ weight_init="skip", fix_init=False,
45
+ device="cpu", dtype=torch.bfloat16
46
+ )
47
+
48
+ joint_model.attn_pool = HydraPool.for_state(state_dict, "attn_pool.", device="cpu", dtype=torch.bfloat16)
49
+ joint_model.head = joint_model.attn_pool.create_head()
50
+ joint_model.num_classes = len(tags)
51
+
52
+ joint_model.load_state_dict(state_dict, strict=False)
53
+ joint_model.attn_pool._q_normed = True
54
+
55
+ joint_model.eval().to(dtype=torch.bfloat16)
56
+ joint_model.to(INITIAL_TORCH_DEVICE)
57
+ JOINT_MODEL = joint_model
58
+
59
+ def run_joint_classifier_func(image: Image, threshold, execution_device: str):
60
+ device_for_tensor = get_torch_device(execution_device)
61
+ processed_img = process_image_jtp(image, PATCH_SIZE, MAX_SEQ_LEN)
62
+ patches, patch_coords, patch_valid = patchify_image(processed_img, PATCH_SIZE, MAX_SEQ_LEN)
63
+
64
+ patches = patches.unsqueeze(0).to(device=device_for_tensor, non_blocking=True)
65
+ patch_coords = patch_coords.unsqueeze(0).to(device=device_for_tensor, non_blocking=True)
66
+ patch_valid = patch_valid.unsqueeze(0).to(device=device_for_tensor, non_blocking=True)
67
+
68
+ patches = patches.to(dtype=torch.bfloat16).div_(127.5).sub_(1.0)
69
+ patch_coords = patch_coords.to(dtype=torch.int32)
70
+
71
+ if next(JOINT_MODEL.parameters()).device.type != device_for_tensor:
72
+ JOINT_MODEL.to(device_for_tensor)
73
+
74
+ with torch.no_grad():
75
+ features = JOINT_MODEL.forward_intermediates(
76
+ patches,
77
+ patch_coord=patch_coords,
78
+ patch_valid=patch_valid,
79
+ output_dict=True,
80
+ output_fmt='NLC'
81
+ )
82
+ logits = JOINT_MODEL.forward_head(features["image_features"], patch_valid=patch_valid)
83
+
84
+ probits = logits[0].float().sigmoid_().mul_(2.0).sub_(1.0)
85
+
86
+ values, indices = probits.cpu().topk(len(JOINT_TAGS))
87
+
88
+ raw_results = []
89
+ for idx, val in zip(indices, values):
90
+ score = val.item()
91
+ if score >= threshold:
92
+ raw_results.append((JOINT_TAGS[idx.item()], score))
93
+
94
+ text_no_impl = ", ".join([t[0] for t in raw_results])
95
+ sorted_tag_score = dict(raw_results)
96
+
97
+ return text_no_impl, sorted_tag_score
98
+
99
+ run_joint_classifier = run_joint_classifier_func
100
+ print(f"JTP-3 Hydra Modeli Başarıyla Yüklendi ({INITIAL_TORCH_DEVICE})")
101
+
102
+ except Exception as e:
103
+ print(f"Joint Tagger (JTP-3) yüklenirken hata: {e}")
104
+ run_joint_classifier = None
105
+
106
+
107
+ class JointTaggerProcessor(TaggerProcessor):
108
+ def predict(self, image, threshold, replacement_file_path, synonym_file_path, addition_file_path, sort_order="Alfabetik", device_pref: str = "Auto"):
109
+ self.replacement_file = replacement_file_path
110
+ self.synonym_file = synonym_file_path
111
+ self.addition_file = addition_file_path
112
+
113
+ if run_joint_classifier is None: return "", "❌ Joint Tagger (JTP-3) yüklenemedi.", []
114
+ if image is None: return "", "⚠️ Resim yüklenmedi.", []
115
+ try:
116
+ ai_tags_string_raw, raw_tags_sorted_by_confidence = run_joint_classifier(image, threshold, device_pref)
117
+ original_order_for_joint = list(raw_tags_sorted_by_confidence.keys())
118
+ final_tags = self.process_tags(ai_tags_string_raw, sort_order, original_order_for_joint)
119
+ return final_tags, "✅ Joint (JTP-3) işlemi tamamlandı!", original_order_for_joint
120
+ except Exception as e:
121
+ return f"Hata: {e}", f"❌ Joint hata: {e}", []
modules/taggers/pixai.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import json
4
+ import pandas as pd
5
+ import numpy as np
6
+ import onnxruntime as ort
7
+ import huggingface_hub
8
+ from PIL import Image
9
+ from PIL.Image import Resampling
10
+ from modules.taggers.base import TaggerProcessor
11
+
12
+ # --- PIXAI Tagger Global Durum ---
13
+ PIXAI_MODEL = None
14
+ PIXAI_MODEL_NAME = None
15
+ PIXAI_TAGS_DF = None
16
+ PIXAI_D_IPS = None
17
+ PIXAI_PREPROCESS_FUNC = None
18
+ PIXAI_THRESHOLDS = None
19
+ PIXAI_CATEGORY_NAMES = None
20
+
21
+ def _download_pixai_files(model_name: str):
22
+ repo_id = model_name if '/' in model_name else f'deepghs/pixai-tagger-{model_name}-onnx'
23
+ return (
24
+ huggingface_hub.hf_hub_download(repo_id=repo_id, filename='model.onnx', library_name="pixai-tagger"),
25
+ huggingface_hub.hf_hub_download(repo_id=repo_id, filename='selected_tags.csv', library_name="pixai-tagger"),
26
+ huggingface_hub.hf_hub_download(repo_id=repo_id, filename='preprocess.json', library_name="pixai-tagger"),
27
+ huggingface_hub.hf_hub_download(repo_id=repo_id, filename='thresholds.csv', library_name="pixai-tagger")
28
+ )
29
+
30
+ def _load_pixai_model_components(device_pref: str):
31
+ global PIXAI_MODEL, PIXAI_MODEL_NAME, PIXAI_TAGS_DF, PIXAI_D_IPS, PIXAI_PREPROCESS_FUNC, PIXAI_THRESHOLDS, PIXAI_CATEGORY_NAMES
32
+ model_name = 'deepghs/pixai-tagger-v0.9-onnx'
33
+ if PIXAI_MODEL_NAME != model_name:
34
+ try:
35
+ m_path, t_path, p_path, th_path = _download_pixai_files(model_name)
36
+ providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] if device_pref == "CUDA" else ['CPUExecutionProvider']
37
+ PIXAI_MODEL = ort.InferenceSession(m_path, providers=providers)
38
+ PIXAI_TAGS_DF = pd.read_csv(t_path)
39
+ PIXAI_D_IPS = {}
40
+ if 'ips' in PIXAI_TAGS_DF.columns:
41
+ PIXAI_TAGS_DF['ips'] = PIXAI_TAGS_DF['ips'].apply(lambda x: json.loads(x) if pd.notna(x) and x != '{}' else {})
42
+
43
+ def transform(img):
44
+ if img.mode != 'RGB': img = img.convert('RGB')
45
+ img = img.resize((448, 448), Resampling.LANCZOS)
46
+ img_array = np.array(img).astype(np.float32) / 255.0
47
+ mean = np.array([0.48145466, 0.4578275, 0.40821073]).astype(np.float32)
48
+ std = np.array([0.26862954, 0.26130258, 0.27577711]).astype(np.float32)
49
+ img_array = (img_array - mean) / std
50
+ return np.transpose(img_array, (2, 0, 1))
51
+
52
+ PIXAI_PREPROCESS_FUNC = transform
53
+ if th_path and os.path.exists(th_path):
54
+ df_th = pd.read_csv(th_path)
55
+ PIXAI_THRESHOLDS = {row['category']: row['threshold'] for _, row in df_th.iterrows()}
56
+ PIXAI_CATEGORY_NAMES = {row['category']: row['name'] for _, row in df_th.iterrows()}
57
+ else:
58
+ PIXAI_THRESHOLDS = {0: 0.3, 4: 0.85, 9: 0.85}
59
+ PIXAI_CATEGORY_NAMES = {0: 'general', 4: 'character', 9: 'rating'}
60
+ PIXAI_MODEL_NAME = model_name
61
+ except Exception as e: print(f"PixAI yükleme hatası: {e}"); raise
62
+ return PIXAI_MODEL, PIXAI_TAGS_DF, PIXAI_D_IPS, PIXAI_PREPROCESS_FUNC, PIXAI_THRESHOLDS, PIXAI_CATEGORY_NAMES
63
+
64
+ def get_pixai_tags(image: Image, thresholds: dict, device_pref: str):
65
+ model, df_tags, _, preprocess, default_thresh, cat_names = _load_pixai_model_components(device_pref)
66
+ input_tensor = preprocess(image)
67
+ if len(input_tensor.shape) == 3: input_tensor = np.expand_dims(input_tensor, axis=0)
68
+
69
+ out = model.run(None, {'input': input_tensor.astype(np.float32)})[0][0]
70
+
71
+ mapped_thresh = {}
72
+ for cat_id, cat_name in cat_names.items():
73
+ if cat_name == 'general': mapped_thresh[cat_id] = thresholds.get('pixai_general_thresh', default_thresh.get(cat_id, 0.3))
74
+ elif cat_name in ['character', 'copyright', 'artist']: mapped_thresh[cat_id] = thresholds.get('pixai_char_thresh', default_thresh.get(cat_id, 0.85))
75
+ else: mapped_thresh[cat_id] = default_thresh.get(cat_id, 0.85)
76
+
77
+ all_tags = []
78
+ for cat in sorted(set(df_tags['category'])):
79
+ mask = df_tags['category'] == cat
80
+ names = df_tags.loc[mask, 'name']
81
+ preds = out[mask]
82
+ thresh = mapped_thresh.get(cat, 0.85)
83
+ sel_mask = preds >= thresh
84
+ for n, s in zip(names[sel_mask], preds[sel_mask]): all_tags.append((n, float(s)))
85
+
86
+ return ", ".join([t[0] for t in all_tags]), [t[0].replace("_", " ") for t in sorted(all_tags, key=lambda x: x[1], reverse=True)]
87
+
88
+ class PixaiTaggerProcessor(TaggerProcessor):
89
+ def predict(self, image, pixai_general_thresh, pixai_char_thresh, replacement_file_path, synonym_file_path, addition_file_path, sort_order="Alfabetik", device_pref: str = "Auto"):
90
+ self.replacement_file = replacement_file_path
91
+ self.synonym_file = synonym_file_path
92
+ self.addition_file = addition_file_path
93
+
94
+ if PIXAI_MODEL is None:
95
+ try: _load_pixai_model_components(device_pref)
96
+ except Exception as e: return "", f"❌ PixAI Tagger modülü yüklenemedi: {e}", []
97
+ if image is None: return "", "⚠️ Resim yüklenmedi.", []
98
+ try:
99
+ thresholds = {'pixai_general_thresh': pixai_general_thresh, 'pixai_char_thresh': pixai_char_thresh}
100
+ ai_tags_string_raw, original_order_for_pixai = get_pixai_tags(image, thresholds, device_pref)
101
+ final_tags = self.process_tags(ai_tags_string_raw, sort_order, original_order_for_pixai)
102
+ return final_tags, "✅ PixAI işlemi tamamlandı!", original_order_for_pixai
103
+ except Exception as e:
104
+ return f"Hata: {e}", f"❌ PixAI hata: {e}", []
modules/tools.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from modules.tools_text import (
3
+ clean_and_uniquify_text_simple,
4
+ convert_lines_to_comma,
5
+ convert_comma_to_lines,
6
+ select_and_copy_random_lines,
7
+ refine_text_by_file,
8
+ append_text_to_wildcard_file,
9
+ REFINELIST_FILENAME
10
+ )
11
+
12
+ from modules.tools_image import (
13
+ process_brightness_contrast,
14
+ process_denoise_sharpen,
15
+ process_resolution_change,
16
+ process_text_watermark,
17
+ process_format_change
18
+ )
19
+
20
+ from modules.tools_renaming import (
21
+ apply_renaming_from_configlist,
22
+ apply_custom_renaming
23
+ )
24
+ from modules.utils.file_utils import get_configlist_files
25
+
26
+ # Export Everything
27
+ __all__ = [
28
+ 'clean_and_uniquify_text_simple', 'convert_lines_to_comma', 'convert_comma_to_lines',
29
+ 'select_and_copy_random_lines', 'refine_text_by_file', 'process_resolution_change',
30
+ 'apply_renaming_from_configlist', 'apply_custom_renaming', 'get_configlist_files',
31
+ 'process_text_watermark', 'process_format_change', 'process_brightness_contrast',
32
+ 'process_denoise_sharpen', 'append_text_to_wildcard_file', 'REFINELIST_FILENAME'
33
+ ]
modules/tools_image.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import gradio as gr
4
+ from PIL import Image, ImageEnhance, ImageFilter, ImageDraw, ImageFont
5
+ from modules.utils.file_utils import _get_all_image_paths
6
+
7
+ def _apply_pil_enhancement(img: Image.Image, enhancer_type: ImageEnhance, level: int) -> Image.Image:
8
+ """PIL ImageEnhance kullanarak parlaklık veya kontrast ayarı yapar."""
9
+ # Level: -100'den 100'e. Factor: 0.0'dan 2.0'a (1.0 orijinal)
10
+ factor = (level / 100.0) + 1.0
11
+ factor = max(0.0, factor)
12
+ enhancer = enhancer_type(img)
13
+ return enhancer.enhance(factor)
14
+
15
+ def process_brightness_contrast(progress=gr.Progress(), folder_paths_str: str="", brightness_level: int=0, contrast_level: int=0):
16
+ folder_paths = [p.strip() for p in folder_paths_str.split('\n') if p.strip()]
17
+ if not folder_paths: return "⚠️ Klasör yolu girin.", gr.update(value="⚠️ İptal.", visible=True)
18
+ if brightness_level == 0 and contrast_level == 0: return "⚠️ Değişiklik yok.", gr.update(value="⚠️ İptal.", visible=True)
19
+
20
+ processed_count = 0
21
+ output_messages = [f"ℹ️ Parlaklık/Kontrast işlemi (P: {brightness_level}, K: {contrast_level})."]
22
+ all_image_paths = _get_all_image_paths(folder_paths)
23
+ total = len(all_image_paths)
24
+
25
+ if total == 0: return "⚠️ Resim bulunamadı.", gr.update(value="⚠️ Resim Yok.", visible=True)
26
+
27
+ for i, file_path in enumerate(all_image_paths):
28
+ filename = os.path.basename(file_path)
29
+ progress((i / total), desc=f"İşleniyor: {filename}")
30
+ try:
31
+ with Image.open(file_path) as img:
32
+ orig_mode = img.mode
33
+ if brightness_level != 0: img = _apply_pil_enhancement(img, ImageEnhance.Brightness, brightness_level)
34
+ if contrast_level != 0: img = _apply_pil_enhancement(img, ImageEnhance.Contrast, contrast_level)
35
+
36
+ ext = os.path.splitext(filename)[1].lower()
37
+ if ext in ('.jpg', '.jpeg'): img.convert("RGB").save(file_path, "JPEG", quality=95)
38
+ elif ext == '.webp': img.save(file_path, "WEBP", lossless=True)
39
+ else:
40
+ if orig_mode in ('RGBA', 'P'): img.save(file_path)
41
+ else: img.convert(orig_mode).save(file_path)
42
+ processed_count += 1
43
+ except Exception as e: output_messages.append(f"❌ Hata ({filename}): {e}")
44
+
45
+ return "\n".join(output_messages), gr.update(value=f"✅ {processed_count} resim işlendi.", visible=True)
46
+
47
+ def process_denoise_sharpen(progress=gr.Progress(), folder_paths_str: str="", denoise_level: int=0, sharpen_amount: float=0.0):
48
+ folder_paths = [p.strip() for p in folder_paths_str.split('\n') if p.strip()]
49
+ if not folder_paths: return "⚠️ Klasör yolu girin.", gr.update(value="⚠️ İptal.", visible=True)
50
+ if denoise_level == 0 and sharpen_amount == 0.0: return "⚠️ Değişiklik yok.", gr.update(value="⚠️ İptal.", visible=True)
51
+
52
+ processed_count = 0
53
+ output_messages = [f"ℹ️ Gürültü/Netlik işlemi (G: {denoise_level}, N: {sharpen_amount})."]
54
+ all_image_paths = _get_all_image_paths(folder_paths)
55
+ total = len(all_image_paths)
56
+
57
+ if total == 0: return "⚠️ Resim bulunamadı.", gr.update(value="⚠️ Resim Yok.", visible=True)
58
+
59
+ for i, file_path in enumerate(all_image_paths):
60
+ filename = os.path.basename(file_path)
61
+ progress((i / total), desc=f"İşleniyor: {filename}")
62
+ try:
63
+ with Image.open(file_path) as img:
64
+ orig_mode = img.mode
65
+ if denoise_level > 0: img = img.filter(ImageFilter.MedianFilter(size=denoise_level * 2 + 1))
66
+ if sharpen_amount > 0.0: img = ImageEnhance.Sharpness(img).enhance(sharpen_amount)
67
+
68
+ ext = os.path.splitext(filename)[1].lower()
69
+ if ext in ('.jpg', '.jpeg'): img.convert("RGB").save(file_path, "JPEG", quality=95)
70
+ elif ext == '.webp': img.save(file_path, "WEBP", lossless=True)
71
+ else:
72
+ if orig_mode in ('RGBA', 'P'): img.save(file_path)
73
+ else: img.convert(orig_mode).save(file_path)
74
+ processed_count += 1
75
+ except Exception as e: output_messages.append(f"❌ Hata ({filename}): {e}")
76
+
77
+ return "\n".join(output_messages), gr.update(value=f"✅ {processed_count} resim işlendi.", visible=True)
78
+
79
+ def process_resolution_change(progress=gr.Progress(), folder_paths_str="", scale_factor=1.0):
80
+ folder_paths = [p.strip() for p in folder_paths_str.split('\n') if p.strip()]
81
+ if not folder_paths: return "⚠️ Klasör yolu girin.", gr.update(value="⚠️ İptal.", visible=True)
82
+
83
+ scale = float(scale_factor)
84
+ processed_count = 0
85
+ output_messages = ["ℹ️ Çözünürlük değiştirme işlemi başladı."]
86
+ all_image_paths = _get_all_image_paths(folder_paths)
87
+ total = len(all_image_paths)
88
+
89
+ if total == 0: return "⚠️ Resim bulunamadı.", gr.update(value="⚠️ Resim Yok.", visible=True)
90
+
91
+ for i, file_path in enumerate(all_image_paths):
92
+ filename = os.path.basename(file_path)
93
+ progress((i / total), desc=f"İşleniyor: {filename}")
94
+ try:
95
+ with Image.open(file_path) as img:
96
+ nw = int(img.width * scale)
97
+ nh = int(img.height * scale)
98
+ if abs(nw - img.width) < 1 and abs(nh - img.height) < 1: continue
99
+ img.resize((nw, nh), Image.Resampling.LANCZOS).save(file_path)
100
+ processed_count += 1
101
+ except Exception as e: output_messages.append(f"❌ Hata ({filename}): {e}")
102
+
103
+ return "\n".join(output_messages), gr.update(value=f"✅ {processed_count} resim yeniden boyutlandırıldı.", visible=True)
104
+
105
+ def process_text_watermark(progress=gr.Progress(), folder_paths_str="", watermark_text="", opacity=0.35, font_size_ratio=0.05, rotation_angle=-45):
106
+ folder_paths = [p.strip() for p in folder_paths_str.split('\n') if p.strip()]
107
+ if not folder_paths or not watermark_text: return "⚠️ Yol veya metin eksik.", gr.update(value="⚠️ İptal.", visible=True)
108
+
109
+ processed = 0
110
+ all_paths = _get_all_image_paths(folder_paths)
111
+ total = len(all_paths)
112
+ if total == 0: return "⚠️ Resim yok.", gr.update(value="⚠️ İptal.", visible=True)
113
+
114
+ font_path = "arial.ttf"
115
+ try: ImageFont.truetype(font_path, 10)
116
+ except: font_path = None # Default kullan
117
+
118
+ for i, path in enumerate(all_paths):
119
+ progress(i/total, desc=f"Filigran: {os.path.basename(path)}")
120
+ try:
121
+ with Image.open(path).convert("RGBA") as base:
122
+ W, H = base.size
123
+ fs = max(10, int(W * font_size_ratio))
124
+ font = ImageFont.truetype(font_path, fs) if font_path else ImageFont.load_default()
125
+
126
+ # Layer oluştur
127
+ txt_layer = Image.new('RGBA', (W*2, H*2), (0,0,0,0))
128
+ draw = ImageDraw.Draw(txt_layer)
129
+ fill = (255, 255, 255, int(255 * opacity))
130
+
131
+ # Metin boyutu
132
+ bbox = draw.textbbox((0,0), watermark_text, font=font)
133
+ tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1]
134
+
135
+ # Döşeme (Tiling)
136
+ xspace, yspace = max(50, tw*2), max(50, th*4)
137
+ for y in range(0, H*2, yspace):
138
+ for x in range(0, W*2, xspace):
139
+ draw.text((x + (y//yspace % 2)*(xspace//2), y), watermark_text, font=font, fill=fill)
140
+
141
+ # Döndür ve Kırp
142
+ if rotation_angle != 0:
143
+ txt_layer = txt_layer.rotate(rotation_angle, resample=Image.BICUBIC)
144
+
145
+ cx, cy = txt_layer.width//2, txt_layer.height//2
146
+ final_layer = txt_layer.crop((cx - W//2, cy - H//2, cx + W//2, cy + H//2))
147
+
148
+ base.alpha_composite(final_layer)
149
+
150
+ ext = os.path.splitext(path)[1].lower()
151
+ if ext in ('.jpg', '.jpeg'): base.convert("RGB").save(path, "JPEG", quality=95)
152
+ else: base.save(path)
153
+ processed += 1
154
+ except Exception as e: print(f"Hata: {e}")
155
+
156
+ return "İşlem tamam.", gr.update(value=f"✅ {processed} resme filigran eklendi.", visible=True)
157
+
158
+ def process_format_change(progress=gr.Progress(), folder_paths_str="", target_format=""):
159
+ folder_paths = [p.strip() for p in folder_paths_str.split('\n') if p.strip()]
160
+ if not folder_paths: return "⚠️ Yol girin.", gr.update(value="⚠️ İptal.", visible=True)
161
+
162
+ fmt = target_format.strip().lower()
163
+ if fmt == '.svg': return "❌ SVG desteklenmez.", gr.update(value="❌ Hata.", visible=True)
164
+
165
+ processed = 0
166
+ all_paths = _get_all_image_paths(folder_paths)
167
+ total = len(all_paths)
168
+
169
+ for i, path in enumerate(all_paths):
170
+ progress(i/total, desc=f"Dönüştürülüyor: {os.path.basename(path)}")
171
+ name, ext = os.path.splitext(path)
172
+ if ext.lower() == fmt: continue
173
+
174
+ try:
175
+ with Image.open(path) as img:
176
+ new_path = f"{name}{fmt}"
177
+ save_fmt = "JPEG" if fmt in ('.jpg', '.jpeg') else fmt.replace('.', '').upper()
178
+
179
+ if img.mode == 'RGBA' and save_fmt == 'JPEG':
180
+ bg = Image.new('RGB', img.size, (255,255,255))
181
+ bg.paste(img, mask=img.split()[3])
182
+ bg.save(new_path, quality=95)
183
+ elif save_fmt == 'PNG':
184
+ img.save(new_path) # RGBA korur
185
+ else:
186
+ img.convert('RGB').save(new_path)
187
+
188
+ # Eskiyi sil
189
+ if os.path.exists(new_path): os.remove(path)
190
+ processed += 1
191
+ except Exception as e: print(f"Hata: {e}")
192
+
193
+ return "İşlem tamam.", gr.update(value=f"✅ {processed} resim dönüştürüldü.", visible=True)
modules/tools_renaming.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import shutil
4
+ import random
5
+ import gradio as gr
6
+ from modules.utils.file_utils import _get_all_image_paths
7
+
8
+ def apply_renaming_from_configlist(progress=gr.Progress(), folder_paths_str="", configlist_file="", renaming_type="Rastgele"):
9
+ """Liste dosyasından rastgele isim seçip dosyaları adlandırır."""
10
+ folder_paths = [p.strip() for p in folder_paths_str.split('\n') if p.strip()]
11
+ if not folder_paths: return "⚠️ Klasör yolu girin.", gr.update(value="⚠️ İptal.", visible=True)
12
+ if not configlist_file or not os.path.exists(configlist_file): return "❌ Liste dosyası seçilmedi.", gr.update(value="❌ Hata.", visible=True)
13
+
14
+ try:
15
+ with open(configlist_file, 'r', encoding='utf-8') as f: name_list = [line.strip() for line in f if line.strip()]
16
+ except Exception as e: return f"❌ Dosya okuma hatası: {e}", gr.update(value="❌ Hata.", visible=True)
17
+
18
+ if not name_list: return "⚠️ Liste boş.", gr.update(value="⚠️ İptal.", visible=True)
19
+
20
+ output_messages = ["ℹ️ Liste bazlı adlandırma başladı."]
21
+ renamed_count = 0
22
+ all_image_paths = _get_all_image_paths(folder_paths)
23
+ total = len(all_image_paths)
24
+
25
+ if total == 0: return "⚠️ Resim bulunamadı.", gr.update(value="⚠️ Resim Yok.", visible=True)
26
+
27
+ # İsim listesini karıştır (Rastgele mod ise)
28
+ if renaming_type == "Rastgele": random.shuffle(name_list)
29
+
30
+ # Yeterli isim var mı?
31
+ if len(name_list) < total: output_messages.append("⚠️ Uyarı: İsim listesi dosya sayısından az, bazıları tekrar edebilir veya işlenmeyebilir.")
32
+
33
+ for i, file_path in enumerate(all_image_paths):
34
+ if i >= len(name_list): break # İsim bitti
35
+ filename = os.path.basename(file_path)
36
+ progress((i / total), desc=f"Adlandırılıyor: {filename}")
37
+
38
+ ext = os.path.splitext(filename)[1]
39
+ new_base = name_list[i].replace(' ', '_').replace(',', '_')
40
+ new_name = f"{new_base}{ext}"
41
+ new_path = os.path.join(os.path.dirname(file_path), new_name)
42
+
43
+ if file_path != new_path:
44
+ try:
45
+ shutil.move(file_path, new_path)
46
+ renamed_count += 1
47
+ except Exception as e: output_messages.append(f"❌ Hata ({filename}): {e}")
48
+
49
+ return "\n".join(output_messages), gr.update(value=f"✅ {renamed_count} dosya adlandırıldı.", visible=True)
50
+
51
+ def apply_custom_renaming(progress=gr.Progress(), folder_paths_str: str="", pattern: str="", start_number: int=1, digit_count: int=3):
52
+ """
53
+ Dosyaları şablon + numara mantığıyla adlandırır.
54
+ Pattern içinde {Number} olmalıdır. Örn: "Adoptable {Number}"
55
+ """
56
+ folder_paths = [p.strip() for p in folder_paths_str.split('\n') if p.strip()]
57
+ if not folder_paths: return "⚠️ Klasör yolu girin.", gr.update(value="⚠️ İptal.", visible=True)
58
+ if not pattern or "{Number}" not in pattern: return "❌ Şablon '{Number}' içermelidir!", gr.update(value="❌ Hatalı Şablon.", visible=True)
59
+
60
+ renamed_count = 0
61
+ curr_num = int(start_number)
62
+ output_messages = [f"ℹ️ Şablonlu adlandırma: '{pattern}', Başlangıç: {curr_num}"]
63
+
64
+ # Her klasör için ayrı işlem (Sıralamayı korumak için)
65
+ for folder in folder_paths:
66
+ if not os.path.isdir(folder): continue
67
+ images = [f for f in os.listdir(folder) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
68
+ images.sort() # İsme göre sırala
69
+
70
+ if not images: continue
71
+
72
+ for i, img_name in enumerate(images):
73
+ progress(0, desc=f"Adlandırılıyor: {img_name}") # Basit progress
74
+
75
+ ext = os.path.splitext(img_name)[1]
76
+ num_str = str(curr_num).zfill(int(digit_count))
77
+ new_base = pattern.replace("{Number}", num_str).strip()
78
+ new_name = f"{new_base}{ext}"
79
+
80
+ old_path = os.path.join(folder, img_name)
81
+ new_path = os.path.join(folder, new_name)
82
+
83
+ if old_path != new_path:
84
+ try:
85
+ if os.path.exists(new_path) and old_path.lower() != new_path.lower():
86
+ output_messages.append(f"⚠️ Çakışma: '{new_name}' zaten var. Atlandı.")
87
+ else:
88
+ shutil.move(old_path, new_path)
89
+ renamed_count += 1
90
+ except Exception as e: output_messages.append(f"❌ Hata ({img_name}): {e}")
91
+
92
+ curr_num += 1
93
+
94
+ return "\n".join(output_messages), gr.update(value=f"✅ {renamed_count} dosya adlandırıldı. Son No: {curr_num-1}", visible=True)
modules/tools_text.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import re
4
+ import random
5
+ import gradio as gr
6
+
7
+ REFINELIST_PATH = "data/text_lists/RafineListesi.txt"
8
+ REFINELIST_FILENAME = "RafineListesi.txt"
9
+
10
+ def clean_and_uniquify_text_simple(text):
11
+ """Metni temizler, alt çizgileri boşluğa çevirir ve etiketleri tekilleştirir."""
12
+ if not text: return ""
13
+ cleaned = re.sub(r'[\s_]+', ' ', text).strip()
14
+ tags = {t.strip() for t in cleaned.split(',') if t.strip()}
15
+ return ", ".join(sorted(tags))
16
+
17
+ def convert_lines_to_comma(text):
18
+ """Satırları virgülle ayrılmış metne çevirir."""
19
+ lines = [line.strip() for line in text.split('\n') if line.strip()]
20
+ if not lines: return "", gr.update(value="⚠️ Girişte geçerli metin bulunamadı.", visible=True)
21
+ return ", ".join(lines), gr.update(value="✅ Satırlar virgüllü metne çevrildi.", visible=True)
22
+
23
+ def convert_comma_to_lines(text):
24
+ """Virgülle ayrılmış metni satırlara çevirir."""
25
+ lines = [line.strip() for line in text.split(',') if line.strip()]
26
+ if not lines: return "", gr.update(value="⚠️ Girişte geçerli metin bulunamadı.", visible=True)
27
+ return "\n".join(lines), gr.update(value="✅ Virgüllü metin satırlara çevrildi.", visible=True)
28
+
29
+ def select_and_copy_random_lines(text_input_raw, num_lines_to_copy_slider):
30
+ """Metinden rastgele satırları seçer."""
31
+ lines = [line.strip() for line in text_input_raw.split('\n') if line.strip()]
32
+ num_lines = int(num_lines_to_copy_slider)
33
+ if not lines: return "", gr.update(value="⚠️ Girişte geçerli metin bulunamadı.", visible=True)
34
+ if num_lines > len(lines):
35
+ return "", gr.update(value=f"⚠️ {len(lines)} satır var, ancak {num_lines} satır istendi. İşlem iptal edildi.", visible=True)
36
+ selected = random.sample(lines, num_lines)
37
+ return "\n".join(selected), gr.update(value=f"✅ {num_lines} adet rastgele satır seçildi.", visible=True)
38
+
39
+ def refine_text_by_file(input_text: str):
40
+ """Kullanıcı metnini RafineListesi.txt dosyasındaki anahtar kelimelere göre filtreler."""
41
+ if not input_text.strip():
42
+ return "", gr.update(value="⚠️ Girişte geçerli metin bulunamadı.", visible=True)
43
+
44
+ target_path = REFINELIST_PATH
45
+ try:
46
+ if not os.path.exists(target_path):
47
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
48
+ with open(target_path, "w", encoding="utf-8") as f: f.write("")
49
+ return input_text, gr.update(value=f"⚠️ Uyarı: '{target_path}' dosyası bulunamadı, boş bir dosya oluşturuldu.", visible=True)
50
+
51
+ with open(target_path, 'r', encoding='utf-8') as f:
52
+ kaldirilacak_kelimeler = [satir.strip().lower() for satir in f if satir.strip()]
53
+
54
+ if not kaldirilacak_kelimeler:
55
+ return input_text, gr.update(value="⚠️ Kaldırılacak kelime listesi boş.", visible=True)
56
+ except Exception as e:
57
+ return input_text, gr.update(value=f"❌ Dosya okuma hatası: {e}", visible=True)
58
+
59
+ bolumler = [b.strip() for b in re.split(r'[, \n]+', input_text) if b.strip()]
60
+ temiz_bolumler = []
61
+ kaldirilan_sayisi = 0
62
+
63
+ for bolum in bolumler:
64
+ bolum_kucuk_harf = bolum.lower()
65
+ gecerli_kalacak_mi = True
66
+ for kelime in kaldirilacak_kelimeler:
67
+ if kelime in bolum_kucuk_harf:
68
+ gecerli_kalacak_mi = False; break
69
+ if gecerli_kalacak_mi: temiz_bolumler.append(bolum)
70
+ else: kaldirilan_sayisi += 1
71
+
72
+ temiz_bolumler_set = sorted(list(set(temiz_bolumler)))
73
+ return ', '.join(temiz_bolumler_set), gr.update(value=f"✅ {len(temiz_bolumler_set)} etiket kaldı, {kaldirilan_sayisi} etiket kaldırıldı.", visible=True)
74
+
75
+ def append_text_to_wildcard_file(text, filename):
76
+ if not text.strip(): return gr.update(value="⚠️ Metin boş.")
77
+ folder = "data/wildcards"
78
+ os.makedirs(folder, exist_ok=True)
79
+ filepath = os.path.join(folder, f"{filename}.txt")
80
+
81
+ try:
82
+ # Mevcut içeriği oku ve duplicate kontrolü yap
83
+ existing_lines = set()
84
+ if os.path.exists(filepath):
85
+ with open(filepath, "r", encoding="utf-8") as f:
86
+ existing_lines = {line.strip() for line in f if line.strip()}
87
+
88
+ new_text = text.strip()
89
+
90
+ # Eğer bu etiket listesi zaten eklenmişse uyarı ver
91
+ if new_text in existing_lines:
92
+ return gr.update(value=f"⚠️ Bu etiket listesi '{filename}' paketine zaten eklenmiş!", visible=True)
93
+
94
+ # Eklenmemişse dosyaya ekle
95
+ with open(filepath, "a", encoding="utf-8") as f:
96
+ f.write(new_text + "\n")
97
+ return gr.update(value=f"✅ '{filename}' paketine eklendi.", visible=True)
98
+ except Exception as e: return gr.update(value=f"❌ Hata: {e}", visible=True)
modules/ui/common.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/ui/common.py
2
+
3
+ CSS_STYLES = """
4
+ body { font-family: 'Inter', 'Segoe UI', Arial, sans-serif; background: #f0f2f5; color: #333; }
5
+ .gradio-container { box-shadow: 0 8px 25px rgba(0, 0, 0, 0.07); border-radius: 12px; overflow: hidden; background: #ffffff; max-width: 96% !important; }
6
+ h1, h2, h3, h4, h5, h6 { color: #2c3e50; font-weight: 600; }
7
+
8
+ .tab-nav { display: flex; justify-content: center; width: 100%; }
9
+ .gr-tab-item { padding: 15px 25px; font-size: 1.1em; font-weight: 500; }
10
+ .gr-tab-item.selected { background: linear-gradient(45deg, #6a11cb 0%, #2575fc 100%); color: white; border-radius: 8px 8px 0 0; }
11
+ .gr-button { border-radius: 8px; font-weight: 600; transition: all 0.3s ease; }
12
+ .gr-button.primary { background: linear-gradient(45deg, #6a11cb 0%, #2575fc 100%); color: white; border: none; }
13
+ .gr-button.primary:hover { opacity: 0.9; transform: translateY(-1px); }
14
+ .gr-button.stop { background: linear-gradient(45deg, #ff4e50 0%, #f9d423 100%); color: white; border: none; }
15
+ .gr-button.stop:hover { opacity: 0.9; transform: translateY(-1px); }
16
+ .gr-textbox, .gr-dropdown, .gr-slider { border-radius: 8px; border: 1px solid #e0e0e0; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); }
17
+ .gr-image, .gr-dataframe { border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); }
18
+ .kategori-grid-app2 { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; padding: 10px; }
19
+ .kategori-card-app2 { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 16px; padding: 20px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); transition: all 0.3s ease; display: flex; flex-direction: column; }
20
+ .kategori-card-app2:hover { transform: translateY(-4px); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); border-color: #cbd5e1; }
21
+ .kategori-title-app2 { font-size: 1.25rem; font-weight: 700; color: #1e293b; margin-bottom: 12px; border-bottom: 2px solid #f1f5f9; padding-bottom: 8px; letter-spacing: -0.025em; }
22
+ .kategori-tags-app2 { font-size: 0.95rem; color: #475569; line-height: 1.6; font-family: 'Consolas', 'Monaco', monospace; background: #f8fafc; padding: 10px; border-radius: 8px; border: 1px solid #e2e8f0; flex-grow: 1; }
23
+ .grid-tag { background:#fff3; display:inline-block; margin:2px 6px 2px 0; padding:5px 10px; border-radius:12px; border:1px solid #cbd5e1; font-size:14px; color:#334155; }
24
+ #alert-box { margin-top: 15px; padding: 12px 18px; border-radius: 8px; font-weight: 600; text-align: center; }
25
+ #alert-box[value^="✅"] { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
26
+ #alert-box[value^="⚠️"] { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
27
+ #alert-box[value^="❌"] { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
28
+ #alert-box[value^="🗑️"] { background-color: #e2e3e5; color: #383d41; border: 1px solid #d6d8db; }
29
+ #renaming-alert-box { margin-top: 15px; padding: 12px 18px; border-radius: 8px; font-weight: 600; text-align: left; white-space: pre-wrap; }
30
+ #renaming-alert-box[value^="✅"] { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
31
+ #renaming-alert-box[value^="⚠️"] { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
32
+ #renaming-alert-box[value^="❌"] { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
33
+ #renaming-alert-box[value^="ℹ️"] { background-color: #cce5ff; color: #004085; border: 1px solid #b8daff; }
34
+ .gr-box { border: 1px solid #e0e0e0 !important; border-radius: 12px !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important; }
35
+ .gr-row, .gr-column { padding: 10px; }
36
+ .copy-output-btn { margin-top: 10px; padding: 8px 12px; border-radius: 6px; background-color: #f0f0f0; border: 1px solid #ccc; cursor: pointer; font-weight: 500; transition: all 0.2s ease; width: 100%; }
37
+ .copy-output-btn:hover { background-color: #e0e0e0; }
38
+ .vertical-divider { border-left: 2px solid #e0e0e0; height: auto; margin: 0 20px; }
39
+
40
+ /* --- Tab Styling Improvements --- */
41
+ .gradio-container .tabs > .tab-nav {
42
+ border-bottom: 2px solid #e5e7eb;
43
+ margin-bottom: 20px;
44
+ background-color: #f8fafc;
45
+ border-radius: 12px 12px 0 0;
46
+ padding: 10px 10px 0 10px;
47
+
48
+ /* Force single line */
49
+ display: flex;
50
+ justify-content: center; /* Or center, depending on preference */
51
+ flex-wrap: nowrap;
52
+ overflow-x: auto; /* Allow scrolling if it really doesn't fit */
53
+ white-space: nowrap;
54
+ }
55
+
56
+ .gradio-container .tabs > .tab-nav > button {
57
+ display: inline-flex;
58
+ justify-content: center;
59
+ align-items: center;
60
+ padding: 5px 8px; /* Reduced padding further */
61
+ font-size: 0.9em; /* Reduced font size further */
62
+ font-weight: 700 !important;
63
+ color: #64748b;
64
+ border: none;
65
+ background: transparent;
66
+ cursor: pointer;
67
+ transition: all 0.2s ease;
68
+ border-radius: 8px 8px 0 0;
69
+ margin-right: 2px;
70
+ font-family: 'Segoe UI', sans-serif;
71
+ letter-spacing: 0.01em;
72
+ flex-shrink: 0; /* Prevent squishing too much */
73
+ }
74
+
75
+ .gradio-container .tabs > .tab-nav > button:hover {
76
+ color: #3b82f6;
77
+ background-color: #e2e8f0;
78
+ }
79
+
80
+ .gradio-container .tabs > .tab-nav > button.selected {
81
+ color: #2563eb;
82
+ background-color: #ffffff;
83
+ border-bottom: 3px solid #2563eb;
84
+ box-shadow: 0 -2px 4px -1px rgba(0, 0, 0, 0.05);
85
+ font-size: 0.95em; /* Slightly larger than normal but smaller than before */
86
+ z-index: 10;
87
+ }
88
+
89
+
90
+
91
+
92
+ .footer-content { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-top: 15px; }
93
+
94
+ /* Hidden Timer Buttons */
95
+ #timer_btn, #main_loop_btn { display: none !important; }
96
+ """
modules/ui/tab_batch.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # modules/ui/tab_batch.py
3
+
4
+ import gradio as gr
5
+ from modules.tagger import toplu_islem_batch
6
+ from modules.tools import append_text_to_wildcard_file
7
+ from modules.managers.localization_manager import get_str
8
+
9
+ def create_batch_tab(settings_inputs):
10
+ """
11
+ Toplu etiketleme sekmesini oluşturur.
12
+ settings_inputs: create_settings_tab fonksiyonundan dönen sözlük.
13
+ """
14
+ with gr.TabItem(get_str("tab_batch_title")):
15
+ with gr.Row():
16
+ toplu_batch_process_button = gr.Button(get_str("btn_process_batch"), variant="primary", size="lg")
17
+
18
+ with gr.Row():
19
+ with gr.Column(scale=3):
20
+ toplu_batch_image_input = gr.File(label=get_str("label_batch_input"), file_count="multiple", file_types=["image"], interactive=True)
21
+
22
+ with gr.Column(scale=7):
23
+ toplu_batch_alert = gr.Textbox(label=get_str("status_label"), elem_id="alert-box", interactive=False)
24
+ with gr.Tab(get_str("tab_batch_original")):
25
+ toplu_batch_combined_original_tags = gr.Textbox(label=get_str("label_batch_original"), interactive=False, lines=15, show_copy_button=True)
26
+ # --- BUTON EKLENDİ ---
27
+ btn_send_toplu = gr.Button(get_str("btn_send_to_cat"), variant="secondary")
28
+
29
+ with gr.Tab(get_str("tab_batch_refined")):
30
+ toplu_batch_refined_tags = gr.Textbox(label=get_str("label_batch_refined"), interactive=False, lines=15, show_copy_button=True)
31
+ with gr.Tab(get_str("tab_batch_cat")):
32
+ toplu_batch_categorized_output = gr.Textbox(label=get_str("label_categorized_tags"), interactive=False, lines=15, show_copy_button=True)
33
+ # --- WILDCARD BUTONLARI (TOPLU) ---
34
+ gr.Markdown(get_str("header_wildcard"))
35
+ with gr.Row():
36
+ btn_female_sfw_toplu = gr.Button("FEMALE SFWPACK", size="sm")
37
+ btn_female_nsfw_toplu = gr.Button("FEMALE NSFWPACK", size="sm")
38
+ btn_female_nude_toplu = gr.Button("FEMALE NUDEPACK", size="sm")
39
+ with gr.Row():
40
+ btn_futa_sfw_toplu = gr.Button("FUTANARI SFWPACK", size="sm")
41
+ btn_futa_nsfw_toplu = gr.Button("FUTANARI NSFWPACK", size="sm")
42
+ btn_futa_nude_toplu = gr.Button("FUTANARI NUDEPACK", size="sm")
43
+ with gr.Row():
44
+ btn_furry_toplu = gr.Button("FURRY-ANTHRO", size="sm")
45
+ btn_hair_toplu = gr.Button("HAIR-EYE", size="sm")
46
+
47
+ with gr.Tab(get_str("tab_batch_gemini")):
48
+ toplu_batch_gemini_output = gr.Textbox(label=get_str("label_batch_gemini"), interactive=False, lines=15, show_copy_button=True)
49
+ with gr.Tab(get_str("tab_batch_html")):
50
+ toplu_batch_results_html = gr.HTML(label=get_str("label_batch_html"))
51
+
52
+ toplu_batch_download_button = gr.File(label=get_str("btn_batch_download"), visible=False, interactive=False)
53
+
54
+ batch_inputs = [
55
+ toplu_batch_image_input,
56
+ settings_inputs["set_joint_thresh"], settings_inputs["set_use_joint"],
57
+ settings_inputs["set_cl_gen_thresh"], settings_inputs["set_cl_char_thresh"], settings_inputs["set_use_cl_tagger"],
58
+ settings_inputs["set_pixai_gen_thresh"], settings_inputs["set_pixai_char_thresh"], settings_inputs["set_use_pixai_tagger"],
59
+ settings_inputs["set_animetagger_model"], settings_inputs["set_animetagger_thresh"], settings_inputs["set_use_animetagger"],
60
+ # Gemini
61
+ settings_inputs["set_use_gemini"], settings_inputs["set_gemini_api_key"], settings_inputs["set_gemini_mode"], settings_inputs["set_gemini_model"],
62
+ settings_inputs["set_gemini_prompt_vision"], settings_inputs["set_gemini_prompt_tags"], settings_inputs["set_gemini_prompt_hybrid"],
63
+ settings_inputs["set_gemini_system_instruction"],
64
+ # Dosyalar
65
+ settings_inputs["set_replacement_file"], settings_inputs["set_synonym_file"], settings_inputs["set_addition_file"],
66
+ settings_inputs["set_toplu_enable_categorization"], settings_inputs["set_toplu_selected_categories"],
67
+ settings_inputs["set_sort_order"], settings_inputs["set_device"], settings_inputs["set_context_weight"]
68
+ ]
69
+ batch_outputs = [toplu_batch_alert, toplu_batch_results_html, toplu_batch_download_button, toplu_batch_categorized_output, toplu_batch_combined_original_tags, toplu_batch_refined_tags, toplu_batch_gemini_output]
70
+ toplu_batch_process_button.click(fn=toplu_islem_batch, inputs=batch_inputs, outputs=batch_outputs).then(lambda file_output: gr.update(visible=file_output is not None), inputs=[toplu_batch_download_button], outputs=[toplu_batch_download_button])
71
+
72
+ # WILDCARD EVENTLERI (TOPLU)
73
+ btn_female_sfw_toplu.click(fn=lambda x: append_text_to_wildcard_file(x, "FEMALE SFWPACK"), inputs=[toplu_batch_categorized_output], outputs=[toplu_batch_alert])
74
+ btn_female_nsfw_toplu.click(fn=lambda x: append_text_to_wildcard_file(x, "FEMALE NSFWPACK"), inputs=[toplu_batch_categorized_output], outputs=[toplu_batch_alert])
75
+ btn_female_nude_toplu.click(fn=lambda x: append_text_to_wildcard_file(x, "FEMALE NUDEPACK"), inputs=[toplu_batch_categorized_output], outputs=[toplu_batch_alert])
76
+
77
+ btn_futa_sfw_toplu.click(fn=lambda x: append_text_to_wildcard_file(x, "FUTANARI SFWPACK"), inputs=[toplu_batch_categorized_output], outputs=[toplu_batch_alert])
78
+ btn_futa_nsfw_toplu.click(fn=lambda x: append_text_to_wildcard_file(x, "FUTANARI NSFWPACK"), inputs=[toplu_batch_categorized_output], outputs=[toplu_batch_alert])
79
+ btn_futa_nude_toplu.click(fn=lambda x: append_text_to_wildcard_file(x, "FUTANARI NUDEPACK"), inputs=[toplu_batch_categorized_output], outputs=[toplu_batch_alert])
80
+
81
+ btn_furry_toplu.click(fn=lambda x: append_text_to_wildcard_file(x, "FURRY-ANTHRO"), inputs=[toplu_batch_categorized_output], outputs=[toplu_batch_alert])
82
+ btn_hair_toplu.click(fn=lambda x: append_text_to_wildcard_file(x, "HAIR-EYE"), inputs=[toplu_batch_categorized_output], outputs=[toplu_batch_alert])
83
+
84
+ return {
85
+ "btn_send_toplu": btn_send_toplu,
86
+ "toplu_batch_combined_original_tags": toplu_batch_combined_original_tags
87
+ }
modules/ui/tab_dual.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # modules/ui/tab_dual.py
3
+
4
+ import gradio as gr
5
+ from modules.tagger import process_dual_images
6
+ from modules.tools import append_text_to_wildcard_file
7
+ from modules.managers.localization_manager import get_str
8
+
9
+ def create_dual_tab(settings_inputs):
10
+ """
11
+ Dual (ikili) etiketleme sekmesini oluşturur.
12
+ settings_inputs: create_settings_tab fonksiyonundan dönen sözlük.
13
+ """
14
+ with gr.TabItem(get_str("tab_dual_title")):
15
+ gr.Markdown(get_str("dual_note"))
16
+ with gr.Row():
17
+ dual_process_button_top = gr.Button(get_str("btn_process_dual"), variant="primary", size="lg", visible=False)
18
+
19
+ with gr.Row():
20
+ with gr.Column(scale=3):
21
+ dual_image_input1 = gr.Image(type="pil", label=get_str("label_img1"), width=300, height=300)
22
+ dual_image_input2 = gr.Image(type="pil", label=get_str("label_img2"), width=300, height=300)
23
+
24
+ with gr.Column(scale=7):
25
+ dual_alert = gr.Textbox(label=get_str("status_label"), elem_id="alert-box", visible=False, interactive=False)
26
+ with gr.Tab(get_str("tab_dual_combined")):
27
+ dual_combined_all_tags = gr.Textbox(label=get_str("label_dual_combined"), interactive=True, lines=8, show_copy_button=True)
28
+ # --- BUTON EKLENDİ ---
29
+ btn_send_dual = gr.Button(get_str("btn_send_to_cat"), variant="secondary")
30
+
31
+ with gr.Tab(get_str("tab_refined_tags")):
32
+ dual_combined_refined_tags = gr.Textbox(label=get_str("label_dual_refined"), interactive=True, lines=8, show_copy_button=True)
33
+ with gr.Tab(get_str("tab_categorized_tags")):
34
+ dual_combined_categorized_output = gr.Textbox(label=get_str("label_dual_cat"), interactive=True, lines=8, show_copy_button=True)
35
+ # --- WILDCARD BUTONLARI (DUAL) ---
36
+ gr.Markdown(get_str("header_wildcard"))
37
+ with gr.Row():
38
+ btn_female_sfw_dual = gr.Button("FEMALE SFWPACK", size="sm")
39
+ btn_female_nsfw_dual = gr.Button("FEMALE NSFWPACK", size="sm")
40
+ btn_female_nude_dual = gr.Button("FEMALE NUDEPACK", size="sm")
41
+ with gr.Row():
42
+ btn_futa_sfw_dual = gr.Button("FUTANARI SFWPACK", size="sm")
43
+ btn_futa_nsfw_dual = gr.Button("FUTANARI NSFWPACK", size="sm")
44
+ btn_futa_nude_dual = gr.Button("FUTANARI NUDEPACK", size="sm")
45
+ with gr.Row():
46
+ btn_furry_dual = gr.Button("FURRY-ANTHRO", size="sm")
47
+ btn_hair_dual = gr.Button("HAIR-EYE", size="sm")
48
+
49
+ with gr.Tab(get_str("tab_gemini_caption")):
50
+ with gr.Row():
51
+ dual_gemini_output_img1 = gr.Textbox(label=get_str("label_gemini_img1"), interactive=True, lines=5, show_copy_button=True)
52
+ dual_gemini_output_img2 = gr.Textbox(label=get_str("label_gemini_img2"), interactive=True, lines=5, show_copy_button=True)
53
+ with gr.Tab(get_str("tab_combined_caption")):
54
+ dual_combined_cat_gem_output = gr.Textbox(label=get_str("label_combined_caption"), interactive=True, lines=10, show_copy_button=True)
55
+
56
+ dual_inputs = [
57
+ dual_image_input1, dual_image_input2,
58
+ settings_inputs["set_joint_thresh"], settings_inputs["set_use_joint"], settings_inputs["set_joint_thresh"], settings_inputs["set_use_joint"],
59
+ settings_inputs["set_cl_gen_thresh"], settings_inputs["set_cl_char_thresh"], settings_inputs["set_use_cl_tagger"], settings_inputs["set_cl_gen_thresh"], settings_inputs["set_cl_char_thresh"], settings_inputs["set_use_cl_tagger"],
60
+ settings_inputs["set_pixai_gen_thresh"], settings_inputs["set_pixai_char_thresh"], settings_inputs["set_use_pixai_tagger"], settings_inputs["set_pixai_gen_thresh"], settings_inputs["set_pixai_char_thresh"], settings_inputs["set_use_pixai_tagger"],
61
+ settings_inputs["set_animetagger_model"], settings_inputs["set_animetagger_thresh"], settings_inputs["set_use_animetagger"], settings_inputs["set_animetagger_model"], settings_inputs["set_animetagger_thresh"], settings_inputs["set_use_animetagger"],
62
+ # Gemini
63
+ settings_inputs["set_use_gemini"], settings_inputs["set_gemini_api_key"], settings_inputs["set_gemini_mode"], settings_inputs["set_gemini_model"],
64
+ settings_inputs["set_gemini_prompt_vision"], settings_inputs["set_gemini_prompt_tags"], settings_inputs["set_gemini_prompt_hybrid"],
65
+ settings_inputs["set_gemini_system_instruction"],
66
+ # Dosyalar
67
+ settings_inputs["set_replacement_file"], settings_inputs["set_synonym_file"], settings_inputs["set_addition_file"],
68
+ settings_inputs["set_dual1_enable_categorization"], settings_inputs["set_dual1_selected_categories"],
69
+ settings_inputs["set_dual2_enable_categorization"], settings_inputs["set_dual2_selected_categories"],
70
+ settings_inputs["set_sort_order"], settings_inputs["set_device"], settings_inputs["set_context_weight"]
71
+ ]
72
+ def update_dual_outputs_wrapper(*args):
73
+ final_overall_alert, all_tags_str, refined_tags_str, combined_cat_tags_str, gemini_out1, gemini_out2, combined_cat_gem = process_dual_images(*args)
74
+ return (gr.update(value=final_overall_alert, visible=True), all_tags_str, refined_tags_str, combined_cat_tags_str, gemini_out1, gemini_out2, combined_cat_gem)
75
+
76
+ dual_process_button_top.click(fn=update_dual_outputs_wrapper, inputs=dual_inputs, outputs=[dual_alert, dual_combined_all_tags, dual_combined_refined_tags, dual_combined_categorized_output, dual_gemini_output_img1, dual_gemini_output_img2, dual_combined_cat_gem_output])
77
+
78
+ # --- OTOMATİK TETİKLEME (RESİM DEĞİŞİNCE) ---
79
+ dual_image_input1.change(fn=update_dual_outputs_wrapper, inputs=dual_inputs, outputs=[dual_alert, dual_combined_all_tags, dual_combined_refined_tags, dual_combined_categorized_output, dual_gemini_output_img1, dual_gemini_output_img2, dual_combined_cat_gem_output])
80
+ dual_image_input2.change(fn=update_dual_outputs_wrapper, inputs=dual_inputs, outputs=[dual_alert, dual_combined_all_tags, dual_combined_refined_tags, dual_combined_categorized_output, dual_gemini_output_img1, dual_gemini_output_img2, dual_combined_cat_gem_output])
81
+
82
+ # WILDCARD EVENTLERI (DUAL)
83
+ btn_female_sfw_dual.click(fn=lambda x: append_text_to_wildcard_file(x, "FEMALE SFWPACK"), inputs=[dual_combined_categorized_output], outputs=[dual_alert])
84
+ btn_female_nsfw_dual.click(fn=lambda x: append_text_to_wildcard_file(x, "FEMALE NSFWPACK"), inputs=[dual_combined_categorized_output], outputs=[dual_alert])
85
+ btn_female_nude_dual.click(fn=lambda x: append_text_to_wildcard_file(x, "FEMALE NUDEPACK"), inputs=[dual_combined_categorized_output], outputs=[dual_alert])
86
+
87
+ btn_futa_sfw_dual.click(fn=lambda x: append_text_to_wildcard_file(x, "FUTANARI SFWPACK"), inputs=[dual_combined_categorized_output], outputs=[dual_alert])
88
+ btn_futa_nsfw_dual.click(fn=lambda x: append_text_to_wildcard_file(x, "FUTANARI NSFWPACK"), inputs=[dual_combined_categorized_output], outputs=[dual_alert])
89
+ btn_futa_nude_dual.click(fn=lambda x: append_text_to_wildcard_file(x, "FUTANARI NUDEPACK"), inputs=[dual_combined_categorized_output], outputs=[dual_alert])
90
+
91
+ btn_furry_dual.click(fn=lambda x: append_text_to_wildcard_file(x, "FURRY-ANTHRO"), inputs=[dual_combined_categorized_output], outputs=[dual_alert])
92
+ btn_hair_dual.click(fn=lambda x: append_text_to_wildcard_file(x, "HAIR-EYE"), inputs=[dual_combined_categorized_output], outputs=[dual_alert])
93
+
94
+ return {
95
+ "btn_send_dual": btn_send_dual,
96
+ "dual_combined_all_tags": dual_combined_all_tags
97
+ }
modules/ui/tab_settings.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # modules/ui/tab_settings.py
3
+
4
+ import os
5
+ import gradio as gr
6
+ from modules.config_manager import save_global_tagger_settings
7
+ from modules.managers.rule_manager import (
8
+ get_replacement_files,
9
+ get_synonym_files,
10
+ get_addition_files
11
+ )
12
+ from modules.managers.category_manager import kategori_listesi
13
+ from modules.managers.localization_manager import get_str # Localization import
14
+ # cl_tagger_instance'ı kontrol etmek için (opsiyonel, main'den de geçilebilir ama import etmek daha kolay)
15
+ from modules.tagger import cl_tagger_instance
16
+
17
+ REPLACEMENT_RULES_KLASORU = "data/rules/replacement_rules"
18
+ SYNONYM_RULES_KLASORU = "data/rules/synonym_rules"
19
+ ADDITION_RULES_KLASORU = "data/rules/addition_rules"
20
+
21
+ def create_settings_tab(app_config):
22
+ initial_global_settings = app_config.get("global_tagger_settings", {})
23
+ initial_general_settings = app_config.get("general_settings", {"device": "Auto", "language": "tr"})
24
+
25
+ # Normalizasyon Haritaları (Legacy -> New Key)
26
+ sort_normalization = {
27
+ "Alfabetik": "alpha",
28
+ "Uzunluğa Göre": "length",
29
+ "Rastgele": "random",
30
+ "Orijinal": "original"
31
+ }
32
+
33
+ anime_normalization = {
34
+ "MobileNet V4 (Hızlı)": "mobilenetv4",
35
+ "ConvNeXt V2 Huge (Pro)": "convnextv2",
36
+ "Caformer B36 (Yeni)": "caformer"
37
+ }
38
+
39
+ # config'den gelen değerleri normalize et
40
+ current_sort = initial_global_settings.get("sort_order", "alpha")
41
+ current_sort = sort_normalization.get(current_sort, current_sort) # Eğer map'te varsa değiştir, yoksa olduğu gibi bırak (zaten key ise)
42
+
43
+ current_anime = initial_global_settings.get("animetagger_model", "mobilenetv4")
44
+ current_anime = anime_normalization.get(current_anime, current_anime)
45
+
46
+ default_synonym_file = os.path.join(SYNONYM_RULES_KLASORU, "varsayilan_birlesimler.txt")
47
+ default_replacement_file = os.path.join(REPLACEMENT_RULES_KLASORU, "varsayilan_degisiklikler.txt")
48
+ default_addition_file = os.path.join(ADDITION_RULES_KLASORU, "varsayilan_eklemeler.txt")
49
+
50
+ with gr.TabItem(get_str("tab_settings_title")):
51
+ gr.Markdown(get_str("settings_header"))
52
+
53
+ with gr.Row():
54
+ save_global_btn = gr.Button(get_str("save_settings_btn"), variant="primary", size="lg")
55
+ global_settings_alert = gr.Textbox(label=get_str("status_label"), elem_id="alert-box", visible=False, interactive=False)
56
+
57
+ # 1. Çalıştırma Cihazı Ayarı
58
+ with gr.Accordion("🖥️ Çalıştırma Cihazı Ayarı", open=False):
59
+ set_device = gr.Radio(["Auto", "CPU", "CUDA"], value=initial_general_settings.get("device", "Auto"), label=get_str("device_label"), info=get_str("device_info"))
60
+
61
+ # 2. Dil ve Tema Ayarı
62
+ with gr.Accordion("🌐 Dil ve Tema Ayarı", open=False):
63
+ with gr.Row():
64
+ set_language = gr.Dropdown(
65
+ label=get_str("language_label"),
66
+ choices=[("Türkçe", "tr"), ("English", "en")],
67
+ value=initial_general_settings.get("language", "tr"),
68
+ interactive=True,
69
+ info=get_str("language_info")
70
+ )
71
+ set_theme = gr.Dropdown(
72
+ label=get_str("theme_label"),
73
+ choices=["Default", "Soft", "Glass", "Monochrome"],
74
+ value=initial_general_settings.get("theme", "Default"),
75
+ interactive=True,
76
+ info=get_str("theme_info")
77
+ )
78
+
79
+ # 3. Kural Yönetimi Ayarları
80
+ with gr.Accordion("📜 Kural Yönetimi Ayarları", open=False):
81
+ set_replacement_file = gr.Dropdown(label=get_str("replacement_file_label"), choices=get_replacement_files(), value=initial_global_settings.get("replacement_file") if initial_global_settings.get("replacement_file") in get_replacement_files() else (default_replacement_file if os.path.exists(default_replacement_file) else None), interactive=True)
82
+ set_synonym_file = gr.Dropdown(label=get_str("synonym_file_label"), choices=get_synonym_files(), value=initial_global_settings.get("synonym_file") if initial_global_settings.get("synonym_file") in get_synonym_files() else (default_synonym_file if os.path.exists(default_synonym_file) else None), interactive=True)
83
+ set_addition_file = gr.Dropdown(label=get_str("addition_file_label"), choices=get_addition_files(), value=initial_global_settings.get("addition_file") if initial_global_settings.get("addition_file") in get_addition_files() else (default_addition_file if os.path.exists(default_addition_file) else None), interactive=True)
84
+
85
+ # 4. Etiket Sıralama Ayarı
86
+ with gr.Accordion("🔡 Etiket Sıralama Ayarı", open=False):
87
+ set_sort_order = gr.Radio(
88
+ label=get_str("sort_order_label"),
89
+ choices=[
90
+ (get_str("sort_alpha"), "alpha"),
91
+ (get_str("sort_len"), "length"),
92
+ (get_str("sort_random"), "random"),
93
+ (get_str("sort_orig"), "original")
94
+ ],
95
+ value=current_sort
96
+ )
97
+
98
+ # 5. Rafine Bağlam Ağırlığı Ayarı
99
+ with gr.Accordion("⚖️ Rafine Bağlam Ağırlığı Ayarı", open=False):
100
+ set_context_weight = gr.Slider(minimum=0.0, maximum=1.0, value=initial_global_settings.get("context_weight", 0.0), step=0.05, label=get_str("context_weight_label"), info=get_str("context_weight_info"))
101
+
102
+ # 6. Kategorizasyon Ayarları
103
+ with gr.Accordion("📂 Kategorizasyon Ayarları", open=False):
104
+
105
+ with gr.Tabs():
106
+ with gr.TabItem(get_str("tab_single")):
107
+ set_tekil_enable_categorization = gr.Checkbox(label=get_str("enable_cat_single"), value=initial_global_settings.get("tekil_enable_categorization", False))
108
+ set_tekil_selected_categories = gr.Dropdown(label=get_str("cat_single_label"), choices=kategori_listesi(), multiselect=True, interactive=True, value=initial_global_settings.get("tekil_selected_categories"), visible=initial_global_settings.get("tekil_enable_categorization", False))
109
+
110
+ with gr.TabItem(get_str("tab_batch")):
111
+ set_toplu_enable_categorization = gr.Checkbox(label=get_str("enable_cat_batch"), value=initial_global_settings.get("toplu_enable_categorization", False))
112
+ set_toplu_selected_categories = gr.Dropdown(label=get_str("cat_batch_label"), choices=kategori_listesi(), multiselect=True, interactive=True, value=initial_global_settings.get("toplu_selected_categories"), visible=initial_global_settings.get("toplu_enable_categorization", False))
113
+
114
+ with gr.TabItem(get_str("tab_dual_left")):
115
+ set_dual1_enable_categorization = gr.Checkbox(label=get_str("enable_cat_dual1"), value=initial_global_settings.get("dual1_enable_categorization", False))
116
+ set_dual1_selected_categories = gr.Dropdown(label=get_str("cat_dual1_label"), choices=kategori_listesi(), multiselect=True, interactive=True, value=initial_global_settings.get("dual1_selected_categories"), visible=initial_global_settings.get("dual1_enable_categorization", False))
117
+
118
+ with gr.TabItem(get_str("tab_dual_right")):
119
+ set_dual2_enable_categorization = gr.Checkbox(label=get_str("enable_cat_dual2"), value=initial_global_settings.get("dual2_enable_categorization", False))
120
+ set_dual2_selected_categories = gr.Dropdown(label=get_str("cat_dual2_label"), choices=kategori_listesi(), multiselect=True, interactive=True, value=initial_global_settings.get("dual2_selected_categories"), visible=initial_global_settings.get("dual2_enable_categorization", False))
121
+
122
+ set_tekil_enable_categorization.change(lambda value: gr.update(visible=value), inputs=[set_tekil_enable_categorization], outputs=[set_tekil_selected_categories])
123
+ set_toplu_enable_categorization.change(lambda value: gr.update(visible=value), inputs=[set_toplu_enable_categorization], outputs=[set_toplu_selected_categories])
124
+ set_dual1_enable_categorization.change(lambda value: gr.update(visible=value), inputs=[set_dual1_enable_categorization], outputs=[set_dual1_selected_categories])
125
+ set_dual2_enable_categorization.change(lambda value: gr.update(visible=value), inputs=[set_dual2_enable_categorization], outputs=[set_dual2_selected_categories])
126
+
127
+ # 7. Tagger Ayarları
128
+ with gr.Accordion("🤖 Tagger Ayarları", open=False):
129
+ with gr.Tabs():
130
+ with gr.TabItem("Joint Tagger"):
131
+ with gr.Row():
132
+ set_use_joint = gr.Checkbox(label=get_str("use_joint"), value=initial_global_settings.get("use_joint", True))
133
+ set_joint_thresh = gr.Slider(minimum=0.01, maximum=1.0, value=initial_global_settings.get("joint_thresh", 0.25), step=0.01, label=get_str("joint_thresh"))
134
+
135
+ with gr.TabItem("CL Tagger"):
136
+ with gr.Row():
137
+ set_use_cl_tagger = gr.Checkbox(label=get_str("use_cl"), value=initial_global_settings.get("use_cl_tagger", True), interactive=(cl_tagger_instance is not None))
138
+ with gr.Row():
139
+ set_cl_gen_thresh = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=initial_global_settings.get("cl_gen_thresh", 0.55), label=get_str("cl_gen_thresh"))
140
+ set_cl_char_thresh = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=initial_global_settings.get("cl_char_thresh", 0.60), label=get_str("cl_char_thresh"))
141
+
142
+ with gr.TabItem("PixAI Tagger"):
143
+ with gr.Row():
144
+ set_use_pixai_tagger = gr.Checkbox(label=get_str("use_pixai"), value=initial_global_settings.get("use_pixai_tagger", False))
145
+ with gr.Row():
146
+ set_pixai_gen_thresh = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=initial_global_settings.get("pixai_general_thresh", 0.30), label=get_str("pixai_gen_thresh"))
147
+ set_pixai_char_thresh = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=initial_global_settings.get("pixai_char_thresh", 0.85), label=get_str("pixai_char_thresh"))
148
+
149
+ with gr.TabItem("Anime Tagger"):
150
+ with gr.Row():
151
+ set_use_animetagger = gr.Checkbox(label=get_str("use_anime"), value=initial_global_settings.get("use_animetagger", False))
152
+ set_animetagger_model = gr.Dropdown(
153
+ label=get_str("anime_model_label"),
154
+ choices=[
155
+ (get_str("model_mobilenet"), "mobilenetv4"),
156
+ (get_str("model_convnext"), "convnextv2"),
157
+ (get_str("model_caformer"), "caformer")
158
+ ],
159
+ value=current_anime,
160
+ interactive=True
161
+ )
162
+ set_animetagger_thresh = gr.Slider(minimum=0.01, maximum=1.0, value=initial_global_settings.get("animetagger_thresh", 0.35), step=0.01, label=get_str("anime_thresh"))
163
+
164
+ with gr.TabItem("Gemini Caption"):
165
+ gr.Markdown(get_str("gemini_desc"))
166
+ set_use_gemini = gr.Checkbox(label=get_str("use_gemini"), value=initial_global_settings.get("use_gemini", False))
167
+ set_gemini_api_key = gr.Textbox(label=get_str("gemini_api_key"), placeholder="AIzaSy...", type="password", value=initial_global_settings.get("gemini_api_key", ""))
168
+
169
+ set_gemini_model = gr.Dropdown(
170
+ label=get_str("gemini_model"),
171
+ choices=["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash"],
172
+ value=initial_global_settings.get("gemini_model", "gemini-2.5-flash"),
173
+ interactive=True,
174
+ info=get_str("gemini_model_info")
175
+ )
176
+
177
+ set_gemini_mode = gr.Radio(
178
+ label=get_str("gemini_mode_label"),
179
+ choices=["Vision", "Tags", "Vision + Tags"],
180
+ value=initial_global_settings.get("gemini_mode", "Vision"),
181
+ info=get_str("gemini_mode_info")
182
+ )
183
+
184
+ set_gemini_system_instruction = gr.Textbox(
185
+ label=get_str("gemini_system_instr"),
186
+ value=initial_global_settings.get("gemini_system_instruction", "You are a helpful assistant that generates detailed and accurate image captions for AI image generation datasets."),
187
+ lines=2,
188
+ placeholder="Örn: You are an expert photographer describing images...",
189
+ info=get_str("gemini_system_info")
190
+ )
191
+
192
+ gr.Markdown(get_str("gemini_prompt_settings"))
193
+ with gr.Tabs():
194
+ with gr.TabItem(get_str("prompt_vision")):
195
+ set_gemini_prompt_vision = gr.Textbox(
196
+ label=get_str("prompt_vision"),
197
+ value=initial_global_settings.get("gemini_prompt_vision", "Describe this image in a detailed, natural language caption for an AI image generator dataset. Focus on the main subject, clothing, pose, and background. Do not use bullet points."),
198
+ lines=3
199
+ )
200
+ with gr.TabItem(get_str("prompt_tags")):
201
+ set_gemini_prompt_tags = gr.Textbox(
202
+ label=get_str("prompt_tags"),
203
+ value=initial_global_settings.get("gemini_prompt_tags", "Create a fluent, natural language caption based on the provided tags. Focus on describing the scene vividly."),
204
+ lines=3
205
+ )
206
+ with gr.TabItem(get_str("prompt_hybrid")):
207
+ set_gemini_prompt_hybrid = gr.Textbox(
208
+ label=get_str("prompt_hybrid"),
209
+ value=initial_global_settings.get("gemini_prompt_hybrid", "Describe this image using the visual details and refine the description with the provided tags for accuracy. Focus on a cohesive narrative."),
210
+ lines=3
211
+ )
212
+
213
+ save_global_btn.click(fn=save_global_tagger_settings, inputs=[
214
+ set_device, set_language, set_theme,
215
+ set_use_joint, set_joint_thresh,
216
+ set_use_cl_tagger, set_cl_gen_thresh, set_cl_char_thresh,
217
+ set_use_pixai_tagger, set_pixai_gen_thresh, set_pixai_char_thresh,
218
+ set_use_animetagger, set_animetagger_model, set_animetagger_thresh,
219
+ # Gemini
220
+ set_use_gemini, set_gemini_api_key, set_gemini_mode, set_gemini_model,
221
+ set_gemini_prompt_vision, set_gemini_prompt_tags, set_gemini_prompt_hybrid,
222
+ set_gemini_system_instruction,
223
+ # Dosyalar
224
+ set_replacement_file, set_synonym_file, set_addition_file,
225
+ set_sort_order,
226
+ set_context_weight,
227
+ # Kategorizasyon
228
+ set_tekil_enable_categorization, set_tekil_selected_categories,
229
+ set_toplu_enable_categorization, set_toplu_selected_categories,
230
+ set_dual1_enable_categorization, set_dual1_selected_categories,
231
+ set_dual2_enable_categorization, set_dual2_selected_categories
232
+ ], outputs=global_settings_alert)
233
+
234
+ # Diğer modüllerde (tab_single, tab_batch, vb.) kullanılmak üzere bu ayarları döndürüyoruz
235
+ return {
236
+ "set_device": set_device,
237
+ "set_use_joint": set_use_joint,
238
+ "set_joint_thresh": set_joint_thresh,
239
+ "set_use_cl_tagger": set_use_cl_tagger,
240
+ "set_cl_gen_thresh": set_cl_gen_thresh,
241
+ "set_cl_char_thresh": set_cl_char_thresh,
242
+ "set_use_pixai_tagger": set_use_pixai_tagger,
243
+ "set_pixai_gen_thresh": set_pixai_gen_thresh,
244
+ "set_pixai_char_thresh": set_pixai_char_thresh,
245
+ "set_use_animetagger": set_use_animetagger,
246
+ "set_animetagger_model": set_animetagger_model,
247
+ "set_animetagger_thresh": set_animetagger_thresh,
248
+ "set_use_gemini": set_use_gemini,
249
+ "set_gemini_api_key": set_gemini_api_key,
250
+ "set_gemini_mode": set_gemini_mode,
251
+ "set_gemini_model": set_gemini_model,
252
+ "set_gemini_prompt_vision": set_gemini_prompt_vision,
253
+ "set_gemini_prompt_tags": set_gemini_prompt_tags,
254
+ "set_gemini_prompt_hybrid": set_gemini_prompt_hybrid,
255
+ "set_gemini_system_instruction": set_gemini_system_instruction,
256
+ "set_replacement_file": set_replacement_file,
257
+ "set_synonym_file": set_synonym_file,
258
+ "set_addition_file": set_addition_file,
259
+ "set_sort_order": set_sort_order,
260
+ "set_context_weight": set_context_weight,
261
+ "set_tekil_enable_categorization": set_tekil_enable_categorization,
262
+ "set_tekil_selected_categories": set_tekil_selected_categories,
263
+ "set_toplu_enable_categorization": set_toplu_enable_categorization,
264
+ "set_toplu_selected_categories": set_toplu_selected_categories,
265
+ "set_dual1_enable_categorization": set_dual1_enable_categorization,
266
+ "set_dual1_selected_categories": set_dual1_selected_categories,
267
+ "set_dual2_enable_categorization": set_dual2_enable_categorization,
268
+ "set_dual2_selected_categories": set_dual2_selected_categories
269
+ }
modules/ui/tab_single.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # modules/ui/tab_single.py
3
+
4
+ import gradio as gr
5
+ from modules.tagger import toplu_islem
6
+ from modules.managers.localization_manager import get_str
7
+ from modules.tools import append_text_to_wildcard_file
8
+
9
+
10
+ # JS Timer main.py üzerinden yönetiliyor
11
+
12
+
13
+ import base64
14
+ import io
15
+ from PIL import Image
16
+ from modules.shared_state import get_web_image, reset_image_event
17
+
18
+
19
+ def fetch_web_image(last_known_ts):
20
+ """Shared state'den son gelen web resmini kontrol eder. Yeni ise döndürür."""
21
+ data, timestamp = get_web_image()
22
+
23
+ # Henüz veri yoksa veya veri eskiyse (zaten işlendiyse) işlem yapma
24
+ if not data or timestamp == last_known_ts:
25
+ return gr.update(), gr.update(), last_known_ts
26
+
27
+ try:
28
+ if "," in data: data = data.split(",")[1]
29
+
30
+ image_data = base64.b64decode(data)
31
+ image = Image.open(io.BytesIO(image_data))
32
+
33
+ # Resmi başarılı şekilde aldık, yeni timestamp'i döndür
34
+ reset_image_event() # Flag'i sıfırla ki JS tekrar basmasın
35
+ return image, "✅ Web'den yeni resim alındı!", timestamp
36
+ except Exception as e:
37
+ return gr.update(), f"❌ Hata: {str(e)}", last_known_ts
38
+
39
+ def create_single_tab(settings_inputs):
40
+ """
41
+ Tekil etiketleme sekmesini oluşturur.
42
+ settings_inputs: create_settings_tab fonksiyonundan dönen sözlük.
43
+ """
44
+ with gr.TabItem(get_str("tab_single_title"), id="tab_single"):
45
+ with gr.Row():
46
+ tekil_process_button_top = gr.Button(get_str("btn_process_single"), variant="primary", size="lg", visible=False)
47
+
48
+ with gr.Row():
49
+ with gr.Column(scale=3):
50
+ # Manuel buton artık gereksiz olduğu için gizlendi (Otomatik çalışıyor)
51
+ btn_get_from_web = gr.Button("🌐 Web'den Yapıştır (Eklenti)", variant="secondary", size="sm", visible=False)
52
+ toplu_image_input = gr.Image(type="pil", label=get_str("label_image_upload"), width=512, height=512)
53
+
54
+ with gr.Column(scale=7):
55
+ toplu_alert = gr.Textbox(label=get_str("status_label"), elem_id="alert-box", visible=False, interactive=False)
56
+
57
+ with gr.Tab(get_str("tab_raw_tags")):
58
+ toplu_filtered_output = gr.Textbox(label=get_str("label_unique_tags"), interactive=True, lines=10, show_copy_button=True)
59
+ # --- BUTON EKLENDİ ---
60
+ btn_send_tekil = gr.Button(get_str("btn_send_to_cat"), variant="secondary")
61
+
62
+
63
+ with gr.Tab(get_str("tab_refined_tags")):
64
+ tekil_refined_output = gr.Textbox(label=get_str("label_refined_tags"), interactive=True, lines=10, show_copy_button=True)
65
+ with gr.Tab(get_str("tab_categorized_tags")):
66
+ tekil_categorized_output = gr.Textbox(label=get_str("label_categorized_tags"), interactive=False, lines=10, show_copy_button=True)
67
+ # --- WILDCARD BUTONLARI (TEKİL) ---
68
+ gr.Markdown(get_str("header_wildcard"))
69
+ with gr.Row():
70
+ btn_female_sfw_tekil = gr.Button("FEMALE SFWPACK", size="sm")
71
+ btn_female_nsfw_tekil = gr.Button("FEMALE NSFWPACK", size="sm")
72
+ btn_female_nude_tekil = gr.Button("FEMALE NUDEPACK", size="sm")
73
+ with gr.Row():
74
+ btn_futa_sfw_tekil = gr.Button("FUTANARI SFWPACK", size="sm")
75
+ btn_futa_nsfw_tekil = gr.Button("FUTANARI NSFWPACK", size="sm")
76
+ btn_futa_nude_tekil = gr.Button("FUTANARI NUDEPACK", size="sm")
77
+ with gr.Row():
78
+ btn_furry_tekil = gr.Button("FURRY-ANTHRO", size="sm")
79
+ btn_hair_tekil = gr.Button("HAIR-EYE", size="sm")
80
+
81
+ with gr.Tab(get_str("tab_gemini_caption")):
82
+ tekil_gemini_output = gr.Textbox(label=get_str("label_gemini_caption"), interactive=True, lines=5, show_copy_button=True)
83
+ with gr.Tab(get_str("tab_combined_caption")):
84
+ tekil_combined_cat_gem_output = gr.Textbox(label=get_str("label_combined_caption"), interactive=True, lines=10, show_copy_button=True)
85
+
86
+ # --- OTOMATİK KONTROL İÇİN STATE VE TIMER BUTTON ---
87
+ ts_state = gr.State(value=0) # Son işlenen resmin zaman damgası
88
+
89
+ # Gradio 3.x uyumlu Timer alternatifi: JS ile tıklanan buton
90
+ # Butonu visible=True yapıp style ile gizliyoruz ki JS bulabilsin
91
+ timer_btn = gr.Button("Timer", elem_id="timer_btn", visible=True)
92
+ # CSS ile gizle: #timer_btn { display: none !important; } (Bunu common.py'ye veya inline css'e ekleyeceğiz)
93
+
94
+
95
+ inputs_for_tekil = [
96
+ toplu_image_input,
97
+ settings_inputs["set_joint_thresh"], settings_inputs["set_use_joint"],
98
+ settings_inputs["set_cl_gen_thresh"], settings_inputs["set_cl_char_thresh"], settings_inputs["set_use_cl_tagger"],
99
+ settings_inputs["set_pixai_gen_thresh"], settings_inputs["set_pixai_char_thresh"], settings_inputs["set_use_pixai_tagger"],
100
+ settings_inputs["set_animetagger_model"], settings_inputs["set_animetagger_thresh"], settings_inputs["set_use_animetagger"],
101
+ # Gemini
102
+ settings_inputs["set_use_gemini"], settings_inputs["set_gemini_api_key"], settings_inputs["set_gemini_mode"], settings_inputs["set_gemini_model"],
103
+ settings_inputs["set_gemini_prompt_vision"], settings_inputs["set_gemini_prompt_tags"], settings_inputs["set_gemini_prompt_hybrid"],
104
+ settings_inputs["set_gemini_system_instruction"],
105
+ # Dosyalar
106
+ settings_inputs["set_replacement_file"], settings_inputs["set_synonym_file"], settings_inputs["set_addition_file"],
107
+ settings_inputs["set_tekil_enable_categorization"], settings_inputs["set_tekil_selected_categories"],
108
+ settings_inputs["set_sort_order"], settings_inputs["set_device"], settings_inputs["set_context_weight"]
109
+ ]
110
+ outputs_for_tekil = [toplu_alert, toplu_filtered_output, tekil_refined_output, tekil_categorized_output, tekil_gemini_output, tekil_combined_cat_gem_output]
111
+ # Button click event'i (Artık gizli ama kod yapısı bozulmasın diye tutuyoruz)
112
+ tekil_process_button_top.click(fn=toplu_islem, inputs=inputs_for_tekil, outputs=outputs_for_tekil)
113
+
114
+ # --- OTOMATİK TETİKLEME (RESİM DEĞİŞİNCE) ---
115
+ toplu_image_input.change(fn=toplu_islem, inputs=inputs_for_tekil, outputs=outputs_for_tekil)
116
+
117
+
118
+ # Web'den resim alma eventi (JS Tetiklemeli)
119
+ timer_btn.click(
120
+ fn=fetch_web_image,
121
+ inputs=[ts_state],
122
+ outputs=[toplu_image_input, toplu_alert, ts_state],
123
+ show_progress="hidden" # Yükleniyor animasyonu gizlendi
124
+ )
125
+
126
+ # MANUEL BUTON
127
+ btn_get_from_web.click(
128
+ fn=fetch_web_image,
129
+ inputs=[ts_state],
130
+ outputs=[toplu_image_input, toplu_alert, ts_state]
131
+ )
132
+
133
+ # WILDCARD EVENTLERI (TEKİL)
134
+ btn_female_sfw_tekil.click(fn=lambda x: append_text_to_wildcard_file(x, "FEMALE SFWPACK"), inputs=[tekil_categorized_output], outputs=[toplu_alert])
135
+ btn_female_nsfw_tekil.click(fn=lambda x: append_text_to_wildcard_file(x, "FEMALE NSFWPACK"), inputs=[tekil_categorized_output], outputs=[toplu_alert])
136
+ btn_female_nude_tekil.click(fn=lambda x: append_text_to_wildcard_file(x, "FEMALE NUDEPACK"), inputs=[tekil_categorized_output], outputs=[toplu_alert])
137
+
138
+ btn_futa_sfw_tekil.click(fn=lambda x: append_text_to_wildcard_file(x, "FUTANARI SFWPACK"), inputs=[tekil_categorized_output], outputs=[toplu_alert])
139
+ btn_futa_nsfw_tekil.click(fn=lambda x: append_text_to_wildcard_file(x, "FUTANARI NSFWPACK"), inputs=[tekil_categorized_output], outputs=[toplu_alert])
140
+ btn_futa_nude_tekil.click(fn=lambda x: append_text_to_wildcard_file(x, "FUTANARI NUDEPACK"), inputs=[tekil_categorized_output], outputs=[toplu_alert])
141
+
142
+ btn_furry_tekil.click(fn=lambda x: append_text_to_wildcard_file(x, "FURRY-ANTHRO"), inputs=[tekil_categorized_output], outputs=[toplu_alert])
143
+ btn_hair_tekil.click(fn=lambda x: append_text_to_wildcard_file(x, "HAIR-EYE"), inputs=[tekil_categorized_output], outputs=[toplu_alert])
144
+
145
+ return {
146
+ "btn_send_tekil": btn_send_tekil,
147
+ "toplu_filtered_output": toplu_filtered_output
148
+ }
modules/ui/tab_tools.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # modules/ui/tab_tools.py
3
+
4
+ import os
5
+ import gradio as gr
6
+ from modules.prompt_generator import create_prompt_generator_ui
7
+ from modules.video_creator import create_video_ui
8
+
9
+ # New Component Imports
10
+ from modules.ui.tools.category_ui import create_category_ui
11
+ from modules.ui.tools.rule_ui import create_rule_ui
12
+ from modules.ui.tools.text_ui import create_text_ui
13
+ from modules.ui.tools.image_ui import create_image_ui
14
+ from modules.ui.tools.art_ui import create_art_ui
15
+ from modules.localization.languages import get_str
16
+
17
+ REPLACEMENT_RULES_KLASORU = "data/rules/replacement_rules"
18
+ SYNONYM_RULES_KLASORU = "data/rules/synonym_rules"
19
+ ADDITION_RULES_KLASORU = "data/rules/addition_rules"
20
+
21
+ def create_tools_tab(app_config):
22
+ initial_image_tools_settings = app_config.get("image_tools_settings", {})
23
+ initial_global_settings = app_config.get("global_tagger_settings", {})
24
+
25
+ default_synonym_file = os.path.join(SYNONYM_RULES_KLASORU, "varsayilan_birlesimler.txt")
26
+ default_replacement_file = os.path.join(REPLACEMENT_RULES_KLASORU, "varsayilan_degisiklikler.txt")
27
+ default_addition_file = os.path.join(ADDITION_RULES_KLASORU, "varsayilan_eklemeler.txt")
28
+
29
+ # --- 1. KATEGORİ ARAÇLARI ---
30
+ with gr.TabItem(get_str("tab_category_tools"), id="tab_kategori_araclari"):
31
+ with gr.Tabs() as tabs_kategori_ic:
32
+ # Common input is now created inside create_category_ui
33
+ etiket_input_common = create_category_ui(tabs_kategori_ic)
34
+
35
+ # --- 2. RESİM ARAÇLARI ---
36
+ with gr.TabItem(get_str("tab_image_tools")):
37
+ create_image_ui(initial_image_tools_settings)
38
+
39
+ # --- 3. METİN ARAÇLARI ---
40
+ with gr.TabItem(get_str("tab_text_tools")):
41
+ create_text_ui()
42
+
43
+ # --- 4. PROMPT OLUŞTURUCU ---
44
+ with gr.TabItem(get_str("tab_prompt_gen")):
45
+ create_prompt_generator_ui()
46
+
47
+ # --- 5. VIDEO CREATOR ---
48
+ with gr.TabItem(get_str("tab_video_creator")):
49
+ create_video_ui()
50
+
51
+ # --- 6. SANAT STÜDYOSU ---
52
+ with gr.TabItem(get_str("tab_art_studio")):
53
+ create_art_ui()
54
+
55
+ # --- 7. KURAL YÖNETİMİ ---
56
+ with gr.TabItem(get_str("tab_rule_management")):
57
+ replacement_file_content, synonym_file_content, addition_file_content = create_rule_ui(
58
+ initial_global_settings,
59
+ default_replacement_file,
60
+ default_synonym_file,
61
+ default_addition_file
62
+ )
63
+
64
+ return {
65
+ "etiket_input_common": etiket_input_common,
66
+ "replacement_file_content": replacement_file_content,
67
+ "synonym_file_content": synonym_file_content,
68
+ "addition_file_content": addition_file_content
69
+ }
modules/ui/tools/art_ui.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from modules.art_tools import create_grid_collage
3
+ from modules.localization.languages import get_str
4
+ from modules.config_manager import save_art_tools_settings, load_config
5
+
6
+ def create_art_ui():
7
+ # Ayarları Yükle
8
+ config = load_config()
9
+ art_settings = config.get("art_tools_settings", {})
10
+
11
+ with gr.Tabs():
12
+ with gr.TabItem(get_str("art_tab_grid")):
13
+ with gr.Row():
14
+ with gr.Column():
15
+ grid_files = gr.File(label=get_str("art_file_input"), file_count="multiple", file_types=["image"])
16
+ with gr.Accordion(get_str("art_grid_settings"), open=True):
17
+ grid_cols = gr.Slider(minimum=1, maximum=5, value=art_settings.get("grid_cols", 3), step=1, label=get_str("art_cols"))
18
+ grid_bg_color = gr.ColorPicker(value=art_settings.get("grid_bg_color", "#000000"), label="Arka Plan Rengi")
19
+
20
+ with gr.Accordion("Banner & Yazı Ayarları", open=True):
21
+ with gr.Row():
22
+ grid_title_text = gr.Textbox(value=art_settings.get("grid_title_text", "ADOPTABLE SALE!"), label="Banner Başlığı")
23
+ with gr.Row():
24
+ grid_banner_color = gr.ColorPicker(value=art_settings.get("grid_banner_color", "#000000"), label="Banner Arka Planı")
25
+ grid_title_color = gr.ColorPicker(value=art_settings.get("grid_title_color", "#FFD700"), label="Başlık Rengi (Gold)")
26
+
27
+ with gr.Accordion(get_str("art_label_settings"), open=True):
28
+ grid_add_labels = gr.Checkbox(label=get_str("art_add_labels_check"), value=art_settings.get("grid_add_labels", True))
29
+ grid_label_type = gr.Dropdown(["Numara (#1, #2...)", "Sadece Numaralar"], value=art_settings.get("grid_label_type", "Numara (#1, #2...)"), label=get_str("art_label_type"))
30
+ grid_start_num = gr.Number(value=art_settings.get("grid_start_num", 1), label=get_str("art_start_num"), precision=0)
31
+
32
+ with gr.Row():
33
+ grid_btn = gr.Button(get_str("art_create_btn"), variant="primary", scale=2)
34
+ save_art_btn = gr.Button("💾 Ayarları Kaydet", variant="secondary", scale=1)
35
+
36
+ with gr.Column():
37
+ grid_output = gr.Image(type="pil", label=get_str("art_output"))
38
+ grid_status = gr.Textbox(label=get_str("status_label"), interactive=False)
39
+
40
+ grid_btn.click(
41
+ fn=create_grid_collage,
42
+ inputs=[grid_files, grid_cols, grid_add_labels, grid_label_type, grid_start_num, grid_bg_color, grid_title_text, grid_banner_color, grid_title_color],
43
+ outputs=[grid_output, grid_status]
44
+ )
45
+
46
+ save_art_btn.click(
47
+ fn=save_art_tools_settings,
48
+ inputs=[grid_cols, grid_bg_color, grid_title_text, grid_banner_color, grid_title_color, grid_add_labels, grid_label_type, grid_start_num],
49
+ outputs=[grid_status]
50
+ )
modules/ui/tools/category_ui.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from modules.managers.category_manager import (
3
+ kategori_listesi,
4
+ filter_and_display_main_categorized_tags,
5
+ kategorileri_grid_html,
6
+ grid_guncelle,
7
+ etiket_ekle,
8
+ etiket_sil
9
+ )
10
+ from modules.localization.languages import get_str
11
+
12
+ def create_category_ui(tabs_kategori_ic):
13
+
14
+ # --- 1. SEKME: CANLI TEST (LIVE TEST) ---
15
+ with gr.TabItem(get_str("cat_redesign_live_tab"), id="tab_cat_live"):
16
+
17
+ # Giriş Alanı (Buraya taşındı, sadece bu sekmede görünsün diye)
18
+ etiket_input_common = gr.Textbox(lines=5, label=get_str("label_tags_input"), interactive=True, placeholder=get_str("placeholder_tags_input"))
19
+
20
+ # Butonlar (Yan Yana)
21
+ with gr.Row():
22
+ main_cat_button = gr.Button(get_str("cat_live_main_btn"), variant="primary", scale=1) # Ana Kategoriye Göre
23
+
24
+ # Transfer Butonu (Sonuçların üstünde)
25
+ btn_transfer_unmatched = gr.Button(get_str("cat_live_unmatched_btn"), variant="stop", visible=False)
26
+ hidden_unmatched_tags = gr.Textbox(visible=False)
27
+
28
+ gr.Markdown("<h3 style='text-align: center; margin-bottom: 20px;'>🔍 Sonuçlar</h3>")
29
+ main_html_output = gr.HTML(label=get_str("cat_live_main_output"), min_height=200)
30
+
31
+ # Event Bağlantıları (Canlı Test)
32
+ def kategorize_ve_buton_kontrol(tags):
33
+ html, unmatched = filter_and_display_main_categorized_tags(tags, None)
34
+ return html, unmatched, gr.update(visible=bool(unmatched)), gr.update(visible=True)
35
+
36
+ main_cat_button.click(
37
+ fn=kategorize_ve_buton_kontrol,
38
+ inputs=[etiket_input_common],
39
+ outputs=[main_html_output, hidden_unmatched_tags, btn_transfer_unmatched, main_html_output]
40
+ )
41
+
42
+ def transfer_et_ve_gec(unmatched_text):
43
+ # Ana Yönetim sekmesine geçiş yap ve input kutusunu doldur
44
+ return (
45
+ unmatched_text,
46
+ gr.Tabs(selected="tab_cat_main"),
47
+ )
48
+
49
+ # --- 2. SEKME: ANA KATEGORİ YÖNETİMİ ---
50
+ with gr.TabItem(get_str("cat_redesign_main_tab"), id="tab_cat_main"):
51
+
52
+ # Üst Panel: Yönetim
53
+ with gr.Group():
54
+ with gr.Row(variant="panel"):
55
+ with gr.Column(scale=1):
56
+ kategori_dd = gr.Dropdown(choices=kategori_listesi(), label="Kategori Seç / Select Category", interactive=True)
57
+ with gr.Column(scale=3):
58
+ with gr.Row():
59
+ etik_giris = gr.Textbox(label="Etiket Ekle/Sil (Virgül ile ayırın)", interactive=True, scale=3)
60
+ with gr.Column(scale=1, min_width=150):
61
+ btn_etik_ekle = gr.Button(get_str("btn_add"), variant="primary", scale=1)
62
+ btn_etik_sil = gr.Button(get_str("btn_delete"), variant="stop", scale=1)
63
+
64
+ alert = gr.Textbox(label=get_str("status_label"), elem_id="alert-box", visible=False)
65
+
66
+ # Alt Panel: Grid Görünümü
67
+ gr.Markdown("<h3 style='text-align: center; margin-bottom: 20px;'>📂 Tüm Kategoriler</h3>")
68
+
69
+ grid_out = gr.HTML(value=kategorileri_grid_html())
70
+
71
+ # Event Bağlantıları (Ana Yönetim)
72
+ btn_etik_ekle.click(fn=etiket_ekle, inputs=[kategori_dd, etik_giris], outputs=alert).then(grid_guncelle, outputs=grid_out)
73
+ btn_etik_sil.click(fn=etiket_sil, inputs=[kategori_dd, etik_giris], outputs=alert).then(grid_guncelle, outputs=grid_out)
74
+
75
+ # --- Geç Bağlantılar (Cross-Tab Events) ---
76
+ btn_transfer_unmatched.click(
77
+ fn=transfer_et_ve_gec,
78
+ inputs=[hidden_unmatched_tags],
79
+ outputs=[etik_giris, tabs_kategori_ic] # tabs_kategori_ic ana tab objesi
80
+ )
81
+
82
+ return etiket_input_common
modules/ui/tools/image_ui.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ import tkinter as tk
4
+ from tkinter import filedialog
5
+ from modules.config_manager import (
6
+ save_image_tools_settings,
7
+ load_renaming_templates,
8
+ add_renaming_template,
9
+ delete_renaming_template
10
+ )
11
+ from modules.localization.languages import get_str
12
+ from modules.tools import (
13
+ apply_custom_renaming,
14
+ process_resolution_change,
15
+ process_text_watermark,
16
+ process_format_change,
17
+ process_brightness_contrast,
18
+ process_denoise_sharpen
19
+ )
20
+
21
+ def create_image_ui(initial_image_tools_settings):
22
+ initial_folder_paths_str = "\n".join(initial_image_tools_settings.get("folder_paths", []))
23
+
24
+ initial_watermark_text = initial_image_tools_settings.get("watermark_text", get_str("img_tools_watermark_default"))
25
+ initial_watermark_opacity = initial_image_tools_settings.get("watermark_opacity", 0.35)
26
+ initial_watermark_font_ratio = initial_image_tools_settings.get("watermark_font_ratio", 0.05)
27
+ initial_watermark_angle = initial_image_tools_settings.get("watermark_angle", -45)
28
+
29
+ initial_brightness_level = initial_image_tools_settings.get("brightness_level", 0)
30
+ initial_contrast_level = initial_image_tools_settings.get("contrast_level", 0)
31
+ initial_denoise_level = initial_image_tools_settings.get("denoise_level", 0)
32
+ initial_sharpen_amount = initial_image_tools_settings.get("sharpen_amount", 0.0)
33
+
34
+ current_templates = load_renaming_templates()
35
+ state_templates = gr.State(current_templates)
36
+
37
+ gr.Markdown(get_str("img_tools_header"))
38
+
39
+ def select_folder_dialog(current_paths):
40
+ try:
41
+ root = tk.Tk()
42
+ root.withdraw()
43
+ root.attributes('-topmost', True)
44
+ folder_path = filedialog.askdirectory(title=get_str("img_tools_select_folder_btn")) # Using button text as title, simpler
45
+ root.destroy()
46
+ if folder_path:
47
+ existing_paths = current_paths.strip() if current_paths else ""
48
+ if existing_paths: return f"{existing_paths}\n{folder_path}"
49
+ else: return folder_path
50
+ return current_paths
51
+ except Exception as e:
52
+ print(f"Klasör seçme hatası: {e}")
53
+ return current_paths
54
+
55
+ with gr.Row():
56
+ with gr.Column(scale=1):
57
+ folder_path_input = gr.Textbox(label=get_str("img_tools_paths_input"), value=initial_folder_paths_str, lines=5)
58
+ with gr.Row():
59
+ select_folder_btn = gr.Button(get_str("img_tools_select_folder_btn"), variant="secondary", size="sm")
60
+ clear_paths_btn = gr.Button(get_str("img_tools_clear_btn"), variant="stop", size="sm")
61
+ save_paths_button = gr.Button(get_str("img_tools_save_settings_btn"), variant="secondary")
62
+ image_tools_alert = gr.Textbox(label=get_str("status_label"), elem_id="renaming-alert-box", interactive=False, lines=5)
63
+
64
+ select_folder_btn.click(fn=select_folder_dialog, inputs=[folder_path_input], outputs=[folder_path_input])
65
+ clear_paths_btn.click(fn=lambda: "", inputs=[], outputs=[folder_path_input])
66
+
67
+ with gr.Column(scale=3):
68
+ with gr.Accordion(get_str("img_tools_res_accordion"), open=False):
69
+ scale_factor_dropdown = gr.Dropdown(label=get_str("img_tools_scale_label"), choices=[1.001, 1.01, 1.1, 1.25, 1.5, 2.0, 4.0], value=1.0, interactive=True)
70
+ process_resolution_button = gr.Button(get_str("btn_apply"), variant="primary")
71
+
72
+ with gr.Accordion(get_str("img_tools_rename_accordion"), open=True):
73
+ gr.Markdown(get_str("img_tools_rename_desc"))
74
+ with gr.Row():
75
+ template_dropdown = gr.Dropdown(label=get_str("img_tools_template_select"), choices=current_templates, value=current_templates[0] if current_templates else "", interactive=True, allow_custom_value=True)
76
+ btn_del_template = gr.Button(get_str("img_tools_delete_template_btn"), variant="stop")
77
+
78
+ with gr.Row():
79
+ new_template_input = gr.Textbox(label=get_str("img_tools_new_template_input"), placeholder=get_str("img_tools_new_template_placeholder"))
80
+ btn_add_template = gr.Button(get_str("img_tools_add_template_btn"), variant="secondary")
81
+
82
+ with gr.Row():
83
+ start_num_input = gr.Number(label=get_str("img_tools_start_num"), value=1, precision=0)
84
+ digit_count_slider = gr.Slider(label=get_str("img_tools_digit_count"), value=3, minimum=1, maximum=10, step=1)
85
+
86
+ apply_renaming_btn = gr.Button(get_str("img_tools_rename_btn"), variant="primary")
87
+
88
+ btn_add_template.click(fn=add_renaming_template, inputs=[new_template_input, state_templates], outputs=[template_dropdown, state_templates])
89
+ btn_del_template.click(fn=delete_renaming_template, inputs=[template_dropdown, state_templates], outputs=[template_dropdown, state_templates])
90
+ apply_renaming_btn.click(fn=apply_custom_renaming, inputs=[folder_path_input, template_dropdown, start_num_input, digit_count_slider], outputs=[image_tools_alert, image_tools_alert])
91
+
92
+ with gr.Accordion(get_str("img_tools_watermark_accordion"), open=False):
93
+ watermark_text_input = gr.Textbox(label=get_str("img_tools_watermark_text"), value=initial_watermark_text)
94
+ watermark_opacity_slider = gr.Slider(minimum=0.05, maximum=1.0, value=initial_watermark_opacity, label=get_str("img_tools_watermark_opacity"))
95
+ watermark_font_ratio = gr.Slider(minimum=0.01, maximum=0.2, value=initial_watermark_font_ratio, label=get_str("img_tools_watermark_size"))
96
+ watermark_angle_slider = gr.Slider(minimum=-90, maximum=90, value=initial_watermark_angle, label=get_str("img_tools_watermark_angle"))
97
+ process_watermark_button = gr.Button(get_str("btn_add"), variant="primary")
98
+
99
+ with gr.Accordion(get_str("img_tools_format_accordion"), open=False):
100
+ format_dropdown = gr.Dropdown(label=get_str("img_tools_format_label"), choices=[".png", ".jpeg", ".webp"], value=".png")
101
+ process_format_button = gr.Button(get_str("btn_convert"), variant="primary")
102
+
103
+ with gr.Accordion(get_str("img_tools_bc_accordion"), open=False):
104
+ brightness_level = gr.Slider(minimum=-100, maximum=100, step=5, value=initial_brightness_level, label=get_str("img_tools_brightness"))
105
+ contrast_level = gr.Slider(minimum=-100, maximum=100, step=5, value=initial_contrast_level, label=get_str("img_tools_contrast"))
106
+ process_brightness_contrast_button = gr.Button(get_str("btn_apply"), variant="primary")
107
+
108
+ with gr.Accordion(get_str("img_tools_ds_accordion"), open=False):
109
+ denoise_level = gr.Slider(minimum=0, maximum=5, step=1, value=initial_denoise_level, label=get_str("img_tools_denoise"))
110
+ sharpen_amount = gr.Slider(minimum=0.0, maximum=3.0, step=0.1, value=initial_sharpen_amount, label=get_str("img_tools_sharpen"))
111
+ process_denoise_sharpen_button = gr.Button(get_str("btn_apply"), variant="primary")
112
+
113
+ process_resolution_button.click(fn=process_resolution_change, inputs=[folder_path_input, scale_factor_dropdown], outputs=[image_tools_alert])
114
+ process_watermark_button.click(fn=process_text_watermark, inputs=[folder_path_input, watermark_text_input, watermark_opacity_slider, watermark_font_ratio, watermark_angle_slider], outputs=[image_tools_alert])
115
+ process_format_button.click(fn=process_format_change, inputs=[folder_path_input, format_dropdown], outputs=[image_tools_alert])
116
+ process_brightness_contrast_button.click(fn=process_brightness_contrast, inputs=[folder_path_input, brightness_level, contrast_level], outputs=[image_tools_alert])
117
+ process_denoise_sharpen_button.click(fn=process_denoise_sharpen, inputs=[folder_path_input, denoise_level, sharpen_amount], outputs=[image_tools_alert])
118
+
119
+ save_paths_button.click(fn=save_image_tools_settings, inputs=[
120
+ folder_path_input, scale_factor_dropdown,
121
+ watermark_text_input, watermark_opacity_slider, watermark_font_ratio, watermark_angle_slider,
122
+ brightness_level, contrast_level, denoise_level, sharpen_amount
123
+ ], outputs=[image_tools_alert])
modules/ui/tools/rule_ui.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import gradio as gr
4
+ from modules.managers.rule_manager import (
5
+ get_replacement_files,
6
+ get_synonym_files,
7
+ get_addition_files,
8
+ create_replacement_file,
9
+ read_replacement_file_content,
10
+ save_replacement_file_content,
11
+ append_replacement_rule_to_file,
12
+ create_synonym_file,
13
+ read_synonym_file_content,
14
+ save_synonym_file_content,
15
+ append_synonym_rule_to_file,
16
+ create_addition_file,
17
+ read_addition_file_content,
18
+ save_addition_file_content
19
+ )
20
+
21
+ from modules.localization.languages import get_str
22
+
23
+ def create_rule_ui(initial_global_settings, default_replacement_file, default_synonym_file, default_addition_file):
24
+
25
+ gr.Markdown(get_str("rule_tools_intro"))
26
+
27
+ replacement_file_content = gr.Textbox(label=get_str("rule_file_content_manual"), visible=False)
28
+ synonym_file_content = gr.Textbox(label=get_str("rule_file_content_manual"), visible=False)
29
+ addition_file_content = gr.Textbox(label=get_str("rule_file_content_manual"), visible=False)
30
+
31
+ # --- 1. DEĞİŞTİRME KURALLARI ---
32
+ with gr.Accordion(get_str("rule_rep_accordion"), open=True):
33
+ gr.Markdown(get_str("rule_rep_desc"))
34
+ with gr.Row():
35
+ with gr.Column(scale=1):
36
+ # initial value logic is complex, simplify
37
+ rep_val = initial_global_settings.get("replacement_file")
38
+ if rep_val not in get_replacement_files():
39
+ rep_val = default_replacement_file if os.path.exists(default_replacement_file) else None
40
+
41
+ replacement_file_selector = gr.Dropdown(label=get_str("rule_active_file"), choices=get_replacement_files(), value=rep_val, interactive=True)
42
+ replacement_file_alert = gr.Textbox(label=get_str("status_label"), elem_id="alert-box", visible=False)
43
+
44
+ with gr.Column(scale=1):
45
+ new_replacement_file_name = gr.Textbox(label=get_str("rule_new_file_name"), placeholder="ozel_degisimler")
46
+ create_replacement_btn = gr.Button(get_str("rule_create_file_btn"))
47
+
48
+ with gr.Tabs():
49
+ with gr.TabItem(get_str("rule_tab_quick_add")):
50
+ with gr.Row():
51
+ txt_rep_old = gr.Textbox(label=get_str("rule_rep_old"), placeholder="örn: blue_hair")
52
+ txt_rep_new = gr.Textbox(label=get_str("rule_rep_new"), placeholder="örn: cyan hair")
53
+ btn_add_rep = gr.Button(get_str("rule_add_to_file_btn"), variant="primary")
54
+
55
+ with gr.TabItem(get_str("rule_tab_edit_file")):
56
+ replacement_file_content = gr.Textbox(label=get_str("rule_file_content_manual"), interactive=True, lines=10)
57
+ save_replacement_rules_btn = gr.Button(get_str("rule_save_all_btn"), variant="secondary")
58
+
59
+ # --- 2. BİRLEŞTİRME KURALLARI ---
60
+ with gr.Accordion(get_str("rule_syn_accordion"), open=False):
61
+ gr.Markdown(get_str("rule_syn_intro"))
62
+ with gr.Row():
63
+ with gr.Column(scale=1):
64
+ syn_val = initial_global_settings.get("synonym_file")
65
+ if syn_val not in get_synonym_files():
66
+ syn_val = default_synonym_file if os.path.exists(default_synonym_file) else None
67
+
68
+ synonym_file_selector = gr.Dropdown(label=get_str("rule_active_file"), choices=get_synonym_files(), value=syn_val, interactive=True)
69
+ synonym_file_alert = gr.Textbox(label=get_str("status_label"), elem_id="alert-box", visible=False)
70
+ with gr.Column(scale=1):
71
+ new_synonym_file_name = gr.Textbox(label=get_str("rule_new_file_name"), placeholder="ozel_birlesimler")
72
+ create_synonym_btn = gr.Button(get_str("rule_create_file_btn"))
73
+
74
+ with gr.Tabs():
75
+ with gr.TabItem(get_str("rule_tab_quick_add")):
76
+ with gr.Row():
77
+ txt_syn_main = gr.Textbox(label=get_str("rule_syn_main"), placeholder="örn: underwear")
78
+ txt_syn_remove = gr.Textbox(label=get_str("rule_syn_remove"), placeholder="örn: panties, briefs")
79
+ btn_add_syn = gr.Button(get_str("rule_add_to_file_btn"), variant="primary")
80
+
81
+ with gr.TabItem(get_str("rule_tab_edit_file")):
82
+ synonym_file_content = gr.Textbox(label=get_str("rule_file_content_manual"), interactive=True, lines=10)
83
+ save_synonym_rules_btn = gr.Button(get_str("rule_save_all_btn"), variant="secondary")
84
+
85
+ # --- 3. EKLEME KURALLARI ---
86
+ with gr.Accordion(get_str("rule_add_accordion"), open=False):
87
+ gr.Markdown(get_str("rule_add_intro"))
88
+ with gr.Row():
89
+ with gr.Column(scale=1):
90
+ add_val = initial_global_settings.get("addition_file")
91
+ if add_val not in get_addition_files():
92
+ add_val = default_addition_file if os.path.exists(default_addition_file) else None
93
+
94
+ addition_file_selector = gr.Dropdown(label=get_str("rule_active_file"), choices=get_addition_files(), value=add_val, interactive=True)
95
+ addition_file_alert = gr.Textbox(label=get_str("status_label"), elem_id="alert-box", visible=False)
96
+ with gr.Column(scale=1):
97
+ new_addition_file_name = gr.Textbox(label=get_str("rule_new_file_name"), placeholder="imza_etiketlerim")
98
+ create_addition_btn = gr.Button(get_str("rule_create_file_btn"))
99
+
100
+ addition_file_content = gr.Textbox(label=get_str("rule_add_content_input"), interactive=True, lines=10, placeholder="best quality, masterpiece\nsigned by artist")
101
+ save_addition_rules_btn = gr.Button(get_str("rule_add_save_btn"), variant="primary")
102
+
103
+
104
+ # --- EVENT HANDLERS ---
105
+ def update_dropdowns(val1, val2, val3):
106
+ files_rep = get_replacement_files()
107
+ files_syn = get_synonym_files()
108
+ files_add = get_addition_files()
109
+ return (
110
+ gr.update(choices=files_rep, value=val1 if val1 in files_rep else None),
111
+ gr.update(choices=files_syn, value=val2 if val2 in files_syn else None),
112
+ gr.update(choices=files_add, value=val3 if val3 in files_add else None)
113
+ )
114
+
115
+ # Replacement Events
116
+ create_replacement_btn.click(fn=create_replacement_file, inputs=[new_replacement_file_name], outputs=[replacement_file_alert]).then(fn=lambda: update_dropdowns(None, None, None)[0], outputs=[replacement_file_selector])
117
+ replacement_file_selector.change(fn=read_replacement_file_content, inputs=[replacement_file_selector], outputs=[replacement_file_content])
118
+ save_replacement_rules_btn.click(fn=save_replacement_file_content, inputs=[replacement_file_selector, replacement_file_content], outputs=[replacement_file_alert])
119
+ btn_add_rep.click(fn=append_replacement_rule_to_file, inputs=[replacement_file_selector, txt_rep_old, txt_rep_new], outputs=[replacement_file_alert]).then(fn=read_replacement_file_content, inputs=[replacement_file_selector], outputs=[replacement_file_content])
120
+
121
+ # Synonym Events
122
+ create_synonym_btn.click(fn=create_synonym_file, inputs=[new_synonym_file_name], outputs=[synonym_file_alert]).then(fn=lambda: update_dropdowns(None, None, None)[1], outputs=[synonym_file_selector])
123
+ synonym_file_selector.change(fn=read_synonym_file_content, inputs=[synonym_file_selector], outputs=[synonym_file_content])
124
+ save_synonym_rules_btn.click(fn=save_synonym_file_content, inputs=[synonym_file_selector, synonym_file_content], outputs=[synonym_file_alert])
125
+ btn_add_syn.click(fn=append_synonym_rule_to_file, inputs=[synonym_file_selector, txt_syn_main, txt_syn_remove], outputs=[synonym_file_alert]).then(fn=read_synonym_file_content, inputs=[synonym_file_selector], outputs=[synonym_file_content])
126
+
127
+ # Addition Events
128
+ create_addition_btn.click(fn=create_addition_file, inputs=[new_addition_file_name], outputs=[addition_file_alert]).then(fn=lambda: update_dropdowns(None, None, None)[2], outputs=[addition_file_selector])
129
+ addition_file_selector.change(fn=read_addition_file_content, inputs=[addition_file_selector], outputs=[addition_file_content])
130
+ save_addition_rules_btn.click(fn=save_addition_file_content, inputs=[addition_file_selector, addition_file_content], outputs=[addition_file_alert])
131
+
132
+ return replacement_file_content, synonym_file_content, addition_file_content
modules/ui/tools/text_ui.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import gradio as gr
3
+ from modules.localization.languages import get_str
4
+ from modules.tools import (
5
+ clean_and_uniquify_text_simple,
6
+ convert_lines_to_comma,
7
+ convert_comma_to_lines,
8
+ select_and_copy_random_lines
9
+ )
10
+
11
+ def create_text_ui():
12
+ with gr.Row():
13
+ with gr.Column():
14
+ gr.Markdown(get_str("txt_tools_header"))
15
+ text_input_raw = gr.Textbox(label=get_str("txt_tools_input"), lines=5, placeholder="örnek: \nred_hair\nlong__hair\nblue_eyes")
16
+ text_cleaner_alert = gr.Textbox(label=get_str("status_label"), elem_id="alert-box", visible=False)
17
+
18
+ with gr.Tabs():
19
+ with gr.Tab(get_str("txt_tools_tab_clean")):
20
+ text_cleaner_process_button = gr.Button(get_str("txt_tools_clean_btn"), variant="primary")
21
+ text_cleaner_output = gr.Textbox(label=get_str("label_result"), lines=5, interactive=False, show_copy_button=True)
22
+ with gr.Tab(get_str("txt_tools_tab_format")):
23
+ format_output = gr.Textbox(label=get_str("label_result"), lines=5, interactive=False, show_copy_button=True)
24
+ with gr.Row():
25
+ lines_to_comma_btn = gr.Button(get_str("txt_tools_lines_to_comma"), variant="primary")
26
+ comma_to_lines_btn = gr.Button(get_str("txt_tools_comma_to_lines"), variant="primary")
27
+ with gr.Tab(get_str("txt_tools_tab_random")):
28
+ num_lines_to_copy_slider = gr.Slider(minimum=1, maximum=300, value=1, step=1, label=get_str("txt_tools_line_count"))
29
+ select_random_lines_button = gr.Button(get_str("btn_select"), variant="primary")
30
+ random_lines_output = gr.Textbox(label=get_str("label_result"), lines=5, interactive=False, show_copy_button=True)
31
+ text_cleaner_process_button.click(fn=lambda text: (clean_and_uniquify_text_simple(text), gr.update(value="✅ Metin temizlendi.", visible=True)), inputs=[text_input_raw], outputs=[text_cleaner_output, text_cleaner_alert])
32
+ lines_to_comma_btn.click(fn=convert_lines_to_comma, inputs=[text_input_raw], outputs=[format_output, text_cleaner_alert])
33
+ comma_to_lines_btn.click(fn=convert_comma_to_lines, inputs=[text_input_raw], outputs=[format_output, text_cleaner_alert])
34
+ select_random_lines_button.click(fn=select_and_copy_random_lines, inputs=[text_input_raw, num_lines_to_copy_slider], outputs=[random_lines_output, text_cleaner_alert])
modules/utils/file_utils.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ from typing import List
4
+
5
+ CONFIGLIST_KLASORU = "data/rules/configlist"
6
+
7
+ def get_configlist_files():
8
+ """Configlist klasöründeki tüm txt dosyalarını döndürür."""
9
+ if os.path.exists(CONFIGLIST_KLASORU):
10
+ return [os.path.join(CONFIGLIST_KLASORU, f) for f in os.listdir(CONFIGLIST_KLASORU) if f.endswith(".txt")]
11
+ return []
12
+
13
+ def _get_all_image_paths(folder_paths: List[str]) -> List[str]:
14
+ """Tüm klasörlerdeki resim dosyalarının yollarını toplar."""
15
+ image_extensions = (('.png', '.jpg', '.jpeg', '.webp'))
16
+ all_paths = []
17
+ for folder_path in folder_paths:
18
+ if os.path.isdir(folder_path):
19
+ for filename in os.listdir(folder_path):
20
+ if filename.lower().endswith(image_extensions):
21
+ all_paths.append(os.path.join(folder_path, filename))
22
+ return all_paths
modules/utils/tag_utils.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import re
3
+ import random
4
+
5
+ def clean_tags_underscore(tags_string):
6
+ if not tags_string: return ""
7
+ cleaned = re.sub(r'\_+', ' ', tags_string).strip()
8
+ return re.sub(r'\s+', ' ', cleaned)
9
+
10
+ def unique_and_sort_tags(tags_string, sort_order="alpha", original_order_ref=None):
11
+ if not tags_string:
12
+ return ""
13
+
14
+ tags = [t.strip() for t in tags_string.split(',') if t.strip()]
15
+ unique_tags = []
16
+ [unique_tags.append(t) for t in tags if t not in unique_tags]
17
+
18
+ if sort_order == "alpha" or sort_order == "Alfabetik":
19
+ return ", ".join(sorted(unique_tags))
20
+ elif sort_order == "length" or sort_order == "Uzunluğa Göre":
21
+ return ", ".join(sorted(unique_tags, key=len))
22
+ elif sort_order == "random" or sort_order == "Rastgele":
23
+ # random is already imported at the top, no need to re-import
24
+ random.shuffle(unique_tags)
25
+ return ", ".join(unique_tags)
26
+ elif sort_order == "original" or sort_order == "Orijinal":
27
+ return ", ".join(unique_tags) # Zaten unique yaparken sıra korundu
28
+
29
+ # Default case if sort_order is not recognized, or if original_order_ref was intended for a different sort
30
+ return ", ".join(unique_tags)
modules/video_creator.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/video_creator.py
2
+
3
+ import os
4
+ import time
5
+ import numpy as np
6
+ import gradio as gr
7
+ from PIL import Image, ImageFilter, ImageDraw, ImageFont
8
+ import matplotlib.pyplot as plt
9
+ import io
10
+
11
+ # MoviePy Kütüphanesi (v2.0+ Uyumlu)
12
+ from moviepy import (
13
+ ImageClip, AudioFileClip, concatenate_audioclips,
14
+ concatenate_videoclips, CompositeVideoClip, VideoClip, ColorClip, TextClip
15
+ )
16
+
17
+ # Çıktı klasörü
18
+ OUTPUT_DIR = "outputs"
19
+
20
+ # Çözünürlük Ayarları
21
+ RESOLUTIONS = {
22
+ "YouTube (16:9)": (1920, 1080),
23
+ "Shorts/Reels (9:16)": (1080, 1920),
24
+ "Kare (1:1)": (1080, 1080)
25
+ }
26
+
27
+ def create_blurred_background(pil_img, target_size):
28
+ """Resmi bulanıklaştırıp arka plana yayar, orijinali ortada tutar."""
29
+ target_w, target_h = target_size
30
+ img_w, img_h = pil_img.size
31
+
32
+ # 1. Arka planı oluştur (Resmi kaplayacak şekilde büyüt ve bulanıklaştır)
33
+ bg_ratio = max(target_w / img_w, target_h / img_h)
34
+ bg_new_size = (int(img_w * bg_ratio), int(img_h * bg_ratio))
35
+ background = pil_img.resize(bg_new_size, Image.Resampling.LANCZOS)
36
+
37
+ # Ortalamak için kırp
38
+ left = (bg_new_size[0] - target_w) / 2
39
+ top = (bg_new_size[1] - target_h) / 2
40
+ background = background.crop((left, top, left + target_w, top + target_h))
41
+
42
+ # Bulanıklaştır
43
+ background = background.filter(ImageFilter.GaussianBlur(radius=20))
44
+
45
+ # 2. Ön planı oluştur (Sığacak şekilde ayarla)
46
+ fg_ratio = min(target_w / img_w, target_h / img_h)
47
+ fg_new_size = (int(img_w * fg_ratio), int(img_h * fg_ratio))
48
+ foreground = pil_img.resize(fg_new_size, Image.Resampling.LANCZOS)
49
+
50
+ # 3. Birleştir
51
+ final_img = background.copy()
52
+ fg_x = (target_w - fg_new_size[0]) // 2
53
+ fg_y = (target_h - fg_new_size[1]) // 2
54
+ final_img.paste(foreground, (fg_x, fg_y))
55
+
56
+ return final_img
57
+
58
+ def create_text_overlay(txt, target_size, duration):
59
+ """Basit metin görseli oluşturur (PIL kullanarak)"""
60
+ w, h = target_size
61
+ # Şeffaf katman
62
+ img = Image.new('RGBA', target_size, (255, 255, 255, 0))
63
+ draw = ImageDraw.Draw(img)
64
+
65
+ # Font boyutu ve ayarı
66
+ font_size = int(h * 0.05) # Yüksekliğin %5'i
67
+ try:
68
+ font = ImageFont.truetype("arial.ttf", font_size)
69
+ except:
70
+ font = ImageFont.load_default()
71
+
72
+ # Metin boyutunu hesapla
73
+ bbox = draw.textbbox((0, 0), txt, font=font)
74
+ text_w = bbox[2] - bbox[0]
75
+ text_h = bbox[3] - bbox[1]
76
+
77
+ # Alt ortaya yerleştir (biraz yukarıda)
78
+ x = (w - text_w) / 2
79
+ y = h - (text_h * 2.5)
80
+
81
+ # Arka plan kutusu (Siyah, yarı saydam)
82
+ padding = 10
83
+ draw.rectangle(
84
+ [x - padding, y - padding, x + text_w + padding, y + text_h + padding],
85
+ fill=(0, 0, 0, 160)
86
+ )
87
+ # Metni yaz
88
+ draw.text((x, y), txt, font=font, fill="white")
89
+
90
+ return ImageClip(np.array(img)).with_duration(duration)
91
+
92
+ def make_audio_spectrum_clip(audio_clip, target_size, duration):
93
+ """Ses dalgası animasyonu oluşturur (Matplotlib ile)"""
94
+ w, h = target_size
95
+ fps = 10
96
+
97
+ audio_array = audio_clip.to_soundarray(fps=fps)
98
+ if audio_array.ndim > 1:
99
+ audio_array = audio_array.mean(axis=1)
100
+
101
+ audio_array = audio_array / np.max(np.abs(audio_array) + 0.0001)
102
+
103
+ def make_frame(t):
104
+ index = int(t * fps)
105
+ if index >= len(audio_array):
106
+ index = len(audio_array) - 1
107
+
108
+ val = abs(audio_array[index])
109
+
110
+ fig, ax = plt.subplots(figsize=(w/100, h/400), dpi=72)
111
+ fig.patch.set_alpha(0)
112
+ ax.axis('off')
113
+
114
+ bar_height = val
115
+ ax.barh([0], [bar_height], color='cyan', height=0.5, alpha=0.8)
116
+ ax.set_xlim(0, 1)
117
+ ax.set_ylim(-0.5, 0.5)
118
+
119
+ buf = io.BytesIO()
120
+ fig.savefig(buf, format='png', bbox_inches='tight', pad_inches=0, transparent=True)
121
+ buf.seek(0)
122
+ plt.close(fig)
123
+
124
+ img = Image.open(buf).convert("RGBA")
125
+ img = img.resize((w, int(h*0.2)))
126
+
127
+ full_frame = Image.new('RGBA', (w, h), (0,0,0,0))
128
+ full_frame.paste(img, (0, int(h * 0.8)))
129
+
130
+ return np.array(full_frame)
131
+
132
+ clip = VideoClip(make_frame, duration=duration)
133
+ mask = VideoClip(make_frame, duration=duration).to_mask()
134
+ return clip.with_mask(mask)
135
+
136
+
137
+ def create_video_logic(
138
+ image_files, audio_files,
139
+ resolution_key,
140
+ custom_text,
141
+ enable_spectrum,
142
+ video_preset, # YENİ PARAMETRE
143
+ progress=gr.Progress()
144
+ ):
145
+ if not image_files: return None, "⚠️ Lütfen en az bir resim seçin."
146
+ if not audio_files: return None, "⚠️ Lütfen en az bir ses dosyası seçin."
147
+
148
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
149
+ output_filename = f"pro_video_{int(time.time())}.mp4"
150
+ output_path = os.path.join(OUTPUT_DIR, output_filename)
151
+
152
+ target_size = RESOLUTIONS.get(resolution_key, (1920, 1080))
153
+
154
+ try:
155
+ progress(0.1, desc="Sesler işleniyor...")
156
+ audio_clips = [AudioFileClip(f.name if hasattr(f, 'name') else f) for f in audio_files]
157
+ final_audio = concatenate_audioclips(audio_clips)
158
+ total_duration = final_audio.duration
159
+
160
+ progress(0.2, desc="Görseller hazırlanıyor (Slayt Modu)...")
161
+
162
+ img_duration = total_duration / len(image_files)
163
+ video_clips = []
164
+
165
+ for img_file in image_files:
166
+ img_path = img_file.name if hasattr(img_file, 'name') else img_file
167
+
168
+ with Image.open(img_path) as pil_img:
169
+ processed_img = create_blurred_background(pil_img, target_size)
170
+
171
+ clip = ImageClip(np.array(processed_img)).with_duration(img_duration)
172
+ video_clips.append(clip)
173
+
174
+ base_video = concatenate_videoclips(video_clips, method="compose")
175
+ base_video = base_video.with_audio(final_audio)
176
+
177
+ final_layers = [base_video]
178
+
179
+ if custom_text and custom_text.strip():
180
+ progress(0.4, desc="Metin katmanı oluşturuluyor...")
181
+ txt_clip = create_text_overlay(custom_text, target_size, total_duration)
182
+ final_layers.append(txt_clip)
183
+
184
+ if enable_spectrum:
185
+ progress(0.5, desc="Ses dalgası grafiği oluşturuluyor (Bu işlem ÇOK yavaş olabilir)...")
186
+ try:
187
+ spectrum_clip = make_audio_spectrum_clip(final_audio, target_size, total_duration)
188
+ final_layers.append(spectrum_clip)
189
+ except Exception as e:
190
+ print(f"Spectrum hatası: {e}")
191
+
192
+ final_video = CompositeVideoClip(final_layers, size=target_size)
193
+
194
+ progress(0.7, desc=f"Video render ediliyor ({video_preset})... Bu işlem uzun sürebilir, lütfen bekleyin.")
195
+
196
+ # Render işlemi - logger='bar' konsola ilerleme yazar
197
+ final_video.write_videofile(
198
+ output_path,
199
+ fps=24,
200
+ codec='libx264',
201
+ audio_codec='aac',
202
+ preset=video_preset,
203
+ threads=os.cpu_count() or 4, # CPU sayısına göre otomatik
204
+ logger='bar' # Konsola ilerleme yazsın
205
+ )
206
+
207
+ # Bellek temizliği
208
+ final_video.close()
209
+ for clip in video_clips:
210
+ clip.close()
211
+ for ac in audio_clips:
212
+ ac.close()
213
+ final_audio.close()
214
+
215
+ progress(1.0, desc="✅ İşlem Tamamlandı!")
216
+
217
+ return output_path, f"✅ Pro Video Hazır! ({resolution_key})"
218
+
219
+ except Exception as e:
220
+ import traceback
221
+ traceback.print_exc()
222
+ return None, f"⛔ Hata: {str(e)}"
223
+
224
+ # --- UI ---
225
+
226
+ import gradio as gr
227
+ from modules.localization.languages import get_str
228
+
229
+ def create_video_ui():
230
+ gr.Markdown(get_str("vid_header"))
231
+ gr.Markdown(get_str("vid_desc"))
232
+
233
+ with gr.Row():
234
+ with gr.Column():
235
+ image_input = gr.File(label=get_str("vid_img_input"), file_count="multiple", file_types=["image"])
236
+ audio_input = gr.File(label=get_str("vid_audio_input"), file_count="multiple", file_types=["audio"])
237
+
238
+ with gr.Row():
239
+ resolution_dropdown = gr.Dropdown(
240
+ choices=["1920x1080", "1280x720", "1080x1920", "720x1280", "1080x1080"],
241
+ value="1920x1080",
242
+ label=get_str("vid_resolution")
243
+ )
244
+ preset_radio = gr.Radio(
245
+ choices=["ultrafast", "superfast", "veryfast", "faster", "fast", "medium"],
246
+ value="medium",
247
+ label=get_str("vid_speed")
248
+ )
249
+
250
+ text_overlay = gr.Textbox(label=get_str("vid_text_overlay"), placeholder="Örn: My Artworks 2024")
251
+ spectrum_check = gr.Checkbox(label=get_str("vid_spectrum_check"), value=False)
252
+
253
+ create_btn = gr.Button(get_str("vid_create_btn"), variant="primary")
254
+
255
+ with gr.Column():
256
+ video_output = gr.Video(label=get_str("vid_preview"))
257
+ status_output = gr.Textbox(label=get_str("vid_status"), interactive=False)
258
+
259
+ create_btn.click(
260
+ fn=create_video_logic,
261
+ inputs=[image_input, audio_input, resolution_dropdown, text_overlay, spectrum_check, preset_radio],
262
+ outputs=[video_output, status_output]
263
+ )