baqu2213 commited on
Commit
d026b24
ยท
verified ยท
1 Parent(s): 3a50879

Upload wd14_test_v2.py

Browse files
Files changed (1) hide show
  1. NAIA/wd14_test_v2.py +1401 -0
NAIA/wd14_test_v2.py ADDED
@@ -0,0 +1,1401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tkinter as tk
2
+ from tkinter import filedialog, messagebox, scrolledtext, ttk
3
+ from PIL import Image, ImageTk, ImageGrab
4
+ import threading
5
+ import os
6
+ import numpy as np
7
+ import pandas as pd
8
+ import cv2
9
+ import tempfile
10
+ import io
11
+ import json
12
+ import sys
13
+ import time # ์‹œ๊ฐ„ ์ธก์ •์šฉ
14
+ from datetime import datetime # ํด๋”๋ช… ์ƒ์„ฑ์šฉ
15
+ import subprocess # ํด๋” ์—ด๊ธฐ์šฉ
16
+ import win32clipboard
17
+ import random
18
+
19
+ # resource_path ํ•จ์ˆ˜๋Š” ๋ณ€๊ฒฝ ์—†์ด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
20
+ def resource_path(relative_path):
21
+ """ PyInstaller์— ์˜ํ•ด ์ž„์‹œ ํด๋”์— ์ƒ์„ฑ๋œ ๋ฆฌ์†Œ์Šค์˜ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. """
22
+ try:
23
+ base_path = sys._MEIPASS
24
+ except Exception:
25
+ base_path = os.path.abspath(".")
26
+ return os.path.join(base_path, relative_path)
27
+
28
+ class WD14TaggerGUI:
29
+ def __init__(self, root):
30
+ self.root = root
31
+ self.root.title("WD14 Auto Prompt Generator (aiart) v2")
32
+
33
+ self.root.minsize(680, 800)
34
+ self.root.resizable(True, True)
35
+
36
+ self.SETTINGS_FILE = "tagger_app_settings.json"
37
+
38
+ # --- ๋ณ€์ˆ˜ ์ดˆ๊ธฐํ™” ---
39
+ self.current_image_path = None
40
+ self.session = None
41
+ self.tags_df = None
42
+ self.is_generating = False
43
+ self.save_counter = 1
44
+ self.last_generated_image = None
45
+ self.temp_image_path = os.path.join(tempfile.gettempdir(), "wd14_temp_generated.png")
46
+ self.save_session_folder = datetime.now().strftime('%Y%m%d_%H%M')
47
+
48
+ # Tkinter ๋ณ€์ˆ˜
49
+ self.general_threshold_var = tk.DoubleVar(value=0.5)
50
+ self.char_threshold_var = tk.DoubleVar(value=0.85)
51
+ self.rm_c_var = tk.BooleanVar()
52
+ self.rm_color_var = tk.BooleanVar()
53
+ self.rm_clothes_var = tk.BooleanVar()
54
+ self.webui_mode_var = tk.BooleanVar()
55
+ self.instant_inference_var = tk.BooleanVar(value=False)
56
+ self.show_removed_tags_var = tk.BooleanVar(value=True)
57
+ self.show_ignore_tags_var = tk.BooleanVar(value=False)
58
+ self.prompt_active_var = tk.BooleanVar(value=False)
59
+ self.expand_ui_var = tk.BooleanVar(value=False)
60
+ self.sampler_var = tk.StringVar()
61
+ self.resolution_var = tk.StringVar()
62
+ self.steps_var = tk.StringVar(value="28")
63
+ self.scale_var = tk.StringVar(value="5.5")
64
+ self.seed_var = tk.StringVar(value="-1")
65
+ self.cfg_rescale_var = tk.StringVar(value="0.25")
66
+ self.nai_token_var = tk.StringVar()
67
+ self.model_var = tk.StringVar()
68
+ self.auto_save_var = tk.BooleanVar(value=True)
69
+ self.instant_generate_var = tk.BooleanVar(value=False)
70
+ self.auto_rating_tag_var = tk.BooleanVar(value=True)
71
+ self.character_prompts = []
72
+ self.char_check_vars = [tk.BooleanVar(value=False) for _ in range(3)]
73
+ self.char_check_vars[0].set(True)
74
+ self.last_generated_seed = random.randint(0, 9999999999)
75
+
76
+ self.init_filter_data()
77
+ self.setup_ui()
78
+ self.load_model()
79
+ self.show_initial_preview()
80
+ self.setup_drag_drop()
81
+ self.load_settings()
82
+ self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
83
+ self.root.bind('<Control-v>', self.handle_paste)
84
+
85
+ def _create_thumbnail(self, pil_image, size=512):
86
+ """PIL ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์•„ ํฐ ๋ฐฐ๊ฒฝ์˜ ์ •์‚ฌ๊ฐํ˜• ์ธ๋„ค์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค."""
87
+ # 1. ์›๋ณธ ์ด๋ฏธ์ง€์˜ ๋„ˆ๋น„์™€ ๋†’์ด ์ค‘ ๋” ํฐ ์ชฝ์„ ๊ธฐ์ค€์œผ๋กœ ์ •์‚ฌ๊ฐํ˜• ์บ”๋ฒ„์Šค ์ƒ์„ฑ
88
+ bg_size = max(pil_image.size)
89
+ thumbnail_bg = Image.new('RGB', (bg_size, bg_size), 'white')
90
+
91
+ # 2. ์บ”๋ฒ„์Šค ์ค‘์•™์— ์›๋ณธ ์ด๋ฏธ์ง€ ๋ถ™์—ฌ๋„ฃ๊ธฐ
92
+ paste_x = (bg_size - pil_image.width) // 2
93
+ paste_y = (bg_size - pil_image.height) // 2
94
+ thumbnail_bg.paste(pil_image, (paste_x, paste_y))
95
+
96
+ # 3. ์ตœ์ข… ์ธ๋„ค์ผ ํฌ๊ธฐ๋กœ ๋ฆฌ์‚ฌ์ด์ฆˆ
97
+ thumbnail_bg.thumbnail((size, size), Image.Resampling.LANCZOS)
98
+ return thumbnail_bg
99
+
100
+ def _update_timer(self):
101
+ """์ƒ์„ฑ ๋ฒ„ํŠผ์˜ ํƒ€์ด๋จธ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค."""
102
+ if self.is_generating:
103
+ elapsed = time.time() - self.generation_start_time
104
+ self.generation_btn.config(text=f"์ƒ์„ฑ ์ค‘: {int(elapsed)}s")
105
+ self.root.after(1000, self._update_timer) # 1์ดˆ๋งˆ๋‹ค ๋ฐ˜๋ณต
106
+
107
+ def _copy_image_to_clipboard(self):
108
+ """์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค (Windows ์ „์šฉ)."""
109
+ if self.last_generated_image is None:
110
+ messagebox.showinfo("์ •๋ณด", "๋ณต์‚ฌํ•  ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
111
+ return
112
+
113
+ if win32clipboard is None:
114
+ messagebox.showerror("์˜ค๋ฅ˜", "์ด๋ฏธ์ง€ ๋ณต์‚ฌ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด 'pywin32' ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.\n(pip install pywin32)")
115
+ return
116
+
117
+ if sys.platform == "win32":
118
+ try:
119
+ output = io.BytesIO()
120
+ self.last_generated_image.convert("RGB").save(output, "BMP")
121
+ data = output.getvalue()[14:]
122
+ output.close()
123
+ win32clipboard.OpenClipboard()
124
+ win32clipboard.EmptyClipboard()
125
+ win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
126
+ win32clipboard.CloseClipboard()
127
+ #messagebox.showinfo("์„ฑ๊ณต", "์ด๋ฏธ์ง€๋ฅผ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌํ–ˆ์Šต๋‹ˆ๋‹ค.")
128
+ except Exception as e:
129
+ messagebox.showerror("๋ณต์‚ฌ ์‹คํŒจ", f"์ด๋ฏธ์ง€ ๋ณต์‚ฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
130
+ else:
131
+ messagebox.showinfo("์ •๋ณด", "์ด๋ฏธ์ง€ ๋ณต์‚ฌ ๊ธฐ๋Šฅ์€ Windows์—์„œ๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค.")
132
+
133
+ def _copy_file_object_to_clipboard(self):
134
+ """์ž„์‹œ ์ €์žฅ๋œ ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ํด๋ฆฝ๋ณด๋“œ์— ํŒŒ์ผ ๊ฐ์ฒด๋กœ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค (Windows ์ „์šฉ)."""
135
+ if not os.path.exists(self.temp_image_path):
136
+ messagebox.showinfo("์ •๋ณด", "๋ณต์‚ฌํ•  ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.")
137
+ return
138
+
139
+ if sys.platform == "win32":
140
+ try:
141
+ command = f"powershell -command \"Get-Item '{self.temp_image_path}' | Set-Clipboard\""
142
+ subprocess.run(command, check=True, shell=True, creationflags=subprocess.CREATE_NO_WINDOW)
143
+ #messagebox.showinfo("์„ฑ๊ณต", "ํŒŒ์ผ์„ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌํ–ˆ์Šต๋‹ˆ๋‹ค.")
144
+ except Exception as e:
145
+ messagebox.showerror("๋ณต์‚ฌ ์‹คํŒจ", f"ํŒŒ์ผ ๋ณต์‚ฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
146
+ else:
147
+ messagebox.showinfo("์ •๋ณด", "ํŒŒ์ผ ๋ณต์‚ฌ ๊ธฐ๋Šฅ์€ Windows์—์„œ๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค.")
148
+
149
+ def _show_context_menu(self, event):
150
+ """์ด๋ฏธ์ง€ ๋ ˆ์ด๋ธ” ์œ„์—์„œ ์šฐํด๋ฆญ ์‹œ ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค."""
151
+ if self.last_generated_image:
152
+ self.image_context_menu.tk_popup(event.x_root, event.y_root)
153
+
154
+ def _save_image_manual(self):
155
+ """(์ˆ˜์ •๋จ) ์ด๋ฏธ์ง€๋ฅผ ์ž๋™ ์ €์žฅ ํด๋”์— ์ˆœ์ฐจ์ ์œผ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค."""
156
+ if self.last_generated_image is None:
157
+ messagebox.showinfo("์ •๋ณด", "์ €์žฅํ•  ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
158
+ return
159
+
160
+ try:
161
+ save_dir = os.path.join("Output_WD14", self.save_session_folder)
162
+ os.makedirs(save_dir, exist_ok=True)
163
+ file_path = os.path.join(save_dir, f"{self.save_counter:05d}.png")
164
+ self.last_generated_image.save(file_path, "PNG")
165
+
166
+ #messagebox.showinfo("์„ฑ๊ณต", f"์ด๋ฏธ์ง€๋ฅผ {file_path}์— ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.")
167
+ self.save_counter += 1
168
+ self.save_btn.config(state=tk.DISABLED) # ์ €์žฅ ํ›„ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™”
169
+ except Exception as e:
170
+ messagebox.showerror("์ €์žฅ ์‹คํŒจ", f"์ด๋ฏธ์ง€ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
171
+
172
+ def _open_save_folder(self):
173
+ """์ž๋™ ์ €์žฅ ํด๋”๋ฅผ ์—ฝ๋‹ˆ๋‹ค."""
174
+ session_path = os.path.join("Output_WD14", self.save_session_folder)
175
+ base_path = "Output_WD14"
176
+
177
+ try:
178
+ if os.path.isdir(session_path):
179
+ path_to_open = session_path
180
+ elif os.path.isdir(base_path):
181
+ path_to_open = base_path
182
+ else:
183
+ messagebox.showinfo("์ •๋ณด", f"'{base_path}' ํด๋”๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
184
+ return
185
+
186
+ if sys.platform == "win32":
187
+ os.startfile(os.path.realpath(path_to_open))
188
+ elif sys.platform == "darwin":
189
+ subprocess.Popen(["open", os.path.realpath(path_to_open)])
190
+ else:
191
+ subprocess.Popen(["xdg-open", os.path.realpath(path_to_open)])
192
+ except Exception as e:
193
+ messagebox.showerror("์˜ค๋ฅ˜", f"ํด๋”๋ฅผ ์—ฌ๋Š” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
194
+
195
+ # Step 2 & 3 & 4์˜ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•  ๋ฉ”์„œ๋“œ
196
+ def toggle_expand_ui(self):
197
+ """์ฒดํฌ๋ฐ•์Šค ์ƒํƒœ์— ๋”ฐ๋ผ ์šฐ์ธก UI์™€ '์ฆ‰์‹œ ์ƒ์„ฑ' ๋ฒ„ํŠผ์˜ ๊ฐ€์‹œ์„ฑ์„ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค."""
198
+ main_frame = self.right_pane.master
199
+ if self.expand_ui_var.get():
200
+ main_frame.grid_columnconfigure(1, weight=1, minsize=400)
201
+ self.right_pane.grid(row=0, column=1, sticky="nsew")
202
+ self.root.minsize(1220, 800)
203
+ # '์ฆ‰์‹œ ์ƒ์„ฑ' ์ฒดํฌ๋ฐ•์Šค ํ‘œ์‹œ
204
+ self.instant_generate_cb.pack(side=tk.LEFT, padx=(5,0))
205
+ else:
206
+ self.right_pane.grid_forget()
207
+ main_frame.grid_columnconfigure(1, weight=0, minsize=0)
208
+ self.root.minsize(680, 800)
209
+ # '์ฆ‰์‹œ ์ƒ์„ฑ' ์ฒดํฌ๋ฐ•์Šค ์ˆจ๊น€
210
+ self.instant_generate_cb.pack_forget()
211
+
212
+ # Step 3์˜ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•  ๋ฉ”์„œ๋“œ
213
+ def show_generation_preview(self):
214
+ """์šฐ์ธก ํŒจ๋„์— 512x512 ํฐ์ƒ‰ ์ด๋ฏธ์ง€๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค."""
215
+ try:
216
+ white_image = Image.new('RGB', (512, 512), 'white')
217
+ photo = ImageTk.PhotoImage(white_image)
218
+ # PhotoImage ๊ฐ์ฒด์— ๋Œ€ํ•œ ์ฐธ์กฐ๋ฅผ ์œ ์ง€ํ•˜์—ฌ ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜์„ ๋ฐฉ์ง€
219
+ self.generation_image_label.image = photo
220
+ self.generation_image_label.config(image=photo)
221
+ except Exception as e:
222
+ self.generation_image_label.config(text=f"์ด๋ฏธ์ง€ ํ‘œ์‹œ ์˜ค๋ฅ˜: {e}")
223
+
224
+ def random_seed(self):
225
+ if int(self.seed_var.get()) == -1:
226
+ self.seed_var.set(str(self.last_generated_seed))
227
+ else:
228
+ self.seed_var.set("-1")
229
+
230
+ def setup_ui(self):
231
+ # ... (๊ธฐ์กด setup_ui์˜ ์‹œ์ž‘ ๋ถ€๋ถ„์€ ๋™์ผ) ...
232
+ self.root.grid_columnconfigure(0, weight=1)
233
+ self.root.grid_rowconfigure(0, weight=1)
234
+
235
+ main_frame = ttk.Frame(self.root, padding="10")
236
+ main_frame.grid(row=0, column=0, sticky="nsew")
237
+
238
+ main_frame.grid_columnconfigure(0, weight=1)
239
+ main_frame.grid_columnconfigure(1, weight=1, minsize=400)
240
+
241
+ main_frame.grid_rowconfigure(0, weight=1)
242
+
243
+ # --- ์ขŒ์ธก UI ํŒจ๋„ ---
244
+ left_pane = ttk.Frame(main_frame)
245
+ left_pane.grid(row=0, column=0, sticky="nsew")
246
+ left_pane.grid_columnconfigure(0, weight=1)
247
+ left_pane.grid_rowconfigure(2, weight=4)
248
+ left_pane.grid_rowconfigure(3, weight=3)
249
+
250
+ # ... (์ขŒ์ธก ํŒจ๋„์˜ ๋ชจ๋“  UI ๊ตฌ์„ฑ์€ ๊ธฐ์กด๊ณผ ๋™์ผ) ...
251
+ load_frame = ttk.Frame(left_pane)
252
+ load_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10))
253
+ self.load_btn = ttk.Button(load_frame, text="์ด๋ฏธ์ง€ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ", command=self.load_image)
254
+ self.load_btn.pack(side="left", padx=(0, 5))
255
+ self.clipboard_btn = ttk.Button(load_frame, text="์ด๋ฏธ์ง€ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ (ํด๋ฆฝ๋ณด๋“œ)", command=self.load_from_clipboard)
256
+ self.clipboard_btn.pack(side="left")
257
+ self.expand_ui_cb = ttk.Checkbutton(load_frame, text="์ƒ์„ฑ UI ํ™•์žฅ",
258
+ variable=self.expand_ui_var,
259
+ command=self.toggle_expand_ui)
260
+ self.expand_ui_cb.pack(side="left", padx=(10, 0))
261
+ content_frame = ttk.Frame(left_pane)
262
+ content_frame.grid(row=1, column=0, sticky="new")
263
+ content_frame.grid_columnconfigure(1, weight=1)
264
+ self.preview_frame = ttk.LabelFrame(content_frame, text="๋ฏธ๋ฆฌ๋ณด๊ธฐ", padding="10")
265
+ self.preview_frame.grid(row=0, column=0, sticky="n", padx=(0, 10))
266
+ self.image_label = ttk.Label(self.preview_frame)
267
+ self.image_label.grid(row=0, column=0)
268
+ settings_frame = ttk.LabelFrame(content_frame, text="์„ค์ •", padding="10")
269
+ settings_frame.grid(row=0, column=1, sticky="nsew")
270
+ settings_frame.grid_columnconfigure(1, weight=1)
271
+ threshold_frame = ttk.Frame(settings_frame)
272
+ threshold_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
273
+ threshold_frame.grid_columnconfigure(1, weight=1)
274
+ ttk.Label(threshold_frame, text="ํƒœ๊ทธ ์ž„๊ณ„๊ฐ’:").grid(row=0, column=0, sticky="w")
275
+ ttk.Spinbox(threshold_frame, from_=0.0, to=1.0, increment=0.05,
276
+ textvariable=self.general_threshold_var, width=15).grid(row=0, column=1, sticky="we", pady=(0, 5))
277
+
278
+ extract_control_frame = ttk.Frame(settings_frame)
279
+ extract_control_frame.grid(row=1, column=0, columnspan=2, pady=5, sticky="ew")
280
+
281
+ self.extract_btn = ttk.Button(extract_control_frame, text="ํƒœ๊ทธ ์ถ”์ถœ", command=self.extract_tags, state='disabled')
282
+ self.extract_btn.pack(side=tk.LEFT, padx=(0, 10))
283
+
284
+ self.instant_inference_cb = ttk.Checkbutton(extract_control_frame, text="์ฆ‰์‹œ ์ถ”๋ก ", variable=self.instant_inference_var)
285
+ self.instant_inference_cb.pack(side=tk.LEFT)
286
+
287
+ # '์ฆ‰์‹œ ์ƒ์„ฑ' ์ฒดํฌ๋ฐ•์Šค ์ƒ์„ฑ (์ดˆ๊ธฐ์—๋Š” ์ˆจ๊ฒจ์ง)
288
+ self.instant_generate_cb = ttk.Checkbutton(extract_control_frame, text="์ฆ‰์‹œ ์ƒ์„ฑ", variable=self.instant_generate_var)
289
+
290
+ filter_frame = ttk.LabelFrame(settings_frame, text="ํƒœ๊ทธ ํ•„ํ„ฐ๋ง", padding="5")
291
+ filter_frame.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(5, 0))
292
+ ttk.Checkbutton(filter_frame, text="์บ๋ฆญํ„ฐ ํŠน์ง•+์ด๋ฆ„ ์ œ๊ฑฐ",
293
+ variable=self.rm_c_var).pack(anchor="w")
294
+ ttk.Checkbutton(filter_frame, text="์˜๋ฅ˜ ์ƒ‰์ƒ ์ œ๊ฑฐ",
295
+ variable=self.rm_color_var).pack(anchor="w")
296
+ ttk.Checkbutton(filter_frame, text="WEBUI/Comfy ๋ชจ๋“œ",
297
+ variable=self.webui_mode_var).pack(anchor="w")
298
+ prompt_outer_frame = ttk.Frame(settings_frame)
299
+ prompt_outer_frame.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(10, 0))
300
+ prompt_outer_frame.grid_columnconfigure(0, weight=1)
301
+ self.prompt_active_cb = ttk.Checkbutton(prompt_outer_frame, text="์„ ํ–‰/ํ›„ํ–‰ ํ”„๋กฌํ”„ํŠธ ํ™œ์„ฑ",
302
+ variable=self.prompt_active_var,
303
+ command=self.toggle_prompts)
304
+ self.prompt_active_cb.grid(row=0, column=0, sticky="w")
305
+ self.prompt_notebook = ttk.Notebook(prompt_outer_frame)
306
+ prefix_frame = ttk.Frame(self.prompt_notebook, padding=5)
307
+ self.prompt_notebook.add(prefix_frame, text="์„ ํ–‰ ํ”„๋กฌํ”„ํŠธ")
308
+ self.prefix_text = scrolledtext.ScrolledText(prefix_frame, height=3, width=30)
309
+ self.prefix_text.pack(fill="both", expand=True)
310
+ suffix_frame = ttk.Frame(self.prompt_notebook, padding=5)
311
+ self.prompt_notebook.add(suffix_frame, text="ํ›„ํ–‰ ํ”„๋กฌํ”„ํŠธ")
312
+ self.suffix_text = scrolledtext.ScrolledText(suffix_frame, height=3, width=30)
313
+ self.suffix_text.pack(fill="both", expand=True)
314
+ self.toggle_prompts()
315
+ result_frame = ttk.LabelFrame(left_pane, text="์ถ”์ถœ๋œ ํƒœ๊ทธ", padding="10")
316
+ result_frame.grid(row=2, column=0, sticky="nsew", pady=(10, 0))
317
+ result_frame.grid_columnconfigure(0, weight=1)
318
+ result_frame.grid_rowconfigure(0, weight=3)
319
+ result_frame.grid_rowconfigure(3, weight=2)
320
+ self.result_text = scrolledtext.ScrolledText(result_frame, height=7, wrap=tk.WORD)
321
+ self.result_text.grid(row=0, column=0, sticky="nsew")
322
+ self.result_text.tag_config("prompt", foreground="grey")
323
+ copy_frame = ttk.Frame(result_frame)
324
+ copy_frame.grid(row=1, column=0, pady=(10, 5), sticky="w")
325
+ ttk.Button(copy_frame, text="์ „์ฒด ๋ณต์‚ฌ", command=self.copy_all_tags).pack(side="left", padx=(0, 5))
326
+ ttk.Button(copy_frame, text="2๋ฒˆ ํƒœ๊ทธ๋ถ€ํ„ฐ ๋ณต์‚ฌ", command=self.copy_from_second).pack(side="left", padx=5)
327
+ ttk.Button(copy_frame, text="3๋ฒˆ ํƒœ๊ทธ๋ถ€ํ„ฐ ๋ณต์‚ฌ", command=self.copy_from_third).pack(side="left", padx=5)
328
+ auto_rating_cb = ttk.Checkbutton(copy_frame, text="nsfw/safe ํƒœ๊ทธ ์ž๋™ ๋ฐ˜์˜", variable=self.auto_rating_tag_var)
329
+ auto_rating_cb.pack(side="left", padx=10)
330
+ self.removed_tags_cb = ttk.Checkbutton(result_frame, text="์ œ๊ฑฐ๋œ ํƒœ๊ทธ ๋ณด๊ธฐ",
331
+ variable=self.show_removed_tags_var,
332
+ command=self.toggle_removed_tags)
333
+ self.removed_tags_cb.grid(row=2, column=0, sticky="w", pady=(10, 2))
334
+ self.removed_tags_text = scrolledtext.ScrolledText(result_frame, height=4, wrap=tk.WORD, state=tk.DISABLED)
335
+ self.removed_tags_text.tag_config("ignored", foreground="red")
336
+ self.toggle_removed_tags()
337
+ bottom_frame = ttk.Frame(left_pane)
338
+ bottom_frame.grid(row=3, column=0, sticky="nsew", pady=(10, 0))
339
+ bottom_frame.grid_columnconfigure(0, weight=1)
340
+ bottom_frame.grid_rowconfigure(1, weight=1)
341
+ bottom_control_frame = ttk.Frame(bottom_frame)
342
+ bottom_control_frame.grid(row=0, column=0, sticky="ew")
343
+ self.ignore_tags_cb = ttk.Checkbutton(bottom_control_frame, text="๋ฌด์‹œํ•  ํƒœ๊ทธ ํŽธ์ง‘ (์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„)",
344
+ variable=self.show_ignore_tags_var,
345
+ command=self.toggle_ignore_tags)
346
+ self.ignore_tags_cb.pack(side="left", anchor="w")
347
+ self.save_settings_btn = ttk.Button(bottom_control_frame, text="์„ค์ • ์ €์žฅ", command=self.save_settings)
348
+ self.save_settings_btn.pack(side="right", anchor="e")
349
+ self.ignore_tags_frame = ttk.Frame(bottom_frame)
350
+ self.ignore_tags_frame.grid_columnconfigure(0, weight=1)
351
+ self.ignore_tags_frame.grid_rowconfigure(0, weight=1)
352
+ self.ignore_tags_text = scrolledtext.ScrolledText(self.ignore_tags_frame, height=4, wrap=tk.WORD)
353
+ self.ignore_tags_text.grid(row=0, column=0, sticky="nsew")
354
+ self.toggle_ignore_tags()
355
+
356
+
357
+ # --- ์šฐ์ธก UI ํŒจ๋„ ---
358
+ self.right_pane = ttk.Frame(main_frame)
359
+ self.right_pane.grid_rowconfigure(0, weight=1)
360
+ self.right_pane.grid_rowconfigure(2, weight=0)
361
+ self.right_pane.grid_columnconfigure(0, weight=1)
362
+
363
+ generation_preview_frame = ttk.LabelFrame(self.right_pane, text="์ƒ์„ฑ ๋ฏธ๋ฆฌ๋ณด๊ธฐ", padding="5")
364
+ generation_preview_frame.grid(row=0, column=0, sticky="nsew")
365
+ generation_preview_frame.grid_columnconfigure(0, weight=1)
366
+ generation_preview_frame.grid_rowconfigure(0, weight=1)
367
+
368
+ self.generation_image_label = ttk.Label(generation_preview_frame, anchor="center")
369
+ self.generation_image_label.grid(row=0, column=0, sticky="nsew")
370
+ self.generation_image_label.bind("<Button-3>", self._show_context_menu) # ์šฐํด๋ฆญ ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ
371
+
372
+ # ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด ์ƒ์„ฑ
373
+ self.image_context_menu = tk.Menu(self.root, tearoff=0)
374
+ self.image_context_menu.add_command(label="์ด๋ฏธ์ง€๋ฅผ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌ", command=self._copy_image_to_clipboard)
375
+ self.image_context_menu.add_command(label="ํŒŒ์ผ์„ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌ", command=self._copy_file_object_to_clipboard)
376
+ self.image_context_menu.add_command(label="๊ธฐ๋ณธ ๋ทฐ์–ด๋กœ ์ด๋ฏธ์ง€ ์—ด๊ธฐ", command=self._show_image_with_pil)
377
+
378
+ # generation_button_frame ๊ตฌ์„ฑ ๋ถ€๋ถ„ ์ˆ˜์ •
379
+ generation_button_frame = ttk.Frame(self.right_pane)
380
+ generation_button_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0), padx=10)
381
+
382
+ self.folder_btn = ttk.Button(generation_button_frame, text="๐Ÿ“", width=2, command=self._open_save_folder)
383
+ self.folder_btn.pack(side="right")
384
+ self.save_btn = ttk.Button(generation_button_frame, text="Save ๐Ÿ’พ", width=8, command=self._save_image_manual)
385
+ self.save_btn.pack(side="right", padx=(5, 0))
386
+ self.copy_btn = ttk.Button(generation_button_frame, text=" Copy ๐Ÿ—ƒ๏ธ", width=8, command=self._copy_image_to_clipboard)
387
+ self.copy_btn.pack(side="right", padx=(5, 0))
388
+
389
+ self.generation_btn = ttk.Button(generation_button_frame, text="Generation", command=self.start_generation)
390
+ self.generation_btn.pack(side="left", fill="x", expand=True)
391
+
392
+ # --- Notebook ๋ฐ ํƒญ ์„ค์ • ---
393
+ self.settings_notebook = ttk.Notebook(self.right_pane)
394
+ self.settings_notebook.grid(row=2, column=0, sticky="nsew", pady=(5, 0), padx=10)
395
+
396
+ # <<<<<<< ํ•ต์‹ฌ ๋ณ€๊ฒฝ์ : ๋ชจ๋“  ํƒญ ํ”„๋ ˆ์ž„์„ ๋จผ์ € ์ƒ์„ฑ >>>>>>>>>
397
+ basic_settings_tab = ttk.Frame(self.settings_notebook, padding="10")
398
+ char_prompt_tab = ttk.Frame(self.settings_notebook, padding=(10, 10, 10, 0))
399
+ account_settings_tab = ttk.Frame(self.settings_notebook, padding="10")
400
+
401
+ # <<<<<<< ํ•ต์‹ฌ ๋ณ€๊ฒฝ์ : ์›ํ•˜๋Š” ์ˆœ์„œ๋Œ€๋กœ .add()๋งŒ ์‚ฌ์šฉ >>>>>>>>>
402
+ self.settings_notebook.add(basic_settings_tab, text="๊ธฐ๋ณธ์„ค์ •")
403
+ self.settings_notebook.add(char_prompt_tab, text="์บ๋ฆญํ„ฐ ํ”„๋กฌํ”„ํŠธ")
404
+ self.settings_notebook.add(account_settings_tab, text="NAI์„ค์ •")
405
+
406
+ # --- ์ด์ œ ๊ฐ ํƒญ์˜ ๋‚ด์šฉ์„ ์ฑ„์›๋‹ˆ๋‹ค ---
407
+
408
+ # 1. '๊ธฐ๋ณธ์„ค์ •' ํƒญ ๋‚ด์šฉ ๊ตฌ์„ฑ
409
+ # ... ('๊ธฐ๋ณธ์„ค์ •' ํƒญ์˜ ๋ชจ๋“  ์œ„์ ฏ ์ฝ”๋“œ๋Š” ์—ฌ๊ธฐ์— ๊ทธ๋Œ€๋กœ ์œ ์ง€) ...
410
+ basic_settings_tab.grid_columnconfigure(1, weight=1); basic_settings_tab.grid_columnconfigure(3, weight=1); basic_settings_tab.grid_columnconfigure(5, weight=1)
411
+ sampler_list = ["k_euler + native", "k_euler + karras", "k_euler + exponential", "k_euler_ancestral + native", "k_euler_ancestral + karras","k_euler_ancestral + exponential", "k_dpmpp_2s_ancestral + native", "k_dpmpp_2s_ancestral + karras", "k_dpmpp_2s_ancestral + exponential", "k_dpmpp_sde + native", "k_dpmpp_sde + karras", "k_dpmpp_sde + exponential", "k_dpmpp_2m + native", "k_dpmpp_2m + karras", "k_dpmpp_2m + exponential","k_dpmpp_2m_sde + native", "k_dpmpp_2m_sde + karras", "k_dpmpp_2m_sde + exponential"]
412
+ ttk.Label(basic_settings_tab, text="Sampler:").grid(row=0, column=0, sticky="w", padx=(0, 5)); sampler_cb = ttk.Combobox(basic_settings_tab, values=sampler_list, textvariable=self.sampler_var, state="readonly", width=25); sampler_cb.grid(row=0, column=1, sticky="ew"); sampler_cb.set(sampler_list[4])
413
+ ttk.Label(basic_settings_tab, text="Steps:").grid(row=0, column=2, sticky="w", padx=(10, 5)); ttk.Entry(basic_settings_tab, textvariable=self.steps_var, width=5).grid(row=0, column=3, sticky="ew")
414
+ ttk.Label(basic_settings_tab, text="Scale:").grid(row=0, column=4, sticky="w", padx=(10, 5)); ttk.Entry(basic_settings_tab, textvariable=self.scale_var, width=5).grid(row=0, column=5, sticky="ew")
415
+ resolution_list = ["1024 x 1024", "960 x 1088", "896 x 1152", "832 x 1216", "1088 x 960", "1152 x 896", "1216 x 832", "์ž๋™ ํ•ด์ƒ๋„"]
416
+ ttk.Label(basic_settings_tab, text="Resolution:").grid(row=1, column=0, sticky="w", pady=(5, 0), padx=(0, 5)); resolution_cb = ttk.Combobox(basic_settings_tab, values=resolution_list, textvariable=self.resolution_var, state="readonly", width=15); resolution_cb.grid(row=1, column=1, sticky="ew", pady=(5, 0)); resolution_cb.set(resolution_list[2])
417
+ ttk.Label(basic_settings_tab, text="CFG Rescale:").grid(row=1, column=2, sticky="w", pady=(5, 0), padx=(10, 5)); ttk.Entry(basic_settings_tab, textvariable=self.cfg_rescale_var, width=5).grid(row=1, column=3, sticky="ew", pady=(5, 0))
418
+ ttk.Label(basic_settings_tab, text="Seed:").grid(row=1, column=4, sticky="w", pady=(5,0), padx=(10, 5)); seed_frame = ttk.Frame(basic_settings_tab); seed_frame.grid(row=1, column=5, sticky="ew", pady=(5,0)); seed_frame.grid_columnconfigure(0, weight=1); ttk.Entry(seed_frame, textvariable=self.seed_var).pack(side="left", fill="x", expand=True); ttk.Button(seed_frame, text="๐ŸŽฒ", width=2, command=self.random_seed).pack(side="right", padx=(5,0))
419
+ uc_header_frame = ttk.Frame(basic_settings_tab)
420
+ uc_header_frame.grid(row=2, column=0, columnspan=6, sticky="ew", pady=(8, 2))
421
+
422
+ # 2. ์ปจํ…Œ์ด๋„ˆ ํ”„๋ ˆ์ž„ ๋‚ด๋ถ€์— ๊ธฐ์กด ๋ ˆ์ด๋ธ”๊ณผ ์‹ ๊ทœ ์ฒดํฌ๋ฐ•์Šค ๋ฐฐ์น˜
423
+ ttk.Label(uc_header_frame, text="Negative Prompt (UC):").pack(side="left")
424
+
425
+ auto_save_cb = ttk.Checkbutton(uc_header_frame, text="NAI ์ƒ์„ฑ ์ด๋ฏธ์ง€๋ฅผ ์ž๋™์œผ๋กœ ์ €์žฅ", variable=self.auto_save_var)
426
+ auto_save_cb.pack(side="right")
427
+
428
+ # UC ์ž…๋ ฅ์ฐฝ์€ ๋‹ค์Œ ํ–‰(row=3)์— ๋ฐฐ์น˜
429
+ self.uc_text = scrolledtext.ScrolledText(basic_settings_tab, height=4, wrap=tk.WORD)
430
+ self.uc_text.grid(row=3, column=0, columnspan=6, sticky="nsew")
431
+
432
+ # 2. '์บ๋ฆญํ„ฐ ํ”„๋กฌํ”„ํŠธ' ํƒญ ๋‚ด์šฉ ๊ตฌ์„ฑ
433
+ char_prompt_tab.grid_columnconfigure(0, weight=1)
434
+ for i in range(3):
435
+ # ๊ฐ ์บ๋ฆญํ„ฐ ๋ผ์ธ์„ ๋‹ด์„ ํ”„๋ ˆ์ž„
436
+ char_frame = ttk.Frame(char_prompt_tab, padding=(0, 0, 0, 5))
437
+ # <<<<<<< Step 1: ์ •์  ๊ทธ๋ฆฌ๋“œ ๋ฐฐ์น˜ >>>>>>>>>
438
+ char_frame.grid(row=i, column=0, sticky="ew", pady=2) # ์ฆ‰์‹œ ๊ณ ์ • ๋ฐฐ์น˜
439
+
440
+ # <<<<<<< Step 3: ๋‹จ์ผ ํ–‰ ๋ ˆ์ด์•„์›ƒ ๋ฐ 3:2 ๋น„์œจ ์„ค์ • >>>>>>>>>
441
+ # char_frame.grid_columnconfigure(2, weight=3) # P ์ž…๋ ฅ์ฐฝ ๊ฐ€๋กœ ๋น„์œจ
442
+ # char_frame.grid_columnconfigure(4, weight=2) # UC ์ž…๋ ฅ์ฐฝ ๊ฐ€๋กœ ๋น„์œจ
443
+
444
+ # ์œ„์ ฏ ์ƒ์„ฑ
445
+ check_btn = ttk.Checkbutton(char_frame, text=f"C{i+1}", variable=self.char_check_vars[i],
446
+ command=self.update_character_layout) # style ์ œ๊ฑฐ
447
+
448
+ prompt_label = ttk.Label(char_frame, text="P:")
449
+ prompt_text = scrolledtext.ScrolledText(char_frame, height=1, wrap=tk.WORD, width=46)
450
+
451
+ uc_label = ttk.Label(char_frame, text="UC:")
452
+ uc_text = scrolledtext.ScrolledText(char_frame, height=1, wrap=tk.WORD, width=24)
453
+
454
+ # ์œ„์ ฏ ๋ฐฐ์น˜ (๋‹จ์ผ ํ–‰ ๊ตฌ์กฐ)
455
+ check_btn.grid(row=0, column=0, sticky="w", padx=(0, 5))
456
+ prompt_label.grid(row=0, column=1, sticky="w", padx=(0, 2))
457
+ # <<<<<<< Step 3: sticky="ew"๋กœ ์ˆ˜์ • >>>>>>>>>
458
+ prompt_text.grid(row=0, column=2, sticky="ew")
459
+
460
+ uc_label.grid(row=0, column=3, sticky="w", padx=(5, 2))
461
+ uc_text.grid(row=0, column=4, sticky="ew") # sticky="ew"๋กœ ์ˆ˜์ •
462
+
463
+ self.character_prompts.append({
464
+ "frame": char_frame,
465
+ "check_var": self.char_check_vars[i],
466
+ "prompt_widget": prompt_text,
467
+ "uc_widget": uc_text
468
+ })
469
+
470
+ # 3. '๊ณ„์ •์„ค์ •' ํƒญ ๋‚ด์šฉ ๊ตฌ์„ฑ
471
+ token_frame = ttk.LabelFrame(account_settings_tab, text="Novel AI ์˜๊ตฌ ํ† ํฐ ์ž…๋ ฅ")
472
+ token_frame.pack(fill="x", expand=False, padx=5, pady=5)
473
+ token_frame.grid_columnconfigure(0, weight=1)
474
+ token_entry = ttk.Entry(token_frame, textvariable=self.nai_token_var, show="*")
475
+ token_entry.grid(row=0, column=0, sticky="ew", padx=(5,5), pady=5)
476
+ verify_btn = ttk.Button(token_frame, text="๊ฒ€์ฆ", command=self.verify_token)
477
+ verify_btn.grid(row=0, column=1, sticky="e", padx=(0,5), pady=5)
478
+ model_frame = ttk.LabelFrame(account_settings_tab, text="๋ชจ๋ธ ์„ค์ •")
479
+ model_frame.pack(fill="x", expand=False, padx=5, pady=(10, 5))
480
+ model_frame.grid_columnconfigure(0, weight=1)
481
+ model_list = ["nai-diffusion-4-5-full"]
482
+ model_cb = ttk.Combobox(model_frame, values=model_list, textvariable=self.model_var, state="disabled")
483
+ model_cb.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
484
+ model_cb.set(model_list[0])
485
+
486
+
487
+ # --- UI ์ดˆ๊ธฐ ์ƒํƒœ ์„ค์ • ---
488
+ self.show_generation_preview()
489
+ self.update_character_layout()
490
+ self.toggle_expand_ui()
491
+
492
+ def update_character_layout(self):
493
+ for p_info in self.character_prompts:
494
+ if p_info['check_var'].get():
495
+ # ์ฒดํฌ๋œ ๊ฒฝ์šฐ: ์ž…๋ ฅ์ฐฝ ํ™œ์„ฑํ™”
496
+ p_info['prompt_widget'].config(state=tk.NORMAL)
497
+ p_info['uc_widget'].config(state=tk.NORMAL)
498
+ else:
499
+ # ์ฒดํฌ ํ•ด์ œ๋œ ๊ฒฝ์šฐ: ์ž…๋ ฅ์ฐฝ ๋น„ํ™œ์„ฑํ™”
500
+ p_info['prompt_widget'].config(state=tk.DISABLED)
501
+ p_info['uc_widget'].config(state=tk.DISABLED)
502
+
503
+ def verify_token(self):
504
+ """[๊ฒ€์ฆ] ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํ† ํฐ ๊ฒ€์ฆ ์Šค๋ ˆ๋“œ๋ฅผ ์‹œ์ž‘ํ•˜๋Š” ๋ฉ”์„œ๋“œ"""
505
+ token = self.nai_token_var.get()
506
+ if not token:
507
+ messagebox.showwarning("์ž…๋ ฅ ์˜ค๋ฅ˜", "ํ† ํฐ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
508
+ return
509
+
510
+ # ๊ฒ€์ฆ ์‹œ์ž‘์„ ์•Œ๋ฆผ (์„ ํƒ ์‚ฌํ•ญ)
511
+ self.root.config(cursor="watch")
512
+
513
+ # ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ API ์š”์ฒญ ์‹คํ–‰
514
+ thread = threading.Thread(target=self._verify_token_thread, args=(token,))
515
+ thread.daemon = True
516
+ thread.start()
517
+
518
+ def _verify_token_thread(self, token):
519
+ """(์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰) ์‹ค์ œ API ์š”์ฒญ ๋ฐ ๊ฒ€์ฆ ๋กœ์ง"""
520
+ import requests
521
+
522
+ try:
523
+ response = requests.get(
524
+ "https://api.novelai.net/user/subscription",
525
+ headers={"Authorization": f"Bearer {token}"},
526
+ timeout=5 # 5์ดˆ ์ด์ƒ ์‘๋‹ต ์—†์œผ๋ฉด ํƒ€์ž„์•„์›ƒ
527
+ )
528
+ response.raise_for_status() # 200๋ฒˆ๋Œ€ ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ
529
+
530
+ data_dict = response.json()
531
+
532
+ # Opus ๋“ฑ๊ธ‰(unlimitedMaxPriority) ๊ตฌ๋… ์—ฌ๋ถ€ ํ™•์ธ
533
+ if data_dict.get('perks', {}).get('unlimitedMaxPriority', False):
534
+ result_message = "Opus ๋“ฑ๊ธ‰ ๊ตฌ๋…์ด ํ™•์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
535
+ result_type = "info"
536
+ else:
537
+ result_message = "์œ ํšจํ•œ ํ† ํฐ์ด๋‚˜ Opus ๋“ฑ๊ธ‰ ๊ตฌ๋…์ด ์•„๋‹™๋‹ˆ๋‹ค."
538
+ result_type = "warning"
539
+
540
+ except requests.exceptions.HTTPError as e:
541
+ if e.response.status_code == 401:
542
+ result_message = "์ธ์ฆ ์‹คํŒจ: ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค."
543
+ else:
544
+ result_message = f"HTTP ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e.response.status_code}"
545
+ result_type = "error"
546
+ except requests.exceptions.RequestException as e:
547
+ result_message = f"๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜: API ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n{e}"
548
+ result_type = "error"
549
+ except Exception as e:
550
+ result_message = f"์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}"
551
+ result_type = "error"
552
+
553
+ # GUI ์—…๋ฐ์ดํŠธ๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ
554
+ self.root.after(0, self._update_verification_result, result_type, result_message)
555
+
556
+ def _update_verification_result(self, result_type, message):
557
+ """(๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰) ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ messagebox๋กœ ํ‘œ์‹œ"""
558
+ self.root.config(cursor="") # ์ปค์„œ ์›๋ž˜๋Œ€๋กœ
559
+
560
+ if result_type == "info":
561
+ messagebox.showinfo("๊ฒ€์ฆ ์„ฑ๊ณต", message)
562
+ elif result_type == "warning":
563
+ messagebox.showwarning("๊ฒ€์ฆ ํ™•์ธ", message)
564
+ else: # error
565
+ messagebox.showerror("๊ฒ€์ฆ ์‹คํŒจ", message)
566
+
567
+ def init_filter_data(self):
568
+ try:
569
+ from tagbag import bag_of_tags, clothes_list
570
+ from character_dictionary import character_dictionary as cd
571
+
572
+ self.bag_of_tags = bag_of_tags
573
+ self.clothes_list = clothes_list
574
+ self.character_keys = list(cd.keys()) if isinstance(cd, dict) else []
575
+
576
+ except ImportError as e:
577
+ print(f"โš  NAIA ํŒŒ์ผ import ์‹คํŒจ: {e}")
578
+ print("๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
579
+ self.bag_of_tags = [
580
+ 'smile', 'blush', 'open_mouth', 'closed_eyes', 'wink', 'frown',
581
+ 'twintails', 'ponytail', 'braid', 'long_hair', 'short_hair',
582
+ 'large_breasts', 'small_breasts', 'cleavage', 'navel'
583
+ ]
584
+ self.clothes_list = [
585
+ 'dress', 'skirt', 'shirt', 'jacket', 'uniform', 'school_uniform'
586
+ ]
587
+ self.character_keys = []
588
+
589
+ self.colors = ['black', 'white', 'blond', 'silver', 'gray', 'yellow',
590
+ 'blue', 'purple', 'red', 'pink', 'brown', 'orange',
591
+ 'green', 'aqua', 'gradient']
592
+
593
+ def toggle_removed_tags(self):
594
+ if self.show_removed_tags_var.get():
595
+ self.removed_tags_text.grid(row=3, column=0, sticky="nsew", pady=(5, 0))
596
+ else:
597
+ self.removed_tags_text.grid_forget()
598
+
599
+ def toggle_ignore_tags(self):
600
+ if self.show_ignore_tags_var.get():
601
+ self.ignore_tags_frame.grid(row=1, column=0, sticky="nsew")
602
+ else:
603
+ self.ignore_tags_frame.grid_forget()
604
+
605
+ def toggle_prompts(self):
606
+ if self.prompt_active_var.get():
607
+ self.prompt_notebook.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(5, 0))
608
+ else:
609
+ self.prompt_notebook.grid_forget()
610
+
611
+ def on_closing(self):
612
+ self.save_settings()
613
+ self.root.destroy()
614
+
615
+ def save_settings(self):
616
+ """ํ˜„์žฌ UI์˜ ๋ชจ๋“  ์„ค์ •์„ JSON ํŒŒ์ผ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค."""
617
+ settings = {
618
+ # ์ขŒ์ธก ํŒจ๋„ ์„ค์ •
619
+ 'general_threshold': self.general_threshold_var.get(),
620
+ 'char_threshold': self.char_threshold_var.get(),
621
+ 'rm_c': self.rm_c_var.get(),
622
+ 'rm_color': self.rm_color_var.get(),
623
+ 'rm_clothes': self.rm_clothes_var.get(),
624
+ 'webui_mode': self.webui_mode_var.get(),
625
+ 'instant_inference': self.instant_inference_var.get(),
626
+ 'ignore_tags': self.ignore_tags_text.get(1.0, tk.END).strip(),
627
+ 'show_removed_tags': self.show_removed_tags_var.get(),
628
+ 'show_ignore_tags': self.show_ignore_tags_var.get(),
629
+ 'prompt_active': self.prompt_active_var.get(),
630
+ 'prefix_prompt': self.prefix_text.get(1.0, tk.END).strip(),
631
+ 'suffix_prompt': self.suffix_text.get(1.0, tk.END).strip(),
632
+
633
+ # ์šฐ์ธก ํŒจ๋„ (์ƒ์„ฑ UI) ์„ค์ •
634
+ 'expand_ui': self.expand_ui_var.get(),
635
+ 'sampler': self.sampler_var.get(),
636
+ 'resolution': self.resolution_var.get(),
637
+ 'steps': self.steps_var.get(),
638
+ 'scale': self.scale_var.get(),
639
+ 'seed': self.seed_var.get(),
640
+ 'cfg_rescale': self.cfg_rescale_var.get(),
641
+ 'uc_prompt': self.uc_text.get(1.0, tk.END).strip(),
642
+ 'nai_token': self.nai_token_var.get()
643
+ }
644
+ settings['auto_save'] = self.auto_save_var.get()
645
+ settings['auto_rating_tag'] = self.auto_rating_tag_var.get()
646
+ # ์บ๋ฆญํ„ฐ ํ”„๋กฌํ”„ํŠธ ์„ค์ • ์ €์žฅ
647
+ char_prompts_data = []
648
+ for p_info in self.character_prompts:
649
+ char_prompts_data.append({
650
+ "checked": p_info['check_var'].get(),
651
+ "prompt": p_info['prompt_widget'].get(1.0, tk.END).strip(),
652
+ "uc": p_info['uc_widget'].get(1.0, tk.END).strip()
653
+ })
654
+ settings['character_prompts'] = char_prompts_data
655
+ try:
656
+ with open(self.SETTINGS_FILE, 'w', encoding='utf-8') as f:
657
+ json.dump(settings, f, indent=4)
658
+ except Exception as e:
659
+ messagebox.showerror("์ €์žฅ ์‹คํŒจ", f"์„ค์ • ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {e}")
660
+
661
+ def load_settings(self):
662
+ """JSON ํŒŒ์ผ์—์„œ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์™€ UI์— ์ ์šฉํ•ฉ๋‹ˆ๋‹ค."""
663
+ try:
664
+ if os.path.exists(self.SETTINGS_FILE):
665
+ with open(self.SETTINGS_FILE, 'r', encoding='utf-8') as f:
666
+ settings = json.load(f)
667
+
668
+ # ์ขŒ์ธก ํŒจ๋„ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
669
+ self.general_threshold_var.set(settings.get('general_threshold', 0.5))
670
+ self.char_threshold_var.set(settings.get('char_threshold', 0.85))
671
+ self.rm_c_var.set(settings.get('rm_c', False))
672
+ self.rm_color_var.set(settings.get('rm_color', False))
673
+ self.rm_clothes_var.set(settings.get('rm_clothes', False))
674
+ self.webui_mode_var.set(settings.get('webui_mode', False))
675
+ self.instant_inference_var.set(settings.get('instant_inference', False))
676
+ self.ignore_tags_text.delete(1.0, tk.END)
677
+ self.ignore_tags_text.insert(1.0, settings.get('ignore_tags', ''))
678
+ self.show_removed_tags_var.set(settings.get('show_removed_tags', True))
679
+ self.show_ignore_tags_var.set(settings.get('show_ignore_tags', False))
680
+ self.prompt_active_var.set(settings.get('prompt_active', False))
681
+ self.prefix_text.delete(1.0, tk.END)
682
+ self.prefix_text.insert(1.0, settings.get('prefix_prompt', ''))
683
+ self.suffix_text.delete(1.0, tk.END)
684
+ self.suffix_text.insert(1.0, settings.get('suffix_prompt', ''))
685
+
686
+ # ์šฐ์ธก ํŒจ๋„ (์ƒ์„ฑ UI) ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
687
+ self.expand_ui_var.set(settings.get('expand_ui', False))
688
+ self.sampler_var.set(settings.get('sampler', 'k_euler_ancestral + karras'))
689
+ self.resolution_var.set(settings.get('resolution', '896 x 1152'))
690
+ self.steps_var.set(settings.get('steps', '28'))
691
+ self.scale_var.set(settings.get('scale', '5.8'))
692
+ self.seed_var.set(settings.get('seed', '-1'))
693
+ self.cfg_rescale_var.set(settings.get('cfg_rescale', '0.25'))
694
+ self.uc_text.delete(1.0, tk.END)
695
+ self.uc_text.insert(1.0, settings.get('uc_prompt', ''))
696
+ self.nai_token_var.set(settings.get('nai_token', ''))
697
+ self.auto_save_var.set(settings.get('auto_save', True))
698
+ self.auto_rating_tag_var.set(settings.get('auto_rating_tag', True))
699
+
700
+ # ์บ๋ฆญํ„ฐ ํ”„๋กฌํ”„ํŠธ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
701
+ char_prompts_data = settings.get('character_prompts', [])
702
+ for i, data in enumerate(char_prompts_data):
703
+ if i < len(self.character_prompts):
704
+ p_info = self.character_prompts[i]
705
+ p_info['check_var'].set(data.get('checked', i==0)) # ๊ธฐ๋ณธ๊ฐ’์€ ์ฒซ๋ฒˆ์งธ๋งŒ True
706
+
707
+ p_info['prompt_widget'].delete(1.0, tk.END)
708
+ p_info['prompt_widget'].insert(1.0, data.get('prompt', ''))
709
+
710
+ p_info['uc_widget'].delete(1.0, tk.END)
711
+ p_info['uc_widget'].insert(1.0, data.get('uc', ''))
712
+
713
+ # UI ์ƒํƒœ ์—…๋ฐ์ดํŠธ
714
+ self.toggle_removed_tags()
715
+ self.toggle_ignore_tags()
716
+ self.toggle_prompts()
717
+ self.update_character_layout()
718
+ self.toggle_expand_ui() # ์ €์žฅ๋œ ๊ฐ’์— ๋”ฐ๋ผ ํ™•์žฅ UI ์ƒํƒœ ๊ฒฐ์ •
719
+
720
+ except Exception as e:
721
+ messagebox.showwarning("์„ค์ • ๋กœ๋“œ ์‹คํŒจ", f"์„ค์ • ํŒŒ์ผ({self.SETTINGS_FILE})์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: {e}")
722
+
723
+ def handle_paste(self, event):
724
+ try:
725
+ clipboard_image = ImageGrab.grabclipboard()
726
+ if isinstance(clipboard_image, Image.Image):
727
+ self.load_from_clipboard()
728
+ return "break"
729
+ except (ValueError, TypeError):
730
+ pass
731
+ except Exception as e:
732
+ print(f"ํด๋ฆฝ๋ณด๋“œ ์ด๋ฏธ์ง€ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜: {e}")
733
+
734
+ try:
735
+ clipboard_text = self.root.clipboard_get()
736
+ if isinstance(clipboard_text, str) and clipboard_text.startswith(('http://', 'https://')):
737
+ self._load_image_from_url(clipboard_text)
738
+ return "break"
739
+
740
+ image_extensions = ('.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff')
741
+ if isinstance(clipboard_text, str) and clipboard_text.lower().endswith(image_extensions):
742
+ path = clipboard_text.strip('"')
743
+ if os.path.exists(path):
744
+ self._load_image_from_path(path)
745
+ return "break"
746
+
747
+ except tk.TclError:
748
+ pass
749
+ except Exception as e:
750
+ messagebox.showerror("๋ถ™์—ฌ๋„ฃ๊ธฐ ์˜ค๋ฅ˜", f"์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
751
+ return "break"
752
+
753
+ return
754
+
755
+ def _load_image_from_url(self, url):
756
+ try:
757
+ img = self.download_image_from_url(url)
758
+ temp_dir = tempfile.gettempdir()
759
+ temp_file = os.path.join(temp_dir, f"wd14_url_{os.getpid()}.png")
760
+ img.save(temp_file)
761
+
762
+ self.current_image_path = temp_file
763
+ self.show_preview(temp_file)
764
+ if self.session is not None:
765
+ self.extract_btn.config(state='normal')
766
+ if self.instant_inference_var.get():
767
+ self.extract_tags()
768
+ except Exception as e:
769
+ messagebox.showerror("์˜ค๋ฅ˜", f"์›น ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ: {str(e)}")
770
+
771
+ def load_model(self):
772
+ try:
773
+ import onnxruntime as ort
774
+
775
+ model_path = resource_path("model.onnx")
776
+ tags_path = resource_path("selected_tags.csv")
777
+
778
+ self.session = ort.InferenceSession(model_path, providers=['CPUExecutionProvider'])
779
+ self.tags_df = pd.read_csv(tags_path)
780
+
781
+ except Exception as e:
782
+ messagebox.showerror("์˜ค๋ฅ˜", f"๋ชจ๋ธ ๋กœ๋“œ ์‹คํŒจ: {str(e)}")
783
+
784
+ def show_initial_preview(self):
785
+ try:
786
+ white_image = Image.new('RGB', (290, 290), '#F0F0F0')
787
+ photo = ImageTk.PhotoImage(white_image)
788
+ self.image_label.config(image=photo, text="์ด๋ฏธ์ง€๋ฅผ Drag & Drop\n๋˜๋Š” ๋ถ™์—ฌ๋„ฃ๊ธฐ(Ctrl+V) ํ•ด์ฃผ์„ธ์š”", compound="center", foreground="gray")
789
+ self.image_label.image = photo
790
+ except Exception as e:
791
+ self.image_label.config(text="์ด๋ฏธ์ง€๋ฅผ Drag & Drop ํ•ด์ฃผ์„ธ์š”")
792
+
793
+ def setup_drag_drop(self):
794
+ try:
795
+ from tkinterdnd2 import TkinterDnD, DND_ALL
796
+ self.TkdndVersion = TkinterDnD._require(self.root)
797
+ self.image_label.drop_target_register(DND_ALL)
798
+ self.image_label.dnd_bind('<<Drop>>', self.on_drop)
799
+ self.image_label.dnd_bind('<<DragEnter>>', self.on_drag_enter)
800
+ self.image_label.dnd_bind('<<DragLeave>>', self.on_drag_leave)
801
+ except ImportError:
802
+ print("tkinterdnd2๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์Œ. pip install tkinterdnd2๋กœ ์„ค์น˜ํ•˜๋ฉด ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
803
+ except Exception as e:
804
+ print(f"Drag & Drop ์ดˆ๊ธฐํ™” ์˜ค๋ฅ˜: {e}")
805
+
806
+ def on_drag_enter(self, event):
807
+ try:
808
+ self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ด๋ฏธ์ง€๋ฅผ ๋†“์•„์ฃผ์„ธ์š”!)")
809
+ except: pass
810
+
811
+ def on_drag_leave(self, event):
812
+ try:
813
+ self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
814
+ except: pass
815
+
816
+ def download_image_from_url(self, url):
817
+ import requests
818
+ response = requests.get(url)
819
+ response.raise_for_status()
820
+ return Image.open(io.BytesIO(response.content))
821
+
822
+ def on_drop(self, event):
823
+ try:
824
+ if not isinstance(event, str):
825
+ file_path_or_url = event.data.strip("{}")
826
+ else:
827
+ file_path_or_url = event
828
+
829
+ if not file_path_or_url:
830
+ messagebox.showwarning("๊ฒฝ๊ณ ", "๋“œ๋กญ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
831
+ self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
832
+ return
833
+
834
+ if file_path_or_url.startswith(('http://', 'https://')):
835
+ self._load_image_from_url(file_path_or_url)
836
+ self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
837
+ return
838
+ else:
839
+ if file_path_or_url.startswith(('blob:')):
840
+ messagebox.showwarning("Blob URL ์˜ค๋ฅ˜", "NAI ํ™ˆํŽ˜์ด์ง€์—์„œ ์ƒ์„ฑํ•œ ์ด๋ฏธ์ง€๋Š” ์ฆ‰์‹œ ๋ณต์‚ฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n์ด๋ฏธ์ง€๋ฅผ ๋‹ค์šด๋กœ๋“œํ•œ ํ›„ ๋“œ๋ž˜๊ทธ & ๋“œ๋กญํ•ด์ฃผ์„ธ์š”.")
841
+ self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
842
+ return
843
+
844
+ file_path_or_url = file_path_or_url.replace("\\", '/')
845
+ if os.path.exists(file_path_or_url):
846
+ self.current_image_path = file_path_or_url
847
+ self.show_preview(file_path_or_url)
848
+ if self.session is not None:
849
+ self.extract_btn.config(state='normal')
850
+ if self.instant_inference_var.get():
851
+ self.extract_tags()
852
+ self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
853
+ else:
854
+ messagebox.showerror("์˜ค๋ฅ˜", f"์œ ํšจํ•˜์ง€ ์•Š์€ ํŒŒ์ผ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค: '{file_path_or_url}'")
855
+ self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
856
+
857
+ except Exception as e:
858
+ messagebox.showerror("์˜ค๋ฅ˜", f"ํŒŒ์ผ ๋“œ๋กญ ์˜ค๋ฅ˜: {str(e)}")
859
+ self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
860
+
861
+ def _load_image_from_path(self, path):
862
+ if os.path.exists(path):
863
+ self.current_image_path = path
864
+ self.show_preview(path)
865
+ if self.session is not None:
866
+ self.extract_btn.config(state='normal')
867
+ if self.instant_inference_var.get():
868
+ self.extract_tags()
869
+ else:
870
+ messagebox.showwarning("๊ฒฝ๋กœ ์˜ค๋ฅ˜", f"ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {path}")
871
+
872
+ def load_from_clipboard(self):
873
+ try:
874
+ image = ImageGrab.grabclipboard()
875
+ if isinstance(image, Image.Image):
876
+ temp_dir = tempfile.gettempdir()
877
+ temp_file = os.path.join(temp_dir, f"wd14_clipboard_{os.getpid()}.png")
878
+ if image.mode == 'RGBA':
879
+ image = image.convert('RGB')
880
+ image.save(temp_file, 'PNG')
881
+
882
+ self.current_image_path = temp_file
883
+ self.show_preview(temp_file)
884
+ if self.session is not None:
885
+ self.extract_btn.config(state='normal')
886
+ if self.instant_inference_var.get():
887
+ self.extract_tags()
888
+ else:
889
+ messagebox.showinfo("์ •๋ณด", "ํด๋ฆฝ๋ณด๋“œ์— ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
890
+ except Exception as e:
891
+ messagebox.showerror("์˜ค๋ฅ˜", f"ํด๋ฆฝ๋ณด๋“œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์˜ค๋ฅ˜: {str(e)}")
892
+
893
+ def load_image(self):
894
+ file_path = filedialog.askopenfilename(
895
+ title="์ด๋ฏธ์ง€ ์„ ํƒ",
896
+ filetypes=[("์ด๋ฏธ์ง€ ํŒŒ์ผ", "*.jpg *.jpeg *.png *.bmp *.tiff *.webp")]
897
+ )
898
+ if file_path:
899
+ self.current_image_path = file_path
900
+ self.show_preview(file_path)
901
+ if self.session is not None:
902
+ self.extract_btn.config(state='normal')
903
+ if self.instant_inference_var.get():
904
+ self.extract_tags()
905
+
906
+ def show_preview(self, image_path):
907
+ try:
908
+ image = Image.open(image_path).convert('RGB')
909
+ size = max(image.size)
910
+ square_image = Image.new('RGB', (size, size), 'white')
911
+ paste_x = (size - image.width) // 2
912
+ paste_y = (size - image.height) // 2
913
+ square_image.paste(image, (paste_x, paste_y))
914
+ square_image.thumbnail((290, 290), Image.Resampling.LANCZOS)
915
+ photo = ImageTk.PhotoImage(square_image)
916
+ self.image_label.config(image=photo, text="")
917
+ self.image_label.image = photo
918
+ except Exception as e:
919
+ self.image_label.config(text=f"์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ: {str(e)}")
920
+
921
+ def extract_tags(self):
922
+ if not self.current_image_path or not self.session:
923
+ return
924
+ self.extract_btn.config(state='disabled', text='์ถ”์ถœ ์ค‘...')
925
+ self.result_text.delete(1.0, tk.END)
926
+ self.result_text.insert(tk.END, "ํƒœ๊ทธ ์ถ”์ถœ ์ค‘...")
927
+ self.removed_tags_text.config(state=tk.NORMAL)
928
+ self.removed_tags_text.delete(1.0, tk.END)
929
+ self.removed_tags_text.config(state=tk.DISABLED)
930
+ thread = threading.Thread(target=self._extract_tags_thread)
931
+ thread.daemon = True
932
+ thread.start()
933
+
934
+ def _extract_tags_thread(self):
935
+ try:
936
+ input_image = Image.open(self.current_image_path)
937
+ _, height, _, _ = self.session.get_inputs()[0].shape
938
+ image = input_image.convert('RGBA')
939
+ new_image = Image.new('RGBA', image.size, 'WHITE')
940
+ new_image.paste(image, mask=image)
941
+ image = new_image.convert('RGB')
942
+ image = np.asarray(image)[:, :, ::-1]
943
+ image = self.make_square(image, height)
944
+ image = self.smart_resize(image, height)
945
+ image = image.astype(np.float32)
946
+ image = np.expand_dims(image, 0)
947
+ input_name = self.session.get_inputs()[0].name
948
+ label_name = self.session.get_outputs()[0].name
949
+ confidents = self.session.run([label_name], {input_name: image})[0]
950
+ tags_with_conf = []
951
+ for i, conf in enumerate(confidents[0]):
952
+ if i < len(self.tags_df):
953
+ tags_with_conf.append((self.tags_df.iloc[i]['name'], float(conf)))
954
+ rating_tags = dict(tags_with_conf[:4])
955
+ general_tags = dict(tags_with_conf[4:])
956
+ best_rating = max(rating_tags, key=rating_tags.get) if rating_tags else None
957
+
958
+ result_tuple = self._format_results(general_tags, best_rating) # best_rating ์ „๋‹ฌ
959
+ self.root.after(0, self._update_results, result_tuple)
960
+ except Exception as e:
961
+ error_msg = f"ํƒœ๊ทธ ์ถ”์ถœ ์˜ค๋ฅ˜: {str(e)}"
962
+ self.root.after(0, self._update_results, ((None, error_msg, "")))
963
+
964
+ def apply_tag_filters(self, tags_list, best_rating=None):
965
+ original_tags = tags_list.copy()
966
+ tags_to_process = [tag.replace('_', ' ') for tag in original_tags]
967
+
968
+ final_indices = list(range(len(tags_to_process)))
969
+
970
+ user_removed_originals = []
971
+ other_removed_originals = []
972
+
973
+ ignore_list_str = self.ignore_tags_text.get(1.0, tk.END).strip()
974
+ if ignore_list_str:
975
+ user_ignored_tags = {tag.strip().replace('_', ' ') for tag in ignore_list_str.split(',') if tag.strip()}
976
+ indices_to_remove = {i for i in final_indices if tags_to_process[i] in user_ignored_tags}
977
+ for i in indices_to_remove:
978
+ user_removed_originals.append(original_tags[i])
979
+ final_indices = [i for i in final_indices if i not in indices_to_remove]
980
+
981
+ if self.rm_c_var.get():
982
+ indices_to_remove = {i for i in final_indices if tags_to_process[i] in self.bag_of_tags or self._is_character_name(tags_to_process[i])}
983
+ for i in indices_to_remove:
984
+ other_removed_originals.append(original_tags[i])
985
+ final_indices = [i for i in final_indices if i not in indices_to_remove]
986
+
987
+ if self.rm_color_var.get():
988
+ indices_to_remove = {i for i in final_indices if any(color+" " in tags_to_process[i] for color in self.colors) and " eyes" not in tags_to_process[i] and " hair" not in tags_to_process[i] and " pupils" not in tags_to_process[i]}
989
+ for i in indices_to_remove:
990
+ other_removed_originals.append(original_tags[i])
991
+ final_indices = [i for i in final_indices if i not in indices_to_remove]
992
+
993
+ if self.rm_clothes_var.get():
994
+ indices_to_remove = {i for i in final_indices if tags_to_process[i] in self.clothes_list and tags_to_process[i] not in self.bag_of_tags}
995
+ for i in indices_to_remove:
996
+ other_removed_originals.append(original_tags[i])
997
+ final_indices = [i for i in final_indices if i not in indices_to_remove]
998
+
999
+ final_tags_with_space = [tags_to_process[i] for i in final_indices]
1000
+
1001
+
1002
+ if self.auto_rating_tag_var.get() and best_rating:
1003
+ prefix_str = self.prefix_text.get(1.0, tk.END)
1004
+ suffix_str = self.suffix_text.get(1.0, tk.END)
1005
+
1006
+ if not self.webui_mode_var.get():
1007
+ best_rating = "rating:" + best_rating
1008
+
1009
+ tag_to_add = None
1010
+ if best_rating in ["rating:explicit", "rating:questionable", "questionable", "explicit"]:
1011
+ if "nsfw" not in prefix_str and "nsfw" not in suffix_str:
1012
+ tag_to_add = best_rating +", nsfw"
1013
+ elif best_rating in ["rating:general", "general"]:
1014
+ if "safe" not in prefix_str and "safe" not in suffix_str:
1015
+ tag_to_add = best_rating +", safe"
1016
+ else:
1017
+ tag_to_add = best_rating
1018
+
1019
+ # nsfw/safe ํƒœ๊ทธ ์ถ”๊ฐ€
1020
+ if tag_to_add:
1021
+ person_tags = {'1girl', '2girls', '3girls', '4girls', '5girls', '6+girls',
1022
+ '1boy', '2boys', '3boys', '4boys', '5boys', '6+boys'}
1023
+ insert_pos = 0
1024
+ for i, tag in enumerate(final_tags_with_space):
1025
+ if tag in person_tags:
1026
+ insert_pos = i + 1
1027
+ break
1028
+ final_tags_with_space.insert(insert_pos, tag_to_add)
1029
+
1030
+ if self.webui_mode_var.get():
1031
+ final_tags_formatted = [tag.replace('(', '\\(').replace(')', '\\)') for tag in final_tags_with_space]
1032
+ else:
1033
+ final_tags_formatted = final_tags_with_space
1034
+
1035
+ final_tags_with_type = [(tag, 'original') for tag in final_tags_formatted]
1036
+ if self.prompt_active_var.get():
1037
+ prefix_str = self.prefix_text.get(1.0, tk.END).strip()
1038
+ if prefix_str:
1039
+ prefix_tags = [t.strip() for t in prefix_str.split(',') if t.strip()]
1040
+ prefix_tags_with_type = [(tag, 'prompt') for tag in prefix_tags]
1041
+ person_tags = {'1girl', '2girls', '3girls', '4girls', '5girls', '6+girls',
1042
+ '1boy', '2boys', '3boys', '4boys', '5boys', '6+boys'}
1043
+ last_person_tag_index = -1
1044
+ search_range = min(len(final_tags_with_type), 4)
1045
+ for i in range(search_range):
1046
+ if final_tags_with_type[i][0] in person_tags:
1047
+ last_person_tag_index = i
1048
+ if last_person_tag_index != -1:
1049
+ final_tags_with_type[last_person_tag_index+1:last_person_tag_index+1] = prefix_tags_with_type
1050
+ else:
1051
+ final_tags_with_type[0:0] = prefix_tags_with_type
1052
+
1053
+ suffix_str = self.suffix_text.get(1.0, tk.END).strip()
1054
+ if suffix_str:
1055
+ suffix_tags = [t.strip() for t in suffix_str.split(',') if t.strip()]
1056
+ suffix_tags_with_type = [(tag, 'prompt') for tag in suffix_tags]
1057
+ final_tags_with_type.extend(suffix_tags_with_type)
1058
+
1059
+ return final_tags_with_type, user_removed_originals, list(set(other_removed_originals))
1060
+
1061
+ def _is_character_name(self, tag):
1062
+ if hasattr(self, 'character_keys') and tag in self.character_keys:
1063
+ return True
1064
+ if ('_' in tag and any(c.isupper() for c in tag) and
1065
+ not tag.startswith('1') and
1066
+ not any(word in tag.lower() for word in ['girl', 'boy', 'solo', 'multiple']) and
1067
+ not any(word in tag.lower() for word in ['hair', 'eye', 'dress', 'shirt']) and
1068
+ len(tag) > 3):
1069
+ return True
1070
+ return False
1071
+
1072
+ def _format_results(self, tags, best_rating=None):
1073
+ general_threshold = self.general_threshold_var.get()
1074
+ char_threshold = self.char_threshold_var.get()
1075
+
1076
+ person_tags = {'1girl', '2girls', '3girls', '4girls', '5girls', '6+girls',
1077
+ '1boy', '2boys', '3boys', '4boys', '5boys', '6+boys',
1078
+ 'multiple_girls', 'multiple_boys', 'solo', 'no_humans'}
1079
+
1080
+ filtered_tags = []
1081
+ for tag, conf in tags.items():
1082
+ if tag in person_tags:
1083
+ if conf >= 0.9: filtered_tags.append(tag)
1084
+ elif any(c.isupper() for c in tag) and '_' in tag and conf >= char_threshold:
1085
+ filtered_tags.append(tag)
1086
+ elif conf >= general_threshold:
1087
+ filtered_tags.append(tag)
1088
+
1089
+ if len(filtered_tags) > 99:
1090
+ filtered_tags = filtered_tags[:99]
1091
+
1092
+ final_tags_with_type, user_removed, other_removed = self.apply_tag_filters(filtered_tags, best_rating)
1093
+
1094
+ removed_for_display = []
1095
+ for tag in user_removed:
1096
+ removed_for_display.append((tag, 'ignored'))
1097
+ for tag in other_removed:
1098
+ if tag not in user_removed:
1099
+ removed_for_display.append((tag, 'other'))
1100
+
1101
+ return final_tags_with_type, removed_for_display
1102
+
1103
+ def copy_all_tags(self):
1104
+ content = self.result_text.get(1.0, tk.END).strip()
1105
+ if content:
1106
+ self.root.clipboard_clear()
1107
+ self.root.clipboard_append(content)
1108
+ else:
1109
+ messagebox.showwarning("๋ณต์‚ฌ ์‹คํŒจ", "๋ณต์‚ฌํ•  ํƒœ๊ทธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
1110
+
1111
+ def copy_from_second(self):
1112
+ self._copy_from_index(1, "2๋ฒˆ ํƒœ๊ทธ๋ถ€ํ„ฐ")
1113
+
1114
+ def copy_from_third(self):
1115
+ self._copy_from_index(2, "3๋ฒˆ ํƒœ๊ทธ๋ถ€ํ„ฐ")
1116
+
1117
+ def _copy_from_index(self, start_index, description):
1118
+ content = self.result_text.get(1.0, tk.END).strip()
1119
+ if content:
1120
+ tags_list = [tag.strip() for tag in content.split(',')]
1121
+ if len(tags_list) > start_index:
1122
+ selected_tags = tags_list[start_index:]
1123
+ result = ', '.join(selected_tags)
1124
+ self.root.clipboard_clear()
1125
+ self.root.clipboard_append(result)
1126
+ else:
1127
+ messagebox.showwarning("๋ณต์‚ฌ ์‹คํŒจ", f"ํƒœ๊ทธ๊ฐ€ {start_index + 1}๊ฐœ ๋ฏธ๋งŒ์ž…๋‹ˆ๋‹ค.")
1128
+ else:
1129
+ messagebox.showwarning("๋ณต์‚ฌ ์‹คํŒจ", "๋ณต์‚ฌํ•  ํƒœ๊ทธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
1130
+
1131
+ def _update_results(self, result_tuple):
1132
+ main_tags_info, removed_tags_info = result_tuple[0], result_tuple[1]
1133
+ if len(result_tuple) == 3: # ์—๋Ÿฌ๊ฐ€ ์•„๋‹Œ ์ •์ƒ ๊ฒฐ๊ณผ์ผ ๊ฒฝ์šฐ
1134
+ main_tags_info, removed_tags_info = result_tuple[1], result_tuple[2]
1135
+
1136
+ # ๋ฉ”์ธ ํƒœ๊ทธ ๋ฐ•์Šค ์—…๋ฐ์ดํŠธ
1137
+ self.result_text.delete(1.0, tk.END)
1138
+ if isinstance(main_tags_info, list):
1139
+ for i, (tag, tag_type) in enumerate(main_tags_info):
1140
+ if i > 0:
1141
+ self.result_text.insert(tk.END, ', ')
1142
+
1143
+ if tag_type == 'prompt':
1144
+ self.result_text.insert(tk.END, tag, 'prompt')
1145
+ else:
1146
+ self.result_text.insert(tk.END, tag)
1147
+ else: # ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
1148
+ self.result_text.insert(tk.END, main_tags_info)
1149
+
1150
+ self.removed_tags_text.config(state=tk.NORMAL)
1151
+ self.removed_tags_text.delete(1.0, tk.END)
1152
+ if removed_tags_info:
1153
+ for i, (tag, tag_type) in enumerate(removed_tags_info):
1154
+ display_tag = tag.replace('_', ' ')
1155
+
1156
+ if i > 0:
1157
+ self.removed_tags_text.insert(tk.END, ', ')
1158
+
1159
+ if tag_type == 'ignored':
1160
+ self.removed_tags_text.insert(tk.END, display_tag, 'ignored')
1161
+ else:
1162
+ self.removed_tags_text.insert(tk.END, display_tag)
1163
+ self.removed_tags_text.config(state=tk.DISABLED)
1164
+
1165
+ self.extract_btn.config(state='normal', text='ํƒœ๊ทธ ์ถ”์ถœ')
1166
+ if (self.instant_generate_var.get() and
1167
+ self.expand_ui_var.get() and
1168
+ self.is_generating == False):
1169
+ self.start_generation()
1170
+
1171
+ def make_square(self, img, target_size):
1172
+ old_size = img.shape[:2]
1173
+ desired_size = max(old_size)
1174
+ desired_size = max(desired_size, target_size)
1175
+ delta_w = desired_size - old_size[1]
1176
+ delta_h = desired_size - old_size[0]
1177
+ top, bottom = delta_h // 2, delta_h - (delta_h // 2)
1178
+ left, right = delta_w // 2, delta_w - (delta_w // 2)
1179
+ color = [255, 255, 255]
1180
+ new_im = cv2.copyMakeBorder(
1181
+ img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color
1182
+ )
1183
+ return new_im
1184
+
1185
+ def smart_resize(self, img, size):
1186
+ if img.shape[0] > size:
1187
+ img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA)
1188
+ elif img.shape[0] < size:
1189
+ img = cv2.resize(img, (size, size), interpolation=cv2.INTER_CUBIC)
1190
+ return img
1191
+
1192
+ def find_max_resolution(self, width, height, max_pixels=1048576, multiple_of=64):
1193
+ """๊ฐ€๋กœ์„ธ๋กœ ๋น„์œจ์„ ์œ ์ง€ํ•˜๋ฉฐ ์ตœ๋Œ€ ํ”ฝ์…€ ์ˆ˜์— ๋งž๋Š” ์ตœ๋Œ€ ํ•ด์ƒ๋„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค."""
1194
+ ratio = width / height
1195
+
1196
+ max_width = int((max_pixels * ratio)**0.5)
1197
+ max_height = int((max_pixels / ratio)**0.5)
1198
+
1199
+ max_width = (max_width // multiple_of) * multiple_of
1200
+ max_height = (max_height // multiple_of) * multiple_of
1201
+
1202
+ while max_width * max_height > max_pixels:
1203
+ max_width -= multiple_of
1204
+ max_height = int(max_width / ratio)
1205
+ max_height = (max_height // multiple_of) * multiple_of
1206
+
1207
+ return (max_width, max_height)
1208
+
1209
+ def start_generation(self):
1210
+ if not self.nai_token_var.get():
1211
+ messagebox.showerror("ํ† ํฐ ์˜ค๋ฅ˜", "NAI ์„ค์ • ํƒญ์—์„œ API ํ† ํฐ์„ ๋จผ์ € ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
1212
+ return
1213
+ if not self.result_text.get(1.0, tk.END).strip():
1214
+ messagebox.showerror("ํ”„๋กฌํ”„ํŠธ ์˜ค๋ฅ˜", "์ขŒ์ธก '์ถ”์ถœ๋œ ํƒœ๊ทธ'์— ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
1215
+ return
1216
+
1217
+ # --- ์ˆ˜์ •๋œ ๋ถ€๋ถ„ ---
1218
+ self.is_generating = True
1219
+ self.generation_start_time = time.time()
1220
+ self.generation_btn.config(state=tk.DISABLED)
1221
+ self.save_btn.config(state=tk.NORMAL)
1222
+ self.generation_image_label.config(image=None, text="๐ŸŽจ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘...")
1223
+ self._update_timer() # ํƒ€์ด๋จธ ์‹œ์ž‘
1224
+
1225
+ thread = threading.Thread(target=self._generation_thread)
1226
+ thread.daemon = True
1227
+ thread.start()
1228
+
1229
+ def _generation_thread(self):
1230
+ """(๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ) ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆ˜์ง‘, API ์š”์ฒญ ๋ฐ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜"""
1231
+ try:
1232
+ # 1. ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆ˜์ง‘ ๋ฐ ์ „์ฒ˜๋ฆฌ
1233
+ access_token = self.nai_token_var.get()
1234
+ model_name = self.model_var.get()
1235
+ main_prompt = self.result_text.get(1.0, tk.END).strip()
1236
+ negative_prompt = self.uc_text.get(1.0, tk.END).strip()
1237
+
1238
+ # ํ•ด์ƒ๋„ ์ฒ˜๋ฆฌ
1239
+ resolution_str = self.resolution_var.get()
1240
+ if resolution_str == "์ž๋™ ํ•ด์ƒ๋„":
1241
+ if self.current_image_path and os.path.exists(self.current_image_path):
1242
+ with Image.open(self.current_image_path) as img:
1243
+ w, h = img.size
1244
+ width, height = self.find_max_resolution(w, h)
1245
+ else: # ๋ถˆ๋Ÿฌ์˜จ ์ด๋ฏธ์ง€๊ฐ€ ์—†์œผ๋ฉด 1:1 ๋น„์œจ๋กœ ๊ณ„์‚ฐ
1246
+ width, height = self.find_max_resolution(1024, 1024)
1247
+ else:
1248
+ parts = resolution_str.split(' x ')
1249
+ width, height = int(parts[0]), int(parts[1])
1250
+
1251
+ # ์ƒ˜ํ”Œ๋Ÿฌ ๋ฐ ์Šค์ผ€์ค„๋Ÿฌ ๋ถ„๋ฆฌ
1252
+ sampler_parts = self.sampler_var.get().split(' + ')
1253
+ sampler = sampler_parts[0]
1254
+ noise_schedule = sampler_parts[1] if len(sampler_parts) > 1 else 'karras' # ๊ธฐ๋ณธ๊ฐ’
1255
+
1256
+ seed = int(self.seed_var.get())
1257
+ if seed == -1:
1258
+ seed = random.randint(0, 9999999999)
1259
+ self.last_generated_seed = seed
1260
+
1261
+ # 2. API Payload ๊ตฌ์„ฑ
1262
+ parameters = {
1263
+ "width": width,
1264
+ "height": height,
1265
+ "n_samples": 1,
1266
+ "seed": seed,
1267
+ "extra_noise_seed": seed,
1268
+ "sampler": sampler,
1269
+ "steps": int(self.steps_var.get()),
1270
+ "scale": float(self.scale_var.get()),
1271
+ "negative_prompt": negative_prompt,
1272
+ "cfg_rescale": float(self.cfg_rescale_var.get()),
1273
+ "noise_schedule": noise_schedule,
1274
+ }
1275
+
1276
+ data = {
1277
+ "input": main_prompt,
1278
+ "model": model_name,
1279
+ "action": "generate",
1280
+ "parameters": parameters
1281
+ }
1282
+
1283
+ # V4 ํŠนํ™” ์„ค์ •
1284
+ if 'nai-diffusion-4' in model_name:
1285
+ data['parameters'].update({
1286
+ 'params_version': 3,
1287
+ 'add_original_image': True,
1288
+ 'legacy': False,
1289
+ 'legacy_uc': False,
1290
+ 'autoSmea': True,
1291
+ 'prefer_brownian': True,
1292
+ 'ucPreset': 0,
1293
+ 'use_coords': False,
1294
+ 'v4_negative_prompt': {'caption': {'base_caption': negative_prompt, 'char_captions': []}, 'legacy_uc': False},
1295
+ 'v4_prompt': {'caption': {'base_caption': main_prompt, 'char_captions': []}, 'use_coords': False, 'use_order': True}
1296
+ })
1297
+
1298
+ # ํ™œ์„ฑํ™”๋œ ์บ๋ฆญํ„ฐ ํ”„๋กฌํ”„ํŠธ ์ฒ˜๋ฆฌ
1299
+ active_chars = [p for p in self.character_prompts if p['check_var'].get()]
1300
+ if active_chars:
1301
+ for char_info in active_chars:
1302
+ char_prompt = char_info['prompt_widget'].get(1.0, tk.END).strip()
1303
+ char_uc = char_info['uc_widget'].get(1.0, tk.END).strip()
1304
+
1305
+ char_v4_prompt = {'char_caption': char_prompt, 'centers': [{'x': 0.5, 'y': 0.5}]}
1306
+ char_v4_uc = {'char_caption': char_uc, 'centers': [{'x': 0.5, 'y': 0.5}]}
1307
+
1308
+ data['parameters']['v4_prompt']['caption']['char_captions'].append(char_v4_prompt)
1309
+ data['parameters']['v4_negative_prompt']['caption']['char_captions'].append(char_v4_uc)
1310
+
1311
+ # 3. API ์š”์ฒญ
1312
+ import requests
1313
+ response = requests.post(
1314
+ "https://image.novelai.net/ai/generate-image",
1315
+ headers={"Authorization": f"Bearer {access_token}"},
1316
+ json=data,
1317
+ timeout=180
1318
+ )
1319
+ response.raise_for_status()
1320
+
1321
+ # 4. ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ
1322
+ image = self._process_nai_response(response.content)
1323
+ self.root.after(0, self._update_generation_result, image)
1324
+
1325
+ except Exception as e:
1326
+ self.root.after(0, self._update_generation_result, e)
1327
+
1328
+ def _process_nai_response(self, content):
1329
+ """NAI ์‘๋‹ต(zip)์„ ์ฒ˜๋ฆฌํ•˜์—ฌ PIL Image ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
1330
+ import zipfile
1331
+ try:
1332
+ zipped = zipfile.ZipFile(io.BytesIO(content))
1333
+ image_bytes = zipped.read(zipped.infolist()[0])
1334
+ image = Image.open(io.BytesIO(image_bytes))
1335
+ return image
1336
+ except Exception as e:
1337
+ raise Exception(f"์‘๋‹ต ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹คํŒจ: {e}")
1338
+
1339
+ def start_generation(self):
1340
+ # ... (๊ธฐ์กด ๊ฒ€์ฆ ๋กœ์ง) ...
1341
+ self.is_generating = True
1342
+ self.generation_start_time = time.time()
1343
+ self.generation_btn.config(state=tk.DISABLED)
1344
+ self.save_btn.config(state=tk.NORMAL) # <<<< ์ƒ์„ฑ ์‹œ์ž‘ ์‹œ ์ €์žฅ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
1345
+ self.generation_image_label.config(image=None, text="๐ŸŽจ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘...")
1346
+ self._update_timer()
1347
+
1348
+ thread = threading.Thread(target=self._generation_thread)
1349
+ thread.daemon = True
1350
+ thread.start()
1351
+
1352
+ def _update_generation_result(self, result):
1353
+ self.is_generating = False
1354
+ self.generation_btn.config(state=tk.NORMAL, text="Generation")
1355
+
1356
+ if isinstance(result, Image.Image):
1357
+ self.last_generated_image = result # ์›๋ณธ ์ด๋ฏธ์ง€ ๋ณด๊ด€
1358
+ self.last_generated_image.save(self.temp_image_path, "PNG") # ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
1359
+
1360
+ if self.auto_save_var.get():
1361
+ try:
1362
+ save_dir = os.path.join("Output_WD14", self.save_session_folder)
1363
+ os.makedirs(save_dir, exist_ok=True)
1364
+ file_path = os.path.join(save_dir, f"{self.save_counter:05d}.png")
1365
+ self.last_generated_image.save(file_path, "PNG")
1366
+ self.save_counter += 1
1367
+ except Exception as e:
1368
+ print(f"์ž๋™ ์ €์žฅ ์‹คํŒจ: {e}")
1369
+
1370
+ thumbnail = self._create_thumbnail(result)
1371
+ photo = ImageTk.PhotoImage(thumbnail)
1372
+ self.generation_image_label.config(image=photo, text="")
1373
+ self.generation_image_label.image = photo
1374
+
1375
+ elif isinstance(result, Exception):
1376
+ self.show_generation_preview()
1377
+ messagebox.showerror("์ƒ์„ฑ ์‹คํŒจ", f"์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค:\n{result}")
1378
+
1379
+ def _show_image_with_pil(self):
1380
+ """PIL ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋งˆ์ง€๋ง‰์œผ๋กœ ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ๊ธฐ๋ณธ ์ด๋ฏธ์ง€ ๋ทฐ์–ด๋กœ ์—ฝ๋‹ˆ๋‹ค."""
1381
+ if self.last_generated_image:
1382
+ try:
1383
+ self.last_generated_image.show()
1384
+ except Exception as e:
1385
+ messagebox.showerror("์˜ค๋ฅ˜", f"์ด๋ฏธ์ง€๋ฅผ ์—ฌ๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {e}")
1386
+ else:
1387
+ messagebox.showinfo("์ •๋ณด", "ํ‘œ์‹œํ•  ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
1388
+
1389
+ def main():
1390
+ try:
1391
+ from tkinterdnd2 import TkinterDnD
1392
+ root = TkinterDnD.Tk()
1393
+ except ImportError:
1394
+ print("tkinterdnd2๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์•„ Drag & Drop ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.")
1395
+ root = tk.Tk()
1396
+
1397
+ app = WD14TaggerGUI(root)
1398
+ root.mainloop()
1399
+
1400
+ if __name__ == "__main__":
1401
+ main()